anolisa-tokenless 0.7.7

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.
Files changed (70) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +770 -0
  3. package/adapters/tokenless/claude-code/.claude-plugin/marketplace.json +15 -0
  4. package/adapters/tokenless/claude-code/.claude-plugin/plugin.json +9 -0
  5. package/adapters/tokenless/claude-code/hooks/hooks.json +38 -0
  6. package/adapters/tokenless/claude-code/scripts/detect.sh +193 -0
  7. package/adapters/tokenless/claude-code/scripts/install.sh +92 -0
  8. package/adapters/tokenless/claude-code/scripts/uninstall.sh +50 -0
  9. package/adapters/tokenless/codex/.codex-plugin/plugin.json +20 -0
  10. package/adapters/tokenless/codex/README.md +160 -0
  11. package/adapters/tokenless/codex/hooks/hooks.json +52 -0
  12. package/adapters/tokenless/codex/scripts/_common.sh +25 -0
  13. package/adapters/tokenless/codex/scripts/check-tokenless +94 -0
  14. package/adapters/tokenless/codex/scripts/compress-response +441 -0
  15. package/adapters/tokenless/codex/scripts/detect.sh +61 -0
  16. package/adapters/tokenless/codex/scripts/install.sh +151 -0
  17. package/adapters/tokenless/codex/scripts/rewrite-hook +282 -0
  18. package/adapters/tokenless/codex/scripts/tool-ready +303 -0
  19. package/adapters/tokenless/codex/scripts/uninstall.sh +78 -0
  20. package/adapters/tokenless/common/commands/tokenless-stats.toml +2 -0
  21. package/adapters/tokenless/common/cosh-extension.json +65 -0
  22. package/adapters/tokenless/common/hooks/compress_response_hook.py +487 -0
  23. package/adapters/tokenless/common/hooks/compress_schema_hook.py +144 -0
  24. package/adapters/tokenless/common/hooks/compress_toon_hook.py +160 -0
  25. package/adapters/tokenless/common/hooks/hook_utils.py +584 -0
  26. package/adapters/tokenless/common/hooks/rewrite_hook.py +223 -0
  27. package/adapters/tokenless/common/hooks/run-hook.sh +62 -0
  28. package/adapters/tokenless/common/hooks/tool_categories.json +99 -0
  29. package/adapters/tokenless/common/hooks/tool_ready_hook.sh +571 -0
  30. package/adapters/tokenless/common/tokenless-env-fix.sh +730 -0
  31. package/adapters/tokenless/common/tool-ready-spec.json +113 -0
  32. package/adapters/tokenless/dsh/cordis.patch.yml +8 -0
  33. package/adapters/tokenless/dsh/dist/index.js +399 -0
  34. package/adapters/tokenless/dsh/package.json +26 -0
  35. package/adapters/tokenless/hermes/__init__.py +572 -0
  36. package/adapters/tokenless/hermes/plugin.yaml +9 -0
  37. package/adapters/tokenless/hermes/scripts/detect.sh +80 -0
  38. package/adapters/tokenless/hermes/scripts/install.sh +60 -0
  39. package/adapters/tokenless/hermes/scripts/uninstall.sh +45 -0
  40. package/adapters/tokenless/manifest.json +147 -0
  41. package/adapters/tokenless/openclaw/dist/index.d.ts +27 -0
  42. package/adapters/tokenless/openclaw/dist/index.js +598 -0
  43. package/adapters/tokenless/openclaw/dist/tool_categories.json +99 -0
  44. package/adapters/tokenless/openclaw/index.ts +720 -0
  45. package/adapters/tokenless/openclaw/openclaw.plugin.json +40 -0
  46. package/adapters/tokenless/openclaw/package.json +24 -0
  47. package/adapters/tokenless/openclaw/scripts/detect.sh +102 -0
  48. package/adapters/tokenless/openclaw/scripts/install.sh +75 -0
  49. package/adapters/tokenless/openclaw/scripts/uninstall.sh +46 -0
  50. package/adapters/tokenless/openclaw/tsconfig.json +13 -0
  51. package/adapters/tokenless/opencode/plugin.js +246 -0
  52. package/adapters/tokenless/opencode/scripts/detect.sh +38 -0
  53. package/adapters/tokenless/opencode/scripts/install.sh +56 -0
  54. package/adapters/tokenless/opencode/scripts/uninstall.sh +29 -0
  55. package/adapters/tokenless/qoder/.qoder-plugin/plugin.json +11 -0
  56. package/adapters/tokenless/qoder/commands/tokenless-stats.md +8 -0
  57. package/adapters/tokenless/qoder/hooks/hooks.json +36 -0
  58. package/adapters/tokenless/qoder/hooks/run-hook.sh +51 -0
  59. package/adapters/tokenless/qoder/scripts/detect.sh +35 -0
  60. package/adapters/tokenless/qoder/scripts/install.sh +136 -0
  61. package/adapters/tokenless/qoder/scripts/uninstall.sh +106 -0
  62. package/adapters/tokenless/qwencode/qwen-extension.json +69 -0
  63. package/adapters/tokenless/qwencode/scripts/detect.sh +125 -0
  64. package/adapters/tokenless/qwencode/scripts/install.sh +128 -0
  65. package/adapters/tokenless/qwencode/scripts/uninstall.sh +62 -0
  66. package/bin/rtk +6 -0
  67. package/bin/tokenless +6 -0
  68. package/bin/toon +6 -0
  69. package/package.json +57 -0
  70. package/scripts/postinstall.js +208 -0
