playcademy 0.28.1-beta.13 → 0.28.1-beta.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bucket.d.ts CHANGED
@@ -138,6 +138,19 @@ interface DashboardThemeConfig {
138
138
  /** Secondary/accent color as a hex string, e.g. '#ffd166' */
139
139
  secondary?: string;
140
140
  }
141
+ /**
142
+ * Child-catalog contract generation (see @playcademy/sdk/contracts).
143
+ * The extractor is a repo-local script (run with the CLI's runtime) that
144
+ * prints `{ generatedFrom, deliveries }` as JSON on stdout; the CLI
145
+ * validates, stamps the contract fields, and writes
146
+ * `.playcademy/catalog.json` deterministically. The document carries no
147
+ * app identity — consuming repos assign the namespace key from sync
148
+ * provenance.
149
+ */
150
+ interface CatalogConfig {
151
+ /** Path to the extractor script, relative to the config file. */
152
+ extractor: string;
153
+ }
141
154
  /**
142
155
  * Unified Playcademy configuration
143
156
  * Used for playcademy.config.{js,json}
@@ -170,6 +183,8 @@ interface PlaycademyConfig {
170
183
  dashboard?: DashboardConfig | boolean;
171
184
  /** Integrations (database, custom routes, external services) */
172
185
  integrations?: IntegrationsConfig;
186
+ /** Child-catalog contract generation (`playcademy catalog`) */
187
+ catalog?: CatalogConfig;
173
188
  }
174
189
 
175
190
  /**
package/dist/bucket.js CHANGED
@@ -44,7 +44,7 @@ var GAME_API_ROUTES_DIRECTORY = join2(GAME_SERVER_ROOT_DIRECTORY, "api");
44
44
  // ../better-auth/package.json
45
45
  var package_default = {
46
46
  name: "@playcademy/better-auth",
47
- version: "0.0.21-beta.13",
47
+ version: "0.0.21-beta.14",
48
48
  type: "module",
49
49
  exports: {
50
50
  "./server": {
package/dist/cli.js CHANGED
@@ -514,7 +514,10 @@ function nestColor(content, color2) {
514
514
  function customTransform(text) {
515
515
  let result = text;
516
516
  result = result.replace(/<<([^>]+)>>/g, (_, url) => blueBright(underline(url)));
517
- result = result.replace(/<([^>]+)>/g, (_, inner) => blueBright(`<${inner}>`));
517
+ result = result.replace(
518
+ /<([^>]+)>/g,
519
+ (match, inner) => blueBright(/[./]/.test(inner) ? inner : match)
520
+ );
518
521
  result = result.replace(/`([^`]+)`/g, (_, code) => nestColor(code, greenBright));
519
522
  return result;
520
523
  }
@@ -1207,7 +1210,7 @@ var SAMPLE_BUCKET_FILENAME = "bucket.ts";
1207
1210
  // ../better-auth/package.json
1208
1211
  var package_default = {
1209
1212
  name: "@playcademy/better-auth",
1210
- version: "0.0.21-beta.13",
1213
+ version: "0.0.21-beta.14",
1211
1214
  type: "module",
1212
1215
  exports: {
1213
1216
  "./server": {
@@ -3041,7 +3044,7 @@ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as re
3041
3044
  import { join as join13 } from "node:path";
3042
3045
 
3043
3046
  // src/version.ts
3044
- var cliVersion = false ? "0.0.0-dev" : "0.28.1-beta.13";
3047
+ var cliVersion = false ? "0.0.0-dev" : "0.28.1-beta.14";
3045
3048
 
3046
3049
  // src/lib/init/database.ts
3047
3050
  var drizzleConfigTemplate = loadTemplateString("database/drizzle-config.ts");
package/dist/constants.js CHANGED
@@ -21,7 +21,7 @@ var SAMPLE_BUCKET_FILENAME = "bucket.ts";
21
21
  // ../better-auth/package.json
22
22
  var package_default = {
23
23
  name: "@playcademy/better-auth",
24
- version: "0.0.21-beta.13",
24
+ version: "0.0.21-beta.14",
25
25
  type: "module",
26
26
  exports: {
27
27
  "./server": {
package/dist/db.js CHANGED
@@ -2335,7 +2335,7 @@ var GAME_API_ROUTES_DIRECTORY = join2(GAME_SERVER_ROOT_DIRECTORY, "api");
2335
2335
  // ../better-auth/package.json
2336
2336
  var package_default = {
2337
2337
  name: "@playcademy/better-auth",
2338
- version: "0.0.21-beta.13",
2338
+ version: "0.0.21-beta.14",
2339
2339
  type: "module",
2340
2340
  exports: {
2341
2341
  "./server": {
@@ -2817,7 +2817,10 @@ function nestColor(content, color2) {
2817
2817
  function customTransform(text) {
2818
2818
  let result = text;
2819
2819
  result = result.replace(/<<([^>]+)>>/g, (_, url) => blueBright(underline(url)));
2820
- result = result.replace(/<([^>]+)>/g, (_, inner) => blueBright(`<${inner}>`));
2820
+ result = result.replace(
2821
+ /<([^>]+)>/g,
2822
+ (match, inner) => blueBright(/[./]/.test(inner) ? inner : match)
2823
+ );
2821
2824
  result = result.replace(/`([^`]+)`/g, (_, code) => nestColor(code, greenBright));
2822
2825
  return result;
2823
2826
  }
package/dist/index.d.ts CHANGED
@@ -479,6 +479,19 @@ interface DashboardThemeConfig {
479
479
  /** Secondary/accent color as a hex string, e.g. '#ffd166' */
480
480
  secondary?: string;
481
481
  }
482
+ /**
483
+ * Child-catalog contract generation (see @playcademy/sdk/contracts).
484
+ * The extractor is a repo-local script (run with the CLI's runtime) that
485
+ * prints `{ generatedFrom, deliveries }` as JSON on stdout; the CLI
486
+ * validates, stamps the contract fields, and writes
487
+ * `.playcademy/catalog.json` deterministically. The document carries no
488
+ * app identity — consuming repos assign the namespace key from sync
489
+ * provenance.
490
+ */
491
+ interface CatalogConfig {
492
+ /** Path to the extractor script, relative to the config file. */
493
+ extractor: string;
494
+ }
482
495
  /**
483
496
  * Unified Playcademy configuration
484
497
  * Used for playcademy.config.{js,json}
@@ -511,6 +524,8 @@ interface PlaycademyConfig {
511
524
  dashboard?: DashboardConfig | boolean;
512
525
  /** Integrations (database, custom routes, external services) */
513
526
  integrations?: IntegrationsConfig;
527
+ /** Child-catalog contract generation (`playcademy catalog`) */
528
+ catalog?: CatalogConfig;
514
529
  }
515
530
 
