Skip to content

Commit 2faa2fe

Browse files
committed
feat: change install location
1 parent cb86792 commit 2faa2fe

5 files changed

Lines changed: 287 additions & 45 deletions

File tree

‎.changeset/wild-experts-chew.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ralph": patch
3+
---
4+
5+
Change install directory and add migration util

‎scripts/install.sh‎

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
set -e
33

44
REPO="nitodeco/ralph"
5-
INSTALL_DIR="/usr/local/bin"
65
BINARY_NAME="ralph"
6+
INSTALL_DIR="${RALPH_INSTALL_DIR:-${HOME}/.local/bin}"
77

88
get_arch() {
99
local arch
@@ -44,6 +44,60 @@ get_latest_version() {
4444
curl -sL "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'
4545
}
4646

47+
get_shell_config_path() {
48+
local shell_name
49+
shell_name=$(basename "$SHELL")
50+
51+
case "$shell_name" in
52+
zsh)
53+
echo "${HOME}/.zshrc"
54+
;;
55+
bash)
56+
if [ -f "${HOME}/.bash_profile" ]; then
57+
echo "${HOME}/.bash_profile"
58+
else
59+
echo "${HOME}/.bashrc"
60+
fi
61+
;;
62+
*)
63+
echo ""
64+
;;
65+
esac
66+
}
67+
68+
configure_path() {
69+
local install_dir="$1"
70+
local config_path
71+
config_path=$(get_shell_config_path)
72+
73+
if [ -z "$config_path" ]; then
74+
echo ""
75+
echo "Could not detect shell configuration file."
76+
echo "Please add the following to your shell configuration manually:"
77+
echo ""
78+
echo " export PATH=\"${install_dir}:\$PATH\""
79+
echo ""
80+
return
81+
fi
82+
83+
local export_line="export PATH=\"${install_dir}:\$PATH\""
84+
local marker_comment="# Added by Ralph CLI"
85+
86+
if [ -f "$config_path" ]; then
87+
if grep -q "$marker_comment" "$config_path" || grep -q "$export_line" "$config_path"; then
88+
return
89+
fi
90+
fi
91+
92+
echo "" >> "$config_path"
93+
echo "$marker_comment" >> "$config_path"
94+
echo "$export_line" >> "$config_path"
95+
96+
echo ""
97+
echo "PATH configured in ${config_path}"
98+
echo "Run 'source ${config_path}' or restart your terminal to apply changes."
99+
}
100+
47101
main() {
48102
echo "Installing Ralph CLI..."
49103
echo ""
@@ -62,6 +116,7 @@ main() {
62116
echo " OS: $os"
63117
echo " Architecture: $arch"
64118
echo " Version: $version"
119+
echo " Install directory: $INSTALL_DIR"
65120
echo ""
66121

67122
download_url="https://github.com/${REPO}/releases/download/${version}/ralph-${os}-${arch}"
@@ -74,20 +129,15 @@ main() {
74129

75130
chmod +x "$binary_path"
76131

77-
echo "Installing to ${INSTALL_DIR}/${BINARY_NAME}..."
78-
79-
if [ -w "$INSTALL_DIR" ]; then
80-
mv "$binary_path" "${INSTALL_DIR}/${BINARY_NAME}"
81-
else
82-
echo "Requesting sudo access to install to ${INSTALL_DIR}..."
83-
sudo mv "$binary_path" "${INSTALL_DIR}/${BINARY_NAME}"
84-
fi
132+
mkdir -p "$INSTALL_DIR"
133+
mv "$binary_path" "${INSTALL_DIR}/${BINARY_NAME}"
85134

86135
rm -rf "$tmp_dir"
87136

88137
echo ""
89138
echo "Ralph CLI installed successfully!"
90-
echo ""
139+
140+
configure_path "$INSTALL_DIR"
91141

92142
"${INSTALL_DIR}/${BINARY_NAME}" setup
93143
}

‎src/components/UpdatePrompt.tsx‎

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import {
77
downloadBinary,
88
fetchLatestVersion,
99
getArchitecture,
10-
getBinaryPath,
1110
getOperatingSystem,
12-
installBinary,
11+
getRemoveOldBinaryCommand,
12+
installWithMigration,
13+
type MigrationResult,
1314
restartApplication,
1415
} from "@/lib/update.ts";
1516
import { Message } from "./common/Message.tsx";
@@ -30,6 +31,7 @@ type UpdateState =
3031
| "downloading"
3132
| "installing"
3233
| "complete"
34+
| "migrated"
3335
| "error";
3436

3537
type UpdateAction = "update" | "remind" | "skip";
@@ -66,6 +68,7 @@ export function UpdatePrompt({
6668
const [downloadedBytes, setDownloadedBytes] = useState<number>(0);
6769
const [totalBytes, setTotalBytes] = useState<number>(0);
6870
const [updatePerformed, setUpdatePerformed] = useState<boolean>(false);
71+
const [migrationResult, setMigrationResult] = useState<MigrationResult | null>(null);
6972

7073
useEffect(() => {
7174
const checkForUpdates = async () => {
@@ -82,13 +85,15 @@ export function UpdatePrompt({
8285
if (comparison <= 0) {
8386
const currentConfig = loadConfig();
8487
currentConfig.skipVersion = undefined;
88+
8589
saveConfig(currentConfig);
8690
setState("up_to_date");
8791
} else {
8892
setState("update_available");
8993
}
9094
} catch (err) {
9195
const errorMessage = err instanceof Error ? err.message : String(err);
96+
9297
setError(errorMessage);
9398
setState("error");
9499
}
@@ -101,15 +106,19 @@ export function UpdatePrompt({
101106
switch (item.value) {
102107
case "update":
103108
await performUpdate();
109+
104110
break;
105111
case "skip":
106112
if (latestVersion) {
107113
skipVersion(latestVersion);
108114
}
115+
109116
setState("complete");
117+
110118
break;
111119
case "remind":
112120
setState("complete");
121+
113122
break;
114123
}
115124
};
@@ -135,11 +144,16 @@ export function UpdatePrompt({
135144
);
136145

137146
setState("installing");
138-
const targetPath = getBinaryPath();
139-
await installBinary(binaryData, targetPath);
147+
const result = await installWithMigration(binaryData);
148+
setMigrationResult(result);
140149

141150
setUpdatePerformed(true);
142-
setState("complete");
151+
152+
if (result.migrated) {
153+
setState("migrated");
154+
} else {
155+
setState("complete");
156+
}
143157
} catch (err) {
144158
const errorMessage = err instanceof Error ? err.message : String(err);
145159
setError(errorMessage);
@@ -150,15 +164,18 @@ export function UpdatePrompt({
150164
useEffect(() => {
151165
if (state === "complete" || state === "up_to_date" || state === "error") {
152166
const timeout = setTimeout(() => {
153-
if (state === "complete" && updatePerformed) {
167+
if (state === "complete" && updatePerformed && migrationResult) {
168+
restartApplication(migrationResult.newPath);
169+
} else if (state === "complete" && updatePerformed) {
154170
restartApplication();
155171
} else {
156172
handleExit();
157173
}
158174
}, 2000);
175+
159176
return () => clearTimeout(timeout);
160177
}
161-
}, [state, handleExit, updatePerformed]);
178+
}, [state, handleExit, updatePerformed, migrationResult]);
162179

163180
const renderContent = () => {
164181
switch (state) {
@@ -207,6 +224,40 @@ export function UpdatePrompt({
207224
case "installing":
208225
return <Spinner label="Installing..." />;
209226

227+
case "migrated":
228+
return (
229+
<Box flexDirection="column" gap={1}>
230+
<Message type="success">Ralph updated successfully to {latestVersion}!</Message>
231+
<Box flexDirection="column" marginTop={1}>
232+
<Text color="yellow" bold>
233+
Installation location has changed
234+
</Text>
235+
<Text>Ralph has been moved to: {migrationResult?.newPath}</Text>
236+
</Box>
237+
{migrationResult?.shellConfigPath && (
238+
<Box flexDirection="column" marginTop={1}>
239+
<Text>PATH has been updated in: {migrationResult.shellConfigPath}</Text>
240+
<Text dimColor>
241+
Restart your terminal or run: source {migrationResult.shellConfigPath}
242+
</Text>
243+
</Box>
244+
)}
245+
{migrationResult?.oldPath && (
246+
<Box flexDirection="column" marginTop={1}>
247+
<Text color="yellow">Please remove the old installation:</Text>
248+
<Box marginTop={1}>
249+
<Text color="cyan" bold>
250+
{getRemoveOldBinaryCommand(migrationResult.oldPath)}
251+
</Text>
252+
</Box>
253+
</Box>
254+
)}
255+
<Box marginTop={1}>
256+
<Text dimColor>Press Ctrl+C to exit, then restart your terminal.</Text>
257+
</Box>
258+
</Box>
259+
);
260+
210261
case "complete":
211262
return (
212263
<Box flexDirection="column" gap={1}>

‎src/lib/paths.ts‎

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,87 @@
1-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
1+
import {
2+
accessSync,
3+
appendFileSync,
4+
constants,
5+
existsSync,
6+
mkdirSync,
7+
readFileSync,
8+
writeFileSync,
9+
} from "node:fs";
210
import { homedir } from "node:os";
311
import { join } from "node:path";
412

513
export const RALPH_DIR = ".ralph";
614
export const GLOBAL_RALPH_DIR = join(homedir(), ".ralph");
15+
export const LOCAL_BIN_DIR = join(homedir(), ".local", "bin");
16+
export const SYSTEM_BIN_DIR = "/usr/local/bin";
17+
18+
export function isDirectoryWritable(directory: string): boolean {
19+
try {
20+
accessSync(directory, constants.W_OK);
21+
22+
return true;
23+
} catch {
24+
return false;
25+
}
26+
}
27+
28+
export function isDirectoryInPath(directory: string): boolean {
29+
const pathEnv = process.env.PATH || "";
30+
const paths = pathEnv.split(":");
31+
return paths.includes(directory);
32+
}
33+
34+
export function needsMigration(currentBinaryPath: string): boolean {
35+
return currentBinaryPath.startsWith(SYSTEM_BIN_DIR) && !isDirectoryWritable(SYSTEM_BIN_DIR);
36+
}
37+
38+
export function getShellConfigPath(): string | null {
39+
const shell = process.env.SHELL || "";
40+
const shellName = shell.split("/").pop() || "";
41+
const home = homedir();
42+
43+
switch (shellName) {
44+
case "zsh":
45+
return join(home, ".zshrc");
46+
case "bash":
47+
if (existsSync(join(home, ".bash_profile"))) {
48+
return join(home, ".bash_profile");
49+
}
50+
return join(home, ".bashrc");
51+
default:
52+
return null;
53+
}
54+
}
55+
56+
export function prependToShellConfig(): string | null {
57+
const configPath = getShellConfigPath();
58+
if (!configPath) {
59+
return null;
60+
}
61+
62+
const exportLine = `export PATH="${LOCAL_BIN_DIR}:$PATH"`;
63+
const markerComment = "# Added by Ralph CLI";
64+
const fullEntry = `\n${markerComment}\n${exportLine}\n`;
65+
66+
if (existsSync(configPath)) {
67+
const content = readFileSync(configPath, "utf-8");
68+
if (content.includes(exportLine) || content.includes(markerComment)) {
69+
return configPath;
70+
}
71+
}
72+
73+
appendFileSync(configPath, fullEntry, "utf-8");
74+
return configPath;
75+
}
76+
77+
export function getDefaultInstallDir(): string {
78+
const envOverride = process.env.RALPH_INSTALL_DIR;
79+
if (envOverride) {
80+
return envOverride;
81+
}
82+
83+
return LOCAL_BIN_DIR;
84+
}
785

886
export function ensureRalphDirExists(): void {
987
if (!existsSync(RALPH_DIR)) {

0 commit comments

Comments
 (0)