@pro-vi/designer 0.3.6 → 0.3.8
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/README.md +2 -1
- package/dist/cli.js +29 -10
- package/dist/designer-controller.js +58 -9
- package/dist/scripts/anchor-patcher.js +118 -0
- package/dist/scripts/auto-heal.js +660 -0
- package/dist/scripts/ci-health.js +277 -0
- package/dist/setup.js +40 -28
- package/dist/ui-anchors.js +46 -12
- package/package.json +12 -4
- package/selectors.json +1 -1
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --import tsx
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
6
|
+
import { createBrowser } from "../browser.js";
|
|
7
|
+
import { runHealth } from "../ui-anchors.js";
|
|
8
|
+
import { REPO_ROOT } from "../repo-root.js";
|
|
9
|
+
const CDP_PORT = process.env.DESIGNER_CDP || '9222';
|
|
10
|
+
const CHROME_PROFILE = path.join(os.homedir(), '.chrome-designer-profile');
|
|
11
|
+
const CHROME_APP = '/Applications/Google Chrome.app';
|
|
12
|
+
const HOME_URL = 'https://claude.ai/design';
|
|
13
|
+
const HOME_READY_SEL = '[data-testid="project-creator"]';
|
|
14
|
+
const SESSION_READY_SEL = '[data-testid="chat-composer-input"]';
|
|
15
|
+
const BROWSER_TIMEOUT_MS = 15_000;
|
|
16
|
+
function runDoctor() {
|
|
17
|
+
const bin = path.join(REPO_ROOT, 'bin', 'designer');
|
|
18
|
+
const r = spawnSync(bin, ['doctor'], { encoding: 'utf8', timeout: 60_000 });
|
|
19
|
+
return {
|
|
20
|
+
exitCode: r.status ?? -1,
|
|
21
|
+
stdout: r.stdout || '',
|
|
22
|
+
stderr: r.stderr || ''
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function pkgVersion() {
|
|
26
|
+
const p = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8'));
|
|
27
|
+
return p.version;
|
|
28
|
+
}
|
|
29
|
+
function todayUtc() {
|
|
30
|
+
return new Date().toISOString().slice(0, 10);
|
|
31
|
+
}
|
|
32
|
+
function scrubArtifact(s) {
|
|
33
|
+
if (!s)
|
|
34
|
+
return s;
|
|
35
|
+
return s
|
|
36
|
+
.replace(/\/[a-z]\/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/gi, (m) => m.slice(0, 3) + '<redacted>')
|
|
37
|
+
.replace(/(https?:\/\/[^\s?#]+)[?#][^\s]*/g, '$1')
|
|
38
|
+
.replace(/\/Users\/[^\/\s]+/g, '/Users/<redacted>')
|
|
39
|
+
.replace(/\/home\/[^\/\s]+/g, '/home/<redacted>');
|
|
40
|
+
}
|
|
41
|
+
function scrubNav(n) {
|
|
42
|
+
if (!n)
|
|
43
|
+
return n;
|
|
44
|
+
return {
|
|
45
|
+
target: scrubArtifact(n.target),
|
|
46
|
+
landedOn: scrubArtifact(n.landedOn),
|
|
47
|
+
...(n.error !== undefined ? { error: scrubArtifact(n.error) } : {})
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function ensureDir(p) {
|
|
51
|
+
fs.mkdirSync(p, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
async function pingCdp() {
|
|
54
|
+
try {
|
|
55
|
+
const ac = new AbortController();
|
|
56
|
+
const t = setTimeout(() => ac.abort(), 2000);
|
|
57
|
+
const r = await fetch(`http://127.0.0.1:${CDP_PORT}/json/version`, { signal: ac.signal });
|
|
58
|
+
clearTimeout(t);
|
|
59
|
+
if (!r.ok)
|
|
60
|
+
return { ok: false, detail: `HTTP ${r.status}` };
|
|
61
|
+
const j = await r.json().catch(() => null);
|
|
62
|
+
return { ok: !!j?.Browser, detail: j?.Browser || 'no Browser field' };
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
return { ok: false, detail: e.message };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function ensureCdp() {
|
|
69
|
+
const first = await pingCdp();
|
|
70
|
+
if (first.ok)
|
|
71
|
+
return { alive: true, attemptedRestart: false, detail: first.detail };
|
|
72
|
+
console.log(`[ci-health] CDP unreachable on :${CDP_PORT} (${first.detail}) — attempting narrow Chrome relaunch`);
|
|
73
|
+
spawn('open', [
|
|
74
|
+
'-na',
|
|
75
|
+
CHROME_APP,
|
|
76
|
+
'--args',
|
|
77
|
+
`--remote-debugging-port=${CDP_PORT}`,
|
|
78
|
+
`--user-data-dir=${CHROME_PROFILE}`
|
|
79
|
+
], { detached: true, stdio: 'ignore' }).unref();
|
|
80
|
+
for (let i = 0; i < 8; i++) {
|
|
81
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
82
|
+
const r = await pingCdp();
|
|
83
|
+
if (r.ok)
|
|
84
|
+
return { alive: true, attemptedRestart: true, detail: r.detail };
|
|
85
|
+
}
|
|
86
|
+
const final = await pingCdp();
|
|
87
|
+
return { alive: false, attemptedRestart: true, detail: final.detail };
|
|
88
|
+
}
|
|
89
|
+
function updateStreak(outDir, results) {
|
|
90
|
+
const streakPath = path.join(outDir, 'streak.json');
|
|
91
|
+
let streak = {};
|
|
92
|
+
if (fs.existsSync(streakPath)) {
|
|
93
|
+
try {
|
|
94
|
+
const raw = fs.readFileSync(streakPath, 'utf8');
|
|
95
|
+
const parsed = JSON.parse(raw);
|
|
96
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
97
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
98
|
+
if (typeof v === 'number' && Number.isFinite(v) && v >= 0)
|
|
99
|
+
streak[k] = v;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
console.log(`[ci-health] streak.json unreadable (${e.message}); resetting`);
|
|
105
|
+
streak = {};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const verdict = new Map();
|
|
109
|
+
for (const r of results) {
|
|
110
|
+
if (r.status === 'skip')
|
|
111
|
+
continue;
|
|
112
|
+
const prev = verdict.get(r.id);
|
|
113
|
+
if (r.status === 'fail') {
|
|
114
|
+
verdict.set(r.id, 'fail');
|
|
115
|
+
}
|
|
116
|
+
else if (r.status === 'ok' && prev !== 'fail') {
|
|
117
|
+
verdict.set(r.id, 'ok');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const [id, v] of verdict) {
|
|
121
|
+
if (v === 'fail') {
|
|
122
|
+
streak[id] = (streak[id] ?? 0) + 1;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
streak[id] = 0;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
fs.writeFileSync(streakPath, JSON.stringify(streak, null, 2) + '\n');
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
console.log(`[ci-health] streak.json write failed (${e.message}); continuing`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const flagged = Object.entries(streak).filter(([, n]) => n >= 2);
|
|
136
|
+
if (flagged.length > 0) {
|
|
137
|
+
console.log(`[ci-health] streak >= 2: ${flagged.map(([id, n]) => `${id}=${n}`).join(', ')}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function adaptiveWait(browser, sel, label) {
|
|
141
|
+
try {
|
|
142
|
+
await browser.waitFor(sel);
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
console.log(`[ci-health] ${label} ready-selector ${sel} not seen within ${BROWSER_TIMEOUT_MS}ms — proceeding (${e.message})`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function maybeSnapshot(browser) {
|
|
149
|
+
try {
|
|
150
|
+
const url = (await browser.url().catch(() => '')) || '';
|
|
151
|
+
const html = await browser.evalValue('document.documentElement.outerHTML').catch(() => '');
|
|
152
|
+
const dir = path.join(REPO_ROOT, 'artifacts', 'health', todayUtc());
|
|
153
|
+
ensureDir(dir);
|
|
154
|
+
const htmlPath = path.join(dir, 'page.html');
|
|
155
|
+
fs.writeFileSync(htmlPath, typeof html === 'string' ? html : '');
|
|
156
|
+
const shotPath = path.join(dir, 'page.png');
|
|
157
|
+
await browser.screenshot(shotPath, { full: true }).catch(() => null);
|
|
158
|
+
return {
|
|
159
|
+
url,
|
|
160
|
+
htmlBytes: typeof html === 'string' ? html.length : 0,
|
|
161
|
+
screenshotPath: fs.existsSync(shotPath) ? path.relative(REPO_ROOT, shotPath) : undefined
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function main() {
|
|
169
|
+
const startedAt = new Date().toISOString();
|
|
170
|
+
const isReprobe = process.env.DESIGNER_REPROBE === '1';
|
|
171
|
+
const artifactName = isReprobe ? `${todayUtc()}.reprobe.json` : `${todayUtc()}.json`;
|
|
172
|
+
const cdp = await ensureCdp();
|
|
173
|
+
if (!cdp.alive) {
|
|
174
|
+
const payload = {
|
|
175
|
+
ok: false,
|
|
176
|
+
generatedAt: startedAt,
|
|
177
|
+
finishedAt: new Date().toISOString(),
|
|
178
|
+
designerVersion: pkgVersion(),
|
|
179
|
+
reason: 'cdp-unreachable',
|
|
180
|
+
cdp,
|
|
181
|
+
hint: `Chrome with --remote-debugging-port=${CDP_PORT} could not be reached or relaunched. On the runner, run \`designer setup\` interactively to re-establish the session, then re-run this workflow.`
|
|
182
|
+
};
|
|
183
|
+
const outDir = path.join(REPO_ROOT, 'artifacts', 'health');
|
|
184
|
+
ensureDir(outDir);
|
|
185
|
+
fs.writeFileSync(path.join(outDir, artifactName), JSON.stringify(payload, null, 2));
|
|
186
|
+
console.error(`[ci-health] FAIL — CDP unreachable on :${CDP_PORT} (${cdp.detail}); restart attempted=${cdp.attemptedRestart}`);
|
|
187
|
+
process.exit(2);
|
|
188
|
+
}
|
|
189
|
+
console.log(`[ci-health] CDP alive — ${cdp.detail}${cdp.attemptedRestart ? ' (restarted)' : ''}`);
|
|
190
|
+
const doctor = runDoctor();
|
|
191
|
+
const browser = createBrowser({ session: 'designer-default', timeoutMs: BROWSER_TIMEOUT_MS });
|
|
192
|
+
let homeNav = null;
|
|
193
|
+
try {
|
|
194
|
+
await browser.open(HOME_URL);
|
|
195
|
+
await adaptiveWait(browser, HOME_READY_SEL, 'home');
|
|
196
|
+
const landedOn = (await browser.url().catch(() => '')) || '';
|
|
197
|
+
homeNav = { target: HOME_URL, landedOn };
|
|
198
|
+
console.log(`[ci-health] navigated to home — landed=${landedOn}`);
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
homeNav = { target: HOME_URL, landedOn: '', error: e.message };
|
|
202
|
+
console.log(`[ci-health] home navigation failed — ${e.message}; home anchors will fail loudly`);
|
|
203
|
+
}
|
|
204
|
+
const homeResults = await runHealth(browser, { phase: 'home' });
|
|
205
|
+
const probeUrl = process.env.DESIGNER_PROBE_PROJECT_URL;
|
|
206
|
+
let sessionNav = null;
|
|
207
|
+
let sessionResults = [];
|
|
208
|
+
if (probeUrl) {
|
|
209
|
+
try {
|
|
210
|
+
await browser.open(probeUrl);
|
|
211
|
+
await adaptiveWait(browser, SESSION_READY_SEL, 'session');
|
|
212
|
+
const landedOn = (await browser.url().catch(() => '')) || '';
|
|
213
|
+
sessionNav = { target: probeUrl, landedOn };
|
|
214
|
+
console.log(`[ci-health] navigated to canary — landed=${landedOn}`);
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
sessionNav = { target: probeUrl, landedOn: '', error: e.message };
|
|
218
|
+
console.log(`[ci-health] canary navigation failed — ${e.message}; session anchors will fail loudly`);
|
|
219
|
+
}
|
|
220
|
+
sessionResults = await runHealth(browser, { phase: 'session' });
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
console.log('[ci-health] DESIGNER_PROBE_PROJECT_URL unset — skipping session phase');
|
|
224
|
+
}
|
|
225
|
+
const results = [...homeResults, ...sessionResults];
|
|
226
|
+
const counts = {
|
|
227
|
+
ok: results.filter((r) => r.status === 'ok').length,
|
|
228
|
+
fail: results.filter((r) => r.status === 'fail').length,
|
|
229
|
+
skip: results.filter((r) => r.status === 'skip').length
|
|
230
|
+
};
|
|
231
|
+
const fail = counts.fail > 0;
|
|
232
|
+
const url = (await browser.url().catch(() => '')) || '';
|
|
233
|
+
const diag = fail ? await maybeSnapshot(browser) : null;
|
|
234
|
+
const payload = {
|
|
235
|
+
ok: !fail,
|
|
236
|
+
generatedAt: startedAt,
|
|
237
|
+
finishedAt: new Date().toISOString(),
|
|
238
|
+
designerVersion: pkgVersion(),
|
|
239
|
+
chromeUrl: scrubArtifact(url),
|
|
240
|
+
canary: scrubNav(sessionNav),
|
|
241
|
+
homeNav: scrubNav(homeNav),
|
|
242
|
+
doctor: {
|
|
243
|
+
exitCode: doctor.exitCode,
|
|
244
|
+
stdout: scrubArtifact(doctor.stdout),
|
|
245
|
+
stderr: scrubArtifact(doctor.stderr)
|
|
246
|
+
},
|
|
247
|
+
health: {
|
|
248
|
+
ok: !fail,
|
|
249
|
+
counts,
|
|
250
|
+
results
|
|
251
|
+
},
|
|
252
|
+
diagnostics: diag
|
|
253
|
+
};
|
|
254
|
+
const outDir = path.join(REPO_ROOT, 'artifacts', 'health');
|
|
255
|
+
ensureDir(outDir);
|
|
256
|
+
const outFile = path.join(outDir, artifactName);
|
|
257
|
+
fs.writeFileSync(outFile, JSON.stringify(payload, null, 2));
|
|
258
|
+
if (!isReprobe) {
|
|
259
|
+
updateStreak(outDir, results);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
console.log('[ci-health] re-probe mode — skipping updateStreak + writing to .reprobe.json');
|
|
263
|
+
}
|
|
264
|
+
const summary = `[ci-health] ${payload.ok ? 'OK' : 'FAIL'} — health ${counts.ok} ok / ${counts.fail} fail / ${counts.skip} skip · doctor exit ${doctor.exitCode} · v${payload.designerVersion}`;
|
|
265
|
+
console.log(summary);
|
|
266
|
+
if (fail) {
|
|
267
|
+
const failed = results.filter((r) => r.status === 'fail').map((r) => ` ${r.id} — ${r.detail || r.description}`);
|
|
268
|
+
console.log(failed.join('\n'));
|
|
269
|
+
}
|
|
270
|
+
console.log(`[ci-health] wrote ${path.relative(REPO_ROOT, outFile)}`);
|
|
271
|
+
if (fail)
|
|
272
|
+
process.exit(2);
|
|
273
|
+
}
|
|
274
|
+
main().catch((e) => {
|
|
275
|
+
console.error(`[ci-health] threw: ${e.message}`);
|
|
276
|
+
process.exit(3);
|
|
277
|
+
});
|
package/dist/setup.js
CHANGED
|
@@ -4,6 +4,7 @@ import os from 'node:os';
|
|
|
4
4
|
import { spawn, spawnSync } from 'node:child_process';
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
6
|
import { REPO_ROOT } from "./repo-root.js";
|
|
7
|
+
import { createBrowser } from "./browser.js";
|
|
7
8
|
const SKILL_SRC = path.join(REPO_ROOT, 'skills', 'designer-loop', 'SKILL.md');
|
|
8
9
|
const SKILL_DEST_DIR = path.join(os.homedir(), '.claude', 'skills', 'designer-loop');
|
|
9
10
|
const SKILL_DEST = path.join(SKILL_DEST_DIR, 'SKILL.md');
|
|
@@ -26,27 +27,26 @@ async function isCdpUp(port) {
|
|
|
26
27
|
return false;
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
|
-
async function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (!res.ok)
|
|
33
|
-
return null;
|
|
34
|
-
const tabs = (await res.json());
|
|
35
|
-
for (const t of tabs) {
|
|
36
|
-
if (t.url && /claude\.ai\/design/.test(t.url) && !/login/i.test(t.url)) {
|
|
37
|
-
return { url: t.url, title: t.title || '' };
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
30
|
+
async function verifySignedIn(browser) {
|
|
31
|
+
const js = '!!(document.querySelector(\'[data-testid="project-creator"]\') || document.querySelector(\'[data-testid="chat-composer-input"]\'))';
|
|
32
|
+
return browser.evalValue(js).catch(() => false);
|
|
45
33
|
}
|
|
46
34
|
function chromeRunning() {
|
|
47
35
|
const r = spawnSync('pgrep', ['-f', 'Google Chrome.app/Contents/MacOS/Google Chrome'], { stdio: 'pipe' });
|
|
48
36
|
return r.status === 0 && (r.stdout?.toString().trim().length ?? 0) > 0;
|
|
49
37
|
}
|
|
38
|
+
function cdpChromeProfileStatus(port) {
|
|
39
|
+
if (!/^\d+$/.test(port))
|
|
40
|
+
return 'unknown';
|
|
41
|
+
const r = spawnSync('sh', ['-c', `ps -Axww -o command | grep -- '--remote-debugging-port=${port}' | grep -v grep`], { stdio: 'pipe' });
|
|
42
|
+
if (r.status !== 0)
|
|
43
|
+
return 'unknown';
|
|
44
|
+
const out = r.stdout?.toString() ?? '';
|
|
45
|
+
if (!out.includes('--user-data-dir='))
|
|
46
|
+
return 'unknown';
|
|
47
|
+
const escaped = PROFILE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
48
|
+
return new RegExp(`--user-data-dir=${escaped}(?= |$)`, 'm').test(out) ? 'match' : 'mismatch';
|
|
49
|
+
}
|
|
50
50
|
async function pollUntil(name, fn, opts) {
|
|
51
51
|
const start = Date.now();
|
|
52
52
|
let emittedReminder = false;
|
|
@@ -126,8 +126,22 @@ function step2AgentBrowser() {
|
|
|
126
126
|
}
|
|
127
127
|
async function step3Chrome(port) {
|
|
128
128
|
if (await isCdpUp(port)) {
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
const profileStatus = cdpChromeProfileStatus(port);
|
|
130
|
+
if (profileStatus !== 'mismatch') {
|
|
131
|
+
log('chrome', 'ok', `CDP already up on :${port}${profileStatus === 'match' ? ' (profile matches)' : ''}`);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
log('chrome', 'wait', `A debug Chrome is on :${port} with a different --user-data-dir (expected ${PROFILE}).\n` +
|
|
135
|
+
` Quit it — I'll launch one with the right profile once it's gone. (Or set DESIGNER_CDP to a free port.)`);
|
|
136
|
+
const freed = await pollUntil('chrome', async () => !(await isCdpUp(port)), {
|
|
137
|
+
intervalMs: 1000,
|
|
138
|
+
timeoutMs: 5 * 60_000,
|
|
139
|
+
reminder: `Still waiting for the wrong-profile debug Chrome on :${port} to quit.`
|
|
140
|
+
});
|
|
141
|
+
if (!freed) {
|
|
142
|
+
log('chrome', 'fail', `Timed out waiting for the debug Chrome on :${port} to quit. Quit it manually, then re-run setup.`);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
131
145
|
}
|
|
132
146
|
if (chromeRunning()) {
|
|
133
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.');
|
|
@@ -164,23 +178,21 @@ async function step3Chrome(port) {
|
|
|
164
178
|
return true;
|
|
165
179
|
}
|
|
166
180
|
async function step4SignIn(port) {
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
log('login', 'wait', 'Sign in to Claude in the DEBUG Chrome window I just opened (it is a separate window with no extensions/bookmarks — NOT your normal Chrome; the two have separate cookie jars). Then navigate to claude.ai/design. I am polling.');
|
|
173
|
-
const ok = await pollUntil('login', async () => (await getDesignTab(port)) !== null, {
|
|
181
|
+
const browser = createBrowser({ session: 'designer-setup', cdp: port });
|
|
182
|
+
await browser.open('https://claude.ai/design').catch(() => undefined);
|
|
183
|
+
await sleep(2500);
|
|
184
|
+
const ok = await pollUntil('login', () => verifySignedIn(browser), {
|
|
174
185
|
intervalMs: 2000,
|
|
175
186
|
timeoutMs: 10 * 60_000,
|
|
176
|
-
reminder: '
|
|
187
|
+
reminder: 'Sign in to Claude in the DEBUG Chrome window I just opened (a separate window with no extensions/bookmarks — NOT your normal Chrome; the two have separate cookie jars). Then return to claude.ai/design. I am polling.',
|
|
177
188
|
hint60s: "If Chrome shows a Google 'new device' or 2FA prompt, complete that first — setup is waiting on you."
|
|
178
189
|
});
|
|
179
190
|
if (!ok) {
|
|
180
|
-
log('login', 'fail', 'Timed out waiting for
|
|
191
|
+
log('login', 'fail', 'Timed out waiting for a signed-in claude.ai/design session. Re-run setup when ready.');
|
|
181
192
|
return false;
|
|
182
193
|
}
|
|
183
|
-
|
|
194
|
+
const url = (await browser.url().catch(() => '')) || 'claude.ai/design';
|
|
195
|
+
log('login', 'ok', `Signed in. Tab on ${url.replace(/\?.*$/, '')}`);
|
|
184
196
|
return true;
|
|
185
197
|
}
|
|
186
198
|
function step5Skill() {
|
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',
|
|
@@ -108,15 +108,9 @@ export const UI_ANCHORS = [
|
|
|
108
108
|
{
|
|
109
109
|
id: 'session.chatTurnPrefix',
|
|
110
110
|
category: 'pattern',
|
|
111
|
-
description:
|
|
111
|
+
description: 'chat-messages renders >=2 turn rows (data-index API)',
|
|
112
112
|
requires: 'session',
|
|
113
|
-
check: async (b) => {
|
|
114
|
-
const sample = await b.evalValue(`(() => { const c = document.querySelector('[data-testid="chat-messages"]'); const inner = c && c.children[0]; if (!inner) return ''; return Array.from(inner.children).slice(0, 3).map(d => (d.innerText||'').slice(0, 40)).join('|'); })()`).catch(() => '');
|
|
115
|
-
if (!sample)
|
|
116
|
-
return { ok: false, detail: 'no chat turns' };
|
|
117
|
-
const ok = /(^|\|)(You|Claude)(\\n|\n|$)/.test(sample) || /You\n|Claude\n/.test(sample);
|
|
118
|
-
return { ok, detail: ok ? undefined : `first turns: ${sample.slice(0, 120)}` };
|
|
119
|
-
}
|
|
113
|
+
check: async (b) => ({ ok: await hasSelector(b, '[data-testid="chat-messages"] [data-index="1"]') })
|
|
120
114
|
},
|
|
121
115
|
{
|
|
122
116
|
id: 'share.shareButton',
|
|
@@ -128,16 +122,30 @@ export const UI_ANCHORS = [
|
|
|
128
122
|
{
|
|
129
123
|
id: 'share.handoffMenuItem',
|
|
130
124
|
category: 'share',
|
|
131
|
-
description: 'Handoff-to-Claude-Code
|
|
125
|
+
description: 'Handoff-to-Claude-Code action (Share → Send to… tab → Claude Code row, or the legacy dropdown item)',
|
|
132
126
|
requires: 'session',
|
|
133
127
|
check: async (b) => {
|
|
134
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);
|
|
135
129
|
if (!opened)
|
|
136
130
|
return { ok: false, detail: 'Share button not clickable' };
|
|
137
131
|
await new Promise((r) => setTimeout(r, 400));
|
|
138
|
-
|
|
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
|
+
}
|
|
139
147
|
await b.evalValue(`document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); true`).catch(() => null);
|
|
140
|
-
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)' };
|
|
141
149
|
}
|
|
142
150
|
},
|
|
143
151
|
{
|
|
@@ -202,6 +210,32 @@ export const UI_ANCHORS = [
|
|
|
202
210
|
];
|
|
203
211
|
export async function runHealth(browser, opts = {}) {
|
|
204
212
|
const currentUrl = (await browser.url().catch(() => '')) || '';
|
|
213
|
+
if (opts.phase) {
|
|
214
|
+
const phase = opts.phase;
|
|
215
|
+
const results = [];
|
|
216
|
+
for (const a of UI_ANCHORS) {
|
|
217
|
+
const applicable = a.requires === 'any' ||
|
|
218
|
+
(phase === 'home' && a.requires === 'home') ||
|
|
219
|
+
(phase === 'session' && a.requires === 'session');
|
|
220
|
+
if (!applicable)
|
|
221
|
+
continue;
|
|
222
|
+
const base = {
|
|
223
|
+
id: a.id,
|
|
224
|
+
category: a.category,
|
|
225
|
+
description: a.description,
|
|
226
|
+
requires: a.requires,
|
|
227
|
+
phase
|
|
228
|
+
};
|
|
229
|
+
try {
|
|
230
|
+
const r = await a.check(browser, currentUrl);
|
|
231
|
+
results.push({ ...base, status: r.ok ? 'ok' : 'fail', detail: r.detail });
|
|
232
|
+
}
|
|
233
|
+
catch (e) {
|
|
234
|
+
results.push({ ...base, status: 'fail', detail: `threw: ${e.message}` });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return results;
|
|
238
|
+
}
|
|
205
239
|
const inSession = /\/design\/p\/[a-f0-9-]+/i.test(currentUrl);
|
|
206
240
|
const onHome = /\/design\/?$/.test(currentUrl) || currentUrl.endsWith('/design');
|
|
207
241
|
const state = inSession ? 'session' : onHome ? 'home' : 'other';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pro-vi/designer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
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",
|
|
@@ -32,13 +32,17 @@
|
|
|
32
32
|
"doctor": "tsx cli.ts doctor",
|
|
33
33
|
"check": "tsc --noEmit",
|
|
34
34
|
"build": "tsc -p tsconfig.build.json",
|
|
35
|
-
"
|
|
35
|
+
"prepack": "npm run check && npm run build",
|
|
36
36
|
"postinstall": "node scripts/postinstall.mjs",
|
|
37
|
-
"probe": "tsx scripts/probe.ts"
|
|
37
|
+
"probe": "tsx scripts/probe.ts",
|
|
38
|
+
"probe:health": "tsx scripts/ci-health.ts",
|
|
39
|
+
"auto-heal": "tsx scripts/auto-heal.ts",
|
|
40
|
+
"smoke": "bash scripts/install-smoke.sh"
|
|
38
41
|
},
|
|
39
42
|
"dependencies": {
|
|
43
|
+
"@anthropic-ai/sdk": "^0.102.0",
|
|
40
44
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
41
|
-
"zod": "^
|
|
45
|
+
"zod": "^4.4.3"
|
|
42
46
|
},
|
|
43
47
|
"devDependencies": {
|
|
44
48
|
"@types/node": "^25.6.0",
|
|
@@ -48,6 +52,10 @@
|
|
|
48
52
|
"engines": {
|
|
49
53
|
"node": ">=20"
|
|
50
54
|
},
|
|
55
|
+
"os": [
|
|
56
|
+
"darwin",
|
|
57
|
+
"linux"
|
|
58
|
+
],
|
|
51
59
|
"files": [
|
|
52
60
|
"dist/",
|
|
53
61
|
"bin/",
|
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\"]"
|