@aayambansal/squint 0.2.7 → 0.2.9

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
@@ -149,6 +150,9 @@ squint doctor --probe # run every engine end to end, verify auth act
149
150
  - **Problems**: findings from gates, the dev server, the runtime probe, and a11y sweeps
150
151
  collect into a list — `/problems` shows it, `/fix` sends everything as one turn,
151
152
  `/fix <n>` targets one. The footer counts what's open.
153
+ - **Sandbox**: `/sandbox on` and asks accumulate in a shadow worktree — the dev server,
154
+ gates, and probes all run there; `/sandbox diff` shows what is staged, `apply` lands it
155
+ as one diff, `discard` walks away with the real tree untouched.
152
156
  - **Variants without leaving**: `/variants 3 <ask>` runs parallel explorations with
153
157
  streaming per-family status; `/variants apply <id>` keeps the winner.
154
158
  - **Commands**: type `/` and matching commands appear with descriptions; tab completes.
@@ -227,6 +231,7 @@ All product behavior lives in the harness, so a new engine is ~80 lines.
227
231
 
228
232
  - [Engine setup guide](./docs/engines.md) — install + auth for all eight, and how to choose
229
233
  - [Configuration](./docs/configuration.md) — every key, every `.squint/` file
234
+ - [The loops](./docs/loops.md) — everything that runs automatically around each turn
230
235
  - [Architecture](./docs/design/2026-07-25-architecture.md)
231
236
  - [How Lovable works under the hood](./docs/research/lovable.md)
232
237
  - [Making agents produce excellent frontend work](./docs/research/frontend-quality.md)
@@ -7,56 +7,14 @@ import {
7
7
  cdpCapture,
8
8
  hasWebSocket
9
9
  } from "./chunk-OC6RU6XH.js";
10
+ import {
11
+ ensureSquintIgnore
12
+ } from "./chunk-O2S6PAJE.js";
10
13
 
11
14
  // src/preview/preview.ts
12
- import fs2 from "fs";
13
- import os from "os";
14
- import path2 from "path";
15
-
16
- // src/state/state.ts
17
15
  import fs from "fs";
16
+ import os from "os";
18
17
  import path from "path";
