builder-assistant-engineer 0.2.0
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/LICENSE +21 -0
- package/README.md +250 -0
- package/dist/analyst/prompt.js +19 -0
- package/dist/analyst/retry.js +38 -0
- package/dist/artifacts/diff.js +47 -0
- package/dist/artifacts/lessons.js +32 -0
- package/dist/artifacts/merge.js +70 -0
- package/dist/artifacts/write.js +44 -0
- package/dist/backends/agent-cli.js +174 -0
- package/dist/backends/api.js +171 -0
- package/dist/backends/index.js +10 -0
- package/dist/backends/manual.js +52 -0
- package/dist/backends/sse.js +24 -0
- package/dist/backends/types.js +1 -0
- package/dist/bin.js +3 -0
- package/dist/cli.js +104 -0
- package/dist/commands/context.js +12 -0
- package/dist/commands/init.js +199 -0
- package/dist/commands/next.js +103 -0
- package/dist/commands/plan.js +71 -0
- package/dist/commands/replan.js +37 -0
- package/dist/commands/review.js +37 -0
- package/dist/commands/shared.js +40 -0
- package/dist/commands/status.js +95 -0
- package/dist/config/commands.js +85 -0
- package/dist/config/schema.js +43 -0
- package/dist/config/settings.js +8 -0
- package/dist/config/store.js +38 -0
- package/dist/core/bash.js +105 -0
- package/dist/core/errors.js +14 -0
- package/dist/core/fs.js +35 -0
- package/dist/core/git.js +104 -0
- package/dist/core/gitignore.js +14 -0
- package/dist/core/json.js +13 -0
- package/dist/core/paths.js +14 -0
- package/dist/core/process.js +56 -0
- package/dist/core/prompt-loader.js +27 -0
- package/dist/core/state.js +27 -0
- package/dist/core/which.js +24 -0
- package/dist/detect/agents.js +17 -0
- package/dist/detect/mode.js +11 -0
- package/dist/digest/baseline.js +192 -0
- package/dist/digest/budget.js +31 -0
- package/dist/digest/files.js +167 -0
- package/dist/digest/format.js +18 -0
- package/dist/digest/git.js +19 -0
- package/dist/digest/index.js +100 -0
- package/dist/digest/manifests.js +133 -0
- package/dist/digest/read.js +28 -0
- package/dist/digest/redact.js +37 -0
- package/dist/digest/render.js +138 -0
- package/dist/digest/select.js +116 -0
- package/dist/digest/todos.js +16 -0
- package/dist/digest/tree.js +55 -0
- package/dist/digest/walk.js +72 -0
- package/dist/gates/capture.js +145 -0
- package/dist/gates/contract.js +339 -0
- package/dist/gates/enforce.js +143 -0
- package/dist/gates/findings.js +17 -0
- package/dist/gates/gate.js +315 -0
- package/dist/gates/ignore-rules.js +104 -0
- package/dist/gates/protected-files.js +311 -0
- package/dist/gates/regression.js +219 -0
- package/dist/gates/reports.js +349 -0
- package/dist/gates/results.js +295 -0
- package/dist/gates/runners.js +189 -0
- package/dist/gates/state-guard.js +56 -0
- package/dist/i18n/en.js +288 -0
- package/dist/i18n/es.js +288 -0
- package/dist/i18n/index.js +18 -0
- package/dist/interview/adaptive.js +69 -0
- package/dist/interview/brief.js +27 -0
- package/dist/interview/questions.js +18 -0
- package/dist/interview/render.js +19 -0
- package/dist/interview/reply.js +34 -0
- package/dist/next/attempts.js +151 -0
- package/dist/next/start.js +179 -0
- package/dist/plan/continuation.js +40 -0
- package/dist/plan/evidence.js +239 -0
- package/dist/plan/filter.js +20 -0
- package/dist/plan/parser.js +283 -0
- package/dist/plan/prior.js +32 -0
- package/dist/plan/questions.js +48 -0
- package/dist/plan/repair.js +33 -0
- package/dist/plan/report.js +37 -0
- package/dist/plan/run.js +191 -0
- package/dist/prompts/analyst.md +101 -0
- package/dist/prompts/continue.md +11 -0
- package/dist/prompts/fix-format.md +13 -0
- package/dist/prompts/fix-paths.md +23 -0
- package/dist/prompts/lesson.md +18 -0
- package/dist/prompts/retry.md +9 -0
- package/dist/prompts/review.md +36 -0
- package/dist/prompts/task.md +9 -0
- package/dist/review/changes.js +177 -0
- package/dist/review/diff.js +115 -0
- package/dist/review/integrity.js +150 -0
- package/dist/review/mechanical.js +62 -0
- package/dist/review/parse.js +40 -0
- package/dist/review/reviewer.js +23 -0
- package/dist/review/run.js +134 -0
- package/dist/review/scope.js +55 -0
- package/dist/review/secrets.js +163 -0
- package/dist/review/snapshot.js +38 -0
- package/dist/review/tests.js +140 -0
- package/dist/tasks/attempts.js +71 -0
- package/dist/tasks/checks.js +181 -0
- package/dist/tasks/frontmatter.js +58 -0
- package/dist/tasks/graph.js +27 -0
- package/dist/tasks/handoff.js +62 -0
- package/dist/tasks/learn.js +128 -0
- package/dist/tasks/load.js +37 -0
- package/dist/tasks/metrics.js +40 -0
- package/dist/tasks/runs.js +12 -0
- package/dist/tasks/schema.js +96 -0
- package/dist/tasks/select.js +15 -0
- package/dist/tasks/shell-words.js +176 -0
- package/dist/tasks/status.js +13 -0
- package/dist/tasks/verify.js +181 -0
- package/dist/ui/clack.js +46 -0
- package/dist/ui/prompter.js +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { bashPath } from "../core/bash.js";
|
|
3
|
+
import { UserError } from "../core/errors.js";
|
|
4
|
+
import { readTextIfExists } from "../core/fs.js";
|
|
5
|
+
import { isTestFile } from "../digest/baseline.js";
|
|
6
|
+
import { t } from "../i18n/index.js";
|
|
7
|
+
import { countIntegrity, hasComparableCounts, testsGrew } from "../review/integrity.js";
|
|
8
|
+
import { capturedTask, formatFindings, reviewTask } from "../review/run.js";
|
|
9
|
+
import { reductionPaths } from "../review/scope.js";
|
|
10
|
+
import { allowlistProblems, trivialityProblems } from "../tasks/checks.js";
|
|
11
|
+
import { logLines, MAX_LOG_LINES } from "../tasks/handoff.js";
|
|
12
|
+
import { parseTask } from "../tasks/schema.js";
|
|
13
|
+
import { findUnsafe, runVerification } from "../tasks/verify.js";
|
|
14
|
+
import { enforceContract } from "./enforce.js";
|
|
15
|
+
import { checkRegressions, formatCounts, runSuite, } from "./regression.js";
|
|
16
|
+
import { guardState } from "./state-guard.js";
|
|
17
|
+
const OUTPUT_TAIL = 4_000;
|
|
18
|
+
export async function runGate(ctx, run) {
|
|
19
|
+
const { capture, checks, approval, acceptance } = run;
|
|
20
|
+
const skips = [...capture.skips];
|
|
21
|
+
const fail = (stage, report, reason, retryable = true) => ({
|
|
22
|
+
passed: false,
|
|
23
|
+
retryable,
|
|
24
|
+
report,
|
|
25
|
+
stage,
|
|
26
|
+
...(reason ? { reason } : {}),
|
|
27
|
+
regressions: [],
|
|
28
|
+
skips,
|
|
29
|
+
});
|
|
30
|
+
const tampered = run.tampered.splice(0);
|
|
31
|
+
if (tampered.length > 0) {
|
|
32
|
+
const reason = t("state.tampered", { files: tampered.join(", ") });
|
|
33
|
+
ctx.prompter.warn(reason);
|
|
34
|
+
const restored = await enforceContract(ctx, capture, acceptance);
|
|
35
|
+
return fail("contract", joinSections([`## ${t("contract.title")}\n\n${reason}`, restored.report]), reason);
|
|
36
|
+
}
|
|
37
|
+
const contract = await enforceContract(ctx, capture, acceptance);
|
|
38
|
+
if (contract.blocked)
|
|
39
|
+
return fail("contract", contract.report, t("contract.failed"));
|
|
40
|
+
const recheck = async () => {
|
|
41
|
+
const changed = await guard.verify();
|
|
42
|
+
const again = await enforceContract(ctx, capture, acceptance);
|
|
43
|
+
if (changed.length === 0 && !again.blocked)
|
|
44
|
+
return undefined;
|
|
45
|
+
const state = changed.length > 0 ? t("state.tampered", { files: changed.join(", ") }) : "";
|
|
46
|
+
if (state)
|
|
47
|
+
ctx.prompter.warn(state);
|
|
48
|
+
return joinSections([state, again.report]);
|
|
49
|
+
};
|
|
50
|
+
const bash = await bashPath();
|
|
51
|
+
const refused = refusal(checks, {
|
|
52
|
+
unattended: run.unattended,
|
|
53
|
+
allow: capture.config.verify.allow,
|
|
54
|
+
...(bash ? { bash } : {}),
|
|
55
|
+
});
|
|
56
|
+
if (refused || !bash) {
|
|
57
|
+
const reason = refused ?? t("verify.noBash");
|
|
58
|
+
ctx.prompter.warn(reason);
|
|
59
|
+
return fail("refused", reason, reason, false);
|
|
60
|
+
}
|
|
61
|
+
const baseline = suiteBaseline(capture);
|
|
62
|
+
const excluded = new Set(baseline?.excluded ?? []);
|
|
63
|
+
const suite = baseline?.skipped ? [] : checks.suite.filter((item) => !excluded.has(item.command));
|
|
64
|
+
if (!approval.granted) {
|
|
65
|
+
const all = [...new Set([...checks.commands, ...suite.map((item) => item.command)])];
|
|
66
|
+
ctx.prompter.note(all.map((command) => `$ ${command}`).join("\n"), t("verify.commands"));
|
|
67
|
+
for (const item of findUnsafe(all))
|
|
68
|
+
ctx.prompter.warn(t("verify.unsafeWarning", item));
|
|
69
|
+
if (!ctx.flags.yes && !(await ctx.prompter.confirm(t("verify.confirm"), true))) {
|
|
70
|
+
ctx.prompter.warn(t("verify.declined"));
|
|
71
|
+
return fail("declined", t("verify.declined"), t("verify.declined"), false);
|
|
72
|
+
}
|
|
73
|
+
approval.granted = true;
|
|
74
|
+
}
|
|
75
|
+
const fix = capturedTask(capture).meta.tests === "fix";
|
|
76
|
+
let guard = await guardState(ctx.cwd);
|
|
77
|
+
const regression = suite.length > 0
|
|
78
|
+
? await regressionStage(ctx, suite, baseline, {
|
|
79
|
+
fix,
|
|
80
|
+
allowSkip: run.allowSkip,
|
|
81
|
+
timeout: capture.config.gates.timeoutMinutes * 60_000,
|
|
82
|
+
})
|
|
83
|
+
: undefined;
|
|
84
|
+
if (run.allowSkip) {
|
|
85
|
+
for (const item of regression?.unread ?? []) {
|
|
86
|
+
const what = t("regression.unreadSkipped", { command: item.command });
|
|
87
|
+
skips.push(`regression: ${what}`);
|
|
88
|
+
ctx.prompter.warn(t("skip.used", { what }));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const sections = [contract.report, regression?.report];
|
|
92
|
+
const afterSuite = await recheck();
|
|
93
|
+
if (afterSuite)
|
|
94
|
+
return fail("contract", joinSections([...sections, afterSuite]), t("contract.failed"));
|
|
95
|
+
if (regression && !regression.passed) {
|
|
96
|
+
return {
|
|
97
|
+
...fail("regression", joinSections(sections), t("regression.failed")),
|
|
98
|
+
regressions: regression.regressions.map((check) => check.command),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const checksRun = regression?.checks ?? [];
|
|
102
|
+
const planned = reductionPaths(capturedTask(capture)).some((path) => isTestFile(path));
|
|
103
|
+
const integrity = countIntegrity(checksRun, acceptance, planned);
|
|
104
|
+
if (integrity.length > 0) {
|
|
105
|
+
const list = formatFindings(integrity);
|
|
106
|
+
ctx.prompter.note(list, t("integrity.title"));
|
|
107
|
+
sections.push(`## ${t("integrity.title")}\n\n${list}`);
|
|
108
|
+
}
|
|
109
|
+
if (integrity.some((finding) => finding.severity === "blocker")) {
|
|
110
|
+
return fail("integrity", joinSections(sections), t("integrity.failed"));
|
|
111
|
+
}
|
|
112
|
+
const known = new Map(checksRun.map((check) => [check.command, check]));
|
|
113
|
+
const excused = new Set([
|
|
114
|
+
...(fix ? [] : (regression?.preexisting ?? []).map((check) => check.command)),
|
|
115
|
+
...excluded,
|
|
116
|
+
]);
|
|
117
|
+
guard = await guardState(ctx.cwd);
|
|
118
|
+
const verification = await runVerification(ctx.cwd, checks.script, {
|
|
119
|
+
bash,
|
|
120
|
+
timeoutMs: capture.config.gates.timeoutMinutes * 60_000,
|
|
121
|
+
onOutput: ctx.print,
|
|
122
|
+
known,
|
|
123
|
+
excused,
|
|
124
|
+
});
|
|
125
|
+
sections.push(verificationReport(verification));
|
|
126
|
+
const afterVerification = await recheck();
|
|
127
|
+
if (afterVerification) {
|
|
128
|
+
return fail("contract", joinSections([...sections, afterVerification]), t("contract.failed"));
|
|
129
|
+
}
|
|
130
|
+
if (!verification.passed) {
|
|
131
|
+
const reason = t("verify.failed", {
|
|
132
|
+
command: verification.failed ?? "Verification",
|
|
133
|
+
code: verification.exitCode,
|
|
134
|
+
});
|
|
135
|
+
ctx.prompter.warn(reason);
|
|
136
|
+
return fail("verification", joinSections(sections), reason);
|
|
137
|
+
}
|
|
138
|
+
ctx.prompter.success(t("verify.passed"));
|
|
139
|
+
const handoff = handoffProblem(await currentTask(ctx, capture));
|
|
140
|
+
if (handoff) {
|
|
141
|
+
ctx.prompter.warn(handoff);
|
|
142
|
+
sections.push(`## Handoff note\n\n${handoff}`);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
ctx.prompter.success(t("handoff.passed"));
|
|
146
|
+
}
|
|
147
|
+
const notes = [...stillFailing(verification, known), ...contract.notes];
|
|
148
|
+
const review = await safeReview(ctx, capture, notes, acceptance, {
|
|
149
|
+
testsGrew: testsGrew(checksRun),
|
|
150
|
+
counted: hasComparableCounts(checksRun),
|
|
151
|
+
secrets: capture.config.secrets,
|
|
152
|
+
}, evidence(checksRun, verification));
|
|
153
|
+
if (review.reason)
|
|
154
|
+
ctx.prompter.warn(review.reason);
|
|
155
|
+
const findings = formatFindings(review.findings);
|
|
156
|
+
if (findings)
|
|
157
|
+
ctx.prompter.note(findings, t("review.findings"));
|
|
158
|
+
sections.push(`## Review\n\n${review.status}\n\n${findings}`.trim(), acceptedReport(acceptance));
|
|
159
|
+
if (review.status === "fail")
|
|
160
|
+
return fail("review", joinSections(sections), review.reason);
|
|
161
|
+
if (review.status !== "pass") {
|
|
162
|
+
const reason = review.reason ?? t("review.noVerdict");
|
|
163
|
+
if (!run.allowSkip) {
|
|
164
|
+
return fail("review", joinSections([...sections, reason]), reason, review.status === "error");
|
|
165
|
+
}
|
|
166
|
+
skips.push(`review: ${reason}`);
|
|
167
|
+
ctx.prompter.warn(t("skip.used", { what: reason }));
|
|
168
|
+
}
|
|
169
|
+
if (review.status === "pass")
|
|
170
|
+
ctx.prompter.success(t("review.passed"));
|
|
171
|
+
return {
|
|
172
|
+
passed: true,
|
|
173
|
+
retryable: false,
|
|
174
|
+
report: joinSections([...sections, skippedReport(skips)]),
|
|
175
|
+
regressions: [],
|
|
176
|
+
skips,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
export function refusal(checks, policy) {
|
|
180
|
+
if (checks.commands.length === 0)
|
|
181
|
+
return t("verify.none");
|
|
182
|
+
if (!policy.bash)
|
|
183
|
+
return t("verify.noBash");
|
|
184
|
+
const suite = checks.suite.map((item) => item.command);
|
|
185
|
+
const lines = [
|
|
186
|
+
...trivialityProblems(checks.script).map((problem) => problemText(problem, "verify")),
|
|
187
|
+
...(policy.unattended
|
|
188
|
+
? [
|
|
189
|
+
...allowlistProblems(checks.script, policy.allow).map((problem) => problemText(problem, "verify")),
|
|
190
|
+
...allowlistProblems(suite, policy.allow).map((problem) => problemText(problem, "regression")),
|
|
191
|
+
]
|
|
192
|
+
: []),
|
|
193
|
+
];
|
|
194
|
+
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
195
|
+
}
|
|
196
|
+
function problemText(problem, source) {
|
|
197
|
+
const vars = { command: problem.command };
|
|
198
|
+
if (problem.reason === "masks")
|
|
199
|
+
return t("verify.masks", vars);
|
|
200
|
+
if (problem.reason === "trivial")
|
|
201
|
+
return t("verify.trivial", vars);
|
|
202
|
+
const why = t(problem.reason === "dynamic" ? "verify.dynamic" : "verify.unknown");
|
|
203
|
+
return t(source === "verify" ? "verify.notAllowed" : "regression.notAllowed", {
|
|
204
|
+
...vars,
|
|
205
|
+
why,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
async function safeReview(ctx, capture, notes, acceptance, facts, proof) {
|
|
209
|
+
try {
|
|
210
|
+
return await reviewTask(ctx, capture, notes, acceptance, facts, proof);
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
if (!(error instanceof UserError))
|
|
214
|
+
throw error;
|
|
215
|
+
return { status: "error", findings: [], reason: t("review.error", { details: error.message }) };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function regressionStage(ctx, suite, baseline, options) {
|
|
219
|
+
const results = await runSuite(ctx.cwd, suite, ctx.print, options.timeout);
|
|
220
|
+
const check = checkRegressions(results, baseline, options.fix, options.allowSkip);
|
|
221
|
+
for (const item of check.regressions)
|
|
222
|
+
ctx.prompter.warn(regressionMessage(item));
|
|
223
|
+
for (const item of check.preexisting) {
|
|
224
|
+
ctx.prompter.info(t("regression.stillFailing", { command: item.command, code: item.exitCode }));
|
|
225
|
+
}
|
|
226
|
+
if (check.passed)
|
|
227
|
+
ctx.prompter.success(t("regression.passed"));
|
|
228
|
+
return check;
|
|
229
|
+
}
|
|
230
|
+
function regressionMessage(check) {
|
|
231
|
+
const vars = { command: check.command, code: check.exitCode };
|
|
232
|
+
if (check.verdict === "mustPass")
|
|
233
|
+
return t("regression.mustPass", vars);
|
|
234
|
+
if (check.verdict === "unknown") {
|
|
235
|
+
return t(check.unrecognized ? "regression.unknownRunner" : "regression.unknown", vars);
|
|
236
|
+
}
|
|
237
|
+
if (check.verdict === "uncomparable")
|
|
238
|
+
return t("regression.uncomparable", vars);
|
|
239
|
+
if (check.verdict === "regression" && check.before && check.before.exitCode !== 0) {
|
|
240
|
+
return t("regression.worse", {
|
|
241
|
+
command: check.command,
|
|
242
|
+
now: check.counts ? formatCounts(check.counts) : "",
|
|
243
|
+
before: check.before.counts ? formatCounts(check.before.counts) : "",
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return t("regression.found", vars);
|
|
247
|
+
}
|
|
248
|
+
function suiteBaseline(capture) {
|
|
249
|
+
if (capture.config.gates.regression !== "full")
|
|
250
|
+
return undefined;
|
|
251
|
+
return capture.baseline;
|
|
252
|
+
}
|
|
253
|
+
function acceptedReport(acceptance) {
|
|
254
|
+
if (acceptance.accepted.length === 0)
|
|
255
|
+
return "";
|
|
256
|
+
return `## ${t("findings.acceptedTitle")}\n\n${formatFindings(acceptance.accepted)}`;
|
|
257
|
+
}
|
|
258
|
+
function skippedReport(skips) {
|
|
259
|
+
if (skips.length === 0)
|
|
260
|
+
return "";
|
|
261
|
+
return `## ${t("skip.title")}\n\n${skips.map((skip) => `- ${skip}`).join("\n")}`;
|
|
262
|
+
}
|
|
263
|
+
function evidence(checks, verification) {
|
|
264
|
+
const suite = checks.map((check) => {
|
|
265
|
+
const counts = check.counts ? `; ${formatCounts(check.counts)}` : "";
|
|
266
|
+
return `- \`${check.command}\` (${check.key}): exit ${check.exitCode}${counts}; ${check.verdict}`;
|
|
267
|
+
});
|
|
268
|
+
const excused = verification.tolerated.map((command) => `- \`${command}\`: already failed before the task`);
|
|
269
|
+
return [
|
|
270
|
+
suite.length > 0 ? `Project commands:\n${suite.join("\n")}` : "",
|
|
271
|
+
`Verification block, run as one bash script with set -Eeuo pipefail, exit ${verification.exitCode}${verification.passed ? " (every line exited 0)" : ""}:\n\`\`\`sh\n${verification.script}\n\`\`\``,
|
|
272
|
+
excused.length > 0
|
|
273
|
+
? `Lines excused because they already failed before the task:\n${excused.join("\n")}`
|
|
274
|
+
: "",
|
|
275
|
+
]
|
|
276
|
+
.filter(Boolean)
|
|
277
|
+
.join("\n\n");
|
|
278
|
+
}
|
|
279
|
+
function verificationReport(verification) {
|
|
280
|
+
const output = verification.output.slice(-OUTPUT_TAIL).trim();
|
|
281
|
+
return [
|
|
282
|
+
"## Verification",
|
|
283
|
+
`\`\`\`sh\n${verification.script}\n\`\`\``,
|
|
284
|
+
`exit ${verification.exitCode}`,
|
|
285
|
+
`\`\`\`text\n${output}\n\`\`\``,
|
|
286
|
+
].join("\n\n");
|
|
287
|
+
}
|
|
288
|
+
function stillFailing(verification, known) {
|
|
289
|
+
return verification.tolerated.map((command) => ({
|
|
290
|
+
severity: "minor",
|
|
291
|
+
message: t("verify.stillFailing", { command, code: known.get(command)?.exitCode ?? 1 }),
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
294
|
+
async function currentTask(ctx, capture) {
|
|
295
|
+
const fallback = capturedTask(capture);
|
|
296
|
+
const text = await readTextIfExists(join(ctx.cwd, ...capture.path.split("/")));
|
|
297
|
+
if (text === undefined)
|
|
298
|
+
return fallback;
|
|
299
|
+
try {
|
|
300
|
+
return parseTask(capture.path, text);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return fallback;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function handoffProblem(task) {
|
|
307
|
+
const count = logLines(task).length;
|
|
308
|
+
const vars = { path: task.path, max: MAX_LOG_LINES, count };
|
|
309
|
+
if (count === 0)
|
|
310
|
+
return t("handoff.missing", vars);
|
|
311
|
+
return count > MAX_LOG_LINES ? t("handoff.tooLong", vars) : undefined;
|
|
312
|
+
}
|
|
313
|
+
function joinSections(sections) {
|
|
314
|
+
return sections.filter(Boolean).join("\n\n");
|
|
315
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { isAbsolute, join } from "node:path";
|
|
4
|
+
import ignore, {} from "ignore";
|
|
5
|
+
import { readTextIfExists } from "../core/fs.js";
|
|
6
|
+
import { git, gitPaths } from "../core/git.js";
|
|
7
|
+
const IGNORE_CASE = process.platform !== "linux";
|
|
8
|
+
export async function captureIgnore(cwd) {
|
|
9
|
+
const globalFile = await globalExcludes(cwd);
|
|
10
|
+
const infoFile = (await git(cwd, ["rev-parse", "--git-path", "info/exclude"]))?.trim();
|
|
11
|
+
const listed = (await gitPaths(cwd, [
|
|
12
|
+
"ls-files",
|
|
13
|
+
"--cached",
|
|
14
|
+
"--others",
|
|
15
|
+
"--exclude-standard",
|
|
16
|
+
"--",
|
|
17
|
+
":(glob)**/.gitignore",
|
|
18
|
+
])) ?? [];
|
|
19
|
+
const perDirectory = await Promise.all([...new Set(listed)].map(async (path) => ({
|
|
20
|
+
dir: path.split("/").slice(0, -1).join("/"),
|
|
21
|
+
text: (await readTextIfExists(join(cwd, ...path.split("/")))) ?? "",
|
|
22
|
+
})));
|
|
23
|
+
const shared = await Promise.all([globalFile, infoFile && (isAbsolute(infoFile) ? infoFile : join(cwd, infoFile))].map(async (path) => ({ dir: "", text: (path && (await readTextIfExists(path))) || "" })));
|
|
24
|
+
return [...shared, ...perDirectory.sort((a, b) => depth(a.dir) - depth(b.dir))].filter((source) => source.text.trim() !== "");
|
|
25
|
+
}
|
|
26
|
+
export function ignoreMatcher(sources) {
|
|
27
|
+
const layers = sources.map((source) => ({
|
|
28
|
+
dir: source.dir,
|
|
29
|
+
matcher: ignore({ allowRelativePaths: true, ignorecase: IGNORE_CASE }).add(source.text),
|
|
30
|
+
}));
|
|
31
|
+
const decide = (path, isDir) => {
|
|
32
|
+
let ignored = false;
|
|
33
|
+
for (const layer of layers) {
|
|
34
|
+
if (layer.dir && !path.startsWith(`${layer.dir}/`))
|
|
35
|
+
continue;
|
|
36
|
+
const relative = layer.dir ? path.slice(layer.dir.length + 1) : path;
|
|
37
|
+
const result = layer.matcher.test(isDir ? `${relative}/` : relative);
|
|
38
|
+
if (result.ignored)
|
|
39
|
+
ignored = true;
|
|
40
|
+
else if (result.unignored)
|
|
41
|
+
ignored = false;
|
|
42
|
+
}
|
|
43
|
+
return ignored;
|
|
44
|
+
};
|
|
45
|
+
return (path, isDir) => {
|
|
46
|
+
const parts = path.split("/");
|
|
47
|
+
for (let index = 1; index < parts.length; index++) {
|
|
48
|
+
if (decide(parts.slice(0, index).join("/"), true))
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return decide(path, isDir);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export async function untrackedFiles(cwd, isIgnored) {
|
|
55
|
+
const entries = await gitPaths(cwd, ["ls-files", "--others", "--directory", "--", "."]);
|
|
56
|
+
if (!entries)
|
|
57
|
+
return undefined;
|
|
58
|
+
const found = [];
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (!entry.endsWith("/")) {
|
|
61
|
+
if (!isIgnored(entry, false))
|
|
62
|
+
found.push(entry);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const dir = entry.slice(0, -1);
|
|
66
|
+
if (!isIgnored(dir, true))
|
|
67
|
+
found.push(...(await walk(cwd, dir, isIgnored)));
|
|
68
|
+
}
|
|
69
|
+
return found.sort();
|
|
70
|
+
}
|
|
71
|
+
async function walk(cwd, dir, isIgnored) {
|
|
72
|
+
const entries = await listDir(join(cwd, ...dir.split("/")));
|
|
73
|
+
const found = [];
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
const path = `${dir}/${entry.name}`;
|
|
76
|
+
if (entry.isDirectory()) {
|
|
77
|
+
if (entry.name !== ".git" && !isIgnored(path, true)) {
|
|
78
|
+
found.push(...(await walk(cwd, path, isIgnored)));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if ((entry.isFile() || entry.isSymbolicLink()) && !isIgnored(path, false)) {
|
|
82
|
+
found.push(path);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return found;
|
|
86
|
+
}
|
|
87
|
+
async function globalExcludes(cwd) {
|
|
88
|
+
const configured = (await git(cwd, ["config", "--path", "--get", "core.excludesFile"]))?.trim();
|
|
89
|
+
if (configured)
|
|
90
|
+
return configured;
|
|
91
|
+
const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
92
|
+
return join(base, "git", "ignore");
|
|
93
|
+
}
|
|
94
|
+
async function listDir(path) {
|
|
95
|
+
try {
|
|
96
|
+
return await readdir(path, { withFileTypes: true });
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function depth(dir) {
|
|
103
|
+
return dir === "" ? 0 : dir.split("/").length;
|
|
104
|
+
}
|