handoff-app 2.0.0-1 → 2.0.0-11

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 (612) hide show
  1. package/AGENTS.md +7 -0
  2. package/CLAUDE.md +7 -0
  3. package/README.md +1001 -301
  4. package/UPGRADE.md +266 -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 +66 -0
  20. package/dist/ai/connections.js +149 -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 +26 -0
  24. package/dist/ai/types.js +10 -0
  25. package/dist/app-builder/build.d.ts +14 -7
  26. package/dist/app-builder/build.js +308 -106
  27. package/dist/app-builder/client-config.js +18 -13
  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.d.ts +0 -12
  31. package/dist/app-builder/paths.js +9 -28
  32. package/dist/app-builder/static-export.js +6 -6
  33. package/dist/app-builder/vercel-output.d.ts +1 -1
  34. package/dist/app-builder/vercel-output.js +11 -10
  35. package/dist/app-builder/watchers/component.js +5 -5
  36. package/dist/app-builder/watchers/utils.d.ts +2 -3
  37. package/dist/app-builder/watchers.d.ts +8 -15
  38. package/dist/app-builder/watchers.js +70 -61
  39. package/dist/artifacts/url.d.ts +7 -7
  40. package/dist/artifacts/url.js +7 -7
  41. package/dist/cache/build-cache.d.ts +27 -4
  42. package/dist/cache/build-cache.js +132 -65
  43. package/dist/cache/build-inputs.d.ts +13 -0
  44. package/dist/cache/build-inputs.js +19 -0
  45. package/dist/cache/index.d.ts +2 -1
  46. package/dist/cache/index.js +5 -1
  47. package/dist/catalog/define.d.ts +6 -0
  48. package/dist/catalog/define.js +72 -0
  49. package/dist/catalog/implementation.d.ts +27 -0
  50. package/dist/catalog/implementation.js +133 -0
  51. package/dist/catalog/index.d.ts +6 -0
  52. package/dist/catalog/index.js +22 -0
  53. package/dist/catalog/normalize.d.ts +16 -0
  54. package/dist/catalog/normalize.js +118 -0
  55. package/dist/catalog/previews.d.ts +18 -0
  56. package/dist/catalog/previews.js +107 -0
  57. package/dist/catalog/renderers.d.ts +43 -0
  58. package/dist/catalog/renderers.js +62 -0
  59. package/dist/catalog/types.d.ts +71 -0
  60. package/dist/cli/auth/browser.d.ts +2 -0
  61. package/dist/cli/auth/browser.js +33 -0
  62. package/dist/cli/auth/device.d.ts +16 -0
  63. package/dist/cli/auth/device.js +158 -0
  64. package/dist/cli/auth/store.d.ts +38 -0
  65. package/dist/cli/auth/store.js +199 -0
  66. package/dist/cli/create.js +48 -3
  67. package/dist/cli/make.d.ts +0 -5
  68. package/dist/cli/make.js +24 -69
  69. package/dist/cli/scaffold.js +39 -181
  70. package/dist/cli.js +39 -7
  71. package/dist/commands/build/app.js +1 -5
  72. package/dist/commands/build/components.js +1 -5
  73. package/dist/commands/build/index.d.ts +1 -1
  74. package/dist/commands/build/index.js +2 -6
  75. package/dist/commands/checkout/index.d.ts +18 -12
  76. package/dist/commands/checkout/index.js +19 -59
  77. package/dist/commands/compat/deprecations.d.ts +46 -0
  78. package/dist/commands/compat/deprecations.js +62 -0
  79. package/dist/commands/compat/push-args.d.ts +48 -0
  80. package/dist/commands/compat/push-args.js +73 -0
  81. package/dist/commands/db/migrate.js +1 -5
  82. package/dist/commands/dev/index.js +1 -5
  83. package/dist/commands/eject/config.js +1 -5
  84. package/dist/commands/eject/pages.js +1 -5
  85. package/dist/commands/eject/theme.js +1 -5
  86. package/dist/commands/fetch/index.js +1 -5
  87. package/dist/commands/index.js +8 -2
  88. package/dist/commands/login/index.d.ts +13 -0
  89. package/dist/commands/login/index.js +61 -0
  90. package/dist/commands/logout/index.d.ts +12 -0
  91. package/dist/commands/logout/index.js +89 -0
  92. package/dist/commands/make/component.js +1 -5
  93. package/dist/commands/make/page.js +3 -7
  94. package/dist/commands/publish/index.d.ts +21 -11
  95. package/dist/commands/publish/index.js +22 -58
  96. package/dist/commands/pull/index.d.ts +13 -0
  97. package/dist/commands/pull/index.js +20 -0
  98. package/dist/commands/push/index.d.ts +23 -0
  99. package/dist/commands/push/index.js +58 -0
  100. package/dist/commands/scaffold/index.js +1 -5
  101. package/dist/commands/start/index.js +1 -5
  102. package/dist/commands/types.d.ts +12 -0
  103. package/dist/commands/utils.d.ts +100 -1
  104. package/dist/commands/utils.js +103 -2
  105. package/dist/commands/validate/components.js +1 -5
  106. package/dist/config/catalog-include.d.ts +43 -0
  107. package/dist/config/catalog-include.js +203 -0
  108. package/dist/config/defaults.d.ts +4 -2
  109. package/dist/config/defaults.js +61 -49
  110. package/dist/config/env.d.ts +9 -0
  111. package/dist/config/env.js +43 -0
  112. package/dist/config/errors.d.ts +4 -0
  113. package/dist/config/errors.js +11 -0
  114. package/dist/config/from-env.d.ts +28 -0
  115. package/dist/config/from-env.js +44 -0
  116. package/dist/config/helpers.d.ts +2 -0
  117. package/dist/config/helpers.js +4 -2
  118. package/dist/config/index.d.ts +6 -3
  119. package/dist/config/index.js +12 -4
  120. package/dist/config/loader.d.ts +40 -9
  121. package/dist/config/loader.js +174 -36
  122. package/dist/config/normalizers/page.d.ts +4 -4
  123. package/dist/config/normalizers/page.js +2 -2
  124. package/dist/config/resolve-env.d.ts +6 -0
  125. package/dist/config/resolve-env.js +118 -0
  126. package/dist/config/runtime.d.ts +15 -16
  127. package/dist/config/runtime.js +179 -197
  128. package/dist/define.d.ts +10 -0
  129. package/dist/define.js +13 -0
  130. package/dist/handlebars.d.ts +17 -0
  131. package/dist/handlebars.js +16 -0
  132. package/dist/index.d.ts +129 -33
  133. package/dist/index.js +256 -73
  134. package/dist/nav/resolver.js +3 -1
  135. package/dist/nav/workspace.js +2 -2
  136. package/dist/pattern.d.ts +7 -0
  137. package/dist/pattern.js +13 -0
  138. package/dist/pipeline/figma.d.ts +13 -2
  139. package/dist/pipeline/figma.js +86 -46
  140. package/dist/pipeline/index.d.ts +1 -1
  141. package/dist/pipeline/index.js +1 -1
  142. package/dist/pipeline/styles.js +1 -1
  143. package/dist/pipeline/validation.d.ts +1 -1
  144. package/dist/pipeline/validation.js +3 -3
  145. package/dist/react.d.ts +43 -0
  146. package/dist/react.js +11 -0
  147. package/dist/registry/asset-storage/define.d.ts +1 -1
  148. package/dist/registry/asset-storage/define.js +1 -1
  149. package/dist/registry/asset-storage/resolve.d.ts +9 -16
  150. package/dist/registry/asset-storage/resolve.js +33 -17
  151. package/dist/registry/asset-storage/types.d.ts +28 -8
  152. package/dist/registry/asset-storage/types.js +5 -5
  153. package/dist/registry/auth/access-tokens.d.ts +54 -0
  154. package/dist/registry/auth/access-tokens.js +191 -0
  155. package/dist/registry/auth/action-tokens.d.ts +66 -0
  156. package/dist/registry/auth/action-tokens.js +178 -0
  157. package/dist/registry/auth/ai-keys.d.ts +32 -0
  158. package/dist/registry/auth/ai-keys.js +69 -0
  159. package/dist/registry/auth/crypto.d.ts +21 -0
  160. package/dist/registry/auth/crypto.js +76 -0
  161. package/dist/registry/auth/database.d.ts +7 -0
  162. package/dist/registry/auth/database.js +22 -0
  163. package/dist/registry/auth/device-authorizations.d.ts +51 -0
  164. package/dist/registry/auth/device-authorizations.js +170 -0
  165. package/dist/registry/auth/gravatar.d.ts +1 -0
  166. package/dist/registry/auth/gravatar.js +8 -0
  167. package/dist/registry/auth/index.d.ts +13 -0
  168. package/dist/registry/auth/index.js +89 -0
  169. package/dist/registry/auth/installation.d.ts +42 -0
  170. package/dist/registry/auth/installation.js +105 -0
  171. package/dist/registry/auth/password.d.ts +12 -0
  172. package/dist/registry/auth/password.js +58 -0
  173. package/dist/registry/auth/rate-limits.d.ts +22 -0
  174. package/dist/registry/auth/rate-limits.js +75 -0
  175. package/dist/registry/auth/sync-secret.d.ts +27 -0
  176. package/dist/registry/auth/sync-secret.js +65 -0
  177. package/dist/registry/auth/types.d.ts +45 -0
  178. package/dist/registry/auth/types.js +11 -0
  179. package/dist/registry/auth/user-record.d.ts +13 -0
  180. package/dist/registry/auth/user-record.js +16 -0
  181. package/dist/registry/auth/users.d.ts +31 -0
  182. package/dist/registry/auth/users.js +135 -0
  183. package/dist/registry/auth/validation.d.ts +14 -0
  184. package/dist/registry/auth/validation.js +45 -0
  185. package/dist/registry/checkout/assets.d.ts +1 -1
  186. package/dist/registry/checkout/assets.js +25 -8
  187. package/dist/registry/checkout/index.d.ts +29 -7
  188. package/dist/registry/checkout/index.js +432 -154
  189. package/dist/registry/checkout/tokens.d.ts +1 -1
  190. package/dist/registry/checkout/tokens.js +16 -7
  191. package/dist/registry/client.d.ts +2 -1
  192. package/dist/registry/client.js +61 -17
  193. package/dist/registry/connection.d.ts +34 -11
  194. package/dist/registry/connection.js +77 -18
  195. package/dist/registry/content-kinds.d.ts +27 -0
  196. package/dist/registry/content-kinds.js +33 -0
  197. package/dist/registry/db/client.d.ts +14 -9
  198. package/dist/registry/db/client.js +22 -9
  199. package/dist/registry/db/driver.d.ts +39 -0
  200. package/dist/registry/db/driver.js +50 -0
  201. package/dist/registry/db/index.d.ts +2 -2
  202. package/dist/registry/db/index.js +14 -9
  203. package/dist/registry/db/migrate.d.ts +1 -1
  204. package/dist/registry/db/migrate.js +5 -5
  205. package/dist/registry/db/schema.d.ts +3035 -938
  206. package/dist/registry/db/schema.js +163 -9
  207. package/dist/registry/email/adapters/resend.d.ts +7 -0
  208. package/dist/registry/email/adapters/resend.js +35 -0
  209. package/dist/registry/email/adapters/smtp.d.ts +10 -0
  210. package/dist/registry/email/adapters/smtp.js +73 -0
  211. package/dist/registry/email/define.d.ts +10 -0
  212. package/dist/registry/email/define.js +13 -0
  213. package/dist/registry/email/resolve.d.ts +29 -0
  214. package/dist/registry/email/resolve.js +76 -0
  215. package/dist/registry/email/types.d.ts +24 -0
  216. package/dist/registry/email/types.js +6 -0
  217. package/dist/registry/path.d.ts +2 -0
  218. package/dist/registry/path.js +11 -1
  219. package/dist/registry/publish/assets.d.ts +2 -2
  220. package/dist/registry/publish/assets.js +46 -35
  221. package/dist/registry/publish/errors.js +3 -3
  222. package/dist/registry/publish/index.d.ts +45 -3
  223. package/dist/registry/publish/index.js +247 -24
  224. package/dist/registry/publish/package.js +22 -1
  225. package/dist/registry/publish/tokens.d.ts +2 -2
  226. package/dist/registry/publish/tokens.js +30 -24
  227. package/dist/registry/selection.d.ts +32 -0
  228. package/dist/registry/selection.js +58 -0
  229. package/dist/registry/transfer.d.ts +17 -0
  230. package/dist/store/filesystem.d.ts +3 -3
  231. package/dist/store/filesystem.js +7 -7
  232. package/dist/store/registry.d.ts +48 -0
  233. package/dist/store/registry.js +99 -15
  234. package/dist/transformers/plugins/csf-render.js +4 -2
  235. package/dist/transformers/plugins/ssr-render.js +52 -18
  236. package/dist/transformers/preview/component/api.d.ts +8 -0
  237. package/dist/transformers/preview/component/api.js +11 -15
  238. package/dist/transformers/preview/component/artifacts.d.ts +1 -1
  239. package/dist/transformers/preview/component/artifacts.js +11 -7
  240. package/dist/transformers/preview/component/builder.d.ts +1 -9
  241. package/dist/transformers/preview/component/builder.js +32 -35
  242. package/dist/transformers/preview/component/css.d.ts +5 -1
  243. package/dist/transformers/preview/component/css.js +100 -62
  244. package/dist/transformers/preview/component/html.d.ts +3 -1
  245. package/dist/transformers/preview/component/html.js +24 -18
  246. package/dist/transformers/preview/component/javascript.d.ts +2 -1
  247. package/dist/transformers/preview/component/javascript.js +12 -8
  248. package/dist/transformers/preview/pattern/builder.js +31 -18
  249. package/dist/transformers/preview/pattern/html.js +3 -1
  250. package/dist/transformers/preview/types.d.ts +30 -3
  251. package/dist/transformers/utils/build.d.ts +24 -0
  252. package/dist/transformers/utils/build.js +100 -1
  253. package/dist/transformers/utils/module.js +2 -1
  254. package/dist/types/config.d.ts +220 -43
  255. package/dist/utils/markdown.d.ts +10 -0
  256. package/dist/utils/markdown.js +17 -0
  257. package/dist/utils/menu-shell.js +14 -7
  258. package/dist/utils/pages.d.ts +22 -0
  259. package/dist/utils/pages.js +73 -0
  260. package/dist/utils/path.d.ts +6 -5
  261. package/dist/utils/path.js +15 -23
  262. package/docs/api.md +41 -7
  263. package/docs/cli.md +7 -3
  264. package/drizzle/0002_add_pages.sql +5 -3
  265. package/drizzle/0005_add_registry_auth.sql +95 -0
  266. package/drizzle/0006_add_component_source_format.sql +3 -0
  267. package/drizzle/0007_add_ai_keys.sql +11 -0
  268. package/drizzle/0008_add_source_dir.sql +2 -0
  269. package/drizzle/meta/0002_snapshot.json +17 -8
  270. package/drizzle/meta/0003_snapshot.json +17 -8
  271. package/drizzle/meta/0004_snapshot.json +17 -8
  272. package/drizzle/meta/0005_snapshot.json +1935 -0
  273. package/drizzle/meta/0006_snapshot.json +1941 -0
  274. package/drizzle/meta/0007_snapshot.json +2013 -0
  275. package/drizzle/meta/0008_snapshot.json +2025 -0
  276. package/drizzle/meta/_journal.json +28 -0
  277. package/drizzle.config.ts +1 -1
  278. package/package.json +57 -9
  279. package/src/ai/README.md +42 -0
  280. package/src/ai/connections.ts +189 -0
  281. package/src/ai/define.ts +15 -0
  282. package/src/ai/types.ts +30 -0
  283. package/src/app/components/AiAssistant/AiAssistantPanel.tsx +144 -0
  284. package/src/app/components/AiAssistant/AiAssistantProvider.tsx +215 -0
  285. package/src/app/components/AiAssistant/AiConversation.tsx +737 -0
  286. package/src/app/components/AiAssistant/AiToolResult.tsx +99 -0
  287. package/src/app/components/AiAssistant/dockState.ts +86 -0
  288. package/src/app/components/AiAssistant/toolLabel.ts +24 -0
  289. package/src/app/components/AiAssistant/useAiChat.ts +147 -0
  290. package/src/app/components/AiAssistant/useAiConnections.ts +86 -0
  291. package/src/app/components/AiAssistant/usePageContext.ts +63 -0
  292. package/src/app/components/Auth/AccountLayout.tsx +126 -0
  293. package/src/app/components/Auth/AuthControls.tsx +99 -0
  294. package/src/app/components/Auth/AuthShell.tsx +61 -0
  295. package/src/app/components/Auth/RegistryInstallGate.tsx +19 -0
  296. package/src/app/components/Auth/RegistrySessionProvider.tsx +15 -0
  297. package/src/app/components/Auth/api.ts +20 -0
  298. package/src/app/components/Auth/installStatus.ts +29 -0
  299. package/src/app/components/Component/PageSliceResolver.tsx +1 -0
  300. package/src/app/components/Component/Preview.tsx +406 -226
  301. package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +4 -0
  302. package/src/app/components/CopyToClipboard.tsx +27 -26
  303. package/src/app/components/Foundations/DisplayLogo.tsx +3 -3
  304. package/src/app/components/Layout/Header.tsx +25 -25
  305. package/src/app/components/Layout/RuntimeModeBadge.tsx +18 -0
  306. package/src/app/components/Markdown/CodeHighlight.tsx +73 -70
  307. package/src/app/components/Markdown/MarkdownComponents.tsx +30 -32
  308. package/src/app/components/McpIntegration/McpConfigDialog.tsx +349 -0
  309. package/src/app/components/McpIntegration/McpIcon.tsx +10 -0
  310. package/src/app/components/Navigation/MobileNav.tsx +7 -1
  311. package/src/app/components/Navigation/SideNav.tsx +1 -15
  312. package/src/app/components/context/NavProvider.tsx +7 -3
  313. package/src/app/components/context/PreviewContext.tsx +5 -5
  314. package/src/app/components/ui/attachment.tsx +50 -0
  315. package/src/app/components/ui/bubble.tsx +57 -0
  316. package/src/app/components/ui/button.tsx +3 -1
  317. package/src/app/components/ui/confirm-dialog.tsx +87 -0
  318. package/src/app/components/ui/dialog.tsx +78 -0
  319. package/src/app/components/ui/marker.tsx +56 -0
  320. package/src/app/components/ui/sidebar.tsx +3 -1
  321. package/src/app/components/util/index.ts +14 -38
  322. package/src/app/css/index.css +64 -0
  323. package/src/app/lib/ai/auth.ts +33 -0
  324. package/src/app/lib/ai/context.ts +99 -0
  325. package/src/app/lib/ai/model.ts +79 -0
  326. package/src/app/lib/ai/prompt.ts +36 -0
  327. package/src/app/lib/ai/resolve.ts +97 -0
  328. package/src/app/lib/ai/tools.ts +120 -0
  329. package/src/app/lib/api/methods.ts +13 -0
  330. package/src/app/lib/asset-storage.ts +23 -24
  331. package/src/app/lib/auth/api.ts +105 -0
  332. package/src/app/lib/auth/config.ts +175 -0
  333. package/src/app/lib/auth/email.ts +108 -0
  334. package/src/app/lib/docs-api/backend.ts +16 -1
  335. package/src/app/lib/docs-api/errors.ts +8 -7
  336. package/src/app/lib/docs-api/handler.ts +2 -3
  337. package/src/app/lib/docs-api/index.ts +2 -0
  338. package/src/app/lib/docs-api/page-discovery.ts +121 -0
  339. package/src/app/lib/docs-api/page-rendering.ts +37 -0
  340. package/src/app/lib/docs-api/page-search.ts +191 -0
  341. package/src/app/lib/docs-api/records.ts +31 -24
  342. package/src/app/lib/docs-api/registry.ts +92 -11
  343. package/src/app/lib/docs-api/runtime-config.ts +56 -39
  344. package/src/app/lib/docs-api/search.ts +120 -0
  345. package/src/app/lib/mcp/auth.ts +64 -0
  346. package/src/app/lib/mcp/components.ts +519 -0
  347. package/src/app/lib/mcp/index.ts +2 -0
  348. package/src/app/lib/mcp/page-url.ts +24 -0
  349. package/src/app/lib/mcp/pages.ts +77 -0
  350. package/src/app/lib/mcp/result.ts +33 -0
  351. package/src/app/lib/mcp/server.ts +40 -0
  352. package/src/app/lib/mcp/tokens.ts +328 -0
  353. package/src/app/lib/registry-api/allowlist.ts +7 -5
  354. package/src/app/lib/registry-api/asset-transfer.ts +170 -35
  355. package/src/app/lib/registry-api/auth.ts +108 -45
  356. package/src/app/lib/registry-api/entity-routes.ts +17 -7
  357. package/src/app/lib/registry-api/errors.ts +14 -6
  358. package/src/app/lib/registry-api/handler.ts +28 -19
  359. package/src/app/lib/registry-api/meta.ts +4 -0
  360. package/src/app/lib/registry-api/revalidate.ts +31 -12
  361. package/src/app/lib/registry-api/store.ts +89 -26
  362. package/src/app/lib/registry-api/sync-compat.ts +166 -0
  363. package/src/app/lib/registry-api/token-transfer.ts +45 -13
  364. package/src/app/lib/registry-api/transfer-batch.ts +116 -0
  365. package/src/app/lib/registry-api/transfer.ts +125 -45
  366. package/src/app/lib/registry-api/validation.ts +33 -1
  367. package/src/app/lib/registry-connection.ts +2 -2
  368. package/src/app/lib/server-module.ts +34 -0
  369. package/src/app/lib/utils.ts +13 -0
  370. package/src/app/next.config.mjs +47 -27
  371. package/src/app/pages/[...slug]/index.tsx +52 -51
  372. package/src/app/pages/_app.tsx +14 -4
  373. package/src/app/pages/_document.tsx +6 -0
  374. package/src/app/pages/account/ai.tsx +183 -0
  375. package/src/app/pages/account/index.tsx +153 -0
  376. package/src/app/pages/account/tokens.tsx +262 -0
  377. package/src/app/pages/account/users.tsx +370 -0
  378. package/src/app/pages/api/account/ai/keys/[connectionId].ts +60 -0
  379. package/src/app/pages/api/account/ai/keys/index.ts +20 -0
  380. package/src/app/pages/api/account/index.ts +26 -0
  381. package/src/app/pages/api/account/tokens/[id].ts +17 -0
  382. package/src/app/pages/api/account/tokens/index.ts +35 -0
  383. package/src/app/pages/api/admin/users/[id]/[action].ts +158 -0
  384. package/src/app/pages/api/admin/users/index.ts +53 -0
  385. package/src/app/pages/api/ai/chat.ts +115 -0
  386. package/src/app/pages/api/ai/connections.ts +23 -0
  387. package/src/app/pages/api/auth/[...nextauth].ts +4 -0
  388. package/src/app/pages/api/auth/request-reset.ts +51 -0
  389. package/src/app/pages/api/auth/reset-password.ts +29 -0
  390. package/src/app/pages/api/docs/search/pages.json.ts +25 -0
  391. package/src/app/pages/api/install.ts +102 -0
  392. package/src/app/pages/api/mcp.ts +56 -0
  393. package/src/app/pages/api/oauth/device/approve.ts +23 -0
  394. package/src/app/pages/api/oauth/device/index.ts +41 -0
  395. package/src/app/pages/api/oauth/revoke.ts +31 -0
  396. package/src/app/pages/api/oauth/token.ts +31 -0
  397. package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +1 -1
  398. package/src/app/pages/api/registry/components/[id].ts +2 -2
  399. package/src/app/pages/api/registry/components/index.ts +1 -1
  400. package/src/app/pages/api/registry/health.ts +4 -3
  401. package/src/app/pages/api/registry/pages/[...id].ts +20 -0
  402. package/src/app/pages/api/registry/pages/index.ts +19 -0
  403. package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +1 -1
  404. package/src/app/pages/api/registry/patterns/[id].ts +2 -2
  405. package/src/app/pages/api/registry/patterns/index.ts +1 -1
  406. package/src/app/pages/api/registry/transfer/[kind]/[id].ts +3 -3
  407. package/src/app/pages/api/registry/transfer/[kind]/index.ts +21 -0
  408. package/src/app/pages/api/registry/transfer/assets/[collection].ts +2 -1
  409. package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/complete.ts +11 -0
  410. package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/upload.ts +12 -0
  411. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +2 -1
  412. package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +2 -2
  413. package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +2 -2
  414. package/src/app/pages/api/sync/upload.ts +21 -0
  415. package/src/app/pages/cli/device.tsx +148 -0
  416. package/src/app/pages/foundations/icons/index.tsx +2 -2
  417. package/src/app/pages/foundations/icons/{[name] → library/[name]}/index.tsx +6 -6
  418. package/src/app/pages/index/index.tsx +14 -0
  419. package/src/app/pages/index.tsx +31 -5
  420. package/src/app/pages/install.tsx +341 -0
  421. package/src/app/pages/login.tsx +117 -0
  422. package/src/app/pages/reset-password.tsx +155 -0
  423. package/src/app/types/next-auth.d.ts +27 -0
  424. package/src/app-builder/README.md +2 -1
  425. package/src/app-builder/build.ts +317 -123
  426. package/src/app-builder/client-config.ts +20 -15
  427. package/src/app-builder/config-diff/strategies/pattern.ts +1 -1
  428. package/src/app-builder/nav-shell.ts +8 -13
  429. package/src/app-builder/paths.ts +9 -30
  430. package/src/app-builder/static-export.ts +6 -6
  431. package/src/app-builder/vercel-output.ts +11 -10
  432. package/src/app-builder/watchers/component.ts +3 -3
  433. package/src/app-builder/watchers/utils.ts +2 -3
  434. package/src/app-builder/watchers.ts +63 -57
  435. package/src/artifacts/url.ts +7 -7
  436. package/src/cache/README.md +4 -1
  437. package/src/cache/build-cache.ts +139 -53
  438. package/src/cache/build-inputs.ts +28 -0
  439. package/src/cache/index.ts +4 -0
  440. package/src/catalog/README.md +95 -0
  441. package/src/catalog/define.ts +79 -0
  442. package/src/catalog/implementation.ts +145 -0
  443. package/src/catalog/index.ts +31 -0
  444. package/src/catalog/normalize.ts +145 -0
  445. package/src/catalog/previews.ts +114 -0
  446. package/src/catalog/renderers.ts +94 -0
  447. package/src/catalog/types.ts +82 -0
  448. package/src/cli/README.md +1 -1
  449. package/src/cli/auth/browser.ts +29 -0
  450. package/src/cli/auth/device.ts +186 -0
  451. package/src/cli/auth/store.ts +194 -0
  452. package/src/cli/create.ts +58 -3
  453. package/src/cli/make.ts +22 -74
  454. package/src/cli/scaffold.ts +39 -222
  455. package/src/cli.ts +31 -7
  456. package/src/commands/README.md +1 -1
  457. package/src/commands/build/app.ts +2 -3
  458. package/src/commands/build/components.ts +2 -3
  459. package/src/commands/build/index.ts +3 -4
  460. package/src/commands/checkout/index.ts +29 -50
  461. package/src/commands/compat/deprecations.ts +79 -0
  462. package/src/commands/compat/push-args.ts +106 -0
  463. package/src/commands/db/migrate.ts +2 -3
  464. package/src/commands/dev/index.ts +2 -3
  465. package/src/commands/eject/config.ts +2 -3
  466. package/src/commands/eject/pages.ts +2 -3
  467. package/src/commands/eject/theme.ts +2 -3
  468. package/src/commands/fetch/index.ts +2 -3
  469. package/src/commands/index.ts +8 -2
  470. package/src/commands/login/index.ts +61 -0
  471. package/src/commands/logout/index.ts +104 -0
  472. package/src/commands/make/component.ts +2 -3
  473. package/src/commands/make/page.ts +4 -5
  474. package/src/commands/publish/index.ts +32 -49
  475. package/src/commands/pull/index.ts +32 -0
  476. package/src/commands/push/index.ts +115 -0
  477. package/src/commands/scaffold/index.ts +2 -3
  478. package/src/commands/start/index.ts +2 -3
  479. package/src/commands/types.ts +13 -1
  480. package/src/commands/utils.ts +107 -2
  481. package/src/commands/validate/components.ts +2 -3
  482. package/src/config/README.md +67 -5
  483. package/src/config/catalog-include.ts +227 -0
  484. package/src/config/defaults.ts +29 -13
  485. package/src/config/env.ts +41 -0
  486. package/src/config/errors.ts +7 -0
  487. package/src/config/from-env.ts +59 -0
  488. package/src/config/helpers.ts +3 -3
  489. package/src/config/index.ts +9 -4
  490. package/src/config/loader.ts +232 -44
  491. package/src/config/normalizers/page.ts +4 -4
  492. package/src/config/resolve-env.ts +107 -0
  493. package/src/config/runtime.ts +213 -227
  494. package/src/define.ts +18 -0
  495. package/src/handlebars.ts +24 -0
  496. package/src/index.d.ts +2 -2
  497. package/src/index.ts +341 -92
  498. package/src/nav/resolver.ts +3 -1
  499. package/src/nav/workspace.ts +2 -2
  500. package/src/pattern.ts +14 -0
  501. package/src/pipeline/README.md +1 -1
  502. package/src/pipeline/figma.ts +98 -49
  503. package/src/pipeline/index.ts +1 -1
  504. package/src/pipeline/styles.ts +1 -1
  505. package/src/pipeline/validation.ts +3 -3
  506. package/src/react.ts +52 -0
  507. package/src/registry/asset-storage/define.ts +1 -1
  508. package/src/registry/asset-storage/resolve.ts +37 -29
  509. package/src/registry/asset-storage/types.ts +30 -8
  510. package/src/registry/auth/access-tokens.ts +222 -0
  511. package/src/registry/auth/action-tokens.ts +225 -0
  512. package/src/registry/auth/ai-keys.ts +62 -0
  513. package/src/registry/auth/crypto.ts +75 -0
  514. package/src/registry/auth/database.ts +15 -0
  515. package/src/registry/auth/device-authorizations.ts +223 -0
  516. package/src/registry/auth/gravatar.ts +6 -0
  517. package/src/registry/auth/index.ts +105 -0
  518. package/src/registry/auth/installation.ts +117 -0
  519. package/src/registry/auth/password.ts +46 -0
  520. package/src/registry/auth/rate-limits.ts +84 -0
  521. package/src/registry/auth/sync-secret.ts +67 -0
  522. package/src/registry/auth/types.ts +57 -0
  523. package/src/registry/auth/user-record.ts +25 -0
  524. package/src/registry/auth/users.ts +145 -0
  525. package/src/registry/auth/validation.ts +42 -0
  526. package/src/registry/checkout/assets.ts +28 -9
  527. package/src/registry/checkout/index.ts +522 -148
  528. package/src/registry/checkout/tokens.ts +16 -8
  529. package/src/registry/client.ts +67 -21
  530. package/src/registry/connection.ts +91 -19
  531. package/src/registry/content-kinds.ts +39 -0
  532. package/src/registry/db/client.ts +34 -14
  533. package/src/registry/db/driver.ts +66 -0
  534. package/src/registry/db/index.ts +16 -5
  535. package/src/registry/db/migrate.ts +5 -5
  536. package/src/registry/db/schema.ts +232 -28
  537. package/src/registry/email/adapters/resend.ts +22 -0
  538. package/src/registry/email/adapters/smtp.ts +39 -0
  539. package/src/registry/email/define.ts +15 -0
  540. package/src/registry/email/resolve.ts +94 -0
  541. package/src/registry/email/types.ts +28 -0
  542. package/src/registry/path.ts +11 -0
  543. package/src/registry/publish/assets.ts +47 -37
  544. package/src/registry/publish/errors.ts +3 -3
  545. package/src/registry/publish/index.ts +286 -27
  546. package/src/registry/publish/package.ts +23 -1
  547. package/src/registry/publish/tokens.ts +35 -26
  548. package/src/registry/selection.ts +63 -0
  549. package/src/registry/transfer.ts +18 -0
  550. package/src/store/filesystem.ts +6 -6
  551. package/src/store/registry.ts +131 -15
  552. package/src/transformers/plugins/csf-render.ts +4 -2
  553. package/src/transformers/plugins/ssr-render.ts +66 -19
  554. package/src/transformers/preview/component/api.ts +13 -1
  555. package/src/transformers/preview/component/artifacts.ts +8 -4
  556. package/src/transformers/preview/component/builder.ts +36 -29
  557. package/src/transformers/preview/component/css.ts +107 -63
  558. package/src/transformers/preview/component/html.ts +36 -21
  559. package/src/transformers/preview/component/javascript.ts +15 -7
  560. package/src/transformers/preview/pattern/builder.ts +33 -20
  561. package/src/transformers/preview/pattern/html.ts +3 -1
  562. package/src/transformers/preview/types.ts +31 -3
  563. package/src/transformers/utils/build.ts +93 -0
  564. package/src/transformers/utils/module.ts +4 -2
  565. package/src/types/config.ts +231 -43
  566. package/src/utils/README.md +2 -1
  567. package/src/utils/markdown.ts +11 -0
  568. package/src/utils/menu-shell.ts +14 -7
  569. package/src/utils/pages.ts +52 -0
  570. package/src/utils/path.ts +15 -29
  571. package/templates/default/README.md.tpl +68 -1
  572. package/templates/default/components/button/button.react.handoff.js.tpl +19 -23
  573. package/templates/default/components/button/button.react.handoff.ts.tpl +22 -22
  574. package/templates/default/env.tpl +10 -0
  575. package/templates/default/gitignore.tpl +26 -3
  576. package/templates/default/handoff.config.js.tpl +27 -164
  577. package/templates/default/handoff.config.ts.tpl +27 -148
  578. package/templates/default/handoff.config.with-examples.js.tpl +28 -162
  579. package/templates/default/handoff.config.with-examples.ts.tpl +28 -146
  580. package/templates/default/package.json.tpl +8 -3
  581. package/templates/default/vercel.json.tpl +4 -0
  582. package/config/docs/foundations/logos.md +0 -7
  583. package/config/templates/component/template.json +0 -49
  584. package/config/templates/template.html +0 -7
  585. package/dist/commands/make/template.d.ts +0 -8
  586. package/dist/commands/make/template.js +0 -47
  587. package/dist/config/normalizers/declaration.d.ts +0 -11
  588. package/dist/config/normalizers/declaration.js +0 -122
  589. package/dist/config/normalizers/pattern.d.ts +0 -8
  590. package/dist/config/normalizers/pattern.js +0 -41
  591. package/dist/config/validator.d.ts +0 -9
  592. package/dist/config/validator.js +0 -26
  593. package/dist/declarations/helpers.d.ts +0 -6
  594. package/dist/declarations/helpers.js +0 -19
  595. package/dist/declarations/index.d.ts +0 -2
  596. package/dist/declarations/index.js +0 -9
  597. package/dist/declarations/types.d.ts +0 -60
  598. package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +0 -17
  599. package/dist/registry/asset-storage/adapters/vercel-blob.js +0 -87
  600. package/dist/registry/db/adapter.d.ts +0 -43
  601. package/dist/registry/db/adapter.js +0 -59
  602. package/src/app/lib/api/redact.ts +0 -46
  603. package/src/commands/make/template.ts +0 -47
  604. package/src/config/normalizers/declaration.ts +0 -179
  605. package/src/config/normalizers/pattern.ts +0 -59
  606. package/src/config/validator.ts +0 -24
  607. package/src/declarations/helpers.ts +0 -36
  608. package/src/declarations/index.ts +0 -19
  609. package/src/declarations/types.ts +0 -70
  610. package/src/registry/asset-storage/adapters/vercel-blob.ts +0 -51
  611. package/src/registry/db/adapter.ts +0 -74
  612. /package/dist/{declarations → catalog}/types.js +0 -0
