@playcraft/cli 0.1.63 → 0.1.65

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 (41) hide show
  1. package/README.md +24 -31
  2. package/SKILLS_REGISTRY.md +1 -1
  3. package/dist/commands/3d.js +41 -33
  4. package/dist/commands/build.js +70 -5
  5. package/dist/commands/create.js +4 -4
  6. package/dist/commands/inspect.js +4 -6
  7. package/dist/index.js +27 -19
  8. package/dist/model-adaptation/node-inspection.js +173 -0
  9. package/dist/model-adaptation/project-template.js +194 -0
  10. package/dist/model-adaptation/project.js +76 -0
  11. package/dist/model-adaptation/source.js +49 -0
  12. package/dist/model-optimization/optimize.js +281 -0
  13. package/dist/model-preparation/bindings.js +35 -0
  14. package/dist/model-preparation/commands.js +28 -0
  15. package/dist/model-preparation/geometry.js +241 -0
  16. package/dist/model-preparation/glb.js +176 -0
  17. package/dist/model-preparation/pipeline-engine.js +157 -0
  18. package/dist/model-preparation/pipeline-files.js +108 -0
  19. package/dist/model-preparation/pipeline-worker.js +32 -0
  20. package/dist/model-preparation/split-rigid.js +302 -0
  21. package/dist/model-preparation/vertex-repairs.js +63 -0
  22. package/dist/workspace-runtime/batch/deliver.js +2 -2
  23. package/dist/workspace-runtime/codex/app-server.js +11 -1
  24. package/dist/workspace-runtime/codex/delivery-tools.js +38 -5
  25. package/dist/workspace-runtime/codex/model-broker.js +19 -1
  26. package/dist/workspace-runtime/previews/publish.js +2 -2
  27. package/dist/workspace-runtime/serve.js +5 -0
  28. package/dist/workspace-runtime/server/directory-access.js +1 -1
  29. package/dist/workspace-runtime/server/dispatch.js +27 -47
  30. package/dist/workspace-runtime/server/http.js +12 -0
  31. package/dist/workspace-runtime/workspaces/build-isolation.js +75 -0
  32. package/dist/workspace-runtime/workspaces/model-build-inputs.js +27 -0
  33. package/dist/workspace-runtime/workspaces/model-preparation-store.js +134 -0
  34. package/dist/workspace-runtime/workspaces/model-preparation.js +357 -0
  35. package/dist/workspace-runtime/workspaces/platform-export.js +1 -0
  36. package/package.json +6 -4
  37. package/project-template/.claude/skills/playcraft-build-optimizer/SKILL.md +19 -39
  38. package/project-templates/empty/AGENTS.md +1 -1
  39. package/project-templates/phaser/.claude/skills/playcraft-build-optimizer/SKILL.md +19 -39
  40. package/project-templates/threejs/.claude/skills/playcraft-build-optimizer/SKILL.md +19 -39
  41. package/dist/workspace-runtime/workspaces/delivery-build.js +0 -440
@@ -30,11 +30,11 @@ triggers: 构建优化,build optimizer,压缩,compress,WebP,pngquant,文件大
30
30
 
31
31
  ```bash
32
32
  # 检查最终输出文件大小
33
- playcraft image info --input dist/index.html --json # 不适用,用 shell
33
+ playcraft image info --input dist/preview/index.html --json # 不适用,用 shell
34
34
 
35
35
  # Shell 方法:检查文件大小(字节)
36
- du -sh dist/index.html
37
- ls -lh dist/index.html
36
+ du -sh dist/preview/index.html
37
+ ls -lh dist/preview/index.html
38
38
 
39
39
  # 计算:若当前原始资产总大小为 X MB
40
40
  # 预计最终 HTML 大小 ≈ X × 1.33(Base64 膨胀)
@@ -112,34 +112,31 @@ grep -r "\.play()" game/ --include="*.js" --include="*.ts"
112
112
 
113
113
  ## 2. playcraft build 优化参数
114
114
 
115
+ - 产物固定为单文件 `dist/preview/index.html`,用于本地 review;在云工作区中它就是产物面板里的可玩预览。
116
+ - 下列参数只影响这份本地预览。渠道包(Meta、Google、AppLovin 等)只能通过平台导出生成,导出不会读取本地 build 的参数,渠道体积以导出时的校验为准。
117
+
115
118
  ### 完整优化构建命令
116
119
 
117
120
  ```bash
