@geekmidas/cli 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/{config-BYn5yUt5.cjs → config-6JHOwLCx.cjs} +30 -2
  3. package/dist/{config-dLNQIvDR.mjs.map → config-6JHOwLCx.cjs.map} +1 -1
  4. package/dist/{config-dLNQIvDR.mjs → config-DxASSNjr.mjs} +25 -3
  5. package/dist/{config-BYn5yUt5.cjs.map → config-DxASSNjr.mjs.map} +1 -1
  6. package/dist/config.cjs +3 -2
  7. package/dist/config.d.cts +14 -2
  8. package/dist/config.d.cts.map +1 -1
  9. package/dist/config.d.mts +14 -2
  10. package/dist/config.d.mts.map +1 -1
  11. package/dist/config.mjs +3 -3
  12. package/dist/{index-Bj5VNxEL.d.mts → index-C-KxSGGK.d.mts} +2 -2
  13. package/dist/{index-Ba21_lNt.d.cts.map → index-C-KxSGGK.d.mts.map} +1 -1
  14. package/dist/{index-Ba21_lNt.d.cts → index-Cyk2rTyj.d.cts} +2 -2
  15. package/dist/{index-Bj5VNxEL.d.mts.map → index-Cyk2rTyj.d.cts.map} +1 -1
  16. package/dist/index.cjs +549 -133
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.mjs +513 -97
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/{openapi-CMTyaIJJ.mjs → openapi-BYlyAbH3.mjs} +6 -5
  21. package/dist/openapi-BYlyAbH3.mjs.map +1 -0
  22. package/dist/{openapi-CqblwJZ4.cjs → openapi-CnvwSRDU.cjs} +6 -5
  23. package/dist/openapi-CnvwSRDU.cjs.map +1 -0
  24. package/dist/openapi.cjs +3 -3
  25. package/dist/openapi.d.cts +1 -0
  26. package/dist/openapi.d.cts.map +1 -1
  27. package/dist/openapi.d.mts +1 -0
  28. package/dist/openapi.d.mts.map +1 -1
  29. package/dist/openapi.mjs +3 -3
  30. package/dist/workspace/index.cjs +1 -1
  31. package/dist/workspace/index.d.cts +1 -1
  32. package/dist/workspace/index.d.mts +1 -1
  33. package/dist/workspace/index.mjs +1 -1
  34. package/dist/{workspace-Dy8k7Wru.mjs → workspace-9IQIjwkQ.mjs} +5 -3
  35. package/dist/workspace-9IQIjwkQ.mjs.map +1 -0
  36. package/dist/{workspace-DIMnYaYt.cjs → workspace-D2ocAlpl.cjs} +5 -3
  37. package/dist/workspace-D2ocAlpl.cjs.map +1 -0
  38. package/package.json +6 -5
  39. package/src/config.ts +44 -0
  40. package/src/dev/__tests__/index.spec.ts +490 -0
  41. package/src/dev/index.ts +313 -18
  42. package/src/generators/Generator.ts +4 -1
  43. package/src/init/__tests__/generators.spec.ts +167 -18
  44. package/src/init/__tests__/init.spec.ts +66 -3
  45. package/src/init/generators/auth.ts +6 -5
  46. package/src/init/generators/config.ts +49 -7
  47. package/src/init/generators/docker.ts +8 -8
  48. package/src/init/generators/index.ts +1 -0
  49. package/src/init/generators/models.ts +3 -5
  50. package/src/init/generators/package.ts +4 -0
  51. package/src/init/generators/test.ts +133 -0
  52. package/src/init/generators/ui.ts +13 -12
  53. package/src/init/generators/web.ts +9 -8
  54. package/src/init/index.ts +2 -0
  55. package/src/init/templates/api.ts +6 -6
  56. package/src/init/templates/minimal.ts +2 -2
  57. package/src/init/templates/worker.ts +2 -2
  58. package/src/init/versions.ts +3 -3
  59. package/src/openapi.ts +6 -2
  60. package/src/test/__tests__/__fixtures__/workspace.ts +104 -0
  61. package/src/test/__tests__/api.spec.ts +199 -0
  62. package/src/test/__tests__/auth.spec.ts +162 -0
  63. package/src/test/__tests__/index.spec.ts +323 -0
  64. package/src/test/__tests__/web.spec.ts +210 -0
  65. package/src/test/index.ts +165 -14
  66. package/src/workspace/__tests__/index.spec.ts +3 -0
  67. package/src/workspace/index.ts +4 -2
  68. package/dist/openapi-CMTyaIJJ.mjs.map +0 -1
  69. package/dist/openapi-CqblwJZ4.cjs.map +0 -1
  70. package/dist/workspace-DIMnYaYt.cjs.map +0 -1
  71. package/dist/workspace-Dy8k7Wru.mjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  const require_chunk = require('./chunk-CUT6urMc.cjs');
3
- const require_workspace = require('./workspace-DIMnYaYt.cjs');
4
- const require_config = require('./config-BYn5yUt5.cjs');
3
+ const require_workspace = require('./workspace-D2ocAlpl.cjs');
4
+ const require_config = require('./config-6JHOwLCx.cjs');
5
5
  const require_credentials = require('./credentials-C8DWtnMY.cjs');
6
- const require_openapi = require('./openapi-CqblwJZ4.cjs');
6
+ const require_openapi = require('./openapi-CnvwSRDU.cjs');
7
7
  const require_storage = require('./storage-CoCNe0Pt.cjs');
8
8
  const require_dokploy_api = require('./dokploy-api-DLgvEQlr.cjs');
9
9
  const require_encryption = require('./encryption-BE0UOb8j.cjs');
@@ -20,6 +20,7 @@ const node_net = require_chunk.__toESM(require("node:net"));
20
20
  const chokidar = require_chunk.__toESM(require("chokidar"));
21
21
  const dotenv = require_chunk.__toESM(require("dotenv"));
22
22
  const fast_glob = require_chunk.__toESM(require("fast-glob"));
23
+ const yaml = require_chunk.__toESM(require("yaml"));
23
24
  const __geekmidas_constructs_crons = require_chunk.__toESM(require("@geekmidas/constructs/crons"));
24
25
  const __geekmidas_constructs_functions = require_chunk.__toESM(require("@geekmidas/constructs/functions"));
25
26
  const __geekmidas_constructs_subscribers = require_chunk.__toESM(require("@geekmidas/constructs/subscribers"));
@@ -32,7 +33,7 @@ const prompts = require_chunk.__toESM(require("prompts"));
32
33
 
33
34
  //#region package.json
34
35
  var name = "@geekmidas/cli";
35
- var version = "1.5.0";
36
+ var version = "1.5.1";
36
37
  var description = "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs";
37
38
  var private$1 = false;
38
39
  var type = "module";
@@ -97,7 +98,8 @@ var dependencies = {
97
98
  "openapi-typescript": "^7.4.2",
98
99
  "pg": "~8.17.1",
99
100
  "prompts": "~2.4.2",
100
- "tsx": "~4.20.3"
101
+ "tsx": "~4.20.3",
102
+ "yaml": "~2.8.2"
101
103
  };
102
104
  var devDependencies = {
103
105
  "@geekmidas/testkit": "workspace:*",
@@ -775,6 +777,152 @@ async function findAvailablePort(preferredPort, maxAttempts = 10) {
775
777
  }
776
778
  throw new Error(`Could not find an available port after trying ${maxAttempts} ports starting from ${preferredPort}`);
777
779
  }
