openuispec 0.2.18 → 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 (45) hide show
  1. package/README.md +2 -10
  2. package/dist/check/audit.js +392 -0
  3. package/dist/check/index.js +216 -0
  4. package/dist/cli/configure-target.js +391 -0
  5. package/dist/cli/index.js +510 -0
  6. package/dist/cli/init.js +1047 -0
  7. package/dist/drift/index.js +903 -0
  8. package/dist/mcp-server/index.js +886 -0
  9. package/dist/mcp-server/preview-render.js +1761 -0
  10. package/dist/mcp-server/preview.js +233 -0
  11. package/dist/mcp-server/screenshot-android.js +458 -0
  12. package/dist/mcp-server/screenshot-ios.js +639 -0
  13. package/dist/mcp-server/screenshot-shared.js +180 -0
  14. package/dist/mcp-server/screenshot.js +459 -0
  15. package/dist/prepare/index.js +1216 -0
  16. package/dist/runtime/package-paths.js +33 -0
  17. package/dist/schema/semantic-lint.js +564 -0
  18. package/dist/schema/validate.js +689 -0
  19. package/dist/status/index.js +194 -0
  20. package/docs/images/how-it-works.svg +56 -0
  21. package/docs/images/workflows.svg +76 -0
  22. package/package.json +12 -13
  23. package/check/audit.ts +0 -426
  24. package/check/index.ts +0 -320
  25. package/cli/configure-target.ts +0 -523
  26. package/cli/index.ts +0 -537
  27. package/cli/init.ts +0 -1253
  28. package/docs/images/how-it-works-dark.png +0 -0
  29. package/docs/images/how-it-works-light.png +0 -0
  30. package/docs/images/workflows-dark.png +0 -0
  31. package/docs/images/workflows-light.png +0 -0
  32. package/drift/index.ts +0 -1165
  33. package/mcp-server/index.ts +0 -1041
  34. package/mcp-server/preview-render.ts +0 -1922
  35. package/mcp-server/preview.ts +0 -292
  36. package/mcp-server/screenshot-android.ts +0 -621
  37. package/mcp-server/screenshot-ios.ts +0 -753
  38. package/mcp-server/screenshot-shared.ts +0 -237
  39. package/mcp-server/screenshot.ts +0 -563
  40. package/prepare/index.ts +0 -1530
  41. package/schema/semantic-lint.ts +0 -692
  42. package/schema/validate.ts +0 -870
  43. package/scripts/regenerate-previews.ts +0 -136
  44. package/scripts/take-all-screenshots.ts +0 -507
  45. package/status/index.ts +0 -275
