@beignet/cli 0.0.40 → 0.0.42

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 (111) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +139 -17
  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/choices.d.ts +18 -0
  24. package/dist/choices.d.ts.map +1 -1
  25. package/dist/choices.js +35 -0
  26. package/dist/choices.js.map +1 -1
  27. package/dist/db.d.ts +49 -13
  28. package/dist/db.d.ts.map +1 -1
  29. package/dist/db.js +202 -21
  30. package/dist/db.js.map +1 -1
  31. package/dist/doctor-fixes.d.ts +64 -0
  32. package/dist/doctor-fixes.d.ts.map +1 -0
  33. package/dist/doctor-fixes.js +142 -0
  34. package/dist/doctor-fixes.js.map +1 -0
  35. package/dist/explain.d.ts +100 -0
  36. package/dist/explain.d.ts.map +1 -0
  37. package/dist/explain.js +606 -0
  38. package/dist/explain.js.map +1 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +165 -21
  41. package/dist/index.js.map +1 -1
  42. package/dist/inspect.d.ts +33 -9
  43. package/dist/inspect.d.ts.map +1 -1
  44. package/dist/inspect.js +406 -119
  45. package/dist/inspect.js.map +1 -1
  46. package/dist/lib.d.ts +10 -2
  47. package/dist/lib.d.ts.map +1 -1
  48. package/dist/lib.js +5 -2
  49. package/dist/lib.js.map +1 -1
  50. package/dist/make/shared.js +3 -3
  51. package/dist/make/shared.js.map +1 -1
  52. package/dist/make.d.ts.map +1 -1
  53. package/dist/make.js +63 -2
  54. package/dist/make.js.map +1 -1
  55. package/dist/mcp.d.ts +1 -1
  56. package/dist/mcp.d.ts.map +1 -1
  57. package/dist/mcp.js +188 -12
  58. package/dist/mcp.js.map +1 -1
  59. package/dist/preflight.d.ts.map +1 -1
  60. package/dist/preflight.js +10 -0
  61. package/dist/preflight.js.map +1 -1
  62. package/dist/provider-add.d.ts.map +1 -1
  63. package/dist/provider-add.js +167 -13
  64. package/dist/provider-add.js.map +1 -1
  65. package/dist/provider-audit.d.ts +6 -0
  66. package/dist/provider-audit.d.ts.map +1 -1
  67. package/dist/provider-audit.js +63 -15
  68. package/dist/provider-audit.js.map +1 -1
  69. package/dist/templates/agents.d.ts.map +1 -1
  70. package/dist/templates/agents.js +35 -6
  71. package/dist/templates/agents.js.map +1 -1
  72. package/dist/templates/base.d.ts.map +1 -1
  73. package/dist/templates/base.js +13 -7
  74. package/dist/templates/base.js.map +1 -1
  75. package/dist/templates/index.d.ts +1 -1
  76. package/dist/templates/index.d.ts.map +1 -1
  77. package/dist/templates/index.js +8 -1
  78. package/dist/templates/index.js.map +1 -1
  79. package/dist/templates/server.d.ts +3 -0
  80. package/dist/templates/server.d.ts.map +1 -1
  81. package/dist/templates/server.js +55 -1
  82. package/dist/templates/server.js.map +1 -1
  83. package/dist/templates/shared.d.ts +2 -1
  84. package/dist/templates/shared.d.ts.map +1 -1
  85. package/dist/templates/shared.js +8 -5
  86. package/dist/templates/shared.js.map +1 -1
  87. package/package.json +3 -2
  88. package/skills/app-structure/SKILL.md +48 -6
  89. package/src/analysis/source-index.ts +395 -0
  90. package/src/analysis/workspace.ts +180 -0
  91. package/src/app-map-schema.ts +28 -0
  92. package/src/app-map.ts +1705 -0
  93. package/src/check.ts +27 -4
  94. package/src/choices.ts +57 -0
  95. package/src/db.ts +276 -28
  96. package/src/doctor-fixes.ts +252 -0
  97. package/src/explain.ts +894 -0
  98. package/src/index.ts +237 -31
  99. package/src/inspect.ts +567 -147
  100. package/src/lib.ts +64 -1
  101. package/src/make/shared.ts +3 -3
  102. package/src/make.ts +90 -2
  103. package/src/mcp.ts +292 -12
  104. package/src/preflight.ts +14 -0
  105. package/src/provider-add.ts +211 -12
  106. package/src/provider-audit.ts +127 -22
  107. package/src/templates/agents.ts +35 -6
  108. package/src/templates/base.ts +25 -6
  109. package/src/templates/index.ts +18 -1
  110. package/src/templates/server.ts +58 -1
  111. package/src/templates/shared.ts +15 -7
