@beignet/cli 0.0.39 → 0.0.41

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 (112) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +110 -16
  3. package/dist/analysis/source-index.d.ts +38 -0
  4. package/dist/analysis/source-index.d.ts.map +1 -0
  5. package/dist/analysis/source-index.js +271 -0
  6. package/dist/analysis/source-index.js.map +1 -0
  7. package/dist/analysis/workspace.d.ts +16 -0
  8. package/dist/analysis/workspace.d.ts.map +1 -0
  9. package/dist/analysis/workspace.js +134 -0
  10. package/dist/analysis/workspace.js.map +1 -0
  11. package/dist/app-map-schema.d.ts +5 -0
  12. package/dist/app-map-schema.d.ts.map +1 -0
  13. package/dist/app-map-schema.js +26 -0
  14. package/dist/app-map-schema.js.map +1 -0
  15. package/dist/app-map.d.ts +96 -0
  16. package/dist/app-map.d.ts.map +1 -0
  17. package/dist/app-map.js +1141 -0
  18. package/dist/app-map.js.map +1 -0
  19. package/dist/check.d.ts +7 -0
  20. package/dist/check.d.ts.map +1 -1
  21. package/dist/check.js +20 -5
  22. package/dist/check.js.map +1 -1
  23. package/dist/config.d.ts +1 -1
  24. package/dist/config.d.ts.map +1 -1
  25. package/dist/config.js +4 -2
  26. package/dist/config.js.map +1 -1
  27. package/dist/db.d.ts +32 -7
  28. package/dist/db.d.ts.map +1 -1
  29. package/dist/db.js +182 -14
  30. package/dist/db.js.map +1 -1
  31. package/dist/explain.d.ts +98 -0
  32. package/dist/explain.d.ts.map +1 -0
  33. package/dist/explain.js +512 -0
  34. package/dist/explain.js.map +1 -0
  35. package/dist/framework.d.ts +3 -0
  36. package/dist/framework.d.ts.map +1 -0
  37. package/dist/framework.js +6 -0
  38. package/dist/framework.js.map +1 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +79 -2
  41. package/dist/index.js.map +1 -1
  42. package/dist/inspect.d.ts +4 -2
  43. package/dist/inspect.d.ts.map +1 -1
  44. package/dist/inspect.js +173 -36
  45. package/dist/inspect.js.map +1 -1
  46. package/dist/lib.d.ts +4 -0
  47. package/dist/lib.d.ts.map +1 -1
  48. package/dist/lib.js +2 -0
  49. package/dist/lib.js.map +1 -1
  50. package/dist/make/payments.d.ts.map +1 -1
  51. package/dist/make/payments.js +2 -0
  52. package/dist/make/payments.js.map +1 -1
  53. package/dist/make.d.ts.map +1 -1
  54. package/dist/make.js +157 -3
  55. package/dist/make.js.map +1 -1
  56. package/dist/mcp.d.ts +1 -1
  57. package/dist/mcp.d.ts.map +1 -1
  58. package/dist/mcp.js +69 -1
  59. package/dist/mcp.js.map +1 -1
  60. package/dist/preflight.d.ts.map +1 -1
  61. package/dist/preflight.js +10 -0
  62. package/dist/preflight.js.map +1 -1
  63. package/dist/provider-add.d.ts.map +1 -1
  64. package/dist/provider-add.js +167 -13
  65. package/dist/provider-add.js.map +1 -1
  66. package/dist/provider-audit.d.ts +6 -0
  67. package/dist/provider-audit.d.ts.map +1 -1
  68. package/dist/provider-audit.js +63 -15
  69. package/dist/provider-audit.js.map +1 -1
  70. package/dist/templates/agents.d.ts.map +1 -1
  71. package/dist/templates/agents.js +17 -4
  72. package/dist/templates/agents.js.map +1 -1
  73. package/dist/templates/base.d.ts.map +1 -1
  74. package/dist/templates/base.js +10 -4
  75. package/dist/templates/base.js.map +1 -1
  76. package/dist/templates/index.d.ts +1 -1
  77. package/dist/templates/index.d.ts.map +1 -1
  78. package/dist/templates/index.js +8 -1
  79. package/dist/templates/index.js.map +1 -1
  80. package/dist/templates/server.d.ts +3 -0
  81. package/dist/templates/server.d.ts.map +1 -1
  82. package/dist/templates/server.js +55 -1
  83. package/dist/templates/server.js.map +1 -1
  84. package/dist/templates/shadcn.js +1 -1
  85. package/dist/templates/shared.js +1 -1
  86. package/package.json +2 -2
  87. package/skills/app-structure/SKILL.md +38 -5
  88. package/src/analysis/source-index.ts +395 -0
  89. package/src/analysis/workspace.ts +180 -0
  90. package/src/app-map-schema.ts +28 -0
  91. package/src/app-map.ts +1705 -0
  92. package/src/check.ts +27 -4
  93. package/src/config.ts +7 -3
  94. package/src/db.ts +232 -14
  95. package/src/explain.ts +786 -0
  96. package/src/framework.ts +13 -0
  97. package/src/index.ts +113 -2
  98. package/src/inspect.ts +259 -40
  99. package/src/lib.ts +36 -0
  100. package/src/make/payments.ts +7 -0
  101. package/src/make.ts +232 -2
  102. package/src/mcp.ts +107 -1
  103. package/src/preflight.ts +14 -0
  104. package/src/provider-add.ts +211 -12
  105. package/src/provider-audit.ts +127 -22
  106. package/src/templates/agents.ts +17 -4
  107. package/src/templates/base.ts +22 -4
  108. package/src/templates/index.ts +18 -1
  109. package/src/templates/server.ts +58 -1
  110. package/src/templates/shadcn.ts +1 -1
  111. package/src/templates/shared.ts +1 -1
  112. package/src/test-helpers/generated-app.ts +1 -1
