@bitmagic/cli 0.1.7 → 0.1.9

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 (52) hide show
  1. package/README.md +140 -0
  2. package/dist/auth/session.js +1 -1
  3. package/dist/auth/session.js.map +1 -1
  4. package/dist/cli.d.ts +29 -0
  5. package/dist/cli.js +6 -0
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/build.d.ts +1 -0
  8. package/dist/commands/build.js +16 -0
  9. package/dist/commands/build.js.map +1 -0
  10. package/dist/commands/publish.d.ts +44 -0
  11. package/dist/commands/publish.js +202 -0
  12. package/dist/commands/publish.js.map +1 -0
  13. package/dist/commands/upgrade.d.ts +36 -0
  14. package/dist/commands/upgrade.js +170 -0
  15. package/dist/commands/upgrade.js.map +1 -0
  16. package/dist/commands/verify.js +8 -9
  17. package/dist/commands/verify.js.map +1 -1
  18. package/dist/forge/apply-modifications.js +7 -13
  19. package/dist/forge/apply-modifications.js.map +1 -1
  20. package/dist/project/context.js +6 -1
  21. package/dist/project/context.js.map +1 -1
  22. package/dist/project/package-manager.d.ts +17 -0
  23. package/dist/project/package-manager.js +48 -0
  24. package/dist/project/package-manager.js.map +1 -0
  25. package/dist/publish/bundle.d.ts +37 -0
  26. package/dist/publish/bundle.js +88 -0
  27. package/dist/publish/bundle.js.map +1 -0
  28. package/dist/publish/client.d.ts +94 -0
  29. package/dist/publish/client.js +150 -0
  30. package/dist/publish/client.js.map +1 -0
  31. package/dist/publish/fingerprint.d.ts +27 -0
  32. package/dist/publish/fingerprint.js +84 -0
  33. package/dist/publish/fingerprint.js.map +1 -0
  34. package/dist/publish/meta-block.d.ts +26 -0
  35. package/dist/publish/meta-block.js +48 -0
  36. package/dist/publish/meta-block.js.map +1 -0
  37. package/dist/scaffold/dependency-diff.d.ts +28 -0
  38. package/dist/scaffold/dependency-diff.js +28 -0
  39. package/dist/scaffold/dependency-diff.js.map +1 -0
  40. package/dist/scaffold/project-files.d.ts +22 -0
  41. package/dist/scaffold/project-files.js +114 -5
  42. package/dist/scaffold/project-files.js.map +1 -1
  43. package/dist/scaffold/project.d.ts +17 -0
  44. package/dist/scaffold/project.js +24 -4
  45. package/dist/scaffold/project.js.map +1 -1
  46. package/dist/scaffold/upgrade-project.d.ts +35 -0
  47. package/dist/scaffold/upgrade-project.js +63 -0
  48. package/dist/scaffold/upgrade-project.js.map +1 -0
  49. package/dist/verify/result.d.ts +60 -0
  50. package/dist/verify/result.js +74 -0
  51. package/dist/verify/result.js.map +1 -0
  52. package/package.json +3 -3
