@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/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 PACKAGE_NAME = "@zhuoyuezs/ml-platform";
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
- const projectDir = path.resolve(expandHome(options.projectDir || process.cwd()));
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
- if (options.apiUrl) {
300
- const apiHealth = runApiHealth(paths.dispatcher, options.apiUrl);
301
- checks.push({ name: "platform_api", ok: apiHealth.ok, detail: apiHealth.detail });
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({ name: "platform_api", ok: null, skipped: true, reason: "未提供 --api-url" });
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 = fs.existsSync(paths.target);
327
- if (targetExists && !current) {
328
- throw new Error(`目标 Skill 不受 ${PACKAGE_NAME} 管理,拒绝覆盖: ${paths.target}`);
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 (fs.existsSync(paths.installRoot) && !sharedRuntime) {
462
+ if (pathEntryExists(paths.installRoot) && !sharedRuntime) {
332
463
  throw new Error(`持久 CLI 目录不受 ${PACKAGE_NAME} 管理,拒绝覆盖: ${paths.installRoot}`);
333
464
  }
334
- if (fs.existsSync(paths.shim) && (!current || sha256File(paths.shim) !== current.shim.sha256)
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 (fs.existsSync(paths.installRoot)) {
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 (fs.existsSync(paths.target)) {
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 (fs.existsSync(paths.shim)) {
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
- return installationResult(current || targetExists ? "upgraded" : "installed", paths, verified.release, record);
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 && fs.existsSync(paths.shim)) fs.rmSync(paths.shim, { force: true });
425
- if (hadShim && fs.existsSync(shimBackup)) fs.renameSync(shimBackup, paths.shim);
426
- if (skillMoved && fs.existsSync(paths.target)) removeOwnedDirectory(paths.target, paths.skillsDir, SKILL_NAME);
427
- if (hadSkill && fs.existsSync(skillBackup)) fs.renameSync(skillBackup, paths.target);
428
- if (stateMoved && fs.existsSync(paths.installRoot)) removeOwnedDirectory(paths.installRoot, paths.stateDir, "current");
429
- if (hadState && fs.existsSync(stateBackup)) fs.renameSync(stateBackup, paths.installRoot);
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 (fs.existsSync(shimTemporary)) fs.rmSync(shimTemporary, { force: true });
433
- if (fs.existsSync(stateStaging)) removeOwnedDirectory(stateStaging, paths.stateDir, ".install.");
434
- if (fs.existsSync(skillStaging)) removeOwnedDirectory(skillStaging, paths.skillsDir, `.${SKILL_NAME}.install.`);
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
- if (!isSemver(left) || !isSemver(right)) throw new Error("无法比较无效 SemVer");
787
- const a = left.split("-")[0].split(".").map(Number);
788
- const b = right.split("-")[0].split(".").map(Number);
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 = /^(>=\d+\.\d+\.\d+)\s+(<\d+\.\d+\.\d+)$/.exec(range);
1027
+ const match = /^>=(\S+)\s+<(\S+)$/.exec(range);
797
1028
  if (!match) return false;
798
- return compareSemver(version, match[1].slice(2)) >= 0 && compareSemver(version, match[2].slice(1)) < 0;
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,