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,237 +0,0 @@
1
- /**
2
- * Shared utilities for platform screenshot tools (web, android, ios).
3
- */
4
-
5
- import { existsSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
6
- import { join, resolve, relative } from "node:path";
7
- import { createHash } from "node:crypto";
8
- import YAML from "yaml";
9
- import { findProjectDir } from "../drift/index.js";
10
-
11
- // ── shared browser manager ──────────────────────────────────────────
12
-
13
- let browserInstance: any = null;
14
- let launchPromise: Promise<any> | null = null;
15
-
16
- export async function getBrowser(): Promise<any> {
17
- if (browserInstance?.connected) return browserInstance;
18
-
19
- if (!launchPromise) {
20
- launchPromise = (async () => {
21
- let puppeteer: any;
22
- try {
23
- puppeteer = await import("puppeteer");
24
- } catch {
25
- throw new Error(
26
- "puppeteer is not installed. Run:\n npm install -g puppeteer\n" +
27
- "or add it to your project's devDependencies.",
28
- );
29
- }
30
-
31
- browserInstance = await puppeteer.launch({
32
- headless: true,
33
- args: ["--no-sandbox", "--disable-setuid-sandbox"],
34
- });
35
- return browserInstance;
36
- })();
37
- }
38
- return launchPromise;
39
- }
40
-
41
- export async function closeBrowser(): Promise<void> {
42
- launchPromise = null;
43
- if (browserInstance) {
44
- try { await browserInstance.close(); } catch { /* ignore */ }
45
- browserInstance = null;
46
- }
47
- }
48
-
49
- // ── shared result type ──────────────────────────────────────────────
50
-
51
- import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
52
-
53
- export type ScreenshotResult = CallToolResult;
54
-
55
- // ── manifest loading ────────────────────────────────────────────────
56
-
57
- export interface ManifestInfo {
58
- projectDir: string;
59
- projectRoot: string;
60
- manifest: any;
61
- projectName: string;
62
- }
63
-
64
- export function loadManifest(projectCwd: string): ManifestInfo {
65
- const projectDir = findProjectDir(projectCwd);
66
- const manifestPath = join(projectDir, "openuispec.yaml");
67
- const manifest = YAML.parse(readFileSync(manifestPath, "utf-8"));
68
- const projectName = manifest.project?.name ?? "app";
69
- const projectRoot = resolve(projectDir, "..");
70
- return { projectDir, projectRoot, manifest, projectName };
71
- }
72
-
73
- // ── generic platform app directory discovery ────────────────────────
74
-
75
- function tryFindProjectDir(cwd: string): string | null {
76
- if (existsSync(join(cwd, "openuispec.yaml"))) {
77
- return cwd;
78
- }
79
- try { return findProjectDir(cwd); } catch { return null; }
80
- }
81
-
82
- export function findPlatformAppDir(
83
- projectCwd: string,
84
- platform: "web" | "android" | "ios",
85
- existsCheck: (dir: string) => boolean,
86
- directDir?: string,
87
- ): string {
88
- const label = platform.charAt(0).toUpperCase() + platform.slice(1);
89
-
90
- // If a direct project dir is provided, use it without manifest lookup
91
- if (directDir) {
92
- const resolved = resolve(directDir);
93
- if (existsCheck(resolved)) return resolved;
94
- throw new Error(`${label} project not found at provided path: ${resolved}`);
95
- }
96
-
97
- // Check if projectCwd has an openuispec.yaml — if so, use manifest-based discovery
98
- const manifestDir = tryFindProjectDir(projectCwd);
99
-
100
- if (manifestDir) {
101
- const manifestPath = join(manifestDir, "openuispec.yaml");
102
- const manifest = YAML.parse(readFileSync(manifestPath, "utf-8"));
103
- const projectName = manifest.project?.name ?? "app";
104
- const projectRoot = resolve(manifestDir, "..");
105
-
106
- // Check custom output_dir first
107
- const customDir = manifest.generation?.output_dir?.[platform];
108
- if (customDir) {
109
- const resolved = resolve(manifestDir, customDir);
110
- if (existsCheck(resolved)) return resolved;
111
- }
112
-
113
- // Default: generated/<platform>/<project-name>/
114
- const defaultDir = join(projectRoot, "generated", platform, projectName);
115
- if (existsCheck(defaultDir)) return defaultDir;
116
-
117
- throw new Error(
118
- `${label} app not found. Checked:\n` +
119
- (customDir ? ` - ${resolve(manifestDir, customDir)}\n` : "") +
120
- ` - ${defaultDir}\n` +
121
- `Generate the ${platform} target first, then try again.`,
122
- );
123
- }
124
-
125
- // No manifest — treat projectCwd itself as the platform project dir
126
- const resolved = resolve(projectCwd);
127
- if (existsCheck(resolved)) return resolved;
128
-
129
- throw new Error(
130
- `${label} project not found at ${resolved}. ` +
131
- `Provide a project_dir parameter or create an openuispec.yaml manifest.`,
132
- );
133
- }
134
-
135
- // ── file hashing / caching ──────────────────────────────────────────
136
-
137
- export function hashContent(content: string): string {
138
- return createHash("md5").update(content).digest("hex");
139
- }
140
-
141
- export function loadHashes(dir: string, hashFile: string): Record<string, string> {
142
- const hashPath = join(dir, hashFile);
143
- try {
144
- return JSON.parse(readFileSync(hashPath, "utf-8"));
145
- } catch {
146
- return {};
147
- }
148
- }
149
-
150
- export function saveHashes(dir: string, hashFile: string, hashes: Record<string, string>): void {
151
- writeFileSync(join(dir, hashFile), JSON.stringify(hashes, null, 2));
152
- }
153
-
154
- // ── screen name filter ──────────────────────────────────────────────
155
-
156
- export function matchesScreenFilter(screenName: string, filter: string): boolean {
157
- const normalizedFilter = filter.toLowerCase().replace(/_/g, "");
158
- const normalizedScreen = screenName.toLowerCase().replace(/_/g, "");
159
- return normalizedScreen.includes(normalizedFilter) || normalizedFilter.includes(normalizedScreen);
160
- }
161
-
162
- // ── recursive file walker ───────────────────────────────────────────
163
-
164
- export function walkFiles(
165
- dir: string,
166
- ext: string,
167
- skipDirs: string[] = [],
168
- ): string[] {
169
- const results: string[] = [];
170
- if (!existsSync(dir)) return results;
171
-
172
- const walk = (d: string) => {
173
- for (const entry of readdirSync(d, { withFileTypes: true })) {
174
- const fullPath = join(d, entry.name);
175
- if (entry.isDirectory()) {
176
- if (!skipDirs.includes(entry.name)) walk(fullPath);
177
- } else if (entry.name.endsWith(ext)) {
178
- results.push(fullPath);
179
- }
180
- }
181
- };
182
-
183
- walk(dir);
184
- return results;
185
- }
186
-
187
- // ── PNG snapshot collector ──────────────────────────────────────────
188
-
189
- export function collectPngSnapshots(
190
- dirs: string[],
191
- rootDir: string,
192
- screenFilter: string | undefined,
193
- nameExtractor: (filename: string) => string,
194
- ): Array<{ screen: string; path: string; data: string }> {
195
- const snapshots: Array<{ screen: string; path: string; data: string }> = [];
196
- const seen = new Set<string>();
197
-
198
- for (const dir of dirs) {
199
- const pngs = walkFiles(dir, ".png");
200
- for (const fullPath of pngs) {
201
- const filename = fullPath.split("/").pop()!;
202
- const screen = nameExtractor(filename);
203
-
204
- if (screenFilter && !matchesScreenFilter(screen, screenFilter)) continue;
205
-
206
- const key = relative(rootDir, fullPath);
207
- if (seen.has(key)) continue;
208
- seen.add(key);
209
-
210
- const data = readFileSync(fullPath).toString("base64");
211
- snapshots.push({ screen, path: key, data });
212
- }
213
- }
214
-
215
- return snapshots;
216
- }
217
-
218
- // ── screenshot response builder ─────────────────────────────────────
219
-
220
- export function buildScreenshotResponse(
221
- snapshots: Array<{ screen: string; path: string; data: string }>,
222
- metadataFn: (snapshot: { screen: string; path: string }) => Record<string, unknown>,
223
- ): ScreenshotResult {
224
- if (snapshots.length === 0) {
225
- return {
226
- content: [{ type: "text", text: "No screenshots generated. Check build output." }],
227
- isError: true,
228
- };
229
- }
230
-
231
- const content: ScreenshotResult["content"] = [];
232
- for (const snapshot of snapshots) {
233
- content.push({ type: "image" as const, data: snapshot.data, mimeType: "image/png" });
234
- content.push({ type: "text" as const, text: JSON.stringify(metadataFn(snapshot), null, 2) });
235
- }
236
- return { content };
237
- }