@geekmidas/cli 10.0.0-alpha.24 → 10.0.0-alpha.26

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