@stupify/cli 0.0.8 โ†’ 0.0.10

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/render.ts CHANGED
@@ -1,43 +1,42 @@
1
1
  import { VERSION } from "./constants.ts";
2
2
  import type { SearchCommand, SearchRunJson } from "./types.ts";
3
+ import { format } from "./ui.ts";
3
4
 
4
5
  export function renderSearchRun(run: SearchRunJson, command: SearchCommand): string {
5
6
  if (command.json) return JSON.stringify(run, null, 2);
6
7
 
7
8
  if (run.stats.skipped && run.stats.skipReason === "input_too_large") {
8
- return `๐Ÿง™ stupify ๐Ÿช„
9
- Search input is too large for precise local search.
10
- Size:
9
+ return `${format.heading("Search input is too large for precise local search.")}
10
+ ${format.heading("Size:")}
11
11
  ~${run.stats.inputTokens ?? "unknown"} tokens
12
- Limit:
12
+ ${format.heading("Limit:")}
13
13
  ${run.stats.inputTokenCap ?? "unknown"} tokens
14
14
  Stupify skipped the search rather than review truncated context.
15
15
  Nothing was blocked.
16
- Try:
16
+ ${format.heading("Try:")}
17
17
  rerun with ${sourceHint(command)} --max-search-input-tokens ${Math.max((run.stats.inputTokens ?? 12_000) + 1, (run.stats.inputTokenCap ?? 12_000) * 2)}`;
18
18
  }
19
19
 
20
20
  if (run.stats.skipped && run.stats.skipReason === "no_candidates") {
21
- return `๐Ÿง™ stupify ๐Ÿช„
22
- Search complete.
23
- Patterns: ${run.patterns.join(", ")}
24
- No search targets found.`;
21
+ return `${format.heading("Search complete.")}
22
+ ${format.label("Patterns:")} ${run.patterns.join(", ")}
23
+ ${format.success("No search targets found.")}`;
25
24
  }
26
25
 
27
26
  if (run.matches.length === 0) {
28
- return `๐Ÿง™ stupify ๐Ÿช„
29
- Search complete.
30
- Patterns: ${run.patterns.join(", ")}
31
- No judgment-offload signals found.`;
27
+ return `${format.heading("Search complete.")}
28
+ ${format.label("Patterns:")} ${run.patterns.join(", ")}
29
+ ${format.success("No judgment-offload signals found.")}`;
32
30
  }
33
31
 
34
- return `๐Ÿง™ stupify ๐Ÿช„
35
- Possible judgment-offload detected:
36
- ${run.matches.map((match, index) => `${index + 1}. ${match.patternId}
37
- Why: ${match.checkWhy ?? "This pattern may indicate judgment-offload."}
38
- Match: ${match.reason}
39
- Proof: ${match.proof}`).join("\n")}
40
- Search mode is warn-only.`;
32
+ return `${format.warn("AI SLOP DETECTED")}
33
+ ${run.matches.map((match, index) => `${index + 1}.
34
+ ${format.muted("who:")} ${committerLabel(run)}
35
+ ${format.muted("what:")} ${match.patternId} - ${match.reason}
36
+ ${format.muted("when:")} ${sourceLabel(command)}
37
+ ${format.muted("where:")} ${match.proof}
38
+ ${format.muted("why:")} ${match.checkWhy ?? "This pattern may indicate judgment-offload."}`).join("\n")}
39
+ ${format.muted("Search mode is warn-only.")}`;
41
40
  }
42
41
 
43
42
  export function helpText(): string {
@@ -95,3 +94,18 @@ function sourceHint(command: SearchCommand): string {
95
94
  if (command.kind === "commits") return `--commits ${command.count}`;
96
95
  return "--stdin";
97
96
  }
97
+
98
+ function sourceLabel(command: SearchCommand): string {
99
+ if (command.kind === "staged") return "staged changes";
100
+ if (command.kind === "since") return `since ${command.since}`;
101
+ if (command.kind === "commit") return `commit ${command.commit}`;
102
+ if (command.kind === "commits") return `last ${command.count} commits`;
103
+ return "stdin diff";
104
+ }
105
+
106
+ function committerLabel(run: SearchRunJson): string {
107
+ const committers = (run.stats.committers ?? []).filter(Boolean);
108
+ if (committers.length === 0) return "unknown committer";
109
+ if (committers.length <= 3) return committers.join(", ");
110
+ return `${committers.slice(0, 3).join(", ")} +${committers.length - 3} more`;
111
+ }
@@ -7,11 +7,13 @@ import { promisify } from "node:util";
7
7
  import { cachedJson, fingerprint } from "./cache.ts";
8
8
  import { readDiffFromStdin } from "./diff.ts";
9
9
  import {
10
+ gitUserLabel,
10
11
  sourceRangeForCommit,
11
12
  sourceRangeForRecentCommits,
12
13
  sourceRangeSince,
13
14
  stagedDiff,
14
15
  } from "./git.ts";
16
+ import { diagnostic } from "./ui.ts";
15
17
  import type {
16
18
  SearchCommand,
17
19
  SemChange,
@@ -26,11 +28,12 @@ const execFileAsync = promisify(execFile);
26
28
  export async function semChangeSetForCommand(
27
29
  command: SearchCommand,
28
30
  ): Promise<SemChangeSet> {
29
- if (command.kind === "stdin") return semChangeSetFromPatch(await readDiffFromStdin(), command.debugSem);
31
+ if (command.kind === "stdin") return semChangeSetFromPatch(await readDiffFromStdin(), command.debugSem, "stdin", ["stdin"]);
30
32
  if (command.kind === "staged") {
31
- const diff = await stagedDiff();
32
- if (!diff.text.trim()) return emptyChangeSet("staged", diff.stats);
33
- return semChangeSetFromPatch(diff.text, command.debugSem, "staged");
33
+ const [diff, committer] = await Promise.all([stagedDiff(), gitUserLabel()]);
34
+ const committers = [committer];
35
+ if (!diff.text.trim()) return emptyChangeSet("staged", diff.stats, committers);
36
+ return semChangeSetFromPatch(diff.text, command.debugSem, "staged", committers);
34
37
  }
35
38
  if (command.kind === "commit") {
36
39
  const range = await sourceRangeForCommit(command.commit);
@@ -51,12 +54,17 @@ export async function semChangeSetForCommand(
51
54
  return withContextWorkspace(normalizeSemDiff(raw, range), command.debugSem);
52
55
  }
53
56
 
54
- function emptyChangeSet(label: string, stats: SourceRange["stats"]): SemChangeSet {
57
+ function emptyChangeSet(
58
+ label: string,
59
+ stats: SourceRange["stats"],
60
+ committers?: readonly string[],
61
+ ): SemChangeSet {
55
62
  return {
56
63
  id: sourceId(label),
57
64
  label,
58
65
  base: label,
59
66
  target: label,
67
+ committers,
60
68
  contextCwd: process.cwd(),
61
69
  cleanup: async () => undefined,
62
70
  changes: [],
@@ -79,7 +87,12 @@ async function semRangeForCommand(command: SearchCommand): Promise<SourceRange>
79
87
  throw new Error("sem cannot resolve stdin as a git range.");
80
88
  }
81
89
 
82
- async function semChangeSetFromPatch(patch: string, debugSem: boolean, label = "stdin"): Promise<SemChangeSet> {
90
+ async function semChangeSetFromPatch(
91
+ patch: string,
92
+ debugSem: boolean,
93
+ label = "stdin",
94
+ committers?: readonly string[],
95
+ ): Promise<SemChangeSet> {
83
96
  if (!patch.trim()) throw new Error("No diff received on stdin.");
84
97
  const raw = await cachedJson(
85
98
  "sem-diff",
@@ -93,11 +106,12 @@ async function semChangeSetFromPatch(patch: string, debugSem: boolean, label = "
93
106
  );
94
107
  return {
95
108
  ...normalizeSemDiff(raw, {
96
- id: sourceId(label),
97
- label,
98
- base: label,
99
- target: label,
100
- stats: { filesChanged: 0, additions: 0, deletions: 0 },
109
+ id: sourceId(label),
110
+ label,
111
+ base: label,
112
+ target: label,
113
+ committers,
114
+ stats: { filesChanged: 0, additions: 0, deletions: 0 },
101
115
  }),
102
116
  contextCwd: process.cwd(),
103
117
  cleanup: async () => undefined,
@@ -140,14 +154,14 @@ async function withContextWorkspace(changeSet: SemChangeSet, debugSem: boolean):
140
154
  }
141
155
 
142
156
  async function runSem(args: readonly string[], debugSem: boolean, cwd = process.cwd()): Promise<unknown> {
143
- if (debugSem) console.error(`sem ${args.join(" ")}`);
157
+ if (debugSem) diagnostic(`sem ${args.join(" ")}`);
144
158
  const { command, commandArgs } = resolveSemCommand(args);
145
159
  try {
146
160
  const { stdout, stderr } = await execFileAsync(command, commandArgs, {
147
161
  cwd,
148
162
  maxBuffer: 128 * 1024 * 1024,
149
163
  });
150
- if (debugSem && stderr.trim()) console.error(stderr.trim());
164
+ if (debugSem && stderr.trim()) diagnostic(stderr.trim());
151
165
  return JSON.parse(stdout);
152
166
  } catch (error) {
153
167
  const message = error instanceof Error ? error.message : String(error);
@@ -156,7 +170,7 @@ async function runSem(args: readonly string[], debugSem: boolean, cwd = process.
156
170
  }
157
171
 
158
172
  async function runSemWithInput(args: readonly string[], stdin: string, debugSem: boolean): Promise<unknown> {
159
- if (debugSem) console.error(`sem ${args.join(" ")}`);
173
+ if (debugSem) diagnostic(`sem ${args.join(" ")}`);
160
174
  const { command, commandArgs } = resolveSemCommand(args);
161
175
  return new Promise((resolve, reject) => {
162
176
  const child = spawn(command, commandArgs, { stdio: ["pipe", "pipe", "pipe"] });
@@ -167,7 +181,7 @@ async function runSemWithInput(args: readonly string[], stdin: string, debugSem:
167
181
  child.on("error", (error) => reject(error));
168
182
  child.on("close", (code) => {
169
183
  const stderrText = Buffer.concat(stderr).toString("utf8");
170
- if (debugSem && stderrText.trim()) console.error(stderrText.trim());
184
+ if (debugSem && stderrText.trim()) diagnostic(stderrText.trim());
171
185
  if (code !== 0) {
172
186
  reject(new Error(`sem failed with exit code ${code}${stderrText ? `: ${stderrText.trim()}` : ""}`));
173
187
  return;
@@ -183,7 +197,7 @@ async function runSemWithInput(args: readonly string[], stdin: string, debugSem:
183
197
  }
184
198
 
185
199
  async function git(args: readonly string[], debugSem: boolean): Promise<void> {
186
- if (debugSem) console.error(`git ${args.join(" ")}`);
200
+ if (debugSem) diagnostic(`git ${args.join(" ")}`);
187
201
  await execFileAsync("git", [...args], { maxBuffer: 128 * 1024 * 1024 });
188
202
  }
189
203
 
@@ -225,6 +239,7 @@ function normalizeSemDiff(value: unknown, range: SourceRange): SemChangeSet {
225
239
  label: range.label,
226
240
  base: range.base,
227
241
  target: range.target,
242
+ committers: range.committers,
228
243
  contextCwd: process.cwd(),
229
244
  cleanup: async () => undefined,
230
245
  changes,
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),
@@ -93,6 +96,7 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
93
96
  stats: {
94
97
  elapsedMs: Date.now() - startedAt,
95
98
  modelCalls: 0,
99
+ committers: changeSet.committers,
96
100
  skipped: true,
97
101
  skipReason: "no_candidates",
98
102
  filesChanged: changeSet.summary.fileCount,
@@ -134,6 +138,7 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
134
138
  stats: {
135
139
  elapsedMs: Date.now() - startedAt,
136
140
  modelCalls: 0,
141
+ committers: changeSet.committers,
137
142
  skipped: true,
138
143
  skipReason: "no_candidates",
139
144
  filesChanged: changeSet.summary.fileCount,
@@ -177,6 +182,7 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
177
182
  modelCalls: 0,
178
183
  inputTokens: batches.estimatedInputTokens,
179
184
  inputTokenCap: maxSearchInputTokens,
185
+ committers: changeSet.committers,
180
186
  skipped: true,
181
187
  skipReason: "input_too_large",
182
188
  filesChanged: changeSet.summary.fileCount,
@@ -197,14 +203,14 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
197
203
  }
198
204
 
199
205
  if (batches.wasSplit && !command.json) {
200
- 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.`);
201
207
  if (batches.skippedTargets > 0) {
202
- 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.`);
203
209
  }
204
210
  }
205
211
 
206
- const modelPath = await firstRunModelBootstrap(command.model);
207
- 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);
208
214
  const matches = [];
209
215
  let modelCalls = 0;
210
216
  let inputTokens = 0;
@@ -215,7 +221,7 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
215
221
  if (batchInputTokens > maxSearchInputTokens) {
216
222
  exactSkippedTargets += batch.contexts.length;
217
223
  if (!command.json) {
218
- 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.`);
219
225
  }
