@beignet/cli 0.0.50 → 0.0.52

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 (105) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +64 -22
  3. package/dist/analysis/layers.d.ts.map +1 -1
  4. package/dist/analysis/layers.js +4 -2
  5. package/dist/analysis/layers.js.map +1 -1
  6. package/dist/analysis/port-wiring.d.ts.map +1 -1
  7. package/dist/analysis/port-wiring.js +2 -1
  8. package/dist/analysis/port-wiring.js.map +1 -1
  9. package/dist/analysis/source-files.d.ts +15 -0
  10. package/dist/analysis/source-files.d.ts.map +1 -0
  11. package/dist/analysis/source-files.js +52 -0
  12. package/dist/analysis/source-files.js.map +1 -0
  13. package/dist/analysis/workspace.d.ts.map +1 -1
  14. package/dist/analysis/workspace.js +21 -25
  15. package/dist/analysis/workspace.js.map +1 -1
  16. package/dist/app-map-changes.d.ts.map +1 -1
  17. package/dist/app-map-changes.js +5 -7
  18. package/dist/app-map-changes.js.map +1 -1
  19. package/dist/app-map.d.ts.map +1 -1
  20. package/dist/app-map.js +33 -30
  21. package/dist/app-map.js.map +1 -1
  22. package/dist/check.d.ts +12 -0
  23. package/dist/check.d.ts.map +1 -1
  24. package/dist/check.js +92 -25
  25. package/dist/check.js.map +1 -1
  26. package/dist/git-changes.d.ts +17 -1
  27. package/dist/git-changes.d.ts.map +1 -1
  28. package/dist/git-changes.js +118 -84
  29. package/dist/git-changes.js.map +1 -1
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +35 -6
  33. package/dist/index.js.map +1 -1
  34. package/dist/inspect.d.ts.map +1 -1
  35. package/dist/inspect.js +436 -112
  36. package/dist/inspect.js.map +1 -1
  37. package/dist/lint.d.ts.map +1 -1
  38. package/dist/lint.js +3 -6
  39. package/dist/lint.js.map +1 -1
  40. package/dist/make.d.ts.map +1 -1
  41. package/dist/make.js +0 -2
  42. package/dist/make.js.map +1 -1
  43. package/dist/mcp.d.ts.map +1 -1
  44. package/dist/mcp.js +23 -5
  45. package/dist/mcp.js.map +1 -1
  46. package/dist/operational-process.d.ts +1 -0
  47. package/dist/operational-process.d.ts.map +1 -1
  48. package/dist/operational-process.js.map +1 -1
  49. package/dist/operational-runner.js +1 -0
  50. package/dist/operational-runner.js.map +1 -1
  51. package/dist/outbox.d.ts +1 -0
  52. package/dist/outbox.d.ts.map +1 -1
  53. package/dist/outbox.js +58 -12
  54. package/dist/outbox.js.map +1 -1
  55. package/dist/templates/agents.d.ts.map +1 -1
  56. package/dist/templates/agents.js +43 -15
  57. package/dist/templates/agents.js.map +1 -1
  58. package/dist/templates/base.js +5 -5
  59. package/dist/templates/db/mysql.d.ts.map +1 -1
  60. package/dist/templates/db/mysql.js +51 -22
  61. package/dist/templates/db/mysql.js.map +1 -1
  62. package/dist/templates/db/postgres.d.ts.map +1 -1
  63. package/dist/templates/db/postgres.js +60 -20
  64. package/dist/templates/db/postgres.js.map +1 -1
  65. package/dist/templates/db/sqlite.d.ts.map +1 -1
  66. package/dist/templates/db/sqlite.js +51 -23
  67. package/dist/templates/db/sqlite.js.map +1 -1
  68. package/dist/templates/shadcn.js +1 -1
  69. package/dist/templates/shadcn.js.map +1 -1
  70. package/dist/templates/shared.js +4 -4
  71. package/dist/templates/shared.js.map +1 -1
  72. package/dist/templates/testing.d.ts.map +1 -1
  73. package/dist/templates/testing.js +7 -19
  74. package/dist/templates/testing.js.map +1 -1
  75. package/dist/test-discovery.d.ts +9 -0
  76. package/dist/test-discovery.d.ts.map +1 -0
  77. package/dist/test-discovery.js +40 -0
  78. package/dist/test-discovery.js.map +1 -0
  79. package/package.json +7 -5
  80. package/skills/app-structure/SKILL.md +37 -10
  81. package/src/analysis/layers.ts +4 -2
  82. package/src/analysis/port-wiring.ts +2 -1
  83. package/src/analysis/source-files.ts +61 -0
  84. package/src/analysis/workspace.ts +27 -31
  85. package/src/app-map-changes.ts +10 -8
  86. package/src/app-map.ts +43 -32
  87. package/src/check.ts +115 -25
  88. package/src/git-changes.ts +218 -86
  89. package/src/index.ts +53 -8
  90. package/src/inspect.ts +627 -146
  91. package/src/lint.ts +6 -5
  92. package/src/make.ts +0 -2
  93. package/src/mcp.ts +34 -11
  94. package/src/operational-process.ts +1 -0
  95. package/src/operational-runner.ts +1 -0
  96. package/src/outbox.ts +63 -12
  97. package/src/templates/agents.ts +43 -15
  98. package/src/templates/base.ts +5 -5
  99. package/src/templates/db/mysql.ts +51 -22
  100. package/src/templates/db/postgres.ts +60 -20
  101. package/src/templates/db/sqlite.ts +51 -23
  102. package/src/templates/shadcn.ts +1 -1
  103. package/src/templates/shared.ts +4 -4
  104. package/src/templates/testing.ts +12 -19
  105. package/src/test-discovery.ts +53 -0
