@zhuoyuezs/ml-platform 0.2.1 → 0.2.2-alpha.1

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/scripts/lib.js CHANGED
@@ -1,12 +1,11 @@
1
1
  "use strict";
2
-
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const crypto = require("crypto");
4
4
  const fs = require("fs");
5
5
  const os = require("os");
6
6
  const path = require("path");
7
7
  const { spawnSync } = require("child_process");
8
8
  const RELEASE_POLICY = require("../release-policy.json");
9
-
10
9
  const PACKAGE_CONFIG = require("../package.json");
11
10
  const PACKAGE_NAME = PACKAGE_CONFIG.name;
12
11
  const PACKAGE_VERSION = PACKAGE_CONFIG.version;
@@ -14,1111 +13,1420 @@ const CLI_NAME = "ml-platform";
14
13
  const SKILL_NAME = "feature-management";
15
14
  const RELEASE_SCHEMA = "data_platform.ml_platform_release/v2";
16
15
  const CHECKSUM_SCHEMA = "data_platform.ml_platform_checksums/v1";
17
- const RECORD_SCHEMA = "data_platform.ml_platform_installation/v2";
18
- const RUNTIME_RECORD_SCHEMA = "data_platform.ml_platform_runtime_installation/v1";
16
+ const RECORD_SCHEMA = "data_platform.ml_platform_installation/v3";
17
+ const LEGACY_RECORD_SCHEMA = "data_platform.ml_platform_installation/v2";
18
+ const RUNTIME_RECORD_SCHEMA = "data_platform.ml_platform_runtime_installation/v2";
19
+ const LEGACY_RUNTIME_RECORD_SCHEMA = "data_platform.ml_platform_runtime_installation/v1";
20
+ const REGISTRY_SCHEMA = "data_platform.ml_platform_installation_registry/v1";
19
21
  const PACKAGE_ROOT = path.resolve(__dirname, "..");
20
22
  const SKILL_TOP_LEVEL = new Set(["SKILL.md", "agents", "assets", "references"]);
21
23
  const CLIENT_TOP_LEVEL = new Set(["package.json", "package-lock.json", "README.md", "src"]);
22
24
  if (RELEASE_POLICY.schema_version !== "data_platform.ml_platform_release_policy/v1") {
23
- throw new Error("不支持的 release policy schema");
25
+ throw new Error("不支持的 release policy schema");
24
26
  }
25
27
  const FORBIDDEN_DIRECTORIES = new Set(RELEASE_POLICY.forbidden_directories);
26
28
  const FORBIDDEN_FILES = new Set(RELEASE_POLICY.forbidden_files);
27
29
  const FORBIDDEN_SUFFIXES = new Set(RELEASE_POLICY.forbidden_suffixes);
28
30
  const FORBIDDEN_CONTENT_PATTERNS = (RELEASE_POLICY.forbidden_content_patterns || []).map((item) => {
29
- if (!item || typeof item.name !== "string" || typeof item.pattern !== "string") {
30
- throw new Error("release policy 包含无效内容规则");
31
- }
32
- return { name: item.name, pattern: new RegExp(item.pattern, "i") };
31
+ if (!item || typeof item.name !== "string" || typeof item.pattern !== "string") {
32
+ throw new Error("release policy 包含无效内容规则");
33
+ }
34
+ return { name: item.name, pattern: new RegExp(item.pattern, "i") };
33
35
  });
