@aayambansal/squint 0.2.0 → 0.2.1

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
@@ -86,10 +86,11 @@ From source: `git clone https://github.com/aayambansal/squint.git && cd squint &
86
86
  and catches what the server never prints: blank pages, exceptions, failed requests.
87
87
  5. **The agent looks at its work.** `/review` screenshots mobile/tablet/desktop and
88
88
  re-prompts the engine to critique what it can see — then fix it.
89
- 6. **Gates keep it honest.** `/check` runs typecheck, lint, test, build failures come
90
- back with orders not to weaken the checks.
91
- 7. **Everything is reversible.** Each ask is snapshotted via git plumbing; `/undo` reverts
92
- the whole turn while your own uncommitted work survives.
89
+ 6. **Gates keep it honest.** Typecheck + lint run automatically after *every* turn and
90
+ auto-fix (capped); `/check` adds tests and the build — failures come back with orders
91
+ not to weaken the checks.
92
+ 7. **Everything is reversible.** Every ask records a checkpoint; `/undo` pops the last,
93
+ `/restore <n>` rewinds files to any earlier point — your own uncommitted work survives.
93
94
  8. **Point at things.** Alt+S in the browser, click any element, and a self-locating
94
95
  reference lands on your clipboard to paste into squint.
95
96
  9. **Explore in parallel.** `squint variants gen 3 "<ask>"` builds the same ask three ways —
@@ -129,11 +130,24 @@ squint config set engine claude
129
130
  squint config set models.claude claude-sonnet-5
130
131
  squint config set autoDev true # dev server starts with the TUI
131
132
  squint config set autoFix true # errors auto-route back (max 2 tries)
133
+ squint config set autoCheck false # skip the per-turn typecheck+lint pass
134
+ squint config set theme ocean # amber · ocean · moss · rose · mono
135
+ squint config set bell false # no bell on turn completion
132
136
  squint doctor # engines + Chrome + WebSocket check
133
137
  ```
134
138
 
135
- Inside the TUI: `/dev` `/check` `/fix` `/shot` `/review [focus]` `/undo` `/resume`
136
- `/engine <id>` `/model <name>` `/clear` — `Esc` interrupts, arrows recall history.
139
+ **Inside the TUI:**
140
+
141
+ - **Modes**: `shift+tab` cycles `safe` (edits auto-approved) → `plan` (read-only
142
+ investigation) → `yolo` (no friction), mapped to each engine's native permission flags.
143
+ - **Type ahead**: keep typing while the agent works — Enter queues asks that dispatch in
144
+ order; `/queue clear` drops them. `Esc` interrupts the current turn.
145
+ - **Editing**: a real line editor — arrows move, `alt+←/→` jump words, `ctrl+a/e/k/u/w`,
146
+ `↑/↓` history. `ctrl+c` twice exits with a session summary.
147
+ - **Commands**: `/dev` `/check` `/fix` `/shot` `/review [focus]` `/undo` `/checkpoints`
148
+ `/restore <n>` `/mode` `/theme` `/resume` `/engine <id>` `/model <name>` `/clear`.
149
+ - Assistant output renders as markdown; the footer tracks session turns and cost; a bell
150
+ rings when a turn finishes.
137
151
 
138
152
  ## Design directions
139
153
 
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  findChrome,
4
4
  screenshot
5
- } from "./chunk-Z46MXU64.js";
6
- import "./chunk-MCYQXOE7.js";
5
+ } from "./chunk-P3V5CWH5.js";
6
+ import "./chunk-2LRIKWBU.js";
7
7
  export {
8
8
  findChrome,
9
9
  screenshot
@@ -1,7 +1,17 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ variantsRoot
4
+ } from "./chunk-LCS47EAG.js";
5
+ import {
6
+ findChrome,
7
+ screenshot
8
+ } from "./chunk-P3V5CWH5.js";
2
9
  import {
3
10
  lineSplitter
4
- } from "./chunk-MCYQXOE7.js";
11
+ } from "./chunk-2LRIKWBU.js";
12
+
13
+ // src/variants/shots.ts
14
+ import path2 from "path";
5
15
 
6
16
  // src/devserver/devserver.ts
7
17
  import { spawn } from "child_process";
