@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,134 @@
1
+ /**
2
+ * A tiny loopback HTTP server that lets the ENGINE reach api-server's signed-upload endpoint.
3
+ *
4
+ * The engine uploads its own `.vxl`/`.vwld` bytes — hundreds of megabytes, so they never cross the
5
+ * message channel — by asking for a presigned PUT first. `StorageUploadUtil.ts` does that with a
6
+ * hardcoded path and no credentials:
7
+ *
8
+ * fetch(`${getAgentUrl()}/api/generate-upload-url`, {
9
+ * headers: { 'Content-Type': 'application/json' }, // no Authorization
10
+ *
11
+ * Neither half of that reaches api-server. The path it calls is game-play-agent's
12
+ * (`mastra/routes.ts`); api-server serves `POST /api/cli/v1/uploads/signed-url`. And every route
13
+ * under `/api/cli/v1/*` is Auth0 + Admin gated, while a browser page has no token — so pointing
14
+ * `agentUrl` straight at api-server answers 401 to every upload, which is exactly what the first
15
+ * end-to-end forge did: 14 archetypes dropped and the level bake failed after voxelizing all 392
16
+ * chunks.
17
+ *
18
+ * So the CLI bridges it instead of the engine changing. That direction is deliberate:
19
+ *
20
+ * - **The engine cannot be the fix.** Published games carry frozen template code compiled against
21
+ * the current engine (see the engine-API rule in AGENTS.md), and `StorageUploadUtil` is shared
22
+ * by every lane. Teaching it to send a bearer token is a breaking change to all of them to serve
23
+ * one.
24
+ * - **The token must not enter the page.** It stays in this process. The forge browser runs with
25
+ * `--disable-web-security`, so a token in page scope is worth strictly less than one outside it.
26
+ *
27
+ * Bound to 127.0.0.1 for the same reason: this server holds a creator's access token and answers
28
+ * with write credentials into Bitmagic's bucket. It must not be reachable off the machine.
29
+ *
30
+ * `gameId` is FORCED from the CLI's own project context rather than taken from the request body.
31
+ * api-server checks ownership regardless (`assertOwnsGame`, and the key is pinned to
32
+ * `worlds/v3/<gameId>/`), so this is defence in depth — but it is cheap, and it means a page that
33
+ * somehow holds the wrong id cannot even ask for the wrong namespace.
34
+ */
35
+ import * as http from 'http';
36
+ import { CliError } from '../errors.js';
37
+ /** The path the ENGINE calls. Not api-server's — that is what this proxy translates. */
38
+ export const ENGINE_UPLOAD_PATH = '/api/generate-upload-url';
39
+ /** The api-server route it forwards to. */
40
+ export const API_SERVER_UPLOAD_PATH = '/api/cli/v1/uploads/signed-url';
41
+ function readBody(req) {
42
+ return new Promise((resolve, reject) => {
43
+ let body = '';
44
+ req.on('data', (chunk) => { body += String(chunk); });
45
+ req.on('end', () => resolve(body));
46
+ req.on('error', reject);
47
+ });
48
+ }
49
+ /**
50
+ * CORS is answered explicitly rather than relying on `--disable-web-security`. The flag is there
51
+ * for the presigned PUT to GCS, not as this proxy's security model, and a request the browser
52
+ * refuses to make is indistinguishable from a server that is down.
53
+ */
54
+ function writeCors(res) {
55
+ res.setHeader('Access-Control-Allow-Origin', '*');
56
+ res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
57
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
58
+ }
59
+ export async function startUploadProxy(options) {
60
+ const log = options.log ?? (() => { });
61
+ const doFetch = options.fetchImpl ?? fetch;
62
+ const apiBase = options.apiUrl.replace(/\/$/, '');
63
+ const server = http.createServer((req, res) => {
64
+ void (async () => {
65
+ writeCors(res);
66
+ if (req.method === 'OPTIONS') {
67
+ res.writeHead(204).end();
68
+ return;
69
+ }
70
+ if (req.method !== 'POST' || req.url !== ENGINE_UPLOAD_PATH) {
71
+ // Named rather than silent: an engine that starts calling a different path should show up
72
+ // as this line in the forge log, not as a mute upload failure.
73
+ log(`[upload-proxy] ignoring ${req.method ?? '?'} ${req.url ?? '?'}`);
74
+ res.writeHead(404, { 'Content-Type': 'application/json' })
75
+ .end(JSON.stringify({ error: 'Not found' }));
76
+ return;
77
+ }
78
+ let parsed = {};
79
+ try {
80
+ const raw = await readBody(req);
81
+ if (raw)
82
+ parsed = JSON.parse(raw);
83
+ }
84
+ catch {
85
+ res.writeHead(400, { 'Content-Type': 'application/json' })
86
+ .end(JSON.stringify({ error: 'Request body must be JSON' }));
87
+ return;
88
+ }
89
+ try {
90
+ const upstream = await doFetch(`${apiBase}${API_SERVER_UPLOAD_PATH}`, {
91
+ method: 'POST',
92
+ headers: {
93
+ 'Content-Type': 'application/json',
94
+ Authorization: `Bearer ${options.token}`,
95
+ },
96
+ // `noCache` is dropped: api-server pins its own immutable cacheControl. gameId is ours,
97
+ // not the page's — see the header.
98
+ body: JSON.stringify({
99
+ gameId: options.gameId,
100
+ filename: parsed.filename,
101
+ contentType: parsed.contentType,
102
+ }),
103
+ });
104
+ const text = await upstream.text();
105
+ if (!upstream.ok) {
106
+ log(`[upload-proxy] api-server refused ${String(parsed.filename ?? '?')}: ${upstream.status} ${text.slice(0, 200)}`);
107
+ }
108
+ res.writeHead(upstream.status, { 'Content-Type': 'application/json' }).end(text);
109
+ }
110
+ catch (error) {
111
+ const message = error instanceof Error ? error.message : String(error);
112
+ log(`[upload-proxy] could not reach api-server: ${message}`);
113
+ res.writeHead(502, { 'Content-Type': 'application/json' })
114
+ .end(JSON.stringify({ error: `Could not reach api-server: ${message}` }));
115
+ }
116
+ })();
117
+ });
118
+ await new Promise((resolve, reject) => {
119
+ server.once('error', reject);
120
+ server.listen(0, '127.0.0.1', resolve);
121
+ });
122
+ const address = server.address();
123
+ if (address === null || typeof address === 'string') {
124
+ server.close();
125
+ throw new CliError('The upload proxy could not bind a local port, so the level cannot be baked.');
126
+ }
127
+ const url = `http://127.0.0.1:${address.port}`;
128
+ log(`Upload proxy on ${url} → ${apiBase}${API_SERVER_UPLOAD_PATH}`);
129
+ return {
130
+ url,
131
+ close: () => new Promise((resolve) => server.close(() => resolve())),
132
+ };
133
+ }
134
+ //# sourceMappingURL=upload-proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload-proxy.js","sourceRoot":"","sources":["../../src/forge/upload-proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAExC,wFAAwF;AACxF,MAAM,CAAC,MAAM,kBAAkB,GAAG,0BAA0B,CAAC;AAE7D,2CAA2C;AAC3C,MAAM,CAAC,MAAM,sBAAsB,GAAG,gCAAgC,CAAC;AAoBvE,SAAS,QAAQ,CAAC,GAAyB;IACzC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,GAAG,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACtD,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACnC,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,SAAS,SAAS,CAAC,GAAwB;IACzC,GAAG,CAAC,SAAS,CAAC,6BAA6B,EAAE,GAAG,CAAC,CAAC;IAClD,GAAG,CAAC,SAAS,CAAC,8BAA8B,EAAE,eAAe,CAAC,CAAC;IAC/D,GAAG,CAAC,SAAS,CAAC,8BAA8B,EAAE,cAAc,CAAC,CAAC;AAChE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAA2B;IAChE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACtC,MAAM,OAAO,GAAG,OAAO,CAAC,SAAS,IAAI,KAAK,CAAC;IAC3C,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAElD,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QAC5C,KAAK,CAAC,KAAK,IAAI,EAAE;YACf,SAAS,CAAC,GAAG,CAAC,CAAC;YACf,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;gBAC7B,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;gBACzB,OAAO;YACT,CAAC;YACD,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,IAAI,GAAG,CAAC,GAAG,KAAK,kBAAkB,EAAE,CAAC;gBAC5D,0FAA0F;gBAC1F,+DAA+D;gBAC/D,GAAG,CAAC,2BAA2B,GAAG,CAAC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC;gBACtE,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC;qBACvD,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC;gBAC/C,OAAO;YACT,CAAC;YAED,IAAI,MAAM,GAA4B,EAAE,CAAC;YACzC,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC;gBAChC,IAAI,GAAG;oBAAE,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA4B,CAAC;YAC/D,CAAC;YAAC,MAAM,CAAC;gBACP,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC;qBACvD,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC,CAAC,CAAC;gBAC/D,OAAO;YACT,CAAC;YAED,IAAI,CAAC;gBACH,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,OAAO,GAAG,sBAAsB,EAAE,EAAE;oBACpE,MAAM,EAAE,MAAM;oBACd,OAAO,EAAE;wBACP,cAAc,EAAE,kBAAkB;wBAClC,aAAa,EAAE,UAAU,OAAO,CAAC,KAAK,EAAE;qBACzC;oBACD,wFAAwF;oBACxF,mCAAmC;oBACnC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;wBACnB,MAAM,EAAE,OAAO,CAAC,MAAM;wBACtB,QAAQ,EAAE,MAAM,CAAC,QAAQ;wBACzB,WAAW,EAAE,MAAM,CAAC,WAAW;qBAChC,CAAC;iBACH,CAAC,CAAC;gBACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;gBACnC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;oBACjB,GAAG,CAAC,qCAAqC,MAAM,CAAC,MAAM,CAAC,QAAQ,IAAI,GAAG,CAAC,KAAK,QAAQ,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;gBACvH,CAAC;gBACD,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACnF,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACvE,GAAG,CAAC,8CAA8C,OAAO,EAAE,CAAC,CAAC;gBAC7D,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC;qBACvD,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,+BAA+B,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;YAC9E,CAAC;QACH,CAAC,CAAC,EAAE,CAAC;IACP,CAAC,CAAC,CAAC;IAEH,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC1C,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC7B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;IACjC,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QACpD,MAAM,CAAC,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,QAAQ,CAAC,6EAA6E,CAAC,CAAC;IACpG,CAAC;IACD,MAAM,GAAG,GAAG,oBAAoB,OAAO,CAAC,IAAI,EAAE,CAAC;IAC/C,GAAG,CAAC,mBAAmB,GAAG,MAAM,OAAO,GAAG,sBAAsB,EAAE,CAAC,CAAC;IAEpE,OAAO;QACL,GAAG;QACH,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;KAC3E,CAAC;AACJ,CAAC"}
@@ -1,15 +1,6 @@
1
1
  import type { WorldPatch } from '@bitmagic/asset-core';
