openuispec 0.2.19 → 0.2.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/check/audit.js +392 -0
  2. package/dist/check/index.js +216 -0
  3. package/dist/cli/configure-target.js +391 -0
  4. package/dist/cli/index.js +510 -0
  5. package/dist/cli/init.js +1047 -0
  6. package/dist/drift/index.js +903 -0
  7. package/dist/mcp-server/index.js +886 -0
  8. package/dist/mcp-server/preview-render.js +1761 -0
  9. package/dist/mcp-server/preview.js +233 -0
  10. package/dist/mcp-server/screenshot-android.js +458 -0
  11. package/dist/mcp-server/screenshot-ios.js +639 -0
  12. package/dist/mcp-server/screenshot-shared.js +180 -0
  13. package/dist/mcp-server/screenshot.js +459 -0
  14. package/dist/prepare/index.js +1216 -0
  15. package/dist/runtime/package-paths.js +33 -0
  16. package/dist/schema/semantic-lint.js +564 -0
  17. package/dist/schema/validate.js +689 -0
  18. package/dist/status/index.js +194 -0
  19. package/package.json +12 -13
  20. package/check/audit.ts +0 -426
  21. package/check/index.ts +0 -320
  22. package/cli/configure-target.ts +0 -523
  23. package/cli/index.ts +0 -537
  24. package/cli/init.ts +0 -1253
  25. package/drift/index.ts +0 -1165
  26. package/mcp-server/index.ts +0 -1041
  27. package/mcp-server/preview-render.ts +0 -1922
  28. package/mcp-server/preview.ts +0 -292
  29. package/mcp-server/screenshot-android.ts +0 -621
  30. package/mcp-server/screenshot-ios.ts +0 -753
  31. package/mcp-server/screenshot-shared.ts +0 -237
  32. package/mcp-server/screenshot.ts +0 -563
  33. package/prepare/index.ts +0 -1530
  34. package/schema/semantic-lint.ts +0 -692
  35. package/schema/validate.ts +0 -870
  36. package/scripts/regenerate-previews.ts +0 -136
  37. package/scripts/take-all-screenshots.ts +0 -507
  38. package/status/index.ts +0 -275