@@ -149,8 +159,55 @@ ${errorBlock}
149
159
  ${context}`;
150
160
  }
151
161
 
162
+ // src/variants/shots.ts
163
+ var URL_TIMEOUT_MS = 45e3;
164
+ function waitForUrl(server) {
165
+ return new Promise((resolve) => {
166
+ const startedAt = Date.now();
167
+ const poll = setInterval(() => {
168
+ if (server.url) {
169
+ clearInterval(poll);
170
+ resolve(server.url);
171
+ } else if (Date.now() - startedAt > URL_TIMEOUT_MS || server.state === "crashed") {
172
+ clearInterval(poll);
173
+ resolve(null);
174
+ }
175
+ }, 200);
176
+ });
177
+ }
178
+ async function screenshotVariants(cwd, variants) {
179
+ const chrome = findChrome();
180
+ if (!chrome) {
181
+ return variants.map((v) => ({ familyId: v.family.id, error: "no Chrome found" }));
182
+ }
183
+ const shots = [];
184
+ for (const variant of variants) {
185
+ const command = detectDevCommand(variant.dir);
186
+ if (!command) {
187
+ shots.push({ familyId: variant.family.id, error: "no dev script" });
188
+ continue;
189
+ }
190
+ const server = new DevServer(variant.dir);
191
+ server.start(command);
192
+ const url = await waitForUrl(server);
193
+ if (!url) {
194
+ server.stop();
195
+ shots.push({ familyId: variant.family.id, error: "dev server did not announce a URL" });
196
+ continue;
197
+ }
198
+ const outPath = path2.join(variantsRoot(cwd), `${variant.family.id}.png`);
199
+ const result = await screenshot(chrome, url, outPath, { width: 1440, height: 900 });
200
+ server.stop();
201
+ shots.push(
202
+ result.ok ? { familyId: variant.family.id, path: outPath } : { familyId: variant.family.id, error: result.error }
203
+ );
204
+ }
205
+ return shots;
206
+ }
207
+
152
208
  export {
153
209
  detectDevCommand,
154
210
  DevServer,
155
- buildFixPrompt
211
+ buildFixPrompt,
212
+ screenshotVariants
156
213
  };
@@ -1,14 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  FAMILIES,
4
+ FIRST_TURN_ADDENDUM,
4
5
  renderFamilyBrief
5
- } from "./chunk-5IR2AZVK.js";
6
+ } from "./chunk-P3H4N2EN.js";
6
7
  import {
7
8
  runAgent
8
- } from "./chunk-FPTBBHOH.js";
9
- import {
10
- FIRST_TURN_ADDENDUM
11
- } from "./chunk-NT2HR4RD.js";
9
+ } from "./chunk-OTG4ZB4R.js";
12
10
 
13
11
  // src/variants/variants.ts
14
12
  import { execFileSync } from "child_process";
@@ -3,7 +3,7 @@ import {
3
3
  findEngineBinary,
4
4
  lineSplitter,
5
5
  truncate
6
- } from "./chunk-MCYQXOE7.js";
6
+ } from "./chunk-2LRIKWBU.js";
7
7
 
8
8
  // src/runner/run.ts
9
9
  import { spawn } from "child_process";
@@ -1,7 +1,60 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- DEFAULT_BRIEF
4
- } from "./chunk-NT2HR4RD.js";
2
+
3
+ // src/prompt/brief.ts
4
+ import fs from "fs";
5
+ import path from "path";
6
+ var DEFAULT_BRIEF = `You are acting as a senior product designer and frontend engineer. Treat every change as production work, not a demo.
7
+
8
+ Direction before code:
9
+ - Commit to one specific visual direction derived from what this product is for, and state it in one sentence before implementing. If the direction could be guessed from the product category alone, sharpen it until it couldn't.
10
+ - Choose a color strategy deliberately: restrained (neutrals + one accent), committed (one dominant color owning 30\u201360% of the surface), or a small palette of 3\u20135 named roles. Never purple/violet gradients unless asked. Body text contrast stays at least 4.5:1 \u2014 no light gray body copy "for elegance".
11
+ - Typography: at most two families, paired on contrast (display serif + geometric sans, or sans + mono). Never Inter/Roboto/Open Sans/system defaults for display type. Pair weight extremes (300 against 700\u2013900); display sizes jump 3x over body, not 1.5x.
12
+
13
+ Tokens are the system:
14
+ - Define or extend design tokens (CSS variables / theme config) first, then compose the UI from them. Never scatter literal colors or one-off spacing values through components.
15
+ - One spacing rhythm on a 4/8px grid; one type scale with a ratio of at least 1.25 between steps.
16
+
17
+ Banned tells \u2014 these read instantly as machine-generated:
18
+ - Purple gradient on white; glassmorphism cards; cream/beige page background as a reflex "warmth" move
19
+ - Emoji as icons; identical icon-topped card grids; stat banner rows; numbered 01/02/03 section markers
20
+ - Tiny all-caps tracked eyebrow labels over every section; gradient text; colored left-border strips on cards
21
+ - Centered hero + badge + three feature cards; unmodified component-library defaults
22
+ If someone could look at the result and say "AI made that" without doubt, it has failed.
23
+
24
+ Craft details:
25
+ - Every interactive element gets hover, focus-visible, and active treatment; keyboard focus stays visible.
26
+ - Anything that loads data gets loading, empty, and error states.
27
+ - Motion: 150\u2013250ms ease-out; one orchestrated entrance with staggered reveals beats scattered micro-interactions; honor prefers-reduced-motion; never leave content invisible until a scroll observer fires.
28
+ - Body line length 65\u201375ch; text-wrap: balance on headings.
29
+
30
+ Engineering:
31
+ - Follow the repo's existing conventions and extend its patterns; new components in new files, small and focused; semantic HTML.
32
+ - Responsive from 360px up with no horizontal overflow \u2014 check intermediate widths, not just phone and desktop.
33
+ - Let errors surface instead of swallowing them in try/catch; log clearly so failures can be traced.
34
+ - Not done until the app builds cleanly, typechecks, and renders without console errors.`;
35
+ var FIRST_TURN_ADDENDUM = `This is the opening move on this task: establish the design foundation before building. State the visual direction, set up the tokens/theme first, then build components from them. The first render should feel like a designed product, not a scaffold \u2014 impressive on sight.`;
36
+ function loadBrief(cwd) {
37
+ const custom = path.join(cwd, ".squint", "brief.md");
38
+ try {
39
+ const text = fs.readFileSync(custom, "utf8").trim();
40
+ if (text.length > 0) return text;
41
+ } catch {
42
+ }
43
+ return DEFAULT_BRIEF;
44
+ }
45
+ function composePrompt(ask, opts) {
46
+ if (opts.noBrief) return ask;
47
+ const brief = loadBrief(opts.cwd);
48
+ const firstTurn = opts.firstTurn ?? true;
49
+ const addendum = firstTurn ? `
50
+
51
+ ${FIRST_TURN_ADDENDUM}` : "";
52
+ return `${brief}${addendum}
53
+
54
+ ## Task
55
+
56
+ ${ask}`;
57
+ }
5
58
 
6
59
  // src/prompt/families.ts
7
60
  var FAMILIES = [
@@ -121,6 +174,8 @@ ${coreStandards()}`;
121
174
  }
122
175
 
123
176
  export {
177
+ FIRST_TURN_ADDENDUM,
178
+ composePrompt,
124
179
  FAMILIES,
125
180
  getFamily,
126
181
  renderFamilyBrief
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  findBinary
4
- } from "./chunk-MCYQXOE7.js";
4
+ } from "./chunk-2LRIKWBU.js";
5
5
 
6
6
  // src/preview/chrome.ts
7
7
  import { spawn } from "child_process";
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  findChrome,
8
8
  screenshot
9
- } from "./chunk-Z46MXU64.js";
9
+ } from "./chunk-P3V5CWH5.js";
10
10
 
11
11
  // src/preview/preview.ts
12
12
  import fs2 from "fs";
