@geekmidas/cli 10.0.0-alpha.6 → 10.0.0-alpha.9

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.
Files changed (70) hide show
  1. package/README.md +12 -8
  2. package/bin/adjacent-tsconfig.mjs +59 -0
  3. package/bin/gkm.mjs +7 -1
  4. package/dist/{config-CNz5KaSR.cjs → config-BZ1F0WuA.cjs} +6 -50
  5. package/dist/config-BZ1F0WuA.cjs.map +1 -0
  6. package/dist/{config-CxP7P-qG.mjs → config-DVVph9yz.mjs} +7 -45
  7. package/dist/config-DVVph9yz.mjs.map +1 -0
  8. package/dist/config.cjs +2 -2
  9. package/dist/config.d.cts +2 -2
  10. package/dist/config.d.mts +2 -2
  11. package/dist/config.mjs +2 -2
  12. package/dist/{credentials-DzsXZzkx.cjs → credentials-Xamqe_NJ.cjs} +19 -18
  13. package/dist/credentials-Xamqe_NJ.cjs.map +1 -0
  14. package/dist/{credentials-BK3Z93MX.mjs → credentials-dP8vRVRv.mjs} +19 -18
  15. package/dist/credentials-dP8vRVRv.mjs.map +1 -0
  16. package/dist/discover-CAlFgYec.mjs.map +1 -1
  17. package/dist/discover-DTmFeloL.cjs.map +1 -1
  18. package/dist/{index-BGGdjueF.d.cts → index-C6e-ULLx.d.mts} +2 -2
  19. package/dist/index-C6e-ULLx.d.mts.map +1 -0
  20. package/dist/{index-DEbaleny.d.mts → index-CR5urhWX.d.cts} +2 -2
  21. package/dist/index-CR5urhWX.d.cts.map +1 -0
  22. package/dist/index.cjs +596 -248
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.mjs +596 -248
  25. package/dist/index.mjs.map +1 -1
  26. package/dist/{openapi-D-dx9Akj.cjs → openapi-SjsgARie.cjs} +4 -4
  27. package/dist/{openapi-D-dx9Akj.cjs.map → openapi-SjsgARie.cjs.map} +1 -1
  28. package/dist/{openapi-Bw8DWFWz.mjs → openapi-Xx0A7Wg0.mjs} +4 -4
  29. package/dist/{openapi-Bw8DWFWz.mjs.map → openapi-Xx0A7Wg0.mjs.map} +1 -1
  30. package/dist/openapi.cjs +3 -3
  31. package/dist/openapi.d.cts +3 -3
  32. package/dist/openapi.d.mts +3 -3
  33. package/dist/openapi.mjs +3 -3
  34. package/dist/reconcile/public.d.cts +1 -1
  35. package/dist/reconcile/public.d.mts +1 -1
  36. package/dist/{setup-DGIPd2fh.mjs → setup-BA8ciTPU.mjs} +4 -4
  37. package/dist/setup-BsZvZP6f.cjs +11 -0
  38. package/dist/{setup-BWMhbW-p.mjs → setup-C8KOx8_8.mjs} +5 -5
  39. package/dist/setup-C8KOx8_8.mjs.map +1 -0
  40. package/dist/{setup-D7L4BgBT.cjs → setup-hC6hI86f.cjs} +5 -5
  41. package/dist/setup-hC6hI86f.cjs.map +1 -0
  42. package/dist/{types-BLSNdrk0.d.cts → types-6pkp2RRj.d.mts} +48 -4
  43. package/dist/types-6pkp2RRj.d.mts.map +1 -0
  44. package/dist/{types-C3POXhw3.d.mts → types-Y3fKQSAK.d.cts} +45 -1
  45. package/dist/types-Y3fKQSAK.d.cts.map +1 -0
  46. package/dist/workspace/index.cjs +1 -1
  47. package/dist/workspace/index.d.cts +2 -2
  48. package/dist/workspace/index.d.mts +2 -2
  49. package/dist/workspace/index.mjs +1 -1
  50. package/dist/workspace-BzrG_2SI.mjs +8 -0
  51. package/dist/{workspace-LKiNyaDy.cjs → workspace-C6VJQVtG.cjs} +79 -1
  52. package/dist/workspace-C6VJQVtG.cjs.map +1 -0
  53. package/dist/{workspace-CGaUAw5A.cjs → workspace-XXtNhqJl.cjs} +3 -3
  54. package/dist/{workspace-DIuZx-cc.mjs → workspace-zoqRbihZ.mjs} +62 -2
  55. package/dist/workspace-zoqRbihZ.mjs.map +1 -0
  56. package/package.json +14 -13
  57. package/dist/config-CNz5KaSR.cjs.map +0 -1
  58. package/dist/config-CxP7P-qG.mjs.map +0 -1
  59. package/dist/credentials-BK3Z93MX.mjs.map +0 -1
  60. package/dist/credentials-DzsXZzkx.cjs.map +0 -1
  61. package/dist/index-BGGdjueF.d.cts.map +0 -1
  62. package/dist/index-DEbaleny.d.mts.map +0 -1
  63. package/dist/setup-8IWPsRIN.cjs +0 -11
  64. package/dist/setup-BWMhbW-p.mjs.map +0 -1
  65. package/dist/setup-D7L4BgBT.cjs.map +0 -1
  66. package/dist/types-BLSNdrk0.d.cts.map +0 -1
  67. package/dist/types-C3POXhw3.d.mts.map +0 -1
  68. package/dist/workspace-CnTqIlB3.mjs +0 -8
  69. package/dist/workspace-DIuZx-cc.mjs.map +0 -1
  70. package/dist/workspace-LKiNyaDy.cjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  import { __require } from "./chunk-Duj1WY3L.mjs";
3
3
  import { ConstructGenerator, DEFAULT_CACHE, discover, isPartitionedRoutes } from "./discover-CAlFgYec.mjs";
4
- import { derivedApps, getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceConfig, parseModuleConfig, resolveAppSpec } from "./config-CxP7P-qG.mjs";
5
- import { allConstructGlobs, getAppBuildOrder, getAppGkmConfig, getDependencyEnvVars, getDeployTargetError, getPublicEnvPrefix, isDeployTargetSupported, stripPublicPrefix } from "./workspace-DIuZx-cc.mjs";
4
+ import { derivedApps, getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceConfig, parseModuleConfig } from "./config-DVVph9yz.mjs";
5
+ import { allConstructGlobs, assertDeployedStage, deployedProblems, getAppBuildOrder, getAppGkmConfig, getDependencyEnvVars, getDeployTargetError, getPublicEnvPrefix, isDeployTargetSupported, stageProblems, stripPublicPrefix } from "./workspace-zoqRbihZ.mjs";
6
6
  import { getCredentialsPath, getDokployCredentials, getDokployRegistryId, getDokployToken, removeDokployCredentials, storeDokployCredentials, storeDokployRegistryId, storeHostingerToken } from "./credentials-B5zRHdAl.mjs";
7
- import { applyPostgres, cacheBackendOf, constructGlobs, createCredentialsPreload, createEntryWrapper, derivedContainers, emailBackendOf, findAvailablePort, isPortAvailable, loadEnvFiles, loadSecretsForApp, prepareEntryCredentials, providerOf, reconcileWorkspace, resolveServicePorts, resourceName, rewriteUrlsWithPorts, startWorkspaceServices, storageBackendOf, usesConstructs } from "./credentials-BK3Z93MX.mjs";
7
+ import { applyPostgres, cacheBackendOf, constructGlobs, createCredentialsPreload, createEntryWrapper, derivedContainers, emailBackendOf, findAvailablePort, isPortAvailable, loadEnvFiles, loadSecretsForApp, prepareEntryCredentials, providerOf, reconcileWorkspace, resolveServicePorts, resourceName, rewriteUrlsWithPorts, startWorkspaceServices, storageBackendOf, usesConstructs } from "./credentials-dP8vRVRv.mjs";
8
8
  import { createStageSecrets, rotateServicePassword } from "./generator-VE2JrFlY.mjs";
9
9
  import { getKeyPath, maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, writeStageSecrets } from "./storage-Bi24WfoW.mjs";
10
- import { EndpointGenerator, OPENAPI_OUTPUT_PATH, generateOpenApi, generateOpenApiFrom, normalizeRoutes, openapiCommand, resolveOpenApiConfig, runtimeFor } from "./openapi-Bw8DWFWz.mjs";
10
+ import { EndpointGenerator, OPENAPI_OUTPUT_PATH, generateOpenApi, generateOpenApiFrom, normalizeRoutes, openapiCommand, resolveOpenApiConfig, runtimeFor } from "./openapi-Xx0A7Wg0.mjs";
11
11
  import { withCompute, withRoutes } from "./emit-CiZS_5Ub.mjs";
12
12
  import { DokployApi } from "./dokploy-api-aTFPz7_4.mjs";
13
13
  import { encryptSecrets } from "./encryption-D-abyhZx.mjs";
@@ -15,7 +15,7 @@ import { CachedStateProvider } from "./CachedStateProvider-DTniehMP.mjs";
15
15
  import { generateDbPassword, generateDbUrl, generateFullstackCustomSecrets } from "./fullstack-secrets-D1o9-G5l.mjs";
16
16
  import { generateReactQueryCommand } from "./openapi-react-query-Byltvm_7.mjs";
17
17
  import "./sync-CmEuNZsB.mjs";
18
- import { setupCommand, trustCommand } from "./setup-BWMhbW-p.mjs";
18
+ import { setupCommand, trustCommand } from "./setup-C8KOx8_8.mjs";
19
19
  import { createRequire } from "node:module";
20
20
  import { copyFileSync, existsSync, readFileSync, unlinkSync } from "node:fs";
21
21
  import { basename, dirname, isAbsolute, join, parse, relative, resolve } from "node:path";
@@ -43,7 +43,7 @@ import prompts from "prompts";
43
43
 
44
44
  //#region package.json
45
45
  var name = "@geekmidas/cli";
46
- var version = "10.0.0-alpha.5";
46
+ var version = "10.0.0-alpha.9";
47
47
  var description = "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs";
48
48
  var private$1 = false;
49
49
  var type = "module";
