@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
package/README.md
CHANGED
|
@@ -46,16 +46,16 @@ miaoda file ls --output json
|
|
|
46
46
|
| `miaoda file ...` | 文件操作:上传、下载、元数据、签名下载、批量删除 |
|
|
47
47
|
| `miaoda db ...` | 数据操作:SQL 执行、表结构查询、数据导入导出 |
|
|
48
48
|
| `miaoda observability ...` | 线上日志、链路、前端源码堆栈反查、监控指标、运营指标 |
|
|
49
|
-
| `miaoda comment ...` |
|
|
50
|
-
| `miaoda registry ...` | 组件 registry:列出 / copy-in
|
|
49
|
+
| `miaoda comment ...` | 应用评论:查看评论列表、解决评论(仅 design-html 场景开放) |
|
|
50
|
+
| `miaoda registry ...` | 组件 registry:列出 / copy-in SDK 文件(本地执行) |
|
|
51
51
|
|
|
52
52
|
完整命令通过 `miaoda --help` 或 `miaoda <domain> --help` 查看。
|
|
53
53
|
|
|
54
|
-
## 组件 registry
|
|
54
|
+
## 组件 registry
|
|
55
55
|
|
|
56
|
-
`miaoda registry`
|
|
56
|
+
`miaoda registry` 提供 shadcn 式的「组件 registry」:把 SDK 文件**拷进**当前项目(copy-in),而非作为依赖安装。registry 是独立 npm 包,按运行时分发(design-html / html → buildless runtime → `@lark-apaas/coding-registry-buildless`)。
|
|
57
57
|
|
|
58
|
-
前置:当前目录(或 `--dir`)已走过 `miaoda app init
|
|
58
|
+
前置:当前目录(或 `--dir`)已走过 `miaoda app init`,且 `.spark/meta.json` 的 `stack` 有对应 registry 包(当前支持 `design-html` / `html`);其余 stack 执行时报 `REGISTRY_STACK_UNSUPPORTED` 并提示支持列表。
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
61
|
# 列出可用 SDK 条目(输出各文件 USAGE 注释块原文,便于 Agent 直接读用法)
|
package/dist/api/app/api.js
CHANGED
|
@@ -5,6 +5,7 @@ exports.updateAppMeta = updateAppMeta;
|
|
|
5
5
|
exports.getCodeArchive = getCodeArchive;
|
|
6
6
|
const http_1 = require("../../utils/http");
|
|
7
7
|
const devops_error_1 = require("../../utils/devops-error");
|
|
8
|
+
const error_1 = require("../../utils/error");
|
|
8
9
|
const DEFAULT_ERR_CODE = 'INTERNAL_DEVOPS_ERROR';
|
|
9
10
|
function envelopeOpts(errPrefix) {
|
|
10
11
|
return {
|
|
@@ -24,22 +25,66 @@ async function updateAppMeta(req) {
|
|
|
24
25
|
// 路径已带 appID,body 里也保留以匹配 BAM IDL 的 sensitive:"no" 透传约定
|
|
25
26
|
return (0, http_1.postInnerApi)(url, req, envelopeOpts('Failed to update app'));
|
|
26
27
|
}
|
|
28
|
+
function mapExportError(bizCode, message, source) {
|
|
29
|
+
// token 是敏感令牌,错误信息只截前 8 位辅助定位
|
|
30
|
+
const sourceDesc = source.sourceAppID
|
|
31
|
+
? `应用 ${source.sourceAppID}`
|
|
32
|
+
: `分享 token ${(source.sourceMetaToken ?? '').slice(0, 8)}...`;
|
|
33
|
+
switch (bizCode) {
|
|
34
|
+
// 身份换取失败,或非源应用协作者(服务端 DOWNLOAD 鉴权不过)
|
|
35
|
+
case devops_error_1.BizErrCode.STUDIO_UNAUTHENTICATED:
|
|
36
|
+
return new error_1.AppError('EXPORT_PERMISSION_DENIED', `无权导出 ${sourceDesc}:${message}`, {
|
|
37
|
+
next_actions: [
|
|
38
|
+
`确认你是源应用的协作者;不是的话请让应用所有者添加协作者后重试`,
|
|
39
|
+
'这是权限问题,不是凭证配置或服务端抖动:不要重复执行本命令,也不要尝试自行签发 token',
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
// meta_token 无效或未绑定妙搭应用(服务端 ResolveAppIDByMetaToken 反查失败)
|
|
43
|
+
case devops_error_1.BizErrCode.STUDIO_APP_NOT_FOUND:
|
|
44
|
+
return new error_1.AppError('EXPORT_APP_NOT_FOUND', `${sourceDesc} 未找到对应应用:${message}`, {
|
|
45
|
+
next_actions: [
|
|
46
|
+
'核对分享 token / 链接是否完整(创意应用 URL 形如 https://<tenant>.feishu.cn/page/<token>)',
|
|
47
|
+
'确认目标是创意应用(只有创意应用有分享 token);这是目标不存在问题:不要重复执行本命令,请向用户确认',
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
// 应用不存在(错误 ID)
|
|
51
|
+
case devops_error_1.BizErrCode.DEVOPS_APP_NOT_FOUND:
|
|
52
|
+
case devops_error_1.BizErrCode.DEVOPS_APP_NOT_FOUND_ALT:
|
|
53
|
+
return new error_1.AppError('EXPORT_APP_NOT_FOUND', `${sourceDesc} 不存在或不可访问:${message}`, {
|
|
54
|
+
next_actions: [
|
|
55
|
+
'核对源应用 ID(应用页 URL 形如 /app/app_xxx,该段才是应用 ID)',
|
|
56
|
+
'这是目标不存在问题:不要重复执行本命令,请向用户确认正确的应用 ID',
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
default:
|
|
60
|
+
return (0, devops_error_1.mapDevopsError)(bizCode, message);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
27
63
|
/**
|
|
28
|
-
* GET /api/v1/studio/innerapi/apps/:appID/code-archive
|
|
64
|
+
* GET /api/v1/studio/innerapi/apps/:appID/code-archive — 导出源应用源码归档。
|
|
29
65
|
* 走 studio_server inner 通道(force 身份,服务侧 force→lark 换取后按 DOWNLOAD 鉴权,
|
|
30
|
-
*
|
|
66
|
+
* 鉴权对象是源应用),与 comment 域同一 inner 信封;创意应用升级链路(app export)消费。
|
|
31
67
|
*
|
|
32
68
|
* path :appID 必须是当前沙箱应用:网关 apaas_force_admin_plugin 强校验 URL appID 与沙箱
|
|
33
69
|
* AK 绑定应用一致(不一致 400 "appID in url is illegal"),跨应用导出目标因此走 query。
|
|
70
|
+
* 源应用定位二选一:sourceAppID(app_xxx),或 sourceMetaToken(创意应用分享 URL
|
|
71
|
+
* /page/<token> 的 CCM token,服务端反查 appID 后按同一口径鉴权)。
|
|
34
72
|
*/
|
|
35
73
|
async function getCodeArchive(params) {
|
|
36
|
-
const { appID, sourceAppID, checkpointID } = params;
|
|
37
|
-
|
|
38
|
-
|
|
74
|
+
const { appID, sourceAppID, sourceMetaToken, checkpointID } = params;
|
|
75
|
+
if (!sourceAppID === !sourceMetaToken) {
|
|
76
|
+
throw new error_1.AppError('ARGS_INVALID', 'sourceAppID 与 sourceMetaToken 必须恰传其一');
|
|
77
|
+
}
|
|
78
|
+
const sourceQuery = sourceAppID
|
|
79
|
+
? `sourceAppID=${encodeURIComponent(sourceAppID)}`
|
|
80
|
+
: `sourceMetaToken=${encodeURIComponent(sourceMetaToken ?? '')}`;
|
|
81
|
+
let url = `/api/v1/studio/innerapi/apps/${encodeURIComponent(appID)}/code-archive?` + sourceQuery;
|
|
39
82
|
if (checkpointID !== undefined && checkpointID > 0) {
|
|
40
83
|
url += `&checkPointID=${String(checkpointID)}`;
|
|
41
84
|
}
|
|
42
85
|
return (0, http_1.getInnerApi)(url, {
|
|
43
86
|
errPrefix: 'Failed to export code archive',
|
|
87
|
+
defaultErrCode: DEFAULT_ERR_CODE,
|
|
88
|
+
mapErr: (bizCode, message) => mapExportError(bizCode, message, { sourceAppID, sourceMetaToken }),
|
|
44
89
|
});
|
|
45
90
|
}
|
|
@@ -122,32 +122,37 @@ JSON 输出
|
|
|
122
122
|
}
|
|
123
123
|
function registerAppExport(parent) {
|
|
124
124
|
const cmd = parent
|
|
125
|
-
.command('export <app-id>')
|
|
126
|
-
.description('
|
|
125
|
+
.command('export <app-id-or-token>')
|
|
126
|
+
.description('导出指定应用的源码到本工程(创意应用升级:拉取创意稿作只读参考物);接受应用 ID(app_xxx)或创意应用分享 token')
|
|
127
127
|
.option('--out <dir>', '输出目录(相对当前目录)', 'source_package/creative')
|
|
128
128
|
.option('--checkpoint <n>', '按检查点版本导出;缺省为最新提交(HEAD)')
|
|
129
129
|
.addHelpText('after', `
|
|
130
130
|
说明
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
从服务端导出源应用(可以不是当前应用)的源码归档并解压到 --out 目录,导出完成后写
|
|
132
|
+
--out/.upgrade-manifest.json(含 sourceAppId / 导出时间 / 文件数),供升级链路幂等
|
|
133
|
+
判定与 skill 发现使用。调用者须为源应用协作者(服务端 DOWNLOAD 鉴权)。
|
|
134
|
+
<app-id-or-token> 两种形态等价:应用 ID(app_xxx)或创意应用分享 token。拿到的是
|
|
135
|
+
分享链接(https://<tenant>.feishu.cn/page/<token>)时,提取其中 /page/ 后的 <token> 段传入。
|
|
136
|
+
请求以当前应用(MIAODA_APP_ID)为路径主体、源应用走 sourceAppID / sourceMetaToken 参数。
|
|
135
137
|
|
|
136
138
|
幂等
|
|
137
|
-
--out 已存在 manifest
|
|
139
|
+
--out 已存在 manifest 且源定位(sourceAppId / sourceMetaToken)一致时跳过导出
|
|
140
|
+
(skipped=true),重复调用安全。
|
|
138
141
|
|
|
139
142
|
JSON 输出
|
|
140
143
|
{"data": {"appId": "...", "out": "source_package/creative", "fileCount": <n>,
|
|
141
144
|
"archiveName": "...", "skipped": false}}
|
|
142
145
|
跳过时:{"data": {"appId": "...", "out": "...", "skipped": true, "reason": "already_exported"}}
|
|
146
|
+
appId 始终是实际导出的源应用 ID(传 token 时为服务端反查结果)。
|
|
143
147
|
|
|
144
148
|
失败
|
|
145
149
|
源应用无可导出内容(从未提交过代码)→ EXPORT_EMPTY_ARCHIVE,请先在源应用完成一轮创作。
|
|
146
|
-
|
|
150
|
+
非源应用协作者 → EXPORT_PERMISSION_DENIED,权限问题,重试无效,需应用所有者加协作者。
|
|
151
|
+
应用 ID 不存在,或 token 未找到对应应用 → EXPORT_APP_NOT_FOUND,核对入参,重试无效。
|
|
147
152
|
|
|
148
153
|
示例
|
|
149
154
|
$ miaoda app export app_demo_source
|
|
150
|
-
$ miaoda app export
|
|
155
|
+
$ miaoda app export DemoPageTokenAbCdEf123456789
|
|
151
156
|
$ miaoda app export app_demo_source --checkpoint 3
|
|
152
157
|
`);
|
|
153
158
|
cmd.action((0, shared_1.withHelp)(cmd, async (appId, rawOpts) => {
|
|
@@ -217,7 +222,7 @@ function registerAppInit(parent) {
|
|
|
217
222
|
.command('init')
|
|
218
223
|
.description('初始化应用代码:抓 template 渲染、同步 upgrade/templates、装 .agent/steering/ skills、写 .spark/meta.json。.spark/meta.json 已存在则直接退出')
|
|
219
224
|
.option('--template <stack>', `技术栈短名(${index_1.SUPPORTED_STACKS.join(' / ')})`)
|
|
220
|
-
.option('--app-type <type>', 'aPaaS 业务类型(html / modern_html / jspage / full_stack);未传 --template 时用于解析技术栈')
|
|
225
|
+
.option('--app-type <type>', 'aPaaS 业务类型(html / modern_html / frontend / jspage / full_stack);未传 --template 时用于解析技术栈')
|
|
221
226
|
.option('--source-path <path>', '导入源目录:把该目录内容聚合进脚手架 src/(当前仅 --app-type modern_html 生效)')
|
|
222
227
|
.option('--conf [json]', 'init 配置 JSON。支持 {"version": "<template 版本>"},默认 latest')
|
|
223
228
|
.option('--skip-install', '跳过依赖安装', false)
|
|
@@ -237,7 +242,7 @@ function registerAppInit(parent) {
|
|
|
237
242
|
|
|
238
243
|
技术栈解析(--template / --app-type)
|
|
239
244
|
--template 传了 → 直接用(vite-react / html / design-html / nestjs-react-fullstack)
|
|
240
|
-
否则 --app-type 传了 → 查表:html→design-html, modern_html→html, jspage→vite-react, full_stack→nestjs-react-fullstack
|
|
245
|
+
否则 --app-type 传了 → 查表:html→design-html, modern_html→html, frontend/jspage→vite-react, full_stack→nestjs-react-fullstack
|
|
241
246
|
两者都没传 → 报错 ARGS_INVALID
|
|
242
247
|
|
|
243
248
|
源码导入(--source-path)
|
|
@@ -17,6 +17,7 @@ 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)
|
|
20
21
|
.option('--conf <json>', '关联元信息 JSON,仅识别 checkPointVersion / commitID 两字段')
|
|
21
22
|
.addHelpText('after', `
|
|
22
23
|
不要用异步模式或后台模式调用 deploy,否则调用可能提前结束,Agent 会误判发布已完成。
|
|
@@ -34,6 +35,15 @@ function registerDeployCommandsModern(program) {
|
|
|
34
35
|
6. savePluginInstances(扫描 dist/output_capabilities/*.json 批量注册)
|
|
35
36
|
7. finalizeLocalRelease(Finished|Failed)
|
|
36
37
|
|
|
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
|
+
|
|
37
47
|
--conf(关联元信息透传)
|
|
38
48
|
值为 JSON string,只识别两个字段,其余 key 忽略:
|
|
39
49
|
checkPointVersion 关联的 checkpoint 版本
|
|
@@ -42,11 +52,13 @@ function registerDeployCommandsModern(program) {
|
|
|
42
52
|
|
|
43
53
|
JSON 输出(stdout)
|
|
44
54
|
{"data": {"appId": "...", "version": <n>, "url": "...", "releaseID": "...", "preReleaseID": "..."}}
|
|
55
|
+
--skip-release 时 url 与 releaseID 为 null,字段本身仍在。
|
|
45
56
|
|
|
46
57
|
示例
|
|
47
58
|
$ miaoda deploy
|
|
48
59
|
$ miaoda deploy --dir ./my-app
|
|
49
60
|
$ miaoda deploy --skip-build
|
|
61
|
+
$ miaoda deploy --skip-release
|
|
50
62
|
$ miaoda deploy --conf '{"checkPointVersion":"v3","commitID":"a1b2c3d"}'
|
|
51
63
|
`);
|
|
52
64
|
deployCmd.action((0, shared_1.withHelp)(deployCmd, async (rawOpts) => {
|
|
@@ -54,6 +66,7 @@ JSON 输出(stdout)
|
|
|
54
66
|
dir: rawOpts.dir ?? '.',
|
|
55
67
|
appId: (0, shared_1.resolveAppId)({}),
|
|
56
68
|
skipBuild: rawOpts.skipBuild,
|
|
69
|
+
skipRelease: rawOpts.skipRelease,
|
|
57
70
|
conf: rawOpts.conf,
|
|
58
71
|
});
|
|
59
72
|
}));
|
|
@@ -31,6 +31,9 @@ function resolveScene(appType, _archType) {
|
|
|
31
31
|
return 'design-html';
|
|
32
32
|
return 'default';
|
|
33
33
|
}
|
|
34
|
+
// registry(shadcn 式 copy-in SDK)挂在全部 scene:scene 路由不感知 registry 的可用性,
|
|
35
|
+
// stack 是否有对应 registry 包由运行时映射表(services/registry STACK_RUNTIME)校验,
|
|
36
|
+
// 无映射的 stack 执行时报 REGISTRY_STACK_UNSUPPORTED 并提示支持列表。
|
|
34
37
|
const SCENE_REGISTRARS = {
|
|
35
38
|
default: (p) => {
|
|
36
39
|
// init / upgrade / skills 都挂出来:本地用户不需要 export MIAODA_APP_TYPE=3
|
|
@@ -42,13 +45,13 @@ const SCENE_REGISTRARS = {
|
|
|
42
45
|
(0, index_1.registerFileCommands)(p);
|
|
43
46
|
(0, index_3.registerObservabilityCommands)(p);
|
|
44
47
|
(0, index_6.registerSkillsCommands)(p);
|
|
45
|
-
(0,
|
|
48
|
+
(0, index_8.registerRegistryCommands)(p);
|
|
46
49
|
},
|
|
47
50
|
modern: (p) => {
|
|
48
51
|
(0, index_4.registerAppCommands)(p, { includeInit: true });
|
|
49
52
|
(0, modern_1.registerDeployCommandsModern)(p);
|
|
50
53
|
(0, index_6.registerSkillsCommands)(p);
|
|
51
|
-
(0,
|
|
54
|
+
(0, index_8.registerRegistryCommands)(p);
|
|
52
55
|
},
|
|
53
56
|
// application scene(AppType_APPLICATION=3):
|
|
54
57
|
// 在 default 命令集基础上加 skills(按 meta.json.stack 拉对应 stack 技能)
|
|
@@ -60,7 +63,7 @@ const SCENE_REGISTRARS = {
|
|
|
60
63
|
(0, index_1.registerFileCommands)(p);
|
|
61
64
|
(0, index_3.registerObservabilityCommands)(p);
|
|
62
65
|
(0, index_6.registerSkillsCommands)(p);
|
|
63
|
-
(0,
|
|
66
|
+
(0, index_8.registerRegistryCommands)(p);
|
|
64
67
|
},
|
|
65
68
|
// design scene(AppType_DESIGN=4):design-stack 仅 SSR 渲染、无后端业务逻辑、
|
|
66
69
|
// 无数据库、无 UGC 文件。命令集裁掉 db / file,保留 deploy / observability / app(init) / skills。
|
|
@@ -69,11 +72,11 @@ const SCENE_REGISTRARS = {
|
|
|
69
72
|
(0, index_5.registerDeployCommands)(p);
|
|
70
73
|
(0, index_3.registerObservabilityCommands)(p);
|
|
71
74
|
(0, index_6.registerSkillsCommands)(p);
|
|
72
|
-
(0,
|
|
75
|
+
(0, index_8.registerRegistryCommands)(p);
|
|
73
76
|
},
|
|
74
77
|
// design-html scene(AppType_DESIGN_HTML=8):buildless 静态 HTML,
|
|
75
78
|
// 命令集与 modern 一致(app init/sync + modern 拆分版 deploy + skills),不挂 db/file/observability。
|
|
76
|
-
//
|
|
79
|
+
// comment(应用评论)仅 design-html 开放。
|
|
77
80
|
'design-html': (p) => {
|
|
78
81
|
(0, index_4.registerAppCommands)(p, { includeInit: true });
|
|
79
82
|
(0, modern_1.registerDeployCommandsModern)(p);
|
|
@@ -4,7 +4,8 @@ exports.registerRegistryCommands = registerRegistryCommands;
|
|
|
4
4
|
const shared_1 = require("../../../cli/commands/shared");
|
|
5
5
|
const index_1 = require("../../../cli/handlers/registry/index");
|
|
6
6
|
/**
|
|
7
|
-
* miaoda registry
|
|
7
|
+
* miaoda registry:「组件 registry」(shadcn 式 copy-in),供有对应 registry 包的 stack 使用
|
|
8
|
+
* (当前 design-html / html,见 services/registry 的 STACK_RUNTIME)。
|
|
8
9
|
*
|
|
9
10
|
* registry 是独立 npm 包,按 runtime 命名分发(buildless runtime → @lark-apaas/coding-registry-buildless)。
|
|
10
11
|
* SDK 文件放在包根的 components/ · scripts/ · stylesheets/ 三个目录下(这三个顶层目录里的代码文件
|
|
@@ -15,7 +16,7 @@ const index_1 = require("../../../cli/handlers/registry/index");
|
|
|
15
16
|
function registerRegistryCommands(program) {
|
|
16
17
|
const registryCmd = program
|
|
17
18
|
.command('registry')
|
|
18
|
-
.description('组件 registry:列出 / copy-in
|
|
19
|
+
.description('组件 registry:列出 / copy-in SDK 文件')
|
|
19
20
|
.usage('<command> [flags]');
|
|
20
21
|
registryCmd.action(() => {
|
|
21
22
|
registryCmd.outputHelp();
|
|
@@ -32,7 +33,8 @@ function registerRegistryCommands(program) {
|
|
|
32
33
|
- add 落地:拍平到项目根(包根 scripts/x.js → 项目 x.js);同名 basename 撞车时后者覆盖前者
|
|
33
34
|
|
|
34
35
|
前置
|
|
35
|
-
当前目录(或 --dir)已走过 'miaoda app init'
|
|
36
|
+
当前目录(或 --dir)已走过 'miaoda app init',且 .spark/meta.json 的 stack 有对应
|
|
37
|
+
registry 包(当前支持 design-html / html);其余 stack 执行时报 REGISTRY_STACK_UNSUPPORTED
|
|
36
38
|
`);
|
|
37
39
|
registerRegistryList(registryCmd);
|
|
38
40
|
registerRegistryAdd(registryCmd);
|
|
@@ -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
|
}
|
|
@@ -46,6 +46,29 @@ const api = __importStar(require("../../../api/index"));
|
|
|
46
46
|
const error_1 = require("../../../utils/error");
|
|
47
47
|
const output_1 = require("../../../utils/output");
|
|
48
48
|
const logger_1 = require("../../../utils/logger");
|
|
49
|
+
/** 妙搭应用 ID 形态:`app_` + 字母数字下划线。非此形态按创意应用分享 token 处理。 */
|
|
50
|
+
const MIAODA_APP_ID_PATTERN = /^app_\w{1,64}$/;
|
|
51
|
+
/** 分享 token 宽松形态:16-128 位字母数字(CCM token 实际 20+ 位)。只拦明显不是 token 的输入(误传 flag / 短串),不追平服务端精确口径。 */
|
|
52
|
+
const META_TOKEN_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
|
|
53
|
+
/**
|
|
54
|
+
* 解析位置参数为源应用定位:app_xxx → appId;满足 token 宽松形态的串 → metaToken 透传
|
|
55
|
+
* (精确校验交给服务端反查,避免和服务端口径漂移)。不支持直接传分享链接——调用方
|
|
56
|
+
* (上游 agent)自行从链接提取 /page/<token> 段后传 token。
|
|
57
|
+
*/
|
|
58
|
+
function parseSourceRef(raw) {
|
|
59
|
+
if (MIAODA_APP_ID_PATTERN.test(raw)) {
|
|
60
|
+
return { appId: raw };
|
|
61
|
+
}
|
|
62
|
+
if (!META_TOKEN_PATTERN.test(raw)) {
|
|
63
|
+
throw new error_1.AppError('ARGS_INVALID', `无法识别的源应用定位:${raw}`, {
|
|
64
|
+
next_actions: [
|
|
65
|
+
'接受两种形态:应用 ID(app_xxx)或创意应用分享 token(16 位以上字母数字)',
|
|
66
|
+
'拿到的是分享链接(https://<tenant>.feishu.cn/page/<token>)时,请提取其中 /page/ 后的 <token> 段传入',
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return { metaToken: raw };
|
|
71
|
+
}
|
|
49
72
|
/** 导出目录默认值:创意升级链路的参考物约定位置。 */
|
|
50
73
|
exports.DEFAULT_EXPORT_OUT = 'source_package/creative';
|
|
51
74
|
/** 升级 manifest 文件名:前置钩子幂等标记 / skill workspace-contains 锚点 / skill 第 0 步信息源。 */
|
|
@@ -97,7 +120,7 @@ function countFiles(dir) {
|
|
|
97
120
|
return count;
|
|
98
121
|
}
|
|
99
122
|
/**
|
|
100
|
-
* miaoda app export <app-id> [--out <dir>] [--checkpoint <n>]
|
|
123
|
+
* miaoda app export <app-id-or-token> [--out <dir>] [--checkpoint <n>]
|
|
101
124
|
*
|
|
102
125
|
* 创意应用升级链路:把源应用(创意稿)的源码从服务端导出到本工程,作为只读参考物。
|
|
103
126
|
* 链路:studio_server InnerGetCodeArchive(inner 通道,force 身份鉴权:调用者须为源应用
|
|
@@ -105,15 +128,18 @@ function countFiles(dir) {
|
|
|
105
128
|
* skill workspace-contains 锚点 + skill 第 0 步信息源)。
|
|
106
129
|
*
|
|
107
130
|
* URL 形状:path :appID 是当前沙箱应用(网关强校验其与沙箱 AK 一致),源应用走 query
|
|
108
|
-
* sourceAppID
|
|
131
|
+
* (sourceAppID 或 sourceMetaToken 二选一)—— 跨应用(升级常态:新应用沙箱里导出源
|
|
132
|
+
* 创意应用)必须如此。位置参数接受应用 ID / 分享 token 两种形态。
|
|
109
133
|
*
|
|
110
|
-
* 幂等:--out 已存在 manifest
|
|
134
|
+
* 幂等:--out 已存在 manifest 且源定位(sourceAppId / sourceMetaToken)一致时跳过导出
|
|
135
|
+
* 直接返回(重试/多轮不重复拉取)。
|
|
111
136
|
*/
|
|
112
137
|
async function handleAppExport(opts) {
|
|
113
|
-
const
|
|
114
|
-
if (!
|
|
115
|
-
throw new error_1.AppError('ARGS_INVALID', 'app-id 不能为空');
|
|
138
|
+
const rawSource = (opts.appId || '').trim();
|
|
139
|
+
if (!rawSource) {
|
|
140
|
+
throw new error_1.AppError('ARGS_INVALID', 'app-id-or-token 不能为空');
|
|
116
141
|
}
|
|
142
|
+
const source = parseSourceRef(rawSource);
|
|
117
143
|
// 当前沙箱应用:网关 AK 校验要求 path :appID 与沙箱绑定应用一致
|
|
118
144
|
const currentAppId = (opts.currentAppId || '').trim();
|
|
119
145
|
if (!currentAppId) {
|
|
@@ -124,13 +150,24 @@ async function handleAppExport(opts) {
|
|
|
124
150
|
const outRel = normalizeOutDir(opts.out ?? exports.DEFAULT_EXPORT_OUT);
|
|
125
151
|
const outDir = node_path_1.default.resolve(process.cwd(), outRel);
|
|
126
152
|
const manifestPath = node_path_1.default.join(outDir, exports.UPGRADE_MANIFEST_FILENAME);
|
|
127
|
-
// 幂等:同源应用已导出过则直接复用(前置钩子重试 /
|
|
153
|
+
// 幂等:同源应用已导出过则直接复用(前置钩子重试 / 多轮对话不重复拉取)。
|
|
154
|
+
// token 入参时按 manifest 记录的 sourceMetaToken 比对(导出前拿不到反查后的 appId)。
|
|
128
155
|
if (node_fs_1.default.existsSync(manifestPath)) {
|
|
129
156
|
try {
|
|
130
157
|
const prev = JSON.parse(node_fs_1.default.readFileSync(manifestPath, 'utf-8'));
|
|
131
|
-
|
|
158
|
+
const matched = source.appId
|
|
159
|
+
? prev.sourceAppId === source.appId
|
|
160
|
+
: prev.sourceMetaToken === source.metaToken;
|
|
161
|
+
if (matched) {
|
|
132
162
|
(0, logger_1.log)('export', `Already exported (manifest matched), skip: ${outRel}`);
|
|
133
|
-
(0, output_1.emit)({
|
|
163
|
+
(0, output_1.emit)({
|
|
164
|
+
data: {
|
|
165
|
+
appId: prev.sourceAppId ?? '',
|
|
166
|
+
out: outRel,
|
|
167
|
+
skipped: true,
|
|
168
|
+
reason: 'already_exported',
|
|
169
|
+
},
|
|
170
|
+
});
|
|
134
171
|
return;
|
|
135
172
|
}
|
|
136
173
|
}
|
|
@@ -138,10 +175,11 @@ async function handleAppExport(opts) {
|
|
|
138
175
|
// manifest 损坏视为未导出,走覆盖导出
|
|
139
176
|
}
|
|
140
177
|
}
|
|
141
|
-
(0, logger_1.log)('export', `Fetching code archive of ${appId}...`);
|
|
178
|
+
(0, logger_1.log)('export', `Fetching code archive of ${source.appId ?? 'meta token source'}...`);
|
|
142
179
|
const resp = await api.app.getCodeArchive({
|
|
143
180
|
appID: currentAppId,
|
|
144
|
-
sourceAppID: appId,
|
|
181
|
+
sourceAppID: source.appId,
|
|
182
|
+
sourceMetaToken: source.metaToken,
|
|
145
183
|
checkpointID: opts.checkpoint,
|
|
146
184
|
});
|
|
147
185
|
const fileBase64 = resp.file ?? '';
|
|
@@ -157,9 +195,11 @@ async function handleAppExport(opts) {
|
|
|
157
195
|
if (archive.length > MAX_ARCHIVE_BYTES) {
|
|
158
196
|
throw new error_1.AppError('EXPORT_ARCHIVE_TOO_LARGE', `归档超过大小上限(${String(archive.length)} > ${String(MAX_ARCHIVE_BYTES)} bytes)`);
|
|
159
197
|
}
|
|
198
|
+
// 实际导出的源应用 ID:服务端回传优先(token 入参时为反查结果),appId 入参时兜底自身
|
|
199
|
+
const resolvedSourceAppId = resp.sourceAppID ?? source.appId ?? '';
|
|
160
200
|
// 归档先落临时文件再解压;解压成功前不动已存在的 outDir 内容之外的文件
|
|
161
201
|
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 ?? `${
|
|
202
|
+
const archivePath = node_path_1.default.join(tmpDir, resp.name ?? `${resolvedSourceAppId || 'source'}.zip`);
|
|
163
203
|
try {
|
|
164
204
|
node_fs_1.default.writeFileSync(archivePath, archive);
|
|
165
205
|
node_fs_1.default.mkdirSync(outDir, { recursive: true });
|
|
@@ -177,7 +217,9 @@ async function handleAppExport(opts) {
|
|
|
177
217
|
node_fs_1.default.rmSync(node_path_1.default.join(outDir, '.gitignore'), { force: true });
|
|
178
218
|
const fileCount = countFiles(outDir);
|
|
179
219
|
const manifest = {
|
|
180
|
-
sourceAppId:
|
|
220
|
+
sourceAppId: resolvedSourceAppId,
|
|
221
|
+
// token 入参时记录 token 供幂等比对;appId 入参时不写该字段
|
|
222
|
+
...(source.metaToken ? { sourceMetaToken: source.metaToken } : {}),
|
|
181
223
|
archiveName: resp.name ?? '',
|
|
182
224
|
checkpointId: opts.checkpoint ?? 0,
|
|
183
225
|
exportedAt: new Date().toISOString(),
|
|
@@ -186,6 +228,12 @@ async function handleAppExport(opts) {
|
|
|
186
228
|
node_fs_1.default.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
|
187
229
|
(0, logger_1.log)('export', 'Exported successfully');
|
|
188
230
|
(0, output_1.emit)({
|
|
189
|
-
data: {
|
|
231
|
+
data: {
|
|
232
|
+
appId: resolvedSourceAppId,
|
|
233
|
+
out: outRel,
|
|
234
|
+
fileCount,
|
|
235
|
+
archiveName: manifest.archiveName,
|
|
236
|
+
skipped: false,
|
|
237
|
+
},
|
|
190
238
|
});
|
|
191
239
|
}
|
|
@@ -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');
|