@stupify/cli 0.0.16 → 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.
Files changed (89) hide show
  1. package/.review/CORPUS.md +44 -0
  2. package/.review/CORPUS.template.md +73 -0
  3. package/.review/REVIEW-PROMPT.md +52 -0
  4. package/.review/RUBRIC.md +46 -0
  5. package/LICENSE +1 -1
  6. package/README.md +95 -37
  7. package/package.json +27 -26
  8. package/packs/antirez.md +10 -0
  9. package/packs/anton-kropp.md +10 -0
  10. package/packs/dhh.md +10 -0
  11. package/packs/dtolnay.md +10 -0
  12. package/packs/jarred-sumner.md +9 -0
  13. package/packs/mitchell-hashimoto.md +10 -0
  14. package/packs/rich-harris.md +10 -0
  15. package/packs/simon-willison.md +10 -0
  16. package/packs/sindre-sorhus.md +10 -0
  17. package/packs/tanner-linsley.md +10 -0
  18. package/packs/zod.md +10 -0
  19. package/src/cli.ts +626 -0
  20. package/src/prime-install.test.ts +109 -0
  21. package/src/prime.ts +50 -0
  22. package/src/review-sweep.test.ts +101 -0
  23. package/src/review-sweep.ts +526 -0
  24. package/dist/analysis.d.ts +0 -16
  25. package/dist/analysis.js +0 -168
  26. package/dist/cache.d.ts +0 -2
  27. package/dist/cache.js +0 -57
  28. package/dist/checks.d.ts +0 -4
  29. package/dist/checks.js +0 -228
  30. package/dist/command.d.ts +0 -2
  31. package/dist/command.js +0 -147
  32. package/dist/constants.d.ts +0 -4
  33. package/dist/constants.js +0 -53
  34. package/dist/counter-scout.d.ts +0 -21
  35. package/dist/counter-scout.js +0 -167
  36. package/dist/diff.d.ts +0 -1
  37. package/dist/diff.js +0 -10
  38. package/dist/doctor.d.ts +0 -16
  39. package/dist/doctor.js +0 -143
  40. package/dist/git.d.ts +0 -17
  41. package/dist/git.js +0 -368
  42. package/dist/hooks.d.ts +0 -5
  43. package/dist/hooks.js +0 -135
  44. package/dist/index.d.ts +0 -1
  45. package/dist/index.js +0 -1
  46. package/dist/model.d.ts +0 -11
  47. package/dist/model.js +0 -296
  48. package/dist/prompts.d.ts +0 -8
  49. package/dist/prompts.js +0 -89
  50. package/dist/render.d.ts +0 -6
  51. package/dist/render.js +0 -295
  52. package/dist/repomix-provider.d.ts +0 -12
  53. package/dist/repomix-provider.js +0 -196
  54. package/dist/search-bench.d.ts +0 -1
  55. package/dist/search-bench.js +0 -677
  56. package/dist/search-profile.d.ts +0 -6
  57. package/dist/search-profile.js +0 -73
  58. package/dist/sem-provider.d.ts +0 -2
  59. package/dist/sem-provider.js +0 -255
  60. package/dist/stupify.d.ts +0 -38
  61. package/dist/stupify.js +0 -505
  62. package/dist/trace.d.ts +0 -31
  63. package/dist/trace.js +0 -86
  64. package/dist/types.d.ts +0 -341
  65. package/dist/types.js +0 -6
  66. package/dist/ui.d.ts +0 -34
  67. package/dist/ui.js +0 -143
  68. package/src/analysis.ts +0 -223
  69. package/src/cache.ts +0 -63
  70. package/src/checks.ts +0 -231
  71. package/src/command.ts +0 -173
  72. package/src/constants.ts +0 -56
  73. package/src/counter-scout.ts +0 -195
  74. package/src/diff.ts +0 -9
  75. package/src/doctor.ts +0 -166
  76. package/src/git.ts +0 -380
  77. package/src/hooks.ts +0 -151
  78. package/src/index.ts +0 -1
  79. package/src/model.ts +0 -367
  80. package/src/prompts.ts +0 -100
  81. package/src/render.ts +0 -328
  82. package/src/repomix-provider.ts +0 -219
  83. package/src/search-bench.ts +0 -783
  84. package/src/search-profile.ts +0 -89
  85. package/src/sem-provider.ts +0 -300
  86. package/src/stupify.ts +0 -604
  87. package/src/trace.ts +0 -126
  88. package/src/types.ts +0 -362
  89. package/src/ui.ts +0 -187