@@ -0,0 +1,1047 @@
1
+ /**
2
+ * openuispec init — interactive project scaffolding
3
+ *
4
+ * Creates folder structure, manifest, and AI assistant rules
5
+ * (CLAUDE.md / AGENTS.md) so AI tools track spec changes properly.
6
+ */
7
+ import { createInterface } from "node:readline/promises";
8
+ import { stdin, stdout } from "node:process";
9
+ import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, appendFileSync, unlinkSync, } from "node:fs";
10
+ import { join, relative } from "node:path";
11
+ import { SUPPORTED_TARGETS, isSupportedTarget } from "../drift/index.js";
12
+ import { readPackageVersion, resolvePackagePath } from "../runtime/package-paths.js";
13
+ export function listInitOptions() {
14
+ const defaults = collectDefaults();
15
+ return {
16
+ command: "init",
17
+ note: "After init, run `openuispec configure-target <target> --list-options` for each target to get stack choices.",
18
+ questions: [
19
+ {
20
+ key: "name",
21
+ prompt: "Project name",
22
+ type: "text",
23
+ default: defaults.name,
24
+ },
25
+ {
26
+ key: "spec_dir",
27
+ prompt: "Spec directory",
28
+ type: "text",
29
+ default: defaults.specDir,
30
+ },
31
+ {
32
+ key: "targets",
33
+ prompt: "Which platforms?",
34
+ type: "list",
35
+ default: defaults.targets,
36
+ options: [...SUPPORTED_TARGETS],
37
+ },
38
+ {
39
+ key: "with_api",
40
+ prompt: "Will this spec declare API endpoints?",
41
+ type: "yes_no",
42
+ default: defaults.withApi,
43
+ },
44
+ {
45
+ key: "backend_path",
46
+ prompt: "Backend folder path relative to openuispec.yaml",
47
+ type: "text",
48
+ default: defaults.backendPath ?? "../backend/",
49
+ },
50
+ {
51
+ key: "configure_targets",
52
+ prompt: "Configure target stacks now?",
53
+ type: "yes_no",
54
+ default: defaults.configureTargets,
55
+ },
56
+ {
57
+ key: "with_shared",
58
+ prompt: "Does the project share code between platforms (e.g. KMP commonMain)?",
59
+ type: "yes_no",
60
+ default: false,
61
+ },
62
+ ],
63
+ configure_targets_note: "If configure_targets is true, use `openuispec configure-target <target> --list-options` for each target after init to present stack choices to the user.",
64
+ shared_layer_note: "If with_shared is true, add shared layer config to generation.shared in the manifest. Each shared layer needs: name, platforms (subset of targets), language, root (path relative to openuispec.yaml), tracks (spec categories: manifest, contracts, flows, screens, tokens, platform, locales), and scope (what code belongs there). Also add generation.structure entries for each target to define where platform-specific UI code goes and its scope.",
65
+ };
66
+ }
67
+ // ── prompts ──────────────────────────────────────────────────────────
68
+ export async function ask(rl, question, fallback) {
69
+ const suffix = fallback ? ` (${fallback})` : "";
70
+ const answer = (await rl.question(`${question}${suffix}: `)).trim();
71
+ return answer || fallback || "";
72
+ }
73
+ async function askList(rl, question, options, defaults) {
74
+ const defaultStr = defaults.join(", ");
75
+ const raw = (await rl.question(`${question} [${options.join(", ")}] (${defaultStr}): `)).trim();
76
+ if (!raw)
77
+ return defaults;
78
+ return raw
79
+ .split(",")
80
+ .map((s) => s.trim().toLowerCase())
81
+ .filter((s) => options.includes(s));
82
+ }
83
+ export async function askChoice(rl, question, options, fallback) {
84
+ const answer = (await rl.question(`${question} [${options.join(", ")}] (${fallback}): `))
85
+ .trim()
86
+ .toLowerCase();
87
+ if (!answer)
88
+ return fallback;
89
+ return options.includes(answer) ? answer : fallback;
90
+ }
91
+ async function askYesNo(rl, question, fallback) {
92
+ const answer = await askChoice(rl, question, ["yes", "no"], fallback ? "yes" : "no");
93
+ return answer === "yes";
94
+ }
95
+ // ── scaffold ─────────────────────────────────────────────────────────
96
+ function ensureDir(path) {
97
+ mkdirSync(path, { recursive: true });
98
+ }
99
+ function writeIfMissing(path, content, quiet = false) {
100
+ if (existsSync(path)) {
101
+ if (!quiet)
102
+ console.log(` skip ${relative(process.cwd(), path)} (exists)`);
103
+ return false;
104
+ }
105
+ writeFileSync(path, content);
106
+ if (!quiet)
107
+ console.log(` create ${relative(process.cwd(), path)}`);
108
+ return true;
109
+ }
110
+ // ── version ─────────────────────────────────────────────────────────
111
+ const RULES_START_MARKER = "<!-- openuispec-rules-start -->";
112
+ const RULES_END_MARKER = "<!-- openuispec-rules-end -->";
113
+ function getPackageVersion() {
114
+ return readPackageVersion(import.meta.url);
115
+ }
116
+ // ── templates ────────────────────────────────────────────────────────
117
+ function manifestTemplate(name, targets, options) {
118
+ const targetList = targets.join(", ");
119
+ const outputLines = targets
120
+ .map((t) => {
121
+ if (t === "ios")
122
+ return ` ios: { language: swift, framework: swiftui, min_version: "17.0" }`;
123
+ if (t === "android")
124
+ return ` android: { language: kotlin, framework: compose, min_sdk: 26 }`;
125
+ if (t === "web")
126
+ return ` web: { language: typescript, framework: react, bundler: vite }`;
127
+ return ` ${t}: {}`;
128
+ })
129
+ .join("\n");
130
+ function yamlPathsBlock(paths) {
131
+ const entries = Object.entries(paths);
132
+ return entries.length > 0
133
+ ? `\n paths:\n${entries.map(([k, v]) => ` ${k}: "${v}"`).join("\n")}`
134
+ : "";
135
+ }
136
+ let sharedBlock = "";
137
+ if (options.sharedLayers.length > 0) {
138
+ const layers = options.sharedLayers.map((layer) => {
139
+ const tracksLine = layer.tracks.length > 0
140
+ ? `\n tracks: [${layer.tracks.join(", ")}]`
141
+ : "";
142
+ return ` ${layer.name}:
143
+ platforms: [${layer.platforms.join(", ")}]
144
+ language: ${layer.language}
145
+ root: "${layer.root}"
146
+ scope: "${layer.scope}"${tracksLine}${yamlPathsBlock(layer.paths)}`;
147
+ }).join("\n");
148
+ sharedBlock = ` shared:\n${layers}\n`;
149
+ }
150
+ let structureBlock = "";
151
+ if (options.structures.length > 0) {
152
+ const entries = options.structures.map((s) => {
153
+ const scopeLine = s.scope ? `\n scope: "${s.scope}"` : "";
154
+ return ` ${s.target}:
155
+ root: "${s.root}"${scopeLine}${yamlPathsBlock(s.paths)}`;
156
+ }).join("\n");
157
+ structureBlock = ` structure:\n${entries}\n`;
158
+ }
159
+ return `# ${name} — OpenUISpec v0.2
160
+ spec_version: "0.2"
161
+
162
+ project:
163
+ name: "${name}"
164
+ description: ""
165
+
166
+ includes:
167
+ tokens: "./tokens/"
168
+ contracts: "./contracts/"
169
+ components: "./components/"
170
+ screens: "./screens/"
171
+ flows: "./flows/"
172
+ platform: "./platform/"
173
+ locales: "./locales/"
174
+
175
+ i18n:
176
+ default_locale: "en"
177
+ supported_locales: [en]
178
+ fallback_strategy: "default"
179
+
180
+ generation:
181
+ targets: [${targetList}]
182
+ # extra_rules: # Optional: project-wide AI authoring conventions
183
+ # - "Generation hint strings may start with [common], [ios], [android], or [web] to indicate scope."
184
+ # output_dir: # Optional: map targets to code directories
185
+ # ios: "../ios-app/" # relative to this file
186
+ # android: "../android-app/"
187
+ # web: "../web-ui/"
188
+ ${options.withApi ? ` code_roots:
189
+ backend: "${options.backendPath}" # Required when api.endpoints are declared
190
+ ` : ""} output_format:
191
+ ${outputLines}
192
+ ${sharedBlock}${structureBlock}
193
+ data_model: {}
194
+
195
+ api:
196
+ base_url: "/api/v1"
197
+ auth: "bearer_token"
198
+ endpoints: {}
199
+ `;
200
+ }
201
+ function specReadmeTemplate(name, targets) {
202
+ const targetList = targets.join(", ");
203
+ return `# ${name} — OpenUISpec
204
+
205
+ This directory contains the **OpenUISpec** semantic UI specification for **${name}**.
206
+
207
+ **Start here:** read \`openuispec.yaml\` — it defines the project structure, data model, API endpoints, and generation targets (**${targetList}**).
208
+
209
+ ## Directory structure
210
+
211
+ | Directory | Contents |
212
+ |-----------|----------|
213
+ | \`tokens/\` | Design tokens — colors, typography, spacing, elevation, motion, icons, themes |
214
+ | \`screens/\` | Screen definitions — one YAML file per screen |
215
+ | \`flows/\` | Navigation flows — multi-step user journeys |
216
+ | \`contracts/\` | Component contracts — standard extensions and custom (\`x_\` prefixed) |
217
+ | \`components/\` | Reusable component compositions — contract slot compositions with states and variants |
218
+ | \`platform/\` | Platform overrides — per-target (iOS, Android, Web) behaviors |
219
+ | \`locales/\` | Localization — i18n strings (JSON, ICU MessageFormat) |
220
+
221
+ ## IMPORTANT — Read the specification before working with spec files
222
+
223
+ The spec format, file schemas, and generation rules are defined in the installed \`openuispec\` package.
224
+ You MUST read these reference files before creating, editing, or generating from any spec file.
225
+ Do NOT guess the file format — skipping this step will produce invalid YAML that fails validation.
226
+
227
+ **Find the package in this order:**
228
+ 1. \`node_modules/openuispec/\` (project dependency)
229
+ 2. Run \`npm root -g\` → \`<prefix>/openuispec/\` (global install)
230
+ 3. Online: \`https://openuispec.rsteam.uz/llms-full.txt\` (if not installed)
231
+
232
+ **Reference files inside the package (read in this order):**
233
+ 1. \`README.md\` — schema tables, file format reference, root wrapper keys
234
+ 2. \`spec/openuispec-v0.2.md\` — full specification (contracts, layout, expressions, etc.)
235
+ 3. \`examples/taskflow/openuispec/\` — complete working example with all file types
236
+ 4. \`schema/\` — JSON Schemas for validation
237
+
238
+ ## MCP Tools (recommended for AI assistants)
239
+
240
+ When the openuispec MCP server is configured, AI assistants should use these tools instead of CLI commands:
241
+
242
+ | Tool | When to use |
243
+ |------|-------------|
244
+ | \`openuispec_spec_types\` | Discover available spec types and their descriptions. |
245
+ | \`openuispec_spec_schema\` | Get the full JSON schema for a specific spec type — exact structure, required fields, allowed values. |
246
+ | \`openuispec_prepare\` | **Before any UI code generation.** Returns spec context, platform config, and constraints. |
247
+ | \`openuispec_read_specs\` | Load spec file contents — the authoritative source for tokens, screens, contracts. |
248
+ | \`openuispec_check\` | After editing spec files. Validates schema + semantics + readiness. Optional \`screens\`/\`contracts\` params scope the audit. |
249
+ | \`openuispec_validate\` | Schema-only validation, optionally by group. |
250
+ | \`openuispec_drift\` | Detect spec changes since last snapshot. |
251
+ | \`openuispec_status\` | To understand cross-target state (baselines, drift, next steps). |
252
+ | \`openuispec_get_screen\` | Get a single screen spec by name — faster than \`read_specs\` for targeted edits. |
253
+ | \`openuispec_get_contract\` | Get a single contract spec, optionally filtered to one variant. |
254
+ | \`openuispec_get_tokens\` | Get tokens for a specific category (color, typography, spacing, etc.). |
255
+ | \`openuispec_get_locale\` | Get a single locale file, optionally filtered to specific keys. |
256
+ | \`openuispec_screenshot\` | Screenshot the web app at a route via headless browser (requires \`puppeteer\`). |
257
+ | \`openuispec_screenshot_android\` | Screenshot Android app on emulator — works with any project via \`project_dir\`. |
258
+ | \`openuispec_screenshot_ios\` | Screenshot iOS app on Simulator via XCUITest — works with any project via \`project_dir\`. |
259
+
260
+ ## CLI commands
261
+
262
+ \`\`\`bash
263
+ # Workflow
264
+ openuispec validate # Validate spec files against schemas
265
+ openuispec validate semantic # Run semantic cross-reference linting
266
+ openuispec configure-target ${targets[0]} [--defaults] # Configure target stack; --defaults stays unconfirmed
267
+ openuispec status # Show cross-target baseline/drift status
268
+ openuispec drift --target ${targets[0]} --explain # Explain semantic spec drift
269
+ openuispec prepare --target ${targets[0]} # Build the target work bundle
270
+ openuispec drift --snapshot --target ${targets[0]} # Snapshot current state + git baseline after target output exists
271
+
272
+ # Spec access
273
+ openuispec read-specs [paths...] # Read spec file contents as JSON
274
+ openuispec get-screen <name> # Get a single screen spec
275
+ openuispec get-contract <name> [--variant v] # Get a contract spec
276
+ openuispec get-tokens <category> # Get tokens for a category
277
+ openuispec get-locale <locale> [--keys k1,k2] # Get a locale file
278
+ openuispec spec-types # List available spec types
279
+ openuispec spec-schema <type> # Get JSON schema for a spec type
280
+
281
+ # Screenshots
282
+ openuispec screenshot --route /home --theme dark --output-dir screenshots
283
+ openuispec screenshot-android --screen home --project-dir /path/to/android
284
+ openuispec screenshot-ios --screen home --project-dir /path/to/ios --scheme MyApp
285
+ \`\`\`
286
+
287
+ The target work bundle has two modes:
288
+ - \`bootstrap\` when no snapshot exists yet, for first-time generation
289
+ - \`update\` after a snapshot exists, for drift-based target updates
290
+
291
+ If target stack values were written with \`--defaults\`, treat them as unconfirmed. Before generating code, ask the user to confirm or change the stack and run \`openuispec configure-target <target>\` without \`--defaults\`.
292
+
293
+ ## Learn more
294
+
295
+ Docs: https://openuispec.rsteam.uz
296
+ `;
297
+ }
298
+ function aiRulesBlock(specDir, targets) {
299
+ const targetList = targets.map((t) => `"${t}"`).join(", ");
300
+ const version = getPackageVersion();
301
+ return `
302
+ ${RULES_START_MARKER}
303
+ <!-- openuispec-rules-version: ${version} -->
304
+ # OpenUISpec — AI Assistant Rules
305
+ # ================================
306
+ # This project uses OpenUISpec to define UI as a semantic spec.
307
+ # Spec files are the single source of truth for all UI across platforms.
308
+ # Targets: ${targetList}
309
+
310
+ ## MANDATORY — UI work requires OpenUISpec tools
311
+
312
+ When the user's request involves UI — screens, navigation, layout, tokens, flows, localization,
313
+ or any visual/structural change — you MUST use the OpenUISpec tools before writing any code.
314
+
315
+ ### MCP Tools (use these when available)
316
+
317
+ Call these MCP tools directly. They return structured JSON with everything you need.
318
+
319
+ **Pre-generation:**
320
+ 1. Call \`openuispec_prepare\` with the target platform — returns spec context, platform config, constraints.
321
+ Use \`include_specs: true\` to embed all spec contents in one call (saves a separate read_specs).
322
+ 2. Call \`openuispec_read_specs\` to load spec file contents if not using include_specs.
323
+ Without paths: returns file listing. With paths: returns contents. Use these as the AUTHORITATIVE source.
324
+ 3. If spec changes are needed, update spec files FIRST, then call \`openuispec_check\`.
325
+ 4. Generate or update the platform UI code based on the spec contents.
326
+
327
+ **Post-generation (EVERY TIME after writing UI code):**
328
+ 5. Call \`openuispec_check\` to validate spec files (schema + semantics) and confirm prepare readiness.
329
+ Note: this validates the SPEC, not the generated code.
330
+ 6. Call \`openuispec_check\` with \`audit: true\` to get a spec-derived checklist, then manually review
331
+ the generated code against it. For each screen, verify:
332
+ - Every field/action in the spec has a corresponding UI element
333
+ - Token values (colors, spacing, radii) match exactly — no approximations
334
+ - Contract \`must_handle\` states are all implemented (loading, error, empty, etc.)
335
+ - Adaptive breakpoints match the spec's \`size_classes\`
336
+ - Locale keys match \`$t:\` references
337
+ - Navigation targets match flow definitions
338
+ 7. Report any real gaps found and fix them before finishing.
339
+
340
+ **Iterating before baseline:**
341
+ Generated code rarely needs just one pass. Read the spec, audit the generated code against it,
342
+ take screenshots to verify visuals, then fix gaps and repeat.
343
+ Multiple generate → review → fix cycles are expected before the user accepts the result.
344
+
345
+ **Baseline reminder:**
346
+ After generation, remind the user to review the output and run the baseline when satisfied:
347
+ > When you're happy with the generated output, run: \`openuispec drift --snapshot --target <t>\`
348
+ > This records the spec state so future changes are tracked as incremental drift.
349
+ Do not baseline on your own initiative — only run the snapshot when the user asks.
350
+
351
+ **Creating new spec files:**
352
+ - Call \`openuispec_spec_types\` to discover available spec types.
353
+ - Call \`openuispec_spec_schema\` with the specific type to get the full JSON schema.
354
+ - Write the spec file following the schema exactly.
355
+
356
+ **Focused getters (prefer these for incremental edits over \`read_specs\`):**
357
+ - \`openuispec_get_screen(name)\` — single screen spec
358
+ - \`openuispec_get_contract(name, variant?)\` — single contract, optionally one variant
359
+ - \`openuispec_get_component(name, variant?)\` — single component, optionally one variant
360
+ - \`openuispec_get_tokens(category)\` — single token category (color, typography, spacing, etc.)
361
+ - \`openuispec_get_locale(locale, keys?)\` — single locale file, optionally filtered keys
362
+ - \`openuispec_check(target, audit?, screens?, contracts?)\` — validation + optional scoped audit checklist
363
+
364
+ Use \`read_specs\` for full-project generation; use focused getters when editing one screen or contract.
365
+
366
+ **Other tools:**
367
+ - \`openuispec_status\` — cross-target summary, good starting point
368
+ - \`openuispec_drift\` with \`explain: true\` — property-level spec changes
369
+ - \`openuispec_validate\` — schema-only validation by group
370
+
371
+ ### CLI fallback (when MCP is not available)
372
+
373
+ If MCP tools are not available, use these CLI commands with \`--json\` flag:
374
+
375
+ **Status & discovery:**
376
+ - \`openuispec status --json\` — cross-target status
377
+ - \`openuispec spec-types\` — list available spec types
378
+ - \`openuispec spec-schema <type>\` — get JSON schema for a spec type
379
+
380
+ **Spec access:**
381
+ - \`openuispec read-specs [paths...]\` — read spec file contents
382
+ - \`openuispec get-screen <name>\` — get a single screen spec
383
+ - \`openuispec get-contract <name> [--variant v]\` — get a contract spec
384
+ - \`openuispec get-component <name> [--variant v]\` — get a component spec
385
+ - \`openuispec get-tokens <category>\` — get tokens for a category
386
+ - \`openuispec get-locale <locale> [--keys k1,k2]\` — get a locale file
387
+
388
+ **Validation & generation workflow:**
389
+ - \`openuispec validate [group...] --json\` — validate spec files against JSON Schemas
390
+ - \`openuispec check --target <t> --json\` — validate spec files + check target generation readiness
391
+ - \`openuispec prepare --target <t> --json\` — build AI-ready work bundle
392
+ - \`openuispec drift --target <t> --explain --json\` — semantic drift
393
+
394
+ **Visual verification:**
395
+ - \`openuispec screenshot --route /path\` — screenshot the web app
396
+ - \`openuispec screenshot --route /path --init-script "..."\` — inject auth/role before rendering (web only; app must implement \`__ous_init\` bootstrapper)
397
+ - \`openuispec screenshot-android [--project-dir path]\` — screenshot Android app
398
+ - \`openuispec screenshot-ios [--project-dir path]\` — screenshot iOS app
399
+
400
+ ### Other CLI commands
401
+ - \`openuispec init\` — scaffold a new spec project
402
+ - \`openuispec configure-target <t>\` — configure target platform stack
403
+ - \`openuispec update-rules\` — update AI rules to match installed package version
404
+ - \`openuispec drift --snapshot --target <t>\` — snapshot current state (user-initiated, after reviewing generated output)
405
+
406
+ ## Spec format reference
407
+
408
+ The spec format, schemas, and generation rules are in the installed \`openuispec\` package.
409
+ You MUST read the reference files before creating or editing spec files — do NOT guess the format.
410
+
411
+ **Find the package:** \`node_modules/openuispec/\` or run \`npm root -g\` → \`<prefix>/openuispec/\`.
412
+ **Online fallback:** \`https://openuispec.rsteam.uz/llms-full.txt\`
413
+
414
+ **Reference files (read in order):**
415
+ 1. \`README.md\` — schema tables, file format, root wrapper keys
416
+ 2. \`spec/openuispec-v0.2.md\` — full specification
417
+ 3. \`examples/taskflow/openuispec/\` — complete working example
418
+ 4. \`schema/\` — JSON Schemas for every file type
419
+
420
+ ## Spec location
421
+ - Spec root: \`${specDir}/\` — read \`${specDir}/openuispec.yaml\` first for actual paths.
422
+ - Default dirs: tokens/, screens/, flows/, contracts/, components/, platform/, locales/
423
+
424
+ ## When to start from spec vs platform code
425
+
426
+ **Spec-first** (use \`openuispec_prepare\` or \`openuispec prepare\`):
427
+ - Screen structure, navigation, fields, actions, validation, data binding changes
428
+ - Token, variant, contract, flow, or localization changes
429
+ - Changes affecting multiple platforms
430
+ - Requests in product/UI terms
431
+
432
+ **Platform-first** (skip spec tools):
433
+ - Platform-specific polish (iOS-only, Android-only, web-only)
434
+ - Local bug fixes that don't alter shared semantic behavior
435
+
436
+ ## If spec directories are empty (first-time setup)
437
+
438
+ Read \`spec/openuispec-v0.2.md\` from the package first, then:
439
+ 1. Scan codebase for UI screens → create \`${specDir}/screens/<name>.yaml\` as \`status: stub\`
440
+ 2. Extract tokens (colors, fonts, spacing) → \`${specDir}/tokens/\`
441
+ 3. Create contract extensions → \`${specDir}/contracts/\`
442
+ 4. Create locale files → \`${specDir}/locales/<locale>.json\`
443
+ 5. Fill in \`data_model\`, \`api.endpoints\` in \`${specDir}/openuispec.yaml\`
444
+
445
+ ## Rules
446
+ - Do not baseline on your own initiative — the user decides when generated output is accepted.
447
+ - After generation, always remind the user to review and baseline: \`openuispec drift --snapshot --target <t>\`.
448
+ - Do not modify generated UI without checking whether the spec must change first.
449
+ - Do not use \`configure-target --defaults\` as silent approval — ask the user to confirm.
450
+ - Always read spec format from the installed package, not from cached/memorized content.
451
+ ${RULES_END_MARKER}
452
+ `;
453
+ }
454
+ // ── update-rules ────────────────────────────────────────────────────
455
+ export function updateRules() {
456
+ const cwd = process.cwd();
457
+ const version = getPackageVersion();
458
+ // Detect spec dir from existing openuispec.yaml
459
+ let specDir = "openuispec";
460
+ for (const candidate of ["openuispec", "spec", "."]) {
461
+ if (existsSync(join(cwd, candidate, "openuispec.yaml"))) {
462
+ specDir = candidate;
463
+ break;
464
+ }
465
+ }
466
+ // Detect targets from manifest
467
+ let targets = [...SUPPORTED_TARGETS];
468
+ try {
469
+ const manifest = readFileSync(join(cwd, specDir, "openuispec.yaml"), "utf-8");
470
+ const match = manifest.match(/targets:\s*\[([^\]]+)\]/);
471
+ if (match) {
472
+ const parsedTargets = match[1]
473
+ .split(",")
474
+ .map((t) => t.trim().replace(/['"]/g, ""))
475
+ .filter(isSupportedTarget);
476
+ if (parsedTargets.length > 0) {
477
+ targets = parsedTargets;
478
+ }
479
+ }
480
+ }
481
+ catch { }
482
+ const rules = aiRulesBlock(specDir, targets);
483
+ let updated = 0;
484
+ for (const file of ["CLAUDE.md", "AGENTS.md"]) {
485
+ const filePath = join(cwd, file);
486
+ if (!existsSync(filePath))
487
+ continue;
488
+ const content = readFileSync(filePath, "utf-8");
489
+ // Try marker-based replacement first
490
+ const startIdx = content.indexOf(RULES_START_MARKER);
491
+ const endIdx = content.indexOf(RULES_END_MARKER);
492
+ if (startIdx !== -1 && endIdx !== -1) {
493
+ const before = content.slice(0, startIdx);
494
+ const after = content.slice(endIdx + RULES_END_MARKER.length);
495
+ writeFileSync(filePath, before + rules.trimStart().trimEnd() + after);
496
+ console.log(` updated ${file} (v${version})`);
497
+ updated++;
498
+ continue;
499
+ }
500
+ // Fallback: find the OpenUISpec rules block by header pattern
501
+ // The block runs from the header to EOF (it's always the last content)
502
+ const headerIdx = content.indexOf("# OpenUISpec — AI Assistant Rules");
503
+ if (headerIdx !== -1) {
504
+ const before = content.slice(0, headerIdx);
505
+ const newContent = before + rules.trimStart().trimEnd() + "\n";
506
+ writeFileSync(filePath, newContent);
507
+ console.log(` updated ${file} (v${version}, migrated to markers)`);
508
+ updated++;
509
+ continue;
510
+ }
511
+ console.log(` skip ${file} (no OpenUISpec rules block found)`);
512
+ }
513
+ if (updated === 0) {
514
+ console.log("No CLAUDE.md or AGENTS.md with OpenUISpec rules found.\nRun `openuispec init` first.");
515
+ }
516
+ else {
517
+ console.log(`\nAI rules updated to v${version}`);
518
+ }
519
+ // Migrate old spec doc version references to current
520
+ migrateDocVersionRefs(cwd, specDir);
521
+ // Ensure MCP server is configured
522
+ configureMcp(cwd, true);
523
+ }
524
+ // ── spec doc version migration ──────────────────────────────────────
525
+ function getCurrentSpecDocVersion() {
526
+ const specDir = resolvePackagePath(import.meta.url, "spec");
527
+ try {
528
+ const files = readdirSync(specDir);
529
+ const match = files
530
+ .map((f) => f.match(/^openuispec-v(.+)\.md$/))
531
+ .filter(Boolean)
532
+ .sort()
533
+ .pop();
534
+ return match ? match[1] : null;
535
+ }
536
+ catch {
537
+ return null;
538
+ }
539
+ }
540
+ function migrateDocVersionRefs(cwd, specDir) {
541
+ const currentVersion = getCurrentSpecDocVersion();
542
+ if (!currentVersion)
543
+ return;
544
+ const currentRef = `openuispec-v${currentVersion}`;
545
+ const pattern = /openuispec-v(\d+\.\d+)/g;
546
+ const filesToCheck = [
547
+ join(cwd, specDir, "README.md"),
548
+ join(cwd, "CLAUDE.md"),
549
+ join(cwd, "AGENTS.md"),
550
+ ];
551
+ for (const filePath of filesToCheck) {
552
+ if (!existsSync(filePath))
553
+ continue;
554
+ const content = readFileSync(filePath, "utf-8");
555
+ const oldRefs = [...content.matchAll(pattern)]
556
+ .filter((m) => m[1] !== currentVersion);
557
+ if (oldRefs.length === 0)
558
+ continue;
559
+ const migrated = content.replace(pattern, (_match, ver) => ver === currentVersion ? _match : currentRef);
560
+ writeFileSync(filePath, migrated);
561
+ const relPath = relative(cwd, filePath);
562
+ const oldVersions = [...new Set(oldRefs.map((m) => m[1]))].join(", ");
563
+ console.log(` updated ${relPath} (migrated v${oldVersions} → v${currentVersion} doc references)`);
564
+ }
565
+ }
566
+ // ── shared MCP config ───────────────────────────────────────────────
567
+ const EXPECTED_MCP_CONFIG = {
568
+ command: "openuispec",
569
+ args: ["mcp"],
570
+ };
571
+ /**
572
+ * MCP config files by agent:
573
+ * .mcp.json — Claude Code (project scope)
574
+ * .vscode/mcp.json — VS Code / Copilot Chat
575
+ * .gemini/settings.json — Gemini CLI (if .gemini/ exists)
576
+ *
577
+ * All use the same { mcpServers: { openuispec: { command, args } } } shape.
578
+ */
579
+ const JSON_MCP_PATHS = [
580
+ ".mcp.json",
581
+ join(".vscode", "mcp.json"),
582
+ join(".gemini", "settings.json"),
583
+ ];
584
+ /** Codex uses TOML: .codex/config.toml */
585
+ const CODEX_CONFIG_PATH = join(".codex", "config.toml");
586
+ const CODEX_MCP_BLOCK = `\n[mcp_servers.openuispec]\ncommand = "openuispec"\nargs = ["mcp"]\n`;
587
+ function configureCodexMcp(cwd, quiet) {
588
+ const codexDir = join(cwd, ".codex");
589
+ const configPath = join(codexDir, "config.toml");
590
+ try {
591
+ let content = "";
592
+ try {
593
+ content = readFileSync(configPath, "utf-8");
594
+ }
595
+ catch {
596
+ // file doesn't exist — will create
597
+ }
598
+ if (content.includes("[mcp_servers.openuispec]")) {
599
+ if (!quiet)
600
+ console.log(` skip ${CODEX_CONFIG_PATH} (openuispec MCP already configured)`);
601
+ return false;
602
+ }
603
+ if (!existsSync(codexDir))
604
+ mkdirSync(codexDir);
605
+ writeFileSync(configPath, content + CODEX_MCP_BLOCK);
606
+ if (!quiet)
607
+ console.log(` ${content ? "update" : "create"} ${CODEX_CONFIG_PATH} (MCP server configured)`);
608
+ return true;
609
+ }
610
+ catch {
611
+ if (!quiet)
612
+ console.log(` skip ${CODEX_CONFIG_PATH} (could not configure MCP server)`);
613
+ return false;
614
+ }
615
+ }
616
+ function configureMcp(cwd, showRestart, quiet = false) {
617
+ let changed = false;
618
+ for (const relPath of JSON_MCP_PATHS) {
619
+ const configPath = join(cwd, relPath);
620
+ // Optional dirs: only write config if the parent directory already exists
621
+ const optionalParent = [".vscode", ".gemini"].find((d) => relPath.startsWith(d));
622
+ if (optionalParent && !existsSync(join(cwd, optionalParent)))
623
+ continue;
624
+ try {
625
+ let config = {};
626
+ try {
627
+ config = JSON.parse(readFileSync(configPath, "utf-8"));
628
+ }
629
+ catch {
630
+ // file doesn't exist or isn't valid JSON — start fresh
631
+ }
632
+ if (!config.mcpServers)
633
+ config.mcpServers = {};
634
+ const existing = config.mcpServers.openuispec;
635
+ const needsUpdate = !existing ||
636
+ existing.command !== EXPECTED_MCP_CONFIG.command ||
637
+ JSON.stringify(existing.args) !== JSON.stringify(EXPECTED_MCP_CONFIG.args);
638
+ if (needsUpdate) {
639
+ config.mcpServers.openuispec = { ...EXPECTED_MCP_CONFIG };
640
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
641
+ if (!quiet)
642
+ console.log(` ${existing ? "update" : "create"} ${relPath} (MCP server configured)`);
643
+ changed = true;
644
+ }
645
+ else {
646
+ if (!quiet)
647
+ console.log(` skip ${relPath} (openuispec MCP already configured)`);
648
+ }
649
+ }
650
+ catch {
651
+ if (!quiet)
652
+ console.log(` skip ${relPath} (could not configure MCP server)`);
653
+ }
654
+ }
655
+ // Codex: .codex/config.toml (TOML format)
656
+ if (configureCodexMcp(cwd, quiet))
657
+ changed = true;
658
+ if (showRestart && changed)
659
+ console.log(`\n Restart your AI coding agent to activate the MCP server.`);
660
+ // Clean up stale .claude.json MCP config from older versions
661
+ const claudeJsonPath = join(cwd, ".claude.json");
662
+ try {
663
+ const claudeJson = JSON.parse(readFileSync(claudeJsonPath, "utf-8"));
664
+ if (claudeJson.mcpServers?.openuispec) {
665
+ delete claudeJson.mcpServers.openuispec;
666
+ if (Object.keys(claudeJson.mcpServers).length === 0)
667
+ delete claudeJson.mcpServers;
668
+ if (Object.keys(claudeJson).length === 0) {
669
+ unlinkSync(claudeJsonPath);
670
+ if (!quiet)
671
+ console.log(` remove .claude.json (migrated MCP config)`);
672
+ }
673
+ else {
674
+ writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
675
+ if (!quiet)
676
+ console.log(` update .claude.json (removed stale MCP config)`);
677
+ }
678
+ }
679
+ }
680
+ catch {
681
+ // .claude.json doesn't exist or not parseable — nothing to clean up
682
+ }
683
+ }
684
+ /**
685
+ * Extract the rules version from a CLAUDE.md / AGENTS.md file.
686
+ * Returns null if no version marker is found.
687
+ */
688
+ export function extractRulesVersion(filePath) {
689
+ if (!existsSync(filePath))
690
+ return null;
691
+ const content = readFileSync(filePath, "utf-8");
692
+ const match = content.match(/<!-- openuispec-rules-version:\s*([^\s]+)\s*-->/);
693
+ return match ? match[1] : null;
694
+ }
695
+ export { getPackageVersion };
696
+ function parseTargetsValue(raw) {
697
+ return raw
698
+ .split(",")
699
+ .map((value) => value.trim().toLowerCase())
700
+ .filter((value) => isSupportedTarget(value));
701
+ }
702
+ function requireFlagValue(argv, index, flag) {
703
+ const value = argv[index + 1];
704
+ if (!value || value.startsWith("--")) {
705
+ console.error(`Error: ${flag} requires a value.`);
706
+ process.exit(1);
707
+ }
708
+ return value;
709
+ }
710
+ function parseInitArgs(argv) {
711
+ const options = { defaults: argv.includes("--defaults"), quiet: argv.includes("--quiet") };
712
+ for (let index = 0; index < argv.length; index++) {
713
+ const arg = argv[index];
714
+ switch (arg) {
715
+ case "--defaults":
716
+ case "--quiet":
717
+ break;
718
+ case "--name":
719
+ options.name = requireFlagValue(argv, index, arg);
720
+ index++;
721
+ break;
722
+ case "--spec-dir":
723
+ options.specDir = requireFlagValue(argv, index, arg);
724
+ index++;
725
+ break;
726
+ case "--targets":
727
+ options.targets = parseTargetsValue(requireFlagValue(argv, index, arg));
728
+ index++;
729
+ break;
730
+ case "--backend":
731
+ options.backendPath = requireFlagValue(argv, index, arg);
732
+ index++;
733
+ break;
734
+ case "--with-api":
735
+ options.withApi = true;
736
+ break;
737
+ case "--no-api":
738
+ options.withApi = false;
739
+ break;
740
+ case "--configure-targets":
741
+ options.configureTargets = true;
742
+ break;
743
+ case "--no-configure-targets":
744
+ options.configureTargets = false;
745
+ break;
746
+ case "--with-shared":
747
+ options.withShared = true;
748
+ break;
749
+ case "--no-shared":
750
+ options.withShared = false;
751
+ break;
752
+ default:
753
+ if (arg.startsWith("--")) {
754
+ console.error(`Error: Unknown init option: ${arg}`);
755
+ process.exit(1);
756
+ }
757
+ }
758
+ }
759
+ return options;
760
+ }
761
+ function collectDefaults() {
762
+ const cwd = process.cwd();
763
+ const defaultName = cwd.split("/").pop() || "MyApp";
764
+ return {
765
+ name: defaultName,
766
+ specDir: "openuispec",
767
+ targets: [...SUPPORTED_TARGETS],
768
+ withApi: true,
769
+ backendPath: "../backend/",
770
+ configureTargets: true,
771
+ sharedLayers: [],
772
+ structures: [],
773
+ };
774
+ }
775
+ const SHARED_LAYER_DEFAULTS = {
776
+ kmp: {
777
+ language: "kotlin",
778
+ root: "../shared",
779
+ tracks: [],
780
+ scope: "Business logic, data models, repositories, API clients, view models/stores. No UI rendering.",
781
+ paths: { domain: "commonMain/domain/", features: "commonMain/features/" },
782
+ structureScope: {
783
+ ios: "Pure SwiftUI views and navigation. All business logic comes from the shared layer.",
784
+ android: "Pure Compose UI and navigation. All business logic comes from the shared layer.",
785
+ },
786
+ },
787
+ };
788
+ function defaultSharedConfig(targets) {
789
+ const mobilePlatforms = targets.filter((t) => t === "ios" || t === "android");
790
+ if (mobilePlatforms.length < 2) {
791
+ return { sharedLayers: [], structures: [] };
792
+ }
793
+ const preset = SHARED_LAYER_DEFAULTS.kmp;
794
+ const sharedLayers = [{
795
+ name: "mobile_common",
796
+ platforms: mobilePlatforms,
797
+ language: preset.language,
798
+ root: preset.root,
799
+ tracks: preset.tracks,
800
+ scope: preset.scope,
801
+ paths: preset.paths,
802
+ }];
803
+ const structures = mobilePlatforms.map((t) => ({
804
+ target: t,
805
+ root: preset.root,
806
+ scope: preset.structureScope[t] ?? `Pure ${t} UI. Business logic comes from the shared layer.`,
807
+ paths: { ui: `${t}App/ui/` },
808
+ }));
809
+ return { sharedLayers, structures };
810
+ }
811
+ async function collectSharedLayerAnswers(rl, targets) {
812
+ const withShared = await askYesNo(rl, "\nShare code between platforms (e.g. KMP commonMain)?", false);
813
+ if (!withShared)
814
+ return { sharedLayers: [], structures: [] };
815
+ const defaults = defaultSharedConfig(targets);
816
+ const defaultLayer = defaults.sharedLayers[0];
817
+ if (!defaultLayer)
818
+ return { sharedLayers: [], structures: [] };
819
+ console.log("\n Shared layer defaults (KMP):");
820
+ console.log(` platforms: ${defaultLayer.platforms.join(", ")}`);
821
+ console.log(` language: ${defaultLayer.language}`);
822
+ console.log(` root: ${defaultLayer.root}`);
823
+ console.log(` scope: ${defaultLayer.scope}`);
824
+ const useDefaults = await askYesNo(rl, " Use these defaults?", true);
825
+ if (useDefaults)
826
+ return defaults;
827
+ const layerName = await ask(rl, " Shared layer name", defaultLayer.name);
828
+ const platformsRaw = await askList(rl, " Platforms", targets, defaultLayer.platforms);
829
+ const language = await ask(rl, " Language", defaultLayer.language);
830
+ const root = await ask(rl, " Root path (relative to openuispec.yaml)", defaultLayer.root);
831
+ const scope = await ask(rl, " Scope (what code belongs here)", defaultLayer.scope);
832
+ const wantTracks = await askYesNo(rl, " Enable hash-based drift tracking for this layer?", false);
833
+ const tracksRaw = wantTracks
834
+ ? await askList(rl, " Tracked spec categories", ["manifest", "tokens", "contracts", "screens", "flows", "platform", "locales"], ["manifest", "contracts", "flows"])
835
+ : [];
836
+ const sharedLayers = [{
837
+ name: layerName,
838
+ platforms: platformsRaw,
839
+ language,
840
+ root,
841
+ tracks: tracksRaw,
842
+ scope,
843
+ paths: defaultLayer.paths,
844
+ }];
845
+ const structures = [];
846
+ for (const t of platformsRaw) {
847
+ const defaultStructure = defaults.structures.find((s) => s.target === t);
848
+ const structRoot = await ask(rl, ` ${t} structure root`, defaultStructure?.root ?? root);
849
+ const structScope = await ask(rl, ` ${t} scope (what code belongs in the ${t} target)`, defaultStructure?.scope ?? `Pure ${t} UI rendering.`);
850
+ structures.push({
851
+ target: t,
852
+ root: structRoot,
853
+ scope: structScope,
854
+ paths: defaultStructure?.paths ?? { ui: `${t}App/ui/` },
855
+ });
856
+ }
857
+ return { sharedLayers, structures };
858
+ }
859
+ async function collectInteractiveAnswers(rl) {
860
+ const defaults = collectDefaults();
861
+ const name = await ask(rl, "Project name", defaults.name);
862
+ const specDir = await ask(rl, "Spec directory", defaults.specDir);
863
+ const targets = await askList(rl, "\nWhich platforms?", [...SUPPORTED_TARGETS], defaults.targets);
864
+ if (targets.length === 0) {
865
+ console.error("At least one target is required.");
866
+ process.exit(1);
867
+ }
868
+ const withApi = await askYesNo(rl, "Will this spec declare API endpoints?", defaults.withApi);
869
+ const backendPath = withApi
870
+ ? await ask(rl, "Backend folder path relative to openuispec.yaml", defaults.backendPath ?? "../backend/")
871
+ : null;
872
+ const configureTargets = await askYesNo(rl, "Configure target stacks now?", defaults.configureTargets);
873
+ const { sharedLayers, structures } = await collectSharedLayerAnswers(rl, targets);
874
+ return {
875
+ name,
876
+ specDir,
877
+ targets,
878
+ withApi,
879
+ backendPath,
880
+ configureTargets,
881
+ sharedLayers,
882
+ structures,
883
+ };
884
+ }
885
+ function collectNonInteractiveAnswers(argv) {
886
+ const parsed = parseInitArgs(argv);
887
+ const defaults = collectDefaults();
888
+ if (!parsed.defaults && argv.filter((a) => a !== "--quiet").length === 0) {
889
+ console.error("Error: `openuispec init` needs a TTY for prompts.\n" +
890
+ "Run with `--list-options` to get prompt definitions as JSON, or pass flags such as `--name`, `--targets`, `--with-api`, `--backend`, and `--configure-targets`.");
891
+ process.exit(1);
892
+ }
893
+ const targets = parsed.targets && parsed.targets.length > 0 ? parsed.targets : defaults.targets;
894
+ if (targets.length === 0) {
895
+ console.error("Error: --targets must include at least one of ios, android, web.");
896
+ process.exit(1);
897
+ }
898
+ const withApi = parsed.withApi ?? defaults.withApi;
899
+ const backendPath = withApi ? parsed.backendPath ?? defaults.backendPath : null;
900
+ const withShared = parsed.withShared ?? false;
901
+ const { sharedLayers, structures } = withShared ? defaultSharedConfig(targets) : { sharedLayers: [], structures: [] };
902
+ return {
903
+ name: parsed.name ?? defaults.name,
904
+ specDir: parsed.specDir ?? defaults.specDir,
905
+ targets,
906
+ withApi,
907
+ backendPath,
908
+ configureTargets: parsed.configureTargets ?? defaults.configureTargets,
909
+ sharedLayers,
910
+ structures,
911
+ };
912
+ }
913
+ // ── main ─────────────────────────────────────────────────────────────
914
+ export async function init(argv = []) {
915
+ if (argv.includes("--list-options")) {
916
+ console.log(JSON.stringify(listInitOptions(), null, 2));
917
+ return;
918
+ }
919
+ const quiet = argv.includes("--quiet");
920
+ const interactive = stdin.isTTY && stdout.isTTY && !argv.includes("--defaults");
921
+ const rl = interactive ? createInterface({ input: stdin, output: stdout }) : null;
922
+ if (!quiet)
923
+ console.log("\nOpenUISpec — Project Setup\n");
924
+ try {
925
+ const cwd = process.cwd();
926
+ const answers = rl ? await collectInteractiveAnswers(rl) : collectNonInteractiveAnswers(argv);
927
+ rl?.close();
928
+ // ── create folders ─────────────────────────────────────────────
929
+ if (!quiet)
930
+ console.log("\nScaffolding...\n");
931
+ const root = join(cwd, answers.specDir);
932
+ const dirs = [
933
+ "tokens",
934
+ "contracts",
935
+ "components",
936
+ "screens",
937
+ "flows",
938
+ "platform",
939
+ "locales",
940
+ ];
941
+ ensureDir(root);
942
+ for (const d of dirs) {
943
+ ensureDir(join(root, d));
944
+ }
945
+ // ── manifest ───────────────────────────────────────────────────
946
+ writeIfMissing(join(root, "openuispec.yaml"), manifestTemplate(answers.name, answers.targets, {
947
+ withApi: answers.withApi,
948
+ backendPath: answers.backendPath,
949
+ sharedLayers: answers.sharedLayers,
950
+ structures: answers.structures,
951
+ }), quiet);
952
+ // ── spec README ──────────────────────────────────────────────
953
+ writeIfMissing(join(root, "README.md"), specReadmeTemplate(answers.name, answers.targets), quiet);
954
+ // ── .gitkeep for empty dirs ────────────────────────────────────
955
+ for (const d of dirs) {
956
+ const dir = join(root, d);
957
+ const entries = existsSync(dir)
958
+ ? readdirSync(dir).filter((f) => f !== ".gitkeep")
959
+ : [];
960
+ if (entries.length === 0) {
961
+ const gk = join(dir, ".gitkeep");
962
+ if (!existsSync(gk)) {
963
+ writeFileSync(gk, "");
964
+ if (!quiet)
965
+ console.log(` create ${relative(cwd, gk)}`);
966
+ }
967
+ }
968
+ }
969
+ // ── AI assistant rules ─────────────────────────────────────────
970
+ const rules = aiRulesBlock(answers.specDir, answers.targets);
971
+ for (const file of ["CLAUDE.md", "AGENTS.md"]) {
972
+ const filePath = join(cwd, file);
973
+ if (existsSync(filePath)) {
974
+ const existing = readFileSync(filePath, "utf-8");
975
+ if (existing.includes("OpenUISpec")) {
976
+ if (!quiet)
977
+ console.log(` skip ${file} (already has OpenUISpec rules)`);
978
+ continue;
979
+ }
980
+ appendFileSync(filePath, "\n" + rules);
981
+ if (!quiet)
982
+ console.log(` update ${file} (appended rules)`);
983
+ }
984
+ else {
985
+ writeFileSync(filePath, rules.trimStart());
986
+ if (!quiet)
987
+ console.log(` create ${file}`);
988
+ }
989
+ }
990
+ // ── MCP server configuration ────────────────────────────────────
991
+ configureMcp(cwd, false, quiet);
992
+ if (answers.configureTargets) {
993
+ if (!quiet)
994
+ console.log("\nConfiguring target stacks...\n");
995
+ const { runConfigureTarget } = await import("./configure-target.js");
996
+ for (const target of answers.targets) {
997
+ await runConfigureTarget([target, ...(interactive ? [] : ["--defaults"]), ...(quiet ? ["--silent"] : [])]);
998
+ }
999
+ }
1000
+ // ── done ───────────────────────────────────────────────────────
1001
+ if (quiet) {
1002
+ console.log(`./${answers.specDir}/`);
1003
+ }
1004
+ else {
1005
+ console.log(`
1006
+ Done! Your spec project is ready at ./${answers.specDir}/
1007
+
1008
+ Getting started (new project):
1009
+ 1. Edit ${answers.specDir}/openuispec.yaml — define your data model and API
1010
+ 2. Create tokens in ${answers.specDir}/tokens/ (colors, typography, spacing, etc.)
1011
+ 3. Create contract extensions in ${answers.specDir}/contracts/ (visual variants for the 7 built-in contracts)
1012
+ 4. Create screens in ${answers.specDir}/screens/ (one YAML per screen)
1013
+ 5. Create flows in ${answers.specDir}/flows/ (multi-step navigation)
1014
+ 6. Create locale files in ${answers.specDir}/locales/ (one JSON per supported locale)
1015
+ 7. Run \`openuispec validate\` and \`openuispec validate semantic\` to check everything
1016
+ 8. Ask AI to generate native code from the spec
1017
+ 9. Run \`openuispec drift --snapshot --target ${answers.targets[0]}\` to baseline the first accepted target state after that target output directory exists
1018
+
1019
+ Getting started (existing project):
1020
+ 1. Ask AI to read your existing UI code and generate spec files:
1021
+ "Read src/screens/HomeScreen.swift and create ${answers.specDir}/screens/home.yaml as status: stub"
1022
+ 2. Spec screens incrementally: stub → draft → ready
1023
+ 3. Only ready/draft screens are tracked by drift detection
1024
+ 4. Run \`openuispec validate\` to check specs against the schema
1025
+ 5. Use \`openuispec prepare --target ${answers.targets[0]}\` before first-time generation, then use \`openuispec drift --target ${answers.targets[0]} --explain\` and \`openuispec prepare --target ${answers.targets[0]}\` before asking AI to update a target
1026
+
1027
+ Commands:
1028
+ openuispec validate Validate spec files
1029
+ openuispec validate semantic Check semantic cross-references
1030
+ openuispec configure-target ios [--defaults] Configure target stack; --defaults stays unconfirmed
1031
+ openuispec status Show cross-target baseline/drift status
1032
+ openuispec drift --target ios --explain Explain semantic spec changes
1033
+ openuispec prepare --target ios Build the target work bundle
1034
+ openuispec drift --snapshot --target ios Save current state + git baseline after target output exists
1035
+
1036
+ AI rules have been added to CLAUDE.md and AGENTS.md.
1037
+ MCP server configured (AI assistants will use openuispec tools automatically).
1038
+
1039
+ Docs: https://openuispec.rsteam.uz
1040
+ `);
1041
+ }
1042
+ }
1043
+ catch (err) {
1044
+ rl?.close();
1045
+ throw err;
1046
+ }
1047
+ }