package/src/check.ts CHANGED
@@ -22,6 +22,7 @@ export type CheckStep = {
22
22
  status: "passed" | "failed" | "skipped";
23
23
  detail?: string;
24
24
  output?: string;
25
+ outputTruncated?: boolean;
25
26
  durationMs: number;
26
27
  };
27
28
 
@@ -40,11 +41,18 @@ export type CheckAppOptions = {
40
41
  color?: boolean;
41
42
  /** Append a `beignet preflight` step after the doctor step. */
42
43
  preflight?: boolean;
44
+ /** Keep only the last N bytes from each package-script output stream. */
45
+ maxOutputBytes?: number;
46
+ /** Cancel the active package script with the owning request. */
47
+ signal?: AbortSignal;
48
+ /** Fail each package script after this many milliseconds. */
49
+ scriptTimeoutMs?: number;
43
50
  /** Called after each step completes, for incremental progress output. */
44
51
  onStep?: (step: CheckStep) => void;
45
52
  };
46
53
 
47
54
  const scriptStepNames = ["lint", "typecheck", "test"] as const;
55
+ const defaultMaxOutputBytes = 64 * 1024;
48
56
 
49
57
  /**
50
58
  * Run the whole app validation loop as one command: `beignet lint`,
@@ -67,17 +75,21 @@ export async function checkApp(
67
75
  options.onStep?.(step);
68
76
  };
69
77
 
78
+ options.signal?.throwIfAborted();
70
79
  record(await lintStep(targetDir, options));
80
+ options.signal?.throwIfAborted();
71
81
  const { step: doctorStep, fixes } = await runDoctorStep(targetDir, options);
72
82
  record(doctorStep);
73
83
 
74
84
  if (options.preflight) {
85
+ options.signal?.throwIfAborted();
75
86
  record(await preflightStep(targetDir));
76
87
  }
77
88
 
78
89
  const scripts = await readPackageScripts(targetDir);
79
90
  for (const script of scriptStepNames) {
80
- record(await scriptStep(targetDir, runner, script, scripts));
91
+ options.signal?.throwIfAborted();
92
+ record(await scriptStep(targetDir, runner, script, scripts, options));
81
93
  }
82
94
 
83
95
  return {
@@ -114,7 +126,10 @@ export function formatCheckStep(
114
126
  .split("\n")
115
127
  .map((outputLine) => ` ${outputLine}`)
116
128
  .join("\n");
117
- return `${line}\n${indented}`;
129
+ const truncation = step.outputTruncated
130
+ ? "\n … output truncated; showing the retained tail"
131
+ : "";
132
+ return `${line}\n${indented}${truncation}`;
118
133
  }
119
134
 
120
135
  /**
@@ -203,6 +218,7 @@ async function scriptStep(
203
218
  runner: string,
204
219
  script: (typeof scriptStepNames)[number],
205
220
  scripts: Record<string, string>,
221
+ options: CheckAppOptions,
206
222
  ): Promise<CheckStep> {
207
223
  const command = `${runner} run ${script}`;
208
224
  if (!scripts[script]) {
@@ -218,17 +234,24 @@ async function scriptStep(
218
234
  const startedAt = Date.now();
219
235
  const result = await spawnCommand(runner, ["run", script], targetDir, {
220
236
  captureOutput: true,
237
+ maxOutputBytes: options.maxOutputBytes ?? defaultMaxOutputBytes,
238
+ signal: options.signal,
239
+ timeoutMs: options.scriptTimeoutMs,
221
240
  });
222
- const failed = result.exitCode !== 0;
241
+ const failed = result.timedOut === true || result.exitCode !== 0;
242
+ const detail = result.timedOut
243
+ ? `${command}; timed out after ${options.scriptTimeoutMs}ms`
244
+ : command;
223
245
 
224
246
  return {
225
247
  name: script,
226
248
  command,
227
249
  status: failed ? "failed" : "passed",
228
- detail: command,
250
+ detail,
229
251
  ...(failed
230
252
  ? { output: [result.stdout, result.stderr].filter(Boolean).join("\n") }
231
253
  : {}),
254
+ ...(failed && result.outputTruncated ? { outputTruncated: true } : {}),
232
255
  durationMs: Date.now() - startedAt,
233
256
  };
234
257
  }
package/src/config.ts CHANGED
@@ -6,7 +6,7 @@ import { createJiti } from "jiti";
6
6
  /**
7
7
  * Supported application framework targets for Beignet config.
8
8
  */