220
226
  continue;
221
227
  }
@@ -240,6 +246,7 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
240
246
  modelCalls,
241
247
  inputTokens,
242
248
  inputTokenCap: maxSearchInputTokens,
249
+ committers: changeSet.committers,
243
250
  filesChanged: changeSet.summary.fileCount,
244
251
  entitiesScanned: changeSet.summary.total,
245
252
  candidates: contexts.length,
@@ -415,11 +422,17 @@ function buildSearchRequest(
415
422
  function printRunPlan(
416
423
  command: SearchCommand,
417
424
  patternIds: readonly string[],
425
+ ui: CliUi,
418
426
  ): void {
419
427
  if (command.json) return;
420
- console.error("๐Ÿง™ stupify ๐Ÿช„");
421
- console.error(`Search: ${sourceLabel(command)}`);
422
- 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
+ );
423
436
  }
424
437
 
425
438
  function formatStep(name: string, ms: number, count?: number, detail?: string): string {
package/src/types.ts CHANGED
@@ -92,6 +92,7 @@ export type SourceRange = Readonly<{
92
92
  label: string;
93
93
  base: string;
94
94
  target: string;
95
+ committers?: readonly string[];
95
96
  stats: NetDiffStats;
96
97
  }>;
97
98
 
@@ -120,6 +121,7 @@ export type SemChangeSet = Readonly<{
120
121
  label: string;
121
122
  base: string;
122
123
  target: string;
124
+ committers?: readonly string[];
123
125
  contextCwd: string;
124
126
  cleanup: () => Promise<void>;
125
127
  changes: readonly SemChange[];
@@ -211,6 +213,7 @@ export type SearchRunJson = Readonly<{
211
213
  inputTokenCap?: number;
212
214
  skipped?: boolean;
213
215
  skipReason?: "input_too_large" | "no_candidates";
216
+ committers?: readonly string[];
214
217
  filesChanged?: number;
215
218
  entitiesScanned?: number;
216
219
  candidates?: number;
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
+ }