34
-
35
36
  function parseOptions(args) {
36
- const options = {
37
- agent: "codex",
38
- scope: "user",
39
- upgrade: false,
40
- allowDowngrade: false,
41
- prewarm: true,
42
- backupUnmanaged: false,
43
- summary: true,
44
- };
45
- let outputFlag = null;
46
- const valueFlags = new Map([
47
- ["--agent", "agent"],
48
- ["--scope", "scope"],
49
- ["--skills-dir", "skillsDir"],
50
- ["--project-dir", "projectDir"],
51
- ["--state-dir", "stateDir"],
52
- ["--bin-dir", "binDir"],
53
- ["--api-url", "apiUrl"],
54
- ]);
55
- for (let index = 0; index < args.length; index += 1) {
56
- const arg = args[index];
57
- if (valueFlags.has(arg)) {
58
- const value = args[index + 1];
59
- if (!value || value.startsWith("--")) throw new Error(`${arg} 缺少值`);
60
- options[valueFlags.get(arg)] = value;
61
- index += 1;
62
- } else if (arg === "--upgrade") {
63
- options.upgrade = true;
64
- } else if (arg === "--allow-downgrade") {
65
- options.allowDowngrade = true;
66
- } else if (arg === "--no-prewarm") {
67
- options.prewarm = false;
68
- } else if (arg === "--backup-unmanaged") {
69
- options.backupUnmanaged = true;
70
- } else if (arg === "--summary") {
71
- if (outputFlag === "json") throw new Error("--summary 与 --json 不能同时使用");
72
- outputFlag = "summary";
73
- options.summary = true;
74
- } else if (arg === "--json") {
75
- if (outputFlag === "summary") throw new Error("--summary 与 --json 不能同时使用");
76
- outputFlag = "json";
77
- options.summary = false;
78
- } else {
79
- throw new Error(`未知参数: ${arg}`);
80
- }
81
- }
82
- return options;
83
- }
84
-
37
+ const options = {
38
+ agent: "codex",
39
+ scope: "user",
40
+ upgrade: false,
41
+ allowDowngrade: false,
42
+ prewarm: true,
43
+ backupUnmanaged: false,
44
+ summary: true,
45
+ };
46
+ let outputFlag = null;
47
+ const valueFlags = new Map([
48
+ ["--agent", "agent"],
49
+ ["--scope", "scope"],
50
+ ["--skills-dir", "skillsDir"],
51
+ ["--project-dir", "projectDir"],
52
+ ["--state-dir", "stateDir"],
53
+ ["--bin-dir", "binDir"],
54
+ ["--api-url", "apiUrl"],
55
+ ]);
56
+ for (let index = 0; index < args.length; index += 1) {
57
+ const arg = args[index];
58
+ if (valueFlags.has(arg)) {
59
+ const value = args[index + 1];
60
+ if (!value || value.startsWith("--"))
61
+ throw new Error(`${arg} 缺少值`);
62
+ options[valueFlags.get(arg)] = value;
63
+ index += 1;
64
+ }
65
+ else if (arg === "--upgrade") {
66
+ options.upgrade = true;
67
+ }
68
+ else if (arg === "--allow-downgrade") {
69
+ options.allowDowngrade = true;
70
+ }
71
+ else if (arg === "--no-prewarm") {
72
+ options.prewarm = false;
73
+ }
74
+ else if (arg === "--backup-unmanaged") {
75
+ options.backupUnmanaged = true;
76
+ }
77
+ else if (arg === "--summary") {
78
+ if (outputFlag === "json")
79
+ throw new Error("--summary 与 --json 不能同时使用");
80
+ outputFlag = "summary";
81
+ options.summary = true;
82
+ }
83
+ else if (arg === "--json") {
84
+ if (outputFlag === "summary")
85
+ throw new Error("--summary 与 --json 不能同时使用");
86
+ outputFlag = "json";
87
+ options.summary = false;
88
+ }
89
+ else {
90
+ throw new Error(`未知参数: ${arg}`);
91
+ }
92
+ }
93
+ return options;
94
+ }
85
95
  function resolveInstallPaths(options = {}) {
86
- const agent = options.agent || "codex";
87
- const scope = options.scope || "user";
88
- if (!["codex", "pi", "custom"].includes(agent)) throw new Error(`不支持的 Agent: ${agent}`);
89
- if (!["user", "project"].includes(scope)) throw new Error(`不支持的安装作用域: ${scope}`);
90
-
91
- let skillsDir;
92
- let projectDir = null;
93
- if (options.skillsDir) {
94
- skillsDir = path.resolve(expandHome(options.skillsDir));
95
- } else if (agent === "codex" && scope === "user") {
96
- const codexHome = process.env.CODEX_HOME
97
- ? path.resolve(expandHome(process.env.CODEX_HOME))
98
- : path.join(os.homedir(), ".codex");
99
- skillsDir = path.join(codexHome, "skills");
100
- } else if (agent === "codex" && scope === "project") {
101
- projectDir = path.resolve(expandHome(options.projectDir || process.cwd()));
102
- if (!fs.existsSync(projectDir)) throw new Error(`Codex 项目目录不存在: ${projectDir}`);
103
- if (!fs.statSync(projectDir).isDirectory()) throw new Error(`Codex 项目路径不是目录: ${projectDir}`);
104
- skillsDir = path.join(projectDir, ".agents", "skills");
105
- } else {
106
- throw new Error(`${agent} 安装必须显式提供 --skills-dir`);
107
- }
108
-
109
- const defaultDataRoot = process.env.XDG_DATA_HOME
110
- ? path.join(path.resolve(expandHome(process.env.XDG_DATA_HOME)), "ml-platform")
111
- : path.join(os.homedir(), ".local", "share", "ml-platform");
112
- const stateDir = path.resolve(expandHome(
113
- options.stateDir || process.env.ML_PLATFORM_HOME || defaultDataRoot,
114
- ));
115
- const binDir = path.resolve(expandHome(
116
- options.binDir || process.env.ML_PLATFORM_BIN_DIR || path.join(os.homedir(), ".local", "bin"),
117
- ));
118
- const installRoot = path.join(stateDir, "current");
119
- const target = path.join(skillsDir, SKILL_NAME);
120
- return {
121
- agent,
122
- scope,
123
- projectDir,
124
- skillsDir,
125
- target,
126
- record: path.join(skillsDir, `.${SKILL_NAME}.ml-platform.json`),
127
- stateDir,
128
- installRoot,
129
- clientRoot: path.join(installRoot, "runtime", "business-client"),
130
- dispatcher: path.join(installRoot, "scripts", "main.js"),
131
- runtimeRecord: path.join(installRoot, "ownership.json"),
132
- lock: path.join(stateDir, ".install.lock"),
133
- binDir,
134
- shim: path.join(binDir, CLI_NAME),
135
- };
136
- }
137
-
96
+ const agent = options.agent || "codex";
97
+ const scope = options.scope || "user";
98
+ if (!["codex", "pi", "custom"].includes(agent))
99
+ throw new Error(`不支持的 Agent: ${agent}`);
100
+ if (!["user", "project"].includes(scope))
101
+ throw new Error(`不支持的安装作用域: ${scope}`);
102
+ let skillsDir;
103
+ let projectDir = null;
104
+ if (options.skillsDir) {
105
+ skillsDir = path.resolve(expandHome(options.skillsDir));
106
+ }
107
+ else if (agent === "codex" && scope === "user") {
108
+ const codexHome = process.env.CODEX_HOME
109
+ ? path.resolve(expandHome(process.env.CODEX_HOME))
110
+ : path.join(os.homedir(), ".codex");
111
+ skillsDir = path.join(codexHome, "skills");
112
+ }
113
+ else if (agent === "codex" && scope === "project") {
114
+ projectDir = path.resolve(expandHome(options.projectDir || process.cwd()));
115
+ if (!fs.existsSync(projectDir))
116
+ throw new Error(`Codex 项目目录不存在: ${projectDir}`);
117
+ if (!fs.statSync(projectDir).isDirectory())
118
+ throw new Error(`Codex 项目路径不是目录: ${projectDir}`);
119
+ skillsDir = path.join(projectDir, ".agents", "skills");
120
+ }
121
+ else {
122
+ throw new Error(`${agent} 安装必须显式提供 --skills-dir`);
123
+ }
124
+ const defaultDataRoot = process.env.XDG_DATA_HOME
125
+ ? path.join(path.resolve(expandHome(process.env.XDG_DATA_HOME)), "ml-platform")
126
+ : path.join(os.homedir(), ".local", "share", "ml-platform");
127
+ const stateDir = path.resolve(expandHome(options.stateDir || process.env.ML_PLATFORM_HOME || defaultDataRoot));
128
+ const binDir = path.resolve(expandHome(options.binDir || process.env.ML_PLATFORM_BIN_DIR || path.join(os.homedir(), ".local", "bin")));
129
+ const installRoot = path.join(stateDir, "current");
130
+ const target = path.join(skillsDir, SKILL_NAME);
131
+ return {
132
+ agent,
133
+ scope,
134
+ projectDir,
135
+ skillsDir,
136
+ target,
137
+ record: path.join(skillsDir, `.${SKILL_NAME}.ml-platform.json`),
138
+ stateDir,
139
+ installRoot,
140
+ clientRoot: path.join(installRoot, "runtime", "business-client"),
141
+ dispatcher: path.join(installRoot, "scripts", "main.js"),
142
+ runtimeRecord: path.join(installRoot, "ownership.json"),
143
+ registry: path.join(stateDir, "installations.json"),
144
+ lock: path.join(stateDir, ".install.lock"),
145
+ binDir,
146
+ shim: path.join(binDir, CLI_NAME),
147
+ };
148
+ }
138
149
  function loadRelease(packageRoot = PACKAGE_ROOT) {
139
- const release = readJson(path.join(packageRoot, "release.json"), "release manifest");
140
- if (release.schema_version !== RELEASE_SCHEMA) {
141
- throw new Error(`不支持的 release schema: ${release.schema_version}`);
142
- }
143
- const policyPath = path.join(packageRoot, "release-policy.json");
144
- if (!isSha256(release.policy_sha256) || !fs.existsSync(policyPath)
145
- || sha256File(policyPath) !== release.policy_sha256) {
146
- throw new Error("release policy 摘要与 release manifest 不匹配");
147
- }
148
- if (!isSemver(release.release_version)) throw new Error("release_version 不是有效 SemVer");
149
- const requirements = release.runtime_requirements;
150
- if (!requirements || requirements.node !== ">=18"
151
- || !Array.isArray(requirements.os)
152
- || requirements.os.join(",") !== "darwin,linux") {
153
- throw new Error("release manifest 缺少有效 runtime requirements");
154
- }
155
- const skills = release.skills;
156
- const skill = skills && skills[SKILL_NAME];
157
- if (!skill || Object.keys(skills).length < 1) {
158
- throw new Error("release manifest 缺少有效 Skill 信息");
159
- }
160
- for (const [name, item] of Object.entries(skills)) {
161
- if (item.path !== `skills/${name}` || !isSha256(item.sha256)
162
- || !isSemver(item.revision) || typeof item.requires_cli !== "string") throw new Error("release manifest 缺少有效 Skill 信息");
163
- }
164
- const cli = release.cli;
165
- if (!cli || cli.name !== CLI_NAME || cli.path !== "runtime/business-client"
166
- || !isSemver(cli.version) || !isSha256(cli.sha256) || cli.entrypoint !== "src/cli.js") {
167
- throw new Error("release manifest 缺少有效 JavaScript CLI 信息");
168
- }
169
- for (const item of Object.values(skills)) if (!versionSatisfies(cli.version, item.requires_cli)) throw new Error("Skill requires_cli release CLI version 不兼容");
170
- return release;
171
- }
172
-
150
+ const release = readJson(path.join(packageRoot, "release.json"), "release manifest");
151
+ if (release.schema_version !== RELEASE_SCHEMA) {
152
+ throw new Error(`不支持的 release schema: ${release.schema_version}`);
153
+ }
154
+ const policyPath = path.join(packageRoot, "release-policy.json");
155
+ if (!isSha256(release.policy_sha256) || !fs.existsSync(policyPath)
156
+ || sha256File(policyPath) !== release.policy_sha256) {
157
+ throw new Error("release policy 摘要与 release manifest 不匹配");
158
+ }
159
+ if (!isSemver(release.release_version))
160
+ throw new Error("release_version 不是有效 SemVer");
161
+ const requirements = release.runtime_requirements;
162
+ if (!requirements || requirements.node !== ">=18"
163
+ || !Array.isArray(requirements.os)
164
+ || requirements.os.join(",") !== "darwin,linux") {
165
+ throw new Error("release manifest 缺少有效 runtime requirements");
166
+ }
167
+ const skills = release.skills;
168
+ const skill = skills && skills[SKILL_NAME];
169
+ if (!skill || Object.keys(skills).length < 1) {
170
+ throw new Error("release manifest 缺少有效 Skill 信息");
171
+ }
172
+ for (const [name, item] of Object.entries(skills)) {
173
+ if (item.path !== `skills/${name}` || !isSha256(item.sha256)
174
+ || !isSemver(item.revision) || typeof item.requires_cli !== "string")
175
+ throw new Error("release manifest 缺少有效 Skill 信息");
176
+ }
177
+ const cli = release.cli;
178
+ if (!cli || cli.name !== CLI_NAME || cli.path !== "runtime/business-client"
179
+ || !isSemver(cli.version) || !isSha256(cli.sha256) || cli.entrypoint !== "src/cli.js") {
180
+ throw new Error("release manifest 缺少有效 JavaScript CLI 信息");
181
+ }
182
+ for (const item of Object.values(skills))
183
+ if (!versionSatisfies(cli.version, item.requires_cli))
184
+ throw new Error("Skill requires_cli 与 release CLI version 不兼容");
185
+ return release;
186
+ }
173
187
  function verifyPackage(packageRoot = PACKAGE_ROOT) {
174
- packageRoot = path.resolve(packageRoot);
175
- const release = loadRelease(packageRoot);
176
- const manifest = readJson(path.join(packageRoot, "checksums.json"), "checksums manifest");
177
- if (manifest.schema_version !== CHECKSUM_SCHEMA || !Array.isArray(manifest.files)) {
178
- throw new Error("checksums manifest 格式无效");
179
- }
180
- const seen = new Set();
181
- for (const item of manifest.files) {
182
- if (!item || typeof item.path !== "string" || !isSha256(item.sha256)
183
- || !Number.isSafeInteger(item.size_bytes) || item.size_bytes < 0) {
184
- throw new Error("checksums manifest 包含无效条目");
185
- }
186
- const relative = normalizeRelative(item.path);
187
- if (!relative.startsWith("runtime/") && !relative.startsWith("skills/")) {
188
- throw new Error(`checksum 路径不属于 runtime 或 skills: ${relative}`);
189
- }
190
- if (seen.has(relative)) throw new Error(`checksums manifest 路径重复: ${relative}`);
191
- seen.add(relative);
192
- const file = resolveInside(packageRoot, relative);
193
- if (!fs.existsSync(file) || !fs.statSync(file).isFile()) throw new Error(`发布文件不是普通文件: ${relative}`);
194
- if (fs.statSync(file).size !== item.size_bytes) throw new Error(`发布文件大小不匹配: ${relative}`);
195
- if (sha256File(file) !== item.sha256) throw new Error(`发布文件 SHA-256 不匹配: ${relative}`);
196
- }
197
-
198
- const releaseRoots = [path.join(packageRoot, "runtime"), path.join(packageRoot, "skills")];
199
- const actualFiles = releaseRoots.flatMap((root) => listFiles(root))
200
- .map((file) => path.relative(packageRoot, file).split(path.sep).join("/"));
201
- if (actualFiles.length !== seen.size || actualFiles.some((file) => !seen.has(file))) {
202
- throw new Error("runtime/skills 文件集合与 checksums manifest 不一致");
203
- }
204
-
205
- assertTopLevel(path.join(packageRoot, "runtime"), new Set(["business-client"]), "runtime");
206
- assertTopLevel(path.join(packageRoot, "skills"), new Set(Object.keys(release.skills)), "skills");
207
- const skillRoot = resolveInside(packageRoot, release.skills[SKILL_NAME].path);
208
- const clientRoot = resolveInside(packageRoot, release.cli.path);
209
- assertTopLevel(skillRoot, SKILL_TOP_LEVEL, "Skill");
210
- assertAllowedClient(clientRoot);
211
- rejectForbiddenTree(skillRoot, "Skill");
212
- rejectForbiddenTree(clientRoot, "business client");
213
- rejectForbiddenContent([
214
- ...listFiles(skillRoot),
215
- ...listFiles(clientRoot),
216
- ...["README.md", "DEVELOPMENT.md"]
217
- .map((name) => path.join(packageRoot, name))
218
- .filter((file) => fs.existsSync(file)),
219
- ], "release payload");
220
- if (fs.existsSync(path.join(skillRoot, "runtime")) || fs.existsSync(path.join(skillRoot, "scripts"))) {
221
- throw new Error("Skill 不得包含 CLI runtime 或 wrapper");
222
- }
223
- const cliEntries = releaseRoots.flatMap((root) => listFiles(root))
224
- .filter((file) => path.basename(file) === "cli.js");
225
- const expectedEntry = path.join(clientRoot, release.cli.entrypoint);
226
- if (cliEntries.length !== 1 || cliEntries[0] !== expectedEntry) {
227
- throw new Error("release 必须且只能包含一份 JavaScript business CLI");
228
- }
229
- const packageFiles = releaseRoots.flatMap((root) => listFiles(root))
230
- .filter((file) => path.basename(file) === "package.json");
231
- if (packageFiles.length !== 1 || packageFiles[0] !== path.join(clientRoot, "package.json")) {
232
- throw new Error("release 包含第二份 CLI 或未知 npm package");
233
- }
234
- if (treeDigest(skillRoot) !== release.skills[SKILL_NAME].sha256) {
235
- throw new Error("Skill 目录摘要与 release manifest 不一致");
236
- }
237
- for (const [name, item] of Object.entries(release.skills)) {
238
- const root = resolveInside(packageRoot, item.path);
239
- const actual = new Set(fs.readdirSync(root));
240
- if (!actual.has("SKILL.md") || !actual.has("agents") || [...actual].some((entry) => !SKILL_TOP_LEVEL.has(entry))) throw new Error(`Skill ${name} 目录结构无效`);
241
- rejectForbiddenTree(root, `Skill ${name}`);
242
- if (treeDigest(root) !== item.sha256) throw new Error(`Skill ${name} 目录摘要与 release manifest 不一致`);
243
- }
244
- if (treeDigest(clientRoot) !== release.cli.sha256) {
245
- throw new Error("JavaScript CLI 摘要与 release manifest 不一致");
246
- }
247
- if (!fs.existsSync(expectedEntry)) throw new Error("JavaScript CLI 入口不存在");
248
- return { release, skillRoot, skillRoots: Object.fromEntries(Object.entries(release.skills).map(([name, item]) => [name, resolveInside(packageRoot, item.path)])), clientRoot };
249
- }
250
-
188
+ packageRoot = path.resolve(packageRoot);
189
+ const release = loadRelease(packageRoot);
190
+ const manifest = readJson(path.join(packageRoot, "checksums.json"), "checksums manifest");
191
+ if (manifest.schema_version !== CHECKSUM_SCHEMA || !Array.isArray(manifest.files)) {
192
+ throw new Error("checksums manifest 格式无效");
193
+ }
194
+ const seen = new Set();
195
+ for (const item of manifest.files) {
196
+ if (!item || typeof item.path !== "string" || !isSha256(item.sha256)
197
+ || !Number.isSafeInteger(item.size_bytes) || item.size_bytes < 0) {
198
+ throw new Error("checksums manifest 包含无效条目");
199
+ }
200
+ const relative = normalizeRelative(item.path);
201
+ if (!relative.startsWith("runtime/") && !relative.startsWith("skills/")) {
202
+ throw new Error(`checksum 路径不属于 runtime 或 skills: ${relative}`);
203
+ }
204
+ if (seen.has(relative))
205
+ throw new Error(`checksums manifest 路径重复: ${relative}`);
206
+ seen.add(relative);
207
+ const file = resolveInside(packageRoot, relative);
208
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile())
209
+ throw new Error(`发布文件不是普通文件: ${relative}`);
210
+ if (fs.statSync(file).size !== item.size_bytes)
211
+ throw new Error(`发布文件大小不匹配: ${relative}`);
212
+ if (sha256File(file) !== item.sha256)
213
+ throw new Error(`发布文件 SHA-256 不匹配: ${relative}`);
214
+ }
215
+ const releaseRoots = [path.join(packageRoot, "runtime"), path.join(packageRoot, "skills")];
216
+ const actualFiles = releaseRoots.flatMap((root) => listFiles(root))
217
+ .map((file) => path.relative(packageRoot, file).split(path.sep).join("/"));
218
+ if (actualFiles.length !== seen.size || actualFiles.some((file) => !seen.has(file))) {
219
+ throw new Error("runtime/skills 文件集合与 checksums manifest 不一致");
220
+ }
221
+ assertTopLevel(path.join(packageRoot, "runtime"), new Set(["business-client"]), "runtime");
222
+ assertTopLevel(path.join(packageRoot, "skills"), new Set(Object.keys(release.skills)), "skills");
223
+ const skillRoot = resolveInside(packageRoot, release.skills[SKILL_NAME].path);
224
+ const clientRoot = resolveInside(packageRoot, release.cli.path);
225
+ assertTopLevel(skillRoot, SKILL_TOP_LEVEL, "Skill");
226
+ assertAllowedClient(clientRoot);
227
+ rejectForbiddenTree(skillRoot, "Skill");
228
+ rejectForbiddenTree(clientRoot, "business client");
229
+ rejectForbiddenContent([
230
+ ...listFiles(skillRoot),
231
+ ...listFiles(clientRoot),
232
+ ...["README.md", "DEVELOPMENT.md"]
233
+ .map((name) => path.join(packageRoot, name))
234
+ .filter((file) => fs.existsSync(file)),
235
+ ], "release payload");
236
+ if (fs.existsSync(path.join(skillRoot, "runtime")) || fs.existsSync(path.join(skillRoot, "scripts"))) {
237
+ throw new Error("Skill 不得包含 CLI runtime 或 wrapper");
238
+ }
239
+ const cliEntries = releaseRoots.flatMap((root) => listFiles(root))
240
+ .filter((file) => path.basename(file) === "cli.js");
241
+ const expectedEntry = path.join(clientRoot, release.cli.entrypoint);
242
+ if (cliEntries.length !== 1 || cliEntries[0] !== expectedEntry) {
243
+ throw new Error("release 必须且只能包含一份 JavaScript business CLI");
244
+ }
245
+ const packageFiles = releaseRoots.flatMap((root) => listFiles(root))
246
+ .filter((file) => path.basename(file) === "package.json");
247
+ if (packageFiles.length !== 1 || packageFiles[0] !== path.join(clientRoot, "package.json")) {
248
+ throw new Error("release 包含第二份 CLI 或未知 npm package");
249
+ }
250
+ if (treeDigest(skillRoot) !== release.skills[SKILL_NAME].sha256) {
251
+ throw new Error("Skill 目录摘要与 release manifest 不一致");
252
+ }
253
+ for (const [name, item] of Object.entries(release.skills)) {
254
+ const root = resolveInside(packageRoot, item.path);
255
+ const actual = new Set(fs.readdirSync(root));
256
+ if (!actual.has("SKILL.md") || !actual.has("agents") || [...actual].some((entry) => !SKILL_TOP_LEVEL.has(entry)))
257
+ throw new Error(`Skill ${name} 目录结构无效`);
258
+ rejectForbiddenTree(root, `Skill ${name}`);
259
+ if (treeDigest(root) !== item.sha256)
260
+ throw new Error(`Skill ${name} 目录摘要与 release manifest 不一致`);
261
+ }
262
+ if (treeDigest(clientRoot) !== release.cli.sha256) {
263
+ throw new Error("JavaScript CLI 摘要与 release manifest 不一致");
264
+ }
265
+ if (!fs.existsSync(expectedEntry))
266
+ throw new Error("JavaScript CLI 入口不存在");
267
+ return { release, skillRoot, skillRoots: Object.fromEntries(Object.entries(release.skills).map(([name, item]) => [name, resolveInside(packageRoot, item.path)])), clientRoot };
268
+ }
251
269
  function status(options = {}, packageRoot = PACKAGE_ROOT) {
252
- const paths = resolveInstallPaths(options);
253
- let record = null;
254
- let recordError = null;
255
- if (fs.existsSync(paths.record)) {
270
+ const paths = resolveInstallPaths(options);
271
+ let record = null;
272
+ let recordError = null;
273
+ if (fs.existsSync(paths.record)) {
274
+ try {
275
+ record = readInstallationRecord(paths.record, paths);
276
+ }
277
+ catch (error) {
278
+ recordError = error.message;
279
+ }
280
+ }
281
+ let packageRelease = null;
282
+ try {
283
+ packageRelease = loadRelease(packageRoot).release_version;
284
+ }
285
+ catch (_) {
286
+ // A source checkout has no generated release until the builder runs.
287
+ }
288
+ const runtime = safeReadRuntimeRecord(paths.runtimeRecord);
289
+ const runtimeHealthy = runtimeMatchesRuntimeRecord(paths, runtime);
290
+ const skillsInstalled = Boolean(record)
291
+ && Object.values(record.skills).every((skill) => digestEquals(skill.path, skill.sha256));
292
+ let registryError = null;
293
+ let registeredScope = false;
256
294
  try {
257
- record = readInstallationRecord(paths.record, paths);
258
- } catch (error) {
259
- recordError = error.message;
260
- }
261
- }
262
- let packageRelease = null;
263
- try {
264
- packageRelease = loadRelease(packageRoot).release_version;
265
- } catch (_) {
266
- // A source checkout has no generated release until the builder runs.
267
- }
268
- const installed = Boolean(record)
269
- && fs.existsSync(paths.target)
270
- && fs.existsSync(paths.clientRoot)
271
- && fs.existsSync(paths.dispatcher)
272
- && fs.existsSync(paths.shim);
273
- return {
274
- ok: installed && !recordError,
275
- installed,
276
- owner: record ? record.owner.package : null,
277
- agent: paths.agent,
278
- scope: paths.scope,
279
- installation_record: paths.record,
280
- installed_release: record ? record.release_version : null,
281
- package_release: packageRelease,
282
- cli: record ? record.cli : { name: CLI_NAME, path: paths.clientRoot, version: null, sha256: null },
283
- skill: record ? record.skill : { name: SKILL_NAME, path: paths.target, revision: null, sha256: null },
284
- shim: record ? record.shim : { path: paths.shim, sha256: null },
285
- record_error: recordError,
286
- };
287
- }
288
-
295
+ const registry = readInstallationRegistry(paths);
296
+ registeredScope = registry.installations[installationId(paths)] !== undefined;
297
+ }
298
+ catch (error) {
299
+ registryError = error.message;
300
+ }
301
+ const installed = Boolean(record)
302
+ && Boolean(runtime)
303
+ && skillsInstalled
304
+ && fs.existsSync(paths.dispatcher)
305
+ && fs.existsSync(paths.shim);
306
+ return {
307
+ ok: installed && runtimeHealthy && !recordError,
308
+ installed,
309
+ owner: record ? record.owner.package : null,
310
+ agent: paths.agent,
311
+ scope: paths.scope,
312
+ installation_record: paths.record,
313
+ installed_release: record ? record.release_version : null,
314
+ runtime_release: runtime ? runtime.release_version : null,
315
+ package_release: packageRelease,
316
+ cli: runtime
317
+ ? { name: CLI_NAME, path: paths.clientRoot, version: runtime.cli_version, sha256: runtime.cli_sha256 }
318
+ : { name: CLI_NAME, path: paths.clientRoot, version: null, sha256: null },
319
+ skill: record ? record.skill : { name: SKILL_NAME, path: paths.target, revision: null, sha256: null },
320
+ skills: record ? record.skills : {},
321
+ shim: record ? record.shim : { path: paths.shim, sha256: null },
322
+ registered_scope: registeredScope,
323
+ registry_error: registryError,
324
+ record_error: recordError,
325
+ };
326
+ }
289
327
  function doctor(options = {}, packageRoot = PACKAGE_ROOT) {
290
- const paths = resolveInstallPaths(options);
291
- const checks = [];
292
- let expectedRelease = null;
293
- const hasCompletePackage = fs.existsSync(path.join(packageRoot, "checksums.json"))
294
- && fs.existsSync(path.join(packageRoot, "skills", SKILL_NAME));
295
-
296
- // Check for legacy Python Skill before proceeding
297
- if (!fs.existsSync(paths.record)) {
298
- const legacy = detectLegacySkill(paths.target);
299
- if (legacy) {
300
- const markers = Object.entries(legacy).filter(([_, v]) => v).map(([k]) => k).join(", ");
301
- return {
302
- ok: false,
303
- error: `检测到旧 Python Skill (${markers})`,
304
- remediation: buildInstallCommand(
305
- paths,
306
- options,
307
- "install",
308
- packageVersion(packageRoot),
309
- ["--backup-unmanaged"],
310
- ),
311
- legacy_skill_detected: true,
312
- legacy_markers: legacy,
313
- skill_root: paths.target,
314
- };
315
- }
316
- }
317
-
318
- try {
319
- if (hasCompletePackage) {
320
- expectedRelease = verifyPackage(packageRoot).release;
321
- checks.push({ name: "package_payload", ok: true });
322
- } else {
323
- expectedRelease = loadRelease(paths.installRoot);
324
- checks.push({ name: "installed_release_metadata", ok: true });
325
- }
326
- } catch (error) {
327
- checks.push({
328
- name: hasCompletePackage ? "package_payload" : "installed_release_metadata",
329
- ok: false,
330
- error: error.message,
331
- });
332
- }
333
-
334
- let record = null;
335
- try {
336
- record = readInstallationRecord(paths.record, paths);
337
- const expectedOk = !expectedRelease
338
- || (record.release_version === expectedRelease.release_version
339
- && record.cli.sha256 === expectedRelease.cli.sha256
340
- && record.skill.sha256 === expectedRelease.skills[SKILL_NAME].sha256);
341
- checks.push({ name: "installation_record", ok: expectedOk });
342
- } catch (error) {
343
- checks.push({ name: "installation_record", ok: false, error: error.message });
344
- }
345
-
346
- checkTree(checks, "installed_cli", paths.clientRoot, record && record.cli.sha256);
347
- checkTree(checks, "installed_skill", paths.target, record && record.skill.sha256);
348
- checkFile(checks, "installed_shim", paths.shim, record && record.shim.sha256);
349
- if (record) {
350
- checkTree(checks, "installed_manager", path.join(paths.installRoot, "scripts"), record.manager_sha256);
351
- checkFile(checks, "installed_release_manifest", path.join(paths.installRoot, "release.json"), record.release_sha256);
352
- checkFile(checks, "installed_release_policy", path.join(paths.installRoot, "release-policy.json"), record.policy_sha256);
353
- }
354
-
355
- const runtime = runCliRuntime(paths.dispatcher, ["version"], false);
356
- checks.push({ name: "cli_runtime", ok: runtime.ok, detail: runtime.detail });
357
- const pathMatches = findAllOnPath(CLI_NAME);
358
- const matchingShim = pathMatches.find((candidate) => sameFileOrContent(candidate, paths.shim));
359
- const pathOk = matchingShim !== undefined;
360
- checks.push({
361
- name: "path_visibility",
362
- ok: pathOk,
363
- expected: paths.shim,
364
- actual: matchingShim ?? pathMatches[0] ?? null,
365
- remediation: pathOk ? null : `将 ${paths.binDir} 加入 PATH,然后重启 Agent 会话`,
366
- });
367
- const apiTarget = resolveEffectiveApiTarget(options, paths.dispatcher);
368
- if (apiTarget.error) {
328
+ const paths = resolveInstallPaths(options);
329
+ const checks = [];
330
+ let expectedRelease = null;
331
+ const hasCompletePackage = fs.existsSync(path.join(packageRoot, "checksums.json"))
332
+ && fs.existsSync(path.join(packageRoot, "skills", SKILL_NAME));
333
+ // Check for legacy Python Skill before proceeding
334
+ if (!fs.existsSync(paths.record)) {
335
+ const legacy = detectLegacySkill(paths.target);
336
+ if (legacy) {
337
+ const markers = Object.entries(legacy).filter(([_, v]) => v).map(([k]) => k).join(", ");
338
+ return {
339
+ ok: false,
340
+ error: `检测到旧 Python Skill (${markers})`,
341
+ remediation: buildInstallCommand(paths, options, "install", packageVersion(packageRoot), ["--backup-unmanaged"]),
342
+ legacy_skill_detected: true,
343
+ legacy_markers: legacy,
344
+ skill_root: paths.target,
345
+ };
346
+ }
347
+ }
348
+ try {
349
+ if (hasCompletePackage) {
350
+ expectedRelease = verifyPackage(packageRoot).release;
351
+ checks.push({ name: "package_payload", ok: true });
352
+ }
353
+ else {
354
+ expectedRelease = loadRelease(paths.installRoot);
355
+ checks.push({ name: "installed_release_metadata", ok: true });
356
+ }
357
+ }
358
+ catch (error) {
359
+ checks.push({
360
+ name: hasCompletePackage ? "package_payload" : "installed_release_metadata",
361
+ ok: false,
362
+ error: error.message,
363
+ });
364
+ }
365
+ let record = null;
366
+ try {
367
+ record = readInstallationRecord(paths.record, paths);
368
+ checks.push({ name: "installation_record", ok: true });
369
+ }
370
+ catch (error) {
371
+ checks.push({ name: "installation_record", ok: false, error: error.message });
372
+ }
373
+ const runtimeRecord = safeReadRuntimeRecord(paths.runtimeRecord);
369
374
  checks.push({
370
- name: "platform_api",
371
- ok: false,
372
- source: apiTarget.source,
373
- detail: apiTarget.error,
375
+ name: "runtime_installation_record",
376
+ ok: Boolean(runtimeRecord) && !runtimeRecord.legacy,
377
+ error: runtimeRecord?.legacy ? "持久 CLI ownership record 需要通过 install/upgrade 升级" : undefined,
374
378
  });
375
- } else if (apiTarget.apiUrl) {
376
- const apiHealth = runApiHealth(paths.dispatcher, apiTarget.apiUrl);
379
+ checkTree(checks, "installed_cli", paths.clientRoot, runtimeRecord && runtimeRecord.cli_sha256);
380
+ if (record) {
381
+ for (const [name, skill] of Object.entries(record.skills)) {
382
+ checkTree(checks, name === SKILL_NAME ? "installed_skill" : `installed_skill_${name}`, skill.path, skill.sha256);
383
+ }
384
+ }
385
+ else {
386
+ checkTree(checks, "installed_skill", paths.target, null);
387
+ }
388
+ checkFile(checks, "installed_shim", paths.shim, record && record.shim.sha256);
389
+ checkTree(checks, "installed_manager", path.join(paths.installRoot, "scripts"), runtimeRecord && runtimeRecord.manager_sha256);
390
+ checkFile(checks, "installed_release_manifest", path.join(paths.installRoot, "release.json"), runtimeRecord && runtimeRecord.release_sha256);
391
+ checkFile(checks, "installed_release_policy", path.join(paths.installRoot, "release-policy.json"), runtimeRecord && runtimeRecord.policy_sha256);
377
392
  checks.push({
378
- name: "platform_api",
379
- ok: apiHealth.ok,
380
- source: apiTarget.source,
381
- api_url: apiTarget.apiUrl,
382
- detail: apiHealth.detail,
393
+ name: "scope_cli_compatibility",
394
+ ok: Boolean(record && runtimeRecord && Object.values(record.skills)
395
+ .every((skill) => versionSatisfies(runtimeRecord.cli_version, skill.requires_cli))),
396
+ cli_version: runtimeRecord?.cli_version ?? null,
383
397
  });
384
- } else {
398
+ try {
399
+ const registry = readInstallationRegistry(paths);
400
+ checks.push({
401
+ name: "installation_registry",
402
+ ok: registryContainsScope(registry, paths),
403
+ registry: paths.registry,
404
+ });
405
+ }
406
+ catch (error) {
407
+ checks.push({ name: "installation_registry", ok: false, error: error.message });
408
+ }
409
+ const runtime = runCliRuntime(paths.dispatcher, ["version"], false);
410
+ checks.push({ name: "cli_runtime", ok: runtime.ok, detail: runtime.detail });
411
+ const pathMatches = findAllOnPath(CLI_NAME);
412
+ const matchingShim = pathMatches.find((candidate) => sameFileOrContent(candidate, paths.shim));
413
+ const pathOk = matchingShim !== undefined;
385
414
  checks.push({
386
- name: "platform_api",
387
- ok: null,
388
- skipped: true,
389
- source: null,
390
- reason: "未通过 --api-url、ML_PLATFORM_API_URL configure 配置 API 地址",
415
+ name: "path_visibility",
416
+ ok: pathOk,
417
+ expected: paths.shim,
418
+ actual: matchingShim ?? pathMatches[0] ?? null,
419
+ remediation: pathOk ? null : `将 ${paths.binDir} 加入 PATH,然后重启 Agent 会话`,
391
420
  });
392
- }
393
- return {
394
- ok: checks.every((check) => check.ok !== false),
395
- release_version: record ? record.release_version : expectedRelease && expectedRelease.release_version,
396
- cli_version: record ? record.cli.version : expectedRelease && expectedRelease.cli.version,
397
- skill_root: paths.target,
398
- checks,
399
- };
400
- }
401
-
421
+ const apiTarget = resolveEffectiveApiTarget(options, paths.dispatcher);
422
+ if (apiTarget.error) {
423
+ checks.push({
424
+ name: "platform_api",
425
+ ok: false,
426
+ source: apiTarget.source,
427
+ detail: apiTarget.error,
428
+ });
429
+ }
430
+ else if (apiTarget.apiUrl) {
431
+ const apiHealth = runApiHealth(paths.dispatcher, apiTarget.apiUrl);
432
+ checks.push({
433
+ name: "platform_api",
434
+ ok: apiHealth.ok,
435
+ source: apiTarget.source,
436
+ api_url: apiTarget.apiUrl,
437
+ detail: apiHealth.detail,
438
+ });
439
+ }
440
+ else {
441
+ checks.push({
442
+ name: "platform_api",
443
+ ok: null,
444
+ skipped: true,
445
+ source: null,
446
+ reason: "未通过 --api-url、ML_PLATFORM_API_URL 或 configure 配置 API 地址",
447
+ });
448
+ }
449
+ return {
450
+ ok: checks.every((check) => check.ok !== false),
451
+ release_version: record ? record.release_version : expectedRelease && expectedRelease.release_version,
452
+ cli_version: runtimeRecord ? runtimeRecord.cli_version : expectedRelease && expectedRelease.cli.version,
453
+ skill_root: paths.target,
454
+ checks,
455
+ };
456
+ }
402
457
  function install(options = {}, packageRoot = PACKAGE_ROOT) {
403
- const verified = verifyPackage(packageRoot);
404
- const paths = resolveInstallPaths(options);
405
- fs.mkdirSync(paths.skillsDir, { recursive: true, mode: 0o700 });
406
- fs.mkdirSync(paths.stateDir, { recursive: true, mode: 0o700 });
407
- fs.mkdirSync(paths.binDir, { recursive: true, mode: 0o755 });
408
- return withInstallLock(paths.lock, () => installLocked(options, packageRoot, verified, paths));
409
- }
410
-
458
+ const verified = verifyPackage(packageRoot);
459
+ const paths = resolveInstallPaths(options);
460
+ fs.mkdirSync(paths.skillsDir, { recursive: true, mode: 0o700 });
461
+ fs.mkdirSync(paths.stateDir, { recursive: true, mode: 0o700 });
462
+ fs.mkdirSync(paths.binDir, { recursive: true, mode: 0o755 });
463
+ return withInstallLock(paths.lock, () => installLocked(options, packageRoot, verified, paths));
464
+ }
411
465
  function detectLegacySkill(skillPath) {
412
- if (!pathEntryExists(skillPath) || !fs.lstatSync(skillPath).isDirectory()) return null;
413
- const markers = {
414
- pythonWheel: fs.existsSync(path.join(skillPath, "assets", "wheels"))
415
- && fs.readdirSync(path.join(skillPath, "assets", "wheels")).some(f => f.endsWith(".whl")),
416
- runtimeJson: fs.existsSync(path.join(skillPath, "assets", "wheels", "runtime.json")),
417
- bootstrapScript: fs.existsSync(path.join(skillPath, "scripts", "bootstrap.py")),
418
- installScript: fs.existsSync(path.join(skillPath, "scripts", "install.py")),
419
- oldLauncher: fs.existsSync(path.join(skillPath, "scripts", "data-platform-demo"))
420
- || fs.existsSync(path.join(skillPath, "scripts", "ml-platform")),
421
- };
422
- const hasAnyMarker = Object.values(markers).some(Boolean);
423
- return hasAnyMarker ? markers : null;
424
- }
425
-
466
+ if (!pathEntryExists(skillPath) || !fs.lstatSync(skillPath).isDirectory())
467
+ return null;
468
+ const markers = {
469
+ pythonWheel: fs.existsSync(path.join(skillPath, "assets", "wheels"))
470
+ && fs.readdirSync(path.join(skillPath, "assets", "wheels")).some(f => f.endsWith(".whl")),
471
+ runtimeJson: fs.existsSync(path.join(skillPath, "assets", "wheels", "runtime.json")),
472
+ bootstrapScript: fs.existsSync(path.join(skillPath, "scripts", "bootstrap.py")),
473
+ installScript: fs.existsSync(path.join(skillPath, "scripts", "install.py")),
474
+ oldLauncher: fs.existsSync(path.join(skillPath, "scripts", "data-platform-demo"))
475
+ || fs.existsSync(path.join(skillPath, "scripts", "ml-platform")),
476
+ };
477
+ const hasAnyMarker = Object.values(markers).some(Boolean);
478
+ return hasAnyMarker ? markers : null;
479
+ }
426
480
  function backupUnmanagedSkill(skillPath, skillsDir, legacy) {
427
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
428
- let suffix = 0;
429
- let backupPath;
430
- do {
431
- const kind = legacy ? "legacy-backup" : "unmanaged-backup";
432
- const backupName = `.${SKILL_NAME}.${kind}.${timestamp}${suffix ? `-${suffix}` : ""}`;
433
- backupPath = path.join(skillsDir, backupName);
434
- suffix += 1;
435
- } while (pathEntryExists(backupPath));
436
- fs.renameSync(skillPath, backupPath);
437
- return backupPath;
438
- }
439
-
481
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
482
+ let suffix = 0;
483
+ let backupPath;
484
+ do {
485
+ const kind = legacy ? "legacy-backup" : "unmanaged-backup";
486
+ const backupName = `.${SKILL_NAME}.${kind}.${timestamp}${suffix ? `-${suffix}` : ""}`;
487
+ backupPath = path.join(skillsDir, backupName);
488
+ suffix += 1;
489
+ } while (pathEntryExists(backupPath));
490
+ fs.renameSync(skillPath, backupPath);
491
+ return backupPath;
492
+ }
440
493
  function installLocked(options, packageRoot, verified, paths) {
441
- const previousRecord = fs.existsSync(paths.record) ? fs.readFileSync(paths.record) : null;
442
- const current = safeReadInstallationRecord(paths.record, paths);
443
- const targetExists = pathEntryExists(paths.target);
444
- const legacy = targetExists && !current ? detectLegacySkill(paths.target) : null;
445
- const backingUpUnmanaged = Boolean(targetExists && !current && options.backupUnmanaged);
446
-
447
- if (options.migrateLegacy && !legacy) {
448
- throw new Error(`目标 Skill 不是旧 Python Skill 或不存在: ${paths.target}`);
449
- }
450
-
451
- if (targetExists && !current && !backingUpUnmanaged) {
452
- if (legacy && !options.backupUnmanaged) {
453
- const markers = Object.entries(legacy).filter(([_, v]) => v).map(([k]) => k).join(", ");
454
- throw new Error(
455
- `目标 Skill 包含旧 Python runtime 标记 (${markers}),拒绝覆盖: ${paths.target}\n` +
456
- ` 迁移方法: 使用 --backup-unmanaged 或运行 'ml-platform migrate' 先备份旧 Skill`
457
- );
458
- } else if (fs.lstatSync(paths.target).isSymbolicLink()) {
459
- throw new Error(
460
- `目标 Skill 路径是未托管的符号链接,拒绝覆盖: ${paths.target}\n` +
461
- " 处理方法: 确认后使用 --backup-unmanaged 备份该链接并安装正式 Skill"
462
- );
463
- } else if (!legacy) {
464
- throw new Error(
465
- `目标 Skill 不受 ${PACKAGE_NAME} 管理,拒绝覆盖: ${paths.target}\n` +
466
- " 处理方法: 确认后使用 --backup-unmanaged 备份原目录"
467
- );
468
- }
469
- }
470
- const sharedRuntime = safeReadRuntimeRecord(paths.runtimeRecord);
471
- if (pathEntryExists(paths.installRoot) && !sharedRuntime) {
472
- throw new Error(`持久 CLI 目录不受 ${PACKAGE_NAME} 管理,拒绝覆盖: ${paths.installRoot}`);
473
- }
474
- if (pathEntryExists(paths.shim) && (!current || sha256File(paths.shim) !== current.shim.sha256)
475
- && (!sharedRuntime || !shimTargetsDispatcher(paths.shim, paths.dispatcher))) {
476
- throw new Error(`CLI shim 不受 ${PACKAGE_NAME} 管理,拒绝覆盖: ${paths.shim}`);
477
- }
478
-
479
- const sharedRuntimeMatches = sharedRuntime
480
- && sharedRuntime.release_version === verified.release.release_version
481
- && sharedRuntime.cli_version === verified.release.cli.version
482
- && sharedRuntime.cli_sha256 === verified.release.cli.sha256
483
- && digestEquals(paths.clientRoot, verified.release.cli.sha256)
484
- && fs.existsSync(paths.shim)
485
- && shimTargetsDispatcher(paths.shim, paths.dispatcher);
486
-
487
- const currentComplete = current
488
- && current.release_version === verified.release.release_version
489
- && current.cli.sha256 === verified.release.cli.sha256
490
- && current.skill.sha256 === verified.release.skills[SKILL_NAME].sha256
491
- && digestEquals(paths.clientRoot, current.cli.sha256)
492
- && digestEquals(paths.target, current.skill.sha256)
493
- && fs.existsSync(paths.shim)
494
- && sha256File(paths.shim) === current.shim.sha256
495
- && digestEquals(path.join(paths.installRoot, "scripts"), current.manager_sha256)
496
- && fs.existsSync(path.join(paths.installRoot, "release.json"))
497
- && sha256File(path.join(paths.installRoot, "release.json")) === current.release_sha256;
498
- if (currentComplete && !options.upgrade) {
499
- if (options.prewarm !== false) assertRuntime(paths.dispatcher);
500
- return installationResult("unchanged", paths, verified.release, current);
501
- }
502
-
503
- if (((targetExists && !backingUpUnmanaged) || (sharedRuntime && !sharedRuntimeMatches)) && !options.upgrade) {
504
- throw new Error("已存在不同版本或摘要;升级时请使用 upgrade 或 install --upgrade");
505
- }
506
- const installedVersion = current ? current.release_version : sharedRuntime && sharedRuntime.release_version;
507
- if (installedVersion && !options.allowDowngrade
508
- && compareSemver(verified.release.release_version, installedVersion) < 0) {
509
- throw new Error("目标版本低于已安装版本;确认回滚时请添加 --allow-downgrade");
510
- }
511
-
512
- const stateStaging = fs.mkdtempSync(path.join(paths.stateDir, ".install."));
513
- const stagedInstall = path.join(stateStaging, "current");
514
- const skillStaging = fs.mkdtempSync(path.join(paths.skillsDir, `.${SKILL_NAME}.install.`));
515
- const stagedSkill = path.join(skillStaging, SKILL_NAME);
516
- const shimTemporary = path.join(paths.binDir, `.${CLI_NAME}.${process.pid}.${Date.now()}.tmp`);
517
- const stateBackup = path.join(paths.stateDir, `.backup.${process.pid}.${Date.now()}`);
518
- const skillBackup = path.join(paths.skillsDir, `.${SKILL_NAME}.backup.${process.pid}.${Date.now()}`);
519
- const shimBackup = path.join(paths.binDir, `.${CLI_NAME}.backup.${process.pid}.${Date.now()}`);
520
- let stateMoved = false;
521
- let skillMoved = false;
522
- let shimMoved = false;
523
- let hadState = false;
524
- let hadSkill = false;
525
- let hadShim = false;
526
- let legacyBackupPath = null;
527
- let legacyMoved = false;
528
- const additionalSkills = Object.keys(verified.release.skills).filter((name) => name !== SKILL_NAME);
529
- const additionalBackups = [];
530
- const installedAdditional = [];
531
- try {
532
- stagePersistentInstall(stagedInstall, packageRoot, verified);
533
- fs.cpSync(verified.skillRoot, stagedSkill, { recursive: true, errorOnExist: true });
534
- if (treeDigest(stagedSkill) !== verified.release.skills[SKILL_NAME].sha256) {
535
- throw new Error("staged Skill 摘要不匹配");
536
- }
537
- writeShim(shimTemporary, paths.dispatcher);
538
-
539
- if (backingUpUnmanaged) {
540
- legacyBackupPath = backupUnmanagedSkill(paths.target, paths.skillsDir, Boolean(legacy));
541
- legacyMoved = true;
542
- }
543
- if (pathEntryExists(paths.installRoot)) {
544
- fs.renameSync(paths.installRoot, stateBackup);
545
- hadState = true;
546
- }
547
- fs.renameSync(stagedInstall, paths.installRoot);
548
- stateMoved = true;
549
- if (pathEntryExists(paths.target)) {
550
- fs.renameSync(paths.target, skillBackup);
551
- hadSkill = true;
552
- }
553
- fs.renameSync(stagedSkill, paths.target);
554
- skillMoved = true;
555
- for (const name of additionalSkills) {
556
- const target = path.join(paths.skillsDir, name);
557
- const backup = path.join(paths.skillsDir, `.${name}.backup.${process.pid}.${Date.now()}`);
558
- if (pathEntryExists(target)) {
559
- if (!current?.skills?.[name] || current.skills[name].path !== target) throw new Error(`拒绝覆盖未托管 Skill: ${name}`);
560
- fs.renameSync(target, backup);
561
- additionalBackups.push({ target, backup });
562
- }
563
- fs.cpSync(verified.skillRoots[name], target, { recursive: true, errorOnExist: true });
564
- installedAdditional.push(target);
565
- }
566
- if (pathEntryExists(paths.shim)) {
567
- fs.renameSync(paths.shim, shimBackup);
568
- hadShim = true;
569
- }
570
- fs.renameSync(shimTemporary, paths.shim);
571
- shimMoved = true;
572
-
573
- const record = buildInstallationRecord(paths, verified.release);
574
- writeJsonAtomic(paths.record, record);
575
- assertInstalledState(paths, record, options.prewarm !== false);
576
-
577
- removeOwnedDirectory(stateStaging, paths.stateDir, ".install.");
578
- removeOwnedDirectory(skillStaging, paths.skillsDir, `.${SKILL_NAME}.install.`);
579
- if (hadState) removeOwnedDirectory(stateBackup, paths.stateDir, ".backup.");
580
- if (hadSkill) removeOwnedDirectory(skillBackup, paths.skillsDir, `.${SKILL_NAME}.backup.`);
581
- for (const { backup } of additionalBackups) fs.rmSync(backup, { recursive: true, force: true });
582
- if (hadShim) fs.rmSync(shimBackup, { force: true });
583
- const result = installationResult(
584
- options.migrateLegacy ? "migrated" : (current || (targetExists && !backingUpUnmanaged) ? "upgraded" : "installed"),
585
- paths,
586
- verified.release,
587
- record,
588
- );
589
- if (legacyBackupPath) {
590
- if (legacy) result.legacy_skill_backup = legacyBackupPath;
591
- else result.unmanaged_skill_backup = legacyBackupPath;
494
+ const previousRecord = fs.existsSync(paths.record) ? fs.readFileSync(paths.record) : null;
495
+ const previousRegistry = fs.existsSync(paths.registry) ? fs.readFileSync(paths.registry) : null;
496
+ const current = safeReadInstallationRecord(paths.record, paths);
497
+ const targetExists = pathEntryExists(paths.target);
498
+ const legacy = targetExists && !current ? detectLegacySkill(paths.target) : null;
499
+ const backingUpUnmanaged = Boolean(targetExists && !current && options.backupUnmanaged);
500
+ if (options.migrateLegacy && !legacy) {
501
+ throw new Error(`目标 Skill 不是旧 Python Skill 或不存在: ${paths.target}`);
592
502
  }
593
- return result;
594
- } catch (error) {
595
- if (shimMoved && pathEntryExists(paths.shim)) fs.rmSync(paths.shim, { force: true });
596
- if (hadShim && pathEntryExists(shimBackup)) fs.renameSync(shimBackup, paths.shim);
597
- if (skillMoved && pathEntryExists(paths.target)) removeOwnedDirectory(paths.target, paths.skillsDir, SKILL_NAME);
598
- if (hadSkill && pathEntryExists(skillBackup)) fs.renameSync(skillBackup, paths.target);
599
- for (const target of installedAdditional.reverse()) fs.rmSync(target, { recursive: true, force: true });
600
- for (const { target, backup } of additionalBackups.reverse()) if (pathEntryExists(backup)) fs.renameSync(backup, target);
601
- if (legacyMoved && legacyBackupPath && pathEntryExists(legacyBackupPath) && !pathEntryExists(paths.target)) {
602
- fs.renameSync(legacyBackupPath, paths.target);
603
- }
604
- if (stateMoved && pathEntryExists(paths.installRoot)) removeOwnedDirectory(paths.installRoot, paths.stateDir, "current");
605
- if (hadState && pathEntryExists(stateBackup)) fs.renameSync(stateBackup, paths.installRoot);
606
- if (previousRecord === null) fs.rmSync(paths.record, { force: true });
607
- else fs.writeFileSync(paths.record, previousRecord);
608
- if (pathEntryExists(shimTemporary)) fs.rmSync(shimTemporary, { force: true });
609
- if (pathEntryExists(stateStaging)) removeOwnedDirectory(stateStaging, paths.stateDir, ".install.");
610
- if (pathEntryExists(skillStaging)) removeOwnedDirectory(skillStaging, paths.skillsDir, `.${SKILL_NAME}.install.`);
611
- throw error;
612
- }
613
- }
614
-
503
+ if (targetExists && !current && !backingUpUnmanaged) {
504
+ if (legacy) {
505
+ const markers = Object.entries(legacy).filter(([_, value]) => value).map(([name]) => name).join(", ");
506
+ throw new Error(`目标 Skill 包含旧 Python runtime 标记 (${markers}),拒绝覆盖: ${paths.target}\n` +
507
+ ` 迁移方法: 使用 --backup-unmanaged 或运行 'ml-platform migrate' 先备份旧 Skill`);
508
+ }
509
+ if (fs.lstatSync(paths.target).isSymbolicLink()) {
510
+ throw new Error(`目标 Skill 路径是未托管的符号链接,拒绝覆盖: ${paths.target}\n` +
511
+ " 处理方法: 确认后使用 --backup-unmanaged 备份该链接并安装正式 Skill");
512
+ }
513
+ throw new Error(`目标 Skill 不受 ${PACKAGE_NAME} 管理,拒绝覆盖: ${paths.target}\n` +
514
+ " 处理方法: 确认后使用 --backup-unmanaged 备份原目录");
515
+ }
516
+ for (const name of Object.keys(verified.release.skills)) {
517
+ if (name === SKILL_NAME)
518
+ continue;
519
+ const target = path.join(paths.skillsDir, name);
520
+ if (pathEntryExists(target) && (!current?.skills?.[name] || current.skills[name].path !== target)) {
521
+ throw new Error(`拒绝覆盖未托管 Skill: ${name}`);
522
+ }
523
+ }
524
+ const sharedRuntime = safeReadRuntimeRecord(paths.runtimeRecord);
525
+ if (pathEntryExists(paths.installRoot) && !sharedRuntime) {
526
+ throw new Error(`持久 CLI 目录不受 ${PACKAGE_NAME} 管理,拒绝覆盖: ${paths.installRoot}`);
527
+ }
528
+ if (pathEntryExists(paths.shim) && (!current || sha256File(paths.shim) !== current.shim.sha256)
529
+ && (!sharedRuntime || !shimTargetsDispatcher(paths.shim, paths.dispatcher))) {
530
+ throw new Error(`CLI shim 不受 ${PACKAGE_NAME} 管理,拒绝覆盖: ${paths.shim}`);
531
+ }
532
+ const { registry, scopes, staleIds } = collectRegisteredScopes(paths);
533
+ const runtimeHealthy = runtimeMatchesRuntimeRecord(paths, sharedRuntime);
534
+ const scopeMatches = current && scopeSkillsMatchRelease(current, verified.release);
535
+ const scopeNeedsRepair = Boolean(current && scopeMatches && !scopeSkillsInstalled(current));
536
+ const currentComplete = Boolean(current && scopeMatches && runtimeHealthy
537
+ && scopeSkillsInstalled(current)
538
+ && shimMatchesRecord(paths, current)
539
+ && runtimeSupportsSkills(sharedRuntime, current.skills));
540
+ if (currentComplete && !options.upgrade) {
541
+ if (options.prewarm !== false)
542
+ assertRuntime(paths.dispatcher);
543
+ return installationResult("unchanged", paths, verified.release, current, sharedRuntime);
544
+ }
545
+ const scopeNeedsReplacement = !scopeMatches;
546
+ if (current && scopeNeedsReplacement && !options.upgrade) {
547
+ throw new Error("当前 scope 已安装不同版本或摘要;升级时请使用 upgrade 或 install --upgrade");
548
+ }
549
+ let stageRuntime = false;
550
+ if (!sharedRuntime) {
551
+ stageRuntime = true;
552
+ }
553
+ else if (!runtimeHealthy) {
554
+ if (!options.upgrade
555
+ && !runtimeIdentityMatchesRelease(sharedRuntime, packageRoot, verified.release)) {
556
+ throw new Error("共享 CLI 已损坏,但当前安装包不是其记录版本;请使用对应 release 执行 install --upgrade 修复");
557
+ }
558
+ stageRuntime = true;
559
+ }
560
+ else if (scopeNeedsReplacement && options.upgrade) {
561
+ stageRuntime = !runtimeMatchesRelease(paths, sharedRuntime, packageRoot, verified.release);
562
+ }
563
+ else if (!runtimeSupportsRelease(sharedRuntime, verified.release)) {
564
+ throw new Error("共享 CLI 不满足目标 Skill 的 requires_cli;升级时请使用 upgrade 或 install --upgrade");
565
+ }
566
+ if (stageRuntime) {
567
+ assertRuntimeCompatibleWithRegisteredScopes(scopes, paths, verified.release);
568
+ }
569
+ const installedVersion = sharedRuntime?.release_version;
570
+ if (stageRuntime && installedVersion && !options.allowDowngrade
571
+ && compareSemver(verified.release.release_version, installedVersion) < 0) {
572
+ throw new Error("目标版本低于已安装版本;确认回滚时请添加 --allow-downgrade");
573
+ }
574
+ const stateStaging = stageRuntime ? fs.mkdtempSync(path.join(paths.stateDir, ".install.")) : null;
575
+ const stagedInstall = stateStaging && path.join(stateStaging, "current");
576
+ const skillStaging = fs.mkdtempSync(path.join(paths.skillsDir, `.${SKILL_NAME}.install.`));
577
+ const stagedSkills = path.join(skillStaging, "skills");
578
+ const shimTemporary = path.join(paths.binDir, `.${CLI_NAME}.${process.pid}.${Date.now()}.tmp`);
579
+ const stateBackup = path.join(paths.stateDir, `.backup.${process.pid}.${Date.now()}`);
580
+ const shimBackup = path.join(paths.binDir, `.${CLI_NAME}.backup.${process.pid}.${Date.now()}`);
581
+ let stateMoved = false;
582
+ let shimMoved = false;
583
+ let hadState = false;
584
+ let hadShim = false;
585
+ let legacyBackupPath = null;
586
+ let legacyMoved = false;
587
+ const skillBackups = [];
588
+ const installedSkills = [];
589
+ try {
590
+ if (stageRuntime)
591
+ stagePersistentInstall(stagedInstall, packageRoot, verified);
592
+ fs.mkdirSync(stagedSkills);
593
+ for (const [name, source] of Object.entries(verified.skillRoots)) {
594
+ const target = path.join(stagedSkills, name);
595
+ fs.cpSync(source, target, { recursive: true, errorOnExist: true });
596
+ if (treeDigest(target) !== verified.release.skills[name].sha256) {
597
+ throw new Error(`staged Skill 摘要不匹配: ${name}`);
598
+ }
599
+ }
600
+ writeShim(shimTemporary, paths.dispatcher);
601
+ if (backingUpUnmanaged) {
602
+ legacyBackupPath = backupUnmanagedSkill(paths.target, paths.skillsDir, Boolean(legacy));
603
+ legacyMoved = true;
604
+ }
605
+ if (stageRuntime && pathEntryExists(paths.installRoot)) {
606
+ fs.renameSync(paths.installRoot, stateBackup);
607
+ hadState = true;
608
+ }
609
+ if (stageRuntime) {
610
+ fs.renameSync(stagedInstall, paths.installRoot);
611
+ stateMoved = true;
612
+ }
613
+ for (const name of Object.keys(verified.release.skills)) {
614
+ const target = path.join(paths.skillsDir, name);
615
+ const staged = path.join(stagedSkills, name);
616
+ const backup = path.join(paths.skillsDir, `.${name}.backup.${process.pid}.${Date.now()}`);
617
+ if (pathEntryExists(target)) {
618
+ fs.renameSync(target, backup);
619
+ skillBackups.push({ target, backup });
620
+ }
621
+ fs.renameSync(staged, target);
622
+ installedSkills.push(target);
623
+ }
624
+ if (pathEntryExists(paths.shim)) {
625
+ fs.renameSync(paths.shim, shimBackup);
626
+ hadShim = true;
627
+ }
628
+ fs.renameSync(shimTemporary, paths.shim);
629
+ shimMoved = true;
630
+ const runtime = safeReadRuntimeRecord(paths.runtimeRecord);
631
+ if (!runtimeMatchesRuntimeRecord(paths, runtime))
632
+ throw new Error("安装后的共享 CLI 摘要不匹配");
633
+ const record = buildInstallationRecord(paths, verified.release, runtime);
634
+ writeJsonAtomic(paths.record, record);
635
+ const nextRegistry = updateInstallationRegistry(registry, paths, staleIds);
636
+ writeJsonAtomic(paths.registry, nextRegistry);
637
+ assertInstalledState(paths, record, runtime, options.prewarm !== false);
638
+ if (stateStaging && pathEntryExists(stateStaging))
639
+ removeOwnedDirectory(stateStaging, paths.stateDir, ".install.");
640
+ if (pathEntryExists(skillStaging))
641
+ removeOwnedDirectory(skillStaging, paths.skillsDir, `.${SKILL_NAME}.install.`);
642
+ if (hadState)
643
+ removeOwnedDirectory(stateBackup, paths.stateDir, ".backup.");
644
+ for (const { backup } of skillBackups)
645
+ fs.rmSync(backup, { recursive: true, force: true });
646
+ if (hadShim)
647
+ fs.rmSync(shimBackup, { force: true });
648
+ const action = options.migrateLegacy ? "migrated"
649
+ : scopeNeedsRepair ? "repaired"
650
+ : current || (targetExists && !backingUpUnmanaged) ? "upgraded" : "installed";
651
+ const result = installationResult(action, paths, verified.release, record, runtime);
652
+ if (legacyBackupPath) {
653
+ if (legacy)
654
+ result.legacy_skill_backup = legacyBackupPath;
655
+ else
656
+ result.unmanaged_skill_backup = legacyBackupPath;
657
+ }
658
+ return result;
659
+ }
660
+ catch (error) {
661
+ if (shimMoved && pathEntryExists(paths.shim))
662
+ fs.rmSync(paths.shim, { force: true });
663
+ if (hadShim && pathEntryExists(shimBackup))
664
+ fs.renameSync(shimBackup, paths.shim);
665
+ for (const target of installedSkills.reverse())
666
+ fs.rmSync(target, { recursive: true, force: true });
667
+ for (const { target, backup } of skillBackups.reverse())
668
+ if (pathEntryExists(backup))
669
+ fs.renameSync(backup, target);
670
+ if (legacyMoved && legacyBackupPath && pathEntryExists(legacyBackupPath) && !pathEntryExists(paths.target)) {
671
+ fs.renameSync(legacyBackupPath, paths.target);
672
+ }
673
+ if (stateMoved && pathEntryExists(paths.installRoot))
674
+ removeOwnedDirectory(paths.installRoot, paths.stateDir, "current");
675
+ if (hadState && pathEntryExists(stateBackup))
676
+ fs.renameSync(stateBackup, paths.installRoot);
677
+ if (previousRecord === null)
678
+ fs.rmSync(paths.record, { force: true });
679
+ else
680
+ fs.writeFileSync(paths.record, previousRecord);
681
+ if (previousRegistry === null)
682
+ fs.rmSync(paths.registry, { force: true });
683
+ else
684
+ fs.writeFileSync(paths.registry, previousRegistry);
685
+ if (pathEntryExists(shimTemporary))
686
+ fs.rmSync(shimTemporary, { force: true });
687
+ if (stateStaging && pathEntryExists(stateStaging))
688
+ removeOwnedDirectory(stateStaging, paths.stateDir, ".install.");
689
+ if (pathEntryExists(skillStaging))
690
+ removeOwnedDirectory(skillStaging, paths.skillsDir, `.${SKILL_NAME}.install.`);
691
+ throw error;
692
+ }
693
+ }
615
694
  function stagePersistentInstall(target, packageRoot, verified) {
616
- fs.mkdirSync(path.join(target, "scripts"), { recursive: true });
617
- fs.mkdirSync(path.join(target, "runtime"), { recursive: true });
618
- for (const name of ["main.js", "lib.js"]) {
619
- fs.copyFileSync(path.join(packageRoot, "scripts", name), path.join(target, "scripts", name));
620
- }
621
- fs.copyFileSync(path.join(packageRoot, "package.json"), path.join(target, "package.json"));
622
- fs.chmodSync(path.join(target, "scripts", "main.js"), 0o755);
623
- fs.cpSync(verified.clientRoot, path.join(target, "runtime", "business-client"), {
624
- recursive: true,
625
- errorOnExist: true,
626
- });
627
- fs.copyFileSync(path.join(packageRoot, "release.json"), path.join(target, "release.json"));
628
- fs.copyFileSync(path.join(packageRoot, "release-policy.json"), path.join(target, "release-policy.json"));
629
- writeJsonAtomic(path.join(target, "ownership.json"), {
630
- schema_version: RUNTIME_RECORD_SCHEMA,
631
- owner: PACKAGE_NAME,
632
- release_version: verified.release.release_version,
633
- cli_version: verified.release.cli.version,
634
- cli_sha256: verified.release.cli.sha256,
635
- });
636
- if (treeDigest(path.join(target, "runtime", "business-client")) !== verified.release.cli.sha256) {
637
- throw new Error("staged JavaScript CLI 摘要不匹配");
638
- }
639
- }
640
-
641
- function buildInstallationRecord(paths, release) {
642
- const record = {
643
- schema_version: RECORD_SCHEMA,
644
- owner: { package: PACKAGE_NAME, installation_id: installationId(paths) },
645
- release_version: release.release_version,
646
- agent: paths.agent,
647
- scope: paths.scope,
648
- cli: {
649
- name: CLI_NAME,
650
- version: release.cli.version,
651
- path: paths.clientRoot,
652
- sha256: release.cli.sha256,
653
- },
654
- skill: {
655
- name: SKILL_NAME,
656
- revision: release.skills[SKILL_NAME].revision,
657
- requires_cli: release.skills[SKILL_NAME].requires_cli,
658
- path: paths.target,
659
- sha256: release.skills[SKILL_NAME].sha256,
660
- },
661
- shim: { path: paths.shim, sha256: sha256File(paths.shim) },
662
- manager_sha256: treeDigest(path.join(paths.installRoot, "scripts")),
663
- release_sha256: sha256File(path.join(paths.installRoot, "release.json")),
664
- policy_sha256: sha256File(path.join(paths.installRoot, "release-policy.json")),
665
- };
666
- record.skills = Object.fromEntries(Object.entries(release.skills).map(([name, item]) => [name, {
667
- name, revision: item.revision, requires_cli: item.requires_cli,
668
- path: path.join(paths.skillsDir, name), sha256: item.sha256,
669
- }]));
670
- return record;
671
- }
672
-
673
- function assertInstalledState(paths, record, prewarm) {
674
- if (!digestEquals(paths.clientRoot, record.cli.sha256)) throw new Error("安装后的 CLI 摘要不匹配");
675
- if (!digestEquals(paths.target, record.skill.sha256)) throw new Error("安装后的 Skill 摘要不匹配");
676
- for (const skill of Object.values(record.skills || {})) if (!digestEquals(skill.path, skill.sha256)) throw new Error(`安装后的 Skill 摘要不匹配: ${skill.name}`);
677
- if (sha256File(paths.shim) !== record.shim.sha256) throw new Error("安装后的 CLI shim 摘要不匹配");
678
- if (prewarm) assertRuntime(paths.dispatcher);
679
- }
680
-
681
- function installationResult(action, paths, release, record) {
682
- const pathMatches = findAllOnPath(CLI_NAME);
683
- const pathReady = pathMatches.some((candidate) => sameFileOrContent(candidate, paths.shim));
684
- return {
685
- ok: true,
686
- action,
687
- release_version: release.release_version,
688
- cli_version: release.cli.version,
689
- cli_root: paths.clientRoot,
690
- cli_shim: paths.shim,
691
- skill: SKILL_NAME,
692
- skill_root: paths.target,
693
- installation_record: paths.record,
694
- ownership: record.owner,
695
- path_ready: pathReady,
696
- path_setup: pathReady ? null : `将 ${paths.binDir} 加入 PATH`,
697
- restart_agent_session: true,
698
- };
699
- }
700
-
695
+ fs.mkdirSync(path.join(target, "scripts"), { recursive: true });
696
+ fs.mkdirSync(path.join(target, "runtime"), { recursive: true });
697
+ for (const name of ["main.js", "lib.js"]) {
698
+ fs.copyFileSync(path.join(packageRoot, "scripts", name), path.join(target, "scripts", name));
699
+ }
700
+ fs.copyFileSync(path.join(packageRoot, "package.json"), path.join(target, "package.json"));
701
+ fs.chmodSync(path.join(target, "scripts", "main.js"), 0o755);
702
+ fs.cpSync(verified.clientRoot, path.join(target, "runtime", "business-client"), {
703
+ recursive: true,
704
+ errorOnExist: true,
705
+ });
706
+ fs.copyFileSync(path.join(packageRoot, "release.json"), path.join(target, "release.json"));
707
+ fs.copyFileSync(path.join(packageRoot, "release-policy.json"), path.join(target, "release-policy.json"));
708
+ writeJsonAtomic(path.join(target, "ownership.json"), {
709
+ schema_version: RUNTIME_RECORD_SCHEMA,
710
+ owner: PACKAGE_NAME,
711
+ release_version: verified.release.release_version,
712
+ cli_version: verified.release.cli.version,
713
+ cli_sha256: verified.release.cli.sha256,
714
+ manager_sha256: treeDigest(path.join(target, "scripts")),
715
+ release_sha256: sha256File(path.join(target, "release.json")),
716
+ policy_sha256: sha256File(path.join(target, "release-policy.json")),
717
+ });
718
+ if (treeDigest(path.join(target, "runtime", "business-client")) !== verified.release.cli.sha256) {
719
+ throw new Error("staged JavaScript CLI 摘要不匹配");
720
+ }
721
+ }
722
+ function buildInstallationRecord(paths, release, runtime) {
723
+ const record = {
724
+ schema_version: RECORD_SCHEMA,
725
+ owner: { package: PACKAGE_NAME, installation_id: installationId(paths) },
726
+ release_version: release.release_version,
727
+ agent: paths.agent,
728
+ scope: paths.scope,
729
+ cli: {
730
+ name: CLI_NAME,
731
+ version: runtime.cli_version,
732
+ path: paths.clientRoot,
733
+ sha256: runtime.cli_sha256,
734
+ },
735
+ skill: {
736
+ name: SKILL_NAME,
737
+ revision: release.skills[SKILL_NAME].revision,
738
+ requires_cli: release.skills[SKILL_NAME].requires_cli,
739
+ path: paths.target,
740
+ sha256: release.skills[SKILL_NAME].sha256,
741
+ },
742
+ shim: { path: paths.shim, sha256: sha256File(paths.shim) },
743
+ };
744
+ record.skills = Object.fromEntries(Object.entries(release.skills).map(([name, item]) => [name, {
745
+ name, revision: item.revision, requires_cli: item.requires_cli,
746
+ path: path.join(paths.skillsDir, name), sha256: item.sha256,
747
+ }]));
748
+ return record;
749
+ }
750
+ function assertInstalledState(paths, record, runtime, prewarm) {
751
+ if (!runtimeMatchesRuntimeRecord(paths, runtime))
752
+ throw new Error("安装后的 CLI 摘要不匹配");
753
+ if (!digestEquals(paths.target, record.skill.sha256))
754
+ throw new Error("安装后的 Skill 摘要不匹配");
755
+ for (const skill of Object.values(record.skills || {}))
756
+ if (!digestEquals(skill.path, skill.sha256))
757
+ throw new Error(`安装后的 Skill 摘要不匹配: ${skill.name}`);
758
+ if (sha256File(paths.shim) !== record.shim.sha256)
759
+ throw new Error("安装后的 CLI shim 摘要不匹配");
760
+ if (prewarm)
761
+ assertRuntime(paths.dispatcher);
762
+ }
763
+ function installationResult(action, paths, release, record, runtime) {
764
+ const pathMatches = findAllOnPath(CLI_NAME);
765
+ const pathReady = pathMatches.some((candidate) => sameFileOrContent(candidate, paths.shim));
766
+ return {
767
+ ok: true,
768
+ action,
769
+ release_version: release.release_version,
770
+ cli_version: runtime.cli_version,
771
+ runtime_release: runtime.release_version,
772
+ cli_root: paths.clientRoot,
773
+ cli_shim: paths.shim,
774
+ skill: SKILL_NAME,
775
+ skill_root: paths.target,
776
+ installation_record: paths.record,
777
+ ownership: record.owner,
778
+ path_ready: pathReady,
779
+ path_setup: pathReady ? null : `将 ${paths.binDir} 加入 PATH`,
780
+ restart_agent_session: true,
781
+ };
782
+ }
783
+ function runtimeMatchesRuntimeRecord(paths, runtime) {
784
+ return Boolean(runtime && !runtime.legacy
785
+ && digestEquals(paths.clientRoot, runtime.cli_sha256)
786
+ && digestEquals(path.join(paths.installRoot, "scripts"), runtime.manager_sha256)
787
+ && fs.existsSync(path.join(paths.installRoot, "release.json"))
788
+ && sha256File(path.join(paths.installRoot, "release.json")) === runtime.release_sha256
789
+ && fs.existsSync(path.join(paths.installRoot, "release-policy.json"))
790
+ && sha256File(path.join(paths.installRoot, "release-policy.json")) === runtime.policy_sha256);
791
+ }
792
+ function runtimeMatchesRelease(paths, runtime, packageRoot, release) {
793
+ return runtimeMatchesRuntimeRecord(paths, runtime)
794
+ && runtime.release_version === release.release_version
795
+ && runtime.cli_version === release.cli.version
796
+ && runtime.cli_sha256 === release.cli.sha256
797
+ && runtime.release_sha256 === sha256File(path.join(packageRoot, "release.json"))
798
+ && runtime.policy_sha256 === sha256File(path.join(packageRoot, "release-policy.json"));
799
+ }
800
+ function runtimeIdentityMatchesRelease(runtime, packageRoot, release) {
801
+ if (!runtime || runtime.release_version !== release.release_version
802
+ || runtime.cli_version !== release.cli.version
803
+ || runtime.cli_sha256 !== release.cli.sha256)
804
+ return false;
805
+ if (runtime.legacy)
806
+ return true;
807
+ return runtime.release_sha256 === sha256File(path.join(packageRoot, "release.json"))
808
+ && runtime.policy_sha256 === sha256File(path.join(packageRoot, "release-policy.json"));
809
+ }
810
+ function runtimeSupportsSkills(runtime, skills) {
811
+ return Boolean(runtime && Object.values(skills).every((skill) => versionSatisfies(runtime.cli_version, skill.requires_cli)));
812
+ }
813
+ function runtimeSupportsRelease(runtime, release) {
814
+ return runtimeSupportsSkills(runtime, release.skills);
815
+ }
816
+ function scopeSkillsMatchRelease(record, release) {
817
+ if (!record || record.release_version !== release.release_version)
818
+ return false;
819
+ const expectedNames = Object.keys(release.skills).sort();
820
+ const actualNames = Object.keys(record.skills || {}).sort();
821
+ if (expectedNames.join("\0") !== actualNames.join("\0"))
822
+ return false;
823
+ return expectedNames.every((name) => {
824
+ const current = record.skills[name];
825
+ const expected = release.skills[name];
826
+ return current.revision === expected.revision
827
+ && current.requires_cli === expected.requires_cli
828
+ && current.sha256 === expected.sha256;
829
+ });
830
+ }
831
+ function scopeSkillsInstalled(record) {
832
+ return Boolean(record && Object.values(record.skills || {}).every((skill) => digestEquals(skill.path, skill.sha256)));
833
+ }
834
+ function shimMatchesRecord(paths, record) {
835
+ return Boolean(record && fs.existsSync(paths.shim)
836
+ && sha256File(paths.shim) === record.shim.sha256
837
+ && shimTargetsDispatcher(paths.shim, paths.dispatcher));
838
+ }
701
839
  function readInstallationRecord(file, paths) {
702
- const record = readJson(file, "installation record");
703
- if (record.schema_version !== RECORD_SCHEMA || record.owner?.package !== PACKAGE_NAME
704
- || record.owner.installation_id !== installationId(paths)
705
- || record.agent !== paths.agent || record.scope !== paths.scope
706
- || record.cli?.name !== CLI_NAME || record.cli.path !== paths.clientRoot
707
- || !isSemver(record.cli?.version) || !isSha256(record.cli?.sha256)
708
- || record.skill?.name !== SKILL_NAME || record.skill.path !== paths.target
709
- || !isSemver(record.skill?.revision) || !isSha256(record.skill?.sha256)
710
- || record.shim?.path !== paths.shim || !isSha256(record.shim?.sha256)
711
- || !isSha256(record.manager_sha256) || !isSha256(record.release_sha256)
712
- || !isSha256(record.policy_sha256)) {
713
- throw new Error("安装记录 ownership、路径、版本或摘要无效");
714
- }
715
- return record;
716
- }
717
-
840
+ const record = readJson(file, "installation record");
841
+ if (![RECORD_SCHEMA, LEGACY_RECORD_SCHEMA].includes(record.schema_version) || record.owner?.package !== PACKAGE_NAME
842
+ || record.owner.installation_id !== installationId(paths)
843
+ || record.agent !== paths.agent || record.scope !== paths.scope
844
+ || record.cli?.name !== CLI_NAME || record.cli.path !== paths.clientRoot
845
+ || !isSemver(record.cli?.version) || !isSha256(record.cli?.sha256)
846
+ || record.skill?.name !== SKILL_NAME || record.skill.path !== paths.target
847
+ || !isSemver(record.skill?.revision) || !isSha256(record.skill?.sha256)
848
+ || record.shim?.path !== paths.shim || !isSha256(record.shim?.sha256)
849
+ || !record.skills || typeof record.skills !== "object") {
850
+ throw new Error("安装记录 ownership、路径、版本或摘要无效");
851
+ }
852
+ for (const [name, skill] of Object.entries(record.skills)) {
853
+ if (!skill || skill.name !== name || skill.path !== path.join(paths.skillsDir, name)
854
+ || !isSemver(skill.revision) || !isSha256(skill.sha256)
855
+ || typeof skill.requires_cli !== "string") {
856
+ throw new Error("安装记录 Skill 路径、版本或摘要无效");
857
+ }
858
+ }
859
+ if (!record.skills[SKILL_NAME]
860
+ || record.skills[SKILL_NAME].sha256 !== record.skill.sha256
861
+ || record.skills[SKILL_NAME].path !== record.skill.path) {
862
+ throw new Error("安装记录主 Skill 与 Skill 列表不一致");
863
+ }
864
+ if (record.schema_version === LEGACY_RECORD_SCHEMA
865
+ && (!isSha256(record.manager_sha256) || !isSha256(record.release_sha256)
866
+ || !isSha256(record.policy_sha256))) {
867
+ throw new Error("旧安装记录运行时摘要无效");
868
+ }
869
+ return record;
870
+ }
718
871
  function safeReadInstallationRecord(file, paths) {
719
- try { return fs.existsSync(file) ? readInstallationRecord(file, paths) : null; }
720
- catch (_) { return null; }
872
+ try {
873
+ return fs.existsSync(file) ? readInstallationRecord(file, paths) : null;
874
+ }
875
+ catch (_) {
876
+ return null;
877
+ }
721
878
  }
