@stupify/cli 0.0.2 → 0.0.3
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/README.md +60 -0
- package/dist/analysis.d.ts +14 -0
- package/dist/analysis.js +276 -0
- package/dist/batcher.d.ts +3 -0
- package/dist/batcher.js +142 -0
- package/dist/cache.d.ts +2 -0
- package/dist/cache.js +59 -0
- package/dist/candidate-context.d.ts +2 -0
- package/dist/candidate-context.js +40 -0
- package/dist/checks.d.ts +3 -0
- package/dist/checks.js +131 -0
- package/dist/command.d.ts +2 -0
- package/dist/command.js +183 -0
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +53 -0
- package/dist/counter-scout.d.ts +14 -0
- package/dist/counter-scout.js +97 -0
- package/dist/diff.d.ts +1 -0
- package/dist/diff.js +10 -0
- package/dist/experiment.d.ts +1 -0
- package/dist/experiment.js +225 -0
- package/dist/git.d.ts +8 -0
- package/dist/git.js +219 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/model.d.ts +24 -0
- package/dist/model.js +281 -0
- package/dist/prompts.d.ts +5 -0
- package/dist/prompts.js +197 -0
- package/dist/render.d.ts +3 -0
- package/dist/render.js +101 -0
- package/dist/repomix-provider.d.ts +4 -0
- package/dist/repomix-provider.js +145 -0
- package/dist/sem-provider.d.ts +2 -0
- package/dist/sem-provider.js +221 -0
- package/dist/stupify.d.ts +2 -0
- package/dist/stupify.js +387 -0
- package/dist/trace.d.ts +29 -0
- package/dist/trace.js +64 -0
- package/dist/types.d.ts +236 -0
- package/dist/types.js +6 -0
- package/package.json +42 -5
- package/src/analysis.ts +408 -0
- package/src/batcher.ts +198 -0
- package/src/cache.ts +65 -0
- package/src/candidate-context.ts +43 -0
- package/src/checks.ts +132 -0
- package/src/command.ts +218 -0
- package/src/constants.ts +56 -0
- package/src/counter-scout.ts +119 -0
- package/src/diff.ts +9 -0
- package/src/experiment.ts +317 -0
- package/src/git.ts +228 -0
- package/src/index.ts +1 -0
- package/src/model.ts +360 -0
- package/src/prompts.ts +234 -0
- package/src/render.ts +107 -0
- package/src/repomix-provider.ts +163 -0
- package/src/sem-provider.ts +255 -0
- package/src/stupify.ts +598 -0
- package/src/trace.ts +103 -0
- package/src/types.ts +264 -0
- package/bin/stupify.mjs +0 -3
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
export async function runExperiment(configPath) {
|
|
7
|
+
const config = await readConfig(configPath);
|
|
8
|
+
const startedAt = new Date();
|
|
9
|
+
const outputDir = path.join(process.cwd(), "experiments", "results", `${safeSegment(config.name)}-${timestamp(startedAt)}`);
|
|
10
|
+
await mkdir(outputDir, { recursive: true });
|
|
11
|
+
const cwd = resolveCwd(config.cwd);
|
|
12
|
+
const cliPath = path.resolve(process.argv[1] ?? "packages/cli/dist/stupify.js");
|
|
13
|
+
const summaries = [];
|
|
14
|
+
for (const run of config.runs) {
|
|
15
|
+
const args = ensureJson([...config.baseCommand, ...run.args]);
|
|
16
|
+
const command = [process.execPath, cliPath, ...args].join(" ");
|
|
17
|
+
const summary = await runOneExperiment({
|
|
18
|
+
id: run.id,
|
|
19
|
+
args,
|
|
20
|
+
command,
|
|
21
|
+
cwd,
|
|
22
|
+
cliPath,
|
|
23
|
+
outputDir,
|
|
24
|
+
});
|
|
25
|
+
summaries.push(summary);
|
|
26
|
+
await writeFile(path.join(outputDir, "summary.json"), `${JSON.stringify({ name: config.name, cwd, runs: summaries }, null, 2)}\n`);
|
|
27
|
+
}
|
|
28
|
+
return outputDir;
|
|
29
|
+
}
|
|
30
|
+
async function readConfig(configPath) {
|
|
31
|
+
const fullPath = path.resolve(configPath);
|
|
32
|
+
const raw = await readFile(fullPath, "utf8");
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
if (!parsed.name)
|
|
35
|
+
throw new Error("Experiment config requires name.");
|
|
36
|
+
if (!Array.isArray(parsed.baseCommand))
|
|
37
|
+
throw new Error("Experiment config requires baseCommand array.");
|
|
38
|
+
if (!Array.isArray(parsed.runs))
|
|
39
|
+
throw new Error("Experiment config requires runs array.");
|
|
40
|
+
return {
|
|
41
|
+
name: parsed.name,
|
|
42
|
+
cwd: parsed.cwd,
|
|
43
|
+
baseCommand: parsed.baseCommand,
|
|
44
|
+
runs: parsed.runs,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function runOneExperiment(input) {
|
|
48
|
+
const startedAt = Date.now();
|
|
49
|
+
try {
|
|
50
|
+
const { stdout, stderr } = await execFileAsync(process.execPath, [input.cliPath, ...input.args], {
|
|
51
|
+
cwd: input.cwd,
|
|
52
|
+
maxBuffer: 128 * 1024 * 1024,
|
|
53
|
+
});
|
|
54
|
+
if (stderr.trim()) {
|
|
55
|
+
await writeFile(path.join(input.outputDir, `${safeSegment(input.id)}.stderr.txt`), stderr);
|
|
56
|
+
}
|
|
57
|
+
await writeFile(path.join(input.outputDir, `${safeSegment(input.id)}.json`), stdout);
|
|
58
|
+
const report = JSON.parse(stdout);
|
|
59
|
+
const summary = summarizeReport(input.id, input.command, Date.now() - startedAt, report, []);
|
|
60
|
+
await writeFile(path.join(input.outputDir, `${safeSegment(input.id)}-findings.md`), findingsMarkdown(summary, report));
|
|
61
|
+
return summary;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
const details = errorDetails(error);
|
|
65
|
+
if (details.stdout)
|
|
66
|
+
await writeFile(path.join(input.outputDir, `${safeSegment(input.id)}.stdout.txt`), details.stdout);
|
|
67
|
+
if (details.stderr)
|
|
68
|
+
await writeFile(path.join(input.outputDir, `${safeSegment(input.id)}.stderr.txt`), details.stderr);
|
|
69
|
+
const summary = summarizeReport(input.id, input.command, Date.now() - startedAt, {}, [details.message]);
|
|
70
|
+
await writeFile(path.join(input.outputDir, `${safeSegment(input.id)}-findings.md`), findingsMarkdown(summary, {}));
|
|
71
|
+
return summary;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function summarizeReport(id, command, elapsedMs, report, errors) {
|
|
75
|
+
const run = objectValue(report.run);
|
|
76
|
+
const auditStats = objectValue(run.auditStats);
|
|
77
|
+
const traceEvents = arrayValue(run.traceEvents).map(objectValue);
|
|
78
|
+
const findings = arrayValue(report.findings).map(objectValue);
|
|
79
|
+
const targetPreview = arrayValue(run.debugTargets).map(objectValue).map(debugTargetRecord);
|
|
80
|
+
const contextPackEvents = traceEvents.filter((event) => event.name === "context.pack");
|
|
81
|
+
const auditBatchEvents = traceEvents.filter((event) => event.name === "audit.batch");
|
|
82
|
+
const auditInputTokens = auditBatchEvents.map((event) => detailNumber(event.detail, "input_tokens")).filter(isNumber);
|
|
83
|
+
return {
|
|
84
|
+
id,
|
|
85
|
+
command,
|
|
86
|
+
totalMs: numberValue(objectValue(run.timingsMs).total, elapsedMs),
|
|
87
|
+
entitiesScanned: numberValue(run.entitiesScanned, 0),
|
|
88
|
+
targets: numberValue(run.auditedCandidateCount, 0),
|
|
89
|
+
targetsByCheck: recordOfNumbers(run.targetsByCheck),
|
|
90
|
+
auditContext: stringValue(run.auditContext, "unknown"),
|
|
91
|
+
auditPrompt: stringValue(run.auditPrompt, "unknown"),
|
|
92
|
+
repomixFiles: maxNumber(contextPackEvents.map((event) => numberValue(event.count, 0))),
|
|
93
|
+
repomixTokens: maxNumber(contextPackEvents.map((event) => detailNumber(event.detail, "pack_tokens") ?? 0)),
|
|
94
|
+
auditCalls: auditBatchEvents.length,
|
|
95
|
+
auditInputTokens,
|
|
96
|
+
auditMs: numberValue(objectValue(run.timingsMs).audit, 0),
|
|
97
|
+
findings: findings.length,
|
|
98
|
+
uncertain: numberValue(auditStats.uncertain, 0),
|
|
99
|
+
clean: numberValue(auditStats.clean, 0),
|
|
100
|
+
findingsByCheck: countBy(findings, "checkId"),
|
|
101
|
+
uncertainByCheck: {},
|
|
102
|
+
targetPreview,
|
|
103
|
+
errors,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function findingsMarkdown(summary, report) {
|
|
107
|
+
const findings = arrayValue(report.findings).map(objectValue);
|
|
108
|
+
const lines = [
|
|
109
|
+
`# ${summary.id}`,
|
|
110
|
+
"",
|
|
111
|
+
`Runtime: ${Math.round(summary.totalMs / 1000)}s`,
|
|
112
|
+
`Targets: ${summary.targets}`,
|
|
113
|
+
`Findings: ${summary.findings}`,
|
|
114
|
+
`Uncertain: ${summary.uncertain}`,
|
|
115
|
+
"",
|
|
116
|
+
"## Targets",
|
|
117
|
+
...targetMarkdown(summary.targetPreview),
|
|
118
|
+
"",
|
|
119
|
+
"## Findings",
|
|
120
|
+
];
|
|
121
|
+
if (findings.length === 0) {
|
|
122
|
+
lines.push("None.");
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
findings.forEach((finding, index) => {
|
|
126
|
+
lines.push(`${index + 1}. ${stringValue(finding.checkId, "unknown")}`, `why: ${stringValue(finding.why, "")}`, `proof: ${stringValue(finding.proof, "")}`, "Manual label: [good/maybe/bad]", "");
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (summary.errors.length > 0) {
|
|
130
|
+
lines.push("", "## Errors", ...summary.errors.map((error) => `- ${error}`));
|
|
131
|
+
}
|
|
132
|
+
return `${lines.join("\n")}\n`;
|
|
133
|
+
}
|
|
134
|
+
function targetMarkdown(targets) {
|
|
135
|
+
if (targets.length === 0)
|
|
136
|
+
return ["None recorded."];
|
|
137
|
+
return targets.map((target) => (`- ${target.targetId} ${target.checkId} ${target.entityKind ?? "unknown"}/${target.changeKind ?? "unknown"} ${target.entityId}
|
|
138
|
+
reason: ${target.scoutReason ?? ""}`));
|
|
139
|
+
}
|
|
140
|
+
function countBy(items, key) {
|
|
141
|
+
const counts = {};
|
|
142
|
+
for (const item of items) {
|
|
143
|
+
const value = stringValue(item[key], "");
|
|
144
|
+
if (!value)
|
|
145
|
+
continue;
|
|
146
|
+
counts[value] = (counts[value] ?? 0) + 1;
|
|
147
|
+
}
|
|
148
|
+
return counts;
|
|
149
|
+
}
|
|
150
|
+
function recordOfNumbers(value) {
|
|
151
|
+
const input = objectValue(value);
|
|
152
|
+
const output = {};
|
|
153
|
+
for (const [key, count] of Object.entries(input)) {
|
|
154
|
+
if (typeof count === "number" && Number.isFinite(count))
|
|
155
|
+
output[key] = count;
|
|
156
|
+
}
|
|
157
|
+
return output;
|
|
158
|
+
}
|
|
159
|
+
function debugTargetRecord(value) {
|
|
160
|
+
return {
|
|
161
|
+
targetId: stringValue(value.targetId, ""),
|
|
162
|
+
checkId: stringValue(value.checkId, ""),
|
|
163
|
+
entityId: stringValue(value.entityId, ""),
|
|
164
|
+
entityKind: optionalString(value.entityKind),
|
|
165
|
+
changeKind: optionalString(value.changeKind),
|
|
166
|
+
scoutReason: optionalString(value.scoutReason),
|
|
167
|
+
sourceLabel: optionalString(value.sourceLabel),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function optionalString(value) {
|
|
171
|
+
return typeof value === "string" ? value : undefined;
|
|
172
|
+
}
|
|
173
|
+
function ensureJson(args) {
|
|
174
|
+
return args.includes("--json") ? args : [...args, "--json"];
|
|
175
|
+
}
|
|
176
|
+
function resolveCwd(value) {
|
|
177
|
+
if (!value)
|
|
178
|
+
return process.cwd();
|
|
179
|
+
if (value.startsWith("$")) {
|
|
180
|
+
const envName = value.slice(1);
|
|
181
|
+
const envValue = process.env[envName];
|
|
182
|
+
if (!envValue)
|
|
183
|
+
throw new Error(`Experiment cwd env var ${envName} is not set.`);
|
|
184
|
+
return path.resolve(envValue);
|
|
185
|
+
}
|
|
186
|
+
return path.resolve(value);
|
|
187
|
+
}
|
|
188
|
+
function objectValue(value) {
|
|
189
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
190
|
+
}
|
|
191
|
+
function arrayValue(value) {
|
|
192
|
+
return Array.isArray(value) ? value : [];
|
|
193
|
+
}
|
|
194
|
+
function stringValue(value, fallback) {
|
|
195
|
+
return typeof value === "string" ? value : fallback;
|
|
196
|
+
}
|
|
197
|
+
function numberValue(value, fallback) {
|
|
198
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
199
|
+
}
|
|
200
|
+
function detailNumber(value, key) {
|
|
201
|
+
if (typeof value !== "string")
|
|
202
|
+
return null;
|
|
203
|
+
const match = new RegExp(`${key}=([0-9]+)`).exec(value);
|
|
204
|
+
return match ? Number(match[1]) : null;
|
|
205
|
+
}
|
|
206
|
+
function maxNumber(values) {
|
|
207
|
+
return values.length === 0 ? 0 : Math.max(...values);
|
|
208
|
+
}
|
|
209
|
+
function isNumber(value) {
|
|
210
|
+
return value !== null;
|
|
211
|
+
}
|
|
212
|
+
function errorDetails(error) {
|
|
213
|
+
const candidate = objectValue(error);
|
|
214
|
+
return {
|
|
215
|
+
message: error instanceof Error ? error.message : String(error),
|
|
216
|
+
stdout: typeof candidate.stdout === "string" ? candidate.stdout : undefined,
|
|
217
|
+
stderr: typeof candidate.stderr === "string" ? candidate.stderr : undefined,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function timestamp(date) {
|
|
221
|
+
return date.toISOString().replace(/[:.]/g, "-");
|
|
222
|
+
}
|
|
223
|
+
function safeSegment(value) {
|
|
224
|
+
return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "experiment";
|
|
225
|
+
}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type NetDiff, type SourceRange } from "./types.ts";
|
|
2
|
+
export declare function netDiffSince(since: string): Promise<NetDiff>;
|
|
3
|
+
export declare function netDiffForCommit(commit: string): Promise<NetDiff>;
|
|
4
|
+
export declare function netDiffForRecentCommits(count: number): Promise<NetDiff>;
|
|
5
|
+
export declare function sourceRangeSince(since: string): Promise<SourceRange>;
|
|
6
|
+
export declare function sourceRangeForCommit(commit: string): Promise<SourceRange>;
|
|
7
|
+
export declare function sourceRangeForRecentCommits(count: number): Promise<SourceRange>;
|
|
8
|
+
export declare function netDiffFromStdin(text: string): Promise<NetDiff>;
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { sourceId } from "./types.js";
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
export async function netDiffSince(since) {
|
|
6
|
+
const range = await sourceRangeSince(since);
|
|
7
|
+
return netDiff(range.base, range.target, range.label, range.id);
|
|
8
|
+
}
|
|
9
|
+
export async function netDiffForCommit(commit) {
|
|
10
|
+
const range = await sourceRangeForCommit(commit);
|
|
11
|
+
return netDiff(range.base, range.target, range.label, range.id);
|
|
12
|
+
}
|
|
13
|
+
export async function netDiffForRecentCommits(count) {
|
|
14
|
+
const range = await sourceRangeForRecentCommits(count);
|
|
15
|
+
return netDiff(range.base, range.target, range.label, range.id);
|
|
16
|
+
}
|
|
17
|
+
export async function sourceRangeSince(since) {
|
|
18
|
+
const [base, target] = await Promise.all([baseBefore(since), revParse("HEAD")]);
|
|
19
|
+
return sourceRange(base, target, `last ${since}`);
|
|
20
|
+
}
|
|
21
|
+
export async function sourceRangeForCommit(commit) {
|
|
22
|
+
const [base, target, shortTarget, message] = await Promise.all([
|
|
23
|
+
revParse(`${commit}^1`),
|
|
24
|
+
revParse(commit),
|
|
25
|
+
shortCommit(commit),
|
|
26
|
+
commitMessage(commit),
|
|
27
|
+
]);
|
|
28
|
+
return sourceRange(base, target, firstLine(message) || shortTarget, sourceId(shortTarget));
|
|
29
|
+
}
|
|
30
|
+
export async function sourceRangeForRecentCommits(count) {
|
|
31
|
+
const commits = await recentCommits(count);
|
|
32
|
+
if (commits.length === 0)
|
|
33
|
+
throw new Error("No non-merge commits found.");
|
|
34
|
+
const oldest = commits[0];
|
|
35
|
+
const newest = commits[commits.length - 1];
|
|
36
|
+
const [base, target, shortBase, shortTarget] = await Promise.all([
|
|
37
|
+
revParse(`${oldest}^1`),
|
|
38
|
+
revParse(newest),
|
|
39
|
+
shortCommit(`${oldest}^1`),
|
|
40
|
+
shortCommit(newest),
|
|
41
|
+
]);
|
|
42
|
+
return sourceRange(base, target, `${commits.length} recent commits`, sourceId(`range:${shortBase}..${shortTarget}`));
|
|
43
|
+
}
|
|
44
|
+
export async function netDiffFromStdin(text) {
|
|
45
|
+
if (!text.trim())
|
|
46
|
+
throw new Error("No diff received on stdin.");
|
|
47
|
+
return {
|
|
48
|
+
id: sourceId("stdin"),
|
|
49
|
+
label: "stdin",
|
|
50
|
+
base: "stdin",
|
|
51
|
+
target: "stdin",
|
|
52
|
+
text,
|
|
53
|
+
stats: statsFromDiff(text),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function netDiff(base, target, label, id) {
|
|
57
|
+
const [text, stats, shortBase, shortTarget] = await Promise.all([
|
|
58
|
+
diff(base, target),
|
|
59
|
+
diffStats(base, target),
|
|
60
|
+
shortCommit(base),
|
|
61
|
+
shortCommit(target),
|
|
62
|
+
]);
|
|
63
|
+
return {
|
|
64
|
+
id: id ?? sourceId(`net:${shortBase}..${shortTarget}`),
|
|
65
|
+
label,
|
|
66
|
+
base,
|
|
67
|
+
target,
|
|
68
|
+
text,
|
|
69
|
+
stats,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async function sourceRange(base, target, label, id) {
|
|
73
|
+
const [stats, shortBase, shortTarget] = await Promise.all([
|
|
74
|
+
diffStats(base, target),
|
|
75
|
+
shortCommit(base),
|
|
76
|
+
shortCommit(target),
|
|
77
|
+
]);
|
|
78
|
+
return {
|
|
79
|
+
id: id ?? sourceId(`net:${shortBase}..${shortTarget}`),
|
|
80
|
+
label,
|
|
81
|
+
base,
|
|
82
|
+
target,
|
|
83
|
+
stats,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function baseBefore(since) {
|
|
87
|
+
try {
|
|
88
|
+
const { stdout } = await execFileAsync("git", [
|
|
89
|
+
"log",
|
|
90
|
+
"--first-parent",
|
|
91
|
+
"--before",
|
|
92
|
+
since,
|
|
93
|
+
"-1",
|
|
94
|
+
"--format=%H",
|
|
95
|
+
]);
|
|
96
|
+
const commit = stdout.trim();
|
|
97
|
+
if (commit)
|
|
98
|
+
return commit;
|
|
99
|
+
return rootCommit();
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
throw new Error(`Could not resolve base commit before ${since}.`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function rootCommit() {
|
|
106
|
+
try {
|
|
107
|
+
const { stdout } = await execFileAsync("git", ["rev-list", "--max-parents=0", "HEAD"]);
|
|
108
|
+
return stdout.trim().split(/\r?\n/, 1)[0] ?? "";
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
throw new Error("Could not resolve repository root commit.");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function diff(base, target) {
|
|
115
|
+
try {
|
|
116
|
+
const { stdout } = await execFileAsync("git", [
|
|
117
|
+
"diff",
|
|
118
|
+
"--no-ext-diff",
|
|
119
|
+
"--no-color",
|
|
120
|
+
"--unified=8",
|
|
121
|
+
base,
|
|
122
|
+
target,
|
|
123
|
+
"--",
|
|
124
|
+
], { maxBuffer: 128 * 1024 * 1024 });
|
|
125
|
+
if (!stdout.trim())
|
|
126
|
+
throw new Error("empty diff");
|
|
127
|
+
return stdout;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
throw new Error(`No diff found for ${base}..${target}.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function diffStats(base, target) {
|
|
134
|
+
try {
|
|
135
|
+
const { stdout } = await execFileAsync("git", ["diff", "--numstat", base, target, "--"], {
|
|
136
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
137
|
+
});
|
|
138
|
+
return statsFromNumstat(stdout);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return { filesChanged: 0, additions: 0, deletions: 0 };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function statsFromDiff(diffText) {
|
|
145
|
+
const files = new Set();
|
|
146
|
+
let additions = 0;
|
|
147
|
+
let deletions = 0;
|
|
148
|
+
for (const line of diffText.split(/\r?\n/)) {
|
|
149
|
+
const fileMatch = /^diff --git a\/.+ b\/(.+)$/.exec(line);
|
|
150
|
+
if (fileMatch)
|
|
151
|
+
files.add(fileMatch[1]);
|
|
152
|
+
else if (line.startsWith("+") && !line.startsWith("+++"))
|
|
153
|
+
additions += 1;
|
|
154
|
+
else if (line.startsWith("-") && !line.startsWith("---"))
|
|
155
|
+
deletions += 1;
|
|
156
|
+
}
|
|
157
|
+
return { filesChanged: files.size, additions, deletions };
|
|
158
|
+
}
|
|
159
|
+
function statsFromNumstat(numstat) {
|
|
160
|
+
let filesChanged = 0;
|
|
161
|
+
let additions = 0;
|
|
162
|
+
let deletions = 0;
|
|
163
|
+
for (const line of numstat.split(/\r?\n/)) {
|
|
164
|
+
if (!line.trim())
|
|
165
|
+
continue;
|
|
166
|
+
const [added, deleted] = line.split(/\s+/, 3);
|
|
167
|
+
filesChanged += 1;
|
|
168
|
+
additions += Number(added) || 0;
|
|
169
|
+
deletions += Number(deleted) || 0;
|
|
170
|
+
}
|
|
171
|
+
return { filesChanged, additions, deletions };
|
|
172
|
+
}
|
|
173
|
+
async function recentCommits(count) {
|
|
174
|
+
try {
|
|
175
|
+
const { stdout } = await execFileAsync("git", [
|
|
176
|
+
"log",
|
|
177
|
+
"--first-parent",
|
|
178
|
+
"--no-merges",
|
|
179
|
+
"--format=%H",
|
|
180
|
+
`-${count}`,
|
|
181
|
+
]);
|
|
182
|
+
return stdout.split(/\r?\n/).filter(Boolean).reverse();
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
throw new Error(`Could not read last ${count} commits.`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function revParse(rev) {
|
|
189
|
+
try {
|
|
190
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", rev]);
|
|
191
|
+
return stdout.trim();
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
throw new Error(`Could not resolve ${rev}.`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function shortCommit(commit) {
|
|
198
|
+
try {
|
|
199
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--short", commit]);
|
|
200
|
+
return stdout.trim();
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
throw new Error(`Could not resolve commit ${commit}.`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function commitMessage(commit) {
|
|
207
|
+
try {
|
|
208
|
+
const { stdout } = await execFileAsync("git", ["show", "--no-patch", "--format=%B", commit], {
|
|
209
|
+
maxBuffer: 1024 * 1024,
|
|
210
|
+
});
|
|
211
|
+
return stdout;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
throw new Error(`Could not read commit message for ${commit}.`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function firstLine(value) {
|
|
218
|
+
return value.trim().split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
219
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { main } from "./stupify.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { main } from "./stupify.js";
|
package/dist/model.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ModelId } from "./types.ts";
|
|
2
|
+
export type ModelProfile = "scout" | "audit";
|
|
3
|
+
export type LocalModel = Readonly<{
|
|
4
|
+
id: ModelId;
|
|
5
|
+
name: string;
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
profile: ModelProfile;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function loadLocalModels(modelId: ModelId): Promise<{
|
|
10
|
+
scoutModel: Readonly<{
|
|
11
|
+
id: ModelId;
|
|
12
|
+
name: string;
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
profile: ModelProfile;
|
|
15
|
+
}>;
|
|
16
|
+
auditModel: Readonly<{
|
|
17
|
+
id: ModelId;
|
|
18
|
+
name: string;
|
|
19
|
+
baseUrl: string;
|
|
20
|
+
profile: ModelProfile;
|
|
21
|
+
}>;
|
|
22
|
+
}>;
|
|
23
|
+
export declare function firstRunModelBootstrap(modelId: ModelId): Promise<string>;
|
|
24
|
+
export declare function loadLocalModel(modelPath: string, modelId: ModelId, profile?: ModelProfile): Promise<LocalModel>;
|