playcademy 0.11.13 → 0.12.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 (30) hide show
  1. package/dist/constants.d.ts +72 -1
  2. package/dist/constants.js +46 -0
  3. package/dist/db.d.ts +61 -0
  4. package/dist/db.js +671 -0
  5. package/dist/edge-play/src/constants.ts +3 -28
  6. package/dist/{types.d.ts → index.d.ts} +118 -14
  7. package/dist/index.js +4650 -6635
  8. package/dist/templates/api/sample-route-with-db.ts.template +141 -0
  9. package/dist/templates/config/playcademy.config.js.template +4 -0
  10. package/dist/templates/config/playcademy.config.json.template +3 -0
  11. package/dist/templates/config/timeback-config.js.template +8 -0
  12. package/dist/templates/database/db-index.ts.template +21 -0
  13. package/dist/templates/database/db-schema-index.ts.template +8 -0
  14. package/dist/templates/database/db-schema-scores.ts.template +43 -0
  15. package/dist/templates/database/db-schema-users.ts.template +23 -0
  16. package/dist/templates/database/db-seed.ts.template +52 -0
  17. package/dist/templates/database/db-types.ts.template +21 -0
  18. package/dist/templates/database/drizzle-config.ts.template +13 -0
  19. package/dist/templates/database/package.json.template +20 -0
  20. package/dist/templates/gitignore.template +17 -0
  21. package/dist/templates/playcademy-gitignore.template +3 -0
  22. package/dist/utils.d.ts +31 -14
  23. package/dist/utils.js +523 -490
  24. package/package.json +18 -2
  25. package/dist/templates/backend-config.js.template +0 -6
  26. package/dist/templates/playcademy.config.js.template +0 -4
  27. package/dist/templates/playcademy.config.json.template +0 -3
  28. package/dist/templates/timeback-config.js.template +0 -17
  29. /package/dist/templates/{sample-route.ts → api/sample-route.ts.template} +0 -0
  30. /package/dist/templates/{integrations-config.js.template → config/integrations-config.js.template} +0 -0
