@aayambansal/squint 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  App
4
- } from "./chunk-OSKYU4PM.js";
5
- import "./chunk-ODSCWVBX.js";
4
+ } from "./chunk-PPC2KM7P.js";
5
+ import "./chunk-B356UR44.js";
6
6
  import "./chunk-YKEAKVIS.js";
7
7
  import "./chunk-LNJ7O5O4.js";
8
- import "./chunk-PC45PRK3.js";
8
+ import "./chunk-3ZHETLUC.js";
9
9
  import "./chunk-UBADCBPT.js";
10
10
  import "./chunk-IEQGTVWN.js";
11
11
  import "./chunk-AUJJGMZG.js";
@@ -16,10 +16,10 @@ import "./chunk-K5QJMSJH.js";
16
16
  import "./chunk-WAJXATCO.js";
17
17
  import "./chunk-62JNF5M2.js";
18
18
  import "./chunk-7CAGWFAQ.js";
19
- import "./chunk-HFQZDT65.js";
19
+ import "./chunk-4DZAWPS2.js";
20
20
  import "./chunk-55JZKI2M.js";
21
21
  import "./chunk-HIUQ4FXO.js";
22
- import "./chunk-GAWQ7YXC.js";
22
+ import "./chunk-6RMF4NAW.js";
23
23
  export {
24
24
  App
25
25
  };
@@ -6,7 +6,7 @@ import {
6
6
  pixelDiffPct,
7
7
  runFlow,
8
8
  summarizeSoftNav
9
- } from "./chunk-HFQZDT65.js";
9
+ } from "./chunk-4DZAWPS2.js";
10
10
  export {
11
11
  cdpCapture,
12
12
  hasWebSocket,
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  loadChecks,
4
4
  runIntervalSweep
5
- } from "./chunk-GAWQ7YXC.js";
5
+ } from "./chunk-6RMF4NAW.js";
6
6
  export {
7
7
  loadChecks,
8
8
  runIntervalSweep
@@ -5,14 +5,14 @@ import {
5
5
  import {
6
6
  cdpCapture,
7
7
  hasWebSocket
8
- } from "./chunk-HFQZDT65.js";
8
+ } from "./chunk-4DZAWPS2.js";
9
9
  import {
10
10
  findChrome,
11
11
  screenshot
12
12
  } from "./chunk-55JZKI2M.js";
13
13
  import {
14
14
  loadChecks
15
- } from "./chunk-GAWQ7YXC.js";
15
+ } from "./chunk-6RMF4NAW.js";
16
16
 
17
17
  // src/preview/preview.ts
18
18
  import fs from "fs";
@@ -51,7 +51,7 @@ async function captureViewports(cwd, url) {
51
51
  if (hasWebSocket()) {
52
52
  try {
53
53
  const checks = loadChecks(cwd);
54
- const { report, shots: shots2, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp, jank } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true, checks);
54
+ const { report, shots: shots2, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp, jank, locale } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true, checks);
55
55
  const errors2 = [];
56
56
  for (const route of routes.slice(1)) {
57
57
  try {
@@ -66,7 +66,7 @@ async function captureViewports(cwd, url) {
66
66
  errors2.push(`${route}: capture failed`);
67
67
  }
68
68
  }
69
- return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp, jank };
69
+ return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp, jank, locale };
70
70
  } catch {
71
71
  }
72
72
  }
@@ -139,13 +139,13 @@ async function probeRuntime(url, cwd) {
139
139
  async function comparePulse(previous, current) {
140
140
  const chrome = findChrome();
141
141
  if (!chrome || !hasWebSocket()) return null;
142
- const { pixelDiffPct } = await import("./cdp-DV4D6PUL.js");
142
+ const { pixelDiffPct } = await import("./cdp-6YBMFE7M.js");
143
143
  return pixelDiffPct(chrome, previous, current);
144
144
  }
145
145
  async function comparePulseAttributed(previous, current, url, outPath) {
146
146
  const chrome = findChrome();
147
147
  if (!chrome || !hasWebSocket()) return null;
148
- const { pixelDiffAttributed } = await import("./cdp-DV4D6PUL.js");
148
+ const { pixelDiffAttributed } = await import("./cdp-6YBMFE7M.js");
149
149
  return pixelDiffAttributed(chrome, previous, current, url, outPath);
150
150
  }
151
151
  function buildRuntimeFixPrompt(report) {
@@ -173,6 +173,16 @@ ${narration.join("\n")}
173
173
 
174
174
  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.`;
175
175
  }
176
+ function localeSection(locale) {
177
+ if (!locale || locale.length === 0) return "";
178
+ return `
179
+
180
+ ## Locale findings (pseudo-localization + RTL)
181
+
182
+ ${locale.join("\n")}
183
+
184
+ Text that clips at +40% expansion truncates in real translations; explicit left-alignment ignores RTL users. Fix with flexible widths and logical properties (text-align: start, margin-inline), not by shortening copy.`;
185
+ }
176
186
  function jankSection(jank) {
177
187
  if (!jank || jank.length === 0) return "";
178
188
  return `
@@ -233,13 +243,13 @@ ${findings.join("\n")}
233
243
 
234
244
  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.`;
235
245
  }
236
- function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms, viewTransitions, components, webmcp, jank) {
246
+ function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms, viewTransitions, components, webmcp, jank, locale) {
237
247
  const list = shots.map((s) => `- ${s.name}: ${s.path}`).join("\n");
238
248
  return `Screenshots of the running app were just captured:
239
249
 
240
250
  ${list}
241
251
 
242
- 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)}${componentSection(components)}${webmcpSection(webmcp)}${jankSection(jank)}`;
252
+ 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)}${componentSection(components)}${webmcpSection(webmcp)}${jankSection(jank)}${localeSection(locale)}`;
243
253
  }
