@lark-apaas/miaoda-cli 0.1.36 → 0.1.38

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.
Files changed (31) hide show
  1. package/README.md +23 -0
  2. package/dist/cli/commands/app/index.js +81 -0
  3. package/dist/cli/handlers/app/index.js +6 -1
  4. package/dist/cli/handlers/app/init.js +10 -2
  5. package/dist/cli/handlers/app/migrate.js +2 -4
  6. package/dist/cli/handlers/app/pack.js +256 -0
  7. package/dist/cli/handlers/skills/sync.js +56 -31
  8. package/dist/config/sync-configs/design-stack.js +1 -1
  9. package/dist/config/sync-configs/index.js +3 -1
  10. package/dist/config/sync-configs/nestjs-react-fullstack.js +4 -4
  11. package/dist/config/sync-configs/vite-react.js +20 -0
  12. package/dist/services/app/pack/archive.js +35 -0
  13. package/dist/services/app/pack/copy-tree.js +51 -0
  14. package/dist/services/app/pack/download.js +52 -0
  15. package/dist/services/app/pack/index.js +27 -0
  16. package/dist/services/app/pack/inline-babel.js +91 -0
  17. package/dist/services/app/pack/naming.js +65 -0
  18. package/dist/services/app/pack/rewrite.js +80 -0
  19. package/dist/services/app/pack/scan.js +95 -0
  20. package/dist/services/app/pack/strategies.js +313 -0
  21. package/dist/services/app/pack/upload.js +110 -0
  22. package/dist/services/deploy/modern/patch/source-scan.js +3 -40
  23. package/dist/utils/coding-steering.js +179 -5
  24. package/dist/utils/dir-lock.js +103 -0
  25. package/dist/utils/env.js +3 -3
  26. package/dist/utils/exclude-patterns.js +49 -0
  27. package/dist/utils/file-ops.js +43 -0
  28. package/dist/utils/sandbox-skills.js +47 -30
  29. package/package.json +1 -1
  30. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/build.sh +5 -0
  31. package/upgrade/templates/vite-react/templates/scripts/build.sh +61 -0
