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.
Files changed (237) hide show
  1. package/README.md +3 -1
  2. package/check/index.ts +17 -0
  3. package/cli/index.ts +21 -3
  4. package/cli/init.ts +224 -10
  5. package/docs/cli.md +13 -8
  6. package/docs/file-formats.md +36 -0
  7. package/docs/implementation-notes.md +7 -0
  8. package/drift/index.ts +281 -40
  9. package/mcp-server/index.ts +179 -119
  10. package/mcp-server/screenshot.ts +19 -4
  11. package/package.json +5 -2
  12. package/prepare/index.ts +155 -18
  13. package/schema/openuispec.schema.json +59 -0
  14. package/schema/semantic-lint.ts +25 -1
  15. package/scripts/take-all-screenshots.ts +507 -0
  16. package/spec/openuispec-v0.1.md +13 -0
  17. package/status/index.ts +72 -2
  18. package/examples/social-app/.mcp.json +0 -10
  19. package/examples/social-app/AGENTS.md +0 -124
  20. package/examples/social-app/CLAUDE.md +0 -124
  21. package/examples/social-app/backend/.gitkeep +0 -1
  22. package/examples/social-app/generated/android/social-app/app/.paparazzi-hashes.json +0 -3
  23. package/examples/social-app/generated/android/social-app/app/build.gradle.kts +0 -94
  24. package/examples/social-app/generated/android/social-app/app/src/main/AndroidManifest.xml +0 -26
  25. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/AppContainer.kt +0 -20
  26. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/MainActivity.kt +0 -35
  27. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/SocialAppApplication.kt +0 -13
  28. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/MockData.kt +0 -98
  29. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/AppPreferences.kt +0 -19
  30. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/DataStorePreferencesRepository.kt +0 -68
  31. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/PreferencesRepository.kt +0 -15
  32. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/model/Models.kt +0 -34
  33. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/MainShell.kt +0 -390
  34. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/Components.kt +0 -234
  35. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/ContractPrimitives.kt +0 -641
  36. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/navigation/RootComponent.kt +0 -113
  37. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ChatDetailScreen.kt +0 -212
  38. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/CreatePostScreen.kt +0 -113
  39. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/DiscoverScreen.kt +0 -137
  40. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/EditProfileScreen.kt +0 -180
  41. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/HomeFeedScreen.kt +0 -169
  42. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/MessagesInboxScreen.kt +0 -85
  43. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/NotificationsScreen.kt +0 -74
  44. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/PostDetailScreen.kt +0 -293
  45. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ProfileSelfScreen.kt +0 -116
  46. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SearchResultsScreen.kt +0 -161
  47. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsScreen.kt +0 -164
  48. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsStore.kt +0 -95
  49. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/UserProfileScreen.kt +0 -123
  50. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Color.kt +0 -33
  51. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Shape.kt +0 -41
  52. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Spacing.kt +0 -20
  53. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Theme.kt +0 -82
  54. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Type.kt +0 -60
  55. package/examples/social-app/generated/android/social-app/app/src/main/res/drawable/ic_launcher_foreground.xml +0 -9
  56. package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +0 -5
  57. package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +0 -5
  58. package/examples/social-app/generated/android/social-app/app/src/main/res/values/strings.xml +0 -91
  59. package/examples/social-app/generated/android/social-app/app/src/main/res/values/themes.xml +0 -10
  60. package/examples/social-app/generated/android/social-app/app/src/main/res/values-ru/strings.xml +0 -79
  61. package/examples/social-app/generated/android/social-app/app/src/main/res/values-uz/strings.xml +0 -79
  62. package/examples/social-app/generated/android/social-app/app/src/main/xml/AndroidManifest.xml +0 -23
  63. package/examples/social-app/generated/android/social-app/app/src/test/kotlin/com/social/app/screenshots/HomeFeedScreenshotTest.kt +0 -34
  64. package/examples/social-app/generated/android/social-app/build.gradle.kts +0 -7
  65. package/examples/social-app/generated/android/social-app/gradle/libs.versions.toml +0 -50
  66. package/examples/social-app/generated/android/social-app/gradle/wrapper/gradle-wrapper.jar +0 -0
  67. package/examples/social-app/generated/android/social-app/gradle/wrapper/gradle-wrapper.properties +0 -8
  68. package/examples/social-app/generated/android/social-app/gradle.properties +0 -11
  69. package/examples/social-app/generated/android/social-app/gradlew +0 -248
  70. package/examples/social-app/generated/android/social-app/settings.gradle.kts +0 -27
  71. package/examples/social-app/generated/web/social-app/index.html +0 -12
  72. package/examples/social-app/generated/web/social-app/package-lock.json +0 -2517
  73. package/examples/social-app/generated/web/social-app/package.json +0 -27
  74. package/examples/social-app/generated/web/social-app/src/app/App.tsx +0 -58
  75. package/examples/social-app/generated/web/social-app/src/components/Shell.tsx +0 -259
  76. package/examples/social-app/generated/web/social-app/src/components/cards.tsx +0 -317
  77. package/examples/social-app/generated/web/social-app/src/components/ui.tsx +0 -340
  78. package/examples/social-app/generated/web/social-app/src/flows/CreatePostFlow.tsx +0 -86
  79. package/examples/social-app/generated/web/social-app/src/i18n.tsx +0 -59
  80. package/examples/social-app/generated/web/social-app/src/lib/icons.tsx +0 -85
  81. package/examples/social-app/generated/web/social-app/src/lib/tokens.ts +0 -70
  82. package/examples/social-app/generated/web/social-app/src/lib/utils.ts +0 -97
  83. package/examples/social-app/generated/web/social-app/src/locales/en.json +0 -67
  84. package/examples/social-app/generated/web/social-app/src/locales/ru.json +0 -67
  85. package/examples/social-app/generated/web/social-app/src/locales/uz.json +0 -67
  86. package/examples/social-app/generated/web/social-app/src/main.tsx +0 -16
  87. package/examples/social-app/generated/web/social-app/src/screens/ChatDetailScreen.tsx +0 -90
  88. package/examples/social-app/generated/web/social-app/src/screens/DiscoverScreen.tsx +0 -86
  89. package/examples/social-app/generated/web/social-app/src/screens/EditProfileScreen.tsx +0 -57
  90. package/examples/social-app/generated/web/social-app/src/screens/HomeFeedScreen.tsx +0 -103
  91. package/examples/social-app/generated/web/social-app/src/screens/MessagesInboxScreen.tsx +0 -52
  92. package/examples/social-app/generated/web/social-app/src/screens/NotificationsScreen.tsx +0 -41
  93. package/examples/social-app/generated/web/social-app/src/screens/PostDetailScreen.tsx +0 -115
  94. package/examples/social-app/generated/web/social-app/src/screens/ProfileSelfScreen.tsx +0 -57
  95. package/examples/social-app/generated/web/social-app/src/screens/ProfileUserScreen.tsx +0 -76
  96. package/examples/social-app/generated/web/social-app/src/screens/SearchResultsScreen.tsx +0 -96
  97. package/examples/social-app/generated/web/social-app/src/screens/SettingsScreen.tsx +0 -79
  98. package/examples/social-app/generated/web/social-app/src/state/store.ts +0 -592
  99. package/examples/social-app/generated/web/social-app/src/styles.css +0 -125
  100. package/examples/social-app/generated/web/social-app/src/vite-env.d.ts +0 -1
  101. package/examples/social-app/generated/web/social-app/tsconfig.json +0 -22
  102. package/examples/social-app/generated/web/social-app/tsconfig.node.json +0 -13
  103. package/examples/social-app/generated/web/social-app/tsconfig.node.tsbuildinfo +0 -1
  104. package/examples/social-app/generated/web/social-app/tsconfig.tsbuildinfo +0 -1
  105. package/examples/social-app/generated/web/social-app/vite.config.d.ts +0 -2
  106. package/examples/social-app/generated/web/social-app/vite.config.js +0 -6
  107. package/examples/social-app/generated/web/social-app/vite.config.ts +0 -7
  108. package/examples/social-app/package.json +0 -13
  109. package/examples/social-app/take-web-screenshots.ts +0 -97
  110. package/examples/taskflow/.codex/config.toml +0 -4
  111. package/examples/taskflow/.mcp.json +0 -10
  112. package/examples/taskflow/AGENTS.md +0 -124
  113. package/examples/taskflow/CLAUDE.md +0 -124
  114. package/examples/taskflow/backend/.gitkeep +0 -1
  115. package/examples/taskflow/generated/android/TaskFlow/README.md +0 -43
  116. package/examples/taskflow/generated/android/TaskFlow/app/build.gradle.kts +0 -76
  117. package/examples/taskflow/generated/android/TaskFlow/app/proguard-rules.pro +0 -1
  118. package/examples/taskflow/generated/android/TaskFlow/app/src/main/AndroidManifest.xml +0 -21
  119. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/MainActivity.kt +0 -19
  120. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/TaskFlowApp.kt +0 -283
  121. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/DomainModels.kt +0 -106
  122. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/SampleData.kt +0 -57
  123. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/components/Common.kt +0 -109
  124. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/HomeScreen.kt +0 -112
  125. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/ProjectsScreen.kt +0 -61
  126. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/SettingsScreen.kt +0 -82
  127. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/TaskDetailScreen.kt +0 -111
  128. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/sheets/Sheets.kt +0 -77
  129. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Color.kt +0 -30
  130. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Theme.kt +0 -86
  131. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Type.kt +0 -57
  132. package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/strings.xml +0 -155
  133. package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/themes.xml +0 -4
  134. package/examples/taskflow/generated/android/TaskFlow/build.gradle.kts +0 -5
  135. package/examples/taskflow/generated/android/TaskFlow/gradle/gradle-daemon-jvm.properties +0 -12
  136. package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.jar +0 -0
  137. package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.properties +0 -7
  138. package/examples/taskflow/generated/android/TaskFlow/gradle.properties +0 -4
  139. package/examples/taskflow/generated/android/TaskFlow/gradlew +0 -18
  140. package/examples/taskflow/generated/android/TaskFlow/gradlew.bat +0 -12
  141. package/examples/taskflow/generated/android/TaskFlow/settings.gradle.kts +0 -18
  142. package/examples/taskflow/generated/ios/TaskFlow/README.md +0 -21
  143. package/examples/taskflow/generated/ios/TaskFlow/Resources/en.lproj/Localizable.strings +0 -115
  144. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/App/TaskFlowApp.swift +0 -24
  145. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Components/AppChrome.swift +0 -150
  146. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Flows/TaskEditorSheet.swift +0 -220
  147. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Models/DomainModels.swift +0 -122
  148. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/CalendarView.swift +0 -21
  149. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/HomeView.swift +0 -201
  150. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProfileEditView.swift +0 -48
  151. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectDetailView.swift +0 -59
  152. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectsView.swift +0 -63
  153. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/SettingsView.swift +0 -85
  154. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/TaskDetailView.swift +0 -219
  155. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppModel.swift +0 -320
  156. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppSupport.swift +0 -41
  157. package/examples/taskflow/generated/ios/TaskFlow/project.yml +0 -31
  158. package/examples/taskflow/generated/web/TaskFlow/README.md +0 -19
  159. package/examples/taskflow/generated/web/TaskFlow/index.html +0 -12
  160. package/examples/taskflow/generated/web/TaskFlow/package-lock.json +0 -1908
  161. package/examples/taskflow/generated/web/TaskFlow/package.json +0 -24
  162. package/examples/taskflow/generated/web/TaskFlow/src/App.tsx +0 -58
  163. package/examples/taskflow/generated/web/TaskFlow/src/AppShell.tsx +0 -55
  164. package/examples/taskflow/generated/web/TaskFlow/src/components/Common.tsx +0 -82
  165. package/examples/taskflow/generated/web/TaskFlow/src/components/Modals.tsx +0 -191
  166. package/examples/taskflow/generated/web/TaskFlow/src/components/Nav.tsx +0 -41
  167. package/examples/taskflow/generated/web/TaskFlow/src/generated/messages.ts +0 -131
  168. package/examples/taskflow/generated/web/TaskFlow/src/hooks.ts +0 -25
  169. package/examples/taskflow/generated/web/TaskFlow/src/i18n.ts +0 -39
  170. package/examples/taskflow/generated/web/TaskFlow/src/locales.en.json +0 -111
  171. package/examples/taskflow/generated/web/TaskFlow/src/main.tsx +0 -13
  172. package/examples/taskflow/generated/web/TaskFlow/src/screens/HomeScreen.tsx +0 -111
  173. package/examples/taskflow/generated/web/TaskFlow/src/screens/ProjectsScreen.tsx +0 -82
  174. package/examples/taskflow/generated/web/TaskFlow/src/screens/SettingsScreens.tsx +0 -132
  175. package/examples/taskflow/generated/web/TaskFlow/src/screens/TaskDetail.tsx +0 -105
  176. package/examples/taskflow/generated/web/TaskFlow/src/store.ts +0 -216
  177. package/examples/taskflow/generated/web/TaskFlow/src/styles.css +0 -617
  178. package/examples/taskflow/generated/web/TaskFlow/src/types.ts +0 -64
  179. package/examples/taskflow/generated/web/TaskFlow/src/utils.ts +0 -78
  180. package/examples/taskflow/generated/web/TaskFlow/tsconfig.json +0 -21
  181. package/examples/taskflow/generated/web/TaskFlow/vite.config.ts +0 -6
  182. package/examples/todo-orbit/.codex/config.toml +0 -4
  183. package/examples/todo-orbit/.mcp.json +0 -10
  184. package/examples/todo-orbit/AGENTS.md +0 -124
  185. package/examples/todo-orbit/CLAUDE.md +0 -124
  186. package/examples/todo-orbit/backend/.gitkeep +0 -1
  187. package/examples/todo-orbit/generated/android/Todo Orbit/README.md +0 -14
  188. package/examples/todo-orbit/generated/android/Todo Orbit/app/build.gradle.kts +0 -58
  189. package/examples/todo-orbit/generated/android/Todo Orbit/app/proguard-rules.pro +0 -1
  190. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/AndroidManifest.xml +0 -20
  191. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/MainActivity.kt +0 -14
  192. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/TodoOrbitApp.kt +0 -345
  193. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/AppLogic.kt +0 -231
  194. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Models.kt +0 -169
  195. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Strings.kt +0 -8
  196. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/components/CommonComponents.kt +0 -236
  197. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/AnalyticsScreen.kt +0 -193
  198. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/SettingsScreen.kt +0 -102
  199. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/TasksScreen.kt +0 -347
  200. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/sheets/EditorSheets.kt +0 -347
  201. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/theme/TodoOrbitTheme.kt +0 -59
  202. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values/strings.xml +0 -149
  203. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values-ru/strings.xml +0 -155
  204. package/examples/todo-orbit/generated/android/Todo Orbit/build.gradle.kts +0 -4
  205. package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.jar +0 -0
  206. package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.properties +0 -7
  207. package/examples/todo-orbit/generated/android/Todo Orbit/gradle.properties +0 -4
  208. package/examples/todo-orbit/generated/android/Todo Orbit/gradlew +0 -248
  209. package/examples/todo-orbit/generated/android/Todo Orbit/gradlew.bat +0 -93
  210. package/examples/todo-orbit/generated/android/Todo Orbit/settings.gradle.kts +0 -18
  211. package/examples/todo-orbit/generated/ios/Todo Orbit/.screenshot-uitest/Sources/ScreenshotUITest.swift +0 -36
  212. package/examples/todo-orbit/generated/ios/Todo Orbit/README.md +0 -29
  213. package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/en.lproj/Localizable.strings +0 -119
  214. package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/ru.lproj/Localizable.strings +0 -119
  215. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/App/TodoOrbitApp.swift +0 -50
  216. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/OrbitChrome.swift +0 -204
  217. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/SchedulePreviewView.swift +0 -126
  218. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/TrendChartView.swift +0 -70
  219. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/RecurringRuleSheet.swift +0 -126
  220. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/TaskEditorSheet.swift +0 -61
  221. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Models/DomainModels.swift +0 -238
  222. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/AnalyticsView.swift +0 -94
  223. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/SettingsView.swift +0 -76
  224. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/TasksHomeView.swift +0 -364
  225. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Support/AppModel.swift +0 -324
  226. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.pbxproj +0 -439
  227. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  228. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/xcshareddata/xcschemes/TodoOrbit.xcscheme +0 -89
  229. package/examples/todo-orbit/generated/ios/Todo Orbit/project.yml +0 -32
  230. package/examples/todo-orbit/generated/web/Todo Orbit/index.html +0 -16
  231. package/examples/todo-orbit/generated/web/Todo Orbit/package-lock.json +0 -1087
  232. package/examples/todo-orbit/generated/web/Todo Orbit/package.json +0 -24
  233. package/examples/todo-orbit/generated/web/Todo Orbit/src/App.tsx +0 -2167
  234. package/examples/todo-orbit/generated/web/Todo Orbit/src/main.tsx +0 -13
  235. package/examples/todo-orbit/generated/web/Todo Orbit/src/styles.css +0 -926
  236. package/examples/todo-orbit/generated/web/Todo Orbit/tsconfig.json +0 -19
  237. package/examples/todo-orbit/generated/web/Todo Orbit/vite.config.ts +0 -6
