@pro-vi/designer 0.3.2 → 0.3.4
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/dist/browser.js +12 -1
- package/dist/cli.js +0 -3
- package/dist/designer-controller.js +63 -16
- package/dist/scripts/probe.js +14 -3
- package/dist/ui-anchors.js +39 -0
- package/package.json +1 -1
package/dist/browser.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
const BIN = process.env.DESIGNER_AGENT_BROWSER_BIN || 'agent-browser';
|
|
3
3
|
const DEFAULT_SESSION = process.env.DESIGNER_SESSION_NAME || 'designer';
|
|
4
|
-
const CDP = process.env.DESIGNER_CDP
|
|
4
|
+
const CDP = process.env.DESIGNER_CDP ?? '9222';
|
|
5
5
|
export function createBrowser({ session = DEFAULT_SESSION, headed = true, timeoutMs = 30_000, cdp = CDP } = {}) {
|
|
6
6
|
const baseEnv = {
|
|
7
7
|
...process.env,
|
|
@@ -53,6 +53,17 @@ export function createBrowser({ session = DEFAULT_SESSION, headed = true, timeou
|
|
|
53
53
|
close: () => run(['close']).catch(() => null),
|
|
54
54
|
url: () => run(['get', 'url']),
|
|
55
55
|
title: () => run(['get', 'title']),
|
|
56
|
+
tabs: async () => {
|
|
57
|
+
const out = await run(['tab', 'list', '--json']);
|
|
58
|
+
const env = JSON.parse(out);
|
|
59
|
+
if (env.success === false) {
|
|
60
|
+
throw new Error(`agent-browser tab list failed: ${JSON.stringify(env.error)}`);
|
|
61
|
+
}
|
|
62
|
+
return env.data?.tabs ?? [];
|
|
63
|
+
},
|
|
64
|
+
activateTab: async (index) => {
|
|
65
|
+
await run(['tab', String(index)]);
|
|
66
|
+
},
|
|
56
67
|
snapshot: ({ interactive = true, scope } = {}) => {
|
|
57
68
|
const args = ['snapshot', '--json'];
|
|
58
69
|
if (interactive)
|
package/dist/cli.js
CHANGED
|
@@ -517,9 +517,6 @@ async function checkAgentBrowser() {
|
|
|
517
517
|
}
|
|
518
518
|
async function checkCdp() {
|
|
519
519
|
const port = process.env.DESIGNER_CDP || '9222';
|
|
520
|
-
if (!process.env.DESIGNER_CDP) {
|
|
521
|
-
return { name: `CDP at port ${port}`, status: 'warn', detail: 'DESIGNER_CDP not set; defaulting to 9222. export DESIGNER_CDP=9222 to silence.' };
|
|
522
|
-
}
|
|
523
520
|
try {
|
|
524
521
|
const res = await fetch(`http://127.0.0.1:${port}/json/version`);
|
|
525
522
|
if (!res.ok)
|
|
@@ -90,19 +90,55 @@ export class DesignerController {
|
|
|
90
90
|
}
|
|
91
91
|
throw new Error(`Unknown action: ${action}`);
|
|
92
92
|
}
|
|
93
|
+
async selectMatchingTab() {
|
|
94
|
+
const tabs = await this.browser.tabs().catch(() => []);
|
|
95
|
+
if (tabs.length === 0)
|
|
96
|
+
return { matched: false, candidates: 0 };
|
|
97
|
+
const stored = getSession(this.key);
|
|
98
|
+
const targetRoot = stored?.designUrl?.split('?')[0];
|
|
99
|
+
const candidates = tabs.filter((t) => {
|
|
100
|
+
if (t.type !== 'page' || !t.url)
|
|
101
|
+
return false;
|
|
102
|
+
if (targetRoot)
|
|
103
|
+
return t.url.startsWith(targetRoot);
|
|
104
|
+
return /^https:\/\/claude\.ai\/design(\/|$|\?)/.test(t.url);
|
|
105
|
+
});
|
|
106
|
+
if (candidates.length === 0)
|
|
107
|
+
return { matched: false, candidates: 0 };
|
|
108
|
+
candidates.sort((a, b) => (Number(b.active) - Number(a.active)) || (a.index - b.index));
|
|
109
|
+
for (const cand of candidates) {
|
|
110
|
+
await this.browser.activateTab(cand.index).catch(() => null);
|
|
111
|
+
const composerOk = await this.browser.isVisible(this.selectors.composer.promptTextarea).catch(() => false);
|
|
112
|
+
const homeOk = this.selectors.login.signedInIndicator
|
|
113
|
+
? await this.browser.isVisible(this.selectors.login.signedInIndicator).catch(() => false)
|
|
114
|
+
: false;
|
|
115
|
+
if (composerOk || homeOk) {
|
|
116
|
+
upsertSession(this.key, { lastUrl: await this.currentUrl() });
|
|
117
|
+
return { matched: true, candidates: candidates.length };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { matched: false, candidates: candidates.length };
|
|
121
|
+
}
|
|
93
122
|
async ensureReady() {
|
|
94
123
|
await ensureCdpUp();
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
await this.
|
|
98
|
-
|
|
124
|
+
const picked = await this.selectMatchingTab();
|
|
125
|
+
if (picked.matched) {
|
|
126
|
+
return { ok: true, url: await this.currentUrl(), inSession: await this.isInSession() };
|
|
127
|
+
}
|
|
128
|
+
if (picked.candidates === 0) {
|
|
129
|
+
const u = await this.currentUrl();
|
|
130
|
+
if (!/claude\.ai\/design/.test(u)) {
|
|
131
|
+
await this.browser.open(DESIGN_HOME);
|
|
132
|
+
await this.browser.waitLoad('networkidle').catch(() => null);
|
|
133
|
+
}
|
|
99
134
|
}
|
|
100
135
|
const homeOk = this.selectors.login.signedInIndicator
|
|
101
136
|
? await this.browser.isVisible(this.selectors.login.signedInIndicator).catch(() => false)
|
|
102
137
|
: false;
|
|
103
138
|
const sessionOk = await this.browser.isVisible(this.selectors.composer.promptTextarea).catch(() => false);
|
|
104
139
|
if (!homeOk && !sessionOk) {
|
|
105
|
-
|
|
140
|
+
const suffix = picked.candidates > 0 ? ` (checked ${picked.candidates} tab(s))` : '';
|
|
141
|
+
throw new Error(`Not signed in to claude.ai/design, or on an unrecognized page${suffix}. Sign in in the CDP-attached Chrome.`);
|
|
106
142
|
}
|
|
107
143
|
upsertSession(this.key, { lastUrl: await this.currentUrl() });
|
|
108
144
|
return { ok: true, url: await this.currentUrl(), inSession: await this.isInSession() };
|
|
@@ -329,17 +365,25 @@ export class DesignerController {
|
|
|
329
365
|
})()`).catch(() => null);
|
|
330
366
|
await new Promise((r) => setTimeout(r, 600));
|
|
331
367
|
const result = await this.browser.evalValue(`(() => {
|
|
332
|
-
|
|
368
|
+
// Walk all text nodes — Claude's file panel wraps filenames in styled-
|
|
369
|
+
// component <div>s whose class hashes change across deploys. Tag-based
|
|
370
|
+
// scraping misses them; text-node walking is resilient.
|
|
333
371
|
const seen = new Set();
|
|
334
372
|
const files = [];
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
373
|
+
let designFilesLabelVisible = false;
|
|
374
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
375
|
+
let node;
|
|
376
|
+
while ((node = walker.nextNode())) {
|
|
377
|
+
const t = (node.textContent || '').trim();
|
|
378
|
+
if (t === 'Design Files') designFilesLabelVisible = true;
|
|
379
|
+
if (!/^[A-Za-z0-9 _.()\\-]+\\.(html|js|css|jsx|tsx|ts|md|json|svg)$/i.test(t)) continue;
|
|
380
|
+
if (t.length > 80 || seen.has(t)) continue;
|
|
339
381
|
seen.add(t);
|
|
340
382
|
files.push(t);
|
|
341
383
|
}
|
|
342
|
-
// Folders: rows whose sibling text is 'Folder' (a Claude-side label)
|
|
384
|
+
// Folders: rows whose sibling text is 'Folder' (a Claude-side label).
|
|
385
|
+
// Still tag-based since folder rows are structurally different —
|
|
386
|
+
// revisit if this breaks.
|
|
343
387
|
const folderSet = new Set();
|
|
344
388
|
const divs = Array.from(document.querySelectorAll('div'));
|
|
345
389
|
for (const d of divs) {
|
|
@@ -349,12 +393,15 @@ export class DesignerController {
|
|
|
349
393
|
folderSet.add(lines[0]);
|
|
350
394
|
}
|
|
351
395
|
}
|
|
352
|
-
return { files, folders: Array.from(folderSet) };
|
|
353
|
-
})()`).catch(() => ({ files: [], folders: [] }));
|
|
396
|
+
return { files, folders: Array.from(folderSet), designFilesLabelVisible };
|
|
397
|
+
})()`).catch(() => ({ files: [], folders: [], designFilesLabelVisible: false }));
|
|
398
|
+
const files = Array.isArray(result.files) ? result.files : [];
|
|
399
|
+
const folders = Array.isArray(result.folders) ? result.folders : [];
|
|
400
|
+
const emptyButLabelVisible = files.length === 0 && result.designFilesLabelVisible === true;
|
|
354
401
|
return {
|
|
355
|
-
files
|
|
356
|
-
folders
|
|
357
|
-
authoritative:
|
|
402
|
+
files,
|
|
403
|
+
folders,
|
|
404
|
+
authoritative: !emptyButLabelVisible && folders.length === 0
|
|
358
405
|
};
|
|
359
406
|
}
|
|
360
407
|
async openFile(filename) {
|
package/dist/scripts/probe.js
CHANGED
|
@@ -3,9 +3,6 @@ import { createBrowser } from "../browser.js";
|
|
|
3
3
|
const cmd = process.argv[2];
|
|
4
4
|
const arg = process.argv[3];
|
|
5
5
|
const browser = createBrowser({ headed: true });
|
|
6
|
-
if (!process.env.DESIGNER_CDP) {
|
|
7
|
-
console.error('[probe] DESIGNER_CDP not set — using agent-browser-managed session (may be blocked by Cloudflare/SSO). Prefer: export DESIGNER_CDP=9222 and relaunch Chrome with --remote-debugging-port=9222.');
|
|
8
|
-
}
|
|
9
6
|
async function main() {
|
|
10
7
|
switch (cmd) {
|
|
11
8
|
case 'login':
|
|
@@ -43,12 +40,26 @@ async function main() {
|
|
|
43
40
|
case 'close':
|
|
44
41
|
await browser.close();
|
|
45
42
|
break;
|
|
43
|
+
case 'tabs': {
|
|
44
|
+
const tabs = await browser.tabs();
|
|
45
|
+
const composer = '[data-testid="chat-composer-input"]';
|
|
46
|
+
const signedIn = '[data-testid="create-project-button"]';
|
|
47
|
+
for (const t of tabs) {
|
|
48
|
+
await browser.activateTab(t.index).catch(() => null);
|
|
49
|
+
const composerOk = await browser.isVisible(composer).catch(() => false);
|
|
50
|
+
const signedInOk = await browser.isVisible(signedIn).catch(() => false);
|
|
51
|
+
const flag = composerOk ? 'composer' : signedInOk ? 'home' : 'unrecognized';
|
|
52
|
+
console.log(`[${t.index}] active=${t.active ? 'Y' : 'N'} ${flag.padEnd(12)} ${t.url}`);
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
46
56
|
default:
|
|
47
57
|
console.log(`Usage:
|
|
48
58
|
probe.ts login open headed window for manual login
|
|
49
59
|
probe.ts open <url> navigate
|
|
50
60
|
probe.ts url print current url
|
|
51
61
|
probe.ts title print current title
|
|
62
|
+
probe.ts tabs list CDP tabs with readiness verdict
|
|
52
63
|
probe.ts snapshot [scope] interactive a11y tree (text)
|
|
53
64
|
probe.ts snapshot-json [scope] interactive a11y tree (JSON)
|
|
54
65
|
probe.ts screenshot [path] full-page screenshot
|
package/dist/ui-anchors.js
CHANGED
|
@@ -153,6 +153,45 @@ export const UI_ANCHORS = [
|
|
|
153
153
|
const ok = /[?&]file=/.test(url);
|
|
154
154
|
return { ok: true, detail: ok ? 'file param present' : '(no file open — not a regression)' };
|
|
155
155
|
}
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: 'session.fileListScrape',
|
|
159
|
+
category: 'session',
|
|
160
|
+
description: 'filename text nodes detectable (listFiles scrape still works)',
|
|
161
|
+
requires: 'session',
|
|
162
|
+
check: async (b, url) => {
|
|
163
|
+
const result = await b
|
|
164
|
+
.evalValue(`(() => {
|
|
165
|
+
const seen = new Set();
|
|
166
|
+
const files = [];
|
|
167
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
168
|
+
let node;
|
|
169
|
+
while ((node = walker.nextNode())) {
|
|
170
|
+
const t = (node.textContent || '').trim();
|
|
171
|
+
if (!/^[A-Za-z0-9 _.()\\-]+\\.(html|js|css|jsx|tsx|ts|md|json|svg)$/i.test(t)) continue;
|
|
172
|
+
if (t.length > 80 || seen.has(t)) continue;
|
|
173
|
+
seen.add(t);
|
|
174
|
+
files.push(t);
|
|
175
|
+
}
|
|
176
|
+
return { files };
|
|
177
|
+
})()`)
|
|
178
|
+
.catch(() => ({ files: [] }));
|
|
179
|
+
const files = Array.isArray(result.files) ? result.files : [];
|
|
180
|
+
if (files.length === 0) {
|
|
181
|
+
return { ok: false, detail: 'found 0 filenames — scraper regex or DOM layout regressed' };
|
|
182
|
+
}
|
|
183
|
+
const match = url.match(/[?&]file=([^&]+)/);
|
|
184
|
+
if (match && match[1]) {
|
|
185
|
+
const activeFile = decodeURIComponent(match[1]);
|
|
186
|
+
if (!files.includes(activeFile)) {
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
detail: `active file "${activeFile}" not in scrape ([${files.slice(0, 3).join(', ')}...]) — scraper missing files`
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { ok: true, detail: `${files.length} file(s) detected` };
|
|
194
|
+
}
|
|
156
195
|
}
|
|
157
196
|
];
|
|
158
197
|
export async function runHealth(browser, opts = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pro-vi/designer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
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",
|