playcademy 0.13.2 → 0.13.3

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.
@@ -72,8 +72,6 @@ declare const CLI_FILES: {
72
72
  readonly AUTH_STORE: "auth.json";
73
73
  /** Games deployment info store */
74
74
  readonly GAMES_STORE: "games.json";
75
- /** Dev server PID file */
76
- readonly DEV_SERVER_PID: "dev-server.pid";
77
75
  /** Initial database file (before miniflare) */
78
76
  readonly INITIAL_DATABASE: "initial.sqlite";
79
77
  };
package/dist/constants.js CHANGED
@@ -35,8 +35,6 @@ var CLI_FILES = {
35
35
  AUTH_STORE: "auth.json",
36
36
  /** Games deployment info store */
37
37
  GAMES_STORE: "games.json",
38
- /** Dev server PID file */
39
- DEV_SERVER_PID: "dev-server.pid",
40
38
  /** Initial database file (before miniflare) */
41
39
  INITIAL_DATABASE: "initial.sqlite"
42
40
  };
package/dist/db.js CHANGED
@@ -39,8 +39,6 @@ var CLI_FILES = {
39
39
  AUTH_STORE: "auth.json",
40
40
  /** Games deployment info store */
41
41
  GAMES_STORE: "games.json",
42
- /** Dev server PID file */
43
- DEV_SERVER_PID: "dev-server.pid",
44
42
  /** Initial database file (before miniflare) */
45
43
  INITIAL_DATABASE: "initial.sqlite"
46
44
  };
package/dist/index.js CHANGED
@@ -2145,8 +2145,6 @@ var init_paths = __esm({
2145
2145
  AUTH_STORE: "auth.json",
2146
2146
  /** Games deployment info store */
2147
2147
  GAMES_STORE: "games.json",
2148
- /** Dev server PID file */
2149
- DEV_SERVER_PID: "dev-server.pid",
2150
2148
  /** Initial database file (before miniflare) */
2151
2149
  INITIAL_DATABASE: "initial.sqlite"
2152
2150
  };
@@ -3726,7 +3724,7 @@ import { program } from "commander";
3726
3724
 
3727
3725
  // src/commands/init/index.ts
3728
3726
  import { execSync as execSync3 } from "child_process";
3729
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
3727
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
3730
3728
  import { resolve as resolve10 } from "path";
3731
3729
 
3732
3730
  // ../../node_modules/@inquirer/core/dist/esm/lib/errors.js
@@ -7564,58 +7562,10 @@ function displayRegisteredRoutes(integrations, customRoutes = []) {
7564
7562
  }
7565
7563
  }
7566
7564
 
7567
- // src/lib/dev/pid.ts
7568
- init_constants2();
7569
- init_core();
7570
- import { existsSync as existsSync15, readFileSync as readFileSync4, unlinkSync as unlinkSync2 } from "fs";
7571
- import { mkdir as mkdir4, unlink, writeFile as writeFile4 } from "fs/promises";
7572
- import { join as join15 } from "path";
7573
- function getDevServerPidPath() {
7574
- return join15(getWorkspace(), CLI_DIRECTORIES.WORKSPACE, CLI_FILES.DEV_SERVER_PID);
7575
- }
7576
- async function createDevServerPidFile() {
7577
- const pidPath = getDevServerPidFile();
7578
- const pidDir = join15(getWorkspace(), CLI_DIRECTORIES.WORKSPACE);
7579
- await mkdir4(pidDir, { recursive: true });
7580
- await writeFile4(pidPath, process.pid.toString());
7581
- }
7582
- async function removeDevServerPidFile() {
7583
- const pidPath = getDevServerPidFile();
7584
- if (existsSync15(pidPath)) {
7585
- await unlink(pidPath);
7586
- }
7587
- }
7588
- function isDevServerRunning() {
7589
- const pidPath = getDevServerPidFile();
7590
- if (!existsSync15(pidPath)) {
7591
- return false;
7592
- }
7593
- try {
7594
- const pid = parseInt(readFileSync4(pidPath, "utf8").trim(), 10);
7595
- if (process.platform === "win32") {
7596
- return true;
7597
- } else {
7598
- try {
7599
- process.kill(pid, 0);
7600
- return true;
7601
- } catch {
7602
- unlinkSync2(pidPath);
7603
- return false;
7604
- }
7605
- }
7606
- } catch {
7607
- unlinkSync2(pidPath);
7608
- return false;
7609
- }
7610
- }
7611
- function getDevServerPidFile() {
7612
- return getDevServerPidPath();
7613
- }
7614
-
7615
7565
  // src/lib/dev/reload.ts
7616
7566
  init_constants2();
7617
7567
  init_core();
7618
- import { join as join16, relative as relative3 } from "path";
7568
+ import { join as join15, relative as relative3 } from "path";
7619
7569
  import chokidar from "chokidar";
7620
7570
  import { bold as bold4, cyan as cyan3, dim as dim6, green as green3 } from "colorette";
