@aayambansal/squint 0.3.3 → 0.4.0

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 CHANGED
@@ -133,6 +133,7 @@ squint config set autoDev true # dev server starts with the TUI
133
133
  squint config set autoFix true # errors auto-route back (max 2 tries)
134
134
  squint config set autoCheck false # skip the per-turn typecheck+lint pass
135
135
  squint config set autoReview true # big visual change → automatic self-critique
136
+ squint config set fixModel haiku # mechanical fix turns run on the cheap tier
136
137
  squint config set theme ocean # amber · ocean · moss · rose · mono
137
138
  squint config set bell false # no bell on turn completion
138
139
  squint doctor # engines + Chrome + WebSocket check
@@ -2,10 +2,12 @@
2
2
  import {
3
3
  cdpCapture,
4
4
  hasWebSocket,
5
- pixelDiffPct
6
- } from "./chunk-XOVOQJFL.js";
5
+ pixelDiffPct,
6
+ runFlow
7
+ } from "./chunk-6FR4TRRQ.js";
7
8
  export {
8
9
  cdpCapture,
9
10
  hasWebSocket,
10
- pixelDiffPct
11
+ pixelDiffPct,
12
+ runFlow
11
13
  };
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ ensureSquintIgnore
4
+ } from "./chunk-O2S6PAJE.js";
2
5
  import {
3
6
  findChrome,
4
7
  screenshot
@@ -6,10 +9,7 @@ import {
6
9
  import {
7
10
  cdpCapture,
8
11
  hasWebSocket
9
- } from "./chunk-XOVOQJFL.js";
10
- import {
11
- ensureSquintIgnore
12
- } from "./chunk-O2S6PAJE.js";
12
+ } from "./chunk-6FR4TRRQ.js";
13
13
 
14
14
  // src/preview/preview.ts
15
15
  import fs from "fs";
@@ -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-UOIP4ZQZ.js");
136
+ const { pixelDiffPct } = await import("./cdp-VRGCGN5R.js");
137
137
  return pixelDiffPct(chrome, previous, current);
138
138
  }
139
139
  function buildRuntimeFixPrompt(report) {
@@ -178,6 +178,60 @@ var describe = (value) => {
178
178
  }
179
179
  return String(value);
180
180
  };
