@aayambansal/squint 0.2.6 → 0.2.8

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 CHANGED
@@ -1,34 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- enrich
4
- } from "./chunk-4LAU5TGK.js";
5
- import {
6
- diffStatSince,
7
- isGitRepo,
8
- restoreSnapshot,
9
- takeSnapshot
10
- } from "./chunk-YHRAOBI2.js";
11
2
  import {
12
3
  DevServer,
13
4
  buildFixPrompt,
14
5
  detectDevCommand,
15
6
  screenshotVariants
16
- } from "./chunk-43L4QXCM.js";
7
+ } from "./chunk-5VKUJPOL.js";
17
8
  import {
18
9
  applyVariant,
19
10
  cleanVariants,
20
11
  listVariants,
21
12
  runVariants
22
- } from "./chunk-LVA2OQSR.js";
23
- import {
24
- composePrompt
25
- } from "./chunk-P3H4N2EN.js";
26
- import {
27
- runAgent
28
- } from "./chunk-C7WKNJG6.js";
29
- import {
30
- completeCommand
31
- } from "./chunk-V3M4WQLH.js";
13
+ } from "./chunk-X6MDBKED.js";
32
14
  import {
33
15
  buildGatePrompt,
34
16
  detectFastGates,
@@ -45,8 +27,13 @@ import {
45
27
  probeRuntime,
46
28
  runtimeSummary,
47
29
  saveState
48
- } from "./chunk-YZEFBPBJ.js";
49
- import "./chunk-OC6RU6XH.js";
30
+ } from "./chunk-CHZE3UHK.js";
31
+ import {
32
+ completeCommand
33
+ } from "./chunk-YTO2YRF7.js";
34
+ import {
35
+ runAgent
36
+ } from "./chunk-C7WKNJG6.js";
50
37
  import {
51
38
  findChrome
52
39
  } from "./chunk-B7LOERSP.js";
@@ -54,10 +41,24 @@ import {
54
41
  detectEngines,
55
42
  getEngine
56
43
  } from "./chunk-BWZFACBT.js";
44
+ import "./chunk-OC6RU6XH.js";
45
+ import {
46
+ enrich
47
+ } from "./chunk-4LAU5TGK.js";
48
+ import {
49
+ composePrompt
50
+ } from "./chunk-P3H4N2EN.js";
51
+ import {
52
+ diffStatSince,
53
+ isGitRepo,
54
+ restoreSnapshot,
55
+ takeSnapshot
56
+ } from "./chunk-YHRAOBI2.js";
57
57
 
58
58
  // src/cli.tsx
59
59
  import { Command } from "commander";
60
- import { render } from "ink";
60
+
61
+ // src/cli/configCmd.ts
61
62
  import pc from "picocolors";
62
63
 
63
64
  // src/config/config.ts
@@ -82,6 +83,8 @@ var ConfigSchema = z.object({
82
83
  bell: z.boolean().optional(),
83
84
  /** Session budget in USD; crossing it warns (never blocks). */
84
85
  budgetUsd: z.number().positive().optional(),
86
+ /** Auto-run /review when the visual pulse shows a big change (default off). */
87
+ autoReview: z.boolean().optional(),
85
88
  /** TUI theme name (amber, ocean, moss, rose, mono). */
86
89
  theme: z.string().optional()
87
90
  });
