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/cli/init.ts
CHANGED
|
@@ -15,12 +15,12 @@ import {
|
|
|
15
15
|
existsSync,
|
|
16
16
|
appendFileSync,
|
|
17
17
|
} from "node:fs";
|
|
18
|
-
import { join, relative, dirname } from "node:path";
|
|
18
|
+
import { join, relative, dirname, resolve } from "node:path";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
|
|
21
21
|
// ── prompts ──────────────────────────────────────────────────────────
|
|
22
22
|
|
|
23
|
-
async function ask(
|
|
23
|
+
export async function ask(
|
|
24
24
|
rl: ReturnType<typeof createInterface>,
|
|
25
25
|
question: string,
|
|
26
26
|
fallback?: string
|
|
@@ -47,12 +47,32 @@ async function askList(
|
|
|
47
47
|
.filter((s) => options.includes(s));
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export async function askChoice(
|
|
51
|
+
rl: ReturnType<typeof createInterface>,
|
|
52
|
+
question: string,
|
|
53
|
+
options: string[],
|
|
54
|
+
fallback: string
|
|
55
|
+
): Promise<string> {
|
|
56
|
+
const answer = (await rl.question(`${question} [${options.join(", ")}] (${fallback}): `))
|
|
57
|
+
.trim()
|
|
58
|
+
.toLowerCase();
|
|
59
|
+
if (!answer) return fallback;
|
|
60
|
+
return options.includes(answer) ? answer : fallback;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function askYesNo(
|
|
64
|
+
rl: ReturnType<typeof createInterface>,
|
|
65
|
+
question: string,
|
|
66
|
+
fallback: boolean
|
|
67
|
+
): Promise<boolean> {
|
|
68
|
+
const answer = await askChoice(rl, question, ["yes", "no"], fallback ? "yes" : "no");
|
|
69
|
+
return answer === "yes";
|
|
70
|
+
}
|
|
71
|
+
|
|
50
72
|
// ── scaffold ─────────────────────────────────────────────────────────
|
|
51
73
|
|
|
52
74
|
function ensureDir(path: string): void {
|
|
53
|
-
|
|
54
|
-
mkdirSync(path, { recursive: true });
|
|
55
|
-
}
|
|
75
|
+
mkdirSync(path, { recursive: true });
|
|
56
76
|
}
|
|
57
77
|
|
|
58
78
|
function writeIfMissing(path: string, content: string): boolean {
|
|
@@ -85,7 +105,7 @@ function getPackageVersion(): string {
|
|
|
85
105
|
function manifestTemplate(
|
|
86
106
|
name: string,
|
|
87
107
|
targets: string[],
|
|
88
|
-
|
|
108
|
+
options: { withApi: boolean; backendPath: string | null }
|
|
89
109
|
): string {
|
|
90
110
|
const targetList = targets.join(", ");
|
|
91
111
|
const outputLines = targets
|
|
@@ -126,7 +146,9 @@ generation:
|
|
|
126
146
|
# ios: "../ios-app/" # relative to this file
|
|
127
147
|
# android: "../android-app/"
|
|
128
148
|
# web: "../web-ui/"
|
|
129
|
-
|
|
149
|
+
${options.withApi ? ` code_roots:
|
|
150
|
+
backend: "${options.backendPath}" # Required when api.endpoints are declared
|
|
151
|
+
` : ""} output_format:
|
|
130
152
|
${outputLines}
|
|
131
153
|
|
|
132
154
|
data_model: {}
|
|
@@ -169,9 +191,9 @@ Do NOT guess the file format — skipping this step will produce invalid YAML th
|
|
|
169
191
|
3. Online: \`https://openuispec.rsteam.uz/llms-full.txt\` (if not installed)
|
|
170
192
|
|
|
171
193
|
**Reference files inside the package (read in this order):**
|
|
172
|
-
1. \`README.md\` — schema tables, file format reference, root keys
|
|
194
|
+
1. \`README.md\` — schema tables, file format reference, root wrapper keys
|
|
173
195
|
2. \`spec/openuispec-v0.1.md\` — full specification (contracts, layout, expressions, etc.)
|
|
174
|
-
3. \`examples/taskflow/\` — complete working example with all file types
|
|
196
|
+
3. \`examples/taskflow/openuispec/\` — complete working example with all file types
|
|
175
197
|
4. \`schema/\` — JSON Schemas for validation
|
|
176
198
|
|
|
177
199
|
## CLI commands
|
|
@@ -180,12 +202,17 @@ Do NOT guess the file format — skipping this step will produce invalid YAML th
|
|
|
180
202
|
openuispec validate # Validate spec files against schemas
|
|
181
203
|
openuispec validate semantic # Run semantic cross-reference linting
|
|
182
204
|
openuispec validate screens # Validate only screens
|
|
205
|
+
openuispec configure-target ${targets[0]} [--defaults] # Configure target stack defaults
|
|
183
206
|
openuispec status # Show cross-target baseline/drift status
|
|
184
207
|
openuispec drift --target ${targets[0]} --explain # Explain semantic spec drift
|
|
185
|
-
openuispec prepare --target ${targets[0]} # Build
|
|
186
|
-
openuispec drift --snapshot --target ${targets[0]} # Snapshot current state + git baseline
|
|
208
|
+
openuispec prepare --target ${targets[0]} # Build the target work bundle
|
|
209
|
+
openuispec drift --snapshot --target ${targets[0]} # Snapshot current state + git baseline after target output exists
|
|
187
210
|
\`\`\`
|
|
188
211
|
|
|
212
|
+
The target work bundle has two modes:
|
|
213
|
+
- \`bootstrap\` when no snapshot exists yet, for first-time generation
|
|
214
|
+
- \`update\` after a snapshot exists, for drift-based target updates
|
|
215
|
+
|
|
189
216
|
## Learn more
|
|
190
217
|
|
|
191
218
|
Docs: https://openuispec.rsteam.uz
|
|
@@ -218,7 +245,7 @@ Do NOT guess the file format — skipping this step will produce invalid YAML th
|
|
|
218
245
|
**Reference files inside the package (read in this order):**
|
|
219
246
|
1. \`README.md\` — schema tables, file format reference, root wrapper keys
|
|
220
247
|
2. \`spec/openuispec-v0.1.md\` — full specification (contracts, layout, expressions, adaptive, etc.)
|
|
221
|
-
3. \`examples/taskflow/\` — complete working example with all file types
|
|
248
|
+
3. \`examples/taskflow/openuispec/\` — complete working example with all file types
|
|
222
249
|
4. \`schema/\` — JSON Schemas for every file type
|
|
223
250
|
|
|
224
251
|
These files are updated with each package version. Always read from the installed package,
|
|
@@ -253,7 +280,7 @@ This means the project has existing UI code but hasn't been specced yet. Your jo
|
|
|
253
280
|
type: scroll_vertical
|
|
254
281
|
\`\`\`
|
|
255
282
|
4. **Extract tokens** — scan for colors, fonts, spacing and create files in \`${specDir}/tokens/\`.
|
|
256
|
-
5. **Update the manifest** — fill in \`data_model\` and \`
|
|
283
|
+
5. **Update the manifest** — fill in \`data_model\`, \`api.endpoints\`, and \`generation.code_roots.backend\` in \`${specDir}/openuispec.yaml\`.
|
|
257
284
|
|
|
258
285
|
## OpenUISpec Source Of Truth
|
|
259
286
|
|
|
@@ -274,9 +301,9 @@ Spec-first workflow:
|
|
|
274
301
|
4. Run \`openuispec validate\`.
|
|
275
302
|
5. Run \`openuispec validate semantic\`.
|
|
276
303
|
6. Run \`openuispec drift --target <target> --explain\` to inspect semantic changes since that target's baseline.
|
|
277
|
-
7. Run \`openuispec prepare --target <target>\` to build the
|
|
304
|
+
7. Run \`openuispec prepare --target <target>\` to build the target work bundle for that target. In \`bootstrap\` mode it provides first-generation constraints; in \`update\` mode it provides drift-based update scope.
|
|
278
305
|
8. Verify the affected UI targets build/run if possible.
|
|
279
|
-
9. Only then run \`openuispec drift --snapshot --target <target>\` for affected targets.
|
|
306
|
+
9. Only then run \`openuispec drift --snapshot --target <target>\` for affected targets, after that target output directory exists.
|
|
280
307
|
10. Run \`openuispec drift --target <target> --explain\` again to confirm no spec changes remain for that target.
|
|
281
308
|
11. Use \`openuispec status\` to see which other targets are still behind the updated spec.
|
|
282
309
|
|
|
@@ -302,8 +329,8 @@ Platform-first workflow:
|
|
|
302
329
|
- \`openuispec validate semantic\` — run semantic cross-reference linting
|
|
303
330
|
- \`openuispec drift --target <t>\` — check for spec drift
|
|
304
331
|
- \`openuispec drift --target <t> --explain\` — explain semantic spec drift since the target baseline
|
|
305
|
-
- \`openuispec drift --snapshot --target <t>\` — snapshot current state
|
|
306
|
-
- \`openuispec prepare --target <t>\` — build
|
|
332
|
+
- \`openuispec drift --snapshot --target <t>\` — snapshot current state after the target output exists
|
|
333
|
+
- \`openuispec prepare --target <t>\` — build the target work bundle
|
|
307
334
|
- \`openuispec status\` — show cross-target baseline/drift status
|
|
308
335
|
- \`openuispec update-rules\` — update AI rules to match installed package version
|
|
309
336
|
- \`openuispec drift --all\` — include stubs in drift check
|
|
@@ -400,43 +427,178 @@ export function extractRulesVersion(filePath: string): string | null {
|
|
|
400
427
|
|
|
401
428
|
export { getPackageVersion };
|
|
402
429
|
|
|
430
|
+
interface InitOptions {
|
|
431
|
+
defaults: boolean;
|
|
432
|
+
name?: string;
|
|
433
|
+
specDir?: string;
|
|
434
|
+
targets?: string[];
|
|
435
|
+
withApi?: boolean;
|
|
436
|
+
backendPath?: string;
|
|
437
|
+
configureTargets?: boolean;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
interface InitAnswers {
|
|
441
|
+
name: string;
|
|
442
|
+
specDir: string;
|
|
443
|
+
targets: string[];
|
|
444
|
+
withApi: boolean;
|
|
445
|
+
backendPath: string | null;
|
|
446
|
+
configureTargets: boolean;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function parseTargetsValue(raw: string): string[] {
|
|
450
|
+
const allowed = new Set(["ios", "android", "web"]);
|
|
451
|
+
return raw
|
|
452
|
+
.split(",")
|
|
453
|
+
.map((value) => value.trim().toLowerCase())
|
|
454
|
+
.filter((value): value is string => allowed.has(value));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function requireFlagValue(argv: string[], index: number, flag: string): string {
|
|
458
|
+
const value = argv[index + 1];
|
|
459
|
+
if (!value || value.startsWith("--")) {
|
|
460
|
+
console.error(`Error: ${flag} requires a value.`);
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
return value;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function parseInitArgs(argv: string[]): InitOptions {
|
|
467
|
+
const options: InitOptions = { defaults: argv.includes("--defaults") };
|
|
468
|
+
|
|
469
|
+
for (let index = 0; index < argv.length; index++) {
|
|
470
|
+
const arg = argv[index];
|
|
471
|
+
switch (arg) {
|
|
472
|
+
case "--defaults":
|
|
473
|
+
break;
|
|
474
|
+
case "--name":
|
|
475
|
+
options.name = requireFlagValue(argv, index, arg);
|
|
476
|
+
index++;
|
|
477
|
+
break;
|
|
478
|
+
case "--spec-dir":
|
|
479
|
+
options.specDir = requireFlagValue(argv, index, arg);
|
|
480
|
+
index++;
|
|
481
|
+
break;
|
|
482
|
+
case "--targets":
|
|
483
|
+
options.targets = parseTargetsValue(requireFlagValue(argv, index, arg));
|
|
484
|
+
index++;
|
|
485
|
+
break;
|
|
486
|
+
case "--backend":
|
|
487
|
+
options.backendPath = requireFlagValue(argv, index, arg);
|
|
488
|
+
index++;
|
|
489
|
+
break;
|
|
490
|
+
case "--with-api":
|
|
491
|
+
options.withApi = true;
|
|
492
|
+
break;
|
|
493
|
+
case "--no-api":
|
|
494
|
+
options.withApi = false;
|
|
495
|
+
break;
|
|
496
|
+
case "--configure-targets":
|
|
497
|
+
options.configureTargets = true;
|
|
498
|
+
break;
|
|
499
|
+
case "--no-configure-targets":
|
|
500
|
+
options.configureTargets = false;
|
|
501
|
+
break;
|
|
502
|
+
default:
|
|
503
|
+
if (arg.startsWith("--")) {
|
|
504
|
+
console.error(`Error: Unknown init option: ${arg}`);
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return options;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function collectDefaults(): InitAnswers {
|
|
514
|
+
const cwd = process.cwd();
|
|
515
|
+
const defaultName = cwd.split("/").pop() || "MyApp";
|
|
516
|
+
return {
|
|
517
|
+
name: defaultName,
|
|
518
|
+
specDir: "openuispec",
|
|
519
|
+
targets: ["ios", "android", "web"],
|
|
520
|
+
withApi: true,
|
|
521
|
+
backendPath: "../backend/",
|
|
522
|
+
configureTargets: true,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function collectInteractiveAnswers(rl: ReturnType<typeof createInterface>): Promise<InitAnswers> {
|
|
527
|
+
const defaults = collectDefaults();
|
|
528
|
+
const name = await ask(rl, "Project name", defaults.name);
|
|
529
|
+
const specDir = await ask(rl, "Spec directory", defaults.specDir);
|
|
530
|
+
const targets = await askList(rl, "\nWhich platforms?", ["ios", "android", "web"], defaults.targets);
|
|
531
|
+
|
|
532
|
+
if (targets.length === 0) {
|
|
533
|
+
console.error("At least one target is required.");
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const withApi = await askYesNo(rl, "Will this spec declare API endpoints?", defaults.withApi);
|
|
538
|
+
const backendPath = withApi
|
|
539
|
+
? await ask(rl, "Backend folder path relative to openuispec.yaml", defaults.backendPath ?? "../backend/")
|
|
540
|
+
: null;
|
|
541
|
+
const configureTargets = await askYesNo(rl, "Configure target stacks now?", defaults.configureTargets);
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
name,
|
|
545
|
+
specDir,
|
|
546
|
+
targets,
|
|
547
|
+
withApi,
|
|
548
|
+
backendPath,
|
|
549
|
+
configureTargets,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function collectNonInteractiveAnswers(argv: string[]): InitAnswers {
|
|
554
|
+
const parsed = parseInitArgs(argv);
|
|
555
|
+
const defaults = collectDefaults();
|
|
556
|
+
|
|
557
|
+
if (!parsed.defaults && argv.length === 0) {
|
|
558
|
+
console.error(
|
|
559
|
+
"Error: `openuispec init` needs a TTY for prompts.\n" +
|
|
560
|
+
"Run with `--defaults` or pass flags such as `--name`, `--targets`, `--with-api`, `--backend`, and `--configure-targets`."
|
|
561
|
+
);
|
|
562
|
+
process.exit(1);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const targets = parsed.targets && parsed.targets.length > 0 ? parsed.targets : defaults.targets;
|
|
566
|
+
if (targets.length === 0) {
|
|
567
|
+
console.error("Error: --targets must include at least one of ios, android, web.");
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const withApi = parsed.withApi ?? defaults.withApi;
|
|
572
|
+
const backendPath = withApi ? parsed.backendPath ?? defaults.backendPath : null;
|
|
573
|
+
|
|
574
|
+
return {
|
|
575
|
+
name: parsed.name ?? defaults.name,
|
|
576
|
+
specDir: parsed.specDir ?? defaults.specDir,
|
|
577
|
+
targets,
|
|
578
|
+
withApi,
|
|
579
|
+
backendPath,
|
|
580
|
+
configureTargets: parsed.configureTargets ?? defaults.configureTargets,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
403
584
|
// ── main ─────────────────────────────────────────────────────────────
|
|
404
585
|
|
|
405
|
-
export async function init(): Promise<void> {
|
|
406
|
-
const
|
|
586
|
+
export async function init(argv: string[] = []): Promise<void> {
|
|
587
|
+
const interactive = stdin.isTTY && stdout.isTTY && !argv.includes("--defaults");
|
|
588
|
+
const rl = interactive ? createInterface({ input: stdin, output: stdout }) : null;
|
|
407
589
|
|
|
408
590
|
console.log("\nOpenUISpec — Project Setup\n");
|
|
409
591
|
|
|
410
592
|
try {
|
|
411
|
-
// 1. Project name (display name in manifest, derived from current folder)
|
|
412
593
|
const cwd = process.cwd();
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
// 2. Spec directory
|
|
417
|
-
const specDir = await ask(rl, "Spec directory", "openuispec");
|
|
418
|
-
|
|
419
|
-
// 3. Platforms
|
|
420
|
-
const allTargets = ["ios", "android", "web"];
|
|
421
|
-
const targets = await askList(
|
|
422
|
-
rl,
|
|
423
|
-
"\nWhich platforms?",
|
|
424
|
-
allTargets,
|
|
425
|
-
allTargets
|
|
426
|
-
);
|
|
427
|
-
|
|
428
|
-
if (targets.length === 0) {
|
|
429
|
-
console.error("At least one target is required.");
|
|
430
|
-
process.exit(1);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
rl.close();
|
|
594
|
+
const answers = rl ? await collectInteractiveAnswers(rl) : collectNonInteractiveAnswers(argv);
|
|
595
|
+
rl?.close();
|
|
434
596
|
|
|
435
597
|
// ── create folders ─────────────────────────────────────────────
|
|
436
598
|
|
|
437
599
|
console.log("\nScaffolding...\n");
|
|
438
600
|
|
|
439
|
-
const root = join(cwd, specDir);
|
|
601
|
+
const root = join(cwd, answers.specDir);
|
|
440
602
|
const dirs = [
|
|
441
603
|
"tokens",
|
|
442
604
|
"contracts",
|
|
@@ -455,14 +617,17 @@ export async function init(): Promise<void> {
|
|
|
455
617
|
|
|
456
618
|
writeIfMissing(
|
|
457
619
|
join(root, "openuispec.yaml"),
|
|
458
|
-
manifestTemplate(name, targets,
|
|
620
|
+
manifestTemplate(answers.name, answers.targets, {
|
|
621
|
+
withApi: answers.withApi,
|
|
622
|
+
backendPath: answers.backendPath,
|
|
623
|
+
})
|
|
459
624
|
);
|
|
460
625
|
|
|
461
626
|
// ── spec README ──────────────────────────────────────────────
|
|
462
627
|
|
|
463
628
|
writeIfMissing(
|
|
464
629
|
join(root, "README.md"),
|
|
465
|
-
specReadmeTemplate(name, targets)
|
|
630
|
+
specReadmeTemplate(answers.name, answers.targets)
|
|
466
631
|
);
|
|
467
632
|
|
|
468
633
|
// ── .gitkeep for empty dirs ────────────────────────────────────
|
|
@@ -481,9 +646,21 @@ export async function init(): Promise<void> {
|
|
|
481
646
|
}
|
|
482
647
|
}
|
|
483
648
|
|
|
649
|
+
if (answers.withApi && answers.backendPath) {
|
|
650
|
+
const backendDir = resolve(root, answers.backendPath);
|
|
651
|
+
const backendExisted = existsSync(backendDir);
|
|
652
|
+
ensureDir(backendDir);
|
|
653
|
+
const backendEntries = readdirSync(backendDir).filter((entry) => entry !== ".gitkeep");
|
|
654
|
+
const backendGitkeep = join(backendDir, ".gitkeep");
|
|
655
|
+
if ((!backendExisted || backendEntries.length === 0) && !existsSync(backendGitkeep)) {
|
|
656
|
+
writeFileSync(backendGitkeep, "");
|
|
657
|
+
console.log(` create ${relative(cwd, backendGitkeep)}`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
484
661
|
// ── AI assistant rules ─────────────────────────────────────────
|
|
485
662
|
|
|
486
|
-
const rules = aiRulesBlock(specDir, targets);
|
|
663
|
+
const rules = aiRulesBlock(answers.specDir, answers.targets);
|
|
487
664
|
|
|
488
665
|
for (const file of ["CLAUDE.md", "AGENTS.md"]) {
|
|
489
666
|
const filePath = join(cwd, file);
|
|
@@ -501,40 +678,49 @@ export async function init(): Promise<void> {
|
|
|
501
678
|
}
|
|
502
679
|
}
|
|
503
680
|
|
|
681
|
+
if (answers.configureTargets) {
|
|
682
|
+
console.log("\nConfiguring target stacks...\n");
|
|
683
|
+
const { runConfigureTarget } = await import("./configure-target.js");
|
|
684
|
+
for (const target of answers.targets) {
|
|
685
|
+
await runConfigureTarget([target, ...(interactive ? [] : ["--defaults"])]);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
504
689
|
// ── done ───────────────────────────────────────────────────────
|
|
505
690
|
|
|
506
691
|
console.log(`
|
|
507
|
-
Done! Your spec project is ready at ./${specDir}/
|
|
692
|
+
Done! Your spec project is ready at ./${answers.specDir}/
|
|
508
693
|
|
|
509
694
|
Getting started (new project):
|
|
510
|
-
1. Edit ${specDir}/openuispec.yaml — define your data model and API
|
|
511
|
-
2. Create screens in ${specDir}/screens/ (one YAML per screen)
|
|
512
|
-
3. Create flows in ${specDir}/flows/ (multi-step navigation)
|
|
695
|
+
1. Edit ${answers.specDir}/openuispec.yaml — define your data model and API
|
|
696
|
+
2. Create screens in ${answers.specDir}/screens/ (one YAML per screen)
|
|
697
|
+
3. Create flows in ${answers.specDir}/flows/ (multi-step navigation)
|
|
513
698
|
4. Ask AI to generate native code from the spec
|
|
514
|
-
5. Run \`openuispec drift --snapshot --target ${targets[0]}\` to baseline the first accepted target state
|
|
699
|
+
5. Run \`openuispec drift --snapshot --target ${answers.targets[0]}\` to baseline the first accepted target state after that target output directory exists
|
|
515
700
|
|
|
516
701
|
Getting started (existing project):
|
|
517
702
|
1. Ask AI to read your existing UI code and generate spec files:
|
|
518
|
-
"Read src/screens/HomeScreen.swift and create ${specDir}/screens/home.yaml as status: stub"
|
|
703
|
+
"Read src/screens/HomeScreen.swift and create ${answers.specDir}/screens/home.yaml as status: stub"
|
|
519
704
|
2. Spec screens incrementally: stub → draft → ready
|
|
520
705
|
3. Only ready/draft screens are tracked by drift detection
|
|
521
706
|
4. Run \`openuispec validate\` to check specs against the schema
|
|
522
|
-
5. Use \`openuispec drift --target ${targets[0]} --explain\` and \`openuispec prepare --target ${targets[0]}\` before asking AI to update a target
|
|
707
|
+
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
|
|
523
708
|
|
|
524
709
|
Commands:
|
|
525
710
|
openuispec validate Validate spec files
|
|
526
711
|
openuispec validate semantic Check semantic cross-references
|
|
712
|
+
openuispec configure-target ios [--defaults] Configure target stack defaults
|
|
527
713
|
openuispec status Show cross-target baseline/drift status
|
|
528
714
|
openuispec drift --target ios --explain Explain semantic spec changes
|
|
529
|
-
openuispec prepare --target ios Build
|
|
530
|
-
openuispec drift --snapshot --target ios Save current state + git baseline
|
|
715
|
+
openuispec prepare --target ios Build the target work bundle
|
|
716
|
+
openuispec drift --snapshot --target ios Save current state + git baseline after target output exists
|
|
531
717
|
|
|
532
718
|
AI rules have been added to CLAUDE.md and AGENTS.md.
|
|
533
719
|
|
|
534
720
|
Docs: https://openuispec.rsteam.uz
|
|
535
721
|
`);
|
|
536
722
|
} catch (err) {
|
|
537
|
-
rl
|
|
723
|
+
rl?.close();
|
|
538
724
|
throw err;
|
|
539
725
|
}
|
|
540
726
|
}
|