@zhuoyuezs/ml-platform 0.1.0 → 0.1.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/DEVELOPMENT.md CHANGED
@@ -10,7 +10,7 @@
10
10
  - Skill 直接调用正式命令 `ml-platform`,不嵌入 CLI runtime。
11
11
  - 正式 npm 包只有 `@zhuoyuezs/ml-platform`,正式 bin 只有 `ml-platform`。
12
12
  - `business-client-js/` 是 CLI 唯一源码,`skills/feature-management/` 是 Skill 唯一源码。
13
- - `npx @zhuoyuezs/ml-platform@<version> install` 是统一安装入口。
13
+ - `npx --registry=https://registry.npmjs.org/ @zhuoyuezs/ml-platform@<version> install` 是统一安装入口。
14
14
  - npm `postinstall` 不得修改 Agent Skills 目录。
15
15
  - Python business client 只保留为迁移 oracle,不进入 release。
16
16
  - Codex CLI 不重新打包;当前只支持唯一自研 Host。
@@ -138,7 +138,7 @@ Host 副本、日志、会话数据、attic、凭证和实验文件。
138
138
  ```json
139
139
  {
140
140
  "name": "@zhuoyuezs/ml-platform",
141
- "version": "0.1.0",
141
+ "version": "0.1.1",
142
142
  "bin": {
143
143
  "ml-platform": "scripts/main.js"
144
144
  },
package/README.md CHANGED
@@ -1,103 +1,76 @@
1
1
  # ML Platform
2
2
 
3
- `@zhuoyuezs/ml-platform` 通过一个 npm 产品提供 JavaScript business CLI 和
4
- `feature-management` Agent Skill。
3
+ ML Platform JavaScript CLI 和 `feature-management` Agent Skill。运行时只需要 Node.js 18+,
4
+ 不需要 Python 或 `uv`。
5
5
 
6
- 正式包名和命令固定为:
6
+ ## 安装
7
7
 
8
- ```text
9
- package: @zhuoyuezs/ml-platform
10
- bin: ml-platform
11
- ```
8
+ 在 Codex 项目根目录运行:
12
9
 
13
- > 当前状态:`0.1.0` release candidate 已实现 release v2 的外部 CLI + 纯 Skill 架构,并通过
14
- > 干净安装、cache 删除、`status`、`doctor` 和摘要验证。当前范围仅包含 Skill + CLI,不打包
15
- > Agent Host。正式发布必须从 `dist/package/` 执行 `npm publish`。
10
+ ```bash
11
+ npx --yes --registry=https://registry.npmjs.org/ \
12
+ @zhuoyuezs/ml-platform@latest install \
13
+ --agent codex \
14
+ --scope project \
15
+ --project-dir "$PWD"
16
+ ```
16
17
 
17
- ## 组件关系
18
+ 安装器会创建持久命令 `~/.local/bin/ml-platform`,并将 Skill 安装到:
18
19
 
19
20
  ```text
20
- 开发人员 / CI
21
- -> ml-platform CLI
22
- -> ML Platform API
23
-
24
- Codex / ml-platform-host
25
- -> feature-management Skill
26
- -> ml-platform CLI
27
- -> ML Platform API
21
+ <project>/.agents/skills/feature-management/
28
22
  ```
29
23
 
30
- - CLI 可以独立运行,不依赖 Skill
31
- - Skill 负责工作流程、业务契约和安全边界,直接调用 `ml-platform`。
32
- - npm release 同时固定 CLI version 和 Skill revision。
33
- - Skill 不再携带 business CLI、Python wheel、bootstrap 或安装器。
34
- - Codex CLI 和唯一自研 Host 是并行入口,都不进入本 npm 包。
24
+ 如果输出 `path_ready: false`,按 `path_setup` 提示配置 PATH,然后重启终端和 Agent
35
25
 
36
- ## 包结构
26
+ 用户级安装:
37
27
 
38
- ```text
39
- @zhuoyuezs/ml-platform/
40
- ├── package.json
41
- ├── scripts/
42
- ├── runtime/business-client/
43
- ├── skills/feature-management/
44
- ├── release.json
45
- ├── checksums.json
46
- ├── README.md
47
- └── DEVELOPMENT.md
28
+ ```bash
29
+ npx --yes --registry=https://registry.npmjs.org/ \
30
+ @zhuoyuezs/ml-platform@latest install \
31
+ --agent codex \
32
+ --scope user
48
33
  ```
