@stupify/cli 0.0.15 → 0.1.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.
Files changed (74) hide show
  1. package/.review/CORPUS.md +73 -0
  2. package/.review/REVIEW-PROMPT.md +52 -0
  3. package/.review/RUBRIC.md +46 -0
  4. package/LICENSE +1 -1
  5. package/README.md +41 -39
  6. package/package.json +24 -25
  7. package/src/cli.ts +358 -0
  8. package/src/review-sweep.ts +492 -0
  9. package/dist/analysis.d.ts +0 -16
  10. package/dist/analysis.js +0 -165
  11. package/dist/cache.d.ts +0 -2
  12. package/dist/cache.js +0 -57
  13. package/dist/checks.d.ts +0 -4
  14. package/dist/checks.js +0 -228
  15. package/dist/command.d.ts +0 -2
  16. package/dist/command.js +0 -147
  17. package/dist/constants.d.ts +0 -4
  18. package/dist/constants.js +0 -53
  19. package/dist/counter-scout.d.ts +0 -21
  20. package/dist/counter-scout.js +0 -167
  21. package/dist/diff.d.ts +0 -1
  22. package/dist/diff.js +0 -10
  23. package/dist/doctor.d.ts +0 -4
  24. package/dist/doctor.js +0 -131
  25. package/dist/git.d.ts +0 -12
  26. package/dist/git.js +0 -298
  27. package/dist/hooks.d.ts +0 -3
  28. package/dist/hooks.js +0 -117
  29. package/dist/index.d.ts +0 -1
  30. package/dist/index.js +0 -1
  31. package/dist/model.d.ts +0 -11
  32. package/dist/model.js +0 -296
  33. package/dist/prompts.d.ts +0 -8
  34. package/dist/prompts.js +0 -89
  35. package/dist/render.d.ts +0 -3
  36. package/dist/render.js +0 -151
  37. package/dist/repomix-provider.d.ts +0 -12
  38. package/dist/repomix-provider.js +0 -196
  39. package/dist/search-bench.d.ts +0 -1
  40. package/dist/search-bench.js +0 -677
  41. package/dist/search-profile.d.ts +0 -6
  42. package/dist/search-profile.js +0 -73
  43. package/dist/sem-provider.d.ts +0 -2
  44. package/dist/sem-provider.js +0 -252
  45. package/dist/stupify.d.ts +0 -38
  46. package/dist/stupify.js +0 -474
  47. package/dist/trace.d.ts +0 -31
  48. package/dist/trace.js +0 -86
  49. package/dist/types.d.ts +0 -328
  50. package/dist/types.js +0 -6
  51. package/dist/ui.d.ts +0 -34
  52. package/dist/ui.js +0 -143
  53. package/src/analysis.ts +0 -220
  54. package/src/cache.ts +0 -63
  55. package/src/checks.ts +0 -231
  56. package/src/command.ts +0 -173
  57. package/src/constants.ts +0 -56
  58. package/src/counter-scout.ts +0 -195
  59. package/src/diff.ts +0 -9
  60. package/src/doctor.ts +0 -140
  61. package/src/git.ts +0 -306
  62. package/src/hooks.ts +0 -134
  63. package/src/index.ts +0 -1
  64. package/src/model.ts +0 -367
  65. package/src/prompts.ts +0 -100
  66. package/src/render.ts +0 -154
  67. package/src/repomix-provider.ts +0 -219
  68. package/src/search-bench.ts +0 -783
  69. package/src/search-profile.ts +0 -89
  70. package/src/sem-provider.ts +0 -297
  71. package/src/stupify.ts +0 -571
  72. package/src/trace.ts +0 -126
  73. package/src/types.ts +0 -348
  74. package/src/ui.ts +0 -187