516
531
  /**
package/dist/index.js CHANGED
@@ -2346,7 +2346,7 @@ var SAMPLE_BUCKET_FILENAME = "bucket.ts";
2346
2346
  // ../better-auth/package.json
2347
2347
  var package_default = {
2348
2348
  name: "@playcademy/better-auth",
2349
- version: "0.0.21-beta.13",
2349
+ version: "0.0.21-beta.14",
2350
2350
  type: "module",
2351
2351
  exports: {
2352
2352
  "./server": {
@@ -3168,7 +3168,10 @@ function nestColor(content, color2) {
3168
3168
  function customTransform(text4) {
3169
3169
  let result = text4;
3170
3170
  result = result.replace(/<<([^>]+)>>/g, (_, url) => blueBright(underline(url)));
3171
- result = result.replace(/<([^>]+)>/g, (_, inner) => blueBright(`<${inner}>`));
3171
+ result = result.replace(
3172
+ /<([^>]+)>/g,
3173
+ (match, inner) => blueBright(/[./]/.test(inner) ? inner : match)
3174
+ );
3172
3175
  result = result.replace(/`([^`]+)`/g, (_, code) => nestColor(code, greenBright));
3173
3176
  return result;
3174
3177
  }
@@ -6235,7 +6238,7 @@ async function checkbox(config) {
6235
6238
  // src/lib/core/subprocess.ts
6236
6239
  import { spawn } from "node:child_process";
6237
6240
  function runShell(command, options = {}) {
6238
- return new Promise((resolve18, reject) => {
6241
+ return new Promise((resolve19, reject) => {
6239
6242
  const env = { ...options.env ?? process.env };
6240
6243
  delete env.ESBUILD_BINARY_PATH;
6241
6244
  const child = spawn(command, {
@@ -6255,7 +6258,7 @@ function runShell(command, options = {}) {
6255
6258
  child.on("error", reject);
6256
6259
  child.on("close", (code) => {
6257
6260
  if (code === 0) {
6258
- resolve18();
6261
+ resolve19();
6259
6262
  } else {
6260
6263
  reject(
6261
6264
  Object.assign(new Error(`Command failed: ${command}`), {
@@ -6468,7 +6471,7 @@ import { existsSync as existsSync11, mkdirSync as mkdirSync2, readFileSync as re
6468
6471
  import { join as join13 } from "node:path";
6469
6472
 
6470
6473
  // src/version.ts
6471
- var cliVersion = false ? "0.0.0-dev" : "0.28.1-beta.13";
6474
+ var cliVersion = false ? "0.0.0-dev" : "0.28.1-beta.14";
6472
6475
 
6473
6476
  // src/lib/init/database.ts
6474
6477
  var drizzleConfigTemplate = loadTemplateString("database/drizzle-config.ts");
@@ -10160,14 +10163,14 @@ function readServerInfo(type, projectRoot) {
10160
10163
  return servers[0] || null;
10161
10164
  }
10162
10165
  async function isPortInUse(port) {
10163
- return new Promise((resolve18) => {
10166
+ return new Promise((resolve19) => {
10164
10167
  const server = createServer();
10165
10168
  server.once("error", () => {
10166
- resolve18(true);
10169
+ resolve19(true);
10167
10170
  });
10168
10171
  server.once("listening", () => {
10169
10172
  server.close();
10170
- resolve18(false);
10173
+ resolve19(false);
10171
10174
  });
10172
10175
  server.listen(port, "0.0.0.0");
10173
10176
  });
@@ -10178,7 +10181,7 @@ async function waitForPort(port, timeoutMs = 5e3) {
10178
10181
  if (Date.now() - start > timeoutMs) {
10179
10182
  throw new Error(`Port ${port} is already in use`);
10180
10183
  }
10181
- await new Promise((resolve18) => setTimeout(resolve18, 100));
10184
+ await new Promise((resolve19) => setTimeout(resolve19, 100));
10182
10185
  }
10183
10186
  }
10184
10187
 
@@ -13451,7 +13454,7 @@ function sleep(ms) {
13451
13454
  if (ms <= 0) {
13452
13455
  return Promise.resolve();
13453
13456
  }
13454
- return new Promise((resolve18) => setTimeout(resolve18, ms));
13457
+ return new Promise((resolve19) => setTimeout(resolve19, ms));
13455
13458
  }
13456
13459
  async function runWithConcurrency(items, concurrency, worker) {
13457
13460
  if (items.length === 0) {
@@ -15848,21 +15851,21 @@ var Matcher = class {
15848
15851
  * @returns {string}
15849
15852
  */
15850
15853
  toString(separator, includeNamespace = true) {
15851
- const sep6 = separator || this.separator;
15852
- const isDefault = sep6 === this.separator && includeNamespace === true;
15854
+ const sep7 = separator || this.separator;
15855
+ const isDefault = sep7 === this.separator && includeNamespace === true;
15853
15856
  if (isDefault) {
15854
15857
  if (this._pathStringCache !== null) {
15855
15858
  return this._pathStringCache;
15856
15859
  }
15857
15860
  const result = this.path.map(
15858
15861
  (n) => n.namespace ? `${n.namespace}:${n.tag}` : n.tag
15859
- ).join(sep6);
15862
+ ).join(sep7);
15860
15863
  this._pathStringCache = result;
15861
15864
  return result;
15862
15865
  }
15863
15866
  return this.path.map(
15864
15867
  (n) => includeNamespace && n.namespace ? `${n.namespace}:${n.tag}` : n.tag
15865
- ).join(sep6);
15868
+ ).join(sep7);
15866
15869
  }
