openuispec 0.2.19 → 0.2.20

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 (38) hide show
  1. package/dist/check/audit.js +392 -0
  2. package/dist/check/index.js +216 -0
  3. package/dist/cli/configure-target.js +391 -0
  4. package/dist/cli/index.js +510 -0
  5. package/dist/cli/init.js +1047 -0
  6. package/dist/drift/index.js +903 -0
  7. package/dist/mcp-server/index.js +886 -0
  8. package/dist/mcp-server/preview-render.js +1761 -0
  9. package/dist/mcp-server/preview.js +233 -0
  10. package/dist/mcp-server/screenshot-android.js +458 -0
  11. package/dist/mcp-server/screenshot-ios.js +639 -0
  12. package/dist/mcp-server/screenshot-shared.js +180 -0
  13. package/dist/mcp-server/screenshot.js +459 -0
  14. package/dist/prepare/index.js +1216 -0
  15. package/dist/runtime/package-paths.js +33 -0
  16. package/dist/schema/semantic-lint.js +564 -0
  17. package/dist/schema/validate.js +689 -0
  18. package/dist/status/index.js +194 -0
  19. package/package.json +12 -13
  20. package/check/audit.ts +0 -426
  21. package/check/index.ts +0 -320
  22. package/cli/configure-target.ts +0 -523
  23. package/cli/index.ts +0 -537
  24. package/cli/init.ts +0 -1253
  25. package/drift/index.ts +0 -1165
  26. package/mcp-server/index.ts +0 -1041
  27. package/mcp-server/preview-render.ts +0 -1922
  28. package/mcp-server/preview.ts +0 -292
  29. package/mcp-server/screenshot-android.ts +0 -621
  30. package/mcp-server/screenshot-ios.ts +0 -753
  31. package/mcp-server/screenshot-shared.ts +0 -237
  32. package/mcp-server/screenshot.ts +0 -563
  33. package/prepare/index.ts +0 -1530
  34. package/schema/semantic-lint.ts +0 -692
  35. package/schema/validate.ts +0 -870
  36. package/scripts/regenerate-previews.ts +0 -136
  37. package/scripts/take-all-screenshots.ts +0 -507
  38. package/status/index.ts +0 -275