7621
7571
  function formatTime() {
@@ -7632,9 +7582,9 @@ function startHotReload(onReload, options = {}) {
7632
7582
  const customRoutesConfig = options.config?.integrations?.customRoutes;
7633
7583
  const customRoutesDir = typeof customRoutesConfig === "object" && customRoutesConfig.directory || DEFAULT_API_ROUTES_DIRECTORY;
7634
7584
  const watchPaths = [
7635
- join16(workspace, customRoutesDir),
7636
- join16(workspace, "playcademy.config.js"),
7637
- join16(workspace, "playcademy.config.json")
7585
+ join15(workspace, customRoutesDir),
7586
+ join15(workspace, "playcademy.config.js"),
7587
+ join15(workspace, "playcademy.config.json")
7638
7588
  ];
7639
7589
  const watcher = chokidar.watch(watchPaths, {
7640
7590
  persistent: true,
@@ -7675,18 +7625,134 @@ function startHotReload(onReload, options = {}) {
7675
7625
 
7676
7626
  // src/lib/dev/server.ts
7677
7627
  init_src2();
7628
+ import { mkdir as mkdir4 } from "fs/promises";
7629
+ import { join as join17 } from "path";
7630
+ import { Miniflare } from "miniflare";
7631
+
7632
+ // ../utils/src/port.ts
7633
+ import { existsSync as existsSync15, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
7634
+ import { createServer as createServer2 } from "node:net";
7635
+ import { homedir as homedir3 } from "node:os";
7636
+ import { join as join16 } from "node:path";
7637
+ async function isPortAvailableOnHost(port, host) {
7638
+ return new Promise((resolve11) => {
7639
+ const server = createServer2();
7640
+ server.once("error", () => {
7641
+ resolve11(false);
7642
+ });
7643
+ server.once("listening", () => {
7644
+ server.close();
7645
+ resolve11(true);
7646
+ });
7647
+ server.listen(port, host);
7648
+ });
7649
+ }
7650
+ async function findAvailablePort(startPort = 4321) {
7651
+ const allInterfacesAvailable = await isPortAvailableOnHost(startPort, "0.0.0.0");
7652
+ const ipv6Available = await isPortAvailableOnHost(startPort, "::1");
7653
+ if (allInterfacesAvailable && ipv6Available) {
7654
+ return startPort;
7655
+ }
7656
+ return findAvailablePort(startPort + 1);
7657
+ }
7658
+ function getRegistryPath() {
7659
+ const home = homedir3();
7660
+ const dir = join16(home, ".playcademy");
7661
+ if (!existsSync15(dir)) {
7662
+ mkdirSync4(dir, { recursive: true });
7663
+ }
7664
+ return join16(dir, ".proc");
7665
+ }
7666
+ function readRegistry() {
7667
+ const registryPath = getRegistryPath();
7668
+ if (!existsSync15(registryPath)) {
7669
+ return {};
7670
+ }
7671
+ try {
7672
+ const content = readFileSync4(registryPath, "utf-8");
7673
+ return JSON.parse(content);
7674
+ } catch {
7675
+ return {};
7676
+ }
7677
+ }
7678
+ function writeRegistry(registry) {
7679
+ const registryPath = getRegistryPath();
7680
+ writeFileSync4(registryPath, JSON.stringify(registry, null, 2), "utf-8");
7681
+ }
7682
+ function getServerKey(type, port) {
7683
+ return `${type}-${port}`;
7684
+ }
7685
+ function writeServerInfo(type, info) {
7686
+ const registry = readRegistry();
7687
+ const key = getServerKey(type, info.port);
7688
+ registry[key] = info;
7689
+ writeRegistry(registry);
7690
+ }
7691
+ function readServerInfo(type, projectRoot) {
7692
+ const registry = readRegistry();
7693
+ const servers = Object.entries(registry).filter(([key]) => key.startsWith(`${type}-`)).map(([, info]) => info);
7694
+ if (servers.length === 0) {
7695
+ return null;
7696
+ }
7697
+ if (projectRoot) {
7698
+ const match = servers.find((s) => s.projectRoot === projectRoot);
7699
+ return match || null;
7700
+ }
7701
+ return servers[0] || null;
7702
+ }
7703
+ function isServerRunning(type, projectRoot) {
7704
+ const info = readServerInfo(type, projectRoot);
7705
+ if (!info) {
7706
+ return false;
7707
+ }
7708
+ try {
7709
+ if (process.platform === "win32") {
7710
+ return true;
7711
+ } else {
7712
+ process.kill(info.pid, 0);
7713
+ return true;
7714
+ }
7715
+ } catch {
7716
+ cleanupServerInfo(type, projectRoot);
7717
+ return false;
7718
+ }
7719
+ }
7720
+ function cleanupServerInfo(type, projectRoot, pid) {
7721
+ const registry = readRegistry();
7722
+ const keysToRemove = [];
7723
+ for (const [key, info] of Object.entries(registry)) {
7724
+ if (key.startsWith(`${type}-`)) {
7725
+ let matches = true;
7726
+ if (projectRoot && info.projectRoot !== projectRoot) {
7727
+ matches = false;
7728
+ }
7729
+ if (pid !== void 0 && info.pid !== pid) {
7730
+ matches = false;
7731
+ }
7732
+ if (matches) {
7733
+ keysToRemove.push(key);
7734
+ }
7735
+ }
7736
+ }
7737
+ for (const key of keysToRemove) {
7738
+ delete registry[key];
7739
+ }
7740
+ if (keysToRemove.length > 0) {
7741
+ writeRegistry(registry);
7742
+ }
7743
+ }
7744
+
7745
+ // src/lib/dev/server.ts
7678
7746
  init_constants2();
7679
7747
  init_loader();
7680
7748
  init_core();
7681
- import { mkdir as mkdir5 } from "fs/promises";
7682
- import { join as join17 } from "path";
7683
- import { Miniflare } from "miniflare";
7684
7749
  async function startDevServer(options) {
7685
7750
  const {
7686
- port,
7751
+ port: preferredPort,
7687
7752
  config: providedConfig,
7688
7753
  platformUrl = process.env.PLAYCADEMY_BASE_URL || "http://localhost:5174"
7689
7754
  } = options;
7755
+ const port = await findAvailablePort(preferredPort);
7690
7756
  const config = providedConfig ?? await loadConfig();
7691
7757
  const hasSandboxTimebackCreds = !!process.env.TIMEBACK_API_CLIENT_ID;
7692
7758
  const devConfig = config.integrations?.timeback && !hasSandboxTimebackCreds ? { ...config, integrations: { ...config.integrations, timeback: void 0 } } : config;
@@ -7721,12 +7787,13 @@ async function startDevServer(options) {
7721
7787
  if (hasDatabase) {
7722
7788
  await initializeDatabase(mf);
7723
7789
  }
7790
+ await writeBackendServerInfo(port);
7724
7791
  return mf;
7725
7792
  }
7726
7793
  async function ensureDatabaseDirectory() {
7727
7794
  const dbDir = join17(getWorkspace(), CLI_DIRECTORIES.DATABASE);
7728
7795
  try {
7729
- await mkdir5(dbDir, { recursive: true });
7796
+ await mkdir4(dbDir, { recursive: true });
7730
7797
  } catch (error) {
7731
7798
  throw new Error(`Failed to create database directory: ${getErrorMessage(error)}`);
7732
7799
  }
@@ -7735,7 +7802,7 @@ async function ensureDatabaseDirectory() {
7735
7802
  async function ensureKvDirectory() {
7736
7803
  const kvDir = join17(getWorkspace(), CLI_DIRECTORIES.KV);
7737
7804
  try {
7738
- await mkdir5(kvDir, { recursive: true });
7805
+ await mkdir4(kvDir, { recursive: true });
7739
7806
  } catch (error) {
7740
7807
  throw new Error(`Failed to create KV directory: ${getErrorMessage(error)}`);
7741
7808
  }
@@ -7744,7 +7811,15 @@ async function ensureKvDirectory() {
7744
7811
  async function initializeDatabase(mf) {
7745
7812
  const d1 = await mf.getD1Database("DB");
7746
7813
  await d1.exec("SELECT 1");
7747
- await createDevServerPidFile();
7814
+ }
7815
+ async function writeBackendServerInfo(port) {
7816
+ writeServerInfo("backend", {
7817
+ pid: process.pid,
7818
+ port,
7819
+ url: `http://localhost:${port}/api`,
7820
+ startedAt: Date.now(),
7821
+ projectRoot: getWorkspace()
7822
+ });
7748
7823
  }
7749
7824
 
7750
7825
  // src/lib/timeback/cleanup.ts
@@ -7810,7 +7885,7 @@ function displayResourcesStatus(resources, logger2) {
7810
7885
  }
7811
7886
 
7812
7887
  // src/commands/init/config.ts
7813
- import { writeFileSync as writeFileSync4 } from "fs";
7888
+ import { writeFileSync as writeFileSync5 } from "fs";
7814
7889
  import { resolve as resolve9 } from "path";
7815
7890
  init_file_loader();
7816
7891
  init_constants2();
@@ -7862,7 +7937,7 @@ var configCommand = new Command("config").description("Create playcademy.config
7862
7937
  emoji: gameInfo.emoji,
7863
7938
  timeback: timebackConfig ?? void 0
7864
7939
  });
7865
- writeFileSync4(resolve9(getWorkspace(), configFileName), configContent, "utf-8");
7940
+ writeFileSync5(resolve9(getWorkspace(), configFileName), configContent, "utf-8");
7866
7941
  displayConfigSuccess(!!timebackConfig);
7867
7942
  } catch (error) {
7868
7943
  logger.newLine();
@@ -7975,7 +8050,7 @@ var initCommand = new Command2("init").description("Initialize a playcademy.conf
7975
8050
  kv: kv ?? void 0,
7976
8051
  timeback: timebackConfig ?? void 0
7977
8052
  });
7978
- writeFileSync5(resolve10(getWorkspace(), configFileName), configContent, "utf-8");
8053
+ writeFileSync6(resolve10(getWorkspace(), configFileName), configContent, "utf-8");
7979
8054
  displaySuccessMessage({
7980
8055
  configFileName,
7981
8056
  apiDirectory: customRoutes?.directory ?? null,
@@ -7997,7 +8072,7 @@ async function addPlaycademySdk() {
7997
8072
  }
7998
8073
  if (!pkg.dependencies) pkg.dependencies = {};
7999
8074
  pkg.dependencies["@playcademy/sdk"] = "latest";
8000
- writeFileSync5(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
8075
+ writeFileSync6(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
8001
8076
  return true;
8002
8077
  }
8003
8078
  initCommand.addCommand(configCommand);
@@ -8587,7 +8662,43 @@ var getStatusCommand = new Command11("status").description("Check your developer
8587
8662
 
8588
8663
  // src/commands/dev/server.ts
8589
8664
  import { blueBright as blueBright4, underline as underline2 } from "colorette";
8665
+ function setupCleanupHandlers(workspace, getServer) {
8666
+ let isShuttingDown = false;
8667
+ const cleanup = () => {
8668
+ if (isShuttingDown) return;
8669
+ isShuttingDown = true;
8670
+ cleanupServerInfo("backend", workspace, process.pid);
8671
+ const server = getServer();
8672
+ if (server) {
8673
+ server.dispose().then(() => process.exit(0)).catch(() => process.exit(1));
8674
+ } else {
8675
+ process.exit(0);
8676
+ }
8677
+ };
8678
+ process.on("SIGINT", cleanup);
8679
+ process.on("SIGTERM", cleanup);
8680
+ }
8681
+ async function setupServerHotReload(serverRef, port, workspace, config, loggerEnabled) {
8682
+ return startHotReload(
8683
+ async () => {
8684
+ if (serverRef.current) {
8685
+ await serverRef.current.dispose();
8686
+ }
8687
+ const newConfig = await loadConfig();
8688
+ serverRef.current = await startDevServer({
8689
+ port,
8690
+ config: newConfig,
8691
+ logger: loggerEnabled
8692
+ });
8693
+ await discoverRoutes(getCustomRoutesDirectory(workspace, newConfig));
8694
+ },
8695
+ { config }
8696
+ );
8697
+ }
8590
8698
  async function runDevServer(options) {
8699
+ const workspace = getWorkspace();
8700
+ const serverRef = { current: null };
8701
+ setupCleanupHandlers(workspace, () => serverRef.current);
8591
8702
  try {
8592
8703
  logger.newLine();
8593
8704
  const config = await loadConfig();
@@ -8605,52 +8716,27 @@ async function runDevServer(options) {
8605
8716
  return;
8606
8717
  }
8607
8718
  const port = parseInt(options.port, 10);
8608
- let server = await startDevServer({
8719
+ const server = await startDevServer({
8609
8720
  port,
8610
8721
  config,
8611
8722
  logger: options.logger !== false
8612
8723
  });
8613
- const hotReload = options.reload !== false;
8724
+ serverRef.current = server;
8614
8725
  logger.success(`Game API started: ${blueBright4(underline2(`http://localhost:${port}/api`))}`);
8615
8726
  logger.newLine();
8616
- const workspace = getWorkspace();
8617
8727
  const customRoutesDir = getCustomRoutesDirectory(workspace, config);
8618
- let customRoutes = await discoverRoutes(customRoutesDir);
8728
+ const customRoutes = await discoverRoutes(customRoutesDir);
8619
8729
  displayRegisteredRoutes(config.integrations, customRoutes);
8620
8730
  logger.newLine();
8621
- if (hotReload) {
8622
- startHotReload(
8623
- async () => {
8624
- await server.dispose();
8625
- clearModuleCache();
8626
- const newConfig = await loadConfig();
8627
- server = await startDevServer({
8628
- port,
8629
- config: newConfig,
8630
- logger: options.logger !== false
8631
- });
8632
- const newCustomRoutesDir = getCustomRoutesDirectory(workspace, newConfig);
8633
- customRoutes = await discoverRoutes(newCustomRoutesDir);
8634
- },
8635
- { config }
8636
- );
8731
+ if (options.reload !== false) {
8732
+ await setupServerHotReload(serverRef, port, workspace, config, options.logger !== false);
8637
8733
  }
8638
8734
  logger.remark(`Press ${underline2("ctrl+c")} to stop`);
8639
8735
  logger.newLine();
8640
- process.on("SIGINT", async () => {
8641
- await removeDevServerPidFile();
8642
- process.exit(0);
8643
- });
8644
- process.on("SIGTERM", async () => {
8645
- await removeDevServerPidFile();
8646
- process.exit(0);
8647
- });
8648
8736
  } catch (error) {
8649
8737
  logAndExit(error, logger, { prefix: "Failed to start dev server" });
8650
8738
  }
8651
8739
  }
8652
- function clearModuleCache() {
8653
- }
8654
8740
 
8655
8741
  // src/commands/dev/index.ts
8656
8742
  var devCommand = new Command12("dev").description("Start local backend development server").option("-p, --port <port>", "Backend server port", String(DEFAULT_PORTS.BACKEND)).option("--no-reload", "Disable hot reload").option("--no-logger", "Disable HTTP request logging").action(runDevServer);
@@ -8762,7 +8848,7 @@ async function runDbDiff() {
8762
8848
  // src/commands/db/init.ts
8763
8849
  init_file_loader();
8764
8850
  init_constants2();
8765
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
8851
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
8766
8852
  import { input as input6 } from "@inquirer/prompts";
8767
8853
  async function runDbInit() {
8768
8854
  try {
@@ -8819,7 +8905,7 @@ async function runDbInit() {
8819
8905
  );
8820
8906
  }
8821
8907
  }
8822
- writeFileSync6(configFile.path, updatedContent, "utf-8");
8908
+ writeFileSync7(configFile.path, updatedContent, "utf-8");
8823
8909
  logger.success(`Updated ${configFile.path.split("/").pop()}`);
8824
8910
  }
8825
8911
  logger.newLine();
@@ -8841,12 +8927,12 @@ async function runDbInit() {
8841
8927
  // src/commands/db/reset.ts
8842
8928
  init_src2();
8843
8929
  init_src();
8844
- init_constants2();
8845
8930
  import { spawn } from "child_process";
8846
8931
  import { existsSync as existsSync16, rmSync as rmSync2 } from "fs";
8847
8932
  import { join as join18 } from "path";
8848
8933
  import { confirm as confirm6 } from "@inquirer/prompts";
8849
8934
  import { Miniflare as Miniflare2 } from "miniflare";
8935
+ init_constants2();
8850
8936
  async function runDbReset() {
8851
8937
  try {
8852
8938
  const workspace = getWorkspace();
@@ -8859,7 +8945,7 @@ async function runDbReset() {
8859
8945
  return;
8860
8946
  }
8861
8947
  logger.newLine();
8862
- if (isDevServerRunning()) {
8948
+ if (isServerRunning("backend", workspace)) {
8863
8949
  logger.admonition("warning", "Stop Dev Server First", [
8864
8950
  "The development server must be stopped before resetting the database.",
8865
8951
  "Stop the server (Ctrl+C) and run this command again to ensure a clean reset."
@@ -10581,7 +10667,7 @@ import { Command as Command27 } from "commander";
10581
10667
  // src/commands/debug/bundle.ts
10582
10668
  init_src();
10583
10669
  init_constants2();
10584
- import { writeFileSync as writeFileSync7 } from "fs";
10670
+ import { writeFileSync as writeFileSync8 } from "fs";
10585
10671
  import { join as join28 } from "path";
10586
10672
  import { Command as Command26 } from "commander";
10587
10673
  var bundleCommand = new Command26("bundle").description("Bundle and inspect the game backend worker code (for debugging)").option("-o, --output <path>", "Output file path", CLI_DEFAULT_OUTPUTS.WORKER_BUNDLE).option("--minify", "Minify the output").option("--sourcemap", "Include source maps").action(async (options) => {
@@ -10613,7 +10699,7 @@ var bundleCommand = new Command26("bundle").description("Bundle and inspect the
10613
10699
  (result) => `Bundled ${formatSize(result.code.length)}`
10614
10700
  );
10615
10701
  const outputPath = join28(workspace, options.output);
10616
- writeFileSync7(outputPath, bundle.code, "utf-8");
10702
+ writeFileSync8(outputPath, bundle.code, "utf-8");
10617
10703
  logger.success(`Bundle saved to ${options.output}`);
10618
10704
  logger.newLine();
10619
10705
  logger.highlight("Bundle Analysis");
@@ -10691,7 +10777,6 @@ export {
10691
10777
  compareIntegrationKeys,
10692
10778
  confirmDeploymentPlan,
10693
10779
  createClient,
10694
- createDevServerPidFile,
10695
10780
  deployBackendIfNeeded,
10696
10781
  deployExternalGame,
10697
10782
  deployGameBackend,
@@ -10730,7 +10815,6 @@ export {
10730
10815
  getCustomRoutesSize,
10731
10816
  getDeployedGame,
10732
10817
  getDeploymentId,
10733
- getDevServerPidPath,
10734
10818
  getDrizzleKitApiExports,
10735
10819
  getEnvironment,
10736
10820
  getErrorMessage,
@@ -10764,7 +10848,6 @@ export {
10764
10848
  importTypescriptDefault,
10765
10849
  importTypescriptFile,
10766
10850
  integrationChangeDetectors,
10767
- isDevServerRunning,
10768
10851
  listProfiles,
10769
10852
  loadAuthStore,
10770
10853
  loadConfig,
@@ -10782,7 +10865,6 @@ export {
10782
10865
  promptForTimeBackIntegration,
10783
10866
  registerCustomRoutes,
10784
10867
  removeDeployedGame,
10785
- removeDevServerPidFile,
10786
10868
  removeProfile,
10787
10869
  reportCancellation,
10788
10870
  reportDeploymentSuccess,
@@ -1,4 +1,3 @@
1
1
  *.zip
2
- *.pid
3
2
  db
4
3
  kv
package/dist/utils.js CHANGED
@@ -303,16 +303,6 @@ var CLI_DIRECTORIES = {
303
303
  /** KV storage directory within workspace */
304
304
  KV: join(WORKSPACE_NAME, "kv")
305
305
  };
306
- var CLI_FILES = {
307
- /** Auth store file in user config directory */
308
- AUTH_STORE: "auth.json",
309
- /** Games deployment info store */
310
- GAMES_STORE: "games.json",
311
- /** Dev server PID file */
312
- DEV_SERVER_PID: "dev-server.pid",
313
- /** Initial database file (before miniflare) */
314
- INITIAL_DATABASE: "initial.sqlite"
315
- };
316
306
 
317
307
  // src/constants/timeback.ts
318
308
  var CONFIG_FILE_NAMES = [
@@ -567,10 +557,70 @@ function processConfigVariables(config) {
567
557
  }
568
558
 
569
559
  // src/lib/dev/server.ts
570
- import { mkdir as mkdir3 } from "fs/promises";
560
+ import { mkdir as mkdir2 } from "fs/promises";
571
561
  import { join as join7 } from "path";
572
562
  import { Miniflare } from "miniflare";
573
563
 
564
+ // ../utils/src/port.ts
565
+ import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "node:fs";
566
+ import { createServer } from "node:net";
567
+ import { homedir } from "node:os";
568
+ import { join as join2 } from "node:path";
569
+ async function isPortAvailableOnHost(port, host) {
570
+ return new Promise((resolve4) => {
571
+ const server = createServer();
572
+ server.once("error", () => {
573
+ resolve4(false);
574
+ });
575
+ server.once("listening", () => {
576
+ server.close();
577
+ resolve4(true);
578
+ });
579
+ server.listen(port, host);
580
+ });
581
+ }
582
+ async function findAvailablePort(startPort = 4321) {
583
+ const allInterfacesAvailable = await isPortAvailableOnHost(startPort, "0.0.0.0");
584
+ const ipv6Available = await isPortAvailableOnHost(startPort, "::1");
585
+ if (allInterfacesAvailable && ipv6Available) {
586
+ return startPort;
587
+ }
588
+ return findAvailablePort(startPort + 1);
589
+ }
590
+ function getRegistryPath() {
591
+ const home = homedir();
592
+ const dir = join2(home, ".playcademy");
593
+ if (!existsSync2(dir)) {
594
+ mkdirSync(dir, { recursive: true });
595
+ }
596
+ return join2(dir, ".proc");
597
+ }
598
+ function readRegistry() {
599
+ const registryPath = getRegistryPath();
600
+ if (!existsSync2(registryPath)) {
601
+ return {};
602
+ }
603
+ try {
604
+ const content = readFileSync(registryPath, "utf-8");
605
+ return JSON.parse(content);
606
+ } catch {
607
+ return {};
608
+ }
609
+ }
610
+ function writeRegistry(registry) {
611
+ const registryPath = getRegistryPath();
612
+ writeFileSync(registryPath, JSON.stringify(registry, null, 2), "utf-8");
613
+ }
614
+ function getServerKey(type, port) {
615
+ return `${type}-${port}`;
616
+ }
617
+ function writeServerInfo(type, info) {
618
+ const registry = readRegistry();
619
+ const key = getServerKey(type, info.port);
620
+ registry[key] = info;
621
+ writeRegistry(registry);
622
+ }
623
+
574
624
  // src/lib/core/client.ts
575
625
  import { PlaycademyClient } from "@playcademy/sdk";
576
626
 
@@ -816,7 +866,7 @@ var CROSS_MARK = String.fromCodePoint(10006);
816
866
  init_package_json();
817
867
 
818
868
  // src/lib/templates/loader.ts
819
- import { existsSync as existsSync2, readFileSync } from "fs";
869
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
820
870
  import { dirname as dirname3, resolve as resolve3 } from "path";
821
871
  import { fileURLToPath } from "url";
822
872
  var currentDir = dirname3(fileURLToPath(import.meta.url));
@@ -829,8 +879,8 @@ function loadTemplateString(filename) {
829
879
  resolve3(currentDir, "templates", name)
830
880
  ]);
831
881
  for (const candidate of candidatePaths) {
832
- if (existsSync2(candidate)) {
833
- return readFileSync(candidate, "utf-8");
882
+ if (existsSync3(candidate)) {
883
+ return readFileSync2(candidate, "utf-8");
834
884
  }
835
885
  }
836
886
  throw new Error(`Template not found: ${filename}. Searched: ${candidatePaths.join(", ")}`);
@@ -839,12 +889,12 @@ function loadTemplateString(filename) {
839
889
  // src/lib/core/import.ts
840
890
  import { mkdtempSync, rmSync } from "fs";
841
891
  import { tmpdir } from "os";
842
- import { join as join2 } from "path";
892
+ import { join as join3 } from "path";
843
893
  import { pathToFileURL } from "url";
844
894
  import * as esbuild from "esbuild";
845
895
  async function importTypescriptFile(filePath, bundleOptions) {
846
- const tempDir = mkdtempSync(join2(tmpdir(), "playcademy-import-"));
847
- const outFile = join2(tempDir, "bundle.mjs");
896
+ const tempDir = mkdtempSync(join3(tmpdir(), "playcademy-import-"));
897
+ const outFile = join3(tempDir, "bundle.mjs");
848
898
  try {
849
899
  await esbuild.build({
850
900
  entryPoints: [filePath],
@@ -872,8 +922,8 @@ async function importTypescriptDefault(filePath, bundleOptions) {
872
922
  }
873
923
 
874
924
  // src/lib/deploy/bundle.ts
875
- import { existsSync as existsSync3 } from "fs";
876
- import { join as join4 } from "path";
925
+ import { existsSync as existsSync4 } from "fs";
926
+ import { join as join5 } from "path";
877
927
 
878
928
  // ../edge-play/src/entry.ts
879
929
  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";
@@ -1129,7 +1179,7 @@ function textLoaderPlugin() {
1129
1179
  init_file_loader();
1130
1180
  import { mkdir, writeFile } from "fs/promises";
1131
1181
  import { tmpdir as tmpdir2 } from "os";
1132
- import { join as join3, relative } from "path";
1182
+ import { join as join4, relative } from "path";
1133
1183
 
1134
1184
  // src/lib/deploy/hash.ts
1135
1185
  import { createHash } from "crypto";
@@ -1148,7 +1198,7 @@ async function discoverRoutes(apiDir) {
1148
1198
  const routes = await Promise.all(
1149
1199
  files.map(async (file) => {
1150
1200
  const routePath = filePathToRoutePath(file);
1151
- const absolutePath = join3(apiDir, file);
1201
+ const absolutePath = join4(apiDir, file);
1152
1202
  const relativePath = relative(getWorkspace(), absolutePath);
1153
1203
  const methods = await detectExportedMethods(absolutePath);
1154
1204
  return {
@@ -1208,10 +1258,10 @@ async function transpileRoute(filePath) {
1208
1258
  if (!result.outputFiles?.[0]) {
1209
1259
  throw new Error("Transpilation failed: no output");
1210
1260
  }
1211
- const tempDir = join3(tmpdir2(), "playcademy-dev");
1261
+ const tempDir = join4(tmpdir2(), "playcademy-dev");
1212
1262
  await mkdir(tempDir, { recursive: true });
1213
1263
  const hash = hashContent(filePath).slice(0, 12);
1214
- const jsPath = join3(tempDir, `${hash}.mjs`);
1264
+ const jsPath = join4(tempDir, `${hash}.mjs`);
1215
1265
  await writeFile(jsPath, result.outputFiles[0].text);
1216
1266
  return jsPath;
1217
1267
  }
@@ -1222,7 +1272,7 @@ async function discoverCustomRoutes(config) {
1222
1272
  const workspace = getWorkspace();
1223
1273
  const customRoutesConfig = config.integrations?.customRoutes;
1224
1274
  const customRoutesDir = typeof customRoutesConfig === "object" && customRoutesConfig.directory || DEFAULT_API_ROUTES_DIRECTORY;
1225
- const customRoutes = await discoverRoutes(join4(workspace, customRoutesDir));
1275
+ const customRoutes = await discoverRoutes(join5(workspace, customRoutesDir));
1226
1276
  const customRouteData = customRoutes.map((r) => ({
1227
1277
  path: r.path,
1228
1278
  file: r.file,
@@ -1234,15 +1284,15 @@ async function discoverCustomRoutes(config) {
1234
1284
  function resolveEmbeddedSourcePaths() {
1235
1285
  const workspace = getWorkspace();
1236
1286
  const distDir = new URL(".", import.meta.url).pathname;
1237
- const embeddedEdgeSrc = join4(distDir, "edge-play", "src");
1238
- const isBuiltPackage = existsSync3(embeddedEdgeSrc);
1287
+ const embeddedEdgeSrc = join5(distDir, "edge-play", "src");
1288
+ const isBuiltPackage = existsSync4(embeddedEdgeSrc);
1239
1289
  const monorepoRoot = getMonorepoRoot();
1240
- const monorepoEdgeSrc = join4(monorepoRoot, "packages/edge-play/src");
1290
+ const monorepoEdgeSrc = join5(monorepoRoot, "packages/edge-play/src");
1241
1291
  const edgePlaySrc = isBuiltPackage ? embeddedEdgeSrc : monorepoEdgeSrc;
1242
- const cliPackageRoot = isBuiltPackage ? join4(distDir, "../../..") : join4(monorepoRoot, "packages/cli");
1243
- const cliNodeModules = isBuiltPackage ? join4(cliPackageRoot, "node_modules") : monorepoRoot;
1244
- const workspaceNodeModules = join4(workspace, "node_modules");
1245
- const constantsEntry = isBuiltPackage ? join4(embeddedEdgeSrc, "..", "..", "constants", "src", "index.ts") : join4(monorepoRoot, "packages", "constants", "src", "index.ts");
1292
+ const cliPackageRoot = isBuiltPackage ? join5(distDir, "../../..") : join5(monorepoRoot, "packages/cli");
1293
+ const cliNodeModules = isBuiltPackage ? join5(cliPackageRoot, "node_modules") : monorepoRoot;
1294
+ const workspaceNodeModules = join5(workspace, "node_modules");
1295
+ const constantsEntry = isBuiltPackage ? join5(embeddedEdgeSrc, "..", "..", "constants", "src", "index.ts") : join5(monorepoRoot, "packages", "constants", "src", "index.ts");
1246
1296
  return {
1247
1297
  isBuiltPackage,
1248
1298
  edgePlaySrc,
@@ -1302,16 +1352,16 @@ function createEsbuildConfig(entryCode, paths, bundleConfig, customRoutesDir, op
1302
1352
  // │ Example: import * as route from '@game-api/hello.ts' │
1303
1353
  // │ Resolves to: /user-project/server/api/hello.ts │
1304
1354
  // └─────────────────────────────────────────────────────────────────┘
1305
- "@game-api": join4(workspace, customRoutesDir),
1355
+ "@game-api": join5(workspace, customRoutesDir),
1306
1356
  // ┌─ Node.js polyfills for Cloudflare Workers ──────────────────────┐
1307
1357
  // │ Workers don't have fs, path, os, etc. Redirect to polyfills │
1308
1358
  // │ that throw helpful errors if user code tries to use them. │
1309
1359
  // └─────────────────────────────────────────────────────────────────┘
1310
- fs: join4(edgePlaySrc, "polyfills.js"),
1311
- "fs/promises": join4(edgePlaySrc, "polyfills.js"),
1312
- path: join4(edgePlaySrc, "polyfills.js"),
1313
- os: join4(edgePlaySrc, "polyfills.js"),
1314
- process: join4(edgePlaySrc, "polyfills.js")
1360
+ fs: join5(edgePlaySrc, "polyfills.js"),
1361
+ "fs/promises": join5(edgePlaySrc, "polyfills.js"),
1362
+ path: join5(edgePlaySrc, "polyfills.js"),
1363
+ os: join5(edgePlaySrc, "polyfills.js"),
1364
+ process: join5(edgePlaySrc, "polyfills.js")
1315
1365
  },
1316
1366
  // ──── Build Plugins ────
1317
1367
  plugins: [textLoaderPlugin()],
@@ -1378,8 +1428,8 @@ import { checkbox, confirm, input, select } from "@inquirer/prompts";
1378
1428
  import { bold as bold3, cyan as cyan2 } from "colorette";
1379
1429
 
1380
1430
  // src/lib/init/database.ts
1381
- import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
1382
- import { join as join5 } from "path";
1431
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1432
+ import { join as join6 } from "path";
1383
1433
  var drizzleConfigTemplate = loadTemplateString("database/drizzle-config.ts");
1384
1434
  var dbSchemaUsersTemplate = loadTemplateString("database/db-schema-users.ts");
1385
1435
  var dbSchemaScoresTemplate = loadTemplateString("database/db-schema-scores.ts");
@@ -1390,9 +1440,9 @@ var packageTemplate = loadTemplateString("database/package.json");
1390
1440
  var rootGitignoreTemplate = loadTemplateString("gitignore");
1391
1441
  function hasDatabaseSetup() {
1392
1442
  const workspace = getWorkspace();
1393
- const drizzleConfigPath = join5(workspace, "drizzle.config.ts");
1394
- const drizzleConfigJsPath = join5(workspace, "drizzle.config.js");
1395
- return existsSync4(drizzleConfigPath) || existsSync4(drizzleConfigJsPath);
1443
+ const drizzleConfigPath = join6(workspace, "drizzle.config.ts");
1444
+ const drizzleConfigJsPath = join6(workspace, "drizzle.config.js");
1445
+ return existsSync5(drizzleConfigPath) || existsSync5(drizzleConfigJsPath);
1396
1446
  }
1397
1447
 
1398
1448
  // src/lib/init/scaffold.ts
@@ -1409,29 +1459,14 @@ function hasKVSetup(config) {
1409
1459
  return !!config.integrations?.kv;
1410
1460
  }
1411
1461
 
1412
- // src/lib/dev/pid.ts
1413
- import { mkdir as mkdir2, unlink, writeFile as writeFile2 } from "fs/promises";
1414
- import { join as join6 } from "path";
1415
- function getDevServerPidPath() {
1416
- return join6(getWorkspace(), CLI_DIRECTORIES.WORKSPACE, CLI_FILES.DEV_SERVER_PID);
1417
- }
1418
- async function createDevServerPidFile() {
1419
- const pidPath = getDevServerPidFile();
1420
- const pidDir = join6(getWorkspace(), CLI_DIRECTORIES.WORKSPACE);
1421
- await mkdir2(pidDir, { recursive: true });
1422
- await writeFile2(pidPath, process.pid.toString());
1423
- }
1424
- function getDevServerPidFile() {
1425
- return getDevServerPidPath();
1426
- }
1427
-
1428
1462
  // src/lib/dev/server.ts
1429
1463
  async function startDevServer(options) {
1430
1464
  const {
1431
- port,
1465
+ port: preferredPort,
1432
1466
  config: providedConfig,
1433
1467
  platformUrl = process.env.PLAYCADEMY_BASE_URL || "http://localhost:5174"
1434
1468
  } = options;
1469
+ const port = await findAvailablePort(preferredPort);
1435
1470
  const config = providedConfig ?? await loadConfig();
1436
1471
  const hasSandboxTimebackCreds = !!process.env.TIMEBACK_API_CLIENT_ID;
1437
1472
  const devConfig = config.integrations?.timeback && !hasSandboxTimebackCreds ? { ...config, integrations: { ...config.integrations, timeback: void 0 } } : config;
@@ -1466,12 +1501,13 @@ async function startDevServer(options) {
1466
1501
  if (hasDatabase) {
1467
1502
  await initializeDatabase(mf);
1468
1503
  }
1504
+ await writeBackendServerInfo(port);
1469
1505
  return mf;
1470
1506
  }
1471
1507
  async function ensureDatabaseDirectory() {
1472
1508
  const dbDir = join7(getWorkspace(), CLI_DIRECTORIES.DATABASE);
1473
1509
  try {
1474
- await mkdir3(dbDir, { recursive: true });
1510
+ await mkdir2(dbDir, { recursive: true });
1475
1511
  } catch (error) {
1476
1512
  throw new Error(`Failed to create database directory: ${getErrorMessage(error)}`);
1477
1513
  }
@@ -1480,7 +1516,7 @@ async function ensureDatabaseDirectory() {
1480
1516
  async function ensureKvDirectory() {
1481
1517
  const kvDir = join7(getWorkspace(), CLI_DIRECTORIES.KV);
1482
1518
  try {
1483
- await mkdir3(kvDir, { recursive: true });
1519
+ await mkdir2(kvDir, { recursive: true });
1484
1520
  } catch (error) {
1485
1521
  throw new Error(`Failed to create KV directory: ${getErrorMessage(error)}`);
1486
1522
  }
@@ -1489,7 +1525,15 @@ async function ensureKvDirectory() {
1489
1525
  async function initializeDatabase(mf) {
1490
1526
  const d1 = await mf.getD1Database("DB");
1491
1527
  await d1.exec("SELECT 1");
1492
- await createDevServerPidFile();
1528
+ }
1529
+ async function writeBackendServerInfo(port) {
1530
+ writeServerInfo("backend", {
1531
+ pid: process.pid,
1532
+ port,
1533
+ url: `http://localhost:${port}/api`,
1534
+ startedAt: Date.now(),
1535
+ projectRoot: getWorkspace()
1536
+ });
1493
1537
  }
1494
1538
 
1495
1539
  // src/lib/dev/reload.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playcademy",
3
- "version": "0.13.2",
3
+ "version": "0.13.3",
4
4
  "type": "module",
5
5
  "module": "./dist/index.js",
6
6
  "main": "./dist/index.js",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@inquirer/prompts": "^7.8.6",
43
- "@playcademy/sdk": "0.1.5",
43
+ "@playcademy/sdk": "0.1.6",
44
44
  "better-sqlite3": "^12.4.1",
45
45
  "chokidar": "^4.0.3",
46
46
  "colorette": "^2.0.20",