rainskills 0.1.44 → 0.1.45

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/SKILL.md CHANGED
@@ -18,6 +18,8 @@ If the adjacent installer is missing, check the local Node.js version before cho
18
18
 
19
19
  Skills-only 安装不需要 Node.js;CDN fallback 只负责安装 Skill 文件,不代表运行环境连接、应用部署或平台安装已经可执行。用户首次提出需要运行环境的动作时,对应业务 Skill 才检查 Node.js;固定 Rainskills launcher 需要 Node.js 18 或更高版本。缺失或版本过低时保留原始 intent 并停止,等待用户或 agent 明确同意安装或升级 Node.js,安装完成消息不得提前提示 Node.js。
20
20
 
21
+ WorkBuddy 中安装、更新和修复必须以前台附着方式启动,不得主动设置后台执行、使用 `&` 或 `nohup`。需要访问 `~/.rainbond` 或确认批量删除时,通过宿主的正常授权流程处理;发生权限或 `SAFE_DELETE_BULK_CONFIRM_REQUIRED` 错误时,先处理授权,不得重复启动后台重试或拆分删除来规避守卫。宿主自动返回持久句柄时继续等待原句柄;安装失败时保留 `[rainskills-cli]` 阶段和原始错误,不能仅凭 Skill 已复制就报告完整安装成功。安装验证成功后的 `[rainskills-cli][cleanup]` 提示表示临时文件待清理,不等于安装失败。
22
+
21
23
  执行优化:同一会话内只检查一次 Node.js 和运行环境状态;仅在 Node.js、Rainskills 或唯一运行环境发生变化后失效。固定 launcher 和 argv 已在本 Skill 中,禁止读取、搜索或探测 `rainskills.js`,也禁止执行 `npm root -g`。
22
24
 
23
25
  ## Completion Message
@@ -54,7 +56,7 @@ Hermes Agent 中必须使用 `terminal` 以 `background=true` 启动授权命令
54
56
 
55
57
  DeepSeek Harness 和 WorkBuddy 也必须保持原命令附着直到退出;如果宿主把长命令转成持久终端或后台任务,只能轮询该原始命令句柄,不得另起 `runtime status` 推测授权结果。WorkBuddy 安装后若当前任务的 Skill 清单尚未刷新,在下一条业务动作前先刷新 Skill 列表或新建任务,不得退回内置 Sites 代替 Rainbond 部署。
56
58
 
57
- Rainskills 只保存一个全局运行环境,不维护环境列表、名称、默认值或环境 ID。使用固定 launcher `node <home>/.rainbond/lib/rainskills/bin/rainskills.js`(运行包版本 `rainskills@0.1.44`):
59
+ Rainskills 只保存一个全局运行环境,不维护环境列表、名称、默认值或环境 ID。使用固定 launcher `node <home>/.rainbond/lib/rainskills/bin/rainskills.js`(运行包版本 `rainskills@0.1.45`):
58
60
 
59
61
  - 状态:执行 `runtime status --json`。
60
62
  - 首次连接:执行 `runtime connect <target> --saas` 或 `runtime connect <target> --rainbond-url <Console origin>`。
package/install.sh CHANGED
@@ -1165,7 +1165,7 @@ install_local_cli() {
1165
1165
  local installer="$SCRIPT_DIR/scripts/install-local-cli.mjs"
1166
1166
  [[ -f "$installer" ]] || die "Rainskills 本地 CLI 安装器缺失。"
1167
1167
  node "$installer" --source-root "$SCRIPT_DIR" --home "$HOME" >/dev/null \
1168
- || die "Rainskills 本地 CLI 安装失败。"
1168
+ || die "Rainskills 本地 CLI 刷新未完成,请查看上方 [rainskills-cli] 阶段和原始错误;目录访问或删除确认被拒绝时,请在前台取得宿主授权后重试。"
1169
1169
  }
1170
1170
 
