@geekmidas/cli 1.10.7 → 1.10.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +44 -1
  3. package/dist/{bundler-NpfYPBUo.cjs → bundler-Bm3Az_sv.cjs} +2 -2
  4. package/dist/{bundler-NpfYPBUo.cjs.map → bundler-Bm3Az_sv.cjs.map} +1 -1
  5. package/dist/{bundler-DQYjKFPm.mjs → bundler-kk_XJTRp.mjs} +2 -2
  6. package/dist/{bundler-DQYjKFPm.mjs.map → bundler-kk_XJTRp.mjs.map} +1 -1
  7. package/dist/config.d.cts +2 -2
  8. package/dist/config.d.mts +2 -2
  9. package/dist/{fullstack-secrets-ca0Kyrvt.mjs → fullstack-secrets-C2lbdbLZ.mjs} +15 -1
  10. package/dist/fullstack-secrets-C2lbdbLZ.mjs.map +1 -0
  11. package/dist/{fullstack-secrets-BctGaE4E.cjs → fullstack-secrets-CtWIYuI0.cjs} +15 -1
  12. package/dist/fullstack-secrets-CtWIYuI0.cjs.map +1 -0
  13. package/dist/{index-9tjTQjFt.d.mts → index-BdJZKXCJ.d.cts} +4 -2
  14. package/dist/index-BdJZKXCJ.d.cts.map +1 -0
  15. package/dist/{index-VOKKO-lm.d.cts → index-DB9VbcCD.d.mts} +4 -2
  16. package/dist/index-DB9VbcCD.d.mts.map +1 -0
  17. package/dist/index.cjs +177 -61
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.mjs +177 -61
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/openapi-BYxAWwok.cjs.map +1 -1
  22. package/dist/openapi-DenF-okj.mjs.map +1 -1
  23. package/dist/openapi.d.cts +1 -1
  24. package/dist/openapi.d.mts +1 -1
  25. package/dist/{reconcile-C5OyCA7V.mjs → reconcile-BnM6FA6g.mjs} +2 -2
  26. package/dist/{reconcile-C5OyCA7V.mjs.map → reconcile-BnM6FA6g.mjs.map} +1 -1
  27. package/dist/{reconcile-TEBsryVn.cjs → reconcile-D6u4HSg8.cjs} +2 -2
  28. package/dist/{reconcile-TEBsryVn.cjs.map → reconcile-D6u4HSg8.cjs.map} +1 -1
  29. package/dist/{storage-DmCbr6DI.mjs → storage-B7H2PPCS.mjs} +8 -1
  30. package/dist/{storage-DmCbr6DI.mjs.map → storage-B7H2PPCS.mjs.map} +1 -1
  31. package/dist/{storage-Dx_jZbq6.mjs → storage-C1FNm2EP.mjs} +1 -1
  32. package/dist/{storage-CoCNe0Pt.cjs → storage-Cs13jkJ9.cjs} +8 -1
  33. package/dist/{storage-CoCNe0Pt.cjs.map → storage-Cs13jkJ9.cjs.map} +1 -1
  34. package/dist/{storage-C7pmBq1u.cjs → storage-D6BGLgWf.cjs} +1 -1
  35. package/dist/{sync-6FoT41G3.mjs → sync-CyGe5f1I.mjs} +1 -1
  36. package/dist/{sync-CbeKrnQV.mjs → sync-CzXruMzP.mjs} +2 -2
  37. package/dist/{sync-CbeKrnQV.mjs.map → sync-CzXruMzP.mjs.map} +1 -1
  38. package/dist/sync-DLlwsrBs.cjs +4 -0
  39. package/dist/{sync-DdkKaHqP.cjs → sync-oCqELfeA.cjs} +2 -2
  40. package/dist/{sync-DdkKaHqP.cjs.map → sync-oCqELfeA.cjs.map} +1 -1
  41. package/dist/{types-C7QJJl9f.d.cts → types-D4MLWXSL.d.cts} +2 -2
  42. package/dist/{types-C7QJJl9f.d.cts.map → types-D4MLWXSL.d.cts.map} +1 -1
  43. package/dist/{types-Iqsq_FIG.d.mts → types-DwpLq_fp.d.mts} +2 -2
  44. package/dist/{types-Iqsq_FIG.d.mts.map → types-DwpLq_fp.d.mts.map} +1 -1
  45. package/dist/workspace/index.d.cts +2 -2
  46. package/dist/workspace/index.d.mts +2 -2
  47. package/dist/workspace-4SP3Gx4Y.cjs.map +1 -1
  48. package/dist/workspace-D4z4A4cq.mjs.map +1 -1
  49. package/package.json +5 -5
  50. package/src/dev/__tests__/index.spec.ts +142 -0
  51. package/src/dev/index.ts +67 -33
  52. package/src/docker/__tests__/compose.spec.ts +151 -2
  53. package/src/docker/compose.ts +105 -8
  54. package/src/init/generators/docker.ts +3 -1
  55. package/src/init/index.ts +1 -0
  56. package/src/init/versions.ts +1 -1
  57. package/src/secrets/__tests__/generator.spec.ts +68 -0
  58. package/src/secrets/__tests__/storage.spec.ts +30 -0
  59. package/src/secrets/generator.ts +18 -0
  60. package/src/secrets/index.ts +9 -0
  61. package/src/secrets/storage.ts +7 -0
  62. package/src/secrets/types.ts +4 -0
  63. package/src/setup/index.ts +1 -0
  64. package/src/test/__tests__/index.spec.ts +115 -0
  65. package/src/test/index.ts +41 -21
  66. package/src/types.ts +1 -1
  67. package/src/workspace/types.ts +2 -0
  68. package/dist/fullstack-secrets-BctGaE4E.cjs.map +0 -1
  69. package/dist/fullstack-secrets-ca0Kyrvt.mjs.map +0 -1
  70. package/dist/index-9tjTQjFt.d.mts.map +0 -1
  71. package/dist/index-VOKKO-lm.d.cts.map +0 -1
  72. package/dist/sync-RsnjXYwG.cjs +0 -4
