@aayambansal/squint 0.2.7 → 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/README.md CHANGED
@@ -132,6 +132,7 @@ squint config set models.claude claude-sonnet-5
132
132
  squint config set autoDev true # dev server starts with the TUI
133
133
  squint config set autoFix true # errors auto-route back (max 2 tries)
134
134
  squint config set autoCheck false # skip the per-turn typecheck+lint pass
135
+ squint config set autoReview true # big visual change → automatic self-critique
135
136
  squint config set theme ocean # amber · ocean · moss · rose · mono
136
137
  squint config set bell false # no bell on turn completion
137
138
  squint doctor # engines + Chrome + WebSocket check
@@ -9,6 +9,7 @@ var COMMANDS = [
9
9
  { name: "shot", description: "screenshot the app at mobile/tablet/desktop" },
10
10
  { name: "review", args: "[focus]", description: "screenshots + the engine critiques its own rendered work" },
11
11
  { name: "variants", args: "<2-4> <ask>", description: "parallel design explorations; apply/list/clean" },
12
+ { name: "sandbox", args: "[on|diff|apply|discard]", description: "asks accumulate in a shadow worktree until you apply" },
12
13
  { name: "undo", description: "revert the last ask (files only)" },
13
14
  { name: "checkpoints", description: "list per-ask checkpoints" },
14
15
  { name: "restore", args: "<n>", description: "rewind files to before ask n" },
package/dist/cli.js CHANGED
@@ -30,7 +30,7 @@ import {
30
30
  } from "./chunk-CHZE3UHK.js";
31
31
  import {
32
32
  completeCommand
33
- } from "./chunk-RZOQPFXC.js";
33
+ } from "./chunk-YTO2YRF7.js";
34
34
  import {
35
35
  runAgent
36
36
  } from "./chunk-C7WKNJG6.js";
@@ -265,22 +265,22 @@ function registerProject(program2) {
265
265
  }
266
266
  });