package/README.md CHANGED
@@ -7,434 +7,1134 @@
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 22 or newer; Node.js 24 LTS is recommended
28
+ - npm 10 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
+ ## Upgrade from version 1.x.x to version 2.x.x
57
+
58
+ See the [version 1.x.x to version 2.x.x migration guide](UPGRADE.md#version-1xx-to-version-2xx)
59
+ for breaking changes, migration steps, and upgrade verification.
60
+
61
+ ## Project layout
62
+
63
+ Every command runs against a working directory: the directory holding
64
+ `handoff.config.ts`. Generated output is written there.
65
+
66
+ ```text
67
+ <working directory>/
68
+ ├─ handoff.config.ts
69
+ ├─ .env
70
+ ├─ components/
71
+ ├─ patterns/
72
+ ├─ pages/
73
+ ├─ exported/ # fetched tokens and assets; commit this directory
74
+ ├─ public/api/ # generated docs API; gitignored
75
+ ├─ out/ # build output; gitignored
76
+ ├─ .vercel/ # `--package vercel` output; gitignored
77
+ └─ .handoff/ # local Handoff state; gitignored
78
+ ```
79
+
80
+ In a repository of its own, which is what `init` scaffolds, the working
81
+ directory is the repository root.
82
+
83
+ ## Adding Handoff to an existing application
84
+
85
+ Declarations sit beside the components they document, so the catalog points
86
+ into the application source:
87
+
88
+ ```ts
89
+ catalog: { include: ['src/components', 'src/blocks'] },
90
+ ```
91
+
92
+ **`handoff-app` must be a dependency of the package holding those
93
+ `*.handoff.ts` files.** They import `handoff-app/react`, which resolves from the
94
+ directory of the declaration, not from the directory of the config. Under npm a
95
+ missing dependency is usually hidden by hoisting. Under pnpm it is not: the
96
+ affected items are skipped with `Could not resolve "handoff-app/react"` and the
97
+ build still exits 0.
98
+
99
+ Two `tsconfig.json` files do different jobs:
100
+
101
+ - The config nearest the declarations type checks them. For files in `src/`
102
+ that is the one at the project root. An editor reads the nearest config only,
103
+ so a config in another directory that includes `src/` does not count, and
104
+ neither the editor nor the application build reports errors in a declaration.
105
+ - The config at the working directory generates the property tables. Without
106
+ it, Handoff warns `TypeScript config not found` and every table is empty.
107
+
108
+ Both files are needed when the working directory is not the project root.
109
+
110
+ Three placements work, all keeping one dependency tree so the catalog renders
111
+ the real components against the same React instance. A separate install with its
112
+ own `node_modules` outside that tree is not supported.
113
+
114
+ **Handoff in a subdirectory — recommended.** Nothing in the application changes.
115
+ `handoff-app` is a dependency of the application package, and
116
+ `HANDOFF_WORKING_PATH` keeps the generated directories in `handoff/`:
117
+
118
+ ```json
119
+ { "scripts": { "handoff:dev": "HANDOFF_WORKING_PATH=handoff handoff-app start" } }
120
+ ```
121
+
122
+ An inline variable does not work in an npm script on Windows, where `cross-env`
123
+ is used instead.
124
+
125
+ **Handoff in the project root.** The working directory is the project root, so
126
+ one `tsconfig.json` does both jobs. Handoff writes to `public/api`, so a bundler
127
+ already serving static files from `public/` must be pointed elsewhere:
128
+
129
+ ```ts
130
+ // vite.config.ts
131
+ export default defineConfig({
132
+ publicDir: 'static',
133
+ });
114
134
  ```
115
135
 
116
- The generated `package.json` wires up the common scripts:
136
+ Without that change the generated documentation API is copied into the
137
+ production build of the application. A Next.js application cannot use this
138
+ placement: `sitesOutputDirectory` moves `out/` away from its export directory,
139
+ but its `public` directory is a fixed convention with no setting to move it.
140
+
141
+ **Handoff as a workspace package.** For a repository that is already a
142
+ workspace. `handoff/` gets its own `package.json` and scripts, generated output
143
+ lands in `handoff/`, and the root forwards to it:
117
144
 
118
145
  ```json
119
146
  {
120
- "scripts": {
121
- "start": "handoff-app start",
122
- "dev": "handoff-app dev",
123
- "build": "handoff-app build",
124
- "fetch": "handoff-app fetch"
125
- }
147
+ "workspaces": ["handoff"],
148
+ "scripts": { "handoff:dev": "npm run dev --workspace handoff" }
126
149
  }
127
150
  ```
128
151
 
129
- Any other command can be run with `npx handoff-app <command>` or added as a
130
- script of your own.
152
+ The dependency rule is easiest to get wrong here: the declarations in `src/`
153
+ belong to the application package, not to the workspace, so both packages
154
+ declare `handoff-app`.
131
155
 
132
- ## Project layout
156
+ ### pnpm
133
157
 
134
- A typical workspace project looks like this:
158
+ `build` and `build --package vercel` work with the default pnpm layout.
159
+ `build --target registry` cannot follow pnpm symlinks when it traces its runtime
160
+ dependencies into a standalone bundle, so it needs a flat layout:
135
161
 
162
+ ```yaml
163
+ # pnpm-workspace.yaml
164
+ nodeLinker: hoisted
136
165
  ```
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)
166
+
167
+ ## Catalog items
168
+
169
+ Every documented UI entry is a catalog item. Each item declares either an implementation or a composition of other items.
170
+ An implementation names its renderer. `defineCatalogItem` comes from the module for that renderer.
171
+ The declaration supplies stable identity, documentation metadata, source entries, and previews.
172
+
173
+ ### React
174
+
175
+ Handoff finds the implementation file from its import, so you do not need to repeat the path, and
176
+ builds the item's CSS from the stylesheets that import chain pulls in, descendants included.
177
+ Previews are named exports. `Preview<typeof item>` gives them the implementation's argument type.
178
+
179
+ ```tsx
180
+ // components/example/Component.tsx
181
+ export type ComponentProps = {
182
+ label: string;
183
+ onSelect?: () => void;
184
+ };
185
+
186
+ export default function Component({ label, onSelect }: ComponentProps) {
187
+ return <div onClick={onSelect}>{label}</div>;
188
+ }
149
189
  ```
