openuispec 0.2.10 → 0.2.12
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 +3 -1
- package/check/index.ts +17 -0
- package/cli/index.ts +21 -3
- package/cli/init.ts +224 -10
- package/docs/cli.md +13 -8
- package/docs/file-formats.md +36 -0
- package/docs/implementation-notes.md +7 -0
- package/drift/index.ts +281 -40
- package/mcp-server/index.ts +179 -119
- package/mcp-server/screenshot.ts +19 -4
- package/package.json +5 -2
- package/prepare/index.ts +155 -18
- package/schema/openuispec.schema.json +59 -0
- package/schema/semantic-lint.ts +25 -1
- package/scripts/take-all-screenshots.ts +507 -0
- package/spec/openuispec-v0.1.md +13 -0
- package/status/index.ts +72 -2
- package/examples/social-app/.mcp.json +0 -10
- package/examples/social-app/AGENTS.md +0 -124
- package/examples/social-app/CLAUDE.md +0 -124
- package/examples/social-app/backend/.gitkeep +0 -1
- package/examples/social-app/generated/android/social-app/app/.paparazzi-hashes.json +0 -3
- package/examples/social-app/generated/android/social-app/app/build.gradle.kts +0 -94
- package/examples/social-app/generated/android/social-app/app/src/main/AndroidManifest.xml +0 -26
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/AppContainer.kt +0 -20
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/MainActivity.kt +0 -35
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/SocialAppApplication.kt +0 -13
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/MockData.kt +0 -98
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/AppPreferences.kt +0 -19
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/DataStorePreferencesRepository.kt +0 -68
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/PreferencesRepository.kt +0 -15
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/model/Models.kt +0 -34
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/MainShell.kt +0 -390
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/Components.kt +0 -234
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/ContractPrimitives.kt +0 -641
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/navigation/RootComponent.kt +0 -113
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ChatDetailScreen.kt +0 -212
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/CreatePostScreen.kt +0 -113
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/DiscoverScreen.kt +0 -137
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/EditProfileScreen.kt +0 -180
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/HomeFeedScreen.kt +0 -169
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/MessagesInboxScreen.kt +0 -85
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/NotificationsScreen.kt +0 -74
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/PostDetailScreen.kt +0 -293
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ProfileSelfScreen.kt +0 -116
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SearchResultsScreen.kt +0 -161
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsScreen.kt +0 -164
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsStore.kt +0 -95
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/UserProfileScreen.kt +0 -123
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Color.kt +0 -33
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Shape.kt +0 -41
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Spacing.kt +0 -20
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Theme.kt +0 -82
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Type.kt +0 -60
- package/examples/social-app/generated/android/social-app/app/src/main/res/drawable/ic_launcher_foreground.xml +0 -9
- package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +0 -5
- package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +0 -5
- package/examples/social-app/generated/android/social-app/app/src/main/res/values/strings.xml +0 -91
- package/examples/social-app/generated/android/social-app/app/src/main/res/values/themes.xml +0 -10
- package/examples/social-app/generated/android/social-app/app/src/main/res/values-ru/strings.xml +0 -79
- package/examples/social-app/generated/android/social-app/app/src/main/res/values-uz/strings.xml +0 -79
- package/examples/social-app/generated/android/social-app/app/src/main/xml/AndroidManifest.xml +0 -23
- package/examples/social-app/generated/android/social-app/app/src/test/kotlin/com/social/app/screenshots/HomeFeedScreenshotTest.kt +0 -34
- package/examples/social-app/generated/android/social-app/build.gradle.kts +0 -7
- package/examples/social-app/generated/android/social-app/gradle/libs.versions.toml +0 -50
- package/examples/social-app/generated/android/social-app/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/examples/social-app/generated/android/social-app/gradle/wrapper/gradle-wrapper.properties +0 -8
- package/examples/social-app/generated/android/social-app/gradle.properties +0 -11
- package/examples/social-app/generated/android/social-app/gradlew +0 -248
- package/examples/social-app/generated/android/social-app/settings.gradle.kts +0 -27
- package/examples/social-app/generated/web/social-app/index.html +0 -12
- package/examples/social-app/generated/web/social-app/package-lock.json +0 -2517
- package/examples/social-app/generated/web/social-app/package.json +0 -27
- package/examples/social-app/generated/web/social-app/src/app/App.tsx +0 -58
- package/examples/social-app/generated/web/social-app/src/components/Shell.tsx +0 -259
- package/examples/social-app/generated/web/social-app/src/components/cards.tsx +0 -317
- package/examples/social-app/generated/web/social-app/src/components/ui.tsx +0 -340
- package/examples/social-app/generated/web/social-app/src/flows/CreatePostFlow.tsx +0 -86
- package/examples/social-app/generated/web/social-app/src/i18n.tsx +0 -59
- package/examples/social-app/generated/web/social-app/src/lib/icons.tsx +0 -85
- package/examples/social-app/generated/web/social-app/src/lib/tokens.ts +0 -70
- package/examples/social-app/generated/web/social-app/src/lib/utils.ts +0 -97
- package/examples/social-app/generated/web/social-app/src/locales/en.json +0 -67
- package/examples/social-app/generated/web/social-app/src/locales/ru.json +0 -67
- package/examples/social-app/generated/web/social-app/src/locales/uz.json +0 -67
- package/examples/social-app/generated/web/social-app/src/main.tsx +0 -16
- package/examples/social-app/generated/web/social-app/src/screens/ChatDetailScreen.tsx +0 -90
- package/examples/social-app/generated/web/social-app/src/screens/DiscoverScreen.tsx +0 -86
- package/examples/social-app/generated/web/social-app/src/screens/EditProfileScreen.tsx +0 -57
- package/examples/social-app/generated/web/social-app/src/screens/HomeFeedScreen.tsx +0 -103
- package/examples/social-app/generated/web/social-app/src/screens/MessagesInboxScreen.tsx +0 -52
- package/examples/social-app/generated/web/social-app/src/screens/NotificationsScreen.tsx +0 -41
- package/examples/social-app/generated/web/social-app/src/screens/PostDetailScreen.tsx +0 -115
- package/examples/social-app/generated/web/social-app/src/screens/ProfileSelfScreen.tsx +0 -57
- package/examples/social-app/generated/web/social-app/src/screens/ProfileUserScreen.tsx +0 -76
- package/examples/social-app/generated/web/social-app/src/screens/SearchResultsScreen.tsx +0 -96
- package/examples/social-app/generated/web/social-app/src/screens/SettingsScreen.tsx +0 -79
- package/examples/social-app/generated/web/social-app/src/state/store.ts +0 -592
- package/examples/social-app/generated/web/social-app/src/styles.css +0 -125
- package/examples/social-app/generated/web/social-app/src/vite-env.d.ts +0 -1
- package/examples/social-app/generated/web/social-app/tsconfig.json +0 -22
- package/examples/social-app/generated/web/social-app/tsconfig.node.json +0 -13
- package/examples/social-app/generated/web/social-app/tsconfig.node.tsbuildinfo +0 -1
- package/examples/social-app/generated/web/social-app/tsconfig.tsbuildinfo +0 -1
- package/examples/social-app/generated/web/social-app/vite.config.d.ts +0 -2
- package/examples/social-app/generated/web/social-app/vite.config.js +0 -6
- package/examples/social-app/generated/web/social-app/vite.config.ts +0 -7
- package/examples/social-app/package.json +0 -13
- package/examples/social-app/take-web-screenshots.ts +0 -97
- package/examples/taskflow/.codex/config.toml +0 -4
- package/examples/taskflow/.mcp.json +0 -10
- package/examples/taskflow/AGENTS.md +0 -124
- package/examples/taskflow/CLAUDE.md +0 -124
- package/examples/taskflow/backend/.gitkeep +0 -1
- package/examples/taskflow/generated/android/TaskFlow/README.md +0 -43
- package/examples/taskflow/generated/android/TaskFlow/app/build.gradle.kts +0 -76
- package/examples/taskflow/generated/android/TaskFlow/app/proguard-rules.pro +0 -1
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/AndroidManifest.xml +0 -21
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/MainActivity.kt +0 -19
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/TaskFlowApp.kt +0 -283
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/DomainModels.kt +0 -106
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/SampleData.kt +0 -57
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/components/Common.kt +0 -109
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/HomeScreen.kt +0 -112
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/ProjectsScreen.kt +0 -61
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/SettingsScreen.kt +0 -82
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/TaskDetailScreen.kt +0 -111
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/sheets/Sheets.kt +0 -77
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Color.kt +0 -30
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Theme.kt +0 -86
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Type.kt +0 -57
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/strings.xml +0 -155
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/themes.xml +0 -4
- package/examples/taskflow/generated/android/TaskFlow/build.gradle.kts +0 -5
- package/examples/taskflow/generated/android/TaskFlow/gradle/gradle-daemon-jvm.properties +0 -12
- package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.properties +0 -7
- package/examples/taskflow/generated/android/TaskFlow/gradle.properties +0 -4
- package/examples/taskflow/generated/android/TaskFlow/gradlew +0 -18
- package/examples/taskflow/generated/android/TaskFlow/gradlew.bat +0 -12
- package/examples/taskflow/generated/android/TaskFlow/settings.gradle.kts +0 -18
- package/examples/taskflow/generated/ios/TaskFlow/README.md +0 -21
- package/examples/taskflow/generated/ios/TaskFlow/Resources/en.lproj/Localizable.strings +0 -115
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/App/TaskFlowApp.swift +0 -24
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Components/AppChrome.swift +0 -150
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Flows/TaskEditorSheet.swift +0 -220
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Models/DomainModels.swift +0 -122
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/CalendarView.swift +0 -21
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/HomeView.swift +0 -201
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProfileEditView.swift +0 -48
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectDetailView.swift +0 -59
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectsView.swift +0 -63
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/SettingsView.swift +0 -85
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/TaskDetailView.swift +0 -219
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppModel.swift +0 -320
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppSupport.swift +0 -41
- package/examples/taskflow/generated/ios/TaskFlow/project.yml +0 -31
- package/examples/taskflow/generated/web/TaskFlow/README.md +0 -19
- package/examples/taskflow/generated/web/TaskFlow/index.html +0 -12
- package/examples/taskflow/generated/web/TaskFlow/package-lock.json +0 -1908
- package/examples/taskflow/generated/web/TaskFlow/package.json +0 -24
- package/examples/taskflow/generated/web/TaskFlow/src/App.tsx +0 -58
- package/examples/taskflow/generated/web/TaskFlow/src/AppShell.tsx +0 -55
- package/examples/taskflow/generated/web/TaskFlow/src/components/Common.tsx +0 -82
- package/examples/taskflow/generated/web/TaskFlow/src/components/Modals.tsx +0 -191
- package/examples/taskflow/generated/web/TaskFlow/src/components/Nav.tsx +0 -41
- package/examples/taskflow/generated/web/TaskFlow/src/generated/messages.ts +0 -131
- package/examples/taskflow/generated/web/TaskFlow/src/hooks.ts +0 -25
- package/examples/taskflow/generated/web/TaskFlow/src/i18n.ts +0 -39
- package/examples/taskflow/generated/web/TaskFlow/src/locales.en.json +0 -111
- package/examples/taskflow/generated/web/TaskFlow/src/main.tsx +0 -13
- package/examples/taskflow/generated/web/TaskFlow/src/screens/HomeScreen.tsx +0 -111
- package/examples/taskflow/generated/web/TaskFlow/src/screens/ProjectsScreen.tsx +0 -82
- package/examples/taskflow/generated/web/TaskFlow/src/screens/SettingsScreens.tsx +0 -132
- package/examples/taskflow/generated/web/TaskFlow/src/screens/TaskDetail.tsx +0 -105
- package/examples/taskflow/generated/web/TaskFlow/src/store.ts +0 -216
- package/examples/taskflow/generated/web/TaskFlow/src/styles.css +0 -617
- package/examples/taskflow/generated/web/TaskFlow/src/types.ts +0 -64
- package/examples/taskflow/generated/web/TaskFlow/src/utils.ts +0 -78
- package/examples/taskflow/generated/web/TaskFlow/tsconfig.json +0 -21
- package/examples/taskflow/generated/web/TaskFlow/vite.config.ts +0 -6
- package/examples/todo-orbit/.codex/config.toml +0 -4
- package/examples/todo-orbit/.mcp.json +0 -10
- package/examples/todo-orbit/AGENTS.md +0 -124
- package/examples/todo-orbit/CLAUDE.md +0 -124
- package/examples/todo-orbit/backend/.gitkeep +0 -1
- package/examples/todo-orbit/generated/android/Todo Orbit/README.md +0 -14
- package/examples/todo-orbit/generated/android/Todo Orbit/app/build.gradle.kts +0 -58
- package/examples/todo-orbit/generated/android/Todo Orbit/app/proguard-rules.pro +0 -1
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/AndroidManifest.xml +0 -20
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/MainActivity.kt +0 -14
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/TodoOrbitApp.kt +0 -345
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/AppLogic.kt +0 -231
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Models.kt +0 -169
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Strings.kt +0 -8
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/components/CommonComponents.kt +0 -236
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/AnalyticsScreen.kt +0 -193
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/SettingsScreen.kt +0 -102
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/TasksScreen.kt +0 -347
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/sheets/EditorSheets.kt +0 -347
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/theme/TodoOrbitTheme.kt +0 -59
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values/strings.xml +0 -149
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values-ru/strings.xml +0 -155
- package/examples/todo-orbit/generated/android/Todo Orbit/build.gradle.kts +0 -4
- package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.properties +0 -7
- package/examples/todo-orbit/generated/android/Todo Orbit/gradle.properties +0 -4
- package/examples/todo-orbit/generated/android/Todo Orbit/gradlew +0 -248
- package/examples/todo-orbit/generated/android/Todo Orbit/gradlew.bat +0 -93
- package/examples/todo-orbit/generated/android/Todo Orbit/settings.gradle.kts +0 -18
- package/examples/todo-orbit/generated/ios/Todo Orbit/.screenshot-uitest/Sources/ScreenshotUITest.swift +0 -36
- package/examples/todo-orbit/generated/ios/Todo Orbit/README.md +0 -29
- package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/en.lproj/Localizable.strings +0 -119
- package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/ru.lproj/Localizable.strings +0 -119
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/App/TodoOrbitApp.swift +0 -50
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/OrbitChrome.swift +0 -204
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/SchedulePreviewView.swift +0 -126
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/TrendChartView.swift +0 -70
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/RecurringRuleSheet.swift +0 -126
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/TaskEditorSheet.swift +0 -61
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Models/DomainModels.swift +0 -238
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/AnalyticsView.swift +0 -94
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/SettingsView.swift +0 -76
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/TasksHomeView.swift +0 -364
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Support/AppModel.swift +0 -324
- package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.pbxproj +0 -439
- package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/xcshareddata/xcschemes/TodoOrbit.xcscheme +0 -89
- package/examples/todo-orbit/generated/ios/Todo Orbit/project.yml +0 -32
- package/examples/todo-orbit/generated/web/Todo Orbit/index.html +0 -16
- package/examples/todo-orbit/generated/web/Todo Orbit/package-lock.json +0 -1087
- package/examples/todo-orbit/generated/web/Todo Orbit/package.json +0 -24
- package/examples/todo-orbit/generated/web/Todo Orbit/src/App.tsx +0 -2167
- package/examples/todo-orbit/generated/web/Todo Orbit/src/main.tsx +0 -13
- package/examples/todo-orbit/generated/web/Todo Orbit/src/styles.css +0 -926
- package/examples/todo-orbit/generated/web/Todo Orbit/tsconfig.json +0 -19
- package/examples/todo-orbit/generated/web/Todo Orbit/vite.config.ts +0 -6
package/mcp-server/index.ts
CHANGED
|
@@ -13,17 +13,14 @@
|
|
|
13
13
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
14
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
15
|
import { z } from "zod";
|
|
16
|
-
import { readFileSync } from "node:fs";
|
|
17
|
-
import { join, dirname } from "node:path";
|
|
16
|
+
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
17
|
+
import { join, dirname, relative, resolve } from "node:path";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
|
-
import { SUPPORTED_TARGETS, findProjectDir, discoverSpecFiles } from "../drift/index.js";
|
|
19
|
+
import { SUPPORTED_TARGETS, findProjectDir, discoverSpecFiles, readProjectName, resolveOutputDir, stateFilePath, loadTargetDrift, createSnapshot } from "../drift/index.js";
|
|
20
20
|
import { buildPrepareResult } from "../prepare/index.js";
|
|
21
21
|
import { buildCheckResult } from "../check/index.js";
|
|
22
22
|
import { buildStatusResult } from "../status/index.js";
|
|
23
23
|
import { buildValidateResult } from "../schema/validate.js";
|
|
24
|
-
import { loadTargetDrift } from "../drift/index.js";
|
|
25
|
-
import { readFileSync as fsReadFileSync, existsSync, readdirSync } from "node:fs";
|
|
26
|
-
import { relative, resolve } from "node:path";
|
|
27
24
|
import YAML from "yaml";
|
|
28
25
|
import { takeScreenshot, takeScreenshotBatch } from "./screenshot.js";
|
|
29
26
|
import { takeAndroidScreenshot, takeAndroidScreenshotBatch } from "./screenshot-android.js";
|
|
@@ -59,8 +56,14 @@ function formatError(err: unknown): string {
|
|
|
59
56
|
return err instanceof Error ? err.message : String(err);
|
|
60
57
|
}
|
|
61
58
|
|
|
62
|
-
function toolResult(data: unknown): { content:
|
|
63
|
-
|
|
59
|
+
function toolResult(data: unknown, hint?: string): { content: { type: "text"; text: string }[] } {
|
|
60
|
+
const parts: { type: "text"; text: string }[] = [
|
|
61
|
+
{ type: "text" as const, text: JSON.stringify(data) },
|
|
62
|
+
];
|
|
63
|
+
if (hint) {
|
|
64
|
+
parts.push({ type: "text" as const, text: hint });
|
|
65
|
+
}
|
|
66
|
+
return { content: parts };
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
function toolError(err: unknown): { content: [{ type: "text"; text: string }]; isError: true } {
|
|
@@ -108,58 +111,21 @@ export const server = new McpServer(
|
|
|
108
111
|
version: getPackageVersion(),
|
|
109
112
|
},
|
|
110
113
|
{
|
|
111
|
-
instructions: `
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
6. Call openuispec_read_specs for the screens/contracts you just generated code for.
|
|
127
|
-
7. Audit your generated code against the spec contents. For each screen, verify:
|
|
128
|
-
- Every field/action in the spec has a corresponding UI element
|
|
129
|
-
- Token values (colors, spacing, radii) match the spec exactly, not approximations
|
|
130
|
-
- Contract must_handle states are all implemented (loading, error, empty, etc.)
|
|
131
|
-
- Adaptive breakpoints match the layout size_classes in the spec
|
|
132
|
-
- Locale keys match $t: references in the spec
|
|
133
|
-
- Navigation targets match flow definitions
|
|
134
|
-
Report any real gaps found and fix them before finishing.
|
|
135
|
-
|
|
136
|
-
CREATING NEW SPEC FILES:
|
|
137
|
-
When you need to create or edit spec files and are unsure of the format:
|
|
138
|
-
1. Call openuispec_spec_types to discover available spec types.
|
|
139
|
-
2. Call openuispec_spec_schema with the specific type to get the full JSON schema.
|
|
140
|
-
3. Write the spec file following the schema exactly.
|
|
141
|
-
|
|
142
|
-
FOCUSED GETTERS (prefer these for incremental edits over read_specs):
|
|
143
|
-
- openuispec_get_screen(name) — single screen spec
|
|
144
|
-
- openuispec_get_contract(name, variant?) — single contract, optionally one variant
|
|
145
|
-
- openuispec_get_tokens(category) — single token category (color, typography, spacing, etc.)
|
|
146
|
-
- openuispec_get_locale(locale, keys?) — single locale file, optionally filtered keys
|
|
147
|
-
- openuispec_check(target, screens?, contracts?) — scoped audit for specific screens/contracts
|
|
148
|
-
Use read_specs for full-project generation; use focused getters when editing one screen or contract.
|
|
149
|
-
|
|
150
|
-
VISUAL VERIFICATION:
|
|
151
|
-
- openuispec_screenshot(route, viewport?, theme?) — screenshot the generated web app at a route.
|
|
152
|
-
Starts the dev server automatically. Use after generation to visually verify UI matches the spec.
|
|
153
|
-
Requires puppeteer (npm install -g puppeteer).
|
|
154
|
-
- openuispec_screenshot_android(screen?, theme?, wait_for?) — screenshot the generated Android app.
|
|
155
|
-
Builds APK, installs on emulator, and captures via adb screencap.
|
|
156
|
-
Shows the real app with navigation, images, and themes. Requires a running emulator.
|
|
157
|
-
- openuispec_screenshot_ios(screen?, device?, nav?, theme?, wait_for?) — screenshot the generated iOS app.
|
|
158
|
-
Builds with xcodebuild, installs on simulator, and captures via xcrun simctl.
|
|
159
|
-
Shows the real app with navigation, images, and themes. Requires Xcode.
|
|
160
|
-
|
|
161
|
-
Skip these tools ONLY when the request is purely non-UI (API logic, database, infrastructure, etc.)
|
|
162
|
-
or explicitly platform-specific polish that doesn't affect shared UI semantics.`,
|
|
114
|
+
instructions: `OpenUISpec — semantic UI spec format. Spec files (YAML) are the single source of truth for all UI.
|
|
115
|
+
|
|
116
|
+
WORKFLOW — each tool response includes a next_tool hint, follow it:
|
|
117
|
+
1. openuispec_prepare(target) → get context + platform config (include_specs=true to embed content)
|
|
118
|
+
2. openuispec_read_specs(paths) → load spec content (omit paths for listing only)
|
|
119
|
+
3. Generate/update code
|
|
120
|
+
4. openuispec_check(target) → validate spec files (audit=true for review checklist, not code inspection)
|
|
121
|
+
5. Remind the user to baseline when satisfied: openuispec drift --snapshot --target <t>
|
|
122
|
+
Do not baseline on your own initiative — the user decides when output is accepted.
|
|
123
|
+
|
|
124
|
+
FOCUSED GETTERS (prefer for incremental edits): get_screen, get_contract, get_tokens, get_locale
|
|
125
|
+
SPEC AUTHORING: spec_types → spec_schema(type, summary?) → write YAML
|
|
126
|
+
SCREENSHOTS: screenshot (web), screenshot_android, screenshot_ios — single + batch variants
|
|
127
|
+
|
|
128
|
+
Skip only for purely non-UI requests.`,
|
|
163
129
|
}
|
|
164
130
|
);
|
|
165
131
|
|
|
@@ -168,12 +134,26 @@ or explicitly platform-specific polish that doesn't affect shared UI semantics.`
|
|
|
168
134
|
server.registerTool(
|
|
169
135
|
"openuispec_prepare",
|
|
170
136
|
{
|
|
171
|
-
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.
|
|
172
|
-
inputSchema: {
|
|
137
|
+
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.",
|
|
138
|
+
inputSchema: {
|
|
139
|
+
target: targetSchema,
|
|
140
|
+
include_specs: z.boolean().optional().default(false).describe("Embed all spec file contents in the response. Saves a separate read_specs call but increases response size."),
|
|
141
|
+
},
|
|
173
142
|
},
|
|
174
|
-
async ({ target }) => {
|
|
143
|
+
async ({ target, include_specs }) => {
|
|
175
144
|
try {
|
|
176
|
-
|
|
145
|
+
const result = buildPrepareResult(target, projectCwd, include_specs);
|
|
146
|
+
const baselinePending = result.baseline_status?.output_exists && !result.baseline_status?.snapshot_exists;
|
|
147
|
+
const baselineReminder = baselinePending
|
|
148
|
+
? " ⚠ Baseline pending — remind user to run `openuispec drift --snapshot --target " + target + "` when satisfied."
|
|
149
|
+
: "";
|
|
150
|
+
const sharedHint = result.shared_layers?.length
|
|
151
|
+
? ` ℹ ${result.shared_layers.length} shared layer(s) detected — check shared_layers for generation guidance.`
|
|
152
|
+
: "";
|
|
153
|
+
const hint = (include_specs
|
|
154
|
+
? "next_tool: openuispec_check (after generating code)"
|
|
155
|
+
: "next_tool: openuispec_read_specs (load spec contents for generation)") + baselineReminder + sharedHint;
|
|
156
|
+
return toolResult(result, hint);
|
|
177
157
|
} catch (err) {
|
|
178
158
|
return toolError(err);
|
|
179
159
|
}
|
|
@@ -184,23 +164,24 @@ server.registerTool(
|
|
|
184
164
|
|
|
185
165
|
function buildAuditChecklist(projectDir: string, target: string, screenFilter?: string[], contractFilter?: string[]): string {
|
|
186
166
|
const lines: string[] = [
|
|
187
|
-
"
|
|
167
|
+
"SPEC-DERIVED CHECKLIST — this is extracted from the spec files, NOT from generated code.",
|
|
168
|
+
"Use it as a guide when you manually review the generated code.",
|
|
188
169
|
"",
|
|
189
|
-
"
|
|
190
|
-
"find the code that implements it, and confirm the values match
|
|
191
|
-
"If you cannot find the implementation, it is a
|
|
170
|
+
"For each item below, read the generated component/screen file,",
|
|
171
|
+
"find the code that implements it, and confirm the values match.",
|
|
172
|
+
"If you cannot find the implementation, it is a gap — fix it.",
|
|
192
173
|
"",
|
|
193
174
|
];
|
|
194
175
|
|
|
195
176
|
// Extract must_handle from contracts
|
|
196
|
-
const manifest = YAML.parse(
|
|
177
|
+
const manifest = YAML.parse(readFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
|
|
197
178
|
const contractsDir = resolveSpecDir(projectDir, manifest, "contracts");
|
|
198
179
|
|
|
199
180
|
if (existsSync(contractsDir)) {
|
|
200
181
|
lines.push("## Contract must_handle requirements");
|
|
201
182
|
for (const file of readdirSync(contractsDir).filter(f => f.endsWith(".yaml")).sort()) {
|
|
202
183
|
try {
|
|
203
|
-
const content = YAML.parse(
|
|
184
|
+
const content = YAML.parse(readFileSync(join(contractsDir, file), "utf-8"));
|
|
204
185
|
const contractName = Object.keys(content)[0];
|
|
205
186
|
if (contractFilter && !contractFilter.includes(contractName)) continue;
|
|
206
187
|
const contract = content[contractName];
|
|
@@ -254,7 +235,7 @@ function buildAuditChecklist(projectDir: string, target: string, screenFilter?:
|
|
|
254
235
|
lines.push("## Screens — verify all sections exist in generated code");
|
|
255
236
|
for (const file of readdirSync(screensDir).filter(f => f.endsWith(".yaml")).sort()) {
|
|
256
237
|
try {
|
|
257
|
-
const content = YAML.parse(
|
|
238
|
+
const content = YAML.parse(readFileSync(join(screensDir, file), "utf-8"));
|
|
258
239
|
const screenName = Object.keys(content)[0];
|
|
259
240
|
if (screenFilter && !screenFilter.includes(screenName)) continue;
|
|
260
241
|
const screen = content[screenName];
|
|
@@ -301,7 +282,7 @@ function buildAuditChecklist(projectDir: string, target: string, screenFilter?:
|
|
|
301
282
|
lines.push("## Locales — verify all locale files are wired");
|
|
302
283
|
for (const file of localeFiles) {
|
|
303
284
|
try {
|
|
304
|
-
const keys = Object.keys(JSON.parse(
|
|
285
|
+
const keys = Object.keys(JSON.parse(readFileSync(join(localesDir, file), "utf-8")));
|
|
305
286
|
lines.push(`- [ ] ${file}: ${keys.length} keys loaded at runtime`);
|
|
306
287
|
} catch { /* skip */ }
|
|
307
288
|
}
|
|
@@ -314,7 +295,7 @@ function buildAuditChecklist(projectDir: string, target: string, screenFilter?:
|
|
|
314
295
|
const platformPath = join(platformDir, `${target}.yaml`);
|
|
315
296
|
if (existsSync(platformPath)) {
|
|
316
297
|
try {
|
|
317
|
-
const platformDoc = YAML.parse(
|
|
298
|
+
const platformDoc = YAML.parse(readFileSync(platformPath, "utf-8"));
|
|
318
299
|
const platformDef = platformDoc?.[target];
|
|
319
300
|
if (platformDef?.generation) {
|
|
320
301
|
lines.push("## Platform generation requirements");
|
|
@@ -336,26 +317,49 @@ function buildAuditChecklist(projectDir: string, target: string, screenFilter?:
|
|
|
336
317
|
server.registerTool(
|
|
337
318
|
"openuispec_check",
|
|
338
319
|
{
|
|
339
|
-
description: "
|
|
320
|
+
description: "Validate spec files (schema + semantic lint) and check prepare readiness. Does NOT inspect generated code. With audit=true, returns a spec-derived checklist of must_handle items, screen sections, and locale files — use it as a guide when YOU manually review the generated code.",
|
|
340
321
|
inputSchema: {
|
|
341
322
|
target: targetSchema,
|
|
342
|
-
|
|
343
|
-
|
|
323
|
+
audit: z.boolean().optional().default(false).describe("Include the full audit checklist. Omit for a compact pass/fail summary."),
|
|
324
|
+
screens: z.array(z.string()).optional().describe("Screen names to audit (e.g. ['home_feed']). Requires audit=true."),
|
|
325
|
+
contracts: z.array(z.string()).optional().describe("Contract names to audit (e.g. ['action_trigger']). Requires audit=true."),
|
|
344
326
|
},
|
|
345
327
|
},
|
|
346
|
-
async ({ target, screens, contracts }) => {
|
|
328
|
+
async ({ target, audit: includeAudit, screens, contracts }) => {
|
|
347
329
|
try {
|
|
348
330
|
const result = buildCheckResult(target, projectCwd);
|
|
331
|
+
const totalErrors = result.validation.total_errors + result.semantic.total_errors;
|
|
332
|
+
const passing = totalErrors === 0 && result.prepare.ready;
|
|
333
|
+
|
|
334
|
+
// bootstrap mode = no snapshot exists yet
|
|
335
|
+
const baselineHint = result.prepare.mode === "bootstrap"
|
|
336
|
+
? `⚠ BASELINE PENDING: Remind the user to run \`openuispec drift --snapshot --target ${target}\` when satisfied with the output.`
|
|
337
|
+
: "";
|
|
338
|
+
|
|
339
|
+
if (passing && !includeAudit) {
|
|
340
|
+
const compact = {
|
|
341
|
+
target,
|
|
342
|
+
status: "PASS",
|
|
343
|
+
validation_errors: 0,
|
|
344
|
+
semantic_errors: 0,
|
|
345
|
+
prepare: { mode: result.prepare.mode, ready: true },
|
|
346
|
+
};
|
|
347
|
+
return toolResult(compact, baselineHint || `Remind the user to baseline: openuispec drift --snapshot --target ${target}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
349
350
|
const projectDir = findProjectDir(projectCwd);
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
351
|
+
const hints: string[] = [JSON.stringify(result)];
|
|
352
|
+
|
|
353
|
+
if (includeAudit) {
|
|
354
|
+
const screenFilter = screens && screens.length > 0 ? screens : undefined;
|
|
355
|
+
const contractFilter = contracts && contracts.length > 0 ? contracts : undefined;
|
|
356
|
+
hints.push(buildAuditChecklist(projectDir, target, screenFilter, contractFilter));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (baselineHint) hints.push(baselineHint);
|
|
360
|
+
hints.push(passing ? "next_tool: openuispec_drift --snapshot (to create/update baseline)" : "Fix validation errors, then re-run openuispec_check.");
|
|
361
|
+
|
|
362
|
+
return { content: hints.map(text => ({ type: "text" as const, text })) };
|
|
359
363
|
} catch (err) {
|
|
360
364
|
return toolError(err);
|
|
361
365
|
}
|
|
@@ -371,7 +375,7 @@ server.registerTool(
|
|
|
371
375
|
},
|
|
372
376
|
async () => {
|
|
373
377
|
try {
|
|
374
|
-
return toolResult(buildStatusResult(projectCwd));
|
|
378
|
+
return toolResult(buildStatusResult(projectCwd), "next_tool: openuispec_prepare for any target that is 'behind' or 'needs generation'");
|
|
375
379
|
} catch (err) {
|
|
376
380
|
return toolError(err);
|
|
377
381
|
}
|
|
@@ -393,7 +397,8 @@ server.registerTool(
|
|
|
393
397
|
},
|
|
394
398
|
async ({ groups }) => {
|
|
395
399
|
try {
|
|
396
|
-
|
|
400
|
+
const result = buildValidateResult(groups, projectCwd);
|
|
401
|
+
return toolResult(result, "next_tool: openuispec_check (for full validation + prepare readiness)");
|
|
397
402
|
} catch (err) {
|
|
398
403
|
return toolError(err);
|
|
399
404
|
}
|
|
@@ -405,31 +410,41 @@ server.registerTool(
|
|
|
405
410
|
server.registerTool(
|
|
406
411
|
"openuispec_read_specs",
|
|
407
412
|
{
|
|
408
|
-
description: "Read
|
|
413
|
+
description: "Read spec file contents. Pass specific paths to load those files. If no paths given, returns a listing of all spec files (path + category, no content) — use that to pick which files to load.",
|
|
409
414
|
inputSchema: {
|
|
410
415
|
paths: z
|
|
411
416
|
.array(z.string())
|
|
412
417
|
.optional()
|
|
413
|
-
.describe("
|
|
418
|
+
.describe("Spec file paths to read (relative, e.g. 'screens/home.yaml'). If omitted, returns listing only."),
|
|
414
419
|
},
|
|
415
420
|
},
|
|
416
421
|
async ({ paths }) => {
|
|
417
422
|
try {
|
|
418
423
|
const projectDir = findProjectDir(projectCwd);
|
|
419
424
|
const allFiles = discoverSpecFiles(projectDir);
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
425
|
+
|
|
426
|
+
if (!paths || paths.length === 0) {
|
|
427
|
+
// Listing mode — paths + categories, no content
|
|
428
|
+
const listing = allFiles.map((f) => {
|
|
429
|
+
const rel = relative(projectDir, f);
|
|
430
|
+
const dir = dirname(rel);
|
|
431
|
+
const category = rel === "openuispec.yaml" ? "manifest" : (dir || "other");
|
|
432
|
+
return { path: rel, category };
|
|
433
|
+
});
|
|
434
|
+
return toolResult(listing, "next_tool: openuispec_read_specs with specific paths to load content");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const filesToRead = allFiles.filter((f) => {
|
|
438
|
+
const rel = relative(projectDir, f);
|
|
439
|
+
return paths.some((p) => rel === p || rel.endsWith(p));
|
|
440
|
+
});
|
|
426
441
|
|
|
427
442
|
const contents = filesToRead.map((f) => ({
|
|
428
443
|
path: relative(projectDir, f),
|
|
429
|
-
content:
|
|
444
|
+
content: readFileSync(f, "utf-8"),
|
|
430
445
|
}));
|
|
431
446
|
|
|
432
|
-
return toolResult(contents);
|
|
447
|
+
return toolResult(contents, "next_tool: generate/update code, then openuispec_check");
|
|
433
448
|
} catch (err) {
|
|
434
449
|
return toolError(err);
|
|
435
450
|
}
|
|
@@ -441,16 +456,26 @@ server.registerTool(
|
|
|
441
456
|
server.registerTool(
|
|
442
457
|
"openuispec_drift",
|
|
443
458
|
{
|
|
444
|
-
description: "Detect spec drift since last snapshot. Shows which spec files changed, were added, or removed. Use explain
|
|
459
|
+
description: "Detect spec drift since last snapshot, or create a new snapshot. Shows which spec files changed, were added, or removed. Use explain for property-level changes. Use snapshot=true after generation to create/update the baseline.",
|
|
445
460
|
inputSchema: {
|
|
446
461
|
target: targetSchema,
|
|
447
462
|
explain: z.boolean().optional().default(false).describe("Include semantic explanation of changes"),
|
|
463
|
+
snapshot: z.boolean().optional().default(false).describe("Create a new snapshot (baseline) instead of checking drift. Use after code generation is complete and verified."),
|
|
448
464
|
},
|
|
449
465
|
},
|
|
450
|
-
async ({ target, explain }) => {
|
|
466
|
+
async ({ target, explain, snapshot: doSnapshot }) => {
|
|
451
467
|
try {
|
|
468
|
+
if (doSnapshot) {
|
|
469
|
+
const result = createSnapshot(projectCwd, target);
|
|
470
|
+
return toolResult(result, "Baseline created. next_tool: openuispec_status (to verify all targets)");
|
|
471
|
+
}
|
|
452
472
|
const { result } = loadTargetDrift(projectCwd, target, false, explain);
|
|
453
|
-
|
|
473
|
+
const d = result.drift;
|
|
474
|
+
const hasDrift = d.changed.length > 0 || d.added.length > 0 || d.removed.length > 0;
|
|
475
|
+
const hint = hasDrift
|
|
476
|
+
? "next_tool: openuispec_prepare (to build work bundle for pending changes)"
|
|
477
|
+
: "No drift detected. Target is up to date.";
|
|
478
|
+
return toolResult(result, hint);
|
|
454
479
|
} catch (err) {
|
|
455
480
|
return toolError(err);
|
|
456
481
|
}
|
|
@@ -490,7 +515,7 @@ server.registerTool(
|
|
|
490
515
|
title: info.title,
|
|
491
516
|
description: info.description,
|
|
492
517
|
}));
|
|
493
|
-
return toolResult(types);
|
|
518
|
+
return toolResult(types, "next_tool: openuispec_spec_schema(type) for the full schema of a specific type");
|
|
494
519
|
}
|
|
495
520
|
);
|
|
496
521
|
|
|
@@ -499,12 +524,13 @@ server.registerTool(
|
|
|
499
524
|
server.registerTool(
|
|
500
525
|
"openuispec_spec_schema",
|
|
501
526
|
{
|
|
502
|
-
description: "Get the
|
|
527
|
+
description: "Get the JSON schema for a specific OpenUISpec spec type. Returns the complete schema definition so you know the exact format when creating or editing spec files.",
|
|
503
528
|
inputSchema: {
|
|
504
|
-
type: z.string().describe("Spec type
|
|
529
|
+
type: z.string().describe("Spec type (e.g. 'screen', 'tokens/color', 'contract'). Use openuispec_spec_types to list all."),
|
|
530
|
+
summary: z.boolean().optional().default(false).describe("Return only top-level property names and types instead of the full schema. Useful for a quick overview."),
|
|
505
531
|
},
|
|
506
532
|
},
|
|
507
|
-
async ({ type }) => {
|
|
533
|
+
async ({ type, summary }) => {
|
|
508
534
|
const entry = SCHEMA_CATALOG[type];
|
|
509
535
|
if (!entry) {
|
|
510
536
|
return toolError(`Unknown spec type "${type}". Call openuispec_spec_types to see available types.`);
|
|
@@ -513,6 +539,19 @@ server.registerTool(
|
|
|
513
539
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
514
540
|
const schemaPath = join(__dirname, "..", "schema", entry.file);
|
|
515
541
|
const schema = JSON.parse(readFileSync(schemaPath, "utf-8"));
|
|
542
|
+
|
|
543
|
+
if (summary) {
|
|
544
|
+
// Extract top-level properties summary
|
|
545
|
+
const props = schema.properties ?? schema.patternProperties ?? {};
|
|
546
|
+
const topLevel: Record<string, string> = {};
|
|
547
|
+
for (const [key, val] of Object.entries(props)) {
|
|
548
|
+
const v = val as any;
|
|
549
|
+
topLevel[key] = v.type ?? (v.$ref ? `ref:${v.$ref}` : "object");
|
|
550
|
+
}
|
|
551
|
+
return toolResult({ type, title: entry.title, required: schema.required ?? [], properties: topLevel },
|
|
552
|
+
"Use summary=false for the full schema when creating/editing spec files.");
|
|
553
|
+
}
|
|
554
|
+
|
|
516
555
|
return toolResult({ type, title: entry.title, schema });
|
|
517
556
|
} catch (err) {
|
|
518
557
|
return toolError(err);
|
|
@@ -533,13 +572,13 @@ server.registerTool(
|
|
|
533
572
|
async ({ name }) => {
|
|
534
573
|
try {
|
|
535
574
|
const projectDir = findProjectDir(projectCwd);
|
|
536
|
-
const manifest = YAML.parse(
|
|
575
|
+
const manifest = YAML.parse(readFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
|
|
537
576
|
const screensDir = resolveSpecDir(projectDir, manifest, "screens");
|
|
538
577
|
const filePath = join(screensDir, `${name}.yaml`);
|
|
539
578
|
if (!existsSync(filePath)) {
|
|
540
579
|
return toolError(`Screen "${name}" not found. Expected file: ${filePath}`);
|
|
541
580
|
}
|
|
542
|
-
const content =
|
|
581
|
+
const content = readFileSync(filePath, "utf-8");
|
|
543
582
|
return toolResult({ name, path: relative(projectDir, filePath), content });
|
|
544
583
|
} catch (err) {
|
|
545
584
|
return toolError(err);
|
|
@@ -561,7 +600,7 @@ server.registerTool(
|
|
|
561
600
|
async ({ name, variant }) => {
|
|
562
601
|
try {
|
|
563
602
|
const projectDir = findProjectDir(projectCwd);
|
|
564
|
-
const manifest = YAML.parse(
|
|
603
|
+
const manifest = YAML.parse(readFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
|
|
565
604
|
const contractsDir = resolveSpecDir(projectDir, manifest, "contracts");
|
|
566
605
|
|
|
567
606
|
if (!existsSync(contractsDir)) {
|
|
@@ -571,7 +610,7 @@ server.registerTool(
|
|
|
571
610
|
// Scan contract files for the matching contract key
|
|
572
611
|
for (const file of readdirSync(contractsDir).filter(f => f.endsWith(".yaml")).sort()) {
|
|
573
612
|
const filePath = join(contractsDir, file);
|
|
574
|
-
const raw =
|
|
613
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
575
614
|
const content = YAML.parse(raw);
|
|
576
615
|
const contractName = Object.keys(content)[0];
|
|
577
616
|
if (contractName !== name) continue;
|
|
@@ -608,7 +647,7 @@ server.registerTool(
|
|
|
608
647
|
async ({ category }) => {
|
|
609
648
|
try {
|
|
610
649
|
const projectDir = findProjectDir(projectCwd);
|
|
611
|
-
const manifest = YAML.parse(
|
|
650
|
+
const manifest = YAML.parse(readFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
|
|
612
651
|
const tokensDir = resolveSpecDir(projectDir, manifest, "tokens");
|
|
613
652
|
|
|
614
653
|
if (!existsSync(tokensDir)) {
|
|
@@ -624,7 +663,7 @@ server.registerTool(
|
|
|
624
663
|
for (const candidate of candidates) {
|
|
625
664
|
const filePath = join(tokensDir, candidate);
|
|
626
665
|
if (existsSync(filePath)) {
|
|
627
|
-
const content =
|
|
666
|
+
const content = readFileSync(filePath, "utf-8");
|
|
628
667
|
return toolResult({ category, path: relative(projectDir, filePath), content });
|
|
629
668
|
}
|
|
630
669
|
}
|
|
@@ -640,6 +679,25 @@ server.registerTool(
|
|
|
640
679
|
}
|
|
641
680
|
);
|
|
642
681
|
|
|
682
|
+
// ── locale key lookup (supports both flat dotted keys and nested objects) ──
|
|
683
|
+
|
|
684
|
+
function lookupLocaleKey(content: Record<string, unknown>, key: string): { found: boolean; value: unknown } {
|
|
685
|
+
// 1. Try flat (literal) key first: { "nav.tasks": "Tasks" }
|
|
686
|
+
if (key in content) {
|
|
687
|
+
return { found: true, value: content[key] };
|
|
688
|
+
}
|
|
689
|
+
// 2. Try nested path: { nav: { tasks: "Tasks" } }
|
|
690
|
+
const parts = key.split(".");
|
|
691
|
+
let current: unknown = content;
|
|
692
|
+
for (const part of parts) {
|
|
693
|
+
if (current === null || current === undefined || typeof current !== "object" || Array.isArray(current)) {
|
|
694
|
+
return { found: false, value: undefined };
|
|
695
|
+
}
|
|
696
|
+
current = (current as Record<string, unknown>)[part];
|
|
697
|
+
}
|
|
698
|
+
return current !== undefined ? { found: true, value: current } : { found: false, value: undefined };
|
|
699
|
+
}
|
|
700
|
+
|
|
643
701
|
// ── tool: openuispec_get_locale ─────────────────────────────────────
|
|
644
702
|
|
|
645
703
|
server.registerTool(
|
|
@@ -654,7 +712,7 @@ server.registerTool(
|
|
|
654
712
|
async ({ locale, keys }) => {
|
|
655
713
|
try {
|
|
656
714
|
const projectDir = findProjectDir(projectCwd);
|
|
657
|
-
const manifest = YAML.parse(
|
|
715
|
+
const manifest = YAML.parse(readFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
|
|
658
716
|
const localesDir = resolveSpecDir(projectDir, manifest, "locales");
|
|
659
717
|
const filePath = join(localesDir, `${locale}.json`);
|
|
660
718
|
|
|
@@ -668,15 +726,14 @@ server.registerTool(
|
|
|
668
726
|
return toolError(`Locales directory not found: ${localesDir}`);
|
|
669
727
|
}
|
|
670
728
|
|
|
671
|
-
const raw =
|
|
729
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
672
730
|
const content = JSON.parse(raw);
|
|
673
731
|
|
|
674
732
|
if (keys && keys.length > 0) {
|
|
675
733
|
const filtered: Record<string, unknown> = {};
|
|
676
734
|
for (const key of keys) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
}
|
|
735
|
+
const { found, value } = lookupLocaleKey(content, key);
|
|
736
|
+
if (found) filtered[key] = value;
|
|
680
737
|
}
|
|
681
738
|
return toolResult({ locale, path: relative(projectDir, filePath), content: filtered });
|
|
682
739
|
}
|
|
@@ -700,6 +757,7 @@ server.registerTool(
|
|
|
700
757
|
width: z.number().default(1280),
|
|
701
758
|
height: z.number().default(800),
|
|
702
759
|
}).optional().describe("Viewport dimensions. Defaults to 1280x800. Use {width: 375, height: 812} for mobile."),
|
|
760
|
+
scale: z.number().optional().default(2).describe("Device pixel ratio used for capture. Higher values produce sharper screenshots (default 2)."),
|
|
703
761
|
theme: z.enum(["light", "dark"]).optional().describe("Force a color scheme via prefers-color-scheme emulation"),
|
|
704
762
|
wait_for: z.number().optional().default(1000).describe("Milliseconds to wait after page load before screenshotting (default 1000)"),
|
|
705
763
|
full_page: z.boolean().optional().default(false).describe("Capture the full scrollable page instead of just the viewport"),
|
|
@@ -707,11 +765,12 @@ server.registerTool(
|
|
|
707
765
|
output_dir: z.string().optional().describe("Directory to save the screenshot PNG (relative to web app root). E.g. 'screenshots'. If omitted, only returns base64 in response."),
|
|
708
766
|
},
|
|
709
767
|
},
|
|
710
|
-
async ({ route, viewport, theme, wait_for, full_page, selector, output_dir }) => {
|
|
768
|
+
async ({ route, viewport, scale, theme, wait_for, full_page, selector, output_dir }) => {
|
|
711
769
|
try {
|
|
712
770
|
return await takeScreenshot(projectCwd, {
|
|
713
771
|
route,
|
|
714
772
|
viewport,
|
|
773
|
+
scale,
|
|
715
774
|
theme,
|
|
716
775
|
wait_for,
|
|
717
776
|
full_page,
|
|
@@ -794,13 +853,14 @@ server.registerTool(
|
|
|
794
853
|
inputSchema: {
|
|
795
854
|
captures: z.array(webBatchCaptureSchema).describe("Array of captures — each with screen name and route"),
|
|
796
855
|
viewport: z.object({ width: z.number().default(1280), height: z.number().default(800) }).optional().describe("Viewport dimensions for all captures"),
|
|
856
|
+
scale: z.number().optional().default(2).describe("Device pixel ratio for all captures (default 2)"),
|
|
797
857
|
theme: z.enum(["light", "dark"]).optional().describe("Force color scheme for all captures"),
|
|
798
858
|
output_dir: z.string().optional().describe("Directory to save all PNGs (relative to web app root)"),
|
|
799
859
|
},
|
|
800
860
|
},
|
|
801
|
-
async ({ captures, viewport, theme, output_dir }) => {
|
|
861
|
+
async ({ captures, viewport, scale, theme, output_dir }) => {
|
|
802
862
|
try {
|
|
803
|
-
return await takeScreenshotBatch(projectCwd, { captures, viewport, theme, output_dir });
|
|
863
|
+
return await takeScreenshotBatch(projectCwd, { captures, viewport, scale, theme, output_dir });
|
|
804
864
|
} catch (err) {
|
|
805
865
|
return toolError(err);
|
|
806
866
|
}
|
package/mcp-server/screenshot.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { findProjectDir } from "../drift/index.js";
|
|
|
17
17
|
export interface ScreenshotOptions {
|
|
18
18
|
route: string;
|
|
19
19
|
viewport?: { width: number; height: number };
|
|
20
|
+
scale?: number;
|
|
20
21
|
theme?: "light" | "dark";
|
|
21
22
|
wait_for?: number;
|
|
22
23
|
full_page?: boolean;
|
|
@@ -177,6 +178,7 @@ export async function takeScreenshot(
|
|
|
177
178
|
const {
|
|
178
179
|
route = "/",
|
|
179
180
|
viewport = { width: 1280, height: 800 },
|
|
181
|
+
scale = 2,
|
|
180
182
|
theme,
|
|
181
183
|
wait_for = 1000,
|
|
182
184
|
full_page = false,
|
|
@@ -192,7 +194,11 @@ export async function takeScreenshot(
|
|
|
192
194
|
// 2. Navigate
|
|
193
195
|
const page = await browser.newPage();
|
|
194
196
|
try {
|
|
195
|
-
await page.setViewport({
|
|
197
|
+
await page.setViewport({
|
|
198
|
+
width: viewport.width,
|
|
199
|
+
height: viewport.height,
|
|
200
|
+
deviceScaleFactor: scale,
|
|
201
|
+
});
|
|
196
202
|
|
|
197
203
|
if (theme) {
|
|
198
204
|
await page.emulateMediaFeatures([
|
|
@@ -245,6 +251,7 @@ export async function takeScreenshot(
|
|
|
245
251
|
route,
|
|
246
252
|
url: targetUrl,
|
|
247
253
|
viewport,
|
|
254
|
+
scale,
|
|
248
255
|
theme: theme ?? "default",
|
|
249
256
|
full_page,
|
|
250
257
|
selector: selector ?? null,
|
|
@@ -271,6 +278,7 @@ export interface WebBatchCapture {
|
|
|
271
278
|
export interface WebScreenshotBatchOptions {
|
|
272
279
|
captures: WebBatchCapture[];
|
|
273
280
|
viewport?: { width: number; height: number };
|
|
281
|
+
scale?: number;
|
|
274
282
|
theme?: "light" | "dark";
|
|
275
283
|
output_dir?: string;
|
|
276
284
|
}
|
|
@@ -281,7 +289,7 @@ export async function takeScreenshotBatch(
|
|
|
281
289
|
projectCwd: string,
|
|
282
290
|
options: WebScreenshotBatchOptions,
|
|
283
291
|
): Promise<ScreenshotResult> {
|
|
284
|
-
const { captures, viewport = { width: 1280, height: 800 }, theme, output_dir } = options;
|
|
292
|
+
const { captures, viewport = { width: 1280, height: 800 }, scale = 2, theme, output_dir } = options;
|
|
285
293
|
|
|
286
294
|
if (captures.length === 0) {
|
|
287
295
|
return { content: [{ type: "text", text: "No web captures specified." }], isError: true };
|
|
@@ -293,7 +301,11 @@ export async function takeScreenshotBatch(
|
|
|
293
301
|
const page = await browser.newPage();
|
|
294
302
|
|
|
295
303
|
try {
|
|
296
|
-
await page.setViewport({
|
|
304
|
+
await page.setViewport({
|
|
305
|
+
width: viewport.width,
|
|
306
|
+
height: viewport.height,
|
|
307
|
+
deviceScaleFactor: scale,
|
|
308
|
+
});
|
|
297
309
|
if (theme) {
|
|
298
310
|
await page.emulateMediaFeatures([{ name: "prefers-color-scheme", value: theme }]);
|
|
299
311
|
}
|
|
@@ -330,7 +342,10 @@ export async function takeScreenshotBatch(
|
|
|
330
342
|
const content: ScreenshotResult["content"] = [];
|
|
331
343
|
for (const s of snapshots) {
|
|
332
344
|
content.push({ type: "image" as const, data: s.data, mimeType: "image/png" });
|
|
333
|
-
content.push({
|
|
345
|
+
content.push({
|
|
346
|
+
type: "text" as const,
|
|
347
|
+
text: JSON.stringify({ screen: s.screen, path: s.path, viewport, scale, theme: themeLabel }, null, 2),
|
|
348
|
+
});
|
|
334
349
|
}
|
|
335
350
|
return { content };
|
|
336
351
|
} finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openuispec",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A semantic UI specification format for AI-native, platform-native app development",
|
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
"schema/",
|
|
20
20
|
"spec/",
|
|
21
21
|
"docs/",
|
|
22
|
-
"examples
|
|
22
|
+
"examples/*/openuispec/**",
|
|
23
|
+
"examples/*/openuispec.yaml",
|
|
24
|
+
"examples/*/README.md",
|
|
25
|
+
"scripts/",
|
|
23
26
|
"README.md",
|
|
24
27
|
"LICENSE"
|
|
25
28
|
],
|