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 +44 -127
- package/dist/mcp-server/index.js +20 -18
- package/dist/mcp-server/preview.js +8 -12
- package/dist/mcp-server/screenshot-shared.js +12 -7
- package/dist/mcp-server/screenshot.js +58 -48
- package/package.json +2 -2
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 \`
|
|
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
|
|
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
|
-
|
|
306
|
+
STOP — before 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
|
-
|
|
313
|
-
or any visual/structural change — you MUST use the OpenUISpec tools before writing any code.
|
|
309
|
+
## Workflow
|
|
314
310
|
|
|
315
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
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
|
|
368
|
-
- \`openuispec_drift
|
|
369
|
-
- \`openuispec_validate\` — schema-only validation
|
|
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
|
-
|
|
332
|
+
**CLI fallback** (when MCP is unavailable): \`openuispec <command> --json\` — same names, with dashes.
|
|
372
333
|
|
|
373
|
-
|
|
334
|
+
## Spec-first vs platform-first
|
|
374
335
|
|
|
375
|
-
**
|
|
376
|
-
-
|
|
377
|
-
-
|
|
378
|
-
-
|
|
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
|
-
|
|
401
|
-
-
|
|
402
|
-
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
|
354
|
+
- Spec root: \`${specDir}/\` — read \`${specDir}/openuispec.yaml\` first.
|
|
422
355
|
- Default dirs: tokens/, screens/, flows/, contracts/, components/, platform/, locales/
|
|
423
356
|
|
|
424
|
-
##
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
-
|
|
447
|
-
-
|
|
448
|
-
-
|
|
449
|
-
-
|
|
450
|
-
-
|
|
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
|
}
|
package/dist/mcp-server/index.js
CHANGED
|
@@ -93,27 +93,29 @@ export const server = new McpServer({
|
|
|
93
93
|
name: "openuispec",
|
|
94
94
|
version: getPackageVersion(),
|
|
95
95
|
}, {
|
|
96
|
-
instructions: `
|
|
96
|
+
instructions: `CRITICAL: NEVER write or edit UI code without calling an OpenUISpec tool first. NO EXCEPTIONS.
|
|
97
97
|
|
|
98
|
-
|
|
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
|
+
OpenUISpec — spec files (YAML) are the ONLY source of truth for all UI.
|
|
105
99
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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
|
|
191
|
+
// 7. Screenshot with Playwright
|
|
192
192
|
const vp = viewport ?? SIZE_CLASS_VIEWPORTS[size_class];
|
|
193
193
|
const browser = await getBrowser();
|
|
194
|
-
const
|
|
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
|
|
13
|
+
if (browserInstance && browserInstance.isConnected())
|
|
14
14
|
return browserInstance;
|
|
15
15
|
if (!launchPromise) {
|
|
16
16
|
launchPromise = (async () => {
|
|
17
|
-
let
|
|
17
|
+
let playwright;
|
|
18
18
|
try {
|
|
19
|
-
|
|
19
|
+
playwright = await import("playwright");
|
|
20
20
|
}
|
|
21
21
|
catch {
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
334
|
-
if (
|
|
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
|
|
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
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
buffer
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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.
|
|
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
|
-
"
|
|
57
|
+
"playwright": "^1.52.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^25.5.0",
|