@geekmidas/cli 10.0.0-alpha.23 → 10.0.0-alpha.25

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 (47) hide show
  1. package/bin/adjacent-tsconfig.mjs +46 -8
  2. package/bin/gkm.mjs +2 -2
  3. package/dist/{config-DdKkoUE1.mjs → config-DBGpRTUy.mjs} +2 -2
  4. package/dist/{config-DdKkoUE1.mjs.map → config-DBGpRTUy.mjs.map} +1 -1
  5. package/dist/{config-DSXI480r.cjs → config-Dn9DW2Fz.cjs} +2 -2
  6. package/dist/{config-DSXI480r.cjs.map → config-Dn9DW2Fz.cjs.map} +1 -1
  7. package/dist/config.cjs +1 -1
  8. package/dist/config.mjs +1 -1
  9. package/dist/{credentials-D9TKJJZc.mjs → credentials-BpoOUa74.mjs} +17 -6
  10. package/dist/credentials-BpoOUa74.mjs.map +1 -0
  11. package/dist/{credentials-COrSpZfF.cjs → credentials-Dbyccl3A.cjs} +17 -6
  12. package/dist/credentials-Dbyccl3A.cjs.map +1 -0
  13. package/dist/deploy/sniffer-routes-worker.cjs +1 -1
  14. package/dist/deploy/sniffer-routes-worker.mjs +1 -1
  15. package/dist/{discover-CK-n3w6b.mjs → discover-0fhCbnk0.mjs} +3 -2
  16. package/dist/{discover-CK-n3w6b.mjs.map → discover-0fhCbnk0.mjs.map} +1 -1
  17. package/dist/{discover-BERGFg8v.cjs → discover-B_HtYJCr.cjs} +3 -2
  18. package/dist/{discover-BERGFg8v.cjs.map → discover-B_HtYJCr.cjs.map} +1 -1
  19. package/dist/{github-B4OQQZBV.mjs → github-BOaa3kQR.mjs} +3 -3
  20. package/dist/{github-B4OQQZBV.mjs.map → github-BOaa3kQR.mjs.map} +1 -1
  21. package/dist/{github-BpsnSDw2.cjs → github-CUjW3G91.cjs} +3 -3
  22. package/dist/{github-BpsnSDw2.cjs.map → github-CUjW3G91.cjs.map} +1 -1
  23. package/dist/index.cjs +1865 -1759
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.mjs +1865 -1759
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/{openapi-CN69hrf0.cjs → openapi-BAKQLho-.cjs} +16 -6
  28. package/dist/openapi-BAKQLho-.cjs.map +1 -0
  29. package/dist/{openapi-B5PSuLym.mjs → openapi-DHQlstv6.mjs} +16 -6
  30. package/dist/openapi-DHQlstv6.mjs.map +1 -0
  31. package/dist/openapi.cjs +1 -1
  32. package/dist/openapi.mjs +1 -1
  33. package/dist/reconcile/public.cjs +1 -1
  34. package/dist/reconcile/public.d.cts +7 -0
  35. package/dist/reconcile/public.d.cts.map +1 -1
  36. package/dist/reconcile/public.d.mts +7 -0
  37. package/dist/reconcile/public.d.mts.map +1 -1
  38. package/dist/reconcile/public.mjs +1 -1
  39. package/dist/{transfer-Bk5YlRyB.mjs → transfer-B88fTvnh.mjs} +2 -2
  40. package/dist/{transfer-Bk5YlRyB.mjs.map → transfer-B88fTvnh.mjs.map} +1 -1
  41. package/dist/{transfer-BZbyaV3I.cjs → transfer-D59M4aaR.cjs} +2 -2
  42. package/dist/{transfer-BZbyaV3I.cjs.map → transfer-D59M4aaR.cjs.map} +1 -1
  43. package/package.json +17 -12
  44. package/dist/credentials-COrSpZfF.cjs.map +0 -1
  45. package/dist/credentials-D9TKJJZc.mjs.map +0 -1
  46. package/dist/openapi-B5PSuLym.mjs.map +0 -1
  47. package/dist/openapi-CN69hrf0.cjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
4
- const require_discover = require("./discover-BERGFg8v.cjs");
5
- const require_config = require("./config-DSXI480r.cjs");
4
+ const require_discover = require("./discover-B_HtYJCr.cjs");
5
+ const require_config = require("./config-Dn9DW2Fz.cjs");
6
6
  const require_workspace = require("./workspace-CRD9YinV.cjs");
7
- const require_credentials = require("./credentials-COrSpZfF.cjs");
7
+ const require_credentials = require("./credentials-Dbyccl3A.cjs");
8
8
  const require_generator = require("./generator-Wdfp0jCS.cjs");
9
9
  const require_storage = require("./storage-DgypfSD6.cjs");
10
- const require_openapi = require("./openapi-CN69hrf0.cjs");
10
+ const require_openapi = require("./openapi-BAKQLho-.cjs");
11
11
  const require_emit = require("./emit-BdoPFjZf.cjs");
12
12
  const require_reconcile = require("./reconcile-hx_vBvux.cjs");
13
- const require_transfer = require("./transfer-BZbyaV3I.cjs");
13
+ const require_transfer = require("./transfer-D59M4aaR.cjs");
14
14
  let commander = require("commander");
15
15
  let node_process = require("node:process");
16
16
  let node_readline_promises = require("node:readline/promises");
@@ -31,12 +31,12 @@ let _geekmidas_db_pg_roles = require("@geekmidas/db/pg/roles");
31
31
  let pg = require("pg");
32
32
  let node_util = require("node:util");
33
33
  let _geekmidas_cache_postgres = require("@geekmidas/cache/postgres");
34
+ let node_url = require("node:url");
34
35
  let _geekmidas_constructs_crons = require("@geekmidas/constructs/crons");
35
36
  let _geekmidas_constructs_functions = require("@geekmidas/constructs/functions");
36
37
  let _geekmidas_constructs_queue = require("@geekmidas/constructs/queue");
37
38
  let _geekmidas_constructs_subscribers = require("@geekmidas/constructs/subscribers");
38
39
  let _geekmidas_constructs_topic = require("@geekmidas/constructs/topic");
39
- let node_url = require("node:url");
40
40
  let node_dns_promises = require("node:dns/promises");
41
41
  let node_module = require("node:module");
42
42
  let prompts = require("prompts");
@@ -44,7 +44,7 @@ prompts = require_rolldown_runtime.__toESM(prompts, 1);
44
44
  let _geekmidas_envkit = require("@geekmidas/envkit");
45
45
  let _geekmidas_services = require("@geekmidas/services");
46
46
  //#region package.json
47
- var version = "10.0.0-alpha.23";
47
+ var version = "10.0.0-alpha.25";
48
48
  //#endregion
49
49
  //#region src/auth/credentials.ts
50
50
  /**
@@ -399,1774 +399,1774 @@ function isEnabled(config) {
399
399
  return config.enabled !== false;
400
400
  }
401
401
  //#endregion
402
- //#region src/generators/CronGenerator.ts
403
- var CronGenerator = class extends require_discover.ConstructGenerator {
404
- async build(context, constructs, outputDir, options) {
405
- const provider = options?.provider || "aws-lambda";
406
- const logger = console;
407
- const cronInfos = [];
408
- if (provider === "server") {
409
- await this.generateServerCronsFile(outputDir, constructs);
410
- logger.log(`Generated server crons file with ${constructs.length} crons`);
411
- return cronInfos;
412
- }
413
- if (constructs.length === 0 || provider !== "aws-lambda") return cronInfos;
414
- const cronsDir = (0, node_path.join)(outputDir, "crons");
415
- await (0, node_fs_promises.mkdir)(cronsDir, { recursive: true });
416
- for (const { key, construct, path } of constructs) {
417
- const handlerFile = await this.generateCronHandler(cronsDir, path.relative, key, context, construct.owner);
418
- cronInfos.push({
419
- name: key,
420
- handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
421
- schedule: construct.schedule || "rate(1 hour)",
422
- timeout: construct.timeout,
423
- memorySize: construct.memorySize,
424
- environment: await construct.getEnvironment({ markOptional: context.markOptional }),
425
- dependencies: construct.constructs
426
- });
427
- logger.log(`Generated cron handler: ${key}`);
428
- }
429
- return cronInfos;
430
- }
431
- isConstruct(value) {
432
- return _geekmidas_constructs_crons.Cron.isCron(value);
433
- }
434
- /**
435
- * The crons file a server entry imports.
436
- *
437
- * Shaped after `generateServerSubscribersFile`: one module, every cron, a
438
- * `setupCrons` the generated entry calls beside `setupSubscribers`.
439
- *
440
- * It picks its scheduler from what the process can reach. With a Postgres it
441
- * uses pg-boss, whose schedules live in the database — so a deployment
442
- * running four replicas fires each job once, which is the thing a timer in
443
- * every process gets wrong and does not report. With no database it falls
444
- * back to timers and says plainly that it is doing so.
445
- */
446
- async generateServerCronsFile(outputDir, crons) {
447
- await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
448
- const cronsPath = (0, node_path.join)(outputDir, "crons.ts");
449
- const importsByFile = /* @__PURE__ */ new Map();
450
- for (const { path, key } of crons) {
451
- const importPath = (0, node_path.relative)((0, node_path.dirname)(cronsPath), path.relative).replace(/\.ts$/, ".js");
452
- if (!importsByFile.has(importPath)) importsByFile.set(importPath, []);
453
- importsByFile.get(importPath)?.push(key);
454
- }
455
- const content = `/**
456
- * Generated cron setup.
457
- *
458
- * Schedules every cron this app declares, in the process that serves it.
459
- *
460
- * The schedule lives in Postgres, through pg-boss, in the database the worker
461
- * named with \`.database(db)\`. That is what makes running several replicas
462
- * safe: each job fires once, where a timer in every process fires it once per
463
- * replica and reports nothing.
464
- *
465
- * No connection string appears here. The construct that owns the database is
466
- * the only thing that knows its key, and it is resolved through service
467
- * discovery like any other dependency.
468
- */
469
- import type { EnvironmentParser } from '@geekmidas/envkit';
470
- import type { Logger } from '@geekmidas/logger';
471
- import { toCronExpression } from '@geekmidas/constructs/crons';
472
- import { ServiceDiscovery } from '@geekmidas/services';
473
- ${Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n")}
474
-
475
- const crons = [
476
- ${crons.map(({ key }) => ` { name: '${key}', cron: ${key} }`).join(",\n")}
477
- ];
478
-
479
- type Stoppable = { stop: () => Promise<void> | void };
480
- const running: Stoppable[] = [];
481
-
482
- export async function setupCrons(
483
- envParser: EnvironmentParser<any>,
484
- logger: Logger,
485
- ): Promise<void> {
486
- if (crons.length === 0) {
487
- return;
488
- }
489
-
490
- const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
491
-
492
- // Resolved once per cron rather than per firing: registering services on
493
- // every tick would reconnect a database every minute.
494
- const prepared = [];
495
- for (const { name, cron } of crons) {
496
- try {
497
- const schedule = toCronExpression(cron.schedule);
498
- const services = cron.services.length > 0
499
- ? await serviceDiscovery.register(cron.services)
500
- : {};
501
-
502
- prepared.push({ name, cron, schedule, services });
503
- } catch (error) {
504
- // An unrepresentable schedule is a build-time mistake, and failing the
505
- // process for it would take the HTTP server down with it.
506
- logger.error({ error, cron: name }, 'Cron has no runnable schedule, skipping');
507
- }
508
- }
509
-
510
- const run = async (entry: (typeof prepared)[number]) => {
511
- try {
512
- await entry.cron.handler({
513
- input: undefined,
514
- services: entry.services,
515
- logger: entry.cron.logger,
516
- });
517
- } catch (error) {
518
- logger.error({ error, cron: entry.name }, 'Cron failed');
519
- }
520
- };
521
-
522
- // Declared once on the worker, carried onto every cron it built — so this
523
- // reads it from any of them rather than from config or the environment.
524
- const scheduleStore = prepared.find(({ cron }) => cron.scheduleStore)?.cron
525
- .scheduleStore;
526
-
527
- if (!scheduleStore) {
528
- // Declared, not discovered. A worker says where its schedules live with
529
- // \`.database(db)\`; inferring one from whatever database happened to be
530
- // around would move the schedules the day an app declared a second.
531
- logger.error(
532
- { crons: prepared.map(({ name }) => name) },
533
- 'These crons have nowhere to keep their schedule. A server fires its own ' +
534
- 'crons, and a process scheduling in memory fires each job once per ' +
535
- 'replica — so the schedule lives in Postgres. Say where with ' +
536
- '\`new Worker(…).database(db)\`.',
537
- );
538
- return;
539
- }
540
-
541
- const [store] = await serviceDiscovery
542
- .register([scheduleStore])
543
- .then((s) => Object.values(s));
544
-
545
- const { PgBoss } = await import('pg-boss');
546
- // The pool the construct already opened — no connection string is read here,
547
- // and none is named anywhere outside the construct that owns it.
548
- const boss = new PgBoss({ db: store as never });
549
- await boss.start();
550
-
551
- // What this app declares now. Anything else under the prefix belonged to a
552
- // cron since renamed or deleted, and would keep firing at a handler that has
553
- // gone.
554
- //
555
- // The prefix is \`cron.\` rather than \`cron:\`: pg-boss accepts
556
- // alphanumerics, underscores, hyphens, periods and slashes in a queue name,
557
- // and rejects a colon outright.
558
- const declared = new Set(prepared.map(({ name }) => \`cron.\${name}\`));
559
- for (const existing of await boss.getSchedules()) {
560
- if (existing.name.startsWith('cron.') && !declared.has(existing.name)) {
561
- await boss.unschedule(existing.name);
562
- logger.info({ cron: existing.name }, 'Removed a schedule nothing declares');
563
- }
564
- }
565
-
566
- for (const entry of prepared) {
567
- const queue = \`cron.\${entry.name}\`;
568
- await boss.createQueue(queue);
569
- // UTC, so this agrees with the schedule a deploy target would have made.
570
- await boss.schedule(queue, entry.schedule, undefined, { tz: 'UTC' });
571
- await boss.work(queue, () => run(entry));
572
-
573
- logger.info({ cron: entry.name, schedule: entry.schedule }, 'Cron scheduled');
574
- }
575
-
576
- running.push({ stop: () => boss.stop() });
577
- logger.info({ count: prepared.length }, 'Crons scheduled');
578
- }
579
-
580
- export async function stopCrons(): Promise<void> {
581
- await Promise.all(running.map((r) => r.stop()));
582
- running.length = 0;
583
- }
584
- `;
585
- await (0, node_fs_promises.writeFile)(cronsPath, content);
586
- return cronsPath;
587
- }
588
- async generateCronHandler(outputDir, sourceFile, exportName, context, owner) {
589
- const handlerFileName = `${exportName}.ts`;
590
- const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
591
- const importPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile).replace(/\.ts$/, ".js");
592
- const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
593
- const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/aws';
594
- import { ${exportName} } from '${importPath}';
595
- ${runtime.imports}
596
- ${runtime.bindings}
597
-
598
- const adapter = new AWSScheduledFunction(envParser, ${exportName});
599
-
600
- export const handler = adapter.handler;
601
- `;
602
- await (0, node_fs_promises.writeFile)(handlerPath, content);
603
- return handlerPath;
604
- }
605
- };
606
- //#endregion
607
- //#region src/generators/drivers.ts
608
- /**
609
- * Driver registration for generated entry points.
610
- *
611
- * A construct declares that it needs object storage and is handed one URL; the
612
- * scheme in that URL picks the driver that builds the client. Which drivers
613
- * exist is therefore the *entry point's* decision, not the construct's and not
614
- * the application's — an app that never talks to S3 should never resolve the AWS
615
- * SDK, and application code that registers a driver has named a provider in the
616
- * one layer this design keeps provider-free.
617
- *
618
- * So the generated entries register: `gkm dev`'s server for local development,
619
- * and each Lambda handler for its own target. Registration is an explicit call
620
- * rather than an import side effect, because a side-effecting module is exactly
621
- * what a bundler is entitled to drop.
622
- */
623
- /** The `s3://` driver, which serves MinIO locally and S3 deployed. */
624
- const S3 = {
625
- imports: `import { registerStorageDriver } from '@geekmidas/storage';\nimport { s3Driver } from '@geekmidas/storage/aws';`,
626
- setup: "registerStorageDriver(s3Driver);"
627
- };
628
- const NONE = {
629
- imports: "",
630
- setup: ""
631
- };
402
+ //#region src/workspace/client-generator.ts
632
403
  /**
633
- * The cache driver for a backend — exactly one, never all three.
634
- *
635
- * Keyed off the *backend* rather than off a dependency, because unlike storage
636
- * the backend already says which driver is right: a project caching in Postgres
637
- * resolves `pg` and never `ioredis`, and one caching in Upstash resolves
638
- * neither. That is the whole reason the drivers live behind separate subpaths.
639
- *
640
- * Both HTTP schemes are registered for Upstash, because the local proxy speaks
641
- * `http://` and Upstash speaks `https://`, and an entry that registered one
642
- * would work in exactly one of the two places.
404
+ * Normalize routes to an array of patterns.
405
+ * Handles string, string[], and PartitionedRoutes (extracts paths).
406
+ * @internal Exported for use in dev command
643
407
  */
