@geekmidas/cli 10.0.0-alpha.11 → 10.0.0-alpha.12

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 (97) hide show
  1. package/README.md +13 -40
  2. package/dist/{bundler-CaTGtiaO.cjs → bundler-BMekgx7a.cjs} +2 -17
  3. package/dist/bundler-BMekgx7a.cjs.map +1 -0
  4. package/dist/{bundler-S3ek7ea-.mjs → bundler-Bux2iF-v.mjs} +2 -17
  5. package/dist/bundler-Bux2iF-v.mjs.map +1 -0
  6. package/dist/{config-B0L2tvvC.mjs → config-DTsVjKZD.mjs} +3 -3
  7. package/dist/{config-B0L2tvvC.mjs.map → config-DTsVjKZD.mjs.map} +1 -1
  8. package/dist/{config-HhDpJmLO.cjs → config-RYR9j6EF.cjs} +3 -3
  9. package/dist/{config-HhDpJmLO.cjs.map → config-RYR9j6EF.cjs.map} +1 -1
  10. package/dist/config.cjs +3 -3
  11. package/dist/config.d.cts +2 -2
  12. package/dist/config.d.mts +2 -2
  13. package/dist/config.mjs +3 -3
  14. package/dist/{credentials-B2Pyh3bJ.cjs → credentials-DQRvPz2S.cjs} +1165 -1077
  15. package/dist/credentials-DQRvPz2S.cjs.map +1 -0
  16. package/dist/{credentials-CZrys9C5.mjs → credentials-jMJC63qs.mjs} +1144 -1068
  17. package/dist/credentials-jMJC63qs.mjs.map +1 -0
  18. package/dist/{discover-CAlFgYec.mjs → discover-BYHQwQqu.mjs} +15 -2
  19. package/dist/discover-BYHQwQqu.mjs.map +1 -0
  20. package/dist/{discover-DTmFeloL.cjs → discover-DmW9Bscc.cjs} +20 -1
  21. package/dist/discover-DmW9Bscc.cjs.map +1 -0
  22. package/dist/{fullstack-secrets-METaRLS1.mjs → fullstack-secrets-B6KBHf-g.mjs} +2 -2
  23. package/dist/{fullstack-secrets-METaRLS1.mjs.map → fullstack-secrets-B6KBHf-g.mjs.map} +1 -1
  24. package/dist/{fullstack-secrets-D6MyHhQP.cjs → fullstack-secrets-DL8v0y4D.cjs} +1 -13
  25. package/dist/{fullstack-secrets-D6MyHhQP.cjs.map → fullstack-secrets-DL8v0y4D.cjs.map} +1 -1
  26. package/dist/{github-BrvaPkFf.cjs → github-BThvbf2W.cjs} +4 -4
  27. package/dist/{github-BrvaPkFf.cjs.map → github-BThvbf2W.cjs.map} +1 -1
  28. package/dist/{github-CrpsXSfV.mjs → github-DpMZurEK.mjs} +4 -4
  29. package/dist/{github-CrpsXSfV.mjs.map → github-DpMZurEK.mjs.map} +1 -1
  30. package/dist/{index-CR5urhWX.d.cts → index-BzJ7QJ9c.d.mts} +2 -6
  31. package/dist/index-BzJ7QJ9c.d.mts.map +1 -0
  32. package/dist/{index-Ayxg_DQY.d.mts → index-CfMjtdVC.d.cts} +2 -6
  33. package/dist/index-CfMjtdVC.d.cts.map +1 -0
  34. package/dist/index.cjs +403 -1356
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.mjs +404 -1357
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/{openapi-y1d7Kalt.mjs → openapi-Bs3-5q_r.mjs} +3 -3
  39. package/dist/{openapi-y1d7Kalt.mjs.map → openapi-Bs3-5q_r.mjs.map} +1 -1
  40. package/dist/{openapi-DaKlTKxz.cjs → openapi-DWLTsYLL.cjs} +3 -3
  41. package/dist/{openapi-DaKlTKxz.cjs.map → openapi-DWLTsYLL.cjs.map} +1 -1
  42. package/dist/{openapi-react-query-DoR5CmzQ.mjs → openapi-react-query-Byltvm_7.mjs} +1 -1
  43. package/dist/{openapi-react-query-DoR5CmzQ.mjs.map → openapi-react-query-Byltvm_7.mjs.map} +1 -1
  44. package/dist/{openapi-react-query-BKH334Ug.cjs → openapi-react-query-CmJ1xDbn.cjs} +1 -1
  45. package/dist/{openapi-react-query-BKH334Ug.cjs.map → openapi-react-query-CmJ1xDbn.cjs.map} +1 -1
  46. package/dist/openapi-react-query.cjs +1 -1
  47. package/dist/openapi-react-query.mjs +1 -1
  48. package/dist/openapi.cjs +4 -4
  49. package/dist/openapi.d.cts +1 -1
  50. package/dist/openapi.d.mts +1 -1
  51. package/dist/openapi.mjs +4 -4
  52. package/dist/reconcile/public.cjs +1 -1
  53. package/dist/reconcile/public.d.cts +1 -1
  54. package/dist/reconcile/public.d.mts +1 -1
  55. package/dist/reconcile/public.mjs +1 -1
  56. package/dist/{reconcile-C13ERt1U.mjs → reconcile-BI0tWkwz.mjs} +2 -2
  57. package/dist/{reconcile-C13ERt1U.mjs.map → reconcile-BI0tWkwz.mjs.map} +1 -1
  58. package/dist/{reconcile-CYMhFSYy.cjs → reconcile-Bhw5fmKu.cjs} +2 -2
  59. package/dist/{reconcile-CYMhFSYy.cjs.map → reconcile-Bhw5fmKu.cjs.map} +1 -1
  60. package/dist/setup-BmLoNrDX.mjs +12 -0
  61. package/dist/setup-BrR1_xGy.cjs +12 -0
  62. package/dist/{setup-DOv92pIu.cjs → setup-BsNR2LBG.cjs} +14 -20
  63. package/dist/setup-BsNR2LBG.cjs.map +1 -0
  64. package/dist/{setup-DQo_h_yK.mjs → setup-D5-JMknk.mjs} +14 -20
  65. package/dist/setup-D5-JMknk.mjs.map +1 -0
  66. package/dist/{types-Y3fKQSAK.d.cts → types-DVIx8I1N.d.mts} +4 -274
  67. package/dist/types-DVIx8I1N.d.mts.map +1 -0
  68. package/dist/{types-CL7jPFM2.d.mts → types-PMP_qXRY.d.cts} +4 -274
  69. package/dist/types-PMP_qXRY.d.cts.map +1 -0
  70. package/dist/workspace/index.cjs +1 -1
  71. package/dist/workspace/index.d.cts +3 -3
  72. package/dist/workspace/index.d.mts +3 -3
  73. package/dist/workspace/index.mjs +1 -1
  74. package/dist/{workspace-DQxxs7Jk.cjs → workspace-Cji6lE8E.cjs} +4 -4
  75. package/dist/workspace-CkJAtRIX.mjs +9 -0
  76. package/dist/{workspace-DaEX7FEg.mjs → workspace-CtnZnWsa.mjs} +16 -68
  77. package/dist/workspace-CtnZnWsa.mjs.map +1 -0
  78. package/dist/{workspace-C-a3G_U0.cjs → workspace-DPV7FtAv.cjs} +16 -68
  79. package/dist/workspace-DPV7FtAv.cjs.map +1 -0
  80. package/package.json +11 -11
  81. package/dist/bundler-CaTGtiaO.cjs.map +0 -1
  82. package/dist/bundler-S3ek7ea-.mjs.map +0 -1
  83. package/dist/credentials-B2Pyh3bJ.cjs.map +0 -1
  84. package/dist/credentials-CZrys9C5.mjs.map +0 -1
  85. package/dist/discover-CAlFgYec.mjs.map +0 -1
  86. package/dist/discover-DTmFeloL.cjs.map +0 -1
  87. package/dist/index-Ayxg_DQY.d.mts.map +0 -1
  88. package/dist/index-CR5urhWX.d.cts.map +0 -1
  89. package/dist/setup-BWgMs4gg.mjs +0 -12
  90. package/dist/setup-DOv92pIu.cjs.map +0 -1
  91. package/dist/setup-DQo_h_yK.mjs.map +0 -1
  92. package/dist/setup-lCx_DcUo.cjs +0 -12
  93. package/dist/types-CL7jPFM2.d.mts.map +0 -1
  94. package/dist/types-Y3fKQSAK.d.cts.map +0 -1
  95. package/dist/workspace-C-a3G_U0.cjs.map +0 -1
  96. package/dist/workspace-DaEX7FEg.mjs.map +0 -1
  97. package/dist/workspace-DbI580qS.mjs +0 -9
package/dist/index.cjs CHANGED
@@ -1,22 +1,22 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  const require_chunk = require('./chunk-CUT6urMc.cjs');
3
- const require_discover = require('./discover-DTmFeloL.cjs');
4
- const require_config = require('./config-HhDpJmLO.cjs');
5
- const require_workspace = require('./workspace-C-a3G_U0.cjs');
3
+ const require_discover = require('./discover-DmW9Bscc.cjs');
4
+ const require_config = require('./config-RYR9j6EF.cjs');
5
+ const require_workspace = require('./workspace-DPV7FtAv.cjs');
6
6
  const require_credentials = require('./credentials-BapzjyV5.cjs');
7
- const require_credentials$1 = require('./credentials-B2Pyh3bJ.cjs');
7
+ const require_credentials$1 = require('./credentials-DQRvPz2S.cjs');
8
8
  const require_generator = require('./generator-CNzBw7g5.cjs');
9
9
  const require_keystore = require('./keystore-C9rint2E.cjs');
10
10
  const require_storage = require('./storage-CSNplaJn.cjs');
11
- const require_openapi = require('./openapi-DaKlTKxz.cjs');
11
+ const require_openapi = require('./openapi-DWLTsYLL.cjs');
12
12
  const require_emit = require('./emit-CUD1BWls.cjs');
13
13
  const require_dokploy_api = require('./dokploy-api-zqKe9lHH.cjs');
14
14
  const require_encryption = require('./encryption-D2pL3tHH.cjs');
15
15
  const require_CachedStateProvider = require('./CachedStateProvider-BEBl2wQT.cjs');
16
- const require_fullstack_secrets = require('./fullstack-secrets-D6MyHhQP.cjs');
17
- const require_openapi_react_query = require('./openapi-react-query-BKH334Ug.cjs');
16
+ const require_openapi_react_query = require('./openapi-react-query-CmJ1xDbn.cjs');
17
+ const require_fullstack_secrets = require('./fullstack-secrets-DL8v0y4D.cjs');
18
18
  require('./sync-0ILCb-1S.cjs');
19
- const require_setup = require('./setup-DOv92pIu.cjs');
19
+ const require_setup = require('./setup-BsNR2LBG.cjs');
20
20
  const node_fs = require_chunk.__toESM(require("node:fs"));
21
21
  const node_path = require_chunk.__toESM(require("node:path"));
22
22
  const __geekmidas_manifest = require_chunk.__toESM(require("@geekmidas/manifest"));
@@ -28,9 +28,9 @@ const node_fs_promises = require_chunk.__toESM(require("node:fs/promises"));
28
28
  const node_child_process = require_chunk.__toESM(require("node:child_process"));
29
29
  const chokidar = require_chunk.__toESM(require("chokidar"));
30
30
  const yaml = require_chunk.__toESM(require("yaml"));
31
- const pg = require_chunk.__toESM(require("pg"));
32
31
  const node_crypto = require_chunk.__toESM(require("node:crypto"));
33
32
  const __geekmidas_db_pg_roles = require_chunk.__toESM(require("@geekmidas/db/pg/roles"));
33
+ const pg = require_chunk.__toESM(require("pg"));
34
34
  const __geekmidas_cache_postgres = require_chunk.__toESM(require("@geekmidas/cache/postgres"));
35
35
  const __geekmidas_constructs_crons = require_chunk.__toESM(require("@geekmidas/constructs/crons"));
36
36
  const __geekmidas_constructs_functions = require_chunk.__toESM(require("@geekmidas/constructs/functions"));
@@ -44,7 +44,7 @@ const prompts = require_chunk.__toESM(require("prompts"));
44
44
 
45
45
  //#region package.json
46
46
  var name = "@geekmidas/cli";
47
- var version = "10.0.0-alpha.11";
47
+ var version = "10.0.0-alpha.12";
48
48
  var description = "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs";
49
49
  var private$1 = false;
50
50
  var type = "module";
@@ -1849,7 +1849,7 @@ async function buildServer(config, context, provider, enableOpenApi, appRoot = p
1849
1849
  ...context,
1850
1850
  storageDrivers: driversFor({
1851
1851
  appRoot,
1852
- cache: cacheBackendsIn(await declaredConstructs(config, appRoot), require_credentials$1.cacheBackendOf(config?.services?.cache))
1852
+ cache: cacheBackendsIn(await declaredConstructs(config, appRoot), require_credentials$1.cacheBackendFor(require_credentials$1.providerOf(config ?? {})))
1853
1853
  })
1854
1854
  };
1855
1855
  const endpointGenerator = new require_openapi.EndpointGenerator();
@@ -2459,8 +2459,8 @@ function rootGkmConfig(workspace) {
2459
2459
  const only = backends.length === 1 ? backends[0] : void 0;
2460
2460
  const own = only ? require_workspace.getAppGkmConfig(workspace, only[0]) : void 0;
2461
2461
  return own ?? {
2462
- services: workspace.services,
2463
2462
  stages: workspace.stages,
2463
+ ...workspace.deploy ? { deploy: workspace.deploy } : {},
2464
2464
  constructs: require_workspace.allConstructGlobs(workspace)
2465
2465
  };
2466
2466
  }
@@ -2478,8 +2478,7 @@ async function buildCommand(options) {
2478
2478
  }
2479
2479
  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;
2480
2480
  const resolved = resolveProviders(config, options);
2481
- const backendConfig = config.services ?? loadedConfig.workspace?.services;
2482
- const cacheBackend = require_credentials$1.cacheBackendOf(backendConfig?.cache);
2481
+ const cacheBackend = require_credentials$1.cacheBackendFor(require_credentials$1.providerOf(loadedConfig.workspace));
2483
2482
  const productionConfigFromGkm = getProductionConfigFromGkm(config);
2484
2483
  const production = normalizeProductionConfig(options.production ?? false, productionConfigFromGkm);
2485
2484
  if (production) logger$7.log(`🏭 Building for PRODUCTION`);
