@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 +2 -2
- package/README.md +43 -70
- package/package.json +1 -1
- package/release.json +3 -3
- package/scripts/lib.js +205 -26
- package/scripts/main.js +10 -3
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.
|
|
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
|
-
|
|
4
|
-
|
|
3
|
+
ML Platform 的 JavaScript CLI 和 `feature-management` Agent Skill。运行时只需要 Node.js 18+,
|
|
4
|
+
不需要 Python 或 `uv`。
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
## 安装
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
package: @zhuoyuezs/ml-platform
|
|
10
|
-
bin: ml-platform
|
|
11
|
-
```
|
|
8
|
+
在 Codex 项目根目录运行:
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
51
|
-
Skill,与 CLI 并列。
|
|
35
|
+
## 迁移旧 Skill
|
|
52
36
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
参考飞书 CLI,正式入口使用显式版本化 npx 命令:
|
|
37
|
+
如果目标位置已有旧 Python Skill、实验 symlink 或其他未托管 Skill,确认后显式备份并安装:
|
|
56
38
|
|
|
57
39
|
```bash
|
|
58
|
-
npx
|
|
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
|
|
44
|
+
--project-dir "$PWD" \
|
|
45
|
+
--backup-unmanaged
|
|
62
46
|
```
|
|
63
47
|
|
|
64
|
-
|
|
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
|
|
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
|
-
|
|
61
|
+
查看完整命令:
|
|
83
62
|
|
|
84
|
-
```
|
|
85
|
-
|
|
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
|
-
|
|
93
|
-
oracle,不进入 npm release。
|
|
67
|
+
## 包内容
|
|
94
68
|
|
|
95
|
-
|
|
69
|
+
- 一份 JavaScript business CLI;
|
|
70
|
+
- 一份纯 `feature-management` Skill;
|
|
71
|
+
- 显式安装、升级、迁移、状态、校验和回滚逻辑。
|
|
96
72
|
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
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.
|
|
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.
|
|
37
|
+
"revision": "0.1.1",
|
|
38
38
|
"sha256": "sha256:f7c2ba836338602f6f01ebec18b595a366f5dd1bcbfcb58e249cda5c10bd61dd"
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
|
-
"source_commit": "
|
|
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
|
|
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
|
-
|
|
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 =
|
|
327
|
-
|
|
328
|
-
|
|
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 (
|
|
413
|
+
if (pathEntryExists(paths.installRoot) && !sharedRuntime) {
|
|
332
414
|
throw new Error(`持久 CLI 目录不受 ${PACKAGE_NAME} 管理,拒绝覆盖: ${paths.installRoot}`);
|
|
333
415
|
}
|
|
334
|
-
if (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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 &&
|
|
425
|
-
if (hadShim &&
|
|
426
|
-
if (skillMoved &&
|
|
427
|
-
if (hadSkill &&
|
|
428
|
-
if (
|
|
429
|
-
|
|
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 (
|
|
433
|
-
if (
|
|
434
|
-
if (
|
|
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
|
-
|
|
787
|
-
const
|
|
788
|
-
const
|
|
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 =
|
|
934
|
+
const match = /^>=(\S+)\s+<(\S+)$/.exec(range);
|
|
797
935
|
if (!match) return false;
|
|
798
|
-
return compareSemver(version, match[1]
|
|
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);
|