@aayambansal/squint 0.2.3 → 0.2.4
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.
|
@@ -62,6 +62,19 @@ var VIEWPORTS = [
|
|
|
62
62
|
{ name: "tablet", width: 768, height: 1024 },
|
|
63
63
|
{ name: "desktop", width: 1440, height: 900 }
|
|
64
64
|
];
|
|
65
|
+
function loadRoutes(cwd) {
|
|
66
|
+
let lines = [];
|
|
67
|
+
try {
|
|
68
|
+
lines = fs2.readFileSync(path2.join(cwd, ".squint", "routes"), "utf8").split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
const routes = ["/", ...lines.filter((l) => l !== "/")];
|
|
72
|
+
return routes.slice(0, 6).map((r) => r.startsWith("/") ? r : `/${r}`);
|
|
73
|
+
}
|
|
74
|
+
function routeShotName(route) {
|
|
75
|
+
const clean = route.replace(/^\/+|\/+$/g, "").replace(/[^a-zA-Z0-9]+/g, "-");
|
|
76
|
+
return clean.length > 0 ? clean : "root";
|
|
77
|
+
}
|
|
65
78
|
function previewDir(cwd) {
|
|
66
79
|
const dir = path2.join(cwd, ".squint", "preview");
|
|
67
80
|
fs2.mkdirSync(dir, { recursive: true });
|
|
@@ -72,10 +85,26 @@ async function captureViewports(cwd, url) {
|
|
|
72
85
|
const chrome = findChrome();
|
|
73
86
|
if (!chrome) return null;
|
|
74
87
|
const dir = previewDir(cwd);
|
|
88
|
+
const routes = loadRoutes(cwd);
|
|
89
|
+
const base = url.replace(/\/+$/, "");
|
|
75
90
|
if (hasWebSocket()) {
|
|
76
91
|
try {
|
|
77
92
|
const { report, shots: shots2, a11y } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true);
|
|
78
|
-
|
|
93
|
+
const errors2 = [];
|
|
94
|
+
for (const route of routes.slice(1)) {
|
|
95
|
+
try {
|
|
96
|
+
const routeCapture = await cdpCapture(chrome, `${base}${route}`, dir, [
|
|
97
|
+
{ name: routeShotName(route), width: 1440, height: 900 }
|
|
98
|
+
]);
|
|
99
|
+
shots2.push(...routeCapture.shots);
|
|
100
|
+
for (const err of routeCapture.report.pageErrors.slice(0, 3)) {
|
|
101
|
+
errors2.push(`${route}: ${err.split("\n")[0]}`);
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
errors2.push(`${route}: capture failed`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { shots: shots2, errors: errors2, runtime: report, a11y };
|
|
79
108
|
} catch {
|
|
80
109
|
}
|
|
81
110
|
}
|
|
@@ -178,6 +207,8 @@ export {
|
|
|
178
207
|
saveState,
|
|
179
208
|
clearState,
|
|
180
209
|
VIEWPORTS,
|
|
210
|
+
loadRoutes,
|
|
211
|
+
routeShotName,
|
|
181
212
|
previewDir,
|
|
182
213
|
captureViewports,
|
|
183
214
|
runtimeSummary,
|
package/dist/cli.js
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
probeRuntime,
|
|
43
43
|
runtimeSummary,
|
|
44
44
|
saveState
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-CIQ3OUAF.js";
|
|
46
46
|
import "./chunk-OC6RU6XH.js";
|
|
47
47
|
import {
|
|
48
48
|
findChrome
|
|
@@ -80,6 +80,8 @@ var ConfigSchema = z.object({
|
|
|
80
80
|
autoCheck: z.boolean().optional(),
|
|
81
81
|
/** Terminal bell when a turn finishes (default on). */
|
|
82
82
|
bell: z.boolean().optional(),
|
|
83
|
+
/** Session budget in USD; crossing it warns (never blocks). */
|
|
84
|
+
budgetUsd: z.number().positive().optional(),
|
|
83
85
|
/** TUI theme name (amber, ocean, moss, rose, mono). */
|
|
84
86
|
theme: z.string().optional()
|
|
85
87
|
});
|
|
@@ -129,13 +131,19 @@ function setConfigValue(file, key, value) {
|
|
|
129
131
|
throw new Error(`"${key}" must be true or false`);
|
|
130
132
|
}
|
|
131
133
|
next = { ...current, [key]: value === "true" };
|
|
134
|
+
} else if (key === "budgetUsd") {
|
|
135
|
+
const budget = Number.parseFloat(value);
|
|
136
|
+
if (!Number.isFinite(budget) || budget <= 0) {
|
|
137
|
+
throw new Error('"budgetUsd" must be a positive number');
|
|
138
|
+
}
|
|
139
|
+
next = { ...current, budgetUsd: budget };
|
|
132
140
|
} else if (key.startsWith("models.")) {
|
|
133
141
|
const engineId = key.slice("models.".length);
|
|
134
142
|
if (!engineId) throw new Error("Usage: squint config set models.<engineId> <model>");
|
|
135
143
|
next = { ...current, models: { ...current.models, [engineId]: value } };
|
|
136
144
|
} else {
|
|
137
145
|
throw new Error(
|
|
138
|
-
`Unknown config key "${key}". Supported: engine, theme, autoDev, autoFix, autoProbe, autoCheck, bell, models.<engineId>`
|
|
146
|
+
`Unknown config key "${key}". Supported: engine, theme, autoDev, autoFix, autoProbe, autoCheck, bell, budgetUsd, models.<engineId>`
|
|
139
147
|
);
|
|
140
148
|
}
|
|
141
149
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -347,16 +355,32 @@ var Session = class {
|
|
|
347
355
|
}
|
|
348
356
|
turnEdits = 0;
|
|
349
357
|
turnTools = 0;
|
|
358
|
+
toolStreak = 0;
|
|
359
|
+
collapsedTools = 0;
|
|
360
|
+
/**
|
|
361
|
+
* Long tool cascades collapse: the first three of a consecutive burst
|
|
362
|
+
* render, the rest fold into one "+N more" line pushed when the burst
|
|
363
|
+
* ends — append-only, so the Static transcript stays valid.
|
|
364
|
+
*/
|
|
365
|
+
flushToolCollapse() {
|
|
366
|
+
if (this.collapsedTools > 0) {
|
|
367
|
+
this.push("tool", `+${this.collapsedTools} more tool call${this.collapsedTools === 1 ? "" : "s"}`);
|
|
368
|
+
this.collapsedTools = 0;
|
|
369
|
+
}
|
|
370
|
+
this.toolStreak = 0;
|
|
371
|
+
}
|
|
350
372
|
handleEvent = (event) => {
|
|
351
373
|
switch (event.type) {
|
|
352
374
|
case "status":
|
|
353
375
|
this.commitLive();
|
|
376
|
+
this.flushToolCollapse();
|
|
354
377
|
this.push("status", event.text);
|
|
355
378
|
break;
|
|
356
379
|
case "delta":
|
|
357
380
|
this.setLive(this.live + event.text);
|
|
358
381
|
break;
|
|
359
382
|
case "text":
|
|
383
|
+
this.flushToolCollapse();
|
|
360
384
|
if (event.streamed) {
|
|
361
385
|
this.live = "";
|
|
362
386
|
this.state = { ...this.state, liveText: "" };
|
|
@@ -367,17 +391,24 @@ var Session = class {
|
|
|
367
391
|
break;
|
|
368
392
|
case "thinking":
|
|
369
393
|
this.commitLive();
|
|
394
|
+
this.flushToolCollapse();
|
|
370
395
|
this.push("thinking", event.text);
|
|
371
396
|
break;
|
|
372
397
|
case "tool": {
|
|
373
398
|
this.commitLive();
|
|
374
399
|
this.turnTools += 1;
|
|
400
|
+
this.toolStreak += 1;
|
|
375
401
|
if (/edit|write|patch|apply/i.test(event.name)) this.turnEdits += 1;
|
|
376
|
-
this.
|
|
402
|
+
if (this.toolStreak <= 3) {
|
|
403
|
+
this.push("tool", event.detail ? `${event.name} \xB7 ${event.detail}` : event.name);
|
|
404
|
+
} else {
|
|
405
|
+
this.collapsedTools += 1;
|
|
406
|
+
}
|
|
377
407
|
break;
|
|
378
408
|
}
|
|
379
409
|
case "error":
|
|
380
410
|
this.commitLive();
|
|
411
|
+
this.flushToolCollapse();
|
|
381
412
|
this.push("error", event.text);
|
|
382
413
|
break;
|
|
383
414
|
case "result":
|
|
@@ -410,6 +441,7 @@ var Session = class {
|
|
|
410
441
|
);
|
|
411
442
|
this.abort = null;
|
|
412
443
|
this.commitLive();
|
|
444
|
+
this.flushToolCollapse();
|
|
413
445
|
if (result.ok) {
|
|
414
446
|
const cost = result.costUsd !== void 0 ? ` \xB7 $${result.costUsd.toFixed(2)}` : "";
|
|
415
447
|
const secs = result.durationMs !== void 0 ? ` \xB7 ${(result.durationMs / 1e3).toFixed(0)}s` : "";
|
|
@@ -417,12 +449,20 @@ var Session = class {
|
|
|
417
449
|
const stat = checkpoint ? diffStatSince(this.opts.cwd, checkpoint.snapshot) : null;
|
|
418
450
|
const work = stat ? ` \xB7 ${stat}` : this.turnEdits > 0 ? ` \xB7 ${this.turnEdits} edit${this.turnEdits === 1 ? "" : "s"}` : this.turnTools > 0 ? ` \xB7 ${this.turnTools} tool call${this.turnTools === 1 ? "" : "s"}` : "";
|
|
419
451
|
this.push("status", `done${secs}${cost}${work}`);
|
|
452
|
+
const before = this.state.totals.costUsd;
|
|
420
453
|
this.notify({
|
|
421
454
|
totals: {
|
|
422
|
-
costUsd:
|
|
455
|
+
costUsd: before + (result.costUsd ?? 0),
|
|
423
456
|
turns: this.state.totals.turns + 1
|
|
424
457
|
}
|
|
425
458
|
});
|
|
459
|
+
const budget = this.opts.budgetUsd;
|
|
460
|
+
if (budget && before < budget && this.state.totals.costUsd >= budget) {
|
|
461
|
+
this.push(
|
|
462
|
+
"error",
|
|
463
|
+
`session cost $${this.state.totals.costUsd.toFixed(2)} crossed your $${budget.toFixed(2)} budget \u2014 squint keeps working, this is just the flag you asked for`
|
|
464
|
+
);
|
|
465
|
+
}
|
|
426
466
|
if (this.sessionId) {
|
|
427
467
|
saveState(this.opts.cwd, {
|
|
428
468
|
engine: this.state.engineId,
|
|
@@ -1227,6 +1267,7 @@ function App({
|
|
|
1227
1267
|
autoProbe,
|
|
1228
1268
|
autoCheck,
|
|
1229
1269
|
bell,
|
|
1270
|
+
budgetUsd,
|
|
1230
1271
|
initialTheme
|
|
1231
1272
|
}) {
|
|
1232
1273
|
const { exit } = useApp();
|
|
@@ -1242,6 +1283,7 @@ function App({
|
|
|
1242
1283
|
autoFix,
|
|
1243
1284
|
autoProbe,
|
|
1244
1285
|
autoCheck,
|
|
1286
|
+
budgetUsd,
|
|
1245
1287
|
// Delay lets the goodbye summary land in the Static scrollback.
|
|
1246
1288
|
onQuit: () => setTimeout(() => exit(), 60)
|
|
1247
1289
|
});
|
|
@@ -1431,7 +1473,7 @@ function App({
|
|
|
1431
1473
|
|
|
1432
1474
|
// src/cli.tsx
|
|
1433
1475
|
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1434
|
-
var VERSION = "0.2.
|
|
1476
|
+
var VERSION = "0.2.4";
|
|
1435
1477
|
var program = new Command();
|
|
1436
1478
|
program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
|
|
1437
1479
|
program.command("run").description("Run one prompt headlessly and stream the result").argument("<prompt...>", "what to build or change").option("-e, --engine <id>", "engine to use (claude, codex, gemini, opencode)").option("-m, --model <name>", "model override for the engine").option("--no-brief", "send the prompt without the squint design brief").option("--json", "emit normalized agent events as ndjson").action(
|
|
@@ -1728,7 +1770,7 @@ program.command("check").description("Run this project\u2019s quality gates (typ
|
|
|
1728
1770
|
if (results.some((r) => !r.ok)) process.exitCode = 1;
|
|
1729
1771
|
});
|
|
1730
1772
|
program.command("shot").description("Screenshot a running app at mobile/tablet/desktop viewports").argument("<url>", "URL of the running app (e.g. http://localhost:5173)").action(async (url) => {
|
|
1731
|
-
const { captureViewports: captureViewports2 } = await import("./preview-
|
|
1773
|
+
const { captureViewports: captureViewports2 } = await import("./preview-LZDH65ID.js");
|
|
1732
1774
|
const result = await captureViewports2(process.cwd(), url);
|
|
1733
1775
|
if (!result) {
|
|
1734
1776
|
console.error(pc.red("\u2717 no Chrome/Chromium found"));
|
|
@@ -1772,6 +1814,7 @@ program.action(() => {
|
|
|
1772
1814
|
autoProbe: config.autoProbe,
|
|
1773
1815
|
autoCheck: config.autoCheck,
|
|
1774
1816
|
bell: config.bell,
|
|
1817
|
+
budgetUsd: config.budgetUsd,
|
|
1775
1818
|
initialTheme: config.theme
|
|
1776
1819
|
}
|
|
1777
1820
|
)
|
|
@@ -5,10 +5,12 @@ import {
|
|
|
5
5
|
buildRuntimeFixPrompt,
|
|
6
6
|
captureViewports,
|
|
7
7
|
comparePulse,
|
|
8
|
+
loadRoutes,
|
|
8
9
|
previewDir,
|
|
9
10
|
probeRuntime,
|
|
11
|
+
routeShotName,
|
|
10
12
|
runtimeSummary
|
|
11
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-CIQ3OUAF.js";
|
|
12
14
|
import "./chunk-OC6RU6XH.js";
|
|
13
15
|
import "./chunk-P3V5CWH5.js";
|
|
14
16
|
import "./chunk-2LRIKWBU.js";
|
|
@@ -18,7 +20,9 @@ export {
|
|
|
18
20
|
buildRuntimeFixPrompt,
|
|
19
21
|
captureViewports,
|
|
20
22
|
comparePulse,
|
|
23
|
+
loadRoutes,
|
|
21
24
|
previewDir,
|
|
22
25
|
probeRuntime,
|
|
26
|
+
routeShotName,
|
|
23
27
|
runtimeSummary
|
|
24
28
|
};
|
package/package.json
CHANGED