@aayambansal/squint 0.4.7 → 0.4.8
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 +3 -0
- package/dist/{App-HWMVSRMF.js → App-766KSGON.js} +4 -4
- package/dist/{cdp-PBQKXC6R.js → cdp-2VVQOHWM.js} +3 -1
- package/dist/{chunk-ZSDCMYZR.js → chunk-7RFQWOQV.js} +23 -6
- package/dist/{chunk-NX7XQY2X.js → chunk-ATNTD3V5.js} +1 -1
- package/dist/{chunk-HC4E42SV.js → chunk-BGBG2MNY.js} +187 -1
- package/dist/{chunk-M25IQ7C7.js → chunk-JZMJGLIZ.js} +1 -1
- package/dist/{chunk-7MOKOZOR.js → chunk-YGPVP4YB.js} +23 -10
- package/dist/cli.js +10 -10
- package/dist/{preview-WT34NVCH.js → preview-FBNVDQIV.js} +4 -2
- package/dist/{remote-6O6FPTUF.js → remote-OH2P4S2H.js} +4 -4
- package/dist/sentinel-42NZRTTS.js +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -174,6 +174,9 @@ squint doctor --probe # run every engine end to end, verify auth act
|
|
|
174
174
|
flagged from the live page, and on Next 16+ the framework's own `/_next/mcp`
|
|
175
175
|
channel feeds structured errors straight into the fix loop. `/context` itemizes
|
|
176
176
|
the injected-context bill per source, with staleness warnings.
|
|
177
|
+
- **The sentinel**: gate evasion (deleted tests, added skips, suppressed
|
|
178
|
+
diagnostics, weakened checks, locked-path touches) detected deterministically per
|
|
179
|
+
turn and reported to you — never sent back to the engine that did it.
|
|
177
180
|
- **Detachable sessions**: `squint serve` owns the session on a unix socket;
|
|
178
181
|
`squint attach` joins from any terminal (observer seats for teammates, driver
|
|
179
182
|
seat inherited on detach). Crash-proof, ssh-friendly, no cloud.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
App
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-JZMJGLIZ.js";
|
|
5
5
|
import "./chunk-43NQNIJY.js";
|
|
6
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-YGPVP4YB.js";
|
|
7
7
|
import "./chunk-ARDV4XH6.js";
|
|
8
8
|
import "./chunk-AUJJGMZG.js";
|
|
9
9
|
import "./chunk-YHRAOBI2.js";
|
|
@@ -13,9 +13,9 @@ import "./chunk-VH7OOFQP.js";
|
|
|
13
13
|
import "./chunk-K5QJMSJH.js";
|
|
14
14
|
import "./chunk-WAJXATCO.js";
|
|
15
15
|
import "./chunk-62JNF5M2.js";
|
|
16
|
-
import "./chunk-
|
|
16
|
+
import "./chunk-7RFQWOQV.js";
|
|
17
17
|
import "./chunk-7CAGWFAQ.js";
|
|
18
|
-
import "./chunk-
|
|
18
|
+
import "./chunk-BGBG2MNY.js";
|
|
19
19
|
import "./chunk-IMDRXXFU.js";
|
|
20
20
|
import "./chunk-KVYGPLWW.js";
|
|
21
21
|
export {
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
import {
|
|
3
3
|
cdpCapture,
|
|
4
4
|
hasWebSocket,
|
|
5
|
+
pixelDiffAttributed,
|
|
5
6
|
pixelDiffPct,
|
|
6
7
|
runFlow
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-BGBG2MNY.js";
|
|
8
9
|
export {
|
|
9
10
|
cdpCapture,
|
|
10
11
|
hasWebSocket,
|
|
12
|
+
pixelDiffAttributed,
|
|
11
13
|
pixelDiffPct,
|
|
12
14
|
runFlow
|
|
13
15
|
};
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
cdpCapture,
|
|
7
7
|
hasWebSocket
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-BGBG2MNY.js";
|
|
9
9
|
import {
|
|
10
10
|
findChrome,
|
|
11
11
|
screenshot
|
|
@@ -75,7 +75,7 @@ async function captureViewports(cwd, url) {
|
|
|
75
75
|
if (hasWebSocket()) {
|
|
76
76
|
try {
|
|
77
77
|
const checks = loadChecks(cwd);
|
|
78
|
-
const { report, shots: shots2, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true, checks);
|
|
78
|
+
const { report, shots: shots2, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp, jank } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true, checks);
|
|
79
79
|
const errors2 = [];
|
|
80
80
|
for (const route of routes.slice(1)) {
|
|
81
81
|
try {
|
|
@@ -90,7 +90,7 @@ async function captureViewports(cwd, url) {
|
|
|
90
90
|
errors2.push(`${route}: capture failed`);
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
-
return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp };
|
|
93
|
+
return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp, jank };
|
|
94
94
|
} catch {
|
|
95
95
|
}
|
|
96
96
|
}
|
|
@@ -163,9 +163,15 @@ async function probeRuntime(url, cwd) {
|
|
|
163
163
|
async function comparePulse(previous, current) {
|
|
164
164
|
const chrome = findChrome();
|
|
165
165
|
if (!chrome || !hasWebSocket()) return null;
|
|
166
|
-
const { pixelDiffPct } = await import("./cdp-
|
|
166
|
+
const { pixelDiffPct } = await import("./cdp-2VVQOHWM.js");
|
|
167
167
|
return pixelDiffPct(chrome, previous, current);
|
|
168
168
|
}
|
|
169
|
+
async function comparePulseAttributed(previous, current, url) {
|
|
170
|
+
const chrome = findChrome();
|
|
171
|
+
if (!chrome || !hasWebSocket()) return null;
|
|
172
|
+
const { pixelDiffAttributed } = await import("./cdp-2VVQOHWM.js");
|
|
173
|
+
return pixelDiffAttributed(chrome, previous, current, url);
|
|
174
|
+
}
|
|
169
175
|
function buildRuntimeFixPrompt(report) {
|
|
170
176
|
return `The running app has runtime problems.${runtimeSection(report)}
|
|
171
177
|
|
|
@@ -191,6 +197,16 @@ ${narration.join("\n")}
|
|
|
191
197
|
|
|
192
198
|
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.`;
|
|
193
199
|
}
|
|
200
|
+
function jankSection(jank) {
|
|
201
|
+
if (!jank || jank.length === 0) return "";
|
|
202
|
+
return `
|
|
203
|
+
|
|
204
|
+
## Jank attribution (main-thread frames \u226550ms)
|
|
205
|
+
|
|
206
|
+
${jank.join("\n")}
|
|
207
|
+
|
|
208
|
+
Each line names the function behind a long animation frame observed during load and a scripted scroll. Fix the work (memoize, virtualize, move off the main thread) \u2014 do not remove the animation.`;
|
|
209
|
+
}
|
|
194
210
|
function webmcpSection(webmcp) {
|
|
195
211
|
if (!webmcp || webmcp.length === 0) return "";
|
|
196
212
|
return `
|
|
@@ -241,13 +257,13 @@ ${findings.join("\n")}
|
|
|
241
257
|
|
|
242
258
|
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.`;
|
|
243
259
|
}
|
|
244
|
-
function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms, viewTransitions, components, webmcp) {
|
|
260
|
+
function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms, viewTransitions, components, webmcp, jank) {
|
|
245
261
|
const list = shots.map((s) => `- ${s.name}: ${s.path}`).join("\n");
|
|
246
262
|
return `Screenshots of the running app were just captured:
|
|
247
263
|
|
|
248
264
|
${list}
|
|
249
265
|
|
|
250
|
-
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)}`;
|
|
266
|
+
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)}`;
|
|
251
267
|
}
|
|
252
268
|
|
|
253
269
|
export {
|
|
@@ -259,6 +275,7 @@ export {
|
|
|
259
275
|
runtimeSummary,
|
|
260
276
|
probeRuntime,
|
|
261
277
|
comparePulse,
|
|
278
|
+
comparePulseAttributed,
|
|
262
279
|
buildRuntimeFixPrompt,
|
|
263
280
|
buildReviewPrompt
|
|
264
281
|
};
|
|
@@ -189,6 +189,7 @@ async function runFlow(chromePath, baseUrl, flow, outDir) {
|
|
|
189
189
|
const { sessionId } = await connection.send("Target.attachToTarget", { targetId, flatten: true });
|
|
190
190
|
await connection.send("Page.enable", {}, sessionId);
|
|
191
191
|
await connection.send("Page.addScriptToEvaluateOnNewDocument", { source: WEBMCP_SHIM }, sessionId).catch(() => null);
|
|
192
|
+
await connection.send("Page.addScriptToEvaluateOnNewDocument", { source: LOAF_SHIM }, sessionId).catch(() => null);
|
|
192
193
|
await connection.send(
|
|
193
194
|
"Emulation.setDeviceMetricsOverride",
|
|
194
195
|
{ width: 1280, height: 800, deviceScaleFactor: 1, mobile: false },
|
|
@@ -237,6 +238,145 @@ async function runFlow(chromePath, baseUrl, flow, outDir) {
|
|
|
237
238
|
setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
|
|
238
239
|
}
|
|
239
240
|
}
|
|
241
|
+
async function pixelDiffAttributed(chromePath, pngA, pngB, url) {
|
|
242
|
+
const { child, wsUrl, profileDir } = await launchChrome(chromePath);
|
|
243
|
+
let connection = null;
|
|
244
|
+
try {
|
|
245
|
+
connection = await CdpConnection.connect(wsUrl, 1e4);
|
|
246
|
+
const { targetId } = await connection.send("Target.createTarget", { url: "about:blank" });
|
|
247
|
+
const { sessionId } = await connection.send("Target.attachToTarget", { targetId, flatten: true });
|
|
248
|
+
await connection.send("Runtime.enable", {}, sessionId);
|
|
249
|
+
const expression = `(async () => {
|
|
250
|
+
const load = (src) => new Promise((resolve, reject) => {
|
|
251
|
+
const img = new Image();
|
|
252
|
+
img.onload = () => resolve(img);
|
|
253
|
+
img.onerror = () => reject(new Error('decode'));
|
|
254
|
+
img.src = src;
|
|
255
|
+
});
|
|
256
|
+
const a = await load('data:image/png;base64,${pngA.toString("base64")}');
|
|
257
|
+
const b = await load('data:image/png;base64,${pngB.toString("base64")}');
|
|
258
|
+
const w = Math.min(a.width, b.width), h = Math.min(a.height, b.height);
|
|
259
|
+
if (w === 0 || h === 0) return null;
|
|
260
|
+
const draw = (img) => {
|
|
261
|
+
const c = document.createElement('canvas');
|
|
262
|
+
c.width = w; c.height = h;
|
|
263
|
+
const ctx = c.getContext('2d');
|
|
264
|
+
ctx.drawImage(img, 0, 0);
|
|
265
|
+
return ctx.getImageData(0, 0, w, h).data;
|
|
266
|
+
};
|
|
267
|
+
const da = draw(a), db = draw(b);
|
|
268
|
+
const CELL = 32;
|
|
269
|
+
const gw = Math.ceil(w / CELL), gh = Math.ceil(h / CELL);
|
|
270
|
+
const marked = new Uint8Array(gw * gh);
|
|
271
|
+
let differ = 0, total = 0;
|
|
272
|
+
for (let y = 0; y < h; y += 2) {
|
|
273
|
+
for (let x = 0; x < w; x += 2) {
|
|
274
|
+
total++;
|
|
275
|
+
const i = (y * w + x) * 4;
|
|
276
|
+
if (Math.abs(da[i] - db[i]) > 8 || Math.abs(da[i + 1] - db[i + 1]) > 8 || Math.abs(da[i + 2] - db[i + 2]) > 8) {
|
|
277
|
+
differ++;
|
|
278
|
+
marked[Math.floor(y / CELL) * gw + Math.floor(x / CELL)] = 1;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Flood-fill marked cells into region bounding boxes.
|
|
283
|
+
const seen = new Uint8Array(gw * gh);
|
|
284
|
+
const regions = [];
|
|
285
|
+
for (let cy = 0; cy < gh; cy++) {
|
|
286
|
+
for (let cx = 0; cx < gw; cx++) {
|
|
287
|
+
const start = cy * gw + cx;
|
|
288
|
+
if (!marked[start] || seen[start]) continue;
|
|
289
|
+
let minX = cx, maxX = cx, minY = cy, maxY = cy, cells = 0;
|
|
290
|
+
const stack = [start];
|
|
291
|
+
seen[start] = 1;
|
|
292
|
+
while (stack.length) {
|
|
293
|
+
const cell = stack.pop();
|
|
294
|
+
const x = cell % gw, y = Math.floor(cell / gw);
|
|
295
|
+
cells++;
|
|
296
|
+
if (x < minX) minX = x; if (x > maxX) maxX = x;
|
|
297
|
+
if (y < minY) minY = y; if (y > maxY) maxY = y;
|
|
298
|
+
for (const [dx, dy] of [[1,0],[-1,0],[0,1],[0,-1]]) {
|
|
299
|
+
const nx = x + dx, ny = y + dy;
|
|
300
|
+
if (nx < 0 || ny < 0 || nx >= gw || ny >= gh) continue;
|
|
301
|
+
const n = ny * gw + nx;
|
|
302
|
+
if (marked[n] && !seen[n]) { seen[n] = 1; stack.push(n); }
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
regions.push({
|
|
306
|
+
x: minX * CELL, y: minY * CELL,
|
|
307
|
+
w: Math.min(w, (maxX + 1) * CELL) - minX * CELL,
|
|
308
|
+
h: Math.min(h, (maxY + 1) * CELL) - minY * CELL,
|
|
309
|
+
cells,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
regions.sort((p, q) => q.cells - p.cells);
|
|
314
|
+
const sizePenalty = (a.width !== b.width || a.height !== b.height) ? 1 : 0;
|
|
315
|
+
return { pct: Math.min(100, (differ / total) * 100 + sizePenalty), regions: regions.slice(0, 5), w, h };
|
|
316
|
+
})()`;
|
|
317
|
+
const { result } = await connection.send(
|
|
318
|
+
"Runtime.evaluate",
|
|
319
|
+
{ expression, awaitPromise: true, returnByValue: true },
|
|
320
|
+
sessionId
|
|
321
|
+
);
|
|
322
|
+
const value = result?.value;
|
|
323
|
+
if (!value || typeof value.pct !== "number") return null;
|
|
324
|
+
if (!url || value.regions.length === 0 || value.pct < 0.5) return { pct: value.pct, sentences: [] };
|
|
325
|
+
let sentences = [];
|
|
326
|
+
try {
|
|
327
|
+
const page = await connection.send("Target.createTarget", { url: "about:blank" });
|
|
328
|
+
const attach = await connection.send("Target.attachToTarget", { targetId: page.targetId, flatten: true });
|
|
329
|
+
const pageSession = attach.sessionId;
|
|
330
|
+
await connection.send("Runtime.enable", {}, pageSession);
|
|
331
|
+
await connection.send("Page.enable", {}, pageSession);
|
|
332
|
+
await connection.send(
|
|
333
|
+
"Emulation.setDeviceMetricsOverride",
|
|
334
|
+
{ width: value.w, height: value.h, deviceScaleFactor: 1, mobile: false },
|
|
335
|
+
pageSession
|
|
336
|
+
);
|
|
337
|
+
await connection.send("Page.navigate", { url }, pageSession);
|
|
338
|
+
await new Promise((resolve) => setTimeout(resolve, 1200));
|
|
339
|
+
const hitTest = `((regions) => regions.map((r) => {
|
|
340
|
+
const el = document.elementsFromPoint(r.x + r.w / 2, r.y + r.h / 2)
|
|
341
|
+
.find((e) => e !== document.documentElement && e !== document.body);
|
|
342
|
+
const where = r.w + '\xD7' + r.h + ' region @ (' + r.x + ',' + r.y + ')';
|
|
343
|
+
if (!el) return where + ' changed';
|
|
344
|
+
let label = el.tagName.toLowerCase();
|
|
345
|
+
if (el.id) label += '#' + el.id;
|
|
346
|
+
else if (el.classList[0]) label += '.' + el.classList[0];
|
|
347
|
+
const key = Object.keys(el).find((k) => k.startsWith('__reactFiber$'));
|
|
348
|
+
let chain = '';
|
|
349
|
+
if (key) {
|
|
350
|
+
let fiber = el[key];
|
|
351
|
+
const names = [];
|
|
352
|
+
let hops = 0;
|
|
353
|
+
while (fiber && hops < 50 && names.length < 2) {
|
|
354
|
+
const t = fiber.type;
|
|
355
|
+
const n = typeof t === 'function' ? (t.displayName || t.name || '') : '';
|
|
356
|
+
if (n && !names.includes(n)) names.push(n);
|
|
357
|
+
fiber = fiber.return; hops++;
|
|
358
|
+
}
|
|
359
|
+
chain = names.join(' < ');
|
|
360
|
+
}
|
|
361
|
+
return '<' + label + '>' + (chain ? ' (' + chain + ')' : '') + ': ' + where + ' changed';
|
|
362
|
+
}))(${JSON.stringify(value.regions)})`;
|
|
363
|
+
const hit = await connection.send(
|
|
364
|
+
"Runtime.evaluate",
|
|
365
|
+
{ expression: hitTest, returnByValue: true },
|
|
366
|
+
pageSession
|
|
367
|
+
);
|
|
368
|
+
if (Array.isArray(hit.result?.value)) sentences = hit.result.value.map(String);
|
|
369
|
+
} catch {
|
|
370
|
+
}
|
|
371
|
+
return { pct: value.pct, sentences };
|
|
372
|
+
} catch {
|
|
373
|
+
return null;
|
|
374
|
+
} finally {
|
|
375
|
+
connection?.close();
|
|
376
|
+
child.kill("SIGKILL");
|
|
377
|
+
setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
240
380
|
async function pixelDiffPct(chromePath, pngA, pngB) {
|
|
241
381
|
const { child, wsUrl, profileDir } = await launchChrome(chromePath);
|
|
242
382
|
let connection = null;
|
|
@@ -457,6 +597,40 @@ var WEBMCP_SHIM = `(() => {
|
|
|
457
597
|
wrap(document.modelContext || (document.modelContext = {}));
|
|
458
598
|
wrap(navigator.modelContext || (navigator.modelContext = {}));
|
|
459
599
|
})()`;
|
|
600
|
+
var LOAF_SHIM = `(() => {
|
|
601
|
+
window.__squintLoaf = [];
|
|
602
|
+
try {
|
|
603
|
+
new PerformanceObserver((list) => {
|
|
604
|
+
for (const e of list.getEntries()) {
|
|
605
|
+
if (e.duration < 50) continue;
|
|
606
|
+
const s = (e.scripts && e.scripts[0]) || {};
|
|
607
|
+
window.__squintLoaf.push({
|
|
608
|
+
duration: Math.round(e.duration),
|
|
609
|
+
fn: s.sourceFunctionName || '',
|
|
610
|
+
url: (s.sourceURL || s.name || '').split('/').pop() || '',
|
|
611
|
+
invoker: s.invoker || '',
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}).observe({ type: 'long-animation-frame', buffered: true });
|
|
615
|
+
} catch {}
|
|
616
|
+
})()`;
|
|
617
|
+
var SCRIPTED_SCROLL = `(async () => {
|
|
618
|
+
const half = Math.max(0, (document.body.scrollHeight - innerHeight) / 2);
|
|
619
|
+
window.scrollTo({ top: half, behavior: 'smooth' });
|
|
620
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
621
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
622
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
623
|
+
const seen = new Map();
|
|
624
|
+
for (const e of window.__squintLoaf || []) {
|
|
625
|
+
const key = e.fn + '@' + e.url + '@' + e.invoker;
|
|
626
|
+
const prev = seen.get(key);
|
|
627
|
+
if (!prev || e.duration > prev.duration) seen.set(key, e);
|
|
628
|
+
}
|
|
629
|
+
return [...seen.values()]
|
|
630
|
+
.sort((a, b) => b.duration - a.duration)
|
|
631
|
+
.slice(0, 6)
|
|
632
|
+
.map((e) => e.duration + 'ms frame \u2014 ' + (e.fn || e.invoker || 'script') + (e.url ? ' @ ' + e.url : ''));
|
|
633
|
+
})()`;
|
|
460
634
|
async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, audit = false, checks = []) {
|
|
461
635
|
const { child, wsUrl, profileDir } = await launchChrome(chromePath);
|
|
462
636
|
const report = { consoleErrors: [], pageErrors: [], failedRequests: [] };
|
|
@@ -470,6 +644,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
470
644
|
let components = [];
|
|
471
645
|
const checkFailures = [];
|
|
472
646
|
let webmcp = [];
|
|
647
|
+
let jank = [];
|
|
473
648
|
const requests = /* @__PURE__ */ new Map();
|
|
474
649
|
let connection = null;
|
|
475
650
|
try {
|
|
@@ -509,6 +684,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
509
684
|
await connection.send("Network.enable", {}, sessionId);
|
|
510
685
|
await connection.send("Page.enable", {}, sessionId);
|
|
511
686
|
await connection.send("Page.addScriptToEvaluateOnNewDocument", { source: WEBMCP_SHIM }, sessionId).catch(() => null);
|
|
687
|
+
await connection.send("Page.addScriptToEvaluateOnNewDocument", { source: LOAF_SHIM }, sessionId).catch(() => null);
|
|
512
688
|
await connection.send("Page.navigate", { url }, sessionId);
|
|
513
689
|
const deadline = Date.now() + 12e3;
|
|
514
690
|
while (!loaded && Date.now() < deadline) {
|
|
@@ -524,6 +700,15 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
524
700
|
if (result?.value && typeof result.value === "object") perf = result.value;
|
|
525
701
|
} catch {
|
|
526
702
|
}
|
|
703
|
+
try {
|
|
704
|
+
const { result } = await connection.send(
|
|
705
|
+
"Runtime.evaluate",
|
|
706
|
+
{ expression: SCRIPTED_SCROLL, returnByValue: true, awaitPromise: true },
|
|
707
|
+
sessionId
|
|
708
|
+
);
|
|
709
|
+
if (Array.isArray(result?.value)) jank = result.value.map(String);
|
|
710
|
+
} catch {
|
|
711
|
+
}
|
|
527
712
|
try {
|
|
528
713
|
const { result } = await connection.send(
|
|
529
714
|
"Runtime.evaluate",
|
|
@@ -659,12 +844,13 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
659
844
|
child.kill("SIGKILL");
|
|
660
845
|
setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
|
|
661
846
|
}
|
|
662
|
-
return { report, shots, a11y, slop, perf, narration, phantoms, viewTransitions, components, checkFailures, webmcp };
|
|
847
|
+
return { report, shots, a11y, slop, perf, narration, phantoms, viewTransitions, components, checkFailures, webmcp, jank };
|
|
663
848
|
}
|
|
664
849
|
|
|
665
850
|
export {
|
|
666
851
|
hasWebSocket,
|
|
667
852
|
runFlow,
|
|
853
|
+
pixelDiffAttributed,
|
|
668
854
|
pixelDiffPct,
|
|
669
855
|
cdpCapture
|
|
670
856
|
};
|
|
@@ -46,10 +46,10 @@ import {
|
|
|
46
46
|
buildReviewPrompt,
|
|
47
47
|
buildRuntimeFixPrompt,
|
|
48
48
|
captureViewports,
|
|
49
|
-
|
|
49
|
+
comparePulseAttributed,
|
|
50
50
|
probeRuntime,
|
|
51
51
|
runtimeSummary
|
|
52
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-7RFQWOQV.js";
|
|
53
53
|
import {
|
|
54
54
|
clearState,
|
|
55
55
|
loadState,
|
|
@@ -267,7 +267,7 @@ ${question}`,
|
|
|
267
267
|
return;
|
|
268
268
|
}
|
|
269
269
|
await this.runTurn(
|
|
270
|
-
buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp),
|
|
270
|
+
buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp, result.jank),
|
|
271
271
|
`\u{1F441} polish round ${round}/${rounds}`
|
|
272
272
|
);
|
|
273
273
|
}
|
|
@@ -509,6 +509,17 @@ ${driftSummary(drift)}`);
|
|
|
509
509
|
}
|
|
510
510
|
} catch {
|
|
511
511
|
}
|
|
512
|
+
try {
|
|
513
|
+
const { scanEvasion, sentinelSummary } = await import("./sentinel-42NZRTTS.js");
|
|
514
|
+
const evasions = scanEvasion(this.execCwd(), checkpoint.snapshot.stashHash ?? "HEAD");
|
|
515
|
+
if (evasions.length > 0) {
|
|
516
|
+
this.push("error", `\u26A0 sentinel: ${evasions.length} gate-evasion pattern(s) this turn
|
|
517
|
+
${sentinelSummary(evasions)}
|
|
518
|
+
Review before trusting green \u2014 /undo rolls the turn back.`);
|
|
519
|
+
runHook(this.opts.cwd, "on-sentinel", { count: String(evasions.length), summary: sentinelSummary(evasions) });
|
|
520
|
+
}
|
|
521
|
+
} catch {
|
|
522
|
+
}
|
|
512
523
|
try {
|
|
513
524
|
const { rulePackSummary, scanRulePacks } = await import("./rulepacks-JOA675OJ.js");
|
|
514
525
|
const findings = scanRulePacks(this.execCwd(), checkpoint.snapshot.stashHash ?? "HEAD");
|
|
@@ -647,7 +658,7 @@ ${probe.checkFailures.slice(0, 5).join("\n")}`);
|
|
|
647
658
|
const captureResult = await this.capture();
|
|
648
659
|
if (captureResult) {
|
|
649
660
|
await this.runTurn(
|
|
650
|
-
buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms, captureResult.viewTransitions, captureResult.components, captureResult.webmcp),
|
|
661
|
+
buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms, captureResult.viewTransitions, captureResult.components, captureResult.webmcp, captureResult.jank),
|
|
651
662
|
"\u{1F441} auto-review rendered UI"
|
|
652
663
|
);
|
|
653
664
|
}
|
|
@@ -744,11 +755,13 @@ ${probe.checkFailures.slice(0, 5).join("\n")}`);
|
|
|
744
755
|
this.push("image", pulsePath);
|
|
745
756
|
return null;
|
|
746
757
|
}
|
|
747
|
-
const
|
|
748
|
-
if (
|
|
758
|
+
const diff = await comparePulseAttributed(previous, current, this.state.devUrl ?? void 0);
|
|
759
|
+
if (diff === null) return null;
|
|
760
|
+
const pct = diff.pct;
|
|
749
761
|
this.push(
|
|
750
762
|
"status",
|
|
751
|
-
pct < 0.5 ? "visual pulse: stable vs last turn" : `visual pulse: ${pct.toFixed(1)}% of the page changed vs last turn`
|
|
763
|
+
pct < 0.5 ? "visual pulse: stable vs last turn" : `visual pulse: ${pct.toFixed(1)}% of the page changed vs last turn${diff.sentences.length > 0 ? `
|
|
764
|
+
${diff.sentences.map((s) => ` ${s}`).join("\n")}` : ""}`
|
|
752
765
|
);
|
|
753
766
|
runHook(this.opts.cwd, "on-pulse-diff", { pct: pct.toFixed(1) });
|
|
754
767
|
if (pct >= 0.5) this.push("image", pulsePath);
|
|
@@ -1070,8 +1083,8 @@ They are objective defects, not style preferences.`
|
|
|
1070
1083
|
this.push("error", "no Chrome/Chromium found for flows");
|
|
1071
1084
|
return;
|
|
1072
1085
|
}
|
|
1073
|
-
const { runFlow } = await import("./cdp-
|
|
1074
|
-
const { previewDir } = await import("./preview-
|
|
1086
|
+
const { runFlow } = await import("./cdp-2VVQOHWM.js");
|
|
1087
|
+
const { previewDir } = await import("./preview-FBNVDQIV.js");
|
|
1075
1088
|
this.push("status", `replaying ${flows.length} flow(s)\u2026`);
|
|
1076
1089
|
this.notify({ running: true, runStartedAt: Date.now() });
|
|
1077
1090
|
const failures = [];
|
|
@@ -1209,7 +1222,7 @@ They are objective defects, not style preferences.`
|
|
|
1209
1222
|
const result = await this.capture();
|
|
1210
1223
|
if (result) {
|
|
1211
1224
|
await this.runTurn(
|
|
1212
|
-
buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp),
|
|
1225
|
+
buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp, result.jank),
|
|
1213
1226
|
`\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
|
|
1214
1227
|
);
|
|
1215
1228
|
}
|
package/dist/cli.js
CHANGED
|
@@ -3,12 +3,12 @@ import {
|
|
|
3
3
|
connectDaemon,
|
|
4
4
|
socketPath,
|
|
5
5
|
startDaemon
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-ATNTD3V5.js";
|
|
7
7
|
import {
|
|
8
8
|
App
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-JZMJGLIZ.js";
|
|
10
10
|
import "./chunk-43NQNIJY.js";
|
|
11
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-YGPVP4YB.js";
|
|
12
12
|
import "./chunk-ARDV4XH6.js";
|
|
13
13
|
import "./chunk-AUJJGMZG.js";
|
|
14
14
|
import "./chunk-YHRAOBI2.js";
|
|
@@ -22,9 +22,9 @@ import {
|
|
|
22
22
|
composePrompt
|
|
23
23
|
} from "./chunk-WAJXATCO.js";
|
|
24
24
|
import "./chunk-62JNF5M2.js";
|
|
25
|
-
import "./chunk-
|
|
25
|
+
import "./chunk-7RFQWOQV.js";
|
|
26
26
|
import "./chunk-7CAGWFAQ.js";
|
|
27
|
-
import "./chunk-
|
|
27
|
+
import "./chunk-BGBG2MNY.js";
|
|
28
28
|
import "./chunk-IMDRXXFU.js";
|
|
29
29
|
import {
|
|
30
30
|
detectEngines,
|
|
@@ -189,11 +189,11 @@ function registerDaemon(program2) {
|
|
|
189
189
|
const cwd = process.cwd();
|
|
190
190
|
if (!opts.plain) {
|
|
191
191
|
try {
|
|
192
|
-
const { RemoteSession } = await import("./remote-
|
|
192
|
+
const { RemoteSession } = await import("./remote-OH2P4S2H.js");
|
|
193
193
|
const remote = await RemoteSession.connect(cwd);
|
|
194
194
|
const config = loadConfig(defaultPaths(cwd));
|
|
195
195
|
const { render: render2 } = await import("ink");
|
|
196
|
-
const { App: App2 } = await import("./App-
|
|
196
|
+
const { App: App2 } = await import("./App-766KSGON.js");
|
|
197
197
|
const React = await import("react");
|
|
198
198
|
render2(
|
|
199
199
|
React.createElement(App2, {
|
|
@@ -310,7 +310,7 @@ function registerEnv(program2) {
|
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
312
|
const { findChrome } = await import("./chrome-SBV3H77F.js");
|
|
313
|
-
const { hasWebSocket } = await import("./cdp-
|
|
313
|
+
const { hasWebSocket } = await import("./cdp-2VVQOHWM.js");
|
|
314
314
|
const chrome = findChrome();
|
|
315
315
|
console.log(
|
|
316
316
|
chrome ? `${pc3.green("\u2713")} Chrome ${pc3.dim(chrome)}` : `${pc3.yellow("\u25CB")} Chrome ${pc3.dim("\u2014 screenshots and runtime probing disabled")}`
|
|
@@ -533,7 +533,7 @@ function registerQuality(program2) {
|
|
|
533
533
|
if (results.some((r) => !r.ok)) process.exitCode = 1;
|
|
534
534
|
});
|
|
535
535
|
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) => {
|
|
536
|
-
const { captureViewports } = await import("./preview-
|
|
536
|
+
const { captureViewports } = await import("./preview-FBNVDQIV.js");
|
|
537
537
|
const result = await captureViewports(process.cwd(), url);
|
|
538
538
|
if (!result) {
|
|
539
539
|
console.error(pc5.red("\u2717 no Chrome/Chromium found"));
|
|
@@ -720,7 +720,7 @@ function registerTui(program2) {
|
|
|
720
720
|
}
|
|
721
721
|
|
|
722
722
|
// src/cli.tsx
|
|
723
|
-
var VERSION = true ? "0.4.
|
|
723
|
+
var VERSION = true ? "0.4.8" : "0.0.0-dev";
|
|
724
724
|
var program = new Command();
|
|
725
725
|
program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
|
|
726
726
|
registerRun(program);
|
|
@@ -5,14 +5,15 @@ import {
|
|
|
5
5
|
buildRuntimeFixPrompt,
|
|
6
6
|
captureViewports,
|
|
7
7
|
comparePulse,
|
|
8
|
+
comparePulseAttributed,
|
|
8
9
|
loadRoutes,
|
|
9
10
|
previewDir,
|
|
10
11
|
probeRuntime,
|
|
11
12
|
routeShotName,
|
|
12
13
|
runtimeSummary
|
|
13
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-7RFQWOQV.js";
|
|
14
15
|
import "./chunk-7CAGWFAQ.js";
|
|
15
|
-
import "./chunk-
|
|
16
|
+
import "./chunk-BGBG2MNY.js";
|
|
16
17
|
import "./chunk-IMDRXXFU.js";
|
|
17
18
|
import "./chunk-KVYGPLWW.js";
|
|
18
19
|
export {
|
|
@@ -21,6 +22,7 @@ export {
|
|
|
21
22
|
buildRuntimeFixPrompt,
|
|
22
23
|
captureViewports,
|
|
23
24
|
comparePulse,
|
|
25
|
+
comparePulseAttributed,
|
|
24
26
|
loadRoutes,
|
|
25
27
|
previewDir,
|
|
26
28
|
probeRuntime,
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import {
|
|
3
3
|
connectDaemon,
|
|
4
4
|
socketPath
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-ATNTD3V5.js";
|
|
6
|
+
import "./chunk-YGPVP4YB.js";
|
|
7
7
|
import "./chunk-ARDV4XH6.js";
|
|
8
8
|
import "./chunk-AUJJGMZG.js";
|
|
9
9
|
import "./chunk-YHRAOBI2.js";
|
|
@@ -13,9 +13,9 @@ import "./chunk-VH7OOFQP.js";
|
|
|
13
13
|
import "./chunk-K5QJMSJH.js";
|
|
14
14
|
import "./chunk-WAJXATCO.js";
|
|
15
15
|
import "./chunk-62JNF5M2.js";
|
|
16
|
-
import "./chunk-
|
|
16
|
+
import "./chunk-7RFQWOQV.js";
|
|
17
17
|
import "./chunk-7CAGWFAQ.js";
|
|
18
|
-
import "./chunk-
|
|
18
|
+
import "./chunk-BGBG2MNY.js";
|
|
19
19
|
import "./chunk-IMDRXXFU.js";
|
|
20
20
|
import "./chunk-KVYGPLWW.js";
|
|
21
21
|
|
|
Binary file
|
package/package.json
CHANGED