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