package/cli/index.ts DELETED
@@ -1,537 +0,0 @@
1
- #!/usr/bin/env -S npx tsx
2
- /**
3
- * OpenUISpec CLI
4
- *
5
- * Usage:
6
- * openuispec init Create a new spec project
7
- * openuispec init --defaults Scaffold non-interactively with unconfirmed defaults
8
- * openuispec init --list-options Print init prompt options as JSON
9
- * openuispec configure-target <t> Configure and confirm target stack choices
10
- * openuispec configure-target <t> --list-options Print target stack prompt options as JSON
11
- * openuispec drift [--target <t>] Check for spec drift
12
- * openuispec drift --snapshot --target <t> Snapshot current state + git baseline
13
- * openuispec drift --target <t> --explain Explain semantic changes since baseline
14
- * openuispec prepare --target <t> Build the target work bundle
15
- * openuispec status Show cross-target baseline/drift status
16
- * openuispec check --target <t> [--json] Composite validation + prepare readiness
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-component <name> [--variant v] Get a component spec
22
- * openuispec get-tokens <category> Get tokens for a category
23
- * openuispec get-locale <locale> [--keys k1,k2] Get a locale file
24
- * openuispec spec-types List available spec types
25
- * openuispec spec-schema <type> Get JSON schema for a spec type
26
- * openuispec screenshot [--route /path] Screenshot the web app
27
- * openuispec screenshot-android [opts] Screenshot Android app on emulator
28
- * openuispec screenshot-android-batch [opts] Batch screenshot Android app on emulator
29
- * openuispec screenshot-ios [opts] Screenshot iOS app on simulator
30
- */
31
-
32
- import { init, updateRules, extractRulesVersion, getPackageVersion } from "./init.js";
33
- import { join, dirname, relative, resolve } from "node:path";
34
- import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from "node:fs";
35
- import { fileURLToPath } from "node:url";
36
-
37
- // ── arg parsing helpers ──────────────────────────────────────────────
38
-
39
- function getFlag(argv: string[], name: string): boolean {
40
- return argv.includes(name);
41
- }
42
-
43
- function getOption(argv: string[], name: string): string | null {
44
- const idx = argv.indexOf(name);
45
- return idx !== -1 && argv[idx + 1] ? argv[idx + 1] : null;
46
- }
47
-
48
- function getPositional(argv: string[], startIdx = 0): string[] {
49
- return argv.slice(startIdx).filter((a) => !a.startsWith("--"));
50
- }
51
-
52
- function readJsonFile(filePath: string): any {
53
- return JSON.parse(readFileSync(resolve(filePath), "utf-8"));
54
- }
55
-
56
- function parseBatchConfig(rest: string[], usage: string): { config: any; captures: any[] } {
57
- const configPath = getOption(rest, "--config");
58
- if (!configPath) {
59
- console.error(usage);
60
- process.exit(1);
61
- }
62
- const config = readJsonFile(configPath);
63
- const captures = Array.isArray(config) ? config : config.captures;
64
- if (!Array.isArray(captures) || captures.length === 0) {
65
- console.error("Batch config must be an array of captures or an object with a non-empty 'captures' array.");
66
- process.exit(1);
67
- }
68
- return { config, captures };
69
- }
70
-
71
- // ── rules version check ─────────────────────────────────────────────
72
-
73
- function checkRulesVersion(): void {
74
- const cwd = process.cwd();
75
- const installed = getPackageVersion();
76
- for (const file of ["CLAUDE.md", "AGENTS.md"]) {
77
- const filePath = join(cwd, file);
78
- if (!existsSync(filePath)) continue;
79
- const rulesVersion = extractRulesVersion(filePath);
80
- if (rulesVersion && rulesVersion !== installed) {
81
- console.log(
82
- `\n⚠ ${file} rules were generated by v${rulesVersion} but v${installed} is installed.` +
83
- `\n Run: openuispec update-rules\n`
84
- );
85
- } else if (!rulesVersion) {
86
- const content = readFileSync(filePath, "utf-8");
87
- if (content.includes("OpenUISpec")) {
88
- console.log(
89
- `\n⚠ ${file} has OpenUISpec rules without a version marker.` +
90
- `\n Run: openuispec update-rules\n`
91
- );
92
- }
93
- }
94
- }
95
- }
96
-
97
- // ── spec helpers (shared with MCP server) ────────────────────────────
98
-
99
- function lookupLocaleKey(content: Record<string, unknown>, key: string): { found: boolean; value: unknown } {
100
- if (key in content) return { found: true, value: content[key] };
101
- const parts = key.split(".");
102
- let current: unknown = content;
103
- for (const part of parts) {
104
- if (current === null || current === undefined || typeof current !== "object" || Array.isArray(current)) {
105
- return { found: false, value: undefined };
106
- }
107
- current = (current as Record<string, unknown>)[part];
108
- }
109
- return current !== undefined ? { found: true, value: current } : { found: false, value: undefined };
110
- }
111
-
112
- function resolveSpecDir(projectDir: string, manifest: any, key: string): string {
113
- return resolve(projectDir, manifest.includes?.[key] ?? `./${key}/`);
114
- }
115
-
116
- const SCHEMA_CATALOG: Record<string, { file: string; title: string; description: string }> = {
117
- manifest: { file: "openuispec.schema.json", title: "Root Manifest", description: "Root manifest (openuispec.yaml)" },
118
- screen: { file: "screen.schema.json", title: "Screen", description: "Screen composition: layout, sections, navigation" },
119
- flow: { file: "flow.schema.json", title: "Flow", description: "Navigation flow definitions" },
120
- platform: { file: "platform.schema.json", title: "Platform", description: "Platform-specific generation config" },
121
- contract: { file: "contract.schema.json", title: "Contract", description: "Built-in UI contract definitions" },
122
- "custom-contract":{ file: "custom-contract.schema.json", title: "Custom Contract", description: "User-defined UI contract definitions (x_ prefixed)" },
123
- component: { file: "component.schema.json", title: "Component", description: "Reusable composition of contracts with named slots" },
124
- locale: { file: "locale.schema.json", title: "Locale", description: "Locale translation files" },
125
- "tokens/color": { file: "tokens/color.schema.json", title: "Color Tokens", description: "Color tokens" },
126
- "tokens/typography": { file: "tokens/typography.schema.json", title: "Typography Tokens", description: "Typography tokens" },
127
- "tokens/spacing": { file: "tokens/spacing.schema.json", title: "Spacing Tokens", description: "Spacing tokens" },
128
- "tokens/elevation": { file: "tokens/elevation.schema.json", title: "Elevation Tokens", description: "Elevation tokens" },
129
- "tokens/motion": { file: "tokens/motion.schema.json", title: "Motion Tokens", description: "Motion tokens" },
130
- "tokens/layout": { file: "tokens/layout.schema.json", title: "Layout Tokens", description: "Layout tokens" },
131
- "tokens/themes": { file: "tokens/themes.schema.json", title: "Theme Tokens", description: "Theme definitions" },
132
- "tokens/icons": { file: "tokens/icons.schema.json", title: "Icon Tokens", description: "Icon tokens" },
133
- };
134
-
135
- // ── main ─────────────────────────────────────────────────────────────
136
-
137
- async function main(): Promise<void> {
138
- const [command, ...rest] = process.argv.slice(2);
139
- const cwd = process.cwd();
140
-
141
- switch (command) {
142
- case "init":
143
- await init(rest);
144
- break;
145
-
146
- case "update-rules":
147
- updateRules();
148
- break;
149
-
150
- case "configure-target": {
151
- const { runConfigureTarget } = await import("./configure-target.js");
152
- await runConfigureTarget(rest);
153
- break;
154
- }
155
-
156
- case "drift": {
157
- const { runDrift } = await import("../drift/index.js");
158
- runDrift(rest);
159
- break;
160
- }
161
-
162
- case "prepare": {
163
- const { runPrepare } = await import("../prepare/index.js");
164
- runPrepare(rest);
165
- break;
166
- }
167
-
168
- case "status": {
169
- const { runStatus } = await import("../status/index.js");
170
- runStatus(rest);
171
- break;
172
- }
173
-
174
- case "check": {
175
- const { runCheck } = await import("../check/index.js");
176
- runCheck(rest);
177
- break;
178
- }
179
-
180
- case "validate": {
181
- const { runValidate } = await import("../schema/validate.js");
182
- runValidate(rest);
183
- checkRulesVersion();
184
- break;
185
- }
186
-
187
- case "mcp": {
188
- const { startMcpServer } = await import("../mcp-server/index.js");
189
- await startMcpServer();
190
- break;
191
- }
192
-
193
- // ── spec getters ───────────────────────────────────────────────
194
-
195
- case "read-specs": {
196
- const { findProjectDir, discoverSpecFiles } = await import("../drift/index.js");
197
- const projectDir = findProjectDir(cwd);
198
- const allFiles = discoverSpecFiles(projectDir);
199
- const paths = getPositional(rest);
200
-
201
- const filesToRead = paths.length > 0
202
- ? allFiles.filter((f) => {
203
- const rel = relative(projectDir, f);
204
- return paths.some((p) => rel === p || rel.endsWith(p));
205
- })
206
- : allFiles;
207
-
208
- const contents = filesToRead.map((f) => ({
209
- path: relative(projectDir, f),
210
- content: readFileSync(f, "utf-8"),
211
- }));
212
- console.log(JSON.stringify(contents, null, 2));
213
- break;
214
- }
215
-
216
- case "get-screen": {
217
- const name = rest[0];
218
- if (!name) { console.error("Usage: openuispec get-screen <name>"); process.exit(1); }
219
- const { findProjectDir } = await import("../drift/index.js");
220
- const YAML = (await import("yaml")).default;
221
- const projectDir = findProjectDir(cwd);
222
- const manifest = YAML.parse(readFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
223
- const screensDir = resolveSpecDir(projectDir, manifest, "screens");
224
- const filePath = join(screensDir, `${name}.yaml`);
225
- if (!existsSync(filePath)) { console.error(`Screen "${name}" not found at ${filePath}`); process.exit(1); }
226
- console.log(readFileSync(filePath, "utf-8"));
227
- break;
228
- }
229
-
230
- case "get-contract":
231
- case "get-component": {
232
- const specType = command === "get-contract" ? "contract" : "component";
233
- const specDir = specType === "contract" ? "contracts" : "components";
234
- const name = rest[0];
235
- if (!name) { console.error(`Usage: openuispec get-${specType} <name> [--variant v]`); process.exit(1); }
236
- const variant = getOption(rest, "--variant");
237
- const { findProjectDir } = await import("../drift/index.js");
238
- const YAML = (await import("yaml")).default;
239
- const projectDir = findProjectDir(cwd);
240
- const manifest = YAML.parse(readFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
241
- const dir = resolveSpecDir(projectDir, manifest, specDir);
242
-
243
- if (!existsSync(dir)) { console.error(`${specDir} directory not found: ${dir}`); process.exit(1); }
244
-
245
- let found = false;
246
- for (const file of readdirSync(dir).filter(f => f.endsWith(".yaml")).sort()) {
247
- const filePath = join(dir, file);
248
- const raw = readFileSync(filePath, "utf-8");
249
- const content = YAML.parse(raw);
250
- const rootKey = Object.keys(content)[0];
251
- if (rootKey !== name) continue;
252
- found = true;
253
-
254
- if (variant) {
255
- const variantDef = content[rootKey]?.variants?.[variant];
256
- if (!variantDef) {
257
- const available = Object.keys(content[rootKey]?.variants ?? {}).join(", ");
258
- console.error(`Variant "${variant}" not found. Available: ${available}`);
259
- process.exit(1);
260
- }
261
- console.log(JSON.stringify({ name, variant, definition: variantDef }, null, 2));
262
- } else {
263
- console.log(raw);
264
- }
265
- break;
266
- }
267
- if (!found) { console.error(`${specType} "${name}" not found in ${dir}`); process.exit(1); }
268
- break;
269
- }
270
-
271
- case "get-tokens": {
272
- const category = rest[0];
273
- if (!category) { console.error("Usage: openuispec get-tokens <category>"); process.exit(1); }
274
- const { findProjectDir } = await import("../drift/index.js");
275
- const YAML = (await import("yaml")).default;
276
- const projectDir = findProjectDir(cwd);
277
- const manifest = YAML.parse(readFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
278
- const tokensDir = resolveSpecDir(projectDir, manifest, "tokens");
279
-
280
- for (const ext of [".yaml", ".yml"]) {
281
- const filePath = join(tokensDir, `${category}${ext}`);
282
- if (existsSync(filePath)) { console.log(readFileSync(filePath, "utf-8")); process.exit(0); }
283
- }
284
-
285
- const available = readdirSync(tokensDir).filter(f => f.endsWith(".yaml") || f.endsWith(".yml")).map(f => f.replace(/\.ya?ml$/, ""));
286
- console.error(`Token category "${category}" not found. Available: ${available.join(", ")}`);
287
- process.exit(1);
288
- break;
289
- }
290
-
291
- case "get-locale": {
292
- const locale = rest[0];
293
- if (!locale) { console.error("Usage: openuispec get-locale <locale> [--keys k1,k2,k3]"); process.exit(1); }
294
- const keysStr = getOption(rest, "--keys");
295
- const keys = keysStr ? keysStr.split(",").map(k => k.trim()) : null;
296
- const { findProjectDir } = await import("../drift/index.js");
297
- const YAML = (await import("yaml")).default;
298
- const projectDir = findProjectDir(cwd);
299
- const manifest = YAML.parse(readFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
300
- const localesDir = resolveSpecDir(projectDir, manifest, "locales");
301
- const filePath = join(localesDir, `${locale}.json`);
302
-
303
- if (!existsSync(filePath)) {
304
- const available = existsSync(localesDir)
305
- ? readdirSync(localesDir).filter(f => f.endsWith(".json")).map(f => f.replace(".json", ""))
306
- : [];
307
- console.error(`Locale "${locale}" not found. Available: ${available.join(", ")}`);
308
- process.exit(1);
309
- }
310
-
311
- const content = JSON.parse(readFileSync(filePath, "utf-8"));
312
- if (keys) {
313
- const filtered: Record<string, unknown> = {};
314
- for (const key of keys) {
315
- const result = lookupLocaleKey(content, key);
316
- if (result.found) filtered[key] = result.value;
317
- }
318
- console.log(JSON.stringify(filtered, null, 2));
319
- } else {
320
- console.log(JSON.stringify(content, null, 2));
321
- }
322
- break;
323
- }
324
-
325
- // ── schema reference ───────────────────────────────────────────
326
-
327
- case "spec-types": {
328
- const types = Object.entries(SCHEMA_CATALOG).map(([type, info]) => ({
329
- type, title: info.title, description: info.description,
330
- }));
331
- console.log(JSON.stringify(types, null, 2));
332
- break;
333
- }
334
-
335
- case "spec-schema": {
336
- const type = rest[0];
337
- if (!type) { console.error("Usage: openuispec spec-schema <type>\nRun `openuispec spec-types` to list types."); process.exit(1); }
338
- const entry = SCHEMA_CATALOG[type];
339
- if (!entry) { console.error(`Unknown spec type "${type}". Run \`openuispec spec-types\` to list types.`); process.exit(1); }
340
- const __dirname = dirname(fileURLToPath(import.meta.url));
341
- const schemaPath = join(__dirname, "..", "schema", entry.file);
342
- console.log(readFileSync(schemaPath, "utf-8"));
343
- break;
344
- }
345
-
346
- // ── screenshot tools ───────────────────────────────────────────
347
-
348
- case "screenshot": {
349
- const { takeScreenshot } = await import("../mcp-server/screenshot.js");
350
- const result = await takeScreenshot(cwd, {
351
- route: getOption(rest, "--route") ?? "/",
352
- viewport: {
353
- width: parseInt(getOption(rest, "--width") ?? "1280"),
354
- height: parseInt(getOption(rest, "--height") ?? "800"),
355
- },
356
- scale: parseFloat(getOption(rest, "--scale") ?? "2"),
357
- theme: getOption(rest, "--theme") as "light" | "dark" | undefined,
358
- wait_for: parseInt(getOption(rest, "--wait-for") ?? "1000"),
359
- full_page: getFlag(rest, "--full-page"),
360
- selector: getOption(rest, "--selector") ?? undefined,
361
- output_dir: getOption(rest, "--output-dir") ?? undefined,
362
- });
363
- printScreenshotResult(result);
364
- break;
365
- }
366
-
367
- case "screenshot-android": {
368
- const { takeAndroidScreenshot } = await import("../mcp-server/screenshot-android.js");
369
- const result = await takeAndroidScreenshot(cwd, {
370
- screen: getOption(rest, "--screen") ?? undefined,
371
- route: getOption(rest, "--route") ?? undefined,
372
- nav: getOption(rest, "--nav")?.split(",") ?? undefined,
373
- theme: getOption(rest, "--theme") as "light" | "dark" | undefined,
374
- wait_for: parseInt(getOption(rest, "--wait-for") ?? "3000"),
375
- output_dir: getOption(rest, "--output-dir") ?? undefined,
376
- project_dir: getOption(rest, "--project-dir") ?? undefined,
377
- module: getOption(rest, "--module") ?? undefined,
378
- });
379
- printScreenshotResult(result);
380
- break;
381
- }
382
-
383
- case "screenshot-android-batch": {
384
- const { takeAndroidScreenshotBatch } = await import("../mcp-server/screenshot-android.js");
385
- const { config, captures } = parseBatchConfig(rest,
386
- "Usage: openuispec screenshot-android-batch --config <captures.json> [--project-dir path] [--module name] [--theme light|dark] [--output-dir dir]");
387
-
388
- const result = await takeAndroidScreenshotBatch(cwd, {
389
- captures,
390
- theme: (getOption(rest, "--theme") ?? config.theme) as "light" | "dark" | undefined,
391
- output_dir: getOption(rest, "--output-dir") ?? config.output_dir ?? undefined,
392
- project_dir: getOption(rest, "--project-dir") ?? config.project_dir ?? undefined,
393
- module: getOption(rest, "--module") ?? config.module ?? undefined,
394
- });
395
- printScreenshotResult(result);
396
- break;
397
- }
398
-
399
- case "screenshot-web-batch": {
400
- const { takeScreenshotBatch } = await import("../mcp-server/screenshot.js");
401
- const { config, captures } = parseBatchConfig(rest,
402
- "Usage: openuispec screenshot-web-batch --config <captures.json> [--theme light|dark] [--output-dir dir]");
403
-
404
- const result = await takeScreenshotBatch(cwd, {
405
- captures,
406
- viewport: config.viewport,
407
- scale: parseFloat(getOption(rest, "--scale") ?? config.scale ?? "2"),
408
- theme: (getOption(rest, "--theme") ?? config.theme) as "light" | "dark" | undefined,
409
- output_dir: getOption(rest, "--output-dir") ?? config.output_dir ?? undefined,
410
- });
411
- printScreenshotResult(result);
412
- break;
413
- }
414
-
415
- case "screenshot-ios": {
416
- const { takeIOSScreenshot } = await import("../mcp-server/screenshot-ios.js");
417
- const result = await takeIOSScreenshot(cwd, {
418
- screen: getOption(rest, "--screen") ?? undefined,
419
- device: getOption(rest, "--device") ?? undefined,
420
- nav: getOption(rest, "--nav")?.split(",") ?? undefined,
421
- theme: getOption(rest, "--theme") as "light" | "dark" | undefined,
422
- wait_for: parseInt(getOption(rest, "--wait-for") ?? "3000"),
423
- output_dir: getOption(rest, "--output-dir") ?? undefined,
424
- project_dir: getOption(rest, "--project-dir") ?? undefined,
425
- scheme: getOption(rest, "--scheme") ?? undefined,
426
- bundle_id: getOption(rest, "--bundle-id") ?? undefined,
427
- });
428
- printScreenshotResult(result);
429
- break;
430
- }
431
-
432
- case "screenshot-ios-batch": {
433
- const { takeIOSScreenshotBatch } = await import("../mcp-server/screenshot-ios.js");
434
- const { config, captures } = parseBatchConfig(rest,
435
- "Usage: openuispec screenshot-ios-batch --config <captures.json> [--project-dir path] [--scheme name] [--bundle-id id] [--device name] [--theme light|dark] [--output-dir dir]");
436
-
437
- const result = await takeIOSScreenshotBatch(cwd, {
438
- captures,
439
- device: getOption(rest, "--device") ?? config.device ?? undefined,
440
- theme: (getOption(rest, "--theme") ?? config.theme) as "light" | "dark" | undefined,
441
- output_dir: getOption(rest, "--output-dir") ?? config.output_dir ?? undefined,
442
- project_dir: getOption(rest, "--project-dir") ?? config.project_dir ?? undefined,
443
- scheme: getOption(rest, "--scheme") ?? config.scheme ?? undefined,
444
- bundle_id: getOption(rest, "--bundle-id") ?? config.bundle_id ?? undefined,
445
- });
446
- printScreenshotResult(result);
447
- break;
448
- }
449
-
450
- // ── help ────────────────────────────────────────────────────────
451
-
452
- case undefined:
453
- case "--help":
454
- case "-h":
455
- console.log(`
456
- OpenUISpec CLI v${getPackageVersion()}
457
-
458
- Usage:
459
- openuispec init Create a new spec project
460
- openuispec init --defaults Scaffold non-interactively with unconfirmed defaults
461
- openuispec init --no-configure-targets Skip target stack setup during init
462
- openuispec update-rules Update AI rules to match installed version
463
- openuispec configure-target <t> [--defaults] Configure target stack
464
- openuispec configure-target <t> --set k=v Set specific stack values (confirmed)
465
-
466
- Workflow:
467
- openuispec status Show cross-target baseline/drift status
468
- openuispec drift [--target <t>] Check for spec drift
469
- openuispec drift --snapshot --target <t> Snapshot current state + git baseline
470
- openuispec drift --target <t> --explain Explain semantic changes since baseline
471
- openuispec prepare --target <t> Build the target work bundle
472
- openuispec check --target <t> [--json] Composite validation + prepare readiness
473
- openuispec validate [group...] [--json] Validate spec files
474
-
475
- Spec access:
476
- openuispec read-specs [paths...] Read spec file contents as JSON
477
- openuispec get-screen <name> Get a single screen spec (YAML)
478
- openuispec get-contract <name> [--variant v] Get a contract spec
479
- openuispec get-component <name> [--variant v] Get a component spec
480
- openuispec get-tokens <category> Get tokens for a category (YAML)
481
- openuispec get-locale <locale> [--keys k1,k2] Get a locale file (JSON)
482
- openuispec spec-types List available spec types
483
- openuispec spec-schema <type> Get full JSON schema for a spec type
484
-
485
- Screenshots (single):
486
- openuispec screenshot [--route /path] [--width px] [--height px] [--scale n] [--theme light|dark] [--output-dir dir]
487
- openuispec screenshot-android [--screen name] [--project-dir path] [--module name]
488
- [--route deeplink] [--nav Step1,Step2] [--theme light|dark] [--output-dir dir]
489
- openuispec screenshot-ios [--screen name] [--project-dir path] [--scheme name]
490
- [--bundle-id id] [--device name] [--nav Step1,Step2] [--theme light|dark]
491
-
492
- Screenshots (batch — build once, capture many):
493
- openuispec screenshot-web-batch --config captures.json [--scale n] [--theme light|dark] [--output-dir dir]
494
- openuispec screenshot-android-batch --config captures.json [--project-dir path]
495
- [--module name] [--theme light|dark] [--output-dir dir]
496
- openuispec screenshot-ios-batch --config captures.json [--project-dir path]
497
- [--scheme name] [--bundle-id id] [--device name] [--theme light|dark] [--output-dir dir]
498
-
499
- Server:
500
- openuispec mcp Start MCP server (stdio transport)
501
-
502
- Validate groups: manifest, tokens, screens, flows, platform, locales, contracts, components, semantic
503
- Exit codes: 0 = success, 1 = missing config/usage error, 2 = validation failure
504
- Docs: https://openuispec.rsteam.uz
505
- `);
506
- break;
507
-
508
- default:
509
- console.error(`Unknown command: ${command}`);
510
- console.error(`Run "openuispec --help" for usage.`);
511
- process.exit(1);
512
- }
513
- }
514
-
515
- // ── screenshot result printer ────────────────────────────────────────
516
-
517
- function printScreenshotResult(result: { content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; isError?: boolean }): void {
518
- if (result.isError) {
519
- for (const item of result.content) {
520
- if (item.type === "text") console.error(item.text);
521
- }
522
- process.exit(1);
523
- }
524
- for (const item of result.content) {
525
- if (item.type === "text") console.log(item.text);
526
- if (item.type === "image" && item.data) {
527
- const outFile = `screenshot-${Date.now()}.png`;
528
- writeFileSync(outFile, Buffer.from(item.data, "base64"));
529
- console.log(`Screenshot saved: ${outFile}`);
530
- }
531
- }
532
- }
533
-
534
- main().catch((err) => {
535
- console.error(err instanceof Error ? err.message : String(err));
536
- process.exit(1);
537
- });