244
254
 
245
255
  export {
@@ -211,10 +211,11 @@ function summarizeSoftNav(entries) {
211
211
  });
212
212
  }
213
213
  async function runFlow(chromePath, baseUrl, flow, outDir) {
214
- const { stepExpression } = await import("./flows-XORMW4DA.js");
214
+ const { stepExpression } = await import("./flows-4QS2ER6N.js");
215
215
  const { child, wsUrl, profileDir } = await launchChrome(chromePath);
216
216
  const shots = [];
217
217
  let transitions = [];
218
+ const leaks = [];
218
219
  let connection = null;
219
220
  try {
220
221
  connection = await CdpConnection.connect(wsUrl, 1e4);
@@ -259,7 +260,7 @@ async function runFlow(chromePath, baseUrl, flow, outDir) {
259
260
  );
260
261
  const value = result?.value;
261
262
  if (!value?.ok) {
262
- return { ok: false, failedStep: stepNumber, detail: value?.detail ?? "step failed", shots, transitions };
263
+ return { ok: false, failedStep: stepNumber, detail: value?.detail ?? "step failed", shots, transitions, leaks };
263
264
  }
264
265
  await new Promise((resolve) => setTimeout(resolve, 300));
265
266
  }
@@ -272,9 +273,27 @@ async function runFlow(chromePath, baseUrl, flow, outDir) {
272
273
  if (Array.isArray(result?.value)) transitions = summarizeSoftNav(result.value);
273
274
  } catch {
274
275
  }
275
- return { ok: true, shots, transitions };
276
+ try {
277
+ const { detachedNodes } = await connection.send("DOM.getDetachedDomNodes", {}, sessionId);
278
+ const names = /* @__PURE__ */ new Map();
279
+ let total = 0;
280
+ for (const retained of detachedNodes ?? []) {
281
+ const walk = (node) => {
282
+ total++;
283
+ const name = (node.nodeName ?? "?").toLowerCase();
284
+ names.set(name, (names.get(name) ?? 0) + 1);
285
+ };
286
+ walk(retained.treeNode ?? retained);
287
+ }
288
+ if (total >= 10) {
289
+ const top = [...names.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([n, c]) => `${n} \xD7${c}`).join(", ");
290
+ leaks.push(`leak: ${total} detached DOM subtree(s) retained after the journey (${top}) \u2014 something holds references to removed nodes`);
291
+ }
292
+ } catch {
293
+ }
294
+ return { ok: true, shots, transitions, leaks };
276
295
  } catch (err) {
277
- return { ok: false, detail: err instanceof Error ? err.message : String(err), shots, transitions };
296
+ return { ok: false, detail: err instanceof Error ? err.message : String(err), shots, transitions, leaks };
278
297
  } finally {
279
298
  connection?.close();
280
299
  child.kill("SIGKILL");
@@ -746,6 +765,106 @@ var SCRIPTED_SCROLL = `(async () => {
746
765
  .slice(0, 6)
747
766
  .map((e) => e.duration + 'ms frame \u2014 ' + (e.fn || e.invoker || 'script') + (e.url ? ' @ ' + e.url : ''));
748
767
  })()`;
768
+ var LOCALE_AUDIT = `(async () => {
769
+ const findings = [];
770
+ const label = (el) => {
771
+ let out = el.tagName.toLowerCase();
772
+ if (el.id) out += '#' + el.id;
773
+ else if (el.classList[0]) out += '.' + el.classList[0];
774
+ return out;
775
+ };
776
+ const MAP = { a:'\xE1', e:'\xE9', i:'\xED', o:'\xF3', u:'\xFC', A:'\xC1', E:'\xC9', I:'\xCD', O:'\xD3', U:'\xDC', n:'\xF1', N:'\xD1' };
777
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
778
+ acceptNode: (n) => {
779
+ const parent = n.parentElement;
780
+ if (!parent || /^(SCRIPT|STYLE|NOSCRIPT)$/.test(parent.tagName)) return NodeFilter.FILTER_REJECT;
781
+ return n.textContent.trim().length > 1 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
782
+ },
783
+ });
784
+ const nodes = [];
785
+ while (walker.nextNode()) nodes.push(walker.currentNode);
786
+ for (const node of nodes.slice(0, 400)) {
787
+ const text = node.textContent;
788
+ const accented = text.replace(/[aeiouAEIOUnN]/g, (c) => MAP[c] || c);
789
+ const pad = '\xB7'.repeat(Math.ceil(text.trim().length * 0.4));
790
+ node.textContent = accented + pad;
791
+ }
792
+ await new Promise((r) => requestAnimationFrame(() => setTimeout(r, 120)));
793
+ const clipped = new Set();
794
+ for (const el of document.querySelectorAll('button, a, h1, h2, h3, [class*="btn"], [class*="badge"], nav *, th, td, li, label, span, p')) {
795
+ if (clipped.size >= 6) break;
796
+ if (el.children.length > 0) continue;
797
+ if (el.scrollWidth > el.clientWidth + 3 && el.clientWidth > 0) {
798
+ const key = label(el);
799
+ if (!clipped.has(key)) {
800
+ clipped.add(key);
801
+ findings.push('locale: <' + key + '> clips at +40% text expansion \u2014 real translations will truncate here');
802
+ }
803
+ }
804
+ }
805
+ document.documentElement.setAttribute('dir', 'rtl');
806
+ await new Promise((r) => requestAnimationFrame(() => setTimeout(r, 120)));
807
+ let leftHardcodes = 0;
808
+ for (const el of document.querySelectorAll('p, h1, h2, h3, li, td, label, div')) {
809
+ if (leftHardcodes >= 3) break;
810
+ const text = (el.textContent || '').trim();
811
+ if (text.length < 10 || el.children.length > 2) continue;
812
+ if (getComputedStyle(el).textAlign === 'left') {
813
+ findings.push('locale: <' + label(el) + '> hardcodes text-align:left \u2014 ignores RTL (use start)');
814
+ leftHardcodes++;
815
+ }
816
+ }
817
+ if (document.documentElement.scrollWidth > innerWidth + 4) {
818
+ findings.push('locale: the page overflows horizontally under RTL');
819
+ }
820
+ return findings;
821
+ })()`;
822
+ var DECEPTION_AUDIT = `(() => {
823
+ const out = [];
824
+ const label = (el) => {
825
+ let s = el.tagName.toLowerCase();
826
+ if (el.id) s += '#' + el.id; else if (el.classList[0]) s += '.' + el.classList[0];
827
+ return s;
828
+ };
829
+ // 1. Preselected consent
830
+ for (const box of document.querySelectorAll('input[type="checkbox"]:checked')) {
831
+ const scope = (box.closest('label') || box.parentElement || box);
832
+ const text = (scope.textContent || '').slice(0, 160);
833
+ if (/subscribe|newsletter|marketing|offers|promotions|updates|share my|third.part/i.test(text)) {
834
+ out.push('deceptive: preselected consent checkbox \u2014 "' + text.trim().slice(0, 60) + '" starts opted in');
835
+ }
836
+ if (out.length >= 6) return out;
837
+ }
838
+ // 2. Urgency countdowns
839
+ for (const el of document.querySelectorAll('body *')) {
840
+ if (el.children.length > 0) continue;
841
+ const text = (el.textContent || '').trim();
842
+ if (text.length > 0 && text.length < 80 && /\bd{1,2}:d{2}(:d{2})?\b/.test(text) && /only|hurry|left|expires|ends|limited|last chance/i.test(text)) {
843
+ out.push('deceptive: urgency countdown on <' + label(el) + '> ("' + text.slice(0, 50) + '") \u2014 verify it is real, not theater');
844
+ break;
845
+ }
846
+ }
847
+ // 3 + 4. Buried decline / confirmshaming inside consent surfaces
848
+ for (const surface of document.querySelectorAll('[role="dialog"], [class*="modal"], [class*="cookie"], [class*="consent"], [class*="banner"]')) {
849
+ const buttons = [...surface.querySelectorAll('button, a, [role="button"]')];
850
+ const accept = buttons.find((b) => /accept|agree|allow|got it|yes/i.test(b.textContent || ''));
851
+ const decline = buttons.find((b) => /decline|reject|refuse|no thanks|later|manage/i.test(b.textContent || ''));
852
+ if (accept && decline) {
853
+ const ar = accept.getBoundingClientRect(), dr = decline.getBoundingClientRect();
854
+ const aSize = parseFloat(getComputedStyle(accept).fontSize), dSize = parseFloat(getComputedStyle(decline).fontSize);
855
+ if ((dr.width * dr.height) < (ar.width * ar.height) * 0.55 || dSize < aSize - 2) {
856
+ out.push('deceptive: <' + label(decline) + '> is visually buried next to <' + label(accept) + '> \u2014 equal choices deserve equal weight');
857
+ }
858
+ }
859
+ for (const b of buttons) {
860
+ if (/no thanks,? i (don.?t|hate|prefer)|i don.?t want to (save|improve|protect)/i.test(b.textContent || '')) {
861
+ out.push('deceptive: confirmshaming copy on <' + label(b) + '> ("' + (b.textContent || '').trim().slice(0, 50) + '")');
862
+ }
863
+ }
864
+ if (out.length >= 6) break;
865
+ }
866
+ return out.slice(0, 6);
867
+ })()`;
749
868
  async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, audit = false, checks = []) {
750
869
  const { child, wsUrl, profileDir } = await launchChrome(chromePath);
751
870
  const report = { consoleErrors: [], pageErrors: [], failedRequests: [] };
@@ -759,6 +878,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
759
878
  let components = [];
760
879
  const checkFailures = [];
761
880
  let webmcp = [];
881
+ let locale = [];
762
882
  let jank = [];
763
883
  const requests = /* @__PURE__ */ new Map();
764
884
  let connection = null;
@@ -916,6 +1036,15 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
916
1036
  if (Array.isArray(result?.value)) phantoms = result.value.map(String);
917
1037
  } catch {
918
1038
  }