722
-
723
879
  function safeReadRuntimeRecord(file) {
724
- try {
725
- const record = readJson(file, "runtime ownership record");
726
- return record.schema_version === RUNTIME_RECORD_SCHEMA && record.owner === PACKAGE_NAME
727
- && isSemver(record.release_version) && isSemver(record.cli_version) && isSha256(record.cli_sha256)
728
- ? record : null;
729
- } catch (_) {
730
- return null;
731
- }
732
- }
733
-
880
+ try {
881
+ const record = readJson(file, "runtime ownership record");
882
+ const coreValid = record.owner === PACKAGE_NAME && isSemver(record.release_version)
883
+ && isSemver(record.cli_version) && isSha256(record.cli_sha256);
884
+ if (!coreValid)
885
+ return null;
886
+ if (record.schema_version === RUNTIME_RECORD_SCHEMA) {
887
+ return isSha256(record.manager_sha256) && isSha256(record.release_sha256)
888
+ && isSha256(record.policy_sha256) ? record : null;
889
+ }
890
+ if (record.schema_version === LEGACY_RUNTIME_RECORD_SCHEMA) {
891
+ return { ...record, legacy: true };
892
+ }
893
+ return null;
894
+ }
895
+ catch (_) {
896
+ return null;
897
+ }
898
+ }
899
+ function readInstallationRegistry(paths) {
900
+ if (!fs.existsSync(paths.registry)) {
901
+ return { schema_version: REGISTRY_SCHEMA, owner: PACKAGE_NAME, installations: {} };
902
+ }
903
+ const registry = readJson(paths.registry, "installation registry");
904
+ if (registry.schema_version !== REGISTRY_SCHEMA || registry.owner !== PACKAGE_NAME
905
+ || !registry.installations || typeof registry.installations !== "object"
906
+ || Array.isArray(registry.installations)) {
907
+ throw new Error("安装 scope 注册表格式无效");
908
+ }
909
+ for (const [id, entry] of Object.entries(registry.installations)) {
910
+ validateRegistryEntry(id, entry);
911
+ }
912
+ return registry;
913
+ }
914
+ function validateRegistryEntry(id, entry) {
915
+ if (!/^[0-9a-f]{64}$/.test(id) || !entry || typeof entry !== "object"
916
+ || !["codex", "pi", "custom"].includes(entry.agent)
917
+ || !["user", "project"].includes(entry.scope)
918
+ || typeof entry.skills_dir !== "string" || !path.isAbsolute(entry.skills_dir)
919
+ || typeof entry.bin_dir !== "string" || !path.isAbsolute(entry.bin_dir)
920
+ || typeof entry.record_path !== "string" || !path.isAbsolute(entry.record_path)
921
+ || entry.record_path !== path.join(entry.skills_dir, `.${SKILL_NAME}.ml-platform.json`)) {
922
+ throw new Error("安装 scope 注册表条目无效");
923
+ }
924
+ const scopePaths = scopePathsFromRegistryEntry(entry);
925
+ if (installationId(scopePaths) !== id)
926
+ throw new Error("安装 scope 注册表 installation_id 无效");
927
+ }
928
+ function scopePathsFromRegistryEntry(entry) {
929
+ const skillsDir = path.resolve(entry.skills_dir);
930
+ const binDir = path.resolve(entry.bin_dir);
931
+ return {
932
+ agent: entry.agent,
933
+ scope: entry.scope,
934
+ skillsDir,
935
+ target: path.join(skillsDir, SKILL_NAME),
936
+ record: path.join(skillsDir, `.${SKILL_NAME}.ml-platform.json`),
937
+ binDir,
938
+ shim: path.join(binDir, CLI_NAME),
939
+ stateDir: null,
940
+ };
941
+ }
942
+ function registeredScopePaths(entry, paths) {
943
+ const scopePaths = scopePathsFromRegistryEntry(entry);
944
+ return {
945
+ ...scopePaths,
946
+ stateDir: paths.stateDir,
947
+ installRoot: paths.installRoot,
948
+ clientRoot: paths.clientRoot,
949
+ dispatcher: paths.dispatcher,
950
+ runtimeRecord: paths.runtimeRecord,
951
+ registry: paths.registry,
952
+ lock: paths.lock,
953
+ };
954
+ }
955
+ function collectRegisteredScopes(paths) {
956
+ const registry = readInstallationRegistry(paths);
957
+ const scopes = [];
958
+ const staleIds = [];
959
+ for (const [id, entry] of Object.entries(registry.installations)) {
960
+ const scopePaths = registeredScopePaths(entry, paths);
961
+ if (!fs.existsSync(scopePaths.record)) {
962
+ if (!pathEntryExists(scopePaths.target)) {
963
+ staleIds.push(id);
964
+ continue;
965
+ }
966
+ throw new Error(`已登记 scope 缺少安装记录,拒绝变更共享 CLI: ${scopePaths.record}`);
967
+ }
968
+ let record;
969
+ try {
970
+ record = readInstallationRecord(scopePaths.record, scopePaths);
971
+ }
972
+ catch (error) {
973
+ throw new Error(`已登记 scope 安装记录无效,拒绝变更共享 CLI: ${error.message}`);
974
+ }
975
+ scopes.push({ id, paths: scopePaths, record });
976
+ }
977
+ return { registry, scopes, staleIds };
978
+ }
979
+ function registryEntry(paths) {
980
+ return {
981
+ agent: paths.agent,
982
+ scope: paths.scope,
983
+ skills_dir: paths.skillsDir,
984
+ bin_dir: paths.binDir,
985
+ record_path: paths.record,
986
+ };
987
+ }
988
+ function updateInstallationRegistry(registry, paths, staleIds) {
989
+ const installations = { ...registry.installations };
990
+ for (const id of staleIds)
991
+ delete installations[id];
992
+ installations[installationId(paths)] = registryEntry(paths);
993
+ return { schema_version: REGISTRY_SCHEMA, owner: PACKAGE_NAME, installations };
994
+ }
995
+ function registryContainsScope(registry, paths) {
996
+ const entry = registry.installations[installationId(paths)];
997
+ return Boolean(entry && entry.record_path === paths.record && entry.skills_dir === paths.skillsDir
998
+ && entry.bin_dir === paths.binDir);
999
+ }
1000
+ function assertRuntimeCompatibleWithRegisteredScopes(scopes, currentPaths, release) {
1001
+ const incompatible = [];
1002
+ for (const scope of scopes) {
1003
+ if (scope.id === installationId(currentPaths))
1004
+ continue;
1005
+ for (const skill of Object.values(scope.record.skills)) {
1006
+ if (!versionSatisfies(release.cli.version, skill.requires_cli)) {
1007
+ incompatible.push(`${scope.paths.skillsDir}/${skill.name} requires ${skill.requires_cli}`);
1008
+ }
1009
+ }
1010
+ }
1011
+ if (incompatible.length > 0) {
1012
+ throw new Error(`目标共享 CLI ${release.cli.version} 与已登记 scope 不兼容: ${incompatible.join(", ")}`);
1013
+ }
1014
+ }
734
1015
  function installationId(paths) {
735
- return crypto.createHash("sha256")
736
- .update(`${paths.agent}\0${paths.scope}\0${paths.skillsDir}`, "utf8")
737
- .digest("hex");
1016
+ return crypto.createHash("sha256")
1017
+ .update(`${paths.agent}\0${paths.scope}\0${paths.skillsDir}`, "utf8")
1018
+ .digest("hex");
738
1019
  }
