@stupify/cli 0.0.9 โ†’ 0.0.11

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.
package/src/stupify.ts CHANGED
@@ -20,46 +20,49 @@ import {
20
20
  } from "./search-profile.ts";
21
21
  import { semChangeSetForCommand } from "./sem-provider.ts";
22
22
  import { createTracer } from "./trace.ts";
23
+ import { createCliUi, type CliUi } from "./ui.ts";
23
24
  import type { SearchCommand, SearchMatch, SearchProfile, SearchRunJson, SemContext, SemContextPack, StupifyCheck } from "./types.ts";
24
25
 
25
26
  export async function main(argv = process.argv.slice(2)): Promise<number> {
26
27
  const startedAt = Date.now();
28
+ let ui = createCliUi();
27
29
  try {
28
30
  const command = parseCommand(argv);
29
31
  if (command.kind === "help") {
30
- console.log(helpText());
32
+ ui.writeStdout(helpText());
31
33
  return 0;
32
34
  }
33
35
  if (command.kind === "hook") {
34
- console.log(await runHookCommand(command.action));
36
+ ui.writeStdout(await runHookCommand(command.action));
35
37
  return 0;
36
38
  }
37
39
  if (command.kind === "doctor") {
38
40
  const result = await runDoctor();
39
- console.log(result.text);
41
+ ui.writeStdout(result.text);
40
42
  return result.exitCode;
41
43
  }
42
44
  if (command.kind === "bench-search") {
43
45
  const { runSearchBench } = await import("./search-bench.ts");
44
- console.log(await runSearchBench(command.configPath));
46
+ ui.writeStdout(await runSearchBench(command.configPath));
45
47
  return 0;
46
48
  }
47
49
 
48
- const run = await runSearchCommand(command, startedAt);
49
- console.log(renderSearchRun(run, command));
50
+ ui = createCliUi({ quiet: command.json });
51
+ const run = await runSearchCommand(command, startedAt, ui);
52
+ ui.writeStdout(renderSearchRun(run, command));
50
53
  return 0;
51
54
  } catch (error) {
52
- console.error(error instanceof Error ? error.message : String(error));
55
+ ui.error(error instanceof Error ? error.message : String(error), { force: true });
53
56
  return 1;
54
57
  }
55
58
  }
56
59
 
