openuispec 0.1.28 → 0.1.30
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 +33 -37
- package/cli/configure-target.ts +523 -0
- package/cli/index.ts +33 -5
- package/cli/init.ts +247 -60
- package/cli/target-presets.json +746 -0
- package/docs/implementation-notes.md +46 -9
- package/docs/release-notes-v0.1.26.md +1 -1
- package/drift/index.ts +18 -9
- package/examples/taskflow/AGENTS.md +6 -4
- package/examples/taskflow/CLAUDE.md +6 -4
- package/examples/taskflow/backend/.gitkeep +1 -0
- package/examples/taskflow/openuispec/README.md +7 -2
- package/examples/taskflow/openuispec/openuispec.yaml +2 -0
- package/examples/todo-orbit/AGENTS.md +6 -4
- package/examples/todo-orbit/CLAUDE.md +6 -4
- package/examples/todo-orbit/backend/.gitkeep +1 -0
- package/examples/todo-orbit/openuispec/README.md +7 -2
- package/examples/todo-orbit/openuispec/openuispec.yaml +2 -0
- package/package.json +1 -1
- package/prepare/index.ts +856 -25
- package/schema/openuispec.schema.json +10 -0
- package/schema/semantic-lint.ts +66 -16
- package/schema/validate.ts +192 -5
- package/status/index.ts +13 -7
package/cli/index.ts
CHANGED
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* openuispec init Create a new spec project
|
|
7
|
+
* openuispec init --defaults Scaffold non-interactively with unconfirmed defaults
|
|
8
|
+
* openuispec configure-target <t> Configure and confirm target stack choices
|
|
9
|
+
* openuispec configure-target <t> --list-options Print target stack prompt options as JSON
|
|
7
10
|
* openuispec drift [--target <t>] Check for spec drift
|
|
8
11
|
* openuispec drift --snapshot --target <t> Snapshot current state + git baseline
|
|
9
12
|
* openuispec drift --target <t> --explain Explain semantic changes since baseline
|
|
10
|
-
* openuispec prepare --target <t> Build
|
|
13
|
+
* openuispec prepare --target <t> Build the target work bundle
|
|
11
14
|
* openuispec status Show cross-target baseline/drift status
|
|
15
|
+
* openuispec check --target <t> [--json] Composite validation + prepare readiness
|
|
12
16
|
* openuispec validate [group...] Validate spec files against schemas
|
|
13
17
|
*/
|
|
14
18
|
|
|
@@ -45,13 +49,19 @@ async function main(): Promise<void> {
|
|
|
45
49
|
|
|
46
50
|
switch (command) {
|
|
47
51
|
case "init":
|
|
48
|
-
await init();
|
|
52
|
+
await init(rest);
|
|
49
53
|
break;
|
|
50
54
|
|
|
51
55
|
case "update-rules":
|
|
52
56
|
updateRules();
|
|
53
57
|
break;
|
|
54
58
|
|
|
59
|
+
case "configure-target": {
|
|
60
|
+
const { runConfigureTarget } = await import("./configure-target.js");
|
|
61
|
+
await runConfigureTarget(rest);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
case "drift": {
|
|
56
66
|
const { runDrift } = await import("../drift/index.js");
|
|
57
67
|
runDrift(rest);
|
|
@@ -70,6 +80,12 @@ async function main(): Promise<void> {
|
|
|
70
80
|
break;
|
|
71
81
|
}
|
|
72
82
|
|
|
83
|
+
case "check": {
|
|
84
|
+
const { runCheck } = await import("../check/index.js");
|
|
85
|
+
runCheck(rest);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
|
|
73
89
|
case "validate": {
|
|
74
90
|
const { runValidate } = await import("../schema/validate.js");
|
|
75
91
|
runValidate(rest);
|
|
@@ -85,16 +101,25 @@ OpenUISpec CLI v0.1
|
|
|
85
101
|
|
|
86
102
|
Usage:
|
|
87
103
|
openuispec init Create a new spec project
|
|
104
|
+
openuispec init --defaults Scaffold non-interactively with unconfirmed defaults
|
|
105
|
+
openuispec init --no-configure-targets Skip target stack setup during init
|
|
88
106
|
openuispec update-rules Update AI rules to match installed version
|
|
107
|
+
openuispec configure-target <t> [--defaults] Configure target stack; --defaults stays unconfirmed
|
|
108
|
+
openuispec configure-target <t> --set k=v Set specific stack values (confirmed)
|
|
109
|
+
openuispec configure-target <t> --list-options Print target stack prompt options as JSON
|
|
89
110
|
openuispec drift [--target <t>] Check for spec drift
|
|
90
111
|
openuispec drift --snapshot --target <t> Snapshot current state + git baseline
|
|
91
112
|
openuispec drift --target <t> --explain Explain semantic changes since baseline
|
|
92
|
-
openuispec prepare --target <t> Build
|
|
113
|
+
openuispec prepare --target <t> Build the target work bundle
|
|
93
114
|
openuispec status Show cross-target baseline/drift status
|
|
94
|
-
openuispec
|
|
115
|
+
openuispec check --target <t> [--json] Composite validation + prepare readiness
|
|
116
|
+
openuispec validate [group...] [--json] Validate spec files
|
|
117
|
+
openuispec validate semantic --json Semantic validation as JSON
|
|
95
118
|
|
|
96
119
|
Validate groups: manifest, tokens, screens, flows, platform, locales, contracts, semantic
|
|
97
120
|
|
|
121
|
+
Exit codes: 0 = success, 1 = missing config/usage error, 2 = validation failure
|
|
122
|
+
|
|
98
123
|
Docs: https://openuispec.rsteam.uz
|
|
99
124
|
`);
|
|
100
125
|
break;
|
|
@@ -106,4 +131,7 @@ Docs: https://openuispec.rsteam.uz
|
|
|
106
131
|
}
|
|
107
132
|
}
|
|
108
133
|
|
|
109
|
-
main()
|
|
134
|
+
main().catch((err) => {
|
|
135
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
136
|
+
process.exit(1);
|
|
137
|
+
});
|
package/cli/init.ts
CHANGED
|
@@ -15,12 +15,13 @@ 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
|
+
import { SUPPORTED_TARGETS, isSupportedTarget, type SupportedTarget } from "../drift/index.js";
|
|
20
21
|
|
|
21
22
|
// ── prompts ──────────────────────────────────────────────────────────
|
|
22
23
|
|
|
23
|
-
async function ask(
|
|
24
|
+
export async function ask(
|
|
24
25
|
rl: ReturnType<typeof createInterface>,
|
|
25
26
|
question: string,
|
|
26
27
|
fallback?: string
|
|
@@ -47,21 +48,41 @@ async function askList(
|
|
|
47
48
|
.filter((s) => options.includes(s));
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
export async function askChoice(
|
|
52
|
+
rl: ReturnType<typeof createInterface>,
|
|
53
|
+
question: string,
|
|
54
|
+
options: string[],
|
|
55
|
+
fallback: string
|
|
56
|
+
): Promise<string> {
|
|
57
|
+
const answer = (await rl.question(`${question} [${options.join(", ")}] (${fallback}): `))
|
|
58
|
+
.trim()
|
|
59
|
+
.toLowerCase();
|
|
60
|
+
if (!answer) return fallback;
|
|
61
|
+
return options.includes(answer) ? answer : fallback;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function askYesNo(
|
|
65
|
+
rl: ReturnType<typeof createInterface>,
|
|
66
|
+
question: string,
|
|
67
|
+
fallback: boolean
|
|
68
|
+
): Promise<boolean> {
|
|
69
|
+
const answer = await askChoice(rl, question, ["yes", "no"], fallback ? "yes" : "no");
|
|
70
|
+
return answer === "yes";
|
|
71
|
+
}
|
|
72
|
+
|
|
50
73
|
// ── scaffold ─────────────────────────────────────────────────────────
|
|
51
74
|
|
|
52
75
|
function ensureDir(path: string): void {
|
|
53
|
-
|
|
54
|
-
mkdirSync(path, { recursive: true });
|
|
55
|
-
}
|
|
76
|
+
mkdirSync(path, { recursive: true });
|
|
56
77
|
}
|
|
57
78
|
|
|
58
|
-
function writeIfMissing(path: string, content: string): boolean {
|
|
79
|
+
function writeIfMissing(path: string, content: string, quiet = false): boolean {
|
|
59
80
|
if (existsSync(path)) {
|
|
60
|
-
console.log(` skip ${relative(process.cwd(), path)} (exists)`);
|
|
81
|
+
if (!quiet) console.log(` skip ${relative(process.cwd(), path)} (exists)`);
|
|
61
82
|
return false;
|
|
62
83
|
}
|
|
63
84
|
writeFileSync(path, content);
|
|
64
|
-
console.log(` create ${relative(process.cwd(), path)}`);
|
|
85
|
+
if (!quiet) console.log(` create ${relative(process.cwd(), path)}`);
|
|
65
86
|
return true;
|
|
66
87
|
}
|
|
67
88
|
|
|
@@ -85,7 +106,7 @@ function getPackageVersion(): string {
|
|
|
85
106
|
function manifestTemplate(
|
|
86
107
|
name: string,
|
|
87
108
|
targets: string[],
|
|
88
|
-
|
|
109
|
+
options: { withApi: boolean; backendPath: string | null }
|
|
89
110
|
): string {
|
|
90
111
|
const targetList = targets.join(", ");
|
|
91
112
|
const outputLines = targets
|
|
@@ -126,7 +147,9 @@ generation:
|
|
|
126
147
|
# ios: "../ios-app/" # relative to this file
|
|
127
148
|
# android: "../android-app/"
|
|
128
149
|
# web: "../web-ui/"
|
|
129
|
-
|
|
150
|
+
${options.withApi ? ` code_roots:
|
|
151
|
+
backend: "${options.backendPath}" # Required when api.endpoints are declared
|
|
152
|
+
` : ""} output_format:
|
|
130
153
|
${outputLines}
|
|
131
154
|
|
|
132
155
|
data_model: {}
|
|
@@ -169,7 +192,7 @@ Do NOT guess the file format — skipping this step will produce invalid YAML th
|
|
|
169
192
|
3. Online: \`https://openuispec.rsteam.uz/llms-full.txt\` (if not installed)
|
|
170
193
|
|
|
171
194
|
**Reference files inside the package (read in this order):**
|
|
172
|
-
1. \`README.md\` — schema tables, file format reference, root keys
|
|
195
|
+
1. \`README.md\` — schema tables, file format reference, root wrapper keys
|
|
173
196
|
2. \`spec/openuispec-v0.1.md\` — full specification (contracts, layout, expressions, etc.)
|
|
174
197
|
3. \`examples/taskflow/openuispec/\` — complete working example with all file types
|
|
175
198
|
4. \`schema/\` — JSON Schemas for validation
|
|
@@ -180,12 +203,19 @@ Do NOT guess the file format — skipping this step will produce invalid YAML th
|
|
|
180
203
|
openuispec validate # Validate spec files against schemas
|
|
181
204
|
openuispec validate semantic # Run semantic cross-reference linting
|
|
182
205
|
openuispec validate screens # Validate only screens
|
|
206
|
+
openuispec configure-target ${targets[0]} [--defaults] # Configure target stack; --defaults stays unconfirmed
|
|
183
207
|
openuispec status # Show cross-target baseline/drift status
|
|
184
208
|
openuispec drift --target ${targets[0]} --explain # Explain semantic spec drift
|
|
185
|
-
openuispec prepare --target ${targets[0]} # Build
|
|
209
|
+
openuispec prepare --target ${targets[0]} # Build the target work bundle
|
|
186
210
|
openuispec drift --snapshot --target ${targets[0]} # Snapshot current state + git baseline after target output exists
|
|
187
211
|
\`\`\`
|
|
188
212
|
|
|
213
|
+
The target work bundle has two modes:
|
|
214
|
+
- \`bootstrap\` when no snapshot exists yet, for first-time generation
|
|
215
|
+
- \`update\` after a snapshot exists, for drift-based target updates
|
|
216
|
+
|
|
217
|
+
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\`.
|
|
218
|
+
|
|
189
219
|
## Learn more
|
|
190
220
|
|
|
191
221
|
Docs: https://openuispec.rsteam.uz
|
|
@@ -253,7 +283,7 @@ This means the project has existing UI code but hasn't been specced yet. Your jo
|
|
|
253
283
|
type: scroll_vertical
|
|
254
284
|
\`\`\`
|
|
255
285
|
4. **Extract tokens** — scan for colors, fonts, spacing and create files in \`${specDir}/tokens/\`.
|
|
256
|
-
5. **Update the manifest** — fill in \`data_model\` and \`
|
|
286
|
+
5. **Update the manifest** — fill in \`data_model\`, \`api.endpoints\`, and \`generation.code_roots.backend\` in \`${specDir}/openuispec.yaml\`.
|
|
257
287
|
|
|
258
288
|
## OpenUISpec Source Of Truth
|
|
259
289
|
|
|
@@ -274,7 +304,8 @@ Spec-first workflow:
|
|
|
274
304
|
4. Run \`openuispec validate\`.
|
|
275
305
|
5. Run \`openuispec validate semantic\`.
|
|
276
306
|
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
|
|
307
|
+
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.
|
|
308
|
+
If the target stack was filled from defaults, stop and ask the user to confirm or change it before implementation.
|
|
278
309
|
8. Verify the affected UI targets build/run if possible.
|
|
279
310
|
9. Only then run \`openuispec drift --snapshot --target <target>\` for affected targets, after that target output directory exists.
|
|
280
311
|
10. Run \`openuispec drift --target <target> --explain\` again to confirm no spec changes remain for that target.
|
|
@@ -295,6 +326,7 @@ Platform-first workflow:
|
|
|
295
326
|
- Do not treat \`openuispec drift\` as proof that generated UI matches the spec.
|
|
296
327
|
- Do not skip \`--explain\` / \`prepare\` when another platform needs to catch up with shared spec changes.
|
|
297
328
|
- Do not modify generated UI without checking whether the spec must change first.
|
|
329
|
+
- Do not use \`configure-target --defaults\` as silent approval for implementation. Ask the user to confirm the stack first.
|
|
298
330
|
|
|
299
331
|
## CLI commands
|
|
300
332
|
- \`openuispec init\` — scaffold a new spec project
|
|
@@ -303,7 +335,7 @@ Platform-first workflow:
|
|
|
303
335
|
- \`openuispec drift --target <t>\` — check for spec drift
|
|
304
336
|
- \`openuispec drift --target <t> --explain\` — explain semantic spec drift since the target baseline
|
|
305
337
|
- \`openuispec drift --snapshot --target <t>\` — snapshot current state after the target output exists
|
|
306
|
-
- \`openuispec prepare --target <t>\` — build
|
|
338
|
+
- \`openuispec prepare --target <t>\` — build the target work bundle and check whether stack confirmation is still pending
|
|
307
339
|
- \`openuispec status\` — show cross-target baseline/drift status
|
|
308
340
|
- \`openuispec update-rules\` — update AI rules to match installed package version
|
|
309
341
|
- \`openuispec drift --all\` — include stubs in drift check
|
|
@@ -327,7 +359,7 @@ export function updateRules(): void {
|
|
|
327
359
|
}
|
|
328
360
|
|
|
329
361
|
// Detect targets from manifest
|
|
330
|
-
let targets = [
|
|
362
|
+
let targets = [...SUPPORTED_TARGETS];
|
|
331
363
|
try {
|
|
332
364
|
const manifest = readFileSync(
|
|
333
365
|
join(cwd, specDir, "openuispec.yaml"),
|
|
@@ -400,43 +432,180 @@ export function extractRulesVersion(filePath: string): string | null {
|
|
|
400
432
|
|
|
401
433
|
export { getPackageVersion };
|
|
402
434
|
|
|
403
|
-
|
|
435
|
+
interface InitOptions {
|
|
436
|
+
defaults: boolean;
|
|
437
|
+
quiet: boolean;
|
|
438
|
+
name?: string;
|
|
439
|
+
specDir?: string;
|
|
440
|
+
targets?: string[];
|
|
441
|
+
withApi?: boolean;
|
|
442
|
+
backendPath?: string;
|
|
443
|
+
configureTargets?: boolean;
|
|
444
|
+
}
|
|
404
445
|
|
|
405
|
-
|
|
406
|
-
|
|
446
|
+
interface InitAnswers {
|
|
447
|
+
name: string;
|
|
448
|
+
specDir: string;
|
|
449
|
+
targets: string[];
|
|
450
|
+
withApi: boolean;
|
|
451
|
+
backendPath: string | null;
|
|
452
|
+
configureTargets: boolean;
|
|
453
|
+
}
|
|
407
454
|
|
|
408
|
-
|
|
455
|
+
function parseTargetsValue(raw: string): string[] {
|
|
456
|
+
return raw
|
|
457
|
+
.split(",")
|
|
458
|
+
.map((value) => value.trim().toLowerCase())
|
|
459
|
+
.filter((value): value is SupportedTarget => isSupportedTarget(value));
|
|
460
|
+
}
|
|
409
461
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
);
|
|
462
|
+
function requireFlagValue(argv: string[], index: number, flag: string): string {
|
|
463
|
+
const value = argv[index + 1];
|
|
464
|
+
if (!value || value.startsWith("--")) {
|
|
465
|
+
console.error(`Error: ${flag} requires a value.`);
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
return value;
|
|
469
|
+
}
|
|
427
470
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
471
|
+
function parseInitArgs(argv: string[]): InitOptions {
|
|
472
|
+
const options: InitOptions = { defaults: argv.includes("--defaults"), quiet: argv.includes("--quiet") };
|
|
473
|
+
|
|
474
|
+
for (let index = 0; index < argv.length; index++) {
|
|
475
|
+
const arg = argv[index];
|
|
476
|
+
switch (arg) {
|
|
477
|
+
case "--defaults":
|
|
478
|
+
case "--quiet":
|
|
479
|
+
break;
|
|
480
|
+
case "--name":
|
|
481
|
+
options.name = requireFlagValue(argv, index, arg);
|
|
482
|
+
index++;
|
|
483
|
+
break;
|
|
484
|
+
case "--spec-dir":
|
|
485
|
+
options.specDir = requireFlagValue(argv, index, arg);
|
|
486
|
+
index++;
|
|
487
|
+
break;
|
|
488
|
+
case "--targets":
|
|
489
|
+
options.targets = parseTargetsValue(requireFlagValue(argv, index, arg));
|
|
490
|
+
index++;
|
|
491
|
+
break;
|
|
492
|
+
case "--backend":
|
|
493
|
+
options.backendPath = requireFlagValue(argv, index, arg);
|
|
494
|
+
index++;
|
|
495
|
+
break;
|
|
496
|
+
case "--with-api":
|
|
497
|
+
options.withApi = true;
|
|
498
|
+
break;
|
|
499
|
+
case "--no-api":
|
|
500
|
+
options.withApi = false;
|
|
501
|
+
break;
|
|
502
|
+
case "--configure-targets":
|
|
503
|
+
options.configureTargets = true;
|
|
504
|
+
break;
|
|
505
|
+
case "--no-configure-targets":
|
|
506
|
+
options.configureTargets = false;
|
|
507
|
+
break;
|
|
508
|
+
default:
|
|
509
|
+
if (arg.startsWith("--")) {
|
|
510
|
+
console.error(`Error: Unknown init option: ${arg}`);
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
431
513
|
}
|
|
514
|
+
}
|
|
432
515
|
|
|
433
|
-
|
|
516
|
+
return options;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function collectDefaults(): InitAnswers {
|
|
520
|
+
const cwd = process.cwd();
|
|
521
|
+
const defaultName = cwd.split("/").pop() || "MyApp";
|
|
522
|
+
return {
|
|
523
|
+
name: defaultName,
|
|
524
|
+
specDir: "openuispec",
|
|
525
|
+
targets: [...SUPPORTED_TARGETS],
|
|
526
|
+
withApi: true,
|
|
527
|
+
backendPath: "../backend/",
|
|
528
|
+
configureTargets: true,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function collectInteractiveAnswers(rl: ReturnType<typeof createInterface>): Promise<InitAnswers> {
|
|
533
|
+
const defaults = collectDefaults();
|
|
534
|
+
const name = await ask(rl, "Project name", defaults.name);
|
|
535
|
+
const specDir = await ask(rl, "Spec directory", defaults.specDir);
|
|
536
|
+
const targets = await askList(rl, "\nWhich platforms?", [...SUPPORTED_TARGETS], defaults.targets);
|
|
537
|
+
|
|
538
|
+
if (targets.length === 0) {
|
|
539
|
+
console.error("At least one target is required.");
|
|
540
|
+
process.exit(1);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const withApi = await askYesNo(rl, "Will this spec declare API endpoints?", defaults.withApi);
|
|
544
|
+
const backendPath = withApi
|
|
545
|
+
? await ask(rl, "Backend folder path relative to openuispec.yaml", defaults.backendPath ?? "../backend/")
|
|
546
|
+
: null;
|
|
547
|
+
const configureTargets = await askYesNo(rl, "Configure target stacks now?", defaults.configureTargets);
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
name,
|
|
551
|
+
specDir,
|
|
552
|
+
targets,
|
|
553
|
+
withApi,
|
|
554
|
+
backendPath,
|
|
555
|
+
configureTargets,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function collectNonInteractiveAnswers(argv: string[]): InitAnswers {
|
|
560
|
+
const parsed = parseInitArgs(argv);
|
|
561
|
+
const defaults = collectDefaults();
|
|
562
|
+
|
|
563
|
+
if (!parsed.defaults && argv.filter((a) => a !== "--quiet").length === 0) {
|
|
564
|
+
console.error(
|
|
565
|
+
"Error: `openuispec init` needs a TTY for prompts.\n" +
|
|
566
|
+
"Run with `--defaults` or pass flags such as `--name`, `--targets`, `--with-api`, `--backend`, and `--configure-targets`."
|
|
567
|
+
);
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const targets = parsed.targets && parsed.targets.length > 0 ? parsed.targets : defaults.targets;
|
|
572
|
+
if (targets.length === 0) {
|
|
573
|
+
console.error("Error: --targets must include at least one of ios, android, web.");
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const withApi = parsed.withApi ?? defaults.withApi;
|
|
578
|
+
const backendPath = withApi ? parsed.backendPath ?? defaults.backendPath : null;
|
|
579
|
+
|
|
580
|
+
return {
|
|
581
|
+
name: parsed.name ?? defaults.name,
|
|
582
|
+
specDir: parsed.specDir ?? defaults.specDir,
|
|
583
|
+
targets,
|
|
584
|
+
withApi,
|
|
585
|
+
backendPath,
|
|
586
|
+
configureTargets: parsed.configureTargets ?? defaults.configureTargets,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ── main ─────────────────────────────────────────────────────────────
|
|
591
|
+
|
|
592
|
+
export async function init(argv: string[] = []): Promise<void> {
|
|
593
|
+
const quiet = argv.includes("--quiet");
|
|
594
|
+
const interactive = stdin.isTTY && stdout.isTTY && !argv.includes("--defaults");
|
|
595
|
+
const rl = interactive ? createInterface({ input: stdin, output: stdout }) : null;
|
|
596
|
+
|
|
597
|
+
if (!quiet) console.log("\nOpenUISpec — Project Setup\n");
|
|
598
|
+
|
|
599
|
+
try {
|
|
600
|
+
const cwd = process.cwd();
|
|
601
|
+
const answers = rl ? await collectInteractiveAnswers(rl) : collectNonInteractiveAnswers(argv);
|
|
602
|
+
rl?.close();
|
|
434
603
|
|
|
435
604
|
// ── create folders ─────────────────────────────────────────────
|
|
436
605
|
|
|
437
|
-
console.log("\nScaffolding...\n");
|
|
606
|
+
if (!quiet) console.log("\nScaffolding...\n");
|
|
438
607
|
|
|
439
|
-
const root = join(cwd, specDir);
|
|
608
|
+
const root = join(cwd, answers.specDir);
|
|
440
609
|
const dirs = [
|
|
441
610
|
"tokens",
|
|
442
611
|
"contracts",
|
|
@@ -455,14 +624,19 @@ export async function init(): Promise<void> {
|
|
|
455
624
|
|
|
456
625
|
writeIfMissing(
|
|
457
626
|
join(root, "openuispec.yaml"),
|
|
458
|
-
manifestTemplate(name, targets,
|
|
627
|
+
manifestTemplate(answers.name, answers.targets, {
|
|
628
|
+
withApi: answers.withApi,
|
|
629
|
+
backendPath: answers.backendPath,
|
|
630
|
+
}),
|
|
631
|
+
quiet
|
|
459
632
|
);
|
|
460
633
|
|
|
461
634
|
// ── spec README ──────────────────────────────────────────────
|
|
462
635
|
|
|
463
636
|
writeIfMissing(
|
|
464
637
|
join(root, "README.md"),
|
|
465
|
-
specReadmeTemplate(name, targets)
|
|
638
|
+
specReadmeTemplate(answers.name, answers.targets),
|
|
639
|
+
quiet
|
|
466
640
|
);
|
|
467
641
|
|
|
468
642
|
// ── .gitkeep for empty dirs ────────────────────────────────────
|
|
@@ -476,65 +650,78 @@ export async function init(): Promise<void> {
|
|
|
476
650
|
const gk = join(dir, ".gitkeep");
|
|
477
651
|
if (!existsSync(gk)) {
|
|
478
652
|
writeFileSync(gk, "");
|
|
479
|
-
console.log(` create ${relative(cwd, gk)}`);
|
|
653
|
+
if (!quiet) console.log(` create ${relative(cwd, gk)}`);
|
|
480
654
|
}
|
|
481
655
|
}
|
|
482
656
|
}
|
|
483
657
|
|
|
484
658
|
// ── AI assistant rules ─────────────────────────────────────────
|
|
485
659
|
|
|
486
|
-
const rules = aiRulesBlock(specDir, targets);
|
|
660
|
+
const rules = aiRulesBlock(answers.specDir, answers.targets);
|
|
487
661
|
|
|
488
662
|
for (const file of ["CLAUDE.md", "AGENTS.md"]) {
|
|
489
663
|
const filePath = join(cwd, file);
|
|
490
664
|
if (existsSync(filePath)) {
|
|
491
665
|
const existing = readFileSync(filePath, "utf-8");
|
|
492
666
|
if (existing.includes("OpenUISpec")) {
|
|
493
|
-
console.log(` skip ${file} (already has OpenUISpec rules)`);
|
|
667
|
+
if (!quiet) console.log(` skip ${file} (already has OpenUISpec rules)`);
|
|
494
668
|
continue;
|
|
495
669
|
}
|
|
496
670
|
appendFileSync(filePath, "\n" + rules);
|
|
497
|
-
console.log(` update ${file} (appended rules)`);
|
|
671
|
+
if (!quiet) console.log(` update ${file} (appended rules)`);
|
|
498
672
|
} else {
|
|
499
673
|
writeFileSync(filePath, rules.trimStart());
|
|
500
|
-
console.log(` create ${file}`);
|
|
674
|
+
if (!quiet) console.log(` create ${file}`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (answers.configureTargets) {
|
|
679
|
+
if (!quiet) console.log("\nConfiguring target stacks...\n");
|
|
680
|
+
const { runConfigureTarget } = await import("./configure-target.js");
|
|
681
|
+
for (const target of answers.targets) {
|
|
682
|
+
await runConfigureTarget([target, ...(interactive ? [] : ["--defaults"]), ...(quiet ? ["--silent"] : [])]);
|
|
501
683
|
}
|
|
502
684
|
}
|
|
503
685
|
|
|
504
686
|
// ── done ───────────────────────────────────────────────────────
|
|
505
687
|
|
|
506
|
-
|
|
507
|
-
|
|
688
|
+
if (quiet) {
|
|
689
|
+
console.log(`./${answers.specDir}/`);
|
|
690
|
+
} else {
|
|
691
|
+
console.log(`
|
|
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 after that target output directory exists
|
|
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 stays unconfirmed
|
|
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
|
|
715
|
+
openuispec prepare --target ios Build the target work bundle
|
|
530
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
|
`);
|
|
722
|
+
}
|
|
536
723
|
} catch (err) {
|
|
537
|
-
rl
|
|
724
|
+
rl?.close();
|
|
538
725
|
throw err;
|
|
539
726
|
}
|
|
540
727
|
}
|