@auraindustry/aurajs 0.1.3 → 0.1.5

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 (108) hide show
  1. package/README.md +7 -0
  2. package/benchmarks/perf-thresholds.json +27 -0
  3. package/package.json +6 -1
  4. package/src/ai-guidance.mjs +302 -0
  5. package/src/authored-project.mjs +498 -2
  6. package/src/build-contract/capabilities.mjs +87 -1
  7. package/src/build-contract/constants.mjs +1 -0
  8. package/src/build-contract.mjs +2 -0
  9. package/src/bundler.mjs +143 -13
  10. package/src/cli.mjs +681 -13
  11. package/src/commands/packs.mjs +741 -0
  12. package/src/commands/project-authoring.mjs +128 -1
  13. package/src/conformance/cases/app-and-ui-runtime-cases.mjs +1 -2
  14. package/src/conformance/cases/core-runtime-cases.mjs +6 -2
  15. package/src/conformance/cases/scene3d-and-media-cases.mjs +238 -0
  16. package/src/conformance/cases/systems-and-gameplay-cases.mjs +265 -4
  17. package/src/conformance-mobile.mjs +166 -0
  18. package/src/conformance.mjs +89 -30
  19. package/src/evidence-bundle.mjs +242 -0
  20. package/src/headless-test/runtime-coordinator.mjs +186 -33
  21. package/src/headless-test.mjs +2 -0
  22. package/src/helpers/2d/index.mjs +183 -0
  23. package/src/helpers/index.mjs +26 -0
  24. package/src/helpers/starter-utils/adventure-objectives.js +102 -0
  25. package/src/helpers/starter-utils/adventure-world-2d.js +221 -0
  26. package/src/helpers/starter-utils/animation-2d.js +337 -0
  27. package/src/helpers/starter-utils/animation-packaging-2d.js +203 -0
  28. package/src/helpers/starter-utils/atlas-assets-2d.js +111 -0
  29. package/src/helpers/starter-utils/autoplay-debug-2d.js +215 -0
  30. package/src/helpers/starter-utils/avatar-3d.js +404 -0
  31. package/src/helpers/starter-utils/combat-feedback-2d.js +320 -0
  32. package/src/helpers/starter-utils/combat-runtime-2d.js +290 -0
  33. package/src/helpers/starter-utils/core.js +150 -0
  34. package/src/helpers/starter-utils/dialogue-2d.js +351 -0
  35. package/src/helpers/starter-utils/enemy-archetypes-2d.js +68 -0
  36. package/src/helpers/starter-utils/index.js +26 -0
  37. package/src/helpers/starter-utils/inventory-2d.js +268 -0
  38. package/src/helpers/starter-utils/journal-2d.js +267 -0
  39. package/src/helpers/starter-utils/platformer-3d.js +132 -0
  40. package/src/helpers/starter-utils/scene-audio-2d.js +236 -0
  41. package/src/helpers/starter-utils/streamed-world-2d.js +378 -0
  42. package/src/helpers/starter-utils/tilemap-nav-2d.js +499 -0
  43. package/src/helpers/starter-utils/tilemap-world-2d.js +205 -0
  44. package/src/helpers/starter-utils/triggers.js +662 -0
  45. package/src/helpers/starter-utils/tween-2d.js +615 -0
  46. package/src/helpers/starter-utils/wave-director.js +101 -0
  47. package/src/helpers/starter-utils/world-compositor-2d.js +253 -0
  48. package/src/helpers/starter-utils/world-persistence-2d.js +180 -0
  49. package/src/mobile/android/build.mjs +606 -0
  50. package/src/mobile/android/host-artifact.mjs +280 -0
  51. package/src/mobile/ios/build.mjs +1323 -0
  52. package/src/mobile/ios/host-artifact.mjs +819 -0
  53. package/src/mobile/shared/capabilities.mjs +174 -0
  54. package/src/packs/catalog.mjs +259 -0
  55. package/src/perf-benchmark-runner.mjs +17 -12
  56. package/src/perf-benchmark.mjs +408 -4
  57. package/src/publish-command.mjs +303 -6
  58. package/src/replay-runtime.mjs +257 -0
  59. package/src/scaffold/config.mjs +2 -0
  60. package/src/scaffold/fs.mjs +8 -1
  61. package/src/scaffold/project-docs.mjs +43 -1
  62. package/src/scaffold.mjs +4 -0
  63. package/src/session-runtime.mjs +4 -3
  64. package/src/web-conformance.mjs +0 -36
  65. package/templates/create/2d-adventure/config/gameplay/adventure.config.js +9 -6
  66. package/templates/create/2d-adventure/content/gameplay/dialogue.js +85 -0
  67. package/templates/create/2d-adventure/content/gameplay/world.js +32 -36
  68. package/templates/create/2d-adventure/content/gameplay/world.tilemap.json +273 -0
  69. package/templates/create/2d-adventure/docs/design/loop.md +4 -3
  70. package/templates/create/2d-adventure/prefabs/relic.prefab.js +10 -10
  71. package/templates/create/2d-adventure/prefabs/world.prefab.js +127 -74
  72. package/templates/create/2d-adventure/scenes/gameplay.scene.js +603 -112
  73. package/templates/create/2d-adventure/src/runtime/capabilities.js +16 -0
  74. package/templates/create/2d-adventure/ui/hud.screen.js +187 -4
  75. package/templates/create/2d-adventure/ui/journal.screen.js +183 -0
  76. package/templates/create/3d/scenes/gameplay.scene.js +30 -3
  77. package/templates/create/3d/src/runtime/capabilities.js +5 -0
  78. package/templates/create/3d/src/runtime/materials.js +10 -0
  79. package/templates/create/3d-adventure/scenes/gameplay.scene.js +30 -3
  80. package/templates/create/3d-adventure/src/runtime/capabilities.js +5 -0
  81. package/templates/create/3d-adventure/src/runtime/materials.js +11 -0
  82. package/templates/create/3d-collectathon/scenes/gameplay.scene.js +30 -3
  83. package/templates/create/3d-collectathon/src/runtime/capabilities.js +5 -0
  84. package/templates/create/3d-collectathon/src/runtime/materials.js +10 -0
  85. package/templates/create/shared/src/runtime/ui-forms.js +552 -0
  86. package/templates/create/shared/src/starter-utils/adventure-world-2d.js +221 -0
  87. package/templates/create/shared/src/starter-utils/animation-packaging-2d.js +203 -0
  88. package/templates/create/shared/src/starter-utils/atlas-assets-2d.js +111 -0
  89. package/templates/create/shared/src/starter-utils/autoplay-debug-2d.js +215 -0
  90. package/templates/create/shared/src/starter-utils/combat-runtime-2d.js +290 -0
  91. package/templates/create/shared/src/starter-utils/dialogue-2d.js +351 -0
  92. package/templates/create/shared/src/starter-utils/index.js +15 -1
  93. package/templates/create/shared/src/starter-utils/inventory-2d.js +268 -0
  94. package/templates/create/shared/src/starter-utils/journal-2d.js +267 -0
  95. package/templates/create/shared/src/starter-utils/scene-audio-2d.js +236 -0
  96. package/templates/create/shared/src/starter-utils/streamed-world-2d.js +378 -0
  97. package/templates/create/shared/src/starter-utils/tilemap-nav-2d.js +499 -0
  98. package/templates/create/shared/src/starter-utils/tilemap-world-2d.js +205 -0
  99. package/templates/create/shared/src/starter-utils/world-compositor-2d.js +253 -0
  100. package/templates/create/shared/src/starter-utils/world-persistence-2d.js +180 -0
  101. package/templates/create-bin/play.js +36 -7
  102. package/templates/skills/auramaxx/SKILL.md +46 -0
  103. package/templates/skills/auramaxx/project-requirements.md +68 -0
  104. package/templates/skills/auramaxx/starter-recipes.md +104 -0
  105. package/templates/skills/auramaxx/validation-checklist.md +49 -0
  106. package/templates/skills/aurajs/SKILL.md +0 -96
  107. package/templates/skills/aurajs/api-contract-3d.md +0 -7
  108. package/templates/skills/aurajs/api-contract.md +0 -7
