@aayambansal/squint 0.1.0
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/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/cdp-5OHNGQXV.js +9 -0
- package/dist/chrome-4WNYZZ42.js +10 -0
- package/dist/chunk-43VZK47W.js +118 -0
- package/dist/chunk-4XHTAWTH.js +164 -0
- package/dist/chunk-5IR2AZVK.js +127 -0
- package/dist/chunk-FQDSJRTY.js +156 -0
- package/dist/chunk-MGHJSERZ.js +86 -0
- package/dist/chunk-MURKQ7SI.js +74 -0
- package/dist/chunk-NT2HR4RD.js +63 -0
- package/dist/chunk-OJTW5SYY.js +197 -0
- package/dist/chunk-UHVE42IQ.js +135 -0
- package/dist/chunk-W6MYXQIU.js +126 -0
- package/dist/chunk-WTA6YYBY.js +54 -0
- package/dist/chunk-YIVPCWSG.js +402 -0
- package/dist/cli.js +968 -0
- package/dist/families-RVP5BWQD.js +12 -0
- package/dist/gates-G5DABJWQ.js +13 -0
- package/dist/init-7AYGAKOS.js +176 -0
- package/dist/preview-5PCHDJL7.js +22 -0
- package/dist/shots-ADBOBALL.js +66 -0
- package/dist/snapshot-KKUY5RR6.js +11 -0
- package/dist/source-MEXHWVP4.js +11 -0
- package/dist/variants-VVIYT7WI.js +27 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aayam Bansal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# squint
|
|
2
|
+
|
|
3
|
+
**Lovable for your terminal.** squint is a frontend harness that sits on top of the coding agents you already have — Claude Code, Codex CLI, Gemini CLI, OpenCode, Amp, Cursor CLI, Copilot CLI, Aider — and turns them into a design-obsessed app builder that works on **any repo**, local-first.
|
|
4
|
+
|
|
5
|
+
Lovable proved the loop: prompt → generate → preview → auto-fix → iterate. But it's a closed SaaS. You can't point it at an existing codebase, choose your agent or model, or own the loop. squint is that missing layer, in your terminal.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌──────────────────────────────────────────────────────────┐
|
|
9
|
+
│ ❯ make the pricing page feel premium │
|
|
10
|
+
│ ⚙ Read · src/pages/Pricing.tsx │
|
|
11
|
+
│ ⚙ Edit · src/pages/Pricing.tsx │
|
|
12
|
+
│ Reworked the tier cards around a real type scale and │
|
|
13
|
+
│ tightened the palette to ink + one accent… │
|
|
14
|
+
│ · done · 41s · $0.18 │
|
|
15
|
+
│ ✗ dev server: 1 error line(s) │
|
|
16
|
+
│ error TS2304: Cannot find name 'Tier' │
|
|
17
|
+
│ · auto-fix attempt 1/2 │
|
|
18
|
+
│ ⛑ fix dev server errors │
|
|
19
|
+
│ · done · 12s · $0.05 │
|
|
20
|
+
│ │
|
|
21
|
+
│ ❯ ▏ │
|
|
22
|
+
│ claude · my-app · http://localhost:5173 · /help │
|
|
23
|
+
└──────────────────────────────────────────────────────────┘
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## The loop
|
|
27
|
+
|
|
28
|
+
1. **You describe.** Every ask is wrapped in squint's design brief — an opinionated standard built from studying Lovable's leaked prompts, v0's design rules, and the documented catalog of "AI slop" tells: commit to a direction before code, tokens are the system, banned generic patterns, real craft details. Override per project with `.squint/brief.md`.
|
|
29
|
+
2. **Your agent builds.** squint drives whichever engine you choose, headlessly, streaming its work into the transcript token by token.
|
|
30
|
+
3. **The dev server judges.** squint runs your dev server, watches for build errors after every turn, and routes fresh breakage straight back to the engine (`/fix`, or automatic with `autoFix`).
|
|
31
|
+
4. **The runtime is watched.** After every clean turn, squint loads the page headlessly (CDP over Chrome, ~2s) and catches what the dev server never prints — blank pages, uncaught exceptions, console errors, failed requests — and feeds those back too.
|
|
32
|
+
5. **The agent looks at its work.** `/review` screenshots the running app at mobile/tablet/desktop and re-prompts the engine to critique what it can *see* — then fix it.
|
|
33
|
+
6. **Gates keep it honest.** `/check` runs typecheck → lint → test → build and feeds failures back, with instructions not to weaken the checks.
|
|
34
|
+
7. **Everything is reversible.** Each ask is snapshotted via git plumbing; `/undo` reverts the whole turn — while your own uncommitted work survives.
|
|
35
|
+
8. **Point at things.** `squint tag` (wired automatically in new apps) adds an element picker to your dev server: **Alt+S**, click any element, and a self-locating reference (`src/App.tsx <h1 class="…"> "Ready."`) lands on your clipboard to paste into squint.
|
|
36
|
+
9. **Explore in parallel.** `squint variants gen 3 "landing page for …"` builds the same ask three ways — three git worktrees, three *different* committed aesthetic directions, one screenshot each. Pick with your eyes, `squint variants apply <id>`.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npm install -g @aayambansal/squint # then just: squint
|
|
42
|
+
npx @aayambansal/squint # or run without installing
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
From source:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
git clone https://github.com/aayambansal/squint.git
|
|
49
|
+
cd squint
|
|
50
|
+
npm install && npm run build && npm link
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
You need at least one engine installed (`squint doctor` shows what's found):
|
|
54
|
+
|
|
55
|
+
| Engine | id | Install |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| Claude Code | `claude` | `npm i -g @anthropic-ai/claude-code` |
|
|
58
|
+
| Codex CLI | `codex` | `npm i -g @openai/codex` |
|
|
59
|
+
| Gemini CLI | `gemini` | `npm i -g @google/gemini-cli` |
|
|
60
|
+
| OpenCode | `opencode` | `npm i -g opencode-ai` |
|
|
61
|
+
| Amp | `amp` | `npm i -g @sourcegraph/amp` |
|
|
62
|
+
| Cursor CLI | `cursor` | `curl https://cursor.com/install -fsS \| bash` |
|
|
63
|
+
| Copilot CLI | `copilot` | `npm i -g @github/copilot` |
|
|
64
|
+
| Aider | `aider` | `pip install aider-install && aider-install` |
|
|
65
|
+
|
|
66
|
+
API keys stay wherever your agent CLIs already read them — squint stores no secrets.
|
|
67
|
+
|
|
68
|
+
## Use
|
|
69
|
+
|
|
70
|
+
**Start fresh** (Lovable-style, from nothing):
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
squint init my-app # Vite + React + TS + Tailwind v4, token-first CSS
|
|
74
|
+
cd my-app && squint # open the TUI, describe what to build
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Or on any existing repo:**
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
cd your-project && squint
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Headless / scriptable:**
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
squint run "add a dark mode toggle to the navbar"
|
|
87
|
+
squint run -e codex -m gpt-5 "tighten the hero spacing"
|
|
88
|
+
squint run --json "…" # normalized ndjson events for scripting
|
|
89
|
+
squint check # quality gates: typecheck → lint → test → build
|
|
90
|
+
squint shot http://localhost:5173 # screenshots at 390/768/1440
|
|
91
|
+
squint brief # list design directions; squint brief terminal commits one
|
|
92
|
+
squint engines # what's installed
|
|
93
|
+
squint doctor # engines + Chrome + WebSocket check
|
|
94
|
+
squint tag # add the Alt+S element picker to any Vite app
|
|
95
|
+
squint variants gen 3 "<ask>" # 3 parallel design explorations (3 engine runs)
|
|
96
|
+
squint variants apply terminal # keep the winner, discard the rest
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Commit a design direction** so every session holds the same look:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
squint brief # editorial-minimal · terminal · warm-editorial · data-dense
|
|
103
|
+
# cinematic-dark · playful · brutalist
|
|
104
|
+
squint brief cinematic-dark # writes .squint/brief.md — plain markdown, made to remix
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Configure** (global `~/.config/squint/config.json`, per-repo `.squint/config.json`):
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
squint config set engine claude
|
|
111
|
+
squint config set models.claude claude-sonnet-5
|
|
112
|
+
squint config set autoDev true # start the dev server with the TUI
|
|
113
|
+
squint config set autoFix true # auto-send build/runtime errors back (max 2 tries)
|
|
114
|
+
squint config set autoProbe false # disable the post-turn runtime probe
|
|
115
|
+
squint config set --project engine codex # per-repo override
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Inside the TUI:**
|
|
119
|
+
|
|
120
|
+
| Command | What it does |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| `/dev` | start/stop the project dev server (auto-detected script + package manager) |
|
|
123
|
+
| `/check` | run quality gates |
|
|
124
|
+
| `/fix` | send captured errors / failed gates to the engine |
|
|
125
|
+
| `/shot` | screenshot the app at mobile/tablet/desktop |
|
|
126
|
+
| `/review [focus]` | screenshots + the engine critiques its own rendered work, then fixes it |
|
|
127
|
+
| `/undo` | revert the whole last ask (your own uncommitted work survives) |
|
|
128
|
+
| `/resume` | pick the previous session back up after a restart |
|
|
129
|
+
| `/engine <id>` · `/model <name>` | switch backend or model mid-session |
|
|
130
|
+
| `/clear` | new session |
|
|
131
|
+
| `Esc` | interrupt the running turn · `↑/↓` prompt history |
|
|
132
|
+
|
|
133
|
+
## Design decisions
|
|
134
|
+
|
|
135
|
+
- **Engines are dumb translators.** Each adapter is ~80 lines: build a headless invocation, parse the stream into one normalized event model. Claude Code, Amp, and Cursor share a single wire-protocol parser. All product behavior lives in the harness, so adding an engine is cheap.
|
|
136
|
+
- **The brief is the product.** Direction-before-code, token-first, banned-tells — encoded from research (`docs/research/`), not vibes.
|
|
137
|
+
- **Zero-dependency eyes.** Screenshots use Chrome's own one-shot headless mode — no Playwright/Puppeteer install.
|
|
138
|
+
- **Sessions resume** where engines support it (Claude `--resume`, Codex `exec resume`, OpenCode `--session`, Amp threads, Cursor chats).
|
|
139
|
+
|
|
140
|
+
## Roadmap
|
|
141
|
+
|
|
142
|
+
- npm distribution
|
|
143
|
+
- Variants inside the TUI with an inline picker
|
|
144
|
+
- Deeper gates: axe accessibility audits, Lighthouse budgets, visual regression baselines
|
|
145
|
+
|
|
146
|
+
Architecture notes in `docs/design/`, research base in `docs/research/`.
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT © Aayam Bansal
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/gates/gates.ts
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
function detectGates(cwd) {
|
|
8
|
+
let pkg;
|
|
9
|
+
try {
|
|
10
|
+
pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
|
|
11
|
+
} catch {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
const scripts = pkg.scripts ?? {};
|
|
15
|
+
const gates = [];
|
|
16
|
+
const npmRun = (script) => ({
|
|
17
|
+
command: "npm",
|
|
18
|
+
args: ["run", script],
|
|
19
|
+
display: `npm run ${script}`
|
|
20
|
+
});
|
|
21
|
+
const hasTs = fs.existsSync(path.join(cwd, "tsconfig.json")) || Boolean(pkg.devDependencies?.typescript ?? pkg.dependencies?.typescript);
|
|
22
|
+
if (scripts.typecheck) {
|
|
23
|
+
gates.push({ id: "typecheck", ...npmRun("typecheck") });
|
|
24
|
+
} else if (hasTs) {
|
|
25
|
+
gates.push({ id: "typecheck", command: "npx", args: ["tsc", "--noEmit"], display: "tsc --noEmit" });
|
|
26
|
+
}
|
|
27
|
+
const hasEslintConfig = [
|
|
28
|
+
"eslint.config.js",
|
|
29
|
+
"eslint.config.mjs",
|
|
30
|
+
"eslint.config.cjs",
|
|
31
|
+
"eslint.config.ts",
|
|
32
|
+
".eslintrc",
|
|
33
|
+
".eslintrc.js",
|
|
34
|
+
".eslintrc.cjs",
|
|
35
|
+
".eslintrc.json"
|
|
36
|
+
].some((file) => fs.existsSync(path.join(cwd, file)));
|
|
37
|
+
if (scripts.lint) {
|
|
38
|
+
gates.push({ id: "lint", ...npmRun("lint") });
|
|
39
|
+
} else if (hasEslintConfig) {
|
|
40
|
+
gates.push({ id: "lint", command: "npx", args: ["eslint", ".", "--max-warnings", "0"], display: "eslint ." });
|
|
41
|
+
}
|
|
42
|
+
const testScript = scripts.test;
|
|
43
|
+
if (testScript && !/no test specified/i.test(testScript)) {
|
|
44
|
+
gates.push({ id: "test", ...npmRun("test") });
|
|
45
|
+
}
|
|
46
|
+
if (scripts.build) {
|
|
47
|
+
gates.push({ id: "build", ...npmRun("build") });
|
|
48
|
+
}
|
|
49
|
+
return gates;
|
|
50
|
+
}
|
|
51
|
+
var TAIL_LINES = 40;
|
|
52
|
+
var GATE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
53
|
+
function runGate(cwd, gate) {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
const startedAt = Date.now();
|
|
56
|
+
let output = "";
|
|
57
|
+
const child = spawn(gate.command, gate.args, {
|
|
58
|
+
cwd,
|
|
59
|
+
// CI collapses watch modes (vitest etc.) into single runs.
|
|
60
|
+
env: { ...process.env, CI: "true", FORCE_COLOR: "0" },
|
|
61
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
62
|
+
});
|
|
63
|
+
const collect = (chunk) => {
|
|
64
|
+
output += chunk;
|
|
65
|
+
if (output.length > 6e4) output = output.slice(-6e4);
|
|
66
|
+
};
|
|
67
|
+
child.stdout.setEncoding("utf8");
|
|
68
|
+
child.stdout.on("data", collect);
|
|
69
|
+
child.stderr.setEncoding("utf8");
|
|
70
|
+
child.stderr.on("data", collect);
|
|
71
|
+
const finish = (ok) => {
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
const lines = output.split("\n").filter((line) => line.trim().length > 0);
|
|
74
|
+
resolve({
|
|
75
|
+
gate,
|
|
76
|
+
ok,
|
|
77
|
+
durationMs: Date.now() - startedAt,
|
|
78
|
+
outputTail: lines.slice(-TAIL_LINES).join("\n")
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
const timer = setTimeout(() => {
|
|
82
|
+
child.kill("SIGKILL");
|
|
83
|
+
output += "\n[gate timed out]";
|
|
84
|
+
}, GATE_TIMEOUT_MS);
|
|
85
|
+
child.on("error", (err) => {
|
|
86
|
+
output += `
|
|
87
|
+
${err.message}`;
|
|
88
|
+
finish(false);
|
|
89
|
+
});
|
|
90
|
+
child.on("close", (code) => finish(code === 0));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async function runGates(cwd, gates, onResult) {
|
|
94
|
+
const results = [];
|
|
95
|
+
for (const gate of gates) {
|
|
96
|
+
const result = await runGate(cwd, gate);
|
|
97
|
+
results.push(result);
|
|
98
|
+
onResult?.(result);
|
|
99
|
+
}
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
102
|
+
function buildGatePrompt(failures) {
|
|
103
|
+
const sections = failures.map((f) => `### ${f.gate.id} (\`${f.gate.display}\`)
|
|
104
|
+
|
|
105
|
+
${f.outputTail}`).join("\n\n");
|
|
106
|
+
return `Quality gates failed. Fix the underlying problems \u2014 do not weaken the checks, skip tests, or loosen compiler/lint settings to get green.
|
|
107
|
+
|
|
108
|
+
${sections}
|
|
109
|
+
|
|
110
|
+
After fixing, the failing commands above must pass.`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export {
|
|
114
|
+
detectGates,
|
|
115
|
+
runGate,
|
|
116
|
+
runGates,
|
|
117
|
+
buildGatePrompt
|
|
118
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
cdpCapture,
|
|
4
|
+
hasWebSocket
|
|
5
|
+
} from "./chunk-OJTW5SYY.js";
|
|
6
|
+
import {
|
|
7
|
+
findChrome,
|
|
8
|
+
screenshot
|
|
9
|
+
} from "./chunk-MURKQ7SI.js";
|
|
10
|
+
|
|
11
|
+
// src/preview/preview.ts
|
|
12
|
+
import fs2 from "fs";
|
|
13
|
+
import os from "os";
|
|
14
|
+
import path2 from "path";
|
|
15
|
+
|
|
16
|
+
// src/state/state.ts
|
|
17
|
+
import fs from "fs";
|
|
18
|
+
import path from "path";
|
|
19
|
+
var IGNORED = ["preview/", "state.json", "variants/"];
|
|
20
|
+
function ensureSquintIgnore(cwd) {
|
|
21
|
+
const dir = path.join(cwd, ".squint");
|
|
22
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
23
|
+
const file = path.join(dir, ".gitignore");
|
|
24
|
+
let existing = "";
|
|
25
|
+
try {
|
|
26
|
+
existing = fs.readFileSync(file, "utf8");
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
const lines = existing.split("\n").filter((l) => l.trim().length > 0);
|
|
30
|
+
const missing = IGNORED.filter((entry) => !lines.includes(entry));
|
|
31
|
+
if (missing.length > 0) {
|
|
32
|
+
fs.writeFileSync(file, [...lines, ...missing].join("\n") + "\n");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function stateFile(cwd) {
|
|
36
|
+
return path.join(cwd, ".squint", "state.json");
|
|
37
|
+
}
|
|
38
|
+
function loadState(cwd) {
|
|
39
|
+
try {
|
|
40
|
+
const raw = JSON.parse(fs.readFileSync(stateFile(cwd), "utf8"));
|
|
41
|
+
if (typeof raw?.engine === "string" && typeof raw?.sessionId === "string" && typeof raw?.at === "number") {
|
|
42
|
+
return raw;
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function saveState(cwd, state) {
|
|
49
|
+
ensureSquintIgnore(cwd);
|
|
50
|
+
fs.writeFileSync(stateFile(cwd), JSON.stringify(state, null, 2) + "\n");
|
|
51
|
+
}
|
|
52
|
+
function clearState(cwd) {
|
|
53
|
+
try {
|
|
54
|
+
fs.rmSync(stateFile(cwd));
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/preview/preview.ts
|
|
60
|
+
var VIEWPORTS = [
|
|
61
|
+
{ name: "mobile", width: 390, height: 844 },
|
|
62
|
+
{ name: "tablet", width: 768, height: 1024 },
|
|
63
|
+
{ name: "desktop", width: 1440, height: 900 }
|
|
64
|
+
];
|
|
65
|
+
function previewDir(cwd) {
|
|
66
|
+
const dir = path2.join(cwd, ".squint", "preview");
|
|
67
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
68
|
+
ensureSquintIgnore(cwd);
|
|
69
|
+
return dir;
|
|
70
|
+
}
|
|
71
|
+
async function captureViewports(cwd, url) {
|
|
72
|
+
const chrome = findChrome();
|
|
73
|
+
if (!chrome) return null;
|
|
74
|
+
const dir = previewDir(cwd);
|
|
75
|
+
if (hasWebSocket()) {
|
|
76
|
+
try {
|
|
77
|
+
const { report, shots: shots2 } = await cdpCapture(chrome, url, dir, VIEWPORTS);
|
|
78
|
+
return { shots: shots2, errors: [], runtime: report };
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const shots = [];
|
|
83
|
+
const errors = [];
|
|
84
|
+
for (const viewport of VIEWPORTS) {
|
|
85
|
+
const outPath = path2.join(dir, `${viewport.name}.png`);
|
|
86
|
+
const result = await screenshot(chrome, url, outPath, {
|
|
87
|
+
width: viewport.width,
|
|
88
|
+
height: viewport.height
|
|
89
|
+
});
|
|
90
|
+
if (result.ok) {
|
|
91
|
+
shots.push({ name: viewport.name, path: outPath });
|
|
92
|
+
} else {
|
|
93
|
+
errors.push(`${viewport.name}: ${result.error}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { shots, errors };
|
|
97
|
+
}
|
|
98
|
+
function runtimeSummary(report) {
|
|
99
|
+
const parts = [];
|
|
100
|
+
if (report.pageErrors.length > 0) parts.push(`${report.pageErrors.length} page error(s)`);
|
|
101
|
+
if (report.consoleErrors.length > 0) parts.push(`${report.consoleErrors.length} console error(s)`);
|
|
102
|
+
if (report.failedRequests.length > 0) parts.push(`${report.failedRequests.length} failed request(s)`);
|
|
103
|
+
return parts.length > 0 ? parts.join(" \xB7 ") : null;
|
|
104
|
+
}
|
|
105
|
+
function runtimeSection(report) {
|
|
106
|
+
if (!report) return "";
|
|
107
|
+
const blocks = [];
|
|
108
|
+
if (report.pageErrors.length > 0) {
|
|
109
|
+
blocks.push(`Uncaught page errors:
|
|
110
|
+
${report.pageErrors.slice(0, 10).join("\n")}`);
|
|
111
|
+
}
|
|
112
|
+
if (report.consoleErrors.length > 0) {
|
|
113
|
+
blocks.push(`Console errors:
|
|
114
|
+
${report.consoleErrors.slice(0, 10).join("\n")}`);
|
|
115
|
+
}
|
|
116
|
+
if (report.failedRequests.length > 0) {
|
|
117
|
+
blocks.push(`Failed requests:
|
|
118
|
+
${report.failedRequests.slice(0, 10).join("\n")}`);
|
|
119
|
+
}
|
|
120
|
+
if (blocks.length === 0) return "";
|
|
121
|
+
return `
|
|
122
|
+
|
|
123
|
+
## Runtime errors observed while loading the page
|
|
124
|
+
|
|
125
|
+
${blocks.join("\n\n")}
|
|
126
|
+
|
|
127
|
+
Fix these first \u2014 a page that errors is broken regardless of how it looks.`;
|
|
128
|
+
}
|
|
129
|
+
async function probeRuntime(url) {
|
|
130
|
+
const chrome = findChrome();
|
|
131
|
+
if (!chrome || !hasWebSocket()) return null;
|
|
132
|
+
try {
|
|
133
|
+
const { report } = await cdpCapture(chrome, url, os.tmpdir(), [], 1500);
|
|
134
|
+
return report;
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function buildRuntimeFixPrompt(report) {
|
|
140
|
+
return `The running app has runtime problems.${runtimeSection(report)}
|
|
141
|
+
|
|
142
|
+
Diagnose and fix the root causes, then confirm the app loads clean.`;
|
|
143
|
+
}
|
|
144
|
+
function buildReviewPrompt(shots, extra, runtime) {
|
|
145
|
+
const list = shots.map((s) => `- ${s.name}: ${s.path}`).join("\n");
|
|
146
|
+
return `Screenshots of the running app were just captured:
|
|
147
|
+
|
|
148
|
+
${list}
|
|
149
|
+
|
|
150
|
+
Read each screenshot and review the rendered UI against the design standards you were given. Check: visual hierarchy and spacing rhythm, typography, color and contrast, alignment, empty-looking or broken regions, and whether the mobile capture shows horizontal overflow or cramped layout. List the concrete issues you can SEE (not hypothetical ones), ranked by visual impact${extra ? `, with special attention to: ${extra}` : ""}. Then fix them and verify the app still builds.${runtimeSection(runtime)}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export {
|
|
154
|
+
loadState,
|
|
155
|
+
saveState,
|
|
156
|
+
clearState,
|
|
157
|
+
VIEWPORTS,
|
|
158
|
+
previewDir,
|
|
159
|
+
captureViewports,
|
|
160
|
+
runtimeSummary,
|
|
161
|
+
probeRuntime,
|
|
162
|
+
buildRuntimeFixPrompt,
|
|
163
|
+
buildReviewPrompt
|
|
164
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_BRIEF
|
|
4
|
+
} from "./chunk-NT2HR4RD.js";
|
|
5
|
+
|
|
6
|
+
// src/prompt/families.ts
|
|
7
|
+
var FAMILIES = [
|
|
8
|
+
{
|
|
9
|
+
id: "editorial-minimal",
|
|
10
|
+
name: "Editorial Minimalism",
|
|
11
|
+
summary: "Linear/Stripe lineage \u2014 restraint, typography does the work",
|
|
12
|
+
direction: `## Direction: Editorial Minimalism
|
|
13
|
+
|
|
14
|
+
Quiet surfaces, decisive typography, one accent used sparingly. The design should feel edited, not decorated.
|
|
15
|
+
|
|
16
|
+
- Type: a characterful grotesque or neo-grotesque for display (never Inter/Roboto), same family or a close neutral sans for body. Weight extremes carry hierarchy (300 vs 700+), not size alone.
|
|
17
|
+
- Color: near-white paper, near-black ink, one accent on less than 10% of the surface. No gradients.
|
|
18
|
+
- Space: generous whitespace on an 8px rhythm; wide margins; content max-width around 68ch.
|
|
19
|
+
- Motion: almost none \u2014 150ms fades and position shifts only where they explain state.
|
|
20
|
+
- Avoid: cards for everything (prefer rules and spacing to separate content), icon grids, decorative illustration.`
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "terminal",
|
|
24
|
+
name: "Terminal-Core",
|
|
25
|
+
summary: "Monospace, dark, dense \u2014 software that looks like software",
|
|
26
|
+
direction: `## Direction: Terminal-Core
|
|
27
|
+
|
|
28
|
+
Unapologetically technical: monospace-forward, dark, information-dense, zero chrome.
|
|
29
|
+
|
|
30
|
+
- Type: a great monospace (JetBrains Mono, Berkeley Mono lineage) for UI and data; optionally one compact sans for long prose.
|
|
31
|
+
- Color: true dark background (not gray soup), phosphor-inspired accent (green/amber/cyan \u2014 pick ONE), semantic red/yellow only for status. Body text stays high-contrast.
|
|
32
|
+
- Space: tight but rhythmic \u2014 4px grid, table-like alignment, visible structure (rules, column guides).
|
|
33
|
+
- Motion: instant. At most a cursor blink or a 100ms state flash.
|
|
34
|
+
- Avoid: rounded-corner softness, shadows, any gradient, lowercase-only affectation that hurts scanning.`
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "warm-editorial",
|
|
38
|
+
name: "Warm Editorial",
|
|
39
|
+
summary: "Serif display, humane warmth \u2014 carried by type and accents, never a beige page",
|
|
40
|
+
direction: `## Direction: Warm Editorial
|
|
41
|
+
|
|
42
|
+
Bookish confidence: a serif with real character doing the talking, warmth carried in accents and imagery.
|
|
43
|
+
|
|
44
|
+
- Type: expressive serif display (Fraunces/Newsreader lineage) paired with a geometric sans for UI. Big size jumps (3x+) between display and body.
|
|
45
|
+
- Color: paper stays crisp near-white \u2014 warmth comes from a terracotta/ochre/oxblood accent and ink that leans warm-black. Never a cream/beige page background.
|
|
46
|
+
- Space: editorial grid with asymmetry; pull-quotes and drop caps welcome where content supports them.
|
|
47
|
+
- Motion: soft 200ms ease-out reveals on load, nothing on scroll.
|
|
48
|
+
- Avoid: script fonts, sepia-toned everything, decorative dividers.`
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "data-dense",
|
|
52
|
+
name: "Data-Dense Pro",
|
|
53
|
+
summary: "Dashboards and tools \u2014 density as a feature, hierarchy through alignment",
|
|
54
|
+
direction: `## Direction: Data-Dense Pro
|
|
55
|
+
|
|
56
|
+
Built for people who live in the product eight hours a day: density, alignment, and speed over air.
|
|
57
|
+
|
|
58
|
+
- Type: one compact sans with strong numerals (tabular figures mandatory for data); 13\u201314px body is correct here.
|
|
59
|
+
- Color: neutral surface ladder (2\u20133 steps), one brand accent for primary actions, strict semantic palette for status. Charts get a deliberate categorical ramp, not rainbow defaults.
|
|
60
|
+
- Space: 4px grid, table-first layouts, column alignment across widgets; every panel earns its border.
|
|
61
|
+
- Motion: none on data; 120ms on overlays only.
|
|
62
|
+
- Avoid: hero sections, marketing spacing, cards-inside-cards, centered text.`
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "cinematic-dark",
|
|
66
|
+
name: "Cinematic Dark",
|
|
67
|
+
summary: "Dramatic dark surfaces, controlled glow, product as protagonist",
|
|
68
|
+
direction: `## Direction: Cinematic Dark
|
|
69
|
+
|
|
70
|
+
Dark with intent \u2014 depth from lighting logic, not gray layers. The product shot is the hero.
|
|
71
|
+
|
|
72
|
+
- Type: a wide/display sans with presence for headlines, tight tracking at size; clean sans body at high contrast (no mid-gray text on black).
|
|
73
|
+
- Color: near-black stage, one luminous accent used like stage lighting (hover glows, focus rings, key CTAs). At most one gradient, in the accent hue, and only where light would fall.
|
|
74
|
+
- Space: generous vertical rhythm; content emerges in layers.
|
|
75
|
+
- Motion: one orchestrated entrance with staggered reveals (200\u2013250ms, ease-out), then calm.
|
|
76
|
+
- Avoid: purple-on-black defaults, glassmorphism panels, glow on everything, starfields.`
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: "playful",
|
|
80
|
+
name: "Playful Color",
|
|
81
|
+
summary: "Saturated, confident color \u2014 joy with a grid underneath",
|
|
82
|
+
direction: `## Direction: Playful Color
|
|
83
|
+
|
|
84
|
+
Color does the branding: saturated fields, chunky type, real personality \u2014 held together by a strict grid.
|
|
85
|
+
|
|
86
|
+
- Type: a rounded or geometric display face with real weight (800+) for headlines; simple sans body.
|
|
87
|
+
- Color: commit \u2014 the surface IS the color (drenched sections in 2\u20133 saturated hues that share a tonal family). Text stays black or white, always AA.
|
|
88
|
+
- Space: big blocks, hard edges between color fields, 8px rhythm inside them.
|
|
89
|
+
- Motion: springy but short (200ms, one bounce maximum); hover states that visibly react.
|
|
90
|
+
- Avoid: pastel timidity, rainbow spread (pick a family), emoji as design elements.`
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "brutalist",
|
|
94
|
+
name: "Neo-Brutalist",
|
|
95
|
+
summary: "Raw structure, stark type, hard edges \u2014 the grid is the aesthetic",
|
|
96
|
+
direction: `## Direction: Neo-Brutalist
|
|
97
|
+
|
|
98
|
+
Structure exposed: visible borders, stark type, unapologetic contrast. Honest, loud, precise.
|
|
99
|
+
|
|
100
|
+
- Type: oversized display type (grotesque or mono), often uppercase, tracking tightened; body stays small and functional.
|
|
101
|
+
- Color: black on white (or one inverted section), plus a single shock accent. Solid fills only.
|
|
102
|
+
- Space: hard 2\u20134px borders, no shadows, no rounding (or one deliberate radius used everywhere); layouts that show their grid.
|
|
103
|
+
- Motion: abrupt on purpose \u2014 instant state changes, maybe one marquee if the content earns it.
|
|
104
|
+
- Avoid: soft shadows sneaking in, gradient anything, politeness.`
|
|
105
|
+
}
|
|
106
|
+
];
|
|
107
|
+
function getFamily(id) {
|
|
108
|
+
return FAMILIES.find((f) => f.id === id);
|
|
109
|
+
}
|
|
110
|
+
function coreStandards() {
|
|
111
|
+
const marker = "Tokens are the system:";
|
|
112
|
+
const index = DEFAULT_BRIEF.indexOf(marker);
|
|
113
|
+
return DEFAULT_BRIEF.slice(index);
|
|
114
|
+
}
|
|
115
|
+
function renderFamilyBrief(family) {
|
|
116
|
+
return `You are acting as a senior product designer and frontend engineer. Treat every change as production work, not a demo. This project has a committed design direction \u2014 hold it consistently in every change.
|
|
117
|
+
|
|
118
|
+
${family.direction}
|
|
119
|
+
|
|
120
|
+
${coreStandards()}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export {
|
|
124
|
+
FAMILIES,
|
|
125
|
+
getFamily,
|
|
126
|
+
renderFamilyBrief
|
|
127
|
+
};
|