@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/stupify.js DELETED
@@ -1,505 +0,0 @@
1
- #!/usr/bin/env node
2
- import { realpathSync } from "node:fs";
3
- import { fileURLToPath } from "node:url";
4
- import { countPromptTokens, runSearch, searchRequest } from "./analysis.js";
5
- import { searchChecks } from "./checks.js";
6
- import { parseCommand } from "./command.js";
7
- import { counterScoutPlan } from "./counter-scout.js";
8
- import { renderDoctorToUi, runDoctor } from "./doctor.js";
9
- import { blameEntity } from "./git.js";
10
- import { renderHookResultToUi, runHookCommand } from "./hooks.js";
11
- import { firstRunModelBootstrap, loadLocalModel } from "./model.js";
12
- import { entityContextsFromChanges, emptyContextPack, repomixContextPack, repomixSearchConfig } from "./repomix-provider.js";
13
- import { helpText, renderSearchRun, renderSearchRunToUi } from "./render.js";
14
- import { effectiveMaxCandidates, effectiveMaxSearchInputTokens, effectiveRepomixConfig, effectiveSearchChecks, loadSearchProfile, } from "./search-profile.js";
15
- import { semChangeSetForCommand } from "./sem-provider.js";
16
- import { createTracer } from "./trace.js";
17
- import { createCliUi } from "./ui.js";
18
- export async function main(argv = process.argv.slice(2)) {
19
- const startedAt = Date.now();
20
- let ui = createCliUi();
21
- try {
22
- const command = parseCommand(argv);
23
- if (command.kind === "help") {
24
- ui.intro("stupify");
25
- ui.note(helpText().trim(), "Help");
26
- ui.outro("Local-only. Warn-only.");
27
- return 0;
28
- }
29
- if (command.kind === "hook") {
30
- ui.intro("stupify");
31
- renderHookResultToUi(await runHookCommand(command.action), ui);
32
- ui.outro("Hook mode is warn-only. Commits are not blocked.");
33
- return 0;
34
- }
35
- if (command.kind === "doctor") {
36
- const result = await runDoctor();
37
- ui.intro("stupify");
38
- renderDoctorToUi(result, ui);
39
- ui.outro(result.exitCode === 0 ? "Ready." : "Fix missing required dependencies, then rerun doctor.");
40
- return result.exitCode;
41
- }
42
- if (command.kind === "bench-search") {
43
- const { runSearchBench } = await import("./search-bench.js");
44
- ui.intro("stupify");
45
- ui.note(await runSearchBench(command.configPath), "Search bench");
46
- ui.outro("Bench complete.");
47
- return 0;
48
- }
49
- ui = createCliUi({ quiet: command.json });
50
- const run = await runSearchCommand(command, startedAt, ui);
51
- if (command.json)
52
- ui.writeStdout(renderSearchRun(run, command));
53
- else
54
- renderSearchRunToUi(run, command, ui);
55
- return 0;
56
- }
57
- catch (error) {
58
- ui.error(error instanceof Error ? error.message : String(error), { force: true });
59
- return 1;
60
- }
61
- }
62
- export async function runSearchCommand(command, startedAt, ui = createCliUi({ quiet: command.json })) {
63
- const activeSpans = new Map();
64
- const t = createTracer({
65
- writeLine: () => undefined,
66
- onEvent: (event) => {
67
- if (command.json)
68
- return;
69
- if (event.phase === "start") {
70
- activeSpans.set(event.name, ui.spinner(formatStartStep(event.name, event.detail)));
71
- return;
72
- }
73
- const active = activeSpans.get(event.name);
74
- activeSpans.delete(event.name);
75
- const message = event.phase === "error"
76
- ? formatErrorStep(event.name, event.ms)
77
- : formatStep(event.name, event.ms, event.count, event.detail);
78
- if (!active) {
79
- if (event.phase === "error")
80
- ui.error(message);
81
- else
82
- ui.step(message);
83
- return;
84
- }
85
- if (event.phase === "error")
86
- active.error(message);
87
- else
88
- active.stop(message);
89
- },
90
- });
91
- const profile = await loadSearchProfile(command.searchProfilePath);
92
- const checks = profile ? effectiveSearchChecks(command.checkIds, profile) : searchChecks(command.checkIds);
93
- const patternIds = checks.map((check) => check.id);
94
- const maxCandidates = effectiveMaxCandidates(command.maxCandidates, profile);
95
- const maxSearchInputTokens = effectiveMaxSearchInputTokens(command.maxSearchInputTokens, profile);
96
- printRunPlan(command, patternIds, ui);
97
- const { value: changeSet } = await t.trace("entity.diff", () => semChangeSetForCommand(command), {
98
- count: (v) => v.summary.total,
99
- detail: (v) => `${v.summary.fileCount} files`,
100
- });
101
- try {
102
- const scoutPlan = counterScoutPlan(changeSet, checks, maxCandidates);
103
- if (!command.json)
104
- ui.step(scoutPlanLine(scoutPlan, changeSet.summary.total));
105
- const candidates = scoutPlan.targets;
106
- const contexts = entityContextsFromChanges(candidates, changeSet.changes);
107
- const targetsByPattern = countTargetsByPattern(contexts);
108
- const targetsPreview = previewTargets(contexts);
109
- if (contexts.length === 0) {
110
- return {
111
- schemaVersion: "search.v1",
112
- mode: "search",
113
- source: command.source,
114
- model: { id: command.model },
115
- patterns: patternIds,
116
- stats: {
117
- elapsedMs: Date.now() - startedAt,
118
- modelCalls: 0,
119
- committers: changeSet.committers,
120
- commitSubjects: changeSet.commitSubjects,
121
- skipped: true,
122
- skipReason: "no_candidates",
123
- filesChanged: changeSet.summary.fileCount,
124
- entitiesScanned: changeSet.summary.total,
125
- candidates: 0,
126
- searchTargets: 0,
127
- repomixFiles: 0,
128
- repomixTokens: 0,
129
- profileId: profile?.id,
130
- targetsByPattern,
131
- targetsPreview,
132
- },
133
- matches: [],
134
- };
135
- }
136
- const baseRepomixConfig = effectiveRepomixConfig(repomixSearchConfig(), profile);
137
- const initialPack = profile?.context === "sem"
138
- ? emptyContextPack()
139
- : await t.trace("context.pack", () => repomixContextPack(changeSet.contextCwd, contexts, changeSet.changes, baseRepomixConfig), {
140
- count: (v) => v.filePaths.length,
141
- detail: (v) => `${v.totalTokens} tokens`,
142
- }).then((result) => result.value);
143
- const packedFiles = new Set(initialPack.filePaths);
144
- const searchContexts = profile?.context === "sem"
145
- ? contexts
146
- : contexts.filter((context) => context.filePath && packedFiles.has(context.filePath));
147
- if (!command.json)
148
- ui.step(targetPlanLine(searchContexts, contexts.length, countTargetsByPattern(searchContexts)));
149
- if (searchContexts.length === 0) {
150
- return {
151
- schemaVersion: "search.v1",
152
- mode: "search",
153
- source: command.source,
154
- model: { id: command.model },
155
- patterns: patternIds,
156
- stats: {
157
- elapsedMs: Date.now() - startedAt,
158
- modelCalls: 0,
159
- committers: changeSet.committers,
160
- commitSubjects: changeSet.commitSubjects,
161
- skipped: true,
162
- skipReason: "no_candidates",
163
- filesChanged: changeSet.summary.fileCount,
164
- entitiesScanned: changeSet.summary.total,
165
- candidates: contexts.length,
166
- searchTargets: 0,
167
- repomixFiles: initialPack.filePaths.length,
168
- repomixTokens: initialPack.totalTokens,
169
- repomixConfig: initialPack.config,
170
- profileId: profile?.id,
171
- targetsByPattern,
172
- targetsPreview,
173
- },
174
- matches: [],
175
- };
176
- }
177
- const pack = profile?.context === "sem" || searchContexts.length === contexts.length
178
- ? initialPack
179
- : await repomixContextPack(changeSet.contextCwd, searchContexts, changeSet.changes, baseRepomixConfig);
180
- const { value: batches } = await t.trace("search.batches", () => buildSearchBatches({
181
- command,
182
- changeSet,
183
- contexts: searchContexts,
184
- initialPack: pack,
185
- checks,
186
- profile,
187
- includeCounterReasonInPrompt: command.includeCounterReasonInPrompt,
188
- maxSearchInputTokens,
189
- baseRepomixConfig,
190
- }), {
191
- startDetail: `${searchContexts.length} targets`,
192
- count: (result) => result.batches.length,
193
- detail: (result) => result.wasSplit
194
- ? `${result.skippedTargets} oversized targets skipped`
195
- : `${result.estimatedInputTokens} estimated tokens`,
196
- });
197
- if (batches.batches.length === 0) {
198
- return {
199
- schemaVersion: "search.v1",
200
- mode: "search",
201
- source: command.source,
202
- model: { id: command.model },
203
- patterns: patternIds,
204
- stats: {
205
- elapsedMs: Date.now() - startedAt,
206
- modelCalls: 0,
207
- inputTokens: batches.estimatedInputTokens,
208
- inputTokenCap: maxSearchInputTokens,
209
- committers: changeSet.committers,
210
- commitSubjects: changeSet.commitSubjects,
211
- skipped: true,
212
- skipReason: "input_too_large",
213
- filesChanged: changeSet.summary.fileCount,
214
- entitiesScanned: changeSet.summary.total,
215
- candidates: contexts.length,
216
- searchTargets: searchContexts.length,
217
- repomixFiles: pack.filePaths.length,
218
- repomixTokens: pack.totalTokens,
219
- repomixConfig: pack.config,
220
- searchBatches: 0,
221
- skippedTargets: batches.skippedTargets,
222
- profileId: profile?.id,
223
- targetsByPattern: countTargetsByPattern(searchContexts),
224
- targetsPreview: previewTargets(searchContexts),
225
- },
226
- matches: [],
227
- };
228
- }
229
- if (batches.wasSplit && !command.json) {
230
- ui.warn(`Search input is large; queued ${batches.batches.length} smaller batches for ${searchContexts.length} targets (${maxSearchInputTokens} token cap).`);
231
- if (batches.skippedTargets > 0) {
232
- ui.warn(`Skipped ${batches.skippedTargets} oversized targets that could not fit alone.`);
233
- }
234
- }
235
- else if (!command.json) {
236
- ui.step(`Search: ${searchContexts.length} targets in ${batches.batches.length} model batch (${maxSearchInputTokens} token cap)`);
237
- }
238
- const modelPath = await firstRunModelBootstrap(command.model, ui);
239
- const model = await loadLocalModel(modelPath, command.model, "scout", ui);
240
- const matches = [];
241
- let modelCalls = 0;
242
- let inputTokens = 0;
243
- let exactSkippedTargets = batches.skippedTargets;
244
- for (const batch of batches.batches) {
245
- const { value: batchInputTokens } = await t.trace("prompt.tokens", () => countPromptTokens(model, batch.request.prompt), {
246
- startDetail: `${batch.contexts.length} targets`,
247
- count: (tokens) => tokens,
248
- });
249
- inputTokens += batchInputTokens;
250
- if (batchInputTokens > maxSearchInputTokens) {
251
- exactSkippedTargets += batch.contexts.length;
252
- if (!command.json) {
253
- ui.warn(`Skipped ${batch.contexts.length} targets after exact token count exceeded the limit.`);
254
- }
255
- continue;
256
- }
257
- const { value } = await t.trace("search.model", () => runSearch(model, batch.request), {
258
- startDetail: `${batch.contexts.length} targets`,
259
- count: (v) => v.length,
260
- });
261
- modelCalls += 1;
262
- matches.push(...withCheckWhy(value, checks));
263
- }
264
- const uniqueMatches = await withEntityBlame(dedupeMatches(matches), changeSet.target, command);
265
- return {
266
- schemaVersion: "search.v1",
267
- mode: "search",
268
- source: command.source,
269
- model: { id: command.model },
270
- patterns: patternIds,
271
- stats: {
272
- elapsedMs: Date.now() - startedAt,
273
- modelCalls,
274
- inputTokens,
275
- inputTokenCap: maxSearchInputTokens,
276
- committers: changeSet.committers,
277
- commitSubjects: changeSet.commitSubjects,
278
- filesChanged: changeSet.summary.fileCount,
279
- entitiesScanned: changeSet.summary.total,
280
- candidates: contexts.length,
281
- searchTargets: searchContexts.length,
282
- repomixFiles: pack.filePaths.length,
283
- repomixTokens: pack.totalTokens,
284
- repomixConfig: pack.config,
285
- searchBatches: batches.batches.length,
286
- skippedTargets: exactSkippedTargets,
287
- profileId: profile?.id,
288
- targetsByPattern: countTargetsByPattern(searchContexts),
289
- targetsPreview: previewTargets(searchContexts),
290
- },
291
- matches: uniqueMatches,
292
- };
293
- }
294
- finally {
295
- await changeSet.cleanup();
296
- }
297
- }
298
- function dedupeMatches(matches) {
299
- const seen = new Set();
300
- return matches.filter((match) => {
301
- const key = `${match.patternId}\n${match.proof.trim()}`;
302
- if (seen.has(key))
303
- return false;
304
- seen.add(key);
305
- return true;
306
- });
307
- }
308
- function withCheckWhy(matches, checks) {
309
- const checksById = new Map(checks.map((check) => [check.id, check]));
310
- return matches.map((match) => ({
311
- ...match,
312
- patternName: checksById.get(match.patternId)?.name,
313
- checkWhy: checksById.get(match.patternId)?.why,
314
- }));
315
- }
316
- async function withEntityBlame(matches, targetRev, command) {
317
- if (command.kind === "staged" || command.kind === "stdin")
318
- return matches;
319
- return Promise.all(matches.map(async (match) => {
320
- if (!match.filePath || !match.entityName)
321
- return match;
322
- const blame = await blameEntity({
323
- filePath: match.filePath,
324
- entityName: match.entityName,
325
- rev: targetRev,
326
- });
327
- return blame ? { ...match, blame } : match;
328
- }));
329
- }
330
- async function buildSearchBatches(input) {
331
- const first = makeSearchBatch(input, input.contexts, input.initialPack);
332
- if (first.estimatedInputTokens <= input.maxSearchInputTokens) {
333
- return {
334
- batches: [first],
335
- estimatedInputTokens: first.estimatedInputTokens,
336
- skippedTargets: 0,
337
- wasSplit: false,
338
- };
339
- }
340
- const batches = [];
341
- let skippedTargets = 0;
342
- let currentContexts = [];
343
- let currentBatch = null;
344
- for (const context of input.contexts) {
345
- const candidateContexts = [...currentContexts, context];
346
- const candidateBatch = await makeSearchBatchWithPack(input, candidateContexts);
347
- if (candidateBatch.estimatedInputTokens <= input.maxSearchInputTokens) {
348
- currentContexts = candidateContexts;
349
- currentBatch = candidateBatch;
350
- continue;
351
- }
352
- if (currentBatch) {
353
- batches.push(currentBatch);
354
- currentContexts = [];
355
- currentBatch = null;
356
- }
357
- const singleBatch = candidateContexts.length === 1
358
- ? candidateBatch
359
- : await makeSearchBatchWithPack(input, [context]);
360
- if (singleBatch.estimatedInputTokens <= input.maxSearchInputTokens) {
361
- currentContexts = [context];
362
- currentBatch = singleBatch;
363
- }
364
- else {
365
- skippedTargets += 1;
366
- }
367
- }
368
- if (currentBatch)
369
- batches.push(currentBatch);
370
- return {
371
- batches,
372
- estimatedInputTokens: first.estimatedInputTokens,
373
- skippedTargets,
374
- wasSplit: true,
375
- };
376
- }
377
- function makeSearchBatch(input, contexts, pack) {
378
- const request = buildSearchRequest(input.changeSet, contexts, pack, input.checks, input.profile, input.includeCounterReasonInPrompt);
379
- return {
380
- contexts,
381
- pack,
382
- request,
383
- estimatedInputTokens: estimatePromptTokens(request.prompt),
384
- };
385
- }
386
- async function makeSearchBatchWithPack(input, contexts) {
387
- const pack = input.profile?.context === "sem"
388
- ? emptyContextPack()
389
- : await repomixContextPack(input.changeSet.contextCwd, contexts, input.changeSet.changes, input.baseRepomixConfig);
390
- return makeSearchBatch(input, contexts, pack);
391
- }
392
- function buildSearchRequest(changeSet, contexts, pack, patterns, profile, includeCounterReasonInPrompt) {
393
- return searchRequest({
394
- changeSet,
395
- contexts,
396
- pack,
397
- patterns,
398
- includeCounterReasonInPrompt: profile?.includeCounterReasonInPrompt ?? includeCounterReasonInPrompt,
399
- });
400
- }
401
- function printRunPlan(command, patternIds, ui) {
402
- if (command.json)
403
- return;
404
- ui.intro("stupify");
405
- ui.note([
406
- `Search: ${sourceLabel(command)}`,
407
- `Patterns: ${patternIds.join(", ")}`,
408
- ].join("\n"), "Run");
409
- }
410
- function formatStartStep(name, detail) {
411
- if (name === "entity.diff")
412
- return "Diff: running sem over the selected git range";
413
- if (name === "context.pack")
414
- return "Context: packing selected target files with Repomix";
415
- if (name === "search.batches")
416
- return `Search: preparing token-bounded model batches${detail ? ` for ${detail}` : ""}`;
417
- if (name === "prompt.tokens")
418
- return `Tokens: counting search prompt${detail ? ` for ${detail}` : ""}`;
419
- if (name === "search.model")
420
- return `Model: searching selected target/check pairs${detail ? ` (${detail})` : ""}`;
421
- return `${name}: working`;
422
- }
423
- function formatStep(name, ms, count, detail) {
424
- if (name === "entity.diff")
425
- return `Diff: ${detail ?? "changed files"}, ${count ?? 0} changed entities (${ms}ms)`;
426
- if (name === "context.pack")
427
- return `Context: ${count ?? 0} files, ${detail ?? "0 tokens"} (${ms}ms)`;
428
- if (name === "search.batches")
429
- return `Search: ${count ?? 0} model batches, ${detail ?? "0 estimated tokens"} (${ms}ms)`;
430
- if (name === "prompt.tokens")
431
- return `Tokens: ${count ?? 0} prompt tokens (${ms}ms)`;
432
- if (name === "search.model")
433
- return `Model: ${count ?? 0} matches (${ms}ms)`;
434
- return `${name}: ${ms}ms`;
435
- }
436
- function formatErrorStep(name, ms) {
437
- if (name === "entity.diff")
438
- return `Diff failed after ${ms}ms`;
439
- if (name === "context.pack")
440
- return `Context packing failed after ${ms}ms`;
441
- if (name === "search.batches")
442
- return `Search batch preparation failed after ${ms}ms`;
443
- if (name === "prompt.tokens")
444
- return `Token counting failed after ${ms}ms`;
445
- if (name === "search.model")
446
- return `Model search failed after ${ms}ms`;
447
- return `${name} failed after ${ms}ms`;
448
- }
449
- function scoutPlanLine(plan, entitiesScanned) {
450
- if (plan.targets.length === 0) {
451
- return `Scout: deterministic counters scanned ${entitiesScanned} entities; no target/check pairs selected`;
452
- }
453
- return [
454
- `Scout: deterministic counters scanned ${entitiesScanned} entities`,
455
- `${plan.totalSignals} counter signals`,
456
- `selected ${plan.targets.length}/${plan.totalSignals} target/check pairs (cap ${plan.maxTargets}, not exhaustive)`,
457
- ].join("; ");
458
- }
459
- function targetPlanLine(searchContexts, selectedTargets, targetsByPattern) {
460
- const retained = searchContexts.length === selectedTargets
461
- ? `${searchContexts.length} selected targets`
462
- : `${searchContexts.length}/${selectedTargets} selected targets retained after context packing`;
463
- return `Targets: model will inspect ${retained}; ${formatCounts(targetsByPattern)}`;
464
- }
465
- function formatCounts(counts) {
466
- const entries = Object.entries(counts).filter(([, count]) => count > 0);
467
- if (entries.length === 0)
468
- return "no target/check pairs";
469
- return entries.map(([id, count]) => `${id}=${count}`).join(", ");
470
- }
471
- function sourceLabel(command) {
472
- if (command.kind === "since")
473
- return `since ${command.since}`;
474
- if (command.kind === "commit")
475
- return `commit ${command.commit}`;
476
- if (command.kind === "commits")
477
- return `last ${command.count} commits`;
478
- if (command.kind === "staged")
479
- return "staged changes";
480
- return "stdin diff";
481
- }
482
- function estimatePromptTokens(prompt) {
483
- return Math.ceil(prompt.length / 3);
484
- }
485
- function countTargetsByPattern(contexts) {
486
- const counts = {};
487
- for (const context of contexts)
488
- counts[context.checkId] = (counts[context.checkId] ?? 0) + 1;
489
- return counts;
490
- }
491
- function previewTargets(contexts) {
492
- return contexts.map((context) => ({
493
- targetId: context.targetId,
494
- patternId: context.checkId,
495
- entityKind: context.entityKind || undefined,
496
- sourceKind: context.filePath ? pathKind(context.filePath) : undefined,
497
- }));
498
- }
499
- function pathKind(filePath) {
500
- const ext = filePath.split(".").pop();
501
- return ext && ext !== filePath ? ext : "unknown";
502
- }
503
- if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
504
- process.exitCode = await main();
505
- }
package/dist/trace.d.ts DELETED
@@ -1,31 +0,0 @@
1
- export type TraceFields = Record<string, string | number | boolean | null | undefined>;
2
- export type Tracer = {
3
- trace<T>(span: string, fn: () => Promise<T>, options?: SpanTraceOptions<T>): Promise<{
4
- value: T;
5
- ms: number;
6
- }>;
7
- trace<T>(span: string, fn: () => T, options?: SpanTraceOptions<T>): {
8
- value: T;
9
- ms: number;
10
- };
11
- };
12
- export type SpanTraceEvent = Readonly<{
13
- name: string;
14
- phase: "start" | "end" | "error";
15
- ms: number;
16
- count?: number;
17
- detail?: string;
18
- }>;
19
- export type SpanTraceOptions<T> = Readonly<{
20
- fields?: TraceFields;
21
- startDetail?: string | (() => string);
22
- count?: (value: T) => number;
23
- detail?: (value: T) => string;
24
- }>;
25
- export type CreateTracerOptions = {
26
- enabled?: boolean;
27
- writeLine?: (line: string) => void;
28
- onEvent?: (event: SpanTraceEvent) => void;
29
- };
30
- export declare function createTracer(options?: CreateTracerOptions): Tracer;
31
- export declare const trace: Tracer;
package/dist/trace.js DELETED
@@ -1,86 +0,0 @@
1
- import { performance } from "node:perf_hooks";
2
- export function createTracer(options) {
3
- const enabled = options?.enabled ?? true;
4
- const writeLine = options?.writeLine ?? ((line) => process.stderr.write(line + "\n"));
5
- const onEvent = options?.onEvent;
6
- const nowMs = () => performance.now();
7
- function emit(span, durationMs, fields) {
8
- if (!enabled)
9
- return;
10
- const payload = { span, ms: Math.round(durationMs) };
11
- for (const [k, v] of Object.entries(fields ?? {})) {
12
- if (v !== undefined)
13
- payload[k] = v;
14
- }
15
- writeLine(`trace ${JSON.stringify(payload)}`);
16
- }
17
- function trace(span, fn, options) {
18
- const startedAtMs = nowMs();
19
- onEvent?.({
20
- name: span,
21
- phase: "start",
22
- ms: 0,
23
- detail: typeof options?.startDetail === "function" ? options.startDetail() : options?.startDetail,
24
- });
25
- try {
26
- const out = fn();
27
- if (isPromiseLike(out)) {
28
- return (async () => {
29
- let durationMs;
30
- try {
31
- const value = await out;
32
- durationMs = nowMs() - startedAtMs;
33
- const event = {
34
- name: span,
35
- phase: "end",
36
- ms: Math.round(durationMs),
37
- count: options?.count?.(value),
38
- detail: options?.detail?.(value),
39
- };
40
- onEvent?.(event);
41
- return { value, ms: event.ms };
42
- }
43
- catch (error) {
44
- durationMs = nowMs() - startedAtMs;
45
- onEvent?.({
46
- name: span,
47
- phase: "error",
48
- ms: Math.round(durationMs),
49
- });
50
- throw error;
51
- }
52
- finally {
53
- durationMs ??= nowMs() - startedAtMs;
54
- emit(span, durationMs, options?.fields);
55
- }
56
- })();
57
- }
58
- const durationMs = nowMs() - startedAtMs;
59
- emit(span, durationMs, options?.fields);
60
- const event = {
61
- name: span,
62
- phase: "end",
63
- ms: Math.round(durationMs),
64
- count: options?.count?.(out),
65
- detail: options?.detail?.(out),
66
- };
67
- onEvent?.(event);
68
- return { value: out, ms: event.ms };
69
- }
70
- catch (error) {
71
- const durationMs = nowMs() - startedAtMs;
72
- onEvent?.({
73
- name: span,
74
- phase: "error",
75
- ms: Math.round(durationMs),
76
- });
77
- emit(span, durationMs, options?.fields);
78
- throw error;
79
- }
80
- }
81
- return { trace };
82
- }
83
- export const trace = createTracer();
84
- function isPromiseLike(value) {
85
- return typeof value === "object" && value !== null && "then" in value;
86
- }