@lunora/codegen 1.0.0-alpha.5 → 1.0.0-alpha.50

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 (42) hide show
  1. package/LICENSE.md +6 -0
  2. package/__assets__/package-og.svg +1 -1
  3. package/dist/index.d.mts +1950 -734
  4. package/dist/index.d.ts +1950 -734
  5. package/dist/index.mjs +32 -24
  6. package/dist/packem_shared/AGENTS_FILENAME-Cxx7wiSs.mjs +158 -0
  7. package/dist/packem_shared/{CONTAINERS_FILENAME-0K-pjNb8.mjs → CONTAINERS_FILENAME-DjpXMqhp.mjs} +1 -1
  8. package/dist/packem_shared/{CodegenDiagnosticError-54jWDxA9.mjs → CodegenDiagnosticError-DyQ5FwkM.mjs} +7 -5
  9. package/dist/packem_shared/FLAGS_FILENAME-Dg4mKUuz.mjs +139 -0
  10. package/dist/packem_shared/GENERATED_HEADER-C5paWNYO.mjs +4 -0
  11. package/dist/packem_shared/{LUNORA_ERROR_CODES-CySpQPD3.mjs → LUNORA_ERROR_CODES-DvTLozCu.mjs} +8 -1
  12. package/dist/packem_shared/MUTATORS_FILENAME-BZOfUhlY.mjs +81 -0
  13. package/dist/packem_shared/{OPENRPC_VERSION-C7zKVeKi.mjs → OPENRPC_VERSION-7A2weq2u.mjs} +2 -2
  14. package/dist/packem_shared/QUEUES_FILENAME-Xv6Vy-Ku.mjs +145 -0
  15. package/dist/packem_shared/SCHEMA_SNAPSHOT_FILENAME-G5dkcxPR.mjs +3502 -0
  16. package/dist/packem_shared/{SCHEMA_SNAPSHOT_VERSION-DzLDbWk3.mjs → SCHEMA_SNAPSHOT_VERSION-D0ARY6rL.mjs} +18 -2
  17. package/dist/packem_shared/SHAPES_FILENAME-DOhPGi-6.mjs +94 -0
  18. package/dist/packem_shared/WORKFLOWS_FILENAME-Dtuzvluc.mjs +158 -0
  19. package/dist/packem_shared/{buildOpenApiDocument-Cns1EkCQ.mjs → buildOpenApiDocument-B7mEdzA2.mjs} +2 -2
  20. package/dist/packem_shared/{discoverAuthApiCalls-C35R6z0T.mjs → discoverAuthApiCalls-Dx3K42rk.mjs} +1 -1
  21. package/dist/packem_shared/{discoverCrons-BL6iGuJ3.mjs → discoverCrons-DvqkEWdx.mjs} +68 -36
  22. package/dist/packem_shared/{discoverFunctions-DEgAcRuD.mjs → discoverFunctions-BJ-qR7Rg.mjs} +72 -14
  23. package/dist/packem_shared/{discoverHttpRoutes-C978pBiG.mjs → discoverHttpRoutes-daCzuTe8.mjs} +10 -2
  24. package/dist/packem_shared/{discoverInserts-CRQdXvHO.mjs → discoverInserts-DI4q5NxE.mjs} +24 -2
  25. package/dist/packem_shared/{discoverMaskProcedures-B64zA740.mjs → discoverMaskProcedures-BcTOEKNU.mjs} +58 -2
  26. package/dist/packem_shared/{discoverMigrations-Doj_-BAA.mjs → discoverMigrations-VNUFvCwr.mjs} +10 -4
  27. package/dist/packem_shared/{discoverNondeterministicCalls-4KiPQxQU.mjs → discoverNondeterministicCalls-S0N2xLCq.mjs} +1 -1
  28. package/dist/packem_shared/{discoverQueries-BkIi0dBD.mjs → discoverQueries-CJnnnLpd.mjs} +1 -1
  29. package/dist/packem_shared/{discoverR2sqlCalls-BpDqvcUn.mjs → discoverR2sqlCalls-pnpicWfz.mjs} +1 -1
  30. package/dist/packem_shared/{discoverRlsMetadata-DpRB1HMe.mjs → discoverRlsMetadata-DppniPUH.mjs} +1 -1
  31. package/dist/packem_shared/discoverSandboxUsage-BUM2r90k.mjs +38 -0
  32. package/dist/packem_shared/{discoverSchema-DB_QrIQe.mjs → discoverSchema-BaSAvooG.mjs} +326 -14
  33. package/dist/packem_shared/{discoverStorageRulesMetadata-DAqJUxUv.mjs → discoverStorageRulesMetadata-CnHl2rXD.mjs} +1 -1
  34. package/dist/packem_shared/{GENERATED_HEADER-DF1hQcix.mjs → emit-ClTbCLy4.mjs} +1458 -154
  35. package/dist/packem_shared/{emitApp-Ci_hcJNO.mjs → emitApp-CzzrjVqH.mjs} +129 -31
  36. package/dist/packem_shared/formatAdvisories-BmF7Mplc.mjs +115 -0
  37. package/dist/packem_shared/{parse-validator-tuQtHrsr.mjs → parse-validator-BSJo1HGP.mjs} +18 -6
  38. package/dist/packem_shared/redact-oTmsol5A.mjs +33 -0
  39. package/package.json +10 -7
  40. package/dist/packem_shared/SCHEMA_SNAPSHOT_FILENAME-BqOtU2lC.mjs +0 -922
  41. package/dist/packem_shared/WORKFLOWS_FILENAME-DRDQdhfq.mjs +0 -84
  42. package/dist/packem_shared/formatAdvisories-8NIv1k0I.mjs +0 -69
