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.
- package/AGENTS.md +7 -0
- package/CLAUDE.md +7 -0
- package/README.md +1001 -301
- package/UPGRADE.md +266 -0
- package/config/config.template.js +1 -4
- package/config/config.template.ts +1 -2
- package/config/docs/foundations/colors/guidelines.md +9 -0
- package/config/docs/foundations/effects/guidelines.md +9 -0
- package/config/docs/foundations/effects/resources.md +9 -0
- package/config/docs/foundations/grid.md +10 -0
- package/config/docs/foundations/icons/guidelines.md +9 -0
- package/config/docs/foundations/images/guidelines.md +9 -0
- package/config/docs/foundations/images/resources.md +9 -0
- package/config/docs/foundations/images.md +10 -0
- package/config/docs/foundations/logo/resources.md +9 -0
- package/config/docs/foundations/typography/guidelines.md +9 -0
- package/config/docs/foundations/typography/resources.md +9 -0
- package/config/docs/foundations.md +5 -3
- package/dist/ai/connections.d.ts +66 -0
- package/dist/ai/connections.js +149 -0
- package/dist/ai/define.d.ts +10 -0
- package/dist/ai/define.js +13 -0
- package/dist/ai/types.d.ts +26 -0
- package/dist/ai/types.js +10 -0
- package/dist/app-builder/build.d.ts +14 -7
- package/dist/app-builder/build.js +308 -106
- package/dist/app-builder/client-config.js +18 -13
- package/dist/app-builder/config-diff/strategies/pattern.js +3 -3
- package/dist/app-builder/nav-shell.js +8 -15
- package/dist/app-builder/paths.d.ts +0 -12
- package/dist/app-builder/paths.js +9 -28
- package/dist/app-builder/static-export.js +6 -6
- package/dist/app-builder/vercel-output.d.ts +1 -1
- package/dist/app-builder/vercel-output.js +11 -10
- package/dist/app-builder/watchers/component.js +5 -5
- package/dist/app-builder/watchers/utils.d.ts +2 -3
- package/dist/app-builder/watchers.d.ts +8 -15
- package/dist/app-builder/watchers.js +70 -61
- package/dist/artifacts/url.d.ts +7 -7
- package/dist/artifacts/url.js +7 -7
- package/dist/cache/build-cache.d.ts +27 -4
- package/dist/cache/build-cache.js +132 -65
- package/dist/cache/build-inputs.d.ts +13 -0
- package/dist/cache/build-inputs.js +19 -0
- package/dist/cache/index.d.ts +2 -1
- package/dist/cache/index.js +5 -1
- package/dist/catalog/define.d.ts +6 -0
- package/dist/catalog/define.js +72 -0
- package/dist/catalog/implementation.d.ts +27 -0
- package/dist/catalog/implementation.js +133 -0
- package/dist/catalog/index.d.ts +6 -0
- package/dist/catalog/index.js +22 -0
- package/dist/catalog/normalize.d.ts +16 -0
- package/dist/catalog/normalize.js +118 -0
- package/dist/catalog/previews.d.ts +18 -0
- package/dist/catalog/previews.js +107 -0
- package/dist/catalog/renderers.d.ts +43 -0
- package/dist/catalog/renderers.js +62 -0
- package/dist/catalog/types.d.ts +71 -0
- package/dist/cli/auth/browser.d.ts +2 -0
- package/dist/cli/auth/browser.js +33 -0
- package/dist/cli/auth/device.d.ts +16 -0
- package/dist/cli/auth/device.js +158 -0
- package/dist/cli/auth/store.d.ts +38 -0
- package/dist/cli/auth/store.js +199 -0
- package/dist/cli/create.js +48 -3
- package/dist/cli/make.d.ts +0 -5
- package/dist/cli/make.js +24 -69
- package/dist/cli/scaffold.js +39 -181
- package/dist/cli.js +39 -7
- package/dist/commands/build/app.js +1 -5
- package/dist/commands/build/components.js +1 -5
- package/dist/commands/build/index.d.ts +1 -1
- package/dist/commands/build/index.js +2 -6
- package/dist/commands/checkout/index.d.ts +18 -12
- package/dist/commands/checkout/index.js +19 -59
- package/dist/commands/compat/deprecations.d.ts +46 -0
- package/dist/commands/compat/deprecations.js +62 -0
- package/dist/commands/compat/push-args.d.ts +48 -0
- package/dist/commands/compat/push-args.js +73 -0
- package/dist/commands/db/migrate.js +1 -5
- package/dist/commands/dev/index.js +1 -5
- package/dist/commands/eject/config.js +1 -5
- package/dist/commands/eject/pages.js +1 -5
- package/dist/commands/eject/theme.js +1 -5
- package/dist/commands/fetch/index.js +1 -5
- package/dist/commands/index.js +8 -2
- package/dist/commands/login/index.d.ts +13 -0
- package/dist/commands/login/index.js +61 -0
- package/dist/commands/logout/index.d.ts +12 -0
- package/dist/commands/logout/index.js +89 -0
- package/dist/commands/make/component.js +1 -5
- package/dist/commands/make/page.js +3 -7
- package/dist/commands/publish/index.d.ts +21 -11
- package/dist/commands/publish/index.js +22 -58
- package/dist/commands/pull/index.d.ts +13 -0
- package/dist/commands/pull/index.js +20 -0
- package/dist/commands/push/index.d.ts +23 -0
- package/dist/commands/push/index.js +58 -0
- package/dist/commands/scaffold/index.js +1 -5
- package/dist/commands/start/index.js +1 -5
- package/dist/commands/types.d.ts +12 -0
- package/dist/commands/utils.d.ts +100 -1
- package/dist/commands/utils.js +103 -2
- package/dist/commands/validate/components.js +1 -5
- package/dist/config/catalog-include.d.ts +43 -0
- package/dist/config/catalog-include.js +203 -0
- package/dist/config/defaults.d.ts +4 -2
- package/dist/config/defaults.js +61 -49
- package/dist/config/env.d.ts +9 -0
- package/dist/config/env.js +43 -0
- package/dist/config/errors.d.ts +4 -0
- package/dist/config/errors.js +11 -0
- package/dist/config/from-env.d.ts +28 -0
- package/dist/config/from-env.js +44 -0
- package/dist/config/helpers.d.ts +2 -0
- package/dist/config/helpers.js +4 -2
- package/dist/config/index.d.ts +6 -3
- package/dist/config/index.js +12 -4
- package/dist/config/loader.d.ts +40 -9
- package/dist/config/loader.js +174 -36
- package/dist/config/normalizers/page.d.ts +4 -4
- package/dist/config/normalizers/page.js +2 -2
- package/dist/config/resolve-env.d.ts +6 -0
- package/dist/config/resolve-env.js +118 -0
- package/dist/config/runtime.d.ts +15 -16
- package/dist/config/runtime.js +179 -197
- package/dist/define.d.ts +10 -0
- package/dist/define.js +13 -0
- package/dist/handlebars.d.ts +17 -0
- package/dist/handlebars.js +16 -0
- package/dist/index.d.ts +129 -33
- package/dist/index.js +256 -73
- package/dist/nav/resolver.js +3 -1
- package/dist/nav/workspace.js +2 -2
- package/dist/pattern.d.ts +7 -0
- package/dist/pattern.js +13 -0
- package/dist/pipeline/figma.d.ts +13 -2
- package/dist/pipeline/figma.js +86 -46
- package/dist/pipeline/index.d.ts +1 -1
- package/dist/pipeline/index.js +1 -1
- package/dist/pipeline/styles.js +1 -1
- package/dist/pipeline/validation.d.ts +1 -1
- package/dist/pipeline/validation.js +3 -3
- package/dist/react.d.ts +43 -0
- package/dist/react.js +11 -0
- package/dist/registry/asset-storage/define.d.ts +1 -1
- package/dist/registry/asset-storage/define.js +1 -1
- package/dist/registry/asset-storage/resolve.d.ts +9 -16
- package/dist/registry/asset-storage/resolve.js +33 -17
- package/dist/registry/asset-storage/types.d.ts +28 -8
- package/dist/registry/asset-storage/types.js +5 -5
- package/dist/registry/auth/access-tokens.d.ts +54 -0
- package/dist/registry/auth/access-tokens.js +191 -0
- package/dist/registry/auth/action-tokens.d.ts +66 -0
- package/dist/registry/auth/action-tokens.js +178 -0
- package/dist/registry/auth/ai-keys.d.ts +32 -0
- package/dist/registry/auth/ai-keys.js +69 -0
- package/dist/registry/auth/crypto.d.ts +21 -0
- package/dist/registry/auth/crypto.js +76 -0
- package/dist/registry/auth/database.d.ts +7 -0
- package/dist/registry/auth/database.js +22 -0
- package/dist/registry/auth/device-authorizations.d.ts +51 -0
- package/dist/registry/auth/device-authorizations.js +170 -0
- package/dist/registry/auth/gravatar.d.ts +1 -0
- package/dist/registry/auth/gravatar.js +8 -0
- package/dist/registry/auth/index.d.ts +13 -0
- package/dist/registry/auth/index.js +89 -0
- package/dist/registry/auth/installation.d.ts +42 -0
- package/dist/registry/auth/installation.js +105 -0
- package/dist/registry/auth/password.d.ts +12 -0
- package/dist/registry/auth/password.js +58 -0
- package/dist/registry/auth/rate-limits.d.ts +22 -0
- package/dist/registry/auth/rate-limits.js +75 -0
- package/dist/registry/auth/sync-secret.d.ts +27 -0
- package/dist/registry/auth/sync-secret.js +65 -0
- package/dist/registry/auth/types.d.ts +45 -0
- package/dist/registry/auth/types.js +11 -0
- package/dist/registry/auth/user-record.d.ts +13 -0
- package/dist/registry/auth/user-record.js +16 -0
- package/dist/registry/auth/users.d.ts +31 -0
- package/dist/registry/auth/users.js +135 -0
- package/dist/registry/auth/validation.d.ts +14 -0
- package/dist/registry/auth/validation.js +45 -0
- package/dist/registry/checkout/assets.d.ts +1 -1
- package/dist/registry/checkout/assets.js +25 -8
- package/dist/registry/checkout/index.d.ts +29 -7
- package/dist/registry/checkout/index.js +432 -154
- package/dist/registry/checkout/tokens.d.ts +1 -1
- package/dist/registry/checkout/tokens.js +16 -7
- package/dist/registry/client.d.ts +2 -1
- package/dist/registry/client.js +61 -17
- package/dist/registry/connection.d.ts +34 -11
- package/dist/registry/connection.js +77 -18
- package/dist/registry/content-kinds.d.ts +27 -0
- package/dist/registry/content-kinds.js +33 -0
- package/dist/registry/db/client.d.ts +14 -9
- package/dist/registry/db/client.js +22 -9
- package/dist/registry/db/driver.d.ts +39 -0
- package/dist/registry/db/driver.js +50 -0
- package/dist/registry/db/index.d.ts +2 -2
- package/dist/registry/db/index.js +14 -9
- package/dist/registry/db/migrate.d.ts +1 -1
- package/dist/registry/db/migrate.js +5 -5
- package/dist/registry/db/schema.d.ts +3035 -938
- package/dist/registry/db/schema.js +163 -9
- package/dist/registry/email/adapters/resend.d.ts +7 -0
- package/dist/registry/email/adapters/resend.js +35 -0
- package/dist/registry/email/adapters/smtp.d.ts +10 -0
- package/dist/registry/email/adapters/smtp.js +73 -0
- package/dist/registry/email/define.d.ts +10 -0
- package/dist/registry/email/define.js +13 -0
- package/dist/registry/email/resolve.d.ts +29 -0
- package/dist/registry/email/resolve.js +76 -0
- package/dist/registry/email/types.d.ts +24 -0
- package/dist/registry/email/types.js +6 -0
- package/dist/registry/path.d.ts +2 -0
- package/dist/registry/path.js +11 -1
- package/dist/registry/publish/assets.d.ts +2 -2
- package/dist/registry/publish/assets.js +46 -35
- package/dist/registry/publish/errors.js +3 -3
- package/dist/registry/publish/index.d.ts +45 -3
- package/dist/registry/publish/index.js +247 -24
- package/dist/registry/publish/package.js +22 -1
- package/dist/registry/publish/tokens.d.ts +2 -2
- package/dist/registry/publish/tokens.js +30 -24
- package/dist/registry/selection.d.ts +32 -0
- package/dist/registry/selection.js +58 -0
- package/dist/registry/transfer.d.ts +17 -0
- package/dist/store/filesystem.d.ts +3 -3
- package/dist/store/filesystem.js +7 -7
- package/dist/store/registry.d.ts +48 -0
- package/dist/store/registry.js +99 -15
- package/dist/transformers/plugins/csf-render.js +4 -2
- package/dist/transformers/plugins/ssr-render.js +52 -18
- package/dist/transformers/preview/component/api.d.ts +8 -0
- package/dist/transformers/preview/component/api.js +11 -15
- package/dist/transformers/preview/component/artifacts.d.ts +1 -1
- package/dist/transformers/preview/component/artifacts.js +11 -7
- package/dist/transformers/preview/component/builder.d.ts +1 -9
- package/dist/transformers/preview/component/builder.js +32 -35
- package/dist/transformers/preview/component/css.d.ts +5 -1
- package/dist/transformers/preview/component/css.js +100 -62
- package/dist/transformers/preview/component/html.d.ts +3 -1
- package/dist/transformers/preview/component/html.js +24 -18
- package/dist/transformers/preview/component/javascript.d.ts +2 -1
- package/dist/transformers/preview/component/javascript.js +12 -8
- package/dist/transformers/preview/pattern/builder.js +31 -18
- package/dist/transformers/preview/pattern/html.js +3 -1
- package/dist/transformers/preview/types.d.ts +30 -3
- package/dist/transformers/utils/build.d.ts +24 -0
- package/dist/transformers/utils/build.js +100 -1
- package/dist/transformers/utils/module.js +2 -1
- package/dist/types/config.d.ts +220 -43
- package/dist/utils/markdown.d.ts +10 -0
- package/dist/utils/markdown.js +17 -0
- package/dist/utils/menu-shell.js +14 -7
- package/dist/utils/pages.d.ts +22 -0
- package/dist/utils/pages.js +73 -0
- package/dist/utils/path.d.ts +6 -5
- package/dist/utils/path.js +15 -23
- package/docs/api.md +41 -7
- package/docs/cli.md +7 -3
- package/drizzle/0002_add_pages.sql +5 -3
- package/drizzle/0005_add_registry_auth.sql +95 -0
- package/drizzle/0006_add_component_source_format.sql +3 -0
- package/drizzle/0007_add_ai_keys.sql +11 -0
- package/drizzle/0008_add_source_dir.sql +2 -0
- package/drizzle/meta/0002_snapshot.json +17 -8
- package/drizzle/meta/0003_snapshot.json +17 -8
- package/drizzle/meta/0004_snapshot.json +17 -8
- package/drizzle/meta/0005_snapshot.json +1935 -0
- package/drizzle/meta/0006_snapshot.json +1941 -0
- package/drizzle/meta/0007_snapshot.json +2013 -0
- package/drizzle/meta/0008_snapshot.json +2025 -0
- package/drizzle/meta/_journal.json +28 -0
- package/drizzle.config.ts +1 -1
- package/package.json +57 -9
- package/src/ai/README.md +42 -0
- package/src/ai/connections.ts +189 -0
- package/src/ai/define.ts +15 -0
- package/src/ai/types.ts +30 -0
- package/src/app/components/AiAssistant/AiAssistantPanel.tsx +144 -0
- package/src/app/components/AiAssistant/AiAssistantProvider.tsx +215 -0
- package/src/app/components/AiAssistant/AiConversation.tsx +737 -0
- package/src/app/components/AiAssistant/AiToolResult.tsx +99 -0
- package/src/app/components/AiAssistant/dockState.ts +86 -0
- package/src/app/components/AiAssistant/toolLabel.ts +24 -0
- package/src/app/components/AiAssistant/useAiChat.ts +147 -0
- package/src/app/components/AiAssistant/useAiConnections.ts +86 -0
- package/src/app/components/AiAssistant/usePageContext.ts +63 -0
- package/src/app/components/Auth/AccountLayout.tsx +126 -0
- package/src/app/components/Auth/AuthControls.tsx +99 -0
- package/src/app/components/Auth/AuthShell.tsx +61 -0
- package/src/app/components/Auth/RegistryInstallGate.tsx +19 -0
- package/src/app/components/Auth/RegistrySessionProvider.tsx +15 -0
- package/src/app/components/Auth/api.ts +20 -0
- package/src/app/components/Auth/installStatus.ts +29 -0
- package/src/app/components/Component/PageSliceResolver.tsx +1 -0
- package/src/app/components/Component/Preview.tsx +406 -226
- package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +4 -0
- package/src/app/components/CopyToClipboard.tsx +27 -26
- package/src/app/components/Foundations/DisplayLogo.tsx +3 -3
- package/src/app/components/Layout/Header.tsx +25 -25
- package/src/app/components/Layout/RuntimeModeBadge.tsx +18 -0
- package/src/app/components/Markdown/CodeHighlight.tsx +73 -70
- package/src/app/components/Markdown/MarkdownComponents.tsx +30 -32
- package/src/app/components/McpIntegration/McpConfigDialog.tsx +349 -0
- package/src/app/components/McpIntegration/McpIcon.tsx +10 -0
- package/src/app/components/Navigation/MobileNav.tsx +7 -1
- package/src/app/components/Navigation/SideNav.tsx +1 -15
- package/src/app/components/context/NavProvider.tsx +7 -3
- package/src/app/components/context/PreviewContext.tsx +5 -5
- package/src/app/components/ui/attachment.tsx +50 -0
- package/src/app/components/ui/bubble.tsx +57 -0
- package/src/app/components/ui/button.tsx +3 -1
- package/src/app/components/ui/confirm-dialog.tsx +87 -0
- package/src/app/components/ui/dialog.tsx +78 -0
- package/src/app/components/ui/marker.tsx +56 -0
- package/src/app/components/ui/sidebar.tsx +3 -1
- package/src/app/components/util/index.ts +14 -38
- package/src/app/css/index.css +64 -0
- package/src/app/lib/ai/auth.ts +33 -0
- package/src/app/lib/ai/context.ts +99 -0
- package/src/app/lib/ai/model.ts +79 -0
- package/src/app/lib/ai/prompt.ts +36 -0
- package/src/app/lib/ai/resolve.ts +97 -0
- package/src/app/lib/ai/tools.ts +120 -0
- package/src/app/lib/api/methods.ts +13 -0
- package/src/app/lib/asset-storage.ts +23 -24
- package/src/app/lib/auth/api.ts +105 -0
- package/src/app/lib/auth/config.ts +175 -0
- package/src/app/lib/auth/email.ts +108 -0
- package/src/app/lib/docs-api/backend.ts +16 -1
- package/src/app/lib/docs-api/errors.ts +8 -7
- package/src/app/lib/docs-api/handler.ts +2 -3
- package/src/app/lib/docs-api/index.ts +2 -0
- package/src/app/lib/docs-api/page-discovery.ts +121 -0
- package/src/app/lib/docs-api/page-rendering.ts +37 -0
- package/src/app/lib/docs-api/page-search.ts +191 -0
- package/src/app/lib/docs-api/records.ts +31 -24
- package/src/app/lib/docs-api/registry.ts +92 -11
- package/src/app/lib/docs-api/runtime-config.ts +56 -39
- package/src/app/lib/docs-api/search.ts +120 -0
- package/src/app/lib/mcp/auth.ts +64 -0
- package/src/app/lib/mcp/components.ts +519 -0
- package/src/app/lib/mcp/index.ts +2 -0
- package/src/app/lib/mcp/page-url.ts +24 -0
- package/src/app/lib/mcp/pages.ts +77 -0
- package/src/app/lib/mcp/result.ts +33 -0
- package/src/app/lib/mcp/server.ts +40 -0
- package/src/app/lib/mcp/tokens.ts +328 -0
- package/src/app/lib/registry-api/allowlist.ts +7 -5
- package/src/app/lib/registry-api/asset-transfer.ts +170 -35
- package/src/app/lib/registry-api/auth.ts +108 -45
- package/src/app/lib/registry-api/entity-routes.ts +17 -7
- package/src/app/lib/registry-api/errors.ts +14 -6
- package/src/app/lib/registry-api/handler.ts +28 -19
- package/src/app/lib/registry-api/meta.ts +4 -0
- package/src/app/lib/registry-api/revalidate.ts +31 -12
- package/src/app/lib/registry-api/store.ts +89 -26
- package/src/app/lib/registry-api/sync-compat.ts +166 -0
- package/src/app/lib/registry-api/token-transfer.ts +45 -13
- package/src/app/lib/registry-api/transfer-batch.ts +116 -0
- package/src/app/lib/registry-api/transfer.ts +125 -45
- package/src/app/lib/registry-api/validation.ts +33 -1
- package/src/app/lib/registry-connection.ts +2 -2
- package/src/app/lib/server-module.ts +34 -0
- package/src/app/lib/utils.ts +13 -0
- package/src/app/next.config.mjs +47 -27
- package/src/app/pages/[...slug]/index.tsx +52 -51
- package/src/app/pages/_app.tsx +14 -4
- package/src/app/pages/_document.tsx +6 -0
- package/src/app/pages/account/ai.tsx +183 -0
- package/src/app/pages/account/index.tsx +153 -0
- package/src/app/pages/account/tokens.tsx +262 -0
- package/src/app/pages/account/users.tsx +370 -0
- package/src/app/pages/api/account/ai/keys/[connectionId].ts +60 -0
- package/src/app/pages/api/account/ai/keys/index.ts +20 -0
- package/src/app/pages/api/account/index.ts +26 -0
- package/src/app/pages/api/account/tokens/[id].ts +17 -0
- package/src/app/pages/api/account/tokens/index.ts +35 -0
- package/src/app/pages/api/admin/users/[id]/[action].ts +158 -0
- package/src/app/pages/api/admin/users/index.ts +53 -0
- package/src/app/pages/api/ai/chat.ts +115 -0
- package/src/app/pages/api/ai/connections.ts +23 -0
- package/src/app/pages/api/auth/[...nextauth].ts +4 -0
- package/src/app/pages/api/auth/request-reset.ts +51 -0
- package/src/app/pages/api/auth/reset-password.ts +29 -0
- package/src/app/pages/api/docs/search/pages.json.ts +25 -0
- package/src/app/pages/api/install.ts +102 -0
- package/src/app/pages/api/mcp.ts +56 -0
- package/src/app/pages/api/oauth/device/approve.ts +23 -0
- package/src/app/pages/api/oauth/device/index.ts +41 -0
- package/src/app/pages/api/oauth/revoke.ts +31 -0
- package/src/app/pages/api/oauth/token.ts +31 -0
- package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +1 -1
- package/src/app/pages/api/registry/components/[id].ts +2 -2
- package/src/app/pages/api/registry/components/index.ts +1 -1
- package/src/app/pages/api/registry/health.ts +4 -3
- package/src/app/pages/api/registry/pages/[...id].ts +20 -0
- package/src/app/pages/api/registry/pages/index.ts +19 -0
- package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +1 -1
- package/src/app/pages/api/registry/patterns/[id].ts +2 -2
- package/src/app/pages/api/registry/patterns/index.ts +1 -1
- package/src/app/pages/api/registry/transfer/[kind]/[id].ts +3 -3
- package/src/app/pages/api/registry/transfer/[kind]/index.ts +21 -0
- package/src/app/pages/api/registry/transfer/assets/[collection].ts +2 -1
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/complete.ts +11 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/upload.ts +12 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +2 -1
- package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +2 -2
- package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +2 -2
- package/src/app/pages/api/sync/upload.ts +21 -0
- package/src/app/pages/cli/device.tsx +148 -0
- package/src/app/pages/foundations/icons/index.tsx +2 -2
- package/src/app/pages/foundations/icons/{[name] → library/[name]}/index.tsx +6 -6
- package/src/app/pages/index/index.tsx +14 -0
- package/src/app/pages/index.tsx +31 -5
- package/src/app/pages/install.tsx +341 -0
- package/src/app/pages/login.tsx +117 -0
- package/src/app/pages/reset-password.tsx +155 -0
- package/src/app/types/next-auth.d.ts +27 -0
- package/src/app-builder/README.md +2 -1
- package/src/app-builder/build.ts +317 -123
- package/src/app-builder/client-config.ts +20 -15
- package/src/app-builder/config-diff/strategies/pattern.ts +1 -1
- package/src/app-builder/nav-shell.ts +8 -13
- package/src/app-builder/paths.ts +9 -30
- package/src/app-builder/static-export.ts +6 -6
- package/src/app-builder/vercel-output.ts +11 -10
- package/src/app-builder/watchers/component.ts +3 -3
- package/src/app-builder/watchers/utils.ts +2 -3
- package/src/app-builder/watchers.ts +63 -57
- package/src/artifacts/url.ts +7 -7
- package/src/cache/README.md +4 -1
- package/src/cache/build-cache.ts +139 -53
- package/src/cache/build-inputs.ts +28 -0
- package/src/cache/index.ts +4 -0
- package/src/catalog/README.md +95 -0
- package/src/catalog/define.ts +79 -0
- package/src/catalog/implementation.ts +145 -0
- package/src/catalog/index.ts +31 -0
- package/src/catalog/normalize.ts +145 -0
- package/src/catalog/previews.ts +114 -0
- package/src/catalog/renderers.ts +94 -0
- package/src/catalog/types.ts +82 -0
- package/src/cli/README.md +1 -1
- package/src/cli/auth/browser.ts +29 -0
- package/src/cli/auth/device.ts +186 -0
- package/src/cli/auth/store.ts +194 -0
- package/src/cli/create.ts +58 -3
- package/src/cli/make.ts +22 -74
- package/src/cli/scaffold.ts +39 -222
- package/src/cli.ts +31 -7
- package/src/commands/README.md +1 -1
- package/src/commands/build/app.ts +2 -3
- package/src/commands/build/components.ts +2 -3
- package/src/commands/build/index.ts +3 -4
- package/src/commands/checkout/index.ts +29 -50
- package/src/commands/compat/deprecations.ts +79 -0
- package/src/commands/compat/push-args.ts +106 -0
- package/src/commands/db/migrate.ts +2 -3
- package/src/commands/dev/index.ts +2 -3
- package/src/commands/eject/config.ts +2 -3
- package/src/commands/eject/pages.ts +2 -3
- package/src/commands/eject/theme.ts +2 -3
- package/src/commands/fetch/index.ts +2 -3
- package/src/commands/index.ts +8 -2
- package/src/commands/login/index.ts +61 -0
- package/src/commands/logout/index.ts +104 -0
- package/src/commands/make/component.ts +2 -3
- package/src/commands/make/page.ts +4 -5
- package/src/commands/publish/index.ts +32 -49
- package/src/commands/pull/index.ts +32 -0
- package/src/commands/push/index.ts +115 -0
- package/src/commands/scaffold/index.ts +2 -3
- package/src/commands/start/index.ts +2 -3
- package/src/commands/types.ts +13 -1
- package/src/commands/utils.ts +107 -2
- package/src/commands/validate/components.ts +2 -3
- package/src/config/README.md +67 -5
- package/src/config/catalog-include.ts +227 -0
- package/src/config/defaults.ts +29 -13
- package/src/config/env.ts +41 -0
- package/src/config/errors.ts +7 -0
- package/src/config/from-env.ts +59 -0
- package/src/config/helpers.ts +3 -3
- package/src/config/index.ts +9 -4
- package/src/config/loader.ts +232 -44
- package/src/config/normalizers/page.ts +4 -4
- package/src/config/resolve-env.ts +107 -0
- package/src/config/runtime.ts +213 -227
- package/src/define.ts +18 -0
- package/src/handlebars.ts +24 -0
- package/src/index.d.ts +2 -2
- package/src/index.ts +341 -92
- package/src/nav/resolver.ts +3 -1
- package/src/nav/workspace.ts +2 -2
- package/src/pattern.ts +14 -0
- package/src/pipeline/README.md +1 -1
- package/src/pipeline/figma.ts +98 -49
- package/src/pipeline/index.ts +1 -1
- package/src/pipeline/styles.ts +1 -1
- package/src/pipeline/validation.ts +3 -3
- package/src/react.ts +52 -0
- package/src/registry/asset-storage/define.ts +1 -1
- package/src/registry/asset-storage/resolve.ts +37 -29
- package/src/registry/asset-storage/types.ts +30 -8
- package/src/registry/auth/access-tokens.ts +222 -0
- package/src/registry/auth/action-tokens.ts +225 -0
- package/src/registry/auth/ai-keys.ts +62 -0
- package/src/registry/auth/crypto.ts +75 -0
- package/src/registry/auth/database.ts +15 -0
- package/src/registry/auth/device-authorizations.ts +223 -0
- package/src/registry/auth/gravatar.ts +6 -0
- package/src/registry/auth/index.ts +105 -0
- package/src/registry/auth/installation.ts +117 -0
- package/src/registry/auth/password.ts +46 -0
- package/src/registry/auth/rate-limits.ts +84 -0
- package/src/registry/auth/sync-secret.ts +67 -0
- package/src/registry/auth/types.ts +57 -0
- package/src/registry/auth/user-record.ts +25 -0
- package/src/registry/auth/users.ts +145 -0
- package/src/registry/auth/validation.ts +42 -0
- package/src/registry/checkout/assets.ts +28 -9
- package/src/registry/checkout/index.ts +522 -148
- package/src/registry/checkout/tokens.ts +16 -8
- package/src/registry/client.ts +67 -21
- package/src/registry/connection.ts +91 -19
- package/src/registry/content-kinds.ts +39 -0
- package/src/registry/db/client.ts +34 -14
- package/src/registry/db/driver.ts +66 -0
- package/src/registry/db/index.ts +16 -5
- package/src/registry/db/migrate.ts +5 -5
- package/src/registry/db/schema.ts +232 -28
- package/src/registry/email/adapters/resend.ts +22 -0
- package/src/registry/email/adapters/smtp.ts +39 -0
- package/src/registry/email/define.ts +15 -0
- package/src/registry/email/resolve.ts +94 -0
- package/src/registry/email/types.ts +28 -0
- package/src/registry/path.ts +11 -0
- package/src/registry/publish/assets.ts +47 -37
- package/src/registry/publish/errors.ts +3 -3
- package/src/registry/publish/index.ts +286 -27
- package/src/registry/publish/package.ts +23 -1
- package/src/registry/publish/tokens.ts +35 -26
- package/src/registry/selection.ts +63 -0
- package/src/registry/transfer.ts +18 -0
- package/src/store/filesystem.ts +6 -6
- package/src/store/registry.ts +131 -15
- package/src/transformers/plugins/csf-render.ts +4 -2
- package/src/transformers/plugins/ssr-render.ts +66 -19
- package/src/transformers/preview/component/api.ts +13 -1
- package/src/transformers/preview/component/artifacts.ts +8 -4
- package/src/transformers/preview/component/builder.ts +36 -29
- package/src/transformers/preview/component/css.ts +107 -63
- package/src/transformers/preview/component/html.ts +36 -21
- package/src/transformers/preview/component/javascript.ts +15 -7
- package/src/transformers/preview/pattern/builder.ts +33 -20
- package/src/transformers/preview/pattern/html.ts +3 -1
- package/src/transformers/preview/types.ts +31 -3
- package/src/transformers/utils/build.ts +93 -0
- package/src/transformers/utils/module.ts +4 -2
- package/src/types/config.ts +231 -43
- package/src/utils/README.md +2 -1
- package/src/utils/markdown.ts +11 -0
- package/src/utils/menu-shell.ts +14 -7
- package/src/utils/pages.ts +52 -0
- package/src/utils/path.ts +15 -29
- package/templates/default/README.md.tpl +68 -1
- package/templates/default/components/button/button.react.handoff.js.tpl +19 -23
- package/templates/default/components/button/button.react.handoff.ts.tpl +22 -22
- package/templates/default/env.tpl +10 -0
- package/templates/default/gitignore.tpl +26 -3
- package/templates/default/handoff.config.js.tpl +27 -164
- package/templates/default/handoff.config.ts.tpl +27 -148
- package/templates/default/handoff.config.with-examples.js.tpl +28 -162
- package/templates/default/handoff.config.with-examples.ts.tpl +28 -146
- package/templates/default/package.json.tpl +8 -3
- package/templates/default/vercel.json.tpl +4 -0
- package/config/docs/foundations/logos.md +0 -7
- package/config/templates/component/template.json +0 -49
- package/config/templates/template.html +0 -7
- package/dist/commands/make/template.d.ts +0 -8
- package/dist/commands/make/template.js +0 -47
- package/dist/config/normalizers/declaration.d.ts +0 -11
- package/dist/config/normalizers/declaration.js +0 -122
- package/dist/config/normalizers/pattern.d.ts +0 -8
- package/dist/config/normalizers/pattern.js +0 -41
- package/dist/config/validator.d.ts +0 -9
- package/dist/config/validator.js +0 -26
- package/dist/declarations/helpers.d.ts +0 -6
- package/dist/declarations/helpers.js +0 -19
- package/dist/declarations/index.d.ts +0 -2
- package/dist/declarations/index.js +0 -9
- package/dist/declarations/types.d.ts +0 -60
- package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +0 -17
- package/dist/registry/asset-storage/adapters/vercel-blob.js +0 -87
- package/dist/registry/db/adapter.d.ts +0 -43
- package/dist/registry/db/adapter.js +0 -59
- package/src/app/lib/api/redact.ts +0 -46
- package/src/commands/make/template.ts +0 -47
- package/src/config/normalizers/declaration.ts +0 -179
- package/src/config/normalizers/pattern.ts +0 -59
- package/src/config/validator.ts +0 -24
- package/src/declarations/helpers.ts +0 -36
- package/src/declarations/index.ts +0 -19
- package/src/declarations/types.ts +0 -70
- package/src/registry/asset-storage/adapters/vercel-blob.ts +0 -51
- package/src/registry/db/adapter.ts +0 -74
- /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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
npm run
|
|
113
|
-
npm run
|
|
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
|
-
|
|
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
|
-
"
|
|
121
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
156
|
+
### pnpm
|
|
133
157
|
|
|
134
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
import
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
id: '
|
|
166
|
-
name: '
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
342
|
+
### Pages
|
|
182
343
|
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
201
|
-
HANDOFF_DEV_ACCESS_TOKEN=
|
|
357
|
+
HANDOFF_FIGMA_PROJECT_ID=figma-file-id
|
|
358
|
+
HANDOFF_DEV_ACCESS_TOKEN=figma-personal-access-token
|
|
202
359
|
```
|
|
203
360
|
|
|
204
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
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
|
-
`
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
230
|
-
decides what gets produced.
|
|
578
|
+
#### Device login
|
|
231
579
|
|
|
232
|
-
|
|
580
|
+
CLI authorization is started from the source workspace with:
|
|
233
581
|
|
|
234
582
|
```bash
|
|
235
|
-
|
|
236
|
-
handoff-app build --target static # explicit, identical output
|
|
583
|
+
npm run login -- --url http://localhost:4000
|
|
237
584
|
```
|
|
238
585
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
634
|
+
npm run publish -- all
|
|
255
635
|
```
|
|
256
636
|
|
|
257
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
656
|
+
npm run publish -- catalog item-id another-id
|
|
269
657
|
```
|
|
270
658
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
665
|
+
npm run publish -- all --dry-run
|
|
666
|
+
npm run publish -- catalog --no-build
|
|
277
667
|
```
|
|
278
668
|
|
|
279
|
-
`
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
287
|
-
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
HANDOFF_REGISTRY_ACCESS_TOKEN=your-registry-token
|
|
705
|
+
npm run build -- --target registry --package vercel
|
|
302
706
|
```
|
|
303
707
|
|
|
304
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
handoff-app
|
|
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
|
-
|
|
312
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
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
|
-
|
|
354
|
-
|
|
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
|
-
|
|
359
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
382
|
-
|
|
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
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
398
|
-
|
|
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
|
-
|
|
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` |
|
|
410
|
-
| `fetch` |
|
|
411
|
-
| `
|
|
412
|
-
| `
|
|
413
|
-
| `build [--target static\|registry]` |
|
|
414
|
-
| `
|
|
415
|
-
| `
|
|
416
|
-
| `
|
|
417
|
-
| `
|
|
418
|
-
| `
|
|
419
|
-
| `
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
450
|
-
|
|
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.
|