@@ -79,10 +79,11 @@ const PICKER = \`(() => {
79
79
  addEventListener('keydown', (e) => {
80
80
  if (e.altKey && (e.key === 's' || e.key === 'S' || e.code === 'KeyS')) {
81
81
  active = !active;
82
- if (!active) { box.style.display = 'none'; tip.style.display = 'none'; }
83
- else toast('squint picker on \u2014 click an element (esc to cancel)');
82
+ if (!active) { window.__squintFinishNote?.(true); window.__squintCompile?.(); box.style.display = 'none'; tip.style.display = 'none'; }
83
+ else toast('squint picker on \u2014 click elements to pin, alt+enter copies all (esc cancels)');
84
84
  } else if (e.key === 'Escape' && active) {
85
- active = false; box.style.display = 'none'; tip.style.display = 'none';
85
+ active = false; window.__squintFinishNote?.(false); window.__squintClearPins?.();
86
+ box.style.display = 'none'; tip.style.display = 'none';
86
87
  }
87
88
  });
88
89
  addEventListener('mousemove', (e) => {
@@ -97,16 +98,65 @@ const PICKER = \`(() => {
97
98
  tip.style.top = Math.max(r.top - 26, 2) + 'px';
98
99
  tip.textContent = '<' + s.tag + '> ' + rel(s.file);
99
100
  }, true);
101
+ // Multi-pin annotations: each click drops a numbered pin with a note;
102
+ // alt+enter (or toggling the picker off) compiles them into one blob.
103
+ const pins = [];
104
+ const badges = [];
105
+ const noteBox = document.createElement('input');
106
+ noteBox.style.cssText = 'position:fixed;z-index:2147483647;background:#1b1b1b;color:#fff;border:2px solid #e8a33d;border-radius:3px;padding:4px 8px;font:12px ui-monospace,monospace;display:none;width:280px;outline:none;';
107
+ noteBox.placeholder = 'note for this pin \u2014 enter to keep, esc to drop';
108
+ document.documentElement.append(noteBox);
109
+ let pending = null;
110
+ const addBadge = (el, n) => {
111
+ const r = el.getBoundingClientRect();
112
+ const b = document.createElement('div');
113
+ b.textContent = String(n);
114
+ b.style.cssText = 'position:fixed;z-index:2147483647;background:#e8a33d;color:#1b1b1b;font:bold 11px ui-monospace,monospace;border-radius:8px;min-width:16px;height:16px;text-align:center;line-height:16px;pointer-events:none;';
115
+ b.style.left = Math.max(r.left - 6, 2) + 'px';
116
+ b.style.top = Math.max(r.top - 6, 2) + 'px';
117
+ document.documentElement.append(b);
118
+ badges.push(b);
119
+ };
120
+ const clearPins = () => { pins.length = 0; badges.forEach((b) => b.remove()); badges.length = 0; };
121
+ const compile = () => {
122
+ if (pins.length === 0) return;
123
+ const blob = pins.length === 1 && !pins[0].note
124
+ ? pins[0].ref
125
+ : pins.map((p, i) => (i + 1) + '. ' + p.ref + (p.note ? ' \u2014 ' + p.note : '')).join('\\\\n');
126
+ window.__squintLastPick = blob;
127
+ const count = pins.length;
128
+ clearPins();
129
+ const done = () => toast('copied ' + count + ' annotation' + (count === 1 ? '' : 's') + ' \u2014 paste into squint');
130
+ if (navigator.clipboard?.writeText) navigator.clipboard.writeText(blob).then(done, done);
131
+ else done();
132
+ };
133
+ const finishNote = (keep) => {
134
+ if (!pending) return;
135
+ if (keep) { pins.push({ ref: pending.ref, note: noteBox.value.trim() }); addBadge(pending.el, pins.length); }
136
+ pending = null; noteBox.value = ''; noteBox.style.display = 'none';
137
+ };
138
+ noteBox.addEventListener('keydown', (e) => {
139
+ e.stopPropagation();
140
+ if (e.key === 'Enter') { finishNote(true); if (e.altKey) { compile(); active = false; box.style.display = 'none'; } }
141
+ else if (e.key === 'Escape') finishNote(false);
142
+ });
143
+ window.__squintFinishNote = finishNote;
144
+ window.__squintCompile = compile;
145
+ window.__squintClearPins = clearPins;
146
+ addEventListener('keydown', (e) => {
147
+ if (active && e.altKey && e.key === 'Enter') { finishNote(true); compile(); active = false; box.style.display = 'none'; tip.style.display = 'none'; }
148
+ });
100
149
  addEventListener('click', (e) => {
101
- if (!active) return;
150
+ if (!active || e.target === noteBox) return;
102
151
  e.preventDefault(); e.stopPropagation();
152
+ finishNote(true);
103
153
  const el = find(e.target); if (!el) return;
104
- const ref = describe(el);
105
- window.__squintLastPick = ref;
106
- const done = () => toast('copied \u2014 paste into squint: ' + ref);
107
- if (navigator.clipboard?.writeText) navigator.clipboard.writeText(ref).then(done, done);
108
- else done();
109
- active = false; box.style.display = 'none';
154
+ pending = { el, ref: describe(el) };
155
+ const r = el.getBoundingClientRect();
156
+ noteBox.style.display = 'block';
157
+ noteBox.style.left = Math.min(r.left, innerWidth - 310) + 'px';
158
+ noteBox.style.top = Math.min(r.bottom + 4, innerHeight - 34) + 'px';
159
+ noteBox.focus();
110
160
  }, true);
111
161
  })();\`
112
162
  `;
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/prompt/skills.ts
4
+ import fs from "fs";
5
+ import path from "path";
6
+ var FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?/;
7
+ function parseSkill(name, raw) {
8
+ const match = FRONTMATTER_RE.exec(raw);
9
+ if (!match) return null;
10
+ const body = raw.slice(match[0].length).trim();
11
+ const triggersLine = /(^|\n)triggers:[ \t]*(.*)/.exec(match[1]);
12
+ if (!triggersLine) return null;
13
+ let triggers;
14
+ const inline = triggersLine[2].trim();
15
+ if (inline.length > 0) {
16
+ triggers = inline.split(",").map((t) => t.trim().toLowerCase());
17
+ } else {
18
+ triggers = [...match[1].matchAll(/\n\s*-\s+(.+)/g)].map((m) => m[1].trim().toLowerCase());
19
+ }
20
+ triggers = triggers.filter((t) => t.length > 0);
21
+ if (triggers.length === 0 || body.length === 0) return null;
22
+ return { name, triggers, body };
23
+ }
24
+ function loadSkills(cwd) {
25
+ const dir = path.join(cwd, ".squint", "skills");
26
+ let entries;
27
+ try {
28
+ entries = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
29
+ } catch {
30
+ return [];
31
+ }
32
+ const skills = [];
33
+ for (const entry of entries.sort()) {
34
+ try {
35
+ const skill = parseSkill(entry.replace(/\.md$/, ""), fs.readFileSync(path.join(dir, entry), "utf8"));
36
+ if (skill) skills.push(skill);
37
+ } catch {
38
+ }
39
+ }
40
+ return skills;
41
+ }
42
+ function loadRules(cwd) {
43
+ try {
44
+ const text = fs.readFileSync(path.join(cwd, ".squint", "rules.md"), "utf8").trim();
45
+ return text.length > 0 ? text : null;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+ function matchSkills(skills, ask) {
51
+ const haystack = ask.toLowerCase();
52
+ return skills.filter((skill) => skill.triggers.some((t) => haystack.includes(t)));
53
+ }
54
+ function enrich(cwd, ask) {
55
+ const parts = [];
56
+ const rules = loadRules(cwd);
57
+ if (rules) parts.push(`## Project rules (always apply)
58
+
59
+ ${rules}`);
60
+ const matched = matchSkills(loadSkills(cwd), ask);
61
+ for (const skill of matched) {
62
+ parts.push(`## Project notes: ${skill.name}
63
+
64
+ ${skill.body}`);
65
+ }
66
+ return {
67
+ sections: parts.length > 0 ? `
68
+
69
+ ${parts.join("\n\n")}` : "",
70
+ matchedSkills: matched.map((s) => s.name)
71
+ };
72
+ }
73
+
74
+ export {
75
+ parseSkill,
76
+ loadSkills,
77
+ loadRules,
78
+ matchSkills,
79
+ enrich
80
+ };
package/dist/cli.js CHANGED
@@ -2,14 +2,21 @@
2
2
  import {
3
3
  DevServer,
4
4
  buildFixPrompt,
5
- detectDevCommand
6
- } from "./chunk-ZJOU46X4.js";
5
+ detectDevCommand,
6
+ screenshotVariants
7
+ } from "./chunk-2IW2MMCY.js";
7
8
  import {
8
- runAgent
9
- } from "./chunk-FPTBBHOH.js";
9
+ applyVariant,
10
+ cleanVariants,
11
+ listVariants,
12
+ runVariants
13
+ } from "./chunk-LCS47EAG.js";
10
14
  import {
11
15
  composePrompt
12
- } from "./chunk-NT2HR4RD.js";
16
+ } from "./chunk-P3H4N2EN.js";
17
+ import {
18
+ runAgent
19
+ } from "./chunk-OTG4ZB4R.js";
13
20
  import {
14
21
  buildGatePrompt,
15
22
  detectFastGates,
@@ -25,14 +32,20 @@ import {
25
32
  probeRuntime,
26
33
  runtimeSummary,
27
34
  saveState
28
- } from "./chunk-4MMIJXYD.js";
35
+ } from "./chunk-REDPR3KI.js";
29
36
  import "./chunk-VJGE7HSP.js";