1171
1171
  copy_skill() {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rainskills",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
4
4
  "description": "Install the complete Rainskills AI deployment skill suite as one product.",
5
5
  "author": {
6
6
  "name": "Goodrain",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rainskills",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
4
4
  "description": "Install the complete Rainskills AI deployment skill suite as one product.",
5
5
  "author": {
6
6
  "name": "Goodrain",
@@ -14,10 +14,12 @@ This is the single marketplace entry for the complete Rainskills product. The in
14
14
  3. Keep stdin, stdout, and stderr attached. When `RAINSKILLS_USER_INPUT_REQUIRED` appears, pause for that installer choice. If the installer emits `rainskills.next-action.v1`, execute only its fixed `argv` through the same launcher; never evaluate output as a shell command. If the adjacent `bin/rainskills.js` exists, use it for fixed next actions; otherwise use the same versioned npm package fallback described below.
15
15
  4. Stay attached until every independent Skill is installed. Do not select, connect, or configure an application runtime during installation. In the user-facing response, output only the fixed completion message below.
16
16
 
17
- If the adjacent installer is missing, check the local Node.js version before choosing the fallback. With `npx` and Node.js 18 or newer, use `npx --yes rainskills@0.1.44 <target>`. With no Node.js or a version below 18, use `bash <(curl -fsSL https://get.rainbond.com/rainskills/install.sh) <target>` instead. Omit `<target>` only when the host cannot be determined reliably. Keep either command attached to the interactive terminal. For an update or repair, refresh this marketplace Skill first, then run the installer again; it compares and updates every independent internal Skill.
17
+ If the adjacent installer is missing, check the local Node.js version before choosing the fallback. With `npx` and Node.js 18 or newer, use `npx --yes rainskills@0.1.45 <target>`. With no Node.js or a version below 18, use `bash <(curl -fsSL https://get.rainbond.com/rainskills/install.sh) <target>` instead. Omit `<target>` only when the host cannot be determined reliably. Keep either command attached to the interactive terminal. For an update or repair, refresh this marketplace Skill first, then run the installer again; it compares and updates every independent internal Skill.
18
18
 
19
19
  Skills-only 安装不需要 Node.js;CDN fallback 只负责安装 Skill 文件,不代表运行环境连接、应用部署或平台安装已经可执行。用户首次提出需要运行环境的动作时,对应业务 Skill 才检查 Node.js;固定 Rainskills launcher 需要 Node.js 18 或更高版本。缺失或版本过低时保留原始 intent 并停止,等待用户或 agent 明确同意安装或升级 Node.js,安装完成消息不得提前提示 Node.js。
20
20
 
21
+ WorkBuddy 中安装、更新和修复必须以前台附着方式启动,不得主动设置后台执行、使用 `&` 或 `nohup`。需要访问 `~/.rainbond` 或确认批量删除时,通过宿主的正常授权流程处理;发生权限或 `SAFE_DELETE_BULK_CONFIRM_REQUIRED` 错误时,先处理授权,不得重复启动后台重试或拆分删除来规避守卫。宿主自动返回持久句柄时继续等待原句柄;安装失败时保留 `[rainskills-cli]` 阶段和原始错误,不能仅凭 Skill 已复制就报告完整安装成功。安装验证成功后的 `[rainskills-cli][cleanup]` 提示表示临时文件待清理,不等于安装失败。
22
+
21
23
  执行优化:同一会话内只检查一次 Node.js 和运行环境状态;仅在 Node.js、Rainskills 或唯一运行环境发生变化后失效。固定 launcher 和 argv 已在本 Skill 中,禁止读取、搜索或探测 `rainskills.js`,也禁止执行 `npm root -g`。
22
24
 
23
25
  ## Completion Message
@@ -54,7 +56,7 @@ Hermes Agent 中必须使用 `terminal` 以 `background=true` 启动授权命令
54
56
 
55
57
  DeepSeek Harness 和 WorkBuddy 也必须保持原命令附着直到退出;如果宿主把长命令转成持久终端或后台任务,只能轮询该原始命令句柄,不得另起 `runtime status` 推测授权结果。WorkBuddy 安装后若当前任务的 Skill 清单尚未刷新,在下一条业务动作前先刷新 Skill 列表或新建任务,不得退回内置 Sites 代替 Rainbond 部署。
56
58
 
57
- Rainskills 只保存一个全局运行环境,不维护环境列表、名称、默认值或环境 ID。使用固定 launcher `node <home>/.rainbond/lib/rainskills/bin/rainskills.js`(运行包版本 `rainskills@0.1.44`):
59
+ Rainskills 只保存一个全局运行环境,不维护环境列表、名称、默认值或环境 ID。使用固定 launcher `node <home>/.rainbond/lib/rainskills/bin/rainskills.js`(运行包版本 `rainskills@0.1.45`):
58
60
 
59
61
  - 状态:执行 `runtime status --json`。
60
62
  - 首次连接:执行 `runtime connect <target> --saas` 或 `runtime connect <target> --rainbond-url <Console origin>`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rainskills",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
4
4
  "description": "Agent Skills for deploying, troubleshooting, and verifying applications on Rainbond from Codex, Claude Code, and other AI coding agents",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://www.rainbond.com/rainskills",
@@ -1,5 +1,5 @@
1
1
  <!-- generated-by: scripts/sync-runtime-contracts.mjs -->
2
- <!-- source-sha256: 407be69c4a1e05267351de92bea3bd86de25e214da29ccab0161cf03995010b4 -->
2
+ <!-- source-sha256: af4e6cce256157708266a81546a2d5f833e7a07ee5cb2cb2cb792e6c3d02569a -->
3
3
  <!-- profile: cli -->
4
4
  <!-- rainskills-runtime-gate:start -->
5
5
  # 单运行环境 CLI 门禁(生成文件)
@@ -46,7 +46,7 @@ Hermes Agent 使用 `terminal` 且 `background=true` 启动授权,随后只对
46
46
  ```json
47
47
  {
48
48
  "schema": "rainskills.single-runtime-contract.v1",
49
- "package_version": "rainskills@0.1.44",
49
+ "package_version": "rainskills@0.1.45",
50
50
  "runtime_status": [
51
51
  "node",
52
52
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -1,12 +1,12 @@
1
1
  <!-- generated-by: scripts/sync-runtime-contracts.mjs -->
2
- <!-- source-sha256: a5776748a0eea260fdab887e06ab14e942104a91af58376b6074a16bfaa8c3a8 -->
2
+ <!-- source-sha256: 25d0104745440047b3af7fbb816ce2e09d38028d4aca5cc3c274bfd72691851d -->
3
3
  <!-- profile: cli -->
4
4
  <!-- rainskills-runtime-gate:start -->
5
5
  # 单运行环境 CLI 门禁(生成文件)
6
6
 
7
7
  本机只允许连接一个 Rainbond 运行环境。当前 Skill 在本会话第一次调用 Rainbond 前,执行固定 launcher 的 `runtime status --json`。只有命令退出码为 0、`state=connected` 且 `usable=true` 时才继续。不得配置或直接调用客户端 MCP,不得枚举环境、读取凭据、创建业务 operation,或生成运行环境 ID、业务 operation ID 与 intent JSON。
8
8
 
9
- 运行状态的 `package_version` 必须与下方 JSON contract 完全一致;缺失或不一致时先执行固定版本 `rainskills@0.1.44` 的更新/修复流程。不得在版本错配时继续业务调用。
9
+ 运行状态的 `package_version` 必须与下方 JSON contract 完全一致;缺失或不一致时先执行固定版本 `rainskills@0.1.45` 的更新/修复流程。不得在版本错配时继续业务调用。
10
10
 
11
11
  没有可用运行环境时,严格按下方 overlay 的 `missing_runtime_mode` 进入对应的 Runtime Routing;不得扩大可选环境范围。连接和重新授权必须使用浏览器 Device Flow,不复用 Shell 中缓存的 JWT;Device Flow 不依赖 stdin TTY,必须保持进程附着直到完成。新凭据只有通过 live probe 后才可覆盖唯一运行环境。CLI 返回 401 时,只读调用可在 `runtime reconnect` 成功后重试一次;写调用不得自动重放,必须先查询平台真实状态。403 直接停止,不重新授权。
12
12
 
@@ -45,7 +45,7 @@ Hermes Agent 使用 `terminal` 且 `background=true` 启动授权,随后只对
45
45
  ```json
46
46
  {
47
47
  "schema": "rainskills.single-runtime-contract.v1",
48
- "package_version": "rainskills@0.1.44",
48
+ "package_version": "rainskills@0.1.45",
49
49
  "runtime_status": [
50
50
  "node",
51
51
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -1,5 +1,5 @@
1
1
  <!-- generated-by: scripts/sync-runtime-contracts.mjs -->
2
- <!-- source-sha256: 0207d062ca886a2af80e400a85489eda888379f1bffe23d1fdc30ca7de3698d7 -->
2
+ <!-- source-sha256: c1ecff8cab7e7b71d85bace2465c54c9fd93a2c2676d1be5cf751a49553e2c44 -->
3
3
  <!-- profile: cli -->
4
4
  <!-- rainskills-runtime-gate:start -->
5
5
  # 单运行环境 CLI 门禁(生成文件)
@@ -44,7 +44,7 @@ Hermes Agent 使用 `terminal` 且 `background=true` 启动授权,随后只对
44
44
  ```json
45
45
  {
46
46
  "schema": "rainskills.single-runtime-contract.v1",
47
- "package_version": "rainskills@0.1.44",
47
+ "package_version": "rainskills@0.1.45",
48
48
  "runtime_status": [
49
49
  "node",
50
50
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -1,5 +1,5 @@
1
1
  <!-- generated-by: scripts/sync-runtime-contracts.mjs -->
2
- <!-- source-sha256: 7b579e60e98ccb039e5bb1b17bebbd0b4f19441159a5ef7bfc4d5ddc36bfb97a -->
2
+ <!-- source-sha256: 26e83b529d67597d2e57692140c14f93619ca1efd2a837f290210dcb76376745 -->
3
3
  <!-- profile: cli -->
4
4
  <!-- rainskills-runtime-gate:start -->
5
5
  # 单运行环境 CLI 门禁(生成文件)
@@ -45,7 +45,7 @@ Hermes Agent 使用 `terminal` 且 `background=true` 启动授权,随后只对
45
45
  ```json
46
46
  {
47
47
  "schema": "rainskills.single-runtime-contract.v1",
48
- "package_version": "rainskills@0.1.44",
48
+ "package_version": "rainskills@0.1.45",
49
49
  "runtime_status": [
50
50
  "node",
51
51
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -1,5 +1,5 @@
1
1
  <!-- generated-by: scripts/sync-runtime-contracts.mjs -->
2
- <!-- source-sha256: e324c222832c1fa91a7205587d637a77cdfcc28cc8b075608b179a0fbac992ef -->
2
+ <!-- source-sha256: 1de9f116eba7833fb91c2353fd4d259be7eabcd120c6a5b8d5e68acb0ed6cccd -->
3
3
  <!-- profile: cli -->
4
4
  <!-- rainskills-runtime-gate:start -->
5
5
  # 单运行环境 CLI 门禁(生成文件)
@@ -45,7 +45,7 @@ Hermes Agent 使用 `terminal` 且 `background=true` 启动授权,随后只对
45
45
  ```json
46
46
  {
47
47
  "schema": "rainskills.single-runtime-contract.v1",
48
- "package_version": "rainskills@0.1.44",
48
+ "package_version": "rainskills@0.1.45",
49
49
  "runtime_status": [
50
50
  "node",
51
51
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -1,12 +1,12 @@
1
1
  <!-- generated-by: scripts/sync-runtime-contracts.mjs -->
2
- <!-- source-sha256: af604e84211aa7dc20a033117bd0d39720d7cace979068864f473cd2c79aa7fa -->
2
+ <!-- source-sha256: 8da03d34dfa7f4e5f021de3b74e4b929756c77497ffbfd82db9a4e918d2298cd -->
3
3
  <!-- profile: cli -->
4
4
  <!-- rainskills-runtime-gate:start -->
5
5
  # 单运行环境 CLI 门禁(生成文件)
6
6
 
7
7
  本机只允许连接一个 Rainbond 运行环境。当前 Skill 在本会话第一次调用 Rainbond 前,执行固定 launcher 的 `runtime status --json`。只有命令退出码为 0、`state=connected` 且 `usable=true` 时才继续。不得配置或直接调用客户端 MCP,不得枚举环境、读取凭据、创建业务 operation,或生成运行环境 ID、业务 operation ID 与 intent JSON。
8
8
 
9
- 运行状态的 `package_version` 必须与下方 JSON contract 完全一致;缺失或不一致时先执行固定版本 `rainskills@0.1.44` 的更新/修复流程。不得在版本错配时继续业务调用。
9
+ 运行状态的 `package_version` 必须与下方 JSON contract 完全一致;缺失或不一致时先执行固定版本 `rainskills@0.1.45` 的更新/修复流程。不得在版本错配时继续业务调用。
10
10
 
11
11
  没有可用运行环境时,严格按下方 overlay 的 `missing_runtime_mode` 进入对应的 Runtime Routing;不得扩大可选环境范围。连接和重新授权必须使用浏览器 Device Flow,不复用 Shell 中缓存的 JWT;Device Flow 不依赖 stdin TTY,必须保持进程附着直到完成。新凭据只有通过 live probe 后才可覆盖唯一运行环境。CLI 返回 401 时,只读调用可在 `runtime reconnect` 成功后重试一次;写调用不得自动重放,必须先查询平台真实状态。403 直接停止,不重新授权。
12
12
 
@@ -45,7 +45,7 @@ Hermes Agent 使用 `terminal` 且 `background=true` 启动授权,随后只对
45
45
  ```json
46
46
  {
47
47
  "schema": "rainskills.single-runtime-contract.v1",
48
- "package_version": "rainskills@0.1.44",
48
+ "package_version": "rainskills@0.1.45",
49
49
  "runtime_status": [
50
50
  "node",
51
51
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -1,5 +1,5 @@
1
1
  <!-- generated-by: scripts/sync-runtime-contracts.mjs -->
2
- <!-- source-sha256: 705856e35a6691b7295b8fec2e4bc6a9aa5f806a462cbfb5c91dda530b692183 -->
2
+ <!-- source-sha256: 4b6a1e5e9fbcd279fc9d50727b24f877d4f760a35cab179f9a6af1a3623b4bca -->
3
3
  <!-- profile: cli -->
4
4
  <!-- rainskills-runtime-gate:start -->
5
5
  # 单运行环境 CLI 门禁(生成文件)
@@ -45,7 +45,7 @@ Hermes Agent 使用 `terminal` 且 `background=true` 启动授权,随后只对
45
45
  ```json
46
46
  {
47
47
  "schema": "rainskills.single-runtime-contract.v1",
48
- "package_version": "rainskills@0.1.44",
48
+ "package_version": "rainskills@0.1.45",
49
49
  "runtime_status": [
50
50
  "node",
51
51
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -1,5 +1,5 @@
1
1
  <!-- generated-by: scripts/sync-runtime-contracts.mjs -->
2
- <!-- source-sha256: e7f02f9aa92464bf47e7f92126e6075b1f88616093e64aefb953ee814166444f -->
2
+ <!-- source-sha256: 8a5fd3a36879552fb183ec31edc5db34cfc22bc091d7e5434ab01206c053caf2 -->
3
3
  <!-- profile: cli -->
4
4
  <!-- rainskills-runtime-gate:start -->
5
5
  # 单运行环境 CLI 门禁(生成文件)
@@ -45,7 +45,7 @@ Hermes Agent 使用 `terminal` 且 `background=true` 启动授权,随后只对
45
45
  ```json
46
46
  {
47
47
  "schema": "rainskills.single-runtime-contract.v1",
48
- "package_version": "rainskills@0.1.44",
48
+ "package_version": "rainskills@0.1.45",
49
49
  "runtime_status": [
50
50
  "node",
51
51
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -32,7 +32,7 @@ Do not use it to deploy an application to an existing Rainbond. Route those requ
32
32
  ## Workflow
33
33
 
34
34
  1. Read [installation-policy.md](references/installation-policy.md).
35
- 2. Use the installed local launcher `["node", "<home>/.rainbond/lib/rainskills/bin/rainskills.js"]`; its protected runtime package marker is `rainskills@0.1.44` and must equal this package's `package.json`. For a Rainskills marker, first validate schema `rainskills.next-action.v1`, action, onboarding id, and the bounded `argv` array, then append that array to the launcher. Never use `latest` or evaluate a shell string from output.
35
+ 2. Use the installed local launcher `["node", "<home>/.rainbond/lib/rainskills/bin/rainskills.js"]`; its protected runtime package marker is `rainskills@0.1.45` and must equal this package's `package.json`. For a Rainskills marker, first validate schema `rainskills.next-action.v1`, action, onboarding id, and the bounded `argv` array, then append that array to the launcher. Never use `latest` or evaluate a shell string from output.
36
36
  3. 业务 Skill 的四项运行环境菜单会把本机或独立服务器选择写入 `rainskills.next-action.v1` 的显式 `--location`;收到这类 next-action 后直接执行固定 argv,不得再次调用 `private-deployment-location`。只有用户直接要求安装 Rainbond 平台且尚未选择部署位置时,才执行 launcher + `["runtime", "message", "--id", "private-deployment-location"]` 并原样输出固定的三项部署位置消息:选择 1 后执行带 `["--location", "local", "--mode", "single-node"]` 的 `platform install`;选择 2 后执行带 `["--location", "server"]` 的 `platform install`,由 helper 继续显示固定的服务器类型消息;选择 3 后执行 launcher + `["runtime", "message", "--id", "private-console-origin"]` 并进入已有环境连接,不得执行 `platform install`。平台安装 onboarding 只保存安装断点,不保存或恢复业务 intent。
37
37
  4. Let the helper perform one read-only preflight against the already selected local or remote target, then show resources, blockers, and applicable host changes. Never invoke `platform install` without an explicit `--location`; the helper must not ask for the deployment location again.
38
38
  5. 主机集群开始前必须获得 explicit confirmation,并使用受限 AI 交接:首次调用在固定安装 argv 后追加 `--agent-handoff`,记录该子进程会话;用户确认后,使用相同固定 argv 追加 `--agent-handoff --yes` 一次。不得向等待进程写入 `y`、不得启动第二个竞争安装、不得 `kill` 安装进程,也不得让用户复制完整安装或恢复命令。用户取消时,仅使用同一 argv 追加 `--agent-handoff --cancel`;它只能清除匹配的待确认状态,不能建立 SSH 连接或修改服务器。
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
 
3
3
  module.exports = Object.freeze({
4
- version: "0.1.44",
4
+ version: "0.1.45",
5
5
  });
@@ -1,5 +1,5 @@
1
1
  <!-- generated-by: scripts/sync-runtime-contracts.mjs -->
2
- <!-- source-sha256: 6e8d3eb9fe4bbc9d07ce8318824b6b0f35cdd938bb90819e4f7a882b2231a432 -->
2
+ <!-- source-sha256: dc5c47857833406978d2b76c11de816e365450e6944edca5f4e1c374e108ddb8 -->
3
3
  <!-- profile: cli -->
4
4
  <!-- rainskills-runtime-gate:start -->
5
5
  # 单运行环境 CLI 门禁(生成文件)
@@ -45,7 +45,7 @@ Hermes Agent 使用 `terminal` 且 `background=true` 启动授权,随后只对
45
45
  ```json
46
46
  {
47
47
  "schema": "rainskills.single-runtime-contract.v1",
48
- "package_version": "rainskills@0.1.44",
48
+ "package_version": "rainskills@0.1.45",
49
49
  "runtime_status": [
50
50
  "node",
51
51
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -1,5 +1,5 @@
1
1
  <!-- generated-by: scripts/sync-runtime-contracts.mjs -->
2
- <!-- source-sha256: 6680fe773d693223188f304b9c560d978d38fd015090bff55b806017be5ee165 -->
2
+ <!-- source-sha256: 4146c5cf054762b12425ac5d613e8dbb51990d28d5934ef72d8e80d155a664e2 -->
3
3
  <!-- profile: cli -->
4
4
  <!-- rainskills-runtime-gate:start -->
5
5
  # 单运行环境 CLI 门禁(生成文件)
@@ -45,7 +45,7 @@ Hermes Agent 使用 `terminal` 且 `background=true` 启动授权,随后只对
45
45
  ```json
46
46
  {
47
47
  "schema": "rainskills.single-runtime-contract.v1",
48
- "package_version": "rainskills@0.1.44",
48
+ "package_version": "rainskills@0.1.45",
49
49
  "runtime_status": [
50
50
  "node",
51
51
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -1,5 +1,5 @@
1
1
  <!-- generated-by: scripts/sync-runtime-contracts.mjs -->
2
- <!-- source-sha256: 7b78902f6a310444505373a91113a650031ee468dc3bd61c5bd7f70966878890 -->
2
+ <!-- source-sha256: 239ace1f451950a0f152afeeec2c84f4c033c743e9dce8fcc340c41b7ffb6572 -->
3
3
  <!-- profile: cli -->
4
4
  <!-- rainskills-runtime-gate:start -->
5
5
  # 单运行环境 CLI 门禁(生成文件)
@@ -45,7 +45,7 @@ Hermes Agent 使用 `terminal` 且 `background=true` 启动授权,随后只对
45
45
  ```json
46
46
  {
47
47
  "schema": "rainskills.single-runtime-contract.v1",
48
- "package_version": "rainskills@0.1.44",
48
+ "package_version": "rainskills@0.1.45",
49
49
  "runtime_status": [
50
50
  "node",
51
51
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -1,5 +1,5 @@
1
1
  <!-- generated-by: scripts/sync-runtime-contracts.mjs -->
2
- <!-- source-sha256: 8776f14d0763a14a1ec01da5080e0131231b18133b10b274a38c5815564979e5 -->
2
+ <!-- source-sha256: 5c06904861105c30dd21af55b36bf828f63f78712e665cdc8fff2d7978fc18d8 -->
3
3
  <!-- profile: cli -->
4
4
  <!-- rainskills-runtime-gate:start -->
5
5
  # 单运行环境 CLI 门禁(生成文件)
@@ -45,7 +45,7 @@ Hermes Agent 使用 `terminal` 且 `background=true` 启动授权,随后只对
45
45
  ```json
46
46
  {
47
47
  "schema": "rainskills.single-runtime-contract.v1",
48
- "package_version": "rainskills@0.1.44",
48
+ "package_version": "rainskills@0.1.45",
49
49
  "runtime_status": [
50
50
  "node",
51
51
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -144,7 +144,7 @@ export function buildSkillManifest({ source_root: sourceRoot, output, revision }
144
144
  ].sort((left, right) => path.basename(left).localeCompare(path.basename(right)));
145
145
  if (skillDirectories.length === 0) throw new Error("no RainSkills Skill directories were found");
146
146
  for (const directory of skillDirectories) assertRegularDirectory(directory, "Skill directory");
147
- assertOutputOutsideSkills(output, skillDirectories);
147
+ if (output !== undefined) assertOutputOutsideSkills(output, skillDirectories);
148
148
 
149
149
  const skills = skillDirectories.map((directory) => {
150
150
  const id = path.basename(directory);
@@ -176,7 +176,8 @@ export function buildSkillManifest({ source_root: sourceRoot, output, revision }
176
176
  source_revision: revision || null,
177
177
  skills,
178
178
  };
179
- writePrivateJson(output, manifest);
179
+ // Installers can compare the complete expected manifest before creating staging.
180
+ if (output !== undefined) writePrivateJson(output, manifest);
180
181
  return manifest;
181
182
  }
182
183
 
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import crypto from "node:crypto";
4
3
  import fs from "node:fs";
5
4
  import os from "node:os";
6
5
  import path from "node:path";
@@ -54,58 +53,89 @@ function assertRegularPath(absolute, expected) {
54
53
  }
55
54
  }
56
55
 
57
- function copyTree(source, destination) {
58
- assertRegularPath(source, "directory");
59
- fs.mkdirSync(destination, { recursive: true, mode: 0o700 });
60
- for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
61
- const from = path.join(source, entry.name);
62
- const to = path.join(destination, entry.name);
63
- const info = fs.lstatSync(from);
64
- if (info.isSymbolicLink()) throw new Error("runtime bundle cannot contain symbolic links");
65
- if (info.isDirectory()) copyTree(from, to);
66
- else if (info.isFile()) fs.copyFileSync(from, to, fs.constants.COPYFILE_EXCL);
67
- else throw new Error("runtime bundle contains an unsupported file type");
56
+ function lstatIfPresent(file) {
57
+ try { return fs.lstatSync(file); } catch (error) {
58
+ if (error.code === "ENOENT") return null;
59
+ throw error;
68
60
  }
69
61
  }
70
62
 
71
- function replaceDirectory(staged, destination) {
72
- const previous = `${destination}.previous.${process.pid}`;
73
- fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
74
- if (fs.existsSync(destination)) fs.renameSync(destination, previous);
75
- try {
76
- fs.renameSync(staged, destination);
77
- } catch (error) {
78
- if (fs.existsSync(previous)) fs.renameSync(previous, destination);
79
- throw error;
63
+ // Capture bytes before replacing anything: source may itself be the installed CLI.
64
+ function readTree(root, { privateDirectories = false } = {}) {
65
+ if (!lstatIfPresent(root)) return null;
66
+ const entries = new Map();
67
+ function visit(relative) {
68
+ const file = path.join(root, relative);
69
+ const info = fs.lstatSync(file);
70
+ if (info.isSymbolicLink()) throw new Error(`symbolic link is not allowed in CLI paths: ${file}`);
71
+ if (info.isDirectory()) {
72
+ entries.set(relative, { kind: "directory", mode: privateDirectories ? 0o700 : info.mode & 0o777 });
73
+ for (const name of fs.readdirSync(file).sort()) visit(path.join(relative, name));
74
+ } else if (info.isFile()) {
75
+ entries.set(relative, { kind: "file", mode: info.mode & 0o777, content: fs.readFileSync(file) });
76
+ } else throw new Error(`unsupported CLI file type: ${file}`);
80
77
  }
81
- if (fs.existsSync(previous)) fs.rmSync(previous, { recursive: true, force: true });
78
+ visit("");
79
+ return entries;
80
+ }
81
+
82
+ function directoryTree() {
83
+ return new Map([["", { kind: "directory", mode: 0o700 }]]);
84
+ }
85
+
86
+ function fileTree(content, mode) {
87
+ return new Map([["", { kind: "file", mode, content: Buffer.from(content) }]]);
82
88
  }
83
89
 
84
- function installRuntimeBundle(source, stagedBundle) {
85
- fs.mkdirSync(stagedBundle, { mode: 0o700 });
86
- for (const directory of RUNTIME_BUNDLE_DIRECTORIES) {
87
- copyTree(path.join(source, directory), path.join(stagedBundle, directory));
90
+ function addSource(tree, source, relative, kind, mode) {
91
+ assertRegularPath(source, kind);
92
+ const entries = readTree(source, { privateDirectories: true });
93
+ if (mode !== undefined) entries.get("").mode = mode;
94
+ for (const [name, entry] of entries) tree.set(relative ? path.join(relative, name) : name, entry);
95
+ }
96
+
97
+ function treesEqual(expected, actual) {
98
+ if (!expected || !actual) return expected === actual;
99
+ if (expected.size !== actual.size) return false;
100
+ for (const [name, entry] of expected) {
101
+ const other = actual.get(name);
102
+ if (!other || other.kind !== entry.kind) return false;
103
+ // Windows ACL protection is not represented by POSIX mode bits.
104
+ if (process.platform !== "win32" && other.mode !== entry.mode) return false;
105
+ if (entry.kind === "file" && !entry.content.equals(other.content)) return false;
88
106
  }
89
- for (const file of RUNTIME_BUNDLE_FILES) {
90
- atomicCopy(path.join(source, file), path.join(stagedBundle, file), file === "install.sh" ? 0o700 : 0o600);
107
+ return true;
108
+ }
109
+
110
+ function writeTree(root, tree) {
111
+ for (const [relative, entry] of tree) {
112
+ const target = path.join(root, relative);
113
+ if (entry.kind === "directory") fs.mkdirSync(target, { mode: entry.mode });
114
+ else fs.writeFileSync(target, entry.content, { mode: entry.mode, flag: "wx" });
115
+ fs.chmodSync(target, entry.mode);
91
116
  }
92
- const yamlPackage = requireFromInstaller.resolve("yaml/package.json", { paths: [source] });
93
- copyTree(path.dirname(yamlPackage), path.join(stagedBundle, "node_modules", "yaml"));
94
117
  }
95
118
 
96
- function atomicCopy(source, destination, mode) {
97
- assertRegularPath(source, "file");
98
- const parent = path.dirname(destination);
99
- fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
100
- const temporary = path.join(parent, `.${path.basename(destination)}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`);
101
- try {
102
- fs.copyFileSync(source, temporary, fs.constants.COPYFILE_EXCL);
103
- fs.chmodSync(temporary, mode);
104
- fs.renameSync(temporary, destination);
105
- fs.chmodSync(destination, mode);
106
- } catch (error) {
107
- try { fs.rmSync(temporary, { force: true }); } catch { /* keep original error */ }
108
- throw error;
119
+ function assertDestinationParents(home, destination) {
120
+ const relative = path.relative(home, destination);
121
+ if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
122
+ throw new Error("CLI destination must be inside the selected home");
123
+ }
124
+ let current = home;
125
+ for (const segment of relative.split(path.sep).slice(0, -1)) {
126
+ current = path.join(current, segment);
127
+ const info = lstatIfPresent(current);
128
+ if (info && (info.isSymbolicLink() || !info.isDirectory())) {
129
+ throw new Error(`CLI destination parent must be a regular directory, not a symbolic link: ${current}`);
130
+ }
131
+ }
132
+ }
133
+
134
+ function ensurePrivateDirectory(directory) {
135
+ if (!lstatIfPresent(directory)) fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
136
+ assertRegularPath(directory, "directory");
137
+ if (process.platform !== "win32" && (fs.statSync(directory).mode & 0o777) !== 0o700) {
138
+ fs.chmodSync(directory, 0o700);
109
139
  }
110
140
  }
111
141
 
@@ -119,19 +149,11 @@ function safeConfigFile(file) {
119
149
  }
120
150
  }
121
151
 
122
- function writeMigratedConfig(file, content) {
123
- const temporary = `${file}.rainskills.${process.pid}.${crypto.randomBytes(5).toString("hex")}.tmp`;
124
- const backup = `${file}.rainskills-backup`;
125
- fs.copyFileSync(file, backup);
126
- fs.chmodSync(backup, 0o600);
127
- try {
128
- fs.writeFileSync(temporary, content, { encoding: "utf8", flag: "wx", mode: 0o600 });
129
- fs.renameSync(temporary, file);
130
- fs.chmodSync(file, 0o600);
131
- } catch (error) {
132
- try { fs.rmSync(temporary, { force: true }); } catch { /* keep original error */ }
133
- throw error;
134
- }
152
+ function migratedConfig(file, original, content) {
153
+ return [
154
+ { destination: `${file}.rainskills-backup`, tree: fileTree(original, 0o600) },
155
+ { destination: file, tree: fileTree(content, 0o600) },
156
+ ];
135
157
  }
136
158
 
137
159
  function isLegacyRainskillsMcp(server) {
@@ -146,7 +168,7 @@ function isLegacyRainskillsMcp(server) {
146
168
 
147
169
  function migrateCodex(home) {
148
170
  const file = path.join(home, ".codex", "config.toml");
149
- if (!safeConfigFile(file)) return false;
171
+ if (!safeConfigFile(file)) return [];
150
172
  const source = fs.readFileSync(file, "utf8");
151
173
  const sections = source.split(/(?=^\s*\[[^\r\n]+\]\s*$)/m);
152
174
  const filtered = sections.filter((section) => {
@@ -157,54 +179,122 @@ function migrateCodex(home) {
157
179
  || !/["']serve["']/i.test(section);
158
180
  });
159
181
  const output = filtered.join("");
160
- if (output === source) return false;
161
- writeMigratedConfig(file, output);
162
- return true;
182
+ if (output === source) return [];
183
+ return migratedConfig(file, source, output);
163
184
  }
164
185
 
165
186
  function migrateClaude(home) {
166
187
  const file = path.join(home, ".claude.json");
167
- if (!safeConfigFile(file)) return false;
188
+ if (!safeConfigFile(file)) return [];
168
189
  const source = fs.readFileSync(file, "utf8");
169
190
  const payload = JSON.parse(source);
170
- if (!isLegacyRainskillsMcp(payload?.mcpServers?.rainbond)) return false;
191
+ if (!isLegacyRainskillsMcp(payload?.mcpServers?.rainbond)) return [];
171
192
  delete payload.mcpServers.rainbond;
172
- writeMigratedConfig(file, `${JSON.stringify(payload, null, 2)}\n`);
173
- return true;
193
+ return migratedConfig(file, source, `${JSON.stringify(payload, null, 2)}\n`);
174
194
  }
175
195
 
176
- export function installLocalCli({ source_root: sourceRoot, home }) {
196
+ export function installLocalCli({ source_root: sourceRoot, home, warn = (message) => process.stderr.write(`${message}\n`) }) {
177
197
  const source = path.resolve(sourceRoot);
178
198
  const targetHome = path.resolve(home);
179
- assertRegularPath(source, "directory");
180
199
  const rainbondHome = path.join(targetHome, ".rainbond");
181
200
  const binDirectory = path.join(rainbondHome, "bin");
182
201
  const libDirectory = path.join(rainbondHome, "lib");
183
- fs.mkdirSync(rainbondHome, { recursive: true, mode: 0o700 });
184
- fs.chmodSync(rainbondHome, 0o700);
185
- const staging = path.join(rainbondHome, `.cli-install.${process.pid}.${crypto.randomBytes(6).toString("hex")}`);
186
- fs.mkdirSync(staging, { mode: 0o700 });
202
+ let staging;
203
+ let phase = "prepare";
204
+ let retainBackups = false;
205
+ const transactions = [];
206
+ const cleanupPending = [];
207
+ const warning = (message) => { try { warn(message); } catch { /* diagnostics cannot mask an install error */ } };
187
208
  try {
188
- const stagedRuntime = path.join(staging, "rainbond-platform-installer", "scripts");
189
- copyTree(path.join(source, "rainbond-platform-installer", "scripts"), stagedRuntime);
190
- const stagedBundle = path.join(staging, "rainskills");
191
- installRuntimeBundle(source, stagedBundle);
192
- const runtimeTarget = path.join(libDirectory, "rainbond-platform-installer");
193
- fs.mkdirSync(libDirectory, { recursive: true, mode: 0o700 });
194
- replaceDirectory(path.join(staging, "rainbond-platform-installer"), runtimeTarget);
195
- replaceDirectory(stagedBundle, path.join(libDirectory, "rainskills"));
196
-
197
- const stagedManifest = path.join(staging, "rainskills-skill-manifest.json");
198
- buildSkillManifest({ source_root: source, output: stagedManifest });
199
- atomicCopy(path.join(source, "bin", "rainskills-tools.js"), path.join(binDirectory, "rainskills-tools.js"), 0o700);
200
- atomicCopy(path.join(source, "bin", "delivery-probe.js"), path.join(binDirectory, "delivery-probe.js"), 0o600);
201
- atomicCopy(stagedManifest, path.join(binDirectory, "rainskills-skill-manifest.json"), 0o600);
202
- migrateCodex(targetHome);
203
- migrateClaude(targetHome);
209
+ assertRegularPath(source, "directory");
210
+ const runtime = directoryTree();
211
+ addSource(runtime, path.join(source, "rainbond-platform-installer", "scripts"), "scripts", "directory");
212
+ const bundle = directoryTree();
213
+ for (const directory of RUNTIME_BUNDLE_DIRECTORIES) addSource(bundle, path.join(source, directory), directory, "directory");
214
+ for (const file of RUNTIME_BUNDLE_FILES) addSource(bundle, path.join(source, file), file, "file", file === "install.sh" ? 0o700 : 0o600);
215
+ bundle.set("node_modules", { kind: "directory", mode: 0o700 });
216
+ const yamlPackage = requireFromInstaller.resolve("yaml/package.json", { paths: [source] });
217
+ addSource(bundle, path.dirname(yamlPackage), path.join("node_modules", "yaml"), "directory");
218
+ const manifest = buildSkillManifest({ source_root: source });
219
+ const plan = [
220
+ { destination: path.join(libDirectory, "rainbond-platform-installer"), tree: runtime },
221
+ { destination: path.join(libDirectory, "rainskills"), tree: bundle },
222
+ ];
223
+ for (const [name, mode] of [["rainskills-tools.js", 0o700], ["delivery-probe.js", 0o600]]) {
224
+ const tree = new Map();
225
+ addSource(tree, path.join(source, "bin", name), "", "file", mode);
226
+ plan.push({ destination: path.join(binDirectory, name), tree });
227
+ }
228
+ plan.push({ destination: path.join(binDirectory, "rainskills-skill-manifest.json"), tree: fileTree(`${JSON.stringify(manifest, null, 2)}\n`, 0o600) });
229
+ plan.push(...migrateCodex(targetHome), ...migrateClaude(targetHome));
230
+ const changes = plan.filter((entry) => {
231
+ assertDestinationParents(targetHome, entry.destination);
232
+ entry.original = readTree(entry.destination);
233
+ return !treesEqual(entry.tree, entry.original);
234
+ });
235
+ for (const directory of [rainbondHome, libDirectory, binDirectory]) ensurePrivateDirectory(directory);
236
+ if (changes.length === 0) return { binDirectory, status: "unchanged", cleanupPending };
237
+
238
+ phase = "stage";
239
+ staging = fs.mkdtempSync(path.join(rainbondHome, `.cli-install.${process.pid}.`));
240
+ fs.chmodSync(staging, 0o700);
241
+ fs.mkdirSync(path.join(staging, "new"), { mode: 0o700 });
242
+ fs.mkdirSync(path.join(staging, "backup"), { mode: 0o700 });
243
+ for (const [index, entry] of changes.entries()) {
244
+ const staged = path.join(staging, "new", String(index));
245
+ const backup = path.join(staging, "backup", String(index));
246
+ writeTree(staged, entry.tree);
247
+ if (!treesEqual(entry.tree, readTree(staged))) throw new Error(`staged content mismatch: ${entry.destination}`);
248
+ transactions.push({ ...entry, staged, backup, backedUp: false, published: false });
249
+ }
250
+ fs.writeFileSync(path.join(staging, "transaction.json"), `${JSON.stringify({
251
+ schema: "rainskills.cli-install-recovery.v1",
252
+ entries: transactions.map(({ destination, staged, backup, original }) => ({
253
+ destination, staged, backup, hadOriginal: original !== null,
254
+ })),
255
+ }, null, 2)}\n`, { flag: "wx", mode: 0o600 });
256
+ phase = "replace";
257
+ for (const entry of transactions) {
258
+ assertDestinationParents(targetHome, entry.destination);
259
+ if (!treesEqual(entry.original, readTree(entry.destination))) {
260
+ throw new Error(`CLI destination changed during preparation: ${entry.destination}`);
261
+ }
262
+ if (lstatIfPresent(entry.destination)) {
263
+ fs.renameSync(entry.destination, entry.backup);
264
+ entry.backedUp = true;
265
+ }
266
+ fs.renameSync(entry.staged, entry.destination);
267
+ entry.published = true;
268
+ }
269
+ phase = "verify";
270
+ for (const entry of plan) {
271
+ if (!treesEqual(entry.tree, readTree(entry.destination))) throw new Error(`installed content mismatch: ${entry.destination}`);
272
+ }
273
+ } catch (error) {
274
+ const rollbackErrors = [];
275
+ for (const entry of [...transactions].reverse()) {
276
+ try {
277
+ if (entry.published) fs.renameSync(entry.destination, entry.staged);
278
+ if (entry.backedUp) fs.renameSync(entry.backup, entry.destination);
279
+ } catch (rollbackError) {
280
+ rollbackErrors.push(`${entry.destination}: ${rollbackError.message}`);
281
+ }
282
+ }
283
+ retainBackups = rollbackErrors.length > 0;
284
+ const recovery = retainBackups ? `; rollback incomplete; backups retained at ${staging}: ${rollbackErrors.join("; ")}` : "";
285
+ const failure = new Error(`[rainskills-cli][${phase}] ${error.message}${recovery}`, { cause: error });
286
+ failure.code = error.code || "RAINSKILLS_CLI_INSTALL_FAILED";
287
+ failure.phase = phase;
288
+ throw failure;
204
289
  } finally {
205
- fs.rmSync(staging, { recursive: true, force: true });
290
+ if (staging && !retainBackups) {
291
+ try { fs.rmSync(staging, { recursive: true, force: true }); } catch (error) {
292
+ cleanupPending.push(staging);
293
+ warning(`[rainskills-cli][cleanup] 临时文件待清理,已保留 ${staging}: ${error.message}`);
294
+ }
295
+ }
206
296
  }
207
- return { binDirectory };
297
+ return { binDirectory, status: "installed", cleanupPending };
208
298
  }
209
299
 
210
300
  if (path.resolve(process.argv[1] || "") === fileURLToPath(import.meta.url)) {