package/dist/utils.js CHANGED
@@ -34,6 +34,7 @@ var init_package_json = __esm({
34
34
  var file_loader_exports = {};
35
35
  __export(file_loader_exports, {
36
36
  findFile: () => findFile,
37
+ findFilesByExtension: () => findFilesByExtension,
37
38
  getCurrentDirectoryName: () => getCurrentDirectoryName,
38
39
  getFileExtension: () => getFileExtension,
39
40
  getPackageField: () => getPackageField,
@@ -178,8 +179,8 @@ function getCurrentDirectoryName(fallback = "unknown-directory") {
178
179
  const dirName = cwd.split("/").pop();
179
180
  return dirName || fallback;
180
181
  }
181
- function getFileExtension(path) {
182
- return path.split(".").pop()?.toLowerCase();
182
+ function getFileExtension(path2) {
183
+ return path2.split(".").pop()?.toLowerCase();
183
184
  }
184
185
  async function loadModule(filename, options = {}) {
185
186
  const { cwd = process.cwd(), required = false, searchUp = false, maxLevels = 3 } = options;
@@ -255,363 +256,32 @@ function scanDirectory(dir, options = {}) {
255
256
  scan(dir);
256
257
  return files;
257
258
  }
258
- var init_file_loader = __esm({
259
- "../utils/src/file-loader.ts"() {
260
- "use strict";
261
- init_package_json();
262
- }
263
- });
264
-
265
- // ../edge-play/src/constants.ts
266
- var TIMEBACK_ROUTES, ROUTES;
267
- var init_constants = __esm({
268
- "../edge-play/src/constants.ts"() {
269
- "use strict";
270
- TIMEBACK_ROUTES = {
271
- END_ACTIVITY: "/integrations/timeback/end-activity"
272
- };
273
- ROUTES = {
274
- /** Route index (lists available routes) */
275
- INDEX: "/api",
276
- /** Health check endpoint */
277
- HEALTH: "/api/health",
278
- /** TimeBack integration routes */
279
- TIMEBACK: {
280
- END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`
281
- }
282
- };
283
- }
284
- });
285
-
286
- // ../edge-play/src/routes/root.html
287
- var root_default;
288
- var init_root = __esm({
289
- "../edge-play/src/routes/root.html"() {
290
- "use strict";
291
- root_default = `<!doctype html>
292
- <html lang="en" class="dark">
293
- <head>
294
- <meta charset="UTF-8" />
295
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
296
- <title>{{GAME_NAME}}</title>
297
- <link rel="preconnect" href="https://fonts.googleapis.com" />
298
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
299
- <link
300
- href="https://fonts.googleapis.com/css2?family=Tomorrow:wght@700&family=VT323&display=swap"
301
- rel="stylesheet"
302
- />
303
- <script src="https://cdn.tailwindcss.com"></script>
304
- <script>
305
- tailwind.config = {
306
- darkMode: 'class',
307
- }
308
- </script>
309
- </head>
310
- <body
311
- class="min-h-screen flex items-center justify-center bg-black dark:bg-white transition-colors relative overflow-hidden"
312
- >
313
- <button
314
- id="themeToggle"
315
- class="fixed top-5 right-5 w-8 h-8 flex items-center justify-center cursor-pointer opacity-40 hover:opacity-100 transition-opacity"
316
- >
317
- <svg
318
- id="sunIcon"
319
- class="w-5 h-5 hidden stroke-white dark:stroke-black"
320
- fill="none"
321
- viewBox="0 0 24 24"
322
- stroke-width="2"
323
- >
324
- <circle cx="12" cy="12" r="4" />
325
- <path
326
- d="M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M4.93 19.07l1.41-1.41m11.32-11.32l1.41-1.41"
327
- />
328
- </svg>
329
- <svg
330
- id="moonIcon"
331
- class="w-5 h-5 stroke-white dark:stroke-black"
332
- fill="none"
333
- viewBox="0 0 24 24"
334
- stroke-width="2"
335
- >
336
- <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
337
- </svg>
338
- </button>
339
-
340
- <!-- Subtle background enhancements: vignette + grid (theme-aware) -->
341
- <div class="pointer-events-none absolute inset-0 z-0">
342
- <!-- Light mode vignette (white on black) -->
343
- <div
344
- class="absolute inset-0 dark:hidden"
345
- style="
346
- background-image: radial-gradient(
347
- 60% 60% at 50% 30%,
348
- rgba(255, 255, 255, 0.1) 0%,
349
- rgba(0, 0, 0, 0) 70%
350
- );
351
- "
352
- ></div>
353
- <!-- Dark mode vignette (black on white) -->
354
- <div
355
- class="absolute inset-0 hidden dark:block"
356
- style="
357
- background-image: radial-gradient(
358
- 60% 60% at 50% 30%,
359
- rgba(0, 0, 0, 0.08) 0%,
360
- rgba(0, 0, 0, 0) 70%
361
- );
362
- "
363
- ></div>
364
- <!-- Light mode grid (white on black) -->
365
- <div
366
- class="absolute inset-0 dark:hidden"
367
- style="
368
- background-image:
369
- linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
370
- linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
371
- background-size: 20px 20px;
372
- "
373
- ></div>
374
- <!-- Dark mode grid (black on white) -->
375
- <div
376
- class="absolute inset-0 hidden dark:block"
377
- style="
378
- background-image:
379
- linear-gradient(rgba(0, 0, 0, 0.02) 1px, transparent 1px),
380
- linear-gradient(90deg, rgba(0, 0, 0, 0.02) 1px, transparent 1px);
381
- background-size: 20px 20px;
382
- "
383
- ></div>
384
- </div>
385
-
386
- <div class="max-w-2xl mx-auto px-6 py-12 relative z-10">
387
- <div class="text-center space-y-8">
388
- <div>
389
- <h1
390
- class="text-4xl md:text-5xl font-bold text-white dark:text-black mb-3"
391
- style="font-family: 'Tomorrow', sans-serif; letter-spacing: -0.02em"
392
- >
393
- {{GAME_NAME}}
394
- </h1>
395
- <p
396
- class="text-lg text-gray-500 dark:text-gray-500"
397
- style="font-family: 'VT323', monospace; letter-spacing: 0.1em"
398
- >
399
- GAME BACKEND API
400
- </p>
401
- </div>
402
-
403
- <a
404
- href="/api"
405
- class="inline-block px-8 py-2.5 bg-white dark:bg-black text-black dark:text-white rounded border-2 border-white dark:border-black hover:bg-gray-200 dark:hover:bg-gray-800 transition-colors"
406
- style="font-family: 'VT323', monospace; letter-spacing: 0.05em; font-size: 1rem"
407
- >
408
- VIEW ROUTES \u2192
409
- </a>
410
-
411
- <div
412
- class="pt-8 text-xs text-gray-600 dark:text-gray-500"
413
- style="font-family: 'VT323', monospace; letter-spacing: 0.05em"
414
- >
415
- POWERED BY
416
- <a
417
- href="{{PLAYCADEMY_HUB_URL}}"
418
- target="_blank"
419
- rel="noopener noreferrer"
420
- class="font-bold hover:text-gray-500 dark:hover:text-gray-600 transition-colors underline decoration-dotted underline-offset-2"
421
- >PLAYCADEMY</a
422
- >
423
- </div>
424
- </div>
425
- </div>
426
-
427
- <script>
428
- const html = document.documentElement
429
- const toggle = document.getElementById('themeToggle')
430
- const sun = document.getElementById('sunIcon')
431
- const moon = document.getElementById('moonIcon')
432
-
433
- function setTheme(isDark) {
434
- if (isDark) {
435
- html.classList.add('dark')
436
- sun.classList.remove('hidden')
437
- moon.classList.add('hidden')
438
- } else {
439
- html.classList.remove('dark')
440
- sun.classList.add('hidden')
441
- moon.classList.remove('hidden')
442
- }
443
- }
444
-
445
- // Initialize theme from localStorage or system preference
446
- const savedTheme = localStorage.getItem('theme')
447
- const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
448
- const isDark = savedTheme === 'dark' || (!savedTheme && prefersDark)
449
- setTheme(isDark)
450
-
451
- // Toggle on click
452
- toggle.addEventListener('click', () => {
453
- const willBeDark = !html.classList.contains('dark')
454
- setTheme(willBeDark)
455
- localStorage.setItem('theme', willBeDark ? 'dark' : 'light')
456
- })
457
- </script>
458
- </body>
459
- </html>
460
- `;
461
- }
462
- });
463
-
464
- // ../edge-play/src/routes/root.ts
465
- var root_exports = {};
466
- __export(root_exports, {
467
- GET: () => GET
468
- });
469
- async function GET(c) {
470
- const config = c.get("config");
471
- const html = root_default.toString().replace(/\{\{GAME_NAME\}\}/g, config.name).replace(/\{\{PLAYCADEMY_HUB_URL\}\}/g, c.env.PLAYCADEMY_BASE_URL);
472
- return c.html(html);
473
- }
474
- var init_root2 = __esm({
475
- "../edge-play/src/routes/root.ts"() {
476
- "use strict";
477
- init_root();
478
- }
479
- });
480
-
481
- // ../edge-play/src/routes/index.ts
482
- var routes_exports = {};
483
- __export(routes_exports, {
484
- GET: () => GET2
485
- });
486
- async function GET2(c) {
487
- const config = c.get("config");
488
- const customRoutes = c.get("customRoutes") || [];
489
- const routes = [`GET ${ROUTES.INDEX}`, `GET ${ROUTES.HEALTH}`];
490
- if (config.integrations?.timeback) {
491
- routes.push(`POST ${ROUTES.TIMEBACK.END_ACTIVITY}`);
492
- }
493
- for (const route of customRoutes) {
494
- const methods = route.methods?.join(", ") || "*";
495
- routes.push(`${methods} ${route.path}`);
496
- }
497
- return c.json({
498
- name: config.name,
499
- routes
500
- });
501
- }
502
- var init_routes = __esm({
503
- "../edge-play/src/routes/index.ts"() {
504
- "use strict";
505
- init_constants();
506
- }
507
- });
508
-
509
- // ../edge-play/src/routes/health.ts
510
- var health_exports = {};
511
- __export(health_exports, {
512
- GET: () => GET3
513
- });
514
- async function GET3(c) {
515
- const config = c.get("config");
516
- const sdk = c.get("sdk");
517
- return c.json({
518
- status: "ok",
519
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
520
- // Environment check
521
- env: {
522
- hasApiKey: !!c.env.PLAYCADEMY_API_KEY,
523
- hasGameId: !!c.env.GAME_ID,
524
- hasBaseUrl: !!c.env.PLAYCADEMY_BASE_URL
525
- },
526
- // Config presence
527
- config: {
528
- hasConfig: !!config,
529
- hasIntegrations: !!config?.integrations
530
- },
531
- // TimeBack status
532
- timeback: {
533
- enabled: !!config?.integrations?.timeback,
534
- courseIdFetched: !!sdk?.timeback?.courseId
535
- },
536
- // Custom routes info
537
- customRoutes: c.get("customRoutes")?.map((r) => ({
538
- path: r.path,
539
- methods: r.methods
540
- })) || []
541
- });
542
- }
543
- var init_health = __esm({
544
- "../edge-play/src/routes/health.ts"() {
545
- "use strict";
259
+ function findFilesByExtension(dir, extension) {
260
+ if (!existsSync(dir)) {
261
+ return [];
546
262
  }
547
- });
548
-
549
- // ../edge-play/src/routes/integrations/timeback/end-activity.ts
550
- var end_activity_exports = {};
551
- __export(end_activity_exports, {
552
- POST: () => POST
553
- });
554
- import { verifyGameToken } from "@playcademy/sdk/server";
555
- function getConfig(c) {
556
- const config = c.get("config");
557
- const timebackConfig = config?.integrations?.timeback;
558
- if (!timebackConfig) throw new Error("TimeBack integration not found");
559
- return config;
560
- }
561
- function enrichActivityData(activityData, config, c) {
562
- const appName = activityData.appName || config?.name;
563
- const subject = activityData.subject || config?.integrations?.timeback?.course?.defaultSubject || config?.integrations?.timeback?.course?.subjects?.[0];
564
- const sensorUrl = activityData.sensorUrl || new URL(c.req.url).origin;
565
- if (!appName) throw new Error("App name is required");
566
- if (!subject) throw new Error("Subject is required");
567
- if (!sensorUrl) throw new Error("Sensor URL is required");
568
- return { ...activityData, appName, subject, sensorUrl };
569
- }
570
- async function POST(c) {
263
+ const ext = extension.toLowerCase().replace(/^\./, "");
264
+ const files = [];
571
265
  try {
572
- const token = c.req.header("Authorization")?.replace("Bearer ", "");
573
- if (!token) {
574
- return c.json({ error: "Unauthorized" }, 401);
575
- }
576
- const { user } = await verifyGameToken(token, { baseUrl: c.env.PLAYCADEMY_BASE_URL });
577
- if (!user.timeback_id) {
578
- return c.json({ error: "User does not have TimeBack integration" }, 400);
579
- }
580
- const { activityData, scoreData, timingData, xpEarned } = await c.req.json();
581
- if (!activityData?.activityId) {
582
- return c.json({ error: "activityId is required" }, 400);
583
- }
584
- if (typeof scoreData?.correctQuestions !== "number" || typeof scoreData?.totalQuestions !== "number") {
585
- return c.json({ error: "correctQuestions and totalQuestions are required" }, 400);
586
- }
587
- if (typeof timingData?.durationSeconds !== "number") {
588
- return c.json({ error: "durationSeconds is required" }, 400);
266
+ const entries = readdirSync(dir);
267
+ for (const entry of entries) {
268
+ const fullPath = resolve(dir, entry);
269
+ const stat = statSync(fullPath);
270
+ if (stat.isFile()) {
271
+ const fileExt = getFileExtension(entry);
272
+ if (fileExt === ext) {
273
+ files.push(fullPath);
274
+ }
275
+ }
589
276
  }
590
- const config = getConfig(c);
591
- const enrichedActivityData = enrichActivityData(activityData, config, c);
592
- const sdk = c.get("sdk");
593
- const result = await sdk.timeback.endActivity(user.timeback_id, {
594
- activityData: enrichedActivityData,
595
- scoreData,
596
- timingData,
597
- xpEarned
598
- });
599
- return c.json(result);
600
- } catch (error) {
601
- console.error("[TimeBack End Activity] Error:", error);
602
- return c.json(
603
- {
604
- error: "Failed to end activity",
605
- message: error instanceof Error ? error.message : String(error),
606
- stack: error instanceof Error ? error.stack : void 0
607
- },
608
- 500
609
- );
277
+ } catch {
610
278
  }
279
+ return files;
611
280
  }
612
- var init_end_activity = __esm({
613
- "../edge-play/src/routes/integrations/timeback/end-activity.ts"() {
281
+ var init_file_loader = __esm({
282
+ "../utils/src/file-loader.ts"() {
614
283
  "use strict";
284
+ init_package_json();
615
285
  }
616
286
  });
617
287
 
@@ -619,6 +289,27 @@ var init_end_activity = __esm({
619
289
  init_file_loader();
620
290
  import { dirname as dirname2, resolve as resolve2 } from "path";
621
291
 
292
+ // src/constants/api.ts
293
+ var DEFAULT_API_ROUTES_DIRECTORY = "server/api";
294
+
295
+ // src/constants/paths.ts
296
+ var CLI_DIRECTORIES = {
297
+ /** Root directory for CLI artifacts in workspace */
298
+ WORKSPACE: ".playcademy",
299
+ /** Database directory within workspace */
300
+ DATABASE: ".playcademy/db"
301
+ };
302
+ var CLI_FILES = {
303
+ /** Auth store file in user config directory */
304
+ AUTH_STORE: "auth.json",
305
+ /** Games deployment info store */
306
+ GAMES_STORE: "games.json",
307
+ /** Dev server PID file */
308
+ DEV_SERVER_PID: "dev-server.pid",
309
+ /** Initial database file (before miniflare) */
310
+ INITIAL_DATABASE: "initial.sqlite"
311
+ };
312
+
622
313
  // src/constants/timeback.ts
623
314
  var CONFIG_FILE_NAMES = [
624
315
  "playcademy.config.js",
@@ -661,6 +352,9 @@ var CORE_GAME_UUIDS = {
661
352
  PLAYGROUND: "00000000-0000-0000-0000-000000000001"
662
353
  };
663
354
 
355
+ // src/constants/index.ts
356
+ var CLOUDFLARE_COMPATIBILITY_DATE = "2024-01-01";
357
+
664
358
  // src/lib/config/loader.ts
665
359
  var ConfigError = class extends Error {
666
360
  constructor(message, field, suggestion) {
@@ -725,27 +419,17 @@ async function findConfigPath(configPath) {
725
419
  }
726
420
  return result.path;
727
421
  }
422
+ async function loadConfigFile(path2) {
423
+ if (path2.endsWith(".json")) {
424
+ return await loadFile(path2, { required: true, parseJson: true });
425
+ }
426
+ const module = await import(`${path2}?t=${Date.now()}`);
427
+ return module.default || module;
428
+ }
728
429
  async function loadConfig(configPath) {
729
430
  try {
730
- let config;
731
- let actualPath;
732
- if (configPath) {
733
- actualPath = resolve2(configPath);
734
- if (actualPath.endsWith(".json")) {
735
- config = await loadFile(actualPath, { required: true, parseJson: true });
736
- } else {
737
- const module = await import(`${actualPath}?t=${Date.now()}`);
738
- config = module.default || module;
739
- }
740
- } else {
741
- actualPath = await findConfigPath();
742
- if (actualPath.endsWith(".json")) {
743
- config = await loadFile(actualPath, { required: true, parseJson: true });
744
- } else {
745
- const module = await import(`${actualPath}?t=${Date.now()}`);
746
- config = module.default || module;
747
- }
748
- }
431
+ const actualPath = configPath ? resolve2(configPath) : await findConfigPath();
432
+ const config = await loadConfigFile(actualPath);
749
433
  if (!config || typeof config !== "object") {
750
434
  throw new ConfigError(
751
435
  "Config file must export or contain a valid configuration object",
@@ -879,35 +563,9 @@ function processConfigVariables(config) {
879
563
  }
880
564
 
881
565
  // src/lib/dev/server.ts
882
- import { join as join2 } from "path";
883
- import { serve } from "@hono/node-server";
884
- import { Hono } from "hono";
885
- import { cors } from "hono/cors";
886
- import { logger as honoLogger } from "hono/logger";
887
-
888
- // ../edge-play/src/register-routes.ts
889
- init_constants();
890
- async function registerBuiltinRoutes(app, integrations) {
891
- const root = await Promise.resolve().then(() => (init_root2(), root_exports));
892
- app.get("/", root.GET);
893
- const routesIndex = await Promise.resolve().then(() => (init_routes(), routes_exports));
894
- app.get(ROUTES.INDEX, routesIndex.GET);
895
- const health = await Promise.resolve().then(() => (init_health(), health_exports));
896
- app.get(ROUTES.HEALTH, health.GET);
897
- if (integrations?.timeback) {
898
- const [endActivity] = await Promise.all([
899
- Promise.resolve().then(() => (init_end_activity(), end_activity_exports))
900
- // ... other routes
901
- ]);
902
- app.post(ROUTES.TIMEBACK.END_ACTIVITY, endActivity.POST);
903
- }
904
- }
905
-
906
- // ../edge-play/src/index.ts
907
- init_constants();
908
-
909
- // src/lib/dev/server.ts
910
- import { PlaycademyClient as PlaycademyClient2 } from "@playcademy/sdk/server";
566
+ import { mkdir as mkdir3 } from "fs/promises";
567
+ import { join as join4 } from "path";
568
+ import { Miniflare } from "miniflare";
911
569
 
912
570
  // src/lib/core/client.ts
913
571
  import { PlaycademyClient } from "@playcademy/sdk";
@@ -1121,6 +779,16 @@ var logger = {
1121
779
  }
1122
780
  };
1123
781
 
782
+ // src/lib/core/errors.ts
783
+ function getErrorMessage(error) {
784
+ if (error instanceof Error) return error.message;
785
+ if (typeof error === "string") return error;
786
+ if (error && typeof error === "object" && "message" in error) {
787
+ return String(error.message);
788
+ }
789
+ return "Unknown error";
790
+ }
791
+
1124
792
  // ../utils/src/ansi.ts
1125
793
  var isInteractive = typeof process !== "undefined" && process.stdout?.isTTY && !process.env.CI && process.env.TERM !== "dumb";
1126
794
 
@@ -1148,11 +816,275 @@ import { dirname as dirname3, resolve as resolve3 } from "path";
1148
816
  import { fileURLToPath } from "url";
1149
817
  var currentDir = dirname3(fileURLToPath(import.meta.url));
1150
818
 
819
+ // src/lib/deploy/bundle.ts
820
+ import { existsSync as existsSync2 } from "fs";
821
+ import { join as join2 } from "path";
822
+
823
+ // ../edge-play/src/entry.ts
824
+ var entry_default = "/**\n * Game Backend Entry Point\n *\n * This file is the main entry point for deployed game backends.\n * It creates a Hono app and registers all enabled integration routes.\n *\n * Bundled with esbuild and deployed to Cloudflare Workers (or AWS Lambda).\n * Config is injected at build time via esbuild's `define` option.\n */\n\nimport { Hono } from 'hono'\nimport { cors } from 'hono/cors'\n\nimport { PlaycademyClient } from '@playcademy/sdk/server'\n\nimport { ENV_VARS } from './constants'\nimport { registerBuiltinRoutes } from './register-routes'\n\nimport type { PlaycademyConfig } from '@playcademy/sdk/server'\nimport type { HonoEnv } from './types'\n\n/**\n * Config injected at build time by esbuild\n *\n * The `declare const` tells TypeScript \"this exists at runtime, trust me.\"\n * During bundling, esbuild's `define` option does literal text replacement:\n *\n * Example bundling:\n * Source: if (PLAYCADEMY_CONFIG.integrations.timeback) { ... }\n * Define: { 'PLAYCADEMY_CONFIG': JSON.stringify({ integrations: { timeback: {...} } }) }\n * Output: if ({\"integrations\":{\"timeback\":{...}}}.integrations.timeback) { ... }\n *\n * This enables tree-shaking: if timeback is not configured, those code paths are removed.\n * The bundled Worker only includes the routes that are actually enabled.\n */\ndeclare const PLAYCADEMY_CONFIG: PlaycademyConfig & {\n customRoutes?: Array<{ path: string; file: string }>\n}\n\n// XXX: Polyfill process global for SDK compatibility\n// SDK code may reference process.env without importing it\n// @ts-expect-error - Adding global for Worker environment\nglobalThis.process = {\n env: {}, // Populated per-request from Worker env bindings\n cwd: () => '/',\n}\n\nconst app = new Hono<HonoEnv>()\n\n// TODO: Harden CORS in production - restrict to trusted origins:\n// - Game's assetBundleBase (for hosted games)\n// - Game's externalUrl (for external games)\n// - Platform frontend domains (hub.playcademy.com, hub.dev.playcademy.net)\n// This would require passing game metadata through env bindings during deployment\napp.use(\n '*',\n cors({\n origin: '*', // Permissive for now\n allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],\n allowHeaders: ['Content-Type', 'Authorization'],\n }),\n)\n\nlet sdkPromise: Promise<PlaycademyClient> | null = null\n\napp.use('*', async (c, next) => {\n // Populate process.env from Worker bindings for SDK compatibility\n globalThis.process.env = {\n [ENV_VARS.PLAYCADEMY_API_KEY]: c.env.PLAYCADEMY_API_KEY,\n [ENV_VARS.GAME_ID]: c.env.GAME_ID,\n [ENV_VARS.PLAYCADEMY_BASE_URL]: c.env.PLAYCADEMY_BASE_URL,\n }\n\n // Set config for all routes\n c.set('config', PLAYCADEMY_CONFIG)\n c.set('customRoutes', PLAYCADEMY_CONFIG.customRoutes || [])\n\n await next()\n})\n\n// Initialize SDK lazily on first request\napp.use('*', async (c, next) => {\n if (!sdkPromise) {\n sdkPromise = PlaycademyClient.init({\n apiKey: c.env[ENV_VARS.PLAYCADEMY_API_KEY],\n gameId: c.env[ENV_VARS.GAME_ID],\n baseUrl: c.env[ENV_VARS.PLAYCADEMY_BASE_URL],\n config: PLAYCADEMY_CONFIG,\n })\n }\n\n c.set('sdk', await sdkPromise)\n await next()\n})\n\n/**\n * Register built-in integration routes based on enabled integrations\n *\n * This function conditionally imports and registers routes like:\n * - POST /api/integrations/timeback/end-activity (if timeback enabled)\n * - GET /api/health (always included)\n *\n * Uses dynamic imports for tree-shaking: if an integration is not enabled,\n * its route code is completely removed from the bundle.\n */\nawait registerBuiltinRoutes(app, PLAYCADEMY_CONFIG.integrations)\n\nexport default app\n";
825
+
826
+ // ../utils/src/path.ts
827
+ import fs from "node:fs";
828
+ import path from "node:path";
829
+
830
+ // ../logger/src/index.ts
831
+ var isBrowser = () => {
832
+ const g = globalThis;
833
+ return typeof g.window !== "undefined" && typeof g.document !== "undefined";
834
+ };
835
+ var isProduction = () => {
836
+ return typeof process !== "undefined" && process.env.NODE_ENV === "production";
837
+ };
838
+ var isDevelopment = () => {
839
+ return typeof process !== "undefined" && process.env.NODE_ENV === "development";
840
+ };
841
+ var isInteractiveTTY = () => {
842
+ return typeof process !== "undefined" && Boolean(process.stdout && process.stdout.isTTY);
843
+ };
844
+ var detectOutputFormat = () => {
845
+ if (isBrowser()) {
846
+ return "browser";
847
+ }
848
+ if (typeof process !== "undefined" && process.env.LOG_FORMAT === "json") {
849
+ return "json-single-line";
850
+ }
851
+ if (typeof process !== "undefined" && process.env.LOG_PRETTY === "true" && isDevelopment()) {
852
+ return "json-pretty";
853
+ }
854
+ const colorPreference = typeof process !== "undefined" ? (process.env.LOG_COLOR ?? "auto").toLowerCase() : "auto";
855
+ if (colorPreference === "always" && !isProduction()) {
856
+ return "color-tty";
857
+ }
858
+ if (colorPreference === "never") {
859
+ return "json-single-line";
860
+ }
861
+ if (isProduction()) {
862
+ return "json-single-line";
863
+ }
864
+ if (isDevelopment() && isInteractiveTTY()) {
865
+ return "color-tty";
866
+ }
867
+ return "json-single-line";
868
+ };
869
+ var colors2 = {
870
+ reset: "\x1B[0m",
871
+ dim: "\x1B[2m",
872
+ red: "\x1B[31m",
873
+ yellow: "\x1B[33m",
874
+ blue: "\x1B[34m",
875
+ cyan: "\x1B[36m",
876
+ gray: "\x1B[90m"
877
+ };
878
+ var getLevelColor = (level) => {
879
+ switch (level) {
880
+ case "debug":
881
+ return colors2.blue;
882
+ case "info":
883
+ return colors2.cyan;
884
+ case "warn":
885
+ return colors2.yellow;
886
+ case "error":
887
+ return colors2.red;
888
+ default:
889
+ return colors2.reset;
890
+ }
891
+ };
892
+ var formatBrowserOutput = (level, message, context2) => {
893
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
894
+ const levelUpper = level.toUpperCase();
895
+ const consoleMethod = getConsoleMethod(level);
896
+ if (context2 && Object.keys(context2).length > 0) {
897
+ consoleMethod(`[${timestamp}] ${levelUpper}`, message, context2);
898
+ } else {
899
+ consoleMethod(`[${timestamp}] ${levelUpper}`, message);
900
+ }
901
+ };
902
+ var formatColorTTY = (level, message, context2) => {
903
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
904
+ const levelColor = getLevelColor(level);
905
+ const levelUpper = level.toUpperCase().padEnd(5);
906
+ const consoleMethod = getConsoleMethod(level);
907
+ const coloredPrefix = `${colors2.dim}[${timestamp}]${colors2.reset} ${levelColor}${levelUpper}${colors2.reset}`;
908
+ if (context2 && Object.keys(context2).length > 0) {
909
+ consoleMethod(`${coloredPrefix} ${message}`, context2);
910
+ } else {
911
+ consoleMethod(`${coloredPrefix} ${message}`);
912
+ }
913
+ };
914
+ var formatJSONSingleLine = (level, message, context2) => {
915
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
916
+ const logEntry = {
917
+ timestamp,
918
+ level: level.toUpperCase(),
919
+ message,
920
+ ...context2 && Object.keys(context2).length > 0 && { context: context2 }
921
+ };
922
+ const consoleMethod = getConsoleMethod(level);
923
+ consoleMethod(JSON.stringify(logEntry));
924
+ };
925
+ var formatJSONPretty = (level, message, context2) => {
926
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
927
+ const logEntry = {
928
+ timestamp,
929
+ level: level.toUpperCase(),
930
+ message,
931
+ ...context2 && Object.keys(context2).length > 0 && { context: context2 }
932
+ };
933
+ const consoleMethod = getConsoleMethod(level);
934
+ consoleMethod(JSON.stringify(logEntry, null, 2));
935
+ };
936
+ var getConsoleMethod = (level) => {
937
+ switch (level) {
938
+ case "debug":
939
+ return console.debug;
940
+ case "info":
941
+ return console.info;
942
+ case "warn":
943
+ return console.warn;
944
+ case "error":
945
+ return console.error;
946
+ default:
947
+ return console.log;
948
+ }
949
+ };
950
+ var levelPriority = {
951
+ debug: 0,
952
+ info: 1,
953
+ warn: 2,
954
+ error: 3
955
+ };
956
+ var getMinimumLogLevel = () => {
957
+ const envLevel = typeof process !== "undefined" ? (process.env.LOG_LEVEL ?? "").toLowerCase() : "";
958
+ if (envLevel && ["debug", "info", "warn", "error"].includes(envLevel)) {
959
+ return envLevel;
960
+ }
961
+ return isProduction() ? "info" : "debug";
962
+ };
963
+ var shouldLog = (level) => {
964
+ const minLevel = getMinimumLogLevel();
965
+ return levelPriority[level] >= levelPriority[minLevel];
966
+ };
967
+ var performLog = (level, message, context2) => {
968
+ if (!shouldLog(level)) return;
969
+ const outputFormat = detectOutputFormat();
970
+ switch (outputFormat) {
971
+ case "browser":
972
+ formatBrowserOutput(level, message, context2);
973
+ break;
974
+ case "color-tty":
975
+ formatColorTTY(level, message, context2);
976
+ break;
977
+ case "json-single-line":
978
+ formatJSONSingleLine(level, message, context2);
979
+ break;
980
+ case "json-pretty":
981
+ formatJSONPretty(level, message, context2);
982
+ break;
983
+ }
984
+ };
985
+ var createLogger = () => {
986
+ return {
987
+ debug: (message, context2) => performLog("debug", message, context2),
988
+ info: (message, context2) => performLog("info", message, context2),
989
+ warn: (message, context2) => performLog("warn", message, context2),
990
+ error: (message, context2) => performLog("error", message, context2),
991
+ log: performLog
992
+ };
993
+ };
994
+ var log = createLogger();
995
+
996
+ // ../utils/src/path.ts
997
+ function findMonorepoRootInternal(startDir) {
998
+ let currentDir2 = startDir;
999
+ while (true) {
1000
+ const packageJsonPath = path.join(currentDir2, "package.json");
1001
+ if (fs.existsSync(packageJsonPath)) {
1002
+ try {
1003
+ const packageJsonContent = fs.readFileSync(packageJsonPath, "utf-8");
1004
+ const packageJson = JSON.parse(packageJsonContent);
1005
+ if (packageJson && typeof packageJson === "object" && "workspaces" in packageJson) {
1006
+ return currentDir2;
1007
+ }
1008
+ } catch (parseError) {
1009
+ log.warn(`[utils/paths] Error parsing ${packageJsonPath}:`, {
1010
+ error: parseError
1011
+ });
1012
+ }
1013
+ }
1014
+ const parentDir = path.dirname(currentDir2);
1015
+ if (parentDir === currentDir2) {
1016
+ throw new Error(
1017
+ "Could not find monorepo root (package.json with workspaces) starting from " + startDir
1018
+ );
1019
+ }
1020
+ currentDir2 = parentDir;
1021
+ }
1022
+ }
1023
+ var memoizedRoot;
1024
+ function getMonorepoRoot() {
1025
+ if (memoizedRoot) return memoizedRoot;
1026
+ try {
1027
+ memoizedRoot = findMonorepoRootInternal(process.cwd());
1028
+ } catch (error) {
1029
+ memoizedRoot = process.cwd();
1030
+ if (typeof Bun !== "undefined") {
1031
+ log.warn(
1032
+ "[utils/paths] Could not locate monorepo root via workspace package.json scan. Falling back to process.cwd() (",
1033
+ { root: memoizedRoot, error }
1034
+ );
1035
+ }
1036
+ }
1037
+ return memoizedRoot;
1038
+ }
1039
+
1040
+ // src/lib/build/plugins.ts
1041
+ function textLoaderPlugin() {
1042
+ return {
1043
+ name: "text-loader",
1044
+ setup(build) {
1045
+ build.onLoad({ filter: /edge-play\/src\/entry\.ts$/ }, async (args) => {
1046
+ const fs2 = await import("fs/promises");
1047
+ const text = await fs2.readFile(args.path, "utf8");
1048
+ return {
1049
+ contents: `export default ${JSON.stringify(text)}`,
1050
+ loader: "js"
1051
+ };
1052
+ });
1053
+ build.onLoad({ filter: /edge-play\/src\/routes\/root\.html$/ }, async (args) => {
1054
+ const fs2 = await import("fs/promises");
1055
+ const text = await fs2.readFile(args.path, "utf8");
1056
+ return {
1057
+ contents: `export default ${JSON.stringify(text)}`,
1058
+ loader: "js"
1059
+ };
1060
+ });
1061
+ build.onLoad({ filter: /templates\/sample-route\.ts$/ }, async (args) => {
1062
+ const fs2 = await import("fs/promises");
1063
+ const text = await fs2.readFile(args.path, "utf8");
1064
+ return {
1065
+ contents: `export default ${JSON.stringify(text)}`,
1066
+ loader: "js"
1067
+ };
1068
+ });
1069
+ }
1070
+ };
1071
+ }
1072
+
1151
1073
  // src/lib/dev/routes.ts
1152
1074
  init_file_loader();
1153
1075
  import { mkdir, writeFile } from "fs/promises";
1154
1076
  import { tmpdir } from "os";
1155
1077
  import { join, relative } from "path";
1078
+
1079
+ // src/lib/deploy/hash.ts
1080
+ import { createHash } from "crypto";
1081
+ init_file_loader();
1082
+ function hashContent(content) {
1083
+ const contentStr = typeof content === "string" ? content : JSON.stringify(content);
1084
+ return createHash("sha256").update(contentStr).digest("hex");
1085
+ }
1086
+
1087
+ // src/lib/dev/routes.ts
1156
1088
  async function discoverRoutes(apiDir) {
1157
1089
  const files = scanDirectory(apiDir, {
1158
1090
  extensions: ["ts", "js"],
@@ -1202,13 +1134,6 @@ function filePathToRoutePath(filePath) {
1202
1134
  function isBun() {
1203
1135
  return typeof Bun !== "undefined";
1204
1136
  }
1205
- function hashPath(path) {
1206
- let hash = 5381;
1207
- for (let i = 0; i < path.length; i++) {
1208
- hash = hash * 33 ^ path.charCodeAt(i);
1209
- }
1210
- return (hash >>> 0).toString(36);
1211
- }
1212
1137
  async function transpileRoute(filePath) {
1213
1138
  if (isBun() || !filePath.endsWith(".ts")) {
1214
1139
  return filePath;
@@ -1230,29 +1155,139 @@ async function transpileRoute(filePath) {
1230
1155
  }
1231
1156
  const tempDir = join(tmpdir(), "playcademy-dev");
1232
1157
  await mkdir(tempDir, { recursive: true });
1233
- const hash = hashPath(filePath);
1158
+ const hash = hashContent(filePath).slice(0, 12);
1234
1159
  const jsPath = join(tempDir, `${hash}.mjs`);
1235
1160
  await writeFile(jsPath, result.outputFiles[0].text);
1236
1161
  return jsPath;
1237
1162
  }
1238
- async function registerCustomRoutes(app, routes) {
1239
- for (const route of routes) {
1240
- try {
1241
- const importPath = await transpileRoute(route.absolutePath);
1242
- const handler = await import(`${importPath}?t=${Date.now()}`);
1243
- if (handler.GET) app.get(route.path, handler.GET);
1244
- if (handler.POST) app.post(route.path, handler.POST);
1245
- if (handler.PUT) app.put(route.path, handler.PUT);
1246
- if (handler.PATCH) app.patch(route.path, handler.PATCH);
1247
- if (handler.DELETE) app.delete(route.path, handler.DELETE);
1248
- } catch (error) {
1249
- logger.newLine();
1250
- logger.error(
1251
- `Failed to load route ${route.file}: ${error instanceof Error ? error.message : String(error)}`
1252
- );
1253
- logger.newLine();
1254
- }
1163
+
1164
+ // src/lib/deploy/bundle.ts
1165
+ var entryTemplate = entry_default;
1166
+ async function bundleBackend(config, options = {}) {
1167
+ const esbuild = await import("esbuild");
1168
+ const workspace = getWorkspace();
1169
+ const customRoutesConfig = config.integrations?.customRoutes;
1170
+ const customRoutesDir = typeof customRoutesConfig === "object" && customRoutesConfig.directory || DEFAULT_API_ROUTES_DIRECTORY;
1171
+ const customRoutes = await discoverRoutes(join2(workspace, customRoutesDir));
1172
+ const customRouteData = customRoutes.map((r) => ({
1173
+ path: r.path,
1174
+ file: r.file,
1175
+ // Use relative path (e.g., 'server/api/test.ts'), not absolute
1176
+ methods: r.methods
1177
+ }));
1178
+ const bundleConfig = {
1179
+ ...config,
1180
+ customRoutes: customRouteData
1181
+ };
1182
+ const entryCode = generateEntryCode(customRouteData, customRoutesDir);
1183
+ const distDir = new URL(".", import.meta.url).pathname;
1184
+ const embeddedEdgeSrc = join2(distDir, "edge-play", "src");
1185
+ const monorepoRoot = getMonorepoRoot();
1186
+ const monorepoEdgeSrc = join2(monorepoRoot, "packages/edge-play/src");
1187
+ const isBuiltPackage = existsSync2(embeddedEdgeSrc);
1188
+ const edgePlaySrc = isBuiltPackage ? embeddedEdgeSrc : monorepoEdgeSrc;
1189
+ const cliPackageRoot = isBuiltPackage ? join2(distDir, "../..") : join2(monorepoRoot, "packages/cli");
1190
+ const cliNodeModules = isBuiltPackage ? cliPackageRoot : monorepoRoot;
1191
+ const workspaceNodeModules = join2(workspace, "node_modules");
1192
+ const result = await esbuild.build({
1193
+ stdin: {
1194
+ contents: entryCode,
1195
+ resolveDir: edgePlaySrc,
1196
+ // For relative imports like ./register-routes
1197
+ loader: "ts"
1198
+ },
1199
+ bundle: true,
1200
+ format: "esm",
1201
+ platform: "browser",
1202
+ target: "es2022",
1203
+ write: false,
1204
+ sourcemap: options.sourcemap ? "inline" : false,
1205
+ minify: options.minify || false,
1206
+ logLevel: "error",
1207
+ nodePaths: [workspaceNodeModules, cliNodeModules],
1208
+ // Check workspace first, then CLI
1209
+ define: {
1210
+ PLAYCADEMY_CONFIG: JSON.stringify(bundleConfig)
1211
+ },
1212
+ alias: {
1213
+ /**
1214
+ * @game-api alias maps to the user's custom routes directory
1215
+ *
1216
+ * This allows custom route imports in the entry code:
1217
+ * import * as customRoute0 from '@game-api/hello.ts'
1218
+ *
1219
+ * The alias resolves to the absolute path of the custom routes directory in the
1220
+ * user's game project (configured via integrations.customRoutes.directory), enabling esbuild
1221
+ * to bundle custom routes into the worker.
1222
+ */
1223
+ "@game-api": join2(workspace, customRoutesDir),
1224
+ /**
1225
+ * Node.js module polyfills for Cloudflare Workers environment
1226
+ *
1227
+ * Cloudflare Workers don't have Node.js APIs (fs, path, os, etc.).
1228
+ * These aliases redirect Node.js imports to a polyfill that throws helpful errors.
1229
+ *
1230
+ * This prevents bundling errors and provides clear runtime messages if
1231
+ * user code accidentally imports Node.js modules that won't work in Workers.
1232
+ */
1233
+ fs: join2(edgePlaySrc, "polyfills.js"),
1234
+ "fs/promises": join2(edgePlaySrc, "polyfills.js"),
1235
+ path: join2(edgePlaySrc, "polyfills.js"),
1236
+ os: join2(edgePlaySrc, "polyfills.js"),
1237
+ process: join2(edgePlaySrc, "polyfills.js")
1238
+ },
1239
+ plugins: [textLoaderPlugin()],
1240
+ external: []
1241
+ });
1242
+ if (!result.outputFiles?.[0]) {
1243
+ throw new Error("Backend bundling failed: no output");
1244
+ }
1245
+ const code = result.outputFiles[0].text;
1246
+ return {
1247
+ code,
1248
+ config,
1249
+ customRoutes: customRouteData
1250
+ };
1251
+ }
1252
+ function generateEntryCode(customRoutes, customRoutesDir) {
1253
+ if (customRoutes.length === 0) {
1254
+ return entryTemplate;
1255
1255
  }
1256
+ const customRoutesPrefix = customRoutesDir.endsWith("/") ? customRoutesDir : `${customRoutesDir}/`;
1257
+ const importStatements = customRoutes.map((route, i) => {
1258
+ const importPath = route.file.startsWith(customRoutesPrefix) ? route.file.slice(customRoutesPrefix.length) : route.file;
1259
+ return `import * as customRoute${i} from '@game-api/${importPath}'`;
1260
+ }).join("\n");
1261
+ const lastImportIdx = entryTemplate.lastIndexOf("import type");
1262
+ const afterLastImport = entryTemplate.indexOf("\n", lastImportIdx) + 1;
1263
+ const withImports = entryTemplate.slice(0, afterLastImport) + "\n" + importStatements + "\n" + entryTemplate.slice(afterLastImport);
1264
+ const registrationStatements = customRoutes.flatMap((route, i) => {
1265
+ const methods = ["GET", "POST", "PUT", "PATCH", "DELETE"];
1266
+ return methods.map((method) => {
1267
+ const methodLower = method.toLowerCase();
1268
+ return `if (customRoute${i}.${method}) app.${methodLower}('${route.path}', customRoute${i}.${method})`;
1269
+ });
1270
+ });
1271
+ const registrationCode = ["// Custom routes", ...registrationStatements, ""].join("\n");
1272
+ const exportIdx = withImports.indexOf("export default app");
1273
+ const result = withImports.slice(0, exportIdx) + registrationCode + withImports.slice(exportIdx);
1274
+ return result;
1275
+ }
1276
+
1277
+ // src/lib/dev/pid.ts
1278
+ import { mkdir as mkdir2, unlink, writeFile as writeFile2 } from "fs/promises";
1279
+ import { join as join3 } from "path";
1280
+ function getDevServerPidPath() {
1281
+ return join3(getWorkspace(), CLI_DIRECTORIES.WORKSPACE, CLI_FILES.DEV_SERVER_PID);
1282
+ }
1283
+ async function createDevServerPidFile() {
1284
+ const pidPath = getDevServerPidFile();
1285
+ const pidDir = join3(getWorkspace(), CLI_DIRECTORIES.WORKSPACE);
1286
+ await mkdir2(pidDir, { recursive: true });
1287
+ await writeFile2(pidPath, process.pid.toString());
1288
+ }
1289
+ function getDevServerPidFile() {
1290
+ return getDevServerPidPath();
1256
1291
  }
1257
1292
 
1258
1293
  // src/lib/dev/server.ts
@@ -1260,77 +1295,65 @@ async function startDevServer(options) {
1260
1295
  const {
1261
1296
  port,
1262
1297
  config,
1263
- logger: enableLogger = true,
1264
- quiet = false,
1265
1298
  platformUrl = process.env.PLAYCADEMY_BASE_URL || "http://localhost:5174"
1266
1299
  } = options;
1267
- const app = new Hono();
1268
- if (enableLogger && !quiet) {
1269
- app.use("*", honoLogger());
1300
+ const hasSandboxTimebackCreds = !!process.env.TIMEBACK_API_CLIENT_ID;
1301
+ const devConfig = config.integrations?.timeback && !hasSandboxTimebackCreds ? { ...config, integrations: { ...config.integrations, timeback: void 0 } } : config;
1302
+ const bundle = await bundleBackend(devConfig, {
1303
+ sourcemap: false,
1304
+ minify: false
1305
+ });
1306
+ const dbDir = join4(getWorkspace(), CLI_DIRECTORIES.DATABASE);
1307
+ try {
1308
+ await mkdir3(dbDir, { recursive: true });
1309
+ } catch (error) {
1310
+ throw new Error(`Failed to create database directory: ${getErrorMessage(error)}`);
1270
1311
  }
1271
- app.use("*", cors());
1272
- const customRoutePath = join2(getWorkspace(), "api");
1273
- const customRoutesList = await discoverRoutes(customRoutePath);
1274
- const customRoutesMetadata = customRoutesList.map((r) => ({
1275
- path: r.path,
1276
- file: r.file,
1277
- methods: r.methods
1278
- }));
1279
- app.use("*", async (c, next) => {
1280
- c.env = {
1312
+ const mf = new Miniflare({
1313
+ port,
1314
+ modules: [
1315
+ {
1316
+ type: "ESModule",
1317
+ path: "index.mjs",
1318
+ contents: bundle.code
1319
+ }
1320
+ ],
1321
+ bindings: {
1281
1322
  PLAYCADEMY_API_KEY: process.env.PLAYCADEMY_API_KEY || "dev-api-key",
1282
1323
  GAME_ID: CORE_GAME_UUIDS.PLAYGROUND,
1283
- // Use playground game seeded in sandbox
1284
1324
  PLAYCADEMY_BASE_URL: platformUrl
1285
- };
1286
- c.set("config", config);
1287
- c.set("customRoutes", customRoutesMetadata);
1288
- await next();
1289
- });
1290
- let sdkPromise = null;
1291
- app.use("*", async (c, next) => {
1292
- if (!sdkPromise) {
1293
- const initConfig = {
1294
- apiKey: c.env.PLAYCADEMY_API_KEY,
1295
- gameId: c.env.GAME_ID,
1296
- baseUrl: c.env.PLAYCADEMY_BASE_URL,
1297
- config
1298
- };
1299
- sdkPromise = PlaycademyClient2.init(initConfig);
1300
- }
1301
- const sdk = await sdkPromise;
1302
- c.set("sdk", sdk);
1303
- await next();
1304
- });
1305
- const hasSandboxTimebackCreds = !!process.env.TIMEBACK_API_CLIENT_ID;
1306
- const devIntegrations = config.integrations?.timeback && !hasSandboxTimebackCreds ? { ...config.integrations, timeback: void 0 } : config.integrations;
1307
- await registerBuiltinRoutes(app, devIntegrations);
1308
- if (config.integrations?.timeback && !hasSandboxTimebackCreds) {
1309
- const timebackWarningHandler = async (c) => {
1310
- return c.json({
1311
- status: "ok",
1312
- __playcademyDevWarning: "timeback-not-configured",
1313
- __playcademyDevMessage: "TimeBack is configured in playcademy.config.js but the sandbox does not have TimeBack credentials.\n\nTo test TimeBack locally:\n \u2022 Set TIMEBACK_ONEROSTER_API_URL, TIMEBACK_CALIPER_API_URL, and TIMEBACK_API_CLIENT_ID/SECRET\n \u2022 Or deploy your game: playcademy deploy\n \u2022 Or wait for @superbuilders/timeback-local (coming soon)"
1314
- });
1315
- };
1316
- app.post("/api/integrations/timeback/end-activity", timebackWarningHandler);
1317
- }
1318
- await registerCustomRoutes(app, customRoutesList);
1319
- return serve({
1320
- fetch: app.fetch,
1321
- port
1325
+ },
1326
+ d1Databases: ["DB"],
1327
+ d1Persist: dbDir,
1328
+ compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE
1322
1329
  });
1330
+ const d1 = await mf.getD1Database("DB");
1331
+ await d1.exec("SELECT 1");
1332
+ await createDevServerPidFile();
1333
+ return mf;
1323
1334
  }
1324
1335
 
1325
1336
  // src/lib/dev/reload.ts
1326
- import { join as join3 } from "path";
1337
+ import { join as join5, relative as relative2 } from "path";
1327
1338
  import chokidar from "chokidar";
1339
+ import { bold as bold3, cyan as cyan2, dim as dim3, green as green2 } from "colorette";
1340
+ function formatTime() {
1341
+ const now = /* @__PURE__ */ new Date();
1342
+ let hours = now.getHours();
1343
+ const minutes = now.getMinutes().toString().padStart(2, "0");
1344
+ const seconds = now.getSeconds().toString().padStart(2, "0");
1345
+ const ampm = hours >= 12 ? "PM" : "AM";
1346
+ hours = hours % 12 || 12;
1347
+ return `${hours}:${minutes}:${seconds} ${ampm}`;
1348
+ }
1328
1349
  function startHotReload(onReload, options = {}) {
1329
1350
  const workspace = getWorkspace();
1351
+ const customRoutesConfig = options.config?.integrations?.customRoutes;
1352
+ const customRoutesDir = typeof customRoutesConfig === "object" && customRoutesConfig.directory || DEFAULT_API_ROUTES_DIRECTORY;
1330
1353
  const watchPaths = [
1331
- join3(workspace, "api"),
1332
- join3(workspace, "playcademy.config.js"),
1333
- join3(workspace, "playcademy.config.json")
1354
+ join5(workspace, customRoutesDir),
1355
+ join5(workspace, "playcademy.config.js"),
1356
+ join5(workspace, "playcademy.config.json")
1334
1357
  ];
1335
1358
  const watcher = chokidar.watch(watchPaths, {
1336
1359
  persistent: true,
@@ -1340,22 +1363,32 @@ function startHotReload(onReload, options = {}) {
1340
1363
  pollInterval: 100
1341
1364
  }
1342
1365
  });
1343
- const logSuccess = options.onSuccess || (() => logger.success("Reloaded"));
1366
+ const logSuccess = options.onSuccess || ((changedPath, eventType) => {
1367
+ if (changedPath) {
1368
+ const relativePath = relative2(workspace, changedPath);
1369
+ const timestamp = dim3(formatTime());
1370
+ const brand = bold3(cyan2("[playcademy]"));
1371
+ const event = eventType === "changed" ? green2("reload") : green2(eventType || "reload");
1372
+ console.log(`${timestamp} ${brand} ${event} ${dim3(relativePath)}`);
1373
+ } else {
1374
+ logger.success("Reloaded");
1375
+ }
1376
+ });
1344
1377
  const logError = options.onError || ((error) => {
1345
1378
  logger.newLine();
1346
- logger.error(`Reload failed: ${error instanceof Error ? error.message : String(error)}`);
1379
+ logger.error(`Reload failed: ${getErrorMessage(error)}`);
1347
1380
  });
1348
- const handleReload = async (path) => {
1381
+ const createReloadHandler = (eventType) => async (path2) => {
1349
1382
  try {
1350
1383
  await onReload();
1351
- logSuccess(path);
1384
+ logSuccess(path2, eventType);
1352
1385
  } catch (error) {
1353
1386
  logError(error);
1354
1387
  }
1355
1388
  };
1356
- watcher.on("change", handleReload);
1357
- watcher.on("add", handleReload);
1358
- watcher.on("unlink", handleReload);
1389
+ watcher.on("change", createReloadHandler("changed"));
1390
+ watcher.on("add", createReloadHandler("added"));
1391
+ watcher.on("unlink", createReloadHandler("removed"));
1359
1392
  return watcher;
1360
1393
  }
1361
1394
  export {