Repository navigation
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·392 lines (355 loc) · 16.1 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·392 lines (355 loc) · 16.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
#!/usr/bin/env bash
# myworktree one-line installer.
#
# Usage:
# curl -fsSL https://raw.githubusercontent.com/linletian/myworktree/main/scripts/install.sh | bash
# curl -fsSL .../install.sh | bash -s -- -v v0.5.2
# curl -fsSL .../install.sh | bash -s -- -v vX.Y.Z-beta.N # pin a beta
# curl -fsSL .../install.sh | bash -s -- --beta # latest beta
# curl -fsSL .../install.sh | bash -s -- --no-modify-path
# INSTALL_ALIAS=mwt bash install.sh # avoid `mw` name clash
#
# Env vars:
# INSTALL_DIR=<dir> target directory (default: ~/.local/bin)
# INSTALL_ALIAS=<name> short alias command (default: mw; empty to skip)
# MYWORKTREE_VERSION=<v> pin version (default: latest release)
# MYWORKTREE_BETA=1 install the latest beta (prerelease) instead
# NO_MODIFY_PATH=1 do not modify shell rc files
set -euo pipefail
REPO="linletian/myworktree"
APP="myworktree"
MUTED='\033[0;2m'
RED='\033[0;31m'
NC='\033[0m'
# Defaults; users can override via env or flags.
INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}"
# INSTALL_ALIAS: default "mw" when the user did NOT set the env var at all;
# honour any explicit value (including the empty string, which means "skip
# the alias"). The ${VAR+set} form distinguishes "unset" from "set to empty".
DEFAULT_ALIAS=mw
if [ -z "${INSTALL_ALIAS+set}" ]; then
INSTALL_ALIAS="$DEFAULT_ALIAS"
INSTALL_ALIAS_SET=0
else
INSTALL_ALIAS_SET=1
fi
requested_version="${MYWORKTREE_VERSION:-}"
want_beta="${MYWORKTREE_BETA:-false}"
no_modify_path="${NO_MODIFY_PATH:-false}"
binary_path=""
usage() {
cat <<EOF
myworktree installer
Usage: install.sh [options]
Options:
-h, --help show this help
-v, --version <ver> install a specific version (e.g., v0.5.2, 0.5.1,
or a beta like vX.Y.Z-beta.N)
--beta install the latest beta (prerelease) instead of
the latest stable release
--no-modify-path do not modify shell rc files
Env vars:
INSTALL_DIR=<dir> target directory (default: ~/.local/bin)
INSTALL_ALIAS=<name> short alias command (default: mw; empty to skip)
MYWORKTREE_VERSION=<v> pin version (default: latest release)
MYWORKTREE_BETA=1 same as --beta
NO_MODIFY_PATH=1 same as --no-modify-path
Examples:
curl -fsSL https://raw.githubusercontent.com/${REPO}/main/scripts/install.sh | bash
curl -fsSL .../install.sh | bash -s -- -v v0.5.2
INSTALL_ALIAS=mwt bash install.sh # avoid conflict with the Debian/Ubuntu 'mw' package
INSTALL_DIR=~/bin bash install.sh # install into a custom dir
EOF
}
die() { echo -e "${RED}Error:${NC} $*" >&2; exit 1; }
info() { echo -e "${MUTED}$*${NC}"; }
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-v|--version)
[[ -n "${2:-}" ]] || die "--version requires a value"
requested_version="$2"; shift 2 ;;
--beta) want_beta=true; shift ;;
--no-modify-path) no_modify_path=true; shift ;;
*) info "Warning: unknown option '$1' (ignored)"; shift ;;
esac
done
# Accept the documented truthy spellings for NO_MODIFY_PATH / MYWORKTREE_BETA
# (the docs say NO_MODIFY_PATH=1; previously only the literal "true" worked).
case "$no_modify_path" in 1|true|yes) no_modify_path=true ;; *) no_modify_path=false ;; esac
case "$want_beta" in 1|true|yes) want_beta=true ;; *) want_beta=false ;; esac
# --- platform detection ---------------------------------------------------
case "$(uname -s)" in
Darwin*) platform="macOS" ;;
Linux*) platform="Linux" ;;
*) die "Unsupported OS: $(uname -s) (only macOS and Linux are supported)" ;;
esac
case "$(uname -m)" in
arm64|aarch64) goarch="arm64" ;;
x86_64|amd64) goarch="amd64" ;;
*) die "Unsupported architecture: $(uname -m)" ;;
esac
# Rosetta 2: x86_64 binary on Apple Silicon → prefer arm64
if [ "$platform" = "macOS" ] && [ "$goarch" = "amd64" ]; then
translated=$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)
if [ "$translated" = "1" ]; then
info "Detected Rosetta x86_64 on Apple Silicon; using arm64 build"
goarch="arm64"
fi
fi
info "Platform: ${platform}/${goarch}"
# --- version resolution --------------------------------------------------
if [ -n "$requested_version" ] && [ "$want_beta" = "true" ]; then
die "--beta and --version are mutually exclusive (a pinned version can already be a beta tag, e.g. -v vX.Y.Z-beta.N)"
fi
if [ -n "$requested_version" ]; then
version="${requested_version#v}"
tag="v${version}"
elif [ "$want_beta" = "true" ]; then
info "Resolving latest beta version from GitHub..."
# /releases/latest never points at prereleases, so resolve from the
# releases atom feed (newest-first, includes prereleases, no API rate
# limit); entry ids look like
# <id>tag:github.com,2008:Repository/<id>/vX.Y.Z-beta.N</id>
# Fall back to the API if the feed yields nothing.
tag=$(curl -fsSL "https://github.com/${REPO}/releases.atom" \
| sed -n 's#.*<id>tag:github.com,2008:Repository/[0-9][0-9]*/\(v[^<]*-beta\.[0-9][0-9]*\)</id>.*#\1#p' \
| head -1)
if [ -z "$tag" ]; then
tag=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases?per_page=30" 2>/dev/null \
| sed -n 's/.*"tag_name": *"\(v[^"]*-beta\.[0-9][0-9]*\)".*/\1/p' \
| head -1 || true)
fi
[ -n "$tag" ] || die "Failed to resolve a beta version from GitHub.
No published beta (prerelease) found, or GitHub was unreachable.
Pin one explicitly instead: bash install.sh -v vX.Y.Z-beta.N
(see https://github.com/${REPO}/releases for the available tags)"
version="${tag#v}"
else
info "Resolving latest version from GitHub..."
# /releases/latest redirects to /releases/tag/<tag>; capture the redirect target
# to avoid the GitHub API rate limit and JSON parsing.
tag=$(curl -fsSLI -o /dev/null -w '%{url_effective}\n' \
"https://github.com/${REPO}/releases/latest" \
| sed -n 's#.*/tag/\(v[^/?]*\).*#\1#p' | head -1)
[ -n "$tag" ] || die "Failed to resolve latest version from GitHub"
version="${tag#v}"
fi
info "Version: ${version} (${tag})"
# --- download + verify ---------------------------------------------------
tmp_dir=$(mktemp -d -t myworktree-install-XXXXXX)
trap 'rm -rf "$tmp_dir"' EXIT
archive="${APP}_v${version}_${platform}_${goarch}.tar.gz"
base_url="https://github.com/${REPO}/releases/download/${tag}"
info "Downloading ${archive}..."
curl -fsSL -o "${tmp_dir}/${archive}" "${base_url}/${archive}" \
|| die "Download failed: ${base_url}/${archive}
Is release ${tag} published? (https://github.com/${REPO}/releases/tag/${tag})"
info "Downloading checksums.txt..."
curl -fsSL -o "${tmp_dir}/checksums.txt" "${base_url}/checksums.txt" \
|| die "Download failed: ${base_url}/checksums.txt"
info "Verifying SHA256..."
(cd "${tmp_dir}" && shasum -a 256 -c checksums.txt --ignore-missing) \
|| die "Checksum verification failed"
info "Extracting..."
tar -xzf "${tmp_dir}/${archive}" -C "${tmp_dir}"
extracted_dir=$(find "${tmp_dir}" -maxdepth 1 -mindepth 1 -type d \
-name "${APP}_v*" | head -1)
[ -n "$extracted_dir" ] || die "Could not find extracted directory in archive"
# --- existing-binary conflict detection ---------------------------------
# Refuse to overwrite if the destination exists and is NOT a myworktree binary.
# (mw is a 2-letter name; Debian/Ubuntu's `mw` package is a real clash.)
# Detection ladder, in order:
# 1) `<path> --version` output mentions the string "myworktree" (cheap, fast)
# 2) `<path> --version` output looks like the myworktree version line
# (`<prog> vX.Y.Z (<commit>) built <date>`) — the prog name may be
# "mw" (symlink), "myworktree", or anything else the user renamed,
# so the text alone doesn't include "myworktree"
# 3) `strings <path>` contains "myworktree" — catches cases where the
# binary is a valid myworktree build but --version is broken (rare)
check_existing() {
local path="$1" label="$2"
[ ! -e "$path" ] && return 0
local out
out=$("$path" --version 2>/dev/null || echo "")
if echo "$out" | grep -qi "$APP"; then
info "Existing ${label} is an older myworktree (matched 'myworktree' in --version); will upgrade in place"
return 0
fi
if [ -n "$out" ] && echo "$out" | grep -Eq '^[^ ]+ +v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?( \([^)]+\))?( built [^ ]+)?$'; then
# matches "<prog> vX.Y.Z[-prerelease] [(commit)] [built <date>]" — the myworktree format
info "Existing ${label} looks like a myworktree build (--version: ${out}); will upgrade in place"
return 0
fi
if command -v strings >/dev/null 2>&1 && strings "$path" 2>/dev/null | grep -q "$APP"; then
info "Existing ${label} is a myworktree build (matched 'myworktree' via strings); will upgrade in place"
return 0
fi
die "${path} exists and is NOT myworktree (got: ${out:-non-myworktree binary}).
Resolve by one of:
1) Remove it: rm '${path}'
2) Different alias: INSTALL_ALIAS=mwt bash install.sh
3) Dedicated dir: INSTALL_DIR=\$HOME/.myworktree/bin bash install.sh
(then 'export PATH=\$HOME/.myworktree/bin:\$PATH')"
}
# --- install -------------------------------------------------------------
mkdir -p "${INSTALL_DIR}"
check_existing "${INSTALL_DIR}/${APP}" "${APP}"
[ -n "$INSTALL_ALIAS" ] && check_existing "${INSTALL_DIR}/${INSTALL_ALIAS}" "$INSTALL_ALIAS"
info "Installing to ${INSTALL_DIR}/"
install -m 755 "${extracted_dir}/${APP}" "${INSTALL_DIR}/${APP}"
if [ -n "$INSTALL_ALIAS" ] && [ "$INSTALL_ALIAS" != "$APP" ]; then
ln -sf "${INSTALL_DIR}/${APP}" "${INSTALL_DIR}/${INSTALL_ALIAS}"
fi
# --- PATH handling -------------------------------------------------------
add_export_line() {
local config_file="$1" line="$2"
[ -f "$config_file" ] || return 1
if grep -Fxq "$line" "$config_file" 2>/dev/null; then
return 0
fi
if [[ -w "$config_file" ]]; then
printf '\n# myworktree\n%s\n' "$line" >> "$config_file"
info "Added ${INSTALL_DIR} to PATH in ${config_file}"
return 0
fi
return 1
}
if [ "$no_modify_path" != "true" ]; then
case ":$PATH:" in
*":${INSTALL_DIR}:"*) : ;; # already on PATH
*)
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
shell_name=$(basename "${SHELL:-sh}")
case "$shell_name" in
fish)
config="${XDG_CONFIG_HOME}/fish/config.fish"
mkdir -p "$(dirname "$config")"
[ -f "$config" ] || touch "$config"
if ! grep -Fxq "fish_add_path ${INSTALL_DIR}" "$config"; then
echo "fish_add_path ${INSTALL_DIR}" >> "$config"
info "Added ${INSTALL_DIR} to PATH in ${config}"
fi
;;
zsh|bash|sh|ash)
config=""
for f in "${ZDOTDIR:-$HOME}/.${shell_name}rc" \
"${ZDOTDIR:-$HOME}/.zshenv" \
"$HOME/.${shell_name}rc" \
"$HOME/.bash_profile" \
"$HOME/.profile"; do
[ -f "$f" ] && config="$f" && break
done
[ -n "$config" ] || config="$HOME/.profile"
add_export_line "$config" "export PATH=\"${INSTALL_DIR}:\$PATH\"" \
|| info "Could not write to ${config}; add to PATH manually:
export PATH=\"${INSTALL_DIR}:\$PATH\""
;;
*)
info "Add ${INSTALL_DIR} to your PATH manually:
export PATH=\"${INSTALL_DIR}:\$PATH\""
;;
esac
;;
esac
fi
# --- GitHub Actions integration -----------------------------------------
if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" = "true" ]; then
echo "${INSTALL_DIR}" >> "$GITHUB_PATH"
info "Added ${INSTALL_DIR} to \$GITHUB_PATH"
fi
# --- macOS Gatekeeper hint ----------------------------------------------
if [ "$platform" = "macOS" ]; then
echo ""
info "macOS may quarantine the downloaded binary (Gatekeeper).
If 'myworktree' does not respond or shows 'cannot be opened':
xattr -d com.apple.quarantine ${INSTALL_DIR}/myworktree"
fi
# --- final message -------------------------------------------------------
HAS_ALIAS=false
if [ -n "$INSTALL_ALIAS" ] && [ "$INSTALL_ALIAS" != "$APP" ] && [ -L "${INSTALL_DIR}/${INSTALL_ALIAS}" ]; then
HAS_ALIAS=true
fi
echo ""
echo -e "${MUTED}myworktree ${version} installed:${NC}"
echo " ${APP} → ${INSTALL_DIR}/${APP}"
$HAS_ALIAS && echo " ${INSTALL_ALIAS} → ${INSTALL_DIR}/${INSTALL_ALIAS} (symlink → ${APP})"
echo ""
echo -e "${MUTED}Next steps:${NC}"
echo " cd <your-git-repo> # any git worktree you want to drive"
if $HAS_ALIAS; then
echo " ${APP} # start the daemon (or use '${INSTALL_ALIAS}')"
else
echo " ${APP} # start the daemon"
fi
echo " ${APP} --version # verify install"
echo ""
if $HAS_ALIAS; then
echo -e "${MUTED}Both '${APP}' and '${INSTALL_ALIAS}' work — pick whichever you type faster.${NC}"
fi
# Explain the alias-skip case so the user isn't left guessing.
if [ "$INSTALL_ALIAS_SET" = "1" ] && [ -z "$INSTALL_ALIAS" ]; then
echo -e "${MUTED}Note:${NC} the '${DEFAULT_ALIAS}' alias was NOT installed (you set INSTALL_ALIAS=)."
echo "Only '${APP}' is installed. To get an alias under a different name:"
echo " INSTALL_ALIAS=mwt bash install.sh"
echo ""
fi
echo -e "${MUTED}If 'command not found', open a new shell (PATH was added to your rc).${NC}"
# --- running-daemon warning --------------------------------------------
# The new binary is on disk; an already-running daemon is still on the OLD
# binary (in-memory code page is unaffected by the file replacement — the
# kernel keeps the old inode until the process exits). Restart the daemon
# to pick up the new binary. We do NOT auto-stop: that could hard-kill
# long-running PTY / opencode / reasonix sessions and discard the
# in-memory ring buffer + WebSocket state.
#
# Detection must check BOTH the binary name and the alias: the process
# comm is the basename of the path the daemon was LAUNCHED through, so a
# daemon started via the alias symlink (the common case — `mw` / `mw
# start`) reports as "mw" and pgrep -x myworktree alone never sees it.
daemon_pids=""
if command -v pgrep >/dev/null 2>&1; then
check_names="$APP"
if [ -n "$INSTALL_ALIAS" ] && [ "$INSTALL_ALIAS" != "$APP" ]; then
check_names="$check_names $INSTALL_ALIAS"
fi
for proc_name in $check_names; do
hits=$(pgrep -x "$proc_name" 2>/dev/null || true)
if [ -n "$hits" ]; then
daemon_pids="${daemon_pids}${daemon_pids:+ }$(echo $hits | tr '\n' ' ')"
fi
done
daemon_pids=$(echo $daemon_pids | tr ' ' '\n' | sort -un | tr '\n' ' ')
fi
if [ -n "${daemon_pids// /}" ]; then
pids=$daemon_pids
echo ""
echo -e "${RED}Heads up:${NC} a myworktree daemon is currently running (PID(s): ${pids})."
echo "The installed binary (${version}) is on disk but the running daemon"
echo "is still using the OLD binary. To pick up the new one, save your"
echo "work and restart the daemon (steps below)."
echo ""
echo -e "${MUTED}Before you restart:${NC}"
echo " - Save anything in the Web UI you haven't sent yet (reasonix chat"
echo " drafts, new-session prompts, in-page text). Restarting closes"
echo " the WebSocket; the in-memory ring buffer for PTY sessions is"
echo " also discarded."
echo " - Decide whether long-running instances can keep going. '${APP} stop'"
echo " asks each running kind to wind down gracefully (PTY/exec sessions"
echo " receive a stop signal; reasonix / opencode serve get a clean"
echo " shutdown). 'pkill -x myworktree' does NOT — it kills the daemon"
echo " immediately and any subprocess the kind had spawned (e.g. an"
echo " opencode serve) is orphaned until you kill it by hand."
echo ""
echo -e "${MUTED}Restart:${NC}"
echo " ${APP} stop # graceful: stops instances, then exits"
echo " ${APP} start # spawns a new daemon on the new binary"
echo " (in any worktree — the daemon does not need to be inside the repo)"
echo ""
echo -e "${MUTED}Force fallback (only if '${APP} stop' hangs):${NC}"
echo " pkill -x ${APP} # then: kill any orphaned children"
if [ -n "$INSTALL_ALIAS" ] && [ "$INSTALL_ALIAS" != "$APP" ]; then
echo " pkill -x ${INSTALL_ALIAS} # (if you started the daemon via the alias)"
fi
echo " pkill -x opencode # e.g. opencode serve, if any"
fi