118
- # 标准优化构建(Meta 平台)
121
+ # 标准优化构建
119
122
  playcraft build . \
120
- --platform facebook \
121
123
  --compress-images \
122
124
  --image-quality 85 \
123
125
  --convert-webp \
124
126
  --js-minify \
125
- --css-minify \
126
- --output dist/
127
+ --css-minify
127
128
 
128
129
  # 带 3D 模型压缩的构建
129
130
  playcraft build . \
130
- --platform facebook \
131
131
  --compress-images \
132
132
  --image-quality 80 \
133
133
  --convert-webp \
134
134
  --compress-models \
135
135
  --model-compression draco \
136
- --js-minify \
137
- --output dist/
136
+ --js-minify
138
137
 
139
138
  # 分析体积占用(不构建,只分析)
140
- playcraft build . \
141
- --platform facebook \
142
- --analyze
139
+ playcraft build . --analyze
143
140
  ```
144
141
 
145
142
  ### 图片优化参数
@@ -170,25 +167,6 @@ playcraft build . \
170
167
  | `--js-minify` | 启用 JS 压缩(Terser + 混淆) |
171
168
  | `--css-minify` | 启用 CSS 压缩 |
172
169
 
173
- ### 多平台批量构建
174
-
175
- ```bash
176
- # 同时构建多个平台版本
177
- playcraft build-all . \
178
- --platforms facebook,google,applovin \
179
- --compress-images \
180
- --image-quality 85 \
181
- --convert-webp \
182
- --js-minify \
183
- --output dist/
184
-
185
- # 或按平台组
186
- playcraft build-all . \
187
- --group mainstream \ # 主流平台
188
- --compress-images \
189
- --convert-webp
190
- ```
191
-
192
170
  ---
193
171
 
194
172
  ## 3. 正确的优化执行顺序
@@ -207,9 +185,11 @@ playcraft build-all . \
207
185
  ↓
208
186
  5. playcraft build --js-minify --css-minify — 代码压缩
209
187
  ↓
210
- 6. 检查最终文件大小(必须 < 5 MB)
188
+ 6. 检查 dist/preview/index.html 大小(仅供参考,渠道体积以平台导出校验为准)
211
189
  ↓
212
190
  7. 合规验证清单(§1)
191
+ ↓
192
+ 8. 通过平台导出生成各渠道包
213
193
  ```
214
194
 
215
195
  **禁止行为**:
@@ -298,9 +278,9 @@ playcraft 3d optimize \
298
278
 
299
279
  ## 6. 常见审核被拒原因速查
300
280
 