package/dist/index.cjs CHANGED
@@ -4,13 +4,13 @@ const require_workspace = require('./workspace-4SP3Gx4Y.cjs');
4
4
  const require_config = require('./config-D3ORuiUs.cjs');
5
5
  const require_credentials = require('./credentials-C8DWtnMY.cjs');
6
6
  const require_openapi = require('./openapi-BYxAWwok.cjs');
7
- const require_storage = require('./storage-CoCNe0Pt.cjs');
7
+ const require_storage = require('./storage-Cs13jkJ9.cjs');
8
8
  const require_dokploy_api = require('./dokploy-api-DLgvEQlr.cjs');
9
9
  const require_encryption = require('./encryption-BE0UOb8j.cjs');
10
10
  const require_CachedStateProvider = require('./CachedStateProvider-D73dCqfH.cjs');
11
- const require_fullstack_secrets = require('./fullstack-secrets-BctGaE4E.cjs');
11
+ const require_fullstack_secrets = require('./fullstack-secrets-CtWIYuI0.cjs');
12
12
  const require_openapi_react_query = require('./openapi-react-query-DYbBq-WJ.cjs');
13
- const require_sync = require('./sync-DdkKaHqP.cjs');
13
+ const require_sync = require('./sync-oCqELfeA.cjs');
14
14
  const node_fs = require_chunk.__toESM(require("node:fs"));
15
15
  const node_path = require_chunk.__toESM(require("node:path"));
16
16
  const commander = require_chunk.__toESM(require("commander"));
@@ -35,7 +35,7 @@ const prompts = require_chunk.__toESM(require("prompts"));
35
35
 
36
36
  //#region package.json
37
37
  var name = "@geekmidas/cli";
38
- var version = "1.10.6";
38
+ var version = "1.10.8";
39
39
  var description = "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs";
40
40
  var private$1 = false;
41
41
  var type = "module";