1039
+ try {
1040
+ const { result } = await connection.send(
1041
+ "Runtime.evaluate",
1042
+ { expression: DECEPTION_AUDIT, returnByValue: true },
1043
+ sessionId
1044
+ );
1045
+ if (Array.isArray(result?.value)) slop.push(...result.value.map(String));
1046
+ } catch {
1047
+ }
919
1048
  try {
920
1049
  const { result } = await connection.send(
921
1050
  "Runtime.evaluate",
@@ -996,12 +1125,83 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
996
1125
  fs.writeFileSync(outPath, Buffer.from(data, "base64"));
997
1126
  shots.push({ name: viewport.name, path: outPath });
998
1127
  }
1128
+ if (audit) {
1129
+ try {
1130
+ await connection.send(
1131
+ "Emulation.setEmulatedMedia",
1132
+ { features: [{ name: "forced-colors", value: "active" }] },
1133
+ sessionId
1134
+ );
1135
+ await new Promise((resolve) => setTimeout(resolve, 200));
1136
+ const { result } = await connection.send(
1137
+ "Runtime.evaluate",
1138
+ {
1139
+ expression: `(() => {
1140
+ const out = [];
1141
+ for (const el of document.querySelectorAll('button, a, [role="button"], input[type="submit"]')) {
1142
+ if (out.length >= 5) break;
1143
+ const cs = getComputedStyle(el);
1144
+ if (cs.display === 'none' || !el.textContent || !el.textContent.trim()) continue;
1145
+ if (cs.color === cs.backgroundColor) {
1146
+ let label = el.tagName.toLowerCase();
1147
+ if (el.id) label += '#' + el.id; else if (el.classList[0]) label += '.' + el.classList[0];
1148
+ out.push('forced-colors: <' + label + '> text matches its background \u2014 invisible in high-contrast mode');
1149
+ }
1150
+ }
1151
+ return out;
1152
+ })()`,
1153
+ returnByValue: true
1154
+ },
1155
+ sessionId
1156
+ );
1157
+ if (Array.isArray(result?.value)) a11y.push(...result.value.map(String));
1158
+ await connection.send("Emulation.setEmulatedMedia", { features: [] }, sessionId);
1159
+ } catch {
1160
+ }
1161
+ try {
1162
+ await connection.send("Emulation.setEmulatedMedia", { media: "print" }, sessionId);
1163
+ await new Promise((resolve) => setTimeout(resolve, 200));
1164
+ const { result } = await connection.send(
1165
+ "Runtime.evaluate",
1166
+ {
1167
+ expression: `(() => {
1168
+ const out = [];
1169
+ const bodyH = document.body.getBoundingClientRect().height;
1170
+ if (getComputedStyle(document.body).display === 'none' || bodyH < 40) {
1171
+ out.push('print: the page prints blank');
1172
+ }
1173
+ for (const el of document.querySelectorAll('nav, aside')) {
1174
+ if (getComputedStyle(el).display !== 'none' && el.getBoundingClientRect().height > 40) {
1175
+ out.push('print: <' + el.tagName.toLowerCase() + '> still renders in print output');
1176
+ break;
1177
+ }
1178
+ }
1179
+ return out;
1180
+ })()`,
1181
+ returnByValue: true
1182
+ },
1183
+ sessionId
1184
+ );
1185
+ if (Array.isArray(result?.value)) slop.push(...result.value.map(String));
1186
+ await connection.send("Emulation.setEmulatedMedia", { media: "" }, sessionId);
1187
+ } catch {
1188
+ }
1189
+ try {
1190
+ const { result } = await connection.send(
1191
+ "Runtime.evaluate",
1192
+ { expression: LOCALE_AUDIT, returnByValue: true, awaitPromise: true },
1193
+ sessionId
1194
+ );
1195
+ if (Array.isArray(result?.value)) locale = result.value.map(String);
1196
+ } catch {
1197
+ }
1198
+ }
999
1199
  } finally {
1000
1200
  connection?.close();
1001
1201
  child.kill("SIGKILL");
1002
1202
  setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
1003
1203
  }
