playcademy 0.13.9 → 0.13.11

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/utils.js CHANGED
@@ -301,7 +301,17 @@ var CLI_DIRECTORIES = {
301
301
  /** Database directory within workspace */
302
302
  DATABASE: join(WORKSPACE_NAME, "db"),
303
303
  /** KV storage directory within workspace */
304
- KV: join(WORKSPACE_NAME, "kv")
304
+ KV: join(WORKSPACE_NAME, "kv"),
305
+ /** Bucket storage directory within workspace */
306
+ BUCKET: join(WORKSPACE_NAME, "bucket")
307
+ };
308
+
309
+ // src/constants/ports.ts
310
+ var DEFAULT_PORTS = {
311
+ /** Sandbox server (mock platform API) */
312
+ SANDBOX: 4321,
313
+ /** Backend dev server (game backend with HMR) */
314
+ BACKEND: 8788
305
315
  };
306
316
 
307
317
  // src/constants/timeback.ts
@@ -558,7 +568,7 @@ function processConfigVariables(config) {
558
568
 
559
569
  // src/lib/dev/server.ts
560
570
  import { mkdir as mkdir2 } from "fs/promises";
561
- import { join as join7 } from "path";
571
+ import { join as join9 } from "path";
562
572
  import { Log, LogLevel, Miniflare } from "miniflare";
563
573
 
564
574
  // ../utils/src/port.ts
@@ -623,10 +633,75 @@ function writeServerInfo(type, info) {
623
633
  registry[key] = info;
624
634
  writeRegistry(registry);
625
635
  }
636
+ function readServerInfo(type, projectRoot) {
637
+ const registry = readRegistry();
638
+ const servers = Object.entries(registry).filter(([key]) => key.startsWith(`${type}-`)).map(([, info]) => info);
639
+ if (servers.length === 0) {
640
+ return null;
641
+ }
642
+ if (projectRoot) {
643
+ const match = servers.find((s) => s.projectRoot === projectRoot);
644
+ return match || null;
645
+ }
646
+ return servers[0] || null;
647
+ }
626
648
 
627
649
  // src/lib/core/client.ts
628
650
  import { PlaycademyClient } from "@playcademy/sdk";
629
651
 
652
+ // ../utils/src/package-manager.ts
653
+ import { execSync } from "child_process";
654
+ import { existsSync as existsSync3 } from "fs";
655
+ import { join as join3 } from "path";
656
+ function isCommandAvailable(command) {
657
+ try {
658
+ execSync(`command -v ${command}`, { stdio: "ignore" });
659
+ return true;
660
+ } catch {
661
+ return false;
662
+ }
663
+ }
664
+ function detectPackageManager(cwd = process.cwd()) {
665
+ if (existsSync3(join3(cwd, "bun.lock")) || existsSync3(join3(cwd, "bun.lockb"))) {
666
+ return "bun";
667
+ }
668
+ if (existsSync3(join3(cwd, "pnpm-lock.yaml"))) {
669
+ return "pnpm";
670
+ }
671
+ if (existsSync3(join3(cwd, "yarn.lock"))) {
672
+ return "yarn";
673
+ }
674
+ if (existsSync3(join3(cwd, "package-lock.json"))) {
675
+ return "npm";
676
+ }
677
+ return detectByCommandAvailability();
678
+ }
679
+ function detectByCommandAvailability() {
680
+ if (isCommandAvailable("bun")) {
681
+ return "bun";
682
+ }
683
+ if (isCommandAvailable("pnpm")) {
684
+ return "pnpm";
685
+ }
686
+ if (isCommandAvailable("yarn")) {
687
+ return "yarn";
688
+ }
689
+ return "npm";
690
+ }
691
+ function getInstallCommand(pm) {
692
+ switch (pm) {
693
+ case "bun":
694
+ return "bun install";
695
+ case "pnpm":
696
+ return "pnpm install";
697
+ case "yarn":
698
+ return "yarn install";
699
+ case "npm":
700
+ default:
701
+ return "npm install";
702
+ }
703
+ }
704
+
630
705
  // src/lib/core/context.ts
631
706
  var context = {};
632
707
  function getWorkspace() {
@@ -869,7 +944,7 @@ var CROSS_MARK = String.fromCodePoint(10006);
869
944
  init_package_json();
870
945
 
871
946
  // src/lib/templates/loader.ts
872
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
947
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
873
948
  import { dirname as dirname3, resolve as resolve3 } from "path";
874
949
  import { fileURLToPath } from "url";
875
950
  var currentDir = dirname3(fileURLToPath(import.meta.url));
@@ -882,7 +957,7 @@ function loadTemplateString(filename) {
882
957
  resolve3(currentDir, "templates", name)
883
958
  ]);
884
959
  for (const candidate of candidatePaths) {
885
- if (existsSync3(candidate)) {
960
+ if (existsSync4(candidate)) {
886
961
  return readFileSync2(candidate, "utf-8");
887
962
  }
888
963
  }
@@ -892,12 +967,12 @@ function loadTemplateString(filename) {
892
967
  // src/lib/core/import.ts
893
968
  import { mkdtempSync, rmSync } from "fs";
894
969
  import { tmpdir } from "os";
895
- import { join as join3 } from "path";
970
+ import { join as join4 } from "path";
896
971
  import { pathToFileURL } from "url";
897
972
  import * as esbuild from "esbuild";
898
973
  async function importTypescriptFile(filePath, bundleOptions) {
899
- const tempDir = mkdtempSync(join3(tmpdir(), "playcademy-import-"));
900
- const outFile = join3(tempDir, "bundle.mjs");
974
+ const tempDir = mkdtempSync(join4(tmpdir(), "playcademy-import-"));
975
+ const outFile = join4(tempDir, "bundle.mjs");
901
976
  try {
902
977
  await esbuild.build({
903
978
  entryPoints: [filePath],
@@ -925,8 +1000,8 @@ async function importTypescriptDefault(filePath, bundleOptions) {
925
1000
  }
926
1001
 
927
1002
  // src/lib/deploy/bundle.ts
928
- import { existsSync as existsSync4 } from "fs";
929
- import { join as join5 } from "path";
1003
+ import { existsSync as existsSync5 } from "fs";
1004
+ import { join as join6 } from "path";
930
1005
 
931
1006
  // ../edge-play/src/entry.ts
932
1007
  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";
@@ -1182,7 +1257,7 @@ function textLoaderPlugin() {
1182
1257
  init_file_loader();
1183
1258
  import { mkdir, writeFile } from "fs/promises";
1184
1259
  import { tmpdir as tmpdir2 } from "os";
1185
- import { join as join4, relative } from "path";
1260
+ import { join as join5, relative } from "path";
1186
1261
 
1187
1262
  // src/lib/deploy/hash.ts
1188
1263
  import { createHash } from "crypto";
@@ -1201,7 +1276,7 @@ async function discoverRoutes(apiDir) {
1201
1276
  const routes = await Promise.all(
1202
1277
  files.map(async (file) => {
1203
1278
  const routePath = filePathToRoutePath(file);
1204
- const absolutePath = join4(apiDir, file);
1279
+ const absolutePath = join5(apiDir, file);
1205
1280
  const relativePath = relative(getWorkspace(), absolutePath);
1206
1281
  const methods = await detectExportedMethods(absolutePath);
1207
1282
  return {
@@ -1261,10 +1336,10 @@ async function transpileRoute(filePath) {
1261
1336
  if (!result.outputFiles?.[0]) {
1262
1337
  throw new Error("Transpilation failed: no output");
1263
1338
  }
1264
- const tempDir = join4(tmpdir2(), "playcademy-dev");
1339
+ const tempDir = join5(tmpdir2(), "playcademy-dev");
1265
1340
  await mkdir(tempDir, { recursive: true });
1266
1341
  const hash = hashContent(filePath).slice(0, 12);
1267
- const jsPath = join4(tempDir, `${hash}.mjs`);
1342
+ const jsPath = join5(tempDir, `${hash}.mjs`);
1268
1343
  await writeFile(jsPath, result.outputFiles[0].text);
1269
1344
  return jsPath;
1270
1345
  }
@@ -1275,7 +1350,7 @@ async function discoverCustomRoutes(config) {
1275
1350
  const workspace = getWorkspace();
1276
1351
  const customRoutesConfig = config.integrations?.customRoutes;
1277
1352
  const customRoutesDir = typeof customRoutesConfig === "object" && customRoutesConfig.directory || DEFAULT_API_ROUTES_DIRECTORY;
1278
- const customRoutes = await discoverRoutes(join5(workspace, customRoutesDir));
1353
+ const customRoutes = await discoverRoutes(join6(workspace, customRoutesDir));
1279
1354
  const customRouteData = customRoutes.map((r) => ({
1280
1355
  path: r.path,
1281
1356
  file: r.file,
@@ -1287,15 +1362,15 @@ async function discoverCustomRoutes(config) {
1287
1362
  function resolveEmbeddedSourcePaths() {
1288
1363
  const workspace = getWorkspace();
1289
1364
  const distDir = new URL(".", import.meta.url).pathname;
1290
- const embeddedEdgeSrc = join5(distDir, "edge-play", "src");
1291
- const isBuiltPackage = existsSync4(embeddedEdgeSrc);
1365
+ const embeddedEdgeSrc = join6(distDir, "edge-play", "src");
1366
+ const isBuiltPackage = existsSync5(embeddedEdgeSrc);
1292
1367
  const monorepoRoot = getMonorepoRoot();
1293
- const monorepoEdgeSrc = join5(monorepoRoot, "packages/edge-play/src");
1368
+ const monorepoEdgeSrc = join6(monorepoRoot, "packages/edge-play/src");
1294
1369
  const edgePlaySrc = isBuiltPackage ? embeddedEdgeSrc : monorepoEdgeSrc;
1295
- const cliPackageRoot = isBuiltPackage ? join5(distDir, "../../..") : join5(monorepoRoot, "packages/cli");
1296
- const cliNodeModules = isBuiltPackage ? join5(cliPackageRoot, "node_modules") : monorepoRoot;
1297
- const workspaceNodeModules = join5(workspace, "node_modules");
1298
- const constantsEntry = isBuiltPackage ? join5(embeddedEdgeSrc, "..", "..", "constants", "src", "index.ts") : join5(monorepoRoot, "packages", "constants", "src", "index.ts");
1370
+ const cliPackageRoot = isBuiltPackage ? join6(distDir, "../../..") : join6(monorepoRoot, "packages/cli");
1371
+ const cliNodeModules = isBuiltPackage ? join6(cliPackageRoot, "node_modules") : monorepoRoot;
1372
+ const workspaceNodeModules = join6(workspace, "node_modules");
1373
+ const constantsEntry = isBuiltPackage ? join6(embeddedEdgeSrc, "..", "..", "constants", "src", "index.ts") : join6(monorepoRoot, "packages", "constants", "src", "index.ts");
1299
1374
  return {
1300
1375
  isBuiltPackage,
1301
1376
  edgePlaySrc,
@@ -1355,16 +1430,16 @@ function createEsbuildConfig(entryCode, paths, bundleConfig, customRoutesDir, op
1355
1430
  // │ Example: import * as route from '@game-api/hello.ts' │
1356
1431
  // │ Resolves to: /user-project/server/api/hello.ts │
1357
1432
  // └─────────────────────────────────────────────────────────────────┘
1358
- "@game-api": join5(workspace, customRoutesDir),
1433
+ "@game-api": join6(workspace, customRoutesDir),
1359
1434
  // ┌─ Node.js polyfills for Cloudflare Workers ──────────────────────┐
1360
1435
  // │ Workers don't have fs, path, os, etc. Redirect to polyfills │
1361
1436
  // │ that throw helpful errors if user code tries to use them. │
1362
1437
  // └─────────────────────────────────────────────────────────────────┘
1363
- fs: join5(edgePlaySrc, "polyfills.js"),
1364
- "fs/promises": join5(edgePlaySrc, "polyfills.js"),
1365
- path: join5(edgePlaySrc, "polyfills.js"),
1366
- os: join5(edgePlaySrc, "polyfills.js"),
1367
- process: join5(edgePlaySrc, "polyfills.js")
1438
+ fs: join6(edgePlaySrc, "polyfills.js"),
1439
+ "fs/promises": join6(edgePlaySrc, "polyfills.js"),
1440
+ path: join6(edgePlaySrc, "polyfills.js"),
1441
+ os: join6(edgePlaySrc, "polyfills.js"),
1442
+ process: join6(edgePlaySrc, "polyfills.js")
1368
1443
  },
1369
1444
  // ──── Build Plugins ────
1370
1445
  plugins: [textLoaderPlugin()],
@@ -1431,8 +1506,8 @@ import { checkbox, confirm, input, select } from "@inquirer/prompts";
1431
1506
  import { bold as bold3, cyan as cyan2 } from "colorette";
1432
1507
 
1433
1508
  // src/lib/init/database.ts
1434
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1435
- import { join as join6 } from "path";
1509
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1510
+ import { join as join7 } from "path";
1436
1511
  var drizzleConfigTemplate = loadTemplateString("database/drizzle-config.ts");
1437
1512
  var dbSchemaUsersTemplate = loadTemplateString("database/db-schema-users.ts");
1438
1513
  var dbSchemaScoresTemplate = loadTemplateString("database/db-schema-scores.ts");
@@ -1440,27 +1515,96 @@ var dbSchemaIndexTemplate = loadTemplateString("database/db-schema-index.ts");
1440
1515
  var dbIndexTemplate = loadTemplateString("database/db-index.ts");
1441
1516
  var dbTypesTemplate = loadTemplateString("database/db-types.ts");
1442
1517
  var packageTemplate = loadTemplateString("database/package.json");
1443
- var rootGitignoreTemplate = loadTemplateString("gitignore");
1444
1518
  function hasDatabaseSetup() {
1445
1519
  const workspace = getWorkspace();
1446
- const drizzleConfigPath = join6(workspace, "drizzle.config.ts");
1447
- const drizzleConfigJsPath = join6(workspace, "drizzle.config.js");
1448
- return existsSync5(drizzleConfigPath) || existsSync5(drizzleConfigJsPath);
1520
+ const drizzleConfigPath = join7(workspace, "drizzle.config.ts");
1521
+ const drizzleConfigJsPath = join7(workspace, "drizzle.config.js");
1522
+ return existsSync6(drizzleConfigPath) || existsSync6(drizzleConfigJsPath);
1523
+ }
1524
+
1525
+ // src/lib/init/types.ts
1526
+ init_file_loader();
1527
+ import { execSync as execSync2 } from "child_process";
1528
+ import { writeFileSync as writeFileSync3 } from "fs";
1529
+ import { dirname as dirname4, join as join8 } from "path";
1530
+ import { fileURLToPath as fileURLToPath2 } from "url";
1531
+
1532
+ // src/lib/init/bucket.ts
1533
+ function hasBucketSetup(config) {
1534
+ return !!config.integrations?.bucket;
1535
+ }
1536
+
1537
+ // src/lib/init/kv.ts
1538
+ function hasKVSetup(config) {
1539
+ return !!config.integrations?.kv;
1540
+ }
1541
+
1542
+ // src/lib/init/types.ts
1543
+ var playcademyEnvTemplate = loadTemplateString("playcademy-env.d.ts");
1544
+ async function ensurePlaycademyTypes() {
1545
+ try {
1546
+ const workspace = getWorkspace();
1547
+ const config = await loadConfig();
1548
+ const hasDB = hasDatabaseSetup();
1549
+ const hasKV = hasKVSetup(config);
1550
+ const hasBucket = hasBucketSetup(config);
1551
+ if (!hasDB && !hasKV && !hasBucket) {
1552
+ return;
1553
+ }
1554
+ const playcademyDir = join8(workspace, CLI_DIRECTORIES.WORKSPACE);
1555
+ const playcademyPkgPath = join8(playcademyDir, "package.json");
1556
+ const __dirname = dirname4(fileURLToPath2(import.meta.url));
1557
+ const cliPkg = await loadPackageJson({ cwd: __dirname, searchUp: true, required: true });
1558
+ const workersTypesVersion = cliPkg?.devDependencies?.["@cloudflare/workers-types"] || "latest";
1559
+ const playcademyPkg = {
1560
+ private: true,
1561
+ dependencies: {
1562
+ hono: "latest"
1563
+ },
1564
+ devDependencies: {
1565
+ "@cloudflare/workers-types": workersTypesVersion
1566
+ }
1567
+ };
1568
+ writeFileSync3(playcademyPkgPath, JSON.stringify(playcademyPkg, null, 4) + "\n");
1569
+ const pm = detectPackageManager(workspace);
1570
+ const installCmd = getInstallCommand(pm);
1571
+ execSync2(installCmd, {
1572
+ cwd: playcademyDir,
1573
+ stdio: ["ignore", "ignore", "ignore"]
1574
+ });
1575
+ const bindings = [];
1576
+ if (hasKV) {
1577
+ bindings.push(" KV: KVNamespace");
1578
+ }
1579
+ if (hasDB) {
1580
+ bindings.push(" DB: D1Database");
1581
+ }
1582
+ if (hasBucket) {
1583
+ bindings.push(" BUCKET: R2Bucket");
1584
+ }
1585
+ const bindingsStr = bindings.length > 0 ? "\n" + bindings.join("\n") : "";
1586
+ const envContent = playcademyEnvTemplate.replace("{{BINDINGS}}", bindingsStr);
1587
+ const envPath = join8(workspace, "playcademy-env.d.ts");
1588
+ writeFileSync3(envPath, envContent);
1589
+ } catch (error) {
1590
+ logger.warn(
1591
+ `Failed to generate TypeScript types: ${error instanceof Error ? error.message : String(error)}`
1592
+ );
1593
+ }
1449
1594
  }
1450
1595
 
1451
1596
  // src/lib/init/scaffold.ts
1452
1597
  var sampleCustomRouteTemplate = loadTemplateString("api/sample-custom.ts");
1453
1598
  var sampleDatabaseRouteTemplate = loadTemplateString("api/sample-database.ts");
1454
1599
  var sampleKvRouteTemplate = loadTemplateString("api/sample-kv.ts");
1600
+ var sampleBucketRouteTemplate = loadTemplateString("api/sample-bucket.ts");
1455
1601
  var playcademyGitignoreTemplate = loadTemplateString("playcademy-gitignore");
1456
1602
 
1457
1603
  // src/lib/init/display.ts
1458
1604
  import { blueBright } from "colorette";
1459
1605
 
1460
- // src/lib/init/kv.ts
1461
- function hasKVSetup(config) {
1462
- return !!config.integrations?.kv;
1463
- }
1606
+ // src/lib/init/gitignore.ts
1607
+ var rootGitignoreTemplate = loadTemplateString("gitignore");
1464
1608
 
1465
1609
  // src/lib/dev/server.ts
1466
1610
  var FilteredLog = class extends Log {
@@ -1470,14 +1614,10 @@ var FilteredLog = class extends Log {
1470
1614
  }
1471
1615
  };
1472
1616
  async function startDevServer(options) {
1473
- const {
1474
- port: preferredPort,
1475
- config: providedConfig,
1476
- platformUrl = process.env.PLAYCADEMY_BASE_URL || "http://localhost:5174",
1477
- logger: logger2 = true
1478
- } = options;
1617
+ const { port: preferredPort, config: providedConfig, platformUrl, logger: logger2 = true } = options;
1479
1618
  const port = await findAvailablePort(preferredPort);
1480
1619
  const config = providedConfig ?? await loadConfig();
1620
+ await ensurePlaycademyTypes();
1481
1621
  const hasSandboxTimebackCreds = !!process.env.TIMEBACK_API_CLIENT_ID;
1482
1622
  const devConfig = config.integrations?.timeback && !hasSandboxTimebackCreds ? { ...config, integrations: { ...config.integrations, timeback: void 0 } } : config;
1483
1623
  const bundle = await bundleBackend(devConfig, {
@@ -1488,7 +1628,11 @@ async function startDevServer(options) {
1488
1628
  const dbDir = hasDatabase ? await ensureDatabaseDirectory() : void 0;
1489
1629
  const hasKV = hasKVSetup(config);
1490
1630
  const kvDir = hasKV ? await ensureKvDirectory() : void 0;
1631
+ const hasBucket = hasBucketSetup(config);
1632
+ const bucketDir = hasBucket ? await ensureBucketDirectory() : void 0;
1491
1633
  const log2 = logger2 ? new FilteredLog(LogLevel.INFO) : new Log(LogLevel.NONE);
1634
+ const sandboxInfo = readServerInfo("sandbox", getWorkspace());
1635
+ const baseUrl = platformUrl ?? sandboxInfo?.url ?? process.env.PLAYCADEMY_BASE_URL ?? `http://localhost:${DEFAULT_PORTS.SANDBOX}`;
1492
1636
  const mf = new Miniflare({
1493
1637
  port,
1494
1638
  log: log2,
@@ -1502,12 +1646,14 @@ async function startDevServer(options) {
1502
1646
  bindings: {
1503
1647
  PLAYCADEMY_API_KEY: process.env.PLAYCADEMY_API_KEY || "dev-api-key",
1504
1648
  GAME_ID: CORE_GAME_UUIDS.PLAYGROUND,
1505
- PLAYCADEMY_BASE_URL: platformUrl
1649
+ PLAYCADEMY_BASE_URL: baseUrl
1506
1650
  },
1507
1651
  d1Databases: hasDatabase ? ["DB"] : [],
1508
1652
  d1Persist: dbDir,
1509
1653
  kvNamespaces: hasKV ? ["KV"] : [],
1510
1654
  kvPersist: kvDir,
1655
+ r2Buckets: hasBucket ? ["BUCKET"] : [],
1656
+ r2Persist: bucketDir,
1511
1657
  compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE
1512
1658
  });
1513
1659
  if (hasDatabase) {
@@ -1517,7 +1663,7 @@ async function startDevServer(options) {
1517
1663
  return { server: mf, port };
1518
1664
  }
1519
1665
  async function ensureDatabaseDirectory() {
1520
- const dbDir = join7(getWorkspace(), CLI_DIRECTORIES.DATABASE);
1666
+ const dbDir = join9(getWorkspace(), CLI_DIRECTORIES.DATABASE);
1521
1667
  try {
1522
1668
  await mkdir2(dbDir, { recursive: true });
1523
1669
  } catch (error) {
@@ -1526,7 +1672,7 @@ async function ensureDatabaseDirectory() {
1526
1672
  return dbDir;
1527
1673
  }
1528
1674
  async function ensureKvDirectory() {
1529
- const kvDir = join7(getWorkspace(), CLI_DIRECTORIES.KV);
1675
+ const kvDir = join9(getWorkspace(), CLI_DIRECTORIES.KV);
1530
1676
  try {
1531
1677
  await mkdir2(kvDir, { recursive: true });
1532
1678
  } catch (error) {
@@ -1534,6 +1680,15 @@ async function ensureKvDirectory() {
1534
1680
  }
1535
1681
  return kvDir;
1536
1682
  }
1683
+ async function ensureBucketDirectory() {
1684
+ const bucketDir = join9(getWorkspace(), CLI_DIRECTORIES.BUCKET);
1685
+ try {
1686
+ await mkdir2(bucketDir, { recursive: true });
1687
+ } catch (error) {
1688
+ throw new Error(`Failed to create bucket directory: ${getErrorMessage(error)}`);
1689
+ }
1690
+ return bucketDir;
1691
+ }
1537
1692
  async function initializeDatabase(mf) {
1538
1693
  const d1 = await mf.getD1Database("DB");
1539
1694
  await d1.exec("SELECT 1");
@@ -1549,7 +1704,7 @@ async function writeBackendServerInfo(port) {
1549
1704
  }
1550
1705
 
1551
1706
  // src/lib/dev/reload.ts
1552
- import { join as join8, relative as relative2 } from "path";
1707
+ import { join as join10, relative as relative2 } from "path";
1553
1708
  import chokidar from "chokidar";
1554
1709
  import { bold as bold4, cyan as cyan3, dim as dim3, green as green2 } from "colorette";
1555
1710
  function formatTime() {
@@ -1566,9 +1721,9 @@ function startHotReload(onReload, options = {}) {
1566
1721
  const customRoutesConfig = options.config?.integrations?.customRoutes;
1567
1722
  const customRoutesDir = typeof customRoutesConfig === "object" && customRoutesConfig.directory || DEFAULT_API_ROUTES_DIRECTORY;
1568
1723
  const watchPaths = [
1569
- join8(workspace, customRoutesDir),
1570
- join8(workspace, "playcademy.config.js"),
1571
- join8(workspace, "playcademy.config.json")
1724
+ join10(workspace, customRoutesDir),
1725
+ join10(workspace, "playcademy.config.js"),
1726
+ join10(workspace, "playcademy.config.json")
1572
1727
  ];
1573
1728
  const watcher = chokidar.watch(watchPaths, {
1574
1729
  persistent: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playcademy",
3
- "version": "0.13.9",
3
+ "version": "0.13.11",
4
4
  "type": "module",
5
5
  "module": "./dist/index.js",
6
6
  "main": "./dist/index.js",