@geekmidas/cli 1.10.15 → 1.10.17

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 (73) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/{bundler-BWsVDer6.mjs → bundler-B4AackW5.mjs} +2 -2
  3. package/dist/{bundler-BWsVDer6.mjs.map → bundler-B4AackW5.mjs.map} +1 -1
  4. package/dist/{bundler-Drh5KoN5.cjs → bundler-BhhfkI9T.cjs} +2 -2
  5. package/dist/{bundler-Drh5KoN5.cjs.map → bundler-BhhfkI9T.cjs.map} +1 -1
  6. package/dist/config.d.cts +2 -2
  7. package/dist/config.d.mts +2 -2
  8. package/dist/{fullstack-secrets-D9rjTNyx.cjs → fullstack-secrets-DOHBU4Rp.cjs} +110 -4
  9. package/dist/fullstack-secrets-DOHBU4Rp.cjs.map +1 -0
  10. package/dist/{fullstack-secrets-BIFFv4UZ.mjs → fullstack-secrets-x2Kffx7-.mjs} +99 -5
  11. package/dist/fullstack-secrets-x2Kffx7-.mjs.map +1 -0
  12. package/dist/{index-UCsZ_Vkw.d.cts → index-BkibYzso.d.cts} +15 -4
  13. package/dist/index-BkibYzso.d.cts.map +1 -0
  14. package/dist/{index-gXAGDSGu.d.mts → index-CY-ieuRp.d.mts} +15 -4
  15. package/dist/index-CY-ieuRp.d.mts.map +1 -0
  16. package/dist/index.cjs +332 -62
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.mjs +332 -62
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/openapi-BYxAWwok.cjs.map +1 -1
  21. package/dist/openapi-DenF-okj.mjs.map +1 -1
  22. package/dist/openapi.d.cts +1 -1
  23. package/dist/openapi.d.mts +1 -1
  24. package/dist/{reconcile-DxTEausy.mjs → reconcile-BLh6rswz.mjs} +2 -2
  25. package/dist/{reconcile-DxTEausy.mjs.map → reconcile-BLh6rswz.mjs.map} +1 -1
  26. package/dist/{reconcile-LaaJkFlO.cjs → reconcile-Ch7sIcf8.cjs} +2 -2
  27. package/dist/{reconcile-LaaJkFlO.cjs.map → reconcile-Ch7sIcf8.cjs.map} +1 -1
  28. package/dist/{storage-Bu44pwPJ.cjs → storage-B1wvztiJ.cjs} +11 -1
  29. package/dist/{storage-clMAp4sc.mjs.map → storage-B1wvztiJ.cjs.map} +1 -1
  30. package/dist/{storage-CauTheT9.mjs → storage-Cs4WBsc4.mjs} +1 -1
  31. package/dist/{storage-DpqzcjQ5.cjs → storage-DOEtT2Hr.cjs} +1 -1
  32. package/dist/{storage-clMAp4sc.mjs → storage-dbb9RyBl.mjs} +11 -1
  33. package/dist/{storage-Bu44pwPJ.cjs.map → storage-dbb9RyBl.mjs.map} +1 -1
  34. package/dist/{sync-BkalF65h.mjs → sync-COnAugP-.mjs} +1 -1
  35. package/dist/sync-D1Pa30oV.cjs +4 -0
  36. package/dist/{sync-BeiI5rFC.cjs → sync-DGXXSk2v.cjs} +2 -2
  37. package/dist/{sync-BeiI5rFC.cjs.map → sync-DGXXSk2v.cjs.map} +1 -1
  38. package/dist/{sync-CWJ6tL0s.mjs → sync-D_NowTkZ.mjs} +2 -2
  39. package/dist/{sync-CWJ6tL0s.mjs.map → sync-D_NowTkZ.mjs.map} +1 -1
  40. package/dist/{types-DiV9Mbvc.d.mts → types-DdHfUbxk.d.cts} +13 -3
  41. package/dist/types-DdHfUbxk.d.cts.map +1 -0
  42. package/dist/{types-JvWj5Ckc.d.cts → types-OszPdw9m.d.mts} +13 -3
  43. package/dist/types-OszPdw9m.d.mts.map +1 -0
  44. package/dist/workspace/index.d.cts +2 -2
  45. package/dist/workspace/index.d.mts +2 -2
  46. package/dist/workspace-4SP3Gx4Y.cjs.map +1 -1
  47. package/dist/workspace-D4z4A4cq.mjs.map +1 -1
  48. package/package.json +4 -4
  49. package/src/dev/__tests__/entry.spec.ts +3 -5
  50. package/src/dev/__tests__/index.spec.ts +73 -5
  51. package/src/dev/index.ts +33 -25
  52. package/src/docker/compose.ts +130 -2
  53. package/src/init/__tests__/generators.spec.ts +84 -0
  54. package/src/init/generators/docker.ts +128 -16
  55. package/src/init/index.ts +26 -1
  56. package/src/init/templates/index.ts +28 -0
  57. package/src/init/versions.ts +1 -1
  58. package/src/secrets/__tests__/generator.spec.ts +183 -0
  59. package/src/secrets/generator.ts +116 -4
  60. package/src/secrets/storage.ts +12 -0
  61. package/src/secrets/types.ts +11 -1
  62. package/src/setup/__tests__/reconcile-secrets.spec.ts +86 -0
  63. package/src/setup/index.ts +64 -1
  64. package/src/test/__tests__/index.spec.ts +1 -4
  65. package/src/types.ts +13 -1
  66. package/src/workspace/types.ts +13 -2
  67. package/dist/fullstack-secrets-BIFFv4UZ.mjs.map +0 -1
  68. package/dist/fullstack-secrets-D9rjTNyx.cjs.map +0 -1
  69. package/dist/index-UCsZ_Vkw.d.cts.map +0 -1
  70. package/dist/index-gXAGDSGu.d.mts.map +0 -1
  71. package/dist/sync-Bp8xRcuQ.cjs +0 -4
  72. package/dist/types-DiV9Mbvc.d.mts.map +0 -1
  73. package/dist/types-JvWj5Ckc.d.cts.map +0 -1