15867
15870
  /**
15868
15871
  * Get path as array of tag names.
@@ -24798,6 +24801,162 @@ async function bundleStubWorker() {
24798
24801
  return cachedBundle;
24799
24802
  }
24800
24803
 
24804
+ // src/lib/catalog/document.ts
24805
+ import {
24806
+ CATALOG_LEVELS,
24807
+ CATALOG_READINESS,
24808
+ CHILD_ATTEMPT_CONTRACT,
24809
+ CHILD_CATALOG_CONTRACT
24810
+ } from "@playcademy/sdk/contracts";
24811
+ var CatalogError = class extends Error {
24812
+ hint;
24813
+ constructor(message2, hint) {
24814
+ super(message2);
24815
+ this.name = "CatalogError";
24816
+ this.hint = hint;
24817
+ }
24818
+ };
24819
+ function isRecord3(value) {
24820
+ return typeof value === "object" && value !== null && !Array.isArray(value);
24821
+ }
24822
+ function assertExtractorOutput(value) {
24823
+ if (!isRecord3(value)) {
24824
+ throw new CatalogError("Extractor output must be a JSON object");
24825
+ }
24826
+ if (typeof value.generatedFrom !== "string" || value.generatedFrom.length === 0) {
24827
+ throw new CatalogError(
24828
+ 'Extractor output is missing "generatedFrom"',
24829
+ 'Name the in-repo source of truth, e.g. "data/e_level_route_catalog.json"'
24830
+ );
24831
+ }
24832
+ if (!isRecord3(value.deliveries)) {
24833
+ throw new CatalogError(
24834
+ 'Extractor output is missing "deliveries" (lesson id \u2192 level \u2192 readiness)'
24835
+ );
24836
+ }
24837
+ for (const [lessonId, levels] of Object.entries(value.deliveries)) {
24838
+ if (!isRecord3(levels)) {
24839
+ throw new CatalogError(`Delivery "${lessonId}" must map levels to readiness`);
24840
+ }
24841
+ for (const [level, readiness] of Object.entries(levels)) {
24842
+ if (!CATALOG_LEVELS.includes(level)) {
24843
+ throw new CatalogError(
24844
+ `Delivery "${lessonId}" has unknown level "${level}"`,
24845
+ `Levels are: ${CATALOG_LEVELS.join(", ")}`
24846
+ );
24847
+ }
24848
+ if (!CATALOG_READINESS.includes(readiness)) {
24849
+ throw new CatalogError(
24850
+ `Delivery "${lessonId}" level "${level}" has unknown readiness "${String(readiness)}"`,
24851
+ `Readiness values are: ${CATALOG_READINESS.join(", ")}`
24852
+ );
24853
+ }
24854
+ }
24855
+ }
24856
+ }
24857
+ function buildCatalog(output, generatedBy) {
24858
+ return {
24859
+ contract: CHILD_CATALOG_CONTRACT,
24860
+ evidence: CHILD_ATTEMPT_CONTRACT,
24861
+ generatedBy,
24862
+ generatedFrom: output.generatedFrom,
24863
+ deliveries: output.deliveries
24864
+ };
24865
+ }
24866
+ function sortDeep(value) {
24867
+ if (Array.isArray(value)) {
24868
+ return value.map(sortDeep);
24869
+ }
24870
+ if (isRecord3(value)) {
24871
+ const out = {};
24872
+ for (const key of Object.keys(value).toSorted()) {
24873
+ out[key] = sortDeep(value[key]);
24874
+ }
24875
+ return out;
24876
+ }
24877
+ return value;
24878
+ }
24879
+ function renderCatalog(catalog) {
24880
+ return `${JSON.stringify(sortDeep(catalog), null, 2)}
24881
+ `;
24882
+ }
24883
+
24884
+ // src/lib/catalog/extract.ts
24885
+ import { spawnSync } from "node:child_process";
24886
+ import { existsSync as existsSync32, mkdirSync as mkdirSync11, readFileSync as readFileSync15, writeFileSync as writeFileSync17 } from "node:fs";
24887
+ import { dirname as dirname9, join as join44, relative as relative7, resolve as resolve14, sep as sep4 } from "node:path";
24888
+ function jsRuntime() {
24889
+ if (!process.isBun) {
24890
+ return process.execPath;
24891
+ }
24892
+ const found = Bun.which("bun") ?? Bun.which("node");
24893
+ if (!found) {
24894
+ throw new CatalogError(
24895
+ "No JavaScript runtime found to run the extractor",
24896
+ "Install bun (or node) and make sure it is on PATH"
24897
+ );
24898
+ }
24899
+ return found;
24900
+ }
24901
+ var CATALOG_PATH = ".playcademy/catalog.json";
24902
+ function assertNotIgnored(path4, root) {
24903
+ const result = spawnSync("git", ["check-ignore", "-q", path4], { cwd: root });
24904
+ if (result.error || result.status !== 0) {
24905
+ return;
24906
+ }
24907
+ throw new CatalogError(
24908
+ `<${CATALOG_PATH}> is gitignored, so it would never be committed or synced`,
24909
+ "Narrow the ignore: replace '.playcademy' with '.playcademy/*' plus '!.playcademy/catalog.json'"
24910
+ );
24911
+ }
24912
+ async function extractCatalog() {
24913
+ const { config, configPath } = await loadConfigWithPath();
24914
+ const root = dirname9(configPath);
24915
+ const extractor = config.catalog?.extractor;
24916
+ if (!extractor) {
24917
+ throw new CatalogError(
24918
+ "No catalog extractor configured",
24919
+ "Add `catalog: { extractor: './tools/extract-catalog.mjs' }` to <playcademy.config>"
24920
+ );
24921
+ }
24922
+ const extractorPath = resolve14(root, extractor);
24923
+ if (!existsSync32(extractorPath)) {
24924
+ throw new CatalogError(`Catalog extractor not found: <${extractor}>`);
24925
+ }
24926
+ const result = spawnSync(jsRuntime(), [extractorPath], {
24927
+ cwd: root,
24928
+ encoding: "utf8",
24929
+ maxBuffer: 16 * 1024 * 1024
24930
+ });
24931
+ if (result.status !== 0) {
24932
+ throw new CatalogError(
24933
+ `Catalog extractor exited with ${String(result.status)}`,
24934
+ result.stderr?.trim() || void 0
24935
+ );
24936
+ }
24937
+ let parsed;
24938
+ try {
24939
+ parsed = JSON.parse(result.stdout);
24940
+ } catch {
24941
+ throw new CatalogError(
24942
+ "Catalog extractor did not print valid JSON on stdout",
24943
+ "The extractor must print only the JSON document (log to stderr instead)"
24944
+ );
24945
+ }
24946
+ assertExtractorOutput(parsed);
24947
+ const generatedBy = relative7(root, extractorPath).split(sep4).join("/");
24948
+ const catalog = buildCatalog(parsed, generatedBy);
24949
+ const path4 = join44(root, CATALOG_PATH);
24950
+ assertNotIgnored(path4, root);
24951
+ const rendered = renderCatalog(catalog);
24952
+ const existing = existsSync32(path4) ? readFileSync15(path4, "utf8") : "";
24953
+ return { catalog, path: path4, rendered, current: existing === rendered };
24954
+ }
24955
+ function writeCatalog(run) {
24956
+ mkdirSync11(dirname9(run.path), { recursive: true });
24957
+ writeFileSync17(run.path, run.rendered);
24958
+ }
24959
+
24801
24960
  // src/lib/dashboard/report.ts
24802
24961
  import { underline as underline2 } from "colorette";
24803
24962
  function reportCredentialLink(kind, email, token, dashboardUrl) {
@@ -24885,8 +25044,8 @@ async function bootstrapDashboardAdmin(slug, dashboardUrl) {
24885
25044
  }
24886
25045
 
24887
25046
  // src/lib/dashboard/init.ts
24888
- import { existsSync as existsSync32, mkdirSync as mkdirSync11, readdirSync as readdirSync4, readFileSync as readFileSync15, writeFileSync as writeFileSync17 } from "node:fs";
24889
- import { join as join44, normalize as normalize3, resolve as resolve14 } from "node:path";
25047
+ import { existsSync as existsSync33, mkdirSync as mkdirSync12, readdirSync as readdirSync4, readFileSync as readFileSync16, writeFileSync as writeFileSync18 } from "node:fs";
25048
+ import { join as join45, normalize as normalize3, resolve as resolve15 } from "node:path";
24890
25049
  var DASHBOARD_PLUGIN_CALL = "playcademy({ dashboard: true })";
24891
25050
  function resolveScaffoldDirectory(dashboard, requested) {
24892
25051
  const normalized = normalizeDashboardConfig(dashboard);
@@ -24905,8 +25064,8 @@ async function scaffoldDashboard(options) {
24905
25064
  const { config, configPath, configDir } = await loadConfigWithPath(options.configPath);
24906
25065
  setCliContext({ workspace: configDir });
24907
25066
  const directory = resolveScaffoldDirectory(config.dashboard, options.directory);
24908
- const destDir = resolve14(configDir, directory);
24909
- if (existsSync32(destDir) && readdirSync4(destDir).length > 0) {
25067
+ const destDir = resolve15(configDir, directory);
25068
+ if (existsSync33(destDir) && readdirSync4(destDir).length > 0) {
24910
25069
  throw new Error(
24911
25070
  `${directory} already exists and is not empty. Remove it or pass a different directory.`
24912
25071
  );
@@ -24938,10 +25097,10 @@ async function scaffoldDashboard(options) {
24938
25097
  displayNextSteps(pm, directory);
24939
25098
  }
24940
25099
  function renamePackage(destDir, projectName) {
24941
- const pkgPath = join44(destDir, "package.json");
24942
- const pkg = JSON.parse(readFileSync15(pkgPath, "utf8"));
25100
+ const pkgPath = join45(destDir, "package.json");
25101
+ const pkg = JSON.parse(readFileSync16(pkgPath, "utf8"));
24943
25102
  pkg.name = `${generateSlug(projectName)}-dashboard`;
24944
- writeFileSync17(pkgPath, `${JSON.stringify(pkg, null, 2)}
25103
+ writeFileSync18(pkgPath, `${JSON.stringify(pkg, null, 2)}
24945
25104
  `);
24946
25105
  }
24947
25106
  async function configureVitePlugin(destDir) {
@@ -24955,7 +25114,7 @@ async function configureVitePlugin(destDir) {
24955
25114
  async function enableDashboardInConfig(configPath, directory) {
24956
25115
  const value = dashboardConfigValue(directory);
24957
25116
  const valueSource = value === true ? "true" : `{ directory: '${directory}' }`;
24958
- const content = readFileSync15(configPath, "utf8");
25117
+ const content = readFileSync16(configPath, "utf8");
24959
25118
  if (configPath.endsWith(".json")) {
24960
25119
  const parsed = JSON.parse(content);
24961
25120
  if (parsed.dashboard !== void 0 && parsed.dashboard !== false) {
@@ -24968,13 +25127,13 @@ async function enableDashboardInConfig(configPath, directory) {
24968
25127
  "dashboard",
24969
25128
  value
24970
25129
  ]);
24971
- writeFileSync17(configPath, `${JSON.stringify(Object.fromEntries(entries), null, 4)}
25130
+ writeFileSync18(configPath, `${JSON.stringify(Object.fromEntries(entries), null, 4)}
24972
25131
  `);
24973
25132
  return;
24974
25133
  }
24975
25134
  const disabledKey = /(["']?dashboard["']?\s*:\s*)false/;
24976
25135
  if (disabledKey.test(content)) {
24977
- writeFileSync17(configPath, content.replace(disabledKey, `$1${valueSource}`));
25136
+ writeFileSync18(configPath, content.replace(disabledKey, `$1${valueSource}`));
24978
25137
  await formatConfigWithPrettier(configPath);
24979
25138
  return;
24980
25139
  }
@@ -24988,7 +25147,7 @@ async function enableDashboardInConfig(configPath, directory) {
24988
25147
  ]);
24989
25148
  return;
24990
25149
  }
24991
- writeFileSync17(configPath, updated);
25150
+ writeFileSync18(configPath, updated);
24992
25151
  await formatConfigWithPrettier(configPath);
24993
25152
  }
24994
25153
  function displayNextSteps(pm, directory) {
@@ -25003,17 +25162,17 @@ function displayNextSteps(pm, directory) {
25003
25162
  logger.newLine();
25004
25163
  }
25005
25164
  function scaffoldSampleApiRoute(destDir) {
25006
- const apiDir = join44(destDir, DASHBOARD_API_ROUTES_DIRECTORY);
25007
- if (existsSync32(apiDir)) {
25165
+ const apiDir = join45(destDir, DASHBOARD_API_ROUTES_DIRECTORY);
25166
+ if (existsSync33(apiDir)) {
25008
25167
  return;
25009
25168
  }
25010
- mkdirSync11(apiDir, { recursive: true });
25011
- writeFileSync17(join44(apiDir, "whoami.ts"), loadTemplateString("dashboard-sample-route.ts"));
25012
- const gitignorePath = join44(destDir, ".gitignore");
25169
+ mkdirSync12(apiDir, { recursive: true });
25170
+ writeFileSync18(join45(apiDir, "whoami.ts"), loadTemplateString("dashboard-sample-route.ts"));
25171
+ const gitignorePath = join45(destDir, ".gitignore");
25013
25172
  const entry = "dashboard-env.d.ts";
25014
- const existing = existsSync32(gitignorePath) ? readFileSync15(gitignorePath, "utf8") : "";
25173
+ const existing = existsSync33(gitignorePath) ? readFileSync16(gitignorePath, "utf8") : "";
25015
25174
  if (!existing.split("\n").some((line) => line.trim() === entry)) {
25016
- writeFileSync17(
25175
+ writeFileSync18(
25017
25176
  gitignorePath,
25018
25177
  `${existing.replace(/\n*$/, "\n")}
25019
25178
  # Generated by Playcademy
@@ -25087,7 +25246,7 @@ async function loadProjectBase(configPath) {
25087
25246
 
25088
25247
  // src/lib/upgrade/channel.ts
25089
25248
  import { mkdir as mkdir6, readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
25090
- import { join as join45 } from "node:path";
25249
+ import { join as join46 } from "node:path";
25091
25250
  var RELEASE_CHANNELS = ["stable", "beta"];
25092
25251
  var CDN_BASE_URLS = {
25093
25252
  stable: "https://cdn.playcademy.net/cli",
@@ -25101,7 +25260,7 @@ function getLatestVersionUrl(channel) {
25101
25260
  }
25102
25261
  var CHANNEL_FILE = "channel";
25103
25262
  function getChannelFilePath() {
25104
- return join45(getConfigDir(), CHANNEL_FILE);
25263
+ return join46(getConfigDir(), CHANNEL_FILE);
25105
25264
  }
25106
25265
  function channelFromVersion(version3) {
25107
25266
  return version3.includes("-beta") ? "beta" : "stable";
@@ -25135,18 +25294,18 @@ var KNOWN_KEYS = [
25135
25294
  "@playcademy/sdk",
25136
25295
  "@playcademy/vite-plugin"
25137
25296
  ];
25138
- function isRecord3(value) {
25297
+ function isRecord4(value) {
25139
25298
  return typeof value === "object" && value !== null && !Array.isArray(value);
25140
25299
  }
25141
25300
  var FLOOR_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
25142
25301
  function validateCompatibilityManifest(data) {
25143
- if (!isRecord3(data)) {
25302
+ if (!isRecord4(data)) {
25144
25303
  return null;
25145
25304
  }
25146
25305
  if (typeof data.updatedAt !== "string") {
25147
25306
  return null;
25148
25307
  }
25149
- if (!isRecord3(data.minimumVersions)) {
25308
+ if (!isRecord4(data.minimumVersions)) {
25150
25309
  return null;
25151
25310
  }
25152
25311
  const minimumVersions = {};
@@ -25183,7 +25342,7 @@ async function fetchCompatibilityManifest(channel) {
25183
25342
  }
25184
25343
 
25185
25344
  // src/lib/deploy/game/baseline-evidence.ts
25186
- import { existsSync as existsSync33 } from "fs";
25345
+ import { existsSync as existsSync34 } from "fs";
25187
25346
  import { readFile as readFile7 } from "fs/promises";
25188
25347
  async function loadBaselineEvidence() {
25189
25348
  const migrationsDir = await getMigrationsDirectory();
@@ -25199,7 +25358,7 @@ async function loadBaselineEvidence() {
25199
25358
  let previous = { tables: {} };
25200
25359
  for (const entry of sorted) {
25201
25360
  const snapshotPath = migrationSnapshotPath(migrationsDir, entry.idx);
25202
- if (!existsSync33(snapshotPath)) {
25361
+ if (!existsSync34(snapshotPath)) {
25203
25362
  return null;
25204
25363
  }
25205
25364
  const snapshot = JSON.parse(await readFile7(snapshotPath, "utf8"));
@@ -25561,13 +25720,13 @@ async function loadDeployConfig(configPath) {
25561
25720
  }
25562
25721
 
25563
25722
  // src/lib/deploy/dashboard.ts
25564
- import { existsSync as existsSync35 } from "node:fs";
25565
- import { join as join48 } from "node:path";
25723
+ import { existsSync as existsSync36 } from "node:fs";
25724
+ import { join as join49 } from "node:path";
25566
25725
  import { underline as underline3 } from "colorette";
25567
25726
 
25568
25727
  // ../utils/src/zip.ts
25569
25728
  import { mkdir as mkdir7, readdir, readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
25570
- import { join as join46, relative as relative7, sep as sep4 } from "path";
25729
+ import { join as join47, relative as relative8, sep as sep5 } from "path";
25571
25730
  import JSZip2 from "jszip";
25572
25731
  async function createZipFromDirectory(sourceDir) {
25573
25732
  try {
@@ -25575,8 +25734,8 @@ async function createZipFromDirectory(sourceDir) {
25575
25734
  const entries = await readdir(sourceDir, { withFileTypes: true, recursive: true });
25576
25735
  for (const entry of entries) {
25577
25736
  if (entry.isFile()) {
25578
- const filePath = join46(entry.parentPath, entry.name);
25579
- const entryName = relative7(sourceDir, filePath).replaceAll(sep4, "/");
25737
+ const filePath = join47(entry.parentPath, entry.name);
25738
+ const entryName = relative8(sourceDir, filePath).replaceAll(sep5, "/");
25580
25739
  zip.file(entryName, readFile8(filePath));
25581
25740
  }
25582
25741
  }
@@ -25703,8 +25862,8 @@ async function executeUpdates(workspace, deps) {
25703
25862
  }
25704
25863
 
25705
25864
  // src/lib/deploy/version-gate.ts
25706
- import { existsSync as existsSync34, readFileSync as readFileSync16 } from "node:fs";
25707
- import { join as join47 } from "node:path";
25865
+ import { existsSync as existsSync35, readFileSync as readFileSync17 } from "node:fs";
25866
+ import { join as join48 } from "node:path";
25708
25867
 
25709
25868
  // src/lib/upgrade/version.ts
25710
25869
  var SAFE_VERSION_PATTERN = /^(?:[0-9a-zA-Z][0-9a-zA-Z._\-+]*|[vV][0-9a-zA-Z][0-9a-zA-Z._\-+]*)$/;
@@ -25825,12 +25984,12 @@ function isDevCliVersion(version3) {
25825
25984
  return version3.startsWith("0.0.0") || version3.includes("-dev");
25826
25985
  }
25827
25986
  function resolveInstalledVersion(name, cwd) {
25828
- const pkgJsonPath = join47(cwd, "node_modules", name, "package.json");
25829
- if (!existsSync34(pkgJsonPath)) {
25987
+ const pkgJsonPath = join48(cwd, "node_modules", name, "package.json");
25988
+ if (!existsSync35(pkgJsonPath)) {
25830
25989
  return null;
25831
25990
  }
25832
25991
  try {
25833
- const parsed = JSON.parse(readFileSync16(pkgJsonPath, "utf8"));
25992
+ const parsed = JSON.parse(readFileSync17(pkgJsonPath, "utf8"));
25834
25993
  return typeof parsed.version === "string" ? parsed.version : null;
25835
25994
  } catch {
25836
25995
  return null;
@@ -25990,7 +26149,7 @@ async function deployDashboard(options) {
25990
26149
  () => runShell(dashboard.buildCommand, { cwd: dashboard.directory })
25991
26150
  );
25992
26151
  }
25993
- if (!existsSync35(join48(dashboard.buildPath, "index.html"))) {
26152
+ if (!existsSync36(join49(dashboard.buildPath, "index.html"))) {
25994
26153
  throw new Error(
25995
26154
  `Dashboard build output at ${dashboard.buildPath} has no index.html; check the dashboard's build configuration`
25996
26155
  );
@@ -26603,7 +26762,7 @@ function displayDeploymentDiff(options) {
26603
26762
  }
26604
26763
 
26605
26764
  // src/lib/deploy/game/interaction.ts
26606
- import { join as join50 } from "path";
26765
+ import { join as join51 } from "path";
26607
26766
  import { bold as bold9 } from "colorette";
26608
26767
  import { ApiError as ApiError4 } from "@playcademy/sdk/internal";
26609
26768
 
@@ -26670,7 +26829,7 @@ var DEPLOY_REFUSAL_CODES = new Set(REFUSAL_CODES);
26670
26829
 
26671
26830
  // src/lib/deploy/game/rollback-check.ts
26672
26831
  import { readFile as readFile9 } from "node:fs/promises";
26673
- import { resolve as resolve15 } from "node:path";
26832
+ import { resolve as resolve16 } from "node:path";
26674
26833
  import JSZip3 from "jszip";
26675
26834
  var PLAYCADEMY_MANIFEST_FILE = "playcademy.manifest.json";
26676
26835
  var TRACKED_VERSIONS = [
@@ -26697,11 +26856,11 @@ function rollbackDisplayName(name) {
26697
26856
  return TRACKED_VERSIONS.find((entry) => entry.key === name).displayName;
26698
26857
  }
26699
26858
  function isManifestV2(manifest) {
26700
- return isRecord3(manifest) && manifest.version === "2" && isRecord3(manifest.versions);
26859
+ return isRecord4(manifest) && manifest.version === "2" && isRecord4(manifest.versions);
26701
26860
  }
26702
26861
  async function readBuildManifest(buildPath) {
26703
26862
  try {
26704
- const zip = await JSZip3.loadAsync(await readFile9(resolve15(buildPath)));
26863
+ const zip = await JSZip3.loadAsync(await readFile9(resolve16(buildPath)));
26705
26864
  const manifestFile = zip.file(PLAYCADEMY_MANIFEST_FILE) ?? Object.values(zip.files).find(
26706
26865
  (file) => !file.dir && file.name.endsWith(`/${PLAYCADEMY_MANIFEST_FILE}`)
26707
26866
  );
@@ -26746,7 +26905,7 @@ async function detectVersionRollback(context2, plan, fetchLiveGame) {
26746
26905
  }
26747
26906
 
26748
26907
  // src/lib/deploy/game/timeback-drift.ts
26749
- import { basename as basename3, join as join49 } from "node:path";
26908
+ import { basename as basename3, join as join50 } from "node:path";
26750
26909
  async function detectDeployTimebackDrift(context2) {
26751
26910
  const timebackConfig = context2.fullConfig?.integrations?.timeback;
26752
26911
  const gameId = context2.existingGame?.id;
@@ -26871,7 +27030,7 @@ async function applyDeployTimebackConfigSync(args) {
26871
27030
  timebackConfig,
26872
27031
  snapshot.platformConfigs
26873
27032
  );
26874
- const configPath = join49(context2.projectPath, context2.configFileName);
27033
+ const configPath = join50(context2.projectPath, context2.configFileName);
26875
27034
  await updateTimebackConfig(configPath, nextTimeback);
26876
27035
  fullConfig.integrations.timeback = nextTimeback;
26877
27036
  logger.success(
@@ -26958,14 +27117,14 @@ function getLocalOverrideCourseChangeCounts(snapshot) {
26958
27117
  }
26959
27118
 
26960
27119
  // src/lib/deploy/game/validate.ts
26961
- import { existsSync as existsSync36 } from "fs";
26962
- import { resolve as resolve16 } from "path";
27120
+ import { existsSync as existsSync37 } from "fs";
27121
+ import { resolve as resolve17 } from "path";
26963
27122
  function validateBuildPath(path4) {
26964
27123
  if (!path4.trim()) {
26965
27124
  return "Build path is required";
26966
27125
  }
26967
- const resolvedPath = resolve16(path4.trim());
26968
- if (!existsSync36(resolvedPath)) {
27126
+ const resolvedPath = resolve17(path4.trim());
27127
+ if (!existsSync37(resolvedPath)) {
26969
27128
  return `Build path not found: ${path4.trim()}`;
26970
27129
  }
26971
27130
  return true;
@@ -27520,7 +27679,7 @@ async function applyPlatformValuesToLocalConfig(args) {
27520
27679
  writeDisplayName
27521
27680
  });
27522
27681
  applyUpdatesToContext(context2, updates, writeDisplayName);
27523
- const configPath = join50(context2.projectPath, context2.configFileName);
27682
+ const configPath = join51(context2.projectPath, context2.configFileName);
27524
27683
  const result = await updateConfigScalars(configPath, updates);
27525
27684
  reportConfigUpdate(result, unresolvableLabels, context2.configFileName);
27526
27685
  return true;
@@ -27635,7 +27794,7 @@ var PLAYCADEMY_PLATFORM_VALUES = ["web", "godot"];
27635
27794
  function narrowToPlaycademyPlatform(value) {
27636
27795
  return PLAYCADEMY_PLATFORM_VALUES.includes(value) ? value : null;
27637
27796
  }
27638
- function isRecord4(value) {
27797
+ function isRecord5(value) {
27639
27798
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
27640
27799
  }
27641
27800
  function extractDeployServerError(error) {
@@ -27643,7 +27802,7 @@ function extractDeployServerError(error) {
27643
27802
  return null;
27644
27803
  }
27645
27804
  const details = error.details;
27646
- if (!isRecord4(details)) {
27805
+ if (!isRecord5(details)) {
27647
27806
  return null;
27648
27807
  }
27649
27808
  if (typeof details.code === "string") {
@@ -27652,7 +27811,7 @@ function extractDeployServerError(error) {
27652
27811
  if (Array.isArray(details.events)) {
27653
27812
  for (let i = details.events.length - 1; i >= 0; i--) {
27654
27813
  const event = details.events[i];
27655
- if (isRecord4(event) && isRecord4(event.details) && typeof event.details.code === "string") {
27814
+ if (isRecord5(event) && isRecord5(event.details) && typeof event.details.code === "string") {
27656
27815
  return { code: event.details.code, message: error.message, details: event.details };
27657
27816
  }
27658
27817
  }
@@ -27672,7 +27831,7 @@ function verdictLines(value) {
27672
27831
  if (!Array.isArray(value)) {
27673
27832
  return [];
27674
27833
  }
27675
- return value.filter(isRecord4).filter((entry) => typeof entry.tag === "string").map(
27834
+ return value.filter(isRecord5).filter((entry) => typeof entry.tag === "string").map(
27676
27835
  (entry) => ` ${entry.tag}${typeof entry.detail === "string" ? `: ${entry.detail}` : ""}`
27677
27836
  );
27678
27837
  }
@@ -27702,7 +27861,7 @@ function reportDeploymentStateConflict(serverError, claimedBaselineHash, current
27702
27861
  function migrationFailureGuidance(args) {
27703
27862
  const { tag: tag2, d1Message, details, context: context2 } = args;
27704
27863
  const resolveHint = ` \`${repairCommand(`db mark ${tag2} --applied`)}\``;
27705
- const assessment = isRecord4(details.alreadyApplied) ? details.alreadyApplied : null;
27864
+ const assessment = isRecord5(details.alreadyApplied) ? details.alreadyApplied : null;
27706
27865
  const present = stringList(assessment?.present);
27707
27866
  const missing = stringList(assessment?.missing);
27708
27867
  if (assessment?.verdict === "all-present") {
@@ -27941,7 +28100,7 @@ function renderDeployServerError(error, context2) {
27941
28100
  }
27942
28101
  case DEPLOY_ERROR_CODES.checksumMismatch: {
27943
28102
  const mismatches = Array.isArray(details.mismatches) ? details.mismatches : [];
27944
- const tags = mismatches.filter(isRecord4).map((mismatch) => mismatch.tag).filter((tag2) => typeof tag2 === "string");
28103
+ const tags = mismatches.filter(isRecord5).map((mismatch) => mismatch.tag).filter((tag2) => typeof tag2 === "string");
27945
28104
  logger.admonition("warning", "Migration Checksum Mismatch", checksumMismatchLines(tags));
27946
28105
  return true;
27947
28106
  }
@@ -28257,7 +28416,7 @@ function logDeploymentPlanDebug(plan, context2) {
28257
28416
 
28258
28417
  // src/lib/deploy/game/preparation.ts
28259
28418
  import { stat } from "node:fs/promises";
28260
- import { join as join51 } from "node:path";
28419
+ import { join as join52 } from "node:path";
28261
28420
 
28262
28421
  // ../data/src/domains/game/helpers.ts
28263
28422
  import { and, eq } from "drizzle-orm";
@@ -29114,8 +29273,8 @@ async function bundleAndHash(context2) {
29114
29273
  });
29115
29274
  const bundleHash = hashContent(bundle.code);
29116
29275
  const [serverSize, dbSize] = await Promise.all([
29117
- getDirectorySize(join51(context2.projectPath, GAME_SERVER_ROOT_DIRECTORY)),
29118
- getDirectorySize(join51(context2.projectPath, DEFAULT_DATABASE_DIRECTORY))
29276
+ getDirectorySize(join52(context2.projectPath, GAME_SERVER_ROOT_DIRECTORY)),
29277
+ getDirectorySize(join52(context2.projectPath, DEFAULT_DATABASE_DIRECTORY))
29119
29278
  ]);
29120
29279
  context2.currentBackendBundle = bundle;
29121
29280
  context2.currentBackendBundleHash = bundleHash;
@@ -29285,12 +29444,12 @@ function hasOptionalFieldsMissing(missing) {
29285
29444
  }
29286
29445
 
29287
29446
  // src/lib/deploy/game/steps.ts
29288
- import { existsSync as existsSync37 } from "fs";
29447
+ import { existsSync as existsSync38 } from "fs";
29289
29448
  import { readFile as readFile10 } from "fs/promises";
29290
- import { basename as basename4, resolve as resolve17 } from "path";
29449
+ import { basename as basename4, resolve as resolve18 } from "path";
29291
29450
  async function prepareBuildFile(buildPath) {
29292
- const resolvedPath = resolve17(buildPath);
29293
- if (resolvedPath.endsWith(".zip") && existsSync37(resolvedPath)) {
29451
+ const resolvedPath = resolve18(buildPath);
29452
+ if (resolvedPath.endsWith(".zip") && existsSync38(resolvedPath)) {
29294
29453
  const buffer = await readFile10(resolvedPath);
29295
29454
  const uint8Array = new Uint8Array(buffer);
29296
29455
  const blob = new Blob([uint8Array], { type: "application/zip" });
@@ -29595,11 +29754,11 @@ import { dim as dim9 } from "colorette";
29595
29754
 
29596
29755
  // src/lib/nudge/state.ts
29597
29756
  import { mkdir as mkdir8, readFile as readFile11, writeFile as writeFile7 } from "node:fs/promises";
29598
- import { join as join52 } from "node:path";
29757
+ import { join as join53 } from "node:path";
29599
29758
  var NUDGE_STATE_FILE = "nudge-state.json";
29600
29759
  var NUDGE_COOLDOWN_MS = 24 * 60 * 60 * 1e3;
29601
29760
  function getStatePath() {
29602
- return join52(getConfigDir(), NUDGE_STATE_FILE);
29761
+ return join53(getConfigDir(), NUDGE_STATE_FILE);
29603
29762
  }
29604
29763
  async function loadNudgeState() {
29605
29764
  try {
@@ -29675,35 +29834,35 @@ async function maybeShowUpdateNudge(options) {
29675
29834
  }
29676
29835
 
29677
29836
  // src/lib/db/path.ts
29678
- import { copyFileSync, existsSync as existsSync38, mkdirSync as mkdirSync12, readdirSync as readdirSync5, unlinkSync } from "fs";
29679
- import { join as join53 } from "path";
29837
+ import { copyFileSync, existsSync as existsSync39, mkdirSync as mkdirSync13, readdirSync as readdirSync5, unlinkSync } from "fs";
29838
+ import { join as join54 } from "path";
29680
29839
  var DB_DIRECTORY = CLI_DIRECTORIES.DATABASE;
29681
29840
  var INITIAL_DB_NAME = CLI_FILES.INITIAL_DATABASE;
29682
29841
  function ensureDirectoryExists(dir) {
29683
- if (!existsSync38(dir)) {
29684
- mkdirSync12(dir, { recursive: true });
29842
+ if (!existsSync39(dir)) {
29843
+ mkdirSync13(dir, { recursive: true });
29685
29844
  }
29686
29845
  }
29687
29846
  function findMiniflareDatabase(dbDir) {
29688
- const miniflareDir = join53(dbDir, MINIFLARE_D1_DIRECTORY);
29689
- if (!existsSync38(miniflareDir)) {
29847
+ const miniflareDir = join54(dbDir, MINIFLARE_D1_DIRECTORY);
29848
+ if (!existsSync39(miniflareDir)) {
29690
29849
  return null;
29691
29850
  }
29692
29851
  const sqliteFiles = readdirSync5(miniflareDir).filter((file) => file.endsWith(".sqlite"));
29693
29852
  if (sqliteFiles.length === 0) {
29694
29853
  return null;
29695
29854
  }
29696
- return join53(miniflareDir, sqliteFiles[0]);
29855
+ return join54(miniflareDir, sqliteFiles[0]);
29697
29856
  }
29698
29857
  function migrateInitialDbToTarget(initialPath, targetPath) {
29699
- if (!existsSync38(initialPath)) {
29858
+ if (!existsSync39(initialPath)) {
29700
29859
  return;
29701
29860
  }
29702
29861
  copyFileSync(initialPath, targetPath);
29703
29862
  unlinkSync(initialPath);
29704
29863
  }
29705
29864
  function getDevDbPath() {
29706
- const initialDbPath = join53(DB_DIRECTORY, INITIAL_DB_NAME);
29865
+ const initialDbPath = join54(DB_DIRECTORY, INITIAL_DB_NAME);
29707
29866
  ensureDirectoryExists(DB_DIRECTORY);
29708
29867
  const miniflareDbPath = findMiniflareDatabase(DB_DIRECTORY);
29709
29868
  if (miniflareDbPath) {
@@ -30404,10 +30563,10 @@ async function bundleSeedWorker(seedFilePath, projectPath) {
30404
30563
  // src/lib/db/reset.ts
30405
30564
  import { execSync as execSync8 } from "child_process";
30406
30565
  import { rmSync as rmSync3 } from "fs";
30407
- import { join as join54 } from "path";
30566
+ import { join as join55 } from "path";
30408
30567
  async function resetDatabase(workspace, mf, options = { debug: false }) {
30409
30568
  const { debug } = options;
30410
- const dbDir = join54(workspace, CLI_DIRECTORIES.DATABASE);
30569
+ const dbDir = join55(workspace, CLI_DIRECTORIES.DATABASE);
30411
30570
  await runStep(
30412
30571
  "Resetting database...",
30413
30572
  async () => {
@@ -30819,7 +30978,7 @@ function startCallbackServer(timeoutMs, expectedState) {
30819
30978
  bound = res;
30820
30979
  bindFailed = rej;
30821
30980
  });
30822
- const result = new Promise((resolve18) => {
30981
+ const result = new Promise((resolve19) => {
30823
30982
  let server = null;
30824
30983
  let timeoutId = null;
30825
30984
  let didBind = false;
@@ -30838,7 +30997,7 @@ function startCallbackServer(timeoutMs, expectedState) {
30838
30997
  }
30839
30998
  timeoutId = setTimeout(() => {
30840
30999
  cleanup(() => {
30841
- resolve18({
31000
+ resolve19({
30842
31001
  success: false,
30843
31002
  error: "Authentication timed out. Please try again."
30844
31003
  });
@@ -30856,7 +31015,7 @@ function startCallbackServer(timeoutMs, expectedState) {
30856
31015
  res.writeHead(200, { "Content-Type": "text/html" });
30857
31016
  res.end(SUCCESS_HTML);
30858
31017
  cleanup(() => {
30859
- resolve18(parsed);
31018
+ resolve19(parsed);
30860
31019
  });
30861
31020
  return;
30862
31021
  }
@@ -30874,7 +31033,7 @@ function startCallbackServer(timeoutMs, expectedState) {
30874
31033
  bindFailed(new Error(message2));
30875
31034
  }
30876
31035
  cleanup(() => {
30877
- resolve18({ success: false, error: message2 });
31036
+ resolve19({ success: false, error: message2 });
30878
31037
  });
30879
31038
  });
30880
31039
  server.listen(CALLBACK_PORT);
@@ -31016,7 +31175,7 @@ async function promptForPassword() {
31016
31175
 
31017
31176
  // src/lib/bucket/collect.ts
31018
31177
  import { readdirSync as readdirSync6, statSync as statSync5 } from "fs";
31019
- import { join as join55, relative as relative8, sep as sep5 } from "path";
31178
+ import { join as join56, relative as relative9, sep as sep6 } from "path";
31020
31179
  function isEnvFile(name) {
31021
31180
  const lower = name.toLowerCase();
31022
31181
  return lower === ".env" || lower === ".envrc" || lower.startsWith(".env.");
@@ -31025,8 +31184,8 @@ function isAlwaysSkippedName(segment) {
31025
31184
  return BUCKET_ALWAYS_SKIP.includes(segment) || isEnvFile(segment);
31026
31185
  }
31027
31186
  function shouldSkipFile(filePath, baseDir, gitignorePatterns) {
31028
- const relativePath = relative8(baseDir, filePath);
31029
- for (const part of relativePath.split(sep5)) {
31187
+ const relativePath = relative9(baseDir, filePath);
31188
+ for (const part of relativePath.split(sep6)) {
31030
31189
  if (isAlwaysSkippedName(part)) {
31031
31190
  return true;
31032
31191
  }
@@ -31038,7 +31197,7 @@ function collectFiles(directory, baseDir, gitignorePatterns) {
31038
31197
  try {
31039
31198
  const entries = readdirSync6(directory, { withFileTypes: true });
31040
31199
  for (const entry of entries) {
31041
- const fullPath = join55(directory, entry.name);
31200
+ const fullPath = join56(directory, entry.name);
31042
31201
  if (!shouldSkipFile(fullPath, baseDir, gitignorePatterns)) {
31043
31202
  if (entry.isDirectory()) {
31044
31203
  files.push(...collectFiles(fullPath, baseDir, gitignorePatterns));
@@ -31046,7 +31205,7 @@ function collectFiles(directory, baseDir, gitignorePatterns) {
31046
31205
  const stats = statSync5(fullPath);
31047
31206
  files.push({
31048
31207
  absolutePath: fullPath,
31049
- relativePath: relative8(baseDir, fullPath),
31208
+ relativePath: relative9(baseDir, fullPath),
31050
31209
  size: stats.size
31051
31210
  });
31052
31211
  }
@@ -31084,10 +31243,10 @@ async function confirmDestructive(message2, hint) {
31084
31243
  import { ApiError as ApiError6 } from "@playcademy/sdk/internal";
31085
31244
 
31086
31245
  // src/lib/bucket/local.ts
31087
- import { join as join56 } from "path";
31246
+ import { join as join57 } from "path";
31088
31247
  import { Miniflare as Miniflare3 } from "miniflare";
31089
31248
  async function openLocalBucket() {
31090
- const bucketDir = join56(getWorkspace(), CLI_DIRECTORIES.BUCKET);
31249
+ const bucketDir = join57(getWorkspace(), CLI_DIRECTORIES.BUCKET);
31091
31250
  const mf = new Miniflare3({
31092
31251
  modules: [{ type: "ESModule", path: "index.mjs", contents: "" }],
31093
31252
  r2Buckets: [CLOUDFLARE_BINDINGS.BUCKET],
@@ -31312,12 +31471,12 @@ async function runDeletePlan(plan, adapter, location, options) {
31312
31471
  }
31313
31472
 
31314
31473
  // src/lib/bucket/dev.ts
31315
- import { existsSync as existsSync39 } from "fs";
31316
- import { join as join57 } from "path";
31474
+ import { existsSync as existsSync40 } from "fs";
31475
+ import { join as join58 } from "path";
31317
31476
  var ASSET_DEV_ROUTE_PREFIX = ASSET_ROUTE_PREFIX;
31318
31477
  function hasCustomAssetsRoute(projectRoot, config) {
31319
31478
  const routesDir = getGameRoutesDirectory(projectRoot, config);
31320
- return existsSync39(join57(routesDir, "assets")) || existsSync39(join57(routesDir, "assets.ts"));
31479
+ return existsSync40(join58(routesDir, "assets")) || existsSync40(join58(routesDir, "assets.ts"));
31321
31480
  }
31322
31481
  function openLocalAssetBucket() {
31323
31482
  return openLocalBucket();
@@ -31343,7 +31502,7 @@ async function readLocalAsset(bucket, servingKey) {
31343
31502
 
31344
31503
  // src/lib/bucket/hash.ts
31345
31504
  import { createHash as createHash5 } from "crypto";
31346
- import { readFileSync as readFileSync17 } from "fs";
31505
+ import { readFileSync as readFileSync18 } from "fs";
31347
31506
  import { extname as extname3 } from "path";
31348
31507
  var HASH_LENGTH = 8;
31349
31508
  function computeHash(buffer) {
@@ -31358,7 +31517,7 @@ function insertHash(key, hash) {
31358
31517
  }
31359
31518
  function hashFiles(files) {
31360
31519
  return files.map((file) => {
31361
- const buffer = readFileSync17(file.absolutePath);
31520
+ const buffer = readFileSync18(file.absolutePath);
31362
31521
  const contentHash = computeHash(buffer);
31363
31522
  const key = getBucketKey(file.relativePath);
31364
31523
  return {
@@ -32588,7 +32747,7 @@ async function connectToLogStream(config) {
32588
32747
  if (backfillTask && !interrupted) {
32589
32748
  await Promise.race([
32590
32749
  backfillTask,
32591
- new Promise((resolve18) => setTimeout(resolve18, BACKFILL_EXIT_GRACE_MS))
32750
+ new Promise((resolve19) => setTimeout(resolve19, BACKFILL_EXIT_GRACE_MS))
32592
32751
  ]);
32593
32752
  }
32594
32753
  report?.();
@@ -33293,7 +33452,7 @@ function formatExit(name, code, signal) {
33293
33452
  return `${name} exited`;
33294
33453
  }
33295
33454
  function upgradeStandalone(targetVersion, channel) {
33296
- return new Promise((resolve18, reject) => {
33455
+ return new Promise((resolve19, reject) => {
33297
33456
  const curl = spawn2("curl", ["-fsSL", INSTALL_SCRIPT_URL], {
33298
33457
  stdio: ["ignore", "pipe", "pipe"]
33299
33458
  });
@@ -33327,7 +33486,7 @@ function upgradeStandalone(targetVersion, channel) {
33327
33486
  if (error) {
33328
33487
  reject(error);
33329
33488
  } else {
33330
- resolve18({ stdout: stdout2, stderr });
33489
+ resolve19({ stdout: stdout2, stderr });
33331
33490
  }
33332
33491
  }
33333
33492
  function tryFinalize() {
@@ -33395,7 +33554,7 @@ function upgradeStandalone(targetVersion, channel) {
33395
33554
  }
33396
33555
 
33397
33556
  // src/lib/upgrade/verify.ts
33398
- import { spawnSync } from "node:child_process";
33557
+ import { spawnSync as spawnSync2 } from "node:child_process";
33399
33558
  function extractReportedVersion(stdout2) {
33400
33559
  const lastLine = stdout2.trim().split("\n").at(-1)?.trim();
33401
33560
  if (!lastLine) {
@@ -33409,7 +33568,7 @@ function extractReportedVersion(stdout2) {
33409
33568
  }
33410
33569
  function verifyInstalledBinary(expectedVersion) {
33411
33570
  const expected = normalizeVersion(expectedVersion);
33412
- const result = spawnSync(process.execPath, ["--version"], {
33571
+ const result = spawnSync2(process.execPath, ["--version"], {
33413
33572
  encoding: "utf8",
33414
33573
  timeout: 3e4
33415
33574
  });
@@ -33429,10 +33588,10 @@ function verifyInstalledBinary(expectedVersion) {
33429
33588
 
33430
33589
  // src/lib/upgrade/windows.ts
33431
33590
  import { createHash as createHash6 } from "node:crypto";
33432
- import { existsSync as existsSync40 } from "node:fs";
33591
+ import { existsSync as existsSync41 } from "node:fs";
33433
33592
  import { copyFile, mkdir as mkdir9, open as open2, rename, rm as rm3 } from "node:fs/promises";
33434
33593
  import { homedir as homedir3 } from "node:os";
33435
- import { basename as basename5, dirname as dirname9, join as join58 } from "node:path";
33594
+ import { basename as basename5, dirname as dirname10, join as join59 } from "node:path";
33436
33595
  var COMPANION_TOOLS = ["workerd", "esbuild"];
33437
33596
  function windowsArtifacts(arch = process.arch) {
33438
33597
  if (arch !== "x64") {
@@ -33459,8 +33618,8 @@ function cliDestinations(execPath, canonical, existingLocalBin) {
33459
33618
  }
33460
33619
  function resolveCliDestinations() {
33461
33620
  const canonical = nativeBinaryPath("playcademy");
33462
- const localBin = join58(homedir3(), ".local", "bin", basename5(canonical));
33463
- return cliDestinations(process.execPath, canonical, existsSync40(localBin) ? localBin : null);
33621
+ const localBin = join59(homedir3(), ".local", "bin", basename5(canonical));
33622
+ return cliDestinations(process.execPath, canonical, existsSync41(localBin) ? localBin : null);
33464
33623
  }
33465
33624
  function parseChecksums(text4) {
33466
33625
  const checksums = /* @__PURE__ */ new Map();