@@ -0,0 +1,730 @@
1
+ #!/usr/bin/env bash
2
+ # tokenless-env-fix — Config-driven environment auto-fix for Tool Ready feature.
3
+ # Reads dependency specs from JSON (tool-ready-spec.json or stdin) and installs
4
+ # missing packages via the declared package manager (rpm/apt/pip/uv/npm/npx/cargo/symlink/dir/path).
5
+ #
6
+ # Usage:
7
+ # tokenless-env-fix.sh fix '<json_dep_spec>' # Fix single dep (JSON object)
8
+ # tokenless-env-fix.sh fix-all '<json_array>' # Fix multiple deps (JSON array)
9
+ # tokenless-env-fix.sh fix-simple <binary> [manager] # Fix by name (defaults to detected manager)
10
+ # tokenless-env-fix.sh check # List all auto-fixable deps from spec
11
+ #
12
+ # Fix results are logged to ~/.tokenless/env-fix.log
13
+ # Duplicate fixes within 24h are skipped.
14
+
15
+ set -euo pipefail
16
+
17
+ SUDO_PREFIX=""
18
+ [ "$(id -u)" -ne 0 ] && SUDO_PREFIX="sudo -n"
19
+
20
+ FIX_LOG_DIR="${HOME}/.tokenless"
21
+ FIX_LOG="${FIX_LOG_DIR}/env-fix.log"
22
+ # Eagerly create the log dir so 2>>"$FIX_LOG" redirects in install steps
23
+ # below never silently drop their stderr because the directory is missing.
24
+ mkdir -p "$FIX_LOG_DIR" 2>/dev/null || true
25
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26
+ SPEC_FILE="${SCRIPT_DIR}/tool-ready-spec.json"
27
+
28
+ # Detect system package manager by underlying mechanism (rpm/dpkg/apk),
29
+ # then pick the best frontend within that family.
30
+ # Priority: rpm-based (Alinux) > dpkg-based > apk-based.
31
+ PACKAGE_MANAGER="rpm"
32
+ if command -v rpm &>/dev/null; then
33
+ # rpm-based system (Alinux): prefer dnf (modern), then yum (legacy)
34
+ if command -v dnf &>/dev/null; then
35
+ PACKAGE_MANAGER="dnf"
36
+ else
37
+ PACKAGE_MANAGER="yum"
38
+ fi
39
+ elif command -v dpkg &>/dev/null; then
40
+ PACKAGE_MANAGER="apt"
41
+ elif command -v apk &>/dev/null; then
42
+ PACKAGE_MANAGER="apk"
43
+ fi
44
+
45
+ # Guard to avoid repeated apt-get update across multiple install_via_system calls
46
+ _APT_UPDATED=false
47
+
48
+ # --- Logging helpers ---
49
+
50
+ log_fix() {
51
+ local dep="$1" status="$2" detail="$3"
52
+ local timestamp
53
+ timestamp=$(date +%Y-%m-%dT%H:%M:%S)
54
+ mkdir -p "$FIX_LOG_DIR"
55
+ echo "${timestamp} fix=${dep} status=${status} detail=${detail}" >> "$FIX_LOG"
56
+ }
57
+
58
+ was_recently_fixed() {
59
+ local dep="$1"
60
+ if [ ! -f "$FIX_LOG" ]; then return 1; fi
61
+ local cutoff
62
+ cutoff=$(date -d '24 hours ago' +%Y-%m-%dT%H:%M:%S 2>/dev/null || date -v-24H +%Y-%m-%dT%H:%M:%S 2>/dev/null || echo "")
63
+ if [ -z "$cutoff" ]; then return 1; fi
64
+ # Use grep -F (fixed-string, not regex) to match the exact dep name —
65
+ # dep names may contain '.' (e.g. "python3.11") which is a regex wildcard.
66
+ awk -v c="$cutoff" '$0 >= c {print}' "$FIX_LOG" 2>/dev/null \
67
+ | grep -Fq "fix=${dep} status=success"
68
+ }
69
+
70
+ classify_recent_failure() {
71
+ if [ ! -f "$FIX_LOG" ]; then
72
+ echo "unknown failure"
73
+ return
74
+ fi
75
+ local recent
76
+ recent=$(tail -n 80 "$FIX_LOG" 2>/dev/null || true)
77
+ if echo "$recent" | grep -Eiq 'sudo:.*(password|required|try again|authentication|not in the sudoers)|a password is required|no tty present'; then
78
+ echo "auth failed: non-interactive sudo unavailable"
79
+ elif echo "$recent" | grep -Eiq '(Could not resolve|Temporary failure resolving|Name or service not known|Network is unreachable|Connection timed out|Connection refused|TLS|SSL|certificate|curl: \([0-9]+\)|wget: unable|Failed to connect)'; then
80
+ echo "network failed"
81
+ elif echo "$recent" | grep -Eiq '(Permission denied|Operation not permitted)'; then
82
+ echo "permission denied"
83
+ else
84
+ echo "unknown failure"
85
+ fi
86
+ }
87
+
88
+ # --- Normalize a dep spec to object format ---
89
+ # Input: string like "jq" → {binary:"jq",package:"jq",manager:"rpm"}
90
+ # Input: object like {"binary":"jq",...} → pass through
91
+ # Output: JSON object
92
+
93
+ normalize_dep() {
94
+ local input="$1"
95
+ # If starts with {, it's already an object
96
+ if echo "$input" | jq -e 'type == "object"' >/dev/null 2>&1; then
97
+ echo "$input"
98
+ return
99
+ fi
100
+ # It's a string — convert to object with defaults
101
+ # Handle version constraints: "rtk>=0.35" → {binary:"rtk",version:">=0.35",...}
102
+ local base_name version_constraint
103
+ base_name=$(echo "$input" | sed 's/[>=<].*//')
104
+ version_constraint=$(echo "$input" | grep -oE '[>=<]+[0-9.]+' || echo "")
105
+ if [ -n "$version_constraint" ]; then
106
+ jq -n --arg bn "$base_name" --arg vc "$version_constraint" --arg pk "$base_name" --arg mgr "$PACKAGE_MANAGER" \
107
+ '{binary:$bn, version:$vc, package:$pk, manager:$mgr}'
108
+ else
109
+ jq -n --arg bn "$base_name" --arg pk "$base_name" --arg mgr "$PACKAGE_MANAGER" \
110
+ '{binary:$bn, package:$pk, manager:$mgr}'
111
+ fi
112
+ }
113
+
114
+ # --- Input validation ---
115
+ # Reject names that could be used for command injection or supply-chain attacks.
116
+
117
+ validate_name() {
118
+ local val="$1" label="$2"
119
+ if [ -z "$val" ]; then
120
+ echo "[tokenless-env-fix] BLOCKED: empty ${label}"
121
+ return 1
122
+ fi
123
+ if [ "${#val}" -gt 128 ]; then
124
+ echo "[tokenless-env-fix] BLOCKED: ${label} too long (${#val} chars): ${val:0:32}..."
125
+ return 1
126
+ fi
127
+ if ! echo "$val" | grep -qE '^[a-zA-Z0-9][a-zA-Z0-9._@+-]*$'; then
128
+ echo "[tokenless-env-fix] BLOCKED: invalid ${label}: ${val}"
129
+ return 1
130
+ fi
131
+ }
132
+
133
+ # is_trusted_source_path — accept system anolisa install dirs unconditionally;
134
+ # for $HOME-relative paths, resolve home via the passwd database (NSS) instead
135
+ # of $HOME and require uid ownership to match the current user (or root).
136
+ # Reading $HOME directly is unsafe — a parent process can override it to
137
+ # redirect trust evaluation toward an attacker-controlled directory.
138
+ is_trusted_source_path() {
139
+ local p="$1"
140
+ case "$p" in
141
+ /usr/lib/anolisa/*|/usr/libexec/anolisa/*|/usr/share/anolisa/*|/usr/local/lib/anolisa/*|/usr/local/libexec/anolisa/*|/usr/local/share/anolisa/*)
142
+ return 0
143
+ ;;
144
+ esac
145
+
146
+ # Resolve the real home from the passwd database. If getent is missing
147
+ # (minimal containers without nsswitch), refuse to trust any $HOME-relative
148
+ # path rather than fall back to $HOME.
149
+ local real_home=""
150
+ if command -v getent &>/dev/null; then
151
+ real_home=$(getent passwd "$(id -u)" 2>/dev/null | awk -F: 'NR==1{print $6}')
152
+ fi
153
+ if [ -z "$real_home" ]; then
154
+ return 1
155
+ fi
156
+ case "$p" in
157
+ "$real_home"/.local/share/anolisa/*)
158
+ local owner_uid
159
+ # Linux uses `stat -c`, BSD/macOS uses `stat -f` — try both.
160
+ owner_uid=$(stat -c '%u' "$p" 2>/dev/null || stat -f '%u' "$p" 2>/dev/null || echo "")
161
+ if [ -z "$owner_uid" ]; then
162
+ return 1
163
+ fi
164
+ if [ "$owner_uid" != "$(id -u)" ] && [ "$owner_uid" != "0" ]; then
165
+ return 1
166
+ fi
167
+ # Check the parent directory for TOCTOU protection.
168
+ local _pd
169
+ _pd=$(dirname "$p")
170
+ local _pdo
171
+ _pdo=$(stat -c '%u' "$_pd" 2>/dev/null || stat -f '%u' "$_pd" 2>/dev/null || echo "-1")
172
+ if [ "$_pdo" != "$(id -u)" ] && [ "$_pdo" != "0" ]; then
173
+ return 1
174
+ fi
175
+ local _pdp
176
+ _pdp=$(stat -c '%a' "$_pd" 2>/dev/null || stat -f '%Lp' "$_pd" 2>/dev/null || echo "777")
177
+ local _pdop="${_pdp: -1}"
178
+ if (( _pdop & 2 )); then
179
+ return 1
180
+ fi
181
+ return 0
182
+ ;;
183
+ esac
184
+ return 1
185
+ }
186
+
187
+ # --- Package manager install functions ---
188
+ # Each installs a package via the declared manager.
189
+ # Returns 0 on success, 1 on failure.
190
+
191
+ install_via_system() {
192
+ local package="$1"
193
+ # Refresh package index before first install on apt-based systems
194
+ case "$PACKAGE_MANAGER" in
195
+ apt) if [ "$_APT_UPDATED" != true ]; then $SUDO_PREFIX apt-get update -qq 2>>"$FIX_LOG" || log_fix "apt-get-update" "failed" "network issue?"; _APT_UPDATED=true; fi ;;
196
+ esac
197
+ # Try detected system manager first, then others as fallback (Alinux dnf/yum > apt > apk).
198
+ # Stderr is appended to $FIX_LOG (instead of 2>/dev/null) so a chain of "all
199
+ # managers failed" leaves a diagnosable trail rather than a silent NOT_READY.
200
+ case "$PACKAGE_MANAGER" in
201
+ dnf) $SUDO_PREFIX dnf install -y "$package" 2>>"$FIX_LOG" || $SUDO_PREFIX yum install -y "$package" 2>>"$FIX_LOG" || $SUDO_PREFIX apt-get install -y "$package" 2>>"$FIX_LOG" || $SUDO_PREFIX apk add "$package" 2>>"$FIX_LOG" ;;
202
+ yum) $SUDO_PREFIX yum install -y "$package" 2>>"$FIX_LOG" || $SUDO_PREFIX dnf install -y "$package" 2>>"$FIX_LOG" || $SUDO_PREFIX apt-get install -y "$package" 2>>"$FIX_LOG" || $SUDO_PREFIX apk add "$package" 2>>"$FIX_LOG" ;;
203
+ apt) $SUDO_PREFIX apt-get install -y "$package" 2>>"$FIX_LOG" || $SUDO_PREFIX dnf install -y "$package" 2>>"$FIX_LOG" || $SUDO_PREFIX yum install -y "$package" 2>>"$FIX_LOG" || $SUDO_PREFIX apk add "$package" 2>>"$FIX_LOG" ;;
204
+ apk) $SUDO_PREFIX apk add "$package" 2>>"$FIX_LOG" || $SUDO_PREFIX dnf install -y "$package" 2>>"$FIX_LOG" || $SUDO_PREFIX yum install -y "$package" 2>>"$FIX_LOG" || $SUDO_PREFIX apt-get install -y "$package" 2>>"$FIX_LOG" ;;
205
+ *) $SUDO_PREFIX yum install -y "$package" 2>>"$FIX_LOG" || $SUDO_PREFIX dnf install -y "$package" 2>>"$FIX_LOG" || $SUDO_PREFIX apt-get install -y "$package" 2>>"$FIX_LOG" || $SUDO_PREFIX apk add "$package" 2>>"$FIX_LOG" ;;
206
+ esac
207
+ }
208
+
209
+ install_via_rpm() {
210
+ # Stderr from each retry is appended to $FIX_LOG (rather than dropped via
211
+ # 2>/dev/null) so a chain of "all rpm frontends failed" leaves diagnosable
212
+ # output for the user.
213
+ $SUDO_PREFIX yum install -y "$1" 2>>"$FIX_LOG" || $SUDO_PREFIX dnf install -y "$1" 2>>"$FIX_LOG" || $SUDO_PREFIX rpm -ivh "$1" 2>>"$FIX_LOG"
214
+ }
215
+
216
+ install_via_pip() {
217
+ local package="$1"
218
+ local pip_name="${2:-$package}"
219
+ local pip_cmd=""
220
+ command -v pip3 &>/dev/null && pip_cmd="pip3" || { command -v pip &>/dev/null && pip_cmd="pip"; }
221
+ if [ -z "$pip_cmd" ]; then return 1; fi
222
+
223
+ # Stderr from each retry is appended to $FIX_LOG (rather than discarded
224
+ # via 2>/dev/null) so a four-stage failure leaves diagnosable output for
225
+ # the user.
226
+
227
+ # Stage 1: default mirror
228
+ $pip_cmd install "$pip_name" 2>>"$FIX_LOG"
229
+ hash -r
230
+ if command -v "$package" &>/dev/null; then return 0; fi
231
+
232
+ # pip reported success but binary missing (stale metadata) — uninstall + reinstall
233
+ $pip_cmd uninstall -y "$pip_name" 2>>"$FIX_LOG" || true
234
+ $pip_cmd install "$pip_name" 2>>"$FIX_LOG"
235
+ hash -r
236
+ if command -v "$package" &>/dev/null; then return 0; fi
237
+
238
+ # Stage 2: purge cache and retry
239
+ $pip_cmd cache purge 2>>"$FIX_LOG"
240
+ $pip_cmd uninstall -y "$pip_name" 2>>"$FIX_LOG" || true
241
+ $pip_cmd install --no-cache-dir "$pip_name" 2>>"$FIX_LOG"
242
+ hash -r
243
+ if command -v "$package" &>/dev/null; then return 0; fi
244
+
245
+ # Stage 3: fallback to official PyPI (mirror may be broken/sync-lag)
246
+ $pip_cmd uninstall -y "$pip_name" 2>>"$FIX_LOG" || true
247
+ $pip_cmd install --no-cache-dir --index-url https://pypi.org/simple/ "$pip_name" 2>>"$FIX_LOG"
248
+ hash -r
249
+ if command -v "$package" &>/dev/null; then return 0; fi
250
+
251
+ return 1
252
+ }
253
+
254
+ install_via_uv() {
255
+ local package="$1"
256
+ local uv_name="${2:-$package}"
257
+ # Append stderr to $FIX_LOG so install failures are diagnosable instead of
258
+ # silently producing a NOT_READY downstream.
259
+ uv tool install "$uv_name" 2>>"$FIX_LOG" || uv pip install "$uv_name" 2>>"$FIX_LOG"
260
+ }
261
+
262
+ install_via_npm() {
263
+ local package="$1"
264
+ local npm_name="${2:-$package}"
265
+ $SUDO_PREFIX npm install -g "$npm_name" 2>>"$FIX_LOG"
266
+ }
267
+
268
+ install_via_npx() {
269
+ # npx doesn't install — just verifies availability. Stderr suppressed
270
+ # because the "package not yet cached" message is normal noise here.
271
+ local package="$1"
272
+ npx -y "$package" --version 2>/dev/null >/dev/null
273
+ }
274
+
275
+ install_via_cargo() {
276
+ cargo install "$1" --locked 2>>"$FIX_LOG"
277
+ }
278
+
279
+ install_via_cargo_build() {
280
+ # Build from local Cargo.toml manifest, copy binary to /usr/local/bin
281
+ local manifest="$1"
282
+ local binary="$2"
283
+ local features="${3:-}"
284
+ if [ ! -f "$manifest" ]; then
285
+ echo "[tokenless-env-fix] BLOCKED: manifest not found: $manifest"
286
+ return 1
287
+ fi
288
+ # Reject untrusted manifests — building from a path the current uid does
289
+ # not own (or worse, from an attacker-writable $HOME path) would let an
290
+ # attacker bake arbitrary code into /usr/local/bin via build.rs.
291
+ if ! is_trusted_source_path "$manifest"; then
292
+ echo "[tokenless-env-fix] BLOCKED: cargo_build manifest not in trusted path or wrong owner: $manifest"
293
+ return 1
294
+ fi
295
+ local -a cargo_args=("--release" "--manifest-path" "$manifest")
296
+ if [ -n "$features" ]; then
297
+ cargo_args+=("--features" "$features")
298
+ fi
299
+ # Every step below must hard-fail: cargo build, the post-build binary
300
+ # check, and the cp/chmod install. Previously cp/chmod used
301
+ # `2>/dev/null || true` and the binary check was best-effort, so the
302
+ # function returned 0 even when nothing was installed — env-check then
303
+ # reported NOT_READY with no diagnostic trail. Return 1 on any failure
304
+ # and log stderr to $FIX_LOG so the fallback chain (or the user) has
305
+ # something to work with.
306
+ cargo build "${cargo_args[@]}" 2>>"$FIX_LOG" || return 1
307
+ local target_dir
308
+ target_dir=$(dirname "$manifest")/target/release
309
+ if [ ! -x "${target_dir}/${binary}" ]; then
310
+ echo "[tokenless-env-fix] BLOCKED: cargo_build produced no binary at ${target_dir}/${binary}"
311
+ return 1
312
+ fi
313
+ $SUDO_PREFIX cp "${target_dir}/${binary}" /usr/local/bin/"${binary}" 2>>"$FIX_LOG" || return 1
314
+ $SUDO_PREFIX chmod +x /usr/local/bin/"${binary}" 2>>"$FIX_LOG" || return 1
315
+ }
316
+
317
+ install_via_symlink() {
318
+ local binary="$1"
319
+ local source="$2"
320
+ if [ ! -f "$source" ]; then
321
+ echo "[tokenless-env-fix] BLOCKED: symlink source does not exist: $source"
322
+ return 1
323
+ fi
324
+ # Reject sources outside the trusted prefix list, and require uid
325
+ # ownership to match the current user (or root) for any $HOME path —
326
+ # $HOME is env-controllable, so a plain path whitelist is not enough.
327
+ if ! is_trusted_source_path "$source"; then
328
+ echo "[tokenless-env-fix] BLOCKED: symlink source not in trusted path or wrong owner: $source"
329
+ return 1
330
+ fi
331
+ # Hard-fail on ln/chmod errors (formerly `|| true`, which caused
332
+ # install_via_symlink to report success even when the symlink wasn't
333
+ # actually created — env-check then reported NOT_READY with no trail).
334
+ $SUDO_PREFIX ln -sf "$source" /usr/local/bin/"$binary" 2>>"$FIX_LOG" || return 1
335
+ # The +x must live on the source file (symlinks have no permissions of
336
+ # their own). Try without sudo first — $HOME-relative trusted paths are
337
+ # owned by the current uid — then escalate for root-owned system paths.
338
+ # Only attempt the sudo path when $SUDO_PREFIX is non-empty; otherwise
339
+ # the retry is identical to the first attempt and would always fail.
340
+ if [ ! -x "$source" ]; then
341
+ chmod +x "$source" 2>>"$FIX_LOG" || \
342
+ { [ -n "$SUDO_PREFIX" ] && $SUDO_PREFIX chmod +x "$source" 2>>"$FIX_LOG"; } || \
343
+ return 1
344
+ fi
345
+ }
346
+
347
+ install_via_path() {
348
+ local path_dir="$1"
349
+ # Reject anything that isn't an absolute path with safe characters.
350
+ # path_dir is appended verbatim into shell rc files, so an unconstrained
351
+ # value (e.g. "/x\"; rm -rf $HOME #") would inject arbitrary shell into
352
+ # every future login. Spaces are intentionally excluded — system install
353
+ # paths never contain spaces, and spaces in PATH assignments create
354
+ # word-splitting ambiguity in shell rc files.
355
+ if [ -z "$path_dir" ] || [ "${path_dir:0:1}" != "/" ]; then
356
+ echo "[tokenless-env-fix] BLOCKED: install_via_path requires an absolute path, got: ${path_dir}"
357
+ return 1
358
+ fi
359
+ if ! echo "$path_dir" | grep -qE '^/[A-Za-z0-9._/+-]+$'; then
360
+ echo "[tokenless-env-fix] BLOCKED: install_via_path path contains unsafe characters: ${path_dir}"
361
+ return 1
362
+ fi
363
+ if [[ ":$PATH:" != *":${path_dir}:"* ]]; then
364
+ export PATH="${path_dir}:${PATH}"
365
+ # Resolve home from passwd (not $HOME) to prevent an attacker-controlled
366
+ # $HOME from redirecting rc-file writes. Falls back to $HOME only when
367
+ # getent is unavailable — the is_trusted_source_path guard already
368
+ # covers this path. NOTE: Rust-side home.rs uses getpwuid_r and never
369
+ # falls back to $HOME; the shell-side fallback here is a tolerated
370
+ # inconsistency because install_via_path is only called after the
371
+ # stricter is_trusted_source_path check passes.
372
+ local real_home="$HOME"
373
+ if command -v getent &>/dev/null; then
374
+ real_home=$(getent passwd "$(id -u)" 2>/dev/null | awk -F: 'NR==1{print $6}') || true
375
+ fi
376
+ [ -n "$real_home" ] || real_home="$HOME"
377
+ local shell_rc="${real_home}/.bashrc"
378
+ [ -f "${real_home}/.zshrc" ] && shell_rc="${real_home}/.zshrc"
379
+ if ! grep -Fq "export PATH=\"${path_dir}" "$shell_rc" 2>/dev/null; then
380
+ echo "[tokenless-env-fix] adding ${path_dir} to PATH in ${shell_rc}"
381
+ echo "export PATH=\"${path_dir}:\$PATH\"" >> "$shell_rc"
382
+ fi
383
+ fi
384
+ }
385
+
386
+ install_via_dir() {
387
+ mkdir -p "$1"
388
+ }
389
+
390
+ install_via_curl_pipe_sh() {
391
+ local url="$1"
392
+ local args="${2:-}"
393
+ local timeout_secs="${3:-120}"
394
+ # Only allow HTTPS URLs from trusted install-script hosts. The github
395
+ # entries require a concrete owner/repo path segment (no root-level
396
+ # github.com/ or raw.githubusercontent.com/), so an attacker who only
397
+ # controls a repo cannot satisfy the regex with a single-segment URL.
398
+ local allowed_domains='^https://('
399
+ allowed_domains+='raw\.githubusercontent\.com/[A-Za-z0-9._-]+/[A-Za-z0-9._-]+/[A-Za-z0-9._-]+/'
400
+ allowed_domains+='|github\.com/[A-Za-z0-9._-]+/[A-Za-z0-9._-]+/raw/'
401
+ allowed_domains+='|sh\.rustup\.rs(/|$)'
402
+ allowed_domains+='|get\.docker\.com(/|$)'
403
+ allowed_domains+='|cli\.run\.nu(/|$)'
404
+ allowed_domains+='|get\.starship\.rs(/|$)'
405
+ allowed_domains+='|astral\.sh(/|$)'
406
+ allowed_domains+=')'
407
+ if ! echo "$url" | grep -qE "$allowed_domains"; then
408
+ echo "[tokenless-env-fix] BLOCKED: curl|sh denied — untrusted or non-HTTPS URL: $url"
409
+ return 1
410
+ fi
411
+ echo "[tokenless-env-fix] NOTE: executing remote script from $url (timeout: ${timeout_secs}s)"
412
+ # Append curl/wget stderr to $FIX_LOG (instead of /dev/null) so a silent
413
+ # network failure leaves a diagnostic behind. PIPESTATUS is then inspected
414
+ # to surface a non-zero downloader exit — a bare `curl … | sh` would
415
+ # otherwise return 0 whenever `sh` consumes a truncated/empty pipe.
416
+ local rc
417
+ if command -v curl &>/dev/null; then
418
+ if [ -n "$args" ]; then
419
+ timeout "$timeout_secs" curl -fsSL --max-redirs 5 "$url" 2>>"$FIX_LOG" | timeout "$timeout_secs" sh -s -- "$args"
420
+ else
421
+ timeout "$timeout_secs" curl -fsSL --max-redirs 5 "$url" 2>>"$FIX_LOG" | timeout "$timeout_secs" sh
422
+ fi
423
+ rc=("${PIPESTATUS[@]}")
424
+ if [ "${rc[0]:-1}" -ne 0 ]; then
425
+ echo "[tokenless-env-fix] curl failed for $url (exit ${rc[0]:-?}); see $FIX_LOG" >&2
426
+ return 1
427
+ fi
428
+ return "${rc[1]:-0}"
429
+ elif command -v wget &>/dev/null; then
430
+ if [ -n "$args" ]; then
431
+ timeout "$timeout_secs" wget --max-redirect=5 -qO- "$url" 2>>"$FIX_LOG" | timeout "$timeout_secs" sh -s -- "$args"
432
+ else
433
+ timeout "$timeout_secs" wget --max-redirect=5 -qO- "$url" 2>>"$FIX_LOG" | timeout "$timeout_secs" sh
434
+ fi
435
+ rc=("${PIPESTATUS[@]}")
436
+ if [ "${rc[0]:-1}" -ne 0 ]; then
437
+ echo "[tokenless-env-fix] wget failed for $url (exit ${rc[0]:-?}); see $FIX_LOG" >&2
438
+ return 1
439
+ fi
440
+ return "${rc[1]:-0}"
441
+ else
442
+ return 1
443
+ fi
444
+ }
445
+
446
+ # --- Core fix logic ---
447
+ # Given a normalized dep JSON object, attempt to install via declared manager + fallbacks.
448
+
449
+ fix_dep() {
450
+ local dep_json="$1"
451
+ local binary package manager version pip_name uv_name npm_name use_npx url args
452
+
453
+ binary=$(echo "$dep_json" | jq -r '.binary // empty')
454
+ package=$(echo "$dep_json" | jq -r '.package // empty')
455
+ # Resolve per-manager package overrides: apt_package/apk_package
456
+ case "$PACKAGE_MANAGER" in
457
+ apt) apt_pkg=$(echo "$dep_json" | jq -r '.apt_package // empty'); [ -n "$apt_pkg" ] && package="$apt_pkg" ;;
458
+ apk) apk_pkg=$(echo "$dep_json" | jq -r '.apk_package // empty'); [ -n "$apk_pkg" ] && package="$apk_pkg" ;;
459
+ esac
460
+ manager=$(echo "$dep_json" | jq -r '.manager // "rpm"')
461
+ version=$(echo "$dep_json" | jq -r '.version // empty')
462
+ pip_name=$(echo "$dep_json" | jq -r '.pip_name // empty')
463
+ uv_name=$(echo "$dep_json" | jq -r '.uv_name // empty')
464
+ npm_name=$(echo "$dep_json" | jq -r '.npm_name // empty')
465
+ use_npx=$(echo "$dep_json" | jq -r '.use_npx // false')
466
+ url=$(echo "$dep_json" | jq -r '.url // empty')
467
+ args=$(echo "$dep_json" | jq -r '.args // empty')
468
+
469
+ # Validate names before any install action
470
+ validate_name "$binary" "binary" || return 1
471
+ validate_name "$package" "package" || return 1
472
+
473
+ # Fill defaults: pip_name/uv_name/npm_name default to package
474
+ [ -z "$pip_name" ] && pip_name="$package"
475
+ [ -z "$uv_name" ] && uv_name="$package"
476
+ [ -z "$npm_name" ] && npm_name="$package"
477
+
478
+ # Validate derived names
479
+ [ -n "$pip_name" ] && { validate_name "$pip_name" "pip_name" || return 1; }
480
+ [ -n "$uv_name" ] && { validate_name "$uv_name" "uv_name" || return 1; }
481
+ [ -n "$npm_name" ] && { validate_name "$npm_name" "npm_name" || return 1; }
482
+
483
+ # Skip if already available (clear hash cache first)
484
+ hash -r
485
+ if command -v "$binary" &>/dev/null; then
486
+ # Check version constraint if present
487
+ if [ -n "$version" ]; then
488
+ local constraint_ver installed_ver
489
+ constraint_ver=$(echo "$version" | sed 's/[>=<]//g')
490
+ installed_ver=$("$binary" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "0.0.0")
491
+
492
+ if [ -n "$installed_ver" ] && [ "$installed_ver" != "0.0.0" ]; then
493
+ # Simple >= check
494
+ local i_major i_minor i_patch r_major r_minor r_patch
495
+ IFS='.' read -r i_major i_minor i_patch <<< "$installed_ver"
496
+ IFS='.' read -r r_major r_minor r_patch <<< "$constraint_ver"
497
+ i_major=${i_major:-0}; i_minor=${i_minor:-0}; i_patch=${i_patch:-0}
498
+ r_major=${r_major:-0}; r_minor=${r_minor:-0}; r_patch=${r_patch:-0}
499
+ if [ "$i_major" -gt "$r_major" ] || \
500
+ { [ "$i_major" -eq "$r_major" ] && [ "$i_minor" -gt "$r_minor" ]; } || \
501
+ { [ "$i_major" -eq "$r_major" ] && [ "$i_minor" -eq "$r_minor" ] && [ "$i_patch" -ge "$r_patch" ]; }; then
502
+ echo "[tokenless-env-fix] ${binary}: already available (v${installed_ver} satisfies ${version})"
503
+ return 0
504
+ fi
505
+ fi
506
+ else
507
+ echo "[tokenless-env-fix] ${binary}: already available"
508
+ return 0
509
+ fi
510
+ fi
511
+
512
+ # Skip if recently fixed successfully AND binary still present
513
+ # (handles the case where dep was fixed then later uninstalled)
514
+ if was_recently_fixed "$binary"; then
515
+ hash -r
516
+ if command -v "$binary" &>/dev/null; then
517
+ echo "[tokenless-env-fix] ${binary}: skipped (recently fixed, still present)"
518
+ return 0
519
+ fi
520
+ echo "[tokenless-env-fix] ${binary}: recently fixed but missing, re-installing"
521
+ fi
522
+
523
+ echo "[tokenless-env-fix] ${binary}: attempting install via ${manager}..."
524
+
525
+ # --- Primary install via declared manager ---
526
+ local primary_ok=false
527
+ case "$manager" in
528
+ auto|rpm|apt|dnf|yum|apk) install_via_system "$package" && primary_ok=true ;;
529
+ pip) install_via_pip "$package" "$pip_name" && primary_ok=true ;;
530
+ uv) install_via_uv "$package" "$uv_name" && primary_ok=true ;;
531
+ npm) install_via_npm "$package" "$npm_name" && primary_ok=true ;;
532
+ npx) install_via_npx "$package" && primary_ok=true ;;
533
+ cargo) install_via_cargo "$package" && primary_ok=true ;;
534
+ symlink) local src; src=$(echo "$dep_json" | jq -r '.source // empty'); install_via_symlink "$binary" "$src" && primary_ok=true ;;
535
+ path) local pdir; pdir=$(echo "$dep_json" | jq -r '.source // "/usr/libexec/anolisa/tokenless"'); if [ ! -d "$pdir" ]; then pdir="/usr/lib/anolisa/tokenless"; fi; install_via_path "$pdir" && primary_ok=true ;;
536
+ dir) local dpath; dpath=$(echo "$dep_json" | jq -r '.source // empty'); install_via_dir "$dpath" && primary_ok=true ;;
537
+ curl_pipe_sh) [ -n "$url" ] && install_via_curl_pipe_sh "$url" "$args" && primary_ok=true ;;
538
+ *)
539
+ echo "[tokenless-env-fix] ${binary}: unknown manager '${manager}'"
540
+ ;;
541
+ esac
542
+
543
+ # Verify primary install (clear hash cache so newly installed binaries are discoverable)
544
+ hash -r
545
+ if $primary_ok && command -v "$binary" &>/dev/null; then
546
+ log_fix "$binary" "success" "installed via ${manager}"
547
+ echo "[tokenless-env-fix] ${binary}: installed via ${manager}"
548
+ return 0
549
+ fi
550
+
551
+ # --- Fallback strategies ---
552
+ local fallbacks
553
+ fallbacks=$(echo "$dep_json" | jq -c '.fallback // []' 2>/dev/null || echo '[]')
554
+ local fallback_count
555
+ fallback_count=$(echo "$fallbacks" | jq 'length' 2>/dev/null || echo 0)
556
+
557
+ if [ "$fallback_count" -gt 0 ]; then
558
+ for i in $(seq 0 $((fallback_count - 1))); do
559
+ local fb_method fb_package fb_binary fb_source fb_manifest fb_features fb_url fb_args
560
+ fb_method=$(echo "$fallbacks" | jq -r ".[$i].method // empty")
561
+ fb_package=$(echo "$fallbacks" | jq -r ".[$i].package // empty")
562
+ fb_binary=$(echo "$fallbacks" | jq -r \
563
+ --arg default_binary "$binary" ".[$i].binary // \$default_binary")
564
+ fb_source=$(echo "$fallbacks" | jq -r ".[$i].source // empty")
565
+ fb_manifest=$(echo "$fallbacks" | jq -r ".[$i].manifest // empty")
566
+ fb_features=$(echo "$fallbacks" | jq -r ".[$i].features // empty")
567
+ fb_url=$(echo "$fallbacks" | jq -r ".[$i].url // empty")
568
+ fb_args=$(echo "$fallbacks" | jq -r ".[$i].args // empty")
569
+
570
+ echo "[tokenless-env-fix] ${binary}: trying fallback ${fb_method}..."
571
+
572
+ local fb_ok=false
573
+ case "$fb_method" in
574
+ rpm|apt|dnf|yum|apk) [ -n "$fb_package" ] && install_via_system "$fb_package" && fb_ok=true ;;
575
+ pip) [ -n "$fb_package" ] && install_via_pip "$fb_package" && fb_ok=true ;;
576
+ uv) [ -n "$fb_package" ] && install_via_uv "$fb_package" && fb_ok=true ;;
577
+ npm) [ -n "$fb_package" ] && install_via_npm "$fb_package" && fb_ok=true ;;
578
+ npx) [ -n "$fb_package" ] && install_via_npx "$fb_package" && fb_ok=true ;;
579
+ cargo) [ -n "$fb_package" ] && install_via_cargo "$fb_package" && fb_ok=true ;;
580
+ cargo_build) [ -n "$fb_manifest" ] && install_via_cargo_build "$fb_manifest" "$fb_binary" "$fb_features" && fb_ok=true ;;
581
+ symlink) [ -n "$fb_source" ] && install_via_symlink "$fb_binary" "$fb_source" && fb_ok=true ;;
582
+ path) local _fb_pdir="${fb_source:-/usr/libexec/anolisa/tokenless}"; if [ ! -d "$_fb_pdir" ]; then _fb_pdir="/usr/lib/anolisa/tokenless"; fi; install_via_path "$_fb_pdir" && fb_ok=true ;;
583
+ dir) [ -n "$fb_source" ] && install_via_dir "$fb_source" && fb_ok=true ;;
584
+ curl_pipe_sh) [ -n "$fb_url" ] && install_via_curl_pipe_sh "$fb_url" "$fb_args" && fb_ok=true ;;
585
+ *) echo "[tokenless-env-fix] ${binary}: unknown fallback method '${fb_method}'" ;;
586
+ esac
587
+
588
+ hash -r
589
+ if $fb_ok && command -v "$fb_binary" &>/dev/null; then
590
+ log_fix "$binary" "success" "installed via fallback ${fb_method}"
591
+ echo "[tokenless-env-fix] ${binary}: installed via fallback ${fb_method}"
592
+ return 0
593
+ fi
594
+ done
595
+ fi
596
+
597
+ # All strategies failed
598
+ failure_detail=$(classify_recent_failure)
599
+ log_fix "$binary" "failed" "${failure_detail} (primary: ${manager}, fallbacks: ${fallback_count})"
600
+ echo "[tokenless-env-fix] ${binary}: install failed — ${failure_detail} (primary: ${manager}, ${fallback_count} fallbacks exhausted)"
601
+ return 1
602
+ }
603
+
604
+ # --- Fix from spec file ---
605
+ # Read all dep entries from tool-ready-spec.json for a given tool
606
+
607
+ fix_tool_from_spec() {
608
+ local tool_name="$1"
609
+ if [ ! -f "$SPEC_FILE" ]; then
610
+ echo "[tokenless-env-fix] spec file not found: $SPEC_FILE"
611
+ return 1
612
+ fi
613
+ local tool_spec
614
+ tool_spec=$(jq -c --arg key "$tool_name" '.[$key]' "$SPEC_FILE" 2>/dev/null || echo 'null')
615
+ if [ "$tool_spec" = "null" ] || [ -z "$tool_spec" ]; then
616
+ echo "[tokenless-env-fix] no spec for tool: $tool_name"
617
+ return 0
618
+ fi
619
+
620
+ # Collect all dep entries from required + recommended
621
+ local all_deps
622
+ all_deps=$(echo "$tool_spec" | jq -c --arg mgr "$PACKAGE_MANAGER" '[(.required // []) + (.recommended // []) | .[] | if type == "string" then (if test("[>=<]") then {binary: (split("[>=<]") | .[0]), version: (capture("[>=<]+[0-9.]+"; "g") | .[0]), package: (split("[>=<]") | .[0]), manager: $mgr} else {binary: ., package: ., manager: $mgr} end) else . end]' 2>/dev/null || echo '[]')
623
+
624
+ local count
625
+ count=$(echo "$all_deps" | jq 'length' 2>/dev/null || echo 0)
626
+
627
+ for i in $(seq 0 $((count - 1))); do
628
+ local dep_json
629
+ dep_json=$(echo "$all_deps" | jq -c ".[$i]")
630
+ fix_dep "$dep_json" || true
631
+ done
632
+ }
633
+
634
+ # --- Main entry point ---
635
+
636
+ case "${1:-}" in
637
+ fix)
638
+ if [ -z "${2:-}" ]; then
639
+ echo "Usage: tokenless-env-fix.sh fix '<json_dep_spec>'"
640
+ echo " tokenless-env-fix.sh fix-simple <binary> [manager]"
641
+ exit 1
642
+ fi
643
+ # Determine if input is JSON or a simple name
644
+ if echo "$2" | jq -e 'type == "object"' >/dev/null 2>&1; then
645
+ fix_dep "$2"
646
+ else
647
+ # Simple name — normalize to object with optional manager
648
+ manager="${3:-$PACKAGE_MANAGER}"
649
+ dep_json=$(jq -n --arg bn "$2" --arg pk "$2" --arg mgr "$manager" '{binary:$bn, package:$pk, manager:$mgr}')
650
+ fix_dep "$dep_json"
651
+ fi
652
+ ;;
653
+ fix-simple)
654
+ # Fix by binary name with optional manager (defaults to detected PACKAGE_MANAGER)
655
+ if [ -z "${2:-}" ]; then
656
+ echo "Usage: tokenless-env-fix.sh fix-simple <binary> [manager]"
657
+ exit 1
658
+ fi
659
+ manager="${3:-$PACKAGE_MANAGER}"
660
+ dep_json=$(jq -n --arg bn "$2" --arg pk "$2" --arg mgr "$manager" '{binary:$bn, package:$pk, manager:$mgr}')
661
+ fix_dep "$dep_json"
662
+ ;;
663
+ fix-all)
664
+ input=""
665
+ if [ -n "${2:-}" ] && [ "$2" != "-" ]; then
666
+ input="$2"
667
+ else
668
+ input=$(cat)
669
+ fi
670
+ # Normalize all entries
671
+ normalized=$(echo "$input" | jq -c --arg mgr "$PACKAGE_MANAGER" '[.[] | if type == "string" then {binary: ., package: ., manager: $mgr} else . end]' 2>/dev/null || echo '[]')
672
+ count=$(echo "$normalized" | jq 'length' 2>/dev/null || echo 0)
673
+ for i in $(seq 0 $((count - 1))); do
674
+ fix_dep "$(echo "$normalized" | jq -c ".[$i]")" || true
675
+ done
676
+ ;;
677
+ fix-tool)
678
+ # Fix all deps for a tool from spec file
679
+ if [ -z "${2:-}" ]; then
680
+ echo "Usage: tokenless-env-fix.sh fix-tool <tool_name>"
681
+ exit 1
682
+ fi
683
+ fix_tool_from_spec "$2"
684
+ ;;
685
+ check)
686
+ if [ ! -f "$SPEC_FILE" ]; then
687
+ echo "[tokenless-env-fix] spec file not found: $SPEC_FILE"
688
+ echo "Supported managers: rpm, apt, pip, uv, npm, npx, cargo, cargo_build, symlink, path, dir, curl_pipe_sh"
689
+ exit 0
690
+ fi
691
+ echo "Auto-fixable dependencies (from spec):"
692
+ # Collect all dep entries across all tools
693
+ all_deps=$(jq -c --arg mgr "$PACKAGE_MANAGER" '[del(."_comment") | to_entries[] | select(.key != "_meta") | .value | (.required // []) + (.recommended // []) | .[] | if type == "string" then {binary: ., package: ., manager: $mgr} else . end]' "$SPEC_FILE" 2>/dev/null || echo '[]')
694
+ count=$(echo "$all_deps" | jq 'length' 2>/dev/null || echo 0)
695
+ dep_json="" binary="" package="" manager="" fb_count=""
696
+ for i in $(seq 0 $((count - 1))); do
697
+ dep_json=$(echo "$all_deps" | jq -c ".[$i]")
698
+ binary=$(echo "$dep_json" | jq -r '.binary')
699
+ package=$(echo "$dep_json" | jq -r '.package')
700
+ manager=$(echo "$dep_json" | jq -r '.manager')
701
+ fb_count=$(echo "$dep_json" | jq '.fallback // [] | length')
702
+ echo " ${binary} — ${manager} (package: ${package}, fallbacks: ${fb_count})"
703
+ done
704
+ echo ""
705
+ echo "Detected system package manager: $PACKAGE_MANAGER"
706
+ echo "Supported managers:"
707
+ echo " rpm — system package manager (auto-detect: dnf/yum for Alinux, apt for Debian, apk for Alpine; current: $PACKAGE_MANAGER)"
708
+ echo " apt — apt-get (Debian/Ubuntu)"
709
+ echo " pip — pip / pip3"
710
+ echo " uv — uv tool install / uv pip install"
711
+ echo " npm — npm install -g"
712
+ echo " npx — npx -y (verify availability)"
713
+ echo " cargo — cargo install --locked"
714
+ echo " cargo_build — cargo build from local manifest"
715
+ echo " symlink — ln -sf from source path"
716
+ echo " path — add directory to PATH"
717
+ echo " dir — mkdir -p"
718
+ echo " curl_pipe_sh — curl/wget | sh (official install scripts)"
719
+ ;;
720
+ *)
721
+ echo "Usage: tokenless-env-fix.sh <command> [args]"
722
+ echo ""
723
+ echo "Commands:"
724
+ echo " fix '<json>' Fix a single dep (JSON object or simple name)"
725
+ echo " fix-simple <name> [mgr] Fix by binary name with optional manager"
726
+ echo " fix-all '<json_arr>' Fix multiple deps (JSON array)"
727
+ echo " fix-tool <name> Fix all deps for a tool from spec file"
728
+ echo " check List all auto-fixable deps from spec"
729
+ ;;
730
+ esac