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/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
- if (!existsSync(path)) {
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
- specDir: string
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
- output_format:
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 an AI-ready target update bundle
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 \`api.endpoints\` in \`${specDir}/openuispec.yaml\`.
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 AI/developer work bundle for that target.
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 an AI-ready target update bundle
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 rl = createInterface({ input: stdin, output: stdout });
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 defaultName = cwd.split("/").pop() || "MyApp";
414
- const name = await ask(rl, "Project name", defaultName);
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, specDir)
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 an AI-ready target update bundle
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.close();
723
+ rl?.close();
538
724
  throw err;
539
725
  }
540
726
  }