1004
- return { report, shots, a11y, slop, perf, narration, phantoms, viewTransitions, components, checkFailures, webmcp, jank };
1204
+ return { report, shots, a11y, slop, perf, narration, phantoms, viewTransitions, components, checkFailures, webmcp, jank, locale };
1005
1205
  }
1006
1206
 
1007
1207
  export {
@@ -143,8 +143,8 @@ function stepExpression(step) {
143
143
  async function suggestFlows(cwd, baseUrl, chromePath) {
144
144
  const fs2 = await import("fs");
145
145
  const path2 = await import("path");
146
- const { cdpCapture } = await import("./cdp-DV4D6PUL.js");
147
- const { loadRoutes } = await import("./preview-CDSJ5WBQ.js");
146
+ const { cdpCapture } = await import("./cdp-6YBMFE7M.js");
147
+ const { loadRoutes } = await import("./preview-UV67QLVI.js");
148
148
  const dir = path2.join(cwd, ".squint", "flows");
149
149
  fs2.mkdirSync(dir, { recursive: true });
150
150
  const created = [];
@@ -44,7 +44,7 @@ async function runIntervalSweep(cwd, url, lastRun) {
44
44
  const chrome = findChrome();
45
45
  if (!chrome) return [];
46
46
  const os = await import("os");
47
- const { cdpCapture } = await import("./cdp-DV4D6PUL.js");
47
+ const { cdpCapture } = await import("./cdp-6YBMFE7M.js");
48
48
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "squint-interval-"));
49
49
  try {
50
50
  const result = await cdpCapture(chrome, url, tmp, [], 800, false, due);
@@ -12,7 +12,7 @@ import {
12
12
  comparePulseAttributed,
13
13
  probeRuntime,
14
14
  runtimeSummary
15
- } from "./chunk-PC45PRK3.js";
15
+ } from "./chunk-3ZHETLUC.js";
16
16
  import {
17
17
  enrich
18
18
  } from "./chunk-UBADCBPT.js";
@@ -274,7 +274,7 @@ ${question}`,
274
274
  return;
275
275
  }
276
276
  await this.runTurn(
277
- buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp, result.jank),
277
+ buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp, result.jank, result.locale),
278
278
  `\u{1F441} polish round ${round}/${rounds}`
279
279
  );
280
280
  }
@@ -665,7 +665,7 @@ ${probe.checkFailures.slice(0, 5).join("\n")}`);
665
665
  const captureResult = await this.capture();
666
666
  if (captureResult) {
667
667
  await this.runTurn(
668
- buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms, captureResult.viewTransitions, captureResult.components, captureResult.webmcp, captureResult.jank),
668
+ buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms, captureResult.viewTransitions, captureResult.components, captureResult.webmcp, captureResult.jank, captureResult.locale),
669
669
  "\u{1F441} auto-review rendered UI"
670
670
  );