9
- export type BeignetFramework = "next";
9
+ export type BeignetFramework = "next" | "web";
10
10
 
11
11
  /**
12
12
  * Configurable app paths used by CLI generators, inspection, linting, and
@@ -306,11 +306,15 @@ function assertConfigObject(
306
306
  throw new Error(`${configFile} must export a Beignet config object.`);
307
307
  }
308
308
 
309
- if (config.framework !== undefined && config.framework !== "next") {
309
+ if (
310
+ config.framework !== undefined &&
311
+ config.framework !== "next" &&
312
+ config.framework !== "web"
313
+ ) {
310
314
  throw new Error(
311
315
  `${configFile} uses unsupported framework "${String(
312
316
  config.framework,
313
- )}". Supported framework: "next".`,
317
+ )}". Supported frameworks: "next", "web".`,
314
318
  );
315
319
  }
316
320
 
package/src/db.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { spawn } from "node:child_process";
1
+ import { type ChildProcess, spawn } from "node:child_process";
2
2
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
 
@@ -33,6 +33,29 @@ export type RunDatabaseCommandResult = {
33
33
  exitCode: number;
34
34
  };
35
35
 
36
+ export type SpawnCommandOptions = {
37
+ captureOutput: boolean;
38
+ /** Keep only the last N bytes from each output stream. Unbounded when omitted. */
39
+ maxOutputBytes?: number;
40
+ /** Cancel the child process when the owning request is aborted. */
41
+ signal?: AbortSignal;
42
+ /** Stop the child process after this many milliseconds. Unbounded when omitted. */
43
+ timeoutMs?: number;
44
+ };
45
+
46
+ export type SpawnCommandResult = {
47
+ exitCode: number;
48
+ stdout?: string;
49
+ stderr?: string;
50
+ outputTruncated?: boolean;
51
+ timedOut?: boolean;
52
+ };
53
+
54
+ type ProcessTerminationPlan =
55
+ | { kind: "child"; signal: NodeJS.Signals }
56
+ | { kind: "process-group"; pid: number; signal: NodeJS.Signals }
57
+ | { kind: "windows-process-tree"; command: string; args: string[] };
58
+
36
59
  export type DatabaseSchemaDialect = "sqlite" | "postgres" | "mysql";
37
60
  export type DatabaseSchemaTable = "audit" | "idempotency" | "outbox";
38
61
 