780
+ const PORT_STATE_PATH = ".gkm/ports.json";
781
+ /**
782
+ * Parse docker-compose.yml and extract all port mappings that use env var interpolation.
783
+ * Entries like `'${POSTGRES_HOST_PORT:-5432}:5432'` are captured.
784
+ * Fixed port mappings like `'5050:80'` are skipped.
785
+ * @internal Exported for testing
786
+ */
787
+ function parseComposePortMappings(composePath) {
788
+ if (!(0, node_fs.existsSync)(composePath)) return [];
789
+ const content = (0, node_fs.readFileSync)(composePath, "utf-8");
790
+ const compose = (0, yaml.parse)(content);
791
+ if (!compose?.services) return [];
792
+ const results = [];
793
+ for (const [serviceName, serviceConfig] of Object.entries(compose.services)) for (const portMapping of serviceConfig?.ports ?? []) {
794
+ const match = String(portMapping).match(/\$\{(\w+):-(\d+)\}:(\d+)/);
795
+ if (match?.[1] && match[2] && match[3]) results.push({
796
+ service: serviceName,
797
+ envVar: match[1],
798
+ defaultPort: Number(match[2]),
799
+ containerPort: Number(match[3])
800
+ });
801
+ }
802
+ return results;
803
+ }
804
+ /**
805
+ * Load saved port state from .gkm/ports.json.
806
+ * @internal Exported for testing
807
+ */
808
+ async function loadPortState(workspaceRoot) {
809
+ try {
810
+ const raw = await (0, node_fs_promises.readFile)((0, node_path.join)(workspaceRoot, PORT_STATE_PATH), "utf-8");
811
+ return JSON.parse(raw);
812
+ } catch {
813
+ return {};
814
+ }
815
+ }
816
+ /**
817
+ * Save port state to .gkm/ports.json.
818
+ * @internal Exported for testing
819
+ */
820
+ async function savePortState(workspaceRoot, ports) {
821
+ const dir = (0, node_path.join)(workspaceRoot, ".gkm");
822
+ await (0, node_fs_promises.mkdir)(dir, { recursive: true });
823
+ await (0, node_fs_promises.writeFile)((0, node_path.join)(workspaceRoot, PORT_STATE_PATH), `${JSON.stringify(ports, null, 2)}\n`);
824
+ }
825
+ /**
826
+ * Check if a project's own Docker container is running and return its host port.
827
+ * Uses `docker compose port` scoped to the project's compose file.
828
+ * @internal Exported for testing
829
+ */
830
+ function getContainerHostPort(workspaceRoot, service, containerPort) {
831
+ try {
832
+ const result = (0, node_child_process.execSync)(`docker compose port ${service} ${containerPort}`, {
833
+ cwd: workspaceRoot,
834
+ stdio: "pipe"
835
+ }).toString().trim();
836
+ const match = result.match(/:(\d+)$/);
837
+ return match ? Number(match[1]) : null;
838
+ } catch {
839
+ return null;
840
+ }
841
+ }
842
+ /**
843
+ * Resolve host ports for Docker services by parsing docker-compose.yml.
844
+ * Priority: running container → saved state → find available port.
845
+ * Persists resolved ports to .gkm/ports.json.
846
+ * @internal Exported for testing
847
+ */
848
+ async function resolveServicePorts(workspaceRoot) {
849
+ const composePath = (0, node_path.join)(workspaceRoot, "docker-compose.yml");
850
+ const mappings = parseComposePortMappings(composePath);
851
+ if (mappings.length === 0) return {
852
+ dockerEnv: {},
853
+ ports: {},
854
+ mappings: []
855
+ };
856
+ const savedState = await loadPortState(workspaceRoot);
857
+ const dockerEnv = {};
858
+ const ports = {};
859
+ logger$9.log("\n🔌 Resolving service ports...");
860
+ for (const mapping of mappings) {
861
+ const containerPort = getContainerHostPort(workspaceRoot, mapping.service, mapping.containerPort);
862
+ if (containerPort !== null) {
863
+ ports[mapping.envVar] = containerPort;
864
+ dockerEnv[mapping.envVar] = String(containerPort);
865
+ logger$9.log(` 🔄 ${mapping.service}:${mapping.containerPort}: reusing existing container on port ${containerPort}`);
866
+ continue;
867
+ }
868
+ const savedPort = savedState[mapping.envVar];
869
+ if (savedPort && await isPortAvailable(savedPort)) {
870
+ ports[mapping.envVar] = savedPort;
871
+ dockerEnv[mapping.envVar] = String(savedPort);
872
+ logger$9.log(` 💾 ${mapping.service}:${mapping.containerPort}: using saved port ${savedPort}`);
873
+ continue;
874
+ }
875
+ const resolvedPort = await findAvailablePort(mapping.defaultPort);
876
+ ports[mapping.envVar] = resolvedPort;
877
+ dockerEnv[mapping.envVar] = String(resolvedPort);
878
+ if (resolvedPort !== mapping.defaultPort) logger$9.log(` ⚡ ${mapping.service}:${mapping.containerPort}: port ${mapping.defaultPort} occupied, using port ${resolvedPort}`);
879
+ else logger$9.log(` ✅ ${mapping.service}:${mapping.containerPort}: using default port ${resolvedPort}`);
880
+ }
881
+ await savePortState(workspaceRoot, ports);
882
+ return {
883
+ dockerEnv,
884
+ ports,
885
+ mappings
886
+ };
887
+ }
888
+ /**
889
+ * Replace a port in a URL string.
890
+ * Handles both `hostname:port` and `localhost:port` patterns.
891
+ * @internal Exported for testing
892
+ */
893
+ function replacePortInUrl(url, oldPort, newPort) {
894
+ if (oldPort === newPort) return url;
895
+ return url.replace(new RegExp(`:${oldPort}(?=/|$)`, "g"), `:${newPort}`);
896
+ }
897
+ /**
898
+ * Rewrite connection URLs and port vars in secrets with resolved ports.
899
+ * Uses the parsed compose mappings to determine which default ports to replace.
900
+ * Pure transform — does not modify secrets on disk.
901
+ * @internal Exported for testing
902
+ */
903
+ function rewriteUrlsWithPorts(secrets, resolvedPorts) {
904
+ const { ports, mappings } = resolvedPorts;
905
+ const result = { ...secrets };
906
+ const portReplacements = [];
907
+ for (const mapping of mappings) {
908
+ const resolved = ports[mapping.envVar];
909
+ if (resolved !== void 0) portReplacements.push({
910
+ defaultPort: mapping.defaultPort,
911
+ resolvedPort: resolved
912
+ });
913
+ }
914
+ for (const [key, value] of Object.entries(result)) {
915
+ if (!key.endsWith("_PORT")) continue;
916
+ for (const { defaultPort, resolvedPort } of portReplacements) if (value === String(defaultPort)) result[key] = String(resolvedPort);
917
+ }
918
+ for (const [key, value] of Object.entries(result)) {
919
+ if (!key.endsWith("_URL") && key !== "DATABASE_URL") continue;
920
+ let rewritten = value;
921
+ for (const { defaultPort, resolvedPort } of portReplacements) rewritten = replacePortInUrl(rewritten, defaultPort, resolvedPort);
922
+ result[key] = rewritten;
923
+ }
924
+ return result;
925
+ }
778
926
  /**
779
927
  * Normalize telescope configuration
780
928
  * @internal Exported for testing
@@ -1002,8 +1150,11 @@ async function devCommand(options) {
1002
1150
  rebuildTimeout = setTimeout(async () => {
1003
1151
  try {
1004
1152
  logger$9.log("🔄 Rebuilding...");
1005
- await buildServer(config, buildContext, resolved.providers[0], enableOpenApi, appRoot);
1006
- if (enableOpenApi) await require_openapi.generateOpenApi(config, { silent: true });
1153
+ await buildServer(config, buildContext, resolved.providers[0], enableOpenApi, appRoot, true);
1154
+ if (enableOpenApi) await require_openapi.generateOpenApi(config, {
1155
+ silent: true,
1156
+ bustCache: true
1157
+ });
1007
1158
  logger$9.log("✅ Rebuild complete, restarting server...");
1008
1159
  await devServer.restart();
1009
1160
  } catch (error) {
@@ -1156,7 +1307,7 @@ async function loadSecretsForApp(secretsRoot, appName) {
1156
1307
  * Start docker-compose services for the workspace.
1157
1308
  * @internal Exported for testing
1158
1309
  */
1159
- async function startWorkspaceServices(workspace) {
1310
+ async function startWorkspaceServices(workspace, portEnv) {
1160
1311
  const services = workspace.services;
1161
1312
  if (!services.db && !services.cache && !services.mail) return;
1162
1313
  const servicesToStart = [];
@@ -1173,7 +1324,11 @@ async function startWorkspaceServices(workspace) {
1173
1324
  }
1174
1325
  (0, node_child_process.execSync)(`docker compose up -d ${servicesToStart.join(" ")}`, {
1175
1326
  cwd: workspace.root,
1176
- stdio: "inherit"
1327
+ stdio: "inherit",
1328
+ env: {
1329
+ ...process.env,
1330
+ ...portEnv
1331
+ }
1177
1332
  });
1178
1333
  logger$9.log("✅ Services started");
1179
1334
  } catch (error) {
@@ -1221,8 +1376,9 @@ async function workspaceDevCommand(workspace, options) {
1221
1376
  const copiedCount = clientResults.filter((r) => r.success).length;
1222
1377
  if (copiedCount > 0) logger$9.log(`\n📦 Copied ${copiedCount} API client(s)`);
1223
1378
  }
1224
- await startWorkspaceServices(workspace);
1225
- const secretsEnv = await loadDevSecrets(workspace);
1379
+ const resolvedPorts = await resolveServicePorts(workspace.root);
1380
+ await startWorkspaceServices(workspace, resolvedPorts.dockerEnv);
1381
+ const secretsEnv = rewriteUrlsWithPorts(await loadDevSecrets(workspace), resolvedPorts);
1226
1382
  if (Object.keys(secretsEnv).length > 0) logger$9.log(` Loaded ${Object.keys(secretsEnv).length} secret(s)`);
1227
1383
  const dependencyEnv = generateAllDependencyEnvVars(workspace);
1228
1384
  if (Object.keys(dependencyEnv).length > 0) {
@@ -1347,16 +1503,16 @@ async function workspaceDevCommand(workspace, options) {
1347
1503
  });
1348
1504
  });
