@aayambansal/squint 0.4.5 → 0.4.7

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.
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ Session
4
+ } from "./chunk-7MOKOZOR.js";
5
+
6
+ // src/daemon/client.ts
7
+ import net from "net";
8
+ function connectDaemon(sock, timeoutMs = 3e3) {
9
+ return new Promise((resolve, reject) => {
10
+ const socket = net.createConnection(sock);
11
+ const timer = setTimeout(() => {
12
+ socket.destroy();
13
+ reject(new Error(`no daemon listening at ${sock}`));
14
+ }, timeoutMs);
15
+ const handlers = [];
16
+ let buffer = "";
17
+ socket.on("connect", () => {
18
+ clearTimeout(timer);
19
+ resolve({
20
+ send: (msg) => socket.write(`${JSON.stringify(msg)}
21
+ `),
22
+ onMessage: (handler) => handlers.push(handler),
23
+ close: () => socket.destroy()
24
+ });
25
+ });
26
+ socket.on("error", (error) => {
27
+ clearTimeout(timer);
28
+ reject(error);
29
+ });
30
+ socket.on("data", (chunk) => {
31
+ buffer += chunk.toString();
32
+ let idx;
33
+ while ((idx = buffer.indexOf("\n")) >= 0) {
34
+ const line = buffer.slice(0, idx);
35
+ buffer = buffer.slice(idx + 1);
36
+ if (!line.trim()) continue;
37
+ try {
38
+ const msg = JSON.parse(line);
39
+ for (const handler of handlers) handler(msg);
40
+ } catch {
41
+ }
42
+ }
43
+ });
44
+ });
45
+ }
46
+
47
+ // src/daemon/server.ts
48
+ import crypto from "crypto";
49
+ import fs from "fs";
50
+ import net2 from "net";
51
+ import os from "os";
52
+ import path from "path";
53
+ var MAX_ITEMS_SENT = 200;
54
+ function socketPath(cwd) {
55
+ const direct = path.join(cwd, ".squint", "daemon.sock");
56
+ if (Buffer.byteLength(direct) <= 96) return direct;
57
+ const hash = crypto.createHash("sha256").update(cwd).digest("hex").slice(0, 12);
58
+ return path.join(os.tmpdir(), `squint-${hash}.sock`);
59
+ }
60
+ function startDaemon(opts) {
61
+ const session = new Session(opts);
62
+ const sock = socketPath(opts.cwd);
63
+ fs.mkdirSync(path.dirname(sock), { recursive: true });
64
+ fs.rmSync(sock, { force: true });
65
+ const clients = [];
66
+ const driver = () => clients[0];
67
+ const serialize = () => {
68
+ const state = session.getState();
69
+ return `${JSON.stringify({ type: "state", state: { ...state, items: state.items.slice(-MAX_ITEMS_SENT) } })}
70
+ `;
71
+ };
72
+ const server = net2.createServer((socket) => {
73
+ clients.push(socket);
74
+ const role = socket === driver() ? "driver" : "observer";
75
+ socket.write(`${JSON.stringify({ type: "hello", role, engineId: session.getState().engineId })}
76
+ `);
77
+ socket.write(serialize());
78
+ let buffer = "";
79
+ socket.on("data", (chunk) => {
80
+ buffer += chunk.toString();
81
+ let idx;
82
+ while ((idx = buffer.indexOf("\n")) >= 0) {
83
+ const line = buffer.slice(0, idx);
84
+ buffer = buffer.slice(idx + 1);
85
+ if (!line.trim()) continue;
86
+ let msg;
87
+ try {
88
+ msg = JSON.parse(line);
89
+ } catch {
90
+ continue;
91
+ }
92
+ if (msg.type === "ping") {
93
+ socket.write(`${JSON.stringify({ type: "ping" })}
94
+ `);
95
+ continue;
96
+ }
97
+ if (socket !== driver()) {
98
+ socket.write(`${JSON.stringify({ type: "denied", reason: "observer \u2014 the driver steers this session" })}
99
+ `);
100
+ continue;
101
+ }
102
+ if (msg.type === "input" && typeof msg.text === "string") session.input(msg.text);
103
+ else if (msg.type === "command" && typeof msg.text === "string") session.command(msg.text);
104
+ else if (msg.type === "interrupt") session.interrupt();
105
+ else if (msg.type === "cycleMode") session.cycleMode();
106
+ }
107
+ });
108
+ const drop = () => {
109
+ const index = clients.indexOf(socket);
110
+ if (index >= 0) clients.splice(index, 1);
111
+ const next = driver();
112
+ if (index === 0 && next && !next.destroyed) {
113
+ next.write(`${JSON.stringify({ type: "hello", role: "driver", engineId: session.getState().engineId })}
114
+ `);
115
+ }
116
+ };
117
+ socket.on("close", drop);
118
+ socket.on("error", drop);
119
+ });
120
+ const unsubscribe = session.subscribe(() => {
121
+ const payload = serialize();
122
+ for (const client of clients) {
123
+ if (!client.destroyed) client.write(payload);
124
+ }
125
+ });
126
+ return new Promise((resolve, reject) => {
127
+ server.once("error", reject);
128
+ server.listen(sock, () => {
129
+ resolve({
130
+ session,
131
+ clientCount: () => clients.length,
132
+ close: () => {
133
+ unsubscribe();
134
+ for (const client of clients) client.destroy();
135
+ server.close();
136
+ fs.rmSync(sock, { force: true });
137
+ }
138
+ });
139
+ });
140
+ });
141
+ }
142
+
143
+ export {
144
+ connectDaemon,
145
+ socketPath,
146
+ startDaemon
147
+ };
@@ -1,20 +1,47 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ensureSquintIgnore
4
- } from "./chunk-O2S6PAJE.js";
4
+ } from "./chunk-7CAGWFAQ.js";
5
+ import {
6
+ cdpCapture,
7
+ hasWebSocket
8
+ } from "./chunk-HC4E42SV.js";
5
9
  import {
6
10
  findChrome,
7
11
  screenshot
8
12
  } from "./chunk-IMDRXXFU.js";
