@pro-vi/designer 0.3.7 → 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 +60 -11
- 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/dist/ui-anchors.js +18 -4
- package/package.json +9 -4
- package/scripts/designer-chrome.ps1 +50 -0
- package/selectors.json +1 -1
- 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";
|
|
@@ -195,17 +195,42 @@ export class DesignerController {
|
|
|
195
195
|
const { promptTextarea, sendButton } = this.selectors.composer;
|
|
196
196
|
await this.browser.waitFor(promptTextarea);
|
|
197
197
|
await this.browser.evalValue(`(() => {
|
|
198
|
-
const
|
|
199
|
-
if (!
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
198
|
+
const el = document.querySelector(${JSON.stringify(promptTextarea)});
|
|
199
|
+
if (!el) throw new Error('composer input not found');
|
|
200
|
+
const text = ${JSON.stringify(prompt)};
|
|
201
|
+
if (el instanceof HTMLTextAreaElement) {
|
|
202
|
+
// Bypass React's value ownership via the native setter, then fire a
|
|
203
|
+
// bubbling input event.
|
|
204
|
+
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
|
|
205
|
+
setter.call(el, text);
|
|
206
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
207
|
+
el.focus();
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
if (el.isContentEditable) {
|
|
211
|
+
// Deliver the text as a synthetic paste so the editor's own paste
|
|
212
|
+
// pipeline updates its internal state; execCommand('insertText')
|
|
213
|
+
// flattens multi-line prompts into one paragraph.
|
|
214
|
+
el.focus();
|
|
215
|
+
const sel = window.getSelection();
|
|
216
|
+
const range = document.createRange();
|
|
217
|
+
range.selectNodeContents(el);
|
|
218
|
+
sel.removeAllRanges();
|
|
219
|
+
sel.addRange(range);
|
|
220
|
+
const dt = new DataTransfer();
|
|
221
|
+
dt.setData('text/plain', text);
|
|
222
|
+
const unhandled = el.dispatchEvent(new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true }));
|
|
223
|
+
if (unhandled) {
|
|
224
|
+
// No editor intercepted the paste — plain contenteditable fallback.
|
|
225
|
+
document.execCommand('insertText', false, text);
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
throw new Error('composer input is neither textarea nor contenteditable: ' + el.tagName);
|
|
205
230
|
})()`);
|
|
206
231
|
for (let i = 0; i < 30; i++) {
|
|
207
232
|
await new Promise((r) => setTimeout(r, 150));
|
|
208
|
-
const disabled = await this.browser.evalValue(`(() => { const b = document.querySelector(${JSON.stringify(sendButton)}); return !b || b.disabled; })()`);
|
|
233
|
+
const disabled = await this.browser.evalValue(`(() => { const b = document.querySelector(${JSON.stringify(sendButton)}); return !b || b.disabled || b.getAttribute('aria-disabled') === 'true'; })()`);
|
|
209
234
|
if (!disabled)
|
|
210
235
|
break;
|
|
211
236
|
}
|
|
@@ -520,7 +545,9 @@ export class DesignerController {
|
|
|
520
545
|
if (!opened)
|
|
521
546
|
await this._clickButtonByText(/^Export$/);
|
|
522
547
|
await new Promise((r) => setTimeout(r, 400));
|
|
523
|
-
await this.
|
|
548
|
+
const viaSendTo = await this._clickClaudeCodeSendTo().catch(() => false);
|
|
549
|
+
if (!viaSendTo)
|
|
550
|
+
await this._clickButtonByText(/handoff to claude code/i);
|
|
524
551
|
let handoffUrl = '';
|
|
525
552
|
for (let i = 0; i < 30; i++) {
|
|
526
553
|
await new Promise((r) => setTimeout(r, 300));
|
|
@@ -547,7 +574,7 @@ export class DesignerController {
|
|
|
547
574
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
548
575
|
fs.writeFileSync(tgzPath, buf);
|
|
549
576
|
await new Promise((resolve, reject) => {
|
|
550
|
-
const child =
|
|
577
|
+
const child = xspawn('tar', ['-xzf', tgzPath, '-C', bundleDir], { stdio: 'pipe' });
|
|
551
578
|
let err = '';
|
|
552
579
|
child.stderr.on('data', (d) => (err += d.toString()));
|
|
553
580
|
child.on('close', (code) => code === 0 ? resolve() : reject(new Error(`tar exited ${code}: ${err}`)));
|
|
@@ -583,6 +610,28 @@ export class DesignerController {
|
|
|
583
610
|
return true;
|
|
584
611
|
})()`);
|
|
585
612
|
}
|
|
613
|
+
async _clickClaudeCodeSendTo() {
|
|
614
|
+
const tabClicked = await this.browser.evalValue(`(() => {
|
|
615
|
+
const tab = Array.from(document.querySelectorAll('button[role="tab"]')).find(t => /send to/i.test(t.textContent || ''));
|
|
616
|
+
if (!tab) return false;
|
|
617
|
+
tab.click();
|
|
618
|
+
return true;
|
|
619
|
+
})()`);
|
|
620
|
+
if (!tabClicked)
|
|
621
|
+
return false;
|
|
622
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
623
|
+
return this.browser.evalValue(`(() => {
|
|
624
|
+
const sends = Array.from(document.querySelectorAll('button')).filter(b => (b.textContent || '').trim() === 'Send');
|
|
625
|
+
const target = sends.find(b => {
|
|
626
|
+
let row = b;
|
|
627
|
+
for (let i = 0; i < 3 && row.parentElement; i++) row = row.parentElement;
|
|
628
|
+
return /claude code/i.test(row.textContent || '');
|
|
629
|
+
});
|
|
630
|
+
if (!target) return false;
|
|
631
|
+
target.click();
|
|
632
|
+
return true;
|
|
633
|
+
})()`);
|
|
634
|
+
}
|
|
586
635
|
async _dialogText() {
|
|
587
636
|
return ((await this.browser
|
|
588
637
|
.evalValue(`(() => {
|
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/dist/ui-anchors.js
CHANGED
|
@@ -70,7 +70,7 @@ export const UI_ANCHORS = [
|
|
|
70
70
|
category: 'session',
|
|
71
71
|
description: 'send button',
|
|
72
72
|
requires: 'session',
|
|
73
|
-
check: async (b) => ({ ok: await hasSelector(b, '[data-testid="chat-send-button"]') })
|
|
73
|
+
check: async (b) => ({ ok: await hasSelector(b, '[data-testid="chat-send-button"], button[title^="Send ("]') })
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
76
|
id: 'session.htmlViewerIframe',
|
|
@@ -122,16 +122,30 @@ export const UI_ANCHORS = [
|
|
|
122
122
|
{
|
|
123
123
|
id: 'share.handoffMenuItem',
|
|
124
124
|
category: 'share',
|
|
125
|
-
description: 'Handoff-to-Claude-Code
|
|
125
|
+
description: 'Handoff-to-Claude-Code action (Share → Send to… tab → Claude Code row, or the legacy dropdown item)',
|
|
126
126
|
requires: 'session',
|
|
127
127
|
check: async (b) => {
|
|
128
128
|
const opened = await b.evalValue(`(() => { const btn = Array.from(document.querySelectorAll('button')).find(x => (x.textContent||'').trim() === 'Share'); if (!btn) return false; btn.click(); return true; })()`).catch(() => false);
|
|
129
129
|
if (!opened)
|
|
130
130
|
return { ok: false, detail: 'Share button not clickable' };
|
|
131
131
|
await new Promise((r) => setTimeout(r, 400));
|
|
132
|
-
|
|
132
|
+
let found = await hasButtonMatching(b, /handoff to claude code/i);
|
|
133
|
+
if (!found) {
|
|
134
|
+
const tabClicked = await b.evalValue(`(() => { const tab = Array.from(document.querySelectorAll('button[role="tab"]')).find(t => /send to/i.test(t.textContent || '')); if (!tab) return false; tab.click(); return true; })()`).catch(() => false);
|
|
135
|
+
if (tabClicked) {
|
|
136
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
137
|
+
found = await b.evalValue(`(() => {
|
|
138
|
+
const sends = Array.from(document.querySelectorAll('button')).filter(x => (x.textContent || '').trim() === 'Send');
|
|
139
|
+
return sends.some(x => {
|
|
140
|
+
let row = x;
|
|
141
|
+
for (let i = 0; i < 3 && row.parentElement; i++) row = row.parentElement;
|
|
142
|
+
return /claude code/i.test(row.textContent || '');
|
|
143
|
+
});
|
|
144
|
+
})()`).catch(() => false);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
133
147
|
await b.evalValue(`document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); true`).catch(() => null);
|
|
134
|
-
return { ok: found, detail: found ? undefined : 'Share opened but no
|
|
148
|
+
return { ok: found, detail: found ? undefined : 'Share opened but no Claude Code handoff action (checked legacy item and Send to… tab)' };
|
|
135
149
|
}
|
|
136
150
|
},
|
|
137
151
|
{
|
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",
|
|
@@ -40,11 +41,13 @@
|
|
|
40
41
|
"smoke": "bash scripts/install-smoke.sh"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
|
-
"@anthropic-ai/sdk": "^0.
|
|
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/selectors.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"composer": {
|
|
20
20
|
"promptTextarea": "[data-testid=\"chat-composer-input\"]",
|
|
21
|
-
"sendButton": "[data-testid=\"chat-send-button\"]",
|
|
21
|
+
"sendButton": "[data-testid=\"chat-send-button\"], button[title^=\"Send (\"]",
|
|
22
22
|
"stopButton": null,
|
|
23
23
|
"attachButton": "button[aria-label=\"Attach file\"]",
|
|
24
24
|
"modelButton": "[data-testid=\"model-selector-button\"]"
|
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
|