1349
1505
  }
1350
- async function buildServer(config, context, provider, enableOpenApi, appRoot = process.cwd()) {
1506
+ async function buildServer(config, context, provider, enableOpenApi, appRoot = process.cwd(), bustCache = false) {
1351
1507
  const endpointGenerator = new require_openapi.EndpointGenerator();
1352
1508
  const functionGenerator = new FunctionGenerator();
1353
1509
  const cronGenerator = new CronGenerator();
1354
1510
  const subscriberGenerator = new SubscriberGenerator();
1355
1511
  const [allEndpoints, allFunctions, allCrons, allSubscribers] = await Promise.all([
1356
- endpointGenerator.load(config.routes, appRoot),
1357
- config.functions ? functionGenerator.load(config.functions, appRoot) : [],
1358
- config.crons ? cronGenerator.load(config.crons, appRoot) : [],
1359
- config.subscribers ? subscriberGenerator.load(config.subscribers, appRoot) : []
1512
+ endpointGenerator.load(config.routes, appRoot, bustCache),
1513
+ config.functions ? functionGenerator.load(config.functions, appRoot, bustCache) : [],
1514
+ config.crons ? cronGenerator.load(config.crons, appRoot, bustCache) : [],
1515
+ config.subscribers ? subscriberGenerator.load(config.subscribers, appRoot, bustCache) : []
1360
1516
  ]);
1361
1517
  const outputDir = (0, node_path.join)(appRoot, ".gkm", provider);
1362
1518
  await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
@@ -1445,10 +1601,10 @@ async function prepareEntryCredentials(options) {
1445
1601
  let secretsRoot = cwd;
1446
1602
  let appName;
1447
1603
  try {
1448
- const appConfig = await require_config.loadAppConfig(cwd);
1449
- workspaceAppPort = appConfig.app.port;
1450
- secretsRoot = appConfig.workspaceRoot;
1451
- appName = appConfig.appName;
1604
+ const appInfo = await require_config.loadWorkspaceAppInfo(cwd);
1605
+ workspaceAppPort = appInfo.app.port;
1606
+ secretsRoot = appInfo.workspaceRoot;
1607
+ appName = appInfo.appName;
1452
1608
  } catch (error) {
1453
1609
  logger$9.log(`⚠️ Could not load workspace config: ${error.message}`);
1454
1610
  secretsRoot = findSecretsRoot(cwd);
@@ -1746,17 +1902,39 @@ async function execCommand(commandArgs, options = {}) {
1746
1902
  if (commandArgs.length === 0) throw new Error("No command specified. Usage: gkm exec -- <command>");
1747
1903
  const defaultEnv = loadEnvFiles(".env");
1748
1904
  if (defaultEnv.loaded.length > 0) logger$9.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
1749
- const { credentials, secretsJsonPath, appName } = await prepareEntryCredentials({ cwd });
1905
+ const { credentials, secretsJsonPath, appName, secretsRoot } = await prepareEntryCredentials({ cwd });
1750
1906
  if (appName) logger$9.log(`📦 App: ${appName}`);
1751
1907
  const secretCount = Object.keys(credentials).filter((k) => k !== "PORT").length;
1752
1908
  if (secretCount > 0) logger$9.log(`🔐 Loaded ${secretCount} secret(s)`);
1909
+ const composePath = (0, node_path.join)(secretsRoot, "docker-compose.yml");
1910
+ const mappings = parseComposePortMappings(composePath);
1911
+ if (mappings.length > 0) {
1912
+ const ports = await loadPortState(secretsRoot);
1913
+ if (Object.keys(ports).length > 0) {
1914
+ const rewritten = rewriteUrlsWithPorts(credentials, {
1915
+ dockerEnv: {},
1916
+ ports,
1917
+ mappings
1918
+ });
1919
+ Object.assign(credentials, rewritten);
1920
+ logger$9.log(`🔌 Applied ${Object.keys(ports).length} port mapping(s)`);
1921
+ }
1922
+ }
1923
+ try {
1924
+ const appInfo = await require_config.loadWorkspaceAppInfo(cwd);
1925
+ if (appInfo.appName) {
1926
+ const depEnv = require_workspace.getDependencyEnvVars(appInfo.workspace, appInfo.appName);
1927
+ Object.assign(credentials, depEnv);
1928
+ }
1929
+ } catch {}
1753
1930
  const preloadDir = (0, node_path.join)(cwd, ".gkm");
1754
1931
  await (0, node_fs_promises.mkdir)(preloadDir, { recursive: true });
1755
1932
  const preloadPath = (0, node_path.join)(preloadDir, "credentials-preload.ts");
1756
1933
  await createCredentialsPreload(preloadPath, secretsJsonPath);
1757
- const [cmd, ...args] = commandArgs;
1934
+ const [cmd, ...rawArgs] = commandArgs;
1758
1935
  if (!cmd) throw new Error("No command specified");
1759
- logger$9.log(`🚀 Running: ${commandArgs.join(" ")}`);
1936
+ const args = rawArgs.map((arg) => arg.replace(/\$PORT\b/g, credentials.PORT ?? "3000"));
1937
+ logger$9.log(`🚀 Running: ${[cmd, ...args].join(" ")}`);
1760
1938
  const existingNodeOptions = process.env.NODE_OPTIONS ?? "";
1761
1939
  const tsxImport = "--import=tsx";
1762
1940
  const preloadImport = `--import=${preloadPath}`;
@@ -2637,7 +2815,7 @@ function generateDockerCompose(options) {
2637
2815
  const { imageName, registry, port, healthCheckPath, services } = options;
2638
2816
  const serviceMap = normalizeServices(services);
2639
2817
  const imageRef = registry ? `\${REGISTRY:-${registry}}/` : "";
2640
- let yaml = `version: '3.8'
2818
+ let yaml$1 = `version: '3.8'
2641
2819
 
2642
2820
  services:
2643
2821
  api:
@@ -2652,30 +2830,30 @@ services:
2652
2830
  environment:
2653
2831
  - NODE_ENV=production
2654
2832
  `;
2655
- if (serviceMap.has("postgres")) yaml += ` - DATABASE_URL=\${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/app}
2833
+ if (serviceMap.has("postgres")) yaml$1 += ` - DATABASE_URL=\${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/app}
2656
2834
  `;
2657
- if (serviceMap.has("redis")) yaml += ` - REDIS_URL=\${REDIS_URL:-redis://redis:6379}
2835
+ if (serviceMap.has("redis")) yaml$1 += ` - REDIS_URL=\${REDIS_URL:-redis://redis:6379}
2658
2836
  `;
2659
- if (serviceMap.has("rabbitmq")) yaml += ` - RABBITMQ_URL=\${RABBITMQ_URL:-amqp://rabbitmq:5672}
2837
+ if (serviceMap.has("rabbitmq")) yaml$1 += ` - RABBITMQ_URL=\${RABBITMQ_URL:-amqp://rabbitmq:5672}
2660
2838
  `;
2661
- yaml += ` healthcheck:
2839
+ yaml$1 += ` healthcheck:
2662
2840
  test: ["CMD", "wget", "-q", "--spider", "http://localhost:${port}${healthCheckPath}"]
2663
2841
  interval: 30s
2664
2842
  timeout: 3s
2665
2843
  retries: 3
2666
2844
  `;
2667
2845
  if (serviceMap.size > 0) {
2668
- yaml += ` depends_on:
2846
+ yaml$1 += ` depends_on:
2669
2847
  `;
2670
- for (const serviceName of serviceMap.keys()) yaml += ` ${serviceName}:
2848
+ for (const serviceName of serviceMap.keys()) yaml$1 += ` ${serviceName}:
2671
2849
  condition: service_healthy
2672
2850
  `;
2673
2851
  }
2674
- yaml += ` networks:
2852
+ yaml$1 += ` networks:
2675
2853
  - app-network
2676
2854
  `;
2677
2855
  const postgresImage = serviceMap.get("postgres");
2678
- if (postgresImage) yaml += `
2856
+ if (postgresImage) yaml$1 += `
2679
2857
  postgres:
2680
2858
  image: ${postgresImage}
2681
2859
  container_name: postgres
@@ -2695,7 +2873,7 @@ services:
2695
2873
  - app-network
2696
2874
  `;
2697
2875
  const redisImage = serviceMap.get("redis");
2698
- if (redisImage) yaml += `
2876
+ if (redisImage) yaml$1 += `
2699
2877
  redis:
2700
2878
  image: ${redisImage}
2701
2879
  container_name: redis
@@ -2711,7 +2889,7 @@ services:
2711
2889
  - app-network
2712
2890
  `;
2713
2891
  const rabbitmqImage = serviceMap.get("rabbitmq");
2714
- if (rabbitmqImage) yaml += `
2892
+ if (rabbitmqImage) yaml$1 += `
2715
2893
  rabbitmq:
2716
2894
  image: ${rabbitmqImage}
2717
2895
  container_name: rabbitmq
@@ -2731,21 +2909,21 @@ services:
2731
2909
  networks:
2732
2910
  - app-network
2733
2911
  `;
2734
- yaml += `
2912
+ yaml$1 += `
2735
2913
  volumes:
2736
2914
  `;
2737
- if (serviceMap.has("postgres")) yaml += ` postgres_data:
2915
+ if (serviceMap.has("postgres")) yaml$1 += ` postgres_data:
2738
2916
  `;
2739
- if (serviceMap.has("redis")) yaml += ` redis_data:
2917
+ if (serviceMap.has("redis")) yaml$1 += ` redis_data:
2740
2918
  `;
2741
- if (serviceMap.has("rabbitmq")) yaml += ` rabbitmq_data:
2919
+ if (serviceMap.has("rabbitmq")) yaml$1 += ` rabbitmq_data:
2742
2920
  `;
2743
- yaml += `
2921
+ yaml$1 += `
2744
2922
  networks:
2745
2923
  app-network:
2746
2924
  driver: bridge
2747
2925
  `;
2748
- return yaml;
2926
+ return yaml$1;
2749
2927
  }
2750
2928
  /**
2751
2929
  * Generate a minimal docker-compose.yml for API only
@@ -2794,17 +2972,17 @@ function generateWorkspaceCompose(workspace, options = {}) {
2794
2972
  const hasMail = services.mail !== void 0 && services.mail !== false;
2795
2973
  const postgresImage = getInfraServiceImage("postgres", services.db);
2796
2974
  const redisImage = getInfraServiceImage("redis", services.cache);
2797
- let yaml = `# Docker Compose for ${workspace.name} workspace
2975
+ let yaml$1 = `# Docker Compose for ${workspace.name} workspace
2798
2976
  # Generated by gkm - do not edit manually
2799
2977
 
2800
2978
  services:
2801
2979
  `;
2802
- for (const [appName, app] of apps) yaml += generateAppService(appName, app, apps, {
2980
+ for (const [appName, app] of apps) yaml$1 += generateAppService(appName, app, apps, {
2803
2981
  registry,
2804
2982
  hasPostgres,
2805
2983
  hasRedis
2806
2984
  });
2807
- if (hasPostgres) yaml += `
2985
+ if (hasPostgres) yaml$1 += `
2808
2986
  postgres:
2809
2987
  image: ${postgresImage}
2810
2988
  container_name: ${workspace.name}-postgres
@@ -2823,7 +3001,7 @@ services:
2823
3001
  networks:
2824
3002
  - workspace-network
2825
3003
  `;
2826
- if (hasRedis) yaml += `
3004
+ if (hasRedis) yaml$1 += `
2827
3005
  redis:
2828
3006
  image: ${redisImage}
2829
3007
  container_name: ${workspace.name}-redis
@@ -2838,7 +3016,7 @@ services:
2838
3016
  networks:
2839
3017
  - workspace-network
2840
3018
  `;
2841
- if (hasMail) yaml += `
3019
+ if (hasMail) yaml$1 += `
2842
3020
  mailpit:
2843
3021
  image: axllent/mailpit:latest
2844
3022
  container_name: ${workspace.name}-mailpit
@@ -2849,19 +3027,19 @@ services:
2849
3027
  networks:
2850
3028
  - workspace-network
2851
3029
  `;
2852
- yaml += `
3030
+ yaml$1 += `
2853
3031
  volumes:
2854
3032
  `;
2855
- if (hasPostgres) yaml += ` postgres_data:
3033
+ if (hasPostgres) yaml$1 += ` postgres_data:
2856
3034
  `;
2857
- if (hasRedis) yaml += ` redis_data:
3035
+ if (hasRedis) yaml$1 += ` redis_data:
2858
3036
  `;
2859
- yaml += `
3037
+ yaml$1 += `
2860
3038
  networks:
2861
3039
  workspace-network:
2862
3040
  driver: bridge
2863
3041
  `;
2864
- return yaml;
3042
+ return yaml$1;
2865
3043
  }
2866
3044
  /**
2867
3045
  * Get infrastructure service image with version.
@@ -2889,7 +3067,7 @@ function generateAppService(appName, app, allApps, options) {
2889
3067
  const imageRef = registry ? `\${REGISTRY:-${registry}}/` : "";
2890
3068
  const healthCheckPath = app.type === "frontend" ? "/" : "/health";
2891
3069
  const healthCheckCmd = app.type === "frontend" ? `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}/"]` : `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}${healthCheckPath}"]`;
2892
- let yaml = `
3070
+ let yaml$1 = `
2893
3071
  ${appName}:
2894
3072
  build:
2895
3073
  context: .
@@ -2905,16 +3083,16 @@ function generateAppService(appName, app, allApps, options) {
2905
3083
  `;
2906
3084
  for (const dep of app.dependencies) {
2907
3085
  const depApp = allApps.find(([name$1]) => name$1 === dep)?.[1];
2908
- if (depApp) yaml += ` - ${dep.toUpperCase()}_URL=http://${dep}:${depApp.port}
3086
+ if (depApp) yaml$1 += ` - ${dep.toUpperCase()}_URL=http://${dep}:${depApp.port}
2909
3087
  `;
2910
3088
  }
2911
3089
  if (app.type === "backend") {
2912
- if (hasPostgres) yaml += ` - DATABASE_URL=\${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/app}
3090
+ if (hasPostgres) yaml$1 += ` - DATABASE_URL=\${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/app}
2913
3091
  `;
2914
- if (hasRedis) yaml += ` - REDIS_URL=\${REDIS_URL:-redis://redis:6379}
3092
+ if (hasRedis) yaml$1 += ` - REDIS_URL=\${REDIS_URL:-redis://redis:6379}
2915
3093
  `;
2916
3094
  }
2917
- yaml += ` healthcheck:
3095
+ yaml$1 += ` healthcheck:
2918
3096
  test: ${healthCheckCmd}
2919
3097
  interval: 30s
2920
3098
  timeout: 3s
@@ -2926,16 +3104,16 @@ function generateAppService(appName, app, allApps, options) {
2926
3104
  if (hasRedis) dependencies$1.push("redis");
2927
3105
  }
2928
3106
  if (dependencies$1.length > 0) {
2929
- yaml += ` depends_on:
3107
+ yaml$1 += ` depends_on:
2930
3108
  `;
2931
- for (const dep of dependencies$1) yaml += ` ${dep}:
3109
+ for (const dep of dependencies$1) yaml$1 += ` ${dep}:
2932
3110
  condition: service_healthy
2933
3111
  `;
2934
3112
  }
2935
- yaml += ` networks:
3113
+ yaml$1 += ` networks:
2936
3114
  - workspace-network
2937
3115
  `;
2938
- return yaml;
3116
+ return yaml$1;
2939
3117
  }
2940
3118
 
2941
3119
  //#endregion
@@ -6369,16 +6547,16 @@ const GEEKMIDAS_VERSIONS = {
6369
6547
  "@geekmidas/cache": "~1.0.0",
6370
6548
  "@geekmidas/client": "~1.0.0",
6371
6549
  "@geekmidas/cloud": "~1.0.0",
6372
- "@geekmidas/constructs": "~1.0.4",
6550
+ "@geekmidas/constructs": "~1.0.5",
6373
6551
  "@geekmidas/db": "~1.0.0",
6374
6552
  "@geekmidas/emailkit": "~1.0.0",
6375
- "@geekmidas/envkit": "~1.0.1",
6553
+ "@geekmidas/envkit": "~1.0.2",
6376
6554
  "@geekmidas/errors": "~1.0.0",
6377
6555
  "@geekmidas/events": "~1.0.0",
6378
6556
  "@geekmidas/logger": "~1.0.0",
6379
6557
  "@geekmidas/rate-limit": "~1.0.0",
6380
6558
  "@geekmidas/schema": "~1.0.0",
6381
- "@geekmidas/services": "~1.0.0",
6559
+ "@geekmidas/services": "~1.0.1",
6382
6560
  "@geekmidas/storage": "~1.0.0",
6383
6561
  "@geekmidas/studio": "~1.0.0",
6384
6562
  "@geekmidas/telescope": "~1.0.0",
@@ -6431,6 +6609,7 @@ function generateAuthAppFiles(options) {
6431
6609
  extends: "../../tsconfig.json",
6432
6610
  compilerOptions: {
6433
6611
  noEmit: true,
6612
+ allowImportingTsExtensions: true,
6434
6613
  baseUrl: ".",
6435
6614
  paths: {
6436
6615
  "~/*": ["./src/*"],
@@ -6461,8 +6640,8 @@ export const logger = createLogger();
6461
6640
  const authTs = `import { betterAuth } from 'better-auth';
6462
6641
  import { magicLink } from 'better-auth/plugins';
6463
6642
  import pg from 'pg';
6464
- import { envParser } from './config/env.js';
6465
- import { logger } from './config/logger.js';
6643
+ import { envParser } from './config/env.ts';
6644
+ import { logger } from './config/logger.ts';
6466
6645
 
6467
6646
  // Parse auth-specific config (no defaults - values from secrets)
6468
6647
  const authConfig = envParser
@@ -6505,9 +6684,9 @@ export type Auth = typeof auth;
6505
6684
  const indexTs = `import { Hono } from 'hono';
6506
6685
  import { cors } from 'hono/cors';
6507
6686
  import { serve } from '@hono/node-server';
6508
- import { auth } from './auth.js';
6509
- import { envParser } from './config/env.js';
6510
- import { logger } from './config/logger.js';
6687
+ import { auth } from './auth.ts';
6688
+ import { envParser } from './config/env.ts';
6689
+ import { logger } from './config/logger.ts';
6511
6690
 
6512
6691
  // Parse server config (no defaults - values from secrets)
6513
6692
  const serverConfig = envParser
@@ -6593,6 +6772,20 @@ dist/
6593
6772
  //#endregion
6594
6773
  //#region src/init/generators/config.ts
6595
6774
  /**
6775
+ * Vitest config content with globalSetup for database-enabled apps
6776
+ */
6777
+ const vitestConfigContent = `import { defineConfig } from 'vitest/config';
6778
+ import tsconfigPaths from 'vite-tsconfig-paths';
6779
+
6780
+ export default defineConfig({
6781
+ plugins: [tsconfigPaths()],
6782
+ test: {
6783
+ environment: 'node',
6784
+ globalSetup: './test/globalSetup.ts',
6785
+ },
6786
+ });
6787
+ `;
6788
+ /**
6596
6789
  * Generate configuration files (gkm.config.ts, tsconfig.json, biome.json, turbo.json)
6597
6790
  */
6598
6791
  function generateConfigFiles(options, template) {
@@ -6644,6 +6837,7 @@ export default defineConfig({
6644
6837
  extends: "../../tsconfig.json",
6645
6838
  compilerOptions: {
6646
6839
  noEmit: true,
6840
+ allowImportingTsExtensions: true,
6647
6841
  baseUrl: ".",
6648
6842
  paths: {
6649
6843
  "~/*": ["./src/*"],
@@ -6663,21 +6857,26 @@ export default defineConfig({
6663
6857
  skipLibCheck: true,
6664
6858
  forceConsistentCasingInFileNames: true,
6665
6859
  resolveJsonModule: true,
6666
- declaration: true,
6667
- declarationMap: true,
6668
- outDir: "./dist",
6669
- rootDir: "./src"
6860
+ noEmit: true,
6861
+ allowImportingTsExtensions: true
6670
6862
  },
6671
6863
  include: ["src/**/*.ts"],
6672
6864
  exclude: ["node_modules", "dist"]
6673
6865
  };
6674
- if (options.monorepo) return [{
6675
- path: "gkm.config.ts",
6676
- content: gkmConfig
6677
- }, {
6678
- path: "tsconfig.json",
6679
- content: `${JSON.stringify(tsConfig, null, 2)}\n`
6680
- }];
6866
+ if (options.monorepo) {
6867
+ const files$1 = [{
6868
+ path: "gkm.config.ts",
6869
+ content: gkmConfig
6870
+ }, {
6871
+ path: "tsconfig.json",
6872
+ content: `${JSON.stringify(tsConfig, null, 2)}\n`
6873
+ }];
6874
+ if (options.database) files$1.push({
6875
+ path: "vitest.config.ts",
6876
+ content: vitestConfigContent
6877
+ });
6878
+ return files$1;
6879
+ }
6681
6880
  const biomeConfig = {
6682
6881
  $schema: "https://biomejs.dev/schemas/2.3.0/schema.json",
6683
6882
  vcs: {
@@ -6743,7 +6942,7 @@ export default defineConfig({
6743
6942
  fmt: { outputs: [] }
6744
6943
  }
6745
6944
  };
6746
- return [
6945
+ const files = [
6747
6946
  {
6748
6947
  path: "gkm.config.ts",
6749
6948
  content: gkmConfig
@@ -6761,12 +6960,18 @@ export default defineConfig({
6761
6960
  content: `${JSON.stringify(turboConfig, null, 2)}\n`
6762
6961
  }
6763
6962
  ];
6963
+ if (options.database) files.push({
6964
+ path: "vitest.config.ts",
6965
+ content: vitestConfigContent
6966
+ });
6967
+ return files;
6764
6968
  }
6765
6969
  function generateSingleAppConfigFiles(options, _template, _helpers) {
6766
6970
  const tsConfig = {
6767
6971
  extends: "../../tsconfig.json",
6768
6972
  compilerOptions: {
6769
6973
  noEmit: true,
6974
+ allowImportingTsExtensions: true,
6770
6975
  baseUrl: ".",
6771
6976
  paths: {
6772
6977
  "~/*": ["./src/*"],
@@ -6776,10 +6981,15 @@ function generateSingleAppConfigFiles(options, _template, _helpers) {
6776
6981
  include: ["src/**/*.ts"],
6777
6982
  exclude: ["node_modules", "dist"]
6778
6983
  };
6779
- return [{
6984
+ const files = [{
6780
6985
  path: "tsconfig.json",
6781
6986
  content: `${JSON.stringify(tsConfig, null, 2)}\n`
6782
6987
  }];
6988
+ if (options.database) files.push({
6989
+ path: "vitest.config.ts",
6990
+ content: vitestConfigContent
6991
+ });
6992
+ return files;
6783
6993
  }
6784
6994
 
6785
6995
  //#endregion
@@ -6810,7 +7020,7 @@ function generateDockerFiles(options, template, dbApps) {
6810
7020
  POSTGRES_PASSWORD: postgres
6811
7021
  POSTGRES_DB: ${options.name.replace(/-/g, "_")}_dev
6812
7022
  ports:
6813
- - '5432:5432'
7023
+ - '\${POSTGRES_HOST_PORT:-5432}:5432'
6814
7024
  volumes:
6815
7025
  - postgres_data:/var/lib/postgresql/data${initVolume}
6816
7026
  healthcheck:
@@ -6836,7 +7046,7 @@ function generateDockerFiles(options, template, dbApps) {
6836
7046
  container_name: ${options.name}-redis
6837
7047
  restart: unless-stopped
6838
7048
  ports:
6839
- - '6379:6379'
7049
+ - '\${REDIS_HOST_PORT:-6379}:6379'
6840
7050
  volumes:
6841
7051
  - redis_data:/data
6842
7052
  healthcheck:
@@ -6850,7 +7060,7 @@ function generateDockerFiles(options, template, dbApps) {
6850
7060
  container_name: ${options.name}-serverless-redis
6851
7061
  restart: unless-stopped
6852
7062
  ports:
6853
- - '8079:80'
7063
+ - '\${SRH_HOST_PORT:-8079}:80'
6854
7064
  environment:
6855
7065
  SRH_MODE: env
6856
7066
  SRH_TOKEN: local_dev_token
@@ -6865,7 +7075,7 @@ function generateDockerFiles(options, template, dbApps) {
6865
7075
  container_name: ${options.name}-redis
6866
7076
  restart: unless-stopped
6867
7077
  ports:
6868
- - '6379:6379'
7078
+ - '\${REDIS_HOST_PORT:-6379}:6379'
6869
7079
  volumes:
6870
7080
  - redis_data:/data
6871
7081
  healthcheck:
@@ -6881,8 +7091,8 @@ function generateDockerFiles(options, template, dbApps) {
6881
7091
  container_name: ${options.name}-rabbitmq
6882
7092
  restart: unless-stopped
6883
7093
  ports:
6884
- - '5672:5672'
6885
- - '15672:15672'
7094
+ - '\${RABBITMQ_HOST_PORT:-5672}:5672'
7095
+ - '\${RABBITMQ_MGMT_HOST_PORT:-15672}:15672'
6886
7096
  environment:
6887
7097
  RABBITMQ_DEFAULT_USER: guest
6888
7098
  RABBITMQ_DEFAULT_PASS: guest
@@ -6900,8 +7110,8 @@ function generateDockerFiles(options, template, dbApps) {
6900
7110
  container_name: ${options.name}-mailpit
6901
7111
  restart: unless-stopped
6902
7112
  ports:
6903
- - '1025:1025'
6904
- - '8025:8025'
7113
+ - '\${MAILPIT_SMTP_HOST_PORT:-1025}:1025'
7114
+ - '\${MAILPIT_UI_HOST_PORT:-8025}:8025'
6905
7115
  environment:
6906
7116
  MP_SMTP_AUTH_ACCEPT_ANY: 1
6907
7117
  MP_SMTP_AUTH_ALLOW_INSECURE: 1`);
@@ -7055,10 +7265,8 @@ function generateModelsPackage(options) {
7055
7265
  const tsConfig = {
7056
7266
  extends: "../../tsconfig.json",
7057
7267
  compilerOptions: {
7058
- declaration: true,
7059
- declarationMap: true,
7060
- outDir: "./dist",
7061
- rootDir: "./src"
7268
+ noEmit: true,
7269
+ allowImportingTsExtensions: true
7062
7270
  },
7063
7271
  include: ["src/**/*.ts"],
7064
7272
  exclude: ["node_modules", "dist"]
@@ -7104,7 +7312,7 @@ export type Timestamps = z.infer<typeof TimestampsSchema>;
7104
7312
  export type Pagination = z.infer<typeof PaginationSchema>;
7105
7313
  `;
7106
7314
  const userTs = `import { z } from 'zod';
7107
- import { IdSchema, TimestampsSchema } from './common.js';
7315
+ import { IdSchema, TimestampsSchema } from './common.ts';
7108
7316
 
7109
7317
  // ============================================
7110
7318
  // User Schemas
@@ -7595,7 +7803,7 @@ export const config = envParser
7595
7803
  {
7596
7804
  path: getRoutePath("health.ts"),
7597
7805
  content: monorepo ? `import { z } from 'zod';
7598
- import { publicRouter } from '~/router';
7806
+ import { publicRouter } from '~/router.ts';
7599
7807
 
7600
7808
  export const healthEndpoint = publicRouter
7601
7809
  .get('/health')
@@ -7738,8 +7946,8 @@ export const authService = {
7738
7946
  path: "src/router.ts",
7739
7947
  content: `import { e } from '@geekmidas/constructs/endpoints';
7740
7948
  import { UnauthorizedError } from '@geekmidas/errors';
7741
- import { authService, type Session } from './services/auth.js';
7742
- import { logger } from './config/logger.js';
7949
+ import { authService, type Session } from './services/auth.ts';
7950
+ import { logger } from './config/logger.ts';
7743
7951
 
7744
7952
  // Public router - no auth required
7745
7953
  export const publicRouter = e.logger(logger);
@@ -7763,7 +7971,7 @@ export const sessionRouter = r.session<Session>(async ({ services, header }) =>
7763
7971
  files.push({
7764
7972
  path: getRoutePath("profile.ts"),
7765
7973
  content: `import { z } from 'zod';
7766
- import { sessionRouter } from '~/router';
7974
+ import { sessionRouter } from '~/router.ts';
7767
7975
 
7768
7976
  export const profileEndpoint = sessionRouter
7769
7977
  .get('/profile')
@@ -7832,8 +8040,8 @@ export const telescope = new Telescope({
7832
8040
  content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
7833
8041
  import { Kysely, PostgresDialect } from 'kysely';
7834
8042
  import pg from 'pg';
7835
- import type { Database } from '../services/database.js';
7836
- import { envParser } from './env.js';
8043
+ import type { Database } from '~/services/database.ts';
8044
+ import { envParser } from '~/config/env.ts';
7837
8045
 
7838
8046
  // Parse database config for Studio
7839
8047
  const studioConfig = envParser
@@ -8006,8 +8214,8 @@ export const telescope = new Telescope({
8006
8214
  content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
8007
8215
  import { Kysely, PostgresDialect } from 'kysely';
8008
8216
  import pg from 'pg';
8009
- import type { Database } from '../services/database.js';
8010
- import { envParser } from './env.js';
8217
+ import type { Database } from '~/services/database.ts';
8218
+ import { envParser } from '~/config/env.ts';
8011
8219
 
8012
8220
  // Parse database config for Studio
8013
8221
  const studioConfig = envParser
@@ -8263,7 +8471,7 @@ export type AppEvents =
8263
8471
  path: "src/events/publisher.ts",
8264
8472
  content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
8265
8473
  import { Publisher, type EventPublisher } from '@geekmidas/events';
8266
- import type { AppEvents } from './types.js';
8474
+ import type { AppEvents } from './types.ts';
8267
8475
 
8268
8476
  export const eventsPublisherService = {
8269
8477
  serviceName: 'events' as const,
@@ -8290,7 +8498,7 @@ export const eventsPublisherService = {
8290
8498
  {
8291
8499
  path: "src/subscribers/user-events.ts",
8292
8500
  content: `import { s } from '@geekmidas/constructs/subscribers';
8293
- import { eventsPublisherService } from '../events/publisher.js';
8501
+ import { eventsPublisherService } from '~/events/publisher.ts';
8294
8502
 
8295
8503
  export const userEventsSubscriber = s
8296
8504
  .publisher(eventsPublisherService)
@@ -8492,6 +8700,9 @@ function generatePackageJson(options, template) {
8492
8700
  dependencies$1.kysely = "~0.28.2";
8493
8701
  dependencies$1.pg = "~8.16.0";
8494
8702
  devDependencies$1["@types/pg"] = "~8.15.0";
8703
+ devDependencies$1["@geekmidas/testkit"] = GEEKMIDAS_VERSIONS["@geekmidas/testkit"];
8704
+ devDependencies$1["@faker-js/faker"] = "~9.8.0";
8705
+ devDependencies$1["vite-tsconfig-paths"] = "~5.1.0";
8495
8706
  }
8496
8707
  if (monorepo) {
8497
8708
  delete devDependencies$1["@biomejs/biome"];
@@ -8538,6 +8749,118 @@ function generateSourceFiles(options, template) {
8538
8749
  return template.files(options);
8539
8750
  }
8540
8751
 
8752
+ //#endregion
8753
+ //#region src/init/generators/test.ts
8754
+ /**
8755
+ * Generate test infrastructure files when database is enabled.
8756
+ * Includes transaction-isolated test config, global setup with migrations,
8757
+ * factory system with builders/seeds, and an example spec.
8758
+ */
8759
+ function generateTestFiles(options, _template) {
8760
+ if (!options.database) return [];
8761
+ return [
8762
+ {
8763
+ path: "test/config.ts",
8764
+ content: `import { it as itVitest } from 'vitest';
8765
+ import { Kysely, PostgresDialect } from 'kysely';
8766
+ import pg from 'pg';
8767
+ import { wrapVitestKyselyTransaction } from '@geekmidas/testkit/kysely';
8768
+ import type { Database } from '~/services/database.ts';
8769
+
8770
+ const connection = new Kysely<Database>({
8771
+ dialect: new PostgresDialect({
8772
+ pool: new pg.Pool({ connectionString: process.env.DATABASE_URL }),
8773
+ }),
8774
+ });
8775
+
8776
+ export const it = wrapVitestKyselyTransaction<Database>(itVitest, {
8777
+ connection,
8778
+ });
8779
+ `
8780
+ },
8781
+ {
8782
+ path: "test/globalSetup.ts",
8783
+ content: `import { Kysely, PostgresDialect } from 'kysely';
8784
+ import pg from 'pg';
8785
+ import { PostgresKyselyMigrator } from '@geekmidas/testkit/kysely';
8786
+ import type { Database } from '~/services/database.ts';
8787
+
8788
+ export async function setup() {
8789
+ const testUrl = process.env.DATABASE_URL;
8790
+ if (!testUrl) throw new Error('DATABASE_URL is required for tests');
8791
+
8792
+ // Run migrations on the test database
8793
+ // (gkm test already rewrites DATABASE_URL to point to the _test database)
8794
+ const db = new Kysely<Database>({
8795
+ dialect: new PostgresDialect({
8796
+ pool: new pg.Pool({ connectionString: testUrl }),
8797
+ }),
8798
+ });
8799
+
8800
+ const migrator = new PostgresKyselyMigrator({
8801
+ db,
8802
+ migrationsPath: './src/migrations',
8803
+ });
8804
+
8805
+ await migrator.migrateToLatest();
8806
+ await db.destroy();
8807
+ }
8808
+ `
8809
+ },
8810
+ {
8811
+ path: "test/factory/index.ts",
8812
+ content: `import type { Kysely } from 'kysely';
8813
+ import { KyselyFactory } from '@geekmidas/testkit/kysely';
8814
+ import type { Database } from '~/services/database.ts';
8815
+ import { usersBuilder } from './users.ts';
8816
+
8817
+ const builders = { users: usersBuilder };
8818
+ const seeds = {};
8819
+
8820
+ export function createFactory(db: Kysely<Database>) {
8821
+ return new KyselyFactory<Database, typeof builders, typeof seeds>(
8822
+ builders,
8823
+ seeds,
8824
+ db,
8825
+ );
8826
+ }
8827
+
8828
+ export type Factory = ReturnType<typeof createFactory>;
8829
+ `
8830
+ },
8831
+ {
8832
+ path: "test/factory/users.ts",
8833
+ content: `import { KyselyFactory } from '@geekmidas/testkit/kysely';
8834
+ import type { Database } from '~/services/database.ts';
8835
+
8836
+ export const usersBuilder = KyselyFactory.createBuilder<Database, 'users'>(
8837
+ 'users',
8838
+ ({ faker }) => ({
8839
+ id: faker.string.uuid(),
8840
+ name: faker.person.fullName(),
8841
+ email: faker.internet.email(),
8842
+ created_at: new Date(),
8843
+ }),
8844
+ );
8845
+ `
8846
+ },
8847
+ {
8848
+ path: "test/example.spec.ts",
8849
+ content: `import { describe, expect } from 'vitest';
8850
+ import { it } from './config.ts';
8851
+
8852
+ describe('example', () => {
8853
+ it('should have a working test setup', async ({ db }) => {
8854
+ // db is a transaction-wrapped Kysely instance
8855
+ // All changes are automatically rolled back after the test
8856
+ expect(db).toBeDefined();
8857
+ });
8858
+ });
8859
+ `
8860
+ }
8861
+ ];
8862
+ }
8863
+
8541
8864
  //#endregion
8542
8865
  //#region src/init/generators/ui.ts
8543
8866
  /**
@@ -8607,6 +8930,7 @@ function generateUiPackageFiles(options) {
8607
8930
  "DOM.Iterable"
8608
8931
  ],
8609
8932
  noEmit: true,
8933
+ allowImportingTsExtensions: true,
8610
8934
  baseUrl: ".",
8611
8935
  paths: { "~/*": ["./src/*"] }
8612
8936
  },
@@ -9780,8 +10104,8 @@ export const Alert: Story = {
9780
10104
  ),
9781
10105
  };
9782
10106
  `;
9783
- const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button';
9784
- export { Input } from './input';
10107
+ const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button.tsx';
10108
+ export { Input } from './input.tsx';
9785
10109
  export {
9786
10110
  Card,
9787
10111
  CardHeader,
@@ -9789,17 +10113,17 @@ export {
9789
10113
  CardTitle,
9790
10114
  CardDescription,
9791
10115
  CardContent,
9792
- } from './card';
9793
- export { Label } from './label';
9794
- export { Badge, type BadgeProps, badgeVariants } from './badge';
9795
- export { Separator } from './separator';
9796
- export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs';
10116
+ } from './card.tsx';
10117
+ export { Label } from './label.tsx';
10118
+ export { Badge, type BadgeProps, badgeVariants } from './badge.tsx';
10119
+ export { Separator } from './separator.tsx';
10120
+ export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs.tsx';
9797
10121
  export {
9798
10122
  Tooltip,
9799
10123
  TooltipTrigger,
9800
10124
  TooltipContent,
9801
10125
  TooltipProvider,
9802
- } from './tooltip';
10126
+ } from './tooltip.tsx';
9803
10127
  export {
9804
10128
  Dialog,
9805
10129
  DialogPortal,
@@ -9811,20 +10135,20 @@ export {
9811
10135
  DialogFooter,
9812
10136
  DialogTitle,
9813
10137
  DialogDescription,
9814
- } from './dialog';
10138
+ } from './dialog.tsx';
9815
10139
  `;
9816
10140
  const buttonIndexTsx = buttonTsx;
9817
10141
  const inputIndexTsx = inputTsx;
9818
10142
  const cardIndexTsx = cardTsx;
9819
- const componentsIndex = `export * from './ui';
10143
+ const componentsIndex = `export * from './ui/index.ts';
9820
10144
  `;
9821
10145
  const indexTs = `// @${options.name}/ui - Shared UI component library
9822
10146
 
9823
10147
  // shadcn/ui components
9824
- export * from './components';
10148
+ export * from './components/index.ts';
9825
10149
 
9826
10150
  // Utilities
9827
- export { cn } from './lib/utils';
10151
+ export { cn } from './lib/utils.ts';
9828
10152
  `;
9829
10153
  const gitignore = `node_modules/
9830
10154
  dist/
@@ -9967,7 +10291,7 @@ function generateWebAppFiles(options) {
9967
10291
  private: true,
9968
10292
  type: "module",
9969
10293
  scripts: {
9970
- dev: "gkm exec -- next dev --turbopack",
10294
+ dev: "gkm exec -- next dev --turbopack -p $PORT",
9971
10295
  build: "gkm exec -- next build",
9972
10296
  start: "next start",
9973
10297
  typecheck: "tsc --noEmit"
@@ -10022,6 +10346,7 @@ export default nextConfig;
10022
10346
  skipLibCheck: true,
10023
10347
  strict: true,
10024
10348
  noEmit: true,
10349
+ allowImportingTsExtensions: true,
10025
10350
  esModuleInterop: true,
10026
10351
  module: "ESNext",
10027
10352
  moduleResolution: "bundler",
@@ -10103,7 +10428,7 @@ export const serverConfig = envParser
10103
10428
  `;
10104
10429
  const authClientTs = `import { createAuthClient } from 'better-auth/react';
10105
10430
  import { magicLinkClient } from 'better-auth/client/plugins';
10106
- import { clientConfig } from '~/config/client';
10431
+ import { clientConfig } from '~/config/client.ts';
10107
10432
 
10108
10433
  export const authClient = createAuthClient({
10109
10434
  baseURL: clientConfig.authUrl,
@@ -10115,7 +10440,7 @@ export const { signIn, signUp, signOut, useSession, magicLink } = authClient;
10115
10440
  const providersTsx = `'use client';
10116
10441
 
10117
10442
  import { QueryClientProvider } from '@tanstack/react-query';
10118
- import { getQueryClient } from '~/lib/query-client';
10443
+ import { getQueryClient } from '~/lib/query-client.ts';
10119
10444
 
10120
10445
  export function Providers({ children }: { children: React.ReactNode }) {
10121
10446
  const queryClient = getQueryClient();
@@ -10125,9 +10450,9 @@ export function Providers({ children }: { children: React.ReactNode }) {
10125
10450
  );
10126
10451
  }
10127
10452
  `;
10128
- const apiIndexTs = `import { createApi } from './openapi';
10129
- import { getQueryClient } from '~/lib/query-client';
10130
- import { clientConfig } from '~/config/client';
10453
+ const apiIndexTs = `import { createApi } from './api.ts';
10454
+ import { getQueryClient } from '~/lib/query-client.ts';
10455
+ import { clientConfig } from '~/config/client.ts';
10131
10456
 
10132
10457
  export const api = createApi({
10133
10458
  baseURL: clientConfig.apiUrl,
@@ -10137,7 +10462,7 @@ export const api = createApi({
10137
10462
  const globalsCss = `@import '${uiPackage}/styles';
10138
10463
  `;
10139
10464
  const layoutTsx = `import type { Metadata } from 'next';
10140
- import { Providers } from './providers';
10465
+ import { Providers } from './providers.tsx';
10141
10466
  import './globals.css';
10142
10467
 
10143
10468
  export const metadata: Metadata = {
@@ -10159,7 +10484,7 @@ export default function RootLayout({
10159
10484
  );
10160
10485
  }
10161
10486
  `;
10162
- const pageTsx = `import { api } from '~/api';
10487
+ const pageTsx = `import { api } from '~/api/index.ts';
10163
10488
  import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from '${uiPackage}/components';
10164
10489
 
10165
10490
  export default async function Home() {
@@ -10502,6 +10827,7 @@ async function initCommand(projectName, options = {}) {
10502
10827
  ...generateConfigFiles(templateOptions, baseTemplate),
10503
10828
  ...generateEnvFiles(templateOptions, baseTemplate),
10504
10829
  ...generateSourceFiles(templateOptions, baseTemplate),
10830
+ ...generateTestFiles(templateOptions, baseTemplate),
10505
10831
  ...isMonorepo$1 ? [] : generateDockerFiles(templateOptions, baseTemplate, dbApps)
10506
10832
  ] : [];
10507
10833
  const dockerFiles = isMonorepo$1 && baseTemplate ? generateDockerFiles(templateOptions, baseTemplate, dbApps) : [];
@@ -10845,23 +11171,61 @@ function maskUrl(url) {
10845
11171
  //#endregion
10846
11172
  //#region src/test/index.ts
10847
11173
  /**
10848
- * Run tests with secrets loaded from the specified stage.
10849
- * Secrets are decrypted and injected into the environment.
11174
+ * Run tests with secrets, dependency URLs, and .env files loaded.
11175
+ * Environment variables are sniffed to inject only what the app needs.
10850
11176
  */
10851
11177
  async function testCommand(options = {}) {
10852
11178
  const stage = options.stage ?? "development";
10853
- console.log(`\n🧪 Running tests with ${stage} secrets...\n`);
10854
- let envVars = {};
11179
+ const cwd = process.cwd();
11180
+ console.log(`\n🧪 Running tests with ${stage} environment...\n`);
11181
+ const defaultEnv = loadEnvFiles(".env");
11182
+ if (defaultEnv.loaded.length > 0) console.log(` 📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
11183
+ let secretsEnv = {};
10855
11184
  try {
10856
11185
  const secrets = await require_storage.readStageSecrets(stage);
10857
11186
  if (secrets) {
10858
- envVars = require_storage.toEmbeddableSecrets(secrets);
10859
- console.log(` Loaded ${Object.keys(envVars).length} secrets from ${stage}\n`);
10860
- } else console.log(` No secrets found for ${stage}, running without secrets\n`);
11187
+ secretsEnv = require_storage.toEmbeddableSecrets(secrets);
11188
+ console.log(` 🔐 Loaded ${Object.keys(secretsEnv).length} secrets from ${stage}`);
11189
+ } else console.log(` No secrets found for ${stage}`);
10861
11190
  } catch (error) {
10862
- if (error instanceof Error && error.message.includes("key not found")) console.log(` Decryption key not found for ${stage}, running without secrets\n`);
11191
+ if (error instanceof Error && error.message.includes("key not found")) console.log(` Decryption key not found for ${stage}`);
10863
11192
  else throw error;
10864
11193
  }
11194
+ const composePath = (0, node_path.join)(cwd, "docker-compose.yml");
11195
+ const mappings = parseComposePortMappings(composePath);
11196
+ if (mappings.length > 0) {
11197
+ const ports = await loadPortState(cwd);
11198
+ if (Object.keys(ports).length > 0) {
11199
+ secretsEnv = rewriteUrlsWithPorts(secretsEnv, {
11200
+ dockerEnv: {},
11201
+ ports,
11202
+ mappings
11203
+ });
11204
+ console.log(` 🔌 Applied ${Object.keys(ports).length} port mapping(s)`);
11205
+ }
11206
+ }
11207
+ secretsEnv = rewriteDatabaseUrlForTests(secretsEnv);
11208
+ await ensureTestDatabase(secretsEnv);
11209
+ let dependencyEnv = {};
11210
+ try {
11211
+ const appInfo = await require_config.loadWorkspaceAppInfo(cwd);
11212
+ dependencyEnv = require_workspace.getDependencyEnvVars(appInfo.workspace, appInfo.appName);
11213
+ if (Object.keys(dependencyEnv).length > 0) console.log(` 🔗 Loaded ${Object.keys(dependencyEnv).length} dependency URL(s)`);
11214
+ const sniffed = await sniffAppEnvironment(appInfo.app, appInfo.appName, appInfo.workspaceRoot, { logWarnings: false });
11215
+ if (sniffed.requiredEnvVars.length > 0) {
11216
+ const needed = new Set(sniffed.requiredEnvVars);
11217
+ const allEnv = {
11218
+ ...secretsEnv,
11219
+ ...dependencyEnv
11220
+ };
11221
+ const filteredEnv = {};
11222
+ for (const [key, value] of Object.entries(allEnv)) if (needed.has(key)) filteredEnv[key] = value;
11223
+ secretsEnv = {};
11224
+ dependencyEnv = filteredEnv;
11225
+ console.log(` 🔍 Sniffed ${sniffed.requiredEnvVars.length} required env var(s)`);
11226
+ }
11227
+ } catch {}
11228
+ console.log("");
10865
11229
  const args = [];
10866
11230
  if (options.run) args.push("run");
10867
11231
  else if (options.watch) args.push("--watch");
@@ -10869,11 +11233,12 @@ async function testCommand(options = {}) {
10869
11233
  if (options.ui) args.push("--ui");
10870
11234
  if (options.pattern) args.push(options.pattern);
10871
11235
  const vitestProcess = (0, node_child_process.spawn)("npx", ["vitest", ...args], {
10872
- cwd: process.cwd(),
11236
+ cwd,
10873
11237
  stdio: "inherit",
10874
11238
  env: {
10875
11239
  ...process.env,
10876
- ...envVars,
11240
+ ...secretsEnv,
11241
+ ...dependencyEnv,
10877
11242
  NODE_ENV: "test"
10878
11243
  }
10879
11244
  });
@@ -10887,6 +11252,57 @@ async function testCommand(options = {}) {
10887
11252
  });
10888
11253
  });
10889
11254
  }
11255
+ const TEST_DB_SUFFIX = "_test";
11256
+ /**
11257
+ * Rewrite DATABASE_URL to point to a separate test database.
11258
+ * Appends `_test` to the database name (e.g., `app` -> `app_test`).
11259
+ * @internal Exported for testing
11260
+ */
11261
+ function rewriteDatabaseUrlForTests(env) {
11262
+ const result = { ...env };
11263
+ for (const key of Object.keys(result)) {
11264
+ if (!key.includes("DATABASE_URL")) continue;
11265
+ const value = result[key];
11266
+ try {
11267
+ const url = new URL(value);
11268
+ const dbName = url.pathname.slice(1);
11269
+ if (dbName && !dbName.endsWith(TEST_DB_SUFFIX)) {
11270
+ url.pathname = `/${dbName}${TEST_DB_SUFFIX}`;
11271
+ result[key] = url.toString();
11272
+ console.log(` 🧪 ${key}: using test database "${dbName}${TEST_DB_SUFFIX}"`);
11273
+ }
11274
+ } catch {}
11275
+ }
11276
+ return result;
11277
+ }
11278
+ /**
11279
+ * Ensure the test database exists by connecting to the default database
11280
+ * and running CREATE DATABASE IF NOT EXISTS.
11281
+ * @internal Exported for testing
11282
+ */
11283
+ async function ensureTestDatabase(env) {
11284
+ const databaseUrl = env.DATABASE_URL;
11285
+ if (!databaseUrl) return;
11286
+ try {
11287
+ const url = new URL(databaseUrl);
11288
+ const testDbName = url.pathname.slice(1);
11289
+ if (!testDbName) return;
11290
+ url.pathname = "/postgres";
11291
+ const { default: pg$1 } = await import("pg");
11292
+ const client = new pg$1.Client({ connectionString: url.toString() });
11293
+ await client.connect();
11294
+ try {
11295
+ await client.query(`CREATE DATABASE "${testDbName}"`);
11296
+ console.log(` 📦 Created test database "${testDbName}"`);
11297
+ } catch (err) {
11298
+ if (err.code !== "42P04") throw err;
11299
+ } finally {
11300
+ await client.end();
11301
+ }
11302
+ } catch (err) {
11303
+ console.log(` ⚠️ Could not ensure test database: ${err.message}`);
11304
+ }
11305
+ }
10890
11306
 
10891
11307
  //#endregion
10892
11308
  //#region src/index.ts