@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.
- package/README.md +6 -0
- package/dist/App-HWMVSRMF.js +23 -0
- package/dist/{cdp-Q4H6ZHPT.js → cdp-PBQKXC6R.js} +1 -1
- package/dist/{chunk-O2S6PAJE.js → chunk-7CAGWFAQ.js} +1 -1
- package/dist/chunk-7MOKOZOR.js +1415 -0
- package/dist/{chunk-ZLEP2TWF.js → chunk-ARDV4XH6.js} +5 -1
- package/dist/{chunk-XEQ6JXXL.js → chunk-AUJJGMZG.js} +1 -1
- package/dist/{chunk-RQHOE5MV.js → chunk-HC4E42SV.js} +50 -2
- package/dist/chunk-M25IQ7C7.js +587 -0
- package/dist/chunk-NX7XQY2X.js +147 -0
- package/dist/{chunk-CV5WVKHU.js → chunk-ZSDCMYZR.js} +58 -18
- package/dist/cli.js +261 -2099
- package/dist/{contextDoctor-U3YTDFVG.js → contextDoctor-A26C2ZDN.js} +1 -1
- package/dist/{preview-ZUCVWGZG.js → preview-WT34NVCH.js} +3 -3
- package/dist/remote-6O6FPTUF.js +114 -0
- package/dist/{sandbox-J2NCPYHJ.js → sandbox-MK7Q2YNO.js} +1 -1
- package/dist/shots-OKWOYF7F.js +2 -2
- package/dist/{skills-6NZP67QT.js → skills-POB4ZZY5.js} +1 -1
- package/dist/{state-QOS7WCZO.js → state-PLY7YAD2.js} +1 -1
- package/dist/variants-3IEP7DFY.js +1 -1
- package/package.json +1 -1
|
@@ -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-
|
|
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
|
|
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 =
|
|
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 =
|
|
38
|
-
|
|
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
|
|
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 =
|
|
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-
|
|
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 {
|