xgem-cli 2.0.0-alpha.14 → 2.0.0-alpha.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,450 @@
1
+ #!/bin/bash
2
+ # xgem toolchain layer: finds tools where version managers actually put them,
3
+ # offers to install missing ones, and reports/applies updates.
4
+ # Kept bash 3.2 compatible (macOS default): no associative arrays.
5
+ # Depends on lib/logger.sh, lib/utils.sh.
6
+
7
+ XGEM_CACHE_DIR="${XGEM_CACHE_DIR:-$HOME/.xgem/cache}"
8
+ : "${XGEM_NO_NETWORK:=0}"
9
+
10
+ TOOL_DIR=""
11
+ TOOL_PATH=""
12
+ TOOL_MANAGER=""
13
+ TOOL_ON_PATH=0
14
+
15
+ _tool_canon() {
16
+ case "$1" in
17
+ npm|npx|corepack) echo node ;;
18
+ pip|pip3|python) echo python3 ;;
19
+ rustc|rustup) echo cargo ;;
20
+ *) echo "$1" ;;
21
+ esac
22
+ }
23
+
24
+ tool_known() {
25
+ case "$(_tool_canon "$1")" in
26
+ flutter|dart|node|python3|go|cargo|docker|git|gh|swift|fvm) return 0 ;;
27
+ *) return 1 ;;
28
+ esac
29
+ }
30
+
31
+ tool_display() {
32
+ case "$1" in
33
+ flutter) echo "Flutter SDK" ;;
34
+ dart) echo "Dart SDK" ;;
35
+ node) echo "Node.js" ;;
36
+ python3) echo "Python 3" ;;
37
+ go) echo "Go" ;;
38
+ cargo) echo "Rust (cargo)" ;;
39
+ docker) echo "Docker" ;;
40
+ git) echo "Git" ;;
41
+ gh) echo "GitHub CLI" ;;
42
+ swift) echo "Swift" ;;
43
+ fvm) echo "FVM (Flutter Version Management)" ;;
44
+ *) echo "$1" ;;
45
+ esac
46
+ }
47
+
48
+ tool_url() {
49
+ case "$1" in
50
+ flutter|dart) echo "https://docs.flutter.dev/get-started/install" ;;
51
+ node) echo "https://nodejs.org/en/download" ;;
52
+ python3) echo "https://www.python.org/downloads/" ;;
53
+ go) echo "https://go.dev/dl/" ;;
54
+ cargo) echo "https://www.rust-lang.org/tools/install" ;;
55
+ docker) echo "https://www.docker.com/products/docker-desktop/" ;;
56
+ git) echo "https://git-scm.com/downloads" ;;
57
+ gh) echo "https://cli.github.com" ;;
58
+ swift) echo "https://www.swift.org/install/" ;;
59
+ fvm) echo "https://fvm.app/documentation/getting-started/installation" ;;
60
+ esac
61
+ }
62
+
63
+ # fw_required_tool <framework> -> the tool that framework's commands need.
64
+ fw_required_tool() {
65
+ case "$1" in
66
+ flutter) echo flutter ;;
67
+ node|react|vue|angular|next) echo node ;;
68
+ python) echo python3 ;;
69
+ go) echo go ;;
70
+ rust) echo cargo ;;
71
+ swift) echo swift ;;
72
+ docker) echo docker ;;
73
+ esac
74
+ }
75
+
76
+ _tool_pretty() {
77
+ case "$1" in
78
+ "$HOME"/*) printf '~%s' "${1#"$HOME"}" ;;
79
+ *) printf '%s' "$1" ;;
80
+ esac
81
+ }
82
+
83
+ _tool_find_up() {
84
+ local dir=$PWD
85
+ while [ -n "$dir" ]; do
86
+ [ "$dir" = "$HOME" ] && [ "$PWD" != "$HOME" ] && break
87
+ [ -e "$dir/$1" ] && { echo "$dir/$1"; return 0; }
88
+ [ "$dir" = "/" ] && break
89
+ dir=$(dirname "$dir")
90
+ done
91
+ return 1
92
+ }
93
+
94
+ # tool_pin <tool> -> the version this project pins for it, if any.
95
+ tool_pin() {
96
+ local f v="" key
97
+ case "$1" in
98
+ flutter|dart)
99
+ if f=$(_tool_find_up .fvmrc); then
100
+ v=$(grep -oE '"flutter"[[:space:]]*:[[:space:]]*"[^"]+"' "$f" | sed -E 's/.*:[[:space:]]*"([^"]+)"/\1/')
101
+ elif f=$(_tool_find_up .fvm/fvm_config.json); then
102
+ v=$(grep -oE '"flutterSdkVersion"[[:space:]]*:[[:space:]]*"[^"]+"' "$f" | sed -E 's/.*:[[:space:]]*"([^"]+)"/\1/')
103
+ fi
104
+ key=flutter
105
+ ;;
106
+ node)
107
+ if f=$(_tool_find_up .nvmrc); then
108
+ v=$(tr -d '[:space:]' < "$f")
109
+ v=${v#v}
110
+ fi
111
+ key=nodejs
112
+ ;;
113
+ python3) key=python ;;
114
+ go) key=golang ;;
115
+ cargo) key=rust ;;
116
+ *) key="" ;;
117
+ esac
118
+ if [ -z "$v" ] && [ -n "$key" ] && f=$(_tool_find_up .tool-versions); then
119
+ v=$(awk -v k="$key" '$1==k {print $2; exit}' "$f")
120
+ fi
121
+ echo "$v"
122
+ }
123
+
124
+ _tool_fvm_roots() {
125
+ [ -n "${FVM_CACHE_PATH:-}" ] && echo "$FVM_CACHE_PATH"
126
+ [ -n "${FVM_HOME:-}" ] && echo "$FVM_HOME"
127
+ echo "$HOME/fvm"
128
+ echo "$HOME/.fvm"
129
+ echo "$HOME/.fvm_flutter"
130
+ }
131
+
132
+ _tool_nvm_candidate() {
133
+ local root=${NVM_DIR:-$HOME/.nvm} pin want v=""
134
+ [ -d "$root/versions/node" ] || return 0
135
+ pin=$(tool_pin node)
136
+ if [[ "$pin" =~ ^[0-9] ]]; then
137
+ v=$(ls -1 "$root/versions/node" | grep -E "^v${pin}" | sort -V | tail -1)
138
+ fi
139
+ if [ -z "$v" ] && [ -f "$root/alias/default" ]; then
140
+ want=$(tr -d '[:space:]' < "$root/alias/default")
141
+ want=${want#v}
142
+ [[ "$want" =~ ^[0-9] ]] && v=$(ls -1 "$root/versions/node" | grep -E "^v${want}" | sort -V | tail -1)
143
+ fi
144
+ [ -z "$v" ] && v=$(ls -1 "$root/versions/node" | sort -V | tail -1)
145
+ [ -n "$v" ] && printf '%s/versions/node/%s/bin\tnvm\n' "$root" "$v"
146
+ }
147
+
148
+ # _tool_candidates <canonical tool> -> "dir<TAB>manager" lines, best first.
149
+ _tool_candidates() {
150
+ local canon=$1 root pin d v
151
+ case "$canon" in
152
+ flutter|dart)
153
+ pin=$(tool_pin flutter)
154
+ if d=$(_tool_find_up .fvm/flutter_sdk) && [ -d "$d/bin" ]; then
155
+ printf '%s/bin\tfvm\n' "$d"
156
+ fi
157
+ while IFS= read -r root; do
158
+ [ -n "$pin" ] && printf '%s/versions/%s/bin\tfvm\n' "$root" "$pin"
159
+ done < <(_tool_fvm_roots)
160
+ printf '%s/.fvm/flutter_sdk/bin\tfvm\n' "$HOME"
161
+ while IFS= read -r root; do
162
+ printf '%s/default/bin\tfvm\n' "$root"
163
+ done < <(_tool_fvm_roots)
164
+ while IFS= read -r root; do
165
+ v=$(ls -1 "$root/versions" 2>/dev/null | sort -V | tail -1)
166
+ [ -n "$v" ] && printf '%s/versions/%s/bin\tfvm\n' "$root" "$v"
167
+ done < <(_tool_fvm_roots)
168
+ printf '%s/.puro/envs/stable/flutter/bin\tpuro\n' "$HOME"
169
+ printf '%s/development/flutter/bin\tother\n' "$HOME"
170
+ printf '%s/flutter/bin\tother\n' "$HOME"
171
+ ;;
172
+ node)
173
+ _tool_nvm_candidate
174
+ printf '%s/.local/share/fnm/aliases/default/bin\tfnm\n' "$HOME"
175
+ printf '%s/Library/Application Support/fnm/aliases/default/bin\tfnm\n' "$HOME"
176
+ printf '%s/.volta/bin\tvolta\n' "$HOME"
177
+ ;;
178
+ python3) printf '%s/.pyenv/shims\tpyenv\n' "$HOME" ;;
179
+ go) printf '/usr/local/go/bin\tother\n' ;;
180
+ cargo) printf '%s/.cargo/bin\trustup\n' "$HOME" ;;
181
+ docker)
182
+ printf '/Applications/Docker.app/Contents/Resources/bin\tother\n'
183
+ printf '%s/.docker/bin\tother\n' "$HOME"
184
+ ;;
185
+ esac
186
+ printf '%s/.asdf/shims\tasdf\n' "$HOME"
187
+ printf '%s/.local/share/mise/shims\tmise\n' "$HOME"
188
+ printf '/opt/homebrew/bin\tbrew\n'
189
+ printf '/usr/local/bin\tsystem\n'
190
+ }
191
+
192
+ _tool_manager_of() {
193
+ case "$1" in
194
+ */.nvm/*) echo nvm ;;
195
+ */fvm/*|*/.fvm/*|*/.fvm_flutter/*) echo fvm ;;
196
+ /opt/homebrew/*|*/Cellar/*|/home/linuxbrew/*) echo brew ;;
197
+ */.cargo/*) echo rustup ;;
198
+ */.pyenv/*) echo pyenv ;;
199
+ */.asdf/*) echo asdf ;;
200
+ */mise/*) echo mise ;;
201
+ */.volta/*) echo volta ;;
202
+ */.puro/*) echo puro ;;
203
+ /usr/bin/*|/bin/*) echo system ;;
204
+ *) echo other ;;
205
+ esac
206
+ }
207
+
208
+ # tool_resolve <command> -> 0 if found; sets TOOL_PATH, TOOL_DIR,
209
+ # TOOL_MANAGER, TOOL_ON_PATH. Never calls has_cmd (no recursion).
210
+ tool_resolve() {
211
+ local cmd=$1 canon dir via p d
212
+ canon=$(_tool_canon "$cmd")
213
+ TOOL_DIR=""; TOOL_PATH=""; TOOL_MANAGER=""; TOOL_ON_PATH=0
214
+
215
+ p=$(command -v "$cmd" 2>/dev/null)
216
+ if [ -n "$p" ] && [ -x "$p" ]; then
217
+ TOOL_PATH=$p
218
+ TOOL_DIR=$(dirname "$p")
219
+ TOOL_ON_PATH=1
220
+ TOOL_MANAGER=$(_tool_manager_of "$p")
221
+ return 0
222
+ fi
223
+
224
+ while IFS=$'\t' read -r dir via; do
225
+ [ -x "$dir/$cmd" ] || continue
226
+ TOOL_PATH="$dir/$cmd"
227
+ TOOL_DIR=$dir
228
+ TOOL_MANAGER=$via
229
+ return 0
230
+ done < <(_tool_candidates "$canon")
231
+
232
+ if [ "$canon" = "flutter" ] || [ "$canon" = "dart" ]; then
233
+ if [ "$(command -v fvm 2>/dev/null)" ]; then
234
+ d=$(fvm api context 2>/dev/null | grep -oE '"fvmDir"[[:space:]]*:[[:space:]]*"[^"]+"' | sed -E 's/.*"([^"]+)"$/\1/')
235
+ for p in "$d/default/bin" "$d/versions/$(ls -1 "$d/versions" 2>/dev/null | sort -V | tail -1)/bin"; do
236
+ [ -x "$p/$cmd" ] || continue
237
+ TOOL_PATH="$p/$cmd"; TOOL_DIR=$p; TOOL_MANAGER=fvm
238
+ return 0
239
+ done
240
+ fi
241
+ fi
242
+ return 1
243
+ }
244
+
245
+ tool_activate() {
246
+ [ "$TOOL_ON_PATH" = "1" ] && return 0
247
+ [ -n "$TOOL_DIR" ] || return 1
248
+ PATH="$TOOL_DIR:$PATH"
249
+ export PATH
250
+ hash -r
251
+ }
252
+
253
+ # tool_version_of <canonical tool> -> installed version of TOOL_PATH.
254
+ tool_version_of() {
255
+ local canon=$1 flag=--version sdk out
256
+ case "$canon" in
257
+ flutter)
258
+ sdk="$(dirname "$TOOL_PATH")/.."
259
+ if [ -f "$sdk/bin/cache/flutter.version.json" ]; then
260
+ out=$(grep -oE '"frameworkVersion"[[:space:]]*:[[:space:]]*"[^"]+"' "$sdk/bin/cache/flutter.version.json" | sed -E 's/.*"([^"]+)"$/\1/')
261
+ [ -n "$out" ] && { echo "$out"; return 0; }
262
+ fi
263
+ ;;
264
+ go) flag=version ;;
265
+ swift)
266
+ "$TOOL_PATH" --version 2>&1 | grep -oE 'Swift version [0-9]+(\.[0-9]+)*' | head -1 | grep -oE '[0-9]+(\.[0-9]+)*'
267
+ return 0
268
+ ;;
269
+ esac
270
+ "$TOOL_PATH" $flag 2>&1 | head -3 | grep -oE '[0-9]+(\.[0-9]+)+' | head -1
271
+ }
272
+
273
+ _tool_ver_lt() {
274
+ [ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -1)" = "$1" ]
275
+ }
276
+
277
+ _tool_curl() {
278
+ has_cmd curl || return 1
279
+ curl -fsSL --connect-timeout 3 -m 8 "$1" 2>/dev/null
280
+ }
281
+
282
+ _tool_flutter_stable_from_json() {
283
+ if command -v python3 >/dev/null 2>&1; then
284
+ python3 -c 'import sys,json;d=json.load(sys.stdin);h=d["current_release"]["stable"];print(next(r["version"] for r in d["releases"] if r["hash"]==h and r["channel"]=="stable"))' 2>/dev/null
285
+ elif command -v node >/dev/null 2>&1; then
286
+ node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{const d=JSON.parse(s);const h=d.current_release.stable;console.log(d.releases.find(r=>r.hash===h&&r.channel==="stable").version)})' 2>/dev/null
287
+ fi
288
+ }
289
+
290
+ _tool_brew_latest() {
291
+ local formula=$1
292
+ case "$1" in python3) formula=python@3 ;; esac
293
+ HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_ANALYTICS=1 brew info --json=v2 "$formula" 2>/dev/null \
294
+ | grep -oE '"stable": ?"[^"]+"' | head -1 | sed -E 's/.*: ?"([^"]+)"/\1/'
295
+ }
296
+
297
+ _tool_fetch_latest() {
298
+ local canon=$1 os line
299
+ case "$canon" in
300
+ flutter)
301
+ os=$(detect_os)
302
+ [ "$os" = "darwin" ] && os=macos
303
+ _tool_curl "https://storage.googleapis.com/flutter_infra_release/releases/releases_${os}.json" | _tool_flutter_stable_from_json
304
+ ;;
305
+ node)
306
+ _tool_curl "https://nodejs.org/dist/index.json" | grep -m1 -E '"lts":"[A-Za-z]' | sed -E 's/.*"version":"v([^"]+)".*/\1/'
307
+ ;;
308
+ go)
309
+ _tool_curl 'https://go.dev/VERSION?m=text' | head -1 | sed 's/^go//'
310
+ ;;
311
+ cargo)
312
+ [ -x "${TOOL_DIR:-}/rustup" ] || return 0
313
+ line=$("$TOOL_DIR/rustup" check 2>/dev/null | grep -m1 '^stable')
314
+ case "$line" in
315
+ *"Update available"*) echo "$line" | sed -E 's/.*-> ([0-9.]+).*/\1/' ;;
316
+ *"Up to date"*) echo "$line" | sed -E 's/.*: ([0-9.]+).*/\1/' ;;
317
+ esac
318
+ ;;
319
+ dart) ;;
320
+ *)
321
+ [ "$TOOL_MANAGER" = "brew" ] && has_cmd brew && _tool_brew_latest "$canon"
322
+ ;;
323
+ esac
324
+ }
325
+
326
+ # tool_latest <canonical tool> -> newest known version; cached 12h. With
327
+ # XGEM_NO_NETWORK=1 only the cache is consulted.
328
+ tool_latest() {
329
+ local canon=$1 f v
330
+ f="$XGEM_CACHE_DIR/latest-$canon"
331
+ if [ "$XGEM_NO_NETWORK" = "1" ]; then
332
+ [ -f "$f" ] && cat "$f"
333
+ return 0
334
+ fi
335
+ if [ -f "$f" ] && [ -n "$(find "$f" -mmin -720 2>/dev/null)" ]; then
336
+ cat "$f"
337
+ return 0
338
+ fi
339
+ v=$(_tool_fetch_latest "$canon")
340
+ if [ -n "$v" ]; then
341
+ mkdir -p "$XGEM_CACHE_DIR"
342
+ echo "$v" > "$f"
343
+ echo "$v"
344
+ elif [ -f "$f" ]; then
345
+ cat "$f"
346
+ fi
347
+ }
348
+
349
+ # tool_install_cmd <canonical tool> -> a shell command that installs it on
350
+ # this platform, or empty if there's no safe automated route.
351
+ tool_install_cmd() {
352
+ local canon=$1 nvm_sh="${NVM_DIR:-$HOME/.nvm}/nvm.sh"
353
+ case "$(detect_os)" in
354
+ darwin)
355
+ case "$canon" in
356
+ flutter|dart)
357
+ if has_cmd fvm; then echo "fvm install stable && fvm global stable"
358
+ else echo "brew install --cask flutter"; fi ;;
359
+ node)
360
+ if [ -s "$nvm_sh" ]; then echo "bash -c 'source \"$nvm_sh\" && nvm install --lts'"
361
+ else echo "brew install node"; fi ;;
362
+ python3) echo "brew install python" ;;
363
+ go) echo "brew install go" ;;
364
+ docker) echo "brew install --cask docker-desktop" ;;
365
+ git) echo "brew install git" ;;
366
+ gh) echo "brew install gh" ;;
367
+ fvm) echo "brew install fvm" ;;
368
+ cargo) echo "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y" ;;
369
+ swift) echo "xcode-select --install" ;;
370
+ esac
371
+ ;;
372
+ linux)
373
+ case "$canon" in
374
+ cargo) echo "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y" ;;
375
+ fvm) echo "curl -fsSL https://fvm.app/install.sh | bash" ;;
376
+ node)
377
+ [ -s "$nvm_sh" ] && echo "bash -c 'source \"$nvm_sh\" && nvm install --lts'" ;;
378
+ esac
379
+ ;;
380
+ esac
381
+ }
382
+
383
+ # tool_ensure <tool> -> 0 once the tool is usable. Explains what's needed,
384
+ # where to get it, and offers to install it; the caller then continues with
385
+ # whatever was originally requested.
386
+ tool_ensure() {
387
+ local tool=$1 canon recipe
388
+ canon=$(_tool_canon "$tool")
389
+ has_cmd "$tool" && return 0
390
+
391
+ log_warn "$(tool_display "$canon") is required for this but wasn't found (checked PATH, version managers like fvm/nvm, and common install locations)."
392
+ echo " Download: $(tool_url "$canon")"
393
+
394
+ recipe=$(tool_install_cmd "$canon")
395
+ if [ -z "$recipe" ]; then
396
+ log_info "Install it from the link above, then re-run this command."
397
+ return 1
398
+ fi
399
+
400
+ case "$recipe" in
401
+ brew*)
402
+ if ! command -v brew >/dev/null 2>&1; then
403
+ log_info "That install route needs Homebrew (https://brew.sh) — install it first, or use the download link above."
404
+ return 1
405
+ fi
406
+ ;;
407
+ esac
408
+
409
+ echo " Install command: $recipe"
410
+ confirm "Install $(tool_display "$canon") now?" || { log_info "Skipped. Install it from the link above when you're ready."; return 1; }
411
+
412
+ if ! bash -c "$recipe"; then
413
+ log_error "The install command failed (see its output above)."
414
+ return 1
415
+ fi
416
+
417
+ hash -r
418
+ if has_cmd "$tool"; then
419
+ log_success "$(tool_display "$canon") is ready."
420
+ return 0
421
+ fi
422
+ log_warn "Install finished but '$tool' still isn't reachable — you may need to open a new terminal or add it to PATH."
423
+ return 1
424
+ }
425
+
426
+ # tool_update_cmd <canonical tool> -> command that updates it given the
427
+ # current TOOL_MANAGER, or empty when only a manual download applies.
428
+ tool_update_cmd() {
429
+ local canon=$1 latest=$2 nvm_sh="${NVM_DIR:-$HOME/.nvm}/nvm.sh"
430
+ case "$canon" in
431
+ flutter)
432
+ case "$TOOL_MANAGER" in
433
+ fvm) echo "fvm install $latest" ;;
434
+ brew) echo "brew upgrade --cask flutter" ;;
435
+ *) echo "flutter upgrade" ;;
436
+ esac ;;
437
+ node)
438
+ case "$TOOL_MANAGER" in
439
+ nvm) echo "bash -c 'source \"$nvm_sh\" && nvm install --lts'" ;;
440
+ brew) echo "brew upgrade node" ;;
441
+ esac ;;
442
+ cargo) [ "$TOOL_MANAGER" = "rustup" ] && echo "rustup update" ;;
443
+ *)
444
+ [ "$TOOL_MANAGER" = "brew" ] && case "$canon" in
445
+ python3) echo "brew upgrade python" ;;
446
+ docker) echo "brew upgrade docker" ;;
447
+ *) echo "brew upgrade $canon" ;;
448
+ esac ;;
449
+ esac
450
+ }
package/lib/update.sh ADDED
@@ -0,0 +1,74 @@
1
+ #!/bin/bash
2
+ # xgem update [tool]: show which installed toolchains have newer versions
3
+ # and apply the ones you approve.
4
+ # Depends on lib/logger.sh, lib/utils.sh, lib/toolchain.sh.
5
+
6
+ _update_one() {
7
+ local tool=$1 explicit=$2 version latest cmd
8
+
9
+ if ! tool_resolve "$tool"; then
10
+ if [ "$explicit" = "1" ]; then
11
+ tool_ensure "$tool"
12
+ fi
13
+ return 0
14
+ fi
15
+
16
+ version=$(tool_version_of "$tool")
17
+ latest=$(XGEM_NO_NETWORK=1 tool_latest "$tool")
18
+
19
+ if [ -z "$latest" ]; then
20
+ [ "$explicit" = "1" ] && log_warn "Couldn't determine the latest $(tool_display "$tool") version (offline, or no version source for a $TOOL_MANAGER install)."
21
+ return 0
22
+ fi
23
+ if [ -z "$version" ] || ! _tool_ver_lt "$version" "$latest"; then
24
+ [ "$explicit" = "1" ] && log_success "$(tool_display "$tool") ${version:-} is up to date."
25
+ return 0
26
+ fi
27
+
28
+ echo ""
29
+ log_info "$(tool_display "$tool"): $version -> $latest"
30
+ cmd=$(tool_update_cmd "$tool" "$latest")
31
+ if [ -z "$cmd" ]; then
32
+ echo " No automated update route for this install ($TOOL_MANAGER). Download: $(tool_url "$tool")"
33
+ return 0
34
+ fi
35
+
36
+ echo " Update command: $cmd"
37
+ confirm "Update $(tool_display "$tool") to $latest?" || return 0
38
+ if ! bash -c "$cmd"; then
39
+ log_error "Update failed (see output above)."
40
+ return 0
41
+ fi
42
+ log_success "$(tool_display "$tool") update finished."
43
+
44
+ if [ "$tool" = "flutter" ] && [ "$TOOL_MANAGER" = "fvm" ]; then
45
+ confirm "Make Flutter $latest your global default (fvm global)?" && fvm global "$latest"
46
+ if _tool_find_up .fvmrc >/dev/null || _tool_find_up .fvm/fvm_config.json >/dev/null; then
47
+ confirm "Also pin $latest for the project in this directory (fvm use)?" && fvm use "$latest"
48
+ fi
49
+ fi
50
+ }
51
+
52
+ cmd_update() {
53
+ local target=${1:-} t
54
+
55
+ if [ -n "$target" ]; then
56
+ tool_known "$target" || die "Unknown tool '$target'. Known: flutter node python3 go cargo docker git gh fvm swift"
57
+ _update_one "$(_tool_canon "$target")" 1
58
+ return 0
59
+ fi
60
+
61
+ if [ "$XGEM_NO_NETWORK" != "1" ]; then
62
+ log_info "Checking installed tools for updates..."
63
+ for t in flutter node cargo go fvm gh git docker python3; do
64
+ ( tool_resolve "$t" && tool_latest "$t" ) >/dev/null 2>&1 &
65
+ done
66
+ wait
67
+ fi
68
+
69
+ for t in flutter node cargo go fvm gh git docker python3; do
70
+ _update_one "$t" 0
71
+ done
72
+ echo ""
73
+ log_success "Update check finished. Run 'xgem doctor' for the full toolchain report."
74
+ }
package/lib/utils.sh CHANGED
@@ -24,21 +24,29 @@ is_apple_silicon() {
24
24
  [ "$(detect_os)" = "darwin" ] && [ "$(detect_arch)" = "arm64" ]
25
25
  }