30
- import "./chunk-Z46MXU64.js";
37
+ import {
38
+ findChrome
39
+ } from "./chunk-P3V5CWH5.js";
31
40
  import {
32
41
  detectEngines,
33
42
  getEngine
34
- } from "./chunk-MCYQXOE7.js";
43
+ } from "./chunk-2LRIKWBU.js";
35
44
  import {
45
+ enrich
46
+ } from "./chunk-XZKQZKEE.js";
47
+ import {
48
+ isGitRepo,
36
49
  restoreSnapshot,
37
50
  takeSnapshot
38
51
  } from "./chunk-WTA6YYBY.js";
@@ -457,7 +470,12 @@ ${errors.slice(-5).join("\n")}`);
457
470
  if (this.checkpoints.length > 20) this.checkpoints.shift();
458
471
  }
459
472
  const isFirstTurn = this.sessionId === void 0;
460
- const prompt = isFirstTurn ? composePrompt(ask, { cwd: this.opts.cwd, firstTurn: true }) : ask;
473
+ let prompt = isFirstTurn ? composePrompt(ask, { cwd: this.opts.cwd, firstTurn: true }) : ask;
474
+ const enrichment = enrich(this.opts.cwd, ask);
475
+ if (enrichment.matchedSkills.length > 0) {
476
+ this.push("status", `skills: ${enrichment.matchedSkills.join(", ")}`);
477
+ }
478
+ prompt += enrichment.sections;
461
479
  await this.runTurn(prompt, ask);
462
480
  }
463
481
  /** Restore files to the state before checkpoint `index`; drop it and everything after. */
@@ -659,6 +677,75 @@ ${result.a11y.slice(0, 5).join("\n")}`);
659
677
  this.push("status", `resumed ${saved.engine} session${saved.lastAsk ? ` \xB7 "${saved.lastAsk}"` : ""}`);
660
678
  break;
661
679
  }
680
+ case "variants": {
681
+ const [sub, ...subRest] = arg.split(/\s+/);
682
+ if (sub === "apply") {
683
+ const id = subRest[0];
684
+ if (!id) {
685
+ this.push("status", "usage: /variants apply <id>");
686
+ break;
687
+ }
688
+ const applied = applyVariant(this.opts.cwd, id);
689
+ if (applied.ok) {
690
+ cleanVariants(this.opts.cwd);
691
+ this.push("status", `applied ${id} to the working tree \u2014 review with git diff`);
692
+ } else {
693
+ this.push("error", applied.detail ?? "apply failed");
694
+ }
695
+ break;
696
+ }
697
+ if (sub === "clean") {
698
+ this.push("status", `removed ${cleanVariants(this.opts.cwd)} variant(s)`);
699
+ break;
700
+ }
701
+ if (sub === "list") {
702
+ const ids = listVariants(this.opts.cwd);
703
+ this.push("status", ids.length > 0 ? ids.join(" \xB7 ") : "no variants \u2014 /variants <2-4> <ask>");
704
+ break;
705
+ }
706
+ const n = Number.parseInt(sub ?? "", 10);
707
+ const ask = subRest.join(" ").trim();
708
+ if (!Number.isInteger(n) || n < 2 || n > 4 || ask.length === 0) {
709
+ this.push("status", "usage: /variants <2-4> <ask> \xB7 /variants apply <id> \xB7 list \xB7 clean");
710
+ break;
711
+ }
712
+ if (!isGitRepo(this.opts.cwd)) {
713
+ this.push("error", "variants need a git repo with at least one commit");
714
+ break;
715
+ }
716
+ void (async () => {
717
+ cleanVariants(this.opts.cwd);
718
+ this.push("status", `generating ${n} directions in parallel \u2014 this runs ${n} engine sessions`);
719
+ this.notify({ running: true, runStartedAt: Date.now() });
720
+ const engine = getEngine(this.state.engineId);
721
+ const runs = await runVariants(
722
+ this.opts.cwd,
723
+ ask,
724
+ n,
725
+ engine,
726
+ this.state.model,
727
+ (familyId, text) => this.push("status", `[${familyId}] ${text}`)
728
+ );
729
+ const succeeded = runs.filter((r) => r.result.ok);
730
+ if (succeeded.length > 0 && findChrome() && detectDevCommand(this.opts.cwd)) {
731
+ this.push("status", "capturing one screenshot per variant\u2026");
732
+ const shots = await screenshotVariants(this.opts.cwd, succeeded.map((r) => r.variant));
733
+ for (const shot of shots) {
734
+ this.push(
735
+ shot.path ? "status" : "error",
736
+ `[${shot.familyId}] ${shot.path ?? shot.error ?? "screenshot failed"}`
737
+ );
738
+ }
739
+ }
740
+ this.notify({ running: false });
741
+ this.push(
742
+ succeeded.length > 0 ? "status" : "error",
743
+ `${succeeded.length}/${runs.length} variants ready \u2014 /variants apply <id> keeps one, /variants clean discards`
744
+ );
745
+ this.drainQueue();
746
+ })();
747
+ break;
748
+ }
662
749
  case "clear":