@@ -1,167 +0,0 @@
1
- const MAX_COUNTER_EXAMPLES_PER_CHECK = 20;
2
- export function counterScoutTargets(changeSet, checks, maxTargets) {
3
- return counterScoutPlan(changeSet, checks, maxTargets).targets;
4
- }
5
- export function counterScoutPlan(changeSet, checks, maxTargets) {
6
- const buckets = runSignalCounters(changeSet, checks);
7
- const targets = [];
8
- let cursor = 0;
9
- while (targets.length < maxTargets && buckets.some((bucket) => cursor < bucket.examples.length)) {
10
- for (const bucket of buckets) {
11
- const signal = bucket.examples[cursor];
12
- if (!signal)
13
- continue;
14
- targets.push({
15
- sourceId: changeSet.id,
16
- targetId: `t${String(targets.length + 1).padStart(3, "0")}`,
17
- entityId: signal.entityId,
18
- checkId: signal.checkId,
19
- reason: signal.reasonCode,
20
- });
21
- if (targets.length >= maxTargets)
22
- break;
23
- }
24
- cursor += 1;
25
- }
26
- return {
27
- buckets,
28
- totalSignals: buckets.reduce((sum, bucket) => sum + bucket.total, 0),
29
- maxTargets,
30
- targets,
31
- };
32
- }
33
- export function runSignalCounters(changeSet, checks) {
34
- return checks
35
- .map((check) => {
36
- const signals = changeSet.changes.flatMap((change) => {
37
- const reasonCode = reasonForCheck(check.id, change);
38
- return reasonCode ? [{ checkId: check.id, entityId: change.entityId, reasonCode }] : [];
39
- });
40
- return {
41
- checkId: check.id,
42
- total: signals.length,
43
- examples: signals.slice(0, MAX_COUNTER_EXAMPLES_PER_CHECK),
44
- };
45
- })
46
- .filter((bucket) => bucket.total > 0);
47
- }
48
- function reasonForCheck(checkId, change) {
49
- if (!isSearchableSourceChange(change))
50
- return null;
51
- const haystack = `${change.entityName}\n${change.entityType}\n${change.filePath}\n${change.afterContent ?? ""}`.toLowerCase();
52
- const changed = change.changeType === "added" || change.changeType === "modified";
53
- if (!changed)
54
- return null;
55
- switch (checkId) {
56
- case "duplicated_schema":
57
- return isDuplicatedSchemaCandidate(change) ? "local_schemaish_copy" : null;
58
- case "unnecessary_complexity":
59
- return /\b(helper|wrapper|service|provider|manager|factory|adapter|resolver|coordinator)\b/i.test(change.entityName)
60
- ? "new_abstraction_name"
61
- : null;
62
- case "fake_precision_windowing":
63
- return /\b(token|budget|window|batch|ratio|estimate|counter|count|limit)\b/i.test(haystack)
64
- ? "precision_accounting_terms"
65
- : null;
66
- case "coauthored_slop":
67
- return /\b(coauhtoried|coauthored|co-authored|co-authored-by)\b/i.test(haystack)
68
- ? "coauthor_text"
69
- : null;
70
- case "mega_file":
71
- return change.entityType === "chunk" && /lines\s+\d+-\d+/i.test(change.entityName)
72
- ? "large_changed_chunk"
73
- : null;
74
- case "over_commenting":
75
- return overCommentingSignal(change)
76
- ? "comment_lines_increased"
77
- : null;
78
- case "lint_bypass":
79
- return lintBypassSignal(change.afterContent ?? "")
80
- ? "lint_or_type_bypass_text"
81
- : null;
82
- case "inconsistent_patterns":
83
- return /\b(manager|factory|provider|adapter|orchestrator|coordinator)\b/i.test(change.entityName)
84
- ? "pattern_abstraction_name"
85
- : null;
86
- case "reinvented_utils":
87
- return reinventedUtilitySignal(change)
88
- ? "generic_utility_name"
89
- : null;
90
- case "operator_style_mismatch":
91
- return /\b(manager|factory|provider|enterprise|orchestrator)\b/i.test(haystack)
92
- ? "style_smell_terms"
93
- : null;
94
- default:
95
- return null;
96
- }
97
- }
98
- function isDuplicatedSchemaCandidate(change) {
99
- if (!/^(interface|type)$/i.test(change.entityType))
100
- return false;
101
- if (/^(public|external|internal|payment|.+client$)/i.test(change.entityName))
102
- return false;
103
- return /\b(local|payload|schema)\b/i.test(words(change.entityName));
104
- }
105
- function overCommentingSignal(change) {
106
- const before = commentLines(change.beforeContent);
107
- const after = commentLines(change.afterContent);
108
- if (after <= before + 3)
109
- return false;
110
- const comments = commentText(change.afterContent);
111
- if (/\b(because|why|constraint|provider|external|api|quirk|edge case|timezone|utc|ledger|finance|reconciliation|rejects|mirrors|keep this)\b/i.test(comments)) {
112
- return false;
113
- }
114
- return true;
115
- }
116
- function lintBypassSignal(value) {
117
- return value.split(/\r?\n/).some((line) => {
118
- const trimmed = line.trim();
119
- const comment = /^(\/\/|\/\*|\*)/.test(trimmed);
120
- if (comment && /@ts-ignore\s*$/i.test(trimmed))
121
- return true;
122
- if (comment && /@ts-expect-error\s*$/i.test(trimmed))
123
- return true;
124
- if (comment && /(eslint-disable|biome-ignore)/i.test(trimmed) && !/\s--\s*\S/.test(trimmed))
125
- return true;
126
- return /\bas unknown as\b|\bas any\b|:\s*any\b/i.test(trimmed);
127
- });
128
- }
129
- function reinventedUtilitySignal(change) {
130
- const name = change.entityName;
131
- if (!/^(clamp|debounce|throttle|slug|slugify|sort|shuffle|memoize|pick|omit|uniq)/i.test(name))
132
- return false;
133
- const content = change.afterContent ?? "";
134
- if (/currency|invoice|refund|subscription|tier|domain/i.test(`${name}\n${content}`))
135
- return false;
136
- return true;
137
- }
138
- function isSearchableSourceChange(change) {
139
- const filePath = change.filePath.toLowerCase();
140
- if (/(^|\/)(bun|package-lock|pnpm-lock|yarn)\.lock$/.test(filePath))
141
- return false;
142
- if (/(^|\/)(dist|build|coverage|generated|vendor|fixtures?|snapshots?)(\/|$)/.test(filePath))
143
- return false;
144
- if (/\.(md|mdx|txt|json|jsonc|ya?ml|toml|lock|csv|svg|png|jpe?g|gif|webp)$/i.test(filePath))
145
- return false;
146
- if (/\.(test|spec|fixture)\.[cm]?[jt]sx?$/i.test(filePath))
147
- return false;
148
- return /\.(ts|tsx|js|jsx|mjs|cjs|mts|cts)$/i.test(filePath);
149
- }
150
- function commentLines(value) {
151
- if (!value)
152
- return 0;
153
- return value.split(/\r?\n/).filter((line) => /^\s*(\/\/|\/\*|\*|#)/.test(line)).length;
154
- }
155
- function commentText(value) {
156
- if (!value)
157
- return "";
158
- return value
159
- .split(/\r?\n/)
160
- .filter((line) => /^\s*(\/\/|\/\*|\*|#)/.test(line))
161
- .join("\n");
162
- }
163
- function words(value) {
164
- return value
165
- .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
166
- .replace(/[_-]+/g, " ");
167
- }
package/dist/diff.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare function readDiffFromStdin(): Promise<string>;
package/dist/diff.js DELETED
@@ -1,10 +0,0 @@
1
- import { stdin as input } from "node:process";
2
- export async function readDiffFromStdin() {
3
- const chunks = [];
4
- for await (const chunk of input)
5
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
6
- const text = Buffer.concat(chunks).toString("utf8");
7
- if (!text.trim())
8
- throw new Error("No diff received on stdin.");
9
- return text;
10
- }
package/dist/doctor.d.ts DELETED
@@ -1,4 +0,0 @@
1
- export declare function runDoctor(): Promise<Readonly<{
2
- exitCode: number;
3
- text: string;
4
- }>>;
package/dist/doctor.js DELETED
@@ -1,131 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { createRequire } from "node:module";
3
- import { homedir, platform } from "node:os";
4
- import path from "node:path";
5
- import { promisify } from "node:util";
6
- import { DEFAULT_MODEL_ID, MODEL_REGISTRY } from "./constants.js";
7
- import { runHookCommand } from "./hooks.js";
8
- const execFileAsync = promisify(execFile);
9
- export async function runDoctor() {
10
- const checks = await Promise.all([
11
- gitCheck(),
12
- hookCheck(),
13
- semCheck(),
14
- repomixCheck(),
15
- llamaServerCheck(),
16
- modelCacheCheck(),
17
- ]);
18
- const requiredMissing = checks.some((check) => check.required && check.status === "missing");
19
- return {
20
- exitCode: requiredMissing ? 1 : 0,
21
- text: renderDoctor(checks),
22
- };
23
- }
24
- async function gitCheck() {
25
- try {
26
- const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], { maxBuffer: 1024 * 1024 });
27
- return { label: "git repo", status: "ok", detail: stdout.trim(), required: true };
28
- }
29
- catch {
30
- return { label: "git repo", status: "missing", detail: "not inside a git repository", required: true };
31
- }
32
- }
33
- async function hookCheck() {
34
- try {
35
- const status = await runHookCommand("status");
36
- return { label: "pre-commit hook", status: "info", detail: status.replace(/^Stupify hook:\s*/, "") };
37
- }
38
- catch (error) {
39
- return { label: "pre-commit hook", status: "info", detail: errorMessage(error) };
40
- }
41
- }
42
- async function semCheck() {
43
- const packageBin = resolvePackage("@ataraxy-labs/sem/bin/sem.js");
44
- if (packageBin)
45
- return { label: "sem", status: "ok", detail: "@ataraxy-labs/sem package binary found", required: true };
46
- if (await commandExists("sem"))
47
- return { label: "sem", status: "ok", detail: "sem found on PATH", required: true };
48
- return { label: "sem", status: "missing", detail: "install @ataraxy-labs/sem or put sem on PATH", required: true };
49
- }
50
- async function repomixCheck() {
51
- if (resolvePackage("repomix"))
52
- return { label: "Repomix", status: "ok", detail: "repomix package found", required: true };
53
- return { label: "Repomix", status: "missing", detail: "repomix package is not installed", required: true };
54
- }
55
- async function llamaServerCheck() {
56
- if (await commandExists("llama-server"))
57
- return { label: "llama-server", status: "ok", detail: "llama-server found on PATH", required: true };
58
- return { label: "llama-server", status: "missing", detail: "install llama.cpp, for example `brew install llama.cpp`", required: true };
59
- }
60
- async function modelCacheCheck() {
61
- const model = MODEL_REGISTRY[DEFAULT_MODEL_ID];
62
- const modelPath = path.join(cacheDir(), "models", model.file);
63
- if (await fileExists(modelPath))
64
- return { label: "default model", status: "ok", detail: `${model.name} cached` };
65
- return {
66
- label: "default model",
67
- status: "info",
68
- detail: `${model.name} not cached yet; first interactive search can download it locally`,
69
- };
70
- }
71
- function renderDoctor(checks) {
72
- const lines = [
73
- "Stupify doctor",
74
- "",
75
- ...checks.map((check) => `${icon(check.status)} ${check.label}: ${check.detail}`),
76
- "",
77
- "Privacy: local-only. Stupify does not upload source, diffs, filenames, repo URLs, commit messages, author names, or private package names.",
78
- ];
79
- return lines.join("\n");
80
- }
81
- function icon(status) {
82
- if (status === "ok")
83
- return "OK";
84
- if (status === "missing")
85
- return "MISSING";
86
- return "INFO";
87
- }
88
- function resolvePackage(specifier) {
89
- try {
90
- const require = createRequire(import.meta.url);
91
- return require.resolve(specifier);
92
- }
93
- catch {
94
- return null;
95
- }
96
- }
97
- async function commandExists(command) {
98
- try {
99
- await execFileAsync("sh", ["-c", `command -v ${shellQuote(command)}`], { maxBuffer: 1024 * 1024 });
100
- return true;
101
- }
102
- catch {
103
- return false;
104
- }
105
- }
106
- async function fileExists(filePath) {
107
- try {
108
- const { stat } = await import("node:fs/promises");
109
- return (await stat(filePath)).isFile();
110
- }
111
- catch {
112
- return false;
113
- }
114
- }
115
- function cacheDir() {
116
- if (process.env.STUPIFY_CACHE_DIR)
117
- return process.env.STUPIFY_CACHE_DIR;
118
- if (process.env.XDG_CACHE_HOME)
119
- return path.join(process.env.XDG_CACHE_HOME, "stupify");
120
- if (platform() === "darwin")
121
- return path.join(homedir(), "Library", "Caches", "stupify");
122
- if (platform() === "win32" && process.env.LOCALAPPDATA)
123
- return path.join(process.env.LOCALAPPDATA, "stupify", "Cache");
124
- return path.join(homedir(), ".cache", "stupify");
125
- }
126
- function shellQuote(value) {
127
- return `'${value.replace(/'/g, "'\\''")}'`;
128
- }
129
- function errorMessage(error) {
130
- return error instanceof Error ? error.message : String(error);
131
- }
package/dist/git.d.ts DELETED
@@ -1,12 +0,0 @@
1
- import { type NetDiff, type SourceRange, type StagedDiff } 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>;
9
- export declare function stagedDiff(): Promise<StagedDiff>;
10
- export declare function gitRoot(): Promise<string>;
11
- export declare function gitPath(pathspec: string): Promise<string>;
12
- export declare function gitUserLabel(): Promise<string>;
package/dist/git.js DELETED
@@ -1,298 +0,0 @@
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
- if (!oldest || !newest)
37
- throw new Error("Could not resolve recent commit range.");
38
- const [base, target, shortBase, shortTarget] = await Promise.all([
39
- revParse(`${oldest}^1`),
40
- revParse(newest),
41
- shortCommit(`${oldest}^1`),
42
- shortCommit(newest),
43
- ]);
44
- return sourceRange(base, target, `${commits.length} recent commits`, sourceId(`range:${shortBase}..${shortTarget}`));
45
- }
46
- export async function netDiffFromStdin(text) {
47
- if (!text.trim())
48
- throw new Error("No diff received on stdin.");
49
- return {
50
- id: sourceId("stdin"),
51
- label: "stdin",
52
- base: "stdin",
53
- target: "stdin",
54
- text,
55
- stats: statsFromDiff(text),
56
- };
57
- }
58
- export async function stagedDiff() {
59
- try {
60
- const { stdout } = await execFileAsync("git", [
61
- "diff",
62
- "--cached",
63
- "--no-ext-diff",
64
- "--no-color",
65
- "--unified=3",
66
- "--",
67
- ], { maxBuffer: 64 * 1024 * 1024 });
68
- return { text: stdout, stats: statsFromDiff(stdout) };
69
- }
70
- catch {
71
- throw new Error("Could not read staged changes. Run stupify inside a git repository.");
72
- }
73
- }
74
- export async function gitRoot() {
75
- try {
76
- const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"]);
77
- return stdout.trim();
78
- }
79
- catch {
80
- throw new Error("Could not find a git repository.");
81
- }
82
- }
83
- export async function gitPath(pathspec) {
84
- try {
85
- const { stdout } = await execFileAsync("git", ["rev-parse", "--git-path", pathspec]);
86
- return stdout.trim();
87
- }
88
- catch {
89
- throw new Error(`Could not resolve git path: ${pathspec}`);
90
- }
91
- }
92
- export async function gitUserLabel() {
93
- const [name, email] = await Promise.all([
94
- gitConfig("user.name"),
95
- gitConfig("user.email"),
96
- ]);
97
- if (name && email)
98
- return `${name} <${email}>`;
99
- return name || email || "working tree";
100
- }
101
- async function netDiff(base, target, label, id) {
102
- const [text, stats, shortBase, shortTarget] = await Promise.all([
103
- diff(base, target),
104
- diffStats(base, target),
105
- shortCommit(base),
106
- shortCommit(target),
107
- ]);
108
- return {
109
- id: id ?? sourceId(`net:${shortBase}..${shortTarget}`),
110
- label,
111
- base,
112
- target,
113
- text,
114
- stats,
115
- };
116
- }
117
- async function sourceRange(base, target, label, id) {
118
- const [stats, shortBase, shortTarget, committers] = await Promise.all([
119
- diffStats(base, target),
120
- shortCommit(base),
121
- shortCommit(target),
122
- committersForRange(base, target),
123
- ]);
124
- return {
125
- id: id ?? sourceId(`net:${shortBase}..${shortTarget}`),
126
- label,
127
- base,
128
- target,
129
- committers,
130
- stats,
131
- };
132
- }
133
- async function gitConfig(key) {
134
- try {
135
- const { stdout } = await execFileAsync("git", ["config", "--get", key]);
136
- return stdout.trim();
137
- }
138
- catch {
139
- return "";
140
- }
141
- }
142
- async function committersForRange(base, target) {
143
- try {
144
- const { stdout } = await execFileAsync("git", ["log", "--format=%cn <%ce>", `${base}..${target}`], {
145
- maxBuffer: 4 * 1024 * 1024,
146
- });
147
- return uniqueLines(stdout);
148
- }
149
- catch {
150
- return [];
151
- }
152
- }
153
- function uniqueLines(value) {
154
- const seen = new Set();
155
- const lines = [];
156
- for (const line of value.split(/\r?\n/)) {
157
- const trimmed = line.trim();
158
- if (!trimmed || seen.has(trimmed))
159
- continue;
160
- seen.add(trimmed);
161
- lines.push(trimmed);
162
- }
163
- return lines;
164
- }
165
- async function baseBefore(since) {
166
- try {
167
- const { stdout } = await execFileAsync("git", [
168
- "log",
169
- "--first-parent",
170
- "--before",
171
- since,
172
- "-1",
173
- "--format=%H",
174
- ]);
175
- const commit = stdout.trim();
176
- if (commit)
177
- return commit;
178
- return rootCommit();
179
- }
180
- catch {
181
- throw new Error(`Could not resolve base commit before ${since}.`);
182
- }
183
- }
184
- async function rootCommit() {
185
- try {
186
- const { stdout } = await execFileAsync("git", ["rev-list", "--max-parents=0", "HEAD"]);
187
- return stdout.trim().split(/\r?\n/, 1)[0] ?? "";
188
- }
189
- catch {
190
- throw new Error("Could not resolve repository root commit.");
191
- }
192
- }
193
- async function diff(base, target) {
194
- try {
195
- const { stdout } = await execFileAsync("git", [
196
- "diff",
197
- "--no-ext-diff",
198
- "--no-color",
199
- "--unified=8",
200
- base,
201
- target,
202
- "--",
203
- ], { maxBuffer: 128 * 1024 * 1024 });
204
- if (!stdout.trim())
205
- throw new Error("empty diff");
206
- return stdout;
207
- }
208
- catch {
209
- throw new Error(`No diff found for ${base}..${target}.`);
210
- }
211
- }
212
- async function diffStats(base, target) {
213
- try {
214
- const { stdout } = await execFileAsync("git", ["diff", "--numstat", base, target, "--"], {
215
- maxBuffer: 16 * 1024 * 1024,
216
- });
217
- return statsFromNumstat(stdout);
218
- }
219
- catch {
220
- return { filesChanged: 0, additions: 0, deletions: 0 };
221
- }
222
- }
223
- function statsFromDiff(diffText) {
224
- const files = new Set();
225
- let additions = 0;
226
- let deletions = 0;
227
- for (const line of diffText.split(/\r?\n/)) {
228
- const fileMatch = /^diff --git a\/.+ b\/(.+)$/.exec(line);
229
- if (fileMatch?.[1])
230
- files.add(fileMatch[1]);
231
- else if (line.startsWith("+") && !line.startsWith("+++"))
232
- additions += 1;
233
- else if (line.startsWith("-") && !line.startsWith("---"))
234
- deletions += 1;
235
- }
236
- return { filesChanged: files.size, additions, deletions };
237
- }
238
- function statsFromNumstat(numstat) {
239
- let filesChanged = 0;
240
- let additions = 0;
241
- let deletions = 0;
242
- for (const line of numstat.split(/\r?\n/)) {
243
- if (!line.trim())
244
- continue;
245
- const [added, deleted] = line.split(/\s+/, 3);
246
- filesChanged += 1;
247
- additions += Number(added) || 0;
248
- deletions += Number(deleted) || 0;
249
- }
250
- return { filesChanged, additions, deletions };
251
- }
252
- async function recentCommits(count) {
253
- try {
254
- const { stdout } = await execFileAsync("git", [
255
- "log",
256
- "--first-parent",
257
- "--no-merges",
258
- "--format=%H",
259
- `-${count}`,
260
- ]);
261
- return stdout.split(/\r?\n/).filter(Boolean).reverse();
262
- }
263
- catch {
264
- throw new Error(`Could not read last ${count} commits.`);
265
- }
266
- }
267
- async function revParse(rev) {
268
- try {
269
- const { stdout } = await execFileAsync("git", ["rev-parse", rev]);
270
- return stdout.trim();
271
- }
272
- catch {
273
- throw new Error(`Could not resolve ${rev}.`);
274
- }
275
- }
276
- async function shortCommit(commit) {
277
- try {
278
- const { stdout } = await execFileAsync("git", ["rev-parse", "--short", commit]);
279
- return stdout.trim();
280
- }
281
- catch {
282
- throw new Error(`Could not resolve commit ${commit}.`);
283
- }
284
- }
285
- async function commitMessage(commit) {
286
- try {
287
- const { stdout } = await execFileAsync("git", ["show", "--no-patch", "--format=%B", commit], {
288
- maxBuffer: 1024 * 1024,
289
- });
290
- return stdout;
291
- }
292
- catch {
293
- throw new Error(`Could not read commit message for ${commit}.`);
294
- }
295
- }
296
- function firstLine(value) {
297
- return value.trim().split(/\r?\n/, 1)[0]?.trim() ?? "";
298
- }
package/dist/hooks.d.ts DELETED
@@ -1,3 +0,0 @@
1
- import type { HookAction } from "./types.ts";
2
- export declare function runHookCommand(action: HookAction): Promise<string>;
3
- export declare function hookSnippet(): string;