@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.
- package/bin/designer.mjs +36 -0
- package/dist/browser.js +4 -4
- package/dist/cdp-ensure.js +5 -8
- package/dist/cli.js +10 -5
- package/dist/cross-platform.js +50 -0
- package/dist/designer-controller.js +2 -2
- package/dist/mcp-server.js +2 -1
- package/dist/package-meta.js +12 -0
- package/dist/repo-root.js +3 -2
- package/dist/setup.js +19 -20
- package/dist/tasting.js +27 -2
- package/package.json +8 -3
- package/scripts/designer-chrome.ps1 +50 -0
- package/bin/designer +0 -35
package/bin/designer.mjs
ADDED
|
@@ -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 {
|
|
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 =
|
|
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
|
}
|
package/dist/cdp-ensure.js
CHANGED
|
@@ -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
|
|
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 ||
|
|
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 (
|
|
31
|
-
throw new Error(`CDP not up on :${PORT} and a non-debug Chrome is already running.
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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}`)));
|
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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 ' +
|
|
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
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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:
|
|
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',
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
232
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|