xgem-cli 2.0.0-alpha.15 → 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.
- package/README.md +6 -3
- package/bin/xgem +13 -2
- package/bin/xgem.js +6 -2
- package/lib/bootstrap.sh +53 -22
- package/lib/ci.sh +46 -11
- package/lib/doctor.sh +83 -11
- package/lib/flutter.sh +3 -0
- package/lib/toolchain.sh +450 -0
- package/lib/update.sh +74 -0
- package/lib/utils.sh +16 -8
- package/lib/version.sh +1 -1
- package/lib-win/bootstrap.js +32 -9
- package/lib-win/ci.js +40 -10
- package/lib-win/doctor.js +48 -20
- package/lib-win/flutter.js +5 -4
- package/lib-win/toolchain.js +284 -0
- package/lib-win/update.js +68 -0
- package/lib-win/utils.js +10 -2
- package/package.json +1 -1
package/lib/toolchain.sh
ADDED
|
@@ -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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
package/lib-win/bootstrap.js
CHANGED
|
@@ -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 (!
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
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);
|