49
34
 
50
- 归档中只有 `runtime/business-client/` 一份 business CLI。`skills/feature-management/` 是纯
51
- Skill,与 CLI 并列。
35
+ ## 迁移旧 Skill
52
36
 
53
- ## 安装体验
54
-
55
- 参考飞书 CLI,正式入口使用显式版本化 npx 命令:
37
+ 如果目标位置已有旧 Python Skill、实验 symlink 或其他未托管 Skill,确认后显式备份并安装:
56
38
 
57
39
  ```bash
58
- npx @zhuoyuezs/ml-platform@<version> install \
40
+ npx --yes --registry=https://registry.npmjs.org/ \
41
+ @zhuoyuezs/ml-platform@latest install \
59
42
  --agent codex \
60
43
  --scope project \
61
- --project-dir /absolute/path/to/project
44
+ --project-dir "$PWD" \
45
+ --backup-unmanaged
62
46
  ```
63
47
 
64
- 安装命令必须建立可持续使用的 `ml-platform` 命令,再把纯 Skill 安装到目标 Agent 目录。它不能
65
- 依赖一次性 npx cache,也不能通过 npm `postinstall` 修改用户 Skills。
66
-
67
- 默认 runtime 位于 `~/.local/share/ml-platform/current/`,shim 位于
68
- `~/.local/bin/ml-platform`。安装结果中的 `path_ready` 和 `path_setup` 会说明是否还需把 bin
69
- 目录加入 PATH。隔离测试可显式传入 `--state-dir` 和 `--bin-dir`。
48
+ 安装器会保留带时间戳的备份,不会直接删除原 Skill
70
49
 
71
- 安装后:
50
+ ## 使用
72
51
 
73
52
  ```bash
74
- ml-platform version
75
- ml-platform status --agent codex --scope project --project-dir /absolute/path/to/project
76
- ml-platform doctor --agent codex --scope project --project-dir /absolute/path/to/project
77
-
78
- ml-platform configure --api-url http://<approved-host>:<port>
53
+ ml-platform configure --api-url http://<host>:<port>
79
54
  ml-platform health
55
+ ml-platform list-parameters
56
+ ml-platform apply /absolute/path/to/catalog --dry-run
57
+ ml-platform status --agent codex --scope project --project-dir "$PWD"
58
+ ml-platform doctor --agent codex --scope project --project-dir "$PWD"
80
59
  ```
81
60
 
82
- ## Canonical Source
61
+ 查看完整命令:
83
62
 
