@beignet/cli 0.0.51 → 0.0.53
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 +46 -0
- package/README.md +182 -19
- package/dist/analysis/layers.d.ts +2 -0
- package/dist/analysis/layers.d.ts.map +1 -1
- package/dist/analysis/layers.js +9 -2
- package/dist/analysis/layers.js.map +1 -1
- package/dist/analysis/port-wiring.d.ts +4 -2
- package/dist/analysis/port-wiring.d.ts.map +1 -1
- package/dist/analysis/port-wiring.js +41 -11
- package/dist/analysis/port-wiring.js.map +1 -1
- package/dist/analysis/source-files.d.ts +15 -0
- package/dist/analysis/source-files.d.ts.map +1 -0
- package/dist/analysis/source-files.js +52 -0
- package/dist/analysis/source-files.js.map +1 -0
- package/dist/analysis/source-index.d.ts +2 -1
- package/dist/analysis/source-index.d.ts.map +1 -1
- package/dist/analysis/source-index.js +34 -5
- package/dist/analysis/source-index.js.map +1 -1
- package/dist/analysis/workspace-routes.d.ts +8 -0
- package/dist/analysis/workspace-routes.d.ts.map +1 -0
- package/dist/analysis/workspace-routes.js +291 -0
- package/dist/analysis/workspace-routes.js.map +1 -0
- package/dist/analysis/workspace.d.ts +24 -3
- package/dist/analysis/workspace.d.ts.map +1 -1
- package/dist/analysis/workspace.js +184 -43
- package/dist/analysis/workspace.js.map +1 -1
- package/dist/app-map-changes.d.ts.map +1 -1
- package/dist/app-map-changes.js +24 -14
- package/dist/app-map-changes.js.map +1 -1
- package/dist/app-map.d.ts.map +1 -1
- package/dist/app-map.js +87 -81
- package/dist/app-map.js.map +1 -1
- package/dist/check.d.ts +12 -0
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +92 -25
- package/dist/check.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +19 -0
- package/dist/config.js.map +1 -1
- package/dist/git-changes.d.ts +17 -1
- package/dist/git-changes.d.ts.map +1 -1
- package/dist/git-changes.js +118 -84
- package/dist/git-changes.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +424 -129
- package/dist/inspect.js.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +108 -43
- package/dist/lint.js.map +1 -1
- package/dist/make/shared.js +1 -1
- package/dist/make.js +18 -1
- package/dist/make.js.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/mcp.js.map +1 -1
- package/dist/provider-add.js +2 -2
- package/dist/provider-add.js.map +1 -1
- package/dist/provider-audit.d.ts.map +1 -1
- package/dist/provider-audit.js +22 -6
- package/dist/provider-audit.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +36 -13
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.js +5 -5
- package/dist/templates/db/mysql.d.ts.map +1 -1
- package/dist/templates/db/mysql.js +51 -22
- package/dist/templates/db/mysql.js.map +1 -1
- package/dist/templates/db/postgres.d.ts.map +1 -1
- package/dist/templates/db/postgres.js +60 -20
- package/dist/templates/db/postgres.js.map +1 -1
- package/dist/templates/db/sqlite.d.ts.map +1 -1
- package/dist/templates/db/sqlite.js +51 -23
- package/dist/templates/db/sqlite.js.map +1 -1
- package/dist/templates/shadcn.js +1 -1
- package/dist/templates/shadcn.js.map +1 -1
- package/dist/templates/shared.js +6 -6
- package/dist/templates/shared.js.map +1 -1
- package/dist/templates/testing.d.ts.map +1 -1
- package/dist/templates/testing.js +7 -19
- package/dist/templates/testing.js.map +1 -1
- package/dist/test-discovery.d.ts +9 -0
- package/dist/test-discovery.d.ts.map +1 -0
- package/dist/test-discovery.js +40 -0
- package/dist/test-discovery.js.map +1 -0
- package/package.json +8 -5
- package/skills/app-structure/SKILL.md +74 -7
- package/src/analysis/layers.ts +13 -2
- package/src/analysis/port-wiring.ts +68 -11
- package/src/analysis/source-files.ts +61 -0
- package/src/analysis/source-index.ts +56 -4
- package/src/analysis/workspace-routes.ts +385 -0
- package/src/analysis/workspace.ts +302 -59
- package/src/app-map-changes.ts +41 -13
- package/src/app-map.ts +114 -103
- package/src/check.ts +115 -25
- package/src/config.ts +33 -0
- package/src/git-changes.ts +218 -86
- package/src/index.ts +33 -5
- package/src/inspect.ts +608 -156
- package/src/lint.ts +152 -48
- package/src/make/shared.ts +1 -1
- package/src/make.ts +31 -1
- package/src/mcp.ts +1 -1
- package/src/provider-add.ts +2 -2
- package/src/provider-audit.ts +30 -10
- package/src/templates/agents.ts +36 -13
- package/src/templates/base.ts +5 -5
- package/src/templates/db/mysql.ts +51 -22
- package/src/templates/db/postgres.ts +60 -20
- package/src/templates/db/sqlite.ts +51 -23
- package/src/templates/shadcn.ts +1 -1
- package/src/templates/shared.ts +6 -6
- package/src/templates/testing.ts +12 -19
- package/src/test-discovery.ts +53 -0
package/src/config.ts
CHANGED
|
@@ -67,6 +67,12 @@ export type BeignetProviderAuditConfig = {
|
|
|
67
67
|
ignoreRequiredEnv?: string[];
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
+
/** Source packages included in read-only application analysis. */
|
|
71
|
+
export type BeignetWorkspaceConfig = {
|
|
72
|
+
/** Package directories relative to this app, including transitive dependencies. */
|
|
73
|
+
packages: string[];
|
|
74
|
+
};
|
|
75
|
+
|
|
70
76
|
/**
|
|
71
77
|
* User-authored Beignet CLI config.
|
|
72
78
|
*/
|
|
@@ -75,6 +81,7 @@ export type BeignetConfig = {
|
|
|
75
81
|
paths?: Partial<BeignetPaths>;
|
|
76
82
|
database?: BeignetDatabaseConfig;
|
|
77
83
|
providerAudit?: BeignetProviderAuditConfig;
|
|
84
|
+
workspace?: BeignetWorkspaceConfig;
|
|
78
85
|
};
|
|
79
86
|
|
|
80
87
|
/**
|
|
@@ -90,6 +97,7 @@ export type ResolvedBeignetConfig = {
|
|
|
90
97
|
providerAudit: {
|
|
91
98
|
ignoreRequiredEnv: string[];
|
|
92
99
|
};
|
|
100
|
+
workspace: BeignetWorkspaceConfig;
|
|
93
101
|
configFile?: string;
|
|
94
102
|
};
|
|
95
103
|
|
|
@@ -134,6 +142,7 @@ export const defaultBeignetConfig: ResolvedBeignetConfig = {
|
|
|
134
142
|
providerAudit: {
|
|
135
143
|
ignoreRequiredEnv: [],
|
|
136
144
|
},
|
|
145
|
+
workspace: { packages: [] },
|
|
137
146
|
};
|
|
138
147
|
|
|
139
148
|
/**
|
|
@@ -174,6 +183,7 @@ export function resolveConfig(
|
|
|
174
183
|
paths: normalizeConfigPaths(paths),
|
|
175
184
|
database: normalizeDatabaseConfig(database),
|
|
176
185
|
providerAudit: normalizeProviderAuditConfig(providerAudit),
|
|
186
|
+
workspace: normalizeWorkspaceConfig(config.workspace),
|
|
177
187
|
configFile,
|
|
178
188
|
};
|
|
179
189
|
}
|
|
@@ -453,6 +463,29 @@ function normalizeIgnoredRequiredEnv(index: number, value: string): string {
|
|
|
453
463
|
return value.trim();
|
|
454
464
|
}
|
|
455
465
|
|
|
466
|
+
function normalizeWorkspaceConfig(
|
|
467
|
+
workspace: BeignetWorkspaceConfig | undefined,
|
|
468
|
+
): BeignetWorkspaceConfig {
|
|
469
|
+
if (workspace === undefined) return { packages: [] };
|
|
470
|
+
if (!isRecord(workspace) || !Array.isArray(workspace.packages)) {
|
|
471
|
+
throw new Error("Beignet config workspace.packages must be an array.");
|
|
472
|
+
}
|
|
473
|
+
const packages = workspace.packages.map((value, index) => {
|
|
474
|
+
if (
|
|
475
|
+
typeof value !== "string" ||
|
|
476
|
+
!value.trim() ||
|
|
477
|
+
path.isAbsolute(value.trim()) ||
|
|
478
|
+
path.win32.isAbsolute(value.trim())
|
|
479
|
+
) {
|
|
480
|
+
throw new Error(
|
|
481
|
+
`Beignet config workspace.packages[${index}] must be a relative package directory.`,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
return path.posix.normalize(normalizePath(value.trim())).replace(/\/$/, "");
|
|
485
|
+
});
|
|
486
|
+
return { packages: [...new Set(packages)].sort() };
|
|
487
|
+
}
|
|
488
|
+
|
|
456
489
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
457
490
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
458
491
|
}
|
package/src/git-changes.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
82
|
-
|
|
83
|
-
"--verify",
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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(
|
|
92
|
-
|
|
93
|
-
baseCommit,
|
|
94
|
-
|
|
95
|
-
|
|
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 =
|
|
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(
|
|
120
|
-
|
|
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(
|
|
125
|
-
|
|
126
|
-
head ?? comparisonCommit,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"--exclude-standard",
|
|
133
|
-
|
|
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
|
-
|
|
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(
|
|
148
|
-
|
|
149
|
-
"--show-object-format=storage",
|
|
150
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
240
|
-
|
|
241
|
-
"--verify",
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const status = await tryGitOutput(
|
|
245
|
-
|
|
246
|
-
"--porcelain=v1",
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
264
|
-
|
|
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(
|
|
401
|
-
|
|
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(
|
|
408
|
-
|
|
409
|
-
"--verify",
|
|
410
|
-
|
|
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(
|
|
415
|
-
|
|
416
|
-
"--quiet",
|
|
417
|
-
|
|
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(
|
|
425
|
-
|
|
426
|
-
"--verify",
|
|
427
|
-
|
|
428
|
-
|
|
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;
|
|
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(
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
@@ -1164,7 +1164,7 @@ const checkCommand = buildCommand<CheckFlags, [], CliContext>({
|
|
|
1164
1164
|
docs: {
|
|
1165
1165
|
brief: "Run the full app validation loop as one command.",
|
|
1166
1166
|
fullDescription:
|
|
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.",
|
|
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.",
|
|
1168
1168
|
},
|
|
1169
1169
|
parameters: {
|
|
1170
1170
|
flags: {
|
|
@@ -1194,9 +1194,12 @@ const checkCommand = buildCommand<CheckFlags, [], CliContext>({
|
|
|
1194
1194
|
} satisfies FlagParametersForType<CheckFlags, CliContext>,
|
|
1195
1195
|
},
|
|
1196
1196
|
loader: async () => {
|
|
1197
|
-
const {
|
|
1198
|
-
|
|
1199
|
-
|
|
1197
|
+
const {
|
|
1198
|
+
checkApp,
|
|
1199
|
+
formatCheckProvenance,
|
|
1200
|
+
formatCheckStep,
|
|
1201
|
+
formatCheckSummary,
|
|
1202
|
+
} = await import("./check.js");
|
|
1200
1203
|
|
|
1201
1204
|
return async function runCheck(this: CliContext, flags: CheckFlags) {
|
|
1202
1205
|
const color = useColor();
|
|
@@ -1218,7 +1221,7 @@ const checkCommand = buildCommand<CheckFlags, [], CliContext>({
|
|
|
1218
1221
|
this,
|
|
1219
1222
|
flags.json
|
|
1220
1223
|
? JSON.stringify(result, null, 2)
|
|
1221
|
-
: `\n${formatCheckSummary(result, { color })}`,
|
|
1224
|
+
: `\n${formatCheckProvenance(result, { color })}\n${formatCheckSummary(result, { color })}`,
|
|
1222
1225
|
);
|
|
1223
1226
|
if (!result.ok) {
|
|
1224
1227
|
this.process.exitCode = 1;
|
|
@@ -1850,6 +1853,30 @@ const scheduleRoutes = buildRouteMap({
|
|
|
1850
1853
|
},
|
|
1851
1854
|
});
|
|
1852
1855
|
|
|
1856
|
+
const encryptionKeyCommand = buildCommand<{ json?: boolean }, [], CliContext>({
|
|
1857
|
+
docs: {
|
|
1858
|
+
brief: "Print a new encryption key without changing environment files.",
|
|
1859
|
+
},
|
|
1860
|
+
parameters: {
|
|
1861
|
+
flags: { json: jsonFlag },
|
|
1862
|
+
},
|
|
1863
|
+
loader: async () => {
|
|
1864
|
+
const { generateEncryptionKey } = await import("@beignet/core/encryption");
|
|
1865
|
+
return async function runEncryptionKey(this: CliContext, flags) {
|
|
1866
|
+
const key = generateEncryptionKey();
|
|
1867
|
+
writeOutput(
|
|
1868
|
+
this,
|
|
1869
|
+
flags.json ? JSON.stringify({ schemaVersion: 1, key }) : key,
|
|
1870
|
+
);
|
|
1871
|
+
};
|
|
1872
|
+
},
|
|
1873
|
+
});
|
|
1874
|
+
|
|
1875
|
+
const encryptionRoutes = buildRouteMap({
|
|
1876
|
+
docs: { brief: "Manage application encryption." },
|
|
1877
|
+
routes: { key: encryptionKeyCommand },
|
|
1878
|
+
});
|
|
1879
|
+
|
|
1853
1880
|
const completionShellFlagParameters = {
|
|
1854
1881
|
shell: {
|
|
1855
1882
|
kind: "enum",
|
|
@@ -2446,6 +2473,7 @@ Run npm create beignet@latest (or bun create beignet) to scaffold a new app.`,
|
|
|
2446
2473
|
create: createCommand,
|
|
2447
2474
|
db: dbRoutes,
|
|
2448
2475
|
doctor: doctorCommand,
|
|
2476
|
+
encryption: encryptionRoutes,
|
|
2449
2477
|
explain: explainRoutes,
|
|
2450
2478
|
lint: lintCommand,
|
|
2451
2479
|
map: mapCommand,
|