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.
- package/README.md +56 -3
- package/cli/index.ts +303 -12
- package/cli/init.ts +28 -1
- package/examples/social-app/AGENTS.md +11 -1
- package/examples/social-app/CLAUDE.md +11 -1
- package/examples/social-app/generated/android/social-app/app/.paparazzi-hashes.json +3 -0
- package/examples/social-app/generated/android/social-app/app/build.gradle.kts +2 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/HomeFeedScreen.kt +12 -0
- package/examples/social-app/generated/android/social-app/app/src/test/kotlin/com/social/app/screenshots/HomeFeedScreenshotTest.kt +34 -0
- package/examples/social-app/generated/android/social-app/build.gradle.kts +1 -0
- package/examples/social-app/generated/android/social-app/gradle/libs.versions.toml +2 -0
- package/examples/social-app/generated/android/social-app/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/examples/social-app/generated/android/social-app/gradlew +239 -16
- package/examples/social-app/generated/android/social-app/settings.gradle.kts +4 -0
- package/examples/social-app/package.json +5 -4
- package/examples/social-app/take-web-screenshots.ts +97 -0
- package/examples/taskflow/.codex/config.toml +4 -0
- package/examples/taskflow/.mcp.json +10 -0
- package/examples/taskflow/AGENTS.md +105 -95
- package/examples/taskflow/CLAUDE.md +105 -95
- package/examples/todo-orbit/.codex/config.toml +4 -0
- package/examples/todo-orbit/.mcp.json +10 -0
- package/examples/todo-orbit/AGENTS.md +105 -95
- package/examples/todo-orbit/CLAUDE.md +105 -95
- package/examples/todo-orbit/generated/ios/Todo Orbit/.screenshot-uitest/Sources/ScreenshotUITest.swift +36 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.pbxproj +204 -212
- package/examples/todo-orbit/generated/ios/Todo Orbit/project.yml +1 -0
- package/mcp-server/index.ts +64 -1
- package/mcp-server/screenshot-android.ts +462 -0
- package/mcp-server/screenshot-ios.ts +541 -0
- package/mcp-server/screenshot-shared.ts +200 -0
- package/mcp-server/screenshot.ts +15 -1
- package/package.json +3 -2
- 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 |
|
|
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,
|
|
195
|
-
│
|
|
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
|
|
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]
|
|
116
|
-
openuispec configure-target <t> --set k=v
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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\` |
|
|
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
|
+
<!-- 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
|
+
<!-- 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,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
|
+
}
|
|
@@ -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" }
|