181
+ async function runFlow(chromePath, baseUrl, flow, outDir) {
182
+ const { stepExpression } = await import("./flows-V5ZLVVSQ.js");
183
+ const { child, wsUrl, profileDir } = await launchChrome(chromePath);
184
+ const shots = [];
185
+ let connection = null;
186
+ try {
187
+ connection = await CdpConnection.connect(wsUrl, 1e4);
188
+ const { targetId } = await connection.send("Target.createTarget", { url: "about:blank" });
189
+ const { sessionId } = await connection.send("Target.attachToTarget", { targetId, flatten: true });
190
+ await connection.send("Page.enable", {}, sessionId);
191
+ await connection.send(
192
+ "Emulation.setDeviceMetricsOverride",
193
+ { width: 1280, height: 800, deviceScaleFactor: 1, mobile: false },
194
+ sessionId
195
+ );
196
+ const base = baseUrl.replace(/\/+$/, "");
197
+ let stepNumber = 0;
198
+ for (const step of flow.steps) {
199
+ stepNumber += 1;
200
+ if (step.kind === "goto") {
201
+ const url = step.route === "/" ? base : `${base}${step.route}`;
202
+ await connection.send("Page.navigate", { url }, sessionId);
203
+ await new Promise((resolve) => setTimeout(resolve, 1800));
204
+ continue;
205
+ }
206
+ if (step.kind === "shot") {
207
+ const { data } = await connection.send("Page.captureScreenshot", { format: "png" }, sessionId);
208
+ const outPath = path.join(outDir, `flow-${flow.name}-${step.name}.png`);
209
+ fs.writeFileSync(outPath, Buffer.from(data, "base64"));
210
+ shots.push(outPath);
211
+ continue;
212
+ }
213
+ const expression = stepExpression(step);
214
+ if (!expression) continue;
215
+ const { result } = await connection.send(
216
+ "Runtime.evaluate",
217
+ { expression, returnByValue: true },
218
+ sessionId
219
+ );
220
+ const value = result?.value;
221
+ if (!value?.ok) {
222
+ return { ok: false, failedStep: stepNumber, detail: value?.detail ?? "step failed", shots };
223
+ }
224
+ await new Promise((resolve) => setTimeout(resolve, 300));
225
+ }
226
+ return { ok: true, shots };
227
+ } catch (err) {
228
+ return { ok: false, detail: err instanceof Error ? err.message : String(err), shots };
229
+ } finally {
230
+ connection?.close();
231
+ child.kill("SIGKILL");
232
+ setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
233
+ }
234
+ }
181
235
  async function pixelDiffPct(chromePath, pngA, pngB) {
182
236
  const { child, wsUrl, profileDir } = await launchChrome(chromePath);
183
237
  let connection = null;
@@ -420,6 +474,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
420
474
 
421
475
  export {
422
476
  hasWebSocket,
477
+ runFlow,
423
478
  pixelDiffPct,
424
479
  cdpCapture
425
480
  };
@@ -14,6 +14,8 @@ var COMMANDS = [
14
14
  { name: "shot", args: "[url]", group: "verify", description: "screenshot the app (or any url) at mobile/tablet/desktop" },
15
15
  { name: "review", args: "[focus]", group: "verify", description: "screenshots + the engine critiques its own rendered work" },
16
16
  { name: "polish", args: "[1-5]", group: "verify", description: "unattended rounds of review \u2192 fix (default 2)" },
17
+ { name: "score", group: "verify", description: "deterministic quality snapshot (problems, a11y, tells, runtime, LCP)" },
18
+ { name: "flows", args: "[name]", group: "verify", description: "replay declared .squint/flows/ journeys headlessly" },
17
19
  { name: "variants", args: "<2-4> <ask>", group: "explore", description: "parallel design explorations; apply/list/clean" },
18
20
  { name: "sandbox", args: "[on|diff|apply|discard]", group: "explore", description: "asks accumulate in a shadow worktree until you apply" },
19
21
  { name: "undo", group: "explore", description: "revert the last ask (files only)" },
@@ -23,6 +25,7 @@ var COMMANDS = [
23
25
  { name: "btw", args: "<question>", group: "session", description: "read-only side question; the main thread is untouched" },
24
26
  { name: "copy", group: "session", description: "copy the last reply to the clipboard" },
25
27
  { name: "save", group: "session", description: "export the transcript to .squint/transcripts/" },
28
+ { name: "find", args: "<term>", group: "session", description: "search this session and saved transcripts" },
26
29
  { name: "resume", group: "session", description: "pick up the previous session for this repo" },
27
30
  { name: "clear", group: "session", description: "new session (transcript, totals, persisted state)" },
28
31
  { name: "help", group: "session", description: "list commands" },
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  completeCommand
4
- } from "./chunk-S2ODU4MN.js";
4
+ } from "./chunk-SVKV77TO.js";
5
5
  import {
6
6
  applySandbox,
7
7
  discardSandbox,
@@ -42,7 +42,12 @@ import {
42
42
  comparePulse,
43
43
  probeRuntime,
44
44
  runtimeSummary
45
- } from "./chunk-SXF7CVRW.js";
45
+ } from "./chunk-2EJWUBUP.js";
46
+ import {
47
+ clearState,
48
+ loadState,
49
+ saveState
50
+ } from "./chunk-O2S6PAJE.js";
46
51
  import {
47
52
  runAgent
48
53
  } from "./chunk-VH7OOFQP.js";
@@ -53,18 +58,13 @@ import {
53
58
  detectEngines,
54
59
  getEngine
55
60
  } from "./chunk-KVYGPLWW.js";
56
- import "./chunk-XOVOQJFL.js";
61
+ import "./chunk-6FR4TRRQ.js";
57
62
  import {
58
63
  enrich
59
64
  } from "./chunk-H5K55LXY.js";
60
65
  import {
61
66
  composePrompt
62
67
  } from "./chunk-P3H4N2EN.js";
63
- import {
64
- clearState,
65
- loadState,
66
- saveState
67
- } from "./chunk-O2S6PAJE.js";
68
68
 
69
69
  // src/cli.tsx
70
70
  import { Command } from "commander";
@@ -96,6 +96,8 @@ var ConfigSchema = z.object({
96
96
  budgetUsd: z.number().positive().optional(),
97
97
  /** Auto-run /review when the visual pulse shows a big change (default off). */
98
98
  autoReview: z.boolean().optional(),
99
+ /** Cheaper model used for auto-fix and /fix turns (mechanical work). */
100
+ fixModel: z.string().optional(),
99
101
  /** TUI theme name (amber, ocean, moss, rose, mono). */
100
102
  theme: z.string().optional()
101
103
  });
@@ -138,7 +140,7 @@ function resolveModel(config, engineId, override) {
138
140
  function setConfigValue(file, key, value) {
139
141
  const current = readConfigFile(file);
140
142
  let next;
141
- if (key === "engine" || key === "theme") {
143
+ if (key === "engine" || key === "theme" || key === "fixModel") {
142
144
  next = { ...current, [key]: value };
143
145
  } else if (key === "autoDev" || key === "autoFix" || key === "autoProbe" || key === "autoCheck" || key === "autoReview" || key === "bell") {
144
146
  if (value !== "true" && value !== "false") {
@@ -157,7 +159,7 @@ function setConfigValue(file, key, value) {
157
159
  next = { ...current, models: { ...current.models, [engineId]: value } };
158
160
  } else {
159
161
  throw new Error(
160
- `Unknown config key "${key}". Supported: engine, theme, autoDev, autoFix, autoProbe, autoCheck, autoReview, bell, budgetUsd, models.<engineId>`
162
+ `Unknown config key "${key}". Supported: engine, theme, autoDev, autoFix, autoProbe, autoCheck, autoReview, bell, budgetUsd, fixModel, models.<engineId>`
161
163
  );
162
164
  }
163
165
  fs.mkdirSync(path.dirname(file), { recursive: true });
@@ -236,7 +238,7 @@ function registerEnv(program2) {
236
238
  }
237
239
  }
238
240
  const { findChrome: findChrome2 } = await import("./chrome-SBV3H77F.js");
239
- const { hasWebSocket } = await import("./cdp-UOIP4ZQZ.js");
241
+ const { hasWebSocket } = await import("./cdp-VRGCGN5R.js");
240
242
  const chrome = findChrome2();
241
243
  console.log(
242
244
  chrome ? `${pc2.green("\u2713")} Chrome ${pc2.dim(chrome)}` : `${pc2.yellow("\u25CB")} Chrome ${pc2.dim("\u2014 screenshots and runtime probing disabled")}`
@@ -459,7 +461,7 @@ function registerQuality(program2) {
459
461
  if (results.some((r) => !r.ok)) process.exitCode = 1;
460
462
  });
461
463
  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-S6FOTRRR.js");
464
+ const { captureViewports: captureViewports2 } = await import("./preview-X7EWTRCZ.js");
463
465
  const result = await captureViewports2(process.cwd(), url);
464
466
  if (!result) {
465
467
  console.error(pc4.red("\u2717 no Chrome/Chromium found"));
@@ -756,10 +758,11 @@ var Session = class {
756
758
  dispatchFix(problems) {
757
759
  if (problems.length === 0) return;
758
760
  const prompt = this.combinedFixPrompt(problems);
759
- const display = `\u26D1 fix: ${problems.map((p) => p.source).join(" + ")}`;
761
+ const fixModel = this.opts.fixModel;
762
+ const display = `\u26D1 fix: ${problems.map((p) => p.source).join(" + ")}${fixModel ? ` \xB7 ${fixModel}` : ""}`;
760
763
  for (const problem of problems) this.problemPrompts.delete(problem.id);
761
764
  this.notify({ problems: this.state.problems.filter((p) => !problems.includes(p)) });
762
- void this.runTurn(prompt, display);
765
+ void this.runTurn(prompt, display, fixModel);
763
766
  }
764
767
  /** Launch a capped auto-fix turn over all open problems. Returns true if launched. */
765
768
  maybeAutoFix() {
@@ -1006,7 +1009,7 @@ ${question}`,
1006
1009
  }
1007
1010
  };
1008
1011
  /** Run one engine turn. `display` is what the transcript shows as the ask. */
1009
- async runTurn(prompt, display) {
1012
+ async runTurn(prompt, display, modelOverride) {
1010
1013
  this.push("user", display);
1011
1014
  this.turnEdits = 0;
1012
1015
  this.turnTools = 0;
@@ -1019,7 +1022,7 @@ ${question}`,
1019
1022
  {
1020
1023
  prompt,
1021
1024
  cwd: this.execCwd(),
1022
- model: this.state.model,
1025
+ model: modelOverride ?? this.state.model,
1023
1026
  mode: this.state.mode,
1024
1027
  sessionId: engine.supportsResume ? this.sessionId : void 0
1025
1028
  },
@@ -1392,6 +1395,50 @@ They are objective defects, not style preferences.`
1392
1395
  }
1393
1396
  break;
1394
1397
  }
1398
+ case "find": {
1399
+ if (!arg) {
1400
+ this.push("status", "usage: /find <term> \u2014 searches this session and saved transcripts");
1401
+ break;
1402
+ }
1403
+ const needle = arg.toLowerCase();
1404
+ const matches = [];
1405
+ for (const item of this.state.items) {
1406
+ if ((item.role === "user" || item.role === "assistant") && item.text.toLowerCase().includes(needle)) {
1407
+ const line = item.text.split("\n").find((l) => l.toLowerCase().includes(needle)) ?? item.text;
1408
+ matches.push(`[live] ${item.role === "user" ? "\u276F " : ""}${line.trim().slice(0, 90)}`);
1409
+ if (matches.length >= 8) break;
1410
+ }
1411
+ }
1412
+ if (matches.length < 8) {
1413
+ void (async () => {
1414
+ try {
1415
+ const fs3 = await import("fs");
1416
+ const path5 = await import("path");
1417
+ const dir = path5.join(this.opts.cwd, ".squint", "transcripts");
1418
+ const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".md")).sort().reverse().slice(0, 20);
1419
+ for (const file of files) {
1420
+ if (matches.length >= 8) break;
1421
+ const text = fs3.readFileSync(path5.join(dir, file), "utf8");
1422
+ for (const line of text.split("\n")) {
1423
+ if (line.toLowerCase().includes(needle)) {
1424
+ matches.push(`[${file.replace(/\.md$/, "")}] ${line.trim().slice(0, 90)}`);
1425
+ break;
1426
+ }
1427
+ }
1428
+ }
1429
+ } catch {
1430
+ }
1431
+ this.push(
1432
+ "status",
1433
+ matches.length > 0 ? `${matches.join("\n")}
1434
+ /checkpoints can rewind \xB7 /save archives this session` : `no matches for "${arg}"`
1435
+ );
1436
+ })();
1437
+ } else {
1438
+ this.push("status", matches.join("\n"));
1439
+ }
1440
+ break;
1441
+ }
1395
1442
  case "save": {
1396
1443
  void (async () => {
1397
1444
  const fs3 = await import("fs");
@@ -1427,6 +1474,81 @@ They are objective defects, not style preferences.`
1427
1474
  })();
1428
1475
  break;
1429
1476
  }
1477
+ case "flows": {
1478
+ if (!this.state.devUrl) {
1479
+ this.push("error", "dev server not running \u2014 /dev first");
1480
+ break;
1481
+ }
1482
+ void (async () => {
1483
+ const { loadFlows } = await import("./flows-V5ZLVVSQ.js");
1484
+ const flows = loadFlows(this.opts.cwd);
1485
+ if (flows.length === 0) {
1486
+ 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");
1487
+ return;
1488
+ }
1489
+ const chrome = findChrome();
1490
+ if (!chrome) {
1491
+ this.push("error", "no Chrome/Chromium found for flows");
1492
+ return;
1493
+ }
1494
+ const { runFlow } = await import("./cdp-VRGCGN5R.js");
1495
+ const { previewDir } = await import("./preview-X7EWTRCZ.js");
1496
+ this.push("status", `replaying ${flows.length} flow(s)\u2026`);
1497
+ this.notify({ running: true, runStartedAt: Date.now() });
1498
+ const failures = [];
1499
+ for (const flow of flows) {
1500
+ const wanted = arg.trim();
1501
+ if (wanted && flow.name !== wanted) continue;
1502
+ const result = await runFlow(chrome, this.state.devUrl, flow, previewDir(this.opts.cwd));
1503
+ if (result.ok) {
1504
+ this.push("status", `\u2713 flow ${flow.name} \xB7 ${flow.steps.length} steps${result.shots.length > 0 ? ` \xB7 ${result.shots.length} shot(s)` : ""}`);
1505
+ for (const shot of result.shots) this.push("image", shot);
1506
+ } else {
1507
+ const where = result.failedStep ? ` at step ${result.failedStep}` : "";
1508
+ this.push("error", `\u2717 flow ${flow.name}${where}: ${result.detail}`);
1509
+ failures.push(`Flow "${flow.name}" fails${where}: ${result.detail}. The flow file is .squint/flows/${flow.name}.flow \u2014 fix the app (or the flow if the UI legitimately changed).`);
1510
+ }
1511
+ }
1512
+ this.notify({ running: false });
1513
+ if (failures.length > 0) {
1514
+ this.addProblem("flow", `${failures.length} flow(s) failing`, failures.join("\n\n"));
1515
+ this.push("status", "/fix sends open problems to the engine \xB7 /problems lists them");
1516
+ } else {
1517
+ this.clearProblems("flow");
1518
+ }
1519
+ this.drainQueue();
1520
+ })();
1521
+ break;
1522
+ }
1523
+ case "score": {
1524
+ if (!this.state.devUrl) {
1525
+ this.push("error", "dev server not running \u2014 /dev first");
1526
+ break;
1527
+ }
1528
+ void (async () => {
1529
+ const result = await this.capture();
1530
+ if (!result) return;
1531
+ const a11yCount = result.a11y?.length ?? 0;
1532
+ const slopCount = result.slop?.length ?? 0;
1533
+ const runtimeBad = result.runtime ? runtimeSummary(result.runtime) ? 1 : 0 : 0;
1534
+ const problems = this.state.problems.length;
1535
+ const score = Math.max(
1536
+ 0,
1537
+ 5 - problems * 0.75 - Math.min(a11yCount, 4) * 0.25 - Math.min(slopCount, 4) * 0.25 - runtimeBad
1538
+ );
1539
+ const lcp = this.lastPerf?.lcpMs !== void 0 ? ` \xB7 LCP ${this.lastPerf.lcpMs}ms` : "";
1540
+ this.push(
1541
+ "status",
1542
+ [
1543
+ `score: ${score.toFixed(2)}/5 (deterministic axes)`,
1544
+ ` open problems: ${problems}${problems > 0 ? ` (${this.state.problems.map((p) => p.source).join(", ")})` : ""}`,
1545
+ ` a11y findings: ${a11yCount} \xB7 distinctiveness tells: ${slopCount} \xB7 runtime ${runtimeBad ? "dirty" : "clean"}${lcp}`,
1546
+ " /review judges what numbers cannot \u2014 hierarchy, taste, coherence"
1547
+ ].join("\n")
1548
+ );
1549
+ })();
1550
+ break;
1551
+ }
1430
1552
  case "polish": {
1431
1553
  const rounds = arg ? Number.parseInt(arg, 10) : 2;
1432
1554
  if (!Number.isInteger(rounds) || rounds < 1 || rounds > 5) {
@@ -1692,7 +1814,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
1692
1814
  this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
1693
1815
  break;
1694
1816
  case "help": {
1695
- void import("./commands-Y5RUKBPS.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1817
+ void import("./commands-DE5Q7VWY.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1696
1818
  break;
1697
1819
  }
1698
1820
  case "quit":
@@ -2063,6 +2185,7 @@ function App({
2063
2185
  autoProbe,
2064
2186
  autoCheck,
2065
2187
  autoReview,
2188
+ fixModel,
2066
2189
  bell,
2067
2190
  budgetUsd,
2068
2191
  initialTheme
@@ -2081,6 +2204,7 @@ function App({
2081
2204
  autoProbe,
2082
2205
  autoCheck,
2083
2206
  autoReview,
2207
+ fixModel,
2084
2208
  budgetUsd,
2085
2209
  // Delay lets the goodbye summary land in the Static scrollback.
2086
2210
  onQuit: () => setTimeout(() => exit(), 60)
@@ -2298,6 +2422,7 @@ function registerTui(program2) {
2298
2422
  autoProbe: config.autoProbe,
2299
2423
  autoCheck: config.autoCheck,
2300
2424
  autoReview: config.autoReview,
2425
+ fixModel: config.fixModel,
2301
2426
  bell: config.bell,
2302
2427
  budgetUsd: config.budgetUsd,
2303
2428
  initialTheme: theme2
@@ -2308,7 +2433,7 @@ function registerTui(program2) {
2308
2433
  }
2309
2434
 
2310
2435
  // src/cli.tsx
2311
- var VERSION = true ? "0.3.3" : "0.0.0-dev";
2436
+ var VERSION = true ? "0.4.0" : "0.0.0-dev";
2312
2437
  var program = new Command();
2313
2438
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
2314
2439
  registerRun(program);
@@ -3,7 +3,7 @@ import {
3
3
  COMMANDS,
4
4
  commandHelp,
5
5
  completeCommand
6
- } from "./chunk-S2ODU4MN.js";
6
+ } from "./chunk-SVKV77TO.js";
7
7
  export {
8
8
  COMMANDS,
9
9
  commandHelp,
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/preview/flows.ts
4
+ import fs from "fs";
5
+ import path from "path";
6
+ function parseFlow(name, text) {
7
+ const steps = [];
8
+ for (const rawLine of text.split("\n")) {
9
+ const line = rawLine.trim();
10
+ if (line.length === 0 || line.startsWith("#")) continue;
11
+ const [verb, ...rest] = line.split(/\s+/);
12
+ const arg = rest.join(" ");
13
+ switch (verb) {
14
+ case "goto":
15
+ steps.push({ kind: "goto", route: arg.startsWith("/") ? arg : `/${arg}` });
16
+ break;
17
+ case "click":
18
+ steps.push({ kind: "click", target: arg });
19
+ break;
20
+ case "fill": {
21
+ const [selector, ...valueParts] = rest;
22
+ if (!selector || valueParts.length === 0) return null;
23
+ steps.push({ kind: "fill", selector, value: valueParts.join(" ") });
24
+ break;
25
+ }
26
+ case "press":
27
+ steps.push({ kind: "press", key: arg });
28
+ break;
29
+ case "expect":
30
+ steps.push({ kind: "expect", text: arg });
31
+ break;
32
+ case "shot":
33
+ steps.push({ kind: "shot", name: arg.replace(/[^a-zA-Z0-9-]/g, "-") });
34
+ break;
35
+ default:
36
+ return null;
37
+ }
38
+ }
39
+ return steps.length > 0 ? { name, steps } : null;
40
+ }
41
+ function loadFlows(cwd) {
42
+ const dir = path.join(cwd, ".squint", "flows");
43
+ let entries;
44
+ try {
45
+ entries = fs.readdirSync(dir).filter((f) => f.endsWith(".flow"));
46
+ } catch {
47
+ return [];
48
+ }
49
+ const flows = [];
50
+ for (const entry of entries.sort()) {
51
+ try {
52
+ const flow = parseFlow(entry.replace(/\.flow$/, ""), fs.readFileSync(path.join(dir, entry), "utf8"));
53
+ if (flow) flows.push(flow);
54
+ } catch {
55
+ }
56
+ }
57
+ return flows;
58
+ }
59
+ function stepExpression(step) {
60
+ switch (step.kind) {
61
+ case "click":
62
+ return `(() => {
63
+ const target = ${JSON.stringify(step.target)};
64
+ let el = null;
65
+ try { el = document.querySelector(target); } catch {}
66
+ if (!el) {
67
+ const all = [...document.querySelectorAll('a, button, [role=button], input[type=submit], summary, label')];
68
+ el = all.find((e) => (e.textContent || '').trim().toLowerCase().includes(target.toLowerCase()));
69
+ }
70
+ if (!el) return { ok: false, detail: 'no element matching ' + target };
71
+ el.click();
72
+ return { ok: true };
73
+ })()`;
74
+ case "fill":
75
+ return `(() => {
76
+ const el = document.querySelector(${JSON.stringify(step.selector)});
77
+ if (!el) return { ok: false, detail: 'no element matching ${step.selector}' };
78
+ const setter = Object.getOwnPropertyDescriptor(el.constructor.prototype, 'value')?.set;
79
+ if (setter) setter.call(el, ${JSON.stringify(step.value)}); else el.value = ${JSON.stringify(step.value)};
80
+ el.dispatchEvent(new Event('input', { bubbles: true }));
81
+ el.dispatchEvent(new Event('change', { bubbles: true }));
82
+ return { ok: true };
83
+ })()`;
84
+ case "expect":
85
+ return `(() => {
86
+ const wanted = ${JSON.stringify(step.text)}.toLowerCase();
87
+ const ok = (document.body.innerText || '').toLowerCase().includes(wanted);
88
+ return ok ? { ok: true } : { ok: false, detail: 'page does not show: ' + ${JSON.stringify(step.text)} };
89
+ })()`;
90
+ case "press":
91
+ return `(() => {
92
+ const key = ${JSON.stringify(step.key)};
93
+ const el = document.activeElement || document.body;
94
+ for (const type of ['keydown', 'keypress', 'keyup']) {
95
+ el.dispatchEvent(new KeyboardEvent(type, { key, bubbles: true, cancelable: true }));
96
+ }
97
+ return { ok: true };
98
+ })()`;
99
+ case "goto":
100
+ case "shot":
101
+ return null;
102
+ }
103
+ }
104
+ export {
105
+ loadFlows,
106
+ parseFlow,
107
+ stepExpression
108
+ };
@@ -10,11 +10,11 @@ import {
10
10
  probeRuntime,
11
11
  routeShotName,
12
12
  runtimeSummary
13
- } from "./chunk-SXF7CVRW.js";
13
+ } from "./chunk-2EJWUBUP.js";
14
+ import "./chunk-O2S6PAJE.js";
14
15
  import "./chunk-IMDRXXFU.js";
15
16
  import "./chunk-KVYGPLWW.js";
16
- import "./chunk-XOVOQJFL.js";
17
- import "./chunk-O2S6PAJE.js";
17
+ import "./chunk-6FR4TRRQ.js";
18
18
  export {
19
19
  VIEWPORTS,
20
20
  buildReviewPrompt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aayambansal/squint",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
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",