84
- ```text
85
- business-client-js/ JavaScript CLI 唯一源码
86
- skills/feature-management/ Skill 唯一源码
87
- npm/ml-platform/ npm dispatcher 与 installer 唯一源码
88
- npm/scripts/build_ml_platform_release.py
89
- agent_poc/codex-host/ 唯一自研 Host
63
+ ```bash
64
+ ml-platform --help
90
65
  ```
91
66
 
92
- 不要修改 `dist/`、临时安装目录或 Agent Skills 安装副本。Python `business-client/` 只保留为迁移
93
- oracle,不进入 npm release。
67
+ ## 包内容
94
68
 
95
- ## 当前开发状态
69
+ - 一份 JavaScript business CLI;
70
+ - 一份纯 `feature-management` Skill;
71
+ - 显式安装、升级、迁移、状态、校验和回滚逻辑。
96
72
 
97
- 1. `business-client-js` parity 与纯 `feature-management` Skill 已完成并独立验收。
98
- 2. npm v2 builder、dispatcher、持久 installer、ownership、digest 与回滚已完成。
99
- 3. 新的本地 `.tgz`、`.tgz.sha256` 和 Codex project clean install smoke 已通过。
100
- 4. Agent Host 当前不在本包构建范围内;后续需明确需求后单独推进。
73
+ 本包不包含 Codex CLI、Python business-client wheel、Agent Host、平台服务端、模型训练或
74
+ Kubernetes 部署。
101
75
 
102
- 完整约束见 [DEVELOPMENT.md](DEVELOPMENT.md)
103
- [`npm/docs/AGENT_SKILL_CLI_PACKAGING_ARCHITECTURE.md`](../docs/AGENT_SKILL_CLI_PACKAGING_ARCHITECTURE.md)。
76
+ 开发和发布约束见 [DEVELOPMENT.md](DEVELOPMENT.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhuoyuezs/ml-platform",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "ML 数据平台 business CLI 与 Agent Skills 管理工具",
5
5
  "bin": {
6
6
  "ml-platform": "scripts/main.js"
package/release.json CHANGED
@@ -25,7 +25,7 @@
25
25
  "ml_data_platform.feature_set/v1",
26
26
  "ml_data_platform.dataset_manifest/v1"
27
27
  ],
28
- "release_version": "0.1.0",
28
+ "release_version": "0.1.1",
29
29
  "runtime_requirements": {
30
30
  "node": ">=18"
31
31
  },
@@ -34,9 +34,9 @@
34
34
  "feature-management": {
35
35
  "path": "skills/feature-management",
36
36
  "requires_cli": ">=0.5.0 <0.6.0",
37
- "revision": "0.1.0",
37
+ "revision": "0.1.1",
38
38
  "sha256": "sha256:f7c2ba836338602f6f01ebec18b595a366f5dd1bcbfcb58e249cda5c10bd61dd"
39
39
  }
40
40
  },
41
- "source_commit": "0a124d08d6d35f07b6bd90bb37fd81ff0f698ab7"
41
+ "source_commit": "07e4f763c89c574d417e37d23d84cf5bc04120d7"
42
42
  }
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";
@@ -31,6 +33,7 @@ function parseOptions(args) {
31
33
  upgrade: false,
32
34
  allowDowngrade: false,
33
35
  prewarm: true,
36
+ backupUnmanaged: false,
34
37
  };
35
38
  const valueFlags = new Map([
36
39
  ["--agent", "agent"],
@@ -54,6 +57,8 @@ function parseOptions(args) {
54
57
  options.allowDowngrade = true;
55
58
  } else if (arg === "--no-prewarm") {
56
59
  options.prewarm = false;
60
+ } else if (arg === "--backup-unmanaged") {
61
+ options.backupUnmanaged = true;
57
62
  } else {
58
63
  throw new Error(`未知参数: ${arg}`);
59
64
  }
@@ -68,6 +73,7 @@ function resolveInstallPaths(options = {}) {
68
73
  if (!["user", "project"].includes(scope)) throw new Error(`不支持的安装作用域: ${scope}`);
69
74
 
70
75
  let skillsDir;
76
+ let projectDir = null;
71
77
  if (options.skillsDir) {
72
78
  skillsDir = path.resolve(expandHome(options.skillsDir));
73
79
  } else if (agent === "codex" && scope === "user") {
@@ -76,7 +82,7 @@ function resolveInstallPaths(options = {}) {
76
82
  : path.join(os.homedir(), ".codex");
77
83
  skillsDir = path.join(codexHome, "skills");
78
84
  } else if (agent === "codex" && scope === "project") {
79
- const projectDir = path.resolve(expandHome(options.projectDir || process.cwd()));
85
+ projectDir = path.resolve(expandHome(options.projectDir || process.cwd()));
80
86
  if (!fs.existsSync(projectDir)) throw new Error(`Codex 项目目录不存在: ${projectDir}`);
81
87
  if (!fs.statSync(projectDir).isDirectory()) throw new Error(`Codex 项目路径不是目录: ${projectDir}`);
82
88
  skillsDir = path.join(projectDir, ".agents", "skills");
@@ -98,6 +104,7 @@ function resolveInstallPaths(options = {}) {
98
104
  return {
99
105
  agent,
100
106
  scope,
107
+ projectDir,
101
108
  skillsDir,
102
109
  target,
103
110
  record: path.join(skillsDir, `.${SKILL_NAME}.ml-platform.json`),
@@ -247,6 +254,29 @@ function doctor(options = {}, packageRoot = PACKAGE_ROOT) {
247
254
  let expectedRelease = null;
248
255
  const hasCompletePackage = fs.existsSync(path.join(packageRoot, "checksums.json"))
249
256
  && fs.existsSync(path.join(packageRoot, "skills", SKILL_NAME));
257
+
258
+ // Check for legacy Python Skill before proceeding
259
+ if (!fs.existsSync(paths.record)) {
260
+ const legacy = detectLegacySkill(paths.target);
261
+ if (legacy) {
262
+ const markers = Object.entries(legacy).filter(([_, v]) => v).map(([k]) => k).join(", ");
263
+ return {
264
+ ok: false,
265
+ error: `检测到旧 Python Skill (${markers})`,
266
+ remediation: buildInstallCommand(
267
+ paths,
268
+ options,
269
+ "install",
270
+ packageVersion(packageRoot),
271
+ ["--backup-unmanaged"],
272
+ ),
273
+ legacy_skill_detected: true,
274
+ legacy_markers: legacy,
275
+ skill_root: paths.target,
276
+ };
277
+ }
278
+ }
279
+
250
280
  try {
251
281
  if (hasCompletePackage) {
252
282
  expectedRelease = verifyPackage(packageRoot).release;
@@ -320,18 +350,70 @@ function install(options = {}, packageRoot = PACKAGE_ROOT) {
320
350
  return withInstallLock(paths.lock, () => installLocked(options, packageRoot, verified, paths));
321
351
  }
322
352
 
353
+ function detectLegacySkill(skillPath) {
354
+ if (!pathEntryExists(skillPath) || !fs.lstatSync(skillPath).isDirectory()) return null;
355
+ const markers = {
356
+ pythonWheel: fs.existsSync(path.join(skillPath, "assets", "wheels"))
357
+ && fs.readdirSync(path.join(skillPath, "assets", "wheels")).some(f => f.endsWith(".whl")),
358
+ runtimeJson: fs.existsSync(path.join(skillPath, "assets", "wheels", "runtime.json")),
359
+ bootstrapScript: fs.existsSync(path.join(skillPath, "scripts", "bootstrap.py")),
360
+ installScript: fs.existsSync(path.join(skillPath, "scripts", "install.py")),
361
+ oldLauncher: fs.existsSync(path.join(skillPath, "scripts", "data-platform-demo"))
362
+ || fs.existsSync(path.join(skillPath, "scripts", "ml-platform")),
363
+ };
364
+ const hasAnyMarker = Object.values(markers).some(Boolean);
365
+ return hasAnyMarker ? markers : null;
366
+ }
367
+
368
+ function backupUnmanagedSkill(skillPath, skillsDir, legacy) {
369
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
370
+ let suffix = 0;
371
+ let backupPath;
372
+ do {
373
+ const kind = legacy ? "legacy-backup" : "unmanaged-backup";
374
+ const backupName = `.${SKILL_NAME}.${kind}.${timestamp}${suffix ? `-${suffix}` : ""}`;
375
+ backupPath = path.join(skillsDir, backupName);
376
+ suffix += 1;
377
+ } while (pathEntryExists(backupPath));
378
+ fs.renameSync(skillPath, backupPath);
379
+ return backupPath;
380
+ }
381
+
323
382
  function installLocked(options, packageRoot, verified, paths) {
324
383
  const previousRecord = fs.existsSync(paths.record) ? fs.readFileSync(paths.record) : null;
325
384
  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}`);
