@bitmagic/cli 0.1.0 → 0.1.2

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 (49) 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.d.ts +1 -0
  45. package/dist/scaffold/project-files.js +175 -5
  46. package/dist/scaffold/project-files.js.map +1 -1
  47. package/dist/scaffold/project.js +2 -1
  48. package/dist/scaffold/project.js.map +1 -1
  49. package/package.json +4 -3
@@ -0,0 +1,105 @@
1
+ import { type ForgeArtifact, type ForgeDeps, type ForgeJobInput, type ForgeLevelSummary, type ForgeLogger, type ForgeStorageEnv, type ForgeTransport, type ForgeWorldJsonSource, type ForgedWorkStore } from '@bitmagic/world-forger/pipeline/index.js';
2
+ /** The project's one world.json — the file the engine loads and the forge writes back to. */
3
+ export declare function projectWorldJsonPath(projectRoot: string): string;
4
+ /**
5
+ * `ForgeDeps.worldJson` for a CLI project. **Both reads resolve to the SAME file**, and that is
6
+ * correct here rather than a shortcut.
7
+ *
8
+ * The seam has two reads because game-play-agent's web lane genuinely has two world.json files
9
+ * mid-session — a per-session copy the agent writes and a monorepo work copy the creator/browser
10
+ * writes — and which one a differential plan is computed against decides what survives a forge.
11
+ * `world-json-source.ts` carries the full argument; the short version is that what each read
12
+ * WANTS is the same thing, "the file the returned modifications will be applied to", and the web
13
+ * lane cannot collapse them without changing behaviour it has no test for.
14
+ *
15
+ * A CLI project has exactly ONE world.json. `applyModificationsToWorld` writes it, the engine
16
+ * loads it, and nothing else exists to diverge from it — so returning it from both reads is not
17
+ * "picking one", it is what "plan against the write target" reduces to when there is a single
18
+ * target. The seam's own header names this lane and says so.
19
+ *
20
+ * Both reads happen LAZILY, when step 5 calls them — never snapshotted when these deps are built.
21
+ * That matters even here: the reads run after a bake that can take 20 minutes, and the scaffolded
22
+ * project's own tooling (or the creator) may have written world.json during it.
23
+ *
24
+ * Both keep the seam's documented degradations. They are contract, not swallowed errors: a
25
+ * game with an unreadable world.json must still be able to forge, and neither `{}` nor `null`
26
+ * hides anything the creator could act on — a missing or corrupt world.json fails far more
27
+ * loudly, and much earlier, in `readProjectGameData` when the game is loaded into the browser.
28
+ */
29
+ export declare function createProjectWorldJsonSource(projectRoot: string): ForgeWorldJsonSource;
30
+ /**
31
+ * A `ForgeLogger` over a plain line printer. The pipeline logs with pino's object-first shape
32
+ * (`info({ jobId, ... }, 'message')`), which is what makes step progress visible at all — so the
33
+ * context object is rendered rather than dropped, but compactly: these lines go to a creator's
34
+ * terminal, not to a log aggregator.
35
+ */
36
+ export declare function createConsoleForgeLogger(log: (message: string) => void): ForgeLogger;
37
+ /**
38
+ * The `ForgeDeps` steps 3-5 read. Four seams, and the two the type also declares are omitted
39
+ * deliberately:
40
+ *
41
+ * - `model` — steps 3-5 never call an LLM (that is step 1, on api-server). The type requires it,
42
+ * so it is a client that THROWS rather than one that returns something plausible: a future
43
+ * change that starts calling a model from the browser-driven half must fail loudly, not
44
+ * silently produce a level designed by a stub. Same discipline api-server applies to the
45
+ * `transport` and `worldJson` seams it does not implement.
46
+ * - `assetCatalog` — omitted; absence degrades to "no reuse", never a failure. It feeds the
47
+ * DESIGNER (step 1), which does not run here, so it would have no reader in this lane even if
48
+ * the CLI built one.
49
+ * - `hqAssetIds` — omitted. A CLI project has no HQ regeneration queue, and the absent seam
50
+ * already means exactly "no jobs in flight" (see the field's own doc), so supplying an empty
51
+ * set would restate the default in a second place.
52
+ */
53
+ export declare function buildCliForgeDeps(options: {
54
+ projectRoot: string;
55
+ storage: ForgeStorageEnv;
56
+ transport: ForgeTransport;
57
+ logger: ForgeLogger;
58
+ }): ForgeDeps;
59
+ /**
60
+ * Put the upstream artifacts into the local store so steps 3-5 can read them.
61
+ *
62
+ * `forge` is written unconditionally: it is what the server just handed back, and on a resume the
63
+ * server returns the SAME cached artifact (`forgeWorld` ends every run with `store.get('forge')`
64
+ * whether or not either step re-ran), so the write is idempotent rather than a clobber.
65
+ *
66
+ * `platformer-movement` is written whenever the terminal frame carried one, on the same reasoning:
67
+ * it is step 1's output, it lives in api-server's store, and step 5 reads it from THIS store to set
68
+ * `worldForgerMovement` on the level record. Only a `--platformer` design produces one, so absence
69
+ * is normal — and on an older server that does not send the field it is also possible, which is
70
+ * what `warnIfPlatformerMovementMissing` still exists to catch.
71
+ *
72
+ * `input` is written ONLY when absent, mirroring api-server's own freeze. It is the job's frozen
73
+ * request, and steps 4 and 5 read the level name, prompt and start-level intent off it — so a
74
+ * resume must persist what was originally asked for, not what this invocation's flags happen to
75
+ * say.
76
+ */
77
+ export declare function seedForgeStore(store: ForgedWorkStore, artifact: ForgeArtifact, input: ForgeJobInput, platformerMovement?: Record<string, unknown>): Promise<void>;
78
+ /** Read a job's frozen `input`, or null if this machine has never run it. */
79
+ export declare function readFrozenInput(storage: ForgeStorageEnv, jobId: string): Promise<ForgeJobInput | null>;
80
+ export interface RunPipelineOptions {
81
+ projectRoot: string;
82
+ store: ForgedWorkStore;
83
+ deps: ForgeDeps;
84
+ /** Progress printer for the command's own step banners. */
85
+ log: (message: string) => void;
86
+ }
87
+ export interface RunPipelineResult {
88
+ summary: ForgeLevelSummary;
89
+ /** False when a resume found the job already applied and nothing was written this run. */
90
+ applied: boolean;
91
+ /** The world.json lines to print, empty on the already-applied path. */
92
+ changes: string[];
93
+ }
94
+ /**
95
+ * Drive steps 3-5 in order, skipping any whose output artifact already exists — the same
96
+ * resume mechanism `run-world-forger.ts` uses, and the same one api-server applies to steps 1-2.
97
+ *
98
+ * The write half deliberately does NOT live in step 5: `placeAndPersistLevel` returns the
99
+ * modifications and the heightfield, and the host applies both. The ordering below is a contract,
100
+ * not a detail — `persisted` is the job-complete marker a resume returns directly, so recording it
101
+ * before the world.json write would let a failed write resume into "done" over an unchanged world.
102
+ * Unlike the web lane's applier (which catches everything and returns `{success:false}`), the
103
+ * CLI's `applyModificationsToWorld` THROWS, so here that ordering does real work.
104
+ */
105
+ export declare function runForgePipeline(options: RunPipelineOptions): Promise<RunPipelineResult>;
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Steps 3-5 of the world forger, hosted by the CLI: create the archetype library assets, bake the
3
+ * level, then place its instances and write the result into the project's world.json.
4
+ *
5
+ * This is the CLI's counterpart to `game-play-agent/src/mastra/tools/run-world-forger.ts` (the
6
+ * step driver) and `world-forger/pipeline.ts` (the host-side writes). The three steps themselves
7
+ * are imported unchanged from `@bitmagic/world-forger` — the PUBLISHABLE half. The private half,
8
+ * `@bitmagic/world-forger-internal`, holds the designer, its system prompts and the geometry
9
+ * engine; it runs on api-server and must never be imported here, nor reach a creator's machine.
10
+ *
11
+ * Kept separate from `commands/forge.ts` so the whole pipeline half is testable without citty, a
12
+ * browser, a dev server or a network: every seam it needs arrives as a parameter.
13
+ */
14
+ import * as fs from 'fs';
15
+ import * as path from 'path';
16
+ import { createForgedWorkStore, createLevelArchetypes, placeAndPersistLevel, voxelizeLevel, } from '@bitmagic/world-forger/pipeline/index.js';
17
+ import { FORGED_HEIGHTFIELD_FILE } from '@bitmagic/world-forger/terrain-height-grid.js';
18
+ import { CliError } from '../errors.js';
19
+ import { aliasSourceDir } from '../scaffold/aliases.js';
20
+ import { applyModificationsToWorld } from './apply-modifications.js';
21
+ /** The project's one world.json — the file the engine loads and the forge writes back to. */
22
+ export function projectWorldJsonPath(projectRoot) {
23
+ return path.join(projectRoot, aliasSourceDir('work'), 'world.json');
24
+ }
25
+ /**
26
+ * `ForgeDeps.worldJson` for a CLI project. **Both reads resolve to the SAME file**, and that is
27
+ * correct here rather than a shortcut.
28
+ *
29
+ * The seam has two reads because game-play-agent's web lane genuinely has two world.json files
30
+ * mid-session — a per-session copy the agent writes and a monorepo work copy the creator/browser
31
+ * writes — and which one a differential plan is computed against decides what survives a forge.
32
+ * `world-json-source.ts` carries the full argument; the short version is that what each read
33
+ * WANTS is the same thing, "the file the returned modifications will be applied to", and the web
34
+ * lane cannot collapse them without changing behaviour it has no test for.
35
+ *
36
+ * A CLI project has exactly ONE world.json. `applyModificationsToWorld` writes it, the engine
37
+ * loads it, and nothing else exists to diverge from it — so returning it from both reads is not
38
+ * "picking one", it is what "plan against the write target" reduces to when there is a single
39
+ * target. The seam's own header names this lane and says so.
40
+ *
41
+ * Both reads happen LAZILY, when step 5 calls them — never snapshotted when these deps are built.
42
+ * That matters even here: the reads run after a bake that can take 20 minutes, and the scaffolded
43
+ * project's own tooling (or the creator) may have written world.json during it.
44
+ *
45
+ * Both keep the seam's documented degradations. They are contract, not swallowed errors: a
46
+ * game with an unreadable world.json must still be able to forge, and neither `{}` nor `null`
47
+ * hides anything the creator could act on — a missing or corrupt world.json fails far more
48
+ * loudly, and much earlier, in `readProjectGameData` when the game is loaded into the browser.
49
+ */
50
+ export function createProjectWorldJsonSource(projectRoot) {
51
+ const file = projectWorldJsonPath(projectRoot);
52
+ const read = () => {
53
+ if (!fs.existsSync(file))
54
+ return null;
55
+ try {
56
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
57
+ return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
58
+ ? parsed
59
+ : null;
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ };
65
+ return {
66
+ readForPersistPlan: async () => (read() ?? {}),
67
+ readForStarterScatterPlan: async () => read(),
68
+ };
69
+ }
70
+ /**
71
+ * A `ForgeLogger` over a plain line printer. The pipeline logs with pino's object-first shape
72
+ * (`info({ jobId, ... }, 'message')`), which is what makes step progress visible at all — so the
73
+ * context object is rendered rather than dropped, but compactly: these lines go to a creator's
74
+ * terminal, not to a log aggregator.
75
+ */
76
+ export function createConsoleForgeLogger(log) {
77
+ const emit = (level, first, rest) => {
78
+ if (typeof first === 'string') {
79
+ log(`${level}${first}`);
80
+ return;
81
+ }
82
+ const message = typeof rest[0] === 'string' ? rest[0] : '';
83
+ const context = first;
84
+ const fields = Object.entries(context)
85
+ // `jobId` is printed by the command itself and repeats on every line; `err` is rendered
86
+ // separately below so a stack does not get JSON-quoted into one unreadable line.
87
+ .filter(([key]) => key !== 'jobId' && key !== 'err')
88
+ .map(([key, value]) => `${key}=${typeof value === 'object' ? JSON.stringify(value) : String(value)}`)
89
+ .join(' ');
90
+ const error = context.err;
91
+ const detail = error instanceof Error ? ` (${error.message})` : '';
92
+ log(`${level}${message}${fields ? ` — ${fields}` : ''}${detail}`);
93
+ };
94
+ return {
95
+ info: (first, ...rest) => emit('', first, rest),
96
+ warn: (first, ...rest) => emit('warning: ', first, rest),
97
+ error: (first, ...rest) => emit('error: ', first, rest),
98
+ };
99
+ }
100
+ /**
101
+ * The `ForgeDeps` steps 3-5 read. Four seams, and the two the type also declares are omitted
102
+ * deliberately:
103
+ *
104
+ * - `model` — steps 3-5 never call an LLM (that is step 1, on api-server). The type requires it,
105
+ * so it is a client that THROWS rather than one that returns something plausible: a future
106
+ * change that starts calling a model from the browser-driven half must fail loudly, not
107
+ * silently produce a level designed by a stub. Same discipline api-server applies to the
108
+ * `transport` and `worldJson` seams it does not implement.
109
+ * - `assetCatalog` — omitted; absence degrades to "no reuse", never a failure. It feeds the
110
+ * DESIGNER (step 1), which does not run here, so it would have no reader in this lane even if
111
+ * the CLI built one.
112
+ * - `hqAssetIds` — omitted. A CLI project has no HQ regeneration queue, and the absent seam
113
+ * already means exactly "no jobs in flight" (see the field's own doc), so supplying an empty
114
+ * set would restate the default in a second place.
115
+ */
116
+ export function buildCliForgeDeps(options) {
117
+ return {
118
+ storage: options.storage,
119
+ transport: options.transport,
120
+ worldJson: createProjectWorldJsonSource(options.projectRoot),
121
+ logger: options.logger,
122
+ model: {
123
+ async generateText() {
124
+ throw new CliError('The CLI hosts steps 3-5 of the forge, which never call a language model — '
125
+ + 'the scene design runs on api-server. This is a bug in the CLI, not in your project.');
126
+ },
127
+ },
128
+ };
129
+ }
130
+ // ============================================================================
131
+ // Seeding the local store from what the server returned
132
+ // ============================================================================
133
+ /**
134
+ * Put the upstream artifacts into the local store so steps 3-5 can read them.
135
+ *
136
+ * `forge` is written unconditionally: it is what the server just handed back, and on a resume the
137
+ * server returns the SAME cached artifact (`forgeWorld` ends every run with `store.get('forge')`
138
+ * whether or not either step re-ran), so the write is idempotent rather than a clobber.
139
+ *
140
+ * `platformer-movement` is written whenever the terminal frame carried one, on the same reasoning:
141
+ * it is step 1's output, it lives in api-server's store, and step 5 reads it from THIS store to set
142
+ * `worldForgerMovement` on the level record. Only a `--platformer` design produces one, so absence
143
+ * is normal — and on an older server that does not send the field it is also possible, which is
144
+ * what `warnIfPlatformerMovementMissing` still exists to catch.
145
+ *
146
+ * `input` is written ONLY when absent, mirroring api-server's own freeze. It is the job's frozen
147
+ * request, and steps 4 and 5 read the level name, prompt and start-level intent off it — so a
148
+ * resume must persist what was originally asked for, not what this invocation's flags happen to
149
+ * say.
150
+ */
151
+ export async function seedForgeStore(store, artifact, input, platformerMovement) {
152
+ await store.put('forge', artifact);
153
+ if (platformerMovement) {
154
+ await store.put('platformer-movement', platformerMovement);
155
+ }
156
+ if (!(await store.has('input'))) {
157
+ await store.put('input', input);
158
+ }
159
+ }
160
+ /** Read a job's frozen `input`, or null if this machine has never run it. */
161
+ export async function readFrozenInput(storage, jobId) {
162
+ return createForgedWorkStore(jobId, storage).get('input');
163
+ }
164
+ /**
165
+ * Warn when a platformer forge is about to persist WITHOUT its movement contract.
166
+ *
167
+ * `platformer-movement` is written by step 1, into api-server's store. It reaches this lane on the
168
+ * terminal SSE frame and is written into the local store by `seedForgeStore`, so the normal path
169
+ * HAS it — this is the belt-and-braces for the one window that remains: a CLI talking to an
170
+ * api-server deployed before that field existed. The two ship independently, and that window is
171
+ * exactly where a creator pays for the flag and silently does not get it.
172
+ *
173
+ * Step 5 reads the artifact with `store.get` and omits `worldForgerMovement` when it is absent,
174
+ * which makes the gap look like a graceful degradation. It is not one worth being quiet about: the
175
+ * forge's completability guarantee is CONDITIONAL on that contract, and without it the engine falls
176
+ * back to its default walking/jumping kit — a jump too short to climb a level designed around the
177
+ * contract's numbers. The result is a paid-for platformer level that is not traversable.
178
+ *
179
+ * Purely local and purely advisory: it changes nothing, it cannot fail the forge, and the level is
180
+ * still worth having.
181
+ */
182
+ async function warnIfPlatformerMovementMissing(store, log) {
183
+ const input = await store.get('input');
184
+ if (input?.platformerMode !== true)
185
+ return;
186
+ if ((await store.get('platformer-movement')) !== null)
187
+ return;
188
+ log('warning: this platformer level is being saved WITHOUT its movement contract. The contract is '
189
+ + 'produced by the design step, which runs on the server, and the CLI lane cannot yet read it '
190
+ + 'back — so the character keeps the engine\'s default jump, which may be too short to climb '
191
+ + 'the level. If the level turns out not to be traversable, tune the character\'s jump height '
192
+ + 'and speed by hand rather than re-forging.');
193
+ }
194
+ /**
195
+ * Drive steps 3-5 in order, skipping any whose output artifact already exists — the same
196
+ * resume mechanism `run-world-forger.ts` uses, and the same one api-server applies to steps 1-2.
197
+ *
198
+ * The write half deliberately does NOT live in step 5: `placeAndPersistLevel` returns the
199
+ * modifications and the heightfield, and the host applies both. The ordering below is a contract,
200
+ * not a detail — `persisted` is the job-complete marker a resume returns directly, so recording it
201
+ * before the world.json write would let a failed write resume into "done" over an unchanged world.
202
+ * Unlike the web lane's applier (which catches everything and returns `{success:false}`), the
203
+ * CLI's `applyModificationsToWorld` THROWS, so here that ordering does real work.
204
+ */
205
+ export async function runForgePipeline(options) {
206
+ const { projectRoot, store, deps, log } = options;
207
+ if (await store.has('persisted')) {
208
+ const cached = await store.get('persisted');
209
+ if (cached) {
210
+ log(`This job was already applied to world.json — nothing to redo. Level "${cached.asset_name}".`);
211
+ return { summary: cached, applied: false, changes: [] };
212
+ }
213
+ }
214
+ if (await store.has('archetype-assets')) {
215
+ log('[3/5] Objects already created — skipping (resumed).');
216
+ }
217
+ else {
218
+ log('[3/5] Creating the level\'s objects in the browser…');
219
+ await createLevelArchetypes(store, deps);
220
+ log('[3/5] Objects created.');
221
+ }
222
+ if (await store.has('level-bake')) {
223
+ log('[4/5] Level already baked — skipping (resumed).');
224
+ }
225
+ else {
226
+ log('[4/5] Baking the level (this is the long one — progress follows)…');
227
+ await voxelizeLevel(store, deps);
228
+ log('[4/5] Level baked.');
229
+ }
230
+ await warnIfPlatformerMovementMissing(store, log);
231
+ log('[5/5] Placing instances and writing world.json…');
232
+ const { summary, modifications, terrainHeightGrid } = await placeAndPersistLevel(store, deps);
233
+ const worldPath = projectWorldJsonPath(projectRoot);
234
+ let outcome;
235
+ try {
236
+ outcome = applyModificationsToWorld(worldPath, modifications);
237
+ }
238
+ catch (error) {
239
+ // The single most expensive failure in the command, and the one a creator is most likely to
240
+ // misread: everything has been designed, forged, baked and placed — and paid for — and only
241
+ // the last write did not land. Saying WHICH is the difference between "resume this" and
242
+ // "start over". `applyModificationsToWorld` writes through a temp file and a rename and
243
+ // validates the document before touching it, so "unchanged" here is a fact, not a hope.
244
+ const detail = error instanceof CliError ? error.message : String(error);
245
+ throw new CliError(`The level was designed, forged and baked successfully, but could not be written into `
246
+ + `world.json: ${detail} world.json is UNCHANGED — the level is not lost, it has been paid `
247
+ + 'for and is saved server-side, but it is not yet in your project. Fix the problem above '
248
+ + 'and resume this job; nothing will be re-forged or re-billed.', error instanceof CliError ? error.exitCode : 1);
249
+ }
250
+ // The world-space heightfield, next to world.json, so objects placed AFTER this forge can
251
+ // resolve `placeOnTerrain` Y on the forged surface — a placement raycast cannot, because the
252
+ // new terrain only loads on the next run. Best-effort by design: a missing sidecar just falls
253
+ // back to runtime snapping, and losing it must never fail a forge that has already landed.
254
+ if (terrainHeightGrid) {
255
+ const sidecarPath = path.join(path.dirname(worldPath), FORGED_HEIGHTFIELD_FILE);
256
+ try {
257
+ fs.writeFileSync(sidecarPath, JSON.stringify(terrainHeightGrid));
258
+ log(`Wrote the terrain heightfield sidecar (${terrainHeightGrid.nx}x${terrainHeightGrid.nz} cells).`);
259
+ }
260
+ catch (error) {
261
+ const detail = error instanceof Error ? error.message : String(error);
262
+ log(`warning: could not write ${FORGED_HEIGHTFIELD_FILE} (${detail}). `
263
+ + 'Objects placed later will fall back to runtime terrain snapping.');
264
+ }
265
+ }
266
+ await store.put('persisted', summary);
267
+ return { summary, applied: true, changes: outcome.summary };
268
+ }
269
+ //# sourceMappingURL=run-pipeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-pipeline.js","sourceRoot":"","sources":["../../src/forge/run-pipeline.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The client for `POST /api/cli/v1/forge/stream` — steps 1-2 of the world forger (LLM scene
3
+ * design, then geometry forge + GLB upload), which run on api-server because they are the paid,
4
+ * private half. The terminal frame carries the `ForgeArtifact` steps 3-5 need; the `jobId` that
5
+ * makes the work resumable is on EVERY frame, terminal or not, and is tracked as such — see
6
+ * `lastJobId` in {@link requestForge}.
7
+ *
8
+ * Mirrors `generate/stream.ts` in shape (same SSE plumbing, same pre-stream status mapping), but
9
+ * the terminal payload is different — `{ success, jobId, artifact }` rather than
10
+ * `{ success, message, patches }` — and so is what a failure means to the creator, which is what
11
+ * {@link classifyForgeFailure} is for.
12
+ */
13
+ import type { ForgeArtifact } from '@bitmagic/world-forger/pipeline/index.js';
14
+ import type { Environment } from '../config/environments.js';
15
+ export interface ForgeSuccess {
16
+ success: true;
17
+ jobId: string;
18
+ artifact: ForgeArtifact;
19
+ /**
20
+ * Step 1's `platformer-movement` artifact, when the design produced one (only a
21
+ * `--platformer` forge does). It lives in api-server's store, and steps 3-5 run against the
22
+ * CLI's own local store — so it has to travel on this frame or step 5 cannot read it. See
23
+ * `seedForgeStore`, which writes it into the local store, and
24
+ * `warnIfPlatformerMovementMissing`, which stays as the older-server fallback.
25
+ */
26
+ platformerMovement?: Record<string, unknown>;
27
+ }
28
+ export interface ForgeFailure {
29
+ success: false;
30
+ jobId: string | null;
31
+ message: string;
32
+ /** Present when the failure carried a forged GLB the creator can still voxelize by hand. */
33
+ glbUrl?: string;
34
+ /**
35
+ * The server's own request deadline fired — the run was cut off mid-flight rather than failing.
36
+ * See {@link isDeadlineFailure} for why this cannot be inferred from {@link progress}.
37
+ */
38
+ timedOut: boolean;
39
+ /** Every progress line the server sent, in order — the input to {@link classifyForgeFailure}. */
40
+ progress: string[];
41
+ }
42
+ export type ForgeOutcome = ForgeSuccess | ForgeFailure;
43
+ /**
44
+ * Which of steps 1-2 failed, which decides the ONLY thing a creator actually wants to know:
45
+ * whether resuming this job can possibly help.
46
+ *
47
+ * - `design` — step 1 writes its `spec` artifact only on success, so nothing was saved. A
48
+ * resume re-runs the design and is billed for it again. It CAN succeed (the designer is
49
+ * non-deterministic and retries internally), but it is not free.
50
+ * - `forge` — step 2 is pure geometry over an already-cached `spec`. It is deterministic: a
51
+ * resume feeds the identical spec to the identical code and fails identically. The web lane
52
+ * says the same thing structurally, with `DETERMINISTIC_STEPS = new Set(['forge'])` in
53
+ * `game-play-agent/src/mastra/tools/run-world-forger.ts`. The fix is a different prompt, not
54
+ * a retry.
55
+ * - `unknown` — we could not tell; the message states both cases rather than guessing.
56
+ */
57
+ export type ForgeFailureStage = 'design' | 'forge' | 'unknown';
58
+ export declare function isDeadlineFailure(failure: Pick<ForgeFailure, 'timedOut' | 'message'>): boolean;
59
+ /**
60
+ * What to tell a creator whose run hit the server deadline: resume, unconditionally. No stage
61
+ * advice, because no stage failed.
62
+ */
63
+ export declare function forgeDeadlineAdvice(jobId: string | null): string;
64
+ /**
65
+ * What to tell a creator whose stream ended with NO terminal frame at all.
66
+ *
67
+ * This is the deadline's twin and needs the same answer — the run was cut off mid-flight, api-server
68
+ * deliberately does not cancel it (`cli-forge.ts`'s run promise is left to finish), and whatever it
69
+ * completes is written to the job's store — except that here nothing said so, because no frame
70
+ * arrived to say it. Without a job id the creator's only move is a fresh `--prompt` run, which mints
71
+ * a NEW job id and re-runs (and re-bills) a design the abandoned job may already have finished and
72
+ * saved. That is why {@link requestForge} tracks the job id off EVERY frame rather than only the
73
+ * terminal one: api-server puts it on every progress frame precisely so this case has an answer.
74
+ */
75
+ export declare function forgeTruncationMessage(jobId: string | null): string;
76
+ export declare function classifyForgeFailure(progress: readonly string[]): ForgeFailureStage;
77
+ /**
78
+ * The resume advice for a failure, as a paragraph appended to the server's own message. Every
79
+ * branch says explicitly that world.json is unchanged, because a creator who does not know has to
80
+ * go and look — and a forge costs enough that "did I just pay for nothing, and is my project
81
+ * still intact?" is the first question.
82
+ */
83
+ export declare function forgeFailureAdvice(stage: ForgeFailureStage, jobId: string | null): string;
84
+ /**
85
+ * The complete message for a failed forge: the server's own text, then the ONE piece of advice
86
+ * that fits. The deadline check comes first and short-circuits the stage classification — see
87
+ * {@link isDeadlineFailure} for why the two must never both speak.
88
+ *
89
+ * The single entry point the command uses, so "which advice wins" is decided in one place and is
90
+ * testable without a network.
91
+ */
92
+ export declare function describeForgeFailure(failure: ForgeFailure): string;
93
+ export interface ForgeStreamDeps {
94
+ fetch: typeof globalThis.fetch;
95
+ onProgress: (message: string) => void;
96
+ }
97
+ /**
98
+ * POSTs to `/api/cli/v1/forge/stream` and consumes the SSE response.
99
+ *
100
+ * A non-2xx response fails before any stream opens (see `toCliError`). On 200, each `progress`
101
+ * event is forwarded AND recorded, and the terminal `result`/`error` event resolves this call. A
102
+ * stream that closes with no terminal event is a truncated connection, not a success — and it fails
103
+ * with {@link forgeTruncationMessage}, naming the job id the frames already reported so the
104
+ * expensive half can be resumed rather than paid for twice.
105
+ */
106
+ export declare function requestForge(environment: Environment, accessToken: string, body: Record<string, unknown>, deps: ForgeStreamDeps): Promise<ForgeOutcome>;