@geekmidas/cli 10.0.0-alpha.4 → 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-DCtipn2h.d.cts → index-DEbaleny.d.mts} +2 -2
  18. package/dist/index-DEbaleny.d.mts.map +1 -0
  19. package/dist/index.cjs +332 -179
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.mjs +332 -179
  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-BhIl-COG.d.cts → types-BLSNdrk0.d.cts} +2 -10
  42. package/dist/{types-C-arnQ1r.d.mts.map → types-BLSNdrk0.d.cts.map} +1 -1
  43. package/dist/{types-C-arnQ1r.d.mts → types-C3POXhw3.d.mts} +2 -10
  44. package/dist/{types-BhIl-COG.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-DCtipn2h.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.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  import { __require } from "./chunk-Duj1WY3L.mjs";
3
3
  import { ConstructGenerator, DEFAULT_CACHE, discover, isPartitionedRoutes } from "./discover-CAlFgYec.mjs";
4
- import { derivedApps, getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceConfig, parseModuleConfig, resolveAppSpec } from "./config-WRdsQYZK.mjs";
5
- import { allConstructGlobs, getAppBuildOrder, getDependencyEnvVars, getDeployTargetError, getPublicEnvPrefix, isDeployTargetSupported, stripPublicPrefix } from "./workspace-BFMrAKqI.mjs";
4
+ import { derivedApps, getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceConfig, parseModuleConfig, resolveAppSpec } from "./config-CxP7P-qG.mjs";
5
+ import { allConstructGlobs, getAppBuildOrder, getAppGkmConfig, getDependencyEnvVars, getDeployTargetError, getPublicEnvPrefix, isDeployTargetSupported, stripPublicPrefix } from "./workspace-DIuZx-cc.mjs";
6
6
  import { getCredentialsPath, getDokployCredentials, getDokployRegistryId, getDokployToken, removeDokployCredentials, storeDokployCredentials, storeDokployRegistryId, storeHostingerToken } from "./credentials-B5zRHdAl.mjs";
7
- import { applyPostgres, cacheBackendOf, constructGlobs, createCredentialsPreload, createEntryWrapper, derivedContainers, emailBackendOf, findAvailablePort, isPortAvailable, loadEnvFiles, loadSecretsForApp, prepareEntryCredentials, providerOf, reconcileWorkspace, resolveServicePorts, resourceName, rewriteUrlsWithPorts, startWorkspaceServices, storageBackendOf, usesConstructs } from "./credentials-BBIRJVp4.mjs";
7
+ import { applyPostgres, cacheBackendOf, constructGlobs, createCredentialsPreload, createEntryWrapper, derivedContainers, emailBackendOf, findAvailablePort, isPortAvailable, loadEnvFiles, loadSecretsForApp, prepareEntryCredentials, providerOf, reconcileWorkspace, resolveServicePorts, resourceName, rewriteUrlsWithPorts, startWorkspaceServices, storageBackendOf, usesConstructs } from "./credentials-BK3Z93MX.mjs";
8
8
  import { createStageSecrets, rotateServicePassword } from "./generator-VE2JrFlY.mjs";
9
9
  import { getKeyPath, maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, writeStageSecrets } from "./storage-Bi24WfoW.mjs";
10
- import { EndpointGenerator, OPENAPI_OUTPUT_PATH, generateOpenApi, normalizeRoutes, openapiCommand, resolveOpenApiConfig } from "./openapi-kkQbsRVj.mjs";
10
+ import { EndpointGenerator, OPENAPI_OUTPUT_PATH, generateOpenApi, generateOpenApiFrom, normalizeRoutes, openapiCommand, resolveOpenApiConfig, runtimeFor } from "./openapi-Bw8DWFWz.mjs";
11
11
  import { withCompute, withRoutes } from "./emit-CiZS_5Ub.mjs";
12
12
  import { DokployApi } from "./dokploy-api-aTFPz7_4.mjs";
13
13
  import { encryptSecrets } from "./encryption-D-abyhZx.mjs";
@@ -15,7 +15,7 @@ import { CachedStateProvider } from "./CachedStateProvider-DTniehMP.mjs";
15
15
  import { generateDbPassword, generateDbUrl, generateFullstackCustomSecrets } from "./fullstack-secrets-D1o9-G5l.mjs";
16
16
  import { generateReactQueryCommand } from "./openapi-react-query-Byltvm_7.mjs";
17
17
  import "./sync-CmEuNZsB.mjs";
18
- import { setupCommand, trustCommand } from "./setup-DXvRLTZN.mjs";
18
+ import { setupCommand, trustCommand } from "./setup-BWMhbW-p.mjs";
19
19
  import { createRequire } from "node:module";
20
20
  import { copyFileSync, existsSync, readFileSync, unlinkSync } from "node:fs";
21
21
  import { basename, dirname, isAbsolute, join, parse, relative, resolve } from "node:path";
@@ -43,7 +43,7 @@ import prompts from "prompts";
43
43
 
44
44
  //#region package.json
45
45
  var name = "@geekmidas/cli";
46
- var version = "10.0.0-alpha.3";
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 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 = join(outputDir, "crons");
420
425
  await 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: relative(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
@@ -436,17 +441,173 @@ var CronGenerator = class extends ConstructGenerator {
436
441
  isConstruct(value) {
437
442
  return 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 mkdir(outputDir, { recursive: true });
458
+ const cronsPath = join(outputDir, "crons.ts");
459
+ const importsByFile = /* @__PURE__ */ new Map();
460
+ for (const { path, key } of crons) {
461
+ const relativePath = relative(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$1]) => `import { ${exports$1.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 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 = join(outputDir, handlerFileName);
442
604
  const relativePath = relative(dirname(handlerPath), sourceFile);
443
605
  const importPath = relativePath.replace(/\.ts$/, ".js");
444
- const relativeEnvParserPath = relative(dirname(handlerPath), context.envParserPath);
445
- const relativeLoggerPath = relative(dirname(handlerPath), context.loggerPath);
606
+ const runtime = runtimeFor(context, 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 ConstructGenerator {
562
723
  const functionsDir = join(outputDir, "functions");
563
724
  await 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: relative(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
@@ -575,17 +736,16 @@ var FunctionGenerator = class extends 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 = join(outputDir, handlerFileName);
581
742
  const relativePath = relative(dirname(handlerPath), sourceFile);
582
743
  const importPath = relativePath.replace(/\.ts$/, ".js");
583
- const relativeEnvParserPath = relative(dirname(handlerPath), context.envParserPath);
584
- const relativeLoggerPath = relative(dirname(handlerPath), context.loggerPath);
744
+ const runtime = runtimeFor(context, 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 ConstructGenerator {
624
784
  const queuesDir = join(outputDir, "queues");
625
785
  await 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: relative(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
@@ -638,13 +798,14 @@ var QueueGenerator = class extends 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 = join(outputDir, `${exportName}.ts`);
643
803
  const importPath = relative(dirname(handlerPath), sourceFile).replace(/\.ts$/, ".js");
644
- const relativeEnvParserPath = relative(dirname(handlerPath), context.envParserPath);
804
+ const runtime = runtimeFor(context, 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 ConstructGenerator {
787
948
  const subscribersDir = join(outputDir, "subscribers");
788
949
  await 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: relative(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
@@ -805,15 +966,16 @@ var SubscriberGenerator = class extends 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 = join(outputDir, handlerFileName);
811
972
  const relativePath = relative(dirname(handlerPath), sourceFile);
812
973
  const importPath = relativePath.replace(/\.ts$/, ".js");
813
- const relativeEnvParserPath = relative(dirname(handlerPath), context.envParserPath);
974
+ const runtime = runtimeFor(context, 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 } = parseModuleConfig(config.envParser, "envParser");
1205
- const { path: loggerPath, importPattern: loggerImportPattern } = 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: ${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 generateOpenApi(config);
1377
+ if (enableOpenApi) await generateOpenApiFrom(config.constructs, { openapi: config.openapi });
1226
1378
  const runtime = config.runtime ?? "node";
1227
1379
  let secretsJsonPath;
1228
1380
  const appSecrets = await 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 fg(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 generateOpenApi(config, {
1445
+ if (enableOpenApi) await 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 ? getAppGkmConfig(workspace, only[0]) : void 0;
2459
+ return own ?? {
2460
+ services: workspace.services,
2461
+ constructs: allConstructGlobs(workspace)
2462
+ };
2463
+ }
2305
2464
  async function buildCommand(options) {
2306
2465
  const loadedConfig = await loadWorkspaceConfig();
2307
2466
  if (loadedConfig.type === "workspace") {
2308
2467
  const cwd = resolve(process.cwd());
2309
2468
  const workspaceRoot = resolve(loadedConfig.workspace.root);
2310
2469
  const isAtWorkspaceRoot = cwd === workspaceRoot;
2311
- if (isAtWorkspaceRoot) {
2470
+ const someAppIsElsewhere = Object.values(loadedConfig.workspace.apps).some((app) => 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 loadAppConfig()).gkmConfig : await loadConfig();
2476
+ const config = loadedConfig.type !== "workspace" ? await loadConfig() : resolve(process.cwd()) === resolve(loadedConfig.workspace.root) ? rootGkmConfig(loadedConfig.workspace) : (await loadAppConfig()).gkmConfig;
2317
2477
  const resolved = resolveProviders(config, options);
2318
2478
  const backendConfig = config.services ?? loadedConfig.workspace?.services;
2319
2479
  const cacheBackend = 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 } = parseModuleConfig(config.envParser, "envParser");
2330
- const { path: loggerPath, importPattern: loggerImportPattern } = 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: 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 && resolve(loadedConfig.workspace.root, resolveAppSpec(id, d.app ?? {}, loadedConfig.workspace.root, "rest-api").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 && resolve(loadedConfig.workspace.root, resolveAppSpec(id, d.app ?? {}, loadedConfig.workspace.root).path) === resolve(process.cwd()));
2414
2576
  if (selfServing) {
2415
2577
  const [id] = selfServing;
2416
2578
  await writeSurfaceEntry(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 generateOpenApi(config, { bustCache: true });
2592
+ await 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: `${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.3",
7818
- "@geekmidas/auth": "~10.0.0-alpha.3",
7819
- "@geekmidas/cache": "~10.0.0-alpha.3",
7820
- "@geekmidas/client": "~10.0.0-alpha.3",
7821
- "@geekmidas/cloud": "~10.0.0-alpha.3",
7822
- "@geekmidas/constructs": "~10.0.0-alpha.3",
7823
- "@geekmidas/db": "~10.0.0-alpha.3",
7824
- "@geekmidas/emailkit": "~10.0.0-alpha.3",
7825
- "@geekmidas/envkit": "~10.0.0-alpha.3",
7826
- "@geekmidas/errors": "~10.0.0-alpha.3",
7827
- "@geekmidas/events": "~10.0.0-alpha.3",
7828
- "@geekmidas/logger": "~10.0.0-alpha.3",
7829
- "@geekmidas/manifest": "~10.0.0-alpha.3",
7830
- "@geekmidas/rate-limit": "~10.0.0-alpha.3",
7831
- "@geekmidas/schema": "~10.0.0-alpha.3",
7832
- "@geekmidas/services": "~10.0.0-alpha.3",
7833
- "@geekmidas/storage": "~10.0.0-alpha.3",
7834
- "@geekmidas/studio": "~10.0.0-alpha.3",
7835
- "@geekmidas/telescope": "~10.0.0-alpha.3",
7836
- "@geekmidas/testkit": "~10.0.0-alpha.3",
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
  ];
@@ -10480,6 +10652,22 @@ export const telescope = new Telescope({
10480
10652
 
10481
10653
  //#endregion
10482
10654
  //#region src/init/templates/worker.ts
10655
+ /**
10656
+ * A project that is background work and nothing else.
10657
+ *
10658
+ * It used to scaffold a `RestApi` with no endpoints on it — an HTTP surface
10659
+ * declared by a project whose premise is that nothing calls it over HTTP —
10660
+ * because a surface was the only construct that made an app exist and the only
10661
+ * way to get a logger into the generated runtime. `Worker` is the construct
10662
+ * that was missing: it takes no authorizer, publishes no address, and hands out
10663
+ * factories already carrying its logger.
10664
+ *
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.
10670
+ */
10483
10671
  const workerTemplate = {
10484
10672
  name: "worker",
10485
10673
  description: "Background job processing",
@@ -10489,11 +10677,8 @@ const workerTemplate = {
10489
10677
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
10490
10678
  "@geekmidas/events": GEEKMIDAS_VERSIONS["@geekmidas/events"],
10491
10679
  "@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
10492
- "@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
10493
10680
  "@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
10494
- "@hono/node-server": "~1.14.1",
10495
- hono: "~4.8.2",
10496
- pino: "~9.6.0"
10681
+ pino: "~10.3.1"
10497
10682
  },
10498
10683
  devDependencies: {
10499
10684
  "@biomejs/biome": "~2.3.0",
@@ -10515,34 +10700,18 @@ const workerTemplate = {
10515
10700
  "fmt:check": "biome format ."
10516
10701
  },
10517
10702
  files: (options) => {
10518
- const { loggerType, routesStructure, name: name$1 } = options;
10703
+ const { loggerType, name: name$1, database } = options;
10519
10704
  const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
10520
10705
 
10521
10706
  export const logger = createLogger();
10522
10707
  `;
10523
- const getRoutePath = (file) => {
10524
- switch (routesStructure) {
10525
- case "centralized-endpoints": return `src/endpoints/${file}`;
10526
- case "centralized-routes": return `src/routes/${file}`;
10527
- case "domain-based": return `src/${file.replace(".ts", "")}/routes/index.ts`;
10528
- }
10529
- };
10530
- const apiImport = routesStructure === "domain-based" ? "../../constructs/api.js" : "../constructs/api.js";
10531
10708
  const files$1 = [
10532
10709
  {
10533
- path: "src/constructs/api.ts",
10534
- content: `import { RestApi } from '@geekmidas/constructs/rest-api';
10535
- import { logger } from '../config/logger.ts';
10710
+ path: "src/constructs/worker.ts",
10711
+ content: `import { Worker } from '@geekmidas/constructs/worker';
10712
+ import { logger } from '../config/logger.ts';${database ? "\nimport { database } from './database.ts';" : ""}
10536
10713
 
10537
- export const api = new RestApi('Api', {
10538
- // Typed out rather than omitted: an API that ships open because a field was
10539
- // left off is the one default worth refusing to have.
10540
- defaultAuthorizer: 'none',
10541
-
10542
- // The actual logger, not a path to one. Every endpoint built from this
10543
- // surface runs with it, and so does the generated entry.
10544
- logger,
10545
- });
10714
+ export const worker = new Worker('Jobs', { logger })${database ? ".database(database)" : ""};
10546
10715
  `
10547
10716
  },
10548
10717
  {
@@ -10566,23 +10735,6 @@ export const config = envParser
10566
10735
  path: "src/config/logger.ts",
10567
10736
  content: loggerContent
10568
10737
  },
10569
- {
10570
- path: getRoutePath("health.ts"),
10571
- content: `import { z } from 'zod';
10572
- import { api } from '${apiImport}';
10573
-
10574
- export const healthEndpoint = api
10575
- .get('/health')
10576
- .output(z.object({
10577
- status: z.string(),
10578
- timestamp: z.string(),
10579
- }))
10580
- .handle(async () => ({
10581
- status: 'ok',
10582
- timestamp: new Date().toISOString(),
10583
- }));
10584
- `
10585
- },
10586
10738
  {
10587
10739
  path: "src/events/types.ts",
10588
10740
  content: `import type { PublishableMessage } from '@geekmidas/events';
@@ -10624,45 +10776,46 @@ export const eventsPublisherService = {
10624
10776
  },
10625
10777
  {
10626
10778
  path: "src/subscribers/user-events.ts",
10627
- content: `import { s } from '@geekmidas/constructs/subscribers';
10779
+ content: `import { worker } from '~/constructs/worker.ts';
10628
10780
  import { eventsPublisherService } from '~/events/publisher.ts';
10629
10781
 
10630
- export const userEventsSubscriber = s
10782
+ export const userEventsSubscriber = worker
10631
10783
  .publisher(eventsPublisherService)
10632
10784
  .subscribe(['user.created', 'user.updated'])
10633
- .handle(async ({ event, logger }) => {
10634
- logger.info({ type: event.type, payload: event.payload }, 'Processing user event');
10635
-
10636
- switch (event.type) {
10637
- case 'user.created':
10638
- // Handle user creation
10639
- logger.info({ userId: event.payload.userId }, 'New user created');
10640
- break;
10641
- case 'user.updated':
10642
- // Handle user update
10643
- logger.info({ userId: event.payload.userId }, 'User updated');
10644
- break;
10785
+ .handle(async ({ events, logger }) => {
10786
+ // A batch, not a single event. Both transports deliver in batches, so
10787
+ // handling them one at a time is a round trip per event for no reason.
10788
+ logger.info({ count: events.length }, 'Processing user events');
10789
+
10790
+ for (const event of events) {
10791
+ switch (event.type) {
10792
+ case 'user.created':
10793
+ logger.info({ userId: event.payload.userId }, 'New user created');
10794
+ break;
10795
+ case 'user.updated':
10796
+ logger.info({ userId: event.payload.userId }, 'User updated');
10797
+ break;
10798
+ }
10645
10799
  }
10646
10800
  });
10647
10801
  `
10648
- },
10649
- {
10650
- path: "src/crons/cleanup.ts",
10651
- content: `import { cron } from '@geekmidas/constructs/crons';
10802
+ }
10803
+ ];
10804
+ if (database) files$1.push({
10805
+ path: "src/crons/cleanup.ts",
10806
+ content: `import { worker } from '~/constructs/worker.ts';
10652
10807
 
10653
- // Run every day at midnight
10654
- export const cleanupCron = cron('0 0 * * *')
10808
+ export const cleanup = worker
10809
+ .cron('rate(1 day)')
10655
10810
  .handle(async ({ logger }) => {
10656
- logger.info('Running cleanup job');
10811
+ logger.info('Running cleanup');
10657
10812
 
10658
- // Add your cleanup logic here
10659
- // e.g., delete old sessions, clean up temp files, etc.
10813
+ // Delete old sessions, clear temp files, whatever falls out of date.
10660
10814
 
10661
- logger.info('Cleanup job completed');
10815
+ logger.info('Cleanup complete');
10662
10816
  });
10663
10817
  `
10664
- }
10665
- ];
10818
+ });
10666
10819
  if (options.database) files$1.push(...databaseFiles(name$1));
10667
10820
  if (options.telescope) files$1.push({
10668
10821
  path: "src/config/telescope.ts",
@@ -13908,7 +14061,7 @@ async function testCommand(options = {}) {
13908
14061
  console.log(`\n🧪 Running tests with ${stage} environment...\n`);
13909
14062
  const autoSetup = options.autoSetup || Boolean(process.env.GKM_AUTO_SETUP);
13910
14063
  if (autoSetup) {
13911
- const { ensureStageSecrets } = await import("./setup-ZijS4qeC.mjs");
14064
+ const { ensureStageSecrets } = await import("./setup-DGIPd2fh.mjs");
13912
14065
  const generated = await ensureStageSecrets(stage, cwd);
13913
14066
  if (generated) console.log(` 🔐 Generated fresh ${stage} secrets (auto-setup)`);
13914
14067
  }
@@ -14383,7 +14536,7 @@ program.command("secrets:push").description("Push secrets to remote provider (SS
14383
14536
  const { workspace } = await loadWorkspaceConfig$1();
14384
14537
  const secrets = await readStageSecrets$1(options.stage, workspace.root);
14385
14538
  if (secrets) {
14386
- const { derivedContainers: derivedContainers$1 } = await import("./workspace-k4l3rsFh.mjs");
14539
+ const { derivedContainers: derivedContainers$1 } = await import("./workspace-CnTqIlB3.mjs");
14387
14540
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
14388
14541
  if (result) {
14389
14542
  await writeStageSecrets$1(result.secrets, workspace.root);
@@ -14412,7 +14565,7 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
14412
14565
  console.error(`No remote secrets found for stage "${options.stage}".`);
14413
14566
  process.exit(1);
14414
14567
  }
14415
- const { derivedContainers: derivedContainers$1 } = await import("./workspace-k4l3rsFh.mjs");
14568
+ const { derivedContainers: derivedContainers$1 } = await import("./workspace-CnTqIlB3.mjs");
14416
14569
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
14417
14570
  if (result) {
14418
14571
  secrets = result.secrets;
@@ -14439,7 +14592,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
14439
14592
  console.error(`No secrets found for stage "${options.stage}". Run "gkm secrets:init --stage ${options.stage}" first.`);
14440
14593
  process.exit(1);
14441
14594
  }
14442
- const { derivedContainers: derivedContainers$1 } = await import("./workspace-k4l3rsFh.mjs");
14595
+ const { derivedContainers: derivedContainers$1 } = await import("./workspace-CnTqIlB3.mjs");
14443
14596
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
14444
14597
  if (!result) {
14445
14598
  console.log(`\n✓ Secrets for stage "${options.stage}" are up-to-date`);