19
- var IGNORED = ["preview/", "state.json", "variants/", "transcripts/"];
20
- function ensureSquintIgnore(cwd) {
21
- const dir = path.join(cwd, ".squint");
22
- fs.mkdirSync(dir, { recursive: true });
23
- const file = path.join(dir, ".gitignore");
24
- let existing = "";
25
- try {
26
- existing = fs.readFileSync(file, "utf8");
27
- } catch {
28
- }
29
- const lines = existing.split("\n").filter((l) => l.trim().length > 0);
30
- const missing = IGNORED.filter((entry) => !lines.includes(entry));
31
- if (missing.length > 0) {
32
- fs.writeFileSync(file, [...lines, ...missing].join("\n") + "\n");
33
- }
34
- }
35
- function stateFile(cwd) {
36
- return path.join(cwd, ".squint", "state.json");
37
- }
38
- function loadState(cwd) {
39
- try {
40
- const raw = JSON.parse(fs.readFileSync(stateFile(cwd), "utf8"));
41
- if (typeof raw?.engine === "string" && typeof raw?.sessionId === "string" && typeof raw?.at === "number") {
42
- return raw;
43
- }
44
- } catch {
45
- }
46
- return null;
47
- }
48
- function saveState(cwd, state) {
49
- ensureSquintIgnore(cwd);
50
- fs.writeFileSync(stateFile(cwd), JSON.stringify(state, null, 2) + "\n");
51
- }
52
- function clearState(cwd) {
53
- try {
54
- fs.rmSync(stateFile(cwd));
55
- } catch {
56
- }
57
- }
58
-
59
- // src/preview/preview.ts
60
18
  var VIEWPORTS = [
61
19
  { name: "mobile", width: 390, height: 844 },
62
20
  { name: "tablet", width: 768, height: 1024 },
@@ -65,7 +23,7 @@ var VIEWPORTS = [
65
23
  function loadRoutes(cwd) {
66
24
  let lines = [];
67
25
  try {
68
- lines = fs2.readFileSync(path2.join(cwd, ".squint", "routes"), "utf8").split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
26
+ lines = fs.readFileSync(path.join(cwd, ".squint", "routes"), "utf8").split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
69
27
  } catch {
70
28
  }
71
29
  const routes = ["/", ...lines.filter((l) => l !== "/")];
@@ -76,8 +34,8 @@ function routeShotName(route) {
76
34
  return clean.length > 0 ? clean : "root";
77
35
  }
78
36
  function previewDir(cwd) {
79
- const dir = path2.join(cwd, ".squint", "preview");
80
- fs2.mkdirSync(dir, { recursive: true });
37
+ const dir = path.join(cwd, ".squint", "preview");
38
+ fs.mkdirSync(dir, { recursive: true });
81
39
  ensureSquintIgnore(cwd);
82
40
  return dir;
83
41
  }
@@ -111,7 +69,7 @@ async function captureViewports(cwd, url) {
111
69
  const shots = [];
112
70
  const errors = [];
113
71
  for (const viewport of VIEWPORTS) {
114
- const outPath = path2.join(dir, `${viewport.name}.png`);
72
+ const outPath = path.join(dir, `${viewport.name}.png`);
115
73
  const result = await screenshot(chrome, url, outPath, {
116
74
  width: viewport.width,
117
75
  height: viewport.height
@@ -203,9 +161,6 @@ Read each screenshot and review the rendered UI against the design standards you
203
161
  }
204
162
 
205
163
  export {
206
- loadState,
207
- saveState,
208
- clearState,
209
164
  VIEWPORTS,
210
165
  loadRoutes,
211
166
  routeShotName,
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/state/state.ts
4
+ import fs from "fs";
5
+ import path from "path";
6
+ var IGNORED = ["preview/", "state.json", "variants/", "sandbox/", "sandbox.patch", "transcripts/"];
7
+ function ensureSquintIgnore(cwd) {
8
+ const dir = path.join(cwd, ".squint");
9
+ fs.mkdirSync(dir, { recursive: true });
10
+ const file = path.join(dir, ".gitignore");
11
+ let existing = "";
12
+ try {
13
+ existing = fs.readFileSync(file, "utf8");
14
+ } catch {
15
+ }
16
+ const lines = existing.split("\n").filter((l) => l.trim().length > 0);
17
+ const missing = IGNORED.filter((entry) => !lines.includes(entry));
18
+ if (missing.length > 0) {
19
+ fs.writeFileSync(file, [...lines, ...missing].join("\n") + "\n");
20
+ }
21
+ }
22
+ function stateFile(cwd) {
23
+ return path.join(cwd, ".squint", "state.json");
24
+ }
25
+ function loadState(cwd) {
26
+ try {
27
+ const raw = JSON.parse(fs.readFileSync(stateFile(cwd), "utf8"));
28
+ if (typeof raw?.engine === "string" && typeof raw?.sessionId === "string" && typeof raw?.at === "number") {
29
+ return raw;
30
+ }
31
+ } catch {
32
+ }
33
+ return null;
34
+ }
35
+ function saveState(cwd, state) {
36
+ ensureSquintIgnore(cwd);
37
+ fs.writeFileSync(stateFile(cwd), JSON.stringify(state, null, 2) + "\n");
38
+ }
39
+ function clearState(cwd) {
40
+ try {
41
+ fs.rmSync(stateFile(cwd));
42
+ } catch {
43
+ }
44
+ }
45
+
46
+ export {
47
+ ensureSquintIgnore,
48
+ loadState,
49
+ saveState,
50
+ clearState
51
+ };
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/session/commands.ts
4
+ var COMMANDS = [
5
+ { name: "dev", args: "[restart|logs]", group: "build", description: "start/stop the dev server; restart or show recent output" },
6
+ { name: "mode", args: "plan|safe|yolo", group: "build", description: "how much the engine may do (shift+tab cycles)" },
7
+ { name: "engine", args: "<id>", group: "build", description: "switch backend (new session)" },
8
+ { name: "engines", group: "build", description: "list installed engines with streaming/resume support" },
9
+ { name: "model", args: "[name]", group: "build", description: "model override for the engine" },
10
+ { name: "queue", args: "clear", group: "build", description: "drop queued asks" },
11
+ { name: "check", group: "verify", description: "run all quality gates (typecheck, lint, format, test, build)" },
12
+ { name: "problems", group: "verify", description: "list open findings from gates, dev server, runtime, a11y" },
13
+ { name: "fix", args: "[n]", group: "verify", description: "send all open problems to the engine, or just problem n" },
14
+ { name: "shot", args: "[url]", group: "verify", description: "screenshot the app (or any url) at mobile/tablet/desktop" },
15
+ { name: "review", args: "[focus]", group: "verify", description: "screenshots + the engine critiques its own rendered work" },
16
+ { name: "variants", args: "<2-4> <ask>", group: "explore", description: "parallel design explorations; apply/list/clean" },
17
+ { name: "sandbox", args: "[on|diff|apply|discard]", group: "explore", description: "asks accumulate in a shadow worktree until you apply" },
18
+ { name: "undo", group: "explore", description: "revert the last ask (files only)" },
19
+ { name: "checkpoints", group: "explore", description: "list per-ask checkpoints" },
20
+ { name: "restore", args: "<n>", group: "explore", description: "rewind files to before ask n" },
21
+ { name: "theme", args: "[name]", group: "session", description: "switch the TUI theme", viewLevel: true },
22
+ { name: "copy", group: "session", description: "copy the last reply to the clipboard" },
23
+ { name: "save", group: "session", description: "export the transcript to .squint/transcripts/" },
24
+ { name: "resume", group: "session", description: "pick up the previous session for this repo" },
25
+ { name: "clear", group: "session", description: "new session (transcript, totals, persisted state)" },
26
+ { name: "help", group: "session", description: "list commands" },
27
+ { name: "quit", group: "session", description: "exit with a session summary" }
28
+ ];
29
+ function completeCommand(partial) {
30
+ const query = partial.toLowerCase();
31
+ return COMMANDS.filter((c) => c.name.startsWith(query));
32
+ }
33
+ var GROUP_TITLES = {
34
+ build: "build",
35
+ verify: "verify",
36
+ explore: "explore & rewind",
37
+ session: "session"
38
+ };
39
+ function commandHelp() {
40
+ const sections = [];
41
+ for (const group of ["build", "verify", "explore", "session"]) {
42
+ const rows = COMMANDS.filter((c) => c.group === group).map(
43
+ (c) => ` /${c.name}${c.args ? ` ${c.args}` : ""} \u2014 ${c.description}`
44
+ );
45
+ sections.push(`${GROUP_TITLES[group]}
46
+ ${rows.join("\n")}`);
47
+ }
48
+ return sections.join("\n");
49
+ }
50
+
51
+ export {
52
+ COMMANDS,
53
+ completeCommand,
54
+ commandHelp
55
+ };
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/vcs/sandbox.ts
4
+ import { execFileSync } from "child_process";
5
+ import fs from "fs";
6
+ import path from "path";
7
+ function git(cwd, args) {
8
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
9
+ }
10
+ function sandboxDir(cwd) {
11
+ return path.join(cwd, ".squint", "sandbox");
12
+ }
13
+ function sandboxExists(cwd) {
14
+ return fs.existsSync(path.join(sandboxDir(cwd), ".git"));
15
+ }
16
+ function openSandbox(cwd) {
17
+ const dir = sandboxDir(cwd);
18
+ void import("./state-QOS7WCZO.js").then(({ ensureSquintIgnore }) => ensureSquintIgnore(cwd)).catch(() => {
19
+ });
20
+ if (sandboxExists(cwd)) return { dir, reused: true };
21
+ fs.rmSync(dir, { recursive: true, force: true });
22
+ fs.mkdirSync(path.dirname(dir), { recursive: true });
23
+ git(cwd, ["worktree", "add", "--force", "--detach", dir, "HEAD"]);
24
+ const mainModules = path.join(cwd, "node_modules");
25
+ const sandboxModules = path.join(dir, "node_modules");
26
+ if (fs.existsSync(mainModules) && !fs.existsSync(sandboxModules)) {
27
+ fs.symlinkSync(mainModules, sandboxModules, "dir");
28
+ }
29
+ return { dir, reused: false };
30
+ }
31
+ function sandboxDiffStat(cwd) {
32
+ if (!sandboxExists(cwd)) return null;
33
+ const dir = sandboxDir(cwd);
34
+ try {
35
+ git(dir, ["add", "-A"]);
36
+ const stat = git(dir, ["diff", "--cached", "--shortstat", "HEAD"]);
37
+ return stat.length > 0 ? stat : null;
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+ function sandboxFiles(cwd) {
43
+ if (!sandboxExists(cwd)) return [];
44
+ const dir = sandboxDir(cwd);
45
+ try {
46
+ git(dir, ["add", "-A"]);
47
+ const out = git(dir, ["diff", "--cached", "--name-status", "HEAD"]);
48
+ return out.length > 0 ? out.split("\n") : [];
49
+ } catch {
50
+ return [];
51
+ }
52
+ }
53
+ function applySandbox(cwd) {
54
+ if (!sandboxExists(cwd)) return { ok: false, detail: "no sandbox open" };
55
+ const dir = sandboxDir(cwd);
56
+ try {
57
+ git(dir, ["add", "-A"]);
58
+ const patch = git(dir, ["diff", "--binary", "--cached", "HEAD"]);
59
+ if (patch.length === 0) return { ok: false, detail: "sandbox has no changes" };
60
+ const patchFile = path.join(cwd, ".squint", "sandbox.patch");
61
+ fs.writeFileSync(patchFile, patch + "\n");
62
+ git(cwd, ["apply", "--whitespace=nowarn", patchFile]);
63
+ fs.rmSync(patchFile, { force: true });
64
+ return { ok: true };
65
+ } catch (err) {
66
+ return { ok: false, detail: err instanceof Error ? err.message.split("\n")[0] : String(err) };
67
+ }
68
+ }
69
+ function discardSandbox(cwd) {
70
+ if (!sandboxExists(cwd)) return false;
71
+ const dir = sandboxDir(cwd);
72
+ try {
73
+ const link = path.join(dir, "node_modules");
74
+ if (fs.existsSync(link) && fs.lstatSync(link).isSymbolicLink()) fs.rmSync(link);
75
+ } catch {
76
+ }
77
+ try {
78
+ git(cwd, ["worktree", "remove", "--force", dir]);
79
+ } catch {
80
+ fs.rmSync(dir, { recursive: true, force: true });
81
+ }
82
+ try {
83
+ git(cwd, ["worktree", "prune"]);
84
+ } catch {
85
+ }
86
+ return true;
87
+ }
88
+
89
+ export {
90
+ sandboxDir,
91
+ sandboxExists,
92
+ openSandbox,
93
+ sandboxDiffStat,
94
+ sandboxFiles,
95
+ applySandbox,
96
+ discardSandbox
97
+ };
package/dist/cli.js CHANGED
@@ -1,4 +1,22 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ completeCommand
4
+ } from "./chunk-RMNG2NTU.js";
5
+ import {
6
+ applySandbox,
7
+ discardSandbox,
8
+ openSandbox,
9
+ sandboxDiffStat,
10
+ sandboxDir,
11
+ sandboxExists,
12
+ sandboxFiles
13
+ } from "./chunk-XEQ6JXXL.js";
14
+ import {
15
+ diffStatSince,
16
+ isGitRepo,
17
+ restoreSnapshot,
18
+ takeSnapshot
19
+ } from "./chunk-YHRAOBI2.js";
2
20
  import {
3
21
  DevServer,
4
22
  buildFixPrompt,
@@ -21,16 +39,10 @@ import {
21
39
  buildReviewPrompt,
22
40
  buildRuntimeFixPrompt,
23
41
  captureViewports,
24
- clearState,
25
42
  comparePulse,
26
- loadState,
27
43
  probeRuntime,
28
- runtimeSummary,
29
- saveState
30
- } from "./chunk-CHZE3UHK.js";
31
- import {
32
- completeCommand
33
- } from "./chunk-RZOQPFXC.js";
44
+ runtimeSummary
45
+ } from "./chunk-GOAFH2VN.js";
34
46
  import {
35
47
  runAgent
36
48
  } from "./chunk-C7WKNJG6.js";
@@ -49,11 +61,10 @@ import {
49
61
  composePrompt
50
62
  } from "./chunk-P3H4N2EN.js";
51
63
  import {
52
- diffStatSince,
53
- isGitRepo,
54
- restoreSnapshot,
55
- takeSnapshot
56
- } from "./chunk-YHRAOBI2.js";
64
+ clearState,
65
+ loadState,
66
+ saveState
67
+ } from "./chunk-O2S6PAJE.js";
57
68
 
58
69
  // src/cli.tsx
59
70
  import { Command } from "commander";
@@ -317,6 +328,39 @@ function registerProject(program2) {
317
328
  console.log(pc3.green(`\u2713 ${family.name} direction written to .squint/brief.md`));
318
329
  console.log(pc3.dim("every squint ask in this repo now holds this direction \u2014 edit the file to remix"));
319
330
  });
331
+ const sandboxCommand = program2.command("sandbox").description("Cumulative diff worktree \u2014 asks accumulate until you apply");
332
+ sandboxCommand.command("diff").description("Show accumulated sandbox changes").action(async () => {
333
+ const { sandboxDiffStat: sandboxDiffStat2, sandboxExists: sandboxExists2, sandboxFiles: sandboxFiles2 } = await import("./sandbox-J2NCPYHJ.js");
334
+ const cwd = process.cwd();
335
+ if (!sandboxExists2(cwd)) {
336
+ console.log(pc3.dim("no sandbox open \u2014 /sandbox on inside the TUI"));
337
+ return;
338
+ }
339
+ const stat = sandboxDiffStat2(cwd);
340
+ if (!stat) {
341
+ console.log(pc3.dim("sandbox is clean"));
342
+ return;
343
+ }
344
+ console.log(stat);
345
+ for (const line of sandboxFiles2(cwd)) console.log(pc3.dim(line));
346
+ });
347
+ sandboxCommand.command("apply").description("Land the sandbox diff on the real tree and close it").action(async () => {
348
+ const { applySandbox: applySandbox2, discardSandbox: discardSandbox2 } = await import("./sandbox-J2NCPYHJ.js");
349
+ const cwd = process.cwd();
350
+ const result = applySandbox2(cwd);
351
+ if (!result.ok) {
352
+ console.error(pc3.red(`\u2717 ${result.detail}`));
353
+ process.exitCode = 1;
354
+ return;
355
+ }
356
+ discardSandbox2(cwd);
357
+ console.log(pc3.green("\u2713 sandbox applied to the real tree") + pc3.dim(" \u2014 review with git diff"));
358
+ });
359
+ sandboxCommand.command("discard").description("Close the sandbox without touching the real tree").action(async () => {
360
+ const { discardSandbox: discardSandbox2 } = await import("./sandbox-J2NCPYHJ.js");
361
+ const had = discardSandbox2(process.cwd());
362
+ console.log(pc3.dim(had ? "sandbox discarded" : "no sandbox open"));
363
+ });
320
364
  const variantsCommand = program2.command("variants").description("Parallel design explorations \u2014 one aesthetic family each, pick with your eyes");
321
365
  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
366
  async (nRaw, promptWords, options) => {
@@ -415,7 +459,7 @@ function registerQuality(program2) {
415
459
  if (results.some((r) => !r.ok)) process.exitCode = 1;
416
460
  });
417
461
  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");
462
+ const { captureViewports: captureViewports2 } = await import("./preview-VW5DAJNV.js");
419
463
  const result = await captureViewports2(process.cwd(), url);
420
464
  if (!result) {
421
465
  console.error(pc4.red("\u2717 no Chrome/Chromium found"));
@@ -592,7 +636,8 @@ var Session = class {
592
636
  totals: { costUsd: 0, turns: 0 },
593
637
  queue: [],
594
638
  mode: "safe",
595
- problems: []
639
+ problems: [],
640
+ sandbox: false
596
641
  };
597
642
  if (opts.autoDev && detectDevCommand(opts.cwd)) {
598
643
  this.devServer().start();
@@ -762,15 +807,33 @@ var Session = class {
762
807
  this.push("assistant", text);
763
808
  }
764
809
  }
810
+ /** Where engines run and servers start: the sandbox worktree when on. */
811
+ execCwd() {
812
+ return this.state.sandbox ? sandboxDir(this.opts.cwd) : this.opts.cwd;
813
+ }
765
814
  devServer() {
766
815
  if (!this.dev) {
767
- this.dev = new DevServer(this.opts.cwd, {
816
+ this.dev = new DevServer(this.execCwd(), {
768
817
  onStateChange: (devState) => this.notify({ devState }),
769
818
  onUrl: (url) => this.notify({ devUrl: url })
770
819
  });
771
820
  }
772
821
  return this.dev;
773
822
  }
823
+ /** Rebind the dev server after the execution tree changes. */
824
+ resetDevServer() {
825
+ const wasRunning = this.dev && this.dev.state !== "stopped";
826
+ this.dev?.stop();
827
+ this.dev = null;
828
+ this.notify({ devUrl: null, devState: "stopped" });
829
+ if (wasRunning) {
830
+ const command = detectDevCommand(this.execCwd());
831
+ if (command) {
832
+ this.devServer().start(command);
833
+ this.push("status", `dev server restarting in ${this.state.sandbox ? "the sandbox" : "the real tree"}`);
834
+ }
835
+ }
836
+ }
774
837
  turnEdits = 0;
775
838
  turnTools = 0;
776
839
  toolStreak = 0;
@@ -849,7 +912,7 @@ var Session = class {
849
912
  engine,
850
913
  {
851
914
  prompt,
852
- cwd: this.opts.cwd,
915
+ cwd: this.execCwd(),
853
916
  model: this.state.model,
854
917
  mode: this.state.mode,
855
918
  sessionId: engine.supportsResume ? this.sessionId : void 0
@@ -892,9 +955,9 @@ var Session = class {
892
955
  }
893
956
  }
894
957
  if (result.ok && this.opts.autoCheck !== false) {
895
- const fastGates = detectFastGates(this.opts.cwd);
958
+ const fastGates = detectFastGates(this.execCwd());
896
959
  if (fastGates.length > 0) {
897
- const gateResults = await runGates(this.opts.cwd, fastGates);
960
+ const gateResults = await runGates(this.execCwd(), fastGates);
898
961
  const failures = gateResults.filter((r) => !r.ok);
899
962
  if (failures.length > 0) {
900
963
  this.addProblem(
@@ -969,7 +1032,7 @@ ${errors.slice(-5).join("\n")}`);
969
1032
  async submit(ask) {
970
1033
  this.fixAttempts = 0;
971
1034
  this.autoReviewedThisAsk = false;
972
- const snapshot = takeSnapshot(this.opts.cwd);
1035
+ const snapshot = this.state.sandbox ? null : takeSnapshot(this.opts.cwd);
973
1036
  if (snapshot) {
974
1037
  this.checkpoints.push({
975
1038
  snapshot,
@@ -1034,13 +1097,14 @@ ${errors.slice(-5).join("\n")}`);
1034
1097
  return pct;
1035
1098
  }
1036
1099
  /** Screenshot the running app (and watch its runtime where CDP is available). */
1037
- async capture() {
1038
- if (!this.state.devUrl) {
1039
- this.push("error", "dev server not running \u2014 /dev first");
1100
+ async capture(urlOverride) {
1101
+ const url = urlOverride || this.state.devUrl;
1102
+ if (!url) {
1103
+ this.push("error", "dev server not running \u2014 /dev first, or /shot <url>");
1040
1104
  return null;
1041
1105
  }
1042
1106
  this.push("status", "capturing screenshots\u2026");
1043
- const result = await captureViewports(this.opts.cwd, this.state.devUrl);
1107
+ const result = await captureViewports(this.opts.cwd, url);
1044
1108
  if (!result) {
1045
1109
  this.push("error", "no Chrome/Chromium found for screenshots");
1046
1110
  return null;
@@ -1259,7 +1323,7 @@ They are objective defects, not style preferences.`
1259
1323
  })();
1260
1324
  break;
1261
1325
  case "shot":
1262
- void this.capture();
1326
+ void this.capture(arg || void 0);
1263
1327
  break;
1264
1328
  case "review":
1265
1329
  void (async () => {
@@ -1320,6 +1384,57 @@ They are objective defects, not style preferences.`
1320
1384
  this.push("status", `resumed ${saved.engine} session${saved.lastAsk ? ` \xB7 "${saved.lastAsk}"` : ""}`);
1321
1385
  break;
1322
1386
  }
1387
+ case "sandbox": {
1388
+ if (this.state.running) {
1389
+ this.push("status", "wait for the current turn before changing sandbox state");
1390
+ break;
1391
+ }
1392
+ if (arg === "diff") {
1393
+ const stat = sandboxDiffStat(this.opts.cwd);
1394
+ if (!stat) {
1395
+ this.push("status", sandboxExists(this.opts.cwd) ? "sandbox is clean" : "no sandbox open \u2014 /sandbox on");
1396
+ } else {
1397
+ this.push("status", `${stat}
1398
+ ${sandboxFiles(this.opts.cwd).join("\n")}`);
1399
+ }
1400
+ break;
1401
+ }
1402
+ if (arg === "apply") {
1403
+ const applied = applySandbox(this.opts.cwd);
1404
+ if (applied.ok) {
1405
+ discardSandbox(this.opts.cwd);
1406
+ this.notify({ sandbox: false });
1407
+ this.resetDevServer();
1408
+ this.push("status", "sandbox applied to the real tree \u2014 review with git diff");
1409
+ } else {
1410
+ this.push("error", applied.detail ?? "apply failed");
1411
+ }
1412
+ break;
1413
+ }
1414
+ if (arg === "discard" || arg === "off") {
1415
+ const had = discardSandbox(this.opts.cwd);
1416
+ this.notify({ sandbox: false });
1417
+ this.resetDevServer();
1418
+ this.push("status", had ? "sandbox discarded \u2014 the real tree was never touched" : "no sandbox open");
1419
+ break;
1420
+ }
1421
+ if (arg === "on" || arg === "") {
1422
+ if (!isGitRepo(this.opts.cwd)) {
1423
+ this.push("error", "sandbox needs a git repo with at least one commit");
1424
+ break;
1425
+ }
1426
+ const { reused } = openSandbox(this.opts.cwd);
1427
+ this.notify({ sandbox: true });
1428
+ this.resetDevServer();
1429
+ this.push(
1430
+ "status",
1431
+ `${reused ? "rejoined the open sandbox" : "sandbox opened"} \u2014 asks now accumulate in a shadow worktree; /sandbox diff \xB7 apply \xB7 discard`
1432
+ );
1433
+ break;
1434
+ }
1435
+ this.push("status", "usage: /sandbox [on] \xB7 diff \xB7 apply \xB7 discard");
1436
+ break;
1437
+ }
1323
1438
  case "variants": {
1324
1439
  const [sub, ...subRest] = arg.split(/\s+/);
1325
1440
  if (sub === "apply") {
@@ -1395,7 +1510,7 @@ They are objective defects, not style preferences.`
1395
1510
  this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
1396
1511
  break;
1397
1512
  case "help": {
1398
- void import("./commands-TYAKF276.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1513
+ void import("./commands-TERCCWFM.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1399
1514
  break;
1400
1515
  }
1401
1516
  case "quit":
@@ -1925,6 +2040,7 @@ function App({
1925
2040
  ]
1926
2041
  }
1927
2042
  ),
2043
+ state.sandbox && /* @__PURE__ */ jsx3(Text3, { color: theme2.accent, bold: true, children: " [sandbox]" }),
1928
2044
  " ",
1929
2045
  state.engineId,
1930
2046
  state.model ? ` \xB7 ${state.model}` : "",
@@ -1979,7 +2095,7 @@ function registerTui(program2) {
1979
2095
  }
1980
2096
 
1981
2097
  // src/cli.tsx
1982
- var VERSION = "0.2.7";
2098
+ var VERSION = true ? "0.2.9" : "0.0.0-dev";
1983
2099
  var program = new Command();
1984
2100
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
1985
2101
  registerRun(program);
@@ -3,7 +3,7 @@ import {
3
3
  COMMANDS,
4
4
  commandHelp,
5
5
  completeCommand
6
- } from "./chunk-RZOQPFXC.js";
6
+ } from "./chunk-RMNG2NTU.js";
7
7
  export {
8
8
  COMMANDS,
9
9
  commandHelp,
@@ -10,10 +10,11 @@ import {
10
10
  probeRuntime,
11
11
  routeShotName,
12
12
  runtimeSummary
13
- } from "./chunk-CHZE3UHK.js";
13
+ } from "./chunk-GOAFH2VN.js";
14
14
  import "./chunk-B7LOERSP.js";
15
15
  import "./chunk-BWZFACBT.js";
16
16
  import "./chunk-OC6RU6XH.js";
17
+ import "./chunk-O2S6PAJE.js";
17
18
  export {
18
19
  VIEWPORTS,
19
20
  buildReviewPrompt,
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ applySandbox,
4
+ discardSandbox,
5
+ openSandbox,
6
+ sandboxDiffStat,
7
+ sandboxDir,
8
+ sandboxExists,
9
+ sandboxFiles
10
+ } from "./chunk-XEQ6JXXL.js";
11
+ export {
12
+ applySandbox,
13
+ discardSandbox,
14
+ openSandbox,
15
+ sandboxDiffStat,
16
+ sandboxDir,
17
+ sandboxExists,
18
+ sandboxFiles
19
+ };
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ clearState,
4
+ ensureSquintIgnore,
5
+ loadState,
6
+ saveState
7
+ } from "./chunk-O2S6PAJE.js";
8
+ export {
9
+ clearState,
10
+ ensureSquintIgnore,
11
+ loadState,
12
+ saveState
13
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aayambansal/squint",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
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",
@@ -1,40 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/session/commands.ts
4
- var COMMANDS = [
5
- { name: "dev", args: "[restart|logs]", description: "start/stop the dev server; restart or show recent output" },
6
- { name: "check", description: "run all quality gates (typecheck, lint, test, build)" },
7
- { name: "problems", description: "list open findings from gates, dev server, runtime, a11y" },
8
- { name: "fix", args: "[n]", description: "send all open problems to the engine, or just problem n" },
9
- { name: "shot", description: "screenshot the app at mobile/tablet/desktop" },
10
- { name: "review", args: "[focus]", description: "screenshots + the engine critiques its own rendered work" },
11
- { name: "variants", args: "<2-4> <ask>", description: "parallel design explorations; apply/list/clean" },
12
- { name: "undo", description: "revert the last ask (files only)" },
13
- { name: "checkpoints", description: "list per-ask checkpoints" },
14
- { name: "restore", args: "<n>", description: "rewind files to before ask n" },
15
- { name: "mode", args: "plan|safe|yolo", description: "how much the engine may do (shift+tab cycles)" },
16
- { name: "engine", args: "<id>", description: "switch backend (new session)" },
17
- { name: "engines", description: "list installed engines with streaming/resume support" },
18
- { name: "model", args: "[name]", description: "model override for the engine" },
19
- { name: "theme", args: "[name]", description: "switch the TUI theme", viewLevel: true },
20
- { name: "copy", description: "copy the last reply to the clipboard" },
21
- { name: "save", description: "export the transcript to .squint/transcripts/" },
22
- { name: "queue", args: "clear", description: "drop queued asks" },
23
- { name: "resume", description: "pick up the previous session for this repo" },
24
- { name: "clear", description: "new session (transcript, totals, persisted state)" },
25
- { name: "help", description: "list commands" },
26
- { name: "quit", description: "exit with a session summary" }
27
- ];
28
- function completeCommand(partial) {
29
- const query = partial.toLowerCase();
30
- return COMMANDS.filter((c) => c.name.startsWith(query));
31
- }
32
- function commandHelp() {
33
- return COMMANDS.map((c) => `/${c.name}${c.args ? ` ${c.args}` : ""} \u2014 ${c.description}`).join("\n");
34
- }
35
-
36
- export {
37
- COMMANDS,
38
- completeCommand,
39
- commandHelp
40
- };