@aayambansal/squint 0.2.8 → 0.3.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 +4 -0
- package/dist/{cdp-SFWNP7OA.js → cdp-4IZHB3W5.js} +1 -1
- package/dist/{chrome-NVU47PLK.js → chrome-SBV3H77F.js} +2 -2
- package/dist/{chunk-X6MDBKED.js → chunk-A64VVTPU.js} +1 -1
- package/dist/chunk-CFWKEIJJ.js +55 -0
- package/dist/{chunk-5VKUJPOL.js → chunk-F6JZSXEO.js} +3 -3
- package/dist/{chunk-B7LOERSP.js → chunk-IMDRXXFU.js} +1 -1
- package/dist/{chunk-BWZFACBT.js → chunk-KVYGPLWW.js} +12 -2
- package/dist/chunk-O2S6PAJE.js +51 -0
- package/dist/{chunk-OC6RU6XH.js → chunk-QEENJGCL.js} +55 -1
- package/dist/{chunk-CHZE3UHK.js → chunk-RQFFAJFE.js} +25 -60
- package/dist/{chunk-C7WKNJG6.js → chunk-VH7OOFQP.js} +1 -1
- package/dist/chunk-XEQ6JXXL.js +97 -0
- package/dist/cli.js +183 -164
- package/dist/{commands-N33DKZKC.js → commands-QKDWUD4K.js} +1 -1
- package/dist/{preview-XGASKUXR.js → preview-56UCV5A3.js} +5 -4
- package/dist/{registry-S3PCXJVF.js → registry-MIJ6LSAY.js} +1 -1
- package/dist/{run-S772BPMZ.js → run-NDSNTVYP.js} +2 -2
- package/dist/sandbox-J2NCPYHJ.js +19 -0
- package/dist/shots-E2AWBDCN.js +12 -0
- package/dist/state-QOS7WCZO.js +13 -0
- package/dist/{variants-UULPQ7Q5.js → variants-3ZSINJGX.js} +3 -3
- package/package.json +2 -1
- package/dist/chunk-YTO2YRF7.js +0 -41
- package/dist/shots-FVG4CDVK.js +0 -12
package/README.md
CHANGED
|
@@ -150,6 +150,9 @@ squint doctor --probe # run every engine end to end, verify auth act
|
|
|
150
150
|
- **Problems**: findings from gates, the dev server, the runtime probe, and a11y sweeps
|
|
151
151
|
collect into a list — `/problems` shows it, `/fix` sends everything as one turn,
|
|
152
152
|
`/fix <n>` targets one. The footer counts what's open.
|
|
153
|
+
- **Sandbox**: `/sandbox on` and asks accumulate in a shadow worktree — the dev server,
|
|
154
|
+
gates, and probes all run there; `/sandbox diff` shows what is staged, `apply` lands it
|
|
155
|
+
as one diff, `discard` walks away with the real tree untouched.
|
|
153
156
|
- **Variants without leaving**: `/variants 3 <ask>` runs parallel explorations with
|
|
154
157
|
streaming per-family status; `/variants apply <id>` keeps the winner.
|
|
155
158
|
- **Commands**: type `/` and matching commands appear with descriptions; tab completes.
|
|
@@ -228,6 +231,7 @@ All product behavior lives in the harness, so a new engine is ~80 lines.
|
|
|
228
231
|
|
|
229
232
|
- [Engine setup guide](./docs/engines.md) — install + auth for all eight, and how to choose
|
|
230
233
|
- [Configuration](./docs/configuration.md) — every key, every `.squint/` file
|
|
234
|
+
- [The loops](./docs/loops.md) — everything that runs automatically around each turn
|
|
231
235
|
- [Architecture](./docs/design/2026-07-25-architecture.md)
|
|
232
236
|
- [How Lovable works under the hood](./docs/research/lovable.md)
|
|
233
237
|
- [Making agents produce excellent frontend work](./docs/research/frontend-quality.md)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/session/commands.ts
|
|
4
|
+
var COMMANDS = [
|
|
5
|
+
{ name: "dev", args: "[restart|logs]", group: "build", description: "start/stop the dev server; restart or show recent output" },
|
|
6
|
+
{ name: "mode", args: "plan|safe|yolo", group: "build", description: "how much the engine may do (shift+tab cycles)" },
|
|
7
|
+
{ name: "engine", args: "<id>", group: "build", description: "switch backend (new session)" },
|
|
8
|
+
{ name: "engines", group: "build", description: "list installed engines with streaming/resume support" },
|
|
9
|
+
{ name: "model", args: "[name]", group: "build", description: "model override for the engine" },
|
|
10
|
+
{ name: "queue", args: "clear|drop <n>", group: "build", description: "clear the queue or drop one queued ask" },
|
|
11
|
+
{ name: "check", group: "verify", description: "run all quality gates (typecheck, lint, format, test, build)" },
|
|
12
|
+
{ name: "problems", group: "verify", description: "list open findings from gates, dev server, runtime, a11y" },
|
|
13
|
+
{ name: "fix", args: "[n]", group: "verify", description: "send all open problems to the engine, or just problem n" },
|
|
14
|
+
{ name: "shot", args: "[url]", group: "verify", description: "screenshot the app (or any url) at mobile/tablet/desktop" },
|
|
15
|
+
{ name: "review", args: "[focus]", group: "verify", description: "screenshots + the engine critiques its own rendered work" },
|
|
16
|
+
{ name: "variants", args: "<2-4> <ask>", group: "explore", description: "parallel design explorations; apply/list/clean" },
|
|
17
|
+
{ name: "sandbox", args: "[on|diff|apply|discard]", group: "explore", description: "asks accumulate in a shadow worktree until you apply" },
|
|
18
|
+
{ name: "undo", group: "explore", description: "revert the last ask (files only)" },
|
|
19
|
+
{ name: "checkpoints", group: "explore", description: "list per-ask checkpoints" },
|
|
20
|
+
{ name: "restore", args: "<n>", group: "explore", description: "rewind files to before ask n" },
|
|
21
|
+
{ name: "theme", args: "[name]", group: "session", description: "switch the TUI theme", viewLevel: true },
|
|
22
|
+
{ name: "copy", group: "session", description: "copy the last reply to the clipboard" },
|
|
23
|
+
{ name: "save", group: "session", description: "export the transcript to .squint/transcripts/" },
|
|
24
|
+
{ name: "resume", group: "session", description: "pick up the previous session for this repo" },
|
|
25
|
+
{ name: "clear", group: "session", description: "new session (transcript, totals, persisted state)" },
|
|
26
|
+
{ name: "help", group: "session", description: "list commands" },
|
|
27
|
+
{ name: "quit", group: "session", description: "exit with a session summary" }
|
|
28
|
+
];
|
|
29
|
+
function completeCommand(partial) {
|
|
30
|
+
const query = partial.toLowerCase();
|
|
31
|
+
return COMMANDS.filter((c) => c.name.startsWith(query));
|
|
32
|
+
}
|
|
33
|
+
var GROUP_TITLES = {
|
|
34
|
+
build: "build",
|
|
35
|
+
verify: "verify",
|
|
36
|
+
explore: "explore & rewind",
|
|
37
|
+
session: "session"
|
|
38
|
+
};
|
|
39
|
+
function commandHelp() {
|
|
40
|
+
const sections = [];
|
|
41
|
+
for (const group of ["build", "verify", "explore", "session"]) {
|
|
42
|
+
const rows = COMMANDS.filter((c) => c.group === group).map(
|
|
43
|
+
(c) => ` /${c.name}${c.args ? ` ${c.args}` : ""} \u2014 ${c.description}`
|
|
44
|
+
);
|
|
45
|
+
sections.push(`${GROUP_TITLES[group]}
|
|
46
|
+
${rows.join("\n")}`);
|
|
47
|
+
}
|
|
48
|
+
return sections.join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
COMMANDS,
|
|
53
|
+
completeCommand,
|
|
54
|
+
commandHelp
|
|
55
|
+
};
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
variantsRoot
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-A64VVTPU.js";
|
|
5
5
|
import {
|
|
6
6
|
findChrome,
|
|
7
7
|
screenshot
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-IMDRXXFU.js";
|
|
9
9
|
import {
|
|
10
10
|
lineSplitter
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-KVYGPLWW.js";
|
|
12
12
|
|
|
13
13
|
// src/variants/shots.ts
|
|
14
14
|
import path2 from "path";
|
|
@@ -98,8 +98,18 @@ function createClaudeStreamParser(readyLabel) {
|
|
|
98
98
|
durationMs: data.duration_ms
|
|
99
99
|
}
|
|
100
100
|
];
|
|
101
|
-
case "user":
|
|
102
|
-
return [];
|
|
101
|
+
case "user": {
|
|
102
|
+
if (data.parent_tool_use_id) return [];
|
|
103
|
+
const events = [];
|
|
104
|
+
for (const block of data.message?.content ?? []) {
|
|
105
|
+
if (block.type === "tool_result" && block.is_error === true) {
|
|
106
|
+
const raw = Array.isArray(block.content) ? block.content.map((c) => c?.text ?? "").join(" ") : String(block.content ?? "");
|
|
107
|
+
const text = raw.trim().split("\n").at(-1) ?? "";
|
|
108
|
+
if (text) events.push({ type: "status", text: `\u26A0 tool error \xB7 ${truncate(text, 100)}` });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return events;
|
|
112
|
+
}
|
|
103
113
|
case "rate_limit_event":
|
|
104
114
|
return [];
|
|
105
115
|
default:
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/state/state.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
var IGNORED = ["preview/", "state.json", "variants/", "sandbox/", "sandbox.patch", "transcripts/"];
|
|
7
|
+
function ensureSquintIgnore(cwd) {
|
|
8
|
+
const dir = path.join(cwd, ".squint");
|
|
9
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
10
|
+
const file = path.join(dir, ".gitignore");
|
|
11
|
+
let existing = "";
|
|
12
|
+
try {
|
|
13
|
+
existing = fs.readFileSync(file, "utf8");
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
const lines = existing.split("\n").filter((l) => l.trim().length > 0);
|
|
17
|
+
const missing = IGNORED.filter((entry) => !lines.includes(entry));
|
|
18
|
+
if (missing.length > 0) {
|
|
19
|
+
fs.writeFileSync(file, [...lines, ...missing].join("\n") + "\n");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function stateFile(cwd) {
|
|
23
|
+
return path.join(cwd, ".squint", "state.json");
|
|
24
|
+
}
|
|
25
|
+
function loadState(cwd) {
|
|
26
|
+
try {
|
|
27
|
+
const raw = JSON.parse(fs.readFileSync(stateFile(cwd), "utf8"));
|
|
28
|
+
if (typeof raw?.engine === "string" && typeof raw?.sessionId === "string" && typeof raw?.at === "number") {
|
|
29
|
+
return raw;
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function saveState(cwd, state) {
|
|
36
|
+
ensureSquintIgnore(cwd);
|
|
37
|
+
fs.writeFileSync(stateFile(cwd), JSON.stringify(state, null, 2) + "\n");
|
|
38
|
+
}
|
|
39
|
+
function clearState(cwd) {
|
|
40
|
+
try {
|
|
41
|
+
fs.rmSync(stateFile(cwd));
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
ensureSquintIgnore,
|
|
48
|
+
loadState,
|
|
49
|
+
saveState,
|
|
50
|
+
clearState
|
|
51
|
+
};
|
|
@@ -201,11 +201,56 @@ async function pixelDiffPct(chromePath, pngA, pngB) {
|
|
|
201
201
|
setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
|
+
var SLOP_AUDIT = `(() => {
|
|
205
|
+
const out = [];
|
|
206
|
+
const bodyFont = (getComputedStyle(document.body).fontFamily || '').toLowerCase();
|
|
207
|
+
const h1 = document.querySelector('h1,h2');
|
|
208
|
+
const displayFont = h1 ? (getComputedStyle(h1).fontFamily || '').toLowerCase() : bodyFont;
|
|
209
|
+
for (const tell of ['inter', 'roboto', 'arial', 'space grotesk']) {
|
|
210
|
+
if (displayFont.includes(tell) || bodyFont.split(',')[0].includes(tell)) {
|
|
211
|
+
out.push('generic font stack: ' + tell + ' (' + (displayFont.includes(tell) ? 'display' : 'body') + ')');
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const vw = innerWidth, vh = innerHeight;
|
|
216
|
+
for (const el of document.querySelectorAll('*')) {
|
|
217
|
+
const r = el.getBoundingClientRect();
|
|
218
|
+
if (r.top > vh || r.width * r.height < vw * vh * 0.2) continue;
|
|
219
|
+
const bg = getComputedStyle(el).backgroundImage || '';
|
|
220
|
+
if (bg.includes('gradient')) {
|
|
221
|
+
const purples = bg.match(/rgb\\((\\d+),\\s*(\\d+),\\s*(\\d+)/g) || [];
|
|
222
|
+
if (purples.some((c) => { const [r2, g2, b2] = c.slice(4).split(',').map(Number); return b2 > 150 && r2 > 80 && r2 < 200 && g2 < r2; })) {
|
|
223
|
+
out.push('purple/violet gradient on a hero-scale element');
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
for (const container of document.querySelectorAll('section, div')) {
|
|
229
|
+
const kids = [...container.children];
|
|
230
|
+
if (kids.length < 3 || kids.length > 4) continue;
|
|
231
|
+
const rects = kids.map((k) => k.getBoundingClientRect());
|
|
232
|
+
if (rects[0].width < 150 || rects[0].top > vh * 2) continue;
|
|
233
|
+
const sameSize = rects.every((r) => Math.abs(r.width - rects[0].width) < 4 && Math.abs(r.height - rects[0].height) < 24);
|
|
234
|
+
const cardish = kids.every((k) => k.querySelector('svg, img') && k.querySelector('h2,h3,h4') && k.querySelector('p'));
|
|
235
|
+
if (sameSize && cardish) { out.push('identical icon-card grid (' + kids.length + ' cards)'); break; }
|
|
236
|
+
}
|
|
237
|
+
let emojiBullets = 0;
|
|
238
|
+
for (const li of document.querySelectorAll('li')) {
|
|
239
|
+
if (/^[\\u{1F300}-\\u{1FAFF}\\u{2600}-\\u{27BF}]/u.test((li.textContent || '').trim())) emojiBullets++;
|
|
240
|
+
}
|
|
241
|
+
if (emojiBullets >= 3) out.push(emojiBullets + ' emoji-bulleted list items');
|
|
242
|
+
const rootStyle = getComputedStyle(document.documentElement);
|
|
243
|
+
if (rootStyle.getPropertyValue('--radius').trim() === '0.5rem' && rootStyle.getPropertyValue('--primary').trim() === '222.2 47.4% 11.2%') {
|
|
244
|
+
out.push('untouched shadcn default theme tokens');
|
|
245
|
+
}
|
|
246
|
+
return out.slice(0, 8);
|
|
247
|
+
})()`;
|
|
204
248
|
async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, audit = false) {
|
|
205
249
|
const { child, wsUrl, profileDir } = await launchChrome(chromePath);
|
|
206
250
|
const report = { consoleErrors: [], pageErrors: [], failedRequests: [] };
|
|
207
251
|
const shots = [];
|
|
208
252
|
let a11y = [];
|
|
253
|
+
let slop = [];
|
|
209
254
|
const requests = /* @__PURE__ */ new Map();
|
|
210
255
|
let connection = null;
|
|
211
256
|
try {
|
|
@@ -260,6 +305,15 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
260
305
|
if (Array.isArray(result?.value)) a11y = result.value.map(String);
|
|
261
306
|
} catch {
|
|
262
307
|
}
|
|
308
|
+
try {
|
|
309
|
+
const { result } = await connection.send(
|
|
310
|
+
"Runtime.evaluate",
|
|
311
|
+
{ expression: SLOP_AUDIT, returnByValue: true },
|
|
312
|
+
sessionId
|
|
313
|
+
);
|
|
314
|
+
if (Array.isArray(result?.value)) slop = result.value.map(String);
|
|
315
|
+
} catch {
|
|
316
|
+
}
|
|
263
317
|
}
|
|
264
318
|
for (const viewport of viewports) {
|
|
265
319
|
await connection.send(
|
|
@@ -283,7 +337,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
283
337
|
child.kill("SIGKILL");
|
|
284
338
|
setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
|
|
285
339
|
}
|
|
286
|
-
return { report, shots, a11y };
|
|
340
|
+
return { report, shots, a11y, slop };
|
|
287
341
|
}
|
|
288
342
|
|
|
289
343
|
export {
|
|
@@ -2,61 +2,19 @@
|
|
|
2
2
|
import {
|
|
3
3
|
findChrome,
|
|
4
4
|
screenshot
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-IMDRXXFU.js";
|
|
6
6
|
import {
|
|
7
7
|
cdpCapture,
|
|
8
8
|
hasWebSocket
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-QEENJGCL.js";
|
|
10
|
+
import {
|
|
11
|
+
ensureSquintIgnore
|
|
12
|
+
} from "./chunk-O2S6PAJE.js";
|
|
10
13
|
|
|
11
14
|
// src/preview/preview.ts
|
|
12
|
-
import fs2 from "fs";
|
|
13
|
-
import os from "os";
|
|
14
|
-
import path2 from "path";
|
|
15
|
-
|
|
16
|
-
// src/state/state.ts
|
|
17
15
|
import fs from "fs";
|
|
16
|
+
import os from "os";
|
|
18
17
|
import path from "path";
|
|
19
|
-
var IGNORED = ["preview/", "state.json", "variants/", "transcripts/"];
|
|
20
|
-
function ensureSquintIgnore(cwd) {
|
|
21
|
-
const dir = path.join(cwd, ".squint");
|
|
22
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
23
|
-
const file = path.join(dir, ".gitignore");
|
|
24
|
-
let existing = "";
|
|
25
|
-
try {
|
|
26
|
-
existing = fs.readFileSync(file, "utf8");
|
|
27
|
-
} catch {
|
|
28
|
-
}
|
|
29
|
-
const lines = existing.split("\n").filter((l) => l.trim().length > 0);
|
|
30
|
-
const missing = IGNORED.filter((entry) => !lines.includes(entry));
|
|
31
|
-
if (missing.length > 0) {
|
|
32
|
-
fs.writeFileSync(file, [...lines, ...missing].join("\n") + "\n");
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function stateFile(cwd) {
|
|
36
|
-
return path.join(cwd, ".squint", "state.json");
|
|
37
|
-
}
|
|
38
|
-
function loadState(cwd) {
|
|
39
|
-
try {
|
|
40
|
-
const raw = JSON.parse(fs.readFileSync(stateFile(cwd), "utf8"));
|
|
41
|
-
if (typeof raw?.engine === "string" && typeof raw?.sessionId === "string" && typeof raw?.at === "number") {
|
|
42
|
-
return raw;
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
}
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
function saveState(cwd, state) {
|
|
49
|
-
ensureSquintIgnore(cwd);
|
|
50
|
-
fs.writeFileSync(stateFile(cwd), JSON.stringify(state, null, 2) + "\n");
|
|
51
|
-
}
|
|
52
|
-
function clearState(cwd) {
|
|
53
|
-
try {
|
|
54
|
-
fs.rmSync(stateFile(cwd));
|
|
55
|
-
} catch {
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// src/preview/preview.ts
|
|
60
18
|
var VIEWPORTS = [
|
|
61
19
|
{ name: "mobile", width: 390, height: 844 },
|
|
62
20
|
{ name: "tablet", width: 768, height: 1024 },
|
|
@@ -65,7 +23,7 @@ var VIEWPORTS = [
|
|
|
65
23
|
function loadRoutes(cwd) {
|
|
66
24
|
let lines = [];
|
|
67
25
|
try {
|
|
68
|
-
lines =
|
|
26
|
+
lines = fs.readFileSync(path.join(cwd, ".squint", "routes"), "utf8").split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
69
27
|
} catch {
|
|
70
28
|
}
|
|
71
29
|
const routes = ["/", ...lines.filter((l) => l !== "/")];
|
|
@@ -76,8 +34,8 @@ function routeShotName(route) {
|
|
|
76
34
|
return clean.length > 0 ? clean : "root";
|
|
77
35
|
}
|
|
78
36
|
function previewDir(cwd) {
|
|
79
|
-
const dir =
|
|
80
|
-
|
|
37
|
+
const dir = path.join(cwd, ".squint", "preview");
|
|
38
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
81
39
|
ensureSquintIgnore(cwd);
|
|
82
40
|
return dir;
|
|
83
41
|
}
|
|
@@ -89,7 +47,7 @@ async function captureViewports(cwd, url) {
|
|
|
89
47
|
const base = url.replace(/\/+$/, "");
|
|
90
48
|
if (hasWebSocket()) {
|
|
91
49
|
try {
|
|
92
|
-
const { report, shots: shots2, a11y } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true);
|
|
50
|
+
const { report, shots: shots2, a11y, slop } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true);
|
|
93
51
|
const errors2 = [];
|
|
94
52
|
for (const route of routes.slice(1)) {
|
|
95
53
|
try {
|
|
@@ -104,14 +62,14 @@ async function captureViewports(cwd, url) {
|
|
|
104
62
|
errors2.push(`${route}: capture failed`);
|
|
105
63
|
}
|
|
106
64
|
}
|
|
107
|
-
return { shots: shots2, errors: errors2, runtime: report, a11y };
|
|
65
|
+
return { shots: shots2, errors: errors2, runtime: report, a11y, slop };
|
|
108
66
|
} catch {
|
|
109
67
|
}
|
|
110
68
|
}
|
|
111
69
|
const shots = [];
|
|
112
70
|
const errors = [];
|
|
113
71
|
for (const viewport of VIEWPORTS) {
|
|
114
|
-
const outPath =
|
|
72
|
+
const outPath = path.join(dir, `${viewport.name}.png`);
|
|
115
73
|
const result = await screenshot(chrome, url, outPath, {
|
|
116
74
|
width: viewport.width,
|
|
117
75
|
height: viewport.height
|
|
@@ -175,7 +133,7 @@ async function probeRuntime(url, cwd) {
|
|
|
175
133
|
async function comparePulse(previous, current) {
|
|
176
134
|
const chrome = findChrome();
|
|
177
135
|
if (!chrome || !hasWebSocket()) return null;
|
|
178
|
-
const { pixelDiffPct } = await import("./cdp-
|
|
136
|
+
const { pixelDiffPct } = await import("./cdp-4IZHB3W5.js");
|
|
179
137
|
return pixelDiffPct(chrome, previous, current);
|
|
180
138
|
}
|
|
181
139
|
function buildRuntimeFixPrompt(report) {
|
|
@@ -193,19 +151,26 @@ ${findings.join("\n")}
|
|
|
193
151
|
|
|
194
152
|
Fix these as part of the pass \u2014 they are objective defects, not style preferences.`;
|
|
195
153
|
}
|
|
196
|
-
function
|
|
154
|
+
function slopSection(findings) {
|
|
155
|
+
if (!findings || findings.length === 0) return "";
|
|
156
|
+
return `
|
|
157
|
+
|
|
158
|
+
## Distinctiveness debt (detected mechanically)
|
|
159
|
+
|
|
160
|
+
${findings.join("\n")}
|
|
161
|
+
|
|
162
|
+
These patterns make the page read as template output. Rework them within the committed design direction \u2014 this is style debt, not a defect list.`;
|
|
163
|
+
}
|
|
164
|
+
function buildReviewPrompt(shots, extra, runtime, a11y, slop) {
|
|
197
165
|
const list = shots.map((s) => `- ${s.name}: ${s.path}`).join("\n");
|
|
198
166
|
return `Screenshots of the running app were just captured:
|
|
199
167
|
|
|
200
168
|
${list}
|
|
201
169
|
|
|
202
|
-
Read each screenshot and review the rendered UI against the design standards you were given. Check: visual hierarchy and spacing rhythm, typography, color and contrast, alignment, empty-looking or broken regions, and whether the mobile capture shows horizontal overflow or cramped layout. List the concrete issues you can SEE (not hypothetical ones), ranked by visual impact${extra ? `, with special attention to: ${extra}` : ""}. Then fix them and verify the app still builds.${runtimeSection(runtime)}${a11ySection(a11y)}`;
|
|
170
|
+
Read each screenshot and review the rendered UI against the design standards you were given. Check: visual hierarchy and spacing rhythm, typography, color and contrast, alignment, empty-looking or broken regions, and whether the mobile capture shows horizontal overflow or cramped layout. List the concrete issues you can SEE (not hypothetical ones), ranked by visual impact${extra ? `, with special attention to: ${extra}` : ""}. Then fix them and verify the app still builds.${runtimeSection(runtime)}${a11ySection(a11y)}${slopSection(slop)}`;
|
|
203
171
|
}
|
|
204
172
|
|
|
205
173
|
export {
|
|
206
|
-
loadState,
|
|
207
|
-
saveState,
|
|
208
|
-
clearState,
|
|
209
174
|
VIEWPORTS,
|
|
210
175
|
loadRoutes,
|
|
211
176
|
routeShotName,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/vcs/sandbox.ts
|
|
4
|
+
import { execFileSync } from "child_process";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
function git(cwd, args) {
|
|
8
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
9
|
+
}
|
|
10
|
+
function sandboxDir(cwd) {
|
|
11
|
+
return path.join(cwd, ".squint", "sandbox");
|
|
12
|
+
}
|
|
13
|
+
function sandboxExists(cwd) {
|
|
14
|
+
return fs.existsSync(path.join(sandboxDir(cwd), ".git"));
|
|
15
|
+
}
|
|
16
|
+
function openSandbox(cwd) {
|
|
17
|
+
const dir = sandboxDir(cwd);
|
|
18
|
+
void import("./state-QOS7WCZO.js").then(({ ensureSquintIgnore }) => ensureSquintIgnore(cwd)).catch(() => {
|
|
19
|
+
});
|
|
20
|
+
if (sandboxExists(cwd)) return { dir, reused: true };
|
|
21
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
22
|
+
fs.mkdirSync(path.dirname(dir), { recursive: true });
|
|
23
|
+
git(cwd, ["worktree", "add", "--force", "--detach", dir, "HEAD"]);
|
|
24
|
+
const mainModules = path.join(cwd, "node_modules");
|
|
25
|
+
const sandboxModules = path.join(dir, "node_modules");
|
|
26
|
+
if (fs.existsSync(mainModules) && !fs.existsSync(sandboxModules)) {
|
|
27
|
+
fs.symlinkSync(mainModules, sandboxModules, "dir");
|
|
28
|
+
}
|
|
29
|
+
return { dir, reused: false };
|
|
30
|
+
}
|
|
31
|
+
function sandboxDiffStat(cwd) {
|
|
32
|
+
if (!sandboxExists(cwd)) return null;
|
|
33
|
+
const dir = sandboxDir(cwd);
|
|
34
|
+
try {
|
|
35
|
+
git(dir, ["add", "-A"]);
|
|
36
|
+
const stat = git(dir, ["diff", "--cached", "--shortstat", "HEAD"]);
|
|
37
|
+
return stat.length > 0 ? stat : null;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function sandboxFiles(cwd) {
|
|
43
|
+
if (!sandboxExists(cwd)) return [];
|
|
44
|
+
const dir = sandboxDir(cwd);
|
|
45
|
+
try {
|
|
46
|
+
git(dir, ["add", "-A"]);
|
|
47
|
+
const out = git(dir, ["diff", "--cached", "--name-status", "HEAD"]);
|
|
48
|
+
return out.length > 0 ? out.split("\n") : [];
|
|
49
|
+
} catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function applySandbox(cwd) {
|
|
54
|
+
if (!sandboxExists(cwd)) return { ok: false, detail: "no sandbox open" };
|
|
55
|
+
const dir = sandboxDir(cwd);
|
|
56
|
+
try {
|
|
57
|
+
git(dir, ["add", "-A"]);
|
|
58
|
+
const patch = git(dir, ["diff", "--binary", "--cached", "HEAD"]);
|
|
59
|
+
if (patch.length === 0) return { ok: false, detail: "sandbox has no changes" };
|
|
60
|
+
const patchFile = path.join(cwd, ".squint", "sandbox.patch");
|
|
61
|
+
fs.writeFileSync(patchFile, patch + "\n");
|
|
62
|
+
git(cwd, ["apply", "--whitespace=nowarn", patchFile]);
|
|
63
|
+
fs.rmSync(patchFile, { force: true });
|
|
64
|
+
return { ok: true };
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return { ok: false, detail: err instanceof Error ? err.message.split("\n")[0] : String(err) };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function discardSandbox(cwd) {
|
|
70
|
+
if (!sandboxExists(cwd)) return false;
|
|
71
|
+
const dir = sandboxDir(cwd);
|
|
72
|
+
try {
|
|
73
|
+
const link = path.join(dir, "node_modules");
|
|
74
|
+
if (fs.existsSync(link) && fs.lstatSync(link).isSymbolicLink()) fs.rmSync(link);
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
git(cwd, ["worktree", "remove", "--force", dir]);
|
|
79
|
+
} catch {
|
|
80
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
git(cwd, ["worktree", "prune"]);
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
sandboxDir,
|
|
91
|
+
sandboxExists,
|
|
92
|
+
openSandbox,
|
|
93
|
+
sandboxDiffStat,
|
|
94
|
+
sandboxFiles,
|
|
95
|
+
applySandbox,
|
|
96
|
+
discardSandbox
|
|
97
|
+
};
|