openuispec 0.2.20 → 0.2.21

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/cli/init.js CHANGED
@@ -253,7 +253,7 @@ When the openuispec MCP server is configured, AI assistants should use these too
253
253
  | \`openuispec_get_contract\` | Get a single contract spec, optionally filtered to one variant. |
254
254
  | \`openuispec_get_tokens\` | Get tokens for a specific category (color, typography, spacing, etc.). |
255
255
  | \`openuispec_get_locale\` | Get a single locale file, optionally filtered to specific keys. |
256
- | \`openuispec_screenshot\` | Screenshot the web app at a route via headless browser (requires \`puppeteer\`). |
256
+ | \`openuispec_screenshot\` | Screenshot the web app at a route via headless browser (requires \`playwright\`). |
257
257
  | \`openuispec_screenshot_android\` | Screenshot Android app on emulator — works with any project via \`project_dir\`. |
258
258
  | \`openuispec_screenshot_ios\` | Screenshot iOS app on Simulator via XCUITest — works with any project via \`project_dir\`. |
259
259
 
@@ -301,153 +301,70 @@ function aiRulesBlock(specDir, targets) {
301
301
  return `
302
302
  ${RULES_START_MARKER}
303
303
  <!-- openuispec-rules-version: ${version} -->
304
- # OpenUISpec — AI Assistant Rules
305
- # ================================
306
- # This project uses OpenUISpec to define UI as a semantic spec.
307
- # Spec files are the single source of truth for all UI across platforms.
308
- # Targets: ${targetList}
304
+ # OpenUISpec Rules
309
305
 
310
- ## MANDATORY — UI work requires OpenUISpec tools
306
+ STOPbefore writing ANY UI code you MUST call an OpenUISpec tool first.
307
+ No exceptions. Spec files are the single source of truth. Targets: ${targetList}
311
308
 
312
- When the user's request involves UI — screens, navigation, layout, tokens, flows, localization,
313
- or any visual/structural change — you MUST use the OpenUISpec tools before writing any code.
309
+ ## Workflow
314
310
 
