openuispec 0.1.29 → 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 +3 -1
- package/cli/configure-target.ts +124 -17
- package/cli/index.ts +23 -6
- package/cli/init.ts +37 -36
- 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,15 @@
|
|
|
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
|
|
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
|
|
9
10
|
* openuispec drift [--target <t>] Check for spec drift
|
|
10
11
|
* openuispec drift --snapshot --target <t> Snapshot current state + git baseline
|
|
11
12
|
* openuispec drift --target <t> --explain Explain semantic changes since baseline
|
|
12
13
|
* openuispec prepare --target <t> Build the target work bundle
|
|
13
14
|
* openuispec status Show cross-target baseline/drift status
|
|
15
|
+
* openuispec check --target <t> [--json] Composite validation + prepare readiness
|
|
14
16
|
* openuispec validate [group...] Validate spec files against schemas
|
|
15
17
|
*/
|
|
16
18
|
|
|
@@ -78,6 +80,12 @@ async function main(): Promise<void> {
|
|
|
78
80
|
break;
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
case "check": {
|
|
84
|
+
const { runCheck } = await import("../check/index.js");
|
|
85
|
+
runCheck(rest);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
|
|
81
89
|
case "validate": {
|
|
82
90
|
const { runValidate } = await import("../schema/validate.js");
|
|
83
91
|
runValidate(rest);
|
|
@@ -93,19 +101,25 @@ OpenUISpec CLI v0.1
|
|
|
93
101
|
|
|
94
102
|
Usage:
|
|
95
103
|
openuispec init Create a new spec project
|
|
96
|
-
openuispec init --defaults Scaffold non-interactively with defaults
|
|
104
|
+
openuispec init --defaults Scaffold non-interactively with unconfirmed defaults
|
|
97
105
|
openuispec init --no-configure-targets Skip target stack setup during init
|
|
98
106
|
openuispec update-rules Update AI rules to match installed version
|
|
99
|
-
openuispec configure-target <t> [--defaults] Configure target stack
|
|
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
|
|
100
110
|
openuispec drift [--target <t>] Check for spec drift
|
|
101
111
|
openuispec drift --snapshot --target <t> Snapshot current state + git baseline
|
|
102
112
|
openuispec drift --target <t> --explain Explain semantic changes since baseline
|
|
103
113
|
openuispec prepare --target <t> Build the target work bundle
|
|
104
114
|
openuispec status Show cross-target baseline/drift status
|
|
105
|
-
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
|
|
106
118
|
|
|
107
119
|
Validate groups: manifest, tokens, screens, flows, platform, locales, contracts, semantic
|
|
108
120
|
|
|
121
|
+
Exit codes: 0 = success, 1 = missing config/usage error, 2 = validation failure
|
|
122
|
+
|
|
109
123
|
Docs: https://openuispec.rsteam.uz
|
|
110
124
|
`);
|
|
111
125
|
break;
|
|
@@ -117,4 +131,7 @@ Docs: https://openuispec.rsteam.uz
|
|
|
117
131
|
}
|
|
118
132
|
}
|
|
119
133
|
|
|
120
|
-
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
|
@@ -17,6 +17,7 @@ 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";
|
|
20
21
|
|
|
21
22
|
// ── prompts ──────────────────────────────────────────────────────────
|
|
22
23
|
|
|
@@ -75,13 +76,13 @@ function ensureDir(path: string): void {
|
|
|
75
76
|
mkdirSync(path, { recursive: true });
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
function writeIfMissing(path: string, content: string): boolean {
|
|
79
|
+
function writeIfMissing(path: string, content: string, quiet = false): boolean {
|
|
79
80
|
if (existsSync(path)) {
|
|
80
|
-
console.log(` skip ${relative(process.cwd(), path)} (exists)`);
|
|
81
|
+
if (!quiet) console.log(` skip ${relative(process.cwd(), path)} (exists)`);
|
|
81
82
|
return false;
|
|
82
83
|
}
|
|
83
84
|
writeFileSync(path, content);
|
|
84
|
-
console.log(` create ${relative(process.cwd(), path)}`);
|
|
85
|
+
if (!quiet) console.log(` create ${relative(process.cwd(), path)}`);
|
|
85
86
|
return true;
|
|
86
87
|
}
|
|
87
88
|
|
|
@@ -202,7 +203,7 @@ Do NOT guess the file format — skipping this step will produce invalid YAML th
|
|
|
202
203
|
openuispec validate # Validate spec files against schemas
|
|
203
204
|
openuispec validate semantic # Run semantic cross-reference linting
|
|
204
205
|
openuispec validate screens # Validate only screens
|
|
205
|
-
openuispec configure-target ${targets[0]} [--defaults] # Configure target stack defaults
|
|
206
|
+
openuispec configure-target ${targets[0]} [--defaults] # Configure target stack; --defaults stays unconfirmed
|
|
206
207
|
openuispec status # Show cross-target baseline/drift status
|
|
207
208
|
openuispec drift --target ${targets[0]} --explain # Explain semantic spec drift
|
|
208
209
|
openuispec prepare --target ${targets[0]} # Build the target work bundle
|
|
@@ -213,6 +214,8 @@ The target work bundle has two modes:
|
|
|
213
214
|
- \`bootstrap\` when no snapshot exists yet, for first-time generation
|
|
214
215
|
- \`update\` after a snapshot exists, for drift-based target updates
|
|
215
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
|
+
|
|
216
219
|
## Learn more
|
|
217
220
|
|
|
218
221
|
Docs: https://openuispec.rsteam.uz
|
|
@@ -302,6 +305,7 @@ Spec-first workflow:
|
|
|
302
305
|
5. Run \`openuispec validate semantic\`.
|
|
303
306
|
6. Run \`openuispec drift --target <target> --explain\` to inspect semantic changes since that target's baseline.
|
|
304
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.
|
|
305
309
|
8. Verify the affected UI targets build/run if possible.
|
|
306
310
|
9. Only then run \`openuispec drift --snapshot --target <target>\` for affected targets, after that target output directory exists.
|
|
307
311
|
10. Run \`openuispec drift --target <target> --explain\` again to confirm no spec changes remain for that target.
|
|
@@ -322,6 +326,7 @@ Platform-first workflow:
|
|
|
322
326
|
- Do not treat \`openuispec drift\` as proof that generated UI matches the spec.
|
|
323
327
|
- Do not skip \`--explain\` / \`prepare\` when another platform needs to catch up with shared spec changes.
|
|
324
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.
|
|
325
330
|
|
|
326
331
|
## CLI commands
|
|
327
332
|
- \`openuispec init\` — scaffold a new spec project
|
|
@@ -330,7 +335,7 @@ Platform-first workflow:
|
|
|
330
335
|
- \`openuispec drift --target <t>\` — check for spec drift
|
|
331
336
|
- \`openuispec drift --target <t> --explain\` — explain semantic spec drift since the target baseline
|
|
332
337
|
- \`openuispec drift --snapshot --target <t>\` — snapshot current state after the target output exists
|
|
333
|
-
- \`openuispec prepare --target <t>\` — build the target work bundle
|
|
338
|
+
- \`openuispec prepare --target <t>\` — build the target work bundle and check whether stack confirmation is still pending
|
|
334
339
|
- \`openuispec status\` — show cross-target baseline/drift status
|
|
335
340
|
- \`openuispec update-rules\` — update AI rules to match installed package version
|
|
336
341
|
- \`openuispec drift --all\` — include stubs in drift check
|
|
@@ -354,7 +359,7 @@ export function updateRules(): void {
|
|
|
354
359
|
}
|
|
355
360
|
|
|
356
361
|
// Detect targets from manifest
|
|
357
|
-
let targets = [
|
|
362
|
+
let targets = [...SUPPORTED_TARGETS];
|
|
358
363
|
try {
|
|
359
364
|
const manifest = readFileSync(
|
|
360
365
|
join(cwd, specDir, "openuispec.yaml"),
|
|
@@ -429,6 +434,7 @@ export { getPackageVersion };
|
|
|
429
434
|
|
|
430
435
|
interface InitOptions {
|
|
431
436
|
defaults: boolean;
|
|
437
|
+
quiet: boolean;
|
|
432
438
|
name?: string;
|
|
433
439
|
specDir?: string;
|
|
434
440
|
targets?: string[];
|
|
@@ -447,11 +453,10 @@ interface InitAnswers {
|
|
|
447
453
|
}
|
|
448
454
|
|
|
449
455
|
function parseTargetsValue(raw: string): string[] {
|
|
450
|
-
const allowed = new Set(["ios", "android", "web"]);
|
|
451
456
|
return raw
|
|
452
457
|
.split(",")
|
|
453
458
|
.map((value) => value.trim().toLowerCase())
|
|
454
|
-
.filter((value): value is
|
|
459
|
+
.filter((value): value is SupportedTarget => isSupportedTarget(value));
|
|
455
460
|
}
|
|
456
461
|
|
|
457
462
|
function requireFlagValue(argv: string[], index: number, flag: string): string {
|
|
@@ -464,12 +469,13 @@ function requireFlagValue(argv: string[], index: number, flag: string): string {
|
|
|
464
469
|
}
|
|
465
470
|
|
|
466
471
|
function parseInitArgs(argv: string[]): InitOptions {
|
|
467
|
-
const options: InitOptions = { defaults: argv.includes("--defaults") };
|
|
472
|
+
const options: InitOptions = { defaults: argv.includes("--defaults"), quiet: argv.includes("--quiet") };
|
|
468
473
|
|
|
469
474
|
for (let index = 0; index < argv.length; index++) {
|
|
470
475
|
const arg = argv[index];
|
|
471
476
|
switch (arg) {
|
|
472
477
|
case "--defaults":
|
|
478
|
+
case "--quiet":
|
|
473
479
|
break;
|
|
474
480
|
case "--name":
|
|
475
481
|
options.name = requireFlagValue(argv, index, arg);
|
|
@@ -516,7 +522,7 @@ function collectDefaults(): InitAnswers {
|
|
|
516
522
|
return {
|
|
517
523
|
name: defaultName,
|
|
518
524
|
specDir: "openuispec",
|
|
519
|
-
targets: [
|
|
525
|
+
targets: [...SUPPORTED_TARGETS],
|
|
520
526
|
withApi: true,
|
|
521
527
|
backendPath: "../backend/",
|
|
522
528
|
configureTargets: true,
|
|
@@ -527,7 +533,7 @@ async function collectInteractiveAnswers(rl: ReturnType<typeof createInterface>)
|
|
|
527
533
|
const defaults = collectDefaults();
|
|
528
534
|
const name = await ask(rl, "Project name", defaults.name);
|
|
529
535
|
const specDir = await ask(rl, "Spec directory", defaults.specDir);
|
|
530
|
-
const targets = await askList(rl, "\nWhich platforms?", [
|
|
536
|
+
const targets = await askList(rl, "\nWhich platforms?", [...SUPPORTED_TARGETS], defaults.targets);
|
|
531
537
|
|
|
532
538
|
if (targets.length === 0) {
|
|
533
539
|
console.error("At least one target is required.");
|
|
@@ -554,7 +560,7 @@ function collectNonInteractiveAnswers(argv: string[]): InitAnswers {
|
|
|
554
560
|
const parsed = parseInitArgs(argv);
|
|
555
561
|
const defaults = collectDefaults();
|
|
556
562
|
|
|
557
|
-
if (!parsed.defaults && argv.length === 0) {
|
|
563
|
+
if (!parsed.defaults && argv.filter((a) => a !== "--quiet").length === 0) {
|
|
558
564
|
console.error(
|
|
559
565
|
"Error: `openuispec init` needs a TTY for prompts.\n" +
|
|
560
566
|
"Run with `--defaults` or pass flags such as `--name`, `--targets`, `--with-api`, `--backend`, and `--configure-targets`."
|
|
@@ -584,10 +590,11 @@ function collectNonInteractiveAnswers(argv: string[]): InitAnswers {
|
|
|
584
590
|
// ── main ─────────────────────────────────────────────────────────────
|
|
585
591
|
|
|
586
592
|
export async function init(argv: string[] = []): Promise<void> {
|
|
593
|
+
const quiet = argv.includes("--quiet");
|
|
587
594
|
const interactive = stdin.isTTY && stdout.isTTY && !argv.includes("--defaults");
|
|
588
595
|
const rl = interactive ? createInterface({ input: stdin, output: stdout }) : null;
|
|
589
596
|
|
|
590
|
-
console.log("\nOpenUISpec — Project Setup\n");
|
|
597
|
+
if (!quiet) console.log("\nOpenUISpec — Project Setup\n");
|
|
591
598
|
|
|
592
599
|
try {
|
|
593
600
|
const cwd = process.cwd();
|
|
@@ -596,7 +603,7 @@ export async function init(argv: string[] = []): Promise<void> {
|
|
|
596
603
|
|
|
597
604
|
// ── create folders ─────────────────────────────────────────────
|
|
598
605
|
|
|
599
|
-
console.log("\nScaffolding...\n");
|
|
606
|
+
if (!quiet) console.log("\nScaffolding...\n");
|
|
600
607
|
|
|
601
608
|
const root = join(cwd, answers.specDir);
|
|
602
609
|
const dirs = [
|
|
@@ -620,14 +627,16 @@ export async function init(argv: string[] = []): Promise<void> {
|
|
|
620
627
|
manifestTemplate(answers.name, answers.targets, {
|
|
621
628
|
withApi: answers.withApi,
|
|
622
629
|
backendPath: answers.backendPath,
|
|
623
|
-
})
|
|
630
|
+
}),
|
|
631
|
+
quiet
|
|
624
632
|
);
|
|
625
633
|
|
|
626
634
|
// ── spec README ──────────────────────────────────────────────
|
|
627
635
|
|
|
628
636
|
writeIfMissing(
|
|
629
637
|
join(root, "README.md"),
|
|
630
|
-
specReadmeTemplate(answers.name, answers.targets)
|
|
638
|
+
specReadmeTemplate(answers.name, answers.targets),
|
|
639
|
+
quiet
|
|
631
640
|
);
|
|
632
641
|
|
|
633
642
|
// ── .gitkeep for empty dirs ────────────────────────────────────
|
|
@@ -641,23 +650,11 @@ export async function init(argv: string[] = []): Promise<void> {
|
|
|
641
650
|
const gk = join(dir, ".gitkeep");
|
|
642
651
|
if (!existsSync(gk)) {
|
|
643
652
|
writeFileSync(gk, "");
|
|
644
|
-
console.log(` create ${relative(cwd, gk)}`);
|
|
653
|
+
if (!quiet) console.log(` create ${relative(cwd, gk)}`);
|
|
645
654
|
}
|
|
646
655
|
}
|
|
647
656
|
}
|
|
648
657
|
|
|
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
658
|
// ── AI assistant rules ─────────────────────────────────────────
|
|
662
659
|
|
|
663
660
|
const rules = aiRulesBlock(answers.specDir, answers.targets);
|
|
@@ -667,28 +664,31 @@ export async function init(argv: string[] = []): Promise<void> {
|
|
|
667
664
|
if (existsSync(filePath)) {
|
|
668
665
|
const existing = readFileSync(filePath, "utf-8");
|
|
669
666
|
if (existing.includes("OpenUISpec")) {
|
|
670
|
-
console.log(` skip ${file} (already has OpenUISpec rules)`);
|
|
667
|
+
if (!quiet) console.log(` skip ${file} (already has OpenUISpec rules)`);
|
|
671
668
|
continue;
|
|
672
669
|
}
|
|
673
670
|
appendFileSync(filePath, "\n" + rules);
|
|
674
|
-
console.log(` update ${file} (appended rules)`);
|
|
671
|
+
if (!quiet) console.log(` update ${file} (appended rules)`);
|
|
675
672
|
} else {
|
|
676
673
|
writeFileSync(filePath, rules.trimStart());
|
|
677
|
-
console.log(` create ${file}`);
|
|
674
|
+
if (!quiet) console.log(` create ${file}`);
|
|
678
675
|
}
|
|
679
676
|
}
|
|
680
677
|
|
|
681
678
|
if (answers.configureTargets) {
|
|
682
|
-
console.log("\nConfiguring target stacks...\n");
|
|
679
|
+
if (!quiet) console.log("\nConfiguring target stacks...\n");
|
|
683
680
|
const { runConfigureTarget } = await import("./configure-target.js");
|
|
684
681
|
for (const target of answers.targets) {
|
|
685
|
-
await runConfigureTarget([target, ...(interactive ? [] : ["--defaults"])]);
|
|
682
|
+
await runConfigureTarget([target, ...(interactive ? [] : ["--defaults"]), ...(quiet ? ["--silent"] : [])]);
|
|
686
683
|
}
|
|
687
684
|
}
|
|
688
685
|
|
|
689
686
|
// ── done ───────────────────────────────────────────────────────
|
|
690
687
|
|
|
691
|
-
|
|
688
|
+
if (quiet) {
|
|
689
|
+
console.log(`./${answers.specDir}/`);
|
|
690
|
+
} else {
|
|
691
|
+
console.log(`
|
|
692
692
|
Done! Your spec project is ready at ./${answers.specDir}/
|
|
693
693
|
|
|
694
694
|
Getting started (new project):
|
|
@@ -709,7 +709,7 @@ Getting started (existing project):
|
|
|
709
709
|
Commands:
|
|
710
710
|
openuispec validate Validate spec files
|
|
711
711
|
openuispec validate semantic Check semantic cross-references
|
|
712
|
-
openuispec configure-target ios [--defaults] Configure target stack defaults
|
|
712
|
+
openuispec configure-target ios [--defaults] Configure target stack; --defaults stays unconfirmed
|
|
713
713
|
openuispec status Show cross-target baseline/drift status
|
|
714
714
|
openuispec drift --target ios --explain Explain semantic spec changes
|
|
715
715
|
openuispec prepare --target ios Build the target work bundle
|
|
@@ -719,6 +719,7 @@ AI rules have been added to CLAUDE.md and AGENTS.md.
|
|
|
719
719
|
|
|
720
720
|
Docs: https://openuispec.rsteam.uz
|
|
721
721
|
`);
|
|
722
|
+
}
|
|
722
723
|
} catch (err) {
|
|
723
724
|
rl?.close();
|
|
724
725
|
throw err;
|