@@ -33517,7 +33676,7 @@ async function upgradeWindows(targetVersion, channel) {
33517
33676
  return { remote, expected, dests };
33518
33677
  });
33519
33678
  await Promise.all(
33520
- [...new Set(plans.flatMap((plan) => plan.dests.map((dest) => dirname9(dest))))].map(
33679
+ [...new Set(plans.flatMap((plan) => plan.dests.map((dest) => dirname10(dest))))].map(
33521
33680
  (dir) => mkdir9(dir, { recursive: true })
33522
33681
  )
33523
33682
  );
@@ -33664,6 +33823,8 @@ async function resolveTarget(targetVersion, channelOverride) {
33664
33823
  }
33665
33824
  export {
33666
33825
  ASSET_DEV_ROUTE_PREFIX,
33826
+ CATALOG_PATH,
33827
+ CatalogError,
33667
33828
  ConfigError,
33668
33829
  DEFAULT_DASHBOARD_DIRECTORY,
33669
33830
  DeployBlockedError,
@@ -33691,10 +33852,12 @@ export {
33691
33852
  applySecretsChanges,
33692
33853
  assembleLogicalLevel,
33693
33854
  assertDestructiveConfirmToken,
33855
+ assertExtractorOutput,
33694
33856
  assertRemoteWhenEnvGiven,
33695
33857
  assessmentFixturePath,
33696
33858
  blockDeploy,
33697
33859
  bootstrapDashboardAdmin,
33860
+ buildCatalog,
33698
33861
  buildCollectorUrl,
33699
33862
  buildDatabasePlan,
33700
33863
  buildDatabaseResetPayload,
@@ -33820,6 +33983,7 @@ export {
33820
33983
  executeSeedFile,
33821
33984
  executeUpdates,
33822
33985
  exitAfterStdoutDrain,
33986
+ extractCatalog,
33823
33987
  extractDeployServerError,
33824
33988
  fetchCompatibilityManifest,
33825
33989
  fetchDashboardUrl,
@@ -33956,7 +34120,7 @@ export {
33956
34120
  isPluginConfigured,
33957
34121
  isProcessExitLikeError,
33958
34122
  isPushAdopted,
33959
- isRecord3 as isRecord,
34123
+ isRecord4 as isRecord,
33960
34124
  isResolvableDriftLabel,
33961
34125
  isRetryable,
33962
34126
  isSchemaAdopted,
@@ -34058,6 +34222,7 @@ export {
34058
34222
  refuseRun,
34059
34223
  removeDeployedGame,
34060
34224
  removeProfile,
34225
+ renderCatalog,
34061
34226
  renderDeleteRun,
34062
34227
  renderDeployServerError,
34063
34228
  renderRestoreUnsupported,
@@ -34167,5 +34332,6 @@ export {
34167
34332
  validateQueueKey,
34168
34333
  validateTimebackCourseRequirements,
34169
34334
  withLocalBucket,
34170
- writeAssessmentFixtureBundle
34335
+ writeAssessmentFixtureBundle,
34336
+ writeCatalog
34171
34337
  };
@@ -1,7 +1,7 @@
1
1
  {
2
- "cliVersion": "0.28.1-beta.13",
3
- "sdkVersion": "0.16.1-beta.13",
4
- "runtimeBuildId": "366f48676d4f",
5
- "inputFingerprint": "366f48676d4f59f82245bac7de237150ecf177448b51bd9e048499e3ce8c0e20",
2
+ "cliVersion": "0.28.1-beta.14",
3
+ "sdkVersion": "0.16.1-beta.14",
4
+ "runtimeBuildId": "3ce61e6be9e1",
5
+ "inputFingerprint": "3ce61e6be9e1a0a36a9263642202c3a2d3227b315087e86599fba386a455f421",
6
6
  "entry": "index.js"
7
7
  }
@@ -12056,9 +12056,9 @@ var init_list = __esm({
12056
12056
  // ../edge-play/src/game/lib/metadata.ts
12057
12057
  function getRuntimeMetadata() {
12058
12058
  return {
12059
- cliVersion: true ? "0.28.1-beta.13" : "0.0.0-dev",
12060
- sdkVersion: true ? "0.16.1-beta.13" : "0.0.0-dev",
12061
- buildId: true ? "366f48676d4f" : "dev-source"
12059
+ cliVersion: true ? "0.28.1-beta.14" : "0.0.0-dev",
12060
+ sdkVersion: true ? "0.16.1-beta.14" : "0.0.0-dev",
12061
+ buildId: true ? "3ce61e6be9e1" : "dev-source"
12062
12062
  };
12063
12063
  }
12064
12064
  var init_metadata = __esm({
@@ -1,7 +1,7 @@
1
1
  {
2
- "cliVersion": "0.28.1-beta.13",
3
- "sdkVersion": "0.16.1-beta.13",
4
- "runtimeBuildId": "366f48676d4f",
5
- "inputFingerprint": "366f48676d4f59f82245bac7de237150ecf177448b51bd9e048499e3ce8c0e20",
2
+ "cliVersion": "0.28.1-beta.14",
3
+ "sdkVersion": "0.16.1-beta.14",
4
+ "runtimeBuildId": "3ce61e6be9e1",
5
+ "inputFingerprint": "3ce61e6be9e1a0a36a9263642202c3a2d3227b315087e86599fba386a455f421",
6
6
  "entry": "index.js"
7
7
  }
package/dist/utils.d.ts CHANGED
@@ -141,6 +141,19 @@ interface DashboardThemeConfig {
141
141
  /** Secondary/accent color as a hex string, e.g. '#ffd166' */
142
142
  secondary?: string;
143
143
  }
144
+ /**
145
+ * Child-catalog contract generation (see @playcademy/sdk/contracts).
146
+ * The extractor is a repo-local script (run with the CLI's runtime) that
147
+ * prints `{ generatedFrom, deliveries }` as JSON on stdout; the CLI
148
+ * validates, stamps the contract fields, and writes
149
+ * `.playcademy/catalog.json` deterministically. The document carries no
150
+ * app identity — consuming repos assign the namespace key from sync
151
+ * provenance.
152
+ */
153
+ interface CatalogConfig {
154
+ /** Path to the extractor script, relative to the config file. */
155
+ extractor: string;
156
+ }
144
157
  /**
145
158
  * Unified Playcademy configuration
146
159
  * Used for playcademy.config.{js,json}
@@ -173,6 +186,8 @@ interface PlaycademyConfig {
173
186
  dashboard?: DashboardConfig | boolean;
174
187
  /** Integrations (database, custom routes, external services) */
175
188
  integrations?: IntegrationsConfig;
189
+ /** Child-catalog contract generation (`playcademy catalog`) */
190
+ catalog?: CatalogConfig;
176
191
  }
177
192
 
178
193
  /**
package/dist/utils.js CHANGED
@@ -388,7 +388,7 @@ var DASHBOARD_API_ROUTES_DIRECTORY = "api";
388
388
  // ../better-auth/package.json
389
389
  var package_default = {
390
390
  name: "@playcademy/better-auth",
391
- version: "0.0.21-beta.13",
391
+ version: "0.0.21-beta.14",
392
392
  type: "module",
393
393
  exports: {
394
394
  "./server": {
@@ -828,7 +828,10 @@ function nestColor(content, color2) {
828
828
  function customTransform(text) {
829
829
  let result = text;
830
830
  result = result.replace(/<<([^>]+)>>/g, (_, url) => blueBright(underline(url)));
831
- result = result.replace(/<([^>]+)>/g, (_, inner) => blueBright(`<${inner}>`));
831
+ result = result.replace(
832
+ /<([^>]+)>/g,
833
+ (match, inner) => blueBright(/[./]/.test(inner) ? inner : match)
834
+ );
832
835
  result = result.replace(/`([^`]+)`/g, (_, code) => nestColor(code, greenBright));
833
836
  return result;
834
837
  }
@@ -2831,7 +2834,7 @@ import { existsSync as existsSync9, mkdirSync as mkdirSync2, writeFileSync as wr
2831
2834
  import { dirname as dirname4, join as join14 } from "node:path";
2832
2835
 
2833
2836
  // src/version.ts
2834
- var cliVersion = false ? "0.0.0-dev" : "0.28.1-beta.13";
2837
+ var cliVersion = false ? "0.0.0-dev" : "0.28.1-beta.14";
2835
2838
 
2836
2839
  // src/lib/build/binary-resource.ts
2837
2840
  function writeFileTree(baseDir, files) {
package/dist/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var cliVersion = false ? "0.0.0-dev" : "0.28.1-beta.13";
2
+ var cliVersion = false ? "0.0.0-dev" : "0.28.1-beta.14";
3
3
  export {
4
4
  cliVersion
5
5
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playcademy",
3
- "version": "0.28.1-beta.13",
3
+ "version": "0.28.1-beta.14",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "@inquirer/prompts": "^7.8.6",
58
- "@playcademy/sdk": "0.16.1-beta.13",
58
+ "@playcademy/sdk": "0.16.1-beta.14",
59
59
  "chokidar": "^4.0.3",
60
60
  "colorette": "^2.0.20",
61
61
  "commander": "^14.0.1",