@aayambansal/squint 0.3.2 → 0.3.3

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.
@@ -3,7 +3,7 @@ import {
3
3
  cdpCapture,
4
4
  hasWebSocket,
5
5
  pixelDiffPct
6
- } from "./chunk-DAETGO2M.js";
6
+ } from "./chunk-XOVOQJFL.js";
7
7
  export {
8
8
  cdpCapture,
9
9
  hasWebSocket,
@@ -1,8 +1,41 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/prompt/skills.ts
4
+ import fs2 from "fs";
5
+ import path2 from "path";
6
+
7
+ // src/prompt/registry.ts
4
8
  import fs from "fs";
5
9
  import path from "path";
10
+ function loadComponentInventory(cwd) {
11
+ let config;
12
+ try {
13
+ config = JSON.parse(fs.readFileSync(path.join(cwd, "components.json"), "utf8"));
14
+ } catch {
15
+ return null;
16
+ }
17
+ const alias = config.aliases?.ui ?? (config.aliases?.components ? `${config.aliases.components}/ui` : "@/components/ui");
18
+ const relative = alias.replace(/^@\//, "src/").replace(/^~\//, "");
19
+ const uiDir = path.join(cwd, relative);
20
+ let entries;
21
+ try {
22
+ entries = fs.readdirSync(uiDir);
23
+ } catch {
24
+ return null;
25
+ }
26
+ const components = entries.filter((f) => /\.(tsx|jsx|vue|svelte)$/.test(f)).map((f) => f.replace(/\.[^.]+$/, "")).sort();
27
+ if (components.length === 0) return null;
28
+ return { components, uiDir: relative };
29
+ }
30
+ function inventorySection(inventory) {
31
+ return `## Installed UI components (${inventory.uiDir})
32
+
33
+ ${inventory.components.join(" \xB7 ")}
34
+
35
+ Compose from these before writing new primitives. More are one command away: \`npx shadcn@latest add <name>\`. Never invent props for these components \u2014 read the file when unsure.`;
36
+ }
37
+
38
+ // src/prompt/skills.ts
6
39
  var FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?/;
7
40
  function parseSkill(name, raw) {
8
41
  const match = FRONTMATTER_RE.exec(raw);
@@ -22,17 +55,17 @@ function parseSkill(name, raw) {
22
55
  return { name, triggers, body };
23
56
  }
24
57
  function loadSkills(cwd) {
25
- const dir = path.join(cwd, ".squint", "skills");
58
+ const dir = path2.join(cwd, ".squint", "skills");
26
59
  let entries;
27
60
  try {
28
- entries = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
61
+ entries = fs2.readdirSync(dir).filter((f) => f.endsWith(".md"));
29
62
  } catch {
30
63
  return [];
31
64
  }
32
65
  const skills = [];
33
66
  for (const entry of entries.sort()) {
34
67
  try {
35
- const skill = parseSkill(entry.replace(/\.md$/, ""), fs.readFileSync(path.join(dir, entry), "utf8"));
68
+ const skill = parseSkill(entry.replace(/\.md$/, ""), fs2.readFileSync(path2.join(dir, entry), "utf8"));
36
69
  if (skill) skills.push(skill);
37
70
  } catch {
38
71
  }
@@ -41,7 +74,7 @@ function loadSkills(cwd) {
41
74
  }
42
75
  function loadRules(cwd) {
43
76
  try {
44
- const text = fs.readFileSync(path.join(cwd, ".squint", "rules.md"), "utf8").trim();
77
+ const text = fs2.readFileSync(path2.join(cwd, ".squint", "rules.md"), "utf8").trim();
45
78
  return text.length > 0 ? text : null;
46
79
  } catch {
47
80
  return null;
@@ -53,7 +86,7 @@ function matchSkills(skills, ask) {
53
86
  }
54
87
  function loadLocks(cwd) {
55
88
  try {
56
- return fs.readFileSync(path.join(cwd, ".squint", "locks"), "utf8").split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
89
+ return fs2.readFileSync(path2.join(cwd, ".squint", "locks"), "utf8").split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
57
90
  } catch {
58
91
  return [];
59
92
  }
@@ -64,6 +97,8 @@ function enrich(cwd, ask) {
64
97
  if (rules) parts.push(`## Project rules (always apply)
65
98
 
66
99
  ${rules}`);
100
+ const inventory = loadComponentInventory(cwd);
101
+ if (inventory) parts.push(inventorySection(inventory));
67
102
  const locks = loadLocks(cwd);
68
103
  if (locks.length > 0) {
69
104
  parts.push(
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  cdpCapture,
8
8
  hasWebSocket
9
- } from "./chunk-DAETGO2M.js";
9
+ } from "./chunk-XOVOQJFL.js";
10
10
  import {
11
11
  ensureSquintIgnore
12
12
  } from "./chunk-O2S6PAJE.js";
@@ -47,7 +47,7 @@ async function captureViewports(cwd, url) {
47
47
  const base = url.replace(/\/+$/, "");
48
48
  if (hasWebSocket()) {
49
49
  try {
50
- const { report, shots: shots2, a11y, slop } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true);
50
+ const { report, shots: shots2, a11y, slop, narration } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true);
51
51
  const errors2 = [];
52
52
  for (const route of routes.slice(1)) {
53
53
  try {
@@ -62,7 +62,7 @@ async function captureViewports(cwd, url) {
62
62
  errors2.push(`${route}: capture failed`);
63
63
  }
64
64
  }
65
- return { shots: shots2, errors: errors2, runtime: report, a11y, slop };
65
+ return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration };
66
66
  } catch {
67
67
  }
68
68
  }
@@ -133,7 +133,7 @@ async function probeRuntime(url, cwd) {
133
133
  async function comparePulse(previous, current) {
134
134
  const chrome = findChrome();
135
135
  if (!chrome || !hasWebSocket()) return null;
136
- const { pixelDiffPct } = await import("./cdp-2XAU5MEA.js");
136
+ const { pixelDiffPct } = await import("./cdp-UOIP4ZQZ.js");
137
137
  return pixelDiffPct(chrome, previous, current);
138
138
  }
139
139
  function buildRuntimeFixPrompt(report) {
@@ -151,6 +151,16 @@ ${findings.join("\n")}
151
151
 
152
152
  Fix these as part of the pass \u2014 they are objective defects, not style preferences.`;
153
153
  }
154
+ function narrationSection(narration) {
155
+ if (!narration || narration.length === 0) return "";
156
+ return `
157
+
158
+ ## What a screen reader would say (accessibility tree, in order)
159
+
160
+ ${narration.join("\n")}
161
+
162
+ Judge this narration as an experience: does the reading order make sense? do names actually describe their targets? is anything announced as "(no accessible name)"? Fix real incoherence \u2014 this is how non-visual users meet the page.`;
163
+ }
154
164
  function slopSection(findings) {
155
165
  if (!findings || findings.length === 0) return "";
156
166
  return `
@@ -161,13 +171,13 @@ ${findings.join("\n")}
161
171
 
162
172
  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.`;
163
173
  }
164
- function buildReviewPrompt(shots, extra, runtime, a11y, slop) {
174
+ function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration) {
165
175
  const list = shots.map((s) => `- ${s.name}: ${s.path}`).join("\n");
166
176
  return `Screenshots of the running app were just captured:
167
177
 
168
178
  ${list}
169
179
 
170
- 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)}`;
180
+ 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)}`;
171
181
  }
172
182
 
173
183
  export {
@@ -278,6 +278,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
278
278
  let a11y = [];
279
279
  let slop = [];
280
280
  let perf = {};
281
+ let narration = [];
281
282
  const requests = /* @__PURE__ */ new Map();
282
283
  let connection = null;
283
284
  try {
@@ -350,6 +351,47 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
350
351
  if (Array.isArray(result?.value)) slop = result.value.map(String);
351
352
  } catch {
352
353
  }
354
+ try {
355
+ await connection.send("Accessibility.enable", {}, sessionId);
356
+ const { nodes } = await connection.send("Accessibility.getFullAXTree", {}, sessionId);
357
+ const interesting = /* @__PURE__ */ new Set([
358
+ "heading",
359
+ "link",
360
+ "button",
361
+ "textbox",
362
+ "checkbox",
363
+ "radio",
364
+ "combobox",
365
+ "listbox",
366
+ "img",
367
+ "image",
368
+ "navigation",
369
+ "main",
370
+ "banner",
371
+ "contentinfo",
372
+ "form",
373
+ "search",
374
+ "tab",
375
+ "menuitem",
376
+ "switch",
377
+ "slider",
378
+ "alert",
379
+ "dialog",
380
+ "list"
381
+ ]);
382
+ if (Array.isArray(nodes)) {
383
+ for (const node of nodes) {
384
+ if (node.ignored) continue;
385
+ const role = node.role?.value;
386
+ if (!role || !interesting.has(role)) continue;
387
+ const name = (node.name?.value ?? "").toString().trim().replace(/\s+/g, " ").slice(0, 80);
388
+ const level = node.properties?.find((p) => p.name === "level")?.value?.value;
389
+ narration.push(`${role}${level ? ` ${level}` : ""}${name ? `: "${name}"` : " (no accessible name)"}`);
390
+ if (narration.length >= 80) break;
391
+ }
392
+ }
393
+ } catch {
394
+ }
353
395
  }
354
396
  for (const viewport of viewports) {
355
397
  await connection.send(
@@ -373,7 +415,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
373
415
  child.kill("SIGKILL");
374
416
  setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
375
417
  }
376
- return { report, shots, a11y, slop, perf };
418
+ return { report, shots, a11y, slop, perf, narration };
377
419
  }
378
420
 
379
421
  export {
package/dist/cli.js CHANGED
@@ -42,7 +42,7 @@ import {
42
42
  comparePulse,
43
43
  probeRuntime,
44
44
  runtimeSummary
45
- } from "./chunk-OTCH66M7.js";
45
+ } from "./chunk-SXF7CVRW.js";
46
46
  import {
47
47
  runAgent
48
48
  } from "./chunk-VH7OOFQP.js";
@@ -53,10 +53,10 @@ import {
53
53
  detectEngines,
54
54
  getEngine
55
55
  } from "./chunk-KVYGPLWW.js";
56
- import "./chunk-DAETGO2M.js";
56
+ import "./chunk-XOVOQJFL.js";
57
57
  import {
58
58
  enrich
59
- } from "./chunk-4LAU5TGK.js";
59
+ } from "./chunk-H5K55LXY.js";
60
60
  import {
61
61
  composePrompt
62
62
  } from "./chunk-P3H4N2EN.js";
@@ -236,7 +236,7 @@ function registerEnv(program2) {
236
236
  }
237
237
  }
238
238
  const { findChrome: findChrome2 } = await import("./chrome-SBV3H77F.js");
239
- const { hasWebSocket } = await import("./cdp-2XAU5MEA.js");
239
+ const { hasWebSocket } = await import("./cdp-UOIP4ZQZ.js");
240
240
  const chrome = findChrome2();
241
241
  console.log(
242
242
  chrome ? `${pc2.green("\u2713")} Chrome ${pc2.dim(chrome)}` : `${pc2.yellow("\u25CB")} Chrome ${pc2.dim("\u2014 screenshots and runtime probing disabled")}`
@@ -260,7 +260,7 @@ import pc3 from "picocolors";
260
260
  function registerProject(program2) {
261
261
  const skillsCommand = program2.command("skills").description("Project knowledge injected into asks (.squint/rules.md + .squint/skills/)");
262
262
  skillsCommand.command("list").description("Show always-on rules and trigger-matched skills").action(async () => {
263
- const { loadRules, loadSkills } = await import("./skills-DNBHZAAU.js");
263
+ const { loadRules, loadSkills } = await import("./skills-ODOVNE6E.js");
264
264
  const cwd = process.cwd();
265
265
  const rules = loadRules(cwd);
266
266
  console.log(
@@ -459,7 +459,7 @@ function registerQuality(program2) {
459
459
  if (results.some((r) => !r.ok)) process.exitCode = 1;
460
460
  });
461
461
  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) => {
462
- const { captureViewports: captureViewports2 } = await import("./preview-PS7WJ2KO.js");
462
+ const { captureViewports: captureViewports2 } = await import("./preview-S6FOTRRR.js");
463
463
  const result = await captureViewports2(process.cwd(), url);
464
464
  if (!result) {
465
465
  console.error(pc4.red("\u2717 no Chrome/Chromium found"));
@@ -820,7 +820,7 @@ ${question}`,
820
820
  return;
821
821
  }
822
822
  await this.runTurn(
823
- buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop),
823
+ buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration),
824
824
  `\u{1F441} polish round ${round}/${rounds}`
825
825
  );
826
826
  }
@@ -1138,7 +1138,7 @@ ${errors.slice(-5).join("\n")}`);
1138
1138
  const captureResult = await this.capture();
1139
1139
  if (captureResult) {
1140
1140
  await this.runTurn(
1141
- buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop),
1141
+ buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration),
1142
1142
  "\u{1F441} auto-review rendered UI"
1143
1143
  );
1144
1144
  }
@@ -1508,7 +1508,7 @@ They are objective defects, not style preferences.`
1508
1508
  const result = await this.capture();
1509
1509
  if (result) {
1510
1510
  await this.runTurn(
1511
- buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop),
1511
+ buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration),
1512
1512
  `\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
1513
1513
  );
1514
1514
  }
@@ -2308,7 +2308,7 @@ function registerTui(program2) {
2308
2308
  }
2309
2309
 
2310
2310
  // src/cli.tsx
2311
- var VERSION = true ? "0.3.2" : "0.0.0-dev";
2311
+ var VERSION = true ? "0.3.3" : "0.0.0-dev";
2312
2312
  var program = new Command();
2313
2313
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
2314
2314
  registerRun(program);
@@ -10,10 +10,10 @@ import {
10
10
  probeRuntime,
11
11
  routeShotName,
12
12
  runtimeSummary
13
- } from "./chunk-OTCH66M7.js";
13
+ } from "./chunk-SXF7CVRW.js";
14
14
  import "./chunk-IMDRXXFU.js";
15
15
  import "./chunk-KVYGPLWW.js";
16
- import "./chunk-DAETGO2M.js";
16
+ import "./chunk-XOVOQJFL.js";
17
17
  import "./chunk-O2S6PAJE.js";
18
18
  export {
19
19
  VIEWPORTS,
@@ -6,7 +6,7 @@ import {
6
6
  loadSkills,
7
7
  matchSkills,
8
8
  parseSkill
9
- } from "./chunk-4LAU5TGK.js";
9
+ } from "./chunk-H5K55LXY.js";
10
10
  export {
11
11
  enrich,
12
12
  loadLocks,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aayambansal/squint",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Lovable for your terminal — a frontend harness on top of Claude Code, Codex, and other coding agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",