@lark-apaas/miaoda-cli 0.1.29 → 0.1.31
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/README.md +5 -5
- package/dist/api/app/api.js +50 -5
- package/dist/cli/commands/app/index.js +16 -11
- package/dist/cli/commands/deploy/modern.js +13 -0
- package/dist/cli/commands/index.js +8 -5
- package/dist/cli/commands/registry/index.js +5 -3
- package/dist/cli/commands/skills/index.js +21 -1
- package/dist/cli/handlers/app/export.js +62 -14
- package/dist/cli/handlers/deploy/modern.js +1 -0
- package/dist/cli/handlers/skills/sync.js +104 -0
- package/dist/config/migrate-configs/html-to-design-html.js +56 -0
- package/dist/config/migrate-configs/index.js +2 -0
- package/dist/services/app/init/template.js +4 -0
- package/dist/services/deploy/modern/atoms/index.js +5 -1
- package/dist/services/deploy/modern/atoms/release-tracker.js +43 -0
- package/dist/services/deploy/modern/pipelines/design-local.js +9 -8
- package/dist/services/deploy/modern/pipelines/local.js +13 -12
- package/dist/services/registry/index.js +2 -1
- package/dist/utils/coding-steering.js +10 -0
- package/dist/utils/devops-error.js +19 -5
- package/dist/utils/file-ops.js +81 -0
- package/dist/utils/npm-pack.js +33 -0
- package/dist/utils/sandbox-skills.js +271 -0
- package/package.json +1 -1
|
@@ -0,0 +1,271 @@
|
|
|
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 分开维护:syncedTop 是
|
|
181
|
+
// 「本轮实际写入」,用于对外汇报;prune 只能依据「源包提供了什么」来判断 stale,
|
|
182
|
+
// 两者一旦哪天再次分叉(比如某分支又加了跳过写入的条件),拿 syncedTop 当保留集
|
|
183
|
+
// 就会把没重写的条目误判成 stale 删掉、下一轮再写回来,反复抖动。
|
|
184
|
+
const sourceTop = new Set();
|
|
185
|
+
if (businessLine !== null) {
|
|
186
|
+
// 合并包 miaoda/miaoda-modern/miaoda-design:cp <extractDir>/<business-line>/* 到 target
|
|
187
|
+
const sourceDir = node_path_1.default.join(fetched.extractDir, businessLine);
|
|
188
|
+
if (node_fs_1.default.existsSync(sourceDir)) {
|
|
189
|
+
for (const entry of node_fs_1.default.readdirSync(sourceDir)) {
|
|
190
|
+
const from = node_path_1.default.join(sourceDir, entry);
|
|
191
|
+
const to = node_path_1.default.join(skillsRoot, entry);
|
|
192
|
+
sourceTop.add(entry);
|
|
193
|
+
node_fs_1.default.cpSync(from, to, { recursive: true });
|
|
194
|
+
syncedTop.push(entry);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
(0, logger_1.debug)(`sandbox-skills: source dir ${sourceDir} not found in package (skipped)`);
|
|
199
|
+
}
|
|
200
|
+
// shared:合并包内 shared/* 到 target。**无条件覆盖**,逐字对齐老 build.sh:42-52 ——
|
|
201
|
+
// 那里业务线先 `cp -r`(:33-34)、shared 后 `cp -r`(:47),后写覆盖前写,
|
|
202
|
+
// 即同名时 **shared 赢**。当前包里两边无同名交集,但语义得跟老链路一致。
|
|
203
|
+
//
|
|
204
|
+
// 早期这里写成「`fs.existsSync(to)` 就 continue」,有两个问题:
|
|
205
|
+
// 1. 同名优先级反了(变成业务线赢);
|
|
206
|
+
// 2. 更严重 —— existsSync 分不清「本轮业务线刚写的」和「上一轮 shared 自己写的」,
|
|
207
|
+
// 于是 shared skill 首次落盘后就永久冻结,包里再更新也同步不到已有沙箱
|
|
208
|
+
// (沙箱磁盘跨会话保留,每次 update-skills 都会命中 existsSync)。
|
|
209
|
+
const sharedDir = node_path_1.default.join(fetched.extractDir, 'shared');
|
|
210
|
+
if (node_fs_1.default.existsSync(sharedDir)) {
|
|
211
|
+
for (const entry of node_fs_1.default.readdirSync(sharedDir)) {
|
|
212
|
+
const from = node_path_1.default.join(sharedDir, entry);
|
|
213
|
+
const to = node_path_1.default.join(skillsRoot, entry);
|
|
214
|
+
sourceTop.add(entry);
|
|
215
|
+
node_fs_1.default.cpSync(from, to, { recursive: true });
|
|
216
|
+
syncedTop.push(entry);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
// openclaw 独立包:直接把包内所有 skill 目录(除 package.json 等元文件外)平铺到 skillsRoot
|
|
222
|
+
for (const entry of node_fs_1.default.readdirSync(fetched.extractDir)) {
|
|
223
|
+
if (entry === 'package.json' || entry === 'README.md' || entry === 'LICENSE')
|
|
224
|
+
continue;
|
|
225
|
+
const from = node_path_1.default.join(fetched.extractDir, entry);
|
|
226
|
+
const stat = node_fs_1.default.statSync(from);
|
|
227
|
+
if (!stat.isDirectory())
|
|
228
|
+
continue;
|
|
229
|
+
const to = node_path_1.default.join(skillsRoot, entry);
|
|
230
|
+
sourceTop.add(entry);
|
|
231
|
+
node_fs_1.default.cpSync(from, to, { recursive: true });
|
|
232
|
+
syncedTop.push(entry);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// rsync 式收尾:删掉源包已不提供的顶层条目(对齐 update-skills.sh 的 deleted)。
|
|
236
|
+
// 保留集用 sourceTop(源包提供的全部)而非 syncedTop(本轮实际写入的)——原因见
|
|
237
|
+
// sourceTop 声明处的注释:否则 shared skill 会被反复删了又写。
|
|
238
|
+
const deleted = pruneStaleEntries(skillsRoot, sourceTop);
|
|
239
|
+
// 写 .last-update 时间戳(对齐老 update-skills.sh;供 feida-ai freshness 检查用)
|
|
240
|
+
const lastUpdatePath = node_path_1.default.join(skillsRoot, '.last-update');
|
|
241
|
+
node_fs_1.default.writeFileSync(lastUpdatePath, String(Date.now()));
|
|
242
|
+
return {
|
|
243
|
+
packageName,
|
|
244
|
+
version: fetched.version,
|
|
245
|
+
lane,
|
|
246
|
+
deleted,
|
|
247
|
+
skippedByKeepFile: false,
|
|
248
|
+
syncedTop,
|
|
249
|
+
targetDir: skillsRoot,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
fetched.cleanup();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* 参数校验:type 白名单 + tgzKey 格式(避免非法值透传到 npm 包名)。
|
|
258
|
+
*/
|
|
259
|
+
function validateSandboxSyncOpts(opts) {
|
|
260
|
+
const validTypes = ['miaoda', 'miaoda-modern', 'miaoda-design', 'openclaw'];
|
|
261
|
+
if (opts.type === undefined || !validTypes.includes(opts.type)) {
|
|
262
|
+
throw new error_1.AppError('SKILLS_INVALID_TYPE', `--type must be one of: ${validTypes.join(', ')} (got: ${opts.type ?? 'undefined'})`);
|
|
263
|
+
}
|
|
264
|
+
if (opts.tgzKey !== undefined && opts.tgzKey !== '') {
|
|
265
|
+
// 校验 tgz key 大致格式(前缀 skills_versions/ + all_ + 允许字符 + .tgz);避免非法值影响 lane 反推
|
|
266
|
+
if (!/^[A-Za-z0-9/_\-.]+\.tgz$/.test(opts.tgzKey)) {
|
|
267
|
+
throw new error_1.AppError('SKILLS_INVALID_TGZ_KEY', `--tgz-key value looks invalid: ${opts.tgzKey}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return { type: opts.type, tgzKey: opts.tgzKey };
|
|
271
|
+
}
|