150
190
 
151
- ## Authoring components and patterns
191
+ ```tsx
192
+ // components/example/example.handoff.ts
193
+ import { defineCatalogItem, type Preview } from 'handoff-app/react';
194
+ import Component from './Component';
195
+
196
+ const item = defineCatalogItem({
197
+ id: 'component-id',
198
+ name: 'Component name',
199
+ description: 'Usage guidance for the component.',
200
+ group: 'Group name',
201
+ implementation: Component,
202
+ });
152
203
 
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.
204
+ export default item;
157
205
 
158
- Declarations are written with the typed helpers exported from `handoff-app`:
206
+ type ComponentPreview = Preview<typeof item>;
207
+
208
+ export const Default = {
209
+ args: { label: 'Example' },
210
+ } satisfies ComponentPreview;
211
+ ```
212
+
213
+ The export name is the preview name. Use `name` to set a different display title.
214
+
215
+ Arguments are serialized, so a preview that needs an icon, a callback, JSX children, or its own
216
+ state declares `render` instead. It renders as a component, so a capitalized name lets it use hooks, and the item keeps
217
+ documenting the implementation rather than a wrapper. Name a declaration that holds JSX
218
+ `example.handoff.tsx`.
219
+
220
+ ```tsx
221
+ export const Interactive = {
222
+ render: function Interactive() {
223
+ const [label, setLabel] = useState('Example');
224
+ return <Component label={label} onSelect={() => setLabel('Selected')} />;
225
+ },
226
+ } satisfies ComponentPreview;
227
+ ```
228
+
229
+ ### Handlebars
230
+
231
+ The implementation is the path to the template. Arguments are `Record<string, unknown>` by default;
232
+ an explicit argument type is supplied through `defineCatalogItem<BadgeArgs>`.
159
233
 
