openuispec 0.1.45 → 0.1.47
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 +17 -5
- package/cli/init.ts +21 -3
- package/examples/social-app/.mcp.json +10 -0
- package/examples/social-app/AGENTS.md +114 -0
- package/examples/social-app/CLAUDE.md +114 -0
- package/examples/social-app/README.md +19 -0
- package/examples/social-app/backend/.gitkeep +1 -0
- package/examples/social-app/generated/android/social-app/app/build.gradle.kts +92 -0
- package/examples/social-app/generated/android/social-app/app/src/main/AndroidManifest.xml +26 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/AppContainer.kt +20 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/MainActivity.kt +35 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/SocialAppApplication.kt +13 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/MockData.kt +98 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/AppPreferences.kt +19 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/DataStorePreferencesRepository.kt +68 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/PreferencesRepository.kt +15 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/model/Models.kt +34 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/MainShell.kt +390 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/Components.kt +234 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/ContractPrimitives.kt +641 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/navigation/RootComponent.kt +113 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ChatDetailScreen.kt +212 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/CreatePostScreen.kt +113 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/DiscoverScreen.kt +137 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/EditProfileScreen.kt +180 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/HomeFeedScreen.kt +157 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/MessagesInboxScreen.kt +85 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/NotificationsScreen.kt +74 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/PostDetailScreen.kt +293 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ProfileSelfScreen.kt +116 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SearchResultsScreen.kt +161 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsScreen.kt +164 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsStore.kt +95 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/UserProfileScreen.kt +123 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Color.kt +33 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Shape.kt +41 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Spacing.kt +20 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Theme.kt +82 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Type.kt +60 -0
- package/examples/social-app/generated/android/social-app/app/src/main/res/drawable/ic_launcher_foreground.xml +9 -0
- package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
- package/examples/social-app/generated/android/social-app/app/src/main/res/values/strings.xml +91 -0
- package/examples/social-app/generated/android/social-app/app/src/main/res/values/themes.xml +10 -0
- package/examples/social-app/generated/android/social-app/app/src/main/res/values-ru/strings.xml +79 -0
- package/examples/social-app/generated/android/social-app/app/src/main/res/values-uz/strings.xml +79 -0
- package/examples/social-app/generated/android/social-app/app/src/main/xml/AndroidManifest.xml +23 -0
- package/examples/social-app/generated/android/social-app/build.gradle.kts +6 -0
- package/examples/social-app/generated/android/social-app/gradle/libs.versions.toml +48 -0
- package/examples/social-app/generated/android/social-app/gradle/wrapper/gradle-wrapper.properties +8 -0
- package/examples/social-app/generated/android/social-app/gradle.properties +11 -0
- package/examples/social-app/generated/android/social-app/gradlew +25 -0
- package/examples/social-app/generated/android/social-app/settings.gradle.kts +23 -0
- package/examples/social-app/generated/web/social-app/index.html +12 -0
- package/examples/social-app/generated/web/social-app/package-lock.json +2517 -0
- package/examples/social-app/generated/web/social-app/package.json +27 -0
- package/examples/social-app/generated/web/social-app/src/app/App.tsx +58 -0
- package/examples/social-app/generated/web/social-app/src/components/Shell.tsx +259 -0
- package/examples/social-app/generated/web/social-app/src/components/cards.tsx +317 -0
- package/examples/social-app/generated/web/social-app/src/components/ui.tsx +340 -0
- package/examples/social-app/generated/web/social-app/src/flows/CreatePostFlow.tsx +86 -0
- package/examples/social-app/generated/web/social-app/src/i18n.tsx +59 -0
- package/examples/social-app/generated/web/social-app/src/lib/icons.tsx +85 -0
- package/examples/social-app/generated/web/social-app/src/lib/tokens.ts +70 -0
- package/examples/social-app/generated/web/social-app/src/lib/utils.ts +97 -0
- package/examples/social-app/generated/web/social-app/src/locales/en.json +67 -0
- package/examples/social-app/generated/web/social-app/src/locales/ru.json +67 -0
- package/examples/social-app/generated/web/social-app/src/locales/uz.json +67 -0
- package/examples/social-app/generated/web/social-app/src/main.tsx +16 -0
- package/examples/social-app/generated/web/social-app/src/screens/ChatDetailScreen.tsx +90 -0
- package/examples/social-app/generated/web/social-app/src/screens/DiscoverScreen.tsx +86 -0
- package/examples/social-app/generated/web/social-app/src/screens/EditProfileScreen.tsx +57 -0
- package/examples/social-app/generated/web/social-app/src/screens/HomeFeedScreen.tsx +103 -0
- package/examples/social-app/generated/web/social-app/src/screens/MessagesInboxScreen.tsx +52 -0
- package/examples/social-app/generated/web/social-app/src/screens/NotificationsScreen.tsx +41 -0
- package/examples/social-app/generated/web/social-app/src/screens/PostDetailScreen.tsx +115 -0
- package/examples/social-app/generated/web/social-app/src/screens/ProfileSelfScreen.tsx +57 -0
- package/examples/social-app/generated/web/social-app/src/screens/ProfileUserScreen.tsx +76 -0
- package/examples/social-app/generated/web/social-app/src/screens/SearchResultsScreen.tsx +96 -0
- package/examples/social-app/generated/web/social-app/src/screens/SettingsScreen.tsx +79 -0
- package/examples/social-app/generated/web/social-app/src/state/store.ts +592 -0
- package/examples/social-app/generated/web/social-app/src/styles.css +124 -0
- package/examples/social-app/generated/web/social-app/src/vite-env.d.ts +1 -0
- package/examples/social-app/generated/web/social-app/tsconfig.json +22 -0
- package/examples/social-app/generated/web/social-app/tsconfig.node.json +13 -0
- package/examples/social-app/generated/web/social-app/tsconfig.node.tsbuildinfo +1 -0
- package/examples/social-app/generated/web/social-app/tsconfig.tsbuildinfo +1 -0
- package/examples/social-app/generated/web/social-app/vite.config.d.ts +2 -0
- package/examples/social-app/generated/web/social-app/vite.config.js +6 -0
- package/examples/social-app/generated/web/social-app/vite.config.ts +7 -0
- package/examples/social-app/openuispec/README.md +56 -0
- package/examples/social-app/openuispec/contracts/.gitkeep +0 -0
- package/examples/social-app/openuispec/contracts/action_trigger.yaml +104 -0
- package/examples/social-app/openuispec/contracts/collection.yaml +43 -0
- package/examples/social-app/openuispec/contracts/data_display.yaml +47 -0
- package/examples/social-app/openuispec/contracts/feedback.yaml +49 -0
- package/examples/social-app/openuispec/contracts/input_field.yaml +41 -0
- package/examples/social-app/openuispec/contracts/nav_container.yaml +34 -0
- package/examples/social-app/openuispec/contracts/surface.yaml +41 -0
- package/examples/social-app/openuispec/flows/.gitkeep +0 -0
- package/examples/social-app/openuispec/flows/create_post.yaml +66 -0
- package/examples/social-app/openuispec/locales/.gitkeep +0 -0
- package/examples/social-app/openuispec/locales/en.json +67 -0
- package/examples/social-app/openuispec/locales/ru.json +67 -0
- package/examples/social-app/openuispec/locales/uz.json +67 -0
- package/examples/social-app/openuispec/openuispec.yaml +214 -0
- package/examples/social-app/openuispec/platform/.gitkeep +0 -0
- package/examples/social-app/openuispec/platform/android.yaml +30 -0
- package/examples/social-app/openuispec/platform/ios.yaml +19 -0
- package/examples/social-app/openuispec/platform/web.yaml +23 -0
- package/examples/social-app/openuispec/screens/.gitkeep +0 -0
- package/examples/social-app/openuispec/screens/chat_detail.yaml +53 -0
- package/examples/social-app/openuispec/screens/discover.yaml +78 -0
- package/examples/social-app/openuispec/screens/edit_profile.yaml +78 -0
- package/examples/social-app/openuispec/screens/home_feed.yaml +138 -0
- package/examples/social-app/openuispec/screens/messages_inbox.yaml +43 -0
- package/examples/social-app/openuispec/screens/notifications.yaml +29 -0
- package/examples/social-app/openuispec/screens/post_detail.yaml +86 -0
- package/examples/social-app/openuispec/screens/profile_self.yaml +53 -0
- package/examples/social-app/openuispec/screens/profile_user.yaml +60 -0
- package/examples/social-app/openuispec/screens/search_results.yaml +62 -0
- package/examples/social-app/openuispec/screens/settings.yaml +99 -0
- package/examples/social-app/openuispec/tokens/.gitkeep +0 -0
- package/examples/social-app/openuispec/tokens/color.yaml +76 -0
- package/examples/social-app/openuispec/tokens/elevation.yaml +31 -0
- package/examples/social-app/openuispec/tokens/icons.yaml +147 -0
- package/examples/social-app/openuispec/tokens/layout.yaml +37 -0
- package/examples/social-app/openuispec/tokens/motion.yaml +28 -0
- package/examples/social-app/openuispec/tokens/spacing.yaml +19 -0
- package/examples/social-app/openuispec/tokens/themes.yaml +31 -0
- package/examples/social-app/openuispec/tokens/typography.yaml +50 -0
- package/examples/social-app/package.json +12 -0
- package/examples/taskflow/openuispec/openuispec.yaml +2 -0
- package/examples/todo-orbit/openuispec/openuispec.yaml +2 -0
- package/mcp-server/index.ts +200 -10
- package/package.json +1 -1
- package/schema/openuispec.schema.json +7 -0
- package/spec/openuispec-v0.1.md +13 -0
package/mcp-server/index.ts
CHANGED
|
@@ -42,6 +42,12 @@ function getPackageVersion(): string {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// ── spec directory resolver ─────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
function resolveSpecDir(projectDir: string, manifest: any, key: string): string {
|
|
48
|
+
return resolve(projectDir, manifest.includes?.[key] ?? `./${key}/`);
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
// ── shared tool helpers ──────────────────────────────────────────────
|
|
46
52
|
|
|
47
53
|
const targetSchema = z.enum(SUPPORTED_TARGETS).describe("Target platform");
|
|
@@ -60,7 +66,7 @@ function toolError(err: unknown): { content: [{ type: "text"; text: string }]; i
|
|
|
60
66
|
|
|
61
67
|
// ── create server ────────────────────────────────────────────────────
|
|
62
68
|
|
|
63
|
-
const server = new McpServer(
|
|
69
|
+
export const server = new McpServer(
|
|
64
70
|
{
|
|
65
71
|
name: "openuispec",
|
|
66
72
|
version: getPackageVersion(),
|
|
@@ -97,6 +103,14 @@ When you need to create or edit spec files and are unsure of the format:
|
|
|
97
103
|
2. Call openuispec_spec_schema with the specific type to get the full JSON schema.
|
|
98
104
|
3. Write the spec file following the schema exactly.
|
|
99
105
|
|
|
106
|
+
FOCUSED GETTERS (prefer these for incremental edits over read_specs):
|
|
107
|
+
- openuispec_get_screen(name) — single screen spec
|
|
108
|
+
- openuispec_get_contract(name, variant?) — single contract, optionally one variant
|
|
109
|
+
- openuispec_get_tokens(category) — single token category (color, typography, spacing, etc.)
|
|
110
|
+
- openuispec_get_locale(locale, keys?) — single locale file, optionally filtered keys
|
|
111
|
+
- openuispec_check(target, screens?, contracts?) — scoped audit for specific screens/contracts
|
|
112
|
+
Use read_specs for full-project generation; use focused getters when editing one screen or contract.
|
|
113
|
+
|
|
100
114
|
Skip these tools ONLY when the request is purely non-UI (API logic, database, infrastructure, etc.)
|
|
101
115
|
or explicitly platform-specific polish that doesn't affect shared UI semantics.`,
|
|
102
116
|
}
|
|
@@ -121,7 +135,7 @@ server.registerTool(
|
|
|
121
135
|
|
|
122
136
|
// ── tool: openuispec_check ───────────────────────────────────────────
|
|
123
137
|
|
|
124
|
-
function buildAuditChecklist(projectDir: string, target: string): string {
|
|
138
|
+
function buildAuditChecklist(projectDir: string, target: string, screenFilter?: string[], contractFilter?: string[]): string {
|
|
125
139
|
const lines: string[] = [
|
|
126
140
|
"POST-GENERATION AUDIT — verify your code against these concrete spec requirements:",
|
|
127
141
|
"",
|
|
@@ -133,7 +147,7 @@ function buildAuditChecklist(projectDir: string, target: string): string {
|
|
|
133
147
|
|
|
134
148
|
// Extract must_handle from contracts
|
|
135
149
|
const manifest = YAML.parse(fsReadFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
|
|
136
|
-
const contractsDir =
|
|
150
|
+
const contractsDir = resolveSpecDir(projectDir, manifest, "contracts");
|
|
137
151
|
|
|
138
152
|
if (existsSync(contractsDir)) {
|
|
139
153
|
lines.push("## Contract must_handle requirements");
|
|
@@ -141,6 +155,7 @@ function buildAuditChecklist(projectDir: string, target: string): string {
|
|
|
141
155
|
try {
|
|
142
156
|
const content = YAML.parse(fsReadFileSync(join(contractsDir, file), "utf-8"));
|
|
143
157
|
const contractName = Object.keys(content)[0];
|
|
158
|
+
if (contractFilter && !contractFilter.includes(contractName)) continue;
|
|
144
159
|
const contract = content[contractName];
|
|
145
160
|
if (!contract?.variants) continue;
|
|
146
161
|
|
|
@@ -168,13 +183,14 @@ function buildAuditChecklist(projectDir: string, target: string): string {
|
|
|
168
183
|
}
|
|
169
184
|
|
|
170
185
|
// Extract screens and their sections
|
|
171
|
-
const screensDir =
|
|
186
|
+
const screensDir = resolveSpecDir(projectDir, manifest, "screens");
|
|
172
187
|
if (existsSync(screensDir)) {
|
|
173
188
|
lines.push("## Screens — verify all sections exist in generated code");
|
|
174
189
|
for (const file of readdirSync(screensDir).filter(f => f.endsWith(".yaml")).sort()) {
|
|
175
190
|
try {
|
|
176
191
|
const content = YAML.parse(fsReadFileSync(join(screensDir, file), "utf-8"));
|
|
177
192
|
const screenName = Object.keys(content)[0];
|
|
193
|
+
if (screenFilter && !screenFilter.includes(screenName)) continue;
|
|
178
194
|
const screen = content[screenName];
|
|
179
195
|
if (screen?.status === "stub") continue;
|
|
180
196
|
|
|
@@ -212,7 +228,7 @@ function buildAuditChecklist(projectDir: string, target: string): string {
|
|
|
212
228
|
}
|
|
213
229
|
|
|
214
230
|
// Locale keys count
|
|
215
|
-
const localesDir =
|
|
231
|
+
const localesDir = resolveSpecDir(projectDir, manifest, "locales");
|
|
216
232
|
if (existsSync(localesDir)) {
|
|
217
233
|
const localeFiles = readdirSync(localesDir).filter(f => f.endsWith(".json"));
|
|
218
234
|
if (localeFiles.length > 0) {
|
|
@@ -228,7 +244,7 @@ function buildAuditChecklist(projectDir: string, target: string): string {
|
|
|
228
244
|
}
|
|
229
245
|
|
|
230
246
|
// Platform-specific checks
|
|
231
|
-
const platformDir =
|
|
247
|
+
const platformDir = resolveSpecDir(projectDir, manifest, "platform");
|
|
232
248
|
const platformPath = join(platformDir, `${target}.yaml`);
|
|
233
249
|
if (existsSync(platformPath)) {
|
|
234
250
|
try {
|
|
@@ -254,14 +270,20 @@ function buildAuditChecklist(projectDir: string, target: string): string {
|
|
|
254
270
|
server.registerTool(
|
|
255
271
|
"openuispec_check",
|
|
256
272
|
{
|
|
257
|
-
description: "Run composite validation + post-generation audit. Returns schema validation results AND a concrete audit checklist derived from your spec files — listing every contract must_handle item, every screen section, and every locale file that must exist in your generated code. Verify each item.",
|
|
258
|
-
inputSchema: {
|
|
273
|
+
description: "Run composite validation + post-generation audit. Returns schema validation results AND a concrete audit checklist derived from your spec files — listing every contract must_handle item, every screen section, and every locale file that must exist in your generated code. Verify each item. Use optional screens/contracts params to scope the audit to specific items (validation still runs on all files).",
|
|
274
|
+
inputSchema: {
|
|
275
|
+
target: targetSchema,
|
|
276
|
+
screens: z.array(z.string()).optional().describe("Screen names to audit (e.g. ['home_feed', 'settings']). If omitted, audits all screens."),
|
|
277
|
+
contracts: z.array(z.string()).optional().describe("Contract names to audit (e.g. ['action_trigger']). If omitted, audits all contracts."),
|
|
278
|
+
},
|
|
259
279
|
},
|
|
260
|
-
async ({ target }) => {
|
|
280
|
+
async ({ target, screens, contracts }) => {
|
|
261
281
|
try {
|
|
262
282
|
const result = buildCheckResult(target, projectCwd);
|
|
263
283
|
const projectDir = findProjectDir(projectCwd);
|
|
264
|
-
const
|
|
284
|
+
const screenFilter = screens && screens.length > 0 ? screens : undefined;
|
|
285
|
+
const contractFilter = contracts && contracts.length > 0 ? contracts : undefined;
|
|
286
|
+
const audit = buildAuditChecklist(projectDir, target, screenFilter, contractFilter);
|
|
265
287
|
return {
|
|
266
288
|
content: [
|
|
267
289
|
{ type: "text" as const, text: JSON.stringify(result, null, 2) },
|
|
@@ -432,6 +454,174 @@ server.registerTool(
|
|
|
432
454
|
}
|
|
433
455
|
);
|
|
434
456
|
|
|
457
|
+
// ── tool: openuispec_get_screen ──────────────────────────────────────
|
|
458
|
+
|
|
459
|
+
server.registerTool(
|
|
460
|
+
"openuispec_get_screen",
|
|
461
|
+
{
|
|
462
|
+
description: "Get the parsed content of a single screen spec file. Faster than read_specs when you only need one screen.",
|
|
463
|
+
inputSchema: {
|
|
464
|
+
name: z.string().describe("Screen name, e.g. 'home_feed' (matches filename without .yaml)"),
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
async ({ name }) => {
|
|
468
|
+
try {
|
|
469
|
+
const projectDir = findProjectDir(projectCwd);
|
|
470
|
+
const manifest = YAML.parse(fsReadFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
|
|
471
|
+
const screensDir = resolveSpecDir(projectDir, manifest, "screens");
|
|
472
|
+
const filePath = join(screensDir, `${name}.yaml`);
|
|
473
|
+
if (!existsSync(filePath)) {
|
|
474
|
+
return toolError(`Screen "${name}" not found. Expected file: ${filePath}`);
|
|
475
|
+
}
|
|
476
|
+
const content = fsReadFileSync(filePath, "utf-8");
|
|
477
|
+
return toolResult({ name, path: relative(projectDir, filePath), content });
|
|
478
|
+
} catch (err) {
|
|
479
|
+
return toolError(err);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
// ── tool: openuispec_get_contract ───────────────────────────────────
|
|
485
|
+
|
|
486
|
+
server.registerTool(
|
|
487
|
+
"openuispec_get_contract",
|
|
488
|
+
{
|
|
489
|
+
description: "Get a single contract spec, optionally filtered to one variant. Faster than read_specs when you only need one contract.",
|
|
490
|
+
inputSchema: {
|
|
491
|
+
name: z.string().describe("Contract name, e.g. 'action_trigger'"),
|
|
492
|
+
variant: z.string().optional().describe("Optional variant name, e.g. 'fab'. If given, returns only that variant's definition."),
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
async ({ name, variant }) => {
|
|
496
|
+
try {
|
|
497
|
+
const projectDir = findProjectDir(projectCwd);
|
|
498
|
+
const manifest = YAML.parse(fsReadFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
|
|
499
|
+
const contractsDir = resolveSpecDir(projectDir, manifest, "contracts");
|
|
500
|
+
|
|
501
|
+
if (!existsSync(contractsDir)) {
|
|
502
|
+
return toolError(`Contracts directory not found: ${contractsDir}`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Scan contract files for the matching contract key
|
|
506
|
+
for (const file of readdirSync(contractsDir).filter(f => f.endsWith(".yaml")).sort()) {
|
|
507
|
+
const filePath = join(contractsDir, file);
|
|
508
|
+
const raw = fsReadFileSync(filePath, "utf-8");
|
|
509
|
+
const content = YAML.parse(raw);
|
|
510
|
+
const contractName = Object.keys(content)[0];
|
|
511
|
+
if (contractName !== name) continue;
|
|
512
|
+
|
|
513
|
+
if (variant) {
|
|
514
|
+
const contract = content[contractName];
|
|
515
|
+
const variantDef = contract?.variants?.[variant];
|
|
516
|
+
if (!variantDef) {
|
|
517
|
+
return toolError(`Variant "${variant}" not found in contract "${name}". Available variants: ${Object.keys(contract?.variants ?? {}).join(", ")}`);
|
|
518
|
+
}
|
|
519
|
+
return toolResult({ name, variant, definition: variantDef });
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return toolResult({ name, path: relative(projectDir, filePath), content: raw });
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return toolError(`Contract "${name}" not found in ${contractsDir}`);
|
|
526
|
+
} catch (err) {
|
|
527
|
+
return toolError(err);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
// ── tool: openuispec_get_tokens ─────────────────────────────────────
|
|
533
|
+
|
|
534
|
+
server.registerTool(
|
|
535
|
+
"openuispec_get_tokens",
|
|
536
|
+
{
|
|
537
|
+
description: "Get tokens for a specific category (color, typography, spacing, elevation, motion, layout, themes, icons). Faster than read_specs when you only need one token file.",
|
|
538
|
+
inputSchema: {
|
|
539
|
+
category: z.string().describe("Token category, e.g. 'color', 'typography', 'spacing', 'elevation', 'motion', 'layout', 'themes', 'icons'"),
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
async ({ category }) => {
|
|
543
|
+
try {
|
|
544
|
+
const projectDir = findProjectDir(projectCwd);
|
|
545
|
+
const manifest = YAML.parse(fsReadFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
|
|
546
|
+
const tokensDir = resolveSpecDir(projectDir, manifest, "tokens");
|
|
547
|
+
|
|
548
|
+
if (!existsSync(tokensDir)) {
|
|
549
|
+
return toolError(`Tokens directory not found: ${tokensDir}`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Try exact match first, then scan for files containing the category name
|
|
553
|
+
const candidates = [
|
|
554
|
+
`${category}.yaml`,
|
|
555
|
+
`${category}.yml`,
|
|
556
|
+
];
|
|
557
|
+
|
|
558
|
+
for (const candidate of candidates) {
|
|
559
|
+
const filePath = join(tokensDir, candidate);
|
|
560
|
+
if (existsSync(filePath)) {
|
|
561
|
+
const content = fsReadFileSync(filePath, "utf-8");
|
|
562
|
+
return toolResult({ category, path: relative(projectDir, filePath), content });
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// List available token files for helpful error
|
|
567
|
+
const available = readdirSync(tokensDir)
|
|
568
|
+
.filter(f => f.endsWith(".yaml") || f.endsWith(".yml"))
|
|
569
|
+
.map(f => f.replace(/\.ya?ml$/, ""));
|
|
570
|
+
return toolError(`Token category "${category}" not found. Available: ${available.join(", ")}`);
|
|
571
|
+
} catch (err) {
|
|
572
|
+
return toolError(err);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
// ── tool: openuispec_get_locale ─────────────────────────────────────
|
|
578
|
+
|
|
579
|
+
server.registerTool(
|
|
580
|
+
"openuispec_get_locale",
|
|
581
|
+
{
|
|
582
|
+
description: "Get a single locale file, optionally filtered to specific keys. Faster than read_specs when you only need one locale or specific translation keys.",
|
|
583
|
+
inputSchema: {
|
|
584
|
+
locale: z.string().describe("Locale code, e.g. 'en', 'ru'"),
|
|
585
|
+
keys: z.array(z.string()).optional().describe("Optional list of keys to filter to, e.g. ['nav.home', 'nav.create']. If omitted, returns the full locale file."),
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
async ({ locale, keys }) => {
|
|
589
|
+
try {
|
|
590
|
+
const projectDir = findProjectDir(projectCwd);
|
|
591
|
+
const manifest = YAML.parse(fsReadFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
|
|
592
|
+
const localesDir = resolveSpecDir(projectDir, manifest, "locales");
|
|
593
|
+
const filePath = join(localesDir, `${locale}.json`);
|
|
594
|
+
|
|
595
|
+
if (!existsSync(filePath)) {
|
|
596
|
+
if (existsSync(localesDir)) {
|
|
597
|
+
const available = readdirSync(localesDir)
|
|
598
|
+
.filter(f => f.endsWith(".json"))
|
|
599
|
+
.map(f => f.replace(/\.json$/, ""));
|
|
600
|
+
return toolError(`Locale "${locale}" not found. Available: ${available.join(", ")}`);
|
|
601
|
+
}
|
|
602
|
+
return toolError(`Locales directory not found: ${localesDir}`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const raw = fsReadFileSync(filePath, "utf-8");
|
|
606
|
+
const content = JSON.parse(raw);
|
|
607
|
+
|
|
608
|
+
if (keys && keys.length > 0) {
|
|
609
|
+
const filtered: Record<string, unknown> = {};
|
|
610
|
+
for (const key of keys) {
|
|
611
|
+
if (key in content) {
|
|
612
|
+
filtered[key] = content[key];
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return toolResult({ locale, path: relative(projectDir, filePath), content: filtered });
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return toolResult({ locale, path: relative(projectDir, filePath), content });
|
|
619
|
+
} catch (err) {
|
|
620
|
+
return toolError(err);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
);
|
|
624
|
+
|
|
435
625
|
// ── start server ─────────────────────────────────────────────────────
|
|
436
626
|
|
|
437
627
|
export async function startMcpServer() {
|
package/package.json
CHANGED
|
@@ -104,6 +104,13 @@
|
|
|
104
104
|
"type": "string"
|
|
105
105
|
}
|
|
106
106
|
},
|
|
107
|
+
"extra_rules": {
|
|
108
|
+
"type": "array",
|
|
109
|
+
"description": "Project-wide authoring and generation conventions for AI, including optional scoped hint labels such as [common], [ios], [android], and [web].",
|
|
110
|
+
"items": {
|
|
111
|
+
"type": "string"
|
|
112
|
+
}
|
|
113
|
+
},
|
|
107
114
|
"output_dir": {
|
|
108
115
|
"type": "object",
|
|
109
116
|
"description": "Per-target output directory (relative to openuispec.yaml). Defaults to generated/<target>/<project_name> if not set.",
|
package/spec/openuispec-v0.1.md
CHANGED
|
@@ -83,6 +83,8 @@ includes:
|
|
|
83
83
|
|
|
84
84
|
generation:
|
|
85
85
|
targets: [ios, android, web]
|
|
86
|
+
extra_rules:
|
|
87
|
+
- "Generation hint strings may start with [common], [ios], [android], or [web] to indicate scope."
|
|
86
88
|
ai_model: "any" # no model lock-in
|
|
87
89
|
output_format:
|
|
88
90
|
ios: { language: swift, framework: swiftui }
|
|
@@ -3248,6 +3250,17 @@ ios:
|
|
|
3248
3250
|
- Generate test code based on `test_cases`
|
|
3249
3251
|
- Add platform-specific enhancements beyond what the contract specifies
|
|
3250
3252
|
|
|
3253
|
+
### Scoped generation hint labels
|
|
3254
|
+
|
|
3255
|
+
Projects may declare authoring conventions in `generation.extra_rules` inside `openuispec.yaml`. One supported convention is prefixing generation hint strings with scope labels such as `[common]`, `[ios]`, `[android]`, and `[web]`. These labels are advisory authoring metadata for humans and AI; they do not change schema semantics unless downstream tooling chooses to interpret them.
|
|
3256
|
+
|
|
3257
|
+
```yaml
|
|
3258
|
+
generation:
|
|
3259
|
+
targets: [ios, android, web]
|
|
3260
|
+
extra_rules:
|
|
3261
|
+
- "Generation hint strings may start with [common], [ios], [android], or [web] to indicate scope."
|
|
3262
|
+
```
|
|
3263
|
+
|
|
3251
3264
|
### 12.8 Extending standard contracts
|
|
3252
3265
|
|
|
3253
3266
|
The 7 built-in contract families (Section 4) can be extended per-project using `contracts/<name>.yaml` files. Extensions add project-specific **variants**, **token overrides**, **platform mapping**, and **generation hints** without redefining the base contract. The base definition (props, states, a11y) remains authoritative from the spec.
|