@pro-vi/designer 0.3.8 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ // Cross-platform entry: works on macOS/Linux/Windows.
3
+ // Resolves the repo root from this file's location, prefers the compiled
4
+ // dist/cli.js, falls back to tsx+source for dev/clone-and-run mode.
5
+ import { spawnSync } from 'node:child_process';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const BIN_DIR = path.dirname(fs.realpathSync(__filename));
12
+ const REPO_ROOT = path.resolve(BIN_DIR, '..');
13
+
14
+ const DIST_CLI = path.join(REPO_ROOT, 'dist', 'cli.js');
15
+ const SRC_CLI = path.join(REPO_ROOT, 'cli.ts');
16
+ const TSX_BIN = path.join(REPO_ROOT, 'node_modules', '.bin', process.platform === 'win32' ? 'tsx.cmd' : 'tsx');
17
+
18
+ const argv = process.argv.slice(2);
19
+
20
+ // Prefer compiled output (npm-installed users + post-build dev). Fall back to
21
+ // tsx-on-source (clone-and-run dev mode, before tsc emits dist/).
22
+ if (fs.existsSync(DIST_CLI)) {
23
+ const r = spawnSync(process.execPath, [DIST_CLI, ...argv], { stdio: 'inherit' });
24
+ process.exit(r.status ?? 1);
25
+ }
26
+
27
+ if (fs.existsSync(TSX_BIN) && fs.existsSync(SRC_CLI)) {
28
+ // shell:true lets Windows resolve the .cmd shim for tsx without a separate code path.
29
+ const r = spawnSync(TSX_BIN, [SRC_CLI, ...argv], { stdio: 'inherit', shell: process.platform === 'win32' });
30
+ process.exit(r.status ?? 1);
31
+ }
32
+
33
+ console.error('[designer] No runnable found.');
34
+ console.error(` Expected ${DIST_CLI} (compiled) or ${TSX_BIN} + ${SRC_CLI} (dev).`);
35
+ console.error(` Run: cd ${REPO_ROOT} && npm install && npm run build`);
36
+ process.exit(1);
package/dist/browser.js CHANGED
@@ -1,4 +1,4 @@
1
- import { spawn } from 'node:child_process';
1
+ import { xspawn } from "./cross-platform.js";
2
2
  const BIN = process.env.DESIGNER_AGENT_BROWSER_BIN || 'agent-browser';
3
3
  const DEFAULT_SESSION = process.env.DESIGNER_SESSION_NAME || 'designer';
4
4
  const CDP = process.env.DESIGNER_CDP ?? '9222';