315
- ### MCP Tools (use these when available)
311
+ **Full generation** (new screens, multi-platform changes, first-time setup):
312
+ 1. \`openuispec_prepare(target, include_specs: true)\` — returns everything
313
+ 2. Generate code strictly from the spec contents returned
314
+ 3. \`openuispec_check(target, audit: true)\` — returns a checklist, verify your code against it
315
+ 4. Fix gaps. Repeat 2–3 until audit passes.
316
316
 
317
- Call these MCP tools directly. They return structured JSON with everything you need.
318
-
319
- **Pre-generation:**
320
- 1. Call \`openuispec_prepare\` with the target platform returns spec context, platform config, constraints.
321
- Use \`include_specs: true\` to embed all spec contents in one call (saves a separate read_specs).
322
- 2. Call \`openuispec_read_specs\` to load spec file contents if not using include_specs.
323
- Without paths: returns file listing. With paths: returns contents. Use these as the AUTHORITATIVE source.
324
- 3. If spec changes are needed, update spec files FIRST, then call \`openuispec_check\`.
325
- 4. Generate or update the platform UI code based on the spec contents.
326
-
327
- **Post-generation (EVERY TIME after writing UI code):**
328
- 5. Call \`openuispec_check\` to validate spec files (schema + semantics) and confirm prepare readiness.
329
- Note: this validates the SPEC, not the generated code.
330
- 6. Call \`openuispec_check\` with \`audit: true\` to get a spec-derived checklist, then manually review
331
- the generated code against it. For each screen, verify:
332
- - Every field/action in the spec has a corresponding UI element
333
- - Token values (colors, spacing, radii) match exactly — no approximations
334
- - Contract \`must_handle\` states are all implemented (loading, error, empty, etc.)
335
- - Adaptive breakpoints match the spec's \`size_classes\`
336
- - Locale keys match \`$t:\` references
337
- - Navigation targets match flow definitions
338
- 7. Report any real gaps found and fix them before finishing.
339
-
340
- **Iterating before baseline:**
341
- Generated code rarely needs just one pass. Read the spec, audit the generated code against it,
342
- take screenshots to verify visuals, then fix gaps and repeat.
343
- Multiple generate → review → fix cycles are expected before the user accepts the result.
344
-
345
- **Baseline reminder:**
346
- After generation, remind the user to review the output and run the baseline when satisfied:
347
- > When you're happy with the generated output, run: \`openuispec drift --snapshot --target <t>\`
348
- > This records the spec state so future changes are tracked as incremental drift.
349
- Do not baseline on your own initiative — only run the snapshot when the user asks.
317
+ **Incremental edits** (one screen, one token, one locale key):
318
+ 1. Use a focused getter: \`get_screen\`, \`get_contract\`, \`get_component\`, \`get_tokens\`, \`get_locale\`
319
+ 2. Edit code based on the spec returned
320
+ 3. \`openuispec_check(target)\` — validate
350
321
 
351
322
  **Creating new spec files:**
352
- - Call \`openuispec_spec_types\` to discover available spec types.
353
- - Call \`openuispec_spec_schema\` with the specific type to get the full JSON schema.
354
- - Write the spec file following the schema exactly.
355
-
356
- **Focused getters (prefer these for incremental edits over \`read_specs\`):**
357
- - \`openuispec_get_screen(name)\` — single screen spec
358
- - \`openuispec_get_contract(name, variant?)\` — single contract, optionally one variant
359
- - \`openuispec_get_component(name, variant?)\` — single component, optionally one variant
360
- - \`openuispec_get_tokens(category)\` — single token category (color, typography, spacing, etc.)
361
- - \`openuispec_get_locale(locale, keys?)\` — single locale file, optionally filtered keys
362
- - \`openuispec_check(target, audit?, screens?, contracts?)\` — validation + optional scoped audit checklist
363
-
364
- Use \`read_specs\` for full-project generation; use focused getters when editing one screen or contract.
323
+ 1. \`openuispec_spec_types\` \`openuispec_spec_schema(type)\` write YAML following the schema
365
324
 
366
325
  **Other tools:**
367
- - \`openuispec_status\` — cross-target summary, good starting point
368
- - \`openuispec_drift\` with \`explain: true\` — property-level spec changes
369
- - \`openuispec_validate\` — schema-only validation by group
326
+ - \`openuispec_status\` — cross-target summary
327
+ - \`openuispec_drift(explain: true)\` — what changed since last baseline
328
+ - \`openuispec_validate\` — schema-only validation
329
+ - \`openuispec_screenshot\` / \`screenshot_android\` / \`screenshot_ios\` — visual verification
330
+ - \`openuispec_preview(screen)\` — render spec as HTML, no running app needed
370
331
 
371
- ### CLI fallback (when MCP is not available)
332
+ **CLI fallback** (when MCP is unavailable): \`openuispec <command> --json\` — same names, with dashes.
372
333
 
373
- If MCP tools are not available, use these CLI commands with \`--json\` flag:
334
+ ## Spec-first vs platform-first
374
335
 
375
- **Status & discovery:**
376
- - \`openuispec status --json\` cross-target status
377
- - \`openuispec spec-types\` list available spec types
378
- - \`openuispec spec-schema <type>\` — get JSON schema for a spec type
379
-
380
- **Spec access:**
381
- - \`openuispec read-specs [paths...]\` — read spec file contents
382
- - \`openuispec get-screen <name>\` — get a single screen spec
383
- - \`openuispec get-contract <name> [--variant v]\` — get a contract spec
384
- - \`openuispec get-component <name> [--variant v]\` — get a component spec
385
- - \`openuispec get-tokens <category>\` — get tokens for a category
386
- - \`openuispec get-locale <locale> [--keys k1,k2]\` — get a locale file
387
-
388
- **Validation & generation workflow:**
389
- - \`openuispec validate [group...] --json\` — validate spec files against JSON Schemas
390
- - \`openuispec check --target <t> --json\` — validate spec files + check target generation readiness
391
- - \`openuispec prepare --target <t> --json\` — build AI-ready work bundle
392
- - \`openuispec drift --target <t> --explain --json\` — semantic drift
393
-
394
- **Visual verification:**
395
- - \`openuispec screenshot --route /path\` — screenshot the web app
396
- - \`openuispec screenshot --route /path --init-script "..."\` — inject auth/role before rendering (web only; app must implement \`__ous_init\` bootstrapper)
397
- - \`openuispec screenshot-android [--project-dir path]\` — screenshot Android app
398
- - \`openuispec screenshot-ios [--project-dir path]\` — screenshot iOS app
336
+ **Spec-first** (use the workflow above):
337
+ - Screen structure, navigation, fields, actions, validation, data binding
338
+ - Token, variant, contract, flow, or localization changes
339
+ - Changes affecting multiple platforms
399
340
 
400
- ### Other CLI commands
401
- - \`openuispec init\` scaffold a new spec project
402
- - \`openuispec configure-target <t>\` configure target platform stack
403
- - \`openuispec update-rules\` — update AI rules to match installed package version
404
- - \`openuispec drift --snapshot --target <t>\` — snapshot current state (user-initiated, after reviewing generated output)
341
+ **Platform-first** (skip spec tools):
342
+ - Platform-only polish (iOS-only animation, web-only CSS tweak)
343
+ - Bug fixes that don't alter shared behavior
405
344
 
406
345
  ## Spec format reference
407
346
 
408
- The spec format, schemas, and generation rules are in the installed \`openuispec\` package.
409
- You MUST read the reference files before creating or editing spec files do NOT guess the format.
410
-
411
- **Find the package:** \`node_modules/openuispec/\` or run \`npm root -g\` → \`<prefix>/openuispec/\`.
412
- **Online fallback:** \`https://openuispec.rsteam.uz/llms-full.txt\`
413
-
414
- **Reference files (read in order):**
415
- 1. \`README.md\` — schema tables, file format, root wrapper keys
416
- 2. \`spec/openuispec-v0.2.md\` — full specification
417
- 3. \`examples/taskflow/openuispec/\` — complete working example
418
- 4. \`schema/\` — JSON Schemas for every file type
347
+ Read from the installed package, NEVER guess the format:
348
+ - \`node_modules/openuispec/README.md\`schema tables, file format
349
+ - \`node_modules/openuispec/spec/openuispec-v0.2.md\` — full specification
350
+ - \`node_modules/openuispec/schema/\` JSON Schemas
351
+ - Online fallback: \`https://openuispec.rsteam.uz/llms-full.txt\`
419
352
 
420
353
  ## Spec location
421
- - Spec root: \`${specDir}/\` — read \`${specDir}/openuispec.yaml\` first for actual paths.
354
+ - Spec root: \`${specDir}/\` — read \`${specDir}/openuispec.yaml\` first.
422
355
  - Default dirs: tokens/, screens/, flows/, contracts/, components/, platform/, locales/
423
356
 
424
- ## When to start from spec vs platform code
425
-
426
- **Spec-first** (use \`openuispec_prepare\` or \`openuispec prepare\`):
427
- - Screen structure, navigation, fields, actions, validation, data binding changes
428
- - Token, variant, contract, flow, or localization changes
429
- - Changes affecting multiple platforms
430
- - Requests in product/UI terms
431
-
432
- **Platform-first** (skip spec tools):
433
- - Platform-specific polish (iOS-only, Android-only, web-only)
434
- - Local bug fixes that don't alter shared semantic behavior
435
-
436
- ## If spec directories are empty (first-time setup)
437
-
438
- Read \`spec/openuispec-v0.2.md\` from the package first, then:
439
- 1. Scan codebase for UI screens → create \`${specDir}/screens/<name>.yaml\` as \`status: stub\`
440
- 2. Extract tokens (colors, fonts, spacing) → \`${specDir}/tokens/\`
441
- 3. Create contract extensions → \`${specDir}/contracts/\`
442
- 4. Create locale files → \`${specDir}/locales/<locale>.json\`
443
- 5. Fill in \`data_model\`, \`api.endpoints\` in \`${specDir}/openuispec.yaml\`
357
+ ## If spec directories are empty
358
+ Read \`spec/openuispec-v0.2.md\` from the package, then create stubs:
359
+ screens \`${specDir}/screens/\`, tokens \`${specDir}/tokens/\`,
360
+ contracts \`${specDir}/contracts/\`, locales \`${specDir}/locales/\`
444
361
 
445
362
  ## Rules
446
- - Do not baseline on your own initiative the user decides when generated output is accepted.
447
- - After generation, always remind the user to review and baseline: \`openuispec drift --snapshot --target <t>\`.
448
- - Do not modify generated UI without checking whether the spec must change first.
449
- - Do not use \`configure-target --defaults\` as silent approval ask the user to confirm.
450
- - Always read spec format from the installed package, not from cached/memorized content.
363
+ - NEVER write UI code without calling an OpenUISpec tool first.
364
+ - NEVER snapshot drift without user approval.
365
+ - NEVER modify generated UI without checking whether the spec must change first.
366
+ - ALWAYS read spec format from the installed package, not from memory.
367
+ - After generation, remind the user: \`openuispec drift --snapshot --target <t>\`
451
368
  ${RULES_END_MARKER}
452
369
  `;