@@ -1,17 +1,22 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
- import { join, resolve } from 'node:path';
4
+ import { basename, join, resolve } from 'node:path';
5
5
 
6
6
  import { preparePublishPackageSurface } from './external-package-surface.mjs';
7
7
  import {
8
8
  PACKAGE_INTEGRITY_PUBLISH_FILES,
9
9
  writeSignedPackageIntegrityArtifacts,
10
10
  } from './package-integrity.mjs';
11
+ import { canPromptInteractively, promptInput, promptSelect } from './terminal-ui.mjs';
11
12
  import { resolvePublishEnvExampleSurface } from './publish-env-example.mjs';
12
13
  import { validatePublishProject } from './publish-validation.mjs';
13
14
  import { formatBytes } from './external-asset-policy.mjs';
14
15
 
16
+ const SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z-.]+)?$/;
17
+ const SCOPED_PACKAGE_RE = /^@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/;
18
+ const UNSCOPED_PACKAGE_RE = /^[a-z0-9][a-z0-9._-]*$/;
19
+
15
20
  export function isNpmPublishLifecycleInvocation(env = process.env) {
16
21
  return env.npm_lifecycle_event === 'publish' && env.npm_command === 'publish';
17
22
  }
@@ -36,6 +41,126 @@ function stripOption(commandArgs, optionName) {
36
41
  return nextArgs;
37
42
  }