663
750
  this.sessionId = void 0;
664
751
  clearState(this.opts.cwd);
@@ -667,7 +754,7 @@ ${result.a11y.slice(0, 5).join("\n")}`);
667
754
  case "help":
668
755
  this.push(
669
756
  "status",
670
- "/engine <id> \xB7 /model <name> \xB7 /dev (start/stop server) \xB7 /check (quality gates) \xB7 /fix (send failures) \xB7 /shot (screenshots) \xB7 /review [focus] (visual self-critique) \xB7 /undo (revert last ask) \xB7 /checkpoints \xB7 /restore <n> \xB7 /resume (last session) \xB7 /clear (new session) \xB7 /quit"
757
+ "/engine <id> \xB7 /model <name> \xB7 /mode plan|safe|yolo \xB7 /dev \xB7 /check (gates) \xB7 /fix \xB7 /shot \xB7 /review [focus] \xB7 /variants <2-4> <ask> \xB7 /undo \xB7 /checkpoints \xB7 /restore <n> \xB7 /resume \xB7 /clear \xB7 /quit"
671
758
  );
672
759
  break;
673
760
  case "quit":
@@ -1176,7 +1263,7 @@ function App({
1176
1263
 
1177
1264
  // src/cli.tsx
1178
1265
  import { jsx as jsx4 } from "react/jsx-runtime";
1179
- var VERSION = "0.2.0";
1266
+ var VERSION = "0.2.1";
1180
1267
  var program = new Command();
1181
1268
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
1182
1269
  program.command("run").description("Run one prompt headlessly and stream the result").argument("<prompt...>", "what to build or change").option("-e, --engine <id>", "engine to use (claude, codex, gemini, opencode)").option("-m, --model <name>", "model override for the engine").option("--no-brief", "send the prompt without the squint design brief").option("--json", "emit normalized agent events as ndjson").action(
@@ -1210,18 +1297,41 @@ program.command("engines").description("List engines and whether they are instal
1210
1297
  console.log(`${status} ${engine.id.padEnd(10)} ${engine.name.padEnd(14)} ${location}`);
1211
1298
  }
1212
1299
  });
1213
- program.command("doctor").description("Check squint prerequisites and engine availability").action(async () => {
1300
+ 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) => {
1214
1301
  const [major] = process.versions.node.split(".");
1215
- const nodeOk = Number(major) >= 20;
1216
- console.log(`${nodeOk ? pc.green("\u2713") : pc.red("\u2717")} node ${process.versions.node}${nodeOk ? "" : " (need >= 20)"}`);
1302
+ const nodeOk = Number(major) >= 22;
1303
+ console.log(`${nodeOk ? pc.green("\u2713") : pc.red("\u2717")} node ${process.versions.node}${nodeOk ? "" : " (need >= 22)"}`);
1217
1304
  const detected = detectEngines();
1218
1305
  for (const { engine, path: binaryPath } of detected) {
1219
1306
  const status = binaryPath ? pc.green("\u2713") : pc.yellow("\u25CB");
1220
1307
  console.log(`${status} ${engine.name}${binaryPath ? "" : pc.dim(` \u2014 install: ${engine.install}`)}`);
1221
1308
  }
1222
- const { findChrome } = await import("./chrome-K4BKFOYS.js");
1309
+ if (options.probe) {
1310
+ const { runAgent: runAgent2 } = await import("./run-XHLCTF6V.js");
1311
+ console.log(pc.dim("\nprobing engines with a one-word prompt (verifies auth end to end)\u2026"));
1312
+ for (const { engine, path: binaryPath } of detected) {
1313
+ if (!binaryPath) continue;
1314
+ const startedAt = Date.now();
1315
+ const abort = new AbortController();
1316
+ const timer = setTimeout(() => abort.abort(), 9e4);
1317
+ const result = await runAgent2(
1318
+ engine,
1319
+ { prompt: "Reply with exactly: ok", cwd: process.cwd() },
1320
+ () => {
1321
+ },
1322
+ abort.signal
1323
+ );
1324
+ clearTimeout(timer);
1325
+ const secs = ((Date.now() - startedAt) / 1e3).toFixed(1);
1326
+ const detail = (result.error ?? "failed").split("\n").filter((l) => l.trim()).at(-1) ?? "failed";
1327
+ console.log(
1328
+ 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))}`
1329
+ );
1330
+ }
1331
+ }
1332
+ const { findChrome: findChrome2 } = await import("./chrome-RD7XQ767.js");
1223
1333
  const { hasWebSocket } = await import("./cdp-RTWPFIX5.js");
1224
- const chrome = findChrome();
1334
+ const chrome = findChrome2();
1225
1335
  console.log(
1226
1336
  chrome ? `${pc.green("\u2713")} Chrome ${pc.dim(chrome)}` : `${pc.yellow("\u25CB")} Chrome ${pc.dim("\u2014 screenshots and runtime probing disabled")}`
1227
1337
  );