160
234
  ```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',
235
+ // components/badge/badge.handoff.ts
236
+ import { defineCatalogItem, type Preview } from 'handoff-app/handlebars';
237
+
238
+ const item = defineCatalogItem({
239
+ id: 'badge',
240
+ name: 'Badge',
241
+ implementation: './Badge.hbs',
242
+ });
243
+
244
+ export default item;
245
+
246
+ type BadgePreview = Preview<typeof item>;
247
+
248
+ export const Primary = {
249
+ args: { variant: 'primary', children: 'New' },
250
+ } satisfies BadgePreview;
251
+ ```
252
+
253
+ ### CSF
254
+
255
+ 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`.
256
+ The CSF file stays unchanged and defines its previews through named exports. Each story's `args` merge with and override `meta.args`.
257
+ Handoff preserves story names, `argTypes`, and `render` functions.
258
+ It finds the React component through `meta.component`, then documents and publishes it with the story file.
259
+
260
+ ```ts
261
+ // components/card/card.handoff.ts
262
+ import { defineCatalogItem, fromCSF } from 'handoff-app/react';
263
+
264
+ export default defineCatalogItem({
265
+ id: 'card',
266
+ name: 'Card',
267
+ implementation: fromCSF('./Card.stories.tsx'),
268
+ });
269
+ ```
270
+
271
+ `Meta` and `StoryObj` are exported from `handoff-app/react`, so a story file type-checks without a
272
+ Storybook dependency. A project that already has Storybook keeps using its own types.
273
+
274
+ ```tsx
275
+ // components/card/Card.stories.tsx
276
+ import { type Meta, type StoryObj } from 'handoff-app/react';
277
+ import Card from './Card';
278
+
279
+ const meta = { component: Card } satisfies Meta<typeof Card>;
280
+ export default meta;
281
+
282
+ export const Primary: StoryObj<typeof meta> = {
283
+ args: { title: 'Example' },
284
+ };
285
+ ```
286
+
287
+ A CSF preview is rendered on the server and is not hydrated. A direct React implementation is
288
+ hydrated in the browser.
289
+
290
+ ### Compositions
291
+
292
+ A composition references other items by stable id. A named preview can be selected and its arguments
293
+ can be overridden by each reference. A composition needs no renderer, so it is declared with
294
+ `defineCatalogItem` from `handoff-app/pattern`.
295
+
296
+ ```ts
297
+ // patterns/example/example.handoff.ts
298
+ import { defineCatalogItem } from 'handoff-app/pattern';
299
+
300
+ export default defineCatalogItem({
301
+ id: 'pattern-id',
302
+ name: 'Pattern name',
303
+ description: 'Purpose and usage of the composition.',
304
+ group: 'Group name',
305
+ composition: [
306
+ { ref: 'component-id', preview: 'Default' },
307
+ { ref: 'badge', args: { children: 'Send' } },
308
+ ],
309
+ });
310
+ ```
311
+
312
+ An item declares `implementation` or `composition`, never both. Terms such as atom, element, and
313
+ block stay optional classification metadata on `type` and `categories`.
314
+
315
+ The `id` addresses the item everywhere: its artifacts, its documentation URL, and its publish and
316
+ checkout commands. Components and patterns share one namespace, so each id must be unique across
317
+ both.
318
+
319
+ If two declarations claim one id, Handoff keeps the first and skips the second. The warning names
320
+ both files. `publish` refuses to run until each id is unique.
321
+ ### Registration
322
+
323
+ Catalog directories are registered in one place. A path is either an item directory or a collection
324
+ directory whose subdirectories are each treated as an item.
325
+
326
+ ```ts
327
+ // handoff.config.ts
328
+ import { defineConfig } from 'handoff-app';
329
+
330
+ export default defineConfig({
331
+ catalog: {
332
+ include: ['components', 'patterns'],
171
333
  },
172
- previews: {
173
- primary: {
174
- title: 'Primary',
175
- args: { type: 'primary', children: 'Click me!' },
334
+ runtime: {
335
+ workspace: {
336
+ declarationFormat: 'ts',
176
337
  },
177
338
  },
178
339
  });
179
340
  ```
180
341
 
181
- The available helpers are:
342
+ ### Pages
182
343
 
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`
344
+ Custom documentation pages are Markdown files under `pages/`. Their relative
345
+ paths become their routes and registry IDs.
188
346
 
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.
347
+ ## Figma foundations
193
348
 
194
- ## Fetching design tokens
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.
195
352
 
196
- Handoff fetches tokens from Figma out of the box. Set your Figma credentials in
197
- `.env`:
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
+ These are the defaults of `integrations.figma` in `handoff.config`. Set the block
362
+ only to use other variable names, to commit the file ID, or to change the file
363
+ for a profile. The access token accepts only an environment reference:
364
+
365
+ ```ts
366
+ integrations: {
367
+ figma: {
368
+ projectId: fromEnv("HANDOFF_FIGMA_PROJECT_ID", { default: null }),
369
+ accessToken: fromEnv("HANDOFF_DEV_ACCESS_TOKEN", { default: null }),
370
+ },
371
+ },
372
+ ```
373
+
374
+ If a value is empty, `fetch` asks for it in a terminal and can save it to `.env`,
375
+ or to `.env.<profile>` when a profile is selected. Without a terminal, such as
376
+ in CI, `fetch` stops with an error that names the missing variable.
377
+
378
+ The `file_content:read` and `library_content:read` scopes must be granted to the
379
+ personal access token. The configured library is fetched with:
205
380
 
206
381
  ```bash
207
382
  npm run fetch
208
383
  ```
209
384
 
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.
385
+ After design changes, the Figma library should be republished and the command
386
+ should be run again so the latest foundations are pulled.
214
387
 
215
- ## Running locally
388
+ Generated tokens, CSS, Sass, and available assets are written below
389
+ `exported/`. The `exported/` directory should be committed so the
390
+ same inputs are used by local and CI builds. A successful fetch can be verified
391
+ by checking for generated token data such as `exported/tokens.json`;
392
+ an empty foundation route does not prove that the fetch succeeded.
216
393
 
217
- Both commands run a workspace-first local server with live preview.
394
+ ## Workspace
395
+
396
+ Components, patterns, pages, styles, and configuration are authored as local
397
+ workspace files. For day-to-day authoring, the documentation server and file
398
+ watchers are started with:
218
399
 
219
400
  ```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
401
+ npm run start
402
+ ```
403
+
404
+ Changes are rebuilt and reflected in the documentation while the command is
405
+ running. When Handoff file watchers are not required, the development server
406
+ can be started with `npm run dev` instead.
407
+
408
+ The following should be visible at http://localhost:3000:
409
+
410
+ - the app reports `Workspace` as the active runtime mode;
411
+ - components appear in the component list;
412
+ - component detail pages render every configured preview;
413
+ - patterns render their referenced components; and
414
+ - foundation pages contain the fetched values.
415
+
416
+ ## Build outputs
417
+
418
+ A static documentation site is built with:
419
+
420
+ ```bash
421
+ npm run build
422
+ ```
423
+
424
+ Static output is written below `out/static` and can be served by any
425
+ static file server or CDN.
426
+
427
+ Components whose files have not changed since the last build are not built
428
+ again. The build cache in `.handoff/.cache/` records the files of each
429
+ component and every file that its last build read, Sass partials included.
430
+ Files under `node_modules` are covered by the lockfile instead. Assets that a
431
+ stylesheet loads through `url()` are not tracked. Every component is built
432
+ again with:
433
+
434
+ ```bash
435
+ npm run build -- --force
436
+ ```
437
+
438
+ A standalone registry application is built with:
439
+
440
+ ```bash
441
+ npm run build -- --target registry
442
+ ```
443
+
444
+ The generated application is automatically configured for registry mode by the
445
+ command. The database-backed Next.js bundle is written to `out/registry`, and
446
+ workspace source is not compiled or served. A Vercel Build Output bundle can be
447
+ produced for either target by adding `--package vercel`.
448
+
449
+ ## Configuration
450
+
451
+ Configuration is read from `handoff.config.ts`, `.js`, `.cjs`, or `.json`, in
452
+ that order. `defineConfig` provides typed authoring. Values merge onto the
453
+ defaults: plain objects merge recursively, and arrays, scalars, `null`, and
454
+ functions replace.
455
+
456
+ Useful environment variables:
457
+
458
+ | Variable | Purpose |
459
+ | --- | --- |
460
+ | `HANDOFF_PROFILE` | Config profile merged onto the base config |
461
+ | `HANDOFF_FIGMA_PROJECT_ID` | Figma file ID used by `fetch` |
462
+ | `HANDOFF_DEV_ACCESS_TOKEN` | Figma personal access token used by `fetch` |
463
+ | `HANDOFF_REGISTRY_URL` | Connected workspace registry URL |
464
+ | `HANDOFF_REGISTRY_ACCESS_TOKEN` | Registry access token used by a connected workspace |
465
+ | `HANDOFF_SYNC_SECRET` | Optional deployment-wide registry credential |
466
+ | `DATABASE_URL` | Registry PostgreSQL connection string |
467
+ | `AUTH_SECRET` | Registry session-signing secret, at least 32 characters |
468
+ | `AUTH_URL` | Canonical public registry URL |
469
+ | `PORT` | Standalone registry server port |
470
+ | `HOSTNAME` | Standalone registry bind hostname |
471
+ | `HANDOFF_AI_KEY_SECRET` | Encrypts reader-supplied AI provider keys, at least 32 characters |
472
+ | `HANDOFF_AI_CONNECTIONS` | JSON array of AI connections, merged over the baked list by `id` |
473
+ | `HANDOFF_OUTPUT_DIR` | Override the fetched output directory |
474
+ | `HANDOFF_CREATE_ASSETS_ZIP_FILES` | Set to `false` to skip the icon and logo zip files in `fetch` |
475
+ | `HANDOFF_SITES_DIR` | Override the build output directory |
476
+ | `HANDOFF_WORKING_PATH` | Directory holding `handoff.config.ts`; defaults to the current directory |
477
+ | `HANDOFF_APP_PORT` | Workspace documentation server port |
478
+ | `HANDOFF_WEBSOCKET_PORT` | Workspace live-reload server port |
479
+
480
+ ## Profiles
481
+
482
+ A profile is a sidecar config file that merges onto the base config. It holds
483
+ only what changes between environments, so a project keeps one shared config
484
+ instead of a second complete copy of it.
485
+
486
+ - The base config is `handoff.config.ts`, `.js`, `.cjs`, or `.json`. It is the
487
+ only config file loaded when no profile is selected.
488
+ - A profile file is `handoff.config.<profile>.*`, with the same four
489
+ extensions.
490
+ - Select a profile with `--profile <name>` or with `HANDOFF_PROFILE`.
491
+ `--profile` takes precedence over `HANDOFF_PROFILE`.
492
+ - Profile names are not predefined. A name can contain lowercase letters,
493
+ numbers, and hyphens.
494
+ - A selected profile must exist. If no `handoff.config.<name>.*` file is found,
495
+ the command stops with an error.
496
+ - `login`, `logout`, `publish`, and `checkout` also accept a profile that has
497
+ only a saved registry login, because there the profile selects the registry.
498
+ Every other command still needs the config file.
499
+ - Layers resolve as defaults, base config, profile, then programmatic config,
500
+ with the merge rules described under [Configuration](#configuration).
501
+ - A selected profile also reads `.env.<profile>` on top of `.env`, when that
502
+ file exists. Both are read from the directory the command runs in, and a
503
+ variable already set in the environment beats both files.
504
+
505
+ ```bash
506
+ npx handoff-app build --target registry --profile registry
507
+ ```
508
+
509
+ `build --target registry` always packages a registry-mode application, so a
510
+ profile does not need to set `runtime.mode`. This makes a profile a good place
511
+ for the registry build settings: the database driver, the database
512
+ environment-variable name, the asset storage, and whether MCP is enabled. A
513
+ hosting provider or a CI job selects the profile through `HANDOFF_PROFILE`.
514
+
515
+ `defineConfig` types a profile as well as a base config. The generated
516
+ `.gitignore` lists `handoff.config.local.*`, which makes `local` the usual name
517
+ for a profile that stays on one machine.
518
+
519
+ Arrays replace, so a profile that declares `catalog.include` also decides what
520
+ `make` and `checkout` register.
521
+
522
+ `.env.<profile>` is read once at startup rather than watched.
523
+ `HANDOFF_WORKING_PATH` cannot be set from it, because the working path is
524
+ resolved before any profile is known.
525
+
526
+ ## Registry setup
527
+
528
+ ### 1. Database migrations
529
+
530
+ Migrations are run from the source workspace or CI checkout, where the project
531
+ dependency and configuration are available:
532
+
533
+ ```bash
534
+ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/handoff?schema=public" \
535
+ npm run db:migrate
536
+ ```
537
+
538
+ Migrations are a controlled release step and are separate from the build,
539
+ server startup, and browser installer.
540
+
541
+ ### 2. Registry runtime
542
+
543
+ The generated registry bundle uses these environment variables:
544
+
545
+ ```dotenv
546
+ PORT=4000
547
+ HOSTNAME=0.0.0.0
548
+ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/handoff?schema=public"
549
+ AUTH_SECRET="replace-with-at-least-32-random-characters"
550
+ AUTH_URL="http://localhost:4000"
222
551
  ```
223
552
 
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.
553
+ `DATABASE_URL`, `AUTH_SECRET`, and `AUTH_URL` are required. The standalone
554
+ server also reads `PORT` and `HOSTNAME`. The generated entrypoint is
555
+ `out/registry/server.js`; how it is started and hosted depends on the deployment
556
+ environment.
557
+
558
+ The defaults work without more configuration: asset blobs are stored in
559
+ PostgreSQL, and email is not sent. Other providers are baked into the build, so
560
+ they must be selected under [Registry providers](#registry-providers) before
561
+ the registry is built.
562
+
563
+ ### 3. Installation
564
+
565
+ The installer is opened at http://localhost:4000/install, where the first
566
+ administrator is created. The deployment is verified by the installer, but
567
+ migrations are never run by it.
568
+
569
+ An uninstalled registry can be claimed by the first visitor. A new deployment
570
+ should not be left unattended before installation is complete.
571
+
572
+ ### 4. Workspace authorization
226
573
 
227
- ## Building artifacts
574
+ A workspace reaches a registry with a registry URL and an access token. There
575
+ are two ways to supply them. Both use the same kind of token, and both are
576
+ listed and revoked under Account → Access tokens in the registry.
228
577
 
229
- `handoff-app build` accepts a `--target` flag. The target, not `NODE_ENV`,
230
- decides what gets produced.
578
+ #### Device login
231
579
 
232
- ### Static site (default)
580
+ CLI authorization is started from the source workspace with:
233
581
 
234
582
  ```bash
235
- handoff-app build # static target (default)
236
- handoff-app build --target static # explicit, identical output
583
+ npm run login -- --url http://localhost:4000
237
584
  ```
238
585
 
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.
586
+ Registry sign-in, entry of the displayed device code, and CLI approval are
587
+ completed in the browser. The issued credential is saved in
588
+ `.handoff/cli-auth.json` for that exact registry URL.
246
589
 
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).
590
+ One credential is saved per profile, so a workspace can stay signed in to
591
+ several registries at the same time:
592
+
593
+ ```bash
594
+ npm run login -- --profile staging --url https://staging.example.com
595
+ npm run publish -- all --profile staging
596
+ ```
597
+
598
+ A login profile name is free. It needs no `handoff.config.<name>.*` file, and
599
+ `login` is the only command that accepts a new name. A profile without its own
600
+ login falls back to the default login, so one `npm run login` is still enough
601
+ for a workspace with one registry.
602
+
603
+ `npm run logout` revokes and removes the login of the selected profile. The
604
+ `--all` option removes every saved login.
605
+
606
+ #### Environment variables
607
+
608
+ A token is created in the registry under Account → Access tokens. Read and
609
+ write access is required to publish; read access is enough to check out. The
610
+ token and the registry URL are then set in `.env`:
611
+
612
+ ```dotenv
613
+ HANDOFF_REGISTRY_URL=http://localhost:4000
614
+ HANDOFF_REGISTRY_ACCESS_TOKEN=hnd_...
615
+ ```
250
616
 
251
- ### Registry app
617
+ No configuration file entry is needed, because these two variables are the
618
+ defaults for `runtime.registryConnection`. A selected profile reads
619
+ `.env.<profile>` on top of `.env`, so one workspace can address a different
620
+ registry per profile. In CI, the job environment supplies the same two
621
+ variables instead of a file.
622
+
623
+ Environment values win over a saved device login, so a CI job stays
624
+ deterministic on a machine where a developer is signed in. A token is only used
625
+ for the registry URL it was issued for. If the environment names a different
626
+ registry than the login of the selected profile, publish and checkout report
627
+ which login was skipped and why.
628
+
629
+ ### 5. Content publishing
630
+
631
+ Every kind in the workspace is published in dependency order with:
252
632
 
253
633
  ```bash
254
- handoff-app build --target registry
634
+ npm run publish -- all
255
635
  ```
256
636
 
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.
637
+ A single kind is published on its own:
262
638
 
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:
639
+ ```bash
640
+ npm run publish -- catalog
641
+ npm run publish -- pages
642
+ npm run publish -- tokens
643
+ npm run publish -- assets
644
+ ```
645
+
646
+ `catalog` covers every catalog item. An item is stored as a component or as a
647
+ pattern, and its declaration decides which. The command names items and never a
648
+ kind.
649
+
650
+ Publishing tokens or assets runs the Figma data pipeline before upload, so the
651
+ documented Figma credentials must be available.
652
+
653
+ One or more IDs can be appended to narrow a publish to those entities:
266
654
 
267
655
  ```bash
268
- DATABASE_URL="postgres://…" node out/registry/server.js
656
+ npm run publish -- catalog item-id another-id
269
657
  ```
270
658
 
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:
659
+ `--dry-run` reports what would be uploaded and contacts no registry at all, so
660
+ it needs neither a registry URL nor a token. It still runs the build, which
661
+ refreshes generated output on disk; `--no-build` skips the build and publishes
662
+ the existing output, and the two combine to leave the workspace untouched:
274
663
 
275
664
  ```bash
276
- handoff-app db:migrate
665
+ npm run publish -- all --dry-run
666
+ npm run publish -- catalog --no-build
277
667
  ```
278
668
 
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.
669
+ `checkout` takes the same `all`, multi-ID, and `--dry-run` forms. A dry-run
670
+ checkout reads from the registry, lists the files it would create or overwrite,
671
+ and writes nothing.
283
672
 
284
- ## Publishing and checkout
673
+ After the registry is reloaded, the published catalog items and foundations are
674
+ visible. Published database records are read by registry pages; the local
675
+ workspace is never read directly.
285
676
 
286
- Connect a workspace to a registry by adding a `registryConnection` block to your
287
- config and setting the access token env var:
677
+ For CI, supply `HANDOFF_REGISTRY_URL` and `HANDOFF_REGISTRY_ACCESS_TOKEN`
678
+ through the job environment, as described under
679
+ [Workspace authorization](#4-workspace-authorization). A connection block in
680
+ `handoff.config.ts` is needed only to pin the URL in the repository, or to read
681
+ the values from differently named variables (`fromEnv` is imported from
682
+ `handoff-app`):
288
683
 
289
684
  ```ts
290
685
  runtime: {
291
- mode: 'workspace',
292
686
  registryConnection: {
293
687
  url: 'https://registry.example.com',
294
- accessTokenEnv: 'HANDOFF_REGISTRY_ACCESS_TOKEN',
688
+ accessToken: fromEnv('HANDOFF_REGISTRY_ACCESS_TOKEN'),
295
689
  },
296
690
  },
297
691
  ```
298
692
 
693
+ ## Registry providers
694
+
695
+ ### Vercel and Neon
696
+
697
+ Vercel packaging and the Neon PostgreSQL driver are supported independently.
698
+ Either integration can be used without the other.
699
+
700
+ #### Vercel packaging
701
+
702
+ A registry bundle that follows the Vercel Build Output API is produced with:
703
+
299
704
  ```bash
300
- # .env
301
- HANDOFF_REGISTRY_ACCESS_TOKEN=your-registry-token
705
+ npm run build -- --target registry --package vercel
302
706
  ```
303
707
 
304
- Then push and pull entities by `id`:
708
+ The generated deployment output is written to `.vercel/output`. The registry
709
+ runtime variables listed above are supplied through the deployment environment.
305
710
 
306
- ```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
711
+ #### Neon PostgreSQL
712
+
713
+ Import `fromEnv` from `handoff-app`. Select the Neon connection driver in `handoff.config.ts`:
714
+
715
+ ```ts
716
+ runtime: {
717
+ registry: {
718
+ database: {
719
+ url: fromEnv('DATABASE_URL'),
720
+ driver: 'neon',
721
+ },
722
+ },
723
+ },
309
724
  ```
310
725
 
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.
726
+ A Neon PostgreSQL connection string is supplied through `DATABASE_URL`. The
727
+ same explicit database migration step is required before deployment.
318
728
 
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.
729
+ ### Asset storage
325
730
 
326
- ## Configuration
731
+ The registry stores asset blobs in PostgreSQL by default, with a limit of 4 MB
732
+ for each blob. `maxInlineBytes` changes this limit.
733
+
734
+ #### Custom adapter
735
+
736
+ A [custom server module](#custom-server-modules) can store blobs in a
737
+ different service. The module default-exports a `defineAssetStorage()` adapter
738
+ with these methods:
739
+
740
+ - `put` stores a blob and returns the `storageRef` that the registry records.
741
+ - `get` returns a blob by its `storageRef`, as `{ kind: 'stream', stream }`,
742
+ `{ kind: 'bytes', bytes }`, or `{ kind: 'redirect', url }`.
743
+ - `delete` removes a blob by its `storageRef`.
744
+ - `createUpload` is optional. It enables [direct uploads](#direct-uploads).
327
745
 
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.
746
+ Literals in `options` are baked into the build. The deployed registry reads
747
+ each `fromEnv()` value at request time and gives the resolved `options` to the
748
+ module. Thus secrets must use `fromEnv()`.
749
+
750
+ The S3 and Vercel Blob examples that follow are complete adapters with direct
751
+ uploads.
752
+
753
+ ##### Direct uploads
754
+
755
+ By default, `publish` sends each blob to the registry, and the registry sends
756
+ it to storage. A serverless host limits the size of one request. Vercel permits
757
+ about 4.5 MB, so a larger blob fails.
758
+
759
+ With `createUpload`, the registry gives the CLI a signed URL, and the CLI sends
760
+ the blob directly to storage. The signed URL must make storage reject bytes
761
+ that do not match `hash`. The registry records the blob only after `get` finds
762
+ the stored object. The registry does not delete an uploaded object that it
763
+ never records.
764
+
765
+ The `database` provider always uploads through the registry, with its
766
+ `maxInlineBytes` limit.
767
+
768
+ ##### S3-compatible storage
769
+
770
+ This adapter works with Amazon S3, Cloudflare R2, MinIO, SeaweedFS, and other
771
+ services with an S3 API. It uses `@aws-sdk/client-s3` and
772
+ `@aws-sdk/s3-request-presigner`.
332
773
 
333
774
  ```ts
334
- import { defineConfig } from 'handoff-app';
775
+ runtime: {
776
+ registry: {
777
+ assetStorage: {
778
+ adapter: 'custom',
779
+ module: './server/storage/s3.mjs',
780
+ options: {
781
+ endpoint: 'http://127.0.0.1:8333',
782
+ bucket: 'handoff-assets',
783
+ accessKey: fromEnv('S3_ACCESS_KEY'),
784
+ secretKey: fromEnv('S3_SECRET_KEY'),
785
+ },
786
+ },
787
+ },
788
+ },
789
+ ```
335
790
 
336
- export default defineConfig({
337
- app: {
338
- title: 'Acme Design System',
339
- client: 'Acme Corp',
340
- basePath: '',
791
+ ```js
792
+ // server/storage/s3.mjs
793
+ import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
794
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
795
+ import { defineAssetStorage } from 'handoff-app/define';
796
+
797
+ export default defineAssetStorage(({ options }) => {
798
+ const client = new S3Client({
799
+ endpoint: options.endpoint,
800
+ region: 'us-east-1',
801
+ forcePathStyle: true,
802
+ credentials: { accessKeyId: options.accessKey, secretAccessKey: options.secretKey },
803
+ });
804
+ const Bucket = options.bucket;
805
+ return {
806
+ async put({ hash, bytes, contentType }) {
807
+ await client.send(new PutObjectCommand({ Bucket, Key: hash, Body: bytes, ContentType: contentType }));
808
+ return { storageRef: hash };
809
+ },
810
+ async get(storageRef) {
811
+ const result = await client.send(new GetObjectCommand({ Bucket, Key: storageRef }));
812
+ return { kind: 'stream', stream: result.Body, contentType: result.ContentType };
813
+ },
814
+ async delete(storageRef) {
815
+ await client.send(new DeleteObjectCommand({ Bucket, Key: storageRef }));
816
+ },
817
+ async createUpload({ hash, contentType }) {
818
+ const checksum = Buffer.from(hash, 'hex').toString('base64');
819
+ const command = new PutObjectCommand({ Bucket, Key: hash, ContentType: contentType, ChecksumSHA256: checksum });
820
+ const url = await getSignedUrl(client, command, {
821
+ expiresIn: 900,
822
+ unhoistableHeaders: new Set(['x-amz-checksum-sha256']),
823
+ });
824
+ return { url, headers: { 'Content-Type': contentType, 'x-amz-checksum-sha256': checksum }, storageRef: hash };
825
+ },
826
+ };
827
+ });
828
+ ```
829
+
830
+ `createUpload` signs a SHA-256 checksum header. Thus storage rejects bytes
831
+ that do not match `hash`.
832
+
833
+ ##### Vercel Blob
834
+
835
+ This adapter stores blobs in a private Vercel Blob store. It uses
836
+ `@vercel/blob` 2.x. When the store is connected to the Vercel project, the SDK
837
+ reads the store credentials from the deployment environment. Thus the adapter
838
+ needs no `options`.
839
+
840
+ ```ts
841
+ runtime: {
842
+ registry: {
843
+ assetStorage: {
844
+ adapter: 'custom',
845
+ module: './server/storage/vercel-blob.mjs',
846
+ },
847
+ },
848
+ },
849
+ ```
850
+
851
+ ```js
852
+ // server/storage/vercel-blob.mjs
853
+ import { Readable } from 'node:stream';
854
+ import { del, get, issueSignedToken, parseStoreIdFromDelegationToken, presignUrl, put } from '@vercel/blob';
855
+ import { defineAssetStorage } from 'handoff-app/define';
856
+
857
+ const access = 'private';
858
+ const pathnameFor = (hash) => `assets/${hash}`;
859
+
860
+ export default defineAssetStorage({
861
+ async put({ hash, bytes, contentType }) {
862
+ const blob = await put(pathnameFor(hash), bytes, { access, contentType, addRandomSuffix: false, allowOverwrite: true });
863
+ return { storageRef: blob.pathname };
341
864
  },
342
- entries: {
343
- scss: './sass/main.scss',
344
- js: './js/main.js',
345
- components: ['components/button', 'components/badge'],
346
- patterns: ['patterns'],
865
+ async get(storageRef) {
866
+ const result = await get(storageRef, { access });
867
+ if (!result?.stream) {
868
+ throw new Error(`Blob "${storageRef}" does not exist.`);
869
+ }
870
+ return { kind: 'stream', stream: Readable.fromWeb(result.stream), contentType: result.blob.contentType };
871
+ },
872
+ async delete(storageRef) {
873
+ await del(storageRef);
874
+ },
875
+ async createUpload({ hash, size, contentType }) {
876
+ const pathname = pathnameFor(hash);
877
+ const token = await issueSignedToken({
878
+ pathname,
879
+ operations: ['put'],
880
+ validUntil: Date.now() + 15 * 60 * 1000,
881
+ allowedContentTypes: [contentType],
882
+ maximumSizeInBytes: size,
883
+ });
884
+ const { presignedUrl } = await presignUrl(token, { operation: 'put', pathname, access, allowOverwrite: true });
885
+ return {
886
+ url: presignedUrl,
887
+ headers: {
888
+ 'x-api-version': '12',
889
+ 'x-vercel-blob-store-id': parseStoreIdFromDelegationToken(token.delegationToken),
890
+ 'x-vercel-blob-access': access,
891
+ 'x-content-type': contentType,
892
+ },
893
+ storageRef: pathname,
894
+ };
347
895
  },
348
896
  });
349
897
  ```
350
898
 
351
- ### The runtime block
899
+ This adapter is different from the S3 example in these ways:
900
+
901
+ - `get` converts the stream. The SDK returns a web stream, but the registry
902
+ needs a Node stream.
903
+ - A private blob has no public URL. Thus the registry sends each blob to the
904
+ reader itself.
905
+ - **Vercel Blob cannot reject bytes that do not match `hash`.** The signed URL
906
+ limits only the path, the size, and the content type. Thus the registry does
907
+ not verify a blob that the CLI uploads directly.
908
+ - Without `createUpload`, each blob goes through the registry, which verifies
909
+ its hash. The Vercel limit of about 4.5 MB then applies to each blob.
910
+ - The CLI sends a plain `PUT` request. Thus `createUpload` returns the headers
911
+ that the SDK sends for a presigned upload. These headers are not a documented
912
+ API, so a new version of `@vercel/blob` can change them.
913
+
914
+ #### Changing the provider
915
+
916
+ The registry records on each blob whether PostgreSQL or the custom adapter
917
+ stores it. Blobs in PostgreSQL stay readable after you select a custom adapter.
918
+ A publish does not move blobs. Thus, if you change the custom adapter or its
919
+ storage, copy the stored objects first. The new adapter must find each object
920
+ by the `storageRef` that the previous adapter returned. After a change back to
921
+ `database`, the registry cannot read the blobs of the custom adapter.
352
922
 
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.
923
+ ### Email delivery
924
+
925
+ The registry sends invitation and password-reset emails when `email.from` is
926
+ set. Without it, an administrator sees each invitation link once and delivers
927
+ it manually, and password reset is not available.
928
+
929
+ Resend is the default provider. It reads its key from `RESEND_API_KEY`, unless
930
+ `options.apiKey` names a different variable:
355
931
 
356
932
  ```ts
357
933
  runtime: {
358
- // Sole determinant of runtime mode. Defaults to 'workspace'.
359
- mode: 'workspace', // 'workspace' | 'registry'
934
+ registry: {
935
+ email: { from: 'Handoff <no-reply@example.com>' },
936
+ },
937
+ },
938
+ ```
939
+
940
+ For an SMTP server, for example Amazon SES, SendGrid, or Microsoft 365:
360
941
 
361
- workspace: {
362
- // Format for newly generated / checkout-synthesized declarations.
363
- declarationFormat: 'ts', // 'ts' | 'js' | 'cjs' | 'json'
942
+ ```ts
943
+ email: {
944
+ from: 'Handoff <no-reply@example.com>',
945
+ provider: 'smtp',
946
+ options: {
947
+ host: fromEnv('SMTP_HOST'),
948
+ port: 465, // default, uses TLS. 587 uses STARTTLS.
949
+ user: fromEnv('SMTP_USER'),
950
+ password: fromEnv('SMTP_PASSWORD'),
364
951
  },
952
+ },
953
+ ```
365
954
 
366
- // Connect a workspace to a remote registry (publish/checkout).
367
- registryConnection: {
368
- url: 'https://registry.example.com',
369
- accessTokenEnv: 'HANDOFF_REGISTRY_ACCESS_TOKEN',
955
+ For a different service, set `provider: 'custom'` and set `module` to a
956
+ [custom server module](#custom-server-modules). The factory gets `options`
957
+ with `fromEnv()` values resolved:
958
+
959
+ ```ts
960
+ import { defineEmailProvider } from 'handoff-app/define';
961
+
962
+ export default defineEmailProvider(({ options }) => ({
963
+ async send({ from, to, subject, html, text }) {
964
+ // Deliver the message. Throw an error if it is not accepted.
370
965
  },
966
+ }));
967
+ ```
371
968
 
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'
969
+ Literals in `options` are baked into the build. `fromEnv()` values are read at
970
+ request time, so they can change without a rebuild. `apiKey` and `password`
971
+ must use `fromEnv()`. A profile can set a different sender or provider. When a
972
+ profile changes the provider, it replaces `options`.
973
+
974
+ ### Custom server modules
975
+
976
+ Asset storage, email, and AI connections can name a custom server module. Each
977
+ module obeys these rules:
978
+
979
+ - The path is relative to the working directory.
980
+ - The file is `.js` or `.mjs`.
981
+ - The default export is the object, or a factory that returns it.
982
+ - The module imports its define helper from `handoff-app/define`. The registry build stops when a module imports `handoff-app`.
983
+ - The packages that the module imports are installed in the project. The registry build copies them into the bundle.
984
+
985
+ ## MCP
986
+
987
+ The documentation app serves a Model Context Protocol endpoint at `/api/mcp/`, so
988
+ coding agents can look up the pages, components and tokens that already exist instead
989
+ of inventing markup and values. It is stateless Streamable HTTP over `POST`;
990
+ there is nothing to start and no extra port. Keep the trailing slash: the app
991
+ sets `trailingSlash: true`, so `/api/mcp` answers with a 308 redirect.
992
+
993
+ | Target | Endpoint | Credential |
994
+ | --- | --- | --- |
995
+ | Workspace (`dev` / `start`) | `http://localhost:3000/api/mcp/` | none, as with `/api/docs/*` |
996
+ | Registry (standalone or Vercel) | `<registry url>/api/mcp/` | an access token, read-only is enough |
997
+ | Static export | not available, a static site has no API routes | n/a |
998
+
999
+ Registry tokens come from `handoff-app login` or Account settings in the
1000
+ registry itself, and are the same credentials the rest of the registry API takes.
1001
+
1002
+ The endpoint is served unless a project turns it off:
1003
+
1004
+ ```ts
1005
+ runtime: {
1006
+ mcp: false,
1007
+ },
1008
+ ```
1009
+
1010
+ Like `runtime.mode`, this is config-only and baked at build time, so changing it
1011
+ in a deployed registry means a rebuild. A disabled build answers 404 on the route,
1012
+ drops the MCP SDK from the packaged bundle, and shows no connect affordance at all
1013
+ rather than one that fails.
1014
+
1015
+ The documentation app carries its own endpoint and a ready-made client config
1016
+ for Claude Code, Cursor and VS Code behind the plug icon in the header. By hand:
1017
+
1018
+ ```json
1019
+ {
1020
+ "mcpServers": {
1021
+ "handoff": {
1022
+ "type": "http",
1023
+ "url": "https://registry.example.com/api/mcp/",
1024
+ "headers": { "Authorization": "Bearer hnd_..." }
1025
+ }
1026
+ }
1027
+ }
1028
+ ```
1029
+
1030
+ ## AI assistant
1031
+
1032
+ The documentation app can answer questions about the design system. The
1033
+ assistant reads through the same MCP tools, so every answer comes from the
1034
+ catalog and links to the pages and components it read. Open it from the search
1035
+ control in the header, or with `⌘K`.
1036
+
1037
+ The deployment chooses the provider:
1038
+
1039
+ ```ts
1040
+ runtime: {
1041
+ ai: {
1042
+ connections: [
1043
+ // Service key: the deployment pays for every reader.
1044
+ {
1045
+ id: 'gateway',
1046
+ label: 'Acme LiteLLM',
1047
+ baseUrl: 'https://llm.acme.internal/v1',
1048
+ apiKey: fromEnv('LITELLM_API_KEY'),
1049
+ models: ['gpt-4o', 'claude-sonnet-4-5', 'grok-4'],
1050
+ },
1051
+ // No credential: the endpoint needs none.
1052
+ { id: 'local', label: 'Ollama', baseUrl: 'http://127.0.0.1:11434/v1', models: ['llama3.1'] },
1053
+ // User key: the config fixes the endpoint and models. Each reader adds their own key.
1054
+ {
1055
+ id: 'openai',
1056
+ label: 'OpenAI',
1057
+ baseUrl: 'https://api.openai.com/v1',
1058
+ credential: 'user',
1059
+ models: ['gpt-4o', 'o3'],
1060
+ },
1061
+ ],
1062
+ defaultModel: 'gateway/claude-sonnet-4-5',
377
1063
  },
378
1064
  },
379
1065
  ```
380
1066
 
381
- Secrets are always referenced by environment-variable name, never written as
382
- values, and are redacted from API responses.
1067
+ The `ai` block turns the assistant on, and `ai: { enabled: false }` turns it
1068
+ back off. Like `runtime.mcp`, that flag is baked at build time, so a build
1069
+ without it serves no chat route and shows no control.
383
1070
 
384
- ### Environment variables
1071
+ **The declared connections are the whole surface.** A reader can only add a key
1072
+ for a connection the config already declares, so the server never calls a URL a
1073
+ reader chose. Without a `credential: 'user'` connection, there is no
1074
+ reader-facing setting.
385
1075
 
386
- | Variable | Purpose |
387
- | --- | --- |
388
- | `HANDOFF_FIGMA_PROJECT_ID` | Figma file ID used by `fetch` |
389
- | `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 |
1076
+ Every connection speaks the OpenAI-compatible `/chat/completions` API. This does
1077
+ not limit which models a reader can use. Ollama serves an OpenAI-compatible API
1078
+ at `/v1`, and LiteLLM, OpenRouter, vLLM, LM Studio and Azure OpenAI each front
1079
+ Anthropic, Google and xAI models. A provider that fits nothing else names a
1080
+ [custom server module](#custom-server-modules) that default-exports
1081
+ `defineAiProvider()`, in place of `baseUrl`. The factory gets `options` with
1082
+ `fromEnv()` values resolved. Literals in `options` are baked into the build, so
1083
+ secrets must use `fromEnv()`.
1084
+
1085
+ ### Reader keys
1086
+
1087
+ A connection marked `credential: 'user'` asks each reader for their own key,
1088
+ under Account → AI providers. Keys are encrypted with `HANDOFF_AI_KEY_SECRET`,
1089
+ not hashed, because the server must send them to the provider. Set that variable
1090
+ to a random value of at least 32 characters. A key is write-only across the API:
1091
+ after a save, a read reports only that a key is in place.
1092
+
1093
+ This applies to registry mode only. A workspace has one user, whose config file
1094
+ and `.env` are their user layer, so a workspace supplies a key through `apiKey`.
394
1095
 
395
- ### Hooks
1096
+ ### Changing connections without a rebuild
1097
+
1098
+ The connection list is deployment data, not build shape, so a registry can
1099
+ change it without a rebuild. `HANDOFF_AI_CONNECTIONS` holds a JSON array, read
1100
+ at request time and merged over the baked list by `id`. It names each key
1101
+ instead of holding its value:
1102
+
1103
+ ```json
1104
+ [{ "id": "gateway", "label": "Acme LiteLLM", "baseUrl": "https://llm.acme.internal/v1",
1105
+ "apiKeyEnv": "LITELLM_API_KEY", "models": ["gpt-4o"] }]
1106
+ ```
396
1107
 
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.
1108
+ A connection in this list can use a `module` only if a connection in the config
1109
+ names the same module. The build copies only those modules into the registry.
402
1110
 
403
- See [docs/api.md](docs/api.md#hooks) for hook arguments and examples.
1111
+ Spend control belongs to the gateway. LiteLLM and OpenRouter both enforce
1112
+ budgets and rate limits per key. The assistant caps only how many steps one
1113
+ question can take.
404
1114
 
405
1115
  ## CLI reference
406
1116
 
407
1117
  | Command | Description |
408
1118
  | --- | --- |
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/)
1119
+ | `npx handoff-app init` | A workspace is scaffolded with the interactive wizard |
1120
+ | `npm run fetch` | Design tokens and assets are fetched from Figma |
1121
+ | `npm run start` | The workspace server and Handoff file watchers are started |
1122
+ | `npm run dev` | The workspace development server is started |
1123
+ | `npm run build -- [--target static\|registry]` | The static site or standalone registry bundle is built |
1124
+ | `npm run db:migrate` | Registry database migrations are applied |
1125
+ | `npm run validate` | Configured components are validated |
1126
+ | `npm run publish -- <kind\|all> [id...]` | Catalog items, pages, tokens, or assets are published |
1127
+ | `npm run checkout -- <kind\|all> [id...]` | Published content is pulled into a workspace |
1128
+ | `npm run login -- [--profile <name>] --url <url>` | The CLI is authorized through the registry device flow |
1129
+ | `npm run logout -- [--profile <name>] [--all]` | Saved CLI credentials are revoked and removed |
1130
+
1131
+ Every command except `init` accepts `-c, --config`, `--profile`, `-d, --debug`,
1132
+ and `-f, --force`. `publish` and `checkout` additionally accept `--dry-run`, and
1133
+ `publish` accepts `--no-build`.
1134
+
1135
+ Arguments after `--` are forwarded to the local CLI. Exact options can be shown
1136
+ by adding `--help` after the separator. Advanced configuration and hooks are
1137
+ documented in [docs/api.md](docs/api.md).
438
1138
 
439
1139
  ## Maintainers
440
1140
 
@@ -446,8 +1146,8 @@ reference.
446
1146
 
447
1147
  ## Contributing
448
1148
 
449
- Feel free to dive in. [Open an issue](https://github.com/Convertiv/handoff-app/issues/new)
450
- or submit a PR.
1149
+ [Issues](https://github.com/Convertiv/handoff-app/issues/new) and pull requests
1150
+ are welcome.
451
1151
 
452
1152
  Handoff follows the [Contributor Covenant](http://contributor-covenant.org/version/1/3/0/)
453
1153
  Code of Conduct.