openuispec 0.1.29 → 0.1.31

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 CHANGED
@@ -188,13 +188,15 @@ Use the commands like this:
188
188
  - `openuispec validate` checks schema correctness
189
189
  - `openuispec validate semantic` checks cross-references such as locale keys, formatters, mappers, contracts, icons, navigation targets, and API endpoints
190
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
191
+ - `openuispec configure-target <t>` records and confirms target stack choices in `platform/<target>.yaml`, while still allowing custom framework/library values when the project uses something outside the catalog
192
192
  - `openuispec drift --target <t> --explain` explains semantic spec changes since that target's accepted baseline
193
193
  - `openuispec prepare --target <t>` builds the target work bundle for either first-time generation or drift-based updates
194
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
195
 
196
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
197
 
198
+ If stack choices were auto-applied via `configure-target --defaults` or `init --defaults`, they remain unconfirmed. `prepare` will block implementation readiness until the user explicitly confirms the target stack, and AI agents should ask the user to confirm or change those choices instead of silently proceeding to code generation.
199
+
198
200
  When target stack choices come from the preset catalog, `prepare --json` also exposes install-oriented refs for the selected options:
199
201
  - Android: Gradle plugin ids and library coordinates
200
202
  - Web: npm package specs
@@ -4,11 +4,9 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { dirname, join, relative, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import YAML from "yaml";
7
- import { findProjectDir, readManifest } from "../drift/index.js";
7
+ import { findProjectDir, isSupportedTarget, readManifest, type SupportedTarget } from "../drift/index.js";
8
8
  import { ask, askChoice } from "./init.js";
9
9
 
10
- type SupportedTarget = "ios" | "android" | "web";
11
-
12
10
  type WizardOptionPreset = {
13
11
  value: string;
14
12
  generation_value?: string;
@@ -44,6 +42,27 @@ type TargetWizardPreset = {
44
42
  questions: ChoiceQuestionPreset[];
45
43
  };
46
44
 
45
+ export type TargetWizardOptionsResponse = {
46
+ target: SupportedTarget;
47
+ defaults_are_unconfirmed: true;
48
+ confirmation_required_before_implementation: true;
49
+ interactive_command: string;
50
+ defaults_command: string;
51
+ framework: {
52
+ prompt: string;
53
+ recommended: string;
54
+ options: string[];
55
+ custom_allowed: true;
56
+ };
57
+ questions: Array<{
58
+ key: string;
59
+ prompt: string;
60
+ recommended: string;
61
+ custom_allowed: true;
62
+ options: WizardOptionPreset[];
63
+ }>;
64
+ };
65
+
47
66
  function readWizardPresets(): Record<SupportedTarget, TargetWizardPreset> {
48
67
  const presetsPath = join(dirname(fileURLToPath(import.meta.url)), "target-presets.json");
49
68
  return JSON.parse(readFileSync(presetsPath, "utf-8")) as Record<SupportedTarget, TargetWizardPreset>;
@@ -51,6 +70,34 @@ function readWizardPresets(): Record<SupportedTarget, TargetWizardPreset> {
51
70
 
52
71
  const TARGET_WIZARDS = readWizardPresets();
53
72
 
73
+ function listFrameworkOptions(wizard: TargetWizardPreset): string[] {
74
+ return [...new Set([...(wizard.framework_options ?? [wizard.framework]), "other"])];
75
+ }
76
+
77
+ export function listTargetWizardOptions(target: SupportedTarget): TargetWizardOptionsResponse {
78
+ const wizard = TARGET_WIZARDS[target];
79
+ return {
80
+ target,
81
+ defaults_are_unconfirmed: true,
82
+ confirmation_required_before_implementation: true,
83
+ interactive_command: `openuispec configure-target ${target}`,
84
+ defaults_command: `openuispec configure-target ${target} --defaults`,
85
+ framework: {
86
+ prompt: wizard.framework_prompt ?? `${target} framework`,
87
+ recommended: wizard.framework,
88
+ options: listFrameworkOptions(wizard),
89
+ custom_allowed: true,
90
+ },
91
+ questions: wizard.questions.map((question) => ({
92
+ key: question.key,
93
+ prompt: question.prompt,
94
+ recommended: question.recommended,
95
+ custom_allowed: true,
96
+ options: question.options,
97
+ })),
98
+ };
99
+ }
100
+
54
101
  function filterOptionsForFramework(
55
102
  question: ChoiceQuestionPreset,
56
103
  framework: string
@@ -279,32 +326,72 @@ function buildGeneration(
279
326
  return generation;
280
327
  }
281
328
 
329
+ function stackConfirmation(useDefaults: boolean): Record<string, string> {
330
+ const now = new Date().toISOString();
331
+ return useDefaults
332
+ ? {
333
+ status: "pending_user_confirmation",
334
+ source: "defaults",
335
+ updated_at: now,
336
+ }
337
+ : {
338
+ status: "confirmed",
339
+ source: "user",
340
+ confirmed_at: now,
341
+ };
342
+ }
343
+
282
344
  function parseTarget(argv: string[]): SupportedTarget | null {
283
345
  const direct = argv[0];
284
- if (direct && ["ios", "android", "web"].includes(direct)) {
285
- return direct as SupportedTarget;
346
+ if (direct && isSupportedTarget(direct)) {
347
+ return direct;
286
348
  }
287
349
  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;
350
+ if (targetIdx !== -1 && argv[targetIdx + 1] && isSupportedTarget(argv[targetIdx + 1])) {
351
+ return argv[targetIdx + 1];
290
352
  }
291
353
  return null;
292
354
  }
293
355
 
356
+ function parseSetPairs(argv: string[]): Record<string, string> {
357
+ const pairs: Record<string, string> = {};
358
+ for (let i = 0; i < argv.length; i++) {
359
+ if (argv[i] === "--set" && argv[i + 1]) {
360
+ const raw = argv[i + 1];
361
+ const eqIdx = raw.indexOf("=");
362
+ if (eqIdx > 0) {
363
+ pairs[raw.slice(0, eqIdx)] = raw.slice(eqIdx + 1);
364
+ }
365
+ i++;
366
+ }
367
+ }
368
+ return pairs;
369
+ }
370
+
294
371
  export async function runConfigureTarget(argv: string[]): Promise<void> {
295
372
  const target = parseTarget(argv);
373
+ const listOptions = argv.includes("--list-options");
296
374
  const useDefaults = argv.includes("--defaults");
297
- const interactive = stdin.isTTY && stdout.isTTY && !useDefaults;
375
+ const quiet = argv.includes("--quiet");
376
+ const setPairs = parseSetPairs(argv);
377
+ const hasSetPairs = Object.keys(setPairs).length > 0;
378
+ const interactive = stdin.isTTY && stdout.isTTY && !useDefaults && !hasSetPairs;
298
379
  if (!target) {
299
380
  console.error("Error: target is required for configure-target");
300
- console.error("Usage: openuispec configure-target <ios|android|web> [--defaults]");
381
+ console.error("Usage: openuispec configure-target <ios|android|web> [--defaults] [--list-options] [--set key=value]");
301
382
  process.exit(1);
302
383
  }
303
384
 
304
- if (!interactive && !useDefaults) {
385
+ if (listOptions) {
386
+ console.log(JSON.stringify(listTargetWizardOptions(target), null, 2));
387
+ return;
388
+ }
389
+
390
+ if (!interactive && !useDefaults && !hasSetPairs) {
305
391
  console.error(
306
392
  "Error: `openuispec configure-target` needs a TTY for prompts.\n" +
307
- "Run with `--defaults` in non-interactive environments."
393
+ "Preferred: ask the user to confirm the target stack, then run `openuispec configure-target <target>` in an interactive terminal.\n" +
394
+ "Fallback: run with `--defaults` only for unattended setup; those values remain unconfirmed and `prepare` will block implementation until the user confirms them."
308
395
  );
309
396
  process.exit(1);
310
397
  }
@@ -351,7 +438,16 @@ export async function runConfigureTarget(argv: string[]): Promise<void> {
351
438
 
352
439
  let answers = computeDefaultAnswers(framework);
353
440
 
354
- if (interactive) {
441
+ if (hasSetPairs) {
442
+ // Non-interactive --set path: merge provided values with existing/defaults
443
+ const knownKeys = new Set(wizard.questions.map((q) => q.key));
444
+ for (const [key, value] of Object.entries(setPairs)) {
445
+ if (!knownKeys.has(key)) {
446
+ console.error(`Warning: "${key}" does not match any wizard question; setting as custom value.`);
447
+ }
448
+ answers[key] = value;
449
+ }
450
+ } else if (interactive) {
355
451
  const rl = createInterface({ input: stdin, output: stdout });
356
452
 
357
453
  try {
@@ -393,10 +489,14 @@ export async function runConfigureTarget(argv: string[]): Promise<void> {
393
489
  }
394
490
  }
395
491
 
492
+ // --set implies confirmed (user explicitly chose values); --defaults without --set is pending
396
493
  const updatedPlatform: Record<string, any> = {
397
494
  ...existingPlatform,
398
495
  framework,
399
- generation: buildGeneration(wizard, answers, existingGeneration, framework),
496
+ generation: {
497
+ ...buildGeneration(wizard, answers, existingGeneration, framework),
498
+ stack_confirmation: stackConfirmation(useDefaults && !hasSetPairs),
499
+ },
400
500
  };
401
501
 
402
502
  if (wizard.language) updatedPlatform.language = wizard.language;
@@ -408,9 +508,16 @@ export async function runConfigureTarget(argv: string[]): Promise<void> {
408
508
 
409
509
  writeFileSync(platformPath, YAML.stringify({ [target]: updatedPlatform }));
410
510
 
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}`);
511
+ const savedPath = relative(process.cwd(), platformPath);
512
+ if (argv.includes("--silent")) {
513
+ // Called as subroutine (e.g. from init --quiet) — no output at all
514
+ } else if (quiet) {
515
+ console.log(savedPath);
516
+ } else {
517
+ console.log(`\nSaved ${savedPath}`);
518
+ console.log("Configured values:");
519
+ for (const [key, value] of Object.entries(answers)) {
520
+ console.log(` - ${key}: ${value}`);
521
+ }
415
522
  }
416
523
  }
package/cli/index.ts CHANGED
@@ -4,13 +4,16 @@
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
+ * openuispec init --defaults Scaffold non-interactively with unconfirmed defaults
8
+ * openuispec init --list-options Print init prompt options as JSON
9
+ * openuispec configure-target <t> Configure and confirm target stack choices
10
+ * openuispec configure-target <t> --list-options Print target stack prompt options as JSON
9
11
  * openuispec drift [--target <t>] Check for spec drift
10
12
  * openuispec drift --snapshot --target <t> Snapshot current state + git baseline
11
13
  * openuispec drift --target <t> --explain Explain semantic changes since baseline
12
14
  * openuispec prepare --target <t> Build the target work bundle
13
15
  * openuispec status Show cross-target baseline/drift status
16
+ * openuispec check --target <t> [--json] Composite validation + prepare readiness
14
17
  * openuispec validate [group...] Validate spec files against schemas
15
18
  */
16
19
 
@@ -78,6 +81,12 @@ async function main(): Promise<void> {
78
81
  break;
79
82
  }
80
83
 
84
+ case "check": {
85
+ const { runCheck } = await import("../check/index.js");
86
+ runCheck(rest);
87
+ break;
88
+ }
89
+
81
90
  case "validate": {
82
91
  const { runValidate } = await import("../schema/validate.js");
83
92
  runValidate(rest);
@@ -93,19 +102,26 @@ OpenUISpec CLI v0.1
93
102
 
94
103
  Usage:
95
104
  openuispec init Create a new spec project
96
- openuispec init --defaults Scaffold non-interactively with defaults
105
+ openuispec init --defaults Scaffold non-interactively with unconfirmed defaults
106
+ openuispec init --list-options Print init prompt options as JSON
97
107
  openuispec init --no-configure-targets Skip target stack setup during init
98
108
  openuispec update-rules Update AI rules to match installed version
99
- openuispec configure-target <t> [--defaults] Configure target stack and managed dependencies
109
+ openuispec configure-target <t> [--defaults] Configure target stack; --defaults stays unconfirmed
110
+ openuispec configure-target <t> --set k=v Set specific stack values (confirmed)
111
+ openuispec configure-target <t> --list-options Print target stack prompt options as JSON
100
112
  openuispec drift [--target <t>] Check for spec drift
101
113
  openuispec drift --snapshot --target <t> Snapshot current state + git baseline
102
114
  openuispec drift --target <t> --explain Explain semantic changes since baseline
103
115
  openuispec prepare --target <t> Build the target work bundle
104
116
  openuispec status Show cross-target baseline/drift status
105
- openuispec validate [group...] Validate spec files
117
+ openuispec check --target <t> [--json] Composite validation + prepare readiness
118
+ openuispec validate [group...] [--json] Validate spec files
119
+ openuispec validate semantic --json Semantic validation as JSON
106
120
 
107
121
  Validate groups: manifest, tokens, screens, flows, platform, locales, contracts, semantic
108
122
 
123
+ Exit codes: 0 = success, 1 = missing config/usage error, 2 = validation failure
124
+
109
125
  Docs: https://openuispec.rsteam.uz
110
126
  `);
111
127
  break;
@@ -117,4 +133,7 @@ Docs: https://openuispec.rsteam.uz
117
133
  }
118
134
  }
119
135
 
120
- main();
136
+ main().catch((err) => {
137
+ console.error(err instanceof Error ? err.message : String(err));
138
+ process.exit(1);
139
+ });
package/cli/init.ts CHANGED
@@ -17,6 +17,71 @@ import {
17
17
  } from "node:fs";
18
18
  import { join, relative, dirname, resolve } from "node:path";
19
19
  import { fileURLToPath } from "node:url";
20
+ import { SUPPORTED_TARGETS, isSupportedTarget, type SupportedTarget } from "../drift/index.js";
21
+
22
+ // ── list-options ────────────────────────────────────────────────────
23
+
24
+ export type InitOptionsResponse = {
25
+ command: "init";
26
+ note: string;
27
+ questions: Array<{
28
+ key: string;
29
+ prompt: string;
30
+ type: "text" | "list" | "yes_no";
31
+ default: string | string[] | boolean;
32
+ options?: string[];
33
+ }>;
34
+ configure_targets_note: string;
35
+ };
36
+
37
+ export function listInitOptions(): InitOptionsResponse {
38
+ const defaults = collectDefaults();
39
+ return {
40
+ command: "init",
41
+ note: "After init, run `openuispec configure-target <target> --list-options` for each target to get stack choices.",
42
+ questions: [
43
+ {
44
+ key: "name",
45
+ prompt: "Project name",
46
+ type: "text",
47
+ default: defaults.name,
48
+ },
49
+ {
50
+ key: "spec_dir",
51
+ prompt: "Spec directory",
52
+ type: "text",
53
+ default: defaults.specDir,
54
+ },
55
+ {
56
+ key: "targets",
57
+ prompt: "Which platforms?",
58
+ type: "list",
59
+ default: defaults.targets,
60
+ options: [...SUPPORTED_TARGETS],
61
+ },
62
+ {
63
+ key: "with_api",
64
+ prompt: "Will this spec declare API endpoints?",
65
+ type: "yes_no",
66
+ default: defaults.withApi,
67
+ },
68
+ {
69
+ key: "backend_path",
70
+ prompt: "Backend folder path relative to openuispec.yaml",
71
+ type: "text",
72
+ default: defaults.backendPath ?? "../backend/",
73
+ },
74
+ {
75
+ key: "configure_targets",
76
+ prompt: "Configure target stacks now?",
77
+ type: "yes_no",
78
+ default: defaults.configureTargets,
79
+ },
80
+ ],
81
+ configure_targets_note:
82
+ "If configure_targets is true, use `openuispec configure-target <target> --list-options` for each target after init to present stack choices to the user.",
83
+ };
84
+ }
20
85
 
21
86
  // ── prompts ──────────────────────────────────────────────────────────
22
87
 
@@ -75,13 +140,13 @@ function ensureDir(path: string): void {
75
140
  mkdirSync(path, { recursive: true });
76
141
  }
77
142
 
78
- function writeIfMissing(path: string, content: string): boolean {
143
+ function writeIfMissing(path: string, content: string, quiet = false): boolean {
79
144
  if (existsSync(path)) {
80
- console.log(` skip ${relative(process.cwd(), path)} (exists)`);
145
+ if (!quiet) console.log(` skip ${relative(process.cwd(), path)} (exists)`);
81
146
  return false;
82
147
  }
83
148
  writeFileSync(path, content);
84
- console.log(` create ${relative(process.cwd(), path)}`);
149
+ if (!quiet) console.log(` create ${relative(process.cwd(), path)}`);
85
150
  return true;
86
151
  }
87
152
 
@@ -202,7 +267,7 @@ Do NOT guess the file format — skipping this step will produce invalid YAML th
202
267
  openuispec validate # Validate spec files against schemas
203
268
  openuispec validate semantic # Run semantic cross-reference linting
204
269
  openuispec validate screens # Validate only screens
205
- openuispec configure-target ${targets[0]} [--defaults] # Configure target stack defaults
270
+ openuispec configure-target ${targets[0]} [--defaults] # Configure target stack; --defaults stays unconfirmed
206
271
  openuispec status # Show cross-target baseline/drift status
207
272
  openuispec drift --target ${targets[0]} --explain # Explain semantic spec drift
208
273
  openuispec prepare --target ${targets[0]} # Build the target work bundle
@@ -213,6 +278,8 @@ The target work bundle has two modes:
213
278
  - \`bootstrap\` when no snapshot exists yet, for first-time generation
214
279
  - \`update\` after a snapshot exists, for drift-based target updates
215
280
 
281
+ If target stack values were written with \`--defaults\`, treat them as unconfirmed. Before generating code, ask the user to confirm or change the stack and run \`openuispec configure-target <target>\` without \`--defaults\`.
282
+
216
283
  ## Learn more
217
284
 
218
285
  Docs: https://openuispec.rsteam.uz
@@ -302,6 +369,7 @@ Spec-first workflow:
302
369
  5. Run \`openuispec validate semantic\`.
303
370
  6. Run \`openuispec drift --target <target> --explain\` to inspect semantic changes since that target's baseline.
304
371
  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.
372
+ If the target stack was filled from defaults, stop and ask the user to confirm or change it before implementation.
305
373
  8. Verify the affected UI targets build/run if possible.
306
374
  9. Only then run \`openuispec drift --snapshot --target <target>\` for affected targets, after that target output directory exists.
307
375
  10. Run \`openuispec drift --target <target> --explain\` again to confirm no spec changes remain for that target.
@@ -322,6 +390,7 @@ Platform-first workflow:
322
390
  - Do not treat \`openuispec drift\` as proof that generated UI matches the spec.
323
391
  - Do not skip \`--explain\` / \`prepare\` when another platform needs to catch up with shared spec changes.
324
392
  - Do not modify generated UI without checking whether the spec must change first.
393
+ - Do not use \`configure-target --defaults\` as silent approval for implementation. Ask the user to confirm the stack first.
325
394
 
326
395
  ## CLI commands
327
396
  - \`openuispec init\` — scaffold a new spec project
@@ -330,7 +399,7 @@ Platform-first workflow:
330
399
  - \`openuispec drift --target <t>\` — check for spec drift
331
400
  - \`openuispec drift --target <t> --explain\` — explain semantic spec drift since the target baseline
332
401
  - \`openuispec drift --snapshot --target <t>\` — snapshot current state after the target output exists
333
- - \`openuispec prepare --target <t>\` — build the target work bundle
402
+ - \`openuispec prepare --target <t>\` — build the target work bundle and check whether stack confirmation is still pending
334
403
  - \`openuispec status\` — show cross-target baseline/drift status
335
404
  - \`openuispec update-rules\` — update AI rules to match installed package version
336
405
  - \`openuispec drift --all\` — include stubs in drift check
@@ -354,7 +423,7 @@ export function updateRules(): void {
354
423
  }
355
424
 
356
425
  // Detect targets from manifest
357
- let targets = ["ios", "android", "web"];
426
+ let targets = [...SUPPORTED_TARGETS];
358
427
  try {
359
428
  const manifest = readFileSync(
360
429
  join(cwd, specDir, "openuispec.yaml"),
@@ -429,6 +498,7 @@ export { getPackageVersion };
429
498
 
430
499
  interface InitOptions {
431
500
  defaults: boolean;
501
+ quiet: boolean;
432
502
  name?: string;
433
503
  specDir?: string;
434
504
  targets?: string[];
@@ -447,11 +517,10 @@ interface InitAnswers {
447
517
  }
448
518
 
449
519
  function parseTargetsValue(raw: string): string[] {
450
- const allowed = new Set(["ios", "android", "web"]);
451
520
  return raw
452
521
  .split(",")
453
522
  .map((value) => value.trim().toLowerCase())
454
- .filter((value): value is string => allowed.has(value));
523
+ .filter((value): value is SupportedTarget => isSupportedTarget(value));
455
524
  }
456
525
 
457
526
  function requireFlagValue(argv: string[], index: number, flag: string): string {
@@ -464,12 +533,13 @@ function requireFlagValue(argv: string[], index: number, flag: string): string {
464
533
  }
465
534
 
466
535
  function parseInitArgs(argv: string[]): InitOptions {
467
- const options: InitOptions = { defaults: argv.includes("--defaults") };
536
+ const options: InitOptions = { defaults: argv.includes("--defaults"), quiet: argv.includes("--quiet") };
468
537
 
469
538
  for (let index = 0; index < argv.length; index++) {
470
539
  const arg = argv[index];
471
540
  switch (arg) {
472
541
  case "--defaults":
542
+ case "--quiet":
473
543
  break;
474
544
  case "--name":
475
545
  options.name = requireFlagValue(argv, index, arg);
@@ -516,7 +586,7 @@ function collectDefaults(): InitAnswers {
516
586
  return {
517
587
  name: defaultName,
518
588
  specDir: "openuispec",
519
- targets: ["ios", "android", "web"],
589
+ targets: [...SUPPORTED_TARGETS],
520
590
  withApi: true,
521
591
  backendPath: "../backend/",
522
592
  configureTargets: true,
@@ -527,7 +597,7 @@ async function collectInteractiveAnswers(rl: ReturnType<typeof createInterface>)
527
597
  const defaults = collectDefaults();
528
598
  const name = await ask(rl, "Project name", defaults.name);
529
599
  const specDir = await ask(rl, "Spec directory", defaults.specDir);
530
- const targets = await askList(rl, "\nWhich platforms?", ["ios", "android", "web"], defaults.targets);
600
+ const targets = await askList(rl, "\nWhich platforms?", [...SUPPORTED_TARGETS], defaults.targets);
531
601
 
532
602
  if (targets.length === 0) {
533
603
  console.error("At least one target is required.");
@@ -554,10 +624,10 @@ function collectNonInteractiveAnswers(argv: string[]): InitAnswers {
554
624
  const parsed = parseInitArgs(argv);
555
625
  const defaults = collectDefaults();
556
626
 
557
- if (!parsed.defaults && argv.length === 0) {
627
+ if (!parsed.defaults && argv.filter((a) => a !== "--quiet").length === 0) {
558
628
  console.error(
559
629
  "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`."
630
+ "Run with `--list-options` to get prompt definitions as JSON, or pass flags such as `--name`, `--targets`, `--with-api`, `--backend`, and `--configure-targets`."
561
631
  );
562
632
  process.exit(1);
563
633
  }
@@ -584,10 +654,16 @@ function collectNonInteractiveAnswers(argv: string[]): InitAnswers {
584
654
  // ── main ─────────────────────────────────────────────────────────────
585
655
 
586
656
  export async function init(argv: string[] = []): Promise<void> {
657
+ if (argv.includes("--list-options")) {
658
+ console.log(JSON.stringify(listInitOptions(), null, 2));
659
+ return;
660
+ }
661
+
662
+ const quiet = argv.includes("--quiet");
587
663
  const interactive = stdin.isTTY && stdout.isTTY && !argv.includes("--defaults");
588
664
  const rl = interactive ? createInterface({ input: stdin, output: stdout }) : null;
589
665
 
590
- console.log("\nOpenUISpec — Project Setup\n");
666
+ if (!quiet) console.log("\nOpenUISpec — Project Setup\n");
591
667
 
592
668
  try {
593
669
  const cwd = process.cwd();
@@ -596,7 +672,7 @@ export async function init(argv: string[] = []): Promise<void> {
596
672
 
597
673
  // ── create folders ─────────────────────────────────────────────
598
674
 
599
- console.log("\nScaffolding...\n");
675
+ if (!quiet) console.log("\nScaffolding...\n");
600
676
 
601
677
  const root = join(cwd, answers.specDir);
602
678
  const dirs = [
@@ -620,14 +696,16 @@ export async function init(argv: string[] = []): Promise<void> {
620
696
  manifestTemplate(answers.name, answers.targets, {
621
697
  withApi: answers.withApi,
622
698
  backendPath: answers.backendPath,
623
- })
699
+ }),
700
+ quiet
624
701
  );
625
702
 
626
703
  // ── spec README ──────────────────────────────────────────────
627
704
 
628
705
  writeIfMissing(
629
706
  join(root, "README.md"),
630
- specReadmeTemplate(answers.name, answers.targets)
707
+ specReadmeTemplate(answers.name, answers.targets),
708
+ quiet
631
709
  );
632
710
 
633
711
  // ── .gitkeep for empty dirs ────────────────────────────────────
@@ -641,23 +719,11 @@ export async function init(argv: string[] = []): Promise<void> {
641
719
  const gk = join(dir, ".gitkeep");
642
720
  if (!existsSync(gk)) {
643
721
  writeFileSync(gk, "");
644
- console.log(` create ${relative(cwd, gk)}`);
722
+ if (!quiet) console.log(` create ${relative(cwd, gk)}`);
645
723
  }
646
724
  }
647
725
  }
648
726
 
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
-
661
727
  // ── AI assistant rules ─────────────────────────────────────────
662
728
 
663
729
  const rules = aiRulesBlock(answers.specDir, answers.targets);
@@ -667,28 +733,31 @@ export async function init(argv: string[] = []): Promise<void> {
667
733
  if (existsSync(filePath)) {
668
734
  const existing = readFileSync(filePath, "utf-8");
669
735
  if (existing.includes("OpenUISpec")) {
670
- console.log(` skip ${file} (already has OpenUISpec rules)`);
736
+ if (!quiet) console.log(` skip ${file} (already has OpenUISpec rules)`);
671
737
  continue;
672
738
  }
673
739
  appendFileSync(filePath, "\n" + rules);
674
- console.log(` update ${file} (appended rules)`);
740
+ if (!quiet) console.log(` update ${file} (appended rules)`);
675
741
  } else {
676
742
  writeFileSync(filePath, rules.trimStart());
677
- console.log(` create ${file}`);
743
+ if (!quiet) console.log(` create ${file}`);
678
744
  }
679
745
  }
680
746
 
681
747
  if (answers.configureTargets) {
682
- console.log("\nConfiguring target stacks...\n");
748
+ if (!quiet) console.log("\nConfiguring target stacks...\n");
683
749
  const { runConfigureTarget } = await import("./configure-target.js");
684
750
  for (const target of answers.targets) {
685
- await runConfigureTarget([target, ...(interactive ? [] : ["--defaults"])]);
751
+ await runConfigureTarget([target, ...(interactive ? [] : ["--defaults"]), ...(quiet ? ["--silent"] : [])]);
686
752
  }
687
753
  }
688
754
 
689
755
  // ── done ───────────────────────────────────────────────────────
690
756
 
691
- console.log(`
757
+ if (quiet) {
758
+ console.log(`./${answers.specDir}/`);
759
+ } else {
760
+ console.log(`
692
761
  Done! Your spec project is ready at ./${answers.specDir}/
693
762
 
694
763
  Getting started (new project):
@@ -709,7 +778,7 @@ Getting started (existing project):
709
778
  Commands:
710
779
  openuispec validate Validate spec files
711
780
  openuispec validate semantic Check semantic cross-references
712
- openuispec configure-target ios [--defaults] Configure target stack defaults
781
+ openuispec configure-target ios [--defaults] Configure target stack; --defaults stays unconfirmed
713
782
  openuispec status Show cross-target baseline/drift status
714
783
  openuispec drift --target ios --explain Explain semantic spec changes
715
784
  openuispec prepare --target ios Build the target work bundle
@@ -719,6 +788,7 @@ AI rules have been added to CLAUDE.md and AGENTS.md.
719
788
 
720
789
  Docs: https://openuispec.rsteam.uz
721
790
  `);
791
+ }
722
792
  } catch (err) {
723
793
  rl?.close();
724
794
  throw err;