@@ -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: [{ type: "text"; text: string }] } {
63
- return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
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: `This project uses OpenUISpec — a semantic UI specification format.
112
- Spec files (YAML) are the single source of truth for all UI across platforms.
113
-
114
- MANDATORY WORKFLOW for any UI-related request (screens, navigation, layout, tokens, flows, localization):
115
-
116
- PRE-GENERATION:
117
- 1. Call openuispec_prepare with the target platform.
118
- 2. Call openuispec_read_specs to load the spec file contents you need.
119
- Use the returned contents as the AUTHORITATIVE source do NOT paraphrase from memory.
120
- Cross-reference exact token values, contract must_handle lists, and locale keys from the content.
121
- 3. If the request requires spec changes, update the spec files FIRST, then call openuispec_check.
122
- 4. Generate or update the platform UI code based on the spec contents.
123
-
124
- POST-GENERATION (do this EVERY TIME after writing UI code):
125
- 5. Call openuispec_check to validate spec integrity.
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 initiativethe 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. Call openuispec_read_specs afterward to load the actual spec file contents you need for generation.",
172
- inputSchema: { target: targetSchema },
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
- return toolResult(buildPrepareResult(target, projectCwd));
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
- "POST-GENERATION AUDITverify your code against these concrete spec requirements:",
167
+ "SPEC-DERIVED CHECKLISTthis 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
- "HOW TO AUDIT: For each item below, READ the generated component/screen file,",
190
- "find the code that implements it, and confirm the values match exactly.",
191
- "If you cannot find the implementation, it is a REAL GAP — fix it.",
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(fsReadFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
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(fsReadFileSync(join(contractsDir, file), "utf-8"));
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(fsReadFileSync(join(screensDir, file), "utf-8"));
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(fsReadFileSync(join(localesDir, file), "utf-8")));
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(fsReadFileSync(platformPath, "utf-8"));
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: "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).",
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
- screens: z.array(z.string()).optional().describe("Screen names to audit (e.g. ['home_feed', 'settings']). If omitted, audits all screens."),
343
- contracts: z.array(z.string()).optional().describe("Contract names to audit (e.g. ['action_trigger']). If omitted, audits all contracts."),
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 screenFilter = screens && screens.length > 0 ? screens : undefined;
351
- const contractFilter = contracts && contracts.length > 0 ? contracts : undefined;
352
- const audit = buildAuditChecklist(projectDir, target, screenFilter, contractFilter);
353
- return {
354
- content: [
355
- { type: "text" as const, text: JSON.stringify(result, null, 2) },
356
- { type: "text" as const, text: audit },
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
- return toolResult(buildValidateResult(groups, projectCwd));
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 the full contents of spec files. Call after openuispec_prepare to load the actual YAML/JSON content. Pass specific file paths from the prepare output, or omit to read all spec files. Use these contents as the authoritative source do NOT paraphrase from memory.",
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("Specific spec file paths to read (relative to spec root, e.g. 'screens/home.yaml'). If omitted, reads all spec files."),
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
- const filesToRead = paths && paths.length > 0
421
- ? allFiles.filter((f) => {
422
- const rel = relative(projectDir, f);
423
- return paths.some((p) => rel === p || rel.endsWith(p));
424
- })
425
- : allFiles;
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: fsReadFileSync(f, "utf-8"),
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 to see property-level changes.",
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
- return toolResult(result);
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 full 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. Call openuispec_spec_types first to see available types.",
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 to get schema for (e.g. 'screen', 'tokens/color', 'contract'). Use openuispec_spec_types to list all available types."),
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(fsReadFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
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 = fsReadFileSync(filePath, "utf-8");
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(fsReadFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
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 = fsReadFileSync(filePath, "utf-8");
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(fsReadFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
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 = fsReadFileSync(filePath, "utf-8");
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(fsReadFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
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 = fsReadFileSync(filePath, "utf-8");
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
- if (key in content) {
678
- filtered[key] = content[key];
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
  }
@@ -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({ width: viewport.width, height: viewport.height });
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({ width: viewport.width, height: viewport.height });
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({ type: "text" as const, text: JSON.stringify({ screen: s.screen, path: s.path, theme: themeLabel }, null, 2) });
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.10",
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
  ],