671
671
  }
@@ -812,7 +812,7 @@ squint verifies every turn \u2014 gates, runtime probe, page audits. Do not decl
812
812
  this.push("image", pulsePath);
813
813
  return null;
814
814
  }
815
- const { previewDir } = await import("./preview-CDSJ5WBQ.js");
815
+ const { previewDir } = await import("./preview-UV67QLVI.js");
816
816
  const diff = await comparePulseAttributed(
817
817
  previous,
818
818
  current,
@@ -1190,7 +1190,7 @@ every ask now carries it; auto-fix presses to ${MAX_GOAL_FIX_ATTEMPTS} attempts
1190
1190
  this.push("error", "no Chrome/Chromium found");
1191
1191
  return;
1192
1192
  }
1193
- const { suggestFlows } = await import("./flows-XORMW4DA.js");
1193
+ const { suggestFlows } = await import("./flows-4QS2ER6N.js");
1194
1194
  this.push("status", "drafting flows from the live routes\u2026");
1195
1195
  const { created, skipped } = await suggestFlows(this.opts.cwd, this.state.devUrl, chrome);
1196
1196
  if (created.length > 0) this.push("status", `drafted ${created.length} flow(s): ${created.join(", ")} \u2014 edit .squint/flows/*.flow or ask the engine to deepen them`);
@@ -1200,7 +1200,7 @@ every ask now carries it; auto-fix presses to ${MAX_GOAL_FIX_ATTEMPTS} attempts
1200
1200
  break;
1201
1201
  }