@@ -1,3 +1,5 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+
1
3
  const SCHEMA_SNAPSHOT_VERSION = 1;
2
4
  const encodeShardMode = (mode) => {
3
5
  if (mode === "global" || mode === "root") {
@@ -39,6 +41,7 @@ const buildSchemaSnapshot = (schema, migrationIds) => {
39
41
  tables[table.name] = tableSnapshotOf(table);
40
42
  }
41
43
  return {
44
+ jurisdiction: schema.jurisdiction,
42
45
  migrationIds: [...migrationIds].toSorted((a, b) => a.localeCompare(b)),
43
46
  tables: sortKeys(tables),
44
47
  version: SCHEMA_SNAPSHOT_VERSION
@@ -46,8 +49,10 @@ const buildSchemaSnapshot = (schema, migrationIds) => {
46
49
  };
47
50
  const serializeSchemaSnapshot = (snapshot) => `${JSON.stringify(snapshot, void 0, 2)}
48
51
  `;
49
- class SchemaSnapshotParseError extends Error {
50
- name = "SchemaSnapshotParseError";
52
+ class SchemaSnapshotParseError extends LunoraError {
53
+ constructor(message) {
54
+ super("SCHEMA_SNAPSHOT_PARSE", message, { name: "SchemaSnapshotParseError" });
55
+ }
51
56
  }
52
57
  const isRecord = (value) => typeof value === "object" && value !== null;
53
58
  const isValidTableSnapshot = (value) => isRecord(value) && isRecord(value.fields) && isRecord(value.indexes) && isRecord(value.relations) && typeof value.shardMode === "string";
@@ -69,7 +74,9 @@ const parseSchemaSnapshot = (content) => {
69
74
  throw new SchemaSnapshotParseError(`baseline table "${name}" has an invalid structure`);
70
75
  }
71
76
  }
77
+ const jurisdiction = typeof parsed.jurisdiction === "string" ? parsed.jurisdiction : void 0;
72
78
  return {
79
+ jurisdiction,
73
80
  migrationIds: Array.isArray(parsed.migrationIds) ? parsed.migrationIds : [],
74
81
  tables: parsed.tables,
75
82
  version: SCHEMA_SNAPSHOT_VERSION
@@ -189,6 +196,15 @@ const diffSchemaSnapshots = (baseline, current) => {
189
196
  });
190
197
  }
191
198
  }
199
+ if (baseline !== void 0 && baseline.jurisdiction !== current.jurisdiction) {
200
+ const from = baseline.jurisdiction ?? "(none)";
201
+ const to = current.jurisdiction ?? "(none)";
202
+ changes.push({
203
+ severity: "breaking",
204
+ summary: `Durable Object jurisdiction changed from ${from} to ${to} — this re-homes every DO and strands all existing shard, scheduler, and session-DO data in the old region (no in-place migration; export then import to move it). Revert the change, or override the gate to proceed intentionally.`,
205
+ type: "changedJurisdiction"
206
+ });
207
+ }
192
208
  return { changes };
193
209
  };
