openuispec 0.2.4 → 0.2.6
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 +14 -0
- package/cli/index.ts +303 -12
- package/cli/init.ts +28 -3
- package/examples/social-app/AGENTS.md +11 -1
- package/examples/social-app/CLAUDE.md +11 -1
- 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/mcp-server/screenshot-android.ts +26 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -332,6 +332,20 @@ Use the commands like this:
|
|
|
332
332
|
- `openuispec prepare --target <t>` builds the target work bundle for either first-time generation or drift-based updates
|
|
333
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
|
|
334
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
|
+
|
|
335
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.
|
|
336
350
|
|
|
337
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,13 +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\` |
|
|
286
|
-
| \`openuispec_screenshot_android\` |
|
|
287
|
-
| \`openuispec_screenshot_ios\` |
|
|
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\`. |
|
|
288
288
|
|
|
289
289
|
## CLI commands
|
|
290
290
|
|
|
291
291
|
\`\`\`bash
|
|
292
|
+
# Workflow
|
|
292
293
|
openuispec validate # Validate spec files against schemas
|
|
293
294
|
openuispec validate semantic # Run semantic cross-reference linting
|
|
294
295
|
openuispec configure-target ${targets[0]} [--defaults] # Configure target stack; --defaults stays unconfirmed
|
|
@@ -296,6 +297,20 @@ openuispec status # Show cross-target baseline/drift status
|
|
|
296
297
|
openuispec drift --target ${targets[0]} --explain # Explain semantic spec drift
|
|
297
298
|
openuispec prepare --target ${targets[0]} # Build the target work bundle
|
|
298
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
|
|
299
314
|
\`\`\`
|
|
300
315
|
|
|
301
316
|
The target work bundle has two modes:
|
|
@@ -376,6 +391,16 @@ If MCP tools are not available, use these CLI commands with \`--json\` flag:
|
|
|
376
391
|
- \`openuispec status --json\` — cross-target status
|
|
377
392
|
- \`openuispec drift --target <t> --explain --json\` — semantic drift
|
|
378
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
|
|
379
404
|
|
|
380
405
|
### Other CLI commands
|
|
381
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
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
"version": "0.0.0",
|
|
5
5
|
"description": "OpenUISpec test project wired to the local ../openuispec checkout",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"openuispec": "node --import
|
|
8
|
-
"validate": "node --import
|
|
9
|
-
"validate:semantic": "node --import
|
|
10
|
-
"status": "node --import
|
|
7
|
+
"openuispec": "node --import ./../../node_modules/tsx/dist/loader.mjs ./../../cli/index.ts",
|
|
8
|
+
"validate": "node --import ./../../node_modules/tsx/dist/loader.mjs ./../../cli/index.ts validate",
|
|
9
|
+
"validate:semantic": "node --import ./../../node_modules/tsx/dist/loader.mjs ./../../cli/index.ts validate semantic",
|
|
10
|
+
"status": "node --import ./../../node_modules/tsx/dist/loader.mjs ./../../cli/index.ts status",
|
|
11
|
+
"screenshots:web": "node --import ./../../node_modules/tsx/dist/loader.mjs ./take-web-screenshots.ts"
|
|
11
12
|
}
|
|
12
13
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { Client } from "@modelcontextprotocol/sdk/client";
|
|
4
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
5
|
+
|
|
6
|
+
type ImageContent = {
|
|
7
|
+
type: "image";
|
|
8
|
+
data: string;
|
|
9
|
+
mimeType: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type TextContent = {
|
|
13
|
+
type: "text";
|
|
14
|
+
text: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type WebCapture = {
|
|
18
|
+
name: string;
|
|
19
|
+
route: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const OUTPUT_DIR = resolve("generated/web/social-app/screenshots");
|
|
23
|
+
const VIEWPORT = { width: 1366, height: 900 };
|
|
24
|
+
const captures: WebCapture[] = [
|
|
25
|
+
{ name: "home", route: "/home" },
|
|
26
|
+
{ name: "discover", route: "/discover" },
|
|
27
|
+
{ name: "notifications", route: "/notifications" },
|
|
28
|
+
{ name: "messages", route: "/messages" },
|
|
29
|
+
{ name: "profile", route: "/profile" },
|
|
30
|
+
{ name: "profile-edit", route: "/profile/edit" },
|
|
31
|
+
{ name: "settings", route: "/settings" },
|
|
32
|
+
{ name: "create", route: "/create" },
|
|
33
|
+
{ name: "search-editorial-ui-posts", route: "/search?query=Editorial%20UI&tab=posts" },
|
|
34
|
+
{ name: "post-post-1", route: "/posts/post-1" },
|
|
35
|
+
{ name: "user-lina", route: "/u/user-lina" },
|
|
36
|
+
{ name: "chat-conversation-1", route: "/chat/conversation-1" },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function getImageData(content: Array<ImageContent | TextContent>): string {
|
|
40
|
+
const image = content.find((item): item is ImageContent => item.type === "image");
|
|
41
|
+
if (!image) {
|
|
42
|
+
throw new Error("Screenshot tool did not return image data.");
|
|
43
|
+
}
|
|
44
|
+
return image.data;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getText(content: Array<ImageContent | TextContent>): string | null {
|
|
48
|
+
const text = content.find((item): item is TextContent => item.type === "text");
|
|
49
|
+
return text?.text ?? null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function main() {
|
|
53
|
+
mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
54
|
+
|
|
55
|
+
const transport = new StdioClientTransport({
|
|
56
|
+
command: "npm",
|
|
57
|
+
args: ["run", "openuispec", "--", "mcp"],
|
|
58
|
+
cwd: process.cwd(),
|
|
59
|
+
stderr: "inherit",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const client = new Client(
|
|
63
|
+
{ name: "social-app-screenshot-runner", version: "0.1.0" },
|
|
64
|
+
{ capabilities: {} },
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
await client.connect(transport);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
for (const capture of captures) {
|
|
71
|
+
const result = await client.callTool({
|
|
72
|
+
name: "openuispec_screenshot",
|
|
73
|
+
arguments: {
|
|
74
|
+
route: capture.route,
|
|
75
|
+
viewport: VIEWPORT,
|
|
76
|
+
wait_for: 1200,
|
|
77
|
+
full_page: true,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (result.isError) {
|
|
82
|
+
throw new Error(`Failed to capture ${capture.route}: ${getText(result.content as Array<ImageContent | TextContent>) ?? "Unknown tool error"}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const outputPath = join(OUTPUT_DIR, `${capture.name}.png`);
|
|
86
|
+
writeFileSync(outputPath, Buffer.from(getImageData(result.content as Array<ImageContent | TextContent>), "base64"));
|
|
87
|
+
console.log(`saved ${outputPath}`);
|
|
88
|
+
}
|
|
89
|
+
} finally {
|
|
90
|
+
await transport.close();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
main().catch((error) => {
|
|
95
|
+
console.error(error instanceof Error ? error.message : error);
|
|
96
|
+
process.exitCode = 1;
|
|
97
|
+
});
|