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 +3 -1
- package/cli/configure-target.ts +124 -17
- package/cli/index.ts +25 -6
- package/cli/init.ts +107 -37
- package/cli/target-presets.json +40 -40
- package/docs/implementation-notes.md +4 -0
- package/drift/index.ts +8 -2
- package/examples/taskflow/AGENTS.md +4 -3
- package/examples/taskflow/CLAUDE.md +4 -3
- package/examples/todo-orbit/AGENTS.md +4 -3
- package/examples/todo-orbit/CLAUDE.md +4 -3
- package/package.json +1 -1
- package/prepare/index.ts +46 -1
- package/schema/semantic-lint.ts +31 -5
- package/schema/validate.ts +192 -5
- package/status/index.ts +13 -7
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
|
|
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
|
package/cli/configure-target.ts
CHANGED
|
@@ -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 &&
|
|
285
|
-
return direct
|
|
346
|
+
if (direct && isSupportedTarget(direct)) {
|
|
347
|
+
return direct;
|
|
286
348
|
}
|
|
287
349
|
const targetIdx = argv.indexOf("--target");
|
|
288
|
-
if (targetIdx !== -1 && argv[targetIdx + 1] &&
|
|
289
|
-
return argv[targetIdx + 1]
|
|
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
|
|
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 (
|
|
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
|
-
"
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
|
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
|
|
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
|
|
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 = [
|
|
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
|
|
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: [
|
|
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?", [
|
|
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 `--
|
|
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
|
-
|
|
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;
|