@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.
- package/dist/cli.d.ts +30 -0
- package/dist/cli.js +23 -11
- package/dist/cli.js.map +1 -1
- package/dist/commands/forge.d.ts +107 -0
- package/dist/commands/forge.js +393 -0
- package/dist/commands/forge.js.map +1 -0
- package/dist/commands/generate.js +58 -45
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/verify.js +16 -16
- package/dist/commands/verify.js.map +1 -1
- package/dist/forge/apply-modifications.d.ts +19 -0
- package/dist/forge/apply-modifications.js +184 -0
- package/dist/forge/apply-modifications.js.map +1 -0
- package/dist/forge/bake-progress.d.ts +44 -0
- package/dist/forge/bake-progress.js +77 -0
- package/dist/forge/bake-progress.js.map +1 -0
- package/dist/forge/browser-host.d.ts +100 -0
- package/dist/forge/browser-host.js +351 -0
- package/dist/forge/browser-host.js.map +1 -0
- package/dist/forge/local-store.d.ts +30 -0
- package/dist/forge/local-store.js +130 -0
- package/dist/forge/local-store.js.map +1 -0
- package/dist/forge/run-pipeline.d.ts +105 -0
- package/dist/forge/run-pipeline.js +269 -0
- package/dist/forge/run-pipeline.js.map +1 -0
- package/dist/forge/stream.d.ts +106 -0
- package/dist/forge/stream.js +286 -0
- package/dist/forge/stream.js.map +1 -0
- package/dist/forge/transport.d.ts +58 -0
- package/dist/forge/transport.js +145 -0
- package/dist/forge/transport.js.map +1 -0
- package/dist/forge/upload-proxy.d.ts +21 -0
- package/dist/forge/upload-proxy.js +134 -0
- package/dist/forge/upload-proxy.js.map +1 -0
- package/dist/generate/stream.d.ts +1 -10
- package/dist/generate/stream.js +5 -62
- package/dist/generate/stream.js.map +1 -1
- package/dist/http/sse.d.ts +40 -0
- package/dist/http/sse.js +98 -0
- package/dist/http/sse.js.map +1 -0
- package/dist/local-port.d.ts +30 -0
- package/dist/local-port.js +53 -0
- package/dist/local-port.js.map +1 -0
- package/dist/scaffold/project-files.d.ts +1 -0
- package/dist/scaffold/project-files.js +175 -5
- package/dist/scaffold/project-files.js.map +1 -1
- package/dist/scaffold/project.js +2 -1
- package/dist/scaffold/project.js.map +1 -1
- 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>;
|