@@ -126,7 +129,7 @@ function setConfigValue(file, key, value) {
126
129
  let next;
127
130
  if (key === "engine" || key === "theme") {
128
131
  next = { ...current, [key]: value };
129
- } else if (key === "autoDev" || key === "autoFix" || key === "autoProbe" || key === "autoCheck" || key === "bell") {
132
+ } else if (key === "autoDev" || key === "autoFix" || key === "autoProbe" || key === "autoCheck" || key === "autoReview" || key === "bell") {
130
133
  if (value !== "true" && value !== "false") {
131
134
  throw new Error(`"${key}" must be true or false`);
132
135
  }
@@ -143,7 +146,7 @@ function setConfigValue(file, key, value) {
143
146
  next = { ...current, models: { ...current.models, [engineId]: value } };
144
147
  } else {
145
148
  throw new Error(
146
- `Unknown config key "${key}". Supported: engine, theme, autoDev, autoFix, autoProbe, autoCheck, bell, budgetUsd, models.<engineId>`
149
+ `Unknown config key "${key}". Supported: engine, theme, autoDev, autoFix, autoProbe, autoCheck, autoReview, bell, budgetUsd, models.<engineId>`
147
150
  );
148
151
  }
149
152
  fs.mkdirSync(path.dirname(file), { recursive: true });
@@ -151,13 +154,513 @@ function setConfigValue(file, key, value) {
151
154
  return next;
152
155
  }
153
156
 
157
+ // src/cli/configCmd.ts
158
+ function registerConfig(program2) {
159
+ const configCommand = program2.command("config").description("Read or change squint configuration");
160
+ configCommand.command("get").description("Print the resolved configuration").action(() => {
161
+ const paths = defaultPaths(process.cwd());
162
+ console.log(JSON.stringify(loadConfig(paths), null, 2));
163
+ });
164
+ configCommand.command("set").description("Set a value (see docs/configuration.md for every key)").argument("<key>").argument("<value>").option("--project", "write to this project (.squint/config.json) instead of global").action((key, value, options) => {
165
+ const paths = defaultPaths(process.cwd());
166
+ const file = options.project ? paths.projectFile : paths.globalFile;
167
+ setConfigValue(file, key, value);
168
+ console.log(pc.green(`\u2713 ${key} = ${value}`) + pc.dim(` (${file})`));
169
+ });
170
+ configCommand.command("path").description("Show config file locations").action(() => {
171
+ const paths = defaultPaths(process.cwd());
172
+ console.log(`global ${paths.globalFile}`);
173
+ console.log(`project ${paths.projectFile}`);
174
+ });
175
+ }
176
+
177
+ // src/cli/env.ts
178
+ import pc2 from "picocolors";
179
+ function registerEnv(program2) {
180
+ program2.command("engines").description("List engines: installed, streaming, session resume").action(() => {
181
+ console.log(pc2.dim(" id name stream resume where"));
182
+ for (const { engine, path: binaryPath } of detectEngines()) {
183
+ const status = binaryPath ? pc2.green("\u2713") : pc2.red("\u2717");
184
+ const stream = engine.createParser ? pc2.green("yes") : pc2.dim("text");
185
+ const resume = engine.supportsResume ? pc2.green("yes") : pc2.dim("no");
186
+ const location = binaryPath ?? pc2.dim(`not found \u2014 ${engine.install}`);
187
+ console.log(
188
+ `${status} ${engine.id.padEnd(10)} ${engine.name.padEnd(14)} ${stream.padEnd(15)} ${resume.padEnd(14)} ${location}`
189
+ );
190
+ }
191
+ console.log(pc2.dim("\nplan/safe/yolo modes map onto every engine \xB7 squint doctor --probe verifies auth"));
192
+ });
193
+ program2.command("doctor").description("Check squint prerequisites and engine availability").option("--probe", "run each detected engine with a one-word prompt to verify auth works").action(async (options) => {
194
+ const [major] = process.versions.node.split(".");
195
+ const nodeOk = Number(major) >= 22;
196
+ console.log(
197
+ `${nodeOk ? pc2.green("\u2713") : pc2.red("\u2717")} node ${process.versions.node}${nodeOk ? "" : " (need >= 22)"}`
198
+ );
199
+ const detected = detectEngines();
200
+ for (const { engine, path: binaryPath } of detected) {
201
+ const status = binaryPath ? pc2.green("\u2713") : pc2.yellow("\u25CB");
202
+ console.log(`${status} ${engine.name}${binaryPath ? "" : pc2.dim(` \u2014 install: ${engine.install}`)}`);
203
+ }
204
+ if (options.probe) {
205
+ const { runAgent: runAgent2 } = await import("./run-S772BPMZ.js");
206
+ console.log(pc2.dim("\nprobing engines with a one-word prompt (verifies auth end to end)\u2026"));
207
+ for (const { engine, path: binaryPath } of detected) {
208
+ if (!binaryPath) continue;
209
+ const startedAt = Date.now();
210
+ const abort = new AbortController();
211
+ const timer = setTimeout(() => abort.abort(), 9e4);
212
+ const result = await runAgent2(
213
+ engine,
214
+ { prompt: "Reply with exactly: ok", cwd: process.cwd() },
215
+ () => {
216
+ },
217
+ abort.signal
218
+ );
219
+ clearTimeout(timer);
220
+ const secs = ((Date.now() - startedAt) / 1e3).toFixed(1);
221
+ const detail = (result.error ?? "failed").split("\n").filter((l) => l.trim()).at(-1) ?? "failed";
222
+ console.log(
223
+ result.ok ? `${pc2.green("\u2713")} ${engine.id.padEnd(10)} responded in ${secs}s` : `${pc2.red("\u2717")} ${engine.id.padEnd(10)} ${pc2.dim(detail.slice(0, 110))}`
224
+ );
225
+ }
226
+ }
227
+ const { findChrome: findChrome2 } = await import("./chrome-NVU47PLK.js");
228
+ const { hasWebSocket } = await import("./cdp-SFWNP7OA.js");
229
+ const chrome = findChrome2();
230
+ console.log(
231
+ chrome ? `${pc2.green("\u2713")} Chrome ${pc2.dim(chrome)}` : `${pc2.yellow("\u25CB")} Chrome ${pc2.dim("\u2014 screenshots and runtime probing disabled")}`
232
+ );
233
+ console.log(
234
+ hasWebSocket() ? `${pc2.green("\u2713")} WebSocket ${pc2.dim("runtime console/network capture available")}` : `${pc2.yellow("\u25CB")} WebSocket ${pc2.dim("\u2014 node 22+ enables runtime capture; screenshots still work")}`
235
+ );
236
+ const available = detected.filter((d) => d.path !== null);
237
+ if (available.length === 0) {
238
+ console.log(pc2.red("\nNo engines found. Install at least one to use squint."));
239
+ process.exitCode = 1;
240
+ } else {
241
+ console.log(pc2.dim(`
242
+ ${available.length} engine(s) ready.`));
243
+ }
244
+ });
245
+ }
246
+
247
+ // src/cli/project.ts
248
+ import pc3 from "picocolors";
249
+ function registerProject(program2) {
250
+ const skillsCommand = program2.command("skills").description("Project knowledge injected into asks (.squint/rules.md + .squint/skills/)");
251
+ skillsCommand.command("list").description("Show always-on rules and trigger-matched skills").action(async () => {
252
+ const { loadRules, loadSkills } = await import("./skills-DNBHZAAU.js");
253
+ const cwd = process.cwd();
254
+ const rules = loadRules(cwd);
255
+ console.log(
256
+ rules ? `${pc3.green("\u2713")} rules.md ${pc3.dim(`(${rules.split("\n").length} lines, always on)`)}` : pc3.dim("\u25CB no .squint/rules.md")
257
+ );
258
+ const skills = loadSkills(cwd);
259
+ if (skills.length === 0) {
260
+ console.log(pc3.dim("\u25CB no skills \u2014 squint skills init writes an example"));
261
+ return;
262
+ }
263
+ for (const skill of skills) {
264
+ console.log(`${pc3.green("\u2713")} ${skill.name.padEnd(20)} ${pc3.dim(`triggers: ${skill.triggers.join(", ")}`)}`);
265
+ }
266
+ });
267
+ skillsCommand.command("init").description("Scaffold .squint/rules.md and an example skill").action(async () => {
268
+ const fs3 = await import("fs");
269
+ const nodePath = await import("path");
270
+ const cwd = process.cwd();
271
+ const skillsDir = nodePath.join(cwd, ".squint", "skills");
272
+ fs3.mkdirSync(skillsDir, { recursive: true });
273
+ const rules = nodePath.join(cwd, ".squint", "rules.md");
274
+ if (!fs3.existsSync(rules)) {
275
+ fs3.writeFileSync(
276
+ rules,
277
+ "# Project rules\n\nThese ride along on every squint ask. Keep them short \u2014 cut anything that would not cause a mistake if removed.\n"
278
+ );
279
+ console.log(pc3.green("\u2713 .squint/rules.md"));
280
+ }
281
+ const example = nodePath.join(skillsDir, "example.md");
282
+ if (!fs3.existsSync(example)) {
283
+ fs3.writeFileSync(
284
+ example,
285
+ "---\ntriggers: example, sample\n---\n\nThis note is injected only when an ask mentions one of the triggers above.\nDocument the parts of this repo an agent would otherwise rediscover every time:\nwhere state lives, which helpers to reuse, what not to touch.\n"
286
+ );
287
+ console.log(pc3.green("\u2713 .squint/skills/example.md"));
288
+ }
289
+ console.log(pc3.dim("rules are always-on; skills inject when an ask mentions a trigger"));
290
+ });
291
+ program2.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) => {
292
+ const fs3 = await import("fs");
293
+ const nodePath = await import("path");
294
+ const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-2G5SLLY7.js");
295
+ if (!familyId) {
296
+ console.log(pc3.bold("Aesthetic families") + pc3.dim(" \u2014 squint brief <id>\n"));
297
+ for (const family2 of FAMILIES) {
298
+ console.log(`${pc3.green(family2.id.padEnd(18))} ${family2.name.padEnd(22)} ${pc3.dim(family2.summary)}`);
299
+ }
300
+ console.log(pc3.dim("\nThe brief wraps every ask; edit .squint/brief.md to remix."));
301
+ return;
302
+ }
303
+ const family = getFamily(familyId);
304
+ if (!family) {
305
+ console.error(pc3.red(`\u2717 unknown family "${familyId}" \u2014 run squint brief to list`));
306
+ process.exitCode = 1;
307
+ return;
308
+ }
309
+ const target = nodePath.join(process.cwd(), ".squint", "brief.md");
310
+ if (fs3.existsSync(target) && !options.force) {
311
+ console.error(pc3.red(`\u2717 ${target} exists \u2014 use --force to overwrite`));
312
+ process.exitCode = 1;
313
+ return;
314
+ }
315
+ fs3.mkdirSync(nodePath.dirname(target), { recursive: true });
316
+ fs3.writeFileSync(target, renderFamilyBrief(family) + "\n");
317
+ console.log(pc3.green(`\u2713 ${family.name} direction written to .squint/brief.md`));
318
+ console.log(pc3.dim("every squint ask in this repo now holds this direction \u2014 edit the file to remix"));
319
+ });
320
+ const variantsCommand = program2.command("variants").description("Parallel design explorations \u2014 one aesthetic family each, pick with your eyes");
321
+ 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(
322
+ async (nRaw, promptWords, options) => {
323
+ const cwd = process.cwd();
324
+ const n = Number.parseInt(nRaw, 10);
325
+ if (!Number.isInteger(n) || n < 2 || n > 4) {
326
+ console.error(pc3.red("\u2717 n must be 2\u20134"));
327
+ process.exitCode = 1;
328
+ return;
329
+ }
330
+ const { isGitRepo: isGitRepo2 } = await import("./snapshot-H6VYFHLN.js");
331
+ if (!isGitRepo2(cwd)) {
332
+ console.error(pc3.red("\u2717 variants need a git repo with at least one commit"));
333
+ process.exitCode = 1;
334
+ return;
335
+ }
336
+ const { runVariants: runVariants2, cleanVariants: cleanVariants2 } = await import("./variants-UULPQ7Q5.js");
337
+ const config = loadConfig(defaultPaths(cwd));
338
+ const engineId = resolveEngineId(config, options.engine);
339
+ const engine = getEngine(engineId);
340
+ const model = resolveModel(config, engineId, options.model);
341
+ const ask = promptWords.join(" ");
342
+ cleanVariants2(cwd);
343
+ console.log(pc3.dim(`generating ${n} directions in parallel via ${engine.id} \u2014 this runs ${n} engine sessions`));
344
+ const runs = await runVariants2(
345
+ cwd,
346
+ ask,
347
+ n,
348
+ engine,
349
+ model,
350
+ (familyId, text) => console.log(`${pc3.cyan(familyId.padEnd(18))} ${pc3.dim(text)}`)
351
+ );
352
+ const succeeded = runs.filter((r) => r.result.ok);
353
+ if (options.shots && succeeded.length > 0) {
354
+ const { screenshotVariants: screenshotVariants2 } = await import("./shots-FVG4CDVK.js");
355
+ console.log(pc3.dim("capturing screenshots\u2026"));
356
+ const shots = await screenshotVariants2(cwd, succeeded.map((r) => r.variant));
357
+ for (const shot of shots) {
358
+ console.log(`${pc3.green("\u2713")} ${shot.familyId.padEnd(18)} ${shot.path ?? pc3.dim(shot.error ?? "")}`);
359
+ }
360
+ }
361
+ console.log(
362
+ `
363
+ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 ` + pc3.bold("squint variants apply <id>") + pc3.dim(" applies the winner, squint variants clean discards all")
364
+ );
365
+ if (succeeded.length === 0) process.exitCode = 1;
366
+ }
367
+ );
368
+ variantsCommand.command("list").description("List generated variants").action(async () => {
369
+ const { listVariants: listVariants2 } = await import("./variants-UULPQ7Q5.js");
370
+ const ids = listVariants2(process.cwd());
371
+ if (ids.length === 0) {
372
+ console.log(pc3.dim('no variants \u2014 squint variants gen <n> "<ask>"'));
373
+ return;
374
+ }
375
+ for (const id of ids) console.log(id);
376
+ });
377
+ 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) => {
378
+ const { applyVariant: applyVariant2, cleanVariants: cleanVariants2 } = await import("./variants-UULPQ7Q5.js");
379
+ const cwd = process.cwd();
380
+ const result = applyVariant2(cwd, id);
381
+ if (!result.ok) {
382
+ console.error(pc3.red(`\u2717 ${result.detail}`));
383
+ process.exitCode = 1;
384
+ return;
385
+ }
386
+ cleanVariants2(cwd);
387
+ console.log(pc3.green(`\u2713 applied ${id} to the working tree`) + pc3.dim(" \u2014 review with git diff"));
388
+ });
389
+ variantsCommand.command("clean").description("Discard all variants").action(async () => {
390
+ const { cleanVariants: cleanVariants2 } = await import("./variants-UULPQ7Q5.js");
391
+ const count = cleanVariants2(process.cwd());
392
+ console.log(pc3.dim(`removed ${count} variant(s)`));
393
+ });
394
+ }
395
+
396
+ // src/cli/quality.ts
397
+ import pc4 from "picocolors";
398
+ function registerQuality(program2) {
399
+ program2.command("check").description("Run this project\u2019s quality gates (typecheck, lint, format, test, build)").action(async () => {
400
+ const { detectGates: detectGates2, runGates: runGates2 } = await import("./gates-ZC67NMW3.js");
401
+ const cwd = process.cwd();
402
+ const gates = detectGates2(cwd);
403
+ if (gates.length === 0) {
404
+ console.log(pc4.dim("no gates detected (no package.json scripts, tsconfig, or eslint config)"));
405
+ return;
406
+ }
407
+ console.log(pc4.dim(`running ${gates.map((g) => g.id).join(" \u2192 ")}`));
408
+ const results = await runGates2(cwd, gates, (result) => {
409
+ const mark = result.ok ? pc4.green("\u2713") : pc4.red("\u2717");
410
+ console.log(
411
+ `${mark} ${result.gate.id.padEnd(10)} ${pc4.dim(`${(result.durationMs / 1e3).toFixed(1)}s \xB7 ${result.gate.display}`)}`
412
+ );
413
+ if (!result.ok) console.log(pc4.dim(result.outputTail.split("\n").slice(-12).join("\n")));
414
+ });
415
+ if (results.some((r) => !r.ok)) process.exitCode = 1;
416
+ });
417
+ 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) => {
418
+ const { captureViewports: captureViewports2 } = await import("./preview-XGASKUXR.js");
419
+ const result = await captureViewports2(process.cwd(), url);
420
+ if (!result) {
421
+ console.error(pc4.red("\u2717 no Chrome/Chromium found"));
422
+ process.exitCode = 1;
423
+ return;
424
+ }
425
+ for (const shot of result.shots) console.log(`${pc4.green("\u2713")} ${shot.name.padEnd(8)} ${shot.path}`);
426
+ for (const error of result.errors) console.error(pc4.red(`\u2717 ${error}`));
427
+ if (result.shots.length === 0) process.exitCode = 1;
428
+ });
429
+ }
430
+
431
+ // src/cli/run.ts
432
+ import pc5 from "picocolors";
433
+ function createPrinter() {
434
+ let streaming = false;
435
+ return (event) => {
436
+ switch (event.type) {
437
+ case "status":
438
+ console.log(pc5.dim(`\xB7 ${event.text}`));
439
+ break;
440
+ case "delta":
441
+ streaming = true;
442
+ process.stdout.write(event.text);
443
+ break;
444
+ case "text":
445
+ if (event.streamed && streaming) {
446
+ process.stdout.write("\n");
447
+ } else {
448
+ console.log(event.text);
449
+ }
450
+ streaming = false;
451
+ break;
452
+ case "thinking":
453
+ console.log(pc5.dim(pc5.italic(event.text)));
454
+ break;
455
+ case "tool":
456
+ if (streaming) {
457
+ process.stdout.write("\n");
458
+ streaming = false;
459
+ }
460
+ console.log(pc5.cyan(`\u2699 ${event.name}${event.detail ? ` \xB7 ${event.detail}` : ""}`));
461
+ break;
462
+ case "error":
463
+ console.error(pc5.red(`\u2717 ${event.text}`));
464
+ break;
465
+ case "result":
466
+ case "raw":
467
+ break;
468
+ }
469
+ };
470
+ }
471
+ function registerRun(program2) {
472
+ program2.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 (see squint engines)").option("-m, --model <name>", "model override for the engine").option("--mode <mode>", "plan (read-only) \xB7 safe (default) \xB7 yolo (no friction)").option("--no-brief", "send the prompt without the squint design brief").option("--json", "emit normalized agent events as ndjson").action(
473
+ async (promptWords, options) => {
474
+ const cwd = process.cwd();
475
+ if (options.mode && !["plan", "safe", "yolo"].includes(options.mode)) {
476
+ console.error(pc5.red("\u2717 --mode must be plan, safe, or yolo"));
477
+ process.exitCode = 1;
478
+ return;
479
+ }
480
+ const mode = options.mode;
481
+ const config = loadConfig(defaultPaths(cwd));
482
+ const engineId = resolveEngineId(config, options.engine);
483
+ const engine = getEngine(engineId);
484
+ const model = resolveModel(config, engineId, options.model);
485
+ const ask = promptWords.join(" ");
486
+ const prompt = composePrompt(ask, { cwd, noBrief: !options.brief });
487
+ const onEvent = options.json ? (event) => {
488
+ if (event.type !== "delta") console.log(JSON.stringify(event));
489
+ } : createPrinter();
490
+ if (!options.json)
491
+ console.log(
492
+ pc5.dim(`squint \xB7 ${engine.id}${model ? ` \xB7 ${model}` : ""}${mode && mode !== "safe" ? ` \xB7 ${mode}` : ""}`)
493
+ );
494
+ const result = await runAgent(engine, { prompt, cwd, model, mode }, onEvent);
495
+ if (options.json) {
496
+ console.log(JSON.stringify({ type: "summary", ...result }));
497
+ } else if (result.ok) {
498
+ const cost = result.costUsd !== void 0 ? ` \xB7 $${result.costUsd.toFixed(2)}` : "";
499
+ const secs = result.durationMs !== void 0 ? ` \xB7 ${(result.durationMs / 1e3).toFixed(0)}s` : "";
500
+ console.log(pc5.green(`\u2713 done${secs}${cost}`));
501
+ }
502
+ if (!result.ok) process.exitCode = 1;
503
+ }
504
+ );
505
+ }
506
+
507
+ // src/cli/scaffold.ts
508
+ import pc6 from "picocolors";
509
+ function registerScaffold(program2) {
510
+ program2.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) => {
511
+ const { installDependencies, writeTemplate } = await import("./init-LSYB32NS.js");
512
+ let result;
513
+ try {
514
+ result = writeTemplate(dir, { force: options.force });
515
+ } catch (err) {
516
+ console.error(pc6.red(`\u2717 ${err instanceof Error ? err.message : String(err)}`));
517
+ process.exitCode = 1;
518
+ return;
519
+ }
520
+ console.log(pc6.green(`\u2713 scaffolded ${result.files.length} files in ${result.dir}`));
521
+ if (options.install) {
522
+ console.log(pc6.dim("installing dependencies\u2026"));
523
+ const ok = await installDependencies(result.dir);
524
+ if (!ok) {
525
+ console.error(pc6.red("\u2717 npm install failed \u2014 run it manually"));
526
+ process.exitCode = 1;
527
+ return;
528
+ }
529
+ }
530
+ const cd = result.dir === "." ? "" : `cd ${result.dir} && `;
531
+ console.log(`
532
+ Next: ${pc6.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
533
+ console.log(pc6.dim("then describe what to build \u2014 /dev starts the preview server"));
534
+ });
535
+ program2.command("tag").description("Add the element picker to this Vite app (Alt+S in the browser \u2192 click \u2192 file:line:col)").action(async () => {
536
+ const fs3 = await import("fs");
537
+ const nodePath = await import("path");
538
+ const { patchViteConfig, TAGGER_FILENAME, TAGGER_SOURCE } = await import("./source-ZZU245VN.js");
539
+ const cwd = process.cwd();
540
+ const taggerPath = nodePath.join(cwd, TAGGER_FILENAME);
541
+ fs3.writeFileSync(taggerPath, TAGGER_SOURCE);
542
+ console.log(pc6.green(`\u2713 ${TAGGER_FILENAME} written`));
543
+ const configPath = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].map((name) => nodePath.join(cwd, name)).find((candidate) => fs3.existsSync(candidate));
544
+ if (!configPath) {
545
+ console.log(pc6.yellow("\u25CB no vite config found \u2014 add the plugin manually:"));
546
+ console.log(pc6.dim(` import squintTagger from './${TAGGER_FILENAME}'
547
+ plugins: [squintTagger(), \u2026]`));
548
+ return;
549
+ }
550
+ const source = fs3.readFileSync(configPath, "utf8");
551
+ const patched = patchViteConfig(source);
552
+ if (patched === "already") {
553
+ console.log(pc6.dim("vite config already wired"));
554
+ } else if (patched === null) {
555
+ console.log(pc6.yellow(`\u25CB could not patch ${nodePath.basename(configPath)} automatically \u2014 add:`));
556
+ console.log(pc6.dim(` import squintTagger from './${TAGGER_FILENAME}'
557
+ plugins: [squintTagger(), \u2026]`));
558
+ } else {
559
+ fs3.writeFileSync(configPath, patched);
560
+ console.log(pc6.green(`\u2713 ${nodePath.basename(configPath)} wired`));
561
+ }
562
+ console.log(
563
+ pc6.dim("\nin the running app: Alt+S \u2192 click elements to pin, alt+enter copies all \u2014 paste into squint")
564
+ );
565
+ });
566
+ }
567
+
568
+ // src/cli/tui.tsx
569
+ import { render } from "ink";
570
+
154
571
  // src/tui/App.tsx
155
572
  import { Box as Box2, Static, Text as Text3, useApp, useInput } from "ink";
156
- import path3 from "path";
573
+ import path4 from "path";
157
574
  import { useMemo, useRef, useState as useState2, useSyncExternalStore } from "react";
158
575
 
159
576
  // src/session/engine.ts
577
+ import path3 from "path";
578
+
579
+ // src/vcs/sandbox.ts
580
+ import { execFileSync } from "child_process";
581
+ import fs2 from "fs";
160
582
  import path2 from "path";
583
+ function git(cwd, args) {
584
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
585
+ }
586
+ function sandboxDir(cwd) {
587
+ return path2.join(cwd, ".squint", "sandbox");
588
+ }
589
+ function sandboxExists(cwd) {
590
+ return fs2.existsSync(path2.join(sandboxDir(cwd), ".git"));
591
+ }
592
+ function openSandbox(cwd) {
593
+ const dir = sandboxDir(cwd);
594
+ if (sandboxExists(cwd)) return { dir, reused: true };
595
+ fs2.rmSync(dir, { recursive: true, force: true });
596
+ fs2.mkdirSync(path2.dirname(dir), { recursive: true });
597
+ git(cwd, ["worktree", "add", "--force", "--detach", dir, "HEAD"]);
598
+ const mainModules = path2.join(cwd, "node_modules");
599
+ const sandboxModules = path2.join(dir, "node_modules");
600
+ if (fs2.existsSync(mainModules) && !fs2.existsSync(sandboxModules)) {
601
+ fs2.symlinkSync(mainModules, sandboxModules, "dir");
602
+ }
603
+ return { dir, reused: false };
604
+ }
605
+ function sandboxDiffStat(cwd) {
606
+ if (!sandboxExists(cwd)) return null;
607
+ const dir = sandboxDir(cwd);
608
+ try {
609
+ git(dir, ["add", "-A"]);
610
+ const stat = git(dir, ["diff", "--cached", "--shortstat", "HEAD"]);
611
+ return stat.length > 0 ? stat : null;
612
+ } catch {
613
+ return null;
614
+ }
615
+ }
616
+ function sandboxFiles(cwd) {
617
+ if (!sandboxExists(cwd)) return [];
618
+ const dir = sandboxDir(cwd);
619
+ try {
620
+ git(dir, ["add", "-A"]);
621
+ const out = git(dir, ["diff", "--cached", "--name-status", "HEAD"]);
622
+ return out.length > 0 ? out.split("\n") : [];
623
+ } catch {
624
+ return [];
625
+ }
626
+ }
627
+ function applySandbox(cwd) {
628
+ if (!sandboxExists(cwd)) return { ok: false, detail: "no sandbox open" };
629
+ const dir = sandboxDir(cwd);
630
+ try {
631
+ git(dir, ["add", "-A"]);
632
+ const patch = git(dir, ["diff", "--binary", "--cached", "HEAD"]);
633
+ if (patch.length === 0) return { ok: false, detail: "sandbox has no changes" };
634
+ const patchFile = path2.join(cwd, ".squint", "sandbox.patch");
635
+ fs2.writeFileSync(patchFile, patch + "\n");
636
+ git(cwd, ["apply", "--whitespace=nowarn", patchFile]);
637
+ fs2.rmSync(patchFile, { force: true });
638
+ return { ok: true };
639
+ } catch (err) {
640
+ return { ok: false, detail: err instanceof Error ? err.message.split("\n")[0] : String(err) };
641
+ }
642
+ }
643
+ function discardSandbox(cwd) {
644
+ if (!sandboxExists(cwd)) return false;
645
+ const dir = sandboxDir(cwd);
646
+ try {
647
+ const link = path2.join(dir, "node_modules");
648
+ if (fs2.existsSync(link) && fs2.lstatSync(link).isSymbolicLink()) fs2.rmSync(link);
649
+ } catch {
650
+ }
651
+ try {
652
+ git(cwd, ["worktree", "remove", "--force", dir]);
653
+ } catch {
654
+ fs2.rmSync(dir, { recursive: true, force: true });
655
+ }
656
+ try {
657
+ git(cwd, ["worktree", "prune"]);
658
+ } catch {
659
+ }
660
+ return true;
661
+ }
662
+
663
+ // src/session/engine.ts
161
664
  var MAX_AUTO_FIX_ATTEMPTS = 2;
162
665
  var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
163
666
  var Session = class {
@@ -175,7 +678,8 @@ var Session = class {
175
678
  totals: { costUsd: 0, turns: 0 },
176
679
  queue: [],
177
680
  mode: "safe",
178
- problems: []
681
+ problems: [],
682
+ sandbox: false
179
683
  };
180
684
  if (opts.autoDev && detectDevCommand(opts.cwd)) {
181
685
  this.devServer().start();
@@ -209,6 +713,7 @@ var Session = class {
209
713
  fixAttempts = 0;
210
714
  reviewTipShown = false;
211
715
  lastPulse = null;
716
+ autoReviewedThisAsk = false;
212
717
  startedAt = Date.now();
213
718
  subscribe(listener) {
214
719
  this.listeners.add(listener);
@@ -344,15 +849,33 @@ var Session = class {
344
849
  this.push("assistant", text);
345
850
  }
346
851
  }
852
+ /** Where engines run and servers start: the sandbox worktree when on. */
853
+ execCwd() {
854
+ return this.state.sandbox ? sandboxDir(this.opts.cwd) : this.opts.cwd;
855
+ }
347
856
  devServer() {
348
857
  if (!this.dev) {
349
- this.dev = new DevServer(this.opts.cwd, {
858
+ this.dev = new DevServer(this.execCwd(), {
350
859
  onStateChange: (devState) => this.notify({ devState }),
351
860
  onUrl: (url) => this.notify({ devUrl: url })
352
861
  });
353
862
  }
354
863
  return this.dev;
355
864
  }
865
+ /** Rebind the dev server after the execution tree changes. */
866
+ resetDevServer() {
867
+ const wasRunning = this.dev && this.dev.state !== "stopped";
868
+ this.dev?.stop();
869
+ this.dev = null;
870
+ this.notify({ devUrl: null, devState: "stopped" });
871
+ if (wasRunning) {
872
+ const command = detectDevCommand(this.execCwd());
873
+ if (command) {
874
+ this.devServer().start(command);
875
+ this.push("status", `dev server restarting in ${this.state.sandbox ? "the sandbox" : "the real tree"}`);
876
+ }
877
+ }
878
+ }
356
879
  turnEdits = 0;
357
880
  turnTools = 0;
358
881
  toolStreak = 0;
@@ -431,7 +954,7 @@ var Session = class {
431
954
  engine,
432
955
  {
433
956
  prompt,
434
- cwd: this.opts.cwd,
957
+ cwd: this.execCwd(),
435
958
  model: this.state.model,
436
959
  mode: this.state.mode,
437
960
  sessionId: engine.supportsResume ? this.sessionId : void 0
@@ -474,9 +997,9 @@ var Session = class {
474
997
  }
475
998
  }
476
999
  if (result.ok && this.opts.autoCheck !== false) {
477
- const fastGates = detectFastGates(this.opts.cwd);
1000
+ const fastGates = detectFastGates(this.execCwd());
478
1001
  if (fastGates.length > 0) {
479
- const gateResults = await runGates(this.opts.cwd, fastGates);
1002
+ const gateResults = await runGates(this.execCwd(), fastGates);
480
1003
  const failures = gateResults.filter((r) => !r.ok);
481
1004
  if (failures.length > 0) {
482
1005
  this.addProblem(
@@ -523,7 +1046,20 @@ ${errors.slice(-5).join("\n")}`);
523
1046
  this.push("status", "/fix sends open problems to the engine \xB7 /problems lists them");
524
1047
  } else if (probe) {
525
1048
  this.clearProblems("runtime");
526
- await this.visualPulse(probe.pulsePath);
1049
+ const pct = await this.visualPulse(probe.pulsePath);
1050
+ if (this.opts.autoReview && pct !== null && pct >= 10 && !this.autoReviewedThisAsk) {
1051
+ this.autoReviewedThisAsk = true;
1052
+ this.push("status", `auto-review: the page changed ${pct.toFixed(0)}% \u2014 capturing for self-critique`);
1053
+ this.notify({ running: false });
1054
+ const captureResult = await this.capture();
1055
+ if (captureResult) {
1056
+ await this.runTurn(
1057
+ buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y),
1058
+ "\u{1F441} auto-review rendered UI"
1059
+ );
1060
+ }
1061
+ return;
1062
+ }
527
1063
  }
528
1064
  }
529
1065
  if (!this.reviewTipShown && this.state.devUrl) {
@@ -537,7 +1073,8 @@ ${errors.slice(-5).join("\n")}`);
537
1073
  }
538
1074
  async submit(ask) {
539
1075
  this.fixAttempts = 0;
540
- const snapshot = takeSnapshot(this.opts.cwd);
1076
+ this.autoReviewedThisAsk = false;
1077
+ const snapshot = this.state.sandbox ? null : takeSnapshot(this.opts.cwd);
541
1078
  if (snapshot) {
542
1079
  this.checkpoints.push({
543
1080
  snapshot,
@@ -580,25 +1117,26 @@ ${errors.slice(-5).join("\n")}`);
580
1117
  * Informational — changes are usually intended; surprises shouldn't be.
581
1118
  */
582
1119
  async visualPulse(pulsePath) {
583
- if (!pulsePath) return;
1120
+ if (!pulsePath) return null;
584
1121
  let current;
585
1122
  try {
586
1123
  current = (await import("fs")).readFileSync(pulsePath);
587
1124
  } catch {
588
- return;
1125
+ return null;
589
1126
  }
590
1127
  const previous = this.lastPulse;
591
1128
  this.lastPulse = current;
592
1129
  if (!previous) {
593
1130
  this.push("status", "visual pulse: baseline captured");
594
- return;
1131
+ return null;
595
1132
  }
596
1133
  const pct = await comparePulse(previous, current);
597
- if (pct === null) return;
1134
+ if (pct === null) return null;
598
1135
  this.push(
599
1136
  "status",
600
1137
  pct < 0.5 ? "visual pulse: stable vs last turn" : `visual pulse: ${pct.toFixed(1)}% of the page changed vs last turn`
601
1138
  );
1139
+ return pct;
602
1140
  }
603
1141
  /** Screenshot the running app (and watch its runtime where CDP is available). */
604
1142
  async capture() {
@@ -616,7 +1154,7 @@ ${errors.slice(-5).join("\n")}`);
616
1154
  if (result.shots.length > 0) {
617
1155
  this.push(
618
1156
  "status",
619
- `captured ${result.shots.map((s) => s.name).join(", ")} \u2192 ${path2.dirname(result.shots[0].path)}`
1157
+ `captured ${result.shots.map((s) => s.name).join(", ")} \u2192 ${path3.dirname(result.shots[0].path)}`
620
1158
  );
621
1159
  }
622
1160
  if (result.runtime) {
@@ -691,6 +1229,23 @@ They are objective defects, not style preferences.`
691
1229
  break;
692
1230
  case "dev": {
693
1231
  const dev = this.devServer();
1232
+ if (arg === "logs") {
1233
+ const tail = dev.tail(15);
1234
+ this.push("status", tail.length > 0 ? tail.join("\n") : "no dev server output captured yet");
1235
+ break;
1236
+ }
1237
+ if (arg === "restart") {
1238
+ if (dev.state !== "stopped") dev.stop();
1239
+ this.notify({ devUrl: null });
1240
+ const devCommand = detectDevCommand(this.opts.cwd);
1241
+ if (!devCommand) {
1242
+ this.push("error", "no dev/start script found in package.json");
1243
+ } else {
1244
+ dev.start(devCommand);
1245
+ this.push("status", `dev server restarting \xB7 ${devCommand.display}`);
1246
+ }
1247
+ break;
1248
+ }
694
1249
  if (dev.state === "stopped" || dev.state === "crashed") {
695
1250
  const devCommand = detectDevCommand(this.opts.cwd);
696
1251
  if (!devCommand) {
@@ -726,12 +1281,12 @@ They are objective defects, not style preferences.`
726
1281
  }
727
1282
  case "save": {
728
1283
  void (async () => {
729
- const fs2 = await import("fs");
730
- const path4 = await import("path");
731
- const dir = path4.join(this.opts.cwd, ".squint", "transcripts");
732
- fs2.mkdirSync(dir, { recursive: true });
1284
+ const fs3 = await import("fs");
1285
+ const path5 = await import("path");
1286
+ const dir = path5.join(this.opts.cwd, ".squint", "transcripts");
1287
+ fs3.mkdirSync(dir, { recursive: true });
733
1288
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
734
- const file = path4.join(dir, `${stamp}.md`);
1289
+ const file = path5.join(dir, `${stamp}.md`);
735
1290
  const lines = [`# squint session \u2014 ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}`, ""];
736
1291
  for (const item of this.state.items) {
737
1292
  switch (item.role) {
@@ -751,8 +1306,8 @@ They are objective defects, not style preferences.`
751
1306
  }
752
1307
  }
753
1308
  lines.push("", `> ${this.summary()}`);
754
- fs2.writeFileSync(file, lines.join("\n") + "\n");
755
- this.push("status", `saved transcript \u2192 ${path4.relative(this.opts.cwd, file)}`);
1309
+ fs3.writeFileSync(file, lines.join("\n") + "\n");
1310
+ this.push("status", `saved transcript \u2192 ${path5.relative(this.opts.cwd, file)}`);
756
1311
  })();
757
1312
  break;
758
1313
  }
@@ -870,6 +1425,57 @@ They are objective defects, not style preferences.`
870
1425
  this.push("status", `resumed ${saved.engine} session${saved.lastAsk ? ` \xB7 "${saved.lastAsk}"` : ""}`);
871
1426
  break;
872
1427
  }
1428
+ case "sandbox": {
1429
+ if (this.state.running) {
1430
+ this.push("status", "wait for the current turn before changing sandbox state");
1431
+ break;
1432
+ }
1433
+ if (arg === "diff") {
1434
+ const stat = sandboxDiffStat(this.opts.cwd);
1435
+ if (!stat) {
1436
+ this.push("status", sandboxExists(this.opts.cwd) ? "sandbox is clean" : "no sandbox open \u2014 /sandbox on");
1437
+ } else {
1438
+ this.push("status", `${stat}
1439
+ ${sandboxFiles(this.opts.cwd).join("\n")}`);
1440
+ }
1441
+ break;
1442
+ }
1443
+ if (arg === "apply") {
1444
+ const applied = applySandbox(this.opts.cwd);
1445
+ if (applied.ok) {
1446
+ discardSandbox(this.opts.cwd);
1447
+ this.notify({ sandbox: false });
1448
+ this.resetDevServer();
1449
+ this.push("status", "sandbox applied to the real tree \u2014 review with git diff");
1450
+ } else {
1451
+ this.push("error", applied.detail ?? "apply failed");
1452
+ }
1453
+ break;
1454
+ }
1455
+ if (arg === "discard" || arg === "off") {
1456
+ const had = discardSandbox(this.opts.cwd);
1457
+ this.notify({ sandbox: false });
1458
+ this.resetDevServer();
1459
+ this.push("status", had ? "sandbox discarded \u2014 the real tree was never touched" : "no sandbox open");
1460
+ break;
1461
+ }
1462
+ if (arg === "on" || arg === "") {
1463
+ if (!isGitRepo(this.opts.cwd)) {
1464
+ this.push("error", "sandbox needs a git repo with at least one commit");
1465
+ break;
1466
+ }
1467
+ const { reused } = openSandbox(this.opts.cwd);
1468
+ this.notify({ sandbox: true });
1469
+ this.resetDevServer();
1470
+ this.push(
1471
+ "status",
1472
+ `${reused ? "rejoined the open sandbox" : "sandbox opened"} \u2014 asks now accumulate in a shadow worktree; /sandbox diff \xB7 apply \xB7 discard`
1473
+ );
1474
+ break;
1475
+ }
1476
+ this.push("status", "usage: /sandbox [on] \xB7 diff \xB7 apply \xB7 discard");
1477
+ break;
1478
+ }
873
1479
  case "variants": {
874
1480
  const [sub, ...subRest] = arg.split(/\s+/);
875
1481
  if (sub === "apply") {
@@ -945,7 +1551,7 @@ They are objective defects, not style preferences.`
945
1551
  this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
946
1552
  break;
947
1553
  case "help": {
948
- void import("./commands-7DTZB4JE.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1554
+ void import("./commands-N33DKZKC.js").then(({ commandHelp }) => this.push("status", commandHelp()));
949
1555
  break;
950
1556
  }
951
1557
  case "quit":
@@ -1287,6 +1893,7 @@ function App({
1287
1893
  autoFix,
1288
1894
  autoProbe,
1289
1895
  autoCheck,
1896
+ autoReview,
1290
1897
  bell,
1291
1898
  budgetUsd,
1292
1899
  initialTheme
@@ -1304,6 +1911,7 @@ function App({
1304
1911
  autoFix,
1305
1912
  autoProbe,
1306
1913
  autoCheck,
1914
+ autoReview,
1307
1915
  budgetUsd,
1308
1916
  // Delay lets the goodbye summary land in the Static scrollback.
1309
1917
  onQuit: () => setTimeout(() => exit(), 60)
@@ -1473,11 +2081,12 @@ function App({
1473
2081
  ]
1474
2082
  }
1475
2083
  ),
2084
+ state.sandbox && /* @__PURE__ */ jsx3(Text3, { color: theme2.accent, bold: true, children: " [sandbox]" }),
1476
2085
  " ",
1477
2086
  state.engineId,
1478
2087
  state.model ? ` \xB7 ${state.model}` : "",
1479
2088
  " \xB7 ",
1480
- path3.basename(cwd),
2089
+ path4.basename(cwd),
1481
2090
  devBadge,
1482
2091
  totalsBadge,
1483
2092
  state.problems.length > 0 && /* @__PURE__ */ jsxs3(Text3, { color: theme2.error, children: [
@@ -1492,409 +2101,49 @@ function App({
1492
2101
  ] }) });
1493
2102
  }
1494
2103
 
1495
- // src/cli.tsx
2104
+ // src/cli/tui.tsx
1496
2105
  import { jsx as jsx4 } from "react/jsx-runtime";
1497
- var VERSION = "0.2.6";
1498
- var program = new Command();
1499
- program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
1500
- 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 (see squint engines)").option("-m, --model <name>", "model override for the engine").option("--mode <mode>", "plan (read-only) \xB7 safe (default) \xB7 yolo (no friction)").option("--no-brief", "send the prompt without the squint design brief").option("--json", "emit normalized agent events as ndjson").action(
1501
- async (promptWords, options) => {
2106
+ function registerTui(program2) {
2107
+ program2.action(async () => {
1502
2108
  const cwd = process.cwd();
1503
- if (options.mode && !["plan", "safe", "yolo"].includes(options.mode)) {
1504
- console.error(pc.red("\u2717 --mode must be plan, safe, or yolo"));
1505
- process.exitCode = 1;
1506
- return;
1507
- }
1508
- const mode = options.mode;
1509
2109
  const config = loadConfig(defaultPaths(cwd));
1510
- const engineId = resolveEngineId(config, options.engine);
1511
- const engine = getEngine(engineId);
1512
- const model = resolveModel(config, engineId, options.model);
1513
- const ask = promptWords.join(" ");
1514
- const prompt = composePrompt(ask, { cwd, noBrief: !options.brief });
1515
- const onEvent = options.json ? (event) => {
1516
- if (event.type !== "delta") console.log(JSON.stringify(event));
1517
- } : createPrinter();
1518
- if (!options.json)
1519
- console.log(pc.dim(`squint \xB7 ${engine.id}${model ? ` \xB7 ${model}` : ""}${mode && mode !== "safe" ? ` \xB7 ${mode}` : ""}`));
1520
- const result = await runAgent(engine, { prompt, cwd, model, mode }, onEvent);
1521
- if (options.json) {
1522
- console.log(JSON.stringify({ type: "summary", ...result }));
1523
- } else if (result.ok) {
1524
- const cost = result.costUsd !== void 0 ? ` \xB7 $${result.costUsd.toFixed(2)}` : "";
1525
- const secs = result.durationMs !== void 0 ? ` \xB7 ${(result.durationMs / 1e3).toFixed(0)}s` : "";
1526
- console.log(pc.green(`\u2713 done${secs}${cost}`));
1527
- }
1528
- if (!result.ok) process.exitCode = 1;
1529
- }
1530
- );
1531
- program.command("engines").description("List engines: installed, streaming, session resume").action(() => {
1532
- console.log(pc.dim(" id name stream resume where"));
1533
- for (const { engine, path: binaryPath } of detectEngines()) {
1534
- const status = binaryPath ? pc.green("\u2713") : pc.red("\u2717");
1535
- const stream = engine.createParser ? pc.green("yes") : pc.dim("text");
1536
- const resume = engine.supportsResume ? pc.green("yes") : pc.dim("no");
1537
- const location = binaryPath ?? pc.dim(`not found \u2014 ${engine.install}`);
1538
- console.log(
1539
- `${status} ${engine.id.padEnd(10)} ${engine.name.padEnd(14)} ${stream.padEnd(15)} ${resume.padEnd(14)} ${location}`
1540
- );
1541
- }
1542
- console.log(pc.dim("\nplan/safe/yolo modes map onto every engine \xB7 squint doctor --probe verifies auth"));
1543
- });
1544
- program.command("doctor").description("Check squint prerequisites and engine availability").option("--probe", "run each detected engine with a one-word prompt to verify auth works").action(async (options) => {
1545
- const [major] = process.versions.node.split(".");
1546
- const nodeOk = Number(major) >= 22;
1547
- console.log(`${nodeOk ? pc.green("\u2713") : pc.red("\u2717")} node ${process.versions.node}${nodeOk ? "" : " (need >= 22)"}`);
1548
- const detected = detectEngines();
1549
- for (const { engine, path: binaryPath } of detected) {
1550
- const status = binaryPath ? pc.green("\u2713") : pc.yellow("\u25CB");
1551
- console.log(`${status} ${engine.name}${binaryPath ? "" : pc.dim(` \u2014 install: ${engine.install}`)}`);
1552
- }
1553
- if (options.probe) {
1554
- const { runAgent: runAgent2 } = await import("./run-S772BPMZ.js");
1555
- console.log(pc.dim("\nprobing engines with a one-word prompt (verifies auth end to end)\u2026"));
1556
- for (const { engine, path: binaryPath } of detected) {
1557
- if (!binaryPath) continue;
1558
- const startedAt = Date.now();
1559
- const abort = new AbortController();
1560
- const timer = setTimeout(() => abort.abort(), 9e4);
1561
- const result = await runAgent2(
1562
- engine,
1563
- { prompt: "Reply with exactly: ok", cwd: process.cwd() },
1564
- () => {
1565
- },
1566
- abort.signal
1567
- );
1568
- clearTimeout(timer);
1569
- const secs = ((Date.now() - startedAt) / 1e3).toFixed(1);
1570
- const detail = (result.error ?? "failed").split("\n").filter((l) => l.trim()).at(-1) ?? "failed";
1571
- console.log(
1572
- result.ok ? `${pc.green("\u2713")} ${engine.id.padEnd(10)} responded in ${secs}s` : `${pc.red("\u2717")} ${engine.id.padEnd(10)} ${pc.dim(detail.slice(0, 110))}`
1573
- );
2110
+ const engineId = resolveEngineId(config);
2111
+ const model = resolveModel(config, engineId);
2112
+ let theme2 = config.theme;
2113
+ if (!theme2 && !process.env.NO_COLOR) {
2114
+ const { detectBackground } = await import("./background-J7OQTYEC.js");
2115
+ if (await detectBackground() === "light") theme2 = "light";
1574
2116
  }
1575
- }
1576
- const { findChrome: findChrome2 } = await import("./chrome-NVU47PLK.js");
1577
- const { hasWebSocket } = await import("./cdp-SFWNP7OA.js");
1578
- const chrome = findChrome2();
1579
- console.log(
1580
- chrome ? `${pc.green("\u2713")} Chrome ${pc.dim(chrome)}` : `${pc.yellow("\u25CB")} Chrome ${pc.dim("\u2014 screenshots and runtime probing disabled")}`
1581
- );
1582
- console.log(
1583
- 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")}`
1584
- );
1585
- const available = detected.filter((d) => d.path !== null);
1586
- if (available.length === 0) {
1587
- console.log(pc.red("\nNo engines found. Install at least one to use squint."));
1588
- process.exitCode = 1;
1589
- } else {
1590
- console.log(pc.dim(`
1591
- ${available.length} engine(s) ready.`));
1592
- }
1593
- });
1594
- 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) => {
1595
- const { installDependencies, writeTemplate } = await import("./init-LSYB32NS.js");
1596
- let result;
1597
- try {
1598
- result = writeTemplate(dir, { force: options.force });
1599
- } catch (err) {
1600
- console.error(pc.red(`\u2717 ${err instanceof Error ? err.message : String(err)}`));
1601
- process.exitCode = 1;
1602
- return;
1603
- }
1604
- console.log(pc.green(`\u2713 scaffolded ${result.files.length} files in ${result.dir}`));
1605
- if (options.install) {
1606
- console.log(pc.dim("installing dependencies\u2026"));
1607
- const ok = await installDependencies(result.dir);
1608
- if (!ok) {
1609
- console.error(pc.red("\u2717 npm install failed \u2014 run it manually"));
1610
- process.exitCode = 1;
1611
- return;
1612
- }
1613
- }
1614
- const cd = result.dir === "." ? "" : `cd ${result.dir} && `;
1615
- console.log(`
1616
- Next: ${pc.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
1617
- console.log(pc.dim("then describe what to build \u2014 /dev starts the preview server"));
1618
- });
1619
- var skillsCommand = program.command("skills").description("Project knowledge injected into asks (.squint/rules.md + .squint/skills/)");
1620
- skillsCommand.command("list").description("Show always-on rules and trigger-matched skills").action(async () => {
1621
- const { loadRules, loadSkills } = await import("./skills-DNBHZAAU.js");
1622
- const cwd = process.cwd();
1623
- const rules = loadRules(cwd);
1624
- console.log(rules ? `${pc.green("\u2713")} rules.md ${pc.dim(`(${rules.split("\n").length} lines, always on)`)}` : pc.dim("\u25CB no .squint/rules.md"));
1625
- const skills = loadSkills(cwd);
1626
- if (skills.length === 0) {
1627
- console.log(pc.dim("\u25CB no skills \u2014 squint skills init writes an example"));
1628
- return;
1629
- }
1630
- for (const skill of skills) {
1631
- console.log(`${pc.green("\u2713")} ${skill.name.padEnd(20)} ${pc.dim(`triggers: ${skill.triggers.join(", ")}`)}`);
1632
- }
1633
- });
1634
- skillsCommand.command("init").description("Scaffold .squint/rules.md and an example skill").action(async () => {
1635
- const fs2 = await import("fs");
1636
- const nodePath = await import("path");
1637
- const cwd = process.cwd();
1638
- const skillsDir = nodePath.join(cwd, ".squint", "skills");
1639
- fs2.mkdirSync(skillsDir, { recursive: true });
1640
- const rules = nodePath.join(cwd, ".squint", "rules.md");
1641
- if (!fs2.existsSync(rules)) {
1642
- fs2.writeFileSync(
1643
- rules,
1644
- "# Project rules\n\nThese ride along on every squint ask. Keep them short \u2014 cut anything that would not cause a mistake if removed.\n"
1645
- );
1646
- console.log(pc.green("\u2713 .squint/rules.md"));
1647
- }
1648
- const example = nodePath.join(skillsDir, "example.md");
1649
- if (!fs2.existsSync(example)) {
1650
- fs2.writeFileSync(
1651
- example,
1652
- "---\ntriggers: example, sample\n---\n\nThis note is injected only when an ask mentions one of the triggers above.\nDocument the parts of this repo an agent would otherwise rediscover every time:\nwhere state lives, which helpers to reuse, what not to touch.\n"
1653
- );
1654
- console.log(pc.green("\u2713 .squint/skills/example.md"));
1655
- }
1656
- console.log(pc.dim("rules are always-on; skills inject when an ask mentions a trigger"));
1657
- });
1658
- 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 () => {
1659
- const fs2 = await import("fs");
1660
- const nodePath = await import("path");
1661
- const { patchViteConfig, TAGGER_FILENAME, TAGGER_SOURCE } = await import("./source-ZZU245VN.js");
1662
- const cwd = process.cwd();
1663
- const taggerPath = nodePath.join(cwd, TAGGER_FILENAME);
1664
- fs2.writeFileSync(taggerPath, TAGGER_SOURCE);
1665
- console.log(pc.green(`\u2713 ${TAGGER_FILENAME} written`));
1666
- const configPath = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].map((name) => nodePath.join(cwd, name)).find((candidate) => fs2.existsSync(candidate));
1667
- if (!configPath) {
1668
- console.log(pc.yellow("\u25CB no vite config found \u2014 add the plugin manually:"));
1669
- console.log(pc.dim(` import squintTagger from './${TAGGER_FILENAME}'
1670
- plugins: [squintTagger(), \u2026]`));
1671
- return;
1672
- }
1673
- const source = fs2.readFileSync(configPath, "utf8");
1674
- const patched = patchViteConfig(source);
1675
- if (patched === "already") {
1676
- console.log(pc.dim("vite config already wired"));
1677
- } else if (patched === null) {
1678
- console.log(pc.yellow(`\u25CB could not patch ${nodePath.basename(configPath)} automatically \u2014 add:`));
1679
- console.log(pc.dim(` import squintTagger from './${TAGGER_FILENAME}'
1680
- plugins: [squintTagger(), \u2026]`));
1681
- } else {
1682
- fs2.writeFileSync(configPath, patched);
1683
- console.log(pc.green(`\u2713 ${nodePath.basename(configPath)} wired`));
1684
- }
1685
- console.log(pc.dim("\nin the running app: Alt+S \u2192 click an element \u2192 paste the copied file:line:col into squint"));
1686
- });
1687
- var variantsCommand = program.command("variants").description("Parallel design explorations \u2014 one aesthetic family each, pick with your eyes");
1688
- 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(
1689
- async (nRaw, promptWords, options) => {
1690
- const cwd = process.cwd();
1691
- const n = Number.parseInt(nRaw, 10);
1692
- if (!Number.isInteger(n) || n < 2 || n > 4) {
1693
- console.error(pc.red("\u2717 n must be 2\u20134"));
1694
- process.exitCode = 1;
1695
- return;
1696
- }
1697
- const { isGitRepo: isGitRepo2 } = await import("./snapshot-H6VYFHLN.js");
1698
- if (!isGitRepo2(cwd)) {
1699
- console.error(pc.red("\u2717 variants need a git repo with at least one commit"));
1700
- process.exitCode = 1;
1701
- return;
1702
- }
1703
- const { runVariants: runVariants2, cleanVariants: cleanVariants2 } = await import("./variants-2IM274AL.js");
1704
- const config = loadConfig(defaultPaths(cwd));
1705
- const engineId = resolveEngineId(config, options.engine);
1706
- const engine = getEngine(engineId);
1707
- const model = resolveModel(config, engineId, options.model);
1708
- const ask = promptWords.join(" ");
1709
- cleanVariants2(cwd);
1710
- console.log(pc.dim(`generating ${n} directions in parallel via ${engine.id} \u2014 this runs ${n} engine sessions`));
1711
- const runs = await runVariants2(
1712
- cwd,
1713
- ask,
1714
- n,
1715
- engine,
1716
- model,
1717
- (familyId, text) => console.log(`${pc.cyan(familyId.padEnd(18))} ${pc.dim(text)}`)
1718
- );
1719
- const succeeded = runs.filter((r) => r.result.ok);
1720
- if (options.shots && succeeded.length > 0) {
1721
- const { screenshotVariants: screenshotVariants2 } = await import("./shots-J7JG2FC4.js");
1722
- console.log(pc.dim("capturing screenshots\u2026"));
1723
- const shots = await screenshotVariants2(cwd, succeeded.map((r) => r.variant));
1724
- for (const shot of shots) {
1725
- console.log(`${pc.green("\u2713")} ${shot.familyId.padEnd(18)} ${shot.path ?? pc.dim(shot.error ?? "")}`);
1726
- }
1727
- }
1728
- console.log(
1729
- `
1730
- ${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")
2117
+ render(
2118
+ /* @__PURE__ */ jsx4(
2119
+ App,
2120
+ {
2121
+ cwd,
2122
+ initialEngine: engineId,
2123
+ initialModel: model,
2124
+ autoDev: config.autoDev,
2125
+ autoFix: config.autoFix,
2126
+ autoProbe: config.autoProbe,
2127
+ autoCheck: config.autoCheck,
2128
+ autoReview: config.autoReview,
2129
+ bell: config.bell,
2130
+ budgetUsd: config.budgetUsd,
2131
+ initialTheme: theme2
2132
+ }
2133
+ )
1731
2134
  );
1732
- if (succeeded.length === 0) process.exitCode = 1;
1733
- }
1734
- );
1735
- variantsCommand.command("list").description("List generated variants").action(async () => {
1736
- const { listVariants: listVariants2 } = await import("./variants-2IM274AL.js");
1737
- const ids = listVariants2(process.cwd());
1738
- if (ids.length === 0) {
1739
- console.log(pc.dim('no variants \u2014 squint variants gen <n> "<ask>"'));
1740
- return;
1741
- }
1742
- for (const id of ids) console.log(id);
1743
- });
1744
- 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) => {
1745
- const { applyVariant: applyVariant2, cleanVariants: cleanVariants2 } = await import("./variants-2IM274AL.js");
1746
- const cwd = process.cwd();
1747
- const result = applyVariant2(cwd, id);
1748
- if (!result.ok) {
1749
- console.error(pc.red(`\u2717 ${result.detail}`));
1750
- process.exitCode = 1;
1751
- return;
1752
- }
1753
- cleanVariants2(cwd);
1754
- console.log(pc.green(`\u2713 applied ${id} to the working tree`) + pc.dim(" \u2014 review with git diff"));
1755
- });
1756
- variantsCommand.command("clean").description("Discard all variants").action(async () => {
1757
- const { cleanVariants: cleanVariants2 } = await import("./variants-2IM274AL.js");
1758
- const count = cleanVariants2(process.cwd());
1759
- console.log(pc.dim(`removed ${count} variant(s)`));
1760
- });
1761
- 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) => {
1762
- const fs2 = await import("fs");
1763
- const nodePath = await import("path");
1764
- const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-2G5SLLY7.js");
1765
- if (!familyId) {
1766
- console.log(pc.bold("Aesthetic families") + pc.dim(" \u2014 squint brief <id>\n"));
1767
- for (const family2 of FAMILIES) {
1768
- console.log(`${pc.green(family2.id.padEnd(18))} ${family2.name.padEnd(22)} ${pc.dim(family2.summary)}`);
1769
- }
1770
- console.log(pc.dim("\nThe brief wraps every ask; edit .squint/brief.md to remix."));
1771
- return;
1772
- }
1773
- const family = getFamily(familyId);
1774
- if (!family) {
1775
- console.error(pc.red(`\u2717 unknown family "${familyId}" \u2014 run squint brief to list`));
1776
- process.exitCode = 1;
1777
- return;
1778
- }
1779
- const target = nodePath.join(process.cwd(), ".squint", "brief.md");
1780
- if (fs2.existsSync(target) && !options.force) {
1781
- console.error(pc.red(`\u2717 ${target} exists \u2014 use --force to overwrite`));
1782
- process.exitCode = 1;
1783
- return;
1784
- }
1785
- fs2.mkdirSync(nodePath.dirname(target), { recursive: true });
1786
- fs2.writeFileSync(target, renderFamilyBrief(family) + "\n");
1787
- console.log(pc.green(`\u2713 ${family.name} direction written to .squint/brief.md`));
1788
- console.log(pc.dim("every squint ask in this repo now holds this direction \u2014 edit the file to remix"));
1789
- });
1790
- program.command("check").description("Run this project\u2019s quality gates (typecheck, lint, test, build)").action(async () => {
1791
- const { detectGates: detectGates2, runGates: runGates2 } = await import("./gates-ZC67NMW3.js");
1792
- const cwd = process.cwd();
1793
- const gates = detectGates2(cwd);
1794
- if (gates.length === 0) {
1795
- console.log(pc.dim("no gates detected (no package.json scripts, tsconfig, or eslint config)"));
1796
- return;
1797
- }
1798
- console.log(pc.dim(`running ${gates.map((g) => g.id).join(" \u2192 ")}`));
1799
- const results = await runGates2(cwd, gates, (result) => {
1800
- const mark = result.ok ? pc.green("\u2713") : pc.red("\u2717");
1801
- console.log(`${mark} ${result.gate.id.padEnd(10)} ${pc.dim(`${(result.durationMs / 1e3).toFixed(1)}s \xB7 ${result.gate.display}`)}`);
1802
- if (!result.ok) console.log(pc.dim(result.outputTail.split("\n").slice(-12).join("\n")));
1803
2135
  });
1804
- if (results.some((r) => !r.ok)) process.exitCode = 1;
1805
- });
1806
- 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) => {
1807
- const { captureViewports: captureViewports2 } = await import("./preview-UCH7H5R5.js");
1808
- const result = await captureViewports2(process.cwd(), url);
1809
- if (!result) {
1810
- console.error(pc.red("\u2717 no Chrome/Chromium found"));
1811
- process.exitCode = 1;
1812
- return;
1813
- }
1814
- for (const shot of result.shots) console.log(`${pc.green("\u2713")} ${shot.name.padEnd(8)} ${shot.path}`);
1815
- for (const error of result.errors) console.error(pc.red(`\u2717 ${error}`));
1816
- if (result.shots.length === 0) process.exitCode = 1;
1817
- });
1818
- var configCommand = program.command("config").description("Read or change squint configuration");
1819
- configCommand.command("get").description("Print the resolved configuration").action(() => {
1820
- const paths = defaultPaths(process.cwd());
1821
- console.log(JSON.stringify(loadConfig(paths), null, 2));
1822
- });
1823
- 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) => {
1824
- const paths = defaultPaths(process.cwd());
1825
- const file = options.project ? paths.projectFile : paths.globalFile;
1826
- setConfigValue(file, key, value);
1827
- console.log(pc.green(`\u2713 ${key} = ${value}`) + pc.dim(` (${file})`));
1828
- });
1829
- configCommand.command("path").description("Show config file locations").action(() => {
1830
- const paths = defaultPaths(process.cwd());
1831
- console.log(`global ${paths.globalFile}`);
1832
- console.log(`project ${paths.projectFile}`);
1833
- });
1834
- program.action(async () => {
1835
- const cwd = process.cwd();
1836
- const config = loadConfig(defaultPaths(cwd));
1837
- const engineId = resolveEngineId(config);
1838
- const model = resolveModel(config, engineId);
1839
- let theme2 = config.theme;
1840
- if (!theme2 && !process.env.NO_COLOR) {
1841
- const { detectBackground } = await import("./background-J7OQTYEC.js");
1842
- if (await detectBackground() === "light") theme2 = "light";
1843
- }
1844
- render(
1845
- /* @__PURE__ */ jsx4(
1846
- App,
1847
- {
1848
- cwd,
1849
- initialEngine: engineId,
1850
- initialModel: model,
1851
- autoDev: config.autoDev,
1852
- autoFix: config.autoFix,
1853
- autoProbe: config.autoProbe,
1854
- autoCheck: config.autoCheck,
1855
- bell: config.bell,
1856
- budgetUsd: config.budgetUsd,
1857
- initialTheme: theme2
1858
- }
1859
- )
1860
- );
1861
- });
1862
- function createPrinter() {
1863
- let streaming = false;
1864
- return (event) => {
1865
- switch (event.type) {
1866
- case "status":
1867
- console.log(pc.dim(`\xB7 ${event.text}`));
1868
- break;
1869
- case "delta":
1870
- streaming = true;
1871
- process.stdout.write(event.text);
1872
- break;
1873
- case "text":
1874
- if (event.streamed && streaming) {
1875
- process.stdout.write("\n");
1876
- } else {
1877
- console.log(event.text);
1878
- }
1879
- streaming = false;
1880
- break;
1881
- case "thinking":
1882
- console.log(pc.dim(pc.italic(event.text)));
1883
- break;
1884
- case "tool":
1885
- if (streaming) {
1886
- process.stdout.write("\n");
1887
- streaming = false;
1888
- }
1889
- console.log(pc.cyan(`\u2699 ${event.name}${event.detail ? ` \xB7 ${event.detail}` : ""}`));
1890
- break;
1891
- case "error":
1892
- console.error(pc.red(`\u2717 ${event.text}`));
1893
- break;
1894
- case "result":
1895
- case "raw":
1896
- break;
1897
- }
1898
- };
1899
2136
  }
2137
+
2138
+ // src/cli.tsx
2139
+ var VERSION = "0.2.8";
2140
+ var program = new Command();
2141
+ program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
2142
+ registerRun(program);
2143
+ registerEnv(program);
2144
+ registerScaffold(program);
2145
+ registerProject(program);
2146
+ registerQuality(program);
2147
+ registerConfig(program);
2148
+ registerTui(program);
1900
2149
  program.parseAsync(process.argv);