package/dist/index.mjs CHANGED
@@ -4,13 +4,13 @@ import { getAppBuildOrder, getDependencyEnvVars, getDeployTargetError, isDeployT
4
4
  import { getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceAppInfo, loadWorkspaceConfig, parseModuleConfig } from "./config-jsRYHOHU.mjs";
5
5
  import { getCredentialsPath, getDokployCredentials, getDokployRegistryId, getDokployToken, removeDokployCredentials, storeDokployCredentials, storeDokployRegistryId } from "./credentials-s1kLcIzK.mjs";
6
6
  import { ConstructGenerator, EndpointGenerator, OPENAPI_OUTPUT_PATH, copyAllClients, copyClientToFrontends, generateOpenApi, getBackendOpenApiPath, isPartitionedRoutes, normalizeRoutes, openapiCommand, resolveOpenApiConfig } from "./openapi-DenF-okj.mjs";
7
- import { getKeyPath, maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, writeStageSecrets } from "./storage-clMAp4sc.mjs";
7
+ import { getKeyPath, maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, writeStageSecrets } from "./storage-dbb9RyBl.mjs";
8
8
  import { DokployApi } from "./dokploy-api-2ldYoN3i.mjs";
9
9
  import { encryptSecrets } from "./encryption-BOH5M-f-.mjs";
10
10
  import { CachedStateProvider } from "./CachedStateProvider-BDq5WqSy.mjs";
11
- import { createStageSecrets, generateConnectionUrls, generateDbPassword, generateDbUrl, generateFullstackCustomSecrets, generateServiceCredentials, rotateServicePassword, writeDockerEnvFromSecrets } from "./fullstack-secrets-BIFFv4UZ.mjs";
11
+ import { createStageSecrets, generateConnectionUrls, generateDbPassword, generateDbUrl, generateFullstackCustomSecrets, generateLocalStackCredentials, generateSecurePassword, generateServiceCredentials, rotateServicePassword, writeDockerEnvFromSecrets } from "./fullstack-secrets-x2Kffx7-.mjs";
12
12
  import { generateReactQueryCommand } from "./openapi-react-query-C4UdILaI.mjs";
13
- import { isSSMConfigured, pullSecrets, pushSecrets } from "./sync-CWJ6tL0s.mjs";
13
+ import { isSSMConfigured, pullSecrets, pushSecrets } from "./sync-D_NowTkZ.mjs";
14
14
  import { createRequire } from "node:module";
15
15
  import { copyFileSync, existsSync, readFileSync, unlinkSync } from "node:fs";
16
16
  import { basename, dirname, join, parse, relative, resolve } from "node:path";
@@ -35,7 +35,7 @@ import prompts from "prompts";
35
35
 
36
36
  //#region package.json
37
37
  var name = "@geekmidas/cli";
38
- var version = "1.10.14";
38
+ var version = "1.10.16";
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";
@@ -808,7 +808,9 @@ async function resolveServicePorts(workspaceRoot) {
808
808
  */
809
809
  function replacePortInUrl(url, oldPort, newPort) {
810
810
  if (oldPort === newPort) return url;
811
- return url.replace(new RegExp(`:${oldPort}(?=/|$)`, "g"), `:${newPort}`);
811
+ let result = url.replace(new RegExp(`:${oldPort}(?=[/?#]|$)`, "g"), `:${newPort}`);
812
+ result = result.replace(new RegExp(`%3A${oldPort}(?=[%/?#&]|$)`, "gi"), `%3A${newPort}`);
813
+ return result;
812
814
  }
813
815
  /**
814
816
  * Rewrite connection URLs and port vars in secrets with resolved ports.
@@ -838,7 +840,7 @@ function rewriteUrlsWithPorts(secrets, resolvedPorts) {
838
840
  for (const { defaultPort, resolvedPort } of portReplacements) if (value === String(defaultPort)) result[key] = String(resolvedPort);
839
841
  }
840
842
  for (const [key, value] of Object.entries(result)) {
841
- if (!key.endsWith("_URL") && !key.endsWith("_ENDPOINT") && key !== "DATABASE_URL") continue;
843
+ if (!key.endsWith("_URL") && !key.endsWith("_ENDPOINT") && !key.endsWith("_CONNECTION_STRING") && key !== "DATABASE_URL") continue;
842
844
  let rewritten = value;
843
845
  for (const name$1 of serviceNames) rewritten = rewritten.replace(new RegExp(`@${name$1}:`, "g"), "@localhost:");
844
846
  for (const { defaultPort, resolvedPort } of portReplacements) rewritten = replacePortInUrl(rewritten, defaultPort, resolvedPort);
@@ -1521,17 +1523,16 @@ function findSecretsRoot(startDir) {
1521
1523
  * @internal
1522
1524
  */
1523
1525
  function generateCredentialsInjection(secretsJsonPath) {
1524
- return `import { Credentials } from '@geekmidas/envkit/credentials';
1525
- import { existsSync, readFileSync } from 'node:fs';
1526
+ return `import { existsSync, readFileSync } from 'node:fs';
1526
1527
 
1527
- // Inject dev secrets into Credentials and process.env
1528
+ // Inject dev secrets via globalThis and process.env
1529
+ // Using globalThis.__gkm_credentials__ avoids CJS/ESM interop issues where
1530
+ // Object.assign on the Credentials export only mutates one module copy.
1528
1531
  const secretsPath = '${secretsJsonPath}';
1529
1532
  if (existsSync(secretsPath)) {
1530
1533
  const secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
1531
- Object.assign(Credentials, secrets);
1534
+ globalThis.__gkm_credentials__ = secrets;
1532
1535
  Object.assign(process.env, secrets);
1533
- // Debug: uncomment to verify preload is running
1534
- // console.log('[gkm preload] Injected', Object.keys(secrets).length, 'credentials');
1535
1536
  }
1536
1537
  `;
1537
1538
  }
@@ -1720,13 +1721,14 @@ var EntryRunner = class {
1720
1721
  */
1721
1722
  function generateServerEntryContent(options) {
1722
1723
  const { secretsJsonPath, runtime = "node", enableOpenApi = false, appImportPath = "./app.js" } = options;
1723
- const credentialsInjection = secretsJsonPath ? `import { Credentials } from '@geekmidas/envkit/credentials';
1724
- import { existsSync, readFileSync } from 'node:fs';
1724
+ const credentialsInjection = secretsJsonPath ? `import { existsSync, readFileSync } from 'node:fs';
1725
1725
 
1726
- // Inject dev secrets into Credentials (must happen before app import)
1726
+ // Inject dev secrets via globalThis (must happen before app import)
1727
1727
  const secretsPath = '${secretsJsonPath}';
1728
1728
  if (existsSync(secretsPath)) {
1729
- Object.assign(Credentials, JSON.parse(readFileSync(secretsPath, 'utf-8')));
1729
+ const __secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
1730
+ globalThis.__gkm_credentials__ = __secrets;
1731
+ Object.assign(process.env, __secrets);
1730
1732
  }
1731
1733
 
1732
1734
  ` : "";
@@ -1894,19 +1896,11 @@ async function execCommand(commandArgs, options = {}) {
1894
1896
  if (appName) logger$11.log(`📦 App: ${appName}`);
1895
1897
  const secretCount = Object.keys(credentials).filter((k) => k !== "PORT").length;
1896
1898
  if (secretCount > 0) logger$11.log(`🔐 Loaded ${secretCount} secret(s)`);
1897
- const composePath = join(secretsRoot, "docker-compose.yml");
1898
- const mappings = parseComposePortMappings(composePath);
1899
- if (mappings.length > 0) {
1900
- const ports = await loadPortState(secretsRoot);
1901
- if (Object.keys(ports).length > 0) {
1902
- const rewritten = rewriteUrlsWithPorts(credentials, {
1903
- dockerEnv: {},
1904
- ports,
1905
- mappings
1906
- });
1907
- Object.assign(credentials, rewritten);
1908
- logger$11.log(`🔌 Applied ${Object.keys(ports).length} port mapping(s)`);
1909
- }
1899
+ const resolvedPorts = await resolveServicePorts(secretsRoot);
1900
+ if (resolvedPorts.mappings.length > 0 && Object.keys(resolvedPorts.ports).length > 0) {
1901
+ const rewritten = rewriteUrlsWithPorts(credentials, resolvedPorts);
1902
+ Object.assign(credentials, rewritten);
1903
+ logger$11.log(`🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`);
1910
1904
  }
1911
1905
  try {
1912
1906
  const appInfo = await loadWorkspaceAppInfo(cwd);
@@ -2220,7 +2214,7 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
2220
2214
  let masterKey;
2221
2215
  if (context.production?.bundle && !skipBundle) {
2222
2216
  logger$9.log(`\n📦 Bundling production server...`);
2223
- const { bundleServer } = await import("./bundler-BWsVDer6.mjs");
2217
+ const { bundleServer } = await import("./bundler-B4AackW5.mjs");
2224
2218
  const allConstructs = [
2225
2219
  ...endpoints.map((e) => e.construct),
2226
2220
  ...functions.map((f) => f.construct),
@@ -2884,7 +2878,8 @@ const DEFAULT_SERVICE_IMAGES = {
2884
2878
  redis: "redis",
2885
2879
  rabbitmq: "rabbitmq",
2886
2880
  minio: "minio/minio",
2887
- mailpit: "axllent/mailpit"
2881
+ mailpit: "axllent/mailpit",
2882
+ localstack: "localstack/localstack"
2888
2883
  };
2889
2884
  /** Default Docker image versions for services */
2890
2885
  const DEFAULT_SERVICE_VERSIONS = {
@@ -2892,7 +2887,8 @@ const DEFAULT_SERVICE_VERSIONS = {
2892
2887
  redis: "7-alpine",
2893
2888
  rabbitmq: "3-management-alpine",
2894
2889
  minio: "latest",
2895
- mailpit: "latest"
2890
+ mailpit: "latest",
2891
+ localstack: "latest"
2896
2892
  };
2897
2893
  /** Get the default full image reference for a service */
2898
2894
  function getDefaultImage(serviceName) {
@@ -2959,6 +2955,11 @@ services:
2959
2955
  - SMTP_PASS=\${SMTP_PASS:-${imageName}}
2960
2956
  - SMTP_SECURE=\${SMTP_SECURE:-false}
2961
2957
  - MAIL_FROM=\${MAIL_FROM:-noreply@localhost}
2958
+ `;
2959
+ if (serviceMap.has("localstack")) yaml += ` - AWS_ACCESS_KEY_ID=\${AWS_ACCESS_KEY_ID:-localstack}
2960
+ - AWS_SECRET_ACCESS_KEY=\${AWS_SECRET_ACCESS_KEY:-localstack}
2961
+ - AWS_REGION=\${AWS_REGION:-us-east-1}
2962
+ - AWS_ENDPOINT_URL=\${AWS_ENDPOINT_URL:-http://localstack:4566}
2962
2963
  `;
2963
2964
  yaml += ` healthcheck:
2964
2965
  test: ["CMD", "wget", "-q", "--spider", "http://localhost:${port}${healthCheckPath}"]
@@ -3075,6 +3076,29 @@ services:
3075
3076
  retries: 5
3076
3077
  networks:
3077
3078
  - app-network
3079
+ `;
3080
+ const localstackImage = serviceMap.get("localstack");
3081
+ if (localstackImage) yaml += `
3082
+ localstack:
3083
+ image: ${localstackImage}
3084
+ container_name: localstack
3085
+ restart: unless-stopped
3086
+ environment:
3087
+ SERVICES: sns,sqs
3088
+ AWS_DEFAULT_REGION: \${AWS_REGION:-us-east-1}
3089
+ AWS_ACCESS_KEY_ID: \${AWS_ACCESS_KEY_ID:-localstack}
3090
+ AWS_SECRET_ACCESS_KEY: \${AWS_SECRET_ACCESS_KEY:-localstack}
3091
+ ports:
3092
+ - "\${LOCALSTACK_PORT:-4566}:4566"
3093
+ volumes:
3094
+ - localstack_data:/var/lib/localstack
3095
+ healthcheck:
3096
+ test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
3097
+ interval: 10s
3098
+ timeout: 5s
3099
+ retries: 5
3100
+ networks:
3101
+ - app-network
3078
3102
  `;
3079
3103
  yaml += `
3080
3104
  volumes:
@@ -3086,6 +3110,8 @@ volumes:
3086
3110
  if (serviceMap.has("rabbitmq")) yaml += ` rabbitmq_data:
3087
3111
  `;
3088
3112
  if (serviceMap.has("minio")) yaml += ` minio_data:
3113
+ `;
3114
+ if (serviceMap.has("localstack")) yaml += ` localstack_data:
3089
3115
  `;
3090
3116
  yaml += `
3091
3117
  networks:
@@ -3142,6 +3168,9 @@ function generateWorkspaceCompose(workspace, options = {}) {
3142
3168
  const hasRedis = services.cache !== void 0 && services.cache !== false;
3143
3169
  const hasMail = services.mail !== void 0 && services.mail !== false;
3144
3170
  const hasMinio = services.storage !== void 0 && services.storage !== false;
3171
+ const eventsBackend = services.events;
3172
+ const hasLocalStack = eventsBackend === "sns";
3173
+ const hasRabbitMQ = eventsBackend === "rabbitmq" || services.rabbitmq !== void 0;
3145
3174
  const postgresImage = getInfraServiceImage("postgres", services.db);
3146
3175
  const redisImage = getInfraServiceImage("redis", services.cache);
3147
3176
  const minioImage = getInfraServiceImage("minio", services.storage);
@@ -3157,7 +3186,8 @@ services:
3157
3186
  hasPostgres,
3158
3187
  hasRedis,
3159
3188
  hasMinio,
3160
- hasMail
3189
+ hasMail,
3190
+ eventsBackend
3161
3191
  });
3162
3192
  if (hasPostgres) yaml += `
3163
3193
  postgres:
@@ -3233,6 +3263,49 @@ services:
3233
3263
  retries: 5
3234
3264
  networks:
3235
3265
  - workspace-network
3266
+ `;
3267
+ if (hasLocalStack) yaml += `
3268
+ localstack:
3269
+ image: localstack/localstack:latest
3270
+ container_name: ${workspace.name}-localstack
3271
+ restart: unless-stopped
3272
+ environment:
3273
+ SERVICES: sns,sqs
3274
+ AWS_DEFAULT_REGION: \${AWS_REGION:-us-east-1}
3275
+ AWS_ACCESS_KEY_ID: \${AWS_ACCESS_KEY_ID:-localstack}
3276
+ AWS_SECRET_ACCESS_KEY: \${AWS_SECRET_ACCESS_KEY:-localstack}
3277
+ ports:
3278
+ - "\${LOCALSTACK_PORT:-4566}:4566"
3279
+ volumes:
3280
+ - localstack_data:/var/lib/localstack
3281
+ healthcheck:
3282
+ test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
3283
+ interval: 10s
3284
+ timeout: 5s
3285
+ retries: 5
3286
+ networks:
3287
+ - workspace-network
3288
+ `;
3289
+ if (hasRabbitMQ) yaml += `
3290
+ rabbitmq:
3291
+ image: rabbitmq:3-management-alpine
3292
+ container_name: ${workspace.name}-rabbitmq
3293
+ restart: unless-stopped
3294
+ environment:
3295
+ RABBITMQ_DEFAULT_USER: \${RABBITMQ_USER:-guest}
3296
+ RABBITMQ_DEFAULT_PASS: \${RABBITMQ_PASSWORD:-guest}
3297
+ ports:
3298
+ - "\${RABBITMQ_HOST_PORT:-5672}:5672"
3299
+ - "\${RABBITMQ_MGMT_HOST_PORT:-15672}:15672"
3300
+ volumes:
3301
+ - rabbitmq_data:/var/lib/rabbitmq
3302
+ healthcheck:
3303
+ test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
3304
+ interval: 10s
3305
+ timeout: 5s
3306
+ retries: 5
3307
+ networks:
3308
+ - workspace-network
3236
3309
  `;
3237
3310
  yaml += `
3238
3311
  volumes:
@@ -3242,6 +3315,10 @@ volumes:
3242
3315
  if (hasRedis) yaml += ` redis_data:
3243
3316
  `;
3244
3317
  if (hasMinio) yaml += ` minio_data:
3318
+ `;
3319
+ if (hasLocalStack) yaml += ` localstack_data:
3320
+ `;
3321
+ if (hasRabbitMQ) yaml += ` rabbitmq_data:
3245
3322
  `;
3246
3323
  yaml += `
3247
3324
  networks:
@@ -3277,7 +3354,7 @@ function getInfraServiceImage(serviceName, config$1) {
3277
3354
  * Generate a service definition for an app.
3278
3355
  */
3279
3356
  function generateAppService(appName, app, allApps, options) {
3280
- const { registry, projectName, hasPostgres, hasRedis, hasMinio, hasMail } = options;
3357
+ const { registry, projectName, hasPostgres, hasRedis, hasMinio, hasMail, eventsBackend } = options;
3281
3358
  const imageRef = registry ? `\${REGISTRY:-${registry}}/` : "";
3282
3359
  const healthCheckPath = app.type === "frontend" ? "/" : "/health";
3283
3360
  const healthCheckCmd = app.type === "frontend" ? `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}/"]` : `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}${healthCheckPath}"]`;
@@ -3319,6 +3396,16 @@ function generateAppService(appName, app, allApps, options) {
3319
3396
  - SMTP_SECURE=\${SMTP_SECURE:-false}
3320
3397
  - MAIL_FROM=\${MAIL_FROM:-noreply@localhost}
3321
3398
  `;
3399
+ if (eventsBackend) {
3400
+ yaml += ` - EVENT_PUBLISHER_CONNECTION_STRING=\${EVENT_PUBLISHER_CONNECTION_STRING}
3401
+ - EVENT_SUBSCRIBER_CONNECTION_STRING=\${EVENT_SUBSCRIBER_CONNECTION_STRING}
3402
+ `;
3403
+ if (eventsBackend === "sns") yaml += ` - AWS_ACCESS_KEY_ID=\${AWS_ACCESS_KEY_ID:-localstack}
3404
+ - AWS_SECRET_ACCESS_KEY=\${AWS_SECRET_ACCESS_KEY:-localstack}
3405
+ - AWS_REGION=\${AWS_REGION:-us-east-1}
3406
+ - AWS_ENDPOINT_URL=\${AWS_ENDPOINT_URL:-http://localstack:4566}
3407
+ `;
3408
+ }
3322
3409
  }
3323
3410
  yaml += ` healthcheck:
3324
3411
  test: ${healthCheckCmd}
@@ -3332,6 +3419,8 @@ function generateAppService(appName, app, allApps, options) {
3332
3419
  if (hasRedis) dependencies$1.push("redis");
3333
3420
  if (hasMinio) dependencies$1.push("minio");
3334
3421
  if (hasMail) dependencies$1.push("mailpit");
3422
+ if (eventsBackend === "sns") dependencies$1.push("localstack");
3423
+ if (eventsBackend === "rabbitmq") dependencies$1.push("rabbitmq");
3335
3424
  }
3336
3425
  if (dependencies$1.length > 0) {
3337
3426
  yaml += ` depends_on:
@@ -6376,7 +6465,7 @@ async function deployCommand(options) {
6376
6465
  dokployConfig = setupResult.config;
6377
6466
  finalRegistry = dokployConfig.registry ?? dockerConfig.registry;
6378
6467
  if (setupResult.serviceUrls) {
6379
- const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await import("./storage-CauTheT9.mjs");
6468
+ const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await import("./storage-Cs4WBsc4.mjs");
6380
6469
  let secrets = await readStageSecrets$1(stage);
6381
6470
  if (!secrets) {
6382
6471
  logger$3.log(` Creating secrets file for stage "${stage}"...`);
@@ -6663,7 +6752,7 @@ const GEEKMIDAS_VERSIONS = {
6663
6752
  "@geekmidas/constructs": "~3.0.2",
6664
6753
  "@geekmidas/db": "~1.0.0",
6665
6754
  "@geekmidas/emailkit": "~1.0.0",
6666
- "@geekmidas/envkit": "~1.0.3",
6755
+ "@geekmidas/envkit": "~1.0.4",
6667
6756
  "@geekmidas/errors": "~1.0.0",
6668
6757
  "@geekmidas/events": "~1.1.0",
6669
6758
  "@geekmidas/logger": "~1.0.0",
@@ -7145,11 +7234,11 @@ function generateDockerFiles(options, template, dbApps) {
7145
7234
  if (isFullstack && dbApps?.length) {
7146
7235
  files.push({
7147
7236
  path: "docker/postgres/init.sh",
7148
- content: generatePostgresInitScript(dbApps)
7237
+ content: generatePostgresInitScript(dbApps, options.services?.events)
7149
7238
  });
7150
7239
  files.push({
7151
7240
  path: "docker/.env",
7152
- content: generateDockerEnv(dbApps)
7241
+ content: generateDockerEnv(dbApps, options.services?.events)
7153
7242
  });
7154
7243
  }
7155
7244
  }
@@ -7249,6 +7338,47 @@ function generateDockerFiles(options, template, dbApps) {
7249
7338
  retries: 5`);
7250
7339
  volumes.push(" minio_data:");
7251
7340
  }
7341
+ if (options.services?.events === "sns") {
7342
+ services.push(` localstack:
7343
+ image: localstack/localstack:latest
7344
+ container_name: ${options.name}-localstack
7345
+ restart: unless-stopped
7346
+ environment:
7347
+ SERVICES: sns,sqs
7348
+ AWS_DEFAULT_REGION: \${AWS_REGION:-us-east-1}
7349
+ AWS_ACCESS_KEY_ID: \${AWS_ACCESS_KEY_ID:-localstack}
7350
+ AWS_SECRET_ACCESS_KEY: \${AWS_SECRET_ACCESS_KEY:-localstack}
7351
+ ports:
7352
+ - '\${LOCALSTACK_PORT:-4566}:4566'
7353
+ volumes:
7354
+ - localstack_data:/var/lib/localstack
7355
+ healthcheck:
7356
+ test: ['CMD', 'curl', '-f', 'http://localhost:4566/_localstack/health']
7357
+ interval: 10s
7358
+ timeout: 5s
7359
+ retries: 5`);
7360
+ volumes.push(" localstack_data:");
7361
+ }
7362
+ if (options.services?.events === "rabbitmq" && !hasWorker) {
7363
+ services.push(` rabbitmq:
7364
+ image: rabbitmq:3-management-alpine
7365
+ container_name: ${options.name}-rabbitmq
7366
+ restart: unless-stopped
7367
+ ports:
7368
+ - '\${RABBITMQ_HOST_PORT:-5672}:5672'
7369
+ - '\${RABBITMQ_MGMT_HOST_PORT:-15672}:15672'
7370
+ environment:
7371
+ RABBITMQ_DEFAULT_USER: guest
7372
+ RABBITMQ_DEFAULT_PASS: guest
7373
+ volumes:
7374
+ - rabbitmq_data:/var/lib/rabbitmq
7375
+ healthcheck:
7376
+ test: ['CMD', 'rabbitmq-diagnostics', 'check_running']
7377
+ interval: 10s
7378
+ timeout: 5s
7379
+ retries: 5`);
7380
+ if (!volumes.includes(" rabbitmq_data:")) volumes.push(" rabbitmq_data:");
7381
+ }
7252
7382
  let dockerCompose = `# Use "gkm dev" or "gkm test" to start services.
7253
7383
  # Running "docker compose up" directly will not inject secrets or resolve ports.
7254
7384
  services:
@@ -7267,11 +7397,12 @@ ${volumes.join("\n")}
7267
7397
  /**
7268
7398
  * Generate .env file for docker-compose with database passwords
7269
7399
  */
7270
- function generateDockerEnv(apps) {
7400
+ function generateDockerEnv(apps, eventsBackend) {
7271
7401
  const envVars = apps.map((app) => {
7272
7402
  const envVar = `${app.name.toUpperCase()}_DB_PASSWORD`;
7273
7403
  return `${envVar}=${app.password}`;
7274
7404
  });
7405
+ if (eventsBackend === "pgboss") envVars.push(`PGBOSS_DB_PASSWORD=pgboss-dev-password`);
7275
7406
  return `# Auto-generated docker environment file
7276
7407
  # Contains database passwords for docker-compose postgres init
7277
7408
  # This file is gitignored - do not commit to version control
@@ -7279,33 +7410,50 @@ ${envVars.join("\n")}
7279
7410
  `;
7280
7411
  }
7281
7412
  /**
7282
- * Generate PostgreSQL init shell script that creates per-app users with separate schemas
7283
- * Uses environment variables for passwords (more secure than hardcoded values)
7413
+ * Generate PostgreSQL init shell script that creates per-app users with separate schemas.
7414
+ * Uses idempotent DO blocks so the script can be re-run safely.
7284
7415
  * - api user: uses public schema
7285
7416
  * - auth user: uses auth schema with search_path=auth
7417
+ * - pgboss user: uses pgboss schema (when events === 'pgboss')
7286
7418
  */
7287
- function generatePostgresInitScript(apps) {
7419
+ function generatePostgresInitScript(apps, eventsBackend) {
7288
7420
  const userCreations = apps.map((app) => {
7289
7421
  const userName = app.name.replace(/-/g, "_");
7290
7422
  const envVar = `${app.name.toUpperCase()}_DB_PASSWORD`;
7291
7423
  const isApi = app.name === "api";
7292
7424
  const schemaName = isApi ? "public" : userName;
7293
7425
  if (isApi) return `
7294
- # Create ${app.name} user (uses public schema)
7426
+ # Create ${app.name} user (uses public schema) - idempotent
7295
7427
  echo "Creating user ${userName}..."
7296
7428
  psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
7297
- CREATE USER ${userName} WITH PASSWORD '$${envVar}';
7429
+ DO \\$\\$
7430
+ BEGIN
7431
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${userName}') THEN
7432
+ CREATE USER ${userName} WITH PASSWORD '$${envVar}';
7433
+ ELSE
7434
+ ALTER USER ${userName} WITH PASSWORD '$${envVar}';
7435
+ END IF;
7436
+ END
7437
+ \\$\\$;
7298
7438
  GRANT ALL ON SCHEMA public TO ${userName};
7299
7439
  ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ${userName};
7300
7440
  ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ${userName};
7301
7441
  EOSQL
7302
7442
  `;
7303
7443
  return `
7304
- # Create ${app.name} user with dedicated schema
7444
+ # Create ${app.name} user with dedicated schema - idempotent
7305
7445
  echo "Creating user ${userName} with schema ${schemaName}..."
7306
7446
  psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
7307
- CREATE USER ${userName} WITH PASSWORD '$${envVar}';
7308
- CREATE SCHEMA ${schemaName} AUTHORIZATION ${userName};
7447
+ DO \\$\\$
7448
+ BEGIN
7449
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${userName}') THEN
7450
+ CREATE USER ${userName} WITH PASSWORD '$${envVar}';
7451
+ ELSE
7452
+ ALTER USER ${userName} WITH PASSWORD '$${envVar}';
7453
+ END IF;
7454
+ END
7455
+ \\$\\$;
7456
+ CREATE SCHEMA IF NOT EXISTS ${schemaName} AUTHORIZATION ${userName};
7309
7457
  ALTER USER ${userName} SET search_path TO ${schemaName};
7310
7458
  GRANT USAGE ON SCHEMA ${schemaName} TO ${userName};
7311
7459
  GRANT ALL ON ALL TABLES IN SCHEMA ${schemaName} TO ${userName};
@@ -7315,14 +7463,46 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E
7315
7463
  EOSQL
7316
7464
  `;
7317
7465
  });
7466
+ let pgbossBlock = "";
7467
+ if (eventsBackend === "pgboss") pgbossBlock = `
7468
+ # Create pgboss user with dedicated schema - idempotent
7469
+ echo "Creating pgboss user and schema..."
7470
+ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
7471
+ DO \\$\\$
7472
+ BEGIN
7473
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'pgboss') THEN
7474
+ CREATE USER pgboss WITH PASSWORD '$PGBOSS_DB_PASSWORD';
7475
+ ELSE
7476
+ ALTER USER pgboss WITH PASSWORD '$PGBOSS_DB_PASSWORD';
7477
+ END IF;
7478
+ END
7479
+ \\$\\$;
7480
+ CREATE SCHEMA IF NOT EXISTS pgboss AUTHORIZATION pgboss;
7481
+ ALTER USER pgboss SET search_path TO pgboss;
7482
+ GRANT USAGE ON SCHEMA pgboss TO pgboss;
7483
+ GRANT ALL ON ALL TABLES IN SCHEMA pgboss TO pgboss;
7484
+ GRANT ALL ON ALL SEQUENCES IN SCHEMA pgboss TO pgboss;
7485
+ ALTER DEFAULT PRIVILEGES IN SCHEMA pgboss GRANT ALL ON TABLES TO pgboss;
7486
+ ALTER DEFAULT PRIVILEGES IN SCHEMA pgboss GRANT ALL ON SEQUENCES TO pgboss;
7487
+ EOSQL
7488
+ `;
7489
+ const extensions = `
7490
+ # Create extensions
7491
+ echo "Creating extensions..."
7492
+ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
7493
+ CREATE EXTENSION IF NOT EXISTS pg_trgm;
7494
+ CREATE EXTENSION IF NOT EXISTS citext;
7495
+ EOSQL
7496
+ `;
7318
7497
  return `#!/bin/bash
7319
7498
  set -e
7320
7499
 
7321
- # Auto-generated PostgreSQL init script
7500
+ # Auto-generated PostgreSQL init script (idempotent - safe to re-run)
7322
7501
  # Creates per-app users with separate schemas in a single database
7323
7502
  # - api: uses public schema
7324
- # - auth: uses auth schema (search_path=auth)
7325
- ${userCreations.join("\n")}
7503
+ # - auth: uses auth schema (search_path=auth)${eventsBackend === "pgboss" ? "\n# - pgboss: uses pgboss schema for event processing" : ""}
7504
+ ${extensions}
7505
+ ${userCreations.join("\n")}${pgbossBlock}
7326
7506
  echo "Database initialization complete!"
7327
7507
  `;
7328
7508
  }
@@ -8814,6 +8994,31 @@ const servicesChoices = [
8814
8994
  }
8815
8995
  ];
8816
8996
  /**
8997
+ * Event backend choices for prompts
8998
+ */
8999
+ const eventsBackendChoices = [
9000
+ {
9001
+ title: "pg-boss",
9002
+ value: "pgboss",
9003
+ description: "PostgreSQL-based job queue (reuses postgres, no extra container)"
9004
+ },
9005
+ {
9006
+ title: "SNS/SQS",
9007
+ value: "sns",
9008
+ description: "AWS SNS+SQS via LocalStack for local dev"
9009
+ },
9010
+ {
9011
+ title: "RabbitMQ",
9012
+ value: "rabbitmq",
9013
+ description: "AMQP message broker"
9014
+ },
9015
+ {
9016
+ title: "None",
9017
+ value: void 0,
9018
+ description: "Skip event backend"
9019
+ }
9020
+ ];
9021
+ /**
8817
9022
  * Get a template by name
8818
9023
  */
8819
9024
  function getTemplate(name$1) {
@@ -10921,6 +11126,13 @@ async function initCommand(projectName, options = {}) {
10921
11126
  })),
10922
11127
  hint: "- Space to select. Return to submit"
10923
11128
  },
11129
+ {
11130
+ type: options.yes ? null : "select",
11131
+ name: "eventsBackend",
11132
+ message: "Event backend:",
11133
+ choices: eventsBackendChoices,
11134
+ initial: 0
11135
+ },
10924
11136
  {
10925
11137
  type: options.yes ? null : "select",
10926
11138
  name: "packageManager",
@@ -10983,12 +11195,15 @@ async function initCommand(projectName, options = {}) {
10983
11195
  "mail",
10984
11196
  "storage"
10985
11197
  ] : answers.services || [];
11198
+ const eventsBackend = options.yes ? isFullstack ? "pgboss" : void 0 : answers.eventsBackend;
10986
11199
  const services = {
10987
11200
  db: servicesArray.includes("db"),
10988
11201
  cache: servicesArray.includes("cache"),
10989
11202
  mail: servicesArray.includes("mail"),
10990
- storage: servicesArray.includes("storage")
11203
+ storage: servicesArray.includes("storage"),
11204
+ events: eventsBackend
10991
11205
  };
11206
+ if (services.events === "pgboss") services.db = true;
10992
11207
  const pkgManager = options.pm ? options.pm : options.yes ? "pnpm" : answers.packageManager ?? detectedPkgManager;
10993
11208
  const deployTarget = options.yes ? "dokploy" : answers.deployTarget ?? "dokploy";
10994
11209
  const database = services.db;
@@ -11071,7 +11286,12 @@ async function initCommand(projectName, options = {}) {
11071
11286
  if (services.cache) secretServices.push("redis");
11072
11287
  if (services.storage) secretServices.push("minio");
11073
11288
  if (services.mail) secretServices.push("mailpit");
11074
- const devSecrets = createStageSecrets("development", secretServices, { projectName: name$1 });
11289
+ if (services.events === "sns") secretServices.push("localstack");
11290
+ if (services.events === "rabbitmq") secretServices.push("rabbitmq");
11291
+ const devSecrets = createStageSecrets("development", secretServices, {
11292
+ projectName: name$1,
11293
+ eventsBackend: services.events
11294
+ });
11075
11295
  const customSecrets = {
11076
11296
  NODE_ENV: "development",
11077
11297
  PORT: "3000",
@@ -11511,10 +11731,55 @@ function reconcileSecrets(secrets, workspace) {
11511
11731
  [name$1]: creds
11512
11732
  }
11513
11733
  };
11514
- result.urls = generateConnectionUrls(result.services);
11734
+ result.urls = generateConnectionUrls(result.services, result.eventsBackend);
11515
11735
  logger$1.log(` 🔄 Adding missing service credentials: ${name$1}`);
11516
11736
  changed = true;
11517
11737
  }
11738
+ const eventsBackend = workspace.services.events;
11739
+ if (eventsBackend && result.eventsBackend !== eventsBackend) {
11740
+ result.eventsBackend = eventsBackend;
11741
+ if (eventsBackend === "pgboss" && !result.services.pgboss) {
11742
+ result = {
11743
+ ...result,
11744
+ services: {
11745
+ ...result.services,
11746
+ pgboss: {
11747
+ host: result.services.postgres?.host ?? "localhost",
11748
+ port: result.services.postgres?.port ?? 5432,
11749
+ username: "pgboss",
11750
+ password: generateSecurePassword(),
11751
+ database: result.services.postgres?.database ?? "app"
11752
+ }
11753
+ }
11754
+ };
11755
+ logger$1.log(" 🔄 Adding missing service credentials: pgboss");
11756
+ changed = true;
11757
+ }
11758
+ if (eventsBackend === "sns" && !result.services.localstack) {
11759
+ result = {
11760
+ ...result,
11761
+ services: {
11762
+ ...result.services,
11763
+ localstack: generateLocalStackCredentials()
11764
+ }
11765
+ };
11766
+ logger$1.log(" 🔄 Adding missing service credentials: localstack");
11767
+ changed = true;
11768
+ }
11769
+ if (eventsBackend === "rabbitmq" && !result.services.rabbitmq) {
11770
+ result = {
11771
+ ...result,
11772
+ services: {
11773
+ ...result.services,
11774
+ rabbitmq: generateServiceCredentials("rabbitmq")
11775
+ }
11776
+ };
11777
+ logger$1.log(" 🔄 Adding missing service credentials: rabbitmq");
11778
+ changed = true;
11779
+ }
11780
+ result.urls = generateConnectionUrls(result.services, eventsBackend);
11781
+ changed = true;
11782
+ }
11518
11783
  const isMultiApp = Object.keys(workspace.apps).length > 1;
11519
11784
  if (isMultiApp) {
11520
11785
  const expected = generateFullstackCustomSecrets(workspace);
@@ -11547,7 +11812,12 @@ async function generateFreshSecrets(stage, workspace, options) {
11547
11812
  if (workspace.services.cache) serviceNames.push("redis");
11548
11813
  if (workspace.services.storage) serviceNames.push("minio");
11549
11814
  if (workspace.services.mail) serviceNames.push("mailpit");
11550
- const secrets = createStageSecrets(stage, serviceNames, { projectName: workspace.name });
11815
+ if (workspace.services.events === "sns") serviceNames.push("localstack");
11816
+ if (workspace.services.events === "rabbitmq") serviceNames.push("rabbitmq");
11817
+ const secrets = createStageSecrets(stage, serviceNames, {
11818
+ projectName: workspace.name,
11819
+ eventsBackend: workspace.services.events
11820
+ });
11551
11821
  const isMultiApp = Object.keys(workspace.apps).length > 1;
11552
11822
  if (isMultiApp) {
11553
11823
  const customSecrets = generateFullstackCustomSecrets(workspace);
@@ -12097,9 +12367,9 @@ program.command("secrets:push").description("Push secrets to remote provider (SS
12097
12367
  const globalOptions = program.opts();
12098
12368
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
12099
12369
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await import("./config.mjs");
12100
- const { pushSecrets: pushSecrets$1 } = await import("./sync-BkalF65h.mjs");
12101
- const { reconcileMissingSecrets } = await import("./reconcile-DxTEausy.mjs");
12102
- const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-CauTheT9.mjs");
12370
+ const { pushSecrets: pushSecrets$1 } = await import("./sync-COnAugP-.mjs");
12371
+ const { reconcileMissingSecrets } = await import("./reconcile-BLh6rswz.mjs");
12372
+ const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-Cs4WBsc4.mjs");
12103
12373
  const { workspace } = await loadWorkspaceConfig$1();
12104
12374
  const secrets = await readStageSecrets$1(options.stage, workspace.root);
12105
12375
  if (secrets) {
@@ -12122,9 +12392,9 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
12122
12392
  const globalOptions = program.opts();
12123
12393
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
12124
12394
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await import("./config.mjs");
12125
- const { pullSecrets: pullSecrets$1 } = await import("./sync-BkalF65h.mjs");
12126
- const { writeStageSecrets: writeStageSecrets$1 } = await import("./storage-CauTheT9.mjs");
12127
- const { reconcileMissingSecrets } = await import("./reconcile-DxTEausy.mjs");
12395
+ const { pullSecrets: pullSecrets$1 } = await import("./sync-COnAugP-.mjs");
12396
+ const { writeStageSecrets: writeStageSecrets$1 } = await import("./storage-Cs4WBsc4.mjs");
12397
+ const { reconcileMissingSecrets } = await import("./reconcile-BLh6rswz.mjs");
12128
12398
  const { workspace } = await loadWorkspaceConfig$1();
12129
12399
  let secrets = await pullSecrets$1(options.stage, workspace);
12130
12400
  if (!secrets) {
@@ -12149,8 +12419,8 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
12149
12419
  const globalOptions = program.opts();
12150
12420
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
12151
12421
  const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await import("./config.mjs");
12152
- const { reconcileMissingSecrets } = await import("./reconcile-DxTEausy.mjs");
12153
- const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-CauTheT9.mjs");
12422
+ const { reconcileMissingSecrets } = await import("./reconcile-BLh6rswz.mjs");
12423
+ const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-Cs4WBsc4.mjs");
12154
12424
  const { workspace } = await loadWorkspaceConfig$1();
12155
12425
  const secrets = await readStageSecrets$1(options.stage, workspace.root);
12156
12426
  if (!secrets) {