385
+ const targetExists = pathEntryExists(paths.target);
386
+ const legacy = targetExists && !current ? detectLegacySkill(paths.target) : null;
387
+ const backingUpUnmanaged = Boolean(targetExists && !current && options.backupUnmanaged);
388
+
389
+ if (options.migrateLegacy && !legacy) {
390
+ throw new Error(`目标 Skill 不是旧 Python Skill 或不存在: ${paths.target}`);
391
+ }
392
+
393
+ if (targetExists && !current && !backingUpUnmanaged) {
394
+ if (legacy && !options.backupUnmanaged) {
395
+ const markers = Object.entries(legacy).filter(([_, v]) => v).map(([k]) => k).join(", ");
396
+ throw new Error(
397
+ `目标 Skill 包含旧 Python runtime 标记 (${markers}),拒绝覆盖: ${paths.target}\n` +
398
+ ` 迁移方法: 使用 --backup-unmanaged 或运行 'ml-platform migrate' 先备份旧 Skill`
399
+ );
400
+ } else if (fs.lstatSync(paths.target).isSymbolicLink()) {
401
+ throw new Error(
402
+ `目标 Skill 路径是未托管的符号链接,拒绝覆盖: ${paths.target}\n` +
403
+ " 处理方法: 确认后使用 --backup-unmanaged 备份该链接并安装正式 Skill"
404
+ );
405
+ } else if (!legacy) {
406
+ throw new Error(
407
+ `目标 Skill 不受 ${PACKAGE_NAME} 管理,拒绝覆盖: ${paths.target}\n` +
408
+ " 处理方法: 确认后使用 --backup-unmanaged 备份原目录"
409
+ );
410
+ }
329
411
  }
