@lark-apaas/miaoda-cli 0.1.24-alpha.800d445 → 0.1.24-alpha.b526562
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/dist/cli/commands/app/index.js +2 -2
- package/dist/cli/commands/deploy/modern.js +0 -13
- package/dist/cli/commands/skills/index.js +21 -1
- package/dist/cli/handlers/deploy/modern.js +0 -1
- package/dist/cli/handlers/skills/sync.js +95 -0
- package/dist/services/app/init/template.js +0 -4
- package/dist/services/deploy/modern/atoms/index.js +1 -5
- package/dist/services/deploy/modern/pipelines/design-local.js +8 -9
- package/dist/services/deploy/modern/pipelines/local.js +12 -13
- package/dist/utils/coding-steering.js +12 -0
- package/dist/utils/file-ops.js +78 -0
- package/dist/utils/npm-pack.js +33 -0
- package/dist/utils/sandbox-skills.js +268 -0
- package/package.json +1 -1
- package/dist/services/deploy/modern/atoms/release-tracker.js +0 -43
|
@@ -217,7 +217,7 @@ function registerAppInit(parent) {
|
|
|
217
217
|
.command('init')
|
|
218
218
|
.description('初始化应用代码:抓 template 渲染、同步 upgrade/templates、装 .agent/steering/ skills、写 .spark/meta.json。.spark/meta.json 已存在则直接退出')
|
|
219
219
|
.option('--template <stack>', `技术栈短名(${index_1.SUPPORTED_STACKS.join(' / ')})`)
|
|
220
|
-
.option('--app-type <type>', 'aPaaS 业务类型(html / modern_html /
|
|
220
|
+
.option('--app-type <type>', 'aPaaS 业务类型(html / modern_html / jspage / full_stack);未传 --template 时用于解析技术栈')
|
|
221
221
|
.option('--source-path <path>', '导入源目录:把该目录内容聚合进脚手架 src/(当前仅 --app-type modern_html 生效)')
|
|
222
222
|
.option('--conf [json]', 'init 配置 JSON。支持 {"version": "<template 版本>"},默认 latest')
|
|
223
223
|
.option('--skip-install', '跳过依赖安装', false)
|
|
@@ -237,7 +237,7 @@ function registerAppInit(parent) {
|
|
|
237
237
|
|
|
238
238
|
技术栈解析(--template / --app-type)
|
|
239
239
|
--template 传了 → 直接用(vite-react / html / design-html / nestjs-react-fullstack)
|
|
240
|
-
否则 --app-type 传了 → 查表:html→design-html, modern_html→html,
|
|
240
|
+
否则 --app-type 传了 → 查表:html→design-html, modern_html→html, jspage→vite-react, full_stack→nestjs-react-fullstack
|
|
241
241
|
两者都没传 → 报错 ARGS_INVALID
|
|
242
242
|
|
|
243
243
|
源码导入(--source-path)
|
|
@@ -17,7 +17,6 @@ function registerDeployCommandsModern(program) {
|
|
|
17
17
|
.description('触发 modern 应用发布:本地构建 + 本地部署(preRelease + localPublish)')
|
|
18
18
|
.option('--dir <path>', '项目目录', '.')
|
|
19
19
|
.option('--skip-build', '跳过 build 步骤(已构建好时使用)', false)
|
|
20
|
-
.option('--skip-release', '不创建/更新本地发布单,发布单状态由外部托管', false)
|
|
21
20
|
.option('--conf <json>', '关联元信息 JSON,仅识别 checkPointVersion / commitID 两字段')
|
|
22
21
|
.addHelpText('after', `
|
|
23
22
|
不要用异步模式或后台模式调用 deploy,否则调用可能提前结束,Agent 会误判发布已完成。
|
|
@@ -35,15 +34,6 @@ function registerDeployCommandsModern(program) {
|
|
|
35
34
|
6. savePluginInstances(扫描 dist/output_capabilities/*.json 批量注册)
|
|
36
35
|
7. finalizeLocalRelease(Finished|Failed)
|
|
37
36
|
|
|
38
|
-
--skip-release(发布单外部托管)
|
|
39
|
-
跳过第 4、7 步(createLocalRelease / updateLocalRelease),其余步骤不变:
|
|
40
|
-
preRelease 仍照常签发 version,构建、上传、capability 注册照常执行。
|
|
41
|
-
外部据 stdout 返回的 version / preReleaseID 自行建单,并据本命令的
|
|
42
|
-
退出码决定把发布单翻成 Finished 还是 Failed —— 失败态由外部负责,CLI 不再兜底。
|
|
43
|
-
该模式下 releaseID / url 输出为 null(CLI 未建单,无从得知)。
|
|
44
|
-
注意:--conf 的 checkPointVersion / commitID 唯一消费者是 createLocalRelease,
|
|
45
|
-
本模式下不生效(静默忽略,不报错),需要关联请在外部建单时自行传递。
|
|
46
|
-
|
|
47
37
|
--conf(关联元信息透传)
|
|
48
38
|
值为 JSON string,只识别两个字段,其余 key 忽略:
|
|
49
39
|
checkPointVersion 关联的 checkpoint 版本
|
|
@@ -52,13 +42,11 @@ function registerDeployCommandsModern(program) {
|
|
|
52
42
|
|
|
53
43
|
JSON 输出(stdout)
|
|
54
44
|
{"data": {"appId": "...", "version": <n>, "url": "...", "releaseID": "...", "preReleaseID": "..."}}
|
|
55
|
-
--skip-release 时 url 与 releaseID 为 null,字段本身仍在。
|
|
56
45
|
|
|
57
46
|
示例
|
|
58
47
|
$ miaoda deploy
|
|
59
48
|
$ miaoda deploy --dir ./my-app
|
|
60
49
|
$ miaoda deploy --skip-build
|
|
61
|
-
$ miaoda deploy --skip-release
|
|
62
50
|
$ miaoda deploy --conf '{"checkPointVersion":"v3","commitID":"a1b2c3d"}'
|
|
63
51
|
`);
|
|
64
52
|
deployCmd.action((0, shared_1.withHelp)(deployCmd, async (rawOpts) => {
|
|
@@ -66,7 +54,6 @@ JSON 输出(stdout)
|
|
|
66
54
|
dir: rawOpts.dir ?? '.',
|
|
67
55
|
appId: (0, shared_1.resolveAppId)({}),
|
|
68
56
|
skipBuild: rawOpts.skipBuild,
|
|
69
|
-
skipRelease: rawOpts.skipRelease,
|
|
70
57
|
conf: rawOpts.conf,
|
|
71
58
|
});
|
|
72
59
|
}));
|
|
@@ -21,11 +21,16 @@ function registerSkillsCommands(program) {
|
|
|
21
21
|
function registerSkillsSync(parent) {
|
|
22
22
|
const cmd = parent
|
|
23
23
|
.command('sync')
|
|
24
|
-
.description('同步 coding-steering
|
|
24
|
+
.description('同步 skills 到 user app(默认 coding-steering;带 --type 时走 sandbox-skills)')
|
|
25
25
|
.option('--dir <path>', '项目目录,默认当前目录', '.')
|
|
26
26
|
.option('--version <ver>', 'coding-steering 包版本或 dist-tag,缺省 latest')
|
|
27
27
|
.option('--local', '走本地 dev / agent 输出形态:拷到 .agents/skills 平铺 + 创建 .claude/skills 软链。' +
|
|
28
28
|
'不传时退回老形态:拷到 .agent/skills/steering/<stack>/skills/(跟沙箱端 update-skills.sh 对齐)', false)
|
|
29
|
+
.option('--type <type>', '走 sandbox 分支:miaoda | miaoda-modern | miaoda-design | openclaw。有值时忽略 --local,' +
|
|
30
|
+
'从 @lark-apaas/coding-<pkg>-sandbox-skills 拉包平铺写入,并额外同步 steering ' +
|
|
31
|
+
'到 .agent/skills/steering/<stack>/(--version 对后者仍生效)')
|
|
32
|
+
.option('--tgz-key <key>', '配合 --type 使用;老 TCC skillsUpdateTgzKey 值透传,' +
|
|
33
|
+
'从 skills_versions/all_<X>.tgz 反推出 lane 名 X,再过滤出 -alpha.<X> 版本;缺省走 @latest')
|
|
29
34
|
.addHelpText('after', `
|
|
30
35
|
前置要求
|
|
31
36
|
当前目录(或 --dir)已走过 'miaoda app init'(.spark/meta.json 含 stack)
|
|
@@ -41,21 +46,36 @@ function registerSkillsSync(parent) {
|
|
|
41
46
|
.agents/tech.md
|
|
42
47
|
.claude/skills → ../.agents/skills(软链,让 Claude Code 识别同一份)
|
|
43
48
|
|
|
49
|
+
传 --type(沙箱链路,两部分都同步):
|
|
50
|
+
.agent/skills/<skill>/... ← 业务线 skill,平铺
|
|
51
|
+
.agent/skills/steering/<stack>/skills/<skill>/ ← steering,同上面默认形态
|
|
52
|
+
(openclaw 的业务线部分落 skills/ 而非 .agent/skills/)
|
|
53
|
+
|
|
44
54
|
JSON 输出
|
|
45
55
|
{"data": {"stack": "...", "version": "...", "syncedSkills": [...], "techSynced": true|false,
|
|
46
56
|
"claudeSkillsLink": "created|updated|noop|conflict" // 仅 --local 时}}
|
|
47
57
|
|
|
58
|
+
传 --type 时:
|
|
59
|
+
{"data": {"mode": "sandbox", "type": "...", "packageName": "...", "version": "...",
|
|
60
|
+
"lane": "...", "syncedTop": [...], "targetDir": "...",
|
|
61
|
+
"steering": {"status": "synced|skipped|failed", ...}}}
|
|
62
|
+
steering 失败或无 stack 不影响整体成功——业务线 skill 已同步,不该因增量部分阻断
|
|
63
|
+
|
|
48
64
|
示例
|
|
49
65
|
$ miaoda skills sync # 拉 @latest
|
|
50
66
|
$ miaoda skills sync --local # 本地 dev / agent 用
|
|
51
67
|
$ miaoda skills sync --version 0.2.0 # 显式指定版本
|
|
52
68
|
$ miaoda skills sync --dir ./my-app
|
|
69
|
+
# 沙箱链路(由 feida-ai 下发):业务线 skill + steering 一次同步到位
|
|
70
|
+
$ miaoda skills sync --type miaoda-modern --tgz-key 'skills_versions/all_demo-lane.tgz'
|
|
53
71
|
`);
|
|
54
72
|
cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
|
|
55
73
|
await (0, index_1.handleSkillsSync)({
|
|
56
74
|
dir: rawOpts.dir,
|
|
57
75
|
version: rawOpts.version,
|
|
58
76
|
local: rawOpts.local,
|
|
77
|
+
type: rawOpts.type,
|
|
78
|
+
tgzKey: rawOpts.tgzKey,
|
|
59
79
|
});
|
|
60
80
|
}));
|
|
61
81
|
}
|
|
@@ -6,9 +6,62 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.handleSkillsSync = handleSkillsSync;
|
|
7
7
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
8
|
const coding_steering_1 = require("../../../utils/coding-steering");
|
|
9
|
+
const sandbox_skills_1 = require("../../../utils/sandbox-skills");
|
|
10
|
+
const file_ops_1 = require("../../../utils/file-ops");
|
|
9
11
|
const spark_meta_1 = require("../../../utils/spark-meta");
|
|
10
12
|
const error_1 = require("../../../utils/error");
|
|
11
13
|
const output_1 = require("../../../utils/output");
|
|
14
|
+
/**
|
|
15
|
+
* sandbox 分支下补同步 steering(`.agent/skills/steering/<stack>/`)。
|
|
16
|
+
*
|
|
17
|
+
* **绝不抛错**,因为它在 sandbox 链路里是「附加项」而非门禁:
|
|
18
|
+
* - 无 `.spark/meta.json` / 无 stack → skipped。对齐老 cp-skills.sh 的
|
|
19
|
+
* `[ -n "$STACK" ] && [ "$STACK" != "null" ]` 判断(无 stack 就跳过 steering)。
|
|
20
|
+
* - 拉包 / 写盘失败 → failed,但仍让整条 sync 算成功。
|
|
21
|
+
*
|
|
22
|
+
* 为什么不能抛:feida-ai 侧 TCC 可配 `allowFailure: false`,一抛就阻断 agent 启动。
|
|
23
|
+
* 平铺业务线 skill 已经同步好了,不该因为 steering 这个增量部分让用户开不了会话。
|
|
24
|
+
*/
|
|
25
|
+
function syncSteeringForSandbox(params) {
|
|
26
|
+
let stack;
|
|
27
|
+
try {
|
|
28
|
+
const meta = (0, spark_meta_1.readSparkMeta)(params.targetDir);
|
|
29
|
+
if (meta.stack === undefined || meta.stack === '' || meta.stack === 'null') {
|
|
30
|
+
return { status: 'skipped', reason: '.spark/meta.json missing stack' };
|
|
31
|
+
}
|
|
32
|
+
stack = meta.stack;
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
return { status: 'skipped', reason: `read .spark/meta.json failed: ${err.message}` };
|
|
36
|
+
}
|
|
37
|
+
// 版本口径与下方本地分支保持一致:--version > BOE 的 dist-tag dev > latest
|
|
38
|
+
const isBoe = process.env.FORCE_FRAMEWORK_ENVIRONMENT === 'boe';
|
|
39
|
+
const effectiveVersion = params.version ?? (isBoe ? 'dev' : undefined);
|
|
40
|
+
try {
|
|
41
|
+
const r = (0, coding_steering_1.syncCodingSteering)({
|
|
42
|
+
stack,
|
|
43
|
+
targetDir: params.targetDir,
|
|
44
|
+
version: effectiveVersion,
|
|
45
|
+
// 必须显式传 mode,不能落到 inferMode():它靠 env MIAODA_DEP_CACHE_DIR 嗅探,
|
|
46
|
+
// 而 mode 决定读包内哪个 layer(sandbox→`skills/`,local→`skills_local/`)。
|
|
47
|
+
// `--type` 分支本身就是沙箱语义,嗅探失手会去读不存在的 skills_local/、
|
|
48
|
+
// 静默同步 0 个 skill。
|
|
49
|
+
mode: 'sandbox',
|
|
50
|
+
// 同理固定 nested:落 .agent/skills/steering/<stack>/,与老链路一致
|
|
51
|
+
outputLayout: 'nested',
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
status: 'synced',
|
|
55
|
+
stack,
|
|
56
|
+
version: r.version,
|
|
57
|
+
syncedSkills: r.syncedSkills,
|
|
58
|
+
techSynced: r.techSynced,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
return { status: 'failed', reason: err.message };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
12
65
|
/**
|
|
13
66
|
* miaoda skills sync [--dir <path>] [--version <ver>]
|
|
14
67
|
*
|
|
@@ -26,6 +79,48 @@ const output_1 = require("../../../utils/output");
|
|
|
26
79
|
async function handleSkillsSync(opts) {
|
|
27
80
|
await Promise.resolve();
|
|
28
81
|
const targetDir = node_path_1.default.resolve(opts.dir ?? process.cwd());
|
|
82
|
+
// --type 走 sandbox-skills 分支(迁仓后沙箱内由 feida-ai 直接下发调用)。
|
|
83
|
+
//
|
|
84
|
+
// 这条分支必须**同时**同步两部分,缺一不可 —— 线上老链路 update-skills.sh 也是两步
|
|
85
|
+
// (自己 rsync 平铺,再于第 230 行调不带 --type 的 `miaoda skills sync` 写 steering):
|
|
86
|
+
// ① syncSandboxSkills:平铺业务线 skill,rsync 式对齐(保 .keep/.config/steering)
|
|
87
|
+
// ② steering:`.agent/skills/steering/<stack>/`,按 .spark/meta.json 的 stack 过滤
|
|
88
|
+
// ③ lockDirReadonly:锁只读收尾(update-skills.sh:241)
|
|
89
|
+
//
|
|
90
|
+
// 历史 bug:早期这里 ① 完就 return,steering 既不同步、又被当时的整目录清空删掉,
|
|
91
|
+
// 沙箱少掉整批技术栈 skill(BOE 实测 scanAllSkills 从 59 掉到 34)。
|
|
92
|
+
//
|
|
93
|
+
// `.keep` 语义(update-skills.sh:149):目标目录挂了 .keep 表示「我手改过,别覆盖」,
|
|
94
|
+
// ① 会跳过并回报 skippedByKeepFile,此时 ② 也必须跳过 —— 否则 steering 仍被覆写,
|
|
95
|
+
// 冻结只冻结了一半。
|
|
96
|
+
if (opts.type !== undefined && opts.type !== '') {
|
|
97
|
+
const { type, tgzKey } = (0, sandbox_skills_1.validateSandboxSyncOpts)({ type: opts.type, tgzKey: opts.tgzKey });
|
|
98
|
+
const result = (0, sandbox_skills_1.syncSandboxSkills)({ type, tgzKey, targetDir });
|
|
99
|
+
const steering = result.skippedByKeepFile
|
|
100
|
+
? { status: 'skipped', reason: '.keep file found, sync frozen' }
|
|
101
|
+
: syncSteeringForSandbox({ targetDir, version: opts.version });
|
|
102
|
+
// ③ 收尾锁只读(update-skills.sh:241 的 chmod -R a=rX)。必须在 ①② 全部写完之后,
|
|
103
|
+
// 锁上就写不进去了。`.keep` 冻结时不锁——老链路那条路径是 exit 0 直接走、
|
|
104
|
+
// 末尾的 chmod 压根不执行,保持一致。
|
|
105
|
+
const lockedReadonly = result.skippedByKeepFile ? false : (0, file_ops_1.lockDirReadonly)(result.targetDir);
|
|
106
|
+
(0, output_1.emit)({
|
|
107
|
+
data: {
|
|
108
|
+
mode: 'sandbox',
|
|
109
|
+
type,
|
|
110
|
+
packageName: result.packageName,
|
|
111
|
+
version: result.version,
|
|
112
|
+
lane: result.lane,
|
|
113
|
+
syncedTop: result.syncedTop,
|
|
114
|
+
deleted: result.deleted,
|
|
115
|
+
skippedByKeepFile: result.skippedByKeepFile,
|
|
116
|
+
lockedReadonly,
|
|
117
|
+
targetDir: result.targetDir,
|
|
118
|
+
steering,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// 原有 coding-steering 同步逻辑(本地 dev / miaoda app init 用)
|
|
29
124
|
const meta = (0, spark_meta_1.readSparkMeta)(targetDir);
|
|
30
125
|
if (meta.stack === undefined || meta.stack === '') {
|
|
31
126
|
throw new error_1.AppError('SKILLS_META_INCOMPLETE', '.spark/meta.json missing stack — run `miaoda app init` first');
|
|
@@ -37,15 +37,11 @@ exports.LOCAL_STEERING_STACKS = new Set(['nestjs-react-fullstack', 'vite-react']
|
|
|
37
37
|
/**
|
|
38
38
|
* app_type(业务类型)→ stack 短名映射。仅在调用方未显式传 --template 时用于解析 stack。
|
|
39
39
|
* 与 src/api/app/types.ts 的 AppType 枚举无关,是独立的业务类型概念。
|
|
40
|
-
*
|
|
41
|
-
* 注:`frontend` 是 lark-cli 对外的纯前端应用类型;`jspage` 是内部历史代号,二者同映射
|
|
42
|
-
* 到 vite-react。保留 jspage 键以兼容内部既有调用方(豆包 / 妙搭链路),不删除。
|
|
43
40
|
*/
|
|
44
41
|
exports.STACK_BY_APP_TYPE = {
|
|
45
42
|
html: 'design-html',
|
|
46
43
|
modern_html: 'html',
|
|
47
44
|
jspage: 'vite-react',
|
|
48
|
-
frontend: 'vite-react',
|
|
49
45
|
full_stack: 'nestjs-react-fullstack',
|
|
50
46
|
};
|
|
51
47
|
exports.APP_TYPES = Object.keys(exports.STACK_BY_APP_TYPE);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.savePluginInstances = exports.
|
|
3
|
+
exports.savePluginInstances = exports.LocalReleaseStatus = exports.finalizeLocalRelease = exports.createLocalRelease = exports.uploadArtifacts = exports.uploadDesignArtifacts = exports.cleanDesignDist = exports.buildDesignOutput = exports.runBuild = exports.preRelease = exports.prepareDeployContext = void 0;
|
|
4
4
|
var context_1 = require("./context");
|
|
5
5
|
Object.defineProperty(exports, "prepareDeployContext", { enumerable: true, get: function () { return context_1.prepareDeployContext; } });
|
|
6
6
|
var pre_release_1 = require("./pre-release");
|
|
@@ -18,9 +18,5 @@ var local_release_1 = require("./local-release");
|
|
|
18
18
|
Object.defineProperty(exports, "createLocalRelease", { enumerable: true, get: function () { return local_release_1.createLocalRelease; } });
|
|
19
19
|
Object.defineProperty(exports, "finalizeLocalRelease", { enumerable: true, get: function () { return local_release_1.finalizeLocalRelease; } });
|
|
20
20
|
Object.defineProperty(exports, "LocalReleaseStatus", { enumerable: true, get: function () { return local_release_1.LocalReleaseStatus; } });
|
|
21
|
-
var release_tracker_1 = require("./release-tracker");
|
|
22
|
-
Object.defineProperty(exports, "NOOP_RELEASE_TRACKER", { enumerable: true, get: function () { return release_tracker_1.NOOP_RELEASE_TRACKER; } });
|
|
23
|
-
Object.defineProperty(exports, "createManagedReleaseTracker", { enumerable: true, get: function () { return release_tracker_1.createManagedReleaseTracker; } });
|
|
24
|
-
Object.defineProperty(exports, "resolveReleaseTracker", { enumerable: true, get: function () { return release_tracker_1.resolveReleaseTracker; } });
|
|
25
21
|
var save_plugin_instances_1 = require("./save-plugin-instances");
|
|
26
22
|
Object.defineProperty(exports, "savePluginInstances", { enumerable: true, get: function () { return save_plugin_instances_1.savePluginInstances; } });
|
|
@@ -24,26 +24,25 @@ async function designLocalPublishPipeline(opts) {
|
|
|
24
24
|
if (!opts.skipBuild) {
|
|
25
25
|
(0, index_1.buildDesignOutput)({ projectDir: ctx.projectDir });
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
extra: { checkPointVersion: opts.checkPointVersion, commitID: opts.commitID },
|
|
27
|
+
(0, logger_1.log)('deploy', 'Creating local release...');
|
|
28
|
+
const release = await (0, index_1.createLocalRelease)(ctx.appId, pre.version, {
|
|
29
|
+
checkPointVersion: opts.checkPointVersion,
|
|
30
|
+
commitID: opts.commitID,
|
|
32
31
|
});
|
|
33
32
|
try {
|
|
34
33
|
await (0, index_1.uploadDesignArtifacts)({ projectDir: ctx.projectDir, data });
|
|
35
|
-
await
|
|
34
|
+
await (0, index_1.finalizeLocalRelease)(ctx.appId, release.releaseID, index_1.LocalReleaseStatus.Finished);
|
|
36
35
|
}
|
|
37
36
|
catch (err) {
|
|
38
|
-
await
|
|
37
|
+
await (0, index_1.finalizeLocalRelease)(ctx.appId, release.releaseID, index_1.LocalReleaseStatus.Failed);
|
|
39
38
|
throw err;
|
|
40
39
|
}
|
|
41
40
|
(0, logger_1.log)('deploy', 'Deployed successfully');
|
|
42
41
|
return {
|
|
43
42
|
appId: ctx.appId,
|
|
44
43
|
version: pre.version,
|
|
45
|
-
url:
|
|
46
|
-
releaseID:
|
|
44
|
+
url: release.onlineUrl ?? '',
|
|
45
|
+
releaseID: release.releaseID,
|
|
47
46
|
preReleaseID: pre.preReleaseID,
|
|
48
47
|
};
|
|
49
48
|
}
|
|
@@ -11,14 +11,14 @@ const index_1 = require("../atoms/index");
|
|
|
11
11
|
* 1. prepareDeployContext — 校验项目 + 读 meta
|
|
12
12
|
* 2. preRelease — 签发 pre_release_id / version / 动态配置 data
|
|
13
13
|
* 3. runBuild — 本地 npm run build(注入 data 中的 CDN 前缀)
|
|
14
|
-
* 4.
|
|
14
|
+
* 4. createLocalRelease — 创建发布单(加锁、拿 releaseID)
|
|
15
15
|
* 5. uploadArtifacts — 把产物推到 preRelease 下发的 pre-signed URL
|
|
16
16
|
* 6. savePluginInstances — 扫描 dist/output_capabilities/*.json,批量注册插件能力
|
|
17
|
-
* 7.
|
|
17
|
+
* 7. finalizeLocalRelease — Finished / Failed 翻终态
|
|
18
18
|
*
|
|
19
|
-
*
|
|
19
|
+
* createLocalRelease 提前到 upload 之前:发布单先建出来,
|
|
20
|
+
* upload / savePluginInstances 在已知 releaseID 的前提下进行;
|
|
20
21
|
* 任一步失败都会精准把该发布单标记为 Failed,避免遗留挂态。
|
|
21
|
-
* `--skip-release` 下发布单由外部托管,tracker 为 NOOP,失败信息通过非零退出码传递。
|
|
22
22
|
*/
|
|
23
23
|
async function localBuildLocalPublishPipeline(opts) {
|
|
24
24
|
const ctx = (0, index_1.prepareDeployContext)(opts.projectDir, opts.appId);
|
|
@@ -41,11 +41,10 @@ async function localBuildLocalPublishPipeline(opts) {
|
|
|
41
41
|
data,
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
extra: { checkPointVersion: opts.checkPointVersion, commitID: opts.commitID },
|
|
44
|
+
(0, logger_1.log)('deploy', 'Creating local release...');
|
|
45
|
+
const release = await (0, index_1.createLocalRelease)(ctx.appId, pre.version, {
|
|
46
|
+
checkPointVersion: opts.checkPointVersion,
|
|
47
|
+
commitID: opts.commitID,
|
|
49
48
|
});
|
|
50
49
|
try {
|
|
51
50
|
(0, logger_1.log)('deploy', 'Uploading artifacts...');
|
|
@@ -58,18 +57,18 @@ async function localBuildLocalPublishPipeline(opts) {
|
|
|
58
57
|
if (saveResult.saved > 0) {
|
|
59
58
|
(0, logger_1.log)('deploy', `${String(saveResult.saved)} capability instance(s) registered`);
|
|
60
59
|
}
|
|
61
|
-
await
|
|
60
|
+
await (0, index_1.finalizeLocalRelease)(ctx.appId, release.releaseID, index_1.LocalReleaseStatus.Finished);
|
|
62
61
|
}
|
|
63
62
|
catch (err) {
|
|
64
|
-
await
|
|
63
|
+
await (0, index_1.finalizeLocalRelease)(ctx.appId, release.releaseID, index_1.LocalReleaseStatus.Failed);
|
|
65
64
|
throw err;
|
|
66
65
|
}
|
|
67
66
|
(0, logger_1.log)('deploy', 'Deployed successfully');
|
|
68
67
|
return {
|
|
69
68
|
appId: ctx.appId,
|
|
70
69
|
version: pre.version,
|
|
71
|
-
url:
|
|
72
|
-
releaseID:
|
|
70
|
+
url: release.onlineUrl ?? '',
|
|
71
|
+
releaseID: release.releaseID,
|
|
73
72
|
preReleaseID: pre.preReleaseID,
|
|
74
73
|
};
|
|
75
74
|
}
|
|
@@ -9,6 +9,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
9
9
|
const npm_pack_1 = require("../utils/npm-pack");
|
|
10
10
|
const logger_1 = require("../utils/logger");
|
|
11
11
|
const env_1 = require("../utils/env");
|
|
12
|
+
const file_ops_1 = require("../utils/file-ops");
|
|
12
13
|
const STEERING_PACKAGE = '@lark-apaas/coding-steering';
|
|
13
14
|
function inferMode() {
|
|
14
15
|
// 沙箱(MIAODA_DEP_CACHE_DIR 非空)→ sandbox;本地 → local
|
|
@@ -63,6 +64,17 @@ function syncCodingSteering(opts) {
|
|
|
63
64
|
dstSkillsDir: node_path_1.default.join(opts.targetDir, '.agent', 'skills', 'steering', opts.stack, 'skills'),
|
|
64
65
|
dstTechPath: node_path_1.default.join(opts.targetDir, '.agent', 'skills', 'steering', opts.stack, 'tech.md'),
|
|
65
66
|
};
|
|
67
|
+
// 写入前解锁:沙箱链路每轮同步末尾会把 `.agent/skills` 锁成 a=rX 只读
|
|
68
|
+
// (file-ops 的 lockDirReadonly,对齐 update-skills.sh:241)。本函数有两个入口
|
|
69
|
+
// ——沙箱 `skills sync --type` 与本地 `skills sync` / `app init`——只要目录被锁过,
|
|
70
|
+
// 不解锁就 EACCES。老链路的解锁在 update-skills.sh:160-165 脚本层,每次必跑;
|
|
71
|
+
// 这里下沉到写入方,任一入口都不会漏。
|
|
72
|
+
//
|
|
73
|
+
// 解锁 layout 对应的**锁定根**而非 dstSkillsDir:锁是 `chmod -R` 打在根上的,
|
|
74
|
+
// 只解 dst 那一层,其祖先目录仍不可写 → mkdir/cp 照样失败。
|
|
75
|
+
(0, file_ops_1.unlockDirForWrite)(layout === 'flat'
|
|
76
|
+
? node_path_1.default.join(opts.targetDir, '.agents')
|
|
77
|
+
: node_path_1.default.join(opts.targetDir, '.agent', 'skills'));
|
|
66
78
|
node_fs_1.default.mkdirSync(dstSkillsDir, { recursive: true });
|
|
67
79
|
let techSynced = false;
|
|
68
80
|
const techSrc = node_path_1.default.join(stackDir, 'tech.md');
|
package/dist/utils/file-ops.js
CHANGED
|
@@ -5,6 +5,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.removeLineFromFile = removeLineFromFile;
|
|
7
7
|
exports.removeLineFromContent = removeLineFromContent;
|
|
8
|
+
exports.unlockDirForWrite = unlockDirForWrite;
|
|
9
|
+
exports.lockDirReadonly = lockDirReadonly;
|
|
10
|
+
exports.forceRemove = forceRemove;
|
|
11
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
12
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
13
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
14
|
const logger_1 = require("./logger");
|
|
@@ -43,3 +47,77 @@ function removeLineFromContent(content, pattern) {
|
|
|
43
47
|
changed: filtered.length !== lines.length,
|
|
44
48
|
};
|
|
45
49
|
}
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
51
|
+
// 目录权限:沙箱 skill 目录被刻意锁成只读,写入前须解锁、写完须锁回。
|
|
52
|
+
//
|
|
53
|
+
// 背景:老 update-skills.sh 同步末尾 `chmod -R a=rX "$TARGET_DIR"`(:241)把
|
|
54
|
+
// skills 锁成只读——skills 是平台下发的资产,不该被用户/agent 手改;下一轮同步
|
|
55
|
+
// 开头再 `chmod -R u+w`(:160-165)解锁。目录 owner 是沙箱当前用户,虽无写位但
|
|
56
|
+
// 有权改权限,所以不需要 sudo(沙箱容器带 no_new_privileges,sudo 本就被禁)。
|
|
57
|
+
//
|
|
58
|
+
// 放在 file-ops 而非 sandbox-skills:coding-steering 与 sandbox-skills 两条写入
|
|
59
|
+
// 路径都要用,避免其中一方 import 另一方造成方向混乱。
|
|
60
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
61
|
+
/**
|
|
62
|
+
* 解锁目录使其可写(`chmod -R u+w`)。
|
|
63
|
+
*
|
|
64
|
+
* 写入被只读锁保护的目录前必须调用,否则 `fs.cpSync` / `fs.rmSync` 报 EACCES。
|
|
65
|
+
* 只加 owner 写位、不动 g/o,与老链路一致。目录不存在直接返回 false(调用方
|
|
66
|
+
* 通常紧接着 mkdir)。失败只 debug 不抛:解锁是尽力而为,真写不进去让后续
|
|
67
|
+
* 写操作自己报错,错误信息更具体。
|
|
68
|
+
*/
|
|
69
|
+
function unlockDirForWrite(dir) {
|
|
70
|
+
if (!node_fs_1.default.existsSync(dir))
|
|
71
|
+
return false;
|
|
72
|
+
try {
|
|
73
|
+
(0, node_child_process_1.execFileSync)('chmod', ['-R', 'u+w', dir], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
(0, logger_1.debug)(`file-ops: chmod -R u+w ${dir} failed: ${err.message}`);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 把目录锁成只读(`chmod -R a=rX`),对齐 update-skills.sh:241。
|
|
83
|
+
*
|
|
84
|
+
* `a=rX`:所有人只读;`X` 是大写,只给目录和「原本已有执行位」的文件加 x,
|
|
85
|
+
* 不会把 SKILL.md 变成可执行。会连 owner 写位一并清掉,所以下一轮写入前必须
|
|
86
|
+
* {@link unlockDirForWrite}。
|
|
87
|
+
*
|
|
88
|
+
* **调用时机必须是全部写入完成之后**——锁上就写不进去了。
|
|
89
|
+
*/
|
|
90
|
+
function lockDirReadonly(dir) {
|
|
91
|
+
if (!node_fs_1.default.existsSync(dir))
|
|
92
|
+
return false;
|
|
93
|
+
try {
|
|
94
|
+
(0, node_child_process_1.execFileSync)('chmod', ['-R', 'a=rX', dir], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
(0, logger_1.debug)(`file-ops: chmod -R a=rX ${dir} failed: ${err.message}`);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 强制删除文件或目录,三层兜底:`chmod -R u+w` → `rm -rf` → `fs.rmSync`。
|
|
104
|
+
*
|
|
105
|
+
* 为什么不能直接 `fs.rmSync`:删条目需要**父目录**写权限,而 skill 目录常处于
|
|
106
|
+
* 只读锁下;另外老 update.sh 曾以 root 跑过,可能留下异主文件(如 root-owned 的
|
|
107
|
+
* `.last-update`),`rmSync{force:true}` 只忽略 ENOENT、遇 EACCES 仍抛。
|
|
108
|
+
* shell `rm -rf` 对异主文件 / 怪符号链接比 rmSync 宽容,故作主档。
|
|
109
|
+
*/
|
|
110
|
+
function forceRemove(target) {
|
|
111
|
+
if (!node_fs_1.default.existsSync(target))
|
|
112
|
+
return;
|
|
113
|
+
unlockDirForWrite(target);
|
|
114
|
+
try {
|
|
115
|
+
(0, node_child_process_1.execFileSync)('rm', ['-rf', target], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
116
|
+
if (!node_fs_1.default.existsSync(target))
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
(0, logger_1.debug)(`file-ops: rm -rf ${target} failed: ${err.message}`);
|
|
121
|
+
}
|
|
122
|
+
node_fs_1.default.rmSync(target, { recursive: true, force: true });
|
|
123
|
+
}
|
package/dist/utils/npm-pack.js
CHANGED
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.pickLaneVersion = pickLaneVersion;
|
|
6
7
|
exports.fetchNpmPackage = fetchNpmPackage;
|
|
7
8
|
const node_child_process_1 = require("node:child_process");
|
|
8
9
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
@@ -11,6 +12,38 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
11
12
|
const error_1 = require("./error");
|
|
12
13
|
const logger_1 = require("./logger");
|
|
13
14
|
const DEFAULT_REGISTRY = 'https://registry.npmmirror.com/';
|
|
15
|
+
/**
|
|
16
|
+
* 从 `npm view <pkg> versions --json` 里过滤出后缀 `-alpha.<X>` 的版本。
|
|
17
|
+
*
|
|
18
|
+
* 用于 sandbox-skills 新姿势(2026-08 迁离 lane-<X> dist-tag 之后):
|
|
19
|
+
* publish-sandbox-skills.sh 每次发的版本号 = `<base>-alpha.<LANE>-<YYYYMMDDHHMMSS>`,
|
|
20
|
+
* TCC 里的 X = `<LANE>-<TIMESTAMP>` 全局唯一。反推侧过滤 versions 数组即可精确命中。
|
|
21
|
+
*
|
|
22
|
+
* **不 throw 是硬要求**(消费方 fallback @latest 依赖返回 null 判定):
|
|
23
|
+
* - `npm view` 网络挂、包不存在、metadata 解析失败 → 返回 null
|
|
24
|
+
* - 无版本匹配 → 返回 null
|
|
25
|
+
* - 多版本匹配(理论上不该发生,因 timestamp 精度到秒)→ 字符串排序取最大(timestamp 最大)
|
|
26
|
+
*/
|
|
27
|
+
function pickLaneVersion(packageName, X, registry) {
|
|
28
|
+
const reg = registry ?? process.env.MIAODA_NPM_REGISTRY ?? DEFAULT_REGISTRY;
|
|
29
|
+
const suffix = `-alpha.${X}`;
|
|
30
|
+
try {
|
|
31
|
+
const stdout = (0, node_child_process_1.execFileSync)('npm', ['view', packageName, 'versions', '--json', '--registry', reg], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
32
|
+
const versions = JSON.parse(stdout);
|
|
33
|
+
if (!Array.isArray(versions))
|
|
34
|
+
return null;
|
|
35
|
+
const matched = versions.filter((v) => typeof v === 'string' && v.endsWith(suffix));
|
|
36
|
+
if (matched.length === 0)
|
|
37
|
+
return null;
|
|
38
|
+
// 通常唯一命中(X 含 timestamp 到秒);多命中时字符串排序取最大(timestamp 更晚)
|
|
39
|
+
matched.sort();
|
|
40
|
+
return matched[matched.length - 1] ?? null;
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
(0, logger_1.debug)(`pickLaneVersion(${packageName}, X=${X}) failed: ${err.message}`);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
14
47
|
/**
|
|
15
48
|
* `npm pack <pkg>@<ver> --registry <r>` + `tar -xzf`,解到临时目录。
|
|
16
49
|
* extractDir 是包内容根(含 package.json)。使用完必须 cleanup()。
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.extractLaneFromTgzKey = extractLaneFromTgzKey;
|
|
7
|
+
exports.syncSandboxSkills = syncSandboxSkills;
|
|
8
|
+
exports.validateSandboxSyncOpts = validateSandboxSyncOpts;
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const npm_pack_1 = require("../utils/npm-pack");
|
|
12
|
+
const error_1 = require("../utils/error");
|
|
13
|
+
const logger_1 = require("../utils/logger");
|
|
14
|
+
const file_ops_1 = require("../utils/file-ops");
|
|
15
|
+
const PACKAGE_BY_TYPE = {
|
|
16
|
+
miaoda: '@lark-apaas/coding-miaoda-sandbox-skills',
|
|
17
|
+
'miaoda-modern': '@lark-apaas/coding-miaoda-sandbox-skills',
|
|
18
|
+
'miaoda-design': '@lark-apaas/coding-miaoda-sandbox-skills',
|
|
19
|
+
openclaw: '@lark-apaas/coding-openclaw-sandbox-skills',
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* 目标根目录下**永不删除**的控制文件(对齐 `update-skills.sh` 的删除白名单):
|
|
23
|
+
* - `.keep` 沙箱级「冻结 skills」开关,存在即整体跳过同步(见 {@link syncSandboxSkills})
|
|
24
|
+
* - `.config` 沙箱级 tgzKey 覆盖,feida-ai 的 `enableConfigFileOverride` 读它
|
|
25
|
+
* (`getKeepFileTgzKey` → `.agent/skills/.config`),删了会让版本钉死失效
|
|
26
|
+
* - `.last-update` 同步时间戳,feida-ai freshness 检查读它;每轮末尾会重写,列进来只是语义完备
|
|
27
|
+
*/
|
|
28
|
+
const PRESERVED_ENTRIES = new Set(['.keep', '.config', '.last-update']);
|
|
29
|
+
/** `.keep` 存在时整体跳过同步的标记文件名,语义同 `update-skills.sh:149`。 */
|
|
30
|
+
const KEEP_FILE = '.keep';
|
|
31
|
+
/**
|
|
32
|
+
* rsync 式对齐:删掉目标里「源包已不提供」的顶层条目,对齐 `update-skills.sh:196-218`。
|
|
33
|
+
*
|
|
34
|
+
* 与整目录清空的区别(后者是本函数取代的旧实现):
|
|
35
|
+
* - 保住 {@link PRESERVED_ENTRIES} 里的控制文件
|
|
36
|
+
* - 保住 `steering/` 子树 —— 它由 handler 层的 syncCodingSteering 独占管理
|
|
37
|
+
* (`update-skills.sh:207` 平铺型分支同样明确「归 miaoda CLI 管,update.sh 不删不覆盖」)
|
|
38
|
+
*
|
|
39
|
+
* 只处理顶层:源包内容本身就是「一个 skill 一个顶层目录」,逐文件比对没有额外收益。
|
|
40
|
+
*/
|
|
41
|
+
function pruneStaleEntries(skillsRoot, sourceTopEntries) {
|
|
42
|
+
if (!node_fs_1.default.existsSync(skillsRoot))
|
|
43
|
+
return [];
|
|
44
|
+
const deleted = [];
|
|
45
|
+
for (const entry of node_fs_1.default.readdirSync(skillsRoot)) {
|
|
46
|
+
if (PRESERVED_ENTRIES.has(entry) || entry === 'steering')
|
|
47
|
+
continue;
|
|
48
|
+
if (sourceTopEntries.has(entry))
|
|
49
|
+
continue;
|
|
50
|
+
const target = node_path_1.default.join(skillsRoot, entry);
|
|
51
|
+
try {
|
|
52
|
+
// forceRemove 的 chmod→rm -rf→rmSync 三层兜底对文件同样适用:
|
|
53
|
+
// 老 update.sh 以 root 跑过、且会 chmod -R 锁只读,直接 rmSync 会 EACCES
|
|
54
|
+
(0, file_ops_1.forceRemove)(target);
|
|
55
|
+
deleted.push(entry);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
(0, logger_1.debug)(`sandbox-skills: prune ${entry} failed: ${err.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return deleted;
|
|
62
|
+
}
|
|
63
|
+
// 合并包内的业务线子目录(miaoda / miaoda-modern / miaoda-design);shared 单独处理。
|
|
64
|
+
const BUSINESS_LINE_BY_TYPE = {
|
|
65
|
+
miaoda: 'miaoda',
|
|
66
|
+
'miaoda-modern': 'miaoda-modern',
|
|
67
|
+
'miaoda-design': 'miaoda-design',
|
|
68
|
+
openclaw: null, // openclaw 独立包,直接展平
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* 从 TCC skillsUpdateTgzKey 反推 lane 名。
|
|
72
|
+
* skills_versions/all_feat-xxx.tgz → feat-xxx
|
|
73
|
+
* skills_versions/all_feat_xxx.tgz → feat-xxx (_ sanitize → -)
|
|
74
|
+
* skills_versions/all_latest.tgz → null(走 @latest)
|
|
75
|
+
* 任何 all_<X>.tgz → <X> 里 _ 全部转 -
|
|
76
|
+
*
|
|
77
|
+
* **宽进严出**:正则允许 `_` 匹配(兼容老 miaoda-skills 时代 TOS key 含 `_` 的存量 TCC
|
|
78
|
+
* 命名),但返回时 sanitize 成 `-` —— 对齐 publish-sandbox-skills.sh 侧只发 `-` 版
|
|
79
|
+
* lane 的现实(npm SemVer pre-release 只允许 `[A-Za-z0-9-]`,不允许 `_`)。
|
|
80
|
+
* 存量 TCC 里如果有 `all_feat_xxx.tgz` 这种命名,反推出 `feat-xxx` 后能正确命中
|
|
81
|
+
* npm 上实际发出的 `@lane-feat-xxx` 包,而不是静默 fallback 到 @latest。
|
|
82
|
+
*
|
|
83
|
+
* 用 nullable 而不是空字符串 —— 让调用方能判断"没 lane 就走 latest"。
|
|
84
|
+
*/
|
|
85
|
+
function extractLaneFromTgzKey(tgzKey) {
|
|
86
|
+
if (tgzKey === undefined || tgzKey === '') {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const match = /(?:^|\/)all_([A-Za-z0-9][A-Za-z0-9_-]*)\.tgz$/.exec(tgzKey);
|
|
90
|
+
if (match === null) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const lane = match[1].replace(/_/g, '-');
|
|
94
|
+
if (lane === 'latest') {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return lane;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 主流程:按 type 拉包 + sync 到目标目录。
|
|
101
|
+
*
|
|
102
|
+
* sync 策略:**rsync 式对齐**,逐条对应线上真实跑的 `update-skills.sh`(不是
|
|
103
|
+
* `cp-skills.sh`——那个脚本线上不执行,日志里的 `copied/skipped/deleted` 出自前者):
|
|
104
|
+
* 1. 目标目录有 `.keep` → 整体跳过(`update-skills.sh:149`)
|
|
105
|
+
* 2. 覆盖式写入源包提供的顶层条目
|
|
106
|
+
* 3. 删掉源包已不提供的顶层条目({@link pruneStaleEntries}),但保住
|
|
107
|
+
* {@link PRESERVED_ENTRIES} 与 `steering/`
|
|
108
|
+
*
|
|
109
|
+
* 不用整目录清空:那会删掉 `.config`(沙箱级 tgzKey 覆盖)、`.keep`(冻结开关)
|
|
110
|
+
* 和 handler 层刚写的 `steering/`,而老链路对这三者都有保护。
|
|
111
|
+
*/
|
|
112
|
+
function syncSandboxSkills(opts) {
|
|
113
|
+
const targetDir = node_path_1.default.resolve(opts.targetDir ?? process.cwd());
|
|
114
|
+
const packageName = PACKAGE_BY_TYPE[opts.type];
|
|
115
|
+
const businessLine = BUSINESS_LINE_BY_TYPE[opts.type];
|
|
116
|
+
const lane = extractLaneFromTgzKey(opts.tgzKey);
|
|
117
|
+
// ---- lane 拉包 ----
|
|
118
|
+
// 新姿势(2026-08 迁离 lane-<X> dist-tag 之后):
|
|
119
|
+
// TCC 里的 X(比如 `feat-migrate-skills-from-miaoda-skills-20260805143512`)
|
|
120
|
+
// 包含 branch + timestamp,`pickLaneVersion` 走 `npm view <pkg> versions --json`
|
|
121
|
+
// 过滤 `endsWith('-alpha.' + X)`(因 X 含 timestamp 全局唯一,唯一命中或 0 命中)。
|
|
122
|
+
// 0 命中 → fallback @latest(对齐旧 TCC 值找不到、灰度切换期间的兜底语义)。
|
|
123
|
+
// `pickLaneVersion` 内部对 `npm view` 报错 / 无匹配都返回 null,绝不 throw ——
|
|
124
|
+
// 目标就是"不挂掉"。
|
|
125
|
+
let fetched;
|
|
126
|
+
const laneVersion = lane === null ? null : (0, npm_pack_1.pickLaneVersion)(packageName, lane, opts.registry);
|
|
127
|
+
if (lane !== null && laneVersion === null) {
|
|
128
|
+
(0, logger_1.debug)(`sandbox-skills: no npm version matched -alpha.${lane}, fallback to @latest ` +
|
|
129
|
+
`(TCC 里的 X 可能是老 lane dist-tag 命名 / 已 unpublish / npm view 挂 / 网络断)`);
|
|
130
|
+
}
|
|
131
|
+
const primaryVersion = laneVersion ?? 'latest';
|
|
132
|
+
try {
|
|
133
|
+
(0, logger_1.debug)(`sandbox-skills: fetching ${packageName}@${primaryVersion}`);
|
|
134
|
+
fetched = (0, npm_pack_1.fetchNpmPackage)({
|
|
135
|
+
packageName,
|
|
136
|
+
version: primaryVersion,
|
|
137
|
+
registry: opts.registry,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
if (primaryVersion === 'latest') {
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
(0, logger_1.debug)(`sandbox-skills: pack ${primaryVersion} failed, fallback to @latest (err: ${err.message})`);
|
|
145
|
+
fetched = (0, npm_pack_1.fetchNpmPackage)({
|
|
146
|
+
packageName,
|
|
147
|
+
version: 'latest',
|
|
148
|
+
registry: opts.registry,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
// 计算 skill 目标根目录:
|
|
153
|
+
// - openclaw → <targetDir>/skills/(跟老 cp-skills.sh 的 openclaw 分支对齐,落 $WORKSPACE_DIR/agent/skills)
|
|
154
|
+
// - 其它 → <targetDir>/.agent/skills/
|
|
155
|
+
const skillsRoot = opts.type === 'openclaw'
|
|
156
|
+
? node_path_1.default.join(targetDir, 'skills')
|
|
157
|
+
: node_path_1.default.join(targetDir, '.agent', 'skills');
|
|
158
|
+
// `.keep` 短路:对齐 update-skills.sh:149-152,沙箱侧放了这个文件即表示
|
|
159
|
+
// 「我手改过 skills,别覆盖我」,整体跳过同步(含 steering —— 调用方看到
|
|
160
|
+
// skipped 就不该再往下写)。
|
|
161
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(skillsRoot, KEEP_FILE))) {
|
|
162
|
+
(0, logger_1.debug)(`sandbox-skills: ${KEEP_FILE} found in ${skillsRoot}, skip sync`);
|
|
163
|
+
return {
|
|
164
|
+
packageName,
|
|
165
|
+
version: fetched.version,
|
|
166
|
+
lane,
|
|
167
|
+
syncedTop: [],
|
|
168
|
+
deleted: [],
|
|
169
|
+
skippedByKeepFile: true,
|
|
170
|
+
targetDir: skillsRoot,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// 写入前解锁,对齐 update-skills.sh:160-165:
|
|
174
|
+
// 上一轮同步末尾会 chmod -R a=rX 锁只读(见 file-ops 的 lockDirReadonly),
|
|
175
|
+
// 不先解锁,下面 fs.cpSync 覆盖已存在的 skill 目录必 EACCES。
|
|
176
|
+
// 只加 u+w(不动 g/o),与老链路一致;owner 是沙箱当前用户,无需 sudo。
|
|
177
|
+
(0, file_ops_1.unlockDirForWrite)(skillsRoot);
|
|
178
|
+
node_fs_1.default.mkdirSync(skillsRoot, { recursive: true });
|
|
179
|
+
const syncedTop = [];
|
|
180
|
+
// 源包提供的全部顶层条目 —— prune 的保留集,**不能用 syncedTop 代替**:
|
|
181
|
+
// shared 那段遇到同名已存在会 `continue` 不写入、也就不进 syncedTop,若拿它当保留集,
|
|
182
|
+
// 第二轮同步会把上一轮写好的 shared skill 当 stale 删掉,第三轮再写回来,反复抖动。
|
|
183
|
+
const sourceTop = new Set();
|
|
184
|
+
if (businessLine !== null) {
|
|
185
|
+
// 合并包 miaoda/miaoda-modern/miaoda-design:cp <extractDir>/<business-line>/* 到 target
|
|
186
|
+
const sourceDir = node_path_1.default.join(fetched.extractDir, businessLine);
|
|
187
|
+
if (node_fs_1.default.existsSync(sourceDir)) {
|
|
188
|
+
for (const entry of node_fs_1.default.readdirSync(sourceDir)) {
|
|
189
|
+
const from = node_path_1.default.join(sourceDir, entry);
|
|
190
|
+
const to = node_path_1.default.join(skillsRoot, entry);
|
|
191
|
+
sourceTop.add(entry);
|
|
192
|
+
node_fs_1.default.cpSync(from, to, { recursive: true });
|
|
193
|
+
syncedTop.push(entry);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
(0, logger_1.debug)(`sandbox-skills: source dir ${sourceDir} not found in package (skipped)`);
|
|
198
|
+
}
|
|
199
|
+
// shared:sync 合并包内 shared/* 到 target(对齐老 build.sh 的 shared 分发行为)
|
|
200
|
+
const sharedDir = node_path_1.default.join(fetched.extractDir, 'shared');
|
|
201
|
+
if (node_fs_1.default.existsSync(sharedDir)) {
|
|
202
|
+
for (const entry of node_fs_1.default.readdirSync(sharedDir)) {
|
|
203
|
+
const from = node_path_1.default.join(sharedDir, entry);
|
|
204
|
+
const to = node_path_1.default.join(skillsRoot, entry);
|
|
205
|
+
// 无论是否跳过写入,shared 提供的条目都算「源包提供」,必须进保留集
|
|
206
|
+
sourceTop.add(entry);
|
|
207
|
+
if (node_fs_1.default.existsSync(to)) {
|
|
208
|
+
// shared 命中已存在的 skill(如同名)— 保留业务线版本,跳过 shared(对齐老 build.sh:shared 是补丁,同名不覆盖)
|
|
209
|
+
(0, logger_1.debug)(`sandbox-skills: shared/${entry} conflicts with business-line skill, skipped`);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
node_fs_1.default.cpSync(from, to, { recursive: true });
|
|
213
|
+
syncedTop.push(entry);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// openclaw 独立包:直接把包内所有 skill 目录(除 package.json 等元文件外)平铺到 skillsRoot
|
|
219
|
+
for (const entry of node_fs_1.default.readdirSync(fetched.extractDir)) {
|
|
220
|
+
if (entry === 'package.json' || entry === 'README.md' || entry === 'LICENSE')
|
|
221
|
+
continue;
|
|
222
|
+
const from = node_path_1.default.join(fetched.extractDir, entry);
|
|
223
|
+
const stat = node_fs_1.default.statSync(from);
|
|
224
|
+
if (!stat.isDirectory())
|
|
225
|
+
continue;
|
|
226
|
+
const to = node_path_1.default.join(skillsRoot, entry);
|
|
227
|
+
sourceTop.add(entry);
|
|
228
|
+
node_fs_1.default.cpSync(from, to, { recursive: true });
|
|
229
|
+
syncedTop.push(entry);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// rsync 式收尾:删掉源包已不提供的顶层条目(对齐 update-skills.sh 的 deleted)。
|
|
233
|
+
// 保留集用 sourceTop(源包提供的全部)而非 syncedTop(本轮实际写入的)——原因见
|
|
234
|
+
// sourceTop 声明处的注释:否则 shared skill 会被反复删了又写。
|
|
235
|
+
const deleted = pruneStaleEntries(skillsRoot, sourceTop);
|
|
236
|
+
// 写 .last-update 时间戳(对齐老 update-skills.sh;供 feida-ai freshness 检查用)
|
|
237
|
+
const lastUpdatePath = node_path_1.default.join(skillsRoot, '.last-update');
|
|
238
|
+
node_fs_1.default.writeFileSync(lastUpdatePath, String(Date.now()));
|
|
239
|
+
return {
|
|
240
|
+
packageName,
|
|
241
|
+
version: fetched.version,
|
|
242
|
+
lane,
|
|
243
|
+
deleted,
|
|
244
|
+
skippedByKeepFile: false,
|
|
245
|
+
syncedTop,
|
|
246
|
+
targetDir: skillsRoot,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
fetched.cleanup();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* 参数校验:type 白名单 + tgzKey 格式(避免非法值透传到 npm 包名)。
|
|
255
|
+
*/
|
|
256
|
+
function validateSandboxSyncOpts(opts) {
|
|
257
|
+
const validTypes = ['miaoda', 'miaoda-modern', 'miaoda-design', 'openclaw'];
|
|
258
|
+
if (opts.type === undefined || !validTypes.includes(opts.type)) {
|
|
259
|
+
throw new error_1.AppError('SKILLS_INVALID_TYPE', `--type must be one of: ${validTypes.join(', ')} (got: ${opts.type ?? 'undefined'})`);
|
|
260
|
+
}
|
|
261
|
+
if (opts.tgzKey !== undefined && opts.tgzKey !== '') {
|
|
262
|
+
// 校验 tgz key 大致格式(前缀 skills_versions/ + all_ + 允许字符 + .tgz);避免非法值影响 lane 反推
|
|
263
|
+
if (!/^[A-Za-z0-9/_\-.]+\.tgz$/.test(opts.tgzKey)) {
|
|
264
|
+
throw new error_1.AppError('SKILLS_INVALID_TGZ_KEY', `--tgz-key value looks invalid: ${opts.tgzKey}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return { type: opts.type, tgzKey: opts.tgzKey };
|
|
268
|
+
}
|
package/package.json
CHANGED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.NOOP_RELEASE_TRACKER = void 0;
|
|
4
|
-
exports.createManagedReleaseTracker = createManagedReleaseTracker;
|
|
5
|
-
exports.resolveReleaseTracker = resolveReleaseTracker;
|
|
6
|
-
const logger_1 = require("../../../../utils/logger");
|
|
7
|
-
const local_release_1 = require("./local-release");
|
|
8
|
-
/**
|
|
9
|
-
* 建本地发布单并返回受管 tracker。
|
|
10
|
-
*
|
|
11
|
-
* url 沿用原有口径:后端 onlineUrl 缺失时为空串(`''` 表示"后端没给",
|
|
12
|
-
* 与外部托管模式的 `null`「本模式不产生」是两个不同语义,别合并)。
|
|
13
|
-
*/
|
|
14
|
-
async function createManagedReleaseTracker(appId, version, extra) {
|
|
15
|
-
(0, logger_1.log)('deploy', 'Creating local release...');
|
|
16
|
-
const release = await (0, local_release_1.createLocalRelease)(appId, version, extra);
|
|
17
|
-
return {
|
|
18
|
-
releaseID: release.releaseID,
|
|
19
|
-
url: release.onlineUrl ?? '',
|
|
20
|
-
finish: () => (0, local_release_1.finalizeLocalRelease)(appId, release.releaseID, local_release_1.LocalReleaseStatus.Finished),
|
|
21
|
-
fail: () => (0, local_release_1.finalizeLocalRelease)(appId, release.releaseID, local_release_1.LocalReleaseStatus.Failed),
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* 空实现:发布单由外部托管,CLI 不建单也不翻终态。
|
|
26
|
-
*
|
|
27
|
-
* 外部据 CLI 的退出码决定把自己的发布单翻成 Finished 还是 Failed,
|
|
28
|
-
* 因此这里的 finish/fail 必须是真正的 no-op,不能顺手打日志误导排查。
|
|
29
|
-
*/
|
|
30
|
-
exports.NOOP_RELEASE_TRACKER = {
|
|
31
|
-
releaseID: null,
|
|
32
|
-
url: null,
|
|
33
|
-
finish: () => Promise.resolve(),
|
|
34
|
-
fail: () => Promise.resolve(),
|
|
35
|
-
};
|
|
36
|
-
/** 按 skipRelease 选 tracker:跳过则 NOOP,否则建真实发布单。 */
|
|
37
|
-
async function resolveReleaseTracker(opts) {
|
|
38
|
-
if (opts.skipRelease) {
|
|
39
|
-
(0, logger_1.log)('deploy', 'Skipping local release (managed externally)');
|
|
40
|
-
return exports.NOOP_RELEASE_TRACKER;
|
|
41
|
-
}
|
|
42
|
-
return createManagedReleaseTracker(opts.appId, opts.version, opts.extra);
|
|
43
|
-
}
|