@@ -0,0 +1,313 @@
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.PACK_SUPPORTED_STACKS = void 0;
7
+ exports.buildStandaloneEnv = buildStandaloneEnv;
8
+ exports.liftNestedEntry = liftNestedEntry;
9
+ exports.copySharedStatic = copySharedStatic;
10
+ exports.pruneStaticNoise = pruneStaticNoise;
11
+ exports.resolveStrategy = resolveStrategy;
12
+ const node_child_process_1 = require("node:child_process");
13
+ const node_fs_1 = __importDefault(require("node:fs"));
14
+ const node_path_1 = __importDefault(require("node:path"));
15
+ const error_1 = require("../../../utils/error");
16
+ const output_1 = require("../../../utils/output");
17
+ const exclude_patterns_1 = require("../../../utils/exclude-patterns");
18
+ const logger_1 = require("../../../utils/logger");
19
+ const copy_tree_1 = require("./copy-tree");
20
+ const inline_babel_1 = require("./inline-babel");
21
+ exports.PACK_SUPPORTED_STACKS = new Set([
22
+ 'design-html',
23
+ 'html',
24
+ 'vite-react',
25
+ 'nestjs-react-fullstack',
26
+ ]);
27
+ /**
28
+ * standalone 构建 env。
29
+ *
30
+ * **不复用 `deploy/modern/atoms/build.ts` 的 `runBuild()`** —— 它的 `ensureHttpsScheme()`
31
+ * 会把 `'./'` 判成域名(含 `.` 且不以 `/` 开头)拼成 `https://./`,破掉 vite 的 base 替换。
32
+ *
33
+ * 删 `MIAODA_APP_ID` 是刻意的:`scripts/build.sh` 写的是
34
+ * `export CLIENT_BASE_PATH="${MIAODA_APP_ID:+/app/$MIAODA_APP_ID}"`,
35
+ * 变量为空时整个展开为空串,正好得到我们要的空 base path。
36
+ */
37
+ function buildStandaloneEnv(base) {
38
+ const env = { ...base };
39
+ delete env.MIAODA_APP_ID;
40
+ delete env.CLIENT_BASE_PATH;
41
+ env.MIAODA_BUILD_TARGET = 'standalone';
42
+ env.NODE_ENV = 'production';
43
+ // vite-react 走 build.sh,由它把 MIAODA_RESOURCE_CDN_PREFIX 映射成 ASSETS_CDN_PATH
44
+ env.MIAODA_RESOURCE_CDN_PREFIX = './';
45
+ // nestjs 走 `npm run build:client` 直接调 vite,**没有 build.sh 那层映射** ——
46
+ // 必须直接给 preset 认的名字,否则 publicPath 为空 → base '/' → 产物里是
47
+ // `/assets/x.js` 绝对路径,file:// 下指向文件系统根,页面白屏(真机踩过)。
48
+ // vite-react 那边 build.sh 会用同样的值覆盖它,两条路结果一致。
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';
56
+ env.STATIC_ASSETS_BASE_URL = './static';
57
+ return env;
58
+ }
59
+ /** 把 `from` 目录内容并入 `into`(into 已存在,不清空)。from 不存在则静默跳过。 */
60
+ function mergeInto(from, into) {
61
+ if (!node_fs_1.default.existsSync(from))
62
+ return;
63
+ node_fs_1.default.mkdirSync(into, { recursive: true });
64
+ node_fs_1.default.cpSync(from, into, { recursive: true });
65
+ }
66
+ /**
67
+ * 把 `<out>/client/index.html` 提到 `<out>/index.html`,并把资源引用降一级。
68
+ *
69
+ * **为什么会嵌一层**:vite 按 input 相对 root 的位置放产物 HTML。nestjs 模板的
70
+ * input 是 `client/index.html`、root 是项目根,所以 HTML 落在 `dist/client/client/`。
71
+ * 平台构建里 `vite-html-output` 插件负责把它提上来,但 standalone 下**刻意不挂**那个
72
+ * 插件——它的上移不修相对引用(平台模式 base 是绝对路径,不需要修)。
73
+ *
74
+ * **为什么必须同时改路径**:standalone 的 base 是 `./`,产物里写的是 `../assets/x`,
75
+ * 那是相对 `<out>/client/` 算的。提到 `<out>/` 后就多了一级,指向不存在的
76
+ * `<out>/../assets/`,页面白屏(实测确认)。层级差恒为 1,故 `../` → `./`。
77
+ *
78
+ * 只改 `src` / `href` 属性开头的 `../`,不做全文替换 —— 否则 inline 脚本里的字符串
79
+ * 常量(如 `var p = "../keep/me"`)会被误伤。
80
+ */
81
+ function liftNestedEntry(outDir) {
82
+ const nestedDir = node_path_1.default.join(outDir, 'client');
83
+ const nestedHtml = node_path_1.default.join(nestedDir, 'index.html');
84
+ if (!node_fs_1.default.existsSync(nestedHtml))
85
+ return;
86
+ const target = node_path_1.default.join(outDir, 'index.html');
87
+ const html = node_fs_1.default.readFileSync(nestedHtml, 'utf-8');
88
+ const rebased = html.replace(/\b(src|href)=("|')\.\.\/(?!\.)/gi, (_m, attr, quote) => `${attr}=${quote}./`);
89
+ node_fs_1.default.writeFileSync(target, rebased, 'utf-8');
90
+ node_fs_1.default.rmSync(nestedHtml, { force: true });
91
+ // 只删空目录:嵌套目录里若还有别的文件,留着比静默丢弃安全
92
+ if (node_fs_1.default.readdirSync(nestedDir).length === 0)
93
+ node_fs_1.default.rmdirSync(nestedDir);
94
+ }
95
+ /**
96
+ * 从产物树里删掉 sourcemap(`*.map`)。
97
+ *
98
+ * preset 的 prod 配置是 `sourcemap: 'hidden'` —— map 文件会产出,但产物里**没有**
99
+ * `sourceMappingURL` 注释,即没人引用它。留在 standalone 包里有两个实打实的问题:
100
+ *
101
+ * 1. **体积**:实测真实应用 4.7MB map / 5.9MB 总体积,占 80%
102
+ * 2. **泄源码**:map 的 `sourcesContent` 内嵌原始源码(实测含 12 个业务文件,
103
+ * 如 `src/lib/utils.ts`)。standalone 包是拿来分发给人的,等于附赠全部源码
104
+ *
105
+ * 只对有编译步骤的 stack 调用;buildless 栈(design-html / html)的产物就是源码,
106
+ * 那里的 `.map`(若有)是用户自己的文件,不该动。
107
+ */
108
+ function dropSourceMaps(outDir) {
109
+ const walk = (dir) => {
110
+ for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
111
+ const abs = node_path_1.default.join(dir, entry.name);
112
+ if (entry.isDirectory())
113
+ walk(abs);
114
+ else if (entry.name.toLowerCase().endsWith('.map'))
115
+ node_fs_1.default.rmSync(abs, { force: true });
116
+ }
117
+ };
118
+ walk(outDir);
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
+ }
200
+ /** 从产物树里删掉 routes.json —— 平台/server 路由用,standalone 无人读取。 */
201
+ function dropRoutesJson(outDir) {
202
+ node_fs_1.default.rmSync(node_path_1.default.join(outDir, 'routes.json'), { force: true });
203
+ }
204
+ function runProjectBuild(projectDir, command) {
205
+ try {
206
+ (0, node_child_process_1.execSync)(command, {
207
+ cwd: projectDir,
208
+ // JSON 模式下把子进程 stdout 重定向到父进程 stderr(fd 2)——构建工具会往
209
+ // stdout 打一大堆进度,会把最终 emit 的 JSON 信封冲掉,下游 parse 直接失败。
210
+ // 终端仍看得到进度(走 stderr)。与 app init 的 stdioFor 同一套做法。
211
+ stdio: (0, output_1.isJsonMode)() ? ['ignore', 2, 'inherit'] : 'inherit',
212
+ env: buildStandaloneEnv(process.env),
213
+ });
214
+ }
215
+ catch (err) {
216
+ throw new error_1.AppError('PACK_BUILD_FAILED', `${command} failed: ${err.message}`, {
217
+ next_actions: ['先手动跑一次该构建命令确认工程本身可构建'],
218
+ });
219
+ }
220
+ }
221
+ /** design-html:项目根纯拷贝,套 deploy 同一份 EXCLUDES。 */
222
+ const designHtmlStrategy = ({ projectDir, outDir }) => {
223
+ (0, logger_1.log)('pack', 'Copying source (design-html)...');
224
+ (0, copy_tree_1.copyTree)(projectDir, outDir, exclude_patterns_1.EXCLUDES);
225
+ (0, inline_babel_1.inlineBabelScripts)(outDir);
226
+ dropRoutesJson(outDir);
227
+ };
228
+ /**
229
+ * html:拷 `src/`,**跳过 `coding-html-devserver build`**。
230
+ * 该命令会注入 slardar/tea 外链与 `{{appName}}` HBS 占位符,standalone 全要撤销,
231
+ * 调它再逐项撤销不如不调(设计文档「html 跳过 coding-html-devserver build」)。
232
+ */
233
+ const htmlStrategy = ({ projectDir, outDir }) => {
234
+ (0, logger_1.log)('pack', 'Copying src/ (html)...');
235
+ (0, copy_tree_1.copyTree)(node_path_1.default.join(projectDir, 'src'), outDir);
236
+ (0, inline_babel_1.inlineBabelScripts)(outDir);
237
+ dropRoutesJson(outDir);
238
+ };
239
+ /**
240
+ * vite-react:跑 `scripts/build.sh`,再把按上传通道拆的四个目录按可访问性合并。
241
+ * `output_capabilities` 丢弃 —— 平台侧注册用,运行时走已打进 bundle 的
242
+ * `virtual:capabilities` 虚拟模块,产物目录不引用它。
243
+ */
244
+ const viteReactStrategy = ({ projectDir, outDir }) => {
245
+ (0, logger_1.log)('pack', 'Building (vite-react)...');
246
+ runProjectBuild(projectDir, 'bash scripts/build.sh');
247
+ const dist = node_path_1.default.join(projectDir, 'dist');
248
+ node_fs_1.default.rmSync(outDir, { recursive: true, force: true });
249
+ node_fs_1.default.mkdirSync(outDir, { recursive: true });
250
+ mergeInto(node_path_1.default.join(dist, 'output'), outDir);
251
+ mergeInto(node_path_1.default.join(dist, 'output_resource', 'assets'), node_path_1.default.join(outDir, 'assets'));
252
+ mergeInto(node_path_1.default.join(dist, 'output_static'), node_path_1.default.join(outDir, 'static'));
253
+ pruneStaticNoise(outDir);
254
+ dropSourceMaps(outDir);
255
+ dropRoutesJson(outDir);
256
+ };
257
+ /**
258
+ * nestjs-react-fullstack:只跑 `build:client`。
259
+ * 完整 `scripts/build.sh` 会跑 `action-plugin init` / `gen:openapi` / `nest build` /
260
+ * `prune-smart`,standalone 只要 client 那一半;且 `action-plugin init` 依赖平台 env,
261
+ * 是已知易失败点。
262
+ *
263
+ * 产物是单目录 `dist/client`,无需合并;但入口 HTML 嵌在 `dist/client/client/` 下
264
+ * (standalone 不挂 preset 的 vite-html-output 插件),需 `liftNestedEntry` 提到根。
265
+ */
266
+ const nestjsStrategy = ({ projectDir, outDir }) => {
267
+ (0, logger_1.log)('pack', 'Building client (nestjs-react-fullstack)...');
268
+ // 构建到**独立临时目录**,不碰 `dist/client`。
269
+ //
270
+ // 两个原因:
271
+ // 1. preset 设了 `emptyOutDir: false`(它指望 `scripts/build.sh` 先 `rm -rf dist`),
272
+ // 而我们绕过 build.sh 直接跑 `build:client`,没人清目录 —— 复用 `dist/client`
273
+ // 会把上次构建的 chunk 一起带进产物(真机见过新旧两个 index-<hash>.js 并存)。
274
+ // 2. `dist/client` 是平台链路的产物目录(dev 期 html-output 会往里写、NestJS 从
275
+ // 那儿取 HTML),导出这种旁路动作**不该清它**,否则破坏既有目录结构。
276
+ //
277
+ // vite CLI 的 `--outDir` 优先级高于 config,`--emptyOutDir` 覆盖 `emptyOutDir: false`。
278
+ // 放在 dist/ 下是为了留在 vite root 内(root 外 vite 会告警)。
279
+ const tmpDist = node_path_1.default.join(projectDir, 'dist', '.standalone-client');
280
+ try {
281
+ runProjectBuild(projectDir, `npm run build:client -- --outDir ${JSON.stringify(node_path_1.default.relative(projectDir, tmpDist))} --emptyOutDir`);
282
+ if (!node_fs_1.default.existsSync(tmpDist)) {
283
+ throw new error_1.AppError('PACK_EMPTY_OUTPUT', `client 构建产物 ${node_path_1.default.relative(projectDir, tmpDist)} 不存在`);
284
+ }
285
+ node_fs_1.default.rmSync(outDir, { recursive: true, force: true });
286
+ node_fs_1.default.mkdirSync(outDir, { recursive: true });
287
+ mergeInto(tmpDist, outDir);
288
+ copySharedStatic(projectDir, outDir);
289
+ pruneStaticNoise(outDir);
290
+ }
291
+ finally {
292
+ // 临时构建目录不留在用户项目里
293
+ node_fs_1.default.rmSync(tmpDist, { recursive: true, force: true });
294
+ }
295
+ liftNestedEntry(outDir);
296
+ dropSourceMaps(outDir);
297
+ dropRoutesJson(outDir);
298
+ };
299
+ const STRATEGY_BY_STACK = new Map([
300
+ ['design-html', designHtmlStrategy],
301
+ ['html', htmlStrategy],
302
+ ['vite-react', viteReactStrategy],
303
+ ['nestjs-react-fullstack', nestjsStrategy],
304
+ ]);
305
+ function resolveStrategy(stack) {
306
+ const strategy = STRATEGY_BY_STACK.get(stack);
307
+ if (!strategy) {
308
+ throw new error_1.AppError('PACK_STACK_UNSUPPORTED', `stack "${stack}" 不支持 standalone 导出`, {
309
+ next_actions: [`支持的 stack:${[...exports.PACK_SUPPORTED_STACKS].join(', ')}`],
310
+ });
311
+ }
312
+ return strategy;
313
+ }
@@ -0,0 +1,110 @@
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.redactUrl = redactUrl;
7
+ exports.assertValidUploadUrl = assertValidUploadUrl;
8
+ exports.uploadZip = uploadZip;
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const error_1 = require("../../../utils/error");
11
+ const logger_1 = require("../../../utils/logger");
12
+ /**
13
+ * 从响应头取排查用的 log id。字节侧网关/对象存储统一给 `x-tt-logid`;
14
+ * 拿不到时退到通用的 `x-request-id`,两个都没有就不报(不编造)。
15
+ */
16
+ function pickLogId(resp) {
17
+ return resp.headers.get('x-tt-logid') ?? resp.headers.get('x-request-id') ?? undefined;
18
+ }
19
+ /** 有 log id 时拼成错误信息后缀,没有就什么都不加。 */
20
+ function logIdSuffix(logId) {
21
+ return logId ? `,log_id=${logId}` : '';
22
+ }
23
+ /** 上传超时。产物可达数 MB,给足余量;沙箱到对象存储通常是内网,不会真跑满。 */
24
+ const UPLOAD_TIMEOUT_MS = 120_000;
25
+ /**
26
+ * 抹掉 URL 的 query 再用于日志 / 错误信息。
27
+ *
28
+ * **预签名 URL 的签名等同于一份凭证** —— 谁拿到谁就能往那个位置写。原样打进日志、
29
+ * 错误上报或 JSON 输出,就等于把上传能力泄漏出去。所以对外可见的地方一律走这里。
30
+ */
31
+ function redactUrl(raw) {
32
+ try {
33
+ const u = new URL(raw);
34
+ return u.search ? `${u.origin}${u.pathname}?<redacted>` : `${u.origin}${u.pathname}`;
35
+ }
36
+ catch {
37
+ // 连解析都失败的串,更不能原样回显
38
+ return '<unparseable url>';
39
+ }
40
+ }
41
+ /**
42
+ * 把 zip 以 **PUT 裸 body** 传到预签名 URL(TOS / S3 的标准上传形态)。
43
+ *
44
+ * 鉴权完全由 URL 自带的签名承担,不加任何额外 header —— 多带 header 反而可能破坏
45
+ * 某些实现的签名校验。
46
+ *
47
+ * 失败一律抛 `PACK_UPLOAD_FAILED`:调用方传了 `--upload-url` 就说明上传
48
+ * 是目的,传不上去等于没完成(设计决策:整个命令失败,退出码 1)。
49
+ */
50
+ /**
51
+ * 校验 `--upload-url` 合法性。**必须在动手构建之前调用** —— 否则 URL 写错要等一整轮
52
+ * 构建 + 图片下载 + 打包之后才被告知参数非法,白等几十秒。
53
+ */
54
+ function assertValidUploadUrl(url) {
55
+ let parsed;
56
+ try {
57
+ parsed = new URL(url);
58
+ }
59
+ catch {
60
+ throw new error_1.AppError('ARGS_INVALID', `--upload-url 不是合法 URL:${redactUrl(url)}`);
61
+ }
62
+ // 只允许 http(s):file: 会把产物写到本地路径,其它协议 fetch 也不支持
63
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
64
+ throw new error_1.AppError('ARGS_INVALID', `--upload-url 只支持 http/https,收到 ${parsed.protocol}`);
65
+ }
66
+ }
67
+ async function uploadZip(zipPath, url) {
68
+ assertValidUploadUrl(url);
69
+ if (!node_fs_1.default.existsSync(zipPath)) {
70
+ throw new error_1.AppError('ARGS_INVALID', `待上传的 zip 不存在:${zipPath}`);
71
+ }
72
+ const body = node_fs_1.default.readFileSync(zipPath);
73
+ // 默认输出**不带 URL**(连脱敏版也不带):预签名 URL 属于凭证类信息,常规日志
74
+ // 里没有它的位置;排查靠响应头的 log_id 就够,那才是服务端能据以定位的东西。
75
+ // 完整地址只在显式 --verbose 时给出。
76
+ (0, logger_1.log)('pack', `Uploading ${String(body.length)} bytes...`);
77
+ (0, logger_1.debug)(`pack: PUT ${url}`);
78
+ let resp;
79
+ try {
80
+ resp = await fetch(url, {
81
+ method: 'PUT',
82
+ headers: {
83
+ 'content-type': 'application/zip',
84
+ 'content-length': String(body.length),
85
+ },
86
+ body,
87
+ signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS),
88
+ });
89
+ }
90
+ catch (err) {
91
+ throw new error_1.AppError('PACK_UPLOAD_FAILED', `上传失败:${err instanceof Error ? err.message : String(err)}`, {
92
+ next_actions: [
93
+ '本地产物已保留,可修正 --upload-url 后重跑,或手动上传',
94
+ '--verbose 可打印完整请求地址',
95
+ ],
96
+ });
97
+ }
98
+ const logId = pickLogId(resp);
99
+ if (!resp.ok) {
100
+ throw new error_1.AppError('PACK_UPLOAD_FAILED', `上传失败:HTTP ${String(resp.status)}${logIdSuffix(logId)}`, {
101
+ next_actions: [
102
+ '预签名 URL 可能已过期或签名不匹配,确认后重试',
103
+ '本地产物已保留,也可手动上传',
104
+ '--verbose 可打印完整请求地址',
105
+ ],
106
+ });
107
+ }
108
+ (0, logger_1.log)('pack', `Uploaded (HTTP ${String(resp.status)}${logIdSuffix(logId)})`);
109
+ return { status: resp.status, ...(logId ? { logId } : {}) };
110
+ }
@@ -7,45 +7,8 @@ exports.EXCLUDES = void 0;
7
7
  exports.listSourceFiles = listSourceFiles;