package/src/app-map.ts CHANGED
@@ -6,6 +6,10 @@ import {
6
6
  isImportedContractDeclaration,
7
7
  } from "./analysis/contract-factories.js";
8
8
  import { analyzePortWiring } from "./analysis/port-wiring.js";
9
+ import {
10
+ isAnalysisSourceFile,
11
+ stripSourceFileExtension,
12
+ } from "./analysis/source-files.js";
9
13
  import {
10
14
  createSourceIndex,
11
15
  exportedVariableDeclarations,
@@ -29,6 +33,11 @@ import {
29
33
  } from "./inspect.js";
30
34
  import { type LintDiagnostic, lintApp } from "./lint.js";
31
35
  import { auditProviders } from "./provider-audit.js";
36
+ import {
37
+ appTestRoots,
38
+ isAppTestFile,
39
+ isWithinAppTestRoots,
40
+ } from "./test-discovery.js";
32
41
 
33
42
  export type { AppMapNodeKind } from "./app-map-schema.js";
34
43
  export { appMapNodeKinds } from "./app-map-schema.js";
@@ -208,6 +217,7 @@ export async function mapApp(
208
217
  const registries: RegistryRecord[] = [];
209
218
  const featuresPath = directoryPath(workspace.config.paths.features);
210
219
  const featureNames = discoverFeatureNames(workspace, featuresPath);
220
+ const testRoots = appTestRoots(workspace.config.paths);
211
221
 
212
222
  const addNode = (node: AppMapNode) => {
213
223
  const existing = nodes.get(node.id);
@@ -241,17 +251,23 @@ export async function mapApp(
241
251
  status: "declared",
242
252
  details: roles,
243
253
  });
254
+ }
244
255
 
245
- for (const file of featureFiles.filter(isTestFile)) {
246
- const id = `test:${file}`;
247
- addNode({
248
- id,
249
- kind: "test",
250
- name: path.posix.basename(file),
251
- feature,
252
- source: { file },
253
- status: "declared",
254
- });
256
+ for (const file of workspace.files.filter(
257
+ (candidate) =>
258
+ isAppTestFile(candidate) && isWithinAppTestRoots(candidate, testRoots),
259
+ )) {
260
+ const id = `test:${file}`;
261
+ const feature = featureForFile(file, featuresPath);
262
+ addNode({
263
+ id,
264
+ kind: "test",
265
+ name: path.posix.basename(file),
266
+ ...(feature ? { feature } : {}),
267
+ source: { file },
268
+ status: "declared",
269
+ });
270
+ if (feature) {
255
271
  addEdge(exactEdge("contains-test", featureId(feature), id, file));
256
272
  }
257
273
  }
@@ -345,7 +361,7 @@ export async function mapApp(
345
361
  details: { type: "server" },
346
362
  });
347
363
  }