194
210
  const evaluateSchemaDrift = (options) => {
@@ -0,0 +1,94 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { SyntaxKind, Node } from 'ts-morph';
4
+ import { diagnosticAt } from './CodegenDiagnosticError-DyQ5FwkM.mjs';
5
+
6
+ const SHAPES_FILENAME = "shapes.ts";
7
+ const SHAPE_MODULE_SPECIFIERS = /* @__PURE__ */ new Set(["@lunora/server", "lunorash/server"]);
8
+ const isDefineShape = (identifier) => {
9
+ const symbol = identifier.getSymbol();
10
+ if (!symbol) {
11
+ return identifier.getText() === "defineShape";
12
+ }
13
+ for (const declaration of symbol.getDeclarations()) {
14
+ if (!Node.isImportSpecifier(declaration)) {
15
+ continue;
16
+ }
17
+ if (!SHAPE_MODULE_SPECIFIERS.has(declaration.getImportDeclaration().getModuleSpecifierValue())) {
18
+ return false;
19
+ }
20
+ return declaration.getNameNode().getText() === "defineShape";
21
+ }
22
+ return false;
23
+ };
24
+ const isShapeNamespaceImport = (identifier) => {
25
+ const symbol = identifier.getSymbol();
26
+ if (!symbol) {
27
+ return false;
28
+ }
29
+ for (const declaration of symbol.getDeclarations()) {
30
+ if (!Node.isNamespaceImport(declaration)) {
31
+ continue;
32
+ }
33
+ const importDeclaration = declaration.getFirstAncestorByKind(SyntaxKind.ImportDeclaration);
34
+ return importDeclaration !== void 0 && SHAPE_MODULE_SPECIFIERS.has(importDeclaration.getModuleSpecifierValue());
35
+ }
36
+ return false;
37
+ };
38
+ const isDefineShapeCallee = (callee) => {
39
+ if (Node.isIdentifier(callee)) {
40
+ return isDefineShape(callee);
41
+ }
42
+ if (Node.isPropertyAccessExpression(callee)) {
43
+ const object = callee.getExpression();
44
+ return callee.getName() === "defineShape" && Node.isIdentifier(object) && isShapeNamespaceImport(object);
45
+ }
46
+ return false;
47
+ };
48
+ const tableLiteralFrom = (call) => {
49
+ const [config] = call.getArguments();
50
+ if (!config || !Node.isObjectLiteralExpression(config)) {
51
+ return void 0;
52
+ }
53
+ const tableProperty = config.getProperty("table");
54
+ if (!tableProperty || !Node.isPropertyAssignment(tableProperty)) {
55
+ return void 0;
56
+ }
57
+ const value = tableProperty.getInitializer();
58
+ return value && Node.isStringLiteral(value) ? value.getLiteralValue() : void 0;
59
+ };
60
+ const shapesFromSource = (source) => {
61
+ const shapes = [];
62
+ for (const declaration of source.getVariableDeclarations()) {
63
+ if (!declaration.isExported()) {
64
+ continue;
65
+ }
66
+ const initializer = declaration.getInitializer();
67
+ if (initializer?.getKind() !== SyntaxKind.CallExpression) {
68
+ continue;
69
+ }
70
+ const callExpression = initializer;
71
+ const callee = callExpression.getExpression();
72
+ if (!isDefineShapeCallee(callee)) {
73
+ continue;
74
+ }
75
+ const nameNode = declaration.getNameNode();
76
+ if (!Node.isIdentifier(nameNode)) {
77
+ throw diagnosticAt(nameNode, "defineShape exports must be plain named exports (no destructuring)");
78
+ }
79
+ shapes.push({ exportName: nameNode.getText(), filePath: "shapes", table: tableLiteralFrom(callExpression) });
80
+ }
81
+ return shapes;
82
+ };
83
+ const discoverShapes = (project, lunoraDirectory) => {
84
+ const shapesPath = join(lunoraDirectory, SHAPES_FILENAME);
85
+ if (!existsSync(shapesPath)) {
86
+ return [];
87
+ }
88
+ const source = project.getSourceFile(shapesPath) ?? project.addSourceFileAtPath(shapesPath);
89
+ const shapes = shapesFromSource(source);
90
+ shapes.sort((a, b) => a.exportName.localeCompare(b.exportName));
91
+ return shapes;
92
+ };
93
+
94
+ export { SHAPES_FILENAME, discoverShapes };
@@ -0,0 +1,158 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { LunoraError } from '@lunora/errors';
4
+ import { workflowDefaultName, workflowClassName, workflowBindingName } from '@lunora/workflow';
5
+ import { Node, SyntaxKind } from 'ts-morph';
6
+ import { diagnosticAt } from './CodegenDiagnosticError-DyQ5FwkM.mjs';
7
+
8
+ const WORKFLOWS_FILENAME = "workflows.ts";
9
+ const STEP_METHODS = /* @__PURE__ */ new Set(["do", "sleep", "sleepUntil", "waitForEvent"]);
10
+ const isDefineWorkflow = (identifier) => {
11
+ const symbol = identifier.getSymbol();
12
+ if (!symbol) {
13
+ return identifier.getText() === "defineWorkflow";
14
+ }
15
+ for (const declaration of symbol.getDeclarations()) {
16
+ if (!Node.isImportSpecifier(declaration)) {
17
+ continue;
18
+ }
19
+ if (declaration.getImportDeclaration().getModuleSpecifierValue() !== "@lunora/workflow") {
20
+ return false;
21
+ }
22
+ return declaration.getNameNode().getText() === "defineWorkflow";
23
+ }
24
+ return false;
25
+ };
26
+ const stringProperty = (expression, exportName, property) => {
27
+ if (Node.isStringLiteral(expression) || Node.isNoSubstitutionTemplateLiteral(expression)) {
28
+ return expression.getLiteralValue();
29
+ }
30
+ throw diagnosticAt(
31
+ expression,
32
+ `workflow "${exportName}": \`${property}\` must be a static string literal — it is deploy configuration codegen writes into wrangler.jsonc`
33
+ );
34
+ };
35
+ const isStepCall = (call) => {
36
+ const callee = call.getExpression();
37
+ if (!Node.isPropertyAccessExpression(callee) || !STEP_METHODS.has(callee.getName())) {
38
+ return false;
39
+ }
40
+ const receiver = callee.getExpression();
41
+ if (Node.isPropertyAccessExpression(receiver) && receiver.getName() === "step") {
42
+ return true;
43
+ }
44
+ return Node.isIdentifier(receiver) && receiver.getText() === "step";
45
+ };
46
+ const stepsFromHandler = (argument) => {
47
+ const handlerProperty = argument.getProperty("handler");
48
+ if (!handlerProperty) {
49
+ return [];
50
+ }
51
+ const body = Node.isPropertyAssignment(handlerProperty) ? handlerProperty.getInitializer() : handlerProperty;
52
+ if (!body) {
53
+ return [];
54
+ }
55
+ const steps = [];
56
+ for (const call of body.getDescendantsOfKind(SyntaxKind.CallExpression)) {
57
+ if (!isStepCall(call)) {
58
+ continue;
59
+ }
60
+ const nameArgument = call.getArguments()[0];
61
+ if (!nameArgument || !(Node.isStringLiteral(nameArgument) || Node.isNoSubstitutionTemplateLiteral(nameArgument))) {
62
+ continue;
63
+ }
64
+ steps.push({
65
+ line: call.getStartLineNumber(),
66
+ // `isStepCall` already proved the callee is a `.<method>` property access.
67
+ method: call.getExpression().getName(),
68
+ name: nameArgument.getLiteralValue()
69
+ });
70
+ }
71
+ return steps;
72
+ };
73
+ const workflowFromCall = (call, exportName) => {
74
+ const argument = call.getArguments()[0];
75
+ if (!argument || !Node.isObjectLiteralExpression(argument)) {
76
+ throw diagnosticAt(call, `workflow "${exportName}": defineWorkflow must be passed an inline object literal`);
77
+ }
78
+ const ir = {
79
+ bindingName: workflowBindingName(exportName),
80
+ className: workflowClassName(exportName),
81
+ exportName,
82
+ name: workflowDefaultName(exportName),
83
+ steps: stepsFromHandler(argument)
84
+ };
85
+ const nameProperty = argument.getProperty("name");
86
+ if (nameProperty && Node.isPropertyAssignment(nameProperty)) {
87
+ ir.name = stringProperty(nameProperty.getInitializerOrThrow(), exportName, "name");
88
+ }
89
+ return ir;
90
+ };
91
+ const unwrapToCallExpression = (node) => {
92
+ let current = node;
93
+ while (current && (Node.isAsExpression(current) || Node.isSatisfiesExpression(current) || Node.isParenthesizedExpression(current))) {
94
+ current = current.getExpression();
95
+ }
96
+ return current && Node.isCallExpression(current) ? current : void 0;
97
+ };
98
+ const workflowsFromSource = (source) => {
99
+ const workflows = [];
100
+ for (const declaration of source.getVariableDeclarations()) {
101
+ if (!declaration.isExported()) {
102
+ continue;
103
+ }
104
+ const call = unwrapToCallExpression(declaration.getInitializer());
105
+ if (!call) {
106
+ continue;
107
+ }
108
+ const callee = call.getExpression();
109
+ if (!Node.isIdentifier(callee) || !isDefineWorkflow(callee)) {
110
+ continue;
111
+ }
112
+ const nameNode = declaration.getNameNode();
113
+ if (!Node.isIdentifier(nameNode)) {
114
+ throw diagnosticAt(nameNode, "defineWorkflow exports must be plain named exports (no destructuring)");
115
+ }
116
+ workflows.push(workflowFromCall(call, nameNode.getText()));
117
+ }
118
+ return workflows;
119
+ };
120
+ const assertUniqueNames = (workflows) => {
121
+ const seenNames = /* @__PURE__ */ new Map();
122
+ const seenBindings = /* @__PURE__ */ new Map();
123
+ for (const workflow of workflows) {
124
+ const priorName = seenNames.get(workflow.name);
125
+ if (priorName !== void 0) {
126
+ throw new LunoraError(
127
+ // eslint-disable-next-line no-secrets/no-secrets -- an error code, not a secret
128
+ "DUPLICATE_WORKFLOW_NAME",
129
+ `Duplicate workflow name "${workflow.name}": produced by both "${priorName}" and "${workflow.exportName}". Deployed workflow names must be unique across the project.`,
130
+ { status: 500 }
131
+ );
132
+ }
133
+ seenNames.set(workflow.name, workflow.exportName);
134
+ const priorBinding = seenBindings.get(workflow.bindingName);
135
+ if (priorBinding !== void 0) {
136
+ throw new LunoraError(
137
+ // eslint-disable-next-line no-secrets/no-secrets -- an error code, not a secret
138
+ "DUPLICATE_WORKFLOW_BINDING",
139
+ `Duplicate workflow binding "${workflow.bindingName}": produced by both "${priorBinding}" and "${workflow.exportName}". Workflow export names must yield unique binding names.`,
140
+ { status: 500 }
141
+ );
142
+ }
143
+ seenBindings.set(workflow.bindingName, workflow.exportName);
144
+ }
145
+ };
146
+ const discoverWorkflows = (project, lunoraDirectory) => {
147
+ const workflowsPath = join(lunoraDirectory, WORKFLOWS_FILENAME);
148
+ if (!existsSync(workflowsPath)) {
149
+ return [];
150
+ }
151
+ const source = project.getSourceFile(workflowsPath) ?? project.addSourceFileAtPath(workflowsPath);
152
+ const workflows = workflowsFromSource(source);
153
+ workflows.sort((a, b) => a.exportName.localeCompare(b.exportName));
154
+ assertUniqueNames(workflows);
155
+ return workflows;
156
+ };
157
+
158
+ export { WORKFLOWS_FILENAME, discoverWorkflows };
@@ -1,6 +1,6 @@
1
- import { GENERATED_HEADER } from './GENERATED_HEADER-DF1hQcix.mjs';
1
+ import { G as GENERATED_HEADER } from './emit-ClTbCLy4.mjs';
2
2
  import { s as sanitizeNamespace } from './paths-BRd6JHuF.mjs';
3
- import { LUNORA_ERROR_CODES, objectSchema, argsObjectSchema, validatorIrToJsonSchema } from './LUNORA_ERROR_CODES-CySpQPD3.mjs';
3
+ import { LUNORA_ERROR_CODES, objectSchema, argsObjectSchema, validatorIrToJsonSchema } from './LUNORA_ERROR_CODES-DvTLozCu.mjs';
4
4
 
5
5
  const ERROR_COMPONENT_REF = "#/components/responses/LunoraError";
6
6
  const ROUTE_PARAM_RE = /:([A-Za-z_$][\w$]*)/gu;
@@ -1,7 +1,7 @@
1
1
  import { relative, sep } from 'node:path';
2
2
  import { SyntaxKind, Node } from 'ts-morph';
3
3
  import { T as TS_EXTENSION_RE, e as enclosingExportName } from './discover-ast-CT6BgBr4.mjs';
4
- import { listLunoraSourceFiles } from './discoverFunctions-DEgAcRuD.mjs';
4
+ import { listLunoraSourceFiles } from './discoverFunctions-BJ-qR7Rg.mjs';
5
5
 
6
6
  const isAuthApiCall = (call) => {
7
7
  const callee = call.getExpression();
@@ -1,6 +1,8 @@
1
+ import { LunoraError } from '@lunora/errors';
1
2
  import { isValidCronExpression, compileCronSchedule, CRON_SCHEDULE_KINDS } from '@lunora/scheduler';
2
3
  import { SyntaxKind, Node } from 'ts-morph';
3
- import { listLunoraSourceFiles } from './discoverFunctions-DEgAcRuD.mjs';
4
+ import { diagnosticAt } from './CodegenDiagnosticError-DyQ5FwkM.mjs';
5
+ import { listLunoraSourceFiles } from './discoverFunctions-BJ-qR7Rg.mjs';
4
6
  import { s as sanitizeNamespace } from './paths-BRd6JHuF.mjs';
5
7
 
6
8
  const CRON_METHODS = /* @__PURE__ */ new Set([...CRON_SCHEDULE_KINDS, "cron"]);
@@ -44,7 +46,7 @@ const rootIdentifierOf = (node) => {
44
46
  };
45
47
  const stringArgument = (call, index) => {
46
48
  const argument = call.getArguments()[index];
47
- return argument && Node.isStringLiteral(argument) ? argument.getLiteralValue() : void 0;
49
+ return argument && (Node.isStringLiteral(argument) || Node.isNoSubstitutionTemplateLiteral(argument)) ? argument.getLiteralValue() : void 0;
48
50
  };
49
51
  const literalValue = (node, jobName) => {
50
52
  if (Node.isStringLiteral(node) || Node.isNoSubstitutionTemplateLiteral(node)) {
@@ -71,7 +73,7 @@ const literalValue = (node, jobName) => {
71
73
  if (Node.isArrayLiteralExpression(node)) {
72
74
  return node.getElements().map((element) => literalValue(element, jobName));
73
75
  }
74
- throw Object.assign(new Error(`Cron job "${jobName}" passes a non-static value where a literal is required; codegen can only read literals.`), {
76
+ throw diagnosticAt(node, `Cron job "${jobName}" passes a non-static value where a literal is required; codegen can only read literals.`, {
75
77
  code: "CRON_NON_STATIC_VALUE",
76
78
  name: "LunoraError",
77
79
  status: 500
@@ -81,8 +83,9 @@ const objectLiteralValue = (object, jobName) => {
81
83
  const result = {};
82
84
  for (const property of object.getProperties()) {
83
85
  if (!Node.isPropertyAssignment(property)) {
84
- throw Object.assign(
85
- new Error(`Cron job "${jobName}" uses an unsupported object property (shorthand/spread/method) where a static literal is required.`),
86
+ throw diagnosticAt(
87
+ property,
88
+ `Cron job "${jobName}" uses an unsupported object property (shorthand/spread/method) where a static literal is required.`,
86
89
  {
87
90
  code: "CRON_NON_STATIC_VALUE",
88
91
  name: "LunoraError",
@@ -100,8 +103,9 @@ const objectLiteralValue = (object, jobName) => {
100
103
  const functionPathFromArgument = (call, index, jobName) => {
101
104
  const argument = call.getArguments()[index];
102
105
  if (!argument || !Node.isPropertyAccessExpression(argument)) {
103
- throw Object.assign(
104
- new Error(`Cron job "${jobName}" must reference a function statically (e.g. internal.email.digest); codegen cannot resolve a dynamic reference.`),
106
+ throw diagnosticAt(
107
+ call,
108
+ `Cron job "${jobName}" must reference a function statically (e.g. internal.email.digest); codegen cannot resolve a dynamic reference.`,
105
109
  {
106
110
  code: "CRON_NON_STATIC_FN",
107
111
  name: "LunoraError",
@@ -112,7 +116,7 @@ const functionPathFromArgument = (call, index, jobName) => {
112
116
  const functionName = argument.getName();
113
117
  const receiver = argument.getExpression();
114
118
  if (!Node.isPropertyAccessExpression(receiver)) {
115
- throw Object.assign(new Error(`Cron job "${jobName}" function reference must be of the form internal.file.fn (two property accesses).`), {
119
+ throw diagnosticAt(argument, `Cron job "${jobName}" function reference must be of the form internal.file.fn (two property accesses).`, {
116
120
  code: "CRON_NON_STATIC_FN",
117
121
  name: "LunoraError",
118
122
  status: 500
@@ -124,17 +128,48 @@ const functionPathFromArgument = (call, index, jobName) => {
124
128
  const workflowTarget = (workflow) => {
125
129
  return { workflow: { binding: workflow.bindingName, exportName: workflow.exportName } };
126
130
  };
127
- const resolveTarget = (call, index, jobName, workflowsByName) => {
131
+ const agentTarget = (agent) => {
132
+ return { workflow: { binding: agent.bindingName, exportName: agent.exportName } };
133
+ };
134
+ const resolveReferenceAccess = (argument, receiverName, byName, jobName, kind, build) => {
135
+ const receiver = argument.getExpression();
136
+ if (!Node.isIdentifier(receiver) || receiver.getText() !== receiverName) {
137
+ return void 0;
138
+ }
139
+ const definition = byName.get(argument.getName());
140
+ if (definition) {
141
+ return build(definition);
142
+ }
143
+ throw diagnosticAt(
144
+ argument,
145
+ `Cron job "${jobName}" targets ${receiverName}.${argument.getName()}, but no such ${kind} is declared in lunora/${kind}s.ts.`,
146
+ {
147
+ code: "CRON_NON_STATIC_FN",
148
+ name: "LunoraError",
149
+ status: 500
150
+ }
151
+ );
152
+ };
153
+ const resolveTarget = (call, index, jobName, workflowsByName, agentsByName) => {
128
154
  const argument = call.getArguments()[index];
129
155
  if (argument && Node.isPropertyAccessExpression(argument)) {
130
- const receiver = argument.getExpression();
131
- if (Node.isIdentifier(receiver) && receiver.getText() === "workflows") {
132
- const workflow = workflowsByName.get(argument.getName());
133
- if (workflow) {
134
- return workflowTarget(workflow);
135
- }
136
- throw Object.assign(
137
- new Error(`Cron job "${jobName}" targets workflows.${argument.getName()}, but no such workflow is declared in lunora/workflows.ts.`),
156
+ const workflowReference = resolveReferenceAccess(argument, "workflows", workflowsByName, jobName, "workflow", workflowTarget);
157
+ if (workflowReference) {
158
+ return workflowReference;
159
+ }
160
+ const agentReference = resolveReferenceAccess(argument, "agents", agentsByName, jobName, "agent", agentTarget);
161
+ if (agentReference) {
162
+ return agentReference;
163
+ }
164
+ return { functionPath: functionPathFromArgument(call, index, jobName) };
165
+ }
166
+ if (argument && Node.isIdentifier(argument)) {
167
+ const workflow = workflowsByName.get(argument.getText());
168
+ const agent = agentsByName.get(argument.getText());
169
+ if (workflow && agent) {
170
+ throw diagnosticAt(
171
+ argument,
172
+ `Cron job "${jobName}" references "${argument.getText()}", which is ambiguous: a workflow and an agent are both declared under that name.`,
138
173
  {
139
174
  code: "CRON_NON_STATIC_FN",
140
175
  name: "LunoraError",
@@ -142,17 +177,15 @@ const resolveTarget = (call, index, jobName, workflowsByName) => {
142
177
  }
143
178
  );
144
179
  }
145
- return { functionPath: functionPathFromArgument(call, index, jobName) };
146
- }
147
- if (argument && Node.isIdentifier(argument)) {
148
- const workflow = workflowsByName.get(argument.getText());
149
180
  if (workflow) {
150
181
  return workflowTarget(workflow);
151
182
  }
152
- throw Object.assign(
153
- new Error(
154
- `Cron job "${jobName}" references "${argument.getText()}", which is neither a function (internal.file.fn / api.file.fn) nor a declared workflow in lunora/workflows.ts.`
155
- ),
183
+ if (agent) {
184
+ return agentTarget(agent);
185
+ }
186
+ throw diagnosticAt(
187
+ argument,
188
+ `Cron job "${jobName}" references "${argument.getText()}", which is neither a function (internal.file.fn / api.file.fn) nor a declared workflow in lunora/workflows.ts nor a declared agent in lunora/agents.ts.`,
156
189
  {
157
190
  code: "CRON_NON_STATIC_FN",
158
191
  name: "LunoraError",
@@ -162,7 +195,7 @@ const resolveTarget = (call, index, jobName, workflowsByName) => {
162
195
  }
163
196
  return { functionPath: functionPathFromArgument(call, index, jobName) };
164
197
  };
165
- const cronFromCall = (call, callee, builderNames, workflowsByName) => {
198
+ const cronFromCall = (call, callee, builderNames, workflowsByName, agentsByName) => {
166
199
  const method = callee.getName();
167
200
  if (!CRON_METHODS.has(method)) {
168
201
  return void 0;
@@ -172,7 +205,7 @@ const cronFromCall = (call, callee, builderNames, workflowsByName) => {
172
205
  }
173
206
  const name = stringArgument(call, 0);
174
207
  if (name === void 0 || name.trim() === "") {
175
- throw Object.assign(new Error(`A cron ".${method}(...)" registration must pass a non-empty string-literal name as its first argument.`), {
208
+ throw diagnosticAt(call, `A cron ".${method}(...)" registration must pass a non-empty string-literal name as its first argument.`, {
176
209
  code: "CRON_NAME_NOT_STATIC",
177
210
  name: "LunoraError",
178
211
  status: 500
@@ -182,14 +215,14 @@ const cronFromCall = (call, callee, builderNames, workflowsByName) => {
182
215
  if (method === "cron") {
183
216
  const expression = stringArgument(call, 1);
184
217
  if (expression === void 0) {
185
- throw Object.assign(new Error(`Cron job "${name}" must pass a string-literal cron expression to ".cron(...)".`), {
218
+ throw diagnosticAt(call, `Cron job "${name}" must pass a string-literal cron expression to ".cron(...)".`, {
186
219
  code: "CRON_EXPR_NOT_STATIC",
187
220
  name: "LunoraError",
188
221
  status: 500
189
222
  });
190
223
  }
191
224
  if (!isValidCronExpression(expression)) {
192
- throw Object.assign(new Error(`Cron job "${name}" has an invalid cron expression "${expression}" — expected 5 or 6 space-separated fields.`), {
225
+ throw diagnosticAt(call, `Cron job "${name}" has an invalid cron expression "${expression}" — expected 5 or 6 space-separated fields.`, {
193
226
  code: "CRON_EXPR_INVALID",
194
227
  name: "LunoraError",
195
228
  status: 500
@@ -199,7 +232,7 @@ const cronFromCall = (call, callee, builderNames, workflowsByName) => {
199
232
  } else {
200
233
  const scheduleArgument = call.getArguments()[1];
201
234
  if (!scheduleArgument || !Node.isObjectLiteralExpression(scheduleArgument)) {
202
- throw Object.assign(new Error(`Cron job "${name}" must pass an object-literal schedule to ".${method}(...)".`), {
235
+ throw diagnosticAt(call, `Cron job "${name}" must pass an object-literal schedule to ".${method}(...)".`, {
203
236
  code: "CRON_SCHEDULE_NOT_STATIC",
204
237
  name: "LunoraError",
205
238
  status: 500
@@ -207,7 +240,7 @@ const cronFromCall = (call, callee, builderNames, workflowsByName) => {
207
240
  }
208
241
  cron = compileCronSchedule(method, objectLiteralValue(scheduleArgument, name));
209
242
  }
210
- const target = resolveTarget(call, 2, name, workflowsByName);
243
+ const target = resolveTarget(call, 2, name, workflowsByName, agentsByName);
211
244
  const argumentsNode = call.getArguments()[3];
212
245
  const args = argumentsNode && Node.isObjectLiteralExpression(argumentsNode) ? objectLiteralValue(argumentsNode, name) : {};
213
246
  return { args, cron, name, ...target };
@@ -216,19 +249,18 @@ const assertUniqueNames = (crons) => {
216
249
  const seen = /* @__PURE__ */ new Set();
217
250
  for (const cron of crons) {
218
251
  if (seen.has(cron.name)) {
219
- throw Object.assign(new Error(`Duplicate cron job name "${cron.name}": cron names must be unique across the project.`), {
220
- code: "DUPLICATE_CRON_NAME",
221
- name: "LunoraError",
252
+ throw new LunoraError("DUPLICATE_CRON_NAME", `Duplicate cron job name "${cron.name}": cron names must be unique across the project.`, {
222
253
  status: 500
223
254
  });
224
255
  }
225
256
  seen.add(cron.name);
226
257
  }
227
258
  };
228
- const discoverCrons = (project, lunoraDirectory, workflows = []) => {
259
+ const discoverCrons = (project, lunoraDirectory, workflows = [], agents = []) => {
229
260
  const filePaths = listLunoraSourceFiles(lunoraDirectory);
230
261
  const crons = [];
231
262
  const workflowsByName = new Map(workflows.map((workflow) => [workflow.exportName, workflow]));
263
+ const agentsByName = new Map(agents.map((agent) => [agent.exportName, agent]));
232
264
  for (const filePath of filePaths) {
233
265
  const source = project.getSourceFile(filePath) ?? project.addSourceFileAtPath(filePath);
234
266
  const builderNames = collectCronBuilderNames(source);
@@ -240,7 +272,7 @@ const discoverCrons = (project, lunoraDirectory, workflows = []) => {
240
272
  if (!Node.isPropertyAccessExpression(callee)) {
241
273
  continue;
242
274
  }
243
- const cron = cronFromCall(call, callee, builderNames, workflowsByName);
275
+ const cron = cronFromCall(call, callee, builderNames, workflowsByName, agentsByName);
244
276
  if (cron) {
245
277
  crons.push(cron);
246
278
  }