1202
1202
  void (async () => {
1203
- const { loadFlows } = await import("./flows-XORMW4DA.js");
1203
+ const { loadFlows } = await import("./flows-4QS2ER6N.js");
1204
1204
  const flows = loadFlows(this.opts.cwd);
1205
1205
  if (flows.length === 0) {
1206
1206
  this.push("status", "no flows \u2014 add .squint/flows/<name>.flow (goto/click/fill/press/expect/shot lines), or ask the engine to write one");
@@ -1211,8 +1211,8 @@ every ask now carries it; auto-fix presses to ${MAX_GOAL_FIX_ATTEMPTS} attempts
1211
1211
  this.push("error", "no Chrome/Chromium found for flows");
1212
1212
  return;
1213
1213
  }
1214
- const { runFlow } = await import("./cdp-DV4D6PUL.js");
1215
- const { previewDir } = await import("./preview-CDSJ5WBQ.js");
1214
+ const { runFlow } = await import("./cdp-6YBMFE7M.js");
1215
+ const { previewDir } = await import("./preview-UV67QLVI.js");
1216
1216
  this.push("status", `replaying ${flows.length} flow(s)\u2026`);
1217
1217
  this.notify({ running: true, runStartedAt: Date.now() });
1218
1218
  const failures = [];
@@ -1223,6 +1223,7 @@ every ask now carries it; auto-fix presses to ${MAX_GOAL_FIX_ATTEMPTS} attempts
1223
1223
  if (result.ok) {
1224
1224
  this.push("status", `\u2713 flow ${flow.name} \xB7 ${flow.steps.length} steps${result.shots.length > 0 ? ` \xB7 ${result.shots.length} shot(s)` : ""}`);
1225
1225
  if (result.transitions.length > 0) this.push("status", result.transitions.map((t) => ` ${t}`).join("\n"));
1226
+ if (result.leaks.length > 0) this.push("error", result.leaks.join("\n"));
1226
1227
  for (const shot of result.shots) this.push("image", shot);
1227
1228
  } else {
1228
1229
  const where = result.failedStep ? ` at step ${result.failedStep}` : "";
@@ -1351,7 +1352,7 @@ every ask now carries it; auto-fix presses to ${MAX_GOAL_FIX_ATTEMPTS} attempts
1351
1352
  const result = await this.capture();
1352
1353
  if (result) {
1353
1354
  await this.runTurn(
1354
- buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp, result.jank),
1355
+ buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp, result.jank, result.locale),
1355
1356
  `\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
1356
1357
  );
1357
1358
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  Session
4
- } from "./chunk-ODSCWVBX.js";
4
+ } from "./chunk-B356UR44.js";
5
5
 
6
6
  // src/daemon/client.ts
7
7
  import net from "net";
@@ -122,7 +122,7 @@ function startDaemon(opts) {
122
122
  const state = session.getState();
123
123
  if (!state.devUrl || state.running) return;
124
124
  try {
125
- const { runIntervalSweep } = await import("./checks-4OK5YYGW.js");
125
+ const { runIntervalSweep } = await import("./checks-GTKPEQ2L.js");
126
126
  const failures = await runIntervalSweep(opts.cwd, state.devUrl, lastRun);
127
127
  if (failures.length > 0) {
128
128
  session.note(`\u23F0 interval check(s) failing:
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  Session
4
- } from "./chunk-ODSCWVBX.js";
4
+ } from "./chunk-B356UR44.js";
5
5
  import {
6
6
  completeCommand
7
7
  } from "./chunk-LNJ7O5O4.js";
package/dist/cli.js CHANGED
@@ -3,14 +3,14 @@ import {
3
3
  connectDaemon,
4
4
  socketPath,
5
5
  startDaemon
6
- } from "./chunk-USAZFY36.js";
6
+ } from "./chunk-MDLNTOGX.js";
7
7
  import {
8
8
  App
9
- } from "./chunk-OSKYU4PM.js";
10
- import "./chunk-ODSCWVBX.js";
9
+ } from "./chunk-PPC2KM7P.js";
10
+ import "./chunk-B356UR44.js";
11
11
  import "./chunk-YKEAKVIS.js";
12
12
  import "./chunk-LNJ7O5O4.js";
13
- import "./chunk-PC45PRK3.js";
13
+ import "./chunk-3ZHETLUC.js";
14
14
  import "./chunk-UBADCBPT.js";
15
15
  import "./chunk-IEQGTVWN.js";
16
16
  import "./chunk-AUJJGMZG.js";
@@ -25,13 +25,13 @@ import {
25
25
  } from "./chunk-WAJXATCO.js";
26
26
  import "./chunk-62JNF5M2.js";
27
27
  import "./chunk-7CAGWFAQ.js";
28
- import "./chunk-HFQZDT65.js";
28
+ import "./chunk-4DZAWPS2.js";
29
29
  import "./chunk-55JZKI2M.js";
30
30
  import {
31
31
  detectEngines,
32
32
  getEngine
33
33
  } from "./chunk-HIUQ4FXO.js";
34
- import "./chunk-GAWQ7YXC.js";
34
+ import "./chunk-6RMF4NAW.js";
35
35
 
36
36
  // src/cli.tsx
37
37
  import { Command } from "commander";
@@ -188,18 +188,18 @@ function registerDaemon(program2) {
188
188
  process.on("SIGTERM", stop);
189
189
  });
190
190
  program2.command("mcp").description("serve the gates as MCP tools over stdio (check, shot, flows, context)").action(async () => {
191
- const { runMcpServer } = await import("./server-PSXRO2XN.js");
191
+ const { runMcpServer } = await import("./server-WOYEQLAA.js");
192
192
  runMcpServer(process.cwd());
193
193
  });
