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.
- package/README.md +52 -55
- package/cli/configure-target.ts +416 -0
- package/cli/index.ts +14 -3
- package/cli/init.ts +241 -55
- package/cli/target-presets.json +746 -0
- package/docs/implementation-notes.md +47 -10
- package/docs/release-notes-v0.1.26.md +1 -1
- package/docs/release-notes-v0.1.28.md +25 -0
- package/docs/stress-test-maturity-report.md +1 -1
- package/drift/index.ts +31 -11
- package/examples/taskflow/AGENTS.md +113 -0
- package/examples/taskflow/CLAUDE.md +113 -0
- package/examples/taskflow/backend/.gitkeep +1 -0
- package/examples/taskflow/generated/android/TaskFlow/README.md +43 -0
- package/examples/taskflow/generated/android/TaskFlow/app/build.gradle.kts +76 -0
- package/examples/taskflow/generated/android/TaskFlow/app/proguard-rules.pro +1 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/AndroidManifest.xml +21 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/MainActivity.kt +19 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/TaskFlowApp.kt +283 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/DomainModels.kt +106 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/SampleData.kt +57 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/components/Common.kt +109 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/HomeScreen.kt +112 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/ProjectsScreen.kt +61 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/SettingsScreen.kt +82 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/TaskDetailScreen.kt +111 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/sheets/Sheets.kt +77 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Color.kt +30 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Theme.kt +86 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Type.kt +57 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/strings.xml +155 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/themes.xml +4 -0
- package/examples/taskflow/generated/android/TaskFlow/build.gradle.kts +5 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle/gradle-daemon-jvm.properties +12 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle.properties +4 -0
- package/examples/taskflow/generated/android/TaskFlow/gradlew +18 -0
- package/examples/taskflow/generated/android/TaskFlow/gradlew.bat +12 -0
- package/examples/taskflow/generated/android/TaskFlow/settings.gradle.kts +18 -0
- package/examples/taskflow/generated/ios/TaskFlow/README.md +21 -0
- package/examples/taskflow/generated/ios/TaskFlow/Resources/en.lproj/Localizable.strings +115 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/App/TaskFlowApp.swift +24 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Components/AppChrome.swift +150 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Flows/TaskEditorSheet.swift +220 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Models/DomainModels.swift +122 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/CalendarView.swift +21 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/HomeView.swift +201 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProfileEditView.swift +48 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectDetailView.swift +59 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectsView.swift +63 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/SettingsView.swift +85 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/TaskDetailView.swift +219 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppModel.swift +320 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppSupport.swift +41 -0
- package/examples/taskflow/generated/ios/TaskFlow/project.yml +26 -0
- package/examples/taskflow/generated/web/TaskFlow/README.md +19 -0
- package/examples/taskflow/generated/web/TaskFlow/index.html +12 -0
- package/examples/taskflow/generated/web/TaskFlow/package-lock.json +1908 -0
- package/examples/taskflow/generated/web/TaskFlow/package.json +24 -0
- package/examples/taskflow/generated/web/TaskFlow/src/App.tsx +58 -0
- package/examples/taskflow/generated/web/TaskFlow/src/AppShell.tsx +55 -0
- package/examples/taskflow/generated/web/TaskFlow/src/components/Common.tsx +82 -0
- package/examples/taskflow/generated/web/TaskFlow/src/components/Modals.tsx +191 -0
- package/examples/taskflow/generated/web/TaskFlow/src/components/Nav.tsx +41 -0
- package/examples/taskflow/generated/web/TaskFlow/src/generated/messages.ts +131 -0
- package/examples/taskflow/generated/web/TaskFlow/src/hooks.ts +25 -0
- package/examples/taskflow/generated/web/TaskFlow/src/i18n.ts +39 -0
- package/examples/taskflow/generated/web/TaskFlow/src/locales.en.json +111 -0
- package/examples/taskflow/generated/web/TaskFlow/src/main.tsx +13 -0
- package/examples/taskflow/generated/web/TaskFlow/src/screens/HomeScreen.tsx +111 -0
- package/examples/taskflow/generated/web/TaskFlow/src/screens/ProjectsScreen.tsx +82 -0
- package/examples/taskflow/generated/web/TaskFlow/src/screens/SettingsScreens.tsx +132 -0
- package/examples/taskflow/generated/web/TaskFlow/src/screens/TaskDetail.tsx +105 -0
- package/examples/taskflow/generated/web/TaskFlow/src/store.ts +216 -0
- package/examples/taskflow/generated/web/TaskFlow/src/styles.css +617 -0
- package/examples/taskflow/generated/web/TaskFlow/src/types.ts +64 -0
- package/examples/taskflow/generated/web/TaskFlow/src/utils.ts +78 -0
- package/examples/taskflow/generated/web/TaskFlow/tsconfig.json +21 -0
- package/examples/taskflow/generated/web/TaskFlow/vite.config.ts +6 -0
- package/examples/taskflow/openuispec/README.md +54 -0
- package/examples/taskflow/{openuispec.yaml → openuispec/openuispec.yaml} +2 -0
- package/examples/todo-orbit/AGENTS.md +48 -22
- package/examples/todo-orbit/CLAUDE.md +48 -22
- package/examples/todo-orbit/backend/.gitkeep +1 -0
- package/examples/todo-orbit/openuispec/README.md +9 -4
- package/examples/todo-orbit/openuispec/openuispec.yaml +2 -0
- package/package.json +1 -1
- package/prepare/index.ts +811 -25
- package/schema/openuispec.schema.json +10 -0
- package/schema/semantic-lint.ts +36 -12
- package/schema/validate.ts +9 -4
- package/status/index.ts +16 -3
- /package/examples/taskflow/{contracts → openuispec/contracts}/README.md +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/action_trigger.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/collection.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/data_display.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/feedback.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/input_field.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/nav_container.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/surface.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/x_media_player.yaml +0 -0
- /package/examples/taskflow/{flows → openuispec/flows}/create_task.yaml +0 -0
- /package/examples/taskflow/{flows → openuispec/flows}/edit_task.yaml +0 -0
- /package/examples/taskflow/{locales → openuispec/locales}/en.json +0 -0
- /package/examples/taskflow/{platform → openuispec/platform}/android.yaml +0 -0
- /package/examples/taskflow/{platform → openuispec/platform}/ios.yaml +0 -0
- /package/examples/taskflow/{platform → openuispec/platform}/web.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/calendar.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/home.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/profile_edit.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/project_detail.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/projects.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/settings.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/task_detail.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/color.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/elevation.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/icons.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/layout.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/motion.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/spacing.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/themes.yaml +0 -0
- /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
|
|
106
|
-
│ │ ├── openuispec
|
|
107
|
-
│ │ ├──
|
|
108
|
-
│ │
|
|
109
|
-
│ │
|
|
110
|
-
│
|
|
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
|
-
│
|
|
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/ #
|
|
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>`
|
|
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
|
|
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
|
|
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
|
|
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
|
|