@geekmidas/cli 1.10.15 → 1.10.16

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 +6 -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 +322 -46
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.mjs +322 -46
  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 +3 -3
  49. package/src/dev/__tests__/entry.spec.ts +3 -5
  50. package/src/dev/__tests__/index.spec.ts +5 -5
  51. package/src/dev/index.ts +10 -10
  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/secrets/__tests__/generator.spec.ts +183 -0
  58. package/src/secrets/generator.ts +116 -4
  59. package/src/secrets/storage.ts +12 -0
  60. package/src/secrets/types.ts +11 -1
  61. package/src/setup/__tests__/reconcile-secrets.spec.ts +86 -0
  62. package/src/setup/index.ts +64 -1
  63. package/src/test/__tests__/index.spec.ts +1 -4
  64. package/src/types.ts +13 -1
  65. package/src/workspace/types.ts +13 -2
  66. package/dist/fullstack-secrets-BIFFv4UZ.mjs.map +0 -1
  67. package/dist/fullstack-secrets-D9rjTNyx.cjs.map +0 -1
  68. package/dist/index-UCsZ_Vkw.d.cts.map +0 -1
  69. package/dist/index-gXAGDSGu.d.mts.map +0 -1
  70. package/dist/sync-Bp8xRcuQ.cjs +0 -4
  71. package/dist/types-DiV9Mbvc.d.mts.map +0 -1
  72. 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.15";
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";
@@ -1521,17 +1521,16 @@ function findSecretsRoot(startDir) {
1521
1521
  * @internal
1522
1522
  */
1523
1523
  function generateCredentialsInjection(secretsJsonPath) {
1524
- return `import { Credentials } from '@geekmidas/envkit/credentials';
1525
- import { existsSync, readFileSync } from 'node:fs';
1524
+ return `import { existsSync, readFileSync } from 'node:fs';
1526
1525
 
1527
- // Inject dev secrets into Credentials and process.env
1526
+ // Inject dev secrets via globalThis and process.env
1527
+ // Using globalThis.__gkm_credentials__ avoids CJS/ESM interop issues where
1528
+ // Object.assign on the Credentials export only mutates one module copy.
1528
1529
  const secretsPath = '${secretsJsonPath}';
1529
1530
  if (existsSync(secretsPath)) {
1530
1531
  const secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
1531
- Object.assign(Credentials, secrets);
1532
+ globalThis.__gkm_credentials__ = secrets;
1532
1533
  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
1534
  }
1536
1535
  `;
1537
1536
  }
@@ -1720,13 +1719,14 @@ var EntryRunner = class {
1720
1719
  */
1721
1720
  function generateServerEntryContent(options) {
1722
1721
  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';
1722
+ const credentialsInjection = secretsJsonPath ? `import { existsSync, readFileSync } from 'node:fs';
1725
1723
 
1726
- // Inject dev secrets into Credentials (must happen before app import)
1724
+ // Inject dev secrets via globalThis (must happen before app import)
1727
1725
  const secretsPath = '${secretsJsonPath}';
1728
1726
  if (existsSync(secretsPath)) {
1729
- Object.assign(Credentials, JSON.parse(readFileSync(secretsPath, 'utf-8')));
1727
+ const __secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
1728
+ globalThis.__gkm_credentials__ = __secrets;
1729
+ Object.assign(process.env, __secrets);
1730
1730
  }
1731
1731
 
1732
1732
  ` : "";
@@ -2220,7 +2220,7 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
2220
2220
  let masterKey;
2221
2221
  if (context.production?.bundle && !skipBundle) {
2222
2222
  logger$9.log(`\n📦 Bundling production server...`);
2223
- const { bundleServer } = await import("./bundler-BWsVDer6.mjs");
2223
+ const { bundleServer } = await import("./bundler-B4AackW5.mjs");
2224
2224
  const allConstructs = [
2225
2225
  ...endpoints.map((e) => e.construct),
2226
2226
  ...functions.map((f) => f.construct),
@@ -2884,7 +2884,8 @@ const DEFAULT_SERVICE_IMAGES = {
2884
2884
  redis: "redis",
2885
2885
  rabbitmq: "rabbitmq",
2886
2886
  minio: "minio/minio",
2887
- mailpit: "axllent/mailpit"
2887
+ mailpit: "axllent/mailpit",
2888
+ localstack: "localstack/localstack"
2888
2889
  };
2889
2890
  /** Default Docker image versions for services */
2890
2891
  const DEFAULT_SERVICE_VERSIONS = {
@@ -2892,7 +2893,8 @@ const DEFAULT_SERVICE_VERSIONS = {
2892
2893
  redis: "7-alpine",
2893
2894
  rabbitmq: "3-management-alpine",
2894
2895
  minio: "latest",
2895
- mailpit: "latest"
2896
+ mailpit: "latest",
2897
+ localstack: "latest"
2896
2898
  };
2897
2899
  /** Get the default full image reference for a service */
2898
2900
  function getDefaultImage(serviceName) {
@@ -2959,6 +2961,11 @@ services:
2959
2961
  - SMTP_PASS=\${SMTP_PASS:-${imageName}}
2960
2962
  - SMTP_SECURE=\${SMTP_SECURE:-false}
2961
2963
  - MAIL_FROM=\${MAIL_FROM:-noreply@localhost}
2964
+ `;
2965
+ if (serviceMap.has("localstack")) yaml += ` - AWS_ACCESS_KEY_ID=\${AWS_ACCESS_KEY_ID:-localstack}
2966
+ - AWS_SECRET_ACCESS_KEY=\${AWS_SECRET_ACCESS_KEY:-localstack}
2967
+ - AWS_REGION=\${AWS_REGION:-us-east-1}
2968
+ - AWS_ENDPOINT_URL=\${AWS_ENDPOINT_URL:-http://localstack:4566}
2962
2969
  `;
2963
2970
  yaml += ` healthcheck:
2964
2971
  test: ["CMD", "wget", "-q", "--spider", "http://localhost:${port}${healthCheckPath}"]
@@ -3075,6 +3082,29 @@ services:
3075
3082
  retries: 5
3076
3083
  networks:
3077
3084
  - app-network
3085
+ `;
3086
+ const localstackImage = serviceMap.get("localstack");
3087
+ if (localstackImage) yaml += `
3088
+ localstack:
3089
+ image: ${localstackImage}
3090
+ container_name: localstack
3091
+ restart: unless-stopped
3092
+ environment:
3093
+ SERVICES: sns,sqs
3094
+ AWS_DEFAULT_REGION: \${AWS_REGION:-us-east-1}
3095
+ AWS_ACCESS_KEY_ID: \${AWS_ACCESS_KEY_ID:-localstack}
3096
+ AWS_SECRET_ACCESS_KEY: \${AWS_SECRET_ACCESS_KEY:-localstack}
3097
+ ports:
3098
+ - "\${LOCALSTACK_PORT:-4566}:4566"
3099
+ volumes:
3100
+ - localstack_data:/var/lib/localstack
3101
+ healthcheck:
3102
+ test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
3103
+ interval: 10s
3104
+ timeout: 5s
3105
+ retries: 5
3106
+ networks:
3107
+ - app-network
3078
3108
  `;
3079
3109
  yaml += `
3080
3110
  volumes:
@@ -3086,6 +3116,8 @@ volumes:
3086
3116
  if (serviceMap.has("rabbitmq")) yaml += ` rabbitmq_data:
3087
3117
  `;
3088
3118
  if (serviceMap.has("minio")) yaml += ` minio_data:
3119
+ `;
3120
+ if (serviceMap.has("localstack")) yaml += ` localstack_data:
3089
3121
  `;
3090
3122
  yaml += `
3091
3123
  networks:
@@ -3142,6 +3174,9 @@ function generateWorkspaceCompose(workspace, options = {}) {
3142
3174
  const hasRedis = services.cache !== void 0 && services.cache !== false;
3143
3175
  const hasMail = services.mail !== void 0 && services.mail !== false;
3144
3176
  const hasMinio = services.storage !== void 0 && services.storage !== false;
3177
+ const eventsBackend = services.events;
3178
+ const hasLocalStack = eventsBackend === "sns";
3179
+ const hasRabbitMQ = eventsBackend === "rabbitmq" || services.rabbitmq !== void 0;
3145
3180
  const postgresImage = getInfraServiceImage("postgres", services.db);
3146
3181
  const redisImage = getInfraServiceImage("redis", services.cache);
3147
3182
  const minioImage = getInfraServiceImage("minio", services.storage);
@@ -3157,7 +3192,8 @@ services:
3157
3192
  hasPostgres,
3158
3193
  hasRedis,
3159
3194
  hasMinio,
3160
- hasMail
3195
+ hasMail,
3196
+ eventsBackend
3161
3197
  });
3162
3198
  if (hasPostgres) yaml += `
3163
3199
  postgres:
@@ -3233,6 +3269,49 @@ services:
3233
3269
  retries: 5
3234
3270
  networks:
3235
3271
  - workspace-network
3272
+ `;
3273
+ if (hasLocalStack) yaml += `
3274
+ localstack:
3275
+ image: localstack/localstack:latest
3276
+ container_name: ${workspace.name}-localstack
3277
+ restart: unless-stopped
3278
+ environment:
3279
+ SERVICES: sns,sqs
3280
+ AWS_DEFAULT_REGION: \${AWS_REGION:-us-east-1}
3281
+ AWS_ACCESS_KEY_ID: \${AWS_ACCESS_KEY_ID:-localstack}
3282
+ AWS_SECRET_ACCESS_KEY: \${AWS_SECRET_ACCESS_KEY:-localstack}
3283
+ ports:
3284
+ - "\${LOCALSTACK_PORT:-4566}:4566"
3285
+ volumes:
3286
+ - localstack_data:/var/lib/localstack
3287
+ healthcheck:
3288
+ test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
3289
+ interval: 10s
3290
+ timeout: 5s
3291
+ retries: 5
3292
+ networks:
3293
+ - workspace-network
3294
+ `;
3295
+ if (hasRabbitMQ) yaml += `
3296
+ rabbitmq:
3297
+ image: rabbitmq:3-management-alpine
3298
+ container_name: ${workspace.name}-rabbitmq
3299
+ restart: unless-stopped
3300
+ environment:
3301
+ RABBITMQ_DEFAULT_USER: \${RABBITMQ_USER:-guest}
3302
+ RABBITMQ_DEFAULT_PASS: \${RABBITMQ_PASSWORD:-guest}
3303
+ ports:
3304
+ - "\${RABBITMQ_HOST_PORT:-5672}:5672"
3305
+ - "\${RABBITMQ_MGMT_HOST_PORT:-15672}:15672"
3306
+ volumes:
3307
+ - rabbitmq_data:/var/lib/rabbitmq
3308
+ healthcheck:
3309
+ test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
3310
+ interval: 10s
3311
+ timeout: 5s
3312
+ retries: 5
3313
+ networks:
3314
+ - workspace-network
3236
3315
  `;
3237
3316
  yaml += `
3238
3317
  volumes:
@@ -3242,6 +3321,10 @@ volumes:
3242
3321
  if (hasRedis) yaml += ` redis_data:
3243
3322
  `;
3244
3323
  if (hasMinio) yaml += ` minio_data:
3324
+ `;
3325
+ if (hasLocalStack) yaml += ` localstack_data:
3326
+ `;
3327
+ if (hasRabbitMQ) yaml += ` rabbitmq_data:
3245
3328
  `;
3246
3329
  yaml += `
3247
3330
  networks:
@@ -3277,7 +3360,7 @@ function getInfraServiceImage(serviceName, config$1) {
3277
3360
  * Generate a service definition for an app.
3278
3361
  */
3279
3362
  function generateAppService(appName, app, allApps, options) {
3280
- const { registry, projectName, hasPostgres, hasRedis, hasMinio, hasMail } = options;
3363
+ const { registry, projectName, hasPostgres, hasRedis, hasMinio, hasMail, eventsBackend } = options;
3281
3364
  const imageRef = registry ? `\${REGISTRY:-${registry}}/` : "";
3282
3365
  const healthCheckPath = app.type === "frontend" ? "/" : "/health";
3283
3366
  const healthCheckCmd = app.type === "frontend" ? `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}/"]` : `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}${healthCheckPath}"]`;
@@ -3319,6 +3402,16 @@ function generateAppService(appName, app, allApps, options) {
3319
3402
  - SMTP_SECURE=\${SMTP_SECURE:-false}
3320
3403
  - MAIL_FROM=\${MAIL_FROM:-noreply@localhost}
3321
3404
  `;
3405
+ if (eventsBackend) {
3406
+ yaml += ` - EVENT_PUBLISHER_CONNECTION_STRING=\${EVENT_PUBLISHER_CONNECTION_STRING}
3407
+ - EVENT_SUBSCRIBER_CONNECTION_STRING=\${EVENT_SUBSCRIBER_CONNECTION_STRING}
3408
+ `;
3409
+ if (eventsBackend === "sns") yaml += ` - AWS_ACCESS_KEY_ID=\${AWS_ACCESS_KEY_ID:-localstack}
3410
+ - AWS_SECRET_ACCESS_KEY=\${AWS_SECRET_ACCESS_KEY:-localstack}
3411
+ - AWS_REGION=\${AWS_REGION:-us-east-1}
3412
+ - AWS_ENDPOINT_URL=\${AWS_ENDPOINT_URL:-http://localstack:4566}
3413
+ `;
3414
+ }
3322
3415
  }
3323
3416
  yaml += ` healthcheck:
3324
3417
  test: ${healthCheckCmd}
@@ -3332,6 +3425,8 @@ function generateAppService(appName, app, allApps, options) {
3332
3425
  if (hasRedis) dependencies$1.push("redis");
3333
3426
  if (hasMinio) dependencies$1.push("minio");
3334
3427
  if (hasMail) dependencies$1.push("mailpit");
3428
+ if (eventsBackend === "sns") dependencies$1.push("localstack");
3429
+ if (eventsBackend === "rabbitmq") dependencies$1.push("rabbitmq");
3335
3430
  }
3336
3431
  if (dependencies$1.length > 0) {
3337
3432
  yaml += ` depends_on:
@@ -6376,7 +6471,7 @@ async function deployCommand(options) {
6376
6471
  dokployConfig = setupResult.config;
6377
6472
  finalRegistry = dokployConfig.registry ?? dockerConfig.registry;
6378
6473
  if (setupResult.serviceUrls) {
6379
- const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await import("./storage-CauTheT9.mjs");
6474
+ const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await import("./storage-Cs4WBsc4.mjs");
6380
6475
  let secrets = await readStageSecrets$1(stage);
6381
6476
  if (!secrets) {
6382
6477
  logger$3.log(` Creating secrets file for stage "${stage}"...`);
@@ -7145,11 +7240,11 @@ function generateDockerFiles(options, template, dbApps) {
7145
7240
  if (isFullstack && dbApps?.length) {
7146
7241
  files.push({
7147
7242
  path: "docker/postgres/init.sh",
7148
- content: generatePostgresInitScript(dbApps)
7243
+ content: generatePostgresInitScript(dbApps, options.services?.events)
7149
7244
  });
7150
7245
  files.push({
7151
7246
  path: "docker/.env",
7152
- content: generateDockerEnv(dbApps)
7247
+ content: generateDockerEnv(dbApps, options.services?.events)
7153
7248
  });
7154
7249
  }
7155
7250
  }
@@ -7249,6 +7344,47 @@ function generateDockerFiles(options, template, dbApps) {
7249
7344
  retries: 5`);
7250
7345
  volumes.push(" minio_data:");
7251
7346
  }
7347
+ if (options.services?.events === "sns") {
7348
+ services.push(` localstack:
7349
+ image: localstack/localstack:latest
7350
+ container_name: ${options.name}-localstack
7351
+ restart: unless-stopped
7352
+ environment:
7353
+ SERVICES: sns,sqs
7354
+ AWS_DEFAULT_REGION: \${AWS_REGION:-us-east-1}
7355
+ AWS_ACCESS_KEY_ID: \${AWS_ACCESS_KEY_ID:-localstack}
7356
+ AWS_SECRET_ACCESS_KEY: \${AWS_SECRET_ACCESS_KEY:-localstack}
7357
+ ports:
7358
+ - '\${LOCALSTACK_PORT:-4566}:4566'
7359
+ volumes:
7360
+ - localstack_data:/var/lib/localstack
7361
+ healthcheck:
7362
+ test: ['CMD', 'curl', '-f', 'http://localhost:4566/_localstack/health']
7363
+ interval: 10s
7364
+ timeout: 5s
7365
+ retries: 5`);
7366
+ volumes.push(" localstack_data:");
7367
+ }
7368
+ if (options.services?.events === "rabbitmq" && !hasWorker) {
7369
+ services.push(` rabbitmq:
7370
+ image: rabbitmq:3-management-alpine
7371
+ container_name: ${options.name}-rabbitmq
7372
+ restart: unless-stopped
7373
+ ports:
7374
+ - '\${RABBITMQ_HOST_PORT:-5672}:5672'
7375
+ - '\${RABBITMQ_MGMT_HOST_PORT:-15672}:15672'
7376
+ environment:
7377
+ RABBITMQ_DEFAULT_USER: guest
7378
+ RABBITMQ_DEFAULT_PASS: guest
7379
+ volumes:
7380
+ - rabbitmq_data:/var/lib/rabbitmq
7381
+ healthcheck:
7382
+ test: ['CMD', 'rabbitmq-diagnostics', 'check_running']
7383
+ interval: 10s
7384
+ timeout: 5s
7385
+ retries: 5`);
7386
+ if (!volumes.includes(" rabbitmq_data:")) volumes.push(" rabbitmq_data:");
7387
+ }
7252
7388
  let dockerCompose = `# Use "gkm dev" or "gkm test" to start services.
7253
7389
  # Running "docker compose up" directly will not inject secrets or resolve ports.
7254
7390
  services:
@@ -7267,11 +7403,12 @@ ${volumes.join("\n")}
7267
7403
  /**
7268
7404
  * Generate .env file for docker-compose with database passwords
7269
7405
  */
7270
- function generateDockerEnv(apps) {
7406
+ function generateDockerEnv(apps, eventsBackend) {
7271
7407
  const envVars = apps.map((app) => {
7272
7408
  const envVar = `${app.name.toUpperCase()}_DB_PASSWORD`;
7273
7409
  return `${envVar}=${app.password}`;
7274
7410
  });
7411
+ if (eventsBackend === "pgboss") envVars.push(`PGBOSS_DB_PASSWORD=pgboss-dev-password`);
7275
7412
  return `# Auto-generated docker environment file
7276
7413
  # Contains database passwords for docker-compose postgres init
7277
7414
  # This file is gitignored - do not commit to version control
@@ -7279,33 +7416,50 @@ ${envVars.join("\n")}
7279
7416
  `;
7280
7417
  }
7281
7418
  /**
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)
7419
+ * Generate PostgreSQL init shell script that creates per-app users with separate schemas.
7420
+ * Uses idempotent DO blocks so the script can be re-run safely.
7284
7421
  * - api user: uses public schema
7285
7422
  * - auth user: uses auth schema with search_path=auth
7423
+ * - pgboss user: uses pgboss schema (when events === 'pgboss')
7286
7424
  */
7287
- function generatePostgresInitScript(apps) {
7425
+ function generatePostgresInitScript(apps, eventsBackend) {
7288
7426
  const userCreations = apps.map((app) => {
7289
7427
  const userName = app.name.replace(/-/g, "_");
7290
7428
  const envVar = `${app.name.toUpperCase()}_DB_PASSWORD`;
7291
7429
  const isApi = app.name === "api";
7292
7430
  const schemaName = isApi ? "public" : userName;
7293
7431
  if (isApi) return `
7294
- # Create ${app.name} user (uses public schema)
7432
+ # Create ${app.name} user (uses public schema) - idempotent
7295
7433
  echo "Creating user ${userName}..."
7296
7434
  psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
7297
- CREATE USER ${userName} WITH PASSWORD '$${envVar}';
7435
+ DO \\$\\$
7436
+ BEGIN
7437
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${userName}') THEN
7438
+ CREATE USER ${userName} WITH PASSWORD '$${envVar}';
7439
+ ELSE
7440
+ ALTER USER ${userName} WITH PASSWORD '$${envVar}';
7441
+ END IF;
7442
+ END
7443
+ \\$\\$;
7298
7444
  GRANT ALL ON SCHEMA public TO ${userName};
7299
7445
  ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ${userName};
7300
7446
  ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ${userName};
7301
7447
  EOSQL
7302
7448
  `;
7303
7449
  return `
7304
- # Create ${app.name} user with dedicated schema
7450
+ # Create ${app.name} user with dedicated schema - idempotent
7305
7451
  echo "Creating user ${userName} with schema ${schemaName}..."
7306
7452
  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};
7453
+ DO \\$\\$
7454
+ BEGIN
7455
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${userName}') THEN
7456
+ CREATE USER ${userName} WITH PASSWORD '$${envVar}';
7457
+ ELSE
7458
+ ALTER USER ${userName} WITH PASSWORD '$${envVar}';
7459
+ END IF;
7460
+ END
7461
+ \\$\\$;
7462
+ CREATE SCHEMA IF NOT EXISTS ${schemaName} AUTHORIZATION ${userName};
7309
7463
  ALTER USER ${userName} SET search_path TO ${schemaName};
7310
7464
  GRANT USAGE ON SCHEMA ${schemaName} TO ${userName};
7311
7465
  GRANT ALL ON ALL TABLES IN SCHEMA ${schemaName} TO ${userName};
@@ -7315,14 +7469,46 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E
7315
7469
  EOSQL
7316
7470
  `;
7317
7471
  });
7472
+ let pgbossBlock = "";
7473
+ if (eventsBackend === "pgboss") pgbossBlock = `
7474
+ # Create pgboss user with dedicated schema - idempotent
7475
+ echo "Creating pgboss user and schema..."
7476
+ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
7477
+ DO \\$\\$
7478
+ BEGIN
7479
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'pgboss') THEN
7480
+ CREATE USER pgboss WITH PASSWORD '$PGBOSS_DB_PASSWORD';
7481
+ ELSE
7482
+ ALTER USER pgboss WITH PASSWORD '$PGBOSS_DB_PASSWORD';
7483
+ END IF;
7484
+ END
7485
+ \\$\\$;
7486
+ CREATE SCHEMA IF NOT EXISTS pgboss AUTHORIZATION pgboss;
7487
+ ALTER USER pgboss SET search_path TO pgboss;
7488
+ GRANT USAGE ON SCHEMA pgboss TO pgboss;
7489
+ GRANT ALL ON ALL TABLES IN SCHEMA pgboss TO pgboss;
7490
+ GRANT ALL ON ALL SEQUENCES IN SCHEMA pgboss TO pgboss;
7491
+ ALTER DEFAULT PRIVILEGES IN SCHEMA pgboss GRANT ALL ON TABLES TO pgboss;
7492
+ ALTER DEFAULT PRIVILEGES IN SCHEMA pgboss GRANT ALL ON SEQUENCES TO pgboss;
7493
+ EOSQL
7494
+ `;
7495
+ const extensions = `
7496
+ # Create extensions
7497
+ echo "Creating extensions..."
7498
+ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
7499
+ CREATE EXTENSION IF NOT EXISTS pg_trgm;
7500
+ CREATE EXTENSION IF NOT EXISTS citext;
7501
+ EOSQL
7502
+ `;
7318
7503
  return `#!/bin/bash
7319
7504
  set -e
7320
7505
 
7321
- # Auto-generated PostgreSQL init script
7506
+ # Auto-generated PostgreSQL init script (idempotent - safe to re-run)
7322
7507
  # Creates per-app users with separate schemas in a single database
7323
7508
  # - api: uses public schema
7324
- # - auth: uses auth schema (search_path=auth)
7325
- ${userCreations.join("\n")}
7509
+ # - auth: uses auth schema (search_path=auth)${eventsBackend === "pgboss" ? "\n# - pgboss: uses pgboss schema for event processing" : ""}
7510
+ ${extensions}
7511
+ ${userCreations.join("\n")}${pgbossBlock}
7326
7512
  echo "Database initialization complete!"
7327
7513
  `;
7328
7514
  }
@@ -8814,6 +9000,31 @@ const servicesChoices = [
8814
9000
  }
8815
9001
  ];
8816
9002
  /**
9003
+ * Event backend choices for prompts
9004
+ */
9005
+ const eventsBackendChoices = [
9006
+ {
9007
+ title: "pg-boss",
9008
+ value: "pgboss",
9009
+ description: "PostgreSQL-based job queue (reuses postgres, no extra container)"
9010
+ },
9011
+ {
9012
+ title: "SNS/SQS",
9013
+ value: "sns",
9014
+ description: "AWS SNS+SQS via LocalStack for local dev"
9015
+ },
9016
+ {
9017
+ title: "RabbitMQ",
9018
+ value: "rabbitmq",
9019
+ description: "AMQP message broker"
9020
+ },
9021
+ {
9022
+ title: "None",
9023
+ value: void 0,
9024
+ description: "Skip event backend"
9025
+ }
9026
+ ];
9027
+ /**
8817
9028
  * Get a template by name
8818
9029
  */
8819
9030
  function getTemplate(name$1) {
@@ -10921,6 +11132,13 @@ async function initCommand(projectName, options = {}) {
10921
11132
  })),
10922
11133
  hint: "- Space to select. Return to submit"
10923
11134
  },
11135
+ {
11136
+ type: options.yes ? null : "select",
11137
+ name: "eventsBackend",
11138
+ message: "Event backend:",
11139
+ choices: eventsBackendChoices,
11140
+ initial: 0
11141
+ },
10924
11142
  {
10925
11143
  type: options.yes ? null : "select",
10926
11144
  name: "packageManager",
@@ -10983,12 +11201,15 @@ async function initCommand(projectName, options = {}) {
10983
11201
  "mail",
10984
11202
  "storage"
10985
11203
  ] : answers.services || [];
11204
+ const eventsBackend = options.yes ? isFullstack ? "pgboss" : void 0 : answers.eventsBackend;
10986
11205
  const services = {
10987
11206
  db: servicesArray.includes("db"),
10988
11207
  cache: servicesArray.includes("cache"),
10989
11208
  mail: servicesArray.includes("mail"),
10990
- storage: servicesArray.includes("storage")
11209
+ storage: servicesArray.includes("storage"),
11210
+ events: eventsBackend
10991
11211
  };
11212
+ if (services.events === "pgboss") services.db = true;
10992
11213
  const pkgManager = options.pm ? options.pm : options.yes ? "pnpm" : answers.packageManager ?? detectedPkgManager;
10993
11214
  const deployTarget = options.yes ? "dokploy" : answers.deployTarget ?? "dokploy";
10994
11215
  const database = services.db;
@@ -11071,7 +11292,12 @@ async function initCommand(projectName, options = {}) {
11071
11292
  if (services.cache) secretServices.push("redis");
11072
11293
  if (services.storage) secretServices.push("minio");
11073
11294
  if (services.mail) secretServices.push("mailpit");
11074
- const devSecrets = createStageSecrets("development", secretServices, { projectName: name$1 });
11295
+ if (services.events === "sns") secretServices.push("localstack");
11296
+ if (services.events === "rabbitmq") secretServices.push("rabbitmq");
11297
+ const devSecrets = createStageSecrets("development", secretServices, {
11298
+ projectName: name$1,
11299
+ eventsBackend: services.events
11300
+ });
11075
11301
  const customSecrets = {
11076
11302
  NODE_ENV: "development",
11077
11303
  PORT: "3000",
@@ -11511,10 +11737,55 @@ function reconcileSecrets(secrets, workspace) {
11511
11737
  [name$1]: creds
11512
11738
  }
11513
11739
  };
11514
- result.urls = generateConnectionUrls(result.services);
11740
+ result.urls = generateConnectionUrls(result.services, result.eventsBackend);
11515
11741
  logger$1.log(` 🔄 Adding missing service credentials: ${name$1}`);
11516
11742
  changed = true;
11517
11743
  }
11744
+ const eventsBackend = workspace.services.events;
11745
+ if (eventsBackend && result.eventsBackend !== eventsBackend) {
11746
+ result.eventsBackend = eventsBackend;
11747
+ if (eventsBackend === "pgboss" && !result.services.pgboss) {
11748
+ result = {
11749
+ ...result,
11750
+ services: {
11751
+ ...result.services,
11752
+ pgboss: {
11753
+ host: result.services.postgres?.host ?? "localhost",
11754
+ port: result.services.postgres?.port ?? 5432,
11755
+ username: "pgboss",
11756
+ password: generateSecurePassword(),
11757
+ database: result.services.postgres?.database ?? "app"
11758
+ }
11759
+ }
11760
+ };
11761
+ logger$1.log(" 🔄 Adding missing service credentials: pgboss");
11762
+ changed = true;
11763
+ }
11764
+ if (eventsBackend === "sns" && !result.services.localstack) {
11765
+ result = {
11766
+ ...result,
11767
+ services: {
11768
+ ...result.services,
11769
+ localstack: generateLocalStackCredentials()
11770
+ }
11771
+ };
11772
+ logger$1.log(" 🔄 Adding missing service credentials: localstack");
11773
+ changed = true;
11774
+ }
11775
+ if (eventsBackend === "rabbitmq" && !result.services.rabbitmq) {
11776
+ result = {
11777
+ ...result,
11778
+ services: {
11779
+ ...result.services,
11780
+ rabbitmq: generateServiceCredentials("rabbitmq")
11781
+ }
11782
+ };
11783
+ logger$1.log(" 🔄 Adding missing service credentials: rabbitmq");
11784
+ changed = true;
11785
+ }
11786
+ result.urls = generateConnectionUrls(result.services, eventsBackend);
11787
+ changed = true;
11788
+ }
11518
11789
  const isMultiApp = Object.keys(workspace.apps).length > 1;
11519
11790
  if (isMultiApp) {
11520
11791
  const expected = generateFullstackCustomSecrets(workspace);
@@ -11547,7 +11818,12 @@ async function generateFreshSecrets(stage, workspace, options) {
11547
11818
  if (workspace.services.cache) serviceNames.push("redis");
11548
11819
  if (workspace.services.storage) serviceNames.push("minio");
11549
11820
  if (workspace.services.mail) serviceNames.push("mailpit");
11550
- const secrets = createStageSecrets(stage, serviceNames, { projectName: workspace.name });
11821
+ if (workspace.services.events === "sns") serviceNames.push("localstack");
11822
+ if (workspace.services.events === "rabbitmq") serviceNames.push("rabbitmq");
11823
+ const secrets = createStageSecrets(stage, serviceNames, {
11824
+ projectName: workspace.name,
11825
+ eventsBackend: workspace.services.events
11826
+ });
11551
11827
  const isMultiApp = Object.keys(workspace.apps).length > 1;
11552
11828
  if (isMultiApp) {
11553
11829
  const customSecrets = generateFullstackCustomSecrets(workspace);
@@ -12097,9 +12373,9 @@ program.command("secrets:push").description("Push secrets to remote provider (SS
12097
12373
  const globalOptions = program.opts();
12098
12374
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
12099
12375
  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");
12376
+ const { pushSecrets: pushSecrets$1 } = await import("./sync-COnAugP-.mjs");
12377
+ const { reconcileMissingSecrets } = await import("./reconcile-BLh6rswz.mjs");
12378
+ const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-Cs4WBsc4.mjs");
12103
12379
  const { workspace } = await loadWorkspaceConfig$1();
12104
12380
  const secrets = await readStageSecrets$1(options.stage, workspace.root);
12105
12381
  if (secrets) {
@@ -12122,9 +12398,9 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
12122
12398
  const globalOptions = program.opts();
12123
12399
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
12124
12400
  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");
12401
+ const { pullSecrets: pullSecrets$1 } = await import("./sync-COnAugP-.mjs");
12402
+ const { writeStageSecrets: writeStageSecrets$1 } = await import("./storage-Cs4WBsc4.mjs");
12403
+ const { reconcileMissingSecrets } = await import("./reconcile-BLh6rswz.mjs");
12128
12404
  const { workspace } = await loadWorkspaceConfig$1();
12129
12405
  let secrets = await pullSecrets$1(options.stage, workspace);
12130
12406
  if (!secrets) {
@@ -12149,8 +12425,8 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
12149
12425
  const globalOptions = program.opts();
12150
12426
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
12151
12427
  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");
12428
+ const { reconcileMissingSecrets } = await import("./reconcile-BLh6rswz.mjs");
12429
+ const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-Cs4WBsc4.mjs");
12154
12430
  const { workspace } = await loadWorkspaceConfig$1();
12155
12431
  const secrets = await readStageSecrets$1(options.stage, workspace.root);
12156
12432
  if (!secrets) {