@playcademy/sandbox 0.1.1 → 0.1.2

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.1",
83913
83913
  description: "Local development server for Playcademy game development",
83914
83914
  type: "module",
83915
83915
  exports: {
@@ -117798,7 +117798,10 @@ class CloudflareProvider {
117798
117798
  namespace: this.config.dispatchNamespace
117799
117799
  });
117800
117800
  if (deleteBindings) {
117801
- await this.deleteD1DatabaseIfExists(deploymentId);
117801
+ await Promise.all([
117802
+ this.deleteD1DatabaseIfExists(deploymentId),
117803
+ this.deleteKVNamespaceIfExists(deploymentId)
117804
+ ]);
117802
117805
  }
117803
117806
  } catch (error2) {
117804
117807
  log2.error("[CloudflareProvider] Deletion from dispatch namespace failed", {
@@ -117849,7 +117852,12 @@ class CloudflareProvider {
117849
117852
  }
117850
117853
  }
117851
117854
  for (const kvName of resourceBindings.kv) {
117852
- log2.warn("[CloudflareProvider] KV namespace binding not yet implemented", { kvName });
117855
+ const namespaceId = await this.ensureKVNamespace(kvName);
117856
+ bindings.push({
117857
+ type: "kv_namespace",
117858
+ name: "KV",
117859
+ namespace_id: namespaceId
117860
+ });
117853
117861
  }
117854
117862
  for (const r2Name of resourceBindings.r2) {
117855
117863
  log2.warn("[CloudflareProvider] R2 bucket binding not yet implemented", { r2Name });
@@ -117961,6 +117969,73 @@ class CloudflareProvider {
117961
117969
  throw new Error(`Failed to execute schema: ${error2 instanceof Error ? error2.message : String(error2)}`);
117962
117970
  }
117963
117971
  }
117972
+ async ensureKVNamespace(namespaceName) {
117973
+ log2.debug("[CloudflareProvider] Ensuring KV namespace exists", { namespaceName });
117974
+ try {
117975
+ const namespaces = await this.client.kv.namespaces.list({
117976
+ account_id: this.config.accountId
117977
+ });
117978
+ for await (const ns of namespaces) {
117979
+ if (ns.title === namespaceName && ns.id) {
117980
+ log2.info("[CloudflareProvider] KV namespace already exists", {
117981
+ namespaceName,
117982
+ namespaceId: ns.id
117983
+ });
117984
+ return ns.id;
117985
+ }
117986
+ }
117987
+ log2.debug("[CloudflareProvider] Creating new KV namespace", { namespaceName });
117988
+ const createResult = await this.client.kv.namespaces.create({
117989
+ account_id: this.config.accountId,
117990
+ title: namespaceName
117991
+ });
117992
+ if (!createResult.id) {
117993
+ throw new Error("KV namespace creation succeeded but no ID returned");
117994
+ }
117995
+ log2.info("[CloudflareProvider] KV namespace created successfully", {
117996
+ namespaceName,
117997
+ namespaceId: createResult.id
117998
+ });
117999
+ return createResult.id;
118000
+ } catch (error2) {
118001
+ log2.error("[CloudflareProvider] Failed to ensure KV namespace", {
118002
+ namespaceName,
118003
+ error: error2
118004
+ });
118005
+ throw new Error(`Failed to ensure KV namespace: ${error2 instanceof Error ? error2.message : String(error2)}`);
118006
+ }
118007
+ }
118008
+ async deleteKVNamespaceIfExists(namespaceName) {
118009
+ try {
118010
+ const namespaces = await this.client.kv.namespaces.list({
118011
+ account_id: this.config.accountId
118012
+ });
118013
+ for await (const ns of namespaces) {
118014
+ if (ns.title === namespaceName && ns.id) {
118015
+ log2.debug("[CloudflareProvider] Deleting KV namespace", {
118016
+ namespaceName,
118017
+ namespaceId: ns.id
118018
+ });
118019
+ await this.client.kv.namespaces.delete(ns.id, {
118020
+ account_id: this.config.accountId
118021
+ });
118022
+ log2.info("[CloudflareProvider] KV namespace deleted successfully", {
118023
+ namespaceName,
118024
+ namespaceId: ns.id
118025
+ });
118026
+ return;
118027
+ }
118028
+ }
118029
+ log2.debug("[CloudflareProvider] KV namespace not found, nothing to delete", {
118030
+ namespaceName
118031
+ });
118032
+ } catch (error2) {
118033
+ log2.warn("[CloudflareProvider] Failed to delete KV namespace", {
118034
+ namespaceName,
118035
+ error: error2
118036
+ });
118037
+ }
118038
+ }
117964
118039
  }
117965
118040
  // ../cloudflare/src/utils/hostname.ts
117966
118041
  var RESERVED_SUBDOMAINS = new Set([
@@ -129396,27 +129471,6 @@ async function startServer(port, project, options = {}) {
129396
129471
  }
129397
129472
  var version3 = package_default.version;
129398
129473
 
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
129474
  // node_modules/commander/esm.mjs
129421
129475
  var import__4 = __toESM(require_commander(), 1);
129422
129476
  var {
@@ -129435,6 +129489,92 @@ var {
129435
129489
 
129436
129490
  // src/cli.ts
129437
129491
  var import_picocolors = __toESM(require_picocolors(), 1);
129492
+
129493
+ // ../utils/src/port.ts
129494
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "node:fs";
129495
+ import { createServer } from "node:net";
129496
+ import { homedir } from "node:os";
129497
+ import { join as join3 } from "node:path";
129498
+ async function isPortAvailableOnHost(port, host) {
129499
+ return new Promise((resolve2) => {
129500
+ const server2 = createServer();
129501
+ server2.once("error", () => {
129502
+ resolve2(false);
129503
+ });
129504
+ server2.once("listening", () => {
129505
+ server2.close();
129506
+ resolve2(true);
129507
+ });
129508
+ server2.listen(port, host);
129509
+ });
129510
+ }
129511
+ async function findAvailablePort(startPort = 4321) {
129512
+ const ipv4Available = await isPortAvailableOnHost(startPort, "127.0.0.1");
129513
+ const ipv6Available = await isPortAvailableOnHost(startPort, "::1");
129514
+ if (ipv4Available && ipv6Available) {
129515
+ return startPort;
129516
+ }
129517
+ return findAvailablePort(startPort + 1);
129518
+ }
129519
+ function getRegistryPath() {
129520
+ const home = homedir();
129521
+ const dir = join3(home, ".playcademy");
129522
+ if (!existsSync2(dir)) {
129523
+ mkdirSync2(dir, { recursive: true });
129524
+ }
129525
+ return join3(dir, ".proc");
129526
+ }
129527
+ function readRegistry() {
129528
+ const registryPath = getRegistryPath();
129529
+ if (!existsSync2(registryPath)) {
129530
+ return {};
129531
+ }
129532
+ try {
129533
+ const content = readFileSync(registryPath, "utf-8");
129534
+ return JSON.parse(content);
129535
+ } catch {
129536
+ return {};
129537
+ }
129538
+ }
129539
+ function writeRegistry(registry2) {
129540
+ const registryPath = getRegistryPath();
129541
+ writeFileSync(registryPath, JSON.stringify(registry2, null, 2), "utf-8");
129542
+ }
129543
+ function getServerKey(type, port) {
129544
+ return `${type}-${port}`;
129545
+ }
129546
+ function writeServerInfo(type, info2) {
129547
+ const registry2 = readRegistry();
129548
+ const key = getServerKey(type, info2.port);
129549
+ registry2[key] = info2;
129550
+ writeRegistry(registry2);
129551
+ }
129552
+ function cleanupServerInfo(type, projectRoot, pid) {
129553
+ const registry2 = readRegistry();
129554
+ const keysToRemove = [];
129555
+ for (const [key, info2] of Object.entries(registry2)) {
129556
+ if (key.startsWith(`${type}-`)) {
129557
+ let matches = true;
129558
+ if (projectRoot && info2.projectRoot !== projectRoot) {
129559
+ matches = false;
129560
+ }
129561
+ if (pid !== undefined && info2.pid !== pid) {
129562
+ matches = false;
129563
+ }
129564
+ if (matches) {
129565
+ keysToRemove.push(key);
129566
+ }
129567
+ }
129568
+ }
129569
+ for (const key of keysToRemove) {
129570
+ delete registry2[key];
129571
+ }
129572
+ if (keysToRemove.length > 0) {
129573
+ writeRegistry(registry2);
129574
+ }
129575
+ }
129576
+
129577
+ // src/cli.ts
129438
129578
  var program2 = new Command;
129439
129579
  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
129580
  try {
@@ -129468,6 +129608,13 @@ program2.name("playcademy-sandbox").description("Local development server for Pl
129468
129608
  },
129469
129609
  timeback: timebackOptions
129470
129610
  });
129611
+ writeServerInfo("sandbox", {
129612
+ pid: process.pid,
129613
+ port: availablePort,
129614
+ url: `http://localhost:${availablePort}/api`,
129615
+ startedAt: Date.now(),
129616
+ projectRoot: process.cwd()
129617
+ });
129471
129618
  console.log("");
129472
129619
  console.log(` ${import_picocolors.default.green(import_picocolors.default.bold("PLAYCADEMY"))} ${import_picocolors.default.green(`v${version3}`)}`);
129473
129620
  console.log("");
@@ -129483,10 +129630,13 @@ program2.name("playcademy-sandbox").description("Local development server for Pl
129483
129630
  console.log("");
129484
129631
  console.log(` ${import_picocolors.default.dim("Press")} ${import_picocolors.default.bold("Ctrl+C")} ${import_picocolors.default.dim("to stop the server")}`);
129485
129632
  console.log("");
129486
- process.on("SIGINT", () => {
129633
+ const cleanup = () => {
129634
+ cleanupServerInfo("sandbox", process.cwd(), process.pid);
129487
129635
  servers.stop();
129488
129636
  process.exit(0);
129489
- });
129637
+ };
129638
+ process.on("SIGINT", cleanup);
129639
+ process.on("SIGTERM", cleanup);
129490
129640
  } catch (error2) {
129491
129641
  console.error(import_picocolors.default.red(`❌ Failed to start sandbox: ${error2}`));
129492
129642
  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.1",
82003
82003
  description: "Local development server for Playcademy game development",
82004
82004
  type: "module",
82005
82005
  exports: {
@@ -115888,7 +115888,10 @@ class CloudflareProvider {
115888
115888
  namespace: this.config.dispatchNamespace
115889
115889
  });
115890
115890
  if (deleteBindings) {
115891
- await this.deleteD1DatabaseIfExists(deploymentId);
115891
+ await Promise.all([
115892
+ this.deleteD1DatabaseIfExists(deploymentId),
115893
+ this.deleteKVNamespaceIfExists(deploymentId)
115894
+ ]);
115892
115895
  }
115893
115896
  } catch (error2) {
115894
115897
  log2.error("[CloudflareProvider] Deletion from dispatch namespace failed", {
@@ -115939,7 +115942,12 @@ class CloudflareProvider {
115939
115942
  }
115940
115943
  }
115941
115944
  for (const kvName of resourceBindings.kv) {
115942
- log2.warn("[CloudflareProvider] KV namespace binding not yet implemented", { kvName });
115945
+ const namespaceId = await this.ensureKVNamespace(kvName);
115946
+ bindings.push({
115947
+ type: "kv_namespace",
115948
+ name: "KV",
115949
+ namespace_id: namespaceId
115950
+ });
115943
115951
  }
115944
115952
  for (const r2Name of resourceBindings.r2) {
115945
115953
  log2.warn("[CloudflareProvider] R2 bucket binding not yet implemented", { r2Name });
@@ -116051,6 +116059,73 @@ class CloudflareProvider {
116051
116059
  throw new Error(`Failed to execute schema: ${error2 instanceof Error ? error2.message : String(error2)}`);
116052
116060
  }
116053
116061
  }
116062
+ async ensureKVNamespace(namespaceName) {
116063
+ log2.debug("[CloudflareProvider] Ensuring KV namespace exists", { namespaceName });
116064
+ try {
116065
+ const namespaces = await this.client.kv.namespaces.list({
116066
+ account_id: this.config.accountId
116067
+ });
116068
+ for await (const ns of namespaces) {
116069
+ if (ns.title === namespaceName && ns.id) {
116070
+ log2.info("[CloudflareProvider] KV namespace already exists", {
116071
+ namespaceName,
116072
+ namespaceId: ns.id
116073
+ });
116074
+ return ns.id;
116075
+ }
116076
+ }
116077
+ log2.debug("[CloudflareProvider] Creating new KV namespace", { namespaceName });
116078
+ const createResult = await this.client.kv.namespaces.create({
116079
+ account_id: this.config.accountId,
116080
+ title: namespaceName
116081
+ });
116082
+ if (!createResult.id) {
116083
+ throw new Error("KV namespace creation succeeded but no ID returned");
116084
+ }
116085
+ log2.info("[CloudflareProvider] KV namespace created successfully", {
116086
+ namespaceName,
116087
+ namespaceId: createResult.id
116088
+ });
116089
+ return createResult.id;
116090
+ } catch (error2) {
116091
+ log2.error("[CloudflareProvider] Failed to ensure KV namespace", {
116092
+ namespaceName,
116093
+ error: error2
116094
+ });
116095
+ throw new Error(`Failed to ensure KV namespace: ${error2 instanceof Error ? error2.message : String(error2)}`);
116096
+ }
116097
+ }
116098
+ async deleteKVNamespaceIfExists(namespaceName) {
116099
+ try {
116100
+ const namespaces = await this.client.kv.namespaces.list({
116101
+ account_id: this.config.accountId
116102
+ });
116103
+ for await (const ns of namespaces) {
116104
+ if (ns.title === namespaceName && ns.id) {
116105
+ log2.debug("[CloudflareProvider] Deleting KV namespace", {
116106
+ namespaceName,
116107
+ namespaceId: ns.id
116108
+ });
116109
+ await this.client.kv.namespaces.delete(ns.id, {
116110
+ account_id: this.config.accountId
116111
+ });
116112
+ log2.info("[CloudflareProvider] KV namespace deleted successfully", {
116113
+ namespaceName,
116114
+ namespaceId: ns.id
116115
+ });
116116
+ return;
116117
+ }
116118
+ }
116119
+ log2.debug("[CloudflareProvider] KV namespace not found, nothing to delete", {
116120
+ namespaceName
116121
+ });
116122
+ } catch (error2) {
116123
+ log2.warn("[CloudflareProvider] Failed to delete KV namespace", {
116124
+ namespaceName,
116125
+ error: error2
116126
+ });
116127
+ }
116128
+ }
116054
116129
  }
116055
116130
  // ../cloudflare/src/utils/hostname.ts
116056
116131
  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.2",
4
4
  "description": "Local development server for Playcademy game development",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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
- };