@flink-app/flink 2.0.0-alpha.58 → 2.0.0-alpha.59

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @flink-app/flink
2
2
 
3
+ ## 2.0.0-alpha.59
4
+
5
+ ### Minor Changes
6
+
7
+ - dbc2119: feat: add generic compiler extension system and @flink-app/inbound-email-plugin
8
+
9
+ Add `compilerPlugins` to `FlinkConfig` and `FlinkCompilerPlugin` interface so plugins can declare custom scan directories for auto-discovery. The TypeScript compiler reads these at build time, generates the corresponding `.flink/generatedXxx.ts` registration files, and includes them in the start script.
10
+
11
+ Introduce `@flink-app/inbound-email-plugin` as the first consumer of this system. It starts an SMTP server and automatically routes inbound emails to matching `EmailHandler` files discovered in `src/email-handlers/`. User and permission context are injected via `AsyncLocalStorage` for seamless integration with Flink tools and agents.
12
+
3
13
  ## 2.0.0-alpha.58
4
14
 
5
15
  ### Patch Changes
package/cli/cli-utils.ts CHANGED
@@ -116,6 +116,7 @@ export async function compile(opts: CompileOptions): Promise<void> {
116
116
  await compiler.parseTools();
117
117
  await compiler.parseAgents();
118
118
  await compiler.parseJobs();
119
+ await compiler.parseAllExtensionDirs();
119
120
  await compiler.parseHandlers();
120
121
  await compiler.generateStartScript(entry);
121
122
  await compiler.generateAllSchemas();
package/cli/dev.ts CHANGED
@@ -181,15 +181,24 @@ function startTscWatch(dir: string): ChildProcess {
181
181
 
182
182
  if (!trimmed) continue;
183
183
 
184
- if (trimmed.includes("Starting compilation") || trimmed.includes("File change detected")) {
184
+ // Filter out noisy watch-mode status lines
185
+ if (
186
+ trimmed.includes("Starting compilation") ||
187
+ trimmed.includes("File change detected") ||
188
+ trimmed.match(/^\[\d+:\d+:\d+\s+[AP]M\] Watching for file changes/)
189
+ ) {
185
190
  continue;
186
191
  }
187
192
 
188
193
  if (trimmed.includes("Found 0 errors")) {
189
194
  console.log(`[tsc] ✓ No type errors`);
190
- } else if (trimmed.includes("Found") && trimmed.includes("error")) {
191
- console.log(`[tsc] ${trimmed}`);
192
- } else if (trimmed.includes("error TS")) {
195
+ } else if (trimmed.match(/Found \d+ errors?/)) {
196
+ // Pretty-print the error count summary
197
+ const match = trimmed.match(/Found (\d+) errors?/);
198
+ const count = match?.[1];
199
+ console.log(`[tsc] ❌ Found ${count} type error${count === "1" ? "" : "s"} — see details above`);
200
+ } else {
201
+ // Show everything else: error details, file paths, code context, etc.
193
202
  console.log(`[tsc] ${trimmed}`);
194
203
  }
195
204
  }
@@ -125,17 +125,20 @@ function compile(opts) {
125
125
  return [4 /*yield*/, compiler.parseJobs()];
126
126
  case 5:
127
127
  _c.sent();
128
- return [4 /*yield*/, compiler.parseHandlers()];
128
+ return [4 /*yield*/, compiler.parseAllExtensionDirs()];
129
129
  case 6:
130
130
  _c.sent();
131
- return [4 /*yield*/, compiler.generateStartScript(entry)];
131
+ return [4 /*yield*/, compiler.parseHandlers()];
132
132
  case 7:
133
133
  _c.sent();
134
- return [4 /*yield*/, compiler.generateAllSchemas()];
134
+ return [4 /*yield*/, compiler.generateStartScript(entry)];
135
135
  case 8:
136
136
  _c.sent();
137
- return [4 /*yield*/, compiler.saveAllModifiedFiles()];
137
+ return [4 /*yield*/, compiler.generateAllSchemas()];
138
138
  case 9:
139
+ _c.sent();
140
+ return [4 /*yield*/, compiler.saveAllModifiedFiles()];
141
+ case 10:
139
142
  _c.sent();
140
143
  if (typeCheck) {
141
144
  stepStart = Date.now();
@@ -145,14 +148,14 @@ function compile(opts) {
145
148
  time("Type checking (getPreEmitDiagnostics)", stepStart);
146
149
  }
147
150
  stepStart = Date.now();
148
- if (!(process.env.FLINK_USE_TSC === "true")) return [3 /*break*/, 10];
151
+ if (!(process.env.FLINK_USE_TSC === "true")) return [3 /*break*/, 11];
149
152
  compiler.emitWithTsc();
150
- return [3 /*break*/, 12];
151
- case 10: return [4 /*yield*/, compiler.emit()];
152
- case 11:
153
- _c.sent();
154
- _c.label = 12;
153
+ return [3 /*break*/, 13];
154
+ case 11: return [4 /*yield*/, compiler.emit()];
155
155
  case 12:
156
+ _c.sent();
157
+ _c.label = 13;
158
+ case 13:
156
159
  time("Transpilation (".concat(process.env.FLINK_USE_TSC === "true" ? "tsc" : "swc", ")"), stepStart);
157
160
  if (timingLogs) {
158
161
  console.log("Compilation done in ".concat(Date.now() - start, "ms"));
package/dist/cli/dev.js CHANGED
@@ -215,16 +215,23 @@ function startTscWatch(dir) {
215
215
  var trimmed = line.replace(/\x1Bc/g, "").trim();
216
216
  if (!trimmed)
217
217
  continue;
218
- if (trimmed.includes("Starting compilation") || trimmed.includes("File change detected")) {
218
+ // Filter out noisy watch-mode status lines
219
+ if (trimmed.includes("Starting compilation") ||
220
+ trimmed.includes("File change detected") ||
221
+ trimmed.match(/^\[\d+:\d+:\d+\s+[AP]M\] Watching for file changes/)) {
219
222
  continue;
220
223
  }
221
224
  if (trimmed.includes("Found 0 errors")) {
222
225
  console.log("[tsc] \u2713 No type errors");
223
226
  }
224
- else if (trimmed.includes("Found") && trimmed.includes("error")) {
225
- console.log("[tsc] ".concat(trimmed));
227
+ else if (trimmed.match(/Found \d+ errors?/)) {
228
+ // Pretty-print the error count summary
229
+ var match = trimmed.match(/Found (\d+) errors?/);
230
+ var count = match === null || match === void 0 ? void 0 : match[1];
231
+ console.log("[tsc] \u274C Found ".concat(count, " type error").concat(count === "1" ? "" : "s", " \u2014 see details above"));
226
232
  }
227
- else if (trimmed.includes("error TS")) {
233
+ else {
234
+ // Show everything else: error details, file paths, code context, etc.
228
235
  console.log("[tsc] ".concat(trimmed));
229
236
  }
230
237
  }
@@ -1,4 +1,5 @@
1
1
  import { SourceFile } from "ts-morph";
2
+ import { FlinkCompilerPlugin } from "./utils/loadFlinkConfig";
2
3
  declare class TypeScriptCompiler {
3
4
  private cwd;
4
5
  private project;
@@ -24,6 +25,14 @@ declare class TypeScriptCompiler {
24
25
  * Tool ID registry for agent validation (built during segmentation)
25
26
  */
26
27
  private toolIdRegistry;
28
+ /**
29
+ * Compiler plugins loaded from flink.config.js
30
+ */
31
+ private compilerPlugins;
32
+ /**
33
+ * Extension files collected during segmentation, keyed by generatedFile name
34
+ */
35
+ private extensionFiles;
27
36
  /**
28
37
  * Generates a schema $id from a file path and type name using the same algorithm
29
38
  * as the schema generator's defineId callback.
@@ -206,5 +215,16 @@ declare class TypeScriptCompiler {
206
215
  * Scans project for jobs so they can be registered during start.
207
216
  */
208
217
  parseJobs(): Promise<SourceFile>;
218
+ /**
219
+ * Generates a .flink/generatedXxx.ts file for a single compiler plugin extension.
220
+ * Mirrors the same namespace-import + spread pattern used by parseJobs.
221
+ */
222
+ parseExtensionDir(ext: FlinkCompilerPlugin): Promise<SourceFile>;
223
+ /**
224
+ * Iterates all compilerPlugins from flink.config.js and generates
225
+ * a .flink/generatedXxx.ts file for each one.
226
+ * Call this after parseJobs() and before generateStartScript().
227
+ */
228
+ parseAllExtensionDirs(): Promise<void>;
209
229
  }
210
230
  export default TypeScriptCompiler;
@@ -90,11 +90,12 @@ var FlinkLogFactory_1 = require("./FlinkLogFactory");
90
90
  var FsUtils_1 = require("./FsUtils");
91
91
  var schema_extraction_1 = require("./schema-extraction");
92
92
  var utils_1 = require("./utils");
93
+ var loadFlinkConfig_1 = require("./utils/loadFlinkConfig");
93
94
  var perfLog = FlinkLogFactory_1.FlinkLogFactory.createLogger("flink.perf");
94
95
  var initLog = FlinkLogFactory_1.FlinkLogFactory.createLogger("flink.init");
95
96
  var TypeScriptCompiler = /** @class */ (function () {
96
97
  function TypeScriptCompiler(cwd) {
97
- var _a, _b;
98
+ var _a, _b, _c;
98
99
  /**
99
100
  * Handler schemas collected during parseHandlers, to be generated later
100
101
  */
@@ -115,6 +116,14 @@ var TypeScriptCompiler = /** @class */ (function () {
115
116
  * Tool ID registry for agent validation (built during segmentation)
116
117
  */
117
118
  this.toolIdRegistry = new Set();
119
+ /**
120
+ * Compiler plugins loaded from flink.config.js
121
+ */
122
+ this.compilerPlugins = [];
123
+ /**
124
+ * Extension files collected during segmentation, keyed by generatedFile name
125
+ */
126
+ this.extensionFiles = new Map();
118
127
  var path = require("path");
119
128
  // Convert to absolute path to ensure consistent path comparisons
120
129
  this.cwd = path.resolve(cwd);
@@ -189,6 +198,15 @@ var TypeScriptCompiler = /** @class */ (function () {
189
198
  var loadTime = Date.now() - loadStartTime;
190
199
  var fileCount = this.project.getSourceFiles().length;
191
200
  perfLog.debug("\u2713 All source files loaded in ".concat(loadTime, "ms (").concat(fileCount, " files)"));
201
+ // Load compiler plugins from flink.config.js
202
+ var flinkCfg = (0, loadFlinkConfig_1.loadFlinkConfig)(this.cwd);
203
+ this.compilerPlugins = (_c = flinkCfg === null || flinkCfg === void 0 ? void 0 : flinkCfg.compilerPlugins) !== null && _c !== void 0 ? _c : [];
204
+ if (this.compilerPlugins.length > 0) {
205
+ initLog.info("Compiler plugins loaded (".concat(this.compilerPlugins.length, "):"), this.compilerPlugins.map(function (p) { return "".concat(p.package, " \u2192 ").concat(p.scanDir); }).join(", "));
206
+ }
207
+ else {
208
+ initLog.info("No compiler plugins configured");
209
+ }
192
210
  // Segment files by type for efficient processing
193
211
  this.segmentSourceFiles();
194
212
  console.log("Loaded", this.project.getSourceFiles().length, "source file(s) from", cwd);
@@ -423,6 +441,7 @@ var TypeScriptCompiler = /** @class */ (function () {
423
441
  * multiple full-file iterations during parse methods.
424
442
  */
425
443
  TypeScriptCompiler.prototype.segmentSourceFiles = function () {
444
+ var _a;
426
445
  var startTime = Date.now();
427
446
  var allFiles = this.project.getSourceFiles();
428
447
  var handlerTime = 0, repoTime = 0, toolTime = 0, agentTime = 0, jobTime = 0;
@@ -470,7 +489,19 @@ var TypeScriptCompiler = /** @class */ (function () {
470
489
  if (filePath.includes("src/jobs/")) {
471
490
  this.jobFiles.push(sf);
472
491
  jobTime += Date.now() - fileStartTime;
473
- // No continue needed (last check)
492
+ continue;
493
+ }
494
+ // Extension dirs from compiler plugins
495
+ for (var _b = 0, _c = this.compilerPlugins; _b < _c.length; _b++) {
496
+ var ext = _c[_b];
497
+ if (filePath.includes(ext.scanDir)) {
498
+ if (!ext.detectBy || ext.detectBy(sf.getFullText(), filePath)) {
499
+ var list = (_a = this.extensionFiles.get(ext.generatedFile)) !== null && _a !== void 0 ? _a : [];
500
+ list.push(sf);
501
+ this.extensionFiles.set(ext.generatedFile, list);
502
+ }
503
+ break;
504
+ }
474
505
  }
475
506
  }
476
507
  var segmentTime = Date.now() - startTime;
@@ -1112,7 +1143,8 @@ var TypeScriptCompiler = /** @class */ (function () {
1112
1143
  */
1113
1144
  TypeScriptCompiler.prototype.generateStartScript = function () {
1114
1145
  return __awaiter(this, arguments, void 0, function (appEntryScript) {
1115
- var path, entryScriptPath, specFiles, sf;
1146
+ var path, entryScriptPath, specFiles, extensionImports, sf;
1147
+ var _this = this;
1116
1148
  if (appEntryScript === void 0) { appEntryScript = "/src/index.ts"; }
1117
1149
  return __generator(this, function (_a) {
1118
1150
  switch (_a.label) {
@@ -1137,7 +1169,10 @@ var TypeScriptCompiler = /** @class */ (function () {
1137
1169
  this.resolveImportedFiles();
1138
1170
  _a.label = 3;
1139
1171
  case 3:
1140
- sf = this.createSourceFile(["start.ts"], "// Generated ".concat(new Date(), "\nimport \"./generatedHandlers").concat(this.isEsm ? ".js" : "", "\";\nimport \"./generatedRepos").concat(this.isEsm ? ".js" : "", "\";\nimport \"./generatedTools").concat(this.isEsm ? ".js" : "", "\";\nimport \"./generatedAgents").concat(this.isEsm ? ".js" : "", "\";\nimport \"./generatedJobs").concat(this.isEsm ? ".js" : "", "\";\nimport \"..").concat(appEntryScript.replace(/\.ts/g, "")).concat(this.isEsm ? ".js" : "", "\";\nexport default {}; // Export an empty object to make it a module\n"));
1172
+ extensionImports = this.compilerPlugins
1173
+ .map(function (ext) { return "import \"./".concat(ext.generatedFile).concat(_this.isEsm ? ".js" : "", "\";"); })
1174
+ .join("\n");
1175
+ sf = this.createSourceFile(["start.ts"], "// Generated ".concat(new Date(), "\nimport \"./generatedHandlers").concat(this.isEsm ? ".js" : "", "\";\nimport \"./generatedRepos").concat(this.isEsm ? ".js" : "", "\";\nimport \"./generatedTools").concat(this.isEsm ? ".js" : "", "\";\nimport \"./generatedAgents").concat(this.isEsm ? ".js" : "", "\";\nimport \"./generatedJobs").concat(this.isEsm ? ".js" : "", "\";\n").concat(extensionImports ? extensionImports + "\n" : "", "import \"..").concat(appEntryScript.replace(/\.ts/g, "")).concat(this.isEsm ? ".js" : "", "\";\nexport default {}; // Export an empty object to make it a module\n"));
1141
1176
  // Defer save until batch save at end (performance optimization)
1142
1177
  return [2 /*return*/, sf];
1143
1178
  }
@@ -1482,6 +1517,66 @@ var TypeScriptCompiler = /** @class */ (function () {
1482
1517
  });
1483
1518
  });
1484
1519
  };
1520
+ /**
1521
+ * Generates a .flink/generatedXxx.ts file for a single compiler plugin extension.
1522
+ * Mirrors the same namespace-import + spread pattern used by parseJobs.
1523
+ */
1524
+ TypeScriptCompiler.prototype.parseExtensionDir = function (ext) {
1525
+ return __awaiter(this, void 0, void 0, function () {
1526
+ var startTime, files, generatedFile, itemsArr, imports, itemElements, elapsed;
1527
+ var _this = this;
1528
+ var _a;
1529
+ return __generator(this, function (_b) {
1530
+ startTime = Date.now();
1531
+ files = (_a = this.extensionFiles.get(ext.generatedFile)) !== null && _a !== void 0 ? _a : [];
1532
+ generatedFile = this.createSourceFile(["".concat(ext.generatedFile, ".ts")], "// Generated ".concat(new Date(), "\nimport { ").concat(ext.registrationVar, " } from \"").concat(ext.package, "\";\nexport const items: any[] = [];\n").concat(ext.registrationVar, ".push(...items);\n"));
1533
+ itemsArr = generatedFile.getVariableDeclarationOrThrow("items").getFirstDescendantByKindOrThrow(ts_morph_1.SyntaxKind.ArrayLiteralExpression);
1534
+ imports = [];
1535
+ itemElements = [];
1536
+ files.forEach(function (sf, i) {
1537
+ var namespaceImport = sf.getBaseNameWithoutExtension().replace(/\./g, "_") + "_" + i;
1538
+ imports.push({
1539
+ defaultImport: "* as " + namespaceImport,
1540
+ moduleSpecifier: _this.getModuleSpecifier(generatedFile, sf),
1541
+ });
1542
+ itemElements.push("{...".concat(namespaceImport, ", __file: \"").concat(_this.getRelativePath(sf), "\"}"));
1543
+ });
1544
+ itemsArr.addElements(itemElements);
1545
+ generatedFile.addImportDeclarations(imports);
1546
+ elapsed = Date.now() - startTime;
1547
+ initLog.info("\u2713 Extension dir \"".concat(ext.scanDir, "\" parsed in ").concat(elapsed, "ms (").concat(files.length, " files)"));
1548
+ return [2 /*return*/, generatedFile];
1549
+ });
1550
+ });
1551
+ };
1552
+ /**
1553
+ * Iterates all compilerPlugins from flink.config.js and generates
1554
+ * a .flink/generatedXxx.ts file for each one.
1555
+ * Call this after parseJobs() and before generateStartScript().
1556
+ */
1557
+ TypeScriptCompiler.prototype.parseAllExtensionDirs = function () {
1558
+ return __awaiter(this, void 0, void 0, function () {
1559
+ var _i, _a, ext;
1560
+ return __generator(this, function (_b) {
1561
+ switch (_b.label) {
1562
+ case 0:
1563
+ _i = 0, _a = this.compilerPlugins;
1564
+ _b.label = 1;
1565
+ case 1:
1566
+ if (!(_i < _a.length)) return [3 /*break*/, 4];
1567
+ ext = _a[_i];
1568
+ return [4 /*yield*/, this.parseExtensionDir(ext)];
1569
+ case 2:
1570
+ _b.sent();
1571
+ _b.label = 3;
1572
+ case 3:
1573
+ _i++;
1574
+ return [3 /*break*/, 1];
1575
+ case 4: return [2 /*return*/];
1576
+ }
1577
+ });
1578
+ });
1579
+ };
1485
1580
  return TypeScriptCompiler;
1486
1581
  }());
1487
1582
  exports.default = TypeScriptCompiler;
@@ -1,9 +1,31 @@
1
1
  import { LoggingConfig } from "../FlinkLogFactory";
2
+ /**
3
+ * Compiler plugin descriptor — declared in flink.config.js, consumed by the TypeScript compiler.
4
+ * Plugin packages export a compilerPlugin() factory that returns this shape.
5
+ */
6
+ export interface FlinkCompilerPlugin {
7
+ /** npm package name, e.g. "@flink-app/inbound-email-plugin" */
8
+ package: string;
9
+ /** Directory to scan for handler files, e.g. "src/email-handlers" */
10
+ scanDir: string;
11
+ /** Base name for the generated .flink/generatedXxx.ts file, e.g. "generatedEmailHandlers" */
12
+ generatedFile: string;
13
+ /** Name of the singleton array exported by the plugin package, e.g. "autoRegisteredEmailHandlers" */
14
+ registrationVar: string;
15
+ /**
16
+ * Optional callback to confirm a file should be registered.
17
+ * Receives the raw file content and the absolute file path.
18
+ * Return true to include the file, false to skip it.
19
+ */
20
+ detectBy?: (fileContent: string, filePath: string) => boolean;
21
+ }
2
22
  /**
3
23
  * Flink configuration file structure
4
24
  */
5
25
  export interface FlinkConfig {
6
26
  logging?: LoggingConfig;
27
+ /** Compiler plugins that extend auto-discovery to custom directories */
28
+ compilerPlugins?: FlinkCompilerPlugin[];
7
29
  }
8
30
  /**
9
31
  * Load Flink configuration from flink.config.js in current working directory
@@ -26,4 +48,4 @@ export interface FlinkConfig {
26
48
  * };
27
49
  * ```
28
50
  */
29
- export declare function loadFlinkConfig(): FlinkConfig | undefined;
51
+ export declare function loadFlinkConfig(cwd?: string): FlinkConfig | undefined;
@@ -47,10 +47,10 @@ var fs = __importStar(require("fs"));
47
47
  * };
48
48
  * ```
49
49
  */
50
- function loadFlinkConfig() {
50
+ function loadFlinkConfig(cwd) {
51
51
  try {
52
- // Look for flink.config.js in current working directory
53
- var configPath = path.join(process.cwd(), "flink.config.js");
52
+ // Look for flink.config.js in current working directory (or provided cwd)
53
+ var configPath = path.join(cwd !== null && cwd !== void 0 ? cwd : process.cwd(), "flink.config.js");
54
54
  // Check if file exists
55
55
  if (!fs.existsSync(configPath)) {
56
56
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flink-app/flink",
3
- "version": "2.0.0-alpha.58",
3
+ "version": "2.0.0-alpha.59",
4
4
  "description": "Typescript only framework for creating REST-like APIs on top of Express and mongodb",
5
5
  "types": "dist/src/index.d.ts",
6
6
  "main": "dist/src/index.js",
@@ -6,6 +6,7 @@ import { FlinkLogFactory } from "./FlinkLogFactory";
6
6
  import { writeJsonFile } from "./FsUtils";
7
7
  import { TypeScriptSourceParser } from "./schema-extraction";
8
8
  import { getCollectionNameForRepo, getHttpMethodFromHandlerName, getRepoInstanceName } from "./utils";
9
+ import { FlinkCompilerPlugin, loadFlinkConfig } from "./utils/loadFlinkConfig";
9
10
 
10
11
  const perfLog = FlinkLogFactory.createLogger("flink.perf");
11
12
  const initLog = FlinkLogFactory.createLogger("flink.init");
@@ -50,6 +51,16 @@ class TypeScriptCompiler {
50
51
  */
51
52
  private toolIdRegistry: Set<string> = new Set();
52
53
 
54
+ /**
55
+ * Compiler plugins loaded from flink.config.js
56
+ */
57
+ private compilerPlugins: FlinkCompilerPlugin[] = [];
58
+
59
+ /**
60
+ * Extension files collected during segmentation, keyed by generatedFile name
61
+ */
62
+ private extensionFiles: Map<string, SourceFile[]> = new Map();
63
+
53
64
  /**
54
65
  * Generates a schema $id from a file path and type name using the same algorithm
55
66
  * as the schema generator's defineId callback.
@@ -216,6 +227,19 @@ class TypeScriptCompiler {
216
227
  const fileCount = this.project.getSourceFiles().length;
217
228
  perfLog.debug(`✓ All source files loaded in ${loadTime}ms (${fileCount} files)`);
218
229
 
230
+ // Load compiler plugins from flink.config.js
231
+ const flinkCfg = loadFlinkConfig(this.cwd);
232
+ this.compilerPlugins = flinkCfg?.compilerPlugins ?? [];
233
+
234
+ if (this.compilerPlugins.length > 0) {
235
+ initLog.info(
236
+ `Compiler plugins loaded (${this.compilerPlugins.length}):`,
237
+ this.compilerPlugins.map((p) => `${p.package} → ${p.scanDir}`).join(", ")
238
+ );
239
+ } else {
240
+ initLog.info("No compiler plugins configured");
241
+ }
242
+
219
243
  // Segment files by type for efficient processing
220
244
  this.segmentSourceFiles();
221
245
 
@@ -457,7 +481,19 @@ class TypeScriptCompiler {
457
481
  if (filePath.includes("src/jobs/")) {
458
482
  this.jobFiles.push(sf);
459
483
  jobTime += Date.now() - fileStartTime;
460
- // No continue needed (last check)
484
+ continue;
485
+ }
486
+
487
+ // Extension dirs from compiler plugins
488
+ for (const ext of this.compilerPlugins) {
489
+ if (filePath.includes(ext.scanDir)) {
490
+ if (!ext.detectBy || ext.detectBy(sf.getFullText(), filePath)) {
491
+ const list = this.extensionFiles.get(ext.generatedFile) ?? [];
492
+ list.push(sf);
493
+ this.extensionFiles.set(ext.generatedFile, list);
494
+ }
495
+ break;
496
+ }
461
497
  }
462
498
  }
463
499
 
@@ -1157,6 +1193,10 @@ autoRegisteredAgents.push(...agents);
1157
1193
  this.resolveImportedFiles();
1158
1194
  }
1159
1195
 
1196
+ const extensionImports = this.compilerPlugins
1197
+ .map((ext) => `import "./${ext.generatedFile}${this.isEsm ? ".js" : ""}";`)
1198
+ .join("\n");
1199
+
1160
1200
  const sf = this.createSourceFile(
1161
1201
  ["start.ts"],
1162
1202
  `// Generated ${new Date()}
@@ -1165,7 +1205,7 @@ import "./generatedRepos${this.isEsm ? ".js" : ""}";
1165
1205
  import "./generatedTools${this.isEsm ? ".js" : ""}";
1166
1206
  import "./generatedAgents${this.isEsm ? ".js" : ""}";
1167
1207
  import "./generatedJobs${this.isEsm ? ".js" : ""}";
1168
- import "..${appEntryScript.replace(/\.ts/g, "")}${this.isEsm ? ".js" : ""}";
1208
+ ${extensionImports ? extensionImports + "\n" : ""}import "..${appEntryScript.replace(/\.ts/g, "")}${this.isEsm ? ".js" : ""}";
1169
1209
  export default {}; // Export an empty object to make it a module
1170
1210
  `
1171
1211
  );
@@ -1560,6 +1600,57 @@ autoRegisteredJobs.push(...jobs);
1560
1600
 
1561
1601
  return generatedFile;
1562
1602
  }
1603
+
1604
+ /**
1605
+ * Generates a .flink/generatedXxx.ts file for a single compiler plugin extension.
1606
+ * Mirrors the same namespace-import + spread pattern used by parseJobs.
1607
+ */
1608
+ async parseExtensionDir(ext: FlinkCompilerPlugin): Promise<SourceFile> {
1609
+ const startTime = Date.now();
1610
+ const files = this.extensionFiles.get(ext.generatedFile) ?? [];
1611
+
1612
+ const generatedFile = this.createSourceFile(
1613
+ [`${ext.generatedFile}.ts`],
1614
+ `// Generated ${new Date()}
1615
+ import { ${ext.registrationVar} } from "${ext.package}";
1616
+ export const items: any[] = [];
1617
+ ${ext.registrationVar}.push(...items);
1618
+ `
1619
+ );
1620
+
1621
+ const itemsArr = generatedFile.getVariableDeclarationOrThrow("items").getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
1622
+
1623
+ const imports: OptionalKind<ImportDeclarationStructure>[] = [];
1624
+ const itemElements: string[] = [];
1625
+
1626
+ files.forEach((sf, i) => {
1627
+ const namespaceImport = sf.getBaseNameWithoutExtension().replace(/\./g, "_") + "_" + i;
1628
+ imports.push({
1629
+ defaultImport: "* as " + namespaceImport,
1630
+ moduleSpecifier: this.getModuleSpecifier(generatedFile, sf),
1631
+ });
1632
+ itemElements.push(`{...${namespaceImport}, __file: "${this.getRelativePath(sf)}"}`);
1633
+ });
1634
+
1635
+ itemsArr.addElements(itemElements);
1636
+ generatedFile.addImportDeclarations(imports);
1637
+
1638
+ const elapsed = Date.now() - startTime;
1639
+ initLog.info(`✓ Extension dir "${ext.scanDir}" parsed in ${elapsed}ms (${files.length} files)`);
1640
+
1641
+ return generatedFile;
1642
+ }
1643
+
1644
+ /**
1645
+ * Iterates all compilerPlugins from flink.config.js and generates
1646
+ * a .flink/generatedXxx.ts file for each one.
1647
+ * Call this after parseJobs() and before generateStartScript().
1648
+ */
1649
+ public async parseAllExtensionDirs(): Promise<void> {
1650
+ for (const ext of this.compilerPlugins) {
1651
+ await this.parseExtensionDir(ext);
1652
+ }
1653
+ }
1563
1654
  }
1564
1655
 
1565
1656
  export default TypeScriptCompiler;
@@ -2,11 +2,34 @@ import * as path from "path";
2
2
  import * as fs from "fs";
3
3
  import { LoggingConfig } from "../FlinkLogFactory";
4
4
 
5
+ /**
6
+ * Compiler plugin descriptor — declared in flink.config.js, consumed by the TypeScript compiler.
7
+ * Plugin packages export a compilerPlugin() factory that returns this shape.
8
+ */
9
+ export interface FlinkCompilerPlugin {
10
+ /** npm package name, e.g. "@flink-app/inbound-email-plugin" */
11
+ package: string;
12
+ /** Directory to scan for handler files, e.g. "src/email-handlers" */
13
+ scanDir: string;
14
+ /** Base name for the generated .flink/generatedXxx.ts file, e.g. "generatedEmailHandlers" */
15
+ generatedFile: string;
16
+ /** Name of the singleton array exported by the plugin package, e.g. "autoRegisteredEmailHandlers" */
17
+ registrationVar: string;
18
+ /**
19
+ * Optional callback to confirm a file should be registered.
20
+ * Receives the raw file content and the absolute file path.
21
+ * Return true to include the file, false to skip it.
22
+ */
23
+ detectBy?: (fileContent: string, filePath: string) => boolean;
24
+ }
25
+
5
26
  /**
6
27
  * Flink configuration file structure
7
28
  */
8
29
  export interface FlinkConfig {
9
30
  logging?: LoggingConfig;
31
+ /** Compiler plugins that extend auto-discovery to custom directories */
32
+ compilerPlugins?: FlinkCompilerPlugin[];
10
33
  }
11
34
 
12
35
  /**
@@ -30,10 +53,10 @@ export interface FlinkConfig {
30
53
  * };
31
54
  * ```
32
55
  */
33
- export function loadFlinkConfig(): FlinkConfig | undefined {
56
+ export function loadFlinkConfig(cwd?: string): FlinkConfig | undefined {
34
57
  try {
35
- // Look for flink.config.js in current working directory
36
- const configPath = path.join(process.cwd(), "flink.config.js");
58
+ // Look for flink.config.js in current working directory (or provided cwd)
59
+ const configPath = path.join(cwd ?? process.cwd(), "flink.config.js");
37
60
 
38
61
  // Check if file exists
39
62
  if (!fs.existsSync(configPath)) {