@bitmagic/cli 0.1.1 → 0.1.3

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 (46) hide show
  1. package/dist/cli.d.ts +30 -0
  2. package/dist/cli.js +23 -11
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/forge.d.ts +107 -0
  5. package/dist/commands/forge.js +393 -0
  6. package/dist/commands/forge.js.map +1 -0
  7. package/dist/commands/generate.js +58 -45
  8. package/dist/commands/generate.js.map +1 -1
  9. package/dist/commands/verify.js +16 -16
  10. package/dist/commands/verify.js.map +1 -1
  11. package/dist/forge/apply-modifications.d.ts +19 -0
  12. package/dist/forge/apply-modifications.js +184 -0
  13. package/dist/forge/apply-modifications.js.map +1 -0
  14. package/dist/forge/bake-progress.d.ts +44 -0
  15. package/dist/forge/bake-progress.js +77 -0
  16. package/dist/forge/bake-progress.js.map +1 -0
  17. package/dist/forge/browser-host.d.ts +100 -0
  18. package/dist/forge/browser-host.js +351 -0
  19. package/dist/forge/browser-host.js.map +1 -0
  20. package/dist/forge/local-store.d.ts +30 -0
  21. package/dist/forge/local-store.js +130 -0
  22. package/dist/forge/local-store.js.map +1 -0
  23. package/dist/forge/run-pipeline.d.ts +105 -0
  24. package/dist/forge/run-pipeline.js +269 -0
  25. package/dist/forge/run-pipeline.js.map +1 -0
  26. package/dist/forge/stream.d.ts +106 -0
  27. package/dist/forge/stream.js +286 -0
  28. package/dist/forge/stream.js.map +1 -0
  29. package/dist/forge/transport.d.ts +58 -0
  30. package/dist/forge/transport.js +145 -0
  31. package/dist/forge/transport.js.map +1 -0
  32. package/dist/forge/upload-proxy.d.ts +21 -0
  33. package/dist/forge/upload-proxy.js +134 -0
  34. package/dist/forge/upload-proxy.js.map +1 -0
  35. package/dist/generate/stream.d.ts +1 -10
  36. package/dist/generate/stream.js +5 -62
  37. package/dist/generate/stream.js.map +1 -1
  38. package/dist/http/sse.d.ts +40 -0
  39. package/dist/http/sse.js +98 -0
  40. package/dist/http/sse.js.map +1 -0
  41. package/dist/local-port.d.ts +30 -0
  42. package/dist/local-port.js +53 -0
  43. package/dist/local-port.js.map +1 -0
  44. package/dist/scaffold/project-files.js +89 -9
  45. package/dist/scaffold/project-files.js.map +1 -1
  46. package/package.json +3 -2
