@stupify/cli 0.0.16 → 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 -168
  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 -16
  24. package/dist/doctor.js +0 -143
  25. package/dist/git.d.ts +0 -17
  26. package/dist/git.js +0 -368
  27. package/dist/hooks.d.ts +0 -5
  28. package/dist/hooks.js +0 -135
  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 -6
  36. package/dist/render.js +0 -295
  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 -255
  45. package/dist/stupify.d.ts +0 -38
  46. package/dist/stupify.js +0 -505
  47. package/dist/trace.d.ts +0 -31
  48. package/dist/trace.js +0 -86
  49. package/dist/types.d.ts +0 -341
  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 -223
  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 -166
  61. package/src/git.ts +0 -380
  62. package/src/hooks.ts +0 -151
  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 -328
  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 -300
  71. package/src/stupify.ts +0 -604
  72. package/src/trace.ts +0 -126
  73. package/src/types.ts +0 -362
  74. 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
- }