@@ -1238,7 +1348,7 @@ ${available.length} engine(s) ready.`));
1238
1348
  }
1239
1349
  });
1240
1350
  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) => {
1241
- const { installDependencies, writeTemplate } = await import("./init-7AYGAKOS.js");
1351
+ const { installDependencies, writeTemplate } = await import("./init-LSYB32NS.js");
1242
1352
  let result;
1243
1353
  try {
1244
1354
  result = writeTemplate(dir, { force: options.force });
@@ -1262,10 +1372,49 @@ program.command("init").description("Scaffold a new Vite + React + TS + Tailwind
1262
1372
  Next: ${pc.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
1263
1373
  console.log(pc.dim("then describe what to build \u2014 /dev starts the preview server"));
1264
1374
  });
1375
+ var skillsCommand = program.command("skills").description("Project knowledge injected into asks (.squint/rules.md + .squint/skills/)");
1376
+ skillsCommand.command("list").description("Show always-on rules and trigger-matched skills").action(async () => {
1377
+ const { loadRules, loadSkills } = await import("./skills-UGHU22BS.js");
1378
+ const cwd = process.cwd();
1379
+ const rules = loadRules(cwd);
1380
+ console.log(rules ? `${pc.green("\u2713")} rules.md ${pc.dim(`(${rules.split("\n").length} lines, always on)`)}` : pc.dim("\u25CB no .squint/rules.md"));
1381
+ const skills = loadSkills(cwd);
1382
+ if (skills.length === 0) {
1383
+ console.log(pc.dim("\u25CB no skills \u2014 squint skills init writes an example"));
1384
+ return;
1385
+ }
1386
+ for (const skill of skills) {
1387
+ console.log(`${pc.green("\u2713")} ${skill.name.padEnd(20)} ${pc.dim(`triggers: ${skill.triggers.join(", ")}`)}`);
1388
+ }
1389
+ });
1390
+ skillsCommand.command("init").description("Scaffold .squint/rules.md and an example skill").action(async () => {
1391
+ const fs2 = await import("fs");
1392
+ const nodePath = await import("path");
1393
+ const cwd = process.cwd();
1394
+ const skillsDir = nodePath.join(cwd, ".squint", "skills");
1395
+ fs2.mkdirSync(skillsDir, { recursive: true });
1396
+ const rules = nodePath.join(cwd, ".squint", "rules.md");
1397
+ if (!fs2.existsSync(rules)) {
1398
+ fs2.writeFileSync(
1399
+ rules,
1400
+ "# 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"
1401
+ );
1402
+ console.log(pc.green("\u2713 .squint/rules.md"));
1403
+ }
1404
+ const example = nodePath.join(skillsDir, "example.md");
1405
+ if (!fs2.existsSync(example)) {
1406
+ fs2.writeFileSync(
1407
+ example,
1408
+ "---\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"
1409
+ );
1410
+ console.log(pc.green("\u2713 .squint/skills/example.md"));
1411
+ }
1412
+ console.log(pc.dim("rules are always-on; skills inject when an ask mentions a trigger"));
1413
+ });
1265
1414
  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 () => {
1266
1415
  const fs2 = await import("fs");
1267
1416
  const nodePath = await import("path");
1268
- const { patchViteConfig, TAGGER_FILENAME, TAGGER_SOURCE } = await import("./source-MEXHWVP4.js");
1417
+ const { patchViteConfig, TAGGER_FILENAME, TAGGER_SOURCE } = await import("./source-ZZU245VN.js");
1269
1418
  const cwd = process.cwd();
1270
1419
  const taggerPath = nodePath.join(cwd, TAGGER_FILENAME);
1271
1420
  fs2.writeFileSync(taggerPath, TAGGER_SOURCE);
@@ -1301,21 +1450,21 @@ variantsCommand.command("gen").description("Generate n variants of one ask in pa
1301
1450
  process.exitCode = 1;
1302
1451
  return;
1303
1452
  }
1304
- const { isGitRepo } = await import("./snapshot-KKUY5RR6.js");
1305
- if (!isGitRepo(cwd)) {
1453
+ const { isGitRepo: isGitRepo2 } = await import("./snapshot-KKUY5RR6.js");
1454
+ if (!isGitRepo2(cwd)) {
1306
1455
  console.error(pc.red("\u2717 variants need a git repo with at least one commit"));
1307
1456
  process.exitCode = 1;
1308
1457
  return;
1309
1458
  }
1310
- const { runVariants, cleanVariants } = await import("./variants-4NCQXTP7.js");
1459
+ const { runVariants: runVariants2, cleanVariants: cleanVariants2 } = await import("./variants-7A7723L7.js");
1311
1460
  const config = loadConfig(defaultPaths(cwd));
1312
1461
  const engineId = resolveEngineId(config, options.engine);
1313
1462
  const engine = getEngine(engineId);
1314
1463
  const model = resolveModel(config, engineId, options.model);
1315
1464
  const ask = promptWords.join(" ");
1316
- cleanVariants(cwd);
1465
+ cleanVariants2(cwd);
1317
1466
  console.log(pc.dim(`generating ${n} directions in parallel via ${engine.id} \u2014 this runs ${n} engine sessions`));
1318
- const runs = await runVariants(
1467
+ const runs = await runVariants2(
1319
1468
  cwd,
1320
1469
  ask,
1321
1470
  n,
@@ -1325,9 +1474,9 @@ variantsCommand.command("gen").description("Generate n variants of one ask in pa
1325
1474
  );
1326
1475
  const succeeded = runs.filter((r) => r.result.ok);
1327
1476
  if (options.shots && succeeded.length > 0) {
1328
- const { screenshotVariants } = await import("./shots-VJBLDMF3.js");
1477
+ const { screenshotVariants: screenshotVariants2 } = await import("./shots-LRYYMTPK.js");
1329
1478
  console.log(pc.dim("capturing screenshots\u2026"));
1330
- const shots = await screenshotVariants(cwd, succeeded.map((r) => r.variant));
1479
+ const shots = await screenshotVariants2(cwd, succeeded.map((r) => r.variant));
1331
1480
  for (const shot of shots) {
1332
1481
  console.log(`${pc.green("\u2713")} ${shot.familyId.padEnd(18)} ${shot.path ?? pc.dim(shot.error ?? "")}`);
1333
1482
  }
@@ -1340,8 +1489,8 @@ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 `
1340
1489
  }
1341
1490
  );
1342
1491
  variantsCommand.command("list").description("List generated variants").action(async () => {
1343
- const { listVariants } = await import("./variants-4NCQXTP7.js");
1344
- const ids = listVariants(process.cwd());
1492
+ const { listVariants: listVariants2 } = await import("./variants-7A7723L7.js");
1493
+ const ids = listVariants2(process.cwd());
1345
1494
  if (ids.length === 0) {
1346
1495
  console.log(pc.dim('no variants \u2014 squint variants gen <n> "<ask>"'));
1347
1496
  return;
@@ -1349,26 +1498,26 @@ variantsCommand.command("list").description("List generated variants").action(as
1349
1498
  for (const id of ids) console.log(id);
1350
1499
  });
1351
1500
  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) => {
1352
- const { applyVariant, cleanVariants } = await import("./variants-4NCQXTP7.js");
1501
+ const { applyVariant: applyVariant2, cleanVariants: cleanVariants2 } = await import("./variants-7A7723L7.js");
1353
1502
  const cwd = process.cwd();
1354
- const result = applyVariant(cwd, id);
1503
+ const result = applyVariant2(cwd, id);
1355
1504
  if (!result.ok) {
1356
1505
  console.error(pc.red(`\u2717 ${result.detail}`));
1357
1506
  process.exitCode = 1;
1358
1507
  return;
1359
1508
  }
1360
- cleanVariants(cwd);
1509
+ cleanVariants2(cwd);
1361
1510
  console.log(pc.green(`\u2713 applied ${id} to the working tree`) + pc.dim(" \u2014 review with git diff"));
1362
1511
  });
