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.
- package/LICENSE +190 -0
- package/README.md +770 -0
- package/adapters/tokenless/claude-code/.claude-plugin/marketplace.json +15 -0
- package/adapters/tokenless/claude-code/.claude-plugin/plugin.json +9 -0
- package/adapters/tokenless/claude-code/hooks/hooks.json +38 -0
- package/adapters/tokenless/claude-code/scripts/detect.sh +193 -0
- package/adapters/tokenless/claude-code/scripts/install.sh +92 -0
- package/adapters/tokenless/claude-code/scripts/uninstall.sh +50 -0
- package/adapters/tokenless/codex/.codex-plugin/plugin.json +20 -0
- package/adapters/tokenless/codex/README.md +160 -0
- package/adapters/tokenless/codex/hooks/hooks.json +52 -0
- package/adapters/tokenless/codex/scripts/_common.sh +25 -0
- package/adapters/tokenless/codex/scripts/check-tokenless +94 -0
- package/adapters/tokenless/codex/scripts/compress-response +441 -0
- package/adapters/tokenless/codex/scripts/detect.sh +61 -0
- package/adapters/tokenless/codex/scripts/install.sh +151 -0
- package/adapters/tokenless/codex/scripts/rewrite-hook +282 -0
- package/adapters/tokenless/codex/scripts/tool-ready +303 -0
- package/adapters/tokenless/codex/scripts/uninstall.sh +78 -0
- package/adapters/tokenless/common/commands/tokenless-stats.toml +2 -0
- package/adapters/tokenless/common/cosh-extension.json +65 -0
- package/adapters/tokenless/common/hooks/compress_response_hook.py +487 -0
- package/adapters/tokenless/common/hooks/compress_schema_hook.py +144 -0
- package/adapters/tokenless/common/hooks/compress_toon_hook.py +160 -0
- package/adapters/tokenless/common/hooks/hook_utils.py +584 -0
- package/adapters/tokenless/common/hooks/rewrite_hook.py +223 -0
- package/adapters/tokenless/common/hooks/run-hook.sh +62 -0
- package/adapters/tokenless/common/hooks/tool_categories.json +99 -0
- package/adapters/tokenless/common/hooks/tool_ready_hook.sh +571 -0
- package/adapters/tokenless/common/tokenless-env-fix.sh +730 -0
- package/adapters/tokenless/common/tool-ready-spec.json +113 -0
- package/adapters/tokenless/dsh/cordis.patch.yml +8 -0
- package/adapters/tokenless/dsh/dist/index.js +399 -0
- package/adapters/tokenless/dsh/package.json +26 -0
- package/adapters/tokenless/hermes/__init__.py +572 -0
- package/adapters/tokenless/hermes/plugin.yaml +9 -0
- package/adapters/tokenless/hermes/scripts/detect.sh +80 -0
- package/adapters/tokenless/hermes/scripts/install.sh +60 -0
- package/adapters/tokenless/hermes/scripts/uninstall.sh +45 -0
- package/adapters/tokenless/manifest.json +147 -0
- package/adapters/tokenless/openclaw/dist/index.d.ts +27 -0
- package/adapters/tokenless/openclaw/dist/index.js +598 -0
- package/adapters/tokenless/openclaw/dist/tool_categories.json +99 -0
- package/adapters/tokenless/openclaw/index.ts +720 -0
- package/adapters/tokenless/openclaw/openclaw.plugin.json +40 -0
- package/adapters/tokenless/openclaw/package.json +24 -0
- package/adapters/tokenless/openclaw/scripts/detect.sh +102 -0
- package/adapters/tokenless/openclaw/scripts/install.sh +75 -0
- package/adapters/tokenless/openclaw/scripts/uninstall.sh +46 -0
- package/adapters/tokenless/openclaw/tsconfig.json +13 -0
- package/adapters/tokenless/opencode/plugin.js +246 -0
- package/adapters/tokenless/opencode/scripts/detect.sh +38 -0
- package/adapters/tokenless/opencode/scripts/install.sh +56 -0
- package/adapters/tokenless/opencode/scripts/uninstall.sh +29 -0
- package/adapters/tokenless/qoder/.qoder-plugin/plugin.json +11 -0
- package/adapters/tokenless/qoder/commands/tokenless-stats.md +8 -0
- package/adapters/tokenless/qoder/hooks/hooks.json +36 -0
- package/adapters/tokenless/qoder/hooks/run-hook.sh +51 -0
- package/adapters/tokenless/qoder/scripts/detect.sh +35 -0
- package/adapters/tokenless/qoder/scripts/install.sh +136 -0
- package/adapters/tokenless/qoder/scripts/uninstall.sh +106 -0
- package/adapters/tokenless/qwencode/qwen-extension.json +69 -0
- package/adapters/tokenless/qwencode/scripts/detect.sh +125 -0
- package/adapters/tokenless/qwencode/scripts/install.sh +128 -0
- package/adapters/tokenless/qwencode/scripts/uninstall.sh +62 -0
- package/bin/rtk +6 -0
- package/bin/tokenless +6 -0
- package/bin/toon +6 -0
- package/package.json +57 -0
- 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
|