644
- const CACHE_DRIVERS = {
645
- upstash: {
646
- imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { upstashCacheDriver, upstashInsecureCacheDriver } from '@geekmidas/cache/upstash';`,
647
- setup: "registerCacheDriver(upstashCacheDriver);\nregisterCacheDriver(upstashInsecureCacheDriver);"
648
- },
649
- elasticache: {
650
- imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { redisCacheDriver, redissCacheDriver } from '@geekmidas/cache/redis';`,
651
- setup: "registerCacheDriver(redisCacheDriver);\nregisterCacheDriver(redissCacheDriver);"
652
- },
653
- db: {
654
- imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { postgresCacheDriver } from '@geekmidas/cache/postgres';`,
655
- setup: "registerCacheDriver(postgresCacheDriver);"
408
+ function normalizeRoutes(routes) {
409
+ if (!routes) return [];
410
+ if (require_discover.isPartitionedRoutes(routes)) return Array.isArray(routes.paths) ? routes.paths : [routes.paths];
411
+ return Array.isArray(routes) ? routes : [routes];
412
+ }
413
+ //#endregion
414
+ //#region src/exec/index.ts
415
+ const logger$12 = console;
416
+ /** `gkm exec` was given nothing to run. */
417
+ var NoCommandSpecified = class extends Error {
418
+ constructor() {
419
+ super("No command specified. Usage: gkm exec -- <command>");
420
+ this.name = "NoCommandSpecified";
656
421
  }
657
422
  };
658
423
  /**
659
- * Everything a generated entry should register: object storage, and the cache.
424
+ * Run a command with secrets injected into Credentials.
425
+ * Uses Node's --import flag to preload a script that populates Credentials
426
+ * before the command loads any modules that depend on them.
660
427
  *
661
- * Merged here rather than threaded separately, because a generated file has one
662
- * import block and one setup block whatever fills them.
428
+ * @example
429
+ * ```bash
430
+ * gkm exec -- npx @better-auth/cli migrate
431
+ * gkm exec -- npx prisma migrate dev
432
+ * ```
663
433
  */
664
- function driversFor(options) {
665
- const backends = options.cache === false || options.cache === void 0 ? [] : typeof options.cache === "string" ? [options.cache] : options.cache;
666
- const parts = [storageDriversFor(options.appRoot), ...[...new Set(backends)].map((backend) => CACHE_DRIVERS[backend] ?? CACHE_DRIVERS[require_discover.DEFAULT_CACHE.aws])].filter((part) => part.imports || part.setup);
667
- if (parts.length === 0) return NONE;
434
+ async function execCommand(commandArgs, options = {}) {
435
+ const cwd = options.cwd ?? process.cwd();
436
+ if (!commandArgs[0]) throw new NoCommandSpecified();
437
+ const defaultEnv = require_credentials.loadEnvFiles(".env");
438
+ if (defaultEnv.loaded.length > 0) logger$12.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
439
+ const { credentials, secretsJsonPath, appName } = await require_credentials.prepareEntryCredentials({
440
+ cwd,
441
+ resolveDockerPorts: "readonly"
442
+ });
443
+ if (appName) logger$12.log(`📦 App: ${appName}`);
444
+ const secretCount = Object.keys(credentials).filter((k) => k !== "PORT").length;
445
+ if (secretCount > 0) logger$12.log(`🔐 Loaded ${secretCount} secret(s)`);
446
+ const preloadDir = (0, node_path.join)(cwd, ".gkm");
447
+ await (0, node_fs_promises.mkdir)(preloadDir, { recursive: true });
448
+ const preloadPath = (0, node_path.join)(preloadDir, "credentials-preload.mjs");
449
+ await require_credentials.createCredentialsPreload(preloadPath, secretsJsonPath);
450
+ const [cmd, ...args] = commandArgs;
451
+ logger$12.log(`🚀 Running: ${[cmd, ...args].join(" ")}`);
452
+ const nodeOptions = [(process.env.NODE_OPTIONS ?? "").replace(/--import[= ]\S+/g, "").trim(), `--import=${preloadPath}`].filter(Boolean).join(" ");
453
+ const child = (0, node_child_process.spawn)(cmd, args, {
454
+ cwd,
455
+ stdio: "inherit",
456
+ env: {
457
+ ...process.env,
458
+ ...credentials,
459
+ NODE_OPTIONS: nodeOptions
460
+ }
461
+ });
462
+ const exitCode = await new Promise((resolve) => {
463
+ child.on("close", (code) => resolve(code ?? 0));
464
+ child.on("error", (error) => {
465
+ logger$12.error(`Failed to run command: ${error.message}`);
466
+ resolve(1);
467
+ });
468
+ });
469
+ if (exitCode !== 0) process.exit(exitCode);
470
+ }
471
+ //#endregion
472
+ //#region src/dev/index.ts
473
+ const logger$11 = console;
474
+ /**
475
+ * Normalize telescope configuration
476
+ * @internal Exported for testing
477
+ */
478
+ function normalizeTelescopeConfig(config) {
479
+ if (config === false) return;
480
+ if (typeof config === "string") {
481
+ const { path: telescopePath, importPattern: telescopeImportPattern } = require_config.parseModuleConfig(config, "telescope");
482
+ return {
483
+ enabled: true,
484
+ telescopePath,
485
+ telescopeImportPattern,
486
+ path: "/__telescope",
487
+ ignore: [],
488
+ recordBody: true,
489
+ maxEntries: 1e3,
490
+ websocket: true
491
+ };
492
+ }
493
+ if (!(config === true || config === void 0 || config.enabled !== false)) return;
494
+ const telescopeConfig = typeof config === "object" ? config : {};
668
495
  return {
669
- imports: parts.map((part) => part.imports).join("\n"),
670
- setup: parts.map((part) => part.setup).join("\n")
496
+ enabled: true,
497
+ path: telescopeConfig.path ?? "/__telescope",
498
+ ignore: telescopeConfig.ignore ?? [],
499
+ recordBody: telescopeConfig.recordBody ?? true,
500
+ maxEntries: telescopeConfig.maxEntries ?? 1e3,
501
+ websocket: telescopeConfig.websocket ?? true
671
502
  };
672
503
  }
673
504
  /**
674
- * Which cache backends an app's entry actually has to speak.
675
- *
676
- * The config answers this for a cache that named nowhere, and the *declaration*
677
- * answers it for one that named a database — `orders.cache('Sessions')` is a
678
- * statement about the application, so a deployment cannot move that cache and
679
- * the entry cannot register a driver for somewhere else.
680
- *
681
- * Reading only the config is how an entry ends up registering the Upstash
682
- * driver for a URL the target composed as `postgres://`, which fails at the
683
- * first request with `UnregisteredCacheScheme` and a stack that points at the
684
- * cache rather than at the config that disagreed.
505
+ * Normalize studio configuration
506
+ * @internal Exported for testing
685
507
  */
686
- function cacheBackendsIn(manifest, configured) {
687
- const caches = Object.values(manifest).filter((d) => d.kind === "cache");
688
- return [...new Set(caches.map((c) => c.of ? "db" : configured))];
508
+ function normalizeStudioConfig(config) {
509
+ if (config === false) return;
510
+ if (typeof config === "string") {
511
+ const { path: studioPath, importPattern: studioImportPattern } = require_config.parseModuleConfig(config, "studio");
512
+ return {
513
+ enabled: true,
514
+ studioPath,
515
+ studioImportPattern,
516
+ path: "/__studio",
517
+ schema: "public"
518
+ };
519
+ }
520
+ if (!(config === true || config === void 0 || config.enabled !== false)) return;
521
+ const studioConfig = typeof config === "object" ? config : {};
522
+ return {
523
+ enabled: true,
524
+ path: studioConfig.path ?? "/__studio",
525
+ schema: studioConfig.schema ?? "public"
526
+ };
689
527
  }
690
528
  /**
691
- * The drivers an app's entry points should register.
692
- *
693
- * Keyed off the dependency rather than off a declared bucket, because the entry
694
- * is generated before discovery has run on the watcher's rebuild path — and an
695
- * app that installed `@geekmidas/storage` has already paid for the SDK it would
696
- * otherwise resolve lazily.
529
+ * Normalize hooks configuration
530
+ * @internal Exported for testing
697
531
  */
698
- function storageDriversFor(appRoot) {
699
- return dependsOnStorage(appRoot) ? S3 : NONE;
532
+ function normalizeHooksConfig(config, cwd = process.cwd()) {
533
+ if (!config?.server) return;
534
+ const serverPath = config.server.endsWith(".ts") ? config.server : `${config.server}.ts`;
535
+ return { serverHooksPath: (0, node_path.resolve)(cwd, serverPath) };
700
536
  }
701
- function dependsOnStorage(appRoot) {
702
- const manifest = (0, node_path.join)(appRoot, "package.json");
703
- if (!(0, node_fs.existsSync)(manifest)) return false;
704
- try {
705
- const pkg = JSON.parse((0, node_fs.readFileSync)(manifest, "utf8"));
706
- return Boolean(pkg.dependencies?.["@geekmidas/storage"] ?? pkg.devDependencies?.["@geekmidas/storage"]);
707
- } catch {
708
- return false;
709
- }
537
+ /**
538
+ * Normalize production configuration
539
+ * @internal Exported for testing
540
+ */
541
+ function normalizeProductionConfig(cliProduction, configProduction) {
542
+ if (!cliProduction) return;
543
+ const config = configProduction ?? {};
544
+ return {
545
+ enabled: true,
546
+ bundle: config.bundle ?? true,
547
+ minify: config.minify ?? true,
548
+ healthCheck: config.healthCheck ?? "/health",
549
+ gracefulShutdown: config.gracefulShutdown ?? true,
550
+ external: config.external ?? [],
551
+ subscribers: config.subscribers ?? "exclude",
552
+ openapi: config.openapi ?? false,
553
+ optimizedHandlers: config.optimizedHandlers ?? true
554
+ };
710
555
  }
711
- //#endregion
712
- //#region src/generators/FunctionGenerator.ts
713
- var FunctionGenerator = class extends require_discover.ConstructGenerator {
714
- isConstruct(value) {
715
- return _geekmidas_constructs_functions.Function.isFunction(value);
716
- }
717
- async build(context, constructs, outputDir, options) {
718
- const provider = options?.provider || "aws-lambda";
719
- const logger = console;
720
- const functionInfos = [];
721
- if (constructs.length === 0 || provider !== "aws-lambda") return functionInfos;
722
- const functionsDir = (0, node_path.join)(outputDir, "functions");
723
- await (0, node_fs_promises.mkdir)(functionsDir, { recursive: true });
724
- for (const { key, construct, path } of constructs) {
725
- const handlerFile = await this.generateFunctionHandler(functionsDir, path.relative, key, context, construct.owner);
726
- functionInfos.push({
727
- name: key,
728
- handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
729
- timeout: construct.timeout,
730
- memorySize: construct.memorySize,
731
- environment: await construct.getEnvironment({ markOptional: context.markOptional }),
732
- dependencies: construct.constructs
556
+ /**
557
+ * Get production config from GkmConfig
558
+ * @internal
559
+ */
560
+ function getProductionConfigFromGkm(config) {
561
+ const serverConfig = config.providers?.server;
562
+ if (typeof serverConfig === "object") return serverConfig.production;
563
+ }
564
+ async function devCommand(options) {
565
+ if (options.entry) return entryDevCommand(options);
566
+ const defaultEnv = require_credentials.loadEnvFiles(".env");
567
+ if (defaultEnv.loaded.length > 0) logger$11.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
568
+ const appName = require_config.getAppNameFromCwd();
569
+ let config;
570
+ let appRoot = process.cwd();
571
+ let secretsRoot = process.cwd();
572
+ let workspaceAppName;
573
+ let workspaceAppPort;
574
+ let workspace;
575
+ if (appName) try {
576
+ const appConfig = await require_config.loadAppConfig();
577
+ config = appConfig.gkmConfig;
578
+ appRoot = appConfig.appRoot;
579
+ secretsRoot = appConfig.workspaceRoot;
580
+ workspaceAppName = appConfig.appName;
581
+ workspaceAppPort = appConfig.app.port;
582
+ workspace = appConfig.workspace;
583
+ logger$11.log(`📦 Running app: ${appConfig.appName} on port ${workspaceAppPort}`);
584
+ if (appConfig.app.entry) {
585
+ logger$11.log(`📄 Using entry point: ${appConfig.app.entry}`);
586
+ return entryDevCommand({
587
+ ...options,
588
+ entry: appConfig.app.entry,
589
+ port: workspaceAppPort,
590
+ portExplicit: true
733
591
  });
734
- logger.log(`Generated function handler: ${key}`);
735
592
  }
736
- return functionInfos;
593
+ } catch {
594
+ const loadedConfig = await require_config.loadWorkspaceConfig();
595
+ if (loadedConfig.type === "workspace") {
596
+ logger$11.log("📦 Detected workspace configuration");
597
+ return workspaceDevCommand(loadedConfig.workspace, options);
598
+ }
599
+ config = loadedConfig.raw;
737
600
  }
738
- async generateFunctionHandler(outputDir, sourceFile, exportName, context, owner) {
739
- const handlerFileName = `${exportName}.ts`;
740
- const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
741
- const importPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile).replace(/\.ts$/, ".js");
742
- const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
743
- const content = `import { AWSLambdaFunction } from '@geekmidas/constructs/aws';
744
- import { ${exportName} } from '${importPath}';
745
- ${runtime.imports}
746
- ${runtime.bindings}
747
-
748
- const adapter = new AWSLambdaFunction(envParser, ${exportName});
749
-
750
- export const handler = adapter.handler;
751
- `;
752
- await (0, node_fs_promises.writeFile)(handlerPath, content);
753
- return handlerPath;
601
+ else {
602
+ const loadedConfig = await require_config.loadWorkspaceConfig();
603
+ if (loadedConfig.type === "workspace") {
604
+ logger$11.log("📦 Detected workspace configuration");
605
+ return workspaceDevCommand(loadedConfig.workspace, options);
606
+ }
607
+ config = loadedConfig.raw;
608
+ workspace = loadedConfig.workspace;
754
609
  }
755
- };
756
- //#endregion
757
- //#region src/generators/QueueGenerator.ts
758
- /**
759
- * Generates the runtime for `q` queue workers.
760
- *
761
- * - **server** (`gkm dev`): a single `queues.ts` exposing `setupQueues()` that
762
- * runs an in-process pg-boss poller alongside the Hono server — no SQS/Lambda.
763
- * Each queue subscribes by its **name** on the shared event connection.
764
- * - **aws-lambda**: one handler file per queue wrapping `AWSLambdaQueue`, backed
765
- * by an SQS event-source mapping.
766
- */
767
- var QueueGenerator = class extends require_discover.ConstructGenerator {
768
- isConstruct(value) {
769
- return _geekmidas_constructs_queue.Queue.isQueue(value);
610
+ if (config.env) {
611
+ const { loaded, missing } = require_credentials.loadEnvFiles(config.env, appRoot);
612
+ if (loaded.length > 0) logger$11.log(`📦 Loaded env: ${loaded.join(", ")}`);
613
+ if (missing.length > 0) logger$11.warn(`⚠️ Missing env files: ${missing.join(", ")}`);
770
614
  }
771
- async build(context, constructs, outputDir, options) {
772
- const provider = options?.provider || "aws-lambda";
773
- const logger = console;
774
- const queueInfos = [];
775
- if (provider === "server") {
776
- await this.generateServerQueuesFile(outputDir, constructs);
777
- logger.log(`Generated server queues file with ${constructs.length} queues (polling mode)`);
778
- return queueInfos;
615
+ const resolved = resolveProviders(config, { provider: "server" });
616
+ logger$11.log("🚀 Starting development server...");
617
+ logger$11.log(`Loading constructs from: ${config.constructs}`);
618
+ const telescope = normalizeTelescopeConfig(config.telescope);
619
+ if (telescope) logger$11.log(`🔭 Telescope enabled at ${telescope.path}`);
620
+ const studio = normalizeStudioConfig(config.studio);
621
+ if (studio) logger$11.log(`🗄️ Studio enabled at ${studio.path}`);
622
+ const hooks = normalizeHooksConfig(config.hooks, appRoot);
623
+ if (hooks) logger$11.log(`🪝 Server hooks enabled from ${config.hooks?.server}`);
624
+ const enableOpenApi = require_openapi.resolveOpenApiConfig(config).enabled || resolved.enableOpenApi;
625
+ if (enableOpenApi) logger$11.log(`📄 OpenAPI output: ${require_openapi.OPENAPI_OUTPUT_PATH}`);
626
+ const build = (bustCache = false) => buildApp({
627
+ config,
628
+ workspaceRoot: workspace?.root ?? secretsRoot,
629
+ appRoot,
630
+ providers: ["server"],
631
+ enableOpenApi,
632
+ cacheBackend: require_credentials.cacheBackendFor(require_credentials.providerOf(workspace ?? config)),
633
+ telescope,
634
+ studio,
635
+ hooks,
636
+ skipBundle: true,
637
+ bustCache,
638
+ serveEmpty: true
639
+ });
640
+ const initial = await build();
641
+ const runtime = config.runtime ?? "node";
642
+ let secretsJsonPath;
643
+ const appSecrets = await require_credentials.loadSecretsForApp(secretsRoot, config.stages.local, workspaceAppName);
644
+ if (workspace && require_credentials.usesConstructs(workspace)) {
645
+ const reconciled = await require_credentials.reconcileWorkspace(workspace, { stage: workspace.stages.local });
646
+ if (reconciled.changed && reconciled.plan.containers.length > 0) {
647
+ logger$11.log(`🐳 Services: ${reconciled.plan.containers.join(", ")}`);
648
+ for (const [container, address] of Object.entries(reconciled.addresses)) logger$11.log(` ${container}: ${address}`);
779
649
  }
780
- if (constructs.length === 0 || provider !== "aws-lambda") return queueInfos;
781
- const queuesDir = (0, node_path.join)(outputDir, "queues");
782
- await (0, node_fs_promises.mkdir)(queuesDir, { recursive: true });
783
- for (const { key, construct, path } of constructs) {
784
- const handlerFile = await this.generateQueueHandler(queuesDir, path.relative, key, context, construct.owner);
785
- queueInfos.push({
786
- name: construct.name,
787
- handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
788
- batchSize: construct.batchSize,
789
- fifo: construct.fifo,
790
- timeout: construct.timeout,
791
- environment: await construct.getEnvironment({ markOptional: context.markOptional }),
792
- dependencies: construct.constructs
793
- });
794
- logger.log(`Generated queue handler: ${key}`);
650
+ Object.assign(appSecrets, reconciled.env);
651
+ } else {
652
+ const resolvedPorts = await require_credentials.resolveServicePorts(secretsRoot);
653
+ if (Object.keys(resolvedPorts.ports).length > 0) {
654
+ const rewritten = require_credentials.rewriteUrlsWithPorts(appSecrets, resolvedPorts);
655
+ Object.assign(appSecrets, rewritten);
656
+ logger$11.log(`🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`);
795
657
  }
796
- return queueInfos;
797
658
  }
798
- async generateQueueHandler(outputDir, sourceFile, exportName, context, owner) {
799
- const handlerPath = (0, node_path.join)(outputDir, `${exportName}.ts`);
800
- const importPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile).replace(/\.ts$/, ".js");
801
- const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
802
- const content = `import { AWSLambdaQueue } from '@geekmidas/constructs/aws';
803
- import { ${exportName} } from '${importPath}';
804
- ${runtime.imports}
805
- ${runtime.bindings}
806
-
807
- const adapter = new AWSLambdaQueue(envParser, ${exportName});
808
-
809
- export const handler = adapter.handler;
810
- `;
811
- await (0, node_fs_promises.writeFile)(handlerPath, content);
812
- return handlerPath;
659
+ if (workspace && workspaceAppName) {
660
+ const depEnv = require_workspace.getDependencyEnvVars(workspace, workspaceAppName);
661
+ Object.assign(appSecrets, depEnv);
813
662
  }
814
- async generateServerQueuesFile(outputDir, queues) {
815
- await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
816
- const queuesPath = (0, node_path.join)(outputDir, "queues.ts");
817
- const importsByFile = /* @__PURE__ */ new Map();
818
- for (const { path, key } of queues) {
819
- const importPath = (0, node_path.relative)((0, node_path.dirname)(queuesPath), path.relative).replace(/\.ts$/, ".js");
820
- if (!importsByFile.has(importPath)) importsByFile.set(importPath, []);
821
- importsByFile.get(importPath)?.push(key);
822
- }
823
- const content = `/**
824
- * Generated queue workers setup
825
- *
826
- * ⚠️ WARNING: This is for LOCAL DEVELOPMENT ONLY
827
- * This runs an in-process pg-boss poller alongside the HTTP server. Each queue
828
- * subscribes by its name on the shared event connection
829
- * (EVENT_SUBSCRIBER_CONNECTION_STRING), so producers using
830
- * <NAME>_PUBLISHER_CONNECTION_STRING (pgboss:// locally) reach it.
831
- *
832
- * For production, queues run as AWS Lambda with SQS event-source mappings.
833
- */
834
- import type { EnvironmentParser } from '@geekmidas/envkit';
835
- import type { Logger } from '@geekmidas/logger';
836
- import { EventConnectionFactory, Subscriber } from '@geekmidas/events';
837
- import type { EventConnection, EventSubscriber } from '@geekmidas/events';
838
- import { ServiceDiscovery } from '@geekmidas/services';
839
- ${Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n")}
840
-
841
- const queues = [
842
- ${queues.map(({ key }) => key).join(",\n ")}
843
- ];
844
-
845
- const activeSubscribers: EventSubscriber<any>[] = [];
846
-
847
- export async function setupQueues(
848
- envParser: EnvironmentParser<any>,
849
- logger: Logger,
850
- ): Promise<void> {
851
- if (queues.length === 0) {
852
- return;
853
- }
854
-
855
- logger.info('Setting up queue workers in polling mode (local development)');
856
-
857
- const config = envParser.create((get) => ({
858
- connectionString: get('EVENT_SUBSCRIBER_CONNECTION_STRING').string(),
859
- })).parse();
860
-
861
- const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
862
-
863
- let connection: EventConnection;
864
- try {
865
- connection = await EventConnectionFactory.fromConnectionString(config.connectionString);
866
- const connectionType = new URL(config.connectionString).protocol.replace(':', '');
867
- logger.info({ connectionType }, 'Created shared event connection for queues');
868
- } catch (error) {
869
- logger.error({ error }, 'Failed to create event connection for queues');
870
- return;
871
- }
872
-
873
- for (const queue of queues) {
874
- try {
875
- const eventSubscriber = await Subscriber.fromConnection(connection);
876
-
877
- const services = queue.services.length > 0
878
- ? await serviceDiscovery.register(queue.services)
879
- : {};
880
-
881
- // A queue subscribes to a single "type" — its own name.
882
- await eventSubscriber.subscribe([queue.name], async (message) => {
883
- try {
884
- const validation = await queue.messageSchema['~standard'].validate(message.payload);
885
- if (validation.issues) {
886
- logger.error({ issues: validation.issues, queue: queue.name }, 'Queue message failed validation');
887
- return;
888
- }
889
-
890
- await queue.handler({
891
- messages: [validation.value],
892
- services: services,
893
- logger: queue.logger,
894
- });
895
-
896
- logger.debug({ queue: queue.name }, 'Successfully processed queue message');
897
- } catch (error) {
898
- logger.error({ error, queue: queue.name }, 'Failed to process queue message');
899
- // Message will become visible again for retry.
900
- }
901
- });
902
-
903
- activeSubscribers.push(eventSubscriber);
904
- logger.info({ queue: queue.name }, 'Queue worker started polling');
905
- } catch (error) {
906
- logger.error({ error, queue: queue.name }, 'Failed to setup queue worker');
907
- }
908
- }
909
-
910
- const shutdown = () => {
911
- logger.info('Stopping all queue workers');
912
- for (const _ of activeSubscribers) {
913
- connection.stop();
914
- }
915
- };
916
-
917
- process.on('SIGTERM', shutdown);
918
- process.on('SIGINT', shutdown);
663
+ if (Object.keys(appSecrets).length > 0) {
664
+ const secretsDir = (0, node_path.join)(secretsRoot, ".gkm");
665
+ await (0, node_fs_promises.mkdir)(secretsDir, { recursive: true });
666
+ const secretsFileName = workspaceAppName ? `dev-secrets-${workspaceAppName}.json` : "dev-secrets.json";
667
+ secretsJsonPath = (0, node_path.join)(secretsDir, secretsFileName);
668
+ await (0, node_fs_promises.writeFile)(secretsJsonPath, JSON.stringify(appSecrets, null, 2));
669
+ logger$11.log(`🔐 Loaded ${Object.keys(appSecrets).length} secret(s)`);
670
+ }
671
+ const devServer = new DevServer(resolved.providers[0], options.port ?? workspaceAppPort ?? 3e3, options.portExplicit ?? false, enableOpenApi, telescope, studio, runtime, appRoot, secretsJsonPath, initial.selfServing !== void 0);
672
+ await devServer.start();
673
+ const hooksFile = (config.hooks?.server?.split("#"))?.[0];
674
+ const normalizedPatterns = [config.constructs, ...hooksFile ? [hooksFile.endsWith(".ts") ? hooksFile : `${hooksFile}.ts`] : []].flat().filter((p) => typeof p === "string").map((p) => p.startsWith("./") ? p.slice(2) : p);
675
+ logger$11.log(`👀 Watching for changes in: ${normalizedPatterns.join(", ")}`);
676
+ const resolvedFiles = await (0, fast_glob.default)(normalizedPatterns, {
677
+ cwd: appRoot,
678
+ absolute: false,
679
+ onlyFiles: true
680
+ });
681
+ const dirsToWatch = [...new Set(resolvedFiles.map((f) => {
682
+ return f.split("/").slice(0, -1).join("/");
683
+ }))];
684
+ logger$11.log(`📁 Found ${resolvedFiles.length} files in ${dirsToWatch.length} directories`);
685
+ const watcher = chokidar.default.watch([...resolvedFiles, ...dirsToWatch], {
686
+ ignored: /(^|[/\\])\../,
687
+ persistent: true,
688
+ ignoreInitial: true,
689
+ cwd: appRoot
690
+ });
691
+ watcher.on("ready", () => {
692
+ logger$11.log("🔍 File watcher ready");
693
+ });
694
+ watcher.on("error", (error) => {
695
+ logger$11.error("❌ Watcher error:", error);
696
+ });
697
+ let rebuildTimeout = null;
698
+ watcher.on("change", async (path) => {
699
+ logger$11.log(`📝 File changed: ${path}`);
700
+ if (rebuildTimeout) clearTimeout(rebuildTimeout);
701
+ rebuildTimeout = setTimeout(async () => {
702
+ try {
703
+ logger$11.log("🔄 Rebuilding...");
704
+ const rebuilt = await build(true);
705
+ logger$11.log("✅ Rebuild complete, restarting server...");
706
+ devServer.selfServing = rebuilt.selfServing !== void 0;
707
+ await devServer.restart();
708
+ } catch (error) {
709
+ const err = error;
710
+ logger$11.error(`❌ Rebuild failed: ${err.message}`);
711
+ if (err.stack) logger$11.error(err.stack);
712
+ }
713
+ }, 300);
714
+ });
715
+ let isShuttingDown = false;
716
+ const shutdown = () => {
717
+ if (isShuttingDown) return;
718
+ isShuttingDown = true;
719
+ logger$11.log("\n🛑 Shutting down...");
720
+ Promise.all([watcher.close(), devServer.stop()]).catch((err) => {
721
+ logger$11.error("Error during shutdown:", err);
722
+ }).finally(() => {
723
+ process.exit(0);
724
+ });
725
+ };
726
+ process.on("SIGINT", shutdown);
727
+ process.on("SIGTERM", shutdown);
919
728
  }
920
- `;
921
- await (0, node_fs_promises.writeFile)(queuesPath, content);
922
- return queuesPath;
729
+ /**
730
+ * Generate all dependency environment variables for all apps.
731
+ * Returns a flat object with all {APP_NAME}_URL variables.
732
+ * @internal Exported for testing
733
+ */
734
+ function generateAllDependencyEnvVars(workspace, urlPrefix = "http://localhost") {
735
+ const env = {};
736
+ for (const appName of Object.keys(workspace.apps)) {
737
+ const appEnv = require_workspace.getDependencyEnvVars(workspace, appName, urlPrefix);
738
+ Object.assign(env, appEnv);
739
+ }
740
+ return env;
741
+ }
742
+ /**
743
+ * Check for port conflicts across all apps.
744
+ * Returns list of conflicts if any ports are duplicated.
745
+ * @internal Exported for testing
746
+ */
747
+ function checkPortConflicts(workspace) {
748
+ const conflicts = [];
749
+ const portToApp = /* @__PURE__ */ new Map();
750
+ for (const [appName, app] of Object.entries(workspace.apps)) {
751
+ const existingApp = portToApp.get(app.port);
752
+ if (existingApp) conflicts.push({
753
+ app1: existingApp,
754
+ app2: appName,
755
+ port: app.port
756
+ });
757
+ else portToApp.set(app.port, appName);
758
+ }
759
+ return conflicts;
760
+ }
761
+ const FRONTEND_FRAMEWORKS = {
762
+ nextjs: {
763
+ displayName: "Next.js",
764
+ configFiles: [
765
+ "next.config.js",
766
+ "next.config.ts",
767
+ "next.config.mjs",
768
+ "next.config.cjs"
769
+ ],
770
+ dependency: "next",
771
+ installCommand: "pnpm add next react react-dom"
772
+ },
773
+ remix: {
774
+ displayName: "Remix",
775
+ configFiles: [
776
+ "remix.config.js",
777
+ "remix.config.mjs",
778
+ "remix.config.ts",
779
+ "vite.config.js",
780
+ "vite.config.ts",
781
+ "vite.config.mjs",
782
+ "vite.config.cjs"
783
+ ],
784
+ dependency: "@remix-run/dev",
785
+ installCommand: "pnpm add -D @remix-run/dev"
786
+ },
787
+ vite: {
788
+ displayName: "Vite",
789
+ configFiles: [
790
+ "vite.config.js",
791
+ "vite.config.ts",
792
+ "vite.config.mjs",
793
+ "vite.config.cjs"
794
+ ],
795
+ dependency: "vite",
796
+ installCommand: "pnpm add -D vite"
797
+ },
798
+ "tanstack-start": {
799
+ displayName: "TanStack Start",
800
+ configFiles: [
801
+ "vite.config.js",
802
+ "vite.config.ts",
803
+ "vite.config.mjs",
804
+ "vite.config.cjs"
805
+ ],
806
+ dependency: "@tanstack/react-start",
807
+ installCommand: "pnpm add @tanstack/react-start @tanstack/react-router"
808
+ },
809
+ expo: {
810
+ displayName: "Expo",
811
+ configFiles: [
812
+ "app.config.ts",
813
+ "app.config.js",
814
+ "app.json"
815
+ ],
816
+ dependency: "expo",
817
+ installCommand: "pnpm add expo"
923
818
  }
924
819
  };
925
- //#endregion
926
- //#region src/generators/SubscriberGenerator.ts
927
- var SubscriberGenerator = class extends require_discover.ConstructGenerator {
928
- isConstruct(value) {
929
- return _geekmidas_constructs_subscribers.Subscriber.isSubscriber(value);
820
+ /** Whether a configured framework is one this table knows how to start. */
821
+ function isClientFramework(framework) {
822
+ return framework !== void 0 && framework in FRONTEND_FRAMEWORKS;
823
+ }
824
+ /**
825
+ * Auto-detect the frontend framework by scanning package.json deps and config
826
+ * files. Dependency match wins over config file match because deps are more
827
+ * distinctive (a Remix app has both vite.config.ts AND @remix-run/dev).
828
+ */
829
+ function detectFrontendFramework(fullPath, deps) {
830
+ const order = [
831
+ "expo",
832
+ "tanstack-start",
833
+ "nextjs",
834
+ "remix",
835
+ "vite"
836
+ ];
837
+ for (const name of order) if (deps[FRONTEND_FRAMEWORKS[name].dependency]) return name;
838
+ for (const name of order) if (FRONTEND_FRAMEWORKS[name].configFiles.some((file) => (0, node_fs.existsSync)((0, node_path.join)(fullPath, file)))) return name;
839
+ }
840
+ /**
841
+ * Validate a frontend app configuration.
842
+ *
843
+ * If `framework` is provided, validates strictly against that framework's
844
+ * expected config file and dependency. Otherwise auto-detects the framework
845
+ * from package.json and config files. If no recognized framework is found, the
846
+ * app is allowed through with a warning provided it has a `dev` script.
847
+ *
848
+ * @internal Exported for testing
849
+ */
850
+ async function validateFrontendApp(appName, appPath, workspaceRoot, framework) {
851
+ const errors = [];
852
+ const warnings = [];
853
+ const fullPath = (0, node_path.join)(workspaceRoot, appPath);
854
+ const packageJsonPath = (0, node_path.join)(fullPath, "package.json");
855
+ if (!(0, node_fs.existsSync)(packageJsonPath)) {
856
+ errors.push(`package.json not found at ${appPath}. Run: pnpm init in the app directory.`);
857
+ return {
858
+ appName,
859
+ valid: false,
860
+ errors,
861
+ warnings
862
+ };
863
+ }
864
+ let pkg;
865
+ try {
866
+ pkg = require(packageJsonPath);
867
+ } catch {
868
+ errors.push(`Failed to read package.json at ${packageJsonPath}`);
869
+ return {
870
+ appName,
871
+ valid: false,
872
+ errors,
873
+ warnings
874
+ };
875
+ }
876
+ const deps = {
877
+ ...pkg.dependencies,
878
+ ...pkg.devDependencies
879
+ };
880
+ const resolvedFramework = framework ?? detectFrontendFramework(fullPath, deps);
881
+ if (resolvedFramework) {
882
+ const spec = FRONTEND_FRAMEWORKS[resolvedFramework];
883
+ if (!spec.configFiles.some((file) => (0, node_fs.existsSync)((0, node_path.join)(fullPath, file)))) errors.push(`${spec.displayName} config file not found. Expected one of: ${spec.configFiles.join(", ")}`);
884
+ if (!deps[spec.dependency]) errors.push(`${spec.displayName} not found in dependencies. Run: ${spec.installCommand}`);
885
+ } else warnings.push("No recognized frontend framework detected (Next.js, Vite, Remix). The app will run via its \"dev\" script. Set `framework` in your app config to enable strict validation.");
886
+ if (!pkg.scripts?.dev) {
887
+ if (resolvedFramework) warnings.push("No \"dev\" script found in package.json. Turbo expects a \"dev\" script to run.");
888
+ else errors.push("No \"dev\" script found in package.json. Without a recognized framework or a dev script, there is nothing to run.");
889
+ }
890
+ return {
891
+ appName,
892
+ valid: errors.length === 0,
893
+ errors,
894
+ warnings
895
+ };
896
+ }
897
+ /**
898
+ * Validate all frontend apps in the workspace.
899
+ * Returns validation results for each frontend app.
900
+ * @internal Exported for testing
901
+ */
902
+ async function validateFrontendApps(workspace) {
903
+ const results = [];
904
+ for (const [appName, app] of Object.entries(workspace.apps)) if (app.type === "web") {
905
+ const result = await validateFrontendApp(appName, app.path, workspace.root, isClientFramework(app.framework) ? app.framework : void 0);
906
+ results.push(result);
907
+ }
908
+ return results;
909
+ }
910
+ /**
911
+ * Load secrets for development stage.
912
+ * Returns env vars to inject, or empty object if secrets not configured/found.
913
+ * @internal Exported for testing
914
+ */
915
+ async function loadDevSecrets(workspace) {
916
+ if (!workspace.secrets.enabled) return {};
917
+ const stage = workspace.stages.local;
918
+ if (require_storage.secretsExist(stage, workspace.root)) {
919
+ const secrets = await require_storage.readStageSecrets(stage, workspace.root);
920
+ if (secrets) {
921
+ logger$11.log(`🔐 Loading secrets from stage: ${stage}`);
922
+ return require_storage.toEmbeddableSecrets(secrets);
923
+ }
924
+ }
925
+ logger$11.warn(`⚠️ Secrets enabled but no "${stage}" secrets found. Run "gkm setup" to initialize the local stage`);
926
+ return {};
927
+ }
928
+ /**
929
+ * Workspace dev command - orchestrates multi-app development using Turbo.
930
+ *
931
+ * Flow:
932
+ * 1. Check for port conflicts
933
+ * 2. Start docker-compose services (db, cache, mail)
934
+ * 3. Generate dependency URLs ({APP_NAME}_URL)
935
+ * 4. Spawn turbo run dev with injected env vars
936
+ */
937
+ async function workspaceDevCommand(workspace, options) {
938
+ const appCount = Object.keys(workspace.apps).length;
939
+ const backendApps = Object.entries(workspace.apps).filter(([_, app]) => app.type === "backend");
940
+ const frontendApps = Object.entries(workspace.apps).filter(([_, app]) => app.type === "web");
941
+ const mobileApps = Object.entries(workspace.apps).filter(([_, app]) => app.type === "mobile");
942
+ logger$11.log(`\n🚀 Starting workspace: ${workspace.name}`);
943
+ const counts = [`${backendApps.length} backend`, `${frontendApps.length} web`];
944
+ if (mobileApps.length > 0) counts.push(`${mobileApps.length} mobile`);
945
+ logger$11.log(` ${counts.join(", ")} app(s)`);
946
+ const conflicts = checkPortConflicts(workspace);
947
+ if (conflicts.length > 0) {
948
+ for (const conflict of conflicts) logger$11.error(`❌ Port conflict: Apps "${conflict.app1}" and "${conflict.app2}" both use port ${conflict.port}`);
949
+ throw new Error("Port conflicts detected. Please assign unique ports to each app.");
930
950
  }
931
- async build(context, constructs, outputDir, options) {
932
- const provider = options?.provider || "aws-lambda";
933
- const logger = console;
934
- const subscriberInfos = [];
935
- if (provider === "server") {
936
- await this.generateServerSubscribersFile(outputDir, constructs);
937
- logger.log(`Generated server subscribers file with ${constructs.length} subscribers (polling mode)`);
938
- return subscriberInfos;
951
+ if (frontendApps.length > 0) {
952
+ logger$11.log("\n🔍 Validating frontend apps...");
953
+ const validationResults = await validateFrontendApps(workspace);
954
+ let hasErrors = false;
955
+ for (const result of validationResults) {
956
+ if (!result.valid) {
957
+ hasErrors = true;
958
+ logger$11.error(`\n❌ Frontend app "${result.appName}" validation failed:`);
959
+ for (const error of result.errors) logger$11.error(` • ${error}`);
960
+ }
961
+ for (const warning of result.warnings) logger$11.warn(` ⚠️ ${result.appName}: ${warning}`);
939
962
  }
940
- if (constructs.length === 0) return subscriberInfos;
941
- if (provider !== "aws-lambda") return subscriberInfos;
942
- const subscribersDir = (0, node_path.join)(outputDir, "subscribers");
943
- await (0, node_fs_promises.mkdir)(subscribersDir, { recursive: true });
944
- for (const { key, construct, path } of constructs) {
945
- const handlerFile = await this.generateSubscriberHandler(subscribersDir, path.relative, key, construct, context, construct.owner);
946
- subscriberInfos.push({
947
- name: key,
948
- handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
949
- subscribedEvents: construct.subscribedEvents || [],
950
- ...construct.topicName ? {
951
- transport: "topic",
952
- topic: construct.topicName
953
- } : {},
954
- timeout: construct.timeout,
955
- memorySize: construct.memorySize,
956
- environment: await construct.getEnvironment({ markOptional: context.markOptional }),
957
- dependencies: construct.constructs
958
- });
959
- logger.log(`Generated subscriber handler: ${key}`);
963
+ if (hasErrors) throw new Error("Frontend app validation failed. Fix the issues above and try again.");
964
+ logger$11.log("✅ Frontend apps validated");
965
+ }
966
+ const rawSecrets = await loadDevSecrets(workspace);
967
+ let secretsEnv;
968
+ if (require_credentials.usesConstructs(workspace)) {
969
+ const reconciled = await require_credentials.reconcileWorkspace(workspace, { stage: workspace.stages.local });
970
+ if (reconciled.changed && reconciled.plan.containers.length > 0) {
971
+ logger$11.log(`🐳 Services: ${reconciled.plan.containers.join(", ")}`);
972
+ for (const [container, address] of Object.entries(reconciled.addresses)) logger$11.log(` ${container}: ${address}`);
960
973
  }
961
- return subscriberInfos;
974
+ secretsEnv = {
975
+ ...rawSecrets,
976
+ ...reconciled.env
977
+ };
978
+ } else {
979
+ const resolvedPorts = await require_credentials.resolveServicePorts(workspace.root);
980
+ await require_credentials.startWorkspaceServices(workspace, resolvedPorts.dockerEnv, rawSecrets);
981
+ secretsEnv = require_credentials.rewriteUrlsWithPorts(rawSecrets, resolvedPorts);
962
982
  }
963
- async generateSubscriberHandler(outputDir, sourceFile, exportName, _subscriber, context, owner) {
964
- const handlerFileName = `${exportName}.ts`;
965
- const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
966
- const importPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile).replace(/\.ts$/, ".js");
967
- const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
968
- const content = `import { AWSLambdaSubscriber } from '@geekmidas/constructs/aws';
969
- import { ${exportName} } from '${importPath}';
970
- ${runtime.imports}
971
- ${runtime.bindings}
972
-
973
- const adapter = new AWSLambdaSubscriber(envParser, ${exportName});
974
-
975
- export const handler = adapter.handler;
976
- `;
977
- await (0, node_fs_promises.writeFile)(handlerPath, content);
978
- return handlerPath;
983
+ if (Object.keys(secretsEnv).length > 0) logger$11.log(` Loaded ${Object.keys(secretsEnv).length} secret(s)`);
984
+ const dependencyEnv = generateAllDependencyEnvVars(workspace);
985
+ if (Object.keys(dependencyEnv).length > 0) {
986
+ logger$11.log("📡 Dependency URLs:");
987
+ for (const [key, value] of Object.entries(dependencyEnv)) logger$11.log(` ${key}=${value}`);
979
988
  }
980
- async generateServerSubscribersFile(outputDir, subscribers) {
981
- await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
982
- const subscribersPath = (0, node_path.join)(outputDir, "subscribers.ts");
983
- const importsByFile = /* @__PURE__ */ new Map();
984
- for (const { path, key } of subscribers) {
985
- const importPath = (0, node_path.relative)((0, node_path.dirname)(subscribersPath), path.relative).replace(/\.ts$/, ".js");
986
- if (!importsByFile.has(importPath)) importsByFile.set(importPath, []);
987
- importsByFile.get(importPath)?.push(key);
989
+ let turboFilter = [];
990
+ if (options.app) {
991
+ if (!workspace.apps[options.app]) {
992
+ const appNames = Object.keys(workspace.apps).join(", ");
993
+ throw new Error(`App "${options.app}" not found. Available apps: ${appNames}`);
988
994
  }
989
- const content = `/**
990
- * Generated subscribers setup
991
- *
992
- * ⚠️ WARNING: This is for LOCAL DEVELOPMENT ONLY
993
- * This uses event polling which is not suitable for production.
994
- *
995
- * For production, use AWS Lambda with SQS/SNS event source mappings.
996
- * Lambda automatically:
997
- * - Scales based on queue depth
998
- * - Handles batch processing and retries
999
- * - Manages dead letter queues
1000
- * - Provides better cost optimization
1001
- *
1002
- * This polling implementation is useful for:
1003
- * - Local development and testing
1004
- * - Understanding event flow without Lambda deployment
1005
- *
1006
- * Supported connection strings:
1007
- * - sqs://region/account-id/queue-name (SQS queue)
1008
- * - sns://region/account-id/topic-name (SNS topic)
1009
- * - rabbitmq://host:port/queue-name (RabbitMQ)
1010
- * - pgboss://user:pass@host:port/database (pg-boss / PostgreSQL)
1011
- * - basic://in-memory (In-memory for testing)
1012
- */
1013
- import type { EnvironmentParser } from '@geekmidas/envkit';
1014
- import type { Logger } from '@geekmidas/logger';
1015
- import { EventConnectionFactory, Subscriber } from '@geekmidas/events';
1016
- import type { EventConnection, EventSubscriber } from '@geekmidas/events';
1017
- import { ServiceDiscovery } from '@geekmidas/services';
1018
- ${Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n")}
1019
-
1020
- const subscribers = [
1021
- ${subscribers.map(({ key }) => key).join(",\n ")}
1022
- ];
1023
-
1024
- const activeSubscribers: EventSubscriber<any>[] = [];
1025
-
1026
- export async function setupSubscribers(
1027
- envParser: EnvironmentParser<any>,
1028
- logger: Logger,
1029
- ): Promise<void> {
1030
- if (subscribers.length === 0) {
1031
- return;
1032
- }
1033
-
1034
- logger.info('Setting up subscribers in polling mode (local development)');
1035
-
1036
- const config = envParser.create((get) => ({
1037
- connectionString: get('EVENT_SUBSCRIBER_CONNECTION_STRING').string(),
1038
- })).parse();
1039
-
1040
- const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
1041
-
1042
- // Create connection once, outside the loop (more efficient)
1043
- // EventConnectionFactory automatically determines the right connection type
1044
- let connection: EventConnection;
1045
- try {
1046
- connection = await EventConnectionFactory.fromConnectionString(config.connectionString);
1047
-
1048
- const connectionType = new URL(config.connectionString).protocol.replace(':', '');
1049
- logger.info({ connectionType }, 'Created shared event connection');
1050
- } catch (error) {
1051
- logger.error({ error }, 'Failed to create event connection');
1052
- return;
1053
- }
1054
-
1055
- for (const subscriber of subscribers) {
1056
- try {
1057
- // Create subscriber from shared connection
1058
- const eventSubscriber = await Subscriber.fromConnection(connection);
1059
-
1060
- // Register services
1061
- const services = subscriber.services.length > 0
1062
- ? await serviceDiscovery.register(subscriber.services)
1063
- : {};
1064
-
1065
- // Subscribe to events
1066
- const subscribedEvents = subscriber.subscribedEvents || [];
1067
-
1068
- if (subscribedEvents.length === 0) {
1069
- logger.warn({ subscriber: subscriber.constructor.name }, 'Subscriber has no subscribed events, skipping');
1070
- continue;
1071
- }
1072
-
1073
- await eventSubscriber.subscribe(subscribedEvents, async (event) => {
1074
- try {
1075
- // Process single event (batch of 1)
1076
- await subscriber.handler({
1077
- events: [event],
1078
- services: services as any,
1079
- logger: subscriber.logger,
1080
- });
1081
-
1082
- logger.debug({ eventType: event.type }, 'Successfully processed event');
1083
- } catch (error) {
1084
- logger.error({ error, event }, 'Failed to process event');
1085
- // Event will become visible again for retry
1086
- }
1087
- });
1088
-
1089
- activeSubscribers.push(eventSubscriber);
1090
-
1091
- logger.info(
1092
- {
1093
- events: subscribedEvents,
1094
- },
1095
- 'Subscriber started polling'
1096
- );
1097
- } catch (error) {
1098
- logger.error({ error, subscriber: subscriber.constructor.name }, 'Failed to setup subscriber');
1099
- }
1100
- }
1101
-
1102
- // Setup graceful shutdown
1103
- const shutdown = () => {
1104
- logger.info('Stopping all subscribers');
1105
- for (const eventSubscriber of activeSubscribers) {
1106
- connection.stop();
1107
- }
1108
- };
1109
-
1110
- process.on('SIGTERM', shutdown);
1111
- process.on('SIGINT', shutdown);
1112
- }
1113
- `;
1114
- await (0, node_fs_promises.writeFile)(subscribersPath, content);
1115
- return subscribersPath;
1116
- }
1117
- };
1118
- //#endregion
1119
- //#region src/generators/TopicGenerator.ts
1120
- /**
1121
- * Discovers `t` topics into `manifest.topics`. A topic is a *resource*, not a
1122
- * function — it has no handler to generate. Producers reach it via its derived
1123
- * publisher (`topic.publisher`, whose connection string is sniffed onto the
1124
- * producing construct) and subscribers bind via `s.topic(topic)`; this generator
1125
- * only records the topic + its event contract so infra can provision the SNS
1126
- * topic. Identical output for every provider (locally pg-boss routes by event
1127
- * name, so there's nothing to run).
1128
- */
1129
- var TopicGenerator = class extends require_discover.ConstructGenerator {
1130
- isConstruct(value) {
1131
- return _geekmidas_constructs_topic.Topic.isTopic(value);
995
+ turboFilter = ["--filter", appPackageName(workspace, options.app) ?? options.app];
996
+ logger$11.log(`\n🎯 Running single app: ${options.app}`);
997
+ } else if (options.filter) {
998
+ turboFilter = ["--filter", options.filter];
999
+ logger$11.log(`\n🔍 Using filter: ${options.filter}`);
1000
+ } else {
1001
+ const { filters, unpackaged } = turboFilters(workspace);
1002
+ turboFilter = filters.flatMap((name) => ["--filter", name]);
1003
+ if (unpackaged.length > 0) logger$11.warn(`⚠️ No package.json with a name for: ${unpackaged.join(", ")} — turbo cannot run ${unpackaged.length === 1 ? "it" : "them"}.`);
1004
+ logger$11.log(`\n🎯 Running all ${appCount} apps`);
1132
1005
  }
1133
- async build(_context, constructs, _outputDir, _options) {
1134
- const logger = console;
1135
- const topicInfos = constructs.map(({ construct }) => ({
1136
- name: construct.name,
1137
- events: construct.eventTypes
1138
- }));
1139
- for (const topic of topicInfos) logger.log(`Discovered topic: ${topic.name}`);
1140
- return topicInfos;
1006
+ const buildOrder = require_workspace.getAppBuildOrder(workspace);
1007
+ logger$11.log("\n📋 Apps (in dependency order):");
1008
+ for (const appName of buildOrder) {
1009
+ const app = workspace.apps[appName];
1010
+ if (!app) continue;
1011
+ const deps = app.dependencies.length > 0 ? ` (depends on: ${app.dependencies.join(", ")})` : "";
1012
+ const icon = app.type === "backend" ? "🔧" : app.type === "mobile" ? "📱" : "🌐";
1013
+ logger$11.log(` ${icon} ${appName} → http://localhost:${app.port}${deps}`);
1141
1014
  }
1142
- };
1143
- //#endregion
1144
- //#region src/workspace/client-generator.ts
1145
- /**
1146
- * Normalize routes to an array of patterns.
1147
- * Handles string, string[], and PartitionedRoutes (extracts paths).
1148
- * @internal Exported for use in dev command
1149
- */
1150
- function normalizeRoutes(routes) {
1151
- if (!routes) return [];
1152
- if (require_discover.isPartitionedRoutes(routes)) return Array.isArray(routes.paths) ? routes.paths : [routes.paths];
1153
- return Array.isArray(routes) ? routes : [routes];
1154
- }
1155
- //#endregion
1156
- //#region src/exec/index.ts
1157
- const logger$12 = console;
1158
- /** `gkm exec` was given nothing to run. */
1159
- var NoCommandSpecified = class extends Error {
1160
- constructor() {
1161
- super("No command specified. Usage: gkm exec -- <command>");
1162
- this.name = "NoCommandSpecified";
1015
+ const configFiles = [
1016
+ "gkm.config.ts",
1017
+ "gkm.config.js",
1018
+ "gkm.config.json"
1019
+ ];
1020
+ let configPath = "";
1021
+ for (const file of configFiles) {
1022
+ const fullPath = (0, node_path.join)(workspace.root, file);
1023
+ if ((0, node_fs.existsSync)(fullPath)) {
1024
+ configPath = fullPath;
1025
+ break;
1026
+ }
1163
1027
  }
1164
- };
1165
- /**
1166
- * Run a command with secrets injected into Credentials.
1167
- * Uses Node's --import flag to preload a script that populates Credentials
1168
- * before the command loads any modules that depend on them.
1169
- *
1170
- * @example
1171
- * ```bash
1172
- * gkm exec -- npx @better-auth/cli migrate
1173
- * gkm exec -- npx prisma migrate dev
1174
- * ```
1175
- */
1176
- async function execCommand(commandArgs, options = {}) {
1177
- const cwd = options.cwd ?? process.cwd();
1178
- if (!commandArgs[0]) throw new NoCommandSpecified();
1179
- const defaultEnv = require_credentials.loadEnvFiles(".env");
1180
- if (defaultEnv.loaded.length > 0) logger$12.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
1181
- const { credentials, secretsJsonPath, appName } = await require_credentials.prepareEntryCredentials({
1182
- cwd,
1183
- resolveDockerPorts: "readonly"
1184
- });
1185
- if (appName) logger$12.log(`📦 App: ${appName}`);
1186
- const secretCount = Object.keys(credentials).filter((k) => k !== "PORT").length;
1187
- if (secretCount > 0) logger$12.log(`🔐 Loaded ${secretCount} secret(s)`);
1188
- const preloadDir = (0, node_path.join)(cwd, ".gkm");
1189
- await (0, node_fs_promises.mkdir)(preloadDir, { recursive: true });
1190
- const preloadPath = (0, node_path.join)(preloadDir, "credentials-preload.mjs");
1191
- await require_credentials.createCredentialsPreload(preloadPath, secretsJsonPath);
1192
- const [cmd, ...args] = commandArgs;
1193
- logger$12.log(`🚀 Running: ${[cmd, ...args].join(" ")}`);
1194
- const nodeOptions = [(process.env.NODE_OPTIONS ?? "").replace(/--import[= ]\S+/g, "").trim(), `--import=${preloadPath}`].filter(Boolean).join(" ");
1195
- const child = (0, node_child_process.spawn)(cmd, args, {
1196
- cwd,
1028
+ const turboEnv = {
1029
+ ...process.env,
1030
+ ...secretsEnv,
1031
+ ...dependencyEnv,
1032
+ NODE_ENV: "development",
1033
+ ...configPath ? { GKM_CONFIG_PATH: configPath } : {}
1034
+ };
1035
+ logger$11.log("\n🏃 Starting turbo run dev...\n");
1036
+ const turboProcess = (0, node_child_process.spawn)("pnpm", [
1037
+ "turbo",
1038
+ "run",
1039
+ "dev",
1040
+ ...turboFilter
1041
+ ], {
1042
+ cwd: workspace.root,
1197
1043
  stdio: "inherit",
1198
- env: {
1199
- ...process.env,
1200
- ...credentials,
1201
- NODE_OPTIONS: nodeOptions
1202
- }
1044
+ env: turboEnv,
1045
+ detached: true
1203
1046
  });
1204
- const exitCode = await new Promise((resolve) => {
1205
- child.on("close", (code) => resolve(code ?? 0));
1206
- child.on("error", (error) => {
1207
- logger$12.error(`Failed to run command: ${error.message}`);
1208
- resolve(1);
1047
+ let isShuttingDown = false;
1048
+ const shutdown = () => {
1049
+ if (isShuttingDown) return;
1050
+ isShuttingDown = true;
1051
+ logger$11.log("\n🛑 Shutting down workspace...");
1052
+ const pid = turboProcess.pid;
1053
+ if (pid) try {
1054
+ process.kill(-pid, "SIGTERM");
1055
+ } catch {
1056
+ try {
1057
+ process.kill(pid, "SIGTERM");
1058
+ } catch {}
1059
+ }
1060
+ setTimeout(() => {
1061
+ if (pid) try {
1062
+ process.kill(-pid, "SIGKILL");
1063
+ } catch {
1064
+ try {
1065
+ process.kill(pid, "SIGKILL");
1066
+ } catch {}
1067
+ }
1068
+ process.exit(0);
1069
+ }, 3e3);
1070
+ };
1071
+ process.on("SIGINT", shutdown);
1072
+ process.on("SIGTERM", shutdown);
1073
+ return new Promise((resolve, reject) => {
1074
+ turboProcess.on("error", (error) => {
1075
+ logger$11.error("❌ Turbo error:", error);
1076
+ reject(error);
1077
+ });
1078
+ turboProcess.on("exit", (code) => {
1079
+ if (code !== null && code !== 0) reject(/* @__PURE__ */ new Error(`Turbo exited with code ${code}`));
1080
+ else resolve();
1209
1081
  });
1210
1082
  });
1211
- if (exitCode !== 0) process.exit(exitCode);
1212
1083
  }
1213
- //#endregion
1214
- //#region src/dev/index.ts
1215
- const logger$11 = console;
1216
1084
  /**
1217
- * Normalize telescope configuration
1218
- * @internal Exported for testing
1085
+ * Run any TypeScript file with secret injection.
1086
+ * Does not require gkm.config.ts.
1219
1087
  */
1220
- function normalizeTelescopeConfig(config) {
1221
- if (config === false) return;
1222
- if (typeof config === "string") {
1223
- const { path: telescopePath, importPattern: telescopeImportPattern } = require_config.parseModuleConfig(config, "telescope");
1224
- return {
1225
- enabled: true,
1226
- telescopePath,
1227
- telescopeImportPattern,
1228
- path: "/__telescope",
1229
- ignore: [],
1230
- recordBody: true,
1231
- maxEntries: 1e3,
1232
- websocket: true
1233
- };
1234
- }
1235
- if (!(config === true || config === void 0 || config.enabled !== false)) return;
1236
- const telescopeConfig = typeof config === "object" ? config : {};
1237
- return {
1238
- enabled: true,
1239
- path: telescopeConfig.path ?? "/__telescope",
1240
- ignore: telescopeConfig.ignore ?? [],
1241
- recordBody: telescopeConfig.recordBody ?? true,
1242
- maxEntries: telescopeConfig.maxEntries ?? 1e3,
1243
- websocket: telescopeConfig.websocket ?? true
1088
+ async function entryDevCommand(options) {
1089
+ const { entry, watch = true } = options;
1090
+ if (!entry) throw new Error("--entry requires a file path");
1091
+ const entryPath = (0, node_path.resolve)(process.cwd(), entry);
1092
+ if (!(0, node_fs.existsSync)(entryPath)) throw new Error(`Entry file not found: ${entryPath}`);
1093
+ const defaultEnv = require_credentials.loadEnvFiles(".env");
1094
+ if (defaultEnv.loaded.length > 0) logger$11.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
1095
+ const { credentials, resolvedPort, secretsJsonPath, appName } = await require_credentials.prepareEntryCredentials({ explicitPort: options.portExplicit ? options.port : void 0 });
1096
+ if (appName) logger$11.log(`📦 App: ${appName} (port ${resolvedPort})`);
1097
+ logger$11.log(`🚀 Starting entry file: ${entry} on port ${resolvedPort}`);
1098
+ if (Object.keys(credentials).length > 1) logger$11.log(`🔐 Loaded ${Object.keys(credentials).length - 1} secret(s) + PORT`);
1099
+ const wrapperDir = (0, node_path.join)(process.cwd(), ".gkm");
1100
+ await (0, node_fs_promises.mkdir)(wrapperDir, { recursive: true });
1101
+ const wrapperPath = (0, node_path.join)(wrapperDir, "entry-wrapper.ts");
1102
+ await require_credentials.createEntryWrapper(wrapperPath, entryPath, secretsJsonPath);
1103
+ const runner = new EntryRunner(wrapperPath, entryPath, watch, resolvedPort);
1104
+ await runner.start();
1105
+ let isShuttingDown = false;
1106
+ const shutdown = () => {
1107
+ if (isShuttingDown) return;
1108
+ isShuttingDown = true;
1109
+ logger$11.log("\n🛑 Shutting down...");
1110
+ runner.stop();
1111
+ process.exit(0);
1244
1112
  };
1113
+ process.on("SIGINT", shutdown);
1114
+ process.on("SIGTERM", shutdown);
1115
+ await new Promise(() => {});
1245
1116
  }
1246
1117
  /**
1247
- * Normalize studio configuration
1248
- * @internal Exported for testing
1118
+ * Runs and watches a TypeScript entry file using tsx.
1249
1119
  */
1250
- function normalizeStudioConfig(config) {
1251
- if (config === false) return;
1252
- if (typeof config === "string") {
1253
- const { path: studioPath, importPattern: studioImportPattern } = require_config.parseModuleConfig(config, "studio");
1254
- return {
1255
- enabled: true,
1256
- studioPath,
1257
- studioImportPattern,
1258
- path: "/__studio",
1259
- schema: "public"
1120
+ var EntryRunner = class {
1121
+ wrapperPath;
1122
+ entryPath;
1123
+ watch;
1124
+ port;
1125
+ childProcess = null;
1126
+ watcher = null;
1127
+ isRunning = false;
1128
+ constructor(wrapperPath, entryPath, watch, port) {
1129
+ this.wrapperPath = wrapperPath;
1130
+ this.entryPath = entryPath;
1131
+ this.watch = watch;
1132
+ this.port = port;
1133
+ }
1134
+ async start() {
1135
+ await this.runProcess();
1136
+ if (this.watch) {
1137
+ const watchDir = (0, node_path.dirname)(this.entryPath);
1138
+ this.watcher = chokidar.default.watch(watchDir, {
1139
+ ignored: /(^|[/\\])\../,
1140
+ persistent: true,
1141
+ ignoreInitial: true
1142
+ });
1143
+ let restartTimeout = null;
1144
+ this.watcher.on("change", (path) => {
1145
+ logger$11.log(`📝 File changed: ${path}`);
1146
+ if (restartTimeout) clearTimeout(restartTimeout);
1147
+ restartTimeout = setTimeout(async () => {
1148
+ logger$11.log("🔄 Restarting...");
1149
+ await this.restart();
1150
+ }, 300);
1151
+ });
1152
+ logger$11.log(`👀 Watching for changes in: ${watchDir}`);
1153
+ }
1154
+ }
1155
+ async runProcess() {
1156
+ const env = {
1157
+ ...process.env,
1158
+ PORT: String(this.port)
1260
1159
  };
1160
+ this.childProcess = (0, node_child_process.spawn)("npx", ["tsx", this.wrapperPath], {
1161
+ stdio: "inherit",
1162
+ env,
1163
+ detached: true
1164
+ });
1165
+ this.isRunning = true;
1166
+ this.childProcess.on("error", (error) => {
1167
+ logger$11.error("❌ Process error:", error);
1168
+ });
1169
+ this.childProcess.on("exit", (code) => {
1170
+ if (code !== null && code !== 0 && code !== 143) logger$11.error(`❌ Process exited with code ${code}`);
1171
+ this.isRunning = false;
1172
+ });
1173
+ await new Promise((resolve) => setTimeout(resolve, 500));
1174
+ if (this.isRunning) {
1175
+ logger$11.log("");
1176
+ logger$11.log(` \x1b[32m✓ Ready\x1b[0m at \x1b[36mhttp://localhost:${this.port}\x1b[0m`);
1177
+ logger$11.log("");
1178
+ }
1261
1179
  }
1262
- if (!(config === true || config === void 0 || config.enabled !== false)) return;
1263
- const studioConfig = typeof config === "object" ? config : {};
1264
- return {
1265
- enabled: true,
1266
- path: studioConfig.path ?? "/__studio",
1267
- schema: studioConfig.schema ?? "public"
1268
- };
1269
- }
1180
+ async restart() {
1181
+ this.stopProcess();
1182
+ await new Promise((resolve) => setTimeout(resolve, 500));
1183
+ await this.runProcess();
1184
+ }
1185
+ stop() {
1186
+ this.watcher?.close();
1187
+ this.stopProcess();
1188
+ }
1189
+ stopProcess() {
1190
+ if (this.childProcess && this.isRunning) {
1191
+ const pid = this.childProcess.pid;
1192
+ if (pid) try {
1193
+ process.kill(-pid, "SIGTERM");
1194
+ } catch {
1195
+ try {
1196
+ process.kill(pid, "SIGTERM");
1197
+ } catch {}
1198
+ }
1199
+ this.childProcess = null;
1200
+ this.isRunning = false;
1201
+ }
1202
+ }
1203
+ };
1270
1204
  /**
1271
- * Normalize hooks configuration
1205
+ * Generate the content of the dev server entry file (server.ts).
1206
+ * Uses dynamic import for createApp so Credentials are populated
1207
+ * before any app modules evaluate.
1272
1208
  * @internal Exported for testing
1273
1209
  */
1274
- function normalizeHooksConfig(config, cwd = process.cwd()) {
1275
- if (!config?.server) return;
1276
- const serverPath = config.server.endsWith(".ts") ? config.server : `${config.server}.ts`;
1277
- return { serverHooksPath: (0, node_path.resolve)(cwd, serverPath) };
1210
+ function generateServerEntryContent(options) {
1211
+ const { secretsJsonPath, runtime = "node", enableOpenApi = false, appImportPath = "./app.js", selfServing = false } = options;
1212
+ const credentialsInjection = secretsJsonPath ? `import { existsSync, readFileSync } from 'node:fs';
1213
+
1214
+ // Inject dev secrets via globalThis (must happen before app import)
1215
+ const secretsPath = '${secretsJsonPath}';
1216
+ if (existsSync(secretsPath)) {
1217
+ const __secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
1218
+ globalThis.__gkm_credentials__ = __secrets;
1219
+ Object.assign(process.env, __secrets);
1278
1220
  }
1221
+
1222
+ ` : "";
1223
+ const serveCode = runtime === "bun" ? `Bun.serve({
1224
+ port,
1225
+ fetch: app.fetch,
1226
+ });` : `const { serve } = await import('@hono/node-server');
1227
+ const server = serve({
1228
+ fetch: app.fetch,
1229
+ port,
1230
+ });
1231
+ // Inject WebSocket support if available
1232
+ const injectWs = (app as any).__injectWebSocket;
1233
+ if (injectWs) {
1234
+ injectWs(server);
1235
+ console.log('🔌 Telescope real-time updates enabled');
1236
+ }`;
1237
+ if (selfServing) return `#!/usr/bin/env node
1279
1238
  /**
1280
- * Normalize production configuration
1281
- * @internal Exported for testing
1282
- */
1283
- function normalizeProductionConfig(cliProduction, configProduction) {
1284
- if (!cliProduction) return;
1285
- const config = configProduction ?? {};
1286
- return {
1287
- enabled: true,
1288
- bundle: config.bundle ?? true,
1289
- minify: config.minify ?? true,
1290
- healthCheck: config.healthCheck ?? "/health",
1291
- gracefulShutdown: config.gracefulShutdown ?? true,
1292
- external: config.external ?? [],
1293
- subscribers: config.subscribers ?? "exclude",
1294
- openapi: config.openapi ?? false,
1295
- optimizedHandlers: config.optimizedHandlers ?? true
1296
- };
1297
- }
1239
+ * Development server entry point for a surface that serves itself
1240
+ * This file is auto-generated by 'gkm dev'
1241
+ */
1242
+ ${credentialsInjection}
1243
+ const port = process.argv.includes('--port')
1244
+ ? Number.parseInt(process.argv[process.argv.indexOf('--port') + 1])
1245
+ : 3000;
1246
+
1247
+ // Dynamic import so Credentials are populated before the surface evaluates
1248
+ const { app } = await import('${appImportPath}');
1249
+
1250
+ ${serveCode.replace(/^ {4}/gm, "")}
1251
+ console.log(\`Server started on port \${port}\`);
1252
+ `;
1253
+ return `#!/usr/bin/env node
1298
1254
  /**
1299
- * Get production config from GkmConfig
1300
- * @internal
1301
- */
1302
- function getProductionConfigFromGkm(config) {
1303
- const serverConfig = config.providers?.server;
1304
- if (typeof serverConfig === "object") return serverConfig.production;
1255
+ * Development server entry point
1256
+ * This file is auto-generated by 'gkm dev'
1257
+ */
1258
+ ${credentialsInjection}
1259
+ const port = process.argv.includes('--port')
1260
+ ? Number.parseInt(process.argv[process.argv.indexOf('--port') + 1])
1261
+ : 3000;
1262
+
1263
+ // Dynamic import so Credentials are populated before env.ts evaluates
1264
+ const { createApp } = await import('${appImportPath}');
1265
+
1266
+ // createApp is async to support optional WebSocket setup
1267
+ const { app, start } = await createApp(undefined, ${enableOpenApi});
1268
+
1269
+ // Start the server
1270
+ start({
1271
+ port,
1272
+ serve: async (app, port) => {
1273
+ ${serveCode}
1274
+ },
1275
+ }).catch((error) => {
1276
+ console.error('Failed to start server:', error);
1277
+ process.exit(1);
1278
+ });
1279
+ `;
1305
1280
  }
1306
- async function devCommand(options) {
1307
- if (options.entry) return entryDevCommand(options);
1308
- const defaultEnv = require_credentials.loadEnvFiles(".env");
1309
- if (defaultEnv.loaded.length > 0) logger$11.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
1310
- const appName = require_config.getAppNameFromCwd();
1311
- let config;
1312
- let appRoot = process.cwd();
1313
- let secretsRoot = process.cwd();
1314
- let workspaceAppName;
1315
- let workspaceAppPort;
1316
- let workspace;
1317
- if (appName) try {
1318
- const appConfig = await require_config.loadAppConfig();
1319
- config = appConfig.gkmConfig;
1320
- appRoot = appConfig.appRoot;
1321
- secretsRoot = appConfig.workspaceRoot;
1322
- workspaceAppName = appConfig.appName;
1323
- workspaceAppPort = appConfig.app.port;
1324
- workspace = appConfig.workspace;
1325
- logger$11.log(`📦 Running app: ${appConfig.appName} on port ${workspaceAppPort}`);
1326
- if (appConfig.app.entry) {
1327
- logger$11.log(`📄 Using entry point: ${appConfig.app.entry}`);
1328
- return entryDevCommand({
1329
- ...options,
1330
- entry: appConfig.app.entry,
1331
- port: workspaceAppPort,
1332
- portExplicit: true
1333
- });
1281
+ var DevServer = class {
1282
+ provider;
1283
+ requestedPort;
1284
+ portExplicit;
1285
+ enableOpenApi;
1286
+ telescope;
1287
+ studio;
1288
+ runtime;
1289
+ appRoot;
1290
+ secretsJsonPath;
1291
+ selfServing;
1292
+ serverProcess = null;
1293
+ isRunning = false;
1294
+ actualPort;
1295
+ startTime = Date.now();
1296
+ constructor(provider, requestedPort, portExplicit, enableOpenApi, telescope, studio, runtime = "node", appRoot = process.cwd(), secretsJsonPath, selfServing = false) {
1297
+ this.provider = provider;
1298
+ this.requestedPort = requestedPort;
1299
+ this.portExplicit = portExplicit;
1300
+ this.enableOpenApi = enableOpenApi;
1301
+ this.telescope = telescope;
1302
+ this.studio = studio;
1303
+ this.runtime = runtime;
1304
+ this.appRoot = appRoot;
1305
+ this.secretsJsonPath = secretsJsonPath;
1306
+ this.selfServing = selfServing;
1307
+ this.actualPort = requestedPort;
1308
+ }
1309
+ async start() {
1310
+ this.startTime = Date.now();
1311
+ if (this.isRunning) await this.stop();
1312
+ if (this.portExplicit) {
1313
+ if (!await require_credentials.isPortAvailable(this.requestedPort)) throw new Error(`Port ${this.requestedPort} is already in use. Either stop the process using that port or omit -p/--port to auto-select an available port.`);
1314
+ this.actualPort = this.requestedPort;
1315
+ } else {
1316
+ this.actualPort = await require_credentials.findAvailablePort(this.requestedPort);
1317
+ if (this.actualPort !== this.requestedPort) logger$11.log(`ℹ️ Port ${this.requestedPort} was in use, using port ${this.actualPort} instead`);
1334
1318
  }
1335
- } catch {
1336
- const loadedConfig = await require_config.loadWorkspaceConfig();
1337
- if (loadedConfig.type === "workspace") {
1338
- logger$11.log("📦 Detected workspace configuration");
1339
- return workspaceDevCommand(loadedConfig.workspace, options);
1319
+ const serverEntryPath = (0, node_path.join)(this.appRoot, ".gkm", this.provider, "server.ts");
1320
+ await this.createServerEntry();
1321
+ logger$11.log(`\n⏳ Starting server...`);
1322
+ this.serverProcess = (0, node_child_process.spawn)("npx", [
1323
+ "tsx",
1324
+ serverEntryPath,
1325
+ "--port",
1326
+ this.actualPort.toString()
1327
+ ], {
1328
+ stdio: "inherit",
1329
+ env: {
1330
+ ...process.env,
1331
+ NODE_ENV: "development"
1332
+ },
1333
+ detached: true
1334
+ });
1335
+ this.isRunning = true;
1336
+ this.serverProcess.on("error", (error) => {
1337
+ logger$11.error("❌ Server error:", error);
1338
+ });
1339
+ this.serverProcess.on("exit", (code, signal) => {
1340
+ if (code !== null && code !== 0 && signal !== "SIGTERM") logger$11.error(`❌ Server exited with code ${code}`);
1341
+ this.isRunning = false;
1342
+ });
1343
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1344
+ if (this.isRunning) {
1345
+ const base = `http://localhost:${this.actualPort}`;
1346
+ const lines = [` Local: ${base}`];
1347
+ if (this.enableOpenApi) lines.push(` API Docs: ${base}/__docs`);
1348
+ if (this.telescope) lines.push(` Telescope: ${base}${this.telescope.path}`);
1349
+ if (this.studio) lines.push(` Studio: ${base}${this.studio.path}`);
1350
+ const maxLen = Math.max(...lines.map((l) => l.length));
1351
+ const pad = (s) => s.padEnd(maxLen);
1352
+ const border = "─".repeat(maxLen + 2);
1353
+ logger$11.log("");
1354
+ logger$11.log(` \x1b[32m✓ Ready\x1b[0m in ${((Date.now() - this.startTime) / 1e3).toFixed(1)}s`);
1355
+ logger$11.log("");
1356
+ logger$11.log(` ┌${border}┐`);
1357
+ for (const line of lines) logger$11.log(` │ ${pad(line)} │`);
1358
+ logger$11.log(` └${border}┘`);
1359
+ logger$11.log("");
1360
+ }
1361
+ }
1362
+ async stop() {
1363
+ const port = this.actualPort;
1364
+ if (this.serverProcess && this.isRunning) {
1365
+ const pid = this.serverProcess.pid;
1366
+ if (pid) try {
1367
+ process.kill(-pid, "SIGKILL");
1368
+ } catch {
1369
+ try {
1370
+ process.kill(pid, "SIGKILL");
1371
+ } catch {}
1372
+ }
1373
+ this.serverProcess = null;
1374
+ this.isRunning = false;
1340
1375
  }
1341
- config = loadedConfig.raw;
1376
+ this.killProcessesOnPort(port);
1342
1377
  }
1343
- else {
1344
- const loadedConfig = await require_config.loadWorkspaceConfig();
1345
- if (loadedConfig.type === "workspace") {
1346
- logger$11.log("📦 Detected workspace configuration");
1347
- return workspaceDevCommand(loadedConfig.workspace, options);
1378
+ killProcessesOnPort(port) {
1379
+ try {
1380
+ (0, node_child_process.execSync)(`lsof -ti tcp:${port} -sTCP:LISTEN | xargs kill -9 2>/dev/null || true`, { stdio: "ignore" });
1381
+ } catch {}
1382
+ }
1383
+ async restart() {
1384
+ const portToReuse = this.actualPort;
1385
+ await this.stop();
1386
+ let attempts = 0;
1387
+ while (attempts < 30) {
1388
+ if (await require_credentials.isPortAvailable(portToReuse)) break;
1389
+ await new Promise((resolve) => setTimeout(resolve, 100));
1390
+ attempts++;
1348
1391
  }
1349
- config = loadedConfig.raw;
1350
- workspace = loadedConfig.workspace;
1392
+ this.requestedPort = portToReuse;
1393
+ await this.start();
1351
1394
  }
1352
- if (config.env) {
1353
- const { loaded, missing } = require_credentials.loadEnvFiles(config.env, appRoot);
1354
- if (loaded.length > 0) logger$11.log(`📦 Loaded env: ${loaded.join(", ")}`);
1355
- if (missing.length > 0) logger$11.warn(`⚠️ Missing env files: ${missing.join(", ")}`);
1395
+ async createServerEntry() {
1396
+ const { writeFile: fsWriteFile } = await import("node:fs/promises");
1397
+ await fsWriteFile((0, node_path.join)(this.appRoot, ".gkm", this.provider, "server.ts"), generateServerEntryContent({
1398
+ secretsJsonPath: this.secretsJsonPath,
1399
+ runtime: this.runtime,
1400
+ enableOpenApi: this.enableOpenApi,
1401
+ selfServing: this.selfServing
1402
+ }));
1356
1403
  }
1357
- const resolved = resolveProviders(config, { provider: "server" });
1358
- logger$11.log("🚀 Starting development server...");
1359
- logger$11.log(`Loading constructs from: ${config.constructs}`);
1360
- const telescope = normalizeTelescopeConfig(config.telescope);
1361
- if (telescope) logger$11.log(`🔭 Telescope enabled at ${telescope.path}`);
1362
- const studio = normalizeStudioConfig(config.studio);
1363
- if (studio) logger$11.log(`🗄️ Studio enabled at ${studio.path}`);
1364
- const hooks = normalizeHooksConfig(config.hooks, appRoot);
1365
- if (hooks) logger$11.log(`🪝 Server hooks enabled from ${config.hooks?.server}`);
1366
- const enableOpenApi = require_openapi.resolveOpenApiConfig(config).enabled || resolved.enableOpenApi;
1367
- if (enableOpenApi) logger$11.log(`📄 OpenAPI output: ${require_openapi.OPENAPI_OUTPUT_PATH}`);
1368
- const buildContext = {
1369
- telescope,
1370
- studio,
1371
- hooks
1372
- };
1373
- await buildServer(config, buildContext, resolved.providers[0], enableOpenApi, appRoot);
1374
- if (enableOpenApi) await require_openapi.generateOpenApiFrom(config.constructs, { openapi: config.openapi });
1375
- const runtime = config.runtime ?? "node";
1376
- let secretsJsonPath;
1377
- const appSecrets = await require_credentials.loadSecretsForApp(secretsRoot, config.stages.local, workspaceAppName);
1378
- if (workspace && require_credentials.usesConstructs(workspace)) {
1379
- const reconciled = await require_credentials.reconcileWorkspace(workspace, { stage: workspace.stages.local });
1380
- if (reconciled.changed && reconciled.plan.containers.length > 0) {
1381
- logger$11.log(`🐳 Services: ${reconciled.plan.containers.join(", ")}`);
1382
- for (const [container, address] of Object.entries(reconciled.addresses)) logger$11.log(` ${container}: ${address}`);
1404
+ };
1405
+ //#endregion
1406
+ //#region src/generators/CronGenerator.ts
1407
+ var CronGenerator = class extends require_discover.ConstructGenerator {
1408
+ async build(context, constructs, outputDir, options) {
1409
+ const provider = options?.provider || "aws-lambda";
1410
+ const logger = console;
1411
+ const cronInfos = [];
1412
+ if (provider === "server") {
1413
+ await this.generateServerCronsFile(outputDir, constructs);
1414
+ logger.log(`Generated server crons file with ${constructs.length} crons`);
1415
+ return cronInfos;
1383
1416
  }
1384
- Object.assign(appSecrets, reconciled.env);
1385
- } else {
1386
- const resolvedPorts = await require_credentials.resolveServicePorts(secretsRoot);
1387
- if (Object.keys(resolvedPorts.ports).length > 0) {
1388
- const rewritten = require_credentials.rewriteUrlsWithPorts(appSecrets, resolvedPorts);
1389
- Object.assign(appSecrets, rewritten);
1390
- logger$11.log(`🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`);
1417
+ if (constructs.length === 0 || provider !== "aws-lambda") return cronInfos;
1418
+ const cronsDir = (0, node_path.join)(outputDir, "crons");
1419
+ await (0, node_fs_promises.mkdir)(cronsDir, { recursive: true });
1420
+ for (const { key, construct, path } of constructs) {
1421
+ const handlerFile = await this.generateCronHandler(cronsDir, path.relative, key, context, construct.owner);
1422
+ cronInfos.push({
1423
+ name: key,
1424
+ handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
1425
+ schedule: construct.schedule || "rate(1 hour)",
1426
+ timeout: construct.timeout,
1427
+ memorySize: construct.memorySize,
1428
+ environment: await construct.getEnvironment({ markOptional: context.markOptional }),
1429
+ dependencies: construct.constructs
1430
+ });
1431
+ logger.log(`Generated cron handler: ${key}`);
1391
1432
  }
1433
+ return cronInfos;
1392
1434
  }
1393
- if (workspace && workspaceAppName) {
1394
- const depEnv = require_workspace.getDependencyEnvVars(workspace, workspaceAppName);
1395
- Object.assign(appSecrets, depEnv);
1396
- }
1397
- if (Object.keys(appSecrets).length > 0) {
1398
- const secretsDir = (0, node_path.join)(secretsRoot, ".gkm");
1399
- await (0, node_fs_promises.mkdir)(secretsDir, { recursive: true });
1400
- const secretsFileName = workspaceAppName ? `dev-secrets-${workspaceAppName}.json` : "dev-secrets.json";
1401
- secretsJsonPath = (0, node_path.join)(secretsDir, secretsFileName);
1402
- await (0, node_fs_promises.writeFile)(secretsJsonPath, JSON.stringify(appSecrets, null, 2));
1403
- logger$11.log(`🔐 Loaded ${Object.keys(appSecrets).length} secret(s)`);
1435
+ isConstruct(value) {
1436
+ return _geekmidas_constructs_crons.Cron.isCron(value);
1404
1437
  }
1405
- const devServer = new DevServer(resolved.providers[0], options.port ?? workspaceAppPort ?? 3e3, options.portExplicit ?? false, enableOpenApi, telescope, studio, runtime, appRoot, secretsJsonPath);
1406
- await devServer.start();
1407
- const hooksFile = (config.hooks?.server?.split("#"))?.[0];
1408
- const normalizedPatterns = [config.constructs, ...hooksFile ? [hooksFile.endsWith(".ts") ? hooksFile : `${hooksFile}.ts`] : []].flat().filter((p) => typeof p === "string").map((p) => p.startsWith("./") ? p.slice(2) : p);
1409
- logger$11.log(`👀 Watching for changes in: ${normalizedPatterns.join(", ")}`);
1410
- const resolvedFiles = await (0, fast_glob.default)(normalizedPatterns, {
1411
- cwd: appRoot,
1412
- absolute: false,
1413
- onlyFiles: true
1414
- });
1415
- const dirsToWatch = [...new Set(resolvedFiles.map((f) => {
1416
- return f.split("/").slice(0, -1).join("/");
1417
- }))];
1418
- logger$11.log(`📁 Found ${resolvedFiles.length} files in ${dirsToWatch.length} directories`);
1419
- const watcher = chokidar.default.watch([...resolvedFiles, ...dirsToWatch], {
1420
- ignored: /(^|[/\\])\../,
1421
- persistent: true,
1422
- ignoreInitial: true,
1423
- cwd: appRoot
1424
- });
1425
- watcher.on("ready", () => {
1426
- logger$11.log("🔍 File watcher ready");
1427
- });
1428
- watcher.on("error", (error) => {
1429
- logger$11.error("❌ Watcher error:", error);
1430
- });
1431
- let rebuildTimeout = null;
1432
- watcher.on("change", async (path) => {
1433
- logger$11.log(`📝 File changed: ${path}`);
1434
- if (rebuildTimeout) clearTimeout(rebuildTimeout);
1435
- rebuildTimeout = setTimeout(async () => {
1436
- try {
1437
- logger$11.log("🔄 Rebuilding...");
1438
- await buildServer(config, buildContext, resolved.providers[0], enableOpenApi, appRoot, true);
1439
- if (enableOpenApi) await require_openapi.generateOpenApiFrom(config.constructs, {
1440
- openapi: config.openapi,
1441
- silent: true,
1442
- bustCache: true
1443
- });
1444
- logger$11.log("✅ Rebuild complete, restarting server...");
1445
- await devServer.restart();
1446
- } catch (error) {
1447
- const err = error;
1448
- logger$11.error(`❌ Rebuild failed: ${err.message}`);
1449
- if (err.stack) logger$11.error(err.stack);
1450
- }
1451
- }, 300);
1452
- });
1453
- let isShuttingDown = false;
1454
- const shutdown = () => {
1455
- if (isShuttingDown) return;
1456
- isShuttingDown = true;
1457
- logger$11.log("\n🛑 Shutting down...");
1458
- Promise.all([watcher.close(), devServer.stop()]).catch((err) => {
1459
- logger$11.error("Error during shutdown:", err);
1460
- }).finally(() => {
1461
- process.exit(0);
1462
- });
1463
- };
1464
- process.on("SIGINT", shutdown);
1465
- process.on("SIGTERM", shutdown);
1438
+ /**
1439
+ * The crons file a server entry imports.
1440
+ *
1441
+ * Shaped after `generateServerSubscribersFile`: one module, every cron, a
1442
+ * `setupCrons` the generated entry calls beside `setupSubscribers`.
1443
+ *
1444
+ * It picks its scheduler from what the process can reach. With a Postgres it
1445
+ * uses pg-boss, whose schedules live in the database — so a deployment
1446
+ * running four replicas fires each job once, which is the thing a timer in
1447
+ * every process gets wrong and does not report. With no database it falls
1448
+ * back to timers and says plainly that it is doing so.
1449
+ */
1450
+ async generateServerCronsFile(outputDir, crons) {
1451
+ await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
1452
+ const cronsPath = (0, node_path.join)(outputDir, "crons.ts");
1453
+ const importsByFile = /* @__PURE__ */ new Map();
1454
+ for (const { path, key } of crons) {
1455
+ const importPath = (0, node_path.relative)((0, node_path.dirname)(cronsPath), path.relative).replace(/\.ts$/, ".js");
1456
+ if (!importsByFile.has(importPath)) importsByFile.set(importPath, []);
1457
+ importsByFile.get(importPath)?.push(key);
1458
+ }
1459
+ const content = `/**
1460
+ * Generated cron setup.
1461
+ *
1462
+ * Schedules every cron this app declares, in the process that serves it.
1463
+ *
1464
+ * The schedule lives in Postgres, through pg-boss, in the database the worker
1465
+ * named with \`.database(db)\`. That is what makes running several replicas
1466
+ * safe: each job fires once, where a timer in every process fires it once per
1467
+ * replica and reports nothing.
1468
+ *
1469
+ * No connection string appears here. The construct that owns the database is
1470
+ * the only thing that knows its key, and it is resolved through service
1471
+ * discovery like any other dependency.
1472
+ */
1473
+ import type { EnvironmentParser } from '@geekmidas/envkit';
1474
+ import type { Logger } from '@geekmidas/logger';
1475
+ import { toCronExpression } from '@geekmidas/constructs/crons';
1476
+ import { ServiceDiscovery } from '@geekmidas/services';
1477
+ ${Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n")}
1478
+
1479
+ const crons = [
1480
+ ${crons.map(({ key }) => ` { name: '${key}', cron: ${key} }`).join(",\n")}
1481
+ ];
1482
+
1483
+ type Stoppable = { stop: () => Promise<void> | void };
1484
+ const running: Stoppable[] = [];
1485
+
1486
+ export async function setupCrons(
1487
+ envParser: EnvironmentParser<any>,
1488
+ logger: Logger,
1489
+ ): Promise<void> {
1490
+ if (crons.length === 0) {
1491
+ return;
1492
+ }
1493
+
1494
+ const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
1495
+
1496
+ // Resolved once per cron rather than per firing: registering services on
1497
+ // every tick would reconnect a database every minute.
1498
+ const prepared = [];
1499
+ for (const { name, cron } of crons) {
1500
+ try {
1501
+ const schedule = toCronExpression(cron.schedule);
1502
+ const services = cron.services.length > 0
1503
+ ? await serviceDiscovery.register(cron.services)
1504
+ : {};
1505
+
1506
+ prepared.push({ name, cron, schedule, services });
1507
+ } catch (error) {
1508
+ // An unrepresentable schedule is a build-time mistake, and failing the
1509
+ // process for it would take the HTTP server down with it.
1510
+ logger.error({ error, cron: name }, 'Cron has no runnable schedule, skipping');
1511
+ }
1512
+ }
1513
+
1514
+ const run = async (entry: (typeof prepared)[number]) => {
1515
+ try {
1516
+ await entry.cron.handler({
1517
+ input: undefined,
1518
+ services: entry.services,
1519
+ logger: entry.cron.logger,
1520
+ });
1521
+ } catch (error) {
1522
+ logger.error({ error, cron: entry.name }, 'Cron failed');
1523
+ }
1524
+ };
1525
+
1526
+ // The app's one pg-boss: the events broker's, as the broker's role, in the
1527
+ // broker's schema. A server fires its own crons, and a process scheduling in
1528
+ // memory fires each job once per replica — so the schedule lives in
1529
+ // Postgres. It used to be a second pg-boss on the worker's database, as the
1530
+ // runtime role, which can neither create a schema nor use the broker's.
1531
+ const { url } = envParser
1532
+ .create((get) => ({
1533
+ url: get('EVENT_SUBSCRIBER_CONNECTION_STRING').string().optional(),
1534
+ }))
1535
+ .parse();
1536
+
1537
+ if (!url?.startsWith('pgboss://')) {
1538
+ logger.error(
1539
+ { crons: prepared.map(({ name }) => name) },
1540
+ 'These crons have nowhere to keep their schedule. On a server they are ' +
1541
+ 'scheduled through the pg-boss events broker ' +
1542
+ '(EVENT_SUBSCRIBER_CONNECTION_STRING), and ' +
1543
+ (url ? 'the broker configured is not pg-boss.' : 'none is configured.'),
1544
+ );
1545
+ return;
1546
+ }
1547
+
1548
+ const { PgBossConnection } = await import('@geekmidas/events/pgboss');
1549
+ const connection = await PgBossConnection.fromConnectionString(url);
1550
+ const boss = connection.instance!;
1551
+
1552
+ // What this app declares now. Anything else under the prefix belonged to a
1553
+ // cron since renamed or deleted, and would keep firing at a handler that has
1554
+ // gone.
1555
+ //
1556
+ // The prefix is \`cron.\` rather than \`cron:\`: pg-boss accepts
1557
+ // alphanumerics, underscores, hyphens, periods and slashes in a queue name,
1558
+ // and rejects a colon outright.
1559
+ const declared = new Set(prepared.map(({ name }) => \`cron.\${name}\`));
1560
+ for (const existing of await boss.getSchedules()) {
1561
+ if (existing.name.startsWith('cron.') && !declared.has(existing.name)) {
1562
+ await boss.unschedule(existing.name);
1563
+ logger.info({ cron: existing.name }, 'Removed a schedule nothing declares');
1564
+ }
1565
+ }
1566
+
1567
+ for (const entry of prepared) {
1568
+ const queue = \`cron.\${entry.name}\`;
1569
+ await boss.createQueue(queue);
1570
+ // UTC, so this agrees with the schedule a deploy target would have made.
1571
+ await boss.schedule(queue, entry.schedule, undefined, { tz: 'UTC' });
1572
+ await boss.work(queue, () => run(entry));
1573
+
1574
+ logger.info({ cron: entry.name, schedule: entry.schedule }, 'Cron scheduled');
1575
+ }
1576
+
1577
+ running.push({ stop: () => connection.close() });
1578
+ logger.info({ count: prepared.length }, 'Crons scheduled');
1466
1579
  }
1467
- /**
1468
- * Generate all dependency environment variables for all apps.
1469
- * Returns a flat object with all {APP_NAME}_URL variables.
1470
- * @internal Exported for testing
1471
- */
1472
- function generateAllDependencyEnvVars(workspace, urlPrefix = "http://localhost") {
1473
- const env = {};
1474
- for (const appName of Object.keys(workspace.apps)) {
1475
- const appEnv = require_workspace.getDependencyEnvVars(workspace, appName, urlPrefix);
1476
- Object.assign(env, appEnv);
1477
- }
1478
- return env;
1580
+
1581
+ export async function stopCrons(): Promise<void> {
1582
+ await Promise.all(running.map((r) => r.stop()));
1583
+ running.length = 0;
1479
1584
  }
1480
- /**
1481
- * Check for port conflicts across all apps.
1482
- * Returns list of conflicts if any ports are duplicated.
1483
- * @internal Exported for testing
1484
- */
1485
- function checkPortConflicts(workspace) {
1486
- const conflicts = [];
1487
- const portToApp = /* @__PURE__ */ new Map();
1488
- for (const [appName, app] of Object.entries(workspace.apps)) {
1489
- const existingApp = portToApp.get(app.port);
1490
- if (existingApp) conflicts.push({
1491
- app1: existingApp,
1492
- app2: appName,
1493
- port: app.port
1494
- });
1495
- else portToApp.set(app.port, appName);
1585
+ `;
1586
+ await (0, node_fs_promises.writeFile)(cronsPath, content);
1587
+ return cronsPath;
1496
1588
  }
1497
- return conflicts;
1498
- }
1499
- const FRONTEND_FRAMEWORKS = {
1500
- nextjs: {
1501
- displayName: "Next.js",
1502
- configFiles: [
1503
- "next.config.js",
1504
- "next.config.ts",
1505
- "next.config.mjs",
1506
- "next.config.cjs"
1507
- ],
1508
- dependency: "next",
1509
- installCommand: "pnpm add next react react-dom"
1510
- },
1511
- remix: {
1512
- displayName: "Remix",
1513
- configFiles: [
1514
- "remix.config.js",
1515
- "remix.config.mjs",
1516
- "remix.config.ts",
1517
- "vite.config.js",
1518
- "vite.config.ts",
1519
- "vite.config.mjs",
1520
- "vite.config.cjs"
1521
- ],
1522
- dependency: "@remix-run/dev",
1523
- installCommand: "pnpm add -D @remix-run/dev"
1524
- },
1525
- vite: {
1526
- displayName: "Vite",
1527
- configFiles: [
1528
- "vite.config.js",
1529
- "vite.config.ts",
1530
- "vite.config.mjs",
1531
- "vite.config.cjs"
1532
- ],
1533
- dependency: "vite",
1534
- installCommand: "pnpm add -D vite"
1535
- },
1536
- "tanstack-start": {
1537
- displayName: "TanStack Start",
1538
- configFiles: [
1539
- "vite.config.js",
1540
- "vite.config.ts",
1541
- "vite.config.mjs",
1542
- "vite.config.cjs"
1543
- ],
1544
- dependency: "@tanstack/react-start",
1545
- installCommand: "pnpm add @tanstack/react-start @tanstack/react-router"
1546
- },
1547
- expo: {
1548
- displayName: "Expo",
1549
- configFiles: [
1550
- "app.config.ts",
1551
- "app.config.js",
1552
- "app.json"
1553
- ],
1554
- dependency: "expo",
1555
- installCommand: "pnpm add expo"
1589
+ async generateCronHandler(outputDir, sourceFile, exportName, context, owner) {
1590
+ const handlerFileName = `${exportName}.ts`;
1591
+ const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
1592
+ const importPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile).replace(/\.ts$/, ".js");
1593
+ const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
1594
+ const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/aws';
1595
+ import { ${exportName} } from '${importPath}';
1596
+ ${runtime.imports}
1597
+ ${runtime.bindings}
1598
+
1599
+ const adapter = new AWSScheduledFunction(envParser, ${exportName});
1600
+
1601
+ export const handler = adapter.handler;
1602
+ `;
1603
+ await (0, node_fs_promises.writeFile)(handlerPath, content);
1604
+ return handlerPath;
1556
1605
  }
1557
1606
  };
1558
- /** Whether a configured framework is one this table knows how to start. */
1559
- function isClientFramework(framework) {
1560
- return framework !== void 0 && framework in FRONTEND_FRAMEWORKS;
1561
- }
1562
- /**
1563
- * Auto-detect the frontend framework by scanning package.json deps and config
1564
- * files. Dependency match wins over config file match because deps are more
1565
- * distinctive (a Remix app has both vite.config.ts AND @remix-run/dev).
1566
- */
1567
- function detectFrontendFramework(fullPath, deps) {
1568
- const order = [
1569
- "expo",
1570
- "tanstack-start",
1571
- "nextjs",
1572
- "remix",
1573
- "vite"
1574
- ];
1575
- for (const name of order) if (deps[FRONTEND_FRAMEWORKS[name].dependency]) return name;
1576
- for (const name of order) if (FRONTEND_FRAMEWORKS[name].configFiles.some((file) => (0, node_fs.existsSync)((0, node_path.join)(fullPath, file)))) return name;
1577
- }
1607
+ //#endregion
1608
+ //#region src/generators/drivers.ts
1578
1609
  /**
1579
- * Validate a frontend app configuration.
1610
+ * Driver registration for generated entry points.
1580
1611
  *
1581
- * If `framework` is provided, validates strictly against that framework's
1582
- * expected config file and dependency. Otherwise auto-detects the framework
1583
- * from package.json and config files. If no recognized framework is found, the
1584
- * app is allowed through with a warning provided it has a `dev` script.
1612
+ * A construct declares that it needs object storage and is handed one URL; the
1613
+ * scheme in that URL picks the driver that builds the client. Which drivers
1614
+ * exist is therefore the *entry point's* decision, not the construct's and not
1615
+ * the application's — an app that never talks to S3 should never resolve the AWS
1616
+ * SDK, and application code that registers a driver has named a provider in the
1617
+ * one layer this design keeps provider-free.
1585
1618
  *
1586
- * @internal Exported for testing
1587
- */
1588
- async function validateFrontendApp(appName, appPath, workspaceRoot, framework) {
1589
- const errors = [];
1590
- const warnings = [];
1591
- const fullPath = (0, node_path.join)(workspaceRoot, appPath);
1592
- const packageJsonPath = (0, node_path.join)(fullPath, "package.json");
1593
- if (!(0, node_fs.existsSync)(packageJsonPath)) {
1594
- errors.push(`package.json not found at ${appPath}. Run: pnpm init in the app directory.`);
1595
- return {
1596
- appName,
1597
- valid: false,
1598
- errors,
1599
- warnings
1600
- };
1601
- }
1602
- let pkg;
1603
- try {
1604
- pkg = require(packageJsonPath);
1605
- } catch {
1606
- errors.push(`Failed to read package.json at ${packageJsonPath}`);
1607
- return {
1608
- appName,
1609
- valid: false,
1610
- errors,
1611
- warnings
1612
- };
1613
- }
1614
- const deps = {
1615
- ...pkg.dependencies,
1616
- ...pkg.devDependencies
1617
- };
1618
- const resolvedFramework = framework ?? detectFrontendFramework(fullPath, deps);
1619
- if (resolvedFramework) {
1620
- const spec = FRONTEND_FRAMEWORKS[resolvedFramework];
1621
- if (!spec.configFiles.some((file) => (0, node_fs.existsSync)((0, node_path.join)(fullPath, file)))) errors.push(`${spec.displayName} config file not found. Expected one of: ${spec.configFiles.join(", ")}`);
1622
- if (!deps[spec.dependency]) errors.push(`${spec.displayName} not found in dependencies. Run: ${spec.installCommand}`);
1623
- } else warnings.push("No recognized frontend framework detected (Next.js, Vite, Remix). The app will run via its \"dev\" script. Set `framework` in your app config to enable strict validation.");
1624
- if (!pkg.scripts?.dev) {
1625
- if (resolvedFramework) warnings.push("No \"dev\" script found in package.json. Turbo expects a \"dev\" script to run.");
1626
- else errors.push("No \"dev\" script found in package.json. Without a recognized framework or a dev script, there is nothing to run.");
1627
- }
1628
- return {
1629
- appName,
1630
- valid: errors.length === 0,
1631
- errors,
1632
- warnings
1633
- };
1634
- }
1635
- /**
1636
- * Validate all frontend apps in the workspace.
1637
- * Returns validation results for each frontend app.
1638
- * @internal Exported for testing
1619
+ * So the generated entries register: `gkm dev`'s server for local development,
1620
+ * and each Lambda handler for its own target. Registration is an explicit call
1621
+ * rather than an import side effect, because a side-effecting module is exactly
1622
+ * what a bundler is entitled to drop.
1639
1623
  */
1640
- async function validateFrontendApps(workspace) {
1641
- const results = [];
1642
- for (const [appName, app] of Object.entries(workspace.apps)) if (app.type === "web") {
1643
- const result = await validateFrontendApp(appName, app.path, workspace.root, isClientFramework(app.framework) ? app.framework : void 0);
1644
- results.push(result);
1645
- }
1646
- return results;
1647
- }
1624
+ /** The `s3://` driver, which serves MinIO locally and S3 deployed. */
1625
+ const S3 = {
1626
+ imports: `import { registerStorageDriver } from '@geekmidas/storage';\nimport { s3Driver } from '@geekmidas/storage/aws';`,
1627
+ setup: "registerStorageDriver(s3Driver);"
1628
+ };
1629
+ const NONE = {
1630
+ imports: "",
1631
+ setup: ""
1632
+ };
1648
1633
  /**
1649
- * Load secrets for development stage.
1650
- * Returns env vars to inject, or empty object if secrets not configured/found.
1651
- * @internal Exported for testing
1634
+ * The cache driver for a backend — exactly one, never all three.
1635
+ *
1636
+ * Keyed off the *backend* rather than off a dependency, because unlike storage
1637
+ * the backend already says which driver is right: a project caching in Postgres
1638
+ * resolves `pg` and never `ioredis`, and one caching in Upstash resolves
1639
+ * neither. That is the whole reason the drivers live behind separate subpaths.
1640
+ *
1641
+ * Both HTTP schemes are registered for Upstash, because the local proxy speaks
1642
+ * `http://` and Upstash speaks `https://`, and an entry that registered one
1643
+ * would work in exactly one of the two places.
1652
1644
  */
1653
- async function loadDevSecrets(workspace) {
1654
- if (!workspace.secrets.enabled) return {};
1655
- const stage = workspace.stages.local;
1656
- if (require_storage.secretsExist(stage, workspace.root)) {
1657
- const secrets = await require_storage.readStageSecrets(stage, workspace.root);
1658
- if (secrets) {
1659
- logger$11.log(`🔐 Loading secrets from stage: ${stage}`);
1660
- return require_storage.toEmbeddableSecrets(secrets);
1661
- }
1645
+ const CACHE_DRIVERS = {
1646
+ upstash: {
1647
+ imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { upstashCacheDriver, upstashInsecureCacheDriver } from '@geekmidas/cache/upstash';`,
1648
+ setup: "registerCacheDriver(upstashCacheDriver);\nregisterCacheDriver(upstashInsecureCacheDriver);"
1649
+ },
1650
+ elasticache: {
1651
+ imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { redisCacheDriver, redissCacheDriver } from '@geekmidas/cache/redis';`,
1652
+ setup: "registerCacheDriver(redisCacheDriver);\nregisterCacheDriver(redissCacheDriver);"
1653
+ },
1654
+ db: {
1655
+ imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { postgresCacheDriver } from '@geekmidas/cache/postgres';`,
1656
+ setup: "registerCacheDriver(postgresCacheDriver);"
1662
1657
  }
1663
- logger$11.warn(`⚠️ Secrets enabled but no "${stage}" secrets found. Run "gkm setup" to initialize the local stage`);
1664
- return {};
1665
- }
1658
+ };
1666
1659
  /**
1667
- * Workspace dev command - orchestrates multi-app development using Turbo.
1660
+ * Everything a generated entry should register: object storage, and the cache.
1668
1661
  *
1669
- * Flow:
1670
- * 1. Check for port conflicts
1671
- * 2. Start docker-compose services (db, cache, mail)
1672
- * 3. Generate dependency URLs ({APP_NAME}_URL)
1673
- * 4. Spawn turbo run dev with injected env vars
1662
+ * Merged here rather than threaded separately, because a generated file has one
1663
+ * import block and one setup block whatever fills them.
1674
1664
  */
1675
- async function workspaceDevCommand(workspace, options) {
1676
- const appCount = Object.keys(workspace.apps).length;
1677
- const backendApps = Object.entries(workspace.apps).filter(([_, app]) => app.type === "backend");
1678
- const frontendApps = Object.entries(workspace.apps).filter(([_, app]) => app.type === "web");
1679
- const mobileApps = Object.entries(workspace.apps).filter(([_, app]) => app.type === "mobile");
1680
- logger$11.log(`\n🚀 Starting workspace: ${workspace.name}`);
1681
- const counts = [`${backendApps.length} backend`, `${frontendApps.length} web`];
1682
- if (mobileApps.length > 0) counts.push(`${mobileApps.length} mobile`);
1683
- logger$11.log(` ${counts.join(", ")} app(s)`);
1684
- const conflicts = checkPortConflicts(workspace);
1685
- if (conflicts.length > 0) {
1686
- for (const conflict of conflicts) logger$11.error(`❌ Port conflict: Apps "${conflict.app1}" and "${conflict.app2}" both use port ${conflict.port}`);
1687
- throw new Error("Port conflicts detected. Please assign unique ports to each app.");
1688
- }
1689
- if (frontendApps.length > 0) {
1690
- logger$11.log("\n🔍 Validating frontend apps...");
1691
- const validationResults = await validateFrontendApps(workspace);
1692
- let hasErrors = false;
1693
- for (const result of validationResults) {
1694
- if (!result.valid) {
1695
- hasErrors = true;
1696
- logger$11.error(`\n❌ Frontend app "${result.appName}" validation failed:`);
1697
- for (const error of result.errors) logger$11.error(` • ${error}`);
1698
- }
1699
- for (const warning of result.warnings) logger$11.warn(` ⚠️ ${result.appName}: ${warning}`);
1700
- }
1701
- if (hasErrors) throw new Error("Frontend app validation failed. Fix the issues above and try again.");
1702
- logger$11.log("✅ Frontend apps validated");
1703
- }
1704
- const rawSecrets = await loadDevSecrets(workspace);
1705
- let secretsEnv;
1706
- if (require_credentials.usesConstructs(workspace)) {
1707
- const reconciled = await require_credentials.reconcileWorkspace(workspace, { stage: workspace.stages.local });
1708
- if (reconciled.changed && reconciled.plan.containers.length > 0) {
1709
- logger$11.log(`🐳 Services: ${reconciled.plan.containers.join(", ")}`);
1710
- for (const [container, address] of Object.entries(reconciled.addresses)) logger$11.log(` ${container}: ${address}`);
1711
- }
1712
- secretsEnv = {
1713
- ...rawSecrets,
1714
- ...reconciled.env
1715
- };
1716
- } else {
1717
- const resolvedPorts = await require_credentials.resolveServicePorts(workspace.root);
1718
- await require_credentials.startWorkspaceServices(workspace, resolvedPorts.dockerEnv, rawSecrets);
1719
- secretsEnv = require_credentials.rewriteUrlsWithPorts(rawSecrets, resolvedPorts);
1720
- }
1721
- if (Object.keys(secretsEnv).length > 0) logger$11.log(` Loaded ${Object.keys(secretsEnv).length} secret(s)`);
1722
- const dependencyEnv = generateAllDependencyEnvVars(workspace);
1723
- if (Object.keys(dependencyEnv).length > 0) {
1724
- logger$11.log("📡 Dependency URLs:");
1725
- for (const [key, value] of Object.entries(dependencyEnv)) logger$11.log(` ${key}=${value}`);
1726
- }
1727
- let turboFilter = [];
1728
- if (options.app) {
1729
- if (!workspace.apps[options.app]) {
1730
- const appNames = Object.keys(workspace.apps).join(", ");
1731
- throw new Error(`App "${options.app}" not found. Available apps: ${appNames}`);
1732
- }
1733
- turboFilter = ["--filter", options.app];
1734
- logger$11.log(`\n🎯 Running single app: ${options.app}`);
1735
- } else if (options.filter) {
1736
- turboFilter = ["--filter", options.filter];
1737
- logger$11.log(`\n🔍 Using filter: ${options.filter}`);
1738
- } else logger$11.log(`\n🎯 Running all ${appCount} apps`);
1739
- const buildOrder = require_workspace.getAppBuildOrder(workspace);
1740
- logger$11.log("\n📋 Apps (in dependency order):");
1741
- for (const appName of buildOrder) {
1742
- const app = workspace.apps[appName];
1743
- if (!app) continue;
1744
- const deps = app.dependencies.length > 0 ? ` (depends on: ${app.dependencies.join(", ")})` : "";
1745
- const icon = app.type === "backend" ? "🔧" : app.type === "mobile" ? "📱" : "🌐";
1746
- logger$11.log(` ${icon} ${appName} → http://localhost:${app.port}${deps}`);
1747
- }
1748
- const configFiles = [
1749
- "gkm.config.ts",
1750
- "gkm.config.js",
1751
- "gkm.config.json"
1752
- ];
1753
- let configPath = "";
1754
- for (const file of configFiles) {
1755
- const fullPath = (0, node_path.join)(workspace.root, file);
1756
- if ((0, node_fs.existsSync)(fullPath)) {
1757
- configPath = fullPath;
1758
- break;
1759
- }
1760
- }
1761
- const turboEnv = {
1762
- ...process.env,
1763
- ...secretsEnv,
1764
- ...dependencyEnv,
1765
- NODE_ENV: "development",
1766
- ...configPath ? { GKM_CONFIG_PATH: configPath } : {}
1767
- };
1768
- logger$11.log("\n🏃 Starting turbo run dev...\n");
1769
- const turboProcess = (0, node_child_process.spawn)("pnpm", [
1770
- "turbo",
1771
- "run",
1772
- "dev",
1773
- ...turboFilter
1774
- ], {
1775
- cwd: workspace.root,
1776
- stdio: "inherit",
1777
- env: turboEnv,
1778
- detached: true
1779
- });
1780
- let isShuttingDown = false;
1781
- const shutdown = () => {
1782
- if (isShuttingDown) return;
1783
- isShuttingDown = true;
1784
- logger$11.log("\n🛑 Shutting down workspace...");
1785
- const pid = turboProcess.pid;
1786
- if (pid) try {
1787
- process.kill(-pid, "SIGTERM");
1788
- } catch {
1789
- try {
1790
- process.kill(pid, "SIGTERM");
1791
- } catch {}
1792
- }
1793
- setTimeout(() => {
1794
- if (pid) try {
1795
- process.kill(-pid, "SIGKILL");
1796
- } catch {
1797
- try {
1798
- process.kill(pid, "SIGKILL");
1799
- } catch {}
1800
- }
1801
- process.exit(0);
1802
- }, 3e3);
1665
+ function driversFor(options) {
1666
+ const backends = options.cache === false || options.cache === void 0 ? [] : typeof options.cache === "string" ? [options.cache] : options.cache;
1667
+ const parts = [storageDriversFor(options.appRoot), ...[...new Set(backends)].map((backend) => CACHE_DRIVERS[backend] ?? CACHE_DRIVERS[require_discover.DEFAULT_CACHE.aws])].filter((part) => part.imports || part.setup);
1668
+ if (parts.length === 0) return NONE;
1669
+ return {
1670
+ imports: parts.map((part) => part.imports).join("\n"),
1671
+ setup: parts.map((part) => part.setup).join("\n")
1803
1672
  };
1804
- process.on("SIGINT", shutdown);
1805
- process.on("SIGTERM", shutdown);
1806
- return new Promise((resolve, reject) => {
1807
- turboProcess.on("error", (error) => {
1808
- logger$11.error("❌ Turbo error:", error);
1809
- reject(error);
1810
- });
1811
- turboProcess.on("exit", (code) => {
1812
- if (code !== null && code !== 0) reject(/* @__PURE__ */ new Error(`Turbo exited with code ${code}`));
1813
- else resolve();
1814
- });
1815
- });
1816
1673
  }
1817
1674
  /**
1818
- * What the app declares, or nothing when it declares in the old way.
1675
+ * Which cache backends an app's entry actually has to speak.
1676
+ *
1677
+ * The config answers this for a cache that named nowhere, and the *declaration*
1678
+ * answers it for one that named a database — `orders.cache('Sessions')` is a
1679
+ * statement about the application, so a deployment cannot move that cache and
1680
+ * the entry cannot register a driver for somewhere else.
1819
1681
  *
1820
- * Discovery imports the application's construct modules, which is why this is
1821
- * guarded rather than unconditional: a project with no `constructs` glob has
1822
- * not adopted the model, and there is nothing to import.
1682
+ * Reading only the config is how an entry ends up registering the Upstash
1683
+ * driver for a URL the target composed as `postgres://`, which fails at the
1684
+ * first request with `UnregisteredCacheScheme` and a stack that points at the
1685
+ * cache rather than at the config that disagreed.
1823
1686
  */
1824
- async function declaredConstructs(config, appRoot) {
1825
- const patterns = typeof config?.constructs === "string" || Array.isArray(config?.constructs) ? config.constructs : void 0;
1826
- if (!patterns) return {};
1827
- try {
1828
- return await require_discover.discover({
1829
- patterns: (Array.isArray(patterns) ? patterns : [patterns]).map((p) => (0, node_path.isAbsolute)(p) ? p : (0, node_path.join)(appRoot, p)),
1830
- cwd: appRoot
1831
- });
1832
- } catch {
1833
- return {};
1834
- }
1835
- }
1836
- async function buildServer(config, context, provider, enableOpenApi, appRoot = process.cwd(), bustCache = false) {
1837
- context = {
1838
- ...context,
1839
- storageDrivers: driversFor({
1840
- appRoot,
1841
- cache: cacheBackendsIn(await declaredConstructs(config, appRoot), require_credentials.cacheBackendFor(require_credentials.providerOf(config ?? {})))
1842
- })
1843
- };
1844
- const endpointGenerator = new require_openapi.EndpointGenerator();
1845
- const functionGenerator = new FunctionGenerator();
1846
- const cronGenerator = new CronGenerator();
1847
- const subscriberGenerator = new SubscriberGenerator();
1848
- const queueGenerator = new QueueGenerator();
1849
- const topicGenerator = new TopicGenerator();
1850
- const [allEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics] = await Promise.all([
1851
- endpointGenerator.load(config.constructs, appRoot, bustCache),
1852
- config.functions ? functionGenerator.load(config.functions, appRoot, bustCache) : [],
1853
- config.crons ? cronGenerator.load(config.crons, appRoot, bustCache) : [],
1854
- config.subscribers ? subscriberGenerator.load(config.subscribers, appRoot, bustCache) : [],
1855
- config.queues ? queueGenerator.load(config.queues, appRoot, bustCache) : [],
1856
- config.topics ? topicGenerator.load(config.topics, appRoot, bustCache) : []
1857
- ]);
1858
- const outputDir = (0, node_path.join)(appRoot, ".gkm", provider);
1859
- await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
1860
- await Promise.all([
1861
- endpointGenerator.build(context, allEndpoints, outputDir, {
1862
- provider,
1863
- enableOpenApi
1864
- }),
1865
- functionGenerator.build(context, allFunctions, outputDir, { provider }),
1866
- cronGenerator.build(context, allCrons, outputDir, { provider }),
1867
- subscriberGenerator.build(context, allSubscribers, outputDir, { provider }),
1868
- queueGenerator.build(context, allQueues, outputDir, { provider }),
1869
- topicGenerator.build(context, allTopics, outputDir, { provider })
1870
- ]);
1687
+ function cacheBackendsIn(manifest, configured) {
1688
+ const caches = Object.values(manifest).filter((d) => d.kind === "cache");
1689
+ return [...new Set(caches.map((c) => c.of ? "db" : configured))];
1871
1690
  }
1872
1691
  /**
1873
- * Run any TypeScript file with secret injection.
1874
- * Does not require gkm.config.ts.
1692
+ * The drivers an app's entry points should register.
1693
+ *
1694
+ * Keyed off the dependency rather than off a declared bucket, because the entry
1695
+ * is generated before discovery has run on the watcher's rebuild path — and an
1696
+ * app that installed `@geekmidas/storage` has already paid for the SDK it would
1697
+ * otherwise resolve lazily.
1875
1698
  */
1876
- async function entryDevCommand(options) {
1877
- const { entry, watch = true } = options;
1878
- if (!entry) throw new Error("--entry requires a file path");
1879
- const entryPath = (0, node_path.resolve)(process.cwd(), entry);
1880
- if (!(0, node_fs.existsSync)(entryPath)) throw new Error(`Entry file not found: ${entryPath}`);
1881
- const defaultEnv = require_credentials.loadEnvFiles(".env");
1882
- if (defaultEnv.loaded.length > 0) logger$11.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
1883
- const { credentials, resolvedPort, secretsJsonPath, appName } = await require_credentials.prepareEntryCredentials({ explicitPort: options.portExplicit ? options.port : void 0 });
1884
- if (appName) logger$11.log(`📦 App: ${appName} (port ${resolvedPort})`);
1885
- logger$11.log(`🚀 Starting entry file: ${entry} on port ${resolvedPort}`);
1886
- if (Object.keys(credentials).length > 1) logger$11.log(`🔐 Loaded ${Object.keys(credentials).length - 1} secret(s) + PORT`);
1887
- const wrapperDir = (0, node_path.join)(process.cwd(), ".gkm");
1888
- await (0, node_fs_promises.mkdir)(wrapperDir, { recursive: true });
1889
- const wrapperPath = (0, node_path.join)(wrapperDir, "entry-wrapper.ts");
1890
- await require_credentials.createEntryWrapper(wrapperPath, entryPath, secretsJsonPath);
1891
- const runner = new EntryRunner(wrapperPath, entryPath, watch, resolvedPort);
1892
- await runner.start();
1893
- let isShuttingDown = false;
1894
- const shutdown = () => {
1895
- if (isShuttingDown) return;
1896
- isShuttingDown = true;
1897
- logger$11.log("\n🛑 Shutting down...");
1898
- runner.stop();
1899
- process.exit(0);
1900
- };
1901
- process.on("SIGINT", shutdown);
1902
- process.on("SIGTERM", shutdown);
1903
- await new Promise(() => {});
1699
+ function storageDriversFor(appRoot) {
1700
+ return dependsOnStorage(appRoot) ? S3 : NONE;
1701
+ }
1702
+ function dependsOnStorage(appRoot) {
1703
+ const manifest = (0, node_path.join)(appRoot, "package.json");
1704
+ if (!(0, node_fs.existsSync)(manifest)) return false;
1705
+ try {
1706
+ const pkg = JSON.parse((0, node_fs.readFileSync)(manifest, "utf8"));
1707
+ return Boolean(pkg.dependencies?.["@geekmidas/storage"] ?? pkg.devDependencies?.["@geekmidas/storage"]);
1708
+ } catch {
1709
+ return false;
1710
+ }
1904
1711
  }
1712
+ //#endregion
1713
+ //#region src/generators/FunctionGenerator.ts
1714
+ var FunctionGenerator = class extends require_discover.ConstructGenerator {
1715
+ isConstruct(value) {
1716
+ return _geekmidas_constructs_functions.Function.isFunction(value);
1717
+ }
1718
+ async build(context, constructs, outputDir, options) {
1719
+ const provider = options?.provider || "aws-lambda";
1720
+ const logger = console;
1721
+ const functionInfos = [];
1722
+ if (constructs.length === 0 || provider !== "aws-lambda") return functionInfos;
1723
+ const functionsDir = (0, node_path.join)(outputDir, "functions");
1724
+ await (0, node_fs_promises.mkdir)(functionsDir, { recursive: true });
1725
+ for (const { key, construct, path } of constructs) {
1726
+ const handlerFile = await this.generateFunctionHandler(functionsDir, path.relative, key, context, construct.owner);
1727
+ functionInfos.push({
1728
+ name: key,
1729
+ handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
1730
+ timeout: construct.timeout,
1731
+ memorySize: construct.memorySize,
1732
+ environment: await construct.getEnvironment({ markOptional: context.markOptional }),
1733
+ dependencies: construct.constructs
1734
+ });
1735
+ logger.log(`Generated function handler: ${key}`);
1736
+ }
1737
+ return functionInfos;
1738
+ }
1739
+ async generateFunctionHandler(outputDir, sourceFile, exportName, context, owner) {
1740
+ const handlerFileName = `${exportName}.ts`;
1741
+ const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
1742
+ const importPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile).replace(/\.ts$/, ".js");
1743
+ const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
1744
+ const content = `import { AWSLambdaFunction } from '@geekmidas/constructs/aws';
1745
+ import { ${exportName} } from '${importPath}';
1746
+ ${runtime.imports}
1747
+ ${runtime.bindings}
1748
+
1749
+ const adapter = new AWSLambdaFunction(envParser, ${exportName});
1750
+
1751
+ export const handler = adapter.handler;
1752
+ `;
1753
+ await (0, node_fs_promises.writeFile)(handlerPath, content);
1754
+ return handlerPath;
1755
+ }
1756
+ };
1757
+ //#endregion
1758
+ //#region src/generators/QueueGenerator.ts
1905
1759
  /**
1906
- * Runs and watches a TypeScript entry file using tsx.
1760
+ * Generates the runtime for `q` queue workers.
1761
+ *
1762
+ * - **server** (`gkm dev`): a single `queues.ts` exposing `setupQueues()` that
1763
+ * runs an in-process pg-boss poller alongside the Hono server — no SQS/Lambda.
1764
+ * Each queue subscribes by its **name** on the shared event connection.
1765
+ * - **aws-lambda**: one handler file per queue wrapping `AWSLambdaQueue`, backed
1766
+ * by an SQS event-source mapping.
1907
1767
  */
1908
- var EntryRunner = class {
1909
- wrapperPath;
1910
- entryPath;
1911
- watch;
1912
- port;
1913
- childProcess = null;
1914
- watcher = null;
1915
- isRunning = false;
1916
- constructor(wrapperPath, entryPath, watch, port) {
1917
- this.wrapperPath = wrapperPath;
1918
- this.entryPath = entryPath;
1919
- this.watch = watch;
1920
- this.port = port;
1768
+ var QueueGenerator = class extends require_discover.ConstructGenerator {
1769
+ isConstruct(value) {
1770
+ return _geekmidas_constructs_queue.Queue.isQueue(value);
1921
1771
  }
1922
- async start() {
1923
- await this.runProcess();
1924
- if (this.watch) {
1925
- const watchDir = (0, node_path.dirname)(this.entryPath);
1926
- this.watcher = chokidar.default.watch(watchDir, {
1927
- ignored: /(^|[/\\])\../,
1928
- persistent: true,
1929
- ignoreInitial: true
1930
- });
1931
- let restartTimeout = null;
1932
- this.watcher.on("change", (path) => {
1933
- logger$11.log(`📝 File changed: ${path}`);
1934
- if (restartTimeout) clearTimeout(restartTimeout);
1935
- restartTimeout = setTimeout(async () => {
1936
- logger$11.log("🔄 Restarting...");
1937
- await this.restart();
1938
- }, 300);
1772
+ async build(context, constructs, outputDir, options) {
1773
+ const provider = options?.provider || "aws-lambda";
1774
+ const logger = console;
1775
+ const queueInfos = [];
1776
+ if (provider === "server") {
1777
+ await this.generateServerQueuesFile(outputDir, constructs);
1778
+ logger.log(`Generated server queues file with ${constructs.length} queues (polling mode)`);
1779
+ return queueInfos;
1780
+ }
1781
+ if (constructs.length === 0 || provider !== "aws-lambda") return queueInfos;
1782
+ const queuesDir = (0, node_path.join)(outputDir, "queues");
1783
+ await (0, node_fs_promises.mkdir)(queuesDir, { recursive: true });
1784
+ for (const { key, construct, path } of constructs) {
1785
+ const handlerFile = await this.generateQueueHandler(queuesDir, path.relative, key, context, construct.owner);
1786
+ queueInfos.push({
1787
+ name: construct.name,
1788
+ handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
1789
+ batchSize: construct.batchSize,
1790
+ fifo: construct.fifo,
1791
+ timeout: construct.timeout,
1792
+ environment: await construct.getEnvironment({ markOptional: context.markOptional }),
1793
+ dependencies: construct.constructs
1939
1794
  });
1940
- logger$11.log(`👀 Watching for changes in: ${watchDir}`);
1795
+ logger.log(`Generated queue handler: ${key}`);
1796
+ }
1797
+ return queueInfos;
1798
+ }
1799
+ async generateQueueHandler(outputDir, sourceFile, exportName, context, owner) {
1800
+ const handlerPath = (0, node_path.join)(outputDir, `${exportName}.ts`);
1801
+ const importPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile).replace(/\.ts$/, ".js");
1802
+ const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
1803
+ const content = `import { AWSLambdaQueue } from '@geekmidas/constructs/aws';
1804
+ import { ${exportName} } from '${importPath}';
1805
+ ${runtime.imports}
1806
+ ${runtime.bindings}
1807
+
1808
+ const adapter = new AWSLambdaQueue(envParser, ${exportName});
1809
+
1810
+ export const handler = adapter.handler;
1811
+ `;
1812
+ await (0, node_fs_promises.writeFile)(handlerPath, content);
1813
+ return handlerPath;
1814
+ }
1815
+ async generateServerQueuesFile(outputDir, queues) {
1816
+ await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
1817
+ const queuesPath = (0, node_path.join)(outputDir, "queues.ts");
1818
+ if (queues.length === 0) {
1819
+ await (0, node_fs_promises.writeFile)(queuesPath, `/**
1820
+ * Generated queues setup — this app declares none.
1821
+ */
1822
+ import type { EnvironmentParser } from '@geekmidas/envkit';
1823
+ import type { Logger } from '@geekmidas/logger';
1824
+
1825
+ export async function setupQueues(
1826
+ _envParser: EnvironmentParser<any>,
1827
+ _logger: Logger,
1828
+ ): Promise<void> {}
1829
+ `);
1830
+ return queuesPath;
1831
+ }
1832
+ const importsByFile = /* @__PURE__ */ new Map();
1833
+ for (const { path, key } of queues) {
1834
+ const importPath = (0, node_path.relative)((0, node_path.dirname)(queuesPath), path.relative).replace(/\.ts$/, ".js");
1835
+ if (!importsByFile.has(importPath)) importsByFile.set(importPath, []);
1836
+ importsByFile.get(importPath)?.push(key);
1941
1837
  }
1838
+ const content = `/**
1839
+ * Generated queue workers setup
1840
+ *
1841
+ * ⚠️ WARNING: This is for LOCAL DEVELOPMENT ONLY
1842
+ * This runs an in-process pg-boss poller alongside the HTTP server. Each queue
1843
+ * subscribes by its name on the shared event connection
1844
+ * (EVENT_SUBSCRIBER_CONNECTION_STRING), so producers using
1845
+ * <NAME>_PUBLISHER_CONNECTION_STRING (pgboss:// locally) reach it.
1846
+ *
1847
+ * For production, queues run as AWS Lambda with SQS event-source mappings.
1848
+ */
1849
+ import type { EnvironmentParser } from '@geekmidas/envkit';
1850
+ import type { Logger } from '@geekmidas/logger';
1851
+ import { EventConnectionFactory, Subscriber } from '@geekmidas/events';
1852
+ import type { EventConnection, EventSubscriber } from '@geekmidas/events';
1853
+ import { ServiceDiscovery } from '@geekmidas/services';
1854
+ ${Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n")}
1855
+
1856
+ const queues = [
1857
+ ${queues.map(({ key }) => key).join(",\n ")}
1858
+ ];
1859
+
1860
+ const activeSubscribers: EventSubscriber<any>[] = [];
1861
+
1862
+ export async function setupQueues(
1863
+ envParser: EnvironmentParser<any>,
1864
+ logger: Logger,
1865
+ ): Promise<void> {
1866
+ if (queues.length === 0) {
1867
+ return;
1868
+ }
1869
+
1870
+ logger.info('Setting up queue workers in polling mode (local development)');
1871
+
1872
+ const config = envParser.create((get) => ({
1873
+ connectionString: get('EVENT_SUBSCRIBER_CONNECTION_STRING').string(),
1874
+ })).parse();
1875
+
1876
+ const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
1877
+
1878
+ let connection: EventConnection;
1879
+ try {
1880
+ connection = await EventConnectionFactory.fromConnectionString(config.connectionString);
1881
+ const connectionType = new URL(config.connectionString).protocol.replace(':', '');
1882
+ logger.info({ connectionType }, 'Created shared event connection for queues');
1883
+ } catch (error) {
1884
+ logger.error({ error }, 'Failed to create event connection for queues');
1885
+ return;
1886
+ }
1887
+
1888
+ for (const queue of queues) {
1889
+ try {
1890
+ const eventSubscriber = await Subscriber.fromConnection(connection);
1891
+
1892
+ const services = queue.services.length > 0
1893
+ ? await serviceDiscovery.register(queue.services)
1894
+ : {};
1895
+
1896
+ // A queue subscribes to a single "type" — its own name.
1897
+ await eventSubscriber.subscribe([queue.name], async (message) => {
1898
+ try {
1899
+ const validation = await queue.messageSchema['~standard'].validate(message.payload);
1900
+ if (validation.issues) {
1901
+ logger.error({ issues: validation.issues, queue: queue.name }, 'Queue message failed validation');
1902
+ return;
1903
+ }
1904
+
1905
+ await queue.handler({
1906
+ messages: [validation.value],
1907
+ services: services,
1908
+ logger: queue.logger,
1909
+ });
1910
+
1911
+ logger.debug({ queue: queue.name }, 'Successfully processed queue message');
1912
+ } catch (error) {
1913
+ logger.error({ error, queue: queue.name }, 'Failed to process queue message');
1914
+ // Message will become visible again for retry.
1915
+ }
1916
+ });
1917
+
1918
+ activeSubscribers.push(eventSubscriber);
1919
+ logger.info({ queue: queue.name }, 'Queue worker started polling');
1920
+ } catch (error) {
1921
+ logger.error({ error, queue: queue.name }, 'Failed to setup queue worker');
1922
+ }
1923
+ }
1924
+
1925
+ const shutdown = () => {
1926
+ logger.info('Stopping all queue workers');
1927
+ for (const _ of activeSubscribers) {
1928
+ connection.stop();
1929
+ }
1930
+ };
1931
+
1932
+ process.on('SIGTERM', shutdown);
1933
+ process.on('SIGINT', shutdown);
1934
+ }
1935
+ `;
1936
+ await (0, node_fs_promises.writeFile)(queuesPath, content);
1937
+ return queuesPath;
1942
1938
  }
1943
- async runProcess() {
1944
- const env = {
1945
- ...process.env,
1946
- PORT: String(this.port)
1947
- };
1948
- this.childProcess = (0, node_child_process.spawn)("npx", ["tsx", this.wrapperPath], {
1949
- stdio: "inherit",
1950
- env,
1951
- detached: true
1952
- });
1953
- this.isRunning = true;
1954
- this.childProcess.on("error", (error) => {
1955
- logger$11.error("❌ Process error:", error);
1956
- });
1957
- this.childProcess.on("exit", (code) => {
1958
- if (code !== null && code !== 0 && code !== 143) logger$11.error(`❌ Process exited with code ${code}`);
1959
- this.isRunning = false;
1960
- });
1961
- await new Promise((resolve) => setTimeout(resolve, 500));
1962
- if (this.isRunning) {
1963
- logger$11.log("");
1964
- logger$11.log(` \x1b[32m✓ Ready\x1b[0m at \x1b[36mhttp://localhost:${this.port}\x1b[0m`);
1965
- logger$11.log("");
1966
- }
1939
+ };
1940
+ //#endregion
1941
+ //#region src/generators/SubscriberGenerator.ts
1942
+ var SubscriberGenerator = class extends require_discover.ConstructGenerator {
1943
+ isConstruct(value) {
1944
+ return _geekmidas_constructs_subscribers.Subscriber.isSubscriber(value);
1967
1945
  }
1968
- async restart() {
1969
- this.stopProcess();
1970
- await new Promise((resolve) => setTimeout(resolve, 500));
1971
- await this.runProcess();
1946
+ async build(context, constructs, outputDir, options) {
1947
+ const provider = options?.provider || "aws-lambda";
1948
+ const logger = console;
1949
+ const subscriberInfos = [];
1950
+ if (provider === "server") {
1951
+ await this.generateServerSubscribersFile(outputDir, constructs);
1952
+ logger.log(`Generated server subscribers file with ${constructs.length} subscribers (polling mode)`);
1953
+ return subscriberInfos;
1954
+ }
1955
+ if (constructs.length === 0) return subscriberInfos;
1956
+ if (provider !== "aws-lambda") return subscriberInfos;
1957
+ const subscribersDir = (0, node_path.join)(outputDir, "subscribers");
1958
+ await (0, node_fs_promises.mkdir)(subscribersDir, { recursive: true });
1959
+ for (const { key, construct, path } of constructs) {
1960
+ const handlerFile = await this.generateSubscriberHandler(subscribersDir, path.relative, key, construct, context, construct.owner);
1961
+ subscriberInfos.push({
1962
+ name: key,
1963
+ handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
1964
+ subscribedEvents: construct.subscribedEvents || [],
1965
+ ...construct.topicName ? {
1966
+ transport: "topic",
1967
+ topic: construct.topicName
1968
+ } : {},
1969
+ timeout: construct.timeout,
1970
+ memorySize: construct.memorySize,
1971
+ environment: await construct.getEnvironment({ markOptional: context.markOptional }),
1972
+ dependencies: construct.constructs
1973
+ });
1974
+ logger.log(`Generated subscriber handler: ${key}`);
1975
+ }
1976
+ return subscriberInfos;
1972
1977
  }
1973
- stop() {
1974
- this.watcher?.close();
1975
- this.stopProcess();
1978
+ async generateSubscriberHandler(outputDir, sourceFile, exportName, _subscriber, context, owner) {
1979
+ const handlerFileName = `${exportName}.ts`;
1980
+ const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
1981
+ const importPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile).replace(/\.ts$/, ".js");
1982
+ const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
1983
+ const content = `import { AWSLambdaSubscriber } from '@geekmidas/constructs/aws';
1984
+ import { ${exportName} } from '${importPath}';
1985
+ ${runtime.imports}
1986
+ ${runtime.bindings}
1987
+
1988
+ const adapter = new AWSLambdaSubscriber(envParser, ${exportName});
1989
+
1990
+ export const handler = adapter.handler;
1991
+ `;
1992
+ await (0, node_fs_promises.writeFile)(handlerPath, content);
1993
+ return handlerPath;
1976
1994
  }
1977
- stopProcess() {
1978
- if (this.childProcess && this.isRunning) {
1979
- const pid = this.childProcess.pid;
1980
- if (pid) try {
1981
- process.kill(-pid, "SIGTERM");
1982
- } catch {
1983
- try {
1984
- process.kill(pid, "SIGTERM");
1985
- } catch {}
1986
- }
1987
- this.childProcess = null;
1988
- this.isRunning = false;
1995
+ async generateServerSubscribersFile(outputDir, subscribers) {
1996
+ await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
1997
+ const subscribersPath = (0, node_path.join)(outputDir, "subscribers.ts");
1998
+ if (subscribers.length === 0) {
1999
+ await (0, node_fs_promises.writeFile)(subscribersPath, `/**
2000
+ * Generated subscribers setup — this app declares none.
2001
+ */
2002
+ import type { EnvironmentParser } from '@geekmidas/envkit';
2003
+ import type { Logger } from '@geekmidas/logger';
2004
+
2005
+ export async function setupSubscribers(
2006
+ _envParser: EnvironmentParser<any>,
2007
+ _logger: Logger,
2008
+ ): Promise<void> {}
2009
+ `);
2010
+ return subscribersPath;
1989
2011
  }
1990
- }
1991
- };
1992
- /**
1993
- * Generate the content of the dev server entry file (server.ts).
1994
- * Uses dynamic import for createApp so Credentials are populated
1995
- * before any app modules evaluate.
1996
- * @internal Exported for testing
1997
- */
1998
- function generateServerEntryContent(options) {
1999
- const { secretsJsonPath, runtime = "node", enableOpenApi = false, appImportPath = "./app.js" } = options;
2000
- return `#!/usr/bin/env node
2001
- /**
2002
- * Development server entry point
2003
- * This file is auto-generated by 'gkm dev'
2012
+ const importsByFile = /* @__PURE__ */ new Map();
2013
+ for (const { path, key } of subscribers) {
2014
+ const importPath = (0, node_path.relative)((0, node_path.dirname)(subscribersPath), path.relative).replace(/\.ts$/, ".js");
2015
+ if (!importsByFile.has(importPath)) importsByFile.set(importPath, []);
2016
+ importsByFile.get(importPath)?.push(key);
2017
+ }
2018
+ const content = `/**
2019
+ * Generated subscribers setup
2020
+ *
2021
+ * ⚠️ WARNING: This is for LOCAL DEVELOPMENT ONLY
2022
+ * This uses event polling which is not suitable for production.
2023
+ *
2024
+ * For production, use AWS Lambda with SQS/SNS event source mappings.
2025
+ * Lambda automatically:
2026
+ * - Scales based on queue depth
2027
+ * - Handles batch processing and retries
2028
+ * - Manages dead letter queues
2029
+ * - Provides better cost optimization
2030
+ *
2031
+ * This polling implementation is useful for:
2032
+ * - Local development and testing
2033
+ * - Understanding event flow without Lambda deployment
2034
+ *
2035
+ * Supported connection strings:
2036
+ * - sqs://region/account-id/queue-name (SQS queue)
2037
+ * - sns://region/account-id/topic-name (SNS topic)
2038
+ * - rabbitmq://host:port/queue-name (RabbitMQ)
2039
+ * - pgboss://user:pass@host:port/database (pg-boss / PostgreSQL)
2040
+ * - basic://in-memory (In-memory for testing)
2004
2041
  */
2005
- ${secretsJsonPath ? `import { existsSync, readFileSync } from 'node:fs';
2042
+ import type { EnvironmentParser } from '@geekmidas/envkit';
2043
+ import type { Logger } from '@geekmidas/logger';
2044
+ import { EventConnectionFactory, Subscriber } from '@geekmidas/events';
2045
+ import type { EventConnection, EventSubscriber } from '@geekmidas/events';
2046
+ import { ServiceDiscovery } from '@geekmidas/services';
2047
+ ${Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n")}
2006
2048
 
2007
- // Inject dev secrets via globalThis (must happen before app import)
2008
- const secretsPath = '${secretsJsonPath}';
2009
- if (existsSync(secretsPath)) {
2010
- const __secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
2011
- globalThis.__gkm_credentials__ = __secrets;
2012
- Object.assign(process.env, __secrets);
2013
- }
2049
+ const subscribers = [
2050
+ ${subscribers.map(({ key }) => key).join(",\n ")}
2051
+ ];
2014
2052
 
2015
- ` : ""}
2016
- const port = process.argv.includes('--port')
2017
- ? Number.parseInt(process.argv[process.argv.indexOf('--port') + 1])
2018
- : 3000;
2053
+ const activeSubscribers: EventSubscriber<any>[] = [];
2019
2054
 
2020
- // Dynamic import so Credentials are populated before env.ts evaluates
2021
- const { createApp } = await import('${appImportPath}');
2055
+ export async function setupSubscribers(
2056
+ envParser: EnvironmentParser<any>,
2057
+ logger: Logger,
2058
+ ): Promise<void> {
2059
+ if (subscribers.length === 0) {
2060
+ return;
2061
+ }
2022
2062
 
2023
- // createApp is async to support optional WebSocket setup
2024
- const { app, start } = await createApp(undefined, ${enableOpenApi});
2063
+ logger.info('Setting up subscribers in polling mode (local development)');
2025
2064
 
2026
- // Start the server
2027
- start({
2028
- port,
2029
- serve: async (app, port) => {
2030
- ${runtime === "bun" ? `Bun.serve({
2031
- port,
2032
- fetch: app.fetch,
2033
- });` : `const { serve } = await import('@hono/node-server');
2034
- const server = serve({
2035
- fetch: app.fetch,
2036
- port,
2037
- });
2038
- // Inject WebSocket support if available
2039
- const injectWs = (app as any).__injectWebSocket;
2040
- if (injectWs) {
2041
- injectWs(server);
2042
- console.log('🔌 Telescope real-time updates enabled');
2043
- }`}
2044
- },
2045
- }).catch((error) => {
2046
- console.error('Failed to start server:', error);
2047
- process.exit(1);
2048
- });
2049
- `;
2065
+ const config = envParser.create((get) => ({
2066
+ connectionString: get('EVENT_SUBSCRIBER_CONNECTION_STRING').string(),
2067
+ })).parse();
2068
+
2069
+ const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
2070
+
2071
+ // Create connection once, outside the loop (more efficient)
2072
+ // EventConnectionFactory automatically determines the right connection type
2073
+ let connection: EventConnection;
2074
+ try {
2075
+ connection = await EventConnectionFactory.fromConnectionString(config.connectionString);
2076
+
2077
+ const connectionType = new URL(config.connectionString).protocol.replace(':', '');
2078
+ logger.info({ connectionType }, 'Created shared event connection');
2079
+ } catch (error) {
2080
+ logger.error({ error }, 'Failed to create event connection');
2081
+ return;
2082
+ }
2083
+
2084
+ for (const subscriber of subscribers) {
2085
+ try {
2086
+ // Create subscriber from shared connection
2087
+ const eventSubscriber = await Subscriber.fromConnection(connection);
2088
+
2089
+ // Register services
2090
+ const services = subscriber.services.length > 0
2091
+ ? await serviceDiscovery.register(subscriber.services)
2092
+ : {};
2093
+
2094
+ // Subscribe to events
2095
+ const subscribedEvents = subscriber.subscribedEvents || [];
2096
+
2097
+ if (subscribedEvents.length === 0) {
2098
+ logger.warn({ subscriber: subscriber.constructor.name }, 'Subscriber has no subscribed events, skipping');
2099
+ continue;
2100
+ }
2101
+
2102
+ await eventSubscriber.subscribe(subscribedEvents, async (event) => {
2103
+ try {
2104
+ // Process single event (batch of 1)
2105
+ await subscriber.handler({
2106
+ events: [event],
2107
+ services: services as any,
2108
+ logger: subscriber.logger,
2109
+ });
2110
+
2111
+ logger.debug({ eventType: event.type }, 'Successfully processed event');
2112
+ } catch (error) {
2113
+ logger.error({ error, event }, 'Failed to process event');
2114
+ // Event will become visible again for retry
2115
+ }
2116
+ });
2117
+
2118
+ activeSubscribers.push(eventSubscriber);
2119
+
2120
+ logger.info(
2121
+ {
2122
+ events: subscribedEvents,
2123
+ },
2124
+ 'Subscriber started polling'
2125
+ );
2126
+ } catch (error) {
2127
+ logger.error({ error, subscriber: subscriber.constructor.name }, 'Failed to setup subscriber');
2128
+ }
2129
+ }
2130
+
2131
+ // Setup graceful shutdown
2132
+ const shutdown = () => {
2133
+ logger.info('Stopping all subscribers');
2134
+ for (const eventSubscriber of activeSubscribers) {
2135
+ connection.stop();
2136
+ }
2137
+ };
2138
+
2139
+ process.on('SIGTERM', shutdown);
2140
+ process.on('SIGINT', shutdown);
2050
2141
  }
2051
- var DevServer = class {
2052
- provider;
2053
- requestedPort;
2054
- portExplicit;
2055
- enableOpenApi;
2056
- telescope;
2057
- studio;
2058
- runtime;
2059
- appRoot;
2060
- secretsJsonPath;
2061
- serverProcess = null;
2062
- isRunning = false;
2063
- actualPort;
2064
- startTime = Date.now();
2065
- constructor(provider, requestedPort, portExplicit, enableOpenApi, telescope, studio, runtime = "node", appRoot = process.cwd(), secretsJsonPath) {
2066
- this.provider = provider;
2067
- this.requestedPort = requestedPort;
2068
- this.portExplicit = portExplicit;
2069
- this.enableOpenApi = enableOpenApi;
2070
- this.telescope = telescope;
2071
- this.studio = studio;
2072
- this.runtime = runtime;
2073
- this.appRoot = appRoot;
2074
- this.secretsJsonPath = secretsJsonPath;
2075
- this.actualPort = requestedPort;
2076
- }
2077
- async start() {
2078
- this.startTime = Date.now();
2079
- if (this.isRunning) await this.stop();
2080
- if (this.portExplicit) {
2081
- if (!await require_credentials.isPortAvailable(this.requestedPort)) throw new Error(`Port ${this.requestedPort} is already in use. Either stop the process using that port or omit -p/--port to auto-select an available port.`);
2082
- this.actualPort = this.requestedPort;
2083
- } else {
2084
- this.actualPort = await require_credentials.findAvailablePort(this.requestedPort);
2085
- if (this.actualPort !== this.requestedPort) logger$11.log(`ℹ️ Port ${this.requestedPort} was in use, using port ${this.actualPort} instead`);
2086
- }
2087
- const serverEntryPath = (0, node_path.join)(this.appRoot, ".gkm", this.provider, "server.ts");
2088
- await this.createServerEntry();
2089
- logger$11.log(`\n⏳ Starting server...`);
2090
- this.serverProcess = (0, node_child_process.spawn)("npx", [
2091
- "tsx",
2092
- serverEntryPath,
2093
- "--port",
2094
- this.actualPort.toString()
2095
- ], {
2096
- stdio: "inherit",
2097
- env: {
2098
- ...process.env,
2099
- NODE_ENV: "development"
2100
- },
2101
- detached: true
2102
- });
2103
- this.isRunning = true;
2104
- this.serverProcess.on("error", (error) => {
2105
- logger$11.error("❌ Server error:", error);
2106
- });
2107
- this.serverProcess.on("exit", (code, signal) => {
2108
- if (code !== null && code !== 0 && signal !== "SIGTERM") logger$11.error(`❌ Server exited with code ${code}`);
2109
- this.isRunning = false;
2110
- });
2111
- await new Promise((resolve) => setTimeout(resolve, 1e3));
2112
- if (this.isRunning) {
2113
- const base = `http://localhost:${this.actualPort}`;
2114
- const lines = [` Local: ${base}`];
2115
- if (this.enableOpenApi) lines.push(` API Docs: ${base}/__docs`);
2116
- if (this.telescope) lines.push(` Telescope: ${base}${this.telescope.path}`);
2117
- if (this.studio) lines.push(` Studio: ${base}${this.studio.path}`);
2118
- const maxLen = Math.max(...lines.map((l) => l.length));
2119
- const pad = (s) => s.padEnd(maxLen);
2120
- const border = "─".repeat(maxLen + 2);
2121
- logger$11.log("");
2122
- logger$11.log(` \x1b[32m✓ Ready\x1b[0m in ${((Date.now() - this.startTime) / 1e3).toFixed(1)}s`);
2123
- logger$11.log("");
2124
- logger$11.log(` ┌${border}┐`);
2125
- for (const line of lines) logger$11.log(` │ ${pad(line)} │`);
2126
- logger$11.log(` └${border}┘`);
2127
- logger$11.log("");
2128
- }
2129
- }
2130
- async stop() {
2131
- const port = this.actualPort;
2132
- if (this.serverProcess && this.isRunning) {
2133
- const pid = this.serverProcess.pid;
2134
- if (pid) try {
2135
- process.kill(-pid, "SIGKILL");
2136
- } catch {
2137
- try {
2138
- process.kill(pid, "SIGKILL");
2139
- } catch {}
2140
- }
2141
- this.serverProcess = null;
2142
- this.isRunning = false;
2143
- }
2144
- this.killProcessesOnPort(port);
2145
- }
2146
- killProcessesOnPort(port) {
2147
- try {
2148
- (0, node_child_process.execSync)(`lsof -ti tcp:${port} | xargs kill -9 2>/dev/null || true`, { stdio: "ignore" });
2149
- } catch {}
2142
+ `;
2143
+ await (0, node_fs_promises.writeFile)(subscribersPath, content);
2144
+ return subscribersPath;
2150
2145
  }
2151
- async restart() {
2152
- const portToReuse = this.actualPort;
2153
- await this.stop();
2154
- let attempts = 0;
2155
- while (attempts < 30) {
2156
- if (await require_credentials.isPortAvailable(portToReuse)) break;
2157
- await new Promise((resolve) => setTimeout(resolve, 100));
2158
- attempts++;
2159
- }
2160
- this.requestedPort = portToReuse;
2161
- await this.start();
2146
+ };
2147
+ //#endregion
2148
+ //#region src/generators/TopicGenerator.ts
2149
+ /**
2150
+ * Discovers `t` topics into `manifest.topics`. A topic is a *resource*, not a
2151
+ * function — it has no handler to generate. Producers reach it via its derived
2152
+ * publisher (`topic.publisher`, whose connection string is sniffed onto the
2153
+ * producing construct) and subscribers bind via `s.topic(topic)`; this generator
2154
+ * only records the topic + its event contract so infra can provision the SNS
2155
+ * topic. Identical output for every provider (locally pg-boss routes by event
2156
+ * name, so there's nothing to run).
2157
+ */
2158
+ var TopicGenerator = class extends require_discover.ConstructGenerator {
2159
+ isConstruct(value) {
2160
+ return _geekmidas_constructs_topic.Topic.isTopic(value);
2162
2161
  }
2163
- async createServerEntry() {
2164
- const { writeFile: fsWriteFile } = await import("node:fs/promises");
2165
- await fsWriteFile((0, node_path.join)(this.appRoot, ".gkm", this.provider, "server.ts"), generateServerEntryContent({
2166
- secretsJsonPath: this.secretsJsonPath,
2167
- runtime: this.runtime,
2168
- enableOpenApi: this.enableOpenApi
2162
+ async build(_context, constructs, _outputDir, _options) {
2163
+ const logger = console;
2164
+ const topicInfos = constructs.map(({ construct }) => ({
2165
+ name: construct.name,
2166
+ events: construct.eventTypes
2169
2167
  }));
2168
+ for (const topic of topicInfos) logger.log(`Discovered topic: ${topic.name}`);
2169
+ return topicInfos;
2170
2170
  }
2171
2171
  };
2172
2172
  //#endregion
@@ -2380,6 +2380,76 @@ function mapTopicMetadata(topics) {
2380
2380
  for (const [partition, partitionTopics] of Object.entries(topics)) result[partition] = partitionTopics.map(mapFn);
2381
2381
  return result;
2382
2382
  }
2383
+ //#endregion
2384
+ //#region src/build/owners.ts
2385
+ /**
2386
+ * What a generated entry imports its runtime from, read off the discovered
2387
+ * constructs: the surface it serves, every construct a runnable can be built
2388
+ * from, and the database Studio browses.
2389
+ *
2390
+ * `gkm build` and `gkm dev` both write entries that import the surface for its
2391
+ * logger and environment parser, so both ask here. A dev server that skipped it
2392
+ * had nothing to import them from.
2393
+ */
2394
+ function ownersContext(options) {
2395
+ const { declared, sources, workspaceRoot, appRoot, studio } = options;
2396
+ const surfaces = restApis(declared);
2397
+ const primary = servedSurface(declared, workspaceRoot, appRoot) ?? surfaces.find((d) => d.endpoints.length === 0) ?? surfaces[0];
2398
+ const browsable = Object.entries(declared).find(([, d]) => d.kind === "database");
2399
+ const browsableSource = browsable ? sources[browsable[0]] : void 0;
2400
+ const owners = {};
2401
+ for (const [id, declaration] of Object.entries(declared)) {
2402
+ if (declaration.kind !== "rest-api" && declaration.kind !== "worker") continue;
2403
+ const source = sources[id];
2404
+ if (!source) continue;
2405
+ owners[id] = {
2406
+ specifier: source.file,
2407
+ exportName: source.exportName
2408
+ };
2409
+ }
2410
+ const primarySource = primary ? sources[primary.id] : void 0;
2411
+ return {
2412
+ owners,
2413
+ ...primary ? { surface: {
2414
+ id: primary.id,
2415
+ trustedOriginsKey: (0, _geekmidas_manifest.provideKey)(primary.id, "trustedOrigins"),
2416
+ ...primary.cors ? { cors: primary.cors } : {},
2417
+ ...primarySource ? { module: {
2418
+ specifier: primarySource.file,
2419
+ exportName: primarySource.exportName
2420
+ } } : {}
2421
+ } } : {},
2422
+ studio: studio && browsableSource ? {
2423
+ ...studio,
2424
+ database: {
2425
+ specifier: browsableSource.file,
2426
+ exportName: browsableSource.exportName
2427
+ }
2428
+ } : studio
2429
+ };
2430
+ }
2431
+ /**
2432
+ * The endpoints one surface serves.
2433
+ *
2434
+ * One glob finds every surface's endpoints, wherever their files live. A build
2435
+ * keeps the ones built from the surface it serves; one built from no surface at
2436
+ * all has nowhere else to go.
2437
+ */
2438
+ function servedBy(endpoints, surface) {
2439
+ if (!surface) return endpoints;
2440
+ return endpoints.filter(({ construct }) => !construct.surface || construct.surface.id === surface.id);
2441
+ }
2442
+ function restApis(declared) {
2443
+ return Object.values(declared).filter((d) => d.kind === "rest-api");
2444
+ }
2445
+ /**
2446
+ * The surface whose declared `path` is the directory being built — asked of
2447
+ * every surface, because a workspace has several and each app's build serves
2448
+ * exactly one.
2449
+ */
2450
+ function servedSurface(declared, workspaceRoot, appRoot) {
2451
+ return restApis(declared).find((d) => (0, node_path.resolve)(workspaceRoot, d.path) === (0, node_path.resolve)(appRoot));
2452
+ }
2383
2453
  /**
2384
2454
  * Check if any construct across the given arrays has a non-undefined partition.
2385
2455
  * When true, the manifest should use the partitioned shape for that construct type.
@@ -2402,7 +2472,29 @@ function groupInfosByPartition(infos, constructs) {
2402
2472
  return groups;
2403
2473
  }
2404
2474
  //#endregion
2405
- //#region src/build/index.ts
2475
+ //#region src/build/surfaceEntry.ts
2476
+ /** Whether a construct serves itself, asked of the construct. */
2477
+ function servesItself(construct) {
2478
+ return typeof construct?.server === "function";
2479
+ }
2480
+ /**
2481
+ * The surface this app serves, when that surface serves itself.
2482
+ *
2483
+ * An auth server's routes are a wildcard the construct mounts, so a glob finds
2484
+ * nothing and the entry only has to start it. That used to be inferred —
2485
+ * "nothing found, and the surface declares routes" — which is a guess about
2486
+ * the construct from the outside. The construct can simply be asked.
2487
+ */
2488
+ function selfServingSurface(options) {
2489
+ const { declared, sources, workspaceRoot, appRoot } = options;
2490
+ const served = servedSurface(declared, workspaceRoot, appRoot);
2491
+ const source = served ? sources[served.id] : void 0;
2492
+ if (!served || !source || !servesItself(source.construct)) return void 0;
2493
+ return {
2494
+ id: served.id,
2495
+ source
2496
+ };
2497
+ }
2406
2498
  /**
2407
2499
  * The entry for a surface that serves itself.
2408
2500
  *
@@ -2436,6 +2528,8 @@ export const { app } = await surface.server({ envParser, context: snifferContext
2436
2528
  export default app;
2437
2529
  `);
2438
2530
  }
2531
+ //#endregion
2532
+ //#region src/build/index.ts
2439
2533
  const logger$9 = console;
2440
2534
  /**
2441
2535
  * What a build run at the workspace root reads.
@@ -2479,58 +2573,71 @@ async function buildCommand(options) {
2479
2573
  if (studio) logger$9.log(`🗄️ Studio enabled at ${studio.path}`);
2480
2574
  const hooks = normalizeHooksConfig(config.hooks);
2481
2575
  if (hooks) logger$9.log(`🪝 Server hooks enabled`);
2576
+ return buildApp({
2577
+ config,
2578
+ workspaceRoot: loadedConfig.workspace.root,
2579
+ appRoot: process.cwd(),
2580
+ providers: resolved.providers,
2581
+ enableOpenApi: resolved.enableOpenApi,
2582
+ cacheBackend,
2583
+ production,
2584
+ telescope,
2585
+ studio,
2586
+ hooks,
2587
+ markOptional: options.markOptional ?? false,
2588
+ skipBundle: options.skipBundle ?? false,
2589
+ stage: options.stage
2590
+ });
2591
+ }
2592
+ /**
2593
+ * Turn one app's constructs into its generated entry.
2594
+ *
2595
+ * `gkm build` and `gkm dev` both run this. They used to be two copies of it,
2596
+ * and every time the build learned something about constructs — that the
2597
+ * entry takes its logger from the surface, that an auth server serves itself —
2598
+ * dev did not, and failed on exactly the apps the build had learned to handle.
2599
+ */
2600
+ async function buildApp(input) {
2601
+ const { config, workspaceRoot, appRoot, providers, enableOpenApi, cacheBackend, production, telescope, studio, hooks, bustCache = false } = input;
2482
2602
  const constructGlobs = typeof config.constructs === "string" || Array.isArray(config.constructs) ? config.constructs : void 0;
2483
2603
  const constructSources = {};
2484
2604
  const declared = constructGlobs ? await require_discover.discover({
2485
2605
  patterns: constructGlobs,
2486
- cwd: process.cwd(),
2606
+ cwd: appRoot,
2607
+ bustCache,
2487
2608
  sources: constructSources
2488
2609
  }) : {};
2489
- const surfaces = Object.values(declared).filter((d) => d.kind === "rest-api");
2490
- const workspaceRoot = loadedConfig.workspace.root;
2491
- const served = surfaces.find((d) => (0, node_path.resolve)(workspaceRoot, d.path) === (0, node_path.resolve)(process.cwd()));
2492
- const primary = served ?? surfaces.find((d) => d.endpoints.length === 0) ?? surfaces[0];
2493
- const browsable = Object.entries(declared).find(([, d]) => d.kind === "database");
2494
- const browsableSource = browsable ? constructSources[browsable[0]] : void 0;
2495
- const owners = {};
2496
- for (const [id, declaration] of Object.entries(declared)) {
2497
- if (declaration.kind !== "rest-api" && declaration.kind !== "worker") continue;
2498
- const source = constructSources[id];
2499
- if (!source) continue;
2500
- owners[id] = {
2501
- specifier: source.file,
2502
- exportName: source.exportName
2503
- };
2504
- }
2610
+ const selfServing = selfServingSurface({
2611
+ declared,
2612
+ sources: constructSources,
2613
+ workspaceRoot,
2614
+ appRoot
2615
+ });
2616
+ if (selfServing) {
2617
+ await writeSurfaceEntry((0, node_path.join)(appRoot, ".gkm", "server"), selfServing.source);
2618
+ logger$9.log(`Generated a server for ${selfServing.id} from its own declaration`);
2619
+ return { selfServing: selfServing.id };
2620
+ }
2621
+ const derived = ownersContext({
2622
+ declared,
2623
+ sources: constructSources,
2624
+ workspaceRoot,
2625
+ appRoot,
2626
+ studio
2627
+ });
2505
2628
  const buildContext = {
2506
- owners,
2507
- ...primary ? { surface: {
2508
- id: primary.id,
2509
- trustedOriginsKey: (0, _geekmidas_manifest.provideKey)(primary.id, "trustedOrigins"),
2510
- ...primary.cors ? { cors: primary.cors } : {},
2511
- ...constructSources[primary.id] ? { module: {
2512
- specifier: constructSources[primary.id].file,
2513
- exportName: constructSources[primary.id].exportName
2514
- } } : {}
2515
- } } : {},
2629
+ ...derived,
2516
2630
  telescope,
2517
- studio: studio && browsableSource ? {
2518
- ...studio,
2519
- database: {
2520
- specifier: browsableSource.file,
2521
- exportName: browsableSource.exportName
2522
- }
2523
- } : studio,
2524
2631
  hooks,
2525
2632
  production,
2526
2633
  constructGlobs,
2527
2634
  cacheBackend,
2528
2635
  emailBackend: "ses",
2529
2636
  storageDrivers: driversFor({
2530
- appRoot: process.cwd(),
2637
+ appRoot,
2531
2638
  cache: cacheBackendsIn(declared, cacheBackend)
2532
2639
  }),
2533
- markOptional: options.markOptional ?? false
2640
+ markOptional: input.markOptional ?? false
2534
2641
  };
2535
2642
  const endpointGenerator = new require_openapi.EndpointGenerator();
2536
2643
  const functionGenerator = new FunctionGenerator();
@@ -2540,43 +2647,37 @@ async function buildCommand(options) {
2540
2647
  const topicGenerator = new TopicGenerator();
2541
2648
  const code = constructGlobs;
2542
2649
  const [loadedEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics] = await Promise.all([
2543
- endpointGenerator.load(code),
2544
- functionGenerator.load(code),
2545
- cronGenerator.load(code),
2546
- subscriberGenerator.load(code),
2547
- queueGenerator.load(code),
2548
- topicGenerator.load(code)
2650
+ endpointGenerator.load(code, appRoot, bustCache),
2651
+ functionGenerator.load(code, appRoot, bustCache),
2652
+ cronGenerator.load(code, appRoot, bustCache),
2653
+ subscriberGenerator.load(code, appRoot, bustCache),
2654
+ queueGenerator.load(code, appRoot, bustCache),
2655
+ topicGenerator.load(code, appRoot, bustCache)
2549
2656
  ]);
2550
- const allEndpoints = primary ? loadedEndpoints.filter(({ construct }) => !construct.surface || construct.surface.id === primary.id) : loadedEndpoints;
2657
+ const allEndpoints = servedBy(loadedEndpoints, derived.surface);
2551
2658
  logger$9.log(`Found ${allEndpoints.length} endpoints`);
2552
2659
  logger$9.log(`Found ${allFunctions.length} functions`);
2553
2660
  logger$9.log(`Found ${allCrons.length} crons`);
2554
2661
  logger$9.log(`Found ${allSubscribers.length} subscribers`);
2555
2662
  logger$9.log(`Found ${allQueues.length} queues`);
2556
2663
  logger$9.log(`Found ${allTopics.length} topics`);
2557
- if (allEndpoints.length === 0 && allFunctions.length === 0 && allCrons.length === 0 && allSubscribers.length === 0 && allQueues.length === 0 && allTopics.length === 0) {
2558
- const selfServing = served && served.endpoints.length > 0 && constructSources[served.id] !== void 0 ? [served.id, served] : void 0;
2559
- if (selfServing) {
2560
- const [id] = selfServing;
2561
- await writeSurfaceEntry((0, node_path.join)(process.cwd(), ".gkm", "server"), constructSources[id]);
2562
- logger$9.log(`Generated a server for ${id} from its own declaration`);
2563
- return {};
2564
- }
2664
+ if (!input.serveEmpty && allEndpoints.length === 0 && allFunctions.length === 0 && allCrons.length === 0 && allSubscribers.length === 0 && allQueues.length === 0 && allTopics.length === 0) {
2565
2665
  logger$9.log("No endpoints, functions, crons, subscribers, queues, or topics found to process");
2566
2666
  return {};
2567
2667
  }
2568
- const rootOutputDir = (0, node_path.join)(process.cwd(), ".gkm");
2668
+ const rootOutputDir = (0, node_path.join)(appRoot, ".gkm");
2569
2669
  await (0, node_fs_promises.mkdir)(rootOutputDir, { recursive: true });
2570
2670
  let result = {};
2571
- for (const provider of resolved.providers) {
2572
- const providerResult = await buildForProvider(provider, buildContext, rootOutputDir, endpointGenerator, functionGenerator, cronGenerator, subscriberGenerator, queueGenerator, topicGenerator, allEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics, resolved.enableOpenApi, options.skipBundle ?? false, options.stage);
2671
+ for (const provider of providers) {
2672
+ const providerResult = await buildForProvider(provider, buildContext, appRoot, declared, endpointGenerator, functionGenerator, cronGenerator, subscriberGenerator, queueGenerator, topicGenerator, allEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics, enableOpenApi, input.skipBundle ?? false, input.stage);
2573
2673
  if (providerResult.masterKey) result = providerResult;
2574
2674
  }
2575
2675
  await require_openapi.generateOpenApi(allEndpoints.map(({ construct }) => construct), { openapi: config.openapi });
2576
2676
  return result;
2577
2677
  }
2578
- async function buildForProvider(provider, context, rootOutputDir, endpointGenerator, functionGenerator, cronGenerator, subscriberGenerator, queueGenerator, topicGenerator, endpoints, functions, crons, subscribers, queues, topics, enableOpenApi, skipBundle, stage) {
2579
- const outputDir = (0, node_path.join)(process.cwd(), ".gkm", provider);
2678
+ async function buildForProvider(provider, context, appRoot, discovered, endpointGenerator, functionGenerator, cronGenerator, subscriberGenerator, queueGenerator, topicGenerator, endpoints, functions, crons, subscribers, queues, topics, enableOpenApi, skipBundle, stage) {
2679
+ const rootOutputDir = (0, node_path.join)(appRoot, ".gkm");
2680
+ const outputDir = (0, node_path.join)(rootOutputDir, provider);
2580
2681
  await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
2581
2682
  logger$9.log(`\nGenerating handlers for provider: ${provider}`);
2582
2683
  const [routes, functionInfos, cronInfos, subscriberInfos, queueInfos, topicInfos] = await Promise.all([
@@ -2592,10 +2693,6 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
2592
2693
  ]);
2593
2694
  logger$9.log(`Generated ${routes.length} routes, ${functionInfos.length} functions, ${cronInfos.length} crons, ${subscriberInfos.length} subscribers, ${queueInfos.length} queues, ${topicInfos.length} topics for ${provider}`);
2594
2695
  const manifestRoutes = assembleManifestField(routes, endpoints);
2595
- const discovered = context.constructGlobs ? await require_discover.discover({
2596
- patterns: context.constructGlobs,
2597
- cwd: process.cwd()
2598
- }) : {};
2599
2696
  const manifestFunctions = assembleManifestField(functionInfos, functions);
2600
2697
  const manifestCrons = assembleManifestField(cronInfos, crons);
2601
2698
  const manifestQueues = assembleManifestField(queueInfos, queues);
@@ -2609,8 +2706,8 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
2609
2706
  authorizer: construct.authorizer?.name ?? "none"
2610
2707
  }))), endpoints);
2611
2708
  await generateServerManifest(rootOutputDir, {
2612
- handler: (0, node_path.relative)(process.cwd(), (0, node_path.join)(outputDir, "app.ts")),
2613
- endpoints: (0, node_path.relative)(process.cwd(), (0, node_path.join)(outputDir, "endpoints.ts"))
2709
+ handler: (0, node_path.relative)(appRoot, (0, node_path.join)(outputDir, "app.ts")),
2710
+ endpoints: (0, node_path.relative)(appRoot, (0, node_path.join)(outputDir, "endpoints.ts"))
2614
2711
  }, serverRouteField, manifestSubscribers, manifestQueues, manifestTopics);
2615
2712
  let masterKey;
2616
2713
  if (context.production?.bundle && !skipBundle) {
@@ -2689,14 +2786,9 @@ function getTurboCommand(pm, filters = []) {
2689
2786
  function turboFilters(workspace) {
2690
2787
  const filters = [];
2691
2788
  const unpackaged = [];
2692
- for (const [appName, app] of Object.entries(workspace.apps)) {
2693
- const pkgPath = (0, node_path.join)(workspace.root, app.path, "package.json");
2694
- if (!(0, node_fs.existsSync)(pkgPath)) {
2695
- unpackaged.push(appName);
2696
- continue;
2697
- }
2698
- const name = JSON.parse((0, node_fs.readFileSync)(pkgPath, "utf8")).name;
2699
- if (typeof name === "string" && name.length > 0) filters.push(name);
2789
+ for (const appName of Object.keys(workspace.apps)) {
2790
+ const name = appPackageName(workspace, appName);
2791
+ if (name) filters.push(name);
2700
2792
  else unpackaged.push(appName);
2701
2793
  }
2702
2794
  return {
@@ -2705,6 +2797,20 @@ function turboFilters(workspace) {
2705
2797
  };
2706
2798
  }
2707
2799
  /**
2800
+ * The package name turbo knows an app by, from its own `package.json`.
2801
+ *
2802
+ * Not the app's key: `api` in the workspace is `@shop/api` to turbo, and a
2803
+ * filter naming the key matches nothing.
2804
+ */
2805
+ function appPackageName(workspace, appName) {
2806
+ const app = workspace.apps[appName];
2807
+ if (!app) return void 0;
2808
+ const pkgPath = (0, node_path.join)(workspace.root, app.path, "package.json");
2809
+ if (!(0, node_fs.existsSync)(pkgPath)) return void 0;
2810
+ const name = JSON.parse((0, node_fs.readFileSync)(pkgPath, "utf8")).name;
2811
+ return typeof name === "string" && name.length > 0 ? name : void 0;
2812
+ }
2813
+ /**
2708
2814
  * Build all apps in a workspace using Turbo for dependency-ordered parallel builds.
2709
2815
  * @internal Exported for testing
2710
2816
  */
@@ -8420,26 +8526,26 @@ function loadPackageJson() {
8420
8526
  * Run: pnpm --filter @geekmidas/cli sync-versions
8421
8527
  */
8422
8528
  const GEEKMIDAS_VERSIONS = {
8423
- "@geekmidas/audit": "~10.0.0-alpha.23",
8424
- "@geekmidas/auth": "~10.0.0-alpha.23",
8425
- "@geekmidas/cache": "~10.0.0-alpha.23",
8426
- "@geekmidas/client": "~10.0.0-alpha.23",
8427
- "@geekmidas/cloud": "~10.0.0-alpha.23",
8428
- "@geekmidas/constructs": "~10.0.0-alpha.23",
8429
- "@geekmidas/db": "~10.0.0-alpha.23",
8430
- "@geekmidas/emailkit": "~10.0.0-alpha.23",
8431
- "@geekmidas/envkit": "~10.0.0-alpha.23",
8432
- "@geekmidas/errors": "~10.0.0-alpha.23",
8433
- "@geekmidas/events": "~10.0.0-alpha.23",
8434
- "@geekmidas/logger": "~10.0.0-alpha.23",
8435
- "@geekmidas/manifest": "~10.0.0-alpha.23",
8436
- "@geekmidas/rate-limit": "~10.0.0-alpha.23",
8437
- "@geekmidas/schema": "~10.0.0-alpha.23",
8438
- "@geekmidas/services": "~10.0.0-alpha.23",
8439
- "@geekmidas/storage": "~10.0.0-alpha.23",
8440
- "@geekmidas/studio": "~10.0.0-alpha.23",
8441
- "@geekmidas/telescope": "~10.0.0-alpha.23",
8442
- "@geekmidas/testkit": "~10.0.0-alpha.23",
8529
+ "@geekmidas/audit": "~10.0.0-alpha.25",
8530
+ "@geekmidas/auth": "~10.0.0-alpha.25",
8531
+ "@geekmidas/cache": "~10.0.0-alpha.25",
8532
+ "@geekmidas/client": "~10.0.0-alpha.25",
8533
+ "@geekmidas/cloud": "~10.0.0-alpha.25",
8534
+ "@geekmidas/constructs": "~10.0.0-alpha.25",
8535
+ "@geekmidas/db": "~10.0.0-alpha.25",
8536
+ "@geekmidas/emailkit": "~10.0.0-alpha.25",
8537
+ "@geekmidas/envkit": "~10.0.0-alpha.25",
8538
+ "@geekmidas/errors": "~10.0.0-alpha.25",
8539
+ "@geekmidas/events": "~10.0.0-alpha.25",
8540
+ "@geekmidas/logger": "~10.0.0-alpha.25",
8541
+ "@geekmidas/manifest": "~10.0.0-alpha.25",
8542
+ "@geekmidas/rate-limit": "~10.0.0-alpha.25",
8543
+ "@geekmidas/schema": "~10.0.0-alpha.25",
8544
+ "@geekmidas/services": "~10.0.0-alpha.25",
8545
+ "@geekmidas/storage": "~10.0.0-alpha.25",
8546
+ "@geekmidas/studio": "~10.0.0-alpha.25",
8547
+ "@geekmidas/telescope": "~10.0.0-alpha.25",
8548
+ "@geekmidas/testkit": "~10.0.0-alpha.25",
8443
8549
  "@geekmidas/cli": `~${loadPackageJson().version}`
8444
8550
  };
8445
8551
  //#endregion
@@ -15860,7 +15966,7 @@ program.command("dev").description("Reconcile constructs and start the dev serve
15860
15966
  const globalOptions = program.opts();
15861
15967
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
15862
15968
  await devCommand({
15863
- port: options.port ? Number.parseInt(options.port, 10) : 3e3,
15969
+ port: options.port ? Number.parseInt(options.port, 10) : void 0,
15864
15970
  portExplicit: !!options.port,
15865
15971
  enableOpenApi: options.enableOpenapi ?? true,
15866
15972
  entry: options.entry,
@@ -16013,7 +16119,7 @@ program.command("secrets:push").description("Send a deployed stage's secrets to
16013
16119
  const globalOptions = program.opts();
16014
16120
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16015
16121
  const { loadWorkspaceConfig } = await Promise.resolve().then(() => require("./config.cjs"));
16016
- const { pushStageSecrets } = await Promise.resolve().then(() => require("./transfer-BZbyaV3I.cjs")).then((n) => n.transfer_exports);
16122
+ const { pushStageSecrets } = await Promise.resolve().then(() => require("./transfer-D59M4aaR.cjs")).then((n) => n.transfer_exports);
16017
16123
  const { workspace } = await loadWorkspaceConfig();
16018
16124
  const { addedKeys } = await pushStageSecrets(workspace, options.stage, { profile: options.profile });
16019
16125
  for (const key of addedKeys) console.log(` + ${key} (reconciled)`);
@@ -16028,7 +16134,7 @@ program.command("secrets:pull").description("Bring a deployed stage's secrets fr
16028
16134
  const globalOptions = program.opts();
16029
16135
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16030
16136
  const { loadWorkspaceConfig } = await Promise.resolve().then(() => require("./config.cjs"));
16031
- const { pullStageSecrets } = await Promise.resolve().then(() => require("./transfer-BZbyaV3I.cjs")).then((n) => n.transfer_exports);
16137
+ const { pullStageSecrets } = await Promise.resolve().then(() => require("./transfer-D59M4aaR.cjs")).then((n) => n.transfer_exports);
16032
16138
  const { workspace } = await loadWorkspaceConfig();
16033
16139
  const { addedKeys } = await pullStageSecrets(workspace, options.stage, { profile: options.profile });
16034
16140
  for (const key of addedKeys) console.log(` + ${key} (reconciled)`);
@@ -16052,7 +16158,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
16052
16158
  console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
16053
16159
  process.exit(1);
16054
16160
  }
16055
- const { derivedContainers } = await Promise.resolve().then(() => require("./credentials-COrSpZfF.cjs")).then((n) => n.workspace_exports);
16161
+ const { derivedContainers } = await Promise.resolve().then(() => require("./credentials-Dbyccl3A.cjs")).then((n) => n.workspace_exports);
16056
16162
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
16057
16163
  if (!result) {
16058
16164
  console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
@@ -16111,7 +16217,7 @@ program.command("deploy:github").description("Let GitHub Actions deploy a stage:
16111
16217
  try {
16112
16218
  const globalOptions = program.opts();
16113
16219
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16114
- const { deployGithubCommand } = await Promise.resolve().then(() => require("./github-BpsnSDw2.cjs"));
16220
+ const { deployGithubCommand } = await Promise.resolve().then(() => require("./github-CUjW3G91.cjs"));
16115
16221
  await deployGithubCommand(options);
16116
16222
  } catch (error) {
16117
16223
  console.error(formatError(error));