1363
1512
  variantsCommand.command("clean").description("Discard all variants").action(async () => {
1364
- const { cleanVariants } = await import("./variants-4NCQXTP7.js");
1365
- const count = cleanVariants(process.cwd());
1513
+ const { cleanVariants: cleanVariants2 } = await import("./variants-7A7723L7.js");
1514
+ const count = cleanVariants2(process.cwd());
1366
1515
  console.log(pc.dim(`removed ${count} variant(s)`));
1367
1516
  });
1368
1517
  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) => {
1369
1518
  const fs2 = await import("fs");
1370
1519
  const nodePath = await import("path");
1371
- const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-RVP5BWQD.js");
1520
+ const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-2G5SLLY7.js");
1372
1521
  if (!familyId) {
1373
1522
  console.log(pc.bold("Aesthetic families") + pc.dim(" \u2014 squint brief <id>\n"));
1374
1523
  for (const family2 of FAMILIES) {
@@ -1411,7 +1560,7 @@ program.command("check").description("Run this project\u2019s quality gates (typ
1411
1560
  if (results.some((r) => !r.ok)) process.exitCode = 1;
1412
1561
  });
1413
1562
  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) => {
1414
- const { captureViewports: captureViewports2 } = await import("./preview-LTWJK3LF.js");
1563
+ const { captureViewports: captureViewports2 } = await import("./preview-XQ7EOHV4.js");
1415
1564
  const result = await captureViewports2(process.cwd(), url);
1416
1565
  if (!result) {
1417
1566
  console.error(pc.red("\u2717 no Chrome/Chromium found"));
@@ -3,8 +3,7 @@ import {
3
3
  FAMILIES,
4
4
  getFamily,
5
5
  renderFamilyBrief
6
- } from "./chunk-5IR2AZVK.js";
7
- import "./chunk-NT2HR4RD.js";
6
+ } from "./chunk-P3H4N2EN.js";
8
7
  export {
9
8
  FAMILIES,
10
9
  getFamily,
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  TAGGER_FILENAME,
4
4
  TAGGER_SOURCE
5
- } from "./chunk-W6MYXQIU.js";
5
+ } from "./chunk-VNLM53RJ.js";
6
6
 
7
7
  // src/scaffold/init.ts
8
8
  import { spawn } from "child_process";
@@ -7,10 +7,10 @@ import {
7
7
  previewDir,
8
8
  probeRuntime,
9
9
  runtimeSummary
10
- } from "./chunk-4MMIJXYD.js";
10
+ } from "./chunk-REDPR3KI.js";
11
11
  import "./chunk-VJGE7HSP.js";
