@aayambansal/squint 0.2.6 → 0.2.8
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/README.md +1 -0
- package/dist/{chunk-43L4QXCM.js → chunk-5VKUJPOL.js} +1 -1
- package/dist/{chunk-V3M4WQLH.js → chunk-YTO2YRF7.js} +2 -1
- package/dist/cli.js +694 -445
- package/dist/{commands-7DTZB4JE.js → commands-N33DKZKC.js} +1 -1
- package/dist/{preview-UCH7H5R5.js → preview-XGASKUXR.js} +2 -2
- package/dist/{shots-J7JG2FC4.js → shots-FVG4CDVK.js} +3 -3
- package/dist/{variants-2IM274AL.js → variants-UULPQ7Q5.js} +2 -2
- package/package.json +1 -1
- package/dist/{chunk-YZEFBPBJ.js → chunk-CHZE3UHK.js} +4 -4
- package/dist/{chunk-LVA2OQSR.js → chunk-X6MDBKED.js} +3 -3
package/dist/cli.js
CHANGED
|
@@ -1,34 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
enrich
|
|
4
|
-
} from "./chunk-4LAU5TGK.js";
|
|
5
|
-
import {
|
|
6
|
-
diffStatSince,
|
|
7
|
-
isGitRepo,
|
|
8
|
-
restoreSnapshot,
|
|
9
|
-
takeSnapshot
|
|
10
|
-
} from "./chunk-YHRAOBI2.js";
|
|
11
2
|
import {
|
|
12
3
|
DevServer,
|
|
13
4
|
buildFixPrompt,
|
|
14
5
|
detectDevCommand,
|
|
15
6
|
screenshotVariants
|
|
16
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-5VKUJPOL.js";
|
|
17
8
|
import {
|
|
18
9
|
applyVariant,
|
|
19
10
|
cleanVariants,
|
|
20
11
|
listVariants,
|
|
21
12
|
runVariants
|
|
22
|
-
} from "./chunk-
|
|
23
|
-
import {
|
|
24
|
-
composePrompt
|
|
25
|
-
} from "./chunk-P3H4N2EN.js";
|
|
26
|
-
import {
|
|
27
|
-
runAgent
|
|
28
|
-
} from "./chunk-C7WKNJG6.js";
|
|
29
|
-
import {
|
|
30
|
-
completeCommand
|
|
31
|
-
} from "./chunk-V3M4WQLH.js";
|
|
13
|
+
} from "./chunk-X6MDBKED.js";
|
|
32
14
|
import {
|
|
33
15
|
buildGatePrompt,
|
|
34
16
|
detectFastGates,
|
|
@@ -45,8 +27,13 @@ import {
|
|
|
45
27
|
probeRuntime,
|
|
46
28
|
runtimeSummary,
|
|
47
29
|
saveState
|
|
48
|
-
} from "./chunk-
|
|
49
|
-
import
|
|
30
|
+
} from "./chunk-CHZE3UHK.js";
|
|
31
|
+
import {
|
|
32
|
+
completeCommand
|
|
33
|
+
} from "./chunk-YTO2YRF7.js";
|
|
34
|
+
import {
|
|
35
|
+
runAgent
|
|
36
|
+
} from "./chunk-C7WKNJG6.js";
|
|
50
37
|
import {
|
|
51
38
|
findChrome
|
|
52
39
|
} from "./chunk-B7LOERSP.js";
|
|
@@ -54,10 +41,24 @@ import {
|
|
|
54
41
|
detectEngines,
|
|
55
42
|
getEngine
|
|
56
43
|
} from "./chunk-BWZFACBT.js";
|
|
44
|
+
import "./chunk-OC6RU6XH.js";
|
|
45
|
+
import {
|
|
46
|
+
enrich
|
|
47
|
+
} from "./chunk-4LAU5TGK.js";
|
|
48
|
+
import {
|
|
49
|
+
composePrompt
|
|
50
|
+
} from "./chunk-P3H4N2EN.js";
|
|
51
|
+
import {
|
|
52
|
+
diffStatSince,
|
|
53
|
+
isGitRepo,
|
|
54
|
+
restoreSnapshot,
|
|
55
|
+
takeSnapshot
|
|
56
|
+
} from "./chunk-YHRAOBI2.js";
|
|
57
57
|
|
|
58
58
|
// src/cli.tsx
|
|
59
59
|
import { Command } from "commander";
|
|
60
|
-
|
|
60
|
+
|
|
61
|
+
// src/cli/configCmd.ts
|
|
61
62
|
import pc from "picocolors";
|
|
62
63
|
|
|
63
64
|
// src/config/config.ts
|
|
@@ -82,6 +83,8 @@ var ConfigSchema = z.object({
|
|
|
82
83
|
bell: z.boolean().optional(),
|
|
83
84
|
/** Session budget in USD; crossing it warns (never blocks). */
|
|
84
85
|
budgetUsd: z.number().positive().optional(),
|
|
86
|
+
/** Auto-run /review when the visual pulse shows a big change (default off). */
|
|
87
|
+
autoReview: z.boolean().optional(),
|
|
85
88
|
/** TUI theme name (amber, ocean, moss, rose, mono). */
|
|
86
89
|
theme: z.string().optional()
|
|
87
90
|
});
|
|
@@ -126,7 +129,7 @@ function setConfigValue(file, key, value) {
|
|
|
126
129
|
let next;
|
|
127
130
|
if (key === "engine" || key === "theme") {
|
|
128
131
|
next = { ...current, [key]: value };
|
|
129
|
-
} else if (key === "autoDev" || key === "autoFix" || key === "autoProbe" || key === "autoCheck" || key === "bell") {
|
|
132
|
+
} else if (key === "autoDev" || key === "autoFix" || key === "autoProbe" || key === "autoCheck" || key === "autoReview" || key === "bell") {
|
|
130
133
|
if (value !== "true" && value !== "false") {
|
|
131
134
|
throw new Error(`"${key}" must be true or false`);
|
|
132
135
|
}
|
|
@@ -143,7 +146,7 @@ function setConfigValue(file, key, value) {
|
|
|
143
146
|
next = { ...current, models: { ...current.models, [engineId]: value } };
|
|
144
147
|
} else {
|
|
145
148
|
throw new Error(
|
|
146
|
-
`Unknown config key "${key}". Supported: engine, theme, autoDev, autoFix, autoProbe, autoCheck, bell, budgetUsd, models.<engineId>`
|
|
149
|
+
`Unknown config key "${key}". Supported: engine, theme, autoDev, autoFix, autoProbe, autoCheck, autoReview, bell, budgetUsd, models.<engineId>`
|
|
147
150
|
);
|
|
148
151
|
}
|
|
149
152
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -151,13 +154,513 @@ function setConfigValue(file, key, value) {
|
|
|
151
154
|
return next;
|
|
152
155
|
}
|
|
153
156
|
|
|
157
|
+
// src/cli/configCmd.ts
|
|
158
|
+
function registerConfig(program2) {
|
|
159
|
+
const configCommand = program2.command("config").description("Read or change squint configuration");
|
|
160
|
+
configCommand.command("get").description("Print the resolved configuration").action(() => {
|
|
161
|
+
const paths = defaultPaths(process.cwd());
|
|
162
|
+
console.log(JSON.stringify(loadConfig(paths), null, 2));
|
|
163
|
+
});
|
|
164
|
+
configCommand.command("set").description("Set a value (see docs/configuration.md for every key)").argument("<key>").argument("<value>").option("--project", "write to this project (.squint/config.json) instead of global").action((key, value, options) => {
|
|
165
|
+
const paths = defaultPaths(process.cwd());
|
|
166
|
+
const file = options.project ? paths.projectFile : paths.globalFile;
|
|
167
|
+
setConfigValue(file, key, value);
|
|
168
|
+
console.log(pc.green(`\u2713 ${key} = ${value}`) + pc.dim(` (${file})`));
|
|
169
|
+
});
|
|
170
|
+
configCommand.command("path").description("Show config file locations").action(() => {
|
|
171
|
+
const paths = defaultPaths(process.cwd());
|
|
172
|
+
console.log(`global ${paths.globalFile}`);
|
|
173
|
+
console.log(`project ${paths.projectFile}`);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/cli/env.ts
|
|
178
|
+
import pc2 from "picocolors";
|
|
179
|
+
function registerEnv(program2) {
|
|
180
|
+
program2.command("engines").description("List engines: installed, streaming, session resume").action(() => {
|
|
181
|
+
console.log(pc2.dim(" id name stream resume where"));
|
|
182
|
+
for (const { engine, path: binaryPath } of detectEngines()) {
|
|
183
|
+
const status = binaryPath ? pc2.green("\u2713") : pc2.red("\u2717");
|
|
184
|
+
const stream = engine.createParser ? pc2.green("yes") : pc2.dim("text");
|
|
185
|
+
const resume = engine.supportsResume ? pc2.green("yes") : pc2.dim("no");
|
|
186
|
+
const location = binaryPath ?? pc2.dim(`not found \u2014 ${engine.install}`);
|
|
187
|
+
console.log(
|
|
188
|
+
`${status} ${engine.id.padEnd(10)} ${engine.name.padEnd(14)} ${stream.padEnd(15)} ${resume.padEnd(14)} ${location}`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
console.log(pc2.dim("\nplan/safe/yolo modes map onto every engine \xB7 squint doctor --probe verifies auth"));
|
|
192
|
+
});
|
|
193
|
+
program2.command("doctor").description("Check squint prerequisites and engine availability").option("--probe", "run each detected engine with a one-word prompt to verify auth works").action(async (options) => {
|
|
194
|
+
const [major] = process.versions.node.split(".");
|
|
195
|
+
const nodeOk = Number(major) >= 22;
|
|
196
|
+
console.log(
|
|
197
|
+
`${nodeOk ? pc2.green("\u2713") : pc2.red("\u2717")} node ${process.versions.node}${nodeOk ? "" : " (need >= 22)"}`
|
|
198
|
+
);
|
|
199
|
+
const detected = detectEngines();
|
|
200
|
+
for (const { engine, path: binaryPath } of detected) {
|
|
201
|
+
const status = binaryPath ? pc2.green("\u2713") : pc2.yellow("\u25CB");
|
|
202
|
+
console.log(`${status} ${engine.name}${binaryPath ? "" : pc2.dim(` \u2014 install: ${engine.install}`)}`);
|
|
203
|
+
}
|
|
204
|
+
if (options.probe) {
|
|
205
|
+
const { runAgent: runAgent2 } = await import("./run-S772BPMZ.js");
|
|
206
|
+
console.log(pc2.dim("\nprobing engines with a one-word prompt (verifies auth end to end)\u2026"));
|
|
207
|
+
for (const { engine, path: binaryPath } of detected) {
|
|
208
|
+
if (!binaryPath) continue;
|
|
209
|
+
const startedAt = Date.now();
|
|
210
|
+
const abort = new AbortController();
|
|
211
|
+
const timer = setTimeout(() => abort.abort(), 9e4);
|
|
212
|
+
const result = await runAgent2(
|
|
213
|
+
engine,
|
|
214
|
+
{ prompt: "Reply with exactly: ok", cwd: process.cwd() },
|
|
215
|
+
() => {
|
|
216
|
+
},
|
|
217
|
+
abort.signal
|
|
218
|
+
);
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
const secs = ((Date.now() - startedAt) / 1e3).toFixed(1);
|
|
221
|
+
const detail = (result.error ?? "failed").split("\n").filter((l) => l.trim()).at(-1) ?? "failed";
|
|
222
|
+
console.log(
|
|
223
|
+
result.ok ? `${pc2.green("\u2713")} ${engine.id.padEnd(10)} responded in ${secs}s` : `${pc2.red("\u2717")} ${engine.id.padEnd(10)} ${pc2.dim(detail.slice(0, 110))}`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const { findChrome: findChrome2 } = await import("./chrome-NVU47PLK.js");
|
|
228
|
+
const { hasWebSocket } = await import("./cdp-SFWNP7OA.js");
|
|
229
|
+
const chrome = findChrome2();
|
|
230
|
+
console.log(
|
|
231
|
+
chrome ? `${pc2.green("\u2713")} Chrome ${pc2.dim(chrome)}` : `${pc2.yellow("\u25CB")} Chrome ${pc2.dim("\u2014 screenshots and runtime probing disabled")}`
|
|
232
|
+
);
|
|
233
|
+
console.log(
|
|
234
|
+
hasWebSocket() ? `${pc2.green("\u2713")} WebSocket ${pc2.dim("runtime console/network capture available")}` : `${pc2.yellow("\u25CB")} WebSocket ${pc2.dim("\u2014 node 22+ enables runtime capture; screenshots still work")}`
|
|
235
|
+
);
|
|
236
|
+
const available = detected.filter((d) => d.path !== null);
|
|
237
|
+
if (available.length === 0) {
|
|
238
|
+
console.log(pc2.red("\nNo engines found. Install at least one to use squint."));
|
|
239
|
+
process.exitCode = 1;
|
|
240
|
+
} else {
|
|
241
|
+
console.log(pc2.dim(`
|
|
242
|
+
${available.length} engine(s) ready.`));
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/cli/project.ts
|
|
248
|
+
import pc3 from "picocolors";
|
|
249
|
+
function registerProject(program2) {
|
|
250
|
+
const skillsCommand = program2.command("skills").description("Project knowledge injected into asks (.squint/rules.md + .squint/skills/)");
|
|
251
|
+
skillsCommand.command("list").description("Show always-on rules and trigger-matched skills").action(async () => {
|
|
252
|
+
const { loadRules, loadSkills } = await import("./skills-DNBHZAAU.js");
|
|
253
|
+
const cwd = process.cwd();
|
|
254
|
+
const rules = loadRules(cwd);
|
|
255
|
+
console.log(
|
|
256
|
+
rules ? `${pc3.green("\u2713")} rules.md ${pc3.dim(`(${rules.split("\n").length} lines, always on)`)}` : pc3.dim("\u25CB no .squint/rules.md")
|
|
257
|
+
);
|
|
258
|
+
const skills = loadSkills(cwd);
|
|
259
|
+
if (skills.length === 0) {
|
|
260
|
+
console.log(pc3.dim("\u25CB no skills \u2014 squint skills init writes an example"));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
for (const skill of skills) {
|
|
264
|
+
console.log(`${pc3.green("\u2713")} ${skill.name.padEnd(20)} ${pc3.dim(`triggers: ${skill.triggers.join(", ")}`)}`);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
skillsCommand.command("init").description("Scaffold .squint/rules.md and an example skill").action(async () => {
|
|
268
|
+
const fs3 = await import("fs");
|
|
269
|
+
const nodePath = await import("path");
|
|
270
|
+
const cwd = process.cwd();
|
|
271
|
+
const skillsDir = nodePath.join(cwd, ".squint", "skills");
|
|
272
|
+
fs3.mkdirSync(skillsDir, { recursive: true });
|
|
273
|
+
const rules = nodePath.join(cwd, ".squint", "rules.md");
|
|
274
|
+
if (!fs3.existsSync(rules)) {
|
|
275
|
+
fs3.writeFileSync(
|
|
276
|
+
rules,
|
|
277
|
+
"# Project rules\n\nThese ride along on every squint ask. Keep them short \u2014 cut anything that would not cause a mistake if removed.\n"
|
|
278
|
+
);
|
|
279
|
+
console.log(pc3.green("\u2713 .squint/rules.md"));
|
|
280
|
+
}
|
|
281
|
+
const example = nodePath.join(skillsDir, "example.md");
|
|
282
|
+
if (!fs3.existsSync(example)) {
|
|
283
|
+
fs3.writeFileSync(
|
|
284
|
+
example,
|
|
285
|
+
"---\ntriggers: example, sample\n---\n\nThis note is injected only when an ask mentions one of the triggers above.\nDocument the parts of this repo an agent would otherwise rediscover every time:\nwhere state lives, which helpers to reuse, what not to touch.\n"
|
|
286
|
+
);
|
|
287
|
+
console.log(pc3.green("\u2713 .squint/skills/example.md"));
|
|
288
|
+
}
|
|
289
|
+
console.log(pc3.dim("rules are always-on; skills inject when an ask mentions a trigger"));
|
|
290
|
+
});
|
|
291
|
+
program2.command("brief").description("Set a committed design direction for this project (.squint/brief.md)").argument("[family]", "aesthetic family id (omit to list)").option("--force", "overwrite an existing project brief").action(async (familyId, options) => {
|
|
292
|
+
const fs3 = await import("fs");
|
|
293
|
+
const nodePath = await import("path");
|
|
294
|
+
const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-2G5SLLY7.js");
|
|
295
|
+
if (!familyId) {
|
|
296
|
+
console.log(pc3.bold("Aesthetic families") + pc3.dim(" \u2014 squint brief <id>\n"));
|
|
297
|
+
for (const family2 of FAMILIES) {
|
|
298
|
+
console.log(`${pc3.green(family2.id.padEnd(18))} ${family2.name.padEnd(22)} ${pc3.dim(family2.summary)}`);
|
|
299
|
+
}
|
|
300
|
+
console.log(pc3.dim("\nThe brief wraps every ask; edit .squint/brief.md to remix."));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const family = getFamily(familyId);
|
|
304
|
+
if (!family) {
|
|
305
|
+
console.error(pc3.red(`\u2717 unknown family "${familyId}" \u2014 run squint brief to list`));
|
|
306
|
+
process.exitCode = 1;
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const target = nodePath.join(process.cwd(), ".squint", "brief.md");
|
|
310
|
+
if (fs3.existsSync(target) && !options.force) {
|
|
311
|
+
console.error(pc3.red(`\u2717 ${target} exists \u2014 use --force to overwrite`));
|
|
312
|
+
process.exitCode = 1;
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
fs3.mkdirSync(nodePath.dirname(target), { recursive: true });
|
|
316
|
+
fs3.writeFileSync(target, renderFamilyBrief(family) + "\n");
|
|
317
|
+
console.log(pc3.green(`\u2713 ${family.name} direction written to .squint/brief.md`));
|
|
318
|
+
console.log(pc3.dim("every squint ask in this repo now holds this direction \u2014 edit the file to remix"));
|
|
319
|
+
});
|
|
320
|
+
const variantsCommand = program2.command("variants").description("Parallel design explorations \u2014 one aesthetic family each, pick with your eyes");
|
|
321
|
+
variantsCommand.command("gen").description("Generate n variants of one ask in parallel (n engine runs \u2014 n\xD7 cost)").argument("<n>", "how many variants (max 4)").argument("<prompt...>", "what to build").option("-e, --engine <id>", "engine to use").option("-m, --model <name>", "model override").option("--no-shots", "skip the screenshot pass").action(
|
|
322
|
+
async (nRaw, promptWords, options) => {
|
|
323
|
+
const cwd = process.cwd();
|
|
324
|
+
const n = Number.parseInt(nRaw, 10);
|
|
325
|
+
if (!Number.isInteger(n) || n < 2 || n > 4) {
|
|
326
|
+
console.error(pc3.red("\u2717 n must be 2\u20134"));
|
|
327
|
+
process.exitCode = 1;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const { isGitRepo: isGitRepo2 } = await import("./snapshot-H6VYFHLN.js");
|
|
331
|
+
if (!isGitRepo2(cwd)) {
|
|
332
|
+
console.error(pc3.red("\u2717 variants need a git repo with at least one commit"));
|
|
333
|
+
process.exitCode = 1;
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const { runVariants: runVariants2, cleanVariants: cleanVariants2 } = await import("./variants-UULPQ7Q5.js");
|
|
337
|
+
const config = loadConfig(defaultPaths(cwd));
|
|
338
|
+
const engineId = resolveEngineId(config, options.engine);
|
|
339
|
+
const engine = getEngine(engineId);
|
|
340
|
+
const model = resolveModel(config, engineId, options.model);
|
|
341
|
+
const ask = promptWords.join(" ");
|
|
342
|
+
cleanVariants2(cwd);
|
|
343
|
+
console.log(pc3.dim(`generating ${n} directions in parallel via ${engine.id} \u2014 this runs ${n} engine sessions`));
|
|
344
|
+
const runs = await runVariants2(
|
|
345
|
+
cwd,
|
|
346
|
+
ask,
|
|
347
|
+
n,
|
|
348
|
+
engine,
|
|
349
|
+
model,
|
|
350
|
+
(familyId, text) => console.log(`${pc3.cyan(familyId.padEnd(18))} ${pc3.dim(text)}`)
|
|
351
|
+
);
|
|
352
|
+
const succeeded = runs.filter((r) => r.result.ok);
|
|
353
|
+
if (options.shots && succeeded.length > 0) {
|
|
354
|
+
const { screenshotVariants: screenshotVariants2 } = await import("./shots-FVG4CDVK.js");
|
|
355
|
+
console.log(pc3.dim("capturing screenshots\u2026"));
|
|
356
|
+
const shots = await screenshotVariants2(cwd, succeeded.map((r) => r.variant));
|
|
357
|
+
for (const shot of shots) {
|
|
358
|
+
console.log(`${pc3.green("\u2713")} ${shot.familyId.padEnd(18)} ${shot.path ?? pc3.dim(shot.error ?? "")}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
console.log(
|
|
362
|
+
`
|
|
363
|
+
${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 ` + pc3.bold("squint variants apply <id>") + pc3.dim(" applies the winner, squint variants clean discards all")
|
|
364
|
+
);
|
|
365
|
+
if (succeeded.length === 0) process.exitCode = 1;
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
variantsCommand.command("list").description("List generated variants").action(async () => {
|
|
369
|
+
const { listVariants: listVariants2 } = await import("./variants-UULPQ7Q5.js");
|
|
370
|
+
const ids = listVariants2(process.cwd());
|
|
371
|
+
if (ids.length === 0) {
|
|
372
|
+
console.log(pc3.dim('no variants \u2014 squint variants gen <n> "<ask>"'));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
for (const id of ids) console.log(id);
|
|
376
|
+
});
|
|
377
|
+
variantsCommand.command("apply").description("Apply one variant\u2019s changes to the main tree and discard the rest").argument("<id>", "family id of the winning variant").action(async (id) => {
|
|
378
|
+
const { applyVariant: applyVariant2, cleanVariants: cleanVariants2 } = await import("./variants-UULPQ7Q5.js");
|
|
379
|
+
const cwd = process.cwd();
|
|
380
|
+
const result = applyVariant2(cwd, id);
|
|
381
|
+
if (!result.ok) {
|
|
382
|
+
console.error(pc3.red(`\u2717 ${result.detail}`));
|
|
383
|
+
process.exitCode = 1;
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
cleanVariants2(cwd);
|
|
387
|
+
console.log(pc3.green(`\u2713 applied ${id} to the working tree`) + pc3.dim(" \u2014 review with git diff"));
|
|
388
|
+
});
|
|
389
|
+
variantsCommand.command("clean").description("Discard all variants").action(async () => {
|
|
390
|
+
const { cleanVariants: cleanVariants2 } = await import("./variants-UULPQ7Q5.js");
|
|
391
|
+
const count = cleanVariants2(process.cwd());
|
|
392
|
+
console.log(pc3.dim(`removed ${count} variant(s)`));
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/cli/quality.ts
|
|
397
|
+
import pc4 from "picocolors";
|
|
398
|
+
function registerQuality(program2) {
|
|
399
|
+
program2.command("check").description("Run this project\u2019s quality gates (typecheck, lint, format, test, build)").action(async () => {
|
|
400
|
+
const { detectGates: detectGates2, runGates: runGates2 } = await import("./gates-ZC67NMW3.js");
|
|
401
|
+
const cwd = process.cwd();
|
|
402
|
+
const gates = detectGates2(cwd);
|
|
403
|
+
if (gates.length === 0) {
|
|
404
|
+
console.log(pc4.dim("no gates detected (no package.json scripts, tsconfig, or eslint config)"));
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
console.log(pc4.dim(`running ${gates.map((g) => g.id).join(" \u2192 ")}`));
|
|
408
|
+
const results = await runGates2(cwd, gates, (result) => {
|
|
409
|
+
const mark = result.ok ? pc4.green("\u2713") : pc4.red("\u2717");
|
|
410
|
+
console.log(
|
|
411
|
+
`${mark} ${result.gate.id.padEnd(10)} ${pc4.dim(`${(result.durationMs / 1e3).toFixed(1)}s \xB7 ${result.gate.display}`)}`
|
|
412
|
+
);
|
|
413
|
+
if (!result.ok) console.log(pc4.dim(result.outputTail.split("\n").slice(-12).join("\n")));
|
|
414
|
+
});
|
|
415
|
+
if (results.some((r) => !r.ok)) process.exitCode = 1;
|
|
416
|
+
});
|
|
417
|
+
program2.command("shot").description("Screenshot a running app at mobile/tablet/desktop viewports (+ .squint/routes)").argument("<url>", "URL of the running app (e.g. http://localhost:5173)").action(async (url) => {
|
|
418
|
+
const { captureViewports: captureViewports2 } = await import("./preview-XGASKUXR.js");
|
|
419
|
+
const result = await captureViewports2(process.cwd(), url);
|
|
420
|
+
if (!result) {
|
|
421
|
+
console.error(pc4.red("\u2717 no Chrome/Chromium found"));
|
|
422
|
+
process.exitCode = 1;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
for (const shot of result.shots) console.log(`${pc4.green("\u2713")} ${shot.name.padEnd(8)} ${shot.path}`);
|
|
426
|
+
for (const error of result.errors) console.error(pc4.red(`\u2717 ${error}`));
|
|
427
|
+
if (result.shots.length === 0) process.exitCode = 1;
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/cli/run.ts
|
|
432
|
+
import pc5 from "picocolors";
|
|
433
|
+
function createPrinter() {
|
|
434
|
+
let streaming = false;
|
|
435
|
+
return (event) => {
|
|
436
|
+
switch (event.type) {
|
|
437
|
+
case "status":
|
|
438
|
+
console.log(pc5.dim(`\xB7 ${event.text}`));
|
|
439
|
+
break;
|
|
440
|
+
case "delta":
|
|
441
|
+
streaming = true;
|
|
442
|
+
process.stdout.write(event.text);
|
|
443
|
+
break;
|
|
444
|
+
case "text":
|
|
445
|
+
if (event.streamed && streaming) {
|
|
446
|
+
process.stdout.write("\n");
|
|
447
|
+
} else {
|
|
448
|
+
console.log(event.text);
|
|
449
|
+
}
|
|
450
|
+
streaming = false;
|
|
451
|
+
break;
|
|
452
|
+
case "thinking":
|
|
453
|
+
console.log(pc5.dim(pc5.italic(event.text)));
|
|
454
|
+
break;
|
|
455
|
+
case "tool":
|
|
456
|
+
if (streaming) {
|
|
457
|
+
process.stdout.write("\n");
|
|
458
|
+
streaming = false;
|
|
459
|
+
}
|
|
460
|
+
console.log(pc5.cyan(`\u2699 ${event.name}${event.detail ? ` \xB7 ${event.detail}` : ""}`));
|
|
461
|
+
break;
|
|
462
|
+
case "error":
|
|
463
|
+
console.error(pc5.red(`\u2717 ${event.text}`));
|
|
464
|
+
break;
|
|
465
|
+
case "result":
|
|
466
|
+
case "raw":
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function registerRun(program2) {
|
|
472
|
+
program2.command("run").description("Run one prompt headlessly and stream the result").argument("<prompt...>", "what to build or change").option("-e, --engine <id>", "engine to use (see squint engines)").option("-m, --model <name>", "model override for the engine").option("--mode <mode>", "plan (read-only) \xB7 safe (default) \xB7 yolo (no friction)").option("--no-brief", "send the prompt without the squint design brief").option("--json", "emit normalized agent events as ndjson").action(
|
|
473
|
+
async (promptWords, options) => {
|
|
474
|
+
const cwd = process.cwd();
|
|
475
|
+
if (options.mode && !["plan", "safe", "yolo"].includes(options.mode)) {
|
|
476
|
+
console.error(pc5.red("\u2717 --mode must be plan, safe, or yolo"));
|
|
477
|
+
process.exitCode = 1;
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const mode = options.mode;
|
|
481
|
+
const config = loadConfig(defaultPaths(cwd));
|
|
482
|
+
const engineId = resolveEngineId(config, options.engine);
|
|
483
|
+
const engine = getEngine(engineId);
|
|
484
|
+
const model = resolveModel(config, engineId, options.model);
|
|
485
|
+
const ask = promptWords.join(" ");
|
|
486
|
+
const prompt = composePrompt(ask, { cwd, noBrief: !options.brief });
|
|
487
|
+
const onEvent = options.json ? (event) => {
|
|
488
|
+
if (event.type !== "delta") console.log(JSON.stringify(event));
|
|
489
|
+
} : createPrinter();
|
|
490
|
+
if (!options.json)
|
|
491
|
+
console.log(
|
|
492
|
+
pc5.dim(`squint \xB7 ${engine.id}${model ? ` \xB7 ${model}` : ""}${mode && mode !== "safe" ? ` \xB7 ${mode}` : ""}`)
|
|
493
|
+
);
|
|
494
|
+
const result = await runAgent(engine, { prompt, cwd, model, mode }, onEvent);
|
|
495
|
+
if (options.json) {
|
|
496
|
+
console.log(JSON.stringify({ type: "summary", ...result }));
|
|
497
|
+
} else if (result.ok) {
|
|
498
|
+
const cost = result.costUsd !== void 0 ? ` \xB7 $${result.costUsd.toFixed(2)}` : "";
|
|
499
|
+
const secs = result.durationMs !== void 0 ? ` \xB7 ${(result.durationMs / 1e3).toFixed(0)}s` : "";
|
|
500
|
+
console.log(pc5.green(`\u2713 done${secs}${cost}`));
|
|
501
|
+
}
|
|
502
|
+
if (!result.ok) process.exitCode = 1;
|
|
503
|
+
}
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/cli/scaffold.ts
|
|
508
|
+
import pc6 from "picocolors";
|
|
509
|
+
function registerScaffold(program2) {
|
|
510
|
+
program2.command("init").description("Scaffold a new Vite + React + TS + Tailwind app with token-first CSS").argument("[dir]", "target directory", ".").option("--force", "write into a non-empty directory").option("--no-install", "skip npm install").action(async (dir, options) => {
|
|
511
|
+
const { installDependencies, writeTemplate } = await import("./init-LSYB32NS.js");
|
|
512
|
+
let result;
|
|
513
|
+
try {
|
|
514
|
+
result = writeTemplate(dir, { force: options.force });
|
|
515
|
+
} catch (err) {
|
|
516
|
+
console.error(pc6.red(`\u2717 ${err instanceof Error ? err.message : String(err)}`));
|
|
517
|
+
process.exitCode = 1;
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
console.log(pc6.green(`\u2713 scaffolded ${result.files.length} files in ${result.dir}`));
|
|
521
|
+
if (options.install) {
|
|
522
|
+
console.log(pc6.dim("installing dependencies\u2026"));
|
|
523
|
+
const ok = await installDependencies(result.dir);
|
|
524
|
+
if (!ok) {
|
|
525
|
+
console.error(pc6.red("\u2717 npm install failed \u2014 run it manually"));
|
|
526
|
+
process.exitCode = 1;
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const cd = result.dir === "." ? "" : `cd ${result.dir} && `;
|
|
531
|
+
console.log(`
|
|
532
|
+
Next: ${pc6.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
|
|
533
|
+
console.log(pc6.dim("then describe what to build \u2014 /dev starts the preview server"));
|
|
534
|
+
});
|
|
535
|
+
program2.command("tag").description("Add the element picker to this Vite app (Alt+S in the browser \u2192 click \u2192 file:line:col)").action(async () => {
|
|
536
|
+
const fs3 = await import("fs");
|
|
537
|
+
const nodePath = await import("path");
|
|
538
|
+
const { patchViteConfig, TAGGER_FILENAME, TAGGER_SOURCE } = await import("./source-ZZU245VN.js");
|
|
539
|
+
const cwd = process.cwd();
|
|
540
|
+
const taggerPath = nodePath.join(cwd, TAGGER_FILENAME);
|
|
541
|
+
fs3.writeFileSync(taggerPath, TAGGER_SOURCE);
|
|
542
|
+
console.log(pc6.green(`\u2713 ${TAGGER_FILENAME} written`));
|
|
543
|
+
const configPath = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].map((name) => nodePath.join(cwd, name)).find((candidate) => fs3.existsSync(candidate));
|
|
544
|
+
if (!configPath) {
|
|
545
|
+
console.log(pc6.yellow("\u25CB no vite config found \u2014 add the plugin manually:"));
|
|
546
|
+
console.log(pc6.dim(` import squintTagger from './${TAGGER_FILENAME}'
|
|
547
|
+
plugins: [squintTagger(), \u2026]`));
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const source = fs3.readFileSync(configPath, "utf8");
|
|
551
|
+
const patched = patchViteConfig(source);
|
|
552
|
+
if (patched === "already") {
|
|
553
|
+
console.log(pc6.dim("vite config already wired"));
|
|
554
|
+
} else if (patched === null) {
|
|
555
|
+
console.log(pc6.yellow(`\u25CB could not patch ${nodePath.basename(configPath)} automatically \u2014 add:`));
|
|
556
|
+
console.log(pc6.dim(` import squintTagger from './${TAGGER_FILENAME}'
|
|
557
|
+
plugins: [squintTagger(), \u2026]`));
|
|
558
|
+
} else {
|
|
559
|
+
fs3.writeFileSync(configPath, patched);
|
|
560
|
+
console.log(pc6.green(`\u2713 ${nodePath.basename(configPath)} wired`));
|
|
561
|
+
}
|
|
562
|
+
console.log(
|
|
563
|
+
pc6.dim("\nin the running app: Alt+S \u2192 click elements to pin, alt+enter copies all \u2014 paste into squint")
|
|
564
|
+
);
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/cli/tui.tsx
|
|
569
|
+
import { render } from "ink";
|
|
570
|
+
|
|
154
571
|
// src/tui/App.tsx
|
|
155
572
|
import { Box as Box2, Static, Text as Text3, useApp, useInput } from "ink";
|
|
156
|
-
import
|
|
573
|
+
import path4 from "path";
|
|
157
574
|
import { useMemo, useRef, useState as useState2, useSyncExternalStore } from "react";
|
|
158
575
|
|
|
159
576
|
// src/session/engine.ts
|
|
577
|
+
import path3 from "path";
|
|
578
|
+
|
|
579
|
+
// src/vcs/sandbox.ts
|
|
580
|
+
import { execFileSync } from "child_process";
|
|
581
|
+
import fs2 from "fs";
|
|
160
582
|
import path2 from "path";
|
|
583
|
+
function git(cwd, args) {
|
|
584
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
585
|
+
}
|
|
586
|
+
function sandboxDir(cwd) {
|
|
587
|
+
return path2.join(cwd, ".squint", "sandbox");
|
|
588
|
+
}
|
|
589
|
+
function sandboxExists(cwd) {
|
|
590
|
+
return fs2.existsSync(path2.join(sandboxDir(cwd), ".git"));
|
|
591
|
+
}
|
|
592
|
+
function openSandbox(cwd) {
|
|
593
|
+
const dir = sandboxDir(cwd);
|
|
594
|
+
if (sandboxExists(cwd)) return { dir, reused: true };
|
|
595
|
+
fs2.rmSync(dir, { recursive: true, force: true });
|
|
596
|
+
fs2.mkdirSync(path2.dirname(dir), { recursive: true });
|
|
597
|
+
git(cwd, ["worktree", "add", "--force", "--detach", dir, "HEAD"]);
|
|
598
|
+
const mainModules = path2.join(cwd, "node_modules");
|
|
599
|
+
const sandboxModules = path2.join(dir, "node_modules");
|
|
600
|
+
if (fs2.existsSync(mainModules) && !fs2.existsSync(sandboxModules)) {
|
|
601
|
+
fs2.symlinkSync(mainModules, sandboxModules, "dir");
|
|
602
|
+
}
|
|
603
|
+
return { dir, reused: false };
|
|
604
|
+
}
|
|
605
|
+
function sandboxDiffStat(cwd) {
|
|
606
|
+
if (!sandboxExists(cwd)) return null;
|
|
607
|
+
const dir = sandboxDir(cwd);
|
|
608
|
+
try {
|
|
609
|
+
git(dir, ["add", "-A"]);
|
|
610
|
+
const stat = git(dir, ["diff", "--cached", "--shortstat", "HEAD"]);
|
|
611
|
+
return stat.length > 0 ? stat : null;
|
|
612
|
+
} catch {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function sandboxFiles(cwd) {
|
|
617
|
+
if (!sandboxExists(cwd)) return [];
|
|
618
|
+
const dir = sandboxDir(cwd);
|
|
619
|
+
try {
|
|
620
|
+
git(dir, ["add", "-A"]);
|
|
621
|
+
const out = git(dir, ["diff", "--cached", "--name-status", "HEAD"]);
|
|
622
|
+
return out.length > 0 ? out.split("\n") : [];
|
|
623
|
+
} catch {
|
|
624
|
+
return [];
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function applySandbox(cwd) {
|
|
628
|
+
if (!sandboxExists(cwd)) return { ok: false, detail: "no sandbox open" };
|
|
629
|
+
const dir = sandboxDir(cwd);
|
|
630
|
+
try {
|
|
631
|
+
git(dir, ["add", "-A"]);
|
|
632
|
+
const patch = git(dir, ["diff", "--binary", "--cached", "HEAD"]);
|
|
633
|
+
if (patch.length === 0) return { ok: false, detail: "sandbox has no changes" };
|
|
634
|
+
const patchFile = path2.join(cwd, ".squint", "sandbox.patch");
|
|
635
|
+
fs2.writeFileSync(patchFile, patch + "\n");
|
|
636
|
+
git(cwd, ["apply", "--whitespace=nowarn", patchFile]);
|
|
637
|
+
fs2.rmSync(patchFile, { force: true });
|
|
638
|
+
return { ok: true };
|
|
639
|
+
} catch (err) {
|
|
640
|
+
return { ok: false, detail: err instanceof Error ? err.message.split("\n")[0] : String(err) };
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
function discardSandbox(cwd) {
|
|
644
|
+
if (!sandboxExists(cwd)) return false;
|
|
645
|
+
const dir = sandboxDir(cwd);
|
|
646
|
+
try {
|
|
647
|
+
const link = path2.join(dir, "node_modules");
|
|
648
|
+
if (fs2.existsSync(link) && fs2.lstatSync(link).isSymbolicLink()) fs2.rmSync(link);
|
|
649
|
+
} catch {
|
|
650
|
+
}
|
|
651
|
+
try {
|
|
652
|
+
git(cwd, ["worktree", "remove", "--force", dir]);
|
|
653
|
+
} catch {
|
|
654
|
+
fs2.rmSync(dir, { recursive: true, force: true });
|
|
655
|
+
}
|
|
656
|
+
try {
|
|
657
|
+
git(cwd, ["worktree", "prune"]);
|
|
658
|
+
} catch {
|
|
659
|
+
}
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// src/session/engine.ts
|
|
161
664
|
var MAX_AUTO_FIX_ATTEMPTS = 2;
|
|
162
665
|
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
163
666
|
var Session = class {
|
|
@@ -175,7 +678,8 @@ var Session = class {
|
|
|
175
678
|
totals: { costUsd: 0, turns: 0 },
|
|
176
679
|
queue: [],
|
|
177
680
|
mode: "safe",
|
|
178
|
-
problems: []
|
|
681
|
+
problems: [],
|
|
682
|
+
sandbox: false
|
|
179
683
|
};
|
|
180
684
|
if (opts.autoDev && detectDevCommand(opts.cwd)) {
|
|
181
685
|
this.devServer().start();
|
|
@@ -209,6 +713,7 @@ var Session = class {
|
|
|
209
713
|
fixAttempts = 0;
|
|
210
714
|
reviewTipShown = false;
|
|
211
715
|
lastPulse = null;
|
|
716
|
+
autoReviewedThisAsk = false;
|
|
212
717
|
startedAt = Date.now();
|
|
213
718
|
subscribe(listener) {
|
|
214
719
|
this.listeners.add(listener);
|
|
@@ -344,15 +849,33 @@ var Session = class {
|
|
|
344
849
|
this.push("assistant", text);
|
|
345
850
|
}
|
|
346
851
|
}
|
|
852
|
+
/** Where engines run and servers start: the sandbox worktree when on. */
|
|
853
|
+
execCwd() {
|
|
854
|
+
return this.state.sandbox ? sandboxDir(this.opts.cwd) : this.opts.cwd;
|
|
855
|
+
}
|
|
347
856
|
devServer() {
|
|
348
857
|
if (!this.dev) {
|
|
349
|
-
this.dev = new DevServer(this.
|
|
858
|
+
this.dev = new DevServer(this.execCwd(), {
|
|
350
859
|
onStateChange: (devState) => this.notify({ devState }),
|
|
351
860
|
onUrl: (url) => this.notify({ devUrl: url })
|
|
352
861
|
});
|
|
353
862
|
}
|
|
354
863
|
return this.dev;
|
|
355
864
|
}
|
|
865
|
+
/** Rebind the dev server after the execution tree changes. */
|
|
866
|
+
resetDevServer() {
|
|
867
|
+
const wasRunning = this.dev && this.dev.state !== "stopped";
|
|
868
|
+
this.dev?.stop();
|
|
869
|
+
this.dev = null;
|
|
870
|
+
this.notify({ devUrl: null, devState: "stopped" });
|
|
871
|
+
if (wasRunning) {
|
|
872
|
+
const command = detectDevCommand(this.execCwd());
|
|
873
|
+
if (command) {
|
|
874
|
+
this.devServer().start(command);
|
|
875
|
+
this.push("status", `dev server restarting in ${this.state.sandbox ? "the sandbox" : "the real tree"}`);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
356
879
|
turnEdits = 0;
|
|
357
880
|
turnTools = 0;
|
|
358
881
|
toolStreak = 0;
|
|
@@ -431,7 +954,7 @@ var Session = class {
|
|
|
431
954
|
engine,
|
|
432
955
|
{
|
|
433
956
|
prompt,
|
|
434
|
-
cwd: this.
|
|
957
|
+
cwd: this.execCwd(),
|
|
435
958
|
model: this.state.model,
|
|
436
959
|
mode: this.state.mode,
|
|
437
960
|
sessionId: engine.supportsResume ? this.sessionId : void 0
|
|
@@ -474,9 +997,9 @@ var Session = class {
|
|
|
474
997
|
}
|
|
475
998
|
}
|
|
476
999
|
if (result.ok && this.opts.autoCheck !== false) {
|
|
477
|
-
const fastGates = detectFastGates(this.
|
|
1000
|
+
const fastGates = detectFastGates(this.execCwd());
|
|
478
1001
|
if (fastGates.length > 0) {
|
|
479
|
-
const gateResults = await runGates(this.
|
|
1002
|
+
const gateResults = await runGates(this.execCwd(), fastGates);
|
|
480
1003
|
const failures = gateResults.filter((r) => !r.ok);
|
|
481
1004
|
if (failures.length > 0) {
|
|
482
1005
|
this.addProblem(
|
|
@@ -523,7 +1046,20 @@ ${errors.slice(-5).join("\n")}`);
|
|
|
523
1046
|
this.push("status", "/fix sends open problems to the engine \xB7 /problems lists them");
|
|
524
1047
|
} else if (probe) {
|
|
525
1048
|
this.clearProblems("runtime");
|
|
526
|
-
await this.visualPulse(probe.pulsePath);
|
|
1049
|
+
const pct = await this.visualPulse(probe.pulsePath);
|
|
1050
|
+
if (this.opts.autoReview && pct !== null && pct >= 10 && !this.autoReviewedThisAsk) {
|
|
1051
|
+
this.autoReviewedThisAsk = true;
|
|
1052
|
+
this.push("status", `auto-review: the page changed ${pct.toFixed(0)}% \u2014 capturing for self-critique`);
|
|
1053
|
+
this.notify({ running: false });
|
|
1054
|
+
const captureResult = await this.capture();
|
|
1055
|
+
if (captureResult) {
|
|
1056
|
+
await this.runTurn(
|
|
1057
|
+
buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y),
|
|
1058
|
+
"\u{1F441} auto-review rendered UI"
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
527
1063
|
}
|
|
528
1064
|
}
|
|
529
1065
|
if (!this.reviewTipShown && this.state.devUrl) {
|
|
@@ -537,7 +1073,8 @@ ${errors.slice(-5).join("\n")}`);
|
|
|
537
1073
|
}
|
|
538
1074
|
async submit(ask) {
|
|
539
1075
|
this.fixAttempts = 0;
|
|
540
|
-
|
|
1076
|
+
this.autoReviewedThisAsk = false;
|
|
1077
|
+
const snapshot = this.state.sandbox ? null : takeSnapshot(this.opts.cwd);
|
|
541
1078
|
if (snapshot) {
|
|
542
1079
|
this.checkpoints.push({
|
|
543
1080
|
snapshot,
|
|
@@ -580,25 +1117,26 @@ ${errors.slice(-5).join("\n")}`);
|
|
|
580
1117
|
* Informational — changes are usually intended; surprises shouldn't be.
|
|
581
1118
|
*/
|
|
582
1119
|
async visualPulse(pulsePath) {
|
|
583
|
-
if (!pulsePath) return;
|
|
1120
|
+
if (!pulsePath) return null;
|
|
584
1121
|
let current;
|
|
585
1122
|
try {
|
|
586
1123
|
current = (await import("fs")).readFileSync(pulsePath);
|
|
587
1124
|
} catch {
|
|
588
|
-
return;
|
|
1125
|
+
return null;
|
|
589
1126
|
}
|
|
590
1127
|
const previous = this.lastPulse;
|
|
591
1128
|
this.lastPulse = current;
|
|
592
1129
|
if (!previous) {
|
|
593
1130
|
this.push("status", "visual pulse: baseline captured");
|
|
594
|
-
return;
|
|
1131
|
+
return null;
|
|
595
1132
|
}
|
|
596
1133
|
const pct = await comparePulse(previous, current);
|
|
597
|
-
if (pct === null) return;
|
|
1134
|
+
if (pct === null) return null;
|
|
598
1135
|
this.push(
|
|
599
1136
|
"status",
|
|
600
1137
|
pct < 0.5 ? "visual pulse: stable vs last turn" : `visual pulse: ${pct.toFixed(1)}% of the page changed vs last turn`
|
|
601
1138
|
);
|
|
1139
|
+
return pct;
|
|
602
1140
|
}
|
|
603
1141
|
/** Screenshot the running app (and watch its runtime where CDP is available). */
|
|
604
1142
|
async capture() {
|
|
@@ -616,7 +1154,7 @@ ${errors.slice(-5).join("\n")}`);
|
|
|
616
1154
|
if (result.shots.length > 0) {
|
|
617
1155
|
this.push(
|
|
618
1156
|
"status",
|
|
619
|
-
`captured ${result.shots.map((s) => s.name).join(", ")} \u2192 ${
|
|
1157
|
+
`captured ${result.shots.map((s) => s.name).join(", ")} \u2192 ${path3.dirname(result.shots[0].path)}`
|
|
620
1158
|
);
|
|
621
1159
|
}
|
|
622
1160
|
if (result.runtime) {
|
|
@@ -691,6 +1229,23 @@ They are objective defects, not style preferences.`
|
|
|
691
1229
|
break;
|
|
692
1230
|
case "dev": {
|
|
693
1231
|
const dev = this.devServer();
|
|
1232
|
+
if (arg === "logs") {
|
|
1233
|
+
const tail = dev.tail(15);
|
|
1234
|
+
this.push("status", tail.length > 0 ? tail.join("\n") : "no dev server output captured yet");
|
|
1235
|
+
break;
|
|
1236
|
+
}
|
|
1237
|
+
if (arg === "restart") {
|
|
1238
|
+
if (dev.state !== "stopped") dev.stop();
|
|
1239
|
+
this.notify({ devUrl: null });
|
|
1240
|
+
const devCommand = detectDevCommand(this.opts.cwd);
|
|
1241
|
+
if (!devCommand) {
|
|
1242
|
+
this.push("error", "no dev/start script found in package.json");
|
|
1243
|
+
} else {
|
|
1244
|
+
dev.start(devCommand);
|
|
1245
|
+
this.push("status", `dev server restarting \xB7 ${devCommand.display}`);
|
|
1246
|
+
}
|
|
1247
|
+
break;
|
|
1248
|
+
}
|
|
694
1249
|
if (dev.state === "stopped" || dev.state === "crashed") {
|
|
695
1250
|
const devCommand = detectDevCommand(this.opts.cwd);
|
|
696
1251
|
if (!devCommand) {
|
|
@@ -726,12 +1281,12 @@ They are objective defects, not style preferences.`
|
|
|
726
1281
|
}
|
|
727
1282
|
case "save": {
|
|
728
1283
|
void (async () => {
|
|
729
|
-
const
|
|
730
|
-
const
|
|
731
|
-
const dir =
|
|
732
|
-
|
|
1284
|
+
const fs3 = await import("fs");
|
|
1285
|
+
const path5 = await import("path");
|
|
1286
|
+
const dir = path5.join(this.opts.cwd, ".squint", "transcripts");
|
|
1287
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
733
1288
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
734
|
-
const file =
|
|
1289
|
+
const file = path5.join(dir, `${stamp}.md`);
|
|
735
1290
|
const lines = [`# squint session \u2014 ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}`, ""];
|
|
736
1291
|
for (const item of this.state.items) {
|
|
737
1292
|
switch (item.role) {
|
|
@@ -751,8 +1306,8 @@ They are objective defects, not style preferences.`
|
|
|
751
1306
|
}
|
|
752
1307
|
}
|
|
753
1308
|
lines.push("", `> ${this.summary()}`);
|
|
754
|
-
|
|
755
|
-
this.push("status", `saved transcript \u2192 ${
|
|
1309
|
+
fs3.writeFileSync(file, lines.join("\n") + "\n");
|
|
1310
|
+
this.push("status", `saved transcript \u2192 ${path5.relative(this.opts.cwd, file)}`);
|
|
756
1311
|
})();
|
|
757
1312
|
break;
|
|
758
1313
|
}
|
|
@@ -870,6 +1425,57 @@ They are objective defects, not style preferences.`
|
|
|
870
1425
|
this.push("status", `resumed ${saved.engine} session${saved.lastAsk ? ` \xB7 "${saved.lastAsk}"` : ""}`);
|
|
871
1426
|
break;
|
|
872
1427
|
}
|
|
1428
|
+
case "sandbox": {
|
|
1429
|
+
if (this.state.running) {
|
|
1430
|
+
this.push("status", "wait for the current turn before changing sandbox state");
|
|
1431
|
+
break;
|
|
1432
|
+
}
|
|
1433
|
+
if (arg === "diff") {
|
|
1434
|
+
const stat = sandboxDiffStat(this.opts.cwd);
|
|
1435
|
+
if (!stat) {
|
|
1436
|
+
this.push("status", sandboxExists(this.opts.cwd) ? "sandbox is clean" : "no sandbox open \u2014 /sandbox on");
|
|
1437
|
+
} else {
|
|
1438
|
+
this.push("status", `${stat}
|
|
1439
|
+
${sandboxFiles(this.opts.cwd).join("\n")}`);
|
|
1440
|
+
}
|
|
1441
|
+
break;
|
|
1442
|
+
}
|
|
1443
|
+
if (arg === "apply") {
|
|
1444
|
+
const applied = applySandbox(this.opts.cwd);
|
|
1445
|
+
if (applied.ok) {
|
|
1446
|
+
discardSandbox(this.opts.cwd);
|
|
1447
|
+
this.notify({ sandbox: false });
|
|
1448
|
+
this.resetDevServer();
|
|
1449
|
+
this.push("status", "sandbox applied to the real tree \u2014 review with git diff");
|
|
1450
|
+
} else {
|
|
1451
|
+
this.push("error", applied.detail ?? "apply failed");
|
|
1452
|
+
}
|
|
1453
|
+
break;
|
|
1454
|
+
}
|
|
1455
|
+
if (arg === "discard" || arg === "off") {
|
|
1456
|
+
const had = discardSandbox(this.opts.cwd);
|
|
1457
|
+
this.notify({ sandbox: false });
|
|
1458
|
+
this.resetDevServer();
|
|
1459
|
+
this.push("status", had ? "sandbox discarded \u2014 the real tree was never touched" : "no sandbox open");
|
|
1460
|
+
break;
|
|
1461
|
+
}
|
|
1462
|
+
if (arg === "on" || arg === "") {
|
|
1463
|
+
if (!isGitRepo(this.opts.cwd)) {
|
|
1464
|
+
this.push("error", "sandbox needs a git repo with at least one commit");
|
|
1465
|
+
break;
|
|
1466
|
+
}
|
|
1467
|
+
const { reused } = openSandbox(this.opts.cwd);
|
|
1468
|
+
this.notify({ sandbox: true });
|
|
1469
|
+
this.resetDevServer();
|
|
1470
|
+
this.push(
|
|
1471
|
+
"status",
|
|
1472
|
+
`${reused ? "rejoined the open sandbox" : "sandbox opened"} \u2014 asks now accumulate in a shadow worktree; /sandbox diff \xB7 apply \xB7 discard`
|
|
1473
|
+
);
|
|
1474
|
+
break;
|
|
1475
|
+
}
|
|
1476
|
+
this.push("status", "usage: /sandbox [on] \xB7 diff \xB7 apply \xB7 discard");
|
|
1477
|
+
break;
|
|
1478
|
+
}
|
|
873
1479
|
case "variants": {
|
|
874
1480
|
const [sub, ...subRest] = arg.split(/\s+/);
|
|
875
1481
|
if (sub === "apply") {
|
|
@@ -945,7 +1551,7 @@ They are objective defects, not style preferences.`
|
|
|
945
1551
|
this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
|
|
946
1552
|
break;
|
|
947
1553
|
case "help": {
|
|
948
|
-
void import("./commands-
|
|
1554
|
+
void import("./commands-N33DKZKC.js").then(({ commandHelp }) => this.push("status", commandHelp()));
|
|
949
1555
|
break;
|
|
950
1556
|
}
|
|
951
1557
|
case "quit":
|
|
@@ -1287,6 +1893,7 @@ function App({
|
|
|
1287
1893
|
autoFix,
|
|
1288
1894
|
autoProbe,
|
|
1289
1895
|
autoCheck,
|
|
1896
|
+
autoReview,
|
|
1290
1897
|
bell,
|
|
1291
1898
|
budgetUsd,
|
|
1292
1899
|
initialTheme
|
|
@@ -1304,6 +1911,7 @@ function App({
|
|
|
1304
1911
|
autoFix,
|
|
1305
1912
|
autoProbe,
|
|
1306
1913
|
autoCheck,
|
|
1914
|
+
autoReview,
|
|
1307
1915
|
budgetUsd,
|
|
1308
1916
|
// Delay lets the goodbye summary land in the Static scrollback.
|
|
1309
1917
|
onQuit: () => setTimeout(() => exit(), 60)
|
|
@@ -1473,11 +2081,12 @@ function App({
|
|
|
1473
2081
|
]
|
|
1474
2082
|
}
|
|
1475
2083
|
),
|
|
2084
|
+
state.sandbox && /* @__PURE__ */ jsx3(Text3, { color: theme2.accent, bold: true, children: " [sandbox]" }),
|
|
1476
2085
|
" ",
|
|
1477
2086
|
state.engineId,
|
|
1478
2087
|
state.model ? ` \xB7 ${state.model}` : "",
|
|
1479
2088
|
" \xB7 ",
|
|
1480
|
-
|
|
2089
|
+
path4.basename(cwd),
|
|
1481
2090
|
devBadge,
|
|
1482
2091
|
totalsBadge,
|
|
1483
2092
|
state.problems.length > 0 && /* @__PURE__ */ jsxs3(Text3, { color: theme2.error, children: [
|
|
@@ -1492,409 +2101,49 @@ function App({
|
|
|
1492
2101
|
] }) });
|
|
1493
2102
|
}
|
|
1494
2103
|
|
|
1495
|
-
// src/cli.tsx
|
|
2104
|
+
// src/cli/tui.tsx
|
|
1496
2105
|
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
|
|
1500
|
-
program.command("run").description("Run one prompt headlessly and stream the result").argument("<prompt...>", "what to build or change").option("-e, --engine <id>", "engine to use (see squint engines)").option("-m, --model <name>", "model override for the engine").option("--mode <mode>", "plan (read-only) \xB7 safe (default) \xB7 yolo (no friction)").option("--no-brief", "send the prompt without the squint design brief").option("--json", "emit normalized agent events as ndjson").action(
|
|
1501
|
-
async (promptWords, options) => {
|
|
2106
|
+
function registerTui(program2) {
|
|
2107
|
+
program2.action(async () => {
|
|
1502
2108
|
const cwd = process.cwd();
|
|
1503
|
-
if (options.mode && !["plan", "safe", "yolo"].includes(options.mode)) {
|
|
1504
|
-
console.error(pc.red("\u2717 --mode must be plan, safe, or yolo"));
|
|
1505
|
-
process.exitCode = 1;
|
|
1506
|
-
return;
|
|
1507
|
-
}
|
|
1508
|
-
const mode = options.mode;
|
|
1509
2109
|
const config = loadConfig(defaultPaths(cwd));
|
|
1510
|
-
const engineId = resolveEngineId(config
|
|
1511
|
-
const
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
if (event.type !== "delta") console.log(JSON.stringify(event));
|
|
1517
|
-
} : createPrinter();
|
|
1518
|
-
if (!options.json)
|
|
1519
|
-
console.log(pc.dim(`squint \xB7 ${engine.id}${model ? ` \xB7 ${model}` : ""}${mode && mode !== "safe" ? ` \xB7 ${mode}` : ""}`));
|
|
1520
|
-
const result = await runAgent(engine, { prompt, cwd, model, mode }, onEvent);
|
|
1521
|
-
if (options.json) {
|
|
1522
|
-
console.log(JSON.stringify({ type: "summary", ...result }));
|
|
1523
|
-
} else if (result.ok) {
|
|
1524
|
-
const cost = result.costUsd !== void 0 ? ` \xB7 $${result.costUsd.toFixed(2)}` : "";
|
|
1525
|
-
const secs = result.durationMs !== void 0 ? ` \xB7 ${(result.durationMs / 1e3).toFixed(0)}s` : "";
|
|
1526
|
-
console.log(pc.green(`\u2713 done${secs}${cost}`));
|
|
1527
|
-
}
|
|
1528
|
-
if (!result.ok) process.exitCode = 1;
|
|
1529
|
-
}
|
|
1530
|
-
);
|
|
1531
|
-
program.command("engines").description("List engines: installed, streaming, session resume").action(() => {
|
|
1532
|
-
console.log(pc.dim(" id name stream resume where"));
|
|
1533
|
-
for (const { engine, path: binaryPath } of detectEngines()) {
|
|
1534
|
-
const status = binaryPath ? pc.green("\u2713") : pc.red("\u2717");
|
|
1535
|
-
const stream = engine.createParser ? pc.green("yes") : pc.dim("text");
|
|
1536
|
-
const resume = engine.supportsResume ? pc.green("yes") : pc.dim("no");
|
|
1537
|
-
const location = binaryPath ?? pc.dim(`not found \u2014 ${engine.install}`);
|
|
1538
|
-
console.log(
|
|
1539
|
-
`${status} ${engine.id.padEnd(10)} ${engine.name.padEnd(14)} ${stream.padEnd(15)} ${resume.padEnd(14)} ${location}`
|
|
1540
|
-
);
|
|
1541
|
-
}
|
|
1542
|
-
console.log(pc.dim("\nplan/safe/yolo modes map onto every engine \xB7 squint doctor --probe verifies auth"));
|
|
1543
|
-
});
|
|
1544
|
-
program.command("doctor").description("Check squint prerequisites and engine availability").option("--probe", "run each detected engine with a one-word prompt to verify auth works").action(async (options) => {
|
|
1545
|
-
const [major] = process.versions.node.split(".");
|
|
1546
|
-
const nodeOk = Number(major) >= 22;
|
|
1547
|
-
console.log(`${nodeOk ? pc.green("\u2713") : pc.red("\u2717")} node ${process.versions.node}${nodeOk ? "" : " (need >= 22)"}`);
|
|
1548
|
-
const detected = detectEngines();
|
|
1549
|
-
for (const { engine, path: binaryPath } of detected) {
|
|
1550
|
-
const status = binaryPath ? pc.green("\u2713") : pc.yellow("\u25CB");
|
|
1551
|
-
console.log(`${status} ${engine.name}${binaryPath ? "" : pc.dim(` \u2014 install: ${engine.install}`)}`);
|
|
1552
|
-
}
|
|
1553
|
-
if (options.probe) {
|
|
1554
|
-
const { runAgent: runAgent2 } = await import("./run-S772BPMZ.js");
|
|
1555
|
-
console.log(pc.dim("\nprobing engines with a one-word prompt (verifies auth end to end)\u2026"));
|
|
1556
|
-
for (const { engine, path: binaryPath } of detected) {
|
|
1557
|
-
if (!binaryPath) continue;
|
|
1558
|
-
const startedAt = Date.now();
|
|
1559
|
-
const abort = new AbortController();
|
|
1560
|
-
const timer = setTimeout(() => abort.abort(), 9e4);
|
|
1561
|
-
const result = await runAgent2(
|
|
1562
|
-
engine,
|
|
1563
|
-
{ prompt: "Reply with exactly: ok", cwd: process.cwd() },
|
|
1564
|
-
() => {
|
|
1565
|
-
},
|
|
1566
|
-
abort.signal
|
|
1567
|
-
);
|
|
1568
|
-
clearTimeout(timer);
|
|
1569
|
-
const secs = ((Date.now() - startedAt) / 1e3).toFixed(1);
|
|
1570
|
-
const detail = (result.error ?? "failed").split("\n").filter((l) => l.trim()).at(-1) ?? "failed";
|
|
1571
|
-
console.log(
|
|
1572
|
-
result.ok ? `${pc.green("\u2713")} ${engine.id.padEnd(10)} responded in ${secs}s` : `${pc.red("\u2717")} ${engine.id.padEnd(10)} ${pc.dim(detail.slice(0, 110))}`
|
|
1573
|
-
);
|
|
2110
|
+
const engineId = resolveEngineId(config);
|
|
2111
|
+
const model = resolveModel(config, engineId);
|
|
2112
|
+
let theme2 = config.theme;
|
|
2113
|
+
if (!theme2 && !process.env.NO_COLOR) {
|
|
2114
|
+
const { detectBackground } = await import("./background-J7OQTYEC.js");
|
|
2115
|
+
if (await detectBackground() === "light") theme2 = "light";
|
|
1574
2116
|
}
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
}
|
|
1593
|
-
});
|
|
1594
|
-
program.command("init").description("Scaffold a new Vite + React + TS + Tailwind app with token-first CSS").argument("[dir]", "target directory", ".").option("--force", "write into a non-empty directory").option("--no-install", "skip npm install").action(async (dir, options) => {
|
|
1595
|
-
const { installDependencies, writeTemplate } = await import("./init-LSYB32NS.js");
|
|
1596
|
-
let result;
|
|
1597
|
-
try {
|
|
1598
|
-
result = writeTemplate(dir, { force: options.force });
|
|
1599
|
-
} catch (err) {
|
|
1600
|
-
console.error(pc.red(`\u2717 ${err instanceof Error ? err.message : String(err)}`));
|
|
1601
|
-
process.exitCode = 1;
|
|
1602
|
-
return;
|
|
1603
|
-
}
|
|
1604
|
-
console.log(pc.green(`\u2713 scaffolded ${result.files.length} files in ${result.dir}`));
|
|
1605
|
-
if (options.install) {
|
|
1606
|
-
console.log(pc.dim("installing dependencies\u2026"));
|
|
1607
|
-
const ok = await installDependencies(result.dir);
|
|
1608
|
-
if (!ok) {
|
|
1609
|
-
console.error(pc.red("\u2717 npm install failed \u2014 run it manually"));
|
|
1610
|
-
process.exitCode = 1;
|
|
1611
|
-
return;
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
const cd = result.dir === "." ? "" : `cd ${result.dir} && `;
|
|
1615
|
-
console.log(`
|
|
1616
|
-
Next: ${pc.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
|
|
1617
|
-
console.log(pc.dim("then describe what to build \u2014 /dev starts the preview server"));
|
|
1618
|
-
});
|
|
1619
|
-
var skillsCommand = program.command("skills").description("Project knowledge injected into asks (.squint/rules.md + .squint/skills/)");
|
|
1620
|
-
skillsCommand.command("list").description("Show always-on rules and trigger-matched skills").action(async () => {
|
|
1621
|
-
const { loadRules, loadSkills } = await import("./skills-DNBHZAAU.js");
|
|
1622
|
-
const cwd = process.cwd();
|
|
1623
|
-
const rules = loadRules(cwd);
|
|
1624
|
-
console.log(rules ? `${pc.green("\u2713")} rules.md ${pc.dim(`(${rules.split("\n").length} lines, always on)`)}` : pc.dim("\u25CB no .squint/rules.md"));
|
|
1625
|
-
const skills = loadSkills(cwd);
|
|
1626
|
-
if (skills.length === 0) {
|
|
1627
|
-
console.log(pc.dim("\u25CB no skills \u2014 squint skills init writes an example"));
|
|
1628
|
-
return;
|
|
1629
|
-
}
|
|
1630
|
-
for (const skill of skills) {
|
|
1631
|
-
console.log(`${pc.green("\u2713")} ${skill.name.padEnd(20)} ${pc.dim(`triggers: ${skill.triggers.join(", ")}`)}`);
|
|
1632
|
-
}
|
|
1633
|
-
});
|
|
1634
|
-
skillsCommand.command("init").description("Scaffold .squint/rules.md and an example skill").action(async () => {
|
|
1635
|
-
const fs2 = await import("fs");
|
|
1636
|
-
const nodePath = await import("path");
|
|
1637
|
-
const cwd = process.cwd();
|
|
1638
|
-
const skillsDir = nodePath.join(cwd, ".squint", "skills");
|
|
1639
|
-
fs2.mkdirSync(skillsDir, { recursive: true });
|
|
1640
|
-
const rules = nodePath.join(cwd, ".squint", "rules.md");
|
|
1641
|
-
if (!fs2.existsSync(rules)) {
|
|
1642
|
-
fs2.writeFileSync(
|
|
1643
|
-
rules,
|
|
1644
|
-
"# Project rules\n\nThese ride along on every squint ask. Keep them short \u2014 cut anything that would not cause a mistake if removed.\n"
|
|
1645
|
-
);
|
|
1646
|
-
console.log(pc.green("\u2713 .squint/rules.md"));
|
|
1647
|
-
}
|
|
1648
|
-
const example = nodePath.join(skillsDir, "example.md");
|
|
1649
|
-
if (!fs2.existsSync(example)) {
|
|
1650
|
-
fs2.writeFileSync(
|
|
1651
|
-
example,
|
|
1652
|
-
"---\ntriggers: example, sample\n---\n\nThis note is injected only when an ask mentions one of the triggers above.\nDocument the parts of this repo an agent would otherwise rediscover every time:\nwhere state lives, which helpers to reuse, what not to touch.\n"
|
|
1653
|
-
);
|
|
1654
|
-
console.log(pc.green("\u2713 .squint/skills/example.md"));
|
|
1655
|
-
}
|
|
1656
|
-
console.log(pc.dim("rules are always-on; skills inject when an ask mentions a trigger"));
|
|
1657
|
-
});
|
|
1658
|
-
program.command("tag").description("Add the element picker to this Vite app (Alt+S in the browser \u2192 click \u2192 file:line:col)").action(async () => {
|
|
1659
|
-
const fs2 = await import("fs");
|
|
1660
|
-
const nodePath = await import("path");
|
|
1661
|
-
const { patchViteConfig, TAGGER_FILENAME, TAGGER_SOURCE } = await import("./source-ZZU245VN.js");
|
|
1662
|
-
const cwd = process.cwd();
|
|
1663
|
-
const taggerPath = nodePath.join(cwd, TAGGER_FILENAME);
|
|
1664
|
-
fs2.writeFileSync(taggerPath, TAGGER_SOURCE);
|
|
1665
|
-
console.log(pc.green(`\u2713 ${TAGGER_FILENAME} written`));
|
|
1666
|
-
const configPath = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].map((name) => nodePath.join(cwd, name)).find((candidate) => fs2.existsSync(candidate));
|
|
1667
|
-
if (!configPath) {
|
|
1668
|
-
console.log(pc.yellow("\u25CB no vite config found \u2014 add the plugin manually:"));
|
|
1669
|
-
console.log(pc.dim(` import squintTagger from './${TAGGER_FILENAME}'
|
|
1670
|
-
plugins: [squintTagger(), \u2026]`));
|
|
1671
|
-
return;
|
|
1672
|
-
}
|
|
1673
|
-
const source = fs2.readFileSync(configPath, "utf8");
|
|
1674
|
-
const patched = patchViteConfig(source);
|
|
1675
|
-
if (patched === "already") {
|
|
1676
|
-
console.log(pc.dim("vite config already wired"));
|
|
1677
|
-
} else if (patched === null) {
|
|
1678
|
-
console.log(pc.yellow(`\u25CB could not patch ${nodePath.basename(configPath)} automatically \u2014 add:`));
|
|
1679
|
-
console.log(pc.dim(` import squintTagger from './${TAGGER_FILENAME}'
|
|
1680
|
-
plugins: [squintTagger(), \u2026]`));
|
|
1681
|
-
} else {
|
|
1682
|
-
fs2.writeFileSync(configPath, patched);
|
|
1683
|
-
console.log(pc.green(`\u2713 ${nodePath.basename(configPath)} wired`));
|
|
1684
|
-
}
|
|
1685
|
-
console.log(pc.dim("\nin the running app: Alt+S \u2192 click an element \u2192 paste the copied file:line:col into squint"));
|
|
1686
|
-
});
|
|
1687
|
-
var variantsCommand = program.command("variants").description("Parallel design explorations \u2014 one aesthetic family each, pick with your eyes");
|
|
1688
|
-
variantsCommand.command("gen").description("Generate n variants of one ask in parallel (n engine runs \u2014 n\xD7 cost)").argument("<n>", "how many variants (max 4)").argument("<prompt...>", "what to build").option("-e, --engine <id>", "engine to use").option("-m, --model <name>", "model override").option("--no-shots", "skip the screenshot pass").action(
|
|
1689
|
-
async (nRaw, promptWords, options) => {
|
|
1690
|
-
const cwd = process.cwd();
|
|
1691
|
-
const n = Number.parseInt(nRaw, 10);
|
|
1692
|
-
if (!Number.isInteger(n) || n < 2 || n > 4) {
|
|
1693
|
-
console.error(pc.red("\u2717 n must be 2\u20134"));
|
|
1694
|
-
process.exitCode = 1;
|
|
1695
|
-
return;
|
|
1696
|
-
}
|
|
1697
|
-
const { isGitRepo: isGitRepo2 } = await import("./snapshot-H6VYFHLN.js");
|
|
1698
|
-
if (!isGitRepo2(cwd)) {
|
|
1699
|
-
console.error(pc.red("\u2717 variants need a git repo with at least one commit"));
|
|
1700
|
-
process.exitCode = 1;
|
|
1701
|
-
return;
|
|
1702
|
-
}
|
|
1703
|
-
const { runVariants: runVariants2, cleanVariants: cleanVariants2 } = await import("./variants-2IM274AL.js");
|
|
1704
|
-
const config = loadConfig(defaultPaths(cwd));
|
|
1705
|
-
const engineId = resolveEngineId(config, options.engine);
|
|
1706
|
-
const engine = getEngine(engineId);
|
|
1707
|
-
const model = resolveModel(config, engineId, options.model);
|
|
1708
|
-
const ask = promptWords.join(" ");
|
|
1709
|
-
cleanVariants2(cwd);
|
|
1710
|
-
console.log(pc.dim(`generating ${n} directions in parallel via ${engine.id} \u2014 this runs ${n} engine sessions`));
|
|
1711
|
-
const runs = await runVariants2(
|
|
1712
|
-
cwd,
|
|
1713
|
-
ask,
|
|
1714
|
-
n,
|
|
1715
|
-
engine,
|
|
1716
|
-
model,
|
|
1717
|
-
(familyId, text) => console.log(`${pc.cyan(familyId.padEnd(18))} ${pc.dim(text)}`)
|
|
1718
|
-
);
|
|
1719
|
-
const succeeded = runs.filter((r) => r.result.ok);
|
|
1720
|
-
if (options.shots && succeeded.length > 0) {
|
|
1721
|
-
const { screenshotVariants: screenshotVariants2 } = await import("./shots-J7JG2FC4.js");
|
|
1722
|
-
console.log(pc.dim("capturing screenshots\u2026"));
|
|
1723
|
-
const shots = await screenshotVariants2(cwd, succeeded.map((r) => r.variant));
|
|
1724
|
-
for (const shot of shots) {
|
|
1725
|
-
console.log(`${pc.green("\u2713")} ${shot.familyId.padEnd(18)} ${shot.path ?? pc.dim(shot.error ?? "")}`);
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
console.log(
|
|
1729
|
-
`
|
|
1730
|
-
${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 ` + pc.bold("squint variants apply <id>") + pc.dim(" applies the winner, squint variants clean discards all")
|
|
2117
|
+
render(
|
|
2118
|
+
/* @__PURE__ */ jsx4(
|
|
2119
|
+
App,
|
|
2120
|
+
{
|
|
2121
|
+
cwd,
|
|
2122
|
+
initialEngine: engineId,
|
|
2123
|
+
initialModel: model,
|
|
2124
|
+
autoDev: config.autoDev,
|
|
2125
|
+
autoFix: config.autoFix,
|
|
2126
|
+
autoProbe: config.autoProbe,
|
|
2127
|
+
autoCheck: config.autoCheck,
|
|
2128
|
+
autoReview: config.autoReview,
|
|
2129
|
+
bell: config.bell,
|
|
2130
|
+
budgetUsd: config.budgetUsd,
|
|
2131
|
+
initialTheme: theme2
|
|
2132
|
+
}
|
|
2133
|
+
)
|
|
1731
2134
|
);
|
|
1732
|
-
if (succeeded.length === 0) process.exitCode = 1;
|
|
1733
|
-
}
|
|
1734
|
-
);
|
|
1735
|
-
variantsCommand.command("list").description("List generated variants").action(async () => {
|
|
1736
|
-
const { listVariants: listVariants2 } = await import("./variants-2IM274AL.js");
|
|
1737
|
-
const ids = listVariants2(process.cwd());
|
|
1738
|
-
if (ids.length === 0) {
|
|
1739
|
-
console.log(pc.dim('no variants \u2014 squint variants gen <n> "<ask>"'));
|
|
1740
|
-
return;
|
|
1741
|
-
}
|
|
1742
|
-
for (const id of ids) console.log(id);
|
|
1743
|
-
});
|
|
1744
|
-
variantsCommand.command("apply").description("Apply one variant\u2019s changes to the main tree and discard the rest").argument("<id>", "family id of the winning variant").action(async (id) => {
|
|
1745
|
-
const { applyVariant: applyVariant2, cleanVariants: cleanVariants2 } = await import("./variants-2IM274AL.js");
|
|
1746
|
-
const cwd = process.cwd();
|
|
1747
|
-
const result = applyVariant2(cwd, id);
|
|
1748
|
-
if (!result.ok) {
|
|
1749
|
-
console.error(pc.red(`\u2717 ${result.detail}`));
|
|
1750
|
-
process.exitCode = 1;
|
|
1751
|
-
return;
|
|
1752
|
-
}
|
|
1753
|
-
cleanVariants2(cwd);
|
|
1754
|
-
console.log(pc.green(`\u2713 applied ${id} to the working tree`) + pc.dim(" \u2014 review with git diff"));
|
|
1755
|
-
});
|
|
1756
|
-
variantsCommand.command("clean").description("Discard all variants").action(async () => {
|
|
1757
|
-
const { cleanVariants: cleanVariants2 } = await import("./variants-2IM274AL.js");
|
|
1758
|
-
const count = cleanVariants2(process.cwd());
|
|
1759
|
-
console.log(pc.dim(`removed ${count} variant(s)`));
|
|
1760
|
-
});
|
|
1761
|
-
program.command("brief").description("Set a committed design direction for this project (.squint/brief.md)").argument("[family]", "aesthetic family id (omit to list)").option("--force", "overwrite an existing project brief").action(async (familyId, options) => {
|
|
1762
|
-
const fs2 = await import("fs");
|
|
1763
|
-
const nodePath = await import("path");
|
|
1764
|
-
const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-2G5SLLY7.js");
|
|
1765
|
-
if (!familyId) {
|
|
1766
|
-
console.log(pc.bold("Aesthetic families") + pc.dim(" \u2014 squint brief <id>\n"));
|
|
1767
|
-
for (const family2 of FAMILIES) {
|
|
1768
|
-
console.log(`${pc.green(family2.id.padEnd(18))} ${family2.name.padEnd(22)} ${pc.dim(family2.summary)}`);
|
|
1769
|
-
}
|
|
1770
|
-
console.log(pc.dim("\nThe brief wraps every ask; edit .squint/brief.md to remix."));
|
|
1771
|
-
return;
|
|
1772
|
-
}
|
|
1773
|
-
const family = getFamily(familyId);
|
|
1774
|
-
if (!family) {
|
|
1775
|
-
console.error(pc.red(`\u2717 unknown family "${familyId}" \u2014 run squint brief to list`));
|
|
1776
|
-
process.exitCode = 1;
|
|
1777
|
-
return;
|
|
1778
|
-
}
|
|
1779
|
-
const target = nodePath.join(process.cwd(), ".squint", "brief.md");
|
|
1780
|
-
if (fs2.existsSync(target) && !options.force) {
|
|
1781
|
-
console.error(pc.red(`\u2717 ${target} exists \u2014 use --force to overwrite`));
|
|
1782
|
-
process.exitCode = 1;
|
|
1783
|
-
return;
|
|
1784
|
-
}
|
|
1785
|
-
fs2.mkdirSync(nodePath.dirname(target), { recursive: true });
|
|
1786
|
-
fs2.writeFileSync(target, renderFamilyBrief(family) + "\n");
|
|
1787
|
-
console.log(pc.green(`\u2713 ${family.name} direction written to .squint/brief.md`));
|
|
1788
|
-
console.log(pc.dim("every squint ask in this repo now holds this direction \u2014 edit the file to remix"));
|
|
1789
|
-
});
|
|
1790
|
-
program.command("check").description("Run this project\u2019s quality gates (typecheck, lint, test, build)").action(async () => {
|
|
1791
|
-
const { detectGates: detectGates2, runGates: runGates2 } = await import("./gates-ZC67NMW3.js");
|
|
1792
|
-
const cwd = process.cwd();
|
|
1793
|
-
const gates = detectGates2(cwd);
|
|
1794
|
-
if (gates.length === 0) {
|
|
1795
|
-
console.log(pc.dim("no gates detected (no package.json scripts, tsconfig, or eslint config)"));
|
|
1796
|
-
return;
|
|
1797
|
-
}
|
|
1798
|
-
console.log(pc.dim(`running ${gates.map((g) => g.id).join(" \u2192 ")}`));
|
|
1799
|
-
const results = await runGates2(cwd, gates, (result) => {
|
|
1800
|
-
const mark = result.ok ? pc.green("\u2713") : pc.red("\u2717");
|
|
1801
|
-
console.log(`${mark} ${result.gate.id.padEnd(10)} ${pc.dim(`${(result.durationMs / 1e3).toFixed(1)}s \xB7 ${result.gate.display}`)}`);
|
|
1802
|
-
if (!result.ok) console.log(pc.dim(result.outputTail.split("\n").slice(-12).join("\n")));
|
|
1803
2135
|
});
|
|
1804
|
-
if (results.some((r) => !r.ok)) process.exitCode = 1;
|
|
1805
|
-
});
|
|
1806
|
-
program.command("shot").description("Screenshot a running app at mobile/tablet/desktop viewports").argument("<url>", "URL of the running app (e.g. http://localhost:5173)").action(async (url) => {
|
|
1807
|
-
const { captureViewports: captureViewports2 } = await import("./preview-UCH7H5R5.js");
|
|
1808
|
-
const result = await captureViewports2(process.cwd(), url);
|
|
1809
|
-
if (!result) {
|
|
1810
|
-
console.error(pc.red("\u2717 no Chrome/Chromium found"));
|
|
1811
|
-
process.exitCode = 1;
|
|
1812
|
-
return;
|
|
1813
|
-
}
|
|
1814
|
-
for (const shot of result.shots) console.log(`${pc.green("\u2713")} ${shot.name.padEnd(8)} ${shot.path}`);
|
|
1815
|
-
for (const error of result.errors) console.error(pc.red(`\u2717 ${error}`));
|
|
1816
|
-
if (result.shots.length === 0) process.exitCode = 1;
|
|
1817
|
-
});
|
|
1818
|
-
var configCommand = program.command("config").description("Read or change squint configuration");
|
|
1819
|
-
configCommand.command("get").description("Print the resolved configuration").action(() => {
|
|
1820
|
-
const paths = defaultPaths(process.cwd());
|
|
1821
|
-
console.log(JSON.stringify(loadConfig(paths), null, 2));
|
|
1822
|
-
});
|
|
1823
|
-
configCommand.command("set").description("Set a value (keys: engine, models.<engineId>)").argument("<key>").argument("<value>").option("--project", "write to this project (.squint/config.json) instead of global").action((key, value, options) => {
|
|
1824
|
-
const paths = defaultPaths(process.cwd());
|
|
1825
|
-
const file = options.project ? paths.projectFile : paths.globalFile;
|
|
1826
|
-
setConfigValue(file, key, value);
|
|
1827
|
-
console.log(pc.green(`\u2713 ${key} = ${value}`) + pc.dim(` (${file})`));
|
|
1828
|
-
});
|
|
1829
|
-
configCommand.command("path").description("Show config file locations").action(() => {
|
|
1830
|
-
const paths = defaultPaths(process.cwd());
|
|
1831
|
-
console.log(`global ${paths.globalFile}`);
|
|
1832
|
-
console.log(`project ${paths.projectFile}`);
|
|
1833
|
-
});
|
|
1834
|
-
program.action(async () => {
|
|
1835
|
-
const cwd = process.cwd();
|
|
1836
|
-
const config = loadConfig(defaultPaths(cwd));
|
|
1837
|
-
const engineId = resolveEngineId(config);
|
|
1838
|
-
const model = resolveModel(config, engineId);
|
|
1839
|
-
let theme2 = config.theme;
|
|
1840
|
-
if (!theme2 && !process.env.NO_COLOR) {
|
|
1841
|
-
const { detectBackground } = await import("./background-J7OQTYEC.js");
|
|
1842
|
-
if (await detectBackground() === "light") theme2 = "light";
|
|
1843
|
-
}
|
|
1844
|
-
render(
|
|
1845
|
-
/* @__PURE__ */ jsx4(
|
|
1846
|
-
App,
|
|
1847
|
-
{
|
|
1848
|
-
cwd,
|
|
1849
|
-
initialEngine: engineId,
|
|
1850
|
-
initialModel: model,
|
|
1851
|
-
autoDev: config.autoDev,
|
|
1852
|
-
autoFix: config.autoFix,
|
|
1853
|
-
autoProbe: config.autoProbe,
|
|
1854
|
-
autoCheck: config.autoCheck,
|
|
1855
|
-
bell: config.bell,
|
|
1856
|
-
budgetUsd: config.budgetUsd,
|
|
1857
|
-
initialTheme: theme2
|
|
1858
|
-
}
|
|
1859
|
-
)
|
|
1860
|
-
);
|
|
1861
|
-
});
|
|
1862
|
-
function createPrinter() {
|
|
1863
|
-
let streaming = false;
|
|
1864
|
-
return (event) => {
|
|
1865
|
-
switch (event.type) {
|
|
1866
|
-
case "status":
|
|
1867
|
-
console.log(pc.dim(`\xB7 ${event.text}`));
|
|
1868
|
-
break;
|
|
1869
|
-
case "delta":
|
|
1870
|
-
streaming = true;
|
|
1871
|
-
process.stdout.write(event.text);
|
|
1872
|
-
break;
|
|
1873
|
-
case "text":
|
|
1874
|
-
if (event.streamed && streaming) {
|
|
1875
|
-
process.stdout.write("\n");
|
|
1876
|
-
} else {
|
|
1877
|
-
console.log(event.text);
|
|
1878
|
-
}
|
|
1879
|
-
streaming = false;
|
|
1880
|
-
break;
|
|
1881
|
-
case "thinking":
|
|
1882
|
-
console.log(pc.dim(pc.italic(event.text)));
|
|
1883
|
-
break;
|
|
1884
|
-
case "tool":
|
|
1885
|
-
if (streaming) {
|
|
1886
|
-
process.stdout.write("\n");
|
|
1887
|
-
streaming = false;
|
|
1888
|
-
}
|
|
1889
|
-
console.log(pc.cyan(`\u2699 ${event.name}${event.detail ? ` \xB7 ${event.detail}` : ""}`));
|
|
1890
|
-
break;
|
|
1891
|
-
case "error":
|
|
1892
|
-
console.error(pc.red(`\u2717 ${event.text}`));
|
|
1893
|
-
break;
|
|
1894
|
-
case "result":
|
|
1895
|
-
case "raw":
|
|
1896
|
-
break;
|
|
1897
|
-
}
|
|
1898
|
-
};
|
|
1899
2136
|
}
|
|
2137
|
+
|
|
2138
|
+
// src/cli.tsx
|
|
2139
|
+
var VERSION = "0.2.8";
|
|
2140
|
+
var program = new Command();
|
|
2141
|
+
program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
|
|
2142
|
+
registerRun(program);
|
|
2143
|
+
registerEnv(program);
|
|
2144
|
+
registerScaffold(program);
|
|
2145
|
+
registerProject(program);
|
|
2146
|
+
registerQuality(program);
|
|
2147
|
+
registerConfig(program);
|
|
2148
|
+
registerTui(program);
|
|
1900
2149
|
program.parseAsync(process.argv);
|