8
8
  const node_fs_1 = __importDefault(require("node:fs"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
- // 与模板 scripts/build.sh 的 rsync EXCLUDES 对齐:这些名字(任意层级 basename)不进产物/不计入路由。
11
- exports.EXCLUDES = new Set([
12
- '.git',
13
- 'node_modules',
14
- 'dist',
15
- 'scripts',
16
- 'package.json',
17
- 'package-lock.json',
18
- 'pnpm-lock.yaml',
19
- 'yarn.lock',
20
- '.gitignore',
21
- '.npmrc',
22
- // agent / 编辑器配置目录与指令文件:只服务本地开发,不属于应用资产。
23
- // 注意 .claude/skills 在 flat layout 下是指向 ../.agents/skills 的软链,而 listSourceFiles
24
- // 会 follow 软链,所以必须按 basename 把 .claude 整个排掉,否则 skills 会从软链侧被带进产物。
25
- '.agent',
26
- '.agents',
27
- '.claude',
28
- '.codex',
29
- '.cursor',
30
- '.gemini',
31
- '.trae',
32
- '.windsurf',
33
- '.vscode',
34
- '.idea',
35
- 'AGENTS.md',
36
- 'CLAUDE.md',
37
- 'GEMINI.md',
38
- 'skills',
39
- '.env',
40
- '.env.local',
41
- 'README.md',
42
- '.DS_Store',
43
- '.spark',
44
- // 本地开发的临时/运行时目录:tmp 里的 .html 是草稿或中间产物,logs 是本地日志,
45
- // 两者既不进产物也不该出现在 routes.json。
46
- 'tmp',
47
- 'logs',
48
- ]);
10
+ const exclude_patterns_1 = require("../../../../utils/exclude-patterns");
11
+ Object.defineProperty(exports, "EXCLUDES", { enumerable: true, get: function () { return exclude_patterns_1.EXCLUDES; } });
49
12
  /**
50
13
  * 递归列出 root 下所有文件的相对 posix 路径,跳过 EXCLUDES(任意层级 basename),
51
14
  * 用 statSync 判定目录/文件以 follow 符号链接(含指向目录的软链),断链等无法 stat 的条目跳过。
@@ -54,7 +17,7 @@ function listSourceFiles(root) {
54
17
  const out = [];
55
18
  const walk = (dir, rel) => {
56
19
  for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
57
- if (exports.EXCLUDES.has(entry.name))
20
+ if (exclude_patterns_1.EXCLUDES.has(entry.name))
58
21
  continue;
59
22
  const childRel = rel ? `${rel}/${entry.name}` : entry.name;
60
23
  const abs = node_path_1.default.join(dir, entry.name);