openuispec 0.2.3 → 0.2.5

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.
Files changed (34) hide show
  1. package/README.md +56 -3
  2. package/cli/index.ts +303 -12
  3. package/cli/init.ts +28 -1
  4. package/examples/social-app/AGENTS.md +11 -1
  5. package/examples/social-app/CLAUDE.md +11 -1
  6. package/examples/social-app/generated/android/social-app/app/.paparazzi-hashes.json +3 -0
  7. package/examples/social-app/generated/android/social-app/app/build.gradle.kts +2 -0
  8. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/HomeFeedScreen.kt +12 -0
  9. package/examples/social-app/generated/android/social-app/app/src/test/kotlin/com/social/app/screenshots/HomeFeedScreenshotTest.kt +34 -0
  10. package/examples/social-app/generated/android/social-app/build.gradle.kts +1 -0
  11. package/examples/social-app/generated/android/social-app/gradle/libs.versions.toml +2 -0
  12. package/examples/social-app/generated/android/social-app/gradle/wrapper/gradle-wrapper.jar +0 -0
  13. package/examples/social-app/generated/android/social-app/gradlew +239 -16
  14. package/examples/social-app/generated/android/social-app/settings.gradle.kts +4 -0
  15. package/examples/social-app/package.json +5 -4
  16. package/examples/social-app/take-web-screenshots.ts +97 -0
  17. package/examples/taskflow/.codex/config.toml +4 -0
  18. package/examples/taskflow/.mcp.json +10 -0
  19. package/examples/taskflow/AGENTS.md +105 -95
  20. package/examples/taskflow/CLAUDE.md +105 -95
  21. package/examples/todo-orbit/.codex/config.toml +4 -0
  22. package/examples/todo-orbit/.mcp.json +10 -0
  23. package/examples/todo-orbit/AGENTS.md +105 -95
  24. package/examples/todo-orbit/CLAUDE.md +105 -95
  25. package/examples/todo-orbit/generated/ios/Todo Orbit/.screenshot-uitest/Sources/ScreenshotUITest.swift +36 -0
  26. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.pbxproj +204 -212
  27. package/examples/todo-orbit/generated/ios/Todo Orbit/project.yml +1 -0
  28. package/mcp-server/index.ts +64 -1
  29. package/mcp-server/screenshot-android.ts +462 -0
  30. package/mcp-server/screenshot-ios.ts +541 -0
  31. package/mcp-server/screenshot-shared.ts +200 -0
  32. package/mcp-server/screenshot.ts +15 -1
  33. package/package.json +3 -2
  34. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/xcshareddata/xcschemes/TodoOrbit.xcscheme +0 -79
package/README.md CHANGED
@@ -123,10 +123,40 @@ Or run directly: `openuispec mcp`
123
123
  | `openuispec_get_contract` | Incremental edits | Get a single contract spec, optionally filtered to one variant |
124
124
  | `openuispec_get_tokens` | Incremental edits | Get tokens for a specific category (color, typography, spacing, etc.) |
125
125
  | `openuispec_get_locale` | Incremental edits | Get a single locale file, optionally filtered to specific keys |
126
- | `openuispec_screenshot` | Visual verification | Take a screenshot of the generated web app at a specific route (requires `puppeteer`) |
126
+ | `openuispec_screenshot` | Visual verification | Screenshot the web app at a route via headless browser (requires `puppeteer`) |
127
+ | `openuispec_screenshot_android` | Visual verification | Screenshot Android app on emulator — builds APK, installs, captures via adb. Works with any Android project via `project_dir` param |
128
+ | `openuispec_screenshot_ios` | Visual verification | Screenshot iOS app on Simulator — builds with xcodebuild, captures via XCUITest. Works with any iOS project via `project_dir` param |
127
129
 
128
130
  The server includes **protocol-level instructions** that trigger on UI-related requests independently of CLAUDE.md rules — so even if CLAUDE.md is buried under other project rules, the MCP enforcement still works.
129
131
 
132
+ ### Visual verification
133
+
134
+ The screenshot tools work with **any** Android/iOS/web project — they don't require an OpenUISpec manifest. Use `project_dir` to point directly at a project root:
135
+
136
+ ```
137
+ # With OpenUISpec manifest (auto-discovers generated app)
138
+ openuispec_screenshot_android(screen: "home")
139
+
140
+ # Standalone — any Android project
141
+ openuispec_screenshot_android(project_dir: "/path/to/android/project", screen: "home")
142
+
143
+ # Standalone — any iOS project
144
+ openuispec_screenshot_ios(project_dir: "/path/to/ios/project", scheme: "MyApp")
145
+ ```
146
+
147
+ Additional override params:
148
+
149
+ | Param | Android | iOS | Purpose |
150
+ |-------|---------|-----|---------|
151
+ | `project_dir` | yes | yes | Skip manifest, point directly at project root |
152
+ | `module` | yes | — | Override app module name (default: auto-detect from `settings.gradle`) |
153
+ | `scheme` | — | yes | Override Xcode scheme (default: auto-detect) |
154
+ | `bundle_id` | — | yes | Override bundle ID (default: auto-detect from `project.pbxproj`) |
155
+
156
+ Auto-detection features:
157
+ - **Android**: Scans `settings.gradle.kts`/`.gradle` to find the module with `com.android.application` plugin. Supports both Kotlin DSL and Groovy DSL.
158
+ - **iOS**: Reads deployment target from `project.pbxproj`. For navigation screenshots, generates an XCUITest project via xcodegen — works with both xcodegen-managed and standalone Xcode projects.
159
+
130
160
  ## Using without MCP
