@aayambansal/squint 0.1.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/dist/cli.js ADDED
@@ -0,0 +1,968 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DevServer,
4
+ buildFixPrompt,
5
+ detectDevCommand
6
+ } from "./chunk-FQDSJRTY.js";
7
+ import {
8
+ runAgent
9
+ } from "./chunk-MGHJSERZ.js";
10
+ import {
11
+ composePrompt
12
+ } from "./chunk-NT2HR4RD.js";
13
+ import {
14
+ buildGatePrompt,
15
+ detectGates,
16
+ runGates
17
+ } from "./chunk-43VZK47W.js";
18
+ import {
19
+ buildReviewPrompt,
20
+ buildRuntimeFixPrompt,
21
+ captureViewports,
22
+ clearState,
23
+ loadState,
24
+ probeRuntime,
25
+ runtimeSummary,
26
+ saveState
27
+ } from "./chunk-4XHTAWTH.js";
28
+ import "./chunk-OJTW5SYY.js";
29
+ import "./chunk-MURKQ7SI.js";
30
+ import {
31
+ detectEngines,
32
+ engines,
33
+ getEngine
34
+ } from "./chunk-YIVPCWSG.js";
35
+ import {
36
+ restoreSnapshot,
37
+ takeSnapshot
38
+ } from "./chunk-WTA6YYBY.js";
39
+
40
+ // src/cli.tsx
41
+ import { Command } from "commander";
42
+ import { render } from "ink";
43
+ import pc from "picocolors";
44
+
45
+ // src/config/config.ts
46
+ import fs from "fs";
47
+ import os from "os";
48
+ import path from "path";
49
+ import { z } from "zod";
50
+ var ConfigSchema = z.object({
51
+ /** Default engine id (claude, codex, gemini, opencode, amp, cursor, copilot, aider). */
52
+ engine: z.string().optional(),
53
+ /** Per-engine model overrides, e.g. { claude: "claude-sonnet-5" }. */
54
+ models: z.record(z.string()).optional(),
55
+ /** Start the project's dev server automatically when the TUI opens. */
56
+ autoDev: z.boolean().optional(),
57
+ /** Automatically send dev-server errors back to the engine (max 2 attempts). */
58
+ autoFix: z.boolean().optional(),
59
+ /** Probe the running app's runtime after each clean turn (default on). */
60
+ autoProbe: z.boolean().optional()
61
+ });
62
+ function defaultPaths(cwd) {
63
+ const xdg = process.env.XDG_CONFIG_HOME;
64
+ const base = xdg && xdg.length > 0 ? xdg : path.join(os.homedir(), ".config");
65
+ return {
66
+ globalFile: path.join(base, "squint", "config.json"),
67
+ projectFile: path.join(cwd, ".squint", "config.json")
68
+ };
69
+ }
70
+ function readConfigFile(file) {
71
+ let raw;
72
+ try {
73
+ raw = fs.readFileSync(file, "utf8");
74
+ } catch {
75
+ return {};
76
+ }
77
+ try {
78
+ return ConfigSchema.parse(JSON.parse(raw));
79
+ } catch (err) {
80
+ throw new Error(`Invalid config at ${file}: ${err instanceof Error ? err.message : String(err)}`);
81
+ }
82
+ }
83
+ function loadConfig(paths) {
84
+ const global = readConfigFile(paths.globalFile);
85
+ const project = readConfigFile(paths.projectFile);
86
+ return {
87
+ ...global,
88
+ ...project,
89
+ models: { ...global.models, ...project.models }
90
+ };
91
+ }
92
+ function resolveEngineId(config, override) {
93
+ return override ?? config.engine ?? "claude";
94
+ }
95
+ function resolveModel(config, engineId, override) {
96
+ return override ?? config.models?.[engineId];
97
+ }
98
+ function setConfigValue(file, key, value) {
99
+ const current = readConfigFile(file);
100
+ let next;
101
+ if (key === "engine") {
102
+ next = { ...current, engine: value };
103
+ } else if (key === "autoDev" || key === "autoFix" || key === "autoProbe") {
104
+ if (value !== "true" && value !== "false") {
105
+ throw new Error(`"${key}" must be true or false`);
106
+ }
107
+ next = { ...current, [key]: value === "true" };
108
+ } else if (key.startsWith("models.")) {
109
+ const engineId = key.slice("models.".length);
110
+ if (!engineId) throw new Error("Usage: squint config set models.<engineId> <model>");
111
+ next = { ...current, models: { ...current.models, [engineId]: value } };
112
+ } else {
113
+ throw new Error(`Unknown config key "${key}". Supported: engine, autoDev, autoFix, autoProbe, models.<engineId>`);
114
+ }
115
+ fs.mkdirSync(path.dirname(file), { recursive: true });
116
+ fs.writeFileSync(file, JSON.stringify(next, null, 2) + "\n");
117
+ return next;
118
+ }
119
+
120
+ // src/tui/App.tsx
121
+ import { Box, Static, Text as Text2, useApp, useInput } from "ink";
122
+ import path2 from "path";
123
+ import { useCallback, useEffect as useEffect2, useRef, useState as useState2 } from "react";
124
+
125
+ // src/tui/messages.tsx
126
+ import { Text } from "ink";
127
+ import { useEffect, useState } from "react";
128
+
129
+ // src/tui/theme.ts
130
+ var theme = {
131
+ accent: "#e8a33d",
132
+ dim: "gray",
133
+ user: "#7aa2f7",
134
+ error: "#f7768e",
135
+ success: "#9ece6a",
136
+ tool: "#7dcfff"
137
+ };
138
+
139
+ // src/tui/messages.tsx
140
+ import { jsx, jsxs } from "react/jsx-runtime";
141
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
142
+ function WorkingLine({ startedAt }) {
143
+ const [frame, setFrame] = useState(0);
144
+ const [elapsed, setElapsed] = useState(0);
145
+ useEffect(() => {
146
+ const timer = setInterval(() => {
147
+ setFrame((f) => (f + 1) % SPINNER_FRAMES.length);
148
+ setElapsed(Math.floor((Date.now() - startedAt) / 1e3));
149
+ }, 80);
150
+ return () => clearInterval(timer);
151
+ }, [startedAt]);
152
+ return /* @__PURE__ */ jsxs(Text, { children: [
153
+ /* @__PURE__ */ jsx(Text, { color: theme.accent, children: SPINNER_FRAMES[frame] }),
154
+ /* @__PURE__ */ jsxs(Text, { color: theme.dim, children: [
155
+ " ",
156
+ "working\u2026 ",
157
+ elapsed,
158
+ "s \xB7 esc to interrupt"
159
+ ] })
160
+ ] });
161
+ }
162
+ function MessageLine({ message }) {
163
+ switch (message.role) {
164
+ case "user":
165
+ return /* @__PURE__ */ jsxs(Text, { color: theme.user, wrap: "wrap", children: [
166
+ "\u276F ",
167
+ message.text
168
+ ] });
169
+ case "assistant":
170
+ return /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: message.text });
171
+ case "status":
172
+ return /* @__PURE__ */ jsxs(Text, { color: theme.dim, wrap: "wrap", children: [
173
+ "\xB7 ",
174
+ message.text
175
+ ] });
176
+ case "tool":
177
+ return /* @__PURE__ */ jsxs(Text, { color: theme.tool, wrap: "wrap", children: [
178
+ "\u2699 ",
179
+ message.text
180
+ ] });
181
+ case "thinking":
182
+ return /* @__PURE__ */ jsx(Text, { color: theme.dim, italic: true, wrap: "wrap", children: message.text });
183
+ case "error":
184
+ return /* @__PURE__ */ jsxs(Text, { color: theme.error, wrap: "wrap", children: [
185
+ "\u2717 ",
186
+ message.text
187
+ ] });
188
+ }
189
+ }
190
+
191
+ // src/tui/App.tsx
192
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
193
+ var MAX_AUTO_FIX_ATTEMPTS = 2;
194
+ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
195
+ function App({ cwd, initialEngine, initialModel, autoDev, autoFix, autoProbe }) {
196
+ const { exit } = useApp();
197
+ const [messages, setMessages] = useState2([]);
198
+ const [input, setInput] = useState2("");
199
+ const [running, setRunning] = useState2(false);
200
+ const [runStartedAt, setRunStartedAt] = useState2(0);
201
+ const [engineId, setEngineId] = useState2(initialEngine);
202
+ const [model, setModel] = useState2(initialModel);
203
+ const [liveText, setLiveText] = useState2("");
204
+ const [devState, setDevState] = useState2("stopped");
205
+ const [devUrl, setDevUrl] = useState2(null);
206
+ const liveRef = useRef("");
207
+ const idRef = useRef(0);
208
+ const sessionRef = useRef(void 0);
209
+ const devRef = useRef(null);
210
+ const abortRef = useRef(null);
211
+ const pendingFixRef = useRef(null);
212
+ const snapshotRef = useRef(null);
213
+ const fixAttemptsRef = useRef(0);
214
+ const reviewTipShownRef = useRef(false);
215
+ const historyRef = useRef([]);
216
+ const historyIndexRef = useRef(-1);
217
+ const push = useCallback((role, text) => {
218
+ idRef.current += 1;
219
+ setMessages((prev) => [...prev, { id: idRef.current, role, text }]);
220
+ }, []);
221
+ const commitLive = useCallback(() => {
222
+ if (liveRef.current.length > 0) {
223
+ push("assistant", liveRef.current);
224
+ liveRef.current = "";
225
+ setLiveText("");
226
+ }
227
+ }, [push]);
228
+ const getDevServer = useCallback(() => {
229
+ if (!devRef.current) {
230
+ devRef.current = new DevServer(cwd, {
231
+ onStateChange: setDevState,
232
+ onUrl: setDevUrl
233
+ });
234
+ }
235
+ return devRef.current;
236
+ }, [cwd]);
237
+ useEffect2(() => {
238
+ if (autoDev && detectDevCommand(cwd)) {
239
+ getDevServer().start();
240
+ }
241
+ const saved = loadState(cwd);
242
+ if (saved) {
243
+ try {
244
+ if (getEngine(saved.engine).supportsResume) {
245
+ const mins = Math.max(1, Math.round((Date.now() - saved.at) / 6e4));
246
+ push(
247
+ "status",
248
+ `previous session (${mins}m ago${saved.lastAsk ? ` \xB7 "${saved.lastAsk}"` : ""}) \u2014 /resume to continue`
249
+ );
250
+ }
251
+ } catch {
252
+ }
253
+ }
254
+ return () => devRef.current?.stop();
255
+ }, [autoDev, cwd, getDevServer, push]);
256
+ const handleEvent = useCallback(
257
+ (event) => {
258
+ switch (event.type) {
259
+ case "status":
260
+ commitLive();
261
+ push("status", event.text);
262
+ break;
263
+ case "delta":
264
+ liveRef.current += event.text;
265
+ setLiveText(liveRef.current);
266
+ break;
267
+ case "text":
268
+ if (event.streamed) {
269
+ liveRef.current = "";
270
+ setLiveText("");
271
+ push("assistant", event.text);
272
+ } else {
273
+ liveRef.current += (liveRef.current.length > 0 ? "\n" : "") + event.text;
274
+ setLiveText(liveRef.current);
275
+ }
276
+ break;
277
+ case "thinking":
278
+ commitLive();
279
+ push("thinking", event.text);
280
+ break;
281
+ case "tool":
282
+ commitLive();
283
+ push("tool", event.detail ? `${event.name} \xB7 ${event.detail}` : event.name);
284
+ break;
285
+ case "error":
286
+ commitLive();
287
+ push("error", event.text);
288
+ break;
289
+ case "result":
290
+ if (event.sessionId) sessionRef.current = event.sessionId;
291
+ break;
292
+ case "raw":
293
+ break;
294
+ }
295
+ },
296
+ [push, commitLive]
297
+ );
298
+ const runTurn = useCallback(
299
+ async (prompt, display) => {
300
+ push("user", display);
301
+ setRunning(true);
302
+ setRunStartedAt(Date.now());
303
+ const runStart = Date.now();
304
+ const engine = getEngine(engineId);
305
+ const abort = new AbortController();
306
+ abortRef.current = abort;
307
+ const result = await runAgent(
308
+ engine,
309
+ {
310
+ prompt,
311
+ cwd,
312
+ model,
313
+ sessionId: engine.supportsResume ? sessionRef.current : void 0
314
+ },
315
+ handleEvent,
316
+ abort.signal
317
+ );
318
+ abortRef.current = null;
319
+ commitLive();
320
+ if (result.ok) {
321
+ const cost = result.costUsd !== void 0 ? ` \xB7 $${result.costUsd.toFixed(2)}` : "";
322
+ const secs = result.durationMs !== void 0 ? ` \xB7 ${(result.durationMs / 1e3).toFixed(0)}s` : "";
323
+ push("status", `done${secs}${cost}`);
324
+ if (sessionRef.current) {
325
+ saveState(cwd, {
326
+ engine: engineId,
327
+ sessionId: sessionRef.current,
328
+ model,
329
+ lastAsk: display.length > 80 ? `${display.slice(0, 79)}\u2026` : display,
330
+ at: Date.now()
331
+ });
332
+ }
333
+ }
334
+ const dev = devRef.current;
335
+ if (result.error !== "interrupted" && dev && (dev.state === "running" || dev.state === "starting")) {
336
+ await delay(1500);
337
+ const errors = dev.errorsSince(runStart);
338
+ if (errors.length > 0) {
339
+ pendingFixRef.current = {
340
+ prompt: buildFixPrompt(errors, dev.tail(30)),
341
+ display: "\u26D1 fix dev server errors"
342
+ };
343
+ push("error", `dev server: ${errors.length} error line(s)
344
+ ${errors.slice(-5).join("\n")}`);
345
+ if (autoFix && fixAttemptsRef.current < MAX_AUTO_FIX_ATTEMPTS) {
346
+ fixAttemptsRef.current += 1;
347
+ push("status", `auto-fix attempt ${fixAttemptsRef.current}/${MAX_AUTO_FIX_ATTEMPTS}`);
348
+ setRunning(false);
349
+ await runTurn(pendingFixRef.current.prompt, pendingFixRef.current.display);
350
+ return;
351
+ }
352
+ push("status", "type /fix to send them to the engine");
353
+ } else {
354
+ pendingFixRef.current = null;
355
+ if (autoProbe !== false && devUrl) {
356
+ const report = await probeRuntime(devUrl);
357
+ const summary = report ? runtimeSummary(report) : null;
358
+ if (report && summary) {
359
+ pendingFixRef.current = {
360
+ prompt: buildRuntimeFixPrompt(report),
361
+ display: "\u26D1 fix runtime errors"
362
+ };
363
+ push("error", `runtime: ${summary}`);
364
+ if (autoFix && fixAttemptsRef.current < MAX_AUTO_FIX_ATTEMPTS) {
365
+ fixAttemptsRef.current += 1;
366
+ push("status", `auto-fix attempt ${fixAttemptsRef.current}/${MAX_AUTO_FIX_ATTEMPTS}`);
367
+ setRunning(false);
368
+ await runTurn(pendingFixRef.current.prompt, pendingFixRef.current.display);
369
+ return;
370
+ }
371
+ push("status", "type /fix to send them to the engine");
372
+ }
373
+ }
374
+ if (!reviewTipShownRef.current && devUrl) {
375
+ reviewTipShownRef.current = true;
376
+ push("status", "tip: /review screenshots the app and has the engine critique its own work");
377
+ }
378
+ }
379
+ }
380
+ setRunning(false);
381
+ },
382
+ [cwd, engineId, model, push, handleEvent, commitLive, autoFix, autoProbe, devUrl]
383
+ );
384
+ const capture = useCallback(async () => {
385
+ if (!devUrl) {
386
+ push("error", "dev server not running \u2014 /dev first");
387
+ return null;
388
+ }
389
+ push("status", "capturing screenshots\u2026");
390
+ const result = await captureViewports(cwd, devUrl);
391
+ if (!result) {
392
+ push("error", "no Chrome/Chromium found for screenshots");
393
+ return null;
394
+ }
395
+ for (const err of result.errors) push("error", `screenshot ${err}`);
396
+ if (result.shots.length > 0) {
397
+ push("status", `captured ${result.shots.map((s) => s.name).join(", ")} \u2192 ${path2.dirname(result.shots[0].path)}`);
398
+ }
399
+ if (result.runtime) {
400
+ const summary = runtimeSummary(result.runtime);
401
+ if (summary) {
402
+ push("error", `runtime: ${summary}`);
403
+ pendingFixRef.current = {
404
+ prompt: buildRuntimeFixPrompt(result.runtime),
405
+ display: "\u26D1 fix runtime errors"
406
+ };
407
+ push("status", "type /fix to send them to the engine");
408
+ } else {
409
+ push("status", "runtime clean \u2014 no console errors, exceptions, or failed requests");
410
+ }
411
+ }
412
+ return result.shots.length > 0 ? result : null;
413
+ }, [cwd, devUrl, push]);
414
+ const submit = useCallback(
415
+ async (ask) => {
416
+ fixAttemptsRef.current = 0;
417
+ snapshotRef.current = takeSnapshot(cwd);
418
+ const isFirstTurn = sessionRef.current === void 0;
419
+ const prompt = isFirstTurn ? composePrompt(ask, { cwd, firstTurn: true }) : ask;
420
+ await runTurn(prompt, ask);
421
+ },
422
+ [cwd, runTurn]
423
+ );
424
+ const handleCommand = useCallback(
425
+ (command) => {
426
+ const [name, ...rest] = command.slice(1).split(/\s+/);
427
+ const arg = rest.join(" ").trim();
428
+ switch (name) {
429
+ case "engine":
430
+ if (!arg) {
431
+ push("status", `engines: ${engines.map((e) => e.id).join(", ")}`);
432
+ } else {
433
+ try {
434
+ getEngine(arg);
435
+ setEngineId(arg);
436
+ sessionRef.current = void 0;
437
+ push("status", `engine \u2192 ${arg} (new session)`);
438
+ } catch (err) {
439
+ push("error", err instanceof Error ? err.message : String(err));
440
+ }
441
+ }
442
+ break;
443
+ case "model":
444
+ setModel(arg || void 0);
445
+ push("status", arg ? `model \u2192 ${arg}` : "model \u2192 engine default");
446
+ break;
447
+ case "dev": {
448
+ const dev = getDevServer();
449
+ if (dev.state === "stopped" || dev.state === "crashed") {
450
+ const devCommand = detectDevCommand(cwd);
451
+ if (!devCommand) {
452
+ push("error", "no dev/start script found in package.json");
453
+ } else {
454
+ dev.start(devCommand);
455
+ push("status", `dev server starting \xB7 ${devCommand.display}`);
456
+ }
457
+ } else {
458
+ dev.stop();
459
+ setDevUrl(null);
460
+ push("status", "dev server stopped");
461
+ }
462
+ break;
463
+ }
464
+ case "fix":
465
+ if (!pendingFixRef.current) {
466
+ push("status", "nothing to fix \u2014 no captured errors or failed gates");
467
+ } else {
468
+ void runTurn(pendingFixRef.current.prompt, pendingFixRef.current.display);
469
+ }
470
+ break;
471
+ case "check":
472
+ void (async () => {
473
+ const gates = detectGates(cwd);
474
+ if (gates.length === 0) {
475
+ push("status", "no gates detected in this project");
476
+ return;
477
+ }
478
+ push("status", `running gates: ${gates.map((g) => g.id).join(" \u2192 ")}`);
479
+ setRunning(true);
480
+ setRunStartedAt(Date.now());
481
+ const results = await runGates(cwd, gates, (result) => {
482
+ push(
483
+ result.ok ? "status" : "error",
484
+ `${result.ok ? "\u2713" : "\u2717"} ${result.gate.id} \xB7 ${(result.durationMs / 1e3).toFixed(1)}s`
485
+ );
486
+ });
487
+ setRunning(false);
488
+ const failures = results.filter((r) => !r.ok);
489
+ if (failures.length > 0) {
490
+ pendingFixRef.current = {
491
+ prompt: buildGatePrompt(failures),
492
+ display: `\u26D1 fix failing gates: ${failures.map((f) => f.gate.id).join(", ")}`
493
+ };
494
+ push("status", "type /fix to send failures to the engine");
495
+ } else {
496
+ push("status", "all gates passed");
497
+ }
498
+ })();
499
+ break;
500
+ case "shot":
501
+ void capture();
502
+ break;
503
+ case "review":
504
+ void (async () => {
505
+ const result = await capture();
506
+ if (result) {
507
+ await runTurn(
508
+ buildReviewPrompt(result.shots, arg || void 0, result.runtime),
509
+ `\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
510
+ );
511
+ }
512
+ })();
513
+ break;
514
+ case "undo": {
515
+ const snapshot = snapshotRef.current;
516
+ if (!snapshot) {
517
+ push("status", "nothing to undo \u2014 no ask this session, or not a git repo with commits");
518
+ break;
519
+ }
520
+ const result = restoreSnapshot(cwd, snapshot);
521
+ if (result.restored) {
522
+ snapshotRef.current = null;
523
+ push(
524
+ "status",
525
+ `reverted the last ask${result.deletedFiles > 0 ? ` \xB7 removed ${result.deletedFiles} created file(s)` : ""}`
526
+ );
527
+ } else {
528
+ push("error", `undo failed: ${result.detail ?? "unknown error"}`);
529
+ }
530
+ break;
531
+ }
532
+ case "resume": {
533
+ const saved = loadState(cwd);
534
+ if (!saved) {
535
+ push("status", "no previous session for this project");
536
+ break;
537
+ }
538
+ try {
539
+ if (!getEngine(saved.engine).supportsResume) {
540
+ push("status", `previous engine ${saved.engine} cannot resume sessions`);
541
+ break;
542
+ }
543
+ } catch {
544
+ push("error", `previous engine ${saved.engine} is no longer available`);
545
+ break;
546
+ }
547
+ setEngineId(saved.engine);
548
+ if (saved.model) setModel(saved.model);
549
+ sessionRef.current = saved.sessionId;
550
+ push("status", `resumed ${saved.engine} session${saved.lastAsk ? ` \xB7 "${saved.lastAsk}"` : ""}`);
551
+ break;
552
+ }
553
+ case "clear":
554
+ setMessages([]);
555
+ sessionRef.current = void 0;
556
+ clearState(cwd);
557
+ break;
558
+ case "help":
559
+ push(
560
+ "status",
561
+ "/engine <id> \xB7 /model <name> \xB7 /dev (start/stop server) \xB7 /check (quality gates) \xB7 /fix (send failures) \xB7 /shot (screenshots) \xB7 /review [focus] (visual self-critique) \xB7 /undo (revert last ask) \xB7 /resume (last session) \xB7 /clear (new session) \xB7 /quit"
562
+ );
563
+ break;
564
+ case "quit":
565
+ case "exit":
566
+ devRef.current?.stop();
567
+ exit();
568
+ break;
569
+ default:
570
+ push("error", `unknown command /${name} \u2014 try /help`);
571
+ }
572
+ },
573
+ [push, exit, cwd, getDevServer, runTurn, capture]
574
+ );
575
+ useInput((char, key) => {
576
+ if (key.ctrl && char === "c") {
577
+ devRef.current?.stop();
578
+ exit();
579
+ return;
580
+ }
581
+ if (running) {
582
+ if (key.escape) abortRef.current?.abort();
583
+ return;
584
+ }
585
+ if (key.return) {
586
+ const value = input.trim();
587
+ setInput("");
588
+ historyIndexRef.current = -1;
589
+ if (!value) return;
590
+ historyRef.current.push(value);
591
+ if (value.startsWith("/")) {
592
+ handleCommand(value);
593
+ } else {
594
+ void submit(value);
595
+ }
596
+ return;
597
+ }
598
+ if (key.upArrow) {
599
+ const history = historyRef.current;
600
+ if (history.length === 0) return;
601
+ const next = historyIndexRef.current === -1 ? history.length - 1 : Math.max(historyIndexRef.current - 1, 0);
602
+ historyIndexRef.current = next;
603
+ setInput(history[next] ?? "");
604
+ return;
605
+ }
606
+ if (key.downArrow) {
607
+ const history = historyRef.current;
608
+ if (historyIndexRef.current === -1) return;
609
+ const next = historyIndexRef.current + 1;
610
+ if (next >= history.length) {
611
+ historyIndexRef.current = -1;
612
+ setInput("");
613
+ } else {
614
+ historyIndexRef.current = next;
615
+ setInput(history[next] ?? "");
616
+ }
617
+ return;
618
+ }
619
+ if (key.backspace || key.delete) {
620
+ setInput((prev) => prev.slice(0, -1));
621
+ return;
622
+ }
623
+ if (char && !key.ctrl && !key.meta) {
624
+ setInput((prev) => prev + char);
625
+ }
626
+ });
627
+ const devBadge = devState === "running" ? ` \xB7 ${devUrl ?? "dev running"}` : devState === "starting" ? " \xB7 dev starting\u2026" : devState === "crashed" ? " \xB7 dev crashed" : "";
628
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", paddingX: 1, children: [
629
+ /* @__PURE__ */ jsx2(Static, { items: messages, children: (message) => /* @__PURE__ */ jsx2(Box, { children: /* @__PURE__ */ jsx2(MessageLine, { message }) }, message.id) }),
630
+ liveText.length > 0 && /* @__PURE__ */ jsx2(Box, { children: /* @__PURE__ */ jsx2(Text2, { wrap: "wrap", children: liveText }) }),
631
+ /* @__PURE__ */ jsx2(Box, { marginTop: 1, children: running ? /* @__PURE__ */ jsx2(WorkingLine, { startedAt: runStartedAt }) : /* @__PURE__ */ jsxs2(Text2, { children: [
632
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: "\u276F " }),
633
+ input,
634
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: "\u258F" })
635
+ ] }) }),
636
+ /* @__PURE__ */ jsx2(Box, { children: /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
637
+ engineId,
638
+ model ? ` \xB7 ${model}` : "",
639
+ " \xB7 ",
640
+ path2.basename(cwd),
641
+ devBadge,
642
+ " \xB7 /help"
643
+ ] }) })
644
+ ] });
645
+ }
646
+
647
+ // src/cli.tsx
648
+ import { jsx as jsx3 } from "react/jsx-runtime";
649
+ var VERSION = "0.1.0";
650
+ var program = new Command();
651
+ program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
652
+ 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(
653
+ async (promptWords, options) => {
654
+ const cwd = process.cwd();
655
+ const config = loadConfig(defaultPaths(cwd));
656
+ const engineId = resolveEngineId(config, options.engine);
657
+ const engine = getEngine(engineId);
658
+ const model = resolveModel(config, engineId, options.model);
659
+ const ask = promptWords.join(" ");
660
+ const prompt = composePrompt(ask, { cwd, noBrief: !options.brief });
661
+ const onEvent = options.json ? (event) => {
662
+ if (event.type !== "delta") console.log(JSON.stringify(event));
663
+ } : createPrinter();
664
+ if (!options.json) console.log(pc.dim(`squint \xB7 ${engine.id}${model ? ` \xB7 ${model}` : ""}`));
665
+ const result = await runAgent(engine, { prompt, cwd, model }, onEvent);
666
+ if (options.json) {
667
+ console.log(JSON.stringify({ type: "summary", ...result }));
668
+ } else if (result.ok) {
669
+ const cost = result.costUsd !== void 0 ? ` \xB7 $${result.costUsd.toFixed(2)}` : "";
670
+ const secs = result.durationMs !== void 0 ? ` \xB7 ${(result.durationMs / 1e3).toFixed(0)}s` : "";
671
+ console.log(pc.green(`\u2713 done${secs}${cost}`));
672
+ }
673
+ if (!result.ok) process.exitCode = 1;
674
+ }
675
+ );
676
+ program.command("engines").description("List engines and whether they are installed").action(() => {
677
+ for (const { engine, path: binaryPath } of detectEngines()) {
678
+ const status = binaryPath ? pc.green("\u2713") : pc.red("\u2717");
679
+ const location = binaryPath ?? pc.dim(`not found \u2014 ${engine.install}`);
680
+ console.log(`${status} ${engine.id.padEnd(10)} ${engine.name.padEnd(14)} ${location}`);
681
+ }
682
+ });
683
+ program.command("doctor").description("Check squint prerequisites and engine availability").action(async () => {
684
+ const [major] = process.versions.node.split(".");
685
+ const nodeOk = Number(major) >= 20;
686
+ console.log(`${nodeOk ? pc.green("\u2713") : pc.red("\u2717")} node ${process.versions.node}${nodeOk ? "" : " (need >= 20)"}`);
687
+ const detected = detectEngines();
688
+ for (const { engine, path: binaryPath } of detected) {
689
+ const status = binaryPath ? pc.green("\u2713") : pc.yellow("\u25CB");
690
+ console.log(`${status} ${engine.name}${binaryPath ? "" : pc.dim(` \u2014 install: ${engine.install}`)}`);
691
+ }
692
+ const { findChrome } = await import("./chrome-4WNYZZ42.js");
693
+ const { hasWebSocket } = await import("./cdp-5OHNGQXV.js");
694
+ const chrome = findChrome();
695
+ console.log(
696
+ chrome ? `${pc.green("\u2713")} Chrome ${pc.dim(chrome)}` : `${pc.yellow("\u25CB")} Chrome ${pc.dim("\u2014 screenshots and runtime probing disabled")}`
697
+ );
698
+ console.log(
699
+ hasWebSocket() ? `${pc.green("\u2713")} WebSocket ${pc.dim("runtime console/network capture available")}` : `${pc.yellow("\u25CB")} WebSocket ${pc.dim("\u2014 node 22+ enables runtime capture; screenshots still work")}`
700
+ );
701
+ const available = detected.filter((d) => d.path !== null);
702
+ if (available.length === 0) {
703
+ console.log(pc.red("\nNo engines found. Install at least one to use squint."));
704
+ process.exitCode = 1;
705
+ } else {
706
+ console.log(pc.dim(`
707
+ ${available.length} engine(s) ready.`));
708
+ }
709
+ });
710
+ program.command("init").description("Scaffold a new Vite + React + TS + Tailwind app with token-first CSS").argument("[dir]", "target directory", ".").option("--force", "write into a non-empty directory").option("--no-install", "skip npm install").action(async (dir, options) => {
711
+ const { installDependencies, writeTemplate } = await import("./init-7AYGAKOS.js");
712
+ let result;
713
+ try {
714
+ result = writeTemplate(dir, { force: options.force });
715
+ } catch (err) {
716
+ console.error(pc.red(`\u2717 ${err instanceof Error ? err.message : String(err)}`));
717
+ process.exitCode = 1;
718
+ return;
719
+ }
720
+ console.log(pc.green(`\u2713 scaffolded ${result.files.length} files in ${result.dir}`));
721
+ if (options.install) {
722
+ console.log(pc.dim("installing dependencies\u2026"));
723
+ const ok = await installDependencies(result.dir);
724
+ if (!ok) {
725
+ console.error(pc.red("\u2717 npm install failed \u2014 run it manually"));
726
+ process.exitCode = 1;
727
+ return;
728
+ }
729
+ }
730
+ const cd = result.dir === "." ? "" : `cd ${result.dir} && `;
731
+ console.log(`
732
+ Next: ${pc.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
733
+ console.log(pc.dim("then describe what to build \u2014 /dev starts the preview server"));
734
+ });
735
+ program.command("tag").description("Add the element picker to this Vite app (Alt+S in the browser \u2192 click \u2192 file:line:col)").action(async () => {
736
+ const fs2 = await import("fs");
737
+ const nodePath = await import("path");
738
+ const { patchViteConfig, TAGGER_FILENAME, TAGGER_SOURCE } = await import("./source-MEXHWVP4.js");
739
+ const cwd = process.cwd();
740
+ const taggerPath = nodePath.join(cwd, TAGGER_FILENAME);
741
+ fs2.writeFileSync(taggerPath, TAGGER_SOURCE);
742
+ console.log(pc.green(`\u2713 ${TAGGER_FILENAME} written`));
743
+ const configPath = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].map((name) => nodePath.join(cwd, name)).find((candidate) => fs2.existsSync(candidate));
744
+ if (!configPath) {
745
+ console.log(pc.yellow("\u25CB no vite config found \u2014 add the plugin manually:"));
746
+ console.log(pc.dim(` import squintTagger from './${TAGGER_FILENAME}'
747
+ plugins: [squintTagger(), \u2026]`));
748
+ return;
749
+ }
750
+ const source = fs2.readFileSync(configPath, "utf8");
751
+ const patched = patchViteConfig(source);
752
+ if (patched === "already") {
753
+ console.log(pc.dim("vite config already wired"));
754
+ } else if (patched === null) {
755
+ console.log(pc.yellow(`\u25CB could not patch ${nodePath.basename(configPath)} automatically \u2014 add:`));
756
+ console.log(pc.dim(` import squintTagger from './${TAGGER_FILENAME}'
757
+ plugins: [squintTagger(), \u2026]`));
758
+ } else {
759
+ fs2.writeFileSync(configPath, patched);
760
+ console.log(pc.green(`\u2713 ${nodePath.basename(configPath)} wired`));
761
+ }
762
+ console.log(pc.dim("\nin the running app: Alt+S \u2192 click an element \u2192 paste the copied file:line:col into squint"));
763
+ });
764
+ var variantsCommand = program.command("variants").description("Parallel design explorations \u2014 one aesthetic family each, pick with your eyes");
765
+ variantsCommand.command("gen").description("Generate n variants of one ask in parallel (n engine runs \u2014 n\xD7 cost)").argument("<n>", "how many variants (max 4)").argument("<prompt...>", "what to build").option("-e, --engine <id>", "engine to use").option("-m, --model <name>", "model override").option("--no-shots", "skip the screenshot pass").action(
766
+ async (nRaw, promptWords, options) => {
767
+ const cwd = process.cwd();
768
+ const n = Number.parseInt(nRaw, 10);
769
+ if (!Number.isInteger(n) || n < 2 || n > 4) {
770
+ console.error(pc.red("\u2717 n must be 2\u20134"));
771
+ process.exitCode = 1;
772
+ return;
773
+ }
774
+ const { isGitRepo } = await import("./snapshot-KKUY5RR6.js");
775
+ if (!isGitRepo(cwd)) {
776
+ console.error(pc.red("\u2717 variants need a git repo with at least one commit"));
777
+ process.exitCode = 1;
778
+ return;
779
+ }
780
+ const { runVariants, cleanVariants } = await import("./variants-VVIYT7WI.js");
781
+ const config = loadConfig(defaultPaths(cwd));
782
+ const engineId = resolveEngineId(config, options.engine);
783
+ const engine = getEngine(engineId);
784
+ const model = resolveModel(config, engineId, options.model);
785
+ const ask = promptWords.join(" ");
786
+ cleanVariants(cwd);
787
+ console.log(pc.dim(`generating ${n} directions in parallel via ${engine.id} \u2014 this runs ${n} engine sessions`));
788
+ const runs = await runVariants(
789
+ cwd,
790
+ ask,
791
+ n,
792
+ engine,
793
+ model,
794
+ (familyId, text) => console.log(`${pc.cyan(familyId.padEnd(18))} ${pc.dim(text)}`)
795
+ );
796
+ const succeeded = runs.filter((r) => r.result.ok);
797
+ if (options.shots && succeeded.length > 0) {
798
+ const { screenshotVariants } = await import("./shots-ADBOBALL.js");
799
+ console.log(pc.dim("capturing screenshots\u2026"));
800
+ const shots = await screenshotVariants(cwd, succeeded.map((r) => r.variant));
801
+ for (const shot of shots) {
802
+ console.log(`${pc.green("\u2713")} ${shot.familyId.padEnd(18)} ${shot.path ?? pc.dim(shot.error ?? "")}`);
803
+ }
804
+ }
805
+ console.log(
806
+ `
807
+ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 ` + pc.bold("squint variants apply <id>") + pc.dim(" applies the winner, squint variants clean discards all")
808
+ );
809
+ if (succeeded.length === 0) process.exitCode = 1;
810
+ }
811
+ );
812
+ variantsCommand.command("list").description("List generated variants").action(async () => {
813
+ const { listVariants } = await import("./variants-VVIYT7WI.js");
814
+ const ids = listVariants(process.cwd());
815
+ if (ids.length === 0) {
816
+ console.log(pc.dim('no variants \u2014 squint variants gen <n> "<ask>"'));
817
+ return;
818
+ }
819
+ for (const id of ids) console.log(id);
820
+ });
821
+ variantsCommand.command("apply").description("Apply one variant\u2019s changes to the main tree and discard the rest").argument("<id>", "family id of the winning variant").action(async (id) => {
822
+ const { applyVariant, cleanVariants } = await import("./variants-VVIYT7WI.js");
823
+ const cwd = process.cwd();
824
+ const result = applyVariant(cwd, id);
825
+ if (!result.ok) {
826
+ console.error(pc.red(`\u2717 ${result.detail}`));
827
+ process.exitCode = 1;
828
+ return;
829
+ }
830
+ cleanVariants(cwd);
831
+ console.log(pc.green(`\u2713 applied ${id} to the working tree`) + pc.dim(" \u2014 review with git diff"));
832
+ });
833
+ variantsCommand.command("clean").description("Discard all variants").action(async () => {
834
+ const { cleanVariants } = await import("./variants-VVIYT7WI.js");
835
+ const count = cleanVariants(process.cwd());
836
+ console.log(pc.dim(`removed ${count} variant(s)`));
837
+ });
838
+ program.command("brief").description("Set a committed design direction for this project (.squint/brief.md)").argument("[family]", "aesthetic family id (omit to list)").option("--force", "overwrite an existing project brief").action(async (familyId, options) => {
839
+ const fs2 = await import("fs");
840
+ const nodePath = await import("path");
841
+ const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-RVP5BWQD.js");
842
+ if (!familyId) {
843
+ console.log(pc.bold("Aesthetic families") + pc.dim(" \u2014 squint brief <id>\n"));
844
+ for (const family2 of FAMILIES) {
845
+ console.log(`${pc.green(family2.id.padEnd(18))} ${family2.name.padEnd(22)} ${pc.dim(family2.summary)}`);
846
+ }
847
+ console.log(pc.dim("\nThe brief wraps every ask; edit .squint/brief.md to remix."));
848
+ return;
849
+ }
850
+ const family = getFamily(familyId);
851
+ if (!family) {
852
+ console.error(pc.red(`\u2717 unknown family "${familyId}" \u2014 run squint brief to list`));
853
+ process.exitCode = 1;
854
+ return;
855
+ }
856
+ const target = nodePath.join(process.cwd(), ".squint", "brief.md");
857
+ if (fs2.existsSync(target) && !options.force) {
858
+ console.error(pc.red(`\u2717 ${target} exists \u2014 use --force to overwrite`));
859
+ process.exitCode = 1;
860
+ return;
861
+ }
862
+ fs2.mkdirSync(nodePath.dirname(target), { recursive: true });
863
+ fs2.writeFileSync(target, renderFamilyBrief(family) + "\n");
864
+ console.log(pc.green(`\u2713 ${family.name} direction written to .squint/brief.md`));
865
+ console.log(pc.dim("every squint ask in this repo now holds this direction \u2014 edit the file to remix"));
866
+ });
867
+ program.command("check").description("Run this project\u2019s quality gates (typecheck, lint, test, build)").action(async () => {
868
+ const { detectGates: detectGates2, runGates: runGates2 } = await import("./gates-G5DABJWQ.js");
869
+ const cwd = process.cwd();
870
+ const gates = detectGates2(cwd);
871
+ if (gates.length === 0) {
872
+ console.log(pc.dim("no gates detected (no package.json scripts, tsconfig, or eslint config)"));
873
+ return;
874
+ }
875
+ console.log(pc.dim(`running ${gates.map((g) => g.id).join(" \u2192 ")}`));
876
+ const results = await runGates2(cwd, gates, (result) => {
877
+ const mark = result.ok ? pc.green("\u2713") : pc.red("\u2717");
878
+ console.log(`${mark} ${result.gate.id.padEnd(10)} ${pc.dim(`${(result.durationMs / 1e3).toFixed(1)}s \xB7 ${result.gate.display}`)}`);
879
+ if (!result.ok) console.log(pc.dim(result.outputTail.split("\n").slice(-12).join("\n")));
880
+ });
881
+ if (results.some((r) => !r.ok)) process.exitCode = 1;
882
+ });
883
+ 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) => {
884
+ const { captureViewports: captureViewports2 } = await import("./preview-5PCHDJL7.js");
885
+ const result = await captureViewports2(process.cwd(), url);
886
+ if (!result) {
887
+ console.error(pc.red("\u2717 no Chrome/Chromium found"));
888
+ process.exitCode = 1;
889
+ return;
890
+ }
891
+ for (const shot of result.shots) console.log(`${pc.green("\u2713")} ${shot.name.padEnd(8)} ${shot.path}`);
892
+ for (const error of result.errors) console.error(pc.red(`\u2717 ${error}`));
893
+ if (result.shots.length === 0) process.exitCode = 1;
894
+ });
895
+ var configCommand = program.command("config").description("Read or change squint configuration");
896
+ configCommand.command("get").description("Print the resolved configuration").action(() => {
897
+ const paths = defaultPaths(process.cwd());
898
+ console.log(JSON.stringify(loadConfig(paths), null, 2));
899
+ });
900
+ configCommand.command("set").description("Set a value (keys: engine, models.<engineId>)").argument("<key>").argument("<value>").option("--project", "write to this project (.squint/config.json) instead of global").action((key, value, options) => {
901
+ const paths = defaultPaths(process.cwd());
902
+ const file = options.project ? paths.projectFile : paths.globalFile;
903
+ setConfigValue(file, key, value);
904
+ console.log(pc.green(`\u2713 ${key} = ${value}`) + pc.dim(` (${file})`));
905
+ });
906
+ configCommand.command("path").description("Show config file locations").action(() => {
907
+ const paths = defaultPaths(process.cwd());
908
+ console.log(`global ${paths.globalFile}`);
909
+ console.log(`project ${paths.projectFile}`);
910
+ });
911
+ program.action(() => {
912
+ const cwd = process.cwd();
913
+ const config = loadConfig(defaultPaths(cwd));
914
+ const engineId = resolveEngineId(config);
915
+ const model = resolveModel(config, engineId);
916
+ render(
917
+ /* @__PURE__ */ jsx3(
918
+ App,
919
+ {
920
+ cwd,
921
+ initialEngine: engineId,
922
+ initialModel: model,
923
+ autoDev: config.autoDev,
924
+ autoFix: config.autoFix,
925
+ autoProbe: config.autoProbe
926
+ }
927
+ )
928
+ );
929
+ });
930
+ function createPrinter() {
931
+ let streaming = false;
932
+ return (event) => {
933
+ switch (event.type) {
934
+ case "status":
935
+ console.log(pc.dim(`\xB7 ${event.text}`));
936
+ break;
937
+ case "delta":
938
+ streaming = true;
939
+ process.stdout.write(event.text);
940
+ break;
941
+ case "text":
942
+ if (event.streamed && streaming) {
943
+ process.stdout.write("\n");
944
+ } else {
945
+ console.log(event.text);
946
+ }
947
+ streaming = false;
948
+ break;
949
+ case "thinking":
950
+ console.log(pc.dim(pc.italic(event.text)));
951
+ break;
952
+ case "tool":
953
+ if (streaming) {
954
+ process.stdout.write("\n");
955
+ streaming = false;
956
+ }
957
+ console.log(pc.cyan(`\u2699 ${event.name}${event.detail ? ` \xB7 ${event.detail}` : ""}`));
958
+ break;
959
+ case "error":
960
+ console.error(pc.red(`\u2717 ${event.text}`));
961
+ break;
962
+ case "result":
963
+ case "raw":
964
+ break;
965
+ }
966
+ };
967
+ }
968
+ program.parseAsync(process.argv);