267
267
  skillsCommand.command("init").description("Scaffold .squint/rules.md and an example skill").action(async () => {
268
- const fs2 = await import("fs");
268
+ const fs3 = await import("fs");
269
269
  const nodePath = await import("path");
270
270
  const cwd = process.cwd();
271
271
  const skillsDir = nodePath.join(cwd, ".squint", "skills");
272
- fs2.mkdirSync(skillsDir, { recursive: true });
272
+ fs3.mkdirSync(skillsDir, { recursive: true });
273
273
  const rules = nodePath.join(cwd, ".squint", "rules.md");
274
- if (!fs2.existsSync(rules)) {
275
- fs2.writeFileSync(
274
+ if (!fs3.existsSync(rules)) {
275
+ fs3.writeFileSync(
276
276
  rules,
277
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
278
  );
279
279
  console.log(pc3.green("\u2713 .squint/rules.md"));
280
280
  }
281
281
  const example = nodePath.join(skillsDir, "example.md");
282
- if (!fs2.existsSync(example)) {
283
- fs2.writeFileSync(
282
+ if (!fs3.existsSync(example)) {
283
+ fs3.writeFileSync(
284
284
  example,
285
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
286
  );
@@ -289,7 +289,7 @@ function registerProject(program2) {
289
289
  console.log(pc3.dim("rules are always-on; skills inject when an ask mentions a trigger"));
290
290
  });
291
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 fs2 = await import("fs");
292
+ const fs3 = await import("fs");
293
293
  const nodePath = await import("path");
294
294
  const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-2G5SLLY7.js");
295
295
  if (!familyId) {
@@ -307,13 +307,13 @@ function registerProject(program2) {
307
307
  return;
308
308
  }
309
309
  const target = nodePath.join(process.cwd(), ".squint", "brief.md");
310
- if (fs2.existsSync(target) && !options.force) {
310
+ if (fs3.existsSync(target) && !options.force) {
311
311
  console.error(pc3.red(`\u2717 ${target} exists \u2014 use --force to overwrite`));
312
312
  process.exitCode = 1;
313
313
  return;
314
314
  }
315
- fs2.mkdirSync(nodePath.dirname(target), { recursive: true });
316
- fs2.writeFileSync(target, renderFamilyBrief(family) + "\n");
315
+ fs3.mkdirSync(nodePath.dirname(target), { recursive: true });
316
+ fs3.writeFileSync(target, renderFamilyBrief(family) + "\n");
317
317
  console.log(pc3.green(`\u2713 ${family.name} direction written to .squint/brief.md`));
318
318
  console.log(pc3.dim("every squint ask in this repo now holds this direction \u2014 edit the file to remix"));
319
319
  });
@@ -533,21 +533,21 @@ Next: ${pc6.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
533
533
  console.log(pc6.dim("then describe what to build \u2014 /dev starts the preview server"));
534
534
  });
535
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 fs2 = await import("fs");
536
+ const fs3 = await import("fs");
537
537
  const nodePath = await import("path");
538
538
  const { patchViteConfig, TAGGER_FILENAME, TAGGER_SOURCE } = await import("./source-ZZU245VN.js");
539
539
  const cwd = process.cwd();
540
540
  const taggerPath = nodePath.join(cwd, TAGGER_FILENAME);
541
- fs2.writeFileSync(taggerPath, TAGGER_SOURCE);
541
+ fs3.writeFileSync(taggerPath, TAGGER_SOURCE);
542
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) => fs2.existsSync(candidate));
543
+ const configPath = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].map((name) => nodePath.join(cwd, name)).find((candidate) => fs3.existsSync(candidate));
544
544
  if (!configPath) {
545
545
  console.log(pc6.yellow("\u25CB no vite config found \u2014 add the plugin manually:"));
546
546
  console.log(pc6.dim(` import squintTagger from './${TAGGER_FILENAME}'
547
547
  plugins: [squintTagger(), \u2026]`));
548
548
  return;
549
549
  }
550
- const source = fs2.readFileSync(configPath, "utf8");
550
+ const source = fs3.readFileSync(configPath, "utf8");
551
551
  const patched = patchViteConfig(source);
552
552
  if (patched === "already") {
553
553
  console.log(pc6.dim("vite config already wired"));
@@ -556,7 +556,7 @@ Next: ${pc6.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
556
556
  console.log(pc6.dim(` import squintTagger from './${TAGGER_FILENAME}'
557
557
  plugins: [squintTagger(), \u2026]`));
558
558
  } else {
559
- fs2.writeFileSync(configPath, patched);
559
+ fs3.writeFileSync(configPath, patched);
560
560
  console.log(pc6.green(`\u2713 ${nodePath.basename(configPath)} wired`));
561
561
  }
562
562
  console.log(
@@ -570,11 +570,97 @@ import { render } from "ink";
570
570
 
571
571
  // src/tui/App.tsx
572
572
  import { Box as Box2, Static, Text as Text3, useApp, useInput } from "ink";
573
- import path3 from "path";
573
+ import path4 from "path";
574
574
  import { useMemo, useRef, useState as useState2, useSyncExternalStore } from "react";
575
575
 
576
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";
577
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
578
664
  var MAX_AUTO_FIX_ATTEMPTS = 2;
579
665
  var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
580
666
  var Session = class {
@@ -592,7 +678,8 @@ var Session = class {
592
678
  totals: { costUsd: 0, turns: 0 },
593
679
  queue: [],
594
680
  mode: "safe",
595
- problems: []
681
+ problems: [],
682
+ sandbox: false
596
683
  };
597
684
  if (opts.autoDev && detectDevCommand(opts.cwd)) {
598
685
  this.devServer().start();
@@ -762,15 +849,33 @@ var Session = class {
762
849
  this.push("assistant", text);
763
850
  }
764
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
+ }
765
856
  devServer() {
766
857
  if (!this.dev) {
767
- this.dev = new DevServer(this.opts.cwd, {
858
+ this.dev = new DevServer(this.execCwd(), {
768
859
  onStateChange: (devState) => this.notify({ devState }),
769
860
  onUrl: (url) => this.notify({ devUrl: url })
770
861
  });
771
862
  }
772
863
  return this.dev;
773
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
+ }
774
879
  turnEdits = 0;
775
880
  turnTools = 0;
776
881
  toolStreak = 0;
@@ -849,7 +954,7 @@ var Session = class {
849
954
  engine,
850
955
  {
851
956
  prompt,
852
- cwd: this.opts.cwd,
957
+ cwd: this.execCwd(),
853
958
  model: this.state.model,
854
959
  mode: this.state.mode,
855
960
  sessionId: engine.supportsResume ? this.sessionId : void 0
@@ -892,9 +997,9 @@ var Session = class {
892
997
  }
893
998
  }
894
999
  if (result.ok && this.opts.autoCheck !== false) {
895
- const fastGates = detectFastGates(this.opts.cwd);
1000
+ const fastGates = detectFastGates(this.execCwd());
896
1001
  if (fastGates.length > 0) {
897
- const gateResults = await runGates(this.opts.cwd, fastGates);
1002
+ const gateResults = await runGates(this.execCwd(), fastGates);
898
1003
  const failures = gateResults.filter((r) => !r.ok);
899
1004
  if (failures.length > 0) {
900
1005
  this.addProblem(
@@ -969,7 +1074,7 @@ ${errors.slice(-5).join("\n")}`);
969
1074
  async submit(ask) {
970
1075
  this.fixAttempts = 0;
971
1076
  this.autoReviewedThisAsk = false;
972
- const snapshot = takeSnapshot(this.opts.cwd);
1077
+ const snapshot = this.state.sandbox ? null : takeSnapshot(this.opts.cwd);
973
1078
  if (snapshot) {
974
1079
  this.checkpoints.push({
975
1080
  snapshot,
@@ -1049,7 +1154,7 @@ ${errors.slice(-5).join("\n")}`);
1049
1154
  if (result.shots.length > 0) {
1050
1155
  this.push(
1051
1156
  "status",
1052
- `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)}`
1053
1158
  );
1054
1159
  }
1055
1160
  if (result.runtime) {
@@ -1176,12 +1281,12 @@ They are objective defects, not style preferences.`
1176
1281
  }
1177
1282
  case "save": {
1178
1283
  void (async () => {
1179
- const fs2 = await import("fs");
1180
- const path4 = await import("path");
1181
- const dir = path4.join(this.opts.cwd, ".squint", "transcripts");
1182
- 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 });
1183
1288
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
1184
- const file = path4.join(dir, `${stamp}.md`);
1289
+ const file = path5.join(dir, `${stamp}.md`);
1185
1290
  const lines = [`# squint session \u2014 ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}`, ""];
1186
1291
  for (const item of this.state.items) {
1187
1292
  switch (item.role) {
@@ -1201,8 +1306,8 @@ They are objective defects, not style preferences.`
1201
1306
  }
1202
1307
  }
1203
1308
  lines.push("", `> ${this.summary()}`);
1204
- fs2.writeFileSync(file, lines.join("\n") + "\n");
1205
- 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)}`);
1206
1311
  })();
1207
1312
  break;
1208
1313
  }
@@ -1320,6 +1425,57 @@ They are objective defects, not style preferences.`
1320
1425
  this.push("status", `resumed ${saved.engine} session${saved.lastAsk ? ` \xB7 "${saved.lastAsk}"` : ""}`);
1321
1426
  break;
1322
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
+ }
1323
1479
  case "variants": {
1324
1480
  const [sub, ...subRest] = arg.split(/\s+/);
1325
1481
  if (sub === "apply") {
@@ -1395,7 +1551,7 @@ They are objective defects, not style preferences.`
1395
1551
  this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
1396
1552
  break;
1397
1553
  case "help": {
1398
- void import("./commands-TYAKF276.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1554
+ void import("./commands-N33DKZKC.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1399
1555
  break;
1400
1556
  }
1401
1557
  case "quit":
@@ -1925,11 +2081,12 @@ function App({
1925
2081
  ]
1926
2082
  }
1927
2083
  ),
2084
+ state.sandbox && /* @__PURE__ */ jsx3(Text3, { color: theme2.accent, bold: true, children: " [sandbox]" }),
1928
2085
  " ",
1929
2086
  state.engineId,
1930
2087
  state.model ? ` \xB7 ${state.model}` : "",
1931
2088
  " \xB7 ",
1932
- path3.basename(cwd),
2089
+ path4.basename(cwd),
1933
2090
  devBadge,
1934
2091
  totalsBadge,
1935
2092
  state.problems.length > 0 && /* @__PURE__ */ jsxs3(Text3, { color: theme2.error, children: [
@@ -1979,7 +2136,7 @@ function registerTui(program2) {
1979
2136
  }
1980
2137
 
1981
2138
  // src/cli.tsx
1982
- var VERSION = "0.2.7";
2139
+ var VERSION = "0.2.8";
1983
2140
  var program = new Command();
1984
2141
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
1985
2142
  registerRun(program);
@@ -3,7 +3,7 @@ import {
3
3
  COMMANDS,
4
4
  commandHelp,
5
5
  completeCommand
6
- } from "./chunk-RZOQPFXC.js";
6
+ } from "./chunk-YTO2YRF7.js";
7
7
  export {
8
8
  COMMANDS,
9
9
  commandHelp,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aayambansal/squint",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Lovable for your terminal — a frontend harness on top of Claude Code, Codex, and other coding agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",