9
- import {
10
- cdpCapture,
11
- hasWebSocket
12
- } from "./chunk-RQHOE5MV.js";
13
13
 
14
14
  // src/preview/preview.ts
15
- import fs from "fs";
15
+ import fs2 from "fs";
16
16
  import os from "os";
17
+ import path2 from "path";
18
+
19
+ // src/preview/checks.ts
20
+ import fs from "fs";
17
21
  import path from "path";
22
+ var MAX_CHECKS = 20;
23
+ var MAX_BYTES = 1e4;
24
+ function loadChecks(cwd) {
25
+ const dir = path.join(cwd, ".squint", "checks");
26
+ let entries;
27
+ try {
28
+ entries = fs.readdirSync(dir).filter((f) => f.endsWith(".js"));
29
+ } catch {
30
+ return [];
31
+ }
32
+ const checks = [];
33
+ for (const entry of entries.sort().slice(0, MAX_CHECKS)) {
34
+ try {
35
+ const source = fs.readFileSync(path.join(dir, entry), "utf8");
36
+ if (source.trim().length === 0 || Buffer.byteLength(source) > MAX_BYTES) continue;
37
+ checks.push({ name: entry.replace(/\.js$/, ""), source });
38
+ } catch {
39
+ }
40
+ }
41
+ return checks;
42
+ }
43
+
44
+ // src/preview/preview.ts
18
45
  var VIEWPORTS = [
19
46
  { name: "mobile", width: 390, height: 844 },
20
47
  { name: "tablet", width: 768, height: 1024 },
@@ -23,7 +50,7 @@ var VIEWPORTS = [
23
50
  function loadRoutes(cwd) {
24
51
  let lines = [];
25
52
  try {
26
- lines = fs.readFileSync(path.join(cwd, ".squint", "routes"), "utf8").split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
53
+ lines = fs2.readFileSync(path2.join(cwd, ".squint", "routes"), "utf8").split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
27
54
  } catch {
28
55
  }
29
56
  const routes = ["/", ...lines.filter((l) => l !== "/")];
@@ -34,8 +61,8 @@ function routeShotName(route) {
34
61
  return clean.length > 0 ? clean : "root";
35
62
  }
36
63
  function previewDir(cwd) {
37
- const dir = path.join(cwd, ".squint", "preview");
38
- fs.mkdirSync(dir, { recursive: true });
64
+ const dir = path2.join(cwd, ".squint", "preview");
65
+ fs2.mkdirSync(dir, { recursive: true });
39
66
  ensureSquintIgnore(cwd);
40
67
  return dir;
41
68
  }
@@ -47,7 +74,8 @@ async function captureViewports(cwd, url) {
47
74
  const base = url.replace(/\/+$/, "");
48
75
  if (hasWebSocket()) {
49
76
  try {
50
- const { report, shots: shots2, a11y, slop, narration, phantoms, viewTransitions, components } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true);
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);
51
79
  const errors2 = [];
52
80
  for (const route of routes.slice(1)) {
53
81
  try {
@@ -62,14 +90,14 @@ async function captureViewports(cwd, url) {
62
90
  errors2.push(`${route}: capture failed`);
63
91
  }
64
92
  }
65
- return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration, phantoms, viewTransitions, components };
93
+ return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp };
66
94
  } catch {
67
95
  }
68
96
  }
69
97
  const shots = [];
70
98
  const errors = [];
71
99
  for (const viewport of VIEWPORTS) {
72
- const outPath = path.join(dir, `${viewport.name}.png`);
100
+ const outPath = path2.join(dir, `${viewport.name}.png`);
73
101
  const result = await screenshot(chrome, url, outPath, {
74
102
  width: viewport.width,
75
103
  height: viewport.height
@@ -118,14 +146,16 @@ async function probeRuntime(url, cwd) {
118
146
  if (!chrome || !hasWebSocket()) return null;
119
147
  try {
120
148
  const dir = cwd ? previewDir(cwd) : os.tmpdir();
121
- const { report, shots, perf } = await cdpCapture(
149
+ const { report, shots, perf, checkFailures } = await cdpCapture(
122
150
  chrome,
123
151
  url,
124
152
  dir,
125
153
  cwd ? [{ name: "pulse", width: 1280, height: 800 }] : [],
126
- 1500
154
+ 1500,
155
+ false,
156
+ cwd ? loadChecks(cwd) : []
127
157
  );
128
- return { report, pulsePath: shots[0]?.path, perf };
158
+ return { report, pulsePath: shots[0]?.path, perf, checkFailures };
129
159
  } catch {
130
160
  return null;
131
161
  }
@@ -133,7 +163,7 @@ async function probeRuntime(url, cwd) {
133
163
  async function comparePulse(previous, current) {
134
164
  const chrome = findChrome();
135
165
  if (!chrome || !hasWebSocket()) return null;
136
- const { pixelDiffPct } = await import("./cdp-Q4H6ZHPT.js");
166
+ const { pixelDiffPct } = await import("./cdp-PBQKXC6R.js");
137
167
  return pixelDiffPct(chrome, previous, current);
138
168
  }
139
169
  function buildRuntimeFixPrompt(report) {
@@ -161,6 +191,16 @@ ${narration.join("\n")}
161
191
 
162
192
  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
193
  }
194
+ function webmcpSection(webmcp) {
195
+ if (!webmcp || webmcp.length === 0) return "";
196
+ return `
197
+
198
+ ## Page-declared WebMCP tools
199
+
200
+ ${webmcp.join("\n")}
201
+
202
+ The page registers these for agents via navigator.modelContext \u2014 keep them working, and prefer extending them over inventing parallel affordances.`;
203
+ }
164
204
  function componentSection(components) {
165
205
  if (!components || components.length === 0) return "";
166
206
  return `
@@ -201,13 +241,13 @@ ${findings.join("\n")}
201
241
 
202
242
  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.`;
203
243
  }
204
- function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms, viewTransitions, components) {
244
+ function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms, viewTransitions, components, webmcp) {
205
245
  const list = shots.map((s) => `- ${s.name}: ${s.path}`).join("\n");
206
246
  return `Screenshots of the running app were just captured:
207
247
 
208
248
  ${list}
209
249
 
210
- 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)}`;
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)}`;
211
251
  }
212
252
 
213
253
  export {