-
Notifications
You must be signed in to change notification settings - Fork 19
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·360 lines (320 loc) · 13.8 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·360 lines (320 loc) · 13.8 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
#!/bin/bash
set -Eeuo pipefail
# ── VoxTerm Installer ──────────────────────────────────────
#
# Install: curl -fsSL https://github.com/dmarzzz/VoxTerm/releases/latest/download/install.sh | bash
# Specific: curl ... | bash -s -- --version v0.1.0
# Uninstall: curl ... | bash -s -- --uninstall
#
# This URL is served with `Cache-Control: no-cache` at every hop, so it
# always resolves to the latest release's install.sh — no CDN/proxy
# staleness, no manual cache-busting needed. The old raw.githubusercontent
# URL still works but can be cached by intermediaries (corp/school proxies,
# some ISPs), which has bitten us in the wild.
# Installer revision — bump on every edit to this file. Printed at startup
# so users can confirm they aren't running a stale copy.
INSTALLER_REV="2026-05-16.3"
REPO="dmarzzz/VoxTerm"
REPO_URL="https://github.com/$REPO"
INSTALL_URL="https://github.com/$REPO/releases/latest/download/install.sh"
INSTALL_DIR="$HOME/.local/share/voxterm"
BIN_DIR="$HOME/.local/bin"
VENV_DIR="$INSTALL_DIR/.venv"
VERSION_FILE="$INSTALL_DIR/.installed-version"
# Colors
GREEN='\033[0;32m'
CYAN='\033[0;36m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
DIM='\033[2m'
BOLD='\033[1m'
RESET='\033[0m'
info() { echo -e "${CYAN}▸${RESET} $1"; }
done_() { echo -e "${GREEN}✓${RESET} $1"; }
dim() { echo -e "${DIM} $1${RESET}"; }
warn() { echo -e "${YELLOW}!${RESET} $1"; }
err() { echo -e "${RED}✗${RESET} $1"; }
# Failure trap — fires on any unhandled non-zero exit thanks to `set -Eeuo`.
on_err() {
local exit_code=$? line=$1 cmd=${2:-?}
err "installer failed (exit $exit_code) at line $line"
dim "command: $cmd"
dim "installer rev: $INSTALLER_REV"
dim "install dir: $INSTALL_DIR"
dim "report this with the full output: $REPO_URL/issues"
}
trap 'on_err $LINENO "$BASH_COMMAND"' ERR
# Hardened curl: retry transient network failures.
fetch() { curl -fsSL --retry 3 --retry-delay 2 --retry-connrefused "$@"; }
# Returns 0 if $1 is a Python >= 3.12.
py_meets_req() {
local v ma mi
v=$("$1" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>/dev/null || echo "0.0")
ma=${v%%.*}; mi=${v#*.}
[ "$ma" -gt 3 ] || { [ "$ma" -eq 3 ] && [ "$mi" -ge 12 ]; }
}
# ── Parse args ────────────────────────────────────────────
REQUESTED_VERSION=""
UNINSTALL=false
while [[ $# -gt 0 ]]; do
case "$1" in
--version) REQUESTED_VERSION="$2"; shift 2 ;;
--version=*) REQUESTED_VERSION="${1#*=}"; shift ;;
--uninstall) UNINSTALL=true; shift ;;
--help|-h)
echo "VoxTerm installer (rev $INSTALLER_REV)"
echo ""
echo "Usage: curl -fsSL .../install.sh | bash [-s -- OPTIONS]"
echo ""
echo "Options:"
echo " --version VERSION Install a specific version (e.g. v0.1.0)"
echo " --uninstall Remove VoxTerm completely"
echo " --help Show this help"
exit 0
;;
*) err "unknown option: $1"; exit 1 ;;
esac
done
# ── Uninstall ─────────────────────────────────────────────
if $UNINSTALL; then
echo ""
echo -e "${BOLD}Uninstalling VoxTerm...${RESET}"
rm -rf "$INSTALL_DIR"
rm -f "$BIN_DIR/voxterm"
done_ "removed $INSTALL_DIR"
done_ "removed $BIN_DIR/voxterm"
echo ""
echo -e "${DIM}voice data at ~/Library/Application Support/voxterm/ was NOT removed${RESET}"
echo -e "${DIM}to remove voice data too: rm -rf ~/Library/Application\\ Support/voxterm${RESET}"
echo ""
exit 0
fi
# ── Header ────────────────────────────────────────────────
echo ""
echo -e "${BOLD}VOXTERM${RESET} — local voice transcription"
echo -e "${DIM}everything runs on your machine, nothing leaves${RESET}"
echo -e "${DIM}installer rev: $INSTALLER_REV${RESET}"
echo ""
# ── Resolve version ───────────────────────────────────────
if [ -z "$REQUESTED_VERSION" ]; then
info "checking latest release..."
# Only look for v* tags (skip utility releases like onnx-models)
REQUESTED_VERSION=$(fetch "https://api.github.com/repos/$REPO/releases" 2>/dev/null \
| grep '"tag_name"' | grep '"v' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/' || echo "")
if [ -z "$REQUESTED_VERSION" ]; then
REQUESTED_VERSION="main"
dim "no releases found, using main branch"
else
done_ "latest release: $REQUESTED_VERSION"
fi
fi
# ── Check if already up to date ───────────────────────────
if [ -f "$VERSION_FILE" ]; then
INSTALLED=$(cat "$VERSION_FILE")
if [ "$INSTALLED" = "$REQUESTED_VERSION" ]; then
done_ "already up to date ($INSTALLED)"
echo ""
exit 0
fi
info "updating $INSTALLED → $REQUESTED_VERSION"
fi
# ── Check Python ──────────────────────────────────────────
info "checking python..."
PYTHON=""
for cmd in python3.14 python3.13 python3.12 python3; do
if command -v "$cmd" &>/dev/null && py_meets_req "$cmd"; then
PYTHON="$cmd"
break
fi
done
if [ -z "$PYTHON" ]; then
err "Python 3.12+ required but not found."
echo ""
echo " Install it with:"
echo " brew install python@3.12 (macOS)"
echo " sudo apt install python3 (Linux)"
exit 1
fi
done_ "found $PYTHON ($($PYTHON --version 2>&1))"
# ── Download release ──────────────────────────────────────
info "downloading voxterm $REQUESTED_VERSION..."
if [ "$REQUESTED_VERSION" = "main" ]; then
ARCHIVE_URL="$REPO_URL/archive/refs/heads/main.tar.gz"
else
ARCHIVE_URL="$REPO_URL/archive/refs/tags/$REQUESTED_VERSION.tar.gz"
fi
# Download and extract to a temp dir, then swap.
# Single-quoted trap so $TMPDIR_DL is expanded at fire-time (still safe under
# set -u since TMPDIR_DL is set right above).
TMPDIR_DL=$(mktemp -d)
trap 'rm -rf "$TMPDIR_DL"' EXIT
fetch "$ARCHIVE_URL" | tar -xz -C "$TMPDIR_DL" --strip-components=1
# Validate the archive actually delivered the package — guards against
# a 200 with truncated/wrong payload silently breaking the install.
if [ ! -f "$TMPDIR_DL/pyproject.toml" ] && [ ! -f "$TMPDIR_DL/requirements.txt" ]; then
err "downloaded archive looks incomplete"
dim "url: $ARCHIVE_URL"
dim "contents:"
ls -la "$TMPDIR_DL" 2>&1 | sed 's/^/ /'
exit 1
fi
# Preserve venv if it exists (avoid re-downloading all deps)
if [ -d "$VENV_DIR" ]; then
mv "$VENV_DIR" "$TMPDIR_DL/.venv"
fi
# Swap into place. mkdir parent first — on a fresh machine, ~/.local/share/
# may not exist yet, and `mv` into a missing parent fails with
# "No such file or directory".
mkdir -p "$(dirname "$INSTALL_DIR")"
rm -rf "$INSTALL_DIR"
mv "$TMPDIR_DL" "$INSTALL_DIR"
done_ "downloaded"
# ── Create venv & install deps ────────────────────────────
info "installing dependencies..."
dim "this may take a minute on first install"
# Recreate the venv if it's missing or its interpreter is below Python 3.12.
# A venv preserved from a prior install may be on an older Python, which would
# fail `pip install -e` against a pyproject with requires-python >=3.12.
RECREATE_VENV=true
if [ -x "$VENV_DIR/bin/python" ]; then
venv_version=$("$VENV_DIR/bin/python" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>/dev/null || echo "0.0")
venv_major=$(echo "$venv_version" | cut -d. -f1)
venv_minor=$(echo "$venv_version" | cut -d. -f2)
if [ "$venv_major" -gt 3 ] || { [ "$venv_major" -eq 3 ] && [ "$venv_minor" -ge 12 ]; }; then
RECREATE_VENV=false
else
dim "existing venv uses Python $venv_version; recreating with $PYTHON"
fi
fi
if $RECREATE_VENV; then
rm -rf "$VENV_DIR"
"$PYTHON" -m venv "$VENV_DIR"
fi
"$VENV_DIR/bin/pip" install --quiet --upgrade pip 2>/dev/null
if [ -f "$INSTALL_DIR/pyproject.toml" ]; then
"$VENV_DIR/bin/pip" install --quiet -e "$INSTALL_DIR"
elif [ -f "$INSTALL_DIR/requirements.txt" ]; then
"$VENV_DIR/bin/pip" install --quiet -r "$INSTALL_DIR/requirements.txt"
else
err "neither pyproject.toml nor requirements.txt found in $INSTALL_DIR"
exit 1
fi
done_ "dependencies installed"
# ── Record installed version ──────────────────────────────
mkdir -p "$INSTALL_DIR"
echo "$REQUESTED_VERSION" > "$VERSION_FILE"
# ── Create launcher ───────────────────────────────────────
info "creating voxterm command..."
mkdir -p "$BIN_DIR"
cat > "$BIN_DIR/voxterm" << 'LAUNCHER'
#!/bin/bash
INSTALL_DIR="$HOME/.local/share/voxterm"
# Release-asset URL: served Cache-Control: no-cache end-to-end and
# resolves to the latest release's install.sh — no CDN/proxy staleness.
INSTALL_URL="https://github.com/dmarzzz/VoxTerm/releases/latest/download/install.sh"
case "${1:-}" in
update)
shift
if [ $# -gt 0 ]; then
# voxterm update <version> → pin to a specific tag
exec bash -c "curl -fsSL --retry 3 --retry-delay 2 '$INSTALL_URL' | bash -s -- --version '$1'"
else
exec bash -c "curl -fsSL --retry 3 --retry-delay 2 '$INSTALL_URL' | bash"
fi
;;
uninstall)
exec bash -c "curl -fsSL --retry 3 --retry-delay 2 '$INSTALL_URL' | bash -s -- --uninstall"
;;
version|-V)
if [ -f "$INSTALL_DIR/.installed-version" ]; then
cat "$INSTALL_DIR/.installed-version"
else
echo "unknown"
fi
exit 0
;;
esac
cd "$INSTALL_DIR"
export PYTHONWARNINGS="ignore::UserWarning"
# `exec -a voxterm` puts "voxterm" in argv[0] so `pkill -f voxterm` and
# `ps -o args` match the TUI process rather than `python -m tui.app`.
# (Plain `pkill voxterm` still matches comm name, which stays `python`.)
exec -a voxterm "$INSTALL_DIR/.venv/bin/python" -m tui.app "$@"
LAUNCHER
chmod +x "$BIN_DIR/voxterm"
done_ "installed to $BIN_DIR/voxterm"
# ── Check PATH ────────────────────────────────────────────
# case-glob instead of grep avoids partial-path false positives
case ":$PATH:" in
*":$BIN_DIR:"*) ;;
*)
# Detect the user's login shell and give shell-specific advice.
# $SHELL is the login shell (set by the OS at session start) and
# survives the curl|bash sub-invocation, so it's the right signal
# for what the user actually uses interactively.
shell_name=$(basename "${SHELL:-sh}")
echo ""
echo -e "${CYAN}▸${RESET} add $BIN_DIR to your PATH"
echo ""
case "$shell_name" in
zsh)
echo " echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.zshrc"
echo " source ~/.zshrc"
;;
bash)
# macOS login bash sources ~/.bash_profile; most Linux distros
# use ~/.bashrc for interactive shells.
if [ "$(uname -s)" = "Darwin" ]; then
rc_file="~/.bash_profile"
else
rc_file="~/.bashrc"
fi
echo " echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> $rc_file"
echo " source $rc_file"
;;
fish)
# fish_add_path writes to a universal variable — persists
# across all future fish sessions, no config edit needed.
echo " fish_add_path \$HOME/.local/bin"
echo ""
echo -e " ${DIM}(persists automatically — no restart or sourcing needed)${RESET}"
;;
ksh|mksh|pdksh|ksh93)
echo " echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.kshrc"
echo " . ~/.kshrc"
;;
dash|ash|sh)
echo " echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.profile"
echo " . ~/.profile"
;;
tcsh|csh)
echo " echo 'setenv PATH \$HOME/.local/bin:\$PATH' >> ~/.${shell_name}rc"
echo " source ~/.${shell_name}rc"
;;
nu)
echo " use std/util 'path add'; path add \$env.HOME/.local/bin"
echo ""
echo -e " ${DIM}for persistence, add the same line to ~/.config/nushell/config.nu${RESET}"
;;
*)
# Unknown shell — print POSIX form and a hint.
echo -e " ${DIM}shell detected: $shell_name (no preset — using POSIX form)${RESET}"
echo ""
echo " export PATH=\"\$HOME/.local/bin:\$PATH\""
echo ""
echo -e " ${DIM}add the above (or your shell's equivalent) to your shell's startup file${RESET}"
;;
esac
echo ""
;;
esac
# ── Done ──────────────────────────────────────────────────
echo ""
echo -e "${GREEN}${BOLD}voxterm $REQUESTED_VERSION installed!${RESET}"
echo ""
echo " run it: voxterm"
echo " update: voxterm update"
echo " uninstall: voxterm uninstall"
echo " pin version: voxterm update v0.1.0"
echo " show version: voxterm version"
echo ""