@pro-vi/designer 0.3.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Provi Zhang
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,210 @@
1
+ # designer
2
+
3
+ MCP + CLI for iterating on **claude.ai/design** — Claude's web-based wireframe and high-fidelity design tool.
4
+
5
+ The human describes intent. The orchestrating agent translates intent into a minimal prompt, relays it to Claude Design, hands the URL back to the human, interprets their reaction, iterates. When the human says "yes", `designer_handoff` downloads a tar.gz bundle (README + chat transcripts + all design files + source JSX) so the implementing agent (Claude Code, typically) can build the design in a real repo.
6
+
7
+ ## Stance
8
+
9
+ - **Single-vendor, single-purpose.** Only `claude.ai/design`. No kitchen sink.
10
+ - **`agent-browser` is the substrate.** Attaches to your real Chrome via CDP — sidesteps Cloudflare + Google SSO.
11
+ - **Human is the designer.** See `~/.claude/skills/designer-loop`. Orchestrator is translation + plumbing; Claude Design has the taste; human arbitrates.
12
+ - **URL is the default taste path.** `designer_prompt` returns a live claude.ai/design URL where tweak sliders work and variants switch. Local tasting harness exists only for when IDE chrome gets in the way.
13
+ - **Artifacts land on disk.** Every iteration + every handoff saves under `./artifacts/{key}/`.
14
+
15
+ ## Install
16
+
17
+ One-call first-run:
18
+
19
+ ```bash
20
+ git clone https://github.com/pro-vi/designer.git && cd designer
21
+ npm install
22
+ ./bin/designer setup
23
+ ```
24
+
25
+ Optional — make `designer` available globally:
26
+
27
+ ```bash
28
+ npm link
29
+ designer setup # now works from any cwd
30
+ ```
31
+
32
+ `designer setup` is idempotent and auto-progresses:
33
+
34
+ 1. Installs deps if missing.
35
+ 2. Checks `agent-browser` is on PATH.
36
+ 3. Asks you to Cmd+Q Chrome if a non-debug Chrome is running (polls until quit).
37
+ 4. Launches a dedicated debug Chrome (`--remote-debugging-port=9222`, profile at `~/.chrome-designer-profile/`).
38
+ 5. Polls until you sign in to Claude and reach `/design`.
39
+ 6. Installs the `designer-loop` skill at `~/.claude/skills/designer-loop/` unless one is already present (respects bootstrap/dotfiles-managed symlinks).
40
+ 7. Registers the MCP with Claude Code at user scope.
41
+
42
+ Re-run any time — every step no-ops when already satisfied.
43
+
44
+ ### Why a dedicated profile?
45
+
46
+ Since Chrome 136, `--remote-debugging-port` is blocked on the default profile for security. The dedicated `~/.chrome-designer-profile/` is a one-time login that persists across launches. Your normal Chrome is untouched.
47
+
48
+ ### Auto-launch
49
+
50
+ After first setup, the MCP auto-launches debug Chrome from the saved profile on the first tool call of any session. You don't have to think about Chrome state again. If a non-debug Chrome is running, auto-launch bails with an actionable error.
51
+
52
+ ### Bot detection
53
+
54
+ Login is a real human typing into a real Chrome window. `--remote-debugging-port` alone doesn't set `navigator.webdriver` (only `--enable-automation` does); user-agent is identical to normal Chrome. Cloudflare and Google OAuth see a normal session. First login on the new profile may trigger Google's "new device" verification — that's a standard one-time prompt.
55
+
56
+ ### Manual setup
57
+
58
+ The MCP registration embeds `DESIGNER_CDP=9222`, so Claude sessions pick it up automatically. The shell export below only matters for direct CLI invocations (`designer session`, `designer prompt`, etc.) from an interactive terminal — add it to `~/.zshenv` or equivalent if you use the CLI directly.
59
+
60
+ ```bash
61
+ ./scripts/designer-chrome.sh # launches debug Chrome
62
+ # sign in to Claude, navigate to /design
63
+ curl -s http://127.0.0.1:9222/json/version | head # verify CDP
64
+ export DESIGNER_CDP=9222 # only needed for direct CLI use
65
+ claude mcp add --transport stdio designer \
66
+ -- env DESIGNER_CDP=9222 "$PWD/bin/designer" mcp serve
67
+ ```
68
+
69
+ ## CLI
70
+
71
+ Top-level help leads with the typical loop:
72
+
73
+ ```
74
+ $ designer --help
75
+
76
+ designer — CLI + MCP for iterating on claude.ai/design
77
+
78
+ Typical loop:
79
+ designer setup (once per machine)
80
+ designer session --action create --name "X" --key x start a project
81
+ designer prompt "design the …" --key x prints 'Taste here: <url>' ← open that
82
+ designer prompt - --key x < follow-up.txt iterate until human says yes
83
+ designer handoff --key x bundle for code implementation
84
+ ```
85
+
86
+ Verbs are grouped: session lifecycle, design operations, file introspection, exit/promotion, setup+ops, internal. Every verb supports `--help`:
87
+
88
+ ```bash
89
+ designer prompt --help # expanded docs: input modes, flags, output shape, examples
90
+ designer help handoff # same
91
+ ```
92
+
93
+ All verbs take `--key <k>` to isolate parallel sessions (e.g., working on two features at once). Local state lives at `~/.designer/sessions.json`.
94
+
95
+ Prompts accept three input modes:
96
+
97
+ ```bash
98
+ designer prompt "short text" --key feat-x # positional
99
+ designer prompt --prompt-file ./brief.md --key feat-x # from file
100
+ cat follow-up.txt | designer prompt - --key feat-x # stdin
101
+ ```
102
+
103
+ Output of `prompt` and `snapshot` leads with `Taste here: <url>` above the JSON — the URL is the default taste path.
104
+
105
+ ## MCP
106
+
107
+ Six tools, registered at user scope by `designer setup`:
108
+
109
+ | Tool | Purpose |
110
+ |---|---|
111
+ | `designer_session` | Enter / inspect / transition. Actions: `status` (default, read-only), `ensure_ready`, `resume`, `create`. Always returns stored state + `currentUrl` + `availableFiles`. |
112
+ | `designer_prompt` | Modify the design (HTML-diff wait). Auto-appends a flat-layout instruction. Returns `url` (hand to human), `newFiles`, `activeFile`, `failureMode`, `htmlPath`, `chatReply`. |
113
+ | `designer_ask` | Q&A with the assistant (chat-panel wait). No file changes. Returns `reply`. |
114
+ | `designer_list` | `scope: 'projects'` (scrapes home) or `'files'` (scrapes file panel — flat-only, see quirks). |
115
+ | `designer_snapshot` | Capture current state. Optional `filename` to switch first. Default: paths + hash only; `includeHtml: true` inlines. |
116
+ | `designer_handoff` | Export → Handoff → download + extract tar.gz. Returns README + paths. Auto-repairs Claude-side em-dash filename bugs. |
117
+
118
+ Registration:
119
+
120
+ ```bash
121
+ claude mcp add --transport stdio designer \
122
+ -- env DESIGNER_CDP=9222 \
123
+ /Users/provi/Development/_projs/designer/bin/designer mcp serve
124
+ ```
125
+
126
+ (`designer setup` runs this. After `npm link` it collapses to `designer mcp serve`.)
127
+
128
+ ## The loop
129
+
130
+ ```
131
+ 1. Intent → human describes what they want to feel / change
132
+ 2. Read → designer_session returns currentUrl + availableFiles
133
+ 3. Relay → designer_prompt with a terse, guide-not-constrain prompt
134
+ 4. Taste → hand the human the url from the return; they react in their own words
135
+ 5. Interpret → next designer_prompt (modify) or designer_ask (clarify)
136
+ 6. Repeat 3-5 → until human says "that's it"
137
+ 7. Promote → designer_handoff — bundle is the decision record
138
+ ```
139
+
140
+ Full guidance in `~/.claude/skills/designer-loop/SKILL.md` (also in-repo at `skills/designer-loop/SKILL.md`).
141
+
142
+ ## Tasting harness
143
+
144
+ Fallback for when claude.ai/design's IDE chrome (chat panel + toolbar) eats too much viewport to judge at real scale. Requires a prior `designer_handoff`.
145
+
146
+ ```bash
147
+ designer tasting --key <key>
148
+ ```
149
+
150
+ Walks the latest bundle's `project/` dir (recursively — handles nested layouts), writes `tasting.html` with variant tabs + keyboard shortcuts (1/2/3) + persistent notes (localStorage), starts a local `http.server`, opens the browser.
151
+
152
+ Default path for tasting is the live URL. Use tasting when: full-viewport comparison matters, Claude didn't build its own `index.html` gallery, or the IDE chrome is distracting.
153
+
154
+ ## Operations
155
+
156
+ - `designer doctor` — diagnose first-run setup state. Checks agent-browser, CDP, a /design tab is open, selectors present, skill installed, MCP registered. Exits 2 on failure.
157
+ - `designer health` — probe every UI anchor this MCP depends on. 17 probes across home / session / share / pattern categories. Exits 2 on any fail. Wire into cron / CI to catch claude.ai UI regressions (it already moved Export under Share once mid-development).
158
+
159
+ ## Layout
160
+
161
+ ```
162
+ designer/
163
+ ├── package.json
164
+ ├── tsconfig.json # type-check only
165
+ ├── tsconfig.build.json # tsc → dist/
166
+ ├── bin/designer # bash wrapper, prefers dist/ then tsx
167
+ ├── mcp-server.ts # MCP stdio server (exports startMcpServer)
168
+ ├── cli.ts # same verbs, directly runnable; rich --help
169
+ ├── designer-controller.ts # core flow: session, prompt, ask, snapshot, handoff
170
+ ├── browser.ts # thin wrapper over agent-browser subprocess
171
+ ├── cdp-ensure.ts # auto-launches debug Chrome on first tool call
172
+ ├── tasting.ts # tasting.html generator + http.server
173
+ ├── ui-anchors.ts # every DOM / URL / structural dependency, enumerated
174
+ ├── setup.ts # designer setup verb
175
+ ├── session-store.ts # per-session state at ~/.designer/
176
+ ├── artifact-store.ts # writes HTML/PNG/JSON under ./artifacts/{key}/
177
+ ├── repo-root.ts # package.json walk so source + compiled both resolve resources
178
+ ├── selectors.json # DOM selectors for the claude.ai/design surface
179
+ ├── scripts/
180
+ │ ├── designer-chrome.sh # standalone Chrome launcher
181
+ │ └── probe.ts # manual DOM exploration helper
182
+ ├── skills/
183
+ │ └── designer-loop/SKILL.md # the skill, copied to ~/.claude/skills/ by setup
184
+ ├── artifacts/ # generated outputs (gitignored)
185
+ └── dist/ # tsc build output (gitignored; published on npm)
186
+ ```
187
+
188
+ ## Known quirks
189
+
190
+ - **Folder-organized variants.** Claude Design sometimes organizes multi-file variants under a subfolder (`directions/sediment.html`). The live MCP's file-list scrape (`designer_list files`, `availableFiles` in session status, `newFiles` diff from `designer_prompt`) is flat-only; nested files are invisible until `designer_handoff`. Mitigation: `designer_prompt` auto-appends *"Keep all generated files at the project root; no subfolders."* Handoff bundle is folder-aware; `designer tasting` walks recursively.
191
+ - **React-controlled inputs.** `agent-browser fill` doesn't fire React's synthetic input event. The controller uses the native `HTMLTextAreaElement` value-setter + `dispatchEvent(new Event('input', { bubbles: true }))`, plus JS `.click()` on Send and Create. Both are canonical React-compat patterns.
192
+ - **Em-dash handoff filenames.** Claude's handoff pipeline wrote em-dash (`—`) into `index.html` hrefs but saved files with regular hyphens. `designer_handoff` detects and repairs (`repaired.renamed: [...]`). No-op when hrefs already resolve.
193
+ - **Cross-origin iframe.** Served HTML lives on `claudeusercontent.com` with a signed `t=` token in the URL. Direct fetch from node works without cookies. The token is session-scoped, not per-iteration.
194
+ - **UI regressions.** Claude has moved critical buttons mid-development (Export → Share dropdown). `designer health` is the early-warning system; run it periodically.
195
+
196
+ ## Publishing (future — not yet on npm)
197
+
198
+ Prepped but not shipped. Prerequisites: npm account with scope `@pro-vi` (currently 404/available), 2FA, `npm login`. Then:
199
+
200
+ ```bash
201
+ npm publish --access public
202
+ ```
203
+
204
+ `prepublishOnly` runs `tsc --noEmit` + `tsc -p tsconfig.build.json`. `files` whitelist in `package.json` keeps the tarball under 35KB. Published package will support:
205
+
206
+ ```bash
207
+ npx @pro-vi/designer setup # trial
208
+ npm i -g @pro-vi/designer && designer setup # daily use
209
+ claude mcp add --transport stdio designer -- designer mcp serve # MCP
210
+ ```
package/bin/designer ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+ # Wrapper that resolves the repo's tsx + cli.ts regardless of the user's cwd.
3
+ # Lets users type `designer <verb>` (after `npm link` or PATH symlink)
4
+ # instead of the verbose `node_modules/.bin/tsx cli.ts <verb>`.
5
+
6
+ set -e
7
+
8
+ # Resolve symlinks to find the real script location, then back up to repo root.
9
+ SOURCE="${BASH_SOURCE[0]}"
10
+ while [ -h "$SOURCE" ]; do
11
+ DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
12
+ SOURCE="$(readlink "$SOURCE")"
13
+ [[ "$SOURCE" != /* ]] && SOURCE="$DIR/$SOURCE"
14
+ done
15
+ BIN_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
16
+ REPO_ROOT="$(cd "$BIN_DIR/.." && pwd)"
17
+
18
+ DIST_CLI="$REPO_ROOT/dist/cli.js"
19
+ TSX="$REPO_ROOT/node_modules/.bin/tsx"
20
+ SRC_CLI="$REPO_ROOT/cli.ts"
21
+
22
+ # Prefer compiled output (npm-installed users + post-build dev). Fall back to
23
+ # tsx-on-source (clone-and-run dev mode, before tsc emits dist/).
24
+ if [ -f "$DIST_CLI" ]; then
25
+ exec node "$DIST_CLI" "$@"
26
+ fi
27
+
28
+ if [ -x "$TSX" ] && [ -f "$SRC_CLI" ]; then
29
+ exec "$TSX" "$SRC_CLI" "$@"
30
+ fi
31
+
32
+ echo "[designer] No runnable found." >&2
33
+ echo " Expected $DIST_CLI (compiled) or $TSX + $SRC_CLI (dev)." >&2
34
+ echo " Run: cd $REPO_ROOT && npm install && npm run build" >&2
35
+ exit 1
@@ -0,0 +1,46 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { REPO_ROOT } from "./repo-root.js";
4
+ const ARTIFACTS_ROOT = process.env.DESIGNER_ARTIFACTS_DIR || path.join(REPO_ROOT, 'artifacts');
5
+ function slug(s) {
6
+ return String(s || 'session')
7
+ .toLowerCase()
8
+ .replace(/[^a-z0-9-]+/g, '-')
9
+ .replace(/^-+|-+$/g, '')
10
+ .slice(0, 64) || 'session';
11
+ }
12
+ function ts() {
13
+ return new Date().toISOString().replace(/[:.]/g, '-');
14
+ }
15
+ export function sessionDir(key) {
16
+ const dir = path.join(ARTIFACTS_ROOT, slug(key));
17
+ fs.mkdirSync(dir, { recursive: true });
18
+ return dir;
19
+ }
20
+ export function saveIteration(key, input) {
21
+ const dir = sessionDir(key);
22
+ const stamp = ts();
23
+ const base = path.join(dir, stamp);
24
+ const record = {
25
+ at: new Date().toISOString(),
26
+ key,
27
+ prompt: input.prompt,
28
+ fidelity: input.fidelity,
29
+ url: input.url,
30
+ meta: input.meta ?? null,
31
+ files: {}
32
+ };
33
+ if (input.html) {
34
+ const p = `${base}.html`;
35
+ fs.writeFileSync(p, input.html);
36
+ record.files.html = p;
37
+ }
38
+ if (input.screenshotPath) {
39
+ record.files.screenshot = input.screenshotPath;
40
+ }
41
+ fs.writeFileSync(`${base}.json`, JSON.stringify(record, null, 2));
42
+ return record;
43
+ }
44
+ export function artifactsRoot() {
45
+ return ARTIFACTS_ROOT;
46
+ }
@@ -0,0 +1,101 @@
1
+ import { spawn } from 'node:child_process';
2
+ const BIN = process.env.DESIGNER_AGENT_BROWSER_BIN || 'agent-browser';
3
+ const DEFAULT_SESSION = process.env.DESIGNER_SESSION_NAME || 'designer';
4
+ const CDP = process.env.DESIGNER_CDP || '';
5
+ export function createBrowser({ session = DEFAULT_SESSION, headed = true, timeoutMs = 30_000, cdp = CDP } = {}) {
6
+ const baseEnv = {
7
+ ...process.env,
8
+ AGENT_BROWSER_DEFAULT_TIMEOUT: String(timeoutMs),
9
+ ...(cdp ? {} : { AGENT_BROWSER_SESSION_NAME: session }),
10
+ ...(headed && !cdp ? { AGENT_BROWSER_HEADED: '1' } : {})
11
+ };
12
+ function connectFlags() {
13
+ if (!cdp)
14
+ return [];
15
+ if (cdp === 'auto' || cdp === '1' || cdp === 'true')
16
+ return ['--auto-connect'];
17
+ return ['--cdp', cdp];
18
+ }
19
+ function run(args, { input, parseJson = false } = {}) {
20
+ return new Promise((resolve, reject) => {
21
+ const finalArgs = [...connectFlags(), ...args];
22
+ const child = spawn(BIN, finalArgs, { env: baseEnv, stdio: ['pipe', 'pipe', 'pipe'] });
23
+ let stdout = '';
24
+ let stderr = '';
25
+ child.stdout.on('data', (d) => (stdout += d.toString()));
26
+ child.stderr.on('data', (d) => (stderr += d.toString()));
27
+ child.on('error', (err) => reject(err));
28
+ child.on('close', (code) => {
29
+ if (code !== 0) {
30
+ const err = new Error(`agent-browser ${finalArgs.join(' ')} exited ${code}: ${stderr.trim() || stdout.trim()}`);
31
+ err.code = code;
32
+ return reject(err);
33
+ }
34
+ if (!parseJson)
35
+ return resolve(stdout.trim());
36
+ try {
37
+ resolve(JSON.parse(stdout));
38
+ }
39
+ catch (e) {
40
+ reject(new Error(`Failed to parse JSON from agent-browser: ${e.message}\n--stdout--\n${stdout}`));
41
+ }
42
+ });
43
+ if (input != null) {
44
+ child.stdin.write(input);
45
+ child.stdin.end();
46
+ }
47
+ });
48
+ }
49
+ return {
50
+ session,
51
+ run,
52
+ open: (url) => run(['open', url]),
53
+ close: () => run(['close']).catch(() => null),
54
+ url: () => run(['get', 'url']),
55
+ title: () => run(['get', 'title']),
56
+ snapshot: ({ interactive = true, scope } = {}) => {
57
+ const args = ['snapshot', '--json'];
58
+ if (interactive)
59
+ args.push('-i');
60
+ if (scope)
61
+ args.push('-s', scope);
62
+ return run(args, { parseJson: true });
63
+ },
64
+ snapshotText: ({ interactive = true, scope } = {}) => {
65
+ const args = ['snapshot'];
66
+ if (interactive)
67
+ args.push('-i');
68
+ if (scope)
69
+ args.push('-s', scope);
70
+ return run(args);
71
+ },
72
+ click: (sel) => run(['click', sel]),
73
+ fill: (sel, text) => run(['fill', sel, text]),
74
+ type: (sel, text) => run(['type', sel, text]),
75
+ press: (key) => run(['press', key]),
76
+ getText: (sel) => run(['get', 'text', sel]),
77
+ getAttr: (sel, name) => run(['get', 'attr', name, sel]),
78
+ getHtml: (sel) => run(['get', 'html', sel]),
79
+ isVisible: (sel) => run(['is', 'visible', sel]).then((s) => s.trim() === 'true'),
80
+ waitFor: (selOrMs) => run(['wait', String(selOrMs)]),
81
+ waitLoad: (state = 'networkidle') => run(['wait', '--load', state]),
82
+ screenshot: (path, { full = false } = {}) => {
83
+ const args = ['screenshot'];
84
+ if (path)
85
+ args.push(path);
86
+ if (full)
87
+ args.push('--full');
88
+ return run(args);
89
+ },
90
+ eval: (js) => run(['eval', js]),
91
+ evalValue: async (js) => {
92
+ const out = await run(['eval', js]);
93
+ try {
94
+ return JSON.parse(out);
95
+ }
96
+ catch (e) {
97
+ throw new Error(`evalValue: stdout was not JSON-parseable: ${e.message}\n--stdout--\n${out.slice(0, 500)}`);
98
+ }
99
+ }
100
+ };
101
+ }
@@ -0,0 +1,44 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawn, spawnSync } from 'node:child_process';
5
+ const PORT = process.env.DESIGNER_CDP || '9222';
6
+ const PROFILE = path.join(os.homedir(), '.chrome-designer-profile');
7
+ const CHROME_BIN = process.env.CHROME_BIN || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
8
+ async function isCdpUp() {
9
+ try {
10
+ const res = await fetch(`http://127.0.0.1:${PORT}/json/version`, { signal: AbortSignal.timeout(1500) });
11
+ return res.ok;
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ function chromeRunning() {
18
+ const r = spawnSync('pgrep', ['-f', 'Google Chrome.app/Contents/MacOS/Google Chrome'], { stdio: 'pipe' });
19
+ return r.status === 0 && (r.stdout?.toString().trim().length ?? 0) > 0;
20
+ }
21
+ function sleep(ms) {
22
+ return new Promise((r) => setTimeout(r, ms));
23
+ }
24
+ export async function ensureCdpUp() {
25
+ if (await isCdpUp())
26
+ return;
27
+ if (!fs.existsSync(PROFILE)) {
28
+ throw new Error(`CDP not up on :${PORT} and no dedicated Chrome profile at ${PROFILE}. Run: designer setup`);
29
+ }
30
+ if (chromeRunning()) {
31
+ throw new Error(`CDP not up on :${PORT} and a non-debug Chrome is already running. Quit Chrome (Cmd+Q) and retry, or run: designer setup`);
32
+ }
33
+ if (!fs.existsSync(CHROME_BIN)) {
34
+ throw new Error(`CDP not up on :${PORT} and Chrome not found at ${CHROME_BIN}. Set CHROME_BIN or install Chrome.`);
35
+ }
36
+ const child = spawn(CHROME_BIN, ['--remote-debugging-port=' + PORT, '--user-data-dir=' + PROFILE, 'https://claude.ai/design'], { detached: true, stdio: 'ignore' });
37
+ child.unref();
38
+ for (let i = 0; i < 40; i++) {
39
+ await sleep(500);
40
+ if (await isCdpUp())
41
+ return;
42
+ }
43
+ throw new Error(`Auto-launched Chrome but CDP didn't come up on :${PORT} within 20s. Check that the launched window survived, or run designer setup.`);
44
+ }