@lark-apaas/miaoda-cli 0.1.35 → 0.1.37
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 +23 -0
- package/dist/cli/commands/app/index.js +81 -0
- package/dist/cli/handlers/app/index.js +6 -1
- package/dist/cli/handlers/app/init.js +10 -2
- package/dist/cli/handlers/app/migrate.js +2 -4
- package/dist/cli/handlers/app/pack.js +256 -0
- package/dist/cli/handlers/skills/sync.js +75 -37
- package/dist/config/sync-configs/design-stack.js +1 -1
- package/dist/config/sync-configs/nestjs-react-fullstack.js +4 -4
- package/dist/services/app/pack/archive.js +35 -0
- package/dist/services/app/pack/copy-tree.js +51 -0
- package/dist/services/app/pack/download.js +52 -0
- package/dist/services/app/pack/index.js +27 -0
- package/dist/services/app/pack/inline-babel.js +91 -0
- package/dist/services/app/pack/naming.js +65 -0
- package/dist/services/app/pack/rewrite.js +80 -0
- package/dist/services/app/pack/scan.js +95 -0
- package/dist/services/app/pack/strategies.js +313 -0
- package/dist/services/app/pack/upload.js +110 -0
- package/dist/services/deploy/modern/patch/source-scan.js +3 -40
- package/dist/utils/coding-steering.js +200 -6
- package/dist/utils/dir-lock.js +103 -0
- package/dist/utils/env.js +3 -3
- package/dist/utils/exclude-patterns.js +49 -0
- package/dist/utils/file-ops.js +43 -0
- package/dist/utils/npm-pack.js +30 -0
- package/dist/utils/sandbox-skills.js +56 -60
- package/package.json +1 -1
- package/upgrade/templates/nestjs-react-fullstack/templates/scripts/lint.js +22 -7
package/README.md
CHANGED
|
@@ -48,9 +48,32 @@ miaoda file ls --output json
|
|
|
48
48
|
| `miaoda observability ...` | 线上日志、链路、前端源码堆栈反查、监控指标、运营指标 |
|
|
49
49
|
| `miaoda comment ...` | 应用评论:查看评论列表、解决评论(仅 design-html 场景开放) |
|
|
50
50
|
| `miaoda registry ...` | 组件 registry:列出 / copy-in SDK 文件(本地执行) |
|
|
51
|
+
| `miaoda app ...` | 应用生命周期:元数据、初始化、平台同步、源码导出、静态产物导出 |
|
|
51
52
|
|
|
52
53
|
完整命令通过 `miaoda --help` 或 `miaoda <domain> --help` 查看。
|
|
53
54
|
|
|
55
|
+
## 打包可本地访问的静态产物
|
|
56
|
+
|
|
57
|
+
`miaoda app pack`(默认 `--mode standalone`)把当前应用构建成一份自包含静态产物,可脱离妙搭平台本地打开。
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
miaoda app pack # → dist/standalone.zip(中间目录打完即删)
|
|
61
|
+
miaoda app pack --out dist/share
|
|
62
|
+
miaoda app pack --json
|
|
63
|
+
miaoda app pack --upload-url 'https://tos.example.com/x.zip?sign=...'
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
支持 `design-html` / `html` / `vite-react` / `nestjs-react-fullstack`。页面与代码里字面以
|
|
67
|
+
`/spark/app` 开头的存储图片会经沙箱网关(localhost:8080)下载到 `images/` 并改写成相对路径。
|
|
68
|
+
|
|
69
|
+
默认输出落在 `dist/` 下是刻意的:`dist` 已在部署扫描的 EXCLUDES 与各模板 `.gitignore` 中,
|
|
70
|
+
产物不会污染部署包、不会被误提交;代价是下一次构建会清掉它。
|
|
71
|
+
|
|
72
|
+
**只保证纯静态可看** —— 接口调用会失败,不做数据快照。多路由 SPA 在 `file://` 下不可用
|
|
73
|
+
(`BrowserRouter` 匹配不上路径),需用本地 HTTP server 打开,命令会在 `warnings` 里提示。
|
|
74
|
+
|
|
75
|
+
> 与 `miaoda app export` **语义无关**:后者拉的是**其他应用**的源码归档(创意应用升级链路的只读参考物)。
|
|
76
|
+
|
|
54
77
|
## 组件 registry
|
|
55
78
|
|
|
56
79
|
`miaoda registry` 提供 shadcn 式的「组件 registry」:把 SDK 文件**拷进**当前项目(copy-in),而非作为依赖安装。registry 是独立 npm 包,按运行时分发(design-html / html → buildless runtime → `@lark-apaas/coding-registry-buildless`)。
|
|
@@ -22,6 +22,7 @@ function registerAppCommands(program, opts = {}) {
|
|
|
22
22
|
registerAppGet(appCmd);
|
|
23
23
|
registerAppUpdate(appCmd);
|
|
24
24
|
registerAppExport(appCmd);
|
|
25
|
+
registerAppPack(appCmd);
|
|
25
26
|
if (opts.includeInit) {
|
|
26
27
|
registerAppInit(appCmd);
|
|
27
28
|
registerAppSync(appCmd);
|
|
@@ -127,6 +128,10 @@ function registerAppExport(parent) {
|
|
|
127
128
|
.option('--out <dir>', '输出目录(相对当前目录)', 'source_package/creative')
|
|
128
129
|
.option('--checkpoint <n>', '按检查点版本导出;缺省为最新提交(HEAD)')
|
|
129
130
|
.addHelpText('after', `
|
|
131
|
+
与 'miaoda app pack' 的区别(**两个命令语义无关,别选错**)
|
|
132
|
+
app export 拉「其他应用」的源码归档(本命令)
|
|
133
|
+
app pack 把「当前应用」打包成产物(默认 standalone 形态)
|
|
134
|
+
|
|
130
135
|
说明
|
|
131
136
|
从服务端导出源应用(可以不是当前应用)的源码归档并解压到 --out 目录,导出完成后写
|
|
132
137
|
--out/.upgrade-manifest.json(含 sourceAppId / 导出时间 / 文件数),供升级链路幂等
|
|
@@ -173,6 +178,82 @@ JSON 输出
|
|
|
173
178
|
});
|
|
174
179
|
}));
|
|
175
180
|
}
|
|
181
|
+
function registerAppPack(parent) {
|
|
182
|
+
const cmd = parent
|
|
183
|
+
.command('pack')
|
|
184
|
+
.description('打包应用产物。默认 standalone 模式:自包含静态产物(目录 + zip),可脱离妙搭平台本地访问')
|
|
185
|
+
.option('--dir <path>', '项目目录,默认 cwd(需含 .spark/meta.json)')
|
|
186
|
+
.option('--mode <mode>', `打包形态(${index_1.PACK_MODES.join(' / ')})`, index_1.DEFAULT_PACK_MODE)
|
|
187
|
+
.option('--out <dir>', '产物路径(相对项目根),实际产出 <out>.zip', index_1.DEFAULT_PACK_OUT)
|
|
188
|
+
.option('--upload-url <url>', '打包后把 zip 以 PUT 上传到该预签名 URL(http/https);上传成功后清掉本地产物')
|
|
189
|
+
.addHelpText('after', `
|
|
190
|
+
与 'miaoda app export' 的区别(**两个命令语义无关,别选错**)
|
|
191
|
+
app export 拉「其他应用」的源码归档(创意应用升级链路的只读参考物)
|
|
192
|
+
app pack 把「当前应用」打包成产物(本命令)
|
|
193
|
+
|
|
194
|
+
打包形态(--mode)
|
|
195
|
+
standalone(默认) 自包含静态产物,可脱离妙搭平台本地访问
|
|
196
|
+
后续新增形态时在此列出;不传 --mode 的行为永远等同 standalone。
|
|
197
|
+
|
|
198
|
+
支持的 stack
|
|
199
|
+
design-html / html / vite-react / nestjs-react-fullstack
|
|
200
|
+
|
|
201
|
+
产物
|
|
202
|
+
<out>.zip **唯一交付物**。内含 index.html + assets/ + images/
|
|
203
|
+
(+ static/,vite 系有 shared/static 时)
|
|
204
|
+
<out>/ 打包用的中间目录,**打完包即删除**——它与 zip 内容重复,
|
|
205
|
+
留着占双份磁盘。要预览请先解压。
|
|
206
|
+
传了 --upload-url 时,zip 会以 PUT 裸 body(Content-Type: application/zip)
|
|
207
|
+
上传到该地址——预签名 URL 的标准形态,鉴权由 URL 自带签名承担,不加额外 header。
|
|
208
|
+
**上传成功后本地 zip 也会被清掉**(目录本就已删),交付物以远端为准,
|
|
209
|
+
JSON 里 localRemoved=true。上传失败则保留 zip,便于修正后重跑或手动上传。
|
|
210
|
+
默认 <out> 是 dist/standalone —— 落在 dist/ 下是刻意的:dist 已在部署扫描的
|
|
211
|
+
EXCLUDES 与各模板 .gitignore 中,产物不会污染部署包、不会被误提交。
|
|
212
|
+
代价:下一次构建会 rm -rf dist,导出产物是易失的。
|
|
213
|
+
|
|
214
|
+
图片本地化
|
|
215
|
+
只处理页面/代码里字面以 /spark/app 开头的 URL,经沙箱网关(localhost:8080)下载到
|
|
216
|
+
<out>/images/ 并改写成相对路径。其它挂载前缀(/spark/p/、/app/<id>、反代)不处理。
|
|
217
|
+
接口运行时返回的 download_url 属动态 URL,静态扫描覆盖不到。
|
|
218
|
+
|
|
219
|
+
已知限制
|
|
220
|
+
只保证纯静态可看——接口调用会失败,不做数据快照。
|
|
221
|
+
多路由 SPA 在 file:// 下不可用(BrowserRouter 匹配不上路径、pushState 抛 SecurityError),
|
|
222
|
+
请用本地 HTTP server 打开;检测到时会在 warnings 里给 SPA_ROUTER_FILE_PROTOCOL。
|
|
223
|
+
design-html 的 cd-elements SDK 是 CDN 外链,断网打开自定义元素不渲染。
|
|
224
|
+
|
|
225
|
+
JSON 输出
|
|
226
|
+
{"data": {"appId": "...", "stack": "...", "out": "dist/standalone", "zip": "dist/standalone.zip",
|
|
227
|
+
"fileCount": N, "bytes": N,
|
|
228
|
+
"images": {"matched": N, "downloaded": N, "failed": [{"url": "...", "reason": "..."}]},
|
|
229
|
+
"localRemoved": false,
|
|
230
|
+
"rewrittenFiles": [...],
|
|
231
|
+
"warnings": [{"code": "...", "message": "..."}]}}
|
|
232
|
+
|
|
233
|
+
失败
|
|
234
|
+
单张图片下载失败 → 跳过并计入 images.failed + warnings,退出码 0。
|
|
235
|
+
命中 > 0 但全部失败 → PACK_IMAGE_ALL_FAILED,退出码 1(系统性问题)。
|
|
236
|
+
stack 不支持 → PACK_STACK_UNSUPPORTED。
|
|
237
|
+
构建失败 → PACK_BUILD_FAILED。
|
|
238
|
+
上传失败(非 2xx / 网络异常)→ PACK_UPLOAD_FAILED,退出码 1。
|
|
239
|
+
本地产物保留,可修正 URL 后重跑或手动上传。
|
|
240
|
+
--upload-url 非 http/https → ARGS_INVALID,退出码 2。
|
|
241
|
+
|
|
242
|
+
示例
|
|
243
|
+
$ miaoda app pack
|
|
244
|
+
$ miaoda app pack --out dist/share
|
|
245
|
+
$ miaoda app pack --json
|
|
246
|
+
$ miaoda app pack --upload-url 'https://tos.example.com/x.zip?sign=...'
|
|
247
|
+
`);
|
|
248
|
+
cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
|
|
249
|
+
await (0, index_1.handleAppPack)({
|
|
250
|
+
dir: rawOpts.dir,
|
|
251
|
+
out: rawOpts.out,
|
|
252
|
+
uploadUrl: rawOpts.uploadUrl,
|
|
253
|
+
mode: rawOpts.mode,
|
|
254
|
+
});
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
176
257
|
function registerAppGet(parent) {
|
|
177
258
|
const cmd = parent
|
|
178
259
|
.command('get')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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;
|
|
3
|
+
exports.PACK_MODES = exports.DEFAULT_PACK_MODE = exports.DEFAULT_PACK_OUT = exports.handleAppPack = 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");
|
|
@@ -19,3 +19,8 @@ Object.defineProperty(exports, "handleAppMigrate", { enumerable: true, get: func
|
|
|
19
19
|
// commands 层渲染 help 时需要这份枚举;从 handler barrel 转发,避免 commands → services 越界
|
|
20
20
|
var index_1 = require("../../../services/app/init/index");
|
|
21
21
|
Object.defineProperty(exports, "SUPPORTED_STACKS", { enumerable: true, get: function () { return index_1.SUPPORTED_STACKS; } });
|
|
22
|
+
var pack_1 = require("./pack");
|
|
23
|
+
Object.defineProperty(exports, "handleAppPack", { enumerable: true, get: function () { return pack_1.handleAppPack; } });
|
|
24
|
+
Object.defineProperty(exports, "DEFAULT_PACK_OUT", { enumerable: true, get: function () { return pack_1.DEFAULT_PACK_OUT; } });
|
|
25
|
+
Object.defineProperty(exports, "DEFAULT_PACK_MODE", { enumerable: true, get: function () { return pack_1.DEFAULT_PACK_MODE; } });
|
|
26
|
+
Object.defineProperty(exports, "PACK_MODES", { enumerable: true, get: function () { return pack_1.PACK_MODES; } });
|
|
@@ -7,6 +7,7 @@ exports.handleAppInit = handleAppInit;
|
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const index_1 = require("../../../services/app/init/index");
|
|
10
|
+
const dir_lock_1 = require("../../../utils/dir-lock");
|
|
10
11
|
const coding_steering_1 = require("../../../utils/coding-steering");
|
|
11
12
|
const logger_1 = require("../../../utils/logger");
|
|
12
13
|
const githooks_1 = require("../../../utils/githooks");
|
|
@@ -86,16 +87,23 @@ async function handleAppInit(opts) {
|
|
|
86
87
|
version: 'unknown',
|
|
87
88
|
syncedSkills: [],
|
|
88
89
|
techSynced: false,
|
|
90
|
+
prunedStacks: [],
|
|
89
91
|
};
|
|
90
92
|
let steeringError;
|
|
91
93
|
if (scaffoldSteering) {
|
|
92
94
|
try {
|
|
93
|
-
|
|
95
|
+
// 与会话期的 `skills sync` 互斥:两者都写 `.agent/skills`,而写入策略是按目录
|
|
96
|
+
// 整清再拷。线上实测过这条竞态 —— init 的 steering 同步撞上 sync 收尾的
|
|
97
|
+
// `chmod -R a=rX`,报 `EACCES: mkdir '.agent/skills/steering/<stack>/skills'`。
|
|
98
|
+
steeringResult = (0, dir_lock_1.withDirLock)(targetDir, () => (0, coding_steering_1.syncCodingSteering)({
|
|
94
99
|
stack,
|
|
95
100
|
targetDir,
|
|
96
101
|
logPrefix: 'init',
|
|
97
102
|
outputLayout,
|
|
98
|
-
|
|
103
|
+
// match-template-version 门禁的比对基准:用刚渲染的 template 实际版本,
|
|
104
|
+
// 与随后写入 .spark/meta.json 的 version 同源(见下方 writeSparkMeta)
|
|
105
|
+
templateVersion: tplResult.version,
|
|
106
|
+
}));
|
|
99
107
|
}
|
|
100
108
|
catch (err) {
|
|
101
109
|
steeringError = err instanceof Error ? err.message : String(err);
|
|
@@ -144,12 +144,10 @@ async function handleAppMigrate(opts) {
|
|
|
144
144
|
// ignoreOutdatedRequests fix 注释)。直接 rm -rf 让新 vite 从干净状态 cold
|
|
145
145
|
// optimize, 一次写到位 lockfile 一致的 metadata, 不再触发中途 re-optimize。
|
|
146
146
|
//
|
|
147
|
-
// -
|
|
147
|
+
// - 只在沙箱内做(isSandboxEnv;本地一般不会撞这条链路)
|
|
148
148
|
// - 只在 install 成功时做(install 挂了 node_modules 状态不对, 清 cache 也救不回来)
|
|
149
149
|
// - 软失败:.vite 不存在 / 删失败都不阻断
|
|
150
|
-
if (installError === undefined &&
|
|
151
|
-
process.env.SANDBOX_ID !== undefined &&
|
|
152
|
-
process.env.SANDBOX_ID !== '') {
|
|
150
|
+
if (installError === undefined && (0, env_1.isSandboxEnv)()) {
|
|
153
151
|
const viteCacheDir = node_path_1.default.join(targetDir, 'node_modules', '.vite');
|
|
154
152
|
if (node_fs_1.default.existsSync(viteCacheDir)) {
|
|
155
153
|
(0, logger_1.log)('migrate', '清理 node_modules/.vite (vite cache 跟新 lockfile 错位会 504)...');
|
|
@@ -0,0 +1,256 @@
|
|
|
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.DEFAULT_PACK_OUT = exports.DEFAULT_PACK_MODE = exports.PACK_MODES = void 0;
|
|
7
|
+
exports.handleAppPack = handleAppPack;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const exclude_patterns_1 = require("../../../utils/exclude-patterns");
|
|
11
|
+
const spark_meta_1 = require("../../../utils/spark-meta");
|
|
12
|
+
const error_1 = require("../../../utils/error");
|
|
13
|
+
const output_1 = require("../../../utils/output");
|
|
14
|
+
const logger_1 = require("../../../utils/logger");
|
|
15
|
+
const index_1 = require("../../../services/app/pack/index");
|
|
16
|
+
/**
|
|
17
|
+
* 支持的打包形态。当前只有 `standalone`;留成枚举是为了让扩展点显式可见 ——
|
|
18
|
+
* 新增形态时只需往这里加,命令 help 会自动列出。
|
|
19
|
+
*
|
|
20
|
+
* ⚠️ 不传 `--mode` 的行为永远等同 `standalone`,这是对调用方的稳定契约:
|
|
21
|
+
* 将来加了新形态也不能改默认值,否则存量调用会静默换语义。
|
|
22
|
+
*/
|
|
23
|
+
exports.PACK_MODES = ['standalone'];
|
|
24
|
+
exports.DEFAULT_PACK_MODE = 'standalone';
|
|
25
|
+
/** standalone 形态的默认输出目录。新增形态时各自用自己的目录名。 */
|
|
26
|
+
exports.DEFAULT_PACK_OUT = 'dist/standalone';
|
|
27
|
+
/** 校验相对输出路径:禁绝对路径、禁含 ".." 段(与 app export 的 normalizeOutDir 同口径)。 */
|
|
28
|
+
function normalizeOutDir(rel) {
|
|
29
|
+
if (node_path_1.default.isAbsolute(rel) || rel.startsWith('/')) {
|
|
30
|
+
throw new error_1.AppError('ARGS_INVALID', `输出目录不能是绝对路径:${rel}`);
|
|
31
|
+
}
|
|
32
|
+
const posix = rel.split(node_path_1.default.sep).join('/').replace(/^\.\//, '');
|
|
33
|
+
if (posix.split('/').includes('..')) {
|
|
34
|
+
throw new error_1.AppError('ARGS_INVALID', `输出目录不能包含 "..":${rel}`);
|
|
35
|
+
}
|
|
36
|
+
return posix;
|
|
37
|
+
}
|
|
38
|
+
/** 沙箱网关地址:它才是代理 `/spark/app` 的那个服务(响应头 `Server: openresty`)。 */
|
|
39
|
+
const SANDBOX_GATEWAY_BASE = 'http://localhost:8080';
|
|
40
|
+
/** 遍历产物树里的文本文件,返回 posix 相对路径。 */
|
|
41
|
+
function* walkText(dir, rel = '') {
|
|
42
|
+
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
43
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
44
|
+
const abs = node_path_1.default.join(dir, entry.name);
|
|
45
|
+
if (entry.isDirectory())
|
|
46
|
+
yield* walkText(abs, childRel);
|
|
47
|
+
else if (index_1.TEXT_EXTENSIONS.has(node_path_1.default.extname(entry.name).toLowerCase()))
|
|
48
|
+
yield childRel;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** 统计产物树的文件数与总字节数。 */
|
|
52
|
+
function measure(dir) {
|
|
53
|
+
let fileCount = 0;
|
|
54
|
+
let bytes = 0;
|
|
55
|
+
const walk = (cur) => {
|
|
56
|
+
for (const entry of node_fs_1.default.readdirSync(cur, { withFileTypes: true })) {
|
|
57
|
+
const abs = node_path_1.default.join(cur, entry.name);
|
|
58
|
+
if (entry.isDirectory())
|
|
59
|
+
walk(abs);
|
|
60
|
+
else {
|
|
61
|
+
fileCount += 1;
|
|
62
|
+
bytes += node_fs_1.default.statSync(abs).size;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
walk(dir);
|
|
67
|
+
return { fileCount, bytes };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 扫**项目源码**里是否用了 BrowserRouter。
|
|
71
|
+
*
|
|
72
|
+
* preset 在 standalone 下会把它编译期换成 HashRouter(`standaloneRouterPlugin`),
|
|
73
|
+
* 换完 file:// 下路由完全可用。这里保留检测是为了**覆盖不到时给提示**:插件只替换
|
|
74
|
+
* 用户源码里字面的 `BrowserRouter` / `createBrowserRouter`,碰不到的情况有——
|
|
75
|
+
* 用户二次封装(`export const Router = BrowserRouter`)、非 react-router 的路由库、
|
|
76
|
+
* 代码里硬编码 `window.location.href = '/about'`。
|
|
77
|
+
*
|
|
78
|
+
* 命中时只是提示"若路由异常请用本地 server",不代表一定坏。
|
|
79
|
+
*
|
|
80
|
+
* ⚠️ **必须扫源码,不能扫产物**:编译型 stack 的产物经过 minify,`BrowserRouter`
|
|
81
|
+
* 这个标识符会被改名,扫产物永远命中不了 —— 而那恰恰是最需要这条警告的场景。
|
|
82
|
+
* 真机踩过:vite-react 应用源码里明明有 BrowserRouter、产物首屏渲染成 404,
|
|
83
|
+
* 但因为扫的是产物,warnings 是空的。
|
|
84
|
+
*
|
|
85
|
+
* 扫描面套 EXCLUDES(跳过 node_modules / dist 等),四个 stack 统一按项目根扫。
|
|
86
|
+
*/
|
|
87
|
+
function detectBrowserRouter(projectDir) {
|
|
88
|
+
const walk = (dir) => {
|
|
89
|
+
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
90
|
+
if (exclude_patterns_1.EXCLUDES.has(entry.name))
|
|
91
|
+
continue;
|
|
92
|
+
const abs = node_path_1.default.join(dir, entry.name);
|
|
93
|
+
let isDir;
|
|
94
|
+
try {
|
|
95
|
+
isDir = node_fs_1.default.statSync(abs).isDirectory();
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (isDir) {
|
|
101
|
+
if (walk(abs))
|
|
102
|
+
return true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (!index_1.TEXT_EXTENSIONS.has(node_path_1.default.extname(entry.name).toLowerCase()))
|
|
106
|
+
continue;
|
|
107
|
+
if (/\b(BrowserRouter|createBrowserRouter)\b/.test(node_fs_1.default.readFileSync(abs, 'utf-8'))) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
};
|
|
113
|
+
return walk(projectDir);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* miaoda app pack [--mode standalone] [--out <dir>]
|
|
117
|
+
*
|
|
118
|
+
* 把当前应用构建成自包含静态产物:按 stack 走各自的 strategy 产出可静态服务的目录树,
|
|
119
|
+
* 再扫出 `/spark/app` 图片经 dev server 代理下载到 `images/` 并改写成相对路径,最后打 zip。
|
|
120
|
+
*
|
|
121
|
+
* 设计文档:docs/superpowers/specs/2026-08-19-app-pack-design.md
|
|
122
|
+
*/
|
|
123
|
+
async function handleAppPack(opts) {
|
|
124
|
+
const mode = opts.mode ?? exports.DEFAULT_PACK_MODE;
|
|
125
|
+
if (!exports.PACK_MODES.includes(mode)) {
|
|
126
|
+
throw new error_1.AppError('ARGS_INVALID', `不支持的打包形态:${mode}`, {
|
|
127
|
+
next_actions: [`可用形态:${exports.PACK_MODES.join(', ')}`],
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const projectDir = node_path_1.default.resolve(opts.dir ?? process.cwd());
|
|
131
|
+
const meta = (0, spark_meta_1.readSparkMeta)(projectDir);
|
|
132
|
+
if (!meta.stack) {
|
|
133
|
+
throw new error_1.AppError('ARGS_INVALID', '.spark/meta.json 缺少 stack —— 请先跑 miaoda app init', {
|
|
134
|
+
next_actions: ['cd 到应用根目录后重试,或先执行 miaoda app init'],
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const strategy = (0, index_1.resolveStrategy)(meta.stack);
|
|
138
|
+
// 参数校验一律前置:URL 写错不该等到构建 + 下载 + 打包跑完才报(那是几十秒)
|
|
139
|
+
if (opts.uploadUrl)
|
|
140
|
+
(0, index_1.assertValidUploadUrl)(opts.uploadUrl);
|
|
141
|
+
const outRel = normalizeOutDir(opts.out ?? exports.DEFAULT_PACK_OUT);
|
|
142
|
+
const outDir = node_path_1.default.join(projectDir, outRel);
|
|
143
|
+
const zipRel = `${outRel}.zip`;
|
|
144
|
+
const zipPath = node_path_1.default.join(projectDir, zipRel);
|
|
145
|
+
// 1. stack-specific:产出可静态服务的目录树
|
|
146
|
+
strategy({ projectDir, outDir });
|
|
147
|
+
if (!node_fs_1.default.existsSync(outDir) || node_fs_1.default.readdirSync(outDir).length === 0) {
|
|
148
|
+
throw new error_1.AppError('PACK_EMPTY_OUTPUT', '构建产物为空', {
|
|
149
|
+
next_actions: ['先手动跑一次该 stack 的构建命令确认工程本身可构建'],
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
// 2. 扫描
|
|
153
|
+
const urls = [];
|
|
154
|
+
const seen = new Set();
|
|
155
|
+
for (const rel of walkText(outDir)) {
|
|
156
|
+
for (const url of (0, index_1.scanSparkAppUrls)(node_fs_1.default.readFileSync(node_path_1.default.join(outDir, rel), 'utf-8'))) {
|
|
157
|
+
if (seen.has(url))
|
|
158
|
+
continue;
|
|
159
|
+
seen.add(url);
|
|
160
|
+
urls.push(url);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// 3. 下载
|
|
164
|
+
const devServerBase = SANDBOX_GATEWAY_BASE;
|
|
165
|
+
(0, logger_1.log)('pack', `Matched ${String(urls.length)} image url(s), fetching via ${devServerBase}`);
|
|
166
|
+
const { mapping, failed } = await (0, index_1.downloadImages)(urls, {
|
|
167
|
+
imagesDir: node_path_1.default.join(outDir, 'images'),
|
|
168
|
+
devServerBase,
|
|
169
|
+
});
|
|
170
|
+
// 命中 > 0 且全部失败 = 系统性问题(代理挂了 / 路径口径错),静默成功会误导调用方
|
|
171
|
+
if (urls.length > 0 && mapping.size === 0) {
|
|
172
|
+
throw new error_1.AppError('PACK_IMAGE_ALL_FAILED', `命中 ${String(urls.length)} 张图片但全部下载失败`, {
|
|
173
|
+
next_actions: [
|
|
174
|
+
'确认 dev server 正在运行且能代理 /spark/app 路径',
|
|
175
|
+
`首个失败:${failed[0]?.url ?? '?'} → ${failed[0]?.reason ?? 'unknown'}`,
|
|
176
|
+
...(failed.length > 1
|
|
177
|
+
? [`其余 ${String(failed.length - 1)} 条见 --verbose(每条请求地址都会打到 stderr)`]
|
|
178
|
+
: []),
|
|
179
|
+
`实际请求前缀:${devServerBase}(沙箱网关)`,
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// 4. 改写
|
|
184
|
+
const rewrittenFiles = (0, index_1.rewriteTree)(outDir, mapping);
|
|
185
|
+
// 5. 统计。**必须在打包前做**——目录随后就被删掉,那时量不到了。
|
|
186
|
+
const { fileCount, bytes } = measure(outDir);
|
|
187
|
+
// 6. 打包,随后删掉目录。目录是**中间态**,zip 才是交付物;两者内容重复,
|
|
188
|
+
// 留着等于占双份磁盘。预览需先解压。
|
|
189
|
+
(0, index_1.zipDir)(outDir, zipPath);
|
|
190
|
+
node_fs_1.default.rmSync(outDir, { recursive: true, force: true });
|
|
191
|
+
(0, logger_1.log)('pack', `Packed ${zipRel}, staging dir removed`);
|
|
192
|
+
// 7. 上传(可选)。放在 emit 之前——传了 --upload-url 就说明上传是目的,
|
|
193
|
+
// 失败要让整个命令失败(退出码 1),不能先 emit 成功再抛。
|
|
194
|
+
// JSON 输出里**不回显 URL**(凭证类信息),只给状态码与服务端 log_id。
|
|
195
|
+
const upload = opts.uploadUrl ? await (0, index_1.uploadZip)(zipPath, opts.uploadUrl) : undefined;
|
|
196
|
+
const warnings = [];
|
|
197
|
+
// 8. 上传成功后清掉本地产物:交付物已在远端,沙箱磁盘不必留一份目录 + 一份 zip。
|
|
198
|
+
//
|
|
199
|
+
// **只在上传成功后清**。上传失败时产物必须留着——错误提示里明确写了「本地产物已保留,
|
|
200
|
+
// 可修正 --upload-url 后重跑,或手动上传」,清掉会让那句话变成谎话,且用户得重跑
|
|
201
|
+
// 整轮构建。失败路径走 uploadZip 抛错,根本到不了这里。
|
|
202
|
+
//
|
|
203
|
+
// 清理失败不算命令失败:上传已经成功,目的达到了,删不掉只是留了点垃圾 → 降级为 warning。
|
|
204
|
+
let localRemoved = false;
|
|
205
|
+
if (upload) {
|
|
206
|
+
try {
|
|
207
|
+
// 目录在打包后已删,这里只需清 zip
|
|
208
|
+
node_fs_1.default.rmSync(zipPath, { force: true });
|
|
209
|
+
localRemoved = true;
|
|
210
|
+
(0, logger_1.log)('pack', `Uploaded, local zip removed (${zipRel})`);
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
warnings.push({
|
|
214
|
+
code: 'LOCAL_CLEANUP_FAILED',
|
|
215
|
+
message: `上传成功但本地产物清理失败:${err instanceof Error ? err.message : String(err)}`,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (failed.length > 0) {
|
|
220
|
+
warnings.push({
|
|
221
|
+
code: 'IMAGE_DOWNLOAD_PARTIAL_FAILURE',
|
|
222
|
+
message: `${String(failed.length)} 张图片下载失败,产物中保留原始绝对路径(离线打不开)`,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (detectBrowserRouter(projectDir)) {
|
|
226
|
+
warnings.push({
|
|
227
|
+
code: 'SPA_ROUTER_FILE_PROTOCOL',
|
|
228
|
+
message: '检测到 BrowserRouter:standalone 构建已自动换成 HashRouter,路由形如 index.html#/about。' +
|
|
229
|
+
'若你对 router 做过二次封装或用了非 react-router 的方案,替换可能覆盖不到 —— ' +
|
|
230
|
+
'此时产物在 file:// 下路由会失效(首屏落 404 页),请改用本地 HTTP server 并从 / 访问。',
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
(0, output_1.emit)({
|
|
234
|
+
data: {
|
|
235
|
+
appId: meta.app_id ?? '',
|
|
236
|
+
stack: meta.stack,
|
|
237
|
+
mode,
|
|
238
|
+
out: outRel,
|
|
239
|
+
zip: zipRel,
|
|
240
|
+
// ⚠️ out 指向的**目录在打包后即被删除**(它只是中间态),永远不要去读它。
|
|
241
|
+
// zip 是本地交付物;localRemoved=true 时连 zip 也被清了(已上传到远端)。
|
|
242
|
+
localRemoved,
|
|
243
|
+
fileCount,
|
|
244
|
+
bytes,
|
|
245
|
+
images: {
|
|
246
|
+
matched: urls.length,
|
|
247
|
+
downloaded: mapping.size,
|
|
248
|
+
failed,
|
|
249
|
+
},
|
|
250
|
+
rewrittenFiles,
|
|
251
|
+
// 只在传了 --upload-url 时出现:{ status, logId? }。刻意不含 URL。
|
|
252
|
+
...(upload ? { upload } : {}),
|
|
253
|
+
warnings,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
}
|
|
@@ -9,6 +9,7 @@ const coding_steering_1 = require("../../../utils/coding-steering");
|
|
|
9
9
|
const sandbox_skills_1 = require("../../../utils/sandbox-skills");
|
|
10
10
|
const file_ops_1 = require("../../../utils/file-ops");
|
|
11
11
|
const spark_meta_1 = require("../../../utils/spark-meta");
|
|
12
|
+
const dir_lock_1 = require("../../../utils/dir-lock");
|
|
12
13
|
const error_1 = require("../../../utils/error");
|
|
13
14
|
const output_1 = require("../../../utils/output");
|
|
14
15
|
/**
|
|
@@ -24,24 +25,35 @@ const output_1 = require("../../../utils/output");
|
|
|
24
25
|
*/
|
|
25
26
|
function syncSteeringForSandbox(params) {
|
|
26
27
|
let stack;
|
|
28
|
+
// 提到 try 外:下面 syncCodingSteering 要用它做 match-template-version 门禁
|
|
29
|
+
let templateVersion;
|
|
27
30
|
try {
|
|
28
31
|
const meta = (0, spark_meta_1.readSparkMeta)(params.targetDir);
|
|
29
32
|
if (meta.stack === undefined || meta.stack === '' || meta.stack === 'null') {
|
|
30
33
|
return { status: 'skipped', reason: '.spark/meta.json missing stack' };
|
|
31
34
|
}
|
|
32
35
|
stack = meta.stack;
|
|
36
|
+
templateVersion = meta.version;
|
|
33
37
|
}
|
|
34
38
|
catch (err) {
|
|
35
39
|
return { status: 'skipped', reason: `read .spark/meta.json failed: ${err.message}` };
|
|
36
40
|
}
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
// 版本口径:--version > lane(TCC tgzKey)> BOE 的 dist-tag dev > latest。
|
|
42
|
+
//
|
|
43
|
+
// **这里不预先把 BOE 解成 'dev'**(下方本地分支为了回报 versionSource 才那么做):
|
|
44
|
+
// 一旦提前填进 version,就会盖住 lane —— version 在优先级里高于 lane,BOE 沙箱上
|
|
45
|
+
// 配了 tgzKey 也永远命中不了。把 version 原样透传(未指定就是 undefined),让
|
|
46
|
+
// syncCodingSteering 一处算全链:version > lane > dev > latest。
|
|
40
47
|
try {
|
|
41
48
|
const r = (0, coding_steering_1.syncCodingSteering)({
|
|
42
49
|
stack,
|
|
43
50
|
targetDir: params.targetDir,
|
|
44
|
-
version:
|
|
51
|
+
version: params.version,
|
|
52
|
+
// TCC 的 X 与平铺 skill 共用:pickLaneVersion 只匹配 `-alpha.<X>` 后缀,
|
|
53
|
+
// 不看 base 版本,所以一个 key 同时定向 coding-steering 与 sandbox-skills。
|
|
54
|
+
tgzKey: params.tgzKey,
|
|
55
|
+
// match-template-version 门禁的比对基准(老 update-skills.sh 读的同一个字段)
|
|
56
|
+
templateVersion,
|
|
45
57
|
// 必须显式传 mode,不能落到 inferMode():它靠 env MIAODA_DEP_CACHE_DIR 嗅探,
|
|
46
58
|
// 而 mode 决定读包内哪个 layer(sandbox→`skills/`,local→`skills_local/`)。
|
|
47
59
|
// `--type` 分支本身就是沙箱语义,嗅探失手会去读不存在的 skills_local/、
|
|
@@ -56,6 +68,7 @@ function syncSteeringForSandbox(params) {
|
|
|
56
68
|
version: r.version,
|
|
57
69
|
syncedSkills: r.syncedSkills,
|
|
58
70
|
techSynced: r.techSynced,
|
|
71
|
+
prunedStacks: r.prunedStacks,
|
|
59
72
|
};
|
|
60
73
|
}
|
|
61
74
|
catch (err) {
|
|
@@ -70,9 +83,16 @@ function syncSteeringForSandbox(params) {
|
|
|
70
83
|
*
|
|
71
84
|
* 版本来源优先级(从高到低):
|
|
72
85
|
* 1. CLI `--version` flag(显式指定)
|
|
73
|
-
* 2. `
|
|
86
|
+
* 2. lane:`--type` 分支下由 TCC `skillsUpdateTgzKey` 透传 `--tgz-key`,反推 X 后
|
|
87
|
+
* 解析出 `-alpha.<X>` 版本。**与平铺 skill 共用同一个 X** —— 匹配只看后缀、
|
|
88
|
+
* 不看 base 版本,所以 TCC 配一个 key 就同时定向 coding-steering 与 sandbox-skills。
|
|
89
|
+
* 本地分支(不带 `--type`)没有 tgzKey,这一档天然不参与。
|
|
90
|
+
* 3. `FORCE_FRAMEWORK_ENVIRONMENT=boe` → dist-tag `dev`(BOE 测试灰度,跟随
|
|
74
91
|
* publish-alpha.sh 在 develop 分支打的 dev tag,便于在 BOE 沙箱里验证预发 steering)
|
|
75
|
-
*
|
|
92
|
+
* 4. 默认 `latest`
|
|
93
|
+
*
|
|
94
|
+
* 2/3/4 档统一在 `syncCodingSteering` 里算(见那边注释),handler 只负责透传,避免
|
|
95
|
+
* 提前把 `dev` 填进 version 把 lane 挤掉。
|
|
76
96
|
*
|
|
77
97
|
* **不读 meta.json**——steering 版本不在 meta 里钉。
|
|
78
98
|
*/
|
|
@@ -95,41 +115,57 @@ async function handleSkillsSync(opts) {
|
|
|
95
115
|
// 冻结只冻结了一半。
|
|
96
116
|
if (opts.type !== undefined && opts.type !== '') {
|
|
97
117
|
const { type, tgzKey } = (0, sandbox_skills_1.validateSandboxSyncOpts)({ type: opts.type, tgzKey: opts.tgzKey });
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
},
|
|
118
|
+
// 并发锁覆盖「平铺同步 + steering 同步 + 收尾 chmod」整段:写入策略是按目录整清再拷,
|
|
119
|
+
// 两个实例并发会互相看到半清空状态,而 feida-ai 的 scanAllSkills 是会话期按需扫盘。
|
|
120
|
+
// 对齐老 update-skills.sh:157-159 的 flock。
|
|
121
|
+
(0, dir_lock_1.withDirLock)(targetDir, () => {
|
|
122
|
+
syncSandboxAndSteering({ opts, targetDir, type, tgzKey });
|
|
129
123
|
});
|
|
130
124
|
return;
|
|
131
125
|
}
|
|
132
126
|
// 原有 coding-steering 同步逻辑(本地 dev / miaoda app init 用)
|
|
127
|
+
(0, dir_lock_1.withDirLock)(targetDir, () => {
|
|
128
|
+
syncSteeringOnly({ opts, targetDir });
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function syncSandboxAndSteering(params) {
|
|
132
|
+
const { opts, targetDir, type, tgzKey } = params;
|
|
133
|
+
const result = (0, sandbox_skills_1.syncSandboxSkills)({ type, tgzKey, targetDir });
|
|
134
|
+
const steering = result.skippedByKeepFile
|
|
135
|
+
? { status: 'skipped', reason: '.keep file found, sync frozen' }
|
|
136
|
+
: syncSteeringForSandbox({ targetDir, version: opts.version, tgzKey });
|
|
137
|
+
// ③ 收尾锁只读(update-skills.sh:241 的 chmod -R a=rX)。必须在 ①② 全部写完之后,
|
|
138
|
+
// 锁上就写不进去了。`.keep` 冻结时不锁 —— 老链路那条路径是 exit 0 直接走、
|
|
139
|
+
// 末尾的 chmod 压根不执行,保持一致。
|
|
140
|
+
//
|
|
141
|
+
// **只锁沙箱侧**:这里的 result.targetDir 只会是 `.agent/skills` 或 `skills`
|
|
142
|
+
// (见 sandbox-skills 的 skillsRoot),都是平台下发的资产根。本地 `--local` 走
|
|
143
|
+
// flat layout 输出到 `.agents/`,压根不经过本分支,权限不会被碰。
|
|
144
|
+
//
|
|
145
|
+
// 回退风险由解锁侧兜:权限是持久化在磁盘的,代码能回退、磁盘状态不会。所以
|
|
146
|
+
// file-ops 的 unlockDirForWrite 在**两处写入前**都调用了(syncSandboxSkills 的
|
|
147
|
+
// 写入前 + syncCodingSteering 的 nested 分支写入前),任一入口都不会因为
|
|
148
|
+
// 上一轮的锁而 EACCES。改动这里之前先确认那两处解锁还在。
|
|
149
|
+
const lockedReadonly = result.skippedByKeepFile ? false : (0, file_ops_1.lockDirReadonly)(result.targetDir);
|
|
150
|
+
(0, output_1.emit)({
|
|
151
|
+
data: {
|
|
152
|
+
mode: 'sandbox',
|
|
153
|
+
type,
|
|
154
|
+
packageName: result.packageName,
|
|
155
|
+
version: result.version,
|
|
156
|
+
lane: result.lane,
|
|
157
|
+
syncedTop: result.syncedTop,
|
|
158
|
+
deleted: result.deleted,
|
|
159
|
+
skippedByKeepFile: result.skippedByKeepFile,
|
|
160
|
+
lockedReadonly,
|
|
161
|
+
targetDir: result.targetDir,
|
|
162
|
+
steering,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/** 不带 --type 的本地 / init 形态:只同步 coding-steering。 */
|
|
167
|
+
function syncSteeringOnly(params) {
|
|
168
|
+
const { opts, targetDir } = params;
|
|
133
169
|
const meta = (0, spark_meta_1.readSparkMeta)(targetDir);
|
|
134
170
|
if (meta.stack === undefined || meta.stack === '') {
|
|
135
171
|
throw new error_1.AppError('SKILLS_META_INCOMPLETE', '.spark/meta.json missing stack — run `miaoda app init` first');
|
|
@@ -141,6 +177,7 @@ async function handleSkillsSync(opts) {
|
|
|
141
177
|
stack: meta.stack,
|
|
142
178
|
targetDir,
|
|
143
179
|
version: effectiveVersion,
|
|
180
|
+
templateVersion: meta.version,
|
|
144
181
|
outputLayout: opts.local === true ? 'flat' : 'nested',
|
|
145
182
|
});
|
|
146
183
|
(0, output_1.emit)({
|
|
@@ -150,6 +187,7 @@ async function handleSkillsSync(opts) {
|
|
|
150
187
|
versionSource,
|
|
151
188
|
syncedSkills: result.syncedSkills,
|
|
152
189
|
techSynced: result.techSynced,
|
|
190
|
+
prunedStacks: result.prunedStacks,
|
|
153
191
|
...(result.claudeSkillsLink !== undefined
|
|
154
192
|
? { claudeSkillsLink: result.claudeSkillsLink }
|
|
155
193
|
: {}),
|
|
@@ -91,7 +91,7 @@ exports.SYNC_CONFIG = {
|
|
|
91
91
|
ifStartsWith: 'npm run upgrade && ',
|
|
92
92
|
},
|
|
93
93
|
// ===== miaoda-cli 本地开发新增规则 =====
|
|
94
|
-
// M1. scripts.dev:local ——
|
|
94
|
+
// M1. scripts.dev:local —— 本地用户绕过沙箱判定直接跑本地链路(npm run dev:local)。
|
|
95
95
|
{
|
|
96
96
|
type: 'add-script',
|
|
97
97
|
name: 'dev:local',
|