handoff-app 2.0.0-1 → 2.0.0-10

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 (550) hide show
  1. package/AGENTS.md +7 -0
  2. package/CLAUDE.md +7 -0
  3. package/README.md +732 -304
  4. package/UPGRADE.md +218 -0
  5. package/config/config.template.js +1 -4
  6. package/config/config.template.ts +1 -2
  7. package/config/docs/foundations/colors/guidelines.md +9 -0
  8. package/config/docs/foundations/effects/guidelines.md +9 -0
  9. package/config/docs/foundations/effects/resources.md +9 -0
  10. package/config/docs/foundations/grid.md +10 -0
  11. package/config/docs/foundations/icons/guidelines.md +9 -0
  12. package/config/docs/foundations/images/guidelines.md +9 -0
  13. package/config/docs/foundations/images/resources.md +9 -0
  14. package/config/docs/foundations/images.md +10 -0
  15. package/config/docs/foundations/logo/resources.md +9 -0
  16. package/config/docs/foundations/typography/guidelines.md +9 -0
  17. package/config/docs/foundations/typography/resources.md +9 -0
  18. package/config/docs/foundations.md +5 -3
  19. package/dist/ai/connections.d.ts +64 -0
  20. package/dist/ai/connections.js +136 -0
  21. package/dist/ai/define.d.ts +10 -0
  22. package/dist/ai/define.js +13 -0
  23. package/dist/ai/types.d.ts +27 -0
  24. package/dist/ai/types.js +10 -0
  25. package/dist/app-builder/build.d.ts +12 -0
  26. package/dist/app-builder/build.js +227 -69
  27. package/dist/app-builder/client-config.js +16 -12
  28. package/dist/app-builder/config-diff/strategies/pattern.js +3 -3
  29. package/dist/app-builder/nav-shell.js +8 -15
  30. package/dist/app-builder/paths.js +7 -9
  31. package/dist/app-builder/static-export.js +6 -6
  32. package/dist/app-builder/vercel-output.js +6 -6
  33. package/dist/app-builder/watchers/component.js +5 -5
  34. package/dist/app-builder/watchers/utils.d.ts +2 -3
  35. package/dist/app-builder/watchers.d.ts +8 -15
  36. package/dist/app-builder/watchers.js +70 -61
  37. package/dist/artifacts/url.d.ts +7 -7
  38. package/dist/artifacts/url.js +7 -7
  39. package/dist/cache/build-cache.js +23 -23
  40. package/dist/catalog/define.d.ts +6 -0
  41. package/dist/catalog/define.js +72 -0
  42. package/dist/catalog/implementation.d.ts +27 -0
  43. package/dist/catalog/implementation.js +133 -0
  44. package/dist/catalog/index.d.ts +6 -0
  45. package/dist/catalog/index.js +22 -0
  46. package/dist/catalog/normalize.d.ts +16 -0
  47. package/dist/catalog/normalize.js +118 -0
  48. package/dist/catalog/previews.d.ts +18 -0
  49. package/dist/catalog/previews.js +107 -0
  50. package/dist/catalog/renderers.d.ts +43 -0
  51. package/dist/catalog/renderers.js +62 -0
  52. package/dist/catalog/types.d.ts +71 -0
  53. package/dist/cli/auth/browser.d.ts +2 -0
  54. package/dist/cli/auth/browser.js +33 -0
  55. package/dist/cli/auth/device.d.ts +16 -0
  56. package/dist/cli/auth/device.js +158 -0
  57. package/dist/cli/auth/store.d.ts +38 -0
  58. package/dist/cli/auth/store.js +199 -0
  59. package/dist/cli/create.js +48 -3
  60. package/dist/cli/make.d.ts +0 -5
  61. package/dist/cli/make.js +24 -69
  62. package/dist/cli/scaffold.js +39 -181
  63. package/dist/cli.js +39 -7
  64. package/dist/commands/build/app.js +1 -5
  65. package/dist/commands/build/components.js +1 -5
  66. package/dist/commands/build/index.js +1 -5
  67. package/dist/commands/checkout/index.d.ts +18 -12
  68. package/dist/commands/checkout/index.js +19 -59
  69. package/dist/commands/compat/deprecations.d.ts +46 -0
  70. package/dist/commands/compat/deprecations.js +62 -0
  71. package/dist/commands/compat/push-args.d.ts +48 -0
  72. package/dist/commands/compat/push-args.js +73 -0
  73. package/dist/commands/db/migrate.js +1 -5
  74. package/dist/commands/dev/index.js +1 -5
  75. package/dist/commands/eject/config.js +1 -5
  76. package/dist/commands/eject/pages.js +1 -5
  77. package/dist/commands/eject/theme.js +1 -5
  78. package/dist/commands/fetch/index.js +1 -5
  79. package/dist/commands/index.js +8 -2
  80. package/dist/commands/login/index.d.ts +13 -0
  81. package/dist/commands/login/index.js +61 -0
  82. package/dist/commands/logout/index.d.ts +12 -0
  83. package/dist/commands/logout/index.js +89 -0
  84. package/dist/commands/make/component.js +1 -5
  85. package/dist/commands/make/page.js +3 -7
  86. package/dist/commands/publish/index.d.ts +21 -11
  87. package/dist/commands/publish/index.js +22 -58
  88. package/dist/commands/pull/index.d.ts +13 -0
  89. package/dist/commands/pull/index.js +20 -0
  90. package/dist/commands/push/index.d.ts +23 -0
  91. package/dist/commands/push/index.js +58 -0
  92. package/dist/commands/scaffold/index.js +1 -5
  93. package/dist/commands/start/index.js +1 -5
  94. package/dist/commands/types.d.ts +12 -0
  95. package/dist/commands/utils.d.ts +100 -1
  96. package/dist/commands/utils.js +103 -2
  97. package/dist/commands/validate/components.js +1 -5
  98. package/dist/config/catalog-include.d.ts +43 -0
  99. package/dist/config/catalog-include.js +203 -0
  100. package/dist/config/defaults.d.ts +4 -2
  101. package/dist/config/defaults.js +58 -49
  102. package/dist/config/env.d.ts +9 -0
  103. package/dist/config/env.js +43 -0
  104. package/dist/config/errors.d.ts +4 -0
  105. package/dist/config/errors.js +11 -0
  106. package/dist/config/from-env.d.ts +24 -0
  107. package/dist/config/from-env.js +7 -0
  108. package/dist/config/helpers.d.ts +2 -0
  109. package/dist/config/helpers.js +3 -1
  110. package/dist/config/index.d.ts +6 -2
  111. package/dist/config/index.js +12 -1
  112. package/dist/config/loader.d.ts +40 -9
  113. package/dist/config/loader.js +148 -35
  114. package/dist/config/normalizers/page.d.ts +4 -4
  115. package/dist/config/normalizers/page.js +2 -2
  116. package/dist/config/resolve-env.d.ts +6 -0
  117. package/dist/config/resolve-env.js +115 -0
  118. package/dist/config/runtime.d.ts +15 -16
  119. package/dist/config/runtime.js +178 -196
  120. package/dist/config/validator.d.ts +2 -2
  121. package/dist/config/validator.js +2 -4
  122. package/dist/handlebars.d.ts +17 -0
  123. package/dist/handlebars.js +16 -0
  124. package/dist/index.d.ts +120 -27
  125. package/dist/index.js +242 -53
  126. package/dist/nav/resolver.js +3 -1
  127. package/dist/nav/workspace.js +2 -2
  128. package/dist/pattern.d.ts +7 -0
  129. package/dist/pattern.js +13 -0
  130. package/dist/pipeline/index.d.ts +1 -1
  131. package/dist/pipeline/index.js +1 -1
  132. package/dist/react.d.ts +43 -0
  133. package/dist/react.js +11 -0
  134. package/dist/registry/asset-storage/adapters/vercel-blob.js +1 -1
  135. package/dist/registry/asset-storage/define.d.ts +1 -1
  136. package/dist/registry/asset-storage/define.js +1 -1
  137. package/dist/registry/asset-storage/resolve.d.ts +3 -3
  138. package/dist/registry/asset-storage/resolve.js +5 -2
  139. package/dist/registry/auth/access-tokens.d.ts +54 -0
  140. package/dist/registry/auth/access-tokens.js +191 -0
  141. package/dist/registry/auth/action-tokens.d.ts +66 -0
  142. package/dist/registry/auth/action-tokens.js +178 -0
  143. package/dist/registry/auth/ai-keys.d.ts +32 -0
  144. package/dist/registry/auth/ai-keys.js +69 -0
  145. package/dist/registry/auth/crypto.d.ts +21 -0
  146. package/dist/registry/auth/crypto.js +76 -0
  147. package/dist/registry/auth/database.d.ts +7 -0
  148. package/dist/registry/auth/database.js +22 -0
  149. package/dist/registry/auth/device-authorizations.d.ts +51 -0
  150. package/dist/registry/auth/device-authorizations.js +170 -0
  151. package/dist/registry/auth/gravatar.d.ts +1 -0
  152. package/dist/registry/auth/gravatar.js +8 -0
  153. package/dist/registry/auth/index.d.ts +13 -0
  154. package/dist/registry/auth/index.js +89 -0
  155. package/dist/registry/auth/installation.d.ts +42 -0
  156. package/dist/registry/auth/installation.js +105 -0
  157. package/dist/registry/auth/password.d.ts +12 -0
  158. package/dist/registry/auth/password.js +58 -0
  159. package/dist/registry/auth/rate-limits.d.ts +22 -0
  160. package/dist/registry/auth/rate-limits.js +75 -0
  161. package/dist/registry/auth/sync-secret.d.ts +27 -0
  162. package/dist/registry/auth/sync-secret.js +65 -0
  163. package/dist/registry/auth/types.d.ts +45 -0
  164. package/dist/registry/auth/types.js +11 -0
  165. package/dist/registry/auth/user-record.d.ts +13 -0
  166. package/dist/registry/auth/user-record.js +16 -0
  167. package/dist/registry/auth/users.d.ts +31 -0
  168. package/dist/registry/auth/users.js +135 -0
  169. package/dist/registry/auth/validation.d.ts +14 -0
  170. package/dist/registry/auth/validation.js +45 -0
  171. package/dist/registry/checkout/assets.d.ts +1 -1
  172. package/dist/registry/checkout/assets.js +25 -8
  173. package/dist/registry/checkout/index.d.ts +27 -6
  174. package/dist/registry/checkout/index.js +479 -131
  175. package/dist/registry/checkout/tokens.d.ts +1 -1
  176. package/dist/registry/checkout/tokens.js +16 -7
  177. package/dist/registry/client.d.ts +2 -1
  178. package/dist/registry/client.js +8 -0
  179. package/dist/registry/connection.d.ts +34 -11
  180. package/dist/registry/connection.js +77 -18
  181. package/dist/registry/content-kinds.d.ts +27 -0
  182. package/dist/registry/content-kinds.js +33 -0
  183. package/dist/registry/db/client.d.ts +14 -9
  184. package/dist/registry/db/client.js +22 -9
  185. package/dist/registry/db/driver.d.ts +39 -0
  186. package/dist/registry/db/driver.js +50 -0
  187. package/dist/registry/db/index.d.ts +2 -2
  188. package/dist/registry/db/index.js +14 -9
  189. package/dist/registry/db/migrate.d.ts +1 -1
  190. package/dist/registry/db/migrate.js +5 -5
  191. package/dist/registry/db/schema.d.ts +2992 -963
  192. package/dist/registry/db/schema.js +158 -8
  193. package/dist/registry/publish/assets.d.ts +2 -2
  194. package/dist/registry/publish/assets.js +46 -35
  195. package/dist/registry/publish/errors.js +3 -3
  196. package/dist/registry/publish/index.d.ts +45 -3
  197. package/dist/registry/publish/index.js +247 -24
  198. package/dist/registry/publish/package.js +14 -1
  199. package/dist/registry/publish/tokens.d.ts +2 -2
  200. package/dist/registry/publish/tokens.js +30 -24
  201. package/dist/registry/selection.d.ts +32 -0
  202. package/dist/registry/selection.js +58 -0
  203. package/dist/registry/transfer.d.ts +13 -0
  204. package/dist/store/filesystem.d.ts +3 -3
  205. package/dist/store/filesystem.js +7 -7
  206. package/dist/store/registry.d.ts +48 -0
  207. package/dist/store/registry.js +99 -15
  208. package/dist/transformers/plugins/csf-render.js +4 -2
  209. package/dist/transformers/plugins/ssr-render.js +52 -18
  210. package/dist/transformers/preview/component/api.d.ts +8 -0
  211. package/dist/transformers/preview/component/api.js +11 -15
  212. package/dist/transformers/preview/component/artifacts.d.ts +1 -1
  213. package/dist/transformers/preview/component/artifacts.js +11 -7
  214. package/dist/transformers/preview/component/builder.js +15 -18
  215. package/dist/transformers/preview/component/css.d.ts +3 -0
  216. package/dist/transformers/preview/component/css.js +93 -58
  217. package/dist/transformers/preview/component/html.js +18 -16
  218. package/dist/transformers/preview/component/javascript.js +2 -4
  219. package/dist/transformers/preview/pattern/builder.js +31 -18
  220. package/dist/transformers/preview/pattern/html.js +3 -1
  221. package/dist/transformers/preview/types.d.ts +30 -3
  222. package/dist/transformers/utils/build.d.ts +19 -0
  223. package/dist/transformers/utils/build.js +83 -1
  224. package/dist/transformers/utils/module.js +2 -1
  225. package/dist/types/config.d.ts +151 -40
  226. package/dist/utils/markdown.d.ts +10 -0
  227. package/dist/utils/markdown.js +17 -0
  228. package/dist/utils/menu-shell.js +14 -7
  229. package/dist/utils/pages.d.ts +22 -0
  230. package/dist/utils/pages.js +73 -0
  231. package/dist/utils/path.d.ts +5 -0
  232. package/dist/utils/path.js +9 -0
  233. package/docs/api.md +35 -3
  234. package/docs/cli.md +6 -2
  235. package/drizzle/0002_add_pages.sql +5 -3
  236. package/drizzle/0005_add_registry_auth.sql +95 -0
  237. package/drizzle/0006_add_component_source_format.sql +3 -0
  238. package/drizzle/0007_add_ai_keys.sql +11 -0
  239. package/drizzle/meta/0002_snapshot.json +17 -8
  240. package/drizzle/meta/0003_snapshot.json +17 -8
  241. package/drizzle/meta/0004_snapshot.json +17 -8
  242. package/drizzle/meta/0005_snapshot.json +1935 -0
  243. package/drizzle/meta/0006_snapshot.json +1941 -0
  244. package/drizzle/meta/0007_snapshot.json +2013 -0
  245. package/drizzle/meta/_journal.json +21 -0
  246. package/drizzle.config.ts +1 -1
  247. package/package.json +44 -5
  248. package/src/ai/README.md +42 -0
  249. package/src/ai/connections.ts +172 -0
  250. package/src/ai/define.ts +15 -0
  251. package/src/ai/types.ts +31 -0
  252. package/src/app/components/AiAssistant/AiAssistantPanel.tsx +144 -0
  253. package/src/app/components/AiAssistant/AiAssistantProvider.tsx +215 -0
  254. package/src/app/components/AiAssistant/AiConversation.tsx +674 -0
  255. package/src/app/components/AiAssistant/AiToolResult.tsx +99 -0
  256. package/src/app/components/AiAssistant/dockState.ts +86 -0
  257. package/src/app/components/AiAssistant/toolLabel.ts +22 -0
  258. package/src/app/components/AiAssistant/useAiChat.ts +147 -0
  259. package/src/app/components/AiAssistant/useAiConnections.ts +86 -0
  260. package/src/app/components/AiAssistant/usePageContext.ts +59 -0
  261. package/src/app/components/Auth/AccountLayout.tsx +124 -0
  262. package/src/app/components/Auth/AuthControls.tsx +99 -0
  263. package/src/app/components/Auth/AuthShell.tsx +73 -0
  264. package/src/app/components/Auth/RegistryInstallGate.tsx +23 -0
  265. package/src/app/components/Auth/RegistrySessionProvider.tsx +15 -0
  266. package/src/app/components/Auth/api.ts +20 -0
  267. package/src/app/components/Component/PageSliceResolver.tsx +1 -0
  268. package/src/app/components/Component/Preview.tsx +406 -226
  269. package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +4 -0
  270. package/src/app/components/CopyToClipboard.tsx +27 -26
  271. package/src/app/components/Foundations/DisplayLogo.tsx +3 -3
  272. package/src/app/components/Layout/Header.tsx +10 -14
  273. package/src/app/components/Layout/RuntimeModeBadge.tsx +18 -0
  274. package/src/app/components/Markdown/CodeHighlight.tsx +73 -70
  275. package/src/app/components/Markdown/MarkdownComponents.tsx +30 -32
  276. package/src/app/components/McpIntegration/McpConfigDialog.tsx +349 -0
  277. package/src/app/components/McpIntegration/McpIcon.tsx +10 -0
  278. package/src/app/components/Navigation/MobileNav.tsx +7 -1
  279. package/src/app/components/Navigation/SideNav.tsx +1 -15
  280. package/src/app/components/context/PreviewContext.tsx +5 -5
  281. package/src/app/components/ui/attachment.tsx +50 -0
  282. package/src/app/components/ui/bubble.tsx +57 -0
  283. package/src/app/components/ui/button.tsx +3 -1
  284. package/src/app/components/ui/dialog.tsx +73 -0
  285. package/src/app/components/ui/marker.tsx +56 -0
  286. package/src/app/components/ui/sidebar.tsx +3 -1
  287. package/src/app/components/util/index.ts +14 -38
  288. package/src/app/css/index.css +64 -0
  289. package/src/app/lib/ai/auth.ts +33 -0
  290. package/src/app/lib/ai/context.ts +96 -0
  291. package/src/app/lib/ai/model.ts +85 -0
  292. package/src/app/lib/ai/prompt.ts +36 -0
  293. package/src/app/lib/ai/resolve.ts +97 -0
  294. package/src/app/lib/ai/tools.ts +120 -0
  295. package/src/app/lib/api/methods.ts +13 -0
  296. package/src/app/lib/auth/api.ts +105 -0
  297. package/src/app/lib/auth/config.ts +175 -0
  298. package/src/app/lib/auth/email.ts +34 -0
  299. package/src/app/lib/docs-api/backend.ts +16 -1
  300. package/src/app/lib/docs-api/errors.ts +8 -7
  301. package/src/app/lib/docs-api/handler.ts +2 -3
  302. package/src/app/lib/docs-api/index.ts +2 -0
  303. package/src/app/lib/docs-api/page-discovery.ts +121 -0
  304. package/src/app/lib/docs-api/page-rendering.ts +37 -0
  305. package/src/app/lib/docs-api/page-search.ts +191 -0
  306. package/src/app/lib/docs-api/records.ts +31 -24
  307. package/src/app/lib/docs-api/registry.ts +92 -11
  308. package/src/app/lib/docs-api/runtime-config.ts +42 -35
  309. package/src/app/lib/docs-api/search.ts +120 -0
  310. package/src/app/lib/mcp/auth.ts +64 -0
  311. package/src/app/lib/mcp/index.ts +2 -0
  312. package/src/app/lib/mcp/server.ts +357 -0
  313. package/src/app/lib/mcp/shape.ts +628 -0
  314. package/src/app/lib/registry-api/allowlist.ts +7 -5
  315. package/src/app/lib/registry-api/asset-transfer.ts +58 -31
  316. package/src/app/lib/registry-api/auth.ts +108 -45
  317. package/src/app/lib/registry-api/entity-routes.ts +17 -7
  318. package/src/app/lib/registry-api/errors.ts +12 -6
  319. package/src/app/lib/registry-api/handler.ts +22 -19
  320. package/src/app/lib/registry-api/meta.ts +4 -0
  321. package/src/app/lib/registry-api/revalidate.ts +31 -12
  322. package/src/app/lib/registry-api/store.ts +89 -26
  323. package/src/app/lib/registry-api/sync-compat.ts +166 -0
  324. package/src/app/lib/registry-api/token-transfer.ts +45 -13
  325. package/src/app/lib/registry-api/transfer-batch.ts +116 -0
  326. package/src/app/lib/registry-api/transfer.ts +99 -41
  327. package/src/app/lib/registry-api/validation.ts +33 -1
  328. package/src/app/lib/registry-connection.ts +2 -2
  329. package/src/app/lib/utils.ts +13 -0
  330. package/src/app/next.config.mjs +62 -16
  331. package/src/app/pages/[...slug]/index.tsx +52 -51
  332. package/src/app/pages/_app.tsx +14 -4
  333. package/src/app/pages/_document.tsx +6 -0
  334. package/src/app/pages/account/ai.tsx +162 -0
  335. package/src/app/pages/account/index.tsx +148 -0
  336. package/src/app/pages/account/tokens.tsx +232 -0
  337. package/src/app/pages/account/users.tsx +310 -0
  338. package/src/app/pages/api/account/ai/keys/[connectionId].ts +60 -0
  339. package/src/app/pages/api/account/ai/keys/index.ts +20 -0
  340. package/src/app/pages/api/account/index.ts +26 -0
  341. package/src/app/pages/api/account/tokens/[id].ts +17 -0
  342. package/src/app/pages/api/account/tokens/index.ts +35 -0
  343. package/src/app/pages/api/admin/users/[id]/[action].ts +89 -0
  344. package/src/app/pages/api/admin/users/index.ts +53 -0
  345. package/src/app/pages/api/ai/chat.ts +115 -0
  346. package/src/app/pages/api/ai/connections.ts +23 -0
  347. package/src/app/pages/api/auth/[...nextauth].ts +4 -0
  348. package/src/app/pages/api/auth/request-reset.ts +53 -0
  349. package/src/app/pages/api/auth/reset-password.ts +29 -0
  350. package/src/app/pages/api/docs/search/pages.json.ts +25 -0
  351. package/src/app/pages/api/install.ts +102 -0
  352. package/src/app/pages/api/mcp.ts +56 -0
  353. package/src/app/pages/api/oauth/device/approve.ts +23 -0
  354. package/src/app/pages/api/oauth/device/index.ts +41 -0
  355. package/src/app/pages/api/oauth/revoke.ts +31 -0
  356. package/src/app/pages/api/oauth/token.ts +31 -0
  357. package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +1 -1
  358. package/src/app/pages/api/registry/components/[id].ts +2 -2
  359. package/src/app/pages/api/registry/components/index.ts +1 -1
  360. package/src/app/pages/api/registry/health.ts +4 -3
  361. package/src/app/pages/api/registry/pages/[...id].ts +20 -0
  362. package/src/app/pages/api/registry/pages/index.ts +19 -0
  363. package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +1 -1
  364. package/src/app/pages/api/registry/patterns/[id].ts +2 -2
  365. package/src/app/pages/api/registry/patterns/index.ts +1 -1
  366. package/src/app/pages/api/registry/transfer/[kind]/[id].ts +3 -3
  367. package/src/app/pages/api/registry/transfer/[kind]/index.ts +21 -0
  368. package/src/app/pages/api/registry/transfer/assets/[collection].ts +2 -1
  369. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +2 -1
  370. package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +2 -2
  371. package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +2 -2
  372. package/src/app/pages/api/sync/upload.ts +21 -0
  373. package/src/app/pages/cli/device.tsx +143 -0
  374. package/src/app/pages/foundations/icons/index.tsx +2 -2
  375. package/src/app/pages/foundations/icons/{[name] → library/[name]}/index.tsx +6 -6
  376. package/src/app/pages/index/index.tsx +14 -0
  377. package/src/app/pages/index.tsx +31 -5
  378. package/src/app/pages/install.tsx +336 -0
  379. package/src/app/pages/login.tsx +117 -0
  380. package/src/app/pages/reset-password.tsx +159 -0
  381. package/src/app/types/next-auth.d.ts +27 -0
  382. package/src/app-builder/build.ts +225 -86
  383. package/src/app-builder/client-config.ts +18 -14
  384. package/src/app-builder/config-diff/strategies/pattern.ts +1 -1
  385. package/src/app-builder/nav-shell.ts +8 -13
  386. package/src/app-builder/paths.ts +7 -9
  387. package/src/app-builder/static-export.ts +6 -6
  388. package/src/app-builder/vercel-output.ts +6 -6
  389. package/src/app-builder/watchers/component.ts +3 -3
  390. package/src/app-builder/watchers/utils.ts +2 -3
  391. package/src/app-builder/watchers.ts +63 -57
  392. package/src/artifacts/url.ts +7 -7
  393. package/src/cache/build-cache.ts +16 -15
  394. package/src/catalog/README.md +95 -0
  395. package/src/catalog/define.ts +79 -0
  396. package/src/catalog/implementation.ts +145 -0
  397. package/src/catalog/index.ts +31 -0
  398. package/src/catalog/normalize.ts +145 -0
  399. package/src/catalog/previews.ts +114 -0
  400. package/src/catalog/renderers.ts +94 -0
  401. package/src/catalog/types.ts +82 -0
  402. package/src/cli/README.md +1 -1
  403. package/src/cli/auth/browser.ts +29 -0
  404. package/src/cli/auth/device.ts +186 -0
  405. package/src/cli/auth/store.ts +194 -0
  406. package/src/cli/create.ts +58 -3
  407. package/src/cli/make.ts +22 -74
  408. package/src/cli/scaffold.ts +39 -222
  409. package/src/cli.ts +31 -7
  410. package/src/commands/README.md +1 -1
  411. package/src/commands/build/app.ts +2 -3
  412. package/src/commands/build/components.ts +2 -3
  413. package/src/commands/build/index.ts +2 -3
  414. package/src/commands/checkout/index.ts +29 -50
  415. package/src/commands/compat/deprecations.ts +79 -0
  416. package/src/commands/compat/push-args.ts +106 -0
  417. package/src/commands/db/migrate.ts +2 -3
  418. package/src/commands/dev/index.ts +2 -3
  419. package/src/commands/eject/config.ts +2 -3
  420. package/src/commands/eject/pages.ts +2 -3
  421. package/src/commands/eject/theme.ts +2 -3
  422. package/src/commands/fetch/index.ts +2 -3
  423. package/src/commands/index.ts +8 -2
  424. package/src/commands/login/index.ts +61 -0
  425. package/src/commands/logout/index.ts +104 -0
  426. package/src/commands/make/component.ts +2 -3
  427. package/src/commands/make/page.ts +4 -5
  428. package/src/commands/publish/index.ts +32 -49
  429. package/src/commands/pull/index.ts +32 -0
  430. package/src/commands/push/index.ts +115 -0
  431. package/src/commands/scaffold/index.ts +2 -3
  432. package/src/commands/start/index.ts +2 -3
  433. package/src/commands/types.ts +13 -1
  434. package/src/commands/utils.ts +107 -2
  435. package/src/commands/validate/components.ts +2 -3
  436. package/src/config/README.md +63 -2
  437. package/src/config/catalog-include.ts +227 -0
  438. package/src/config/defaults.ts +26 -13
  439. package/src/config/env.ts +41 -0
  440. package/src/config/errors.ts +7 -0
  441. package/src/config/from-env.ts +26 -0
  442. package/src/config/helpers.ts +4 -2
  443. package/src/config/index.ts +10 -2
  444. package/src/config/loader.ts +208 -43
  445. package/src/config/normalizers/page.ts +4 -4
  446. package/src/config/resolve-env.ts +107 -0
  447. package/src/config/runtime.ts +212 -226
  448. package/src/config/validator.ts +4 -6
  449. package/src/handlebars.ts +24 -0
  450. package/src/index.d.ts +2 -2
  451. package/src/index.ts +325 -72
  452. package/src/nav/resolver.ts +3 -1
  453. package/src/nav/workspace.ts +2 -2
  454. package/src/pattern.ts +14 -0
  455. package/src/pipeline/index.ts +1 -1
  456. package/src/react.ts +52 -0
  457. package/src/registry/asset-storage/adapters/vercel-blob.ts +1 -1
  458. package/src/registry/asset-storage/define.ts +1 -1
  459. package/src/registry/asset-storage/resolve.ts +7 -4
  460. package/src/registry/auth/access-tokens.ts +222 -0
  461. package/src/registry/auth/action-tokens.ts +225 -0
  462. package/src/registry/auth/ai-keys.ts +62 -0
  463. package/src/registry/auth/crypto.ts +75 -0
  464. package/src/registry/auth/database.ts +15 -0
  465. package/src/registry/auth/device-authorizations.ts +223 -0
  466. package/src/registry/auth/gravatar.ts +6 -0
  467. package/src/registry/auth/index.ts +105 -0
  468. package/src/registry/auth/installation.ts +117 -0
  469. package/src/registry/auth/password.ts +46 -0
  470. package/src/registry/auth/rate-limits.ts +84 -0
  471. package/src/registry/auth/sync-secret.ts +67 -0
  472. package/src/registry/auth/types.ts +57 -0
  473. package/src/registry/auth/user-record.ts +25 -0
  474. package/src/registry/auth/users.ts +145 -0
  475. package/src/registry/auth/validation.ts +42 -0
  476. package/src/registry/checkout/assets.ts +28 -9
  477. package/src/registry/checkout/index.ts +582 -130
  478. package/src/registry/checkout/tokens.ts +16 -8
  479. package/src/registry/client.ts +7 -1
  480. package/src/registry/connection.ts +91 -19
  481. package/src/registry/content-kinds.ts +39 -0
  482. package/src/registry/db/client.ts +34 -14
  483. package/src/registry/db/driver.ts +66 -0
  484. package/src/registry/db/index.ts +16 -5
  485. package/src/registry/db/migrate.ts +5 -5
  486. package/src/registry/db/schema.ts +225 -25
  487. package/src/registry/publish/assets.ts +47 -37
  488. package/src/registry/publish/errors.ts +3 -3
  489. package/src/registry/publish/index.ts +286 -27
  490. package/src/registry/publish/package.ts +14 -1
  491. package/src/registry/publish/tokens.ts +35 -26
  492. package/src/registry/selection.ts +63 -0
  493. package/src/registry/transfer.ts +14 -0
  494. package/src/store/filesystem.ts +6 -6
  495. package/src/store/registry.ts +131 -15
  496. package/src/transformers/plugins/csf-render.ts +4 -2
  497. package/src/transformers/plugins/ssr-render.ts +66 -19
  498. package/src/transformers/preview/component/api.ts +13 -1
  499. package/src/transformers/preview/component/artifacts.ts +8 -4
  500. package/src/transformers/preview/component/builder.ts +17 -4
  501. package/src/transformers/preview/component/css.ts +99 -60
  502. package/src/transformers/preview/component/html.ts +30 -19
  503. package/src/transformers/preview/component/javascript.ts +1 -3
  504. package/src/transformers/preview/pattern/builder.ts +33 -20
  505. package/src/transformers/preview/pattern/html.ts +3 -1
  506. package/src/transformers/preview/types.ts +31 -3
  507. package/src/transformers/utils/build.ts +80 -0
  508. package/src/transformers/utils/module.ts +4 -2
  509. package/src/types/config.ts +163 -40
  510. package/src/utils/README.md +1 -0
  511. package/src/utils/markdown.ts +11 -0
  512. package/src/utils/menu-shell.ts +14 -7
  513. package/src/utils/pages.ts +52 -0
  514. package/src/utils/path.ts +15 -6
  515. package/templates/default/README.md.tpl +68 -1
  516. package/templates/default/components/button/button.react.handoff.js.tpl +19 -23
  517. package/templates/default/components/button/button.react.handoff.ts.tpl +22 -22
  518. package/templates/default/env.tpl +10 -0
  519. package/templates/default/gitignore.tpl +26 -3
  520. package/templates/default/handoff.config.js.tpl +21 -166
  521. package/templates/default/handoff.config.ts.tpl +21 -150
  522. package/templates/default/handoff.config.with-examples.js.tpl +22 -164
  523. package/templates/default/handoff.config.with-examples.ts.tpl +22 -148
  524. package/templates/default/package.json.tpl +6 -1
  525. package/templates/default/vercel.json.tpl +4 -0
  526. package/config/docs/foundations/logos.md +0 -7
  527. package/config/templates/component/template.json +0 -49
  528. package/config/templates/template.html +0 -7
  529. package/dist/commands/make/template.d.ts +0 -8
  530. package/dist/commands/make/template.js +0 -47
  531. package/dist/config/normalizers/declaration.d.ts +0 -11
  532. package/dist/config/normalizers/declaration.js +0 -122
  533. package/dist/config/normalizers/pattern.d.ts +0 -8
  534. package/dist/config/normalizers/pattern.js +0 -41
  535. package/dist/declarations/helpers.d.ts +0 -6
  536. package/dist/declarations/helpers.js +0 -19
  537. package/dist/declarations/index.d.ts +0 -2
  538. package/dist/declarations/index.js +0 -9
  539. package/dist/declarations/types.d.ts +0 -60
  540. package/dist/registry/db/adapter.d.ts +0 -43
  541. package/dist/registry/db/adapter.js +0 -59
  542. package/src/app/lib/api/redact.ts +0 -46
  543. package/src/commands/make/template.ts +0 -47
  544. package/src/config/normalizers/declaration.ts +0 -179
  545. package/src/config/normalizers/pattern.ts +0 -59
  546. package/src/declarations/helpers.ts +0 -36
  547. package/src/declarations/index.ts +0 -19
  548. package/src/declarations/types.ts +0 -70
  549. package/src/registry/db/adapter.ts +0 -74
  550. /package/dist/{declarations → catalog}/types.js +0 -0
