@geekmidas/cli 10.0.0-alpha.1 → 10.0.0-alpha.10

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 (76) 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-DbPE1KXF.cjs → config-BZ1F0WuA.cjs} +7 -80
  5. package/dist/config-BZ1F0WuA.cjs.map +1 -0
  6. package/dist/{config-D8o6I_7Z.mjs → config-DVVph9yz.mjs} +9 -76
  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-K961ThG3.cjs → credentials-Xamqe_NJ.cjs} +20 -19
  13. package/dist/credentials-Xamqe_NJ.cjs.map +1 -0
  14. package/dist/{credentials-Dh0Q6HzY.mjs → credentials-dP8vRVRv.mjs} +20 -19
  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-BThUzEIp.d.mts → index-C6e-ULLx.d.mts} +2 -2
  19. package/dist/index-C6e-ULLx.d.mts.map +1 -0
  20. package/dist/{index-CHB4tSzX.d.cts → index-CR5urhWX.d.cts} +2 -2
  21. package/dist/index-CR5urhWX.d.cts.map +1 -0
  22. package/dist/index.cjs +1917 -597
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.mjs +1919 -599
  25. package/dist/index.mjs.map +1 -1
  26. package/dist/{openapi-CljONd_M.cjs → openapi-SjsgARie.cjs} +127 -50
  27. package/dist/openapi-SjsgARie.cjs.map +1 -0
  28. package/dist/{openapi-CgpMWMmM.mjs → openapi-Xx0A7Wg0.mjs} +109 -50
  29. package/dist/openapi-Xx0A7Wg0.mjs.map +1 -0
  30. package/dist/openapi.cjs +5 -3
  31. package/dist/openapi.d.cts +46 -10
  32. package/dist/openapi.d.cts.map +1 -1
  33. package/dist/openapi.d.mts +46 -10
  34. package/dist/openapi.d.mts.map +1 -1
  35. package/dist/openapi.mjs +4 -4
  36. package/dist/reconcile/public.d.cts +1 -1
  37. package/dist/reconcile/public.d.mts +1 -1
  38. package/dist/{setup-DSutDAlt.mjs → setup-BA8ciTPU.mjs} +4 -4
  39. package/dist/setup-BsZvZP6f.cjs +11 -0
  40. package/dist/{setup-DOsYueeQ.mjs → setup-C8KOx8_8.mjs} +5 -5
  41. package/dist/setup-C8KOx8_8.mjs.map +1 -0
  42. package/dist/{setup-CJiQM2pX.cjs → setup-hC6hI86f.cjs} +5 -5
  43. package/dist/setup-hC6hI86f.cjs.map +1 -0
  44. package/dist/{types-C-arnQ1r.d.mts → types-6pkp2RRj.d.mts} +46 -10
  45. package/dist/types-6pkp2RRj.d.mts.map +1 -0
  46. package/dist/{types-abJQuk7m.d.cts → types-Y3fKQSAK.d.cts} +46 -10
  47. package/dist/types-Y3fKQSAK.d.cts.map +1 -0
  48. package/dist/workspace/index.cjs +1 -1
  49. package/dist/workspace/index.d.cts +2 -2
  50. package/dist/workspace/index.d.mts +2 -2
  51. package/dist/workspace/index.mjs +1 -1
  52. package/dist/workspace-BzrG_2SI.mjs +8 -0
  53. package/dist/workspace-C6VJQVtG.cjs +6845 -0
  54. package/dist/workspace-C6VJQVtG.cjs.map +1 -0
  55. package/dist/{workspace-8acupeaI.cjs → workspace-XXtNhqJl.cjs} +3 -3
  56. package/dist/workspace-zoqRbihZ.mjs +6678 -0
  57. package/dist/workspace-zoqRbihZ.mjs.map +1 -0
  58. package/package.json +33 -32
  59. package/dist/config-D8o6I_7Z.mjs.map +0 -1
  60. package/dist/config-DbPE1KXF.cjs.map +0 -1
  61. package/dist/credentials-Dh0Q6HzY.mjs.map +0 -1
  62. package/dist/credentials-K961ThG3.cjs.map +0 -1
  63. package/dist/index-BThUzEIp.d.mts.map +0 -1
  64. package/dist/index-CHB4tSzX.d.cts.map +0 -1
  65. package/dist/openapi-CgpMWMmM.mjs.map +0 -1
  66. package/dist/openapi-CljONd_M.cjs.map +0 -1
  67. package/dist/setup-CJiQM2pX.cjs.map +0 -1
  68. package/dist/setup-DMtU3p41.cjs +0 -11
  69. package/dist/setup-DOsYueeQ.mjs.map +0 -1
  70. package/dist/types-C-arnQ1r.d.mts.map +0 -1
  71. package/dist/types-abJQuk7m.d.cts.map +0 -1
  72. package/dist/workspace-7FqprxuJ.mjs +0 -4323
  73. package/dist/workspace-7FqprxuJ.mjs.map +0 -1
  74. package/dist/workspace-XvqO9jIv.cjs +0 -4472
  75. package/dist/workspace-XvqO9jIv.cjs.map +0 -1
  76. package/dist/workspace-yP5AHnrn.mjs +0 -8
package/dist/index.cjs CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  const require_chunk = require('./chunk-CUT6urMc.cjs');
3
3
  const require_discover = require('./discover-DTmFeloL.cjs');
4
- const require_config = require('./config-DbPE1KXF.cjs');
5
- const require_workspace = require('./workspace-XvqO9jIv.cjs');
4
+ const require_config = require('./config-BZ1F0WuA.cjs');
5
+ const require_workspace = require('./workspace-C6VJQVtG.cjs');
6
6
  const require_credentials = require('./credentials-BapzjyV5.cjs');
7
- const require_credentials$1 = require('./credentials-K961ThG3.cjs');
7
+ const require_credentials$1 = require('./credentials-Xamqe_NJ.cjs');
8
8
  const require_generator = require('./generator-CNzBw7g5.cjs');
9
9
  const require_storage = require('./storage-BJmzYxt8.cjs');
10
- const require_openapi = require('./openapi-CljONd_M.cjs');
10
+ const require_openapi = require('./openapi-SjsgARie.cjs');
11
11
  const require_emit = require('./emit-CbcRolsa.cjs');
12
12
  const require_dokploy_api = require('./dokploy-api-BsVH0y_s.cjs');
13
13
  const require_encryption = require('./encryption-C6jaU0hE.cjs');
