@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/dist/analysis.js +30 -2
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/model.d.ts +3 -2
- package/dist/model.js +42 -43
- package/dist/render.js +33 -23
- package/dist/sem-provider.js +6 -5
- package/dist/stupify.d.ts +35 -1
- package/dist/stupify.js +24 -19
- package/dist/types.d.ts +1 -0
- package/dist/ui.d.ts +34 -0
- package/dist/ui.js +143 -0
- package/package.json +3 -1
- package/src/analysis.ts +29 -2
- package/src/constants.ts +1 -1
- package/src/model.ts +49 -55
- package/src/render.ts +35 -23
- package/src/sem-provider.ts +6 -5
- package/src/stupify.ts +27 -18
- package/src/types.ts +1 -0
- package/src/ui.ts +187 -0
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
|
-
|
|
32
|
+
ui.writeStdout(helpText());
|
|
31
33
|
return 0;
|
|
32
34
|
}
|
|
33
35
|
if (command.kind === "hook") {
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
+
ui.writeStdout(await runSearchBench(command.configPath));
|
|
45
47
|
return 0;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
206
|
+
ui.warn(`Search input is large; queued ${batches.batches.length} smaller search batches.`);
|
|
204
207
|
if (batches.skippedTargets > 0) {
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
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
|
+
}
|