package/README.md CHANGED
@@ -7,434 +7,862 @@
7
7
  <img alt="" src="https://img.shields.io/npm/l/handoff-app?style=for-the-badge&labelColor=000000">
8
8
  </a>
9
9
 
10
- Handoff is a design system runtime. It turns the source of truth for your design
11
- system, the tokens, components, and patterns, into dev-ready artifacts and living
12
- documentation, and serves both through one consistent runtime whether you are
13
- authoring locally or running a shared, deployed catalog.
14
-
15
- ## Table of contents
16
-
17
- * [What is Handoff?](#what-is-handoff)
18
- * [How it works](#how-it-works)
19
- * [Requirements](#requirements)
20
- * [Quick start](#quick-start)
21
- * [Project layout](#project-layout)
22
- * [Authoring components and patterns](#authoring-components-and-patterns)
23
- * [Fetching design tokens](#fetching-design-tokens)
24
- * [Running locally](#running-locally)
25
- * [Building artifacts](#building-artifacts)
26
- * [Static site (default)](#static-site-default)
27
- * [Registry app](#registry-app)
28
- * [Publishing and checkout](#publishing-and-checkout)
29
- * [Configuration](#configuration)
30
- * [The runtime block](#the-runtime-block)
31
- * [Environment variables](#environment-variables)
32
- * [Hooks](#hooks)
33
- * [CLI reference](#cli-reference)
34
- * [Further reading](#further-reading)
35
- * [Maintainers](#maintainers)
36
- * [Contributing](#contributing)
37
- * [License](#license)
38
-
39
- ## What is Handoff?
40
-
41
- Handoff is an open source design system runtime that closes the gap between
42
- design and development. By automating the delivery of your design system, it
43
- helps eliminate the bottlenecks between the two.
44
-
45
- It brings a few pieces together:
46
-
47
- * **Token extraction.** Pull standardized foundations from your design source and
48
- store them as JSON. Figma is supported out of the box through the Figma REST
49
- API.
50
- * **Transformation pipeline.** Produce SCSS, CSS, Style Dictionary, and preview
51
- snippets from that data.
52
- * **Documentation runtime.** Render live, working previews of your components,
53
- tokens, and styles, served through a stable docs read API.
54
- * **Delivery tooling.** Build and ship the deliverables, either as a static site
55
- or as a deployed, database-backed catalog.
56
-
57
- Out of the box Handoff ships SCSS and CSS maps for [Bootstrap 5](https://getbootstrap.com/).
58
- If you use another framework or custom CSS, you can write your own map files to
59
- connect the generated tokens to your project.
60
-
61
- You install `handoff-app` as a project-local dependency and run its commands in
62
- your project's context, so every project pins its own toolchain version.
63
-
64
- ## How it works
65
-
66
- Handoff has two runtime modes. Mode is resolved solely from `runtime.mode` in
67
- your config and defaults to `workspace`. It is never inferred from environment
68
- variables or token presence, so setting `DATABASE_URL` can never silently flip
69
- your app into another mode. The app chrome shows a `Workspace` or `Registry`
70
- badge so the active mode is always clear.
71
-
72
- * **Workspace mode (default).** Your local filesystem is the source of truth.
73
- You author components, patterns, and tokens locally, preview them with a dev
74
- server, and build a static documentation snapshot. Local authoring stays fast
75
- and self-contained, with no database or registry required.
76
- * **Registry mode.** A deployed, dynamic Next.js app backed by PostgreSQL serves
77
- a shared catalog. It only ever serves what a workspace published to it. The
78
- registry never compiles or materializes workspace source.
79
-
80
- A **connected workspace** is workspace mode plus a registry connection. It still
81
- renders only your local files and still reads as `Workspace`, but it gains two
82
- commands: `publish` to push a locally built component or pattern up to a
83
- registry, and `checkout` to pull one back down into your workspace.
84
-
85
- Under the hood, documentation in every mode is consumed through one stable docs
86
- read API and one canonical artifact URL scheme (`/api/docs/artifacts/{path}`),
87
- so the same UI serves workspace, static, and registry backings without knowing
88
- which store is behind it.
10
+ Design tokens, components, and patterns are turned by Handoff into working
11
+ documentation and distributable artifacts. A local workspace, a static site,
12
+ or a shared PostgreSQL-backed registry can be used.
13
+
14
+ ## Runtime modes
15
+
16
+ - **Workspace** is the default. Local files are the source of truth and no
17
+ database is required.
18
+ - **Registry** is a deployed catalog backed by PostgreSQL.
19
+ - A **connected workspace** remains in workspace mode but can publish to and
20
+ check out from a registry.
21
+
22
+ Workspace mode is used by local projects for authoring. A separate
23
+ registry-mode application is created automatically by a registry build.
89
24
 
90
25
  ## Requirements
91
26
 
92
- * Node 18.17+
93
- * npm 8+
94
- * A paid Figma account, if you fetch tokens from a Figma library
95
- * PostgreSQL (or a Postgres-compatible service such as Neon) for registry mode
27
+ - Node.js 18.18 or newer; Node.js 20 LTS or newer is recommended
28
+ - npm 8 or newer
29
+ - A paid Figma account when fetching a Figma library
30
+ - PostgreSQL when running a registry
96
31
 
97
32
  ## Quick start
98
33
 
99
- Scaffold a new project with the interactive wizard:
34
+ A project is created with the interactive wizard:
100
35
 
101
36
  ```bash
102
37
  npx handoff-app init
38
+ cd my-handoff-project
103
39
  ```
104
40
 
105
- The wizard asks for a project name, whether to include sample components, your
106
- preferred language (TypeScript or JavaScript), and optionally your Figma project
107
- ID and access token. It then creates the project directory, writes the config
108
- and starter files, and installs dependencies.
41
+ The project name, sample content, JavaScript or TypeScript, optional Vercel
42
+ setup, and optional Figma credentials are requested by the wizard. Starter
43
+ files are created and `handoff-app` is installed locally. Generated npm scripts
44
+ are used for project operations so the pinned project version is shared by
45
+ local development and CI:
109
46
 
110
47
  ```bash
111
- cd my-handoff-project
112
- npm run fetch # pull design tokens from Figma (optional if no Figma creds yet)
113
- npm run start # author locally with live preview at http://localhost:3000
48
+ npm run fetch # optional: fetch Figma foundations
49
+ npm run start # local server with Handoff file watchers
50
+ npm run dev # local development server without Handoff file watchers
51
+ npm run build # static build by default
52
+ npm run db:migrate # registry database migrations
53
+ npm run validate
54
+ ```
55
+
56
+ ## Project layout
57
+
58
+ Every command runs against a working directory: the directory holding
59
+ `handoff.config.ts`. Generated output is written there.
60
+
61
+ ```text
62
+ <working directory>/
63
+ ├─ handoff.config.ts
64
+ ├─ .env
65
+ ├─ components/
66
+ ├─ patterns/
67
+ ├─ pages/
68
+ ├─ exported/ # fetched tokens and assets; commit this directory
69
+ ├─ public/api/ # generated docs API; gitignored
70
+ ├─ out/ # build output; gitignored
71
+ ├─ .vercel/ # `--package vercel` output; gitignored
72
+ └─ .handoff/ # saved CLI login; gitignored
73
+ ```
74
+
75
+ In a repository of its own, which is what `init` scaffolds, the working
76
+ directory is the repository root.
77
+
78
+ ### Adding Handoff to an existing application
79
+
80
+ Declarations sit beside the components they document, so the catalog points
81
+ into the application source:
82
+
83
+ ```ts
84
+ catalog: { include: ['src/components', 'src/blocks'] },
114
85
  ```
115
86
 
116
- The generated `package.json` wires up the common scripts:
87
+ **`handoff-app` must be a dependency of the package holding those
88
+ `*.handoff.ts` files.** They import `handoff-app/react`, which resolves from the
89
+ directory of the declaration, not from the directory of the config. Under npm a
90
+ missing dependency is usually hidden by hoisting. Under pnpm it is not: the
91
+ affected items are skipped with `Could not resolve "handoff-app/react"` and the
92
+ build still exits 0.
93
+
94
+ Two `tsconfig.json` files do different jobs:
95
+
96
+ - The config nearest the declarations type checks them. For files in `src/`
97
+ that is the one at the project root. An editor reads the nearest config only,
98
+ so a config in another directory that includes `src/` does not count, and
99
+ neither the editor nor the application build reports errors in a declaration.
100
+ - The config at the working directory generates the property tables. Without
101
+ it, Handoff warns `TypeScript config not found` and every table is empty.
102
+
103
+ Both files are needed when the working directory is not the project root.
104
+
105
+ Three placements work, all keeping one dependency tree so the catalog renders
106
+ the real components against the same React instance. A separate install with its
107
+ own `node_modules` outside that tree is not supported.
108
+
109
+ **Handoff in a subdirectory — recommended.** Nothing in the application changes.
110
+ `handoff-app` is a dependency of the application package, and
111
+ `HANDOFF_WORKING_PATH` keeps the generated directories in `handoff/`:
112
+
113
+ ```json
114
+ { "scripts": { "handoff:dev": "HANDOFF_WORKING_PATH=handoff handoff-app start" } }
115
+ ```
116
+
117
+ An inline variable does not work in an npm script on Windows, where `cross-env`
118
+ is used instead.
119
+
120
+ **Handoff in the project root.** The working directory is the project root, so
121
+ one `tsconfig.json` does both jobs. Handoff writes to `public/api`, so a bundler
122
+ already serving static files from `public/` must be pointed elsewhere:
123
+
124
+ ```ts
125
+ // vite.config.ts
126
+ export default defineConfig({
127
+ publicDir: 'static',
128
+ });
129
+ ```
130
+
131
+ Without that change the generated documentation API is copied into the
132
+ production build of the application. A Next.js application cannot use this
133
+ placement: `sitesOutputDirectory` moves `out/` away from its export directory,
134
+ but its `public` directory is a fixed convention with no setting to move it.
135
+
136
+ **Handoff as a workspace package.** For a repository that is already a
137
+ workspace. `handoff/` gets its own `package.json` and scripts, generated output
138
+ lands in `handoff/`, and the root forwards to it:
117
139
 
118
140
  ```json
119
141
  {
120
- "scripts": {
121
- "start": "handoff-app start",
122
- "dev": "handoff-app dev",
123
- "build": "handoff-app build",
124
- "fetch": "handoff-app fetch"
125
- }
142
+ "workspaces": ["handoff"],
143
+ "scripts": { "handoff:dev": "npm run dev --workspace handoff" }
126
144
  }
127
145
  ```
128
146
 
129
- Any other command can be run with `npx handoff-app <command>` or added as a
130
- script of your own.
147
+ The dependency rule is easiest to get wrong here: the declarations in `src/`
148
+ belong to the application package, not to the workspace, so both packages
149
+ declare `handoff-app`.
131
150
 
132
- ## Project layout
151
+ ### pnpm
133
152
 
134
- A typical workspace project looks like this:
153
+ `build` and `build --package vercel` work with the default pnpm layout.
154
+ `build --target registry` cannot follow pnpm symlinks when it traces its runtime
155
+ dependencies into a standalone bundle, so it needs a flat layout:
135
156
 
157
+ ```yaml
158
+ # pnpm-workspace.yaml
159
+ nodeLinker: hoisted
136
160
  ```
137
- my-handoff-project/
138
- ├─ handoff.config.ts # project + runtime configuration
139
- ├─ .env # Figma credentials and other secrets
140
- ├─ components/
141
- │ └─ button/
142
- │ ├─ Button.tsx # component implementation
143
- │ └─ button.handoff.ts # component declaration
144
- ├─ patterns/ # multi-component compositions
145
- ├─ sass/ # optional global SCSS entry
146
- ├─ js/ # optional global JS entry
147
- ├─ exported/ # generated tokens and artifacts (gitignored)
148
- └─ out/ # build output (gitignored)
161
+
162
+ ## Catalog items
163
+
164
+ Every documented UI entry is a catalog item. Each item declares either an implementation or a composition of other items.
165
+ An implementation names its renderer. `defineCatalogItem` comes from the module for that renderer.
166
+ The declaration supplies stable identity, documentation metadata, source entries, and previews.
167
+
168
+ ### React
169
+
170
+ Handoff finds the implementation file from its import, so you do not need to repeat the path, and
171
+ builds the item's CSS from the stylesheets that import chain pulls in, descendants included.
172
+ Previews are named exports. `Preview<typeof item>` gives them the implementation's argument type.
173
+
174
+ ```tsx
175
+ // components/example/Component.tsx
176
+ export type ComponentProps = {
177
+ label: string;
178
+ onSelect?: () => void;
179
+ };
180
+
181
+ export default function Component({ label, onSelect }: ComponentProps) {
182
+ return <div onClick={onSelect}>{label}</div>;
183
+ }
184
+ ```
185
+
186
+ ```tsx
187
+ // components/example/example.handoff.ts
188
+ import { defineCatalogItem, type Preview } from 'handoff-app/react';
189
+ import Component from './Component';
190
+
191
+ const item = defineCatalogItem({
192
+ id: 'component-id',
193
+ name: 'Component name',
194
+ description: 'Usage guidance for the component.',
195
+ group: 'Group name',
196
+ implementation: Component,
197
+ });
198
+
199
+ export default item;
200
+
201
+ type ComponentPreview = Preview<typeof item>;
202
+
203
+ export const Default = {
204
+ args: { label: 'Example' },
205
+ } satisfies ComponentPreview;
206
+ ```
207
+
208
+ The export name is the preview name. Use `name` to set a different display title.
209
+
210
+ Arguments are serialized, so a preview that needs an icon, a callback, JSX children, or its own
211
+ state declares `render` instead. It renders as a component, so a capitalized name lets it use hooks, and the item keeps
212
+ documenting the implementation rather than a wrapper. Name a declaration that holds JSX
213
+ `example.handoff.tsx`.
214
+
215
+ ```tsx
216
+ export const Interactive = {
217
+ render: function Interactive() {
218
+ const [label, setLabel] = useState('Example');
219
+ return <Component label={label} onSelect={() => setLabel('Selected')} />;
220
+ },
221
+ } satisfies ComponentPreview;
222
+ ```
223
+
224
+ ### Handlebars
225
+
226
+ The implementation is the path to the template. Arguments are `Record<string, unknown>` by default;
227
+ an explicit argument type is supplied through `defineCatalogItem<BadgeArgs>`.
228
+
229
+ ```ts
230
+ // components/badge/badge.handoff.ts
231
+ import { defineCatalogItem, type Preview } from 'handoff-app/handlebars';
232
+
233
+ const item = defineCatalogItem({
234
+ id: 'badge',
235
+ name: 'Badge',
236
+ implementation: './Badge.hbs',
237
+ });
238
+
239
+ export default item;
240
+
241
+ type BadgePreview = Preview<typeof item>;
242
+
243
+ export const Primary = {
244
+ args: { variant: 'primary', children: 'New' },
245
+ } satisfies BadgePreview;
246
+ ```
247
+
248
+ ### CSF
249
+
250
+ CSF is a source format. A story file uses the renderer from the module that declares it. A React item references an existing CSF file with `fromCSF`.
251
+ The CSF file stays unchanged and defines its previews through named exports. Each story's `args` merge with and override `meta.args`.
252
+ Handoff preserves story names, `argTypes`, and `render` functions.
253
+ It finds the React component through `meta.component`, then documents and publishes it with the story file.
254
+
255
+ ```ts
256
+ // components/card/card.handoff.ts
257
+ import { defineCatalogItem, fromCSF } from 'handoff-app/react';
258
+
259
+ export default defineCatalogItem({
260
+ id: 'card',
261
+ name: 'Card',
262
+ implementation: fromCSF('./Card.stories.tsx'),
263
+ });
264
+ ```
265
+
266
+ `Meta` and `StoryObj` are exported from `handoff-app/react`, so a story file type-checks without a
267
+ Storybook dependency. A project that already has Storybook keeps using its own types.
268
+
269
+ ```tsx
270
+ // components/card/Card.stories.tsx
271
+ import { type Meta, type StoryObj } from 'handoff-app/react';
272
+ import Card from './Card';
273
+
274
+ const meta = { component: Card } satisfies Meta<typeof Card>;
275
+ export default meta;
276
+
277
+ export const Primary: StoryObj<typeof meta> = {
278
+ args: { title: 'Example' },
279
+ };
149
280
  ```
150
281
 
151
- ## Authoring components and patterns
282
+ A CSF preview is rendered on the server and is not hydrated. A direct React implementation is
283
+ hydrated in the browser.
152
284
 
153
- A component is a directory with an implementation file and a declaration. The
154
- declaration is discovered as `*.handoff.{ts,js,cjs,json}` (the legacy
155
- `{dirname}.{js,cjs,json}` form keeps working too). List the directories you want
156
- built under `entries.components` in your config.
285
+ ### Compositions
157
286
 
158
- Declarations are written with the typed helpers exported from `handoff-app`:
287
+ A composition references other items by stable id. A named preview can be selected and its arguments
288
+ can be overridden by each reference. A composition needs no renderer, so it is declared with
289
+ `defineCatalogItem` from `handoff-app/pattern`.
159
290
 
160
291
  ```ts
161
- import { defineReactComponent } from 'handoff-app';
162
- import Button from './Button';
163
-
164
- export default defineReactComponent(Button, {
165
- id: 'button',
166
- name: 'Button',
167
- description: 'Interactive button used for primary and secondary actions.',
168
- group: 'Atomic Elements',
169
- entries: {
170
- component: './Button.tsx',
292
+ // patterns/example/example.handoff.ts
293
+ import { defineCatalogItem } from 'handoff-app/pattern';
294
+
295
+ export default defineCatalogItem({
296
+ id: 'pattern-id',
297
+ name: 'Pattern name',
298
+ description: 'Purpose and usage of the composition.',
299
+ group: 'Group name',
300
+ composition: [
301
+ { ref: 'component-id', preview: 'Default' },
302
+ { ref: 'badge', args: { children: 'Send' } },
303
+ ],
304
+ });
305
+ ```
306
+
307
+ An item declares `implementation` or `composition`, never both. Terms such as atom, element, and
308
+ block stay optional classification metadata on `type` and `categories`.
309
+
310
+ The `id` addresses the item everywhere: its artifacts, its documentation URL, and its publish and
311
+ checkout commands. Components and patterns share one namespace, so each id must be unique across
312
+ both.
313
+
314
+ If two declarations claim one id, Handoff keeps the first and skips the second. The warning names
315
+ both files. `publish` refuses to run until each id is unique.
316
+
317
+
318
+ ### Registration
319
+
320
+ Catalog directories are registered in one place. A path is either an item directory or a collection
321
+ directory whose subdirectories are each treated as an item.
322
+
323
+ ```ts
324
+ // handoff.config.ts
325
+ import { defineConfig } from 'handoff-app';
326
+
327
+ export default defineConfig({
328
+ catalog: {
329
+ include: ['components', 'patterns'],
171
330
  },
172
- previews: {
173
- primary: {
174
- title: 'Primary',
175
- args: { type: 'primary', children: 'Click me!' },
331
+ runtime: {
332
+ workspace: {
333
+ declarationFormat: 'ts',
176
334
  },
177
335
  },
178
336
  });
179
337
  ```
180
338
 
181
- The available helpers are:
339
+ ### Upgrade from version 1.x.x to version 2.x.x
182
340
 
183
- * `defineReactComponent` for React components
184
- * `defineHandlebarsComponent` for Handlebars templates
185
- * `defineCsfComponent` for Component Story Format stories
186
- * `defineComponent` for a generic, renderer-agnostic declaration
187
- * `definePattern` for compositions that reference several components by `id`
341
+ See the [version 1.x.x to version 2.x.x migration guide](UPGRADE.md#version-1xx-to-version-2xx)
342
+ for breaking changes, migration steps, and upgrade verification.
188
343
 
189
- Every component and pattern has a stable `id` (explicit `id` field, otherwise the
190
- directory name). The display `name`/`title` is independent of the `id`, so you
191
- can rename freely without breaking references. Identity is matched by `id`
192
- during publish and checkout.
344
+ Custom documentation pages are Markdown files under `pages/`. Their relative
345
+ paths become their routes and registry IDs.
193
346
 
194
- ## Fetching design tokens
347
+ ## Figma foundations
195
348
 
196
- Handoff fetches tokens from Figma out of the box. Set your Figma credentials in
197
- `.env`:
349
+ Figma integration is optional. When Figma credentials are entered during
350
+ `init`, they are written to `.env` by the wizard and the fetch command can be
351
+ run directly.
352
+
353
+ When that step is skipped or the Figma source must be changed, these values can
354
+ be added or updated in `.env`:
198
355
 
199
356
  ```bash
200
- HANDOFF_FIGMA_PROJECT_ID=your-figma-file-id
201
- HANDOFF_DEV_ACCESS_TOKEN=your-figma-personal-access-token
357
+ HANDOFF_FIGMA_PROJECT_ID=figma-file-id
358
+ HANDOFF_DEV_ACCESS_TOKEN=figma-personal-access-token
202
359
  ```
203
360
 
204
- Then pull the latest foundations and component data:
361
+ The `file_content:read` and `library_content:read` scopes must be granted to the
362
+ personal access token. The configured library is fetched with:
205
363
 
206
364
  ```bash
207
365
  npm run fetch
208
366
  ```
209
367
 
210
- To get a personal access token in Figma, open the Figma menu, go to
211
- `Help and Account` then `Account Settings`, scroll to `Personal Access Token`,
212
- name a token, and copy it. Republish your Figma library after design changes and
213
- run `fetch` again to pick them up.
368
+ After design changes, the Figma library should be republished and the command
369
+ should be run again so the latest foundations are pulled.
370
+
371
+ Generated tokens, CSS, Sass, and available assets are written below
372
+ `exported/<projectId>`. The `exported/` directory should be committed so the
373
+ same inputs are used by local and CI builds. A successful fetch can be verified
374
+ by checking for generated token data such as `exported/<projectId>/tokens.json`;
375
+ an empty foundation route does not prove that the fetch succeeded.
214
376
 
215
- ## Running locally
377
+ ## Workspace
216
378
 
217
- Both commands run a workspace-first local server with live preview.
379
+ Components, patterns, pages, styles, and configuration are authored as local
380
+ workspace files. For day-to-day authoring, the documentation server and file
381
+ watchers are started with:
218
382
 
219
383
  ```bash
220
- handoff-app dev # development server with the live docs read API
221
- handoff-app start # dev server plus file watchers that rebuild on change
384
+ npm run start
222
385
  ```
223
386
 
224
- `start` is the day-to-day authoring loop: edit a component, save, and the
225
- preview updates. The site boots at http://localhost:3000 by default.
387
+ Changes are rebuilt and reflected in the documentation while the command is
388
+ running. When Handoff file watchers are not required, the development server
389
+ can be started with `npm run dev` instead.
390
+
391
+ The following should be visible at http://localhost:3000:
226
392
 
227
- ## Building artifacts
393
+ - the app reports `Workspace` as the active runtime mode;
394
+ - components appear in the component list;
395
+ - component detail pages render every configured preview;
396
+ - patterns render their referenced components; and
397
+ - foundation pages contain the fetched values.
228
398
 
229
- `handoff-app build` accepts a `--target` flag. The target, not `NODE_ENV`,
230
- decides what gets produced.
399
+ ## Build outputs
231
400
 
232
- ### Static site (default)
401
+ A static documentation site is built with:
233
402
 
234
403
  ```bash
235
- handoff-app build # static target (default)
236
- handoff-app build --target static # explicit, identical output
404
+ npm run build
237
405
  ```
238
406
 
239
- The static target builds your workspace components, patterns, and tokens,
240
- materializes the docs read model into route-shaped static files, and runs a
241
- static export to `out/<projectId>` (respecting `sitesOutputDirectory`). The
242
- output is plain static files: it renders list, detail, preview, and inspect
243
- pages and loads previews through canonical artifact URLs with no live server. It
244
- works on a managed host like Vercel and on a plain static file server with no
245
- host-specific rewrites.
407
+ Static output is written below `out/<projectId>` and can be served by any
408
+ static file server or CDN.
246
409
 
247
- The build fails clearly if a required artifact referenced by generated HTML
248
- cannot be materialized, and it fails if the runtime is registry-only (there is
249
- no local workspace to export).
250
-
251
- ### Registry app
410
+ A standalone registry application is built with:
252
411
 
253
412
  ```bash
254
- handoff-app build --target registry
413
+ npm run build -- --target registry
255
414
  ```
256
415
 
257
- The registry target packages a deployable, dynamic Next.js app to
258
- `<sitesOutputDirectory>/registry` (default `out/registry`) as a Next.js
259
- standalone bundle. It never builds or bundles workspace source and has no
260
- workspace-source runtime dependency. It serves the docs read API and the
261
- registry API from the database and published artifacts.
416
+ The generated application is automatically configured for registry mode by the
417
+ command. The database-backed Next.js bundle is written to `out/registry`, and
418
+ workspace source is not compiled or served. A Vercel Build Output bundle can be
419
+ produced for either target by adding `--package vercel`.
420
+
421
+ ## Registry setup
422
+
423
+ ### 1. Database migrations
262
424
 
263
- The bundle is self-hostable on a Node server, VPS, or container. Start it with
264
- `node server.js` from the output directory and supply the database connection at
265
- deploy time:
425
+ Migrations are run from the source workspace or CI checkout, where the project
426
+ dependency and configuration are available:
266
427
 
267
428
  ```bash
268
- DATABASE_URL="postgres://…" node out/registry/server.js
429
+ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/handoff?schema=public" \
430
+ npm run db:migrate
269
431
  ```
270
432
 
271
- A generated `README.md` next to the bundle documents the required env vars and
272
- Docker/Vercel specifics. Apply database migrations from the CLI as a controlled
273
- release step (e.g. in CI/CD) before deploying:
433
+ Migrations are a controlled release step and are separate from the build,
434
+ server startup, and browser installer.
435
+
436
+ ### 2. Registry runtime
437
+
438
+ The generated registry bundle uses these environment variables:
439
+
440
+ ```dotenv
441
+ PORT=4000
442
+ HOSTNAME=0.0.0.0
443
+ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/handoff?schema=public"
444
+ AUTH_SECRET="replace-with-at-least-32-random-characters"
445
+ AUTH_URL="http://localhost:4000"
446
+ ```
447
+
448
+ `DATABASE_URL`, `AUTH_SECRET`, and `AUTH_URL` are required. The standalone
449
+ server also reads `PORT` and `HOSTNAME`. The generated entrypoint is
450
+ `out/registry/server.js`; how it is started and hosted depends on the deployment
451
+ environment.
452
+
453
+ ### Vercel and Neon
454
+
455
+ Vercel packaging and the Neon PostgreSQL driver are supported independently.
456
+ Either integration can be used without the other.
457
+
458
+ #### Vercel packaging
459
+
460
+ A registry bundle that follows the Vercel Build Output API is produced with:
274
461
 
275
462
  ```bash
276
- handoff-app db:migrate
463
+ npm run build -- --target registry --package vercel
277
464
  ```
278
465
 
279
- `db:migrate` reads your project config and DB env vars, applies the
280
- package-owned migration set, and runs independently of `build`. The database
281
- adapter (`pg` or `neon`) is chosen with `runtime.registry.database.adapter`, and
282
- both adapters ship with the package.
466
+ The generated deployment output is written to `.vercel/output`. The registry
467
+ runtime variables listed above are supplied through the deployment environment.
283
468
 
284
- ## Publishing and checkout
469
+ #### Neon PostgreSQL
285
470
 
286
- Connect a workspace to a registry by adding a `registryConnection` block to your
287
- config and setting the access token env var:
471
+ Import `fromEnv` from `handoff-app`. Select the Neon connection driver in `handoff.config.ts`:
288
472
 
289
473
  ```ts
290
474
  runtime: {
291
- mode: 'workspace',
292
- registryConnection: {
293
- url: 'https://registry.example.com',
294
- accessTokenEnv: 'HANDOFF_REGISTRY_ACCESS_TOKEN',
475
+ registry: {
476
+ databaseUrl: fromEnv('DATABASE_URL'),
477
+ database: {
478
+ driver: 'neon',
479
+ },
480
+ },
481
+ },
482
+ ```
483
+
484
+ A Neon PostgreSQL connection string is supplied through `DATABASE_URL`. The
485
+ same explicit database migration step is required before deployment.
486
+
487
+ #### Optional Vercel Blob storage
488
+
489
+ Registry assets are stored in PostgreSQL by default. Vercel Blob can be selected
490
+ when external asset storage is required:
491
+
492
+ ```ts
493
+ runtime: {
494
+ registry: {
495
+ assetStorage: {
496
+ adapter: 'vercel-blob',
497
+ },
295
498
  },
296
499
  },
297
500
  ```
298
501
 
502
+ The associated `BLOB_READ_WRITE_TOKEN` is supplied through the deployment
503
+ environment.
504
+
505
+ ### 3. Installation
506
+
507
+ The installer is opened at http://localhost:4000/install, where the first
508
+ administrator is created. The deployment is verified by the installer, but
509
+ migrations are never run by it.
510
+
511
+ An uninstalled registry can be claimed by the first visitor. A new deployment
512
+ should not be left unattended before installation is complete.
513
+
514
+ ### 4. Workspace authorization
515
+
516
+ A workspace reaches a registry with a registry URL and an access token. There
517
+ are two ways to supply them. Both use the same kind of token, and both are
518
+ listed and revoked under Account → Access tokens in the registry.
519
+
520
+ #### Device login
521
+
522
+ CLI authorization is started from the source workspace with:
523
+
299
524
  ```bash
300
- # .env
301
- HANDOFF_REGISTRY_ACCESS_TOKEN=your-registry-token
525
+ npm run login -- --url http://localhost:4000
302
526
  ```
303
527
 
304
- Then push and pull entities by `id`:
528
+ Registry sign-in, entry of the displayed device code, and CLI approval are
529
+ completed in the browser. The issued credential is saved in
530
+ `.handoff/cli-auth.json` for that exact registry URL.
531
+
532
+ One credential is saved per profile, so a workspace can stay signed in to
533
+ several registries at the same time:
305
534
 
306
535
  ```bash
307
- handoff-app publish component button # build locally, upload that entity's package
308
- handoff-app checkout pattern hero # pull a registry entity into this workspace
536
+ npm run login -- --profile staging --url https://staging.example.com
537
+ npm run publish -- all --profile staging
309
538
  ```
310
539
 
311
- `publish` runs a fresh targeted build of the selected entity and uploads only
312
- that entity's package: normalized metadata, source files referenced by its
313
- `entries`, rendered docs artifacts (including required shared artifacts like
314
- `component/main.css` and `component/main.js`), and build metadata. It fails
315
- before upload if required artifacts are missing, stale, or incompatible, and it
316
- never touches unrelated entities' artifacts. Declaration files are never
317
- uploaded.
540
+ A login profile name is free. It needs no `handoff.config.<name>.*` file, and
541
+ `login` is the only command that accepts a new name. A profile without its own
542
+ login falls back to the default login, so one `npm run login` is still enough
543
+ for a workspace with one registry.
318
544
 
319
- `checkout` reads the registry record and source files, writes them in standard
320
- authoring form, and synthesizes a local declaration in your configured
321
- `runtime.workspace.declarationFormat` (defaulting to `js`). Declarations are a
322
- workspace-only concern, so they are always synthesized locally and never read
323
- from the registry. Overwriting existing local files requires `--force` or an
324
- interactive confirmation.
545
+ `npm run logout` revokes and removes the login of the selected profile. The
546
+ `--all` option removes every saved login.
325
547
 
326
- ## Configuration
548
+ #### Environment variables
327
549
 
328
- Project configuration lives in `handoff.config.{ts,js,cjs,json}` (resolved in
329
- that precedence order). TypeScript and ESM configs use `defineConfig` for typed
330
- authoring; plain CommonJS configs that never call `defineConfig` keep working
331
- unchanged.
550
+ A token is created in the registry under Account → Access tokens. Read and
551
+ write access is required to publish; read access is enough to check out. The
552
+ token and the registry URL are then set in `.env`:
332
553
 
333
- ```ts
334
- import { defineConfig } from 'handoff-app';
554
+ ```dotenv
555
+ HANDOFF_REGISTRY_URL=http://localhost:4000
556
+ HANDOFF_REGISTRY_ACCESS_TOKEN=hnd_...
557
+ ```
335
558
 
336
- export default defineConfig({
337
- app: {
338
- title: 'Acme Design System',
339
- client: 'Acme Corp',
340
- basePath: '',
341
- },
342
- entries: {
343
- scss: './sass/main.scss',
344
- js: './js/main.js',
345
- components: ['components/button', 'components/badge'],
346
- patterns: ['patterns'],
347
- },
348
- });
559
+ No configuration file entry is needed, because these two variables are the
560
+ defaults for `runtime.registryConnection`. A selected profile reads
561
+ `.env.<profile>` on top of `.env`, so one workspace can address a different
562
+ registry per profile. In CI, the job environment supplies the same two
563
+ variables instead of a file.
564
+
565
+ Environment values win over a saved device login, so a CI job stays
566
+ deterministic on a machine where a developer is signed in. A token is only used
567
+ for the registry URL it was issued for. If the environment names a different
568
+ registry than the login of the selected profile, publish and checkout report
569
+ which login was skipped and why.
570
+
571
+ ### 5. Content publishing
572
+
573
+ Every kind in the workspace is published in dependency order with:
574
+
575
+ ```bash
576
+ npm run publish -- all
349
577
  ```
350
578
 
351
- ### The runtime block
579
+ A single kind is published on its own:
352
580
 
353
- The `runtime` block selects the mode and carries mode-specific settings. It is
354
- optional and deep-merged into the defaults, so a partial block never wipes them.
581
+ ```bash
582
+ npm run publish -- catalog
583
+ npm run publish -- pages
584
+ npm run publish -- tokens
585
+ npm run publish -- assets
586
+ ```
355
587
 
356
- ```ts
357
- runtime: {
358
- // Sole determinant of runtime mode. Defaults to 'workspace'.
359
- mode: 'workspace', // 'workspace' | 'registry'
588
+ `catalog` covers every catalog item. An item is stored as a component or as a
589
+ pattern, and its declaration decides which. The command names items and never a
590
+ kind.
360
591
 
361
- workspace: {
362
- // Format for newly generated / checkout-synthesized declarations.
363
- declarationFormat: 'ts', // 'ts' | 'js' | 'cjs' | 'json'
364
- },
592
+ Publishing tokens or assets runs the Figma data pipeline before upload, so the
593
+ documented Figma credentials must be available.
594
+
595
+ One or more IDs can be appended to narrow a publish to those entities:
596
+
597
+ ```bash
598
+ npm run publish -- catalog item-id another-id
599
+ ```
600
+
601
+ `--dry-run` reports what would be uploaded and contacts no registry at all, so
602
+ it needs neither a registry URL nor a token. It still runs the build, which
603
+ refreshes generated output on disk; `--no-build` skips the build and publishes
604
+ the existing output, and the two combine to leave the workspace untouched:
605
+
606
+ ```bash
607
+ npm run publish -- all --dry-run
608
+ npm run publish -- catalog --no-build
609
+ ```
610
+
611
+ `checkout` takes the same `all`, multi-ID, and `--dry-run` forms. A dry-run
612
+ checkout reads from the registry, lists the files it would create or overwrite,
613
+ and writes nothing.
614
+
615
+ After the registry is reloaded, the published catalog items and foundations are
616
+ visible. Published database records are read by registry pages; the local
617
+ workspace is never read directly.
365
618
 
366
- // Connect a workspace to a remote registry (publish/checkout).
619
+ For CI, supply `HANDOFF_REGISTRY_URL` and `HANDOFF_REGISTRY_ACCESS_TOKEN`
620
+ through the job environment, as described under
621
+ [Workspace authorization](#4-workspace-authorization). A connection block in
622
+ `handoff.config.ts` is needed only to pin the URL in the repository, or to read
623
+ the values from differently named variables (`fromEnv` is imported from
624
+ `handoff-app`):
625
+
626
+ ```ts
627
+ runtime: {
367
628
  registryConnection: {
368
629
  url: 'https://registry.example.com',
369
- accessTokenEnv: 'HANDOFF_REGISTRY_ACCESS_TOKEN',
630
+ accessToken: fromEnv('HANDOFF_REGISTRY_ACCESS_TOKEN'),
370
631
  },
632
+ },
633
+ ```
371
634
 
372
- // Registry deployment settings (build --target registry, db:migrate).
373
- registry: {
374
- databaseUrlEnv: 'DATABASE_URL',
375
- apiTokenEnv: 'HANDOFF_REGISTRY_API_TOKEN',
376
- database: { adapter: 'pg' }, // 'pg' | 'neon'
635
+ ## MCP
636
+
637
+ The documentation app serves a Model Context Protocol endpoint at `/api/mcp/`, so
638
+ coding agents can look up the pages, components and tokens that already exist instead
639
+ of inventing markup and values. It is stateless Streamable HTTP over `POST`;
640
+ there is nothing to start and no extra port. Keep the trailing slash: the app
641
+ sets `trailingSlash: true`, so `/api/mcp` answers with a 308 redirect.
642
+
643
+ | Target | Endpoint | Credential |
644
+ | --- | --- | --- |
645
+ | Workspace (`dev` / `start`) | `http://localhost:3000/api/mcp/` | none, as with `/api/docs/*` |
646
+ | Registry (standalone or Vercel) | `<registry url>/api/mcp/` | an access token, read-only is enough |
647
+ | Static export | not available, a static site has no API routes | n/a |
648
+
649
+ Registry tokens come from `handoff-app login` or Account settings in the
650
+ registry itself, and are the same credentials the rest of the registry API takes.
651
+
652
+ The endpoint is served unless a project turns it off:
653
+
654
+ ```ts
655
+ runtime: {
656
+ mcp: false,
657
+ },
658
+ ```
659
+
660
+ Like `runtime.mode`, this is config-only and baked at build time, so changing it
661
+ in a deployed registry means a rebuild. A disabled build answers 404 on the route,
662
+ drops the MCP SDK from the packaged bundle, and shows no connect affordance at all
663
+ rather than one that fails.
664
+
665
+ The documentation app carries its own endpoint and a ready-made client config
666
+ for Claude Code, Cursor and VS Code behind the plug icon in the header. By hand:
667
+
668
+ ```json
669
+ {
670
+ "mcpServers": {
671
+ "handoff": {
672
+ "type": "http",
673
+ "url": "https://registry.example.com/api/mcp/",
674
+ "headers": { "Authorization": "Bearer hnd_..." }
675
+ }
676
+ }
677
+ }
678
+ ```
679
+
680
+ ## AI assistant
681
+
682
+ The documentation app can answer questions about the design system. The
683
+ assistant reads through the same MCP tools, so every answer comes from the
684
+ catalog and links to the pages and components it read. Open it from the search
685
+ control in the header, or with `⌘K`.
686
+
687
+ The deployment chooses the provider:
688
+
689
+ ```ts
690
+ runtime: {
691
+ ai: {
692
+ connections: [
693
+ // Service key: the deployment pays for every reader.
694
+ {
695
+ id: 'gateway',
696
+ label: 'Acme LiteLLM',
697
+ baseUrl: 'https://llm.acme.internal/v1',
698
+ apiKey: fromEnv('LITELLM_API_KEY'),
699
+ models: ['gpt-4o', 'claude-sonnet-4-5', 'grok-4'],
700
+ },
701
+ // No credential: the endpoint needs none.
702
+ { id: 'local', label: 'Ollama', baseUrl: 'http://127.0.0.1:11434/v1', models: ['llama3.1'] },
703
+ // User key: the config fixes the endpoint and models. Each reader adds their own key.
704
+ {
705
+ id: 'openai',
706
+ label: 'OpenAI',
707
+ baseUrl: 'https://api.openai.com/v1',
708
+ credential: 'user',
709
+ models: ['gpt-4o', 'o3'],
710
+ },
711
+ ],
712
+ defaultModel: 'gateway/claude-sonnet-4-5',
377
713
  },
378
714
  },
379
715
  ```
380
716
 
381
- Secrets are always referenced by environment-variable name, never written as
382
- values, and are redacted from API responses.
717
+ The `ai` block turns the assistant on, and `ai: { enabled: false }` turns it
718
+ back off. Like `runtime.mcp`, that flag is baked at build time, so a build
719
+ without it serves no chat route and shows no control.
720
+
721
+ **The declared connections are the whole surface.** A reader can only add a key
722
+ for a connection the config already declares, so the server never calls a URL a
723
+ reader chose. Without a `credential: 'user'` connection, there is no
724
+ reader-facing setting.
725
+
726
+ Every connection speaks the OpenAI-compatible `/chat/completions` API. This does
727
+ not limit which models a reader can use. Ollama serves an OpenAI-compatible API
728
+ at `/v1`, and LiteLLM, OpenRouter, vLLM, LM Studio and Azure OpenAI each front
729
+ Anthropic, Google and xAI models. A provider that fits nothing else names a
730
+ server-only module that default-exports `defineAiProvider()`, the way
731
+ `runtime.registry.assetStorage.module` does.
732
+
733
+ ### Reader keys
734
+
735
+ A connection marked `credential: 'user'` asks each reader for their own key,
736
+ under Account → AI providers. Keys are encrypted with `HANDOFF_AI_KEY_SECRET`,
737
+ not hashed, because the server must send them to the provider. Set that variable
738
+ to a random value of at least 32 characters. A key is write-only across the API:
739
+ after a save, a read reports only that a key is in place.
740
+
741
+ This applies to registry mode only. A workspace has one user, whose config file
742
+ and `.env` are their user layer, so a workspace supplies a key through `apiKey`.
743
+
744
+ ### Changing connections without a rebuild
383
745
 
384
- ### Environment variables
746
+ The connection list is deployment data, not build shape, so a registry can
747
+ change it without a rebuild. `HANDOFF_AI_CONNECTIONS` holds a JSON array, read
748
+ at request time and merged over the baked list by `id`. It names each key
749
+ instead of holding its value:
750
+
751
+ ```json
752
+ [{ "id": "gateway", "label": "Acme LiteLLM", "baseUrl": "https://llm.acme.internal/v1",
753
+ "apiKeyEnv": "LITELLM_API_KEY", "models": ["gpt-4o"] }]
754
+ ```
755
+
756
+ Spend control belongs to the gateway. LiteLLM and OpenRouter both enforce
757
+ budgets and rate limits per key. The assistant caps only how many steps one
758
+ question can take.
759
+
760
+ ## Configuration
761
+
762
+ Configuration is read from `handoff.config.ts`, `.js`, `.cjs`, or `.json`, in
763
+ that order. `defineConfig` provides typed authoring. Values merge onto the
764
+ defaults: plain objects merge recursively, and arrays, scalars, `null`, and
765
+ functions replace.
766
+
767
+ Useful environment variables:
385
768
 
386
769
  | Variable | Purpose |
387
770
  | --- | --- |
771
+ | `HANDOFF_PROFILE` | Config profile merged onto the base config |
388
772
  | `HANDOFF_FIGMA_PROJECT_ID` | Figma file ID used by `fetch` |
389
773
  | `HANDOFF_DEV_ACCESS_TOKEN` | Figma personal access token used by `fetch` |
390
- | `HANDOFF_REGISTRY_URL` | Default env var for a connected workspace's registry URL |
391
- | `HANDOFF_REGISTRY_ACCESS_TOKEN` | Connected-workspace access token for publish/checkout |
392
- | `DATABASE_URL` | Registry database connection string (name is configurable) |
393
- | `HANDOFF_REGISTRY_API_TOKEN` | Bearer token guarding registry management mutations |
774
+ | `HANDOFF_REGISTRY_URL` | Connected workspace registry URL |
775
+ | `HANDOFF_REGISTRY_ACCESS_TOKEN` | Registry access token used by a connected workspace |
776
+ | `HANDOFF_SYNC_SECRET` | Optional deployment-wide registry credential |
777
+ | `DATABASE_URL` | Registry PostgreSQL connection string |
778
+ | `AUTH_SECRET` | Registry session-signing secret, at least 32 characters |
779
+ | `AUTH_URL` | Canonical public registry URL |
780
+ | `PORT` | Standalone registry server port |
781
+ | `HOSTNAME` | Standalone registry bind hostname |
782
+ | `BLOB_READ_WRITE_TOKEN` | Credential for the Vercel Blob asset adapter |
783
+ | `HANDOFF_AI_KEY_SECRET` | Encrypts reader-supplied AI provider keys, at least 32 characters |
784
+ | `HANDOFF_AI_CONNECTIONS` | JSON array of AI connections, merged over the baked list by `id` |
785
+ | `HANDOFF_OUTPUT_DIR` | Override the fetched output directory |
786
+ | `HANDOFF_SITES_DIR` | Override the build output directory |
787
+ | `HANDOFF_WORKING_PATH` | Directory holding `handoff.config.ts`; defaults to the current directory |
788
+ | `HANDOFF_APP_PORT` | Workspace documentation server port |
789
+ | `HANDOFF_WEBSOCKET_PORT` | Workspace live-reload server port |
790
+
791
+ Registry assets are stored in PostgreSQL by default with a 4 MB per-blob limit.
792
+ Vercel Blob or a custom adapter can be selected through
793
+ `runtime.registry.assetStorage`. The `pg` or `neon` connection driver can be
794
+ selected through `runtime.registry.database.driver`; PostgreSQL is used by
795
+ both.
796
+
797
+ ## Profiles
798
+
799
+ A profile is a sidecar config file that merges onto the base config. It holds
800
+ only what changes between environments, so a project keeps one shared config
801
+ instead of a second complete copy of it.
802
+
803
+ - The base config is `handoff.config.ts`, `.js`, `.cjs`, or `.json`. It is the
804
+ only config file loaded when no profile is selected.
805
+ - A profile file is `handoff.config.<profile>.*`, with the same four
806
+ extensions.
807
+ - Select a profile with `--profile <name>` or with `HANDOFF_PROFILE`.
808
+ `--profile` takes precedence over `HANDOFF_PROFILE`.
809
+ - Profile names are not predefined. A name can contain lowercase letters,
810
+ numbers, and hyphens.
811
+ - A selected profile must exist. If no `handoff.config.<name>.*` file is found,
812
+ the command stops with an error.
813
+ - `login`, `logout`, `publish`, and `checkout` also accept a profile that has
814
+ only a saved registry login, because there the profile selects the registry.
815
+ Every other command still needs the config file.
816
+ - Layers resolve as defaults, base config, profile, then programmatic config,
817
+ with the merge rules described under [Configuration](#configuration).
818
+ - A selected profile also reads `.env.<profile>` on top of `.env`, when that
819
+ file exists. Both are read from the directory the command runs in, and a
820
+ variable already set in the environment beats both files.
821
+
822
+ ```bash
823
+ npx handoff-app build --target registry --profile registry
824
+ ```
825
+
826
+ `build --target registry` always packages a registry-mode application, so a
827
+ profile does not need to set `runtime.mode`. This makes a profile a good place
828
+ for the registry build settings: the database driver, the database
829
+ environment-variable name, the asset storage, and whether MCP is enabled. A
830
+ hosting provider or a CI job selects the profile through `HANDOFF_PROFILE`.
394
831
 
395
- ### Hooks
832
+ `defineConfig` types a profile as well as a base config. The generated
833
+ `.gitignore` lists `handoff.config.local.*`, which makes `local` the usual name
834
+ for a profile that stays on one machine.
396
835
 
397
- Pipeline customization is done in your config under `hooks` (camelCase names such
398
- as `validateComponent`, `jsBuildConfig`, `registerHandlebarsHelpers`). For
399
- example, `registerHandlebarsHelpers` runs after Handoff registers the built-in
400
- `field` and `eq` helpers, so you can register your own helpers for `.hbs`
401
- preview templates.
836
+ Arrays replace, so a profile that declares `catalog.include` also decides what
837
+ `make` and `checkout` register.
402
838
 
403
- See [docs/api.md](docs/api.md#hooks) for hook arguments and examples.
839
+ `.env.<profile>` is read once at startup rather than watched.
840
+ `HANDOFF_WORKING_PATH` cannot be set from it, because the working path is
841
+ resolved before any profile is known.
404
842
 
405
843
  ## CLI reference
406
844
 
407
845
  | Command | Description |
408
846
  | --- | --- |
409
- | `init` | Scaffold a new workspace project with the interactive wizard |
410
- | `fetch` | Fetch design tokens from Figma |
411
- | `dev` | Start the local dev server with the live docs read API |
412
- | `start` | Start the dev server with file watchers for live authoring |
413
- | `build [--target static\|registry]` | Build the static site (default) or package the registry app |
414
- | `build:components [component]` | Build components without building the full app |
415
- | `publish <component\|pattern> <id>` | Build and publish an entity to the connected registry |
416
- | `checkout <component\|pattern> <id>` | Pull an entity from the connected registry |
417
- | `db:migrate` | Run registry database migrations (Drizzle / PostgreSQL) |
418
- | `make:component <name>` | Scaffold a new component |
419
- | `make:page <name> [parent]` | Scaffold a documentation page |
420
- | `make:template <component> [state]` | Scaffold a preview template |
421
- | `eject:config` | Eject the default config to the project |
422
- | `eject:pages` | Eject the default documentation pages |
423
- | `eject:theme` | Eject the default theme |
424
- | `validate:components` | Run component validation |
425
-
426
- Global flags: `-c, --config <file>`, `-d, --debug`, `-f, --force`,
427
- `-h, --help`, `-v, --version`. See [docs/cli.md](docs/cli.md) for the full
428
- reference.
429
-
430
- ## Further reading
431
-
432
- * [Configure your project](https://www.handoff.com/docs/customization)
433
- * [Customize the content](https://www.handoff.com/docs/customization/content)
434
- * [Integrate tokens with your project](https://www.handoff.com/docs/tokens/integration)
435
- * [Build to static assets](https://www.handoff.com/docs/tokens/publishing)
436
- * [GitHub Actions CI/CD](https://www.handoff.com/docs/infrastructure/github/)
437
- * [Bitbucket Pipelines CI/CD](https://www.handoff.com/docs/infrastructure/bitbucket/)
847
+ | `npx handoff-app init` | A workspace is scaffolded with the interactive wizard |
848
+ | `npm run fetch` | Design tokens and assets are fetched from Figma |
849
+ | `npm run start` | The workspace server and Handoff file watchers are started |
850
+ | `npm run dev` | The workspace development server is started |
851
+ | `npm run build -- [--target static\|registry]` | The static site or standalone registry bundle is built |
852
+ | `npm run db:migrate` | Registry database migrations are applied |
853
+ | `npm run validate` | Configured components are validated |
854
+ | `npm run publish -- <kind\|all> [id...]` | Catalog items, pages, tokens, or assets are published |
855
+ | `npm run checkout -- <kind\|all> [id...]` | Published content is pulled into a workspace |
856
+ | `npm run login -- [--profile <name>] --url <url>` | The CLI is authorized through the registry device flow |
857
+ | `npm run logout -- [--profile <name>] [--all]` | Saved CLI credentials are revoked and removed |
858
+
859
+ Every command except `init` accepts `-c, --config`, `--profile`, `-d, --debug`,
860
+ and `-f, --force`. `publish` and `checkout` additionally accept `--dry-run`, and
861
+ `publish` accepts `--no-build`.
862
+
863
+ Arguments after `--` are forwarded to the local CLI. Exact options can be shown
864
+ by adding `--help` after the separator. Advanced configuration and hooks are
865
+ documented in [docs/api.md](docs/api.md).
438
866
 
439
867
  ## Maintainers
440
868
 
@@ -446,8 +874,8 @@ reference.
446
874
 
447
875
  ## Contributing
448
876
 
449
- Feel free to dive in. [Open an issue](https://github.com/Convertiv/handoff-app/issues/new)
450
- or submit a PR.
877
+ [Issues](https://github.com/Convertiv/handoff-app/issues/new) and pull requests
878
+ are welcome.
451
879
 
452
880
  Handoff follows the [Contributor Covenant](http://contributor-covenant.org/version/1/3/0/)
453
881
  Code of Conduct.