rrskill 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +237 -0
- package/bin/rrskill.js +8 -0
- package/dist/index.js +1264 -0
- package/dist/install.sh +580 -0
- package/dist/rrskill.md +157 -0
- package/package.json +18 -0
- package/src/builtins/plugins/openclaw/index.ts +63 -0
- package/src/builtins/plugins/openclaw/openclaw.plugin.json +34 -0
- package/src/builtins/skills/find-skills.md +63 -0
- package/src/builtins/skills/rrskill-preference.md +15 -0
package/dist/install.sh
ADDED
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 文件作用:面向最终用户的安装脚本,负责准备 Node 运行时、安装 rrskill 并执行 bootstrap。
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
# Keep these mirrored defaults aligned with src/installer/constants.ts.
|
|
6
|
+
DEFAULT_HOST="openclaw"
|
|
7
|
+
DEFAULT_BIN_NAME="rrskill"
|
|
8
|
+
HOST="${RRSKILL_HOST:-$DEFAULT_HOST}"
|
|
9
|
+
MIN_NODE_MAJOR=20
|
|
10
|
+
NODE_VERSION="${RRSKILL_NODE_VERSION:-20.19.0}"
|
|
11
|
+
INSTALL_BASE="${RRSKILL_INSTALL_BASE:-${HOME}/.rrskill}"
|
|
12
|
+
RUNTIME_ROOT="${INSTALL_BASE}/runtime/node"
|
|
13
|
+
MANAGED_NODE_DIR="${RUNTIME_ROOT}/v${NODE_VERSION}"
|
|
14
|
+
CURRENT_NODE_DIR="${RUNTIME_ROOT}/current"
|
|
15
|
+
NPM_PREFIX="${RRSKILL_NPM_PREFIX:-${INSTALL_BASE}/npm-global}"
|
|
16
|
+
BIN_DIR="${RRSKILL_BIN_DIR:-${HOME}/.local/bin}"
|
|
17
|
+
PACKAGE_SPEC="${RRSKILL_NPM_PACKAGE:-rrskill}"
|
|
18
|
+
NODE_DOWNLOAD_BASE="${RRSKILL_NODE_DOWNLOAD_BASE:-https://nodejs.org/dist}"
|
|
19
|
+
|
|
20
|
+
MODE="install"
|
|
21
|
+
OUTPUT_MODE="text"
|
|
22
|
+
SKIP_BOOTSTRAP=0
|
|
23
|
+
BOOTSTRAP_SKIPPED=0
|
|
24
|
+
|
|
25
|
+
NODE_BIN=""
|
|
26
|
+
NPM_BIN=""
|
|
27
|
+
RRSKILL_BIN=""
|
|
28
|
+
|
|
29
|
+
STEP_RESULTS=()
|
|
30
|
+
NEXT_COMMANDS=()
|
|
31
|
+
LAST_CHANGED=false
|
|
32
|
+
|
|
33
|
+
json_escape() {
|
|
34
|
+
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
record_step() {
|
|
38
|
+
local name="$1"
|
|
39
|
+
local status="$2"
|
|
40
|
+
local changed="${3:-false}"
|
|
41
|
+
local message="${4:-}"
|
|
42
|
+
local json
|
|
43
|
+
|
|
44
|
+
json="{\"name\":\"$(json_escape "$name")\",\"status\":\"$(json_escape "$status")\",\"changed\":${changed}"
|
|
45
|
+
if [[ -n "$message" ]]; then
|
|
46
|
+
json="${json},\"message\":\"$(json_escape "$message")\""
|
|
47
|
+
fi
|
|
48
|
+
json="${json}}"
|
|
49
|
+
STEP_RESULTS+=("$json")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
add_next_command() {
|
|
53
|
+
NEXT_COMMANDS+=("$1")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
render_steps_json() {
|
|
57
|
+
local first=1
|
|
58
|
+
printf '['
|
|
59
|
+
for step in "${STEP_RESULTS[@]-}"; do
|
|
60
|
+
if [[ $first -eq 0 ]]; then
|
|
61
|
+
printf ','
|
|
62
|
+
fi
|
|
63
|
+
first=0
|
|
64
|
+
printf '%s' "$step"
|
|
65
|
+
done
|
|
66
|
+
printf ']'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
render_next_commands_json() {
|
|
70
|
+
local first=1
|
|
71
|
+
printf '['
|
|
72
|
+
for command in "${NEXT_COMMANDS[@]-}"; do
|
|
73
|
+
if [[ $first -eq 0 ]]; then
|
|
74
|
+
printf ','
|
|
75
|
+
fi
|
|
76
|
+
first=0
|
|
77
|
+
printf '"%s"' "$(json_escape "$command")"
|
|
78
|
+
done
|
|
79
|
+
printf ']'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
emit_json_success() {
|
|
83
|
+
printf '{'
|
|
84
|
+
printf '"ok":true,'
|
|
85
|
+
printf '"mode":"%s",' "$(json_escape "$MODE")"
|
|
86
|
+
printf '"host":"%s",' "$(json_escape "$HOST")"
|
|
87
|
+
printf '"bootstrap_skipped":%s,' "$([[ $BOOTSTRAP_SKIPPED -eq 1 ]] && printf 'true' || printf 'false')"
|
|
88
|
+
printf '"steps":%s,' "$(render_steps_json)"
|
|
89
|
+
printf '"next_commands":%s' "$(render_next_commands_json)"
|
|
90
|
+
printf '}\n'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
emit_json_error() {
|
|
94
|
+
local code="$1"
|
|
95
|
+
local message="$2"
|
|
96
|
+
|
|
97
|
+
printf '{'
|
|
98
|
+
printf '"ok":false,'
|
|
99
|
+
printf '"mode":"%s",' "$(json_escape "$MODE")"
|
|
100
|
+
printf '"host":"%s",' "$(json_escape "$HOST")"
|
|
101
|
+
printf '"error":{"code":"%s","message":"%s","host":"%s"},' \
|
|
102
|
+
"$(json_escape "$code")" \
|
|
103
|
+
"$(json_escape "$message")" \
|
|
104
|
+
"$(json_escape "$HOST")"
|
|
105
|
+
printf '"steps":%s,' "$(render_steps_json)"
|
|
106
|
+
printf '"next_commands":%s' "$(render_next_commands_json)"
|
|
107
|
+
printf '}\n'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
log() {
|
|
111
|
+
if [[ "$OUTPUT_MODE" == "text" ]]; then
|
|
112
|
+
printf '[rrskill-install] %s\n' "$*"
|
|
113
|
+
fi
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
warn() {
|
|
117
|
+
if [[ "$OUTPUT_MODE" == "text" ]]; then
|
|
118
|
+
printf '[rrskill-install] Warning: %s\n' "$*" >&2
|
|
119
|
+
fi
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
die() {
|
|
123
|
+
local code="$1"
|
|
124
|
+
local message="$2"
|
|
125
|
+
local exit_code="${3:-1}"
|
|
126
|
+
|
|
127
|
+
if [[ "$OUTPUT_MODE" == "json" ]]; then
|
|
128
|
+
emit_json_error "$code" "$message"
|
|
129
|
+
else
|
|
130
|
+
printf '[rrskill-install] Error: %s\n' "$message" >&2
|
|
131
|
+
fi
|
|
132
|
+
exit "$exit_code"
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
usage() {
|
|
136
|
+
cat <<'USAGE'
|
|
137
|
+
Usage: install.sh [--host <openclaw|codex|claude-code>] [--openclaw|--codex|--claude-code] [--check|--dry-run] [--skip-bootstrap] [--output <json|text>|--json]
|
|
138
|
+
|
|
139
|
+
Modes:
|
|
140
|
+
--check Verify current environment only and report missing steps
|
|
141
|
+
--dry-run Print the planned initialization steps without changing anything
|
|
142
|
+
--skip-bootstrap Install the CLI without attempting host bootstrap repair
|
|
143
|
+
|
|
144
|
+
Examples:
|
|
145
|
+
install.sh --host openclaw
|
|
146
|
+
install.sh --host openclaw --check --output json
|
|
147
|
+
install.sh --host codex --skip-bootstrap --output json
|
|
148
|
+
install.sh --dry-run
|
|
149
|
+
USAGE
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
default_retry_command() {
|
|
153
|
+
printf 'curl -fsSL https://rrskill.ai/install/install.sh | bash -s -- --host %s' "$HOST"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
skip_bootstrap_retry_command() {
|
|
157
|
+
printf 'curl -fsSL https://rrskill.ai/install/install.sh | bash -s -- --host %s --skip-bootstrap' "$HOST"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
validate_host() {
|
|
161
|
+
case "${HOST}" in
|
|
162
|
+
openclaw|codex|claude-code) ;;
|
|
163
|
+
*) die "unsupported_host" "unsupported host: ${HOST}" ;;
|
|
164
|
+
esac
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
validate_output_mode() {
|
|
168
|
+
case "${OUTPUT_MODE}" in
|
|
169
|
+
text|json) ;;
|
|
170
|
+
*) die "unsupported_output_mode" "unsupported output mode: ${OUTPUT_MODE}" ;;
|
|
171
|
+
esac
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
ensure_bootstrap_mode_allowed() {
|
|
175
|
+
if [[ "$HOST" == "openclaw" || $SKIP_BOOTSTRAP -eq 1 || "$MODE" == "check" || "$MODE" == "dry-run" ]]; then
|
|
176
|
+
return 0
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
add_next_command "$(skip_bootstrap_retry_command)"
|
|
180
|
+
add_next_command "npm install -g rrskill"
|
|
181
|
+
die "unsupported_bootstrap_host" "bootstrap is currently supported only for openclaw; ${HOST} integration is not implemented yet"
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
parse_args() {
|
|
185
|
+
while [[ $# -gt 0 ]]; do
|
|
186
|
+
case "$1" in
|
|
187
|
+
--host)
|
|
188
|
+
[[ $# -ge 2 ]] || die "missing_host_value" "--host requires a value"
|
|
189
|
+
HOST="$2"
|
|
190
|
+
shift 2
|
|
191
|
+
;;
|
|
192
|
+
--openclaw)
|
|
193
|
+
HOST="openclaw"
|
|
194
|
+
shift
|
|
195
|
+
;;
|
|
196
|
+
--codex)
|
|
197
|
+
HOST="codex"
|
|
198
|
+
shift
|
|
199
|
+
;;
|
|
200
|
+
--claude-code)
|
|
201
|
+
HOST="claude-code"
|
|
202
|
+
shift
|
|
203
|
+
;;
|
|
204
|
+
--check)
|
|
205
|
+
MODE="check"
|
|
206
|
+
shift
|
|
207
|
+
;;
|
|
208
|
+
--dry-run)
|
|
209
|
+
MODE="dry-run"
|
|
210
|
+
shift
|
|
211
|
+
;;
|
|
212
|
+
--skip-bootstrap)
|
|
213
|
+
SKIP_BOOTSTRAP=1
|
|
214
|
+
shift
|
|
215
|
+
;;
|
|
216
|
+
--output)
|
|
217
|
+
[[ $# -ge 2 ]] || die "missing_output_mode" "--output requires a value"
|
|
218
|
+
OUTPUT_MODE="$2"
|
|
219
|
+
shift 2
|
|
220
|
+
;;
|
|
221
|
+
--json)
|
|
222
|
+
OUTPUT_MODE="json"
|
|
223
|
+
shift
|
|
224
|
+
;;
|
|
225
|
+
-h|--help)
|
|
226
|
+
usage
|
|
227
|
+
exit 0
|
|
228
|
+
;;
|
|
229
|
+
*)
|
|
230
|
+
die "unknown_argument" "unknown argument: $1"
|
|
231
|
+
;;
|
|
232
|
+
esac
|
|
233
|
+
done
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
resolve_platform() {
|
|
237
|
+
local os
|
|
238
|
+
local arch
|
|
239
|
+
|
|
240
|
+
case "$(uname -s)" in
|
|
241
|
+
Darwin) os="darwin" ;;
|
|
242
|
+
Linux) os="linux" ;;
|
|
243
|
+
*) die "unsupported_os" "unsupported operating system: $(uname -s)" ;;
|
|
244
|
+
esac
|
|
245
|
+
|
|
246
|
+
case "$(uname -m)" in
|
|
247
|
+
x86_64|amd64) arch="x64" ;;
|
|
248
|
+
arm64|aarch64) arch="arm64" ;;
|
|
249
|
+
*) die "unsupported_arch" "unsupported architecture: $(uname -m)" ;;
|
|
250
|
+
esac
|
|
251
|
+
|
|
252
|
+
printf '%s %s\n' "$os" "$arch"
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
node_major_version() {
|
|
256
|
+
local version_text
|
|
257
|
+
version_text="$("$1" --version 2>/dev/null || true)"
|
|
258
|
+
version_text="${version_text#v}"
|
|
259
|
+
printf '%s\n' "${version_text%%.*}"
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
use_node_pair() {
|
|
263
|
+
NODE_BIN="$1"
|
|
264
|
+
NPM_BIN="$2"
|
|
265
|
+
export PATH="$(dirname "$NODE_BIN"):$PATH"
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
has_system_node() {
|
|
269
|
+
if ! command -v node >/dev/null 2>&1 || ! command -v npm >/dev/null 2>&1; then
|
|
270
|
+
return 1
|
|
271
|
+
fi
|
|
272
|
+
|
|
273
|
+
local system_major
|
|
274
|
+
system_major="$(node_major_version "$(command -v node)")"
|
|
275
|
+
[[ "$system_major" =~ ^[0-9]+$ ]] && (( system_major >= MIN_NODE_MAJOR ))
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
has_managed_node() {
|
|
279
|
+
if [[ ! -x "${CURRENT_NODE_DIR}/bin/node" || ! -x "${CURRENT_NODE_DIR}/bin/npm" ]]; then
|
|
280
|
+
return 1
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
local current_major
|
|
284
|
+
current_major="$(node_major_version "${CURRENT_NODE_DIR}/bin/node")"
|
|
285
|
+
[[ "$current_major" =~ ^[0-9]+$ ]] && (( current_major >= MIN_NODE_MAJOR ))
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
check_npm_prefix() {
|
|
289
|
+
[[ -d "${NPM_PREFIX}/bin" && -d "${BIN_DIR}" ]]
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
check_cli_installed() {
|
|
293
|
+
[[ -x "${NPM_PREFIX}/bin/${DEFAULT_BIN_NAME}" ]]
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
check_bin_link() {
|
|
297
|
+
[[ -x "${BIN_DIR}/${DEFAULT_BIN_NAME}" ]]
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
check_openclaw_bootstrap() {
|
|
301
|
+
[[ -f "${HOME}/.openclaw/workspace/skills/find-skills/SKILL.md" ]] || return 1
|
|
302
|
+
[[ -f "${HOME}/.openclaw/workspace/skills/rrskill-preference/SKILL.md" ]] || return 1
|
|
303
|
+
[[ -f "${HOME}/.openclaw/extensions/rrskill/index.ts" ]] || return 1
|
|
304
|
+
[[ -f "${HOME}/.openclaw/extensions/rrskill/openclaw.plugin.json" ]] || return 1
|
|
305
|
+
[[ -f "${HOME}/.openclaw/openclaw.json" ]] || return 1
|
|
306
|
+
|
|
307
|
+
grep -q '"rrskill"' "${HOME}/.openclaw/openclaw.json" || return 1
|
|
308
|
+
grep -q '"primaryCli"[[:space:]]*:[[:space:]]*"rrskill"' "${HOME}/.openclaw/openclaw.json" || return 1
|
|
309
|
+
grep -q '"primaryLabel"[[:space:]]*:[[:space:]]*"official-registry"' "${HOME}/.openclaw/openclaw.json" || return 1
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
record_check_results() {
|
|
313
|
+
local all_ok=0
|
|
314
|
+
|
|
315
|
+
if has_system_node || has_managed_node; then
|
|
316
|
+
record_step "ensure_node" "ok" false "node runtime available"
|
|
317
|
+
else
|
|
318
|
+
all_ok=1
|
|
319
|
+
record_step "ensure_node" "missing" false "node runtime is not ready"
|
|
320
|
+
add_next_command "$(default_retry_command)"
|
|
321
|
+
fi
|
|
322
|
+
|
|
323
|
+
if check_npm_prefix; then
|
|
324
|
+
record_step "ensure_npm_prefix" "ok" false "npm prefix directories present"
|
|
325
|
+
else
|
|
326
|
+
all_ok=1
|
|
327
|
+
record_step "ensure_npm_prefix" "missing" false "npm prefix directories are missing"
|
|
328
|
+
add_next_command "$(default_retry_command)"
|
|
329
|
+
fi
|
|
330
|
+
|
|
331
|
+
if check_cli_installed; then
|
|
332
|
+
record_step "ensure_cli_installed" "ok" false "rrskill CLI is installed"
|
|
333
|
+
else
|
|
334
|
+
all_ok=1
|
|
335
|
+
record_step "ensure_cli_installed" "missing" false "rrskill CLI is not installed"
|
|
336
|
+
add_next_command "$(default_retry_command)"
|
|
337
|
+
fi
|
|
338
|
+
|
|
339
|
+
if check_bin_link; then
|
|
340
|
+
record_step "ensure_bin_link" "ok" false "rrskill executable link is present"
|
|
341
|
+
else
|
|
342
|
+
all_ok=1
|
|
343
|
+
record_step "ensure_bin_link" "missing" false "rrskill executable link is missing"
|
|
344
|
+
add_next_command "$(default_retry_command)"
|
|
345
|
+
fi
|
|
346
|
+
|
|
347
|
+
if [[ "$HOST" == "openclaw" && $SKIP_BOOTSTRAP -eq 0 ]]; then
|
|
348
|
+
if check_openclaw_bootstrap; then
|
|
349
|
+
record_step "run_bootstrap" "ok" false "openclaw bootstrap is complete"
|
|
350
|
+
else
|
|
351
|
+
all_ok=1
|
|
352
|
+
record_step "run_bootstrap" "missing" false "openclaw bootstrap is not complete"
|
|
353
|
+
add_next_command "rrskill bootstrap --host openclaw"
|
|
354
|
+
fi
|
|
355
|
+
elif [[ $SKIP_BOOTSTRAP -eq 1 ]]; then
|
|
356
|
+
BOOTSTRAP_SKIPPED=1
|
|
357
|
+
record_step "run_bootstrap" "skipped" false "bootstrap explicitly skipped"
|
|
358
|
+
else
|
|
359
|
+
BOOTSTRAP_SKIPPED=1
|
|
360
|
+
record_step "run_bootstrap" "skipped" false "bootstrap integration is not implemented for this host"
|
|
361
|
+
add_next_command "rrskill search <query>"
|
|
362
|
+
add_next_command "rrskill install <slug>"
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
return "$all_ok"
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
run_check_mode() {
|
|
369
|
+
set +e
|
|
370
|
+
record_check_results
|
|
371
|
+
local ok=$?
|
|
372
|
+
set -e
|
|
373
|
+
|
|
374
|
+
if [[ "$OUTPUT_MODE" == "json" ]]; then
|
|
375
|
+
if [[ "$ok" -eq 0 ]]; then
|
|
376
|
+
emit_json_success
|
|
377
|
+
exit 0
|
|
378
|
+
fi
|
|
379
|
+
|
|
380
|
+
emit_json_error "environment_not_ready" "rrskill environment is not fully initialized"
|
|
381
|
+
exit 1
|
|
382
|
+
fi
|
|
383
|
+
|
|
384
|
+
if [[ "$ok" -eq 0 ]]; then
|
|
385
|
+
log "Environment check passed"
|
|
386
|
+
exit 0
|
|
387
|
+
fi
|
|
388
|
+
|
|
389
|
+
die "environment_not_ready" "rrskill environment is not fully initialized"
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
run_dry_run_mode() {
|
|
393
|
+
record_step "ensure_node" "planned" false "would ensure a supported Node runtime"
|
|
394
|
+
record_step "ensure_npm_prefix" "planned" false "would ensure npm prefix directories"
|
|
395
|
+
record_step "ensure_cli_installed" "planned" false "would install rrskill via npm"
|
|
396
|
+
record_step "ensure_bin_link" "planned" false "would link rrskill into the final bin directory"
|
|
397
|
+
|
|
398
|
+
if [[ "$HOST" == "openclaw" && $SKIP_BOOTSTRAP -eq 0 ]]; then
|
|
399
|
+
record_step "run_bootstrap" "planned" false "would run rrskill bootstrap for openclaw"
|
|
400
|
+
else
|
|
401
|
+
BOOTSTRAP_SKIPPED=1
|
|
402
|
+
record_step "run_bootstrap" "planned" false "would skip bootstrap for this host"
|
|
403
|
+
fi
|
|
404
|
+
|
|
405
|
+
if [[ "$OUTPUT_MODE" == "json" ]]; then
|
|
406
|
+
emit_json_success
|
|
407
|
+
else
|
|
408
|
+
log "Dry run complete"
|
|
409
|
+
fi
|
|
410
|
+
exit 0
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
install_managed_node() {
|
|
414
|
+
command -v curl >/dev/null 2>&1 || die "missing_curl" "curl is required to install a managed Node runtime"
|
|
415
|
+
command -v tar >/dev/null 2>&1 || die "missing_tar" "tar is required to install a managed Node runtime"
|
|
416
|
+
|
|
417
|
+
if has_managed_node; then
|
|
418
|
+
log "Using previously installed managed Node ${NODE_VERSION}"
|
|
419
|
+
use_node_pair "${CURRENT_NODE_DIR}/bin/node" "${CURRENT_NODE_DIR}/bin/npm"
|
|
420
|
+
LAST_CHANGED=false
|
|
421
|
+
return 0
|
|
422
|
+
fi
|
|
423
|
+
|
|
424
|
+
local os
|
|
425
|
+
local arch
|
|
426
|
+
read -r os arch <<<"$(resolve_platform)"
|
|
427
|
+
|
|
428
|
+
local archive_name
|
|
429
|
+
archive_name="node-v${NODE_VERSION}-${os}-${arch}.tar.xz"
|
|
430
|
+
|
|
431
|
+
local tmp_dir
|
|
432
|
+
tmp_dir="$(mktemp -d)"
|
|
433
|
+
local archive_path="${tmp_dir}/${archive_name}"
|
|
434
|
+
|
|
435
|
+
log "Installing managed Node ${NODE_VERSION} (${os}/${arch})"
|
|
436
|
+
curl -fsSL "${NODE_DOWNLOAD_BASE}/v${NODE_VERSION}/${archive_name}" -o "${archive_path}"
|
|
437
|
+
tar -xJf "${archive_path}" -C "${tmp_dir}"
|
|
438
|
+
|
|
439
|
+
local extracted_node
|
|
440
|
+
extracted_node="$(find "${tmp_dir}" -type f -path '*/bin/node' | head -n 1 || true)"
|
|
441
|
+
[[ -n "${extracted_node}" ]] || die "missing_extracted_node" "failed to locate extracted node binary"
|
|
442
|
+
|
|
443
|
+
local extracted_root
|
|
444
|
+
extracted_root="$(dirname "$(dirname "${extracted_node}")")"
|
|
445
|
+
|
|
446
|
+
mkdir -p "${RUNTIME_ROOT}"
|
|
447
|
+
rm -rf "${MANAGED_NODE_DIR}"
|
|
448
|
+
cp -R "${extracted_root}" "${MANAGED_NODE_DIR}"
|
|
449
|
+
rm -rf "${CURRENT_NODE_DIR}"
|
|
450
|
+
ln -s "${MANAGED_NODE_DIR}" "${CURRENT_NODE_DIR}"
|
|
451
|
+
rm -rf "${tmp_dir}"
|
|
452
|
+
|
|
453
|
+
use_node_pair "${CURRENT_NODE_DIR}/bin/node" "${CURRENT_NODE_DIR}/bin/npm"
|
|
454
|
+
LAST_CHANGED=true
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
ensure_node() {
|
|
458
|
+
if has_system_node; then
|
|
459
|
+
local system_node
|
|
460
|
+
local system_npm
|
|
461
|
+
local system_major
|
|
462
|
+
system_node="$(command -v node)"
|
|
463
|
+
system_npm="$(command -v npm)"
|
|
464
|
+
system_major="$(node_major_version "${system_node}")"
|
|
465
|
+
log "Using system Node ${system_major}"
|
|
466
|
+
use_node_pair "${system_node}" "${system_npm}"
|
|
467
|
+
record_step "ensure_node" "ok" false "using supported system Node"
|
|
468
|
+
return 0
|
|
469
|
+
fi
|
|
470
|
+
|
|
471
|
+
if command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1; then
|
|
472
|
+
log "System Node is too old; installing managed Node ${NODE_VERSION}"
|
|
473
|
+
else
|
|
474
|
+
log "Node/npm not found; installing managed Node ${NODE_VERSION}"
|
|
475
|
+
fi
|
|
476
|
+
|
|
477
|
+
install_managed_node
|
|
478
|
+
record_step "ensure_node" "ok" "$LAST_CHANGED" "managed Node runtime is ready"
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
ensure_npm_prefix() {
|
|
482
|
+
local changed=false
|
|
483
|
+
if ! check_npm_prefix; then
|
|
484
|
+
changed=true
|
|
485
|
+
fi
|
|
486
|
+
|
|
487
|
+
mkdir -p "${NPM_PREFIX}/bin" "${BIN_DIR}"
|
|
488
|
+
export NPM_CONFIG_PREFIX="${NPM_PREFIX}"
|
|
489
|
+
export PATH="${NPM_PREFIX}/bin:$PATH"
|
|
490
|
+
|
|
491
|
+
record_step "ensure_npm_prefix" "ok" "$changed" "npm prefix directories are ready"
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
ensure_rrskill_installed() {
|
|
495
|
+
log "Installing ${PACKAGE_SPEC} via npm"
|
|
496
|
+
"${NPM_BIN}" install -g "${PACKAGE_SPEC}"
|
|
497
|
+
record_step "ensure_cli_installed" "ok" true "rrskill CLI installed via npm"
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
ensure_bin_link() {
|
|
501
|
+
local target="${NPM_PREFIX}/bin/${DEFAULT_BIN_NAME}"
|
|
502
|
+
[[ -x "${target}" ]] || die "missing_cli_binary" "expected ${DEFAULT_BIN_NAME} binary at ${target} after npm install"
|
|
503
|
+
|
|
504
|
+
local changed=true
|
|
505
|
+
if [[ -L "${BIN_DIR}/${DEFAULT_BIN_NAME}" ]]; then
|
|
506
|
+
local existing_target
|
|
507
|
+
existing_target="$(readlink "${BIN_DIR}/${DEFAULT_BIN_NAME}" || true)"
|
|
508
|
+
if [[ "${existing_target}" == "${target}" ]]; then
|
|
509
|
+
changed=false
|
|
510
|
+
fi
|
|
511
|
+
fi
|
|
512
|
+
|
|
513
|
+
mkdir -p "${BIN_DIR}"
|
|
514
|
+
rm -f "${BIN_DIR}/${DEFAULT_BIN_NAME}"
|
|
515
|
+
ln -s "${target}" "${BIN_DIR}/${DEFAULT_BIN_NAME}"
|
|
516
|
+
RRSKILL_BIN="${BIN_DIR}/${DEFAULT_BIN_NAME}"
|
|
517
|
+
|
|
518
|
+
record_step "ensure_bin_link" "ok" "$changed" "rrskill executable link is ready"
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
warn_if_bin_dir_missing_from_path() {
|
|
522
|
+
case ":${PATH}:" in
|
|
523
|
+
*":${BIN_DIR}:"*) ;;
|
|
524
|
+
*)
|
|
525
|
+
warn "${BIN_DIR} is not on PATH. You may need to add it before running rrskill in a new shell."
|
|
526
|
+
;;
|
|
527
|
+
esac
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
run_bootstrap() {
|
|
531
|
+
if [[ $SKIP_BOOTSTRAP -eq 1 ]]; then
|
|
532
|
+
BOOTSTRAP_SKIPPED=1
|
|
533
|
+
record_step "run_bootstrap" "skipped" false "bootstrap explicitly skipped"
|
|
534
|
+
return 0
|
|
535
|
+
fi
|
|
536
|
+
|
|
537
|
+
if [[ "$HOST" != "openclaw" ]]; then
|
|
538
|
+
BOOTSTRAP_SKIPPED=1
|
|
539
|
+
record_step "run_bootstrap" "skipped" false "bootstrap integration is not implemented for this host"
|
|
540
|
+
return 0
|
|
541
|
+
fi
|
|
542
|
+
|
|
543
|
+
log "Running rrskill bootstrap --host ${HOST}"
|
|
544
|
+
"${RRSKILL_BIN}" bootstrap --host "${HOST}"
|
|
545
|
+
record_step "run_bootstrap" "ok" true "bootstrap completed"
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
run_install_mode() {
|
|
549
|
+
ensure_node
|
|
550
|
+
ensure_npm_prefix
|
|
551
|
+
ensure_rrskill_installed
|
|
552
|
+
ensure_bin_link
|
|
553
|
+
warn_if_bin_dir_missing_from_path
|
|
554
|
+
run_bootstrap
|
|
555
|
+
|
|
556
|
+
if [[ "$OUTPUT_MODE" == "json" ]]; then
|
|
557
|
+
emit_json_success
|
|
558
|
+
else
|
|
559
|
+
log "Install complete"
|
|
560
|
+
fi
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
main() {
|
|
564
|
+
parse_args "$@"
|
|
565
|
+
validate_output_mode
|
|
566
|
+
validate_host
|
|
567
|
+
|
|
568
|
+
if [[ "$MODE" == "check" ]]; then
|
|
569
|
+
run_check_mode
|
|
570
|
+
fi
|
|
571
|
+
|
|
572
|
+
if [[ "$MODE" == "dry-run" ]]; then
|
|
573
|
+
run_dry_run_mode
|
|
574
|
+
fi
|
|
575
|
+
|
|
576
|
+
ensure_bootstrap_mode_allowed
|
|
577
|
+
run_install_mode
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
main "$@"
|