@@ -2491,16 +2490,6 @@ async function buildCommand(options) {
2491
2490
  if (studio) logger$7.log(`🗄️ Studio enabled at ${studio.path}`);
2492
2491
  const hooks = normalizeHooksConfig(config.hooks);
2493
2492
  if (hooks) logger$7.log(`🪝 Server hooks enabled`);
2494
- const services = config.docker?.compose?.services;
2495
- const dockerServices = services ? Array.isArray(services) ? {
2496
- postgres: services.includes("postgres"),
2497
- redis: services.includes("redis"),
2498
- rabbitmq: services.includes("rabbitmq")
2499
- } : {
2500
- postgres: Boolean(services.postgres),
2501
- redis: Boolean(services.redis),
2502
- rabbitmq: Boolean(services.rabbitmq)
2503
- } : void 0;
2504
2493
  const constructGlobs$1 = typeof config.constructs === "string" || Array.isArray(config.constructs) ? config.constructs : void 0;
2505
2494
  const constructSources = {};
2506
2495
  const declared = constructGlobs$1 ? await require_discover.discover({
@@ -2545,10 +2534,9 @@ async function buildCommand(options) {
2545
2534
  } : studio,
2546
2535
  hooks,
2547
2536
  production,
2548
- dockerServices,
2549
2537
  constructGlobs: constructGlobs$1,
2550
2538
  cacheBackend,
2551
- emailBackend: require_credentials$1.emailBackendOf(backendConfig?.mail),
2539
+ emailBackend: require_discover.DEFAULT_EMAIL,
2552
2540
  storageDrivers: driversFor({
2553
2541
  appRoot: process.cwd(),
2554
2542
  cache: cacheBackendsIn(declared, cacheBackend)
@@ -2640,7 +2628,7 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
2640
2628
  let masterKey;
2641
2629
  if (context.production?.bundle && !skipBundle) {
2642
2630
  logger$7.log(`\n📦 Bundling production server...`);
2643
- const { bundleServer } = await Promise.resolve().then(() => require("./bundler-CaTGtiaO.cjs"));
2631
+ const { bundleServer } = await Promise.resolve().then(() => require("./bundler-BMekgx7a.cjs"));
2644
2632
  const allConstructs = [
2645
2633
  ...endpoints.map((e) => e.construct),
2646
2634
  ...functions.map((f) => f.construct),
@@ -2648,7 +2636,6 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
2648
2636
  ...subscribers.map((s) => s.construct),
2649
2637
  ...queues.map((q) => q.construct)
2650
2638
  ];
2651
- const dockerServices = context.dockerServices;
2652
2639
  const bundleResult = await bundleServer({
2653
2640
  entryPoint: (0, node_path.join)(outputDir, "server.ts"),
2654
2641
  outputDir: (0, node_path.join)(outputDir, "dist"),
@@ -2656,8 +2643,7 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
2656
2643
  sourcemap: false,
2657
2644
  external: context.production.external,
2658
2645
  stage,
2659
- constructs: allConstructs,
2660
- dockerServices
2646
+ constructs: allConstructs
2661
2647
  });
2662
2648
  masterKey = bundleResult.masterKey;
2663
2649
  logger$7.log(`✅ Bundle complete: .gkm/server/dist/server.mjs`);
@@ -2734,71 +2720,6 @@ function turboFilters(workspace) {
2734
2720
  };
2735
2721
  }
2736
2722
  /**
2737
- * The directory turbo treats as the repo root: the nearest ancestor with a
2738
- * `turbo.json`. Its own path is what `$TURBO_ROOT$` resolves to.
2739
- */
2740
- function turboRoot(from) {
2741
- let dir = (0, node_path.resolve)(from);
2742
- for (;;) {
2743
- if ((0, node_fs.existsSync)((0, node_path.join)(dir, "turbo.json"))) return dir;
2744
- const parent = (0, node_path.dirname)(dir);
2745
- if (parent === dir) return void 0;
2746
- dir = parent;
2747
- }
2748
- }
2749
- /**
2750
- * Write each app's turbo task config, so a construct edit invalidates the build
2751
- * that read it.
2752
- *
2753
- * Turbo hashes a package's own files. An app's constructs live *above* it, in
2754
- * the workspace that declares them for everybody — so without this, editing a
2755
- * database or a surface leaves every cached app build in place and the apps
2756
- * keep serving a graph that no longer exists. It is a silent wrong answer,
2757
- * which is the worst kind: the build succeeds and the output is stale.
2758
- *
2759
- * Generated rather than documented because the globs are already known here and
2760
- * a hand-written copy is one more list to keep in step — the same reason the
2761
- * apps themselves are no longer written down. A file that someone has since
2762
- * edited by hand is left alone.
2763
- */
2764
- async function writeTurboConfigs(workspace) {
2765
- const root = turboRoot(workspace.root);
2766
- if (!root) return;
2767
- const globs = require_workspace.allConstructGlobs(workspace).map((glob) => `$TURBO_ROOT$/${(0, node_path.relative)(root, glob)}`);
2768
- if (globs.length === 0) return;
2769
- const configPath = (0, node_path.join)(workspace.root, "gkm.config.ts");
2770
- const inputs = [
2771
- "$TURBO_DEFAULT$",
2772
- ...globs,
2773
- ...(0, node_fs.existsSync)(configPath) ? [`$TURBO_ROOT$/${(0, node_path.relative)(root, configPath)}`] : []
2774
- ];
2775
- for (const app of Object.values(workspace.apps)) {
2776
- const dir = (0, node_path.join)(workspace.root, app.path);
2777
- if (!(0, node_fs.existsSync)((0, node_path.join)(dir, "package.json"))) continue;
2778
- const file = (0, node_path.join)(dir, "turbo.json");
2779
- const existing = (0, node_fs.existsSync)(file) ? (0, node_fs.readFileSync)(file, "utf8") : void 0;
2780
- if (existing && !existing.includes(GENERATED_BY_GKM)) continue;
2781
- const content = `{
2782
- "$schema": "https://turborepo.com/schema.json",
2783
- ${GENERATED_BY_GKM}
2784
- "extends": ["//"],
2785
- "tasks": {
2786
- "build": {
2787
- "inputs": [
2788
- ${inputs.map((glob) => `\t\t\t\t${JSON.stringify(glob)}`).join(",\n")}
2789
- ],
2790
- "outputs": [${app.type === "backend" ? "\".gkm/**\"" : "\"dist/**\", \".next/**\", \"!.next/cache/**\""}]
2791
- }
2792
- }
2793
- }
2794
- `;
2795
- if (existing === content) continue;
2796
- await (0, node_fs_promises.writeFile)(file, content);
2797
- }
2798
- }
2799
- /** Marks a turbo config as gkm's to rewrite. Edit the file and it is left alone. */
2800
- const GENERATED_BY_GKM = "// Generated by `gkm build`. Delete this line to take ownership of the file.";
2801
- /**
2802
2723
  * Build all apps in a workspace using Turbo for dependency-ordered parallel builds.
2803
2724
  * @internal Exported for testing
2804
2725
  */
@@ -2816,7 +2737,6 @@ async function workspaceBuildCommand(workspace, options) {
2816
2737
  const pm = detectPackageManager$2();
2817
2738
  logger$7.log(`\n📦 Using ${pm} with Turbo for parallel builds...\n`);
2818
2739
  try {
2819
- await writeTurboConfigs(workspace);
2820
2740
  const { filters, unpackaged } = turboFilters(workspace);
2821
2741
  if (unpackaged.length > 0) throw new Error(`No package.json for workspace app(s): ${unpackaged.join(", ")}. Each app needs one — turbo builds packages, and gkm reads the name back to know which app it is building.`);
2822
2742
  const turboCommand = getTurboCommand(pm, filters);
@@ -3309,16 +3229,16 @@ var UnresolvedParent = class extends Error {
3309
3229
  /** A cache with nowhere to live. */
3310
3230
  var CacheNeedsAHome = class extends Error {
3311
3231
  constructor(id, backend) {
3312
- super(`'${id}' is a cache and nothing says where it lives. Declare it from its database — \`orders.cache('${id}')\` — or set services.cache.` + (backend ? ` The configured backend is '${backend}'.` : ""));
3232
+ super(`'${id}' is a cache and nothing says where it lives. Declare it from its database — \`orders.cache('${id}')\`.` + (backend ? ` The deploy target's backend is '${backend}'.` : ""));
3313
3233
  this.id = id;
3314
3234
  this.backend = backend;
3315
3235
  this.name = "CacheNeedsAHome";
3316
3236
  }
3317
3237
  };
3318
- /** `services.cache: 'db'` in an app declaring more than one database. */
3238
+ /** A `'db'` cache (a server target's default) with more than one database. */
3319
3239
  var CacheIsAmbiguous = class extends Error {
3320
3240
  constructor(databases) {
3321
- super(`A cache backed by the database needs to know which one, and this app declares ${databases.length}: ${databases.join(", ")}. Declare the cache from its database rather than with services.cache.`);
3241
+ super(`A cache backed by the database needs to know which one, and this app declares ${databases.length}: ${databases.join(", ")}. Declare the cache from its database rather than as a bare new Cache().`);
3322
3242
  this.databases = databases;
3323
3243
  this.name = "CacheIsAmbiguous";
3324
3244
  }
@@ -3334,7 +3254,7 @@ var SurfaceHasNoAddress = class extends Error {
3334
3254
  /** A queue or topic on a backend this target has no primitive for. */
3335
3255
  var UnprovisionableCarrier = class extends Error {
3336
3256
  constructor(id, backend) {
3337
- super(`'${id}' is carried by '${backend}', which this target cannot provision. SNS needs a topic ARN that has to exist first and RabbitMQ needs a broker Dokploy has no primitive for; pg-boss is a schema tenant of a database this app already declares, which is why it is the one that works here. Set services.events to 'pgboss', or deploy this app to a target that has the backend.`);
3257
+ super(`'${id}' is carried by '${backend}', which this target cannot provision. SNS needs a topic ARN that has to exist first and RabbitMQ needs a broker Dokploy has no primitive for; pg-boss is a schema tenant of a database this app already declares, which is why it is the one that works here. Deploy this app to a target that has the backend.`);
3338
3258
  this.id = id;
3339
3259
  this.backend = backend;
3340
3260
  this.name = "UnprovisionableCarrier";
@@ -3343,7 +3263,7 @@ var UnprovisionableCarrier = class extends Error {
3343
3263
  /** A pg-boss carrier in an app that declares no database to host it. */
3344
3264
  var BrokerNeedsADatabase = class extends Error {
3345
3265
  constructor(id) {
3346
- super(`'${id}' is carried by pg-boss, which lives in the database the app declares — and this app declares none. Declare a database, or set services.events to a backend that brings its own broker.`);
3266
+ super(`'${id}' is carried by pg-boss, which lives in the database the app declares — and this app declares none. Declare a database, or deploy to a target whose broker is its own.`);
3347
3267
  this.id = id;
3348
3268
  this.name = "BrokerNeedsADatabase";
3349
3269
  }
@@ -3351,7 +3271,7 @@ var BrokerNeedsADatabase = class extends Error {
3351
3271
  /** A bucket on a backend this target has no primitive for. */
3352
3272
  var UnprovisionableBucket = class extends Error {
3353
3273
  constructor(id, backend) {
3354
- super(`'${id}' is stored on '${backend}', which this target cannot provision: an S3 or R2 bucket belongs to an account this deploy does not hold, the way an API key does. Set services.storage to 'minio' to run one on the box, or supply the bucket's URL.`);
3274
+ super(`'${id}' is stored on '${backend}', which this target cannot provision: an S3 or R2 bucket belongs to an account this deploy does not hold, the way an API key does. Supply the bucket's URL.`);
3355
3275
  this.id = id;
3356
3276
  this.backend = backend;
3357
3277
  this.name = "UnprovisionableBucket";
@@ -3389,9 +3309,9 @@ async function provisionDeclared(options) {
3389
3309
  environmentId: options.environmentId,
3390
3310
  stage: options.stage,
3391
3311
  project: workspace.name,
3392
- cache: require_credentials$1.cacheBackendOf(workspace.services.cache, require_credentials$1.providerOf(workspace)),
3393
- ...workspace.services.events ? { events: workspace.services.events } : {},
3394
- storage: require_credentials$1.storageBackendOf(workspace.services.storage, require_credentials$1.providerOf(workspace)),
3312
+ cache: require_credentials$1.cacheBackendFor(require_credentials$1.providerOf(workspace)),
3313
+ events: require_credentials$1.eventsBackendFor(require_credentials$1.providerOf(workspace)),
3314
+ storage: require_credentials$1.storageBackendFor(require_credentials$1.providerOf(workspace)),
3395
3315
  addresses: surfaceAddresses(workspace, manifest, options.appUrls),
3396
3316
  ...options.secrets ? { secrets: options.secrets } : {},
3397
3317
  deferred: [],
@@ -4030,563 +3950,6 @@ function getPublicUrlArgNames(app) {
4030
3950
  return app.dependencies.map((dep) => `${prefix}${dep.toUpperCase()}_URL`);
4031
3951
  }
4032
3952
 
4033
- //#endregion
4034
- //#region src/docker/compose.ts
4035
- /** Default Docker images for services */
4036
- const DEFAULT_SERVICE_IMAGES = {
4037
- postgres: "postgres",
4038
- redis: "redis",
4039
- rabbitmq: "rabbitmq",
4040
- minio: "pgsty/minio",
4041
- mailpit: "axllent/mailpit",
4042
- localstack: "floci/floci"
4043
- };
4044
- /** Default Docker image versions for services */
4045
- const DEFAULT_SERVICE_VERSIONS = {
4046
- postgres: `${__geekmidas_manifest.DEFAULT_POSTGRES_VERSION}-alpine`,
4047
- redis: "7-alpine",
4048
- rabbitmq: "3-management-alpine",
4049
- minio: "RELEASE.2026-08-04T00-00-00Z",
4050
- mailpit: "latest",
4051
- localstack: "latest"
4052
- };
4053
- /** Get the default full image reference for a service */
4054
- function getDefaultImage(serviceName$1) {
4055
- return `${DEFAULT_SERVICE_IMAGES[serviceName$1]}:${DEFAULT_SERVICE_VERSIONS[serviceName$1]}`;
4056
- }
4057
- /** Normalize services config to a consistent format - returns Map of service name to full image reference */
4058
- function normalizeServices(services) {
4059
- const result = /* @__PURE__ */ new Map();
4060
- if (Array.isArray(services)) for (const name$1 of services) result.set(name$1, getDefaultImage(name$1));
4061
- else for (const [name$1, config] of Object.entries(services)) {
4062
- const serviceName$1 = name$1;
4063
- if (config === true) result.set(serviceName$1, getDefaultImage(serviceName$1));
4064
- else if (config && typeof config === "object") {
4065
- const serviceConfig = config;
4066
- if (serviceConfig.image) result.set(serviceName$1, serviceConfig.image);
4067
- else {
4068
- const version$1 = serviceConfig.version ?? DEFAULT_SERVICE_VERSIONS[serviceName$1];
4069
- result.set(serviceName$1, `${DEFAULT_SERVICE_IMAGES[serviceName$1]}:${version$1}`);
4070
- }
4071
- }
4072
- }
4073
- return result;
4074
- }
4075
- /**
4076
- * Generate docker-compose.yml for production deployment
4077
- */
4078
- function generateDockerCompose(options) {
4079
- const { imageName, registry, port, healthCheckPath, services } = options;
4080
- const serviceMap = normalizeServices(services);
4081
- const imageRef = registry ? `\${REGISTRY:-${registry}}/` : "";
4082
- let yaml$1 = `# Use "gkm dev" or "gkm test" to start services.
4083
- # Running "docker compose up" directly will not inject secrets or resolve ports.
4084
- version: '3.8'
4085
-
4086
- services:
4087
- api:
4088
- build:
4089
- context: ../..
4090
- dockerfile: .gkm/docker/Dockerfile
4091
- image: ${imageRef}\${IMAGE_NAME:-${imageName}}:\${TAG:-latest}
4092
- container_name: ${imageName}
4093
- restart: unless-stopped
4094
- ports:
4095
- - "\${PORT:-${port}}:${port}"
4096
- environment:
4097
- - NODE_ENV=production
4098
- `;
4099
- if (serviceMap.has("postgres")) yaml$1 += ` - DATABASE_URL=\${DATABASE_URL:-postgresql://\${POSTGRES_USER:-postgres}:\${POSTGRES_PASSWORD:-postgres}@postgres:5432/\${POSTGRES_DB:-app}}
4100
- `;
4101
- if (serviceMap.has("redis")) yaml$1 += ` - REDIS_URL=\${REDIS_URL:-redis://redis:6379}
4102
- `;
4103
- if (serviceMap.has("rabbitmq")) yaml$1 += ` - RABBITMQ_URL=\${RABBITMQ_URL:-amqp://rabbitmq:5672}
4104
- `;
4105
- if (serviceMap.has("minio")) yaml$1 += ` - STORAGE_ENDPOINT=\${STORAGE_ENDPOINT:-http://minio:9000}
4106
- - STORAGE_ACCESS_KEY_ID=\${STORAGE_ACCESS_KEY_ID:-${imageName}}
4107
- - STORAGE_SECRET_ACCESS_KEY=\${STORAGE_SECRET_ACCESS_KEY:-${imageName}}
4108
- - STORAGE_BUCKET=\${STORAGE_BUCKET:-${imageName}}
4109
- - STORAGE_REGION=\${STORAGE_REGION:-eu-west-1}
4110
- - STORAGE_FORCE_PATH_STYLE=true
4111
- `;
4112
- if (serviceMap.has("mailpit")) yaml$1 += ` - SMTP_HOST=\${SMTP_HOST:-mailpit}
4113
- - SMTP_PORT=\${SMTP_PORT:-1025}
4114
- - SMTP_USER=\${SMTP_USER:-${imageName}}
4115
- - SMTP_PASS=\${SMTP_PASS:-${imageName}}
4116
- - SMTP_SECURE=\${SMTP_SECURE:-false}
4117
- - MAIL_FROM=\${MAIL_FROM:-noreply@localhost}
4118
- `;
4119
- if (serviceMap.has("localstack")) yaml$1 += ` - AWS_ACCESS_KEY_ID=\${AWS_ACCESS_KEY_ID:-localstack}
4120
- - AWS_SECRET_ACCESS_KEY=\${AWS_SECRET_ACCESS_KEY:-localstack}
4121
- - AWS_REGION=\${AWS_REGION:-us-east-1}
4122
- - AWS_ENDPOINT_URL=\${AWS_ENDPOINT_URL:-http://localstack:4566}
4123
- `;
4124
- yaml$1 += ` healthcheck:
4125
- test: ["CMD", "wget", "-q", "--spider", "http://localhost:${port}${healthCheckPath}"]
4126
- interval: 30s
4127
- timeout: 3s
4128
- retries: 3
4129
- `;
4130
- if (serviceMap.size > 0) {
4131
- yaml$1 += ` depends_on:
4132
- `;
4133
- for (const serviceName$1 of serviceMap.keys()) yaml$1 += ` ${serviceName$1}:
4134
- condition: service_healthy
4135
- `;
4136
- }
4137
- yaml$1 += ` networks:
4138
- - app-network
4139
- `;
4140
- const postgresImage = serviceMap.get("postgres");
4141
- if (postgresImage) yaml$1 += `
4142
- postgres:
4143
- image: ${postgresImage}
4144
- container_name: postgres
4145
- restart: unless-stopped
4146
- environment:
4147
- POSTGRES_USER: \${POSTGRES_USER:-postgres}
4148
- POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-postgres}
4149
- POSTGRES_DB: \${POSTGRES_DB:-app}
4150
- volumes:
4151
- - dbdata:/var/lib/postgresql
4152
- healthcheck:
4153
- test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
4154
- interval: 5s
4155
- timeout: 5s
4156
- retries: 5
4157
- networks:
4158
- - app-network
4159
- `;
4160
- const redisImage = serviceMap.get("redis");
4161
- if (redisImage) yaml$1 += `
4162
- redis:
4163
- image: ${redisImage}
4164
- container_name: redis
4165
- restart: unless-stopped
4166
- volumes:
4167
- - redis_data:/data
4168
- healthcheck:
4169
- test: ["CMD", "redis-cli", "ping"]
4170
- interval: 5s
4171
- timeout: 5s
4172
- retries: 5
4173
- networks:
4174
- - app-network
4175
- `;
4176
- const rabbitmqImage = serviceMap.get("rabbitmq");
4177
- if (rabbitmqImage) yaml$1 += `
4178
- rabbitmq:
4179
- image: ${rabbitmqImage}
4180
- container_name: rabbitmq
4181
- restart: unless-stopped
4182
- environment:
4183
- RABBITMQ_DEFAULT_USER: \${RABBITMQ_USER:-guest}
4184
- RABBITMQ_DEFAULT_PASS: \${RABBITMQ_PASSWORD:-guest}
4185
- ports:
4186
- - "15672:15672" # Management UI
4187
- volumes:
4188
- - rabbitmq_data:/var/lib/rabbitmq
4189
- healthcheck:
4190
- test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
4191
- interval: 10s
4192
- timeout: 5s
4193
- retries: 5
4194
- networks:
4195
- - app-network
4196
- `;
4197
- const minioImage = serviceMap.get("minio");
4198
- if (minioImage) yaml$1 += `
4199
- minio:
4200
- image: ${minioImage}
4201
- container_name: minio
4202
- restart: unless-stopped
4203
- entrypoint: sh
4204
- command: -c 'mkdir -p /data/\${STORAGE_BUCKET:-${imageName}} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /data'
4205
- environment:
4206
- MINIO_ROOT_USER: \${STORAGE_ACCESS_KEY_ID:-${imageName}}
4207
- MINIO_ROOT_PASSWORD: \${STORAGE_SECRET_ACCESS_KEY:-${imageName}}
4208
- ports:
4209
- - "\${MINIO_API_PORT:-9000}:9000"
4210
- - "\${MINIO_CONSOLE_PORT:-9001}:9001"
4211
- volumes:
4212
- - minio_data:/data
4213
- healthcheck:
4214
- test: ["CMD", "mc", "ready", "local"]
4215
- interval: 10s
4216
- timeout: 5s
4217
- retries: 5
4218
- networks:
4219
- - app-network
4220
- `;
4221
- const mailpitImage = serviceMap.get("mailpit");
4222
- if (mailpitImage) yaml$1 += `
4223
- mailpit:
4224
- image: ${mailpitImage}
4225
- container_name: mailpit
4226
- restart: unless-stopped
4227
- environment:
4228
- MP_SMTP_AUTH: \${SMTP_USER:-${imageName}}:\${SMTP_PASS:-${imageName}}
4229
- MP_SMTP_AUTH_ALLOW_INSECURE: true
4230
- ports:
4231
- - "\${MAILPIT_PORT:-8025}:8025" # Web UI / API
4232
- - "\${SMTP_PORT:-1025}:1025" # SMTP
4233
- healthcheck:
4234
- test: ["CMD", "wget", "-q", "--spider", "http://localhost:8025"]
4235
- interval: 5s
4236
- timeout: 5s
4237
- retries: 5
4238
- networks:
4239
- - app-network
4240
- `;
4241
- const localstackImage = serviceMap.get("localstack");
4242
- if (localstackImage) yaml$1 += `
4243
- localstack:
4244
- image: ${localstackImage}
4245
- container_name: localstack
4246
- restart: unless-stopped
4247
- environment:
4248
- AWS_DEFAULT_REGION: \${AWS_REGION:-us-east-1}
4249
- AWS_ACCESS_KEY_ID: \${AWS_ACCESS_KEY_ID:-localstack}
4250
- AWS_SECRET_ACCESS_KEY: \${AWS_SECRET_ACCESS_KEY:-localstack}
4251
- ports:
4252
- - "\${LOCALSTACK_PORT:-4566}:4566"
4253
- volumes:
4254
- - localstack_data:/var/lib/localstack
4255
- healthcheck:
4256
- test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
4257
- interval: 10s
4258
- timeout: 5s
4259
- retries: 5
4260
- networks:
4261
- - app-network
4262
- `;
4263
- yaml$1 += `
4264
- volumes:
4265
- `;
4266
- if (serviceMap.has("postgres")) yaml$1 += ` dbdata:
4267
- `;
4268
- if (serviceMap.has("redis")) yaml$1 += ` redis_data:
4269
- `;
4270
- if (serviceMap.has("rabbitmq")) yaml$1 += ` rabbitmq_data:
4271
- `;
4272
- if (serviceMap.has("minio")) yaml$1 += ` minio_data:
4273
- `;
4274
- if (serviceMap.has("localstack")) yaml$1 += ` localstack_data:
4275
- `;
4276
- yaml$1 += `
4277
- networks:
4278
- app-network:
4279
- driver: bridge
4280
- `;
4281
- return yaml$1;
4282
- }
4283
- /**
4284
- * Generate a minimal docker-compose.yml for API only
4285
- */
4286
- function generateMinimalDockerCompose(options) {
4287
- const { imageName, registry, port, healthCheckPath } = options;
4288
- const imageRef = registry ? `\${REGISTRY:-${registry}}/` : "";
4289
- return `# Use "gkm dev" or "gkm test" to start services.
4290
- # Running "docker compose up" directly will not inject secrets or resolve ports.
4291
- version: '3.8'
4292
-
4293
- services:
4294
- api:
4295
- build:
4296
- context: ../..
4297
- dockerfile: .gkm/docker/Dockerfile
4298
- image: ${imageRef}\${IMAGE_NAME:-${imageName}}:\${TAG:-latest}
4299
- container_name: ${imageName}
4300
- restart: unless-stopped
4301
- ports:
4302
- - "\${PORT:-${port}}:${port}"
4303
- environment:
4304
- - NODE_ENV=production
4305
- healthcheck:
4306
- test: ["CMD", "wget", "-q", "--spider", "http://localhost:${port}${healthCheckPath}"]
4307
- interval: 30s
4308
- timeout: 3s
4309
- retries: 3
4310
- networks:
4311
- - app-network
4312
-
4313
- networks:
4314
- app-network:
4315
- driver: bridge
4316
- `;
4317
- }
4318
- /**
4319
- * Generate docker-compose.yml for a workspace with all apps as services.
4320
- * Apps can communicate with each other via service names.
4321
- * @internal Exported for testing
4322
- */
4323
- function generateWorkspaceCompose(workspace, options = {}) {
4324
- const { registry } = options;
4325
- const apps = Object.entries(workspace.apps);
4326
- const services = workspace.services;
4327
- const containers = new Set(options.containers ?? []);
4328
- const hasPostgres = containers.has("postgres");
4329
- const hasRedis = containers.has("redis") || containers.has("redis-http");
4330
- const hasMail = containers.has("mailpit");
4331
- const hasMinio = containers.has("minio");
4332
- const hasLocalStack = containers.has("localstack");
4333
- const hasRabbitMQ = containers.has("rabbitmq");
4334
- const eventsBackend = services.events;
4335
- const postgresImage = getInfraServiceImage("postgres", services.images?.postgres);
4336
- const redisImage = getInfraServiceImage("redis", services.images?.redis);
4337
- const minioImage = getInfraServiceImage("minio", services.images?.minio);
4338
- let yaml$1 = `# Docker Compose for ${workspace.name} workspace
4339
- # Use "gkm dev" or "gkm test" to start services.
4340
- # Running "docker compose up" directly will not inject secrets or resolve ports.
4341
-
4342
- services:
4343
- `;
4344
- for (const [appName, app] of apps) {
4345
- if (app.type === "mobile") continue;
4346
- yaml$1 += generateAppService(appName, app, apps, {
4347
- registry,
4348
- projectName: workspace.name,
4349
- hasPostgres,
4350
- hasRedis,
4351
- hasMinio,
4352
- hasMail,
4353
- eventsBackend
4354
- });
4355
- }
4356
- if (hasPostgres) yaml$1 += `
4357
- postgres:
4358
- image: ${postgresImage}
4359
- container_name: ${workspace.name}-postgres
4360
- restart: unless-stopped
4361
- environment:
4362
- POSTGRES_USER: \${POSTGRES_USER:-postgres}
4363
- POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-postgres}
4364
- POSTGRES_DB: \${POSTGRES_DB:-app}
4365
- volumes:
4366
- - dbdata:/var/lib/postgresql
4367
- healthcheck:
4368
- test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
4369
- interval: 5s
4370
- timeout: 5s
4371
- retries: 5
4372
- networks:
4373
- - workspace-network
4374
- `;
4375
- if (hasRedis) yaml$1 += `
4376
- redis:
4377
- image: ${redisImage}
4378
- container_name: ${workspace.name}-redis
4379
- restart: unless-stopped
4380
- volumes:
4381
- - redis_data:/data
4382
- healthcheck:
4383
- test: ["CMD", "redis-cli", "ping"]
4384
- interval: 5s
4385
- timeout: 5s
4386
- retries: 5
4387
- networks:
4388
- - workspace-network
4389
- `;
4390
- if (hasMail) yaml$1 += `
4391
- mailpit:
4392
- image: axllent/mailpit:latest
4393
- container_name: ${workspace.name}-mailpit
4394
- restart: unless-stopped
4395
- environment:
4396
- MP_SMTP_AUTH: \${SMTP_USER:-${workspace.name}}:\${SMTP_PASS:-${workspace.name}}
4397
- MP_SMTP_AUTH_ALLOW_INSECURE: true
4398
- ports:
4399
- - "\${MAILPIT_PORT:-8025}:8025" # Web UI / API
4400
- - "\${SMTP_PORT:-1025}:1025" # SMTP
4401
- healthcheck:
4402
- test: ["CMD", "wget", "-q", "--spider", "http://localhost:8025"]
4403
- interval: 5s
4404
- timeout: 5s
4405
- retries: 5
4406
- networks:
4407
- - workspace-network
4408
- `;
4409
- if (hasMinio) yaml$1 += `
4410
- minio:
4411
- image: ${minioImage}
4412
- container_name: ${workspace.name}-minio
4413
- restart: unless-stopped
4414
- entrypoint: sh
4415
- command: -c 'mkdir -p /data/\${STORAGE_BUCKET:-${workspace.name}} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /data'
4416
- environment:
4417
- MINIO_ROOT_USER: \${STORAGE_ACCESS_KEY_ID:-${workspace.name}}
4418
- MINIO_ROOT_PASSWORD: \${STORAGE_SECRET_ACCESS_KEY:-${workspace.name}}
4419
- ports:
4420
- - "\${MINIO_API_PORT:-9000}:9000"
4421
- - "\${MINIO_CONSOLE_PORT:-9001}:9001"
4422
- volumes:
4423
- - minio_data:/data
4424
- healthcheck:
4425
- test: ["CMD", "mc", "ready", "local"]
4426
- interval: 10s
4427
- timeout: 5s
4428
- retries: 5
4429
- networks:
4430
- - workspace-network
4431
- `;
4432
- if (hasLocalStack) yaml$1 += `
4433
- localstack:
4434
- image: floci/floci:latest
4435
- container_name: ${workspace.name}-localstack
4436
- restart: unless-stopped
4437
- environment:
4438
- AWS_DEFAULT_REGION: \${AWS_REGION:-us-east-1}
4439
- AWS_ACCESS_KEY_ID: \${AWS_ACCESS_KEY_ID:-localstack}
4440
- AWS_SECRET_ACCESS_KEY: \${AWS_SECRET_ACCESS_KEY:-localstack}
4441
- ports:
4442
- - "\${LOCALSTACK_PORT:-4566}:4566"
4443
- volumes:
4444
- - localstack_data:/var/lib/localstack
4445
- healthcheck:
4446
- test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
4447
- interval: 10s
4448
- timeout: 5s
4449
- retries: 5
4450
- networks:
4451
- - workspace-network
4452
- `;
4453
- if (hasRabbitMQ) yaml$1 += `
4454
- rabbitmq:
4455
- image: rabbitmq:3-management-alpine
4456
- container_name: ${workspace.name}-rabbitmq
4457
- restart: unless-stopped
4458
- environment:
4459
- RABBITMQ_DEFAULT_USER: \${RABBITMQ_USER:-guest}
4460
- RABBITMQ_DEFAULT_PASS: \${RABBITMQ_PASSWORD:-guest}
4461
- ports:
4462
- - "\${RABBITMQ_HOST_PORT:-5672}:5672"
4463
- - "\${RABBITMQ_MGMT_HOST_PORT:-15672}:15672"
4464
- volumes:
4465
- - rabbitmq_data:/var/lib/rabbitmq
4466
- healthcheck:
4467
- test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
4468
- interval: 10s
4469
- timeout: 5s
4470
- retries: 5
4471
- networks:
4472
- - workspace-network
4473
- `;
4474
- yaml$1 += `
4475
- volumes:
4476
- `;
4477
- if (hasPostgres) yaml$1 += ` dbdata:
4478
- `;
4479
- if (hasRedis) yaml$1 += ` redis_data:
4480
- `;
4481
- if (hasMinio) yaml$1 += ` minio_data:
4482
- `;
4483
- if (hasLocalStack) yaml$1 += ` localstack_data:
4484
- `;
4485
- if (hasRabbitMQ) yaml$1 += ` rabbitmq_data:
4486
- `;
4487
- yaml$1 += `
4488
- networks:
4489
- workspace-network:
4490
- driver: bridge
4491
- `;
4492
- return yaml$1;
4493
- }
4494
- /**
4495
- * Get infrastructure service image with version.
4496
- */
4497
- function getInfraServiceImage(serviceName$1, pin) {
4498
- const defaults = {
4499
- postgres: "postgres:18-alpine",
4500
- redis: "redis:7-alpine",
4501
- minio: "pgsty/minio:RELEASE.2026-08-04T00-00-00Z"
4502
- };
4503
- return pin || defaults[serviceName$1];
4504
- }
4505
- /**
4506
- * Generate a service definition for an app.
4507
- */
4508
- function generateAppService(appName, app, allApps, options) {
4509
- const { registry, projectName, hasPostgres, hasRedis, hasMinio, hasMail, eventsBackend } = options;
4510
- const imageRef = registry ? `\${REGISTRY:-${registry}}/` : "";
4511
- const healthCheckPath = app.type === "web" ? "/" : "/health";
4512
- const healthCheckCmd = app.type === "web" ? `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}/"]` : `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}${healthCheckPath}"]`;
4513
- let yaml$1 = `
4514
- ${appName}:
4515
- build:
4516
- context: .
4517
- dockerfile: .gkm/docker/Dockerfile.${appName}
4518
- image: ${imageRef}\${${appName.toUpperCase()}_IMAGE:-${appName}}:\${TAG:-latest}
4519
- container_name: ${appName}
4520
- restart: unless-stopped
4521
- ports:
4522
- - "\${${appName.toUpperCase()}_PORT:-${app.port}}:${app.port}"
4523
- environment:
4524
- - NODE_ENV=production
4525
- - PORT=${app.port}
4526
- `;
4527
- for (const dep of app.dependencies) {
4528
- const depApp = allApps.find(([name$1]) => name$1 === dep)?.[1];
4529
- if (depApp) yaml$1 += ` - ${dep.toUpperCase()}_URL=http://${dep}:${depApp.port}
4530
- `;
4531
- }
4532
- if (app.type === "backend") {
4533
- if (hasPostgres) yaml$1 += ` - DATABASE_URL=\${DATABASE_URL:-postgresql://\${POSTGRES_USER:-postgres}:\${POSTGRES_PASSWORD:-postgres}@postgres:5432/\${POSTGRES_DB:-app}}
4534
- `;
4535
- if (hasRedis) yaml$1 += ` - REDIS_URL=\${REDIS_URL:-redis://redis:6379}
4536
- `;
4537
- if (hasMinio) yaml$1 += ` - STORAGE_ENDPOINT=\${STORAGE_ENDPOINT:-http://minio:9000}
4538
- - STORAGE_ACCESS_KEY_ID=\${STORAGE_ACCESS_KEY_ID:-${projectName}}
4539
- - STORAGE_SECRET_ACCESS_KEY=\${STORAGE_SECRET_ACCESS_KEY:-${projectName}}
4540
- - STORAGE_BUCKET=\${STORAGE_BUCKET:-${projectName}}
4541
- - STORAGE_REGION=\${STORAGE_REGION:-eu-west-1}
4542
- - STORAGE_FORCE_PATH_STYLE=true
4543
- `;
4544
- if (hasMail) yaml$1 += ` - SMTP_HOST=\${SMTP_HOST:-mailpit}
4545
- - SMTP_PORT=\${SMTP_PORT:-1025}
4546
- - SMTP_USER=\${SMTP_USER:-${projectName}}
4547
- - SMTP_PASS=\${SMTP_PASS:-${projectName}}
4548
- - SMTP_SECURE=\${SMTP_SECURE:-false}
4549
- - MAIL_FROM=\${MAIL_FROM:-noreply@localhost}
4550
- `;
4551
- if (eventsBackend) {
4552
- yaml$1 += ` - EVENT_PUBLISHER_CONNECTION_STRING=\${EVENT_PUBLISHER_CONNECTION_STRING}
4553
- - EVENT_SUBSCRIBER_CONNECTION_STRING=\${EVENT_SUBSCRIBER_CONNECTION_STRING}
4554
- `;
4555
- if (eventsBackend === "sns") yaml$1 += ` - AWS_ACCESS_KEY_ID=\${AWS_ACCESS_KEY_ID:-localstack}
4556
- - AWS_SECRET_ACCESS_KEY=\${AWS_SECRET_ACCESS_KEY:-localstack}
4557
- - AWS_REGION=\${AWS_REGION:-us-east-1}
4558
- - AWS_ENDPOINT_URL=\${AWS_ENDPOINT_URL:-http://localstack:4566}
4559
- `;
4560
- }
4561
- }
4562
- yaml$1 += ` healthcheck:
4563
- test: ${healthCheckCmd}
4564
- interval: 30s
4565
- timeout: 3s
4566
- retries: 3
4567
- `;
4568
- const dependencies$1 = [...app.dependencies];
4569
- if (app.type === "backend") {
4570
- if (hasPostgres) dependencies$1.push("postgres");
4571
- if (hasRedis) dependencies$1.push("redis");
4572
- if (hasMinio) dependencies$1.push("minio");
4573
- if (hasMail) dependencies$1.push("mailpit");
4574
- if (eventsBackend === "sns") dependencies$1.push("localstack");
4575
- if (eventsBackend === "rabbitmq") dependencies$1.push("rabbitmq");
4576
- }
4577
- if (dependencies$1.length > 0) {
4578
- yaml$1 += ` depends_on:
4579
- `;
4580
- for (const dep of dependencies$1) yaml$1 += ` ${dep}:
4581
- condition: service_healthy
4582
- `;
4583
- }
4584
- yaml$1 += ` networks:
4585
- - workspace-network
4586
- `;
4587
- return yaml$1;
4588
- }
4589
-
4590
3953
  //#endregion
4591
3954
  //#region src/docker/templates.ts
4592
3955
  const LOCKFILES = [
@@ -5017,8 +4380,7 @@ function resolveDockerConfig(config) {
5017
4380
  registry: docker.registry ?? "",
5018
4381
  imageName: docker.imageName ?? defaultImageName,
5019
4382
  baseImage: docker.baseImage ?? "node:22-alpine",
5020
- port: docker.port ?? 3e3,
5021
- compose: docker.compose
4383
+ port: docker.port ?? 3e3
5022
4384
  };
5023
4385
  }
5024
4386
  /**
@@ -5460,7 +4822,7 @@ async function dockerCommand(options) {
5460
4822
  const loadedConfig = await require_config.loadWorkspaceConfig();
5461
4823
  if (loadedConfig.type === "workspace") {
5462
4824
  logger$5.log("📦 Detected workspace configuration");
5463
- return workspaceDockerCommand(loadedConfig.workspace, options);
4825
+ return workspaceDockerCommand(loadedConfig.workspace);
5464
4826
  }
5465
4827
  const config = await require_config.loadConfig();
5466
4828
  const dockerConfig = resolveDockerConfig(config);
@@ -5505,18 +4867,7 @@ async function dockerCommand(options) {
5505
4867
  const dockerfilePath = (0, node_path.join)(dockerDir, "Dockerfile");
5506
4868
  await (0, node_fs_promises.writeFile)(dockerfilePath, dockerfile);
5507
4869
  logger$5.log(`Generated: .gkm/docker/Dockerfile (${dockerMode}, ${packageManager})`);
5508
- const composeOptions = {
5509
- imageName: dockerConfig.imageName,
5510
- registry: options.registry ?? dockerConfig.registry,
5511
- port: dockerConfig.port,
5512
- healthCheckPath,
5513
- services: dockerConfig.compose?.services ?? {}
5514
- };
5515
- const hasServices = Array.isArray(composeOptions.services) ? composeOptions.services.length > 0 : Object.keys(composeOptions.services).length > 0;
5516
- const dockerCompose = hasServices ? generateDockerCompose(composeOptions) : generateMinimalDockerCompose(composeOptions);
5517
- const composePath = (0, node_path.join)(dockerDir, "docker-compose.yml");
5518
- await (0, node_fs_promises.writeFile)(composePath, dockerCompose);
5519
- logger$5.log("Generated: .gkm/docker/docker-compose.yml");
4870
+ const composePath = await writeConstructsCompose(loadedConfig.workspace);
5520
4871
  const dockerignore = generateDockerignore();
5521
4872
  const dockerignorePath = (0, node_path.join)(process.cwd(), ".dockerignore");
5522
4873
  await (0, node_fs_promises.writeFile)(dockerignorePath, dockerignore);
@@ -5621,7 +4972,7 @@ function getAppPackageName(appPath) {
5621
4972
  * Generate Dockerfiles for all apps in a workspace.
5622
4973
  * @internal Exported for testing
5623
4974
  */
5624
- async function workspaceDockerCommand(workspace, options) {
4975
+ async function workspaceDockerCommand(workspace) {
5625
4976
  const results = [];
5626
4977
  const apps = Object.entries(workspace.apps);
5627
4978
  logger$5.log(`\n🐳 Generating Dockerfiles for workspace: ${workspace.name}`);
@@ -5696,27 +5047,41 @@ async function workspaceDockerCommand(workspace, options) {
5696
5047
  const dockerignorePath = (0, node_path.join)(workspace.root, ".dockerignore");
5697
5048
  await (0, node_fs_promises.writeFile)(dockerignorePath, dockerignore);
5698
5049
  logger$5.log(`\n Generated: .dockerignore (workspace root)`);
5699
- const dockerCompose = generateWorkspaceCompose(workspace, {
5700
- registry: options.registry,
5701
- containers: await require_credentials$1.derivedContainers(workspace, workspace.stages.local)
5702
- });
5703
- const composePath = (0, node_path.join)(dockerDir, "docker-compose.yml");
5704
- await (0, node_fs_promises.writeFile)(composePath, dockerCompose);
5705
- logger$5.log(` Generated: .gkm/docker/docker-compose.yml`);
5706
- logger$5.log(`\n✅ Generated ${results.length} Dockerfile(s) + docker-compose.yml`);
5050
+ const composePath = await writeConstructsCompose(workspace);
5051
+ logger$5.log(`\n✅ Generated ${results.length} Dockerfile(s) + ${require_credentials$1.COMPOSE_PATH}`);
5707
5052
  logger$5.log("\n📋 Build commands:");
5708
5053
  for (const result of results) {
5709
5054
  const icon = result.type === "backend" ? "⚙️" : result.type === "mobile" ? "📱" : "🌐";
5710
5055
  logger$5.log(` ${icon} docker build -f .gkm/docker/Dockerfile.${result.appName} -t ${result.imageName} .`);
5711
5056
  }
5712
- logger$5.log("\n📋 Run all services:");
5713
- logger$5.log(" docker compose -f .gkm/docker/docker-compose.yml up --build");
5057
+ printRunInstructions(workspace);
5714
5058
  return {
5715
5059
  apps: results,
5716
5060
  dockerCompose: composePath,
5717
5061
  dockerignore: dockerignorePath
5718
5062
  };
5719
5063
  }
5064
+ /**
5065
+ * Write `docker-compose.constructs.yml` — the same file `gkm dev` writes, with
5066
+ * the apps beside the containers their constructs derive.
5067
+ *
5068
+ * Through reconcile rather than a second generator, so what `gkm docker`
5069
+ * describes and what `gkm dev` runs cannot disagree. Nothing is started.
5070
+ */
5071
+ async function writeConstructsCompose(workspace) {
5072
+ await require_credentials$1.reconcileWorkspace(workspace, {
5073
+ stage: workspace.stages.local,
5074
+ start: false
5075
+ });
5076
+ logger$5.log(` Generated: ${require_credentials$1.COMPOSE_PATH} (project root)`);
5077
+ return (0, node_path.join)(workspace.root, require_credentials$1.COMPOSE_PATH);
5078
+ }
5079
+ function printRunInstructions(workspace) {
5080
+ const project = (0, node_fs.existsSync)((0, node_path.join)(workspace.root, "docker-compose.yml")) ? " -f docker-compose.yml" : "";
5081
+ logger$5.log("\n📋 Run everything (the apps sit behind the \"apps\" profile):");
5082
+ logger$5.log(" gkm setup # starts the containers and creates what they hold");
5083
+ logger$5.log(` docker compose -f ${require_credentials$1.COMPOSE_PATH}${project} --profile apps up --build`);
5084
+ }
5720
5085
 
5721
5086
  //#endregion
5722
5087
  //#region src/deploy/docker.ts
@@ -7698,7 +7063,8 @@ the stale one is always the copy nobody opened.
7698
7063
  `;
7699
7064
  }
7700
7065
  function agentsContent(options, template) {
7701
- const { name: name$1, monorepo, database, telescope, studio, packageManager, services } = options;
7066
+ const { name: name$1, monorepo, telescope, studio, packageManager } = options;
7067
+ const { database } = options.constructs;
7702
7068
  const constructsGlob = monorepo ? WORKSPACE_CONSTRUCTS_GLOB : CONSTRUCTS_GLOB;
7703
7069
  const isWorker = template.name === "worker";
7704
7070
  const isServerless = template.name === "serverless";
@@ -7764,7 +7130,7 @@ ${run} gkm secrets:show
7764
7130
  environment and fail in a way that looks like a config bug. Go through
7765
7131
  \`gkm exec --\`, which is what injects them.`);
7766
7132
  if (!isWorker) sections.push(endpointSection(options));
7767
- if (isWorker || isServerless || services.events) sections.push(backgroundSection(template));
7133
+ if (isWorker || isServerless) sections.push(backgroundSection(template));
7768
7134
  if (database) sections.push(databaseSection());
7769
7135
  sections.push(`## Environment
7770
7136
 
@@ -7812,7 +7178,8 @@ reaches — and neither shows up until something reads the graph.`);
7812
7178
  return `${sections.join("\n\n")}\n`;
7813
7179
  }
7814
7180
  function layoutBlock(options, template) {
7815
- const { monorepo, apiPath, database } = options;
7181
+ const { monorepo, apiPath } = options;
7182
+ const { database } = options.constructs;
7816
7183
  const isWorker = template.name === "worker";
7817
7184
  if (monorepo) return `constructs/ # every construct: the surface, the database, topics
7818
7185
  ${apiPath}/ # the API app
@@ -7828,7 +7195,7 @@ gkm.config.ts # name, constructs glob, secrets`;
7828
7195
  return lines.join("\n");
7829
7196
  }
7830
7197
  function endpointSection(options) {
7831
- const { database } = options;
7198
+ const { database } = options.constructs;
7832
7199
  return `## Adding an endpoint
7833
7200
 
7834
7201
  An endpoint is built **from the surface that serves it**. That is where its
@@ -7984,26 +7351,110 @@ Run tests through \`gkm test\`, not \`vitest\` directly${database ? " — it is
7984
7351
  //#endregion
7985
7352
  //#region src/init/dependencies.ts
7986
7353
  /**
7987
- * Third-party ranges a scaffold installs beside the `@geekmidas` packages.
7354
+ * Every third-party range `gkm init` writes, in one place.
7988
7355
  *
7989
- * Held to the peer ranges those packages declare, in one place, because a
7990
- * template that pinned its own copy drifted below them: `kysely ~0.28` against
7991
- * a `~0.29.6` peer is two copies of Kysely and a `Kysely<DB>` from one that is
7992
- * not assignable to the other's.
7356
+ * Templates used to pin their own copies, and those drifted: below the peer
7357
+ * ranges the `@geekmidas` packages declare (`kysely ~0.28` against a `~0.29.6`
7358
+ * peer is two copies of Kysely, and a `Kysely<DB>` from one is not assignable
7359
+ * to the other's), and below what the rest of the scaffold assumed (`turbo
7360
+ * ~2.3.0` ignores `$TURBO_ROOT$`, which the root `turbo.json` relies on). No
7361
+ * generator writes a version literal; a scan test holds them to that.
7993
7362
  *
7994
7363
  * Hand-maintained, unlike `versions.ts`, which `sync-versions` rewrites whole.
7364
+ * Current as of 2026-09-27, inside every `@geekmidas` peer range.
7995
7365
  */
7996
7366
  const DEPENDENCY_VERSIONS = {
7997
- "@tanstack/react-query": "~5.80.0",
7998
- "better-auth": "~1.7.5",
7999
- hono: "~4.13.8",
7367
+ "better-auth": "~1.7.6",
7368
+ hono: "~4.13.9",
7369
+ "@hono/node-server": "~1.19.17",
8000
7370
  kysely: "~0.29.6",
8001
7371
  "kysely-ctl": "~0.21.0",
8002
7372
  pg: "~8.23.0",
8003
7373
  pino: "~10.3.1",
7374
+ zod: "~4.6.5",
8004
7375
  sst: "~4.17.1",
8005
- zod: "~4.6.5"
7376
+ next: "~16.3.6",
7377
+ react: "~19.3.0",
7378
+ "react-dom": "~19.3.0",
7379
+ "@tanstack/react-query": "~5.104.0",
7380
+ "@tanstack/react-router": "~1.170.40",
7381
+ "@tanstack/react-start": "~1.168.59",
7382
+ "@tanstack/router-plugin": "~1.168.41",
7383
+ tailwindcss: "~4.3.3",
7384
+ "@tailwindcss/postcss": "~4.3.3",
7385
+ "@tailwindcss/vite": "~4.3.3",
7386
+ "@radix-ui/react-dialog": "~1.1.23",
7387
+ "@radix-ui/react-label": "~2.1.15",
7388
+ "@radix-ui/react-separator": "~1.1.15",
7389
+ "@radix-ui/react-slot": "~1.3.3",
7390
+ "@radix-ui/react-tabs": "~1.1.21",
7391
+ "@radix-ui/react-tooltip": "~1.2.16",
7392
+ "class-variance-authority": "~0.7.1",
7393
+ clsx: "~2.1.1",
7394
+ "lucide-react": "~0.577.0",
7395
+ "tailwind-merge": "~3.7.0",
7396
+ "vite-tsconfig-paths": "~5.1.4",
7397
+ "@biomejs/biome": "~2.5.14",
7398
+ "@faker-js/faker": "~10.6.0",
7399
+ turbo: "~2.11.4",
7400
+ "@types/node": "~22.20.4",
7401
+ "@types/pg": "~8.23.1",
7402
+ "@types/react": "~19.3.0",
7403
+ "@types/react-dom": "~19.3.0",
7404
+ "@types/aws-lambda": "~8.10.163"
8006
7405
  };
7406
+ /**
7407
+ * The build and test toolchain, at the versions it was already on.
7408
+ *
7409
+ * Held here so it moves in one place, but not moved yet: it changes how every
7410
+ * scaffold compiles and runs, and tranche 2 (#40) moves it together with the
7411
+ * repository's own toolchain.
7412
+ */
7413
+ const TOOLCHAIN_VERSIONS = {
7414
+ typescript: "~5.8.2",
7415
+ tsx: "~4.20.0",
7416
+ esbuild: "~0.27.0",
7417
+ vitest: "~4.0.0",
7418
+ vite: "^7.0.0",
7419
+ "@vitejs/plugin-react": "^4.3.4",
7420
+ storybook: "^8.4.7",
7421
+ "@storybook/addon-a11y": "^8.4.7",
7422
+ "@storybook/addon-essentials": "^8.4.7",
7423
+ "@storybook/addon-interactions": "^8.4.7",
7424
+ "@storybook/react": "^8.4.7",
7425
+ "@storybook/react-vite": "^8.4.7",
7426
+ "vite@storybook": "^6.0.0"
7427
+ };
7428
+ /**
7429
+ * The Expo app's packages, which track an Expo SDK release (55) rather than
7430
+ * their own latest: SDK 55 is tested against exactly these, React Native and
7431
+ * React included, so they move when the SDK does.
7432
+ */
7433
+ const EXPO_VERSIONS = {
7434
+ expo: "~55.0.0",
7435
+ "expo-constants": "~55.0.0",
7436
+ "expo-dev-client": "~55.0.0",
7437
+ "expo-linking": "~55.0.0",
7438
+ "expo-router": "~55.0.0",
7439
+ "expo-splash-screen": "~55.0.0",
7440
+ "expo-status-bar": "~55.0.0",
7441
+ "expo-secure-store": "~14.2.0",
7442
+ react: "19.2.0",
7443
+ "react-dom": "19.2.0",
7444
+ "react-native": "0.83.6",
7445
+ "react-native-gesture-handler": "~2.30.0",
7446
+ "react-native-reanimated": "~4.2.0",
7447
+ "react-native-safe-area-context": "~5.6.0",
7448
+ "react-native-screens": "~4.23.0",
7449
+ "react-native-web": "~0.21.0",
7450
+ "@react-navigation/native": "^7.1.0",
7451
+ "@babel/core": "^7.25.0",
7452
+ "@types/react": "~19.0.0",
7453
+ nativewind: "~4.2.0",
7454
+ tailwindcss: "~3.4.0"
7455
+ };
7456
+ /** The Biome config schema matching the Biome the scaffold installs. */
7457
+ const BIOME_SCHEMA = `https://biomejs.dev/schemas/${DEPENDENCY_VERSIONS["@biomejs/biome"].replace(/^[~^]/, "")}/schema.json`;
8007
7458
 
8008
7459
  //#endregion
8009
7460
  //#region src/init/versions.ts
@@ -8027,26 +7478,26 @@ const CLI_VERSION = `~${pkg.version}`;
8027
7478
  * Run: pnpm --filter @geekmidas/cli sync-versions
8028
7479
  */
8029
7480
  const GEEKMIDAS_VERSIONS = {
8030
- "@geekmidas/audit": "~10.0.0-alpha.11",
8031
- "@geekmidas/auth": "~10.0.0-alpha.11",
8032
- "@geekmidas/cache": "~10.0.0-alpha.11",
8033
- "@geekmidas/client": "~10.0.0-alpha.11",
8034
- "@geekmidas/cloud": "~10.0.0-alpha.11",
8035
- "@geekmidas/constructs": "~10.0.0-alpha.11",
8036
- "@geekmidas/db": "~10.0.0-alpha.11",
8037
- "@geekmidas/emailkit": "~10.0.0-alpha.11",
8038
- "@geekmidas/envkit": "~10.0.0-alpha.11",
8039
- "@geekmidas/errors": "~10.0.0-alpha.11",
8040
- "@geekmidas/events": "~10.0.0-alpha.11",
8041
- "@geekmidas/logger": "~10.0.0-alpha.11",
8042
- "@geekmidas/manifest": "~10.0.0-alpha.11",
8043
- "@geekmidas/rate-limit": "~10.0.0-alpha.11",
8044
- "@geekmidas/schema": "~10.0.0-alpha.11",
8045
- "@geekmidas/services": "~10.0.0-alpha.11",
8046
- "@geekmidas/storage": "~10.0.0-alpha.11",
8047
- "@geekmidas/studio": "~10.0.0-alpha.11",
8048
- "@geekmidas/telescope": "~10.0.0-alpha.11",
8049
- "@geekmidas/testkit": "~10.0.0-alpha.11",
7481
+ "@geekmidas/audit": "~10.0.0-alpha.12",
7482
+ "@geekmidas/auth": "~10.0.0-alpha.12",
7483
+ "@geekmidas/cache": "~10.0.0-alpha.12",
7484
+ "@geekmidas/client": "~10.0.0-alpha.12",
7485
+ "@geekmidas/cloud": "~10.0.0-alpha.12",
7486
+ "@geekmidas/constructs": "~10.0.0-alpha.12",
7487
+ "@geekmidas/db": "~10.0.0-alpha.12",
7488
+ "@geekmidas/emailkit": "~10.0.0-alpha.12",
7489
+ "@geekmidas/envkit": "~10.0.0-alpha.12",
7490
+ "@geekmidas/errors": "~10.0.0-alpha.12",
7491
+ "@geekmidas/events": "~10.0.0-alpha.12",
7492
+ "@geekmidas/logger": "~10.0.0-alpha.12",
7493
+ "@geekmidas/manifest": "~10.0.0-alpha.12",
7494
+ "@geekmidas/rate-limit": "~10.0.0-alpha.12",
7495
+ "@geekmidas/schema": "~10.0.0-alpha.12",
7496
+ "@geekmidas/services": "~10.0.0-alpha.12",
7497
+ "@geekmidas/storage": "~10.0.0-alpha.12",
7498
+ "@geekmidas/studio": "~10.0.0-alpha.12",
7499
+ "@geekmidas/telescope": "~10.0.0-alpha.12",
7500
+ "@geekmidas/testkit": "~10.0.0-alpha.12",
8050
7501
  "@geekmidas/cli": CLI_VERSION
8051
7502
  };
8052
7503
 
@@ -8078,7 +7529,7 @@ function generateAuthAppFiles(options) {
8078
7529
  "@geekmidas/db": GEEKMIDAS_VERSIONS["@geekmidas/db"],
8079
7530
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
8080
7531
  "@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
8081
- "@hono/node-server": "~1.14.1",
7532
+ "@hono/node-server": DEPENDENCY_VERSIONS["@hono/node-server"],
8082
7533
  "better-auth": DEPENDENCY_VERSIONS["better-auth"],
8083
7534
  hono: DEPENDENCY_VERSIONS.hono,
8084
7535
  kysely: DEPENDENCY_VERSIONS.kysely,
@@ -8086,10 +7537,10 @@ function generateAuthAppFiles(options) {
8086
7537
  },
8087
7538
  devDependencies: {
8088
7539
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
8089
- "@types/node": "~22.0.0",
8090
- "@types/pg": "~8.23.1",
8091
- tsx: "~4.20.0",
8092
- typescript: "~5.8.2"
7540
+ "@types/node": DEPENDENCY_VERSIONS["@types/node"],
7541
+ "@types/pg": DEPENDENCY_VERSIONS["@types/pg"],
7542
+ tsx: TOOLCHAIN_VERSIONS["tsx"],
7543
+ typescript: TOOLCHAIN_VERSIONS["typescript"]
8093
7544
  }
8094
7545
  };
8095
7546
  const tsConfig = {
@@ -8196,21 +7647,6 @@ export default defineConfig({${stagesBlock(options.stages)}${options.monorepo ?
8196
7647
  logger: './src/config/logger#logger',`;
8197
7648
  if (isServerless || hasWorker) gkmConfig += `
8198
7649
  functions: './src/functions/**/*.ts',`;
8199
- if (!options.monorepo) {
8200
- const { cache, mail, events } = options.services;
8201
- if (cache || mail || events) {
8202
- gkmConfig += `
8203
- services: {`;
8204
- if (cache) gkmConfig += `
8205
- cache: true,`;
8206
- if (mail) gkmConfig += `
8207
- mail: true,`;
8208
- if (events) gkmConfig += `
8209
- events: '${events}',`;
8210
- gkmConfig += `
8211
- },`;
8212
- }
8213
- }
8214
7650
  if (hasWorker) gkmConfig += `
8215
7651
  crons: './src/crons/**/*.ts',
8216
7652
  subscribers: './src/subscribers/**/*.ts',`;
@@ -8255,14 +7691,14 @@ export default defineConfig({${stagesBlock(options.stages)}${options.monorepo ?
8255
7691
  path: "tsconfig.json",
8256
7692
  content: `${JSON.stringify(tsConfig, null, 2)}\n`
8257
7693
  }];
8258
- if (options.database) files$2.push({
7694
+ if (options.constructs.database) files$2.push({
8259
7695
  path: "vitest.config.ts",
8260
7696
  content: vitestConfigContent
8261
7697
  });
8262
7698
  return files$2;
8263
7699
  }
8264
7700
  const biomeConfig = {
8265
- $schema: "https://biomejs.dev/schemas/2.3.0/schema.json",
7701
+ $schema: BIOME_SCHEMA,
8266
7702
  vcs: {
8267
7703
  enabled: true,
8268
7704
  clientKind: "git",
@@ -8346,7 +7782,7 @@ export default defineConfig({${stagesBlock(options.stages)}${options.monorepo ?
8346
7782
  content: `${JSON.stringify(turboConfig, null, 2)}\n`
8347
7783
  }
8348
7784
  ];
8349
- if (options.database) files$1.push({
7785
+ if (options.constructs.database) files$1.push({
8350
7786
  path: "vitest.config.ts",
8351
7787
  content: vitestConfigContent
8352
7788
  });
@@ -8383,325 +7819,13 @@ function generateSingleAppConfigFiles(options, _template, _helpers) {
8383
7819
  path: "tsconfig.json",
8384
7820
  content: `${JSON.stringify(tsConfig, null, 2)}\n`
8385
7821
  }];
8386
- if (options.database) files$1.push({
7822
+ if (options.constructs.database) files$1.push({
8387
7823
  path: "vitest.config.ts",
8388
7824
  content: vitestConfigContent
8389
7825
  });
8390
7826
  return files$1;
8391
7827
  }
8392
7828
 
8393
- //#endregion
8394
- //#region src/init/generators/docker.ts
8395
- /**
8396
- * Generate docker-compose.yml based on template and options
8397
- */
8398
- function generateDockerFiles(options, template, dbApps) {
8399
- const { database } = options;
8400
- const isServerless = template.name === "serverless";
8401
- const hasWorker = template.name === "worker";
8402
- const isFullstack = options.template === "fullstack";
8403
- const services = [];
8404
- const volumes = [];
8405
- const files$1 = [];
8406
- if (database) {
8407
- const initVolume = isFullstack && dbApps?.length ? `
8408
- - ./docker/postgres/init.sh:/docker-entrypoint-initdb.d/init.sh:ro` : "";
8409
- const envFile = isFullstack && dbApps?.length ? `
8410
- env_file:
8411
- - ./docker/.env` : "";
8412
- services.push(` postgres:
8413
- image: postgres:18-alpine
8414
- container_name: ${options.name}-postgres
8415
- restart: unless-stopped${envFile}
8416
- environment:
8417
- POSTGRES_USER: \${POSTGRES_USER:-postgres}
8418
- POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-postgres}
8419
- POSTGRES_DB: \${POSTGRES_DB:-${options.name.replace(/-/g, "_")}_dev}
8420
- ports:
8421
- - '\${POSTGRES_HOST_PORT:-5432}:5432'
8422
- volumes:
8423
- - dbdata:/var/lib/postgresql${initVolume}
8424
- healthcheck:
8425
- test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER']
8426
- interval: 5s
8427
- timeout: 5s
8428
- retries: 5`);
8429
- volumes.push(" dbdata:");
8430
- if (isFullstack && dbApps?.length) {
8431
- files$1.push({
8432
- path: "docker/postgres/init.sh",
8433
- content: generatePostgresInitScript(dbApps, options.services?.events)
8434
- });
8435
- files$1.push({
8436
- path: "docker/.env",
8437
- content: generateDockerEnv(dbApps, options.services?.events)
8438
- });
8439
- }
8440
- }
8441
- if (isServerless) {
8442
- services.push(` redis:
8443
- image: redis:7-alpine
8444
- container_name: ${options.name}-redis
8445
- restart: unless-stopped
8446
- ports:
8447
- - '\${REDIS_HOST_PORT:-6379}:6379'
8448
- volumes:
8449
- - redis_data:/data
8450
- healthcheck:
8451
- test: ['CMD', 'redis-cli', 'ping']
8452
- interval: 5s
8453
- timeout: 5s
8454
- retries: 5
8455
-
8456
- serverless-redis:
8457
- image: hiett/serverless-redis-http:latest
8458
- container_name: ${options.name}-serverless-redis
8459
- restart: unless-stopped
8460
- ports:
8461
- - '\${SRH_HOST_PORT:-8079}:80'
8462
- environment:
8463
- SRH_MODE: env
8464
- SRH_TOKEN: local_dev_token
8465
- SRH_CONNECTION_STRING: redis://redis:6379
8466
- depends_on:
8467
- redis:
8468
- condition: service_healthy`);
8469
- volumes.push(" redis_data:");
8470
- } else {
8471
- services.push(` redis:
8472
- image: redis:7-alpine
8473
- container_name: ${options.name}-redis
8474
- restart: unless-stopped
8475
- ports:
8476
- - '\${REDIS_HOST_PORT:-6379}:6379'
8477
- volumes:
8478
- - redis_data:/data
8479
- healthcheck:
8480
- test: ['CMD', 'redis-cli', 'ping']
8481
- interval: 5s
8482
- timeout: 5s
8483
- retries: 5`);
8484
- volumes.push(" redis_data:");
8485
- }
8486
- if (hasWorker) {
8487
- services.push(` rabbitmq:
8488
- image: rabbitmq:3-management-alpine
8489
- container_name: ${options.name}-rabbitmq
8490
- restart: unless-stopped
8491
- ports:
8492
- - '\${RABBITMQ_HOST_PORT:-5672}:5672'
8493
- - '\${RABBITMQ_MGMT_HOST_PORT:-15672}:15672'
8494
- environment:
8495
- RABBITMQ_DEFAULT_USER: guest
8496
- RABBITMQ_DEFAULT_PASS: guest
8497
- volumes:
8498
- - rabbitmq_data:/var/lib/rabbitmq
8499
- healthcheck:
8500
- test: ['CMD', 'rabbitmq-diagnostics', 'check_running']
8501
- interval: 10s
8502
- timeout: 5s
8503
- retries: 5`);
8504
- volumes.push(" rabbitmq_data:");
8505
- }
8506
- if (options.services?.mail) services.push(` mailpit:
8507
- image: axllent/mailpit:latest
8508
- container_name: ${options.name}-mailpit
8509
- restart: unless-stopped
8510
- ports:
8511
- - '\${SMTP_PORT:-1025}:1025'
8512
- - '\${MAILPIT_PORT:-8025}:8025'
8513
- environment:
8514
- MP_SMTP_AUTH: \${SMTP_USER:-${options.name}}:\${SMTP_PASS:-${options.name}}`);
8515
- if (options.services?.storage) {
8516
- services.push(` minio:
8517
- image: pgsty/minio:RELEASE.2026-08-04T00-00-00Z
8518
- container_name: ${options.name}-minio
8519
- restart: unless-stopped
8520
- entrypoint: sh
8521
- command: -c 'mkdir -p /data/\${STORAGE_BUCKET:-${options.name}} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /data'
8522
- environment:
8523
- MINIO_ROOT_USER: \${STORAGE_ACCESS_KEY_ID:-${options.name}}
8524
- MINIO_ROOT_PASSWORD: \${STORAGE_SECRET_ACCESS_KEY:-${options.name}}
8525
- ports:
8526
- - '\${MINIO_API_HOST_PORT:-9000}:9000'
8527
- - '\${MINIO_CONSOLE_HOST_PORT:-9001}:9001'
8528
- volumes:
8529
- - minio_data:/data
8530
- healthcheck:
8531
- test: ['CMD', 'mc', 'ready', 'local']
8532
- interval: 10s
8533
- timeout: 5s
8534
- retries: 5`);
8535
- volumes.push(" minio_data:");
8536
- }
8537
- if (options.services?.events === "sns") {
8538
- services.push(` localstack:
8539
- image: floci/floci:latest
8540
- container_name: ${options.name}-localstack
8541
- restart: unless-stopped
8542
- environment:
8543
- AWS_DEFAULT_REGION: \${AWS_REGION:-us-east-1}
8544
- AWS_ACCESS_KEY_ID: \${AWS_ACCESS_KEY_ID:-localstack}
8545
- AWS_SECRET_ACCESS_KEY: \${AWS_SECRET_ACCESS_KEY:-localstack}
8546
- ports:
8547
- - '\${LOCALSTACK_PORT:-4566}:4566'
8548
- volumes:
8549
- - localstack_data:/var/lib/localstack
8550
- healthcheck:
8551
- test: ['CMD', 'curl', '-f', 'http://localhost:4566/_localstack/health']
8552
- interval: 10s
8553
- timeout: 5s
8554
- retries: 5`);
8555
- volumes.push(" localstack_data:");
8556
- }
8557
- if (options.services?.events === "rabbitmq" && !hasWorker) {
8558
- services.push(` rabbitmq:
8559
- image: rabbitmq:3-management-alpine
8560
- container_name: ${options.name}-rabbitmq
8561
- restart: unless-stopped
8562
- ports:
8563
- - '\${RABBITMQ_HOST_PORT:-5672}:5672'
8564
- - '\${RABBITMQ_MGMT_HOST_PORT:-15672}:15672'
8565
- environment:
8566
- RABBITMQ_DEFAULT_USER: guest
8567
- RABBITMQ_DEFAULT_PASS: guest
8568
- volumes:
8569
- - rabbitmq_data:/var/lib/rabbitmq
8570
- healthcheck:
8571
- test: ['CMD', 'rabbitmq-diagnostics', 'check_running']
8572
- interval: 10s
8573
- timeout: 5s
8574
- retries: 5`);
8575
- if (!volumes.includes(" rabbitmq_data:")) volumes.push(" rabbitmq_data:");
8576
- }
8577
- let dockerCompose = `# Use "gkm dev" or "gkm test" to start services.
8578
- # Running "docker compose up" directly will not inject secrets or resolve ports.
8579
- services:
8580
- ${services.join("\n\n")}
8581
- `;
8582
- if (volumes.length > 0) dockerCompose += `
8583
- volumes:
8584
- ${volumes.join("\n")}
8585
- `;
8586
- files$1.push({
8587
- path: "docker-compose.yml",
8588
- content: dockerCompose
8589
- });
8590
- return files$1;
8591
- }
8592
- /**
8593
- * Generate .env file for docker-compose with database passwords
8594
- */
8595
- function generateDockerEnv(apps, _eventsBackend) {
8596
- const envVars = apps.map((app) => {
8597
- const envVar = `${app.name.toUpperCase()}_DB_PASSWORD`;
8598
- return `${envVar}=${app.password}`;
8599
- });
8600
- envVars.push(`PGBOSS_DB_PASSWORD=pgboss-dev-password`);
8601
- return `# Auto-generated docker environment file
8602
- # Contains database passwords for docker-compose postgres init
8603
- # This file is gitignored - do not commit to version control
8604
- ${envVars.join("\n")}
8605
- `;
8606
- }
8607
- /**
8608
- * Generate PostgreSQL init shell script that creates per-app users with separate schemas.
8609
- * Uses idempotent DO blocks so the script can be re-run safely.
8610
- * - api user: uses public schema
8611
- * - auth user: uses auth schema with search_path=auth
8612
- * - pgboss user: uses pgboss schema (when events === 'pgboss')
8613
- */
8614
- function generatePostgresInitScript(apps, _eventsBackend) {
8615
- const userCreations = apps.map((app) => {
8616
- const userName = app.name.replace(/-/g, "_");
8617
- const envVar = `${app.name.toUpperCase()}_DB_PASSWORD`;
8618
- const isApi = app.name === "api";
8619
- const schemaName = isApi ? "public" : userName;
8620
- if (isApi) return `
8621
- # Create ${app.name} user (uses public schema) - idempotent
8622
- echo "Creating user ${userName}..."
8623
- psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
8624
- DO \\$\\$
8625
- BEGIN
8626
- IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${userName}') THEN
8627
- CREATE USER ${userName} WITH PASSWORD '$${envVar}';
8628
- ELSE
8629
- ALTER USER ${userName} WITH PASSWORD '$${envVar}';
8630
- END IF;
8631
- END
8632
- \\$\\$;
8633
- GRANT ALL ON SCHEMA public TO ${userName};
8634
- ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ${userName};
8635
- ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ${userName};
8636
- EOSQL
8637
- `;
8638
- return `
8639
- # Create ${app.name} user with dedicated schema - idempotent
8640
- echo "Creating user ${userName} with schema ${schemaName}..."
8641
- psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
8642
- DO \\$\\$
8643
- BEGIN
8644
- IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${userName}') THEN
8645
- CREATE USER ${userName} WITH PASSWORD '$${envVar}';
8646
- ELSE
8647
- ALTER USER ${userName} WITH PASSWORD '$${envVar}';
8648
- END IF;
8649
- END
8650
- \\$\\$;
8651
- CREATE SCHEMA IF NOT EXISTS ${schemaName} AUTHORIZATION ${userName};
8652
- ALTER USER ${userName} SET search_path TO ${schemaName};
8653
- GRANT USAGE ON SCHEMA ${schemaName} TO ${userName};
8654
- GRANT ALL ON ALL TABLES IN SCHEMA ${schemaName} TO ${userName};
8655
- GRANT ALL ON ALL SEQUENCES IN SCHEMA ${schemaName} TO ${userName};
8656
- ALTER DEFAULT PRIVILEGES IN SCHEMA ${schemaName} GRANT ALL ON TABLES TO ${userName};
8657
- ALTER DEFAULT PRIVILEGES IN SCHEMA ${schemaName} GRANT ALL ON SEQUENCES TO ${userName};
8658
- EOSQL
8659
- `;
8660
- });
8661
- const pgbossBlock = `
8662
- # Create pgboss user with dedicated schema - idempotent
8663
- echo "Creating pgboss user and schema..."
8664
- psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
8665
- DO \\$\\$
8666
- BEGIN
8667
- IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'pgboss') THEN
8668
- CREATE USER pgboss WITH PASSWORD '$PGBOSS_DB_PASSWORD';
8669
- ELSE
8670
- ALTER USER pgboss WITH PASSWORD '$PGBOSS_DB_PASSWORD';
8671
- END IF;
8672
- END
8673
- \\$\\$;
8674
- CREATE SCHEMA IF NOT EXISTS pgboss AUTHORIZATION pgboss;
8675
- ALTER USER pgboss SET search_path TO pgboss;
8676
- GRANT USAGE ON SCHEMA pgboss TO pgboss;
8677
- GRANT ALL ON ALL TABLES IN SCHEMA pgboss TO pgboss;
8678
- GRANT ALL ON ALL SEQUENCES IN SCHEMA pgboss TO pgboss;
8679
- ALTER DEFAULT PRIVILEGES IN SCHEMA pgboss GRANT ALL ON TABLES TO pgboss;
8680
- ALTER DEFAULT PRIVILEGES IN SCHEMA pgboss GRANT ALL ON SEQUENCES TO pgboss;
8681
- EOSQL
8682
- `;
8683
- const extensions = `
8684
- # Create extensions
8685
- echo "Creating extensions..."
8686
- psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
8687
- CREATE EXTENSION IF NOT EXISTS pg_trgm;
8688
- CREATE EXTENSION IF NOT EXISTS citext;
8689
- EOSQL
8690
- `;
8691
- return `#!/bin/bash
8692
- set -e
8693
-
8694
- # Auto-generated PostgreSQL init script (idempotent - safe to re-run)
8695
- # Creates per-app users with separate schemas in a single database
8696
- # - api: uses public schema
8697
- # - auth: uses auth schema (search_path=auth)
8698
- # - pgboss: uses pgboss schema for event processing
8699
- ${extensions}
8700
- ${userCreations.join("\n")}${pgbossBlock}
8701
- echo "Database initialization complete!"
8702
- `;
8703
- }
8704
-
8705
7829
  //#endregion
8706
7830
  //#region src/init/generators/env.ts
8707
7831
  /**
@@ -8720,6 +7844,9 @@ node_modules/
8720
7844
  dist/
8721
7845
  .gkm/
8722
7846
 
7847
+ # Written by gkm from the constructs; your own docker-compose.yml is not
7848
+ docker-compose.constructs.yml
7849
+
8723
7850
  # Environment (legacy - use gkm secrets instead)
8724
7851
  .env
8725
7852
  .env.local
@@ -9111,32 +8238,32 @@ function generateExpoAppFiles(options) {
9111
8238
  "@better-auth/expo": DEPENDENCY_VERSIONS["better-auth"],
9112
8239
  "@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
9113
8240
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
9114
- "@react-navigation/native": "^7.1.0",
8241
+ "@react-navigation/native": EXPO_VERSIONS["@react-navigation/native"],
9115
8242
  "@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
9116
8243
  "better-auth": DEPENDENCY_VERSIONS["better-auth"],
9117
- expo: "~55.0.0",
9118
- "expo-constants": "~55.0.0",
9119
- "expo-dev-client": "~55.0.0",
9120
- "expo-linking": "~55.0.0",
9121
- "expo-router": "~55.0.0",
9122
- "expo-secure-store": "~14.2.0",
9123
- "expo-splash-screen": "~55.0.0",
9124
- "expo-status-bar": "~55.0.0",
9125
- nativewind: "~4.2.0",
9126
- react: "19.2.0",
9127
- "react-dom": "19.2.0",
9128
- "react-native": "0.83.6",
9129
- "react-native-gesture-handler": "~2.30.0",
9130
- "react-native-reanimated": "~4.2.0",
9131
- "react-native-safe-area-context": "~5.6.0",
9132
- "react-native-screens": "~4.23.0",
9133
- "react-native-web": "~0.21.0",
9134
- tailwindcss: "~3.4.0"
8244
+ expo: EXPO_VERSIONS["expo"],
8245
+ "expo-constants": EXPO_VERSIONS["expo-constants"],
8246
+ "expo-dev-client": EXPO_VERSIONS["expo-dev-client"],
8247
+ "expo-linking": EXPO_VERSIONS["expo-linking"],
8248
+ "expo-router": EXPO_VERSIONS["expo-router"],
8249
+ "expo-secure-store": EXPO_VERSIONS["expo-secure-store"],
8250
+ "expo-splash-screen": EXPO_VERSIONS["expo-splash-screen"],
8251
+ "expo-status-bar": EXPO_VERSIONS["expo-status-bar"],
8252
+ nativewind: EXPO_VERSIONS["nativewind"],
8253
+ react: EXPO_VERSIONS["react"],
8254
+ "react-dom": EXPO_VERSIONS["react-dom"],
8255
+ "react-native": EXPO_VERSIONS["react-native"],
8256
+ "react-native-gesture-handler": EXPO_VERSIONS["react-native-gesture-handler"],
8257
+ "react-native-reanimated": EXPO_VERSIONS["react-native-reanimated"],
8258
+ "react-native-safe-area-context": EXPO_VERSIONS["react-native-safe-area-context"],
8259
+ "react-native-screens": EXPO_VERSIONS["react-native-screens"],
8260
+ "react-native-web": EXPO_VERSIONS["react-native-web"],
8261
+ tailwindcss: EXPO_VERSIONS["tailwindcss"]
9135
8262
  },
9136
8263
  devDependencies: {
9137
- "@babel/core": "^7.25.0",
9138
- "@types/react": "~19.0.0",
9139
- typescript: "~5.8.2"
8264
+ "@babel/core": EXPO_VERSIONS["@babel/core"],
8265
+ "@types/react": EXPO_VERSIONS["@types/react"],
8266
+ typescript: TOOLCHAIN_VERSIONS["typescript"]
9140
8267
  }
9141
8268
  };
9142
8269
  const appConfig = `import type { ConfigContext, ExpoConfig } from '@expo/config';
@@ -9690,7 +8817,7 @@ function generateModelsPackage(options) {
9690
8817
  exports: { "./*": "./src/*.ts" },
9691
8818
  scripts: { typecheck: "tsc --noEmit" },
9692
8819
  dependencies: {},
9693
- devDependencies: { typescript: "~5.8.2" }
8820
+ devDependencies: { typescript: TOOLCHAIN_VERSIONS["typescript"] }
9694
8821
  };
9695
8822
  const tsConfig = {
9696
8823
  extends: "../../tsconfig.json",
@@ -9846,11 +8973,11 @@ function generateDeployFiles(options) {
9846
8973
  if (!options.region) throw new SstNeedsRegion();
9847
8974
  const db = databaseFor();
9848
8975
  const mail = emailFor();
9849
- const hasDatabase = options.template === "fullstack" || options.services.db;
8976
+ const hasDatabase = options.constructs.database;
9850
8977
  const inputs = [...hasDatabase ? [` // RDS needs a network. Created here rather than assumed, because a
9851
8978
  // VPC with NAT is a monthly cost in an account that may already
9852
8979
  // have one — replace it with \`sst.aws.Vpc.get(…)\` if so.
9853
- ${db.id}: { vpc },`] : [], ...options.services.mail ? [` // Every provider rejects an unverified sender, so there is no
8980
+ ${db.id}: { vpc },`] : [], ...options.constructs.mail ? [` // Every provider rejects an unverified sender, so there is no
9854
8981
  // default; the synth stops with \`EmailNeedsSender\` until it is set.
9855
8982
  ${mail.id}: { from: process.env.MAIL_FROM as string },`] : []];
9856
8983
  return [{
@@ -9940,9 +9067,9 @@ function rootConstructDependencies(options) {
9940
9067
  "@geekmidas/db": v["@geekmidas/db"],
9941
9068
  kysely: DEPENDENCY_VERSIONS.kysely,
9942
9069
  pg: DEPENDENCY_VERSIONS.pg,
9943
- ...options.services.storage ? { "@geekmidas/storage": v["@geekmidas/storage"] } : {},
9944
- ...options.services.mail ? { "@geekmidas/emailkit": v["@geekmidas/emailkit"] } : {},
9945
- ...options.services.cache ? { "@geekmidas/cache": v["@geekmidas/cache"] } : {}
9070
+ ...options.constructs.uploads ? { "@geekmidas/storage": v["@geekmidas/storage"] } : {},
9071
+ ...options.constructs.mail ? { "@geekmidas/emailkit": v["@geekmidas/emailkit"] } : {},
9072
+ ...options.constructs.cache ? { "@geekmidas/cache": v["@geekmidas/cache"] } : {}
9946
9073
  };
9947
9074
  }
9948
9075
  /**
@@ -9977,13 +9104,13 @@ function generateMonorepoFiles(options, _template) {
9977
9104
  ...deploy$1.dependencies
9978
9105
  },
9979
9106
  devDependencies: {
9980
- "@biomejs/biome": "~2.3.0",
9107
+ "@biomejs/biome": DEPENDENCY_VERSIONS["@biomejs/biome"],
9981
9108
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
9982
- esbuild: "~0.27.0",
9983
- tsx: "~4.20.0",
9984
- turbo: "~2.3.0",
9985
- typescript: "~5.8.2",
9986
- vitest: "~4.0.0",
9109
+ esbuild: TOOLCHAIN_VERSIONS["esbuild"],
9110
+ tsx: TOOLCHAIN_VERSIONS["tsx"],
9111
+ turbo: DEPENDENCY_VERSIONS["turbo"],
9112
+ typescript: TOOLCHAIN_VERSIONS["typescript"],
9113
+ vitest: TOOLCHAIN_VERSIONS["vitest"],
9987
9114
  ...deploy$1.devDependencies
9988
9115
  }
9989
9116
  };
@@ -9994,7 +9121,7 @@ function generateMonorepoFiles(options, _template) {
9994
9121
  - 'packages/*'
9995
9122
  `;
9996
9123
  const biomeConfig = {
9997
- $schema: "https://biomejs.dev/schemas/2.3.0/schema.json",
9124
+ $schema: BIOME_SCHEMA,
9998
9125
  vcs: {
9999
9126
  enabled: true,
10000
9127
  clientKind: "git",
@@ -10039,7 +9166,17 @@ function generateMonorepoFiles(options, _template) {
10039
9166
  tasks: {
10040
9167
  build: {
10041
9168
  dependsOn: ["^build"],
10042
- outputs: ["dist/**"]
9169
+ inputs: [
9170
+ "$TURBO_DEFAULT$",
9171
+ "$TURBO_ROOT$/constructs/**/*.ts",
9172
+ "$TURBO_ROOT$/gkm.config.ts"
9173
+ ],
9174
+ outputs: [
9175
+ "dist/**",
9176
+ ".gkm/**",
9177
+ ".next/**",
9178
+ "!.next/cache/**"
9179
+ ]
10043
9180
  },
10044
9181
  dev: {
10045
9182
  cache: false,
@@ -10068,6 +9205,9 @@ node_modules/
10068
9205
  dist/
10069
9206
  .gkm/
10070
9207
 
9208
+ # Written by gkm from the constructs; your own docker-compose.yml is not
9209
+ docker-compose.constructs.yml
9210
+
10071
9211
  # Environment
10072
9212
  .env
10073
9213
  .env.local
@@ -10233,7 +9373,7 @@ export default defineConfig({
10233
9373
  * table in the database that is already there, storage is MinIO, mail is SES.
10234
9374
  */
10235
9375
  function generateWorkspaceConfig(options) {
10236
- let config = `import { defineWorkspace } from '@geekmidas/cli/config';
9376
+ const config = `import { defineWorkspace } from '@geekmidas/cli/config';
10237
9377
 
10238
9378
  export default defineWorkspace({
10239
9379
  // The scope every physical name is built from: \`Database\` becomes
@@ -10256,11 +9396,6 @@ ${workspaceConstructsGlobs(options.routesStructure, (0, node_path.dirname)(optio
10256
9396
  },
10257
9397
  });
10258
9398
  `;
10259
- if (options.services.events && options.services.events !== "pgboss") config = config.replace(" secrets: {", ` services: {
10260
- events: '${options.services.events}',
10261
- },
10262
-
10263
- secrets: {`);
10264
9399
  return config;
10265
9400
  }
10266
9401
  /**
@@ -10277,7 +9412,8 @@ ${workspaceConstructsGlobs(options.routesStructure, (0, node_path.dirname)(optio
10277
9412
  */
10278
9413
  function generateRootConstructs(options) {
10279
9414
  if (!options.monorepo || options.template !== "fullstack") return [];
10280
- const { name: name$1, services, frontendFramework } = options;
9415
+ const { name: name$1, frontendFramework } = options;
9416
+ const { cache, uploads, mail } = options.constructs;
10281
9417
  const db = databaseFor();
10282
9418
  const files$1 = [];
10283
9419
  files$1.push({
@@ -10392,7 +9528,7 @@ import { auth } from './auth.ts';
10392
9528
  export const web = new StaticSite('Web', ${variant}).dependsOn([api, auth]);
10393
9529
  `
10394
9530
  });
10395
- if (services.storage) {
9531
+ if (uploads) {
10396
9532
  const bucket = storageFor();
10397
9533
  files$1.push({
10398
9534
  path: "constructs/storage.ts",
@@ -10403,18 +9539,18 @@ export const uploads = new ObjectStorage('${bucket.id}');
10403
9539
  `
10404
9540
  });
10405
9541
  }
10406
- if (services.mail) {
10407
- const mail = emailFor();
9542
+ if (mail) {
9543
+ const mail$1 = emailFor();
10408
9544
  files$1.push({
10409
9545
  path: "constructs/email.ts",
10410
9546
  content: `import { Email } from '@geekmidas/constructs/email';
10411
9547
 
10412
9548
  /** Outbound mail — Mailpit locally, SES deployed. */
10413
- export const email = new Email('${mail.id}', { templates: {} });
9549
+ export const email = new Email('${mail$1.id}', { templates: {} });
10414
9550
  `
10415
9551
  });
10416
9552
  }
10417
- if (services.cache) {
9553
+ if (cache) {
10418
9554
  const kv = cacheFor();
10419
9555
  files$1.push({
10420
9556
  path: "constructs/cache.ts",
@@ -10447,20 +9583,20 @@ const apiTemplate = {
10447
9583
  "@geekmidas/services": GEEKMIDAS_VERSIONS["@geekmidas/services"],
10448
9584
  "@geekmidas/errors": GEEKMIDAS_VERSIONS["@geekmidas/errors"],
10449
9585
  "@geekmidas/auth": GEEKMIDAS_VERSIONS["@geekmidas/auth"],
10450
- "@hono/node-server": "~1.14.1",
9586
+ "@hono/node-server": DEPENDENCY_VERSIONS["@hono/node-server"],
10451
9587
  hono: DEPENDENCY_VERSIONS.hono,
10452
9588
  pino: DEPENDENCY_VERSIONS.pino,
10453
9589
  zod: DEPENDENCY_VERSIONS.zod
10454
9590
  },
10455
9591
  devDependencies: {
10456
- "@biomejs/biome": "~2.3.0",
9592
+ "@biomejs/biome": DEPENDENCY_VERSIONS["@biomejs/biome"],
10457
9593
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
10458
- "@types/node": "~22.0.0",
10459
- esbuild: "~0.27.0",
10460
- tsx: "~4.20.0",
10461
- turbo: "~2.3.0",
10462
- typescript: "~5.8.2",
10463
- vitest: "~4.0.0"
9594
+ "@types/node": DEPENDENCY_VERSIONS["@types/node"],
9595
+ esbuild: TOOLCHAIN_VERSIONS["esbuild"],
9596
+ tsx: TOOLCHAIN_VERSIONS["tsx"],
9597
+ turbo: DEPENDENCY_VERSIONS["turbo"],
9598
+ typescript: TOOLCHAIN_VERSIONS["typescript"],
9599
+ vitest: TOOLCHAIN_VERSIONS["vitest"]
10464
9600
  },
10465
9601
  scripts: {
10466
9602
  dev: "gkm dev",
@@ -10473,7 +9609,8 @@ const apiTemplate = {
10473
9609
  "fmt:check": "biome format ."
10474
9610
  },
10475
9611
  files: (options) => {
10476
- const { loggerType, routesStructure, monorepo, name: name$1, services } = options;
9612
+ const { loggerType, routesStructure, monorepo, name: name$1 } = options;
9613
+ const { cache, uploads, mail: sendsMail } = options.constructs;
10477
9614
  const bucket = storageFor();
10478
9615
  const kv = cacheFor();
10479
9616
  const db = databaseFor();
@@ -10583,7 +9720,7 @@ import { router } from '${src("router.ts")}';
10583
9720
  export const listUsersEndpoint = router
10584
9721
  .get('/users')
10585
9722
  .output(ListUsersResponseSchema)
10586
- ${options.database ? ` // \`db\` is here because the router named the database construct.
9723
+ ${options.constructs.database ? ` // \`db\` is here because the router named the database construct.
10587
9724
  .handle(async ({ db }) => ({
10588
9725
  users: await db.selectFrom('users').select(['id', 'name']).execute(),
10589
9726
  }));
@@ -10606,7 +9743,7 @@ export const listUsersEndpoint = router
10606
9743
  .output(z.object({
10607
9744
  users: z.array(UserSchema),
10608
9745
  }))
10609
- ${options.database ? ` // \`db\` is here because the router named the database construct.
9746
+ ${options.constructs.database ? ` // \`db\` is here because the router named the database construct.
10610
9747
  .handle(async ({ db }) => ({
10611
9748
  users: await db.selectFrom('users').select(['id', 'name']).execute(),
10612
9749
  }));
@@ -10698,18 +9835,18 @@ export const authService = {
10698
9835
  files$1.push({
10699
9836
  path: "src/router.ts",
10700
9837
  content: `import { UnauthorizedError } from '@geekmidas/errors';
10701
- import { api } from '${constructsImport("api")}';${options.database ? `
9838
+ import { api } from '${constructsImport("api")}';${options.constructs.database ? `
10702
9839
  import { database } from '${constructsImport("database")}';` : ""}
10703
9840
  import { authService, type Session } from './services/auth.ts';
10704
9841
 
10705
9842
  /**
10706
9843
  * The shared endpoint factory — no session required.
10707
- *${options.database ? `
9844
+ *${options.constructs.database ? `
10708
9845
  * Naming the database construct is what puts \`db\` in every handler built from
10709
9846
  * this router. Depend on other constructs per endpoint with \`.dependsOn([…])\`.` : `
10710
9847
  * Depend on constructs per endpoint with \`.dependsOn([…])\`.`}
10711
9848
  */
10712
- export const router = api.endpoints${options.database ? ".database(database)" : ""};
9849
+ export const router = api.endpoints${options.constructs.database ? ".database(database)" : ""};
10713
9850
 
10714
9851
  // The auth client available, but the session not enforced.
10715
9852
  export const r = router.services([authService]);
@@ -10745,22 +9882,22 @@ export const profileEndpoint = sessionRouter
10745
9882
  }
10746
9883
  if (!monorepo) files$1.push({
10747
9884
  path: "src/router.ts",
10748
- content: `import { api } from '${constructsImport("api")}';${options.database ? `
9885
+ content: `import { api } from '${constructsImport("api")}';${options.constructs.database ? `
10749
9886
  import { database } from '${constructsImport("database")}';` : ""}
10750
9887
 
10751
9888
  /**
10752
9889
  * The shared endpoint factory.
10753
- *${options.database ? `
9890
+ *${options.constructs.database ? `
10754
9891
  * Naming the database construct is what puts \`db\` in every handler built from
10755
9892
  * this router. Depend on other constructs per endpoint with \`.dependsOn([…])\`.` : `
10756
9893
  * Depend on constructs per endpoint with \`.dependsOn([…])\`.`}
10757
9894
  */
10758
- export const router = api.endpoints${options.database ? ".database(database)" : ""};
9895
+ export const router = api.endpoints${options.constructs.database ? ".database(database)" : ""};
10759
9896
  `
10760
9897
  });
10761
- if (options.database && declares) files$1.push(...databaseFiles());
10762
- else if (options.database) files$1.push(usersMigration());
10763
- if (services.storage && declares) files$1.push({
9898
+ if (options.constructs.database && declares) files$1.push(...databaseFiles());
9899
+ else if (options.constructs.database) files$1.push(usersMigration());
9900
+ if (uploads && declares) files$1.push({
10764
9901
  path: "src/constructs/storage.ts",
10765
9902
  content: `import { ObjectStorage } from '@geekmidas/constructs/object-storage';
10766
9903
 
@@ -10774,7 +9911,7 @@ export const router = api.endpoints${options.database ? ".database(database)" :
10774
9911
  export const uploads = new ObjectStorage('${bucket.id}');
10775
9912
  `
10776
9913
  });
10777
- if (services.mail && declares) files$1.push({
9914
+ if (sendsMail && declares) files$1.push({
10778
9915
  path: "src/constructs/email.ts",
10779
9916
  content: `import { Email } from '@geekmidas/constructs/email';
10780
9917
 
@@ -10783,13 +9920,13 @@ export const uploads = new ObjectStorage('${bucket.id}');
10783
9920
  *
10784
9921
  * Add React templates to \`templates\` and \`sendTemplate\` becomes typed
10785
9922
  * against them. Reach it with \`.dependsOn([email])\` for
10786
- * \`services.${mail.service}\`; who delivers it is \`services.mail\` in
10787
- * \`gkm.config.ts\`.
9923
+ * \`services.${mail.service}\`. Who delivers it is whichever provider the
9924
+ * stage's \`${mail.urlKey}\` points at — every one of them speaks SMTP.
10788
9925
  */
10789
9926
  export const email = new Email('${mail.id}', { templates: {} });
10790
9927
  `
10791
9928
  });
10792
- if (services.cache && declares) files$1.push({
9929
+ if (cache && declares) files$1.push({
10793
9930
  path: "src/constructs/cache.ts",
10794
9931
  content: `import { Cache } from '@geekmidas/constructs/cache';
10795
9932
 
@@ -10797,18 +9934,18 @@ export const email = new Email('${mail.id}', { templates: {} });
10797
9934
  * A cache, declared once.
10798
9935
  *
10799
9936
  * Reach it with \`.dependsOn([cache])\` for \`services.${kv.service}\`. This
10800
- * form says the app caches and leaves *where* to the deployment: which backend
10801
- * serves it — Upstash, ElastiCache, or a database — is \`services.cache\` in
10802
- * \`gkm.config.ts\`, because the same code caches into any of them.
9937
+ * form says the app caches and leaves *where* to the deployment: Upstash on
9938
+ * AWS, a table in the database on a server — the same code caches into
9939
+ * either.
10803
9940
  *
10804
9941
  * To say it caches in a particular database instead, declare it from that
10805
9942
  * database — \`database.cache()\` — and entries become a table in it, in its
10806
- * schema and reached by its role, which config can no longer move.
9943
+ * schema and reached by its role, whatever the target.
10807
9944
  */
10808
9945
  export const cache = new Cache('${kv.id}');
10809
9946
  `
10810
9947
  });
10811
- if (options.database && !declares) files$1.push({
9948
+ if (options.constructs.database && !declares) files$1.push({
10812
9949
  path: "src/services/database.ts",
10813
9950
  content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
10814
9951
  import { Kysely, PostgresDialect } from 'kysely';
@@ -10859,7 +9996,7 @@ export const telescope = new Telescope({
10859
9996
  });
10860
9997
  `
10861
9998
  });
10862
- if (options.studio && options.database) files$1.push({
9999
+ if (options.studio && options.constructs.database) files$1.push({
10863
10000
  path: "src/config/studio.ts",
10864
10001
  content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
10865
10002
  import { Kysely, PostgresDialect } from 'kysely';
@@ -10908,20 +10045,20 @@ const minimalTemplate = {
10908
10045
  "@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
10909
10046
  "@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
10910
10047
  "@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
10911
- "@hono/node-server": "~1.14.1",
10048
+ "@hono/node-server": DEPENDENCY_VERSIONS["@hono/node-server"],
10912
10049
  hono: DEPENDENCY_VERSIONS.hono,
10913
10050
  pino: DEPENDENCY_VERSIONS.pino,
10914
10051
  zod: DEPENDENCY_VERSIONS.zod
10915
10052
  },
10916
10053
  devDependencies: {
10917
- "@biomejs/biome": "~2.3.0",
10054
+ "@biomejs/biome": DEPENDENCY_VERSIONS["@biomejs/biome"],
10918
10055
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
10919
- "@types/node": "~22.0.0",
10920
- esbuild: "~0.27.0",
10921
- tsx: "~4.20.0",
10922
- turbo: "~2.3.0",
10923
- typescript: "~5.8.2",
10924
- vitest: "~4.0.0"
10056
+ "@types/node": DEPENDENCY_VERSIONS["@types/node"],
10057
+ esbuild: TOOLCHAIN_VERSIONS["esbuild"],
10058
+ tsx: TOOLCHAIN_VERSIONS["tsx"],
10059
+ turbo: DEPENDENCY_VERSIONS["turbo"],
10060
+ typescript: TOOLCHAIN_VERSIONS["typescript"],
10061
+ vitest: TOOLCHAIN_VERSIONS["vitest"]
10925
10062
  },
10926
10063
  scripts: {
10927
10064
  dev: "gkm dev",
@@ -11007,7 +10144,7 @@ export const healthEndpoint = api
11007
10144
  `
11008
10145
  }
11009
10146
  ];
11010
- if (options.database) files$1.push(...databaseFiles());
10147
+ if (options.constructs.database) files$1.push(...databaseFiles());
11011
10148
  if (options.telescope) files$1.push({
11012
10149
  path: "src/config/telescope.ts",
11013
10150
  content: `import { Telescope } from '@geekmidas/telescope';
@@ -11019,7 +10156,7 @@ export const telescope = new Telescope({
11019
10156
  });
11020
10157
  `
11021
10158
  });
11022
- if (options.studio && options.database) files$1.push({
10159
+ if (options.studio && options.constructs.database) files$1.push({
11023
10160
  path: "src/config/studio.ts",
11024
10161
  content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
11025
10162
  import { Kysely, PostgresDialect } from 'kysely';
@@ -11070,19 +10207,19 @@ const serverlessTemplate = {
11070
10207
  "@geekmidas/cloud": GEEKMIDAS_VERSIONS["@geekmidas/cloud"],
11071
10208
  "@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
11072
10209
  "@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
11073
- "@hono/node-server": "~1.14.1",
10210
+ "@hono/node-server": DEPENDENCY_VERSIONS["@hono/node-server"],
11074
10211
  hono: DEPENDENCY_VERSIONS.hono,
11075
10212
  pino: DEPENDENCY_VERSIONS.pino
11076
10213
  },
11077
10214
  devDependencies: {
11078
- "@biomejs/biome": "~2.3.0",
10215
+ "@biomejs/biome": DEPENDENCY_VERSIONS["@biomejs/biome"],
11079
10216
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
11080
- "@types/aws-lambda": "~8.10.92",
11081
- "@types/node": "~22.0.0",
11082
- tsx: "~4.20.0",
11083
- turbo: "~2.3.0",
11084
- typescript: "~5.8.2",
11085
- vitest: "~4.0.0"
10217
+ "@types/aws-lambda": DEPENDENCY_VERSIONS["@types/aws-lambda"],
10218
+ "@types/node": DEPENDENCY_VERSIONS["@types/node"],
10219
+ tsx: TOOLCHAIN_VERSIONS["tsx"],
10220
+ turbo: DEPENDENCY_VERSIONS["turbo"],
10221
+ typescript: TOOLCHAIN_VERSIONS["typescript"],
10222
+ vitest: TOOLCHAIN_VERSIONS["vitest"]
11086
10223
  },
11087
10224
  scripts: {
11088
10225
  dev: "gkm dev",
@@ -11190,7 +10327,7 @@ export const worker = new Worker('Jobs', { logger });
11190
10327
  `
11191
10328
  }
11192
10329
  ];
11193
- if (options.database) files$1.push(...databaseFiles());
10330
+ if (options.constructs.database) files$1.push(...databaseFiles());
11194
10331
  if (options.telescope) files$1.push({
11195
10332
  path: "src/config/telescope.ts",
11196
10333
  content: `import { Telescope } from '@geekmidas/telescope';
@@ -11238,13 +10375,13 @@ const workerTemplate = {
11238
10375
  pino: DEPENDENCY_VERSIONS.pino
11239
10376
  },
11240
10377
  devDependencies: {
11241
- "@biomejs/biome": "~2.3.0",
10378
+ "@biomejs/biome": DEPENDENCY_VERSIONS["@biomejs/biome"],
11242
10379
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
11243
- "@types/node": "~22.0.0",
11244
- tsx: "~4.20.0",
11245
- turbo: "~2.3.0",
11246
- typescript: "~5.8.2",
11247
- vitest: "~4.0.0"
10380
+ "@types/node": DEPENDENCY_VERSIONS["@types/node"],
10381
+ tsx: TOOLCHAIN_VERSIONS["tsx"],
10382
+ turbo: DEPENDENCY_VERSIONS["turbo"],
10383
+ typescript: TOOLCHAIN_VERSIONS["typescript"],
10384
+ vitest: TOOLCHAIN_VERSIONS["vitest"]
11248
10385
  },
11249
10386
  scripts: {
11250
10387
  dev: "gkm dev",
@@ -11257,7 +10394,8 @@ const workerTemplate = {
11257
10394
  "fmt:check": "biome format ."
11258
10395
  },
11259
10396
  files: (options) => {
11260
- const { loggerType, name: name$1, database } = options;
10397
+ const { loggerType, name: name$1 } = options;
10398
+ const { database } = options.constructs;
11261
10399
  const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
11262
10400
 
11263
10401
  export const logger = createLogger();
@@ -11373,7 +10511,7 @@ export const cleanup = worker
11373
10511
  });
11374
10512
  `
11375
10513
  });
11376
- if (options.database) files$1.push(...databaseFiles());
10514
+ if (options.constructs.database) files$1.push(...databaseFiles());
11377
10515
  if (options.telescope) files$1.push({
11378
10516
  path: "src/config/telescope.ts",
11379
10517
  content: `import { Telescope } from '@geekmidas/telescope';
@@ -11518,60 +10656,32 @@ const deployTargetChoices = [
11518
10656
  }
11519
10657
  ];
11520
10658
  /**
11521
- * Services choices for multi-select prompt
11522
- */
11523
- /**
11524
- * What the project declares — and therefore which containers it runs.
10659
+ * Which constructs to declare.
11525
10660
  *
11526
- * Each choice scaffolds a construct rather than a container: the container is
11527
- * what `gkm dev` derives from the declaration, which is why the descriptions
11528
- * name the construct and the titles name the thing you will see running.
10661
+ * The containers that run them locally and the backends that serve them
10662
+ * deployed are derived from these, so the prompt names only what the code
10663
+ * will say.
11529
10664
  */
11530
- const servicesChoices = [
10665
+ const constructChoices = [
11531
10666
  {
11532
- title: "PostgreSQL",
11533
- value: "db",
11534
- description: "A declared database — KyselyDatabase construct"
10667
+ title: "Database",
10668
+ value: "database",
10669
+ description: "KyselyDatabase — Postgres"
11535
10670
  },
11536
10671
  {
11537
- title: "Redis",
10672
+ title: "Cache",
11538
10673
  value: "cache",
11539
- description: "A declared cache — Cache construct"
11540
- },
11541
- {
11542
- title: "Mailpit",
11543
- value: "mail",
11544
- description: "Declared outbound mail — Email construct"
11545
- },
11546
- {
11547
- title: "MinIO",
11548
- value: "storage",
11549
- description: "A declared bucket — ObjectStorage construct"
11550
- }
11551
- ];
11552
- /**
11553
- * Event backend choices for prompts
11554
- */
11555
- const eventsBackendChoices = [
11556
- {
11557
- title: "pg-boss",
11558
- value: "pgboss",
11559
- description: "PostgreSQL-based job queue (reuses postgres, no extra container)"
11560
- },
11561
- {
11562
- title: "SNS/SQS",
11563
- value: "sns",
11564
- description: "AWS SNS+SQS via LocalStack for local dev"
10674
+ description: "Cache — backed by what the deploy target implies"
11565
10675
  },
11566
10676
  {
11567
- title: "RabbitMQ",
11568
- value: "rabbitmq",
11569
- description: "AMQP message broker"
10677
+ title: "Uploads",
10678
+ value: "uploads",
10679
+ description: "ObjectStorage — a bucket"
11570
10680
  },
11571
10681
  {
11572
- title: "None",
11573
- value: void 0,
11574
- description: "Skip event backend"
10682
+ title: "Mail",
10683
+ value: "mail",
10684
+ description: "Email — outbound mail over SMTP"
11575
10685
  }
11576
10686
  ];
11577
10687
  /**
@@ -11596,26 +10706,27 @@ function isFullstackTemplate(name$1) {
11596
10706
  * Generate package.json with dependencies based on template and options
11597
10707
  */
11598
10708
  function generatePackageJson(options, template) {
11599
- const { name: name$1, telescope, database, studio, monorepo, services } = options;
10709
+ const { name: name$1, telescope, studio, monorepo } = options;
10710
+ const { database, cache, uploads, mail } = options.constructs;
11600
10711
  const dependencies$1 = { ...template.dependencies };
11601
10712
  const devDependencies$1 = { ...template.devDependencies };
11602
10713
  const scripts$1 = { ...template.scripts };
11603
10714
  if (telescope) dependencies$1["@geekmidas/telescope"] = GEEKMIDAS_VERSIONS["@geekmidas/telescope"];
11604
10715
  if (studio) dependencies$1["@geekmidas/studio"] = GEEKMIDAS_VERSIONS["@geekmidas/studio"];
11605
10716
  const declares = !monorepo;
11606
- if (declares && (database || services.storage || services.cache || services.mail)) dependencies$1["@geekmidas/manifest"] = GEEKMIDAS_VERSIONS["@geekmidas/manifest"];
11607
- if (declares && services.storage) dependencies$1["@geekmidas/storage"] = GEEKMIDAS_VERSIONS["@geekmidas/storage"];
11608
- if (declares && services.cache) dependencies$1["@geekmidas/cache"] = GEEKMIDAS_VERSIONS["@geekmidas/cache"];
11609
- if (declares && services.mail) dependencies$1["@geekmidas/emailkit"] = GEEKMIDAS_VERSIONS["@geekmidas/emailkit"];
10717
+ if (declares && (database || uploads || cache || mail)) dependencies$1["@geekmidas/manifest"] = GEEKMIDAS_VERSIONS["@geekmidas/manifest"];
10718
+ if (declares && uploads) dependencies$1["@geekmidas/storage"] = GEEKMIDAS_VERSIONS["@geekmidas/storage"];
10719
+ if (declares && cache) dependencies$1["@geekmidas/cache"] = GEEKMIDAS_VERSIONS["@geekmidas/cache"];
10720
+ if (declares && mail) dependencies$1["@geekmidas/emailkit"] = GEEKMIDAS_VERSIONS["@geekmidas/emailkit"];
11610
10721
  if (database) {
11611
10722
  dependencies$1["@geekmidas/db"] = GEEKMIDAS_VERSIONS["@geekmidas/db"];
11612
10723
  dependencies$1.kysely = DEPENDENCY_VERSIONS.kysely;
11613
10724
  dependencies$1.pg = DEPENDENCY_VERSIONS.pg;
11614
- devDependencies$1["@types/pg"] = "~8.23.1";
10725
+ devDependencies$1["@types/pg"] = DEPENDENCY_VERSIONS["@types/pg"];
11615
10726
  devDependencies$1["@geekmidas/testkit"] = GEEKMIDAS_VERSIONS["@geekmidas/testkit"];
11616
- devDependencies$1["@faker-js/faker"] = "~9.8.0";
10727
+ devDependencies$1["@faker-js/faker"] = DEPENDENCY_VERSIONS["@faker-js/faker"];
11617
10728
  devDependencies$1["kysely-ctl"] = DEPENDENCY_VERSIONS["kysely-ctl"];
11618
- devDependencies$1["vite-tsconfig-paths"] = "~5.1.0";
10729
+ devDependencies$1["vite-tsconfig-paths"] = DEPENDENCY_VERSIONS["vite-tsconfig-paths"];
11619
10730
  }
11620
10731
  if (monorepo) {
11621
10732
  delete devDependencies$1["@biomejs/biome"];
@@ -11672,7 +10783,7 @@ function generateSourceFiles(options, template) {
11672
10783
  * factory system with builders/seeds, and an example spec.
11673
10784
  */
11674
10785
  function generateTestFiles(options, _template) {
11675
- if (!options.database) return [];
10786
+ if (!options.constructs.database) return [];
11676
10787
  const db = databaseFor();
11677
10788
  const declares = !options.monorepo;
11678
10789
  const schema = declares ? "../src/constructs/database.ts" : "~/services/database.ts";
@@ -11840,32 +10951,32 @@ function generateUiPackageFiles(options) {
11840
10951
  "build:storybook": "storybook build -o dist/storybook"
11841
10952
  },
11842
10953
  dependencies: {
11843
- "@radix-ui/react-dialog": "~1.1.4",
11844
- "@radix-ui/react-label": "~2.1.2",
11845
- "@radix-ui/react-separator": "~1.1.2",
11846
- "@radix-ui/react-slot": "~1.2.4",
11847
- "@radix-ui/react-tabs": "~1.1.2",
11848
- "@radix-ui/react-tooltip": "~1.1.6",
11849
- "class-variance-authority": "~0.7.1",
11850
- clsx: "^2.1.1",
11851
- "lucide-react": "~0.562.0",
11852
- "tailwind-merge": "~3.4.0"
10954
+ "@radix-ui/react-dialog": DEPENDENCY_VERSIONS["@radix-ui/react-dialog"],
10955
+ "@radix-ui/react-label": DEPENDENCY_VERSIONS["@radix-ui/react-label"],
10956
+ "@radix-ui/react-separator": DEPENDENCY_VERSIONS["@radix-ui/react-separator"],
10957
+ "@radix-ui/react-slot": DEPENDENCY_VERSIONS["@radix-ui/react-slot"],
10958
+ "@radix-ui/react-tabs": DEPENDENCY_VERSIONS["@radix-ui/react-tabs"],
10959
+ "@radix-ui/react-tooltip": DEPENDENCY_VERSIONS["@radix-ui/react-tooltip"],
10960
+ "class-variance-authority": DEPENDENCY_VERSIONS["class-variance-authority"],
10961
+ clsx: DEPENDENCY_VERSIONS["clsx"],
10962
+ "lucide-react": DEPENDENCY_VERSIONS["lucide-react"],
10963
+ "tailwind-merge": DEPENDENCY_VERSIONS["tailwind-merge"]
11853
10964
  },
11854
10965
  devDependencies: {
11855
- "@storybook/addon-a11y": "^8.4.7",
11856
- "@storybook/addon-essentials": "^8.4.7",
11857
- "@storybook/addon-interactions": "^8.4.7",
11858
- "@storybook/react": "^8.4.7",
11859
- "@storybook/react-vite": "^8.4.7",
11860
- "@tailwindcss/vite": "^4.0.0",
11861
- "@types/react": "^19.0.0",
11862
- "@types/react-dom": "^19.0.0",
11863
- react: "^19.0.0",
11864
- "react-dom": "^19.0.0",
11865
- storybook: "^8.4.7",
11866
- tailwindcss: "^4.0.0",
11867
- typescript: "^5.8.2",
11868
- vite: "^6.0.0"
10966
+ "@storybook/addon-a11y": TOOLCHAIN_VERSIONS["@storybook/addon-a11y"],
10967
+ "@storybook/addon-essentials": TOOLCHAIN_VERSIONS["@storybook/addon-essentials"],
10968
+ "@storybook/addon-interactions": TOOLCHAIN_VERSIONS["@storybook/addon-interactions"],
10969
+ "@storybook/react": TOOLCHAIN_VERSIONS["@storybook/react"],
10970
+ "@storybook/react-vite": TOOLCHAIN_VERSIONS["@storybook/react-vite"],
10971
+ "@tailwindcss/vite": DEPENDENCY_VERSIONS["@tailwindcss/vite"],
10972
+ "@types/react": DEPENDENCY_VERSIONS["@types/react"],
10973
+ "@types/react-dom": DEPENDENCY_VERSIONS["@types/react-dom"],
10974
+ react: DEPENDENCY_VERSIONS["react"],
10975
+ "react-dom": DEPENDENCY_VERSIONS["react-dom"],
10976
+ storybook: TOOLCHAIN_VERSIONS["storybook"],
10977
+ tailwindcss: DEPENDENCY_VERSIONS["tailwindcss"],
10978
+ typescript: TOOLCHAIN_VERSIONS["typescript"],
10979
+ vite: TOOLCHAIN_VERSIONS["vite@storybook"]
11869
10980
  },
11870
10981
  peerDependencies: {
11871
10982
  react: ">=18.0.0",
@@ -13258,19 +12369,19 @@ function generateWebAppFiles(options) {
13258
12369
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
13259
12370
  "@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
13260
12371
  "better-auth": DEPENDENCY_VERSIONS["better-auth"],
13261
- next: "~16.1.0",
13262
- react: "~19.2.0",
13263
- "react-dom": "~19.2.0"
12372
+ next: DEPENDENCY_VERSIONS["next"],
12373
+ react: DEPENDENCY_VERSIONS["react"],
12374
+ "react-dom": DEPENDENCY_VERSIONS["react-dom"]
13264
12375
  },
13265
12376
  devDependencies: {
13266
12377
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
13267
- "@tailwindcss/postcss": "^4.0.0",
13268
- "@types/node": "~22.0.0",
13269
- "@types/react": "~19.0.0",
13270
- "@types/react-dom": "~19.0.0",
13271
- tailwindcss: "^4.0.0",
13272
- tsx: "~4.20.0",
13273
- typescript: "~5.8.2"
12378
+ "@tailwindcss/postcss": DEPENDENCY_VERSIONS["@tailwindcss/postcss"],
12379
+ "@types/node": DEPENDENCY_VERSIONS["@types/node"],
12380
+ "@types/react": DEPENDENCY_VERSIONS["@types/react"],
12381
+ "@types/react-dom": DEPENDENCY_VERSIONS["@types/react-dom"],
12382
+ tailwindcss: DEPENDENCY_VERSIONS["tailwindcss"],
12383
+ tsx: TOOLCHAIN_VERSIONS["tsx"],
12384
+ typescript: TOOLCHAIN_VERSIONS["typescript"]
13274
12385
  }
13275
12386
  };
13276
12387
  const nextConfig = `import type { NextConfig } from 'next';
@@ -13590,25 +12701,25 @@ function generateTanStackWebFiles(options) {
13590
12701
  "@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
13591
12702
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
13592
12703
  "@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
13593
- "@tanstack/react-router": "^1.87.0",
13594
- "@tanstack/react-start": "^1.87.0",
12704
+ "@tanstack/react-router": DEPENDENCY_VERSIONS["@tanstack/react-router"],
12705
+ "@tanstack/react-start": DEPENDENCY_VERSIONS["@tanstack/react-start"],
13595
12706
  "better-auth": DEPENDENCY_VERSIONS["better-auth"],
13596
- react: "~19.2.0",
13597
- "react-dom": "~19.2.0"
12707
+ react: DEPENDENCY_VERSIONS["react"],
12708
+ "react-dom": DEPENDENCY_VERSIONS["react-dom"]
13598
12709
  },
13599
12710
  devDependencies: {
13600
12711
  "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
13601
- "@tailwindcss/vite": "^4.0.0",
13602
- "@tanstack/router-plugin": "^1.87.0",
13603
- "@types/node": "~22.0.0",
13604
- "@types/react": "~19.0.0",
13605
- "@types/react-dom": "~19.0.0",
13606
- "@vitejs/plugin-react": "^4.3.4",
13607
- tailwindcss: "^4.0.0",
13608
- tsx: "~4.20.0",
13609
- typescript: "~5.8.2",
13610
- vite: "^7.0.0",
13611
- "vite-tsconfig-paths": "~5.1.0"
12712
+ "@tailwindcss/vite": DEPENDENCY_VERSIONS["@tailwindcss/vite"],
12713
+ "@tanstack/router-plugin": DEPENDENCY_VERSIONS["@tanstack/router-plugin"],
12714
+ "@types/node": DEPENDENCY_VERSIONS["@types/node"],
12715
+ "@types/react": DEPENDENCY_VERSIONS["@types/react"],
12716
+ "@types/react-dom": DEPENDENCY_VERSIONS["@types/react-dom"],
12717
+ "@vitejs/plugin-react": TOOLCHAIN_VERSIONS["@vitejs/plugin-react"],
12718
+ tailwindcss: DEPENDENCY_VERSIONS["tailwindcss"],
12719
+ tsx: TOOLCHAIN_VERSIONS["tsx"],
12720
+ typescript: TOOLCHAIN_VERSIONS["typescript"],
12721
+ vite: TOOLCHAIN_VERSIONS["vite"],
12722
+ "vite-tsconfig-paths": DEPENDENCY_VERSIONS["vite-tsconfig-paths"]
13612
12723
  }
13613
12724
  };
13614
12725
  const viteConfig = `import { Credentials } from '@geekmidas/envkit/credentials';
@@ -14144,21 +13255,14 @@ async function initCommand(projectName, options = {}) {
14144
13255
  },
14145
13256
  {
14146
13257
  type: options.yes ? null : "multiselect",
14147
- name: "services",
14148
- message: "Services (space to select, enter to confirm):",
14149
- choices: servicesChoices.map((c) => ({
13258
+ name: "constructs",
13259
+ message: "Constructs to declare (space to select, enter to confirm):",
13260
+ choices: constructChoices.map((c) => ({
14150
13261
  ...c,
14151
13262
  selected: true
14152
13263
  })),
14153
13264
  hint: "- Space to select. Return to submit"
14154
13265
  },
14155
- {
14156
- type: options.yes ? null : "select",
14157
- name: "eventsBackend",
14158
- message: "Event backend:",
14159
- choices: eventsBackendChoices,
14160
- initial: 0
14161
- },
14162
13266
  {
14163
13267
  type: options.yes ? null : "select",
14164
13268
  name: "packageManager",
@@ -14259,30 +13363,21 @@ async function initCommand(projectName, options = {}) {
14259
13363
  const template = options.template || answers.template || "api";
14260
13364
  const isFullstack = isFullstackTemplate(template);
14261
13365
  const monorepo = isFullstack || options.monorepo || false;
14262
- const servicesArray = options.yes ? [
14263
- "db",
14264
- "cache",
14265
- "mail",
14266
- "storage"
14267
- ] : answers.services || [];
14268
- const eventsBackend = options.yes ? "pgboss" : answers.eventsBackend;
14269
- const services = {
14270
- db: servicesArray.includes("db"),
14271
- cache: servicesArray.includes("cache"),
14272
- mail: servicesArray.includes("mail"),
14273
- storage: servicesArray.includes("storage"),
14274
- events: eventsBackend
13366
+ const chosen = options.yes ? constructChoices.map((c) => c.value) : answers.constructs || [];
13367
+ const constructs = {
13368
+ database: isFullstack || chosen.includes("database"),
13369
+ cache: chosen.includes("cache"),
13370
+ uploads: chosen.includes("uploads"),
13371
+ mail: chosen.includes("mail")
14275
13372
  };
14276
- if (services.events === "pgboss") services.db = true;
14277
13373
  const pkgManager = options.pm ? options.pm : options.yes ? "pnpm" : answers.packageManager ?? detectedPkgManager;
14278
13374
  const deployTarget = options.deploy ?? (options.yes ? "none" : answers.deployTarget ?? "none");
14279
- const database = services.db;
13375
+ const database = constructs.database;
14280
13376
  const frontendFramework = isFullstack ? options.yes ? "nextjs" : answers.frontendFramework ?? "nextjs" : void 0;
14281
13377
  const templateOptions = {
14282
13378
  name: name$1,
14283
13379
  template,
14284
13380
  telescope: options.yes ? true : answers.telescope ?? true,
14285
- database,
14286
13381
  studio: database,
14287
13382
  loggerType: options.yes ? "pino" : answers.loggerType ?? "pino",
14288
13383
  routesStructure: options.yes ? "centralized-endpoints" : answers.routesStructure ?? "centralized-endpoints",
@@ -14292,7 +13387,7 @@ async function initCommand(projectName, options = {}) {
14292
13387
  deployTarget,
14293
13388
  stages: resolveStages(options, answers),
14294
13389
  ...deployTarget === "sst" ? { region: options.region ?? (options.yes ? "eu-west-1" : answers.region?.trim()) } : {},
14295
- services,
13390
+ constructs,
14296
13391
  frontendFramework
14297
13392
  };
14298
13393
  const targetDir = (0, node_path.join)(cwd, name$1);
@@ -14303,23 +13398,13 @@ async function initCommand(projectName, options = {}) {
14303
13398
  await (0, node_fs_promises.mkdir)(targetDir, { recursive: true });
14304
13399
  const appDir = isMonorepo$1 ? (0, node_path.join)(targetDir, apiPath) : targetDir;
14305
13400
  if (isMonorepo$1) await (0, node_fs_promises.mkdir)(appDir, { recursive: true });
14306
- const dbApps = [];
14307
- if (isFullstack && services.db) dbApps.push({
14308
- name: "api",
14309
- password: require_fullstack_secrets.generateDbPassword()
14310
- }, {
14311
- name: "auth",
14312
- password: require_fullstack_secrets.generateDbPassword()
14313
- });
14314
13401
  const appFiles = baseTemplate ? [
14315
13402
  ...generatePackageJson(templateOptions, baseTemplate),
14316
13403
  ...generateConfigFiles(templateOptions, baseTemplate),
14317
13404
  ...generateEnvFiles(templateOptions, baseTemplate),
14318
13405
  ...generateSourceFiles(templateOptions, baseTemplate),
14319
- ...generateTestFiles(templateOptions, baseTemplate),
14320
- ...isMonorepo$1 ? [] : generateDockerFiles(templateOptions, baseTemplate, dbApps)
13406
+ ...generateTestFiles(templateOptions, baseTemplate)
14321
13407
  ] : [];
14322
- const dockerFiles = isMonorepo$1 && baseTemplate ? generateDockerFiles(templateOptions, baseTemplate, dbApps) : [];
14323
13408
  const rootFiles = baseTemplate ? [
14324
13409
  ...generateAgentFiles(templateOptions, baseTemplate),
14325
13410
  ...generateGithubFiles(templateOptions),
@@ -14335,11 +13420,6 @@ async function initCommand(projectName, options = {}) {
14335
13420
  await (0, node_fs_promises.mkdir)((0, node_path.dirname)(fullPath), { recursive: true });
14336
13421
  await (0, node_fs_promises.writeFile)(fullPath, content);
14337
13422
  }
14338
- for (const { path, content } of dockerFiles) {
14339
- const fullPath = (0, node_path.join)(targetDir, path);
14340
- await (0, node_fs_promises.mkdir)((0, node_path.dirname)(fullPath), { recursive: true });
14341
- await (0, node_fs_promises.writeFile)(fullPath, content);
14342
- }
14343
13423
  for (const { path, content } of appFiles) {
14344
13424
  const fullPath = (0, node_path.join)(appDir, path);
14345
13425
  await (0, node_fs_promises.mkdir)((0, node_path.dirname)(fullPath), { recursive: true });
@@ -14361,30 +13441,14 @@ async function initCommand(projectName, options = {}) {
14361
13441
  await (0, node_fs_promises.writeFile)(fullPath, content);
14362
13442
  }
14363
13443
  console.log("🔐 Initializing encrypted secrets...\n");
14364
- const secretServices = [];
14365
- if (services.db) secretServices.push("postgres");
14366
- if (services.cache) secretServices.push("redis");
14367
- if (services.storage) secretServices.push("minio");
14368
- if (services.mail) secretServices.push("mailpit");
14369
- if (services.events === "sns") secretServices.push("localstack");
14370
- if (services.events === "rabbitmq") secretServices.push("rabbitmq");
14371
13444
  const local = templateOptions.stages.local;
14372
- const devSecrets = require_generator.createStageSecrets(local, secretServices, {
14373
- projectName: name$1,
14374
- eventsBackend: services.events
14375
- });
13445
+ const devSecrets = require_generator.createStageSecrets(local, [], { projectName: name$1 });
14376
13446
  const customSecrets = {
14377
13447
  PORT: "3000",
14378
13448
  LOG_LEVEL: "debug",
14379
13449
  JWT_SECRET: `dev-${Date.now()}-${Math.random().toString(36).slice(2)}`
14380
13450
  };
14381
- if (isFullstack && dbApps.length > 0) {
14382
- for (const app of dbApps) {
14383
- const urlKey = `${app.name.toUpperCase()}_DATABASE_URL`;
14384
- customSecrets[urlKey] = require_fullstack_secrets.generateDbUrl(app.name, app.password, name$1);
14385
- const passwordKey = `${app.name.toUpperCase()}_DB_PASSWORD`;
14386
- customSecrets[passwordKey] = app.password;
14387
- }
13451
+ if (isFullstack) {
14388
13452
  customSecrets.AUTH_PORT = "3002";
14389
13453
  customSecrets.AUTH_URL = "http://localhost:3002";
14390
13454
  customSecrets.BETTER_AUTH_SECRET = `better-auth-${Date.now()}-${Math.random().toString(36).slice(2)}`;
@@ -14449,12 +13513,8 @@ function printNextSteps(projectName, options, pkgManager) {
14449
13513
  console.log("\n✅ Project created successfully!\n");
14450
13514
  console.log("Next steps:\n");
14451
13515
  console.log(` ${cdCommand}`);
14452
- if (options.services.db && options.monorepo) {
14453
- console.log(` # Start PostgreSQL (if not running)`);
14454
- console.log(` docker compose up -d postgres`);
14455
- }
14456
13516
  console.log(` ${devCommand$1}`);
14457
- if (!options.monorepo && options.services.db) {
13517
+ if (!options.monorepo && options.constructs.database) {
14458
13518
  console.log("");
14459
13519
  console.log(" # Then, once the container is up:");
14460
13520
  console.log(` gkm exec -- pnpm kysely migrate:latest`);
@@ -14500,7 +13560,7 @@ function printNextSteps(projectName, options, pkgManager) {
14500
13560
  console.log("🚀 Deployment (AWS, through SST):");
14501
13561
  for (const stage of deployed) console.log(` ${getRunCommand(pkgManager, `deploy:${stage}`)} # gkm build --provider aws, then sst deploy`);
14502
13562
  console.log(` Uses your AWS credentials, in ${options.region}.`);
14503
- if (options.services.mail) console.log(" Set MAIL_FROM to a sender verified in SES first.");
13563
+ if (options.constructs.mail) console.log(" Set MAIL_FROM to a sender verified in SES first.");
14504
13564
  console.log("");
14505
13565
  }
14506
13566
  console.log("📚 Documentation: https://geekmidas.github.io/toolbox/");
@@ -14547,14 +13607,6 @@ var NotAnAwsRegion = class extends Error {
14547
13607
  //#region src/secrets/index.ts
14548
13608
  const logger$1 = console;
14549
13609
  /**
14550
- * Extract service names from compose config.
14551
- */
14552
- function getServicesFromConfig(services) {
14553
- if (!services) return [];
14554
- if (Array.isArray(services)) return services;
14555
- return Object.entries(services).filter(([, config]) => config).map(([name$1]) => name$1);
14556
- }
14557
- /**
14558
13610
  * Initialize secrets for a stage.
14559
13611
  * Generates secure random passwords for configured services.
14560
13612
  */
@@ -14564,9 +13616,6 @@ async function secretsInitCommand(options) {
14564
13616
  logger$1.error(`Secrets already exist for stage "${stage}". Use --force to overwrite.`);
14565
13617
  process.exit(1);
14566
13618
  }
14567
- const config = await require_config.loadConfig();
14568
- const services = getServicesFromConfig(config.docker?.compose?.services);
14569
- if (services.length === 0) logger$1.warn("No services configured in docker.compose.services. Creating secrets with empty services.");
14570
13619
  let projectName;
14571
13620
  let workspaceSecrets;
14572
13621
  try {
@@ -14578,13 +13627,11 @@ async function secretsInitCommand(options) {
14578
13627
  logger$1.log(" Detected workspace mode — generating per-app secrets");
14579
13628
  }
14580
13629
  } catch {}
14581
- const secrets = require_generator.createStageSecrets(stage, services, { projectName });
13630
+ const secrets = require_generator.createStageSecrets(stage, [], { projectName });
14582
13631
  if (workspaceSecrets) secrets.custom = workspaceSecrets;
14583
13632
  await require_storage.writeStageSecrets(secrets);
14584
13633
  logger$1.log(`\n✓ Secrets initialized for stage "${stage}"`);
14585
13634
  logger$1.log(` Location: .gkm/secrets/${stage}.json`);
14586
- logger$1.log("\n Generated credentials for:");
14587
- for (const service of services) logger$1.log(` - ${service}`);
14588
13635
  if (secrets.urls.DATABASE_URL) logger$1.log(`\n DATABASE_URL: ${maskUrl(secrets.urls.DATABASE_URL)}`);
14589
13636
  if (secrets.urls.REDIS_URL) logger$1.log(` REDIS_URL: ${maskUrl(secrets.urls.REDIS_URL)}`);
14590
13637
  if (secrets.urls.RABBITMQ_URL) logger$1.log(` RABBITMQ_URL: ${maskUrl(secrets.urls.RABBITMQ_URL)}`);
@@ -14758,7 +13805,7 @@ async function testCommand(options = {}) {
14758
13805
  console.log(`\n🧪 Running tests with ${stage} environment...\n`);
14759
13806
  const autoSetup = options.autoSetup || Boolean(process.env.GKM_AUTO_SETUP);
14760
13807
  if (autoSetup) {
14761
- const { ensureStageSecrets } = await Promise.resolve().then(() => require("./setup-lCx_DcUo.cjs"));
13808
+ const { ensureStageSecrets } = await Promise.resolve().then(() => require("./setup-BrR1_xGy.cjs"));
14762
13809
  const generated = await ensureStageSecrets(stage, cwd);
14763
13810
  if (generated) console.log(` 🔐 Generated fresh ${stage} secrets (auto-setup)`);
14764
13811
  }
@@ -15395,13 +14442,13 @@ program.command("secrets:push").description("Push secrets to remote provider (SS
15395
14442
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
15396
14443
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await Promise.resolve().then(() => require("./config.cjs"));
15397
14444
  const { pushSecrets } = await Promise.resolve().then(() => require("./sync-DYENfOt4.cjs"));
15398
- const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-CYMhFSYy.cjs"));
14445
+ const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-Bhw5fmKu.cjs"));
15399
14446
  const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-DlVol_oj.cjs"));
15400
14447
  const { workspace } = await loadWorkspaceConfig$1();
15401
14448
  const secrets = await readStageSecrets$1(options.stage, workspace.root);
15402
14449
  if (secrets) {
15403
- const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-DQxxs7Jk.cjs"));
15404
- const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
14450
+ const { derivedContainers } = await Promise.resolve().then(() => require("./workspace-Cji6lE8E.cjs"));
14451
+ const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, options.stage));
15405
14452
  if (result) {
15406
14453
  await writeStageSecrets$1(result.secrets, workspace.root);
15407
14454
  console.log(` Reconciled ${result.addedKeys.length} missing secret(s):`);
@@ -15422,15 +14469,15 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
15422
14469
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await Promise.resolve().then(() => require("./config.cjs"));
15423
14470
  const { pullSecrets } = await Promise.resolve().then(() => require("./sync-DYENfOt4.cjs"));
15424
14471
  const { writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-DlVol_oj.cjs"));
15425
- const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-CYMhFSYy.cjs"));
14472
+ const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-Bhw5fmKu.cjs"));
15426
14473
  const { workspace } = await loadWorkspaceConfig$1();
15427
14474
  let secrets = await pullSecrets(options.stage, workspace);
15428
14475
  if (!secrets) {
15429
14476
  console.error(`No remote secrets found for stage "${options.stage}".`);
15430
14477
  process.exit(1);
15431
14478
  }
15432
- const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-DQxxs7Jk.cjs"));
15433
- const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
14479
+ const { derivedContainers } = await Promise.resolve().then(() => require("./workspace-Cji6lE8E.cjs"));
14480
+ const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, options.stage));
15434
14481
  if (result) {
15435
14482
  secrets = result.secrets;
15436
14483
  console.log(` Reconciled ${result.addedKeys.length} missing secret(s):`);
@@ -15448,7 +14495,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
15448
14495
  const globalOptions = program.opts();
15449
14496
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
15450
14497
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await Promise.resolve().then(() => require("./config.cjs"));
15451
- const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-CYMhFSYy.cjs"));
14498
+ const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-Bhw5fmKu.cjs"));
15452
14499
  const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-DlVol_oj.cjs"));
15453
14500
  const { workspace } = await loadWorkspaceConfig$1();
15454
14501
  const stage = options.stage ?? workspace.stages.local;
@@ -15457,8 +14504,8 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
15457
14504
  console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
15458
14505
  process.exit(1);
15459
14506
  }
15460
- const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-DQxxs7Jk.cjs"));
15461
- const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, stage));
14507
+ const { derivedContainers } = await Promise.resolve().then(() => require("./workspace-Cji6lE8E.cjs"));
14508
+ const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
15462
14509
  if (!result) {
15463
14510
  console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
15464
14511
  return;
@@ -15516,7 +14563,7 @@ program.command("deploy:github").description("Let GitHub Actions deploy a stage:
15516
14563
  try {
15517
14564
  const globalOptions = program.opts();
15518
14565
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
15519
- const { deployGithubCommand } = await Promise.resolve().then(() => require("./github-BrvaPkFf.cjs"));
14566
+ const { deployGithubCommand } = await Promise.resolve().then(() => require("./github-BThvbf2W.cjs"));
15520
14567
  await deployGithubCommand(options);
15521
14568
  } catch (error) {
15522
14569
  console.error(formatError(error));