openuispec 0.2.19 → 0.2.21

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 +964 -0
  6. package/dist/drift/index.js +903 -0
  7. package/dist/mcp-server/index.js +888 -0
  8. package/dist/mcp-server/preview-render.js +1761 -0
  9. package/dist/mcp-server/preview.js +229 -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 +185 -0
  13. package/dist/mcp-server/screenshot.js +469 -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 +13 -14
  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
@@ -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
- }