@@ -767,25 +767,30 @@ async function resolveServicePorts(workspaceRoot) {
767
767
  const savedState = await loadPortState(workspaceRoot);
768
768
  const dockerEnv = {};
769
769
  const ports = {};
770
+ const assignedPorts = /* @__PURE__ */ new Set();
770
771
  logger$11.log("\n🔌 Resolving service ports...");
771
772
  for (const mapping of mappings) {
772
773
  const containerPort = getContainerHostPort(workspaceRoot, mapping.service, mapping.containerPort);
773
774
  if (containerPort !== null) {
774
775
  ports[mapping.envVar] = containerPort;
775
776
  dockerEnv[mapping.envVar] = String(containerPort);
777
+ assignedPorts.add(containerPort);
776
778
  logger$11.log(` 🔄 ${mapping.service}:${mapping.containerPort}: reusing existing container on port ${containerPort}`);
777
779
  continue;
778
780
  }
779
781
  const savedPort = savedState[mapping.envVar];
780
- if (savedPort && await isPortAvailable(savedPort)) {
782
+ if (savedPort && !assignedPorts.has(savedPort) && await isPortAvailable(savedPort)) {
781
783
  ports[mapping.envVar] = savedPort;
782
784
  dockerEnv[mapping.envVar] = String(savedPort);
785
+ assignedPorts.add(savedPort);
783
786
  logger$11.log(` 💾 ${mapping.service}:${mapping.containerPort}: using saved port ${savedPort}`);
784
787
  continue;
785
788
  }
786
- const resolvedPort = await findAvailablePort(mapping.defaultPort);
789
+ let resolvedPort = await findAvailablePort(mapping.defaultPort);
790
+ while (assignedPorts.has(resolvedPort)) resolvedPort = await findAvailablePort(resolvedPort + 1);
787
791
  ports[mapping.envVar] = resolvedPort;
788
792
  dockerEnv[mapping.envVar] = String(resolvedPort);
793
+ assignedPorts.add(resolvedPort);
789
794
  if (resolvedPort !== mapping.defaultPort) logger$11.log(` ⚡ ${mapping.service}:${mapping.containerPort}: port ${mapping.defaultPort} occupied, using port ${resolvedPort}`);
790
795
  else logger$11.log(` ✅ ${mapping.service}:${mapping.containerPort}: using default port ${resolvedPort}`);
791
796
  }
@@ -1222,31 +1227,48 @@ async function loadSecretsForApp(secretsRoot, appName) {
1222
1227
  return mapped;
1223
1228
  }
1224
1229
  /**
1230
+ * Build the environment variables to pass to `docker compose up`.
1231
+ * Merges process.env, secrets, and port mappings so that Docker Compose
1232
+ * can interpolate variables like ${POSTGRES_USER} correctly.
1233
+ * @internal Exported for testing
1234
+ */
1235
+ function buildDockerComposeEnv(secretsEnv, portEnv) {
1236
+ return {
1237
+ ...process.env,
1238
+ ...secretsEnv,
1239
+ ...portEnv
1240
+ };
1241
+ }
1242
+ /**
1243
+ * Parse all service names from a docker-compose.yml file.
1244
+ * @internal Exported for testing
1245
+ */
1246
+ function parseComposeServiceNames(composePath) {
1247
+ if (!(0, node_fs.existsSync)(composePath)) return [];
1248
+ const content = (0, node_fs.readFileSync)(composePath, "utf-8");
1249
+ const compose = (0, yaml.parse)(content);
1250
+ return Object.keys(compose?.services ?? {});
1251
+ }
1252
+ /**
1225
1253
  * Start docker-compose services for the workspace.
1254
+ * Parses the docker-compose.yml to discover all services and starts
1255
+ * everything except app services (which are managed by turbo).
1256
+ * This ensures manually added services are always started.
1226
1257
  * @internal Exported for testing
1227
1258
  */
1228
- async function startWorkspaceServices(workspace, portEnv) {
1229
- const services = workspace.services;
1230
- if (!services.db && !services.cache && !services.mail) return;
1231
- const servicesToStart = [];
1232
- if (services.db) servicesToStart.push("postgres");
1233
- if (services.cache) servicesToStart.push("redis");
1234
- if (services.mail) servicesToStart.push("mailpit");
1259
+ async function startWorkspaceServices(workspace, portEnv, secretsEnv) {
1260
+ const composeFile = (0, node_path.join)(workspace.root, "docker-compose.yml");
1261
+ if (!(0, node_fs.existsSync)(composeFile)) return;
1262
+ const allServices = parseComposeServiceNames(composeFile);
1263
+ const appNames = new Set(Object.keys(workspace.apps));
1264
+ const servicesToStart = allServices.filter((name$1) => !appNames.has(name$1));
1235
1265
  if (servicesToStart.length === 0) return;
1236
1266
  logger$11.log(`🐳 Starting services: ${servicesToStart.join(", ")}`);
1237
1267
  try {
1238
- const composeFile = (0, node_path.join)(workspace.root, "docker-compose.yml");
1239
- if (!(0, node_fs.existsSync)(composeFile)) {
1240
- logger$11.warn("⚠️ No docker-compose.yml found. Services will not be started.");
1241
- return;
1242
- }
1243
1268
  (0, node_child_process.execSync)(`docker compose up -d ${servicesToStart.join(" ")}`, {
1244
1269
  cwd: workspace.root,
1245
1270
  stdio: "inherit",
1246
- env: {
1247
- ...process.env,
1248
- ...portEnv
1249
- }
1271
+ env: buildDockerComposeEnv(secretsEnv, portEnv)
1250
1272
  });
1251
1273
  logger$11.log("✅ Services started");
1252
1274
  } catch (error) {
@@ -1295,8 +1317,9 @@ async function workspaceDevCommand(workspace, options) {
1295
1317
  if (copiedCount > 0) logger$11.log(`\n📦 Copied ${copiedCount} API client(s)`);
1296
1318
  }
1297
1319
  const resolvedPorts = await resolveServicePorts(workspace.root);
1298
- await startWorkspaceServices(workspace, resolvedPorts.dockerEnv);
1299
- const secretsEnv = rewriteUrlsWithPorts(await loadDevSecrets(workspace), resolvedPorts);
1320
+ const rawSecrets = await loadDevSecrets(workspace);
1321
+ await startWorkspaceServices(workspace, resolvedPorts.dockerEnv, rawSecrets);
1322
+ const secretsEnv = rewriteUrlsWithPorts(rawSecrets, resolvedPorts);
1300
1323
  if (Object.keys(secretsEnv).length > 0) logger$11.log(` Loaded ${Object.keys(secretsEnv).length} secret(s)`);
1301
1324
  const dependencyEnv = generateAllDependencyEnvVars(workspace);
1302
1325
  if (Object.keys(dependencyEnv).length > 0) {
@@ -2175,7 +2198,7 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
2175
2198
  let masterKey;
2176
2199
  if (context.production?.bundle && !skipBundle) {
2177
2200
  logger$9.log(`\n📦 Bundling production server...`);
2178
- const { bundleServer } = await Promise.resolve().then(() => require("./bundler-NpfYPBUo.cjs"));
2201
+ const { bundleServer } = await Promise.resolve().then(() => require("./bundler-Bm3Az_sv.cjs"));
2179
2202
  const allConstructs = [
2180
2203
  ...endpoints.map((e) => e.construct),
2181
2204
  ...functions.map((f) => f.construct),
@@ -2837,13 +2860,15 @@ async function verifyDnsRecords(appHostnames, serverIp, state) {
2837
2860
  const DEFAULT_SERVICE_IMAGES = {
2838
2861
  postgres: "postgres",
2839
2862
  redis: "redis",
2840
- rabbitmq: "rabbitmq"
2863
+ rabbitmq: "rabbitmq",
2864
+ minio: "minio/minio"
2841
2865
  };
2842
2866
  /** Default Docker image versions for services */
2843
2867
  const DEFAULT_SERVICE_VERSIONS = {
2844
2868
  postgres: "18-alpine",
2845
2869
  redis: "7-alpine",
2846
- rabbitmq: "3-management-alpine"
2870
+ rabbitmq: "3-management-alpine",
2871
+ minio: "latest"
2847
2872
  };
2848
2873
  /** Get the default full image reference for a service */
2849
2874
  function getDefaultImage(serviceName) {
@@ -2874,7 +2899,9 @@ function generateDockerCompose(options) {
2874
2899
  const { imageName, registry, port, healthCheckPath, services } = options;
2875
2900
  const serviceMap = normalizeServices(services);
2876
2901
  const imageRef = registry ? `\${REGISTRY:-${registry}}/` : "";
2877
- let yaml$1 = `version: '3.8'
2902
+ let yaml$1 = `# Use "gkm dev" or "gkm test" to start services.
2903
+ # Running "docker compose up" directly will not inject secrets or resolve ports.
2904
+ version: '3.8'
2878
2905
 
2879
2906
  services:
2880
2907
  api:
@@ -2894,6 +2921,13 @@ services:
2894
2921
  if (serviceMap.has("redis")) yaml$1 += ` - REDIS_URL=\${REDIS_URL:-redis://redis:6379}
2895
2922
  `;
2896
2923
  if (serviceMap.has("rabbitmq")) yaml$1 += ` - RABBITMQ_URL=\${RABBITMQ_URL:-amqp://rabbitmq:5672}
2924
+ `;
2925
+ if (serviceMap.has("minio")) yaml$1 += ` - S3_ENDPOINT=\${S3_ENDPOINT:-http://minio:9000}
2926
+ - S3_ACCESS_KEY_ID=\${MINIO_ACCESS_KEY:-app}
2927
+ - S3_SECRET_ACCESS_KEY=\${MINIO_SECRET_KEY:-app}
2928
+ - S3_BUCKET=\${MINIO_BUCKET:-app}
2929
+ - S3_REGION=\${S3_REGION:-eu-west-1}
2930
+ - S3_FORCE_PATH_STYLE=true
2897
2931
  `;
2898
2932
  yaml$1 += ` healthcheck:
2899
2933
  test: ["CMD", "wget", "-q", "--spider", "http://localhost:${port}${healthCheckPath}"]
@@ -2967,6 +3001,29 @@ services:
2967
3001
  retries: 5
2968
3002
  networks:
2969
3003
  - app-network
3004
+ `;
3005
+ const minioImage = serviceMap.get("minio");
3006
+ if (minioImage) yaml$1 += `
3007
+ minio:
3008
+ image: ${minioImage}
3009
+ container_name: minio
3010
+ restart: unless-stopped
3011
+ entrypoint: sh
3012
+ command: -c 'mkdir -p /data/\${MINIO_BUCKET:-app} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /data'
3013
+ environment:
3014
+ MINIO_ROOT_USER: \${MINIO_ACCESS_KEY:-app}
3015
+ MINIO_ROOT_PASSWORD: \${MINIO_SECRET_KEY:-app}
3016
+ ports:
3017
+ - "9001:9001" # Console UI
3018
+ volumes:
3019
+ - minio_data:/data
3020
+ healthcheck:
3021
+ test: ["CMD", "mc", "ready", "local"]
3022
+ interval: 10s
3023
+ timeout: 5s
3024
+ retries: 5
3025
+ networks:
3026
+ - app-network
2970
3027
  `;
2971
3028
  yaml$1 += `
2972
3029
  volumes:
@@ -2976,6 +3033,8 @@ volumes:
2976
3033
  if (serviceMap.has("redis")) yaml$1 += ` redis_data:
2977
3034
  `;
2978
3035
  if (serviceMap.has("rabbitmq")) yaml$1 += ` rabbitmq_data:
3036
+ `;
3037
+ if (serviceMap.has("minio")) yaml$1 += ` minio_data:
2979
3038
  `;
2980
3039
  yaml$1 += `
2981
3040
  networks:
@@ -2990,7 +3049,9 @@ networks:
2990
3049
  function generateMinimalDockerCompose(options) {
2991
3050
  const { imageName, registry, port, healthCheckPath } = options;
2992
3051
  const imageRef = registry ? `\${REGISTRY:-${registry}}/` : "";
2993
- return `version: '3.8'
3052
+ return `# Use "gkm dev" or "gkm test" to start services.
3053
+ # Running "docker compose up" directly will not inject secrets or resolve ports.
3054
+ version: '3.8'
2994
3055
 
2995
3056
  services:
2996
3057
  api:
@@ -3029,17 +3090,21 @@ function generateWorkspaceCompose(workspace, options = {}) {
3029
3090
  const hasPostgres = services.db !== void 0 && services.db !== false;
3030
3091
  const hasRedis = services.cache !== void 0 && services.cache !== false;
3031
3092
  const hasMail = services.mail !== void 0 && services.mail !== false;
3093
+ const hasMinio = services.storage !== void 0 && services.storage !== false;
3032
3094
  const postgresImage = getInfraServiceImage("postgres", services.db);
3033
3095
  const redisImage = getInfraServiceImage("redis", services.cache);
3096
+ const minioImage = getInfraServiceImage("minio", services.storage);
3034
3097
  let yaml$1 = `# Docker Compose for ${workspace.name} workspace
3035
- # Generated by gkm - do not edit manually
3098
+ # Use "gkm dev" or "gkm test" to start services.
3099
+ # Running "docker compose up" directly will not inject secrets or resolve ports.
3036
3100
 
3037
3101
  services:
3038
3102
  `;
3039
3103
  for (const [appName, app] of apps) yaml$1 += generateAppService(appName, app, apps, {
3040
3104
  registry,
3041
3105
  hasPostgres,
3042
- hasRedis
3106
+ hasRedis,
3107
+ hasMinio
3043
3108
  });
3044
3109
  if (hasPostgres) yaml$1 += `
3045
3110
  postgres:
@@ -3085,6 +3150,28 @@ services:
3085
3150
  - "1025:1025" # SMTP
3086
3151
  networks:
3087
3152
  - workspace-network
3153
+ `;
3154
+ if (hasMinio) yaml$1 += `
3155
+ minio:
3156
+ image: ${minioImage}
3157
+ container_name: ${workspace.name}-minio
3158
+ restart: unless-stopped
3159
+ entrypoint: sh
3160
+ command: -c 'mkdir -p /data/\${MINIO_BUCKET:-app} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /data'
3161
+ environment:
3162
+ MINIO_ROOT_USER: \${MINIO_ACCESS_KEY:-app}
3163
+ MINIO_ROOT_PASSWORD: \${MINIO_SECRET_KEY:-app}
3164
+ ports:
3165
+ - "9001:9001" # Console UI
3166
+ volumes:
3167
+ - minio_data:/data
3168
+ healthcheck:
3169
+ test: ["CMD", "mc", "ready", "local"]
3170
+ interval: 10s
3171
+ timeout: 5s
3172
+ retries: 5
3173
+ networks:
3174
+ - workspace-network
3088
3175
  `;
3089
3176
  yaml$1 += `
3090
3177
  volumes:
@@ -3092,6 +3179,8 @@ volumes:
3092
3179
  if (hasPostgres) yaml$1 += ` dbdata:
3093
3180
  `;
3094
3181
  if (hasRedis) yaml$1 += ` redis_data:
3182
+ `;
3183
+ if (hasMinio) yaml$1 += ` minio_data:
3095
3184
  `;
3096
3185
  yaml$1 += `
3097
3186
  networks:
@@ -3106,14 +3195,19 @@ networks:
3106
3195
  function getInfraServiceImage(serviceName, config) {
3107
3196
  const defaults = {
3108
3197
  postgres: "postgres:18-alpine",
3109
- redis: "redis:7-alpine"
3198
+ redis: "redis:7-alpine",
3199
+ minio: "minio/minio:latest"
3110
3200
  };
3111
3201
  if (!config || config === true) return defaults[serviceName];
3112
3202
  if (typeof config === "object") {
3113
3203
  if (config.image) return config.image;
3114
3204
  if (config.version) {
3115
- const baseImage = serviceName === "postgres" ? "postgres" : "redis";
3116
- return `${baseImage}:${config.version}`;
3205
+ const baseImages = {
3206
+ postgres: "postgres",
3207
+ redis: "redis",
3208
+ minio: "minio/minio"
3209
+ };
3210
+ return `${baseImages[serviceName]}:${config.version}`;
3117
3211
  }
3118
3212
  }
3119
3213
  return defaults[serviceName];
@@ -3122,7 +3216,7 @@ function getInfraServiceImage(serviceName, config) {
3122
3216
  * Generate a service definition for an app.
3123
3217
  */
3124
3218
  function generateAppService(appName, app, allApps, options) {
3125
- const { registry, hasPostgres, hasRedis } = options;
3219
+ const { registry, hasPostgres, hasRedis, hasMinio } = options;
3126
3220
  const imageRef = registry ? `\${REGISTRY:-${registry}}/` : "";
3127
3221
  const healthCheckPath = app.type === "frontend" ? "/" : "/health";
3128
3222
  const healthCheckCmd = app.type === "frontend" ? `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}/"]` : `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}${healthCheckPath}"]`;
@@ -3149,6 +3243,13 @@ function generateAppService(appName, app, allApps, options) {
3149
3243
  if (hasPostgres) yaml$1 += ` - DATABASE_URL=\${DATABASE_URL:-postgresql://\${POSTGRES_USER:-postgres}:\${POSTGRES_PASSWORD:-postgres}@postgres:5432/\${POSTGRES_DB:-app}}
3150
3244
  `;
3151
3245
  if (hasRedis) yaml$1 += ` - REDIS_URL=\${REDIS_URL:-redis://redis:6379}
3246
+ `;
3247
+ if (hasMinio) yaml$1 += ` - S3_ENDPOINT=\${S3_ENDPOINT:-http://minio:9000}
3248
+ - S3_ACCESS_KEY_ID=\${MINIO_ACCESS_KEY:-app}
3249
+ - S3_SECRET_ACCESS_KEY=\${MINIO_SECRET_KEY:-app}
3250
+ - S3_BUCKET=\${MINIO_BUCKET:-app}
3251
+ - S3_REGION=\${S3_REGION:-eu-west-1}
3252
+ - S3_FORCE_PATH_STYLE=true
3152
3253
  `;
3153
3254
  }
3154
3255
  yaml$1 += ` healthcheck:
@@ -3161,6 +3262,7 @@ function generateAppService(appName, app, allApps, options) {
3161
3262
  if (app.type === "backend") {
3162
3263
  if (hasPostgres) dependencies$1.push("postgres");
3163
3264
  if (hasRedis) dependencies$1.push("redis");
3265
+ if (hasMinio) dependencies$1.push("minio");
3164
3266
  }
3165
3267
  if (dependencies$1.length > 0) {
3166
3268
  yaml$1 += ` depends_on:
@@ -6205,7 +6307,7 @@ async function deployCommand(options) {
6205
6307
  dokployConfig = setupResult.config;
6206
6308
  finalRegistry = dokployConfig.registry ?? dockerConfig.registry;
6207
6309
  if (setupResult.serviceUrls) {
6208
- const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await Promise.resolve().then(() => require("./storage-C7pmBq1u.cjs"));
6310
+ const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await Promise.resolve().then(() => require("./storage-D6BGLgWf.cjs"));
6209
6311
  let secrets = await readStageSecrets$1(stage);
6210
6312
  if (!secrets) {
6211
6313
  logger$3.log(` Creating secrets file for stage "${stage}"...`);
@@ -6502,7 +6604,7 @@ const GEEKMIDAS_VERSIONS = {
6502
6604
  "@geekmidas/storage": "~2.0.0",
6503
6605
  "@geekmidas/studio": "~1.0.0",
6504
6606
  "@geekmidas/telescope": "~1.0.0",
6505
- "@geekmidas/testkit": "~1.0.2",
6607
+ "@geekmidas/testkit": "~1.0.5",
6506
6608
  "@geekmidas/cli": CLI_VERSION
6507
6609
  };
6508
6610
 
@@ -7057,7 +7159,9 @@ function generateDockerFiles(options, template, dbApps) {
7057
7159
  environment:
7058
7160
  MP_SMTP_AUTH_ACCEPT_ANY: 1
7059
7161
  MP_SMTP_AUTH_ALLOW_INSECURE: 1`);
7060
- let dockerCompose = `services:
7162
+ let dockerCompose = `# Use "gkm dev" or "gkm test" to start services.
7163
+ # Running "docker compose up" directly will not inject secrets or resolve ports.
7164
+ services:
7061
7165
  ${services.join("\n\n")}
7062
7166
  `;
7063
7167
  if (volumes.length > 0) dockerCompose += `
@@ -10868,6 +10972,7 @@ async function initCommand(projectName, options = {}) {
10868
10972
  const secretServices = [];
10869
10973
  if (services.db) secretServices.push("postgres");
10870
10974
  if (services.cache) secretServices.push("redis");
10975
+ if (services.storage) secretServices.push("minio");
10871
10976
  const devSecrets = require_fullstack_secrets.createStageSecrets("development", secretServices, { projectName: name$1 });
10872
10977
  const customSecrets = {
10873
10978
  NODE_ENV: "development",
@@ -11026,6 +11131,7 @@ async function secretsInitCommand(options) {
11026
11131
  if (secrets.urls.DATABASE_URL) logger$2.log(`\n DATABASE_URL: ${maskUrl(secrets.urls.DATABASE_URL)}`);
11027
11132
  if (secrets.urls.REDIS_URL) logger$2.log(` REDIS_URL: ${maskUrl(secrets.urls.REDIS_URL)}`);
11028
11133
  if (secrets.urls.RABBITMQ_URL) logger$2.log(` RABBITMQ_URL: ${maskUrl(secrets.urls.RABBITMQ_URL)}`);
11134
+ if (secrets.urls.S3_ENDPOINT) logger$2.log(` S3_ENDPOINT: ${secrets.urls.S3_ENDPOINT}`);
11029
11135
  if (Object.keys(secrets.custom).length > 0) logger$2.log(`\n Custom secrets: ${Object.keys(secrets.custom).length}`);
11030
11136
  logger$2.log(`\n Use "gkm secrets:show --stage ${stage}" to view secrets`);
11031
11137
  logger$2.log(" Use \"gkm secrets:set <KEY> <VALUE> --stage " + stage + "\" to add custom secrets");
@@ -11087,11 +11193,13 @@ async function secretsShowCommand(options) {
11087
11193
  logger$2.log(` password: ${reveal ? creds.password : require_storage.maskPassword(creds.password)}`);
11088
11194
  if (creds.database) logger$2.log(` database: ${creds.database}`);
11089
11195
  if (creds.vhost) logger$2.log(` vhost: ${creds.vhost}`);
11196
+ if (creds.bucket) logger$2.log(` bucket: ${creds.bucket}`);
11090
11197
  }
11091
11198
  logger$2.log("\nConnection URLs:");
11092
11199
  if (secrets.urls.DATABASE_URL) logger$2.log(` DATABASE_URL: ${reveal ? secrets.urls.DATABASE_URL : maskUrl(secrets.urls.DATABASE_URL)}`);
11093
11200
  if (secrets.urls.REDIS_URL) logger$2.log(` REDIS_URL: ${reveal ? secrets.urls.REDIS_URL : maskUrl(secrets.urls.REDIS_URL)}`);
11094
11201
  if (secrets.urls.RABBITMQ_URL) logger$2.log(` RABBITMQ_URL: ${reveal ? secrets.urls.RABBITMQ_URL : maskUrl(secrets.urls.RABBITMQ_URL)}`);
11202
+ if (secrets.urls.S3_ENDPOINT) logger$2.log(` S3_ENDPOINT: ${secrets.urls.S3_ENDPOINT}`);
11095
11203
  const customKeys = Object.keys(secrets.custom);
11096
11204
  if (customKeys.length > 0) {
11097
11205
  logger$2.log("\nCustom Secrets:");
@@ -11295,6 +11403,7 @@ async function generateFreshSecrets(stage, workspace, options) {
11295
11403
  const serviceNames = [];
11296
11404
  if (workspace.services.db) serviceNames.push("postgres");
11297
11405
  if (workspace.services.cache) serviceNames.push("redis");
11406
+ if (workspace.services.storage) serviceNames.push("minio");
11298
11407
  const secrets = require_fullstack_secrets.createStageSecrets(stage, serviceNames, { projectName: workspace.name });
11299
11408
  const isMultiApp = Object.keys(workspace.apps).length > 1;
11300
11409
  if (isMultiApp) {
@@ -11364,23 +11473,15 @@ async function testCommand(options = {}) {
11364
11473
  if (error instanceof Error && error.message.includes("key not found")) console.log(` Decryption key not found for ${stage}`);
11365
11474
  else throw error;
11366
11475
  }
11367
- const composePath = (0, node_path.join)(cwd, "docker-compose.yml");
11368
- const mappings = parseComposePortMappings(composePath);
11369
- if (mappings.length > 0) {
11370
- const ports = await loadPortState(cwd);
11371
- if (Object.keys(ports).length > 0) {
11372
- secretsEnv = rewriteUrlsWithPorts(secretsEnv, {
11373
- dockerEnv: {},
11374
- ports,
11375
- mappings
11376
- });
11377
- console.log(` 🔌 Applied ${Object.keys(ports).length} port mapping(s)`);
11378
- }
11379
- }
11380
- secretsEnv = rewriteDatabaseUrlForTests(secretsEnv);
11381
11476
  let dependencyEnv = {};
11382
11477
  try {
11383
11478
  const appInfo = await require_config.loadWorkspaceAppInfo(cwd);
11479
+ const resolvedPorts = await resolveServicePorts(appInfo.workspaceRoot);
11480
+ await startWorkspaceServices(appInfo.workspace, resolvedPorts.dockerEnv, secretsEnv);
11481
+ if (resolvedPorts.mappings.length > 0) {
11482
+ secretsEnv = rewriteUrlsWithPorts(secretsEnv, resolvedPorts);
11483
+ console.log(` 🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`);
11484
+ }
11384
11485
  dependencyEnv = require_workspace.getDependencyEnvVars(appInfo.workspace, appInfo.appName);
11385
11486
  if (Object.keys(dependencyEnv).length > 0) console.log(` 🔗 Loaded ${Object.keys(dependencyEnv).length} dependency URL(s)`);
11386
11487
  const sniffed = await sniffAppEnvironment(appInfo.app, appInfo.appName, appInfo.workspaceRoot, { logWarnings: false });
@@ -11396,7 +11497,22 @@ async function testCommand(options = {}) {
11396
11497
  dependencyEnv = filteredEnv;
11397
11498
  console.log(` 🔍 Sniffed ${sniffed.requiredEnvVars.length} required env var(s)`);
11398
11499
  }
11399
- } catch {}
11500
+ } catch {
11501
+ const composePath = (0, node_path.join)(cwd, "docker-compose.yml");
11502
+ const mappings = parseComposePortMappings(composePath);
11503
+ if (mappings.length > 0) {
11504
+ const ports = await loadPortState(cwd);
11505
+ if (Object.keys(ports).length > 0) {
11506
+ secretsEnv = rewriteUrlsWithPorts(secretsEnv, {
11507
+ dockerEnv: {},
11508
+ ports,
11509
+ mappings
11510
+ });
11511
+ console.log(` 🔌 Applied ${Object.keys(ports).length} port mapping(s)`);
11512
+ }
11513
+ }
11514
+ }
11515
+ secretsEnv = rewriteDatabaseUrlForTests(secretsEnv);
11400
11516
  console.log("");
11401
11517
  const allSecrets = {
11402
11518
  ...secretsEnv,
@@ -11831,9 +11947,9 @@ program.command("secrets:push").description("Push secrets to remote provider (SS
11831
11947
  const globalOptions = program.opts();
11832
11948
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
11833
11949
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await Promise.resolve().then(() => require("./config.cjs"));
11834
- const { pushSecrets: pushSecrets$1 } = await Promise.resolve().then(() => require("./sync-RsnjXYwG.cjs"));
11835
- const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-TEBsryVn.cjs"));
11836
- const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-C7pmBq1u.cjs"));
11950
+ const { pushSecrets: pushSecrets$1 } = await Promise.resolve().then(() => require("./sync-DLlwsrBs.cjs"));
11951
+ const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-D6u4HSg8.cjs"));
11952
+ const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-D6BGLgWf.cjs"));
11837
11953
  const { workspace } = await loadWorkspaceConfig$1();
11838
11954
  const secrets = await readStageSecrets$1(options.stage, workspace.root);
11839
11955
  if (secrets) {
@@ -11856,9 +11972,9 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
11856
11972
  const globalOptions = program.opts();
11857
11973
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
11858
11974
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await Promise.resolve().then(() => require("./config.cjs"));
11859
- const { pullSecrets: pullSecrets$1 } = await Promise.resolve().then(() => require("./sync-RsnjXYwG.cjs"));
11860
- const { writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-C7pmBq1u.cjs"));
11861
- const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-TEBsryVn.cjs"));
11975
+ const { pullSecrets: pullSecrets$1 } = await Promise.resolve().then(() => require("./sync-DLlwsrBs.cjs"));
11976
+ const { writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-D6BGLgWf.cjs"));
11977
+ const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-D6u4HSg8.cjs"));
11862
11978
  const { workspace } = await loadWorkspaceConfig$1();
11863
11979
  let secrets = await pullSecrets$1(options.stage, workspace);
11864
11980
  if (!secrets) {
@@ -11883,8 +11999,8 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
11883
11999
  const globalOptions = program.opts();
11884
12000
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
11885
12001
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await Promise.resolve().then(() => require("./config.cjs"));
11886
- const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-TEBsryVn.cjs"));
11887
- const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-C7pmBq1u.cjs"));
12002
+ const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-D6u4HSg8.cjs"));
12003
+ const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-D6BGLgWf.cjs"));
11888
12004
  const { workspace } = await loadWorkspaceConfig$1();
11889
12005
  const secrets = await readStageSecrets$1(options.stage, workspace.root);
11890
12006
  if (!secrets) {