739
-
740
1020
  function writeShim(file, dispatcher) {
741
- const source = `#!/usr/bin/env node\n"use strict";\nconst { spawnSync } = require("child_process");\nconst result = spawnSync(process.execPath, [${JSON.stringify(dispatcher)}, ...process.argv.slice(2)], { stdio: "inherit", env: process.env });\nif (result.error) { process.stderr.write(result.error.message + "\\n"); process.exitCode = 1; } else { process.exitCode = result.status === null ? 1 : result.status; }\n`;
742
- fs.writeFileSync(file, source, { mode: 0o755 });
1021
+ const source = `#!/usr/bin/env node\n"use strict";\nconst { spawnSync } = require("child_process");\nconst result = spawnSync(process.execPath, [${JSON.stringify(dispatcher)}, ...process.argv.slice(2)], { stdio: "inherit", env: process.env });\nif (result.error) { process.stderr.write(result.error.message + "\\n"); process.exitCode = 1; } else { process.exitCode = result.status === null ? 1 : result.status; }\n`;
1022
+ fs.writeFileSync(file, source, { mode: 0o755 });
743
1023
  }
744
-
745
1024
  function shimTargetsDispatcher(shim, dispatcher) {
746
- try { return fs.readFileSync(shim, "utf8").includes(JSON.stringify(dispatcher)); }
747
- catch (_) { return false; }
1025
+ try {
1026
+ return fs.readFileSync(shim, "utf8").includes(JSON.stringify(dispatcher));
1027
+ }
1028
+ catch (_) {
1029
+ return false;
1030
+ }
748
1031
  }