@@ -504,34 +527,229 @@ export function spawnCommand(
504
527
  command: string,
505
528
  args: readonly string[],
506
529
  cwd: string,
507
- options: { captureOutput: boolean },
508
- ): Promise<{ exitCode: number; stdout?: string; stderr?: string }> {
530
+ options: SpawnCommandOptions,
531
+ ): Promise<SpawnCommandResult> {
532
+ if (options.signal?.aborted) {
533
+ return Promise.reject(commandAbortError(options.signal));
534
+ }
535
+ const timeoutMs =
536
+ options.timeoutMs === undefined
537
+ ? undefined
538
+ : normalizePositiveInteger(options.timeoutMs, "Command timeout");
539
+ const maxOutputBytes =
540
+ options.maxOutputBytes === undefined
541
+ ? undefined
542
+ : normalizeNonNegativeInteger(
543
+ options.maxOutputBytes,
544
+ "Maximum output bytes",
545
+ );
546
+
509
547
  return new Promise((resolve, reject) => {
548
+ let settled = false;
549
+ let timedOut = false;
550
+ let timeout: ReturnType<typeof setTimeout> | undefined;
551
+ let forceKillTimeout: ReturnType<typeof setTimeout> | undefined;
552
+ const ownsProcessGroup = commandOwnsProcessGroup(process.platform, {
553
+ signal: options.signal,
554
+ timeoutMs,
555
+ });
510
556
  const child = spawn(command, args, {
511
557
  cwd,
558
+ detached: ownsProcessGroup,
512
559
  env: process.env,
513
560
  stdio: options.captureOutput ? ["ignore", "pipe", "pipe"] : "inherit",
514
561
  });
515
562
 
516
- const stdout: Buffer[] = [];
517
- const stderr: Buffer[] = [];
563
+ const stdout = createOutputCapture(maxOutputBytes);
564
+ const stderr = createOutputCapture(maxOutputBytes);
565
+
566
+ const cleanup = () => {
567
+ if (timeout) clearTimeout(timeout);
568
+ if (forceKillTimeout) clearTimeout(forceKillTimeout);
569
+ options.signal?.removeEventListener("abort", abort);
570
+ };
571
+ const abort = () => {
572
+ if (settled) return;
573
+ settled = true;
574
+ terminateChild(child, "SIGKILL", ownsProcessGroup);
575
+ cleanup();
576
+ reject(commandAbortError(options.signal));
577
+ };
518
578
 
519
579
  if (options.captureOutput) {
520
- child.stdout?.on("data", (chunk: Buffer) => stdout.push(chunk));
521
- child.stderr?.on("data", (chunk: Buffer) => stderr.push(chunk));
580
+ child.stdout?.on("data", (chunk: Buffer) => stdout.append(chunk));
581
+ child.stderr?.on("data", (chunk: Buffer) => stderr.append(chunk));
522
582
  }
523
583
 
524
- child.on("error", reject);
525
- child.on("close", (code) =>
584
+ child.on("error", (error) => {
585
+ if (settled) return;
586
+ settled = true;
587
+ cleanup();
588
+ reject(error);
589
+ });
590
+ child.on("close", (code) => {
591
+ if (settled) return;
592
+ settled = true;
593
+ cleanup();
526
594
  resolve({
527
- exitCode: code ?? 1,
595
+ exitCode: timedOut ? 1 : (code ?? 1),
528
596
  ...(options.captureOutput
529
597
  ? {
530
- stdout: Buffer.concat(stdout).toString("utf8"),
531
- stderr: Buffer.concat(stderr).toString("utf8"),
598
+ stdout: stdout.text(),
599
+ stderr: stderr.text(),
600
+ ...(stdout.truncated() || stderr.truncated()
601
+ ? { outputTruncated: true }
602
+ : {}),
532
603
  }
533
604
  : {}),
534
- }),
535
- );
605
+ ...(timedOut ? { timedOut: true } : {}),
606
+ });
607
+ });
608
+
609
+ options.signal?.addEventListener("abort", abort, { once: true });
610
+ if (options.signal?.aborted) {
611
+ abort();
612
+ return;
613
+ }
614
+ if (timeoutMs !== undefined) {
615
+ timeout = setTimeout(() => {
616
+ if (settled) return;
617
+ timedOut = true;
618
+ terminateChild(child, "SIGTERM", ownsProcessGroup);
619
+ forceKillTimeout = setTimeout(() => {
620
+ if (!settled) terminateChild(child, "SIGKILL", ownsProcessGroup);
621
+ }, 1_000);
622
+ forceKillTimeout.unref?.();
623
+ }, timeoutMs);
624
+ timeout.unref?.();
625
+ }
536
626
  });
537
627
  }