131
161
 
132
162
  You can also use OpenUISpec with any AI by providing context manually:
@@ -191,8 +221,17 @@ openuispec/
191
221
  │ ├── index.ts # Entry point
192
222
  │ └── init.ts # Project scaffolding + AI rules
193
223
  ├── mcp-server/ # MCP server (openuispec-mcp)
194
- │ ├── index.ts # Stdio transport, 13 tools
195
- └── screenshot.ts # Dev server + headless browser screenshot
224
+ │ ├── index.ts # Stdio transport, 15 tools
225
+ ├── screenshot.ts # Dev server + headless browser screenshot (web)
226
+ │ ├── screenshot-shared.ts # Shared utilities for platform screenshot tools
227
+ │ ├── screenshot-android.ts # Android screenshot via emulator (adb screencap)
228
+ │ └── screenshot-ios.ts # iOS screenshot via Simulator (xcodebuild + XCUITest)
229
+ ├── scripts/
230
+ │ └── take-all-screenshots.ts # Batch screenshot capture for all example projects
231
+ ├── artifacts/ # Screenshot artifacts from generated apps
232
+ │ ├── social-app/screenshots/ # Web + Android screenshots
233
+ │ ├── todo-orbit/screenshots/ # Web + Android + iOS screenshots
234
+ │ └── taskflow/screenshots/ # Web + Android + iOS screenshots
196
235
  ├── check/ # Composite validation command
197
236
  │ └── index.ts # Schema + semantic + readiness
198
237
  ├── drift/ # Drift detection (spec change tracking)
@@ -293,6 +332,20 @@ Use the commands like this:
293
332
  - `openuispec prepare --target <t>` builds the target work bundle for either first-time generation or drift-based updates
294
333
  - `openuispec status` shows every target's snapshot state, baseline commit, and whether that target is behind the current spec, still needs a baseline, or has not been generated yet
295
334
 
335
+ Spec access commands (also available as MCP tools):
336
+ - `openuispec read-specs [paths...]` reads spec file contents as JSON
337
+ - `openuispec get-screen <name>` gets a single screen spec
338
+ - `openuispec get-contract <name> [--variant v]` gets a contract spec, optionally one variant
339
+ - `openuispec get-tokens <category>` gets tokens for a category (color, typography, spacing, etc.)
340
+ - `openuispec get-locale <locale> [--keys k1,k2]` gets a locale file, optionally filtered keys
341
+ - `openuispec spec-types` lists all available spec types with descriptions
342
+ - `openuispec spec-schema <type>` gets the full JSON schema for a spec type
343
+
344
+ Screenshot commands (also available as MCP tools):
345
+ - `openuispec screenshot --route /path [--theme dark] [--output-dir dir]` screenshots the web app
346
+ - `openuispec screenshot-android [--project-dir path] [--screen name] [--module name]` screenshots the Android app on an emulator
347
+ - `openuispec screenshot-ios [--project-dir path] [--screen name] [--scheme name]` screenshots the iOS app on a Simulator
348
+
296
349
  In first-time generation mode, `prepare` also carries target-specific generation constraints such as native localization requirements, multi-file output rules, target folder layout expectations, and a requirement to refresh current platform/framework setup knowledge before code generation.
297
350
 
298
351
  If stack choices were auto-applied via `configure-target --defaults` or `init --defaults`, they remain unconfirmed. `prepare` will block implementation readiness until the user explicitly confirms the target stack, and AI agents should ask the user to confirm or change those choices instead of silently proceeding to code generation.
package/cli/index.ts CHANGED
@@ -15,11 +15,39 @@
15
15
  * openuispec status Show cross-target baseline/drift status
16
16
  * openuispec check --target <t> [--json] Composite validation + prepare readiness
17
17
  * openuispec validate [group...] Validate spec files against schemas
18
+ * openuispec read-specs [paths...] Read spec file contents
19
+ * openuispec get-screen <name> Get a single screen spec
20
+ * openuispec get-contract <name> [--variant v] Get a contract spec
21
+ * openuispec get-tokens <category> Get tokens for a category
22
+ * openuispec get-locale <locale> [--keys k1,k2] Get a locale file
23
+ * openuispec spec-types List available spec types
24
+ * openuispec spec-schema <type> Get JSON schema for a spec type
25
+ * openuispec screenshot [--route /path] Screenshot the web app
26
+ * openuispec screenshot-android [opts] Screenshot Android app on emulator
27
+ * openuispec screenshot-ios [opts] Screenshot iOS app on simulator
18
28
  */
