@beignet/cli 0.0.27 → 0.0.28
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 +12 -0
- package/README.md +31 -6
- package/dist/check.d.ts +61 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +164 -0
- package/dist/check.js.map +1 -0
- package/dist/db.d.ts +8 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +2 -2
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -8
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +70 -0
- package/dist/inspect.js.map +1 -1
- package/dist/make/shared.d.ts +17 -0
- package/dist/make/shared.d.ts.map +1 -1
- package/dist/make/shared.js +67 -0
- package/dist/make/shared.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +11 -55
- package/dist/make.js.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/mcp.js.map +1 -1
- package/dist/provider-audit.d.ts.map +1 -1
- package/dist/provider-audit.js +77 -1
- package/dist/provider-audit.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +27 -9
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +42 -12
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/db/sqlite.d.ts.map +1 -1
- package/dist/templates/db/sqlite.js +7 -1
- package/dist/templates/db/sqlite.js.map +1 -1
- package/package.json +2 -2
- package/src/check.ts +246 -0
- package/src/db.ts +2 -2
- package/src/index.ts +62 -8
- package/src/inspect.ts +100 -0
- package/src/make/shared.ts +88 -0
- package/src/make.ts +10 -68
- package/src/mcp.ts +1 -1
- package/src/provider-audit.ts +95 -3
- package/src/templates/agents.ts +27 -9
- package/src/templates/base.ts +43 -12
- package/src/templates/db/sqlite.ts +7 -1
package/src/check.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createPainter } from "./ansi.js";
|
|
4
|
+
import { detectPackageManager, spawnCommand } from "./db.js";
|
|
5
|
+
import {
|
|
6
|
+
applyDoctorFixes,
|
|
7
|
+
formatDoctor,
|
|
8
|
+
type InspectFix,
|
|
9
|
+
inspectApp,
|
|
10
|
+
} from "./inspect.js";
|
|
11
|
+
import { formatLint, lintApp } from "./lint.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One step of `beignet check`.
|
|
15
|
+
*
|
|
16
|
+
* `output` carries the failing step's formatted diagnostics or captured
|
|
17
|
+
* script output so the summary is actionable without a re-run.
|
|
18
|
+
*/
|
|
19
|
+
export type CheckStep = {
|
|
20
|
+
name: string;
|
|
21
|
+
command: string;
|
|
22
|
+
status: "passed" | "failed" | "skipped";
|
|
23
|
+
detail?: string;
|
|
24
|
+
output?: string;
|
|
25
|
+
durationMs: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type CheckAppResult = {
|
|
29
|
+
schemaVersion: 1;
|
|
30
|
+
targetDir: string;
|
|
31
|
+
runner: string;
|
|
32
|
+
ok: boolean;
|
|
33
|
+
fixes: InspectFix[];
|
|
34
|
+
steps: CheckStep[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type CheckAppOptions = {
|
|
38
|
+
cwd?: string;
|
|
39
|
+
fix?: boolean;
|
|
40
|
+
color?: boolean;
|
|
41
|
+
/** Called after each step completes, for incremental progress output. */
|
|
42
|
+
onStep?: (step: CheckStep) => void;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const scriptStepNames = ["lint", "typecheck", "test"] as const;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Run the whole app validation loop as one command: `beignet lint`,
|
|
49
|
+
* `beignet doctor --strict` (optionally with fixes), and the app's own
|
|
50
|
+
* `lint`, `typecheck`, and `test` package scripts through the detected
|
|
51
|
+
* package manager.
|
|
52
|
+
*
|
|
53
|
+
* Every step runs even when an earlier one fails, so a single run reports
|
|
54
|
+
* everything that needs fixing. Missing package scripts are reported as
|
|
55
|
+
* skipped, never as failures.
|
|
56
|
+
*/
|
|
57
|
+
export async function checkApp(
|
|
58
|
+
options: CheckAppOptions = {},
|
|
59
|
+
): Promise<CheckAppResult> {
|
|
60
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
61
|
+
const runner = await detectPackageManager(targetDir);
|
|
62
|
+
const steps: CheckStep[] = [];
|
|
63
|
+
const record = (step: CheckStep): void => {
|
|
64
|
+
steps.push(step);
|
|
65
|
+
options.onStep?.(step);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
record(await lintStep(targetDir, options));
|
|
69
|
+
const { step: doctorStep, fixes } = await runDoctorStep(targetDir, options);
|
|
70
|
+
record(doctorStep);
|
|
71
|
+
|
|
72
|
+
const scripts = await readPackageScripts(targetDir);
|
|
73
|
+
for (const script of scriptStepNames) {
|
|
74
|
+
record(await scriptStep(targetDir, runner, script, scripts));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
schemaVersion: 1,
|
|
79
|
+
targetDir,
|
|
80
|
+
runner,
|
|
81
|
+
ok: steps.every((step) => step.status !== "failed"),
|
|
82
|
+
fixes,
|
|
83
|
+
steps,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Format a completed step as a single status line, with the failing step's
|
|
89
|
+
* output indented underneath.
|
|
90
|
+
*/
|
|
91
|
+
export function formatCheckStep(
|
|
92
|
+
step: CheckStep,
|
|
93
|
+
options: { color?: boolean } = {},
|
|
94
|
+
): string {
|
|
95
|
+
const paint = createPainter(options.color);
|
|
96
|
+
const mark =
|
|
97
|
+
step.status === "passed"
|
|
98
|
+
? paint("✓", "green")
|
|
99
|
+
: step.status === "failed"
|
|
100
|
+
? paint("✗", "red")
|
|
101
|
+
: paint("-", "cyan");
|
|
102
|
+
const detail = step.detail ? ` (${step.detail})` : "";
|
|
103
|
+
const line = `${mark} ${step.name}${detail}`;
|
|
104
|
+
if (step.status !== "failed" || !step.output?.trim()) return line;
|
|
105
|
+
|
|
106
|
+
const indented = step.output
|
|
107
|
+
.trimEnd()
|
|
108
|
+
.split("\n")
|
|
109
|
+
.map((outputLine) => ` ${outputLine}`)
|
|
110
|
+
.join("\n");
|
|
111
|
+
return `${line}\n${indented}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Format the closing summary line, e.g. `3 passed, 1 failed, 1 skipped`.
|
|
116
|
+
*/
|
|
117
|
+
export function formatCheckSummary(
|
|
118
|
+
result: CheckAppResult,
|
|
119
|
+
options: { color?: boolean } = {},
|
|
120
|
+
): string {
|
|
121
|
+
const paint = createPainter(options.color);
|
|
122
|
+
const counts = {
|
|
123
|
+
passed: result.steps.filter((step) => step.status === "passed").length,
|
|
124
|
+
failed: result.steps.filter((step) => step.status === "failed").length,
|
|
125
|
+
skipped: result.steps.filter((step) => step.status === "skipped").length,
|
|
126
|
+
};
|
|
127
|
+
const parts = [`${counts.passed} passed`];
|
|
128
|
+
if (counts.failed > 0) parts.push(paint(`${counts.failed} failed`, "red"));
|
|
129
|
+
if (counts.skipped > 0) parts.push(`${counts.skipped} skipped`);
|
|
130
|
+
return parts.join(", ");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Format a full result at once (JSON-free callers and tests).
|
|
135
|
+
*/
|
|
136
|
+
export function formatCheck(
|
|
137
|
+
result: CheckAppResult,
|
|
138
|
+
options: { color?: boolean } = {},
|
|
139
|
+
): string {
|
|
140
|
+
const lines = result.steps.map((step) => formatCheckStep(step, options));
|
|
141
|
+
return `${lines.join("\n")}\n\n${formatCheckSummary(result, options)}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function lintStep(
|
|
145
|
+
targetDir: string,
|
|
146
|
+
options: CheckAppOptions,
|
|
147
|
+
): Promise<CheckStep> {
|
|
148
|
+
const startedAt = Date.now();
|
|
149
|
+
const result = await lintApp({ cwd: targetDir });
|
|
150
|
+
const failed = result.diagnostics.length > 0;
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
name: "beignet lint",
|
|
154
|
+
command: "beignet lint",
|
|
155
|
+
status: failed ? "failed" : "passed",
|
|
156
|
+
...(failed ? { output: formatLint(result, { color: options.color }) } : {}),
|
|
157
|
+
durationMs: Date.now() - startedAt,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function runDoctorStep(
|
|
162
|
+
targetDir: string,
|
|
163
|
+
options: CheckAppOptions,
|
|
164
|
+
): Promise<{ step: CheckStep; fixes: InspectFix[] }> {
|
|
165
|
+
const startedAt = Date.now();
|
|
166
|
+
const fixes = options.fix
|
|
167
|
+
? await applyDoctorFixes({ cwd: targetDir, strict: true })
|
|
168
|
+
: [];
|
|
169
|
+
const result = await inspectApp({ cwd: targetDir, strict: true });
|
|
170
|
+
result.fixes = fixes;
|
|
171
|
+
const failed = result.diagnostics.some(
|
|
172
|
+
(diagnostic) =>
|
|
173
|
+
diagnostic.severity === "error" || diagnostic.severity === "warning",
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
step: {
|
|
178
|
+
name: "beignet doctor --strict",
|
|
179
|
+
command: "beignet doctor --strict",
|
|
180
|
+
status: failed ? "failed" : "passed",
|
|
181
|
+
...(fixes.length > 0
|
|
182
|
+
? {
|
|
183
|
+
detail: `${fixes.length} ${fixes.length === 1 ? "fix" : "fixes"} applied`,
|
|
184
|
+
}
|
|
185
|
+
: {}),
|
|
186
|
+
...(failed
|
|
187
|
+
? { output: formatDoctor(result, { color: options.color }) }
|
|
188
|
+
: {}),
|
|
189
|
+
durationMs: Date.now() - startedAt,
|
|
190
|
+
},
|
|
191
|
+
fixes,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function scriptStep(
|
|
196
|
+
targetDir: string,
|
|
197
|
+
runner: string,
|
|
198
|
+
script: (typeof scriptStepNames)[number],
|
|
199
|
+
scripts: Record<string, string>,
|
|
200
|
+
): Promise<CheckStep> {
|
|
201
|
+
const command = `${runner} run ${script}`;
|
|
202
|
+
if (!scripts[script]) {
|
|
203
|
+
return {
|
|
204
|
+
name: script,
|
|
205
|
+
command,
|
|
206
|
+
status: "skipped",
|
|
207
|
+
detail: `no "${script}" script in package.json`,
|
|
208
|
+
durationMs: 0,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const startedAt = Date.now();
|
|
213
|
+
const result = await spawnCommand(runner, ["run", script], targetDir, {
|
|
214
|
+
captureOutput: true,
|
|
215
|
+
});
|
|
216
|
+
const failed = result.exitCode !== 0;
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
name: script,
|
|
220
|
+
command,
|
|
221
|
+
status: failed ? "failed" : "passed",
|
|
222
|
+
detail: command,
|
|
223
|
+
...(failed
|
|
224
|
+
? { output: [result.stdout, result.stderr].filter(Boolean).join("\n") }
|
|
225
|
+
: {}),
|
|
226
|
+
durationMs: Date.now() - startedAt,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function readPackageScripts(
|
|
231
|
+
targetDir: string,
|
|
232
|
+
): Promise<Record<string, string>> {
|
|
233
|
+
let source: string;
|
|
234
|
+
try {
|
|
235
|
+
source = await readFile(path.join(targetDir, "package.json"), "utf8");
|
|
236
|
+
} catch {
|
|
237
|
+
throw new Error(
|
|
238
|
+
"Could not find package.json. Run beignet check from the app root.",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const parsed = JSON.parse(source) as {
|
|
243
|
+
scripts?: Record<string, string>;
|
|
244
|
+
};
|
|
245
|
+
return parsed.scripts ?? {};
|
|
246
|
+
}
|
package/src/db.ts
CHANGED
|
@@ -482,7 +482,7 @@ async function readPackageJson(cwd: string): Promise<PackageJson> {
|
|
|
482
482
|
}
|
|
483
483
|
}
|
|
484
484
|
|
|
485
|
-
async function detectPackageManager(cwd: string): Promise<string> {
|
|
485
|
+
export async function detectPackageManager(cwd: string): Promise<string> {
|
|
486
486
|
if (await exists(path.join(cwd, "bun.lock"))) return "bun";
|
|
487
487
|
if (await exists(path.join(cwd, "bun.lockb"))) return "bun";
|
|
488
488
|
if (await exists(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
@@ -500,7 +500,7 @@ async function exists(filePath: string): Promise<boolean> {
|
|
|
500
500
|
}
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
-
function spawnCommand(
|
|
503
|
+
export function spawnCommand(
|
|
504
504
|
command: string,
|
|
505
505
|
args: readonly string[],
|
|
506
506
|
cwd: string,
|
package/src/index.ts
CHANGED
|
@@ -105,6 +105,12 @@ type LintFlags = {
|
|
|
105
105
|
format?: OutputFormat;
|
|
106
106
|
};
|
|
107
107
|
|
|
108
|
+
type CheckFlags = {
|
|
109
|
+
json?: boolean;
|
|
110
|
+
fix?: boolean;
|
|
111
|
+
cwd?: string;
|
|
112
|
+
};
|
|
113
|
+
|
|
108
114
|
type DbFlags = {
|
|
109
115
|
json?: boolean;
|
|
110
116
|
dryRun?: boolean;
|
|
@@ -601,6 +607,55 @@ const doctorCommand = buildCommand<DoctorFlags, [], CliContext>({
|
|
|
601
607
|
},
|
|
602
608
|
});
|
|
603
609
|
|
|
610
|
+
const checkCommand = buildCommand<CheckFlags, [], CliContext>({
|
|
611
|
+
docs: {
|
|
612
|
+
brief: "Run the full app validation loop as one command.",
|
|
613
|
+
fullDescription:
|
|
614
|
+
"Runs beignet lint, beignet doctor --strict, and the app's lint, typecheck, and test package scripts through the detected package manager. Every step runs even when an earlier one fails, so one run reports everything. Missing package scripts are skipped, not failed.",
|
|
615
|
+
},
|
|
616
|
+
parameters: {
|
|
617
|
+
flags: {
|
|
618
|
+
json: jsonFlag,
|
|
619
|
+
fix: {
|
|
620
|
+
kind: "boolean",
|
|
621
|
+
optional: true,
|
|
622
|
+
withNegated: false,
|
|
623
|
+
brief: "Apply low-risk doctor fixes before checking.",
|
|
624
|
+
},
|
|
625
|
+
cwd: cwdFlag,
|
|
626
|
+
} satisfies FlagParametersForType<CheckFlags, CliContext>,
|
|
627
|
+
},
|
|
628
|
+
loader: async () => {
|
|
629
|
+
const { checkApp, formatCheckStep, formatCheckSummary } = await import(
|
|
630
|
+
"./check.js"
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
return async function runCheck(this: CliContext, flags: CheckFlags) {
|
|
634
|
+
const color = useColor();
|
|
635
|
+
const result = await checkApp({
|
|
636
|
+
cwd: flags.cwd,
|
|
637
|
+
fix: flags.fix,
|
|
638
|
+
color,
|
|
639
|
+
...(flags.json
|
|
640
|
+
? {}
|
|
641
|
+
: {
|
|
642
|
+
onStep: (step) =>
|
|
643
|
+
writeOutput(this, formatCheckStep(step, { color })),
|
|
644
|
+
}),
|
|
645
|
+
});
|
|
646
|
+
writeOutput(
|
|
647
|
+
this,
|
|
648
|
+
flags.json
|
|
649
|
+
? JSON.stringify(result, null, 2)
|
|
650
|
+
: `\n${formatCheckSummary(result, { color })}`,
|
|
651
|
+
);
|
|
652
|
+
if (!result.ok) {
|
|
653
|
+
this.process.exitCode = 1;
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
},
|
|
657
|
+
});
|
|
658
|
+
|
|
604
659
|
const lintCommand = buildCommand<LintFlags, [], CliContext>({
|
|
605
660
|
docs: {
|
|
606
661
|
brief: "Check Beignet dependency direction conventions.",
|
|
@@ -1493,6 +1548,7 @@ const rootRoutes = buildRouteMap({
|
|
|
1493
1548
|
Run npm create beignet@latest (or bun create beignet) to scaffold a new app.`,
|
|
1494
1549
|
},
|
|
1495
1550
|
routes: {
|
|
1551
|
+
check: checkCommand,
|
|
1496
1552
|
completion: completionRoutes,
|
|
1497
1553
|
create: createCommand,
|
|
1498
1554
|
db: dbRoutes,
|
|
@@ -1506,6 +1562,10 @@ Run npm create beignet@latest (or bun create beignet) to scaffold a new app.`,
|
|
|
1506
1562
|
schedule: scheduleRoutes,
|
|
1507
1563
|
task: taskRoutes,
|
|
1508
1564
|
},
|
|
1565
|
+
aliases: {
|
|
1566
|
+
// The singular form is a common near-miss for the providers commands.
|
|
1567
|
+
provider: "providers",
|
|
1568
|
+
},
|
|
1509
1569
|
});
|
|
1510
1570
|
|
|
1511
1571
|
const cli = buildApplication(rootRoutes, {
|
|
@@ -1697,19 +1757,13 @@ ${openStep}
|
|
|
1697
1757
|
|
|
1698
1758
|
Inspect the app:
|
|
1699
1759
|
${cli} routes
|
|
1700
|
-
${
|
|
1701
|
-
${cli} lint
|
|
1702
|
-
${cli} doctor
|
|
1760
|
+
${cli} check
|
|
1703
1761
|
|
|
1704
1762
|
Generate a feature:
|
|
1705
1763
|
${cli} make feature projects
|
|
1706
1764
|
${cli} db generate
|
|
1707
1765
|
${cli} db migrate
|
|
1708
|
-
${
|
|
1709
|
-
${run} lint
|
|
1710
|
-
${run} typecheck
|
|
1711
|
-
${cli} lint
|
|
1712
|
-
${cli} doctor`;
|
|
1766
|
+
${cli} check`;
|
|
1713
1767
|
}
|
|
1714
1768
|
|
|
1715
1769
|
function packageRunnerFromPackageManager(pm: PackageManager): string {
|
package/src/inspect.ts
CHANGED
|
@@ -19,6 +19,10 @@ import {
|
|
|
19
19
|
formatGithubAnnotation,
|
|
20
20
|
type GithubAnnotationSeverity,
|
|
21
21
|
} from "./github-annotations.js";
|
|
22
|
+
import {
|
|
23
|
+
providersFilePath,
|
|
24
|
+
wireListenersProviderSource,
|
|
25
|
+
} from "./make/shared.js";
|
|
22
26
|
import {
|
|
23
27
|
detectedProviderVariants,
|
|
24
28
|
diagnosticPackageJsonFile,
|
|
@@ -269,6 +273,14 @@ export async function applyDoctorFixes(
|
|
|
269
273
|
...(await fixUnregisteredOutboxEntries(targetDir, files, drift, config)),
|
|
270
274
|
);
|
|
271
275
|
|
|
276
|
+
const listenerFix = await fixUnregisteredListeners(
|
|
277
|
+
targetDir,
|
|
278
|
+
files,
|
|
279
|
+
drift,
|
|
280
|
+
config,
|
|
281
|
+
);
|
|
282
|
+
if (listenerFix) fixes.push(listenerFix);
|
|
283
|
+
|
|
272
284
|
const openApiFix = await fixDirectOpenApiArrayDrift(
|
|
273
285
|
targetDir,
|
|
274
286
|
files,
|
|
@@ -5462,6 +5474,94 @@ async function fixUnregisteredTasks(
|
|
|
5462
5474
|
});
|
|
5463
5475
|
}
|
|
5464
5476
|
|
|
5477
|
+
/**
|
|
5478
|
+
* Wire fully unregistered feature listener registries into the providers
|
|
5479
|
+
* file with the same `<registry>Provider` block `beignet make listener`
|
|
5480
|
+
* generates, so both write paths stay byte-compatible.
|
|
5481
|
+
*
|
|
5482
|
+
* Bails without writing when the providers file or its exported array is
|
|
5483
|
+
* missing or customized, when the app has no eventBus port to register
|
|
5484
|
+
* against, or when the registry name is already imported from another file.
|
|
5485
|
+
* The diagnostic stays in every bail case.
|
|
5486
|
+
*/
|
|
5487
|
+
async function fixUnregisteredListeners(
|
|
5488
|
+
targetDir: string,
|
|
5489
|
+
files: string[],
|
|
5490
|
+
drift: WorkflowRegistrationDrift,
|
|
5491
|
+
config: ResolvedBeignetConfig,
|
|
5492
|
+
): Promise<InspectFix | undefined> {
|
|
5493
|
+
const candidates = drift.listeners.unregistered.filter(
|
|
5494
|
+
(entry) => entry.fullyUnregistered,
|
|
5495
|
+
);
|
|
5496
|
+
if (candidates.length === 0) return undefined;
|
|
5497
|
+
|
|
5498
|
+
const providersFile = providersFilePath(config);
|
|
5499
|
+
const contextFile = normalizePath(
|
|
5500
|
+
path.join(path.dirname(config.paths.server), "context.ts"),
|
|
5501
|
+
);
|
|
5502
|
+
const wiringDependencies = [
|
|
5503
|
+
providersFile,
|
|
5504
|
+
contextFile,
|
|
5505
|
+
config.paths.appContext,
|
|
5506
|
+
config.paths.ports,
|
|
5507
|
+
];
|
|
5508
|
+
if (wiringDependencies.some((file) => !files.includes(file))) {
|
|
5509
|
+
return undefined;
|
|
5510
|
+
}
|
|
5511
|
+
|
|
5512
|
+
// The generated wiring registers listeners on ports.eventBus; without the
|
|
5513
|
+
// port the fix would produce code that cannot typecheck.
|
|
5514
|
+
const portsSource = await readFile(
|
|
5515
|
+
path.join(targetDir, config.paths.ports),
|
|
5516
|
+
"utf8",
|
|
5517
|
+
);
|
|
5518
|
+
if (!/\beventBus\s*[:?]/.test(portsSource)) return undefined;
|
|
5519
|
+
|
|
5520
|
+
const providersPath = path.join(targetDir, providersFile);
|
|
5521
|
+
const original = await readFile(providersPath, "utf8");
|
|
5522
|
+
let next = original;
|
|
5523
|
+
const registeredNames: string[] = [];
|
|
5524
|
+
|
|
5525
|
+
for (const { registry } of candidates) {
|
|
5526
|
+
const imported = parseNamedImportSources(next).get(registry.registryName);
|
|
5527
|
+
if (imported) {
|
|
5528
|
+
const importedFile = sourceFileFromImport(
|
|
5529
|
+
imported.sourcePath,
|
|
5530
|
+
providersFile,
|
|
5531
|
+
files,
|
|
5532
|
+
);
|
|
5533
|
+
if (importedFile !== registry.indexFile) return undefined;
|
|
5534
|
+
}
|
|
5535
|
+
|
|
5536
|
+
const result = wireListenersProviderSource(next, {
|
|
5537
|
+
registryName: registry.registryName,
|
|
5538
|
+
featureKebab: listenerFeatureKebab(registry.indexFile),
|
|
5539
|
+
listenerModule: `@/${modulePath(registry.indexFile)}`,
|
|
5540
|
+
config,
|
|
5541
|
+
});
|
|
5542
|
+
if (result.kind === "missing") return undefined;
|
|
5543
|
+
if (result.kind === "updated") {
|
|
5544
|
+
next = result.source;
|
|
5545
|
+
registeredNames.push(registry.registryName);
|
|
5546
|
+
}
|
|
5547
|
+
}
|
|
5548
|
+
|
|
5549
|
+
if (next === original || registeredNames.length === 0) return undefined;
|
|
5550
|
+
|
|
5551
|
+
await writeFile(providersPath, next);
|
|
5552
|
+
return {
|
|
5553
|
+
code: "BEIGNET_LISTENER_UNREGISTERED",
|
|
5554
|
+
file: providersFile,
|
|
5555
|
+
message: `Registered ${registeredNames.join(", ")} with registerListeners(...) in ${providersFile}.`,
|
|
5556
|
+
};
|
|
5557
|
+
}
|
|
5558
|
+
|
|
5559
|
+
function listenerFeatureKebab(indexFile: string): string {
|
|
5560
|
+
return path.posix.basename(
|
|
5561
|
+
path.posix.dirname(path.posix.dirname(normalizePath(indexFile))),
|
|
5562
|
+
);
|
|
5563
|
+
}
|
|
5564
|
+
|
|
5465
5565
|
async function fixUnregisteredOutboxEntries(
|
|
5466
5566
|
targetDir: string,
|
|
5467
5567
|
files: string[],
|
package/src/make/shared.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type ResolvedBeignetConfig,
|
|
8
8
|
} from "../config.js";
|
|
9
9
|
import {
|
|
10
|
+
type AppendResult,
|
|
10
11
|
appendToArrayExpression,
|
|
11
12
|
appendToNamedArray,
|
|
12
13
|
arrayInitializerInfo,
|
|
@@ -349,6 +350,93 @@ export function providersFilePath(config: ResolvedBeignetConfig): string {
|
|
|
349
350
|
return `${directoryPath(path.dirname(config.paths.server))}/providers.ts`;
|
|
350
351
|
}
|
|
351
352
|
|
|
353
|
+
/**
|
|
354
|
+
* Wire a feature listener registry into the generated providers file by
|
|
355
|
+
* adding a `<registry>Provider` lifecycle provider whose setup calls
|
|
356
|
+
* `registerListeners(...)` and registering it in the providers array.
|
|
357
|
+
*
|
|
358
|
+
* Shared by `beignet make listener` and `beignet doctor --fix` so both write
|
|
359
|
+
* paths produce byte-identical wiring. Returns `missing` when the exported
|
|
360
|
+
* providers array anchor cannot be found; callers decide whether that is a
|
|
361
|
+
* hard error (make) or a keep-the-diagnostic bail (doctor).
|
|
362
|
+
*/
|
|
363
|
+
export function wireListenersProviderSource(
|
|
364
|
+
source: string,
|
|
365
|
+
options: {
|
|
366
|
+
registryName: string;
|
|
367
|
+
featureKebab: string;
|
|
368
|
+
listenerModule: string;
|
|
369
|
+
config: ResolvedBeignetConfig;
|
|
370
|
+
},
|
|
371
|
+
): AppendResult {
|
|
372
|
+
const { registryName, featureKebab, listenerModule, config } = options;
|
|
373
|
+
const providerName = `${registryName}Provider`;
|
|
374
|
+
let next = source;
|
|
375
|
+
next = addNamedImport(next, "registerListeners", "@beignet/core/events");
|
|
376
|
+
next = addNamedImport(next, "createServiceActor", "@beignet/core/ports");
|
|
377
|
+
next = addNamedImport(next, "createProvider", "@beignet/core/providers");
|
|
378
|
+
next = addNamedTypeImport(
|
|
379
|
+
next,
|
|
380
|
+
"AppContext",
|
|
381
|
+
aliasModule(config.paths.appContext),
|
|
382
|
+
);
|
|
383
|
+
next = addNamedTypeImport(
|
|
384
|
+
next,
|
|
385
|
+
"AppServiceContextInput",
|
|
386
|
+
aliasModule(path.join(path.dirname(config.paths.server), "context.ts")),
|
|
387
|
+
);
|
|
388
|
+
next = addNamedTypeImport(next, "AppPorts", aliasModule(config.paths.ports));
|
|
389
|
+
|
|
390
|
+
const listenerImport = `import { ${registryName} } from "${listenerModule}";`;
|
|
391
|
+
if (!next.includes(listenerImport)) {
|
|
392
|
+
next = insertAfterImports(next, listenerImport);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (!new RegExp(`\\bconst\\s+${providerName}\\b`).test(next)) {
|
|
396
|
+
const providerDefinition = `const ${providerName} = createProvider<
|
|
397
|
+
\tPick<AppPorts, "eventBus" | "logger">,
|
|
398
|
+
\tAppContext,
|
|
399
|
+
\tAppServiceContextInput
|
|
400
|
+
>()({
|
|
401
|
+
\tname: "${featureKebab}-listeners",
|
|
402
|
+
\tsetup({ ports, createServiceContext }) {
|
|
403
|
+
\t\tconst unregister = registerListeners(ports.eventBus, ${registryName}, {
|
|
404
|
+
\t\t\tctx: () =>
|
|
405
|
+
\t\t\t\tcreateServiceContext({
|
|
406
|
+
\t\t\t\t\tactor: createServiceActor("beignet-listener"),
|
|
407
|
+
\t\t\t\t}),
|
|
408
|
+
\t\t\tonError(error, listener) {
|
|
409
|
+
\t\t\t\tports.logger.error("Event listener failed", {
|
|
410
|
+
\t\t\t\t\terror,
|
|
411
|
+
\t\t\t\t\tlistenerName: listener.name,
|
|
412
|
+
\t\t\t\t});
|
|
413
|
+
\t\t\t},
|
|
414
|
+
\t\t});
|
|
415
|
+
|
|
416
|
+
\t\treturn {
|
|
417
|
+
\t\t\tstop() {
|
|
418
|
+
\t\t\t\tunregister();
|
|
419
|
+
\t\t\t},
|
|
420
|
+
\t\t};
|
|
421
|
+
\t},
|
|
422
|
+
});
|
|
423
|
+
`;
|
|
424
|
+
const withProvider = next.replace(
|
|
425
|
+
/\nexport const providers = \[/,
|
|
426
|
+
`\n${providerDefinition}\nexport const providers = [`,
|
|
427
|
+
);
|
|
428
|
+
if (withProvider === next) return { kind: "missing" };
|
|
429
|
+
next = withProvider;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const appended = appendToNamedArray(next, "providers", providerName);
|
|
433
|
+
if (appended.kind === "missing") return { kind: "missing" };
|
|
434
|
+
if (appended.kind === "updated") next = appended.source;
|
|
435
|
+
|
|
436
|
+
if (next === source) return { kind: "unchanged" };
|
|
437
|
+
return { kind: "updated", source: next };
|
|
438
|
+
}
|
|
439
|
+
|
|
352
440
|
/**
|
|
353
441
|
* Wire provider-backed app ports a generator depends on.
|
|
354
442
|
*
|
package/src/make.ts
CHANGED
|
@@ -131,6 +131,7 @@ import {
|
|
|
131
131
|
usesFeatureOwnedPolicies,
|
|
132
132
|
usesFeatureOwnedTests,
|
|
133
133
|
type WriteGeneratedFileResult,
|
|
134
|
+
wireListenersProviderSource,
|
|
134
135
|
wirePortProviders,
|
|
135
136
|
writeGeneratedFile,
|
|
136
137
|
writePlannedGeneratedFile,
|
|
@@ -1392,79 +1393,20 @@ async function updateListenerProviderWiring(
|
|
|
1392
1393
|
}
|
|
1393
1394
|
|
|
1394
1395
|
const listenerIndex = featureArtifactIndexFile("listener", names, config);
|
|
1395
|
-
const
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
"AppContext",
|
|
1403
|
-
aliasModule(config.paths.appContext),
|
|
1404
|
-
);
|
|
1405
|
-
next = addNamedTypeImport(
|
|
1406
|
-
next,
|
|
1407
|
-
"AppServiceContextInput",
|
|
1408
|
-
aliasModule(path.join(path.dirname(config.paths.server), "context.ts")),
|
|
1409
|
-
);
|
|
1410
|
-
next = addNamedTypeImport(next, "AppPorts", aliasModule(config.paths.ports));
|
|
1411
|
-
|
|
1412
|
-
const listenerImport = `import { ${listenerIndex.registryName} } from "${aliasModule(listenerIndex.path)}";`;
|
|
1413
|
-
if (!next.includes(listenerImport)) {
|
|
1414
|
-
next = insertAfterImports(next, listenerImport);
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
if (!new RegExp(`\\bconst\\s+${providerName}\\b`).test(next)) {
|
|
1418
|
-
const providerDefinition = `const ${providerName} = createProvider<
|
|
1419
|
-
\tPick<AppPorts, "eventBus" | "logger">,
|
|
1420
|
-
\tAppContext,
|
|
1421
|
-
\tAppServiceContextInput
|
|
1422
|
-
>()({
|
|
1423
|
-
\tname: "${names.feature.kebab}-listeners",
|
|
1424
|
-
\tsetup({ ports, createServiceContext }) {
|
|
1425
|
-
\t\tconst unregister = registerListeners(ports.eventBus, ${listenerIndex.registryName}, {
|
|
1426
|
-
\t\t\tctx: () =>
|
|
1427
|
-
\t\t\t\tcreateServiceContext({
|
|
1428
|
-
\t\t\t\t\tactor: createServiceActor("beignet-listener"),
|
|
1429
|
-
\t\t\t\t}),
|
|
1430
|
-
\t\t\tonError(error, listener) {
|
|
1431
|
-
\t\t\t\tports.logger.error("Event listener failed", {
|
|
1432
|
-
\t\t\t\t\terror,
|
|
1433
|
-
\t\t\t\t\tlistenerName: listener.name,
|
|
1434
|
-
\t\t\t\t});
|
|
1435
|
-
\t\t\t},
|
|
1436
|
-
\t\t});
|
|
1437
|
-
|
|
1438
|
-
\t\treturn {
|
|
1439
|
-
\t\t\tstop() {
|
|
1440
|
-
\t\t\t\tunregister();
|
|
1441
|
-
\t\t\t},
|
|
1442
|
-
\t\t};
|
|
1443
|
-
\t},
|
|
1444
|
-
});
|
|
1445
|
-
`;
|
|
1446
|
-
const withProvider = next.replace(
|
|
1447
|
-
/\nexport const providers = \[/,
|
|
1448
|
-
`\n${providerDefinition}\nexport const providers = [`,
|
|
1449
|
-
);
|
|
1450
|
-
if (withProvider === next) {
|
|
1451
|
-
throw new Error(
|
|
1452
|
-
`Could not find the exported providers array in ${providersFile}. Register ${listenerIndex.registryName} manually with registerListeners(...), or restore the generated providers file before running make listener.`,
|
|
1453
|
-
);
|
|
1454
|
-
}
|
|
1455
|
-
next = withProvider;
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
const appended = appendToNamedArray(next, "providers", providerName);
|
|
1459
|
-
if (appended.kind === "missing") {
|
|
1396
|
+
const result = wireListenersProviderSource(original, {
|
|
1397
|
+
registryName: listenerIndex.registryName,
|
|
1398
|
+
featureKebab: names.feature.kebab,
|
|
1399
|
+
listenerModule: aliasModule(listenerIndex.path),
|
|
1400
|
+
config,
|
|
1401
|
+
});
|
|
1402
|
+
if (result.kind === "missing") {
|
|
1460
1403
|
throw new Error(
|
|
1461
1404
|
`Could not find the exported providers array in ${providersFile}. Register ${listenerIndex.registryName} manually with registerListeners(...), or restore the generated providers file before running make listener.`,
|
|
1462
1405
|
);
|
|
1463
1406
|
}
|
|
1464
|
-
if (appended.kind === "updated") next = appended.source;
|
|
1465
1407
|
|
|
1466
|
-
if (
|
|
1467
|
-
if (!options.dryRun) await writeFile(filePath,
|
|
1408
|
+
if (result.kind === "unchanged") return "skipped";
|
|
1409
|
+
if (!options.dryRun) await writeFile(filePath, result.source);
|
|
1468
1410
|
return "updated";
|
|
1469
1411
|
}
|
|
1470
1412
|
|
package/src/mcp.ts
CHANGED
|
@@ -380,7 +380,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
|
|
|
380
380
|
"doctor_fix",
|
|
381
381
|
{
|
|
382
382
|
description:
|
|
383
|
-
"Apply low-risk doctor fixes, then re-check the app. Repairs route-group, schedule, task,
|
|
383
|
+
"Apply low-risk doctor fixes, then re-check the app. Repairs route-group, schedule, task, outbox, and listener registration drift. Returns applied fixes and remaining diagnostics as JSON.",
|
|
384
384
|
inputSchema: {
|
|
385
385
|
strict: z
|
|
386
386
|
.boolean()
|