330
412
  const sharedRuntime = safeReadRuntimeRecord(paths.runtimeRecord);
331
- if (fs.existsSync(paths.installRoot) && !sharedRuntime) {
413
+ if (pathEntryExists(paths.installRoot) && !sharedRuntime) {
332
414
  throw new Error(`持久 CLI 目录不受 ${PACKAGE_NAME} 管理,拒绝覆盖: ${paths.installRoot}`);
333
415
  }
334
- if (fs.existsSync(paths.shim) && (!current || sha256File(paths.shim) !== current.shim.sha256)
416
+ if (pathEntryExists(paths.shim) && (!current || sha256File(paths.shim) !== current.shim.sha256)
335
417
  && (!sharedRuntime || !shimTargetsDispatcher(paths.shim, paths.dispatcher))) {
336
418
  throw new Error(`CLI shim 不受 ${PACKAGE_NAME} 管理,拒绝覆盖: ${paths.shim}`);
337
419
  }
@@ -360,7 +442,7 @@ function installLocked(options, packageRoot, verified, paths) {
360
442
  return installationResult("unchanged", paths, verified.release, current);
361
443
  }
362
444
 
363
- if ((targetExists || (sharedRuntime && !sharedRuntimeMatches)) && !options.upgrade) {
445
+ if (((targetExists && !backingUpUnmanaged) || (sharedRuntime && !sharedRuntimeMatches)) && !options.upgrade) {
364
446
  throw new Error("已存在不同版本或摘要;升级时请使用 upgrade 或 install --upgrade");
365
447
  }
366
448
  const installedVersion = current ? current.release_version : sharedRuntime && sharedRuntime.release_version;
@@ -383,6 +465,8 @@ function installLocked(options, packageRoot, verified, paths) {
383
465
  let hadState = false;
384
466
  let hadSkill = false;
385
467
  let hadShim = false;
468
+ let legacyBackupPath = null;
469
+ let legacyMoved = false;
386
470
  try {
387
471
  stagePersistentInstall(stagedInstall, packageRoot, verified);
388
472
  fs.cpSync(verified.skillRoot, stagedSkill, { recursive: true, errorOnExist: true });
@@ -391,19 +475,23 @@ function installLocked(options, packageRoot, verified, paths) {
391
475
  }
392
476
  writeShim(shimTemporary, paths.dispatcher);
393
477
 
394
- if (fs.existsSync(paths.installRoot)) {
478
+ if (backingUpUnmanaged) {
479
+ legacyBackupPath = backupUnmanagedSkill(paths.target, paths.skillsDir, Boolean(legacy));
480
+ legacyMoved = true;
481
+ }
482
+ if (pathEntryExists(paths.installRoot)) {
395
483
  fs.renameSync(paths.installRoot, stateBackup);
396
484
  hadState = true;
397
485
  }
398
486
  fs.renameSync(stagedInstall, paths.installRoot);
399
487
  stateMoved = true;
400
- if (fs.existsSync(paths.target)) {
488
+ if (pathEntryExists(paths.target)) {
401
489
  fs.renameSync(paths.target, skillBackup);
402
490
  hadSkill = true;
403
491
  }
404
492
  fs.renameSync(stagedSkill, paths.target);
405
493
  skillMoved = true;
406
- if (fs.existsSync(paths.shim)) {
494
+ if (pathEntryExists(paths.shim)) {
407
495
  fs.renameSync(paths.shim, shimBackup);
408
496
  hadShim = true;
409
497
  }
@@ -419,19 +507,32 @@ function installLocked(options, packageRoot, verified, paths) {
419
507
  if (hadState) removeOwnedDirectory(stateBackup, paths.stateDir, ".backup.");
420
508
  if (hadSkill) removeOwnedDirectory(skillBackup, paths.skillsDir, `.${SKILL_NAME}.backup.`);
421
509
  if (hadShim) fs.rmSync(shimBackup, { force: true });
422
- return installationResult(current || targetExists ? "upgraded" : "installed", paths, verified.release, record);
510
+ const result = installationResult(
511
+ options.migrateLegacy ? "migrated" : (current || (targetExists && !backingUpUnmanaged) ? "upgraded" : "installed"),
512
+ paths,
513
+ verified.release,
514
+ record,
515
+ );
516
+ if (legacyBackupPath) {
517
+ if (legacy) result.legacy_skill_backup = legacyBackupPath;
518
+ else result.unmanaged_skill_backup = legacyBackupPath;
519
+ }
520
+ return result;
423
521
  } 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);
522
+ if (shimMoved && pathEntryExists(paths.shim)) fs.rmSync(paths.shim, { force: true });
523
+ if (hadShim && pathEntryExists(shimBackup)) fs.renameSync(shimBackup, paths.shim);
524
+ if (skillMoved && pathEntryExists(paths.target)) removeOwnedDirectory(paths.target, paths.skillsDir, SKILL_NAME);
525
+ if (hadSkill && pathEntryExists(skillBackup)) fs.renameSync(skillBackup, paths.target);
526
+ if (legacyMoved && legacyBackupPath && pathEntryExists(legacyBackupPath) && !pathEntryExists(paths.target)) {
527
+ fs.renameSync(legacyBackupPath, paths.target);
528
+ }
529
+ if (stateMoved && pathEntryExists(paths.installRoot)) removeOwnedDirectory(paths.installRoot, paths.stateDir, "current");
530
+ if (hadState && pathEntryExists(stateBackup)) fs.renameSync(stateBackup, paths.installRoot);
430
531
  if (previousRecord === null) fs.rmSync(paths.record, { force: true });
431
532
  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.`);
533
+ if (pathEntryExists(shimTemporary)) fs.rmSync(shimTemporary, { force: true });
534
+ if (pathEntryExists(stateStaging)) removeOwnedDirectory(stateStaging, paths.stateDir, ".install.");
535
+ if (pathEntryExists(skillStaging)) removeOwnedDirectory(skillStaging, paths.skillsDir, `.${SKILL_NAME}.install.`);
435
536
  throw error;
436
537
  }
437
538
  }
@@ -442,6 +543,7 @@ function stagePersistentInstall(target, packageRoot, verified) {
442
543
  for (const name of ["main.js", "lib.js"]) {
443
544
  fs.copyFileSync(path.join(packageRoot, "scripts", name), path.join(target, "scripts", name));
444
545
  }
546
+ fs.copyFileSync(path.join(packageRoot, "package.json"), path.join(target, "package.json"));
445
547
  fs.chmodSync(path.join(target, "scripts", "main.js"), 0o755);
446
548
  fs.cpSync(verified.clientRoot, path.join(target, "runtime", "business-client"), {
447
549
  recursive: true,
@@ -618,6 +720,16 @@ function digestEquals(root, expected) {
618
720
  catch (_) { return false; }
619
721
  }
620
722
 
723
+ function pathEntryExists(entry) {
724
+ try {
725
+ fs.lstatSync(entry);
726
+ return true;
727
+ } catch (error) {
728
+ if (error.code === "ENOENT") return false;
729
+ throw error;
730
+ }
731
+ }
732
+
621
733
  function withInstallLock(lockPath, callback) {
622
734
  let descriptor;
623
735
  try {
@@ -782,34 +894,101 @@ function isSemver(value) {
782
894
  return typeof value === "string" && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(value);
783
895
  }
784
896
 
897
+ function parseSemver(value) {
898
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/.exec(value);
899
+ if (!match) throw new Error("无法比较无效 SemVer");
900
+ return {
901
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
902
+ prerelease: match[4] ? match[4].split(".") : null,
903
+ };
904
+ }
905
+
785
906
  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);
907
+ const aParsed = parseSemver(left);
908
+ const bParsed = parseSemver(right);
909
+ const a = aParsed.core;
910
+ const b = bParsed.core;
789
911
  for (let index = 0; index < 3; index += 1) {
790
912
  if (a[index] !== b[index]) return a[index] < b[index] ? -1 : 1;
791
913
  }
914
+ if (!aParsed.prerelease && !bParsed.prerelease) return 0;
915
+ if (!aParsed.prerelease) return 1;
916
+ if (!bParsed.prerelease) return -1;
917
+ const length = Math.max(aParsed.prerelease.length, bParsed.prerelease.length);
918
+ for (let index = 0; index < length; index += 1) {
919
+ if (index >= aParsed.prerelease.length) return -1;
920
+ if (index >= bParsed.prerelease.length) return 1;
921
+ const leftPart = aParsed.prerelease[index];
922
+ const rightPart = bParsed.prerelease[index];
923
+ if (leftPart === rightPart) continue;
924
+ const leftNumeric = /^\d+$/.test(leftPart);
925
+ const rightNumeric = /^\d+$/.test(rightPart);
926
+ if (leftNumeric && rightNumeric) return Number(leftPart) < Number(rightPart) ? -1 : 1;
927
+ if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
928
+ return leftPart < rightPart ? -1 : 1;
929
+ }
792
930
  return 0;
793
931
  }
794
932
 
795
933
  function versionSatisfies(version, range) {
796
- const match = /^(>=\d+\.\d+\.\d+)\s+(<\d+\.\d+\.\d+)$/.exec(range);
934
+ const match = /^>=(\S+)\s+<(\S+)$/.exec(range);
797
935
  if (!match) return false;
798
- return compareSemver(version, match[1].slice(2)) >= 0 && compareSemver(version, match[2].slice(1)) < 0;
936
+ return compareSemver(version, match[1]) >= 0 && compareSemver(version, match[2]) < 0;
937
+ }
938
+
939
+ function migrate(options = {}, packageRoot = PACKAGE_ROOT) {
940
+ return install({ ...options, backupUnmanaged: true, migrateLegacy: true }, packageRoot);
941
+ }
942
+
943
+ function packageVersion(packageRoot = PACKAGE_ROOT) {
944
+ try {
945
+ const release = readJson(path.join(packageRoot, "release.json"), "release manifest");
946
+ if (isSemver(release.release_version)) return release.release_version;
947
+ } catch (_) { /* source checkout may not have a generated release */ }
948
+ try {
949
+ const packageConfig = readJson(path.join(packageRoot, "package.json"), "package.json");
950
+ if (isSemver(packageConfig.version)) return packageConfig.version;
951
+ } catch (_) { /* use the package fallback */ }
952
+ return PACKAGE_VERSION;
953
+ }
954
+
955
+ function shellQuote(value) {
956
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
957
+ }
958
+
959
+ function buildInstallCommand(paths, options, command, version, extraArgs = []) {
960
+ const args = [
961
+ "npm exec --yes --registry=https://registry.npmjs.org/",
962
+ `--package=${PACKAGE_NAME}@${version}`,
963
+ "--",
964
+ "ml-platform",
965
+ command,
966
+ "--agent", paths.agent,
967
+ "--scope", paths.scope,
968
+ ];
969
+ if (paths.projectDir) args.push("--project-dir", paths.projectDir);
970
+ if (options.skillsDir) args.push("--skills-dir", paths.skillsDir);
971
+ if (options.stateDir) args.push("--state-dir", paths.stateDir);
972
+ if (options.binDir) args.push("--bin-dir", paths.binDir);
973
+ args.push(...extraArgs);
974
+ return args.map((arg, index) => index < 2 ? arg : shellQuote(arg)).join(" ");
799
975
  }
800
976
 
801
977
  module.exports = {
802
978
  CHECKSUM_SCHEMA,
803
979
  CLI_NAME,
804
980
  PACKAGE_NAME,
981
+ PACKAGE_VERSION,
805
982
  RECORD_SCHEMA,
806
983
  RELEASE_SCHEMA,
807
984
  SKILL_NAME,
808
985
  compareSemver,
986
+ detectLegacySkill,
809
987
  doctor,
810
988
  install,
811
989
  listFiles,
812
990
  loadRelease,
991
+ migrate,
813
992
  parseOptions,
814
993
  resolveInstallPaths,
815
994
  sha256File,
package/scripts/main.js CHANGED
@@ -7,23 +7,25 @@ const { spawnSync } = require("child_process");
7
7
  const {
8
8
  doctor,
9
9
  install,
10
+ migrate,
10
11
  parseOptions,
11
12
  status,
12
13
  } = require("./lib");
13
14
 
14
- const MANAGEMENT_COMMANDS = new Set(["install", "upgrade", "status", "doctor"]);
15
+ const MANAGEMENT_COMMANDS = new Set(["install", "upgrade", "migrate", "status", "doctor"]);
15
16
  const BUSINESS_ENTRY = path.resolve(__dirname, "..", "runtime", "business-client", "src", "cli.js");
16
17
 
17
18
  function usage() {
18
19
  return `用法:
19
- ml-platform install [--agent codex|pi|custom] [--scope user|project] [--skills-dir PATH] [--project-dir PATH] [--state-dir PATH] [--bin-dir PATH] [--upgrade] [--allow-downgrade]
20
+ ml-platform install [--agent codex|pi|custom] [--scope user|project] [--skills-dir PATH] [--project-dir PATH] [--state-dir PATH] [--bin-dir PATH] [--upgrade] [--allow-downgrade] [--backup-unmanaged]
20
21
  ml-platform upgrade [install options]
22
+ ml-platform migrate [--agent codex|pi|custom] [--scope user|project] [--skills-dir PATH] [--project-dir PATH]
21
23
  ml-platform status [--agent codex|pi|custom] [--scope user|project] [--skills-dir PATH] [--project-dir PATH] [--state-dir PATH] [--bin-dir PATH]
22
24
  ml-platform doctor [status options] [--api-url URL]
23
25
  ml-platform <business-command> [args]
24
26
 
25
27
  安装管理命令:
26
- install, upgrade, status, doctor
28
+ install, upgrade, migrate, status, doctor
27
29
 
28
30
  其余已注册业务命令由同一份 JavaScript Business CLI 执行。
29
31
  `;
@@ -70,6 +72,11 @@ function main(argv) {
70
72
  printJson(install(options));
71
73
  return 0;
72
74
  }
75
+ if (command === "migrate") {
76
+ const result = migrate(options);
77
+ printJson(result);
78
+ return result.ok ? 0 : 1;
79
+ }
73
80
  if (command === "status") {
74
81
  const result = status(options);
75
82
  printJson(result);