@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
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import type { Stats } from "node:fs";
3
3
  import { createReadStream } from "node:fs";
4
- import { lstat, readlink } from "node:fs/promises";
4
+ import { lstat, readlink, realpath } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { spawnCommand } from "./db.js";
7
7
 
@@ -58,17 +58,35 @@ export type GitChangeSet = {
58
58
  files: GitChangedFile[];
59
59
  };
60
60
 
61
+ /** One reproducible snapshot of the repository state around an app check. */
62
+ export type GitSourceSnapshot =
63
+ | {
64
+ kind: "git";
65
+ repositoryRoot: string;
66
+ comparison: GitChangeComparison;
67
+ repositoryChangeHash: string;
68
+ }
69
+ | {
70
+ kind: "unavailable";
71
+ reason: "not-a-git-worktree";
72
+ };
73
+
61
74
  export async function readGitChangeSet(options: {
62
75
  cwd: string;
63
76
  base?: string;
77
+ signal?: AbortSignal;
64
78
  }): Promise<GitChangeSet> {
65
79
  if (options.base !== undefined && options.base.length === 0) {
66
80
  throw new Error("Git base must not be empty.");
67
81
  }
68
82
  const repositoryRoot = path.resolve(
69
- await gitText(options.cwd, ["rev-parse", "--show-toplevel"]),
83
+ await gitText(
84
+ options.cwd,
85
+ ["rev-parse", "--show-toplevel"],
86
+ options.signal,
87
+ ),
70
88
  );
71
- const head = await resolveHeadCommit(repositoryRoot);
89
+ const head = await resolveHeadCommit(repositoryRoot, options.signal);
72
90
 
73
91
  let comparison: GitChangeComparison;
74
92
  let comparisonCommit: string;
@@ -78,21 +96,22 @@ export async function readGitChangeSet(options: {
78
96
  `Cannot compare --base ${JSON.stringify(options.base)} because this repository does not have a HEAD commit.`,
79
97
  );
80
98
  }
81
- const baseCommit = await gitText(repositoryRoot, [
82
- "rev-parse",
83
- "--verify",
84
- "--end-of-options",
85
- `${options.base}^{commit}`,
86
- ]).catch(() => {
99
+ const baseCommit = await gitText(
100
+ repositoryRoot,
101
+ ["rev-parse", "--verify", "--end-of-options", `${options.base}^{commit}`],
102
+ options.signal,
103
+ ).catch(() => {
104
+ options.signal?.throwIfAborted();
87
105
  throw new Error(
88
106
  `Could not resolve local Git base ${JSON.stringify(options.base)}. Fetch it before running Beignet; changed mapping never accesses the network.`,
89
107
  );
90
108
  });
91
- const mergeBase = await gitText(repositoryRoot, [
92
- "merge-base",
93
- baseCommit,
94
- head,
95
- ]).catch(() => {
109
+ const mergeBase = await gitText(
110
+ repositoryRoot,
111
+ ["merge-base", baseCommit, head],
112
+ options.signal,
113
+ ).catch(() => {
114
+ options.signal?.throwIfAborted();
96
115
  throw new Error(
97
116
  `Could not find a merge base between ${JSON.stringify(options.base)} and HEAD.`,
98
117
  );
@@ -106,7 +125,8 @@ export async function readGitChangeSet(options: {
106
125
  head,
107
126
  };
108
127
  } else {
109
- comparisonCommit = head ?? (await emptyTreeObjectId(repositoryRoot));
128
+ comparisonCommit =
129
+ head ?? (await emptyTreeObjectId(repositoryRoot, options.signal));
110
130
  comparison = {
111
131
  mode: "worktree",
112
132
  comparisonCommit,
@@ -116,22 +136,23 @@ export async function readGitChangeSet(options: {
116
136
 
117
137
  const committed =
118
138
  comparison.mode === "base"
119
- ? await gitDiffOutput(repositoryRoot, [
120
- comparison.mergeBase,
121
- comparison.head,
122
- ])
139
+ ? await gitDiffOutput(
140
+ repositoryRoot,
141
+ [comparison.mergeBase, comparison.head],
142
+ options.signal,
143
+ )
123
144
  : "";
124
- const staged = await gitDiffOutput(repositoryRoot, [
125
- "--cached",
126
- head ?? comparisonCommit,
127
- ]);
128
- const unstaged = await gitDiffOutput(repositoryRoot, []);
129
- const untracked = await gitOutput(repositoryRoot, [
130
- "ls-files",
131
- "--others",
132
- "--exclude-standard",
133
- "-z",
134
- ]);
145
+ const staged = await gitDiffOutput(
146
+ repositoryRoot,
147
+ ["--cached", head ?? comparisonCommit],
148
+ options.signal,
149
+ );
150
+ const unstaged = await gitDiffOutput(repositoryRoot, [], options.signal);
151
+ const untracked = await gitOutput(
152
+ repositoryRoot,
153
+ ["ls-files", "--others", "--exclude-standard", "-z"],
154
+ options.signal,
155
+ );
135
156
  const files = mergeChangedFiles(
136
157
  parseNameStatus(committed),
137
158
  parseNameStatus(staged),
@@ -142,12 +163,43 @@ export async function readGitChangeSet(options: {
142
163
  return { repositoryRoot, comparison, files };
143
164
  }
144
165
 
145
- async function emptyTreeObjectId(repositoryRoot: string): Promise<string> {
166
+ /** Capture Git-backed source state, or report that no worktree is available. */
167
+ export async function readGitSourceSnapshot(options: {
168
+ cwd: string;
169
+ signal?: AbortSignal;
170
+ }): Promise<GitSourceSnapshot> {
171
+ try {
172
+ const changeSet = await readGitChangeSet(options);
173
+ const repositoryChangeHash = await hashGitChanges(
174
+ changeSet,
175
+ changeSet.files,
176
+ options.signal,
177
+ );
178
+ options.signal?.throwIfAborted();
179
+ return {
180
+ kind: "git",
181
+ repositoryRoot: changeSet.repositoryRoot,
182
+ comparison: changeSet.comparison,
183
+ repositoryChangeHash,
184
+ };
185
+ } catch (error) {
186
+ options.signal?.throwIfAborted();
187
+ if (await hasGitMetadata(options.cwd, options.signal)) throw error;
188
+ options.signal?.throwIfAborted();
189
+ return { kind: "unavailable", reason: "not-a-git-worktree" };
190
+ }
191
+ }
192
+
193
+ async function emptyTreeObjectId(
194
+ repositoryRoot: string,
195
+ signal?: AbortSignal,
196
+ ): Promise<string> {
146
197
  const objectFormat =
147
- (await tryGitText(repositoryRoot, [
148
- "rev-parse",
149
- "--show-object-format=storage",
150
- ])) ?? "sha1";
198
+ (await tryGitText(
199
+ repositoryRoot,
200
+ ["rev-parse", "--show-object-format=storage"],
201
+ signal,
202
+ )) ?? "sha1";
151
203
  const objectId =
152
204
  emptyTreeObjectIds[objectFormat as keyof typeof emptyTreeObjectIds];
153
205
  if (!objectId) {
@@ -161,13 +213,18 @@ async function emptyTreeObjectId(repositoryRoot: string): Promise<string> {
161
213
  export async function hashGitChanges(
162
214
  changeSet: Pick<GitChangeSet, "comparison" | "files" | "repositoryRoot">,
163
215
  files = changeSet.files,
216
+ signal?: AbortSignal,
164
217
  ): Promise<string> {
165
218
  const hash = createHash("sha256");
166
- const indexEntries = await readGitIndexEntries(changeSet.repositoryRoot);
219
+ const indexEntries = await readGitIndexEntries(
220
+ changeSet.repositoryRoot,
221
+ signal,
222
+ );
167
223
  hash.update(JSON.stringify(changeSet.comparison));
168
224
  hash.update("\0");
169
225
 
170
226
  for (const file of [...files].sort(compareChangedFiles)) {
227
+ signal?.throwIfAborted();
171
228
  hash.update(
172
229
  JSON.stringify({
173
230
  status: file.status,
@@ -181,7 +238,12 @@ export async function hashGitChanges(
181
238
  hash.update("\0index\0");
182
239
  hash.update(indexEntries.get(relativePath) ?? "missing");
183
240
  hash.update("\0worktree\0");
184
- await hashCurrentPath(hash, changeSet.repositoryRoot, relativePath);
241
+ await hashCurrentPath(
242
+ hash,
243
+ changeSet.repositoryRoot,
244
+ relativePath,
245
+ signal,
246
+ );
185
247
  hash.update("\0");
186
248
  }
187
249
  }
@@ -191,8 +253,13 @@ export async function hashGitChanges(
191
253
 
192
254
  async function readGitIndexEntries(
193
255
  repositoryRoot: string,
256
+ signal?: AbortSignal,
194
257
  ): Promise<Map<string, string>> {
195
- const output = await gitOutput(repositoryRoot, ["ls-files", "--stage", "-z"]);
258
+ const output = await gitOutput(
259
+ repositoryRoot,
260
+ ["ls-files", "--stage", "-z"],
261
+ signal,
262
+ );
196
263
  const entries = new Map<string, string[]>();
197
264
  for (const field of output.split("\0").filter(Boolean)) {
198
265
  const separator = field.indexOf("\t");
@@ -214,39 +281,44 @@ async function hashCurrentPath(
214
281
  hash: ReturnType<typeof createHash>,
215
282
  repositoryRoot: string,
216
283
  relativePath: string,
284
+ signal?: AbortSignal,
217
285
  ): Promise<void> {
286
+ signal?.throwIfAborted();
218
287
  const absolutePath = path.join(repositoryRoot, relativePath);
219
288
  let stats: Stats;
220
289
  try {
221
290
  stats = await lstat(absolutePath);
222
291
  } catch (error) {
292
+ signal?.throwIfAborted();
223
293
  if ((error as NodeJS.ErrnoException).code === "ENOENT") {
224
294
  hash.update("missing");
225
295
  return;
226
296
  }
227
297
  throw error;
228
298
  }
299
+ signal?.throwIfAborted();
229
300
 
230
301
  if (stats.isSymbolicLink()) {
302
+ const target = await readlink(absolutePath);
303
+ signal?.throwIfAborted();
231
304
  hash.update("symlink\0");
232
305
  hash.update(String(stats.mode));
233
306
  hash.update("\0");
234
- hash.update(await readlink(absolutePath));
307
+ hash.update(target);
235
308
  return;
236
309
  }
237
310
  if (stats.isDirectory()) {
238
311
  hash.update("directory\0");
239
- const head = await tryGitText(absolutePath, [
240
- "rev-parse",
241
- "--verify",
242
- "HEAD^{commit}",
243
- ]);
244
- const status = await tryGitOutput(absolutePath, [
245
- "status",
246
- "--porcelain=v1",
247
- "-z",
248
- "--untracked-files=all",
249
- ]);
312
+ const head = await tryGitText(
313
+ absolutePath,
314
+ ["rev-parse", "--verify", "HEAD^{commit}"],
315
+ signal,
316
+ );
317
+ const status = await tryGitOutput(
318
+ absolutePath,
319
+ ["status", "--porcelain=v1", "-z", "--untracked-files=all"],
320
+ signal,
321
+ );
250
322
  hash.update(head ?? "unknown-head");
251
323
  hash.update("\0");
252
324
  hash.update(status ?? "unknown-status");
@@ -260,8 +332,47 @@ async function hashCurrentPath(
260
332
  hash.update("file\0");
261
333
  hash.update(String(stats.mode));
262
334
  hash.update("\0");
263
- for await (const chunk of createReadStream(absolutePath)) {
264
- hash.update(chunk as Buffer);
335
+ try {
336
+ for await (const chunk of createReadStream(absolutePath, { signal })) {
337
+ hash.update(chunk as Buffer);
338
+ }
339
+ } catch (error) {
340
+ signal?.throwIfAborted();
341
+ throw error;
342
+ }
343
+ signal?.throwIfAborted();
344
+ }
345
+
346
+ async function hasGitMetadata(
347
+ cwd: string,
348
+ signal?: AbortSignal,
349
+ ): Promise<boolean> {
350
+ signal?.throwIfAborted();
351
+ if (process.env.GIT_DIR || process.env.GIT_WORK_TREE) return true;
352
+ let root = path.resolve(cwd);
353
+ try {
354
+ root = await realpath(root);
355
+ } catch (error) {
356
+ signal?.throwIfAborted();
357
+ const code = (error as NodeJS.ErrnoException).code;
358
+ if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
359
+ }
360
+
361
+ let current = root;
362
+ while (true) {
363
+ signal?.throwIfAborted();
364
+ try {
365
+ await lstat(path.join(current, ".git"));
366
+ signal?.throwIfAborted();
367
+ return true;
368
+ } catch (error) {
369
+ signal?.throwIfAborted();
370
+ const code = (error as NodeJS.ErrnoException).code;
371
+ if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
372
+ }
373
+ const parent = path.dirname(current);
374
+ if (parent === current) return false;
375
+ current = parent;
265
376
  }
266
377
  }
267
378
 
@@ -397,36 +508,45 @@ function normalizeGitPath(file: string): string {
397
508
  return process.platform === "win32" ? file.replaceAll("\\", "/") : file;
398
509
  }
399
510
 
400
- async function gitText(cwd: string, args: readonly string[]): Promise<string> {
401
- return (await gitOutput(cwd, args)).trim();
511
+ async function gitText(
512
+ cwd: string,
513
+ args: readonly string[],
514
+ signal?: AbortSignal,
515
+ ): Promise<string> {
516
+ return stripFinalLineEnding(await gitOutput(cwd, args, signal));
517
+ }
518
+
519
+ function stripFinalLineEnding(output: string): string {
520
+ if (output.endsWith("\n")) return output.slice(0, -1);
521
+ return output;
402
522
  }
403
523
 
404
524
  async function resolveHeadCommit(
405
525
  repositoryRoot: string,
526
+ signal?: AbortSignal,
406
527
  ): Promise<string | undefined> {
407
- const head = await runGit(repositoryRoot, [
408
- "rev-parse",
409
- "--verify",
410
- "HEAD^{commit}",
411
- ]);
528
+ const head = await runGit(
529
+ repositoryRoot,
530
+ ["rev-parse", "--verify", "HEAD^{commit}"],
531
+ signal,
532
+ );
412
533
  if (head.exitCode === 0) return head.stdout.trim();
413
534
 
414
- const symbolicHead = await runGit(repositoryRoot, [
415
- "symbolic-ref",
416
- "--quiet",
417
- "HEAD",
418
- ]);
535
+ const symbolicHead = await runGit(
536
+ repositoryRoot,
537
+ ["symbolic-ref", "--quiet", "HEAD"],
538
+ signal,
539
+ );
419
540
  if (symbolicHead.exitCode !== 0) {
420
541
  throw gitCommandError(["rev-parse", "--verify", "HEAD^{commit}"], head);
421
542
  }
422
543
 
423
544
  const ref = symbolicHead.stdout.trim();
424
- const refExists = await runGit(repositoryRoot, [
425
- "show-ref",
426
- "--verify",
427
- "--quiet",
428
- ref,
429
- ]);
545
+ const refExists = await runGit(
546
+ repositoryRoot,
547
+ ["show-ref", "--verify", "--quiet", ref],
548
+ signal,
549
+ );
430
550
  if (refExists.exitCode === 1) return undefined;
431
551
  throw gitCommandError(["rev-parse", "--verify", "HEAD^{commit}"], head);
432
552
  }
@@ -434,10 +554,12 @@ async function resolveHeadCommit(
434
554
  async function tryGitText(
435
555
  cwd: string,
436
556
  args: readonly string[],
557
+ signal?: AbortSignal,
437
558
  ): Promise<string | undefined> {
438
559
  try {
439
- return await gitText(cwd, args);
560
+ return await gitText(cwd, args, signal);
440
561
  } catch {
562
+ signal?.throwIfAborted();
441
563
  return undefined;
442
564
  }
443
565
  }
@@ -445,10 +567,12 @@ async function tryGitText(
445
567
  async function tryGitOutput(
446
568
  cwd: string,
447
569
  args: readonly string[],
570
+ signal?: AbortSignal,
448
571
  ): Promise<string | undefined> {
449
572
  try {
450
- return await gitOutput(cwd, args);
573
+ return await gitOutput(cwd, args, signal);
451
574
  } catch {
575
+ signal?.throwIfAborted();
452
576
  return undefined;
453
577
  }
454
578
  }
@@ -456,8 +580,9 @@ async function tryGitOutput(
456
580
  async function gitOutput(
457
581
  cwd: string,
458
582
  args: readonly string[],
583
+ signal?: AbortSignal,
459
584
  ): Promise<string> {
460
- const result = await runGit(cwd, args);
585
+ const result = await runGit(cwd, args, signal);
461
586
  if (result.exitCode !== 0) throw gitCommandError(args, result);
462
587
  return result.stdout;
463
588
  }
@@ -465,14 +590,16 @@ async function gitOutput(
465
590
  async function runGit(
466
591
  cwd: string,
467
592
  args: readonly string[],
593
+ signal?: AbortSignal,
468
594
  ): Promise<GitCommandResult> {
469
595
  const result = await spawnCommand("git", args, cwd, {
470
596
  captureOutput: true,
471
597
  maxOutputBytes: maxGitOutputBytes,
598
+ signal,
472
599
  });
473
600
  if (result.outputTruncated) {
474
601
  throw new Error(
475
- `Git output exceeded ${maxGitOutputBytes} bytes; changed mapping cannot produce a complete report.`,
602
+ `Git output exceeded ${maxGitOutputBytes} bytes; source inspection cannot produce a complete report.`,
476
603
  );
477
604
  }
478
605
  return {
@@ -495,17 +622,22 @@ function gitCommandError(
495
622
  async function gitDiffOutput(
496
623
  repositoryRoot: string,
497
624
  comparisonArgs: readonly string[],
625
+ signal?: AbortSignal,
498
626
  ): Promise<string> {
499
- return gitOutput(repositoryRoot, [
500
- "diff",
501
- "--no-ext-diff",
502
- "--no-textconv",
503
- "--name-status",
504
- "-z",
505
- "--find-renames=50%",
506
- "--find-copies=50%",
507
- "--find-copies-harder",
508
- ...comparisonArgs,
509
- "--",
510
- ]);
627
+ return gitOutput(
628
+ repositoryRoot,
629
+ [
630
+ "diff",
631
+ "--no-ext-diff",
632
+ "--no-textconv",
633
+ "--name-status",
634
+ "-z",
635
+ "--find-renames=50%",
636
+ "--find-copies=50%",
637
+ "--find-copies-harder",
638
+ ...comparisonArgs,
639
+ "--",
640
+ ],
641
+ signal,
642
+ );
511
643
  }
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  type CommandContext,
9
9
  type FlagParametersForType,
10
10
  proposeCompletions,
11
+ type RouteMap,
11
12
  run,
12
13
  type StricliDynamicCommandContext,
13
14
  type StricliProcess,
@@ -180,6 +181,7 @@ type OutboxDrainFlags = {
180
181
  json?: boolean;
181
182
  module?: string;
182
183
  batchSize?: number;
184
+ concurrency?: number;
183
185
  cwd?: string;
184
186
  };
185
187
 
@@ -486,7 +488,13 @@ const outboxDrainFlagParameters = {
486
488
  kind: "parsed",
487
489
  parse: parsePositiveInteger,
488
490
  optional: true,
489
- brief: "Maximum messages to claim in one drain pass.",
491
+ brief: "Maximum eligible messages to handle in one drain pass.",
492
+ },
493
+ concurrency: {
494
+ kind: "parsed",
495
+ parse: parsePositiveInteger,
496
+ optional: true,
497
+ brief: "Maximum concurrent deliveries; values above one are unordered.",
490
498
  },
491
499
  cwd: cwdFlag,
492
500
  } satisfies FlagParametersForType<OutboxDrainFlags, CliContext>;
@@ -1156,7 +1164,7 @@ const checkCommand = buildCommand<CheckFlags, [], CliContext>({
1156
1164
  docs: {
1157
1165
  brief: "Run the full app validation loop as one command.",
1158
1166
  fullDescription:
1159
- "Runs beignet lint, beignet doctor --strict, optional disconnected or connected preflight, 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.",
1167
+ "Runs beignet lint, beignet doctor --strict, optional disconnected or connected preflight, 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. Inside Git, the command fingerprints source before and after validation and fails stale results whose ending fingerprint differs from the baseline.",
1160
1168
  },
1161
1169
  parameters: {
1162
1170
  flags: {
@@ -1186,9 +1194,12 @@ const checkCommand = buildCommand<CheckFlags, [], CliContext>({
1186
1194
  } satisfies FlagParametersForType<CheckFlags, CliContext>,
1187
1195
  },
1188
1196
  loader: async () => {
1189
- const { checkApp, formatCheckStep, formatCheckSummary } = await import(
1190
- "./check.js"
1191
- );
1197
+ const {
1198
+ checkApp,
1199
+ formatCheckProvenance,
1200
+ formatCheckStep,
1201
+ formatCheckSummary,
1202
+ } = await import("./check.js");
1192
1203
 
1193
1204
  return async function runCheck(this: CliContext, flags: CheckFlags) {
1194
1205
  const color = useColor();
@@ -1210,7 +1221,7 @@ const checkCommand = buildCommand<CheckFlags, [], CliContext>({
1210
1221
  this,
1211
1222
  flags.json
1212
1223
  ? JSON.stringify(result, null, 2)
1213
- : `\n${formatCheckSummary(result, { color })}`,
1224
+ : `\n${formatCheckProvenance(result, { color })}\n${formatCheckSummary(result, { color })}`,
1214
1225
  );
1215
1226
  if (!result.ok) {
1216
1227
  this.process.exitCode = 1;
@@ -1597,6 +1608,7 @@ const outboxDrainCommand = buildCommand<OutboxDrainFlags, [], CliContext>({
1597
1608
  cwd: flags.cwd,
1598
1609
  modulePath: flags.module,
1599
1610
  batchSize: flags.batchSize,
1611
+ concurrency: flags.concurrency,
1600
1612
  });
1601
1613
 
1602
1614
  writeOutput(
@@ -1605,6 +1617,9 @@ const outboxDrainCommand = buildCommand<OutboxDrainFlags, [], CliContext>({
1605
1617
  ? JSON.stringify(result, null, 2)
1606
1618
  : outboxDrainNextSteps(result),
1607
1619
  );
1620
+ if (result.result.settlementFailed > 0 || result.result.leaseLost > 0) {
1621
+ this.process.exitCode = 1;
1622
+ }
1608
1623
  };
1609
1624
  },
1610
1625
  });
@@ -2448,6 +2463,27 @@ Run npm create beignet@latest (or bun create beignet) to scaffold a new app.`,
2448
2463
  },
2449
2464
  });
2450
2465
 
2466
+ const completionProposalCommandPath = ["completion", "propose"] as const;
2467
+
2468
+ function commandPathsFromRouteMap(
2469
+ routeMap: RouteMap<CliContext>,
2470
+ prefix: readonly string[] = [],
2471
+ ): string[][] {
2472
+ return routeMap.getAllEntries().flatMap((entry) => {
2473
+ const path = [...prefix, entry.name["convert-camel-to-kebab"]];
2474
+ if ("getAllEntries" in entry.target) {
2475
+ return commandPathsFromRouteMap(entry.target, path);
2476
+ }
2477
+ return [path];
2478
+ });
2479
+ }
2480
+
2481
+ /** Every canonical CLI command path, derived from runtime routing definitions. */
2482
+ export const cliCommandPaths: readonly (readonly string[])[] = [
2483
+ ...commandPathsFromRouteMap(rootRoutes),
2484
+ completionProposalCommandPath,
2485
+ ].sort((left, right) => left.join(" ").localeCompare(right.join(" ")));
2486
+
2451
2487
  const cli = buildApplication(rootRoutes, {
2452
2488
  name: "beignet",
2453
2489
  versionInfo: {
@@ -2541,6 +2577,9 @@ type OutboxDrainNextStepsResult = {
2541
2577
  delivered: number;
2542
2578
  retried: number;
2543
2579
  deadLettered: number;
2580
+ abandonedDeadLettered: number;
2581
+ settlementFailed: number;
2582
+ leaseLost: number;
2544
2583
  };
2545
2584
  };
2546
2585
 
@@ -2608,7 +2647,10 @@ Result:
2608
2647
  claimed: ${result.result.claimed}
2609
2648
  delivered: ${result.result.delivered}
2610
2649
  retried: ${result.result.retried}
2611
- deadLettered: ${result.result.deadLettered}`;
2650
+ deadLettered: ${result.result.deadLettered}
2651
+ abandonedDeadLettered: ${result.result.abandonedDeadLettered}
2652
+ settlementFailed: ${result.result.settlementFailed}
2653
+ leaseLost: ${result.result.leaseLost}`;
2612
2654
  }
2613
2655
 
2614
2656
  function formatOutboxDate(value: Date | string | null | undefined): string {
@@ -3053,7 +3095,10 @@ export async function main(
3053
3095
  // Shell completion scripts call `beignet completion propose <words...>`.
3054
3096
  // Handle it before stricli parses the inputs: the trailing words are a
3055
3097
  // partial command line, not flags for this CLI.
3056
- if (inputs[0] === "completion" && inputs[1] === "propose") {
3098
+ if (
3099
+ inputs[0] === completionProposalCommandPath[0] &&
3100
+ inputs[1] === completionProposalCommandPath[1]
3101
+ ) {
3057
3102
  await writeCompletionProposals(inputs.slice(2), context);
3058
3103
  return;
3059
3104
  }