26
26
 
27
- # has_cmd <name> -> 0/1, no output
27
+ # has_cmd <name> -> 0/1, no output. Also finds tools that version managers
28
+ # (fvm, nvm, ...) keep off PATH, and puts them on this process's PATH.
28
29
  has_cmd() {
29
- command -v "$1" >/dev/null 2>&1
30
+ command -v "$1" >/dev/null 2>&1 && return 0
31
+ if declare -F tool_known >/dev/null 2>&1 && tool_known "$1"; then
32
+ tool_resolve "$1" && tool_activate
33
+ return $?
34
+ fi
35
+ return 1
30
36
  }
31
37
 
32
38
  # require_cmd <name> [install-hint]
33
39
  require_cmd() {
34
40
  local name=$1
35
41
  local hint=${2:-}
36
- if ! has_cmd "$name"; then
37
- if [ -n "$hint" ]; then
38
- die "'$name' is required but not found. $hint"
39
- else
40
- die "'$name' is required but not found in PATH."
41
- fi
42
+ has_cmd "$name" && return 0
43
+ if declare -F tool_known >/dev/null 2>&1 && tool_known "$name"; then
44
+ tool_ensure "$(_tool_canon "$name")" && return 0
45
+ fi
46
+ if [ -n "$hint" ]; then
47
+ die "'$name' is required but not found. $hint"
48
+ else
49
+ die "'$name' is required but not found in PATH."
42
50
  fi
43
51
  }