628
+
629
+ export function commandOwnsProcessGroup(
630
+ platform: NodeJS.Platform,
631
+ options: Pick<SpawnCommandOptions, "signal" | "timeoutMs">,
632
+ ): boolean {
633
+ return (
634
+ platform !== "win32" &&
635
+ (options.signal !== undefined || options.timeoutMs !== undefined)
636
+ );
637
+ }
638
+
639
+ export function commandTerminationPlan(
640
+ platform: NodeJS.Platform,
641
+ pid: number | undefined,
642
+ signal: NodeJS.Signals,
643
+ ownsProcessGroup: boolean,
644
+ ): ProcessTerminationPlan {
645
+ if (platform === "win32" && pid !== undefined) {
646
+ return {
647
+ kind: "windows-process-tree",
648
+ command: "taskkill",
649
+ args: ["/PID", String(pid), "/T", "/F"],
650
+ };
651
+ }
652
+ if (ownsProcessGroup && pid !== undefined) {
653
+ return { kind: "process-group", pid: -pid, signal };
654
+ }
655
+ return { kind: "child", signal };
656
+ }
657
+
658
+ function terminateChild(
659
+ child: ChildProcess,
660
+ signal: NodeJS.Signals,
661
+ ownsProcessGroup: boolean,
662
+ ): void {
663
+ const plan = commandTerminationPlan(
664
+ process.platform,
665
+ child.pid,
666
+ signal,
667
+ ownsProcessGroup,
668
+ );
669
+ if (plan.kind === "windows-process-tree") {
670
+ const killer = spawn(plan.command, plan.args, {
671
+ stdio: "ignore",
672
+ windowsHide: true,
673
+ });
674
+ const fallback = () => {
675
+ if (child.exitCode === null) child.kill(signal);
676
+ };
677
+ killer.once("error", fallback);
678
+ killer.once("close", (code) => {
679
+ if (code !== 0) fallback();
680
+ });
681
+ killer.unref();
682
+ return;
683
+ }
684
+ if (plan.kind === "process-group") {
685
+ try {
686
+ process.kill(plan.pid, plan.signal);
687
+ return;
688
+ } catch (error) {
689
+ if ((error as NodeJS.ErrnoException).code === "ESRCH") return;
690
+ }
691
+ }
692
+ child.kill(plan.signal);
693
+ }
694
+
695
+ function createOutputCapture(maxOutputBytes?: number): {
696
+ append(chunk: Buffer): void;
697
+ text(): string;
698
+ truncated(): boolean;
699
+ } {
700
+ const limit = maxOutputBytes;
701
+ const unboundedChunks: Buffer[] = [];
702
+ let output: Buffer = Buffer.alloc(0);
703
+ let wasTruncated = false;
704
+
705
+ return {
706
+ append(chunk) {
707
+ if (limit === undefined) {
708
+ unboundedChunks.push(chunk);
709
+ return;
710
+ }
711
+ if (limit === 0) {
712
+ if (chunk.length > 0) wasTruncated = true;
713
+ return;
714
+ }
715
+ if (chunk.length >= limit) {
716
+ wasTruncated ||= output.length > 0 || chunk.length > limit;
717
+ output = Buffer.from(chunk.subarray(chunk.length - limit));
718
+ return;
719
+ }
720
+ const overflow = output.length + chunk.length - limit;
721
+ if (overflow > 0) {
722
+ wasTruncated = true;
723
+ output = Buffer.concat([output.subarray(overflow), chunk]);
724
+ return;
725
+ }
726
+ output = Buffer.concat([output, chunk]);
727
+ },
728
+ text: () =>
729
+ (limit === undefined ? Buffer.concat(unboundedChunks) : output).toString(
730
+ "utf8",
731
+ ),
732
+ truncated: () => wasTruncated,
733
+ };
734
+ }
735
+
736
+ function commandAbortError(signal?: AbortSignal): Error {
737
+ if (signal?.reason instanceof Error) return signal.reason;
738
+ const error = new Error("Command cancelled.");
739
+ error.name = "AbortError";
740
+ return error;
741
+ }
742
+
743
+ function normalizePositiveInteger(value: number, label: string): number {
744
+ if (!Number.isSafeInteger(value) || value <= 0) {
745
+ throw new Error(`${label} must be a positive integer.`);
746
+ }
747
+ return value;
748
+ }
749
+
750
+ function normalizeNonNegativeInteger(value: number, label: string): number {
751
+ if (!Number.isSafeInteger(value) || value < 0) {
752
+ throw new Error(`${label} must be a non-negative integer.`);
753
+ }
754
+ return value;
755
+ }