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.
@@ -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 "$@"