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.
- package/dist/check/audit.js +392 -0
- package/dist/check/index.js +216 -0
- package/dist/cli/configure-target.js +391 -0
- package/dist/cli/index.js +510 -0
- package/dist/cli/init.js +1047 -0
- package/dist/drift/index.js +903 -0
- package/dist/mcp-server/index.js +886 -0
- package/dist/mcp-server/preview-render.js +1761 -0
- package/dist/mcp-server/preview.js +233 -0
- package/dist/mcp-server/screenshot-android.js +458 -0
- package/dist/mcp-server/screenshot-ios.js +639 -0
- package/dist/mcp-server/screenshot-shared.js +180 -0
- package/dist/mcp-server/screenshot.js +459 -0
- package/dist/prepare/index.js +1216 -0
- package/dist/runtime/package-paths.js +33 -0
- package/dist/schema/semantic-lint.js +564 -0
- package/dist/schema/validate.js +689 -0
- package/dist/status/index.js +194 -0
- package/package.json +12 -13
- package/check/audit.ts +0 -426
- package/check/index.ts +0 -320
- package/cli/configure-target.ts +0 -523
- package/cli/index.ts +0 -537
- package/cli/init.ts +0 -1253
- package/drift/index.ts +0 -1165
- package/mcp-server/index.ts +0 -1041
- package/mcp-server/preview-render.ts +0 -1922
- package/mcp-server/preview.ts +0 -292
- package/mcp-server/screenshot-android.ts +0 -621
- package/mcp-server/screenshot-ios.ts +0 -753
- package/mcp-server/screenshot-shared.ts +0 -237
- package/mcp-server/screenshot.ts +0 -563
- package/prepare/index.ts +0 -1530
- package/schema/semantic-lint.ts +0 -692
- package/schema/validate.ts +0 -870
- package/scripts/regenerate-previews.ts +0 -136
- package/scripts/take-all-screenshots.ts +0 -507
- package/status/index.ts +0 -275
package/check/index.ts
DELETED
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env tsx
|
|
2
|
-
/**
|
|
3
|
-
* Composite check command for OpenUISpec projects.
|
|
4
|
-
*
|
|
5
|
-
* Combines schema validation, semantic linting, and prepare readiness
|
|
6
|
-
* into a single call for AI agents.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* openuispec check --target web # human-readable summary
|
|
10
|
-
* openuispec check --target ios --json # machine-readable output
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
-
import { join, resolve } from "node:path";
|
|
15
|
-
import YAML from "yaml";
|
|
16
|
-
import {
|
|
17
|
-
computeSharedDrift,
|
|
18
|
-
findProjectDir,
|
|
19
|
-
hasDriftChanges,
|
|
20
|
-
readManifest,
|
|
21
|
-
readProjectName,
|
|
22
|
-
resolveOutputDir,
|
|
23
|
-
sharedLayersForTarget,
|
|
24
|
-
} from "../drift/index.js";
|
|
25
|
-
import {
|
|
26
|
-
buildAjv,
|
|
27
|
-
readIncludes,
|
|
28
|
-
GROUPS,
|
|
29
|
-
type JsonGroupResult,
|
|
30
|
-
} from "../schema/validate.js";
|
|
31
|
-
import { collectSemanticLint } from "../schema/semantic-lint.js";
|
|
32
|
-
import { buildAuditResult, formatAuditResult, type AuditResult } from './audit.js';
|
|
33
|
-
|
|
34
|
-
// ── types ─────────────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
interface CheckValidation {
|
|
37
|
-
total_errors: number;
|
|
38
|
-
groups: JsonGroupResult[];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface CheckSemantic {
|
|
42
|
-
total_errors: number;
|
|
43
|
-
errors: Array<{ path: string; message: string }>;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface CheckPrepare {
|
|
47
|
-
mode: "bootstrap" | "update";
|
|
48
|
-
ready: boolean;
|
|
49
|
-
missing: string[];
|
|
50
|
-
warnings: string[];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface CheckResult {
|
|
54
|
-
target: string;
|
|
55
|
-
validation: CheckValidation;
|
|
56
|
-
semantic: CheckSemantic;
|
|
57
|
-
prepare: CheckPrepare;
|
|
58
|
-
audit?: AuditResult;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ── prepare readiness helpers ─────────────────────────────────────────
|
|
62
|
-
|
|
63
|
-
const PRESENTATION_ONLY_KEYS = new Set(["naming", "bundler", "css"]);
|
|
64
|
-
|
|
65
|
-
function platformStackKeys(target: string): string[] {
|
|
66
|
-
switch (target) {
|
|
67
|
-
case "android":
|
|
68
|
-
return ["architecture", "state", "preferences", "database", "di", "naming"];
|
|
69
|
-
case "web":
|
|
70
|
-
return ["runtime", "routing", "state", "storage_backend", "bundler", "css", "naming"];
|
|
71
|
-
case "ios":
|
|
72
|
-
return ["architecture", "persistence", "di", "naming"];
|
|
73
|
-
default:
|
|
74
|
-
return [];
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function requiredPlatformDecisionKeys(target: string): string[] {
|
|
79
|
-
return platformStackKeys(target).filter((key) => !PRESENTATION_ONLY_KEYS.has(key));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function readPlatformDefinition(
|
|
83
|
-
projectDir: string,
|
|
84
|
-
manifest: Record<string, any>,
|
|
85
|
-
target: string,
|
|
86
|
-
): Record<string, any> {
|
|
87
|
-
const platformDir = resolve(projectDir, manifest.includes?.platform ?? "./platform/");
|
|
88
|
-
const platformPath = join(platformDir, `${target}.yaml`);
|
|
89
|
-
if (!existsSync(platformPath)) return {};
|
|
90
|
-
try {
|
|
91
|
-
const doc = YAML.parse(readFileSync(platformPath, "utf-8"));
|
|
92
|
-
return doc?.[target] ?? {};
|
|
93
|
-
} catch {
|
|
94
|
-
return {};
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function missingPlatformDecisions(
|
|
99
|
-
target: string,
|
|
100
|
-
platformDef: Record<string, any>,
|
|
101
|
-
): string[] {
|
|
102
|
-
const generation = platformDef.generation ?? {};
|
|
103
|
-
return requiredPlatformDecisionKeys(target).filter((key) => {
|
|
104
|
-
const value = generation[key];
|
|
105
|
-
return typeof value !== "string" || value.trim().length === 0;
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function hasApiEndpoints(manifest: Record<string, any>): boolean {
|
|
110
|
-
const endpoints = manifest.api?.endpoints;
|
|
111
|
-
return typeof endpoints === "object" && endpoints !== null && Object.keys(endpoints).length > 0;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function resolveBackendRoot(
|
|
115
|
-
projectDir: string,
|
|
116
|
-
manifest: Record<string, any>,
|
|
117
|
-
): string | null {
|
|
118
|
-
const backendRoot = manifest.generation?.code_roots?.backend;
|
|
119
|
-
if (typeof backendRoot !== "string" || backendRoot.trim().length === 0) {
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
return resolve(projectDir, backendRoot);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function determinePrepare(
|
|
126
|
-
projectDir: string,
|
|
127
|
-
projectName: string,
|
|
128
|
-
target: string,
|
|
129
|
-
): CheckPrepare {
|
|
130
|
-
const manifest = readManifest(projectDir);
|
|
131
|
-
const outputDir = resolveOutputDir(projectDir, projectName, target);
|
|
132
|
-
const statePath = join(outputDir, ".openuispec-state.json");
|
|
133
|
-
const snapshotExists = existsSync(statePath);
|
|
134
|
-
const mode: "bootstrap" | "update" = snapshotExists ? "update" : "bootstrap";
|
|
135
|
-
|
|
136
|
-
const platformDef = readPlatformDefinition(projectDir, manifest, target);
|
|
137
|
-
const missing = missingPlatformDecisions(target, platformDef);
|
|
138
|
-
const warnings: string[] = [];
|
|
139
|
-
|
|
140
|
-
const backendContextRequired = hasApiEndpoints(manifest);
|
|
141
|
-
const backendRoot = resolveBackendRoot(projectDir, manifest);
|
|
142
|
-
const backendContextReady =
|
|
143
|
-
!backendContextRequired || (backendRoot !== null && existsSync(backendRoot));
|
|
144
|
-
|
|
145
|
-
if (!backendContextReady) {
|
|
146
|
-
warnings.push(
|
|
147
|
-
"api endpoints require generation.code_roots.backend to point at an existing backend folder",
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const stackConfirmation = platformDef.generation?.stack_confirmation;
|
|
152
|
-
const pendingUserConfirmation =
|
|
153
|
-
typeof stackConfirmation === "string" && stackConfirmation !== "confirmed";
|
|
154
|
-
|
|
155
|
-
if (pendingUserConfirmation) {
|
|
156
|
-
warnings.push(
|
|
157
|
-
`Target stack for "${target}" requires explicit user confirmation before implementation.`,
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Check for shared layer drift (only when tracks are configured)
|
|
162
|
-
const sharedLayers = sharedLayersForTarget(projectDir, target);
|
|
163
|
-
for (const layer of sharedLayers) {
|
|
164
|
-
if (layer.tracks.length === 0) continue;
|
|
165
|
-
const driftResult = computeSharedDrift(projectDir, layer);
|
|
166
|
-
if (driftResult.state !== null) {
|
|
167
|
-
if (hasDriftChanges(driftResult.drift)) {
|
|
168
|
-
warnings.push(
|
|
169
|
-
`Shared layer "${layer.name}" has spec drift — shared code may need updates before ${target} generation.`
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const ready =
|
|
176
|
-
missing.length === 0 && backendContextReady && !pendingUserConfirmation;
|
|
177
|
-
|
|
178
|
-
return { mode, ready, missing, warnings };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// ── core (importable, no process.exit) ───────────────────────────────
|
|
182
|
-
|
|
183
|
-
export function buildCheckResult(target: string, cwd: string = process.cwd(), includeAudit: boolean = false): CheckResult {
|
|
184
|
-
const projectDir = findProjectDir(cwd);
|
|
185
|
-
const projectName = readProjectName(projectDir);
|
|
186
|
-
const includes = readIncludes(projectDir);
|
|
187
|
-
const ajv = buildAjv();
|
|
188
|
-
|
|
189
|
-
// 1. Schema validation (all groups except semantic)
|
|
190
|
-
const schemaGroupKeys = Object.keys(GROUPS).filter((k) => k !== "semantic");
|
|
191
|
-
const validationGroups: JsonGroupResult[] = [];
|
|
192
|
-
let validationTotalErrors = 0;
|
|
193
|
-
|
|
194
|
-
for (const key of schemaGroupKeys) {
|
|
195
|
-
const result = GROUPS[key].collectJson(ajv, projectDir, includes, key);
|
|
196
|
-
validationGroups.push(result);
|
|
197
|
-
validationTotalErrors += result.errors.length;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const validation: CheckValidation = {
|
|
201
|
-
total_errors: validationTotalErrors,
|
|
202
|
-
groups: validationGroups,
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
// 2. Semantic validation
|
|
206
|
-
const semanticErrors = collectSemanticLint(projectDir, includes);
|
|
207
|
-
const semantic: CheckSemantic = {
|
|
208
|
-
total_errors: semanticErrors.length,
|
|
209
|
-
errors: semanticErrors.map((e) => ({ path: e.path, message: e.message })),
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
// 3. Prepare readiness
|
|
213
|
-
const prepare = determinePrepare(projectDir, projectName, target);
|
|
214
|
-
|
|
215
|
-
const audit = includeAudit ? buildAuditResult(projectDir) : undefined;
|
|
216
|
-
return { target, validation, semantic, prepare, audit };
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// ── main ──────────────────────────────────────────────────────────────
|
|
220
|
-
|
|
221
|
-
export function runCheck(argv: string[]): void {
|
|
222
|
-
const isJson = argv.includes("--json");
|
|
223
|
-
const targetIdx = argv.indexOf("--target");
|
|
224
|
-
const target =
|
|
225
|
-
targetIdx !== -1 && argv[targetIdx + 1] ? argv[targetIdx + 1] : null;
|
|
226
|
-
|
|
227
|
-
if (!target) {
|
|
228
|
-
console.error("Error: --target is required for check");
|
|
229
|
-
console.error("Usage: openuispec check --target <target> [--json]");
|
|
230
|
-
process.exit(1);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const includeAudit = argv.includes("--audit");
|
|
234
|
-
const minScoreIdx = argv.indexOf("--min-score");
|
|
235
|
-
const minScore = minScoreIdx !== -1 && argv[minScoreIdx + 1] ? parseInt(argv[minScoreIdx + 1], 10) : 0;
|
|
236
|
-
|
|
237
|
-
const result = buildCheckResult(target, undefined, includeAudit);
|
|
238
|
-
|
|
239
|
-
if (isJson) {
|
|
240
|
-
console.log(JSON.stringify(result, null, 2));
|
|
241
|
-
} else {
|
|
242
|
-
printReport(result);
|
|
243
|
-
if (result.audit) {
|
|
244
|
-
console.log("\nDesign Quality Audit");
|
|
245
|
-
console.log("====================");
|
|
246
|
-
console.log(formatAuditResult(result.audit));
|
|
247
|
-
if (minScore > 0 && result.audit.score < minScore) {
|
|
248
|
-
console.error(`\nFAIL: Score ${result.audit.score} is below --min-score ${minScore}`);
|
|
249
|
-
process.exit(1);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Exit codes: 0 = clean + ready, 2 = validation errors, 1 = config error
|
|
255
|
-
const totalErrors = result.validation.total_errors + result.semantic.total_errors;
|
|
256
|
-
if (totalErrors > 0) {
|
|
257
|
-
process.exit(2);
|
|
258
|
-
}
|
|
259
|
-
if (!result.prepare.ready) {
|
|
260
|
-
process.exit(2);
|
|
261
|
-
}
|
|
262
|
-
process.exit(0);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function printReport(result: CheckResult): void {
|
|
266
|
-
console.log("OpenUISpec Check");
|
|
267
|
-
console.log("================");
|
|
268
|
-
console.log(`Target: ${result.target}`);
|
|
269
|
-
|
|
270
|
-
const totalValidation = result.validation.total_errors;
|
|
271
|
-
const totalSemantic = result.semantic.total_errors;
|
|
272
|
-
|
|
273
|
-
console.log(`\nSchema validation: ${totalValidation === 0 ? "PASS" : `FAIL (${totalValidation} error(s))`}`);
|
|
274
|
-
if (totalValidation > 0) {
|
|
275
|
-
for (const group of result.validation.groups) {
|
|
276
|
-
if (group.errors.length > 0) {
|
|
277
|
-
console.log(` ${group.group}: ${group.errors.length} error(s)`);
|
|
278
|
-
for (const err of group.errors.slice(0, 3)) {
|
|
279
|
-
console.log(` [${err.file}] ${err.path}: ${err.message}`);
|
|
280
|
-
}
|
|
281
|
-
if (group.errors.length > 3) {
|
|
282
|
-
console.log(` ... and ${group.errors.length - 3} more`);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
console.log(`Semantic lint: ${totalSemantic === 0 ? "PASS" : `FAIL (${totalSemantic} error(s))`}`);
|
|
289
|
-
if (totalSemantic > 0) {
|
|
290
|
-
for (const err of result.semantic.errors.slice(0, 5)) {
|
|
291
|
-
console.log(` [${err.path}] ${err.message}`);
|
|
292
|
-
}
|
|
293
|
-
if (result.semantic.errors.length > 5) {
|
|
294
|
-
console.log(` ... and ${result.semantic.errors.length - 5} more`);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
console.log(`\nPrepare readiness: ${result.prepare.ready ? "READY" : "NOT READY"}`);
|
|
299
|
-
console.log(` Mode: ${result.prepare.mode}`);
|
|
300
|
-
if (result.prepare.missing.length > 0) {
|
|
301
|
-
console.log(` Missing platform decisions: ${result.prepare.missing.join(", ")}`);
|
|
302
|
-
}
|
|
303
|
-
for (const w of result.prepare.warnings) {
|
|
304
|
-
console.log(` Warning: ${w}`);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const totalErrors = result.validation.total_errors + result.semantic.total_errors;
|
|
308
|
-
console.log(
|
|
309
|
-
`\nResult: ${totalErrors === 0 && result.prepare.ready ? "ALL CLEAR" : "ACTION NEEDED"}`,
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Direct execution
|
|
314
|
-
const isDirectRun =
|
|
315
|
-
process.argv[1]?.endsWith("check/index.ts") ||
|
|
316
|
-
process.argv[1]?.endsWith("check/index.js");
|
|
317
|
-
|
|
318
|
-
if (isDirectRun) {
|
|
319
|
-
runCheck(process.argv.slice(2));
|
|
320
|
-
}
|