301
- | 被拒原因 | 占比 | 检查方法 | 修复方法 |
302
- | ----------------- | ---- | ------------------------ | ------------------------------------------------------- |
303
- | 文件大小超过 5 MB | 45% | `ls -lh dist/index.html` | `--compress-images --convert-webp` 或降低 image-quality |
304
- | 包含外部请求 | 30% | `grep -r "https://"` | Developer 将外部资源内嵌 |
305
- | CTA 未实现 | 15% | `grep "FbPlayableAd"` | Developer 添加平台 CTA API |
306
- | 音频自动播放 | 10% | `grep "autoplay"` | Developer 改为用户交互触发 |
281
+ | 被拒原因 | 占比 | 检查方法 | 修复方法 |
282
+ | ----------------- | ---- | -------------------------------- | ------------------------------------------------------- |
283
+ | 文件大小超过 5 MB | 45% | `ls -lh dist/preview/index.html` | `--compress-images --convert-webp` 或降低 image-quality |
284
+ | 包含外部请求 | 30% | `grep -r "https://"` | Developer 将外部资源内嵌 |
285
+ | CTA 未实现 | 15% | `grep "FbPlayableAd"` | Developer 添加平台 CTA API |
286
+ | 音频自动播放 | 10% | `grep "autoplay"` | Developer 改为用户交互触发 |
@@ -1,440 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { constants } from 'node:fs';
3
- import { copyFile, cp, lstat, mkdir, mkdtemp, readdir, readlink, rename, rm, stat, symlink } from 'node:fs/promises';
4
- import { tmpdir } from 'node:os';
5
- import { basename, dirname, join, relative, sep } from 'node:path';
6
- import { createPlatformAdapter, getChannelRuleProfile, } from '@playcraft/build';
7
- import { DELIVERY_QUEUE_LIMIT, isWorkspaceArtifactOutputPath, } from '@playcraft/common/cloud-workspace/runtime';
8
- import { revisionScope, workspaceRoot } from './registry.js';
9
- import { excludeFromGit, ensureWorkspaceDependencies } from './dependencies.js';
10
- import { listWorkspaceBuildInputs } from './git.js';
11
- import { runDeveloperCommand } from './developer-command.js';
12
- import { openScratchToDevelopment } from './directory-access.js';
13
- import { ensureSharedDirectory } from './shared-write.js';
14
- import { driftScope, permissionDriftFromOutput, repairWorkspaceShares } from './workspace-shares.js';
15
- import { DeliveryStore } from './delivery-store.js';
16
- import { DeliveryValidationError, validateDeliveryArtifact } from './delivery.js';
17
- export const DELIVERY_BUILD_TIMEOUT_MS = 10 * 60_000;
18
- /** Staging under the workspace from earlier runtimes; kept out of Git so leftovers never commit. */
19
- const STAGING_DIRECTORY = '.playcraft-delivery';
20
- /** Never copied into the build copy: history, outputs and caches that the build neither reads nor owns. */
21
- const ISOLATE_SKIP = new Set(['.git', 'dist', '.turbo', STAGING_DIRECTORY]);
22
- export class DeliveryBuildError extends Error {
23
- code;
24
- constructor(code) {
25
- super(code);
26
- this.code = code;
27
- this.name = 'DeliveryBuildError';
28
- }
29
- }
30
- const queues = new WeakMap();
31
- const running = new WeakMap();
32
- export function trackDeliveryBuild(state, task) {
33
- const tasks = running.get(state) ?? new Set();
34
- running.set(state, tasks);
35
- const tracked = task.finally(() => tasks.delete(tracked));
36
- tasks.add(tracked);
37
- }
38
- export async function settleDeliveryBuilds(state) {
39
- await Promise.all([...(running.get(state) ?? [])]);
40
- }
41
- const scopeKey = (workspaceId, directoryName) => `${workspaceId}\0${directoryName}`;
42
- function buildQueues(state) {
43
- const known = queues.get(state) ?? new Map();
44
- queues.set(state, known);
45
- return known;
46
- }
47
- function queuedRecord(input, createdAt, queuePosition) {
48
- return {
49
- buildId: input.buildId,
50
- workspaceId: input.workspaceId,
51
- directoryName: input.directoryName,
52
- channel: input.payload.channel,
53
- format: input.payload.format,
54
- status: 'queued',
55
- ...(input.actor ? { actor: input.actor } : {}),
56
- queuePosition,
57
- createdAt,
58
- };
59
- }
60
- async function refreshQueuedRecords(state, key) {
61
- const queue = buildQueues(state).get(key);
62
- if (!queue)
63
- return;
64
- const waiting = queue.queue.filter(item => !item.started && !item.canceled);
65
- const workspaceId = waiting[0]?.input.workspaceId ?? queue.active?.pending.input.workspaceId;
66
- if (!workspaceId)
67
- return;
68
- const store = new DeliveryStore(state.config.stateDir, workspaceId);
69
- await Promise.all(waiting.map((item, index) => {
70
- if (item.started || item.canceled)
71
- return Promise.resolve();
72
- return store.write(queuedRecord(item.input, item.createdAt, index + 1)).catch(() => undefined);
73
- }));
74
- }
75
- function pump(state, key, queue) {
76
- if (queue.active)
77
- return;
78
- const next = queue.queue.shift();
79
- if (!next)
80
- return;
81
- next.started = true;
82
- queue.active = { buildId: next.input.buildId, pending: next };
83
- void executeBuild(state, next.input, next.controller)
84
- .then(outcome => next.resolve(outcome), error => next.reject(error))
85
- .finally(() => {
86
- queue.active = undefined;
87
- pump(state, key, queue);
88
- void refreshQueuedRecords(state, key);
89
- });
90
- }
91
- export function runDeliveryBuild(state, input) {
92
- if (input.directoryName === 'root')
93
- return Promise.reject(new DeliveryBuildError('invalid_workspace_directory'));
94
- const key = scopeKey(input.workspaceId, input.directoryName);
95
- const scopes = buildQueues(state);
96
- const queue = scopes.get(key) ?? { queue: [] };
97
- if ((queue.active ? 1 : 0) + queue.queue.length >= DELIVERY_QUEUE_LIMIT) {
98
- return Promise.reject(new DeliveryBuildError('delivery_queue_full'));
99
- }
100
- let resolve;
101
- let reject;
102
- const promise = new Promise((resolvePromise, rejectPromise) => {
103
- resolve = resolvePromise;
104
- reject = rejectPromise;
105
- });
106
- const pending = {
107
- input,
108
- controller: new AbortController(),
109
- promise,
110
- resolve,
111
- reject,
112
- createdAt: new Date().toISOString(),
113
- canceled: false,
114
- started: false,
115
- };
116
- queue.queue.push(pending);
117
- scopes.set(key, queue);
118
- pump(state, key, queue);
119
- void refreshQueuedRecords(state, key);
120
- return promise;
121
- }
122
- export async function cancelDeliveryBuild(state, workspaceId, directoryName, buildId) {
123
- const key = scopeKey(workspaceId, directoryName);
124
- const queue = buildQueues(state).get(key);
125
- if (!queue)
126
- return false;
127
- if (queue.active?.buildId === buildId) {
128
- queue.active.pending.controller.abort();
129
- return true;
130
- }
131
- const index = queue.queue.findIndex(item => item.input.buildId === buildId);
132
- if (index < 0)
133
- return false;
134
- const [pending] = queue.queue.splice(index, 1);
135
- if (!pending)
136
- return false;
137
- pending.canceled = true;
138
- const record = {
139
- ...queuedRecord(pending.input, pending.createdAt, 0),
140
- status: 'canceled',
141
- finishedAt: new Date().toISOString(),
142
- error: 'delivery_canceled',
143
- };
144
- await new DeliveryStore(state.config.stateDir, workspaceId).write(record).catch(() => undefined);
145
- pending.resolve({ record });
146
- void refreshQueuedRecords(state, key);
147
- return true;
148
- }
149
- export function activeDeliveryBuild(state, workspaceId, directoryName) {
150
- return buildQueues(state).get(scopeKey(workspaceId, directoryName))?.active?.buildId;
151
- }
152
- function shellQuote(value) {
153
- return `'${value.replace(/'/g, `'\\''`)}'`;
154
- }
155
- function outputExtension(format) {
156
- return format === 'zip' ? '.zip' : '.html';
157
- }
158
- async function listStagedFiles(staging, prefix = '') {
159
- const entries = await readdir(join(staging, prefix), { withFileTypes: true });
160
- const files = [];
161
- for (const entry of entries) {
162
- if (entry.isSymbolicLink() || entry.name.startsWith('.'))
163
- continue;
164
- const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
165
- if (entry.isDirectory()) {
166
- files.push(...await listStagedFiles(staging, relative));
167
- if (files.length > 512)
168
- throw new DeliveryBuildError('delivery_output_missing');
169
- }
170
- else if (entry.isFile()) {
171
- files.push(relative);
172
- }
173
- }
174
- return files;
175
- }
176
- function choosePrimary(files, format) {
177
- const extension = outputExtension(format);
178
- const candidates = files.filter(file => file.toLowerCase().endsWith(extension));
179
- if (!candidates.length)
180
- throw new DeliveryBuildError('delivery_output_missing');
181
- const preferred = format === 'zip' ? 'playable.zip' : 'index.html';
182
- return candidates.find(file => basename(file).toLowerCase() === preferred) ?? candidates[0];
183
- }
184
- /**
185
- * Copies a single artifact, or the whole staging tree for fragment/mixed channels. Each file is
186
- * staged next to its final name and renamed, so a partially written package never appears in dist.
187
- */
188
- async function copyToDist(root, staging, channel, format) {
189
- const files = await listStagedFiles(staging);
190
- const primary = choosePrimary(files, format);
191
- const mode = getChannelRuleProfile(channel).deliveryMode;
192
- const selected = files.length > 1 && (mode === 'fragment-assets' || mode === 'mixed') ? files : [primary];
193
- const paths = [];
194
- let sizeBytes = 0;
195
- for (const file of selected) {
196
- const relative = `dist/delivery/${channel}/${file}`;
197
- if (!isWorkspaceArtifactOutputPath(relative))
198
- throw new DeliveryBuildError('delivery_output_missing');
199
- const target = join(root, relative);
200
- const directory = dirname(target);
201
- await ensureSharedDirectory(root, directory);
202
- const temporary = join(directory, `.${basename(file)}.${randomUUID()}.tmp`);
203
- try {
204
- await copyFile(join(staging, file), temporary);
205
- await rename(temporary, target);
206
- }
207
- catch {
208
- await rm(temporary, { force: true }).catch(() => undefined);
209
- throw new DeliveryBuildError('delivery_copy_failed');
210
- }
211
- sizeBytes += (await stat(target)).size;
212
- paths.push(relative);
213
- }
214
- return { primary: `dist/delivery/${channel}/${primary}`, paths, sizeBytes };
215
- }
216
- /** Copy-on-write clone where the filesystem supports it (APFS, btrfs, XFS); a plain copy elsewhere. */
217
- const CLONE = constants.COPYFILE_FICLONE;
218
- const COPY_CONCURRENCY = 16;
219
- function skipped(path) {
220
- const parts = path.split(/[\\/]/);
221
- return ISOLATE_SKIP.has(parts[0]) || parts.includes('node_modules');
222
- }
223
- async function copyInput(root, destination, path) {
224
- const source = join(root, path);
225
- const target = join(destination, path);
226
- let entry;
227
- try {
228
- entry = await lstat(source);
229
- }
230
- catch (error) {
231
- // A tracked file deleted in the work tree is not a build input.
232
- if (error.code === 'ENOENT')
233
- return;
234
- throw error;
235
- }
236
- await mkdir(dirname(target), { recursive: true });
237
- if (entry.isSymbolicLink())
238
- await symlink(await readlink(source), target);
239
- else if (entry.isDirectory())
240
- await cp(source, target, { recursive: true, verbatimSymlinks: true, mode: CLONE });
241
- else
242
- await copyFile(source, target, CLONE);
243
- }
244
- /**
245
- * Copies the workspace into `destination` for one build, like the platform export runner, with the
246
- * top-level `node_modules` linked (the build only reads it). Builders rewrite project files in place
247
- * (`builds.config.js` and its `.bak`, `package.json` scripts, migrated imports), so the build must
248
- * never run in the workspace itself, where autosave would commit those rewrites.
249
- *
250
- * Only the files Git would carry are copied: tracked ones and untracked ones `.gitignore` keeps, so
251
- * caches, logs and local outputs stay behind. Without a repository the whole tree is copied.
252
- */
253
- export async function isolateWorkspaceForBuild(root, destination) {
254
- // The build runs as the development identity and writes its scratch folder inside this copy.
255
- await ensureSharedDirectory(root, destination);
256
- const modules = join(root, 'node_modules');
257
- const modulesEntry = await lstat(modules).catch(() => undefined);
258
- if (modulesEntry?.isDirectory() || modulesEntry?.isSymbolicLink()) {
259
- await symlink(modulesEntry.isSymbolicLink() ? await readlink(modules) : modules, join(destination, 'node_modules'), 'dir');
260
- }
261
- const inputs = await listWorkspaceBuildInputs(root).catch(() => undefined);
262
- const paths = inputs
263
- ?? (await readdir(root)).filter((name) => name !== 'node_modules');
264
- const queue = paths.filter((path) => !skipped(path));
265
- const workers = Array.from({ length: Math.min(COPY_CONCURRENCY, queue.length) }, async () => {
266
- for (let path = queue.shift(); path !== undefined; path = queue.shift()) {
267
- if (inputs)
268
- await copyInput(root, destination, path);
269
- else {
270
- await cp(join(root, path), join(destination, path), {
271
- recursive: true,
272
- verbatimSymlinks: true,
273
- mode: CLONE,
274
- // Nested dependency folders stay out as well; the build resolves through the linked root.
275
- filter: (source) => !relative(join(root, path), source).split(sep).includes('node_modules'),
276
- });
277
- }
278
- }
279
- });
280
- await Promise.all(workers);
281
- }
282
- /**
283
- * Runs one channel build in a disposable copy of the workspace. The build writes into a disposable
284
- * output directory; only the final artifact is copied into dist so the builder's `--clean` can never
285
- * erase another delivery.
286
- */
287
- async function executeBuild(state, input, controller) {
288
- if (input.directoryName === 'root')
289
- throw new DeliveryBuildError('invalid_workspace_directory');
290
- const root = await workspaceRoot(state, input.workspaceId, input.directoryName);
291
- const store = new DeliveryStore(state.config.stateDir, input.workspaceId);
292
- const startedAt = new Date().toISOString();
293
- const base = {
294
- buildId: input.buildId,
295
- workspaceId: input.workspaceId,
296
- directoryName: input.directoryName,
297
- channel: input.payload.channel,
298
- format: input.payload.format,
299
- status: 'running',
300
- ...(input.actor ? { actor: input.actor } : {}),
301
- createdAt: startedAt,
302
- startedAt,
303
- };
304
- await store.write(base);
305
- let scratch;
306
- let staging = '';
307
- let status = 'running';
308
- let error;
309
- let validationError;
310
- let outputPath;
311
- let outputPaths;
312
- let sizeBytes;
313
- let sizeLimitBytes;
314
- let report;
315
- let logChain = Promise.resolve();
316
- let record;
317
- try {
318
- scratch = await mkdtemp(join(tmpdir(), 'playcraft-delivery-'));
319
- // The build runs as the development identity while this scratch tree belongs to the controller.
320
- await openScratchToDevelopment(scratch, (await lstat(root)).gid);
321
- staging = join(scratch, 'output');
322
- const project = join(scratch, 'project');
323
- await ensureSharedDirectory(root, staging);
324
- await excludeFromGit(root, `/${STAGING_DIRECTORY}/`);
325
- await excludeFromGit(root, '/dist/');
326
- const dependencies = await ensureWorkspaceDependencies(state, revisionScope(state, input.workspaceId, input.directoryName) ?? input.workspaceId, root);
327
- if (!dependencies.ok) {
328
- if (dependencies.logTail) {
329
- await store.appendLog(input.buildId, `\n${dependencies.logTail}\n`);
330
- }
331
- throw new DeliveryBuildError('delivery_dependencies_failed');
332
- }
333
- try {
334
- await isolateWorkspaceForBuild(root, project);
335
- }
336
- catch {
337
- throw new DeliveryBuildError('delivery_isolation_failed');
338
- }
339
- const planned = process.env.PLAYCRAFT_DELIVERY_BUILD_COMMAND || 'playcraft build';
340
- const command = `umask 002 && NO_COLOR=1 CI=1 ${planned} . --platform ${input.payload.channel} --format ${input.payload.format} --output ${shellQuote(staging)} --mode full`;
341
- let outputTail = '';
342
- const runBuild = () => runDeveloperCommand(state, {
343
- processId: `delivery-${input.buildId}`,
344
- cwd: project,
345
- command,
346
- timeoutMs: DELIVERY_BUILD_TIMEOUT_MS,
347
- signal: controller.signal,
348
- onOutput: (chunk) => {
349
- const text = String(chunk);
350
- if (!text)
351
- return;
352
- outputTail = (outputTail + text).slice(-64 * 1024);
353
- logChain = logChain.then(() => store.appendLog(input.buildId, text)).catch(() => undefined);
354
- },
355
- });
356
- let outcome = await runBuild();
357
- let permissionDrift = false;
358
- if (outcome.code !== 0 && !outcome.canceled && !outcome.timedOut) {
359
- // The build runs as the development identity; an entry only that identity owns repairs its
360
- // own shared subtree, then the build is retried exactly once.
361
- const drift = permissionDriftFromOutput(outputTail);
362
- if (drift) {
363
- permissionDrift = true;
364
- await repairWorkspaceShares(state, root, await driftScope(root, drift.path)).catch(() => undefined);
365
- outcome = await runBuild();
366
- permissionDrift = outcome.code !== 0;
367
- }
368
- }
369
- if (outcome.code === undefined) {
370
- status = 'failed';
371
- error = 'delivery_process_unknown';
372
- }
373
- else if (outcome.canceled) {
374
- status = 'canceled';
375
- error = 'delivery_canceled';
376
- }
377
- else if (outcome.timedOut) {
378
- status = 'failed';
379
- error = 'delivery_timeout';
380
- }
381
- else if (outcome.code !== 0) {
382
- status = 'failed';
383
- error = permissionDrift ? 'workspace_permission_drift' : 'delivery_build_failed';
384
- }
385
- else {
386
- const output = await copyToDist(root, staging, input.payload.channel, input.payload.format);
387
- outputPath = output.primary;
388
- outputPaths = output.paths;
389
- sizeBytes = output.sizeBytes;
390
- sizeLimitBytes = createPlatformAdapter({
391
- platform: input.payload.channel,
392
- format: input.payload.format,
393
- }).getSizeLimit();
394
- try {
395
- const result = await validateDeliveryArtifact(root, {
396
- channel: input.payload.channel,
397
- format: input.payload.format,
398
- path: outputPath,
399
- });
400
- report = result.report;
401
- sizeLimitBytes = result.sizeLimitBytes;
402
- }
403
- catch (cause) {
404
- validationError = cause instanceof DeliveryValidationError ? cause.code : 'delivery_report_unavailable';
405
- }
406
- status = 'succeeded';
407
- }
408
- }
409
- catch (cause) {
410
- status = 'failed';
411
- error = cause instanceof DeliveryBuildError ? cause.code : 'delivery_build_failed';
412
- }
413
- finally {
414
- // `rm` removes the node_modules link, never its target.
415
- if (scratch)
416
- await rm(scratch, { recursive: true, force: true }).catch(() => undefined);
417
- await logChain;
418
- if (report) {
419
- await store.writeReport(input.buildId, report).catch(() => {
420
- validationError = validationError ?? 'delivery_report_unavailable';
421
- });
422
- }
423
- const recordValue = {
424
- ...base,
425
- status,
426
- finishedAt: new Date().toISOString(),
427
- ...(outputPath ? { outputPath } : {}),
428
- ...(outputPaths && outputPaths.length > 1 ? { outputPaths } : {}),
429
- ...(sizeBytes !== undefined ? { sizeBytes } : {}),
430
- ...(sizeLimitBytes !== undefined ? { sizeLimitBytes } : {}),
431
- ...(report ? { summary: report.summary, buildChain: report.buildChain } : {}),
432
- ...(error ? { error } : {}),
433
- ...(validationError ? { validationError } : {}),
434
- logBytes: await store.logBytes(input.buildId),
435
- };
436
- record = recordValue;
437
- await store.write(recordValue);
438
- }
439
- return { record, ...(report ? { report } : {}) };
440
- }