@aayambansal/squint 0.4.2 → 0.4.4
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 +10 -1
- package/dist/{cdp-TPP2RGSR.js → cdp-C6MOKVEN.js} +1 -1
- package/dist/{chunk-Y47X4ENN.js → chunk-JFFNQIBU.js} +51 -1
- package/dist/{chunk-P3H4N2EN.js → chunk-K5QJMSJH.js} +3 -58
- package/dist/{chunk-S3WV5C3X.js → chunk-O7NWGLS3.js} +1 -0
- package/dist/{chunk-F6JZSXEO.js → chunk-QCHBDP46.js} +1 -1
- package/dist/chunk-WAJXATCO.js +64 -0
- package/dist/{chunk-A64VVTPU.js → chunk-YGSF2TSO.js} +4 -2
- package/dist/{chunk-KGZILC3S.js → chunk-YT6K2YIS.js} +4 -0
- package/dist/{chunk-WCLQZFOR.js → chunk-ZUEGDTAM.js} +16 -6
- package/dist/cli.js +82 -23
- package/dist/{commands-B6I6C5LP.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/nextMcp-42Y63M7W.js +97 -0
- package/dist/{preview-SKHIXVCE.js → preview-Q43LOK6P.js} +2 -2
- package/dist/rulepacks-JOA675OJ.js +135 -0
- package/dist/{shots-E2AWBDCN.js → shots-OKWOYF7F.js} +4 -3
- package/dist/{skills-U2J7Z3B2.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,16 @@ 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 — and version-aware rule-packs catch Tailwind v3
|
|
173
|
+
muscle memory in v4 projects at gate time, rename in hand. view-transition breakage (duplicate names, missing reduced-motion handling) is
|
|
174
|
+
flagged from the live page, and on Next 16+ the framework's own `/_next/mcp`
|
|
175
|
+
channel feeds structured errors straight into the fix loop. `/context` itemizes
|
|
176
|
+
the injected-context bill per source, with staleness warnings.
|
|
177
|
+
- **The design ledger**: `/decide` (plus chosen variants, rollbacks, accepted
|
|
178
|
+
sandboxes) appends to a committed `.squint/design-log.jsonl`; recent decisions ride
|
|
179
|
+
into every ask so they stop getting silently undone between sessions.
|
|
171
180
|
- **`/btw <question>`** asks about the codebase read-only without touching the main
|
|
172
181
|
thread's context. `.squint/locks` lists paths the engine must never touch; `/save`
|
|
173
182
|
exports the transcript as markdown.
|
|
@@ -360,6 +360,46 @@ var PHANTOM_AUDIT = `(() => {
|
|
|
360
360
|
}
|
|
361
361
|
return out;
|
|
362
362
|
})()`;
|
|
363
|
+
var VT_AUDIT = `(() => {
|
|
364
|
+
const findings = [];
|
|
365
|
+
const names = new Map();
|
|
366
|
+
for (const el of document.querySelectorAll('*')) {
|
|
367
|
+
const name = getComputedStyle(el).viewTransitionName;
|
|
368
|
+
if (name && name !== 'none' && name !== 'auto') {
|
|
369
|
+
const label = el.tagName.toLowerCase() + (el.id ? '#' + el.id : '');
|
|
370
|
+
const list = names.get(name) || [];
|
|
371
|
+
list.push(label);
|
|
372
|
+
names.set(name, list);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
for (const [name, els] of names) {
|
|
376
|
+
if (els.length > 1) {
|
|
377
|
+
findings.push('duplicate view-transition-name "' + name + '" on ' + els.length + ' elements (' + els.slice(0, 3).join(', ') + ') \u2014 the browser skips the entire transition');
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
let vtCss = false;
|
|
381
|
+
let reducedGuard = false;
|
|
382
|
+
const walk = (list, inReduced) => {
|
|
383
|
+
for (const rule of list) {
|
|
384
|
+
const media = rule.media && rule.media.mediaText || '';
|
|
385
|
+
const nowReduced = inReduced || /prefers-reduced-motion/.test(media);
|
|
386
|
+
if (rule.cssText && rule.cssText.includes('::view-transition')) {
|
|
387
|
+
vtCss = true;
|
|
388
|
+
if (nowReduced) reducedGuard = true;
|
|
389
|
+
}
|
|
390
|
+
if (nowReduced && rule.cssText && /animation|transition/.test(rule.cssText)) reducedGuard = true;
|
|
391
|
+
if (rule.cssRules) walk(rule.cssRules, nowReduced);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
for (const sheet of document.styleSheets) {
|
|
395
|
+
let rules; try { rules = sheet.cssRules; } catch { continue; }
|
|
396
|
+
try { walk(rules, false); } catch {}
|
|
397
|
+
}
|
|
398
|
+
if ((names.size > 0 || vtCss) && !reducedGuard) {
|
|
399
|
+
findings.push('view transitions declared with no prefers-reduced-motion handling anywhere in the CSS \u2014 motion-sensitive users get the full animation');
|
|
400
|
+
}
|
|
401
|
+
return findings;
|
|
402
|
+
})()`;
|
|
363
403
|
async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, audit = false) {
|
|
364
404
|
const { child, wsUrl, profileDir } = await launchChrome(chromePath);
|
|
365
405
|
const report = { consoleErrors: [], pageErrors: [], failedRequests: [] };
|
|
@@ -369,6 +409,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
369
409
|
let perf = {};
|
|
370
410
|
let narration = [];
|
|
371
411
|
let phantoms = [];
|
|
412
|
+
let viewTransitions = [];
|
|
372
413
|
const requests = /* @__PURE__ */ new Map();
|
|
373
414
|
let connection = null;
|
|
374
415
|
try {
|
|
@@ -450,6 +491,15 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
450
491
|
if (Array.isArray(result?.value)) phantoms = result.value.map(String);
|
|
451
492
|
} catch {
|
|
452
493
|
}
|
|
494
|
+
try {
|
|
495
|
+
const { result } = await connection.send(
|
|
496
|
+
"Runtime.evaluate",
|
|
497
|
+
{ expression: VT_AUDIT, returnByValue: true },
|
|
498
|
+
sessionId
|
|
499
|
+
);
|
|
500
|
+
if (Array.isArray(result?.value)) viewTransitions = result.value.map(String);
|
|
501
|
+
} catch {
|
|
502
|
+
}
|
|
453
503
|
try {
|
|
454
504
|
await connection.send("Accessibility.enable", {}, sessionId);
|
|
455
505
|
const { nodes } = await connection.send("Accessibility.getFullAXTree", {}, sessionId);
|
|
@@ -514,7 +564,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
514
564
|
child.kill("SIGKILL");
|
|
515
565
|
setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
|
|
516
566
|
}
|
|
517
|
-
return { report, shots, a11y, slop, perf, narration, phantoms };
|
|
567
|
+
return { report, shots, a11y, slop, perf, narration, phantoms, viewTransitions };
|
|
518
568
|
}
|
|
519
569
|
|
|
520
570
|
export {
|
|
@@ -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
|
|
@@ -27,6 +27,7 @@ var COMMANDS = [
|
|
|
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
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" },
|
|
30
31
|
{ name: "resume", group: "session", description: "pick up the previous session for this repo" },
|
|
31
32
|
{ name: "clear", group: "session", description: "new session (transcript, totals, persisted state)" },
|
|
32
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
|
+
};
|
|
@@ -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";
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
cdpCapture,
|
|
11
11
|
hasWebSocket
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-JFFNQIBU.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, phantoms } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true);
|
|
50
|
+
const { report, shots: shots2, a11y, slop, narration, phantoms, viewTransitions } = 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, phantoms };
|
|
65
|
+
return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration, phantoms, viewTransitions };
|
|
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-C6MOKVEN.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 vtSection(viewTransitions) {
|
|
165
|
+
if (!viewTransitions || viewTransitions.length === 0) return "";
|
|
166
|
+
return `
|
|
167
|
+
|
|
168
|
+
## View-transition findings
|
|
169
|
+
|
|
170
|
+
${viewTransitions.join("\n")}
|
|
171
|
+
|
|
172
|
+
Duplicate names abort the whole transition at runtime; missing reduced-motion handling animates for users who opted out. Fix the names / add the media query rather than removing the transitions.`;
|
|
173
|
+
}
|
|
164
174
|
function phantomSection(phantoms) {
|
|
165
175
|
if (!phantoms || phantoms.length === 0) return "";
|
|
166
176
|
return `
|
|
@@ -181,13 +191,13 @@ ${findings.join("\n")}
|
|
|
181
191
|
|
|
182
192
|
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.`;
|
|
183
193
|
}
|
|
184
|
-
function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms) {
|
|
194
|
+
function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms, viewTransitions) {
|
|
185
195
|
const list = shots.map((s) => `- ${s.name}: ${s.path}`).join("\n");
|
|
186
196
|
return `Screenshots of the running app were just captured:
|
|
187
197
|
|
|
188
198
|
${list}
|
|
189
199
|
|
|
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)}`;
|
|
200
|
+
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)}${vtSection(viewTransitions)}`;
|
|
191
201
|
}
|
|
192
202
|
|
|
193
203
|
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-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-ZUEGDTAM.js";
|
|
46
46
|
import {
|
|
47
47
|
clearState,
|
|
48
48
|
loadState,
|
|
@@ -58,14 +58,15 @@ import {
|
|
|
58
58
|
detectEngines,
|
|
59
59
|
getEngine
|
|
60
60
|
} from "./chunk-KVYGPLWW.js";
|
|
61
|
-
import "./chunk-
|
|
61
|
+
import "./chunk-JFFNQIBU.js";
|
|
62
62
|
import {
|
|
63
63
|
appendDecision,
|
|
64
64
|
enrich
|
|
65
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-YT6K2YIS.js";
|
|
66
|
+
import "./chunk-K5QJMSJH.js";
|
|
66
67
|
import {
|
|
67
68
|
composePrompt
|
|
68
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-WAJXATCO.js";
|
|
69
70
|
|
|
70
71
|
// src/cli.tsx
|
|
71
72
|
import { Command } from "commander";
|
|
@@ -239,7 +240,7 @@ function registerEnv(program2) {
|
|
|
239
240
|
}
|
|
240
241
|
}
|
|
241
242
|
const { findChrome: findChrome2 } = await import("./chrome-SBV3H77F.js");
|
|
242
|
-
const { hasWebSocket } = await import("./cdp-
|
|
243
|
+
const { hasWebSocket } = await import("./cdp-C6MOKVEN.js");
|
|
243
244
|
const chrome = findChrome2();
|
|
244
245
|
console.log(
|
|
245
246
|
chrome ? `${pc2.green("\u2713")} Chrome ${pc2.dim(chrome)}` : `${pc2.yellow("\u25CB")} Chrome ${pc2.dim("\u2014 screenshots and runtime probing disabled")}`
|
|
@@ -263,7 +264,7 @@ import pc3 from "picocolors";
|
|
|
263
264
|
function registerProject(program2) {
|
|
264
265
|
const skillsCommand = program2.command("skills").description("Project knowledge injected into asks (.squint/rules.md + .squint/skills/)");
|
|
265
266
|
skillsCommand.command("list").description("Show always-on rules and trigger-matched skills").action(async () => {
|
|
266
|
-
const { loadRules, loadSkills } = await import("./skills-
|
|
267
|
+
const { loadRules, loadSkills } = await import("./skills-GTTF4PNU.js");
|
|
267
268
|
const cwd = process.cwd();
|
|
268
269
|
const rules = loadRules(cwd);
|
|
269
270
|
console.log(
|
|
@@ -305,7 +306,7 @@ function registerProject(program2) {
|
|
|
305
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) => {
|
|
306
307
|
const fs3 = await import("fs");
|
|
307
308
|
const nodePath = await import("path");
|
|
308
|
-
const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-
|
|
309
|
+
const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-3ARYRBMH.js");
|
|
309
310
|
if (!familyId) {
|
|
310
311
|
console.log(pc3.bold("Aesthetic families") + pc3.dim(" \u2014 squint brief <id>\n"));
|
|
311
312
|
for (const family2 of FAMILIES) {
|
|
@@ -380,7 +381,7 @@ function registerProject(program2) {
|
|
|
380
381
|
process.exitCode = 1;
|
|
381
382
|
return;
|
|
382
383
|
}
|
|
383
|
-
const { runVariants: runVariants2, cleanVariants: cleanVariants2 } = await import("./variants-
|
|
384
|
+
const { runVariants: runVariants2, cleanVariants: cleanVariants2 } = await import("./variants-3IEP7DFY.js");
|
|
384
385
|
const config = loadConfig(defaultPaths(cwd));
|
|
385
386
|
const engineId = resolveEngineId(config, options.engine);
|
|
386
387
|
const engine = getEngine(engineId);
|
|
@@ -398,7 +399,7 @@ function registerProject(program2) {
|
|
|
398
399
|
);
|
|
399
400
|
const succeeded = runs.filter((r) => r.result.ok);
|
|
400
401
|
if (options.shots && succeeded.length > 0) {
|
|
401
|
-
const { screenshotVariants: screenshotVariants2 } = await import("./shots-
|
|
402
|
+
const { screenshotVariants: screenshotVariants2 } = await import("./shots-OKWOYF7F.js");
|
|
402
403
|
console.log(pc3.dim("capturing screenshots\u2026"));
|
|
403
404
|
const shots = await screenshotVariants2(cwd, succeeded.map((r) => r.variant));
|
|
404
405
|
for (const shot of shots) {
|
|
@@ -413,7 +414,7 @@ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 `
|
|
|
413
414
|
}
|
|
414
415
|
);
|
|
415
416
|
variantsCommand.command("list").description("List generated variants").action(async () => {
|
|
416
|
-
const { listVariants: listVariants2 } = await import("./variants-
|
|
417
|
+
const { listVariants: listVariants2 } = await import("./variants-3IEP7DFY.js");
|
|
417
418
|
const ids = listVariants2(process.cwd());
|
|
418
419
|
if (ids.length === 0) {
|
|
419
420
|
console.log(pc3.dim('no variants \u2014 squint variants gen <n> "<ask>"'));
|
|
@@ -422,7 +423,7 @@ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 `
|
|
|
422
423
|
for (const id of ids) console.log(id);
|
|
423
424
|
});
|
|
424
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) => {
|
|
425
|
-
const { applyVariant: applyVariant2, cleanVariants: cleanVariants2 } = await import("./variants-
|
|
426
|
+
const { applyVariant: applyVariant2, cleanVariants: cleanVariants2 } = await import("./variants-3IEP7DFY.js");
|
|
426
427
|
const cwd = process.cwd();
|
|
427
428
|
const result = applyVariant2(cwd, id);
|
|
428
429
|
if (!result.ok) {
|
|
@@ -434,7 +435,7 @@ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 `
|
|
|
434
435
|
console.log(pc3.green(`\u2713 applied ${id} to the working tree`) + pc3.dim(" \u2014 review with git diff"));
|
|
435
436
|
});
|
|
436
437
|
variantsCommand.command("clean").description("Discard all variants").action(async () => {
|
|
437
|
-
const { cleanVariants: cleanVariants2 } = await import("./variants-
|
|
438
|
+
const { cleanVariants: cleanVariants2 } = await import("./variants-3IEP7DFY.js");
|
|
438
439
|
const count = cleanVariants2(process.cwd());
|
|
439
440
|
console.log(pc3.dim(`removed ${count} variant(s)`));
|
|
440
441
|
});
|
|
@@ -462,7 +463,7 @@ function registerQuality(program2) {
|
|
|
462
463
|
if (results.some((r) => !r.ok)) process.exitCode = 1;
|
|
463
464
|
});
|
|
464
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) => {
|
|
465
|
-
const { captureViewports: captureViewports2 } = await import("./preview-
|
|
466
|
+
const { captureViewports: captureViewports2 } = await import("./preview-Q43LOK6P.js");
|
|
466
467
|
const result = await captureViewports2(process.cwd(), url);
|
|
467
468
|
if (!result) {
|
|
468
469
|
console.error(pc4.red("\u2717 no Chrome/Chromium found"));
|
|
@@ -824,7 +825,7 @@ ${question}`,
|
|
|
824
825
|
return;
|
|
825
826
|
}
|
|
826
827
|
await this.runTurn(
|
|
827
|
-
buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms),
|
|
828
|
+
buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions),
|
|
828
829
|
`\u{1F441} polish round ${round}/${rounds}`
|
|
829
830
|
);
|
|
830
831
|
}
|
|
@@ -1051,6 +1052,26 @@ ${driftSummary(drift)}`);
|
|
|
1051
1052
|
}
|
|
1052
1053
|
} catch {
|
|
1053
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
|
+
}
|
|
1054
1075
|
}
|
|
1055
1076
|
runHook(this.opts.cwd, "on-turn-end", {
|
|
1056
1077
|
cost: String(result.costUsd ?? 0),
|
|
@@ -1111,6 +1132,18 @@ ${driftSummary(drift)}`);
|
|
|
1111
1132
|
if (result.error !== "interrupted" && dev && (dev.state === "running" || dev.state === "starting")) {
|
|
1112
1133
|
await delay(1500);
|
|
1113
1134
|
const errors = dev.errorsSince(runStart);
|
|
1135
|
+
if (this.state.devUrl) {
|
|
1136
|
+
try {
|
|
1137
|
+
const { hasNextMcp, probeNextMcp } = await import("./nextMcp-42Y63M7W.js");
|
|
1138
|
+
if (hasNextMcp(this.execCwd())) {
|
|
1139
|
+
const mcp = await probeNextMcp(this.state.devUrl);
|
|
1140
|
+
if (mcp.available && mcp.errors.length > 0) {
|
|
1141
|
+
for (const err of mcp.errors) errors.push(`[next mcp] ${err}`);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
} catch {
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1114
1147
|
if (errors.length > 0) {
|
|
1115
1148
|
this.addProblem(
|
|
1116
1149
|
"dev",
|
|
@@ -1142,7 +1175,7 @@ ${errors.slice(-5).join("\n")}`);
|
|
|
1142
1175
|
const captureResult = await this.capture();
|
|
1143
1176
|
if (captureResult) {
|
|
1144
1177
|
await this.runTurn(
|
|
1145
|
-
buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms),
|
|
1178
|
+
buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms, captureResult.viewTransitions),
|
|
1146
1179
|
"\u{1F441} auto-review rendered UI"
|
|
1147
1180
|
);
|
|
1148
1181
|
}
|
|
@@ -1282,6 +1315,24 @@ ${errors.slice(-5).join("\n")}`);
|
|
|
1282
1315
|
this.push("status", "runtime clean \u2014 no console errors, exceptions, or failed requests");
|
|
1283
1316
|
}
|
|
1284
1317
|
}
|
|
1318
|
+
const vtHard = (result.viewTransitions ?? []).filter((f) => f.startsWith("duplicate"));
|
|
1319
|
+
if (vtHard.length > 0) {
|
|
1320
|
+
this.push("error", `view transitions: ${vtHard.length} broken
|
|
1321
|
+
${vtHard.join("\n")}`);
|
|
1322
|
+
this.addProblem(
|
|
1323
|
+
"runtime",
|
|
1324
|
+
`${vtHard.length} view transition(s) the browser will skip`,
|
|
1325
|
+
`Duplicate view-transition-name values make the browser abort the entire transition at runtime:
|
|
1326
|
+
|
|
1327
|
+
${vtHard.join("\n")}
|
|
1328
|
+
|
|
1329
|
+
Give each simultaneously rendered element a unique name (or scope names per item, e.g. per list key). Do not remove the transitions.`
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
const vtSoft = (result.viewTransitions ?? []).filter((f) => !f.startsWith("duplicate"));
|
|
1333
|
+
if (vtSoft.length > 0) {
|
|
1334
|
+
this.push("status", `view transitions: ${vtSoft.join("; ")}`);
|
|
1335
|
+
}
|
|
1285
1336
|
if (result.phantoms && result.phantoms.length > 0) {
|
|
1286
1337
|
this.push("error", `phantom classes: ${result.phantoms.length} (in the DOM, absent from CSS)
|
|
1287
1338
|
${result.phantoms.slice(0, 5).join("\n")}`);
|
|
@@ -1411,6 +1462,14 @@ They are objective defects, not style preferences.`
|
|
|
1411
1462
|
}
|
|
1412
1463
|
break;
|
|
1413
1464
|
}
|
|
1465
|
+
case "context": {
|
|
1466
|
+
import("./contextDoctor-M3R3PICP.js").then(({ contextReport, formatContextReport }) => {
|
|
1467
|
+
this.push("status", formatContextReport(contextReport(this.execCwd())));
|
|
1468
|
+
}).catch((error) => {
|
|
1469
|
+
this.push("status", `context report failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1470
|
+
});
|
|
1471
|
+
break;
|
|
1472
|
+
}
|
|
1414
1473
|
case "decide": {
|
|
1415
1474
|
if (!arg) {
|
|
1416
1475
|
this.push("status", "usage: /decide <the decision> \u2014 recorded in .squint/design-log.jsonl and injected into every future ask");
|
|
@@ -1520,8 +1579,8 @@ They are objective defects, not style preferences.`
|
|
|
1520
1579
|
this.push("error", "no Chrome/Chromium found for flows");
|
|
1521
1580
|
return;
|
|
1522
1581
|
}
|
|
1523
|
-
const { runFlow } = await import("./cdp-
|
|
1524
|
-
const { previewDir } = await import("./preview-
|
|
1582
|
+
const { runFlow } = await import("./cdp-C6MOKVEN.js");
|
|
1583
|
+
const { previewDir } = await import("./preview-Q43LOK6P.js");
|
|
1525
1584
|
this.push("status", `replaying ${flows.length} flow(s)\u2026`);
|
|
1526
1585
|
this.notify({ running: true, runStartedAt: Date.now() });
|
|
1527
1586
|
const failures = [];
|
|
@@ -1659,7 +1718,7 @@ They are objective defects, not style preferences.`
|
|
|
1659
1718
|
const result = await this.capture();
|
|
1660
1719
|
if (result) {
|
|
1661
1720
|
await this.runTurn(
|
|
1662
|
-
buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms),
|
|
1721
|
+
buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions),
|
|
1663
1722
|
`\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
|
|
1664
1723
|
);
|
|
1665
1724
|
}
|
|
@@ -1845,7 +1904,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
|
|
|
1845
1904
|
this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
|
|
1846
1905
|
break;
|
|
1847
1906
|
case "help": {
|
|
1848
|
-
void import("./commands-
|
|
1907
|
+
void import("./commands-VDICJJT2.js").then(({ commandHelp }) => this.push("status", commandHelp()));
|
|
1849
1908
|
break;
|
|
1850
1909
|
}
|
|
1851
1910
|
case "quit":
|
|
@@ -2464,7 +2523,7 @@ function registerTui(program2) {
|
|
|
2464
2523
|
}
|
|
2465
2524
|
|
|
2466
2525
|
// src/cli.tsx
|
|
2467
|
-
var VERSION = true ? "0.4.
|
|
2526
|
+
var VERSION = true ? "0.4.4" : "0.0.0-dev";
|
|
2468
2527
|
var program = new Command();
|
|
2469
2528
|
program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
|
|
2470
2529
|
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
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/preview/nextMcp.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
function parseBody(contentType, text) {
|
|
7
|
+
try {
|
|
8
|
+
if (contentType.includes("text/event-stream")) {
|
|
9
|
+
const events = text.split("\n").filter((l) => l.startsWith("data:"));
|
|
10
|
+
const last = events.at(-1);
|
|
11
|
+
return last ? JSON.parse(last.slice(5).trim()) : null;
|
|
12
|
+
}
|
|
13
|
+
return text.trim().length > 0 ? JSON.parse(text) : null;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function rpc(url, sessionId, body, timeoutMs) {
|
|
19
|
+
const headers = {
|
|
20
|
+
"content-type": "application/json",
|
|
21
|
+
accept: "application/json, text/event-stream"
|
|
22
|
+
};
|
|
23
|
+
if (sessionId) headers["mcp-session-id"] = sessionId;
|
|
24
|
+
const res = await fetch(url, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers,
|
|
27
|
+
body: JSON.stringify(body),
|
|
28
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
29
|
+
});
|
|
30
|
+
const newSession = res.headers.get("mcp-session-id") ?? sessionId;
|
|
31
|
+
if (res.status === 202) return { response: {}, sessionId: newSession };
|
|
32
|
+
if (!res.ok) return { response: null, sessionId: newSession };
|
|
33
|
+
const text = await res.text();
|
|
34
|
+
return { response: parseBody(res.headers.get("content-type") ?? "", text), sessionId: newSession };
|
|
35
|
+
}
|
|
36
|
+
function extractText(result) {
|
|
37
|
+
const content = result?.content;
|
|
38
|
+
if (!Array.isArray(content)) return [];
|
|
39
|
+
return content.map((c) => c && typeof c === "object" && "text" in c ? String(c.text) : "").filter((t) => t.trim().length > 0);
|
|
40
|
+
}
|
|
41
|
+
async function probeNextMcp(baseUrl, timeoutMs = 4e3) {
|
|
42
|
+
const url = new URL("/_next/mcp", baseUrl).toString();
|
|
43
|
+
const none = { available: false, tools: [], errors: [] };
|
|
44
|
+
let id = 0;
|
|
45
|
+
try {
|
|
46
|
+
const init = await rpc(url, null, {
|
|
47
|
+
jsonrpc: "2.0",
|
|
48
|
+
id: ++id,
|
|
49
|
+
method: "initialize",
|
|
50
|
+
params: {
|
|
51
|
+
protocolVersion: "2025-06-18",
|
|
52
|
+
capabilities: {},
|
|
53
|
+
clientInfo: { name: "squint", version: "0" }
|
|
54
|
+
}
|
|
55
|
+
}, timeoutMs);
|
|
56
|
+
if (!init.response?.result) return none;
|
|
57
|
+
const session = init.sessionId;
|
|
58
|
+
await rpc(url, session, { jsonrpc: "2.0", method: "notifications/initialized" }, timeoutMs).catch(() => null);
|
|
59
|
+
const list = await rpc(url, session, { jsonrpc: "2.0", id: ++id, method: "tools/list" }, timeoutMs);
|
|
60
|
+
const toolDefs = list.response?.result?.tools ?? [];
|
|
61
|
+
const tools = toolDefs.map((t) => String(t.name ?? "")).filter((n) => n.length > 0);
|
|
62
|
+
const errors = [];
|
|
63
|
+
for (const name of tools.filter((n) => /error|issue|diagnostic/i.test(n)).slice(0, 3)) {
|
|
64
|
+
const call = await rpc(url, session, {
|
|
65
|
+
jsonrpc: "2.0",
|
|
66
|
+
id: ++id,
|
|
67
|
+
method: "tools/call",
|
|
68
|
+
params: { name, arguments: {} }
|
|
69
|
+
}, timeoutMs);
|
|
70
|
+
for (const text of extractText(call.response?.result)) {
|
|
71
|
+
if (!/no (build |runtime )?errors|^\s*\[\]\s*$|no issues/i.test(text)) {
|
|
72
|
+
errors.push(`[${name}] ${text.slice(0, 600)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (errors.length >= 5) break;
|
|
76
|
+
}
|
|
77
|
+
return { available: true, tools, errors };
|
|
78
|
+
} catch {
|
|
79
|
+
return none;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function hasNextMcp(cwd) {
|
|
83
|
+
try {
|
|
84
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
|
|
85
|
+
for (const key of ["dependencies", "devDependencies"]) {
|
|
86
|
+
const range = pkg?.[key]?.next;
|
|
87
|
+
const major = range?.match(/(\d+)/)?.[1];
|
|
88
|
+
if (major) return Number.parseInt(major, 10) >= 16;
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
hasNextMcp,
|
|
96
|
+
probeNextMcp
|
|
97
|
+
};
|
|
@@ -10,11 +10,11 @@ import {
|
|
|
10
10
|
probeRuntime,
|
|
11
11
|
routeShotName,
|
|
12
12
|
runtimeSummary
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-ZUEGDTAM.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-JFFNQIBU.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