@aayambansal/squint 0.4.1 → 0.4.3
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 +6 -1
- package/dist/{cdp-UGLMW5R5.js → cdp-TPP2RGSR.js} +1 -1
- package/dist/{chunk-P3H4N2EN.js → chunk-K5QJMSJH.js} +3 -58
- package/dist/{chunk-SVKV77TO.js → chunk-O7NWGLS3.js} +2 -0
- package/dist/{chunk-F6JZSXEO.js → chunk-QCHBDP46.js} +1 -1
- package/dist/chunk-WAJXATCO.js +64 -0
- package/dist/{chunk-UTBTLCUO.js → chunk-WCLQZFOR.js} +16 -6
- package/dist/{chunk-7YKO5MMN.js → chunk-Y47X4ENN.js} +42 -1
- package/dist/{chunk-A64VVTPU.js → chunk-YGSF2TSO.js} +4 -2
- package/dist/{chunk-H5K55LXY.js → chunk-YT6K2YIS.js} +63 -11
- package/dist/cli.js +83 -23
- package/dist/{commands-DE5Q7VWY.js → commands-VDICJJT2.js} +1 -1
- package/dist/contextDoctor-M3R3PICP.js +73 -0
- package/dist/{families-2G5SLLY7.js → families-3ARYRBMH.js} +2 -1
- package/dist/{preview-6MML2NEG.js → preview-SKHIXVCE.js} +2 -2
- package/dist/rulepacks-JOA675OJ.js +135 -0
- package/dist/{shots-E2AWBDCN.js → shots-OKWOYF7F.js} +4 -3
- package/dist/{skills-ODOVNE6E.js → skills-GTTF4PNU.js} +1 -1
- package/dist/{variants-3ZSINJGX.js → variants-3IEP7DFY.js} +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -167,7 +167,12 @@ squint doctor --probe # run every engine end to end, verify auth act
|
|
|
167
167
|
pixel-compared with the last (drift as a number), load performance is tracked with
|
|
168
168
|
deltas (`perf: LCP 812ms (+420ms)`), hardcoded colors get pointed at the nearest
|
|
169
169
|
design token, and the mechanical anti-slop sweep flags generic-AI tells as
|
|
170
|
-
distinctiveness debt in `/review`.
|
|
170
|
+
distinctiveness debt in `/review`. The phantom-class check diffs every DOM class
|
|
171
|
+
against the compiled CSS — hallucinated utilities surface as named problems instead
|
|
172
|
+
of silently unstyled elements.
|
|
173
|
+
- **The design ledger**: `/decide` (plus chosen variants, rollbacks, accepted
|
|
174
|
+
sandboxes) appends to a committed `.squint/design-log.jsonl`; recent decisions ride
|
|
175
|
+
into every ask so they stop getting silently undone between sessions.
|
|
171
176
|
- **`/btw <question>`** asks about the codebase read-only without touching the main
|
|
172
177
|
thread's context. `.squint/locks` lists paths the engine must never touch; `/save`
|
|
173
178
|
exports the transcript as markdown.
|
|
@@ -1,60 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import path from "path";
|
|
6
|
-
var DEFAULT_BRIEF = `You are acting as a senior product designer and frontend engineer. Treat every change as production work, not a demo.
|
|
7
|
-
|
|
8
|
-
Direction before code:
|
|
9
|
-
- Commit to one specific visual direction derived from what this product is for, and state it in one sentence before implementing. If the direction could be guessed from the product category alone, sharpen it until it couldn't.
|
|
10
|
-
- Choose a color strategy deliberately: restrained (neutrals + one accent), committed (one dominant color owning 30\u201360% of the surface), or a small palette of 3\u20135 named roles. Never purple/violet gradients unless asked. Body text contrast stays at least 4.5:1 \u2014 no light gray body copy "for elegance".
|
|
11
|
-
- Typography: at most two families, paired on contrast (display serif + geometric sans, or sans + mono). Never Inter/Roboto/Open Sans/system defaults for display type. Pair weight extremes (300 against 700\u2013900); display sizes jump 3x over body, not 1.5x.
|
|
12
|
-
|
|
13
|
-
Tokens are the system:
|
|
14
|
-
- Define or extend design tokens (CSS variables / theme config) first, then compose the UI from them. Never scatter literal colors or one-off spacing values through components.
|
|
15
|
-
- One spacing rhythm on a 4/8px grid; one type scale with a ratio of at least 1.25 between steps.
|
|
16
|
-
|
|
17
|
-
Banned tells \u2014 these read instantly as machine-generated:
|
|
18
|
-
- Purple gradient on white; glassmorphism cards; cream/beige page background as a reflex "warmth" move
|
|
19
|
-
- Emoji as icons; identical icon-topped card grids; stat banner rows; numbered 01/02/03 section markers
|
|
20
|
-
- Tiny all-caps tracked eyebrow labels over every section; gradient text; colored left-border strips on cards
|
|
21
|
-
- Centered hero + badge + three feature cards; unmodified component-library defaults
|
|
22
|
-
If someone could look at the result and say "AI made that" without doubt, it has failed.
|
|
23
|
-
|
|
24
|
-
Craft details:
|
|
25
|
-
- Every interactive element gets hover, focus-visible, and active treatment; keyboard focus stays visible.
|
|
26
|
-
- Anything that loads data gets loading, empty, and error states.
|
|
27
|
-
- Motion: 150\u2013250ms ease-out; one orchestrated entrance with staggered reveals beats scattered micro-interactions; honor prefers-reduced-motion; never leave content invisible until a scroll observer fires.
|
|
28
|
-
- Body line length 65\u201375ch; text-wrap: balance on headings.
|
|
29
|
-
|
|
30
|
-
Engineering:
|
|
31
|
-
- Follow the repo's existing conventions and extend its patterns; new components in new files, small and focused; semantic HTML.
|
|
32
|
-
- Responsive from 360px up with no horizontal overflow \u2014 check intermediate widths, not just phone and desktop.
|
|
33
|
-
- Let errors surface instead of swallowing them in try/catch; log clearly so failures can be traced.
|
|
34
|
-
- Not done until the app builds cleanly, typechecks, and renders without console errors.`;
|
|
35
|
-
var FIRST_TURN_ADDENDUM = `This is the opening move on this task: establish the design foundation before building. State the visual direction, set up the tokens/theme first, then build components from them. The first render should feel like a designed product, not a scaffold \u2014 impressive on sight.`;
|
|
36
|
-
function loadBrief(cwd) {
|
|
37
|
-
const custom = path.join(cwd, ".squint", "brief.md");
|
|
38
|
-
try {
|
|
39
|
-
const text = fs.readFileSync(custom, "utf8").trim();
|
|
40
|
-
if (text.length > 0) return text;
|
|
41
|
-
} catch {
|
|
42
|
-
}
|
|
43
|
-
return DEFAULT_BRIEF;
|
|
44
|
-
}
|
|
45
|
-
function composePrompt(ask, opts) {
|
|
46
|
-
if (opts.noBrief) return ask;
|
|
47
|
-
const brief = loadBrief(opts.cwd);
|
|
48
|
-
const firstTurn = opts.firstTurn ?? true;
|
|
49
|
-
const addendum = firstTurn ? `
|
|
50
|
-
|
|
51
|
-
${FIRST_TURN_ADDENDUM}` : "";
|
|
52
|
-
return `${brief}${addendum}
|
|
53
|
-
|
|
54
|
-
## Task
|
|
55
|
-
|
|
56
|
-
${ask}`;
|
|
57
|
-
}
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_BRIEF
|
|
4
|
+
} from "./chunk-WAJXATCO.js";
|
|
58
5
|
|
|
59
6
|
// src/prompt/families.ts
|
|
60
7
|
var FAMILIES = [
|
|
@@ -174,8 +121,6 @@ ${coreStandards()}`;
|
|
|
174
121
|
}
|
|
175
122
|
|
|
176
123
|
export {
|
|
177
|
-
FIRST_TURN_ADDENDUM,
|
|
178
|
-
composePrompt,
|
|
179
124
|
FAMILIES,
|
|
180
125
|
getFamily,
|
|
181
126
|
renderFamilyBrief
|
|
@@ -26,6 +26,8 @@ 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" },
|
|
30
|
+
{ name: "context", group: "session", description: "what squint injects per ask, token-costed, with staleness warnings" },
|
|
29
31
|
{ name: "resume", group: "session", description: "pick up the previous session for this repo" },
|
|
30
32
|
{ name: "clear", group: "session", description: "new session (transcript, totals, persisted state)" },
|
|
31
33
|
{ name: "help", group: "session", description: "list commands" },
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/prompt/brief.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
var DEFAULT_BRIEF = `You are acting as a senior product designer and frontend engineer. Treat every change as production work, not a demo.
|
|
7
|
+
|
|
8
|
+
Direction before code:
|
|
9
|
+
- Commit to one specific visual direction derived from what this product is for, and state it in one sentence before implementing. If the direction could be guessed from the product category alone, sharpen it until it couldn't.
|
|
10
|
+
- Choose a color strategy deliberately: restrained (neutrals + one accent), committed (one dominant color owning 30\u201360% of the surface), or a small palette of 3\u20135 named roles. Never purple/violet gradients unless asked. Body text contrast stays at least 4.5:1 \u2014 no light gray body copy "for elegance".
|
|
11
|
+
- Typography: at most two families, paired on contrast (display serif + geometric sans, or sans + mono). Never Inter/Roboto/Open Sans/system defaults for display type. Pair weight extremes (300 against 700\u2013900); display sizes jump 3x over body, not 1.5x.
|
|
12
|
+
|
|
13
|
+
Tokens are the system:
|
|
14
|
+
- Define or extend design tokens (CSS variables / theme config) first, then compose the UI from them. Never scatter literal colors or one-off spacing values through components.
|
|
15
|
+
- One spacing rhythm on a 4/8px grid; one type scale with a ratio of at least 1.25 between steps.
|
|
16
|
+
|
|
17
|
+
Banned tells \u2014 these read instantly as machine-generated:
|
|
18
|
+
- Purple gradient on white; glassmorphism cards; cream/beige page background as a reflex "warmth" move
|
|
19
|
+
- Emoji as icons; identical icon-topped card grids; stat banner rows; numbered 01/02/03 section markers
|
|
20
|
+
- Tiny all-caps tracked eyebrow labels over every section; gradient text; colored left-border strips on cards
|
|
21
|
+
- Centered hero + badge + three feature cards; unmodified component-library defaults
|
|
22
|
+
If someone could look at the result and say "AI made that" without doubt, it has failed.
|
|
23
|
+
|
|
24
|
+
Craft details:
|
|
25
|
+
- Every interactive element gets hover, focus-visible, and active treatment; keyboard focus stays visible.
|
|
26
|
+
- Anything that loads data gets loading, empty, and error states.
|
|
27
|
+
- Motion: 150\u2013250ms ease-out; one orchestrated entrance with staggered reveals beats scattered micro-interactions; honor prefers-reduced-motion; never leave content invisible until a scroll observer fires.
|
|
28
|
+
- Body line length 65\u201375ch; text-wrap: balance on headings.
|
|
29
|
+
|
|
30
|
+
Engineering:
|
|
31
|
+
- Follow the repo's existing conventions and extend its patterns; new components in new files, small and focused; semantic HTML.
|
|
32
|
+
- Responsive from 360px up with no horizontal overflow \u2014 check intermediate widths, not just phone and desktop.
|
|
33
|
+
- Let errors surface instead of swallowing them in try/catch; log clearly so failures can be traced.
|
|
34
|
+
- Not done until the app builds cleanly, typechecks, and renders without console errors.`;
|
|
35
|
+
var FIRST_TURN_ADDENDUM = `This is the opening move on this task: establish the design foundation before building. State the visual direction, set up the tokens/theme first, then build components from them. The first render should feel like a designed product, not a scaffold \u2014 impressive on sight.`;
|
|
36
|
+
function loadBrief(cwd) {
|
|
37
|
+
const custom = path.join(cwd, ".squint", "brief.md");
|
|
38
|
+
try {
|
|
39
|
+
const text = fs.readFileSync(custom, "utf8").trim();
|
|
40
|
+
if (text.length > 0) return text;
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
return DEFAULT_BRIEF;
|
|
44
|
+
}
|
|
45
|
+
function composePrompt(ask, opts) {
|
|
46
|
+
if (opts.noBrief) return ask;
|
|
47
|
+
const brief = loadBrief(opts.cwd);
|
|
48
|
+
const firstTurn = opts.firstTurn ?? true;
|
|
49
|
+
const addendum = firstTurn ? `
|
|
50
|
+
|
|
51
|
+
${FIRST_TURN_ADDENDUM}` : "";
|
|
52
|
+
return `${brief}${addendum}
|
|
53
|
+
|
|
54
|
+
## Task
|
|
55
|
+
|
|
56
|
+
${ask}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
DEFAULT_BRIEF,
|
|
61
|
+
FIRST_TURN_ADDENDUM,
|
|
62
|
+
loadBrief,
|
|
63
|
+
composePrompt
|
|
64
|
+
};
|
|
@@ -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 {
|
|
@@ -329,6 +329,37 @@ var SLOP_AUDIT = `(() => {
|
|
|
329
329
|
}
|
|
330
330
|
return out.slice(0, 8);
|
|
331
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
|
+
})()`;
|
|
332
363
|
async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, audit = false) {
|
|
333
364
|
const { child, wsUrl, profileDir } = await launchChrome(chromePath);
|
|
334
365
|
const report = { consoleErrors: [], pageErrors: [], failedRequests: [] };
|
|
@@ -337,6 +368,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
337
368
|
let slop = [];
|
|
338
369
|
let perf = {};
|
|
339
370
|
let narration = [];
|
|
371
|
+
let phantoms = [];
|
|
340
372
|
const requests = /* @__PURE__ */ new Map();
|
|
341
373
|
let connection = null;
|
|
342
374
|
try {
|
|
@@ -409,6 +441,15 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
409
441
|
if (Array.isArray(result?.value)) slop = result.value.map(String);
|
|
410
442
|
} catch {
|
|
411
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
|
+
}
|
|
412
453
|
try {
|
|
413
454
|
await connection.send("Accessibility.enable", {}, sessionId);
|
|
414
455
|
const { nodes } = await connection.send("Accessibility.getFullAXTree", {}, sessionId);
|
|
@@ -473,7 +514,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
473
514
|
child.kill("SIGKILL");
|
|
474
515
|
setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
|
|
475
516
|
}
|
|
476
|
-
return { report, shots, a11y, slop, perf, narration };
|
|
517
|
+
return { report, shots, a11y, slop, perf, narration, phantoms };
|
|
477
518
|
}
|
|
478
519
|
|
|
479
520
|
export {
|
|
@@ -4,9 +4,11 @@ import {
|
|
|
4
4
|
} from "./chunk-VH7OOFQP.js";
|
|
5
5
|
import {
|
|
6
6
|
FAMILIES,
|
|
7
|
-
FIRST_TURN_ADDENDUM,
|
|
8
7
|
renderFamilyBrief
|
|
9
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-K5QJMSJH.js";
|
|
9
|
+
import {
|
|
10
|
+
FIRST_TURN_ADDENDUM
|
|
11
|
+
} from "./chunk-WAJXATCO.js";
|
|
10
12
|
|
|
11
13
|
// src/variants/variants.ts
|
|
12
14
|
import { execFileSync } from "child_process";
|
|
@@ -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,11 @@ ${parts.join("\n\n")}` : "",
|
|
|
124
171
|
}
|
|
125
172
|
|
|
126
173
|
export {
|
|
174
|
+
appendDecision,
|
|
175
|
+
loadDecisions,
|
|
176
|
+
decisionsSection,
|
|
177
|
+
loadComponentInventory,
|
|
178
|
+
inventorySection,
|
|
127
179
|
parseSkill,
|
|
128
180
|
loadSkills,
|
|
129
181
|
loadRules,
|
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-O7NWGLS3.js";
|
|
5
5
|
import {
|
|
6
6
|
applySandbox,
|
|
7
7
|
discardSandbox,
|
|
@@ -22,13 +22,13 @@ import {
|
|
|
22
22
|
buildFixPrompt,
|
|
23
23
|
detectDevCommand,
|
|
24
24
|
screenshotVariants
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-QCHBDP46.js";
|
|
26
26
|
import {
|
|
27
27
|
applyVariant,
|
|
28
28
|
cleanVariants,
|
|
29
29
|
listVariants,
|
|
30
30
|
runVariants
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-YGSF2TSO.js";
|
|
32
32
|
import {
|
|
33
33
|
buildGatePrompt,
|
|
34
34
|
detectFastGates,
|
|
@@ -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,13 +58,15 @@ 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-YT6K2YIS.js";
|
|
66
|
+
import "./chunk-K5QJMSJH.js";
|
|
65
67
|
import {
|
|
66
68
|
composePrompt
|
|
67
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-WAJXATCO.js";
|
|
68
70
|
|
|
69
71
|
// src/cli.tsx
|
|
70
72
|
import { Command } from "commander";
|
|
@@ -238,7 +240,7 @@ function registerEnv(program2) {
|
|
|
238
240
|
}
|
|
239
241
|
}
|
|
240
242
|
const { findChrome: findChrome2 } = await import("./chrome-SBV3H77F.js");
|
|
241
|
-
const { hasWebSocket } = await import("./cdp-
|
|
243
|
+
const { hasWebSocket } = await import("./cdp-TPP2RGSR.js");
|
|
242
244
|
const chrome = findChrome2();
|
|
243
245
|
console.log(
|
|
244
246
|
chrome ? `${pc2.green("\u2713")} Chrome ${pc2.dim(chrome)}` : `${pc2.yellow("\u25CB")} Chrome ${pc2.dim("\u2014 screenshots and runtime probing disabled")}`
|
|
@@ -262,7 +264,7 @@ import pc3 from "picocolors";
|
|
|
262
264
|
function registerProject(program2) {
|
|
263
265
|
const skillsCommand = program2.command("skills").description("Project knowledge injected into asks (.squint/rules.md + .squint/skills/)");
|
|
264
266
|
skillsCommand.command("list").description("Show always-on rules and trigger-matched skills").action(async () => {
|
|
265
|
-
const { loadRules, loadSkills } = await import("./skills-
|
|
267
|
+
const { loadRules, loadSkills } = await import("./skills-GTTF4PNU.js");
|
|
266
268
|
const cwd = process.cwd();
|
|
267
269
|
const rules = loadRules(cwd);
|
|
268
270
|
console.log(
|
|
@@ -304,7 +306,7 @@ function registerProject(program2) {
|
|
|
304
306
|
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) => {
|
|
305
307
|
const fs3 = await import("fs");
|
|
306
308
|
const nodePath = await import("path");
|
|
307
|
-
const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-
|
|
309
|
+
const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-3ARYRBMH.js");
|
|
308
310
|
if (!familyId) {
|
|
309
311
|
console.log(pc3.bold("Aesthetic families") + pc3.dim(" \u2014 squint brief <id>\n"));
|
|
310
312
|
for (const family2 of FAMILIES) {
|
|
@@ -379,7 +381,7 @@ function registerProject(program2) {
|
|
|
379
381
|
process.exitCode = 1;
|
|
380
382
|
return;
|
|
381
383
|
}
|
|
382
|
-
const { runVariants: runVariants2, cleanVariants: cleanVariants2 } = await import("./variants-
|
|
384
|
+
const { runVariants: runVariants2, cleanVariants: cleanVariants2 } = await import("./variants-3IEP7DFY.js");
|
|
383
385
|
const config = loadConfig(defaultPaths(cwd));
|
|
384
386
|
const engineId = resolveEngineId(config, options.engine);
|
|
385
387
|
const engine = getEngine(engineId);
|
|
@@ -397,7 +399,7 @@ function registerProject(program2) {
|
|
|
397
399
|
);
|
|
398
400
|
const succeeded = runs.filter((r) => r.result.ok);
|
|
399
401
|
if (options.shots && succeeded.length > 0) {
|
|
400
|
-
const { screenshotVariants: screenshotVariants2 } = await import("./shots-
|
|
402
|
+
const { screenshotVariants: screenshotVariants2 } = await import("./shots-OKWOYF7F.js");
|
|
401
403
|
console.log(pc3.dim("capturing screenshots\u2026"));
|
|
402
404
|
const shots = await screenshotVariants2(cwd, succeeded.map((r) => r.variant));
|
|
403
405
|
for (const shot of shots) {
|
|
@@ -412,7 +414,7 @@ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 `
|
|
|
412
414
|
}
|
|
413
415
|
);
|
|
414
416
|
variantsCommand.command("list").description("List generated variants").action(async () => {
|
|
415
|
-
const { listVariants: listVariants2 } = await import("./variants-
|
|
417
|
+
const { listVariants: listVariants2 } = await import("./variants-3IEP7DFY.js");
|
|
416
418
|
const ids = listVariants2(process.cwd());
|
|
417
419
|
if (ids.length === 0) {
|
|
418
420
|
console.log(pc3.dim('no variants \u2014 squint variants gen <n> "<ask>"'));
|
|
@@ -421,7 +423,7 @@ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 `
|
|
|
421
423
|
for (const id of ids) console.log(id);
|
|
422
424
|
});
|
|
423
425
|
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) => {
|
|
424
|
-
const { applyVariant: applyVariant2, cleanVariants: cleanVariants2 } = await import("./variants-
|
|
426
|
+
const { applyVariant: applyVariant2, cleanVariants: cleanVariants2 } = await import("./variants-3IEP7DFY.js");
|
|
425
427
|
const cwd = process.cwd();
|
|
426
428
|
const result = applyVariant2(cwd, id);
|
|
427
429
|
if (!result.ok) {
|
|
@@ -433,7 +435,7 @@ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 `
|
|
|
433
435
|
console.log(pc3.green(`\u2713 applied ${id} to the working tree`) + pc3.dim(" \u2014 review with git diff"));
|
|
434
436
|
});
|
|
435
437
|
variantsCommand.command("clean").description("Discard all variants").action(async () => {
|
|
436
|
-
const { cleanVariants: cleanVariants2 } = await import("./variants-
|
|
438
|
+
const { cleanVariants: cleanVariants2 } = await import("./variants-3IEP7DFY.js");
|
|
437
439
|
const count = cleanVariants2(process.cwd());
|
|
438
440
|
console.log(pc3.dim(`removed ${count} variant(s)`));
|
|
439
441
|
});
|
|
@@ -461,7 +463,7 @@ function registerQuality(program2) {
|
|
|
461
463
|
if (results.some((r) => !r.ok)) process.exitCode = 1;
|
|
462
464
|
});
|
|
463
465
|
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-
|
|
466
|
+
const { captureViewports: captureViewports2 } = await import("./preview-SKHIXVCE.js");
|
|
465
467
|
const result = await captureViewports2(process.cwd(), url);
|
|
466
468
|
if (!result) {
|
|
467
469
|
console.error(pc4.red("\u2717 no Chrome/Chromium found"));
|
|
@@ -823,7 +825,7 @@ ${question}`,
|
|
|
823
825
|
return;
|
|
824
826
|
}
|
|
825
827
|
await this.runTurn(
|
|
826
|
-
buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration),
|
|
828
|
+
buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms),
|
|
827
829
|
`\u{1F441} polish round ${round}/${rounds}`
|
|
828
830
|
);
|
|
829
831
|
}
|
|
@@ -1050,6 +1052,26 @@ ${driftSummary(drift)}`);
|
|
|
1050
1052
|
}
|
|
1051
1053
|
} catch {
|
|
1052
1054
|
}
|
|
1055
|
+
try {
|
|
1056
|
+
const { rulePackSummary, scanRulePacks } = await import("./rulepacks-JOA675OJ.js");
|
|
1057
|
+
const findings = scanRulePacks(this.execCwd(), checkpoint.snapshot.stashHash ?? "HEAD");
|
|
1058
|
+
const hard = findings.filter((f) => f.hard);
|
|
1059
|
+
if (hard.length > 0) {
|
|
1060
|
+
this.addProblem(
|
|
1061
|
+
"gates",
|
|
1062
|
+
`${hard.length} class(es)/idiom(s) from an older toolchain major
|
|
1063
|
+
${rulePackSummary(hard)}`,
|
|
1064
|
+
`This project's toolchain is newer than the patterns just written. Apply the exact renames below \u2014 do not downgrade dependencies or add compatibility configs:
|
|
1065
|
+
${rulePackSummary(hard)}`
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
const soft = findings.filter((f) => !f.hard);
|
|
1069
|
+
if (soft.length > 0) {
|
|
1070
|
+
this.push("status", `renamed-scale traps (verify intent):
|
|
1071
|
+
${rulePackSummary(soft)}`);
|
|
1072
|
+
}
|
|
1073
|
+
} catch {
|
|
1074
|
+
}
|
|
1053
1075
|
}
|
|
1054
1076
|
runHook(this.opts.cwd, "on-turn-end", {
|
|
1055
1077
|
cost: String(result.costUsd ?? 0),
|
|
@@ -1141,7 +1163,7 @@ ${errors.slice(-5).join("\n")}`);
|
|
|
1141
1163
|
const captureResult = await this.capture();
|
|
1142
1164
|
if (captureResult) {
|
|
1143
1165
|
await this.runTurn(
|
|
1144
|
-
buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration),
|
|
1166
|
+
buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms),
|
|
1145
1167
|
"\u{1F441} auto-review rendered UI"
|
|
1146
1168
|
);
|
|
1147
1169
|
}
|
|
@@ -1189,6 +1211,7 @@ ${errors.slice(-5).join("\n")}`);
|
|
|
1189
1211
|
const result = restoreSnapshot(this.opts.cwd, checkpoint.snapshot);
|
|
1190
1212
|
if (result.restored) {
|
|
1191
1213
|
const dropped = this.checkpoints.length - index;
|
|
1214
|
+
appendDecision(this.opts.cwd, { decision: `rejected and rolled back: "${checkpoint.label}"`, source: "restore" });
|
|
1192
1215
|
this.checkpoints = this.checkpoints.slice(0, index);
|
|
1193
1216
|
this.push(
|
|
1194
1217
|
"status",
|
|
@@ -1280,6 +1303,20 @@ ${errors.slice(-5).join("\n")}`);
|
|
|
1280
1303
|
this.push("status", "runtime clean \u2014 no console errors, exceptions, or failed requests");
|
|
1281
1304
|
}
|
|
1282
1305
|
}
|
|
1306
|
+
if (result.phantoms && result.phantoms.length > 0) {
|
|
1307
|
+
this.push("error", `phantom classes: ${result.phantoms.length} (in the DOM, absent from CSS)
|
|
1308
|
+
${result.phantoms.slice(0, 5).join("\n")}`);
|
|
1309
|
+
this.addProblem(
|
|
1310
|
+
"runtime",
|
|
1311
|
+
`${result.phantoms.length} phantom class(es) \u2014 elements silently unstyled`,
|
|
1312
|
+
`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:
|
|
1313
|
+
|
|
1314
|
+
${result.phantoms.join("\n")}
|
|
1315
|
+
|
|
1316
|
+
Fix the class names or define the styles, then confirm visually.`
|
|
1317
|
+
);
|
|
1318
|
+
this.push("status", "/fix sends open problems to the engine");
|
|
1319
|
+
}
|
|
1283
1320
|
if (result.slop && result.slop.length > 0) {
|
|
1284
1321
|
this.push("status", `distinctiveness: ${result.slop.length} tell(s)
|
|
1285
1322
|
${result.slop.join("\n")}`);
|
|
@@ -1395,6 +1432,27 @@ They are objective defects, not style preferences.`
|
|
|
1395
1432
|
}
|
|
1396
1433
|
break;
|
|
1397
1434
|
}
|
|
1435
|
+
case "context": {
|
|
1436
|
+
import("./contextDoctor-M3R3PICP.js").then(({ contextReport, formatContextReport }) => {
|
|
1437
|
+
this.push("status", formatContextReport(contextReport(this.execCwd())));
|
|
1438
|
+
}).catch((error) => {
|
|
1439
|
+
this.push("status", `context report failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1440
|
+
});
|
|
1441
|
+
break;
|
|
1442
|
+
}
|
|
1443
|
+
case "decide": {
|
|
1444
|
+
if (!arg) {
|
|
1445
|
+
this.push("status", "usage: /decide <the decision> \u2014 recorded in .squint/design-log.jsonl and injected into every future ask");
|
|
1446
|
+
break;
|
|
1447
|
+
}
|
|
1448
|
+
appendDecision(this.opts.cwd, {
|
|
1449
|
+
decision: arg,
|
|
1450
|
+
source: "decide",
|
|
1451
|
+
screenshot: this.lastPulse ? ".squint/preview/pulse.png" : void 0
|
|
1452
|
+
});
|
|
1453
|
+
this.push("status", `decision recorded: ${arg}`);
|
|
1454
|
+
break;
|
|
1455
|
+
}
|
|
1398
1456
|
case "find": {
|
|
1399
1457
|
if (!arg) {
|
|
1400
1458
|
this.push("status", "usage: /find <term> \u2014 searches this session and saved transcripts");
|
|
@@ -1491,8 +1549,8 @@ They are objective defects, not style preferences.`
|
|
|
1491
1549
|
this.push("error", "no Chrome/Chromium found for flows");
|
|
1492
1550
|
return;
|
|
1493
1551
|
}
|
|
1494
|
-
const { runFlow } = await import("./cdp-
|
|
1495
|
-
const { previewDir } = await import("./preview-
|
|
1552
|
+
const { runFlow } = await import("./cdp-TPP2RGSR.js");
|
|
1553
|
+
const { previewDir } = await import("./preview-SKHIXVCE.js");
|
|
1496
1554
|
this.push("status", `replaying ${flows.length} flow(s)\u2026`);
|
|
1497
1555
|
this.notify({ running: true, runStartedAt: Date.now() });
|
|
1498
1556
|
const failures = [];
|
|
@@ -1630,7 +1688,7 @@ They are objective defects, not style preferences.`
|
|
|
1630
1688
|
const result = await this.capture();
|
|
1631
1689
|
if (result) {
|
|
1632
1690
|
await this.runTurn(
|
|
1633
|
-
buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration),
|
|
1691
|
+
buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms),
|
|
1634
1692
|
`\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
|
|
1635
1693
|
);
|
|
1636
1694
|
}
|
|
@@ -1709,6 +1767,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
|
|
|
1709
1767
|
discardSandbox(this.opts.cwd);
|
|
1710
1768
|
this.notify({ sandbox: false });
|
|
1711
1769
|
this.resetDevServer();
|
|
1770
|
+
appendDecision(this.opts.cwd, { decision: "accepted the sandboxed session onto the real tree", source: "sandbox" });
|
|
1712
1771
|
this.push("status", "sandbox applied to the real tree \u2014 review with git diff");
|
|
1713
1772
|
} else {
|
|
1714
1773
|
this.push("error", applied.detail ?? "apply failed");
|
|
@@ -1750,6 +1809,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
|
|
|
1750
1809
|
const applied = applyVariant(this.opts.cwd, id);
|
|
1751
1810
|
if (applied.ok) {
|
|
1752
1811
|
cleanVariants(this.opts.cwd);
|
|
1812
|
+
appendDecision(this.opts.cwd, { decision: `chose the ${id} direction over the other variants`, source: "variant" });
|
|
1753
1813
|
this.push("status", `applied ${id} to the working tree \u2014 review with git diff`);
|
|
1754
1814
|
} else {
|
|
1755
1815
|
this.push("error", applied.detail ?? "apply failed");
|
|
@@ -1814,7 +1874,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
|
|
|
1814
1874
|
this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
|
|
1815
1875
|
break;
|
|
1816
1876
|
case "help": {
|
|
1817
|
-
void import("./commands-
|
|
1877
|
+
void import("./commands-VDICJJT2.js").then(({ commandHelp }) => this.push("status", commandHelp()));
|
|
1818
1878
|
break;
|
|
1819
1879
|
}
|
|
1820
1880
|
case "quit":
|
|
@@ -2433,7 +2493,7 @@ function registerTui(program2) {
|
|
|
2433
2493
|
}
|
|
2434
2494
|
|
|
2435
2495
|
// src/cli.tsx
|
|
2436
|
-
var VERSION = true ? "0.4.
|
|
2496
|
+
var VERSION = true ? "0.4.3" : "0.0.0-dev";
|
|
2437
2497
|
var program = new Command();
|
|
2438
2498
|
program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
|
|
2439
2499
|
registerRun(program);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
decisionsSection,
|
|
4
|
+
inventorySection,
|
|
5
|
+
loadComponentInventory,
|
|
6
|
+
loadDecisions,
|
|
7
|
+
loadLocks,
|
|
8
|
+
loadRules,
|
|
9
|
+
loadSkills
|
|
10
|
+
} from "./chunk-YT6K2YIS.js";
|
|
11
|
+
import {
|
|
12
|
+
loadBrief
|
|
13
|
+
} from "./chunk-WAJXATCO.js";
|
|
14
|
+
|
|
15
|
+
// src/quality/contextDoctor.ts
|
|
16
|
+
import fs from "fs";
|
|
17
|
+
import path from "path";
|
|
18
|
+
var tokens = (text) => Math.ceil(text.length / 4);
|
|
19
|
+
function contextReport(cwd) {
|
|
20
|
+
const lines = [];
|
|
21
|
+
const warnings = [];
|
|
22
|
+
const brief = loadBrief(cwd);
|
|
23
|
+
const custom = fs.existsSync(path.join(cwd, ".squint", "brief.md"));
|
|
24
|
+
lines.push({ source: custom ? "brief (.squint/brief.md)" : "brief (built-in)", tokens: tokens(brief), when: "first turn" });
|
|
25
|
+
if (tokens(brief) > 1500) warnings.push(`the brief is ${tokens(brief)} tokens \u2014 past ~1500 the engine starts skimming; distill it`);
|
|
26
|
+
const rules = loadRules(cwd);
|
|
27
|
+
if (rules) {
|
|
28
|
+
lines.push({ source: "rules (.squint/rules.md)", tokens: tokens(rules), when: "every ask" });
|
|
29
|
+
if (tokens(rules) > 800) warnings.push(`rules.md is ${tokens(rules)} tokens of always-on context \u2014 move situational parts into triggered skills`);
|
|
30
|
+
}
|
|
31
|
+
const decisions = decisionsSection(cwd);
|
|
32
|
+
if (decisions) {
|
|
33
|
+
lines.push({ source: `design ledger (${loadDecisions(cwd).length} recent decisions)`, tokens: tokens(decisions), when: "every ask" });
|
|
34
|
+
}
|
|
35
|
+
const inventory = loadComponentInventory(cwd);
|
|
36
|
+
if (inventory) {
|
|
37
|
+
lines.push({ source: "component inventory", tokens: tokens(inventorySection(inventory)), when: "every ask" });
|
|
38
|
+
}
|
|
39
|
+
const locks = loadLocks(cwd);
|
|
40
|
+
if (locks.length > 0) {
|
|
41
|
+
lines.push({ source: `locks (${locks.length} paths)`, tokens: tokens(locks.join("\n")) + 40, when: "every ask" });
|
|
42
|
+
for (const lock of locks) {
|
|
43
|
+
if (!fs.existsSync(path.join(cwd, lock))) warnings.push(`stale lock: ${lock} no longer exists \u2014 remove it from .squint/locks`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const skill of loadSkills(cwd)) {
|
|
47
|
+
lines.push({
|
|
48
|
+
source: `skill: ${skill.name}`,
|
|
49
|
+
tokens: tokens(skill.body),
|
|
50
|
+
when: `when the ask mentions ${skill.triggers.map((t) => `"${t}"`).join(", ")}`
|
|
51
|
+
});
|
|
52
|
+
const generic = skill.triggers.filter((t) => t.length <= 3);
|
|
53
|
+
if (generic.length > 0) {
|
|
54
|
+
warnings.push(`skill "${skill.name}" has trigger(s) ${generic.map((t) => `"${t}"`).join(", ")} short enough to match almost any ask \u2014 make them more specific`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const totalAlways = lines.filter((l) => l.when === "every ask").reduce((sum, l) => sum + l.tokens, 0);
|
|
58
|
+
if (totalAlways > 3e3) warnings.push(`~${totalAlways} tokens ride on every single ask \u2014 that is real money and real attention; trim the always-on set`);
|
|
59
|
+
return { lines, warnings, totalAlways };
|
|
60
|
+
}
|
|
61
|
+
function formatContextReport(report) {
|
|
62
|
+
const width = Math.max(...report.lines.map((l) => l.source.length), 10);
|
|
63
|
+
const rows = report.lines.map((l) => ` ${l.source.padEnd(width)} ~${String(l.tokens).padStart(5)} tok ${l.when}`);
|
|
64
|
+
const out = [`what squint injects (estimates):`, ...rows, ` ${"always-on total".padEnd(width)} ~${String(report.totalAlways).padStart(5)} tok every ask`];
|
|
65
|
+
if (report.warnings.length > 0) {
|
|
66
|
+
out.push("", "warnings:", ...report.warnings.map((w) => ` \u26A0 ${w}`));
|
|
67
|
+
}
|
|
68
|
+
return out.join("\n");
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
contextReport,
|
|
72
|
+
formatContextReport
|
|
73
|
+
};
|
|
@@ -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,
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/quality/rulepacks.ts
|
|
4
|
+
import { execFileSync } from "child_process";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
var TAILWIND_V4_RULES = [
|
|
8
|
+
{
|
|
9
|
+
re: /\bbg-gradient-to-(t|tr|r|br|b|bl|l|tl)\b/g,
|
|
10
|
+
hint: (m) => `${m} is v3 \u2014 v4 renamed it bg-linear-to-${m.split("-").pop()}`,
|
|
11
|
+
hard: true
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
re: /\bflex-(shrink|grow)(-\d+)?\b/g,
|
|
15
|
+
hint: (m) => `${m} is v3 \u2014 v4 uses ${m.replace("flex-", "")}`,
|
|
16
|
+
hard: true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
re: /\boverflow-ellipsis\b/g,
|
|
20
|
+
hint: () => "overflow-ellipsis is v3 \u2014 v4 uses text-ellipsis",
|
|
21
|
+
hard: true
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
re: /\bdecoration-(slice|clone)\b/g,
|
|
25
|
+
hint: (m) => `${m} is v3 \u2014 v4 uses box-${m}`,
|
|
26
|
+
hard: true
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
re: /\b(shadow|rounded|blur|drop-shadow)-sm\b/g,
|
|
30
|
+
hint: (m) => `${m} means one step larger in v4 (the scale shifted; v3 ${m} is now ${m.replace(/-sm$/, "-xs")}) \u2014 verify the intent`,
|
|
31
|
+
hard: false
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
re: /\boutline-none\b/g,
|
|
35
|
+
hint: () => "outline-none changed meaning in v4 (now outline-style: none; the old invisible-but-accessible reset is outline-hidden) \u2014 verify the intent",
|
|
36
|
+
hard: false
|
|
37
|
+
}
|
|
38
|
+
];
|
|
39
|
+
var VITE_RULES = [
|
|
40
|
+
{
|
|
41
|
+
re: /\bsplitVendorChunkPlugin\b/g,
|
|
42
|
+
hint: () => "splitVendorChunkPlugin was removed \u2014 use build.rollupOptions.output.manualChunks",
|
|
43
|
+
hard: true
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
re: /\boptimizeDeps:\s*\{[^}]*esbuildOptions\b|\besbuildOptions\b/g,
|
|
47
|
+
hint: () => "optimizeDeps.esbuildOptions is esbuild-era \u2014 Rolldown Vite uses optimizeDeps.rollupOptions",
|
|
48
|
+
hard: false
|
|
49
|
+
}
|
|
50
|
+
];
|
|
51
|
+
function readPackageJson(cwd) {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function depMajor(pkg, name) {
|
|
59
|
+
if (!pkg) return null;
|
|
60
|
+
for (const key of ["dependencies", "devDependencies"]) {
|
|
61
|
+
const deps = pkg[key];
|
|
62
|
+
const range = deps?.[name];
|
|
63
|
+
const major = range?.match(/(\d+)/)?.[1];
|
|
64
|
+
if (major) return Number.parseInt(major, 10);
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function detectTailwindMajor(cwd) {
|
|
69
|
+
const pkg = readPackageJson(cwd);
|
|
70
|
+
const major = depMajor(pkg, "tailwindcss");
|
|
71
|
+
if (major !== null) return major;
|
|
72
|
+
if (depMajor(pkg, "@tailwindcss/vite") !== null) return 4;
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function detectViteMajor(cwd) {
|
|
76
|
+
return depMajor(readPackageJson(cwd), "vite");
|
|
77
|
+
}
|
|
78
|
+
function scanRulePacks(cwd, source) {
|
|
79
|
+
const packs = [];
|
|
80
|
+
const tailwind = detectTailwindMajor(cwd);
|
|
81
|
+
if (tailwind !== null && tailwind >= 4) packs.push({ rules: TAILWIND_V4_RULES });
|
|
82
|
+
const vite = detectViteMajor(cwd);
|
|
83
|
+
if (vite !== null && vite >= 6) packs.push({ rules: VITE_RULES, files: /vite\.config\.[cm]?[jt]s$/ });
|
|
84
|
+
if (packs.length === 0) return [];
|
|
85
|
+
let diff;
|
|
86
|
+
try {
|
|
87
|
+
diff = execFileSync("git", ["diff", "-U0", source, "--", "*.tsx", "*.jsx", "*.ts", "*.js", "*.html", "*.css", "*.vue", "*.svelte"], {
|
|
88
|
+
cwd,
|
|
89
|
+
encoding: "utf8",
|
|
90
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
91
|
+
});
|
|
92
|
+
} catch {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
const findings = [];
|
|
96
|
+
const seen = /* @__PURE__ */ new Set();
|
|
97
|
+
let currentFile = "";
|
|
98
|
+
for (const line of diff.split("\n")) {
|
|
99
|
+
if (line.startsWith("+++ b/")) {
|
|
100
|
+
currentFile = line.slice(6);
|
|
101
|
+
if (tailwind !== null && tailwind >= 4 && /(^|\/)tailwind\.config\.[cm]?[jt]s$/.test(currentFile)) {
|
|
102
|
+
findings.push({
|
|
103
|
+
file: currentFile,
|
|
104
|
+
match: "tailwind.config",
|
|
105
|
+
hint: "Tailwind v4 is CSS-first \u2014 configure with @theme in your CSS, not a tailwind.config file",
|
|
106
|
+
hard: true
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!line.startsWith("+") || line.startsWith("+++")) continue;
|
|
112
|
+
for (const pack of packs) {
|
|
113
|
+
if (pack.files && !pack.files.test(currentFile)) continue;
|
|
114
|
+
for (const rule of pack.rules) {
|
|
115
|
+
for (const match of line.matchAll(rule.re)) {
|
|
116
|
+
const key = `${currentFile}:${match[0]}`;
|
|
117
|
+
if (seen.has(key)) continue;
|
|
118
|
+
seen.add(key);
|
|
119
|
+
findings.push({ file: currentFile, match: match[0], hint: rule.hint(match[0]), hard: rule.hard });
|
|
120
|
+
if (findings.length >= 12) return findings;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return findings;
|
|
126
|
+
}
|
|
127
|
+
function rulePackSummary(findings) {
|
|
128
|
+
return findings.map((f) => ` ${f.file}: ${f.hint}`).join("\n");
|
|
129
|
+
}
|
|
130
|
+
export {
|
|
131
|
+
detectTailwindMajor,
|
|
132
|
+
detectViteMajor,
|
|
133
|
+
rulePackSummary,
|
|
134
|
+
scanRulePacks
|
|
135
|
+
};
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
screenshotVariants
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-QCHBDP46.js";
|
|
5
|
+
import "./chunk-YGSF2TSO.js";
|
|
6
6
|
import "./chunk-VH7OOFQP.js";
|
|
7
7
|
import "./chunk-IMDRXXFU.js";
|
|
8
8
|
import "./chunk-KVYGPLWW.js";
|
|
9
|
-
import "./chunk-
|
|
9
|
+
import "./chunk-K5QJMSJH.js";
|
|
10
|
+
import "./chunk-WAJXATCO.js";
|
|
10
11
|
export {
|
|
11
12
|
screenshotVariants
|
|
12
13
|
};
|
|
@@ -9,10 +9,11 @@ import {
|
|
|
9
9
|
runVariants,
|
|
10
10
|
variantPrompt,
|
|
11
11
|
variantsRoot
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-YGSF2TSO.js";
|
|
13
13
|
import "./chunk-VH7OOFQP.js";
|
|
14
14
|
import "./chunk-KVYGPLWW.js";
|
|
15
|
-
import "./chunk-
|
|
15
|
+
import "./chunk-K5QJMSJH.js";
|
|
16
|
+
import "./chunk-WAJXATCO.js";
|
|
16
17
|
export {
|
|
17
18
|
MAX_VARIANTS,
|
|
18
19
|
applyVariant,
|
package/package.json
CHANGED