@lark-apaas/miaoda-cli 0.1.33-alpha.2ce4ff9 → 0.1.33-alpha.3cbfb24
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 +1 -1
- package/dist/cli/commands/app/index.js +7 -8
- 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 +12 -23
- package/dist/cli/handlers/skills/sync.js +75 -37
- package/dist/config/sync-configs/design-stack.js +1 -1
- package/dist/config/sync-configs/index.js +3 -1
- package/dist/config/sync-configs/nestjs-react-fullstack.js +4 -4
- package/dist/config/sync-configs/vite-react.js +20 -0
- package/dist/services/app/pack/scan.js +23 -0
- package/dist/services/app/pack/strategies.js +91 -0
- 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/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/build.sh +5 -0
- package/upgrade/templates/vite-react/templates/scripts/build.sh +61 -0
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ miaoda file ls --output json
|
|
|
57
57
|
`miaoda app pack`(默认 `--mode standalone`)把当前应用构建成一份自包含静态产物,可脱离妙搭平台本地打开。
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
|
-
miaoda app pack # → dist/standalone
|
|
60
|
+
miaoda app pack # → dist/standalone.zip(中间目录打完即删)
|
|
61
61
|
miaoda app pack --out dist/share
|
|
62
62
|
miaoda app pack --json
|
|
63
63
|
miaoda app pack --upload-url 'https://tos.example.com/x.zip?sign=...'
|
|
@@ -184,8 +184,7 @@ function registerAppPack(parent) {
|
|
|
184
184
|
.description('打包应用产物。默认 standalone 模式:自包含静态产物(目录 + zip),可脱离妙搭平台本地访问')
|
|
185
185
|
.option('--dir <path>', '项目目录,默认 cwd(需含 .spark/meta.json)')
|
|
186
186
|
.option('--mode <mode>', `打包形态(${index_1.PACK_MODES.join(' / ')})`, index_1.DEFAULT_PACK_MODE)
|
|
187
|
-
.option('--out <dir>', '
|
|
188
|
-
.option('--dev-server <url>', '图片下载的请求前缀,默认沙箱网关 http://localhost:8080')
|
|
187
|
+
.option('--out <dir>', '产物路径(相对项目根),实际产出 <out>.zip', index_1.DEFAULT_PACK_OUT)
|
|
189
188
|
.option('--upload-url <url>', '打包后把 zip 以 PUT 上传到该预签名 URL(http/https);上传成功后清掉本地产物')
|
|
190
189
|
.addHelpText('after', `
|
|
191
190
|
与 'miaoda app export' 的区别(**两个命令语义无关,别选错**)
|
|
@@ -200,13 +199,14 @@ function registerAppPack(parent) {
|
|
|
200
199
|
design-html / html / vite-react / nestjs-react-fullstack
|
|
201
200
|
|
|
202
201
|
产物
|
|
203
|
-
<out
|
|
204
|
-
|
|
202
|
+
<out>.zip **唯一交付物**。内含 index.html + assets/ + images/
|
|
203
|
+
(+ static/,vite 系有 shared/static 时)
|
|
204
|
+
<out>/ 打包用的中间目录,**打完包即删除**——它与 zip 内容重复,
|
|
205
|
+
留着占双份磁盘。要预览请先解压。
|
|
205
206
|
传了 --upload-url 时,zip 会以 PUT 裸 body(Content-Type: application/zip)
|
|
206
207
|
上传到该地址——预签名 URL 的标准形态,鉴权由 URL 自带签名承担,不加额外 header。
|
|
207
|
-
|
|
208
|
-
localRemoved=true
|
|
209
|
-
上传失败则保留本地产物,便于修正后重跑或手动上传。
|
|
208
|
+
**上传成功后本地 zip 也会被清掉**(目录本就已删),交付物以远端为准,
|
|
209
|
+
JSON 里 localRemoved=true。上传失败则保留 zip,便于修正后重跑或手动上传。
|
|
210
210
|
默认 <out> 是 dist/standalone —— 落在 dist/ 下是刻意的:dist 已在部署扫描的
|
|
211
211
|
EXCLUDES 与各模板 .gitignore 中,产物不会污染部署包、不会被误提交。
|
|
212
212
|
代价:下一次构建会 rm -rf dist,导出产物是易失的。
|
|
@@ -249,7 +249,6 @@ JSON 输出
|
|
|
249
249
|
await (0, index_1.handleAppPack)({
|
|
250
250
|
dir: rawOpts.dir,
|
|
251
251
|
out: rawOpts.out,
|
|
252
|
-
devServer: rawOpts.devServer,
|
|
253
252
|
uploadUrl: rawOpts.uploadUrl,
|
|
254
253
|
mode: rawOpts.mode,
|
|
255
254
|
});
|
|
@@ -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)...');
|
|
@@ -37,19 +37,6 @@ function normalizeOutDir(rel) {
|
|
|
37
37
|
}
|
|
38
38
|
/** 沙箱网关地址:它才是代理 `/spark/app` 的那个服务(响应头 `Server: openresty`)。 */
|
|
39
39
|
const SANDBOX_GATEWAY_BASE = 'http://localhost:8080';
|
|
40
|
-
/**
|
|
41
|
-
* 图片下载的请求前缀。
|
|
42
|
-
*
|
|
43
|
-
* **刻意写死 8080,不读 `CLIENT_DEV_PORT`。** 那个 env 表示的是「应用自己的 dev server
|
|
44
|
-
* 监听端口」,与「谁在代理 `/spark/app`」是两回事:design-html / html 用
|
|
45
|
-
* `coding-static-server`(默认 8001,纯静态、完全不代理),读了它会把请求打到静态
|
|
46
|
-
* server 上,稳定 404。真机踩过。
|
|
47
|
-
*/
|
|
48
|
-
function resolveDevServerBase(explicit) {
|
|
49
|
-
if (explicit)
|
|
50
|
-
return explicit.replace(/\/+$/, '');
|
|
51
|
-
return SANDBOX_GATEWAY_BASE;
|
|
52
|
-
}
|
|
53
40
|
/** 遍历产物树里的文本文件,返回 posix 相对路径。 */
|
|
54
41
|
function* walkText(dir, rel = '') {
|
|
55
42
|
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -174,7 +161,7 @@ async function handleAppPack(opts) {
|
|
|
174
161
|
}
|
|
175
162
|
}
|
|
176
163
|
// 3. 下载
|
|
177
|
-
const devServerBase =
|
|
164
|
+
const devServerBase = SANDBOX_GATEWAY_BASE;
|
|
178
165
|
(0, logger_1.log)('pack', `Matched ${String(urls.length)} image url(s), fetching via ${devServerBase}`);
|
|
179
166
|
const { mapping, failed } = await (0, index_1.downloadImages)(urls, {
|
|
180
167
|
imagesDir: node_path_1.default.join(outDir, 'images'),
|
|
@@ -189,17 +176,19 @@ async function handleAppPack(opts) {
|
|
|
189
176
|
...(failed.length > 1
|
|
190
177
|
? [`其余 ${String(failed.length - 1)} 条见 --verbose(每条请求地址都会打到 stderr)`]
|
|
191
178
|
: []),
|
|
192
|
-
`实际请求前缀:${devServerBase}
|
|
193
|
-
'若端口不对,可用 --dev-server http://localhost:<port> 覆盖',
|
|
179
|
+
`实际请求前缀:${devServerBase}(沙箱网关)`,
|
|
194
180
|
],
|
|
195
181
|
});
|
|
196
182
|
}
|
|
197
183
|
// 4. 改写
|
|
198
184
|
const rewrittenFiles = (0, index_1.rewriteTree)(outDir, mapping);
|
|
199
|
-
// 5.
|
|
200
|
-
(0, index_1.zipDir)(outDir, zipPath);
|
|
201
|
-
// 6. 统计。**必须在清理之前**——上传成功后本地产物会被删掉,那时就量不到了。
|
|
185
|
+
// 5. 统计。**必须在打包前做**——目录随后就被删掉,那时量不到了。
|
|
202
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`);
|
|
203
192
|
// 7. 上传(可选)。放在 emit 之前——传了 --upload-url 就说明上传是目的,
|
|
204
193
|
// 失败要让整个命令失败(退出码 1),不能先 emit 成功再抛。
|
|
205
194
|
// JSON 输出里**不回显 URL**(凭证类信息),只给状态码与服务端 log_id。
|
|
@@ -215,10 +204,10 @@ async function handleAppPack(opts) {
|
|
|
215
204
|
let localRemoved = false;
|
|
216
205
|
if (upload) {
|
|
217
206
|
try {
|
|
218
|
-
|
|
207
|
+
// 目录在打包后已删,这里只需清 zip
|
|
219
208
|
node_fs_1.default.rmSync(zipPath, { force: true });
|
|
220
209
|
localRemoved = true;
|
|
221
|
-
(0, logger_1.log)('pack', `Uploaded, local
|
|
210
|
+
(0, logger_1.log)('pack', `Uploaded, local zip removed (${zipRel})`);
|
|
222
211
|
}
|
|
223
212
|
catch (err) {
|
|
224
213
|
warnings.push({
|
|
@@ -248,8 +237,8 @@ async function handleAppPack(opts) {
|
|
|
248
237
|
mode,
|
|
249
238
|
out: outRel,
|
|
250
239
|
zip: zipRel,
|
|
251
|
-
//
|
|
252
|
-
//
|
|
240
|
+
// ⚠️ out 指向的**目录在打包后即被删除**(它只是中间态),永远不要去读它。
|
|
241
|
+
// zip 是本地交付物;localRemoved=true 时连 zip 也被清了(已上传到远端)。
|
|
253
242
|
localRemoved,
|
|
254
243
|
fileCount,
|
|
255
244
|
bytes,
|
|
@@ -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',
|
|
@@ -23,13 +23,15 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
23
23
|
const node_path_1 = __importDefault(require("node:path"));
|
|
24
24
|
const nestjs_react_fullstack_1 = __importDefault(require("./nestjs-react-fullstack"));
|
|
25
25
|
const design_stack_1 = __importDefault(require("./design-stack"));
|
|
26
|
+
const vite_react_1 = __importDefault(require("./vite-react"));
|
|
26
27
|
/**
|
|
27
|
-
* 已纳入新 sync 机制的 stack 注册表。未在表里的 stack
|
|
28
|
+
* 已纳入新 sync 机制的 stack 注册表。未在表里的 stack(如 html / design-html)走旧的
|
|
28
29
|
* `upgrade/templates/<stack>/{files,patches}` 机制,由 sync handler 兜底。
|
|
29
30
|
*/
|
|
30
31
|
const STACK_REGISTRY = {
|
|
31
32
|
'nestjs-react-fullstack': nestjs_react_fullstack_1.default,
|
|
32
33
|
'design-stack': design_stack_1.default,
|
|
34
|
+
'vite-react': vite_react_1.default,
|
|
33
35
|
};
|
|
34
36
|
/** 返回该 stack 的 SyncConfig;表里没有时返回 null。 */
|
|
35
37
|
function getSyncConfig(stack) {
|
|
@@ -135,8 +135,8 @@ exports.SYNC_CONFIG = {
|
|
|
135
135
|
ifStartsWith: 'concurrently ',
|
|
136
136
|
},
|
|
137
137
|
// 10. 把老 `npm run upgrade && ./scripts/dev.sh` 形式迁移到 `./scripts/dev.sh`,
|
|
138
|
-
// 让本地 `npm run dev` 不再前置跑 fullstack-cli sync。新版 dev.sh 按
|
|
139
|
-
//
|
|
138
|
+
// 让本地 `npm run dev` 不再前置跑 fullstack-cli sync。新版 dev.sh 按 MIAODA_DEP_CACHE_DIR
|
|
139
|
+
// 是否非空判分支:沙箱直接 exec dev.js(脚本同步由平台 pod 启动时做过,dev 入口不再 upgrade);
|
|
140
140
|
// 本地走 miaoda app sync 兜底 + dev-local.js。
|
|
141
141
|
{
|
|
142
142
|
type: 'patch-script',
|
|
@@ -160,8 +160,8 @@ exports.SYNC_CONFIG = {
|
|
|
160
160
|
ifStartsWith: 'npx -y @lark-apaas/fullstack-cli sync',
|
|
161
161
|
},
|
|
162
162
|
// ===== miaoda-cli 本地开发新增规则(fullstack-cli sync 不会执行) =====
|
|
163
|
-
// M1. scripts.dev:local ——
|
|
164
|
-
// dev.sh 在
|
|
163
|
+
// M1. scripts.dev:local —— 本地用户绕过沙箱判定直接跑本地链路(npm run dev:local)。
|
|
164
|
+
// dev.sh 在 MIAODA_DEP_CACHE_DIR 非空时跑 dev.js(沙箱保活)、否则 exec dev-local.js;显式 dev:local
|
|
165
165
|
// 用于 agent 在本地稳定调试,不受任何 env 影响。
|
|
166
166
|
{
|
|
167
167
|
type: 'add-script',
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* vite-react sync 规则(镜像 fullstack-cli 的 viteReactProfile):只同步 build.sh 一支,
|
|
4
|
+
* 回溯 public/* → dist/output/ 修复。
|
|
5
|
+
* ⚠️ build.sh 源须与 miaoda-coding 模板、fullstack-cli `templates/vite-react/build.sh` 逐字一致。
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.SYNC_CONFIG = void 0;
|
|
9
|
+
exports.SYNC_CONFIG = {
|
|
10
|
+
sync: [
|
|
11
|
+
// 覆盖 scripts/build.sh:回溯 public/* → dist/output/(同源根 /app/<appId>/*)修复
|
|
12
|
+
{
|
|
13
|
+
type: 'file',
|
|
14
|
+
from: 'scripts/build.sh',
|
|
15
|
+
to: 'scripts/build.sh',
|
|
16
|
+
overwrite: true,
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
exports.default = exports.SYNC_CONFIG;
|
|
@@ -53,6 +53,25 @@ function isSameOriginRelative(text, index) {
|
|
|
53
53
|
return true;
|
|
54
54
|
return BOUNDARY_CHARS.has(text[index - 1]);
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* aPaaS 存储路径特征段。命中其一才认为是「应用存储里的资源」。
|
|
58
|
+
*
|
|
59
|
+
* **为什么必须限定**:只按 `/spark/app/` 前缀匹配会把**接口地址**一并扫走 ——
|
|
60
|
+
* 真机踩过,SDK 里的 `/spark/app/<id>/runtime/api/v1/observability/logs/collect`、
|
|
61
|
+
* `.../permissions/roles` 四条接口进了下载队列,全部 404 后被报成「4 张图片下载失败」。
|
|
62
|
+
*
|
|
63
|
+
* 这三段与 `miaoda-coding` 的 `cd-elements/src/core/tos.ts` 的 `SRC_ALLOWLIST` 同一份。
|
|
64
|
+
*/
|
|
65
|
+
const STORAGE_PATH_SEGMENTS = [
|
|
66
|
+
'/runtime/api/v1/storage/object/',
|
|
67
|
+
'/aily/api/v1/feisuda/attachments/',
|
|
68
|
+
'/aily/api/v1/files/static/',
|
|
69
|
+
];
|
|
70
|
+
/**
|
|
71
|
+
* 模板字符串插值标记:`/spark/app/${e}/...`。压缩后的 SDK 里大量存在,它们**不是具体 URL**
|
|
72
|
+
* ——变量在运行时才求值,照原样请求必然 404。
|
|
73
|
+
*/
|
|
74
|
+
const TEMPLATE_INTERPOLATION = '${';
|
|
56
75
|
/**
|
|
57
76
|
* 扫出去重后的 URL 列表,保持首次出现的顺序(便于测试断言与日志可读)。
|
|
58
77
|
*/
|
|
@@ -63,6 +82,10 @@ function scanSparkAppUrls(text) {
|
|
|
63
82
|
if (!isSameOriginRelative(text, m.index))
|
|
64
83
|
continue;
|
|
65
84
|
const url = m[0];
|
|
85
|
+
if (url.includes(TEMPLATE_INTERPOLATION))
|
|
86
|
+
continue;
|
|
87
|
+
if (!STORAGE_PATH_SEGMENTS.some((seg) => url.includes(seg)))
|
|
88
|
+
continue;
|
|
66
89
|
if (seen.has(url))
|
|
67
90
|
continue;
|
|
68
91
|
seen.add(url);
|
|
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.PACK_SUPPORTED_STACKS = void 0;
|
|
7
7
|
exports.buildStandaloneEnv = buildStandaloneEnv;
|
|
8
8
|
exports.liftNestedEntry = liftNestedEntry;
|
|
9
|
+
exports.copySharedStatic = copySharedStatic;
|
|
10
|
+
exports.pruneStaticNoise = pruneStaticNoise;
|
|
9
11
|
exports.resolveStrategy = resolveStrategy;
|
|
10
12
|
const node_child_process_1 = require("node:child_process");
|
|
11
13
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
@@ -45,6 +47,12 @@ function buildStandaloneEnv(base) {
|
|
|
45
47
|
// `/assets/x.js` 绝对路径,file:// 下指向文件系统根,页面白屏(真机踩过)。
|
|
46
48
|
// vite-react 那边 build.sh 会用同样的值覆盖它,两条路结果一致。
|
|
47
49
|
env.ASSETS_CDN_PATH = './';
|
|
50
|
+
// shared/static 的 URL 前缀。同样要**两个名字都给**:
|
|
51
|
+
// vite-react 走 build.sh,里面 `export STATIC_ASSETS_BASE_URL="${MIAODA_STATIC_CDN_PREFIX}"`
|
|
52
|
+
// 会无条件覆盖直接注入的值 —— 只给 STATIC_ASSETS_BASE_URL 会被冲成空,
|
|
53
|
+
// 插件退化成绝对路径 `/avatar.jpg`,file:// 下必挂(真机踩过)。
|
|
54
|
+
// nestjs 走 build:client 直连 vite,没有那层映射,只认 STATIC_ASSETS_BASE_URL。
|
|
55
|
+
env.MIAODA_STATIC_CDN_PREFIX = './static';
|
|
48
56
|
env.STATIC_ASSETS_BASE_URL = './static';
|
|
49
57
|
return env;
|
|
50
58
|
}
|
|
@@ -109,6 +117,86 @@ function dropSourceMaps(outDir) {
|
|
|
109
117
|
};
|
|
110
118
|
walk(outDir);
|
|
111
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* 把 `shared/static/` 拷进产物的 `static/`。
|
|
122
|
+
*
|
|
123
|
+
* `shared/static/*` 不是 vite 的产物 —— 插件只把它们解析成 URL 字符串,真实文件靠
|
|
124
|
+
* 构建脚本搬。vite-react 的 `scripts/build.sh` 会 rsync 到 `dist/output_static`,
|
|
125
|
+
* 但 **nestjs 的 build.sh 完全没有这段**,而我们又绕过它直接跑 `build:client`,
|
|
126
|
+
* 所以没人搬 —— 产物里引用了 `./static/avatar.jpg` 却没有这个文件(真机踩过)。
|
|
127
|
+
*
|
|
128
|
+
* 排除规则对齐 vite-react build.sh:跳过 `.ts/.tsx/.js/.jsx`(那些是代码不是资源,
|
|
129
|
+
* 由 bundler 处理)。JSON 无需排除:它走 `window.__STATIC_JSON__` 内联,多拷一份无害。
|
|
130
|
+
*
|
|
131
|
+
* 只负责「搬」,模板 README 的剔除交给 `pruneStaticNoise` —— vite-react 那条路径
|
|
132
|
+
* 的 static/ 是 build.sh 搬的、根本不经过这里,规则放这儿会漏掉它(真机漏过)。
|
|
133
|
+
*/
|
|
134
|
+
const SHARED_STATIC_SKIP_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx']);
|
|
135
|
+
function copySharedStatic(projectDir, outDir) {
|
|
136
|
+
const src = node_path_1.default.join(projectDir, 'shared', 'static');
|
|
137
|
+
if (!node_fs_1.default.existsSync(src))
|
|
138
|
+
return;
|
|
139
|
+
const dest = node_path_1.default.join(outDir, 'static');
|
|
140
|
+
const walk = (dir, rel) => {
|
|
141
|
+
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
142
|
+
const abs = node_path_1.default.join(dir, entry.name);
|
|
143
|
+
const childRel = rel ? node_path_1.default.join(rel, entry.name) : entry.name;
|
|
144
|
+
if (entry.isDirectory()) {
|
|
145
|
+
walk(abs, childRel);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (SHARED_STATIC_SKIP_EXTENSIONS.has(node_path_1.default.extname(entry.name).toLowerCase())) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const to = node_path_1.default.join(dest, childRel);
|
|
152
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(to), { recursive: true });
|
|
153
|
+
node_fs_1.default.copyFileSync(abs, to);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
walk(src, '');
|
|
157
|
+
}
|
|
158
|
+
const STATIC_NOISE_FILENAME = 'readme.md';
|
|
159
|
+
/**
|
|
160
|
+
* 从产物的 `static/` 里剔掉模板自带的 README.md,并清掉因此空掉的目录。
|
|
161
|
+
*
|
|
162
|
+
* 模板给 `shared/static/` 配了一份 README.md 讲这个目录怎么用(跟 `src/assets`、
|
|
163
|
+
* `public` 的区别、为什么要走 SDK 拿带 token 的 URL),是给开发者看的文档不是应用
|
|
164
|
+
* 资源。多数应用没往这个目录放东西,产物里就只剩这一个 README 孤零零躺着。
|
|
165
|
+
*
|
|
166
|
+
* **两条路径都要过这里**:nestjs 侧 static/ 由 `copySharedStatic` 搬,vite-react 侧
|
|
167
|
+
* 由 `scripts/build.sh` rsync 到 `dist/output_static` 再 merge 进来——build.sh 的
|
|
168
|
+
* `--exclude` 只排了四个代码扩展名,README 照样进产物。规则只写在搬运侧会漏掉另一条。
|
|
169
|
+
*
|
|
170
|
+
* 只排 `README.md` 这一个名字(大小写不敏感)。其余 `.md` 照留——应用拿 markdown
|
|
171
|
+
* 当资源(说明书、协议文本)是合理用法,不该替它做主。
|
|
172
|
+
*/
|
|
173
|
+
function pruneStaticNoise(outDir) {
|
|
174
|
+
const root = node_path_1.default.join(outDir, 'static');
|
|
175
|
+
if (!node_fs_1.default.existsSync(root))
|
|
176
|
+
return;
|
|
177
|
+
// 返回该目录清理后是否已空,空则连目录一起删(空目录进 zip 同样是噪音)
|
|
178
|
+
const prune = (dir) => {
|
|
179
|
+
let kept = 0;
|
|
180
|
+
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
181
|
+
const abs = node_path_1.default.join(dir, entry.name);
|
|
182
|
+
if (entry.isDirectory()) {
|
|
183
|
+
if (!prune(abs))
|
|
184
|
+
kept += 1;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (entry.name.toLowerCase() === STATIC_NOISE_FILENAME) {
|
|
188
|
+
node_fs_1.default.rmSync(abs, { force: true });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
kept += 1;
|
|
192
|
+
}
|
|
193
|
+
if (kept > 0)
|
|
194
|
+
return false;
|
|
195
|
+
node_fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
196
|
+
return true;
|
|
197
|
+
};
|
|
198
|
+
prune(root);
|
|
199
|
+
}
|
|
112
200
|
/** 从产物树里删掉 routes.json —— 平台/server 路由用,standalone 无人读取。 */
|
|
113
201
|
function dropRoutesJson(outDir) {
|
|
114
202
|
node_fs_1.default.rmSync(node_path_1.default.join(outDir, 'routes.json'), { force: true });
|
|
@@ -162,6 +250,7 @@ const viteReactStrategy = ({ projectDir, outDir }) => {
|
|
|
162
250
|
mergeInto(node_path_1.default.join(dist, 'output'), outDir);
|
|
163
251
|
mergeInto(node_path_1.default.join(dist, 'output_resource', 'assets'), node_path_1.default.join(outDir, 'assets'));
|
|
164
252
|
mergeInto(node_path_1.default.join(dist, 'output_static'), node_path_1.default.join(outDir, 'static'));
|
|
253
|
+
pruneStaticNoise(outDir);
|
|
165
254
|
dropSourceMaps(outDir);
|
|
166
255
|
dropRoutesJson(outDir);
|
|
167
256
|
};
|
|
@@ -196,6 +285,8 @@ const nestjsStrategy = ({ projectDir, outDir }) => {
|
|
|
196
285
|
node_fs_1.default.rmSync(outDir, { recursive: true, force: true });
|
|
197
286
|
node_fs_1.default.mkdirSync(outDir, { recursive: true });
|
|
198
287
|
mergeInto(tmpDist, outDir);
|
|
288
|
+
copySharedStatic(projectDir, outDir);
|
|
289
|
+
pruneStaticNoise(outDir);
|
|
199
290
|
}
|
|
200
291
|
finally {
|
|
201
292
|
// 临时构建目录不留在用户项目里
|