2
2
  import type { Environment } from '../config/environments.js';
3
- export interface SseEvent {
4
- event: string;
5
- data: unknown;
6
- }
7
- /**
8
- * Pure, synchronous parse of a full set of chunks — cheap and exhaustive to test. Production
9
- * streaming uses the same underlying buffer (`createSseFrameBuffer`) one network read at a time,
10
- * via `requestGeneration`, so this function is not itself on that path.
11
- */
12
- export declare function parseSseChunks(chunks: string[]): SseEvent[];
3
+ export { parseSseChunks, type SseEvent } from '../http/sse.js';
13
4
  export interface GenerationOutcome {
14
5
  success: boolean;
15
6
  message: string;
@@ -1,66 +1,9 @@
1
1
  import { CliError } from '../errors.js';
2
- /**
3
- * The unit of `event:`/`data:` lines terminated by a blank line, per the SSE wire format
4
- * `api-server/src/cli/cli-assets.ts` writes. Splits on `\n`, joining multiple `data:` lines (the
5
- * spec allows more than one) with `\n` before parsing as JSON. A leading `:` line (the server's
6
- * `: keepalive` comment) carries no `data:`, so it naturally produces no event.
7
- */
8
- function parseFrame(frame) {
9
- let eventName = 'message';
10
- const dataLines = [];
11
- for (const line of frame.split('\n')) {
12
- if (line.startsWith('event:')) {
13
- eventName = line.slice('event:'.length).trim();
14
- }
15
- else if (line.startsWith('data:')) {
16
- dataLines.push(line.slice('data:'.length).trim());
17
- }
18
- // Any other line (including a `:`-prefixed comment, or blank) carries no event data.
19
- }
20
- if (dataLines.length === 0)
21
- return null;
22
- return { event: eventName, data: JSON.parse(dataLines.join('\n')) };
23
- }
24
- /**
25
- * Incremental frame buffer: SSE frames arrive in arbitrary chunks over the wire — one frame can
26
- * span two reads, and two frames can arrive in one — so this accumulates bytes and only emits
27
- * events once a full `\n\n`-delimited frame is available. The trailing, not-yet-delimited
28
- * remainder stays in `buffer` for the next push. Shared by both `parseSseChunks` (fed the whole
29
- * chunk array at once) and `requestGeneration` (fed one chunk per network read), so the two
30
- * cannot drift apart.
31
- */
32
- function createSseFrameBuffer() {
33
- let buffer = '';
34
- return {
35
- push(chunk) {
36
- buffer += chunk;
37
- const frames = buffer.split('\n\n');
38
- // The last element is either '' (buffer ended exactly on a delimiter) or an incomplete
39
- // trailing frame — either way it is not yet a complete frame, so keep it buffered.
40
- buffer = frames.pop() ?? '';
41
- const events = [];
42
- for (const frame of frames) {
43
- const event = parseFrame(frame);
44
- if (event)
45
- events.push(event);
46
- }
47
- return events;
48
- },
49
- };
50
- }
51
- /**
52
- * Pure, synchronous parse of a full set of chunks — cheap and exhaustive to test. Production
53
- * streaming uses the same underlying buffer (`createSseFrameBuffer`) one network read at a time,
54
- * via `requestGeneration`, so this function is not itself on that path.
55
- */
56
- export function parseSseChunks(chunks) {
57
- const frameBuffer = createSseFrameBuffer();
58
- const events = [];
59
- for (const chunk of chunks) {
60
- events.push(...frameBuffer.push(chunk));
61
- }
62
- return events;
63
- }
2
+ import { createSseFrameBuffer } from '../http/sse.js';
3
+ // The frame buffer and its parser moved to ../http/sse.ts when `bitmagic forge` gained a second
4
+ // consumer of the identical wire format. Re-exported from here so this module's own callers and
5
+ // tests are unaffected by that move.
6
+ export { parseSseChunks } from '../http/sse.js';
64
7
  function isRecord(value) {
65
8
  return typeof value === 'object' && value !== null;
66
9
  }
@@ -1 +1 @@
1
- {"version":3,"file":"stream.js","sourceRoot":"","sources":["../../src/generate/stream.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"stream.js","sourceRoot":"","sources":["../../src/generate/stream.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The SSE wire format the CLI-lane endpoints speak (`api-server/src/cli/cli-assets.ts`,
3
+ * `cli-forge.ts`): `event:` / `data:` lines terminated by a blank line, with `: keepalive`
4
+ * comment lines interleaved.
5
+ *
6
+ * Extracted from `generate/stream.ts` — which still re-exports `parseSseChunks` so its own
7
+ * tests and callers are unchanged — because `forge/stream.ts` consumes the identical format
8
+ * from a different endpoint. Two copies of a frame buffer that must agree byte-for-byte with
9
+ * one server is exactly the kind of thing that drifts silently: a fix to one (a multi-line
10
+ * `data:`, a chunk boundary mid-frame) would not reach the other, and the symptom would be a
11
+ * dropped terminal frame on one command only.
12
+ */
13
+ export interface SseEvent {
14
+ event: string;
15
+ data: unknown;
16
+ }
17
+ /**
18
+ * Incremental frame buffer: SSE frames arrive in arbitrary chunks over the wire — one frame can
19
+ * span two reads, and two frames can arrive in one — so this accumulates bytes and only emits
20
+ * events once a full `\n\n`-delimited frame is available. The trailing, not-yet-delimited
21
+ * remainder stays in `buffer` for the next push.
22
+ */
23
+ export declare function createSseFrameBuffer(): {
24
+ push: (chunk: string) => SseEvent[];
25
+ };
26
+ /**
27
+ * Pure, synchronous parse of a full set of chunks — cheap and exhaustive to test. Production
28
+ * streaming uses the same underlying buffer one network read at a time, so this function is not
29
+ * itself on that path.
30
+ */
31
+ export declare function parseSseChunks(chunks: string[]): SseEvent[];
32
+ /**
33
+ * Read an SSE response body to completion, handing every parsed event to `onEvent`. Stops early
34
+ * — and returns `true` — the moment `onEvent` reports the terminal frame arrived, so a server
35
+ * that holds the connection open after its result does not stall the command.
36
+ *
37
+ * Returns `false` when the stream ended with no terminal frame: a truncated connection, which
38
+ * every caller must report as "not a confirmed failure" rather than as a clean empty result.
39
+ */
40
+ export declare function consumeSseStream(body: ReadableStream<Uint8Array>, onEvent: (event: SseEvent) => boolean): Promise<boolean>;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * The SSE wire format the CLI-lane endpoints speak (`api-server/src/cli/cli-assets.ts`,
3
+ * `cli-forge.ts`): `event:` / `data:` lines terminated by a blank line, with `: keepalive`
4
+ * comment lines interleaved.
5
+ *
6
+ * Extracted from `generate/stream.ts` — which still re-exports `parseSseChunks` so its own
7
+ * tests and callers are unchanged — because `forge/stream.ts` consumes the identical format
8
+ * from a different endpoint. Two copies of a frame buffer that must agree byte-for-byte with
9
+ * one server is exactly the kind of thing that drifts silently: a fix to one (a multi-line
10
+ * `data:`, a chunk boundary mid-frame) would not reach the other, and the symptom would be a
11
+ * dropped terminal frame on one command only.
12
+ */
13
+ /**
14
+ * One `\n\n`-delimited frame. Joins multiple `data:` lines (the spec allows more than one)
15
+ * with `\n` before parsing as JSON. A leading `:` line (the server's `: keepalive` comment)
16
+ * carries no `data:`, so it naturally produces no event.
17
+ */
18
+ function parseFrame(frame) {
19
+ let eventName = 'message';
20
+ const dataLines = [];
21
+ for (const line of frame.split('\n')) {
22
+ if (line.startsWith('event:')) {
23
+ eventName = line.slice('event:'.length).trim();
24
+ }
25
+ else if (line.startsWith('data:')) {
26
+ dataLines.push(line.slice('data:'.length).trim());
27
+ }
28
+ // Any other line (including a `:`-prefixed comment, or blank) carries no event data.
29
+ }
30
+ if (dataLines.length === 0)
31
+ return null;
32
+ return { event: eventName, data: JSON.parse(dataLines.join('\n')) };
33
+ }
34
+ /**
35
+ * Incremental frame buffer: SSE frames arrive in arbitrary chunks over the wire — one frame can
36
+ * span two reads, and two frames can arrive in one — so this accumulates bytes and only emits
37
+ * events once a full `\n\n`-delimited frame is available. The trailing, not-yet-delimited
38
+ * remainder stays in `buffer` for the next push.
39
+ */
40
+ export function createSseFrameBuffer() {
41
+ let buffer = '';
42
+ return {
43
+ push(chunk) {
44
+ buffer += chunk;
45
+ const frames = buffer.split('\n\n');
46
+ // The last element is either '' (buffer ended exactly on a delimiter) or an incomplete
47
+ // trailing frame — either way it is not yet a complete frame, so keep it buffered.
48
+ buffer = frames.pop() ?? '';
49
+ const events = [];
50
+ for (const frame of frames) {
51
+ const event = parseFrame(frame);
52
+ if (event)
53
+ events.push(event);
54
+ }
55
+ return events;
56
+ },
57
+ };
58
+ }
59
+ /**
60
+ * Pure, synchronous parse of a full set of chunks — cheap and exhaustive to test. Production
61
+ * streaming uses the same underlying buffer one network read at a time, so this function is not
62
+ * itself on that path.
63
+ */
64
+ export function parseSseChunks(chunks) {
65
+ const frameBuffer = createSseFrameBuffer();
66
+ const events = [];
67
+ for (const chunk of chunks) {
68
+ events.push(...frameBuffer.push(chunk));
69
+ }
70
+ return events;
71
+ }
72
+ /**
73
+ * Read an SSE response body to completion, handing every parsed event to `onEvent`. Stops early
74
+ * — and returns `true` — the moment `onEvent` reports the terminal frame arrived, so a server
75
+ * that holds the connection open after its result does not stall the command.
76
+ *
77
+ * Returns `false` when the stream ended with no terminal frame: a truncated connection, which
78
+ * every caller must report as "not a confirmed failure" rather than as a clean empty result.
79
+ */
80
+ export async function consumeSseStream(body, onEvent) {
81
+ const reader = body.getReader();
82
+ const decoder = new TextDecoder();
83
+ const frameBuffer = createSseFrameBuffer();
84
+ for (;;) {
85
+ const { value, done } = await reader.read();
86
+ if (done)
87
+ return false;
88
+ // `{ stream: true }` retains any trailing partial multi-byte UTF-8 sequence inside the
89
+ // decoder itself, so a character split across two network chunks decodes correctly once its
90
+ // remaining bytes arrive in the next read, instead of emitting U+FFFD.
91
+ const text = decoder.decode(value, { stream: true });
92
+ for (const event of frameBuffer.push(text)) {
93
+ if (onEvent(event))
94
+ return true;
95
+ }
96
+ }
97
+ }
98
+ //# sourceMappingURL=sse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.js","sourceRoot":"","sources":["../../src/http/sse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The port window the platform's bucket CORS policy allowlists as an origin.
3
+ *
4
+ * A scaffolded project fetches its forged level (`.vwld`) and its GLBs from the portal CDN, so a
5
+ * project served on localhost is making CROSS-ORIGIN requests for them. The bucket answers with
6
+ * `Access-Control-Allow-Origin` only for the origins named in its CORS config — the portal and
7
+ * creator hosts, plus `http://localhost:3000`–`3199` (roleverse-infra,
8
+ * the portal-cdn terragrunt config per environment). `bitmagic dev` sits inside that window at
9
+ * 3010; anything that asks the OS for an ephemeral port (`listen(0)` → 49xxx+) falls outside it.
10
+ *
11
+ * The failure is quiet and easy to misread. The fetch is blocked, `WorldGenerator` falls back to
12
+ * an empty voxel world, the level's terrain simply is not there, and the symptom is a player
13
+ * falling through the floor forever — which reads as a broken forge, not a blocked request. The
14
+ * forge's own browser hides it too, because it runs with `--disable-web-security` for the presigned
15
+ * PUTs, so uploads and reads both succeed there while a normal browser fails.
16
+ */
17
+ export declare const CORS_ALLOWED_PORT_MIN = 3000;
18
+ export declare const CORS_ALLOWED_PORT_MAX = 3199;
19
+ /**
20
+ * A free port the CDN will serve assets to, or `null` when the whole window is taken.
21
+ *
22
+ * Returning `null` rather than silently falling back to an ephemeral port is deliberate: the
23
+ * caller can then say what the consequence is, instead of the run proceeding to a level with no
24
+ * terrain and no explanation.
25
+ *
26
+ * The scan starts at a pseudo-random offset so two concurrent runs do not both take 3000 and race;
27
+ * `tryListen` closing before the caller binds leaves a window either way, which is why callers
28
+ * must handle a bind failure rather than trusting this.
29
+ */
30
+ export declare function reserveCorsAllowedPort(): Promise<number | null>;
@@ -0,0 +1,53 @@
1
+ import * as net from 'net';
2
+ /**
3
+ * The port window the platform's bucket CORS policy allowlists as an origin.
4
+ *
5
+ * A scaffolded project fetches its forged level (`.vwld`) and its GLBs from the portal CDN, so a
6
+ * project served on localhost is making CROSS-ORIGIN requests for them. The bucket answers with
7
+ * `Access-Control-Allow-Origin` only for the origins named in its CORS config — the portal and
8
+ * creator hosts, plus `http://localhost:3000`–`3199` (roleverse-infra,
9
+ * the portal-cdn terragrunt config per environment). `bitmagic dev` sits inside that window at
10
+ * 3010; anything that asks the OS for an ephemeral port (`listen(0)` → 49xxx+) falls outside it.
11
+ *
12
+ * The failure is quiet and easy to misread. The fetch is blocked, `WorldGenerator` falls back to
13
+ * an empty voxel world, the level's terrain simply is not there, and the symptom is a player
14
+ * falling through the floor forever — which reads as a broken forge, not a blocked request. The
15
+ * forge's own browser hides it too, because it runs with `--disable-web-security` for the presigned
16
+ * PUTs, so uploads and reads both succeed there while a normal browser fails.
17
+ */
18
+ export const CORS_ALLOWED_PORT_MIN = 3000;
19
+ export const CORS_ALLOWED_PORT_MAX = 3199;
20
+ function tryListen(port) {
21
+ return new Promise((resolve) => {
22
+ const server = net.createServer();
23
+ server.once('error', () => resolve(null));
24
+ server.listen(port, '127.0.0.1', () => {
25
+ const address = server.address();
26
+ const bound = address !== null && typeof address !== 'string' ? address.port : null;
27
+ server.close(() => resolve(bound));
28
+ });
29
+ });
30
+ }
31
+ /**
32
+ * A free port the CDN will serve assets to, or `null` when the whole window is taken.
33
+ *
34
+ * Returning `null` rather than silently falling back to an ephemeral port is deliberate: the
35
+ * caller can then say what the consequence is, instead of the run proceeding to a level with no
36
+ * terrain and no explanation.
37
+ *
38
+ * The scan starts at a pseudo-random offset so two concurrent runs do not both take 3000 and race;
39
+ * `tryListen` closing before the caller binds leaves a window either way, which is why callers
40
+ * must handle a bind failure rather than trusting this.
41
+ */
42
+ export async function reserveCorsAllowedPort() {
43
+ const span = CORS_ALLOWED_PORT_MAX - CORS_ALLOWED_PORT_MIN + 1;
44
+ const start = Math.floor(Math.random() * span);
45
+ for (let i = 0; i < span; i += 1) {
46
+ const port = CORS_ALLOWED_PORT_MIN + ((start + i) % span);
47
+ const bound = await tryListen(port);
48
+ if (bound !== null)
49
+ return bound;
50
+ }
51
+ return null;
52
+ }
53
+ //# sourceMappingURL=local-port.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-port.js","sourceRoot":"","sources":["../src/local-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAE3B;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAC1C,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAE1C,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,EAAE,CAAC;QAClC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QAC1C,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE;YACpC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;YACjC,MAAM,KAAK,GAAG,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;YACpF,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;QACrC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB;IAC1C,MAAM,IAAI,GAAG,qBAAqB,GAAG,qBAAqB,GAAG,CAAC,CAAC;IAC/D,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC;IAC/C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,qBAAqB,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;QAC1D,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;IACnC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -11,5 +11,6 @@ export interface ProjectMetadata {
11
11
  }
12
12
  export declare function renderBitmagicJson(metadata: ProjectMetadata): string;
13
13
  export declare function renderAgentsMd(): string;
14
+ export declare function renderGenerateSkill(): string;
14
15
  export declare function renderClaudeMd(): string;
15
16
  export declare function renderSkillsReadme(): string;