348
- const sourceFiles = workspace.files.filter(isTypeScriptSource);
364
+ const sourceFiles = workspace.files.filter(isAnalysisSourceFile);
349
365
  for (const file of sourceFiles) {
350
366
  const sourceFile = await workspace.readSourceFile(file);
351
367
  for (const { exportName, declaration } of exportedVariableDeclarations(
@@ -918,7 +934,8 @@ async function addRouteGroups(args: {
918
934
  const routeFiles = args.workspace.files.filter(
919
935
  (file) =>
920
936
  featureForFile(file, args.featuresPath) !== undefined &&
921
- file.endsWith("/routes.ts"),
937
+ isAnalysisSourceFile(file) &&
938
+ stripSourceFileExtension(file).endsWith("/routes"),
922
939
  );
923
940
 
924
941
  for (const file of routeFiles) {
@@ -1192,8 +1209,8 @@ async function addOutboxDeliveryGraph(args: {
1192
1209
  ]);
1193
1210
  for (const file of args.workspace.files) {
1194
1211
  if (
1195
- !isTypeScriptSource(file) ||
1196
- isTestFile(file) ||
1212
+ !isAnalysisSourceFile(file) ||
1213
+ isAppTestFile(file) ||
1197
1214
  ![...runtimeDirectories].some(
1198
1215
  (directory) =>
1199
1216
  directory === "." ||
@@ -1276,9 +1293,10 @@ function isProviderLifecycleSource(
1276
1293
  file: string,
1277
1294
  sourceFile: ts.SourceFile,
1278
1295
  ): boolean {
1296
+ const sourcePath = stripSourceFileExtension(file);
1279
1297
  if (
1280
- /(^|\/)provider(s)?\.ts$/.test(file) ||
1281
- /(^|\/)[^/]*provider[^/]*\.ts$/.test(file) ||
1298
+ /(^|\/)provider(s)?$/.test(sourcePath) ||
1299
+ /(^|\/)[^/]*provider[^/]*$/.test(sourcePath) ||
1282
1300
  /(^|\/)providers?\//.test(file)
1283
1301
  ) {
1284
1302
  return true;
@@ -1390,7 +1408,7 @@ async function addTables(args: {
1390
1408
  const schemaPrefix = `${infraDir}/db/schema/`;
1391
1409
  const files = args.workspace.files.filter(
1392
1410
  (file) =>
1393
- isTypeScriptSource(file) &&
1411
+ isAnalysisSourceFile(file) &&
1394
1412
  (file.startsWith(schemaPrefix) ||
1395
1413
  schemaSources.some((source) =>
1396
1414
  file.startsWith(staticPathPrefix(source)),
@@ -1447,7 +1465,7 @@ async function addFeatureDependencies(args: {
1447
1465
  }
1448
1466
  >();
1449
1467
 
1450
- for (const file of args.workspace.files.filter(isTypeScriptSource)) {
1468
+ for (const file of args.workspace.files.filter(isAnalysisSourceFile)) {
1451
1469
  const from = featureForFile(file, args.featuresPath);
1452
1470
  if (!from || !args.featureNames.has(from)) continue;
1453
1471
  for (const reference of await localImportReferences(args.workspace, file)) {
@@ -1496,7 +1514,7 @@ async function addUnresolvedLocalImports(args: {
1496
1514
  workspace: AnalysisWorkspace;
1497
1515
  unresolved: AppMapUnresolvedReference[];
1498
1516
  }): Promise<void> {
1499
- for (const file of args.workspace.files.filter(isTypeScriptSource)) {
1517
+ for (const file of args.workspace.files.filter(isAnalysisSourceFile)) {
1500
1518
  for (const reference of await localImportReferences(args.workspace, file)) {
1501
1519
  if (reference.resolvedFile) continue;
1502
1520
  args.unresolved.push({
@@ -1556,15 +1574,16 @@ function appMapContractSourceFiles(workspace: AnalysisWorkspace): string[] {
1556
1574
  contractsPath === directoryPath(workspace.config.paths.features);
1557
1575
 
1558
1576
  return workspace.files.filter((file) => {
1559
- if (!file.endsWith(".ts") || file.endsWith(".test.ts")) return false;
1577
+ if (!isAnalysisSourceFile(file) || isAppTestFile(file)) return false;
1578
+ const sourcePath = stripSourceFileExtension(file);
1560
1579
  if (featureOwned) {
1561
1580
  const prefix = `${contractsPath}/`;
1562
1581
  if (!file.startsWith(prefix)) return false;
1563
- const parts = file.slice(prefix.length).split("/");
1564
- return parts.length === 2 && parts[1] === "contracts.ts";
1582
+ const parts = sourcePath.slice(prefix.length).split("/");
1583
+ return parts.length === 2 && parts[1] === "contracts";
1565
1584
  }
1566
1585
  return (
1567
- file === `${contractsPath}.ts` || file.startsWith(`${contractsPath}/`)
1586
+ sourcePath === contractsPath || sourcePath.startsWith(`${contractsPath}/`)
1568
1587
  );
1569
1588
  });
1570
1589
  }
@@ -1824,7 +1843,7 @@ function summarizeFeatureFiles(files: string[]): Record<string, number> {
1824
1843
  client: files.filter((file) => file.includes("/client/")).length,
1825
1844
  components: files.filter((file) => file.includes("/components/")).length,
1826
1845
  domain: files.filter((file) => file.includes("/domain/")).length,
1827
- tests: files.filter(isTestFile).length,
1846
+ tests: files.filter(isAppTestFile).length,
1828
1847
  };
1829
1848
  }
1830
1849
 
@@ -2017,14 +2036,6 @@ function sourceNodeEdge(
2017
2036
  };
2018
2037
  }
2019
2038
 
2020
- function isTestFile(file: string): boolean {
2021
- return /\.(test|spec)\.(ts|tsx|mts|cts)$/.test(file);
2022
- }
2023
-
2024
- function isTypeScriptSource(file: string): boolean {
2025
- return /\.(ts|tsx|mts|cts)$/.test(file) && !file.endsWith(".d.ts");
2026
- }
2027
-
2028
2039
  function staticPathPrefix(pattern: string): string {
2029
2040
  const wildcard = pattern.search(/[?*[{]/);
2030
2041
  return wildcard === -1 ? pattern : pattern.slice(0, wildcard);
package/src/check.ts CHANGED
@@ -1,7 +1,11 @@
1
- import { readdir, readFile, stat } from "node:fs/promises";
1
+ import { readdir, readFile, realpath, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { createPainter } from "./ansi.js";
4
4
  import { detectPackageManager, spawnCommand } from "./db.js";
5
+ import {
6
+ type GitSourceSnapshot,
7
+ readGitSourceSnapshot,
8
+ } from "./git-changes.js";
5
9
  import {
6
10
  applyDoctorFixes,
7
11
  formatDoctor,
@@ -33,6 +37,14 @@ export type CheckAppResult = {
33
37
  ok: boolean;
34
38
  fixes: InspectFix[];
35
39
  steps: CheckStep[];
40
+ provenance: CheckProvenance;
41
+ };
42
+
43
+ /** Source-state evidence captured around one complete check run. */
44
+ export type CheckProvenance = {
45
+ status: "current" | "stale" | "unavailable";
46
+ start: GitSourceSnapshot;
47
+ end: GitSourceSnapshot;
36
48
  };
37
49
 
38
50
  export type CheckAppOptions = {
@@ -74,19 +86,34 @@ const defaultMaxOutputBytes = 64 * 1024;
74
86
  export async function checkApp(
75
87
  options: CheckAppOptions = {},
76
88
  ): Promise<CheckAppResult> {
77
- const targetDir = path.resolve(options.cwd ?? process.cwd());
78
- const scripts = await readAppPackageScripts(targetDir);
79
- const runner = await detectPackageManager(targetDir);
89
+ options.signal?.throwIfAborted();
90
+ const targetDir = await resolveCheckTargetDir(options.cwd ?? process.cwd());
91
+ options.signal?.throwIfAborted();
92
+ // Validate the app root before fixes may write; reload execution metadata
93
+ // after the provenance baseline so the selected scripts are covered by it.
94
+ await readAppPackageScripts(targetDir);
95
+ options.signal?.throwIfAborted();
96
+ const fixes = options.fix
97
+ ? await applyDoctorFixes({ cwd: targetDir, strict: true })
98
+ : [];
99
+ options.signal?.throwIfAborted();
80
100
  const steps: CheckStep[] = [];
81
101
  const record = (step: CheckStep): void => {
82
102
  steps.push(step);
83
103
  options.onStep?.(step);
84
104
  };
85
105
 
106
+ const start = await readGitSourceSnapshot({
107
+ cwd: targetDir,
108
+ signal: options.signal,
109
+ });
110
+ options.signal?.throwIfAborted();
111
+ const scripts = await readAppPackageScripts(targetDir);
112
+ const runner = await detectPackageManager(targetDir);
86
113
  options.signal?.throwIfAborted();
87
114
  record(await lintStep(targetDir, options));
88
115
  options.signal?.throwIfAborted();
89
- const { step: doctorStep, fixes } = await runDoctorStep(targetDir, options);
116
+ const doctorStep = await runDoctorStep(targetDir, options, fixes);
90
117
  record(doctorStep);
91
118
 
92
119
  if (options.preflight || options.preflightConnect) {
@@ -105,16 +132,38 @@ export async function checkApp(
105
132
  record(await scriptStep(targetDir, runner, script, scripts, options));
106
133
  }
107
134
 
135
+ options.signal?.throwIfAborted();
136
+ const end = await readGitSourceSnapshot({
137
+ cwd: targetDir,
138
+ signal: options.signal,
139
+ });
140
+ options.signal?.throwIfAborted();
141
+ const provenance = checkProvenance(start, end);
142
+
108
143
  return {
109
144
  schemaVersion: 1,
110
145
  targetDir,
111
146
  runner,
112
- ok: steps.every((step) => step.status !== "failed"),
147
+ ok:
148
+ steps.every((step) => step.status !== "failed") &&
149
+ provenance.status !== "stale",
113
150
  fixes,
114
151
  steps,
152
+ provenance,
115
153
  };
116
154
  }
117
155
 
156
+ async function resolveCheckTargetDir(cwd: string): Promise<string> {
157
+ const targetDir = path.resolve(cwd);
158
+ try {
159
+ return await realpath(targetDir);
160
+ } catch (error) {
161
+ const code = (error as NodeJS.ErrnoException).code;
162
+ if (code === "ENOENT" || code === "ENOTDIR") return targetDir;
163
+ throw error;
164
+ }
165
+ }
166
+
118
167
  /**
119
168
  * Format a completed step as a single status line, with the failing step's
120
169
  * output indented underneath.
@@ -161,9 +210,35 @@ export function formatCheckSummary(
161
210
  const parts = [`${counts.passed} passed`];
162
211
  if (counts.failed > 0) parts.push(paint(`${counts.failed} failed`, "red"));
163
212
  if (counts.skipped > 0) parts.push(`${counts.skipped} skipped`);
213
+ if (result.provenance.status === "stale") {
214
+ parts.push(paint("source state stale", "red"));
215
+ }
164
216
  return parts.join(", ");
165
217
  }
166
218
 
219
+ /** Format the source state captured before and after validation. */
220
+ export function formatCheckProvenance(
221
+ result: CheckAppResult,
222
+ options: { color?: boolean } = {},
223
+ ): string {
224
+ const paint = createPainter(options.color);
225
+ const { provenance } = result;
226
+ if (provenance.status === "unavailable") {
227
+ return `${paint("-", "cyan")} Source provenance unavailable (not a Git worktree)`;
228
+ }
229
+ if (provenance.status === "stale") {
230
+ return `${paint("✗", "red")} Source state changed while checks were running; rerun beignet check`;
231
+ }
232
+ const snapshot = provenance.end;
233
+ if (snapshot.kind !== "git") {
234
+ throw new Error("Current check provenance must contain a Git snapshot.");
235
+ }
236
+ const head = snapshot.comparison.head
237
+ ? `HEAD ${snapshot.comparison.head.slice(0, 8)}`
238
+ : "unborn Git repository";
239
+ return `${paint("✓", "green")} Source state current (${head}, ${shortHash(snapshot.repositoryChangeHash)})`;
240
+ }
241
+
167
242
  /**
168
243
  * Format a full result at once (JSON-free callers and tests).
169
244
  */
@@ -172,6 +247,7 @@ export function formatCheck(
172
247
  options: { color?: boolean } = {},
173
248
  ): string {
174
249
  const lines = result.steps.map((step) => formatCheckStep(step, options));
250
+ lines.push(formatCheckProvenance(result, options));
175
251
  return `${lines.join("\n")}\n\n${formatCheckSummary(result, options)}`;
176
252
  }
177
253
 
@@ -195,11 +271,9 @@ async function lintStep(
195
271
  async function runDoctorStep(
196
272
  targetDir: string,
197
273
  options: CheckAppOptions,
198
- ): Promise<{ step: CheckStep; fixes: InspectFix[] }> {
274
+ fixes: InspectFix[],
275
+ ): Promise<CheckStep> {
199
276
  const startedAt = Date.now();
200
- const fixes = options.fix
201
- ? await applyDoctorFixes({ cwd: targetDir, strict: true })
202
- : [];
203
277
  const result = await inspectApp({ cwd: targetDir, strict: true });
204
278
  result.fixes = fixes;
205
279
  const failed = result.diagnostics.some(
@@ -208,24 +282,40 @@ async function runDoctorStep(
208
282
  );
209
283
 
210
284
  return {
211
- step: {
212
- name: "beignet doctor --strict",
213
- command: "beignet doctor --strict",
214
- status: failed ? "failed" : "passed",
215
- ...(fixes.length > 0
216
- ? {
217
- detail: `${fixes.length} ${fixes.length === 1 ? "fix" : "fixes"} applied`,
218
- }
219
- : {}),
220
- ...(failed
221
- ? { output: formatDoctor(result, { color: options.color }) }
222
- : {}),
223
- durationMs: Date.now() - startedAt,
224
- },
225
- fixes,
285
+ name: "beignet doctor --strict",
286
+ command: "beignet doctor --strict",
287
+ status: failed ? "failed" : "passed",
288
+ ...(fixes.length > 0
289
+ ? {
290
+ detail: `${fixes.length} ${fixes.length === 1 ? "fix" : "fixes"} applied`,
291
+ }
292
+ : {}),
293
+ ...(failed
294
+ ? { output: formatDoctor(result, { color: options.color }) }
295
+ : {}),
296
+ durationMs: Date.now() - startedAt,
226
297
  };
227
298
  }
228
299
 
300
+ function checkProvenance(
301
+ start: GitSourceSnapshot,
302
+ end: GitSourceSnapshot,
303
+ ): CheckProvenance {
304
+ if (start.kind === "unavailable" && end.kind === "unavailable") {
305
+ return { status: "unavailable", start, end };
306
+ }
307
+ const current =
308
+ start.kind === "git" &&
309
+ end.kind === "git" &&
310
+ start.repositoryRoot === end.repositoryRoot &&
311
+ start.repositoryChangeHash === end.repositoryChangeHash;
312
+ return { status: current ? "current" : "stale", start, end };
313
+ }
314
+
315
+ function shortHash(hash: string): string {
316
+ return `${hash.slice(0, 15)}…`;
317
+ }
318
+
229
319
  async function scriptStep(
230
320
  targetDir: string,
231
321
  runner: string,