@agent-native/core 0.80.4 → 0.80.6
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/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +28 -2
- package/corpus/core/docs/content/locales/ar-SA/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/de-DE/real-time-collaboration.mdx +3 -3
- package/corpus/core/docs/content/locales/de-DE/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/es-ES/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/es-ES/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/fr-FR/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/ja-JP/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/ko-KR/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/pt-BR/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-CN/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-TW/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +1 -1
- package/corpus/core/docs/content/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/template-design.mdx +1 -1
- package/corpus/core/package.json +3 -7
- package/corpus/core/src/action.ts +21 -0
- package/corpus/core/src/agent/durable-background.ts +32 -11
- package/corpus/core/src/agent/production-agent.ts +11 -1
- package/corpus/core/src/agent/run-loop-with-resume.ts +3 -1
- package/corpus/core/src/brand-kit/fig/index.ts +17 -37
- package/corpus/core/src/cli/create.ts +1 -0
- package/corpus/core/src/cli/index.ts +1 -1
- package/corpus/core/src/cli/mcp-config-writers.ts +187 -51
- package/corpus/core/src/cli/skills.ts +18 -3
- package/corpus/core/src/client/AssistantChat.tsx +11 -1
- package/corpus/core/src/client/agent-chat.ts +12 -1
- package/corpus/core/src/client/composer/PromptComposer.tsx +100 -41
- package/corpus/core/src/client/org/OrgSwitcher.tsx +1 -1
- package/corpus/core/src/client/sharing/ShareButton.tsx +3 -27
- package/corpus/core/src/collab/presence.ts +1 -1
- package/corpus/core/src/mcp/connect-route.ts +34 -5
- package/corpus/core/src/server/action-discovery.ts +4 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +20 -0
- package/corpus/core/src/sharing/registry.ts +17 -0
- package/corpus/core/src/templates/default/pnpm-workspace.yaml +1 -0
- package/corpus/core/src/templates/workspace-root/pnpm-workspace.yaml +1 -0
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +2 -0
- package/corpus/templates/analytics/actions/update-dashboard.ts +8 -0
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +117 -33
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +4 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +7 -0
- package/corpus/templates/analytics/changelog/2026-06-27-dashboards-can-now-nest-under-a-parent-in-the-sidebar-via-a-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-analytics-pages-now-sit-closer-under-the-header-with-tighter.md +6 -0
- package/corpus/templates/assets/actions/generate-image-batch.ts +3 -0
- package/corpus/templates/assets/actions/generate-image.ts +3 -0
- package/corpus/templates/assets/changelog/2026-06-29-longer-agent-generation-window.md +6 -0
- package/corpus/templates/assets/netlify.toml +3 -3
- package/corpus/templates/assets/server/plugins/agent-chat.ts +4 -1
- package/corpus/templates/clips/app/components/capture-install-options.tsx +7 -8
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +1 -1
- package/corpus/templates/content/AGENTS.md +57 -44
- package/corpus/templates/content/README.md +16 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +125 -0
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +28 -6
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +238 -69
- package/corpus/templates/content/actions/_builder-cms-write-settings.ts +125 -43
- package/corpus/templates/content/actions/_database-source-utils.ts +746 -97
- package/corpus/templates/content/actions/_database-utils.ts +17 -14
- package/corpus/templates/content/actions/_federation-join.ts +14 -2
- package/corpus/templates/content/actions/_join-suggestion.ts +89 -14
- package/corpus/templates/content/actions/_local-file-documents.ts +2 -1
- package/corpus/templates/content/actions/attach-content-database-source.ts +124 -5
- package/corpus/templates/content/actions/bind-content-database-source-field.ts +256 -0
- package/corpus/templates/content/actions/change-content-database-source-role.ts +420 -0
- package/corpus/templates/content/actions/create-document.ts +8 -0
- package/corpus/templates/content/actions/execute-builder-source-batch.ts +282 -0
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +220 -8
- package/corpus/templates/content/actions/get-content-database.ts +10 -2
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +19 -2
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +47 -15
- package/corpus/templates/content/actions/refresh-content-database-source.ts +11 -4
- package/corpus/templates/content/actions/review-content-database-source-change-set.ts +6 -2
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +32 -12
- package/corpus/templates/content/actions/stage-builder-revision.ts +14 -10
- package/corpus/templates/content/actions/validate-builder-source-execution.ts +25 -3
- package/corpus/templates/content/app/blocks/BuilderDocsBlocks.tsx +10 -0
- package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +1 -0
- package/corpus/templates/content/app/components/editor/ContentReferencePreview.tsx +185 -0
- package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +1520 -553
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +3 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +150 -1
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +19 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +647 -66
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +543 -204
- package/corpus/templates/content/app/components/editor/extensions/ContentReferenceNode.tsx +82 -0
- package/corpus/templates/content/app/global.css +9 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +62 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +76 -0
- package/corpus/templates/content/app/i18n-data.ts +924 -0
- package/corpus/templates/content/changelog/2026-06-29-builder-review-checks-now-preserve-publish-and-unpublish-cho.md +6 -0
- package/corpus/templates/content/changelog/2026-06-29-database-sources-can-now-be-added-as-more-rows-or-matched.md +6 -0
- package/corpus/templates/content/changelog/2026-06-29-fixed-loading-the-final-rows-in-larger-databases-without-bre.md +6 -0
- package/corpus/templates/content/changelog/2026-06-29-reusable-mdx-references-now-preview-linked-local-documents-i.md +6 -0
- package/corpus/templates/content/shared/api.ts +91 -1
- package/corpus/templates/content/shared/builder-docs-blocks.ts +8 -0
- package/corpus/templates/content/shared/builder-mdx.ts +141 -4
- package/corpus/templates/content/shared/nfm.ts +62 -0
- package/corpus/templates/content/vite.config.ts +14 -7
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +10 -11
- package/corpus/templates/design/AGENTS.md +5 -0
- package/corpus/templates/design/_gitignore +7 -0
- package/corpus/templates/design/actions/apply-visual-edit.ts +22 -12
- package/corpus/templates/design/actions/create-design-system.ts +15 -4
- package/corpus/templates/design/actions/create-file.ts +20 -1
- package/corpus/templates/design/actions/delete-design-system.ts +2 -2
- package/corpus/templates/design/actions/duplicate-design.ts +35 -9
- package/corpus/templates/design/actions/generate-design.ts +146 -69
- package/corpus/templates/design/actions/generate-screens.ts +51 -19
- package/corpus/templates/design/actions/get-design-snapshot.ts +5 -1
- package/corpus/templates/design/actions/get-design.ts +2 -0
- package/corpus/templates/design/actions/import-design-project.ts +28 -9
- package/corpus/templates/design/actions/import-document.ts +22 -8
- package/corpus/templates/design/actions/index-design-system-with-builder.ts +6 -153
- package/corpus/templates/design/actions/navigate.ts +105 -54
- package/corpus/templates/design/actions/set-default-design-system.ts +17 -3
- package/corpus/templates/design/actions/show-design-questions.ts +2 -0
- package/corpus/templates/design/actions/update-design.ts +2 -1
- package/corpus/templates/design/actions/view-screen.ts +12 -6
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +4 -4
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +159 -41
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +8 -4
- package/corpus/templates/design/app/components/design/DrawOverlay.tsx +37 -42
- package/corpus/templates/design/app/components/design/EditPanel.tsx +336 -183
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +153 -34
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +135 -33
- package/corpus/templates/design/app/components/design/QuestionFlow.tsx +6 -0
- package/corpus/templates/design/app/components/design/TweaksPanel.tsx +2 -1
- package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +4 -10
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +84 -43
- package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +4 -4
- package/corpus/templates/design/app/components/design/inspector/{FigmaColorPicker.tsx → DesignColorPicker.tsx} +89 -62
- package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +14 -26
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +40 -12
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +13 -4
- package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +13 -13
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +13 -9
- package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +13 -4
- package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +6 -6
- package/corpus/templates/design/app/components/design/inspector/{figma-icons.tsx → design-icons.tsx} +2 -2
- package/corpus/templates/design/app/components/design/inspector/index.ts +12 -12
- package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +5 -2
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +3 -3
- package/corpus/templates/design/app/components/layout/Layout.tsx +5 -2
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +167 -91
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +16 -3
- package/corpus/templates/design/app/components/visual-editor/SaveStatusIndicator.tsx +15 -0
- package/corpus/templates/design/app/global.css +4 -0
- package/corpus/templates/design/app/hooks/use-design-system.ts +4 -1
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +38 -3
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +9 -2
- package/corpus/templates/design/app/i18n/zh-TW.ts +22 -0
- package/corpus/templates/design/app/i18n-data.ts +527 -0
- package/corpus/templates/design/app/lib/design-systems.ts +6 -4
- package/corpus/templates/design/app/lib/design-templates.ts +15 -3
- package/corpus/templates/design/app/pages/DesignEditor.tsx +641 -158
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +42 -34
- package/corpus/templates/design/app/pages/VisualEdit.tsx +100 -0
- package/corpus/templates/design/app/root.tsx +42 -9
- package/corpus/templates/design/app/routes/visual-edit.tsx +7 -0
- package/corpus/templates/design/changelog/2026-06-28-live-cursors-now-keep-names-beside-a-cleaner-pointer-marker.md +1 -1
- package/corpus/templates/design/changelog/2026-06-28-mode-switching-now-lives-in-a-compact-bottom-right-icon-dock.md +1 -1
- package/corpus/templates/design/changelog/2026-06-28-visual-code-design-mode.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-generation-prompts-now-show-connect-ai-before-sending.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-mode-top-chrome-now-groups-collaborators-preview-shar.md +1 -1
- package/corpus/templates/design/changelog/2026-06-29-fixed-design-editor-exports-and-gradient-edits-so-multi-scre.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-longer-hosted-agent-turns.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-png-export-handles-modern-css-color-functions-more-reliably.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-png-export-now-ignores-duplicate-clicks-while-a-download-is-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-public-visual-edit-and-shared-design-links-now-invite-visito.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-visual-edit-actions-now-reject-missing-design-targets-before.md +6 -0
- package/corpus/templates/design/e2e/README.md +71 -0
- package/corpus/templates/design/e2e/global-setup.ts +157 -0
- package/corpus/templates/design/e2e/helpers.ts +148 -0
- package/corpus/templates/design/netlify.toml +3 -4
- package/corpus/templates/design/package.json +5 -1
- package/corpus/templates/design/playwright.config.ts +51 -0
- package/corpus/templates/design/server/handlers/index-design-system-with-builder.ts +121 -0
- package/corpus/templates/design/server/handlers/uploads.ts +89 -38
- package/corpus/templates/design/server/lib/builder-design-system-proxy.ts +181 -0
- package/corpus/templates/design/server/lib/coding-handoff.ts +33 -4
- package/corpus/templates/design/server/lib/design-export.ts +35 -8
- package/corpus/templates/design/server/plugins/agent-chat.ts +4 -0
- package/corpus/templates/design/server/plugins/auth.ts +10 -1
- package/corpus/templates/design/server/routes/api/index-design-system-with-builder.post.ts +1 -0
- package/corpus/templates/design/shared/api.ts +2 -0
- package/corpus/templates/design/shared/apply-edits.ts +12 -8
- package/corpus/templates/design/shared/canvas-math.ts +19 -1
- package/corpus/templates/design/shared/code-layer.ts +87 -28
- package/corpus/templates/design/shared/generation-session.ts +1 -0
- package/corpus/templates/design/shared/resolve-tweaks.ts +4 -3
- package/corpus/templates/design/shared/shader-presets.ts +10 -0
- package/corpus/templates/design/shared/source-mode.ts +9 -0
- package/corpus/templates/design/vitest.config.ts +2 -1
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +1 -1
- package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +1 -1
- package/corpus/templates/plan/AGENTS.md +1 -1
- package/corpus/templates/plan/actions/create-plan-design.ts +1 -1
- package/corpus/templates/plan/actions/validate-local-plan-source.ts +2 -10
- package/corpus/templates/plan/app/pages/PlansPage.tsx +1 -0
- package/corpus/templates/plan/docs/commenting-ux-plan.md +2 -2
- package/corpus/templates/plan/server/plan-content.ts +1 -1
- package/corpus/templates/plan/server/ui-plan-html.ts +2 -2
- package/corpus/templates/slides/actions/index-design-system-with-builder.ts +6 -153
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +53 -38
- package/corpus/templates/slides/app/components/design-system/{fig-import-response.ts → builder-index-response.ts} +9 -6
- package/corpus/templates/slides/app/components/visual-editor/MultiSelectChip.tsx +1 -1
- package/corpus/templates/slides/changelog/2026-06-29-design-system-setup-now-indexes-uploaded-design-files-throug.md +6 -0
- package/corpus/templates/slides/changelog/2026-06-29-longer-hosted-agent-turns.md +6 -0
- package/corpus/templates/slides/netlify.toml +3 -6
- package/corpus/templates/slides/server/handlers/index-design-system-with-builder.ts +116 -0
- package/corpus/templates/slides/server/lib/builder-design-system-proxy.ts +179 -0
- package/corpus/templates/slides/server/plugins/agent-chat.ts +4 -1
- package/corpus/templates/slides/server/routes/api/index-design-system-with-builder.post.ts +1 -0
- package/dist/action.d.ts +15 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +6 -0
- package/dist/action.js.map +1 -1
- package/dist/agent/durable-background.d.ts +9 -7
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +27 -11
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/production-agent.d.ts +7 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +4 -1
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-loop-with-resume.d.ts +2 -1
- package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
- package/dist/agent/run-loop-with-resume.js +2 -2
- package/dist/agent/run-loop-with-resume.js.map +1 -1
- package/dist/brand-kit/fig/index.d.ts +4 -4
- package/dist/brand-kit/fig/index.d.ts.map +1 -1
- package/dist/brand-kit/fig/index.js +14 -27
- package/dist/brand-kit/fig/index.js.map +1 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +1 -0
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-config-writers.d.ts +15 -7
- package/dist/cli/mcp-config-writers.d.ts.map +1 -1
- package/dist/cli/mcp-config-writers.js +198 -45
- package/dist/cli/mcp-config-writers.js.map +1 -1
- package/dist/cli/skills.d.ts +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +18 -3
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +11 -1
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +15 -2
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/composer/PromptComposer.d.ts.map +1 -1
- package/dist/client/composer/PromptComposer.js +35 -1
- package/dist/client/composer/PromptComposer.js.map +1 -1
- package/dist/client/org/OrgSwitcher.js +1 -1
- package/dist/client/org/OrgSwitcher.js.map +1 -1
- package/dist/client/sharing/ShareButton.d.ts +1 -1
- package/dist/client/sharing/ShareButton.d.ts.map +1 -1
- package/dist/client/sharing/ShareButton.js +2 -15
- package/dist/client/sharing/ShareButton.js.map +1 -1
- package/dist/collab/presence.d.ts +1 -1
- package/dist/collab/presence.js +1 -1
- package/dist/collab/presence.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/mcp/connect-route.d.ts.map +1 -1
- package/dist/mcp/connect-route.js +34 -5
- package/dist/mcp/connect-route.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +8 -8
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +5 -0
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts +6 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +12 -3
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/sharing/registry.d.ts.map +1 -1
- package/dist/sharing/registry.js +11 -0
- package/dist/sharing/registry.js.map +1 -1
- package/dist/templates/default/pnpm-workspace.yaml +1 -0
- package/dist/templates/workspace-root/pnpm-workspace.yaml +1 -0
- package/docs/content/locales/ar-SA/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/ar-SA/template-design.mdx +1 -1
- package/docs/content/locales/de-DE/real-time-collaboration.mdx +3 -3
- package/docs/content/locales/de-DE/template-design.mdx +1 -1
- package/docs/content/locales/es-ES/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/es-ES/template-design.mdx +1 -1
- package/docs/content/locales/fr-FR/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/fr-FR/template-design.mdx +1 -1
- package/docs/content/locales/hi-IN/template-design.mdx +1 -1
- package/docs/content/locales/ja-JP/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/ja-JP/template-design.mdx +1 -1
- package/docs/content/locales/ko-KR/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/ko-KR/template-design.mdx +1 -1
- package/docs/content/locales/pt-BR/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/pt-BR/template-design.mdx +1 -1
- package/docs/content/locales/zh-CN/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/zh-CN/template-design.mdx +1 -1
- package/docs/content/locales/zh-TW/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/zh-TW/template-design.mdx +1 -1
- package/docs/content/real-time-collaboration.mdx +2 -2
- package/docs/content/template-design.mdx +1 -1
- package/package.json +3 -7
- package/src/templates/default/pnpm-workspace.yaml +1 -0
- package/src/templates/workspace-root/pnpm-workspace.yaml +1 -0
- package/corpus/core/src/brand-kit/fig/decode.ts +0 -410
- package/corpus/core/src/brand-kit/fig/extract-design-system.ts +0 -947
- package/corpus/core/src/brand-kit/fig/fig-to-html.ts +0 -1906
- package/corpus/templates/design/actions/import-figma.ts +0 -55
- package/corpus/templates/design/changelog/2026-06-28-figma-style-code-design-mode.md +0 -6
- package/corpus/templates/design/server/handlers/import-figma-system.ts +0 -86
- package/corpus/templates/design/server/routes/api/import-figma-system.post.ts +0 -1
- package/corpus/templates/slides/server/handlers/import-figma-system.ts +0 -101
- package/corpus/templates/slides/server/routes/api/import-figma-system.post.ts +0 -1
- package/dist/brand-kit/fig/decode.d.ts +0 -33
- package/dist/brand-kit/fig/decode.d.ts.map +0 -1
- package/dist/brand-kit/fig/decode.js +0 -358
- package/dist/brand-kit/fig/decode.js.map +0 -1
- package/dist/brand-kit/fig/extract-design-system.d.ts +0 -44
- package/dist/brand-kit/fig/extract-design-system.d.ts.map +0 -1
- package/dist/brand-kit/fig/extract-design-system.js +0 -752
- package/dist/brand-kit/fig/extract-design-system.js.map +0 -1
- package/dist/brand-kit/fig/fig-to-html.d.ts +0 -246
- package/dist/brand-kit/fig/fig-to-html.d.ts.map +0 -1
- package/dist/brand-kit/fig/fig-to-html.js +0 -1506
- package/dist/brand-kit/fig/fig-to-html.js.map +0 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { chromium, type FullConfig } from "@playwright/test";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Global setup: authenticate a test user (email/password; there is no dev auth
|
|
8
|
+
* bypass) and seed one design with a known fixture HTML so specs run against
|
|
9
|
+
* deterministic content. Writes:
|
|
10
|
+
* e2e/.auth/state.json - signed session storageState
|
|
11
|
+
* e2e/.auth/seed.json - { designId } of the seeded design
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const E2E_EMAIL = "e2e@local.test";
|
|
15
|
+
export const E2E_PASSWORD = "password-e2e-1234";
|
|
16
|
+
export const SEED_TITLE = "E2E Seed Design";
|
|
17
|
+
|
|
18
|
+
const AUTH_DIR = path.join(import.meta.dirname, ".auth");
|
|
19
|
+
const STATE_PATH = path.join(AUTH_DIR, "state.json");
|
|
20
|
+
const SEED_PATH = path.join(AUTH_DIR, "seed.json");
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Fixture HTML with distinct, text-identifiable elements. Plain inline styles
|
|
24
|
+
* (no CDN) so the layout is deterministic and offline. The flex row of two
|
|
25
|
+
* buttons exercises reorder/move; headings and paragraphs exercise select.
|
|
26
|
+
*/
|
|
27
|
+
export const FIXTURE_HTML = `<!doctype html>
|
|
28
|
+
<html lang="en">
|
|
29
|
+
<head>
|
|
30
|
+
<meta charset="utf-8" />
|
|
31
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
32
|
+
<title>E2E Fixture</title>
|
|
33
|
+
</head>
|
|
34
|
+
<body style="margin:0;font-family:system-ui,sans-serif;background:#0f1115;color:#f4f4f5">
|
|
35
|
+
<main style="max-width:720px;margin:0 auto;padding:48px 32px;display:flex;flex-direction:column;gap:24px">
|
|
36
|
+
<h1 style="font-size:40px;font-weight:800;margin:0">E2E Hero Heading</h1>
|
|
37
|
+
<p style="font-size:18px;line-height:1.6;margin:0;color:#a1a1aa">First fixture paragraph for selection tests.</p>
|
|
38
|
+
<p style="font-size:18px;line-height:1.6;margin:0;color:#a1a1aa">Second fixture paragraph for selection tests.</p>
|
|
39
|
+
<div style="display:flex;flex-direction:row;gap:16px">
|
|
40
|
+
<button style="padding:14px 28px;border-radius:10px;border:0;background:#6366f1;color:#fff;font-size:16px">Alpha Button</button>
|
|
41
|
+
<button style="padding:14px 28px;border-radius:10px;border:0;background:#22c55e;color:#06240f;font-size:16px">Beta Button</button>
|
|
42
|
+
</div>
|
|
43
|
+
<section style="margin-top:16px;padding:24px;border-radius:14px;background:#1a1d24">
|
|
44
|
+
<h2 style="font-size:24px;margin:0 0 8px">Fixture Card Title</h2>
|
|
45
|
+
<p style="margin:0;color:#a1a1aa">Card body text inside a nested container.</p>
|
|
46
|
+
</section>
|
|
47
|
+
</main>
|
|
48
|
+
</body>
|
|
49
|
+
</html>`;
|
|
50
|
+
|
|
51
|
+
async function postAction(
|
|
52
|
+
request: import("@playwright/test").APIRequestContext,
|
|
53
|
+
baseURL: string,
|
|
54
|
+
name: string,
|
|
55
|
+
input: Record<string, unknown>,
|
|
56
|
+
): Promise<any> {
|
|
57
|
+
const res = await request.post(`${baseURL}/_agent-native/actions/${name}`, {
|
|
58
|
+
data: input,
|
|
59
|
+
headers: { "Content-Type": "application/json" },
|
|
60
|
+
});
|
|
61
|
+
if (!res.ok()) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`action ${name} failed: ${res.status()} ${await res.text()}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return res.json();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default async function globalSetup(config: FullConfig) {
|
|
70
|
+
const baseURL =
|
|
71
|
+
(config.projects[0]?.use?.baseURL as string | undefined) ??
|
|
72
|
+
"http://127.0.0.1:9333";
|
|
73
|
+
await mkdir(AUTH_DIR, { recursive: true });
|
|
74
|
+
|
|
75
|
+
const browser = await chromium.launch();
|
|
76
|
+
const context = await browser.newContext();
|
|
77
|
+
const page = await context.newPage();
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
await page.goto(`${baseURL}/`, { waitUntil: "domcontentloaded" });
|
|
81
|
+
|
|
82
|
+
const isSignIn = async () => /sign in/i.test(await page.title());
|
|
83
|
+
|
|
84
|
+
if (await isSignIn()) {
|
|
85
|
+
// Try to create the account; if it already exists, fall back to sign in.
|
|
86
|
+
await page.getByRole("textbox", { name: "Email" }).fill(E2E_EMAIL);
|
|
87
|
+
await page
|
|
88
|
+
.getByRole("textbox", { name: "Password", exact: true })
|
|
89
|
+
.fill(E2E_PASSWORD);
|
|
90
|
+
const confirm = page.getByRole("textbox", { name: "Confirm password" });
|
|
91
|
+
if (await confirm.count()) await confirm.fill(E2E_PASSWORD);
|
|
92
|
+
await page
|
|
93
|
+
.locator("#signup-form")
|
|
94
|
+
.getByRole("button", { name: "Create account" })
|
|
95
|
+
.click()
|
|
96
|
+
.catch(() => {});
|
|
97
|
+
await page.waitForTimeout(2500);
|
|
98
|
+
|
|
99
|
+
if (await isSignIn()) {
|
|
100
|
+
// Account exists; switch to the Sign in tab and log in.
|
|
101
|
+
await page
|
|
102
|
+
.getByRole("button", { name: "Sign in", exact: true })
|
|
103
|
+
.first()
|
|
104
|
+
.click()
|
|
105
|
+
.catch(() => {});
|
|
106
|
+
await page.getByRole("textbox", { name: "Email" }).fill(E2E_EMAIL);
|
|
107
|
+
await page
|
|
108
|
+
.getByRole("textbox", { name: "Password", exact: true })
|
|
109
|
+
.fill(E2E_PASSWORD);
|
|
110
|
+
await page
|
|
111
|
+
.getByRole("button", { name: /sign in/i })
|
|
112
|
+
.last()
|
|
113
|
+
.click()
|
|
114
|
+
.catch(() => {});
|
|
115
|
+
await page.waitForTimeout(2500);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await page
|
|
120
|
+
.waitForFunction(() => !/sign in/i.test(document.title), null, {
|
|
121
|
+
timeout: 20_000,
|
|
122
|
+
})
|
|
123
|
+
.catch(() => {});
|
|
124
|
+
|
|
125
|
+
await context.storageState({ path: STATE_PATH });
|
|
126
|
+
|
|
127
|
+
// Seed a design + fixture file via the authenticated action surface.
|
|
128
|
+
const created = await postAction(
|
|
129
|
+
context.request,
|
|
130
|
+
baseURL,
|
|
131
|
+
"create-design",
|
|
132
|
+
{
|
|
133
|
+
title: SEED_TITLE,
|
|
134
|
+
projectType: "prototype",
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
const designId: string =
|
|
138
|
+
created?.id ?? created?.data?.id ?? created?.design?.id;
|
|
139
|
+
if (!designId) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`create-design did not return an id: ${JSON.stringify(created)}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
await postAction(context.request, baseURL, "create-file", {
|
|
145
|
+
designId,
|
|
146
|
+
filename: "index.html",
|
|
147
|
+
content: FIXTURE_HTML,
|
|
148
|
+
fileType: "html",
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await writeFile(SEED_PATH, JSON.stringify({ designId }, null, 2));
|
|
152
|
+
// eslint-disable-next-line no-console
|
|
153
|
+
console.log(`[e2e] seeded design ${designId} for ${E2E_EMAIL}`);
|
|
154
|
+
} finally {
|
|
155
|
+
await browser.close();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { expect, type Page, type FrameLocator } from "@playwright/test";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Helpers for driving the Design visual editor in real Chrome.
|
|
8
|
+
*
|
|
9
|
+
* Hard-won facts for this editor:
|
|
10
|
+
* - The design renders inside an iframe, so tests should use a frame locator
|
|
11
|
+
* (`page.frameLocator('iframe')`) instead of parent-page CSS selectors.
|
|
12
|
+
* - A pointer-capturing shield `<div data-agent-native-edit-overlay="shield">`
|
|
13
|
+
* sits on top inside the iframe, so clicks need `{ force: true }`.
|
|
14
|
+
* - Selection/edits are reported to the parent via postMessage
|
|
15
|
+
* (`element-select`, `element-hover`, `visual-style-change`,
|
|
16
|
+
* `visual-structure-change`). Assert on those + the parent inspector DOM.
|
|
17
|
+
* - `page.screenshot()` HANGS (the page never idles). Use `cdpScreenshot()`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export async function readSeedDesignId(): Promise<string> {
|
|
21
|
+
const seedPath = path.join(import.meta.dirname, ".auth", "seed.json");
|
|
22
|
+
const raw = await readFile(seedPath, "utf8");
|
|
23
|
+
const { designId } = JSON.parse(raw) as { designId: string };
|
|
24
|
+
if (!designId) throw new Error("no seeded designId - global-setup failed");
|
|
25
|
+
return designId;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function designFrame(page: Page): FrameLocator {
|
|
29
|
+
return page.frameLocator("iframe");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Open the editor for a design and wait for the toolbar + iframe to be ready. */
|
|
33
|
+
export async function gotoEditor(page: Page, designId: string): Promise<void> {
|
|
34
|
+
await page.goto(`/design/${designId}`, { waitUntil: "domcontentloaded" });
|
|
35
|
+
await expect(
|
|
36
|
+
page.getByRole("button", { name: "Move", exact: true }),
|
|
37
|
+
).toBeVisible({ timeout: 30_000 });
|
|
38
|
+
await expect(page.locator("iframe").first()).toBeVisible();
|
|
39
|
+
// Wait for the iframe bridge to stamp at least one selectable node.
|
|
40
|
+
await expect
|
|
41
|
+
.poll(
|
|
42
|
+
async () =>
|
|
43
|
+
designFrame(page)
|
|
44
|
+
.locator("[data-agent-native-node-id], h1, h2, p, button")
|
|
45
|
+
.count(),
|
|
46
|
+
{ timeout: 20_000 },
|
|
47
|
+
)
|
|
48
|
+
.toBeGreaterThan(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Start capturing bridge postMessages on the parent window. */
|
|
52
|
+
export async function installBridge(page: Page): Promise<void> {
|
|
53
|
+
await page.evaluate(() => {
|
|
54
|
+
(window as any).__bridge = [];
|
|
55
|
+
window.addEventListener("message", (e: MessageEvent) => {
|
|
56
|
+
const t = (e.data as any)?.type;
|
|
57
|
+
if (typeof t === "string" && /^(element-|visual-)/.test(t)) {
|
|
58
|
+
(window as any).__bridge.push(e.data);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function bridgeMessages(page: Page): Promise<any[]> {
|
|
65
|
+
return page.evaluate(() => (window as any).__bridge ?? []);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Wait until a bridge message of `type` arrives, returning its payload. */
|
|
69
|
+
export async function waitForBridge(
|
|
70
|
+
page: Page,
|
|
71
|
+
type: string,
|
|
72
|
+
timeout = 8_000,
|
|
73
|
+
): Promise<any> {
|
|
74
|
+
const handle = await page.waitForFunction(
|
|
75
|
+
(t) =>
|
|
76
|
+
((window as any).__bridge ?? []).find((m: any) => m.type === t) ?? null,
|
|
77
|
+
type,
|
|
78
|
+
{ timeout },
|
|
79
|
+
);
|
|
80
|
+
return handle.jsonValue();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Click an element inside the design iframe by its visible text and return the
|
|
85
|
+
* resulting `element-select` payload. Uses force:true to punch through the
|
|
86
|
+
* shield overlay (which is what actually drives selection).
|
|
87
|
+
*/
|
|
88
|
+
export async function selectByText(page: Page, text: string): Promise<any> {
|
|
89
|
+
await installBridge(page);
|
|
90
|
+
await page.evaluate(() => ((window as any).__bridge = []));
|
|
91
|
+
await designFrame(page)
|
|
92
|
+
.getByText(text, { exact: false })
|
|
93
|
+
.first()
|
|
94
|
+
.click({ force: true, timeout: 8_000 });
|
|
95
|
+
const sel = await waitForBridge(page, "element-select");
|
|
96
|
+
return sel?.payload ?? sel;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Number of inputs in the right-hand inspector (proxy for "inspector populated"). */
|
|
100
|
+
export async function inspectorInputCount(page: Page): Promise<number> {
|
|
101
|
+
return page.locator("input").count();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Drag an element inside the iframe by `(dx, dy)` parent-page pixels using real
|
|
106
|
+
* pointer events (the canvas bridge handles mousedown/mousemove/mouseup).
|
|
107
|
+
* Selects it first so the move/reorder interaction is armed.
|
|
108
|
+
*/
|
|
109
|
+
export async function dragCanvasByText(
|
|
110
|
+
page: Page,
|
|
111
|
+
text: string,
|
|
112
|
+
dx: number,
|
|
113
|
+
dy: number,
|
|
114
|
+
): Promise<string[]> {
|
|
115
|
+
await selectByText(page, text);
|
|
116
|
+
await page.evaluate(() => ((window as any).__bridge = []));
|
|
117
|
+
const box = await designFrame(page)
|
|
118
|
+
.getByText(text, { exact: false })
|
|
119
|
+
.first()
|
|
120
|
+
.boundingBox();
|
|
121
|
+
if (!box) throw new Error(`no bounding box for "${text}"`);
|
|
122
|
+
const cx = box.x + box.width / 2;
|
|
123
|
+
const cy = box.y + box.height / 2;
|
|
124
|
+
await page.mouse.move(cx, cy);
|
|
125
|
+
await page.mouse.down();
|
|
126
|
+
const steps = 10;
|
|
127
|
+
for (let i = 1; i <= steps; i++) {
|
|
128
|
+
await page.mouse.move(cx + (dx * i) / steps, cy + (dy * i) / steps);
|
|
129
|
+
}
|
|
130
|
+
await page.waitForTimeout(150);
|
|
131
|
+
await page.mouse.up();
|
|
132
|
+
await page.waitForTimeout(400);
|
|
133
|
+
const msgs = await bridgeMessages(page);
|
|
134
|
+
return [...new Set(msgs.map((m) => m.type))];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Screenshot via CDP; bypasses Playwright's stability wait, which never settles. */
|
|
138
|
+
export async function cdpScreenshot(
|
|
139
|
+
page: Page,
|
|
140
|
+
filePath: string,
|
|
141
|
+
): Promise<void> {
|
|
142
|
+
const client = await page.context().newCDPSession(page);
|
|
143
|
+
const { data } = await client.send("Page.captureScreenshot", {
|
|
144
|
+
format: "png",
|
|
145
|
+
});
|
|
146
|
+
const { writeFile } = await import("node:fs/promises");
|
|
147
|
+
await writeFile(filePath, Buffer.from(data, "base64"));
|
|
148
|
+
}
|
|
@@ -8,10 +8,9 @@ functions = "templates/design/.netlify/functions-internal"
|
|
|
8
8
|
GA_MEASUREMENT_ID = "G-ESF7FYXGN9"
|
|
9
9
|
NITRO_PRESET = "netlify"
|
|
10
10
|
NPM_CONFIG_PRODUCTION = "false"
|
|
11
|
+
AGENT_CHAT_DURABLE_BACKGROUND = "true"
|
|
11
12
|
|
|
12
|
-
#
|
|
13
|
-
# The
|
|
14
|
-
# AGENT_RUN_SOFT_TIMEOUT_MS, keep it below this function timeout so the hidden
|
|
15
|
-
# auto-continuation path can hand off before Netlify kills the function.
|
|
13
|
+
# Emit the Netlify background function used by Design's durable agent-chat opt-in.
|
|
14
|
+
# The normal server timeout still protects non-background HTTP handlers.
|
|
16
15
|
[functions."*"]
|
|
17
16
|
timeout = 75
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
"test": "vitest --run",
|
|
11
11
|
"typecheck": "agent-native typecheck",
|
|
12
12
|
"action": "agent-native action",
|
|
13
|
-
"script": "agent-native script"
|
|
13
|
+
"script": "agent-native script",
|
|
14
|
+
"e2e": "playwright test",
|
|
15
|
+
"e2e:headed": "playwright test --headed",
|
|
16
|
+
"e2e:ui": "playwright test --ui"
|
|
14
17
|
},
|
|
15
18
|
"dependencies": {
|
|
16
19
|
"@agent-native/core": "workspace:*",
|
|
@@ -31,6 +34,7 @@
|
|
|
31
34
|
"zod": "^4.3.6"
|
|
32
35
|
},
|
|
33
36
|
"devDependencies": {
|
|
37
|
+
"@playwright/test": "^1.61.1",
|
|
34
38
|
"@radix-ui/react-accordion": "^1.2.12",
|
|
35
39
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
36
40
|
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { defineConfig, devices } from "@playwright/test";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* E2E config for the Design visual editor.
|
|
7
|
+
*
|
|
8
|
+
* Runs real Chromium against a dev server backed by a throwaway local SQLite
|
|
9
|
+
* database (`data/e2e.db`). `global-setup.ts` signs up a test user, saves the
|
|
10
|
+
* signed session as `storageState`, and seeds one design with a known fixture.
|
|
11
|
+
*
|
|
12
|
+
* Run: `pnpm e2e` (headless), `pnpm e2e:headed`, `pnpm e2e:ui`.
|
|
13
|
+
* Override the target with `E2E_BASE_URL` (e.g. point at an already-running
|
|
14
|
+
* server on :9300); then `webServer.reuseExistingServer` keeps it.
|
|
15
|
+
*/
|
|
16
|
+
const PORT = Number(process.env.E2E_PORT ?? 9333);
|
|
17
|
+
const BASE_URL = process.env.E2E_BASE_URL ?? `http://127.0.0.1:${PORT}`;
|
|
18
|
+
|
|
19
|
+
export default defineConfig({
|
|
20
|
+
testDir: "./e2e",
|
|
21
|
+
// The editor is heavy (iframe bridge + polling); give generous budgets.
|
|
22
|
+
timeout: 90_000,
|
|
23
|
+
expect: { timeout: 15_000 },
|
|
24
|
+
fullyParallel: false,
|
|
25
|
+
workers: 1,
|
|
26
|
+
retries: process.env.CI ? 1 : 0,
|
|
27
|
+
reporter: process.env.CI ? "github" : "list",
|
|
28
|
+
globalSetup: path.join(import.meta.dirname, "e2e", "global-setup.ts"),
|
|
29
|
+
use: {
|
|
30
|
+
baseURL: BASE_URL,
|
|
31
|
+
storageState: path.join(import.meta.dirname, "e2e", ".auth", "state.json"),
|
|
32
|
+
trace: "on-first-retry",
|
|
33
|
+
actionTimeout: 15_000,
|
|
34
|
+
navigationTimeout: 30_000,
|
|
35
|
+
},
|
|
36
|
+
projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
|
|
37
|
+
webServer: process.env.E2E_BASE_URL
|
|
38
|
+
? undefined
|
|
39
|
+
: {
|
|
40
|
+
// APP_NAME + the app-prefixed DESIGN_DATABASE_URL is checked BEFORE the
|
|
41
|
+
// generic DATABASE_URL, so a `.env` Postgres URL can never override this
|
|
42
|
+
// throwaway local SQLite db.
|
|
43
|
+
command: `APP_NAME=design DESIGN_DATABASE_URL="file:./data/e2e.db" PORT=${PORT} pnpm dev`,
|
|
44
|
+
url: BASE_URL,
|
|
45
|
+
reuseExistingServer: !process.env.CI,
|
|
46
|
+
// Cold Vite dep-optimization on first boot can exceed two minutes.
|
|
47
|
+
timeout: 300_000,
|
|
48
|
+
stdout: "ignore",
|
|
49
|
+
stderr: "pipe",
|
|
50
|
+
},
|
|
51
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FeatureNotConfiguredError,
|
|
3
|
+
getSession,
|
|
4
|
+
startBuilderDesignSystemIndex,
|
|
5
|
+
} from "@agent-native/core/server";
|
|
6
|
+
import {
|
|
7
|
+
defineEventHandler,
|
|
8
|
+
getRequestHeader,
|
|
9
|
+
readMultipartFormData,
|
|
10
|
+
setResponseStatus,
|
|
11
|
+
} from "h3";
|
|
12
|
+
|
|
13
|
+
import { upsertBuilderProxyDesignSystem } from "../lib/builder-design-system-proxy.js";
|
|
14
|
+
|
|
15
|
+
const MAX_FIG_BYTES = 200 * 1024 * 1024;
|
|
16
|
+
const MULTIPART_OVERHEAD_BYTES = 1024 * 1024;
|
|
17
|
+
|
|
18
|
+
function requestContentLength(event: Parameters<typeof getRequestHeader>[0]) {
|
|
19
|
+
const raw = getRequestHeader(event, "content-length");
|
|
20
|
+
if (!raw) return null;
|
|
21
|
+
const parsed = Number(raw);
|
|
22
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Builder-indexing endpoint: accepts a `.fig` upload (multipart field `file`)
|
|
27
|
+
* and starts Builder's design-system indexing pipeline. The app does not decode
|
|
28
|
+
* Figma files locally; Builder owns extraction, generated docs, and the
|
|
29
|
+
* asynchronous indexing job.
|
|
30
|
+
*/
|
|
31
|
+
export const indexDesignSystemWithBuilder = defineEventHandler(
|
|
32
|
+
async (event) => {
|
|
33
|
+
const session = await getSession(event).catch(() => null);
|
|
34
|
+
if (!session?.email) {
|
|
35
|
+
setResponseStatus(event, 401);
|
|
36
|
+
return { error: "Unauthorized" };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const contentLength = requestContentLength(event);
|
|
40
|
+
if (
|
|
41
|
+
contentLength !== null &&
|
|
42
|
+
contentLength > MAX_FIG_BYTES + MULTIPART_OVERHEAD_BYTES
|
|
43
|
+
) {
|
|
44
|
+
setResponseStatus(event, 413);
|
|
45
|
+
return {
|
|
46
|
+
error: `File too large (max ${Math.round(MAX_FIG_BYTES / 1024 / 1024)} MB).`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let parts;
|
|
51
|
+
try {
|
|
52
|
+
parts = await readMultipartFormData(event);
|
|
53
|
+
} catch {
|
|
54
|
+
setResponseStatus(event, 413);
|
|
55
|
+
return { error: "Upload too large or malformed." };
|
|
56
|
+
}
|
|
57
|
+
const part = parts?.find(
|
|
58
|
+
(p) => (p.name === "file" || p.name === "fig") && p.data,
|
|
59
|
+
);
|
|
60
|
+
if (!part) {
|
|
61
|
+
setResponseStatus(event, 400);
|
|
62
|
+
return {
|
|
63
|
+
error: "No .fig file uploaded (expected multipart field 'file').",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (part.data.length > MAX_FIG_BYTES) {
|
|
67
|
+
setResponseStatus(event, 413);
|
|
68
|
+
return {
|
|
69
|
+
error: `File too large (max ${Math.round(MAX_FIG_BYTES / 1024 / 1024)} MB).`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const suggestedTitle =
|
|
74
|
+
(part.filename || "Imported brand")
|
|
75
|
+
.replace(/\.fig$/i, "")
|
|
76
|
+
.replace(/[-_]+/g, " ")
|
|
77
|
+
.trim() || "Imported brand";
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const result = await startBuilderDesignSystemIndex({
|
|
81
|
+
projectName: suggestedTitle,
|
|
82
|
+
files: [
|
|
83
|
+
{
|
|
84
|
+
name: part.filename || "brand.fig",
|
|
85
|
+
data: part.data,
|
|
86
|
+
mimeType: "application/octet-stream",
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
const proxy = await upsertBuilderProxyDesignSystem({
|
|
91
|
+
result,
|
|
92
|
+
ownerEmail: session.email,
|
|
93
|
+
orgId: session.orgId ?? null,
|
|
94
|
+
projectName: suggestedTitle,
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
...result,
|
|
98
|
+
...proxy,
|
|
99
|
+
uploadedFileCount: 1,
|
|
100
|
+
};
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (err instanceof FeatureNotConfiguredError) {
|
|
103
|
+
setResponseStatus(event, 412);
|
|
104
|
+
return {
|
|
105
|
+
error:
|
|
106
|
+
err.message ||
|
|
107
|
+
"Connect Builder.io before indexing a design system from Figma.",
|
|
108
|
+
builderConnectUrl:
|
|
109
|
+
err.builderConnectUrl ?? "/_agent-native/builder/connect",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
setResponseStatus(event, 502);
|
|
113
|
+
return {
|
|
114
|
+
error:
|
|
115
|
+
err instanceof Error
|
|
116
|
+
? err.message
|
|
117
|
+
: "Builder design-system indexing failed.",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
);
|
|
@@ -5,6 +5,7 @@ import path from "path";
|
|
|
5
5
|
import { getSession } from "@agent-native/core/server";
|
|
6
6
|
import {
|
|
7
7
|
defineEventHandler,
|
|
8
|
+
getRequestHeader,
|
|
8
9
|
readMultipartFormData,
|
|
9
10
|
setResponseStatus,
|
|
10
11
|
} from "h3";
|
|
@@ -132,6 +133,20 @@ async function extractUploadText(
|
|
|
132
133
|
return {};
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
type UploadedFileResult = {
|
|
137
|
+
path: string;
|
|
138
|
+
originalName: string;
|
|
139
|
+
filename: string;
|
|
140
|
+
type: string;
|
|
141
|
+
size: number;
|
|
142
|
+
textContent?: string;
|
|
143
|
+
textTruncated?: boolean;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
type InternalUploadedFileResult = UploadedFileResult & {
|
|
147
|
+
_destPath: string;
|
|
148
|
+
};
|
|
149
|
+
|
|
135
150
|
export const uploadFiles = defineEventHandler(async (event) => {
|
|
136
151
|
const session = await getSession(event).catch(() => null);
|
|
137
152
|
if (!session?.email) {
|
|
@@ -139,6 +154,21 @@ export const uploadFiles = defineEventHandler(async (event) => {
|
|
|
139
154
|
return { error: "Unauthorized" };
|
|
140
155
|
}
|
|
141
156
|
|
|
157
|
+
const MAX_FILES = 20;
|
|
158
|
+
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
159
|
+
// Total-body pre-flight: reject before buffering if Content-Length is clearly
|
|
160
|
+
// over the theoretical max (MAX_FILES × MAX_FILE_SIZE each, plus overhead).
|
|
161
|
+
// Content-Length can be spoofed but cheaply eliminates accidental oversized
|
|
162
|
+
// uploads without allocating any memory for the body first.
|
|
163
|
+
const TOTAL_BODY_LIMIT = MAX_FILES * MAX_FILE_SIZE;
|
|
164
|
+
const contentLength = Number(
|
|
165
|
+
getRequestHeader(event, "content-length") ?? "0",
|
|
166
|
+
);
|
|
167
|
+
if (contentLength > TOTAL_BODY_LIMIT) {
|
|
168
|
+
setResponseStatus(event, 413);
|
|
169
|
+
return { error: "Request body too large" };
|
|
170
|
+
}
|
|
171
|
+
|
|
142
172
|
const parts = await readMultipartFormData(event);
|
|
143
173
|
const fileParts = parts?.filter((p) => p.name === "files" && p.data) ?? [];
|
|
144
174
|
|
|
@@ -147,9 +177,6 @@ export const uploadFiles = defineEventHandler(async (event) => {
|
|
|
147
177
|
return { error: "No files uploaded" };
|
|
148
178
|
}
|
|
149
179
|
|
|
150
|
-
const MAX_FILES = 20;
|
|
151
|
-
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
152
|
-
|
|
153
180
|
if (fileParts.length > MAX_FILES) {
|
|
154
181
|
setResponseStatus(event, 413);
|
|
155
182
|
return { error: `Too many files (max ${MAX_FILES})` };
|
|
@@ -161,41 +188,65 @@ export const uploadFiles = defineEventHandler(async (event) => {
|
|
|
161
188
|
return { error: "File too large (max 50 MB per file)" };
|
|
162
189
|
}
|
|
163
190
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
191
|
+
const results = await Promise.allSettled<InternalUploadedFileResult>(
|
|
192
|
+
fileParts.map(async (part) => {
|
|
193
|
+
const originalName = part.filename || "upload";
|
|
194
|
+
const filename = safeFilename(originalName);
|
|
195
|
+
if (!filename) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
"Unsupported file type. Allowed: code, docs, text, JSON, CSV, and raster images.",
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
const ext = path.extname(filename).toLowerCase();
|
|
201
|
+
if (!hasExpectedSignature(ext, part.data)) {
|
|
202
|
+
throw new Error(`File contents do not match ${ext} upload type`);
|
|
203
|
+
}
|
|
204
|
+
const uploadDir = tenantUploadDir(session.email);
|
|
205
|
+
await fs.promises.mkdir(uploadDir, { recursive: true });
|
|
206
|
+
const destPath = path.join(uploadDir, filename);
|
|
207
|
+
await fs.promises.writeFile(destPath, part.data);
|
|
208
|
+
const extracted = await extractUploadText(ext, part.data);
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
// Return the filename (nanoid + ext) as the opaque path token rather
|
|
212
|
+
// than the internal filesystem path so we don't expose the server
|
|
213
|
+
// directory layout or per-tenant hash to the client.
|
|
214
|
+
path: filename,
|
|
215
|
+
originalName,
|
|
216
|
+
filename,
|
|
217
|
+
type: part.type || "application/octet-stream",
|
|
218
|
+
size: part.data.length,
|
|
219
|
+
...extracted,
|
|
220
|
+
_destPath: destPath,
|
|
221
|
+
};
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const failures = results.filter((r) => r.status === "rejected");
|
|
226
|
+
if (failures.length > 0) {
|
|
227
|
+
// Clean up any files that were successfully written before the failure.
|
|
228
|
+
const writtenPaths = results
|
|
229
|
+
.filter((r) => r.status === "fulfilled")
|
|
230
|
+
.map(
|
|
231
|
+
(r) =>
|
|
232
|
+
(r as PromiseFulfilledResult<InternalUploadedFileResult>).value
|
|
233
|
+
._destPath,
|
|
234
|
+
);
|
|
235
|
+
await Promise.allSettled(writtenPaths.map((p) => fs.promises.unlink(p)));
|
|
236
|
+
|
|
237
|
+
const firstError = (failures[0] as PromiseRejectedResult).reason;
|
|
198
238
|
setResponseStatus(event, 400);
|
|
199
|
-
return {
|
|
239
|
+
return {
|
|
240
|
+
error:
|
|
241
|
+
firstError instanceof Error ? firstError.message : "Invalid upload",
|
|
242
|
+
};
|
|
200
243
|
}
|
|
244
|
+
|
|
245
|
+
// Strip the internal _destPath field before returning to the client.
|
|
246
|
+
return results.map((r) => {
|
|
247
|
+
const { _destPath: _unused, ...rest } = (
|
|
248
|
+
r as PromiseFulfilledResult<InternalUploadedFileResult>
|
|
249
|
+
).value;
|
|
250
|
+
return rest;
|
|
251
|
+
});
|
|
201
252
|
});
|