openuispec 0.1.27 → 0.1.29

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 (123) hide show
  1. package/README.md +52 -55
  2. package/cli/configure-target.ts +416 -0
  3. package/cli/index.ts +14 -3
  4. package/cli/init.ts +241 -55
  5. package/cli/target-presets.json +746 -0
  6. package/docs/implementation-notes.md +47 -10
  7. package/docs/release-notes-v0.1.26.md +1 -1
  8. package/docs/release-notes-v0.1.28.md +25 -0
  9. package/docs/stress-test-maturity-report.md +1 -1
  10. package/drift/index.ts +31 -11
  11. package/examples/taskflow/AGENTS.md +113 -0
  12. package/examples/taskflow/CLAUDE.md +113 -0
  13. package/examples/taskflow/backend/.gitkeep +1 -0
  14. package/examples/taskflow/generated/android/TaskFlow/README.md +43 -0
  15. package/examples/taskflow/generated/android/TaskFlow/app/build.gradle.kts +76 -0
  16. package/examples/taskflow/generated/android/TaskFlow/app/proguard-rules.pro +1 -0
  17. package/examples/taskflow/generated/android/TaskFlow/app/src/main/AndroidManifest.xml +21 -0
  18. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/MainActivity.kt +19 -0
  19. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/TaskFlowApp.kt +283 -0
  20. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/DomainModels.kt +106 -0
  21. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/SampleData.kt +57 -0
  22. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/components/Common.kt +109 -0
  23. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/HomeScreen.kt +112 -0
  24. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/ProjectsScreen.kt +61 -0
  25. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/SettingsScreen.kt +82 -0
  26. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/TaskDetailScreen.kt +111 -0
  27. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/sheets/Sheets.kt +77 -0
  28. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Color.kt +30 -0
  29. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Theme.kt +86 -0
  30. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Type.kt +57 -0
  31. package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/strings.xml +155 -0
  32. package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/themes.xml +4 -0
  33. package/examples/taskflow/generated/android/TaskFlow/build.gradle.kts +5 -0
  34. package/examples/taskflow/generated/android/TaskFlow/gradle/gradle-daemon-jvm.properties +12 -0
  35. package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.jar +0 -0
  36. package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.properties +7 -0
  37. package/examples/taskflow/generated/android/TaskFlow/gradle.properties +4 -0
  38. package/examples/taskflow/generated/android/TaskFlow/gradlew +18 -0
  39. package/examples/taskflow/generated/android/TaskFlow/gradlew.bat +12 -0
  40. package/examples/taskflow/generated/android/TaskFlow/settings.gradle.kts +18 -0
  41. package/examples/taskflow/generated/ios/TaskFlow/README.md +21 -0
  42. package/examples/taskflow/generated/ios/TaskFlow/Resources/en.lproj/Localizable.strings +115 -0
  43. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/App/TaskFlowApp.swift +24 -0
  44. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Components/AppChrome.swift +150 -0
  45. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Flows/TaskEditorSheet.swift +220 -0
  46. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Models/DomainModels.swift +122 -0
  47. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/CalendarView.swift +21 -0
  48. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/HomeView.swift +201 -0
  49. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProfileEditView.swift +48 -0
  50. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectDetailView.swift +59 -0
  51. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectsView.swift +63 -0
  52. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/SettingsView.swift +85 -0
  53. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/TaskDetailView.swift +219 -0
  54. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppModel.swift +320 -0
  55. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppSupport.swift +41 -0
  56. package/examples/taskflow/generated/ios/TaskFlow/project.yml +26 -0
  57. package/examples/taskflow/generated/web/TaskFlow/README.md +19 -0
  58. package/examples/taskflow/generated/web/TaskFlow/index.html +12 -0
  59. package/examples/taskflow/generated/web/TaskFlow/package-lock.json +1908 -0
  60. package/examples/taskflow/generated/web/TaskFlow/package.json +24 -0
  61. package/examples/taskflow/generated/web/TaskFlow/src/App.tsx +58 -0
  62. package/examples/taskflow/generated/web/TaskFlow/src/AppShell.tsx +55 -0
  63. package/examples/taskflow/generated/web/TaskFlow/src/components/Common.tsx +82 -0
  64. package/examples/taskflow/generated/web/TaskFlow/src/components/Modals.tsx +191 -0
  65. package/examples/taskflow/generated/web/TaskFlow/src/components/Nav.tsx +41 -0
  66. package/examples/taskflow/generated/web/TaskFlow/src/generated/messages.ts +131 -0
  67. package/examples/taskflow/generated/web/TaskFlow/src/hooks.ts +25 -0
  68. package/examples/taskflow/generated/web/TaskFlow/src/i18n.ts +39 -0
  69. package/examples/taskflow/generated/web/TaskFlow/src/locales.en.json +111 -0
  70. package/examples/taskflow/generated/web/TaskFlow/src/main.tsx +13 -0
  71. package/examples/taskflow/generated/web/TaskFlow/src/screens/HomeScreen.tsx +111 -0
  72. package/examples/taskflow/generated/web/TaskFlow/src/screens/ProjectsScreen.tsx +82 -0
  73. package/examples/taskflow/generated/web/TaskFlow/src/screens/SettingsScreens.tsx +132 -0
  74. package/examples/taskflow/generated/web/TaskFlow/src/screens/TaskDetail.tsx +105 -0
  75. package/examples/taskflow/generated/web/TaskFlow/src/store.ts +216 -0
  76. package/examples/taskflow/generated/web/TaskFlow/src/styles.css +617 -0
  77. package/examples/taskflow/generated/web/TaskFlow/src/types.ts +64 -0
  78. package/examples/taskflow/generated/web/TaskFlow/src/utils.ts +78 -0
  79. package/examples/taskflow/generated/web/TaskFlow/tsconfig.json +21 -0
  80. package/examples/taskflow/generated/web/TaskFlow/vite.config.ts +6 -0
  81. package/examples/taskflow/openuispec/README.md +54 -0
  82. package/examples/taskflow/{openuispec.yaml → openuispec/openuispec.yaml} +2 -0
  83. package/examples/todo-orbit/AGENTS.md +48 -22
  84. package/examples/todo-orbit/CLAUDE.md +48 -22
  85. package/examples/todo-orbit/backend/.gitkeep +1 -0
  86. package/examples/todo-orbit/openuispec/README.md +9 -4
  87. package/examples/todo-orbit/openuispec/openuispec.yaml +2 -0
  88. package/package.json +1 -1
  89. package/prepare/index.ts +811 -25
  90. package/schema/openuispec.schema.json +10 -0
  91. package/schema/semantic-lint.ts +36 -12
  92. package/schema/validate.ts +9 -4
  93. package/status/index.ts +16 -3
  94. /package/examples/taskflow/{contracts → openuispec/contracts}/README.md +0 -0
  95. /package/examples/taskflow/{contracts → openuispec/contracts}/action_trigger.yaml +0 -0
  96. /package/examples/taskflow/{contracts → openuispec/contracts}/collection.yaml +0 -0
  97. /package/examples/taskflow/{contracts → openuispec/contracts}/data_display.yaml +0 -0
  98. /package/examples/taskflow/{contracts → openuispec/contracts}/feedback.yaml +0 -0
  99. /package/examples/taskflow/{contracts → openuispec/contracts}/input_field.yaml +0 -0
  100. /package/examples/taskflow/{contracts → openuispec/contracts}/nav_container.yaml +0 -0
  101. /package/examples/taskflow/{contracts → openuispec/contracts}/surface.yaml +0 -0
  102. /package/examples/taskflow/{contracts → openuispec/contracts}/x_media_player.yaml +0 -0
  103. /package/examples/taskflow/{flows → openuispec/flows}/create_task.yaml +0 -0
  104. /package/examples/taskflow/{flows → openuispec/flows}/edit_task.yaml +0 -0
  105. /package/examples/taskflow/{locales → openuispec/locales}/en.json +0 -0
  106. /package/examples/taskflow/{platform → openuispec/platform}/android.yaml +0 -0
  107. /package/examples/taskflow/{platform → openuispec/platform}/ios.yaml +0 -0
  108. /package/examples/taskflow/{platform → openuispec/platform}/web.yaml +0 -0
  109. /package/examples/taskflow/{screens → openuispec/screens}/calendar.yaml +0 -0
  110. /package/examples/taskflow/{screens → openuispec/screens}/home.yaml +0 -0
  111. /package/examples/taskflow/{screens → openuispec/screens}/profile_edit.yaml +0 -0
  112. /package/examples/taskflow/{screens → openuispec/screens}/project_detail.yaml +0 -0
  113. /package/examples/taskflow/{screens → openuispec/screens}/projects.yaml +0 -0
  114. /package/examples/taskflow/{screens → openuispec/screens}/settings.yaml +0 -0
  115. /package/examples/taskflow/{screens → openuispec/screens}/task_detail.yaml +0 -0
  116. /package/examples/taskflow/{tokens → openuispec/tokens}/color.yaml +0 -0
  117. /package/examples/taskflow/{tokens → openuispec/tokens}/elevation.yaml +0 -0
  118. /package/examples/taskflow/{tokens → openuispec/tokens}/icons.yaml +0 -0
  119. /package/examples/taskflow/{tokens → openuispec/tokens}/layout.yaml +0 -0
  120. /package/examples/taskflow/{tokens → openuispec/tokens}/motion.yaml +0 -0
  121. /package/examples/taskflow/{tokens → openuispec/tokens}/spacing.yaml +0 -0
  122. /package/examples/taskflow/{tokens → openuispec/tokens}/themes.yaml +0 -0
  123. /package/examples/taskflow/{tokens → openuispec/tokens}/typography.yaml +0 -0