44
52
 
package/lib/version.sh CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  # xgem version info.
3
3
 
4
- XGEM_VERSION="2.0.0-alpha.14"
4
+ XGEM_VERSION="2.0.0-alpha.16"
5
5
 
6
6
  print_version() {
7
7
  echo "xgem $XGEM_VERSION"
@@ -8,8 +8,13 @@ const { spawnSync } = require('node:child_process');
8
8
  const { logInfo, logSuccess, logWarn, logDebug, die } = require('./logger');
9
9
  const { hasCmd, confirm, prompt } = require('./utils');
10
10
  const scaffold = require('./scaffold');
11
+ const toolchain = require('./toolchain');
11
12
 
12
13
  function run(cmd, args, opts = {}) {
14
+ if (process.env.XGEM_DRY_RUN === '1') {
15
+ logInfo(`(dry-run) would run: ${cmd} ${args.join(' ')}`);
16
+ return { status: 0 };
17
+ }
13
18
  return spawnSync(cmd, args, { stdio: 'inherit', shell: process.platform === 'win32', ...opts });
14
19
  }
15
20
 
@@ -42,6 +47,10 @@ async function bootstrapEnvFile() {
42
47
  if (fs.existsSync('.env')) { logDebug('.env already exists, leaving it as-is.'); return; }
43
48
  for (const example of ['.env.example', '.env.sample']) {
44
49
  if (!fs.existsSync(example)) continue;
50
+ if (process.env.XGEM_DRY_RUN === '1') {
51
+ logInfo(`(dry-run) would create .env from ${example}`);
52
+ return;
53
+ }
45
54
  fs.copyFileSync(example, '.env');
46
55
  logSuccess(`Created .env from ${example}.`);
47
56
  const missing = fs.readFileSync('.env', 'utf8')
@@ -72,6 +81,22 @@ function installWith(pm, extraArgs = []) {
72
81
  }
73
82
  }
74
83
 
84
+ async function preflight(fw) {
85
+ const tool = toolchain.fwTool(fw);
86
+ if (tool && !(await toolchain.ensure(tool))) return false;
87
+ if (fw === 'flutter' && !(await toolchain.ensure('dart'))) return false;
88
+ if (['node', 'react', 'vue', 'angular', 'next'].includes(fw)) {
89
+ const pm = nodePm();
90
+ if (pm !== 'npm' && !hasCmd(pm)) {
91
+ logWarn(`This project uses ${pm} (lockfile found) but '${pm}' isn't installed.`);
92
+ console.log(` Enable it via Node's bundled corepack: corepack enable (or: npm install -g ${pm})`);
93
+ if (!(await confirm(`Run 'npm install -g ${pm}' now?`))) return false;
94
+ run('npm', ['install', '-g', pm]);
95
+ }
96
+ }
97
+ return true;
98
+ }
99
+
75
100
  async function bootstrapInstall(fw) {
76
101
  switch (fw) {
77
102
  case 'node':
@@ -80,36 +105,33 @@ async function bootstrapInstall(fw) {
80
105
  case 'angular':
81
106
  case 'next': {
82
107
  const pm = nodePm();
108
+ if (!(await confirm(`Install dependencies with ${pm}?`))) break;
83
109
  logInfo(`Installing dependencies with ${pm}...`);
84
110
  installWith(pm);
85
111
  break;
86
112
  }
87
113
  case 'flutter':
88
- if (!hasCmd('flutter')) { logWarn("flutter not found — skipping 'flutter pub get'."); break; }
89
- logInfo('Running flutter pub get...');
114
+ if (!(await confirm("Run 'flutter pub get'?"))) break;
90
115
  run('flutter', ['pub', 'get']);
91
116
  break;
92
117
  case 'python':
93
118
  if (fs.existsSync('pyproject.toml') && hasCmd('poetry')) {
94
- logInfo('Installing dependencies with poetry...');
119
+ if (!(await confirm('Install dependencies with poetry?'))) break;
95
120
  run('poetry', ['install']);
96
121
  } else if (fs.existsSync('requirements.txt')) {
97
- if (!hasCmd('python') && !hasCmd('python3')) { logWarn('python not found — skipping install.'); break; }
98
122
  const py = hasCmd('python') ? 'python' : 'python3';
123
+ if (!(await confirm('Create .venv and install requirements.txt into it?'))) break;
99
124
  if (!fs.existsSync('.venv')) run(py, ['-m', 'venv', '.venv']);
100
125
  const pipPath = process.platform === 'win32' ? path.join('.venv', 'Scripts', 'pip.exe') : path.join('.venv', 'bin', 'pip');
101
- logInfo('Installing dependencies into .venv...');
102
126
  run(pipPath, ['install', '-r', 'requirements.txt']);
103
127
  }
104
128
  break;
105
129
  case 'go':
106
- if (!hasCmd('go')) { logWarn("go not found — skipping 'go mod download'."); break; }
107
- logInfo('Running go mod download...');
130
+ if (!(await confirm("Run 'go mod download'?"))) break;
108
131
  run('go', ['mod', 'download']);
109
132
  break;
110
133
  case 'rust':
111
- if (!hasCmd('cargo')) { logWarn("cargo not found — skipping 'cargo fetch'."); break; }
112
- logInfo('Running cargo fetch...');
134
+ if (!(await confirm("Run 'cargo fetch'?"))) break;
113
135
  run('cargo', ['fetch']);
114
136
  break;
115
137
  case 'docker':
@@ -172,6 +194,7 @@ async function cmdBootstrap(configDir) {
172
194
  logSuccess(`Detected: ${fw}`);
173
195
  }
174
196
 
197
+ if (!(await preflight(fw))) die('Bootstrap stopped: a required tool is missing.');
175
198
  await bootstrapEnvFile();
176
199
  await bootstrapInstall(fw);
177
200
  await bootstrapMigrations(fw);