instabox 0.5.0 → 0.5.2

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,370 @@
1
+ #!/usr/bin/env bash
2
+ # instabox-move-repo: install a moved project's toolchain and dependencies on the box.
3
+ #
4
+ # bash -l deps.sh [--dry-run] [--no-smoke] [DIR]
5
+ #
6
+ # Detects the toolchain from the project's lockfiles and manifests (DIR, default the current
7
+ # directory, and each first-level subdirectory that has a lockfile of its own), installs a runtime
8
+ # only when it is missing or the project pins another version, installs the dependencies with
9
+ # the project's own package manager without rewriting its lockfile, then runs an obvious build
10
+ # as a smoke test. A failed step is reported and the next one still runs; the exit status is 1
11
+ # when any step failed. --dry-run prints the steps without running them.
12
+ # Never reads .env files: it only says where one is expected.
13
+ set -u
14
+
15
+ DRY=0
16
+ SMOKE=1
17
+ DIR=.
18
+ while [ $# -gt 0 ]; do
19
+ case $1 in
20
+ --dry-run) DRY=1 ;;
21
+ --no-smoke) SMOKE=0 ;;
22
+ -h | --help)
23
+ sed -n '2,13p' "$0" | sed 's/^# \{0,1\}//'
24
+ exit 0
25
+ ;;
26
+ -*)
27
+ echo "deps.sh: unknown option $1" >&2
28
+ exit 2
29
+ ;;
30
+ *) DIR=$1 ;;
31
+ esac
32
+ shift
33
+ done
34
+ cd "$DIR" || exit 2
35
+ ROOT=$(pwd)
36
+
37
+ export DEBIAN_FRONTEND=noninteractive COREPACK_ENABLE_DOWNLOAD_PROMPT=0 CI=1
38
+ OK=()
39
+ FAILED=()
40
+ NOTES=()
41
+ DONE_RUNTIMES=" "
42
+
43
+ have() { command -v "$1" >/dev/null 2>&1; }
44
+ note() { NOTES+=("$1"); }
45
+
46
+ # step LABEL CMD: run CMD (a shell string) in a subshell in the current directory, stdin closed.
47
+ step() {
48
+ local label=$1 cmd=$2
49
+ echo "==> $label: $cmd"
50
+ if [ "$DRY" = 1 ]; then
51
+ OK+=("$label")
52
+ return 0
53
+ fi
54
+ if (eval "$cmd") </dev/null; then
55
+ OK+=("$label")
56
+ else
57
+ echo "!!! $label failed" >&2
58
+ FAILED+=("$label")
59
+ return 1
60
+ fi
61
+ }
62
+
63
+ # runtime NAME CMD: like step, in this shell (so PATH changes stick), once per runtime.
64
+ runtime() {
65
+ local name=$1 cmd=$2
66
+ case $DONE_RUNTIMES in *" $name "*) return 0 ;; esac
67
+ DONE_RUNTIMES="$DONE_RUNTIMES$name "
68
+ echo "==> install $name: $cmd"
69
+ if [ "$DRY" = 1 ]; then
70
+ OK+=("install $name")
71
+ return 0
72
+ fi
73
+ if eval "$cmd" </dev/null; then
74
+ OK+=("install $name")
75
+ else
76
+ echo "!!! install $name failed" >&2
77
+ FAILED+=("install $name")
78
+ return 1
79
+ fi
80
+ }
81
+
82
+ # persist LINE: add a line to the login and interactive shell files once, for later shells.
83
+ persist() {
84
+ [ "$DRY" = 1 ] && return 0
85
+ local f p=
86
+ for f in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do
87
+ [ -e "$f" ] && p=$f && break
88
+ done
89
+ [ -n "$p" ] || p=$HOME/.profile
90
+ for f in "$p" "$HOME/.bashrc"; do
91
+ grep -qxF "$1" "$f" 2>/dev/null || printf '%s\n' "$1" >>"$f"
92
+ done
93
+ }
94
+
95
+ APT_UPDATED=0
96
+ apt_install() {
97
+ local pre=
98
+ [ "$APT_UPDATED" = 1 ] || pre='sudo apt-get update -qq && '
99
+ APT_UPDATED=1
100
+ echo "${pre}sudo apt-get install -y -qq $*"
101
+ }
102
+
103
+ arch() {
104
+ case $(uname -m) in
105
+ x86_64 | amd64) echo amd64 ;;
106
+ aarch64 | arm64) echo arm64 ;;
107
+ *) uname -m ;;
108
+ esac
109
+ }
110
+
111
+ first_line() { head -n 1 "$1" 2>/dev/null | tr -d ' \r\t' | sed 's/^v//'; }
112
+
113
+ # --- runtimes -----------------------------------------------------------------------------
114
+
115
+ ensure_mise() {
116
+ have mise || [ -x "$HOME/.local/bin/mise" ] ||
117
+ runtime mise 'curl -fsSL https://mise.run | sh'
118
+ export PATH="$HOME/.local/bin:$HOME/.local/share/mise/shims:$PATH"
119
+ persist 'export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$PATH"'
120
+ }
121
+
122
+ ensure_node() {
123
+ local want=$1 have_major=
124
+ have node && have_major=$(node -v 2>/dev/null | sed 's/^v//; s/\..*//')
125
+ case $want in
126
+ '' | node | stable | current | 'lts/*' | lts) [ -n "$have_major" ] && return 0 ;;
127
+ [0-9]*) [ "${want%%.*}" = "$have_major" ] && return 0 ;;
128
+ esac
129
+ [ -n "$want" ] || want=--lts
130
+ local nvm='export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] || curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | PROFILE=/dev/null bash; . "$NVM_DIR/nvm.sh"'
131
+ runtime "node $want" "$nvm && nvm install $want && nvm alias default $want" &&
132
+ persist 'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"'
133
+ }
134
+
135
+ ensure_pm() { # pnpm | yarn
136
+ have "$1" && return 0
137
+ runtime "$1" "corepack enable 2>/dev/null || sudo -n env PATH=\"\$PATH\" corepack enable 2>/dev/null || npm install -g $1 || sudo -n env PATH=\"\$PATH\" npm install -g $1"
138
+ }
139
+
140
+ ensure_bun() {
141
+ have bun && return 0
142
+ runtime bun 'curl -fsSL https://bun.sh/install | bash' && export PATH="$HOME/.bun/bin:$PATH"
143
+ persist 'export PATH="$HOME/.bun/bin:$PATH"'
144
+ }
145
+
146
+ ensure_uv() {
147
+ have uv && return 0
148
+ runtime uv 'curl -LsSf https://astral.sh/uv/install.sh | sh' && export PATH="$HOME/.local/bin:$PATH"
149
+ persist 'export PATH="$HOME/.local/bin:$PATH"'
150
+ }
151
+
152
+ ensure_uv_tool() { # name
153
+ have "$1" && return 0
154
+ ensure_uv
155
+ runtime "$1" "uv tool install $1" && export PATH="$HOME/.local/bin:$PATH"
156
+ }
157
+
158
+ ensure_rust() {
159
+ have cargo && return 0
160
+ [ -x "$HOME/.cargo/bin/cargo" ] && export PATH="$HOME/.cargo/bin:$PATH" && return 0
161
+ runtime rust "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal" &&
162
+ export PATH="$HOME/.cargo/bin:$PATH"
163
+ persist '[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"'
164
+ }
165
+
166
+ ensure_go() {
167
+ have go && return 0
168
+ [ -x /usr/local/go/bin/go ] && export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH" && return 0
169
+ runtime go "v=\$(curl -fsSL 'https://go.dev/VERSION?m=text' | head -n 1) && curl -fsSL \"https://go.dev/dl/\$v.linux-$(arch).tar.gz\" | sudo tar -C /usr/local -xz" &&
170
+ export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH"
171
+ persist 'export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH"'
172
+ }
173
+
174
+ ensure_ruby() {
175
+ have ruby && have bundle && return 0
176
+ have ruby || runtime ruby "$(apt_install ruby-full build-essential)"
177
+ have bundle || runtime bundler 'gem install --user-install bundler' &&
178
+ export PATH="$(ruby -e 'print Gem.user_dir' 2>/dev/null)/bin:$PATH"
179
+ persist 'command -v ruby >/dev/null && export PATH="$(ruby -e "print Gem.user_dir")/bin:$PATH"'
180
+ }
181
+
182
+ ensure_java() { # also maven when asked
183
+ have java && have javac || runtime jdk "$(apt_install default-jdk-headless)"
184
+ [ "${1-}" = maven ] && ! have mvn && runtime maven "$(apt_install maven)"
185
+ return 0
186
+ }
187
+
188
+ ensure_php() {
189
+ have php && have composer && return 0
190
+ runtime php "$(apt_install php-cli php-xml php-mbstring php-curl php-zip unzip composer)"
191
+ }
192
+
193
+ # --- one project directory -------------------------------------------------------------------
194
+
195
+ # has_script NAME: package.json's "scripts" object has NAME. Node is installed by then; without it
196
+ # (a dry run on a machine with no node) fall back to a plain text match.
197
+ has_script() {
198
+ have node || { grep -Eq "\"$1\"[[:space:]]*:" package.json 2>/dev/null; return; }
199
+ node -e 'const s=(JSON.parse(require("fs").readFileSync("package.json","utf8"))||{}).scripts; process.exit(s && typeof s[process.argv[1]]==="string" ? 0 : 1)' "$1" 2>/dev/null
200
+ }
201
+
202
+ node_project() { # label
203
+ local L=$1 pm= want=
204
+ [ -f package.json ] || return 0
205
+ pm=$(sed -n 's/.*"packageManager"[[:space:]]*:[[:space:]]*"\([a-z]*\)@.*/\1/p' package.json | head -n 1)
206
+ if [ -z "$pm" ]; then
207
+ if [ -f pnpm-lock.yaml ]; then pm=pnpm
208
+ elif [ -f yarn.lock ]; then pm=yarn
209
+ elif [ -f bun.lockb ] || [ -f bun.lock ]; then pm=bun
210
+ else pm=npm
211
+ fi
212
+ fi
213
+ [ -f .nvmrc ] && want=$(first_line .nvmrc)
214
+ [ -z "$want" ] && [ -f .node-version ] && want=$(first_line .node-version)
215
+ if [ "$pm" = bun ]; then ensure_bun; else ensure_node "$want"; fi
216
+ case $pm in
217
+ pnpm)
218
+ ensure_pm pnpm
219
+ step "$L pnpm install" 'pnpm install --frozen-lockfile'
220
+ ;;
221
+ yarn)
222
+ ensure_pm yarn
223
+ if [ -f .yarnrc.yml ]; then step "$L yarn install" 'yarn install --immutable'
224
+ else step "$L yarn install" 'yarn install --frozen-lockfile'
225
+ fi
226
+ ;;
227
+ bun) step "$L bun install" 'bun install --frozen-lockfile' ;;
228
+ *)
229
+ if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then step "$L npm ci" 'npm ci'
230
+ else step "$L npm install" 'npm install --no-package-lock'
231
+ fi
232
+ ;;
233
+ esac
234
+ if [ "$SMOKE" = 1 ] && has_script build; then step "$L build" "$pm run build"; fi
235
+ }
236
+
237
+ python_project() {
238
+ local L=$1 reqs= f
239
+ if [ -f uv.lock ]; then
240
+ ensure_uv
241
+ step "$L uv sync" 'uv sync --frozen'
242
+ elif [ -f poetry.lock ]; then
243
+ ensure_uv_tool poetry
244
+ step "$L poetry install" 'poetry install --no-interaction'
245
+ elif [ -f Pipfile.lock ]; then
246
+ ensure_uv_tool pipenv
247
+ step "$L pipenv sync" 'pipenv sync --dev'
248
+ elif [ -f pdm.lock ]; then
249
+ ensure_uv_tool pdm
250
+ step "$L pdm sync" 'pdm sync'
251
+ else
252
+ for f in requirements*.txt *requirements.txt; do
253
+ [ -f "$f" ] || continue
254
+ case " $reqs " in *" -r $f "*) continue ;; esac
255
+ reqs="$reqs -r $f"
256
+ done
257
+ if [ -n "$reqs" ]; then
258
+ ensure_uv
259
+ step "$L pip install" "{ [ -d .venv ] || uv venv; } && uv pip install$reqs"
260
+ elif [ -f pyproject.toml ] || [ -f setup.py ]; then
261
+ ensure_uv
262
+ step "$L pip install -e ." '{ [ -d .venv ] || uv venv; } && uv pip install -e .'
263
+ fi
264
+ fi
265
+ }
266
+
267
+ project() { # dir label
268
+ local d=$1 L=$2 f
269
+ cd "$ROOT/$d" || return 0
270
+ # Toolchain pins for many languages at once (.tool-versions, mise.toml): mise installs them.
271
+ if [ -f .tool-versions ] || [ -f mise.toml ] || [ -f .mise.toml ]; then
272
+ ensure_mise
273
+ step "$L mise install" 'mise trust --yes . >/dev/null 2>&1; mise install --yes'
274
+ fi
275
+ node_project "$L"
276
+ if [ -f uv.lock ] || [ -f poetry.lock ] || [ -f Pipfile.lock ] || [ -f pdm.lock ] || [ -f pyproject.toml ] ||
277
+ [ -f setup.py ] || ls requirements*.txt *requirements.txt >/dev/null 2>&1; then
278
+ python_project "$L"
279
+ fi
280
+ if [ -f Cargo.toml ] && { [ "$d" = . ] || [ ! -f "$ROOT/Cargo.toml" ]; }; then
281
+ ensure_rust
282
+ if [ -f Cargo.lock ]; then step "$L cargo fetch" 'cargo fetch --locked'
283
+ else step "$L cargo fetch" 'cargo fetch'
284
+ fi
285
+ [ "$SMOKE" = 1 ] && step "$L cargo build" 'cargo build'
286
+ fi
287
+ if [ -f go.mod ] && { [ "$d" = . ] || [ ! -f "$ROOT/go.work" ]; }; then
288
+ ensure_go
289
+ step "$L go mod download" 'go mod download'
290
+ [ "$SMOKE" = 1 ] && step "$L go build" 'go build ./...'
291
+ fi
292
+ if [ -f Gemfile ]; then
293
+ ensure_ruby
294
+ step "$L bundle install" 'bundle config set --global path "$HOME/.bundle/gems" && bundle install'
295
+ fi
296
+ if [ -f pom.xml ]; then
297
+ if [ -x mvnw ]; then
298
+ ensure_java
299
+ step "$L maven deps" './mvnw -B -q dependency:go-offline'
300
+ [ "$SMOKE" = 1 ] && step "$L maven compile" './mvnw -B -q -DskipTests compile'
301
+ else
302
+ ensure_java maven
303
+ step "$L maven deps" 'mvn -B -q dependency:go-offline'
304
+ [ "$SMOKE" = 1 ] && step "$L maven compile" 'mvn -B -q -DskipTests compile'
305
+ fi
306
+ fi
307
+ if [ -f build.gradle ] || [ -f build.gradle.kts ]; then
308
+ if [ -x gradlew ]; then
309
+ ensure_java
310
+ [ "$SMOKE" = 1 ] && step "$L gradle assemble" './gradlew --no-daemon -q assemble'
311
+ elif [ "$d" = . ]; then
312
+ note "$L: Gradle project without ./gradlew: install Gradle by hand"
313
+ fi
314
+ fi
315
+ if [ -f composer.json ]; then
316
+ ensure_php
317
+ step "$L composer install" 'composer install --no-interaction'
318
+ fi
319
+ [ -f mix.exs ] && note "$L: Elixir (mix.exs) is not installed automatically"
320
+ { [ -f deno.json ] || [ -f deno.jsonc ]; } && note "$L: Deno project: install deno (curl -fsSL https://deno.land/install.sh | sh)"
321
+ ls ./*.csproj ./*.sln >/dev/null 2>&1 && note "$L: .NET project: install the dotnet SDK by hand"
322
+ [ -f Package.swift ] && note "$L: Swift package: not installed automatically"
323
+ [ -f pubspec.yaml ] && note "$L: Dart/Flutter (pubspec.yaml): not installed automatically"
324
+ { [ -f docker-compose.yml ] || [ -f docker-compose.yaml ] || [ -f compose.yml ] || [ -f compose.yaml ]; } &&
325
+ note "$L: has a compose file; its services were not started (docker compose up -d)"
326
+ for f in .env.example .env.sample .env.template; do
327
+ if [ -f "$f" ] && [ ! -e .env ]; then
328
+ note "$L: expects a .env (see $f); .env files are not copied: bring it through the Vault (instabox-move-repo skill, .env files)"
329
+ break
330
+ fi
331
+ done
332
+ cd "$ROOT" || exit 2
333
+ }
334
+
335
+ # --- main ------------------------------------------------------------------------------------
336
+
337
+ status() { git status --porcelain 2>/dev/null; }
338
+ BEFORE=$(status)
339
+
340
+ project . root
341
+ # First-level subdirectories that are projects of their own (they have a lockfile or a
342
+ # standalone manifest); workspace members without one are installed by the root's install.
343
+ for d in */; do
344
+ d=${d%/}
345
+ case $d in node_modules | vendor | target | dist | build | out | .* | _*) continue ;; esac
346
+ for m in package-lock.json pnpm-lock.yaml yarn.lock bun.lockb bun.lock uv.lock poetry.lock Pipfile.lock \
347
+ requirements.txt Cargo.toml go.mod Gemfile pom.xml composer.json; do
348
+ if [ -f "$d/$m" ]; then
349
+ project "$d" "$d"
350
+ break
351
+ fi
352
+ done
353
+ done
354
+
355
+ if [ "$DRY" = 0 ] && [ "$BEFORE" != "$(status)" ]; then
356
+ note "git status changed during the installs (a lockfile or a generated file?): compare with git status and git diff"
357
+ fi
358
+
359
+ echo
360
+ if [ "$DRY" = 1 ]; then
361
+ echo "instabox-move-repo deps (dry run): ${#OK[@]} step(s) planned"
362
+ [ ${#OK[@]} -gt 0 ] && printf ' plan: %s\n' "${OK[@]}"
363
+ else
364
+ echo "instabox-move-repo deps: ${#OK[@]} step(s) ok, ${#FAILED[@]} failed"
365
+ [ ${#OK[@]} -gt 0 ] && printf ' ok: %s\n' "${OK[@]}"
366
+ fi
367
+ [ ${#FAILED[@]} -gt 0 ] && printf ' FAILED: %s\n' "${FAILED[@]}"
368
+ [ ${#NOTES[@]} -gt 0 ] && printf ' note: %s\n' "${NOTES[@]}"
369
+ [ ${#OK[@]} -eq 0 ] && [ ${#FAILED[@]} -eq 0 ] && echo " no lockfile or manifest found: nothing installed"
370
+ [ ${#FAILED[@]} -eq 0 ]