12
- import "./chunk-Z46MXU64.js";
13
- import "./chunk-MCYQXOE7.js";
12
+ import "./chunk-P3V5CWH5.js";
13
+ import "./chunk-2LRIKWBU.js";
14
14
  export {
15
15
  VIEWPORTS,
16
16
  buildReviewPrompt,
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ runAgent
4
+ } from "./chunk-OTG4ZB4R.js";
5
+ import "./chunk-2LRIKWBU.js";
6
+ export {
7
+ runAgent
8
+ };
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ screenshotVariants
4
+ } from "./chunk-2IW2MMCY.js";
5
+ import "./chunk-LCS47EAG.js";
6
+ import "./chunk-P3H4N2EN.js";
7
+ import "./chunk-OTG4ZB4R.js";
8
+ import "./chunk-P3V5CWH5.js";
9
+ import "./chunk-2LRIKWBU.js";
10
+ export {
11
+ screenshotVariants
12
+ };
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ enrich,
4
+ loadRules,
5
+ loadSkills,
6
+ matchSkills,
7
+ parseSkill
8
+ } from "./chunk-XZKQZKEE.js";
9
+ export {
10
+ enrich,
11
+ loadRules,
12
+ loadSkills,
13
+ matchSkills,
14
+ parseSkill
15
+ };
@@ -3,7 +3,7 @@ import {
3
3
  TAGGER_FILENAME,
4
4
  TAGGER_SOURCE,
5
5
  patchViteConfig
6
- } from "./chunk-W6MYXQIU.js";
6
+ } from "./chunk-VNLM53RJ.js";
7
7
  export {
8
8
  TAGGER_FILENAME,
9
9
  TAGGER_SOURCE,
@@ -9,11 +9,10 @@ import {
9
9
  runVariants,
10
10
  variantPrompt,
11
11
  variantsRoot
12
- } from "./chunk-7XUORM3K.js";
13
- import "./chunk-5IR2AZVK.js";
14
- import "./chunk-FPTBBHOH.js";
15
- import "./chunk-NT2HR4RD.js";
16
- import "./chunk-MCYQXOE7.js";
12
+ } from "./chunk-LCS47EAG.js";
13
+ import "./chunk-P3H4N2EN.js";
14
+ import "./chunk-OTG4ZB4R.js";
15
+ import "./chunk-2LRIKWBU.js";
17
16
  export {
18
17
  MAX_VARIANTS,
19
18
  applyVariant,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aayambansal/squint",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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,63 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/prompt/brief.ts
4
- import fs from "fs";
5
- import path from "path";
6
- var DEFAULT_BRIEF = `You are acting as a senior product designer and frontend engineer. Treat every change as production work, not a demo.
7
-
8
- Direction before code:
9
- - Commit to one specific visual direction derived from what this product is for, and state it in one sentence before implementing. If the direction could be guessed from the product category alone, sharpen it until it couldn't.
10
- - Choose a color strategy deliberately: restrained (neutrals + one accent), committed (one dominant color owning 30\u201360% of the surface), or a small palette of 3\u20135 named roles. Never purple/violet gradients unless asked. Body text contrast stays at least 4.5:1 \u2014 no light gray body copy "for elegance".
11
- - Typography: at most two families, paired on contrast (display serif + geometric sans, or sans + mono). Never Inter/Roboto/Open Sans/system defaults for display type. Pair weight extremes (300 against 700\u2013900); display sizes jump 3x over body, not 1.5x.
12
-
13
- Tokens are the system:
14
- - Define or extend design tokens (CSS variables / theme config) first, then compose the UI from them. Never scatter literal colors or one-off spacing values through components.
15
- - One spacing rhythm on a 4/8px grid; one type scale with a ratio of at least 1.25 between steps.
16
-
17
- Banned tells \u2014 these read instantly as machine-generated:
18
- - Purple gradient on white; glassmorphism cards; cream/beige page background as a reflex "warmth" move
19
- - Emoji as icons; identical icon-topped card grids; stat banner rows; numbered 01/02/03 section markers
20
- - Tiny all-caps tracked eyebrow labels over every section; gradient text; colored left-border strips on cards
21
- - Centered hero + badge + three feature cards; unmodified component-library defaults
22
- If someone could look at the result and say "AI made that" without doubt, it has failed.
23
-
24
- Craft details:
25
- - Every interactive element gets hover, focus-visible, and active treatment; keyboard focus stays visible.
26
- - Anything that loads data gets loading, empty, and error states.
27
- - Motion: 150\u2013250ms ease-out; one orchestrated entrance with staggered reveals beats scattered micro-interactions; honor prefers-reduced-motion; never leave content invisible until a scroll observer fires.
28
- - Body line length 65\u201375ch; text-wrap: balance on headings.
29
-
30
- Engineering:
31
- - Follow the repo's existing conventions and extend its patterns; new components in new files, small and focused; semantic HTML.
32
- - Responsive from 360px up with no horizontal overflow \u2014 check intermediate widths, not just phone and desktop.
33
- - Let errors surface instead of swallowing them in try/catch; log clearly so failures can be traced.
34
- - Not done until the app builds cleanly, typechecks, and renders without console errors.`;
35
- var FIRST_TURN_ADDENDUM = `This is the opening move on this task: establish the design foundation before building. State the visual direction, set up the tokens/theme first, then build components from them. The first render should feel like a designed product, not a scaffold \u2014 impressive on sight.`;
36
- function loadBrief(cwd) {
37
- const custom = path.join(cwd, ".squint", "brief.md");
38
- try {
39
- const text = fs.readFileSync(custom, "utf8").trim();
40
- if (text.length > 0) return text;
41
- } catch {
42
- }
43
- return DEFAULT_BRIEF;
44
- }
45
- function composePrompt(ask, opts) {
46
- if (opts.noBrief) return ask;
47
- const brief = loadBrief(opts.cwd);
48
- const firstTurn = opts.firstTurn ?? true;
49
- const addendum = firstTurn ? `
50
-
51
- ${FIRST_TURN_ADDENDUM}` : "";
52
- return `${brief}${addendum}
53
-
54
- ## Task
55
-
56
- ${ask}`;
57
- }
58
-
59
- export {
60
- DEFAULT_BRIEF,
61
- FIRST_TURN_ADDENDUM,
62
- composePrompt
63
- };
@@ -1,66 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- variantsRoot
4
- } from "./chunk-7XUORM3K.js";
5
- import "./chunk-5IR2AZVK.js";
6
- import {
7
- DevServer,
8
- detectDevCommand
9
- } from "./chunk-ZJOU46X4.js";
10
- import "./chunk-FPTBBHOH.js";
11
- import "./chunk-NT2HR4RD.js";
12
- import {
13
- findChrome,
14
- screenshot
15
- } from "./chunk-Z46MXU64.js";
16
- import "./chunk-MCYQXOE7.js";
17
-
18
- // src/variants/shots.ts
19
- import path from "path";
20
- var URL_TIMEOUT_MS = 45e3;
21
- function waitForUrl(server) {
22
- return new Promise((resolve) => {
23
- const startedAt = Date.now();
24
- const poll = setInterval(() => {
25
- if (server.url) {
26
- clearInterval(poll);
27
- resolve(server.url);
28
- } else if (Date.now() - startedAt > URL_TIMEOUT_MS || server.state === "crashed") {
29
- clearInterval(poll);
30
- resolve(null);
31
- }
32
- }, 200);
33
- });
34
- }
35
- async function screenshotVariants(cwd, variants) {
36
- const chrome = findChrome();
37
- if (!chrome) {
38
- return variants.map((v) => ({ familyId: v.family.id, error: "no Chrome found" }));
39
- }
40
- const shots = [];
41
- for (const variant of variants) {
42
- const command = detectDevCommand(variant.dir);
43
- if (!command) {
44
- shots.push({ familyId: variant.family.id, error: "no dev script" });
45
- continue;
46
- }
47
- const server = new DevServer(variant.dir);
48
- server.start(command);
49
- const url = await waitForUrl(server);
50
- if (!url) {
51
- server.stop();
52
- shots.push({ familyId: variant.family.id, error: "dev server did not announce a URL" });
53
- continue;
54
- }
55
- const outPath = path.join(variantsRoot(cwd), `${variant.family.id}.png`);
56
- const result = await screenshot(chrome, url, outPath, { width: 1440, height: 900 });
57
- server.stop();
58
- shots.push(
59
- result.ok ? { familyId: variant.family.id, path: outPath } : { familyId: variant.family.id, error: result.error }
60
- );
61
- }
62
- return shots;
63
- }
64
- export {
65
- screenshotVariants
66
- };
@@ -1,23 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/engines/registry.ts
4
- import fs from "fs";
5
- import path from "path";
6
-
7
- // src/engines/aider.ts
8
- var aider = {
9
- id: "aider",
10
- name: "Aider",
11
- binary: "aider",
12
- install: "python -m pip install aider-install && aider-install",
13
- supportsResume: false,
14
- buildArgs(opts) {
15
- const args = ["--message", opts.prompt, "--yes-always", "--no-auto-commits"];
16
- if (opts.model) args.push("--model", opts.model);
17
- return args;
18
- }
19
- };
20
-
21
3
  // src/util/stream.ts
22
4
  function lineSplitter(onLine) {
23
5
  let buffer = "";
@@ -42,6 +24,24 @@ function truncate(text, max) {
42
24
  return text.slice(0, max - 1) + "\u2026";
43
25
  }
44
26
 
27
+ // src/engines/registry.ts
28
+ import fs from "fs";
29
+ import path from "path";
30
+
31
+ // src/engines/aider.ts
32
+ var aider = {
33
+ id: "aider",
34
+ name: "Aider",
35
+ binary: "aider",
36
+ install: "python -m pip install aider-install && aider-install",
37
+ supportsResume: false,
38
+ buildArgs(opts) {
39
+ const args = ["--message", opts.prompt, "--yes-always", "--no-auto-commits"];
40
+ if (opts.model) args.push("--model", opts.model);
41
+ return args;
42
+ }
43
+ };
44
+
45
45
  // src/engines/claudeProtocol.ts
46
46
  function createClaudeStreamParser(readyLabel) {
47
47
  let sawTextDelta = false;