57
- export async function runSearchCommand(command: SearchCommand, startedAt: number): Promise<SearchRunJson> {
60
+ export async function runSearchCommand(command: SearchCommand, startedAt: number, ui = createCliUi({ quiet: command.json })): Promise<SearchRunJson> {
58
61
  const t = createTracer({
59
62
  writeLine: () => undefined,
60
63
  onEvent: (event) => {
61
64
  if (command.json) return;
62
- console.error(formatStep(event.name, event.ms, event.count, event.detail));
65
+ ui.step(formatStep(event.name, event.ms, event.count, event.detail));
63
66
  },
64
67
  });
65
68
 
@@ -68,7 +71,7 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
68
71
  const patternIds = checks.map((check) => check.id);
69
72
  const maxCandidates = effectiveMaxCandidates(command.maxCandidates, profile);
70
73
  const maxSearchInputTokens = effectiveMaxSearchInputTokens(command.maxSearchInputTokens, profile);
71
- printRunPlan(command, patternIds);
74
+ printRunPlan(command, patternIds, ui);
72
75
  const { value: changeSet } = await t.trace(
73
76
  "entity.diff",
74
77
  () => semChangeSetForCommand(command),
@@ -200,14 +203,14 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
200
203
  }
201
204
 
202
205
  if (batches.wasSplit && !command.json) {
203
- console.error(`Search input is large; queued ${batches.batches.length} smaller search batches.`);
206
+ ui.warn(`Search input is large; queued ${batches.batches.length} smaller search batches.`);
204
207
  if (batches.skippedTargets > 0) {
205
- console.error(`Skipped ${batches.skippedTargets} oversized targets that could not fit alone.`);
208
+ ui.warn(`Skipped ${batches.skippedTargets} oversized targets that could not fit alone.`);
206
209
  }
207
210
  }
208
211
 
209
- const modelPath = await firstRunModelBootstrap(command.model);
210
- const model = await loadLocalModel(modelPath, command.model, "scout");
212
+ const modelPath = await firstRunModelBootstrap(command.model, ui);
213
+ const model = await loadLocalModel(modelPath, command.model, "scout", ui);
211
214
  const matches = [];
212
215
  let modelCalls = 0;
213
216
  let inputTokens = 0;
@@ -218,7 +221,7 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
218
221
  if (batchInputTokens > maxSearchInputTokens) {
219
222
  exactSkippedTargets += batch.contexts.length;
220
223
  if (!command.json) {
221
- console.error(`Skipped ${batch.contexts.length} targets after exact token count exceeded the limit.`);
224
+ ui.warn(`Skipped ${batch.contexts.length} targets after exact token count exceeded the limit.`);
222
225
  }
223
226
  continue;
224
227
  }
@@ -419,11 +422,17 @@ function buildSearchRequest(
419
422
  function printRunPlan(
420
423
  command: SearchCommand,
421
424
  patternIds: readonly string[],
425
+ ui: CliUi,
422
426
  ): void {
423
427
  if (command.json) return;
424
- console.error("๐Ÿง™ stupify ๐Ÿช„");
425
- console.error(`Search: ${sourceLabel(command)}`);
426
- console.error(`Patterns: ${patternIds.join(", ")}`);
428
+ ui.intro("stupify");
429
+ ui.note(
430
+ [
431
+ `Search: ${sourceLabel(command)}`,
432
+ `Patterns: ${patternIds.join(", ")}`,
433
+ ].join("\n"),
434
+ "Run",
435
+ );
427
436
  }
428
437
 
429
438
  function formatStep(name: string, ms: number, count?: number, detail?: string): string {
package/src/types.ts CHANGED
@@ -198,6 +198,7 @@ export type SearchMatch = Readonly<{
198
198
  checkWhy?: string;
199
199
  reason: string;
200
200
  proof: string;
201
+ snapshot?: string;
201
202
  }>;
202
203
 
203
204
  export type SearchRunJson = Readonly<{
package/src/ui.ts ADDED
@@ -0,0 +1,187 @@
1
+ import {
2
+ confirm as clackConfirm,
3
+ intro as clackIntro,
4
+ isCancel,
5
+ log,
6
+ note,
7
+ outro as clackOutro,
8
+ progress,
9
+ spinner,
10
+ type SpinnerResult,
11
+ } from "@clack/prompts";
12
+ import { createReadStream, createWriteStream, type ReadStream, type WriteStream } from "node:fs";
13
+ import { platform } from "node:os";
14
+ import { stdin, stderr, stdout } from "node:process";
15
+ import type { Readable, Writable } from "node:stream";
16
+ import pc from "picocolors";
17
+
18
+ export type CliUi = ReturnType<typeof createCliUi>;
19
+
20
+ export type CliUiOptions = Readonly<{
21
+ quiet?: boolean;
22
+ }>;
23
+
24
+ type LogOptions = Readonly<{
25
+ force?: boolean;
26
+ }>;
27
+
28
+ type PromptIo = Readonly<{
29
+ input: Readable;
30
+ output: Writable;
31
+ close: () => void;
32
+ }>;
33
+
34
+ export function createCliUi(options: CliUiOptions = {}) {
35
+ const quiet = options.quiet ?? false;
36
+ const output = stderr;
37
+
38
+ function shouldWrite(logOptions?: LogOptions): boolean {
39
+ return logOptions?.force === true || !quiet;
40
+ }
41
+
42
+ function withPromptIo<T>(
43
+ run: (io: PromptIo) => Promise<T>,
44
+ ): Promise<T> {
45
+ const io = promptIo();
46
+ return run(io).finally(() => io.close());
47
+ }
48
+
49
+ return {
50
+ intro(title: string, logOptions?: LogOptions): void {
51
+ if (shouldWrite(logOptions)) clackIntro(title, { output });
52
+ },
53
+
54
+ outro(message: string, logOptions?: LogOptions): void {
55
+ if (shouldWrite(logOptions)) clackOutro(message, { output });
56
+ },
57
+
58
+ note(message: string, title?: string, logOptions?: LogOptions): void {
59
+ if (shouldWrite(logOptions)) note(message, title, { output });
60
+ },
61
+
62
+ info(message: string, logOptions?: LogOptions): void {
63
+ if (shouldWrite(logOptions)) log.info(message, { output });
64
+ },
65
+
66
+ step(message: string, logOptions?: LogOptions): void {
67
+ if (shouldWrite(logOptions)) log.step(message, { output });
68
+ },
69
+
70
+ success(message: string, logOptions?: LogOptions): void {
71
+ if (shouldWrite(logOptions)) log.success(message, { output });
72
+ },
73
+
74
+ warn(message: string, logOptions?: LogOptions): void {
75
+ if (shouldWrite(logOptions)) log.warn(message, { output });
76
+ },
77
+
78
+ error(message: string, logOptions?: LogOptions): void {
79
+ if (shouldWrite(logOptions)) log.error(message, { output });
80
+ },
81
+
82
+ debug(message: string): void {
83
+ if (!quiet) log.message(message, { output, symbol: pc.dim("trace") });
84
+ },
85
+
86
+ async confirm(message: string): Promise<boolean> {
87
+ return withPromptIo(async (io) => {
88
+ const result = await clackConfirm({
89
+ message,
90
+ active: "Yes",
91
+ inactive: "No",
92
+ initialValue: false,
93
+ input: io.input,
94
+ output: io.output,
95
+ });
96
+ if (isCancel(result)) return false;
97
+ return result;
98
+ });
99
+ },
100
+
101
+ spinner(message: string, logOptions?: LogOptions): SpinnerResult {
102
+ if (!shouldWrite(logOptions)) return silentSpinner();
103
+ const active = spinner({ output });
104
+ active.start(message);
105
+ return active;
106
+ },
107
+
108
+ progress(message: string, max: number, logOptions?: LogOptions) {
109
+ if (!shouldWrite(logOptions)) return silentProgress();
110
+ const active = progress({ output, max });
111
+ active.start(message);
112
+ return active;
113
+ },
114
+
115
+ writeStdout(text: string): void {
116
+ stdout.write(text.endsWith("\n") ? text : `${text}\n`);
117
+ },
118
+ };
119
+ }
120
+
121
+ export const format = {
122
+ heading: (value: string) => pc.bold(value),
123
+ label: (value: string) => pc.cyan(value),
124
+ muted: (value: string) => pc.dim(value),
125
+ success: (value: string) => pc.green(value),
126
+ warn: (value: string) => pc.yellow(value),
127
+ error: (value: string) => pc.red(value),
128
+ };
129
+
130
+ export function diagnostic(message: string): void {
131
+ log.message(message, {
132
+ output: stderr,
133
+ symbol: pc.dim("ยท"),
134
+ spacing: 0,
135
+ withGuide: false,
136
+ });
137
+ }
138
+
139
+ export function diagnosticError(message: string): void {
140
+ log.error(message, { output: stderr, spacing: 0, withGuide: false });
141
+ }
142
+
143
+ function promptIo(): PromptIo {
144
+ if (stdin.isTTY && stderr.isTTY) {
145
+ return { input: stdin, output: stderr, close: () => undefined };
146
+ }
147
+
148
+ if (platform() === "win32") {
149
+ throw new Error(
150
+ "No interactive terminal found. Run `stupify` once in an interactive terminal to set up the model.",
151
+ );
152
+ }
153
+
154
+ const input = createReadStream("/dev/tty");
155
+ const output = createWriteStream("/dev/tty");
156
+ return {
157
+ input,
158
+ output,
159
+ close: () => closePromptIo(input, output),
160
+ };
161
+ }
162
+
163
+ function closePromptIo(input: ReadStream, output: WriteStream): void {
164
+ input.destroy();
165
+ output.end();
166
+ }
167
+
168
+ function silentSpinner(): SpinnerResult {
169
+ return {
170
+ start: () => undefined,
171
+ stop: () => undefined,
172
+ cancel: () => undefined,
173
+ error: () => undefined,
174
+ message: () => undefined,
175
+ clear: () => undefined,
176
+ get isCancelled() {
177
+ return false;
178
+ },
179
+ };
180
+ }
181
+
182
+ function silentProgress() {
183
+ return {
184
+ ...silentSpinner(),
185
+ advance: () => undefined,
186
+ };
187
+ }