453
370
  }
@@ -93,27 +93,29 @@ export const server = new McpServer({
93
93
  name: "openuispec",
94
94
  version: getPackageVersion(),
95
95
  }, {
96
- instructions: `OpenUISpec semantic UI spec format. Spec files (YAML) are the single source of truth for all UI.
96
+ instructions: `CRITICAL: NEVER write or edit UI code without calling an OpenUISpec tool first. NO EXCEPTIONS.
97
97
 
98
- WORKFLOWeach tool response includes a next_tool hint, follow it:
99
- 1. openuispec_prepare(target) → get context + platform config (include_specs=true to embed content)
100
- 2. openuispec_read_specs(paths) → load spec content (omit paths for listing only)
101
- 3. Generate/update code
102
- 4. openuispec_check(target) → validate spec files (audit=true for review checklist, not code inspection)
103
- 5. Remind the user to baseline when satisfied: openuispec drift --snapshot --target <t>
104
- Do not baseline on your own initiative — the user decides when output is accepted.
98
+ OpenUISpecspec files (YAML) are the ONLY source of truth for all UI.
105
99
 
106
- FOCUSED GETTERS (prefer for incremental edits): get_screen, get_contract, get_component, get_tokens, get_locale
107
- SPEC AUTHORING: spec_types → spec_schema(type, summary?) → write YAML
108
- PREVIEW: openuispec_preview(screen) render spec as HTML with mock data, returns screenshot (no app needed)
109
- SCREENSHOTS: screenshot (web), screenshot_android, screenshot_ios single + batch variants
100
+ FULL GENERATION (new screens, multi-platform, first-time):
101
+ 1. openuispec_prepare(target, include_specs=true) → MUST be first action
102
+ 2. Generate code strictly from spec contents do NOT improvise UI structure
103
+ 3. openuispec_check(target, audit=true) verify code against the returned checklist
104
+ 4. Fix gaps, repeat 2–3 until audit passes
105
+
106
+ INCREMENTAL EDITS (one screen, one token, one locale key):
107
+ 1. Focused getter: get_screen, get_contract, get_component, get_tokens, get_locale
108
+ 2. Edit code based on spec returned
109
+ 3. openuispec_check(target) → validate
110
110
 
111
- Skip only for purely non-UI requests.
111
+ SPEC AUTHORING: spec_types spec_schema(type) → write YAML
112
+ PREVIEW: openuispec_preview(screen) → render spec as HTML screenshot (no app needed)
113
+ SCREENSHOTS: screenshot (web), screenshot_android, screenshot_ios — single + batch variants
112
114
 
113
- When generating UI code:
114
- 1. Check anti_patterns in the prepare result — hard constraints on what NOT to produce
115
- 2. Check design_context match the complexity level and personality description
116
- 3. Apply the AI Fingerprint Test: would a viewer immediately say "AI made this"? If yes, revise.`,
115
+ NEVER skip the tools. NEVER generate UI from memory or conversation alone.
116
+ ALWAYS check anti_patterns and design_context from prepare — hard constraints.
117
+ ALWAYS remind user to baseline when satisfied: openuispec drift --snapshot --target <t>
118
+ Apply the AI Fingerprint Test: if it looks obviously AI-generated, revise.`,
117
119
  });