38
43
 
44
+ function sanitizePackageSlug(value) {
45
+ const compact = String(value || '')
46
+ .trim()
47
+ .toLowerCase()
48
+ .replace(/^@[^/]+\//, '')
49
+ .replace(/\s+/g, '-')
50
+ .replace(/[^a-z0-9._-]/g, '-')
51
+ .replace(/-+/g, '-')
52
+ .replace(/^[-._]+|[-._]+$/g, '');
53
+ if (!compact) {
54
+ throw new Error(`Cannot derive package slug from "${value}".`);
55
+ }
56
+ return compact;
57
+ }
58
+
59
+ function buildDefaultPackageName(projectRoot) {
60
+ return `@aurajs/${sanitizePackageSlug(basename(projectRoot))}`;
61
+ }
62
+
63
+ function ensureScopedPackageName(value) {
64
+ const trimmed = String(value || '').trim();
65
+ if (!trimmed) {
66
+ throw new Error('Package name cannot be empty.');
67
+ }
68
+ if (/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/.test(trimmed)) {
69
+ return `@${trimmed}`;
70
+ }
71
+ return trimmed;
72
+ }
73
+
74
+ function isValidPackageName(value) {
75
+ const normalized = String(value || '').trim();
76
+ if (!normalized) {
77
+ return false;
78
+ }
79
+ try {
80
+ const candidate = ensureScopedPackageName(normalized);
81
+ return SCOPED_PACKAGE_RE.test(candidate) || UNSCOPED_PACKAGE_RE.test(candidate);
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+
87
+ function validatePackageName(value) {
88
+ const normalized = ensureScopedPackageName(value);
89
+ if (!SCOPED_PACKAGE_RE.test(normalized) && !UNSCOPED_PACKAGE_RE.test(normalized)) {
90
+ throw new Error(`Invalid npm package name "${value}".`);
91
+ }
92
+ }
93
+
94
+ function normalizeVersion(value) {
95
+ return String(value || '').trim().replace(/^v/i, '');
96
+ }
97
+
98
+ function isValidVersion(value) {
99
+ return SEMVER_RE.test(String(value || '').trim());
100
+ }
101
+
102
+ function validateVersion(value) {
103
+ if (!isValidVersion(value)) {
104
+ throw new Error(`Invalid version "${value}". Expected semver like 1.2.3 or 1.2.3-beta.1.`);
105
+ }
106
+ }
107
+
108
+ function bumpVersion(currentVersion, bumpKind) {
109
+ const match = normalizeVersion(currentVersion).match(SEMVER_RE);
110
+ if (!match) {
111
+ throw new Error(`Current version "${currentVersion}" is not valid semver.`);
112
+ }
113
+ const major = Number.parseInt(match[1], 10);
114
+ const minor = Number.parseInt(match[2], 10);
115
+ const patch = Number.parseInt(match[3], 10);
116
+ const prerelease = match[4] || '';
117
+
118
+ if (bumpKind === 'major') return `${major + 1}.0.0${prerelease}`;
119
+ if (bumpKind === 'minor') return `${major}.${minor + 1}.0${prerelease}`;
120
+ return `${major}.${minor}.${patch + 1}${prerelease}`;
121
+ }
122
+
123
+ function extractInternalPublishOptions(commandArgs) {
124
+ const sanitizedArgs = [];
125
+ let explicitName = null;
126
+ let explicitVersion = null;
127
+ let yes = false;
128
+
129
+ for (let index = 0; index < commandArgs.length; index += 1) {
130
+ const current = commandArgs[index];
131
+ if (current === '--yes' || current === '-y') {
132
+ yes = true;
133
+ continue;
134
+ }
135
+ if (current === '--name') {
136
+ explicitName = commandArgs[index + 1] || '';
137
+ index += 1;
138
+ continue;
139
+ }
140
+ if (current.startsWith('--name=')) {
141
+ explicitName = current.slice('--name='.length);
142
+ continue;
143
+ }
144
+ if (current === '--version') {
145
+ explicitVersion = commandArgs[index + 1] || '';
146
+ index += 1;
147
+ continue;
148
+ }
149
+ if (current.startsWith('--version=')) {
150
+ explicitVersion = current.slice('--version='.length);
151
+ continue;
152
+ }
153
+ sanitizedArgs.push(current);
154
+ }
155
+
156
+ return {
157
+ sanitizedArgs,
158
+ explicitName,
159
+ explicitVersion,
160
+ yes,
161
+ };
162
+ }
163
+
39
164
  export function extractPublishToken(commandArgs, env = process.env) {
40
165
  let explicitToken = null;
41
166
 
@@ -130,7 +255,7 @@ function createPublishTokenEnv(token, env = process.env) {
130
255
  };
131
256
  }
132
257
 
133
- export function readProjectPackage(projectRoot = process.cwd()) {
258
+ function readProjectPackageManifest(projectRoot = process.cwd()) {
134
259
  const packagePath = resolve(projectRoot, 'package.json');
135
260
  if (!existsSync(packagePath)) {
136
261
  throw new Error('aura publish requires a package.json in the current project root.');
@@ -146,9 +271,6 @@ export function readProjectPackage(projectRoot = process.cwd()) {
146
271
  const packageName = typeof projectPackage?.name === 'string' && projectPackage.name.trim().length > 0
147
272
  ? projectPackage.name.trim()
148
273
  : null;
149
- if (!packageName) {
150
- throw new Error('aura publish requires package.json -> name to be set.');
151
- }
152
274
 
153
275
  return {
154
276
  packagePath,
@@ -157,6 +279,158 @@ export function readProjectPackage(projectRoot = process.cwd()) {
157
279
  };
158
280
  }
159
281
 
282
+ export function readProjectPackage(projectRoot = process.cwd()) {
283
+ const projectPackage = readProjectPackageManifest(projectRoot);
284
+ if (!projectPackage.packageName) {
285
+ throw new Error('aura publish requires package.json -> name to be set.');
286
+ }
287
+ return projectPackage;
288
+ }
289
+
290
+ async function resolvePublishPackageName(
291
+ commandArgs,
292
+ {
293
+ projectRoot = process.cwd(),
294
+ canPromptInteractivelyImpl = canPromptInteractively,
295
+ promptInputImpl = promptInput,
296
+ } = {},
297
+ ) {
298
+ const { packagePath, projectPackage, packageName: currentPackageName } = readProjectPackageManifest(projectRoot);
299
+ const { sanitizedArgs, explicitName, yes } = extractInternalPublishOptions(commandArgs);
300
+ const defaultPackageName = buildDefaultPackageName(projectRoot);
301
+ const currentValidPackageName = currentPackageName && isValidPackageName(currentPackageName)
302
+ ? ensureScopedPackageName(currentPackageName)
303
+ : null;
304
+
305
+ let targetPackageName = currentPackageName;
306
+
307
+ if (explicitName !== null) {
308
+ targetPackageName = ensureScopedPackageName(explicitName);
309
+ validatePackageName(targetPackageName);
310
+ } else if (yes) {
311
+ targetPackageName = currentPackageName || defaultPackageName;
312
+ validatePackageName(targetPackageName);
313
+ } else if (canPromptInteractivelyImpl()) {
314
+ const fallbackPackageName = currentValidPackageName || defaultPackageName;
315
+ const fallbackDisplay = fallbackPackageName.startsWith('@')
316
+ ? fallbackPackageName.slice(1)
317
+ : fallbackPackageName;
318
+ const input = await promptInputImpl(` Package name (default: ${fallbackDisplay})`);
319
+ targetPackageName = ensureScopedPackageName(input || fallbackPackageName);
320
+ validatePackageName(targetPackageName);
321
+ }
322
+
323
+ if (targetPackageName && projectPackage.name !== targetPackageName) {
324
+ projectPackage.name = targetPackageName;
325
+ writeFileSync(packagePath, `${JSON.stringify(projectPackage, null, 2)}\n`, 'utf8');
326
+ }
327
+
328
+ return {
329
+ publishArgs: sanitizedArgs,
330
+ packageName: targetPackageName,
331
+ };
332
+ }
333
+
334
+ async function resolvePublishVersion(
335
+ commandArgs,
336
+ {
337
+ projectRoot = process.cwd(),
338
+ canPromptInteractivelyImpl = canPromptInteractively,
339
+ promptSelectImpl = promptSelect,
340
+ promptInputImpl = promptInput,
341
+ } = {},
342
+ ) {
343
+ const { packagePath, projectPackage } = readProjectPackage(projectRoot);
344
+ const { sanitizedArgs, explicitVersion, yes } = extractInternalPublishOptions(commandArgs);
345
+ const packageVersion = typeof projectPackage?.version === 'string' && projectPackage.version.trim().length > 0
346
+ ? normalizeVersion(projectPackage.version)
347
+ : null;
348
+ const currentVersion = isValidVersion(packageVersion || '') ? packageVersion : null;
349
+
350
+ let targetVersion = packageVersion;
351
+
352
+ if (explicitVersion !== null) {
353
+ const normalized = normalizeVersion(explicitVersion);
354
+ validateVersion(normalized);
355
+ targetVersion = normalized;
356
+ } else if (yes) {
357
+ if (packageVersion) {
358
+ validateVersion(packageVersion);
359
+ targetVersion = packageVersion;
360
+ } else {
361
+ targetVersion = '0.1.0';
362
+ }
363
+ } else if (canPromptInteractivelyImpl()) {
364
+ if (currentVersion) {
365
+ const choice = await promptSelectImpl(
366
+ ' Version selection',
367
+ [
368
+ { value: 'current', label: `Current (${currentVersion})`, aliases: ['c', 'keep'] },
369
+ { value: 'bump', label: 'Bump version', aliases: ['b'] },
370
+ { value: 'custom', label: 'Custom version', aliases: ['x'] },
371
+ ],
372
+ 'current',
373
+ );
374
+
375
+ if (choice === 'bump') {
376
+ const bumpKind = await promptSelectImpl(
377
+ ' Bump type',
378
+ [
379
+ { value: 'patch', label: `Patch (${bumpVersion(currentVersion, 'patch')})`, aliases: ['p'] },
380
+ { value: 'minor', label: `Minor (${bumpVersion(currentVersion, 'minor')})`, aliases: ['m'] },
381
+ { value: 'major', label: `Major (${bumpVersion(currentVersion, 'major')})`, aliases: ['M'] },
382
+ ],
383
+ 'patch',
384
+ );
385
+ targetVersion = bumpVersion(currentVersion, bumpKind);
386
+ } else if (choice === 'custom') {
387
+ while (true) {
388
+ const input = normalizeVersion(await promptInputImpl(' Custom version (semver)'));
389
+ if (!input) {
390
+ console.error(' Version is required.');
391
+ continue;
392
+ }
393
+ if (!isValidVersion(input)) {
394
+ console.error(' Invalid version format. Use semver like 1.2.3 or 1.2.3-beta.1');
395
+ continue;
396
+ }
397
+ targetVersion = input;
398
+ break;
399
+ }
400
+ }
401
+ } else {
402
+ const defaultVersion = packageVersion ? '' : '0.1.0';
403
+ const promptLabel = defaultVersion
404
+ ? ` Package version (semver, default: ${defaultVersion})`
405
+ : ' Package version (semver)';
406
+ while (true) {
407
+ const input = normalizeVersion(await promptInputImpl(promptLabel));
408
+ const candidate = input || defaultVersion;
409
+ if (!candidate) {
410
+ console.error(' Version is required.');
411
+ continue;
412
+ }
413
+ if (!isValidVersion(candidate)) {
414
+ console.error(' Invalid version format. Use semver like 1.2.3 or 1.2.3-beta.1');
415
+ continue;
416
+ }
417
+ targetVersion = candidate;
418
+ break;
419
+ }
420
+ }
421
+ }
422
+
423
+ if (targetVersion && projectPackage.version !== targetVersion) {
424
+ projectPackage.version = targetVersion;
425
+ writeFileSync(packagePath, `${JSON.stringify(projectPackage, null, 2)}\n`, 'utf8');
426
+ }
427
+
428
+ return {
429
+ publishArgs: sanitizedArgs,
430
+ packageVersion: targetVersion,
431
+ };
432
+ }
433
+
160
434
  export function buildPublishArgs(commandArgs, { packageName } = {}) {
161
435
  const sanitizedArgs = stripOption(commandArgs, '--token');
162
436
  const publishArgs = ['publish', ...sanitizedArgs];
@@ -217,6 +491,9 @@ export async function runPublishCommand(
217
491
  stdout = process.stdout,
218
492
  stderr = process.stderr,
219
493
  validateProject = validatePublishProject,
494
+ canPromptInteractivelyImpl = canPromptInteractively,
495
+ promptSelectImpl = promptSelect,
496
+ promptInputImpl = promptInput,
220
497
  } = {},
221
498
  ) {
222
499
  if (isNpmPublishLifecycleInvocation(env)) {
@@ -231,6 +508,19 @@ export async function runPublishCommand(
231
508
  };
232
509
  }
233
510
 
511
+ await resolvePublishPackageName(commandArgs, {
512
+ projectRoot,
513
+ canPromptInteractivelyImpl,
514
+ promptInputImpl,
515
+ });
516
+
517
+ const publishVersionResolution = await resolvePublishVersion(commandArgs, {
518
+ projectRoot,
519
+ canPromptInteractivelyImpl,
520
+ promptSelectImpl,
521
+ promptInputImpl,
522
+ });
523
+
234
524
  let validation;
235
525
  try {
236
526
  validation = await validateProject({
@@ -260,7 +550,10 @@ export async function runPublishCommand(
260
550
 
261
551
  const packageName = validation.packageName;
262
552
  const { projectPackage } = readProjectPackage(projectRoot);
263
- const { publishArgs: sanitizedCommandArgs, publishEnv, publishToken } = extractPublishToken(commandArgs, env);
553
+ const { publishArgs: sanitizedCommandArgs, publishEnv, publishToken } = extractPublishToken(
554
+ publishVersionResolution.publishArgs,
555
+ env,
556
+ );
264
557
  const publishArgs = buildPublishArgs(sanitizedCommandArgs, { packageName });
265
558
  const publishTokenEnv = createPublishTokenEnv(publishToken, publishEnv);
266
559
  const dryRun = hasOption(sanitizedCommandArgs, '--dry-run');
@@ -284,6 +577,9 @@ export async function runPublishCommand(
284
577
  });
285
578
  stdout?.write(`\n aura publish: ${dryRun ? 'dry-run' : 'npm-first publish'}\n`);
286
579
  stdout?.write(` Package: ${packageName}\n`);
580
+ if (publishVersionResolution.packageVersion) {
581
+ stdout?.write(` Version: ${publishVersionResolution.packageVersion}\n`);
582
+ }
287
583
  stdout?.write(` Validation report: ${validation.reportPath}\n`);
288
584
  if (Number.isFinite(validation?.report?.validation?.assets?.assetBytes)) {
289
585
  const assets = validation.report.validation.assets;
@@ -309,6 +605,7 @@ export async function runPublishCommand(
309
605
  skipped: false,
310
606
  reasonCode: dryRun ? 'publish_dry_run_ok' : 'publish_ok',
311
607
  packageName,
608
+ packageVersion: publishVersionResolution.packageVersion || validation.packageVersion || null,
312
609
  reportPath: validation.reportPath,
313
610
  publishArgs,
314
611
  };
@@ -0,0 +1,257 @@
1
+ import { resolve } from 'node:path';
2
+
3
+ import { executeFrame } from './headless-test.mjs';
4
+ import {
5
+ applySessionState,
6
+ bootSessionRuntime,
7
+ exportSessionState,
8
+ } from './session-runtime.mjs';
9
+
10
+ export const GAME_REPLAY_SCHEMA_VERSION = 'aurajs.game-replay.v1';
11
+ export const GAME_REPLAY_REPORT_SCHEMA_VERSION = 'aurajs.game-replay-report.v1';
12
+
13
+ export class ReplayError extends Error {
14
+ constructor(reasonCode, message, details = {}) {
15
+ super(message);
16
+ this.name = 'ReplayError';
17
+ this.reasonCode = reasonCode;
18
+ this.details = details;
19
+ }
20
+ }
21
+
22
+ function parseReplayFrames(value, fallback = 1) {
23
+ if (value == null) return fallback;
24
+ const numeric = Number(value);
25
+ if (!Number.isInteger(numeric) || numeric < 1) {
26
+ throw new ReplayError('invalid_replay_frames', 'Replay step frames must be a positive integer.');
27
+ }
28
+ return numeric;
29
+ }
30
+
31
+ function normalizeReplayPayload(payload) {
32
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
33
+ throw new ReplayError('invalid_replay_payload', 'Replay payload must be an object.');
34
+ }
35
+ if (payload.schemaVersion !== GAME_REPLAY_SCHEMA_VERSION) {
36
+ throw new ReplayError(
37
+ 'schema_version_mismatch',
38
+ `Unsupported replay schema "${payload.schemaVersion}". Expected "${GAME_REPLAY_SCHEMA_VERSION}".`,
39
+ );
40
+ }
41
+ if (!Array.isArray(payload.steps) || payload.steps.length === 0) {
42
+ throw new ReplayError('invalid_replay_payload', 'Replay payload requires a non-empty steps array.');
43
+ }
44
+
45
+ return {
46
+ schemaVersion: GAME_REPLAY_SCHEMA_VERSION,
47
+ seed: payload.seed == null ? null : String(payload.seed),
48
+ stopOnDivergence: payload.stopOnDivergence !== false,
49
+ initialState: payload.initialState ?? null,
50
+ steps: payload.steps.map((step, index) => {
51
+ if (!step || typeof step !== 'object' || Array.isArray(step)) {
52
+ throw new ReplayError('invalid_replay_step', `Replay step at index ${index} must be an object.`);
53
+ }
54
+ return {
55
+ label: typeof step.label === 'string' && step.label.trim().length > 0 ? step.label.trim() : null,
56
+ checkpointId: typeof step.checkpointId === 'string' && step.checkpointId.trim().length > 0 ? step.checkpointId.trim() : null,
57
+ expectFingerprint: typeof step.expectFingerprint === 'string' && step.expectFingerprint.trim().length > 0
58
+ ? step.expectFingerprint.trim()
59
+ : null,
60
+ frames: parseReplayFrames(step.frames, 1),
61
+ input: step.input && typeof step.input === 'object' && !Array.isArray(step.input)
62
+ ? step.input
63
+ : null,
64
+ };
65
+ }),
66
+ };
67
+ }
68
+
69
+ function hashReplaySeed(seed) {
70
+ const text = String(seed || '');
71
+ let hash = 2166136261;
72
+ for (let index = 0; index < text.length; index += 1) {
73
+ hash ^= text.charCodeAt(index);
74
+ hash = Math.imul(hash, 16777619);
75
+ }
76
+ return hash >>> 0;
77
+ }
78
+
79
+ function createSeededMath(seed) {
80
+ if (seed == null) return Math;
81
+ let state = hashReplaySeed(seed);
82
+ if (state === 0) state = 0x6d2b79f5;
83
+ const nextRandom = () => {
84
+ state = (state + 0x6d2b79f5) >>> 0;
85
+ let t = state;
86
+ t = Math.imul(t ^ (t >>> 15), t | 1);
87
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
88
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
89
+ };
90
+ const math = Object.create(Math);
91
+ math.random = nextRandom;
92
+ return math;
93
+ }
94
+
95
+ function captureStateOrThrow(runtime, seed, capturedAt = null) {
96
+ const result = exportSessionState(runtime, {
97
+ mode: 'headless',
98
+ seed,
99
+ capturedAt,
100
+ });
101
+ if (!result || typeof result !== 'object' || result.ok !== true || !result.payload) {
102
+ throw new ReplayError(
103
+ typeof result?.reasonCode === 'string' ? result.reasonCode : 'replay_state_export_failed',
104
+ result?.detail
105
+ ? `Replay state export failed: ${result.detail}`
106
+ : 'Replay state export failed.',
107
+ { stateResult: result || null },
108
+ );
109
+ }
110
+ return result.payload;
111
+ }
112
+
113
+ function buildCheckpoint({ step, stepIndex, runtime, payload, matchedExpectation }) {
114
+ return {
115
+ checkpointId: step.checkpointId || null,
116
+ label: step.label || null,
117
+ stepIndex,
118
+ frameIndex: runtime.frameIndex,
119
+ elapsedSeconds: runtime.elapsedSeconds,
120
+ fingerprint: payload.export?.fingerprint || null,
121
+ matchedExpectation,
122
+ };
123
+ }
124
+
125
+ export async function runHeadlessReplay(options = {}) {
126
+ const projectRoot = resolve(options.projectRoot || process.cwd());
127
+ const replay = normalizeReplayPayload(options.payload);
128
+ const runtime = await bootSessionRuntime({
129
+ projectRoot,
130
+ file: options.file,
131
+ width: options.width,
132
+ height: options.height,
133
+ math: createSeededMath(replay.seed),
134
+ });
135
+
136
+ const inputController = runtime.aura?.input?.__headless;
137
+ if (!inputController || typeof inputController.applyFrame !== 'function' || typeof inputController.clearFrameTransitions !== 'function') {
138
+ throw new ReplayError(
139
+ 'replay_input_unavailable',
140
+ 'Headless replay requires the headless input controller.',
141
+ );
142
+ }
143
+
144
+ let initialStateApplied = false;
145
+ if (replay.initialState) {
146
+ const applyResult = applySessionState(runtime, replay.initialState);
147
+ if (!applyResult || typeof applyResult !== 'object' || applyResult.ok !== true) {
148
+ throw new ReplayError(
149
+ typeof applyResult?.reasonCode === 'string' ? applyResult.reasonCode : 'replay_initial_state_apply_failed',
150
+ applyResult?.detail
151
+ ? `Replay initial state apply failed: ${applyResult.detail}`
152
+ : 'Replay initial state apply failed.',
153
+ { applyResult: applyResult || null },
154
+ );
155
+ }
156
+ initialStateApplied = true;
157
+ }
158
+
159
+ const checkpoints = [];
160
+ let divergence = null;
161
+
162
+ for (let index = 0; index < replay.steps.length; index += 1) {
163
+ const step = replay.steps[index];
164
+ if (step.input) {
165
+ inputController.applyFrame(step.input);
166
+ } else {
167
+ inputController.clearFrameTransitions();
168
+ }
169
+
170
+ for (let frame = 0; frame < step.frames; frame += 1) {
171
+ await executeFrame(runtime.aura, 1 / 60);
172
+ runtime.frameIndex += 1;
173
+ runtime.elapsedSeconds += 1 / 60;
174
+ if (frame < step.frames - 1) {
175
+ inputController.clearFrameTransitions();
176
+ }
177
+ }
178
+
179
+ const needsCheckpoint = step.checkpointId || step.expectFingerprint;
180
+ if (!needsCheckpoint) {
181
+ inputController.clearFrameTransitions();
182
+ continue;
183
+ }
184
+
185
+ const payload = captureStateOrThrow(runtime, replay.seed, null);
186
+ const actualFingerprint = payload.export?.fingerprint || null;
187
+ const matchedExpectation = step.expectFingerprint == null || step.expectFingerprint === actualFingerprint;
188
+ checkpoints.push(buildCheckpoint({
189
+ step,
190
+ stepIndex: index,
191
+ runtime,
192
+ payload,
193
+ matchedExpectation,
194
+ }));
195
+
196
+ if (!matchedExpectation) {
197
+ divergence = {
198
+ stepIndex: index,
199
+ label: step.label || null,
200
+ checkpointId: step.checkpointId || null,
201
+ expectedFingerprint: step.expectFingerprint,
202
+ actualFingerprint,
203
+ frameIndex: runtime.frameIndex,
204
+ elapsedSeconds: runtime.elapsedSeconds,
205
+ };
206
+ if (replay.stopOnDivergence) {
207
+ break;
208
+ }
209
+ }
210
+
211
+ inputController.clearFrameTransitions();
212
+ }
213
+
214
+ const finalState = captureStateOrThrow(runtime, replay.seed, null);
215
+ const completed = divergence == null;
216
+ const report = {
217
+ schemaVersion: GAME_REPLAY_REPORT_SCHEMA_VERSION,
218
+ ok: completed,
219
+ reasonCode: completed ? 'replay_ok' : 'replay_diverged',
220
+ replaySchemaVersion: GAME_REPLAY_SCHEMA_VERSION,
221
+ mode: 'headless',
222
+ projectRoot,
223
+ entryFile: runtime.entryFile,
224
+ seed: replay.seed,
225
+ initialStateApplied,
226
+ totalSteps: replay.steps.length,
227
+ executedFrames: runtime.frameIndex,
228
+ elapsedSeconds: runtime.elapsedSeconds,
229
+ finalFingerprint: finalState.export?.fingerprint || null,
230
+ checkpoints,
231
+ divergence,
232
+ };
233
+
234
+ return {
235
+ replay,
236
+ report,
237
+ finalState,
238
+ runtime,
239
+ runtimeSummary: {
240
+ width: runtime.width,
241
+ height: runtime.height,
242
+ framesExecuted: runtime.frameIndex,
243
+ elapsedSeconds: runtime.elapsedSeconds,
244
+ bundleOutFile: runtime.bundle.outFile,
245
+ moduleCount: runtime.bundle.moduleCount,
246
+ assertionsPassed: runtime.testState.passes,
247
+ drawCalls: runtime.testState.drawCalls,
248
+ audioCalls: runtime.testState.audioCalls,
249
+ },
250
+ };
251
+ }
252
+
253
+ export function serializeReplayReport(payload, compact = false) {
254
+ return compact
255
+ ? `${JSON.stringify(payload)}\n`
256
+ : `${JSON.stringify(payload, null, 2)}\n`;
257
+ }
@@ -4,9 +4,11 @@ import { fileURLToPath } from 'node:url';
4
4
  const SCAFFOLD_DIR = dirname(fileURLToPath(import.meta.url));
5
5
  const CLI_SRC_DIR = resolve(SCAFFOLD_DIR, '..');
6
6
  const CLI_PACKAGE = JSON.parse(readFileSync(resolve(CLI_SRC_DIR, '../package.json'), 'utf8'));
7
+ export const CLI_PACKAGE_NAME = CLI_PACKAGE.name;
7
8
  const CLI_PACKAGE_VERSION = CLI_PACKAGE.version;
8
9
 
9
10
  export const LEGACY_STARTER_TEMPLATE_DIR = resolve(CLI_SRC_DIR, '../templates/starter');
11
+ export const PACKAGED_STARTER_UTILS_DIR = resolve(CLI_SRC_DIR, './helpers/starter-utils');
10
12
 
11
13
  export const CREATE_TEMPLATE_DIRS = {
12
14
  '2d-adventure': resolve(CLI_SRC_DIR, '../templates/create/2d-adventure'),
@@ -1,3 +1,4 @@
1
+ import { isUtf8 } from 'node:buffer';
1
2
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
3
  import { dirname, join } from 'node:path';
3
4
 
@@ -33,7 +34,13 @@ export function copyTree(src, dest, replacements) {
33
34
  continue;
34
35
  }
35
36
 
36
- let content = readFileSync(srcPath, 'utf8');
37
+ const sourceBytes = readFileSync(srcPath);
38
+ if (!isUtf8(sourceBytes)) {
39
+ writeFileSync(destPath, sourceBytes);
40
+ continue;
41
+ }
42
+
43
+ let content = sourceBytes.toString('utf8');
37
44
  for (const [key, value] of Object.entries(replacements)) {
38
45
  content = content.replaceAll(`{{${key}}}`, value);
39
46
  }