@playcademy/sandbox 0.1.1 → 0.1.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.
package/dist/cli.js CHANGED
@@ -83909,7 +83909,7 @@ var logger = (fn = console.log) => {
83909
83909
  // package.json
83910
83910
  var package_default = {
83911
83911
  name: "@playcademy/sandbox",
83912
- version: "0.1.0",
83912
+ version: "0.1.2",
83913
83913
  description: "Local development server for Playcademy game development",
83914
83914
  type: "module",
83915
83915
  exports: {
@@ -83946,7 +83946,6 @@ var package_default = {
83946
83946
  dependencies: {
83947
83947
  "@electric-sql/pglite": "^0.3.2",
83948
83948
  "@hono/node-server": "^1.14.2",
83949
- "@playcademy/constants": "workspace:*",
83950
83949
  commander: "^12.1.0",
83951
83950
  "drizzle-kit": "^0.31.0",
83952
83951
  "drizzle-orm": "^0.42.0",
@@ -117798,7 +117797,10 @@ class CloudflareProvider {
117798
117797
  namespace: this.config.dispatchNamespace
117799
117798
  });
117800
117799
  if (deleteBindings) {
117801
- await this.deleteD1DatabaseIfExists(deploymentId);
117800
+ await Promise.all([
117801
+ this.deleteD1DatabaseIfExists(deploymentId),
117802
+ this.deleteKVNamespaceIfExists(deploymentId)
117803
+ ]);
117802
117804
  }
117803
117805
  } catch (error2) {
117804
117806
  log2.error("[CloudflareProvider] Deletion from dispatch namespace failed", {
@@ -117849,7 +117851,12 @@ class CloudflareProvider {
117849
117851
  }
117850
117852
  }
117851
117853
  for (const kvName of resourceBindings.kv) {
117852
- log2.warn("[CloudflareProvider] KV namespace binding not yet implemented", { kvName });
117854
+ const namespaceId = await this.ensureKVNamespace(kvName);
117855
+ bindings.push({
117856
+ type: "kv_namespace",
117857
+ name: "KV",
117858
+ namespace_id: namespaceId
117859
+ });
117853
117860
  }
117854
117861
  for (const r2Name of resourceBindings.r2) {
117855
117862
  log2.warn("[CloudflareProvider] R2 bucket binding not yet implemented", { r2Name });
@@ -117961,6 +117968,73 @@ class CloudflareProvider {
117961
117968
  throw new Error(`Failed to execute schema: ${error2 instanceof Error ? error2.message : String(error2)}`);
117962
117969
  }
117963
117970
  }
117971
+ async ensureKVNamespace(namespaceName) {
117972
+ log2.debug("[CloudflareProvider] Ensuring KV namespace exists", { namespaceName });
117973
+ try {
117974
+ const namespaces = await this.client.kv.namespaces.list({
117975
+ account_id: this.config.accountId
117976
+ });
117977
+ for await (const ns of namespaces) {
117978
+ if (ns.title === namespaceName && ns.id) {
117979
+ log2.info("[CloudflareProvider] KV namespace already exists", {
117980
+ namespaceName,
117981
+ namespaceId: ns.id
117982
+ });
117983
+ return ns.id;
117984
+ }
117985
+ }
117986
+ log2.debug("[CloudflareProvider] Creating new KV namespace", { namespaceName });
117987
+ const createResult = await this.client.kv.namespaces.create({
117988
+ account_id: this.config.accountId,
117989
+ title: namespaceName
117990
+ });
117991
+ if (!createResult.id) {
117992
+ throw new Error("KV namespace creation succeeded but no ID returned");
117993
+ }
117994
+ log2.info("[CloudflareProvider] KV namespace created successfully", {
117995
+ namespaceName,
117996
+ namespaceId: createResult.id
117997
+ });
117998
+ return createResult.id;
117999
+ } catch (error2) {
118000
+ log2.error("[CloudflareProvider] Failed to ensure KV namespace", {
118001
+ namespaceName,
118002
+ error: error2
118003
+ });
118004
+ throw new Error(`Failed to ensure KV namespace: ${error2 instanceof Error ? error2.message : String(error2)}`);
118005
+ }
118006
+ }
118007
+ async deleteKVNamespaceIfExists(namespaceName) {
118008
+ try {
118009
+ const namespaces = await this.client.kv.namespaces.list({
118010
+ account_id: this.config.accountId
118011
+ });
118012
+ for await (const ns of namespaces) {
118013
+ if (ns.title === namespaceName && ns.id) {
118014
+ log2.debug("[CloudflareProvider] Deleting KV namespace", {
118015
+ namespaceName,
118016
+ namespaceId: ns.id
118017
+ });
118018
+ await this.client.kv.namespaces.delete(ns.id, {
118019
+ account_id: this.config.accountId
118020
+ });
118021
+ log2.info("[CloudflareProvider] KV namespace deleted successfully", {
118022
+ namespaceName,
118023
+ namespaceId: ns.id
118024
+ });
118025
+ return;
118026
+ }
118027
+ }
118028
+ log2.debug("[CloudflareProvider] KV namespace not found, nothing to delete", {
118029
+ namespaceName
118030
+ });
118031
+ } catch (error2) {
118032
+ log2.warn("[CloudflareProvider] Failed to delete KV namespace", {
118033
+ namespaceName,
118034
+ error: error2
118035
+ });
118036
+ }
118037
+ }
117964
118038
  }
117965
118039
  // ../cloudflare/src/utils/hostname.ts
117966
118040
  var RESERVED_SUBDOMAINS = new Set([
@@ -129396,27 +129470,6 @@ async function startServer(port, project, options = {}) {
129396
129470
  }
129397
129471
  var version3 = package_default.version;
129398
129472
 
129399
- // src/utils/port.ts
129400
- import { createServer } from "node:net";
129401
- async function findAvailablePort(startPort = 4321) {
129402
- return new Promise((resolve2, reject) => {
129403
- const server2 = createServer();
129404
- server2.listen(startPort, () => {
129405
- const address = server2.address();
129406
- const port = address && typeof address === "object" ? address.port : startPort;
129407
- server2.close(() => resolve2(port));
129408
- });
129409
- server2.on("error", () => {
129410
- findAvailablePort(startPort + 1).then(resolve2).catch(reject);
129411
- });
129412
- });
129413
- }
129414
- async function allocateSandboxPorts(preferred = 4321) {
129415
- const apiPort = await findAvailablePort(preferred);
129416
- const realtimePort = await findAvailablePort(apiPort + 1);
129417
- return { apiPort, realtimePort };
129418
- }
129419
-
129420
129473
  // node_modules/commander/esm.mjs
129421
129474
  var import__4 = __toESM(require_commander(), 1);
129422
129475
  var {
@@ -129435,6 +129488,92 @@ var {
129435
129488
 
129436
129489
  // src/cli.ts
129437
129490
  var import_picocolors = __toESM(require_picocolors(), 1);
129491
+
129492
+ // ../utils/src/port.ts
129493
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "node:fs";
129494
+ import { createServer } from "node:net";
129495
+ import { homedir } from "node:os";
129496
+ import { join as join3 } from "node:path";
129497
+ async function isPortAvailableOnHost(port, host) {
129498
+ return new Promise((resolve2) => {
129499
+ const server2 = createServer();
129500
+ server2.once("error", () => {
129501
+ resolve2(false);
129502
+ });
129503
+ server2.once("listening", () => {
129504
+ server2.close();
129505
+ resolve2(true);
129506
+ });
129507
+ server2.listen(port, host);
129508
+ });
129509
+ }
129510
+ async function findAvailablePort(startPort = 4321) {
129511
+ const ipv4Available = await isPortAvailableOnHost(startPort, "127.0.0.1");
129512
+ const ipv6Available = await isPortAvailableOnHost(startPort, "::1");
129513
+ if (ipv4Available && ipv6Available) {
129514
+ return startPort;
129515
+ }
129516
+ return findAvailablePort(startPort + 1);
129517
+ }
129518
+ function getRegistryPath() {
129519
+ const home = homedir();
129520
+ const dir = join3(home, ".playcademy");
129521
+ if (!existsSync2(dir)) {
129522
+ mkdirSync2(dir, { recursive: true });
129523
+ }
129524
+ return join3(dir, ".proc");
129525
+ }
129526
+ function readRegistry() {
129527
+ const registryPath = getRegistryPath();
129528
+ if (!existsSync2(registryPath)) {
129529
+ return {};
129530
+ }
129531
+ try {
129532
+ const content = readFileSync(registryPath, "utf-8");
129533
+ return JSON.parse(content);
129534
+ } catch {
129535
+ return {};
129536
+ }
129537
+ }
129538
+ function writeRegistry(registry2) {
129539
+ const registryPath = getRegistryPath();
129540
+ writeFileSync(registryPath, JSON.stringify(registry2, null, 2), "utf-8");
129541
+ }
129542
+ function getServerKey(type, port) {
129543
+ return `${type}-${port}`;
129544
+ }
129545
+ function writeServerInfo(type, info2) {
129546
+ const registry2 = readRegistry();
129547
+ const key = getServerKey(type, info2.port);
129548
+ registry2[key] = info2;
129549
+ writeRegistry(registry2);
129550
+ }
129551
+ function cleanupServerInfo(type, projectRoot, pid) {
129552
+ const registry2 = readRegistry();
129553
+ const keysToRemove = [];
129554
+ for (const [key, info2] of Object.entries(registry2)) {
129555
+ if (key.startsWith(`${type}-`)) {
129556
+ let matches = true;
129557
+ if (projectRoot && info2.projectRoot !== projectRoot) {
129558
+ matches = false;
129559
+ }
129560
+ if (pid !== undefined && info2.pid !== pid) {
129561
+ matches = false;
129562
+ }
129563
+ if (matches) {
129564
+ keysToRemove.push(key);
129565
+ }
129566
+ }
129567
+ }
129568
+ for (const key of keysToRemove) {
129569
+ delete registry2[key];
129570
+ }
129571
+ if (keysToRemove.length > 0) {
129572
+ writeRegistry(registry2);
129573
+ }
129574
+ }
129575
+
129576
+ // src/cli.ts
129438
129577
  var program2 = new Command;
129439
129578
  program2.name("playcademy-sandbox").description("Local development server for Playcademy game development").version("0.1.0").option("-p, --port <number>", "Port to run the server on", "4321").option("-v, --verbose", "Enable verbose logging", false).option("--project-name <name>", "Name of the current project").option("--project-slug <slug>", "Slug of the current project").option("--realtime", "Enable the realtime server", false).option("--realtime-port <number>", "Port for the realtime server (defaults to main port + 1)").option("--no-seed", "Do not seed the database with demo data").option("--timeback-local", "Use local TimeBack instance").option("--timeback-oneroster-url <url>", "TimeBack OneRoster API URL").option("--timeback-caliper-url <url>", "TimeBack Caliper API URL").option("--timeback-course-id <id>", "TimeBack course ID for seeding").option("--timeback-student-id <id>", "TimeBack student ID for demo user").action(async (options) => {
129440
129579
  try {
@@ -129468,6 +129607,13 @@ program2.name("playcademy-sandbox").description("Local development server for Pl
129468
129607
  },
129469
129608
  timeback: timebackOptions
129470
129609
  });
129610
+ writeServerInfo("sandbox", {
129611
+ pid: process.pid,
129612
+ port: availablePort,
129613
+ url: `http://localhost:${availablePort}/api`,
129614
+ startedAt: Date.now(),
129615
+ projectRoot: process.cwd()
129616
+ });
129471
129617
  console.log("");
129472
129618
  console.log(` ${import_picocolors.default.green(import_picocolors.default.bold("PLAYCADEMY"))} ${import_picocolors.default.green(`v${version3}`)}`);
129473
129619
  console.log("");
@@ -129483,10 +129629,13 @@ program2.name("playcademy-sandbox").description("Local development server for Pl
129483
129629
  console.log("");
129484
129630
  console.log(` ${import_picocolors.default.dim("Press")} ${import_picocolors.default.bold("Ctrl+C")} ${import_picocolors.default.dim("to stop the server")}`);
129485
129631
  console.log("");
129486
- process.on("SIGINT", () => {
129632
+ const cleanup = () => {
129633
+ cleanupServerInfo("sandbox", process.cwd(), process.pid);
129487
129634
  servers.stop();
129488
129635
  process.exit(0);
129489
- });
129636
+ };
129637
+ process.on("SIGINT", cleanup);
129638
+ process.on("SIGTERM", cleanup);
129490
129639
  } catch (error2) {
129491
129640
  console.error(import_picocolors.default.red(`❌ Failed to start sandbox: ${error2}`));
129492
129641
  process.exit(1);
package/dist/server.js CHANGED
@@ -81999,7 +81999,7 @@ var logger = (fn = console.log) => {
81999
81999
  // package.json
82000
82000
  var package_default = {
82001
82001
  name: "@playcademy/sandbox",
82002
- version: "0.1.0",
82002
+ version: "0.1.2",
82003
82003
  description: "Local development server for Playcademy game development",
82004
82004
  type: "module",
82005
82005
  exports: {
@@ -82036,7 +82036,6 @@ var package_default = {
82036
82036
  dependencies: {
82037
82037
  "@electric-sql/pglite": "^0.3.2",
82038
82038
  "@hono/node-server": "^1.14.2",
82039
- "@playcademy/constants": "workspace:*",
82040
82039
  commander: "^12.1.0",
82041
82040
  "drizzle-kit": "^0.31.0",
82042
82041
  "drizzle-orm": "^0.42.0",
@@ -115888,7 +115887,10 @@ class CloudflareProvider {
115888
115887
  namespace: this.config.dispatchNamespace
115889
115888
  });
115890
115889
  if (deleteBindings) {
115891
- await this.deleteD1DatabaseIfExists(deploymentId);
115890
+ await Promise.all([
115891
+ this.deleteD1DatabaseIfExists(deploymentId),
115892
+ this.deleteKVNamespaceIfExists(deploymentId)
115893
+ ]);
115892
115894
  }
115893
115895
  } catch (error2) {
115894
115896
  log2.error("[CloudflareProvider] Deletion from dispatch namespace failed", {
@@ -115939,7 +115941,12 @@ class CloudflareProvider {
115939
115941
  }
115940
115942
  }
115941
115943
  for (const kvName of resourceBindings.kv) {
115942
- log2.warn("[CloudflareProvider] KV namespace binding not yet implemented", { kvName });
115944
+ const namespaceId = await this.ensureKVNamespace(kvName);
115945
+ bindings.push({
115946
+ type: "kv_namespace",
115947
+ name: "KV",
115948
+ namespace_id: namespaceId
115949
+ });
115943
115950
  }
115944
115951
  for (const r2Name of resourceBindings.r2) {
115945
115952
  log2.warn("[CloudflareProvider] R2 bucket binding not yet implemented", { r2Name });
@@ -116051,6 +116058,73 @@ class CloudflareProvider {
116051
116058
  throw new Error(`Failed to execute schema: ${error2 instanceof Error ? error2.message : String(error2)}`);
116052
116059
  }
116053
116060
  }
116061
+ async ensureKVNamespace(namespaceName) {
116062
+ log2.debug("[CloudflareProvider] Ensuring KV namespace exists", { namespaceName });
116063
+ try {
116064
+ const namespaces = await this.client.kv.namespaces.list({
116065
+ account_id: this.config.accountId
116066
+ });
116067
+ for await (const ns of namespaces) {
116068
+ if (ns.title === namespaceName && ns.id) {
116069
+ log2.info("[CloudflareProvider] KV namespace already exists", {
116070
+ namespaceName,
116071
+ namespaceId: ns.id
116072
+ });
116073
+ return ns.id;
116074
+ }
116075
+ }
116076
+ log2.debug("[CloudflareProvider] Creating new KV namespace", { namespaceName });
116077
+ const createResult = await this.client.kv.namespaces.create({
116078
+ account_id: this.config.accountId,
116079
+ title: namespaceName
116080
+ });
116081
+ if (!createResult.id) {
116082
+ throw new Error("KV namespace creation succeeded but no ID returned");
116083
+ }
116084
+ log2.info("[CloudflareProvider] KV namespace created successfully", {
116085
+ namespaceName,
116086
+ namespaceId: createResult.id
116087
+ });
116088
+ return createResult.id;
116089
+ } catch (error2) {
116090
+ log2.error("[CloudflareProvider] Failed to ensure KV namespace", {
116091
+ namespaceName,
116092
+ error: error2
116093
+ });
116094
+ throw new Error(`Failed to ensure KV namespace: ${error2 instanceof Error ? error2.message : String(error2)}`);
116095
+ }
116096
+ }
116097
+ async deleteKVNamespaceIfExists(namespaceName) {
116098
+ try {
116099
+ const namespaces = await this.client.kv.namespaces.list({
116100
+ account_id: this.config.accountId
116101
+ });
116102
+ for await (const ns of namespaces) {
116103
+ if (ns.title === namespaceName && ns.id) {
116104
+ log2.debug("[CloudflareProvider] Deleting KV namespace", {
116105
+ namespaceName,
116106
+ namespaceId: ns.id
116107
+ });
116108
+ await this.client.kv.namespaces.delete(ns.id, {
116109
+ account_id: this.config.accountId
116110
+ });
116111
+ log2.info("[CloudflareProvider] KV namespace deleted successfully", {
116112
+ namespaceName,
116113
+ namespaceId: ns.id
116114
+ });
116115
+ return;
116116
+ }
116117
+ }
116118
+ log2.debug("[CloudflareProvider] KV namespace not found, nothing to delete", {
116119
+ namespaceName
116120
+ });
116121
+ } catch (error2) {
116122
+ log2.warn("[CloudflareProvider] Failed to delete KV namespace", {
116123
+ namespaceName,
116124
+ error: error2
116125
+ });
116126
+ }
116127
+ }
116054
116128
  }
116055
116129
  // ../cloudflare/src/utils/hostname.ts
116056
116130
  var RESERVED_SUBDOMAINS = new Set([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sandbox",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Local development server for Playcademy game development",
5
5
  "type": "module",
6
6
  "exports": {
@@ -37,7 +37,6 @@
37
37
  "dependencies": {
38
38
  "@electric-sql/pglite": "^0.3.2",
39
39
  "@hono/node-server": "^1.14.2",
40
- "@playcademy/constants": "0.0.1",
41
40
  "commander": "^12.1.0",
42
41
  "drizzle-kit": "^0.31.0",
43
42
  "drizzle-orm": "^0.42.0",
@@ -1,18 +0,0 @@
1
- export declare function findAvailablePort(startPort?: number): Promise<number>;
2
- /**
3
- * Allocate a pair of ports for the sandbox: one for the main HTTP API server
4
- * and one for the realtime WebSocket server.
5
- *
6
- * • We first look for an available port starting from `preferred` (defaults to
7
- * 4321). That becomes the API port.
8
- * • We then look for the next available port **after** the chosen API port for
9
- * the realtime server. This guarantees the two ports are distinct and
10
- * stable for the lifetime of the sandbox.
11
- *
12
- * The helper centralises the port-allocation logic so that the sandbox CLI,
13
- * integration-test harness and Vite plugin all follow the exact same rules.
14
- */
15
- export declare function allocateSandboxPorts(preferred?: number): Promise<{
16
- apiPort: number;
17
- realtimePort: number;
18
- }>;
@@ -1,54 +0,0 @@
1
- import { createRequire } from "node:module";
2
- var __create = Object.create;
3
- var __getProtoOf = Object.getPrototypeOf;
4
- var __defProp = Object.defineProperty;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __toESM = (mod, isNodeMode, target) => {
8
- target = mod != null ? __create(__getProtoOf(mod)) : {};
9
- const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
- for (let key of __getOwnPropNames(mod))
11
- if (!__hasOwnProp.call(to, key))
12
- __defProp(to, key, {
13
- get: () => mod[key],
14
- enumerable: true
15
- });
16
- return to;
17
- };
18
- var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
19
- var __export = (target, all) => {
20
- for (var name in all)
21
- __defProp(target, name, {
22
- get: all[name],
23
- enumerable: true,
24
- configurable: true,
25
- set: (newValue) => all[name] = () => newValue
26
- });
27
- };
28
- var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
29
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
30
-
31
- // src/utils/port.ts
32
- import { createServer } from "node:net";
33
- async function findAvailablePort(startPort = 4321) {
34
- return new Promise((resolve, reject) => {
35
- const server = createServer();
36
- server.listen(startPort, () => {
37
- const address = server.address();
38
- const port = address && typeof address === "object" ? address.port : startPort;
39
- server.close(() => resolve(port));
40
- });
41
- server.on("error", () => {
42
- findAvailablePort(startPort + 1).then(resolve).catch(reject);
43
- });
44
- });
45
- }
46
- async function allocateSandboxPorts(preferred = 4321) {
47
- const apiPort = await findAvailablePort(preferred);
48
- const realtimePort = await findAvailablePort(apiPort + 1);
49
- return { apiPort, realtimePort };
50
- }
51
- export {
52
- findAvailablePort,
53
- allocateSandboxPorts
54
- };