@geekmidas/cli 10.0.0-alpha.5 → 10.0.0-alpha.6

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 (65) hide show
  1. package/dist/{config-C21ocYNd.cjs → config-CNz5KaSR.cjs} +16 -45
  2. package/dist/config-CNz5KaSR.cjs.map +1 -0
  3. package/dist/{config-WRdsQYZK.mjs → config-CxP7P-qG.mjs} +17 -46
  4. package/dist/config-CxP7P-qG.mjs.map +1 -0
  5. package/dist/config.cjs +2 -2
  6. package/dist/config.d.cts +2 -2
  7. package/dist/config.d.mts +2 -2
  8. package/dist/config.mjs +2 -2
  9. package/dist/{credentials-BBIRJVp4.mjs → credentials-BK3Z93MX.mjs} +5 -5
  10. package/dist/credentials-BK3Z93MX.mjs.map +1 -0
  11. package/dist/{credentials-G1JwBoXH.cjs → credentials-DzsXZzkx.cjs} +5 -5
  12. package/dist/credentials-DzsXZzkx.cjs.map +1 -0
  13. package/dist/discover-CAlFgYec.mjs.map +1 -1
  14. package/dist/discover-DTmFeloL.cjs.map +1 -1
  15. package/dist/{index-BThUzEIp.d.mts → index-BGGdjueF.d.cts} +2 -2
  16. package/dist/index-BGGdjueF.d.cts.map +1 -0
  17. package/dist/{index-CHB4tSzX.d.cts → index-DEbaleny.d.mts} +2 -2
  18. package/dist/index-DEbaleny.d.mts.map +1 -0
  19. package/dist/index.cjs +307 -121
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.mjs +307 -121
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/{openapi-kkQbsRVj.mjs → openapi-Bw8DWFWz.mjs} +104 -50
  24. package/dist/openapi-Bw8DWFWz.mjs.map +1 -0
  25. package/dist/{openapi-CA0yJUTN.cjs → openapi-D-dx9Akj.cjs} +122 -50
  26. package/dist/openapi-D-dx9Akj.cjs.map +1 -0
  27. package/dist/openapi.cjs +5 -3
  28. package/dist/openapi.d.cts +46 -10
  29. package/dist/openapi.d.cts.map +1 -1
  30. package/dist/openapi.d.mts +46 -10
  31. package/dist/openapi.d.mts.map +1 -1
  32. package/dist/openapi.mjs +4 -4
  33. package/dist/reconcile/public.d.cts +1 -1
  34. package/dist/reconcile/public.d.mts +1 -1
  35. package/dist/setup-8IWPsRIN.cjs +11 -0
  36. package/dist/{setup-DXvRLTZN.mjs → setup-BWMhbW-p.mjs} +3 -3
  37. package/dist/{setup-DXvRLTZN.mjs.map → setup-BWMhbW-p.mjs.map} +1 -1
  38. package/dist/{setup-Cm2UnAdh.cjs → setup-D7L4BgBT.cjs} +3 -3
  39. package/dist/{setup-Cm2UnAdh.cjs.map → setup-D7L4BgBT.cjs.map} +1 -1
  40. package/dist/{setup-ZijS4qeC.mjs → setup-DGIPd2fh.mjs} +4 -4
  41. package/dist/{types-C-arnQ1r.d.mts → types-BLSNdrk0.d.cts} +5 -13
  42. package/dist/{types-C-arnQ1r.d.mts.map → types-BLSNdrk0.d.cts.map} +1 -1
  43. package/dist/{types-abJQuk7m.d.cts → types-C3POXhw3.d.mts} +2 -10
  44. package/dist/{types-abJQuk7m.d.cts.map → types-C3POXhw3.d.mts.map} +1 -1
  45. package/dist/workspace/index.cjs +1 -1
  46. package/dist/workspace/index.d.cts +2 -2
  47. package/dist/workspace/index.d.mts +2 -2
  48. package/dist/workspace/index.mjs +1 -1
  49. package/dist/{workspace-DdJss8I6.cjs → workspace-CGaUAw5A.cjs} +3 -3
  50. package/dist/workspace-CnTqIlB3.mjs +8 -0
  51. package/dist/{workspace-BFMrAKqI.mjs → workspace-DIuZx-cc.mjs} +1 -17
  52. package/dist/{workspace-BFMrAKqI.mjs.map → workspace-DIuZx-cc.mjs.map} +1 -1
  53. package/dist/{workspace-BcucjwvA.cjs → workspace-LKiNyaDy.cjs} +1 -17
  54. package/dist/{workspace-BcucjwvA.cjs.map → workspace-LKiNyaDy.cjs.map} +1 -1
  55. package/package.json +11 -11
  56. package/dist/config-C21ocYNd.cjs.map +0 -1
  57. package/dist/config-WRdsQYZK.mjs.map +0 -1
  58. package/dist/credentials-BBIRJVp4.mjs.map +0 -1
  59. package/dist/credentials-G1JwBoXH.cjs.map +0 -1
  60. package/dist/index-BThUzEIp.d.mts.map +0 -1
  61. package/dist/index-CHB4tSzX.d.cts.map +0 -1
  62. package/dist/openapi-CA0yJUTN.cjs.map +0 -1
  63. package/dist/openapi-kkQbsRVj.mjs.map +0 -1
  64. package/dist/setup-B5_oHHUj.cjs +0 -11
  65. package/dist/workspace-k4l3rsFh.mjs +0 -8
package/dist/index.cjs CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  const require_chunk = require('./chunk-CUT6urMc.cjs');
3
3
  const require_discover = require('./discover-DTmFeloL.cjs');
4
- const require_config = require('./config-C21ocYNd.cjs');
5
- const require_workspace = require('./workspace-BcucjwvA.cjs');
4
+ const require_config = require('./config-CNz5KaSR.cjs');
5
+ const require_workspace = require('./workspace-LKiNyaDy.cjs');
6
6
  const require_credentials = require('./credentials-BapzjyV5.cjs');
7
- const require_credentials$1 = require('./credentials-G1JwBoXH.cjs');
7
+ const require_credentials$1 = require('./credentials-DzsXZzkx.cjs');
8
8
  const require_generator = require('./generator-CNzBw7g5.cjs');
9
9
  const require_storage = require('./storage-BJmzYxt8.cjs');
10
- const require_openapi = require('./openapi-CA0yJUTN.cjs');
10
+ const require_openapi = require('./openapi-D-dx9Akj.cjs');
11
11
  const require_emit = require('./emit-CbcRolsa.cjs');
