@aayambansal/squint 0.4.0 → 0.4.2
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 -1
- package/dist/{cdp-VRGCGN5R.js → cdp-TPP2RGSR.js} +1 -1
- package/dist/{chunk-H5K55LXY.js → chunk-KGZILC3S.js} +59 -11
- package/dist/{chunk-SVKV77TO.js → chunk-S3WV5C3X.js} +1 -0
- package/dist/{chunk-2EJWUBUP.js → chunk-WCLQZFOR.js} +16 -6
- package/dist/{chunk-6FR4TRRQ.js → chunk-Y47X4ENN.js} +47 -2
- package/dist/cli.js +47 -16
- package/dist/{commands-DE5Q7VWY.js → commands-B6I6C5LP.js} +1 -1
- package/dist/{flows-V5ZLVVSQ.js → flows-6LK7NGWS.js} +39 -0
- package/dist/{init-LSYB32NS.js → init-YB2IMA4N.js} +6 -0
- package/dist/{preview-X7EWTRCZ.js → preview-SKHIXVCE.js} +2 -2
- package/dist/{skills-ODOVNE6E.js → skills-U2J7Z3B2.js} +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -148,7 +148,10 @@ squint doctor --probe # run every engine end to end, verify auth act
|
|
|
148
148
|
order; `/queue clear` drops them. `Esc` interrupts the current turn.
|
|
149
149
|
- **Editing**: a real line editor — arrows move, `alt+←/→` jump words, `ctrl+a/e/k/u/w`,
|
|
150
150
|
`↑/↓` history. `ctrl+c` twice exits with a session summary.
|
|
151
|
-
- **
|
|
151
|
+
- **Flows**: declare user journeys as six readable lines in `.squint/flows/`; `/flows`
|
|
152
|
+
replays them headlessly and failing steps join the fix loop. `/score` snapshots quality
|
|
153
|
+
deterministically.
|
|
154
|
+
- **Problems**: findings from gates, the dev server, the runtime probe, a11y sweeps, and flows
|
|
152
155
|
collect into a list — `/problems` shows it, `/fix` sends everything as one turn,
|
|
153
156
|
`/fix <n>` targets one. The footer counts what's open.
|
|
154
157
|
- **Sandbox**: `/sandbox on` and asks accumulate in a shadow worktree — the dev server,
|
|
@@ -1,25 +1,70 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/prompt/skills.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import fs3 from "fs";
|
|
5
|
+
import path3 from "path";
|
|
6
6
|
|
|
7
|
-
// src/
|
|
7
|
+
// src/session/designLog.ts
|
|
8
8
|
import fs from "fs";
|
|
9
9
|
import path from "path";
|
|
10
|
+
function logFile(cwd) {
|
|
11
|
+
return path.join(cwd, ".squint", "design-log.jsonl");
|
|
12
|
+
}
|
|
13
|
+
function appendDecision(cwd, entry) {
|
|
14
|
+
try {
|
|
15
|
+
fs.mkdirSync(path.join(cwd, ".squint"), { recursive: true });
|
|
16
|
+
const record = { ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry };
|
|
17
|
+
fs.appendFileSync(logFile(cwd), JSON.stringify(record) + "\n");
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function loadDecisions(cwd, limit = 8) {
|
|
22
|
+
try {
|
|
23
|
+
const lines = fs.readFileSync(logFile(cwd), "utf8").trim().split("\n");
|
|
24
|
+
const decisions = [];
|
|
25
|
+
for (const line of lines.slice(-limit * 2)) {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(line);
|
|
28
|
+
if (typeof parsed?.decision === "string" && typeof parsed?.ts === "string") decisions.push(parsed);
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return decisions.slice(-limit);
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function decisionsSection(cwd) {
|
|
38
|
+
const decisions = loadDecisions(cwd);
|
|
39
|
+
if (decisions.length === 0) return "";
|
|
40
|
+
const lines = decisions.map((d) => {
|
|
41
|
+
const days = Math.floor((Date.now() - Date.parse(d.ts)) / 864e5);
|
|
42
|
+
const age = days <= 0 ? "today" : `${days}d ago`;
|
|
43
|
+
return `- ${d.decision} (${d.source}, ${age}${d.screenshot ? `, evidence: ${d.screenshot}` : ""})`;
|
|
44
|
+
});
|
|
45
|
+
return `## Design decisions on record
|
|
46
|
+
|
|
47
|
+
${lines.join("\n")}
|
|
48
|
+
|
|
49
|
+
These were decided deliberately. Do not silently undo them; if a task genuinely requires reversing one, say so explicitly first.`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/prompt/registry.ts
|
|
53
|
+
import fs2 from "fs";
|
|
54
|
+
import path2 from "path";
|
|
10
55
|
function loadComponentInventory(cwd) {
|
|
11
56
|
let config;
|
|
12
57
|
try {
|
|
13
|
-
config = JSON.parse(
|
|
58
|
+
config = JSON.parse(fs2.readFileSync(path2.join(cwd, "components.json"), "utf8"));
|
|
14
59
|
} catch {
|
|
15
60
|
return null;
|
|
16
61
|
}
|
|
17
62
|
const alias = config.aliases?.ui ?? (config.aliases?.components ? `${config.aliases.components}/ui` : "@/components/ui");
|
|
18
63
|
const relative = alias.replace(/^@\//, "src/").replace(/^~\//, "");
|
|
19
|
-
const uiDir =
|
|
64
|
+
const uiDir = path2.join(cwd, relative);
|
|
20
65
|
let entries;
|
|
21
66
|
try {
|
|
22
|
-
entries =
|
|
67
|
+
entries = fs2.readdirSync(uiDir);
|
|
23
68
|
} catch {
|
|
24
69
|
return null;
|
|
25
70
|
}
|
|
@@ -55,17 +100,17 @@ function parseSkill(name, raw) {
|
|
|
55
100
|
return { name, triggers, body };
|
|
56
101
|
}
|
|
57
102
|
function loadSkills(cwd) {
|
|
58
|
-
const dir =
|
|
103
|
+
const dir = path3.join(cwd, ".squint", "skills");
|
|
59
104
|
let entries;
|
|
60
105
|
try {
|
|
61
|
-
entries =
|
|
106
|
+
entries = fs3.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
62
107
|
} catch {
|
|
63
108
|
return [];
|
|
64
109
|
}
|
|
65
110
|
const skills = [];
|
|
66
111
|
for (const entry of entries.sort()) {
|
|
67
112
|
try {
|
|
68
|
-
const skill = parseSkill(entry.replace(/\.md$/, ""),
|
|
113
|
+
const skill = parseSkill(entry.replace(/\.md$/, ""), fs3.readFileSync(path3.join(dir, entry), "utf8"));
|
|
69
114
|
if (skill) skills.push(skill);
|
|
70
115
|
} catch {
|
|
71
116
|
}
|
|
@@ -74,7 +119,7 @@ function loadSkills(cwd) {
|
|
|
74
119
|
}
|
|
75
120
|
function loadRules(cwd) {
|
|
76
121
|
try {
|
|
77
|
-
const text =
|
|
122
|
+
const text = fs3.readFileSync(path3.join(cwd, ".squint", "rules.md"), "utf8").trim();
|
|
78
123
|
return text.length > 0 ? text : null;
|
|
79
124
|
} catch {
|
|
80
125
|
return null;
|
|
@@ -86,7 +131,7 @@ function matchSkills(skills, ask) {
|
|
|
86
131
|
}
|
|
87
132
|
function loadLocks(cwd) {
|
|
88
133
|
try {
|
|
89
|
-
return
|
|
134
|
+
return fs3.readFileSync(path3.join(cwd, ".squint", "locks"), "utf8").split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
90
135
|
} catch {
|
|
91
136
|
return [];
|
|
92
137
|
}
|
|
@@ -97,6 +142,8 @@ function enrich(cwd, ask) {
|
|
|
97
142
|
if (rules) parts.push(`## Project rules (always apply)
|
|
98
143
|
|
|
99
144
|
${rules}`);
|
|
145
|
+
const decisions = decisionsSection(cwd);
|
|
146
|
+
if (decisions) parts.push(decisions);
|
|
100
147
|
const inventory = loadComponentInventory(cwd);
|
|
101
148
|
if (inventory) parts.push(inventorySection(inventory));
|
|
102
149
|
const locks = loadLocks(cwd);
|
|
@@ -124,6 +171,7 @@ ${parts.join("\n\n")}` : "",
|
|
|
124
171
|
}
|
|
125
172
|
|
|
126
173
|
export {
|
|
174
|
+
appendDecision,
|
|
127
175
|
parseSkill,
|
|
128
176
|
loadSkills,
|
|
129
177
|
loadRules,
|
|
@@ -26,6 +26,7 @@ var COMMANDS = [
|
|
|
26
26
|
{ name: "copy", group: "session", description: "copy the last reply to the clipboard" },
|
|
27
27
|
{ name: "save", group: "session", description: "export the transcript to .squint/transcripts/" },
|
|
28
28
|
{ name: "find", args: "<term>", group: "session", description: "search this session and saved transcripts" },
|
|
29
|
+
{ name: "decide", args: "<text>", group: "session", description: "record a design decision; injected into every future ask" },
|
|
29
30
|
{ name: "resume", group: "session", description: "pick up the previous session for this repo" },
|
|
30
31
|
{ name: "clear", group: "session", description: "new session (transcript, totals, persisted state)" },
|
|
31
32
|
{ name: "help", group: "session", description: "list commands" },
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
cdpCapture,
|
|
11
11
|
hasWebSocket
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-Y47X4ENN.js";
|
|
13
13
|
|
|
14
14
|
// src/preview/preview.ts
|
|
15
15
|
import fs from "fs";
|
|
@@ -47,7 +47,7 @@ async function captureViewports(cwd, url) {
|
|
|
47
47
|
const base = url.replace(/\/+$/, "");
|
|
48
48
|
if (hasWebSocket()) {
|
|
49
49
|
try {
|
|
50
|
-
const { report, shots: shots2, a11y, slop, narration } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true);
|
|
50
|
+
const { report, shots: shots2, a11y, slop, narration, phantoms } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true);
|
|
51
51
|
const errors2 = [];
|
|
52
52
|
for (const route of routes.slice(1)) {
|
|
53
53
|
try {
|
|
@@ -62,7 +62,7 @@ async function captureViewports(cwd, url) {
|
|
|
62
62
|
errors2.push(`${route}: capture failed`);
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
-
return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration };
|
|
65
|
+
return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration, phantoms };
|
|
66
66
|
} catch {
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -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-TPP2RGSR.js");
|
|
137
137
|
return pixelDiffPct(chrome, previous, current);
|
|
138
138
|
}
|
|
139
139
|
function buildRuntimeFixPrompt(report) {
|
|
@@ -161,6 +161,16 @@ ${narration.join("\n")}
|
|
|
161
161
|
|
|
162
162
|
Judge this narration as an experience: does the reading order make sense? do names actually describe their targets? is anything announced as "(no accessible name)"? Fix real incoherence \u2014 this is how non-visual users meet the page.`;
|
|
163
163
|
}
|
|
164
|
+
function phantomSection(phantoms) {
|
|
165
|
+
if (!phantoms || phantoms.length === 0) return "";
|
|
166
|
+
return `
|
|
167
|
+
|
|
168
|
+
## Phantom classes (in the DOM, absent from the CSS)
|
|
169
|
+
|
|
170
|
+
${phantoms.join("\n")}
|
|
171
|
+
|
|
172
|
+
Each of these is an element silently unstyled \u2014 usually a misspelled or version-mismatched utility (Tailwind v3 spellings in a v4 project) or a concatenated class the scanner never compiled. Fix the class names or define the styles.`;
|
|
173
|
+
}
|
|
164
174
|
function slopSection(findings) {
|
|
165
175
|
if (!findings || findings.length === 0) return "";
|
|
166
176
|
return `
|
|
@@ -171,13 +181,13 @@ ${findings.join("\n")}
|
|
|
171
181
|
|
|
172
182
|
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.`;
|
|
173
183
|
}
|
|
174
|
-
function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration) {
|
|
184
|
+
function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms) {
|
|
175
185
|
const list = shots.map((s) => `- ${s.name}: ${s.path}`).join("\n");
|
|
176
186
|
return `Screenshots of the running app were just captured:
|
|
177
187
|
|
|
178
188
|
${list}
|
|
179
189
|
|
|
180
|
-
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)}${narrationSection(narration)}`;
|
|
190
|
+
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)}${narrationSection(narration)}${phantomSection(phantoms)}`;
|
|
181
191
|
}
|
|
182
192
|
|
|
183
193
|
export {
|
|
@@ -179,7 +179,7 @@ var describe = (value) => {
|
|
|
179
179
|
return String(value);
|
|
180
180
|
};
|
|
181
181
|
async function runFlow(chromePath, baseUrl, flow, outDir) {
|
|
182
|
-
const { stepExpression } = await import("./flows-
|
|
182
|
+
const { stepExpression } = await import("./flows-6LK7NGWS.js");
|
|
183
183
|
const { child, wsUrl, profileDir } = await launchChrome(chromePath);
|
|
184
184
|
const shots = [];
|
|
185
185
|
let connection = null;
|
|
@@ -203,6 +203,10 @@ async function runFlow(chromePath, baseUrl, flow, outDir) {
|
|
|
203
203
|
await new Promise((resolve) => setTimeout(resolve, 1800));
|
|
204
204
|
continue;
|
|
205
205
|
}
|
|
206
|
+
if (step.kind === "wait") {
|
|
207
|
+
await new Promise((resolve) => setTimeout(resolve, step.ms));
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
206
210
|
if (step.kind === "shot") {
|
|
207
211
|
const { data } = await connection.send("Page.captureScreenshot", { format: "png" }, sessionId);
|
|
208
212
|
const outPath = path.join(outDir, `flow-${flow.name}-${step.name}.png`);
|
|
@@ -325,6 +329,37 @@ var SLOP_AUDIT = `(() => {
|
|
|
325
329
|
}
|
|
326
330
|
return out.slice(0, 8);
|
|
327
331
|
})()`;
|
|
332
|
+
var PHANTOM_AUDIT = `(() => {
|
|
333
|
+
const defined = new Set();
|
|
334
|
+
for (const sheet of document.styleSheets) {
|
|
335
|
+
let rules; try { rules = sheet.cssRules; } catch { continue; }
|
|
336
|
+
const walk = (list) => {
|
|
337
|
+
for (const rule of list) {
|
|
338
|
+
if (rule.selectorText) {
|
|
339
|
+
for (const m of rule.selectorText.matchAll(/\\.((?:[\\w-]|\\\\.)+)/g)) {
|
|
340
|
+
defined.add(m[1].replace(/\\\\(.)/g, '$1'));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (rule.cssRules) walk(rule.cssRules);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
walk(rules);
|
|
347
|
+
}
|
|
348
|
+
if (defined.size < 10) return []; // no CSSOM visibility \u2014 stay silent
|
|
349
|
+
const seen = new Map();
|
|
350
|
+
for (const el of document.querySelectorAll('[class]')) {
|
|
351
|
+
for (const cls of el.classList) {
|
|
352
|
+
if (cls.length < 3) continue;
|
|
353
|
+
if (!defined.has(cls) && !seen.has(cls)) seen.set(cls, el.tagName.toLowerCase());
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const out = [];
|
|
357
|
+
for (const [cls, tag] of seen) {
|
|
358
|
+
out.push(cls + ' (on <' + tag + '>)');
|
|
359
|
+
if (out.length >= 12) break;
|
|
360
|
+
}
|
|
361
|
+
return out;
|
|
362
|
+
})()`;
|
|
328
363
|
async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, audit = false) {
|
|
329
364
|
const { child, wsUrl, profileDir } = await launchChrome(chromePath);
|
|
330
365
|
const report = { consoleErrors: [], pageErrors: [], failedRequests: [] };
|
|
@@ -333,6 +368,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
333
368
|
let slop = [];
|
|
334
369
|
let perf = {};
|
|
335
370
|
let narration = [];
|
|
371
|
+
let phantoms = [];
|
|
336
372
|
const requests = /* @__PURE__ */ new Map();
|
|
337
373
|
let connection = null;
|
|
338
374
|
try {
|
|
@@ -405,6 +441,15 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
405
441
|
if (Array.isArray(result?.value)) slop = result.value.map(String);
|
|
406
442
|
} catch {
|
|
407
443
|
}
|
|
444
|
+
try {
|
|
445
|
+
const { result } = await connection.send(
|
|
446
|
+
"Runtime.evaluate",
|
|
447
|
+
{ expression: PHANTOM_AUDIT, returnByValue: true },
|
|
448
|
+
sessionId
|
|
449
|
+
);
|
|
450
|
+
if (Array.isArray(result?.value)) phantoms = result.value.map(String);
|
|
451
|
+
} catch {
|
|
452
|
+
}
|
|
408
453
|
try {
|
|
409
454
|
await connection.send("Accessibility.enable", {}, sessionId);
|
|
410
455
|
const { nodes } = await connection.send("Accessibility.getFullAXTree", {}, sessionId);
|
|
@@ -469,7 +514,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
469
514
|
child.kill("SIGKILL");
|
|
470
515
|
setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
|
|
471
516
|
}
|
|
472
|
-
return { report, shots, a11y, slop, perf, narration };
|
|
517
|
+
return { report, shots, a11y, slop, perf, narration, phantoms };
|
|
473
518
|
}
|
|
474
519
|
|
|
475
520
|
export {
|
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-S3WV5C3X.js";
|
|
5
5
|
import {
|
|
6
6
|
applySandbox,
|
|
7
7
|
discardSandbox,
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
comparePulse,
|
|
43
43
|
probeRuntime,
|
|
44
44
|
runtimeSummary
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-WCLQZFOR.js";
|
|
46
46
|
import {
|
|
47
47
|
clearState,
|
|
48
48
|
loadState,
|
|
@@ -58,10 +58,11 @@ import {
|
|
|
58
58
|
detectEngines,
|
|
59
59
|
getEngine
|
|
60
60
|
} from "./chunk-KVYGPLWW.js";
|
|
61
|
-
import "./chunk-
|
|
61
|
+
import "./chunk-Y47X4ENN.js";
|
|
62
62
|
import {
|
|
63
|
+
appendDecision,
|
|
63
64
|
enrich
|
|
64
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-KGZILC3S.js";
|
|
65
66
|
import {
|
|
66
67
|
composePrompt
|
|
67
68
|
} from "./chunk-P3H4N2EN.js";
|
|
@@ -238,7 +239,7 @@ function registerEnv(program2) {
|
|
|
238
239
|
}
|
|
239
240
|
}
|
|
240
241
|
const { findChrome: findChrome2 } = await import("./chrome-SBV3H77F.js");
|
|
241
|
-
const { hasWebSocket } = await import("./cdp-
|
|
242
|
+
const { hasWebSocket } = await import("./cdp-TPP2RGSR.js");
|
|
242
243
|
const chrome = findChrome2();
|
|
243
244
|
console.log(
|
|
244
245
|
chrome ? `${pc2.green("\u2713")} Chrome ${pc2.dim(chrome)}` : `${pc2.yellow("\u25CB")} Chrome ${pc2.dim("\u2014 screenshots and runtime probing disabled")}`
|
|
@@ -262,7 +263,7 @@ import pc3 from "picocolors";
|
|
|
262
263
|
function registerProject(program2) {
|
|
263
264
|
const skillsCommand = program2.command("skills").description("Project knowledge injected into asks (.squint/rules.md + .squint/skills/)");
|
|
264
265
|
skillsCommand.command("list").description("Show always-on rules and trigger-matched skills").action(async () => {
|
|
265
|
-
const { loadRules, loadSkills } = await import("./skills-
|
|
266
|
+
const { loadRules, loadSkills } = await import("./skills-U2J7Z3B2.js");
|
|
266
267
|
const cwd = process.cwd();
|
|
267
268
|
const rules = loadRules(cwd);
|
|
268
269
|
console.log(
|
|
@@ -461,7 +462,7 @@ function registerQuality(program2) {
|
|
|
461
462
|
if (results.some((r) => !r.ok)) process.exitCode = 1;
|
|
462
463
|
});
|
|
463
464
|
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) => {
|
|
464
|
-
const { captureViewports: captureViewports2 } = await import("./preview-
|
|
465
|
+
const { captureViewports: captureViewports2 } = await import("./preview-SKHIXVCE.js");
|
|
465
466
|
const result = await captureViewports2(process.cwd(), url);
|
|
466
467
|
if (!result) {
|
|
467
468
|
console.error(pc4.red("\u2717 no Chrome/Chromium found"));
|
|
@@ -554,7 +555,7 @@ function registerRun(program2) {
|
|
|
554
555
|
import pc6 from "picocolors";
|
|
555
556
|
function registerScaffold(program2) {
|
|
556
557
|
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) => {
|
|
557
|
-
const { installDependencies, writeTemplate } = await import("./init-
|
|
558
|
+
const { installDependencies, writeTemplate } = await import("./init-YB2IMA4N.js");
|
|
558
559
|
let result;
|
|
559
560
|
try {
|
|
560
561
|
result = writeTemplate(dir, { force: options.force });
|
|
@@ -823,7 +824,7 @@ ${question}`,
|
|
|
823
824
|
return;
|
|
824
825
|
}
|
|
825
826
|
await this.runTurn(
|
|
826
|
-
buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration),
|
|
827
|
+
buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms),
|
|
827
828
|
`\u{1F441} polish round ${round}/${rounds}`
|
|
828
829
|
);
|
|
829
830
|
}
|
|
@@ -1141,7 +1142,7 @@ ${errors.slice(-5).join("\n")}`);
|
|
|
1141
1142
|
const captureResult = await this.capture();
|
|
1142
1143
|
if (captureResult) {
|
|
1143
1144
|
await this.runTurn(
|
|
1144
|
-
buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration),
|
|
1145
|
+
buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms),
|
|
1145
1146
|
"\u{1F441} auto-review rendered UI"
|
|
1146
1147
|
);
|
|
1147
1148
|
}
|
|
@@ -1189,6 +1190,7 @@ ${errors.slice(-5).join("\n")}`);
|
|
|
1189
1190
|
const result = restoreSnapshot(this.opts.cwd, checkpoint.snapshot);
|
|
1190
1191
|
if (result.restored) {
|
|
1191
1192
|
const dropped = this.checkpoints.length - index;
|
|
1193
|
+
appendDecision(this.opts.cwd, { decision: `rejected and rolled back: "${checkpoint.label}"`, source: "restore" });
|
|
1192
1194
|
this.checkpoints = this.checkpoints.slice(0, index);
|
|
1193
1195
|
this.push(
|
|
1194
1196
|
"status",
|
|
@@ -1280,6 +1282,20 @@ ${errors.slice(-5).join("\n")}`);
|
|
|
1280
1282
|
this.push("status", "runtime clean \u2014 no console errors, exceptions, or failed requests");
|
|
1281
1283
|
}
|
|
1282
1284
|
}
|
|
1285
|
+
if (result.phantoms && result.phantoms.length > 0) {
|
|
1286
|
+
this.push("error", `phantom classes: ${result.phantoms.length} (in the DOM, absent from CSS)
|
|
1287
|
+
${result.phantoms.slice(0, 5).join("\n")}`);
|
|
1288
|
+
this.addProblem(
|
|
1289
|
+
"runtime",
|
|
1290
|
+
`${result.phantoms.length} phantom class(es) \u2014 elements silently unstyled`,
|
|
1291
|
+
`These class tokens appear in the DOM but match no CSS rule, so the elements are silently unstyled. Usually a misspelled or version-mismatched utility (e.g. Tailwind v3 spellings in a v4 project) or a concatenated class the scanner never compiled:
|
|
1292
|
+
|
|
1293
|
+
${result.phantoms.join("\n")}
|
|
1294
|
+
|
|
1295
|
+
Fix the class names or define the styles, then confirm visually.`
|
|
1296
|
+
);
|
|
1297
|
+
this.push("status", "/fix sends open problems to the engine");
|
|
1298
|
+
}
|
|
1283
1299
|
if (result.slop && result.slop.length > 0) {
|
|
1284
1300
|
this.push("status", `distinctiveness: ${result.slop.length} tell(s)
|
|
1285
1301
|
${result.slop.join("\n")}`);
|
|
@@ -1395,6 +1411,19 @@ They are objective defects, not style preferences.`
|
|
|
1395
1411
|
}
|
|
1396
1412
|
break;
|
|
1397
1413
|
}
|
|
1414
|
+
case "decide": {
|
|
1415
|
+
if (!arg) {
|
|
1416
|
+
this.push("status", "usage: /decide <the decision> \u2014 recorded in .squint/design-log.jsonl and injected into every future ask");
|
|
1417
|
+
break;
|
|
1418
|
+
}
|
|
1419
|
+
appendDecision(this.opts.cwd, {
|
|
1420
|
+
decision: arg,
|
|
1421
|
+
source: "decide",
|
|
1422
|
+
screenshot: this.lastPulse ? ".squint/preview/pulse.png" : void 0
|
|
1423
|
+
});
|
|
1424
|
+
this.push("status", `decision recorded: ${arg}`);
|
|
1425
|
+
break;
|
|
1426
|
+
}
|
|
1398
1427
|
case "find": {
|
|
1399
1428
|
if (!arg) {
|
|
1400
1429
|
this.push("status", "usage: /find <term> \u2014 searches this session and saved transcripts");
|
|
@@ -1480,7 +1509,7 @@ They are objective defects, not style preferences.`
|
|
|
1480
1509
|
break;
|
|
1481
1510
|
}
|
|
1482
1511
|
void (async () => {
|
|
1483
|
-
const { loadFlows } = await import("./flows-
|
|
1512
|
+
const { loadFlows } = await import("./flows-6LK7NGWS.js");
|
|
1484
1513
|
const flows = loadFlows(this.opts.cwd);
|
|
1485
1514
|
if (flows.length === 0) {
|
|
1486
1515
|
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");
|
|
@@ -1491,8 +1520,8 @@ They are objective defects, not style preferences.`
|
|
|
1491
1520
|
this.push("error", "no Chrome/Chromium found for flows");
|
|
1492
1521
|
return;
|
|
1493
1522
|
}
|
|
1494
|
-
const { runFlow } = await import("./cdp-
|
|
1495
|
-
const { previewDir } = await import("./preview-
|
|
1523
|
+
const { runFlow } = await import("./cdp-TPP2RGSR.js");
|
|
1524
|
+
const { previewDir } = await import("./preview-SKHIXVCE.js");
|
|
1496
1525
|
this.push("status", `replaying ${flows.length} flow(s)\u2026`);
|
|
1497
1526
|
this.notify({ running: true, runStartedAt: Date.now() });
|
|
1498
1527
|
const failures = [];
|
|
@@ -1630,7 +1659,7 @@ They are objective defects, not style preferences.`
|
|
|
1630
1659
|
const result = await this.capture();
|
|
1631
1660
|
if (result) {
|
|
1632
1661
|
await this.runTurn(
|
|
1633
|
-
buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration),
|
|
1662
|
+
buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms),
|
|
1634
1663
|
`\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
|
|
1635
1664
|
);
|
|
1636
1665
|
}
|
|
@@ -1709,6 +1738,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
|
|
|
1709
1738
|
discardSandbox(this.opts.cwd);
|
|
1710
1739
|
this.notify({ sandbox: false });
|
|
1711
1740
|
this.resetDevServer();
|
|
1741
|
+
appendDecision(this.opts.cwd, { decision: "accepted the sandboxed session onto the real tree", source: "sandbox" });
|
|
1712
1742
|
this.push("status", "sandbox applied to the real tree \u2014 review with git diff");
|
|
1713
1743
|
} else {
|
|
1714
1744
|
this.push("error", applied.detail ?? "apply failed");
|
|
@@ -1750,6 +1780,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
|
|
|
1750
1780
|
const applied = applyVariant(this.opts.cwd, id);
|
|
1751
1781
|
if (applied.ok) {
|
|
1752
1782
|
cleanVariants(this.opts.cwd);
|
|
1783
|
+
appendDecision(this.opts.cwd, { decision: `chose the ${id} direction over the other variants`, source: "variant" });
|
|
1753
1784
|
this.push("status", `applied ${id} to the working tree \u2014 review with git diff`);
|
|
1754
1785
|
} else {
|
|
1755
1786
|
this.push("error", applied.detail ?? "apply failed");
|
|
@@ -1814,7 +1845,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
|
|
|
1814
1845
|
this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
|
|
1815
1846
|
break;
|
|
1816
1847
|
case "help": {
|
|
1817
|
-
void import("./commands-
|
|
1848
|
+
void import("./commands-B6I6C5LP.js").then(({ commandHelp }) => this.push("status", commandHelp()));
|
|
1818
1849
|
break;
|
|
1819
1850
|
}
|
|
1820
1851
|
case "quit":
|
|
@@ -2433,7 +2464,7 @@ function registerTui(program2) {
|
|
|
2433
2464
|
}
|
|
2434
2465
|
|
|
2435
2466
|
// src/cli.tsx
|
|
2436
|
-
var VERSION = true ? "0.4.
|
|
2467
|
+
var VERSION = true ? "0.4.2" : "0.0.0-dev";
|
|
2437
2468
|
var program = new Command();
|
|
2438
2469
|
program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
|
|
2439
2470
|
registerRun(program);
|
|
@@ -32,6 +32,18 @@ function parseFlow(name, text) {
|
|
|
32
32
|
case "shot":
|
|
33
33
|
steps.push({ kind: "shot", name: arg.replace(/[^a-zA-Z0-9-]/g, "-") });
|
|
34
34
|
break;
|
|
35
|
+
case "hover":
|
|
36
|
+
steps.push({ kind: "hover", target: arg });
|
|
37
|
+
break;
|
|
38
|
+
case "scroll":
|
|
39
|
+
steps.push({ kind: "scroll", target: arg || "bottom" });
|
|
40
|
+
break;
|
|
41
|
+
case "wait": {
|
|
42
|
+
const ms = Number.parseInt(arg, 10);
|
|
43
|
+
if (!Number.isInteger(ms) || ms < 0 || ms > 1e4) return null;
|
|
44
|
+
steps.push({ kind: "wait", ms });
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
35
47
|
default:
|
|
36
48
|
return null;
|
|
37
49
|
}
|
|
@@ -96,8 +108,35 @@ function stepExpression(step) {
|
|
|
96
108
|
}
|
|
97
109
|
return { ok: true };
|
|
98
110
|
})()`;
|
|
111
|
+
case "hover":
|
|
112
|
+
return `(() => {
|
|
113
|
+
const target = ${JSON.stringify(step.target)};
|
|
114
|
+
let el = null;
|
|
115
|
+
try { el = document.querySelector(target); } catch {}
|
|
116
|
+
if (!el) {
|
|
117
|
+
const all = [...document.querySelectorAll('*')];
|
|
118
|
+
el = all.find((e) => e.children.length === 0 && (e.textContent || '').trim().toLowerCase().includes(target.toLowerCase()));
|
|
119
|
+
}
|
|
120
|
+
if (!el) return { ok: false, detail: 'no element matching ' + target };
|
|
121
|
+
for (const type of ['pointerover', 'mouseover', 'mouseenter']) {
|
|
122
|
+
el.dispatchEvent(new MouseEvent(type, { bubbles: type !== 'mouseenter' }));
|
|
123
|
+
}
|
|
124
|
+
return { ok: true };
|
|
125
|
+
})()`;
|
|
126
|
+
case "scroll":
|
|
127
|
+
return `(() => {
|
|
128
|
+
const target = ${JSON.stringify(step.target)};
|
|
129
|
+
if (target === 'bottom') { window.scrollTo(0, document.body.scrollHeight); return { ok: true }; }
|
|
130
|
+
if (target === 'top') { window.scrollTo(0, 0); return { ok: true }; }
|
|
131
|
+
let el = null;
|
|
132
|
+
try { el = document.querySelector(target); } catch {}
|
|
133
|
+
if (!el) return { ok: false, detail: 'no element matching ' + target };
|
|
134
|
+
el.scrollIntoView({ block: 'center' });
|
|
135
|
+
return { ok: true };
|
|
136
|
+
})()`;
|
|
99
137
|
case "goto":
|
|
100
138
|
case "shot":
|
|
139
|
+
case "wait":
|
|
101
140
|
return null;
|
|
102
141
|
}
|
|
103
142
|
}
|
|
@@ -139,6 +139,12 @@ body {
|
|
|
139
139
|
dist/
|
|
140
140
|
*.log
|
|
141
141
|
.DS_Store
|
|
142
|
+
`,
|
|
143
|
+
".squint/flows/home.flow": `# The starter journey: replayed headlessly by /flows after every change.
|
|
144
|
+
# Grow this file as the app grows \u2014 or ask the engine to.
|
|
145
|
+
goto /
|
|
146
|
+
expect Ready
|
|
147
|
+
shot home
|
|
142
148
|
`
|
|
143
149
|
};
|
|
144
150
|
}
|
|
@@ -10,11 +10,11 @@ import {
|
|
|
10
10
|
probeRuntime,
|
|
11
11
|
routeShotName,
|
|
12
12
|
runtimeSummary
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-WCLQZFOR.js";
|
|
14
14
|
import "./chunk-O2S6PAJE.js";
|
|
15
15
|
import "./chunk-IMDRXXFU.js";
|
|
16
16
|
import "./chunk-KVYGPLWW.js";
|
|
17
|
-
import "./chunk-
|
|
17
|
+
import "./chunk-Y47X4ENN.js";
|
|
18
18
|
export {
|
|
19
19
|
VIEWPORTS,
|
|
20
20
|
buildReviewPrompt,
|
package/package.json
CHANGED