749
-
750
1032
  function assertRuntime(dispatcher) {
751
- const result = runCliRuntime(dispatcher, ["version"], false);
752
- if (!result.ok) throw new Error(`CLI runtime 检查失败: ${result.detail || "未知错误"}`);
1033
+ const result = runCliRuntime(dispatcher, ["version"], false);
1034
+ if (!result.ok)
1035
+ throw new Error(`CLI runtime 检查失败: ${result.detail || "未知错误"}`);
753
1036
  }
754
-
755
1037
  function runCliRuntime(dispatcher, args, inheritOutput) {
756
- if (!fs.existsSync(dispatcher)) return { ok: false, detail: "缺少持久 ml-platform dispatcher" };
757
- const result = spawnSync(process.execPath, [dispatcher, ...args], {
758
- encoding: "utf8",
759
- stdio: inheritOutput ? "inherit" : ["ignore", "pipe", "pipe"],
760
- env: { ...process.env },
761
- });
762
- if (result.error) return { ok: false, detail: result.error.message };
763
- const detail = inheritOutput ? null : (result.stdout || result.stderr || "").trim();
764
- return { ok: result.status === 0, detail };
765
- }
766
-
1038
+ if (!fs.existsSync(dispatcher))
1039
+ return { ok: false, detail: "缺少持久 ml-platform dispatcher" };
1040
+ const result = spawnSync(process.execPath, [dispatcher, ...args], {
1041
+ encoding: "utf8",
1042
+ stdio: inheritOutput ? "inherit" : ["ignore", "pipe", "pipe"],
1043
+ env: { ...process.env },
1044
+ });
1045
+ if (result.error)
1046
+ return { ok: false, detail: result.error.message };
1047
+ const detail = inheritOutput ? null : (result.stdout || result.stderr || "").trim();
1048
+ return { ok: result.status === 0, detail };
1049
+ }
767
1050
  function runApiHealth(dispatcher, apiUrl) {
768
- let parsed;
769
- try { parsed = new URL(apiUrl); }
770
- catch (_) { return { ok: false, detail: "--api-url 必须是绝对 HTTP(S) URL" }; }
771
- if (!["http:", "https:"].includes(parsed.protocol) || !parsed.host) {
772
- return { ok: false, detail: "--api-url 必须是绝对 HTTP(S) URL" };
773
- }
774
- return runCliRuntime(dispatcher, ["--api-url", apiUrl.replace(/\/+$/, ""), "health"], false);
775
- }
776
-
777
- function resolveEffectiveApiTarget(
778
- options,
779
- dispatcher,
780
- env = process.env,
781
- runtime = runCliRuntime,
782
- ) {
783
- if (options.apiUrl && String(options.apiUrl).trim()) {
784
- return { apiUrl: String(options.apiUrl).trim(), source: "argument" };
785
- }
786
- if (env.ML_PLATFORM_API_URL && String(env.ML_PLATFORM_API_URL).trim()) {
787
- return { apiUrl: String(env.ML_PLATFORM_API_URL).trim(), source: "environment" };
788
- }
789
- const configured = runtime(dispatcher, ["show-config"], false);
790
- if (!configured.ok) {
791
- return {
792
- apiUrl: null,
793
- source: "saved_config",
794
- error: `无法读取已保存的 API 配置: ${configured.detail || "未知错误"}`,
795
- };
796
- }
797
- let payload;
798
- try {
799
- payload = JSON.parse(configured.detail);
800
- } catch (_) {
801
- return { apiUrl: null, source: "saved_config", error: "show-config 未返回有效 JSON" };
802
- }
803
- const apiUrl = payload && payload.effective && payload.effective.api_url;
804
- return apiUrl
805
- ? { apiUrl: String(apiUrl).trim(), source: "saved_config" }
806
- : { apiUrl: null, source: null };
807
- }
808
-
1051
+ let parsed;
1052
+ try {
1053
+ parsed = new URL(apiUrl);
1054
+ }
1055
+ catch (_) {
1056
+ return { ok: false, detail: "--api-url 必须是绝对 HTTP(S) URL" };
1057
+ }
1058
+ if (!["http:", "https:"].includes(parsed.protocol) || !parsed.host) {
1059
+ return { ok: false, detail: "--api-url 必须是绝对 HTTP(S) URL" };
1060
+ }
1061
+ return runCliRuntime(dispatcher, ["--api-url", apiUrl.replace(/\/+$/, ""), "health"], false);
1062
+ }
1063
+ function resolveEffectiveApiTarget(options, dispatcher, env = process.env, runtime = runCliRuntime) {
1064
+ if (options.apiUrl && String(options.apiUrl).trim()) {
1065
+ return { apiUrl: String(options.apiUrl).trim(), source: "argument" };
1066
+ }
1067
+ if (env.ML_PLATFORM_API_URL && String(env.ML_PLATFORM_API_URL).trim()) {
1068
+ return { apiUrl: String(env.ML_PLATFORM_API_URL).trim(), source: "environment" };
1069
+ }
1070
+ const configured = runtime(dispatcher, ["show-config"], false);
1071
+ if (!configured.ok) {
1072
+ return {
1073
+ apiUrl: null,
1074
+ source: "saved_config",
1075
+ error: `无法读取已保存的 API 配置: ${configured.detail || "未知错误"}`,
1076
+ };
1077
+ }
1078
+ let payload;
1079
+ try {
1080
+ payload = JSON.parse(configured.detail);
1081
+ }
1082
+ catch (_) {
1083
+ return { apiUrl: null, source: "saved_config", error: "show-config 未返回有效 JSON" };
1084
+ }
1085
+ const apiUrl = payload && payload.effective && payload.effective.api_url;
1086
+ return apiUrl
1087
+ ? { apiUrl: String(apiUrl).trim(), source: "saved_config" }
1088
+ : { apiUrl: null, source: null };
1089
+ }
809
1090
  function checkTree(checks, name, root, expected) {
810
- if (!expected || !fs.existsSync(root)) {
811
- checks.push({ name, ok: false, error: `缺少安装目录或预期摘要: ${root}` });
812
- return;
813
- }
814
- try {
815
- const actual = treeDigest(root);
816
- checks.push({ name, ok: actual === expected, expected_sha256: expected, actual_sha256: actual });
817
- } catch (error) {
818
- checks.push({ name, ok: false, error: error.message });
819
- }
1091
+ if (!expected || !fs.existsSync(root)) {
1092
+ checks.push({ name, ok: false, error: `缺少安装目录或预期摘要: ${root}` });
1093
+ return;
1094
+ }
1095
+ try {
1096
+ const actual = treeDigest(root);
1097
+ checks.push({ name, ok: actual === expected, expected_sha256: expected, actual_sha256: actual });
1098
+ }
1099
+ catch (error) {
1100
+ checks.push({ name, ok: false, error: error.message });
1101
+ }
820
1102
  }