19
29
 
20
30
  import { init, updateRules, extractRulesVersion, getPackageVersion } from "./init.js";
21
- import { join } from "node:path";
22
- import { existsSync, readFileSync } from "node:fs";
31
+ import { join, dirname, relative, resolve } from "node:path";
32
+ import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from "node:fs";
33
+ import { fileURLToPath } from "node:url";
34
+
35
+ // ── arg parsing helpers ──────────────────────────────────────────────
36
+
37
+ function getFlag(argv: string[], name: string): boolean {
38
+ return argv.includes(name);
39
+ }
40
+
41
+ function getOption(argv: string[], name: string): string | null {
42
+ const idx = argv.indexOf(name);
43
+ return idx !== -1 && argv[idx + 1] ? argv[idx + 1] : null;
44
+ }
45
+
46
+ function getPositional(argv: string[], startIdx = 0): string[] {
47
+ return argv.slice(startIdx).filter((a) => !a.startsWith("--"));
48
+ }
49
+
50
+ // ── rules version check ─────────────────────────────────────────────
23
51
 
24
52
  function checkRulesVersion(): void {
25
53
  const cwd = process.cwd();
@@ -45,8 +73,35 @@ function checkRulesVersion(): void {
45
73
  }
46
74
  }
47
75
 
76
+ // ── spec helpers (shared with MCP server) ────────────────────────────
77
+
78
+ function resolveSpecDir(projectDir: string, manifest: any, key: string): string {
79
+ return resolve(projectDir, manifest.includes?.[key] ?? `./${key}/`);
80
+ }
81
+
82
+ const SCHEMA_CATALOG: Record<string, { file: string; title: string; description: string }> = {
83
+ manifest: { file: "openuispec.schema.json", title: "Root Manifest", description: "Root manifest (openuispec.yaml)" },
84
+ screen: { file: "screen.schema.json", title: "Screen", description: "Screen composition: layout, sections, navigation" },
85
+ flow: { file: "flow.schema.json", title: "Flow", description: "Navigation flow definitions" },
86
+ platform: { file: "platform.schema.json", title: "Platform", description: "Platform-specific generation config" },
87
+ contract: { file: "contract.schema.json", title: "Contract", description: "Built-in UI contract definitions" },
88
+ "custom-contract":{ file: "custom-contract.schema.json", title: "Custom Contract", description: "User-defined UI contract definitions (x_ prefixed)" },
89
+ locale: { file: "locale.schema.json", title: "Locale", description: "Locale translation files" },
90
+ "tokens/color": { file: "tokens/color.schema.json", title: "Color Tokens", description: "Color tokens" },
91
+ "tokens/typography": { file: "tokens/typography.schema.json", title: "Typography Tokens", description: "Typography tokens" },
92
+ "tokens/spacing": { file: "tokens/spacing.schema.json", title: "Spacing Tokens", description: "Spacing tokens" },
93
+ "tokens/elevation": { file: "tokens/elevation.schema.json", title: "Elevation Tokens", description: "Elevation tokens" },
94
+ "tokens/motion": { file: "tokens/motion.schema.json", title: "Motion Tokens", description: "Motion tokens" },
95
+ "tokens/layout": { file: "tokens/layout.schema.json", title: "Layout Tokens", description: "Layout tokens" },
96
+ "tokens/themes": { file: "tokens/themes.schema.json", title: "Theme Tokens", description: "Theme definitions" },
97
+ "tokens/icons": { file: "tokens/icons.schema.json", title: "Icon Tokens", description: "Icon tokens" },
98
+ };
99
+
100
+ // ── main ─────────────────────────────────────────────────────────────
101
+
48
102
  async function main(): Promise<void> {
49
103
  const [command, ...rest] = process.argv.slice(2);
104
+ const cwd = process.cwd();
50
105
 
51
106
  switch (command) {
52
107
  case "init":
@@ -100,35 +155,252 @@ async function main(): Promise<void> {
100
155
  break;
101
156
  }
102
157
 
158
+ // ── spec getters ───────────────────────────────────────────────
159
+
160
+ case "read-specs": {
161
+ const { findProjectDir, discoverSpecFiles } = await import("../drift/index.js");
162
+ const projectDir = findProjectDir(cwd);
163
+ const allFiles = discoverSpecFiles(projectDir);
164
+ const paths = getPositional(rest);
165
+
166
+ const filesToRead = paths.length > 0
167
+ ? allFiles.filter((f) => {
168
+ const rel = relative(projectDir, f);
169
+ return paths.some((p) => rel === p || rel.endsWith(p));
170
+ })
171
+ : allFiles;
172
+
173
+ const contents = filesToRead.map((f) => ({
174
+ path: relative(projectDir, f),
175
+ content: readFileSync(f, "utf-8"),
176
+ }));
177
+ console.log(JSON.stringify(contents, null, 2));
178
+ break;
179
+ }
180
+
181
+ case "get-screen": {
182
+ const name = rest[0];
183
+ if (!name) { console.error("Usage: openuispec get-screen <name>"); process.exit(1); }
184
+ const { findProjectDir } = await import("../drift/index.js");
185
+ const YAML = (await import("yaml")).default;
186
+ const projectDir = findProjectDir(cwd);
187
+ const manifest = YAML.parse(readFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
188
+ const screensDir = resolveSpecDir(projectDir, manifest, "screens");
189
+ const filePath = join(screensDir, `${name}.yaml`);
190
+ if (!existsSync(filePath)) { console.error(`Screen "${name}" not found at ${filePath}`); process.exit(1); }
191
+ console.log(readFileSync(filePath, "utf-8"));
192
+ break;
193
+ }
194
+
195
+ case "get-contract": {
196
+ const name = rest[0];
197
+ if (!name) { console.error("Usage: openuispec get-contract <name> [--variant v]"); process.exit(1); }
198
+ const variant = getOption(rest, "--variant");
199
+ const { findProjectDir } = await import("../drift/index.js");
200
+ const YAML = (await import("yaml")).default;
201
+ const projectDir = findProjectDir(cwd);
202
+ const manifest = YAML.parse(readFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
203
+ const contractsDir = resolveSpecDir(projectDir, manifest, "contracts");
204
+
205
+ if (!existsSync(contractsDir)) { console.error(`Contracts directory not found: ${contractsDir}`); process.exit(1); }
206
+
207
+ let found = false;
208
+ for (const file of readdirSync(contractsDir).filter(f => f.endsWith(".yaml")).sort()) {
209
+ const filePath = join(contractsDir, file);
210
+ const raw = readFileSync(filePath, "utf-8");
211
+ const content = YAML.parse(raw);
212
+ const contractName = Object.keys(content)[0];
213
+ if (contractName !== name) continue;
214
+ found = true;
215
+
216
+ if (variant) {
217
+ const variantDef = content[contractName]?.variants?.[variant];
218
+ if (!variantDef) {
219
+ const available = Object.keys(content[contractName]?.variants ?? {}).join(", ");
220
+ console.error(`Variant "${variant}" not found. Available: ${available}`);
221
+ process.exit(1);
222
+ }
223
+ console.log(JSON.stringify({ name, variant, definition: variantDef }, null, 2));
224
+ } else {
225
+ console.log(raw);
226
+ }
227
+ break;
228
+ }
229
+ if (!found) { console.error(`Contract "${name}" not found in ${contractsDir}`); process.exit(1); }
230
+ break;
231
+ }
232
+
233
+ case "get-tokens": {
234
+ const category = rest[0];
235
+ if (!category) { console.error("Usage: openuispec get-tokens <category>"); process.exit(1); }
236
+ const { findProjectDir } = await import("../drift/index.js");
237
+ const YAML = (await import("yaml")).default;
238
+ const projectDir = findProjectDir(cwd);
239
+ const manifest = YAML.parse(readFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
240
+ const tokensDir = resolveSpecDir(projectDir, manifest, "tokens");
241
+
242
+ for (const ext of [".yaml", ".yml"]) {
243
+ const filePath = join(tokensDir, `${category}${ext}`);
244
+ if (existsSync(filePath)) { console.log(readFileSync(filePath, "utf-8")); process.exit(0); }
245
+ }
246
+
247
+ const available = readdirSync(tokensDir).filter(f => f.endsWith(".yaml") || f.endsWith(".yml")).map(f => f.replace(/\.ya?ml$/, ""));
248
+ console.error(`Token category "${category}" not found. Available: ${available.join(", ")}`);
249
+ process.exit(1);
250
+ break;
251
+ }
252
+
253
+ case "get-locale": {
254
+ const locale = rest[0];
255
+ if (!locale) { console.error("Usage: openuispec get-locale <locale> [--keys k1,k2,k3]"); process.exit(1); }
256
+ const keysStr = getOption(rest, "--keys");
257
+ const keys = keysStr ? keysStr.split(",").map(k => k.trim()) : null;
258
+ const { findProjectDir } = await import("../drift/index.js");
259
+ const YAML = (await import("yaml")).default;
260
+ const projectDir = findProjectDir(cwd);
261
+ const manifest = YAML.parse(readFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
262
+ const localesDir = resolveSpecDir(projectDir, manifest, "locales");
263
+ const filePath = join(localesDir, `${locale}.json`);
264
+
265
+ if (!existsSync(filePath)) {
266
+ const available = existsSync(localesDir)
267
+ ? readdirSync(localesDir).filter(f => f.endsWith(".json")).map(f => f.replace(".json", ""))
268
+ : [];
269
+ console.error(`Locale "${locale}" not found. Available: ${available.join(", ")}`);
270
+ process.exit(1);
271
+ }
272
+
273
+ const content = JSON.parse(readFileSync(filePath, "utf-8"));
274
+ if (keys) {
275
+ const filtered: Record<string, unknown> = {};
276
+ for (const key of keys) { if (key in content) filtered[key] = content[key]; }
277
+ console.log(JSON.stringify(filtered, null, 2));
278
+ } else {
279
+ console.log(JSON.stringify(content, null, 2));
280
+ }
281
+ break;
282
+ }
283
+
284
+ // ── schema reference ───────────────────────────────────────────
285
+
286
+ case "spec-types": {
287
+ const types = Object.entries(SCHEMA_CATALOG).map(([type, info]) => ({
288
+ type, title: info.title, description: info.description,
289
+ }));
290
+ console.log(JSON.stringify(types, null, 2));
291
+ break;
292
+ }
293
+
294
+ case "spec-schema": {
295
+ const type = rest[0];
296
+ if (!type) { console.error("Usage: openuispec spec-schema <type>\nRun `openuispec spec-types` to list types."); process.exit(1); }
297
+ const entry = SCHEMA_CATALOG[type];
298
+ if (!entry) { console.error(`Unknown spec type "${type}". Run \`openuispec spec-types\` to list types.`); process.exit(1); }
299
+ const __dirname = dirname(fileURLToPath(import.meta.url));
300
+ const schemaPath = join(__dirname, "..", "schema", entry.file);
301
+ console.log(readFileSync(schemaPath, "utf-8"));
302
+ break;
303
+ }
304
+
305
+ // ── screenshot tools ───────────────────────────────────────────
306
+
307
+ case "screenshot": {
308
+ const { takeScreenshot } = await import("../mcp-server/screenshot.js");
309
+ const result = await takeScreenshot(cwd, {
310
+ route: getOption(rest, "--route") ?? "/",
311
+ viewport: {
312
+ width: parseInt(getOption(rest, "--width") ?? "1280"),
313
+ height: parseInt(getOption(rest, "--height") ?? "800"),
314
+ },
315
+ theme: getOption(rest, "--theme") as "light" | "dark" | undefined,
316
+ wait_for: parseInt(getOption(rest, "--wait-for") ?? "1000"),
317
+ full_page: getFlag(rest, "--full-page"),
318
+ selector: getOption(rest, "--selector") ?? undefined,
319
+ output_dir: getOption(rest, "--output-dir") ?? undefined,
320
+ });
321
+ printScreenshotResult(result);
322
+ break;
323
+ }
324
+
325
+ case "screenshot-android": {
326
+ const { takeAndroidScreenshot } = await import("../mcp-server/screenshot-android.js");
327
+ const result = await takeAndroidScreenshot(cwd, {
328
+ screen: getOption(rest, "--screen") ?? undefined,
329
+ route: getOption(rest, "--route") ?? undefined,
330
+ nav: getOption(rest, "--nav")?.split(",") ?? undefined,
331
+ theme: getOption(rest, "--theme") as "light" | "dark" | undefined,
332
+ wait_for: parseInt(getOption(rest, "--wait-for") ?? "3000"),
333
+ output_dir: getOption(rest, "--output-dir") ?? undefined,
334
+ project_dir: getOption(rest, "--project-dir") ?? undefined,
335
+ module: getOption(rest, "--module") ?? undefined,
336
+ });
337
+ printScreenshotResult(result);
338
+ break;
339
+ }
340
+
341
+ case "screenshot-ios": {
342
+ const { takeIOSScreenshot } = await import("../mcp-server/screenshot-ios.js");
343
+ const result = await takeIOSScreenshot(cwd, {
344
+ screen: getOption(rest, "--screen") ?? undefined,
345
+ device: getOption(rest, "--device") ?? undefined,
346
+ nav: getOption(rest, "--nav")?.split(",") ?? undefined,
347
+ theme: getOption(rest, "--theme") as "light" | "dark" | undefined,
348
+ wait_for: parseInt(getOption(rest, "--wait-for") ?? "3000"),
349
+ output_dir: getOption(rest, "--output-dir") ?? undefined,
350
+ project_dir: getOption(rest, "--project-dir") ?? undefined,
351
+ scheme: getOption(rest, "--scheme") ?? undefined,
352
+ bundle_id: getOption(rest, "--bundle-id") ?? undefined,
353
+ });
354
+ printScreenshotResult(result);
355
+ break;
356
+ }
357
+
358
+ // ── help ────────────────────────────────────────────────────────
359
+
103
360
  case undefined:
104
361
  case "--help":
105
362
  case "-h":
106
363
  console.log(`
107
- OpenUISpec CLI v0.1
364
+ OpenUISpec CLI v${getPackageVersion()}
108
365
 
109
366
  Usage:
110
367
  openuispec init Create a new spec project
111
368
  openuispec init --defaults Scaffold non-interactively with unconfirmed defaults
112
- openuispec init --list-options Print init prompt options as JSON
113
369
  openuispec init --no-configure-targets Skip target stack setup during init
114
370
  openuispec update-rules Update AI rules to match installed version
115
- openuispec configure-target <t> [--defaults] Configure target stack; --defaults stays unconfirmed
116
- openuispec configure-target <t> --set k=v Set specific stack values (confirmed)
117
- openuispec configure-target <t> --list-options Print target stack prompt options as JSON
371
+ openuispec configure-target <t> [--defaults] Configure target stack
372
+ openuispec configure-target <t> --set k=v Set specific stack values (confirmed)
373
+
374
+ Workflow:
375
+ openuispec status Show cross-target baseline/drift status
118
376
  openuispec drift [--target <t>] Check for spec drift
119
377
  openuispec drift --snapshot --target <t> Snapshot current state + git baseline
120
378
  openuispec drift --target <t> --explain Explain semantic changes since baseline
121
379
  openuispec prepare --target <t> Build the target work bundle
122
- openuispec status Show cross-target baseline/drift status
123
380
  openuispec check --target <t> [--json] Composite validation + prepare readiness
124
381
  openuispec validate [group...] [--json] Validate spec files
125
- openuispec validate semantic --json Semantic validation as JSON
126
- openuispec mcp Start MCP server (stdio transport)
127
382
 
128
- Validate groups: manifest, tokens, screens, flows, platform, locales, contracts, semantic
383
+ Spec access:
384
+ openuispec read-specs [paths...] Read spec file contents as JSON
385
+ openuispec get-screen <name> Get a single screen spec (YAML)
386
+ openuispec get-contract <name> [--variant v] Get a contract spec
387
+ openuispec get-tokens <category> Get tokens for a category (YAML)
388
+ openuispec get-locale <locale> [--keys k1,k2] Get a locale file (JSON)
389
+ openuispec spec-types List available spec types
390
+ openuispec spec-schema <type> Get full JSON schema for a spec type
129
391
 
130
- Exit codes: 0 = success, 1 = missing config/usage error, 2 = validation failure
392
+ Screenshots:
393
+ openuispec screenshot [--route /path] [--theme light|dark] [--output-dir dir]
394
+ openuispec screenshot-android [--screen name] [--project-dir path] [--module name]
395
+ [--route deeplink] [--nav Step1,Step2] [--theme light|dark] [--output-dir dir]
396
+ openuispec screenshot-ios [--screen name] [--project-dir path] [--scheme name]
397
+ [--bundle-id id] [--device name] [--nav Step1,Step2] [--theme light|dark]
398
+
399
+ Server:
400
+ openuispec mcp Start MCP server (stdio transport)
131
401
 
402
+ Validate groups: manifest, tokens, screens, flows, platform, locales, contracts, semantic
403
+ Exit codes: 0 = success, 1 = missing config/usage error, 2 = validation failure
132
404
  Docs: https://openuispec.rsteam.uz
133
405
  `);
134
406
  break;
@@ -140,6 +412,25 @@ Docs: https://openuispec.rsteam.uz
140
412
  }
141
413
  }
142
414
 
415
+ // ── screenshot result printer ────────────────────────────────────────
416
+
417
+ function printScreenshotResult(result: { content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; isError?: boolean }): void {
418
+ if (result.isError) {
419
+ for (const item of result.content) {
420
+ if (item.type === "text") console.error(item.text);
421
+ }
422
+ process.exit(1);
423
+ }
424
+ for (const item of result.content) {
425
+ if (item.type === "text") console.log(item.text);
426
+ if (item.type === "image" && item.data) {
427
+ const outFile = `screenshot-${Date.now()}.png`;
428
+ writeFileSync(outFile, Buffer.from(item.data, "base64"));
429
+ console.log(`Screenshot saved: ${outFile}`);
430
+ }
431
+ }
432
+ }
433
+
143
434
  main().catch((err) => {
144
435
  console.error(err instanceof Error ? err.message : String(err));
145
436
  process.exit(1);
package/cli/init.ts CHANGED
@@ -282,11 +282,14 @@ When the openuispec MCP server is configured, AI assistants should use these too
282
282
  | \`openuispec_get_contract\` | Get a single contract spec, optionally filtered to one variant. |
283
283
  | \`openuispec_get_tokens\` | Get tokens for a specific category (color, typography, spacing, etc.). |
284
284
  | \`openuispec_get_locale\` | Get a single locale file, optionally filtered to specific keys. |
285
- | \`openuispec_screenshot\` | Take a screenshot of the generated web app at a route (requires \`puppeteer\`). |
285
+ | \`openuispec_screenshot\` | Screenshot the web app at a route via headless browser (requires \`puppeteer\`). |
286
+ | \`openuispec_screenshot_android\` | Screenshot Android app on emulator — works with any project via \`project_dir\`. |
287
+ | \`openuispec_screenshot_ios\` | Screenshot iOS app on Simulator via XCUITest — works with any project via \`project_dir\`. |
286
288
 
287
289
  ## CLI commands
288
290
 
289
291
  \`\`\`bash
292
+ # Workflow
290
293
  openuispec validate # Validate spec files against schemas
291
294
  openuispec validate semantic # Run semantic cross-reference linting
292
295
  openuispec configure-target ${targets[0]} [--defaults] # Configure target stack; --defaults stays unconfirmed
@@ -294,6 +297,20 @@ openuispec status # Show cross-target baseline/drift status
294
297
  openuispec drift --target ${targets[0]} --explain # Explain semantic spec drift
295
298
  openuispec prepare --target ${targets[0]} # Build the target work bundle
296
299
  openuispec drift --snapshot --target ${targets[0]} # Snapshot current state + git baseline after target output exists
300
+
301
+ # Spec access
302
+ openuispec read-specs [paths...] # Read spec file contents as JSON
303
+ openuispec get-screen <name> # Get a single screen spec
304
+ openuispec get-contract <name> [--variant v] # Get a contract spec
305
+ openuispec get-tokens <category> # Get tokens for a category
306
+ openuispec get-locale <locale> [--keys k1,k2] # Get a locale file
307
+ openuispec spec-types # List available spec types
308
+ openuispec spec-schema <type> # Get JSON schema for a spec type
309
+
310
+ # Screenshots
311
+ openuispec screenshot --route /home --theme dark --output-dir screenshots
312
+ openuispec screenshot-android --screen home --project-dir /path/to/android
313
+ openuispec screenshot-ios --screen home --project-dir /path/to/ios --scheme MyApp
297
314
  \`\`\`
298
315
 
299
316
  The target work bundle has two modes:
@@ -374,6 +391,16 @@ If MCP tools are not available, use these CLI commands with \`--json\` flag:
374
391
  - \`openuispec status --json\` — cross-target status
375
392
  - \`openuispec drift --target <t> --explain --json\` — semantic drift
376
393
  - \`openuispec validate [group...] --json\` — schema validation
394
+ - \`openuispec read-specs [paths...]\` — read spec file contents
395
+ - \`openuispec get-screen <name>\` — get a single screen spec
396
+ - \`openuispec get-contract <name> [--variant v]\` — get a contract spec
397
+ - \`openuispec get-tokens <category>\` — get tokens for a category
398
+ - \`openuispec get-locale <locale> [--keys k1,k2]\` — get a locale file
399
+ - \`openuispec spec-types\` — list available spec types
400
+ - \`openuispec spec-schema <type>\` — get JSON schema for a spec type
401
+ - \`openuispec screenshot --route /path\` — screenshot the web app
402
+ - \`openuispec screenshot-android [--project-dir path]\` — screenshot Android app
403
+ - \`openuispec screenshot-ios [--project-dir path]\` — screenshot iOS app
377
404
 
378
405
  ### Other CLI commands
379
406
  - \`openuispec init\` — scaffold a new spec project
@@ -1,5 +1,5 @@
1
1
  <!-- openuispec-rules-start -->
2
- <!-- openuispec-rules-version: 0.2.2 -->
2
+ <!-- openuispec-rules-version: 0.2.4 -->
3
3
  # OpenUISpec — AI Assistant Rules
4
4
  # ================================
5
5
  # This project uses OpenUISpec to define UI as a semantic spec.
@@ -60,6 +60,16 @@ If MCP tools are not available, use these CLI commands with `--json` flag:
60
60
  - `openuispec status --json` — cross-target status
61
61
  - `openuispec drift --target <t> --explain --json` — semantic drift
62
62
  - `openuispec validate [group...] --json` — schema validation
63
+ - `openuispec read-specs [paths...]` — read spec file contents
64
+ - `openuispec get-screen <name>` — get a single screen spec
65
+ - `openuispec get-contract <name> [--variant v]` — get a contract spec
66
+ - `openuispec get-tokens <category>` — get tokens for a category
67
+ - `openuispec get-locale <locale> [--keys k1,k2]` — get a locale file
68
+ - `openuispec spec-types` — list available spec types
69
+ - `openuispec spec-schema <type>` — get JSON schema for a spec type
70
+ - `openuispec screenshot --route /path` — screenshot the web app
71
+ - `openuispec screenshot-android [--project-dir path]` — screenshot Android app
72
+ - `openuispec screenshot-ios [--project-dir path]` — screenshot iOS app
63
73
 
64
74
  ### Other CLI commands
65
75
  - `openuispec init` — scaffold a new spec project
@@ -1,5 +1,5 @@
1
1
  <!-- openuispec-rules-start -->
2
- <!-- openuispec-rules-version: 0.2.2 -->
2
+ <!-- openuispec-rules-version: 0.2.4 -->
3
3
  # OpenUISpec — AI Assistant Rules
4
4
  # ================================
5
5
  # This project uses OpenUISpec to define UI as a semantic spec.
@@ -60,6 +60,16 @@ If MCP tools are not available, use these CLI commands with `--json` flag:
60
60
  - `openuispec status --json` — cross-target status
61
61
  - `openuispec drift --target <t> --explain --json` — semantic drift
62
62
  - `openuispec validate [group...] --json` — schema validation
63
+ - `openuispec read-specs [paths...]` — read spec file contents
64
+ - `openuispec get-screen <name>` — get a single screen spec
65
+ - `openuispec get-contract <name> [--variant v]` — get a contract spec
66
+ - `openuispec get-tokens <category>` — get tokens for a category
67
+ - `openuispec get-locale <locale> [--keys k1,k2]` — get a locale file
68
+ - `openuispec spec-types` — list available spec types
69
+ - `openuispec spec-schema <type>` — get JSON schema for a spec type
70
+ - `openuispec screenshot --route /path` — screenshot the web app
71
+ - `openuispec screenshot-android [--project-dir path]` — screenshot Android app
72
+ - `openuispec screenshot-ios [--project-dir path]` — screenshot iOS app
63
73
 
64
74
  ### Other CLI commands
65
75
  - `openuispec init` — scaffold a new spec project
@@ -0,0 +1,3 @@
1
+ {
2
+ "/Users/rustam/Projects/openuispec/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/HomeFeedScreen.kt": "5917db1b3b8c8633cd315829f8d2ecaf"
3
+ }
@@ -1,4 +1,5 @@
1
1
  plugins {
2
+ id("app.cash.paparazzi")
2
3
  alias(libs.plugins.android.application)
3
4
  alias(libs.plugins.compose.compiler)
4
5
  kotlin("plugin.serialization")
@@ -89,4 +90,5 @@ dependencies {
89
90
  androidTestImplementation("androidx.compose.ui:ui-test-junit4")
90
91
  debugImplementation(libs.androidx.ui.tooling)
91
92
  debugImplementation("androidx.compose.ui:ui-test-manifest")
93
+
92
94
  }
@@ -28,6 +28,7 @@ import androidx.compose.runtime.setValue
28
28
  import androidx.compose.runtime.snapshotFlow
29
29
  import androidx.compose.ui.Modifier
30
30
  import androidx.compose.ui.res.stringResource
31
+ import androidx.compose.ui.tooling.preview.Preview
31
32
  import com.social.app.R
32
33
  import com.social.app.data.MockData
33
34
  import com.social.app.ui.components.ChipOption
@@ -155,3 +156,14 @@ fun HomeFeedScreen(
155
156
  }
156
157
  }
157
158
  }
159
+
160
+ @Preview(showBackground = true)
161
+ @Composable
162
+ fun HomeFeedScreenPreview() {
163
+ MaterialTheme {
164
+ HomeFeedScreen(
165
+ onPostClick = {},
166
+ onUserClick = {},
167
+ )
168
+ }
169
+ }
@@ -0,0 +1,34 @@
1
+ package com.social.app.screenshots
2
+
3
+ import app.cash.paparazzi.DeviceConfig
4
+ import app.cash.paparazzi.Paparazzi
5
+ import org.junit.Rule
6
+ import org.junit.Test
7
+ import com.social.app.ui.screens.*
8
+ import kotlinx.coroutines.Dispatchers
9
+ import kotlinx.coroutines.test.setMain
10
+ import kotlinx.coroutines.test.resetMain
11
+ import org.junit.After
12
+ import org.junit.Before
13
+
14
+ class HomeFeedScreenshotTest {
15
+ @get:Rule
16
+ val paparazzi = Paparazzi(deviceConfig = DeviceConfig.PIXEL_5)
17
+
18
+ @Before
19
+ fun setUp() {
20
+ Dispatchers.setMain(Dispatchers.Unconfined)
21
+ }
22
+
23
+ @After
24
+ fun tearDown() {
25
+ Dispatchers.resetMain()
26
+ }
27
+
28
+ @Test
29
+ fun homeFeedScreenPreview() {
30
+ paparazzi.snapshot {
31
+ HomeFeedScreenPreview()
32
+ }
33
+ }
34
+ }
@@ -3,4 +3,5 @@ plugins {
3
3
  alias(libs.plugins.android.application) apply false
4
4
  alias(libs.plugins.compose.compiler) apply false
5
5
  kotlin("plugin.serialization") version "2.3.0" apply false
6
+ id("app.cash.paparazzi") version "2.0.0-alpha04" apply false
6
7
  }
@@ -1,4 +1,5 @@
1
1
  [versions]
2
+ paparazzi = "2.0.0-alpha04"
2
3
  agp = "9.1.0"
3
4
  kotlin = "2.3.0"
4
5
  coreKtx = "1.18.0"
@@ -43,6 +44,7 @@ androidx-datastore-preferences = { group = "androidx.datastore", name = "datasto
43
44
  androidx-datastore-core = { group = "androidx.datastore", name = "datastore-core", version.ref = "datastore" }
44
45
 
45
46
  [plugins]
47
+ paparazzi = { id = "app.cash.paparazzi", version.ref = "paparazzi" }
46
48
  android-application = { id = "com.android.application", version.ref = "agp" }
47
49
  kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
48
50
  compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }