openuispec 0.2.18 → 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 (45) hide show
  1. package/README.md +2 -10
  2. package/dist/check/audit.js +392 -0
  3. package/dist/check/index.js +216 -0
  4. package/dist/cli/configure-target.js +391 -0
  5. package/dist/cli/index.js +510 -0
  6. package/dist/cli/init.js +1047 -0
  7. package/dist/drift/index.js +903 -0
  8. package/dist/mcp-server/index.js +886 -0
  9. package/dist/mcp-server/preview-render.js +1761 -0
  10. package/dist/mcp-server/preview.js +233 -0
  11. package/dist/mcp-server/screenshot-android.js +458 -0
  12. package/dist/mcp-server/screenshot-ios.js +639 -0
  13. package/dist/mcp-server/screenshot-shared.js +180 -0
  14. package/dist/mcp-server/screenshot.js +459 -0
  15. package/dist/prepare/index.js +1216 -0
  16. package/dist/runtime/package-paths.js +33 -0
  17. package/dist/schema/semantic-lint.js +564 -0
  18. package/dist/schema/validate.js +689 -0
  19. package/dist/status/index.js +194 -0
  20. package/docs/images/how-it-works.svg +56 -0
  21. package/docs/images/workflows.svg +76 -0
  22. package/package.json +12 -13
  23. package/check/audit.ts +0 -426
  24. package/check/index.ts +0 -320
  25. package/cli/configure-target.ts +0 -523
  26. package/cli/index.ts +0 -537
  27. package/cli/init.ts +0 -1253
  28. package/docs/images/how-it-works-dark.png +0 -0
  29. package/docs/images/how-it-works-light.png +0 -0
  30. package/docs/images/workflows-dark.png +0 -0
  31. package/docs/images/workflows-light.png +0 -0
  32. package/drift/index.ts +0 -1165
  33. package/mcp-server/index.ts +0 -1041
  34. package/mcp-server/preview-render.ts +0 -1922
  35. package/mcp-server/preview.ts +0 -292
  36. package/mcp-server/screenshot-android.ts +0 -621
  37. package/mcp-server/screenshot-ios.ts +0 -753
  38. package/mcp-server/screenshot-shared.ts +0 -237
  39. package/mcp-server/screenshot.ts +0 -563
  40. package/prepare/index.ts +0 -1530
  41. package/schema/semantic-lint.ts +0 -692
  42. package/schema/validate.ts +0 -870
  43. package/scripts/regenerate-previews.ts +0 -136
  44. package/scripts/take-all-screenshots.ts +0 -507
  45. package/status/index.ts +0 -275
@@ -1,523 +0,0 @@
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 { dirname, join, relative, resolve } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import YAML from "yaml";
7
- import { findProjectDir, isSupportedTarget, readManifest, type SupportedTarget } from "../drift/index.js";
8
- import { ask, askChoice } from "./init.js";
9
-
10
- type WizardOptionPreset = {
11
- value: string;
12
- generation_value?: string;
13
- framework_filter?: string[];
14
- dependencies?: string[];
15
- extra_generation?: Record<string, any>;
16
- refs?: {
17
- plugins?: string[];
18
- libraries?: string[];
19
- packages?: string[];
20
- docs?: string[];
21
- };
22
- };
23
-
24
- type ChoiceQuestionPreset = {
25
- key: string;
26
- prompt: string;
27
- recommended: string;
28
- options: WizardOptionPreset[];
29
- };
30
-
31
- type TargetWizardPreset = {
32
- framework: string;
33
- framework_prompt?: string;
34
- framework_options?: string[];
35
- language?: string;
36
- min_version?: string;
37
- min_sdk?: number;
38
- target_sdk?: number;
39
- generation_defaults?: Record<string, any>;
40
- base_dependencies?: string[];
41
- framework_dependencies?: Record<string, string[]>;
42
- questions: ChoiceQuestionPreset[];
43
- };
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
-
66
- function readWizardPresets(): Record<SupportedTarget, TargetWizardPreset> {
67
- const presetsPath = join(dirname(fileURLToPath(import.meta.url)), "target-presets.json");
68
- return JSON.parse(readFileSync(presetsPath, "utf-8")) as Record<SupportedTarget, TargetWizardPreset>;
69
- }
70
-
71
- const TARGET_WIZARDS = readWizardPresets();
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
-
101
- function filterOptionsForFramework(
102
- question: ChoiceQuestionPreset,
103
- framework: string
104
- ): WizardOptionPreset[] {
105
- return question.options.filter((option) => {
106
- if (!option.framework_filter) return true;
107
- return option.framework_filter.includes(framework);
108
- });
109
- }
110
-
111
- function effectiveDefault(
112
- question: ChoiceQuestionPreset,
113
- framework: string,
114
- inferred: string | undefined
115
- ): string {
116
- if (inferred) return inferred;
117
- const filtered = filterOptionsForFramework(question, framework);
118
- if (filtered.some((o) => o.value === question.recommended)) return question.recommended;
119
- return filtered[0]?.value ?? question.recommended;
120
- }
121
-
122
- function mergeDependencies(
123
- derived: string[],
124
- existing: unknown,
125
- managedDependencies: Set<string>
126
- ): string[] {
127
- const extras = Array.isArray(existing)
128
- ? existing.filter((dep): dep is string => typeof dep === "string" && !managedDependencies.has(dep))
129
- : [];
130
- return Array.from(new Set([...derived, ...extras]));
131
- }
132
-
133
- function findOption(question: ChoiceQuestionPreset, value: string): WizardOptionPreset | null {
134
- return question.options.find((option) => option.value === value) ?? null;
135
- }
136
-
137
- function collectManagedDependencies(wizard: TargetWizardPreset): Set<string> {
138
- const managed = new Set<string>(wizard.base_dependencies ?? []);
139
- if (wizard.framework_dependencies) {
140
- for (const deps of Object.values(wizard.framework_dependencies)) {
141
- for (const dep of deps) managed.add(dep);
142
- }
143
- }
144
- for (const question of wizard.questions) {
145
- for (const option of question.options) {
146
- for (const dependency of option.dependencies ?? []) {
147
- managed.add(dependency);
148
- }
149
- }
150
- }
151
- return managed;
152
- }
153
-
154
- function normalizeAndroid(existingPlatform: Record<string, any>): Record<string, string> {
155
- const generation = existingPlatform.generation ?? {};
156
- const deps = new Set(Array.isArray(generation.dependencies) ? generation.dependencies : []);
157
- const architectureValue = typeof generation.architecture === "string" ? generation.architecture.toLowerCase() : "";
158
- const stateValue = typeof generation.state === "string" ? generation.state.toLowerCase() : "";
159
- const persistenceValue = typeof generation.persistence === "string" ? generation.persistence.toLowerCase() : "";
160
- return {
161
- architecture:
162
- typeof generation.architecture === "string" &&
163
- !architectureValue.includes("decompose") &&
164
- !architectureValue.includes("compose") &&
165
- !deps.has("decompose") &&
166
- !deps.has("navigation-compose")
167
- ? generation.architecture
168
- : architectureValue.includes("decompose") || deps.has("decompose")
169
- ? "decompose"
170
- : architectureValue.includes("compose") || deps.has("navigation-compose")
171
- ? "plain_compose"
172
- : "decompose",
173
- state:
174
- typeof generation.state === "string" &&
175
- !stateValue.includes("mvikotlin") &&
176
- !stateValue.includes("viewmodel") &&
177
- !deps.has("mvikotlin") &&
178
- !deps.has("lifecycle-viewmodel-compose")
179
- ? generation.state
180
- : stateValue.includes("mvikotlin") || deps.has("mvikotlin")
181
- ? "mvikotlin"
182
- : stateValue.includes("viewmodel") || deps.has("lifecycle-viewmodel-compose")
183
- ? "viewmodel"
184
- : "mvikotlin",
185
- preferences:
186
- typeof generation.preferences === "string"
187
- ? generation.preferences
188
- : persistenceValue === "datastore" || deps.has("datastore-preferences")
189
- ? "datastore"
190
- : "datastore",
191
- database:
192
- typeof generation.database === "string"
193
- ? generation.database
194
- : persistenceValue === "sqldelight" || deps.has("sqldelight")
195
- ? "sqldelight"
196
- : persistenceValue === "room" || deps.has("room-runtime")
197
- ? "room"
198
- : "none",
199
- di:
200
- typeof generation.di === "string"
201
- ? ["metro", "koin", "hilt", "none"].includes(generation.di)
202
- ? generation.di
203
- : generation.di
204
- : deps.has("metro")
205
- ? "metro"
206
- : deps.has("koin-android")
207
- ? "koin"
208
- : deps.has("hilt-android")
209
- ? "hilt"
210
- : "metro",
211
- };
212
- }
213
-
214
- function normalizeWeb(existingPlatform: Record<string, any>): Record<string, string> {
215
- const generation = existingPlatform.generation ?? {};
216
- const result: Record<string, string> = {
217
- runtime:
218
- typeof generation.runtime === "string"
219
- ? generation.runtime
220
- : "frontend_only",
221
- css:
222
- typeof generation.css === "string"
223
- ? generation.css
224
- : "tailwind",
225
- storage_backend:
226
- typeof generation.storage_backend === "string"
227
- ? generation.storage_backend
228
- : "none",
229
- };
230
-
231
- if (typeof generation.routing === "string") {
232
- result.routing = generation.routing.includes("tanstack")
233
- ? "tanstack_router"
234
- : generation.routing.includes("react-router") || generation.routing === "react_router"
235
- ? "react_router"
236
- : generation.routing;
237
- }
238
-
239
- if (typeof generation.state === "string") {
240
- result.state = generation.state === "redux-toolkit"
241
- ? "redux"
242
- : generation.state === "tanstack-query"
243
- ? "query_only"
244
- : generation.state;
245
- }
246
-
247
- return result;
248
- }
249
-
250
- function normalizeIos(existingPlatform: Record<string, any>): Record<string, string> {
251
- const generation = existingPlatform.generation ?? {};
252
- const deps = new Set(Array.isArray(generation.dependencies) ? generation.dependencies : []);
253
- return {
254
- architecture:
255
- typeof generation.architecture === "string" &&
256
- !generation.architecture.toLowerCase().includes("tca") &&
257
- generation.architecture.toLowerCase() !== "native swiftui"
258
- ? generation.architecture
259
- : typeof generation.architecture === "string" && generation.architecture.toLowerCase().includes("tca")
260
- ? "tca_style"
261
- : deps.has("swift-composable-architecture")
262
- ? "tca_style"
263
- : "native",
264
- persistence:
265
- typeof generation.persistence === "string"
266
- ? generation.persistence
267
- : deps.has("sqlite")
268
- ? "sqlite"
269
- : deps.has("swiftdata")
270
- ? "swiftdata"
271
- : "swiftdata",
272
- di:
273
- typeof generation.di === "string"
274
- ? generation.di
275
- : deps.has("factory")
276
- ? "factory"
277
- : deps.has("custom-di")
278
- ? "custom"
279
- : "none",
280
- };
281
- }
282
-
283
- function normalizeExisting(target: SupportedTarget, existingPlatform: Record<string, any>): Record<string, string> {
284
- switch (target) {
285
- case "android":
286
- return normalizeAndroid(existingPlatform);
287
- case "web":
288
- return normalizeWeb(existingPlatform);
289
- case "ios":
290
- return normalizeIos(existingPlatform);
291
- }
292
- }
293
-
294
- function buildGeneration(
295
- wizard: TargetWizardPreset,
296
- answers: Record<string, string>,
297
- existingGeneration: Record<string, any>,
298
- framework: string
299
- ): Record<string, any> {
300
- const generation = {
301
- ...(wizard.generation_defaults ?? {}),
302
- ...existingGeneration,
303
- };
304
- const managedDependencies = collectManagedDependencies(wizard);
305
- const frameworkDeps = wizard.framework_dependencies?.[framework] ?? [];
306
- const derivedDependencies = [...(wizard.base_dependencies ?? []), ...frameworkDeps];
307
-
308
- for (const question of wizard.questions) {
309
- const answer = answers[question.key];
310
- const selected = answer ? findOption(question, answer) : null;
311
- if (!selected) {
312
- generation[question.key] = answer || question.recommended;
313
- continue;
314
- }
315
- generation[question.key] = selected.generation_value ?? selected.value;
316
- Object.assign(generation, selected.extra_generation ?? {});
317
- derivedDependencies.push(...(selected.dependencies ?? []));
318
- }
319
-
320
- generation.dependencies = mergeDependencies(
321
- derivedDependencies,
322
- existingGeneration.dependencies,
323
- managedDependencies
324
- );
325
-
326
- return generation;
327
- }
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
-
344
- function parseTarget(argv: string[]): SupportedTarget | null {
345
- const direct = argv[0];
346
- if (direct && isSupportedTarget(direct)) {
347
- return direct;
348
- }
349
- const targetIdx = argv.indexOf("--target");
350
- if (targetIdx !== -1 && argv[targetIdx + 1] && isSupportedTarget(argv[targetIdx + 1])) {
351
- return argv[targetIdx + 1];
352
- }
353
- return null;
354
- }
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
-
371
- export async function runConfigureTarget(argv: string[]): Promise<void> {
372
- const target = parseTarget(argv);
373
- const listOptions = argv.includes("--list-options");
374
- const useDefaults = argv.includes("--defaults");
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;
379
- if (!target) {
380
- console.error("Error: target is required for configure-target");
381
- console.error("Usage: openuispec configure-target <ios|android|web> [--defaults] [--list-options] [--set key=value]");
382
- process.exit(1);
383
- }
384
-
385
- if (listOptions) {
386
- console.log(JSON.stringify(listTargetWizardOptions(target), null, 2));
387
- return;
388
- }
389
-
390
- if (!interactive && !useDefaults && !hasSetPairs) {
391
- console.error(
392
- "Error: `openuispec configure-target` needs a TTY for prompts.\n" +
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."
395
- );
396
- process.exit(1);
397
- }
398
-
399
- const projectDir = findProjectDir(process.cwd());
400
- const manifest = readManifest(projectDir);
401
- const configuredTargets: string[] = manifest.generation?.targets ?? [];
402
- if (configuredTargets.length > 0 && !configuredTargets.includes(target)) {
403
- console.error(
404
- `Error: target "${target}" is not listed in generation.targets.\n` +
405
- `Configured targets: ${configuredTargets.join(", ")}`
406
- );
407
- process.exit(1);
408
- }
409
-
410
- const platformDir = resolve(projectDir, manifest.includes?.platform ?? "./platform/");
411
- mkdirSync(platformDir, { recursive: true });
412
-
413
- const platformPath = join(platformDir, `${target}.yaml`);
414
- const existingDoc = existsSync(platformPath)
415
- ? (YAML.parse(readFileSync(platformPath, "utf-8")) as Record<string, any>)
416
- : {};
417
- const existingPlatform = existingDoc[target] ?? {};
418
- const existingGeneration = existingPlatform.generation ?? {};
419
- const wizard = TARGET_WIZARDS[target];
420
- const defaultFramework =
421
- typeof existingPlatform.framework === "string" && existingPlatform.framework.trim().length > 0
422
- ? existingPlatform.framework
423
- : wizard.framework;
424
- const inferredDefaults = {
425
- ...normalizeExisting(target, existingPlatform),
426
- };
427
-
428
- let framework = defaultFramework;
429
-
430
- function computeDefaultAnswers(fw: string): Record<string, string> {
431
- return Object.fromEntries(
432
- wizard.questions.map((question) => {
433
- const defaultValue = effectiveDefault(question, fw, inferredDefaults[question.key]);
434
- return [question.key, defaultValue];
435
- })
436
- );
437
- }
438
-
439
- let answers = computeDefaultAnswers(framework);
440
-
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) {
451
- const rl = createInterface({ input: stdin, output: stdout });
452
-
453
- try {
454
- console.log(`\nOpenUISpec — Configure ${target}\n`);
455
- console.log(`Writing target stack choices to ${relative(process.cwd(), platformPath)}\n`);
456
-
457
- const frameworkOptions = [
458
- ...(wizard.framework_options ?? [wizard.framework]),
459
- "other",
460
- ];
461
- const chosenFramework = await askChoice(
462
- rl,
463
- wizard.framework_prompt ?? `${target} framework`,
464
- frameworkOptions,
465
- framework
466
- );
467
- framework =
468
- chosenFramework === "other"
469
- ? await ask(rl, `Custom ${target} framework`, framework)
470
- : chosenFramework;
471
-
472
- const defaultAnswers = computeDefaultAnswers(framework);
473
- answers = {};
474
- for (const question of wizard.questions) {
475
- const filtered = filterOptionsForFramework(question, framework);
476
- const chosen = await askChoice(
477
- rl,
478
- question.prompt,
479
- [...filtered.map((option) => option.value), "other"],
480
- defaultAnswers[question.key]
481
- );
482
- answers[question.key] =
483
- chosen === "other"
484
- ? await ask(rl, `Custom value for ${question.key}`, defaultAnswers[question.key])
485
- : chosen;
486
- }
487
- } finally {
488
- rl.close();
489
- }
490
- }
491
-
492
- // --set implies confirmed (user explicitly chose values); --defaults without --set is pending
493
- const updatedPlatform: Record<string, any> = {
494
- ...existingPlatform,
495
- framework,
496
- generation: {
497
- ...buildGeneration(wizard, answers, existingGeneration, framework),
498
- stack_confirmation: stackConfirmation(useDefaults && !hasSetPairs),
499
- },
500
- };
501
-
502
- if (wizard.language) updatedPlatform.language = wizard.language;
503
- if (wizard.min_version) updatedPlatform.min_version = existingPlatform.min_version ?? wizard.min_version;
504
- if (typeof wizard.min_sdk === "number") updatedPlatform.min_sdk = existingPlatform.min_sdk ?? wizard.min_sdk;
505
- if (typeof wizard.target_sdk === "number") {
506
- updatedPlatform.target_sdk = existingPlatform.target_sdk ?? wizard.target_sdk;
507
- }
508
-
509
- writeFileSync(platformPath, YAML.stringify({ [target]: updatedPlatform }));
510
-
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
- }
522
- }
523
- }