821
-
822
1103
  function checkFile(checks, name, file, expected) {
823
- if (!expected || !fs.existsSync(file)) {
824
- checks.push({ name, ok: false, error: `缺少安装文件或预期摘要: ${file}` });
825
- return;
826
- }
827
- const actual = sha256File(file);
828
- checks.push({ name, ok: actual === expected, expected_sha256: expected, actual_sha256: actual });
829
- }
830
-
1104
+ if (!expected || !fs.existsSync(file)) {
1105
+ checks.push({ name, ok: false, error: `缺少安装文件或预期摘要: ${file}` });
1106
+ return;
1107
+ }
1108
+ const actual = sha256File(file);
1109
+ checks.push({ name, ok: actual === expected, expected_sha256: expected, actual_sha256: actual });
1110
+ }
831
1111
  function digestEquals(root, expected) {
832
- try { return fs.existsSync(root) && treeDigest(root) === expected; }
833
- catch (_) { return false; }
1112
+ try {
1113
+ return fs.existsSync(root) && treeDigest(root) === expected;
1114
+ }
1115
+ catch (_) {
1116
+ return false;
1117
+ }
834
1118
  }
835
-
836
1119
  function pathEntryExists(entry) {
837
- try {
838
- fs.lstatSync(entry);
839
- return true;
840
- } catch (error) {
841
- if (error.code === "ENOENT") return false;
842
- throw error;
843
- }
844
- }
845
-
1120
+ try {
1121
+ fs.lstatSync(entry);
1122
+ return true;
1123
+ }
1124
+ catch (error) {
1125
+ if (error.code === "ENOENT")
1126
+ return false;
1127
+ throw error;
1128
+ }
1129
+ }
846
1130
  function withInstallLock(lockPath, callback) {
847
- let descriptor;
848
- try {
849
- descriptor = fs.openSync(lockPath, "wx", 0o600);
850
- } catch (error) {
851
- if (error.code === "EEXIST") throw new Error(`另一个安装进程正在运行: ${lockPath}`);
852
- throw error;
853
- }
854
- fs.closeSync(descriptor);
855
- try { return callback(); }
856
- finally { fs.rmSync(lockPath, { force: true }); }
857
- }
858
-
1131
+ let descriptor;
1132
+ try {
1133
+ descriptor = fs.openSync(lockPath, "wx", 0o600);
1134
+ }
1135
+ catch (error) {
1136
+ if (error.code === "EEXIST")
1137
+ throw new Error(`另一个安装进程正在运行: ${lockPath}`);
1138
+ throw error;
1139
+ }
1140
+ fs.closeSync(descriptor);
1141
+ try {
1142
+ return callback();
1143
+ }
1144
+ finally {
1145
+ fs.rmSync(lockPath, { force: true });
1146
+ }
1147
+ }
859
1148
  function removeOwnedDirectory(target, parent, requiredName) {
860
- const resolvedParent = fs.realpathSync(parent);
861
- const resolvedTarget = fs.realpathSync(target);
862
- const basename = path.basename(resolvedTarget);
863
- const nameAllowed = requiredName.endsWith(".")
864
- ? basename.startsWith(requiredName)
865
- : basename === requiredName;
866
- if (path.dirname(resolvedTarget) !== resolvedParent || !nameAllowed) {
867
- throw new Error(`拒绝删除不受安装器管理的目录: ${resolvedTarget}`);
868
- }
869
- fs.rmSync(resolvedTarget, { recursive: true, force: true });
870
- }
871
-
1149
+ const resolvedParent = fs.realpathSync(parent);
1150
+ const resolvedTarget = fs.realpathSync(target);
1151
+ const basename = path.basename(resolvedTarget);
1152
+ const nameAllowed = requiredName.endsWith(".")
1153
+ ? basename.startsWith(requiredName)
1154
+ : basename === requiredName;
1155
+ if (path.dirname(resolvedTarget) !== resolvedParent || !nameAllowed) {
1156
+ throw new Error(`拒绝删除不受安装器管理的目录: ${resolvedTarget}`);
1157
+ }
1158
+ fs.rmSync(resolvedTarget, { recursive: true, force: true });
1159
+ }
872
1160
  function assertTopLevel(root, allowed, label) {
873
- if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) throw new Error(`${label} 目录不存在: ${root}`);
874
- const actual = new Set(fs.readdirSync(root));
875
- if (actual.size !== allowed.size || [...actual].some((item) => !allowed.has(item))) {
876
- throw new Error(`${label} 顶层文件不在 allowlist`);
877
- }
1161
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory())
1162
+ throw new Error(`${label} 目录不存在: ${root}`);
1163
+ const actual = new Set(fs.readdirSync(root));
1164
+ if (actual.size !== allowed.size || [...actual].some((item) => !allowed.has(item))) {
1165
+ throw new Error(`${label} 顶层文件不在 allowlist`);
1166
+ }
878
1167
  }