@@ -143,6 +143,7 @@ var dependencies = {
143
143
  "dotenv": "~18.0.1",
144
144
  "esbuild": "~0.28.2",
145
145
  "fast-glob": "~3.3.3",
146
+ "get-tsconfig": "~4.10.1",
146
147
  "hono": "~4.13.8",
147
148
  "lodash.kebabcase": "~4.1.1",
148
149
  "openapi-typescript": "~7.13.0",
@@ -157,8 +158,8 @@ var devDependencies = {
157
158
  "@types/node": "~24.9.1",
158
159
  "@types/pg": "~8.23.1",
159
160
  "@types/prompts": "~2.4.9",
160
- "typescript": "^5.8.2",
161
- "vitest": "^3.2.4",
161
+ "typescript": "~5.8.2",
162
+ "vitest": "~3.2.4",
162
163
  "zod": "~4.6.5"
163
164
  };
164
165
  var peerDependencies = { "@geekmidas/telescope": "workspace:~" };
@@ -1377,9 +1378,9 @@ async function devCommand(options) {
1377
1378
  if (enableOpenApi) await generateOpenApiFrom(config.constructs, { openapi: config.openapi });
1378
1379
  const runtime = config.runtime ?? "node";
1379
1380
  let secretsJsonPath;
1380
- const appSecrets = await loadSecretsForApp(secretsRoot, workspaceAppName);
1381
+ const appSecrets = await loadSecretsForApp(secretsRoot, config.stages.local, workspaceAppName);
1381
1382
  if (workspace && usesConstructs(workspace)) {
1382
- const reconciled = await reconcileWorkspace(workspace, { stage: "development" });
1383
+ const reconciled = await reconcileWorkspace(workspace, { stage: workspace.stages.local });
1383
1384
  if (reconciled.changed && reconciled.plan.containers.length > 0) {
1384
1385
  logger$9.log(`🐳 Services: ${reconciled.plan.containers.join(", ")}`);
1385
1386
  for (const [container, address] of Object.entries(reconciled.addresses)) logger$9.log(` ${container}: ${address}`);
@@ -1661,15 +1662,15 @@ async function validateFrontendApps(workspace) {
1661
1662
  */
1662
1663
  async function loadDevSecrets(workspace) {
1663
1664
  if (!workspace.secrets.enabled) return {};
1664
- const stages = ["dev", "development"];
1665
- for (const stage of stages) if (secretsExist(stage, workspace.root)) {
1665
+ const stage = workspace.stages.local;
1666
+ if (secretsExist(stage, workspace.root)) {
1666
1667
  const secrets = await readStageSecrets(stage, workspace.root);
1667
1668
  if (secrets) {
1668
1669
  logger$9.log(`🔐 Loading secrets from stage: ${stage}`);
1669
1670
  return toEmbeddableSecrets(secrets);
1670
1671
  }
1671
1672
  }
1672
- logger$9.warn("⚠️ Secrets enabled but no dev/development secrets found. Run \"gkm setup\" to initialize your development environment");
1673
+ logger$9.warn(`⚠️ Secrets enabled but no "${stage}" secrets found. Run "gkm setup" to initialize the local stage`);
1673
1674
  return {};
1674
1675
  }
1675
1676
  /**
@@ -1713,7 +1714,7 @@ async function workspaceDevCommand(workspace, options) {
1713
1714
  const rawSecrets = await loadDevSecrets(workspace);
1714
1715
  let secretsEnv;
1715
1716
  if (usesConstructs(workspace)) {
1716
- const reconciled = await reconcileWorkspace(workspace, { stage: "development" });
1717
+ const reconciled = await reconcileWorkspace(workspace, { stage: workspace.stages.local });
1717
1718
  if (reconciled.changed && reconciled.plan.containers.length > 0) {
1718
1719
  logger$9.log(`🐳 Services: ${reconciled.plan.containers.join(", ")}`);
1719
1720
  for (const [container, address] of Object.entries(reconciled.addresses)) logger$9.log(` ${container}: ${address}`);
@@ -2458,6 +2459,7 @@ function rootGkmConfig(workspace) {
2458
2459
  const own = only ? getAppGkmConfig(workspace, only[0]) : void 0;
2459
2460
  return own ?? {
2460
2461
  services: workspace.services,
2462
+ stages: workspace.stages,
2461
2463
  constructs: allConstructGlobs(workspace)
2462
2464
  };
2463
2465
  }
@@ -2465,9 +2467,9 @@ async function buildCommand(options) {
2465
2467
  const loadedConfig = await loadWorkspaceConfig();
2466
2468
  if (loadedConfig.type === "workspace") {
2467
2469
  const cwd = resolve(process.cwd());
2468
- const workspaceRoot = resolve(loadedConfig.workspace.root);
2469
- const isAtWorkspaceRoot = cwd === workspaceRoot;
2470
- const someAppIsElsewhere = Object.values(loadedConfig.workspace.apps).some((app) => resolve(workspaceRoot, app.path) !== workspaceRoot);
2470
+ const workspaceRoot$1 = resolve(loadedConfig.workspace.root);
2471
+ const isAtWorkspaceRoot = cwd === workspaceRoot$1;
2472
+ const someAppIsElsewhere = Object.values(loadedConfig.workspace.apps).some((app) => resolve(workspaceRoot$1, app.path) !== workspaceRoot$1);
2471
2473
  if (isAtWorkspaceRoot && someAppIsElsewhere) {
2472
2474
  logger$7.log("📦 Detected workspace configuration");
2473
2475
  return workspaceBuildCommand(loadedConfig.workspace, options);
@@ -2506,7 +2508,9 @@ async function buildCommand(options) {
2506
2508
  sources: constructSources
2507
2509
  }) : {};
2508
2510
  const surfaces = Object.values(declared).filter((d) => d.kind === "rest-api");
2509
- const primary = surfaces.find((d) => d.endpoints.length === 0) ?? surfaces[0];
2511
+ const workspaceRoot = loadedConfig.workspace.root;
2512
+ const served = surfaces.find((d) => resolve(workspaceRoot, d.path) === resolve(process.cwd()));
2513
+ const primary = served ?? surfaces.find((d) => d.endpoints.length === 0) ?? surfaces[0];
2510
2514
  const browsable = Object.entries(declared).find(([, d]) => d.kind === "database");
2511
2515
  const browsableSource = browsable ? constructSources[browsable[0]] : void 0;
2512
2516
  const owners = {};
@@ -2557,7 +2561,7 @@ async function buildCommand(options) {
2557
2561
  const queueGenerator = new QueueGenerator();
2558
2562
  const topicGenerator = new TopicGenerator();
2559
2563
  const code = constructGlobs$1;
2560
- const [allEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics] = await Promise.all([
2564
+ const [loadedEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics] = await Promise.all([
2561
2565
  endpointGenerator.load(code),
2562
2566
  functionGenerator.load(code),
2563
2567
  cronGenerator.load(code),
@@ -2565,6 +2569,7 @@ async function buildCommand(options) {
2565
2569
  queueGenerator.load(code),
2566
2570
  topicGenerator.load(code)
2567
2571
  ]);
2572
+ const allEndpoints = primary ? loadedEndpoints.filter(({ construct }) => !construct.surface || construct.surface.id === primary.id) : loadedEndpoints;
2568
2573
  logger$7.log(`Found ${allEndpoints.length} endpoints`);
2569
2574
  logger$7.log(`Found ${allFunctions.length} functions`);
2570
2575
  logger$7.log(`Found ${allCrons.length} crons`);
@@ -2572,7 +2577,7 @@ async function buildCommand(options) {
2572
2577
  logger$7.log(`Found ${allQueues.length} queues`);
2573
2578
  logger$7.log(`Found ${allTopics.length} topics`);
2574
2579
  if (allEndpoints.length === 0 && allFunctions.length === 0 && allCrons.length === 0 && allSubscribers.length === 0 && allQueues.length === 0 && allTopics.length === 0) {
2575
- const selfServing = Object.entries(declared).find(([id, d]) => d.kind === "rest-api" && d.endpoints.length > 0 && constructSources[id] !== void 0 && resolve(loadedConfig.workspace.root, resolveAppSpec(id, d.app ?? {}, loadedConfig.workspace.root).path) === resolve(process.cwd()));
2580
+ const selfServing = served && served.endpoints.length > 0 && constructSources[served.id] !== void 0 ? [served.id, served] : void 0;
2576
2581
  if (selfServing) {
2577
2582
  const [id] = selfServing;
2578
2583
  await writeSurfaceEntry(join(process.cwd(), ".gkm", "server"), constructSources[id]);
@@ -2843,7 +2848,6 @@ async function workspaceBuildCommand(workspace, options) {
2843
2848
  });
2844
2849
  }
2845
2850
  logger$7.log(`\n✅ Workspace build complete!`);
2846
- await openapiCommand({ cwd: workspace.root });
2847
2851
  logger$7.log(`\n📋 Build Summary:`);
2848
2852
  for (const result of results) {
2849
2853
  const icon = result.type === "backend" ? "⚙️" : result.type === "mobile" ? "📱" : "🌐";
@@ -5693,7 +5697,7 @@ async function workspaceDockerCommand(workspace, options) {
5693
5697
  logger$5.log(`\n Generated: .dockerignore (workspace root)`);
5694
5698
  const dockerCompose = generateWorkspaceCompose(workspace, {
5695
5699
  registry: options.registry,
5696
- containers: await derivedContainers(workspace, "development")
5700
+ containers: await derivedContainers(workspace, workspace.stages.local)
5697
5701
  });
5698
5702
  const composePath = join(dockerDir, "docker-compose.yml");
5699
5703
  await writeFile(composePath, dockerCompose);
@@ -7139,6 +7143,7 @@ async function workspaceDeployCommand(configured, options) {
7139
7143
  */
7140
7144
  async function deployCommand(options) {
7141
7145
  const loadedConfig = await loadWorkspaceConfig();
7146
+ assertDeployedStage(loadedConfig.workspace.stages, options.stage);
7142
7147
  return workspaceDeployCommand(loadedConfig.workspace, options);
7143
7148
  }
7144
7149
 
@@ -7514,47 +7519,47 @@ const CONSTRUCTS_GLOB = "./src/constructs/**/*.ts";
7514
7519
  */
7515
7520
  const WORKSPACE_CONSTRUCTS_GLOB = "./constructs/**/*.ts";
7516
7521
  /**
7517
- * The database a project named `name` declares.
7522
+ * Where each routes structure puts an app's handlers, relative to the app.
7518
7523
  *
7519
- * Named after the project rather than `Db`, so two apps in one workspace can
7520
- * declare one each without colliding on an id or on an env key.
7524
+ * `.ts` only, never `.tsx`: a TanStack site keeps its own `src/routes/`, and
7525
+ * this glob must not import a frontend's route components.
7521
7526
  */
7522
- function databaseFor(name$1) {
7523
- const id = canonicalId(name$1);
7524
- return {
7525
- id,
7526
- service: serviceKey(id),
7527
- urlKey: provideKey(id, "url"),
7528
- ownerUrlKey: provideKey(id, "ownerUrl")
7529
- };
7530
- }
7531
- /** The bucket a project named `name` declares. */
7532
- function storageFor(name$1) {
7533
- const id = canonicalId(`${name$1}-uploads`);
7534
- return {
7535
- id,
7536
- service: serviceKey(id),
7537
- urlKey: provideKey(id, "url")
7538
- };
7527
+ function routesGlob(structure) {
7528
+ switch (structure) {
7529
+ case "centralized-endpoints": return "./src/endpoints/**/*.ts";
7530
+ case "centralized-routes": return "./src/routes/**/*.ts";
7531
+ case "domain-based": return "./src/**/routes/*.ts";
7532
+ }
7539
7533
  }
7540
- /** The mail sender a project named `name` declares. */
7541
- function emailFor(name$1) {
7542
- const id = canonicalId(`${name$1}-mail`);
7534
+ /**
7535
+ * Plain ids — `Database`, not `Beetlefit`.
7536
+ *
7537
+ * The workspace `name` already scopes every physical name
7538
+ * (`production-beetlefit-database`), so an id that carried the project too
7539
+ * would say it twice: `production-beetlefit-beetlefit-cache`. Within a
7540
+ * workspace the constructs are declared once, at its root, so there is no
7541
+ * second app's `Database` to collide with.
7542
+ */
7543
+ function scaffolded(id) {
7543
7544
  return {
7544
7545
  id,
7545
7546
  service: serviceKey(id),
7546
7547
  urlKey: provideKey(id, "url")
7547
7548
  };
7548
7549
  }
7549
- /** The cache a project named `name` declares. */
7550
- function cacheFor(name$1) {
7551
- const id = canonicalId(`${name$1}-cache`);
7550
+ /** The database a project declares. */
7551
+ function databaseFor() {
7552
7552
  return {
7553
- id,
7554
- service: serviceKey(id),
7555
- urlKey: provideKey(id, "url")
7553
+ ...scaffolded("Database"),
7554
+ ownerUrlKey: provideKey("Database", "ownerUrl")
7556
7555
  };
7557
7556
  }
7557
+ /** The bucket a project declares. */
7558
+ const storageFor = () => scaffolded("Uploads");
7559
+ /** The mail sender a project declares. */
7560
+ const emailFor = () => scaffolded("Mail");
7561
+ /** The cache a project declares. */
7562
+ const cacheFor = () => scaffolded("Cache");
7558
7563
  /**
7559
7564
  * The files a project's declared database needs.
7560
7565
  *
@@ -7562,8 +7567,8 @@ function cacheFor(name$1) {
7562
7567
  * the same way — and so the schema type, the migration, and the id the test
7563
7568
  * setup reads all come from one place.
7564
7569
  */
7565
- function databaseFiles(name$1) {
7566
- const db = databaseFor(name$1);
7570
+ function databaseFiles() {
7571
+ const db = databaseFor();
7567
7572
  return [{
7568
7573
  path: "src/constructs/database.ts",
7569
7574
  content: `import { KyselyDatabase } from '@geekmidas/constructs/database/kysely';
@@ -7592,7 +7597,16 @@ export interface Database {
7592
7597
  */
7593
7598
  export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}');
7594
7599
  `
7595
- }, {
7600
+ }, usersMigration()];
7601
+ }
7602
+ /**
7603
+ * The table the scaffolded endpoints and factories expect.
7604
+ *
7605
+ * Its own function because a workspace's API needs it without the database
7606
+ * construct beside it — that one lives at the workspace root.
7607
+ */
7608
+ function usersMigration() {
7609
+ return {
7596
7610
  path: "src/db/migrations/001_create_users.ts",
7597
7611
  content: `import type { Kysely } from 'kysely';
7598
7612
 
@@ -7620,7 +7634,18 @@ export async function down(db: Kysely<unknown>): Promise<void> {
7620
7634
  await db.schema.dropTable('users').execute();
7621
7635
  }
7622
7636
  `
7623
- }];
7637
+ };
7638
+ }
7639
+ /**
7640
+ * What a workspace's `constructs` glob reaches: the root constructs, and the
7641
+ * handlers of every app laid out the way `init` was told.
7642
+ *
7643
+ * Every app in `appsDir` (`apps/*`) rather than the API's own directory.
7644
+ * Endpoints are split by the surface they were built from, so naming one app
7645
+ * here would only decide which apps' endpoints are silently never loaded.
7646
+ */
7647
+ function workspaceConstructsGlobs(structure, appsDir) {
7648
+ return [WORKSPACE_CONSTRUCTS_GLOB, `./${appsDir}/*/${routesGlob(structure).replace(/^\.\//, "")}`];
7624
7649
  }
7625
7650
 
7626
7651
  //#endregion
@@ -7700,6 +7725,7 @@ what the OpenAPI spec says, which env vars a process needs.
7700
7725
  \`\`\`typescript
7701
7726
  export default defineWorkspace({
7702
7727
  name: '${name$1}',
7728
+ stages: { local: 'dev', deployed: ['staging', 'prod'], protected: ['prod'] },
7703
7729
  constructs: '${constructsGlob}',
7704
7730
  secrets: { enabled: true },
7705
7731
  });
@@ -7954,6 +7980,30 @@ ${database ? "gkm test # starts services, injects secrets, then runs vites
7954
7980
  Run tests through \`gkm test\`, not \`vitest\` directly${database ? " — it is what starts the database and injects the secrets the suite needs" : ""}.`;
7955
7981
  }
7956
7982
 
7983
+ //#endregion
7984
+ //#region src/init/dependencies.ts
7985
+ /**
7986
+ * Third-party ranges a scaffold installs beside the `@geekmidas` packages.
7987
+ *
7988
+ * Held to the peer ranges those packages declare, in one place, because a
7989
+ * template that pinned its own copy drifted below them: `kysely ~0.28` against
7990
+ * a `~0.29.6` peer is two copies of Kysely and a `Kysely<DB>` from one that is
7991
+ * not assignable to the other's.
7992
+ *
7993
+ * Hand-maintained, unlike `versions.ts`, which `sync-versions` rewrites whole.
7994
+ */
7995
+ const DEPENDENCY_VERSIONS = {
7996
+ "@tanstack/react-query": "~5.80.0",
7997
+ "better-auth": "~1.7.5",
7998
+ hono: "~4.13.8",
7999
+ kysely: "~0.29.6",
8000
+ "kysely-ctl": "~0.21.0",
8001
+ pg: "~8.23.0",
8002
+ pino: "~10.3.1",
8003
+ sst: "~4.17.1",
8004
+ zod: "~4.6.5"
8005
+ };
8006
+
7957
8007
  //#endregion
7958
8008
  //#region src/init/versions.ts
7959
8009
  const require$1 = createRequire(import.meta.url);
@@ -7976,26 +8026,26 @@ const CLI_VERSION = `~${pkg.version}`;
7976
8026
  * Run: pnpm --filter @geekmidas/cli sync-versions
7977
8027
  */
7978
8028
  const GEEKMIDAS_VERSIONS = {
7979
- "@geekmidas/audit": "~10.0.0-alpha.5",
7980
- "@geekmidas/auth": "~10.0.0-alpha.5",
7981
- "@geekmidas/cache": "~10.0.0-alpha.5",
7982
- "@geekmidas/client": "~10.0.0-alpha.5",
7983
- "@geekmidas/cloud": "~10.0.0-alpha.5",
7984
- "@geekmidas/constructs": "~10.0.0-alpha.5",
7985
- "@geekmidas/db": "~10.0.0-alpha.5",
7986
- "@geekmidas/emailkit": "~10.0.0-alpha.5",
7987
- "@geekmidas/envkit": "~10.0.0-alpha.5",
7988
- "@geekmidas/errors": "~10.0.0-alpha.5",
7989
- "@geekmidas/events": "~10.0.0-alpha.5",
7990
- "@geekmidas/logger": "~10.0.0-alpha.5",
7991
- "@geekmidas/manifest": "~10.0.0-alpha.5",
7992
- "@geekmidas/rate-limit": "~10.0.0-alpha.5",
7993
- "@geekmidas/schema": "~10.0.0-alpha.5",
7994
- "@geekmidas/services": "~10.0.0-alpha.5",
7995
- "@geekmidas/storage": "~10.0.0-alpha.5",
7996
- "@geekmidas/studio": "~10.0.0-alpha.5",
7997
- "@geekmidas/telescope": "~10.0.0-alpha.5",
7998
- "@geekmidas/testkit": "~10.0.0-alpha.5",
8029
+ "@geekmidas/audit": "~10.0.0-alpha.9",
8030
+ "@geekmidas/auth": "~10.0.0-alpha.9",
8031
+ "@geekmidas/cache": "~10.0.0-alpha.9",
8032
+ "@geekmidas/client": "~10.0.0-alpha.9",
8033
+ "@geekmidas/cloud": "~10.0.0-alpha.9",
8034
+ "@geekmidas/constructs": "~10.0.0-alpha.9",
8035
+ "@geekmidas/db": "~10.0.0-alpha.9",
8036
+ "@geekmidas/emailkit": "~10.0.0-alpha.9",
8037
+ "@geekmidas/envkit": "~10.0.0-alpha.9",
8038
+ "@geekmidas/errors": "~10.0.0-alpha.9",
8039
+ "@geekmidas/events": "~10.0.0-alpha.9",
8040
+ "@geekmidas/logger": "~10.0.0-alpha.9",
8041
+ "@geekmidas/manifest": "~10.0.0-alpha.9",
8042
+ "@geekmidas/rate-limit": "~10.0.0-alpha.9",
8043
+ "@geekmidas/schema": "~10.0.0-alpha.9",
8044
+ "@geekmidas/services": "~10.0.0-alpha.9",
8045
+ "@geekmidas/storage": "~10.0.0-alpha.9",
8046
+ "@geekmidas/studio": "~10.0.0-alpha.9",
8047
+ "@geekmidas/telescope": "~10.0.0-alpha.9",
8048
+ "@geekmidas/testkit": "~10.0.0-alpha.9",
7999
8049
  "@geekmidas/cli": CLI_VERSION
8000
8050
  };
8001
8051
 
@@ -8015,27 +8065,28 @@ function generateAuthAppFiles(options) {
8015
8065
  private: true,
8016
8066
  type: "module",
8017
8067
  scripts: {
8018
- dev: "gkm dev --entry ./src/index.ts",
8019
- build: "tsc",
8020
- start: "node dist/index.js",
8021
- typecheck: "tsc --noEmit",
8068
+ dev: "gkm dev",
8069
+ build: "gkm build",
8022
8070
  "db:migrate": "gkm exec -- npx @better-auth/cli migrate",
8023
8071
  "db:generate": "gkm exec -- npx @better-auth/cli generate"
8024
8072
  },
8025
8073
  dependencies: {
8026
8074
  [modelsPackage]: "workspace:*",
8075
+ "@geekmidas/auth": GEEKMIDAS_VERSIONS["@geekmidas/auth"],
8076
+ "@geekmidas/constructs": GEEKMIDAS_VERSIONS["@geekmidas/constructs"],
8077
+ "@geekmidas/db": GEEKMIDAS_VERSIONS["@geekmidas/db"],
8027
8078
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
8028
8079
  "@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
8029
- "@hono/node-server": "~1.13.0",
8030
- "better-auth": "~1.2.0",
8031
- hono: "~4.8.0",
8032
- kysely: "~0.27.0",
8033
- pg: "~8.13.0"
8080
+ "@hono/node-server": "~1.14.1",
8081
+ "better-auth": DEPENDENCY_VERSIONS["better-auth"],
8082
+ hono: DEPENDENCY_VERSIONS.hono,
8083
+ kysely: DEPENDENCY_VERSIONS.kysely,
8084
+ pg: DEPENDENCY_VERSIONS.pg
8034
8085
  },
8035
8086
  devDependencies: {
8036
8087
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
8037
8088
  "@types/node": "~22.0.0",
8038
- "@types/pg": "~8.11.0",
8089
+ "@types/pg": "~8.23.1",
8039
8090
  tsx: "~4.20.0",
8040
8091
  typescript: "~5.8.2"
8041
8092
  }
@@ -8075,6 +8126,30 @@ dist/
8075
8126
  ];
8076
8127
  }
8077
8128
 
8129
+ //#endregion
8130
+ //#region src/init/generators/stages.ts
8131
+ /** The `stages` block a generated gkm.config.ts declares, indented two spaces. */
8132
+ function stagesBlock(stages) {
8133
+ const list = (names) => `[${names.map((name$1) => `'${name$1}'`).join(", ")}]`;
8134
+ return `
8135
+ // Named by this project and read by every command: \`gkm dev\`, \`exec\` and
8136
+ // \`test\` run as \`local\`, and \`gkm deploy --stage\` accepts only \`deployed\`.
8137
+ stages: {
8138
+ local: '${stages.local}',
8139
+ deployed: ${list(stages.deployed)},${stages.protected?.length ? `
8140
+ // Retained and protected on removal.
8141
+ protected: ${list(stages.protected)},` : ""}
8142
+ },`;
8143
+ }
8144
+ /**
8145
+ * The `STAGE` the scaffolded env config parses: one of the declared stages,
8146
+ * defaulting to the local one.
8147
+ */
8148
+ function stageEnv(stages) {
8149
+ const names = [stages.local, ...stages.deployed].map((name$1) => `'${name$1}'`).join(", ");
8150
+ return `get('STAGE').enum([${names}]).default('${stages.local}')`;
8151
+ }
8152
+
8078
8153
  //#endregion
8079
8154
  //#region src/init/generators/config.ts
8080
8155
  /**
@@ -8099,13 +8174,7 @@ function generateConfigFiles(options, template) {
8099
8174
  const isServerless = template.name === "serverless";
8100
8175
  const hasWorker = template.name === "worker";
8101
8176
  const isFullstack = options.template === "fullstack";
8102
- const getRoutesGlob = () => {
8103
- switch (routesStructure) {
8104
- case "centralized-endpoints": return "./src/endpoints/**/*.ts";
8105
- case "centralized-routes": return "./src/routes/**/*.ts";
8106
- case "domain-based": return "./src/**/routes/*.ts";
8107
- }
8108
- };
8177
+ const getRoutesGlob = () => routesGlob(routesStructure);
8109
8178
  if (isFullstack) return generateSingleAppConfigFiles(options, template, {
8110
8179
  telescope,
8111
8180
  studio,
@@ -8116,7 +8185,7 @@ function generateConfigFiles(options, template) {
8116
8185
  });
8117
8186
  let gkmConfig = `import { defineConfig } from '@geekmidas/cli/config';
8118
8187
 
8119
- export default defineConfig({${options.monorepo ? "" : `
8188
+ export default defineConfig({${stagesBlock(options.stages)}${options.monorepo ? "" : `
8120
8189
  // One glob, every kind. What reconcile reads to derive the containers this
8121
8190
  // app needs: a declared database is why a Postgres exists at all, so nothing
8122
8191
  // lists \`postgres\` anywhere.
@@ -8158,21 +8227,7 @@ export default defineConfig({${options.monorepo ? "" : `
8158
8227
  gkmConfig += `
8159
8228
  });
8160
8229
  `;
8161
- const tsConfig = options.monorepo ? {
8162
- extends: "../../tsconfig.json",
8163
- compilerOptions: {
8164
- noEmit: true,
8165
- allowImportingTsExtensions: true,
8166
- baseUrl: ".",
8167
- paths: {
8168
- "~/*": ["./src/*"],
8169
- [`@${options.name}/constructs/*`]: ["../../constructs/*"],
8170
- [`@${options.name}/*`]: ["../../packages/*/src"]
8171
- }
8172
- },
8173
- include: ["src/**/*.ts"],
8174
- exclude: ["node_modules", "dist"]
8175
- } : {
8230
+ const tsConfig = options.monorepo ? workspaceApiTsConfig(options.name) : {
8176
8231
  compilerOptions: {
8177
8232
  target: "ES2022",
8178
8233
  module: "NodeNext",
@@ -8212,7 +8267,7 @@ export default defineConfig({${options.monorepo ? "" : `
8212
8267
  clientKind: "git",
8213
8268
  useIgnoreFile: true
8214
8269
  },
8215
- organizeImports: { enabled: true },
8270
+ assist: { actions: { source: { organizeImports: "on" } } },
8216
8271
  formatter: {
8217
8272
  enabled: true,
8218
8273
  indentStyle: "space",
@@ -8236,11 +8291,13 @@ export default defineConfig({${options.monorepo ? "" : `
8236
8291
  style: { noNonNullAssertion: "off" }
8237
8292
  }
8238
8293
  },
8239
- files: { ignore: [
8240
- "node_modules",
8241
- "dist",
8242
- ".gkm",
8243
- "coverage"
8294
+ css: { parser: { tailwindDirectives: true } },
8295
+ files: { includes: [
8296
+ "**",
8297
+ "!**/node_modules",
8298
+ "!**/dist",
8299
+ "!**/.gkm",
8300
+ "!**/coverage"
8244
8301
  ] }
8245
8302
  };
8246
8303
  const turboConfig = {
@@ -8294,8 +8351,16 @@ export default defineConfig({${options.monorepo ? "" : `
8294
8351
  });
8295
8352
  return files$1;
8296
8353
  }
8297
- function generateSingleAppConfigFiles(options, _template, _helpers) {
8298
- const tsConfig = {
8354
+ /**
8355
+ * The tsconfig of an API inside a workspace.
8356
+ *
8357
+ * One definition for both ways of getting one — the fullstack template and
8358
+ * `--monorepo` — because the two copies had drifted: only one of them mapped
8359
+ * the root constructs, so in the other `@<name>/constructs/api.ts` fell through
8360
+ * to the `packages/` mapping and nothing could load the surface.
8361
+ */
8362
+ function workspaceApiTsConfig(name$1) {
8363
+ return {
8299
8364
  extends: "../../tsconfig.json",
8300
8365
  compilerOptions: {
8301
8366
  noEmit: true,
@@ -8303,12 +8368,16 @@ function generateSingleAppConfigFiles(options, _template, _helpers) {
8303
8368
  baseUrl: ".",
8304
8369
  paths: {
8305
8370
  "~/*": ["./src/*"],
8306
- [`@${options.name}/*`]: ["../../packages/*/src"]
8371
+ [`@${name$1}/constructs/*`]: ["../../constructs/*"],
8372
+ [`@${name$1}/*`]: ["../../packages/*/src"]
8307
8373
  }
8308
8374
  },
8309
8375
  include: ["src/**/*.ts"],
8310
8376
  exclude: ["node_modules", "dist"]
8311
8377
  };
8378
+ }
8379
+ function generateSingleAppConfigFiles(options, _template, _helpers) {
8380
+ const tsConfig = workspaceApiTsConfig(options.name);
8312
8381
  const files$1 = [{
8313
8382
  path: "tsconfig.json",
8314
8383
  content: `${JSON.stringify(tsConfig, null, 2)}\n`
@@ -8717,12 +8786,12 @@ function generateExpoAppFiles(options) {
8717
8786
  dependencies: {
8718
8787
  [apiPackage]: "workspace:*",
8719
8788
  [modelsPackage]: "workspace:*",
8720
- "@better-auth/expo": "~1.2.0",
8789
+ "@better-auth/expo": DEPENDENCY_VERSIONS["better-auth"],
8721
8790
  "@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
8722
8791
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
8723
8792
  "@react-navigation/native": "^7.1.0",
8724
- "@tanstack/react-query": "~5.80.0",
8725
- "better-auth": "~1.2.0",
8793
+ "@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
8794
+ "better-auth": DEPENDENCY_VERSIONS["better-auth"],
8726
8795
  expo: "~55.0.0",
8727
8796
  "expo-constants": "~55.0.0",
8728
8797
  "expo-dev-client": "~55.0.0",
@@ -8858,7 +8927,7 @@ module.exports = withNativeWind(config, { input: './global.css' });
8858
8927
  "@/*": ["./*"],
8859
8928
  [modelsPackage]: ["../../packages/models/src"],
8860
8929
  [`${modelsPackage}/*`]: ["../../packages/models/src/*"],
8861
- [`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"]
8930
+ [`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi/api.ts"]
8862
8931
  }
8863
8932
  },
8864
8933
  include: [
@@ -9416,14 +9485,142 @@ export type ListUsersResponse = z.infer<typeof ListUsersResponseSchema>;
9416
9485
  ];
9417
9486
  }
9418
9487
 
9488
+ //#endregion
9489
+ //#region src/init/generators/deploy.ts
9490
+ /** One `deploy:<stage>` script per deployed stage. */
9491
+ const perStage = (options, command) => Object.fromEntries(options.stages.deployed.map((stage) => [`deploy:${stage}`, command(stage)]));
9492
+ function deployPackage(options) {
9493
+ switch (options.deployTarget) {
9494
+ case "dokploy": return {
9495
+ scripts: perStage(options, (stage) => `gkm deploy --provider dokploy --stage ${stage}`),
9496
+ dependencies: {},
9497
+ devDependencies: {}
9498
+ };
9499
+ case "sst": {
9500
+ const v = GEEKMIDAS_VERSIONS;
9501
+ return {
9502
+ scripts: perStage(options, (stage) => `gkm build --provider aws && sst deploy --stage ${stage}`),
9503
+ dependencies: {
9504
+ "@geekmidas/cloud": v["@geekmidas/cloud"],
9505
+ "@geekmidas/db": v["@geekmidas/db"],
9506
+ "@geekmidas/envkit": v["@geekmidas/envkit"],
9507
+ "@geekmidas/events": v["@geekmidas/events"],
9508
+ "@geekmidas/manifest": v["@geekmidas/manifest"],
9509
+ "@geekmidas/storage": v["@geekmidas/storage"],
9510
+ pg: DEPENDENCY_VERSIONS.pg
9511
+ },
9512
+ devDependencies: { sst: DEPENDENCY_VERSIONS.sst }
9513
+ };
9514
+ }
9515
+ default: return {
9516
+ scripts: {},
9517
+ dependencies: {},
9518
+ devDependencies: {}
9519
+ };
9520
+ }
9521
+ }
9522
+ function generateDeployFiles(options) {
9523
+ if (options.deployTarget !== "sst") return [];
9524
+ if (!options.region) throw new Error("An SST deploy needs a region.");
9525
+ const db = databaseFor();
9526
+ const mail = emailFor();
9527
+ const hasDatabase = options.template === "fullstack" || options.services.db;
9528
+ const inputs = [...hasDatabase ? [` // RDS needs a network. Created here rather than assumed, because a
9529
+ // VPC with NAT is a monthly cost in an account that may already
9530
+ // have one — replace it with \`sst.aws.Vpc.get(…)\` if so.
9531
+ ${db.id}: { vpc },`] : [], ...options.services.mail ? [` // Every provider rejects an unverified sender, so there is no
9532
+ // default; the synth stops with \`EmailNeedsSender\` until it is set.
9533
+ ${mail.id}: { from: process.env.MAIL_FROM as string },`] : []];
9534
+ return [{
9535
+ path: "sst.config.ts",
9536
+ content: `/// <reference path="./.sst/platform/config.d.ts" />
9537
+
9538
+ /**
9539
+ * ${options.name} on AWS.
9540
+ *
9541
+ * No bucket, cluster, queue or IAM is listed here: they come from the
9542
+ * constructs the application declares, through \`fromManifest\`. What is here
9543
+ * is what varies by stage, and the inputs a declaration cannot carry.
9544
+ *
9545
+ * Deploy with \`pnpm run deploy:<stage>\`, which builds the manifest first.
9546
+ */
9547
+ const region = '${options.region}';
9548
+
9549
+ /** From \`stages.protected\` in gkm.config.ts. */
9550
+ const PROTECTED: string[] = ${JSON.stringify(options.stages.protected ?? []).replace(/"/g, "'")};
9551
+
9552
+ export default $config({
9553
+ app(input) {
9554
+ return {
9555
+ name: '${options.name}',
9556
+ // Any other stage is disposable; a protected one's database should not
9557
+ // vanish because somebody removed a stack.
9558
+ removal: PROTECTED.includes(input?.stage) ? 'retain' : 'remove',
9559
+ protect: PROTECTED.includes(input?.stage),
9560
+ home: 'aws',
9561
+ providers: { aws: { region } },
9562
+ };
9563
+ },
9564
+
9565
+ async run() {
9566
+ const { App, fromManifest, Stack } = await import('@geekmidas/cloud/sst');
9567
+ // Written by \`gkm build --provider aws\`. Imported rather than discovered,
9568
+ // so SST's toolchain never evaluates the application's own modules.
9569
+ const { backends, constructs } = await import('./.gkm/manifest/aws.js');
9570
+
9571
+ ${hasDatabase ? " const vpc = new sst.aws.Vpc('Vpc', { nat: 'ec2' });\n\n" : ""} const app = new App({
9572
+ name: $app.name,
9573
+ stage: $app.stage,
9574
+ domain: '',
9575
+ region,
9576
+ hostedZoneId: '',
9577
+ });
9578
+
9579
+ return fromManifest(
9580
+ new Stack(app, '${canonicalId(options.name)}'),
9581
+ constructs,
9582
+ {
9583
+ ${inputs.join("\n")}
9584
+ },
9585
+ backends,
9586
+ );
9587
+ },
9588
+ });
9589
+ `
9590
+ }];
9591
+ }
9592
+
9419
9593
  //#endregion
9420
9594
  //#region src/init/generators/monorepo.ts
9421
9595
  /**
9596
+ * What the root `constructs/` folder imports, and the peers each construct
9597
+ * needs, for the services this workspace was scaffolded with.
9598
+ */
9599
+ function rootConstructDependencies(options) {
9600
+ const v = GEEKMIDAS_VERSIONS;
9601
+ return {
9602
+ "@geekmidas/constructs": v["@geekmidas/constructs"],
9603
+ "@geekmidas/logger": v["@geekmidas/logger"],
9604
+ ...options.loggerType === "pino" ? { pino: DEPENDENCY_VERSIONS.pino } : {},
9605
+ "@geekmidas/telescope": v["@geekmidas/telescope"],
9606
+ "@geekmidas/auth": v["@geekmidas/auth"],
9607
+ "better-auth": DEPENDENCY_VERSIONS["better-auth"],
9608
+ "@geekmidas/db": v["@geekmidas/db"],
9609
+ kysely: DEPENDENCY_VERSIONS.kysely,
9610
+ pg: DEPENDENCY_VERSIONS.pg,
9611
+ ...options.services.storage ? { "@geekmidas/storage": v["@geekmidas/storage"] } : {},
9612
+ ...options.services.mail ? { "@geekmidas/emailkit": v["@geekmidas/emailkit"] } : {},
9613
+ ...options.services.cache ? { "@geekmidas/cache": v["@geekmidas/cache"] } : {}
9614
+ };
9615
+ }
9616
+ /**
9422
9617
  * Generate monorepo root files (pnpm-workspace.yaml, root package.json, etc.)
9423
9618
  */
9424
9619
  function generateMonorepoFiles(options, _template) {
9425
9620
  if (!options.monorepo) return [];
9426
9621
  const isFullstack = options.template === "fullstack";
9622
+ const deploy = deployPackage(options);
9623
+ const isSst = options.deployTarget === "sst";
9427
9624
  const rootPackageJson = {
9428
9625
  name: options.name,
9429
9626
  version: "0.0.1",
@@ -9440,9 +9637,13 @@ function generateMonorepoFiles(options, _template) {
9440
9637
  fmt: "biome format . --write",
9441
9638
  "fmt:check": "biome format .",
9442
9639
  ...isFullstack ? { storybook: "pnpm --filter ./packages/ui storybook" } : {},
9443
- ...options.deployTarget === "dokploy" ? { deploy: "gkm deploy --provider dokploy --stage production" } : {}
9640
+ ...deploy.scripts
9641
+ },
9642
+ dependencies: {
9643
+ zod: DEPENDENCY_VERSIONS.zod,
9644
+ ...isFullstack ? rootConstructDependencies(options) : {},
9645
+ ...deploy.dependencies
9444
9646
  },
9445
- dependencies: { zod: "~4.1.0" },
9446
9647
  devDependencies: {
9447
9648
  "@biomejs/biome": "~2.3.0",
9448
9649
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
@@ -9450,7 +9651,8 @@ function generateMonorepoFiles(options, _template) {
9450
9651
  tsx: "~4.20.0",
9451
9652
  turbo: "~2.3.0",
9452
9653
  typescript: "~5.8.2",
9453
- vitest: "~4.0.0"
9654
+ vitest: "~4.0.0",
9655
+ ...deploy.devDependencies
9454
9656
  }
9455
9657
  };
9456
9658
  const apiPathParts = options.apiPath.split("/");
@@ -9466,7 +9668,7 @@ function generateMonorepoFiles(options, _template) {
9466
9668
  clientKind: "git",
9467
9669
  useIgnoreFile: true
9468
9670
  },
9469
- organizeImports: { enabled: true },
9671
+ assist: { actions: { source: { organizeImports: "on" } } },
9470
9672
  formatter: {
9471
9673
  enabled: true,
9472
9674
  indentStyle: "space",
@@ -9490,11 +9692,14 @@ function generateMonorepoFiles(options, _template) {
9490
9692
  style: { noNonNullAssertion: "off" }
9491
9693
  }
9492
9694
  },
9493
- files: { ignore: [
9494
- "node_modules",
9495
- "dist",
9496
- ".gkm",
9497
- "coverage"
9695
+ css: { parser: { tailwindDirectives: true } },
9696
+ files: { includes: [
9697
+ "**",
9698
+ "!**/node_modules",
9699
+ "!**/dist",
9700
+ "!**/.gkm",
9701
+ "!**/coverage",
9702
+ ...isSst ? ["!**/.sst", "!sst-env.d.ts"] : []
9498
9703
  ] }
9499
9704
  };
9500
9705
  const turboConfig = {
@@ -9560,12 +9765,14 @@ coverage/
9560
9765
 
9561
9766
  # Turbo
9562
9767
  .turbo/
9563
- `;
9768
+ ${isSst ? "\n# SST\n.sst/\n" : ""}`;
9564
9769
  const tsConfig = {
9565
9770
  compilerOptions: {
9566
9771
  ...isFullstack ? {
9567
9772
  baseUrl: ".",
9568
- paths: { [`@${options.name}/constructs/*`]: ["./constructs/*"] }
9773
+ paths: { [`@${options.name}/constructs/*`]: ["./constructs/*"] },
9774
+ allowImportingTsExtensions: true,
9775
+ noEmit: true
9569
9776
  } : {},
9570
9777
  target: "ES2022",
9571
9778
  module: "NodeNext",
@@ -9577,16 +9784,21 @@ coverage/
9577
9784
  forceConsistentCasingInFileNames: true,
9578
9785
  resolveJsonModule: true
9579
9786
  },
9580
- exclude: ["node_modules", "dist"]
9787
+ exclude: [
9788
+ "node_modules",
9789
+ "dist",
9790
+ ...isSst ? ["sst.config.ts", ".sst"] : []
9791
+ ]
9581
9792
  };
9582
9793
  const vitestConfig = `import { defineConfig } from 'vitest/config';
9583
9794
 
9584
9795
  export default defineConfig({
9585
9796
  test: {
9586
- globals: true,
9587
- environment: 'node',
9588
- include: ['apps/**/*.{test,spec}.ts', 'packages/**/*.{test,spec}.ts'],
9589
- exclude: ['**/node_modules/**', '**/dist/**'],
9797
+ // Each app and package is its own project, so its own config — an app's
9798
+ // \`globalSetup\`, its path aliases — applies to its own tests.
9799
+ projects: ['apps/*', 'packages/*'],
9800
+ // The shared packages ship without tests of their own.
9801
+ passWithNoTests: true,
9590
9802
  coverage: {
9591
9803
  provider: 'v8',
9592
9804
  reporter: ['text', 'json', 'html'],
@@ -9667,7 +9879,8 @@ export default defineConfig({
9667
9879
  {
9668
9880
  path: ".vscode/extensions.json",
9669
9881
  content: `${JSON.stringify(vscodeExtensions, null, " ")}\n`
9670
- }
9882
+ },
9883
+ ...generateDeployFiles(options)
9671
9884
  ];
9672
9885
  if (isFullstack) files$1.push({
9673
9886
  path: "gkm.config.ts",
@@ -9694,12 +9907,17 @@ export default defineWorkspace({
9694
9907
  // The scope every physical name is built from: \`Database\` becomes
9695
9908
  // \`production-${options.name}-database\` on Dokploy and on AWS alike.
9696
9909
  name: '${options.name}',
9910
+ ${stagesBlock(options.stages)}
9697
9911
 
9698
- // One glob, every kind. A declared database is why a Postgres exists, a
9912
+ // Every kind, in every app. A declared database is why a Postgres exists, a
9699
9913
  // declared bucket is why MinIO does, a declared topic is why a broker does —
9700
9914
  // none of it listed here. It is also where the apps come from: a
9701
- // \`StaticSite\` is an app, and so is every \`RestApi\`.
9702
- constructs: '${WORKSPACE_CONSTRUCTS_GLOB}',
9915
+ // \`StaticSite\` is an app, and so is every \`RestApi\`. And it finds each
9916
+ // app's endpoints, crons and subscribers: an endpoint belongs to the surface
9917
+ // it was built from, not to the directory it sits in.
9918
+ constructs: [
9919
+ ${workspaceConstructsGlobs(options.routesStructure, dirname(options.apiPath)).map((glob) => ` '${glob}',`).join("\n")}
9920
+ ],
9703
9921
 
9704
9922
  secrets: {
9705
9923
  enabled: true,
@@ -9728,7 +9946,7 @@ export default defineWorkspace({
9728
9946
  function generateRootConstructs(options) {
9729
9947
  if (!options.monorepo || options.template !== "fullstack") return [];
9730
9948
  const { name: name$1, services, frontendFramework } = options;
9731
- const db = databaseFor(name$1);
9949
+ const db = databaseFor();
9732
9950
  const files$1 = [];
9733
9951
  files$1.push({
9734
9952
  path: "constructs/logger.ts",
@@ -9794,6 +10012,7 @@ import { authDb } from './database.ts';
9794
10012
  * origin down.
9795
10013
  */
9796
10014
  export const auth = new BetterAuth('Auth', {
10015
+ path: 'apps/auth',
9797
10016
  database: authDb,
9798
10017
  basePath: '/api/auth',
9799
10018
  });
@@ -9808,25 +10027,22 @@ import { logger } from './logger.ts';
9808
10027
  /**
9809
10028
  * The application's HTTP surface — one RestApi, one container.
9810
10029
  *
9811
- * No \`app\`: \`Api\` means \`apps/api\`, which the id already said. The
9812
- * \`code\` glob is here only because this scaffold puts its endpoints under
9813
- * \`src/\`, where the conventional layout has them at the app root.
10030
+ * \`path\` is where its app lives. Its handlers are found by the workspace's
10031
+ * \`constructs\` glob, and belong to it because they were built from it.
9814
10032
  */
9815
10033
  export const api = new RestApi('Api', {
10034
+ path: '${options.apiPath}',
10035
+
9816
10036
  // Typed out rather than omitted: an API that ships open because a field was
9817
10037
  // left off is the one default worth refusing to have.
9818
10038
  defaultAuthorizer: 'none',
9819
10039
 
9820
10040
  // The actual logger, not a path to one.
9821
10041
  logger,
9822
-
9823
- app: {
9824
- code: './src/{endpoints,functions,crons,queues,topics,subscribers}/**/*.ts',
9825
- },
9826
10042
  }).auth(auth);
9827
10043
  `
9828
10044
  });
9829
- const variant = frontendFramework === "tanstack-start" ? "{ variant: 'tanstack' }" : frontendFramework === "expo" ? void 0 : "{ variant: 'next' }";
10045
+ const variant = frontendFramework === "tanstack-start" ? "{ path: 'apps/web', variant: 'tanstack' }" : frontendFramework === "expo" ? void 0 : "{ path: 'apps/web', variant: 'next' }";
9830
10046
  if (variant !== void 0) files$1.push({
9831
10047
  path: "constructs/site.ts",
9832
10048
  content: `import { StaticSite } from '@geekmidas/constructs/site';
@@ -9836,7 +10052,7 @@ import { auth } from './auth.ts';
9836
10052
  /**
9837
10053
  * The frontend — a construct like any other, which is what makes it an app.
9838
10054
  *
9839
- * No \`path\`: \`Web\` means \`apps/web\`. \`.dependsOn()\` is the single fact
10055
+ * \`path\` is where it lives. \`.dependsOn()\` is the single fact
9840
10056
  * behind four things that are hand-maintained otherwise: this site's
9841
10057
  * build-time API URL, the API's CORS origins, the auth server's trusted
9842
10058
  * origins, and which generated client lands here.
@@ -9845,7 +10061,7 @@ export const web = new StaticSite('Web', ${variant}).dependsOn([api, auth]);
9845
10061
  `
9846
10062
  });
9847
10063
  if (services.storage) {
9848
- const bucket = storageFor(name$1);
10064
+ const bucket = storageFor();
9849
10065
  files$1.push({
9850
10066
  path: "constructs/storage.ts",
9851
10067
  content: `import { ObjectStorage } from '@geekmidas/constructs/object-storage';
@@ -9856,7 +10072,7 @@ export const uploads = new ObjectStorage('${bucket.id}');
9856
10072
  });
9857
10073
  }
9858
10074
  if (services.mail) {
9859
- const mail = emailFor(name$1);
10075
+ const mail = emailFor();
9860
10076
  files$1.push({
9861
10077
  path: "constructs/email.ts",
9862
10078
  content: `import { Email } from '@geekmidas/constructs/email';
@@ -9867,7 +10083,7 @@ export const email = new Email('${mail.id}', { templates: {} });
9867
10083
  });
9868
10084
  }
9869
10085
  if (services.cache) {
9870
- const kv = cacheFor(name$1);
10086
+ const kv = cacheFor();
9871
10087
  files$1.push({
9872
10088
  path: "constructs/cache.ts",
9873
10089
  content: `import { Cache } from '@geekmidas/constructs/cache';
@@ -9900,9 +10116,9 @@ const apiTemplate = {
9900
10116
  "@geekmidas/errors": GEEKMIDAS_VERSIONS["@geekmidas/errors"],
9901
10117
  "@geekmidas/auth": GEEKMIDAS_VERSIONS["@geekmidas/auth"],
9902
10118
  "@hono/node-server": "~1.14.1",
9903
- hono: "~4.8.2",
9904
- pino: "~9.6.0",
9905
- zod: "~4.1.0"
10119
+ hono: DEPENDENCY_VERSIONS.hono,
10120
+ pino: DEPENDENCY_VERSIONS.pino,
10121
+ zod: DEPENDENCY_VERSIONS.zod
9906
10122
  },
9907
10123
  devDependencies: {
9908
10124
  "@biomejs/biome": "~2.3.0",
@@ -9926,10 +10142,10 @@ const apiTemplate = {
9926
10142
  },
9927
10143
  files: (options) => {
9928
10144
  const { loggerType, routesStructure, monorepo, name: name$1, services } = options;
9929
- const bucket = storageFor(name$1);
9930
- const kv = cacheFor(name$1);
9931
- const db = databaseFor(name$1);
9932
- const mail = emailFor(name$1);
10145
+ const bucket = storageFor();
10146
+ const kv = cacheFor();
10147
+ const db = databaseFor();
10148
+ const mail = emailFor();
9933
10149
  const src = (path) => monorepo ? `~/${path}` : `./${path}`;
9934
10150
  const declares = !monorepo;
9935
10151
  const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
@@ -9963,6 +10179,9 @@ export const logger = createLogger();
9963
10179
  import { logger } from '../config/logger.ts';
9964
10180
 
9965
10181
  export const api = new RestApi('Api', {
10182
+ // The project is the app.
10183
+ path: '.',
10184
+
9966
10185
  // Typed out rather than omitted: an API that ships open because a field was
9967
10186
  // left off is the one default worth refusing to have.
9968
10187
  defaultAuthorizer: 'none',
@@ -9985,7 +10204,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
9985
10204
  export const config = envParser
9986
10205
  .create((get) => ({
9987
10206
  nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
9988
- stage: get('STAGE').enum(['development', 'staging', 'production']).default('development'),
10207
+ stage: ${stageEnv(options.stages)},
9989
10208
  }))
9990
10209
  .parse();
9991
10210
  `
@@ -10137,7 +10356,7 @@ export const authService = {
10137
10356
  headers: { cookie },
10138
10357
  });
10139
10358
  if (!res.ok) return null;
10140
- return res.json();
10359
+ return (await res.json()) as Session | null;
10141
10360
  },
10142
10361
  };
10143
10362
  },
@@ -10150,7 +10369,6 @@ export const authService = {
10150
10369
  import { api } from '${constructsImport("api")}';${options.database ? `
10151
10370
  import { database } from '${constructsImport("database")}';` : ""}
10152
10371
  import { authService, type Session } from './services/auth.ts';
10153
- import { logger } from './config/logger.ts';
10154
10372
 
10155
10373
  /**
10156
10374
  * The shared endpoint factory — no session required.
@@ -10208,7 +10426,8 @@ import { database } from '${constructsImport("database")}';` : ""}
10208
10426
  export const router = api.endpoints${options.database ? ".database(database)" : ""};
10209
10427
  `
10210
10428
  });
10211
- if (options.database && declares) files$1.push(...databaseFiles(name$1));
10429
+ if (options.database && declares) files$1.push(...databaseFiles());
10430
+ else if (options.database) files$1.push(usersMigration());
10212
10431
  if (services.storage && declares) files$1.push({
10213
10432
  path: "src/constructs/storage.ts",
10214
10433
  content: `import { ObjectStorage } from '@geekmidas/constructs/object-storage';
@@ -10358,9 +10577,9 @@ const minimalTemplate = {
10358
10577
  "@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
10359
10578
  "@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
10360
10579
  "@hono/node-server": "~1.14.1",
10361
- hono: "~4.8.2",
10362
- pino: "~9.6.0",
10363
- zod: "~4.1.0"
10580
+ hono: DEPENDENCY_VERSIONS.hono,
10581
+ pino: DEPENDENCY_VERSIONS.pino,
10582
+ zod: DEPENDENCY_VERSIONS.zod
10364
10583
  },
10365
10584
  devDependencies: {
10366
10585
  "@biomejs/biome": "~2.3.0",
@@ -10384,7 +10603,7 @@ const minimalTemplate = {
10384
10603
  },
10385
10604
  files: (options) => {
10386
10605
  const { loggerType, routesStructure, name: name$1 } = options;
10387
- const db = databaseFor(name$1);
10606
+ const db = databaseFor();
10388
10607
  const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
10389
10608
 
10390
10609
  export const logger = createLogger();
@@ -10404,6 +10623,9 @@ export const logger = createLogger();
10404
10623
  import { logger } from '../config/logger.ts';
10405
10624
 
10406
10625
  export const api = new RestApi('Api', {
10626
+ // The project is the app.
10627
+ path: '.',
10628
+
10407
10629
  // Typed out rather than omitted: an API that ships open because a field was
10408
10630
  // left off is the one default worth refusing to have.
10409
10631
  defaultAuthorizer: 'none',
@@ -10426,7 +10648,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
10426
10648
  export const config = envParser
10427
10649
  .create((get) => ({
10428
10650
  nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
10429
- stage: get('STAGE').enum(['development', 'staging', 'production']).default('development'),
10651
+ stage: ${stageEnv(options.stages)},
10430
10652
  }))
10431
10653
  .parse();
10432
10654
  `
@@ -10453,7 +10675,7 @@ export const healthEndpoint = api
10453
10675
  `
10454
10676
  }
10455
10677
  ];
10456
- if (options.database) files$1.push(...databaseFiles(name$1));
10678
+ if (options.database) files$1.push(...databaseFiles());
10457
10679
  if (options.telescope) files$1.push({
10458
10680
  path: "src/config/telescope.ts",
10459
10681
  content: `import { Telescope } from '@geekmidas/telescope';
@@ -10517,8 +10739,8 @@ const serverlessTemplate = {
10517
10739
  "@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
10518
10740
  "@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
10519
10741
  "@hono/node-server": "~1.14.1",
10520
- hono: "~4.8.2",
10521
- pino: "~9.6.0"
10742
+ hono: DEPENDENCY_VERSIONS.hono,
10743
+ pino: DEPENDENCY_VERSIONS.pino
10522
10744
  },
10523
10745
  devDependencies: {
10524
10746
  "@biomejs/biome": "~2.3.0",
@@ -10561,6 +10783,9 @@ export const logger = createLogger();
10561
10783
  import { logger } from '../config/logger.ts';
10562
10784
 
10563
10785
  export const api = new RestApi('Api', {
10786
+ // The project is the app.
10787
+ path: '.',
10788
+
10564
10789
  // Typed out rather than omitted: an API that ships open because a field was
10565
10790
  // left off is the one default worth refusing to have.
10566
10791
  defaultAuthorizer: 'none',
@@ -10583,7 +10808,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
10583
10808
  export const config = envParser
10584
10809
  .create((get) => ({
10585
10810
  nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
10586
- stage: get('STAGE').enum(['dev', 'staging', 'prod']).default('dev'),
10811
+ stage: ${stageEnv(options.stages)},
10587
10812
  }))
10588
10813
  .parse();
10589
10814
  `
@@ -10633,7 +10858,7 @@ export const worker = new Worker('Jobs', { logger });
10633
10858
  `
10634
10859
  }
10635
10860
  ];
10636
- if (options.database) files$1.push(...databaseFiles(name$1));
10861
+ if (options.database) files$1.push(...databaseFiles());
10637
10862
  if (options.telescope) files$1.push({
10638
10863
  path: "src/config/telescope.ts",
10639
10864
  content: `import { Telescope } from '@geekmidas/telescope';
@@ -10642,7 +10867,7 @@ import { InMemoryStorage } from '@geekmidas/telescope/storage/memory';
10642
10867
  // Note: For production Lambda, consider using a persistent storage
10643
10868
  export const telescope = new Telescope({
10644
10869
  storage: new InMemoryStorage({ maxEntries: 50 }),
10645
- enabled: process.env.STAGE === 'dev',
10870
+ enabled: process.env.STAGE === '${options.stages.local}',
10646
10871
  });
10647
10872
  `
10648
10873
  });
@@ -10678,7 +10903,7 @@ const workerTemplate = {
10678
10903
  "@geekmidas/events": GEEKMIDAS_VERSIONS["@geekmidas/events"],
10679
10904
  "@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
10680
10905
  "@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
10681
- pino: "~10.3.1"
10906
+ pino: DEPENDENCY_VERSIONS.pino
10682
10907
  },
10683
10908
  devDependencies: {
10684
10909
  "@biomejs/biome": "~2.3.0",
@@ -10726,7 +10951,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
10726
10951
  export const config = envParser
10727
10952
  .create((get) => ({
10728
10953
  nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
10729
- stage: get('STAGE').enum(['development', 'staging', 'production']).default('development'),
10954
+ stage: ${stageEnv(options.stages)},
10730
10955
  }))
10731
10956
  .parse();
10732
10957
  `
@@ -10816,7 +11041,7 @@ export const cleanup = worker
10816
11041
  });
10817
11042
  `
10818
11043
  });
10819
- if (options.database) files$1.push(...databaseFiles(name$1));
11044
+ if (options.database) files$1.push(...databaseFiles());
10820
11045
  if (options.telescope) files$1.push({
10821
11046
  path: "src/config/telescope.ts",
10822
11047
  content: `import { Telescope } from '@geekmidas/telescope';
@@ -10837,7 +11062,7 @@ export const telescope = new Telescope({
10837
11062
  /**
10838
11063
  * OpenAPI output path (fixed, not configurable)
10839
11064
  */
10840
- const OPENAPI_OUTPUT_PATH$1 = "./.gkm/openapi.ts";
11065
+ const OPENAPI_OUTPUT_PATH$1 = "./.gkm/openapi/api.ts";
10841
11066
  /**
10842
11067
  * Endpoints output path (generated by gkm dev/build)
10843
11068
  */
@@ -10943,15 +11168,23 @@ const packageManagerChoices = [
10943
11168
  /**
10944
11169
  * Deploy target choices for prompts
10945
11170
  */
10946
- const deployTargetChoices = [{
10947
- title: "Dokploy",
10948
- value: "dokploy",
10949
- description: "Deploy to Dokploy (Docker-based hosting)"
10950
- }, {
10951
- title: "Configure later",
10952
- value: "none",
10953
- description: "Skip deployment setup for now"
10954
- }];
11171
+ const deployTargetChoices = [
11172
+ {
11173
+ title: "Dokploy",
11174
+ value: "dokploy",
11175
+ description: "Deploy to Dokploy (Docker-based hosting)"
11176
+ },
11177
+ {
11178
+ title: "AWS (SST)",
11179
+ value: "sst",
11180
+ description: "Deploy to your AWS account through SST"
11181
+ },
11182
+ {
11183
+ title: "Configure later",
11184
+ value: "none",
11185
+ description: "Skip deployment setup for now"
11186
+ }
11187
+ ];
10955
11188
  /**
10956
11189
  * Services choices for multi-select prompt
10957
11190
  */
@@ -11044,12 +11277,12 @@ function generatePackageJson(options, template) {
11044
11277
  if (declares && services.mail) dependencies$1["@geekmidas/emailkit"] = GEEKMIDAS_VERSIONS["@geekmidas/emailkit"];
11045
11278
  if (database) {
11046
11279
  dependencies$1["@geekmidas/db"] = GEEKMIDAS_VERSIONS["@geekmidas/db"];
11047
- dependencies$1.kysely = "~0.28.2";
11048
- dependencies$1.pg = "~8.16.0";
11049
- devDependencies$1["@types/pg"] = "~8.15.0";
11280
+ dependencies$1.kysely = DEPENDENCY_VERSIONS.kysely;
11281
+ dependencies$1.pg = DEPENDENCY_VERSIONS.pg;
11282
+ devDependencies$1["@types/pg"] = "~8.23.1";
11050
11283
  devDependencies$1["@geekmidas/testkit"] = GEEKMIDAS_VERSIONS["@geekmidas/testkit"];
11051
11284
  devDependencies$1["@faker-js/faker"] = "~9.8.0";
11052
- devDependencies$1["kysely-ctl"] = "~0.10.0";
11285
+ devDependencies$1["kysely-ctl"] = DEPENDENCY_VERSIONS["kysely-ctl"];
11053
11286
  devDependencies$1["vite-tsconfig-paths"] = "~5.1.0";
11054
11287
  }
11055
11288
  if (monorepo) {
@@ -11069,6 +11302,8 @@ function generatePackageJson(options, template) {
11069
11302
  const appName = pathParts[pathParts.length - 1] || "api";
11070
11303
  packageName = `@${name$1}/${appName}`;
11071
11304
  }
11305
+ dependencies$1["@geekmidas/client"] = GEEKMIDAS_VERSIONS["@geekmidas/client"];
11306
+ dependencies$1["@tanstack/react-query"] = DEPENDENCY_VERSIONS["@tanstack/react-query"];
11072
11307
  const packageJson = {
11073
11308
  name: packageName,
11074
11309
  version: "0.0.1",
@@ -11106,7 +11341,7 @@ function generateSourceFiles(options, template) {
11106
11341
  */
11107
11342
  function generateTestFiles(options, _template) {
11108
11343
  if (!options.database) return [];
11109
- const db = databaseFor(options.name);
11344
+ const db = databaseFor();
11110
11345
  const declares = !options.monorepo;
11111
11346
  const schema = declares ? "../src/constructs/database.ts" : "~/services/database.ts";
11112
11347
  const runtimeUrl = declares ? db.urlKey : "DATABASE_URL";
@@ -11142,14 +11377,16 @@ import pg from 'pg';
11142
11377
  import { wrapVitestKyselyTransaction } from '@geekmidas/testkit/kysely';
11143
11378
  import type { Database } from '${schema}';
11144
11379
 
11145
- const connection = new Kysely<Database>({
11380
+ const db = new Kysely<Database>({
11146
11381
  dialect: new PostgresDialect({
11147
11382
  pool: new pg.Pool({ connectionString: process.env.${runtimeUrl} }),
11148
11383
  }),
11149
11384
  });
11150
11385
 
11151
11386
  export const it = wrapVitestKyselyTransaction<Database>(itVitest, {
11152
- connection,
11387
+ // A function, or a construct: an instance is taken for a construct and has
11388
+ // no service to register.
11389
+ connection: () => db,
11153
11390
  });
11154
11391
  `
11155
11392
  },
@@ -11234,10 +11471,10 @@ export const usersBuilder = KyselyFactory.createBuilder<Database, 'users'>(
11234
11471
  import { it } from './config.ts';
11235
11472
 
11236
11473
  describe('example', () => {
11237
- it('should have a working test setup', async ({ db }) => {
11238
- // db is a transaction-wrapped Kysely instance
11239
- // All changes are automatically rolled back after the test
11240
- expect(db).toBeDefined();
11474
+ it('should have a working test setup', async ({ trx }) => {
11475
+ // trx is a Kysely transaction; everything in it is rolled back after the
11476
+ // test
11477
+ expect(trx).toBeDefined();
11241
11478
  });
11242
11479
  });
11243
11480
  `
@@ -12488,8 +12725,8 @@ export const Alert: Story = {
12488
12725
  ),
12489
12726
  };
12490
12727
  `;
12491
- const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button.tsx';
12492
- export { Input } from './input.tsx';
12728
+ const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button/index.tsx';
12729
+ export { Input } from './input/index.tsx';
12493
12730
  export {
12494
12731
  Card,
12495
12732
  CardHeader,
@@ -12497,17 +12734,17 @@ export {
12497
12734
  CardTitle,
12498
12735
  CardDescription,
12499
12736
  CardContent,
12500
- } from './card.tsx';
12501
- export { Label } from './label.tsx';
12502
- export { Badge, type BadgeProps, badgeVariants } from './badge.tsx';
12503
- export { Separator } from './separator.tsx';
12504
- export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs.tsx';
12737
+ } from './card/index.tsx';
12738
+ export { Label } from './label/index.tsx';
12739
+ export { Badge, type BadgeProps, badgeVariants } from './badge/index.tsx';
12740
+ export { Separator } from './separator/index.tsx';
12741
+ export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs/index.tsx';
12505
12742
  export {
12506
12743
  Tooltip,
12507
12744
  TooltipTrigger,
12508
12745
  TooltipContent,
12509
12746
  TooltipProvider,
12510
- } from './tooltip.tsx';
12747
+ } from './tooltip/index.tsx';
12511
12748
  export {
12512
12749
  Dialog,
12513
12750
  DialogPortal,
@@ -12519,7 +12756,7 @@ export {
12519
12756
  DialogFooter,
12520
12757
  DialogTitle,
12521
12758
  DialogDescription,
12522
- } from './dialog.tsx';
12759
+ } from './dialog/index.tsx';
12523
12760
  `;
12524
12761
  const buttonIndexTsx = buttonTsx;
12525
12762
  const inputIndexTsx = inputTsx;
@@ -12687,8 +12924,8 @@ function generateWebAppFiles(options) {
12687
12924
  [uiPackage]: "workspace:*",
12688
12925
  "@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
12689
12926
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
12690
- "@tanstack/react-query": "~5.80.0",
12691
- "better-auth": "~1.2.0",
12927
+ "@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
12928
+ "better-auth": DEPENDENCY_VERSIONS["better-auth"],
12692
12929
  next: "~16.1.0",
12693
12930
  react: "~19.2.0",
12694
12931
  "react-dom": "~19.2.0"
@@ -12744,7 +12981,7 @@ export default nextConfig;
12744
12981
  baseUrl: ".",
12745
12982
  paths: {
12746
12983
  "~/*": ["./src/*", "../../packages/ui/src/*"],
12747
- [`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"],
12984
+ [`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi/api.ts"],
12748
12985
  [`${modelsPackage}`]: ["../../packages/models/src"],
12749
12986
  [`${modelsPackage}/*`]: ["../../packages/models/src/*"],
12750
12987
  [`${uiPackage}`]: ["../../packages/ui/src"],
@@ -12757,12 +12994,7 @@ export default nextConfig;
12757
12994
  "**/*.tsx",
12758
12995
  ".next/types/**/*.ts"
12759
12996
  ],
12760
- exclude: ["node_modules"],
12761
- references: [
12762
- { path: "../../apps/api" },
12763
- { path: "../../packages/ui" },
12764
- { path: "../../packages/models" }
12765
- ]
12997
+ exclude: ["node_modules"]
12766
12998
  };
12767
12999
  const queryClientTs = `import { QueryClient } from '@tanstack/react-query';
12768
13000
 
@@ -13025,10 +13257,10 @@ function generateTanStackWebFiles(options) {
13025
13257
  [uiPackage]: "workspace:*",
13026
13258
  "@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
13027
13259
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
13028
- "@tanstack/react-query": "~5.80.0",
13260
+ "@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
13029
13261
  "@tanstack/react-router": "^1.87.0",
13030
13262
  "@tanstack/react-start": "^1.87.0",
13031
- "better-auth": "~1.2.0",
13263
+ "better-auth": DEPENDENCY_VERSIONS["better-auth"],
13032
13264
  react: "~19.2.0",
13033
13265
  "react-dom": "~19.2.0"
13034
13266
  },
@@ -13096,7 +13328,7 @@ export default defineConfig({
13096
13328
  [`${modelsPackage}/*`]: ["../../packages/models/src/*"],
13097
13329
  [uiPackage]: ["../../packages/ui/src"],
13098
13330
  [`${uiPackage}/*`]: ["../../packages/ui/src/*"],
13099
- [`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"]
13331
+ [`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi/api.ts"]
13100
13332
  }
13101
13333
  },
13102
13334
  include: [
@@ -13386,12 +13618,27 @@ function detectPackageManager(cwd = process.cwd()) {
13386
13618
  if (userAgent.includes("bun")) return "bun";
13387
13619
  return "npm";
13388
13620
  }
13621
+ /** `gkm init`'s flags, which a name can swallow when a space goes missing. */
13622
+ const INIT_FLAGS = [
13623
+ "yes",
13624
+ "name",
13625
+ "template",
13626
+ "monorepo",
13627
+ "api-path",
13628
+ "pm",
13629
+ "skip-install"
13630
+ ];
13389
13631
  /**
13390
13632
  * Validate project name for npm package naming conventions
13391
13633
  */
13392
13634
  function validateProjectName(name$1) {
13393
13635
  if (!name$1) return "Project name is required";
13394
13636
  if (!/^[a-z0-9-_@/.]+$/i.test(name$1)) return "Project name can only contain letters, numbers, hyphens, underscores, @, /, and .";
13637
+ const flag = name$1.match(/--(\w[\w-]*)$/)?.[1];
13638
+ if (flag && INIT_FLAGS.includes(flag)) {
13639
+ const intended = name$1.slice(0, -(flag.length + 2));
13640
+ return `"${name$1}" ends in the --${flag} flag. Did you mean \`${intended} --${flag}\`?`;
13641
+ }
13395
13642
  const reserved = [
13396
13643
  "node_modules",
13397
13644
  ".git",
@@ -13431,6 +13678,18 @@ function getRunCommand(pkgManager, script) {
13431
13678
  default: return `npm run ${script}`;
13432
13679
  }
13433
13680
  }
13681
+ /**
13682
+ * Run a binary the project installed, through its package manager — so it is
13683
+ * the project's own version, not whatever \`npx\` would fetch.
13684
+ */
13685
+ function getExecCommand(pkgManager, command) {
13686
+ switch (pkgManager) {
13687
+ case "pnpm": return `pnpm exec ${command}`;
13688
+ case "yarn": return `yarn ${command}`;
13689
+ case "bun": return `bunx ${command}`;
13690
+ default: return `npx --no-install ${command}`;
13691
+ }
13692
+ }
13434
13693
  const lockfileByPm = {
13435
13694
  pnpm: "pnpm-lock.yaml",
13436
13695
  yarn: "yarn.lock",
@@ -13501,12 +13760,31 @@ function findWorkspacePackages(cwd, pm) {
13501
13760
 
13502
13761
  //#endregion
13503
13762
  //#region src/init/index.ts
13763
+ /** `' staging, prod '` → `['staging', 'prod']`. */
13764
+ const stageList = (value) => value.split(",").map((name$1) => name$1.trim()).filter(Boolean);
13765
+ /** `eu-west-1`, `us-gov-west-1`, `ap-southeast-2`. */
13766
+ const AWS_REGION = /^[a-z]{2}(-gov)?-[a-z]+-\d$/;
13504
13767
  /**
13505
13768
  * Main init command - scaffolds a new project
13506
13769
  */
13507
13770
  async function initCommand(projectName, options = {}) {
13508
13771
  const cwd = process.cwd();
13509
13772
  const detectedPkgManager = detectPackageManager(cwd);
13773
+ if (options.deploy && !deployTargetChoices.some((choice) => choice.value === options.deploy)) throw new Error(`Unknown deploy target "${options.deploy}". Use ${deployTargetChoices.map((c) => c.value).join(", ")}.`);
13774
+ if (options.region && !AWS_REGION.test(options.region)) throw new Error(`"${options.region}" is not an AWS region. Use one like eu-west-1.`);
13775
+ if (options.stages || options.localStage || options.protectedStage) {
13776
+ const deployed = stageList(options.stages ?? "");
13777
+ const problems = [
13778
+ ...deployedProblems(deployed),
13779
+ ...options.localStage ? stageProblems({
13780
+ local: options.localStage,
13781
+ deployed
13782
+ }) : [],
13783
+ ...options.protectedStage && !deployed.includes(options.protectedStage) ? [`--protected-stage "${options.protectedStage}" is not in --stages`] : []
13784
+ ];
13785
+ if (problems.length) throw new Error([...new Set(problems)].join("\n"));
13786
+ }
13787
+ let deployedSoFar = [];
13510
13788
  prompts.override({});
13511
13789
  const onCancel = () => {
13512
13790
  process.exit(0);
@@ -13557,12 +13835,49 @@ async function initCommand(projectName, options = {}) {
13557
13835
  initial: packageManagerChoices.findIndex((c) => c.value === detectedPkgManager)
13558
13836
  },
13559
13837
  {
13560
- type: options.yes ? null : "select",
13838
+ type: options.yes || options.deploy ? null : "select",
13561
13839
  name: "deployTarget",
13562
13840
  message: "Deployment target:",
13563
13841
  choices: deployTargetChoices,
13564
13842
  initial: 0
13565
13843
  },
13844
+ {
13845
+ type: (_prev, values) => !options.yes && !options.region && (options.deploy ?? values.deployTarget) === "sst" ? "text" : null,
13846
+ name: "region",
13847
+ message: "AWS region (e.g. eu-west-1):",
13848
+ validate: (value) => AWS_REGION.test(value.trim()) || "An AWS region, like eu-west-1"
13849
+ },
13850
+ {
13851
+ type: options.yes || options.stages ? null : "text",
13852
+ name: "deployedStages",
13853
+ message: "Deployed stages, comma-separated (e.g. staging, prod):",
13854
+ validate: (value) => {
13855
+ const deployed = stageList(value);
13856
+ if (deployed.length === 0) return "At least one stage";
13857
+ return deployedProblems(deployed)[0] ?? true;
13858
+ }
13859
+ },
13860
+ {
13861
+ type: (_prev, values) => options.yes || options.protectedStage ? null : stageList(options.stages ?? values.deployedStages ?? "").length > 1 ? "select" : null,
13862
+ name: "protectedStage",
13863
+ message: "Which one is production (retained and protected)?",
13864
+ choices: (_prev, values) => stageList(options.stages ?? values.deployedStages ?? "").map((name$2) => ({
13865
+ title: name$2,
13866
+ value: name$2
13867
+ }))
13868
+ },
13869
+ {
13870
+ type: (_prev, values) => {
13871
+ deployedSoFar = stageList(options.stages ?? values.deployedStages ?? "");
13872
+ return options.yes || options.localStage ? null : "text";
13873
+ },
13874
+ name: "localStage",
13875
+ message: "Local stage, what gkm dev runs as (e.g. dev):",
13876
+ validate: (value) => stageProblems({
13877
+ local: value.trim(),
13878
+ deployed: deployedSoFar
13879
+ })[0] ?? true
13880
+ },
13566
13881
  {
13567
13882
  type: options.yes ? null : "confirm",
13568
13883
  name: "telescope",
@@ -13628,7 +13943,7 @@ async function initCommand(projectName, options = {}) {
13628
13943
  };
13629
13944
  if (services.events === "pgboss") services.db = true;
13630
13945
  const pkgManager = options.pm ? options.pm : options.yes ? "pnpm" : answers.packageManager ?? detectedPkgManager;
13631
- const deployTarget = options.yes ? "dokploy" : answers.deployTarget ?? "dokploy";
13946
+ const deployTarget = options.deploy ?? (options.yes ? "none" : answers.deployTarget ?? "none");
13632
13947
  const database = services.db;
13633
13948
  const frontendFramework = isFullstack ? options.yes ? "nextjs" : answers.frontendFramework ?? "nextjs" : void 0;
13634
13949
  const templateOptions = {
@@ -13643,6 +13958,8 @@ async function initCommand(projectName, options = {}) {
13643
13958
  apiPath: monorepo ? options.apiPath ?? "apps/api" : "",
13644
13959
  packageManager: pkgManager,
13645
13960
  deployTarget,
13961
+ stages: resolveStages(options, answers),
13962
+ ...deployTarget === "sst" ? { region: options.region ?? (options.yes ? "eu-west-1" : answers.region?.trim()) } : {},
13646
13963
  services,
13647
13964
  frontendFramework
13648
13965
  };
@@ -13718,12 +14035,12 @@ async function initCommand(projectName, options = {}) {
13718
14035
  if (services.mail) secretServices.push("mailpit");
13719
14036
  if (services.events === "sns") secretServices.push("localstack");
13720
14037
  if (services.events === "rabbitmq") secretServices.push("rabbitmq");
13721
- const devSecrets = createStageSecrets("development", secretServices, {
14038
+ const local = templateOptions.stages.local;
14039
+ const devSecrets = createStageSecrets(local, secretServices, {
13722
14040
  projectName: name$1,
13723
14041
  eventsBackend: services.events
13724
14042
  });
13725
14043
  const customSecrets = {
13726
- NODE_ENV: "development",
13727
14044
  PORT: "3000",
13728
14045
  LOG_LEVEL: "debug",
13729
14046
  JWT_SECRET: `dev-${Date.now()}-${Math.random().toString(36).slice(2)}`
@@ -13743,8 +14060,8 @@ async function initCommand(projectName, options = {}) {
13743
14060
  }
13744
14061
  devSecrets.custom = customSecrets;
13745
14062
  await writeStageSecrets(devSecrets, targetDir);
13746
- const keyPath = getKeyPath("development", name$1);
13747
- console.log(` Secrets: .gkm/secrets/development.json (encrypted)`);
14063
+ const keyPath = getKeyPath(local, name$1);
14064
+ console.log(` Secrets: .gkm/secrets/${local}.json (encrypted)`);
13748
14065
  console.log(` Key: ${keyPath}\n`);
13749
14066
  if (!options.skipInstall) {
13750
14067
  console.log("\n📦 Installing dependencies...\n");
@@ -13757,11 +14074,13 @@ async function initCommand(projectName, options = {}) {
13757
14074
  console.error("Failed to install dependencies");
13758
14075
  }
13759
14076
  try {
13760
- execSync("npx @biomejs/biome format --write --unsafe .", {
14077
+ execSync(getExecCommand(pkgManager, "biome check --write ."), {
13761
14078
  cwd: targetDir,
13762
14079
  stdio: "inherit"
13763
14080
  });
13764
- } catch {}
14081
+ } catch {
14082
+ console.warn(`\n⚠️ Formatting did not finish cleanly. Run ${getRunCommand(pkgManager, "fmt")} to see why.`);
14083
+ }
13765
14084
  }
13766
14085
  console.log("\n📦 Initializing git repository...\n");
13767
14086
  try {
@@ -13833,19 +14152,46 @@ function printNextSteps(projectName, options, pkgManager) {
13833
14152
  console.log(` └── turbo.json # Turbo config`);
13834
14153
  console.log("");
13835
14154
  }
14155
+ const { local, deployed } = options.stages;
13836
14156
  console.log("🔐 Secrets management:");
13837
- console.log(` gkm secrets:show --stage development # View secrets`);
13838
- console.log(` gkm secrets:set KEY VALUE --stage development # Add secret`);
13839
- console.log(` gkm secrets:init --stage production # Create production secrets`);
14157
+ console.log(` gkm secrets:show --stage ${local} # View secrets`);
14158
+ console.log(` gkm secrets:set KEY VALUE --stage ${local} # Add secret`);
14159
+ for (const stage of deployed) console.log(` gkm secrets:init --stage ${stage} # Create ${stage} secrets`);
13840
14160
  console.log("");
13841
14161
  if (options.deployTarget === "dokploy") {
13842
14162
  console.log("🚀 Deployment:");
13843
- console.log(` ${getRunCommand(pkgManager, "deploy")}`);
14163
+ for (const stage of deployed) console.log(` ${getRunCommand(pkgManager, `deploy:${stage}`)}`);
14164
+ console.log("");
14165
+ }
14166
+ if (options.deployTarget === "sst") {
14167
+ console.log("🚀 Deployment (AWS, through SST):");
14168
+ for (const stage of deployed) console.log(` ${getRunCommand(pkgManager, `deploy:${stage}`)} # gkm build --provider aws, then sst deploy`);
14169
+ console.log(` Uses your AWS credentials, in ${options.region}.`);
14170
+ if (options.services.mail) console.log(" Set MAIL_FROM to a sender verified in SES first.");
13844
14171
  console.log("");
13845
14172
  }
13846
14173
  console.log("📚 Documentation: https://geekmidas.github.io/toolbox/");
13847
14174
  console.log("");
13848
14175
  }
14176
+ /**
14177
+ * The stages the project declares, from the answers or the flags.
14178
+ *
14179
+ * `--yes` with no stage flags has nobody to ask and needs an answer, so it
14180
+ * takes `local`, and one deployed stage, `production`, that is protected.
14181
+ */
14182
+ function resolveStages(options, answers) {
14183
+ const deployed = stageList(options.stages ?? answers.deployedStages ?? (options.yes ? "production" : ""));
14184
+ const kept = options.protectedStage ?? answers.protectedStage ?? (deployed.length === 1 || options.yes ? deployed[0] : void 0);
14185
+ const local = (options.localStage ?? answers.localStage ?? (options.yes ? "local" : "")).trim();
14186
+ const stages = {
14187
+ local,
14188
+ deployed,
14189
+ ...kept ? { protected: [kept] } : {}
14190
+ };
14191
+ const problems = stageProblems(stages);
14192
+ if (problems.length) throw new Error(problems.join("\n"));
14193
+ return stages;
14194
+ }
13849
14195
 
13850
14196
  //#endregion
13851
14197
  //#region src/secrets/index.ts
@@ -14056,19 +14402,20 @@ function maskUrl(url) {
14056
14402
  * Environment variables are sniffed to inject only what the app needs.
14057
14403
  */
14058
14404
  async function testCommand(options = {}) {
14059
- const stage = options.stage ?? "development";
14060
14405
  const cwd = process.cwd();
14406
+ const stage = options.stage ?? await loadWorkspaceConfig(cwd).then((loaded) => loaded.workspace.stages.local).catch(() => void 0);
14407
+ if (!stage) throw new Error("No stage to test with: add `stages` to gkm.config.ts, or pass --stage.");
14061
14408
  console.log(`\n🧪 Running tests with ${stage} environment...\n`);
14062
14409
  const autoSetup = options.autoSetup || Boolean(process.env.GKM_AUTO_SETUP);
14063
14410
  if (autoSetup) {
14064
- const { ensureStageSecrets } = await import("./setup-DGIPd2fh.mjs");
14411
+ const { ensureStageSecrets } = await import("./setup-BA8ciTPU.mjs");
14065
14412
  const generated = await ensureStageSecrets(stage, cwd);
14066
14413
  if (generated) console.log(` 🔐 Generated fresh ${stage} secrets (auto-setup)`);
14067
14414
  }
14068
14415
  const defaultEnv = loadEnvFiles(".env");
14069
14416
  if (defaultEnv.loaded.length > 0) console.log(` 📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
14070
14417
  const result = await prepareEntryCredentials({
14071
- stages: [stage],
14418
+ stage,
14072
14419
  startDocker: true,
14073
14420
  secretsFileName: "test-secrets.json",
14074
14421
  resolveDockerPorts: "full",
@@ -14296,7 +14643,7 @@ program.name("gkm").description("GeekMidas backend framework CLI").version(packa
14296
14643
  const opts = program.opts();
14297
14644
  if (opts.debug) enableDebug();
14298
14645
  });
14299
- program.command("init").description("Scaffold a new project").argument("[name]", "Project name").option("--template <template>", "Project template (minimal, api, serverless, worker)").option("--skip-install", "Skip dependency installation", false).option("-y, --yes", "Skip prompts, use defaults", false).option("--monorepo", "Setup as monorepo with packages/models", false).option("--api-path <path>", "API app path in monorepo (default: apps/api)").option("--pm <manager>", "Package manager (pnpm, npm, yarn, bun)").action(async (name$1, options) => {
14646
+ program.command("init").description("Scaffold a new project").argument("[name]", "Project name").option("--template <template>", "Project template (minimal, api, serverless, worker)").option("--skip-install", "Skip dependency installation", false).option("-y, --yes", "Skip prompts, use defaults", false).option("--monorepo", "Setup as monorepo with packages/models", false).option("--api-path <path>", "API app path in monorepo (default: apps/api)").option("--pm <manager>", "Package manager (pnpm, npm, yarn, bun)").option("--deploy <target>", "Where it deploys (dokploy, sst, none)").option("--region <region>", "AWS region for an SST deploy (e.g. eu-west-1)").option("--stages <names>", "Deployed stages, comma-separated (e.g. staging,prod)").option("--protected-stage <name>", "Which deployed stage is production").option("--local-stage <name>", "What gkm dev runs as (e.g. dev)").option("--region <region>", "AWS region for an SST deploy (e.g. eu-west-1)").action(async (name$1, options) => {
14300
14647
  try {
14301
14648
  const globalOptions = program.opts();
14302
14649
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
@@ -14316,7 +14663,7 @@ program.command("trust").description("Trust the local edge's certificate authori
14316
14663
  process.exit(1);
14317
14664
  }
14318
14665
  });
14319
- program.command("setup").description("Reconcile declared constructs — containers, databases, buckets, secrets").option("--stage <stage>", "Stage name", "development").option("--force", "Regenerate secrets even if they exist").option("--skip-docker", "Skip starting Docker services").option("-y, --yes", "Skip prompts").action(async (options) => {
14666
+ program.command("setup").description("Reconcile declared constructs — containers, databases, buckets, secrets").option("--stage <stage>", "Stage name (default: stages.local)").option("--force", "Regenerate secrets even if they exist").option("--skip-docker", "Skip starting Docker services").option("-y, --yes", "Skip prompts").action(async (options) => {
14320
14667
  try {
14321
14668
  const globalOptions = program.opts();
14322
14669
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
@@ -14388,7 +14735,7 @@ program.command("exec").description("Run a command with secrets injected into Cr
14388
14735
  process.exit(1);
14389
14736
  }
14390
14737
  });
14391
- program.command("test").description("Run tests with secrets loaded from environment").option("--stage <stage>", "Stage to load secrets from", "development").option("--run", "Run tests once without watch mode").option("--watch", "Enable watch mode").option("--coverage", "Generate coverage report").option("--ui", "Open Vitest UI").option("--auto-setup", "Generate a fresh stage (secrets + key) from the workspace config when none exists (for CI; also via GKM_AUTO_SETUP)").argument("[pattern]", "Pattern to filter tests").action(async (pattern, options) => {
14738
+ program.command("test").description("Run tests with secrets loaded from environment").option("--stage <stage>", "Stage to load secrets from (default: stages.local)").option("--run", "Run tests once without watch mode").option("--watch", "Enable watch mode").option("--coverage", "Generate coverage report").option("--ui", "Open Vitest UI").option("--auto-setup", "Generate a fresh stage (secrets + key) from the workspace config when none exists (for CI; also via GKM_AUTO_SETUP)").argument("[pattern]", "Pattern to filter tests").action(async (pattern, options) => {
14392
14739
  try {
14393
14740
  const globalOptions = program.opts();
14394
14741
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
@@ -14536,7 +14883,7 @@ program.command("secrets:push").description("Push secrets to remote provider (SS
14536
14883
  const { workspace } = await loadWorkspaceConfig$1();
14537
14884
  const secrets = await readStageSecrets$1(options.stage, workspace.root);
14538
14885
  if (secrets) {
14539
- const { derivedContainers: derivedContainers$1 } = await import("./workspace-CnTqIlB3.mjs");
14886
+ const { derivedContainers: derivedContainers$1 } = await import("./workspace-BzrG_2SI.mjs");
14540
14887
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
14541
14888
  if (result) {
14542
14889
  await writeStageSecrets$1(result.secrets, workspace.root);
@@ -14565,7 +14912,7 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
14565
14912
  console.error(`No remote secrets found for stage "${options.stage}".`);
14566
14913
  process.exit(1);
14567
14914
  }
14568
- const { derivedContainers: derivedContainers$1 } = await import("./workspace-CnTqIlB3.mjs");
14915
+ const { derivedContainers: derivedContainers$1 } = await import("./workspace-BzrG_2SI.mjs");
14569
14916
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
14570
14917
  if (result) {
14571
14918
  secrets = result.secrets;
@@ -14579,7 +14926,7 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
14579
14926
  process.exit(1);
14580
14927
  }
14581
14928
  });
14582
- program.command("secrets:reconcile").description("Backfill missing custom secrets from workspace config").option("--stage <stage>", "Stage name", "development").action(async (options) => {
14929
+ program.command("secrets:reconcile").description("Backfill missing custom secrets from workspace config").option("--stage <stage>", "Stage name (default: stages.local)").action(async (options) => {
14583
14930
  try {
14584
14931
  const globalOptions = program.opts();
14585
14932
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
@@ -14587,19 +14934,20 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
14587
14934
  const { reconcileMissingSecrets } = await import("./reconcile-CFs8C_1a.mjs");
14588
14935
  const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-CYlGwWTj.mjs");
14589
14936
  const { workspace } = await loadWorkspaceConfig$1();
14590
- const secrets = await readStageSecrets$1(options.stage, workspace.root);
14937
+ const stage = options.stage ?? workspace.stages.local;
14938
+ const secrets = await readStageSecrets$1(stage, workspace.root);
14591
14939
  if (!secrets) {
14592
- console.error(`No secrets found for stage "${options.stage}". Run "gkm secrets:init --stage ${options.stage}" first.`);
14940
+ console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
14593
14941
  process.exit(1);
14594
14942
  }
14595
- const { derivedContainers: derivedContainers$1 } = await import("./workspace-CnTqIlB3.mjs");
14596
- const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
14943
+ const { derivedContainers: derivedContainers$1 } = await import("./workspace-BzrG_2SI.mjs");
14944
+ const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, stage));
14597
14945
  if (!result) {
14598
- console.log(`\n✓ Secrets for stage "${options.stage}" are up-to-date`);
14946
+ console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
14599
14947
  return;
14600
14948
  }
14601
14949
  await writeStageSecrets$1(result.secrets, workspace.root);
14602
- console.log(`\n✓ Reconciled ${result.addedKeys.length} missing secret(s) for stage "${options.stage}":`);
14950
+ console.log(`\n✓ Reconciled ${result.addedKeys.length} missing secret(s) for stage "${stage}":`);
14603
14951
  for (const key of result.addedKeys) console.log(` + ${key}`);
14604
14952
  } catch (error) {
14605
14953
  console.error(formatError(error));