@@ -0,0 +1,391 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { stdin, stdout } from "node:process";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { join, relative, resolve } from "node:path";
5
+ import YAML from "yaml";
6
+ import { findProjectDir, isSupportedTarget, readManifest } from "../drift/index.js";
7
+ import { resolvePackagePath } from "../runtime/package-paths.js";
8
+ import { ask, askChoice } from "./init.js";
9
+ function readWizardPresets() {
10
+ const presetsPath = resolvePackagePath(import.meta.url, "cli", "target-presets.json");
11
+ return JSON.parse(readFileSync(presetsPath, "utf-8"));
12
+ }
13
+ const TARGET_WIZARDS = readWizardPresets();
14
+ function listFrameworkOptions(wizard) {
15
+ return [...new Set([...(wizard.framework_options ?? [wizard.framework]), "other"])];
16
+ }
17
+ export function listTargetWizardOptions(target) {
18
+ const wizard = TARGET_WIZARDS[target];
19
+ return {
20
+ target,
21
+ defaults_are_unconfirmed: true,
22
+ confirmation_required_before_implementation: true,
23
+ interactive_command: `openuispec configure-target ${target}`,
24
+ defaults_command: `openuispec configure-target ${target} --defaults`,
25
+ framework: {
26
+ prompt: wizard.framework_prompt ?? `${target} framework`,
27
+ recommended: wizard.framework,
28
+ options: listFrameworkOptions(wizard),
29
+ custom_allowed: true,
30
+ },
31
+ questions: wizard.questions.map((question) => ({
32
+ key: question.key,
33
+ prompt: question.prompt,
34
+ recommended: question.recommended,
35
+ custom_allowed: true,
36
+ options: question.options,
37
+ })),
38
+ };
39
+ }
40
+ function filterOptionsForFramework(question, framework) {
41
+ return question.options.filter((option) => {
42
+ if (!option.framework_filter)
43
+ return true;
44
+ return option.framework_filter.includes(framework);
45
+ });
46
+ }
47
+ function effectiveDefault(question, framework, inferred) {
48
+ if (inferred)
49
+ return inferred;
50
+ const filtered = filterOptionsForFramework(question, framework);
51
+ if (filtered.some((o) => o.value === question.recommended))
52
+ return question.recommended;
53
+ return filtered[0]?.value ?? question.recommended;
54
+ }
55
+ function mergeDependencies(derived, existing, managedDependencies) {
56
+ const extras = Array.isArray(existing)
57
+ ? existing.filter((dep) => typeof dep === "string" && !managedDependencies.has(dep))
58
+ : [];
59
+ return Array.from(new Set([...derived, ...extras]));
60
+ }
61
+ function findOption(question, value) {
62
+ return question.options.find((option) => option.value === value) ?? null;
63
+ }
64
+ function collectManagedDependencies(wizard) {
65
+ const managed = new Set(wizard.base_dependencies ?? []);
66
+ if (wizard.framework_dependencies) {
67
+ for (const deps of Object.values(wizard.framework_dependencies)) {
68
+ for (const dep of deps)
69
+ managed.add(dep);
70
+ }
71
+ }
72
+ for (const question of wizard.questions) {
73
+ for (const option of question.options) {
74
+ for (const dependency of option.dependencies ?? []) {
75
+ managed.add(dependency);
76
+ }
77
+ }
78
+ }
79
+ return managed;
80
+ }
81
+ function normalizeAndroid(existingPlatform) {
82
+ const generation = existingPlatform.generation ?? {};
83
+ const deps = new Set(Array.isArray(generation.dependencies) ? generation.dependencies : []);
84
+ const architectureValue = typeof generation.architecture === "string" ? generation.architecture.toLowerCase() : "";
85
+ const stateValue = typeof generation.state === "string" ? generation.state.toLowerCase() : "";
86
+ const persistenceValue = typeof generation.persistence === "string" ? generation.persistence.toLowerCase() : "";
87
+ return {
88
+ architecture: typeof generation.architecture === "string" &&
89
+ !architectureValue.includes("decompose") &&
90
+ !architectureValue.includes("compose") &&
91
+ !deps.has("decompose") &&
92
+ !deps.has("navigation-compose")
93
+ ? generation.architecture
94
+ : architectureValue.includes("decompose") || deps.has("decompose")
95
+ ? "decompose"
96
+ : architectureValue.includes("compose") || deps.has("navigation-compose")
97
+ ? "plain_compose"
98
+ : "decompose",
99
+ state: typeof generation.state === "string" &&
100
+ !stateValue.includes("mvikotlin") &&
101
+ !stateValue.includes("viewmodel") &&
102
+ !deps.has("mvikotlin") &&
103
+ !deps.has("lifecycle-viewmodel-compose")
104
+ ? generation.state
105
+ : stateValue.includes("mvikotlin") || deps.has("mvikotlin")
106
+ ? "mvikotlin"
107
+ : stateValue.includes("viewmodel") || deps.has("lifecycle-viewmodel-compose")
108
+ ? "viewmodel"
109
+ : "mvikotlin",
110
+ preferences: typeof generation.preferences === "string"
111
+ ? generation.preferences
112
+ : persistenceValue === "datastore" || deps.has("datastore-preferences")
113
+ ? "datastore"
114
+ : "datastore",
115
+ database: typeof generation.database === "string"
116
+ ? generation.database
117
+ : persistenceValue === "sqldelight" || deps.has("sqldelight")
118
+ ? "sqldelight"
119
+ : persistenceValue === "room" || deps.has("room-runtime")
120
+ ? "room"
121
+ : "none",
122
+ di: typeof generation.di === "string"
123
+ ? ["metro", "koin", "hilt", "none"].includes(generation.di)
124
+ ? generation.di
125
+ : generation.di
126
+ : deps.has("metro")
127
+ ? "metro"
128
+ : deps.has("koin-android")
129
+ ? "koin"
130
+ : deps.has("hilt-android")
131
+ ? "hilt"
132
+ : "metro",
133
+ };
134
+ }
135
+ function normalizeWeb(existingPlatform) {
136
+ const generation = existingPlatform.generation ?? {};
137
+ const result = {
138
+ runtime: typeof generation.runtime === "string"
139
+ ? generation.runtime
140
+ : "frontend_only",
141
+ css: typeof generation.css === "string"
142
+ ? generation.css
143
+ : "tailwind",
144
+ storage_backend: typeof generation.storage_backend === "string"
145
+ ? generation.storage_backend
146
+ : "none",
147
+ };
148
+ if (typeof generation.routing === "string") {
149
+ result.routing = generation.routing.includes("tanstack")
150
+ ? "tanstack_router"
151
+ : generation.routing.includes("react-router") || generation.routing === "react_router"
152
+ ? "react_router"
153
+ : generation.routing;
154
+ }
155
+ if (typeof generation.state === "string") {
156
+ result.state = generation.state === "redux-toolkit"
157
+ ? "redux"
158
+ : generation.state === "tanstack-query"
159
+ ? "query_only"
160
+ : generation.state;
161
+ }
162
+ return result;
163
+ }
164
+ function normalizeIos(existingPlatform) {
165
+ const generation = existingPlatform.generation ?? {};
166
+ const deps = new Set(Array.isArray(generation.dependencies) ? generation.dependencies : []);
167
+ return {
168
+ architecture: typeof generation.architecture === "string" &&
169
+ !generation.architecture.toLowerCase().includes("tca") &&
170
+ generation.architecture.toLowerCase() !== "native swiftui"
171
+ ? generation.architecture
172
+ : typeof generation.architecture === "string" && generation.architecture.toLowerCase().includes("tca")
173
+ ? "tca_style"
174
+ : deps.has("swift-composable-architecture")
175
+ ? "tca_style"
176
+ : "native",
177
+ persistence: typeof generation.persistence === "string"
178
+ ? generation.persistence
179
+ : deps.has("sqlite")
180
+ ? "sqlite"
181
+ : deps.has("swiftdata")
182
+ ? "swiftdata"
183
+ : "swiftdata",
184
+ di: typeof generation.di === "string"
185
+ ? generation.di
186
+ : deps.has("factory")
187
+ ? "factory"
188
+ : deps.has("custom-di")
189
+ ? "custom"
190
+ : "none",
191
+ };
192
+ }
193
+ function normalizeExisting(target, existingPlatform) {
194
+ switch (target) {
195
+ case "android":
196
+ return normalizeAndroid(existingPlatform);
197
+ case "web":
198
+ return normalizeWeb(existingPlatform);
199
+ case "ios":
200
+ return normalizeIos(existingPlatform);
201
+ }
202
+ }
203
+ function buildGeneration(wizard, answers, existingGeneration, framework) {
204
+ const generation = {
205
+ ...(wizard.generation_defaults ?? {}),
206
+ ...existingGeneration,
207
+ };
208
+ const managedDependencies = collectManagedDependencies(wizard);
209
+ const frameworkDeps = wizard.framework_dependencies?.[framework] ?? [];
210
+ const derivedDependencies = [...(wizard.base_dependencies ?? []), ...frameworkDeps];
211
+ for (const question of wizard.questions) {
212
+ const answer = answers[question.key];
213
+ const selected = answer ? findOption(question, answer) : null;
214
+ if (!selected) {
215
+ generation[question.key] = answer || question.recommended;
216
+ continue;
217
+ }
218
+ generation[question.key] = selected.generation_value ?? selected.value;
219
+ Object.assign(generation, selected.extra_generation ?? {});
220
+ derivedDependencies.push(...(selected.dependencies ?? []));
221
+ }
222
+ generation.dependencies = mergeDependencies(derivedDependencies, existingGeneration.dependencies, managedDependencies);
223
+ return generation;
224
+ }
225
+ function stackConfirmation(useDefaults) {
226
+ const now = new Date().toISOString();
227
+ return useDefaults
228
+ ? {
229
+ status: "pending_user_confirmation",
230
+ source: "defaults",
231
+ updated_at: now,
232
+ }
233
+ : {
234
+ status: "confirmed",
235
+ source: "user",
236
+ confirmed_at: now,
237
+ };
238
+ }
239
+ function parseTarget(argv) {
240
+ const direct = argv[0];
241
+ if (direct && isSupportedTarget(direct)) {
242
+ return direct;
243
+ }
244
+ const targetIdx = argv.indexOf("--target");
245
+ const candidate = targetIdx !== -1 ? argv[targetIdx + 1] : null;
246
+ if (candidate && isSupportedTarget(candidate)) {
247
+ return candidate;
248
+ }
249
+ return null;
250
+ }
251
+ function parseSetPairs(argv) {
252
+ const pairs = {};
253
+ for (let i = 0; i < argv.length; i++) {
254
+ if (argv[i] === "--set" && argv[i + 1]) {
255
+ const raw = argv[i + 1];
256
+ const eqIdx = raw.indexOf("=");
257
+ if (eqIdx > 0) {
258
+ pairs[raw.slice(0, eqIdx)] = raw.slice(eqIdx + 1);
259
+ }
260
+ i++;
261
+ }
262
+ }
263
+ return pairs;
264
+ }
265
+ export async function runConfigureTarget(argv) {
266
+ const target = parseTarget(argv);
267
+ const listOptions = argv.includes("--list-options");
268
+ const useDefaults = argv.includes("--defaults");
269
+ const quiet = argv.includes("--quiet");
270
+ const setPairs = parseSetPairs(argv);
271
+ const hasSetPairs = Object.keys(setPairs).length > 0;
272
+ const interactive = stdin.isTTY && stdout.isTTY && !useDefaults && !hasSetPairs;
273
+ if (!target) {
274
+ console.error("Error: target is required for configure-target");
275
+ console.error("Usage: openuispec configure-target <ios|android|web> [--defaults] [--list-options] [--set key=value]");
276
+ process.exit(1);
277
+ }
278
+ if (listOptions) {
279
+ console.log(JSON.stringify(listTargetWizardOptions(target), null, 2));
280
+ return;
281
+ }
282
+ if (!interactive && !useDefaults && !hasSetPairs) {
283
+ console.error("Error: `openuispec configure-target` needs a TTY for prompts.\n" +
284
+ "Preferred: ask the user to confirm the target stack, then run `openuispec configure-target <target>` in an interactive terminal.\n" +
285
+ "Fallback: run with `--defaults` only for unattended setup; those values remain unconfirmed and `prepare` will block implementation until the user confirms them.");
286
+ process.exit(1);
287
+ }
288
+ const projectDir = findProjectDir(process.cwd());
289
+ const manifest = readManifest(projectDir);
290
+ const configuredTargets = manifest.generation?.targets ?? [];
291
+ if (configuredTargets.length > 0 && !configuredTargets.includes(target)) {
292
+ console.error(`Error: target "${target}" is not listed in generation.targets.\n` +
293
+ `Configured targets: ${configuredTargets.join(", ")}`);
294
+ process.exit(1);
295
+ }
296
+ const platformDir = resolve(projectDir, manifest.includes?.platform ?? "./platform/");
297
+ mkdirSync(platformDir, { recursive: true });
298
+ const platformPath = join(platformDir, `${target}.yaml`);
299
+ const existingDoc = existsSync(platformPath)
300
+ ? YAML.parse(readFileSync(platformPath, "utf-8"))
301
+ : {};
302
+ const existingPlatform = existingDoc[target] ?? {};
303
+ const existingGeneration = existingPlatform.generation ?? {};
304
+ const wizard = TARGET_WIZARDS[target];
305
+ const defaultFramework = typeof existingPlatform.framework === "string" && existingPlatform.framework.trim().length > 0
306
+ ? existingPlatform.framework
307
+ : wizard.framework;
308
+ const inferredDefaults = {
309
+ ...normalizeExisting(target, existingPlatform),
310
+ };
311
+ let framework = defaultFramework;
312
+ function computeDefaultAnswers(fw) {
313
+ return Object.fromEntries(wizard.questions.map((question) => {
314
+ const defaultValue = effectiveDefault(question, fw, inferredDefaults[question.key]);
315
+ return [question.key, defaultValue];
316
+ }));
317
+ }
318
+ let answers = computeDefaultAnswers(framework);
319
+ if (hasSetPairs) {
320
+ // Non-interactive --set path: merge provided values with existing/defaults
321
+ const knownKeys = new Set(wizard.questions.map((q) => q.key));
322
+ for (const [key, value] of Object.entries(setPairs)) {
323
+ if (!knownKeys.has(key)) {
324
+ console.error(`Warning: "${key}" does not match any wizard question; setting as custom value.`);
325
+ }
326
+ answers[key] = value;
327
+ }
328
+ }
329
+ else if (interactive) {
330
+ const rl = createInterface({ input: stdin, output: stdout });
331
+ try {
332
+ console.log(`\nOpenUISpec — Configure ${target}\n`);
333
+ console.log(`Writing target stack choices to ${relative(process.cwd(), platformPath)}\n`);
334
+ const frameworkOptions = [
335
+ ...(wizard.framework_options ?? [wizard.framework]),
336
+ "other",
337
+ ];
338
+ const chosenFramework = await askChoice(rl, wizard.framework_prompt ?? `${target} framework`, frameworkOptions, framework);
339
+ framework =
340
+ chosenFramework === "other"
341
+ ? await ask(rl, `Custom ${target} framework`, framework)
342
+ : chosenFramework;
343
+ const defaultAnswers = computeDefaultAnswers(framework);
344
+ answers = {};
345
+ for (const question of wizard.questions) {
346
+ const filtered = filterOptionsForFramework(question, framework);
347
+ const chosen = await askChoice(rl, question.prompt, [...filtered.map((option) => option.value), "other"], defaultAnswers[question.key]);
348
+ answers[question.key] =
349
+ chosen === "other"
350
+ ? await ask(rl, `Custom value for ${question.key}`, defaultAnswers[question.key])
351
+ : chosen;
352
+ }
353
+ }
354
+ finally {
355
+ rl.close();
356
+ }
357
+ }
358
+ // --set implies confirmed (user explicitly chose values); --defaults without --set is pending
359
+ const updatedPlatform = {
360
+ ...existingPlatform,
361
+ framework,
362
+ generation: {
363
+ ...buildGeneration(wizard, answers, existingGeneration, framework),
364
+ stack_confirmation: stackConfirmation(useDefaults && !hasSetPairs),
365
+ },
366
+ };
367
+ if (wizard.language)
368
+ updatedPlatform.language = wizard.language;
369
+ if (wizard.min_version)
370
+ updatedPlatform.min_version = existingPlatform.min_version ?? wizard.min_version;
371
+ if (typeof wizard.min_sdk === "number")
372
+ updatedPlatform.min_sdk = existingPlatform.min_sdk ?? wizard.min_sdk;
373
+ if (typeof wizard.target_sdk === "number") {
374
+ updatedPlatform.target_sdk = existingPlatform.target_sdk ?? wizard.target_sdk;
375
+ }
376
+ writeFileSync(platformPath, YAML.stringify({ [target]: updatedPlatform }));
377
+ const savedPath = relative(process.cwd(), platformPath);
378
+ if (argv.includes("--silent")) {
379
+ // Called as subroutine (e.g. from init --quiet) — no output at all
380
+ }
381
+ else if (quiet) {
382
+ console.log(savedPath);
383
+ }
384
+ else {
385
+ console.log(`\nSaved ${savedPath}`);
386
+ console.log("Configured values:");
387
+ for (const [key, value] of Object.entries(answers)) {
388
+ console.log(` - ${key}: ${value}`);
389
+ }
390
+ }
391
+ }