879
-
880
1168
  function assertAllowedClient(root) {
881
- const actual = new Set(fs.readdirSync(root));
882
- const required = ["package.json", "README.md", "src"];
883
- if (required.some((item) => !actual.has(item)) || [...actual].some((item) => !CLIENT_TOP_LEVEL.has(item))) {
884
- throw new Error("business client 顶层文件不在 allowlist");
885
- }
886
- const packageJson = readJson(path.join(root, "package.json"), "business client package.json");
887
- if (packageJson.bin?.[CLI_NAME] !== "src/cli.js") throw new Error("business client bin 入口无效");
888
- }
889
-
1169
+ const actual = new Set(fs.readdirSync(root));
1170
+ const required = ["package.json", "README.md", "src"];
1171
+ if (required.some((item) => !actual.has(item)) || [...actual].some((item) => !CLIENT_TOP_LEVEL.has(item))) {
1172
+ throw new Error("business client 顶层文件不在 allowlist");
1173
+ }
1174
+ const packageJson = readJson(path.join(root, "package.json"), "business client package.json");
1175
+ if (packageJson.bin?.[CLI_NAME] !== "src/cli.js")
1176
+ throw new Error("business client bin 入口无效");
1177
+ }
890
1178
  function rejectForbiddenTree(root, label) {
891
- const visit = (directory) => {
892
- for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
893
- const item = path.join(directory, entry.name);
894
- if (entry.isSymbolicLink()) throw new Error(`${label} 不允许符号链接: ${item}`);
895
- if (entry.isDirectory()) {
896
- if (FORBIDDEN_DIRECTORIES.has(entry.name) && fs.readdirSync(item).length > 0) {
897
- throw new Error(`${label} 包含禁止目录: ${item}`);
1179
+ const visit = (directory) => {
1180
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
1181
+ const item = path.join(directory, entry.name);
1182
+ if (entry.isSymbolicLink())
1183
+ throw new Error(`${label} 不允许符号链接: ${item}`);
1184
+ if (entry.isDirectory()) {
1185
+ if (FORBIDDEN_DIRECTORIES.has(entry.name) && fs.readdirSync(item).length > 0) {
1186
+ throw new Error(`${label} 包含禁止目录: ${item}`);
1187
+ }
1188
+ visit(item);
1189
+ }
1190
+ else if (entry.isFile()) {
1191
+ if (FORBIDDEN_FILES.has(entry.name) || FORBIDDEN_SUFFIXES.has(path.extname(entry.name).toLowerCase())) {
1192
+ throw new Error(`${label} 包含禁止文件: ${item}`);
1193
+ }
1194
+ }
1195
+ else {
1196
+ throw new Error(`${label} 包含不支持的文件类型: ${item}`);
1197
+ }
898
1198
  }
899
- visit(item);
900
- } else if (entry.isFile()) {
901
- if (FORBIDDEN_FILES.has(entry.name) || FORBIDDEN_SUFFIXES.has(path.extname(entry.name).toLowerCase())) {
902
- throw new Error(`${label} 包含禁止文件: ${item}`);
903
- }
904
- } else {
905
- throw new Error(`${label} 包含不支持的文件类型: ${item}`);
906
- }
907
- }
908
- };
909
- visit(root);
1199
+ };
1200
+ visit(root);
910
1201
  }
911
-
912
1202
  function rejectForbiddenContent(files, label) {
913
- for (const file of files) {
914
- if (!fs.statSync(file).isFile() || path.basename(file) === "release-policy.json") continue;
915
- const text = fs.readFileSync(file, "utf8");
916
- for (const rule of FORBIDDEN_CONTENT_PATTERNS) {
917
- if (rule.pattern.test(text)) {
918
- throw new Error(`${label} 包含禁止发布内容 ${rule.name}: ${file}`);
919
- }
1203
+ for (const file of files) {
1204
+ if (!fs.statSync(file).isFile() || path.basename(file) === "release-policy.json")
1205
+ continue;
1206
+ const text = fs.readFileSync(file, "utf8");
1207
+ for (const rule of FORBIDDEN_CONTENT_PATTERNS) {
1208
+ if (rule.pattern.test(text)) {
1209
+ throw new Error(`${label} 包含禁止发布内容 ${rule.name}: ${file}`);
1210
+ }
1211
+ }
920
1212
  }
921
- }
922
1213
  }
923
-
924
1214
  function treeDigest(root) {
925
- const files = listFiles(root);
926
- const hash = crypto.createHash("sha256");
927
- for (const file of files) {
928
- const relative = path.relative(root, file).split(path.sep).join("/");
929
- hash.update(relative, "utf8");
930
- hash.update("\0");
931
- hash.update(sha256File(file), "utf8");
932
- hash.update("\n");
933
- }
934
- return `sha256:${hash.digest("hex")}`;
935
- }
936
-
1215
+ const files = listFiles(root);
1216
+ const hash = crypto.createHash("sha256");
1217
+ for (const file of files) {
1218
+ const relative = path.relative(root, file).split(path.sep).join("/");
1219
+ hash.update(relative, "utf8");
1220
+ hash.update("\0");
1221
+ hash.update(sha256File(file), "utf8");
1222
+ hash.update("\n");
1223
+ }
1224
+ return `sha256:${hash.digest("hex")}`;
1225
+ }
937
1226
  function listFiles(root) {
938
- if (!fs.existsSync(root)) throw new Error(`目录不存在: ${root}`);
939
- const result = [];
940
- const visit = (directory) => {
941
- const entries = fs.readdirSync(directory, { withFileTypes: true })
942
- .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
943
- for (const entry of entries) {
944
- const item = path.join(directory, entry.name);
945
- if (entry.isSymbolicLink()) throw new Error(`发布目录不允许符号链接: ${item}`);
946
- if (entry.isDirectory()) visit(item);
947
- else if (entry.isFile()) result.push(item);
948
- else throw new Error(`发布目录包含不支持的文件类型: ${item}`);
949
- }
950
- };
951
- visit(root);
952
- return result;
953
- }
954
-
1227
+ if (!fs.existsSync(root))
1228
+ throw new Error(`目录不存在: ${root}`);
1229
+ const result = [];
1230
+ const visit = (directory) => {
1231
+ const entries = fs.readdirSync(directory, { withFileTypes: true })
1232
+ .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
1233
+ for (const entry of entries) {
1234
+ const item = path.join(directory, entry.name);
1235
+ if (entry.isSymbolicLink())
1236
+ throw new Error(`发布目录不允许符号链接: ${item}`);
1237
+ if (entry.isDirectory())
1238
+ visit(item);
1239
+ else if (entry.isFile())
1240
+ result.push(item);
1241
+ else
1242
+ throw new Error(`发布目录包含不支持的文件类型: ${item}`);
1243
+ }
1244
+ };
1245
+ visit(root);
1246
+ return result;
1247
+ }
955
1248
  function sha256File(file) {
956
- return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex")}`;
1249
+ return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex")}`;
957
1250
  }