118
120
  // ── tool: openuispec_prepare ─────────────────────────────────────────
119
121
  server.registerTool("openuispec_prepare", {
@@ -687,7 +689,7 @@ server.registerTool("openuispec_get_locale", {
687
689
  });
688
690
  // ── tool: openuispec_screenshot ──────────────────────────────────────
689
691
  server.registerTool("openuispec_screenshot", {
690
- description: "Take a screenshot of the generated web app at a specific route. Starts the Vite dev server automatically if needed (first call may take longer). Returns a PNG image for visual verification of generated UI. Requires puppeteer to be installed (npm install -g puppeteer).",
692
+ description: "Take a screenshot of the generated web app at a specific route. Starts the Vite dev server automatically if needed (first call may take longer). Returns a PNG image for visual verification of generated UI. Requires playwright to be installed (npm install -g playwright).",
691
693
  inputSchema: {
692
694
  route: z.string().default("/").describe("Route path to navigate to, e.g. '/home', '/settings', '/posts/123'"),
693
695
  viewport: z.object({
@@ -188,21 +188,16 @@ export async function renderPreview(projectCwd, options) {
188
188
  };
189
189
  // 6. Render HTML
190
190
  const html = renderPage(ctx);
191
- // 7. Screenshot with Puppeteer
191
+ // 7. Screenshot with Playwright
192
192
  const vp = viewport ?? SIZE_CLASS_VIEWPORTS[size_class];
193
193
  const browser = await getBrowser();
194
- const page = await browser.newPage();
194
+ const context = await browser.newContext({
195
+ viewport: { width: vp.width, height: vp.height },
196
+ deviceScaleFactor: 2,
197
+ colorScheme: theme === "dark" ? "dark" : "light",
198
+ });
199
+ const page = await context.newPage();
195
200
  try {
196
- await page.setViewport({
197
- width: vp.width,
198
- height: vp.height,
199
- deviceScaleFactor: 2,
200
- });
201
- if (theme === "dark") {
202
- await page.emulateMediaFeatures([
203
- { name: "prefers-color-scheme", value: "dark" },
204
- ]);
205
- }
206
201
  await page.setContent(html, { waitUntil: "load", timeout: 10_000 });
207
202
  // Small delay for CSS to settle
208
203
  await new Promise((r) => setTimeout(r, 200));
@@ -229,5 +224,6 @@ export async function renderPreview(projectCwd, options) {
229
224
  }
230
225
  finally {
231
226
  await page.close();
227
+ await context.close();
232
228
  }
233
229
  }
@@ -6,23 +6,28 @@ import { join, resolve, relative } from "node:path";
6
6
  import { createHash } from "node:crypto";
7
7
  import YAML from "yaml";
8
8
  import { findProjectDir } from "../drift/index.js";
9
- // ── shared browser manager ──────────────────────────────────────────
9
+ // ── shared browser manager (Playwright) ─────────────────────────────
10
10
  let browserInstance = null;
11
11
  let launchPromise = null;
12
12
  export async function getBrowser() {
13
- if (browserInstance?.connected)
13
+ if (browserInstance && browserInstance.isConnected())
14
14
  return browserInstance;
15
15
  if (!launchPromise) {
16
16
  launchPromise = (async () => {
17
- let puppeteer;
17
+ let playwright;
18
18
  try {
19
- puppeteer = await import("puppeteer");
19
+ playwright = await import("playwright");
20
20
  }
21
21
  catch {
22
- throw new Error("puppeteer is not installed. Run:\n npm install -g puppeteer\n" +
23
- "or add it to your project's devDependencies.");
22
+ try {
23
+ playwright = await import("playwright-core");
24
+ }
25
+ catch {
26
+ throw new Error("playwright is not installed. Run:\n npm install -g playwright\n" +
27
+ "or add it to your project's devDependencies.");
28
+ }
24
29
  }
25
- browserInstance = await puppeteer.launch({
30
+ browserInstance = await playwright.chromium.launch({
26
31
  headless: true,
27
32
  args: ["--no-sandbox", "--disable-setuid-sandbox"],
28
33
  });
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Screenshot tool — launches dev server + headless browser, captures pages.
3
3
  *
4
- * Both the Vite dev server and the Puppeteer browser are kept alive between
4
+ * Both the Vite dev server and the Playwright browser are kept alive between
5
5
  * calls and torn down when the MCP server process exits.
6
6
  */
7
7
  import { spawn, execSync } from "node:child_process";
@@ -306,38 +306,33 @@ export async function takeScreenshot(projectCwd, options) {
306
306
  const webDir = findWebAppDir(projectCwd);
307
307
  const server = await startDevServer(webDir);
308
308
  const browser = await getBrowser();
309
- // 2. Navigate
310
- const page = await browser.newPage();
309
+ // 2. Navigate (Playwright context + page)
310
+ const context = await browser.newContext({
311
+ viewport: { width: viewport.width, height: viewport.height },
312
+ deviceScaleFactor: scale,
313
+ colorScheme: theme ?? "light",
314
+ });
315
+ const page = await context.newPage();
311
316
  try {
312
- await page.setViewport({
313
- width: viewport.width,
314
- height: viewport.height,
315
- deviceScaleFactor: scale,
316
- });
317
- if (theme) {
318
- await page.emulateMediaFeatures([
319
- { name: "prefers-color-scheme", value: theme },
320
- ]);
321
- }
322
317
  const base = server.url.replace(/\/+$/, "");
323
318
  let targetUrl = `${base}${route.startsWith("/") ? "" : "/"}${route}`;
324
319
  if (init_script)
325
320
  targetUrl = appendInitParam(targetUrl, init_script);
326
- await page.goto(targetUrl, { waitUntil: "networkidle2", timeout: 30_000 });
321
+ await page.goto(targetUrl, { waitUntil: "networkidle", timeout: 30_000 });
327
322
  if (wait_for > 0) {
328
323
  await new Promise((r) => setTimeout(r, wait_for));
329
324
  }
330
325
  // 3. Screenshot
331
326
  let buffer;
332
327
  if (selector) {
333
- const element = await page.$(selector);
334
- if (!element) {
328
+ const loc = page.locator(selector);
329
+ if ((await loc.count()) === 0) {
335
330
  return {
336
331
  content: [{ type: "text", text: `Error: Element not found for selector: ${selector}` }],
337
332
  isError: true,
338
333
  };
339
334
  }
340
- buffer = await element.screenshot({ type: "png" });
335
+ buffer = await loc.screenshot({ type: "png" });
341
336
  }
342
337
  else {
343
338
  buffer = await page.screenshot({ type: "png", fullPage: full_page });
@@ -375,6 +370,7 @@ export async function takeScreenshot(projectCwd, options) {
375
370
  }
376
371
  finally {
377
372
  await page.close();
373
+ await context.close();
378
374
  }
379
375
  }
380
376
  // ── batch screenshot ─────────────────────────────────────────────────
@@ -386,45 +382,58 @@ export async function takeScreenshotBatch(projectCwd, options) {
386
382
  const webDir = findWebAppDir(projectCwd);
387
383
  const server = await startDevServer(webDir);
388
384
  const browser = await getBrowser();
389
- const page = await browser.newPage();
385
+ const context = await browser.newContext({
386
+ viewport: { width: viewport.width, height: viewport.height },
387
+ deviceScaleFactor: scale,
388
+ colorScheme: theme ?? "light",
389
+ });
390
+ const page = await context.newPage();
390
391
  try {
391
- await page.setViewport({
392
- width: viewport.width,
393
- height: viewport.height,
394
- deviceScaleFactor: scale,
395
- });
396
- if (theme) {
397
- await page.emulateMediaFeatures([{ name: "prefers-color-scheme", value: theme }]);
398
- }
399
392
  const base = server.url.replace(/\/+$/, "");
400
393
  const themeLabel = theme ?? "default";
401
394
  const snapshots = [];
402
- for (const capture of captures) {
403
- const effectiveInitScript = capture.init_script ?? sharedInitScript;
404
- let targetUrl = `${base}${capture.route.startsWith("/") ? "" : "/"}${capture.route}`;
405
- if (effectiveInitScript)
406
- targetUrl = appendInitParam(targetUrl, effectiveInitScript);
407
- await page.goto(targetUrl, { waitUntil: "networkidle2", timeout: 30_000 });
408
- await new Promise((r) => setTimeout(r, capture.wait_for ?? 1000));
409
- let buffer;
410
- if (capture.selector) {
411
- const el = await page.$(capture.selector);
412
- buffer = el ? await el.screenshot({ type: "png" }) : await page.screenshot({ type: "png" });
413
- }
414
- else {
415
- buffer = await page.screenshot({ type: "png", fullPage: capture.full_page ?? false });
395
+ const errors = [];
396
+ for (let i = 0; i < captures.length; i++) {
397
+ const capture = captures[i];
398
+ try {
399
+ const effectiveInitScript = capture.init_script ?? sharedInitScript;
400
+ let targetUrl = `${base}${capture.route.startsWith("/") ? "" : "/"}${capture.route}`;
401
+ if (effectiveInitScript)
402
+ targetUrl = appendInitParam(targetUrl, effectiveInitScript);
403
+ await page.goto(targetUrl, { waitUntil: "networkidle", timeout: 30_000 });
404
+ await new Promise((r) => setTimeout(r, capture.wait_for ?? 1000));
405
+ let buffer;
406
+ if (capture.selector) {
407
+ const loc = page.locator(capture.selector);
408
+ buffer = (await loc.count()) > 0
409
+ ? await loc.screenshot({ type: "png" })
410
+ : await page.screenshot({ type: "png" });
411
+ }
412
+ else {
413
+ buffer = await page.screenshot({ type: "png", fullPage: capture.full_page ?? false });
414
+ }
415
+ const filename = `${capture.screen}_${themeLabel}.png`;
416
+ let savedPath = filename;
417
+ if (output_dir) {
418
+ const outDir = resolve(webDir, output_dir);
419
+ mkdirSync(outDir, { recursive: true });
420
+ savedPath = join(outDir, filename);
421
+ writeFileSync(savedPath, buffer);
422
+ }
423
+ snapshots.push({ screen: capture.screen, path: savedPath, data: buffer.toString("base64"), init_script: effectiveInitScript });
424
+ options.onProgress?.({ screen: capture.screen, index: i + 1, total: captures.length, ok: true });
416
425
  }
417
- const filename = `${capture.screen}_${themeLabel}.png`;
418
- let savedPath = filename;
419
- if (output_dir) {
420
- const outDir = resolve(webDir, output_dir);
421
- mkdirSync(outDir, { recursive: true });
422
- savedPath = join(outDir, filename);
423
- writeFileSync(savedPath, buffer);
426
+ catch (err) {
427
+ const msg = err instanceof Error ? err.message : String(err);
428
+ errors.push({ screen: capture.screen, error: msg });
429
+ options.onProgress?.({ screen: capture.screen, index: i + 1, total: captures.length, ok: false, error: msg });
424
430
  }
425
- snapshots.push({ screen: capture.screen, path: savedPath, data: buffer.toString("base64"), init_script: effectiveInitScript });
426
431
  }
427
432
  const content = [];
433
+ content.push({
434
+ type: "text",
435
+ text: JSON.stringify({ captured: snapshots.length, failed: errors.length, errors }, null, 2),
436
+ });
428
437
  for (const s of snapshots) {
429
438
  content.push({ type: "image", data: s.data, mimeType: "image/png" });
430
439
  content.push({
@@ -436,6 +445,7 @@ export async function takeScreenshotBatch(projectCwd, options) {
436
445
  }
437
446
  finally {
438
447
  await page.close();
448
+ await context.close();
439
449
  }
440
450
  }
441
451
  // ── cleanup ─────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openuispec",
3
- "version": "0.2.20",
3
+ "version": "0.2.21",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A semantic UI specification format for AI-native, platform-native app development",
@@ -54,7 +54,7 @@
54
54
  "zod": "^3.25.76"
55
55
  },
56
56
  "optionalDependencies": {
57
- "puppeteer": "^24.39.1"
57
+ "playwright": "^1.52.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^25.5.0",