@@ -15,7 +15,7 @@ const require_CachedStateProvider = require('./CachedStateProvider-CqPdW7CB.cjs'
15
15
  const require_fullstack_secrets = require('./fullstack-secrets-C-GGohke.cjs');
16
16
  const require_openapi_react_query = require('./openapi-react-query-CmJ1xDbn.cjs');
17
17
  require('./sync-SizaChmC.cjs');
18
- const require_setup = require('./setup-CJiQM2pX.cjs');
18
+ const require_setup = require('./setup-hC6hI86f.cjs');
19
19
  const node_fs = require_chunk.__toESM(require("node:fs"));
20
20
  const node_path = require_chunk.__toESM(require("node:path"));
21
21
  const __geekmidas_manifest = require_chunk.__toESM(require("@geekmidas/manifest"));
@@ -43,7 +43,7 @@ const prompts = require_chunk.__toESM(require("prompts"));
43
43
 
44
44
  //#region package.json
45
45
  var name = "@geekmidas/cli";
46
- var version = "10.0.0-alpha.0";
46
+ var version = "10.0.0-alpha.10";
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";
@@ -124,12 +124,12 @@ var repository = {
124
124
  "url": "https://github.com/geekmidas/toolbox"
125
125
  };
126
126
  var dependencies = {
127
- "@apidevtools/swagger-parser": "^10.1.0",
128
- "@aws-sdk/client-iam": "~3.992.0",
129
- "@aws-sdk/client-route-53": "~3.992.0",
130
- "@aws-sdk/client-s3": "~3.992.0",
131
- "@aws-sdk/client-ssm": "~3.992.0",
132
- "@aws-sdk/credential-providers": "~3.992.0",
127
+ "@apidevtools/swagger-parser": "~13.1.0",
128
+ "@aws-sdk/client-iam": "~3.1136.0",
129
+ "@aws-sdk/client-route-53": "~3.1136.0",
130
+ "@aws-sdk/client-s3": "~3.1136.0",
131
+ "@aws-sdk/client-ssm": "~3.1136.0",
132
+ "@aws-sdk/credential-providers": "~3.1136.0",
133
133
  "@geekmidas/cache": "workspace:*",
134
134
  "@geekmidas/constructs": "workspace:~",
135
135
  "@geekmidas/db": "workspace:*",
@@ -138,28 +138,29 @@ var dependencies = {
138
138
  "@geekmidas/logger": "workspace:~",
139
139
  "@geekmidas/manifest": "workspace:*",
140
140
  "@geekmidas/schema": "workspace:~",
141
- "chokidar": "~4.0.3",
142
- "commander": "^12.1.0",
143
- "dotenv": "~17.2.3",
144
- "esbuild": "^0.28.2",
145
- "fast-glob": "^3.3.2",
146
- "hono": "~4.8.0",
147
- "lodash.kebabcase": "^4.1.1",
148
- "openapi-typescript": "^7.4.2",
149
- "pg": "~8.17.1",
141
+ "chokidar": "~5.0.0",
142
+ "commander": "~15.0.0",
143
+ "dotenv": "~18.0.1",
144
+ "esbuild": "~0.28.2",
145
+ "fast-glob": "~3.3.3",
146
+ "get-tsconfig": "~4.10.1",
147
+ "hono": "~4.13.8",
148
+ "lodash.kebabcase": "~4.1.1",
149
+ "openapi-typescript": "~7.13.0",
150
+ "pg": "~8.23.0",
150
151
  "prompts": "~2.4.2",
151
152
  "tsx": "~4.20.3",
152
- "yaml": "~2.8.2"
153
+ "yaml": "~2.9.1"
153
154
  };
154
155
  var devDependencies = {
155
156
  "@geekmidas/testkit": "workspace:*",
156
- "@types/lodash.kebabcase": "^4.1.9",
157
+ "@types/lodash.kebabcase": "~4.1.9",
157
158
  "@types/node": "~24.9.1",
158
- "@types/pg": "~8.16.0",
159
+ "@types/pg": "~8.23.1",
159
160
  "@types/prompts": "~2.4.9",
160
- "typescript": "^5.8.2",
161
- "vitest": "^3.2.4",
162
- "zod": "~4.1.13"
161
+ "typescript": "~5.8.2",
162
+ "vitest": "~3.2.4",
163
+ "zod": "~4.6.5"
163
164
  };
164
165
  var peerDependencies = { "@geekmidas/telescope": "workspace:~" };
165
166
  var peerDependenciesMeta = { "@geekmidas/telescope": { "optional": true } };
@@ -415,11 +416,16 @@ var CronGenerator = class extends require_discover.ConstructGenerator {
415
416
  const provider = options?.provider || "aws-lambda";
416
417
  const logger$12 = console;
417
418
  const cronInfos = [];
419
+ if (provider === "server") {
420
+ await this.generateServerCronsFile(outputDir, constructs);
421
+ logger$12.log(`Generated server crons file with ${constructs.length} crons`);
422
+ return cronInfos;
423
+ }
418
424
  if (constructs.length === 0 || provider !== "aws-lambda") return cronInfos;
419
425
  const cronsDir = (0, node_path.join)(outputDir, "crons");
420
426
  await (0, node_fs_promises.mkdir)(cronsDir, { recursive: true });
421
427
  for (const { key, construct, path } of constructs) {
422
- const handlerFile = await this.generateCronHandler(cronsDir, path.relative, key, context);
428
+ const handlerFile = await this.generateCronHandler(cronsDir, path.relative, key, context, construct.owner);
423
429
  cronInfos.push({
424
430
  name: key,
425
431
  handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
@@ -436,17 +442,173 @@ var CronGenerator = class extends require_discover.ConstructGenerator {
436
442
  isConstruct(value) {
437
443
  return __geekmidas_constructs_crons.Cron.isCron(value);
438
444
  }
439
- async generateCronHandler(outputDir, sourceFile, exportName, context) {
445
+ /**
446
+ * The crons file a server entry imports.
447
+ *
448
+ * Shaped after `generateServerSubscribersFile`: one module, every cron, a
449
+ * `setupCrons` the generated entry calls beside `setupSubscribers`.
450
+ *
451
+ * It picks its scheduler from what the process can reach. With a Postgres it
452
+ * uses pg-boss, whose schedules live in the database — so a deployment
453
+ * running four replicas fires each job once, which is the thing a timer in
454
+ * every process gets wrong and does not report. With no database it falls
455
+ * back to timers and says plainly that it is doing so.
456
+ */
457
+ async generateServerCronsFile(outputDir, crons) {
458
+ await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
459
+ const cronsPath = (0, node_path.join)(outputDir, "crons.ts");
460
+ const importsByFile = /* @__PURE__ */ new Map();
461
+ for (const { path, key } of crons) {
462
+ const relativePath = (0, node_path.relative)((0, node_path.dirname)(cronsPath), path.relative);
463
+ const importPath = relativePath.replace(/\.ts$/, ".js");
464
+ if (!importsByFile.has(importPath)) importsByFile.set(importPath, []);
465
+ importsByFile.get(importPath)?.push(key);
466
+ }
467
+ const imports = Array.from(importsByFile.entries()).map(([importPath, exports$2]) => `import { ${exports$2.join(", ")} } from '${importPath}';`).join("\n");
468
+ const entries = crons.map(({ key }) => ` { name: '${key}', cron: ${key} }`).join(",\n");
469
+ const content = `/**
470
+ * Generated cron setup.
471
+ *
472
+ * Schedules every cron this app declares, in the process that serves it.
473
+ *
474
+ * The schedule lives in Postgres, through pg-boss, in the database the worker
475
+ * named with \`.database(db)\`. That is what makes running several replicas
476
+ * safe: each job fires once, where a timer in every process fires it once per
477
+ * replica and reports nothing.
478
+ *
479
+ * No connection string appears here. The construct that owns the database is
480
+ * the only thing that knows its key, and it is resolved through service
481
+ * discovery like any other dependency.
482
+ */
483
+ import type { EnvironmentParser } from '@geekmidas/envkit';
484
+ import type { Logger } from '@geekmidas/logger';
485
+ import { toCronExpression } from '@geekmidas/constructs/crons';
486
+ import { ServiceDiscovery } from '@geekmidas/services';
487
+ ${imports}
488
+
489
+ const crons = [
490
+ ${entries}
491
+ ];
492
+
493
+ type Stoppable = { stop: () => Promise<void> | void };
494
+ const running: Stoppable[] = [];
495
+
496
+ export async function setupCrons(
497
+ envParser: EnvironmentParser<any>,
498
+ logger: Logger,
499
+ ): Promise<void> {
500
+ if (crons.length === 0) {
501
+ return;
502
+ }
503
+
504
+ const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
505
+
506
+ // Resolved once per cron rather than per firing: registering services on
507
+ // every tick would reconnect a database every minute.
508
+ const prepared = [];
509
+ for (const { name, cron } of crons) {
510
+ try {
511
+ const schedule = toCronExpression(cron.schedule);
512
+ const services = cron.services.length > 0
513
+ ? await serviceDiscovery.register(cron.services)
514
+ : {};
515
+
516
+ prepared.push({ name, cron, schedule, services });
517
+ } catch (error) {
518
+ // An unrepresentable schedule is a build-time mistake, and failing the
519
+ // process for it would take the HTTP server down with it.
520
+ logger.error({ error, cron: name }, 'Cron has no runnable schedule, skipping');
521
+ }
522
+ }
523
+
524
+ const run = async (entry: (typeof prepared)[number]) => {
525
+ try {
526
+ await entry.cron.handler({
527
+ input: undefined,
528
+ services: entry.services,
529
+ logger: entry.cron.logger,
530
+ });
531
+ } catch (error) {
532
+ logger.error({ error, cron: entry.name }, 'Cron failed');
533
+ }
534
+ };
535
+
536
+ // Declared once on the worker, carried onto every cron it built — so this
537
+ // reads it from any of them rather than from config or the environment.
538
+ const scheduleStore = prepared.find(({ cron }) => cron.scheduleStore)?.cron
539
+ .scheduleStore;
540
+
541
+ if (!scheduleStore) {
542
+ // Declared, not discovered. A worker says where its schedules live with
543
+ // \`.database(db)\`; inferring one from whatever database happened to be
544
+ // around would move the schedules the day an app declared a second.
545
+ logger.error(
546
+ { crons: prepared.map(({ name }) => name) },
547
+ 'These crons have nowhere to keep their schedule. A server fires its own ' +
548
+ 'crons, and a process scheduling in memory fires each job once per ' +
549
+ 'replica — so the schedule lives in Postgres. Say where with ' +
550
+ '\`new Worker(…).database(db)\`.',
551
+ );
552
+ return;
553
+ }
554
+
555
+ const [store] = await serviceDiscovery
556
+ .register([scheduleStore])
557
+ .then((s) => Object.values(s));
558
+
559
+ const { PgBoss } = await import('pg-boss');
560
+ // The pool the construct already opened — no connection string is read here,
561
+ // and none is named anywhere outside the construct that owns it.
562
+ const boss = new PgBoss({ db: store as never });
563
+ await boss.start();
564
+
565
+ // What this app declares now. Anything else under the prefix belonged to a
566
+ // cron since renamed or deleted, and would keep firing at a handler that has
567
+ // gone.
568
+ //
569
+ // The prefix is \`cron.\` rather than \`cron:\`: pg-boss accepts
570
+ // alphanumerics, underscores, hyphens, periods and slashes in a queue name,
571
+ // and rejects a colon outright.
572
+ const declared = new Set(prepared.map(({ name }) => \`cron.\${name}\`));
573
+ for (const existing of await boss.getSchedules()) {
574
+ if (existing.name.startsWith('cron.') && !declared.has(existing.name)) {
575
+ await boss.unschedule(existing.name);
576
+ logger.info({ cron: existing.name }, 'Removed a schedule nothing declares');
577
+ }
578
+ }
579
+
580
+ for (const entry of prepared) {
581
+ const queue = \`cron.\${entry.name}\`;
582
+ await boss.createQueue(queue);
583
+ // UTC, so this agrees with the schedule a deploy target would have made.
584
+ await boss.schedule(queue, entry.schedule, undefined, { tz: 'UTC' });
585
+ await boss.work(queue, () => run(entry));
586
+
587
+ logger.info({ cron: entry.name, schedule: entry.schedule }, 'Cron scheduled');
588
+ }
589
+
590
+ running.push({ stop: () => boss.stop() });
591
+ logger.info({ count: prepared.length }, 'Crons scheduled');
592
+ }
593
+
594
+ export async function stopCrons(): Promise<void> {
595
+ await Promise.all(running.map((r) => r.stop()));
596
+ running.length = 0;
597
+ }
598
+ `;
599
+ await (0, node_fs_promises.writeFile)(cronsPath, content);
600
+ return cronsPath;
601
+ }
602
+ async generateCronHandler(outputDir, sourceFile, exportName, context, owner) {
440
603
  const handlerFileName = `${exportName}.ts`;
441
604
  const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
442
605
  const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
443
606
  const importPath = relativePath.replace(/\.ts$/, ".js");
444
- const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
445
- const relativeLoggerPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.loggerPath);
446
- const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/crons';
607
+ const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
608
+ const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/aws';
447
609
  import { ${exportName} } from '${importPath}';
448
- import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
449
- import ${context.loggerImportPattern} from '${relativeLoggerPath}';
610
+ ${runtime.imports}
611
+ ${runtime.bindings}
450
612
 
451
613
  const adapter = new AWSScheduledFunction(envParser, ${exportName});
452
614
 
@@ -562,7 +724,7 @@ var FunctionGenerator = class extends require_discover.ConstructGenerator {
562
724
  const functionsDir = (0, node_path.join)(outputDir, "functions");
563
725
  await (0, node_fs_promises.mkdir)(functionsDir, { recursive: true });
564
726
  for (const { key, construct, path } of constructs) {
565
- const handlerFile = await this.generateFunctionHandler(functionsDir, path.relative, key, context);
727
+ const handlerFile = await this.generateFunctionHandler(functionsDir, path.relative, key, context, construct.owner);
566
728
  functionInfos.push({
567
729
  name: key,
568
730
  handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
@@ -575,17 +737,16 @@ var FunctionGenerator = class extends require_discover.ConstructGenerator {
575
737
  }
576
738
  return functionInfos;
577
739
  }
578
- async generateFunctionHandler(outputDir, sourceFile, exportName, context) {
740
+ async generateFunctionHandler(outputDir, sourceFile, exportName, context, owner) {
579
741
  const handlerFileName = `${exportName}.ts`;
580
742
  const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
581
743
  const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
582
744
  const importPath = relativePath.replace(/\.ts$/, ".js");
583
- const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
584
- const relativeLoggerPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.loggerPath);
745
+ const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
585
746
  const content = `import { AWSLambdaFunction } from '@geekmidas/constructs/aws';
586
747
  import { ${exportName} } from '${importPath}';
587
- import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
588
- import ${context.loggerImportPattern} from '${relativeLoggerPath}';
748
+ ${runtime.imports}
749
+ ${runtime.bindings}
589
750
 
590
751
  const adapter = new AWSLambdaFunction(envParser, ${exportName});
591
752
 
@@ -624,7 +785,7 @@ var QueueGenerator = class extends require_discover.ConstructGenerator {
624
785
  const queuesDir = (0, node_path.join)(outputDir, "queues");
625
786
  await (0, node_fs_promises.mkdir)(queuesDir, { recursive: true });
626
787
  for (const { key, construct, path } of constructs) {
627
- const handlerFile = await this.generateQueueHandler(queuesDir, path.relative, key, context);
788
+ const handlerFile = await this.generateQueueHandler(queuesDir, path.relative, key, context, construct.owner);
628
789
  queueInfos.push({
629
790
  name: construct.name,
630
791
  handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
@@ -638,13 +799,14 @@ var QueueGenerator = class extends require_discover.ConstructGenerator {
638
799
  }
639
800
  return queueInfos;
640
801
  }
641
- async generateQueueHandler(outputDir, sourceFile, exportName, context) {
802
+ async generateQueueHandler(outputDir, sourceFile, exportName, context, owner) {
642
803
  const handlerPath = (0, node_path.join)(outputDir, `${exportName}.ts`);
643
804
  const importPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile).replace(/\.ts$/, ".js");
644
- const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
805
+ const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
645
806
  const content = `import { AWSLambdaQueue } from '@geekmidas/constructs/aws';
646
807
  import { ${exportName} } from '${importPath}';
647
- import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
808
+ ${runtime.imports}
809
+ ${runtime.bindings}
648
810
 
649
811
  const adapter = new AWSLambdaQueue(envParser, ${exportName});
650
812
 
@@ -787,7 +949,7 @@ var SubscriberGenerator = class extends require_discover.ConstructGenerator {
787
949
  const subscribersDir = (0, node_path.join)(outputDir, "subscribers");
788
950
  await (0, node_fs_promises.mkdir)(subscribersDir, { recursive: true });
789
951
  for (const { key, construct, path } of constructs) {
790
- const handlerFile = await this.generateSubscriberHandler(subscribersDir, path.relative, key, construct, context);
952
+ const handlerFile = await this.generateSubscriberHandler(subscribersDir, path.relative, key, construct, context, construct.owner);
791
953
  subscriberInfos.push({
792
954
  name: key,
793
955
  handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
@@ -805,15 +967,16 @@ var SubscriberGenerator = class extends require_discover.ConstructGenerator {
805
967
  }
806
968
  return subscriberInfos;
807
969
  }
808
- async generateSubscriberHandler(outputDir, sourceFile, exportName, _subscriber, context) {
970
+ async generateSubscriberHandler(outputDir, sourceFile, exportName, _subscriber, context, owner) {
809
971
  const handlerFileName = `${exportName}.ts`;
810
972
  const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
811
973
  const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
812
974
  const importPath = relativePath.replace(/\.ts$/, ".js");
813
- const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
975
+ const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
814
976
  const content = `import { AWSLambdaSubscriber } from '@geekmidas/constructs/aws';
815
977
  import { ${exportName} } from '${importPath}';
816
- import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
978
+ ${runtime.imports}
979
+ ${runtime.bindings}
817
980
 
818
981
  const adapter = new AWSLambdaSubscriber(envParser, ${exportName});
819
982
 
@@ -1196,13 +1359,7 @@ async function devCommand(options) {
1196
1359
  }
1197
1360
  const resolved = resolveProviders(config, { provider: "server" });
1198
1361
  logger$9.log("🚀 Starting development server...");
1199
- logger$9.log(`Loading routes from: ${config.routes}`);
1200
- if (config.functions) logger$9.log(`Loading functions from: ${config.functions}`);
1201
- if (config.crons) logger$9.log(`Loading crons from: ${config.crons}`);
1202
- if (config.subscribers) logger$9.log(`Loading subscribers from: ${config.subscribers}`);
1203
- logger$9.log(`Using envParser: ${config.envParser}`);
1204
- const { path: envParserPath, importPattern: envParserImportPattern } = require_config.parseModuleConfig(config.envParser, "envParser");
1205
- const { path: loggerPath, importPattern: loggerImportPattern } = require_config.parseModuleConfig(config.logger, "logger");
1362
+ logger$9.log(`Loading constructs from: ${config.constructs}`);
1206
1363
  const telescope = normalizeTelescopeConfig(config.telescope);
1207
1364
  if (telescope) logger$9.log(`🔭 Telescope enabled at ${telescope.path}`);
1208
1365
  const studio = normalizeStudioConfig(config.studio);
@@ -1213,21 +1370,17 @@ async function devCommand(options) {
1213
1370
  const enableOpenApi = openApiConfig.enabled || resolved.enableOpenApi;
1214
1371
  if (enableOpenApi) logger$9.log(`📄 OpenAPI output: ${require_openapi.OPENAPI_OUTPUT_PATH}`);
1215
1372
  const buildContext = {
1216
- envParserPath,
1217
- envParserImportPattern,
1218
- loggerPath,
1219
- loggerImportPattern,
1220
1373
  telescope,
1221
1374
  studio,
1222
1375
  hooks
1223
1376
  };
1224
1377
  await buildServer(config, buildContext, resolved.providers[0], enableOpenApi, appRoot);
1225
- if (enableOpenApi) await require_openapi.generateOpenApi(config);
1378
+ if (enableOpenApi) await require_openapi.generateOpenApiFrom(config.constructs, { openapi: config.openapi });
1226
1379
  const runtime = config.runtime ?? "node";
1227
1380
  let secretsJsonPath;
1228
- const appSecrets = await require_credentials$1.loadSecretsForApp(secretsRoot, workspaceAppName);
1381
+ const appSecrets = await require_credentials$1.loadSecretsForApp(secretsRoot, config.stages.local, workspaceAppName);
1229
1382
  if (workspace && require_credentials$1.usesConstructs(workspace)) {
1230
- const reconciled = await require_credentials$1.reconcileWorkspace(workspace, { stage: "development" });
1383
+ const reconciled = await require_credentials$1.reconcileWorkspace(workspace, { stage: workspace.stages.local });
1231
1384
  if (reconciled.changed && reconciled.plan.containers.length > 0) {
1232
1385
  logger$9.log(`🐳 Services: ${reconciled.plan.containers.join(", ")}`);
1233
1386
  for (const [container, address] of Object.entries(reconciled.addresses)) logger$9.log(` ${container}: ${address}`);
@@ -1255,19 +1408,9 @@ async function devCommand(options) {
1255
1408
  }
1256
1409
  const devServer = new DevServer(resolved.providers[0], options.port ?? workspaceAppPort ?? 3e3, options.portExplicit ?? false, enableOpenApi, telescope, studio, runtime, appRoot, secretsJsonPath);
1257
1410
  await devServer.start();
1258
- const envParserFile = config.envParser.split("#")[0] ?? config.envParser;
1259
- const loggerFile = config.logger.split("#")[0] ?? config.logger;
1260
1411
  const hooksFileParts = config.hooks?.server?.split("#");
1261
1412
  const hooksFile = hooksFileParts?.[0];
1262
- const watchPatterns = [
1263
- config.routes,
1264
- ...config.functions ? [config.functions] : [],
1265
- ...config.crons ? [config.crons] : [],
1266
- ...config.subscribers ? [config.subscribers] : [],
1267
- envParserFile.endsWith(".ts") ? envParserFile : `${envParserFile}.ts`,
1268
- loggerFile.endsWith(".ts") ? loggerFile : `${loggerFile}.ts`,
1269
- ...hooksFile ? [hooksFile.endsWith(".ts") ? hooksFile : `${hooksFile}.ts`] : []
1270
- ].flat().filter((p) => typeof p === "string");
1413
+ const watchPatterns = [config.constructs, ...hooksFile ? [hooksFile.endsWith(".ts") ? hooksFile : `${hooksFile}.ts`] : []].flat().filter((p) => typeof p === "string");
1271
1414
  const normalizedPatterns = watchPatterns.map((p) => p.startsWith("./") ? p.slice(2) : p);
1272
1415
  logger$9.log(`👀 Watching for changes in: ${normalizedPatterns.join(", ")}`);
1273
1416
  const resolvedFiles = await (0, fast_glob.default)(normalizedPatterns, {
@@ -1300,7 +1443,8 @@ async function devCommand(options) {
1300
1443
  try {
1301
1444
  logger$9.log("🔄 Rebuilding...");
1302
1445
  await buildServer(config, buildContext, resolved.providers[0], enableOpenApi, appRoot, true);
1303
- if (enableOpenApi) await require_openapi.generateOpenApi(config, {
1446
+ if (enableOpenApi) await require_openapi.generateOpenApiFrom(config.constructs, {
1447
+ openapi: config.openapi,
1304
1448
  silent: true,
1305
1449
  bustCache: true
1306
1450
  });
@@ -1518,15 +1662,15 @@ async function validateFrontendApps(workspace) {
1518
1662
  */
1519
1663
  async function loadDevSecrets(workspace) {
1520
1664
  if (!workspace.secrets.enabled) return {};
1521
- const stages = ["dev", "development"];
1522
- for (const stage of stages) if (require_storage.secretsExist(stage, workspace.root)) {
1665
+ const stage = workspace.stages.local;
1666
+ if (require_storage.secretsExist(stage, workspace.root)) {
1523
1667
  const secrets = await require_storage.readStageSecrets(stage, workspace.root);
1524
1668
  if (secrets) {
1525
1669
  logger$9.log(`🔐 Loading secrets from stage: ${stage}`);
1526
1670
  return require_storage.toEmbeddableSecrets(secrets);
1527
1671
  }
1528
1672
  }
1529
- 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`);
1530
1674
  return {};
1531
1675
  }
1532
1676
  /**
@@ -1570,7 +1714,7 @@ async function workspaceDevCommand(workspace, options) {
1570
1714
  const rawSecrets = await loadDevSecrets(workspace);
1571
1715
  let secretsEnv;
1572
1716
  if (require_credentials$1.usesConstructs(workspace)) {
1573
- const reconciled = await require_credentials$1.reconcileWorkspace(workspace, { stage: "development" });
1717
+ const reconciled = await require_credentials$1.reconcileWorkspace(workspace, { stage: workspace.stages.local });
1574
1718
  if (reconciled.changed && reconciled.plan.containers.length > 0) {
1575
1719
  logger$9.log(`🐳 Services: ${reconciled.plan.containers.join(", ")}`);
1576
1720
  for (const [container, address] of Object.entries(reconciled.addresses)) logger$9.log(` ${container}: ${address}`);
@@ -1714,7 +1858,7 @@ async function buildServer(config, context, provider, enableOpenApi, appRoot = p
1714
1858
  const queueGenerator = new QueueGenerator();
1715
1859
  const topicGenerator = new TopicGenerator();
1716
1860
  const [allEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics] = await Promise.all([
1717
- endpointGenerator.load(config.routes, appRoot, bustCache),
1861
+ endpointGenerator.load(config.constructs, appRoot, bustCache),
1718
1862
  config.functions ? functionGenerator.load(config.functions, appRoot, bustCache) : [],
1719
1863
  config.crons ? cronGenerator.load(config.crons, appRoot, bustCache) : [],
1720
1864
  config.subscribers ? subscriberGenerator.load(config.subscribers, appRoot, bustCache) : [],
@@ -2302,18 +2446,36 @@ export default app;
2302
2446
  `);
2303
2447
  }
2304
2448
  const logger$7 = console;
2449
+ /**
2450
+ * What a build run at the workspace root reads.
2451
+ *
2452
+ * One backend app there, and it is that app's config, so its own `openapi`
2453
+ * and `telescope` settings still apply. Otherwise it is the workspace's: every
2454
+ * construct glob, and the backends they resolve to.
2455
+ */
2456
+ function rootGkmConfig(workspace) {
2457
+ const backends = Object.entries(workspace.apps).filter(([, app]) => app.type === "backend");
2458
+ const only = backends.length === 1 ? backends[0] : void 0;
2459
+ const own = only ? require_workspace.getAppGkmConfig(workspace, only[0]) : void 0;
2460
+ return own ?? {
2461
+ services: workspace.services,
2462
+ stages: workspace.stages,
2463
+ constructs: require_workspace.allConstructGlobs(workspace)
2464
+ };
2465
+ }
2305
2466
  async function buildCommand(options) {
2306
2467
  const loadedConfig = await require_config.loadWorkspaceConfig();
2307
2468
  if (loadedConfig.type === "workspace") {
2308
2469
  const cwd = (0, node_path.resolve)(process.cwd());
2309
- const workspaceRoot = (0, node_path.resolve)(loadedConfig.workspace.root);
2310
- const isAtWorkspaceRoot = cwd === workspaceRoot;
2311
- if (isAtWorkspaceRoot) {
2470
+ const workspaceRoot$1 = (0, node_path.resolve)(loadedConfig.workspace.root);
2471
+ const isAtWorkspaceRoot = cwd === workspaceRoot$1;
2472
+ const someAppIsElsewhere = Object.values(loadedConfig.workspace.apps).some((app) => (0, node_path.resolve)(workspaceRoot$1, app.path) !== workspaceRoot$1);
2473
+ if (isAtWorkspaceRoot && someAppIsElsewhere) {
2312
2474
  logger$7.log("📦 Detected workspace configuration");
2313
2475
  return workspaceBuildCommand(loadedConfig.workspace, options);
2314
2476
  }
2315
2477
  }
2316
- const config = loadedConfig.type === "workspace" ? (await require_config.loadAppConfig()).gkmConfig : await require_config.loadConfig();
2478
+ const config = loadedConfig.type !== "workspace" ? await require_config.loadConfig() : (0, node_path.resolve)(process.cwd()) === (0, node_path.resolve)(loadedConfig.workspace.root) ? rootGkmConfig(loadedConfig.workspace) : (await require_config.loadAppConfig()).gkmConfig;
2317
2479
  const resolved = resolveProviders(config, options);
2318
2480
  const backendConfig = config.services ?? loadedConfig.workspace?.services;
2319
2481
  const cacheBackend = require_credentials$1.cacheBackendOf(backendConfig?.cache);
@@ -2321,13 +2483,7 @@ async function buildCommand(options) {
2321
2483
  const production = normalizeProductionConfig(options.production ?? false, productionConfigFromGkm);
2322
2484
  if (production) logger$7.log(`🏭 Building for PRODUCTION`);
2323
2485
  logger$7.log(`Building with providers: ${resolved.providers.join(", ")}`);
2324
- logger$7.log(`Loading routes from: ${formatRoutes(config.routes)}`);
2325
- if (config.functions) logger$7.log(`Loading functions from: ${formatRoutes(config.functions)}`);
2326
- if (config.crons) logger$7.log(`Loading crons from: ${formatRoutes(config.crons)}`);
2327
- if (config.subscribers) logger$7.log(`Loading subscribers from: ${formatRoutes(config.subscribers)}`);
2328
- logger$7.log(`Using envParser: ${config.envParser}`);
2329
- const { path: envParserPath, importPattern: envParserImportPattern } = require_config.parseModuleConfig(config.envParser, "envParser");
2330
- const { path: loggerPath, importPattern: loggerImportPattern } = require_config.parseModuleConfig(config.logger, "logger");
2486
+ logger$7.log(`Loading constructs from: ${formatRoutes(config.constructs)}`);
2331
2487
  const telescope = production ? void 0 : normalizeTelescopeConfig(config.telescope);
2332
2488
  if (telescope) logger$7.log(`🔭 Telescope enabled at ${telescope.path}`);
2333
2489
  const studio = production ? void 0 : normalizeStudioConfig(config.studio);
@@ -2352,10 +2508,23 @@ async function buildCommand(options) {
2352
2508
  sources: constructSources
2353
2509
  }) : {};
2354
2510
  const surfaces = Object.values(declared).filter((d) => d.kind === "rest-api");
2355
- const primary = surfaces.find((d) => d.endpoints.length === 0) ?? surfaces[0];
2511
+ const workspaceRoot = loadedConfig.workspace.root;
2512
+ const served = surfaces.find((d) => (0, node_path.resolve)(workspaceRoot, d.path) === (0, node_path.resolve)(process.cwd()));
2513
+ const primary = served ?? surfaces.find((d) => d.endpoints.length === 0) ?? surfaces[0];
2356
2514
  const browsable = Object.entries(declared).find(([, d]) => d.kind === "database");
2357
2515
  const browsableSource = browsable ? constructSources[browsable[0]] : void 0;
2516
+ const owners = {};
2517
+ for (const [id, declaration] of Object.entries(declared)) {
2518
+ if (declaration.kind !== "rest-api" && declaration.kind !== "worker") continue;
2519
+ const source = constructSources[id];
2520
+ if (!source) continue;
2521
+ owners[id] = {
2522
+ specifier: source.file,
2523
+ exportName: source.exportName
2524
+ };
2525
+ }
2358
2526
  const buildContext = {
2527
+ owners,
2359
2528
  ...primary ? { surface: {
2360
2529
  id: primary.id,
2361
2530
  trustedOriginsKey: (0, __geekmidas_manifest.provideKey)(primary.id, "trustedOrigins"),
@@ -2365,10 +2534,6 @@ async function buildCommand(options) {
2365
2534
  exportName: constructSources[primary.id].exportName
2366
2535
  } } : {}
2367
2536
  } } : {},
2368
- envParserPath,
2369
- envParserImportPattern,
2370
- loggerPath,
2371
- loggerImportPattern,
2372
2537
  telescope,
2373
2538
  studio: studio && browsableSource ? {
2374
2539
  ...studio,
@@ -2395,14 +2560,16 @@ async function buildCommand(options) {
2395
2560
  const subscriberGenerator = new SubscriberGenerator();
2396
2561
  const queueGenerator = new QueueGenerator();
2397
2562
  const topicGenerator = new TopicGenerator();
2398
- const [allEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics] = await Promise.all([
2399
- endpointGenerator.load(config.routes),
2400
- config.functions ? functionGenerator.load(config.functions) : [],
2401
- config.crons ? cronGenerator.load(config.crons) : [],
2402
- config.subscribers ? subscriberGenerator.load(config.subscribers) : [],
2403
- config.queues ? queueGenerator.load(config.queues) : [],
2404
- config.topics ? topicGenerator.load(config.topics) : []
2563
+ const code = constructGlobs$1;
2564
+ const [loadedEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics] = await Promise.all([
2565
+ endpointGenerator.load(code),
2566
+ functionGenerator.load(code),
2567
+ cronGenerator.load(code),
2568
+ subscriberGenerator.load(code),
2569
+ queueGenerator.load(code),
2570
+ topicGenerator.load(code)
2405
2571
  ]);
2572
+ const allEndpoints = primary ? loadedEndpoints.filter(({ construct }) => !construct.surface || construct.surface.id === primary.id) : loadedEndpoints;
2406
2573
  logger$7.log(`Found ${allEndpoints.length} endpoints`);
2407
2574
  logger$7.log(`Found ${allFunctions.length} functions`);
2408
2575
  logger$7.log(`Found ${allCrons.length} crons`);
@@ -2410,7 +2577,7 @@ async function buildCommand(options) {
2410
2577
  logger$7.log(`Found ${allQueues.length} queues`);
2411
2578
  logger$7.log(`Found ${allTopics.length} topics`);
2412
2579
  if (allEndpoints.length === 0 && allFunctions.length === 0 && allCrons.length === 0 && allSubscribers.length === 0 && allQueues.length === 0 && allTopics.length === 0) {
2413
- const selfServing = Object.entries(declared).find(([id, d]) => d.kind === "rest-api" && d.endpoints.length > 0 && constructSources[id] !== void 0 && (0, node_path.resolve)(loadedConfig.workspace.root, require_config.resolveAppSpec(id, d.app ?? {}, loadedConfig.workspace.root, "rest-api").path) === (0, node_path.resolve)(process.cwd()));
2580
+ const selfServing = served && served.endpoints.length > 0 && constructSources[served.id] !== void 0 ? [served.id, served] : void 0;
2414
2581
  if (selfServing) {
2415
2582
  const [id] = selfServing;
2416
2583
  await writeSurfaceEntry((0, node_path.join)(process.cwd(), ".gkm", "server"), constructSources[id]);
@@ -2427,7 +2594,7 @@ async function buildCommand(options) {
2427
2594
  const providerResult = await buildForProvider(provider, buildContext, rootOutputDir, endpointGenerator, functionGenerator, cronGenerator, subscriberGenerator, queueGenerator, topicGenerator, allEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics, resolved.enableOpenApi, options.skipBundle ?? false, options.stage);
2428
2595
  if (providerResult.masterKey) result = providerResult;
2429
2596
  }
2430
- await require_openapi.generateOpenApi(config, { bustCache: true });
2597
+ await require_openapi.generateOpenApi(allEndpoints.map(({ construct }) => construct), { openapi: config.openapi });
2431
2598
  return result;
2432
2599
  }
2433
2600
  async function buildForProvider(provider, context, rootOutputDir, endpointGenerator, functionGenerator, cronGenerator, subscriberGenerator, queueGenerator, topicGenerator, endpoints, functions, crons, subscribers, queues, topics, enableOpenApi, skipBundle, stage) {
@@ -2681,7 +2848,6 @@ async function workspaceBuildCommand(workspace, options) {
2681
2848
  });
2682
2849
  }
2683
2850
  logger$7.log(`\n✅ Workspace build complete!`);
2684
- await require_openapi.openapiCommand({ cwd: workspace.root });
2685
2851
  logger$7.log(`\n📋 Build Summary:`);
2686
2852
  for (const result of results) {
2687
2853
  const icon = result.type === "backend" ? "⚙️" : result.type === "mobile" ? "📱" : "🌐";
@@ -3051,7 +3217,7 @@ function minioCompose(options) {
3051
3217
  return [
3052
3218
  "services:",
3053
3219
  ` ${service}:`,
3054
- " image: quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z",
3220
+ " image: pgsty/minio:RELEASE.2026-08-04T00-00-00Z",
3055
3221
  ` command: server /data --console-address ":9001"`,
3056
3222
  " environment:",
3057
3223
  ` MINIO_ROOT_USER: ${user}`,
@@ -3870,7 +4036,7 @@ const DEFAULT_SERVICE_IMAGES = {
3870
4036
  postgres: "postgres",
3871
4037
  redis: "redis",
3872
4038
  rabbitmq: "rabbitmq",
3873
- minio: "quay.io/minio/minio",
4039
+ minio: "pgsty/minio",
3874
4040
  mailpit: "axllent/mailpit",
3875
4041
  localstack: "floci/floci"
3876
4042
  };
@@ -3879,7 +4045,7 @@ const DEFAULT_SERVICE_VERSIONS = {
3879
4045
  postgres: `${__geekmidas_manifest.DEFAULT_POSTGRES_VERSION}-alpine`,
3880
4046
  redis: "7-alpine",
3881
4047
  rabbitmq: "3-management-alpine",
3882
- minio: "RELEASE.2025-09-07T16-13-09Z",
4048
+ minio: "RELEASE.2026-08-04T00-00-00Z",
3883
4049
  mailpit: "latest",
3884
4050
  localstack: "latest"
3885
4051
  };
@@ -4331,7 +4497,7 @@ function getInfraServiceImage(serviceName$1, pin) {
4331
4497
  const defaults = {
4332
4498
  postgres: "postgres:18-alpine",
4333
4499
  redis: "redis:7-alpine",
4334
- minio: "quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z"
4500
+ minio: "pgsty/minio:RELEASE.2026-08-04T00-00-00Z"
4335
4501
  };
4336
4502
  return pin || defaults[serviceName$1];
4337
4503
  }
@@ -5531,7 +5697,7 @@ async function workspaceDockerCommand(workspace, options) {
5531
5697
  logger$5.log(`\n Generated: .dockerignore (workspace root)`);
5532
5698
  const dockerCompose = generateWorkspaceCompose(workspace, {
5533
5699
  registry: options.registry,
5534
- containers: await require_credentials$1.derivedContainers(workspace, "development")
5700
+ containers: await require_credentials$1.derivedContainers(workspace, workspace.stages.local)
5535
5701
  });
5536
5702
  const composePath = (0, node_path.join)(dockerDir, "docker-compose.yml");
5537
5703
  await (0, node_fs_promises.writeFile)(composePath, dockerCompose);
@@ -6977,6 +7143,7 @@ async function workspaceDeployCommand(configured, options) {
6977
7143
  */
6978
7144
  async function deployCommand(options) {
6979
7145
  const loadedConfig = await require_config.loadWorkspaceConfig();
7146
+ require_workspace.assertDeployedStage(loadedConfig.workspace.stages, options.stage);
6980
7147
  return workspaceDeployCommand(loadedConfig.workspace, options);
6981
7148
  }
6982
7149
 
@@ -7342,6 +7509,501 @@ function printStateDetails(state) {
7342
7509
  }
7343
7510
  }
7344
7511
 
7512
+ //#endregion
7513
+ //#region src/init/constructs.ts
7514
+ /** The glob every generated config points at. One glob, every kind. */
7515
+ const CONSTRUCTS_GLOB = "./src/constructs/**/*.ts";
7516
+ /**
7517
+ * Where a workspace keeps its constructs: at the root, beside the apps that
7518
+ * share them, rather than inside any one of them.
7519
+ */
7520
+ const WORKSPACE_CONSTRUCTS_GLOB = "./constructs/**/*.ts";
7521
+ /**
7522
+ * Where each routes structure puts an app's handlers, relative to the app.
7523
+ *
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.
7526
+ */
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
+ }
7533
+ }
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) {
7544
+ return {
7545
+ id,
7546
+ service: (0, __geekmidas_manifest.serviceKey)(id),
7547
+ urlKey: (0, __geekmidas_manifest.provideKey)(id, "url")
7548
+ };
7549
+ }
7550
+ /** The database a project declares. */
7551
+ function databaseFor() {
7552
+ return {
7553
+ ...scaffolded("Database"),
7554
+ ownerUrlKey: (0, __geekmidas_manifest.provideKey)("Database", "ownerUrl")
7555
+ };
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");
7563
+ /**
7564
+ * The files a project's declared database needs.
7565
+ *
7566
+ * Shared by every template, so a worker and an API declare the same database
7567
+ * the same way — and so the schema type, the migration, and the id the test
7568
+ * setup reads all come from one place.
7569
+ */
7570
+ function databaseFiles() {
7571
+ const db = databaseFor();
7572
+ return [{
7573
+ path: "src/constructs/database.ts",
7574
+ content: `import { KyselyDatabase } from '@geekmidas/constructs/database/kysely';
7575
+ import type { Generated } from 'kysely';
7576
+
7577
+ /** Your database schema. Add tables here. */
7578
+ export interface Database {
7579
+ users: {
7580
+ id: Generated<string>;
7581
+ name: string;
7582
+ email: string;
7583
+ created_at: Generated<Date>;
7584
+ };
7585
+ }
7586
+
7587
+ /**
7588
+ * The app's database, declared once.
7589
+ *
7590
+ * The container, the database inside it, its roles and schema, and
7591
+ * \`${db.urlKey}\` all derive from this line — \`gkm dev\` reconciles them
7592
+ * before the server starts, which is why nothing lists \`postgres\` anywhere.
7593
+ *
7594
+ * Both type arguments or neither: TypeScript has no partial type-argument
7595
+ * inference, so passing only \`Database\` would leave the name at \`string\`
7596
+ * and widen the service key away from \`${db.service}\`.
7597
+ */
7598
+ export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}');
7599
+ `
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 {
7610
+ path: "src/db/migrations/001_create_users.ts",
7611
+ content: `import type { Kysely } from 'kysely';
7612
+
7613
+ export async function up(db: Kysely<unknown>): Promise<void> {
7614
+ await db.schema
7615
+ .createTable('users')
7616
+ .addColumn('id', 'uuid', (col) =>
7617
+ col.primaryKey().defaultTo(db.fn('gen_random_uuid')),
7618
+ )
7619
+ .addColumn('name', 'varchar(255)', (col) => col.notNull())
7620
+ .addColumn('email', 'varchar(255)', (col) => col.notNull().unique())
7621
+ .addColumn('created_at', 'timestamptz', (col) =>
7622
+ col.notNull().defaultTo(db.fn('now')),
7623
+ )
7624
+ .execute();
7625
+
7626
+ await db.schema
7627
+ .createIndex('users_email_idx')
7628
+ .on('users')
7629
+ .column('email')
7630
+ .execute();
7631
+ }
7632
+
7633
+ export async function down(db: Kysely<unknown>): Promise<void> {
7634
+ await db.schema.dropTable('users').execute();
7635
+ }
7636
+ `
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(/^\.\//, "")}`];
7649
+ }
7650
+
7651
+ //#endregion
7652
+ //#region src/init/generators/agents.ts
7653
+ /**
7654
+ * `AGENTS.md` and `CLAUDE.md` for a scaffolded project.
7655
+ *
7656
+ * A generated project is the first thing an agent sees, and without this it
7657
+ * sees only the output: a `gkm.config.ts` naming three keys, a `constructs/`
7658
+ * directory, and a build step that reads both. None of that says what the
7659
+ * shape *is*, so an agent reconstructs it from the framework it recognises —
7660
+ * and writes an Express app beside a `RestApi`, or restates in config what a
7661
+ * construct already declared, which is the exact mistake v10 exists to make
7662
+ * impossible.
7663
+ *
7664
+ * Instructions live in `AGENTS.md` because that is the file every tool reads.
7665
+ * `CLAUDE.md` points at it rather than copying it: two files saying the same
7666
+ * thing is two files to keep in step, and the copy that drifts is always the
7667
+ * one nobody opened.
7668
+ *
7669
+ * Content is derived from the same `TemplateOptions` the rest of the scaffold
7670
+ * is built from, so a project with no database gets no database section, and a
7671
+ * worker gets no endpoint guide. A guide that documents what was not generated
7672
+ * teaches the reader to distrust it.
7673
+ */
7674
+ function generateAgentFiles(options, template) {
7675
+ return [{
7676
+ path: "AGENTS.md",
7677
+ content: agentsContent(options, template)
7678
+ }, {
7679
+ path: "CLAUDE.md",
7680
+ content: claudeContent()
7681
+ }];
7682
+ }
7683
+ /**
7684
+ * The pointer.
7685
+ *
7686
+ * Deliberately short and deliberately not a summary — a summary is a second
7687
+ * copy of the conventions that ages independently of the first.
7688
+ */
7689
+ function claudeContent() {
7690
+ return `# CLAUDE.md
7691
+
7692
+ The conventions for this project live in [AGENTS.md](./AGENTS.md).
7693
+
7694
+ Read that file before writing code here. It is the single copy, kept that way
7695
+ on purpose: anything repeated here would be a second thing to keep in step, and
7696
+ the stale one is always the copy nobody opened.
7697
+ `;
7698
+ }
7699
+ function agentsContent(options, template) {
7700
+ const { name: name$1, monorepo, database, telescope, studio, packageManager, services } = options;
7701
+ const constructsGlob = monorepo ? WORKSPACE_CONSTRUCTS_GLOB : CONSTRUCTS_GLOB;
7702
+ const isWorker = template.name === "worker";
7703
+ const isServerless = template.name === "serverless";
7704
+ const run = packageManager === "npm" ? "npx" : `${packageManager} exec`;
7705
+ const sections = [];
7706
+ sections.push(`# Agent conventions — ${name$1}
7707
+
7708
+ Rules for anyone, human or agent, writing code in this project. Built with
7709
+ [\`@geekmidas/toolbox\`](https://github.com/geekmidas/toolbox).
7710
+
7711
+ Read this before adding a file. Most of what follows exists because the obvious
7712
+ approach from another framework produces something that compiles, runs, and is
7713
+ wrong in a way nothing catches.`);
7714
+ sections.push(`## The one idea
7715
+
7716
+ **Constructs declare; config does not restate.**
7717
+
7718
+ A construct — a \`RestApi\`, a database, a bucket, a topic — is the single
7719
+ declaration of a thing. The build reads those declarations and derives
7720
+ everything downstream from them: which containers exist, which apps get built,
7721
+ what the OpenAPI spec says, which env vars a process needs.
7722
+
7723
+ \`gkm.config.ts\` holds only what no construct can answer:
7724
+
7725
+ \`\`\`typescript
7726
+ export default defineWorkspace({
7727
+ name: '${name$1}',
7728
+ stages: { local: 'dev', deployed: ['staging', 'prod'], protected: ['prod'] },
7729
+ constructs: '${constructsGlob}',
7730
+ secrets: { enabled: true },
7731
+ });
7732
+ \`\`\`
7733
+
7734
+ If you find yourself adding a key to config that names something already
7735
+ declared in a construct — a path, a port, a route, a container — stop. That is
7736
+ the drift this design removes. The second copy is the one that goes wrong.`);
7737
+ sections.push(`## Layout
7738
+
7739
+ \`\`\`
7740
+ ${layoutBlock(options, template)}
7741
+ \`\`\`
7742
+
7743
+ Handlers are found by **one glob**, and what kind a file exports is decided by
7744
+ the value it exports, not by the directory it sits in.`);
7745
+ sections.push(`## Commands
7746
+
7747
+ \`\`\`bash
7748
+ ${run} gkm dev # dev server, hot reload, services up, secrets injected
7749
+ ${run} gkm build # production build
7750
+ ${run} gkm test # tests, with secrets injected
7751
+ ${run} gkm exec -- <cmd> # run any command with secrets injected
7752
+ ${run} gkm openapi # regenerate the OpenAPI spec
7753
+ \`\`\`
7754
+
7755
+ Secrets are encrypted at rest and injected at run time:
7756
+
7757
+ \`\`\`bash
7758
+ ${run} gkm secrets:set DATABASE_URL postgres://...
7759
+ ${run} gkm secrets:show
7760
+ \`\`\`
7761
+
7762
+ **Never run a tool that needs project env vars directly.** It will see an empty
7763
+ environment and fail in a way that looks like a config bug. Go through
7764
+ \`gkm exec --\`, which is what injects them.`);
7765
+ if (!isWorker) sections.push(endpointSection(options));
7766
+ if (isWorker || isServerless || services.events) sections.push(backgroundSection(template));
7767
+ if (database) sections.push(databaseSection());
7768
+ sections.push(`## Environment
7769
+
7770
+ Environment is parsed once, through \`envkit\`, and never read from
7771
+ \`process.env\` directly:
7772
+
7773
+ \`\`\`typescript
7774
+ import { EnvironmentParser } from '@geekmidas/envkit';
7775
+ import { Credentials } from '@geekmidas/envkit/credentials';
7776
+
7777
+ export const envParser = new EnvironmentParser({
7778
+ ...process.env,
7779
+ ...Credentials,
7780
+ });
7781
+
7782
+ export const config = envParser
7783
+ .create((get) => ({
7784
+ port: get('PORT').string().transform(Number).default(3000),
7785
+ }))
7786
+ .parse();
7787
+ \`\`\`
7788
+
7789
+ \`Credentials\` is what \`gkm dev\` and \`gkm exec\` inject. A bare
7790
+ \`process.env.FOO\` bypasses both the decryption and the validation, so it reads
7791
+ as \`undefined\` in exactly the environments that matter.`);
7792
+ sections.push(conventionsSection());
7793
+ sections.push(testingSection(database));
7794
+ if (telescope || studio) {
7795
+ const tools = [telescope && "**Telescope** — request and exception monitoring", studio && "**Studio** — dev dashboard and database browser"].filter(Boolean);
7796
+ sections.push(`## Dev tools
7797
+
7798
+ ${tools.map((t) => `- ${t}`).join("\n")}
7799
+
7800
+ Both are derived from declared constructs. Neither needs wiring up by hand.`);
7801
+ }
7802
+ sections.push(`## Before you say it works
7803
+
7804
+ \`\`\`bash
7805
+ ${run} gkm build
7806
+ \`\`\`
7807
+
7808
+ A build is the only thing that proves the manifest still resolves. Code that
7809
+ typechecks can still declare two constructs with one id, or a handler no glob
7810
+ reaches — and neither shows up until something reads the graph.`);
7811
+ return `${sections.join("\n\n")}\n`;
7812
+ }
7813
+ function layoutBlock(options, template) {
7814
+ const { monorepo, apiPath, database } = options;
7815
+ const isWorker = template.name === "worker";
7816
+ if (monorepo) return `constructs/ # every construct: the surface, the database, topics
7817
+ ${apiPath}/ # the API app
7818
+ gkm.config.ts # name, constructs glob, secrets`;
7819
+ const lines = ["src/constructs/ # the surface, and anything it declares", "src/config/ # env parser, logger"];
7820
+ if (!isWorker) lines.push("src/endpoints/ # HTTP handlers");
7821
+ if (isWorker) {
7822
+ lines.push("src/crons/ # scheduled work");
7823
+ lines.push("src/subscribers/ # topic consumers");
7824
+ }
7825
+ if (database) lines.push("src/db/ # migrations, schema");
7826
+ lines.push("gkm.config.ts # name, constructs glob, secrets");
7827
+ return lines.join("\n");
7828
+ }
7829
+ function endpointSection(options) {
7830
+ const { database } = options;
7831
+ return `## Adding an endpoint
7832
+
7833
+ An endpoint is built **from the surface that serves it**. That is where its
7834
+ logger, environment parser and authorizers come from, so none of them is passed
7835
+ per endpoint:
7836
+
7837
+ \`\`\`typescript
7838
+ import { z } from 'zod';
7839
+ import { router } from '../constructs/router.ts';
7840
+
7841
+ export const createUser = router
7842
+ .post('/users')
7843
+ .body(z.object({ name: z.string(), email: z.email() }))
7844
+ .output(z.object({ id: z.uuid() }))
7845
+ .handle(async ({ body, logger${database ? ", services" : ""} }) => {
7846
+ logger.info({ name: body.name }, 'creating user');
7847
+ ${database ? ` const user = await services.database
7848
+ .insertInto('users')
7849
+ .values({ name: body.name, email: body.email })
7850
+ .returningAll()
7851
+ .executeTakeFirstOrThrow();
7852
+
7853
+ return { id: user.id };` : ` return { id: crypto.randomUUID() };`}
7854
+ });
7855
+ \`\`\`
7856
+
7857
+ **Export it.** An endpoint that is never exported is never found — the glob
7858
+ loads the module and filters by value, so a handler assigned to a local
7859
+ \`const\` simply does not exist as far as the build is concerned.
7860
+
7861
+ **Do not write an HTTP server.** No \`express()\`, no \`new Hono()\`, no
7862
+ \`app.listen\`. The entry is generated from the surface; a hand-written server
7863
+ beside it is a second app that shadows the real one.
7864
+
7865
+ ### Branching the factory
7866
+
7867
+ Share what a *group* of endpoints needs by branching once, in its own file:
7868
+
7869
+ \`\`\`typescript
7870
+ export const router = api.endpoints${database ? ".database(database)" : ""};
7871
+ export const sessionRouter = router.session(/* … */);
7872
+ \`\`\`
7873
+
7874
+ Branch for a group, never for a single endpoint — a one-endpoint branch is
7875
+ indirection with nothing on the other side of it.`;
7876
+ }
7877
+ function backgroundSection(template) {
7878
+ const isWorker = template.name === "worker";
7879
+ return `## Background work
7880
+
7881
+ Crons, subscribers and functions are declared the same way endpoints are: as
7882
+ exported values the build discovers.
7883
+
7884
+ \`\`\`typescript
7885
+ // a scheduled job, built from the worker that runs it
7886
+ import { worker } from '../constructs/worker.ts';
7887
+
7888
+ export const cleanup = worker
7889
+ .cron('rate(1 day)')
7890
+ .handle(async ({ logger }) => {
7891
+ logger.info('cleaning up');
7892
+ });
7893
+ \`\`\`
7894
+
7895
+ \`\`\`typescript
7896
+ // a topic subscriber
7897
+ import { userEvents } from '../constructs/topics.ts';
7898
+ import { worker } from '../constructs/worker.ts';
7899
+
7900
+ export const onUserCreated = worker
7901
+ .topic(userEvents)
7902
+ .subscribe(['user.created'])
7903
+ .handle(async ({ events, logger }) => {
7904
+ logger.info({ count: events.length }, 'handling user events');
7905
+ });
7906
+ \`\`\`
7907
+
7908
+ Handlers receive a **batch**, not one event. Both transports deliver in
7909
+ batches, so a per-event round trip is a round trip per event for no reason.
7910
+
7911
+ ${isWorker ? `This project is background work and nothing else — there is no HTTP surface
7912
+ to add routes to.` : `Declaring a topic is what makes the broker exist. Nothing lists it in config.`}`;
7913
+ }
7914
+ function databaseSection() {
7915
+ return `## Database
7916
+
7917
+ The database is a **construct**, not a hand-rolled service. Declaring it is what
7918
+ makes a Postgres exist locally and a database exist on deploy — nothing names
7919
+ \`postgres\` in config.
7920
+
7921
+ An endpoint reaches it by declaring the dependency, and gets it typed:
7922
+
7923
+ \`\`\`typescript
7924
+ export const listUsers = router
7925
+ .get('/users')
7926
+ .output(z.array(z.object({ id: z.uuid() })))
7927
+ .handle(async ({ services }) => {
7928
+ return services.database.selectFrom('users').selectAll().execute();
7929
+ });
7930
+ \`\`\`
7931
+
7932
+ Queries go through Kysely. Never open your own connection pool — the one the
7933
+ construct provides is the one that gets credentials, pooling and shutdown.`;
7934
+ }
7935
+ function conventionsSection() {
7936
+ return `## Conventions
7937
+
7938
+ **Zod formats are top-level schemas.**
7939
+
7940
+ \`\`\`typescript
7941
+ // yes
7942
+ z.email()
7943
+ z.url()
7944
+ z.uuid()
7945
+
7946
+ // no
7947
+ z.string().email()
7948
+ z.string().url()
7949
+ \`\`\`
7950
+
7951
+ Zod 4 made each format a schema in its own right. The chained form still runs,
7952
+ but it types as a \`ZodString\` carrying a check, so it cannot be composed or
7953
+ narrowed — and it emits different JSON Schema, which means a published API
7954
+ contract would depend on which spelling someone reached for.
7955
+
7956
+ \`envkit\` is not affected: \`get('ADMIN_EMAIL').string().email()\` is its own
7957
+ builder, not Zod's. Leave it alone.
7958
+
7959
+ **Style.** Two-space indent, single quotes, semicolons, trailing commas, 80
7960
+ columns. \`import type\` for type-only imports. Run the formatter rather than
7961
+ matching it by hand.
7962
+
7963
+ **Construct ids are unique.** Two constructs sharing an id fails discovery for
7964
+ the whole project, not just the second one.`;
7965
+ }
7966
+ function testingSection(database) {
7967
+ return `## Testing
7968
+
7969
+ **Integration over unit.** Prefer the real dependency to a mock — a real
7970
+ database, a real cache. Mocks are for things you genuinely cannot run:
7971
+ the filesystem, the clock, external HTTP (use MSW for that).
7972
+
7973
+ **Test behaviour, not implementation.** Assert what the code did, not which
7974
+ functions it called on the way.
7975
+
7976
+ \`\`\`bash
7977
+ ${database ? "gkm test # starts services, injects secrets, then runs vitest" : "gkm test"}
7978
+ \`\`\`
7979
+
7980
+ Run tests through \`gkm test\`, not \`vitest\` directly${database ? " — it is what starts the database and injects the secrets the suite needs" : ""}.`;
7981
+ }
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
+
7345
8007
  //#endregion
7346
8008
  //#region src/init/versions.ts
7347
8009
  const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
@@ -7364,26 +8026,26 @@ const CLI_VERSION = `~${pkg.version}`;
7364
8026
  * Run: pnpm --filter @geekmidas/cli sync-versions
7365
8027
  */
7366
8028
  const GEEKMIDAS_VERSIONS = {
7367
- "@geekmidas/audit": "~10.0.0-alpha.0",
7368
- "@geekmidas/auth": "~10.0.0-alpha.0",
7369
- "@geekmidas/cache": "~10.0.0-alpha.0",
7370
- "@geekmidas/client": "~10.0.0-alpha.0",
7371
- "@geekmidas/cloud": "~10.0.0-alpha.0",
7372
- "@geekmidas/constructs": "~10.0.0-alpha.0",
7373
- "@geekmidas/db": "~10.0.0-alpha.0",
7374
- "@geekmidas/emailkit": "~10.0.0-alpha.0",
7375
- "@geekmidas/envkit": "~10.0.0-alpha.0",
7376
- "@geekmidas/errors": "~10.0.0-alpha.0",
7377
- "@geekmidas/events": "~10.0.0-alpha.0",
7378
- "@geekmidas/logger": "~10.0.0-alpha.0",
7379
- "@geekmidas/manifest": "~10.0.0-alpha.0",
7380
- "@geekmidas/rate-limit": "~10.0.0-alpha.0",
7381
- "@geekmidas/schema": "~10.0.0-alpha.0",
7382
- "@geekmidas/services": "~10.0.0-alpha.0",
7383
- "@geekmidas/storage": "~10.0.0-alpha.0",
7384
- "@geekmidas/studio": "~10.0.0-alpha.0",
7385
- "@geekmidas/telescope": "~10.0.0-alpha.0",
7386
- "@geekmidas/testkit": "~10.0.0-alpha.0",
8029
+ "@geekmidas/audit": "~10.0.0-alpha.10",
8030
+ "@geekmidas/auth": "~10.0.0-alpha.10",
8031
+ "@geekmidas/cache": "~10.0.0-alpha.10",
8032
+ "@geekmidas/client": "~10.0.0-alpha.10",
8033
+ "@geekmidas/cloud": "~10.0.0-alpha.10",
8034
+ "@geekmidas/constructs": "~10.0.0-alpha.10",
8035
+ "@geekmidas/db": "~10.0.0-alpha.10",
8036
+ "@geekmidas/emailkit": "~10.0.0-alpha.10",
8037
+ "@geekmidas/envkit": "~10.0.0-alpha.10",
8038
+ "@geekmidas/errors": "~10.0.0-alpha.10",
8039
+ "@geekmidas/events": "~10.0.0-alpha.10",
8040
+ "@geekmidas/logger": "~10.0.0-alpha.10",
8041
+ "@geekmidas/manifest": "~10.0.0-alpha.10",
8042
+ "@geekmidas/rate-limit": "~10.0.0-alpha.10",
8043
+ "@geekmidas/schema": "~10.0.0-alpha.10",
8044
+ "@geekmidas/services": "~10.0.0-alpha.10",
8045
+ "@geekmidas/storage": "~10.0.0-alpha.10",
8046
+ "@geekmidas/studio": "~10.0.0-alpha.10",
8047
+ "@geekmidas/telescope": "~10.0.0-alpha.10",
8048
+ "@geekmidas/testkit": "~10.0.0-alpha.10",
7387
8049
  "@geekmidas/cli": CLI_VERSION
7388
8050
  };
7389
8051
 
@@ -7403,27 +8065,28 @@ function generateAuthAppFiles(options) {
7403
8065
  private: true,
7404
8066
  type: "module",
7405
8067
  scripts: {
7406
- dev: "gkm dev --entry ./src/index.ts",
7407
- build: "tsc",
7408
- start: "node dist/index.js",
7409
- typecheck: "tsc --noEmit",
8068
+ dev: "gkm dev",
8069
+ build: "gkm build",
7410
8070
  "db:migrate": "gkm exec -- npx @better-auth/cli migrate",
7411
8071
  "db:generate": "gkm exec -- npx @better-auth/cli generate"
7412
8072
  },
7413
8073
  dependencies: {
7414
8074
  [modelsPackage]: "workspace:*",
8075
+ "@geekmidas/auth": GEEKMIDAS_VERSIONS["@geekmidas/auth"],
8076
+ "@geekmidas/constructs": GEEKMIDAS_VERSIONS["@geekmidas/constructs"],
8077
+ "@geekmidas/db": GEEKMIDAS_VERSIONS["@geekmidas/db"],
7415
8078
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
7416
8079
  "@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
7417
- "@hono/node-server": "~1.13.0",
7418
- "better-auth": "~1.2.0",
7419
- hono: "~4.8.0",
7420
- kysely: "~0.27.0",
7421
- 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
7422
8085
  },
7423
8086
  devDependencies: {
7424
8087
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
7425
8088
  "@types/node": "~22.0.0",
7426
- "@types/pg": "~8.11.0",
8089
+ "@types/pg": "~8.23.1",
7427
8090
  tsx: "~4.20.0",
7428
8091
  typescript: "~5.8.2"
7429
8092
  }
@@ -7464,117 +8127,27 @@ dist/
7464
8127
  }
7465
8128
 
7466
8129
  //#endregion
7467
- //#region src/init/constructs.ts
7468
- /** The glob every generated config points at. One glob, every kind. */
7469
- const CONSTRUCTS_GLOB = "./src/constructs/**/*.ts";
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
+ }
7470
8144
  /**
7471
- * The database a project named `name` declares.
7472
- *
7473
- * Named after the project rather than `Db`, so two apps in one workspace can
7474
- * declare one each without colliding on an id or on an env key.
8145
+ * The `STAGE` the scaffolded env config parses: one of the declared stages,
8146
+ * defaulting to the local one.
7475
8147
  */
7476
- function databaseFor(name$1) {
7477
- const id = (0, __geekmidas_manifest.canonicalId)(name$1);
7478
- return {
7479
- id,
7480
- service: (0, __geekmidas_manifest.serviceKey)(id),
7481
- urlKey: (0, __geekmidas_manifest.provideKey)(id, "url"),
7482
- ownerUrlKey: (0, __geekmidas_manifest.provideKey)(id, "ownerUrl")
7483
- };
7484
- }
7485
- /** The bucket a project named `name` declares. */
7486
- function storageFor(name$1) {
7487
- const id = (0, __geekmidas_manifest.canonicalId)(`${name$1}-uploads`);
7488
- return {
7489
- id,
7490
- service: (0, __geekmidas_manifest.serviceKey)(id),
7491
- urlKey: (0, __geekmidas_manifest.provideKey)(id, "url")
7492
- };
7493
- }
7494
- /** The mail sender a project named `name` declares. */
7495
- function emailFor(name$1) {
7496
- const id = (0, __geekmidas_manifest.canonicalId)(`${name$1}-mail`);
7497
- return {
7498
- id,
7499
- service: (0, __geekmidas_manifest.serviceKey)(id),
7500
- urlKey: (0, __geekmidas_manifest.provideKey)(id, "url")
7501
- };
7502
- }
7503
- /** The cache a project named `name` declares. */
7504
- function cacheFor(name$1) {
7505
- const id = (0, __geekmidas_manifest.canonicalId)(`${name$1}-cache`);
7506
- return {
7507
- id,
7508
- service: (0, __geekmidas_manifest.serviceKey)(id),
7509
- urlKey: (0, __geekmidas_manifest.provideKey)(id, "url")
7510
- };
7511
- }
7512
- /**
7513
- * The files a project's declared database needs.
7514
- *
7515
- * Shared by every template, so a worker and an API declare the same database
7516
- * the same way — and so the schema type, the migration, and the id the test
7517
- * setup reads all come from one place.
7518
- */
7519
- function databaseFiles(name$1) {
7520
- const db = databaseFor(name$1);
7521
- return [{
7522
- path: "src/constructs/database.ts",
7523
- content: `import { KyselyDatabase } from '@geekmidas/constructs/database/kysely';
7524
- import type { Generated } from 'kysely';
7525
-
7526
- /** Your database schema. Add tables here. */
7527
- export interface Database {
7528
- users: {
7529
- id: Generated<string>;
7530
- name: string;
7531
- email: string;
7532
- created_at: Generated<Date>;
7533
- };
7534
- }
7535
-
7536
- /**
7537
- * The app's database, declared once.
7538
- *
7539
- * The container, the database inside it, its roles and schema, and
7540
- * \`${db.urlKey}\` all derive from this line — \`gkm dev\` reconciles them
7541
- * before the server starts, which is why nothing lists \`postgres\` anywhere.
7542
- *
7543
- * Both type arguments or neither: TypeScript has no partial type-argument
7544
- * inference, so passing only \`Database\` would leave the name at \`string\`
7545
- * and widen the service key away from \`${db.service}\`.
7546
- */
7547
- export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}');
7548
- `
7549
- }, {
7550
- path: "src/db/migrations/001_create_users.ts",
7551
- content: `import type { Kysely } from 'kysely';
7552
-
7553
- export async function up(db: Kysely<unknown>): Promise<void> {
7554
- await db.schema
7555
- .createTable('users')
7556
- .addColumn('id', 'uuid', (col) =>
7557
- col.primaryKey().defaultTo(db.fn('gen_random_uuid')),
7558
- )
7559
- .addColumn('name', 'varchar(255)', (col) => col.notNull())
7560
- .addColumn('email', 'varchar(255)', (col) => col.notNull().unique())
7561
- .addColumn('created_at', 'timestamptz', (col) =>
7562
- col.notNull().defaultTo(db.fn('now')),
7563
- )
7564
- .execute();
7565
-
7566
- await db.schema
7567
- .createIndex('users_email_idx')
7568
- .on('users')
7569
- .column('email')
7570
- .execute();
7571
- }
7572
-
7573
- export async function down(db: Kysely<unknown>): Promise<void> {
7574
- await db.schema.dropTable('users').execute();
7575
- }
7576
- `
7577
- }];
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}')`;
7578
8151
  }
7579
8152
 
7580
8153
  //#endregion
@@ -7601,13 +8174,7 @@ function generateConfigFiles(options, template) {
7601
8174
  const isServerless = template.name === "serverless";
7602
8175
  const hasWorker = template.name === "worker";
7603
8176
  const isFullstack = options.template === "fullstack";
7604
- const getRoutesGlob = () => {
7605
- switch (routesStructure) {
7606
- case "centralized-endpoints": return "./src/endpoints/**/*.ts";
7607
- case "centralized-routes": return "./src/routes/**/*.ts";
7608
- case "domain-based": return "./src/**/routes/*.ts";
7609
- }
7610
- };
8177
+ const getRoutesGlob = () => routesGlob(routesStructure);
7611
8178
  if (isFullstack) return generateSingleAppConfigFiles(options, template, {
7612
8179
  telescope,
7613
8180
  studio,
@@ -7618,7 +8185,7 @@ function generateConfigFiles(options, template) {
7618
8185
  });
7619
8186
  let gkmConfig = `import { defineConfig } from '@geekmidas/cli/config';
7620
8187
 
7621
- export default defineConfig({${options.monorepo ? "" : `
8188
+ export default defineConfig({${stagesBlock(options.stages)}${options.monorepo ? "" : `
7622
8189
  // One glob, every kind. What reconcile reads to derive the containers this
7623
8190
  // app needs: a declared database is why a Postgres exists at all, so nothing
7624
8191
  // lists \`postgres\` anywhere.
@@ -7660,21 +8227,7 @@ export default defineConfig({${options.monorepo ? "" : `
7660
8227
  gkmConfig += `
7661
8228
  });
7662
8229
  `;
7663
- const tsConfig = options.monorepo ? {
7664
- extends: "../../tsconfig.json",
7665
- compilerOptions: {
7666
- noEmit: true,
7667
- allowImportingTsExtensions: true,
7668
- baseUrl: ".",
7669
- paths: {
7670
- "~/*": ["./src/*"],
7671
- [`@${options.name}/constructs/*`]: ["../../constructs/*"],
7672
- [`@${options.name}/*`]: ["../../packages/*/src"]
7673
- }
7674
- },
7675
- include: ["src/**/*.ts"],
7676
- exclude: ["node_modules", "dist"]
7677
- } : {
8230
+ const tsConfig = options.monorepo ? workspaceApiTsConfig(options.name) : {
7678
8231
  compilerOptions: {
7679
8232
  target: "ES2022",
7680
8233
  module: "NodeNext",
@@ -7686,7 +8239,9 @@ export default defineConfig({${options.monorepo ? "" : `
7686
8239
  forceConsistentCasingInFileNames: true,
7687
8240
  resolveJsonModule: true,
7688
8241
  noEmit: true,
7689
- allowImportingTsExtensions: true
8242
+ allowImportingTsExtensions: true,
8243
+ baseUrl: ".",
8244
+ paths: { "~/*": ["./src/*"] }
7690
8245
  },
7691
8246
  include: ["src/**/*.ts"],
7692
8247
  exclude: ["node_modules", "dist"]
@@ -7712,7 +8267,7 @@ export default defineConfig({${options.monorepo ? "" : `
7712
8267
  clientKind: "git",
7713
8268
  useIgnoreFile: true
7714
8269
  },
7715
- organizeImports: { enabled: true },
8270
+ assist: { actions: { source: { organizeImports: "on" } } },
7716
8271
  formatter: {
7717
8272
  enabled: true,
7718
8273
  indentStyle: "space",
@@ -7736,11 +8291,13 @@ export default defineConfig({${options.monorepo ? "" : `
7736
8291
  style: { noNonNullAssertion: "off" }
7737
8292
  }
7738
8293
  },
7739
- files: { ignore: [
7740
- "node_modules",
7741
- "dist",
7742
- ".gkm",
7743
- "coverage"
8294
+ css: { parser: { tailwindDirectives: true } },
8295
+ files: { includes: [
8296
+ "**",
8297
+ "!**/node_modules",
8298
+ "!**/dist",
8299
+ "!**/.gkm",
8300
+ "!**/coverage"
7744
8301
  ] }
7745
8302
  };
7746
8303
  const turboConfig = {
@@ -7794,8 +8351,16 @@ export default defineConfig({${options.monorepo ? "" : `
7794
8351
  });
7795
8352
  return files$1;
7796
8353
  }
7797
- function generateSingleAppConfigFiles(options, _template, _helpers) {
7798
- 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 {
7799
8364
  extends: "../../tsconfig.json",
7800
8365
  compilerOptions: {
7801
8366
  noEmit: true,
@@ -7803,12 +8368,16 @@ function generateSingleAppConfigFiles(options, _template, _helpers) {
7803
8368
  baseUrl: ".",
7804
8369
  paths: {
7805
8370
  "~/*": ["./src/*"],
7806
- [`@${options.name}/*`]: ["../../packages/*/src"]
8371
+ [`@${name$1}/constructs/*`]: ["../../constructs/*"],
8372
+ [`@${name$1}/*`]: ["../../packages/*/src"]
7807
8373
  }
7808
8374
  },
7809
8375
  include: ["src/**/*.ts"],
7810
8376
  exclude: ["node_modules", "dist"]
7811
8377
  };
8378
+ }
8379
+ function generateSingleAppConfigFiles(options, _template, _helpers) {
8380
+ const tsConfig = workspaceApiTsConfig(options.name);
7812
8381
  const files$1 = [{
7813
8382
  path: "tsconfig.json",
7814
8383
  content: `${JSON.stringify(tsConfig, null, 2)}\n`
@@ -7944,7 +8513,7 @@ function generateDockerFiles(options, template, dbApps) {
7944
8513
  MP_SMTP_AUTH: \${SMTP_USER:-${options.name}}:\${SMTP_PASS:-${options.name}}`);
7945
8514
  if (options.services?.storage) {
7946
8515
  services.push(` minio:
7947
- image: quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z
8516
+ image: pgsty/minio:RELEASE.2026-08-04T00-00-00Z
7948
8517
  container_name: ${options.name}-minio
7949
8518
  restart: unless-stopped
7950
8519
  entrypoint: sh
@@ -8185,6 +8754,327 @@ coverage/
8185
8754
  return files$1;
8186
8755
  }
8187
8756
 
8757
+ //#endregion
8758
+ //#region src/init/generators/github.ts
8759
+ /** How each package manager is set up, installs, and runs a script. */
8760
+ const TOOLING = {
8761
+ pnpm: {
8762
+ setup: ` - uses: pnpm/action-setup@v4
8763
+ PNPM_VERSION
8764
+ - uses: actions/setup-node@v4
8765
+ with:
8766
+ node-version: 22
8767
+ cache: pnpm
8768
+ `,
8769
+ install: "pnpm install --frozen-lockfile",
8770
+ run: "pnpm run",
8771
+ exec: "pnpm exec"
8772
+ },
8773
+ npm: {
8774
+ setup: ` - uses: actions/setup-node@v4
8775
+ with:
8776
+ node-version: 22
8777
+ cache: npm
8778
+ `,
8779
+ install: "npm ci",
8780
+ run: "npm run",
8781
+ exec: "npx --no-install"
8782
+ },
8783
+ yarn: {
8784
+ setup: ` - run: corepack enable
8785
+
8786
+ - uses: actions/setup-node@v4
8787
+ with:
8788
+ node-version: 22
8789
+ cache: yarn
8790
+ `,
8791
+ install: "yarn install --immutable",
8792
+ run: "yarn run",
8793
+ exec: "yarn"
8794
+ },
8795
+ bun: {
8796
+ setup: ` - uses: oven-sh/setup-bun@v2
8797
+ `,
8798
+ install: "bun install --frozen-lockfile",
8799
+ run: "bun run",
8800
+ exec: "bunx"
8801
+ }
8802
+ };
8803
+ /** The package manager's steps, with pnpm pinned where package.json does not. */
8804
+ function tooling(options) {
8805
+ const pm = TOOLING[options.packageManager];
8806
+ return {
8807
+ ...pm,
8808
+ setup: pm.setup.replace("PNPM_VERSION\n", options.monorepo ? "" : " with:\n version: 10\n")
8809
+ };
8810
+ }
8811
+ function ci(options) {
8812
+ const pm = tooling(options);
8813
+ return `name: CI
8814
+
8815
+ on:
8816
+ pull_request:
8817
+ branches: [main]
8818
+
8819
+ concurrency:
8820
+ group: \${{ github.workflow }}-\${{ github.ref }}
8821
+ cancel-in-progress: true
8822
+
8823
+ permissions:
8824
+ contents: read
8825
+
8826
+ jobs:
8827
+ validate:
8828
+ name: Validate
8829
+ runs-on: ubuntu-latest
8830
+ steps:
8831
+ - uses: actions/checkout@v4
8832
+
8833
+ ${pm.setup}
8834
+ - name: Install
8835
+ run: ${pm.install}
8836
+
8837
+ - name: Build
8838
+ run: ${pm.run} build
8839
+
8840
+ - name: Lint
8841
+ run: ${pm.run} lint
8842
+
8843
+ - name: Typecheck
8844
+ run: ${pm.run} typecheck
8845
+
8846
+ # \`gkm test\` starts the containers the declared constructs need (the
8847
+ # runner has Docker) and, with GKM_AUTO_SETUP, generates throwaway
8848
+ # secrets for the test stage — there is no key to share with CI.
8849
+ - name: Test
8850
+ timeout-minutes: 15
8851
+ run: ${pm.run} test:once
8852
+ env:
8853
+ CI: true
8854
+ GKM_AUTO_SETUP: 1
8855
+ `;
8856
+ }
8857
+ const releaseDrafterWorkflow = `name: Release Drafter
8858
+
8859
+ on:
8860
+ push:
8861
+ branches: [main]
8862
+ pull_request:
8863
+ types: [opened, reopened, synchronize, edited]
8864
+
8865
+ permissions:
8866
+ contents: read
8867
+
8868
+ jobs:
8869
+ draft:
8870
+ name: Update release draft
8871
+ runs-on: ubuntu-latest
8872
+ permissions:
8873
+ contents: write
8874
+ pull-requests: write
8875
+ steps:
8876
+ - uses: release-drafter/release-drafter@v6
8877
+ with:
8878
+ commitish: main
8879
+ env:
8880
+ GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
8881
+ `;
8882
+ function releaseDrafterConfig(options) {
8883
+ const mobile = options.frontendFramework === "expo" ? ` - title: "📱 Mobile"
8884
+ labels: [mobile, ios, android]
8885
+ collapse-after: 10
8886
+ ` : "";
8887
+ return `name-template: "v$RESOLVED_VERSION"
8888
+ tag-template: "v$RESOLVED_VERSION"
8889
+
8890
+ categories:
8891
+ - title: "✨ New Features"
8892
+ labels: [feature, enhancement]
8893
+ collapse-after: 10
8894
+ - title: "🐛 Bug Fixes"
8895
+ labels: [bug, fix]
8896
+ collapse-after: 10
8897
+ ${mobile} - title: "🏗 Infrastructure"
8898
+ labels: [infrastructure, ci]
8899
+ collapse-after: 10
8900
+ - title: "📦 Dependencies"
8901
+ labels: [dependencies]
8902
+ collapse-after: 5
8903
+ - title: "🔒 Security"
8904
+ labels: [security]
8905
+ - title: "🧹 Maintenance"
8906
+ labels: [chore, refactor]
8907
+ collapse-after: 5
8908
+
8909
+ change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
8910
+ change-title-escapes: '\\<*_&'
8911
+
8912
+ version-resolver:
8913
+ major:
8914
+ labels: [breaking-change, major]
8915
+ minor:
8916
+ labels: [feature, enhancement, minor]
8917
+ patch:
8918
+ labels: [bug, fix, chore, dependencies, patch]
8919
+ default: patch
8920
+
8921
+ # Labelled from the conventional-commit prefix of the pull request title.
8922
+ autolabeler:
8923
+ - label: feature
8924
+ title: ["/^feat/i"]
8925
+ - label: bug
8926
+ title: ["/^fix/i"]
8927
+ - label: chore
8928
+ title: ["/^chore/i", "/^refactor/i"]
8929
+ - label: dependencies
8930
+ title: ["/^deps/i", "/^chore\\\\(deps\\\\)/i"]
8931
+ - label: ci
8932
+ title: ["/^ci/i"]
8933
+ - label: breaking-change
8934
+ title: ["/^\\\\w+(\\\\(.+\\\\))?!:/"]
8935
+
8936
+ template: |
8937
+ ## What's Changed
8938
+
8939
+ $CHANGES
8940
+
8941
+ **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION
8942
+ `;
8943
+ }
8944
+ function deploy(options) {
8945
+ const pm = tooling(options);
8946
+ const sst = options.deployTarget === "sst";
8947
+ const credentials = sst ? `
8948
+ # An IAM role GitHub assumes through OIDC — no long-lived AWS keys. Set
8949
+ # AWS_ROLE_ARN as a variable on each environment.
8950
+ - uses: aws-actions/configure-aws-credentials@v4
8951
+ with:
8952
+ role-to-assume: \${{ vars.AWS_ROLE_ARN }}
8953
+ aws-region: ${options.region}
8954
+ ` : "";
8955
+ const deployEnv = sst ? "" : `
8956
+ DOKPLOY_API_TOKEN: \${{ secrets.DOKPLOY_API_TOKEN }}
8957
+ DOKPLOY_ENDPOINT: \${{ vars.DOKPLOY_ENDPOINT }}`;
8958
+ return `name: Deploy
8959
+
8960
+ # Which stages exist, and which are protected, is read from \`stages\` in
8961
+ # gkm.config.ts — nothing here names one:
8962
+ #
8963
+ # push to main every deployed stage that is not protected
8964
+ # release published the protected stages (publish the drafted release)
8965
+ # run workflow the one stage you name
8966
+ on:
8967
+ push:
8968
+ branches: [main]
8969
+ release:
8970
+ types: [published]
8971
+ workflow_dispatch:
8972
+ inputs:
8973
+ stage:
8974
+ description: A deployed stage from gkm.config.ts
8975
+ required: true
8976
+
8977
+ permissions:
8978
+ contents: read
8979
+ id-token: write
8980
+
8981
+ jobs:
8982
+ stages:
8983
+ name: Pick stages
8984
+ runs-on: ubuntu-latest
8985
+ outputs:
8986
+ stages: \${{ steps.pick.outputs.stages }}
8987
+ steps:
8988
+ - uses: actions/checkout@v4
8989
+
8990
+ ${pm.setup}
8991
+ - name: Install
8992
+ run: ${pm.install}
8993
+
8994
+ - name: Read stages from gkm.config.ts
8995
+ id: pick
8996
+ run: |
8997
+ ${pm.exec} tsx -e "
8998
+ import config from './gkm.config.ts';
8999
+ const { deployed, protected: kept = [] } = config.stages;
9000
+ const event = process.env.EVENT;
9001
+ if (event === 'workflow_dispatch' && !deployed.includes(process.env.STAGE)) {
9002
+ console.error('Not a deployed stage: ' + process.env.STAGE + ' (deployed: ' + deployed.join(', ') + ')');
9003
+ process.exit(1);
9004
+ }
9005
+ const stages =
9006
+ event === 'workflow_dispatch' ? [process.env.STAGE]
9007
+ : event === 'release' ? kept
9008
+ : deployed.filter((stage) => !kept.includes(stage));
9009
+ console.log('stages=' + JSON.stringify(stages));
9010
+ " >> "$GITHUB_OUTPUT"
9011
+ env:
9012
+ EVENT: \${{ github.event_name }}
9013
+ STAGE: \${{ inputs.stage }}
9014
+
9015
+ deploy:
9016
+ name: Deploy \${{ matrix.stage }}
9017
+ needs: stages
9018
+ if: needs.stages.outputs.stages != '[]'
9019
+ runs-on: ubuntu-latest
9020
+ strategy:
9021
+ fail-fast: false
9022
+ matrix:
9023
+ stage: \${{ fromJSON(needs.stages.outputs.stages) }}
9024
+ # One GitHub environment per stage: its secrets and variables, and any
9025
+ # approval rule you put on a protected one.
9026
+ environment: \${{ matrix.stage }}
9027
+ concurrency:
9028
+ group: deploy-\${{ matrix.stage }}
9029
+ cancel-in-progress: false
9030
+ steps:
9031
+ - uses: actions/checkout@v4
9032
+
9033
+ ${pm.setup}
9034
+ - name: Install
9035
+ run: ${pm.install}
9036
+
9037
+ # The stage's secrets are committed encrypted; the key never is. Each
9038
+ # environment holds its own as GKM_SECRETS_KEY.
9039
+ # Owner-only, as the CLI writes its own keys. The stage reaches the shell
9040
+ # as a variable, never pasted into the script: on a manual run it is
9041
+ # whatever was typed.
9042
+ - name: Stage secrets key
9043
+ run: |
9044
+ mkdir -p -m 700 ~/.gkm/${options.name}
9045
+ (umask 077 && printf '%s' "$KEY" > ~/.gkm/${options.name}/"$STAGE".key)
9046
+ env:
9047
+ KEY: \${{ secrets.GKM_SECRETS_KEY }}
9048
+ STAGE: \${{ matrix.stage }}
9049
+ ${credentials}
9050
+ - name: Deploy
9051
+ run: ${pm.run} "deploy:$STAGE"
9052
+ env:
9053
+ STAGE: \${{ matrix.stage }}${deployEnv}
9054
+ `;
9055
+ }
9056
+ function generateGithubFiles(options) {
9057
+ const files$1 = [
9058
+ {
9059
+ path: ".github/workflows/ci.yml",
9060
+ content: ci(options)
9061
+ },
9062
+ {
9063
+ path: ".github/workflows/release-drafter.yml",
9064
+ content: releaseDrafterWorkflow
9065
+ },
9066
+ {
9067
+ path: ".github/release-drafter.yml",
9068
+ content: releaseDrafterConfig(options)
9069
+ }
9070
+ ];
9071
+ if (options.monorepo && options.deployTarget !== "none") files$1.push({
9072
+ path: ".github/workflows/deploy.yml",
9073
+ content: deploy(options)
9074
+ });
9075
+ return files$1;
9076
+ }
9077
+
8188
9078
  //#endregion
8189
9079
  //#region src/init/generators/mobile-expo.ts
8190
9080
  /**
@@ -8217,12 +9107,12 @@ function generateExpoAppFiles(options) {
8217
9107
  dependencies: {
8218
9108
  [apiPackage]: "workspace:*",
8219
9109
  [modelsPackage]: "workspace:*",
8220
- "@better-auth/expo": "~1.2.0",
9110
+ "@better-auth/expo": DEPENDENCY_VERSIONS["better-auth"],
8221
9111
  "@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
8222
9112
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
8223
9113
  "@react-navigation/native": "^7.1.0",
8224
- "@tanstack/react-query": "~5.80.0",
8225
- "better-auth": "~1.2.0",
9114
+ "@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
9115
+ "better-auth": DEPENDENCY_VERSIONS["better-auth"],
8226
9116
  expo: "~55.0.0",
8227
9117
  "expo-constants": "~55.0.0",
8228
9118
  "expo-dev-client": "~55.0.0",
@@ -8358,7 +9248,7 @@ module.exports = withNativeWind(config, { input: './global.css' });
8358
9248
  "@/*": ["./*"],
8359
9249
  [modelsPackage]: ["../../packages/models/src"],
8360
9250
  [`${modelsPackage}/*`]: ["../../packages/models/src/*"],
8361
- [`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"]
9251
+ [`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi/api.ts"]
8362
9252
  }
8363
9253
  },
8364
9254
  include: [
@@ -8916,14 +9806,142 @@ export type ListUsersResponse = z.infer<typeof ListUsersResponseSchema>;
8916
9806
  ];
8917
9807
  }
8918
9808
 
9809
+ //#endregion
9810
+ //#region src/init/generators/deploy.ts
9811
+ /** One `deploy:<stage>` script per deployed stage. */
9812
+ const perStage = (options, command) => Object.fromEntries(options.stages.deployed.map((stage) => [`deploy:${stage}`, command(stage)]));
9813
+ function deployPackage(options) {
9814
+ switch (options.deployTarget) {
9815
+ case "dokploy": return {
9816
+ scripts: perStage(options, (stage) => `gkm deploy --provider dokploy --stage ${stage}`),
9817
+ dependencies: {},
9818
+ devDependencies: {}
9819
+ };
9820
+ case "sst": {
9821
+ const v = GEEKMIDAS_VERSIONS;
9822
+ return {
9823
+ scripts: perStage(options, (stage) => `gkm build --provider aws && sst deploy --stage ${stage}`),
9824
+ dependencies: {
9825
+ "@geekmidas/cloud": v["@geekmidas/cloud"],
9826
+ "@geekmidas/db": v["@geekmidas/db"],
9827
+ "@geekmidas/envkit": v["@geekmidas/envkit"],
9828
+ "@geekmidas/events": v["@geekmidas/events"],
9829
+ "@geekmidas/manifest": v["@geekmidas/manifest"],
9830
+ "@geekmidas/storage": v["@geekmidas/storage"],
9831
+ pg: DEPENDENCY_VERSIONS.pg
9832
+ },
9833
+ devDependencies: { sst: DEPENDENCY_VERSIONS.sst }
9834
+ };
9835
+ }
9836
+ default: return {
9837
+ scripts: {},
9838
+ dependencies: {},
9839
+ devDependencies: {}
9840
+ };
9841
+ }
9842
+ }
9843
+ function generateDeployFiles(options) {
9844
+ if (options.deployTarget !== "sst") return [];
9845
+ if (!options.region) throw new Error("An SST deploy needs a region.");
9846
+ const db = databaseFor();
9847
+ const mail = emailFor();
9848
+ const hasDatabase = options.template === "fullstack" || options.services.db;
9849
+ const inputs = [...hasDatabase ? [` // RDS needs a network. Created here rather than assumed, because a
9850
+ // VPC with NAT is a monthly cost in an account that may already
9851
+ // have one — replace it with \`sst.aws.Vpc.get(…)\` if so.
9852
+ ${db.id}: { vpc },`] : [], ...options.services.mail ? [` // Every provider rejects an unverified sender, so there is no
9853
+ // default; the synth stops with \`EmailNeedsSender\` until it is set.
9854
+ ${mail.id}: { from: process.env.MAIL_FROM as string },`] : []];
9855
+ return [{
9856
+ path: "sst.config.ts",
9857
+ content: `/// <reference path="./.sst/platform/config.d.ts" />
9858
+
9859
+ /**
9860
+ * ${options.name} on AWS.
9861
+ *
9862
+ * No bucket, cluster, queue or IAM is listed here: they come from the
9863
+ * constructs the application declares, through \`fromManifest\`. What is here
9864
+ * is what varies by stage, and the inputs a declaration cannot carry.
9865
+ *
9866
+ * Deploy with \`pnpm run deploy:<stage>\`, which builds the manifest first.
9867
+ */
9868
+ const region = '${options.region}';
9869
+
9870
+ /** From \`stages.protected\` in gkm.config.ts. */
9871
+ const PROTECTED: string[] = ${JSON.stringify(options.stages.protected ?? []).replace(/"/g, "'")};
9872
+
9873
+ export default $config({
9874
+ app(input) {
9875
+ return {
9876
+ name: '${options.name}',
9877
+ // Any other stage is disposable; a protected one's database should not
9878
+ // vanish because somebody removed a stack.
9879
+ removal: PROTECTED.includes(input?.stage) ? 'retain' : 'remove',
9880
+ protect: PROTECTED.includes(input?.stage),
9881
+ home: 'aws',
9882
+ providers: { aws: { region } },
9883
+ };
9884
+ },
9885
+
9886
+ async run() {
9887
+ const { App, fromManifest, Stack } = await import('@geekmidas/cloud/sst');
9888
+ // Written by \`gkm build --provider aws\`. Imported rather than discovered,
9889
+ // so SST's toolchain never evaluates the application's own modules.
9890
+ const { backends, constructs } = await import('./.gkm/manifest/aws.js');
9891
+
9892
+ ${hasDatabase ? " const vpc = new sst.aws.Vpc('Vpc', { nat: 'ec2' });\n\n" : ""} const app = new App({
9893
+ name: $app.name,
9894
+ stage: $app.stage,
9895
+ domain: '',
9896
+ region,
9897
+ hostedZoneId: '',
9898
+ });
9899
+
9900
+ return fromManifest(
9901
+ new Stack(app, '${(0, __geekmidas_manifest.canonicalId)(options.name)}'),
9902
+ constructs,
9903
+ {
9904
+ ${inputs.join("\n")}
9905
+ },
9906
+ backends,
9907
+ );
9908
+ },
9909
+ });
9910
+ `
9911
+ }];
9912
+ }
9913
+
8919
9914
  //#endregion
8920
9915
  //#region src/init/generators/monorepo.ts
8921
9916
  /**
9917
+ * What the root `constructs/` folder imports, and the peers each construct
9918
+ * needs, for the services this workspace was scaffolded with.
9919
+ */
9920
+ function rootConstructDependencies(options) {
9921
+ const v = GEEKMIDAS_VERSIONS;
9922
+ return {
9923
+ "@geekmidas/constructs": v["@geekmidas/constructs"],
9924
+ "@geekmidas/logger": v["@geekmidas/logger"],
9925
+ ...options.loggerType === "pino" ? { pino: DEPENDENCY_VERSIONS.pino } : {},
9926
+ "@geekmidas/telescope": v["@geekmidas/telescope"],
9927
+ "@geekmidas/auth": v["@geekmidas/auth"],
9928
+ "better-auth": DEPENDENCY_VERSIONS["better-auth"],
9929
+ "@geekmidas/db": v["@geekmidas/db"],
9930
+ kysely: DEPENDENCY_VERSIONS.kysely,
9931
+ pg: DEPENDENCY_VERSIONS.pg,
9932
+ ...options.services.storage ? { "@geekmidas/storage": v["@geekmidas/storage"] } : {},
9933
+ ...options.services.mail ? { "@geekmidas/emailkit": v["@geekmidas/emailkit"] } : {},
9934
+ ...options.services.cache ? { "@geekmidas/cache": v["@geekmidas/cache"] } : {}
9935
+ };
9936
+ }
9937
+ /**
8922
9938
  * Generate monorepo root files (pnpm-workspace.yaml, root package.json, etc.)
8923
9939
  */
8924
9940
  function generateMonorepoFiles(options, _template) {
8925
9941
  if (!options.monorepo) return [];
8926
9942
  const isFullstack = options.template === "fullstack";
9943
+ const deploy$1 = deployPackage(options);
9944
+ const isSst = options.deployTarget === "sst";
8927
9945
  const rootPackageJson = {
8928
9946
  name: options.name,
8929
9947
  version: "0.0.1",
@@ -8940,9 +9958,13 @@ function generateMonorepoFiles(options, _template) {
8940
9958
  fmt: "biome format . --write",
8941
9959
  "fmt:check": "biome format .",
8942
9960
  ...isFullstack ? { storybook: "pnpm --filter ./packages/ui storybook" } : {},
8943
- ...options.deployTarget === "dokploy" ? { deploy: "gkm deploy --provider dokploy --stage production" } : {}
9961
+ ...deploy$1.scripts
9962
+ },
9963
+ dependencies: {
9964
+ zod: DEPENDENCY_VERSIONS.zod,
9965
+ ...isFullstack ? rootConstructDependencies(options) : {},
9966
+ ...deploy$1.dependencies
8944
9967
  },
8945
- dependencies: { zod: "~4.1.0" },
8946
9968
  devDependencies: {
8947
9969
  "@biomejs/biome": "~2.3.0",
8948
9970
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
@@ -8950,7 +9972,8 @@ function generateMonorepoFiles(options, _template) {
8950
9972
  tsx: "~4.20.0",
8951
9973
  turbo: "~2.3.0",
8952
9974
  typescript: "~5.8.2",
8953
- vitest: "~4.0.0"
9975
+ vitest: "~4.0.0",
9976
+ ...deploy$1.devDependencies
8954
9977
  }
8955
9978
  };
8956
9979
  const apiPathParts = options.apiPath.split("/");
@@ -8966,7 +9989,7 @@ function generateMonorepoFiles(options, _template) {
8966
9989
  clientKind: "git",
8967
9990
  useIgnoreFile: true
8968
9991
  },
8969
- organizeImports: { enabled: true },
9992
+ assist: { actions: { source: { organizeImports: "on" } } },
8970
9993
  formatter: {
8971
9994
  enabled: true,
8972
9995
  indentStyle: "space",
@@ -8990,11 +10013,14 @@ function generateMonorepoFiles(options, _template) {
8990
10013
  style: { noNonNullAssertion: "off" }
8991
10014
  }
8992
10015
  },
8993
- files: { ignore: [
8994
- "node_modules",
8995
- "dist",
8996
- ".gkm",
8997
- "coverage"
10016
+ css: { parser: { tailwindDirectives: true } },
10017
+ files: { includes: [
10018
+ "**",
10019
+ "!**/node_modules",
10020
+ "!**/dist",
10021
+ "!**/.gkm",
10022
+ "!**/coverage",
10023
+ ...isSst ? ["!**/.sst", "!sst-env.d.ts"] : []
8998
10024
  ] }
8999
10025
  };
9000
10026
  const turboConfig = {
@@ -9060,12 +10086,14 @@ coverage/
9060
10086
 
9061
10087
  # Turbo
9062
10088
  .turbo/
9063
- `;
10089
+ ${isSst ? "\n# SST\n.sst/\n" : ""}`;
9064
10090
  const tsConfig = {
9065
10091
  compilerOptions: {
9066
10092
  ...isFullstack ? {
9067
10093
  baseUrl: ".",
9068
- paths: { [`@${options.name}/constructs/*`]: ["./constructs/*"] }
10094
+ paths: { [`@${options.name}/constructs/*`]: ["./constructs/*"] },
10095
+ allowImportingTsExtensions: true,
10096
+ noEmit: true
9069
10097
  } : {},
9070
10098
  target: "ES2022",
9071
10099
  module: "NodeNext",
@@ -9077,16 +10105,21 @@ coverage/
9077
10105
  forceConsistentCasingInFileNames: true,
9078
10106
  resolveJsonModule: true
9079
10107
  },
9080
- exclude: ["node_modules", "dist"]
10108
+ exclude: [
10109
+ "node_modules",
10110
+ "dist",
10111
+ ...isSst ? ["sst.config.ts", ".sst"] : []
10112
+ ]
9081
10113
  };
9082
10114
  const vitestConfig = `import { defineConfig } from 'vitest/config';
9083
10115
 
9084
10116
  export default defineConfig({
9085
10117
  test: {
9086
- globals: true,
9087
- environment: 'node',
9088
- include: ['apps/**/*.{test,spec}.ts', 'packages/**/*.{test,spec}.ts'],
9089
- exclude: ['**/node_modules/**', '**/dist/**'],
10118
+ // Each app and package is its own project, so its own config — an app's
10119
+ // \`globalSetup\`, its path aliases — applies to its own tests.
10120
+ projects: ['apps/*', 'packages/*'],
10121
+ // The shared packages ship without tests of their own.
10122
+ passWithNoTests: true,
9090
10123
  coverage: {
9091
10124
  provider: 'v8',
9092
10125
  reporter: ['text', 'json', 'html'],
@@ -9167,7 +10200,8 @@ export default defineConfig({
9167
10200
  {
9168
10201
  path: ".vscode/extensions.json",
9169
10202
  content: `${JSON.stringify(vscodeExtensions, null, " ")}\n`
9170
- }
10203
+ },
10204
+ ...generateDeployFiles(options)
9171
10205
  ];
9172
10206
  if (isFullstack) files$1.push({
9173
10207
  path: "gkm.config.ts",
@@ -9194,12 +10228,17 @@ export default defineWorkspace({
9194
10228
  // The scope every physical name is built from: \`Database\` becomes
9195
10229
  // \`production-${options.name}-database\` on Dokploy and on AWS alike.
9196
10230
  name: '${options.name}',
10231
+ ${stagesBlock(options.stages)}
9197
10232
 
9198
- // One glob, every kind. A declared database is why a Postgres exists, a
10233
+ // Every kind, in every app. A declared database is why a Postgres exists, a
9199
10234
  // declared bucket is why MinIO does, a declared topic is why a broker does —
9200
10235
  // none of it listed here. It is also where the apps come from: a
9201
- // \`StaticSite\` is an app, and so is every \`RestApi\`.
9202
- constructs: './constructs/**/*.ts',
10236
+ // \`StaticSite\` is an app, and so is every \`RestApi\`. And it finds each
10237
+ // app's endpoints, crons and subscribers: an endpoint belongs to the surface
10238
+ // it was built from, not to the directory it sits in.
10239
+ constructs: [
10240
+ ${workspaceConstructsGlobs(options.routesStructure, (0, node_path.dirname)(options.apiPath)).map((glob) => ` '${glob}',`).join("\n")}
10241
+ ],
9203
10242
 
9204
10243
  secrets: {
9205
10244
  enabled: true,
@@ -9228,7 +10267,7 @@ export default defineWorkspace({
9228
10267
  function generateRootConstructs(options) {
9229
10268
  if (!options.monorepo || options.template !== "fullstack") return [];
9230
10269
  const { name: name$1, services, frontendFramework } = options;
9231
- const db = databaseFor(name$1);
10270
+ const db = databaseFor();
9232
10271
  const files$1 = [];
9233
10272
  files$1.push({
9234
10273
  path: "constructs/logger.ts",
@@ -9294,6 +10333,7 @@ import { authDb } from './database.ts';
9294
10333
  * origin down.
9295
10334
  */
9296
10335
  export const auth = new BetterAuth('Auth', {
10336
+ path: 'apps/auth',
9297
10337
  database: authDb,
9298
10338
  basePath: '/api/auth',
9299
10339
  });
@@ -9308,25 +10348,22 @@ import { logger } from './logger.ts';
9308
10348
  /**
9309
10349
  * The application's HTTP surface — one RestApi, one container.
9310
10350
  *
9311
- * No \`app\`: \`Api\` means \`apps/api\`, which the id already said. The
9312
- * \`code\` glob is here only because this scaffold puts its endpoints under
9313
- * \`src/\`, where the conventional layout has them at the app root.
10351
+ * \`path\` is where its app lives. Its handlers are found by the workspace's
10352
+ * \`constructs\` glob, and belong to it because they were built from it.
9314
10353
  */
9315
10354
  export const api = new RestApi('Api', {
10355
+ path: '${options.apiPath}',
10356
+
9316
10357
  // Typed out rather than omitted: an API that ships open because a field was
9317
10358
  // left off is the one default worth refusing to have.
9318
10359
  defaultAuthorizer: 'none',
9319
10360
 
9320
10361
  // The actual logger, not a path to one.
9321
10362
  logger,
9322
-
9323
- app: {
9324
- code: './src/{endpoints,functions,crons,queues,topics,subscribers}/**/*.ts',
9325
- },
9326
10363
  }).auth(auth);
9327
10364
  `
9328
10365
  });
9329
- const variant = frontendFramework === "tanstack-start" ? "{ variant: 'tanstack' }" : frontendFramework === "expo" ? void 0 : "{ variant: 'next' }";
10366
+ const variant = frontendFramework === "tanstack-start" ? "{ path: 'apps/web', variant: 'tanstack' }" : frontendFramework === "expo" ? void 0 : "{ path: 'apps/web', variant: 'next' }";
9330
10367
  if (variant !== void 0) files$1.push({
9331
10368
  path: "constructs/site.ts",
9332
10369
  content: `import { StaticSite } from '@geekmidas/constructs/site';
@@ -9336,7 +10373,7 @@ import { auth } from './auth.ts';
9336
10373
  /**
9337
10374
  * The frontend — a construct like any other, which is what makes it an app.
9338
10375
  *
9339
- * No \`path\`: \`Web\` means \`apps/web\`. \`.dependsOn()\` is the single fact
10376
+ * \`path\` is where it lives. \`.dependsOn()\` is the single fact
9340
10377
  * behind four things that are hand-maintained otherwise: this site's
9341
10378
  * build-time API URL, the API's CORS origins, the auth server's trusted
9342
10379
  * origins, and which generated client lands here.
@@ -9345,7 +10382,7 @@ export const web = new StaticSite('Web', ${variant}).dependsOn([api, auth]);
9345
10382
  `
9346
10383
  });
9347
10384
  if (services.storage) {
9348
- const bucket = storageFor(name$1);
10385
+ const bucket = storageFor();
9349
10386
  files$1.push({
9350
10387
  path: "constructs/storage.ts",
9351
10388
  content: `import { ObjectStorage } from '@geekmidas/constructs/object-storage';
@@ -9356,7 +10393,7 @@ export const uploads = new ObjectStorage('${bucket.id}');
9356
10393
  });
9357
10394
  }
9358
10395
  if (services.mail) {
9359
- const mail = emailFor(name$1);
10396
+ const mail = emailFor();
9360
10397
  files$1.push({
9361
10398
  path: "constructs/email.ts",
9362
10399
  content: `import { Email } from '@geekmidas/constructs/email';
@@ -9367,7 +10404,7 @@ export const email = new Email('${mail.id}', { templates: {} });
9367
10404
  });
9368
10405
  }
9369
10406
  if (services.cache) {
9370
- const kv = cacheFor(name$1);
10407
+ const kv = cacheFor();
9371
10408
  files$1.push({
9372
10409
  path: "constructs/cache.ts",
9373
10410
  content: `import { Cache } from '@geekmidas/constructs/cache';
@@ -9400,9 +10437,9 @@ const apiTemplate = {
9400
10437
  "@geekmidas/errors": GEEKMIDAS_VERSIONS["@geekmidas/errors"],
9401
10438
  "@geekmidas/auth": GEEKMIDAS_VERSIONS["@geekmidas/auth"],
9402
10439
  "@hono/node-server": "~1.14.1",
9403
- hono: "~4.8.2",
9404
- pino: "~9.6.0",
9405
- zod: "~4.1.0"
10440
+ hono: DEPENDENCY_VERSIONS.hono,
10441
+ pino: DEPENDENCY_VERSIONS.pino,
10442
+ zod: DEPENDENCY_VERSIONS.zod
9406
10443
  },
9407
10444
  devDependencies: {
9408
10445
  "@biomejs/biome": "~2.3.0",
@@ -9426,10 +10463,10 @@ const apiTemplate = {
9426
10463
  },
9427
10464
  files: (options) => {
9428
10465
  const { loggerType, routesStructure, monorepo, name: name$1, services } = options;
9429
- const bucket = storageFor(name$1);
9430
- const kv = cacheFor(name$1);
9431
- const db = databaseFor(name$1);
9432
- const mail = emailFor(name$1);
10466
+ const bucket = storageFor();
10467
+ const kv = cacheFor();
10468
+ const db = databaseFor();
10469
+ const mail = emailFor();
9433
10470
  const src = (path) => monorepo ? `~/${path}` : `./${path}`;
9434
10471
  const declares = !monorepo;
9435
10472
  const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
@@ -9463,6 +10500,9 @@ export const logger = createLogger();
9463
10500
  import { logger } from '../config/logger.ts';
9464
10501
 
9465
10502
  export const api = new RestApi('Api', {
10503
+ // The project is the app.
10504
+ path: '.',
10505
+
9466
10506
  // Typed out rather than omitted: an API that ships open because a field was
9467
10507
  // left off is the one default worth refusing to have.
9468
10508
  defaultAuthorizer: 'none',
@@ -9485,7 +10525,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
9485
10525
  export const config = envParser
9486
10526
  .create((get) => ({
9487
10527
  nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
9488
- stage: get('STAGE').enum(['development', 'staging', 'production']).default('development'),
10528
+ stage: ${stageEnv(options.stages)},
9489
10529
  }))
9490
10530
  .parse();
9491
10531
  `
@@ -9637,7 +10677,7 @@ export const authService = {
9637
10677
  headers: { cookie },
9638
10678
  });
9639
10679
  if (!res.ok) return null;
9640
- return res.json();
10680
+ return (await res.json()) as Session | null;
9641
10681
  },
9642
10682
  };
9643
10683
  },
@@ -9650,7 +10690,6 @@ export const authService = {
9650
10690
  import { api } from '${constructsImport("api")}';${options.database ? `
9651
10691
  import { database } from '${constructsImport("database")}';` : ""}
9652
10692
  import { authService, type Session } from './services/auth.ts';
9653
- import { logger } from './config/logger.ts';
9654
10693
 
9655
10694
  /**
9656
10695
  * The shared endpoint factory — no session required.
@@ -9708,7 +10747,8 @@ import { database } from '${constructsImport("database")}';` : ""}
9708
10747
  export const router = api.endpoints${options.database ? ".database(database)" : ""};
9709
10748
  `
9710
10749
  });
9711
- if (options.database && declares) files$1.push(...databaseFiles(name$1));
10750
+ if (options.database && declares) files$1.push(...databaseFiles());
10751
+ else if (options.database) files$1.push(usersMigration());
9712
10752
  if (services.storage && declares) files$1.push({
9713
10753
  path: "src/constructs/storage.ts",
9714
10754
  content: `import { ObjectStorage } from '@geekmidas/constructs/object-storage';
@@ -9858,9 +10898,9 @@ const minimalTemplate = {
9858
10898
  "@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
9859
10899
  "@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
9860
10900
  "@hono/node-server": "~1.14.1",
9861
- hono: "~4.8.2",
9862
- pino: "~9.6.0",
9863
- zod: "~4.1.0"
10901
+ hono: DEPENDENCY_VERSIONS.hono,
10902
+ pino: DEPENDENCY_VERSIONS.pino,
10903
+ zod: DEPENDENCY_VERSIONS.zod
9864
10904
  },
9865
10905
  devDependencies: {
9866
10906
  "@biomejs/biome": "~2.3.0",
@@ -9884,7 +10924,7 @@ const minimalTemplate = {
9884
10924
  },
9885
10925
  files: (options) => {
9886
10926
  const { loggerType, routesStructure, name: name$1 } = options;
9887
- const db = databaseFor(name$1);
10927
+ const db = databaseFor();
9888
10928
  const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
9889
10929
 
9890
10930
  export const logger = createLogger();
@@ -9904,6 +10944,9 @@ export const logger = createLogger();
9904
10944
  import { logger } from '../config/logger.ts';
9905
10945
 
9906
10946
  export const api = new RestApi('Api', {
10947
+ // The project is the app.
10948
+ path: '.',
10949
+
9907
10950
  // Typed out rather than omitted: an API that ships open because a field was
9908
10951
  // left off is the one default worth refusing to have.
9909
10952
  defaultAuthorizer: 'none',
@@ -9926,7 +10969,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
9926
10969
  export const config = envParser
9927
10970
  .create((get) => ({
9928
10971
  nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
9929
- stage: get('STAGE').enum(['development', 'staging', 'production']).default('development'),
10972
+ stage: ${stageEnv(options.stages)},
9930
10973
  }))
9931
10974
  .parse();
9932
10975
  `
@@ -9953,7 +10996,7 @@ export const healthEndpoint = api
9953
10996
  `
9954
10997
  }
9955
10998
  ];
9956
- if (options.database) files$1.push(...databaseFiles(name$1));
10999
+ if (options.database) files$1.push(...databaseFiles());
9957
11000
  if (options.telescope) files$1.push({
9958
11001
  path: "src/config/telescope.ts",
9959
11002
  content: `import { Telescope } from '@geekmidas/telescope';
@@ -10017,8 +11060,8 @@ const serverlessTemplate = {
10017
11060
  "@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
10018
11061
  "@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
10019
11062
  "@hono/node-server": "~1.14.1",
10020
- hono: "~4.8.2",
10021
- pino: "~9.6.0"
11063
+ hono: DEPENDENCY_VERSIONS.hono,
11064
+ pino: DEPENDENCY_VERSIONS.pino
10022
11065
  },
10023
11066
  devDependencies: {
10024
11067
  "@biomejs/biome": "~2.3.0",
@@ -10061,6 +11104,9 @@ export const logger = createLogger();
10061
11104
  import { logger } from '../config/logger.ts';
10062
11105
 
10063
11106
  export const api = new RestApi('Api', {
11107
+ // The project is the app.
11108
+ path: '.',
11109
+
10064
11110
  // Typed out rather than omitted: an API that ships open because a field was
10065
11111
  // left off is the one default worth refusing to have.
10066
11112
  defaultAuthorizer: 'none',
@@ -10083,7 +11129,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
10083
11129
  export const config = envParser
10084
11130
  .create((get) => ({
10085
11131
  nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
10086
- stage: get('STAGE').enum(['dev', 'staging', 'prod']).default('dev'),
11132
+ stage: ${stageEnv(options.stages)},
10087
11133
  }))
10088
11134
  .parse();
10089
11135
  `
@@ -10113,19 +11159,27 @@ export const healthEndpoint = api
10113
11159
  },
10114
11160
  {
10115
11161
  path: "src/functions/hello.ts",
10116
- content: `import { f } from '@geekmidas/constructs/functions';
10117
- import { z } from 'zod';
11162
+ content: `import { z } from 'zod';
11163
+ import { worker } from '~/constructs/worker.ts';
10118
11164
 
10119
- export const helloFunction = f
11165
+ export const helloFunction = worker
10120
11166
  .input(z.object({ name: z.string() }))
10121
11167
  .output(z.object({ message: z.string() }))
10122
11168
  .handle(async ({ input }) => ({
10123
11169
  message: \`Hello, \${input.name}!\`,
10124
11170
  }));
11171
+ `
11172
+ },
11173
+ {
11174
+ path: "src/constructs/worker.ts",
11175
+ content: `import { Worker } from '@geekmidas/constructs/worker';
11176
+ import { logger } from '../config/logger.ts';
11177
+
11178
+ export const worker = new Worker('Jobs', { logger });
10125
11179
  `
10126
11180
  }
10127
11181
  ];
10128
- if (options.database) files$1.push(...databaseFiles(name$1));
11182
+ if (options.database) files$1.push(...databaseFiles());
10129
11183
  if (options.telescope) files$1.push({
10130
11184
  path: "src/config/telescope.ts",
10131
11185
  content: `import { Telescope } from '@geekmidas/telescope';
@@ -10134,7 +11188,7 @@ import { InMemoryStorage } from '@geekmidas/telescope/storage/memory';
10134
11188
  // Note: For production Lambda, consider using a persistent storage
10135
11189
  export const telescope = new Telescope({
10136
11190
  storage: new InMemoryStorage({ maxEntries: 50 }),
10137
- enabled: process.env.STAGE === 'dev',
11191
+ enabled: process.env.STAGE === '${options.stages.local}',
10138
11192
  });
10139
11193
  `
10140
11194
  });
@@ -10144,6 +11198,22 @@ export const telescope = new Telescope({
10144
11198
 
10145
11199
  //#endregion
10146
11200
  //#region src/init/templates/worker.ts
11201
+ /**
11202
+ * A project that is background work and nothing else.
11203
+ *
11204
+ * It used to scaffold a `RestApi` with no endpoints on it — an HTTP surface
11205
+ * declared by a project whose premise is that nothing calls it over HTTP —
11206
+ * because a surface was the only construct that made an app exist and the only
11207
+ * way to get a logger into the generated runtime. `Worker` is the construct
11208
+ * that was missing: it takes no authorizer, publishes no address, and hands out
11209
+ * factories already carrying its logger.
11210
+ *
11211
+ * It scaffolds a cron when the project has a database, and not otherwise. A
11212
+ * server fires its own crons, and the schedule lives in Postgres so that
11213
+ * running more than one replica still fires each job once — so a worker names
11214
+ * the database it keeps schedules in. Without one there is nowhere to keep
11215
+ * them, and an example that could not run is worse than none.
11216
+ */
10147
11217
  const workerTemplate = {
10148
11218
  name: "worker",
10149
11219
  description: "Background job processing",
@@ -10153,11 +11223,8 @@ const workerTemplate = {
10153
11223
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
10154
11224
  "@geekmidas/events": GEEKMIDAS_VERSIONS["@geekmidas/events"],
10155
11225
  "@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
10156
- "@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
10157
11226
  "@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
10158
- "@hono/node-server": "~1.14.1",
10159
- hono: "~4.8.2",
10160
- pino: "~9.6.0"
11227
+ pino: DEPENDENCY_VERSIONS.pino
10161
11228
  },
10162
11229
  devDependencies: {
10163
11230
  "@biomejs/biome": "~2.3.0",
@@ -10179,34 +11246,18 @@ const workerTemplate = {
10179
11246
  "fmt:check": "biome format ."
10180
11247
  },
10181
11248
  files: (options) => {
10182
- const { loggerType, routesStructure, name: name$1 } = options;
11249
+ const { loggerType, name: name$1, database } = options;
10183
11250
  const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
10184
11251
 
10185
11252
  export const logger = createLogger();
10186
11253
  `;
10187
- const getRoutePath = (file) => {
10188
- switch (routesStructure) {
10189
- case "centralized-endpoints": return `src/endpoints/${file}`;
10190
- case "centralized-routes": return `src/routes/${file}`;
10191
- case "domain-based": return `src/${file.replace(".ts", "")}/routes/index.ts`;
10192
- }
10193
- };
10194
- const apiImport = routesStructure === "domain-based" ? "../../constructs/api.js" : "../constructs/api.js";
10195
11254
  const files$1 = [
10196
11255
  {
10197
- path: "src/constructs/api.ts",
10198
- content: `import { RestApi } from '@geekmidas/constructs/rest-api';
10199
- import { logger } from '../config/logger.ts';
11256
+ path: "src/constructs/worker.ts",
11257
+ content: `import { Worker } from '@geekmidas/constructs/worker';
11258
+ import { logger } from '../config/logger.ts';${database ? "\nimport { database } from './database.ts';" : ""}
10200
11259
 
10201
- export const api = new RestApi('Api', {
10202
- // Typed out rather than omitted: an API that ships open because a field was
10203
- // left off is the one default worth refusing to have.
10204
- defaultAuthorizer: 'none',
10205
-
10206
- // The actual logger, not a path to one. Every endpoint built from this
10207
- // surface runs with it, and so does the generated entry.
10208
- logger,
10209
- });
11260
+ export const worker = new Worker('Jobs', { logger })${database ? ".database(database)" : ""};
10210
11261
  `
10211
11262
  },
10212
11263
  {
@@ -10221,7 +11272,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
10221
11272
  export const config = envParser
10222
11273
  .create((get) => ({
10223
11274
  nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
10224
- stage: get('STAGE').enum(['development', 'staging', 'production']).default('development'),
11275
+ stage: ${stageEnv(options.stages)},
10225
11276
  }))
10226
11277
  .parse();
10227
11278
  `
@@ -10230,23 +11281,6 @@ export const config = envParser
10230
11281
  path: "src/config/logger.ts",
10231
11282
  content: loggerContent
10232
11283
  },
10233
- {
10234
- path: getRoutePath("health.ts"),
10235
- content: `import { z } from 'zod';
10236
- import { api } from '${apiImport}';
10237
-
10238
- export const healthEndpoint = api
10239
- .get('/health')
10240
- .output(z.object({
10241
- status: z.string(),
10242
- timestamp: z.string(),
10243
- }))
10244
- .handle(async () => ({
10245
- status: 'ok',
10246
- timestamp: new Date().toISOString(),
10247
- }));
10248
- `
10249
- },
10250
11284
  {
10251
11285
  path: "src/events/types.ts",
10252
11286
  content: `import type { PublishableMessage } from '@geekmidas/events';
@@ -10288,46 +11322,47 @@ export const eventsPublisherService = {
10288
11322
  },
10289
11323
  {
10290
11324
  path: "src/subscribers/user-events.ts",
10291
- content: `import { s } from '@geekmidas/constructs/subscribers';
11325
+ content: `import { worker } from '~/constructs/worker.ts';
10292
11326
  import { eventsPublisherService } from '~/events/publisher.ts';
10293
11327
 
10294
- export const userEventsSubscriber = s
11328
+ export const userEventsSubscriber = worker
10295
11329
  .publisher(eventsPublisherService)
10296
11330
  .subscribe(['user.created', 'user.updated'])
10297
- .handle(async ({ event, logger }) => {
10298
- logger.info({ type: event.type, payload: event.payload }, 'Processing user event');
10299
-
10300
- switch (event.type) {
10301
- case 'user.created':
10302
- // Handle user creation
10303
- logger.info({ userId: event.payload.userId }, 'New user created');
10304
- break;
10305
- case 'user.updated':
10306
- // Handle user update
10307
- logger.info({ userId: event.payload.userId }, 'User updated');
10308
- break;
11331
+ .handle(async ({ events, logger }) => {
11332
+ // A batch, not a single event. Both transports deliver in batches, so
11333
+ // handling them one at a time is a round trip per event for no reason.
11334
+ logger.info({ count: events.length }, 'Processing user events');
11335
+
11336
+ for (const event of events) {
11337
+ switch (event.type) {
11338
+ case 'user.created':
11339
+ logger.info({ userId: event.payload.userId }, 'New user created');
11340
+ break;
11341
+ case 'user.updated':
11342
+ logger.info({ userId: event.payload.userId }, 'User updated');
11343
+ break;
11344
+ }
10309
11345
  }
10310
11346
  });
10311
11347
  `
10312
- },
10313
- {
10314
- path: "src/crons/cleanup.ts",
10315
- content: `import { cron } from '@geekmidas/constructs/crons';
11348
+ }
11349
+ ];
11350
+ if (database) files$1.push({
11351
+ path: "src/crons/cleanup.ts",
11352
+ content: `import { worker } from '~/constructs/worker.ts';
10316
11353
 
10317
- // Run every day at midnight
10318
- export const cleanupCron = cron('0 0 * * *')
11354
+ export const cleanup = worker
11355
+ .cron('rate(1 day)')
10319
11356
  .handle(async ({ logger }) => {
10320
- logger.info('Running cleanup job');
11357
+ logger.info('Running cleanup');
10321
11358
 
10322
- // Add your cleanup logic here
10323
- // e.g., delete old sessions, clean up temp files, etc.
11359
+ // Delete old sessions, clear temp files, whatever falls out of date.
10324
11360
 
10325
- logger.info('Cleanup job completed');
11361
+ logger.info('Cleanup complete');
10326
11362
  });
10327
11363
  `
10328
- }
10329
- ];
10330
- if (options.database) files$1.push(...databaseFiles(name$1));
11364
+ });
11365
+ if (options.database) files$1.push(...databaseFiles());
10331
11366
  if (options.telescope) files$1.push({
10332
11367
  path: "src/config/telescope.ts",
10333
11368
  content: `import { Telescope } from '@geekmidas/telescope';
@@ -10348,7 +11383,7 @@ export const telescope = new Telescope({
10348
11383
  /**
10349
11384
  * OpenAPI output path (fixed, not configurable)
10350
11385
  */
10351
- const OPENAPI_OUTPUT_PATH$1 = "./.gkm/openapi.ts";
11386
+ const OPENAPI_OUTPUT_PATH$1 = "./.gkm/openapi/api.ts";
10352
11387
  /**
10353
11388
  * Endpoints output path (generated by gkm dev/build)
10354
11389
  */
@@ -10454,15 +11489,23 @@ const packageManagerChoices = [
10454
11489
  /**
10455
11490
  * Deploy target choices for prompts
10456
11491
  */
10457
- const deployTargetChoices = [{
10458
- title: "Dokploy",
10459
- value: "dokploy",
10460
- description: "Deploy to Dokploy (Docker-based hosting)"
10461
- }, {
10462
- title: "Configure later",
10463
- value: "none",
10464
- description: "Skip deployment setup for now"
10465
- }];
11492
+ const deployTargetChoices = [
11493
+ {
11494
+ title: "Dokploy",
11495
+ value: "dokploy",
11496
+ description: "Deploy to Dokploy (Docker-based hosting)"
11497
+ },
11498
+ {
11499
+ title: "AWS (SST)",
11500
+ value: "sst",
11501
+ description: "Deploy to your AWS account through SST"
11502
+ },
11503
+ {
11504
+ title: "Configure later",
11505
+ value: "none",
11506
+ description: "Skip deployment setup for now"
11507
+ }
11508
+ ];
10466
11509
  /**
10467
11510
  * Services choices for multi-select prompt
10468
11511
  */
@@ -10555,12 +11598,12 @@ function generatePackageJson(options, template) {
10555
11598
  if (declares && services.mail) dependencies$1["@geekmidas/emailkit"] = GEEKMIDAS_VERSIONS["@geekmidas/emailkit"];
10556
11599
  if (database) {
10557
11600
  dependencies$1["@geekmidas/db"] = GEEKMIDAS_VERSIONS["@geekmidas/db"];
10558
- dependencies$1.kysely = "~0.28.2";
10559
- dependencies$1.pg = "~8.16.0";
10560
- devDependencies$1["@types/pg"] = "~8.15.0";
11601
+ dependencies$1.kysely = DEPENDENCY_VERSIONS.kysely;
11602
+ dependencies$1.pg = DEPENDENCY_VERSIONS.pg;
11603
+ devDependencies$1["@types/pg"] = "~8.23.1";
10561
11604
  devDependencies$1["@geekmidas/testkit"] = GEEKMIDAS_VERSIONS["@geekmidas/testkit"];
10562
11605
  devDependencies$1["@faker-js/faker"] = "~9.8.0";
10563
- devDependencies$1["kysely-ctl"] = "~0.10.0";
11606
+ devDependencies$1["kysely-ctl"] = DEPENDENCY_VERSIONS["kysely-ctl"];
10564
11607
  devDependencies$1["vite-tsconfig-paths"] = "~5.1.0";
10565
11608
  }
10566
11609
  if (monorepo) {
@@ -10580,6 +11623,8 @@ function generatePackageJson(options, template) {
10580
11623
  const appName = pathParts[pathParts.length - 1] || "api";
10581
11624
  packageName = `@${name$1}/${appName}`;
10582
11625
  }
11626
+ dependencies$1["@geekmidas/client"] = GEEKMIDAS_VERSIONS["@geekmidas/client"];
11627
+ dependencies$1["@tanstack/react-query"] = DEPENDENCY_VERSIONS["@tanstack/react-query"];
10583
11628
  const packageJson = {
10584
11629
  name: packageName,
10585
11630
  version: "0.0.1",
@@ -10617,7 +11662,7 @@ function generateSourceFiles(options, template) {
10617
11662
  */
10618
11663
  function generateTestFiles(options, _template) {
10619
11664
  if (!options.database) return [];
10620
- const db = databaseFor(options.name);
11665
+ const db = databaseFor();
10621
11666
  const declares = !options.monorepo;
10622
11667
  const schema = declares ? "../src/constructs/database.ts" : "~/services/database.ts";
10623
11668
  const runtimeUrl = declares ? db.urlKey : "DATABASE_URL";
@@ -10653,14 +11698,16 @@ import pg from 'pg';
10653
11698
  import { wrapVitestKyselyTransaction } from '@geekmidas/testkit/kysely';
10654
11699
  import type { Database } from '${schema}';
10655
11700
 
10656
- const connection = new Kysely<Database>({
11701
+ const db = new Kysely<Database>({
10657
11702
  dialect: new PostgresDialect({
10658
11703
  pool: new pg.Pool({ connectionString: process.env.${runtimeUrl} }),
10659
11704
  }),
10660
11705
  });
10661
11706
 
10662
11707
  export const it = wrapVitestKyselyTransaction<Database>(itVitest, {
10663
- connection,
11708
+ // A function, or a construct: an instance is taken for a construct and has
11709
+ // no service to register.
11710
+ connection: () => db,
10664
11711
  });
10665
11712
  `
10666
11713
  },
@@ -10745,10 +11792,10 @@ export const usersBuilder = KyselyFactory.createBuilder<Database, 'users'>(
10745
11792
  import { it } from './config.ts';
10746
11793
 
10747
11794
  describe('example', () => {
10748
- it('should have a working test setup', async ({ db }) => {
10749
- // db is a transaction-wrapped Kysely instance
10750
- // All changes are automatically rolled back after the test
10751
- expect(db).toBeDefined();
11795
+ it('should have a working test setup', async ({ trx }) => {
11796
+ // trx is a Kysely transaction; everything in it is rolled back after the
11797
+ // test
11798
+ expect(trx).toBeDefined();
10752
11799
  });
10753
11800
  });
10754
11801
  `
@@ -11999,8 +13046,8 @@ export const Alert: Story = {
11999
13046
  ),
12000
13047
  };
12001
13048
  `;
12002
- const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button.tsx';
12003
- export { Input } from './input.tsx';
13049
+ const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button/index.tsx';
13050
+ export { Input } from './input/index.tsx';
12004
13051
  export {
12005
13052
  Card,
12006
13053
  CardHeader,
@@ -12008,17 +13055,17 @@ export {
12008
13055
  CardTitle,
12009
13056
  CardDescription,
12010
13057
  CardContent,
12011
- } from './card.tsx';
12012
- export { Label } from './label.tsx';
12013
- export { Badge, type BadgeProps, badgeVariants } from './badge.tsx';
12014
- export { Separator } from './separator.tsx';
12015
- export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs.tsx';
13058
+ } from './card/index.tsx';
13059
+ export { Label } from './label/index.tsx';
13060
+ export { Badge, type BadgeProps, badgeVariants } from './badge/index.tsx';
13061
+ export { Separator } from './separator/index.tsx';
13062
+ export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs/index.tsx';
12016
13063
  export {
12017
13064
  Tooltip,
12018
13065
  TooltipTrigger,
12019
13066
  TooltipContent,
12020
13067
  TooltipProvider,
12021
- } from './tooltip.tsx';
13068
+ } from './tooltip/index.tsx';
12022
13069
  export {
12023
13070
  Dialog,
12024
13071
  DialogPortal,
@@ -12030,7 +13077,7 @@ export {
12030
13077
  DialogFooter,
12031
13078
  DialogTitle,
12032
13079
  DialogDescription,
12033
- } from './dialog.tsx';
13080
+ } from './dialog/index.tsx';
12034
13081
  `;
12035
13082
  const buttonIndexTsx = buttonTsx;
12036
13083
  const inputIndexTsx = inputTsx;
@@ -12198,8 +13245,8 @@ function generateWebAppFiles(options) {
12198
13245
  [uiPackage]: "workspace:*",
12199
13246
  "@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
12200
13247
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
12201
- "@tanstack/react-query": "~5.80.0",
12202
- "better-auth": "~1.2.0",
13248
+ "@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
13249
+ "better-auth": DEPENDENCY_VERSIONS["better-auth"],
12203
13250
  next: "~16.1.0",
12204
13251
  react: "~19.2.0",
12205
13252
  "react-dom": "~19.2.0"
@@ -12255,7 +13302,7 @@ export default nextConfig;
12255
13302
  baseUrl: ".",
12256
13303
  paths: {
12257
13304
  "~/*": ["./src/*", "../../packages/ui/src/*"],
12258
- [`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"],
13305
+ [`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi/api.ts"],
12259
13306
  [`${modelsPackage}`]: ["../../packages/models/src"],
12260
13307
  [`${modelsPackage}/*`]: ["../../packages/models/src/*"],
12261
13308
  [`${uiPackage}`]: ["../../packages/ui/src"],
@@ -12268,12 +13315,7 @@ export default nextConfig;
12268
13315
  "**/*.tsx",
12269
13316
  ".next/types/**/*.ts"
12270
13317
  ],
12271
- exclude: ["node_modules"],
12272
- references: [
12273
- { path: "../../apps/api" },
12274
- { path: "../../packages/ui" },
12275
- { path: "../../packages/models" }
12276
- ]
13318
+ exclude: ["node_modules"]
12277
13319
  };
12278
13320
  const queryClientTs = `import { QueryClient } from '@tanstack/react-query';
12279
13321
 
@@ -12536,10 +13578,10 @@ function generateTanStackWebFiles(options) {
12536
13578
  [uiPackage]: "workspace:*",
12537
13579
  "@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
12538
13580
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
12539
- "@tanstack/react-query": "~5.80.0",
13581
+ "@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
12540
13582
  "@tanstack/react-router": "^1.87.0",
12541
13583
  "@tanstack/react-start": "^1.87.0",
12542
- "better-auth": "~1.2.0",
13584
+ "better-auth": DEPENDENCY_VERSIONS["better-auth"],
12543
13585
  react: "~19.2.0",
12544
13586
  "react-dom": "~19.2.0"
12545
13587
  },
@@ -12607,7 +13649,7 @@ export default defineConfig({
12607
13649
  [`${modelsPackage}/*`]: ["../../packages/models/src/*"],
12608
13650
  [uiPackage]: ["../../packages/ui/src"],
12609
13651
  [`${uiPackage}/*`]: ["../../packages/ui/src/*"],
12610
- [`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"]
13652
+ [`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi/api.ts"]
12611
13653
  }
12612
13654
  },
12613
13655
  include: [
@@ -12897,12 +13939,27 @@ function detectPackageManager(cwd = process.cwd()) {
12897
13939
  if (userAgent.includes("bun")) return "bun";
12898
13940
  return "npm";
12899
13941
  }
13942
+ /** `gkm init`'s flags, which a name can swallow when a space goes missing. */
13943
+ const INIT_FLAGS = [
13944
+ "yes",
13945
+ "name",
13946
+ "template",
13947
+ "monorepo",
13948
+ "api-path",
13949
+ "pm",
13950
+ "skip-install"
13951
+ ];
12900
13952
  /**
12901
13953
  * Validate project name for npm package naming conventions
12902
13954
  */
12903
13955
  function validateProjectName(name$1) {
12904
13956
  if (!name$1) return "Project name is required";
12905
13957
  if (!/^[a-z0-9-_@/.]+$/i.test(name$1)) return "Project name can only contain letters, numbers, hyphens, underscores, @, /, and .";
13958
+ const flag = name$1.match(/--(\w[\w-]*)$/)?.[1];
13959
+ if (flag && INIT_FLAGS.includes(flag)) {
13960
+ const intended = name$1.slice(0, -(flag.length + 2));
13961
+ return `"${name$1}" ends in the --${flag} flag. Did you mean \`${intended} --${flag}\`?`;
13962
+ }
12906
13963
  const reserved = [
12907
13964
  "node_modules",
12908
13965
  ".git",
@@ -12942,6 +13999,18 @@ function getRunCommand(pkgManager, script) {
12942
13999
  default: return `npm run ${script}`;
12943
14000
  }
12944
14001
  }
14002
+ /**
14003
+ * Run a binary the project installed, through its package manager — so it is
14004
+ * the project's own version, not whatever \`npx\` would fetch.
14005
+ */
14006
+ function getExecCommand(pkgManager, command) {
14007
+ switch (pkgManager) {
14008
+ case "pnpm": return `pnpm exec ${command}`;
14009
+ case "yarn": return `yarn ${command}`;
14010
+ case "bun": return `bunx ${command}`;
14011
+ default: return `npx --no-install ${command}`;
14012
+ }
14013
+ }
12945
14014
  const lockfileByPm = {
12946
14015
  pnpm: "pnpm-lock.yaml",
12947
14016
  yarn: "yarn.lock",
@@ -13012,12 +14081,31 @@ function findWorkspacePackages(cwd, pm) {
13012
14081
 
13013
14082
  //#endregion
13014
14083
  //#region src/init/index.ts
14084
+ /** `' staging, prod '` → `['staging', 'prod']`. */
14085
+ const stageList = (value) => value.split(",").map((name$1) => name$1.trim()).filter(Boolean);
14086
+ /** `eu-west-1`, `us-gov-west-1`, `ap-southeast-2`. */
14087
+ const AWS_REGION = /^[a-z]{2}(-gov)?-[a-z]+-\d$/;
13015
14088
  /**
13016
14089
  * Main init command - scaffolds a new project
13017
14090
  */
13018
14091
  async function initCommand(projectName, options = {}) {
13019
14092
  const cwd = process.cwd();
13020
14093
  const detectedPkgManager = detectPackageManager(cwd);
14094
+ 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(", ")}.`);
14095
+ if (options.region && !AWS_REGION.test(options.region)) throw new Error(`"${options.region}" is not an AWS region. Use one like eu-west-1.`);
14096
+ if (options.stages || options.localStage || options.protectedStage) {
14097
+ const deployed = stageList(options.stages ?? "");
14098
+ const problems = [
14099
+ ...require_workspace.deployedProblems(deployed),
14100
+ ...options.localStage ? require_workspace.stageProblems({
14101
+ local: options.localStage,
14102
+ deployed
14103
+ }) : [],
14104
+ ...options.protectedStage && !deployed.includes(options.protectedStage) ? [`--protected-stage "${options.protectedStage}" is not in --stages`] : []
14105
+ ];
14106
+ if (problems.length) throw new Error([...new Set(problems)].join("\n"));
14107
+ }
14108
+ let deployedSoFar = [];
13021
14109
  prompts.default.override({});
13022
14110
  const onCancel = () => {
13023
14111
  process.exit(0);
@@ -13068,12 +14156,49 @@ async function initCommand(projectName, options = {}) {
13068
14156
  initial: packageManagerChoices.findIndex((c) => c.value === detectedPkgManager)
13069
14157
  },
13070
14158
  {
13071
- type: options.yes ? null : "select",
14159
+ type: options.yes || options.deploy ? null : "select",
13072
14160
  name: "deployTarget",
13073
14161
  message: "Deployment target:",
13074
14162
  choices: deployTargetChoices,
13075
14163
  initial: 0
13076
14164
  },
14165
+ {
14166
+ type: (_prev, values) => !options.yes && !options.region && (options.deploy ?? values.deployTarget) === "sst" ? "text" : null,
14167
+ name: "region",
14168
+ message: "AWS region (e.g. eu-west-1):",
14169
+ validate: (value) => AWS_REGION.test(value.trim()) || "An AWS region, like eu-west-1"
14170
+ },
14171
+ {
14172
+ type: options.yes || options.stages ? null : "text",
14173
+ name: "deployedStages",
14174
+ message: "Deployed stages, comma-separated (e.g. staging, prod):",
14175
+ validate: (value) => {
14176
+ const deployed = stageList(value);
14177
+ if (deployed.length === 0) return "At least one stage";
14178
+ return require_workspace.deployedProblems(deployed)[0] ?? true;
14179
+ }
14180
+ },
14181
+ {
14182
+ type: (_prev, values) => options.yes || options.protectedStage ? null : stageList(options.stages ?? values.deployedStages ?? "").length > 1 ? "select" : null,
14183
+ name: "protectedStage",
14184
+ message: "Which one is production (retained and protected)?",
14185
+ choices: (_prev, values) => stageList(options.stages ?? values.deployedStages ?? "").map((name$2) => ({
14186
+ title: name$2,
14187
+ value: name$2
14188
+ }))
14189
+ },
14190
+ {
14191
+ type: (_prev, values) => {
14192
+ deployedSoFar = stageList(options.stages ?? values.deployedStages ?? "");
14193
+ return options.yes || options.localStage ? null : "text";
14194
+ },
14195
+ name: "localStage",
14196
+ message: "Local stage, what gkm dev runs as (e.g. dev):",
14197
+ validate: (value) => require_workspace.stageProblems({
14198
+ local: value.trim(),
14199
+ deployed: deployedSoFar
14200
+ })[0] ?? true
14201
+ },
13077
14202
  {
13078
14203
  type: options.yes ? null : "confirm",
13079
14204
  name: "telescope",
@@ -13139,7 +14264,7 @@ async function initCommand(projectName, options = {}) {
13139
14264
  };
13140
14265
  if (services.events === "pgboss") services.db = true;
13141
14266
  const pkgManager = options.pm ? options.pm : options.yes ? "pnpm" : answers.packageManager ?? detectedPkgManager;
13142
- const deployTarget = options.yes ? "dokploy" : answers.deployTarget ?? "dokploy";
14267
+ const deployTarget = options.deploy ?? (options.yes ? "none" : answers.deployTarget ?? "none");
13143
14268
  const database = services.db;
13144
14269
  const frontendFramework = isFullstack ? options.yes ? "nextjs" : answers.frontendFramework ?? "nextjs" : void 0;
13145
14270
  const templateOptions = {
@@ -13154,6 +14279,8 @@ async function initCommand(projectName, options = {}) {
13154
14279
  apiPath: monorepo ? options.apiPath ?? "apps/api" : "",
13155
14280
  packageManager: pkgManager,
13156
14281
  deployTarget,
14282
+ stages: resolveStages(options, answers),
14283
+ ...deployTarget === "sst" ? { region: options.region ?? (options.yes ? "eu-west-1" : answers.region?.trim()) } : {},
13157
14284
  services,
13158
14285
  frontendFramework
13159
14286
  };
@@ -13183,6 +14310,8 @@ async function initCommand(projectName, options = {}) {
13183
14310
  ] : [];
13184
14311
  const dockerFiles = isMonorepo$1 && baseTemplate ? generateDockerFiles(templateOptions, baseTemplate, dbApps) : [];
13185
14312
  const rootFiles = baseTemplate ? [
14313
+ ...generateAgentFiles(templateOptions, baseTemplate),
14314
+ ...generateGithubFiles(templateOptions),
13186
14315
  ...generateMonorepoFiles(templateOptions, baseTemplate),
13187
14316
  ...generateRootConstructs(templateOptions),
13188
14317
  ...generateModelsPackage(templateOptions)
@@ -13228,12 +14357,12 @@ async function initCommand(projectName, options = {}) {
13228
14357
  if (services.mail) secretServices.push("mailpit");
13229
14358
  if (services.events === "sns") secretServices.push("localstack");
13230
14359
  if (services.events === "rabbitmq") secretServices.push("rabbitmq");
13231
- const devSecrets = require_generator.createStageSecrets("development", secretServices, {
14360
+ const local = templateOptions.stages.local;
14361
+ const devSecrets = require_generator.createStageSecrets(local, secretServices, {
13232
14362
  projectName: name$1,
13233
14363
  eventsBackend: services.events
13234
14364
  });
13235
14365
  const customSecrets = {
13236
- NODE_ENV: "development",
13237
14366
  PORT: "3000",
13238
14367
  LOG_LEVEL: "debug",
13239
14368
  JWT_SECRET: `dev-${Date.now()}-${Math.random().toString(36).slice(2)}`
@@ -13253,8 +14382,8 @@ async function initCommand(projectName, options = {}) {
13253
14382
  }
13254
14383
  devSecrets.custom = customSecrets;
13255
14384
  await require_storage.writeStageSecrets(devSecrets, targetDir);
13256
- const keyPath = require_storage.getKeyPath("development", name$1);
13257
- console.log(` Secrets: .gkm/secrets/development.json (encrypted)`);
14385
+ const keyPath = require_storage.getKeyPath(local, name$1);
14386
+ console.log(` Secrets: .gkm/secrets/${local}.json (encrypted)`);
13258
14387
  console.log(` Key: ${keyPath}\n`);
13259
14388
  if (!options.skipInstall) {
13260
14389
  console.log("\n📦 Installing dependencies...\n");
@@ -13267,11 +14396,13 @@ async function initCommand(projectName, options = {}) {
13267
14396
  console.error("Failed to install dependencies");
13268
14397
  }
13269
14398
  try {
13270
- (0, node_child_process.execSync)("npx @biomejs/biome format --write --unsafe .", {
14399
+ (0, node_child_process.execSync)(getExecCommand(pkgManager, "biome check --write ."), {
13271
14400
  cwd: targetDir,
13272
14401
  stdio: "inherit"
13273
14402
  });
13274
- } catch {}
14403
+ } catch {
14404
+ console.warn(`\n⚠️ Formatting did not finish cleanly. Run ${getRunCommand(pkgManager, "fmt")} to see why.`);
14405
+ }
13275
14406
  }
13276
14407
  console.log("\n📦 Initializing git repository...\n");
13277
14408
  try {
@@ -13343,19 +14474,46 @@ function printNextSteps(projectName, options, pkgManager) {
13343
14474
  console.log(` └── turbo.json # Turbo config`);
13344
14475
  console.log("");
13345
14476
  }
14477
+ const { local, deployed } = options.stages;
13346
14478
  console.log("🔐 Secrets management:");
13347
- console.log(` gkm secrets:show --stage development # View secrets`);
13348
- console.log(` gkm secrets:set KEY VALUE --stage development # Add secret`);
13349
- console.log(` gkm secrets:init --stage production # Create production secrets`);
14479
+ console.log(` gkm secrets:show --stage ${local} # View secrets`);
14480
+ console.log(` gkm secrets:set KEY VALUE --stage ${local} # Add secret`);
14481
+ for (const stage of deployed) console.log(` gkm secrets:init --stage ${stage} # Create ${stage} secrets`);
13350
14482
  console.log("");
13351
14483
  if (options.deployTarget === "dokploy") {
13352
14484
  console.log("🚀 Deployment:");
13353
- console.log(` ${getRunCommand(pkgManager, "deploy")}`);
14485
+ for (const stage of deployed) console.log(` ${getRunCommand(pkgManager, `deploy:${stage}`)}`);
14486
+ console.log("");
14487
+ }
14488
+ if (options.deployTarget === "sst") {
14489
+ console.log("🚀 Deployment (AWS, through SST):");
14490
+ for (const stage of deployed) console.log(` ${getRunCommand(pkgManager, `deploy:${stage}`)} # gkm build --provider aws, then sst deploy`);
14491
+ console.log(` Uses your AWS credentials, in ${options.region}.`);
14492
+ if (options.services.mail) console.log(" Set MAIL_FROM to a sender verified in SES first.");
13354
14493
  console.log("");
13355
14494
  }
13356
14495
  console.log("📚 Documentation: https://geekmidas.github.io/toolbox/");
13357
14496
  console.log("");
13358
14497
  }
14498
+ /**
14499
+ * The stages the project declares, from the answers or the flags.
14500
+ *
14501
+ * `--yes` with no stage flags has nobody to ask and needs an answer, so it
14502
+ * takes `local`, and one deployed stage, `production`, that is protected.
14503
+ */
14504
+ function resolveStages(options, answers) {
14505
+ const deployed = stageList(options.stages ?? answers.deployedStages ?? (options.yes ? "production" : ""));
14506
+ const kept = options.protectedStage ?? answers.protectedStage ?? (deployed.length === 1 || options.yes ? deployed[0] : void 0);
14507
+ const local = (options.localStage ?? answers.localStage ?? (options.yes ? "local" : "")).trim();
14508
+ const stages = {
14509
+ local,
14510
+ deployed,
14511
+ ...kept ? { protected: [kept] } : {}
14512
+ };
14513
+ const problems = require_workspace.stageProblems(stages);
14514
+ if (problems.length) throw new Error(problems.join("\n"));
14515
+ return stages;
14516
+ }
13359
14517
 
13360
14518
  //#endregion
13361
14519
  //#region src/secrets/index.ts
@@ -13566,19 +14724,20 @@ function maskUrl(url) {
13566
14724
  * Environment variables are sniffed to inject only what the app needs.
13567
14725
  */
13568
14726
  async function testCommand(options = {}) {
13569
- const stage = options.stage ?? "development";
13570
14727
  const cwd = process.cwd();
14728
+ const stage = options.stage ?? await require_config.loadWorkspaceConfig(cwd).then((loaded) => loaded.workspace.stages.local).catch(() => void 0);
14729
+ if (!stage) throw new Error("No stage to test with: add `stages` to gkm.config.ts, or pass --stage.");
13571
14730
  console.log(`\n🧪 Running tests with ${stage} environment...\n`);
13572
14731
  const autoSetup = options.autoSetup || Boolean(process.env.GKM_AUTO_SETUP);
13573
14732
  if (autoSetup) {
13574
- const { ensureStageSecrets } = await Promise.resolve().then(() => require("./setup-DMtU3p41.cjs"));
14733
+ const { ensureStageSecrets } = await Promise.resolve().then(() => require("./setup-BsZvZP6f.cjs"));
13575
14734
  const generated = await ensureStageSecrets(stage, cwd);
13576
14735
  if (generated) console.log(` 🔐 Generated fresh ${stage} secrets (auto-setup)`);
13577
14736
  }
13578
14737
  const defaultEnv = require_credentials$1.loadEnvFiles(".env");
13579
14738
  if (defaultEnv.loaded.length > 0) console.log(` 📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
13580
14739
  const result = await require_credentials$1.prepareEntryCredentials({
13581
- stages: [stage],
14740
+ stage,
13582
14741
  startDocker: true,
13583
14742
  secretsFileName: "test-secrets.json",
13584
14743
  resolveDockerPorts: "full",
@@ -13659,145 +14818,305 @@ function rewriteDatabaseUrlForTests(env) {
13659
14818
  return result;
13660
14819
  }
13661
14820
 
14821
+ //#endregion
14822
+ //#region src/upgrade/version.ts
14823
+ /**
14824
+ * Just enough semver for `gkm upgrade`: ordering versions, and reading the
14825
+ * floor out of a simple range. The same ordering the release pipeline uses to
14826
+ * decide what is published (`scripts/release-state.mjs`).
14827
+ */
14828
+ const VERSION = /(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/;
14829
+ function parse(version$1) {
14830
+ const match = version$1.match(VERSION);
14831
+ if (!match) return void 0;
14832
+ const [, major, minor, patch, pre] = match;
14833
+ return {
14834
+ core: [
14835
+ Number(major),
14836
+ Number(minor),
14837
+ Number(patch)
14838
+ ],
14839
+ ids: pre ? pre.split(".").map((id) => /^\d+$/.test(id) ? Number(id) : id) : []
14840
+ };
14841
+ }
14842
+ /** Negative, zero or positive, as `a` is below, equal to or above `b`. */
14843
+ function compareVersions(a, b) {
14844
+ const x = parse(a);
14845
+ const y = parse(b);
14846
+ if (!x || !y) return 0;
14847
+ for (let i = 0; i < 3; i++) if (x.core[i] !== y.core[i]) return x.core[i] - y.core[i];
14848
+ if (!x.ids.length || !y.ids.length) return y.ids.length - x.ids.length;
14849
+ for (let i = 0; i < Math.max(x.ids.length, y.ids.length); i++) {
14850
+ const p = x.ids[i];
14851
+ const q = y.ids[i];
14852
+ if (p === void 0) return -1;
14853
+ if (q === void 0) return 1;
14854
+ if (p === q) continue;
14855
+ if (typeof p === "number" && typeof q === "number") return p - q;
14856
+ if (typeof p === "number") return -1;
14857
+ if (typeof q === "number") return 1;
14858
+ return p < q ? -1 : 1;
14859
+ }
14860
+ return 0;
14861
+ }
14862
+ /** `alpha` for `10.0.0-alpha.6`; undefined for a release. */
14863
+ function prereleaseTag(version$1) {
14864
+ const id = parse(version$1)?.ids[0];
14865
+ return typeof id === "string" ? id : void 0;
14866
+ }
14867
+ /**
14868
+ * A range that is one operator and one version — `~1.2.3`, `^1.2.3`, `>=1.2.3`,
14869
+ * `1.2.3` — split into the two. Anything else (`>=8 <10`, `a || b`) is not
14870
+ * something to rewrite mechanically.
14871
+ */
14872
+ function simpleRange(range) {
14873
+ const match = range.trim().match(/^(\^|~|>=|)v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/);
14874
+ return match ? {
14875
+ operator: match[1],
14876
+ version: match[2]
14877
+ } : void 0;
14878
+ }
14879
+
13662
14880
  //#endregion
13663
14881
  //#region src/upgrade/index.ts
13664
14882
  const logger = console;
14883
+ const SCOPE = "@geekmidas/";
14884
+ const DEP_FIELDS = [
14885
+ "dependencies",
14886
+ "devDependencies",
14887
+ "peerDependencies",
14888
+ "optionalDependencies"
14889
+ ];
14890
+ async function packument(name$1) {
14891
+ const res = await fetch(`https://registry.npmjs.org/${name$1.replace("/", "%2F")}`, { headers: { accept: "application/vnd.npm.install-v1+json" } });
14892
+ if (!res.ok) throw new RegistryUnavailable(name$1, res.status);
14893
+ return await res.json();
14894
+ }
14895
+ /** Every registry range in the workspace's package.json files. */
14896
+ function packageOccurrences(paths) {
14897
+ const found = [];
14898
+ for (const file of paths) {
14899
+ const pkg$1 = JSON.parse((0, node_fs.readFileSync)(file, "utf-8"));
14900
+ for (const field of DEP_FIELDS) for (const [name$1, range] of Object.entries(pkg$1[field] ?? {})) found.push({
14901
+ name: name$1,
14902
+ range,
14903
+ file,
14904
+ field
14905
+ });
14906
+ }
14907
+ return found;
14908
+ }
14909
+ /** pnpm catalog entries, which is where `catalog:` references resolve. */
14910
+ function catalogOccurrences(root) {
14911
+ const file = (0, node_path.join)(root, "pnpm-workspace.yaml");
14912
+ if (!(0, node_fs.existsSync)(file)) return [];
14913
+ const doc = (0, yaml.parseDocument)((0, node_fs.readFileSync)(file, "utf-8")).toJS() ?? {};
14914
+ const found = [];
14915
+ for (const [name$1, range] of Object.entries(doc.catalog ?? {})) found.push({
14916
+ name: name$1,
14917
+ range: String(range),
14918
+ file,
14919
+ field: "catalog"
14920
+ });
14921
+ for (const [catalog, entries] of Object.entries(doc.catalogs ?? {})) for (const [name$1, range] of Object.entries(entries ?? {})) found.push({
14922
+ name: name$1,
14923
+ range: String(range),
14924
+ file,
14925
+ field: "catalog",
14926
+ catalog
14927
+ });
14928
+ return found;
14929
+ }
14930
+ /** Not a registry range: resolved elsewhere, so not ours to move. */
14931
+ const isIndirect = (range) => /^(workspace|catalog|link|file|npm|git|github|https?):/.test(range);
14932
+ /**
14933
+ * The version every `@geekmidas` package moves to.
14934
+ *
14935
+ * All of them share one version, so the CLI's line answers for the rest. The
14936
+ * tag is the one the project is already on — a project on `10.0.0-alpha.6`
14937
+ * follows `alpha` — because `latest` names a different major while a new one
14938
+ * is in prerelease, and following it would move the project backwards.
14939
+ */
14940
+ function resolveTarget(distTags, installed, tag) {
14941
+ const highest = [...installed].sort(compareVersions).at(-1);
14942
+ const chosen = tag ?? (highest && prereleaseTag(highest)) ?? "latest";
14943
+ const version$1 = distTags[chosen];
14944
+ if (!version$1) throw new NoReleaseOnTag(chosen, Object.keys(distTags));
14945
+ return {
14946
+ tag: chosen,
14947
+ version: version$1
14948
+ };
14949
+ }
14950
+ /** The same operator on the new version — `^`, `~`, `>=` or none, as written. */
14951
+ function withVersion(range, version$1) {
14952
+ return `${simpleRange(range)?.operator ?? ""}${version$1}`;
14953
+ }
13665
14954
  async function upgradeCommand(options = {}) {
13666
14955
  const cwd = process.cwd();
13667
- logger.log("\n📦 Scanning workspace for @geekmidas packages...\n");
13668
14956
  const pm = detectPackageManager(cwd);
14957
+ const root = findWorkspaceRoot(cwd, pm);
14958
+ logger.log("\n📦 Scanning workspace for @geekmidas packages...\n");
13669
14959
  logger.log(` Package manager: ${pm}`);
13670
- const packageJsonPaths = findWorkspacePackages(cwd, pm);
13671
- logger.log(` Found ${packageJsonPaths.length} package(s) in workspace\n`);
13672
- const dependencies$1 = scanForGeekmidasDeps(packageJsonPaths);
13673
- if (dependencies$1.length === 0) {
14960
+ const packageJsons = findWorkspacePackages(cwd, pm);
14961
+ const occurrences = [...packageOccurrences(packageJsons), ...catalogOccurrences(root)];
14962
+ logger.log(` Found ${packageJsons.length} package(s) in workspace\n`);
14963
+ const ours = occurrences.filter((o) => o.name.startsWith(SCOPE) && !isIndirect(o.range));
14964
+ if (ours.length === 0) {
13674
14965
  logger.log(" No @geekmidas packages found.\n");
13675
14966
  return;
13676
14967
  }
13677
- const uniquePackages = [...new Set(dependencies$1.map((d) => d.packageName))];
13678
- logger.log(` Checking ${uniquePackages.length} unique @geekmidas package(s) on npm...\n`);
13679
- const latestVersions = await fetchLatestVersions(uniquePackages);
13680
- const upgradeInfos = dependencies$1.map((dep) => resolveUpgradeInfo(dep, latestVersions));
13681
- printUpgradeTable(upgradeInfos);
13682
- const upgradable = upgradeInfos.filter((info) => info.needsUpgrade && !info.isWorkspaceRef);
13683
- if (upgradable.length === 0) {
13684
- logger.log("\n All @geekmidas packages are up to date!\n");
14968
+ const installed = ours.map((o) => simpleRange(o.range)?.version).filter((v) => Boolean(v));
14969
+ const cli = await packument(`${SCOPE}cli`);
14970
+ const target = resolveTarget(cli["dist-tags"], installed, options.tag);
14971
+ logger.log(` Target: ${target.version} (npm "${target.tag}")\n`);
14972
+ const highest = installed.sort(compareVersions).at(-1);
14973
+ if (compareVersions(target.version, highest) < 0) throw new WouldDowngrade(highest, target.tag, target.version);
14974
+ const moving = options.all ? ours : ours.filter((o) => o.name === `${SCOPE}cli`);
14975
+ const changes = [];
14976
+ for (const o of moving) {
14977
+ if (!simpleRange(o.range)) {
14978
+ logger.log(` ⚠ ${o.name} ${o.range} is not a simple range; left as is (target ${target.version}).`);
14979
+ continue;
14980
+ }
14981
+ const next = withVersion(o.range, target.version);
14982
+ if (next !== o.range) changes.push({
14983
+ ...o,
14984
+ next
14985
+ });
14986
+ }
14987
+ if (options.all) changes.push(...await peerChanges(occurrences, ours, target.version));
14988
+ if (changes.length === 0) {
14989
+ logger.log(" Everything is already on the target version.\n");
13685
14990
  return;
13686
14991
  }
13687
- logger.log(`\n ${upgradable.length} package(s) can be upgraded.\n`);
14992
+ printChanges(changes, root);
14993
+ if (!options.all && ours.some((o) => o.name !== `${SCOPE}cli`)) logger.log("\n Only @geekmidas/cli moves without --all. Afterwards, run `gkm upgrade --all` with the new CLI.");
13688
14994
  if (options.dryRun) {
13689
- logger.log(" --dry-run: No changes made.\n");
13690
- printUpgradeCommands(upgradable, pm);
14995
+ logger.log("\n --dry-run: no changes made.\n");
13691
14996
  return;
13692
14997
  }
13693
- executeUpgrade(upgradable, pm, cwd);
13694
- logger.log("\n ✅ Upgrade complete! Run your tests to verify.\n");
14998
+ applyChanges(changes);
14999
+ install(pm, root);
15000
+ logger.log("\n ✅ Upgrade complete. Run your tests to verify.\n");
13695
15001
  }
13696
- function scanForGeekmidasDeps(packageJsonPaths) {
13697
- const results = [];
13698
- const depTypes = [
13699
- "dependencies",
13700
- "devDependencies",
13701
- "peerDependencies"
13702
- ];
13703
- for (const pkgJsonPath of packageJsonPaths) {
13704
- const pkg$1 = JSON.parse((0, node_fs.readFileSync)(pkgJsonPath, "utf-8"));
13705
- const workspaceName = pkg$1.name ?? pkgJsonPath;
13706
- for (const depType of depTypes) {
13707
- const deps = pkg$1[depType];
13708
- if (!deps) continue;
13709
- for (const [name$1, version$1] of Object.entries(deps)) {
13710
- if (!name$1.startsWith("@geekmidas/")) continue;
13711
- results.push({
13712
- packageName: name$1,
13713
- currentVersion: version$1,
13714
- depType,
13715
- packageJsonPath: pkgJsonPath,
13716
- workspaceName
13717
- });
13718
- }
15002
+ /**
15003
+ * Third-party packages the project already lists, raised to the floor of the
15004
+ * peer range the target `@geekmidas` versions declare.
15005
+ *
15006
+ * Only raised, never lowered, and only where both the peer range and the
15007
+ * project's range are one operator and one version; anything else is reported
15008
+ * for a person to decide.
15009
+ */
15010
+ async function peerChanges(occurrences, ours, version$1) {
15011
+ const floors = /* @__PURE__ */ new Map();
15012
+ const names = [...new Set(ours.map((o) => o.name))];
15013
+ const docs = await Promise.all(names.map(packument));
15014
+ for (const doc of docs) {
15015
+ const peers = doc.versions[version$1]?.peerDependencies ?? {};
15016
+ for (const [peer, range] of Object.entries(peers)) {
15017
+ if (peer.startsWith(SCOPE)) continue;
15018
+ const floor = simpleRange(range)?.version;
15019
+ if (!floor) continue;
15020
+ const known = floors.get(peer);
15021
+ if (!known || compareVersions(floor, known) > 0) floors.set(peer, floor);
15022
+ }
15023
+ }
15024
+ const changes = [];
15025
+ for (const o of occurrences) {
15026
+ const floor = floors.get(o.name);
15027
+ if (!floor || isIndirect(o.range) || o.field === "peerDependencies") continue;
15028
+ const current = simpleRange(o.range);
15029
+ if (!current) {
15030
+ logger.log(` ⚠ ${o.name} ${o.range} is not a simple range; @geekmidas needs at least ${floor}.`);
15031
+ continue;
13719
15032
  }
15033
+ if (compareVersions(current.version, floor) < 0) changes.push({
15034
+ ...o,
15035
+ next: withVersion(o.range, floor)
15036
+ });
13720
15037
  }
13721
- return results;
13722
- }
13723
- async function fetchLatestVersions(packageNames) {
13724
- const versions = /* @__PURE__ */ new Map();
13725
- const results = await Promise.allSettled(packageNames.map(async (name$1) => {
13726
- const res = await fetch(`https://registry.npmjs.org/${name$1}/latest`);
13727
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
13728
- const data = await res.json();
13729
- return {
13730
- name: name$1,
13731
- version: data.version
13732
- };
13733
- }));
13734
- for (const result of results) if (result.status === "fulfilled") versions.set(result.value.name, result.value.version);
13735
- return versions;
13736
- }
13737
- function resolveUpgradeInfo(dep, latestVersions) {
13738
- const isWorkspaceRef = dep.currentVersion.startsWith("workspace:");
13739
- const latestVersion = latestVersions.get(dep.packageName) ?? "unknown";
13740
- const currentBare = dep.currentVersion.replace(/^[\^~>=<]*/g, "");
13741
- const needsUpgrade = !isWorkspaceRef && latestVersion !== "unknown" && currentBare !== latestVersion;
13742
- return {
13743
- ...dep,
13744
- latestVersion,
13745
- isWorkspaceRef,
13746
- needsUpgrade
13747
- };
13748
- }
13749
- function printUpgradeTable(infos) {
13750
- const byPackage = /* @__PURE__ */ new Map();
13751
- for (const info of infos) if (!byPackage.has(info.packageName)) byPackage.set(info.packageName, info);
13752
- const nameWidth = 33;
13753
- const verWidth = 14;
13754
- const statusWidth = 14;
13755
- const hr = ` ${"─".repeat(nameWidth + verWidth * 2 + statusWidth + 5)}`;
13756
- logger.log(hr);
13757
- logger.log(` ${"Package".padEnd(nameWidth)} ${"Current".padEnd(verWidth)} ${"Latest".padEnd(verWidth)} ${"Status".padEnd(statusWidth)}`);
13758
- logger.log(hr);
13759
- for (const [, info] of byPackage) {
13760
- const name$1 = info.packageName.padEnd(nameWidth);
13761
- const current = info.currentVersion.padEnd(verWidth);
13762
- const latest = info.latestVersion.padEnd(verWidth);
13763
- let status;
13764
- if (info.isWorkspaceRef) status = "workspace";
13765
- else if (info.needsUpgrade) status = "⬆ upgrade";
13766
- else status = "✓ up-to-date";
13767
- logger.log(` ${name$1} ${current} ${latest} ${status}`);
13768
- }
13769
- logger.log(hr);
13770
- }
13771
- function printUpgradeCommands(upgradable, pm) {
13772
- logger.log(" Commands that would be run:\n");
13773
- const uniquePackages = [...new Set(upgradable.map((i) => i.packageName))];
13774
- const cmd = getWorkspaceUpgradeCommand(pm, uniquePackages);
13775
- logger.log(` ${cmd}\n`);
13776
- }
13777
- function getWorkspaceUpgradeCommand(pm, packages) {
13778
- const pkgList = packages.join(" ");
13779
- switch (pm) {
13780
- case "pnpm": return `pnpm update -r ${pkgList} --latest`;
13781
- case "yarn": return `yarn upgrade ${pkgList}`;
13782
- case "bun": return `bun update ${pkgList}`;
13783
- case "npm": return `npm update ${pkgList} --workspaces`;
13784
- default: return `npm update ${pkgList}`;
15038
+ return changes;
15039
+ }
15040
+ function printChanges(changes, root) {
15041
+ logger.log(" Changes:\n");
15042
+ for (const c of changes) {
15043
+ const where = c.field === "catalog" ? `pnpm-workspace.yaml (${c.catalog ? `catalogs.${c.catalog}` : "catalog"})` : `${(0, node_path.relative)(root, c.file) || "package.json"} (${c.field})`;
15044
+ logger.log(` ${c.name.padEnd(34)} ${c.range} → ${c.next} ${where}`);
15045
+ }
15046
+ }
15047
+ function applyChanges(changes) {
15048
+ const byFile = /* @__PURE__ */ new Map();
15049
+ for (const c of changes) byFile.set(c.file, [...byFile.get(c.file) ?? [], c]);
15050
+ for (const [file, fileChanges] of byFile) {
15051
+ const text = (0, node_fs.readFileSync)(file, "utf-8");
15052
+ if (file.endsWith(".yaml")) {
15053
+ const doc = (0, yaml.parseDocument)(text);
15054
+ for (const c of fileChanges) {
15055
+ const path = c.catalog ? [
15056
+ "catalogs",
15057
+ c.catalog,
15058
+ c.name
15059
+ ] : ["catalog", c.name];
15060
+ doc.setIn(path, c.next);
15061
+ }
15062
+ (0, node_fs.writeFileSync)(file, doc.toString());
15063
+ continue;
15064
+ }
15065
+ const pkg$1 = JSON.parse(text);
15066
+ for (const c of fileChanges) pkg$1[c.field][c.name] = c.next;
15067
+ const indent = text.match(/^[ \t]+(?=")/m)?.[0] ?? " ";
15068
+ (0, node_fs.writeFileSync)(file, `${JSON.stringify(pkg$1, null, indent)}\n`);
13785
15069
  }
13786
15070
  }
13787
- function executeUpgrade(upgradable, pm, cwd) {
13788
- const uniquePackages = [...new Set(upgradable.map((i) => i.packageName))];
13789
- const cmd = getWorkspaceUpgradeCommand(pm, uniquePackages);
13790
- logger.log(` Running: ${cmd}\n`);
15071
+ function install(pm, cwd) {
15072
+ const command = getInstallCommand(pm);
15073
+ logger.log(`\n Running: ${command}\n`);
13791
15074
  try {
13792
- (0, node_child_process.execSync)(cmd, {
15075
+ (0, node_child_process.execSync)(command, {
13793
15076
  cwd,
13794
15077
  stdio: "inherit",
13795
- timeout: 12e4
15078
+ timeout: 3e5
13796
15079
  });
13797
15080
  } catch {
13798
- throw new Error("Package upgrade failed. Check the output above for details.");
15081
+ throw new InstallFailed(command);
13799
15082
  }
13800
15083
  }
15084
+ /** npm did not answer for a package, so there is no version to compare. */
15085
+ var RegistryUnavailable = class extends Error {
15086
+ constructor(packageName, status) {
15087
+ super(`npm answered ${status} for ${packageName}. Check the name and your network.`);
15088
+ this.packageName = packageName;
15089
+ this.status = status;
15090
+ this.name = "RegistryUnavailable";
15091
+ }
15092
+ };
15093
+ /** A dist-tag npm does not have, e.g. `--tag beta` before any beta. */
15094
+ var NoReleaseOnTag = class extends Error {
15095
+ constructor(tag, tags) {
15096
+ super(`npm has no "${tag}" release of @geekmidas/cli. Tags: ${tags.join(", ")}.`);
15097
+ this.tag = tag;
15098
+ this.tags = tags;
15099
+ this.name = "NoReleaseOnTag";
15100
+ }
15101
+ };
15102
+ /** The line asked for is behind what is installed; upgrade never goes back. */
15103
+ var WouldDowngrade = class extends Error {
15104
+ constructor(installed, tag, target) {
15105
+ super(`The project is already on ${installed}, ahead of npm's "${tag}" (${target}). Pass --tag to follow another line.`);
15106
+ this.installed = installed;
15107
+ this.tag = tag;
15108
+ this.target = target;
15109
+ this.name = "WouldDowngrade";
15110
+ }
15111
+ };
15112
+ /** The install after rewriting versions failed; its own output says why. */
15113
+ var InstallFailed = class extends Error {
15114
+ constructor(command) {
15115
+ super(`\`${command}\` failed. Check the output above for details.`);
15116
+ this.command = command;
15117
+ this.name = "InstallFailed";
15118
+ }
15119
+ };
13801
15120
 
13802
15121
  //#endregion
13803
15122
  //#region src/index.ts
@@ -13806,7 +15125,7 @@ program.name("gkm").description("GeekMidas backend framework CLI").version(packa
13806
15125
  const opts = program.opts();
13807
15126
  if (opts.debug) enableDebug();
13808
15127
  });
13809
- 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) => {
15128
+ 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) => {
13810
15129
  try {
13811
15130
  const globalOptions = program.opts();
13812
15131
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
@@ -13826,7 +15145,7 @@ program.command("trust").description("Trust the local edge's certificate authori
13826
15145
  process.exit(1);
13827
15146
  }
13828
15147
  });
13829
- 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) => {
15148
+ 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) => {
13830
15149
  try {
13831
15150
  const globalOptions = program.opts();
13832
15151
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
@@ -13898,7 +15217,7 @@ program.command("exec").description("Run a command with secrets injected into Cr
13898
15217
  process.exit(1);
13899
15218
  }
13900
15219
  });
13901
- 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) => {
15220
+ 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) => {
13902
15221
  try {
13903
15222
  const globalOptions = program.opts();
13904
15223
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
@@ -14046,7 +15365,7 @@ program.command("secrets:push").description("Push secrets to remote provider (SS
14046
15365
  const { workspace } = await loadWorkspaceConfig$1();
14047
15366
  const secrets = await readStageSecrets$1(options.stage, workspace.root);
14048
15367
  if (secrets) {
14049
- const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-8acupeaI.cjs"));
15368
+ const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-XXtNhqJl.cjs"));
14050
15369
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
14051
15370
  if (result) {
14052
15371
  await writeStageSecrets$1(result.secrets, workspace.root);
@@ -14075,7 +15394,7 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
14075
15394
  console.error(`No remote secrets found for stage "${options.stage}".`);
14076
15395
  process.exit(1);
14077
15396
  }
14078
- const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-8acupeaI.cjs"));
15397
+ const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-XXtNhqJl.cjs"));
14079
15398
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
14080
15399
  if (result) {
14081
15400
  secrets = result.secrets;
@@ -14089,7 +15408,7 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
14089
15408
  process.exit(1);
14090
15409
  }
14091
15410
  });
14092
- program.command("secrets:reconcile").description("Backfill missing custom secrets from workspace config").option("--stage <stage>", "Stage name", "development").action(async (options) => {
15411
+ program.command("secrets:reconcile").description("Backfill missing custom secrets from workspace config").option("--stage <stage>", "Stage name (default: stages.local)").action(async (options) => {
14093
15412
  try {
14094
15413
  const globalOptions = program.opts();
14095
15414
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
@@ -14097,19 +15416,20 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
14097
15416
  const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-D0pimNiy.cjs"));
14098
15417
  const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-BM9hGpgv.cjs"));
14099
15418
  const { workspace } = await loadWorkspaceConfig$1();
14100
- const secrets = await readStageSecrets$1(options.stage, workspace.root);
15419
+ const stage = options.stage ?? workspace.stages.local;
15420
+ const secrets = await readStageSecrets$1(stage, workspace.root);
14101
15421
  if (!secrets) {
14102
- console.error(`No secrets found for stage "${options.stage}". Run "gkm secrets:init --stage ${options.stage}" first.`);
15422
+ console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
14103
15423
  process.exit(1);
14104
15424
  }
14105
- const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-8acupeaI.cjs"));
14106
- const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
15425
+ const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-XXtNhqJl.cjs"));
15426
+ const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, stage));
14107
15427
  if (!result) {
14108
- console.log(`\n✓ Secrets for stage "${options.stage}" are up-to-date`);
15428
+ console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
14109
15429
  return;
14110
15430
  }
14111
15431
  await writeStageSecrets$1(result.secrets, workspace.root);
14112
- console.log(`\n✓ Reconciled ${result.addedKeys.length} missing secret(s) for stage "${options.stage}":`);
15432
+ console.log(`\n✓ Reconciled ${result.addedKeys.length} missing secret(s) for stage "${stage}":`);
14113
15433
  for (const key of result.addedKeys) console.log(` + ${key}`);
14114
15434
  } catch (error) {
14115
15435
  console.error(formatError(error));
@@ -14263,7 +15583,7 @@ program.command("state:diff").description("Compare local and remote deployment s
14263
15583
  process.exit(1);
14264
15584
  }
14265
15585
  });
14266
- program.command("upgrade").description("Upgrade all @geekmidas packages to their latest versions").option("--dry-run", "Show what would be upgraded without making changes").action(async (options) => {
15586
+ program.command("upgrade").description("Upgrade @geekmidas/cli (with --all, every @geekmidas package) along the release line the project is on").option("--dry-run", "Show what would be upgraded without making changes").option("--all", "Every @geekmidas package, and the third-party packages their peers require").option("--tag <tag>", "npm dist-tag to follow (default: the line you are on)").action(async (options) => {
14267
15587
  try {
14268
15588
  const globalOptions = program.opts();
14269
15589
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);