@aayambansal/squint 0.3.3 → 0.4.0
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/{cdp-UOIP4ZQZ.js → cdp-VRGCGN5R.js} +5 -3
- package/dist/{chunk-SXF7CVRW.js → chunk-2EJWUBUP.js} +5 -5
- package/dist/{chunk-XOVOQJFL.js → chunk-6FR4TRRQ.js} +55 -0
- package/dist/{chunk-S2ODU4MN.js → chunk-SVKV77TO.js} +3 -0
- package/dist/cli.js +143 -18
- package/dist/{commands-Y5RUKBPS.js → commands-DE5Q7VWY.js} +1 -1
- package/dist/flows-V5ZLVVSQ.js +108 -0
- package/dist/{preview-S6FOTRRR.js → preview-X7EWTRCZ.js} +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -133,6 +133,7 @@ squint config set autoDev true # dev server starts with the TUI
|
|
|
133
133
|
squint config set autoFix true # errors auto-route back (max 2 tries)
|
|
134
134
|
squint config set autoCheck false # skip the per-turn typecheck+lint pass
|
|
135
135
|
squint config set autoReview true # big visual change → automatic self-critique
|
|
136
|
+
squint config set fixModel haiku # mechanical fix turns run on the cheap tier
|
|
136
137
|
squint config set theme ocean # amber · ocean · moss · rose · mono
|
|
137
138
|
squint config set bell false # no bell on turn completion
|
|
138
139
|
squint doctor # engines + Chrome + WebSocket check
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ensureSquintIgnore
|
|
4
|
+
} from "./chunk-O2S6PAJE.js";
|
|
2
5
|
import {
|
|
3
6
|
findChrome,
|
|
4
7
|
screenshot
|
|
@@ -6,10 +9,7 @@ import {
|
|
|
6
9
|
import {
|
|
7
10
|
cdpCapture,
|
|
8
11
|
hasWebSocket
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import {
|
|
11
|
-
ensureSquintIgnore
|
|
12
|
-
} from "./chunk-O2S6PAJE.js";
|
|
12
|
+
} from "./chunk-6FR4TRRQ.js";
|
|
13
13
|
|
|
14
14
|
// src/preview/preview.ts
|
|
15
15
|
import fs from "fs";
|
|
@@ -133,7 +133,7 @@ async function probeRuntime(url, cwd) {
|
|
|
133
133
|
async function comparePulse(previous, current) {
|
|
134
134
|
const chrome = findChrome();
|
|
135
135
|
if (!chrome || !hasWebSocket()) return null;
|
|
136
|
-
const { pixelDiffPct } = await import("./cdp-
|
|
136
|
+
const { pixelDiffPct } = await import("./cdp-VRGCGN5R.js");
|
|
137
137
|
return pixelDiffPct(chrome, previous, current);
|
|
138
138
|
}
|
|
139
139
|
function buildRuntimeFixPrompt(report) {
|
|
@@ -178,6 +178,60 @@ var describe = (value) => {
|
|
|
178
178
|
}
|
|
179
179
|
return String(value);
|
|
180
180
|
};
|
|
181
|
+
async function runFlow(chromePath, baseUrl, flow, outDir) {
|
|
182
|
+
const { stepExpression } = await import("./flows-V5ZLVVSQ.js");
|
|
183
|
+
const { child, wsUrl, profileDir } = await launchChrome(chromePath);
|
|
184
|
+
const shots = [];
|
|
185
|
+
let connection = null;
|
|
186
|
+
try {
|
|
187
|
+
connection = await CdpConnection.connect(wsUrl, 1e4);
|
|
188
|
+
const { targetId } = await connection.send("Target.createTarget", { url: "about:blank" });
|
|
189
|
+
const { sessionId } = await connection.send("Target.attachToTarget", { targetId, flatten: true });
|
|
190
|
+
await connection.send("Page.enable", {}, sessionId);
|
|
191
|
+
await connection.send(
|
|
192
|
+
"Emulation.setDeviceMetricsOverride",
|
|
193
|
+
{ width: 1280, height: 800, deviceScaleFactor: 1, mobile: false },
|
|
194
|
+
sessionId
|
|
195
|
+
);
|
|
196
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
197
|
+
let stepNumber = 0;
|
|
198
|
+
for (const step of flow.steps) {
|
|
199
|
+
stepNumber += 1;
|
|
200
|
+
if (step.kind === "goto") {
|
|
201
|
+
const url = step.route === "/" ? base : `${base}${step.route}`;
|
|
202
|
+
await connection.send("Page.navigate", { url }, sessionId);
|
|
203
|
+
await new Promise((resolve) => setTimeout(resolve, 1800));
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (step.kind === "shot") {
|
|
207
|
+
const { data } = await connection.send("Page.captureScreenshot", { format: "png" }, sessionId);
|
|
208
|
+
const outPath = path.join(outDir, `flow-${flow.name}-${step.name}.png`);
|
|
209
|
+
fs.writeFileSync(outPath, Buffer.from(data, "base64"));
|
|
210
|
+
shots.push(outPath);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const expression = stepExpression(step);
|
|
214
|
+
if (!expression) continue;
|
|
215
|
+
const { result } = await connection.send(
|
|
216
|
+
"Runtime.evaluate",
|
|
217
|
+
{ expression, returnByValue: true },
|
|
218
|
+
sessionId
|
|
219
|
+
);
|
|
220
|
+
const value = result?.value;
|
|
221
|
+
if (!value?.ok) {
|
|
222
|
+
return { ok: false, failedStep: stepNumber, detail: value?.detail ?? "step failed", shots };
|
|
223
|
+
}
|
|
224
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
225
|
+
}
|
|
226
|
+
return { ok: true, shots };
|
|
227
|
+
} catch (err) {
|
|
228
|
+
return { ok: false, detail: err instanceof Error ? err.message : String(err), shots };
|
|
229
|
+
} finally {
|
|
230
|
+
connection?.close();
|
|
231
|
+
child.kill("SIGKILL");
|
|
232
|
+
setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
181
235
|
async function pixelDiffPct(chromePath, pngA, pngB) {
|
|
182
236
|
const { child, wsUrl, profileDir } = await launchChrome(chromePath);
|
|
183
237
|
let connection = null;
|
|
@@ -420,6 +474,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
420
474
|
|
|
421
475
|
export {
|
|
422
476
|
hasWebSocket,
|
|
477
|
+
runFlow,
|
|
423
478
|
pixelDiffPct,
|
|
424
479
|
cdpCapture
|
|
425
480
|
};
|
|
@@ -14,6 +14,8 @@ var COMMANDS = [
|
|
|
14
14
|
{ name: "shot", args: "[url]", group: "verify", description: "screenshot the app (or any url) at mobile/tablet/desktop" },
|
|
15
15
|
{ name: "review", args: "[focus]", group: "verify", description: "screenshots + the engine critiques its own rendered work" },
|
|
16
16
|
{ name: "polish", args: "[1-5]", group: "verify", description: "unattended rounds of review \u2192 fix (default 2)" },
|
|
17
|
+
{ name: "score", group: "verify", description: "deterministic quality snapshot (problems, a11y, tells, runtime, LCP)" },
|
|
18
|
+
{ name: "flows", args: "[name]", group: "verify", description: "replay declared .squint/flows/ journeys headlessly" },
|
|
17
19
|
{ name: "variants", args: "<2-4> <ask>", group: "explore", description: "parallel design explorations; apply/list/clean" },
|
|
18
20
|
{ name: "sandbox", args: "[on|diff|apply|discard]", group: "explore", description: "asks accumulate in a shadow worktree until you apply" },
|
|
19
21
|
{ name: "undo", group: "explore", description: "revert the last ask (files only)" },
|
|
@@ -23,6 +25,7 @@ var COMMANDS = [
|
|
|
23
25
|
{ name: "btw", args: "<question>", group: "session", description: "read-only side question; the main thread is untouched" },
|
|
24
26
|
{ name: "copy", group: "session", description: "copy the last reply to the clipboard" },
|
|
25
27
|
{ name: "save", group: "session", description: "export the transcript to .squint/transcripts/" },
|
|
28
|
+
{ name: "find", args: "<term>", group: "session", description: "search this session and saved transcripts" },
|
|
26
29
|
{ name: "resume", group: "session", description: "pick up the previous session for this repo" },
|
|
27
30
|
{ name: "clear", group: "session", description: "new session (transcript, totals, persisted state)" },
|
|
28
31
|
{ name: "help", group: "session", description: "list commands" },
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
completeCommand
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-SVKV77TO.js";
|
|
5
5
|
import {
|
|
6
6
|
applySandbox,
|
|
7
7
|
discardSandbox,
|
|
@@ -42,7 +42,12 @@ import {
|
|
|
42
42
|
comparePulse,
|
|
43
43
|
probeRuntime,
|
|
44
44
|
runtimeSummary
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-2EJWUBUP.js";
|
|
46
|
+
import {
|
|
47
|
+
clearState,
|
|
48
|
+
loadState,
|
|
49
|
+
saveState
|
|
50
|
+
} from "./chunk-O2S6PAJE.js";
|
|
46
51
|
import {
|
|
47
52
|
runAgent
|
|
48
53
|
} from "./chunk-VH7OOFQP.js";
|
|
@@ -53,18 +58,13 @@ import {
|
|
|
53
58
|
detectEngines,
|
|
54
59
|
getEngine
|
|
55
60
|
} from "./chunk-KVYGPLWW.js";
|
|
56
|
-
import "./chunk-
|
|
61
|
+
import "./chunk-6FR4TRRQ.js";
|
|
57
62
|
import {
|
|
58
63
|
enrich
|
|
59
64
|
} from "./chunk-H5K55LXY.js";
|
|
60
65
|
import {
|
|
61
66
|
composePrompt
|
|
62
67
|
} from "./chunk-P3H4N2EN.js";
|
|
63
|
-
import {
|
|
64
|
-
clearState,
|
|
65
|
-
loadState,
|
|
66
|
-
saveState
|
|
67
|
-
} from "./chunk-O2S6PAJE.js";
|
|
68
68
|
|
|
69
69
|
// src/cli.tsx
|
|
70
70
|
import { Command } from "commander";
|
|
@@ -96,6 +96,8 @@ var ConfigSchema = z.object({
|
|
|
96
96
|
budgetUsd: z.number().positive().optional(),
|
|
97
97
|
/** Auto-run /review when the visual pulse shows a big change (default off). */
|
|
98
98
|
autoReview: z.boolean().optional(),
|
|
99
|
+
/** Cheaper model used for auto-fix and /fix turns (mechanical work). */
|
|
100
|
+
fixModel: z.string().optional(),
|
|
99
101
|
/** TUI theme name (amber, ocean, moss, rose, mono). */
|
|
100
102
|
theme: z.string().optional()
|
|
101
103
|
});
|
|
@@ -138,7 +140,7 @@ function resolveModel(config, engineId, override) {
|
|
|
138
140
|
function setConfigValue(file, key, value) {
|
|
139
141
|
const current = readConfigFile(file);
|
|
140
142
|
let next;
|
|
141
|
-
if (key === "engine" || key === "theme") {
|
|
143
|
+
if (key === "engine" || key === "theme" || key === "fixModel") {
|
|
142
144
|
next = { ...current, [key]: value };
|
|
143
145
|
} else if (key === "autoDev" || key === "autoFix" || key === "autoProbe" || key === "autoCheck" || key === "autoReview" || key === "bell") {
|
|
144
146
|
if (value !== "true" && value !== "false") {
|
|
@@ -157,7 +159,7 @@ function setConfigValue(file, key, value) {
|
|
|
157
159
|
next = { ...current, models: { ...current.models, [engineId]: value } };
|
|
158
160
|
} else {
|
|
159
161
|
throw new Error(
|
|
160
|
-
`Unknown config key "${key}". Supported: engine, theme, autoDev, autoFix, autoProbe, autoCheck, autoReview, bell, budgetUsd, models.<engineId>`
|
|
162
|
+
`Unknown config key "${key}". Supported: engine, theme, autoDev, autoFix, autoProbe, autoCheck, autoReview, bell, budgetUsd, fixModel, models.<engineId>`
|
|
161
163
|
);
|
|
162
164
|
}
|
|
163
165
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -236,7 +238,7 @@ function registerEnv(program2) {
|
|
|
236
238
|
}
|
|
237
239
|
}
|
|
238
240
|
const { findChrome: findChrome2 } = await import("./chrome-SBV3H77F.js");
|
|
239
|
-
const { hasWebSocket } = await import("./cdp-
|
|
241
|
+
const { hasWebSocket } = await import("./cdp-VRGCGN5R.js");
|
|
240
242
|
const chrome = findChrome2();
|
|
241
243
|
console.log(
|
|
242
244
|
chrome ? `${pc2.green("\u2713")} Chrome ${pc2.dim(chrome)}` : `${pc2.yellow("\u25CB")} Chrome ${pc2.dim("\u2014 screenshots and runtime probing disabled")}`
|
|
@@ -459,7 +461,7 @@ function registerQuality(program2) {
|
|
|
459
461
|
if (results.some((r) => !r.ok)) process.exitCode = 1;
|
|
460
462
|
});
|
|
461
463
|
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) => {
|
|
462
|
-
const { captureViewports: captureViewports2 } = await import("./preview-
|
|
464
|
+
const { captureViewports: captureViewports2 } = await import("./preview-X7EWTRCZ.js");
|
|
463
465
|
const result = await captureViewports2(process.cwd(), url);
|
|
464
466
|
if (!result) {
|
|
465
467
|
console.error(pc4.red("\u2717 no Chrome/Chromium found"));
|
|
@@ -756,10 +758,11 @@ var Session = class {
|
|
|
756
758
|
dispatchFix(problems) {
|
|
757
759
|
if (problems.length === 0) return;
|
|
758
760
|
const prompt = this.combinedFixPrompt(problems);
|
|
759
|
-
const
|
|
761
|
+
const fixModel = this.opts.fixModel;
|
|
762
|
+
const display = `\u26D1 fix: ${problems.map((p) => p.source).join(" + ")}${fixModel ? ` \xB7 ${fixModel}` : ""}`;
|
|
760
763
|
for (const problem of problems) this.problemPrompts.delete(problem.id);
|
|
761
764
|
this.notify({ problems: this.state.problems.filter((p) => !problems.includes(p)) });
|
|
762
|
-
void this.runTurn(prompt, display);
|
|
765
|
+
void this.runTurn(prompt, display, fixModel);
|
|
763
766
|
}
|
|
764
767
|
/** Launch a capped auto-fix turn over all open problems. Returns true if launched. */
|
|
765
768
|
maybeAutoFix() {
|
|
@@ -1006,7 +1009,7 @@ ${question}`,
|
|
|
1006
1009
|
}
|
|
1007
1010
|
};
|
|
1008
1011
|
/** Run one engine turn. `display` is what the transcript shows as the ask. */
|
|
1009
|
-
async runTurn(prompt, display) {
|
|
1012
|
+
async runTurn(prompt, display, modelOverride) {
|
|
1010
1013
|
this.push("user", display);
|
|
1011
1014
|
this.turnEdits = 0;
|
|
1012
1015
|
this.turnTools = 0;
|
|
@@ -1019,7 +1022,7 @@ ${question}`,
|
|
|
1019
1022
|
{
|
|
1020
1023
|
prompt,
|
|
1021
1024
|
cwd: this.execCwd(),
|
|
1022
|
-
model: this.state.model,
|
|
1025
|
+
model: modelOverride ?? this.state.model,
|
|
1023
1026
|
mode: this.state.mode,
|
|
1024
1027
|
sessionId: engine.supportsResume ? this.sessionId : void 0
|
|
1025
1028
|
},
|
|
@@ -1392,6 +1395,50 @@ They are objective defects, not style preferences.`
|
|
|
1392
1395
|
}
|
|
1393
1396
|
break;
|
|
1394
1397
|
}
|
|
1398
|
+
case "find": {
|
|
1399
|
+
if (!arg) {
|
|
1400
|
+
this.push("status", "usage: /find <term> \u2014 searches this session and saved transcripts");
|
|
1401
|
+
break;
|
|
1402
|
+
}
|
|
1403
|
+
const needle = arg.toLowerCase();
|
|
1404
|
+
const matches = [];
|
|
1405
|
+
for (const item of this.state.items) {
|
|
1406
|
+
if ((item.role === "user" || item.role === "assistant") && item.text.toLowerCase().includes(needle)) {
|
|
1407
|
+
const line = item.text.split("\n").find((l) => l.toLowerCase().includes(needle)) ?? item.text;
|
|
1408
|
+
matches.push(`[live] ${item.role === "user" ? "\u276F " : ""}${line.trim().slice(0, 90)}`);
|
|
1409
|
+
if (matches.length >= 8) break;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
if (matches.length < 8) {
|
|
1413
|
+
void (async () => {
|
|
1414
|
+
try {
|
|
1415
|
+
const fs3 = await import("fs");
|
|
1416
|
+
const path5 = await import("path");
|
|
1417
|
+
const dir = path5.join(this.opts.cwd, ".squint", "transcripts");
|
|
1418
|
+
const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".md")).sort().reverse().slice(0, 20);
|
|
1419
|
+
for (const file of files) {
|
|
1420
|
+
if (matches.length >= 8) break;
|
|
1421
|
+
const text = fs3.readFileSync(path5.join(dir, file), "utf8");
|
|
1422
|
+
for (const line of text.split("\n")) {
|
|
1423
|
+
if (line.toLowerCase().includes(needle)) {
|
|
1424
|
+
matches.push(`[${file.replace(/\.md$/, "")}] ${line.trim().slice(0, 90)}`);
|
|
1425
|
+
break;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
} catch {
|
|
1430
|
+
}
|
|
1431
|
+
this.push(
|
|
1432
|
+
"status",
|
|
1433
|
+
matches.length > 0 ? `${matches.join("\n")}
|
|
1434
|
+
/checkpoints can rewind \xB7 /save archives this session` : `no matches for "${arg}"`
|
|
1435
|
+
);
|
|
1436
|
+
})();
|
|
1437
|
+
} else {
|
|
1438
|
+
this.push("status", matches.join("\n"));
|
|
1439
|
+
}
|
|
1440
|
+
break;
|
|
1441
|
+
}
|
|
1395
1442
|
case "save": {
|
|
1396
1443
|
void (async () => {
|
|
1397
1444
|
const fs3 = await import("fs");
|
|
@@ -1427,6 +1474,81 @@ They are objective defects, not style preferences.`
|
|
|
1427
1474
|
})();
|
|
1428
1475
|
break;
|
|
1429
1476
|
}
|
|
1477
|
+
case "flows": {
|
|
1478
|
+
if (!this.state.devUrl) {
|
|
1479
|
+
this.push("error", "dev server not running \u2014 /dev first");
|
|
1480
|
+
break;
|
|
1481
|
+
}
|
|
1482
|
+
void (async () => {
|
|
1483
|
+
const { loadFlows } = await import("./flows-V5ZLVVSQ.js");
|
|
1484
|
+
const flows = loadFlows(this.opts.cwd);
|
|
1485
|
+
if (flows.length === 0) {
|
|
1486
|
+
this.push("status", "no flows \u2014 add .squint/flows/<name>.flow (goto/click/fill/press/expect/shot lines), or ask the engine to write one");
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
const chrome = findChrome();
|
|
1490
|
+
if (!chrome) {
|
|
1491
|
+
this.push("error", "no Chrome/Chromium found for flows");
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
const { runFlow } = await import("./cdp-VRGCGN5R.js");
|
|
1495
|
+
const { previewDir } = await import("./preview-X7EWTRCZ.js");
|
|
1496
|
+
this.push("status", `replaying ${flows.length} flow(s)\u2026`);
|
|
1497
|
+
this.notify({ running: true, runStartedAt: Date.now() });
|
|
1498
|
+
const failures = [];
|
|
1499
|
+
for (const flow of flows) {
|
|
1500
|
+
const wanted = arg.trim();
|
|
1501
|
+
if (wanted && flow.name !== wanted) continue;
|
|
1502
|
+
const result = await runFlow(chrome, this.state.devUrl, flow, previewDir(this.opts.cwd));
|
|
1503
|
+
if (result.ok) {
|
|
1504
|
+
this.push("status", `\u2713 flow ${flow.name} \xB7 ${flow.steps.length} steps${result.shots.length > 0 ? ` \xB7 ${result.shots.length} shot(s)` : ""}`);
|
|
1505
|
+
for (const shot of result.shots) this.push("image", shot);
|
|
1506
|
+
} else {
|
|
1507
|
+
const where = result.failedStep ? ` at step ${result.failedStep}` : "";
|
|
1508
|
+
this.push("error", `\u2717 flow ${flow.name}${where}: ${result.detail}`);
|
|
1509
|
+
failures.push(`Flow "${flow.name}" fails${where}: ${result.detail}. The flow file is .squint/flows/${flow.name}.flow \u2014 fix the app (or the flow if the UI legitimately changed).`);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
this.notify({ running: false });
|
|
1513
|
+
if (failures.length > 0) {
|
|
1514
|
+
this.addProblem("flow", `${failures.length} flow(s) failing`, failures.join("\n\n"));
|
|
1515
|
+
this.push("status", "/fix sends open problems to the engine \xB7 /problems lists them");
|
|
1516
|
+
} else {
|
|
1517
|
+
this.clearProblems("flow");
|
|
1518
|
+
}
|
|
1519
|
+
this.drainQueue();
|
|
1520
|
+
})();
|
|
1521
|
+
break;
|
|
1522
|
+
}
|
|
1523
|
+
case "score": {
|
|
1524
|
+
if (!this.state.devUrl) {
|
|
1525
|
+
this.push("error", "dev server not running \u2014 /dev first");
|
|
1526
|
+
break;
|
|
1527
|
+
}
|
|
1528
|
+
void (async () => {
|
|
1529
|
+
const result = await this.capture();
|
|
1530
|
+
if (!result) return;
|
|
1531
|
+
const a11yCount = result.a11y?.length ?? 0;
|
|
1532
|
+
const slopCount = result.slop?.length ?? 0;
|
|
1533
|
+
const runtimeBad = result.runtime ? runtimeSummary(result.runtime) ? 1 : 0 : 0;
|
|
1534
|
+
const problems = this.state.problems.length;
|
|
1535
|
+
const score = Math.max(
|
|
1536
|
+
0,
|
|
1537
|
+
5 - problems * 0.75 - Math.min(a11yCount, 4) * 0.25 - Math.min(slopCount, 4) * 0.25 - runtimeBad
|
|
1538
|
+
);
|
|
1539
|
+
const lcp = this.lastPerf?.lcpMs !== void 0 ? ` \xB7 LCP ${this.lastPerf.lcpMs}ms` : "";
|
|
1540
|
+
this.push(
|
|
1541
|
+
"status",
|
|
1542
|
+
[
|
|
1543
|
+
`score: ${score.toFixed(2)}/5 (deterministic axes)`,
|
|
1544
|
+
` open problems: ${problems}${problems > 0 ? ` (${this.state.problems.map((p) => p.source).join(", ")})` : ""}`,
|
|
1545
|
+
` a11y findings: ${a11yCount} \xB7 distinctiveness tells: ${slopCount} \xB7 runtime ${runtimeBad ? "dirty" : "clean"}${lcp}`,
|
|
1546
|
+
" /review judges what numbers cannot \u2014 hierarchy, taste, coherence"
|
|
1547
|
+
].join("\n")
|
|
1548
|
+
);
|
|
1549
|
+
})();
|
|
1550
|
+
break;
|
|
1551
|
+
}
|
|
1430
1552
|
case "polish": {
|
|
1431
1553
|
const rounds = arg ? Number.parseInt(arg, 10) : 2;
|
|
1432
1554
|
if (!Number.isInteger(rounds) || rounds < 1 || rounds > 5) {
|
|
@@ -1692,7 +1814,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
|
|
|
1692
1814
|
this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
|
|
1693
1815
|
break;
|
|
1694
1816
|
case "help": {
|
|
1695
|
-
void import("./commands-
|
|
1817
|
+
void import("./commands-DE5Q7VWY.js").then(({ commandHelp }) => this.push("status", commandHelp()));
|
|
1696
1818
|
break;
|
|
1697
1819
|
}
|
|
1698
1820
|
case "quit":
|
|
@@ -2063,6 +2185,7 @@ function App({
|
|
|
2063
2185
|
autoProbe,
|
|
2064
2186
|
autoCheck,
|
|
2065
2187
|
autoReview,
|
|
2188
|
+
fixModel,
|
|
2066
2189
|
bell,
|
|
2067
2190
|
budgetUsd,
|
|
2068
2191
|
initialTheme
|
|
@@ -2081,6 +2204,7 @@ function App({
|
|
|
2081
2204
|
autoProbe,
|
|
2082
2205
|
autoCheck,
|
|
2083
2206
|
autoReview,
|
|
2207
|
+
fixModel,
|
|
2084
2208
|
budgetUsd,
|
|
2085
2209
|
// Delay lets the goodbye summary land in the Static scrollback.
|
|
2086
2210
|
onQuit: () => setTimeout(() => exit(), 60)
|
|
@@ -2298,6 +2422,7 @@ function registerTui(program2) {
|
|
|
2298
2422
|
autoProbe: config.autoProbe,
|
|
2299
2423
|
autoCheck: config.autoCheck,
|
|
2300
2424
|
autoReview: config.autoReview,
|
|
2425
|
+
fixModel: config.fixModel,
|
|
2301
2426
|
bell: config.bell,
|
|
2302
2427
|
budgetUsd: config.budgetUsd,
|
|
2303
2428
|
initialTheme: theme2
|
|
@@ -2308,7 +2433,7 @@ function registerTui(program2) {
|
|
|
2308
2433
|
}
|
|
2309
2434
|
|
|
2310
2435
|
// src/cli.tsx
|
|
2311
|
-
var VERSION = true ? "0.
|
|
2436
|
+
var VERSION = true ? "0.4.0" : "0.0.0-dev";
|
|
2312
2437
|
var program = new Command();
|
|
2313
2438
|
program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
|
|
2314
2439
|
registerRun(program);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/preview/flows.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
function parseFlow(name, text) {
|
|
7
|
+
const steps = [];
|
|
8
|
+
for (const rawLine of text.split("\n")) {
|
|
9
|
+
const line = rawLine.trim();
|
|
10
|
+
if (line.length === 0 || line.startsWith("#")) continue;
|
|
11
|
+
const [verb, ...rest] = line.split(/\s+/);
|
|
12
|
+
const arg = rest.join(" ");
|
|
13
|
+
switch (verb) {
|
|
14
|
+
case "goto":
|
|
15
|
+
steps.push({ kind: "goto", route: arg.startsWith("/") ? arg : `/${arg}` });
|
|
16
|
+
break;
|
|
17
|
+
case "click":
|
|
18
|
+
steps.push({ kind: "click", target: arg });
|
|
19
|
+
break;
|
|
20
|
+
case "fill": {
|
|
21
|
+
const [selector, ...valueParts] = rest;
|
|
22
|
+
if (!selector || valueParts.length === 0) return null;
|
|
23
|
+
steps.push({ kind: "fill", selector, value: valueParts.join(" ") });
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
case "press":
|
|
27
|
+
steps.push({ kind: "press", key: arg });
|
|
28
|
+
break;
|
|
29
|
+
case "expect":
|
|
30
|
+
steps.push({ kind: "expect", text: arg });
|
|
31
|
+
break;
|
|
32
|
+
case "shot":
|
|
33
|
+
steps.push({ kind: "shot", name: arg.replace(/[^a-zA-Z0-9-]/g, "-") });
|
|
34
|
+
break;
|
|
35
|
+
default:
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return steps.length > 0 ? { name, steps } : null;
|
|
40
|
+
}
|
|
41
|
+
function loadFlows(cwd) {
|
|
42
|
+
const dir = path.join(cwd, ".squint", "flows");
|
|
43
|
+
let entries;
|
|
44
|
+
try {
|
|
45
|
+
entries = fs.readdirSync(dir).filter((f) => f.endsWith(".flow"));
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const flows = [];
|
|
50
|
+
for (const entry of entries.sort()) {
|
|
51
|
+
try {
|
|
52
|
+
const flow = parseFlow(entry.replace(/\.flow$/, ""), fs.readFileSync(path.join(dir, entry), "utf8"));
|
|
53
|
+
if (flow) flows.push(flow);
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return flows;
|
|
58
|
+
}
|
|
59
|
+
function stepExpression(step) {
|
|
60
|
+
switch (step.kind) {
|
|
61
|
+
case "click":
|
|
62
|
+
return `(() => {
|
|
63
|
+
const target = ${JSON.stringify(step.target)};
|
|
64
|
+
let el = null;
|
|
65
|
+
try { el = document.querySelector(target); } catch {}
|
|
66
|
+
if (!el) {
|
|
67
|
+
const all = [...document.querySelectorAll('a, button, [role=button], input[type=submit], summary, label')];
|
|
68
|
+
el = all.find((e) => (e.textContent || '').trim().toLowerCase().includes(target.toLowerCase()));
|
|
69
|
+
}
|
|
70
|
+
if (!el) return { ok: false, detail: 'no element matching ' + target };
|
|
71
|
+
el.click();
|
|
72
|
+
return { ok: true };
|
|
73
|
+
})()`;
|
|
74
|
+
case "fill":
|
|
75
|
+
return `(() => {
|
|
76
|
+
const el = document.querySelector(${JSON.stringify(step.selector)});
|
|
77
|
+
if (!el) return { ok: false, detail: 'no element matching ${step.selector}' };
|
|
78
|
+
const setter = Object.getOwnPropertyDescriptor(el.constructor.prototype, 'value')?.set;
|
|
79
|
+
if (setter) setter.call(el, ${JSON.stringify(step.value)}); else el.value = ${JSON.stringify(step.value)};
|
|
80
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
81
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
82
|
+
return { ok: true };
|
|
83
|
+
})()`;
|
|
84
|
+
case "expect":
|
|
85
|
+
return `(() => {
|
|
86
|
+
const wanted = ${JSON.stringify(step.text)}.toLowerCase();
|
|
87
|
+
const ok = (document.body.innerText || '').toLowerCase().includes(wanted);
|
|
88
|
+
return ok ? { ok: true } : { ok: false, detail: 'page does not show: ' + ${JSON.stringify(step.text)} };
|
|
89
|
+
})()`;
|
|
90
|
+
case "press":
|
|
91
|
+
return `(() => {
|
|
92
|
+
const key = ${JSON.stringify(step.key)};
|
|
93
|
+
const el = document.activeElement || document.body;
|
|
94
|
+
for (const type of ['keydown', 'keypress', 'keyup']) {
|
|
95
|
+
el.dispatchEvent(new KeyboardEvent(type, { key, bubbles: true, cancelable: true }));
|
|
96
|
+
}
|
|
97
|
+
return { ok: true };
|
|
98
|
+
})()`;
|
|
99
|
+
case "goto":
|
|
100
|
+
case "shot":
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export {
|
|
105
|
+
loadFlows,
|
|
106
|
+
parseFlow,
|
|
107
|
+
stepExpression
|
|
108
|
+
};
|
|
@@ -10,11 +10,11 @@ import {
|
|
|
10
10
|
probeRuntime,
|
|
11
11
|
routeShotName,
|
|
12
12
|
runtimeSummary
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-2EJWUBUP.js";
|
|
14
|
+
import "./chunk-O2S6PAJE.js";
|
|
14
15
|
import "./chunk-IMDRXXFU.js";
|
|
15
16
|
import "./chunk-KVYGPLWW.js";
|
|
16
|
-
import "./chunk-
|
|
17
|
-
import "./chunk-O2S6PAJE.js";
|
|
17
|
+
import "./chunk-6FR4TRRQ.js";
|
|
18
18
|
export {
|
|
19
19
|
VIEWPORTS,
|
|
20
20
|
buildReviewPrompt,
|
package/package.json
CHANGED