@@ -0,0 +1,94 @@
1
+ import type { Environment } from '../config/environments.js';
2
+ export interface ApiDeps {
3
+ fetch: typeof globalThis.fetch;
4
+ }
5
+ /**
6
+ * One status→exit-code mapping for both publish calls.
7
+ *
8
+ * The distinction that matters is 4 vs 5: "your engine is too old" is self-serve
9
+ * (`bitmagic upgrade`), while a ban or an ownership mismatch is not. An agent that cannot
10
+ * tell them apart will either loop on upgrade or give up on a fixable problem.
11
+ */
12
+ export declare function publishExitCode(status: number, body: {
13
+ code?: string;
14
+ }): number;
15
+ export interface BeginPublishRequest {
16
+ gameId: string;
17
+ visibility: 'public' | 'private';
18
+ name?: string;
19
+ description?: string;
20
+ engineVersion: string;
21
+ thumbnailUrl: string;
22
+ }
23
+ export interface BeginPublishResponse {
24
+ signedUrl: string;
25
+ key: string;
26
+ publishVersion: number;
27
+ metaBlock: string;
28
+ cacheControl: string;
29
+ }
30
+ export declare function beginPublish(environment: Environment, accessToken: string, body: BeginPublishRequest, deps: ApiDeps): Promise<BeginPublishResponse>;
31
+ export interface CompletePublishRequest {
32
+ gameId: string;
33
+ key: string;
34
+ publishVersion: number;
35
+ visibility: 'public' | 'private';
36
+ name?: string;
37
+ description?: string;
38
+ thumbnailUrl: string;
39
+ bundleBytes: number;
40
+ sha256: string;
41
+ /**
42
+ * The audit trail the spec calls for: `engineVersion`, `fingerprint` and `sha256` recorded
43
+ * together for the publish that actually landed. All three are declarative — the server logs
44
+ * them, it does not verify them (there is no column for any of them today, and verifying the
45
+ * hash would mean reading bytes this design deliberately never reads).
46
+ *
47
+ * `engineVersion` is NOT a second floor gate. It cannot be: `begin` is the only step that
48
+ * mints the signed URL, so its floor check is already unavoidable, and a value re-declared
49
+ * here proves nothing about what the bundle was built with. It is sent so the log line that
50
+ * records a publish says which engine produced it — without it, the only record of a bundle's
51
+ * engine is a `begin` log entry that may belong to an abandoned attempt.
52
+ *
53
+ * `fingerprint` is the same hash `verify` recorded and `publish` gated on, so a support
54
+ * question about a published bundle can be tied back to the exact project state that produced
55
+ * it. Nothing else on the wire identifies the source.
56
+ */
57
+ engineVersion: string;
58
+ fingerprint: string;
59
+ }
60
+ export interface CompletePublishResponse {
61
+ url: string;
62
+ publishVersion: number;
63
+ visibility: 'public' | 'private';
64
+ }
65
+ export declare function completePublish(environment: Environment, accessToken: string, body: CompletePublishRequest, deps: ApiDeps): Promise<CompletePublishResponse>;
66
+ /**
67
+ * `begin` always signs `text/html` for the entry HTML (cli-publish.ts's `getSignedUrl` call) but
68
+ * does not echo a `contentType` field back in its response — there is nothing to echo, so this
69
+ * fixed value is the one Content-Type that can ever be correct for the PUT.
70
+ */
71
+ export declare const PUBLISH_BUNDLE_CONTENT_TYPE = "text/html";
72
+ /** PUT the built (and meta-block-injected) bundle to the signed URL `begin` minted. */
73
+ export declare function putBundle(signedUrl: string, html: string, cacheControl: string, deps: ApiDeps): Promise<void>;
74
+ export interface ThumbnailUploadRequest {
75
+ gameId: string;
76
+ filename: string;
77
+ contentType: string;
78
+ }
79
+ /** `cli-uploads.ts`'s `generateUploadSignedUrl` response shape, the fields this lane uses. */
80
+ export interface ThumbnailUploadUrl {
81
+ signedUrl: string;
82
+ publicUrl: string;
83
+ cacheControl: string;
84
+ contentEncoding?: string;
85
+ }
86
+ export declare function requestThumbnailUploadUrl(environment: Environment, accessToken: string, body: ThumbnailUploadRequest, deps: ApiDeps): Promise<ThumbnailUploadUrl>;
87
+ /**
88
+ * PUT the thumbnail bytes to the signed URL `requestThumbnailUploadUrl` minted. `contentType` is
89
+ * the SAME value this lane requested (the server never echoes it back — see
90
+ * `PUBLISH_BUNDLE_CONTENT_TYPE`'s doc comment for why that is fine: the caller already knows it),
91
+ * while `cacheControl` and `contentEncoding` are read back from the signed-URL response, because
92
+ * the server — not this lane — decides both.
93
+ */
94
+ export declare function putThumbnail(signed: ThumbnailUploadUrl, bytes: Uint8Array, contentType: string, deps: ApiDeps): Promise<void>;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * The HTTP client for `bitmagic publish`: the two authorizing calls to api-server
3
+ * (`publish/begin`, `publish/complete`), the thumbnail's signed-upload lane
4
+ * (`uploads/signed-url`, the same route `forge/upload-proxy.ts` bridges for the browser), and the
5
+ * two raw PUTs against the signed GCS URLs those calls mint.
6
+ *
7
+ * `publishExitCode` is the one status→exit-code mapping every call above shares, so `publish.ts`
8
+ * never has to duplicate the 401/403/409 distinctions per call site.
9
+ */
10
+ import { CliError } from '../errors.js';
11
+ function isRecord(value) {
12
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
13
+ }
14
+ function stringField(value) {
15
+ return typeof value === 'string' ? value : undefined;
16
+ }
17
+ async function readJsonBody(response) {
18
+ try {
19
+ const parsed = await response.json();
20
+ return isRecord(parsed) ? parsed : {};
21
+ }
22
+ catch {
23
+ return {};
24
+ }
25
+ }
26
+ /**
27
+ * One status→exit-code mapping for both publish calls.
28
+ *
29
+ * The distinction that matters is 4 vs 5: "your engine is too old" is self-serve
30
+ * (`bitmagic upgrade`), while a ban or an ownership mismatch is not. An agent that cannot
31
+ * tell them apart will either loop on upgrade or give up on a fixable problem.
32
+ */
33
+ export function publishExitCode(status, body) {
34
+ if (status === 401)
35
+ return 2;
36
+ if (status === 403)
37
+ return 5;
38
+ if (status === 409 && body.code === 'engine-below-floor')
39
+ return 4;
40
+ return 6;
41
+ }
42
+ /**
43
+ * POST `path` as JSON with a bearer token, and map a non-2xx response through `publishExitCode`.
44
+ * Shared by every authenticated JSON call this module makes — begin, complete, and the thumbnail
45
+ * signed-URL mint — so the three cannot drift on how a failure is reported.
46
+ */
47
+ async function postJson(url, accessToken, body, deps) {
48
+ let response;
49
+ try {
50
+ response = await deps.fetch(url, {
51
+ method: 'POST',
52
+ headers: {
53
+ Authorization: `Bearer ${accessToken}`,
54
+ Accept: 'application/json',
55
+ 'Content-Type': 'application/json',
56
+ },
57
+ body: JSON.stringify(body),
58
+ });
59
+ }
60
+ catch {
61
+ // A transport failure and an error status send people to very different places.
62
+ throw new CliError(`Cannot reach api-server at ${url}.`, 6);
63
+ }
64
+ // A 2xx response MUST parse as a JSON object — this is the one branch that feeds its result
65
+ // straight back to the caller as `T` with no further validation (`beginPublish`'s `signedUrl`,
66
+ // `completePublish`'s `url`, ...). Falling back to `{}` here the way the error branch below
67
+ // does would silently turn a malformed 200 into every field of `T` being `undefined` — e.g. a
68
+ // PUT sent to the literal string `"undefined"` — instead of a clear failure at the point the
69
+ // bad response actually arrived.
70
+ if (response.ok) {
71
+ let parsed;
72
+ try {
73
+ parsed = await response.json();
74
+ }
75
+ catch {
76
+ throw new CliError(`api-server returned a ${response.status} from ${url} with a body that is not valid JSON.`, 6);
77
+ }
78
+ if (!isRecord(parsed)) {
79
+ throw new CliError(`api-server returned an unexpected response shape from ${url}.`, 6);
80
+ }
81
+ return parsed;
82
+ }
83
+ // The error path can tolerate an unparsable/absent body — the fallback is just a generic
84
+ // message, not a value silently flowing on to become someone else's `undefined`.
85
+ const parsedError = await readJsonBody(response);
86
+ const code = stringField(parsedError.code);
87
+ const message = stringField(parsedError.error) ?? `api-server returned HTTP ${response.status} for ${url}.`;
88
+ throw new CliError(message, publishExitCode(response.status, { code }));
89
+ }
90
+ /**
91
+ * Raw PUT of already-prepared bytes to a GCS V4 signed URL.
92
+ *
93
+ * `headers` must match exactly what the URL's signature bound (Cache-Control, and — for the
94
+ * thumbnail lane — Content-Encoding when the server chose to gzip). A mismatch is a 403 from GCS,
95
+ * not a stale-cache symptom; see cli-publish.ts's and cli-uploads.ts's own doc comments for why.
96
+ */
97
+ async function putSignedUrl(url, body, headers, deps) {
98
+ let response;
99
+ try {
100
+ // `@types/node`'s ambient `Uint8Array` (generic over `ArrayBufferLike` since TS 5.7) does not
101
+ // structurally match this project's `BodyInit` (this tsconfig's `"DOM"` lib pulls in lib.dom's
102
+ // own copy, which `@types/node`'s conditional global-augmentation defers to whenever DOM
103
+ // globals are present — see node_modules/@types/node/web-globals/fetch.d.ts). Deriving the
104
+ // body type from `Parameters<typeof fetch>[1]` instead of naming `BodyInit` was tried and hits
105
+ // the identical mismatch, because it resolves to the same lib.dom type in THIS project. Both a
106
+ // `string` and a `Uint8Array` are valid `fetch` bodies at runtime regardless.
107
+ response = await deps.fetch(url, { method: 'PUT', headers, body: body });
108
+ }
109
+ catch (error) {
110
+ const message = error instanceof Error ? error.message : String(error);
111
+ throw new CliError(`Could not upload to storage: ${message}`, 6);
112
+ }
113
+ if (!response.ok) {
114
+ const text = await response.text().catch(() => '');
115
+ throw new CliError(`Upload to storage failed: HTTP ${response.status}${text ? ` — ${text.slice(0, 200)}` : ''}`, 6);
116
+ }
117
+ }
118
+ export async function beginPublish(environment, accessToken, body, deps) {
119
+ return postJson(`${environment.apiUrl}/api/cli/v1/publish/begin`, accessToken, body, deps);
120
+ }
121
+ export async function completePublish(environment, accessToken, body, deps) {
122
+ return postJson(`${environment.apiUrl}/api/cli/v1/publish/complete`, accessToken, body, deps);
123
+ }
124
+ /**
125
+ * `begin` always signs `text/html` for the entry HTML (cli-publish.ts's `getSignedUrl` call) but
126
+ * does not echo a `contentType` field back in its response — there is nothing to echo, so this
127
+ * fixed value is the one Content-Type that can ever be correct for the PUT.
128
+ */
129
+ export const PUBLISH_BUNDLE_CONTENT_TYPE = 'text/html';
130
+ /** PUT the built (and meta-block-injected) bundle to the signed URL `begin` minted. */
131
+ export async function putBundle(signedUrl, html, cacheControl, deps) {
132
+ await putSignedUrl(signedUrl, html, { 'Content-Type': PUBLISH_BUNDLE_CONTENT_TYPE, 'Cache-Control': cacheControl }, deps);
133
+ }
134
+ export async function requestThumbnailUploadUrl(environment, accessToken, body, deps) {
135
+ return postJson(`${environment.apiUrl}/api/cli/v1/uploads/signed-url`, accessToken, body, deps);
136
+ }
137
+ /**
138
+ * PUT the thumbnail bytes to the signed URL `requestThumbnailUploadUrl` minted. `contentType` is
139
+ * the SAME value this lane requested (the server never echoes it back — see
140
+ * `PUBLISH_BUNDLE_CONTENT_TYPE`'s doc comment for why that is fine: the caller already knows it),
141
+ * while `cacheControl` and `contentEncoding` are read back from the signed-URL response, because
142
+ * the server — not this lane — decides both.
143
+ */
144
+ export async function putThumbnail(signed, bytes, contentType, deps) {
145
+ const headers = { 'Content-Type': contentType, 'Cache-Control': signed.cacheControl };
146
+ if (signed.contentEncoding)
147
+ headers['Content-Encoding'] = signed.contentEncoding;
148
+ await putSignedUrl(signed.signedUrl, bytes, headers, deps);
149
+ }
150
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/publish/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A relative path as the fingerprint hashes it: always forward-slashed.
3
+ *
4
+ * `sep` is a parameter rather than a direct `path.sep` read so this is testable on any
5
+ * platform — the Windows behaviour that makes normalization necessary cannot otherwise be
6
+ * exercised on the POSIX runners CI uses, which is exactly how a vacuous test gets written.
7
+ */
8
+ export declare function toPosixPath(relativePath: string, sep?: string): string;
9
+ /**
10
+ * A hash of everything that determines the published bundle.
11
+ *
12
+ * This is what lets `publish` decide whether a `verify` is too stale to trust, by CONTENT
13
+ * rather than by clock: a timestamp policy would accept an edit made one second after
14
+ * verifying and reject an untouched project that merely sat overnight.
15
+ *
16
+ * The path is hashed alongside the content so that moving a file — which changes what the
17
+ * bundle imports — changes the fingerprint even though no byte of any file did.
18
+ *
19
+ * Kept deliberately coarse (every tracked file, not a module graph): a false "stale" costs one
20
+ * `bitmagic verify`, while a false "fresh" ships an unverified bundle.
21
+ *
22
+ * Any filesystem error (unreadable directory, permission denied, etc.) propagates immediately
23
+ * rather than being silently omitted: the project source is the creator's own files, so an
24
+ * unreadable subtree is a real problem worth surfacing, not something to hide. An omitted
25
+ * directory would produce a fingerprint that matches a stale project — the costly failure mode.
26
+ */
27
+ export declare function computeFingerprint(root: string): string;
@@ -0,0 +1,84 @@
1
+ import { createHash } from 'crypto';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ /**
5
+ * Directories whose contents never change what the bundle contains: our own artifacts
6
+ * (`.bitmagic/`, `dist/`), the dependency tree (`node_modules/` — pinned by the lockfile the
7
+ * scaffold writes), vite's cache, and git's object store. Including any of them would make the
8
+ * fingerprint change on something other than an edit, which would make the staleness gate reject
9
+ * a publish of an unchanged project.
10
+ *
11
+ * `.git` is the one that bit in practice. Hashing it means an EMPTY `git commit` — no source
12
+ * change at all — flips the fingerprint, because the commit writes new objects, a new ref value
13
+ * and a new reflog entry. That turns the ordinary `bitmagic verify` → `git commit` →
14
+ * `bitmagic publish` sequence into exit 3 ("The project has changed since it was last verified"),
15
+ * and `bitmagic upgrade` actively pushes creators into git by refusing a dirty tree. The
16
+ * scaffolded AGENTS.md's "do not edit any tracked file between verifying and publishing" cannot
17
+ * prevent it either: committing is not editing. It also made every publish rebuild, since the
18
+ * build manifest's own recorded fingerprint could never still match by the time publish ran.
19
+ *
20
+ * `.DS_Store` is excluded for the same reason at file granularity: on macOS, Finder writes it
21
+ * merely for opening the project folder, and no build tool ever reads it — so it is another way
22
+ * to flip the fingerprint without editing anything. It is the only excluded FILE; the list is
23
+ * deliberately not a general ignore mechanism, because a false "fresh" ships an unverified
24
+ * bundle while a false "stale" only costs one `bitmagic verify`.
25
+ */
26
+ const EXCLUDED_DIRS = new Set(['.bitmagic', 'node_modules', 'dist', '.vite-cache', '.git']);
27
+ /** See EXCLUDED_DIRS: tool-written files that can never change what the bundle contains. */
28
+ const EXCLUDED_FILES = new Set(['.DS_Store']);
29
+ /**
30
+ * A relative path as the fingerprint hashes it: always forward-slashed.
31
+ *
32
+ * `sep` is a parameter rather than a direct `path.sep` read so this is testable on any
33
+ * platform — the Windows behaviour that makes normalization necessary cannot otherwise be
34
+ * exercised on the POSIX runners CI uses, which is exactly how a vacuous test gets written.
35
+ */
36
+ export function toPosixPath(relativePath, sep = path.sep) {
37
+ return relativePath.split(sep).join('/');
38
+ }
39
+ function walk(dir, root, out) {
40
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
41
+ for (const entry of entries) {
42
+ if (entry.isDirectory()) {
43
+ if (EXCLUDED_DIRS.has(entry.name))
44
+ continue;
45
+ walk(path.join(dir, entry.name), root, out);
46
+ }
47
+ else if (entry.isFile()) {
48
+ if (EXCLUDED_FILES.has(entry.name))
49
+ continue;
50
+ const relPath = path.relative(root, path.join(dir, entry.name));
51
+ out.push(toPosixPath(relPath));
52
+ }
53
+ }
54
+ }
55
+ /**
56
+ * A hash of everything that determines the published bundle.
57
+ *
58
+ * This is what lets `publish` decide whether a `verify` is too stale to trust, by CONTENT
59
+ * rather than by clock: a timestamp policy would accept an edit made one second after
60
+ * verifying and reject an untouched project that merely sat overnight.
61
+ *
62
+ * The path is hashed alongside the content so that moving a file — which changes what the
63
+ * bundle imports — changes the fingerprint even though no byte of any file did.
64
+ *
65
+ * Kept deliberately coarse (every tracked file, not a module graph): a false "stale" costs one
66
+ * `bitmagic verify`, while a false "fresh" ships an unverified bundle.
67
+ *
68
+ * Any filesystem error (unreadable directory, permission denied, etc.) propagates immediately
69
+ * rather than being silently omitted: the project source is the creator's own files, so an
70
+ * unreadable subtree is a real problem worth surfacing, not something to hide. An omitted
71
+ * directory would produce a fingerprint that matches a stale project — the costly failure mode.
72
+ */
73
+ export function computeFingerprint(root) {
74
+ const files = [];
75
+ walk(root, root, files);
76
+ files.sort();
77
+ const hash = createHash('sha256');
78
+ for (const rel of files) {
79
+ const content = fs.readFileSync(path.join(root, rel));
80
+ hash.update(`${rel}:${createHash('sha256').update(content).digest('hex')}\n`);
81
+ }
82
+ return hash.digest('hex');
83
+ }
84
+ //# sourceMappingURL=fingerprint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../../src/publish/fingerprint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAE7B;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,cAAc,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC;AAE5F,4FAA4F;AAC5F,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;AAE9C;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,YAAoB,EAAE,MAAc,IAAI,CAAC,GAAG;IACtE,OAAO,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3C,CAAC;AAED,SAAS,IAAI,CAAC,GAAW,EAAE,IAAY,EAAE,GAAa;IACpD,MAAM,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7D,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACxB,IAAI,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;gBAAE,SAAS;YAC5C,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;QAC9C,CAAC;aAAM,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;YAC1B,IAAI,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;gBAAE,SAAS;YAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;YAChE,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;IACxB,KAAK,CAAC,IAAI,EAAE,CAAC;IAEb,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IAClC,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QACxB,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;QACtD,IAAI,CAAC,MAAM,CAAC,GAAG,GAAG,IAAI,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChF,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,26 @@
1
+ import type { VerifyRunRecord } from '../verify/result.js';
2
+ /**
3
+ * Put the server-issued block immediately after `<head>`, matching where the web lane puts its
4
+ * own meta tags — and close enough to the start of the file that the server's 64KB range read
5
+ * always covers it.
6
+ *
7
+ * String insertion, not DOM parsing: this HTML carries the whole inlined engine.
8
+ */
9
+ export declare function injectMetaBlock(html: string, block: string): string;
10
+ export type VerifyGate = {
11
+ ok: true;
12
+ } | {
13
+ ok: false;
14
+ message: string;
15
+ };
16
+ /**
17
+ * May this project be published?
18
+ *
19
+ * Three distinct answers, deliberately: "never verified", "verified then edited", and "verified
20
+ * and broken" have three different fixes, and collapsing them into "run verify" is how an agent
21
+ * ends up looping verify against a game that is genuinely broken.
22
+ *
23
+ * `force` overrides the VERDICT and STALENESS, not the existence of a record: the thumbnail comes
24
+ * from the verify screenshot, so with no verify there is nothing to publish a thumbnail from.
25
+ */
26
+ export declare function checkVerifyGate(record: VerifyRunRecord | null, fingerprint: string, force: boolean): VerifyGate;
@@ -0,0 +1,48 @@
1
+ import { CliError } from '../errors.js';
2
+ const HEAD_OPEN = '<head>';
3
+ /**
4
+ * Put the server-issued block immediately after `<head>`, matching where the web lane puts its
5
+ * own meta tags — and close enough to the start of the file that the server's 64KB range read
6
+ * always covers it.
7
+ *
8
+ * String insertion, not DOM parsing: this HTML carries the whole inlined engine.
9
+ */
10
+ export function injectMetaBlock(html, block) {
11
+ if (html.includes('<!--bitmagic:meta:v1-->')) {
12
+ throw new CliError('This bundle already carries a meta block. Run `bitmagic build` again.', 6);
13
+ }
14
+ const at = html.indexOf(HEAD_OPEN);
15
+ if (at === -1) {
16
+ throw new CliError('The built bundle has no <head> to inject into.', 1);
17
+ }
18
+ const cut = at + HEAD_OPEN.length;
19
+ return html.slice(0, cut) + block + html.slice(cut);
20
+ }
21
+ /**
22
+ * May this project be published?
23
+ *
24
+ * Three distinct answers, deliberately: "never verified", "verified then edited", and "verified
25
+ * and broken" have three different fixes, and collapsing them into "run verify" is how an agent
26
+ * ends up looping verify against a game that is genuinely broken.
27
+ *
28
+ * `force` overrides the VERDICT and STALENESS, not the existence of a record: the thumbnail comes
29
+ * from the verify screenshot, so with no verify there is nothing to publish a thumbnail from.
30
+ */
31
+ export function checkVerifyGate(record, fingerprint, force) {
32
+ if (record === null) {
33
+ return { ok: false, message: 'This game has not been verified. Run `bitmagic verify` first.' };
34
+ }
35
+ if (force)
36
+ return { ok: true };
37
+ if (record.fingerprint !== fingerprint) {
38
+ return { ok: false, message: 'The project has changed since it was last verified. Run `bitmagic verify` again.' };
39
+ }
40
+ if (!record.ok) {
41
+ return {
42
+ ok: false,
43
+ message: `The last verify failed:\n${record.failures.map((f) => ` ✗ ${f}`).join('\n')}\nFix these, or publish with --force.`,
44
+ };
45
+ }
46
+ return { ok: true };
47
+ }
48
+ //# sourceMappingURL=meta-block.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta-block.js","sourceRoot":"","sources":["../../src/publish/meta-block.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAGxC,MAAM,SAAS,GAAG,QAAQ,CAAC;AAE3B;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,KAAa;IACzD,IAAI,IAAI,CAAC,QAAQ,CAAC,yBAAyB,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,QAAQ,CAAC,uEAAuE,EAAE,CAAC,CAAC,CAAC;IACjG,CAAC;IACD,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACnC,IAAI,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;QACd,MAAM,IAAI,QAAQ,CAAC,gDAAgD,EAAE,CAAC,CAAC,CAAC;IAC1E,CAAC;IACD,MAAM,GAAG,GAAG,EAAE,GAAG,SAAS,CAAC,MAAM,CAAC;IAClC,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACtD,CAAC;AAID;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAC7B,MAA8B,EAC9B,WAAmB,EACnB,KAAc;IAEd,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,+DAA+D,EAAE,CAAC;IACjG,CAAC;IACD,IAAI,KAAK;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IAC/B,IAAI,MAAM,CAAC,WAAW,KAAK,WAAW,EAAE,CAAC;QACvC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,kFAAkF,EAAE,CAAC;IACpH,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,4BAA4B,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,uCAAuC;SAC9H,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC"}
@@ -0,0 +1,28 @@
1
+ /** The subset of package.json this module reads — never the whole file. */
2
+ export interface PackageJsonDeps {
3
+ dependencies?: Record<string, string>;
4
+ devDependencies?: Record<string, string>;
5
+ }
6
+ export interface RequiredDependencies {
7
+ dependencies: Record<string, string>;
8
+ devDependencies: Record<string, string>;
9
+ }
10
+ export interface DependencyMismatch {
11
+ name: string;
12
+ kind: 'dependency' | 'devDependency';
13
+ required: string;
14
+ /** undefined when the package is missing from package.json entirely, not just outdated. */
15
+ current?: string;
16
+ }
17
+ /**
18
+ * Compares a project's declared dependencies against what the vendored engine actually needs
19
+ * (`DEPENDENCIES`/`DEV_DEPENDENCIES` in project-files.ts), and reports every mismatch — missing
20
+ * entirely, or present at a different version string.
21
+ *
22
+ * Pure and filesystem-free by design: `upgradeProject` never writes package.json (a creator's
23
+ * added dependencies must survive an upgrade untouched), so this is the other half of that
24
+ * decision — the mismatch it creates has to be *reported*, not silently left for `bitmagic build`
25
+ * to fail on later with an opaque module-resolution error. Kept separate from the command layer
26
+ * that reads package.json off disk so the comparison itself is testable without a fixture file.
27
+ */
28
+ export declare function diffDependencies(pkg: PackageJsonDeps, required: RequiredDependencies): DependencyMismatch[];
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Compares a project's declared dependencies against what the vendored engine actually needs
3
+ * (`DEPENDENCIES`/`DEV_DEPENDENCIES` in project-files.ts), and reports every mismatch — missing
4
+ * entirely, or present at a different version string.
5
+ *
6
+ * Pure and filesystem-free by design: `upgradeProject` never writes package.json (a creator's
7
+ * added dependencies must survive an upgrade untouched), so this is the other half of that
8
+ * decision — the mismatch it creates has to be *reported*, not silently left for `bitmagic build`
9
+ * to fail on later with an opaque module-resolution error. Kept separate from the command layer
10
+ * that reads package.json off disk so the comparison itself is testable without a fixture file.
11
+ */
12
+ export function diffDependencies(pkg, required) {
13
+ const mismatches = [];
14
+ for (const [name, version] of Object.entries(required.dependencies)) {
15
+ const current = pkg.dependencies?.[name];
16
+ if (current !== version) {
17
+ mismatches.push({ name, kind: 'dependency', required: version, current });
18
+ }
19
+ }
20
+ for (const [name, version] of Object.entries(required.devDependencies)) {
21
+ const current = pkg.devDependencies?.[name];
22
+ if (current !== version) {
23
+ mismatches.push({ name, kind: 'devDependency', required: version, current });
24
+ }
25
+ }
26
+ return mismatches;
27
+ }
28
+ //# sourceMappingURL=dependency-diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dependency-diff.js","sourceRoot":"","sources":["../../src/scaffold/dependency-diff.ts"],"names":[],"mappings":"AAmBA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAoB,EAAE,QAA8B;IACnF,MAAM,UAAU,GAAyB,EAAE,CAAC;IAC5C,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;QACpE,MAAM,OAAO,GAAG,GAAG,CAAC,YAAY,EAAE,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;YACxB,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;QAC5E,CAAC;IACH,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC;QACvE,MAAM,OAAO,GAAG,GAAG,CAAC,eAAe,EAAE,CAAC,IAAI,CAAC,CAAC;QAC5C,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;YACxB,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,eAAe,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;QAC/E,CAAC;IACH,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC"}
@@ -1,7 +1,29 @@
1
+ /**
2
+ * Mirrors game/package.json so the pro lane resolves exactly what CI builds against.
3
+ *
4
+ * Exported so `bitmagic upgrade` can diff a project's actual package.json against what the
5
+ * vendored engine currently needs — package.json itself is never rewritten (see
6
+ * PLATFORM_GENERATED_FILES in project.ts), so this is the only source of truth upgrade has for
7
+ * telling a creator what to add by hand.
8
+ */
9
+ export declare const DEPENDENCIES: Record<string, string>;
10
+ export declare const DEV_DEPENDENCIES: Record<string, string>;
1
11
  export declare function renderPackageJson(name: string): string;
2
12
  export declare function renderTsconfig(): string;
3
13
  export declare function renderIndexHtml(): string;
4
14
  export declare function renderViteConfig(): string;
15
+ /**
16
+ * The production bundle config, vendored into the project next to `engine/`.
17
+ *
18
+ * One self-contained HTML: `viteSingleFile` with an effectively unlimited inline limit, so the
19
+ * engine and all JS/CSS land inside index.html. Game ASSETS are deliberately NOT inlined — they
20
+ * already live on the CDN with absolute URLs, and inlining them is what produced multi-hundred-MB
21
+ * bundles and OOM-killed the web publish pod.
22
+ *
23
+ * Vendored rather than generated per-build so a creator can read exactly what ships, and so it
24
+ * versions with the engine it was written for.
25
+ */
26
+ export declare function renderVitePublishConfig(): string;
5
27
  export declare function renderGitignore(): string;
6
28
  export interface ProjectMetadata {
7
29
  gameId: string;