package/README.md CHANGED
@@ -53,6 +53,7 @@ openuispec init
53
53
  ```
54
54
 
55
55
  This scaffolds a spec directory, starter tokens, and adds rules to `CLAUDE.md` / `AGENTS.md` so AI assistants track spec changes automatically.
56
+ Use `openuispec init --no-configure-targets` if you want to scaffold first and choose target stacks later.
56
57
 
57
58
  Then hand your spec to any AI code generator:
58
59
 
@@ -68,7 +69,7 @@ For platform generation, treat these as hard output constraints:
68
69
 
69
70
  See the examples for concrete reference projects:
70
71
 
71
- - [TaskFlow](./examples/taskflow/) for a compact spec covering all 7 contract families
72
+ - [TaskFlow](./examples/taskflow/openuispec/) for a compact reference spec covering all 7 contract families, with generated iOS, Android, and web targets under `examples/taskflow/generated/`
72
73
  - [Todo Orbit](./examples/todo-orbit/openuispec/) for a bilingual task app with generated iOS, Android, and web targets under `examples/todo-orbit/generated/`
73
74
 
74
75
  ## Repository structure
@@ -102,47 +103,22 @@ openuispec/
102
103
  │ │ └── validation.schema.json # Validation rule definitions
103
104
  │ └── validate.ts # Validation script (npm run validate)
104
105
  ├── examples/
105
- │ ├── taskflow/ # Compact reference spec
106
- │ │ ├── openuispec.yaml # Root manifest + data model + API endpoints
107
- │ │ ├── tokens/
108
- │ │ ├── color.yaml # Brand + semantic + status colors
109
- │ │ │ ├── typography.yaml # Font family + 8-step type scale
110
- │ │ ├── spacing.yaml # 4px base unit, 9-step scale
111
- │ │ │ ├── elevation.yaml # 4-level elevation (none/sm/md/lg)
112
- │ │ │ ├── motion.yaml # Durations, easings, patterns
113
- │ │ │ ├── layout.yaml # Size classes, primitives, reflow rules
114
- │ │ │ ├── themes.yaml # Light, dark, warm variants
115
- │ │ │ └── icons.yaml # Icon registry with platform mappings
116
- │ │ ├── contracts/ # Standard contract extensions + custom contracts
117
- │ │ │ ├── input_field.yaml # Standard contract with cut_corner variant
118
- │ │ │ └── x_media_player.yaml # Custom media player contract (Section 12)
119
- │ │ ├── screens/
120
- │ │ │ ├── home.yaml # Task list with search, filters, FAB, adaptive nav
121
- │ │ │ ├── task_detail.yaml # Full task view with actions + assignee sheet
122
- │ │ │ ├── projects.yaml # Project grid + new project dialog
123
- │ │ │ ├── project_detail.yaml # Single project with task list (stub)
124
- │ │ │ ├── settings.yaml # Preferences, toggles, account management
125
- │ │ │ ├── profile_edit.yaml # Edit profile form (stub)
126
- │ │ │ └── calendar.yaml # Calendar view (stub)
127
- │ │ ├── flows/
128
- │ │ │ ├── create_task.yaml # Task creation form (sheet presentation)
129
- │ │ │ └── edit_task.yaml # Task editing flow
130
- │ │ ├── locales/
131
- │ │ │ └── en.json # English locale (ICU MessageFormat)
132
- │ │ └── platform/
133
- │ │ ├── ios.yaml # SwiftUI overrides + behaviors
134
- │ │ ├── android.yaml # Compose overrides + behaviors
135
- │ │ └── web.yaml # React overrides + responsive rules
136
- │ └── todo-orbit/ # Full showcase app with generated targets
106
+ │ ├── taskflow/ # Compact reference sample
107
+ │ │ ├── openuispec/ # Source OpenUISpec project
108
+ │ │ ├── generated/ # Generated iOS, Android, and web apps
109
+ │ │ ├── README.md # Sample overview and structure
110
+ │ │ └── AGENTS.md / CLAUDE.md # AI rules generated from the package
111
+ └── todo-orbit/ # Full showcase sample
137
112
  │ ├── openuispec/ # Source OpenUISpec project
138
113
  │ ├── generated/ # Generated iOS, Android, and web apps
139
- └── artifacts/ # Screenshots and supporting outputs
114
+ ├── README.md # Sample overview and structure
115
+ │ └── AGENTS.md / CLAUDE.md # AI rules generated from the package
140
116
  ├── cli/ # CLI tool (openuispec init, drift, prepare, validate)
141
117
  │ ├── index.ts # Entry point
142
118
  │ └── init.ts # Project scaffolding + AI rules
143
119
  ├── drift/ # Drift detection (spec change tracking)
144
120
  │ └── index.ts # Hash-based drift checker
145
- ├── prepare/ # AI-ready target work bundle generation
121
+ ├── prepare/ # Target work bundle generation
146
122
  │ └── index.ts # Baseline-aware target preparation
147
123
  ├── LICENSE
148
124
  └── README.md
@@ -154,21 +130,21 @@ Every file type has a corresponding JSON Schema in `schema/`. **Read the schema
154
130
 
155
131
  | File | Schema | Root key | Example |
156
132
  |------|--------|----------|---------|
157
- | `openuispec.yaml` | `openuispec.schema.json` | `spec_version` | [openuispec.yaml](./examples/taskflow/openuispec.yaml) |
158
- | `screens/*.yaml` | `screen.schema.json` | `<screen_id>` | [home.yaml](./examples/taskflow/screens/home.yaml) |
159
- | `flows/*.yaml` | `flow.schema.json` | `<flow_id>` | [create_task.yaml](./examples/taskflow/flows/create_task.yaml) |
160
- | `platform/*.yaml` | `platform.schema.json` | `platform` | [ios.yaml](./examples/taskflow/platform/ios.yaml) |
161
- | `locales/*.json` | `locale.schema.json` | (object) | [en.json](./examples/taskflow/locales/en.json) |
162
- | `contracts/<name>.yaml` | `contract.schema.json` | `<contract_name>` | [input_field.yaml](./examples/taskflow/contracts/input_field.yaml) |
163
- | `contracts/x_*.yaml` | `custom-contract.schema.json` | `<x_name>` | [x_media_player.yaml](./examples/taskflow/contracts/x_media_player.yaml) |
164
- | `tokens/color.yaml` | `tokens/color.schema.json` | `color` | [color.yaml](./examples/taskflow/tokens/color.yaml) |
165
- | `tokens/typography.yaml` | `tokens/typography.schema.json` | `typography` | [typography.yaml](./examples/taskflow/tokens/typography.yaml) |
166
- | `tokens/spacing.yaml` | `tokens/spacing.schema.json` | `spacing` | [spacing.yaml](./examples/taskflow/tokens/spacing.yaml) |
167
- | `tokens/elevation.yaml` | `tokens/elevation.schema.json` | `elevation` | [elevation.yaml](./examples/taskflow/tokens/elevation.yaml) |
168
- | `tokens/motion.yaml` | `tokens/motion.schema.json` | `motion` | [motion.yaml](./examples/taskflow/tokens/motion.yaml) |
169
- | `tokens/layout.yaml` | `tokens/layout.schema.json` | `layout` | [layout.yaml](./examples/taskflow/tokens/layout.yaml) |
170
- | `tokens/themes.yaml` | `tokens/themes.schema.json` | `themes` | [themes.yaml](./examples/taskflow/tokens/themes.yaml) |
171
- | `tokens/icons.yaml` | `tokens/icons.schema.json` | `icons` | [icons.yaml](./examples/taskflow/tokens/icons.yaml) |
133
+ | `openuispec.yaml` | `openuispec.schema.json` | `spec_version` | [openuispec.yaml](./examples/taskflow/openuispec/openuispec.yaml) |
134
+ | `screens/*.yaml` | `screen.schema.json` | `<screen_id>` | [home.yaml](./examples/taskflow/openuispec/screens/home.yaml) |
135
+ | `flows/*.yaml` | `flow.schema.json` | `<flow_id>` | [create_task.yaml](./examples/taskflow/openuispec/flows/create_task.yaml) |
136
+ | `platform/*.yaml` | `platform.schema.json` | `platform` | [ios.yaml](./examples/taskflow/openuispec/platform/ios.yaml) |
137
+ | `locales/*.json` | `locale.schema.json` | (object) | [en.json](./examples/taskflow/openuispec/locales/en.json) |
138
+ | `contracts/<name>.yaml` | `contract.schema.json` | `<contract_name>` | [input_field.yaml](./examples/taskflow/openuispec/contracts/input_field.yaml) |
139
+ | `contracts/x_*.yaml` | `custom-contract.schema.json` | `<x_name>` | [x_media_player.yaml](./examples/taskflow/openuispec/contracts/x_media_player.yaml) |
140
+ | `tokens/color.yaml` | `tokens/color.schema.json` | `color` | [color.yaml](./examples/taskflow/openuispec/tokens/color.yaml) |
141
+ | `tokens/typography.yaml` | `tokens/typography.schema.json` | `typography` | [typography.yaml](./examples/taskflow/openuispec/tokens/typography.yaml) |
142
+ | `tokens/spacing.yaml` | `tokens/spacing.schema.json` | `spacing` | [spacing.yaml](./examples/taskflow/openuispec/tokens/spacing.yaml) |
143
+ | `tokens/elevation.yaml` | `tokens/elevation.schema.json` | `elevation` | [elevation.yaml](./examples/taskflow/openuispec/tokens/elevation.yaml) |
144
+ | `tokens/motion.yaml` | `tokens/motion.schema.json` | `motion` | [motion.yaml](./examples/taskflow/openuispec/tokens/motion.yaml) |
145
+ | `tokens/layout.yaml` | `tokens/layout.schema.json` | `layout` | [layout.yaml](./examples/taskflow/openuispec/tokens/layout.yaml) |
146
+ | `tokens/themes.yaml` | `tokens/themes.schema.json` | `themes` | [themes.yaml](./examples/taskflow/openuispec/tokens/themes.yaml) |
147
+ | `tokens/icons.yaml` | `tokens/icons.schema.json` | `icons` | [icons.yaml](./examples/taskflow/openuispec/tokens/icons.yaml) |
172
148
 
173
149
  Every token file **must** have a single root wrapper key matching its type:
174
150
 
@@ -198,16 +174,33 @@ generation:
198
174
  web: "../web-ui/"
199
175
  android: "../kmp-ui/"
200
176
  ios: "../kmp-ui/iosApp/"
177
+ code_roots:
178
+ backend: "../api/"
201
179
  ```
202
180
 
203
181
  Paths are relative to `openuispec.yaml`. The `.openuispec-state.json` file is stored inside each output directory and records spec file hashes plus the git baseline commit metadata captured at snapshot time.
204
182
 
183
+ If `api.endpoints` are declared, `generation.code_roots.backend` is required. It should point at the backend folder the AI must inspect when generating API clients or wiring request/response behavior.
184
+
185
+ `openuispec drift --snapshot --target <target>` requires that target output directory to already exist. If it does not, generate the target code first, then snapshot the accepted baseline.
186
+
205
187
  Use the commands like this:
206
188
  - `openuispec validate` checks schema correctness
207
189
  - `openuispec validate semantic` checks cross-references such as locale keys, formatters, mappers, contracts, icons, navigation targets, and API endpoints
190
+ - `openuispec init --no-configure-targets` scaffolds the spec project without running the target-stack wizard
191
+ - `openuispec configure-target <t>` records target stack choices in `platform/<target>.yaml` using preset defaults, while still allowing custom framework/library values when the project uses something outside the catalog
208
192
  - `openuispec drift --target <t> --explain` explains semantic spec changes since that target's accepted baseline
209
- - `openuispec prepare --target <t>` turns those changes into an AI-ready target update bundle
210
- - `openuispec status` shows every target's snapshot state, baseline commit, and whether that target is behind the current spec
193
+ - `openuispec prepare --target <t>` builds the target work bundle for either first-time generation or drift-based updates
194
+ - `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
195
+
196
+ 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.
197
+
198
+ When target stack choices come from the preset catalog, `prepare --json` also exposes install-oriented refs for the selected options:
199
+ - Android: Gradle plugin ids and library coordinates
200
+ - Web: npm package specs
201
+ - iOS: package identifiers plus docs links
202
+
203
+ Those refs are anchors, not a full dependency manifest. The AI is expected to add any supporting build, plugin, repository, annotation-processing, runtime, dev, and test dependencies required by the current platform setup.
211
204
 
212
205
  If a target snapshot was created before baseline metadata was added, `--explain` and `status` will tell you to re-run `openuispec drift --snapshot --target <target>` for that target.
213
206
 
@@ -217,18 +210,22 @@ When a shared spec change needs to be applied to a target:
217
210
 
218
211
  ```bash
219
212
  openuispec validate
213
+ openuispec validate semantic
214
+ openuispec status
220
215
  openuispec drift --target ios --explain
221
216
  openuispec prepare --target ios
222
217
  # update the ios implementation
218
+ # ensure the ios output directory already exists
223
219
  openuispec drift --snapshot --target ios
224
220
  ```
225
221
 
226
222
  Meaning:
227
223
  - `validate` checks schema correctness
228
224
  - `validate semantic` checks cross-reference integrity
225
+ - `status` shows which targets are up to date, need a baseline, or still need generation
229
226
  - `drift --explain` shows semantic spec changes since that target's accepted baseline
230
- - `prepare` packages those changes into an AI/developer work bundle
231
- - `drift --snapshot` accepts the updated state after the target UI has been updated
227
+ - `prepare` packages the target work bundle for AI/developers. It runs in `bootstrap` mode for first-time generation and `update` mode after a target snapshot exists.
228
+ - `drift --snapshot` accepts the updated state after the target UI has been updated and the target output directory exists
232
229
 
233
230
  Before picking the next platform to update, run:
234
231
 
@@ -238,7 +235,7 @@ openuispec status
238
235
 
239
236
  to see which targets are already up to date and which ones still need to catch up with shared spec changes.
240
237
 
241
- `drift --snapshot` is bookkeeping. It does not prove that the target code matches the spec.
238
+ `drift --snapshot` is bookkeeping. It does not prove that the target code matches the spec, and it will not create a missing target output directory for you.
242
239
 
243
240
  ## Spec at a glance
244
241
 
@@ -0,0 +1,416 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { stdin, stdout } from "node:process";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { dirname, join, relative, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import YAML from "yaml";
7
+ import { findProjectDir, readManifest } from "../drift/index.js";
8
+ import { ask, askChoice } from "./init.js";
9
+
10
+ type SupportedTarget = "ios" | "android" | "web";
11
+
12
+ type WizardOptionPreset = {
13
+ value: string;
14
+ generation_value?: string;
15
+ framework_filter?: string[];
16
+ dependencies?: string[];
17
+ extra_generation?: Record<string, any>;
18
+ refs?: {
19
+ plugins?: string[];
20
+ libraries?: string[];
21
+ packages?: string[];
22
+ docs?: string[];
23
+ };
24
+ };
25
+
26
+ type ChoiceQuestionPreset = {
27
+ key: string;
28
+ prompt: string;
29
+ recommended: string;
30
+ options: WizardOptionPreset[];
31
+ };
32
+
33
+ type TargetWizardPreset = {
34
+ framework: string;
35
+ framework_prompt?: string;
36
+ framework_options?: string[];
37
+ language?: string;
38
+ min_version?: string;
39
+ min_sdk?: number;
40
+ target_sdk?: number;
41
+ generation_defaults?: Record<string, any>;
42
+ base_dependencies?: string[];
43
+ framework_dependencies?: Record<string, string[]>;
44
+ questions: ChoiceQuestionPreset[];
45
+ };
46
+
47
+ function readWizardPresets(): Record<SupportedTarget, TargetWizardPreset> {
48
+ const presetsPath = join(dirname(fileURLToPath(import.meta.url)), "target-presets.json");
49
+ return JSON.parse(readFileSync(presetsPath, "utf-8")) as Record<SupportedTarget, TargetWizardPreset>;
50
+ }
51
+
52
+ const TARGET_WIZARDS = readWizardPresets();
53
+
54
+ function filterOptionsForFramework(
55
+ question: ChoiceQuestionPreset,
56
+ framework: string
57
+ ): WizardOptionPreset[] {
58
+ return question.options.filter((option) => {
59
+ if (!option.framework_filter) return true;
60
+ return option.framework_filter.includes(framework);
61
+ });
62
+ }
63
+
64
+ function effectiveDefault(
65
+ question: ChoiceQuestionPreset,
66
+ framework: string,
67
+ inferred: string | undefined
68
+ ): string {
69
+ if (inferred) return inferred;
70
+ const filtered = filterOptionsForFramework(question, framework);
71
+ if (filtered.some((o) => o.value === question.recommended)) return question.recommended;
72
+ return filtered[0]?.value ?? question.recommended;
73
+ }
74
+
75
+ function mergeDependencies(
76
+ derived: string[],
77
+ existing: unknown,
78
+ managedDependencies: Set<string>
79
+ ): string[] {
80
+ const extras = Array.isArray(existing)
81
+ ? existing.filter((dep): dep is string => typeof dep === "string" && !managedDependencies.has(dep))
82
+ : [];
83
+ return Array.from(new Set([...derived, ...extras]));
84
+ }
85
+
86
+ function findOption(question: ChoiceQuestionPreset, value: string): WizardOptionPreset | null {
87
+ return question.options.find((option) => option.value === value) ?? null;
88
+ }
89
+
90
+ function collectManagedDependencies(wizard: TargetWizardPreset): Set<string> {
91
+ const managed = new Set<string>(wizard.base_dependencies ?? []);
92
+ if (wizard.framework_dependencies) {
93
+ for (const deps of Object.values(wizard.framework_dependencies)) {
94
+ for (const dep of deps) managed.add(dep);
95
+ }
96
+ }
97
+ for (const question of wizard.questions) {
98
+ for (const option of question.options) {
99
+ for (const dependency of option.dependencies ?? []) {
100
+ managed.add(dependency);
101
+ }
102
+ }
103
+ }
104
+ return managed;
105
+ }
106
+
107
+ function normalizeAndroid(existingPlatform: Record<string, any>): Record<string, string> {
108
+ const generation = existingPlatform.generation ?? {};
109
+ const deps = new Set(Array.isArray(generation.dependencies) ? generation.dependencies : []);
110
+ const architectureValue = typeof generation.architecture === "string" ? generation.architecture.toLowerCase() : "";
111
+ const stateValue = typeof generation.state === "string" ? generation.state.toLowerCase() : "";
112
+ const persistenceValue = typeof generation.persistence === "string" ? generation.persistence.toLowerCase() : "";
113
+ return {
114
+ architecture:
115
+ typeof generation.architecture === "string" &&
116
+ !architectureValue.includes("decompose") &&
117
+ !architectureValue.includes("compose") &&
118
+ !deps.has("decompose") &&
119
+ !deps.has("navigation-compose")
120
+ ? generation.architecture
121
+ : architectureValue.includes("decompose") || deps.has("decompose")
122
+ ? "decompose"
123
+ : architectureValue.includes("compose") || deps.has("navigation-compose")
124
+ ? "plain_compose"
125
+ : "decompose",
126
+ state:
127
+ typeof generation.state === "string" &&
128
+ !stateValue.includes("mvikotlin") &&
129
+ !stateValue.includes("viewmodel") &&
130
+ !deps.has("mvikotlin") &&
131
+ !deps.has("lifecycle-viewmodel-compose")
132
+ ? generation.state
133
+ : stateValue.includes("mvikotlin") || deps.has("mvikotlin")
134
+ ? "mvikotlin"
135
+ : stateValue.includes("viewmodel") || deps.has("lifecycle-viewmodel-compose")
136
+ ? "viewmodel"
137
+ : "mvikotlin",
138
+ preferences:
139
+ typeof generation.preferences === "string"
140
+ ? generation.preferences
141
+ : persistenceValue === "datastore" || deps.has("datastore-preferences")
142
+ ? "datastore"
143
+ : "datastore",
144
+ database:
145
+ typeof generation.database === "string"
146
+ ? generation.database
147
+ : persistenceValue === "sqldelight" || deps.has("sqldelight")
148
+ ? "sqldelight"
149
+ : persistenceValue === "room" || deps.has("room-runtime")
150
+ ? "room"
151
+ : "none",
152
+ di:
153
+ typeof generation.di === "string"
154
+ ? ["metro", "koin", "hilt", "none"].includes(generation.di)
155
+ ? generation.di
156
+ : generation.di
157
+ : deps.has("metro")
158
+ ? "metro"
159
+ : deps.has("koin-android")
160
+ ? "koin"
161
+ : deps.has("hilt-android")
162
+ ? "hilt"
163
+ : "metro",
164
+ };
165
+ }
166
+
167
+ function normalizeWeb(existingPlatform: Record<string, any>): Record<string, string> {
168
+ const generation = existingPlatform.generation ?? {};
169
+ const result: Record<string, string> = {
170
+ runtime:
171
+ typeof generation.runtime === "string"
172
+ ? generation.runtime
173
+ : "frontend_only",
174
+ css:
175
+ typeof generation.css === "string"
176
+ ? generation.css
177
+ : "tailwind",
178
+ storage_backend:
179
+ typeof generation.storage_backend === "string"
180
+ ? generation.storage_backend
181
+ : "none",
182
+ };
183
+
184
+ if (typeof generation.routing === "string") {
185
+ result.routing = generation.routing.includes("tanstack")
186
+ ? "tanstack_router"
187
+ : generation.routing.includes("react-router") || generation.routing === "react_router"
188
+ ? "react_router"
189
+ : generation.routing;
190
+ }
191
+
192
+ if (typeof generation.state === "string") {
193
+ result.state = generation.state === "redux-toolkit"
194
+ ? "redux"
195
+ : generation.state === "tanstack-query"
196
+ ? "query_only"
197
+ : generation.state;
198
+ }
199
+
200
+ return result;
201
+ }
202
+
203
+ function normalizeIos(existingPlatform: Record<string, any>): Record<string, string> {
204
+ const generation = existingPlatform.generation ?? {};
205
+ const deps = new Set(Array.isArray(generation.dependencies) ? generation.dependencies : []);
206
+ return {
207
+ architecture:
208
+ typeof generation.architecture === "string" &&
209
+ !generation.architecture.toLowerCase().includes("tca") &&
210
+ generation.architecture.toLowerCase() !== "native swiftui"
211
+ ? generation.architecture
212
+ : typeof generation.architecture === "string" && generation.architecture.toLowerCase().includes("tca")
213
+ ? "tca_style"
214
+ : deps.has("swift-composable-architecture")
215
+ ? "tca_style"
216
+ : "native",
217
+ persistence:
218
+ typeof generation.persistence === "string"
219
+ ? generation.persistence
220
+ : deps.has("sqlite")
221
+ ? "sqlite"
222
+ : deps.has("swiftdata")
223
+ ? "swiftdata"
224
+ : "swiftdata",
225
+ di:
226
+ typeof generation.di === "string"
227
+ ? generation.di
228
+ : deps.has("factory")
229
+ ? "factory"
230
+ : deps.has("custom-di")
231
+ ? "custom"
232
+ : "none",
233
+ };
234
+ }
235
+
236
+ function normalizeExisting(target: SupportedTarget, existingPlatform: Record<string, any>): Record<string, string> {
237
+ switch (target) {
238
+ case "android":
239
+ return normalizeAndroid(existingPlatform);
240
+ case "web":
241
+ return normalizeWeb(existingPlatform);
242
+ case "ios":
243
+ return normalizeIos(existingPlatform);
244
+ }
245
+ }
246
+
247
+ function buildGeneration(
248
+ wizard: TargetWizardPreset,
249
+ answers: Record<string, string>,
250
+ existingGeneration: Record<string, any>,
251
+ framework: string
252
+ ): Record<string, any> {
253
+ const generation = {
254
+ ...(wizard.generation_defaults ?? {}),
255
+ ...existingGeneration,
256
+ };
257
+ const managedDependencies = collectManagedDependencies(wizard);
258
+ const frameworkDeps = wizard.framework_dependencies?.[framework] ?? [];
259
+ const derivedDependencies = [...(wizard.base_dependencies ?? []), ...frameworkDeps];
260
+
261
+ for (const question of wizard.questions) {
262
+ const answer = answers[question.key];
263
+ const selected = answer ? findOption(question, answer) : null;
264
+ if (!selected) {
265
+ generation[question.key] = answer || question.recommended;
266
+ continue;
267
+ }
268
+ generation[question.key] = selected.generation_value ?? selected.value;
269
+ Object.assign(generation, selected.extra_generation ?? {});
270
+ derivedDependencies.push(...(selected.dependencies ?? []));
271
+ }
272
+
273
+ generation.dependencies = mergeDependencies(
274
+ derivedDependencies,
275
+ existingGeneration.dependencies,
276
+ managedDependencies
277
+ );
278
+
279
+ return generation;
280
+ }
281
+
282
+ function parseTarget(argv: string[]): SupportedTarget | null {
283
+ const direct = argv[0];
284
+ if (direct && ["ios", "android", "web"].includes(direct)) {
285
+ return direct as SupportedTarget;
286
+ }
287
+ const targetIdx = argv.indexOf("--target");
288
+ if (targetIdx !== -1 && argv[targetIdx + 1] && ["ios", "android", "web"].includes(argv[targetIdx + 1])) {
289
+ return argv[targetIdx + 1] as SupportedTarget;
290
+ }
291
+ return null;
292
+ }
293
+
294
+ export async function runConfigureTarget(argv: string[]): Promise<void> {
295
+ const target = parseTarget(argv);
296
+ const useDefaults = argv.includes("--defaults");
297
+ const interactive = stdin.isTTY && stdout.isTTY && !useDefaults;
298
+ if (!target) {
299
+ console.error("Error: target is required for configure-target");
300
+ console.error("Usage: openuispec configure-target <ios|android|web> [--defaults]");
301
+ process.exit(1);
302
+ }
303
+
304
+ if (!interactive && !useDefaults) {
305
+ console.error(
306
+ "Error: `openuispec configure-target` needs a TTY for prompts.\n" +
307
+ "Run with `--defaults` in non-interactive environments."
308
+ );
309
+ process.exit(1);
310
+ }
311
+
312
+ const projectDir = findProjectDir(process.cwd());
313
+ const manifest = readManifest(projectDir);
314
+ const configuredTargets: string[] = manifest.generation?.targets ?? [];
315
+ if (configuredTargets.length > 0 && !configuredTargets.includes(target)) {
316
+ console.error(
317
+ `Error: target "${target}" is not listed in generation.targets.\n` +
318
+ `Configured targets: ${configuredTargets.join(", ")}`
319
+ );
320
+ process.exit(1);
321
+ }
322
+
323
+ const platformDir = resolve(projectDir, manifest.includes?.platform ?? "./platform/");
324
+ mkdirSync(platformDir, { recursive: true });
325
+
326
+ const platformPath = join(platformDir, `${target}.yaml`);
327
+ const existingDoc = existsSync(platformPath)
328
+ ? (YAML.parse(readFileSync(platformPath, "utf-8")) as Record<string, any>)
329
+ : {};
330
+ const existingPlatform = existingDoc[target] ?? {};
331
+ const existingGeneration = existingPlatform.generation ?? {};
332
+ const wizard = TARGET_WIZARDS[target];
333
+ const defaultFramework =
334
+ typeof existingPlatform.framework === "string" && existingPlatform.framework.trim().length > 0
335
+ ? existingPlatform.framework
336
+ : wizard.framework;
337
+ const inferredDefaults = {
338
+ ...normalizeExisting(target, existingPlatform),
339
+ };
340
+
341
+ let framework = defaultFramework;
342
+
343
+ function computeDefaultAnswers(fw: string): Record<string, string> {
344
+ return Object.fromEntries(
345
+ wizard.questions.map((question) => {
346
+ const defaultValue = effectiveDefault(question, fw, inferredDefaults[question.key]);
347
+ return [question.key, defaultValue];
348
+ })
349
+ );
350
+ }
351
+
352
+ let answers = computeDefaultAnswers(framework);
353
+
354
+ if (interactive) {
355
+ const rl = createInterface({ input: stdin, output: stdout });
356
+
357
+ try {
358
+ console.log(`\nOpenUISpec — Configure ${target}\n`);
359
+ console.log(`Writing target stack choices to ${relative(process.cwd(), platformPath)}\n`);
360
+
361
+ const frameworkOptions = [
362
+ ...(wizard.framework_options ?? [wizard.framework]),
363
+ "other",
364
+ ];
365
+ const chosenFramework = await askChoice(
366
+ rl,
367
+ wizard.framework_prompt ?? `${target} framework`,
368
+ frameworkOptions,
369
+ framework
370
+ );
371
+ framework =
372
+ chosenFramework === "other"
373
+ ? await ask(rl, `Custom ${target} framework`, framework)
374
+ : chosenFramework;
375
+
376
+ const defaultAnswers = computeDefaultAnswers(framework);
377
+ answers = {};
378
+ for (const question of wizard.questions) {
379
+ const filtered = filterOptionsForFramework(question, framework);
380
+ const chosen = await askChoice(
381
+ rl,
382
+ question.prompt,
383
+ [...filtered.map((option) => option.value), "other"],
384
+ defaultAnswers[question.key]
385
+ );
386
+ answers[question.key] =
387
+ chosen === "other"
388
+ ? await ask(rl, `Custom value for ${question.key}`, defaultAnswers[question.key])
389
+ : chosen;
390
+ }
391
+ } finally {
392
+ rl.close();
393
+ }
394
+ }
395
+
396
+ const updatedPlatform: Record<string, any> = {
397
+ ...existingPlatform,
398
+ framework,
399
+ generation: buildGeneration(wizard, answers, existingGeneration, framework),
400
+ };
401
+
402
+ if (wizard.language) updatedPlatform.language = wizard.language;
403
+ if (wizard.min_version) updatedPlatform.min_version = existingPlatform.min_version ?? wizard.min_version;
404
+ if (typeof wizard.min_sdk === "number") updatedPlatform.min_sdk = existingPlatform.min_sdk ?? wizard.min_sdk;
405
+ if (typeof wizard.target_sdk === "number") {
406
+ updatedPlatform.target_sdk = existingPlatform.target_sdk ?? wizard.target_sdk;
407
+ }
408
+
409
+ writeFileSync(platformPath, YAML.stringify({ [target]: updatedPlatform }));
410
+
411
+ console.log(`\nSaved ${relative(process.cwd(), platformPath)}`);
412
+ console.log("Configured values:");
413
+ for (const [key, value] of Object.entries(answers)) {
414
+ console.log(` - ${key}: ${value}`);
415
+ }
416
+ }
package/cli/index.ts CHANGED
@@ -4,10 +4,12 @@
4
4
  *
5
5
  * Usage:
6
6
  * openuispec init Create a new spec project
7
+ * openuispec init --defaults Scaffold non-interactively with defaults
8
+ * openuispec configure-target <t> Configure target stack and managed dependencies
7
9
  * openuispec drift [--target <t>] Check for spec drift
8
10
  * openuispec drift --snapshot --target <t> Snapshot current state + git baseline
9
11
  * openuispec drift --target <t> --explain Explain semantic changes since baseline
10
- * openuispec prepare --target <t> Build an AI-ready target update bundle
12
+ * openuispec prepare --target <t> Build the target work bundle
11
13
  * openuispec status Show cross-target baseline/drift status
12
14
  * openuispec validate [group...] Validate spec files against schemas
13
15
  */