12
12
  const require_dokploy_api = require('./dokploy-api-BsVH0y_s.cjs');
13
13
  const require_encryption = require('./encryption-C6jaU0hE.cjs');
@@ -15,7 +15,7 @@ const require_CachedStateProvider = require('./CachedStateProvider-CqPdW7CB.cjs'
15
15
  const require_fullstack_secrets = require('./fullstack-secrets-C-GGohke.cjs');
16
16
  const require_openapi_react_query = require('./openapi-react-query-CmJ1xDbn.cjs');
17
17
  require('./sync-SizaChmC.cjs');
18
- const require_setup = require('./setup-Cm2UnAdh.cjs');
18
+ const require_setup = require('./setup-D7L4BgBT.cjs');
19
19
  const node_fs = require_chunk.__toESM(require("node:fs"));
20
20
  const node_path = require_chunk.__toESM(require("node:path"));
21
21
  const __geekmidas_manifest = require_chunk.__toESM(require("@geekmidas/manifest"));
@@ -43,7 +43,7 @@ const prompts = require_chunk.__toESM(require("prompts"));
43
43
 
44
44
  //#region package.json
45
45
  var name = "@geekmidas/cli";
46
- var version = "10.0.0-alpha.4";
46
+ var version = "10.0.0-alpha.5";
47
47
  var description = "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs";
48
48
  var private$1 = false;
49
49
  var type = "module";
@@ -415,11 +415,16 @@ var CronGenerator = class extends require_discover.ConstructGenerator {
415
415
  const provider = options?.provider || "aws-lambda";
416
416
  const logger$12 = console;
417
417
  const cronInfos = [];
418
+ if (provider === "server") {
419
+ await this.generateServerCronsFile(outputDir, constructs);
420
+ logger$12.log(`Generated server crons file with ${constructs.length} crons`);
421
+ return cronInfos;
422
+ }
418
423
  if (constructs.length === 0 || provider !== "aws-lambda") return cronInfos;
419
424
  const cronsDir = (0, node_path.join)(outputDir, "crons");
420
425
  await (0, node_fs_promises.mkdir)(cronsDir, { recursive: true });
421
426
  for (const { key, construct, path } of constructs) {
422
- const handlerFile = await this.generateCronHandler(cronsDir, path.relative, key, context);
427
+ const handlerFile = await this.generateCronHandler(cronsDir, path.relative, key, context, construct.owner);
423
428
  cronInfos.push({
424
429
  name: key,
425
430
  handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
@@ -436,17 +441,173 @@ var CronGenerator = class extends require_discover.ConstructGenerator {
436
441
  isConstruct(value) {
437
442
  return __geekmidas_constructs_crons.Cron.isCron(value);
438
443
  }
439
- async generateCronHandler(outputDir, sourceFile, exportName, context) {
444
+ /**
445
+ * The crons file a server entry imports.
446
+ *
447
+ * Shaped after `generateServerSubscribersFile`: one module, every cron, a
448
+ * `setupCrons` the generated entry calls beside `setupSubscribers`.
449
+ *
450
+ * It picks its scheduler from what the process can reach. With a Postgres it
451
+ * uses pg-boss, whose schedules live in the database — so a deployment
452
+ * running four replicas fires each job once, which is the thing a timer in
453
+ * every process gets wrong and does not report. With no database it falls
454
+ * back to timers and says plainly that it is doing so.
455
+ */
456
+ async generateServerCronsFile(outputDir, crons) {
457
+ await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
458
+ const cronsPath = (0, node_path.join)(outputDir, "crons.ts");
459
+ const importsByFile = /* @__PURE__ */ new Map();
460
+ for (const { path, key } of crons) {
461
+ const relativePath = (0, node_path.relative)((0, node_path.dirname)(cronsPath), path.relative);
462
+ const importPath = relativePath.replace(/\.ts$/, ".js");
463
+ if (!importsByFile.has(importPath)) importsByFile.set(importPath, []);
464
+ importsByFile.get(importPath)?.push(key);
465
+ }
466
+ const imports = Array.from(importsByFile.entries()).map(([importPath, exports$2]) => `import { ${exports$2.join(", ")} } from '${importPath}';`).join("\n");
467
+ const entries = crons.map(({ key }) => ` { name: '${key}', cron: ${key} }`).join(",\n");
468
+ const content = `/**
469
+ * Generated cron setup.
470
+ *
471
+ * Schedules every cron this app declares, in the process that serves it.
472
+ *
473
+ * The schedule lives in Postgres, through pg-boss, in the database the worker
474
+ * named with \`.database(db)\`. That is what makes running several replicas
475
+ * safe: each job fires once, where a timer in every process fires it once per
476
+ * replica and reports nothing.
477
+ *
478
+ * No connection string appears here. The construct that owns the database is
479
+ * the only thing that knows its key, and it is resolved through service
480
+ * discovery like any other dependency.
481
+ */
482
+ import type { EnvironmentParser } from '@geekmidas/envkit';
483
+ import type { Logger } from '@geekmidas/logger';
484
+ import { toCronExpression } from '@geekmidas/constructs/crons';
485
+ import { ServiceDiscovery } from '@geekmidas/services';
486
+ ${imports}
487
+
488
+ const crons = [
489
+ ${entries}
490
+ ];
491
+
492
+ type Stoppable = { stop: () => Promise<void> | void };
493
+ const running: Stoppable[] = [];
494
+
495
+ export async function setupCrons(
496
+ envParser: EnvironmentParser<any>,
497
+ logger: Logger,
498
+ ): Promise<void> {
499
+ if (crons.length === 0) {
500
+ return;
501
+ }
502
+
503
+ const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
504
+
505
+ // Resolved once per cron rather than per firing: registering services on
506
+ // every tick would reconnect a database every minute.
507
+ const prepared = [];
508
+ for (const { name, cron } of crons) {
509
+ try {
510
+ const schedule = toCronExpression(cron.schedule);
511
+ const services = cron.services.length > 0
512
+ ? await serviceDiscovery.register(cron.services)
513
+ : {};
514
+
515
+ prepared.push({ name, cron, schedule, services });
516
+ } catch (error) {
517
+ // An unrepresentable schedule is a build-time mistake, and failing the
518
+ // process for it would take the HTTP server down with it.
519
+ logger.error({ error, cron: name }, 'Cron has no runnable schedule, skipping');
520
+ }
521
+ }
522
+
523
+ const run = async (entry: (typeof prepared)[number]) => {
524
+ try {
525
+ await entry.cron.handler({
526
+ input: undefined,
527
+ services: entry.services,
528
+ logger: entry.cron.logger,
529
+ });
530
+ } catch (error) {
531
+ logger.error({ error, cron: entry.name }, 'Cron failed');
532
+ }
533
+ };
534
+
535
+ // Declared once on the worker, carried onto every cron it built — so this
536
+ // reads it from any of them rather than from config or the environment.
537
+ const scheduleStore = prepared.find(({ cron }) => cron.scheduleStore)?.cron
538
+ .scheduleStore;
539
+
540
+ if (!scheduleStore) {
541
+ // Declared, not discovered. A worker says where its schedules live with
542
+ // \`.database(db)\`; inferring one from whatever database happened to be
543
+ // around would move the schedules the day an app declared a second.
544
+ logger.error(
545
+ { crons: prepared.map(({ name }) => name) },
546
+ 'These crons have nowhere to keep their schedule. A server fires its own ' +
547
+ 'crons, and a process scheduling in memory fires each job once per ' +
548
+ 'replica — so the schedule lives in Postgres. Say where with ' +
549
+ '\`new Worker(…).database(db)\`.',
550
+ );
551
+ return;
552
+ }
553
+
554
+ const [store] = await serviceDiscovery
555
+ .register([scheduleStore])
556
+ .then((s) => Object.values(s));
557
+
558
+ const { PgBoss } = await import('pg-boss');
559
+ // The pool the construct already opened — no connection string is read here,
560
+ // and none is named anywhere outside the construct that owns it.
561
+ const boss = new PgBoss({ db: store as never });
562
+ await boss.start();
563
+
564
+ // What this app declares now. Anything else under the prefix belonged to a
565
+ // cron since renamed or deleted, and would keep firing at a handler that has
566
+ // gone.
567
+ //
568
+ // The prefix is \`cron.\` rather than \`cron:\`: pg-boss accepts
569
+ // alphanumerics, underscores, hyphens, periods and slashes in a queue name,
570
+ // and rejects a colon outright.
571
+ const declared = new Set(prepared.map(({ name }) => \`cron.\${name}\`));
572
+ for (const existing of await boss.getSchedules()) {
573
+ if (existing.name.startsWith('cron.') && !declared.has(existing.name)) {
574
+ await boss.unschedule(existing.name);
575
+ logger.info({ cron: existing.name }, 'Removed a schedule nothing declares');
576
+ }
577
+ }
578
+
579
+ for (const entry of prepared) {
580
+ const queue = \`cron.\${entry.name}\`;
581
+ await boss.createQueue(queue);
582
+ // UTC, so this agrees with the schedule a deploy target would have made.
583
+ await boss.schedule(queue, entry.schedule, undefined, { tz: 'UTC' });
584
+ await boss.work(queue, () => run(entry));
585
+
586
+ logger.info({ cron: entry.name, schedule: entry.schedule }, 'Cron scheduled');
587
+ }
588
+
589
+ running.push({ stop: () => boss.stop() });
590
+ logger.info({ count: prepared.length }, 'Crons scheduled');
591
+ }
592
+
593
+ export async function stopCrons(): Promise<void> {
594
+ await Promise.all(running.map((r) => r.stop()));
595
+ running.length = 0;
596
+ }
597
+ `;
598
+ await (0, node_fs_promises.writeFile)(cronsPath, content);
599
+ return cronsPath;
600
+ }
601
+ async generateCronHandler(outputDir, sourceFile, exportName, context, owner) {
440
602
  const handlerFileName = `${exportName}.ts`;
441
603
  const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
442
604
  const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
443
605
  const importPath = relativePath.replace(/\.ts$/, ".js");
444
- const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
445
- const relativeLoggerPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.loggerPath);
606
+ const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
446
607
  const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/aws';
447
608
  import { ${exportName} } from '${importPath}';
448
- import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
449
- import ${context.loggerImportPattern} from '${relativeLoggerPath}';
609
+ ${runtime.imports}
610
+ ${runtime.bindings}
450
611
 
451
612
  const adapter = new AWSScheduledFunction(envParser, ${exportName});
452
613
 
@@ -562,7 +723,7 @@ var FunctionGenerator = class extends require_discover.ConstructGenerator {
562
723
  const functionsDir = (0, node_path.join)(outputDir, "functions");
563
724
  await (0, node_fs_promises.mkdir)(functionsDir, { recursive: true });
564
725
  for (const { key, construct, path } of constructs) {
565
- const handlerFile = await this.generateFunctionHandler(functionsDir, path.relative, key, context);
726
+ const handlerFile = await this.generateFunctionHandler(functionsDir, path.relative, key, context, construct.owner);
566
727
  functionInfos.push({
567
728
  name: key,
568
729
  handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
@@ -575,17 +736,16 @@ var FunctionGenerator = class extends require_discover.ConstructGenerator {
575
736
  }
576
737
  return functionInfos;
577
738
  }
578
- async generateFunctionHandler(outputDir, sourceFile, exportName, context) {
739
+ async generateFunctionHandler(outputDir, sourceFile, exportName, context, owner) {
579
740
  const handlerFileName = `${exportName}.ts`;
580
741
  const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
581
742
  const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
582
743
  const importPath = relativePath.replace(/\.ts$/, ".js");
583
- const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
584
- const relativeLoggerPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.loggerPath);
744
+ const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
585
745
  const content = `import { AWSLambdaFunction } from '@geekmidas/constructs/aws';
586
746
  import { ${exportName} } from '${importPath}';
587
- import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
588
- import ${context.loggerImportPattern} from '${relativeLoggerPath}';
747
+ ${runtime.imports}
748
+ ${runtime.bindings}
589
749
 
590
750
  const adapter = new AWSLambdaFunction(envParser, ${exportName});
591
751
 
@@ -624,7 +784,7 @@ var QueueGenerator = class extends require_discover.ConstructGenerator {
624
784
  const queuesDir = (0, node_path.join)(outputDir, "queues");
625
785
  await (0, node_fs_promises.mkdir)(queuesDir, { recursive: true });
626
786
  for (const { key, construct, path } of constructs) {
627
- const handlerFile = await this.generateQueueHandler(queuesDir, path.relative, key, context);
787
+ const handlerFile = await this.generateQueueHandler(queuesDir, path.relative, key, context, construct.owner);
628
788
  queueInfos.push({
629
789
  name: construct.name,
630
790
  handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
@@ -638,13 +798,14 @@ var QueueGenerator = class extends require_discover.ConstructGenerator {
638
798
  }
639
799
  return queueInfos;
640
800
  }
641
- async generateQueueHandler(outputDir, sourceFile, exportName, context) {
801
+ async generateQueueHandler(outputDir, sourceFile, exportName, context, owner) {
642
802
  const handlerPath = (0, node_path.join)(outputDir, `${exportName}.ts`);
643
803
  const importPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile).replace(/\.ts$/, ".js");
644
- const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
804
+ const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
645
805
  const content = `import { AWSLambdaQueue } from '@geekmidas/constructs/aws';
646
806
  import { ${exportName} } from '${importPath}';
647
- import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
807
+ ${runtime.imports}
808
+ ${runtime.bindings}
648
809
 
649
810
  const adapter = new AWSLambdaQueue(envParser, ${exportName});
650
811
 
@@ -787,7 +948,7 @@ var SubscriberGenerator = class extends require_discover.ConstructGenerator {
787
948
  const subscribersDir = (0, node_path.join)(outputDir, "subscribers");
788
949
  await (0, node_fs_promises.mkdir)(subscribersDir, { recursive: true });
789
950
  for (const { key, construct, path } of constructs) {
790
- const handlerFile = await this.generateSubscriberHandler(subscribersDir, path.relative, key, construct, context);
951
+ const handlerFile = await this.generateSubscriberHandler(subscribersDir, path.relative, key, construct, context, construct.owner);
791
952
  subscriberInfos.push({
792
953
  name: key,
793
954
  handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
@@ -805,15 +966,16 @@ var SubscriberGenerator = class extends require_discover.ConstructGenerator {
805
966
  }
806
967
  return subscriberInfos;
807
968
  }
808
- async generateSubscriberHandler(outputDir, sourceFile, exportName, _subscriber, context) {
969
+ async generateSubscriberHandler(outputDir, sourceFile, exportName, _subscriber, context, owner) {
809
970
  const handlerFileName = `${exportName}.ts`;
810
971
  const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
811
972
  const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
812
973
  const importPath = relativePath.replace(/\.ts$/, ".js");
813
- const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
974
+ const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
814
975
  const content = `import { AWSLambdaSubscriber } from '@geekmidas/constructs/aws';
815
976
  import { ${exportName} } from '${importPath}';
816
- import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
977
+ ${runtime.imports}
978
+ ${runtime.bindings}
817
979
 
818
980
  const adapter = new AWSLambdaSubscriber(envParser, ${exportName});
819
981
 
@@ -1196,13 +1358,7 @@ async function devCommand(options) {
1196
1358
  }
1197
1359
  const resolved = resolveProviders(config, { provider: "server" });
1198
1360
  logger$9.log("🚀 Starting development server...");
1199
- logger$9.log(`Loading routes from: ${config.routes}`);
1200
- if (config.functions) logger$9.log(`Loading functions from: ${config.functions}`);
1201
- if (config.crons) logger$9.log(`Loading crons from: ${config.crons}`);
1202
- if (config.subscribers) logger$9.log(`Loading subscribers from: ${config.subscribers}`);
1203
- logger$9.log(`Using envParser: ${config.envParser}`);
1204
- const { path: envParserPath, importPattern: envParserImportPattern } = require_config.parseModuleConfig(config.envParser, "envParser");
1205
- const { path: loggerPath, importPattern: loggerImportPattern } = require_config.parseModuleConfig(config.logger, "logger");
1361
+ logger$9.log(`Loading constructs from: ${config.constructs}`);
1206
1362
  const telescope = normalizeTelescopeConfig(config.telescope);
1207
1363
  if (telescope) logger$9.log(`🔭 Telescope enabled at ${telescope.path}`);
1208
1364
  const studio = normalizeStudioConfig(config.studio);
@@ -1213,16 +1369,12 @@ async function devCommand(options) {
1213
1369
  const enableOpenApi = openApiConfig.enabled || resolved.enableOpenApi;
1214
1370
  if (enableOpenApi) logger$9.log(`📄 OpenAPI output: ${require_openapi.OPENAPI_OUTPUT_PATH}`);
1215
1371
  const buildContext = {
1216
- envParserPath,
1217
- envParserImportPattern,
1218
- loggerPath,
1219
- loggerImportPattern,
1220
1372
  telescope,
1221
1373
  studio,
1222
1374
  hooks
1223
1375
  };
1224
1376
  await buildServer(config, buildContext, resolved.providers[0], enableOpenApi, appRoot);
1225
- if (enableOpenApi) await require_openapi.generateOpenApi(config);
1377
+ if (enableOpenApi) await require_openapi.generateOpenApiFrom(config.constructs, { openapi: config.openapi });
1226
1378
  const runtime = config.runtime ?? "node";
1227
1379
  let secretsJsonPath;
1228
1380
  const appSecrets = await require_credentials$1.loadSecretsForApp(secretsRoot, workspaceAppName);
@@ -1255,19 +1407,9 @@ async function devCommand(options) {
1255
1407
  }
1256
1408
  const devServer = new DevServer(resolved.providers[0], options.port ?? workspaceAppPort ?? 3e3, options.portExplicit ?? false, enableOpenApi, telescope, studio, runtime, appRoot, secretsJsonPath);
1257
1409
  await devServer.start();
1258
- const envParserFile = config.envParser.split("#")[0] ?? config.envParser;
1259
- const loggerFile = config.logger.split("#")[0] ?? config.logger;
1260
1410
  const hooksFileParts = config.hooks?.server?.split("#");
1261
1411
  const hooksFile = hooksFileParts?.[0];
1262
- const watchPatterns = [
1263
- config.routes,
1264
- ...config.functions ? [config.functions] : [],
1265
- ...config.crons ? [config.crons] : [],
1266
- ...config.subscribers ? [config.subscribers] : [],
1267
- envParserFile.endsWith(".ts") ? envParserFile : `${envParserFile}.ts`,
1268
- loggerFile.endsWith(".ts") ? loggerFile : `${loggerFile}.ts`,
1269
- ...hooksFile ? [hooksFile.endsWith(".ts") ? hooksFile : `${hooksFile}.ts`] : []
1270
- ].flat().filter((p) => typeof p === "string");
1412
+ const watchPatterns = [config.constructs, ...hooksFile ? [hooksFile.endsWith(".ts") ? hooksFile : `${hooksFile}.ts`] : []].flat().filter((p) => typeof p === "string");
1271
1413
  const normalizedPatterns = watchPatterns.map((p) => p.startsWith("./") ? p.slice(2) : p);
1272
1414
  logger$9.log(`👀 Watching for changes in: ${normalizedPatterns.join(", ")}`);
1273
1415
  const resolvedFiles = await (0, fast_glob.default)(normalizedPatterns, {
@@ -1300,7 +1442,8 @@ async function devCommand(options) {
1300
1442
  try {
1301
1443
  logger$9.log("🔄 Rebuilding...");
1302
1444
  await buildServer(config, buildContext, resolved.providers[0], enableOpenApi, appRoot, true);
1303
- if (enableOpenApi) await require_openapi.generateOpenApi(config, {
1445
+ if (enableOpenApi) await require_openapi.generateOpenApiFrom(config.constructs, {
1446
+ openapi: config.openapi,
1304
1447
  silent: true,
1305
1448
  bustCache: true
1306
1449
  });
@@ -1714,7 +1857,7 @@ async function buildServer(config, context, provider, enableOpenApi, appRoot = p
1714
1857
  const queueGenerator = new QueueGenerator();
1715
1858
  const topicGenerator = new TopicGenerator();
1716
1859
  const [allEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics] = await Promise.all([
1717
- endpointGenerator.load(config.routes, appRoot, bustCache),
1860
+ endpointGenerator.load(config.constructs, appRoot, bustCache),
1718
1861
  config.functions ? functionGenerator.load(config.functions, appRoot, bustCache) : [],
1719
1862
  config.crons ? cronGenerator.load(config.crons, appRoot, bustCache) : [],
1720
1863
  config.subscribers ? subscriberGenerator.load(config.subscribers, appRoot, bustCache) : [],
@@ -2302,18 +2445,35 @@ export default app;
2302
2445
  `);
2303
2446
  }
2304
2447
  const logger$7 = console;
2448
+ /**
2449
+ * What a build run at the workspace root reads.
2450
+ *
2451
+ * One backend app there, and it is that app's config, so its own `openapi`
2452
+ * and `telescope` settings still apply. Otherwise it is the workspace's: every
2453
+ * construct glob, and the backends they resolve to.
2454
+ */
2455
+ function rootGkmConfig(workspace) {
2456
+ const backends = Object.entries(workspace.apps).filter(([, app]) => app.type === "backend");
2457
+ const only = backends.length === 1 ? backends[0] : void 0;
2458
+ const own = only ? require_workspace.getAppGkmConfig(workspace, only[0]) : void 0;
2459
+ return own ?? {
2460
+ services: workspace.services,
2461
+ constructs: require_workspace.allConstructGlobs(workspace)
2462
+ };
2463
+ }
2305
2464
  async function buildCommand(options) {
2306
2465
  const loadedConfig = await require_config.loadWorkspaceConfig();
2307
2466
  if (loadedConfig.type === "workspace") {
2308
2467
  const cwd = (0, node_path.resolve)(process.cwd());
2309
2468
  const workspaceRoot = (0, node_path.resolve)(loadedConfig.workspace.root);
2310
2469
  const isAtWorkspaceRoot = cwd === workspaceRoot;
2311
- if (isAtWorkspaceRoot) {
2470
+ const someAppIsElsewhere = Object.values(loadedConfig.workspace.apps).some((app) => (0, node_path.resolve)(workspaceRoot, app.path) !== workspaceRoot);
2471
+ if (isAtWorkspaceRoot && someAppIsElsewhere) {
2312
2472
  logger$7.log("📦 Detected workspace configuration");
2313
2473
  return workspaceBuildCommand(loadedConfig.workspace, options);
2314
2474
  }
2315
2475
  }
2316
- const config = loadedConfig.type === "workspace" ? (await require_config.loadAppConfig()).gkmConfig : await require_config.loadConfig();
2476
+ const config = loadedConfig.type !== "workspace" ? await require_config.loadConfig() : (0, node_path.resolve)(process.cwd()) === (0, node_path.resolve)(loadedConfig.workspace.root) ? rootGkmConfig(loadedConfig.workspace) : (await require_config.loadAppConfig()).gkmConfig;
2317
2477
  const resolved = resolveProviders(config, options);
2318
2478
  const backendConfig = config.services ?? loadedConfig.workspace?.services;
2319
2479
  const cacheBackend = require_credentials$1.cacheBackendOf(backendConfig?.cache);
@@ -2321,13 +2481,7 @@ async function buildCommand(options) {
2321
2481
  const production = normalizeProductionConfig(options.production ?? false, productionConfigFromGkm);
2322
2482
  if (production) logger$7.log(`🏭 Building for PRODUCTION`);
2323
2483
  logger$7.log(`Building with providers: ${resolved.providers.join(", ")}`);
2324
- logger$7.log(`Loading routes from: ${formatRoutes(config.routes)}`);
2325
- if (config.functions) logger$7.log(`Loading functions from: ${formatRoutes(config.functions)}`);
2326
- if (config.crons) logger$7.log(`Loading crons from: ${formatRoutes(config.crons)}`);
2327
- if (config.subscribers) logger$7.log(`Loading subscribers from: ${formatRoutes(config.subscribers)}`);
2328
- logger$7.log(`Using envParser: ${config.envParser}`);
2329
- const { path: envParserPath, importPattern: envParserImportPattern } = require_config.parseModuleConfig(config.envParser, "envParser");
2330
- const { path: loggerPath, importPattern: loggerImportPattern } = require_config.parseModuleConfig(config.logger, "logger");
2484
+ logger$7.log(`Loading constructs from: ${formatRoutes(config.constructs)}`);
2331
2485
  const telescope = production ? void 0 : normalizeTelescopeConfig(config.telescope);
2332
2486
  if (telescope) logger$7.log(`🔭 Telescope enabled at ${telescope.path}`);
2333
2487
  const studio = production ? void 0 : normalizeStudioConfig(config.studio);
@@ -2355,7 +2509,18 @@ async function buildCommand(options) {
2355
2509
  const primary = surfaces.find((d) => d.endpoints.length === 0) ?? surfaces[0];
2356
2510
  const browsable = Object.entries(declared).find(([, d]) => d.kind === "database");
2357
2511
  const browsableSource = browsable ? constructSources[browsable[0]] : void 0;
2512
+ const owners = {};
2513
+ for (const [id, declaration] of Object.entries(declared)) {
2514
+ if (declaration.kind !== "rest-api" && declaration.kind !== "worker") continue;
2515
+ const source = constructSources[id];
2516
+ if (!source) continue;
2517
+ owners[id] = {
2518
+ specifier: source.file,
2519
+ exportName: source.exportName
2520
+ };
2521
+ }
2358
2522
  const buildContext = {
2523
+ owners,
2359
2524
  ...primary ? { surface: {
2360
2525
  id: primary.id,
2361
2526
  trustedOriginsKey: (0, __geekmidas_manifest.provideKey)(primary.id, "trustedOrigins"),
@@ -2365,10 +2530,6 @@ async function buildCommand(options) {
2365
2530
  exportName: constructSources[primary.id].exportName
2366
2531
  } } : {}
2367
2532
  } } : {},
2368
- envParserPath,
2369
- envParserImportPattern,
2370
- loggerPath,
2371
- loggerImportPattern,
2372
2533
  telescope,
2373
2534
  studio: studio && browsableSource ? {
2374
2535
  ...studio,
@@ -2395,13 +2556,14 @@ async function buildCommand(options) {
2395
2556
  const subscriberGenerator = new SubscriberGenerator();
2396
2557
  const queueGenerator = new QueueGenerator();
2397
2558
  const topicGenerator = new TopicGenerator();
2559
+ const code = constructGlobs$1;
2398
2560
  const [allEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics] = await Promise.all([
2399
- endpointGenerator.load(config.routes),
2400
- config.functions ? functionGenerator.load(config.functions) : [],
2401
- config.crons ? cronGenerator.load(config.crons) : [],
2402
- config.subscribers ? subscriberGenerator.load(config.subscribers) : [],
2403
- config.queues ? queueGenerator.load(config.queues) : [],
2404
- config.topics ? topicGenerator.load(config.topics) : []
2561
+ endpointGenerator.load(code),
2562
+ functionGenerator.load(code),
2563
+ cronGenerator.load(code),
2564
+ subscriberGenerator.load(code),
2565
+ queueGenerator.load(code),
2566
+ topicGenerator.load(code)
2405
2567
  ]);
2406
2568
  logger$7.log(`Found ${allEndpoints.length} endpoints`);
2407
2569
  logger$7.log(`Found ${allFunctions.length} functions`);
@@ -2410,7 +2572,7 @@ async function buildCommand(options) {
2410
2572
  logger$7.log(`Found ${allQueues.length} queues`);
2411
2573
  logger$7.log(`Found ${allTopics.length} topics`);
2412
2574
  if (allEndpoints.length === 0 && allFunctions.length === 0 && allCrons.length === 0 && allSubscribers.length === 0 && allQueues.length === 0 && allTopics.length === 0) {
2413
- const selfServing = Object.entries(declared).find(([id, d]) => d.kind === "rest-api" && d.endpoints.length > 0 && constructSources[id] !== void 0 && (0, node_path.resolve)(loadedConfig.workspace.root, require_config.resolveAppSpec(id, d.app ?? {}, loadedConfig.workspace.root, "rest-api").path) === (0, node_path.resolve)(process.cwd()));
2575
+ const selfServing = Object.entries(declared).find(([id, d]) => d.kind === "rest-api" && d.endpoints.length > 0 && constructSources[id] !== void 0 && (0, node_path.resolve)(loadedConfig.workspace.root, require_config.resolveAppSpec(id, d.app ?? {}, loadedConfig.workspace.root).path) === (0, node_path.resolve)(process.cwd()));
2414
2576
  if (selfServing) {
2415
2577
  const [id] = selfServing;
2416
2578
  await writeSurfaceEntry((0, node_path.join)(process.cwd(), ".gkm", "server"), constructSources[id]);
@@ -2427,7 +2589,7 @@ async function buildCommand(options) {
2427
2589
  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);
2428
2590
  if (providerResult.masterKey) result = providerResult;
2429
2591
  }
2430
- await require_openapi.generateOpenApi(config, { bustCache: true });
2592
+ await require_openapi.generateOpenApi(allEndpoints.map(({ construct }) => construct), { openapi: config.openapi });
2431
2593
  return result;
2432
2594
  }
2433
2595
  async function buildForProvider(provider, context, rootOutputDir, endpointGenerator, functionGenerator, cronGenerator, subscriberGenerator, queueGenerator, topicGenerator, endpoints, functions, crons, subscribers, queues, topics, enableOpenApi, skipBundle, stage) {
@@ -3051,7 +3213,7 @@ function minioCompose(options) {
3051
3213
  return [
3052
3214
  "services:",
3053
3215
  ` ${service}:`,
3054
- " image: quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z",
3216
+ " image: pgsty/minio:RELEASE.2026-08-04T00-00-00Z",
3055
3217
  ` command: server /data --console-address ":9001"`,
3056
3218
  " environment:",
3057
3219
  ` MINIO_ROOT_USER: ${user}`,
@@ -3870,7 +4032,7 @@ const DEFAULT_SERVICE_IMAGES = {
3870
4032
  postgres: "postgres",
3871
4033
  redis: "redis",
3872
4034
  rabbitmq: "rabbitmq",
3873
- minio: "quay.io/minio/minio",
4035
+ minio: "pgsty/minio",
3874
4036
  mailpit: "axllent/mailpit",
3875
4037
  localstack: "floci/floci"
3876
4038
  };
@@ -3879,7 +4041,7 @@ const DEFAULT_SERVICE_VERSIONS = {
3879
4041
  postgres: `${__geekmidas_manifest.DEFAULT_POSTGRES_VERSION}-alpine`,
3880
4042
  redis: "7-alpine",
3881
4043
  rabbitmq: "3-management-alpine",
3882
- minio: "RELEASE.2025-09-07T16-13-09Z",
4044
+ minio: "RELEASE.2026-08-04T00-00-00Z",
3883
4045
  mailpit: "latest",
3884
4046
  localstack: "latest"
3885
4047
  };
@@ -4331,7 +4493,7 @@ function getInfraServiceImage(serviceName$1, pin) {
4331
4493
  const defaults = {
4332
4494
  postgres: "postgres:18-alpine",
4333
4495
  redis: "redis:7-alpine",
4334
- minio: "quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z"
4496
+ minio: "pgsty/minio:RELEASE.2026-08-04T00-00-00Z"
4335
4497
  };
4336
4498
  return pin || defaults[serviceName$1];
4337
4499
  }
@@ -7694,11 +7856,11 @@ Crons, subscribers and functions are declared the same way endpoints are: as
7694
7856
  exported values the build discovers.
7695
7857
 
7696
7858
  \`\`\`typescript
7697
- // a scheduled job
7698
- import { c } from '@geekmidas/constructs/crons';
7859
+ // a scheduled job, built from the worker that runs it
7860
+ import { worker } from '../constructs/worker.ts';
7699
7861
 
7700
- export const cleanup = c
7701
- .schedule('rate(1 day)')
7862
+ export const cleanup = worker
7863
+ .cron('rate(1 day)')
7702
7864
  .handle(async ({ logger }) => {
7703
7865
  logger.info('cleaning up');
7704
7866
  });
@@ -7706,10 +7868,10 @@ export const cleanup = c
7706
7868
 
7707
7869
  \`\`\`typescript
7708
7870
  // a topic subscriber
7709
- import { s } from '@geekmidas/constructs/subscribers';
7710
7871
  import { userEvents } from '../constructs/topics.ts';
7872
+ import { worker } from '../constructs/worker.ts';
7711
7873
 
7712
- export const onUserCreated = s
7874
+ export const onUserCreated = worker
7713
7875
  .topic(userEvents)
7714
7876
  .subscribe(['user.created'])
7715
7877
  .handle(async ({ events, logger }) => {
@@ -7814,26 +7976,26 @@ const CLI_VERSION = `~${pkg.version}`;
7814
7976
  * Run: pnpm --filter @geekmidas/cli sync-versions
7815
7977
  */
7816
7978
  const GEEKMIDAS_VERSIONS = {
7817
- "@geekmidas/audit": "~10.0.0-alpha.4",
7818
- "@geekmidas/auth": "~10.0.0-alpha.4",
7819
- "@geekmidas/cache": "~10.0.0-alpha.4",
7820
- "@geekmidas/client": "~10.0.0-alpha.4",
7821
- "@geekmidas/cloud": "~10.0.0-alpha.4",
7822
- "@geekmidas/constructs": "~10.0.0-alpha.4",
7823
- "@geekmidas/db": "~10.0.0-alpha.4",
7824
- "@geekmidas/emailkit": "~10.0.0-alpha.4",
7825
- "@geekmidas/envkit": "~10.0.0-alpha.4",
7826
- "@geekmidas/errors": "~10.0.0-alpha.4",
7827
- "@geekmidas/events": "~10.0.0-alpha.4",
7828
- "@geekmidas/logger": "~10.0.0-alpha.4",
7829
- "@geekmidas/manifest": "~10.0.0-alpha.4",
7830
- "@geekmidas/rate-limit": "~10.0.0-alpha.4",
7831
- "@geekmidas/schema": "~10.0.0-alpha.4",
7832
- "@geekmidas/services": "~10.0.0-alpha.4",
7833
- "@geekmidas/storage": "~10.0.0-alpha.4",
7834
- "@geekmidas/studio": "~10.0.0-alpha.4",
7835
- "@geekmidas/telescope": "~10.0.0-alpha.4",
7836
- "@geekmidas/testkit": "~10.0.0-alpha.4",
7979
+ "@geekmidas/audit": "~10.0.0-alpha.5",
7980
+ "@geekmidas/auth": "~10.0.0-alpha.5",
7981
+ "@geekmidas/cache": "~10.0.0-alpha.5",
7982
+ "@geekmidas/client": "~10.0.0-alpha.5",
7983
+ "@geekmidas/cloud": "~10.0.0-alpha.5",
7984
+ "@geekmidas/constructs": "~10.0.0-alpha.5",
7985
+ "@geekmidas/db": "~10.0.0-alpha.5",
7986
+ "@geekmidas/emailkit": "~10.0.0-alpha.5",
7987
+ "@geekmidas/envkit": "~10.0.0-alpha.5",
7988
+ "@geekmidas/errors": "~10.0.0-alpha.5",
7989
+ "@geekmidas/events": "~10.0.0-alpha.5",
7990
+ "@geekmidas/logger": "~10.0.0-alpha.5",
7991
+ "@geekmidas/manifest": "~10.0.0-alpha.5",
7992
+ "@geekmidas/rate-limit": "~10.0.0-alpha.5",
7993
+ "@geekmidas/schema": "~10.0.0-alpha.5",
7994
+ "@geekmidas/services": "~10.0.0-alpha.5",
7995
+ "@geekmidas/storage": "~10.0.0-alpha.5",
7996
+ "@geekmidas/studio": "~10.0.0-alpha.5",
7997
+ "@geekmidas/telescope": "~10.0.0-alpha.5",
7998
+ "@geekmidas/testkit": "~10.0.0-alpha.5",
7837
7999
  "@geekmidas/cli": CLI_VERSION
7838
8000
  };
7839
8001
 
@@ -8022,7 +8184,9 @@ export default defineConfig({${options.monorepo ? "" : `
8022
8184
  forceConsistentCasingInFileNames: true,
8023
8185
  resolveJsonModule: true,
8024
8186
  noEmit: true,
8025
- allowImportingTsExtensions: true
8187
+ allowImportingTsExtensions: true,
8188
+ baseUrl: ".",
8189
+ paths: { "~/*": ["./src/*"] }
8026
8190
  },
8027
8191
  include: ["src/**/*.ts"],
8028
8192
  exclude: ["node_modules", "dist"]
@@ -8280,7 +8444,7 @@ function generateDockerFiles(options, template, dbApps) {
8280
8444
  MP_SMTP_AUTH: \${SMTP_USER:-${options.name}}:\${SMTP_PASS:-${options.name}}`);
8281
8445
  if (options.services?.storage) {
8282
8446
  services.push(` minio:
8283
- image: quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z
8447
+ image: pgsty/minio:RELEASE.2026-08-04T00-00-00Z
8284
8448
  container_name: ${options.name}-minio
8285
8449
  restart: unless-stopped
8286
8450
  entrypoint: sh
@@ -10449,15 +10613,23 @@ export const healthEndpoint = api
10449
10613
  },
10450
10614
  {
10451
10615
  path: "src/functions/hello.ts",
10452
- content: `import { f } from '@geekmidas/constructs/functions';
10453
- import { z } from 'zod';
10616
+ content: `import { z } from 'zod';
10617
+ import { worker } from '~/constructs/worker.ts';
10454
10618
 
10455
- export const helloFunction = f
10619
+ export const helloFunction = worker
10456
10620
  .input(z.object({ name: z.string() }))
10457
10621
  .output(z.object({ message: z.string() }))
10458
10622
  .handle(async ({ input }) => ({
10459
10623
  message: \`Hello, \${input.name}!\`,
10460
10624
  }));
10625
+ `
10626
+ },
10627
+ {
10628
+ path: "src/constructs/worker.ts",
10629
+ content: `import { Worker } from '@geekmidas/constructs/worker';
10630
+ import { logger } from '../config/logger.ts';
10631
+
10632
+ export const worker = new Worker('Jobs', { logger });
10461
10633
  `
10462
10634
  }
10463
10635
  ];
@@ -10490,12 +10662,11 @@ export const telescope = new Telescope({
10490
10662
  * that was missing: it takes no authorizer, publishes no address, and hands out
10491
10663
  * factories already carrying its logger.
10492
10664
  *
10493
- * It scaffolds no cron. A cron is the obvious thing to put in a worker and it
10494
- * would not run: `CronGenerator` emits handlers for `aws-lambda` only, and
10495
- * `.gkm/server/` has no crons file, so a scheduled job on a server target
10496
- * deploys nothing at all. Subscribers and queues do have a server runtime,
10497
- * which is why those are what this template teaches. The cron example returns
10498
- * when there is a scheduler to run it.
10665
+ * It scaffolds a cron when the project has a database, and not otherwise. A
10666
+ * server fires its own crons, and the schedule lives in Postgres so that
10667
+ * running more than one replica still fires each job once — so a worker names
10668
+ * the database it keeps schedules in. Without one there is nowhere to keep
10669
+ * them, and an example that could not run is worse than none.
10499
10670
  */
10500
10671
  const workerTemplate = {
10501
10672
  name: "worker",
@@ -10529,7 +10700,7 @@ const workerTemplate = {
10529
10700
  "fmt:check": "biome format ."
10530
10701
  },
10531
10702
  files: (options) => {
10532
- const { loggerType, name: name$1 } = options;
10703
+ const { loggerType, name: name$1, database } = options;
10533
10704
  const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
10534
10705
 
10535
10706
  export const logger = createLogger();
@@ -10538,9 +10709,9 @@ export const logger = createLogger();
10538
10709
  {
10539
10710
  path: "src/constructs/worker.ts",
10540
10711
  content: `import { Worker } from '@geekmidas/constructs/worker';
10541
- import { logger } from '../config/logger.ts';
10712
+ import { logger } from '../config/logger.ts';${database ? "\nimport { database } from './database.ts';" : ""}
10542
10713
 
10543
- export const worker = new Worker('Jobs', { logger });
10714
+ export const worker = new Worker('Jobs', { logger })${database ? ".database(database)" : ""};
10544
10715
  `
10545
10716
  },
10546
10717
  {
@@ -10608,7 +10779,7 @@ export const eventsPublisherService = {
10608
10779
  content: `import { worker } from '~/constructs/worker.ts';
10609
10780
  import { eventsPublisherService } from '~/events/publisher.ts';
10610
10781
 
10611
- export const userEventsSubscriber = worker.subscribers
10782
+ export const userEventsSubscriber = worker
10612
10783
  .publisher(eventsPublisherService)
10613
10784
  .subscribe(['user.created', 'user.updated'])
10614
10785
  .handle(async ({ events, logger }) => {
@@ -10630,6 +10801,21 @@ export const userEventsSubscriber = worker.subscribers
10630
10801
  `
10631
10802
  }
10632
10803
  ];
10804
+ if (database) files$1.push({
10805
+ path: "src/crons/cleanup.ts",
10806
+ content: `import { worker } from '~/constructs/worker.ts';
10807
+
10808
+ export const cleanup = worker
10809
+ .cron('rate(1 day)')
10810
+ .handle(async ({ logger }) => {
10811
+ logger.info('Running cleanup');
10812
+
10813
+ // Delete old sessions, clear temp files, whatever falls out of date.
10814
+
10815
+ logger.info('Cleanup complete');
10816
+ });
10817
+ `
10818
+ });
10633
10819
  if (options.database) files$1.push(...databaseFiles(name$1));
10634
10820
  if (options.telescope) files$1.push({
10635
10821
  path: "src/config/telescope.ts",
@@ -13875,7 +14061,7 @@ async function testCommand(options = {}) {
13875
14061
  console.log(`\n🧪 Running tests with ${stage} environment...\n`);
13876
14062
  const autoSetup = options.autoSetup || Boolean(process.env.GKM_AUTO_SETUP);
13877
14063
  if (autoSetup) {
13878
- const { ensureStageSecrets } = await Promise.resolve().then(() => require("./setup-B5_oHHUj.cjs"));
14064
+ const { ensureStageSecrets } = await Promise.resolve().then(() => require("./setup-8IWPsRIN.cjs"));
13879
14065
  const generated = await ensureStageSecrets(stage, cwd);
13880
14066
  if (generated) console.log(` 🔐 Generated fresh ${stage} secrets (auto-setup)`);
13881
14067
  }
@@ -14350,7 +14536,7 @@ program.command("secrets:push").description("Push secrets to remote provider (SS
14350
14536
  const { workspace } = await loadWorkspaceConfig$1();
14351
14537
  const secrets = await readStageSecrets$1(options.stage, workspace.root);
14352
14538
  if (secrets) {
14353
- const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-DdJss8I6.cjs"));
14539
+ const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-CGaUAw5A.cjs"));
14354
14540
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
14355
14541
  if (result) {
14356
14542
  await writeStageSecrets$1(result.secrets, workspace.root);
@@ -14379,7 +14565,7 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
14379
14565
  console.error(`No remote secrets found for stage "${options.stage}".`);
14380
14566
  process.exit(1);
14381
14567
  }
14382
- const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-DdJss8I6.cjs"));
14568
+ const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-CGaUAw5A.cjs"));
14383
14569
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
14384
14570
  if (result) {
14385
14571
  secrets = result.secrets;
@@ -14406,7 +14592,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
14406
14592
  console.error(`No secrets found for stage "${options.stage}". Run "gkm secrets:init --stage ${options.stage}" first.`);
14407
14593
  process.exit(1);
14408
14594
  }
14409
- const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-DdJss8I6.cjs"));
14595
+ const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-CGaUAw5A.cjs"));
14410
14596
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
14411
14597
  if (!result) {
14412
14598
  console.log(`\n✓ Secrets for stage "${options.stage}" are up-to-date`);