@lark-apaas/miaoda-cli 0.1.24-alpha.28e47ce → 0.1.24-alpha.2eaf5d9
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/api/app/api.js +20 -0
- package/dist/api/app/index.js +2 -1
- package/dist/cli/commands/app/index.js +49 -0
- package/dist/cli/commands/skills/index.js +21 -1
- package/dist/cli/handlers/app/export.js +191 -0
- package/dist/cli/handlers/app/index.js +5 -1
- package/dist/cli/handlers/app/init.js +29 -17
- package/dist/cli/handlers/skills/sync.js +104 -0
- package/dist/config/migrate-configs/vite-react-to-nestjs-react-fullstack.js +4 -0
- package/dist/services/app/init/index.js +2 -1
- package/dist/services/app/init/template.js +8 -1
- package/dist/services/deploy/modern/pipelines/design-local.js +0 -4
- package/dist/services/deploy/modern/pipelines/local.js +0 -4
- package/dist/utils/coding-steering.js +33 -0
- package/dist/utils/file-ops.js +101 -0
- package/dist/utils/http.js +7 -1
- package/dist/utils/npm-pack.js +33 -0
- package/dist/utils/sandbox-skills.js +271 -0
- package/package.json +1 -1
package/dist/api/app/api.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getAppInfo = getAppInfo;
|
|
4
4
|
exports.updateAppMeta = updateAppMeta;
|
|
5
|
+
exports.getCodeArchive = getCodeArchive;
|
|
5
6
|
const http_1 = require("../../utils/http");
|
|
6
7
|
const devops_error_1 = require("../../utils/devops-error");
|
|
7
8
|
const DEFAULT_ERR_CODE = 'INTERNAL_DEVOPS_ERROR';
|
|
@@ -23,3 +24,22 @@ async function updateAppMeta(req) {
|
|
|
23
24
|
// 路径已带 appID,body 里也保留以匹配 BAM IDL 的 sensitive:"no" 透传约定
|
|
24
25
|
return (0, http_1.postInnerApi)(url, req, envelopeOpts('Failed to update app'));
|
|
25
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* GET /api/v1/studio/innerapi/apps/:appID/code-archive?sourceAppID=xxx — 导出源应用源码归档。
|
|
29
|
+
* 走 studio_server inner 通道(force 身份,服务侧 force→lark 换取后按 DOWNLOAD 鉴权,
|
|
30
|
+
* 鉴权对象是 sourceAppID),与 comment 域同一 inner 信封;创意应用升级链路(app export)消费。
|
|
31
|
+
*
|
|
32
|
+
* path :appID 必须是当前沙箱应用:网关 apaas_force_admin_plugin 强校验 URL appID 与沙箱
|
|
33
|
+
* AK 绑定应用一致(不一致 400 "appID in url is illegal"),跨应用导出目标因此走 query。
|
|
34
|
+
*/
|
|
35
|
+
async function getCodeArchive(params) {
|
|
36
|
+
const { appID, sourceAppID, checkpointID } = params;
|
|
37
|
+
let url = `/api/v1/studio/innerapi/apps/${encodeURIComponent(appID)}/code-archive` +
|
|
38
|
+
`?sourceAppID=${encodeURIComponent(sourceAppID)}`;
|
|
39
|
+
if (checkpointID !== undefined && checkpointID > 0) {
|
|
40
|
+
url += `&checkPointID=${String(checkpointID)}`;
|
|
41
|
+
}
|
|
42
|
+
return (0, http_1.getInnerApi)(url, {
|
|
43
|
+
errPrefix: 'Failed to export code archive',
|
|
44
|
+
});
|
|
45
|
+
}
|
package/dist/api/app/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ArchType = exports.Source = exports.BizType = exports.AppType = exports.AppMode = exports.AppStatus = exports.appMetaSchema = exports.updateAppMeta = exports.getAppInfo = void 0;
|
|
3
|
+
exports.ArchType = exports.Source = exports.BizType = exports.AppType = exports.AppMode = exports.AppStatus = exports.appMetaSchema = exports.getCodeArchive = exports.updateAppMeta = exports.getAppInfo = void 0;
|
|
4
4
|
var api_1 = require("./api");
|
|
5
5
|
Object.defineProperty(exports, "getAppInfo", { enumerable: true, get: function () { return api_1.getAppInfo; } });
|
|
6
6
|
Object.defineProperty(exports, "updateAppMeta", { enumerable: true, get: function () { return api_1.updateAppMeta; } });
|
|
7
|
+
Object.defineProperty(exports, "getCodeArchive", { enumerable: true, get: function () { return api_1.getCodeArchive; } });
|
|
7
8
|
var schemas_1 = require("./schemas");
|
|
8
9
|
Object.defineProperty(exports, "appMetaSchema", { enumerable: true, get: function () { return schemas_1.appMetaSchema; } });
|
|
9
10
|
var types_1 = require("./types");
|
|
@@ -21,6 +21,7 @@ function registerAppCommands(program, opts = {}) {
|
|
|
21
21
|
`);
|
|
22
22
|
registerAppGet(appCmd);
|
|
23
23
|
registerAppUpdate(appCmd);
|
|
24
|
+
registerAppExport(appCmd);
|
|
24
25
|
if (opts.includeInit) {
|
|
25
26
|
registerAppInit(appCmd);
|
|
26
27
|
registerAppSync(appCmd);
|
|
@@ -119,6 +120,54 @@ JSON 输出
|
|
|
119
120
|
});
|
|
120
121
|
}));
|
|
121
122
|
}
|
|
123
|
+
function registerAppExport(parent) {
|
|
124
|
+
const cmd = parent
|
|
125
|
+
.command('export <app-id>')
|
|
126
|
+
.description('导出指定应用的源码到本工程(创意应用升级:拉取创意稿作只读参考物)')
|
|
127
|
+
.option('--out <dir>', '输出目录(相对当前目录)', 'source_package/creative')
|
|
128
|
+
.option('--checkpoint <n>', '按检查点版本导出;缺省为最新提交(HEAD)')
|
|
129
|
+
.addHelpText('after', `
|
|
130
|
+
说明
|
|
131
|
+
从服务端导出 <app-id> 对应应用(源应用,可以不是当前应用)的源码归档并解压到 --out
|
|
132
|
+
目录,导出完成后写 --out/.upgrade-manifest.json(含 sourceAppId / 导出时间 / 文件数),
|
|
133
|
+
供升级链路幂等判定与 skill 发现使用。调用者须为源应用协作者(服务端 DOWNLOAD 鉴权)。
|
|
134
|
+
请求以当前应用(MIAODA_APP_ID)为路径主体、源应用走 sourceAppID 参数。
|
|
135
|
+
|
|
136
|
+
幂等
|
|
137
|
+
--out 已存在 manifest 且 sourceAppId 一致时跳过导出(skipped=true),重复调用安全。
|
|
138
|
+
|
|
139
|
+
JSON 输出
|
|
140
|
+
{"data": {"appId": "...", "out": "source_package/creative", "fileCount": <n>,
|
|
141
|
+
"archiveName": "...", "skipped": false}}
|
|
142
|
+
跳过时:{"data": {"appId": "...", "out": "...", "skipped": true, "reason": "already_exported"}}
|
|
143
|
+
|
|
144
|
+
失败
|
|
145
|
+
源应用无可导出内容(从未提交过代码)→ EXPORT_EMPTY_ARCHIVE,请先在源应用完成一轮创作。
|
|
146
|
+
无权限(非源应用协作者)→ 服务端鉴权错误透传。
|
|
147
|
+
|
|
148
|
+
示例
|
|
149
|
+
$ miaoda app export app_demo_source
|
|
150
|
+
$ miaoda app export app_demo_source --out source_package/creative
|
|
151
|
+
$ miaoda app export app_demo_source --checkpoint 3
|
|
152
|
+
`);
|
|
153
|
+
cmd.action((0, shared_1.withHelp)(cmd, async (appId, rawOpts) => {
|
|
154
|
+
const checkpointRaw = rawOpts.checkpoint;
|
|
155
|
+
let checkpoint;
|
|
156
|
+
if (checkpointRaw !== undefined) {
|
|
157
|
+
checkpoint = Number(checkpointRaw);
|
|
158
|
+
if (!Number.isInteger(checkpoint) || checkpoint <= 0) {
|
|
159
|
+
(0, shared_1.failArgs)(`--checkpoint 须为正整数:${checkpointRaw}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
await (0, index_1.handleAppExport)({
|
|
163
|
+
appId,
|
|
164
|
+
// path :appID 必须是当前沙箱应用(网关 AK 校验),从 MIAODA_APP_ID/app_id 解析
|
|
165
|
+
currentAppId: (0, shared_1.resolveAppId)({}),
|
|
166
|
+
out: rawOpts.out,
|
|
167
|
+
checkpoint,
|
|
168
|
+
});
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
122
171
|
function registerAppGet(parent) {
|
|
123
172
|
const cmd = parent
|
|
124
173
|
.command('get')
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.UPGRADE_MANIFEST_FILENAME = exports.DEFAULT_EXPORT_OUT = void 0;
|
|
40
|
+
exports.handleAppExport = handleAppExport;
|
|
41
|
+
const node_child_process_1 = require("node:child_process");
|
|
42
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
43
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
44
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
45
|
+
const api = __importStar(require("../../../api/index"));
|
|
46
|
+
const error_1 = require("../../../utils/error");
|
|
47
|
+
const output_1 = require("../../../utils/output");
|
|
48
|
+
const logger_1 = require("../../../utils/logger");
|
|
49
|
+
/** 导出目录默认值:创意升级链路的参考物约定位置。 */
|
|
50
|
+
exports.DEFAULT_EXPORT_OUT = 'source_package/creative';
|
|
51
|
+
/** 升级 manifest 文件名:前置钩子幂等标记 / skill workspace-contains 锚点 / skill 第 0 步信息源。 */
|
|
52
|
+
exports.UPGRADE_MANIFEST_FILENAME = '.upgrade-manifest.json';
|
|
53
|
+
/** 解码后归档大小上限(对齐服务端 UploadHtmlArtifact 的 50MB 口径)。 */
|
|
54
|
+
const MAX_ARCHIVE_BYTES = 50 * 1024 * 1024;
|
|
55
|
+
/** 校验相对输出路径:禁绝对路径、禁含 ".." 段(与 deploy patch 的 normalizeRel 同口径)。 */
|
|
56
|
+
function normalizeOutDir(rel) {
|
|
57
|
+
if (node_path_1.default.isAbsolute(rel) || rel.startsWith('/')) {
|
|
58
|
+
throw new error_1.AppError('ARGS_INVALID', `输出目录不能是绝对路径:${rel}`);
|
|
59
|
+
}
|
|
60
|
+
const posix = rel.split(node_path_1.default.sep).join('/').replace(/^\.\//, '');
|
|
61
|
+
if (posix.split('/').includes('..')) {
|
|
62
|
+
throw new error_1.AppError('ARGS_INVALID', `输出目录不能包含 "..":${rel}`);
|
|
63
|
+
}
|
|
64
|
+
return posix;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 按魔数识别归档格式并解压到 destDir。
|
|
68
|
+
* 服务端 GetCodeArchive 的归档名以 .zip 结尾,但底层 git archive 格式由 devops_platform
|
|
69
|
+
* 决定,历史文档亦有 tar.gz 记载 —— 两种都兼容,识别不了才报错。
|
|
70
|
+
*/
|
|
71
|
+
function extractArchive(archivePath, destDir, head) {
|
|
72
|
+
const isZip = head.length >= 4 && head[0] === 0x50 && head[1] === 0x4b;
|
|
73
|
+
const isGzip = head.length >= 2 && head[0] === 0x1f && head[1] === 0x8b;
|
|
74
|
+
if (isZip) {
|
|
75
|
+
(0, node_child_process_1.execFileSync)('unzip', ['-q', '-o', archivePath, '-d', destDir], { stdio: 'pipe' });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (isGzip) {
|
|
79
|
+
(0, node_child_process_1.execFileSync)('tar', ['-xzf', archivePath, '-C', destDir], { stdio: 'pipe' });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
throw new error_1.AppError('EXPORT_ARCHIVE_INVALID', '无法识别归档格式(非 zip / tar.gz)', {
|
|
83
|
+
next_actions: ['稍后重试 miaoda app export;持续失败请携带 log_id 反馈平台'],
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function countFiles(dir) {
|
|
87
|
+
let count = 0;
|
|
88
|
+
const walk = (cur) => {
|
|
89
|
+
for (const entry of node_fs_1.default.readdirSync(cur, { withFileTypes: true })) {
|
|
90
|
+
if (entry.isDirectory())
|
|
91
|
+
walk(node_path_1.default.join(cur, entry.name));
|
|
92
|
+
else
|
|
93
|
+
count += 1;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
walk(dir);
|
|
97
|
+
return count;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* miaoda app export <app-id> [--out <dir>] [--checkpoint <n>]
|
|
101
|
+
*
|
|
102
|
+
* 创意应用升级链路:把源应用(创意稿)的源码从服务端导出到本工程,作为只读参考物。
|
|
103
|
+
* 链路:studio_server InnerGetCodeArchive(inner 通道,force 身份鉴权:调用者须为源应用
|
|
104
|
+
* 协作者)→ git archive → 本地解压 → 末尾写 .upgrade-manifest.json(幂等标记 +
|
|
105
|
+
* skill workspace-contains 锚点 + skill 第 0 步信息源)。
|
|
106
|
+
*
|
|
107
|
+
* URL 形状:path :appID 是当前沙箱应用(网关强校验其与沙箱 AK 一致),源应用走 query
|
|
108
|
+
* sourceAppID —— 跨应用(升级常态:新应用沙箱里导出源创意应用)必须如此。
|
|
109
|
+
*
|
|
110
|
+
* 幂等:--out 已存在 manifest 且 sourceAppId 一致时跳过导出直接返回(重试/多轮不重复拉取)。
|
|
111
|
+
*/
|
|
112
|
+
async function handleAppExport(opts) {
|
|
113
|
+
const appId = (opts.appId || '').trim();
|
|
114
|
+
if (!appId) {
|
|
115
|
+
throw new error_1.AppError('ARGS_INVALID', 'app-id 不能为空');
|
|
116
|
+
}
|
|
117
|
+
// 当前沙箱应用:网关 AK 校验要求 path :appID 与沙箱绑定应用一致
|
|
118
|
+
const currentAppId = (opts.currentAppId || '').trim();
|
|
119
|
+
if (!currentAppId) {
|
|
120
|
+
throw new error_1.AppError('APP_ID_MISSING', '未指定当前应用', {
|
|
121
|
+
next_actions: ['设置 export MIAODA_APP_ID=<id>'],
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const outRel = normalizeOutDir(opts.out ?? exports.DEFAULT_EXPORT_OUT);
|
|
125
|
+
const outDir = node_path_1.default.resolve(process.cwd(), outRel);
|
|
126
|
+
const manifestPath = node_path_1.default.join(outDir, exports.UPGRADE_MANIFEST_FILENAME);
|
|
127
|
+
// 幂等:同源应用已导出过则直接复用(前置钩子重试 / 多轮对话不重复拉取)
|
|
128
|
+
if (node_fs_1.default.existsSync(manifestPath)) {
|
|
129
|
+
try {
|
|
130
|
+
const prev = JSON.parse(node_fs_1.default.readFileSync(manifestPath, 'utf-8'));
|
|
131
|
+
if (prev.sourceAppId === appId) {
|
|
132
|
+
(0, logger_1.log)('export', `Already exported (manifest matched), skip: ${outRel}`);
|
|
133
|
+
(0, output_1.emit)({ data: { appId, out: outRel, skipped: true, reason: 'already_exported' } });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// manifest 损坏视为未导出,走覆盖导出
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
(0, logger_1.log)('export', `Fetching code archive of ${appId}...`);
|
|
142
|
+
const resp = await api.app.getCodeArchive({
|
|
143
|
+
appID: currentAppId,
|
|
144
|
+
sourceAppID: appId,
|
|
145
|
+
checkpointID: opts.checkpoint,
|
|
146
|
+
});
|
|
147
|
+
const fileBase64 = resp.file ?? '';
|
|
148
|
+
if (!fileBase64) {
|
|
149
|
+
throw new error_1.AppError('EXPORT_EMPTY_ARCHIVE', '源应用没有可导出的内容(可能从未提交过代码)', {
|
|
150
|
+
next_actions: ['请先在源应用完成一轮创作(至少一次成功提交)后重试 miaoda app export'],
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const archive = Buffer.from(fileBase64, 'base64');
|
|
154
|
+
if (archive.length === 0) {
|
|
155
|
+
throw new error_1.AppError('EXPORT_EMPTY_ARCHIVE', '归档内容为空,请稍后重试');
|
|
156
|
+
}
|
|
157
|
+
if (archive.length > MAX_ARCHIVE_BYTES) {
|
|
158
|
+
throw new error_1.AppError('EXPORT_ARCHIVE_TOO_LARGE', `归档超过大小上限(${String(archive.length)} > ${String(MAX_ARCHIVE_BYTES)} bytes)`);
|
|
159
|
+
}
|
|
160
|
+
// 归档先落临时文件再解压;解压成功前不动已存在的 outDir 内容之外的文件
|
|
161
|
+
const tmpDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'miaoda-export-'));
|
|
162
|
+
const archivePath = node_path_1.default.join(tmpDir, resp.name ?? `${appId}.zip`);
|
|
163
|
+
try {
|
|
164
|
+
node_fs_1.default.writeFileSync(archivePath, archive);
|
|
165
|
+
node_fs_1.default.mkdirSync(outDir, { recursive: true });
|
|
166
|
+
(0, logger_1.log)('export', `Extracting to ${outRel}...`);
|
|
167
|
+
extractArchive(archivePath, outDir, archive.subarray(0, 4));
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
171
|
+
}
|
|
172
|
+
// 防御清理:git archive 语义不含 .git,防通道实现变化带出嵌套仓(git 会记 gitlink,
|
|
173
|
+
// 参考稿进不了提交);源仓 .gitignore 会成为新仓子树的嵌套忽略规则,极端 case(源仓
|
|
174
|
+
// force-add 过被自身 ignore 的文件)会让参考稿部分文件被 git add 静默忽略。
|
|
175
|
+
// 两者对 UX 对照均无价值,解压后移除,保证"进 git 的参考稿 = 导出的全部内容文件"。
|
|
176
|
+
node_fs_1.default.rmSync(node_path_1.default.join(outDir, '.git'), { recursive: true, force: true });
|
|
177
|
+
node_fs_1.default.rmSync(node_path_1.default.join(outDir, '.gitignore'), { force: true });
|
|
178
|
+
const fileCount = countFiles(outDir);
|
|
179
|
+
const manifest = {
|
|
180
|
+
sourceAppId: appId,
|
|
181
|
+
archiveName: resp.name ?? '',
|
|
182
|
+
checkpointId: opts.checkpoint ?? 0,
|
|
183
|
+
exportedAt: new Date().toISOString(),
|
|
184
|
+
fileCount,
|
|
185
|
+
};
|
|
186
|
+
node_fs_1.default.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
|
187
|
+
(0, logger_1.log)('export', 'Exported successfully');
|
|
188
|
+
(0, output_1.emit)({
|
|
189
|
+
data: { appId, out: outRel, fileCount, archiveName: manifest.archiveName, skipped: false },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SUPPORTED_STACKS = exports.handleAppMigrate = exports.handleAppUpgrade = exports.handleAppSync = exports.handleAppInit = exports.handleAppUpdate = exports.handleAppGet = void 0;
|
|
3
|
+
exports.SUPPORTED_STACKS = exports.handleAppMigrate = exports.UPGRADE_MANIFEST_FILENAME = exports.DEFAULT_EXPORT_OUT = exports.handleAppExport = exports.handleAppUpgrade = exports.handleAppSync = exports.handleAppInit = exports.handleAppUpdate = exports.handleAppGet = void 0;
|
|
4
4
|
var get_1 = require("./get");
|
|
5
5
|
Object.defineProperty(exports, "handleAppGet", { enumerable: true, get: function () { return get_1.handleAppGet; } });
|
|
6
6
|
var update_1 = require("./update");
|
|
@@ -10,6 +10,10 @@ Object.defineProperty(exports, "handleAppInit", { enumerable: true, get: functio
|
|
|
10
10
|
var sync_1 = require("./sync");
|
|
11
11
|
Object.defineProperty(exports, "handleAppSync", { enumerable: true, get: function () { return sync_1.handleAppSync; } });
|
|
12
12
|
Object.defineProperty(exports, "handleAppUpgrade", { enumerable: true, get: function () { return sync_1.handleAppUpgrade; } });
|
|
13
|
+
var export_1 = require("./export");
|
|
14
|
+
Object.defineProperty(exports, "handleAppExport", { enumerable: true, get: function () { return export_1.handleAppExport; } });
|
|
15
|
+
Object.defineProperty(exports, "DEFAULT_EXPORT_OUT", { enumerable: true, get: function () { return export_1.DEFAULT_EXPORT_OUT; } });
|
|
16
|
+
Object.defineProperty(exports, "UPGRADE_MANIFEST_FILENAME", { enumerable: true, get: function () { return export_1.UPGRADE_MANIFEST_FILENAME; } });
|
|
13
17
|
var migrate_1 = require("./migrate");
|
|
14
18
|
Object.defineProperty(exports, "handleAppMigrate", { enumerable: true, get: function () { return migrate_1.handleAppMigrate; } });
|
|
15
19
|
// commands 层渲染 help 时需要这份枚举;从 handler barrel 转发,避免 commands → services 越界
|
|
@@ -60,11 +60,16 @@ async function handleAppInit(opts) {
|
|
|
60
60
|
const tplResult = (0, index_1.renderTemplate)({ stack, version, targetDir, projectName });
|
|
61
61
|
// source_path 导入:当前仅 app_type=modern_html 生效。渲染完模板后、装依赖前叠加进 src/。
|
|
62
62
|
const sourceImport = applySourcePathImport(opts, targetDir);
|
|
63
|
+
// 本地脚手架门禁:沙箱内所有栈照旧同步(nested,产出 .agent/);本地只有 LOCAL_STEERING_STACKS
|
|
64
|
+
// 白名单栈(nestjs-react-fullstack / vite-react)才落地 steering(.claude/.agents)与 logs/。
|
|
65
|
+
// html / design-html 是纯静态 / 设计产物,本地不需要这套 agent skills 脚手架,整段跳过。
|
|
66
|
+
const sandbox = (0, env_1.isSandboxEnv)();
|
|
67
|
+
const scaffoldSteering = sandbox || index_1.LOCAL_STEERING_STACKS.has(stack);
|
|
63
68
|
// 先建 logs/,防止 user 跑 dev 之前 AI/工具 redirect 到 logs/*.log 因为父目录不存在直接挂
|
|
64
69
|
// (dev.js / dev-local.js 自己也建 logs/,但只在它启动后;启动前的 shell redirect 会先于此)。
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
if (!
|
|
70
|
+
// 仅本地白名单栈需要:沙箱环境由平台 supervisor 拉起 dev,无 init 前的 shell redirect 场景;
|
|
71
|
+
// 非白名单栈本地不落 steering 脚手架,也无需预建 logs/。
|
|
72
|
+
if (!sandbox && scaffoldSteering) {
|
|
68
73
|
(0, logs_dir_1.ensureLogsDir)(targetDir);
|
|
69
74
|
}
|
|
70
75
|
// skills 同步软失败:拉不到 coding-steering 包不该阻断 writeSparkMeta /
|
|
@@ -74,21 +79,28 @@ async function handleAppInit(opts) {
|
|
|
74
79
|
// 跟沙箱端 update-skills.sh 链路对齐
|
|
75
80
|
// - 本地 → flat (.agents/skills + .claude/skills 软链),
|
|
76
81
|
// Agent / Claude Code 用同一份;nrf 在本地是主战场,vite-react 等也一样适用
|
|
77
|
-
const outputLayout =
|
|
78
|
-
|
|
82
|
+
const outputLayout = sandbox ? 'nested' : 'flat';
|
|
83
|
+
// 默认空结果:非白名单栈跳过 steering 同步时(scaffoldSteering=false)直接沿用,
|
|
84
|
+
// 与"拉不到 coding-steering 包"软失败路径共用同一份占位,emit / meta 落盘不受影响。
|
|
85
|
+
let steeringResult = {
|
|
86
|
+
version: 'unknown',
|
|
87
|
+
syncedSkills: [],
|
|
88
|
+
techSynced: false,
|
|
89
|
+
};
|
|
79
90
|
let steeringError;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
if (scaffoldSteering) {
|
|
92
|
+
try {
|
|
93
|
+
steeringResult = (0, coding_steering_1.syncCodingSteering)({
|
|
94
|
+
stack,
|
|
95
|
+
targetDir,
|
|
96
|
+
logPrefix: 'init',
|
|
97
|
+
outputLayout,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
steeringError = err instanceof Error ? err.message : String(err);
|
|
102
|
+
(0, logger_1.log)('init', `⚠ skills sync failed (continuing init): ${steeringError}`);
|
|
103
|
+
}
|
|
92
104
|
}
|
|
93
105
|
// 依赖安装:async 模式留到 meta 落盘后派发后台进程(不等装完),同步模式当场装。
|
|
94
106
|
// 同步:装模板钉死的依赖。不带 <pkg>@latest 位置参数,因此 npm 不会改写 package.json/lockfile
|
|
@@ -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,57 @@ 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
|
+
//
|
|
106
|
+
// **只锁沙箱侧**:这里的 result.targetDir 只会是 `.agent/skills` 或 `skills`
|
|
107
|
+
// (见 sandbox-skills 的 skillsRoot),都是平台下发的资产根。本地 `--local` 走
|
|
108
|
+
// flat layout 输出到 `.agents/`,压根不经过本分支,权限不会被碰。
|
|
109
|
+
//
|
|
110
|
+
// 回退风险由解锁侧兜:权限是持久化在磁盘的,代码能回退、磁盘状态不会。所以
|
|
111
|
+
// file-ops 的 unlockDirForWrite 在**两处写入前**都调用了(syncSandboxSkills 的
|
|
112
|
+
// 写入前 + syncCodingSteering 的 nested 分支写入前),任一入口都不会因为
|
|
113
|
+
// 上一轮的锁而 EACCES。改动这里之前先确认那两处解锁还在。
|
|
114
|
+
const lockedReadonly = result.skippedByKeepFile ? false : (0, file_ops_1.lockDirReadonly)(result.targetDir);
|
|
115
|
+
(0, output_1.emit)({
|
|
116
|
+
data: {
|
|
117
|
+
mode: 'sandbox',
|
|
118
|
+
type,
|
|
119
|
+
packageName: result.packageName,
|
|
120
|
+
version: result.version,
|
|
121
|
+
lane: result.lane,
|
|
122
|
+
syncedTop: result.syncedTop,
|
|
123
|
+
deleted: result.deleted,
|
|
124
|
+
skippedByKeepFile: result.skippedByKeepFile,
|
|
125
|
+
lockedReadonly,
|
|
126
|
+
targetDir: result.targetDir,
|
|
127
|
+
steering,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// 原有 coding-steering 同步逻辑(本地 dev / miaoda app init 用)
|
|
29
133
|
const meta = (0, spark_meta_1.readSparkMeta)(targetDir);
|
|
30
134
|
if (meta.stack === undefined || meta.stack === '') {
|
|
31
135
|
throw new error_1.AppError('SKILLS_META_INCOMPLETE', '.spark/meta.json missing stack — run `miaoda app init` first');
|
|
@@ -263,6 +263,10 @@ exports.MIGRATE_CONFIG = {
|
|
|
263
263
|
'dotenv',
|
|
264
264
|
// client-toolkit:替代 lite,业务代码 codemod 后走它
|
|
265
265
|
'@lark-apaas/client-toolkit',
|
|
266
|
+
// drizzle-orm —— `gen:db-schema` 生成的 server/database/schema.ts 从
|
|
267
|
+
// 'drizzle-orm/pg-core' / 'drizzle-orm' 导入 pgTable / sql 等 runtime API,
|
|
268
|
+
// 缺依赖时 nest build / dev 起来直接 ERR_MODULE_NOT_FOUND。
|
|
269
|
+
'drizzle-orm',
|
|
266
270
|
],
|
|
267
271
|
devDependencies: [
|
|
268
272
|
// nest start / nest build 必需
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ASYNC_INSTALL_LOG = exports.runAsyncInstallWorker = exports.dispatchAsyncInstall = exports.resolveNpmInstallRegistry = exports.installDependencies = exports.writeSparkMeta = exports.readSparkMeta = exports.IMPORT_SKIP_FILES = exports.IMPORT_SKIP_DIRS = exports.importSourceSrc = exports.TEMPLATE_PACKAGE_BY_STACK = exports.STACK_BY_APP_TYPE = exports.APP_TYPES = exports.SUPPORTED_STACKS = exports.resolveStack = exports.renderTemplate = void 0;
|
|
3
|
+
exports.ASYNC_INSTALL_LOG = exports.runAsyncInstallWorker = exports.dispatchAsyncInstall = exports.resolveNpmInstallRegistry = exports.installDependencies = exports.writeSparkMeta = exports.readSparkMeta = exports.IMPORT_SKIP_FILES = exports.IMPORT_SKIP_DIRS = exports.importSourceSrc = exports.TEMPLATE_PACKAGE_BY_STACK = exports.STACK_BY_APP_TYPE = exports.APP_TYPES = exports.LOCAL_STEERING_STACKS = exports.SUPPORTED_STACKS = exports.resolveStack = exports.renderTemplate = void 0;
|
|
4
4
|
var template_1 = require("./template");
|
|
5
5
|
Object.defineProperty(exports, "renderTemplate", { enumerable: true, get: function () { return template_1.renderTemplate; } });
|
|
6
6
|
Object.defineProperty(exports, "resolveStack", { enumerable: true, get: function () { return template_1.resolveStack; } });
|
|
7
7
|
Object.defineProperty(exports, "SUPPORTED_STACKS", { enumerable: true, get: function () { return template_1.SUPPORTED_STACKS; } });
|
|
8
|
+
Object.defineProperty(exports, "LOCAL_STEERING_STACKS", { enumerable: true, get: function () { return template_1.LOCAL_STEERING_STACKS; } });
|
|
8
9
|
Object.defineProperty(exports, "APP_TYPES", { enumerable: true, get: function () { return template_1.APP_TYPES; } });
|
|
9
10
|
Object.defineProperty(exports, "STACK_BY_APP_TYPE", { enumerable: true, get: function () { return template_1.STACK_BY_APP_TYPE; } });
|
|
10
11
|
Object.defineProperty(exports, "TEMPLATE_PACKAGE_BY_STACK", { enumerable: true, get: function () { return template_1.TEMPLATE_PACKAGE_BY_STACK; } });
|
|
@@ -3,7 +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.APP_TYPES = exports.STACK_BY_APP_TYPE = exports.SUPPORTED_STACKS = exports.TEMPLATE_PINNED_VERSION_BY_STACK = exports.TEMPLATE_PACKAGE_BY_STACK = void 0;
|
|
6
|
+
exports.APP_TYPES = exports.STACK_BY_APP_TYPE = exports.LOCAL_STEERING_STACKS = exports.SUPPORTED_STACKS = exports.TEMPLATE_PINNED_VERSION_BY_STACK = exports.TEMPLATE_PACKAGE_BY_STACK = void 0;
|
|
7
7
|
exports.resolveStack = resolveStack;
|
|
8
8
|
exports.renderTemplate = renderTemplate;
|
|
9
9
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
@@ -27,6 +27,13 @@ exports.TEMPLATE_PACKAGE_BY_STACK = {
|
|
|
27
27
|
*/
|
|
28
28
|
exports.TEMPLATE_PINNED_VERSION_BY_STACK = {};
|
|
29
29
|
exports.SUPPORTED_STACKS = Object.keys(exports.TEMPLATE_PACKAGE_BY_STACK);
|
|
30
|
+
/**
|
|
31
|
+
* 本地开发(非沙箱)需要落地 agent skills 脚手架(.claude/.agents)与 logs/ 的 stack 白名单。
|
|
32
|
+
* 只有这些全栈 / 前端框架栈在本地才需要 coding-steering 的 skills;html / design-html 是纯
|
|
33
|
+
* 静态 / 设计产物,本地无需这套脚手架,init 直接跳过 syncCodingSteering 与 logs 创建。
|
|
34
|
+
* 沙箱环境不受此白名单约束(所有栈照旧走 nested 同步,产出 .agent/)。
|
|
35
|
+
*/
|
|
36
|
+
exports.LOCAL_STEERING_STACKS = new Set(['nestjs-react-fullstack', 'vite-react']);
|
|
30
37
|
/**
|
|
31
38
|
* app_type(业务类型)→ stack 短名映射。仅在调用方未显式传 --template 时用于解析 stack。
|
|
32
39
|
* 与 src/api/app/types.ts 的 AppType 枚举无关,是独立的业务类型概念。
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.designLocalPublishPipeline = designLocalPublishPipeline;
|
|
4
|
-
const spark_meta_1 = require("../../../../utils/spark-meta");
|
|
5
4
|
const error_1 = require("../../../../utils/error");
|
|
6
5
|
const logger_1 = require("../../../../utils/logger");
|
|
7
6
|
const index_1 = require("../atoms/index");
|
|
@@ -38,9 +37,6 @@ async function designLocalPublishPipeline(opts) {
|
|
|
38
37
|
await (0, index_1.finalizeLocalRelease)(ctx.appId, release.releaseID, index_1.LocalReleaseStatus.Failed);
|
|
39
38
|
throw err;
|
|
40
39
|
}
|
|
41
|
-
if (release.onlineUrl !== undefined && release.onlineUrl !== '') {
|
|
42
|
-
(0, spark_meta_1.writeSparkMeta)(ctx.projectDir, { appUrl: release.onlineUrl });
|
|
43
|
-
}
|
|
44
40
|
(0, logger_1.log)('deploy', 'Deployed successfully');
|
|
45
41
|
return {
|
|
46
42
|
appId: ctx.appId,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.localBuildLocalPublishPipeline = localBuildLocalPublishPipeline;
|
|
4
|
-
const spark_meta_1 = require("../../../../utils/spark-meta");
|
|
5
4
|
const error_1 = require("../../../../utils/error");
|
|
6
5
|
const logger_1 = require("../../../../utils/logger");
|
|
7
6
|
const index_1 = require("../atoms/index");
|
|
@@ -64,9 +63,6 @@ async function localBuildLocalPublishPipeline(opts) {
|
|
|
64
63
|
await (0, index_1.finalizeLocalRelease)(ctx.appId, release.releaseID, index_1.LocalReleaseStatus.Failed);
|
|
65
64
|
throw err;
|
|
66
65
|
}
|
|
67
|
-
if (release.onlineUrl !== undefined && release.onlineUrl !== '') {
|
|
68
|
-
(0, spark_meta_1.writeSparkMeta)(ctx.projectDir, { appUrl: release.onlineUrl });
|
|
69
|
-
}
|
|
70
66
|
(0, logger_1.log)('deploy', 'Deployed successfully');
|
|
71
67
|
return {
|
|
72
68
|
appId: ctx.appId,
|
|
@@ -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,29 @@ 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` chmod -R a=rX 锁成只读
|
|
68
|
+
// (file-ops 的 lockDirReadonly,对齐 update-skills.sh:241),不解锁下面 mkdir/cp
|
|
69
|
+
// 就 EACCES。老链路的解锁在 update-skills.sh:160-165 脚本层;这里下沉到写入方,
|
|
70
|
+
// 沙箱 `--type` 与本地 `skills sync` / `app init` 任一入口都不会漏。
|
|
71
|
+
//
|
|
72
|
+
// **只放开真正要写的两处**,不是整棵树递归解锁:
|
|
73
|
+
// - `.agent/skills` 本身(**非递归**):只为了能创建 `steering/` 这个条目
|
|
74
|
+
// - `.agent/skills/steering/`(递归):本函数唯一会改的子树
|
|
75
|
+
// 那 20 多个平铺业务线 skill 跟 steering 毫无关系,递归解锁会把它们一并打开 ——
|
|
76
|
+
// 早期就是这么写的,等于用户跑一次 `miaoda skills sync` 就把整个 skills 目录解封了。
|
|
77
|
+
//
|
|
78
|
+
// **先判断再动手**:目录没被锁(本地开发者的常态)就一次 chmod 都不跑,权限位一位
|
|
79
|
+
// 不动。判据是 owner 写位而非 accessSync,理由见 isLockedReadonly 的注释。
|
|
80
|
+
//
|
|
81
|
+
// **仅 nested**:flat 的输出根是 `.agents/`,lockDirReadonly 只作用于 `--type` 分支的
|
|
82
|
+
// `.agent/skills` / `skills`,`.agents/` 永远不会被锁,对它 chmod 纯属多余。
|
|
83
|
+
const nestedSkillsRoot = node_path_1.default.join(opts.targetDir, '.agent', 'skills');
|
|
84
|
+
const nestedSteeringDir = node_path_1.default.join(nestedSkillsRoot, 'steering');
|
|
85
|
+
const shouldRelock = layout === 'nested' && (0, file_ops_1.isLockedReadonly)(nestedSkillsRoot);
|
|
86
|
+
if (shouldRelock) {
|
|
87
|
+
(0, file_ops_1.unlockDirForWrite)(nestedSkillsRoot, { recursive: false });
|
|
88
|
+
(0, file_ops_1.unlockDirForWrite)(nestedSteeringDir);
|
|
89
|
+
}
|
|
66
90
|
node_fs_1.default.mkdirSync(dstSkillsDir, { recursive: true });
|
|
67
91
|
let techSynced = false;
|
|
68
92
|
const techSrc = node_path_1.default.join(stackDir, 'tech.md');
|
|
@@ -96,6 +120,15 @@ function syncCodingSteering(opts) {
|
|
|
96
120
|
if (layout === 'flat') {
|
|
97
121
|
claudeSkillsLink = ensureClaudeSkillsSymlink(opts.targetDir, logPrefix);
|
|
98
122
|
}
|
|
123
|
+
// 还原只读锁:解锁与加锁必须对称。否则单独跑一次 `miaoda skills sync` / `app init`
|
|
124
|
+
// 就把锁永久打开了 —— 老链路里这条命令是 update-skills.sh:230 调的、脚本在 :241
|
|
125
|
+
// 补了 chmod,单独跑没人补。范围与上面解锁的两处严格一致,先子树后自身。
|
|
126
|
+
//
|
|
127
|
+
// 只在「进来时本来就锁着」时还原,所以本地永远不会被莫名锁上。
|
|
128
|
+
if (shouldRelock) {
|
|
129
|
+
(0, file_ops_1.lockDirReadonly)(nestedSteeringDir);
|
|
130
|
+
(0, file_ops_1.lockDirReadonly)(nestedSkillsRoot, { recursive: false });
|
|
131
|
+
}
|
|
99
132
|
(0, logger_1.log)(logPrefix, `Synced ${String(synced.length)} skill(s), tech.md ${techSynced ? 'yes' : 'no'} (mode=${mode}, layout=${layout})`);
|
|
100
133
|
return {
|
|
101
134
|
version: fetched.version,
|
package/dist/utils/file-ops.js
CHANGED
|
@@ -5,6 +5,11 @@ 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.isLockedReadonly = isLockedReadonly;
|
|
10
|
+
exports.lockDirReadonly = lockDirReadonly;
|
|
11
|
+
exports.forceRemove = forceRemove;
|
|
12
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
13
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
14
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
15
|
const logger_1 = require("./logger");
|
|
@@ -43,3 +48,99 @@ function removeLineFromContent(content, pattern) {
|
|
|
43
48
|
changed: filtered.length !== lines.length,
|
|
44
49
|
};
|
|
45
50
|
}
|
|
51
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
// 目录权限:沙箱 skill 目录被刻意锁成只读,写入前须解锁、写完须锁回。
|
|
53
|
+
//
|
|
54
|
+
// 背景:老 update-skills.sh 同步末尾 `chmod -R a=rX "$TARGET_DIR"`(:241)把
|
|
55
|
+
// skills 锁成只读——skills 是平台下发的资产,不该被用户/agent 手改;下一轮同步
|
|
56
|
+
// 开头再 `chmod -R u+w`(:160-165)解锁。目录 owner 是沙箱当前用户,虽无写位但
|
|
57
|
+
// 有权改权限,所以不需要 sudo(沙箱容器带 no_new_privileges,sudo 本就被禁)。
|
|
58
|
+
//
|
|
59
|
+
// 放在 file-ops 而非 sandbox-skills:coding-steering 与 sandbox-skills 两条写入
|
|
60
|
+
// 路径都要用,避免其中一方 import 另一方造成方向混乱。
|
|
61
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
62
|
+
/**
|
|
63
|
+
* 解锁目录使其可写(`chmod -R u+w`)。
|
|
64
|
+
*
|
|
65
|
+
* 写入被只读锁保护的目录前必须调用,否则 `fs.cpSync` / `fs.rmSync` 报 EACCES。
|
|
66
|
+
* 只加 owner 写位、不动 g/o,与老链路一致。目录不存在直接返回 false(调用方
|
|
67
|
+
* 通常紧接着 mkdir)。失败只 debug 不抛:解锁是尽力而为,真写不进去让后续
|
|
68
|
+
* 写操作自己报错,错误信息更具体。
|
|
69
|
+
*/
|
|
70
|
+
function unlockDirForWrite(dir, opts) {
|
|
71
|
+
if (!node_fs_1.default.existsSync(dir))
|
|
72
|
+
return false;
|
|
73
|
+
const recursive = opts?.recursive ?? true;
|
|
74
|
+
const args = recursive ? ['-R', 'u+w', dir] : ['u+w', dir];
|
|
75
|
+
try {
|
|
76
|
+
(0, node_child_process_1.execFileSync)('chmod', args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
(0, logger_1.debug)(`file-ops: chmod ${args.join(' ')} failed: ${err.message}`);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 目录当前是否处于只读锁下 —— 判据是 **owner 写位缺失**(`mode & 0o200 === 0`),
|
|
86
|
+
* 正是 {@link lockDirReadonly} 的 `a=rX` 造成的状态。
|
|
87
|
+
*
|
|
88
|
+
* 不用 `fs.accessSync(dir, W_OK)`:那个按**进程 uid** 判定,root 对任何 mode 都返回
|
|
89
|
+
* 可写(老 update.sh 就以 root 跑过),会把锁着的目录误判成没锁。读 mode 位与 uid 无关。
|
|
90
|
+
*
|
|
91
|
+
* 用途是「先判断再决定要不要 chmod」:本地开发者的目录从来不会被我们锁,于是一次
|
|
92
|
+
* chmod 都不跑,权限位一位不动。
|
|
93
|
+
*/
|
|
94
|
+
function isLockedReadonly(dir) {
|
|
95
|
+
try {
|
|
96
|
+
return (node_fs_1.default.statSync(dir).mode & 0o200) === 0;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false; // 不存在 / stat 失败:当作没锁,让后续写操作自己报错
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 把目录锁成只读(`chmod -R a=rX`),对齐 update-skills.sh:241。
|
|
104
|
+
*
|
|
105
|
+
* `a=rX`:所有人只读;`X` 是大写,只给目录和「原本已有执行位」的文件加 x,
|
|
106
|
+
* 不会把 SKILL.md 变成可执行。会连 owner 写位一并清掉,所以下一轮写入前必须
|
|
107
|
+
* {@link unlockDirForWrite}。
|
|
108
|
+
*
|
|
109
|
+
* **调用时机必须是全部写入完成之后**——锁上就写不进去了。
|
|
110
|
+
*/
|
|
111
|
+
function lockDirReadonly(dir, opts) {
|
|
112
|
+
if (!node_fs_1.default.existsSync(dir))
|
|
113
|
+
return false;
|
|
114
|
+
const recursive = opts?.recursive ?? true;
|
|
115
|
+
const args = recursive ? ['-R', 'a=rX', dir] : ['a=rX', dir];
|
|
116
|
+
try {
|
|
117
|
+
(0, node_child_process_1.execFileSync)('chmod', args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
(0, logger_1.debug)(`file-ops: chmod ${args.join(' ')} failed: ${err.message}`);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 强制删除文件或目录,三层兜底:`chmod -R u+w` → `rm -rf` → `fs.rmSync`。
|
|
127
|
+
*
|
|
128
|
+
* 为什么不能直接 `fs.rmSync`:删条目需要**父目录**写权限,而 skill 目录常处于
|
|
129
|
+
* 只读锁下;另外老 update.sh 曾以 root 跑过,可能留下异主文件(如 root-owned 的
|
|
130
|
+
* `.last-update`),`rmSync{force:true}` 只忽略 ENOENT、遇 EACCES 仍抛。
|
|
131
|
+
* shell `rm -rf` 对异主文件 / 怪符号链接比 rmSync 宽容,故作主档。
|
|
132
|
+
*/
|
|
133
|
+
function forceRemove(target) {
|
|
134
|
+
if (!node_fs_1.default.existsSync(target))
|
|
135
|
+
return;
|
|
136
|
+
unlockDirForWrite(target);
|
|
137
|
+
try {
|
|
138
|
+
(0, node_child_process_1.execFileSync)('rm', ['-rf', target], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
139
|
+
if (!node_fs_1.default.existsSync(target))
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
(0, logger_1.debug)(`file-ops: rm -rf ${target} failed: ${err.message}`);
|
|
144
|
+
}
|
|
145
|
+
node_fs_1.default.rmSync(target, { recursive: true, force: true });
|
|
146
|
+
}
|
package/dist/utils/http.js
CHANGED
|
@@ -63,8 +63,14 @@ function createClient(opts) {
|
|
|
63
63
|
/**
|
|
64
64
|
* 默认灰度泳道。当前 miaoda CLI 接入的新接口(modern deploy 等)只在该泳道生效,
|
|
65
65
|
* 缺省走这个。MIAODA_CANARY_HEADER 可显式覆盖;设为空字符串则去掉头。
|
|
66
|
+
*
|
|
67
|
+
* ⚠️ TEMP(creative-upgrade 联调用):默认泳道临时指向开发泳道 boe_debug_detail_lookup,
|
|
68
|
+
* 使 alpha 发版进沙箱镜像后,app export / code-archive 等新接口无需 wrapper 即可命中
|
|
69
|
+
* 尚未合流的 studio_server 改动。
|
|
70
|
+
* 上线前必须还原为 'boe_miaoda_ccm',或改为由 agent_tool 按请求 x-tt-env 注入
|
|
71
|
+
* MIAODA_CANARY_HEADER(详见 http.ts 的 applyCanaryHeader 与 agent_tool ExecShellCmd)。
|
|
66
72
|
*/
|
|
67
|
-
const DEFAULT_CANARY_HEADER = '
|
|
73
|
+
const DEFAULT_CANARY_HEADER = 'boe_debug_detail_lookup';
|
|
68
74
|
/**
|
|
69
75
|
* 注入 x-tt-env 小流量头:env 显式设值 > DEFAULT_CANARY_HEADER。
|
|
70
76
|
*
|
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,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
|
+
}
|