194
194
  program2.command("attach").description("attach this terminal to a running squint daemon (full TUI; --plain for line mode)").option("--plain", "line-mode attach instead of the full TUI").action(async (opts) => {
195
195
  const cwd = process.cwd();
196
196
  if (!opts.plain) {
197
197
  try {
198
- const { RemoteSession } = await import("./remote-WLUNMQRO.js");
198
+ const { RemoteSession } = await import("./remote-ICNK2PPK.js");
199
199
  const remote = await RemoteSession.connect(cwd);
200
200
  const config = loadConfig(defaultPaths(cwd));
201
201
  const { render: render2 } = await import("ink");
202
- const { App: App2 } = await import("./App-I2MB7S6W.js");
202
+ const { App: App2 } = await import("./App-C3LOYRTR.js");
203
203
  const React = await import("react");
204
204
  render2(
205
205
  React.createElement(App2, {
@@ -316,7 +316,7 @@ function registerEnv(program2) {
316
316
  }
317
317
  }
318
318
  const { findChrome } = await import("./chrome-BQ4WOCOH.js");
319
- const { hasWebSocket } = await import("./cdp-DV4D6PUL.js");
319
+ const { hasWebSocket } = await import("./cdp-6YBMFE7M.js");
320
320
  const chrome = findChrome();
321
321
  console.log(
322
322
  chrome ? `${pc3.green("\u2713")} Chrome ${pc3.dim(chrome)}` : `${pc3.yellow("\u25CB")} Chrome ${pc3.dim("\u2014 screenshots and runtime probing disabled")}`
@@ -552,7 +552,7 @@ function registerQuality(program2) {
552
552
  report.gates = gateResults.map((r) => ({ id: r.gate.id, ok: r.ok, durationMs: r.durationMs, outputTail: r.ok ? void 0 : r.outputTail }));
553
553
  if (gateResults.some((r) => !r.ok)) failed = true;
554
554
  if (opts.url) {
555
- const { captureViewports, runtimeSummary, previewDir } = await import("./preview-CDSJ5WBQ.js");
555
+ const { captureViewports, runtimeSummary, previewDir } = await import("./preview-UV67QLVI.js");
556
556
  const capture = await captureViewports(cwd, opts.url);
557
557
  if (!capture) {
558
558
  console.log(pc5.red("\u2717 audit: capture failed (Chrome missing or URL unreachable)"));
@@ -584,14 +584,14 @@ function registerQuality(program2) {
584
584
  console.log(`${pc5.green("\u2713")} audit ${pc5.dim(`(${advisories.a11y.length} a11y, ${advisories.slop.length} slop, ${advisories.jank.length} jank advisories)`)}`);
585
585
  }
586
586
  }
587
- const { loadFlows } = await import("./flows-XORMW4DA.js");
587
+ const { loadFlows } = await import("./flows-4QS2ER6N.js");
588
588
  const flows = loadFlows(cwd);
589
589
  if (flows.length > 0) {
590
590
  const { findChrome } = await import("./chrome-BQ4WOCOH.js");
591
591
  const chrome = findChrome();
592
592
  const flowReport = [];
593
593
  if (chrome) {
594
- const { runFlow } = await import("./cdp-DV4D6PUL.js");
594
+ const { runFlow } = await import("./cdp-6YBMFE7M.js");
595
595
  for (const flow of flows) {
596
596
  const result = await runFlow(chrome, opts.url, flow, previewDir(cwd));
597
597
  flowReport.push({ name: flow.name, ok: result.ok, detail: result.ok ? void 0 : `step ${result.failedStep}: ${result.detail}` });
@@ -605,7 +605,7 @@ function registerQuality(program2) {
605
605
  report.ok = !failed;
606
606
  report.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
607
607
  try {
608
- const { writeReceipt } = await import("./receipts-HMBEL7NM.js");
608
+ const { writeReceipt } = await import("./receipts-7PERXBJO.js");
609
609
  console.log(pc5.dim(`receipt \u2192 ${writeReceipt(cwd, report)}`));
610
610
  } catch {
611
611
  }
@@ -617,7 +617,7 @@ function registerQuality(program2) {
617
617
  if (failed) process.exitCode = 1;
618
618
  });
619
619
  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) => {
620
- const { captureViewports } = await import("./preview-CDSJ5WBQ.js");
620
+ const { captureViewports } = await import("./preview-UV67QLVI.js");
621
621
  const result = await captureViewports(process.cwd(), url);
622
622
  if (!result) {
623
623
  console.error(pc5.red("\u2717 no Chrome/Chromium found"));
@@ -804,7 +804,7 @@ function registerTui(program2) {
804
804
  }
805
805
 
806
806
  // src/cli.tsx
807
- var VERSION = true ? "0.8.0" : "0.0.0-dev";
807
+ var VERSION = true ? "0.8.2" : "0.0.0-dev";
808
808
  var program = new Command();
809
809
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
810
810
  registerRun(program);
@@ -4,7 +4,7 @@ import {
4
4
  parseFlow,
5
5
  stepExpression,
6
6
  suggestFlows
7
- } from "./chunk-WOPJFP4M.js";
7
+ } from "./chunk-53DPUAXN.js";
8
8
  export {
9
9
  loadFlows,
10
10
  parseFlow,
@@ -11,12 +11,12 @@ import {
11
11
  probeRuntime,
12
12
  routeShotName,
13
13
  runtimeSummary
14
- } from "./chunk-PC45PRK3.js";
14
+ } from "./chunk-3ZHETLUC.js";
15
15
  import "./chunk-7CAGWFAQ.js";
16
- import "./chunk-HFQZDT65.js";
16
+ import "./chunk-4DZAWPS2.js";
17
17
  import "./chunk-55JZKI2M.js";
18
18
  import "./chunk-HIUQ4FXO.js";
19
- import "./chunk-GAWQ7YXC.js";
19
+ import "./chunk-6RMF4NAW.js";
20
20
  export {
21
21
  VIEWPORTS,
22
22
  buildReviewPrompt,
@@ -31,7 +31,7 @@ function buildReceipt(cwd, report) {
31
31
  }
32
32
  }
33
33
  const unsigned = {
34
- version: true ? "0.8.0" : "0.0.0-dev",
34
+ version: true ? "0.8.2" : "0.0.0-dev",
35
35
  node: process.version,
36
36
  gitHead,
37
37
  report,
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  connectDaemon,
4
4
  socketPath
5
- } from "./chunk-USAZFY36.js";
6
- import "./chunk-ODSCWVBX.js";
5
+ } from "./chunk-MDLNTOGX.js";
6
+ import "./chunk-B356UR44.js";
7
7
  import "./chunk-YKEAKVIS.js";
8
- import "./chunk-PC45PRK3.js";
8
+ import "./chunk-3ZHETLUC.js";
9
9
  import "./chunk-UBADCBPT.js";
10
10
  import "./chunk-IEQGTVWN.js";
11
11
  import "./chunk-AUJJGMZG.js";
@@ -16,10 +16,10 @@ import "./chunk-K5QJMSJH.js";
16
16
  import "./chunk-WAJXATCO.js";
17
17
  import "./chunk-62JNF5M2.js";
18
18
  import "./chunk-7CAGWFAQ.js";
19
- import "./chunk-HFQZDT65.js";
19
+ import "./chunk-4DZAWPS2.js";
20
20
  import "./chunk-55JZKI2M.js";
21
21
  import "./chunk-HIUQ4FXO.js";
22
- import "./chunk-GAWQ7YXC.js";
22
+ import "./chunk-6RMF4NAW.js";
23
23
 
24
24
  // src/daemon/remote.ts
25
25
  var noteId = 0;
@@ -7,7 +7,7 @@ import {
7
7
  captureViewports,
8
8
  previewDir,
9
9
  runtimeSummary
10
- } from "./chunk-PC45PRK3.js";
10
+ } from "./chunk-3ZHETLUC.js";
11
11
  import "./chunk-UBADCBPT.js";
12
12
  import "./chunk-IEQGTVWN.js";
13
13
  import "./chunk-WAJXATCO.js";
@@ -18,13 +18,13 @@ import {
18
18
  import "./chunk-7CAGWFAQ.js";
19
19
  import {
20
20
  loadFlows
21
- } from "./chunk-WOPJFP4M.js";
22
- import "./chunk-HFQZDT65.js";
21
+ } from "./chunk-53DPUAXN.js";
22
+ import "./chunk-4DZAWPS2.js";
23
23
  import {
24
24
  findChrome
25
25
  } from "./chunk-55JZKI2M.js";
26
26
  import "./chunk-HIUQ4FXO.js";
27
- import "./chunk-GAWQ7YXC.js";
27
+ import "./chunk-6RMF4NAW.js";
28
28
 
29
29
  // src/mcp/server.ts
30
30
  import readline from "readline";
@@ -58,6 +58,7 @@ ${r.outputTail.slice(-1500)}`}`).join("\n");
58
58
  ["phantom classes", result.phantoms],
59
59
  ["view transitions", result.viewTransitions],
60
60
  ["jank", result.jank],
61
+ ["locale", result.locale],
61
62
  ["components", result.components]
62
63
  ]) {
63
64
  if (list && list.length > 0) sections.push(`${label}:
@@ -76,13 +77,14 @@ ${list.join("\n")}`);
76
77
  if (flows.length === 0) return "no flows declared \u2014 add .squint/flows/<name>.flow";
77
78
  const chrome = findChrome();
78
79
  if (!chrome) return "no Chrome/Chromium found";
79
- const { runFlow } = await import("./cdp-DV4D6PUL.js");
80
+ const { runFlow } = await import("./cdp-6YBMFE7M.js");
80
81
  const lines = [];
81
82
  for (const flow of flows) {
82
83
  const result = await runFlow(chrome, url, flow, previewDir(cwd));
83
84
  lines.push(
84
85
  result.ok ? `\u2713 ${flow.name} (${flow.steps.length} steps)${result.transitions.length > 0 ? `
85
- ${result.transitions.map((t) => ` ${t}`).join("\n")}` : ""}` : `\u2717 ${flow.name} at step ${result.failedStep}: ${result.detail}`
86
+ ${result.transitions.map((t) => ` ${t}`).join("\n")}` : ""}${result.leaks.length > 0 ? `
87
+ ${result.leaks.map((l) => ` ${l}`).join("\n")}` : ""}` : `\u2717 ${flow.name} at step ${result.failedStep}: ${result.detail}`
86
88
  );
87
89
  }
88
90
  return lines.join("\n");
@@ -95,7 +97,7 @@ ${result.transitions.map((t) => ` ${t}`).join("\n")}` : ""}` : `\u2717 ${flow.n
95
97
  async run(args, cwd) {
96
98
  const chrome = findChrome();
97
99
  if (!chrome) return "no Chrome/Chromium found";
98
- const { suggestFlows } = await import("./flows-XORMW4DA.js");
100
+ const { suggestFlows } = await import("./flows-4QS2ER6N.js");
99
101
  const { created, skipped } = await suggestFlows(cwd, String(args.url ?? ""), chrome);
100
102
  return [
101
103
  created.length > 0 ? `drafted: ${created.join(", ")}` : "nothing drafted",
@@ -110,7 +112,7 @@ ${result.transitions.map((t) => ` ${t}`).join("\n")}` : ""}` : `\u2717 ${flow.n
110
112
  async run(args, cwd) {
111
113
  const fs = await import("fs");
112
114
  const path = await import("path");
113
- const { verifyReceipt } = await import("./receipts-HMBEL7NM.js");
115
+ const { verifyReceipt } = await import("./receipts-7PERXBJO.js");
114
116
  const file = path.isAbsolute(String(args.path)) ? String(args.path) : path.join(cwd, String(args.path));
115
117
  let receipt;
116
118
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aayambansal/squint",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and other coding agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",