package/dist/git.js DELETED
@@ -1,368 +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
- export async function blameEntity(input) {
102
- try {
103
- const { stdout } = await execFileAsync("git", [
104
- "blame",
105
- "--line-porcelain",
106
- "-L",
107
- `:${input.entityName}`,
108
- input.rev,
109
- "--",
110
- input.filePath,
111
- ], { maxBuffer: 16 * 1024 * 1024 });
112
- return summarizeBlame(stdout);
113
- }
114
- catch {
115
- return null;
116
- }
117
- }
118
- async function netDiff(base, target, label, id) {
119
- const [text, stats, shortBase, shortTarget] = await Promise.all([
120
- diff(base, target),
121
- diffStats(base, target),
122
- shortCommit(base),
123
- shortCommit(target),
124
- ]);
125
- return {
126
- id: id ?? sourceId(`net:${shortBase}..${shortTarget}`),
127
- label,
128
- base,
129
- target,
130
- text,
131
- stats,
132
- };
133
- }
134
- async function sourceRange(base, target, label, id) {
135
- const [stats, shortBase, shortTarget, committers, commitSubjects] = await Promise.all([
136
- diffStats(base, target),
137
- shortCommit(base),
138
- shortCommit(target),
139
- committersForRange(base, target),
140
- commitSubjectsForRange(base, target),
141
- ]);
142
- return {
143
- id: id ?? sourceId(`net:${shortBase}..${shortTarget}`),
144
- label,
145
- base,
146
- target,
147
- committers,
148
- commitSubjects,
149
- stats,
150
- };
151
- }
152
- async function gitConfig(key) {
153
- try {
154
- const { stdout } = await execFileAsync("git", ["config", "--get", key]);
155
- return stdout.trim();
156
- }
157
- catch {
158
- return "";
159
- }
160
- }
161
- async function committersForRange(base, target) {
162
- try {
163
- const { stdout } = await execFileAsync("git", ["log", "--format=%cn <%ce>", `${base}..${target}`], {
164
- maxBuffer: 4 * 1024 * 1024,
165
- });
166
- return uniqueLines(stdout);
167
- }
168
- catch {
169
- return [];
170
- }
171
- }
172
- async function commitSubjectsForRange(base, target) {
173
- try {
174
- const { stdout } = await execFileAsync("git", ["log", "--format=%s", `${base}..${target}`], {
175
- maxBuffer: 4 * 1024 * 1024,
176
- });
177
- return uniqueLines(stdout);
178
- }
179
- catch {
180
- return [];
181
- }
182
- }
183
- function uniqueLines(value) {
184
- const seen = new Set();
185
- const lines = [];
186
- for (const line of value.split(/\r?\n/)) {
187
- const trimmed = line.trim();
188
- if (!trimmed || seen.has(trimmed))
189
- continue;
190
- seen.add(trimmed);
191
- lines.push(trimmed);
192
- }
193
- return lines;
194
- }
195
- async function baseBefore(since) {
196
- try {
197
- const { stdout } = await execFileAsync("git", [
198
- "log",
199
- "--first-parent",
200
- "--before",
201
- since,
202
- "-1",
203
- "--format=%H",
204
- ]);
205
- const commit = stdout.trim();
206
- if (commit)
207
- return commit;
208
- return rootCommit();
209
- }
210
- catch {
211
- throw new Error(`Could not resolve base commit before ${since}.`);
212
- }
213
- }
214
- async function rootCommit() {
215
- try {
216
- const { stdout } = await execFileAsync("git", ["rev-list", "--max-parents=0", "HEAD"]);
217
- return stdout.trim().split(/\r?\n/, 1)[0] ?? "";
218
- }
219
- catch {
220
- throw new Error("Could not resolve repository root commit.");
221
- }
222
- }
223
- async function diff(base, target) {
224
- try {
225
- const { stdout } = await execFileAsync("git", [
226
- "diff",
227
- "--no-ext-diff",
228
- "--no-color",
229
- "--unified=8",
230
- base,
231
- target,
232
- "--",
233
- ], { maxBuffer: 128 * 1024 * 1024 });
234
- if (!stdout.trim())
235
- throw new Error("empty diff");
236
- return stdout;
237
- }
238
- catch {
239
- throw new Error(`No diff found for ${base}..${target}.`);
240
- }
241
- }
242
- async function diffStats(base, target) {
243
- try {
244
- const { stdout } = await execFileAsync("git", ["diff", "--numstat", base, target, "--"], {
245
- maxBuffer: 16 * 1024 * 1024,
246
- });
247
- return statsFromNumstat(stdout);
248
- }
249
- catch {
250
- return { filesChanged: 0, additions: 0, deletions: 0 };
251
- }
252
- }
253
- function statsFromDiff(diffText) {
254
- const files = new Set();
255
- let additions = 0;
256
- let deletions = 0;
257
- for (const line of diffText.split(/\r?\n/)) {
258
- const fileMatch = /^diff --git a\/.+ b\/(.+)$/.exec(line);
259
- if (fileMatch?.[1])
260
- files.add(fileMatch[1]);
261
- else if (line.startsWith("+") && !line.startsWith("+++"))
262
- additions += 1;
263
- else if (line.startsWith("-") && !line.startsWith("---"))
264
- deletions += 1;
265
- }
266
- return { filesChanged: files.size, additions, deletions };
267
- }
268
- function statsFromNumstat(numstat) {
269
- let filesChanged = 0;
270
- let additions = 0;
271
- let deletions = 0;
272
- for (const line of numstat.split(/\r?\n/)) {
273
- if (!line.trim())
274
- continue;
275
- const [added, deleted] = line.split(/\s+/, 3);
276
- filesChanged += 1;
277
- additions += Number(added) || 0;
278
- deletions += Number(deleted) || 0;
279
- }
280
- return { filesChanged, additions, deletions };
281
- }
282
- async function recentCommits(count) {
283
- try {
284
- const { stdout } = await execFileAsync("git", [
285
- "log",
286
- "--first-parent",
287
- "--no-merges",
288
- "--format=%H",
289
- `-${count}`,
290
- ]);
291
- return stdout.split(/\r?\n/).filter(Boolean).reverse();
292
- }
293
- catch {
294
- throw new Error(`Could not read last ${count} commits.`);
295
- }
296
- }
297
- async function revParse(rev) {
298
- try {
299
- const { stdout } = await execFileAsync("git", ["rev-parse", rev]);
300
- return stdout.trim();
301
- }
302
- catch {
303
- throw new Error(`Could not resolve ${rev}.`);
304
- }
305
- }
306
- async function shortCommit(commit) {
307
- try {
308
- const { stdout } = await execFileAsync("git", ["rev-parse", "--short", commit]);
309
- return stdout.trim();
310
- }
311
- catch {
312
- throw new Error(`Could not resolve commit ${commit}.`);
313
- }
314
- }
315
- async function commitMessage(commit) {
316
- try {
317
- const { stdout } = await execFileAsync("git", ["show", "--no-patch", "--format=%B", commit], {
318
- maxBuffer: 1024 * 1024,
319
- });
320
- return stdout;
321
- }
322
- catch {
323
- throw new Error(`Could not read commit message for ${commit}.`);
324
- }
325
- }
326
- function firstLine(value) {
327
- return value.trim().split(/\r?\n/, 1)[0]?.trim() ?? "";
328
- }
329
- function summarizeBlame(output) {
330
- const entries = new Map();
331
- let currentCommit = "";
332
- let currentAuthor = "";
333
- let currentSubject = "";
334
- for (const line of output.split(/\r?\n/)) {
335
- const header = /^([0-9a-f]{40})\s+/.exec(line);
336
- if (header?.[1]) {
337
- currentCommit = header[1];
338
- currentAuthor = "";
339
- currentSubject = "";
340
- continue;
341
- }
342
- if (line.startsWith("author ")) {
343
- currentAuthor = line.slice("author ".length).trim();
344
- continue;
345
- }
346
- if (line.startsWith("summary ")) {
347
- currentSubject = line.slice("summary ".length).trim();
348
- continue;
349
- }
350
- if (!line.startsWith("\t") || !currentCommit)
351
- continue;
352
- const previous = entries.get(currentCommit);
353
- entries.set(currentCommit, {
354
- commit: currentCommit,
355
- author: currentAuthor || previous?.author || "unknown author",
356
- subject: currentSubject || previous?.subject || currentCommit.slice(0, 7),
357
- count: (previous?.count ?? 0) + 1,
358
- });
359
- }
360
- const [best] = [...entries.values()].sort((a, b) => b.count - a.count);
361
- if (!best)
362
- return null;
363
- return {
364
- commit: best.commit.slice(0, 7),
365
- author: best.author,
366
- subject: best.subject,
367
- };
368
- }
package/dist/hooks.d.ts DELETED
@@ -1,5 +0,0 @@
1
- import type { HookAction } from "./types.ts";
2
- import type { CliUi } from "./ui.ts";
3
- export declare function runHookCommand(action: HookAction): Promise<string>;
4
- export declare function renderHookResultToUi(result: string, ui: CliUi): void;
5
- export declare function hookSnippet(): string;
package/dist/hooks.js DELETED
@@ -1,135 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { existsSync } from "node:fs";
3
- import { chmod, readFile, rm, writeFile } from "node:fs/promises";
4
- import path from "node:path";
5
- import { promisify } from "node:util";
6
- import { gitPath, gitRoot } from "./git.js";
7
- const execFileAsync = promisify(execFile);
8
- const START = "# stupify hook start";
9
- const END = "# stupify hook end";
10
- export async function runHookCommand(action) {
11
- if (action === "status")
12
- return hookStatus();
13
- if (action === "install")
14
- return installHook();
15
- return uninstallHook();
16
- }
17
- export function renderHookResultToUi(result, ui) {
18
- const [firstLine = "Stupify hook: no status returned", ...rest] = result.split(/\r?\n/);
19
- if (firstLine.includes("not installed")) {
20
- ui.info(firstLine);
21
- }
22
- else if (firstLine.includes("installed") || firstLine.includes("updated") || firstLine.includes("uninstalled")) {
23
- ui.success(firstLine);
24
- }
25
- else if (firstLine.includes("existing non-Stupify")) {
26
- ui.warn(firstLine);
27
- }
28
- else {
29
- ui.info(firstLine);
30
- }
31
- const detail = rest.join("\n").trim();
32
- if (detail)
33
- ui.note(detail, "Hook");
34
- }
35
- export function hookSnippet() {
36
- return managedBlock("stupify --staged");
37
- }
38
- async function hookStatus() {
39
- const hookPath = await preCommitHookPath();
40
- if (!existsSync(hookPath))
41
- return "Stupify hook: not installed";
42
- const content = await readFile(hookPath, "utf8");
43
- if (hasManagedBlock(content))
44
- return "Stupify hook: installed";
45
- return "Stupify hook: existing non-Stupify pre-commit hook found";
46
- }
47
- async function installHook() {
48
- const hookPath = await preCommitHookPath();
49
- const block = await managedBlockForInstall();
50
- if (!existsSync(hookPath)) {
51
- await writeFile(hookPath, `#!/bin/sh\n${block}\n`, "utf8");
52
- await chmod(hookPath, 0o755);
53
- return "Stupify hook: installed";
54
- }
55
- const content = await readFile(hookPath, "utf8");
56
- if (hasManagedBlock(content)) {
57
- await writeFile(hookPath, `${replaceManagedBlock(content, block).trimEnd()}\n`, "utf8");
58
- await chmod(hookPath, 0o755);
59
- return "Stupify hook: updated";
60
- }
61
- if (isEffectivelyEmptyHook(content)) {
62
- await writeFile(hookPath, `#!/bin/sh\n${block}\n`, "utf8");
63
- await chmod(hookPath, 0o755);
64
- return "Stupify hook: installed";
65
- }
66
- return `Stupify hook: existing non-Stupify pre-commit hook found; not modified.
67
- Add this snippet manually if you want Stupify in that hook:
68
- ${block}`;
69
- }
70
- async function uninstallHook() {
71
- const hookPath = await preCommitHookPath();
72
- if (!existsSync(hookPath))
73
- return "Stupify hook: not installed";
74
- const content = await readFile(hookPath, "utf8");
75
- if (!hasManagedBlock(content))
76
- return "Stupify hook: not installed";
77
- const next = replaceManagedBlock(content, "").trim();
78
- if (isEffectivelyEmptyHook(next)) {
79
- await rm(hookPath, { force: true });
80
- return "Stupify hook: uninstalled";
81
- }
82
- await writeFile(hookPath, `${next}\n`, "utf8");
83
- await chmod(hookPath, 0o755);
84
- return "Stupify hook: uninstalled";
85
- }
86
- async function preCommitHookPath() {
87
- const [root, hook] = await Promise.all([gitRoot(), gitPath("hooks/pre-commit")]);
88
- return path.isAbsolute(hook) ? hook : path.join(root, hook);
89
- }
90
- function hasManagedBlock(content) {
91
- return content.includes(START) && content.includes(END);
92
- }
93
- async function managedBlockForInstall() {
94
- if (await commandExists("stupify"))
95
- return managedBlock("stupify --staged");
96
- const root = await gitRoot();
97
- const localEntrypoint = path.join(root, "packages", "cli", "src", "stupify.ts");
98
- if (existsSync(localEntrypoint) && await commandExists("bun")) {
99
- return managedBlock(`bun ${shellQuote(localEntrypoint)} --staged`);
100
- }
101
- return managedBlock("stupify --staged");
102
- }
103
- function managedBlock(command) {
104
- return `${START}
105
- ${command} || true
106
- ${END}`;
107
- }
108
- function replaceManagedBlock(content, replacement) {
109
- const pattern = new RegExp(`${escapeRegExp(START)}[\\s\\S]*?${escapeRegExp(END)}`);
110
- return content.replace(pattern, replacement);
111
- }
112
- function isEffectivelyEmptyHook(content) {
113
- return content
114
- .split(/\r?\n/)
115
- .map((line) => line.trim())
116
- .filter((line) => line && line !== "#!/bin/sh" && line !== "#!/usr/bin/env sh")
117
- .length === 0;
118
- }
119
- function escapeRegExp(value) {
120
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
121
- }
122
- async function commandExists(command) {
123
- try {
124
- await execFileAsync("sh", ["-c", `command -v ${shellQuote(command)}`], {
125
- maxBuffer: 1024 * 1024,
126
- });
127
- return true;
128
- }
129
- catch {
130
- return false;
131
- }
132
- }
133
- function shellQuote(value) {
134
- return `'${value.replace(/'/g, "'\\''")}'`;
135
- }
package/dist/index.d.ts DELETED
@@ -1 +0,0 @@
1
- export { main } from "./stupify.ts";
package/dist/index.js DELETED
@@ -1 +0,0 @@
1
- export { main } from "./stupify.js";
package/dist/model.d.ts DELETED
@@ -1,11 +0,0 @@
1
- import type { ModelId } from "./types.ts";
2
- import type { CliUi } from "./ui.ts";
3
- export type ModelProfile = "scout";
4
- export type LocalModel = Readonly<{
5
- id: ModelId;
6
- name: string;
7
- baseUrl: string;
8
- profile: ModelProfile;
9
- }>;
10
- export declare function firstRunModelBootstrap(modelId: ModelId, ui: CliUi): Promise<string>;
11
- export declare function loadLocalModel(modelPath: string, modelId: ModelId, profile: ModelProfile, ui: CliUi): Promise<LocalModel>;