playcademy 0.12.9 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2118,15 +2118,19 @@ var init_http_server = __esm({
2118
2118
  });
2119
2119
 
2120
2120
  // src/constants/paths.ts
2121
- var CLI_DIRECTORIES, CLI_USER_DIRECTORIES, CLI_DEFAULT_OUTPUTS, CLI_FILES;
2121
+ import { join as join2 } from "path";
2122
+ var WORKSPACE_NAME, CLI_DIRECTORIES, CLI_USER_DIRECTORIES, CLI_DEFAULT_OUTPUTS, CLI_FILES;
2122
2123
  var init_paths = __esm({
2123
2124
  "src/constants/paths.ts"() {
2124
2125
  "use strict";
2126
+ WORKSPACE_NAME = ".playcademy";
2125
2127
  CLI_DIRECTORIES = {
2126
2128
  /** Root directory for CLI artifacts in workspace */
2127
- WORKSPACE: ".playcademy",
2129
+ WORKSPACE: WORKSPACE_NAME,
2128
2130
  /** Database directory within workspace */
2129
- DATABASE: ".playcademy/db"
2131
+ DATABASE: join2(WORKSPACE_NAME, "db"),
2132
+ /** KV storage directory within workspace */
2133
+ KV: join2(WORKSPACE_NAME, "kv")
2130
2134
  };
2131
2135
  CLI_USER_DIRECTORIES = {
2132
2136
  /** User config directory for auth, games store, etc. */
@@ -2423,12 +2427,12 @@ var init_config = __esm({
2423
2427
  // src/lib/auth/storage.ts
2424
2428
  import { access, mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
2425
2429
  import { homedir } from "node:os";
2426
- import { join as join2 } from "node:path";
2430
+ import { join as join3 } from "node:path";
2427
2431
  function getAuthPath() {
2428
- return join2(homedir(), CLI_USER_DIRECTORIES.CONFIG, CLI_FILES.AUTH_STORE);
2432
+ return join3(homedir(), CLI_USER_DIRECTORIES.CONFIG, CLI_FILES.AUTH_STORE);
2429
2433
  }
2430
2434
  async function ensureAuthDir() {
2431
- const authDir = join2(homedir(), CLI_USER_DIRECTORIES.CONFIG);
2435
+ const authDir = join3(homedir(), CLI_USER_DIRECTORIES.CONFIG);
2432
2436
  await mkdir(authDir, { recursive: true });
2433
2437
  }
2434
2438
  function createEmptyEnvironmentProfiles() {
@@ -2584,7 +2588,7 @@ async function findConfigPath(configPath) {
2584
2588
  throw new ConfigError(
2585
2589
  "No Playcademy config file found in this directory or any parent directory",
2586
2590
  void 0,
2587
- `Create one of: ${CONFIG_FILE_NAMES.join(", ")} or set PLAYCADEMY_CONFIG_PATH environment variable`
2591
+ `Run 'playcademy init' to create a config file, or manually create one of: ${CONFIG_FILE_NAMES.join(", ")}`
2588
2592
  );
2589
2593
  }
2590
2594
  return result.path;
@@ -3137,7 +3141,7 @@ function loadTemplate(filename) {
3137
3141
  return loadTemplateString(`config/${filename}`);
3138
3142
  }
3139
3143
  function generateJsConfig(options) {
3140
- const { name, description, emoji, customRoutesDirectory, databaseDirectory, timeback } = options;
3144
+ const { name, description, emoji, customRoutesDirectory, databaseDirectory, kv, timeback } = options;
3141
3145
  let template = loadTemplate("playcademy.config.js.template");
3142
3146
  template = template.replace("{{GAME_NAME}}", name);
3143
3147
  const descriptionLine = description ? `
@@ -3153,6 +3157,9 @@ function generateJsConfig(options) {
3153
3157
  if (databaseDirectory) {
3154
3158
  integrationsParts.push(` database: { directory: '${databaseDirectory}' }`);
3155
3159
  }
3160
+ if (kv) {
3161
+ integrationsParts.push(` kv: true`);
3162
+ }
3156
3163
  if (timeback) {
3157
3164
  let timebackTemplate = loadTemplate("timeback-config.js.template");
3158
3165
  timebackTemplate = timebackTemplate.replace(
@@ -3179,7 +3186,7 @@ function generateJsConfig(options) {
3179
3186
  return template;
3180
3187
  }
3181
3188
  function generateJsonConfig(options) {
3182
- const { name, description, emoji, customRoutesDirectory, databaseDirectory, timeback } = options;
3189
+ const { name, description, emoji, customRoutesDirectory, databaseDirectory, kv, timeback } = options;
3183
3190
  let template = loadTemplate("playcademy.config.json.template");
3184
3191
  template = template.replace("{{GAME_NAME}}", name);
3185
3192
  const descriptionLine = description ? `,
@@ -3195,6 +3202,9 @@ function generateJsonConfig(options) {
3195
3202
  if (databaseDirectory) {
3196
3203
  integrationsConfig.database = { directory: databaseDirectory };
3197
3204
  }
3205
+ if (kv) {
3206
+ integrationsConfig.kv = true;
3207
+ }
3198
3208
  if (timeback) {
3199
3209
  const courseConfig = {
3200
3210
  subjects: timeback.subjects,
@@ -3613,12 +3623,12 @@ var init_errors = __esm({
3613
3623
  // src/lib/core/import.ts
3614
3624
  import { mkdtempSync, rmSync } from "fs";
3615
3625
  import { tmpdir } from "os";
3616
- import { join as join3 } from "path";
3626
+ import { join as join4 } from "path";
3617
3627
  import { pathToFileURL } from "url";
3618
3628
  import * as esbuild from "esbuild";
3619
3629
  async function importTypescriptFile(filePath, bundleOptions) {
3620
- const tempDir = mkdtempSync(join3(tmpdir(), "playcademy-import-"));
3621
- const outFile = join3(tempDir, "bundle.mjs");
3630
+ const tempDir = mkdtempSync(join4(tmpdir(), "playcademy-import-"));
3631
+ const outFile = join4(tempDir, "bundle.mjs");
3622
3632
  try {
3623
3633
  await esbuild.build({
3624
3634
  entryPoints: [filePath],
@@ -4519,7 +4529,7 @@ init_core();
4519
4529
  // src/lib/db/path.ts
4520
4530
  init_constants2();
4521
4531
  import { copyFileSync, existsSync as existsSync5, mkdirSync, readdirSync as readdirSync2, unlinkSync } from "fs";
4522
- import { join as join4 } from "path";
4532
+ import { join as join5 } from "path";
4523
4533
  import Database from "better-sqlite3";
4524
4534
  var DB_DIRECTORY = CLI_DIRECTORIES.DATABASE;
4525
4535
  var INITIAL_DB_NAME = CLI_FILES.INITIAL_DATABASE;
@@ -4533,11 +4543,11 @@ var createEmptyDatabase = (path2) => {
4533
4543
  db.close();
4534
4544
  };
4535
4545
  var findMiniflareDatabase = (dbDir) => {
4536
- const miniflareDir = join4(dbDir, "miniflare-D1DatabaseObject");
4546
+ const miniflareDir = join5(dbDir, "miniflare-D1DatabaseObject");
4537
4547
  if (!existsSync5(miniflareDir)) return null;
4538
4548
  const sqliteFiles = readdirSync2(miniflareDir).filter((file) => file.endsWith(".sqlite"));
4539
4549
  if (sqliteFiles.length === 0) return null;
4540
- return join4(miniflareDir, sqliteFiles[0]);
4550
+ return join5(miniflareDir, sqliteFiles[0]);
4541
4551
  };
4542
4552
  var migrateInitialDbToTarget = (initialPath, targetPath) => {
4543
4553
  if (!existsSync5(initialPath)) return;
@@ -4545,7 +4555,7 @@ var migrateInitialDbToTarget = (initialPath, targetPath) => {
4545
4555
  unlinkSync(initialPath);
4546
4556
  };
4547
4557
  function getDevDbPath() {
4548
- const initialDbPath = join4(DB_DIRECTORY, INITIAL_DB_NAME);
4558
+ const initialDbPath = join5(DB_DIRECTORY, INITIAL_DB_NAME);
4549
4559
  ensureDirectoryExists(DB_DIRECTORY);
4550
4560
  const miniflareDbPath = findMiniflareDatabase(DB_DIRECTORY);
4551
4561
  if (miniflareDbPath) {
@@ -4605,17 +4615,16 @@ init_src();
4605
4615
  init_constants2();
4606
4616
  init_core();
4607
4617
  import { existsSync as existsSync10 } from "node:fs";
4608
- import { join as join10 } from "node:path";
4618
+ import { join as join11 } from "node:path";
4609
4619
 
4610
4620
  // src/lib/init/database.ts
4611
4621
  init_log();
4612
4622
  init_slug();
4613
- init_constants2();
4614
4623
  init_core();
4615
4624
  init_logger();
4616
4625
  init_loader2();
4617
4626
  import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
4618
- import { join as join5 } from "path";
4627
+ import { join as join6 } from "path";
4619
4628
  var drizzleConfigTemplate = loadTemplateString("database/drizzle-config.ts");
4620
4629
  var dbSchemaUsersTemplate = loadTemplateString("database/db-schema-users.ts");
4621
4630
  var dbSchemaScoresTemplate = loadTemplateString("database/db-schema-scores.ts");
@@ -4623,13 +4632,12 @@ var dbSchemaIndexTemplate = loadTemplateString("database/db-schema-index.ts");
4623
4632
  var dbIndexTemplate = loadTemplateString("database/db-index.ts");
4624
4633
  var dbTypesTemplate = loadTemplateString("database/db-types.ts");
4625
4634
  var packageTemplate = loadTemplateString("database/package.json");
4626
- var playcademyGitignoreTemplate = loadTemplateString("playcademy-gitignore");
4627
4635
  var rootGitignoreTemplate = loadTemplateString("gitignore");
4628
4636
  function normalizeGitignoreEntry(entry) {
4629
4637
  return entry.replace(/^\/+/, "").replace(/\/+$/, "");
4630
4638
  }
4631
4639
  function ensureGitignoreEntries(workspace) {
4632
- const gitignorePath = join5(workspace, ".gitignore");
4640
+ const gitignorePath = join6(workspace, ".gitignore");
4633
4641
  if (!existsSync6(gitignorePath)) {
4634
4642
  writeFileSync2(gitignorePath, rootGitignoreTemplate);
4635
4643
  return;
@@ -4659,32 +4667,26 @@ async function scaffoldDatabaseSetup(options) {
4659
4667
  await runStep(
4660
4668
  "Configuring database...",
4661
4669
  async () => {
4662
- const dbDir = join5(workspace, "db");
4663
- const schemaDir = join5(dbDir, "schema");
4670
+ const dbDir = join6(workspace, "db");
4671
+ const schemaDir = join6(dbDir, "schema");
4664
4672
  if (!existsSync6(dbDir)) {
4665
4673
  mkdirSync2(dbDir, { recursive: true });
4666
4674
  }
4667
4675
  if (!existsSync6(schemaDir)) {
4668
4676
  mkdirSync2(schemaDir, { recursive: true });
4669
4677
  }
4670
- const usersSchemaPath = join5(schemaDir, "users.ts");
4678
+ const usersSchemaPath = join6(schemaDir, "users.ts");
4671
4679
  writeFileSync2(usersSchemaPath, dbSchemaUsersTemplate);
4672
- const scoresSchemaPath = join5(schemaDir, "scores.ts");
4680
+ const scoresSchemaPath = join6(schemaDir, "scores.ts");
4673
4681
  writeFileSync2(scoresSchemaPath, dbSchemaScoresTemplate);
4674
- const schemaIndexPath = join5(schemaDir, "index.ts");
4682
+ const schemaIndexPath = join6(schemaDir, "index.ts");
4675
4683
  writeFileSync2(schemaIndexPath, dbSchemaIndexTemplate);
4676
- const dbIndexPath = join5(dbDir, "index.ts");
4684
+ const dbIndexPath = join6(dbDir, "index.ts");
4677
4685
  writeFileSync2(dbIndexPath, dbIndexTemplate);
4678
- const dbTypesPath = join5(dbDir, "types.ts");
4686
+ const dbTypesPath = join6(dbDir, "types.ts");
4679
4687
  writeFileSync2(dbTypesPath, dbTypesTemplate);
4680
- const drizzleConfigPath = join5(workspace, "drizzle.config.ts");
4688
+ const drizzleConfigPath = join6(workspace, "drizzle.config.ts");
4681
4689
  writeFileSync2(drizzleConfigPath, drizzleConfigTemplate);
4682
- const playcademyDir = join5(workspace, CLI_DIRECTORIES.WORKSPACE);
4683
- if (!existsSync6(playcademyDir)) {
4684
- mkdirSync2(playcademyDir, { recursive: true });
4685
- }
4686
- const playcademyGitignorePath = join5(playcademyDir, ".gitignore");
4687
- writeFileSync2(playcademyGitignorePath, playcademyGitignoreTemplate);
4688
4690
  ensureGitignoreEntries(workspace);
4689
4691
  packagesUpdated = await setupPackageJson(workspace, options.gameName);
4690
4692
  },
@@ -4693,7 +4695,7 @@ async function scaffoldDatabaseSetup(options) {
4693
4695
  return packagesUpdated;
4694
4696
  }
4695
4697
  async function setupPackageJson(workspace, gameName) {
4696
- const pkgPath = join5(workspace, "package.json");
4698
+ const pkgPath = join6(workspace, "package.json");
4697
4699
  const dbDeps = {
4698
4700
  "drizzle-orm": "^0.42.0",
4699
4701
  "better-sqlite3": "^12.0.0"
@@ -4733,11 +4735,16 @@ async function setupPackageJson(workspace, gameName) {
4733
4735
  }
4734
4736
  function hasDatabaseSetup() {
4735
4737
  const workspace = getWorkspace();
4736
- const drizzleConfigPath = join5(workspace, "drizzle.config.ts");
4737
- const drizzleConfigJsPath = join5(workspace, "drizzle.config.js");
4738
+ const drizzleConfigPath = join6(workspace, "drizzle.config.ts");
4739
+ const drizzleConfigJsPath = join6(workspace, "drizzle.config.js");
4738
4740
  return existsSync6(drizzleConfigPath) || existsSync6(drizzleConfigJsPath);
4739
4741
  }
4740
4742
 
4743
+ // src/lib/init/kv.ts
4744
+ function hasKVSetup(config) {
4745
+ return !!config.integrations?.kv;
4746
+ }
4747
+
4741
4748
  // src/lib/integrations/timeback.ts
4742
4749
  init_src();
4743
4750
  init_core();
@@ -4868,7 +4875,7 @@ var integrationChangeDetectors = {
4868
4875
 
4869
4876
  // src/lib/deploy/bundle.ts
4870
4877
  import { existsSync as existsSync7 } from "fs";
4871
- import { join as join7 } from "path";
4878
+ import { join as join8 } from "path";
4872
4879
 
4873
4880
  // ../edge-play/src/entry.ts
4874
4881
  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";
@@ -5131,7 +5138,7 @@ init_file_loader();
5131
5138
  init_core();
5132
5139
  import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
5133
5140
  import { tmpdir as tmpdir2 } from "os";
5134
- import { join as join6, relative } from "path";
5141
+ import { join as join7, relative } from "path";
5135
5142
 
5136
5143
  // src/lib/deploy/hash.ts
5137
5144
  init_file_loader();
@@ -5179,7 +5186,7 @@ async function discoverRoutes(apiDir) {
5179
5186
  const routes = await Promise.all(
5180
5187
  files.map(async (file) => {
5181
5188
  const routePath = filePathToRoutePath(file);
5182
- const absolutePath = join6(apiDir, file);
5189
+ const absolutePath = join7(apiDir, file);
5183
5190
  const relativePath = relative(getWorkspace(), absolutePath);
5184
5191
  const methods = await detectExportedMethods(absolutePath);
5185
5192
  return {
@@ -5239,10 +5246,10 @@ async function transpileRoute(filePath) {
5239
5246
  if (!result.outputFiles?.[0]) {
5240
5247
  throw new Error("Transpilation failed: no output");
5241
5248
  }
5242
- const tempDir = join6(tmpdir2(), "playcademy-dev");
5249
+ const tempDir = join7(tmpdir2(), "playcademy-dev");
5243
5250
  await mkdir2(tempDir, { recursive: true });
5244
5251
  const hash = hashContent(filePath).slice(0, 12);
5245
- const jsPath = join6(tempDir, `${hash}.mjs`);
5252
+ const jsPath = join7(tempDir, `${hash}.mjs`);
5246
5253
  await writeFile2(jsPath, result.outputFiles[0].text);
5247
5254
  return jsPath;
5248
5255
  }
@@ -5270,7 +5277,7 @@ async function discoverCustomRoutes(config) {
5270
5277
  const workspace = getWorkspace();
5271
5278
  const customRoutesConfig = config.integrations?.customRoutes;
5272
5279
  const customRoutesDir = typeof customRoutesConfig === "object" && customRoutesConfig.directory || DEFAULT_API_ROUTES_DIRECTORY;
5273
- const customRoutes = await discoverRoutes(join7(workspace, customRoutesDir));
5280
+ const customRoutes = await discoverRoutes(join8(workspace, customRoutesDir));
5274
5281
  const customRouteData = customRoutes.map((r) => ({
5275
5282
  path: r.path,
5276
5283
  file: r.file,
@@ -5282,15 +5289,15 @@ async function discoverCustomRoutes(config) {
5282
5289
  function resolveEmbeddedSourcePaths() {
5283
5290
  const workspace = getWorkspace();
5284
5291
  const distDir = new URL(".", import.meta.url).pathname;
5285
- const embeddedEdgeSrc = join7(distDir, "edge-play", "src");
5292
+ const embeddedEdgeSrc = join8(distDir, "edge-play", "src");
5286
5293
  const isBuiltPackage = existsSync7(embeddedEdgeSrc);
5287
5294
  const monorepoRoot = getMonorepoRoot();
5288
- const monorepoEdgeSrc = join7(monorepoRoot, "packages/edge-play/src");
5295
+ const monorepoEdgeSrc = join8(monorepoRoot, "packages/edge-play/src");
5289
5296
  const edgePlaySrc = isBuiltPackage ? embeddedEdgeSrc : monorepoEdgeSrc;
5290
- const cliPackageRoot = isBuiltPackage ? join7(distDir, "../../..") : join7(monorepoRoot, "packages/cli");
5291
- const cliNodeModules = isBuiltPackage ? join7(cliPackageRoot, "node_modules") : monorepoRoot;
5292
- const workspaceNodeModules = join7(workspace, "node_modules");
5293
- const constantsEntry = isBuiltPackage ? join7(embeddedEdgeSrc, "..", "..", "constants", "src", "index.ts") : join7(monorepoRoot, "packages", "constants", "src", "index.ts");
5297
+ const cliPackageRoot = isBuiltPackage ? join8(distDir, "../../..") : join8(monorepoRoot, "packages/cli");
5298
+ const cliNodeModules = isBuiltPackage ? join8(cliPackageRoot, "node_modules") : monorepoRoot;
5299
+ const workspaceNodeModules = join8(workspace, "node_modules");
5300
+ const constantsEntry = isBuiltPackage ? join8(embeddedEdgeSrc, "..", "..", "constants", "src", "index.ts") : join8(monorepoRoot, "packages", "constants", "src", "index.ts");
5294
5301
  return {
5295
5302
  isBuiltPackage,
5296
5303
  edgePlaySrc,
@@ -5350,16 +5357,16 @@ function createEsbuildConfig(entryCode, paths, bundleConfig, customRoutesDir, op
5350
5357
  // │ Example: import * as route from '@game-api/hello.ts' │
5351
5358
  // │ Resolves to: /user-project/server/api/hello.ts │
5352
5359
  // └─────────────────────────────────────────────────────────────────┘
5353
- "@game-api": join7(workspace, customRoutesDir),
5360
+ "@game-api": join8(workspace, customRoutesDir),
5354
5361
  // ┌─ Node.js polyfills for Cloudflare Workers ──────────────────────┐
5355
5362
  // │ Workers don't have fs, path, os, etc. Redirect to polyfills │
5356
5363
  // │ that throw helpful errors if user code tries to use them. │
5357
5364
  // └─────────────────────────────────────────────────────────────────┘
5358
- fs: join7(edgePlaySrc, "polyfills.js"),
5359
- "fs/promises": join7(edgePlaySrc, "polyfills.js"),
5360
- path: join7(edgePlaySrc, "polyfills.js"),
5361
- os: join7(edgePlaySrc, "polyfills.js"),
5362
- process: join7(edgePlaySrc, "polyfills.js")
5365
+ fs: join8(edgePlaySrc, "polyfills.js"),
5366
+ "fs/promises": join8(edgePlaySrc, "polyfills.js"),
5367
+ path: join8(edgePlaySrc, "polyfills.js"),
5368
+ os: join8(edgePlaySrc, "polyfills.js"),
5369
+ process: join8(edgePlaySrc, "polyfills.js")
5363
5370
  },
5364
5371
  // ──── Build Plugins ────
5365
5372
  plugins: [textLoaderPlugin()],
@@ -5425,7 +5432,7 @@ function generateEntryCode(customRoutes, customRoutesDir) {
5425
5432
  init_core();
5426
5433
  import { existsSync as existsSync9 } from "fs";
5427
5434
  import { createRequire } from "module";
5428
- import { join as join9 } from "path";
5435
+ import { join as join10 } from "path";
5429
5436
 
5430
5437
  // src/lib/init/prompts.ts
5431
5438
  init_constants3();
@@ -5435,20 +5442,29 @@ import { bold as bold3, cyan as cyan2 } from "colorette";
5435
5442
 
5436
5443
  // src/lib/init/scaffold.ts
5437
5444
  init_src();
5445
+ init_constants2();
5438
5446
  init_core();
5439
5447
  init_loader2();
5440
5448
  import { existsSync as existsSync8, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
5441
- import { join as join8, resolve as resolve6 } from "path";
5442
- var sampleRouteTemplate = loadTemplateString("api/sample-route.ts");
5443
- var sampleRouteWithDbTemplate = loadTemplateString("api/sample-route-with-db.ts");
5444
- async function scaffoldApiDirectory(apiDirectory, sampleRouteTemplate2) {
5449
+ import { join as join9, resolve as resolve6 } from "path";
5450
+ var sampleCustomRouteTemplate = loadTemplateString("api/sample-custom.ts");
5451
+ var sampleDatabaseRouteTemplate = loadTemplateString("api/sample-database.ts");
5452
+ var sampleKvRouteTemplate = loadTemplateString("api/sample-kv.ts");
5453
+ var playcademyGitignoreTemplate = loadTemplateString("playcademy-gitignore");
5454
+ async function scaffoldApiDirectory(apiDirectory, sampleRoutes) {
5445
5455
  const apiPath = resolve6(getWorkspace(), apiDirectory);
5456
+ const samplePath = join9(apiPath, "sample");
5446
5457
  await runStep(
5447
5458
  "Scaffolding API directory",
5448
5459
  async () => {
5449
5460
  if (!existsSync8(apiPath)) {
5450
5461
  mkdirSync3(apiPath, { recursive: true });
5451
- writeFileSync3(join8(apiPath, "hello.ts"), sampleRouteTemplate2, "utf-8");
5462
+ }
5463
+ if (!existsSync8(samplePath)) {
5464
+ mkdirSync3(samplePath, { recursive: true });
5465
+ }
5466
+ for (const route of sampleRoutes) {
5467
+ writeFileSync3(join9(samplePath, route.filename), route.template, "utf-8");
5452
5468
  }
5453
5469
  },
5454
5470
  "API directory scaffolded"
@@ -5461,10 +5477,29 @@ function validateApiDirectoryDoesNotExist(value) {
5461
5477
  }
5462
5478
  return true;
5463
5479
  }
5464
- async function scaffoldIntegrations(customRoutes, database, gameName) {
5480
+ function ensurePlaycademyGitignore() {
5481
+ const workspace = getWorkspace();
5482
+ const playcademyDir = join9(workspace, CLI_DIRECTORIES.WORKSPACE);
5483
+ if (!existsSync8(playcademyDir)) {
5484
+ mkdirSync3(playcademyDir, { recursive: true });
5485
+ }
5486
+ const gitignorePath = join9(playcademyDir, ".gitignore");
5487
+ writeFileSync3(gitignorePath, playcademyGitignoreTemplate);
5488
+ }
5489
+ async function scaffoldIntegrations(customRoutes, database, kv, gameName) {
5490
+ ensurePlaycademyGitignore();
5465
5491
  if (customRoutes) {
5466
- const template = database ? sampleRouteWithDbTemplate : sampleRouteTemplate;
5467
- await scaffoldApiDirectory(customRoutes.directory, template);
5492
+ const sampleRoutes = [
5493
+ // Always include basic custom route example
5494
+ { filename: "custom.ts", template: sampleCustomRouteTemplate }
5495
+ ];
5496
+ if (database) {
5497
+ sampleRoutes.push({ filename: "database.ts", template: sampleDatabaseRouteTemplate });
5498
+ }
5499
+ if (kv) {
5500
+ sampleRoutes.push({ filename: "kv.ts", template: sampleKvRouteTemplate });
5501
+ }
5502
+ await scaffoldApiDirectory(customRoutes.directory, sampleRoutes);
5468
5503
  }
5469
5504
  if (database) {
5470
5505
  return await scaffoldDatabaseSetup({ gameName });
@@ -5568,6 +5603,13 @@ async function promptForDatabase() {
5568
5603
  });
5569
5604
  return { directory };
5570
5605
  }
5606
+ async function promptForKV() {
5607
+ const wantsKV = await confirm({
5608
+ message: "KV storage?",
5609
+ default: false
5610
+ });
5611
+ return wantsKV;
5612
+ }
5571
5613
  async function promptForCustomRoutes(requiresRoutes = false) {
5572
5614
  let wantsCustomRoutes = requiresRoutes;
5573
5615
  if (!requiresRoutes) {
@@ -5594,10 +5636,12 @@ async function promptForCustomRoutes(requiresRoutes = false) {
5594
5636
  async function promptForIntegrations() {
5595
5637
  const timeback = await promptForTimeBackIntegration();
5596
5638
  const database = await promptForDatabase();
5597
- const customRoutes = await promptForCustomRoutes(!!database);
5639
+ const kv = await promptForKV();
5640
+ const customRoutes = await promptForCustomRoutes(!!database || kv);
5598
5641
  return {
5599
5642
  timeback,
5600
5643
  database,
5644
+ kv,
5601
5645
  customRoutes
5602
5646
  };
5603
5647
  }
@@ -5675,19 +5719,26 @@ function displaySuccessMessage(context2) {
5675
5719
  }
5676
5720
 
5677
5721
  // src/lib/deploy/schema.ts
5722
+ function getDrizzleKitApiExports() {
5723
+ const require2 = createRequire(import.meta.url);
5724
+ const drizzleKitApi = require2("drizzle-kit/api");
5725
+ const { generateSQLiteDrizzleJson, generateSQLiteMigration } = drizzleKitApi;
5726
+ return {
5727
+ generateSQLiteDrizzleJson,
5728
+ generateSQLiteMigration
5729
+ };
5730
+ }
5678
5731
  async function getSchemaInfo(previousSchemaSnapshot) {
5679
5732
  const workspace = getWorkspace();
5680
5733
  if (!hasDatabaseSetup()) {
5681
5734
  return null;
5682
5735
  }
5683
- const schemaPath = join9(workspace, "db/schema/index.ts");
5736
+ const schemaPath = join10(workspace, "db/schema/index.ts");
5684
5737
  if (!existsSync9(schemaPath)) {
5685
5738
  return null;
5686
5739
  }
5687
5740
  try {
5688
- const require2 = createRequire(import.meta.url);
5689
- const drizzleKitApi = require2("drizzle-kit/api");
5690
- const { generateSQLiteDrizzleJson, generateSQLiteMigration } = drizzleKitApi;
5741
+ const { generateSQLiteDrizzleJson, generateSQLiteMigration } = getDrizzleKitApiExports();
5691
5742
  const currentSchema = await importTypescriptDefault(schemaPath);
5692
5743
  const nextJson = await generateSQLiteDrizzleJson(currentSchema);
5693
5744
  const prevJson = previousSchemaSnapshot ? previousSchemaSnapshot : await generateSQLiteDrizzleJson({});
@@ -5730,7 +5781,7 @@ var CUSTOM_ROUTES_EXTENSIONS_WITH_DOT = [".ts", ".js", ".mjs", ".cjs"];
5730
5781
  function getCustomRoutesDirectory(projectPath, config) {
5731
5782
  const customRoutes = config?.integrations?.customRoutes;
5732
5783
  const customRoutesDir = typeof customRoutes === "object" && customRoutes.directory || DEFAULT_API_ROUTES_DIRECTORY;
5733
- return join10(projectPath, customRoutesDir);
5784
+ return join11(projectPath, customRoutesDir);
5734
5785
  }
5735
5786
  function hasLocalCustomRoutes(projectPath, config) {
5736
5787
  const customRoutesDir = getCustomRoutesDirectory(projectPath, config);
@@ -5742,7 +5793,7 @@ async function getCustomRoutesHash(projectPath, config) {
5742
5793
  }
5743
5794
  async function getCustomRoutesSize(projectPath, config) {
5744
5795
  const { stat: stat3, readdir } = await import("node:fs/promises");
5745
- const { join: join20 } = await import("node:path");
5796
+ const { join: join29 } = await import("node:path");
5746
5797
  const customRoutesDir = getCustomRoutesDirectory(projectPath, config);
5747
5798
  if (!existsSync10(customRoutesDir)) {
5748
5799
  return null;
@@ -5751,7 +5802,7 @@ async function getCustomRoutesSize(projectPath, config) {
5751
5802
  async function calculateDirSize(dir) {
5752
5803
  const entries = await readdir(dir, { withFileTypes: true });
5753
5804
  for (const entry of entries) {
5754
- const fullPath = join20(dir, entry.name);
5805
+ const fullPath = join29(dir, entry.name);
5755
5806
  if (entry.isDirectory()) {
5756
5807
  await calculateDirSize(fullPath);
5757
5808
  } else if (entry.isFile()) {
@@ -5787,16 +5838,22 @@ async function getBackendSize(config) {
5787
5838
  return null;
5788
5839
  }
5789
5840
  }
5790
- function getBackendBindings(slug) {
5841
+ function getBackendBindings(config, slug) {
5791
5842
  const deploymentId = getDeploymentId(slug);
5792
5843
  const bindings = {};
5793
5844
  if (hasDatabaseSetup()) {
5794
5845
  bindings.database = [deploymentId];
5795
5846
  }
5847
+ if (hasKVSetup(config)) {
5848
+ bindings.keyValue = [deploymentId];
5849
+ }
5796
5850
  return Object.keys(bindings).length > 0 ? bindings : void 0;
5797
5851
  }
5798
5852
  async function deployGameBackend(client, slug, config, projectPath, previousCustomRoutesHash, fullConfig, forceBackend, previousIntegrationKeys, previousSchemaSnapshot, debug) {
5799
5853
  try {
5854
+ if (fullConfig === void 0 || fullConfig === null) {
5855
+ return null;
5856
+ }
5800
5857
  const hasCustomRoutes2 = hasLocalCustomRoutes(projectPath, fullConfig);
5801
5858
  const currentIntegrationKeys = getIntegrationKeys(fullConfig?.integrations);
5802
5859
  const hasIntegrations = currentIntegrationKeys.length > 0;
@@ -5819,10 +5876,10 @@ async function deployGameBackend(client, slug, config, projectPath, previousCust
5819
5876
  }
5820
5877
  const bundle = await runStep(
5821
5878
  "Bundling backend",
5822
- async () => bundleBackend(fullConfig || config),
5879
+ async () => bundleBackend(fullConfig),
5823
5880
  "Backend bundled"
5824
5881
  );
5825
- const bindings = getBackendBindings(slug);
5882
+ const bindings = getBackendBindings(fullConfig, slug);
5826
5883
  const schemaInfo = await getSchemaInfo(previousSchemaSnapshot);
5827
5884
  if (debug && schemaInfo) {
5828
5885
  logger.newLine();
@@ -6121,10 +6178,10 @@ import { dim as dim4 } from "colorette";
6121
6178
  init_file_loader();
6122
6179
  init_constants2();
6123
6180
  init_core();
6124
- import { join as join11, relative as relative2 } from "path";
6181
+ import { join as join12, relative as relative2 } from "path";
6125
6182
  function findSingleBuildZip() {
6126
6183
  const workspace = getWorkspace();
6127
- const playcademyDir = join11(workspace, CLI_DIRECTORIES.WORKSPACE);
6184
+ const playcademyDir = join12(workspace, CLI_DIRECTORIES.WORKSPACE);
6128
6185
  const zipFiles = findFilesByExtension(playcademyDir, "zip");
6129
6186
  if (zipFiles.length === 1) {
6130
6187
  return zipFiles[0] ? relative2(workspace, zipFiles[0]) : null;
@@ -6303,6 +6360,18 @@ async function promptForMissingConfig(context2) {
6303
6360
  if (hasDatabase) {
6304
6361
  parts.push("Database");
6305
6362
  }
6363
+ if (context2.fullConfig?.integrations?.kv) {
6364
+ parts.push("KV Storage");
6365
+ }
6366
+ if (hasIntegrations && context2.fullConfig?.integrations) {
6367
+ const integrationNames = getUserFacingIntegrationNames(
6368
+ context2.fullConfig.integrations
6369
+ );
6370
+ const otherIntegrations = integrationNames.filter(
6371
+ (name) => name.toLowerCase() !== "kv"
6372
+ );
6373
+ parts.push(...otherIntegrations);
6374
+ }
6306
6375
  if (hasCustomRoutes2) {
6307
6376
  const customRoutesDir = getCustomRoutesDirectory(
6308
6377
  context2.projectPath,
@@ -6310,17 +6379,11 @@ async function promptForMissingConfig(context2) {
6310
6379
  );
6311
6380
  const routes = await discoverRoutes(customRoutesDir);
6312
6381
  if (routes.length > 0) {
6313
- parts.push(`${routes.length} custom ${pluralize(routes.length, "route")}`);
6382
+ parts.push(`Custom Routes (${routes.length})`);
6314
6383
  }
6315
6384
  }
6316
- if (hasIntegrations && context2.fullConfig?.integrations) {
6317
- const integrationNames = getUserFacingIntegrationNames(
6318
- context2.fullConfig.integrations
6319
- );
6320
- parts.push(...integrationNames);
6321
- }
6322
6385
  if (parts.length > 0) {
6323
- logger.success(`Integrations: ${parts.join(" + ")}`);
6386
+ logger.success(`Integrations: ${parts.join(", ")}`);
6324
6387
  }
6325
6388
  }
6326
6389
  } else if (config.gameType === "external" && context2.deployBackend !== false) {
@@ -6960,12 +7023,12 @@ init_constants2();
6960
7023
  import { existsSync as existsSync12 } from "node:fs";
6961
7024
  import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
6962
7025
  import { homedir as homedir2 } from "node:os";
6963
- import { join as join12 } from "node:path";
7026
+ import { join as join13 } from "node:path";
6964
7027
  function getGamesStorePath() {
6965
- return join12(homedir2(), CLI_USER_DIRECTORIES.CONFIG, CLI_FILES.GAMES_STORE);
7028
+ return join13(homedir2(), CLI_USER_DIRECTORIES.CONFIG, CLI_FILES.GAMES_STORE);
6966
7029
  }
6967
7030
  async function ensureConfigDir() {
6968
- const configDir = join12(homedir2(), CLI_USER_DIRECTORIES.CONFIG);
7031
+ const configDir = join13(homedir2(), CLI_USER_DIRECTORIES.CONFIG);
6969
7032
  await mkdir3(configDir, { recursive: true });
6970
7033
  }
6971
7034
  async function loadGameStore() {
@@ -7456,12 +7519,12 @@ async function saveDeploymentState(game, backendDeployment, context2) {
7456
7519
  init_constants2();
7457
7520
  init_core();
7458
7521
  import { existsSync as existsSync14 } from "fs";
7459
- import { join as join13 } from "path";
7522
+ import { join as join14 } from "path";
7460
7523
  function hasCustomRoutes(config) {
7461
7524
  const workspace = getWorkspace();
7462
7525
  const customRoutesConfig = config?.integrations?.customRoutes;
7463
7526
  const customRoutesDir = typeof customRoutesConfig === "object" && customRoutesConfig.directory || DEFAULT_API_ROUTES_DIRECTORY;
7464
- return existsSync14(join13(workspace, customRoutesDir));
7527
+ return existsSync14(join14(workspace, customRoutesDir));
7465
7528
  }
7466
7529
  function needsBackend(config) {
7467
7530
  return !!config?.integrations || hasCustomRoutes(config);
@@ -7512,13 +7575,13 @@ init_constants2();
7512
7575
  init_core();
7513
7576
  import { existsSync as existsSync15, readFileSync as readFileSync4, unlinkSync as unlinkSync2 } from "fs";
7514
7577
  import { mkdir as mkdir4, unlink, writeFile as writeFile4 } from "fs/promises";
7515
- import { join as join14 } from "path";
7578
+ import { join as join15 } from "path";
7516
7579
  function getDevServerPidPath() {
7517
- return join14(getWorkspace(), CLI_DIRECTORIES.WORKSPACE, CLI_FILES.DEV_SERVER_PID);
7580
+ return join15(getWorkspace(), CLI_DIRECTORIES.WORKSPACE, CLI_FILES.DEV_SERVER_PID);
7518
7581
  }
7519
7582
  async function createDevServerPidFile() {
7520
7583
  const pidPath = getDevServerPidFile();
7521
- const pidDir = join14(getWorkspace(), CLI_DIRECTORIES.WORKSPACE);
7584
+ const pidDir = join15(getWorkspace(), CLI_DIRECTORIES.WORKSPACE);
7522
7585
  await mkdir4(pidDir, { recursive: true });
7523
7586
  await writeFile4(pidPath, process.pid.toString());
7524
7587
  }
@@ -7558,7 +7621,7 @@ function getDevServerPidFile() {
7558
7621
  // src/lib/dev/reload.ts
7559
7622
  init_constants2();
7560
7623
  init_core();
7561
- import { join as join15, relative as relative3 } from "path";
7624
+ import { join as join16, relative as relative3 } from "path";
7562
7625
  import chokidar from "chokidar";
7563
7626
  import { bold as bold4, cyan as cyan3, dim as dim6, green as green3 } from "colorette";
7564
7627
  function formatTime() {
@@ -7575,9 +7638,9 @@ function startHotReload(onReload, options = {}) {
7575
7638
  const customRoutesConfig = options.config?.integrations?.customRoutes;
7576
7639
  const customRoutesDir = typeof customRoutesConfig === "object" && customRoutesConfig.directory || DEFAULT_API_ROUTES_DIRECTORY;
7577
7640
  const watchPaths = [
7578
- join15(workspace, customRoutesDir),
7579
- join15(workspace, "playcademy.config.js"),
7580
- join15(workspace, "playcademy.config.json")
7641
+ join16(workspace, customRoutesDir),
7642
+ join16(workspace, "playcademy.config.js"),
7643
+ join16(workspace, "playcademy.config.json")
7581
7644
  ];
7582
7645
  const watcher = chokidar.watch(watchPaths, {
7583
7646
  persistent: true,
@@ -7619,28 +7682,28 @@ function startHotReload(onReload, options = {}) {
7619
7682
  // src/lib/dev/server.ts
7620
7683
  init_src2();
7621
7684
  init_constants2();
7685
+ init_loader();
7622
7686
  init_core();
7623
7687
  import { mkdir as mkdir5 } from "fs/promises";
7624
- import { join as join16 } from "path";
7688
+ import { join as join17 } from "path";
7625
7689
  import { Miniflare } from "miniflare";
7626
7690
  async function startDevServer(options) {
7627
7691
  const {
7628
7692
  port,
7629
- config,
7693
+ config: providedConfig,
7630
7694
  platformUrl = process.env.PLAYCADEMY_BASE_URL || "http://localhost:5174"
7631
7695
  } = options;
7696
+ const config = providedConfig ?? await loadConfig();
7632
7697
  const hasSandboxTimebackCreds = !!process.env.TIMEBACK_API_CLIENT_ID;
7633
- const devConfig = config?.integrations?.timeback && !hasSandboxTimebackCreds ? { ...config, integrations: { ...config.integrations, timeback: void 0 } } : config;
7698
+ const devConfig = config.integrations?.timeback && !hasSandboxTimebackCreds ? { ...config, integrations: { ...config.integrations, timeback: void 0 } } : config;
7634
7699
  const bundle = await bundleBackend(devConfig, {
7635
7700
  sourcemap: false,
7636
7701
  minify: false
7637
7702
  });
7638
- const dbDir = join16(getWorkspace(), CLI_DIRECTORIES.DATABASE);
7639
- try {
7640
- await mkdir5(dbDir, { recursive: true });
7641
- } catch (error) {
7642
- throw new Error(`Failed to create database directory: ${getErrorMessage(error)}`);
7643
- }
7703
+ const hasDatabase = hasDatabaseSetup();
7704
+ const dbDir = hasDatabase ? await ensureDatabaseDirectory() : void 0;
7705
+ const hasKV = hasKVSetup(config);
7706
+ const kvDir = hasKV ? await ensureKvDirectory() : void 0;
7644
7707
  const mf = new Miniflare({
7645
7708
  port,
7646
7709
  modules: [
@@ -7655,14 +7718,39 @@ async function startDevServer(options) {
7655
7718
  GAME_ID: CORE_GAME_UUIDS.PLAYGROUND,
7656
7719
  PLAYCADEMY_BASE_URL: platformUrl
7657
7720
  },
7658
- d1Databases: ["DB"],
7721
+ d1Databases: hasDatabase ? ["DB"] : [],
7659
7722
  d1Persist: dbDir,
7723
+ kvNamespaces: hasKV ? ["KV"] : [],
7724
+ kvPersist: kvDir,
7660
7725
  compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE
7661
7726
  });
7727
+ if (hasDatabase) {
7728
+ await initializeDatabase(mf);
7729
+ }
7730
+ return mf;
7731
+ }
7732
+ async function ensureDatabaseDirectory() {
7733
+ const dbDir = join17(getWorkspace(), CLI_DIRECTORIES.DATABASE);
7734
+ try {
7735
+ await mkdir5(dbDir, { recursive: true });
7736
+ } catch (error) {
7737
+ throw new Error(`Failed to create database directory: ${getErrorMessage(error)}`);
7738
+ }
7739
+ return dbDir;
7740
+ }
7741
+ async function ensureKvDirectory() {
7742
+ const kvDir = join17(getWorkspace(), CLI_DIRECTORIES.KV);
7743
+ try {
7744
+ await mkdir5(kvDir, { recursive: true });
7745
+ } catch (error) {
7746
+ throw new Error(`Failed to create KV directory: ${getErrorMessage(error)}`);
7747
+ }
7748
+ return kvDir;
7749
+ }
7750
+ async function initializeDatabase(mf) {
7662
7751
  const d1 = await mf.getD1Database("DB");
7663
7752
  await d1.exec("SELECT 1");
7664
7753
  await createDevServerPidFile();
7665
- return mf;
7666
7754
  }
7667
7755
 
7668
7756
  // src/lib/timeback/cleanup.ts
@@ -7843,6 +7931,7 @@ var initCommand = new Command2("init").description("Initialize a playcademy.conf
7843
7931
  const {
7844
7932
  timeback: timebackConfig,
7845
7933
  database,
7934
+ kv,
7846
7935
  customRoutes
7847
7936
  } = await promptForIntegrations();
7848
7937
  let depsAdded = false;
@@ -7857,6 +7946,7 @@ var initCommand = new Command2("init").description("Initialize a playcademy.conf
7857
7946
  const scaffoldDepsAdded = await scaffoldIntegrations(
7858
7947
  customRoutes,
7859
7948
  database,
7949
+ kv,
7860
7950
  gameInfo.name
7861
7951
  );
7862
7952
  if (scaffoldDepsAdded) depsAdded = true;
@@ -7872,6 +7962,7 @@ var initCommand = new Command2("init").description("Initialize a playcademy.conf
7872
7962
  emoji: gameInfo.emoji,
7873
7963
  customRoutesDirectory: customRoutes?.directory ?? void 0,
7874
7964
  databaseDirectory: database?.directory ?? void 0,
7965
+ kv: kv ?? void 0,
7875
7966
  timeback: timebackConfig ?? void 0
7876
7967
  }) : generateJsonConfig({
7877
7968
  name: gameInfo.name,
@@ -7879,6 +7970,7 @@ var initCommand = new Command2("init").description("Initialize a playcademy.conf
7879
7970
  emoji: gameInfo.emoji,
7880
7971
  customRoutesDirectory: customRoutes?.directory ?? void 0,
7881
7972
  databaseDirectory: database?.directory ?? void 0,
7973
+ kv: kv ?? void 0,
7882
7974
  timeback: timebackConfig ?? void 0
7883
7975
  });
7884
7976
  writeFileSync5(resolve10(getWorkspace(), configFileName), configContent, "utf-8");
@@ -7904,7 +7996,6 @@ async function addPlaycademySdk() {
7904
7996
  if (!pkg.dependencies) pkg.dependencies = {};
7905
7997
  pkg.dependencies["@playcademy/sdk"] = "latest";
7906
7998
  writeFileSync5(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
7907
- logger.success("Added @playcademy/sdk to dependencies");
7908
7999
  return true;
7909
8000
  }
7910
8001
  async function installDependencies() {
@@ -8579,27 +8670,14 @@ devCommand.addCommand(getStatusCommand);
8579
8670
  import { Command as Command13 } from "commander";
8580
8671
 
8581
8672
  // src/commands/api/init.ts
8582
- init_file_loader();
8583
8673
  init_constants2();
8584
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
8585
8674
  import { input as input5 } from "@inquirer/prompts";
8675
+ init_writer();
8586
8676
  init_loader2();
8587
8677
  async function runApiInit() {
8588
8678
  try {
8589
8679
  logger.newLine();
8590
- const configFile = await findFile(CONFIG_FILE_NAMES, {
8591
- searchUp: false,
8592
- maxLevels: 0
8593
- });
8594
- if (!configFile) {
8595
- logger.error("No playcademy.config file found");
8596
- logger.newLine();
8597
- logger.admonition("tip", "Getting Started", [
8598
- "Run `playcademy init` to create a config file first"
8599
- ]);
8600
- logger.newLine();
8601
- process.exit(1);
8602
- }
8680
+ const config = await loadConfig();
8603
8681
  logger.highlight("Add Custom API Routes");
8604
8682
  logger.newLine();
8605
8683
  const apiDir = await input5({
@@ -8612,48 +8690,39 @@ async function runApiInit() {
8612
8690
  return validateApiDirectoryDoesNotExist(value);
8613
8691
  }
8614
8692
  });
8615
- const hasDb = hasDatabaseSetup();
8616
- const sampleRouteTemplate2 = hasDb ? loadTemplateString("api/sample-route-with-db.ts") : loadTemplateString("api/sample-route.ts");
8617
- await scaffoldApiDirectory(apiDir, sampleRouteTemplate2);
8618
- const configPath = configFile.path;
8619
- const config = await loadConfig(configPath);
8693
+ const sampleRoutes = [
8694
+ // Always include basic custom route example
8695
+ { filename: "custom.ts", template: loadTemplateString("api/sample-custom.ts") }
8696
+ ];
8697
+ if (hasDatabaseSetup()) {
8698
+ sampleRoutes.push({
8699
+ filename: "database.ts",
8700
+ template: loadTemplateString("api/sample-database.ts")
8701
+ });
8702
+ }
8703
+ if (hasKVSetup(config)) {
8704
+ sampleRoutes.push({
8705
+ filename: "kv.ts",
8706
+ template: loadTemplateString("api/sample-kv.ts")
8707
+ });
8708
+ }
8709
+ await scaffoldApiDirectory(apiDir, sampleRoutes);
8620
8710
  if (!config.integrations?.customRoutes) {
8621
- const configContent = readFileSync6(configPath, "utf-8");
8622
- const isJson = configPath.endsWith(".json");
8623
- let updatedContent;
8624
- if (isJson) {
8625
- const parsed = JSON.parse(configContent);
8626
- if (!parsed.integrations) parsed.integrations = {};
8627
- parsed.integrations.customRoutes = { directory: apiDir };
8628
- updatedContent = JSON.stringify(parsed, null, 4);
8629
- } else {
8630
- if (configContent.includes("integrations:")) {
8631
- const customRoutesConfig = `
8632
- customRoutes: { directory: '${apiDir}' },`;
8633
- updatedContent = configContent.replace(
8634
- /(integrations:\s*{)/,
8635
- `$1${customRoutesConfig}`
8636
- );
8637
- } else {
8638
- const integrationsConfig = `
8639
- integrations: {
8640
- customRoutes: { directory: '${apiDir}' },
8641
- },`;
8642
- updatedContent = configContent.replace(
8643
- /(\n\s*})(\s*)$/,
8644
- `${integrationsConfig}$1$2`
8645
- );
8711
+ const configPath = await findConfigPath();
8712
+ await updateConfigFile(configPath, {
8713
+ integrations: {
8714
+ ...config.integrations,
8715
+ customRoutes: { directory: apiDir }
8646
8716
  }
8647
- }
8648
- writeFileSync6(configPath, updatedContent, "utf-8");
8649
- logger.success(`Updated ${configFile.path.split("/").pop()}`);
8717
+ });
8718
+ logger.success(`Updated ${configPath.split("/").pop()}`);
8650
8719
  }
8651
8720
  logger.newLine();
8652
8721
  logger.success("Custom API routes added!");
8653
8722
  logger.newLine();
8654
8723
  logger.admonition("tip", "Next Steps", [
8655
- `1. Review the sample route in ${apiDir}/hello.ts`,
8656
- `2. Create your custom routes in ${apiDir}/`,
8724
+ `1. Review sample routes in ${apiDir}/sample/`,
8725
+ `2. Create custom routes in ${apiDir}/`,
8657
8726
  "3. Run `playcademy dev` to test locally",
8658
8727
  "4. Run `playcademy deploy --backend` to deploy"
8659
8728
  ]);
@@ -8702,7 +8771,7 @@ async function runDbDiff() {
8702
8771
  // src/commands/db/init.ts
8703
8772
  init_file_loader();
8704
8773
  init_constants2();
8705
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
8774
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
8706
8775
  import { input as input6 } from "@inquirer/prompts";
8707
8776
  async function runDbInit() {
8708
8777
  try {
@@ -8733,9 +8802,10 @@ async function runDbInit() {
8733
8802
  return true;
8734
8803
  }
8735
8804
  });
8805
+ ensurePlaycademyGitignore();
8736
8806
  await scaffoldDatabaseSetup({ gameName: config.name });
8737
8807
  if (!config.integrations?.database) {
8738
- const configContent = readFileSync7(configFile.path, "utf-8");
8808
+ const configContent = readFileSync6(configFile.path, "utf-8");
8739
8809
  const isJson = configFile.path.endsWith(".json");
8740
8810
  let updatedContent;
8741
8811
  if (isJson) {
@@ -8758,7 +8828,7 @@ async function runDbInit() {
8758
8828
  );
8759
8829
  }
8760
8830
  }
8761
- writeFileSync7(configFile.path, updatedContent, "utf-8");
8831
+ writeFileSync6(configFile.path, updatedContent, "utf-8");
8762
8832
  logger.success(`Updated ${configFile.path.split("/").pop()}`);
8763
8833
  }
8764
8834
  logger.newLine();
@@ -8783,13 +8853,13 @@ init_src();
8783
8853
  init_constants2();
8784
8854
  import { spawn } from "child_process";
8785
8855
  import { existsSync as existsSync16, rmSync as rmSync2 } from "fs";
8786
- import { join as join17 } from "path";
8856
+ import { join as join18 } from "path";
8787
8857
  import { confirm as confirm6 } from "@inquirer/prompts";
8788
8858
  import { Miniflare as Miniflare2 } from "miniflare";
8789
8859
  async function runDbReset() {
8790
8860
  try {
8791
8861
  const workspace = getWorkspace();
8792
- const dbDir = join17(workspace, CLI_DIRECTORIES.DATABASE);
8862
+ const dbDir = join18(workspace, CLI_DIRECTORIES.DATABASE);
8793
8863
  if (!existsSync16(dbDir)) {
8794
8864
  logger.warn("No database found to reset");
8795
8865
  logger.newLine();
@@ -8894,7 +8964,7 @@ async function runDbReset() {
8894
8964
  init_package_manager();
8895
8965
  import { execSync as execSync4 } from "child_process";
8896
8966
  import { existsSync as existsSync17 } from "fs";
8897
- import { join as join18 } from "path";
8967
+ import { join as join19 } from "path";
8898
8968
  async function runDbSeed(options) {
8899
8969
  const workspace = getWorkspace();
8900
8970
  try {
@@ -8903,7 +8973,7 @@ async function runDbSeed(options) {
8903
8973
  logger.newLine();
8904
8974
  }
8905
8975
  if (options.file) {
8906
- const seedPath = join18(workspace, options.file);
8976
+ const seedPath = join19(workspace, options.file);
8907
8977
  if (!existsSync17(seedPath)) {
8908
8978
  logger.error(`Seed file not found: ${options.file}`);
8909
8979
  logger.newLine();
@@ -8929,102 +8999,1143 @@ dbCommand.command("reset").description("Reset local development database (delete
8929
8999
  dbCommand.command("seed [file]").description("Seed local database with initial data").option("-r, --reset", "Reset database before seeding").action((file, options) => runDbSeed({ file, reset: options.reset }));
8930
9000
  dbCommand.command("diff").description("Show schema changes since last deployment").action(runDbDiff);
8931
9001
 
8932
- // src/commands/profiles/index.ts
8933
- import { Command as Command18 } from "commander";
9002
+ // src/commands/kv/index.ts
9003
+ import { Command as Command15 } from "commander";
8934
9004
 
8935
- // src/commands/profiles/list.ts
9005
+ // src/commands/kv/clear.ts
8936
9006
  init_string();
8937
- import { Command as Command15 } from "commander";
8938
- async function listProfilesAction() {
9007
+ init_constants2();
9008
+ import { join as join20 } from "path";
9009
+ import { confirm as confirm7 } from "@inquirer/prompts";
9010
+ import { Miniflare as Miniflare3 } from "miniflare";
9011
+ async function runKVClear(options = {}) {
8939
9012
  try {
8940
- const profilesMap = await listProfiles();
8941
- logger.newLine();
8942
- let hasAnyProfiles = false;
8943
- for (const [, profiles] of profilesMap.entries()) {
8944
- if (profiles.length > 0) {
8945
- hasAnyProfiles = true;
8946
- break;
9013
+ if (!options.raw && !options.json) {
9014
+ logger.newLine();
9015
+ }
9016
+ if (options.remote) {
9017
+ if (!options.raw && !options.json) {
9018
+ logger.newLine();
9019
+ logger.warn("Remote KV operations are not yet implemented");
9020
+ logger.newLine();
9021
+ logger.admonition("info", "Coming Soon", [
9022
+ "Remote KV support is on the roadmap and will be available soon.",
9023
+ "For now, KV commands only work with local development storage."
9024
+ ]);
9025
+ logger.newLine();
8947
9026
  }
9027
+ process.exit(1);
8948
9028
  }
8949
- if (!hasAnyProfiles) {
8950
- logger.warn("No authentication profiles found");
8951
- logger.newLine();
8952
- logger.admonition("tip", "Getting Started", [
8953
- "Run `playcademy login` to create your first profile"
8954
- ]);
8955
- logger.newLine();
8956
- return;
9029
+ const config = await loadConfig();
9030
+ if (!hasKVSetup(config)) {
9031
+ if (!options.raw && !options.json) {
9032
+ logger.error("KV storage is not configured");
9033
+ logger.newLine();
9034
+ logger.admonition("tip", "Getting Started", [
9035
+ "Run `playcademy kv init` to enable KV storage"
9036
+ ]);
9037
+ logger.newLine();
9038
+ }
9039
+ process.exit(1);
8957
9040
  }
8958
- for (const [environment, profiles] of profilesMap.entries()) {
8959
- if (profiles.length === 0) continue;
8960
- const tableData = [];
8961
- for (const profileName of profiles) {
8962
- const profile = await getProfile(environment, profileName);
8963
- tableData.push({
8964
- Profile: profileName,
8965
- Email: profile?.email ?? ""
9041
+ const bundle = await bundleBackend(config, {
9042
+ sourcemap: false,
9043
+ minify: false
9044
+ });
9045
+ const kvDir = join20(getWorkspace(), CLI_DIRECTORIES.KV);
9046
+ const mf = new Miniflare3({
9047
+ modules: [
9048
+ {
9049
+ type: "ESModule",
9050
+ path: "index.mjs",
9051
+ contents: bundle.code
9052
+ }
9053
+ ],
9054
+ kvNamespaces: ["KV"],
9055
+ kvPersist: kvDir,
9056
+ compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE
9057
+ });
9058
+ try {
9059
+ const kv = await mf.getKVNamespace("KV");
9060
+ const listResult = await kv.list();
9061
+ const keyCount = listResult.keys?.length || 0;
9062
+ if (keyCount === 0) {
9063
+ if (options.json) {
9064
+ logger.json({
9065
+ success: true,
9066
+ deleted: 0,
9067
+ message: "No keys to clear"
9068
+ });
9069
+ } else if (options.raw) {
9070
+ logger.raw("0");
9071
+ } else {
9072
+ logger.info("No keys found in KV namespace");
9073
+ logger.newLine();
9074
+ }
9075
+ return;
9076
+ }
9077
+ if (!options.force && !options.raw && !options.json) {
9078
+ logger.warn(`This will delete ${keyCount} ${pluralize(keyCount, "key")}`);
9079
+ logger.newLine();
9080
+ const confirmed = await confirm7({
9081
+ message: "Are you sure you want to clear all keys?",
9082
+ default: false
8966
9083
  });
9084
+ if (!confirmed) {
9085
+ logger.info("Cancelled");
9086
+ logger.newLine();
9087
+ return;
9088
+ }
9089
+ logger.newLine();
8967
9090
  }
8968
- if (tableData.length > 0) {
8969
- const envTitle = capitalize(environment);
8970
- logger.table(tableData, envTitle);
9091
+ const keys = listResult.keys || [];
9092
+ for (const key of keys) {
9093
+ await kv.delete(key.name);
9094
+ }
9095
+ if (options.json) {
9096
+ logger.json({
9097
+ success: true,
9098
+ deleted: keyCount
9099
+ });
9100
+ } else if (options.raw) {
9101
+ logger.raw(keyCount.toString());
9102
+ } else {
9103
+ logger.success(`Deleted ${keyCount} ${pluralize(keyCount, "key")}`);
8971
9104
  logger.newLine();
8972
9105
  }
9106
+ } finally {
9107
+ await mf.dispose();
8973
9108
  }
8974
9109
  } catch (error) {
8975
- logger.error(`Failed to list profiles: ${getErrorMessage(error)}`);
9110
+ if (!options.raw && !options.json) {
9111
+ logger.error(
9112
+ `Failed to clear KV: ${error instanceof Error ? error.message : String(error)}`
9113
+ );
9114
+ logger.newLine();
9115
+ }
8976
9116
  process.exit(1);
8977
9117
  }
8978
9118
  }
8979
- var listCommand2 = new Command15("list").alias("ls").description("List all stored authentication profiles").action(listProfilesAction);
8980
9119
 
8981
- // src/commands/profiles/remove.ts
8982
- import { bold as bold6 } from "colorette";
8983
- import { Command as Command16 } from "commander";
8984
- var removeCommand = new Command16("remove").alias("rm").description('Remove an authentication profile (defaults to "default")').argument("[name]", "Profile name to remove", "default").option("--env <environment>", "Environment to remove profile from (staging or production)").action(async (name, options) => {
8985
- const { env } = options;
8986
- const environment = ensureEnvironment(env, logger);
9120
+ // src/commands/kv/delete.ts
9121
+ init_constants2();
9122
+ import { join as join21 } from "path";
9123
+ import { Miniflare as Miniflare4 } from "miniflare";
9124
+ async function runKVDelete(key, options = {}) {
8987
9125
  try {
8988
- logger.newLine();
8989
- const profilesMap = await listProfiles();
8990
- const envProfiles = profilesMap.get(environment) || [];
8991
- if (!envProfiles.includes(name)) {
8992
- logger.error(`Profile "${name}" not found in ${environment}`);
9126
+ if (!options.raw && !options.json) {
8993
9127
  logger.newLine();
9128
+ }
9129
+ if (options.remote) {
9130
+ if (!options.raw && !options.json) {
9131
+ logger.newLine();
9132
+ logger.warn("Remote KV operations are not yet implemented");
9133
+ logger.newLine();
9134
+ logger.admonition("info", "Coming Soon", [
9135
+ "Remote KV support is on the roadmap and will be available soon.",
9136
+ "For now, KV commands only work with local development storage."
9137
+ ]);
9138
+ logger.newLine();
9139
+ }
8994
9140
  process.exit(1);
8995
9141
  }
8996
- await removeProfile(environment, name);
8997
- logger.admonition("note", "Removed!", [
8998
- `Profile ${bold6(name)} removed from ${environment}`,
8999
- environment === "production" ? `To re-authenticate run \`playcademy login --env ${environment}\`` : "To re-authenticate run `playcademy login`"
9000
- ]);
9001
- logger.newLine();
9142
+ if (!key) {
9143
+ if (!options.raw && !options.json) {
9144
+ logger.error("Key is required");
9145
+ logger.newLine();
9146
+ logger.admonition("tip", "Usage", ["`playcademy kv delete <key>`"]);
9147
+ logger.newLine();
9148
+ }
9149
+ process.exit(1);
9150
+ }
9151
+ const config = await loadConfig();
9152
+ if (!hasKVSetup(config)) {
9153
+ if (!options.raw && !options.json) {
9154
+ logger.error("KV storage is not configured");
9155
+ logger.newLine();
9156
+ logger.admonition("tip", "Getting Started", [
9157
+ "Run `playcademy kv init` to enable KV storage"
9158
+ ]);
9159
+ logger.newLine();
9160
+ }
9161
+ process.exit(1);
9162
+ }
9163
+ const bundle = await bundleBackend(config, {
9164
+ sourcemap: false,
9165
+ minify: false
9166
+ });
9167
+ const kvDir = join21(getWorkspace(), CLI_DIRECTORIES.KV);
9168
+ const mf = new Miniflare4({
9169
+ modules: [
9170
+ {
9171
+ type: "ESModule",
9172
+ path: "index.mjs",
9173
+ contents: bundle.code
9174
+ }
9175
+ ],
9176
+ kvNamespaces: ["KV"],
9177
+ kvPersist: kvDir,
9178
+ compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE
9179
+ });
9180
+ try {
9181
+ const kv = await mf.getKVNamespace("KV");
9182
+ await kv.delete(key);
9183
+ if (options.json) {
9184
+ logger.json({
9185
+ key,
9186
+ success: true
9187
+ });
9188
+ } else if (options.raw) {
9189
+ logger.raw("ok");
9190
+ } else {
9191
+ logger.success(`Deleted key: ${key}`);
9192
+ logger.newLine();
9193
+ }
9194
+ } finally {
9195
+ await mf.dispose();
9196
+ }
9002
9197
  } catch (error) {
9003
- logAndExit(error, logger, { prefix: "Failed to remove profile" });
9198
+ if (!options.raw && !options.json) {
9199
+ logger.error(
9200
+ `Failed to delete key: ${error instanceof Error ? error.message : String(error)}`
9201
+ );
9202
+ logger.newLine();
9203
+ }
9204
+ process.exit(1);
9004
9205
  }
9005
- });
9206
+ }
9006
9207
 
9007
- // src/commands/profiles/reset.ts
9008
- init_string();
9009
- import { confirm as confirm7 } from "@inquirer/prompts";
9010
- import { Command as Command17 } from "commander";
9011
- var resetCommand = new Command17("reset").description(
9012
- "Remove all authentication profiles across all environments (requires confirmation)"
9013
- ).alias("clear").action(async () => {
9208
+ // src/commands/kv/get.ts
9209
+ init_constants2();
9210
+ import { join as join22 } from "path";
9211
+ import { Miniflare as Miniflare5 } from "miniflare";
9212
+ async function runKVGet(key, options = {}) {
9014
9213
  try {
9015
- const profilesMap = await listProfiles();
9016
- let totalProfiles = 0;
9017
- for (const profiles of profilesMap.values()) {
9018
- totalProfiles += profiles.length;
9019
- }
9020
- if (totalProfiles === 0) {
9021
- logger.newLine();
9022
- logger.warn("No authentication profiles found");
9214
+ if (!options.raw && !options.json) {
9023
9215
  logger.newLine();
9024
- return;
9025
9216
  }
9026
- logger.newLine();
9027
- logger.warn(
9217
+ if (options.remote) {
9218
+ if (!options.raw && !options.json) {
9219
+ logger.newLine();
9220
+ logger.warn("Remote KV operations are not yet implemented");
9221
+ logger.newLine();
9222
+ logger.admonition("info", "Coming Soon", [
9223
+ "Remote KV support is on the roadmap and will be available soon.",
9224
+ "For now, KV commands only work with local development storage."
9225
+ ]);
9226
+ logger.newLine();
9227
+ }
9228
+ process.exit(1);
9229
+ }
9230
+ if (!key) {
9231
+ if (!options.raw && !options.json) {
9232
+ logger.error("Key is required");
9233
+ logger.newLine();
9234
+ logger.admonition("tip", "Usage", ["`playcademy kv get <key>`"]);
9235
+ logger.newLine();
9236
+ }
9237
+ process.exit(1);
9238
+ }
9239
+ const config = await loadConfig();
9240
+ if (!hasKVSetup(config)) {
9241
+ if (!options.raw && !options.json) {
9242
+ logger.error("KV storage is not configured");
9243
+ logger.newLine();
9244
+ logger.admonition("tip", "Getting Started", [
9245
+ "Run `playcademy kv init` to enable KV storage"
9246
+ ]);
9247
+ logger.newLine();
9248
+ }
9249
+ process.exit(1);
9250
+ }
9251
+ const bundle = await bundleBackend(config, {
9252
+ sourcemap: false,
9253
+ minify: false
9254
+ });
9255
+ const kvDir = join22(getWorkspace(), CLI_DIRECTORIES.KV);
9256
+ const mf = new Miniflare5({
9257
+ modules: [
9258
+ {
9259
+ type: "ESModule",
9260
+ path: "index.mjs",
9261
+ contents: bundle.code
9262
+ }
9263
+ ],
9264
+ kvNamespaces: ["KV"],
9265
+ kvPersist: kvDir,
9266
+ compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE
9267
+ });
9268
+ try {
9269
+ const kv = await mf.getKVNamespace("KV");
9270
+ const value = await kv.get(key);
9271
+ if (value === null) {
9272
+ if (!options.raw && !options.json) {
9273
+ logger.warn(`Key '${key}' not found`);
9274
+ logger.newLine();
9275
+ logger.admonition("tip", "Hint", [
9276
+ "Use `playcademy kv list` to see all available keys"
9277
+ ]);
9278
+ logger.newLine();
9279
+ }
9280
+ process.exit(1);
9281
+ }
9282
+ let parsedValue;
9283
+ let isJson = false;
9284
+ try {
9285
+ parsedValue = JSON.parse(value);
9286
+ isJson = true;
9287
+ } catch {
9288
+ parsedValue = value;
9289
+ }
9290
+ if (options.json) {
9291
+ if (isJson) {
9292
+ logger.json(parsedValue);
9293
+ } else {
9294
+ logger.json(value);
9295
+ }
9296
+ return;
9297
+ }
9298
+ if (options.raw) {
9299
+ logger.raw(value);
9300
+ return;
9301
+ }
9302
+ logger.success(`Key: ${key}`);
9303
+ logger.newLine();
9304
+ if (isJson) {
9305
+ logger.json(parsedValue, 1);
9306
+ } else {
9307
+ logger.data("Value", value, 1);
9308
+ }
9309
+ logger.newLine();
9310
+ } finally {
9311
+ await mf.dispose();
9312
+ }
9313
+ } catch (error) {
9314
+ if (!options.raw && !options.json) {
9315
+ logger.error(
9316
+ `Failed to get key: ${error instanceof Error ? error.message : String(error)}`
9317
+ );
9318
+ logger.newLine();
9319
+ }
9320
+ process.exit(1);
9321
+ }
9322
+ }
9323
+
9324
+ // src/commands/kv/init.ts
9325
+ init_writer();
9326
+ async function runKVInit() {
9327
+ try {
9328
+ logger.newLine();
9329
+ const config = await loadConfig();
9330
+ if (hasKVSetup(config)) {
9331
+ logger.success("KV storage is already configured");
9332
+ logger.newLine();
9333
+ logger.admonition("tip", "Already Set Up", [
9334
+ "Your playcademy.config file already has KV integration enabled",
9335
+ "You can start using `c.env.KV` in your API routes"
9336
+ ]);
9337
+ logger.newLine();
9338
+ return;
9339
+ }
9340
+ ensurePlaycademyGitignore();
9341
+ logger.highlight("Add KV Storage Integration");
9342
+ logger.newLine();
9343
+ const configPath = await findConfigPath();
9344
+ await updateConfigFile(configPath, {
9345
+ integrations: {
9346
+ ...config.integrations,
9347
+ kv: true
9348
+ }
9349
+ });
9350
+ logger.success("Added KV integration to config");
9351
+ logger.newLine();
9352
+ logger.admonition("tip", "Usage Example", [
9353
+ "Access KV in your API routes:",
9354
+ "",
9355
+ "```typescript",
9356
+ "export async function GET(c: Context) {",
9357
+ " // Read from KV",
9358
+ ` const data = await c.env.KV.get('user:123')`,
9359
+ "",
9360
+ " // Write to KV",
9361
+ ` await c.env.KV.put('user:123', JSON.stringify({ score: 100 }))`,
9362
+ "",
9363
+ " // Delete from KV",
9364
+ ` await c.env.KV.delete('user:123')`,
9365
+ "",
9366
+ " return c.json({ success: true })",
9367
+ "}",
9368
+ "```"
9369
+ ]);
9370
+ logger.newLine();
9371
+ logger.remark("Run `playcademy dev` to start using KV in local development");
9372
+ logger.newLine();
9373
+ } catch (error) {
9374
+ logger.error(
9375
+ `Failed to add KV integration: ${error instanceof Error ? error.message : String(error)}`
9376
+ );
9377
+ logger.newLine();
9378
+ process.exit(1);
9379
+ }
9380
+ }
9381
+
9382
+ // src/commands/kv/inspect.ts
9383
+ init_constants2();
9384
+ import { join as join23 } from "path";
9385
+ import { Miniflare as Miniflare6 } from "miniflare";
9386
+ async function runKVInspect(key, options = {}) {
9387
+ try {
9388
+ if (!options.raw && !options.json) {
9389
+ logger.newLine();
9390
+ }
9391
+ if (options.remote) {
9392
+ if (!options.raw && !options.json) {
9393
+ logger.newLine();
9394
+ logger.warn("Remote KV operations are not yet implemented");
9395
+ logger.newLine();
9396
+ logger.admonition("info", "Coming Soon", [
9397
+ "Remote KV support is on the roadmap and will be available soon.",
9398
+ "For now, KV commands only work with local development storage."
9399
+ ]);
9400
+ logger.newLine();
9401
+ }
9402
+ process.exit(1);
9403
+ }
9404
+ if (!key) {
9405
+ if (!options.raw && !options.json) {
9406
+ logger.error("Key is required");
9407
+ logger.newLine();
9408
+ logger.admonition("tip", "Usage", ["`playcademy kv inspect <key>`"]);
9409
+ logger.newLine();
9410
+ }
9411
+ process.exit(1);
9412
+ }
9413
+ const config = await loadConfig();
9414
+ if (!hasKVSetup(config)) {
9415
+ if (!options.raw && !options.json) {
9416
+ logger.error("KV storage is not configured");
9417
+ logger.newLine();
9418
+ logger.admonition("tip", "Getting Started", [
9419
+ "Run `playcademy kv init` to enable KV storage"
9420
+ ]);
9421
+ logger.newLine();
9422
+ }
9423
+ process.exit(1);
9424
+ }
9425
+ const bundle = await bundleBackend(config, {
9426
+ sourcemap: false,
9427
+ minify: false
9428
+ });
9429
+ const kvDir = join23(getWorkspace(), CLI_DIRECTORIES.KV);
9430
+ const mf = new Miniflare6({
9431
+ modules: [
9432
+ {
9433
+ type: "ESModule",
9434
+ path: "index.mjs",
9435
+ contents: bundle.code
9436
+ }
9437
+ ],
9438
+ kvNamespaces: ["KV"],
9439
+ kvPersist: kvDir,
9440
+ compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE
9441
+ });
9442
+ try {
9443
+ const kv = await mf.getKVNamespace("KV");
9444
+ const value = await kv.get(key);
9445
+ if (value === null) {
9446
+ const metadata2 = {
9447
+ key,
9448
+ exists: false
9449
+ };
9450
+ if (options.json) {
9451
+ logger.json(metadata2);
9452
+ } else if (!options.raw) {
9453
+ logger.warn(`Key '${key}' not found`);
9454
+ logger.newLine();
9455
+ logger.admonition("tip", "Hint", [
9456
+ "Use `playcademy kv list` to see all available keys"
9457
+ ]);
9458
+ logger.newLine();
9459
+ }
9460
+ process.exit(1);
9461
+ }
9462
+ const size = new Blob([value]).size;
9463
+ let parsedValue;
9464
+ let valueType = "string";
9465
+ try {
9466
+ parsedValue = JSON.parse(value);
9467
+ valueType = "json";
9468
+ } catch {
9469
+ parsedValue = value;
9470
+ }
9471
+ const metadata = {
9472
+ key,
9473
+ exists: true,
9474
+ size,
9475
+ value: parsedValue,
9476
+ valueType
9477
+ };
9478
+ if (options.json) {
9479
+ logger.json(metadata);
9480
+ } else if (options.raw) {
9481
+ logger.raw(size.toString());
9482
+ } else {
9483
+ logger.success(`Key: ${key}`);
9484
+ logger.newLine();
9485
+ const sizeKB = (size / 1024).toFixed(2);
9486
+ const sizeDisplay = size >= 1024 ? `${sizeKB} KB` : `${size} bytes`;
9487
+ logger.data("Size", sizeDisplay, 1);
9488
+ logger.data("Type", valueType === "json" ? "JSON" : "String", 1);
9489
+ logger.newLine();
9490
+ logger.data("Value", "", 1);
9491
+ if (valueType === "json") {
9492
+ logger.json(parsedValue, 2);
9493
+ } else {
9494
+ const lines = value.split("\n");
9495
+ const maxLines = 5;
9496
+ const preview = lines.slice(0, maxLines);
9497
+ for (const line of preview) {
9498
+ logger.dim(line, 2);
9499
+ }
9500
+ if (lines.length > maxLines) {
9501
+ logger.dim(`... (${lines.length - maxLines} more lines)`, 2);
9502
+ }
9503
+ }
9504
+ logger.newLine();
9505
+ }
9506
+ } finally {
9507
+ await mf.dispose();
9508
+ }
9509
+ } catch (error) {
9510
+ if (!options.raw && !options.json) {
9511
+ logger.error(
9512
+ `Failed to inspect key: ${error instanceof Error ? error.message : String(error)}`
9513
+ );
9514
+ logger.newLine();
9515
+ }
9516
+ process.exit(1);
9517
+ }
9518
+ }
9519
+
9520
+ // src/commands/kv/list.ts
9521
+ init_string();
9522
+ init_constants2();
9523
+ import { join as join24 } from "path";
9524
+ import { Miniflare as Miniflare7 } from "miniflare";
9525
+ async function runKVList(options = {}) {
9526
+ try {
9527
+ if (!options.raw && !options.json) {
9528
+ logger.newLine();
9529
+ }
9530
+ if (options.remote) {
9531
+ if (!options.raw && !options.json) {
9532
+ logger.newLine();
9533
+ logger.warn("Remote KV operations are not yet implemented");
9534
+ logger.newLine();
9535
+ logger.admonition("info", "Coming Soon", [
9536
+ "Remote KV support is on the roadmap and will be available soon.",
9537
+ "For now, KV commands only work with local development storage."
9538
+ ]);
9539
+ logger.newLine();
9540
+ }
9541
+ process.exit(1);
9542
+ }
9543
+ const config = await loadConfig();
9544
+ if (!hasKVSetup(config)) {
9545
+ if (!options.raw && !options.json) {
9546
+ logger.error("KV storage is not configured");
9547
+ logger.newLine();
9548
+ logger.admonition("tip", "Getting Started", [
9549
+ "Run `playcademy kv init` to enable KV storage"
9550
+ ]);
9551
+ logger.newLine();
9552
+ }
9553
+ process.exit(1);
9554
+ }
9555
+ const bundle = await bundleBackend(config, {
9556
+ sourcemap: false,
9557
+ minify: false
9558
+ });
9559
+ const kvDir = join24(getWorkspace(), CLI_DIRECTORIES.KV);
9560
+ const mf = new Miniflare7({
9561
+ modules: [
9562
+ {
9563
+ type: "ESModule",
9564
+ path: "index.mjs",
9565
+ contents: bundle.code
9566
+ }
9567
+ ],
9568
+ kvNamespaces: ["KV"],
9569
+ kvPersist: kvDir,
9570
+ compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE
9571
+ });
9572
+ try {
9573
+ const kv = await mf.getKVNamespace("KV");
9574
+ const listResult = await kv.list();
9575
+ const keyNames = listResult.keys?.map((k) => k.name) || [];
9576
+ if (options.json) {
9577
+ logger.json(keyNames);
9578
+ return;
9579
+ }
9580
+ if (options.raw) {
9581
+ for (const keyName of keyNames) {
9582
+ logger.raw(keyName);
9583
+ }
9584
+ return;
9585
+ }
9586
+ if (keyNames.length === 0) {
9587
+ logger.info("No keys found in KV namespace");
9588
+ } else {
9589
+ logger.success(`Found ${keyNames.length} ${pluralize(keyNames.length, "key")}`);
9590
+ logger.newLine();
9591
+ for (const keyName of keyNames) {
9592
+ logger.bold(keyName, 1);
9593
+ }
9594
+ }
9595
+ logger.newLine();
9596
+ } finally {
9597
+ await mf.dispose();
9598
+ }
9599
+ } catch (error) {
9600
+ if (!options.raw && !options.json) {
9601
+ logger.error(
9602
+ `Failed to list KV keys: ${error instanceof Error ? error.message : String(error)}`
9603
+ );
9604
+ logger.newLine();
9605
+ }
9606
+ process.exit(1);
9607
+ }
9608
+ }
9609
+
9610
+ // src/commands/kv/seed.ts
9611
+ init_file_loader();
9612
+ init_string();
9613
+ init_constants2();
9614
+ import { join as join25 } from "path";
9615
+ import { confirm as confirm8 } from "@inquirer/prompts";
9616
+ import { Miniflare as Miniflare8 } from "miniflare";
9617
+ async function runKVSeed(seedFile, options = {}) {
9618
+ try {
9619
+ if (!options.raw && !options.json) {
9620
+ logger.newLine();
9621
+ }
9622
+ if (options.remote) {
9623
+ if (!options.raw && !options.json) {
9624
+ logger.newLine();
9625
+ logger.warn("Remote KV operations are not yet implemented");
9626
+ logger.newLine();
9627
+ logger.admonition("info", "Coming Soon", [
9628
+ "Remote KV support is on the roadmap and will be available soon.",
9629
+ "For now, KV commands only work with local development storage."
9630
+ ]);
9631
+ logger.newLine();
9632
+ }
9633
+ process.exit(1);
9634
+ }
9635
+ if (!seedFile) {
9636
+ if (!options.raw && !options.json) {
9637
+ logger.error("Seed file is required");
9638
+ logger.newLine();
9639
+ logger.admonition("tip", "Usage", [
9640
+ "`playcademy kv seed <file>`",
9641
+ "",
9642
+ "Example:",
9643
+ "`playcademy kv seed seeds/kv.json`"
9644
+ ]);
9645
+ logger.newLine();
9646
+ }
9647
+ process.exit(1);
9648
+ }
9649
+ const config = await loadConfig();
9650
+ if (!hasKVSetup(config)) {
9651
+ if (!options.raw && !options.json) {
9652
+ logger.error("KV storage is not configured");
9653
+ logger.newLine();
9654
+ logger.admonition("tip", "Getting Started", [
9655
+ "Run `playcademy kv init` to enable KV storage"
9656
+ ]);
9657
+ logger.newLine();
9658
+ }
9659
+ process.exit(1);
9660
+ }
9661
+ const workspace = getWorkspace();
9662
+ const seedData = await loadFile(seedFile, {
9663
+ cwd: workspace,
9664
+ parseJson: true
9665
+ });
9666
+ if (!seedData) {
9667
+ if (!options.raw && !options.json) {
9668
+ logger.error(`Seed file not found: ${seedFile}`);
9669
+ logger.newLine();
9670
+ }
9671
+ process.exit(1);
9672
+ }
9673
+ if (typeof seedData !== "object" || seedData === null || Array.isArray(seedData)) {
9674
+ if (!options.raw && !options.json) {
9675
+ logger.error("Seed file must contain a JSON object");
9676
+ logger.newLine();
9677
+ }
9678
+ process.exit(1);
9679
+ }
9680
+ const bundle = await bundleBackend(config, {
9681
+ sourcemap: false,
9682
+ minify: false
9683
+ });
9684
+ const kvDir = join25(workspace, CLI_DIRECTORIES.KV);
9685
+ const mf = new Miniflare8({
9686
+ modules: [
9687
+ {
9688
+ type: "ESModule",
9689
+ path: "index.mjs",
9690
+ contents: bundle.code
9691
+ }
9692
+ ],
9693
+ kvNamespaces: ["KV"],
9694
+ kvPersist: kvDir,
9695
+ compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE
9696
+ });
9697
+ try {
9698
+ const kv = await mf.getKVNamespace("KV");
9699
+ const keysToSeed = Object.keys(seedData);
9700
+ if (!options.force && !options.replace && !options.raw && !options.json) {
9701
+ const existingKeys = [];
9702
+ for (const key of keysToSeed) {
9703
+ const existingValue = await kv.get(key);
9704
+ if (existingValue !== null) {
9705
+ existingKeys.push(key);
9706
+ }
9707
+ }
9708
+ if (existingKeys.length > 0) {
9709
+ logger.warn(
9710
+ `${existingKeys.length} ${pluralize(existingKeys.length, "key")} will be overwritten`
9711
+ );
9712
+ logger.newLine();
9713
+ const confirmed = await confirm8({
9714
+ message: "Continue seeding?",
9715
+ default: false
9716
+ });
9717
+ if (!confirmed) {
9718
+ logger.info("Cancelled");
9719
+ logger.newLine();
9720
+ return;
9721
+ }
9722
+ logger.newLine();
9723
+ }
9724
+ }
9725
+ if (options.replace) {
9726
+ const listResult = await kv.list();
9727
+ const existingKeys = listResult.keys || [];
9728
+ for (const key of existingKeys) {
9729
+ await kv.delete(key.name);
9730
+ }
9731
+ }
9732
+ let seededCount = 0;
9733
+ for (const key of keysToSeed) {
9734
+ const value = seedData[key];
9735
+ const valueString = typeof value === "string" ? value : JSON.stringify(value);
9736
+ await kv.put(key, valueString);
9737
+ seededCount++;
9738
+ }
9739
+ if (options.json) {
9740
+ logger.json({
9741
+ success: true,
9742
+ seeded: seededCount,
9743
+ ...options.replace && { cleared: true }
9744
+ });
9745
+ } else if (options.raw) {
9746
+ logger.raw(seededCount.toString());
9747
+ } else {
9748
+ if (options.replace) {
9749
+ logger.success("Cleared existing keys");
9750
+ }
9751
+ logger.success(`Seeded ${seededCount} ${pluralize(seededCount, "key")}`);
9752
+ logger.newLine();
9753
+ }
9754
+ } finally {
9755
+ await mf.dispose();
9756
+ }
9757
+ } catch (error) {
9758
+ if (!options.raw && !options.json) {
9759
+ logger.error(
9760
+ `Failed to seed KV: ${error instanceof Error ? error.message : String(error)}`
9761
+ );
9762
+ logger.newLine();
9763
+ }
9764
+ process.exit(1);
9765
+ }
9766
+ }
9767
+
9768
+ // src/commands/kv/set.ts
9769
+ init_file_loader();
9770
+ init_constants2();
9771
+ import { join as join26 } from "path";
9772
+ import { Miniflare as Miniflare9 } from "miniflare";
9773
+ async function runKVSet(key, value, options = {}) {
9774
+ try {
9775
+ if (!options.raw && !options.json) {
9776
+ logger.newLine();
9777
+ }
9778
+ if (options.remote) {
9779
+ if (!options.raw && !options.json) {
9780
+ logger.newLine();
9781
+ logger.warn("Remote KV operations are not yet implemented");
9782
+ logger.newLine();
9783
+ logger.admonition("info", "Coming Soon", [
9784
+ "Remote KV support is on the roadmap and will be available soon.",
9785
+ "For now, KV commands only work with local development storage."
9786
+ ]);
9787
+ logger.newLine();
9788
+ }
9789
+ process.exit(1);
9790
+ }
9791
+ if (!key) {
9792
+ if (!options.raw && !options.json) {
9793
+ logger.error("Key is required");
9794
+ logger.newLine();
9795
+ logger.admonition("tip", "Usage", [
9796
+ "`playcademy kv set <key> <value>`",
9797
+ "`playcademy kv set <key> --file <path>`"
9798
+ ]);
9799
+ logger.newLine();
9800
+ }
9801
+ process.exit(1);
9802
+ }
9803
+ const config = await loadConfig();
9804
+ if (!hasKVSetup(config)) {
9805
+ if (!options.raw && !options.json) {
9806
+ logger.error("KV storage is not configured");
9807
+ logger.newLine();
9808
+ logger.admonition("tip", "Getting Started", [
9809
+ "Run `playcademy kv init` to enable KV storage"
9810
+ ]);
9811
+ logger.newLine();
9812
+ }
9813
+ process.exit(1);
9814
+ }
9815
+ let valueToSet;
9816
+ if (options.file) {
9817
+ const workspace = getWorkspace();
9818
+ const fileContent = await loadFile(options.file, { cwd: workspace });
9819
+ if (!fileContent) {
9820
+ if (!options.raw && !options.json) {
9821
+ logger.error(`File not found: ${options.file}`);
9822
+ logger.newLine();
9823
+ }
9824
+ process.exit(1);
9825
+ }
9826
+ valueToSet = fileContent;
9827
+ } else if (value !== void 0) {
9828
+ valueToSet = value;
9829
+ } else {
9830
+ if (!options.raw && !options.json) {
9831
+ logger.error("Value is required (either as argument or via --file)");
9832
+ logger.newLine();
9833
+ logger.admonition("tip", "Usage", [
9834
+ "`playcademy kv set <key> <value>`",
9835
+ "`playcademy kv set <key> --file <path>`"
9836
+ ]);
9837
+ logger.newLine();
9838
+ }
9839
+ process.exit(1);
9840
+ }
9841
+ const bundle = await bundleBackend(config, {
9842
+ sourcemap: false,
9843
+ minify: false
9844
+ });
9845
+ const kvDir = join26(getWorkspace(), CLI_DIRECTORIES.KV);
9846
+ const mf = new Miniflare9({
9847
+ modules: [
9848
+ {
9849
+ type: "ESModule",
9850
+ path: "index.mjs",
9851
+ contents: bundle.code
9852
+ }
9853
+ ],
9854
+ kvNamespaces: ["KV"],
9855
+ kvPersist: kvDir,
9856
+ compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE
9857
+ });
9858
+ try {
9859
+ const kv = await mf.getKVNamespace("KV");
9860
+ await kv.put(key, valueToSet);
9861
+ if (options.json) {
9862
+ logger.json({
9863
+ key,
9864
+ success: true
9865
+ });
9866
+ } else if (options.raw) {
9867
+ logger.raw("ok");
9868
+ } else {
9869
+ logger.success(`Set key: ${key}`);
9870
+ logger.newLine();
9871
+ }
9872
+ } finally {
9873
+ await mf.dispose();
9874
+ }
9875
+ } catch (error) {
9876
+ if (!options.raw && !options.json) {
9877
+ logger.error(
9878
+ `Failed to set key: ${error instanceof Error ? error.message : String(error)}`
9879
+ );
9880
+ logger.newLine();
9881
+ }
9882
+ process.exit(1);
9883
+ }
9884
+ }
9885
+
9886
+ // src/commands/kv/stats.ts
9887
+ init_string();
9888
+ init_constants2();
9889
+ import { join as join27 } from "path";
9890
+ import { Miniflare as Miniflare10 } from "miniflare";
9891
+ async function runKVStats(options = {}) {
9892
+ try {
9893
+ if (!options.raw && !options.json) {
9894
+ logger.newLine();
9895
+ }
9896
+ if (options.remote) {
9897
+ if (!options.raw && !options.json) {
9898
+ logger.newLine();
9899
+ logger.warn("Remote KV operations are not yet implemented");
9900
+ logger.newLine();
9901
+ logger.admonition("info", "Coming Soon", [
9902
+ "Remote KV support is on the roadmap and will be available soon.",
9903
+ "For now, KV commands only work with local development storage."
9904
+ ]);
9905
+ logger.newLine();
9906
+ }
9907
+ process.exit(1);
9908
+ }
9909
+ const config = await loadConfig();
9910
+ if (!hasKVSetup(config)) {
9911
+ if (!options.raw && !options.json) {
9912
+ logger.error("KV storage is not configured");
9913
+ logger.newLine();
9914
+ logger.admonition("tip", "Getting Started", [
9915
+ "Run `playcademy kv init` to enable KV storage"
9916
+ ]);
9917
+ logger.newLine();
9918
+ }
9919
+ process.exit(1);
9920
+ }
9921
+ const bundle = await bundleBackend(config, {
9922
+ sourcemap: false,
9923
+ minify: false
9924
+ });
9925
+ const kvDir = join27(getWorkspace(), CLI_DIRECTORIES.KV);
9926
+ const mf = new Miniflare10({
9927
+ modules: [
9928
+ {
9929
+ type: "ESModule",
9930
+ path: "index.mjs",
9931
+ contents: bundle.code
9932
+ }
9933
+ ],
9934
+ kvNamespaces: ["KV"],
9935
+ kvPersist: kvDir,
9936
+ compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE
9937
+ });
9938
+ try {
9939
+ const kv = await mf.getKVNamespace("KV");
9940
+ const listResult = await kv.list();
9941
+ const keys = listResult.keys || [];
9942
+ let totalSize = 0;
9943
+ let largestKey;
9944
+ const prefixes = {};
9945
+ for (const key of keys) {
9946
+ const value = await kv.get(key.name);
9947
+ const size = value ? new Blob([value]).size : 0;
9948
+ totalSize += size;
9949
+ if (!largestKey || size > largestKey.size) {
9950
+ largestKey = { name: key.name, size };
9951
+ }
9952
+ const prefixMatch = key.name.match(/^([^:]+):/);
9953
+ const prefix = prefixMatch ? `${prefixMatch[1]}:*` : "(no prefix)";
9954
+ prefixes[prefix] = (prefixes[prefix] || 0) + 1;
9955
+ }
9956
+ const stats = {
9957
+ totalKeys: keys.length,
9958
+ totalSize,
9959
+ largestKey,
9960
+ prefixes
9961
+ };
9962
+ if (options.json) {
9963
+ logger.json(stats);
9964
+ } else if (options.raw) {
9965
+ logger.raw(stats.totalKeys.toString());
9966
+ } else {
9967
+ if (stats.totalKeys === 0) {
9968
+ logger.info("No keys found in KV namespace");
9969
+ logger.newLine();
9970
+ return;
9971
+ }
9972
+ logger.data("Total Keys", stats.totalKeys.toString(), 1);
9973
+ const sizeKB = (stats.totalSize / 1024).toFixed(2);
9974
+ const sizeMB = (stats.totalSize / 1024 / 1024).toFixed(2);
9975
+ const sizeDisplay = stats.totalSize >= 1024 * 1024 ? `${sizeMB} MB` : `${sizeKB} KB`;
9976
+ logger.data("Total Size", sizeDisplay, 1);
9977
+ if (stats.largestKey) {
9978
+ const largestKB = (stats.largestKey.size / 1024).toFixed(2);
9979
+ logger.data("Largest Key", `${stats.largestKey.name} (${largestKB} KB)`, 1);
9980
+ }
9981
+ const sortedPrefixes = Object.entries(stats.prefixes).sort((a, b) => b[1] - a[1]);
9982
+ if (sortedPrefixes.length > 0) {
9983
+ logger.newLine();
9984
+ logger.data("Keys by Prefix:", "", 1);
9985
+ for (const [prefix, count] of sortedPrefixes) {
9986
+ logger.dim(`${prefix.padEnd(20)} ${count} ${pluralize(count, "key")}`, 2);
9987
+ }
9988
+ }
9989
+ logger.newLine();
9990
+ }
9991
+ } finally {
9992
+ await mf.dispose();
9993
+ }
9994
+ } catch (error) {
9995
+ if (!options.raw && !options.json) {
9996
+ logger.error(
9997
+ `Failed to get KV stats: ${error instanceof Error ? error.message : String(error)}`
9998
+ );
9999
+ logger.newLine();
10000
+ }
10001
+ process.exit(1);
10002
+ }
10003
+ }
10004
+
10005
+ // src/commands/kv/index.ts
10006
+ var kvCommand = new Command15("kv").description("Manage KV storage integration").action(() => {
10007
+ kvCommand.help();
10008
+ });
10009
+ kvCommand.command("init").description("Add KV storage integration to your project").action(runKVInit);
10010
+ kvCommand.command("list").alias("ls").description("List keys in local KV namespace (dev mode)").option("--raw", "Output key names one per line, no formatting").option("--json", "Output as JSON array").option("--remote", "Use remote KV storage (not yet implemented)").option(
10011
+ "--env <environment>",
10012
+ "Environment to use with --remote: staging (default) or production"
10013
+ ).action(runKVList);
10014
+ kvCommand.command("stats").description("Show statistics about local KV namespace (dev mode)").option("--raw", "Output total key count only").option("--json", "Output statistics as JSON").option("--remote", "Use remote KV storage (not yet implemented)").option(
10015
+ "--env <environment>",
10016
+ "Environment to use with --remote: staging (default) or production"
10017
+ ).action(runKVStats);
10018
+ kvCommand.command("get <key>").description("Get value for a specific key in local KV namespace (dev mode)").option("--raw", "Output raw value without formatting").option("--json", "Force JSON pretty-printing (default for valid JSON)").option("--remote", "Use remote KV storage (not yet implemented)").option(
10019
+ "--env <environment>",
10020
+ "Environment to use with --remote: staging (default) or production"
10021
+ ).action(runKVGet);
10022
+ kvCommand.command("inspect <key>").description("Inspect metadata and value for a specific key in local KV namespace (dev mode)").option("--raw", "Output size in bytes only").option("--json", "Output metadata as JSON").option("--remote", "Use remote KV storage (not yet implemented)").option(
10023
+ "--env <environment>",
10024
+ "Environment to use with --remote: staging (default) or production"
10025
+ ).action(runKVInspect);
10026
+ kvCommand.command("set <key> [value]").description("Set a key-value pair in local KV namespace (dev mode)").option("--raw", "Output minimal confirmation").option("--json", "Output result as JSON").option("--file <path>", "Read value from file").option("--remote", "Use remote KV storage (not yet implemented)").option(
10027
+ "--env <environment>",
10028
+ "Environment to use with --remote: staging (default) or production"
10029
+ ).action(runKVSet);
10030
+ kvCommand.command("delete <key>").alias("del").alias("rm").description("Delete a specific key from local KV namespace (dev mode)").option("--raw", "Output minimal confirmation").option("--json", "Output result as JSON").option("--remote", "Use remote KV storage (not yet implemented)").option(
10031
+ "--env <environment>",
10032
+ "Environment to use with --remote: staging (default) or production"
10033
+ ).action(runKVDelete);
10034
+ kvCommand.command("clear").description("Clear all keys from local KV namespace (dev mode)").option("--raw", "Output count of deleted keys").option("--json", "Output result as JSON").option("-f, --force", "Skip confirmation prompt").option("--remote", "Use remote KV storage (not yet implemented)").option(
10035
+ "--env <environment>",
10036
+ "Environment to use with --remote: staging (default) or production"
10037
+ ).action(runKVClear);
10038
+ kvCommand.command("seed <file>").description("Seed KV namespace with key-value pairs from JSON file (dev mode)").option("--raw", "Output count of seeded keys").option("--json", "Output result as JSON").option("--replace", "Clear existing keys before seeding").option("-f, --force", "Skip confirmation prompt").option("--remote", "Use remote KV storage (not yet implemented)").option(
10039
+ "--env <environment>",
10040
+ "Environment to use with --remote: staging (default) or production"
10041
+ ).action(runKVSeed);
10042
+
10043
+ // src/commands/profiles/index.ts
10044
+ import { Command as Command19 } from "commander";
10045
+
10046
+ // src/commands/profiles/list.ts
10047
+ init_string();
10048
+ import { Command as Command16 } from "commander";
10049
+ async function listProfilesAction() {
10050
+ try {
10051
+ const profilesMap = await listProfiles();
10052
+ logger.newLine();
10053
+ let hasAnyProfiles = false;
10054
+ for (const [, profiles] of profilesMap.entries()) {
10055
+ if (profiles.length > 0) {
10056
+ hasAnyProfiles = true;
10057
+ break;
10058
+ }
10059
+ }
10060
+ if (!hasAnyProfiles) {
10061
+ logger.warn("No authentication profiles found");
10062
+ logger.newLine();
10063
+ logger.admonition("tip", "Getting Started", [
10064
+ "Run `playcademy login` to create your first profile"
10065
+ ]);
10066
+ logger.newLine();
10067
+ return;
10068
+ }
10069
+ for (const [environment, profiles] of profilesMap.entries()) {
10070
+ if (profiles.length === 0) continue;
10071
+ const tableData = [];
10072
+ for (const profileName of profiles) {
10073
+ const profile = await getProfile(environment, profileName);
10074
+ tableData.push({
10075
+ Profile: profileName,
10076
+ Email: profile?.email ?? ""
10077
+ });
10078
+ }
10079
+ if (tableData.length > 0) {
10080
+ const envTitle = capitalize(environment);
10081
+ logger.table(tableData, envTitle);
10082
+ logger.newLine();
10083
+ }
10084
+ }
10085
+ } catch (error) {
10086
+ logger.error(`Failed to list profiles: ${getErrorMessage(error)}`);
10087
+ process.exit(1);
10088
+ }
10089
+ }
10090
+ var listCommand2 = new Command16("list").alias("ls").description("List all stored authentication profiles").action(listProfilesAction);
10091
+
10092
+ // src/commands/profiles/remove.ts
10093
+ import { bold as bold6 } from "colorette";
10094
+ import { Command as Command17 } from "commander";
10095
+ var removeCommand = new Command17("remove").alias("rm").description('Remove an authentication profile (defaults to "default")').argument("[name]", "Profile name to remove", "default").option("--env <environment>", "Environment to remove profile from (staging or production)").action(async (name, options) => {
10096
+ const { env } = options;
10097
+ const environment = ensureEnvironment(env, logger);
10098
+ try {
10099
+ logger.newLine();
10100
+ const profilesMap = await listProfiles();
10101
+ const envProfiles = profilesMap.get(environment) || [];
10102
+ if (!envProfiles.includes(name)) {
10103
+ logger.error(`Profile "${name}" not found in ${environment}`);
10104
+ logger.newLine();
10105
+ process.exit(1);
10106
+ }
10107
+ await removeProfile(environment, name);
10108
+ logger.admonition("note", "Removed!", [
10109
+ `Profile ${bold6(name)} removed from ${environment}`,
10110
+ environment === "production" ? `To re-authenticate run \`playcademy login --env ${environment}\`` : "To re-authenticate run `playcademy login`"
10111
+ ]);
10112
+ logger.newLine();
10113
+ } catch (error) {
10114
+ logAndExit(error, logger, { prefix: "Failed to remove profile" });
10115
+ }
10116
+ });
10117
+
10118
+ // src/commands/profiles/reset.ts
10119
+ init_string();
10120
+ import { confirm as confirm9 } from "@inquirer/prompts";
10121
+ import { Command as Command18 } from "commander";
10122
+ var resetCommand = new Command18("reset").description(
10123
+ "Remove all authentication profiles across all environments (requires confirmation)"
10124
+ ).alias("clear").action(async () => {
10125
+ try {
10126
+ const profilesMap = await listProfiles();
10127
+ let totalProfiles = 0;
10128
+ for (const profiles of profilesMap.values()) {
10129
+ totalProfiles += profiles.length;
10130
+ }
10131
+ if (totalProfiles === 0) {
10132
+ logger.newLine();
10133
+ logger.warn("No authentication profiles found");
10134
+ logger.newLine();
10135
+ return;
10136
+ }
10137
+ logger.newLine();
10138
+ logger.warn(
9028
10139
  `This will remove ${totalProfiles} authentication ${pluralize(totalProfiles, "profile")} across all environments:`
9029
10140
  );
9030
10141
  logger.newLine();
@@ -9044,7 +10155,7 @@ var resetCommand = new Command17("reset").description(
9044
10155
  logger.newLine();
9045
10156
  }
9046
10157
  }
9047
- const confirmed = await confirm7({
10158
+ const confirmed = await confirm9({
9048
10159
  message: "Are you sure you want to remove all profiles?",
9049
10160
  default: false
9050
10161
  });
@@ -9082,19 +10193,19 @@ var resetCommand = new Command17("reset").description(
9082
10193
  });
9083
10194
 
9084
10195
  // src/commands/profiles/index.ts
9085
- var profilesCommand = new Command18("profiles").description("Manage authentication profiles").action(listProfilesAction);
10196
+ var profilesCommand = new Command19("profiles").description("Manage authentication profiles").action(listProfilesAction);
9086
10197
  profilesCommand.addCommand(listCommand2);
9087
10198
  profilesCommand.addCommand(removeCommand);
9088
10199
  profilesCommand.addCommand(resetCommand);
9089
10200
 
9090
10201
  // src/commands/timeback/index.ts
9091
- import { Command as Command24 } from "commander";
10202
+ import { Command as Command25 } from "commander";
9092
10203
 
9093
10204
  // src/commands/timeback/cleanup.ts
9094
10205
  init_src();
9095
- import { confirm as confirm8 } from "@inquirer/prompts";
9096
- import { Command as Command19 } from "commander";
9097
- var cleanupCommand = new Command19("cleanup").description("Remove TimeBack integration for your game").option(
10206
+ import { confirm as confirm10 } from "@inquirer/prompts";
10207
+ import { Command as Command20 } from "commander";
10208
+ var cleanupCommand = new Command20("cleanup").description("Remove TimeBack integration for your game").option(
9098
10209
  "--env <environment>",
9099
10210
  "Environment to remove TimeBack integration from (staging or production)"
9100
10211
  ).action(async (options) => {
@@ -9116,7 +10227,7 @@ var cleanupCommand = new Command19("cleanup").description("Remove TimeBack integ
9116
10227
  return;
9117
10228
  }
9118
10229
  displayCleanupWarning(integration, game.displayName, logger);
9119
- const confirmed = await confirm8({
10230
+ const confirmed = await confirm10({
9120
10231
  message: "Are you sure you want to remove TimeBack integration?",
9121
10232
  default: false
9122
10233
  });
@@ -9145,8 +10256,8 @@ var cleanupCommand = new Command19("cleanup").description("Remove TimeBack integ
9145
10256
  });
9146
10257
 
9147
10258
  // src/commands/timeback/init.ts
9148
- import { Command as Command20 } from "commander";
9149
- var initCommand2 = new Command20("init").description("Add TimeBack integration to your project").action(async () => {
10259
+ import { Command as Command21 } from "commander";
10260
+ var initCommand2 = new Command21("init").description("Add TimeBack integration to your project").action(async () => {
9150
10261
  try {
9151
10262
  logger.newLine();
9152
10263
  const configPath = await findConfigPath().catch(() => {
@@ -9199,8 +10310,8 @@ var initCommand2 = new Command20("init").description("Add TimeBack integration t
9199
10310
 
9200
10311
  // src/commands/timeback/setup.ts
9201
10312
  init_src();
9202
- import { Command as Command21 } from "commander";
9203
- var setupCommand = new Command21("setup").description("Set up TimeBack integration for your game").option("--dry-run", "Validate configuration without creating resources").option("--verbose, -v", "Output detailed information").option(
10313
+ import { Command as Command22 } from "commander";
10314
+ var setupCommand = new Command22("setup").description("Set up TimeBack integration for your game").option("--dry-run", "Validate configuration without creating resources").option("--verbose, -v", "Output detailed information").option(
9204
10315
  "--env <environment>",
9205
10316
  "Environment to set up TimeBack integration in (staging or production)"
9206
10317
  ).action(async (options) => {
@@ -9292,10 +10403,10 @@ var setupCommand = new Command21("setup").description("Set up TimeBack integrati
9292
10403
  // src/commands/timeback/update.ts
9293
10404
  init_src();
9294
10405
  init_string();
9295
- import { confirm as confirm9 } from "@inquirer/prompts";
10406
+ import { confirm as confirm11 } from "@inquirer/prompts";
9296
10407
  import { green as green4, red as red3 } from "colorette";
9297
- import { Command as Command22 } from "commander";
9298
- var updateCommand = new Command22("update").description("Update TimeBack integration configuration for your game").option("--verbose, -v", "Output detailed information").option(
10408
+ import { Command as Command23 } from "commander";
10409
+ var updateCommand = new Command23("update").description("Update TimeBack integration configuration for your game").option("--verbose, -v", "Output detailed information").option(
9299
10410
  "--env <environment>",
9300
10411
  "Environment to update TimeBack integration in (staging or production)"
9301
10412
  ).action(async (options) => {
@@ -9373,7 +10484,7 @@ var updateCommand = new Command22("update").description("Update TimeBack integra
9373
10484
  logger.data(change.label, `${red3(change.current)} \u2192 ${green4(change.next)}`, 1);
9374
10485
  }
9375
10486
  logger.newLine();
9376
- const confirmed = await confirm9({
10487
+ const confirmed = await confirm11({
9377
10488
  message: `Update ${changeDetails.length} ${pluralize(changeDetails.length, "field")} in TimeBack?`,
9378
10489
  default: false
9379
10490
  });
@@ -9413,8 +10524,8 @@ var updateCommand = new Command22("update").description("Update TimeBack integra
9413
10524
 
9414
10525
  // src/commands/timeback/verify.ts
9415
10526
  init_src();
9416
- import { Command as Command23 } from "commander";
9417
- var verifyCommand = new Command23("verify").description("Verify TimeBack integration for your game").option("--verbose, -v", "Output detailed resource information").option(
10527
+ import { Command as Command24 } from "commander";
10528
+ var verifyCommand = new Command24("verify").description("Verify TimeBack integration for your game").option("--verbose, -v", "Output detailed resource information").option(
9418
10529
  "--env <environment>",
9419
10530
  "Environment to verify TimeBack integration in (staging or production)"
9420
10531
  ).action(async (options) => {
@@ -9464,7 +10575,7 @@ var verifyCommand = new Command23("verify").description("Verify TimeBack integra
9464
10575
  });
9465
10576
 
9466
10577
  // src/commands/timeback/index.ts
9467
- var timebackCommand = new Command24("timeback").description(
10578
+ var timebackCommand = new Command25("timeback").description(
9468
10579
  "TimeBack integration management"
9469
10580
  );
9470
10581
  timebackCommand.addCommand(initCommand2);
@@ -9474,15 +10585,15 @@ timebackCommand.addCommand(verifyCommand);
9474
10585
  timebackCommand.addCommand(cleanupCommand);
9475
10586
 
9476
10587
  // src/commands/debug/index.ts
9477
- import { Command as Command26 } from "commander";
10588
+ import { Command as Command27 } from "commander";
9478
10589
 
9479
10590
  // src/commands/debug/bundle.ts
9480
10591
  init_src();
9481
10592
  init_constants2();
9482
- import { writeFileSync as writeFileSync8 } from "fs";
9483
- import { join as join19 } from "path";
9484
- import { Command as Command25 } from "commander";
9485
- var bundleCommand = new Command25("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) => {
10593
+ import { writeFileSync as writeFileSync7 } from "fs";
10594
+ import { join as join28 } from "path";
10595
+ import { Command as Command26 } from "commander";
10596
+ 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) => {
9486
10597
  try {
9487
10598
  const workspace = getWorkspace();
9488
10599
  logger.newLine();
@@ -9510,8 +10621,8 @@ var bundleCommand = new Command25("bundle").description("Bundle and inspect the
9510
10621
  }),
9511
10622
  (result) => `Bundled ${formatSize(result.code.length)}`
9512
10623
  );
9513
- const outputPath = join19(workspace, options.output);
9514
- writeFileSync8(outputPath, bundle.code, "utf-8");
10624
+ const outputPath = join28(workspace, options.output);
10625
+ writeFileSync7(outputPath, bundle.code, "utf-8");
9515
10626
  logger.success(`Bundle saved to ${options.output}`);
9516
10627
  logger.newLine();
9517
10628
  logger.highlight("Bundle Analysis");
@@ -9551,7 +10662,7 @@ var bundleCommand = new Command25("bundle").description("Bundle and inspect the
9551
10662
  });
9552
10663
 
9553
10664
  // src/commands/debug/index.ts
9554
- var debugCommand = new Command26("debug").description("Debug and inspect game builds").addCommand(bundleCommand);
10665
+ var debugCommand = new Command27("debug").description("Debug and inspect game builds").addCommand(bundleCommand);
9555
10666
 
9556
10667
  // src/index.ts
9557
10668
  var __dirname = dirname4(fileURLToPath2(import.meta.url));
@@ -9568,6 +10679,7 @@ program.addCommand(meCommand);
9568
10679
  program.addCommand(devCommand);
9569
10680
  program.addCommand(apiCommand);
9570
10681
  program.addCommand(dbCommand);
10682
+ program.addCommand(kvCommand);
9571
10683
  program.addCommand(gamesCommand);
9572
10684
  program.addCommand(deployCommand);
9573
10685
  program.addCommand(timebackCommand);
@@ -9607,6 +10719,7 @@ export {
9607
10719
  displaySuccessMessage,
9608
10720
  ensureEnvironment,
9609
10721
  ensureGameExists,
10722
+ ensurePlaycademyGitignore,
9610
10723
  findConfigPath,
9611
10724
  formatSize,
9612
10725
  generateEntryCode,
@@ -9627,6 +10740,7 @@ export {
9627
10740
  getDeployedGame,
9628
10741
  getDeploymentId,
9629
10742
  getDevServerPidPath,
10743
+ getDrizzleKitApiExports,
9630
10744
  getEnvironment,
9631
10745
  getErrorMessage,
9632
10746
  getGameFromConfig,
@@ -9647,6 +10761,7 @@ export {
9647
10761
  hasCustomRoutes,
9648
10762
  hasCustomRoutesChanged,
9649
10763
  hasDatabaseSetup,
10764
+ hasKVSetup,
9650
10765
  hasLocalCustomRoutes,
9651
10766
  hasMandatoryFieldsMissing,
9652
10767
  hasOptionalFieldsMissing,