@bitmagic/cli 0.1.0

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 (79) hide show
  1. package/dist/auth/device-flow.d.ts +18 -0
  2. package/dist/auth/device-flow.js +130 -0
  3. package/dist/auth/device-flow.js.map +1 -0
  4. package/dist/auth/session.d.ts +3 -0
  5. package/dist/auth/session.js +19 -0
  6. package/dist/auth/session.js.map +1 -0
  7. package/dist/bin.d.ts +2 -0
  8. package/dist/bin.js +18 -0
  9. package/dist/bin.js.map +1 -0
  10. package/dist/cli.d.ts +70 -0
  11. package/dist/cli.js +96 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/commands/check.d.ts +1 -0
  14. package/dist/commands/check.js +22 -0
  15. package/dist/commands/check.js.map +1 -0
  16. package/dist/commands/dev.d.ts +6 -0
  17. package/dist/commands/dev.js +89 -0
  18. package/dist/commands/dev.js.map +1 -0
  19. package/dist/commands/generate.d.ts +62 -0
  20. package/dist/commands/generate.js +348 -0
  21. package/dist/commands/generate.js.map +1 -0
  22. package/dist/commands/init.d.ts +23 -0
  23. package/dist/commands/init.js +123 -0
  24. package/dist/commands/init.js.map +1 -0
  25. package/dist/commands/login.d.ts +6 -0
  26. package/dist/commands/login.js +36 -0
  27. package/dist/commands/login.js.map +1 -0
  28. package/dist/commands/logout.d.ts +6 -0
  29. package/dist/commands/logout.js +18 -0
  30. package/dist/commands/logout.js.map +1 -0
  31. package/dist/commands/verify.d.ts +6 -0
  32. package/dist/commands/verify.js +124 -0
  33. package/dist/commands/verify.js.map +1 -0
  34. package/dist/commands/whoami.d.ts +11 -0
  35. package/dist/commands/whoami.js +29 -0
  36. package/dist/commands/whoami.js.map +1 -0
  37. package/dist/config/credentials.d.ts +13 -0
  38. package/dist/config/credentials.js +114 -0
  39. package/dist/config/credentials.js.map +1 -0
  40. package/dist/config/environments.d.ts +21 -0
  41. package/dist/config/environments.js +67 -0
  42. package/dist/config/environments.js.map +1 -0
  43. package/dist/errors.d.ts +8 -0
  44. package/dist/errors.js +13 -0
  45. package/dist/errors.js.map +1 -0
  46. package/dist/generate/apply-patches.d.ts +7 -0
  47. package/dist/generate/apply-patches.js +97 -0
  48. package/dist/generate/apply-patches.js.map +1 -0
  49. package/dist/generate/stream.d.ts +33 -0
  50. package/dist/generate/stream.js +185 -0
  51. package/dist/generate/stream.js.map +1 -0
  52. package/dist/http/client.d.ts +6 -0
  53. package/dist/http/client.js +49 -0
  54. package/dist/http/client.js.map +1 -0
  55. package/dist/project/context.d.ts +18 -0
  56. package/dist/project/context.js +67 -0
  57. package/dist/project/context.js.map +1 -0
  58. package/dist/scaffold/aliases.d.ts +27 -0
  59. package/dist/scaffold/aliases.js +51 -0
  60. package/dist/scaffold/aliases.js.map +1 -0
  61. package/dist/scaffold/engine-download.d.ts +27 -0
  62. package/dist/scaffold/engine-download.js +78 -0
  63. package/dist/scaffold/engine-download.js.map +1 -0
  64. package/dist/scaffold/project-files.d.ts +15 -0
  65. package/dist/scaffold/project-files.js +225 -0
  66. package/dist/scaffold/project-files.js.map +1 -0
  67. package/dist/scaffold/project.d.ts +28 -0
  68. package/dist/scaffold/project.js +118 -0
  69. package/dist/scaffold/project.js.map +1 -0
  70. package/dist/verify/browser.d.ts +13 -0
  71. package/dist/verify/browser.js +75 -0
  72. package/dist/verify/browser.js.map +1 -0
  73. package/dist/verify/classify.d.ts +29 -0
  74. package/dist/verify/classify.js +59 -0
  75. package/dist/verify/classify.js.map +1 -0
  76. package/dist/verify/wait-for-server.d.ts +7 -0
  77. package/dist/verify/wait-for-server.js +25 -0
  78. package/dist/verify/wait-for-server.js.map +1 -0
  79. package/package.json +70 -0