@@ -45,13 +47,19 @@ async function main(): Promise<void> {
45
47
 
46
48
  switch (command) {
47
49
  case "init":
48
- await init();
50
+ await init(rest);
49
51
  break;
50
52
 
51
53
  case "update-rules":
52
54
  updateRules();
53
55
  break;
54
56
 
57
+ case "configure-target": {
58
+ const { runConfigureTarget } = await import("./configure-target.js");
59
+ await runConfigureTarget(rest);
60
+ break;
61
+ }
62
+
55
63
  case "drift": {
56
64
  const { runDrift } = await import("../drift/index.js");
57
65
  runDrift(rest);
@@ -85,11 +93,14 @@ OpenUISpec CLI v0.1
85
93
 
86
94
  Usage:
87
95
  openuispec init Create a new spec project
96
+ openuispec init --defaults Scaffold non-interactively with defaults
97
+ openuispec init --no-configure-targets Skip target stack setup during init
88
98
  openuispec update-rules Update AI rules to match installed version
99
+ openuispec configure-target <t> [--defaults] Configure target stack and managed dependencies
89
100
  openuispec drift [--target <t>] Check for spec drift
90
101
  openuispec drift --snapshot --target <t> Snapshot current state + git baseline
91
102
  openuispec drift --target <t> --explain Explain semantic changes since baseline
92
- openuispec prepare --target <t> Build an AI-ready target update bundle
103
+ openuispec prepare --target <t> Build the target work bundle
93
104
  openuispec status Show cross-target baseline/drift status
94
105
  openuispec validate [group...] Validate spec files
95
106