@@ -0,0 +1,286 @@
1
+ import { CliError } from '../errors.js';
2
+ import { consumeSseStream } from '../http/sse.js';
3
+ /**
4
+ * Did the SERVER'S REQUEST DEADLINE fire, rather than a step failing?
5
+ *
6
+ * This has to be answered BEFORE {@link classifyForgeFailure}, because the classifier is
7
+ * structurally unable to get it right: a deadline that fires during the geometry step has
8
+ * `Scene design complete` in its progress, so the classifier correctly reports stage `forge` — and
9
+ * the advice for a genuine forge failure is "that step is deterministic, change the prompt rather
10
+ * than resuming", which is the exact opposite of what a timed-out run needs. The run was cut off
11
+ * mid-flight, is unharmed, and IS resumable; api-server's own deadline message says so, so without
12
+ * this the creator gets two contradictory sentences with the wrong one last.
13
+ *
14
+ * The deadline is not an edge case — `cli-forge.ts` notes the designer alone takes minutes with up
15
+ * to four attempts against a 280s budget, so it is expected to fire routinely.
16
+ *
17
+ * Primary signal is the structural `timedOut` flag on the frame. The message fallback exists
18
+ * because the CLI and api-server ship independently: a CLI carrying this fix can talk to a server
19
+ * deployed before the flag existed, and that is precisely the window in which getting this wrong
20
+ * costs a creator a second design. It matches on the server's own distinctive phrase and is
21
+ * deliberately narrow — a false positive would suppress correct "change the prompt" advice, which
22
+ * is the cheaper of the two errors.
23
+ */
24
+ const DEADLINE_MESSAGE_MARKER = 's server limit';
25
+ export function isDeadlineFailure(failure) {
26
+ return failure.timedOut || failure.message.includes(DEADLINE_MESSAGE_MARKER);
27
+ }
28
+ /**
29
+ * What to tell a creator whose run hit the server deadline: resume, unconditionally. No stage
30
+ * advice, because no stage failed.
31
+ */
32
+ export function forgeDeadlineAdvice(jobId) {
33
+ const unchanged = 'world.json is unchanged.';
34
+ if (jobId === null) {
35
+ return `${unchanged} No job id was reported, so there is nothing to resume — run the command again.`;
36
+ }
37
+ return `${unchanged} Nothing failed — the server simply ran out of request time, and the work so `
38
+ + `far is saved. Resume it with \`bitmagic forge --resume ${jobId}\`; the steps already `
39
+ + 'completed are skipped and are not billed again. Do NOT start a fresh forge.';
40
+ }
41
+ /**
42
+ * What to tell a creator whose stream ended with NO terminal frame at all.
43
+ *
44
+ * This is the deadline's twin and needs the same answer — the run was cut off mid-flight, api-server
45
+ * deliberately does not cancel it (`cli-forge.ts`'s run promise is left to finish), and whatever it
46
+ * completes is written to the job's store — except that here nothing said so, because no frame
47
+ * arrived to say it. Without a job id the creator's only move is a fresh `--prompt` run, which mints
48
+ * a NEW job id and re-runs (and re-bills) a design the abandoned job may already have finished and
49
+ * saved. That is why {@link requestForge} tracks the job id off EVERY frame rather than only the
50
+ * terminal one: api-server puts it on every progress frame precisely so this case has an answer.
51
+ */
52
+ export function forgeTruncationMessage(jobId) {
53
+ const opening = 'The connection to api-server ended before the forge finished. This is NOT a '
54
+ + 'confirmed failure: the server may already have designed the level (and billed for it) before '
55
+ + 'the stream broke. world.json is unchanged.';
56
+ if (jobId === null) {
57
+ return `${opening} No job id arrived before the stream broke, so there is nothing to resume — `
58
+ + 'run `bitmagic forge` again with the same prompt.';
59
+ }
60
+ return `${opening} The run was reported as job "${jobId}", and the server keeps whatever it `
61
+ + `completed — resume it with \`bitmagic forge --resume ${jobId}\` so the design is not re-run `
62
+ + 'and not billed a second time. Do NOT start a fresh forge.';
63
+ }
64
+ /**
65
+ * The progress lines `cli-forge.ts` emits around step 1. They bracket the step precisely — one
66
+ * before `steps.design(...)` is awaited, one after it returns — so "did a design-completed line
67
+ * arrive before the error frame?" is an exact answer to "did step 1 succeed in this request?".
68
+ *
69
+ * This is a coupling to another service's log strings, and it is deliberate but bounded: there is
70
+ * no step key on the wire, and `classifyForgeFailure` degrades to `unknown` (the both-cases
71
+ * message) the moment any of these stops matching. A drift therefore costs precision, never
72
+ * correctness — which is why the fallback exists rather than a default guess of one stage.
73
+ */
74
+ const DESIGN_STARTED_MARKER = 'Designing the level scene';
75
+ const DESIGN_FINISHED_MARKERS = ['Scene design complete', 'Design already complete'];
76
+ export function classifyForgeFailure(progress) {
77
+ const designFinished = progress.some((line) => DESIGN_FINISHED_MARKERS.some((marker) => line.includes(marker)));
78
+ if (designFinished)
79
+ return 'forge';
80
+ if (progress.some((line) => line.includes(DESIGN_STARTED_MARKER)))
81
+ return 'design';
82
+ return 'unknown';
83
+ }
84
+ /**
85
+ * The resume advice for a failure, as a paragraph appended to the server's own message. Every
86
+ * branch says explicitly that world.json is unchanged, because a creator who does not know has to
87
+ * go and look — and a forge costs enough that "did I just pay for nothing, and is my project
88
+ * still intact?" is the first question.
89
+ */
90
+ export function forgeFailureAdvice(stage, jobId) {
91
+ const unchanged = 'world.json is unchanged.';
92
+ if (jobId === null) {
93
+ return `${unchanged} No job id was reported, so there is nothing to resume — run the command again.`;
94
+ }
95
+ const resume = `\`bitmagic forge --resume ${jobId}\``;
96
+ switch (stage) {
97
+ case 'design':
98
+ return `${unchanged} The scene design did NOT complete, and it is saved only on success — `
99
+ + `so ${resume} re-runs the design and is billed for it again. It may well succeed on a `
100
+ + 'second attempt; if it keeps failing, change the prompt.';
101
+ case 'forge':
102
+ return `${unchanged} The scene design IS saved, but building its geometry failed. That step `
103
+ + `is deterministic: ${resume} feeds the same design to the same code and fails the same `
104
+ + 'way. Change the prompt and run a fresh forge rather than resuming this one.';
105
+ case 'unknown':
106
+ return `${unchanged} If the DESIGN step failed, it was not saved and ${resume} re-runs (and `
107
+ + 're-charges) it. If the GEOMETRY step failed, resuming fails identically — that step is '
108
+ + 'deterministic — so change the prompt instead of retrying.';
109
+ }
110
+ }
111
+ /**
112
+ * The complete message for a failed forge: the server's own text, then the ONE piece of advice
113
+ * that fits. The deadline check comes first and short-circuits the stage classification — see
114
+ * {@link isDeadlineFailure} for why the two must never both speak.
115
+ *
116
+ * The single entry point the command uses, so "which advice wins" is decided in one place and is
117
+ * testable without a network.
118
+ */
119
+ export function describeForgeFailure(failure) {
120
+ const advice = isDeadlineFailure(failure)
121
+ ? forgeDeadlineAdvice(failure.jobId)
122
+ : forgeFailureAdvice(classifyForgeFailure(failure.progress), failure.jobId);
123
+ const glb = failure.glbUrl
124
+ ? ` The forged GLB is still available at ${failure.glbUrl} and can be voxelized by hand.`
125
+ : '';
126
+ return `${failure.message} ${advice}${glb}`;
127
+ }
128
+ /**
129
+ * Excludes arrays, unlike the `typeof x === 'object'` shorthand — matching `browser-host.ts`'s
130
+ * definition. It matters at the one call site that narrows a payload rather than a wrapper:
131
+ * `!isRecord(data.artifact)` is what rejects a malformed terminal frame, and a JSON array is an
132
+ * `object` that is emphatically not a `ForgeArtifact`.
133
+ */
134
+ function isRecord(value) {
135
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
136
+ }
137
+ function stringField(value) {
138
+ return typeof value === 'string' && value.length > 0 ? value : null;
139
+ }
140
+ async function readJsonBody(response) {
141
+ try {
142
+ const parsed = await response.json();
143
+ return isRecord(parsed) ? parsed : {};
144
+ }
145
+ catch {
146
+ return {};
147
+ }
148
+ }
149
+ /**
150
+ * Maps the pre-stream failure shapes `cli-forge.ts` documents into a message an agent can act on.
151
+ * Never includes the access token — only the game id and the server's own text.
152
+ */
153
+ async function toCliError(response, gameId) {
154
+ const body = await readJsonBody(response);
155
+ const serverMessage = stringField(body.error);
156
+ if (response.status === 403) {
157
+ // Covers both "you do not own this game" and "this job was created for a different game" —
158
+ // the server deliberately does not distinguish them, so neither does this.
159
+ return new CliError(`${serverMessage ?? `You cannot forge a level for game "${gameId}".`} world.json is unchanged.`);
160
+ }
161
+ if (response.status === 402) {
162
+ const { balance, required } = body;
163
+ const detail = typeof balance === 'number' && typeof required === 'number'
164
+ ? ` You have ${balance} sparks; starting a forge requires at least ${required}.`
165
+ : '';
166
+ return new CliError(`Not enough sparks to start a forge.${detail} world.json is unchanged.`);
167
+ }
168
+ if (response.status === 400) {
169
+ return new CliError(`${serverMessage ?? 'The server rejected this forge request.'} world.json is unchanged.`);
170
+ }
171
+ if (response.status === 503) {
172
+ return new CliError(`${serverMessage ?? 'Forge storage is not configured on the server.'} `
173
+ + 'This is a server-side configuration problem, not something this project can fix. '
174
+ + 'world.json is unchanged.');
175
+ }
176
+ return new CliError(`api-server returned HTTP ${response.status} starting the forge`
177
+ + (serverMessage ? `: ${serverMessage}.` : '.')
178
+ + ' world.json is unchanged.');
179
+ }
180
+ /** The terminal `result` frame: `{ success: true, jobId, artifact, platformerMovement? }`. */
181
+ function asForgeSuccess(data) {
182
+ if (!isRecord(data) || data.success !== true)
183
+ return null;
184
+ const jobId = stringField(data.jobId);
185
+ if (jobId === null || !isRecord(data.artifact))
186
+ return null;
187
+ // `artifact` is trusted structurally, not field-by-field: it is a large schema produced by the
188
+ // very package this CLI then hands it back to, and `requireArtifact` plus the step code itself
189
+ // are what validate it in the only way that matters. A shallow re-validation here would give
190
+ // false confidence without catching anything the steps would not.
191
+ return {
192
+ success: true,
193
+ jobId,
194
+ artifact: data.artifact,
195
+ // Absent on every non-platformer forge, and absent from any server deployed before the field
196
+ // existed — so it is optional here rather than a reason to reject the frame.
197
+ ...(isRecord(data.platformerMovement) ? { platformerMovement: data.platformerMovement } : {}),
198
+ };
199
+ }
200
+ /**
201
+ * POSTs to `/api/cli/v1/forge/stream` and consumes the SSE response.
202
+ *
203
+ * A non-2xx response fails before any stream opens (see `toCliError`). On 200, each `progress`
204
+ * event is forwarded AND recorded, and the terminal `result`/`error` event resolves this call. A
205
+ * stream that closes with no terminal event is a truncated connection, not a success — and it fails
206
+ * with {@link forgeTruncationMessage}, naming the job id the frames already reported so the
207
+ * expensive half can be resumed rather than paid for twice.
208
+ */
209
+ export async function requestForge(environment, accessToken, body, deps) {
210
+ const gameId = typeof body.gameId === 'string' ? body.gameId : '(unknown game)';
211
+ const url = `${environment.apiUrl}/api/cli/v1/forge/stream`;
212
+ let response;
213
+ try {
214
+ response = await deps.fetch(url, {
215
+ method: 'POST',
216
+ headers: {
217
+ Authorization: `Bearer ${accessToken}`,
218
+ Accept: 'text/event-stream',
219
+ 'Content-Type': 'application/json',
220
+ },
221
+ body: JSON.stringify(body),
222
+ });
223
+ }
224
+ catch {
225
+ // A transport failure and an error status send people to very different places.
226
+ throw new CliError(`Cannot reach api-server at ${environment.apiUrl}. world.json is unchanged.`);
227
+ }
228
+ if (!response.ok) {
229
+ throw await toCliError(response, gameId);
230
+ }
231
+ if (!response.body) {
232
+ throw new CliError('The server accepted the forge but returned no stream body. world.json is unchanged.');
233
+ }
234
+ const progress = [];
235
+ let outcome = null;
236
+ /**
237
+ * The job id from the LAST frame that carried one, whatever kind of frame that was.
238
+ *
239
+ * Read off every frame in ONE place rather than per-branch, because the paths that most need it
240
+ * are exactly the ones no per-branch read covers: a stream that breaks before any terminal frame,
241
+ * and a terminal frame malformed enough that `asForgeSuccess` rejects it. api-server puts the job
242
+ * id on every progress frame (`{ jobId, message }`) for precisely this reason, so as soon as the
243
+ * first one arrives the work is resumable — and losing it costs the creator a second paid design.
244
+ */
245
+ let lastJobId = null;
246
+ const onEvent = (event) => {
247
+ if (isRecord(event.data)) {
248
+ lastJobId = stringField(event.data.jobId) ?? lastJobId;
249
+ }
250
+ if (event.event === 'progress') {
251
+ const message = isRecord(event.data) ? stringField(event.data.message) : null;
252
+ if (message !== null) {
253
+ progress.push(message);
254
+ deps.onProgress(message);
255
+ }
256
+ return false;
257
+ }
258
+ if (event.event === 'result') {
259
+ const success = asForgeSuccess(event.data);
260
+ if (success) {
261
+ outcome = success;
262
+ return true;
263
+ }
264
+ return false;
265
+ }
266
+ if (event.event === 'error') {
267
+ const data = isRecord(event.data) ? event.data : {};
268
+ outcome = {
269
+ success: false,
270
+ jobId: lastJobId,
271
+ message: stringField(data.message) ?? 'The forge failed without reporting a reason.',
272
+ ...(stringField(data.glbUrl) !== null ? { glbUrl: stringField(data.glbUrl) } : {}),
273
+ timedOut: data.timedOut === true,
274
+ progress,
275
+ };
276
+ return true;
277
+ }
278
+ return false;
279
+ };
280
+ const sawTerminal = await consumeSseStream(response.body, onEvent);
281
+ if (!sawTerminal || outcome === null) {
282
+ throw new CliError(forgeTruncationMessage(lastJobId));
283
+ }
284
+ return outcome;
285
+ }
286
+ //# sourceMappingURL=stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.js","sourceRoot":"","sources":["../../src/forge/stream.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAExC,OAAO,EAAE,gBAAgB,EAAiB,MAAM,gBAAgB,CAAC;AAwDjE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,uBAAuB,GAAG,gBAAgB,CAAC;AAEjD,MAAM,UAAU,iBAAiB,CAAC,OAAmD;IACnF,OAAO,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAC,CAAC;AAC/E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAoB;IACtD,MAAM,SAAS,GAAG,0BAA0B,CAAC;IAC7C,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO,GAAG,SAAS,iFAAiF,CAAC;IACvG,CAAC;IACD,OAAO,GAAG,SAAS,+EAA+E;UAC9F,0DAA0D,KAAK,wBAAwB;UACvF,6EAA6E,CAAC;AACpF,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAAoB;IACzD,MAAM,OAAO,GAAG,8EAA8E;UAC1F,+FAA+F;UAC/F,4CAA4C,CAAC;IACjD,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO,GAAG,OAAO,8EAA8E;cAC3F,kDAAkD,CAAC;IACzD,CAAC;IACD,OAAO,GAAG,OAAO,iCAAiC,KAAK,sCAAsC;UACzF,wDAAwD,KAAK,iCAAiC;UAC9F,2DAA2D,CAAC;AAClE,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,qBAAqB,GAAG,2BAA2B,CAAC;AAC1D,MAAM,uBAAuB,GAAG,CAAC,uBAAuB,EAAE,yBAAyB,CAAC,CAAC;AAErF,MAAM,UAAU,oBAAoB,CAAC,QAA2B;IAC9D,MAAM,cAAc,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAC5C,uBAAuB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACnE,IAAI,cAAc;QAAE,OAAO,OAAO,CAAC;IACnC,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,qBAAqB,CAAC,CAAC;QAAE,OAAO,QAAQ,CAAC;IACnF,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAwB,EAAE,KAAoB;IAC/E,MAAM,SAAS,GAAG,0BAA0B,CAAC;IAC7C,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO,GAAG,SAAS,iFAAiF,CAAC;IACvG,CAAC;IACD,MAAM,MAAM,GAAG,6BAA6B,KAAK,IAAI,CAAC;IACtD,QAAQ,KAAK,EAAE,CAAC;QACd,KAAK,QAAQ;YACX,OAAO,GAAG,SAAS,wEAAwE;kBACvF,MAAM,MAAM,2EAA2E;kBACvF,yDAAyD,CAAC;QAChE,KAAK,OAAO;YACV,OAAO,GAAG,SAAS,0EAA0E;kBACzF,qBAAqB,MAAM,6DAA6D;kBACxF,6EAA6E,CAAC;QACpF,KAAK,SAAS;YACZ,OAAO,GAAG,SAAS,oDAAoD,MAAM,gBAAgB;kBACzF,yFAAyF;kBACzF,2DAA2D,CAAC;IACpE,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAqB;IACxD,MAAM,MAAM,GAAG,iBAAiB,CAAC,OAAO,CAAC;QACvC,CAAC,CAAC,mBAAmB,CAAC,OAAO,CAAC,KAAK,CAAC;QACpC,CAAC,CAAC,kBAAkB,CAAC,oBAAoB,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9E,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM;QACxB,CAAC,CAAC,yCAAyC,OAAO,CAAC,MAAM,gCAAgC;QACzF,CAAC,CAAC,EAAE,CAAC;IACP,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,MAAM,GAAG,GAAG,EAAE,CAAC;AAC9C,CAAC;AAWD;;;;;GAKG;AACH,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,WAAW,CAAC,KAAc;IACjC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACtE,CAAC;AAED,KAAK,UAAU,YAAY,CAAC,QAAuB;IACjD,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC9C,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,KAAK,UAAU,UAAU,CAAC,QAAuB,EAAE,MAAc;IAC/D,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,QAAQ,CAAC,CAAC;IAC1C,MAAM,aAAa,GAAG,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAE9C,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,2FAA2F;QAC3F,2EAA2E;QAC3E,OAAO,IAAI,QAAQ,CACjB,GAAG,aAAa,IAAI,sCAAsC,MAAM,IAAI,2BAA2B,CAChG,CAAC;IACJ,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;QACnC,MAAM,MAAM,GAAG,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ;YACxE,CAAC,CAAC,aAAa,OAAO,+CAA+C,QAAQ,GAAG;YAChF,CAAC,CAAC,EAAE,CAAC;QACP,OAAO,IAAI,QAAQ,CAAC,sCAAsC,MAAM,2BAA2B,CAAC,CAAC;IAC/F,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,OAAO,IAAI,QAAQ,CAAC,GAAG,aAAa,IAAI,yCAAyC,2BAA2B,CAAC,CAAC;IAChH,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,OAAO,IAAI,QAAQ,CACjB,GAAG,aAAa,IAAI,gDAAgD,GAAG;cACrE,mFAAmF;cACnF,0BAA0B,CAC7B,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,QAAQ,CACjB,4BAA4B,QAAQ,CAAC,MAAM,qBAAqB;UAC5D,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,aAAa,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;UAC7C,2BAA2B,CAChC,CAAC;AACJ,CAAC;AAED,8FAA8F;AAC9F,SAAS,cAAc,CAAC,IAAa;IACnC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC1D,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtC,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5D,+FAA+F;IAC/F,+FAA+F;IAC/F,6FAA6F;IAC7F,kEAAkE;IAClE,OAAO;QACL,OAAO,EAAE,IAAI;QACb,KAAK;QACL,QAAQ,EAAE,IAAI,CAAC,QAAoC;QACnD,6FAA6F;QAC7F,6EAA6E;QAC7E,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,EAAE,kBAAkB,EAAE,IAAI,CAAC,kBAAkB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9F,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,WAAwB,EACxB,WAAmB,EACnB,IAA6B,EAC7B,IAAqB;IAErB,MAAM,MAAM,GAAG,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,gBAAgB,CAAC;IAChF,MAAM,GAAG,GAAG,GAAG,WAAW,CAAC,MAAM,0BAA0B,CAAC;IAE5D,IAAI,QAAuB,CAAC;IAC5B,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE;YAC/B,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,aAAa,EAAE,UAAU,WAAW,EAAE;gBACtC,MAAM,EAAE,mBAAmB;gBAC3B,cAAc,EAAE,kBAAkB;aACnC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;SAC3B,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,gFAAgF;QAChF,MAAM,IAAI,QAAQ,CAAC,8BAA8B,WAAW,CAAC,MAAM,4BAA4B,CAAC,CAAC;IACnG,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,MAAM,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC3C,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnB,MAAM,IAAI,QAAQ,CAAC,qFAAqF,CAAC,CAAC;IAC5G,CAAC;IAED,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,OAAO,GAAwB,IAAI,CAAC;IAExC;;;;;;;;OAQG;IACH,IAAI,SAAS,GAAkB,IAAI,CAAC;IAEpC,MAAM,OAAO,GAAG,CAAC,KAAe,EAAW,EAAE;QAC3C,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACzB,SAAS,GAAG,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,SAAS,CAAC;QACzD,CAAC;QACD,IAAI,KAAK,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;YAC/B,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAC9E,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;gBACrB,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACvB,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;YAC3B,CAAC;YACD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,KAAK,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC7B,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC3C,IAAI,OAAO,EAAE,CAAC;gBACZ,OAAO,GAAG,OAAO,CAAC;gBAClB,OAAO,IAAI,CAAC;YACd,CAAC;YACD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,KAAK,CAAC,KAAK,KAAK,OAAO,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YACpD,OAAO,GAAG;gBACR,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE,SAAS;gBAChB,OAAO,EAAE,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,8CAA8C;gBACpF,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,IAAI,CAAC,MAAM,CAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC5F,QAAQ,EAAE,IAAI,CAAC,QAAQ,KAAK,IAAI;gBAChC,QAAQ;aACT,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;IAEF,MAAM,WAAW,GAAG,MAAM,gBAAgB,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACnE,IAAI,CAAC,WAAW,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACrC,MAAM,IAAI,QAAQ,CAAC,sBAAsB,CAAC,SAAS,CAAC,CAAC,CAAC;IACxD,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The CLI lane's `ForgeTransport` — the same seam game-play-agent fills with a WebSocket
3
+ * (`utils/voxel-ws.ts`'s `sendAndWaitForResult`), filled here by posting straight into a page the
4
+ * CLI owns. Semantics are deliberately re-implemented, not redesigned: null on timeout or
5
+ * transport failure, 3 attempts by default, an idle timeout when `progressType` is given, and
6
+ * matching on type AND requestId. Read `voxel-ws.ts` alongside this file — every rule below has a
7
+ * counterpart there, and the two must not drift.
8
+ *
9
+ * Two things differ from the web lane, both because there is no relay in between:
10
+ *
11
+ * - The command envelope. The engine's message handler reads `{ type, data }`, so the flat
12
+ * command the pipeline builds (`{ type, requestId, ...payload }`) is repacked here.
13
+ * - The reply rename. The engine posts `*_SAVED` / `*_PLACED`; the pipeline awaits `*_RESULT`.
14
+ * In the web lane the creator does that rename in the browser. Doing it HERE, in Node, is what
15
+ * keeps the extracted pipeline byte-identical for both lanes.
16
+ *
17
+ * NOTE on the import below: `@bitmagic/world-forger` is a real **dependency** of this package,
18
+ * like `@bitmagic/asset-core`. It is the PUBLISHABLE half of the forger — the artifact schema, the
19
+ * host seams and the three browser-driven pipeline steps — and `publish-npm.yml` publishes it
20
+ * ahead of the CLI so npm can resolve it at install time. The private design/geometry half lives
21
+ * in `@bitmagic/world-forger-internal`, which is never published and must never be imported from
22
+ * here, nor bundled into `dist/`: bundled JS is readable, and minification is not protection.
23
+ */
24
+ import type { ForgeTransport } from '@bitmagic/world-forger/pipeline/transport-types.js';
25
+ /** The `{ type, data }` envelope `CreatorMessageHandler` dispatches on. */
26
+ export interface PageCommand {
27
+ type: string;
28
+ data: Record<string, unknown>;
29
+ }
30
+ /**
31
+ * The narrow slice of "a loaded game page" the transport needs. `browser-host.ts` implements it
32
+ * over Playwright; tests implement it with an object, which is why every rule in this file is
33
+ * unit-testable without a browser.
34
+ */
35
+ export interface ForgePageBridge {
36
+ /** Post a command into the page. Rejecting means the page is gone — a transport failure. */
37
+ postCommand(command: PageCommand): Promise<void>;
38
+ /** Subscribe to every message the page posted. Returns an unsubscribe. */
39
+ onMessage(listener: (message: Record<string, unknown>) => void): () => void;
40
+ }
41
+ export interface ForgePageTransportOptions {
42
+ log?: (message: string) => void;
43
+ /** Gap between attempts. Overridden in tests; 1s in production, matching the web lane. */
44
+ retryDelayMs?: number;
45
+ }
46
+ /**
47
+ * Reply-name → awaited-name. Authoritative list carried over from the creator's own relay
48
+ * (`game-play-agent/src/lab/browser.ts:112-123`). The last two entries are identity: the level
49
+ * bake already posts its result and its progress under the names the pipeline awaits, and they are
50
+ * listed rather than omitted so this map reads as the complete forge vocabulary.
51
+ */
52
+ export declare const RESULT_TYPE_MAP: Record<string, string>;
53
+ /**
54
+ * Build the transport over a page bridge. The returned object is the shared package's
55
+ * `ForgeTransport` and nothing more — `commands/forge.ts` hands it straight to
56
+ * `ForgeDeps.transport` via `buildCliForgeDeps`.
57
+ */
58
+ export declare function createForgePageTransport(bridge: ForgePageBridge, options?: ForgePageTransportOptions): ForgeTransport;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Reply-name → awaited-name. Authoritative list carried over from the creator's own relay
3
+ * (`game-play-agent/src/lab/browser.ts:112-123`). The last two entries are identity: the level
4
+ * bake already posts its result and its progress under the names the pipeline awaits, and they are
5
+ * listed rather than omitted so this map reads as the complete forge vocabulary.
6
+ */
7
+ export const RESULT_TYPE_MAP = {
8
+ VOXEL_OBJECT_PLACED: 'VOXEL_PLACE_RESULT',
9
+ BATCH_VOXEL_OBJECTS_PLACED: 'BATCH_VOXEL_PLACE_RESULT',
10
+ VOXEL_OBJECT_MODIFIED: 'VOXEL_MODIFY_RESULT',
11
+ VOXEL_OBJECT_DELETED: 'VOXEL_DELETE_RESULT',
12
+ VOXEL_ASSET_SAVED: 'VOXEL_ASSET_RESULT',
13
+ ASSET_FROM_GLB_URL_SAVED: 'ASSET_FROM_GLB_URL_RESULT',
14
+ VOXELIZE_GLB_AS_LEVEL_RESULT: 'VOXELIZE_GLB_AS_LEVEL_RESULT',
15
+ VOXELIZE_GLB_AS_LEVEL_PROGRESS: 'VOXELIZE_GLB_AS_LEVEL_PROGRESS',
16
+ };
17
+ const DEFAULT_MAX_RETRIES = 3;
18
+ const DEFAULT_RETRY_DELAY_MS = 1000;
19
+ function sleep(ms) {
20
+ return new Promise((resolve) => setTimeout(resolve, ms));
21
+ }
22
+ /**
23
+ * Whether a page message answers a caller awaiting `expected`.
24
+ *
25
+ * BOTH the message's own type and its renamed form count, because the pipeline is not consistent
26
+ * about which vocabulary it awaits — and it cannot be made consistent from this side, since the
27
+ * same code runs behind the web lane's relay:
28
+ *
29
+ * - `create-level-archetypes.ts` awaits `ASSET_FROM_GLB_URL_RESULT` — the RENAMED name.
30
+ * - `voxelize-level.ts` awaits `VOXELIZE_GLB_AS_LEVEL_RESULT` — renamed, but identity.
31
+ * - `place-and-persist-level.ts` awaits `BATCH_VOXEL_OBJECTS_PLACED` — the ENGINE's OWN name,
32
+ * which the map renames to `BATCH_VOXEL_PLACE_RESULT`.
33
+ *
34
+ * Renaming alone therefore answers two of the three and silently starves the third: the engine
35
+ * posts `BATCH_VOXEL_OBJECTS_PLACED`, this returns `BATCH_VOXEL_PLACE_RESULT`, nothing matches,
36
+ * and placement waits out its full idle timeout three times over. That is not hypothetical — it
37
+ * is what the first end-to-end forge did, after successfully baking every chunk.
38
+ *
39
+ * Accepting either name is safe here because no engine reply name is another's renamed form; the
40
+ * map's values are all distinct from its keys except the two deliberate identities.
41
+ */
42
+ function matchesAwaited(message, expected) {
43
+ const raw = message.type;
44
+ if (typeof raw !== 'string')
45
+ return false;
46
+ // Unmapped types compare on their own name rather than being dropped, so a reply the engine
47
+ // gains later reaches a caller awaiting it instead of vanishing into a timeout.
48
+ return raw === expected || RESULT_TYPE_MAP[raw] === expected;
49
+ }
50
+ /**
51
+ * One send + wait. Resolves the matching message, or null on timeout / a failed post — never
52
+ * rejects, because callers distinguish "no answer" from "answered with an error".
53
+ */
54
+ function singleAttempt(bridge, command, requestId, resultType, timeoutMs, progressType, log) {
55
+ return new Promise((resolve) => {
56
+ let settled = false;
57
+ let timer;
58
+ let unsubscribe;
59
+ const finish = (result) => {
60
+ if (settled)
61
+ return;
62
+ settled = true;
63
+ if (timer)
64
+ clearTimeout(timer);
65
+ unsubscribe?.();
66
+ resolve(result);
67
+ };
68
+ // (Re)arm the idle timer. Called once up front and again on every progress event, so the
69
+ // deadline is "no progress (or result) for timeoutMs", not a flat cap on the whole job.
70
+ const armTimer = () => {
71
+ if (settled)
72
+ return;
73
+ if (timer)
74
+ clearTimeout(timer);
75
+ timer = setTimeout(() => {
76
+ log(`forge transport: no ${resultType} within ${timeoutMs}ms of silence`);
77
+ finish(null);
78
+ }, timeoutMs);
79
+ };
80
+ unsubscribe = bridge.onMessage((message) => {
81
+ // Progress for THIS request keeps the attempt alive and resets the idle timer.
82
+ if (progressType && matchesAwaited(message, progressType) && (!requestId || message.requestId === requestId)) {
83
+ armTimer();
84
+ return;
85
+ }
86
+ if (!matchesAwaited(message, resultType))
87
+ return;
88
+ if (requestId && message.requestId !== requestId)
89
+ return;
90
+ // Surface the real failure reason: some engine paths put it in `message`, but every caller
91
+ // reads `result.error` — mirror it rather than let a clear reason degrade to "unknown".
92
+ // Copied, never mutated in place: `ForgeBrowserHost.dispatch` hands the SAME object to every
93
+ // subscribed listener, and step 5 places instances in batches with concurrent waits, so a
94
+ // mutation here would be visible to another waiter. (`voxel-ws.ts` mutates safely only
95
+ // because each of its connections parses its own object.)
96
+ if (message.success === false && !message.error && typeof message.message === 'string') {
97
+ finish({ ...message, error: message.message });
98
+ return;
99
+ }
100
+ finish(message);
101
+ });
102
+ // Armed before the post, not after it, so a `page.evaluate` that never settles (a hung or
103
+ // crashed renderer) is bounded by the same timeout as a silent game.
104
+ armTimer();
105
+ bridge.postCommand(command).catch((error) => {
106
+ const detail = error instanceof Error ? error.message : String(error);
107
+ log(`forge transport: could not post ${command.type} into the page: ${detail}`);
108
+ finish(null);
109
+ });
110
+ });
111
+ }
112
+ /**
113
+ * Build the transport over a page bridge. The returned object is the shared package's
114
+ * `ForgeTransport` and nothing more — `commands/forge.ts` hands it straight to
115
+ * `ForgeDeps.transport` via `buildCliForgeDeps`.
116
+ */
117
+ export function createForgePageTransport(bridge, options) {
118
+ const log = options?.log ?? (() => { });
119
+ const retryDelayMs = options?.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
120
+ return {
121
+ async sendAndWait(command, resultType, timeoutMs, waitOptions) {
122
+ const { type, ...payload } = command;
123
+ if (typeof type !== 'string') {
124
+ // Not a timeout and not a transport failure — a malformed command is a caller bug, and
125
+ // returning null here would hide it as "the browser never answered".
126
+ throw new Error(`A forge command must carry a string \`type\`, got ${JSON.stringify(type)}`);
127
+ }
128
+ const requestId = typeof command.requestId === 'string' ? command.requestId : undefined;
129
+ const envelope = { type, data: payload };
130
+ const maxRetries = waitOptions?.maxRetries ?? DEFAULT_MAX_RETRIES;
131
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
132
+ const result = await singleAttempt(bridge, envelope, requestId, resultType, timeoutMs, waitOptions?.progressType, log);
133
+ if (result !== null)
134
+ return result;
135
+ if (attempt < maxRetries) {
136
+ log(`forge transport: ${type} attempt ${attempt} failed, retrying in ${retryDelayMs}ms`);
137
+ await sleep(retryDelayMs);
138
+ }
139
+ }
140
+ log(`forge transport: ${type} exhausted ${maxRetries} attempt(s) waiting for ${resultType}`);
141
+ return null;
142
+ },
143
+ };
144
+ }
145
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/forge/transport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ /** The path the ENGINE calls. Not api-server's — that is what this proxy translates. */
2
+ export declare const ENGINE_UPLOAD_PATH = "/api/generate-upload-url";
3
+ /** The api-server route it forwards to. */
4
+ export declare const API_SERVER_UPLOAD_PATH = "/api/cli/v1/uploads/signed-url";
5
+ export interface UploadProxyOptions {
6
+ /** api-server base URL, e.g. `https://beta.portal.bitmagic.cloud`. */
7
+ apiUrl: string;
8
+ /** The creator's access token. Never leaves this process. */
9
+ token: string;
10
+ /** The game the CLI project owns. Overrides whatever the page asks for. */
11
+ gameId: string;
12
+ log?: (message: string) => void;
13
+ /** Injected in tests. Defaults to global fetch. */
14
+ fetchImpl?: typeof fetch;
15
+ }
16
+ export interface UploadProxy {
17
+ /** What `agentUrl` should be set to, e.g. `http://127.0.0.1:51234`. */
18
+ readonly url: string;
19
+ close(): Promise<void>;
20
+ }
21
+ export declare function startUploadProxy(options: UploadProxyOptions): Promise<UploadProxy>;