@@ -0,0 +1,124 @@
1
+ import { defineCommand } from 'citty';
2
+ import * as fs from 'fs';
3
+ import * as net from 'net';
4
+ import * as path from 'path';
5
+ import { spawn, spawnSync } from 'child_process';
6
+ import { CliError } from '../errors.js';
7
+ import { loadProjectContext, projectBin } from '../project/context.js';
8
+ import { runHeadlessCheck } from '../verify/browser.js';
9
+ import { classifyVerifyRun } from '../verify/classify.js';
10
+ import { waitForServer } from '../verify/wait-for-server.js';
11
+ const DEFAULT_SETTLE_MS = 15_000;
12
+ /** Bind to port 0 so the OS picks a free one — verify must never collide with a running `dev`. */
13
+ async function freePort() {
14
+ return new Promise((resolve, reject) => {
15
+ const server = net.createServer();
16
+ server.on('error', reject);
17
+ server.listen(0, () => {
18
+ const address = server.address();
19
+ if (address === null || typeof address === 'string') {
20
+ server.close();
21
+ reject(new CliError('Could not reserve a local port for the dev server.'));
22
+ return;
23
+ }
24
+ const { port } = address;
25
+ server.close(() => resolve(port));
26
+ });
27
+ });
28
+ }
29
+ /**
30
+ * Rejects if `child` exits (non-signal, non-zero) or errors before `waitForServer` resolves, so
31
+ * a vite that crashes on a broken config fails immediately with its stderr instead of `verify`
32
+ * waiting out the full startup timeout and reporting a port/timing diagnosis for what is
33
+ * actually a config error.
34
+ */
35
+ function watchForEarlyExit(child, getStderr) {
36
+ return new Promise((_resolve, reject) => {
37
+ child.on('exit', (code, signal) => {
38
+ if (signal !== null)
39
+ return; // killed by us (shutdown) — not a startup failure.
40
+ if (code === 0)
41
+ return;
42
+ const stderr = getStderr().trim();
43
+ reject(new CliError(`vite exited before the dev server came up (code ${code}).` +
44
+ (stderr ? `\n${stderr}` : '')));
45
+ });
46
+ child.on('error', (error) => {
47
+ reject(new CliError(`Could not start vite: ${error.message}`));
48
+ });
49
+ });
50
+ }
51
+ /** `--timeout` is settle time in ms and must be a usable duration, not just "parses as a number":
52
+ * NaN (e.g. `30s`) would settle for ~0ms and fail a healthy game; 0 or negative are meaningless. */
53
+ function parseTimeoutArg(raw) {
54
+ if (raw === undefined)
55
+ return DEFAULT_SETTLE_MS;
56
+ const value = Number(raw);
57
+ if (!Number.isFinite(value) || value <= 0) {
58
+ throw new CliError(`--timeout must be a positive number of milliseconds, got ${JSON.stringify(raw)}.`);
59
+ }
60
+ return value;
61
+ }
62
+ export const verifyCommand = defineCommand({
63
+ meta: { name: 'verify', description: 'Boot the game in a real browser and report what broke.' },
64
+ args: {
65
+ timeout: { type: 'string', description: `Settle time in ms (default ${DEFAULT_SETTLE_MS}).` },
66
+ },
67
+ async run({ args }) {
68
+ const settleMs = parseTimeoutArg(args.timeout);
69
+ const context = loadProjectContext();
70
+ const tsc = projectBin(context.root, 'tsc');
71
+ const vite = projectBin(context.root, 'vite');
72
+ console.log('Building…');
73
+ const build = spawnSync(tsc, [], { cwd: context.root, stdio: 'inherit' });
74
+ if (build.error) {
75
+ throw new CliError(`Could not run the TypeScript compiler: ${build.error.message}`);
76
+ }
77
+ if (build.status !== 0) {
78
+ // Nothing to verify: the emitted output the browser would load does not exist.
79
+ throw new CliError('Build failed, so there is nothing to verify.', build.status ?? 1);
80
+ }
81
+ const artifactDir = path.join(context.root, '.bitmagic', 'verify');
82
+ fs.mkdirSync(artifactDir, { recursive: true });
83
+ const screenshotPath = path.join(artifactDir, 'screenshot.png');
84
+ const consolePath = path.join(artifactDir, 'console.log');
85
+ const port = await freePort();
86
+ let server;
87
+ try {
88
+ server = spawn(vite, ['--port', String(port), '--strictPort'], {
89
+ cwd: context.root,
90
+ stdio: ['ignore', 'ignore', 'pipe'],
91
+ });
92
+ let viteStderr = '';
93
+ server.stderr?.on('data', (chunk) => {
94
+ viteStderr += String(chunk);
95
+ });
96
+ await Promise.race([
97
+ waitForServer(port, 30_000),
98
+ watchForEarlyExit(server, () => viteStderr),
99
+ ]);
100
+ // ?autostart=1 skips the Play menu (engine/CreatorMode.ts:22) so gameplay actually starts —
101
+ // otherwise verify would only ever prove the menu renders.
102
+ const observations = await runHeadlessCheck({
103
+ url: `http://localhost:${port}/?autostart=1`,
104
+ screenshotPath,
105
+ consolePath,
106
+ settleMs,
107
+ });
108
+ const result = classifyVerifyRun(observations);
109
+ for (const warning of result.warnings)
110
+ console.log(`warning: ${warning}`);
111
+ if (!result.ok) {
112
+ for (const failure of result.failures)
113
+ console.error(`✗ ${failure}`);
114
+ console.error(`\nArtifacts: ${consolePath}, ${screenshotPath}`);
115
+ throw new CliError('Verification failed.');
116
+ }
117
+ console.log(`Verification passed. Artifacts in ${artifactDir}`);
118
+ }
119
+ finally {
120
+ server?.kill('SIGTERM');
121
+ }
122
+ },
123
+ });
124
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/commands/verify.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ export interface WhoamiResponse {
2
+ userId: string;
3
+ roles: string[];
4
+ }
5
+ export declare function runWhoami(explicitEnv?: string): Promise<WhoamiResponse>;
6
+ export declare const whoamiCommand: import("citty").CommandDef<{
7
+ readonly env: {
8
+ readonly type: "string";
9
+ readonly description: "Environment to query (dev, prod, local).";
10
+ };
11
+ }>;
@@ -0,0 +1,29 @@
1
+ import { defineCommand } from 'citty';
2
+ import { resolveEnvironment, resolveAuth0ClientId } from '../config/environments.js';
3
+ import { readDefaultEnvironment } from '../config/credentials.js';
4
+ import { getAccessToken } from '../auth/session.js';
5
+ import { apiGet } from '../http/client.js';
6
+ const deviceFlowDeps = { fetch: globalThis.fetch, sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
7
+ export async function runWhoami(explicitEnv) {
8
+ const environment = resolveEnvironment({
9
+ explicit: explicitEnv,
10
+ storedDefault: readDefaultEnvironment(),
11
+ });
12
+ const clientId = resolveAuth0ClientId(environment);
13
+ const token = await getAccessToken(environment, clientId, deviceFlowDeps);
14
+ return apiGet(environment, '/api/cli/v1/whoami', token, {
15
+ fetch: globalThis.fetch,
16
+ });
17
+ }
18
+ export const whoamiCommand = defineCommand({
19
+ meta: { name: 'whoami', description: 'Show the identity the CLI is authenticated as.' },
20
+ args: {
21
+ env: { type: 'string', description: 'Environment to query (dev, prod, local).' },
22
+ },
23
+ async run({ args }) {
24
+ const me = await runWhoami(args.env);
25
+ console.log(`user: ${me.userId}`);
26
+ console.log(`roles: ${me.roles.length > 0 ? me.roles.join(', ') : '(none)'}`);
27
+ },
28
+ });
29
+ //# sourceMappingURL=whoami.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whoami.js","sourceRoot":"","sources":["../../src/commands/whoami.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AACtC,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACrF,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAO3C,MAAM,cAAc,GAAG,EAAE,KAAK,EAAE,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC;AAEvH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,WAAoB;IAClD,MAAM,WAAW,GAAG,kBAAkB,CAAC;QACrC,QAAQ,EAAE,WAAW;QACrB,aAAa,EAAE,sBAAsB,EAAE;KACxC,CAAC,CAAC;IACH,MAAM,QAAQ,GAAG,oBAAoB,CAAC,WAAW,CAAC,CAAC;IACnD,MAAM,KAAK,GAAG,MAAM,cAAc,CAAC,WAAW,EAAE,QAAQ,EAAE,cAAc,CAAC,CAAC;IAC1E,OAAO,MAAM,CAAiB,WAAW,EAAE,oBAAoB,EAAE,KAAK,EAAE;QACtE,KAAK,EAAE,UAAU,CAAC,KAAK;KACxB,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAC;IACzC,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,gDAAgD,EAAE;IACvF,IAAI,EAAE;QACJ,GAAG,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,0CAA0C,EAAE;KACjF;IACD,KAAK,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE;QAChB,MAAM,EAAE,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACrC,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC;QACnC,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;IAChF,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,13 @@
1
+ export interface StoredCredentials {
2
+ refreshToken: string;
3
+ accessToken: string;
4
+ /** Epoch milliseconds at which accessToken stops being valid. */
5
+ expiresAt: number;
6
+ }
7
+ export declare function defaultBaseDir(): string;
8
+ export declare function credentialsPath(baseDir?: string): string;
9
+ export declare function readCredentials(environment: string, baseDir?: string): StoredCredentials | null;
10
+ export declare function writeCredentials(environment: string, credentials: StoredCredentials, baseDir?: string): void;
11
+ export declare function clearCredentials(environment: string, baseDir?: string): void;
12
+ export declare function readDefaultEnvironment(baseDir?: string): string | undefined;
13
+ export declare function writeDefaultEnvironment(environment: string, baseDir?: string): void;
@@ -0,0 +1,114 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import { CliError } from '../errors.js';
5
+ const DIR_MODE = 0o700;
6
+ const FILE_MODE = 0o600;
7
+ /**
8
+ * Runtime guard: is `value` usable as a stored default-environment name? This only checks the
9
+ * type (non-empty string) — credentials.ts must not import the environment table, so it cannot
10
+ * validate the value against the actual set of environment names. An unrecognised-but-well-typed
11
+ * name (e.g. a stale "staging" left by an older build) is passed through here and dropped later,
12
+ * where the environment is resolved: resolveEnvironment falls back to "dev" for a stored default
13
+ * it doesn't recognise instead of throwing, so a bad stored default can never wedge the CLI.
14
+ */
15
+ function isStoredDefaultEnvironment(value) {
16
+ return typeof value === 'string' && value.length > 0;
17
+ }
18
+ /** Runtime guard: is `value` a well-formed StoredCredentials entry? */
19
+ function isStoredCredentials(value) {
20
+ if (typeof value !== 'object' || value === null)
21
+ return false;
22
+ const v = value;
23
+ return (typeof v.refreshToken === 'string' &&
24
+ v.refreshToken.length > 0 &&
25
+ typeof v.accessToken === 'string' &&
26
+ v.accessToken.length > 0 &&
27
+ typeof v.expiresAt === 'number' &&
28
+ Number.isFinite(v.expiresAt));
29
+ }
30
+ export function defaultBaseDir() {
31
+ return path.join(os.homedir(), '.bitmagic');
32
+ }
33
+ export function credentialsPath(baseDir = defaultBaseDir()) {
34
+ return path.join(baseDir, 'credentials.json');
35
+ }
36
+ function readFile(baseDir) {
37
+ const filePath = credentialsPath(baseDir);
38
+ let raw;
39
+ try {
40
+ raw = fs.readFileSync(filePath, 'utf-8');
41
+ }
42
+ catch (err) {
43
+ const code = err.code;
44
+ if (code === 'ENOENT' || code === 'ENOTDIR') {
45
+ // No credentials yet is the normal first-run state, not an error.
46
+ return { credentials: {} };
47
+ }
48
+ // A real filesystem problem (e.g. permissions) must not be reported as "not logged in" —
49
+ // that sends the user down a login flow that can't fix it.
50
+ throw new CliError(`Cannot read credentials file at ${filePath}: ${code ?? String(err)}`);
51
+ }
52
+ try {
53
+ const parsed = JSON.parse(raw);
54
+ if (typeof parsed !== 'object' || parsed === null)
55
+ return { credentials: {} };
56
+ const file = parsed;
57
+ const rawCredentials = file.credentials;
58
+ const defaultEnvironment = isStoredDefaultEnvironment(file.defaultEnvironment)
59
+ ? file.defaultEnvironment
60
+ : undefined;
61
+ if (typeof rawCredentials !== 'object' || rawCredentials === null) {
62
+ return { defaultEnvironment, credentials: {} };
63
+ }
64
+ const credentials = {};
65
+ for (const [env, entry] of Object.entries(rawCredentials)) {
66
+ if (isStoredCredentials(entry)) {
67
+ credentials[env] = entry;
68
+ }
69
+ // Entries that fail the guard are dropped, same as a corrupt file: treated as absent.
70
+ }
71
+ return {
72
+ defaultEnvironment,
73
+ credentials,
74
+ };
75
+ }
76
+ catch {
77
+ // A corrupt file must not wedge the CLI: `bitmagic login` overwrites it.
78
+ return { credentials: {} };
79
+ }
80
+ }
81
+ function writeFile(baseDir, file) {
82
+ fs.mkdirSync(baseDir, { recursive: true, mode: DIR_MODE });
83
+ const filePath = credentialsPath(baseDir);
84
+ const tmpPath = path.join(baseDir, `.credentials.json.${process.pid}.${Date.now()}.tmp`);
85
+ fs.writeFileSync(tmpPath, `${JSON.stringify(file, null, 2)}\n`, { mode: FILE_MODE });
86
+ // writeFileSync's mode applies only when it creates the file, so tighten explicitly before
87
+ // the file is visible under its final name, then again after rename for defense in depth.
88
+ fs.chmodSync(tmpPath, FILE_MODE);
89
+ fs.renameSync(tmpPath, filePath);
90
+ fs.chmodSync(filePath, FILE_MODE);
91
+ fs.chmodSync(baseDir, DIR_MODE);
92
+ }
93
+ export function readCredentials(environment, baseDir = defaultBaseDir()) {
94
+ return readFile(baseDir).credentials[environment] ?? null;
95
+ }
96
+ export function writeCredentials(environment, credentials, baseDir = defaultBaseDir()) {
97
+ const file = readFile(baseDir);
98
+ file.credentials[environment] = credentials;
99
+ writeFile(baseDir, file);
100
+ }
101
+ export function clearCredentials(environment, baseDir = defaultBaseDir()) {
102
+ const file = readFile(baseDir);
103
+ delete file.credentials[environment];
104
+ writeFile(baseDir, file);
105
+ }
106
+ export function readDefaultEnvironment(baseDir = defaultBaseDir()) {
107
+ return readFile(baseDir).defaultEnvironment;
108
+ }
109
+ export function writeDefaultEnvironment(environment, baseDir = defaultBaseDir()) {
110
+ const file = readFile(baseDir);
111
+ file.defaultEnvironment = environment;
112
+ writeFile(baseDir, file);
113
+ }
114
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/config/credentials.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ export type EnvironmentName = 'dev' | 'prod' | 'local';
2
+ export interface Environment {
3
+ name: EnvironmentName;
4
+ apiUrl: string;
5
+ auth0Domain: string;
6
+ auth0Audience: string;
7
+ /** Empty until the Auth0 Native application exists; see resolveAuth0ClientId. */
8
+ auth0ClientId: string;
9
+ }
10
+ export declare const ENVIRONMENTS: Record<EnvironmentName, Environment>;
11
+ export declare const ENVIRONMENT_NAMES: EnvironmentName[];
12
+ export declare function isEnvironmentName(value: string): value is EnvironmentName;
13
+ export interface ResolveEnvironmentOptions {
14
+ /** The --env flag, if given. */
15
+ explicit?: string;
16
+ /** The default recorded in the credentials file, if any. */
17
+ storedDefault?: string;
18
+ env?: Record<string, string | undefined>;
19
+ }
20
+ export declare function resolveEnvironment(options?: ResolveEnvironmentOptions): Environment;
21
+ export declare function resolveAuth0ClientId(environment: Environment, env?: Record<string, string | undefined>): string;
@@ -0,0 +1,67 @@
1
+ import { CliError } from '../errors.js';
2
+ const DEV_AUTH0_DOMAIN = 'bitmagic-v3-staging.us.auth0.com';
3
+ const DEV_AUTH0_AUDIENCE = 'https://api.bitmagic.cloud';
4
+ // Public identifier of the "Bitmagic CLI APP" Auth0 Native application, not a secret — native
5
+ // apps cannot hold one, which is why the device-code flow uses PKCE-free device verification.
6
+ // Verified end to end against the staging tenant: the device-flow access token carries the
7
+ // https://bitmagic.ai/roles claim, so the api-server Admin gate accepts it.
8
+ const DEV_AUTH0_CLIENT_ID = 'wEp5Jsw43MyExDT1DfMCFyQSi8II0mSu';
9
+ export const ENVIRONMENTS = {
10
+ dev: {
11
+ name: 'dev',
12
+ apiUrl: 'https://beta.portal.bitmagic.cloud',
13
+ auth0Domain: DEV_AUTH0_DOMAIN,
14
+ auth0Audience: DEV_AUTH0_AUDIENCE,
15
+ auth0ClientId: DEV_AUTH0_CLIENT_ID,
16
+ },
17
+ prod: {
18
+ name: 'prod',
19
+ apiUrl: 'https://bitmagic.ai',
20
+ auth0Domain: 'bitmagic-v3.us.auth0.com',
21
+ auth0Audience: 'https://api.bitmagic.ai',
22
+ // No prod Auth0 Native application exists yet, so `bitmagic login --env prod` fails
23
+ // before any network call, naming BITMAGIC_AUTH0_CLIENT_ID. Prod delivery of the pro lane
24
+ // is deferred anyway: prod has no engine bucket and no manifest.
25
+ auth0ClientId: '',
26
+ },
27
+ // There is no local Auth0. Pointing local at the dev tenant is deliberate: minting a fake
28
+ // local identity would reintroduce the localhost-bypass hazard the CLI auth design rejected.
29
+ local: {
30
+ name: 'local',
31
+ apiUrl: 'http://localhost:3002',
32
+ auth0Domain: DEV_AUTH0_DOMAIN,
33
+ auth0Audience: DEV_AUTH0_AUDIENCE,
34
+ auth0ClientId: DEV_AUTH0_CLIENT_ID,
35
+ },
36
+ };
37
+ export const ENVIRONMENT_NAMES = Object.keys(ENVIRONMENTS);
38
+ export function isEnvironmentName(value) {
39
+ return Object.prototype.hasOwnProperty.call(ENVIRONMENTS, value);
40
+ }
41
+ export function resolveEnvironment(options = {}) {
42
+ const env = options.env ?? process.env;
43
+ // A stored default is written by an older or corrupted CLI, not typed by the user, so an
44
+ // unrecognised value (e.g. a stale "staging") must not wedge every command with a CliError —
45
+ // treat it as absent and fall through to "dev". An unrecognised --env flag or BITMAGIC_ENV is
46
+ // still a hard error below: those are typed by the user right now and deserve direct feedback.
47
+ const validStoredDefault = options.storedDefault !== undefined && isEnvironmentName(options.storedDefault)
48
+ ? options.storedDefault
49
+ : undefined;
50
+ const candidate = options.explicit ?? env.BITMAGIC_ENV ?? validStoredDefault ?? 'dev';
51
+ if (!isEnvironmentName(candidate)) {
52
+ throw new CliError(`Unknown environment "${candidate}". Valid environments: ${ENVIRONMENT_NAMES.join(', ')}.`);
53
+ }
54
+ const resolved = ENVIRONMENTS[candidate];
55
+ const apiUrlOverride = env.BITMAGIC_API_URL;
56
+ return apiUrlOverride ? { ...resolved, apiUrl: apiUrlOverride } : resolved;
57
+ }
58
+ export function resolveAuth0ClientId(environment, env = process.env) {
59
+ const clientId = env.BITMAGIC_AUTH0_CLIENT_ID ?? environment.auth0ClientId;
60
+ if (!clientId) {
61
+ throw new CliError(`No Auth0 client id is configured for the "${environment.name}" environment. ` +
62
+ 'Set BITMAGIC_AUTH0_CLIENT_ID to the client id of the Auth0 Native application ' +
63
+ 'that has the Device Code grant enabled.');
64
+ }
65
+ return clientId;
66
+ }
67
+ //# sourceMappingURL=environments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environments.js","sourceRoot":"","sources":["../../src/config/environments.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAaxC,MAAM,gBAAgB,GAAG,kCAAkC,CAAC;AAC5D,MAAM,kBAAkB,GAAG,4BAA4B,CAAC;AAExD,8FAA8F;AAC9F,8FAA8F;AAC9F,2FAA2F;AAC3F,4EAA4E;AAC5E,MAAM,mBAAmB,GAAG,kCAAkC,CAAC;AAE/D,MAAM,CAAC,MAAM,YAAY,GAAyC;IAChE,GAAG,EAAE;QACH,IAAI,EAAE,KAAK;QACX,MAAM,EAAE,oCAAoC;QAC5C,WAAW,EAAE,gBAAgB;QAC7B,aAAa,EAAE,kBAAkB;QACjC,aAAa,EAAE,mBAAmB;KACnC;IACD,IAAI,EAAE;QACJ,IAAI,EAAE,MAAM;QACZ,MAAM,EAAE,qBAAqB;QAC7B,WAAW,EAAE,0BAA0B;QACvC,aAAa,EAAE,yBAAyB;QACxC,oFAAoF;QACpF,0FAA0F;QAC1F,iEAAiE;QACjE,aAAa,EAAE,EAAE;KAClB;IACD,0FAA0F;IAC1F,6FAA6F;IAC7F,KAAK,EAAE;QACL,IAAI,EAAE,OAAO;QACb,MAAM,EAAE,uBAAuB;QAC/B,WAAW,EAAE,gBAAgB;QAC7B,aAAa,EAAE,kBAAkB;QACjC,aAAa,EAAE,mBAAmB;KACnC;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,CAAsB,CAAC;AAEhF,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC7C,OAAO,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;AACnE,CAAC;AAUD,MAAM,UAAU,kBAAkB,CAAC,UAAqC,EAAE;IACxE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACvC,yFAAyF;IACzF,6FAA6F;IAC7F,8FAA8F;IAC9F,+FAA+F;IAC/F,MAAM,kBAAkB,GACtB,OAAO,CAAC,aAAa,KAAK,SAAS,IAAI,iBAAiB,CAAC,OAAO,CAAC,aAAa,CAAC;QAC7E,CAAC,CAAC,OAAO,CAAC,aAAa;QACvB,CAAC,CAAC,SAAS,CAAC;IAChB,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,IAAI,GAAG,CAAC,YAAY,IAAI,kBAAkB,IAAI,KAAK,CAAC;IAEtF,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,QAAQ,CAChB,wBAAwB,SAAS,0BAA0B,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC3F,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC;IACzC,MAAM,cAAc,GAAG,GAAG,CAAC,gBAAgB,CAAC;IAC5C,OAAO,cAAc,CAAC,CAAC,CAAC,EAAE,GAAG,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC7E,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,WAAwB,EACxB,MAA0C,OAAO,CAAC,GAAG;IAErD,MAAM,QAAQ,GAAG,GAAG,CAAC,wBAAwB,IAAI,WAAW,CAAC,aAAa,CAAC;IAC3E,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,QAAQ,CAChB,6CAA6C,WAAW,CAAC,IAAI,iBAAiB;YAC5E,gFAAgF;YAChF,yCAAyC,CAC5C,CAAC;IACJ,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * An error whose message is meant for the user, not a stack trace.
3
+ * cli.ts prints the message and exits with `exitCode`.
4
+ */
5
+ export declare class CliError extends Error {
6
+ readonly exitCode: number;
7
+ constructor(message: string, exitCode?: number);
8
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * An error whose message is meant for the user, not a stack trace.
3
+ * cli.ts prints the message and exits with `exitCode`.
4
+ */
5
+ export class CliError extends Error {
6
+ exitCode;
7
+ constructor(message, exitCode = 1) {
8
+ super(message);
9
+ this.name = 'CliError';
10
+ this.exitCode = exitCode;
11
+ }
12
+ }
13
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,OAAO,QAAS,SAAQ,KAAK;IACxB,QAAQ,CAAS;IAE1B,YAAY,OAAe,EAAE,QAAQ,GAAG,CAAC;QACvC,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;QACvB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;CACF"}
@@ -0,0 +1,7 @@
1
+ import { type WorldPatch } from '@bitmagic/asset-core';
2
+ export interface ApplyOutcome {
3
+ applied: number;
4
+ /** One human-readable line per patch, for the command to print. */
5
+ summary: string[];
6
+ }
7
+ export declare function applyPatchesToWorld(worldPath: string, patches: WorldPatch[]): ApplyOutcome;
@@ -0,0 +1,97 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { buildUpsertPredicate, isUpsertRootPatch, validateWorldPatch, } from '@bitmagic/asset-core';
4
+ import { CliError } from '../errors.js';
5
+ function isJsonObject(value) {
6
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
7
+ }
8
+ /** Walks `path`, creating intermediate objects, and returns the container plus the final key. */
9
+ function resolveContainer(root, segments) {
10
+ let current = root;
11
+ for (const segment of segments.slice(0, -1)) {
12
+ const next = current[segment];
13
+ if (!isJsonObject(next)) {
14
+ current[segment] = {};
15
+ }
16
+ current = current[segment];
17
+ }
18
+ const key = segments[segments.length - 1];
19
+ if (key === undefined) {
20
+ throw new CliError('A patch arrived with an empty path.');
21
+ }
22
+ return { container: current, key };
23
+ }
24
+ /**
25
+ * `set` and `push` address paths INSIDE worldProfileData; the Root variants address the document
26
+ * root. This mirrors game-play-agent/src/mastra/utils/world-json-editor.ts:315-331 exactly —
27
+ * writing a `set` at the root puts it where the engine never looks, which reads as success.
28
+ */
29
+ function targetFor(world, patch) {
30
+ if (patch.type === 'set' || patch.type === 'push') {
31
+ if (!isJsonObject(world.worldProfileData)) {
32
+ world.worldProfileData = {};
33
+ }
34
+ return world.worldProfileData;
35
+ }
36
+ return world;
37
+ }
38
+ function applyOne(world, patch) {
39
+ const target = targetFor(world, patch);
40
+ const { container, key } = resolveContainer(target, patch.path);
41
+ if (isUpsertRootPatch(patch)) {
42
+ const existing = Array.isArray(container[key]) ? container[key] : [];
43
+ const matches = buildUpsertPredicate(patch);
44
+ const index = existing.findIndex(matches);
45
+ if (index >= 0) {
46
+ existing[index] = patch.value;
47
+ container[key] = existing;
48
+ return `updated ${patch.path.join('.')}[${index}]`;
49
+ }
50
+ existing.push(patch.value);
51
+ container[key] = existing;
52
+ return `added to ${patch.path.join('.')}`;
53
+ }
54
+ if (patch.type === 'push' || patch.type === 'pushRoot') {
55
+ const existing = Array.isArray(container[key]) ? container[key] : [];
56
+ existing.push(patch.value);
57
+ container[key] = existing;
58
+ return `added to ${patch.path.join('.')}`;
59
+ }
60
+ container[key] = patch.value;
61
+ return `set ${patch.path.join('.')}`;
62
+ }
63
+ export function applyPatchesToWorld(worldPath, patches) {
64
+ // Validate the whole batch BEFORE touching anything: a half-applied world is worse than an
65
+ // unchanged one, and the creator has no way to tell which half landed.
66
+ for (const patch of patches) {
67
+ const problem = validateWorldPatch(patch);
68
+ if (problem !== null) {
69
+ throw new CliError(`The server sent a patch this CLI cannot apply: ${problem}`);
70
+ }
71
+ }
72
+ let raw;
73
+ try {
74
+ raw = fs.readFileSync(worldPath, 'utf-8');
75
+ }
76
+ catch {
77
+ throw new CliError(`Cannot read ${worldPath}. Is this a Bitmagic project?`);
78
+ }
79
+ let world;
80
+ try {
81
+ world = JSON.parse(raw);
82
+ }
83
+ catch {
84
+ throw new CliError(`${worldPath} is not valid JSON. Nothing has been changed.`);
85
+ }
86
+ if (!isJsonObject(world)) {
87
+ throw new CliError(`${worldPath} does not contain a world object. Nothing has been changed.`);
88
+ }
89
+ const summary = patches.map((patch) => applyOne(world, patch));
90
+ // Write once, through a temp file in the same directory, so an interrupted write cannot leave
91
+ // a truncated world.json behind.
92
+ const temp = path.join(path.dirname(worldPath), `.world.json.${process.pid}.tmp`);
93
+ fs.writeFileSync(temp, `${JSON.stringify(world, null, 2)}\n`);
94
+ fs.renameSync(temp, worldPath);
95
+ return { applied: patches.length, summary };
96
+ }
97
+ //# sourceMappingURL=apply-patches.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-patches.js","sourceRoot":"","sources":["../../src/generate/apply-patches.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { WorldPatch } from '@bitmagic/asset-core';
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[];
13
+ export interface GenerationOutcome {
14
+ success: boolean;
15
+ message: string;
16
+ patches: WorldPatch[];
17
+ }
18
+ export interface StreamDeps {
19
+ fetch: typeof globalThis.fetch;
20
+ onProgress: (message: string) => void;
21
+ }
22
+ /**
23
+ * POSTs to `/api/cli/v1/assets/<type>/stream` and consumes the SSE response.
24
+ *
25
+ * - A non-2xx response fails before any stream opens; see `toCliError` for the status mapping.
26
+ * - On 200, each `progress` event invokes `deps.onProgress`; the terminal `result` or `error`
27
+ * event resolves this call with the outcome it carried (an `error` event's `success: false`
28
+ * is a legitimate generation failure, not a thrown CliError — the caller decides how to
29
+ * present it).
30
+ * - A stream that closes with **no terminal event** is a truncated connection, not a success
31
+ * with zero patches, and fails with a CliError.
32
+ */
33
+ export declare function requestGeneration(environment: Environment, accessToken: string, type: string, body: Record<string, unknown>, deps: StreamDeps): Promise<GenerationOutcome>;