@@ -19,7 +19,7 @@ export function createBrowser({ session = DEFAULT_SESSION, headed = true, timeou
19
19
  function run(args, { input, parseJson = false } = {}) {
20
20
  return new Promise((resolve, reject) => {
21
21
  const finalArgs = [...connectFlags(), ...args];
22
- const child = spawn(BIN, finalArgs, { env: baseEnv, stdio: ['pipe', 'pipe', 'pipe'] });
22
+ const child = xspawn(BIN, finalArgs, { env: baseEnv, stdio: ['pipe', 'pipe', 'pipe'] });
23
23
  let stdout = '';
24
24
  let stderr = '';
25
25
  child.stdout.on('data', (d) => (stdout += d.toString()));
@@ -98,9 +98,9 @@ export function createBrowser({ session = DEFAULT_SESSION, headed = true, timeou
98
98
  args.push('--full');
99
99
  return run(args);
100
100
  },
101
- eval: (js) => run(['eval', js]),
101
+ eval: (js) => run(['eval', '--stdin'], { input: js }),
102
102
  evalValue: async (js) => {
103
- const out = await run(['eval', js]);
103
+ const out = await run(['eval', '--stdin'], { input: js });
104
104
  try {
105
105
  return JSON.parse(out);
106
106
  }
@@ -1,10 +1,11 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { spawn, spawnSync } from 'node:child_process';
4
+ import { spawn } from 'node:child_process';
5
+ import { defaultChromeBin, isChromeRunning, QUIT_CHROME_HINT } from "./cross-platform.js";
5
6
  const PORT = process.env.DESIGNER_CDP || '9222';
6
7
  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
+ const CHROME_BIN = process.env.CHROME_BIN || defaultChromeBin();
8
9
  async function isCdpUp() {
9
10
  try {
10
11
  const res = await fetch(`http://127.0.0.1:${PORT}/json/version`, { signal: AbortSignal.timeout(1500) });
@@ -14,10 +15,6 @@ async function isCdpUp() {
14
15
  return false;
15
16
  }
16
17
  }
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
18
  function sleep(ms) {
22
19
  return new Promise((r) => setTimeout(r, ms));
23
20
  }
@@ -27,8 +24,8 @@ export async function ensureCdpUp() {
27
24
  if (!fs.existsSync(PROFILE)) {
28
25
  throw new Error(`CDP not up on :${PORT} and no dedicated Chrome profile at ${PROFILE}. Run: designer setup`);
29
26
  }
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`);
27
+ if (isChromeRunning()) {
28
+ throw new Error(`CDP not up on :${PORT} and a non-debug Chrome is already running. ${QUIT_CHROME_HINT} Then retry, or run: designer setup`);
32
29
  }
33
30
  if (!fs.existsSync(CHROME_BIN)) {
34
31
  throw new Error(`CDP not up on :${PORT} and Chrome not found at ${CHROME_BIN}. Set CHROME_BIN or install Chrome.`);
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env -S node --import tsx
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { spawn, spawnSync } from 'node:child_process';
5
4
  import { createHash } from 'node:crypto';
5
+ import { xspawn, xspawnSync, WHICH, IS_WIN } from "./cross-platform.js";
6
6
  import { DesignerController } from "./designer-controller.js";
7
7
  import { listSessions, getSession } from "./session-store.js";
8
8
  import { createBrowser } from "./browser.js";
@@ -12,6 +12,7 @@ import { runSetup } from "./setup.js";
12
12
  import { startMcpServer } from "./mcp-server.js";
13
13
  import { REPO_ROOT } from "./repo-root.js";
14
14
  import { runHealth } from "./ui-anchors.js";
15
+ import { PACKAGE_VERSION } from "./package-meta.js";
15
16
  const [, , cmd, ...rest] = process.argv;
16
17
  function parseFlags(args) {
17
18
  const out = { _: [] };
@@ -37,6 +38,10 @@ function parseFlags(args) {
37
38
  const flags = parseFlags(rest);
38
39
  const key = flags.key || 'default';
39
40
  async function main() {
41
+ if (cmd === '--version' || cmd === '-v' || cmd === 'version' || flags.version === true || flags.v === true) {
42
+ console.log(PACKAGE_VERSION);
43
+ return;
44
+ }
40
45
  if (flags.help === true || flags.h === true) {
41
46
  if (cmd && HELP[cmd]) {
42
47
  console.log(HELP[cmd]);
@@ -533,7 +538,7 @@ function checkDeps() {
533
538
  }
534
539
  async function checkAgentBrowser() {
535
540
  return new Promise((resolve) => {
536
- const c = spawn('agent-browser', ['--version'], { stdio: 'pipe' });
541
+ const c = xspawn('agent-browser', ['--version'], { stdio: 'pipe' });
537
542
  let v = '';
538
543
  c.stdout.on('data', (d) => (v += d.toString()));
539
544
  c.on('error', () => resolve({ name: 'agent-browser installed', status: 'fail', detail: 'binary not found on PATH; install from https://github.com/agent-browser/agent-browser' }));
@@ -553,7 +558,7 @@ async function checkCdp() {
553
558
  return {
554
559
  name: `CDP at port ${port}`,
555
560
  status: 'fail',
556
- detail: `not reachable. Run: ./scripts/designer-chrome.sh (launches Chrome with --remote-debugging-port=${port} in a dedicated profile)`
561
+ detail: `not reachable. Run: ${IS_WIN ? 'powershell scripts\\designer-chrome.ps1' : './scripts/designer-chrome.sh'} (launches Chrome with --remote-debugging-port=${port} in a dedicated profile)`
557
562
  };
558
563
  }
559
564
  }
@@ -619,11 +624,11 @@ function checkSkillInstalled() {
619
624
  return { name: 'designer-loop skill installed', status: 'ok', detail: skillDir };
620
625
  }
621
626
  async function checkMcpRegistered() {
622
- const which = spawnSync('which', ['claude'], { stdio: 'pipe' });
627
+ const which = xspawnSync(WHICH, ['claude'], { stdio: 'pipe' });
623
628
  if (which.status !== 0) {
624
629
  return { name: 'MCP registered with Claude Code', status: 'warn', detail: 'claude CLI not on PATH; install Claude Code to verify' };
625
630
  }
626
- const list = spawnSync('claude', ['mcp', 'list'], { stdio: 'pipe' });
631
+ const list = xspawnSync('claude', ['mcp', 'list'], { stdio: 'pipe' });
627
632
  if (list.status !== 0) {
628
633
  return { name: 'MCP registered with Claude Code', status: 'fail', detail: `\`claude mcp list\` exited ${list.status}` };
629
634
  }
@@ -0,0 +1,50 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from 'node:child_process';
4
+ import crossSpawn from 'cross-spawn';
5
+ export const IS_WIN = process.platform === 'win32';
6
+ export const IS_MAC = process.platform === 'darwin';
7
+ export const xspawn = crossSpawn;
8
+ export const xspawnSync = crossSpawn.sync;
9
+ export const WHICH = IS_WIN ? 'where' : 'which';
10
+ export function defaultChromeBin() {
11
+ if (IS_WIN) {
12
+ const candidates = [
13
+ path.join(process.env['ProgramFiles'] || 'C:\\Program Files', 'Google', 'Chrome', 'Application', 'chrome.exe'),
14
+ path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Google', 'Chrome', 'Application', 'chrome.exe'),
15
+ path.join(process.env['LOCALAPPDATA'] || '', 'Google', 'Chrome', 'Application', 'chrome.exe'),
16
+ ];
17
+ for (const c of candidates)
18
+ if (c && fs.existsSync(c))
19
+ return c;
20
+ return candidates[0] ?? 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
21
+ }
22
+ if (IS_MAC)
23
+ return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
24
+ for (const c of ['/usr/bin/google-chrome', '/usr/bin/chromium', '/usr/bin/chromium-browser']) {
25
+ if (fs.existsSync(c))
26
+ return c;
27
+ }
28
+ return '/usr/bin/google-chrome';
29
+ }
30
+ export function isChromeRunning() {
31
+ if (IS_WIN) {
32
+ const r = nodeSpawnSync('tasklist', ['/FI', 'IMAGENAME eq chrome.exe', '/NH', '/FO', 'CSV'], { stdio: 'pipe' });
33
+ if (r.status !== 0)
34
+ return false;
35
+ const out = r.stdout?.toString() || '';
36
+ return out.toLowerCase().includes('chrome.exe');
37
+ }
38
+ if (IS_MAC) {
39
+ const r = nodeSpawnSync('pgrep', ['-f', 'Google Chrome.app/Contents/MacOS/Google Chrome'], { stdio: 'pipe' });
40
+ return r.status === 0 && (r.stdout?.toString().trim().length ?? 0) > 0;
41
+ }
42
+ const r = nodeSpawnSync('pgrep', ['-f', 'chrome'], { stdio: 'pipe' });
43
+ return r.status === 0 && (r.stdout?.toString().trim().length ?? 0) > 0;
44
+ }
45
+ export const QUIT_CHROME_HINT = IS_WIN
46
+ ? 'Close all Chrome windows (or end chrome.exe in Task Manager).'
47
+ : IS_MAC
48
+ ? 'Cmd+Q on the Chrome menu, then close Activity Monitor entries if any.'
49
+ : 'Close all Chrome windows or `pkill chrome`.';
50
+ export { nodeSpawn, nodeSpawnSync };
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
4
  import crypto from 'node:crypto';
5
- import { spawn } from 'node:child_process';
5
+ import { xspawn } from "./cross-platform.js";
6
6
  import { createBrowser } from "./browser.js";
7
7
  import { sessionDir, saveIteration } from "./artifact-store.js";
8
8
  import { upsertSession, appendHistory, getSession } from "./session-store.js";
@@ -574,7 +574,7 @@ export class DesignerController {
574
574
  const buf = Buffer.from(await res.arrayBuffer());
575
575
  fs.writeFileSync(tgzPath, buf);
576
576
  await new Promise((resolve, reject) => {
577
- const child = spawn('tar', ['-xzf', tgzPath, '-C', bundleDir], { stdio: 'pipe' });
577
+ const child = xspawn('tar', ['-xzf', tgzPath, '-C', bundleDir], { stdio: 'pipe' });
578
578
  let err = '';
579
579
  child.stderr.on('data', (d) => (err += d.toString()));
580
580
  child.on('close', (code) => code === 0 ? resolve() : reject(new Error(`tar exited ${code}: ${err}`)));
@@ -7,7 +7,8 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
7
7
  import { z } from 'zod';
8
8
  import { DesignerController } from "./designer-controller.js";
9
9
  import { sessionDir } from "./artifact-store.js";
10
- const server = new McpServer({ name: 'designer', version: '0.3.0' });
10
+ import { PACKAGE_VERSION } from "./package-meta.js";
11
+ const server = new McpServer({ name: 'designer', version: PACKAGE_VERSION });
11
12
  const controllers = new Map();
12
13
  function getController(key) {
13
14
  const k = key || 'default';
@@ -0,0 +1,12 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { REPO_ROOT } from "./repo-root.js";
4
+ function readPackageMetadata() {
5
+ const raw = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8'));
6
+ return {
7
+ name: typeof raw.name === 'string' ? raw.name : 'designer',
8
+ version: typeof raw.version === 'string' ? raw.version : '0.0.0'
9
+ };
10
+ }
11
+ export const PACKAGE_METADATA = readPackageMetadata();
12
+ export const PACKAGE_VERSION = PACKAGE_METADATA.version;
package/dist/repo-root.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
3
4
  function findRepoRoot() {
4
- let dir = path.dirname(new URL(import.meta.url).pathname);
5
+ let dir = path.dirname(fileURLToPath(import.meta.url));
5
6
  for (let i = 0; i < 8; i++) {
6
7
  if (fs.existsSync(path.join(dir, 'package.json')))
7
8
  return dir;
@@ -10,6 +11,6 @@ function findRepoRoot() {
10
11
  break;
11
12
  dir = parent;
12
13
  }
13
- throw new Error('repo-root: could not find package.json walking up from ' + new URL(import.meta.url).pathname);
14
+ throw new Error('repo-root: could not find package.json walking up from ' + fileURLToPath(import.meta.url));
14
15
  }
15
16
  export const REPO_ROOT = findRepoRoot();
package/dist/setup.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
- import { spawn, spawnSync } from 'node:child_process';
4
+ import { spawn } from 'node:child_process';
5
5
  import { createHash } from 'node:crypto';
6
6
  import { REPO_ROOT } from "./repo-root.js";
7
+ import { defaultChromeBin, isChromeRunning, xspawnSync, WHICH, IS_WIN, QUIT_CHROME_HINT } from "./cross-platform.js";
7
8
  import { createBrowser } from "./browser.js";
8
9
  const SKILL_SRC = path.join(REPO_ROOT, 'skills', 'designer-loop', 'SKILL.md');
9
10
  const SKILL_DEST_DIR = path.join(os.homedir(), '.claude', 'skills', 'designer-loop');
10
11
  const SKILL_DEST = path.join(SKILL_DEST_DIR, 'SKILL.md');
11
- const CHROME_BIN = process.env.CHROME_BIN || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
12
+ const CHROME_BIN = process.env.CHROME_BIN || defaultChromeBin();
12
13
  const DEFAULT_PORT = process.env.DESIGNER_CDP || '9222';
13
14
  const PROFILE = path.join(os.homedir(), '.chrome-designer-profile');
14
15
  function log(stage, status, msg) {
@@ -32,13 +33,14 @@ async function verifySignedIn(browser) {
32
33
  return browser.evalValue(js).catch(() => false);
33
34
  }
34
35
  function chromeRunning() {
35
- const r = spawnSync('pgrep', ['-f', 'Google Chrome.app/Contents/MacOS/Google Chrome'], { stdio: 'pipe' });
36
- return r.status === 0 && (r.stdout?.toString().trim().length ?? 0) > 0;
36
+ return isChromeRunning();
37
37
  }
38
38
  function cdpChromeProfileStatus(port) {
39
39
  if (!/^\d+$/.test(port))
40
40
  return 'unknown';
41
- const r = spawnSync('sh', ['-c', `ps -Axww -o command | grep -- '--remote-debugging-port=${port}' | grep -v grep`], { stdio: 'pipe' });
41
+ if (IS_WIN)
42
+ return 'unknown';
43
+ const r = xspawnSync('sh', ['-c', `ps -Axww -o command | grep -- '--remote-debugging-port=${port}' | grep -v grep`], { stdio: 'pipe' });
42
44
  if (r.status !== 0)
43
45
  return 'unknown';
44
46
  const out = r.stdout?.toString() ?? '';
@@ -107,7 +109,7 @@ async function step1NpmInstall() {
107
109
  else {
108
110
  log('deps', 'wait', 'running npm install...');
109
111
  }
110
- const r = spawnSync('npm', ['install'], { cwd: REPO_ROOT, stdio: 'inherit' });
112
+ const r = xspawnSync('npm', ['install'], { cwd: REPO_ROOT, stdio: 'inherit' });
111
113
  if (r.status !== 0) {
112
114
  log('deps', 'fail', `npm install exited ${r.status}`);
113
115
  return false;
@@ -116,9 +118,9 @@ async function step1NpmInstall() {
116
118
  return true;
117
119
  }
118
120
  function step2AgentBrowser() {
119
- const r = spawnSync('agent-browser', ['--version'], { stdio: 'pipe' });
121
+ const r = xspawnSync('agent-browser', ['--version'], { stdio: 'pipe' });
120
122
  if (r.status !== 0) {
121
- log('agent-browser', 'fail', 'not found on PATH. Install: brew install agent-browser OR npm i -g agent-browser');
123
+ log('agent-browser', 'fail', 'not found on PATH. Install: npm i -g agent-browser');
122
124
  return false;
123
125
  }
124
126
  log('agent-browser', 'ok', r.stdout?.toString().trim() || 'present');
@@ -144,11 +146,11 @@ async function step3Chrome(port) {
144
146
  }
145
147
  }
146
148
  if (chromeRunning()) {
147
- log('chrome', 'wait', 'A non-debug Chrome is running. Quit it (Cmd+Q on the Chrome menu, then close Activity Monitor entries if any). I am polling.');
149
+ log('chrome', 'wait', `A non-debug Chrome is running. ${QUIT_CHROME_HINT} I am polling.`);
148
150
  const quit = await pollUntil('chrome', () => !chromeRunning(), {
149
151
  intervalMs: 1000,
150
152
  timeoutMs: 5 * 60_000,
151
- reminder: 'Still waiting for Chrome to fully quit. Cmd+Q on Chrome.'
153
+ reminder: `Still waiting for Chrome to fully quit. ${QUIT_CHROME_HINT}`
152
154
  });
153
155
  if (!quit) {
154
156
  log('chrome', 'fail', 'Timed out waiting for Chrome to quit. Quit manually then re-run setup.');
@@ -171,7 +173,8 @@ async function step3Chrome(port) {
171
173
  reminder: `Waiting for CDP at :${port}...`
172
174
  });
173
175
  if (!up) {
174
- log('chrome', 'fail', 'Chrome launched but CDP did not come up. Try `./scripts/designer-chrome.sh` manually.');
176
+ const fallback = IS_WIN ? 'scripts\\designer-chrome.ps1' : './scripts/designer-chrome.sh';
177
+ log('chrome', 'fail', `Chrome launched but CDP did not come up. Try \`${fallback}\` manually.`);
175
178
  return false;
176
179
  }
177
180
  log('chrome', 'ok', `CDP up on :${port}`);
@@ -217,25 +220,21 @@ function step5Skill() {
217
220
  return true;
218
221
  }
219
222
  function step6Mcp(port) {
220
- const claudeBin = spawnSync('which', ['claude'], { stdio: 'pipe' });
223
+ const claudeBin = xspawnSync(WHICH, ['claude'], { stdio: 'pipe' });
221
224
  if (claudeBin.status !== 0) {
222
225
  log('mcp', 'wait', 'claude CLI not on PATH; skipping MCP registration. Install Claude Code to register.');
223
226
  return true;
224
227
  }
225
- const list = spawnSync('claude', ['mcp', 'list'], { stdio: 'pipe' });
228
+ const list = xspawnSync('claude', ['mcp', 'list'], { stdio: 'pipe' });
226
229
  const stdout = list.stdout?.toString() || '';
227
230
  if (/(\s|^)designer\b/i.test(stdout)) {
228
231
  log('mcp', 'ok', 'Already registered.');
229
232
  return true;
230
233
  }
231
- const wrapper = path.join(REPO_ROOT, 'bin', 'designer');
232
- if (!fs.existsSync(wrapper)) {
233
- log('mcp', 'fail', `Missing wrapper ${wrapper}`);
234
- return false;
235
- }
236
- const cmd = ['mcp', 'add', '--scope', 'user', '--transport', 'stdio', 'designer', '--', 'env', `DESIGNER_CDP=${port}`, wrapper, 'mcp', 'serve'];
234
+ const envFlags = port === '9222' ? [] : ['-e', `DESIGNER_CDP=${port}`];
235
+ const cmd = ['mcp', 'add', '--scope', 'user', '--transport', 'stdio', ...envFlags, 'designer', '--', 'designer', 'mcp', 'serve'];
237
236
  log('mcp', 'wait', `Registering: claude ${cmd.join(' ')}`);
238
- const reg = spawnSync('claude', cmd, { stdio: 'inherit' });
237
+ const reg = xspawnSync('claude', cmd, { stdio: 'inherit' });
239
238
  if (reg.status !== 0) {
240
239
  log('mcp', 'fail', `claude mcp add exited ${reg.status}. Run manually:\n claude ${cmd.join(' ')}`);
241
240
  return false;
package/dist/tasting.js CHANGED
@@ -1,6 +1,27 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { spawn } from 'node:child_process';
4
+ import { IS_WIN, IS_MAC, xspawn, xspawnSync } from "./cross-platform.js";
5
+ function findPython() {
6
+ const candidates = IS_WIN ? ['py', 'python', 'python3'] : ['python3', 'python'];
7
+ for (const c of candidates) {
8
+ const r = xspawnSync(c, ['--version'], { stdio: 'pipe' });
9
+ if (r.status === 0)
10
+ return c;
11
+ }
12
+ return null;
13
+ }
14
+ function openUrl(url) {
15
+ if (IS_WIN) {
16
+ spawn('cmd', ['/c', 'start', '""', url], { stdio: 'ignore', detached: true }).unref();
17
+ }
18
+ else if (IS_MAC) {
19
+ spawn('open', [url], { stdio: 'ignore', detached: true }).unref();
20
+ }
21
+ else {
22
+ spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();
23
+ }
24
+ }
4
25
  export function writeTastingHtml({ projectDir, variants, outPath, title = 'Tasting' }) {
5
26
  const target = outPath || path.join(projectDir, 'tasting.html');
6
27
  const html = renderTastingHtml({ variants, title });
@@ -85,7 +106,11 @@ function escapeHtml(s) {
85
106
  const servers = new Map();
86
107
  export async function serveAndOpen(projectDir, { file = 'tasting.html', port } = {}) {
87
108
  const chosenPort = port || (await pickFreePort(8765));
88
- const child = spawn('python3', ['-m', 'http.server', String(chosenPort)], {
109
+ const python = findPython();
110
+ if (!python) {
111
+ throw new Error('tasting requires Python 3. Install python3 (macOS/Linux) or Python 3 from python.org (Windows).');
112
+ }
113
+ const child = xspawn(python, ['-m', 'http.server', String(chosenPort)], {
89
114
  cwd: projectDir,
90
115
  stdio: 'ignore',
91
116
  detached: true
@@ -93,7 +118,7 @@ export async function serveAndOpen(projectDir, { file = 'tasting.html', port } =
93
118
  child.unref();
94
119
  await sleep(500);
95
120
  const url = `http://127.0.0.1:${chosenPort}/${encodeURI(file)}`;
96
- spawn('open', [url], { stdio: 'ignore', detached: true }).unref();
121
+ openUrl(url);
97
122
  const pid = child.pid ?? -1;
98
123
  servers.set(projectDir, { port: chosenPort, pid });
99
124
  return { url, port: chosenPort, pid };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pro-vi/designer",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "type": "module",
5
5
  "description": "MCP + CLI for autonomous iteration of claude.ai/design — drives the design surface via agent-browser, downloads handoff bundles, and exposes a tasting harness for full-viewport variant comparison.",
6
6
  "license": "MIT",
@@ -23,13 +23,14 @@
23
23
  "anthropic"
24
24
  ],
25
25
  "bin": {
26
- "designer": "./bin/designer"
26
+ "designer": "./bin/designer.mjs"
27
27
  },
28
28
  "scripts": {
29
29
  "mcp": "tsx mcp-server.ts",
30
30
  "cli": "tsx cli.ts",
31
31
  "setup": "tsx cli.ts setup",
32
32
  "doctor": "tsx cli.ts doctor",
33
+ "test": "npm run build && node --test tests/cli-metadata.test.mjs",
33
34
  "check": "tsc --noEmit",
34
35
  "build": "tsc -p tsconfig.build.json",
35
36
  "prepack": "npm run check && npm run build",
@@ -42,9 +43,11 @@
42
43
  "dependencies": {
43
44
  "@anthropic-ai/sdk": "^0.102.0",
44
45
  "@modelcontextprotocol/sdk": "^1.26.0",
46
+ "cross-spawn": "^7.0.6",
45
47
  "zod": "^4.4.3"
46
48
  },
47
49
  "devDependencies": {
50
+ "@types/cross-spawn": "^6.0.6",
48
51
  "@types/node": "^25.6.0",
49
52
  "tsx": "^4.21.0",
50
53
  "typescript": "^6.0.3"
@@ -54,13 +57,15 @@
54
57
  },
55
58
  "os": [
56
59
  "darwin",
57
- "linux"
60
+ "linux",
61
+ "win32"
58
62
  ],
59
63
  "files": [
60
64
  "dist/",
61
65
  "bin/",
62
66
  "skills/",
63
67
  "scripts/designer-chrome.sh",
68
+ "scripts/designer-chrome.ps1",
64
69
  "scripts/postinstall.mjs",
65
70
  "selectors.json",
66
71
  "README.md",
@@ -0,0 +1,50 @@
1
+ # Launch a Chrome instance with remote debugging enabled, in a dedicated
2
+ # user-data-dir so the default profile's debug-port lockdown (Chrome 136+)
3
+ # doesn't block us. Sign in to Claude once inside the launched window;
4
+ # the profile persists.
5
+ #
6
+ # PowerShell equivalent of scripts/designer-chrome.sh for Windows users.
7
+
8
+ $ErrorActionPreference = 'Stop'
9
+
10
+ $Port = if ($env:DESIGNER_CDP) { $env:DESIGNER_CDP } else { '9222' }
11
+ $Profile = Join-Path $env:USERPROFILE '.chrome-designer-profile'
12
+
13
+ # Default Chrome locations on Windows. Override with $env:CHROME_BIN.
14
+ $DefaultChromes = @(
15
+ (Join-Path ${env:ProgramFiles} 'Google\Chrome\Application\chrome.exe'),
16
+ (Join-Path ${env:ProgramFiles(x86)} 'Google\Chrome\Application\chrome.exe'),
17
+ (Join-Path $env:LOCALAPPDATA 'Google\Chrome\Application\chrome.exe')
18
+ )
19
+ $Chrome = if ($env:CHROME_BIN) { $env:CHROME_BIN } else { $DefaultChromes | Where-Object { Test-Path $_ } | Select-Object -First 1 }
20
+
21
+ if (-not $Chrome -or -not (Test-Path $Chrome)) {
22
+ Write-Error "[designer-chrome] Chrome not found. Set `$env:CHROME_BIN to override."
23
+ exit 1
24
+ }
25
+
26
+ # CDP already listening?
27
+ try {
28
+ $null = Invoke-WebRequest -UseBasicParsing -Uri "http://127.0.0.1:$Port/json/version" -TimeoutSec 2
29
+ Write-Host "[designer-chrome] CDP already listening on port $Port - nothing to do."
30
+ Write-Host " curl http://127.0.0.1:$Port/json/version"
31
+ exit 0
32
+ } catch {
33
+ # not running - continue
34
+ }
35
+
36
+ # Warn if a non-debug Chrome is up
37
+ if (Get-Process -Name chrome -ErrorAction SilentlyContinue) {
38
+ Write-Warning "[designer-chrome] Chrome is already running."
39
+ Write-Warning " If it's NOT a debug-mode Chrome, the launched window may not get the debug port."
40
+ Write-Warning " Close existing Chrome windows first, or accept the risk and continue."
41
+ }
42
+
43
+ Write-Host "[designer-chrome] Launching: $Chrome --remote-debugging-port=$Port --user-data-dir=$Profile"
44
+ Write-Host "[designer-chrome] Sign in to claude.ai in the new window. Then navigate to https://claude.ai/design."
45
+ Write-Host "[designer-chrome] When done, leave this window open. The CDP server runs as long as Chrome runs."
46
+
47
+ & $Chrome `
48
+ "--remote-debugging-port=$Port" `
49
+ "--user-data-dir=$Profile" `
50
+ "https://claude.ai/design"
package/bin/designer DELETED
@@ -1,35 +0,0 @@
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