@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/dist/analysis.js +3 -2
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/git.d.ts +1 -0
- package/dist/git.js +44 -1
- package/dist/model.d.ts +3 -2
- package/dist/model.js +42 -43
- package/dist/render.js +38 -20
- package/dist/sem-provider.js +17 -12
- package/dist/stupify.d.ts +35 -1
- package/dist/stupify.js +28 -19
- package/dist/types.d.ts +3 -0
- package/dist/ui.d.ts +34 -0
- package/dist/ui.js +143 -0
- package/package.json +3 -1
- package/src/analysis.ts +3 -2
- package/src/constants.ts +1 -1
- package/src/git.ts +44 -1
- package/src/model.ts +49 -55
- package/src/render.ts +34 -20
- package/src/sem-provider.ts +31 -16
- package/src/stupify.ts +31 -18
- package/src/types.ts +3 -0
- package/src/ui.ts +187 -0
package/dist/analysis.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cachedJson, fingerprint } from "./cache.js";
|
|
2
2
|
import { searchPrompt } from "./prompts.js";
|
|
3
|
+
import { diagnostic, diagnosticError } from "./ui.js";
|
|
3
4
|
export async function runSearch(model, request) {
|
|
4
5
|
const raw = await runJsonPrompt(model, request.prompt, request.schema, 0);
|
|
5
6
|
return uncheckedSearchMatches(raw, request.contexts);
|
|
@@ -100,8 +101,8 @@ Your previous response was not valid JSON. Return the requested JSON object only
|
|
|
100
101
|
const retryParsed = parseJson(retry);
|
|
101
102
|
if (retryParsed.ok)
|
|
102
103
|
return retryParsed.value;
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
diagnosticError("Raw model output:");
|
|
105
|
+
diagnostic(retry);
|
|
105
106
|
throw new Error("Model returned invalid JSON.");
|
|
106
107
|
}
|
|
107
108
|
async function complete(model, prompt, schema, temperature) {
|
package/dist/constants.d.ts
CHANGED
package/dist/constants.js
CHANGED
package/dist/git.d.ts
CHANGED
|
@@ -9,3 +9,4 @@ export declare function netDiffFromStdin(text: string): Promise<NetDiff>;
|
|
|
9
9
|
export declare function stagedDiff(): Promise<StagedDiff>;
|
|
10
10
|
export declare function gitRoot(): Promise<string>;
|
|
11
11
|
export declare function gitPath(pathspec: string): Promise<string>;
|
|
12
|
+
export declare function gitUserLabel(): Promise<string>;
|
package/dist/git.js
CHANGED
|
@@ -87,6 +87,15 @@ export async function gitPath(pathspec) {
|
|
|
87
87
|
throw new Error(`Could not resolve git path: ${pathspec}`);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
export async function gitUserLabel() {
|
|
91
|
+
const [name, email] = await Promise.all([
|
|
92
|
+
gitConfig("user.name"),
|
|
93
|
+
gitConfig("user.email"),
|
|
94
|
+
]);
|
|
95
|
+
if (name && email)
|
|
96
|
+
return `${name} <${email}>`;
|
|
97
|
+
return name || email || "working tree";
|
|
98
|
+
}
|
|
90
99
|
async function netDiff(base, target, label, id) {
|
|
91
100
|
const [text, stats, shortBase, shortTarget] = await Promise.all([
|
|
92
101
|
diff(base, target),
|
|
@@ -104,19 +113,53 @@ async function netDiff(base, target, label, id) {
|
|
|
104
113
|
};
|
|
105
114
|
}
|
|
106
115
|
async function sourceRange(base, target, label, id) {
|
|
107
|
-
const [stats, shortBase, shortTarget] = await Promise.all([
|
|
116
|
+
const [stats, shortBase, shortTarget, committers] = await Promise.all([
|
|
108
117
|
diffStats(base, target),
|
|
109
118
|
shortCommit(base),
|
|
110
119
|
shortCommit(target),
|
|
120
|
+
committersForRange(base, target),
|
|
111
121
|
]);
|
|
112
122
|
return {
|
|
113
123
|
id: id ?? sourceId(`net:${shortBase}..${shortTarget}`),
|
|
114
124
|
label,
|
|
115
125
|
base,
|
|
116
126
|
target,
|
|
127
|
+
committers,
|
|
117
128
|
stats,
|
|
118
129
|
};
|
|
119
130
|
}
|
|
131
|
+
async function gitConfig(key) {
|
|
132
|
+
try {
|
|
133
|
+
const { stdout } = await execFileAsync("git", ["config", "--get", key]);
|
|
134
|
+
return stdout.trim();
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return "";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function committersForRange(base, target) {
|
|
141
|
+
try {
|
|
142
|
+
const { stdout } = await execFileAsync("git", ["log", "--format=%cn <%ce>", `${base}..${target}`], {
|
|
143
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
144
|
+
});
|
|
145
|
+
return uniqueLines(stdout);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function uniqueLines(value) {
|
|
152
|
+
const seen = new Set();
|
|
153
|
+
const lines = [];
|
|
154
|
+
for (const line of value.split(/\r?\n/)) {
|
|
155
|
+
const trimmed = line.trim();
|
|
156
|
+
if (!trimmed || seen.has(trimmed))
|
|
157
|
+
continue;
|
|
158
|
+
seen.add(trimmed);
|
|
159
|
+
lines.push(trimmed);
|
|
160
|
+
}
|
|
161
|
+
return lines;
|
|
162
|
+
}
|
|
120
163
|
async function baseBefore(since) {
|
|
121
164
|
try {
|
|
122
165
|
const { stdout } = await execFileAsync("git", [
|
package/dist/model.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ModelId } from "./types.ts";
|
|
2
|
+
import type { CliUi } from "./ui.ts";
|
|
2
3
|
export type ModelProfile = "scout";
|
|
3
4
|
export type LocalModel = Readonly<{
|
|
4
5
|
id: ModelId;
|
|
@@ -6,5 +7,5 @@ export type LocalModel = Readonly<{
|
|
|
6
7
|
baseUrl: string;
|
|
7
8
|
profile: ModelProfile;
|
|
8
9
|
}>;
|
|
9
|
-
export declare function firstRunModelBootstrap(modelId: ModelId): Promise<string>;
|
|
10
|
-
export declare function loadLocalModel(modelPath: string, modelId: ModelId, profile
|
|
10
|
+
export declare function firstRunModelBootstrap(modelId: ModelId, ui: CliUi): Promise<string>;
|
|
11
|
+
export declare function loadLocalModel(modelPath: string, modelId: ModelId, profile: ModelProfile, ui: CliUi): Promise<LocalModel>;
|
package/dist/model.js
CHANGED
|
@@ -1,42 +1,39 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
|
-
import { createReadStream, createWriteStream } from "node:fs";
|
|
3
2
|
import { mkdir, open, readFile, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
4
3
|
import { homedir, platform } from "node:os";
|
|
5
|
-
import { stdin as input, stderr as statusOutput, stdout as output, } from "node:process";
|
|
6
|
-
import { createInterface } from "node:readline/promises";
|
|
7
4
|
import path from "node:path";
|
|
8
5
|
import { promisify } from "node:util";
|
|
9
6
|
import { MODEL_REGISTRY } from "./constants.js";
|
|
10
7
|
const execFileAsync = promisify(execFile);
|
|
11
8
|
const LLAMA_SERVER_HOST = "127.0.0.1";
|
|
12
|
-
export async function firstRunModelBootstrap(modelId) {
|
|
9
|
+
export async function firstRunModelBootstrap(modelId, ui) {
|
|
13
10
|
const selectedModel = MODEL_REGISTRY[modelId];
|
|
14
11
|
const modelDir = path.join(cacheDir(), "models");
|
|
15
12
|
const modelPath = path.join(modelDir, selectedModel.file);
|
|
16
13
|
if (await exists(modelPath))
|
|
17
14
|
return modelPath;
|
|
18
|
-
|
|
15
|
+
ui.note(`No local Stupify model found.
|
|
19
16
|
Stupify runs locally.
|
|
20
17
|
Download this model now?
|
|
21
18
|
Model: ${selectedModel.name}
|
|
22
|
-
Size: ${selectedModel.size}
|
|
23
|
-
if (!(await confirm("Continue?
|
|
19
|
+
Size: ${selectedModel.size}`, "Setup", { force: true });
|
|
20
|
+
if (!(await ui.confirm("Continue?")))
|
|
24
21
|
throw new Error("Setup cancelled.");
|
|
25
22
|
await mkdir(modelDir, { recursive: true });
|
|
26
|
-
await downloadModel(modelPath, selectedModel.url);
|
|
23
|
+
await downloadModel(modelPath, selectedModel.url, ui);
|
|
27
24
|
if (!(await exists(modelPath)))
|
|
28
25
|
throw new Error("Model download failed: file was not created.");
|
|
29
26
|
return modelPath;
|
|
30
27
|
}
|
|
31
|
-
export async function loadLocalModel(modelPath, modelId, profile
|
|
28
|
+
export async function loadLocalModel(modelPath, modelId, profile, ui) {
|
|
32
29
|
const selectedModel = MODEL_REGISTRY[modelId];
|
|
33
30
|
const runtime = modelRuntime(profile);
|
|
34
31
|
const runningModel = await runningServerModel(runtime.baseUrl);
|
|
35
32
|
if (runningModel) {
|
|
36
33
|
if (runningModel !== modelId)
|
|
37
|
-
await stopManagedServer(runtime);
|
|
34
|
+
await stopManagedServer(runtime, ui);
|
|
38
35
|
if (runningModel === modelId) {
|
|
39
|
-
|
|
36
|
+
ui.info(`Using local model: ${selectedModel.name}`);
|
|
40
37
|
return {
|
|
41
38
|
id: modelId,
|
|
42
39
|
name: selectedModel.name,
|
|
@@ -46,8 +43,16 @@ export async function loadLocalModel(modelPath, modelId, profile = "scout") {
|
|
|
46
43
|
}
|
|
47
44
|
}
|
|
48
45
|
await ensureLlamaServerBinary();
|
|
49
|
-
await startLlamaServer(modelPath, modelId, selectedModel.name, runtime);
|
|
50
|
-
|
|
46
|
+
await startLlamaServer(modelPath, modelId, selectedModel.name, runtime, ui);
|
|
47
|
+
const ready = ui.spinner(`Waiting for local ${profile} model`);
|
|
48
|
+
try {
|
|
49
|
+
await waitForServer(runtime.baseUrl, modelId);
|
|
50
|
+
ready.stop(`Local ${profile} model ready`);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
ready.error(`Local ${profile} model failed to start`);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
51
56
|
return {
|
|
52
57
|
id: modelId,
|
|
53
58
|
name: selectedModel.name,
|
|
@@ -101,14 +106,14 @@ Install llama.cpp first:
|
|
|
101
106
|
brew install llama.cpp`);
|
|
102
107
|
}
|
|
103
108
|
}
|
|
104
|
-
async function startLlamaServer(modelPath, modelId, modelName, runtime) {
|
|
109
|
+
async function startLlamaServer(modelPath, modelId, modelName, runtime, ui) {
|
|
105
110
|
const logDir = path.join(cacheDir(), "logs");
|
|
106
111
|
await mkdir(logDir, { recursive: true });
|
|
107
112
|
const logPath = path.join(logDir, "llama-server.log");
|
|
108
113
|
const out = await open(logPath, "a");
|
|
109
114
|
const err = await open(logPath, "a");
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
ui.step(`Starting local model server: ${modelName}`);
|
|
116
|
+
ui.info(`llama-server log: ${logPath}`);
|
|
112
117
|
const args = [
|
|
113
118
|
"-m",
|
|
114
119
|
modelPath,
|
|
@@ -151,14 +156,14 @@ async function startLlamaServer(modelPath, modelId, modelName, runtime) {
|
|
|
151
156
|
await out.close();
|
|
152
157
|
await err.close();
|
|
153
158
|
}
|
|
154
|
-
async function stopManagedServer(runtime) {
|
|
159
|
+
async function stopManagedServer(runtime, ui) {
|
|
155
160
|
const pid = await managedServerPid(runtime);
|
|
156
161
|
if (!pid) {
|
|
157
162
|
const runningModel = await runningServerModel(runtime.baseUrl);
|
|
158
163
|
throw new Error(`A llama-server is already running with ${runningModel ?? "another model"}.
|
|
159
164
|
Stop it before switching models, or use STUPIFY_LLAMA_SERVER_URL for that server.`);
|
|
160
165
|
}
|
|
161
|
-
|
|
166
|
+
ui.step("Restarting local model server for selected model.");
|
|
162
167
|
try {
|
|
163
168
|
process.kill(pid, "SIGTERM");
|
|
164
169
|
}
|
|
@@ -214,10 +219,11 @@ async function waitForServer(baseUrl, modelId) {
|
|
|
214
219
|
function sleep(ms) {
|
|
215
220
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
216
221
|
}
|
|
217
|
-
async function downloadModel(modelPath, modelUrl) {
|
|
222
|
+
async function downloadModel(modelPath, modelUrl, ui) {
|
|
218
223
|
const tempPath = `${modelPath}.download`;
|
|
219
224
|
await rm(tempPath, { force: true });
|
|
220
|
-
|
|
225
|
+
const downloadSpinner = ui.spinner("Downloading model");
|
|
226
|
+
let downloadProgress = null;
|
|
221
227
|
try {
|
|
222
228
|
const response = await fetch(modelUrl);
|
|
223
229
|
if (!response.ok || !response.body)
|
|
@@ -225,8 +231,13 @@ async function downloadModel(modelPath, modelUrl) {
|
|
|
225
231
|
const total = Number(response.headers.get("content-length") ?? 0);
|
|
226
232
|
let received = 0;
|
|
227
233
|
let lastPrint = 0;
|
|
234
|
+
let lastProgressBytes = 0;
|
|
228
235
|
const reader = response.body.getReader();
|
|
229
236
|
const file = await open(tempPath, "wx");
|
|
237
|
+
if (total > 0) {
|
|
238
|
+
downloadSpinner.clear();
|
|
239
|
+
downloadProgress = ui.progress("Downloading model", total);
|
|
240
|
+
}
|
|
230
241
|
try {
|
|
231
242
|
while (true) {
|
|
232
243
|
const { done, value } = await reader.read();
|
|
@@ -237,42 +248,30 @@ async function downloadModel(modelPath, modelUrl) {
|
|
|
237
248
|
const now = Date.now();
|
|
238
249
|
if (total > 0 && now - lastPrint > 500) {
|
|
239
250
|
lastPrint = now;
|
|
240
|
-
|
|
251
|
+
downloadProgress?.advance(received - lastProgressBytes, `${formatBytes(received)} / ${formatBytes(total)}`);
|
|
252
|
+
lastProgressBytes = received;
|
|
241
253
|
}
|
|
242
254
|
}
|
|
243
255
|
}
|
|
244
256
|
finally {
|
|
245
257
|
await file.close();
|
|
246
258
|
}
|
|
247
|
-
if (
|
|
248
|
-
|
|
259
|
+
if (downloadProgress && received > lastProgressBytes) {
|
|
260
|
+
downloadProgress.advance(received - lastProgressBytes, `${formatBytes(received)} / ${formatBytes(total)}`);
|
|
261
|
+
}
|
|
262
|
+
const activeProgress = downloadProgress ?? downloadSpinner;
|
|
263
|
+
activeProgress.stop(total > 0
|
|
264
|
+
? `Downloaded ${formatBytes(received)} / ${formatBytes(total)}`
|
|
265
|
+
: "Downloaded model");
|
|
249
266
|
await rename(tempPath, modelPath);
|
|
250
267
|
}
|
|
251
268
|
catch (error) {
|
|
269
|
+
const activeProgress = downloadProgress ?? downloadSpinner;
|
|
270
|
+
activeProgress.error("Model download failed");
|
|
252
271
|
await rm(tempPath, { force: true });
|
|
253
272
|
throw error;
|
|
254
273
|
}
|
|
255
274
|
}
|
|
256
|
-
async function confirm(question) {
|
|
257
|
-
const rl = createInterface(terminalIo());
|
|
258
|
-
try {
|
|
259
|
-
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
260
|
-
return answer === "y" || answer === "yes";
|
|
261
|
-
}
|
|
262
|
-
finally {
|
|
263
|
-
rl.close();
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
function terminalIo() {
|
|
267
|
-
if (input.isTTY)
|
|
268
|
-
return { input, output };
|
|
269
|
-
if (platform() !== "win32")
|
|
270
|
-
return {
|
|
271
|
-
input: createReadStream("/dev/tty"),
|
|
272
|
-
output: createWriteStream("/dev/tty"),
|
|
273
|
-
};
|
|
274
|
-
throw new Error("No local Stupify model found. Run `stupify` once in an interactive terminal to set up the model.");
|
|
275
|
-
}
|
|
276
275
|
function cacheDir() {
|
|
277
276
|
if (process.env.STUPIFY_CACHE_DIR)
|
|
278
277
|
return process.env.STUPIFY_CACHE_DIR;
|
package/dist/render.js
CHANGED
|
@@ -1,38 +1,37 @@
|
|
|
1
1
|
import { VERSION } from "./constants.js";
|
|
2
|
+
import { format } from "./ui.js";
|
|
2
3
|
export function renderSearchRun(run, command) {
|
|
3
4
|
if (command.json)
|
|
4
5
|
return JSON.stringify(run, null, 2);
|
|
5
6
|
if (run.stats.skipped && run.stats.skipReason === "input_too_large") {
|
|
6
|
-
return
|
|
7
|
-
|
|
8
|
-
Size:
|
|
7
|
+
return `${format.heading("Search input is too large for precise local search.")}
|
|
8
|
+
${format.heading("Size:")}
|
|
9
9
|
~${run.stats.inputTokens ?? "unknown"} tokens
|
|
10
|
-
Limit:
|
|
10
|
+
${format.heading("Limit:")}
|
|
11
11
|
${run.stats.inputTokenCap ?? "unknown"} tokens
|
|
12
12
|
Stupify skipped the search rather than review truncated context.
|
|
13
13
|
Nothing was blocked.
|
|
14
|
-
Try:
|
|
14
|
+
${format.heading("Try:")}
|
|
15
15
|
rerun with ${sourceHint(command)} --max-search-input-tokens ${Math.max((run.stats.inputTokens ?? 12_000) + 1, (run.stats.inputTokenCap ?? 12_000) * 2)}`;
|
|
16
16
|
}
|
|
17
17
|
if (run.stats.skipped && run.stats.skipReason === "no_candidates") {
|
|
18
|
-
return
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
No search targets found.`;
|
|
18
|
+
return `${format.heading("Search complete.")}
|
|
19
|
+
${format.label("Patterns:")} ${run.patterns.join(", ")}
|
|
20
|
+
${format.success("No search targets found.")}`;
|
|
22
21
|
}
|
|
23
22
|
if (run.matches.length === 0) {
|
|
24
|
-
return
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
No judgment-offload signals found.`;
|
|
23
|
+
return `${format.heading("Search complete.")}
|
|
24
|
+
${format.label("Patterns:")} ${run.patterns.join(", ")}
|
|
25
|
+
${format.success("No judgment-offload signals found.")}`;
|
|
28
26
|
}
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
${
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
return `${format.warn("AI SLOP DETECTED")}
|
|
28
|
+
${run.matches.map((match, index) => `${index + 1}.
|
|
29
|
+
${format.muted("who:")} ${committerLabel(run)}
|
|
30
|
+
${format.muted("what:")} ${match.patternId} - ${match.reason}
|
|
31
|
+
${format.muted("when:")} ${sourceLabel(command)}
|
|
32
|
+
${format.muted("where:")} ${match.proof}
|
|
33
|
+
${format.muted("why:")} ${match.checkWhy ?? "This pattern may indicate judgment-offload."}`).join("\n")}
|
|
34
|
+
${format.muted("Search mode is warn-only.")}`;
|
|
36
35
|
}
|
|
37
36
|
export function helpText() {
|
|
38
37
|
return `Stupify ${VERSION}
|
|
@@ -92,3 +91,22 @@ function sourceHint(command) {
|
|
|
92
91
|
return `--commits ${command.count}`;
|
|
93
92
|
return "--stdin";
|
|
94
93
|
}
|
|
94
|
+
function sourceLabel(command) {
|
|
95
|
+
if (command.kind === "staged")
|
|
96
|
+
return "staged changes";
|
|
97
|
+
if (command.kind === "since")
|
|
98
|
+
return `since ${command.since}`;
|
|
99
|
+
if (command.kind === "commit")
|
|
100
|
+
return `commit ${command.commit}`;
|
|
101
|
+
if (command.kind === "commits")
|
|
102
|
+
return `last ${command.count} commits`;
|
|
103
|
+
return "stdin diff";
|
|
104
|
+
}
|
|
105
|
+
function committerLabel(run) {
|
|
106
|
+
const committers = (run.stats.committers ?? []).filter(Boolean);
|
|
107
|
+
if (committers.length === 0)
|
|
108
|
+
return "unknown committer";
|
|
109
|
+
if (committers.length <= 3)
|
|
110
|
+
return committers.join(", ");
|
|
111
|
+
return `${committers.slice(0, 3).join(", ")} +${committers.length - 3} more`;
|
|
112
|
+
}
|
package/dist/sem-provider.js
CHANGED
|
@@ -6,17 +6,19 @@ import path from "node:path";
|
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { cachedJson, fingerprint } from "./cache.js";
|
|
8
8
|
import { readDiffFromStdin } from "./diff.js";
|
|
9
|
-
import { sourceRangeForCommit, sourceRangeForRecentCommits, sourceRangeSince, stagedDiff, } from "./git.js";
|
|
9
|
+
import { gitUserLabel, sourceRangeForCommit, sourceRangeForRecentCommits, sourceRangeSince, stagedDiff, } from "./git.js";
|
|
10
|
+
import { diagnostic } from "./ui.js";
|
|
10
11
|
import { sourceId } from "./types.js";
|
|
11
12
|
const execFileAsync = promisify(execFile);
|
|
12
13
|
export async function semChangeSetForCommand(command) {
|
|
13
14
|
if (command.kind === "stdin")
|
|
14
|
-
return semChangeSetFromPatch(await readDiffFromStdin(), command.debugSem);
|
|
15
|
+
return semChangeSetFromPatch(await readDiffFromStdin(), command.debugSem, "stdin", ["stdin"]);
|
|
15
16
|
if (command.kind === "staged") {
|
|
16
|
-
const diff = await stagedDiff();
|
|
17
|
+
const [diff, committer] = await Promise.all([stagedDiff(), gitUserLabel()]);
|
|
18
|
+
const committers = [committer];
|
|
17
19
|
if (!diff.text.trim())
|
|
18
|
-
return emptyChangeSet("staged", diff.stats);
|
|
19
|
-
return semChangeSetFromPatch(diff.text, command.debugSem, "staged");
|
|
20
|
+
return emptyChangeSet("staged", diff.stats, committers);
|
|
21
|
+
return semChangeSetFromPatch(diff.text, command.debugSem, "staged", committers);
|
|
20
22
|
}
|
|
21
23
|
if (command.kind === "commit") {
|
|
22
24
|
const range = await sourceRangeForCommit(command.commit);
|
|
@@ -27,12 +29,13 @@ export async function semChangeSetForCommand(command) {
|
|
|
27
29
|
const raw = await cachedSemDiff(["diff", "--from", range.base, "--to", range.target, "--format", "json"], range, command.debugSem);
|
|
28
30
|
return withContextWorkspace(normalizeSemDiff(raw, range), command.debugSem);
|
|
29
31
|
}
|
|
30
|
-
function emptyChangeSet(label, stats) {
|
|
32
|
+
function emptyChangeSet(label, stats, committers) {
|
|
31
33
|
return {
|
|
32
34
|
id: sourceId(label),
|
|
33
35
|
label,
|
|
34
36
|
base: label,
|
|
35
37
|
target: label,
|
|
38
|
+
committers,
|
|
36
39
|
contextCwd: process.cwd(),
|
|
37
40
|
cleanup: async () => undefined,
|
|
38
41
|
changes: [],
|
|
@@ -56,7 +59,7 @@ async function semRangeForCommand(command) {
|
|
|
56
59
|
return sourceRangeForRecentCommits(command.count);
|
|
57
60
|
throw new Error("sem cannot resolve stdin as a git range.");
|
|
58
61
|
}
|
|
59
|
-
async function semChangeSetFromPatch(patch, debugSem, label = "stdin") {
|
|
62
|
+
async function semChangeSetFromPatch(patch, debugSem, label = "stdin", committers) {
|
|
60
63
|
if (!patch.trim())
|
|
61
64
|
throw new Error("No diff received on stdin.");
|
|
62
65
|
const raw = await cachedJson("sem-diff", fingerprint({
|
|
@@ -71,6 +74,7 @@ async function semChangeSetFromPatch(patch, debugSem, label = "stdin") {
|
|
|
71
74
|
label,
|
|
72
75
|
base: label,
|
|
73
76
|
target: label,
|
|
77
|
+
committers,
|
|
74
78
|
stats: { filesChanged: 0, additions: 0, deletions: 0 },
|
|
75
79
|
}),
|
|
76
80
|
contextCwd: process.cwd(),
|
|
@@ -105,7 +109,7 @@ async function withContextWorkspace(changeSet, debugSem) {
|
|
|
105
109
|
}
|
|
106
110
|
async function runSem(args, debugSem, cwd = process.cwd()) {
|
|
107
111
|
if (debugSem)
|
|
108
|
-
|
|
112
|
+
diagnostic(`sem ${args.join(" ")}`);
|
|
109
113
|
const { command, commandArgs } = resolveSemCommand(args);
|
|
110
114
|
try {
|
|
111
115
|
const { stdout, stderr } = await execFileAsync(command, commandArgs, {
|
|
@@ -113,7 +117,7 @@ async function runSem(args, debugSem, cwd = process.cwd()) {
|
|
|
113
117
|
maxBuffer: 128 * 1024 * 1024,
|
|
114
118
|
});
|
|
115
119
|
if (debugSem && stderr.trim())
|
|
116
|
-
|
|
120
|
+
diagnostic(stderr.trim());
|
|
117
121
|
return JSON.parse(stdout);
|
|
118
122
|
}
|
|
119
123
|
catch (error) {
|
|
@@ -123,7 +127,7 @@ async function runSem(args, debugSem, cwd = process.cwd()) {
|
|
|
123
127
|
}
|
|
124
128
|
async function runSemWithInput(args, stdin, debugSem) {
|
|
125
129
|
if (debugSem)
|
|
126
|
-
|
|
130
|
+
diagnostic(`sem ${args.join(" ")}`);
|
|
127
131
|
const { command, commandArgs } = resolveSemCommand(args);
|
|
128
132
|
return new Promise((resolve, reject) => {
|
|
129
133
|
const child = spawn(command, commandArgs, { stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -135,7 +139,7 @@ async function runSemWithInput(args, stdin, debugSem) {
|
|
|
135
139
|
child.on("close", (code) => {
|
|
136
140
|
const stderrText = Buffer.concat(stderr).toString("utf8");
|
|
137
141
|
if (debugSem && stderrText.trim())
|
|
138
|
-
|
|
142
|
+
diagnostic(stderrText.trim());
|
|
139
143
|
if (code !== 0) {
|
|
140
144
|
reject(new Error(`sem failed with exit code ${code}${stderrText ? `: ${stderrText.trim()}` : ""}`));
|
|
141
145
|
return;
|
|
@@ -152,7 +156,7 @@ async function runSemWithInput(args, stdin, debugSem) {
|
|
|
152
156
|
}
|
|
153
157
|
async function git(args, debugSem) {
|
|
154
158
|
if (debugSem)
|
|
155
|
-
|
|
159
|
+
diagnostic(`git ${args.join(" ")}`);
|
|
156
160
|
await execFileAsync("git", [...args], { maxBuffer: 128 * 1024 * 1024 });
|
|
157
161
|
}
|
|
158
162
|
async function cleanupWorktree(tempDir, worktreeAdded, debugSem) {
|
|
@@ -194,6 +198,7 @@ function normalizeSemDiff(value, range) {
|
|
|
194
198
|
label: range.label,
|
|
195
199
|
base: range.base,
|
|
196
200
|
target: range.target,
|
|
201
|
+
committers: range.committers,
|
|
197
202
|
contextCwd: process.cwd(),
|
|
198
203
|
cleanup: async () => undefined,
|
|
199
204
|
changes,
|
package/dist/stupify.d.ts
CHANGED
|
@@ -1,4 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import type { SearchCommand, SearchRunJson } from "./types.ts";
|
|
3
3
|
export declare function main(argv?: string[]): Promise<number>;
|
|
4
|
-
export declare function runSearchCommand(command: SearchCommand, startedAt: number
|
|
4
|
+
export declare function runSearchCommand(command: SearchCommand, startedAt: number, ui?: {
|
|
5
|
+
intro(title: string, logOptions?: Readonly<{
|
|
6
|
+
force?: boolean;
|
|
7
|
+
}>): void;
|
|
8
|
+
outro(message: string, logOptions?: Readonly<{
|
|
9
|
+
force?: boolean;
|
|
10
|
+
}>): void;
|
|
11
|
+
note(message: string, title?: string, logOptions?: Readonly<{
|
|
12
|
+
force?: boolean;
|
|
13
|
+
}>): void;
|
|
14
|
+
info(message: string, logOptions?: Readonly<{
|
|
15
|
+
force?: boolean;
|
|
16
|
+
}>): void;
|
|
17
|
+
step(message: string, logOptions?: Readonly<{
|
|
18
|
+
force?: boolean;
|
|
19
|
+
}>): void;
|
|
20
|
+
success(message: string, logOptions?: Readonly<{
|
|
21
|
+
force?: boolean;
|
|
22
|
+
}>): void;
|
|
23
|
+
warn(message: string, logOptions?: Readonly<{
|
|
24
|
+
force?: boolean;
|
|
25
|
+
}>): void;
|
|
26
|
+
error(message: string, logOptions?: Readonly<{
|
|
27
|
+
force?: boolean;
|
|
28
|
+
}>): void;
|
|
29
|
+
debug(message: string): void;
|
|
30
|
+
confirm(message: string): Promise<boolean>;
|
|
31
|
+
spinner(message: string, logOptions?: Readonly<{
|
|
32
|
+
force?: boolean;
|
|
33
|
+
}>): import("@clack/prompts").SpinnerResult;
|
|
34
|
+
progress(message: string, max: number, logOptions?: Readonly<{
|
|
35
|
+
force?: boolean;
|
|
36
|
+
}>): import("@clack/prompts").ProgressResult;
|
|
37
|
+
writeStdout(text: string): void;
|
|
38
|
+
}): Promise<SearchRunJson>;
|