openuispec 0.1.33 → 0.1.35
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/README.md +43 -1
- package/check/index.ts +287 -0
- package/cli/init.ts +145 -99
- package/drift/index.ts +4 -7
- package/mcp-server/index.ts +183 -0
- package/package.json +6 -2
- package/prepare/index.ts +2 -3
- package/schema/validate.ts +30 -0
- package/status/index.ts +3 -4
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ cd your-project
|
|
|
52
52
|
openuispec init
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
This scaffolds a spec directory, starter tokens,
|
|
55
|
+
This scaffolds a spec directory, starter tokens, adds rules to `CLAUDE.md` / `AGENTS.md`, and configures the MCP server in `.claude.json` so AI assistants track spec changes automatically.
|
|
56
56
|
Use `openuispec init --no-configure-targets` if you want to scaffold first and choose target stacks later.
|
|
57
57
|
|
|
58
58
|
Then hand your spec to any AI code generator:
|
|
@@ -116,10 +116,16 @@ openuispec/
|
|
|
116
116
|
├── cli/ # CLI tool (openuispec init, drift, prepare, validate)
|
|
117
117
|
│ ├── index.ts # Entry point
|
|
118
118
|
│ └── init.ts # Project scaffolding + AI rules
|
|
119
|
+
├── mcp-server/ # MCP server (openuispec-mcp)
|
|
120
|
+
│ └── index.ts # Stdio transport, 5 tools
|
|
121
|
+
├── check/ # Composite validation command
|
|
122
|
+
│ └── index.ts # Schema + semantic + readiness
|
|
119
123
|
├── drift/ # Drift detection (spec change tracking)
|
|
120
124
|
│ └── index.ts # Hash-based drift checker
|
|
121
125
|
├── prepare/ # Target work bundle generation
|
|
122
126
|
│ └── index.ts # Baseline-aware target preparation
|
|
127
|
+
├── status/ # Cross-target status summary
|
|
128
|
+
│ └── index.ts # Baseline/drift overview
|
|
123
129
|
├── LICENSE
|
|
124
130
|
└── README.md
|
|
125
131
|
```
|
|
@@ -239,6 +245,41 @@ to see which targets are already up to date and which ones still need to catch u
|
|
|
239
245
|
|
|
240
246
|
`drift --snapshot` is bookkeeping. It does not prove that the target code matches the spec, and it will not create a missing target output directory for you.
|
|
241
247
|
|
|
248
|
+
## MCP server
|
|
249
|
+
|
|
250
|
+
OpenUISpec includes an MCP (Model Context Protocol) server that exposes CLI commands as tools for AI assistants. This is the recommended way to integrate with Claude Code and other MCP-compatible clients — tools are called more reliably than CLAUDE.md instructions.
|
|
251
|
+
|
|
252
|
+
### Setup
|
|
253
|
+
|
|
254
|
+
`openuispec init` automatically configures the MCP server in `.claude.json`. For existing projects, add it manually:
|
|
255
|
+
|
|
256
|
+
```json
|
|
257
|
+
{
|
|
258
|
+
"mcpServers": {
|
|
259
|
+
"openuispec": {
|
|
260
|
+
"command": "npx",
|
|
261
|
+
"args": ["openuispec-mcp"]
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Or run directly: `npx openuispec-mcp`
|
|
268
|
+
|
|
269
|
+
Set `OPENUISPEC_PROJECT_DIR` to override the working directory.
|
|
270
|
+
|
|
271
|
+
### Tools
|
|
272
|
+
|
|
273
|
+
| Tool | Description |
|
|
274
|
+
|------|-------------|
|
|
275
|
+
| `openuispec_prepare` | Build AI-ready work bundle for a target. **Call before any UI code generation.** |
|
|
276
|
+
| `openuispec_check` | Schema validation + semantic lint + prepare readiness. Call after spec edits. |
|
|
277
|
+
| `openuispec_status` | Cross-target summary: baselines, drift, next steps. |
|
|
278
|
+
| `openuispec_validate` | Schema-only validation, optionally filtered by group. |
|
|
279
|
+
| `openuispec_drift` | Detect spec drift since last snapshot, with optional semantic explanation. |
|
|
280
|
+
|
|
281
|
+
All tools return structured JSON. The server includes protocol-level instructions that tell AI assistants to call `openuispec_prepare` before any UI work — this works independently of CLAUDE.md rules.
|
|
282
|
+
|
|
242
283
|
## Spec at a glance
|
|
243
284
|
|
|
244
285
|
| Section | What it defines |
|
|
@@ -289,6 +330,7 @@ to see which targets are already up to date and which ones still need to catch u
|
|
|
289
330
|
- [x] Form system (validation rules, field dependencies)
|
|
290
331
|
- [x] Drift detection (spec change tracking per platform)
|
|
291
332
|
- [x] CLI tool (`openuispec init` for project scaffolding + AI rules)
|
|
333
|
+
- [x] MCP server for AI tool integration (`openuispec-mcp`)
|
|
292
334
|
- [x] Multi-platform showcase app (`examples/todo-orbit/`)
|
|
293
335
|
- [ ] More example apps (e-commerce, social, dashboard)
|
|
294
336
|
|
package/check/index.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
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
|
+
findProjectDir,
|
|
18
|
+
readManifest,
|
|
19
|
+
readProjectName,
|
|
20
|
+
resolveOutputDir,
|
|
21
|
+
} from "../drift/index.js";
|
|
22
|
+
import {
|
|
23
|
+
buildAjv,
|
|
24
|
+
readIncludes,
|
|
25
|
+
GROUPS,
|
|
26
|
+
type JsonGroupResult,
|
|
27
|
+
} from "../schema/validate.js";
|
|
28
|
+
import { collectSemanticLint } from "../schema/semantic-lint.js";
|
|
29
|
+
|
|
30
|
+
// ── types ─────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
interface CheckValidation {
|
|
33
|
+
total_errors: number;
|
|
34
|
+
groups: JsonGroupResult[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface CheckSemantic {
|
|
38
|
+
total_errors: number;
|
|
39
|
+
errors: Array<{ path: string; message: string }>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface CheckPrepare {
|
|
43
|
+
mode: "bootstrap" | "update";
|
|
44
|
+
ready: boolean;
|
|
45
|
+
missing: string[];
|
|
46
|
+
warnings: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface CheckResult {
|
|
50
|
+
target: string;
|
|
51
|
+
validation: CheckValidation;
|
|
52
|
+
semantic: CheckSemantic;
|
|
53
|
+
prepare: CheckPrepare;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── prepare readiness helpers ─────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
const PRESENTATION_ONLY_KEYS = new Set(["naming", "bundler", "css"]);
|
|
59
|
+
|
|
60
|
+
function platformStackKeys(target: string): string[] {
|
|
61
|
+
switch (target) {
|
|
62
|
+
case "android":
|
|
63
|
+
return ["architecture", "state", "preferences", "database", "di", "naming"];
|
|
64
|
+
case "web":
|
|
65
|
+
return ["runtime", "routing", "state", "storage_backend", "bundler", "css", "naming"];
|
|
66
|
+
case "ios":
|
|
67
|
+
return ["architecture", "persistence", "di", "naming"];
|
|
68
|
+
default:
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function requiredPlatformDecisionKeys(target: string): string[] {
|
|
74
|
+
return platformStackKeys(target).filter((key) => !PRESENTATION_ONLY_KEYS.has(key));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readPlatformDefinition(
|
|
78
|
+
projectDir: string,
|
|
79
|
+
manifest: Record<string, any>,
|
|
80
|
+
target: string,
|
|
81
|
+
): Record<string, any> {
|
|
82
|
+
const platformDir = resolve(projectDir, manifest.includes?.platform ?? "./platform/");
|
|
83
|
+
const platformPath = join(platformDir, `${target}.yaml`);
|
|
84
|
+
if (!existsSync(platformPath)) return {};
|
|
85
|
+
try {
|
|
86
|
+
const doc = YAML.parse(readFileSync(platformPath, "utf-8"));
|
|
87
|
+
return doc?.[target] ?? {};
|
|
88
|
+
} catch {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function missingPlatformDecisions(
|
|
94
|
+
target: string,
|
|
95
|
+
platformDef: Record<string, any>,
|
|
96
|
+
): string[] {
|
|
97
|
+
const generation = platformDef.generation ?? {};
|
|
98
|
+
return requiredPlatformDecisionKeys(target).filter((key) => {
|
|
99
|
+
const value = generation[key];
|
|
100
|
+
return typeof value !== "string" || value.trim().length === 0;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function hasApiEndpoints(manifest: Record<string, any>): boolean {
|
|
105
|
+
const endpoints = manifest.api?.endpoints;
|
|
106
|
+
return typeof endpoints === "object" && endpoints !== null && Object.keys(endpoints).length > 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveBackendRoot(
|
|
110
|
+
projectDir: string,
|
|
111
|
+
manifest: Record<string, any>,
|
|
112
|
+
): string | null {
|
|
113
|
+
const backendRoot = manifest.generation?.code_roots?.backend;
|
|
114
|
+
if (typeof backendRoot !== "string" || backendRoot.trim().length === 0) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
return resolve(projectDir, backendRoot);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function determinePrepare(
|
|
121
|
+
projectDir: string,
|
|
122
|
+
projectName: string,
|
|
123
|
+
target: string,
|
|
124
|
+
): CheckPrepare {
|
|
125
|
+
const manifest = readManifest(projectDir);
|
|
126
|
+
const outputDir = resolveOutputDir(projectDir, projectName, target);
|
|
127
|
+
const statePath = join(outputDir, ".openuispec-state.json");
|
|
128
|
+
const snapshotExists = existsSync(statePath);
|
|
129
|
+
const mode: "bootstrap" | "update" = snapshotExists ? "update" : "bootstrap";
|
|
130
|
+
|
|
131
|
+
const platformDef = readPlatformDefinition(projectDir, manifest, target);
|
|
132
|
+
const missing = missingPlatformDecisions(target, platformDef);
|
|
133
|
+
const warnings: string[] = [];
|
|
134
|
+
|
|
135
|
+
const backendContextRequired = hasApiEndpoints(manifest);
|
|
136
|
+
const backendRoot = resolveBackendRoot(projectDir, manifest);
|
|
137
|
+
const backendContextReady =
|
|
138
|
+
!backendContextRequired || (backendRoot !== null && existsSync(backendRoot));
|
|
139
|
+
|
|
140
|
+
if (!backendContextReady) {
|
|
141
|
+
warnings.push(
|
|
142
|
+
"api endpoints require generation.code_roots.backend to point at an existing backend folder",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const stackConfirmation = platformDef.generation?.stack_confirmation;
|
|
147
|
+
const pendingUserConfirmation =
|
|
148
|
+
typeof stackConfirmation === "string" && stackConfirmation !== "confirmed";
|
|
149
|
+
|
|
150
|
+
if (pendingUserConfirmation) {
|
|
151
|
+
warnings.push(
|
|
152
|
+
`Target stack for "${target}" requires explicit user confirmation before implementation.`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const ready =
|
|
157
|
+
missing.length === 0 && backendContextReady && !pendingUserConfirmation;
|
|
158
|
+
|
|
159
|
+
return { mode, ready, missing, warnings };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── core (importable, no process.exit) ───────────────────────────────
|
|
163
|
+
|
|
164
|
+
export function buildCheckResult(target: string, cwd: string = process.cwd()): CheckResult {
|
|
165
|
+
const projectDir = findProjectDir(cwd);
|
|
166
|
+
const projectName = readProjectName(projectDir);
|
|
167
|
+
const includes = readIncludes(projectDir);
|
|
168
|
+
const ajv = buildAjv();
|
|
169
|
+
|
|
170
|
+
// 1. Schema validation (all groups except semantic)
|
|
171
|
+
const schemaGroupKeys = Object.keys(GROUPS).filter((k) => k !== "semantic");
|
|
172
|
+
const validationGroups: JsonGroupResult[] = [];
|
|
173
|
+
let validationTotalErrors = 0;
|
|
174
|
+
|
|
175
|
+
for (const key of schemaGroupKeys) {
|
|
176
|
+
const result = GROUPS[key].collectJson(ajv, projectDir, includes, key);
|
|
177
|
+
validationGroups.push(result);
|
|
178
|
+
validationTotalErrors += result.errors.length;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const validation: CheckValidation = {
|
|
182
|
+
total_errors: validationTotalErrors,
|
|
183
|
+
groups: validationGroups,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// 2. Semantic validation
|
|
187
|
+
const semanticErrors = collectSemanticLint(projectDir, includes);
|
|
188
|
+
const semantic: CheckSemantic = {
|
|
189
|
+
total_errors: semanticErrors.length,
|
|
190
|
+
errors: semanticErrors.map((e) => ({ path: e.path, message: e.message })),
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// 3. Prepare readiness
|
|
194
|
+
const prepare = determinePrepare(projectDir, projectName, target);
|
|
195
|
+
|
|
196
|
+
return { target, validation, semantic, prepare };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── main ──────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
export function runCheck(argv: string[]): void {
|
|
202
|
+
const isJson = argv.includes("--json");
|
|
203
|
+
const targetIdx = argv.indexOf("--target");
|
|
204
|
+
const target =
|
|
205
|
+
targetIdx !== -1 && argv[targetIdx + 1] ? argv[targetIdx + 1] : null;
|
|
206
|
+
|
|
207
|
+
if (!target) {
|
|
208
|
+
console.error("Error: --target is required for check");
|
|
209
|
+
console.error("Usage: openuispec check --target <target> [--json]");
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const result = buildCheckResult(target);
|
|
214
|
+
|
|
215
|
+
if (isJson) {
|
|
216
|
+
console.log(JSON.stringify(result, null, 2));
|
|
217
|
+
} else {
|
|
218
|
+
printReport(result);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Exit codes: 0 = clean + ready, 2 = validation errors, 1 = config error
|
|
222
|
+
const totalErrors = result.validation.total_errors + result.semantic.total_errors;
|
|
223
|
+
if (totalErrors > 0) {
|
|
224
|
+
process.exit(2);
|
|
225
|
+
}
|
|
226
|
+
if (!result.prepare.ready) {
|
|
227
|
+
process.exit(2);
|
|
228
|
+
}
|
|
229
|
+
process.exit(0);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function printReport(result: CheckResult): void {
|
|
233
|
+
console.log("OpenUISpec Check");
|
|
234
|
+
console.log("================");
|
|
235
|
+
console.log(`Target: ${result.target}`);
|
|
236
|
+
|
|
237
|
+
const totalValidation = result.validation.total_errors;
|
|
238
|
+
const totalSemantic = result.semantic.total_errors;
|
|
239
|
+
|
|
240
|
+
console.log(`\nSchema validation: ${totalValidation === 0 ? "PASS" : `FAIL (${totalValidation} error(s))`}`);
|
|
241
|
+
if (totalValidation > 0) {
|
|
242
|
+
for (const group of result.validation.groups) {
|
|
243
|
+
if (group.errors.length > 0) {
|
|
244
|
+
console.log(` ${group.group}: ${group.errors.length} error(s)`);
|
|
245
|
+
for (const err of group.errors.slice(0, 3)) {
|
|
246
|
+
console.log(` [${err.file}] ${err.path}: ${err.message}`);
|
|
247
|
+
}
|
|
248
|
+
if (group.errors.length > 3) {
|
|
249
|
+
console.log(` ... and ${group.errors.length - 3} more`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
console.log(`Semantic lint: ${totalSemantic === 0 ? "PASS" : `FAIL (${totalSemantic} error(s))`}`);
|
|
256
|
+
if (totalSemantic > 0) {
|
|
257
|
+
for (const err of result.semantic.errors.slice(0, 5)) {
|
|
258
|
+
console.log(` [${err.path}] ${err.message}`);
|
|
259
|
+
}
|
|
260
|
+
if (result.semantic.errors.length > 5) {
|
|
261
|
+
console.log(` ... and ${result.semantic.errors.length - 5} more`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log(`\nPrepare readiness: ${result.prepare.ready ? "READY" : "NOT READY"}`);
|
|
266
|
+
console.log(` Mode: ${result.prepare.mode}`);
|
|
267
|
+
if (result.prepare.missing.length > 0) {
|
|
268
|
+
console.log(` Missing platform decisions: ${result.prepare.missing.join(", ")}`);
|
|
269
|
+
}
|
|
270
|
+
for (const w of result.prepare.warnings) {
|
|
271
|
+
console.log(` Warning: ${w}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const totalErrors = result.validation.total_errors + result.semantic.total_errors;
|
|
275
|
+
console.log(
|
|
276
|
+
`\nResult: ${totalErrors === 0 && result.prepare.ready ? "ALL CLEAR" : "ACTION NEEDED"}`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Direct execution
|
|
281
|
+
const isDirectRun =
|
|
282
|
+
process.argv[1]?.endsWith("check/index.ts") ||
|
|
283
|
+
process.argv[1]?.endsWith("check/index.js");
|
|
284
|
+
|
|
285
|
+
if (isDirectRun) {
|
|
286
|
+
runCheck(process.argv.slice(2));
|
|
287
|
+
}
|
package/cli/init.ts
CHANGED
|
@@ -261,12 +261,23 @@ Do NOT guess the file format — skipping this step will produce invalid YAML th
|
|
|
261
261
|
3. \`examples/taskflow/openuispec/\` — complete working example with all file types
|
|
262
262
|
4. \`schema/\` — JSON Schemas for validation
|
|
263
263
|
|
|
264
|
+
## MCP Tools (recommended for AI assistants)
|
|
265
|
+
|
|
266
|
+
When the openuispec MCP server is configured (see \`.claude.json\`), AI assistants should use these tools instead of CLI commands:
|
|
267
|
+
|
|
268
|
+
| Tool | When to use |
|
|
269
|
+
|------|-------------|
|
|
270
|
+
| \`openuispec_prepare\` | **Before any UI code generation.** Returns spec context, platform config, and constraints. |
|
|
271
|
+
| \`openuispec_check\` | After editing spec files. Validates schema + semantics + readiness. |
|
|
272
|
+
| \`openuispec_status\` | To understand cross-target state (baselines, drift, next steps). |
|
|
273
|
+
| \`openuispec_validate\` | Schema-only validation, optionally by group. |
|
|
274
|
+
| \`openuispec_drift\` | Detect spec changes since last snapshot. |
|
|
275
|
+
|
|
264
276
|
## CLI commands
|
|
265
277
|
|
|
266
278
|
\`\`\`bash
|
|
267
279
|
openuispec validate # Validate spec files against schemas
|
|
268
280
|
openuispec validate semantic # Run semantic cross-reference linting
|
|
269
|
-
openuispec validate screens # Validate only screens
|
|
270
281
|
openuispec configure-target ${targets[0]} [--defaults] # Configure target stack; --defaults stays unconfirmed
|
|
271
282
|
openuispec status # Show cross-target baseline/drift status
|
|
272
283
|
openuispec drift --target ${targets[0]} --explain # Explain semantic spec drift
|
|
@@ -298,111 +309,90 @@ ${RULES_START_MARKER}
|
|
|
298
309
|
# Spec files are the single source of truth for all UI across platforms.
|
|
299
310
|
# Targets: ${targetList}
|
|
300
311
|
|
|
301
|
-
##
|
|
312
|
+
## MANDATORY — UI work requires OpenUISpec tools
|
|
302
313
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
Do NOT guess the file format — skipping this step will produce invalid YAML that fails validation.
|
|
314
|
+
When the user's request involves UI — screens, navigation, layout, tokens, flows, localization,
|
|
315
|
+
or any visual/structural change — you MUST use the OpenUISpec tools before writing any code.
|
|
306
316
|
|
|
307
|
-
|
|
308
|
-
1. \`node_modules/openuispec/\` (project dependency)
|
|
309
|
-
2. Run \`npm root -g\` → \`<prefix>/openuispec/\` (global install)
|
|
310
|
-
3. Online: \`https://openuispec.rsteam.uz/llms-full.txt\` (if not installed)
|
|
317
|
+
### MCP Tools (use these when available)
|
|
311
318
|
|
|
312
|
-
|
|
313
|
-
1. \`README.md\` — schema tables, file format reference, root wrapper keys
|
|
314
|
-
2. \`spec/openuispec-v0.1.md\` — full specification (contracts, layout, expressions, adaptive, etc.)
|
|
315
|
-
3. \`examples/taskflow/openuispec/\` — complete working example with all file types
|
|
316
|
-
4. \`schema/\` — JSON Schemas for every file type
|
|
319
|
+
Call these MCP tools directly. They return structured JSON with everything you need.
|
|
317
320
|
|
|
318
|
-
|
|
319
|
-
|
|
321
|
+
1. **Before ANY UI code generation or modification:**
|
|
322
|
+
Call \`openuispec_prepare\` with the target platform. This returns the spec context,
|
|
323
|
+
platform config, generation constraints, and semantic changes. Do not skip this step.
|
|
320
324
|
|
|
321
|
-
|
|
322
|
-
|
|
325
|
+
2. **After editing spec files:**
|
|
326
|
+
Call \`openuispec_check\` to validate schema + semantic lint + prepare readiness.
|
|
323
327
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
- Manifest: \`${specDir}/openuispec.yaml\` — always read this first.
|
|
327
|
-
- Tokens: \`${specDir}/tokens/\`
|
|
328
|
-
- Screens: \`${specDir}/screens/\`
|
|
329
|
-
- Flows: \`${specDir}/flows/\`
|
|
330
|
-
- Contracts: \`${specDir}/contracts/\`
|
|
331
|
-
- Platform: \`${specDir}/platform/\`
|
|
332
|
-
- Locales: \`${specDir}/locales/\`
|
|
328
|
+
3. **To understand project state:**
|
|
329
|
+
Call \`openuispec_status\` for cross-target summary.
|
|
333
330
|
|
|
334
|
-
|
|
331
|
+
4. **To detect what changed:**
|
|
332
|
+
Call \`openuispec_drift\` with \`explain: true\` to see property-level spec changes.
|
|
335
333
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
1. **Read the spec first** — find and read \`spec/openuispec-v0.1.md\` from the installed package.
|
|
340
|
-
2. **Find existing screens** — scan the codebase for UI screen files.
|
|
341
|
-
3. **Create stubs** — for each screen, create \`${specDir}/screens/<name>.yaml\` with:
|
|
342
|
-
\`\`\`yaml
|
|
343
|
-
screen_name:
|
|
344
|
-
semantic: "Brief description of what this screen does"
|
|
345
|
-
status: stub
|
|
346
|
-
layout:
|
|
347
|
-
type: scroll_vertical
|
|
348
|
-
\`\`\`
|
|
349
|
-
4. **Extract tokens** — scan for colors, fonts, spacing and create files in \`${specDir}/tokens/\`.
|
|
350
|
-
5. **Update the manifest** — fill in \`data_model\`, \`api.endpoints\`, and \`generation.code_roots.backend\` in \`${specDir}/openuispec.yaml\`.
|
|
351
|
-
|
|
352
|
-
## OpenUISpec Source Of Truth
|
|
353
|
-
|
|
354
|
-
OpenUISpec spec files are the primary source of truth for UI behavior across platforms.
|
|
355
|
-
|
|
356
|
-
### Start from spec when:
|
|
357
|
-
- the request changes screen structure
|
|
358
|
-
- the request changes navigation
|
|
359
|
-
- the request changes fields, actions, validation, or data binding
|
|
360
|
-
- the request changes tokens, variants, contracts, flows, or localization
|
|
361
|
-
- the request affects more than one platform
|
|
362
|
-
- the request is phrased in product/UI terms rather than platform-code terms
|
|
363
|
-
|
|
364
|
-
Spec-first workflow:
|
|
365
|
-
1. Read \`${specDir}/openuispec.yaml\` and the relevant spec files first.
|
|
366
|
-
2. Update the spec first.
|
|
367
|
-
3. Update the affected generated/native UI code to match the spec.
|
|
368
|
-
4. Run \`openuispec validate\`.
|
|
369
|
-
5. Run \`openuispec validate semantic\`.
|
|
370
|
-
6. Run \`openuispec drift --target <target> --explain\` to inspect semantic changes since that target's baseline.
|
|
371
|
-
7. Run \`openuispec prepare --target <target>\` to build the target work bundle for that target. In \`bootstrap\` mode it provides first-generation constraints; in \`update\` mode it provides drift-based update scope.
|
|
372
|
-
If the target stack was filled from defaults, stop and ask the user to confirm or change it before implementation.
|
|
373
|
-
8. Verify the affected UI targets build/run if possible.
|
|
374
|
-
9. Only then run \`openuispec drift --snapshot --target <target>\` for affected targets, after that target output directory exists.
|
|
375
|
-
10. Run \`openuispec drift --target <target> --explain\` again to confirm no spec changes remain for that target.
|
|
376
|
-
11. Use \`openuispec status\` to see which other targets are still behind the updated spec.
|
|
377
|
-
|
|
378
|
-
### Start from platform code when:
|
|
379
|
-
- the change is platform-specific polish
|
|
380
|
-
- the change is a local bug fix that does not alter shared semantic behavior
|
|
381
|
-
- the request explicitly asks for an iOS-only, Android-only, or web-only adjustment
|
|
382
|
-
|
|
383
|
-
Platform-first workflow:
|
|
384
|
-
1. Update native/platform code.
|
|
385
|
-
2. If the change affects shared semantics, sync the spec afterward.
|
|
386
|
-
3. If the change is intentionally platform-specific, document it in \`platform/*.yaml\` when appropriate.
|
|
387
|
-
|
|
388
|
-
### Never do this:
|
|
389
|
-
- Do not snapshot drift immediately after changing spec unless the UI code has also been updated.
|
|
390
|
-
- Do not treat \`openuispec drift\` as proof that generated UI matches the spec.
|
|
391
|
-
- Do not skip \`--explain\` / \`prepare\` when another platform needs to catch up with shared spec changes.
|
|
392
|
-
- Do not modify generated UI without checking whether the spec must change first.
|
|
393
|
-
- Do not use \`configure-target --defaults\` as silent approval for implementation. Ask the user to confirm the stack first.
|
|
334
|
+
5. **For schema validation only:**
|
|
335
|
+
Call \`openuispec_validate\` optionally with specific groups.
|
|
394
336
|
|
|
395
|
-
|
|
337
|
+
### CLI fallback (when MCP is not available)
|
|
338
|
+
|
|
339
|
+
If MCP tools are not available, use these CLI commands with \`--json\` flag:
|
|
340
|
+
- \`openuispec prepare --target <t> --json\` — build AI-ready work bundle
|
|
341
|
+
- \`openuispec check --target <t> --json\` — composite validation
|
|
342
|
+
- \`openuispec status --json\` — cross-target status
|
|
343
|
+
- \`openuispec drift --target <t> --explain --json\` — semantic drift
|
|
344
|
+
- \`openuispec validate [group...] --json\` — schema validation
|
|
345
|
+
|
|
346
|
+
### Other CLI commands
|
|
396
347
|
- \`openuispec init\` — scaffold a new spec project
|
|
397
|
-
- \`openuispec
|
|
398
|
-
- \`openuispec
|
|
399
|
-
- \`openuispec drift --target <t>\` — check for spec drift
|
|
400
|
-
- \`openuispec drift --target <t> --explain\` — explain semantic spec drift since the target baseline
|
|
401
|
-
- \`openuispec drift --snapshot --target <t>\` — snapshot current state after the target output exists
|
|
402
|
-
- \`openuispec prepare --target <t>\` — build the target work bundle and check whether stack confirmation is still pending
|
|
403
|
-
- \`openuispec status\` — show cross-target baseline/drift status
|
|
348
|
+
- \`openuispec drift --snapshot --target <t>\` — snapshot current state (only after UI code is updated)
|
|
349
|
+
- \`openuispec configure-target <t>\` — configure target platform stack
|
|
404
350
|
- \`openuispec update-rules\` — update AI rules to match installed package version
|
|
405
|
-
|
|
351
|
+
|
|
352
|
+
## Spec format reference
|
|
353
|
+
|
|
354
|
+
The spec format, schemas, and generation rules are in the installed \`openuispec\` package.
|
|
355
|
+
You MUST read the reference files before creating or editing spec files — do NOT guess the format.
|
|
356
|
+
|
|
357
|
+
**Find the package:** \`node_modules/openuispec/\` or run \`npm root -g\` → \`<prefix>/openuispec/\`.
|
|
358
|
+
**Online fallback:** \`https://openuispec.rsteam.uz/llms-full.txt\`
|
|
359
|
+
|
|
360
|
+
**Reference files (read in order):**
|
|
361
|
+
1. \`README.md\` — schema tables, file format, root wrapper keys
|
|
362
|
+
2. \`spec/openuispec-v0.1.md\` — full specification
|
|
363
|
+
3. \`examples/taskflow/openuispec/\` — complete working example
|
|
364
|
+
4. \`schema/\` — JSON Schemas for every file type
|
|
365
|
+
|
|
366
|
+
## Spec location
|
|
367
|
+
- Spec root: \`${specDir}/\` — read \`${specDir}/openuispec.yaml\` first for actual paths.
|
|
368
|
+
- Default dirs: tokens/, screens/, flows/, contracts/, platform/, locales/
|
|
369
|
+
|
|
370
|
+
## When to start from spec vs platform code
|
|
371
|
+
|
|
372
|
+
**Spec-first** (use \`openuispec_prepare\` or \`openuispec prepare\`):
|
|
373
|
+
- Screen structure, navigation, fields, actions, validation, data binding changes
|
|
374
|
+
- Token, variant, contract, flow, or localization changes
|
|
375
|
+
- Changes affecting multiple platforms
|
|
376
|
+
- Requests in product/UI terms
|
|
377
|
+
|
|
378
|
+
**Platform-first** (skip spec tools):
|
|
379
|
+
- Platform-specific polish (iOS-only, Android-only, web-only)
|
|
380
|
+
- Local bug fixes that don't alter shared semantic behavior
|
|
381
|
+
|
|
382
|
+
## If spec directories are empty (first-time setup)
|
|
383
|
+
|
|
384
|
+
Read \`spec/openuispec-v0.1.md\` from the package first, then:
|
|
385
|
+
1. Scan codebase for UI screens → create \`${specDir}/screens/<name>.yaml\` as \`status: stub\`
|
|
386
|
+
2. Extract tokens (colors, fonts, spacing) → \`${specDir}/tokens/\`
|
|
387
|
+
3. Create contract extensions → \`${specDir}/contracts/\`
|
|
388
|
+
4. Create locale files → \`${specDir}/locales/<locale>.json\`
|
|
389
|
+
5. Fill in \`data_model\`, \`api.endpoints\` in \`${specDir}/openuispec.yaml\`
|
|
390
|
+
|
|
391
|
+
## Rules
|
|
392
|
+
- Do not snapshot drift unless the UI code has also been updated.
|
|
393
|
+
- Do not modify generated UI without checking whether the spec must change first.
|
|
394
|
+
- Do not use \`configure-target --defaults\` as silent approval — ask the user to confirm.
|
|
395
|
+
- Always read spec format from the installed package, not from cached/memorized content.
|
|
406
396
|
${RULES_END_MARKER}
|
|
407
397
|
`;
|
|
408
398
|
}
|
|
@@ -479,6 +469,30 @@ export function updateRules(): void {
|
|
|
479
469
|
} else {
|
|
480
470
|
console.log(`\nAI rules updated to v${version}`);
|
|
481
471
|
}
|
|
472
|
+
|
|
473
|
+
// Ensure MCP server is configured
|
|
474
|
+
const claudeJsonPath = join(cwd, ".claude.json");
|
|
475
|
+
const mcpConfig = {
|
|
476
|
+
command: "npx",
|
|
477
|
+
args: ["openuispec-mcp"],
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
let claudeJson: Record<string, any> = {};
|
|
482
|
+
try {
|
|
483
|
+
claudeJson = JSON.parse(readFileSync(claudeJsonPath, "utf-8"));
|
|
484
|
+
} catch {
|
|
485
|
+
// file doesn't exist or isn't valid JSON — start fresh
|
|
486
|
+
}
|
|
487
|
+
if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
|
|
488
|
+
if (!claudeJson.mcpServers.openuispec) {
|
|
489
|
+
claudeJson.mcpServers.openuispec = mcpConfig;
|
|
490
|
+
writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
491
|
+
console.log(` create .claude.json (MCP server configured)`);
|
|
492
|
+
}
|
|
493
|
+
} catch {
|
|
494
|
+
// non-critical — skip silently
|
|
495
|
+
}
|
|
482
496
|
}
|
|
483
497
|
|
|
484
498
|
/**
|
|
@@ -744,6 +758,33 @@ export async function init(argv: string[] = []): Promise<void> {
|
|
|
744
758
|
}
|
|
745
759
|
}
|
|
746
760
|
|
|
761
|
+
// ── MCP server configuration ────────────────────────────────────
|
|
762
|
+
|
|
763
|
+
const claudeJsonPath = join(cwd, ".claude.json");
|
|
764
|
+
const mcpConfig = {
|
|
765
|
+
command: "npx",
|
|
766
|
+
args: ["openuispec-mcp"],
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
try {
|
|
770
|
+
let claudeJson: Record<string, any> = {};
|
|
771
|
+
try {
|
|
772
|
+
claudeJson = JSON.parse(readFileSync(claudeJsonPath, "utf-8"));
|
|
773
|
+
} catch {
|
|
774
|
+
// file doesn't exist or isn't valid JSON — start fresh
|
|
775
|
+
}
|
|
776
|
+
if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
|
|
777
|
+
if (!claudeJson.mcpServers.openuispec) {
|
|
778
|
+
claudeJson.mcpServers.openuispec = mcpConfig;
|
|
779
|
+
writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
780
|
+
if (!quiet) console.log(` create .claude.json (MCP server configured)`);
|
|
781
|
+
} else {
|
|
782
|
+
if (!quiet) console.log(` skip .claude.json (openuispec MCP already configured)`);
|
|
783
|
+
}
|
|
784
|
+
} catch {
|
|
785
|
+
if (!quiet) console.log(` skip .claude.json (could not configure MCP server)`);
|
|
786
|
+
}
|
|
787
|
+
|
|
747
788
|
if (answers.configureTargets) {
|
|
748
789
|
if (!quiet) console.log("\nConfiguring target stacks...\n");
|
|
749
790
|
const { runConfigureTarget } = await import("./configure-target.js");
|
|
@@ -762,10 +803,14 @@ Done! Your spec project is ready at ./${answers.specDir}/
|
|
|
762
803
|
|
|
763
804
|
Getting started (new project):
|
|
764
805
|
1. Edit ${answers.specDir}/openuispec.yaml — define your data model and API
|
|
765
|
-
2. Create
|
|
766
|
-
3. Create
|
|
767
|
-
4.
|
|
768
|
-
5.
|
|
806
|
+
2. Create tokens in ${answers.specDir}/tokens/ (colors, typography, spacing, etc.)
|
|
807
|
+
3. Create contract extensions in ${answers.specDir}/contracts/ (visual variants for the 7 built-in contracts)
|
|
808
|
+
4. Create screens in ${answers.specDir}/screens/ (one YAML per screen)
|
|
809
|
+
5. Create flows in ${answers.specDir}/flows/ (multi-step navigation)
|
|
810
|
+
6. Create locale files in ${answers.specDir}/locales/ (one JSON per supported locale)
|
|
811
|
+
7. Run \`openuispec validate\` and \`openuispec validate semantic\` to check everything
|
|
812
|
+
8. Ask AI to generate native code from the spec
|
|
813
|
+
9. Run \`openuispec drift --snapshot --target ${answers.targets[0]}\` to baseline the first accepted target state after that target output directory exists
|
|
769
814
|
|
|
770
815
|
Getting started (existing project):
|
|
771
816
|
1. Ask AI to read your existing UI code and generate spec files:
|
|
@@ -785,6 +830,7 @@ Commands:
|
|
|
785
830
|
openuispec drift --snapshot --target ios Save current state + git baseline after target output exists
|
|
786
831
|
|
|
787
832
|
AI rules have been added to CLAUDE.md and AGENTS.md.
|
|
833
|
+
MCP server configured in .claude.json (AI assistants will use openuispec tools automatically).
|
|
788
834
|
|
|
789
835
|
Docs: https://openuispec.rsteam.uz
|
|
790
836
|
`);
|
package/drift/index.ts
CHANGED
|
@@ -137,8 +137,7 @@ export function hasStatusSemantics(relPath: string): boolean {
|
|
|
137
137
|
export function discoverSpecFiles(projectDir: string): string[] {
|
|
138
138
|
const manifest = join(projectDir, "openuispec.yaml");
|
|
139
139
|
if (!existsSync(manifest)) {
|
|
140
|
-
|
|
141
|
-
process.exit(1);
|
|
140
|
+
throw new Error(`No openuispec.yaml found in ${projectDir}`);
|
|
142
141
|
}
|
|
143
142
|
|
|
144
143
|
const doc = YAML.parse(readFileSync(manifest, "utf-8"));
|
|
@@ -433,11 +432,10 @@ export function findProjectDir(cwd: string): string {
|
|
|
433
432
|
return dir;
|
|
434
433
|
}
|
|
435
434
|
}
|
|
436
|
-
|
|
437
|
-
"
|
|
435
|
+
throw new Error(
|
|
436
|
+
"No openuispec.yaml found. " +
|
|
438
437
|
"Run from a directory containing openuispec.yaml or an openuispec/ subdirectory."
|
|
439
438
|
);
|
|
440
|
-
process.exit(1);
|
|
441
439
|
}
|
|
442
440
|
|
|
443
441
|
/** Read and parse the manifest YAML. */
|
|
@@ -655,8 +653,7 @@ export function loadTargetDrift(
|
|
|
655
653
|
const projectName = readProjectName(projectDir);
|
|
656
654
|
const statePath = stateFilePath(projectDir, projectName, target);
|
|
657
655
|
if (!existsSync(statePath)) {
|
|
658
|
-
|
|
659
|
-
process.exit(1);
|
|
656
|
+
throw new Error(missingSnapshotMessage(cwd, projectDir, projectName, target));
|
|
660
657
|
}
|
|
661
658
|
|
|
662
659
|
const state: StateFile = JSON.parse(readFileSync(statePath, "utf-8"));
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env -S npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* OpenUISpec MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Exposes OpenUISpec CLI commands as MCP tools so AI assistants
|
|
6
|
+
* can call them directly instead of relying on CLAUDE.md instructions.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx openuispec-mcp # stdio transport
|
|
10
|
+
* OPENUISPEC_PROJECT_DIR=/path npx openuispec-mcp # explicit project dir
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import { join, dirname } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { SUPPORTED_TARGETS } from "../drift/index.js";
|
|
20
|
+
import { buildPrepareResult } from "../prepare/index.js";
|
|
21
|
+
import { buildCheckResult } from "../check/index.js";
|
|
22
|
+
import { buildStatusResult } from "../status/index.js";
|
|
23
|
+
import { buildValidateResult } from "../schema/validate.js";
|
|
24
|
+
import { loadTargetDrift } from "../drift/index.js";
|
|
25
|
+
|
|
26
|
+
// ── resolve project cwd ──────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const projectCwd = process.env.OPENUISPEC_PROJECT_DIR || process.cwd();
|
|
29
|
+
|
|
30
|
+
// ── read package version ─────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function getPackageVersion(): string {
|
|
33
|
+
try {
|
|
34
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
36
|
+
return pkg.version ?? "unknown";
|
|
37
|
+
} catch {
|
|
38
|
+
return "unknown";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── shared tool helpers ──────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
const targetSchema = z.enum(SUPPORTED_TARGETS).describe("Target platform");
|
|
45
|
+
|
|
46
|
+
function formatError(err: unknown): string {
|
|
47
|
+
return err instanceof Error ? err.message : String(err);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toolResult(data: unknown): { content: [{ type: "text"; text: string }] } {
|
|
51
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function toolError(err: unknown): { content: [{ type: "text"; text: string }]; isError: true } {
|
|
55
|
+
return { content: [{ type: "text" as const, text: `Error: ${formatError(err)}` }], isError: true };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── create server ────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
const server = new McpServer(
|
|
61
|
+
{
|
|
62
|
+
name: "openuispec",
|
|
63
|
+
version: getPackageVersion(),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
instructions: `This project uses OpenUISpec — a semantic UI specification format.
|
|
67
|
+
Spec files (YAML) are the single source of truth for all UI across platforms.
|
|
68
|
+
|
|
69
|
+
MANDATORY WORKFLOW for any UI-related request (screens, navigation, layout, tokens, flows, localization):
|
|
70
|
+
1. BEFORE writing or modifying any UI code, call openuispec_prepare with the target platform.
|
|
71
|
+
This returns the spec context, platform config, and generation constraints you must follow.
|
|
72
|
+
2. If the request requires spec changes, update the spec files FIRST, then call openuispec_check to validate.
|
|
73
|
+
3. Only then generate or update the platform UI code based on the prepare output.
|
|
74
|
+
|
|
75
|
+
Skip these tools ONLY when the request is purely non-UI (API logic, database, infrastructure, etc.)
|
|
76
|
+
or explicitly platform-specific polish that doesn't affect shared UI semantics.`,
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// ── tool: openuispec_prepare ─────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
server.registerTool(
|
|
83
|
+
"openuispec_prepare",
|
|
84
|
+
{
|
|
85
|
+
description: "Build AI-ready work bundle for a target platform. REQUIRED before any UI code generation. Returns spec context, platform config, semantic changes, and generation constraints.",
|
|
86
|
+
inputSchema: { target: targetSchema },
|
|
87
|
+
},
|
|
88
|
+
async ({ target }) => {
|
|
89
|
+
try {
|
|
90
|
+
return toolResult(buildPrepareResult(target, projectCwd));
|
|
91
|
+
} catch (err) {
|
|
92
|
+
return toolError(err);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// ── tool: openuispec_check ───────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
server.registerTool(
|
|
100
|
+
"openuispec_check",
|
|
101
|
+
{
|
|
102
|
+
description: "Run composite validation: schema validation, semantic linting, and prepare readiness check. Call after spec edits to verify correctness.",
|
|
103
|
+
inputSchema: { target: targetSchema },
|
|
104
|
+
},
|
|
105
|
+
async ({ target }) => {
|
|
106
|
+
try {
|
|
107
|
+
return toolResult(buildCheckResult(target, projectCwd));
|
|
108
|
+
} catch (err) {
|
|
109
|
+
return toolError(err);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// ── tool: openuispec_status ──────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
server.registerTool(
|
|
117
|
+
"openuispec_status",
|
|
118
|
+
{
|
|
119
|
+
description: "Show cross-target status summary: baseline, drift, and recommended next steps for all configured targets. Good starting point to understand project state.",
|
|
120
|
+
},
|
|
121
|
+
async () => {
|
|
122
|
+
try {
|
|
123
|
+
return toolResult(buildStatusResult(projectCwd));
|
|
124
|
+
} catch (err) {
|
|
125
|
+
return toolError(err);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// ── tool: openuispec_validate ────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
server.registerTool(
|
|
133
|
+
"openuispec_validate",
|
|
134
|
+
{
|
|
135
|
+
description: "Validate spec files against JSON schemas. Returns validation errors grouped by type (manifest, tokens, screens, flows, platform, locales, contracts, semantic).",
|
|
136
|
+
inputSchema: {
|
|
137
|
+
groups: z
|
|
138
|
+
.array(z.enum(["manifest", "tokens", "screens", "flows", "platform", "locales", "contracts", "semantic"]))
|
|
139
|
+
.optional()
|
|
140
|
+
.describe("Specific groups to validate. If omitted, validates all groups."),
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
async ({ groups }) => {
|
|
144
|
+
try {
|
|
145
|
+
return toolResult(buildValidateResult(groups, projectCwd));
|
|
146
|
+
} catch (err) {
|
|
147
|
+
return toolError(err);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// ── tool: openuispec_drift ───────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
server.registerTool(
|
|
155
|
+
"openuispec_drift",
|
|
156
|
+
{
|
|
157
|
+
description: "Detect spec drift since last snapshot. Shows which spec files changed, were added, or removed. Use explain to see property-level changes.",
|
|
158
|
+
inputSchema: {
|
|
159
|
+
target: targetSchema,
|
|
160
|
+
explain: z.boolean().optional().default(false).describe("Include semantic explanation of changes"),
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
async ({ target, explain }) => {
|
|
164
|
+
try {
|
|
165
|
+
const { result } = loadTargetDrift(projectCwd, target, false, explain);
|
|
166
|
+
return toolResult(result);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
return toolError(err);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// ── start server ─────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
async function main() {
|
|
176
|
+
const transport = new StdioServerTransport();
|
|
177
|
+
await server.connect(transport);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
main().catch((err) => {
|
|
181
|
+
console.error("Failed to start OpenUISpec MCP server:", err);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openuispec",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.35",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A semantic UI specification format for AI-native, platform-native app development",
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"cli/",
|
|
14
|
+
"check/",
|
|
14
15
|
"drift/",
|
|
16
|
+
"mcp-server/",
|
|
15
17
|
"prepare/",
|
|
16
18
|
"status/",
|
|
17
19
|
"schema/",
|
|
@@ -22,7 +24,8 @@
|
|
|
22
24
|
"LICENSE"
|
|
23
25
|
],
|
|
24
26
|
"bin": {
|
|
25
|
-
"openuispec": "./cli/index.ts"
|
|
27
|
+
"openuispec": "./cli/index.ts",
|
|
28
|
+
"openuispec-mcp": "./mcp-server/index.ts"
|
|
26
29
|
},
|
|
27
30
|
"scripts": {
|
|
28
31
|
"test": "node --import tsx --test tests/*.test.ts",
|
|
@@ -40,6 +43,7 @@
|
|
|
40
43
|
"postinstall": "echo \"\\n ✓ openuispec installed — if upgrading, run: openuispec update-rules\\n\""
|
|
41
44
|
},
|
|
42
45
|
"dependencies": {
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
43
47
|
"ajv": "^8.17.1",
|
|
44
48
|
"ajv-formats": "^3.0.1",
|
|
45
49
|
"tsx": "^4.19.4",
|
package/prepare/index.ts
CHANGED
|
@@ -116,7 +116,7 @@ interface PrepareBootstrapBundle {
|
|
|
116
116
|
reference_examples: string[];
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
interface PrepareResult {
|
|
119
|
+
export interface PrepareResult {
|
|
120
120
|
mode: "bootstrap" | "update";
|
|
121
121
|
project: string;
|
|
122
122
|
target: string;
|
|
@@ -1180,8 +1180,7 @@ function buildUpdatePrepareResult(cwd: string, target: string): PrepareResult {
|
|
|
1180
1180
|
};
|
|
1181
1181
|
}
|
|
1182
1182
|
|
|
1183
|
-
function buildPrepareResult(target: string): PrepareResult {
|
|
1184
|
-
const cwd = process.cwd();
|
|
1183
|
+
export function buildPrepareResult(target: string, cwd: string = process.cwd()): PrepareResult {
|
|
1185
1184
|
const projectDir = findProjectDir(cwd);
|
|
1186
1185
|
const projectName = readProjectName(projectDir);
|
|
1187
1186
|
const outputDir = resolveOutputDir(projectDir, projectName, target);
|
package/schema/validate.ts
CHANGED
|
@@ -755,6 +755,36 @@ function findProjectDir(cwd: string): string {
|
|
|
755
755
|
export { buildAjv, readIncludes, GROUPS };
|
|
756
756
|
export type { JsonGroupResult, JsonError };
|
|
757
757
|
|
|
758
|
+
interface ValidateResult {
|
|
759
|
+
total_errors: number;
|
|
760
|
+
groups: JsonGroupResult[];
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
export function buildValidateResult(
|
|
764
|
+
groups?: string[],
|
|
765
|
+
cwd: string = process.cwd(),
|
|
766
|
+
): ValidateResult {
|
|
767
|
+
const projectDir = findProjectDir(cwd);
|
|
768
|
+
const includes = readIncludes(projectDir);
|
|
769
|
+
const ajv = buildAjv();
|
|
770
|
+
|
|
771
|
+
const keys =
|
|
772
|
+
groups && groups.length > 0
|
|
773
|
+
? groups.filter((k) => k in GROUPS)
|
|
774
|
+
: Object.keys(GROUPS);
|
|
775
|
+
|
|
776
|
+
const results: JsonGroupResult[] = [];
|
|
777
|
+
let totalErrors = 0;
|
|
778
|
+
|
|
779
|
+
for (const key of keys) {
|
|
780
|
+
const result = GROUPS[key].collectJson(ajv, projectDir, includes, key);
|
|
781
|
+
results.push(result);
|
|
782
|
+
totalErrors += result.errors.length;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return { total_errors: totalErrors, groups: results };
|
|
786
|
+
}
|
|
787
|
+
|
|
758
788
|
export function runValidate(argv: string[]): void {
|
|
759
789
|
const jsonMode = argv.includes("--json");
|
|
760
790
|
const filteredArgs = argv.filter((a) => a !== "--json");
|
package/status/index.ts
CHANGED
|
@@ -45,7 +45,7 @@ interface TargetStatus {
|
|
|
45
45
|
note?: string;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
interface StatusResult {
|
|
48
|
+
export interface StatusResult {
|
|
49
49
|
project: string;
|
|
50
50
|
targets: TargetStatus[];
|
|
51
51
|
}
|
|
@@ -141,8 +141,7 @@ function buildTargetStatus(cwd: string, projectDir: string, projectName: string,
|
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
function buildStatusResult(): StatusResult {
|
|
145
|
-
const cwd = process.cwd();
|
|
144
|
+
export function buildStatusResult(cwd: string = process.cwd()): StatusResult {
|
|
146
145
|
const projectDir = findProjectDir(cwd);
|
|
147
146
|
const projectName = readProjectName(projectDir);
|
|
148
147
|
const targets = allTargets(projectDir, projectName).map((target) =>
|
|
@@ -187,7 +186,7 @@ function printReport(result: StatusResult): void {
|
|
|
187
186
|
|
|
188
187
|
export function runStatus(argv: string[]): void {
|
|
189
188
|
const isJson = argv.includes("--json");
|
|
190
|
-
const result = buildStatusResult();
|
|
189
|
+
const result = buildStatusResult(process.cwd());
|
|
191
190
|
|
|
192
191
|
if (isJson) {
|
|
193
192
|
console.log(JSON.stringify(result, null, 2));
|