@pro-vi/designer 0.3.9 → 0.3.10

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 CHANGED
@@ -118,7 +118,7 @@ Writes `tasting.html` with variant tabs + 1/2/3 shortcuts + persistent notes, se
118
118
  ## Operations
119
119
 
120
120
  - `designer doctor` — diagnose setup state. Exits 2 on failure.
121
- - `designer health [--json]` — probe 17 UI anchors. Wire into cron to catch claude.ai UI regressions.
121
+ - `designer health [--json]` — probe every UI anchor designer depends on. Wire into cron to catch claude.ai UI regressions.
122
122
  - **Daily CI** in `.github/workflows/`: `daily-health.yml` runs the auth-required UI probe on a self-hosted macOS runner once per day; `ci.yml` typechecks + builds + does a Docker clean-room install smoke on every PR; `release-please.yml` opens a release PR on conventional commits, merging it tags + publishes via `release-publish.yml`. Selector regressions land as auto-opened PRs under the `selectors-drift` label.
123
123
 
124
124
  ## Known quirks
package/dist/browser.js CHANGED
@@ -12,9 +12,10 @@ export function createBrowser({ session = DEFAULT_SESSION, headed = true, timeou
12
12
  function connectFlags() {
13
13
  if (!cdp)
14
14
  return [];
15
+ const scope = ['--session', `designer-cdp-${cdp.replace(/[^a-zA-Z0-9.-]/g, '_')}`];
15
16
  if (cdp === 'auto' || cdp === '1' || cdp === 'true')
16
- return ['--auto-connect'];
17
- return ['--cdp', cdp];
17
+ return [...scope, '--auto-connect'];
18
+ return [...scope, '--cdp', cdp];
18
19
  }
19
20
  function run(args, { input, parseJson = false } = {}) {
20
21
  return new Promise((resolve, reject) => {
@@ -64,6 +65,15 @@ export function createBrowser({ session = DEFAULT_SESSION, headed = true, timeou
64
65
  activateTab: async (index) => {
65
66
  await run(['tab', String(index)]);
66
67
  },
68
+ reload: () => run(['reload']),
69
+ cookies: async () => {
70
+ const out = await run(['cookies', 'get', '--json']);
71
+ const env = JSON.parse(out);
72
+ if (env.success === false) {
73
+ throw new Error(`agent-browser cookies get failed: ${JSON.stringify(env.error)}`);
74
+ }
75
+ return env.data?.cookies ?? [];
76
+ },
67
77
  snapshot: ({ interactive = true, scope } = {}) => {
68
78
  const args = ['snapshot', '--json'];
69
79
  if (interactive)
package/dist/setup.js CHANGED
@@ -162,7 +162,14 @@ async function step3Chrome(port) {
162
162
  log('chrome', 'fail', `Chrome not found at ${CHROME_BIN}. Set CHROME_BIN to override.`);
163
163
  return false;
164
164
  }
165
- const child = spawn(CHROME_BIN, ['--remote-debugging-port=' + port, '--user-data-dir=' + PROFILE, 'https://claude.ai/design'], {
165
+ const child = spawn(CHROME_BIN, [
166
+ '--remote-debugging-port=' + port,
167
+ '--user-data-dir=' + PROFILE,
168
+ '--no-first-run',
169
+ '--no-default-browser-check',
170
+ '--disable-search-engine-choice-screen',
171
+ 'https://claude.ai/design'
172
+ ], {
166
173
  detached: true,
167
174
  stdio: 'ignore'
168
175
  });
@@ -184,7 +191,28 @@ async function step4SignIn(port) {
184
191
  const browser = createBrowser({ session: 'designer-setup', cdp: port });
185
192
  await browser.open('https://claude.ai/design').catch(() => undefined);
186
193
  await sleep(2500);
187
- const ok = await pollUntil('login', () => verifySignedIn(browser), {
194
+ const MAX_RECOVERY_NAVS = 4;
195
+ const hasAuthCookie = async () => {
196
+ try {
197
+ return (await browser.cookies()).some((c) => /^sessionKey/.test(c.name) && /claude\.ai$/.test(c.domain) && c.value.length > 20);
198
+ }
199
+ catch {
200
+ return false;
201
+ }
202
+ };
203
+ let recoveryNavs = 0;
204
+ const checkSignedIn = async () => {
205
+ if (await verifySignedIn(browser))
206
+ return true;
207
+ if (recoveryNavs < MAX_RECOVERY_NAVS && (await hasAuthCookie())) {
208
+ recoveryNavs++;
209
+ await browser.open('https://claude.ai/design').catch(() => undefined);
210
+ await sleep(3000);
211
+ return verifySignedIn(browser);
212
+ }
213
+ return false;
214
+ };
215
+ const ok = await pollUntil('login', checkSignedIn, {
188
216
  intervalMs: 2000,
189
217
  timeoutMs: 10 * 60_000,
190
218
  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.',
@@ -192,6 +220,9 @@ async function step4SignIn(port) {
192
220
  });
193
221
  if (!ok) {
194
222
  log('login', 'fail', 'Timed out waiting for a signed-in claude.ai/design session. Re-run setup when ready.');
223
+ const watched = await browser.url().catch(() => '(unreachable)');
224
+ const authCookie = await hasAuthCookie();
225
+ log('login', 'fail', `Watched tab: ${watched} | Claude auth cookie: ${authCookie ? 'present — login succeeded but the tab stayed stale; re-run: designer setup' : 'absent (or watched tab is off claude.ai origin) — see watched URL above'}`);
195
226
  return false;
196
227
  }
197
228
  const url = (await browser.url().catch(() => '')) || 'claude.ai/design';
@@ -9,6 +9,25 @@ async function hasButtonMatching(browser, pattern) {
9
9
  .catch(() => false));
10
10
  }
11
11
  export const UI_ANCHORS = [
12
+ {
13
+ id: 'login.signedIn',
14
+ category: 'pattern',
15
+ description: 'signed in (claude.ai is rendering the app shell, not the login wall)',
16
+ requires: 'any',
17
+ check: async (b, url) => {
18
+ if (/claude\.ai\/login/.test(url)) {
19
+ return { ok: false, detail: `signed out — Chrome is on the login wall (${url.slice(0, 80)}). Run: designer setup` };
20
+ }
21
+ if (/claude\.ai\/design/.test(url)) {
22
+ const signedIn = (await hasSelector(b, '[data-testid="project-creator"]')) ||
23
+ (await hasSelector(b, '[data-testid="chat-composer-input"]'));
24
+ return signedIn
25
+ ? { ok: true }
26
+ : { ok: false, detail: `login wall rendered at ${url.slice(0, 80)} (no app shell) — signed out. Run: designer setup` };
27
+ }
28
+ return { ok: true, detail: `not on a claude.ai/design surface (url=${url.slice(0, 60)}) — sign-in not checked here` };
29
+ }
30
+ },
12
31
  {
13
32
  id: 'home.creator',
14
33
  category: 'home',
@@ -65,6 +84,31 @@ export const UI_ANCHORS = [
65
84
  requires: 'session',
66
85
  check: async (b) => ({ ok: await hasSelector(b, '[data-testid="chat-composer-input"]') })
67
86
  },
87
+ {
88
+ id: 'session.composerFillable',
89
+ category: 'session',
90
+ description: 'composer is fillable (textarea or contenteditable, per _submitPrompt)',
91
+ requires: 'session',
92
+ check: async (b) => {
93
+ const shape = await b
94
+ .evalValue(`(() => {
95
+ const el = document.querySelector('[data-testid="chat-composer-input"]');
96
+ if (!el) return { found: false };
97
+ const fillable = el instanceof HTMLTextAreaElement || el.isContentEditable;
98
+ return { found: true, tag: el.tagName, contentEditable: el.isContentEditable, fillable };
99
+ })()`)
100
+ .catch(() => ({ found: false }));
101
+ if (!shape.found)
102
+ return { ok: false, detail: 'composer not found' };
103
+ if (shape.fillable) {
104
+ return { ok: true, detail: shape.contentEditable ? 'contenteditable' : `<${(shape.tag || '').toLowerCase()}>` };
105
+ }
106
+ return {
107
+ ok: false,
108
+ detail: `composer is <${(shape.tag || '?').toLowerCase()}> — neither textarea nor contenteditable; _submitPrompt cannot fill it (composer shape drifted)`
109
+ };
110
+ }
111
+ },
68
112
  {
69
113
  id: 'session.sendButton',
70
114
  category: 'session',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pro-vi/designer",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
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",