@zhuoyuezs/ml-platform 0.1.0 → 0.1.3
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/DEVELOPMENT.md +96 -9
- package/README.md +97 -73
- package/checksums.json +12 -12
- package/package.json +5 -1
- package/release-policy.json +10 -0
- package/release.json +13 -9
- package/runtime/business-client/README.md +13 -0
- package/runtime/business-client/package-lock.json +2 -2
- package/runtime/business-client/package.json +1 -1
- package/runtime/business-client/src/catalog.js +37 -16
- package/runtime/business-client/src/cli.js +67 -15
- package/scripts/lib.js +303 -30
- package/scripts/main.js +103 -11
- package/skills/feature-management/SKILL.md +37 -8
- package/skills/feature-management/references/commands.md +37 -4
package/scripts/lib.js
CHANGED
|
@@ -7,7 +7,9 @@ const path = require("path");
|
|
|
7
7
|
const { spawnSync } = require("child_process");
|
|
8
8
|
const RELEASE_POLICY = require("../release-policy.json");
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const PACKAGE_CONFIG = require("../package.json");
|
|
11
|
+
const PACKAGE_NAME = PACKAGE_CONFIG.name;
|
|
12
|
+
const PACKAGE_VERSION = PACKAGE_CONFIG.version;
|
|
11
13
|
const CLI_NAME = "ml-platform";
|
|
12
14
|
const SKILL_NAME = "feature-management";
|
|
13
15
|
const RELEASE_SCHEMA = "data_platform.ml_platform_release/v2";
|
|
@@ -23,6 +25,12 @@ if (RELEASE_POLICY.schema_version !== "data_platform.ml_platform_release_policy/
|
|
|
23
25
|
const FORBIDDEN_DIRECTORIES = new Set(RELEASE_POLICY.forbidden_directories);
|
|
24
26
|
const FORBIDDEN_FILES = new Set(RELEASE_POLICY.forbidden_files);
|
|
25
27
|
const FORBIDDEN_SUFFIXES = new Set(RELEASE_POLICY.forbidden_suffixes);
|
|
28
|
+
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") };
|
|
33
|
+
});
|
|
26
34
|
|
|
27
35
|
function parseOptions(args) {
|
|
28
36
|
const options = {
|
|
@@ -31,7 +39,10 @@ function parseOptions(args) {
|
|
|
31
39
|
upgrade: false,
|
|
32
40
|
allowDowngrade: false,
|
|
33
41
|
prewarm: true,
|
|
42
|
+
backupUnmanaged: false,
|
|
43
|
+
summary: true,
|
|
34
44
|
};
|
|
45
|
+
let outputFlag = null;
|
|
35
46
|
const valueFlags = new Map([
|
|
36
47
|
["--agent", "agent"],
|
|
37
48
|
["--scope", "scope"],
|
|
@@ -54,6 +65,16 @@ function parseOptions(args) {
|
|
|
54
65
|
options.allowDowngrade = true;
|
|
55
66
|
} else if (arg === "--no-prewarm") {
|
|
56
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;
|
|
57
78
|
} else {
|
|
58
79
|
throw new Error(`未知参数: ${arg}`);
|
|
59
80
|
}
|
|
@@ -68,6 +89,7 @@ function resolveInstallPaths(options = {}) {
|
|
|
68
89
|
if (!["user", "project"].includes(scope)) throw new Error(`不支持的安装作用域: ${scope}`);
|
|
69
90
|
|
|
70
91
|
let skillsDir;
|
|
92
|
+
let projectDir = null;
|
|
71
93
|
if (options.skillsDir) {
|
|
72
94
|
skillsDir = path.resolve(expandHome(options.skillsDir));
|
|
73
95
|
} else if (agent === "codex" && scope === "user") {
|
|
@@ -76,7 +98,7 @@ function resolveInstallPaths(options = {}) {
|
|
|
76
98
|
: path.join(os.homedir(), ".codex");
|
|
77
99
|
skillsDir = path.join(codexHome, "skills");
|
|
78
100
|
} else if (agent === "codex" && scope === "project") {
|
|
79
|
-
|
|
101
|
+
projectDir = path.resolve(expandHome(options.projectDir || process.cwd()));
|
|
80
102
|
if (!fs.existsSync(projectDir)) throw new Error(`Codex 项目目录不存在: ${projectDir}`);
|
|
81
103
|
if (!fs.statSync(projectDir).isDirectory()) throw new Error(`Codex 项目路径不是目录: ${projectDir}`);
|
|
82
104
|
skillsDir = path.join(projectDir, ".agents", "skills");
|
|
@@ -98,6 +120,7 @@ function resolveInstallPaths(options = {}) {
|
|
|
98
120
|
return {
|
|
99
121
|
agent,
|
|
100
122
|
scope,
|
|
123
|
+
projectDir,
|
|
101
124
|
skillsDir,
|
|
102
125
|
target,
|
|
103
126
|
record: path.join(skillsDir, `.${SKILL_NAME}.ml-platform.json`),
|
|
@@ -123,6 +146,12 @@ function loadRelease(packageRoot = PACKAGE_ROOT) {
|
|
|
123
146
|
throw new Error("release policy 摘要与 release manifest 不匹配");
|
|
124
147
|
}
|
|
125
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
|
+
}
|
|
126
155
|
const skill = release.skills && release.skills[SKILL_NAME];
|
|
127
156
|
if (!skill || skill.path !== `skills/${SKILL_NAME}` || !isSha256(skill.sha256)
|
|
128
157
|
|| !isSemver(skill.revision) || typeof skill.requires_cli !== "string") {
|
|
@@ -179,6 +208,13 @@ function verifyPackage(packageRoot = PACKAGE_ROOT) {
|
|
|
179
208
|
assertAllowedClient(clientRoot);
|
|
180
209
|
rejectForbiddenTree(skillRoot, "Skill");
|
|
181
210
|
rejectForbiddenTree(clientRoot, "business client");
|
|
211
|
+
rejectForbiddenContent([
|
|
212
|
+
...listFiles(skillRoot),
|
|
213
|
+
...listFiles(clientRoot),
|
|
214
|
+
...["README.md", "DEVELOPMENT.md"]
|
|
215
|
+
.map((name) => path.join(packageRoot, name))
|
|
216
|
+
.filter((file) => fs.existsSync(file)),
|
|
217
|
+
], "release payload");
|
|
182
218
|
if (fs.existsSync(path.join(skillRoot, "runtime")) || fs.existsSync(path.join(skillRoot, "scripts"))) {
|
|
183
219
|
throw new Error("Skill 不得包含 CLI runtime 或 wrapper");
|
|
184
220
|
}
|
|
@@ -247,6 +283,29 @@ function doctor(options = {}, packageRoot = PACKAGE_ROOT) {
|
|
|
247
283
|
let expectedRelease = null;
|
|
248
284
|
const hasCompletePackage = fs.existsSync(path.join(packageRoot, "checksums.json"))
|
|
249
285
|
&& fs.existsSync(path.join(packageRoot, "skills", SKILL_NAME));
|
|
286
|
+
|
|
287
|
+
// Check for legacy Python Skill before proceeding
|
|
288
|
+
if (!fs.existsSync(paths.record)) {
|
|
289
|
+
const legacy = detectLegacySkill(paths.target);
|
|
290
|
+
if (legacy) {
|
|
291
|
+
const markers = Object.entries(legacy).filter(([_, v]) => v).map(([k]) => k).join(", ");
|
|
292
|
+
return {
|
|
293
|
+
ok: false,
|
|
294
|
+
error: `检测到旧 Python Skill (${markers})`,
|
|
295
|
+
remediation: buildInstallCommand(
|
|
296
|
+
paths,
|
|
297
|
+
options,
|
|
298
|
+
"install",
|
|
299
|
+
packageVersion(packageRoot),
|
|
300
|
+
["--backup-unmanaged"],
|
|
301
|
+
),
|
|
302
|
+
legacy_skill_detected: true,
|
|
303
|
+
legacy_markers: legacy,
|
|
304
|
+
skill_root: paths.target,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
250
309
|
try {
|
|
251
310
|
if (hasCompletePackage) {
|
|
252
311
|
expectedRelease = verifyPackage(packageRoot).release;
|
|
@@ -296,11 +355,31 @@ function doctor(options = {}, packageRoot = PACKAGE_ROOT) {
|
|
|
296
355
|
actual: matchingShim ?? pathMatches[0] ?? null,
|
|
297
356
|
remediation: pathOk ? null : `将 ${paths.binDir} 加入 PATH,然后重启 Agent 会话`,
|
|
298
357
|
});
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
checks.push({
|
|
358
|
+
const apiTarget = resolveEffectiveApiTarget(options, paths.dispatcher);
|
|
359
|
+
if (apiTarget.error) {
|
|
360
|
+
checks.push({
|
|
361
|
+
name: "platform_api",
|
|
362
|
+
ok: false,
|
|
363
|
+
source: apiTarget.source,
|
|
364
|
+
detail: apiTarget.error,
|
|
365
|
+
});
|
|
366
|
+
} else if (apiTarget.apiUrl) {
|
|
367
|
+
const apiHealth = runApiHealth(paths.dispatcher, apiTarget.apiUrl);
|
|
368
|
+
checks.push({
|
|
369
|
+
name: "platform_api",
|
|
370
|
+
ok: apiHealth.ok,
|
|
371
|
+
source: apiTarget.source,
|
|
372
|
+
api_url: apiTarget.apiUrl,
|
|
373
|
+
detail: apiHealth.detail,
|
|
374
|
+
});
|
|
302
375
|
} else {
|
|
303
|
-
checks.push({
|
|
376
|
+
checks.push({
|
|
377
|
+
name: "platform_api",
|
|
378
|
+
ok: null,
|
|
379
|
+
skipped: true,
|
|
380
|
+
source: null,
|
|
381
|
+
reason: "未通过 --api-url、ML_PLATFORM_API_URL 或 configure 配置 API 地址",
|
|
382
|
+
});
|
|
304
383
|
}
|
|
305
384
|
return {
|
|
306
385
|
ok: checks.every((check) => check.ok !== false),
|
|
@@ -320,18 +399,70 @@ function install(options = {}, packageRoot = PACKAGE_ROOT) {
|
|
|
320
399
|
return withInstallLock(paths.lock, () => installLocked(options, packageRoot, verified, paths));
|
|
321
400
|
}
|
|
322
401
|
|
|
402
|
+
function detectLegacySkill(skillPath) {
|
|
403
|
+
if (!pathEntryExists(skillPath) || !fs.lstatSync(skillPath).isDirectory()) return null;
|
|
404
|
+
const markers = {
|
|
405
|
+
pythonWheel: fs.existsSync(path.join(skillPath, "assets", "wheels"))
|
|
406
|
+
&& fs.readdirSync(path.join(skillPath, "assets", "wheels")).some(f => f.endsWith(".whl")),
|
|
407
|
+
runtimeJson: fs.existsSync(path.join(skillPath, "assets", "wheels", "runtime.json")),
|
|
408
|
+
bootstrapScript: fs.existsSync(path.join(skillPath, "scripts", "bootstrap.py")),
|
|
409
|
+
installScript: fs.existsSync(path.join(skillPath, "scripts", "install.py")),
|
|
410
|
+
oldLauncher: fs.existsSync(path.join(skillPath, "scripts", "data-platform-demo"))
|
|
411
|
+
|| fs.existsSync(path.join(skillPath, "scripts", "ml-platform")),
|
|
412
|
+
};
|
|
413
|
+
const hasAnyMarker = Object.values(markers).some(Boolean);
|
|
414
|
+
return hasAnyMarker ? markers : null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function backupUnmanagedSkill(skillPath, skillsDir, legacy) {
|
|
418
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
419
|
+
let suffix = 0;
|
|
420
|
+
let backupPath;
|
|
421
|
+
do {
|
|
422
|
+
const kind = legacy ? "legacy-backup" : "unmanaged-backup";
|
|
423
|
+
const backupName = `.${SKILL_NAME}.${kind}.${timestamp}${suffix ? `-${suffix}` : ""}`;
|
|
424
|
+
backupPath = path.join(skillsDir, backupName);
|
|
425
|
+
suffix += 1;
|
|
426
|
+
} while (pathEntryExists(backupPath));
|
|
427
|
+
fs.renameSync(skillPath, backupPath);
|
|
428
|
+
return backupPath;
|
|
429
|
+
}
|
|
430
|
+
|
|
323
431
|
function installLocked(options, packageRoot, verified, paths) {
|
|
324
432
|
const previousRecord = fs.existsSync(paths.record) ? fs.readFileSync(paths.record) : null;
|
|
325
433
|
const current = safeReadInstallationRecord(paths.record, paths);
|
|
326
|
-
const targetExists =
|
|
327
|
-
|
|
328
|
-
|
|
434
|
+
const targetExists = pathEntryExists(paths.target);
|
|
435
|
+
const legacy = targetExists && !current ? detectLegacySkill(paths.target) : null;
|
|
436
|
+
const backingUpUnmanaged = Boolean(targetExists && !current && options.backupUnmanaged);
|
|
437
|
+
|
|
438
|
+
if (options.migrateLegacy && !legacy) {
|
|
439
|
+
throw new Error(`目标 Skill 不是旧 Python Skill 或不存在: ${paths.target}`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (targetExists && !current && !backingUpUnmanaged) {
|
|
443
|
+
if (legacy && !options.backupUnmanaged) {
|
|
444
|
+
const markers = Object.entries(legacy).filter(([_, v]) => v).map(([k]) => k).join(", ");
|
|
445
|
+
throw new Error(
|
|
446
|
+
`目标 Skill 包含旧 Python runtime 标记 (${markers}),拒绝覆盖: ${paths.target}\n` +
|
|
447
|
+
` 迁移方法: 使用 --backup-unmanaged 或运行 'ml-platform migrate' 先备份旧 Skill`
|
|
448
|
+
);
|
|
449
|
+
} else if (fs.lstatSync(paths.target).isSymbolicLink()) {
|
|
450
|
+
throw new Error(
|
|
451
|
+
`目标 Skill 路径是未托管的符号链接,拒绝覆盖: ${paths.target}\n` +
|
|
452
|
+
" 处理方法: 确认后使用 --backup-unmanaged 备份该链接并安装正式 Skill"
|
|
453
|
+
);
|
|
454
|
+
} else if (!legacy) {
|
|
455
|
+
throw new Error(
|
|
456
|
+
`目标 Skill 不受 ${PACKAGE_NAME} 管理,拒绝覆盖: ${paths.target}\n` +
|
|
457
|
+
" 处理方法: 确认后使用 --backup-unmanaged 备份原目录"
|
|
458
|
+
);
|
|
459
|
+
}
|
|
329
460
|
}
|
|
330
461
|
const sharedRuntime = safeReadRuntimeRecord(paths.runtimeRecord);
|
|
331
|
-
if (
|
|
462
|
+
if (pathEntryExists(paths.installRoot) && !sharedRuntime) {
|
|
332
463
|
throw new Error(`持久 CLI 目录不受 ${PACKAGE_NAME} 管理,拒绝覆盖: ${paths.installRoot}`);
|
|
333
464
|
}
|
|
334
|
-
if (
|
|
465
|
+
if (pathEntryExists(paths.shim) && (!current || sha256File(paths.shim) !== current.shim.sha256)
|
|
335
466
|
&& (!sharedRuntime || !shimTargetsDispatcher(paths.shim, paths.dispatcher))) {
|
|
336
467
|
throw new Error(`CLI shim 不受 ${PACKAGE_NAME} 管理,拒绝覆盖: ${paths.shim}`);
|
|
337
468
|
}
|
|
@@ -360,7 +491,7 @@ function installLocked(options, packageRoot, verified, paths) {
|
|
|
360
491
|
return installationResult("unchanged", paths, verified.release, current);
|
|
361
492
|
}
|
|
362
493
|
|
|
363
|
-
if ((targetExists || (sharedRuntime && !sharedRuntimeMatches)) && !options.upgrade) {
|
|
494
|
+
if (((targetExists && !backingUpUnmanaged) || (sharedRuntime && !sharedRuntimeMatches)) && !options.upgrade) {
|
|
364
495
|
throw new Error("已存在不同版本或摘要;升级时请使用 upgrade 或 install --upgrade");
|
|
365
496
|
}
|
|
366
497
|
const installedVersion = current ? current.release_version : sharedRuntime && sharedRuntime.release_version;
|
|
@@ -383,6 +514,8 @@ function installLocked(options, packageRoot, verified, paths) {
|
|
|
383
514
|
let hadState = false;
|
|
384
515
|
let hadSkill = false;
|
|
385
516
|
let hadShim = false;
|
|
517
|
+
let legacyBackupPath = null;
|
|
518
|
+
let legacyMoved = false;
|
|
386
519
|
try {
|
|
387
520
|
stagePersistentInstall(stagedInstall, packageRoot, verified);
|
|
388
521
|
fs.cpSync(verified.skillRoot, stagedSkill, { recursive: true, errorOnExist: true });
|
|
@@ -391,19 +524,23 @@ function installLocked(options, packageRoot, verified, paths) {
|
|
|
391
524
|
}
|
|
392
525
|
writeShim(shimTemporary, paths.dispatcher);
|
|
393
526
|
|
|
394
|
-
if (
|
|
527
|
+
if (backingUpUnmanaged) {
|
|
528
|
+
legacyBackupPath = backupUnmanagedSkill(paths.target, paths.skillsDir, Boolean(legacy));
|
|
529
|
+
legacyMoved = true;
|
|
530
|
+
}
|
|
531
|
+
if (pathEntryExists(paths.installRoot)) {
|
|
395
532
|
fs.renameSync(paths.installRoot, stateBackup);
|
|
396
533
|
hadState = true;
|
|
397
534
|
}
|
|
398
535
|
fs.renameSync(stagedInstall, paths.installRoot);
|
|
399
536
|
stateMoved = true;
|
|
400
|
-
if (
|
|
537
|
+
if (pathEntryExists(paths.target)) {
|
|
401
538
|
fs.renameSync(paths.target, skillBackup);
|
|
402
539
|
hadSkill = true;
|
|
403
540
|
}
|
|
404
541
|
fs.renameSync(stagedSkill, paths.target);
|
|
405
542
|
skillMoved = true;
|
|
406
|
-
if (
|
|
543
|
+
if (pathEntryExists(paths.shim)) {
|
|
407
544
|
fs.renameSync(paths.shim, shimBackup);
|
|
408
545
|
hadShim = true;
|
|
409
546
|
}
|
|
@@ -419,19 +556,32 @@ function installLocked(options, packageRoot, verified, paths) {
|
|
|
419
556
|
if (hadState) removeOwnedDirectory(stateBackup, paths.stateDir, ".backup.");
|
|
420
557
|
if (hadSkill) removeOwnedDirectory(skillBackup, paths.skillsDir, `.${SKILL_NAME}.backup.`);
|
|
421
558
|
if (hadShim) fs.rmSync(shimBackup, { force: true });
|
|
422
|
-
|
|
559
|
+
const result = installationResult(
|
|
560
|
+
options.migrateLegacy ? "migrated" : (current || (targetExists && !backingUpUnmanaged) ? "upgraded" : "installed"),
|
|
561
|
+
paths,
|
|
562
|
+
verified.release,
|
|
563
|
+
record,
|
|
564
|
+
);
|
|
565
|
+
if (legacyBackupPath) {
|
|
566
|
+
if (legacy) result.legacy_skill_backup = legacyBackupPath;
|
|
567
|
+
else result.unmanaged_skill_backup = legacyBackupPath;
|
|
568
|
+
}
|
|
569
|
+
return result;
|
|
423
570
|
} catch (error) {
|
|
424
|
-
if (shimMoved &&
|
|
425
|
-
if (hadShim &&
|
|
426
|
-
if (skillMoved &&
|
|
427
|
-
if (hadSkill &&
|
|
428
|
-
if (
|
|
429
|
-
|
|
571
|
+
if (shimMoved && pathEntryExists(paths.shim)) fs.rmSync(paths.shim, { force: true });
|
|
572
|
+
if (hadShim && pathEntryExists(shimBackup)) fs.renameSync(shimBackup, paths.shim);
|
|
573
|
+
if (skillMoved && pathEntryExists(paths.target)) removeOwnedDirectory(paths.target, paths.skillsDir, SKILL_NAME);
|
|
574
|
+
if (hadSkill && pathEntryExists(skillBackup)) fs.renameSync(skillBackup, paths.target);
|
|
575
|
+
if (legacyMoved && legacyBackupPath && pathEntryExists(legacyBackupPath) && !pathEntryExists(paths.target)) {
|
|
576
|
+
fs.renameSync(legacyBackupPath, paths.target);
|
|
577
|
+
}
|
|
578
|
+
if (stateMoved && pathEntryExists(paths.installRoot)) removeOwnedDirectory(paths.installRoot, paths.stateDir, "current");
|
|
579
|
+
if (hadState && pathEntryExists(stateBackup)) fs.renameSync(stateBackup, paths.installRoot);
|
|
430
580
|
if (previousRecord === null) fs.rmSync(paths.record, { force: true });
|
|
431
581
|
else fs.writeFileSync(paths.record, previousRecord);
|
|
432
|
-
if (
|
|
433
|
-
if (
|
|
434
|
-
if (
|
|
582
|
+
if (pathEntryExists(shimTemporary)) fs.rmSync(shimTemporary, { force: true });
|
|
583
|
+
if (pathEntryExists(stateStaging)) removeOwnedDirectory(stateStaging, paths.stateDir, ".install.");
|
|
584
|
+
if (pathEntryExists(skillStaging)) removeOwnedDirectory(skillStaging, paths.skillsDir, `.${SKILL_NAME}.install.`);
|
|
435
585
|
throw error;
|
|
436
586
|
}
|
|
437
587
|
}
|
|
@@ -442,6 +592,7 @@ function stagePersistentInstall(target, packageRoot, verified) {
|
|
|
442
592
|
for (const name of ["main.js", "lib.js"]) {
|
|
443
593
|
fs.copyFileSync(path.join(packageRoot, "scripts", name), path.join(target, "scripts", name));
|
|
444
594
|
}
|
|
595
|
+
fs.copyFileSync(path.join(packageRoot, "package.json"), path.join(target, "package.json"));
|
|
445
596
|
fs.chmodSync(path.join(target, "scripts", "main.js"), 0o755);
|
|
446
597
|
fs.cpSync(verified.clientRoot, path.join(target, "runtime", "business-client"), {
|
|
447
598
|
recursive: true,
|
|
@@ -591,6 +742,38 @@ function runApiHealth(dispatcher, apiUrl) {
|
|
|
591
742
|
return runCliRuntime(dispatcher, ["--api-url", apiUrl.replace(/\/+$/, ""), "health"], false);
|
|
592
743
|
}
|
|
593
744
|
|
|
745
|
+
function resolveEffectiveApiTarget(
|
|
746
|
+
options,
|
|
747
|
+
dispatcher,
|
|
748
|
+
env = process.env,
|
|
749
|
+
runtime = runCliRuntime,
|
|
750
|
+
) {
|
|
751
|
+
if (options.apiUrl && String(options.apiUrl).trim()) {
|
|
752
|
+
return { apiUrl: String(options.apiUrl).trim(), source: "argument" };
|
|
753
|
+
}
|
|
754
|
+
if (env.ML_PLATFORM_API_URL && String(env.ML_PLATFORM_API_URL).trim()) {
|
|
755
|
+
return { apiUrl: String(env.ML_PLATFORM_API_URL).trim(), source: "environment" };
|
|
756
|
+
}
|
|
757
|
+
const configured = runtime(dispatcher, ["show-config"], false);
|
|
758
|
+
if (!configured.ok) {
|
|
759
|
+
return {
|
|
760
|
+
apiUrl: null,
|
|
761
|
+
source: "saved_config",
|
|
762
|
+
error: `无法读取已保存的 API 配置: ${configured.detail || "未知错误"}`,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
let payload;
|
|
766
|
+
try {
|
|
767
|
+
payload = JSON.parse(configured.detail);
|
|
768
|
+
} catch (_) {
|
|
769
|
+
return { apiUrl: null, source: "saved_config", error: "show-config 未返回有效 JSON" };
|
|
770
|
+
}
|
|
771
|
+
const apiUrl = payload && payload.effective && payload.effective.api_url;
|
|
772
|
+
return apiUrl
|
|
773
|
+
? { apiUrl: String(apiUrl).trim(), source: "saved_config" }
|
|
774
|
+
: { apiUrl: null, source: null };
|
|
775
|
+
}
|
|
776
|
+
|
|
594
777
|
function checkTree(checks, name, root, expected) {
|
|
595
778
|
if (!expected || !fs.existsSync(root)) {
|
|
596
779
|
checks.push({ name, ok: false, error: `缺少安装目录或预期摘要: ${root}` });
|
|
@@ -618,6 +801,16 @@ function digestEquals(root, expected) {
|
|
|
618
801
|
catch (_) { return false; }
|
|
619
802
|
}
|
|
620
803
|
|
|
804
|
+
function pathEntryExists(entry) {
|
|
805
|
+
try {
|
|
806
|
+
fs.lstatSync(entry);
|
|
807
|
+
return true;
|
|
808
|
+
} catch (error) {
|
|
809
|
+
if (error.code === "ENOENT") return false;
|
|
810
|
+
throw error;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
621
814
|
function withInstallLock(lockPath, callback) {
|
|
622
815
|
let descriptor;
|
|
623
816
|
try {
|
|
@@ -684,6 +877,18 @@ function rejectForbiddenTree(root, label) {
|
|
|
684
877
|
visit(root);
|
|
685
878
|
}
|
|
686
879
|
|
|
880
|
+
function rejectForbiddenContent(files, label) {
|
|
881
|
+
for (const file of files) {
|
|
882
|
+
if (!fs.statSync(file).isFile() || path.basename(file) === "release-policy.json") continue;
|
|
883
|
+
const text = fs.readFileSync(file, "utf8");
|
|
884
|
+
for (const rule of FORBIDDEN_CONTENT_PATTERNS) {
|
|
885
|
+
if (rule.pattern.test(text)) {
|
|
886
|
+
throw new Error(`${label} 包含禁止发布内容 ${rule.name}: ${file}`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
687
892
|
function treeDigest(root) {
|
|
688
893
|
const files = listFiles(root);
|
|
689
894
|
const hash = crypto.createHash("sha256");
|
|
@@ -782,35 +987,103 @@ function isSemver(value) {
|
|
|
782
987
|
return typeof value === "string" && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(value);
|
|
783
988
|
}
|
|
784
989
|
|
|
990
|
+
function parseSemver(value) {
|
|
991
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/.exec(value);
|
|
992
|
+
if (!match) throw new Error("无法比较无效 SemVer");
|
|
993
|
+
return {
|
|
994
|
+
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
995
|
+
prerelease: match[4] ? match[4].split(".") : null,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
785
999
|
function compareSemver(left, right) {
|
|
786
|
-
|
|
787
|
-
const
|
|
788
|
-
const
|
|
1000
|
+
const aParsed = parseSemver(left);
|
|
1001
|
+
const bParsed = parseSemver(right);
|
|
1002
|
+
const a = aParsed.core;
|
|
1003
|
+
const b = bParsed.core;
|
|
789
1004
|
for (let index = 0; index < 3; index += 1) {
|
|
790
1005
|
if (a[index] !== b[index]) return a[index] < b[index] ? -1 : 1;
|
|
791
1006
|
}
|
|
1007
|
+
if (!aParsed.prerelease && !bParsed.prerelease) return 0;
|
|
1008
|
+
if (!aParsed.prerelease) return 1;
|
|
1009
|
+
if (!bParsed.prerelease) return -1;
|
|
1010
|
+
const length = Math.max(aParsed.prerelease.length, bParsed.prerelease.length);
|
|
1011
|
+
for (let index = 0; index < length; index += 1) {
|
|
1012
|
+
if (index >= aParsed.prerelease.length) return -1;
|
|
1013
|
+
if (index >= bParsed.prerelease.length) return 1;
|
|
1014
|
+
const leftPart = aParsed.prerelease[index];
|
|
1015
|
+
const rightPart = bParsed.prerelease[index];
|
|
1016
|
+
if (leftPart === rightPart) continue;
|
|
1017
|
+
const leftNumeric = /^\d+$/.test(leftPart);
|
|
1018
|
+
const rightNumeric = /^\d+$/.test(rightPart);
|
|
1019
|
+
if (leftNumeric && rightNumeric) return Number(leftPart) < Number(rightPart) ? -1 : 1;
|
|
1020
|
+
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
|
|
1021
|
+
return leftPart < rightPart ? -1 : 1;
|
|
1022
|
+
}
|
|
792
1023
|
return 0;
|
|
793
1024
|
}
|
|
794
1025
|
|
|
795
1026
|
function versionSatisfies(version, range) {
|
|
796
|
-
const match =
|
|
1027
|
+
const match = /^>=(\S+)\s+<(\S+)$/.exec(range);
|
|
797
1028
|
if (!match) return false;
|
|
798
|
-
return compareSemver(version, match[1]
|
|
1029
|
+
return compareSemver(version, match[1]) >= 0 && compareSemver(version, match[2]) < 0;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function migrate(options = {}, packageRoot = PACKAGE_ROOT) {
|
|
1033
|
+
return install({ ...options, backupUnmanaged: true, migrateLegacy: true }, packageRoot);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function packageVersion(packageRoot = PACKAGE_ROOT) {
|
|
1037
|
+
try {
|
|
1038
|
+
const release = readJson(path.join(packageRoot, "release.json"), "release manifest");
|
|
1039
|
+
if (isSemver(release.release_version)) return release.release_version;
|
|
1040
|
+
} catch (_) { /* source checkout may not have a generated release */ }
|
|
1041
|
+
try {
|
|
1042
|
+
const packageConfig = readJson(path.join(packageRoot, "package.json"), "package.json");
|
|
1043
|
+
if (isSemver(packageConfig.version)) return packageConfig.version;
|
|
1044
|
+
} catch (_) { /* use the package fallback */ }
|
|
1045
|
+
return PACKAGE_VERSION;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function shellQuote(value) {
|
|
1049
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function buildInstallCommand(paths, options, command, version, extraArgs = []) {
|
|
1053
|
+
const args = [
|
|
1054
|
+
"npm exec --yes --registry=https://registry.npmjs.org/",
|
|
1055
|
+
`--package=${PACKAGE_NAME}@${version}`,
|
|
1056
|
+
"--",
|
|
1057
|
+
"ml-platform",
|
|
1058
|
+
command,
|
|
1059
|
+
"--agent", paths.agent,
|
|
1060
|
+
"--scope", paths.scope,
|
|
1061
|
+
];
|
|
1062
|
+
if (paths.projectDir) args.push("--project-dir", paths.projectDir);
|
|
1063
|
+
if (options.skillsDir) args.push("--skills-dir", paths.skillsDir);
|
|
1064
|
+
if (options.stateDir) args.push("--state-dir", paths.stateDir);
|
|
1065
|
+
if (options.binDir) args.push("--bin-dir", paths.binDir);
|
|
1066
|
+
args.push(...extraArgs);
|
|
1067
|
+
return args.map((arg, index) => index < 2 ? arg : shellQuote(arg)).join(" ");
|
|
799
1068
|
}
|
|
800
1069
|
|
|
801
1070
|
module.exports = {
|
|
802
1071
|
CHECKSUM_SCHEMA,
|
|
803
1072
|
CLI_NAME,
|
|
804
1073
|
PACKAGE_NAME,
|
|
1074
|
+
PACKAGE_VERSION,
|
|
805
1075
|
RECORD_SCHEMA,
|
|
806
1076
|
RELEASE_SCHEMA,
|
|
807
1077
|
SKILL_NAME,
|
|
808
1078
|
compareSemver,
|
|
1079
|
+
detectLegacySkill,
|
|
809
1080
|
doctor,
|
|
810
1081
|
install,
|
|
811
1082
|
listFiles,
|
|
812
1083
|
loadRelease,
|
|
1084
|
+
migrate,
|
|
813
1085
|
parseOptions,
|
|
1086
|
+
resolveEffectiveApiTarget,
|
|
814
1087
|
resolveInstallPaths,
|
|
815
1088
|
sha256File,
|
|
816
1089
|
status,
|