@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 +20 -6
- package/dist/{chrome-K4BKFOYS.js → chrome-RD7XQ767.js} +2 -2
- package/dist/{chunk-ZJOU46X4.js → chunk-2IW2MMCY.js} +59 -2
- package/dist/{chunk-7XUORM3K.js → chunk-LCS47EAG.js} +3 -5
- package/dist/{chunk-FPTBBHOH.js → chunk-OTG4ZB4R.js} +1 -1
- package/dist/{chunk-5IR2AZVK.js → chunk-P3H4N2EN.js} +58 -3
- package/dist/{chunk-Z46MXU64.js → chunk-P3V5CWH5.js} +1 -1
- package/dist/{chunk-4MMIJXYD.js → chunk-REDPR3KI.js} +1 -1
- package/dist/{chunk-W6MYXQIU.js → chunk-VNLM53RJ.js} +60 -10
- package/dist/chunk-XZKQZKEE.js +80 -0
- package/dist/cli.js +183 -34
- package/dist/{families-RVP5BWQD.js → families-2G5SLLY7.js} +1 -2
- package/dist/{init-7AYGAKOS.js → init-LSYB32NS.js} +1 -1
- package/dist/{preview-LTWJK3LF.js → preview-XQ7EOHV4.js} +3 -3
- package/dist/run-XHLCTF6V.js +8 -0
- package/dist/shots-LRYYMTPK.js +12 -0
- package/dist/skills-UGHU22BS.js +15 -0
- package/dist/{source-MEXHWVP4.js → source-ZZU245VN.js} +1 -1
- package/dist/{variants-4NCQXTP7.js → variants-7A7723L7.js} +4 -5
- package/package.json +1 -1
- package/dist/chunk-NT2HR4RD.js +0 -63
- package/dist/shots-VJBLDMF3.js +0 -66
- package/dist/{chunk-MCYQXOE7.js → chunk-2LRIKWBU.js} +18 -18
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.**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
136
|
-
|
|
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
|
|
|
@@ -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-
|
|
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-
|
|
6
|
+
} from "./chunk-P3H4N2EN.js";
|
|
6
7
|
import {
|
|
7
8
|
runAgent
|
|
8
|
-
} from "./chunk-
|
|
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";
|
|
@@ -1,7 +1,60 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
@@ -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
|
|
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;
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
5
|
+
detectDevCommand,
|
|
6
|
+
screenshotVariants
|
|
7
|
+
} from "./chunk-2IW2MMCY.js";
|
|
7
8
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
applyVariant,
|
|
10
|
+
cleanVariants,
|
|
11
|
+
listVariants,
|
|
12
|
+
runVariants
|
|
13
|
+
} from "./chunk-LCS47EAG.js";
|
|
10
14
|
import {
|
|
11
15
|
composePrompt
|
|
12
|
-
} from "./chunk-
|
|
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-
|
|
35
|
+
} from "./chunk-REDPR3KI.js";
|
|
29
36
|
import "./chunk-VJGE7HSP.js";
|
|
30
|
-
import
|
|
37
|
+
import {
|
|
38
|
+
findChrome
|
|
39
|
+
} from "./chunk-P3V5CWH5.js";
|
|
31
40
|
import {
|
|
32
41
|
detectEngines,
|
|
33
42
|
getEngine
|
|
34
|
-
} from "./chunk-
|
|
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
|
-
|
|
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 /
|
|
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.
|
|
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) >=
|
|
1216
|
-
console.log(`${nodeOk ? pc.green("\u2713") : pc.red("\u2717")} node ${process.versions.node}${nodeOk ? "" : " (need >=
|
|
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
|
-
|
|
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 =
|
|
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-
|
|
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-
|
|
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 (!
|
|
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-
|
|
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
|
-
|
|
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
|
|
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-
|
|
1477
|
+
const { screenshotVariants: screenshotVariants2 } = await import("./shots-LRYYMTPK.js");
|
|
1329
1478
|
console.log(pc.dim("capturing screenshots\u2026"));
|
|
1330
|
-
const shots = await
|
|
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-
|
|
1344
|
-
const ids =
|
|
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-
|
|
1501
|
+
const { applyVariant: applyVariant2, cleanVariants: cleanVariants2 } = await import("./variants-7A7723L7.js");
|
|
1353
1502
|
const cwd = process.cwd();
|
|
1354
|
-
const result =
|
|
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
|
-
|
|
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-
|
|
1365
|
-
const count =
|
|
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-
|
|
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-
|
|
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"));
|
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
previewDir,
|
|
8
8
|
probeRuntime,
|
|
9
9
|
runtimeSummary
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-REDPR3KI.js";
|
|
11
11
|
import "./chunk-VJGE7HSP.js";
|
|
12
|
-
import "./chunk-
|
|
13
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-P3V5CWH5.js";
|
|
13
|
+
import "./chunk-2LRIKWBU.js";
|
|
14
14
|
export {
|
|
15
15
|
VIEWPORTS,
|
|
16
16
|
buildReviewPrompt,
|
|
@@ -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
|
+
};
|
|
@@ -9,11 +9,10 @@ import {
|
|
|
9
9
|
runVariants,
|
|
10
10
|
variantPrompt,
|
|
11
11
|
variantsRoot
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
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
package/dist/chunk-NT2HR4RD.js
DELETED
|
@@ -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
|
-
};
|
package/dist/shots-VJBLDMF3.js
DELETED
|
@@ -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;
|