958
-
959
1251
  function writeJsonAtomic(file, payload) {
960
- const temporary = `${file}.${process.pid}.tmp`;
961
- fs.writeFileSync(temporary, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
962
- fs.renameSync(temporary, file);
1252
+ const temporary = `${file}.${process.pid}.tmp`;
1253
+ fs.writeFileSync(temporary, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
1254
+ fs.renameSync(temporary, file);
963
1255
  }
964
-
965
1256
  function readJson(file, label) {
966
- try { return JSON.parse(fs.readFileSync(file, "utf8")); }
967
- catch (error) { throw new Error(`无法读取 ${label}: ${file}: ${error.message}`); }
1257
+ try {
1258
+ return JSON.parse(fs.readFileSync(file, "utf8"));
1259
+ }
1260
+ catch (error) {
1261
+ throw new Error(`无法读取 ${label}: ${file}: ${error.message}`);
1262
+ }
968
1263
  }
969
-
970
1264
  function resolveInside(root, relative) {
971
- const resolvedRoot = path.resolve(root);
972
- const target = path.resolve(resolvedRoot, relative);
973
- if (target === resolvedRoot || !target.startsWith(`${resolvedRoot}${path.sep}`)) {
974
- throw new Error(`路径越界: ${relative}`);
975
- }
976
- return target;
977
- }
978
-
1265
+ const resolvedRoot = path.resolve(root);
1266
+ const target = path.resolve(resolvedRoot, relative);
1267
+ if (target === resolvedRoot || !target.startsWith(`${resolvedRoot}${path.sep}`)) {
1268
+ throw new Error(`路径越界: ${relative}`);
1269
+ }
1270
+ return target;
1271
+ }
979
1272
  function normalizeRelative(value) {
980
- if (value.includes("\\")) throw new Error(`checksum 路径必须使用 /: ${value}`);
981
- const normalized = path.posix.normalize(value);
982
- if (normalized !== value || normalized.startsWith("../") || normalized.startsWith("/") || normalized === "..") {
983
- throw new Error(`非法相对路径: ${value}`);
984
- }
985
- return normalized;
986
- }
987
-
1273
+ if (value.includes("\\"))
1274
+ throw new Error(`checksum 路径必须使用 /: ${value}`);
1275
+ const normalized = path.posix.normalize(value);
1276
+ if (normalized !== value || normalized.startsWith("../") || normalized.startsWith("/") || normalized === "..") {
1277
+ throw new Error(`非法相对路径: ${value}`);
1278
+ }
1279
+ return normalized;
1280
+ }
988
1281
  function expandHome(value) {
989
- if (value === "~") return os.homedir();
990
- if (value.startsWith(`~${path.sep}`) || value.startsWith("~/")) return path.join(os.homedir(), value.slice(2));
991
- return value;
1282
+ if (value === "~")
1283
+ return os.homedir();
1284
+ if (value.startsWith(`~${path.sep}`) || value.startsWith("~/"))
1285
+ return path.join(os.homedir(), value.slice(2));
1286
+ return value;
992
1287
  }
993
-
994
1288
  function findAllOnPath(name) {
995
- const matches = [];
996
- const extensions = process.platform === "win32" ? [".cmd", ".exe", ""] : [""];
997
- for (const directory of (process.env.PATH || "").split(path.delimiter).filter(Boolean)) {
998
- for (const extension of extensions) {
999
- const candidate = path.join(directory, `${name}${extension}`);
1000
- try {
1001
- fs.accessSync(candidate, fs.constants.X_OK);
1002
- if (fs.statSync(candidate).isFile()) matches.push(candidate);
1003
- } catch (_) { /* continue */ }
1004
- }
1005
- }
1006
- return matches;
1007
- }
1008
-
1289
+ const matches = [];
1290
+ const extensions = process.platform === "win32" ? [".cmd", ".exe", ""] : [""];
1291
+ for (const directory of (process.env.PATH || "").split(path.delimiter).filter(Boolean)) {
1292
+ for (const extension of extensions) {
1293
+ const candidate = path.join(directory, `${name}${extension}`);
1294
+ try {
1295
+ fs.accessSync(candidate, fs.constants.X_OK);
1296
+ if (fs.statSync(candidate).isFile())
1297
+ matches.push(candidate);
1298
+ }
1299
+ catch (_) { /* continue */ }
1300
+ }
1301
+ }
1302
+ return matches;
1303
+ }
1009
1304
  function sameFileOrContent(left, right) {
1010
- try { return fs.realpathSync(left) === fs.realpathSync(right) || sha256File(left) === sha256File(right); }
1011
- catch (_) { return false; }
1305
+ try {
1306
+ return fs.realpathSync(left) === fs.realpathSync(right) || sha256File(left) === sha256File(right);
1307
+ }
1308
+ catch (_) {
1309
+ return false;
1310
+ }
1012
1311
  }
1013
-
1014
1312
  function isSha256(value) {
1015
- return typeof value === "string" && /^sha256:[0-9a-f]{64}$/.test(value);
1313
+ return typeof value === "string" && /^sha256:[0-9a-f]{64}$/.test(value);
1016
1314
  }
1017
-
1018
1315
  function isSemver(value) {
1019
- return typeof value === "string" && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(value);
1316
+ return typeof value === "string" && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(value);
1020
1317
  }
1021
-
1022
1318
  function parseSemver(value) {
1023
- const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/.exec(value);
1024
- if (!match) throw new Error("无法比较无效 SemVer");
1025
- return {
1026
- core: [Number(match[1]), Number(match[2]), Number(match[3])],
1027
- prerelease: match[4] ? match[4].split(".") : null,
1028
- };
1029
- }
1030
-
1319
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/.exec(value);
1320
+ if (!match)
1321
+ throw new Error("无法比较无效 SemVer");
1322
+ return {
1323
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
1324
+ prerelease: match[4] ? match[4].split(".") : null,
1325
+ };
1326
+ }
1031
1327
  function compareSemver(left, right) {
1032
- const aParsed = parseSemver(left);
1033
- const bParsed = parseSemver(right);
1034
- const a = aParsed.core;
1035
- const b = bParsed.core;
1036
- for (let index = 0; index < 3; index += 1) {
1037
- if (a[index] !== b[index]) return a[index] < b[index] ? -1 : 1;
1038
- }
1039
- if (!aParsed.prerelease && !bParsed.prerelease) return 0;
1040
- if (!aParsed.prerelease) return 1;
1041
- if (!bParsed.prerelease) return -1;
1042
- const length = Math.max(aParsed.prerelease.length, bParsed.prerelease.length);
1043
- for (let index = 0; index < length; index += 1) {
1044
- if (index >= aParsed.prerelease.length) return -1;
1045
- if (index >= bParsed.prerelease.length) return 1;
1046
- const leftPart = aParsed.prerelease[index];
1047
- const rightPart = bParsed.prerelease[index];
1048
- if (leftPart === rightPart) continue;
1049
- const leftNumeric = /^\d+$/.test(leftPart);
1050
- const rightNumeric = /^\d+$/.test(rightPart);
1051
- if (leftNumeric && rightNumeric) return Number(leftPart) < Number(rightPart) ? -1 : 1;
1052
- if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
1053
- return leftPart < rightPart ? -1 : 1;
1054
- }
1055
- return 0;
1056
- }
1057
-
1328
+ const aParsed = parseSemver(left);
1329
+ const bParsed = parseSemver(right);
1330
+ const a = aParsed.core;
1331
+ const b = bParsed.core;
1332
+ for (let index = 0; index < 3; index += 1) {
1333
+ if (a[index] !== b[index])
1334
+ return a[index] < b[index] ? -1 : 1;
1335
+ }
1336
+ if (!aParsed.prerelease && !bParsed.prerelease)
1337
+ return 0;
1338
+ if (!aParsed.prerelease)
1339
+ return 1;
1340
+ if (!bParsed.prerelease)
1341
+ return -1;
1342
+ const length = Math.max(aParsed.prerelease.length, bParsed.prerelease.length);
1343
+ for (let index = 0; index < length; index += 1) {
1344
+ if (index >= aParsed.prerelease.length)
1345
+ return -1;
1346
+ if (index >= bParsed.prerelease.length)
1347
+ return 1;
1348
+ const leftPart = aParsed.prerelease[index];
1349
+ const rightPart = bParsed.prerelease[index];
1350
+ if (leftPart === rightPart)
1351
+ continue;
1352
+ const leftNumeric = /^\d+$/.test(leftPart);
1353
+ const rightNumeric = /^\d+$/.test(rightPart);
1354
+ if (leftNumeric && rightNumeric)
1355
+ return Number(leftPart) < Number(rightPart) ? -1 : 1;
1356
+ if (leftNumeric !== rightNumeric)
1357
+ return leftNumeric ? -1 : 1;
1358
+ return leftPart < rightPart ? -1 : 1;
1359
+ }
1360
+ return 0;
1361
+ }
1058
1362
  function versionSatisfies(version, range) {
1059
- const match = /^>=(\S+)\s+<(\S+)$/.exec(range);
1060
- if (!match) return false;
1061
- return compareSemver(version, match[1]) >= 0 && compareSemver(version, match[2]) < 0;
1363
+ const match = /^>=(\S+)\s+<(\S+)$/.exec(range);
1364
+ if (!match)
1365
+ return false;
1366
+ return compareSemver(version, match[1]) >= 0 && compareSemver(version, match[2]) < 0;
1062
1367
  }
1063
-
1064
1368
  function migrate(options = {}, packageRoot = PACKAGE_ROOT) {
1065
- return install({ ...options, backupUnmanaged: true, migrateLegacy: true }, packageRoot);
1369
+ return install({ ...options, backupUnmanaged: true, migrateLegacy: true }, packageRoot);
1066
1370
  }
1067
-
1068
1371
  function packageVersion(packageRoot = PACKAGE_ROOT) {
1069
- try {
1070
- const release = readJson(path.join(packageRoot, "release.json"), "release manifest");
1071
- if (isSemver(release.release_version)) return release.release_version;
1072
- } catch (_) { /* source checkout may not have a generated release */ }
1073
- try {
1074
- const packageConfig = readJson(path.join(packageRoot, "package.json"), "package.json");
1075
- if (isSemver(packageConfig.version)) return packageConfig.version;
1076
- } catch (_) { /* use the package fallback */ }
1077
- return PACKAGE_VERSION;
1078
- }
1079
-
1372
+ try {
1373
+ const release = readJson(path.join(packageRoot, "release.json"), "release manifest");
1374
+ if (isSemver(release.release_version))
1375
+ return release.release_version;
1376
+ }
1377
+ catch (_) { /* source checkout may not have a generated release */ }
1378
+ try {
1379
+ const packageConfig = readJson(path.join(packageRoot, "package.json"), "package.json");
1380
+ if (isSemver(packageConfig.version))
1381
+ return packageConfig.version;
1382
+ }
1383
+ catch (_) { /* use the package fallback */ }
1384
+ return PACKAGE_VERSION;
1385
+ }
1080
1386
  function shellQuote(value) {
1081
- return `'${String(value).replace(/'/g, "'\\''")}'`;
1387
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
1082
1388
  }
1083
-
1084
1389
  function buildInstallCommand(paths, options, command, version, extraArgs = []) {
1085
- const args = [
1086
- "npm exec --yes --registry=https://registry.npmjs.org/",
1087
- `--package=${PACKAGE_NAME}@${version}`,
1088
- "--",
1089
- "ml-platform",
1090
- command,
1091
- "--agent", paths.agent,
1092
- "--scope", paths.scope,
1093
- ];
1094
- if (paths.projectDir) args.push("--project-dir", paths.projectDir);
1095
- if (options.skillsDir) args.push("--skills-dir", paths.skillsDir);
1096
- if (options.stateDir) args.push("--state-dir", paths.stateDir);
1097
- if (options.binDir) args.push("--bin-dir", paths.binDir);
1098
- args.push(...extraArgs);
1099
- return args.map((arg, index) => index < 2 ? arg : shellQuote(arg)).join(" ");
1100
- }
1101
-
1390
+ const args = [
1391
+ "npm exec --yes --registry=https://registry.npmjs.org/",
1392
+ `--package=${PACKAGE_NAME}@${version}`,
1393
+ "--",
1394
+ "ml-platform",
1395
+ command,
1396
+ "--agent", paths.agent,
1397
+ "--scope", paths.scope,
1398
+ ];
1399
+ if (paths.projectDir)
1400
+ args.push("--project-dir", paths.projectDir);
1401
+ if (options.skillsDir)
1402
+ args.push("--skills-dir", paths.skillsDir);
1403
+ if (options.stateDir)
1404
+ args.push("--state-dir", paths.stateDir);
1405
+ if (options.binDir)
1406
+ args.push("--bin-dir", paths.binDir);
1407
+ args.push(...extraArgs);
1408
+ return args.map((arg, index) => index < 2 ? arg : shellQuote(arg)).join(" ");
1409
+ }
1102
1410
  module.exports = {
1103
- CHECKSUM_SCHEMA,
1104
- CLI_NAME,
1105
- PACKAGE_NAME,
1106
- PACKAGE_VERSION,
1107
- RECORD_SCHEMA,
1108
- RELEASE_SCHEMA,
1109
- SKILL_NAME,
1110
- compareSemver,
1111
- detectLegacySkill,
1112
- doctor,
1113
- install,
1114
- listFiles,
1115
- loadRelease,
1116
- migrate,
1117
- parseOptions,
1118
- resolveEffectiveApiTarget,
1119
- resolveInstallPaths,
1120
- sha256File,
1121
- status,
1122
- treeDigest,
1123
- verifyPackage,
1411
+ CHECKSUM_SCHEMA,
1412
+ CLI_NAME,
1413
+ PACKAGE_NAME,
1414
+ PACKAGE_VERSION,
1415
+ RECORD_SCHEMA,
1416
+ RELEASE_SCHEMA,
1417
+ SKILL_NAME,
1418
+ compareSemver,
1419
+ detectLegacySkill,
1420
+ doctor,
1421
+ install,
1422
+ listFiles,
1423
+ loadRelease,
1424
+ migrate,
1425
+ parseOptions,
1426
+ resolveEffectiveApiTarget,
1427
+ resolveInstallPaths,
1428
+ sha256File,
1429
+ status,
1430
+ treeDigest,
1431
+ verifyPackage,
1124
1432
  };