@aayambansal/squint 0.4.3 → 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 CHANGED
@@ -169,7 +169,11 @@ squint doctor --probe # run every engine end to end, verify auth act
169
169
  design token, and the mechanical anti-slop sweep flags generic-AI tells as
170
170
  distinctiveness debt in `/review`. The phantom-class check diffs every DOM class
171
171
  against the compiled CSS — hallucinated utilities surface as named problems instead
172
- of silently unstyled elements.
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.
173
177
  - **The design ledger**: `/decide` (plus chosen variants, rollbacks, accepted
174
178
  sandboxes) appends to a committed `.squint/design-log.jsonl`; recent decisions ride
175
179
  into every ask so they stop getting silently undone between sessions.
@@ -4,7 +4,7 @@ import {
4
4
  hasWebSocket,
5
5
  pixelDiffPct,
6
6
  runFlow
7
- } from "./chunk-Y47X4ENN.js";
7
+ } from "./chunk-JFFNQIBU.js";
8
8
  export {
9
9
  cdpCapture,
10
10
  hasWebSocket,
@@ -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 {
@@ -9,7 +9,7 @@ import {
9
9
  import {
10
10
  cdpCapture,
11
11
  hasWebSocket
12
- } from "./chunk-Y47X4ENN.js";
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-TPP2RGSR.js");
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
@@ -42,7 +42,7 @@ import {
42
42
  comparePulse,
43
43
  probeRuntime,
44
44
  runtimeSummary
45
- } from "./chunk-WCLQZFOR.js";
45
+ } from "./chunk-ZUEGDTAM.js";
46
46
  import {
47
47
  clearState,
48
48
  loadState,
@@ -58,7 +58,7 @@ import {
58
58
  detectEngines,
59
59
  getEngine
60
60
  } from "./chunk-KVYGPLWW.js";
61
- import "./chunk-Y47X4ENN.js";
61
+ import "./chunk-JFFNQIBU.js";
62
62
  import {
63
63
  appendDecision,
64
64
  enrich
@@ -240,7 +240,7 @@ function registerEnv(program2) {
240
240
  }
241
241
  }
242
242
  const { findChrome: findChrome2 } = await import("./chrome-SBV3H77F.js");
243
- const { hasWebSocket } = await import("./cdp-TPP2RGSR.js");
243
+ const { hasWebSocket } = await import("./cdp-C6MOKVEN.js");
244
244
  const chrome = findChrome2();
245
245
  console.log(
246
246
  chrome ? `${pc2.green("\u2713")} Chrome ${pc2.dim(chrome)}` : `${pc2.yellow("\u25CB")} Chrome ${pc2.dim("\u2014 screenshots and runtime probing disabled")}`
@@ -463,7 +463,7 @@ function registerQuality(program2) {
463
463
  if (results.some((r) => !r.ok)) process.exitCode = 1;
464
464
  });
465
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) => {
466
- const { captureViewports: captureViewports2 } = await import("./preview-SKHIXVCE.js");
466
+ const { captureViewports: captureViewports2 } = await import("./preview-Q43LOK6P.js");
467
467
  const result = await captureViewports2(process.cwd(), url);
468
468
  if (!result) {
469
469
  console.error(pc4.red("\u2717 no Chrome/Chromium found"));
@@ -825,7 +825,7 @@ ${question}`,
825
825
  return;
826
826
  }
827
827
  await this.runTurn(
828
- 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),
829
829
  `\u{1F441} polish round ${round}/${rounds}`
830
830
  );
831
831
  }
@@ -1132,6 +1132,18 @@ ${rulePackSummary(soft)}`);
1132
1132
  if (result.error !== "interrupted" && dev && (dev.state === "running" || dev.state === "starting")) {
1133
1133
  await delay(1500);
1134
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
+ }
1135
1147
  if (errors.length > 0) {
1136
1148
  this.addProblem(
1137
1149
  "dev",
@@ -1163,7 +1175,7 @@ ${errors.slice(-5).join("\n")}`);
1163
1175
  const captureResult = await this.capture();
1164
1176
  if (captureResult) {
1165
1177
  await this.runTurn(
1166
- 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),
1167
1179
  "\u{1F441} auto-review rendered UI"
1168
1180
  );
1169
1181
  }
@@ -1303,6 +1315,24 @@ ${errors.slice(-5).join("\n")}`);
1303
1315
  this.push("status", "runtime clean \u2014 no console errors, exceptions, or failed requests");
1304
1316
  }
1305
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
+ }
1306
1336
  if (result.phantoms && result.phantoms.length > 0) {
1307
1337
  this.push("error", `phantom classes: ${result.phantoms.length} (in the DOM, absent from CSS)
1308
1338
  ${result.phantoms.slice(0, 5).join("\n")}`);
@@ -1549,8 +1579,8 @@ They are objective defects, not style preferences.`
1549
1579
  this.push("error", "no Chrome/Chromium found for flows");
1550
1580
  return;
1551
1581
  }
1552
- const { runFlow } = await import("./cdp-TPP2RGSR.js");
1553
- const { previewDir } = await import("./preview-SKHIXVCE.js");
1582
+ const { runFlow } = await import("./cdp-C6MOKVEN.js");
1583
+ const { previewDir } = await import("./preview-Q43LOK6P.js");
1554
1584
  this.push("status", `replaying ${flows.length} flow(s)\u2026`);
1555
1585
  this.notify({ running: true, runStartedAt: Date.now() });
1556
1586
  const failures = [];
@@ -1688,7 +1718,7 @@ They are objective defects, not style preferences.`
1688
1718
  const result = await this.capture();
1689
1719
  if (result) {
1690
1720
  await this.runTurn(
1691
- 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),
1692
1722
  `\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
1693
1723
  );
1694
1724
  }
@@ -2493,7 +2523,7 @@ function registerTui(program2) {
2493
2523
  }
2494
2524
 
2495
2525
  // src/cli.tsx
2496
- var VERSION = true ? "0.4.3" : "0.0.0-dev";
2526
+ var VERSION = true ? "0.4.4" : "0.0.0-dev";
2497
2527
  var program = new Command();
2498
2528
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
2499
2529
  registerRun(program);
@@ -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-WCLQZFOR.js";
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-Y47X4ENN.js";
17
+ import "./chunk-JFFNQIBU.js";
18
18
  export {
19
19
  VIEWPORTS,
20
20
  buildReviewPrompt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aayambansal/squint",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "Lovable for your terminal — a frontend harness on top of Claude Code, Codex, and other coding agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",