@@ -0,0 +1,252 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { createTwoFilesPatch } from "diff";
5
+ import { type DoctorFixOperationId, doctorFixOperationIds } from "./choices.js";
6
+
7
+ export type { DoctorFixOperationId } from "./choices.js";
8
+ export { doctorFixOperationIds } from "./choices.js";
9
+
10
+ /** Automatic doctor fix that was applied. */
11
+ export type InspectFix = {
12
+ code: string;
13
+ message: string;
14
+ file: string;
15
+ };
16
+
17
+ /** Exact file mutation proposed by a doctor fix operation. */
18
+ export type DoctorFixFileChange = {
19
+ file: string;
20
+ kind: "create" | "update";
21
+ beforeHash: string | null;
22
+ afterHash: string;
23
+ patch: string;
24
+ };
25
+
26
+ /** Selectable low-risk repair and every file change it requires. */
27
+ export type DoctorFixOperation = {
28
+ id: DoctorFixOperationId;
29
+ fixes: InspectFix[];
30
+ changes: DoctorFixFileChange[];
31
+ };
32
+
33
+ /** Deterministic, read-only repair plan for the current app state. */
34
+ export type DoctorFixPlan = {
35
+ schemaVersion: 1;
36
+ targetDir: string;
37
+ strict: boolean;
38
+ planId: string;
39
+ operations: DoctorFixOperation[];
40
+ };
41
+
42
+ /** Result of applying all or part of a guarded doctor fix plan. */
43
+ export type DoctorFixApplyResult = {
44
+ schemaVersion: 1;
45
+ targetDir: string;
46
+ planId: string;
47
+ operationIds: DoctorFixOperationId[];
48
+ fixes: InspectFix[];
49
+ };
50
+
51
+ export type PlannedDoctorFixFileChange = DoctorFixFileChange & {
52
+ before: string | undefined;
53
+ after: string;
54
+ };
55
+
56
+ export type PlannedDoctorFixOperation = Omit<DoctorFixOperation, "changes"> & {
57
+ changes: PlannedDoctorFixFileChange[];
58
+ };
59
+
60
+ export type PlannedDoctorFixPlan = Omit<DoctorFixPlan, "operations"> & {
61
+ operations: PlannedDoctorFixOperation[];
62
+ };
63
+
64
+ export function planDoctorFixFileChange(options: {
65
+ file: string;
66
+ before?: string;
67
+ after: string;
68
+ }): PlannedDoctorFixFileChange {
69
+ const oldName =
70
+ options.before === undefined ? "/dev/null" : `a/${options.file}`;
71
+ const newName = `b/${options.file}`;
72
+ return {
73
+ file: options.file,
74
+ kind: options.before === undefined ? "create" : "update",
75
+ beforeHash:
76
+ options.before === undefined ? null : contentHash(options.before),
77
+ afterHash: contentHash(options.after),
78
+ patch: createTwoFilesPatch(
79
+ oldName,
80
+ newName,
81
+ options.before ?? "",
82
+ options.after,
83
+ "",
84
+ "",
85
+ { context: 3 },
86
+ ),
87
+ before: options.before,
88
+ after: options.after,
89
+ };
90
+ }
91
+
92
+ export function createDoctorFixPlan(options: {
93
+ targetDir: string;
94
+ strict: boolean;
95
+ operations: PlannedDoctorFixOperation[];
96
+ }): PlannedDoctorFixPlan {
97
+ const operationOrder = new Map(
98
+ doctorFixOperationIds.map((id, index) => [id, index]),
99
+ );
100
+ const operations = [...options.operations].sort(
101
+ (left, right) =>
102
+ (operationOrder.get(left.id) ?? Number.MAX_SAFE_INTEGER) -
103
+ (operationOrder.get(right.id) ?? Number.MAX_SAFE_INTEGER),
104
+ );
105
+ const fingerprint = {
106
+ targetDir: path.resolve(options.targetDir),
107
+ operations: operations.map((operation) => ({
108
+ id: operation.id,
109
+ changes: operation.changes.map((change) => ({
110
+ file: change.file,
111
+ beforeHash: change.beforeHash,
112
+ afterHash: change.afterHash,
113
+ })),
114
+ })),
115
+ };
116
+
117
+ return {
118
+ schemaVersion: 1,
119
+ targetDir: path.resolve(options.targetDir),
120
+ strict: options.strict,
121
+ planId: contentHash(JSON.stringify(fingerprint)),
122
+ operations,
123
+ };
124
+ }
125
+
126
+ export function publicDoctorFixPlan(plan: PlannedDoctorFixPlan): DoctorFixPlan {
127
+ return {
128
+ ...plan,
129
+ operations: plan.operations.map((operation) => ({
130
+ id: operation.id,
131
+ fixes: operation.fixes,
132
+ changes: operation.changes.map(
133
+ ({ before: _before, after: _after, ...change }) => change,
134
+ ),
135
+ })),
136
+ };
137
+ }
138
+
139
+ export async function applyPlannedDoctorFixes(
140
+ plan: PlannedDoctorFixPlan,
141
+ fixIds?: readonly DoctorFixOperationId[],
142
+ hooks: {
143
+ beforeWrite?: (
144
+ change: PlannedDoctorFixFileChange,
145
+ index: number,
146
+ ) => void | Promise<void>;
147
+ } = {},
148
+ ): Promise<DoctorFixApplyResult> {
149
+ const requested = fixIds ? new Set(fixIds) : undefined;
150
+ const operations = requested
151
+ ? plan.operations.filter((operation) => requested.has(operation.id))
152
+ : plan.operations;
153
+
154
+ if (requested) {
155
+ const available = new Set(plan.operations.map((operation) => operation.id));
156
+ const missing = [...requested].filter((id) => !available.has(id));
157
+ if (missing.length > 0) {
158
+ throw new Error(
159
+ `Doctor fix operations are not available in this plan: ${missing.join(", ")}. Run beignet doctor --fix --dry-run again.`,
160
+ );
161
+ }
162
+ }
163
+
164
+ const changes = operations.flatMap((operation) => operation.changes);
165
+ const duplicateFiles = duplicateValues(changes.map((change) => change.file));
166
+ if (duplicateFiles.length > 0) {
167
+ throw new Error(
168
+ `Doctor fix operations overlap on ${duplicateFiles.join(", ")}; no files were changed.`,
169
+ );
170
+ }
171
+
172
+ for (const change of changes) {
173
+ const current = await readOptionalFile(
174
+ path.join(plan.targetDir, change.file),
175
+ );
176
+ const currentHash = current === undefined ? null : contentHash(current);
177
+ if (currentHash !== change.beforeHash) {
178
+ throw new Error(
179
+ `Doctor fix plan is stale because ${change.file} changed. No files were changed; run beignet doctor --fix --dry-run again.`,
180
+ );
181
+ }
182
+ }
183
+
184
+ const attempted: PlannedDoctorFixFileChange[] = [];
185
+ try {
186
+ for (const [index, change] of changes.entries()) {
187
+ attempted.push(change);
188
+ await hooks.beforeWrite?.(change, index);
189
+ const destination = path.join(plan.targetDir, change.file);
190
+ await mkdir(path.dirname(destination), { recursive: true });
191
+ await writeFile(destination, change.after);
192
+ }
193
+ } catch (error) {
194
+ const rollbackErrors: string[] = [];
195
+ for (const change of attempted.reverse()) {
196
+ const destination = path.join(plan.targetDir, change.file);
197
+ try {
198
+ if (change.before === undefined) {
199
+ await rm(destination, { force: true });
200
+ } else {
201
+ await mkdir(path.dirname(destination), { recursive: true });
202
+ await writeFile(destination, change.before);
203
+ }
204
+ } catch (rollbackError) {
205
+ rollbackErrors.push(
206
+ rollbackError instanceof Error
207
+ ? rollbackError.message
208
+ : String(rollbackError),
209
+ );
210
+ }
211
+ }
212
+
213
+ const message = error instanceof Error ? error.message : String(error);
214
+ throw new Error(
215
+ rollbackErrors.length === 0
216
+ ? `Doctor fix application failed and was rolled back: ${message}`
217
+ : `Doctor fix application failed: ${message}. Rollback also failed: ${rollbackErrors.join("; ")}`,
218
+ { cause: error },
219
+ );
220
+ }
221
+
222
+ return {
223
+ schemaVersion: 1,
224
+ targetDir: plan.targetDir,
225
+ planId: plan.planId,
226
+ operationIds: operations.map((operation) => operation.id),
227
+ fixes: operations.flatMap((operation) => operation.fixes),
228
+ };
229
+ }
230
+
231
+ function contentHash(value: string): string {
232
+ return createHash("sha256").update(value).digest("hex");
233
+ }
234
+
235
+ async function readOptionalFile(file: string): Promise<string | undefined> {
236
+ try {
237
+ return await readFile(file, "utf8");
238
+ } catch (error) {
239
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
240
+ throw error;
241
+ }
242
+ }
243
+
244
+ function duplicateValues(values: readonly string[]): string[] {
245
+ const seen = new Set<string>();
246
+ const duplicates = new Set<string>();
247
+ for (const value of values) {
248
+ if (seen.has(value)) duplicates.add(value);
249
+ seen.add(value);
250
+ }
251
+ return [...duplicates].sort();
252
+ }