@lark-apaas/miaoda-cli 0.1.24-alpha.7499e8d → 0.1.24-alpha.77e43f5
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.
|
@@ -26,10 +26,11 @@ function registerSkillsSync(parent) {
|
|
|
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
|
|
30
|
-
'
|
|
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 对后者仍生效)')
|
|
31
32
|
.option('--tgz-key <key>', '配合 --type 使用;老 TCC skillsUpdateTgzKey 值透传,' +
|
|
32
|
-
'从 skills_versions/all_<X>.tgz 反推出
|
|
33
|
+
'从 skills_versions/all_<X>.tgz 反推出 lane 名 X,再过滤出 -alpha.<X> 版本;缺省走 @latest')
|
|
33
34
|
.addHelpText('after', `
|
|
34
35
|
前置要求
|
|
35
36
|
当前目录(或 --dir)已走过 'miaoda app init'(.spark/meta.json 含 stack)
|
|
@@ -45,15 +46,28 @@ function registerSkillsSync(parent) {
|
|
|
45
46
|
.agents/tech.md
|
|
46
47
|
.claude/skills → ../.agents/skills(软链,让 Claude Code 识别同一份)
|
|
47
48
|
|
|
49
|
+
传 --type(沙箱链路,两部分都同步):
|
|
50
|
+
.agent/skills/<skill>/... ← 业务线 skill,平铺
|
|
51
|
+
.agent/skills/steering/<stack>/skills/<skill>/ ← steering,同上面默认形态
|
|
52
|
+
(openclaw 的业务线部分落 skills/ 而非 .agent/skills/)
|
|
53
|
+
|
|
48
54
|
JSON 输出
|
|
49
55
|
{"data": {"stack": "...", "version": "...", "syncedSkills": [...], "techSynced": true|false,
|
|
50
56
|
"claudeSkillsLink": "created|updated|noop|conflict" // 仅 --local 时}}
|
|
51
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
|
+
|
|
52
64
|
示例
|
|
53
65
|
$ miaoda skills sync # 拉 @latest
|
|
54
66
|
$ miaoda skills sync --local # 本地 dev / agent 用
|
|
55
67
|
$ miaoda skills sync --version 0.2.0 # 显式指定版本
|
|
56
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'
|
|
57
71
|
`);
|
|
58
72
|
cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
|
|
59
73
|
await (0, index_1.handleSkillsSync)({
|
|
@@ -10,6 +10,57 @@ const sandbox_skills_1 = require("../../../utils/sandbox-skills");
|
|
|
10
10
|
const spark_meta_1 = require("../../../utils/spark-meta");
|
|
11
11
|
const error_1 = require("../../../utils/error");
|
|
12
12
|
const output_1 = require("../../../utils/output");
|
|
13
|
+
/**
|
|
14
|
+
* sandbox 分支下补同步 steering(`.agent/skills/steering/<stack>/`)。
|
|
15
|
+
*
|
|
16
|
+
* **绝不抛错**,因为它在 sandbox 链路里是「附加项」而非门禁:
|
|
17
|
+
* - 无 `.spark/meta.json` / 无 stack → skipped。对齐老 cp-skills.sh 的
|
|
18
|
+
* `[ -n "$STACK" ] && [ "$STACK" != "null" ]` 判断(无 stack 就跳过 steering)。
|
|
19
|
+
* - 拉包 / 写盘失败 → failed,但仍让整条 sync 算成功。
|
|
20
|
+
*
|
|
21
|
+
* 为什么不能抛:feida-ai 侧 TCC 可配 `allowFailure: false`,一抛就阻断 agent 启动。
|
|
22
|
+
* 平铺业务线 skill 已经同步好了,不该因为 steering 这个增量部分让用户开不了会话。
|
|
23
|
+
*/
|
|
24
|
+
function syncSteeringForSandbox(params) {
|
|
25
|
+
let stack;
|
|
26
|
+
try {
|
|
27
|
+
const meta = (0, spark_meta_1.readSparkMeta)(params.targetDir);
|
|
28
|
+
if (meta.stack === undefined || meta.stack === '' || meta.stack === 'null') {
|
|
29
|
+
return { status: 'skipped', reason: '.spark/meta.json missing stack' };
|
|
30
|
+
}
|
|
31
|
+
stack = meta.stack;
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
return { status: 'skipped', reason: `read .spark/meta.json failed: ${err.message}` };
|
|
35
|
+
}
|
|
36
|
+
// 版本口径与下方本地分支保持一致:--version > BOE 的 dist-tag dev > latest
|
|
37
|
+
const isBoe = process.env.FORCE_FRAMEWORK_ENVIRONMENT === 'boe';
|
|
38
|
+
const effectiveVersion = params.version ?? (isBoe ? 'dev' : undefined);
|
|
39
|
+
try {
|
|
40
|
+
const r = (0, coding_steering_1.syncCodingSteering)({
|
|
41
|
+
stack,
|
|
42
|
+
targetDir: params.targetDir,
|
|
43
|
+
version: effectiveVersion,
|
|
44
|
+
// 必须显式传 mode,不能落到 inferMode():它靠 env MIAODA_DEP_CACHE_DIR 嗅探,
|
|
45
|
+
// 而 mode 决定读包内哪个 layer(sandbox→`skills/`,local→`skills_local/`)。
|
|
46
|
+
// `--type` 分支本身就是沙箱语义,嗅探失手会去读不存在的 skills_local/、
|
|
47
|
+
// 静默同步 0 个 skill。
|
|
48
|
+
mode: 'sandbox',
|
|
49
|
+
// 同理固定 nested:落 .agent/skills/steering/<stack>/,与老链路一致
|
|
50
|
+
outputLayout: 'nested',
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
status: 'synced',
|
|
54
|
+
stack,
|
|
55
|
+
version: r.version,
|
|
56
|
+
syncedSkills: r.syncedSkills,
|
|
57
|
+
techSynced: r.techSynced,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
return { status: 'failed', reason: err.message };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
13
64
|
/**
|
|
14
65
|
* miaoda skills sync [--dir <path>] [--version <ver>]
|
|
15
66
|
*
|
|
@@ -27,11 +78,25 @@ const output_1 = require("../../../utils/output");
|
|
|
27
78
|
async function handleSkillsSync(opts) {
|
|
28
79
|
await Promise.resolve();
|
|
29
80
|
const targetDir = node_path_1.default.resolve(opts.dir ?? process.cwd());
|
|
30
|
-
// --type 走 sandbox-skills
|
|
31
|
-
//
|
|
81
|
+
// --type 走 sandbox-skills 分支(迁仓后沙箱内由 feida-ai 直接下发调用)。
|
|
82
|
+
//
|
|
83
|
+
// 这条分支必须**同时**同步两部分,缺一不可 —— 线上老链路 update-skills.sh 也是两步
|
|
84
|
+
// (自己 rsync 平铺,再于第 230 行调不带 --type 的 `miaoda skills sync` 写 steering):
|
|
85
|
+
// ① syncSandboxSkills:平铺业务线 skill,rsync 式对齐(保 .keep/.config/steering)
|
|
86
|
+
// ② steering:`.agent/skills/steering/<stack>/`,按 .spark/meta.json 的 stack 过滤
|
|
87
|
+
//
|
|
88
|
+
// 历史 bug:早期这里 ① 完就 return,steering 既不同步、又被当时的整目录清空删掉,
|
|
89
|
+
// 沙箱少掉整批技术栈 skill(BOE 实测 scanAllSkills 从 59 掉到 34)。
|
|
90
|
+
//
|
|
91
|
+
// `.keep` 语义(update-skills.sh:149):目标目录挂了 .keep 表示「我手改过,别覆盖」,
|
|
92
|
+
// ① 会跳过并回报 skippedByKeepFile,此时 ② 也必须跳过 —— 否则 steering 仍被覆写,
|
|
93
|
+
// 冻结只冻结了一半。
|
|
32
94
|
if (opts.type !== undefined && opts.type !== '') {
|
|
33
95
|
const { type, tgzKey } = (0, sandbox_skills_1.validateSandboxSyncOpts)({ type: opts.type, tgzKey: opts.tgzKey });
|
|
34
96
|
const result = (0, sandbox_skills_1.syncSandboxSkills)({ type, tgzKey, targetDir });
|
|
97
|
+
const steering = result.skippedByKeepFile
|
|
98
|
+
? { status: 'skipped', reason: '.keep file found, sync frozen' }
|
|
99
|
+
: syncSteeringForSandbox({ targetDir, version: opts.version });
|
|
35
100
|
(0, output_1.emit)({
|
|
36
101
|
data: {
|
|
37
102
|
mode: 'sandbox',
|
|
@@ -40,7 +105,10 @@ async function handleSkillsSync(opts) {
|
|
|
40
105
|
version: result.version,
|
|
41
106
|
lane: result.lane,
|
|
42
107
|
syncedTop: result.syncedTop,
|
|
108
|
+
deleted: result.deleted,
|
|
109
|
+
skippedByKeepFile: result.skippedByKeepFile,
|
|
43
110
|
targetDir: result.targetDir,
|
|
111
|
+
steering,
|
|
44
112
|
},
|
|
45
113
|
});
|
|
46
114
|
return;
|
|
@@ -44,11 +44,53 @@ function forceRemoveDir(dir) {
|
|
|
44
44
|
node_fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
45
45
|
}
|
|
46
46
|
const PACKAGE_BY_TYPE = {
|
|
47
|
-
miaoda: '@lark-apaas/coding-miaoda-sandbox',
|
|
48
|
-
'miaoda-modern': '@lark-apaas/coding-miaoda-sandbox',
|
|
49
|
-
'miaoda-design': '@lark-apaas/coding-miaoda-sandbox',
|
|
50
|
-
openclaw: '@lark-apaas/coding-openclaw-sandbox',
|
|
47
|
+
miaoda: '@lark-apaas/coding-miaoda-sandbox-skills',
|
|
48
|
+
'miaoda-modern': '@lark-apaas/coding-miaoda-sandbox-skills',
|
|
49
|
+
'miaoda-design': '@lark-apaas/coding-miaoda-sandbox-skills',
|
|
50
|
+
openclaw: '@lark-apaas/coding-openclaw-sandbox-skills',
|
|
51
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* 目标根目录下**永不删除**的控制文件(对齐 `update-skills.sh` 的删除白名单):
|
|
54
|
+
* - `.keep` 沙箱级「冻结 skills」开关,存在即整体跳过同步(见 {@link syncSandboxSkills})
|
|
55
|
+
* - `.config` 沙箱级 tgzKey 覆盖,feida-ai 的 `enableConfigFileOverride` 读它
|
|
56
|
+
* (`getKeepFileTgzKey` → `.agent/skills/.config`),删了会让版本钉死失效
|
|
57
|
+
* - `.last-update` 同步时间戳,feida-ai freshness 检查读它;每轮末尾会重写,列进来只是语义完备
|
|
58
|
+
*/
|
|
59
|
+
const PRESERVED_ENTRIES = new Set(['.keep', '.config', '.last-update']);
|
|
60
|
+
/** `.keep` 存在时整体跳过同步的标记文件名,语义同 `update-skills.sh:149`。 */
|
|
61
|
+
const KEEP_FILE = '.keep';
|
|
62
|
+
/**
|
|
63
|
+
* rsync 式对齐:删掉目标里「源包已不提供」的顶层条目,对齐 `update-skills.sh:196-218`。
|
|
64
|
+
*
|
|
65
|
+
* 与整目录清空的区别(后者是本函数取代的旧实现):
|
|
66
|
+
* - 保住 {@link PRESERVED_ENTRIES} 里的控制文件
|
|
67
|
+
* - 保住 `steering/` 子树 —— 它由 handler 层的 syncCodingSteering 独占管理
|
|
68
|
+
* (`update-skills.sh:207` 平铺型分支同样明确「归 miaoda CLI 管,update.sh 不删不覆盖」)
|
|
69
|
+
*
|
|
70
|
+
* 只处理顶层:源包内容本身就是「一个 skill 一个顶层目录」,逐文件比对没有额外收益。
|
|
71
|
+
*/
|
|
72
|
+
function pruneStaleEntries(skillsRoot, sourceTopEntries) {
|
|
73
|
+
if (!node_fs_1.default.existsSync(skillsRoot))
|
|
74
|
+
return [];
|
|
75
|
+
const deleted = [];
|
|
76
|
+
for (const entry of node_fs_1.default.readdirSync(skillsRoot)) {
|
|
77
|
+
if (PRESERVED_ENTRIES.has(entry) || entry === 'steering')
|
|
78
|
+
continue;
|
|
79
|
+
if (sourceTopEntries.has(entry))
|
|
80
|
+
continue;
|
|
81
|
+
const target = node_path_1.default.join(skillsRoot, entry);
|
|
82
|
+
try {
|
|
83
|
+
// forceRemoveDir 的 chmod→rm -rf→rmSync 三层兜底对文件同样适用:
|
|
84
|
+
// 老 update.sh 以 root 跑过、且会 chmod -R 锁只读,直接 rmSync 会 EACCES
|
|
85
|
+
forceRemoveDir(target);
|
|
86
|
+
deleted.push(entry);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
(0, logger_1.debug)(`sandbox-skills: prune ${entry} failed: ${err.message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return deleted;
|
|
93
|
+
}
|
|
52
94
|
// 合并包内的业务线子目录(miaoda / miaoda-modern / miaoda-design);shared 单独处理。
|
|
53
95
|
const BUSINESS_LINE_BY_TYPE = {
|
|
54
96
|
miaoda: 'miaoda',
|
|
@@ -88,8 +130,15 @@ function extractLaneFromTgzKey(tgzKey) {
|
|
|
88
130
|
/**
|
|
89
131
|
* 主流程:按 type 拉包 + sync 到目标目录。
|
|
90
132
|
*
|
|
91
|
-
* sync
|
|
92
|
-
*
|
|
133
|
+
* sync 策略:**rsync 式对齐**,逐条对应线上真实跑的 `update-skills.sh`(不是
|
|
134
|
+
* `cp-skills.sh`——那个脚本线上不执行,日志里的 `copied/skipped/deleted` 出自前者):
|
|
135
|
+
* 1. 目标目录有 `.keep` → 整体跳过(`update-skills.sh:149`)
|
|
136
|
+
* 2. 覆盖式写入源包提供的顶层条目
|
|
137
|
+
* 3. 删掉源包已不提供的顶层条目({@link pruneStaleEntries}),但保住
|
|
138
|
+
* {@link PRESERVED_ENTRIES} 与 `steering/`
|
|
139
|
+
*
|
|
140
|
+
* 不用整目录清空:那会删掉 `.config`(沙箱级 tgzKey 覆盖)、`.keep`(冻结开关)
|
|
141
|
+
* 和 handler 层刚写的 `steering/`,而老链路对这三者都有保护。
|
|
93
142
|
*/
|
|
94
143
|
function syncSandboxSkills(opts) {
|
|
95
144
|
const targetDir = node_path_1.default.resolve(opts.targetDir ?? process.cwd());
|
|
@@ -137,11 +186,21 @@ function syncSandboxSkills(opts) {
|
|
|
137
186
|
const skillsRoot = opts.type === 'openclaw'
|
|
138
187
|
? node_path_1.default.join(targetDir, 'skills')
|
|
139
188
|
: node_path_1.default.join(targetDir, '.agent', 'skills');
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
189
|
+
// `.keep` 短路:对齐 update-skills.sh:149-152,沙箱侧放了这个文件即表示
|
|
190
|
+
// 「我手改过 skills,别覆盖我」,整体跳过同步(含 steering —— 调用方看到
|
|
191
|
+
// skipped 就不该再往下写)。
|
|
192
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(skillsRoot, KEEP_FILE))) {
|
|
193
|
+
(0, logger_1.debug)(`sandbox-skills: ${KEEP_FILE} found in ${skillsRoot}, skip sync`);
|
|
194
|
+
return {
|
|
195
|
+
packageName,
|
|
196
|
+
version: fetched.version,
|
|
197
|
+
lane,
|
|
198
|
+
syncedTop: [],
|
|
199
|
+
deleted: [],
|
|
200
|
+
skippedByKeepFile: true,
|
|
201
|
+
targetDir: skillsRoot,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
145
204
|
node_fs_1.default.mkdirSync(skillsRoot, { recursive: true });
|
|
146
205
|
const syncedTop = [];
|
|
147
206
|
if (businessLine !== null) {
|
|
@@ -188,6 +247,10 @@ function syncSandboxSkills(opts) {
|
|
|
188
247
|
syncedTop.push(entry);
|
|
189
248
|
}
|
|
190
249
|
}
|
|
250
|
+
// rsync 式收尾:删掉源包已不提供的顶层条目(对齐 update-skills.sh 的 deleted)。
|
|
251
|
+
// 放在写入之后:syncedTop 就是本轮源包提供的全部顶层条目,直接作为保留集,
|
|
252
|
+
// 不必再遍历一遍源目录。
|
|
253
|
+
const deleted = pruneStaleEntries(skillsRoot, new Set(syncedTop));
|
|
191
254
|
// 写 .last-update 时间戳(对齐老 update-skills.sh;供 feida-ai freshness 检查用)
|
|
192
255
|
const lastUpdatePath = node_path_1.default.join(skillsRoot, '.last-update');
|
|
193
256
|
node_fs_1.default.writeFileSync(lastUpdatePath, String(Date.now()));
|
|
@@ -195,6 +258,8 @@ function syncSandboxSkills(opts) {
|
|
|
195
258
|
packageName,
|
|
196
259
|
version: fetched.version,
|
|
197
260
|
lane,
|
|
261
|
+
deleted,
|
|
262
|
+
skippedByKeepFile: false,
|
|
198
263
|
syncedTop,
|
|
199
264
|
targetDir: skillsRoot,
|
|
200
265
|
};
|