handoff-app 2.0.0-9 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/{Changelog.md → CHANGELOG.md} +85 -1
  2. package/{License.md → LICENSE.md} +1 -1
  3. package/README.md +813 -406
  4. package/RECIPES.md +975 -0
  5. package/UPGRADE.md +57 -9
  6. package/components.json +20 -20
  7. package/config/docs/foundations/colors/guidelines.md +0 -1
  8. package/config/docs/foundations/colors.md +0 -1
  9. package/config/docs/foundations/effects/guidelines.md +0 -1
  10. package/config/docs/foundations/effects/resources.md +0 -1
  11. package/config/docs/foundations/effects.md +0 -1
  12. package/config/docs/foundations/fonts.md +0 -1
  13. package/config/docs/foundations/grid.md +0 -1
  14. package/config/docs/foundations/icon.md +0 -1
  15. package/config/docs/foundations/icons/guidelines.md +0 -1
  16. package/config/docs/foundations/icons.md +0 -1
  17. package/config/docs/foundations/images/guidelines.md +0 -1
  18. package/config/docs/foundations/images/resources.md +0 -1
  19. package/config/docs/foundations/images.md +0 -1
  20. package/config/docs/foundations/logo/resources.md +0 -1
  21. package/config/docs/foundations/logo.md +0 -1
  22. package/config/docs/foundations/typography/guidelines.md +0 -1
  23. package/config/docs/foundations/typography/resources.md +0 -1
  24. package/config/docs/foundations/typography.md +0 -1
  25. package/config/docs/foundations.md +0 -1
  26. package/config/docs/guidelines.md +0 -1
  27. package/config/docs/index.md +0 -1
  28. package/config/docs/system/tokens/foundations/colors.md +0 -4
  29. package/config/docs/system/tokens/foundations/effects.md +0 -4
  30. package/config/docs/system/tokens/foundations/typography.md +0 -2
  31. package/config/docs/system.md +0 -2
  32. package/config/templates/page.md +2 -1
  33. package/config/templates/page.mdx +2 -1
  34. package/dist/ai/connections.d.ts +66 -0
  35. package/dist/ai/connections.js +149 -0
  36. package/dist/ai/define.d.ts +10 -0
  37. package/dist/ai/define.js +13 -0
  38. package/dist/ai/types.d.ts +26 -0
  39. package/dist/ai/types.js +10 -0
  40. package/dist/app-builder/build.d.ts +14 -7
  41. package/dist/app-builder/build.js +195 -66
  42. package/dist/app-builder/client-config.js +3 -2
  43. package/dist/app-builder/paths.d.ts +0 -12
  44. package/dist/app-builder/paths.js +3 -20
  45. package/dist/app-builder/static-export.js +1 -1
  46. package/dist/app-builder/vercel-output.d.ts +1 -1
  47. package/dist/app-builder/vercel-output.js +5 -4
  48. package/dist/cache/build-cache.d.ts +27 -4
  49. package/dist/cache/build-cache.js +114 -47
  50. package/dist/cache/build-inputs.d.ts +13 -0
  51. package/dist/cache/build-inputs.js +19 -0
  52. package/dist/cache/index.d.ts +2 -1
  53. package/dist/cache/index.js +5 -1
  54. package/dist/cli/create.js +1 -2
  55. package/dist/commands/build/index.d.ts +1 -1
  56. package/dist/commands/build/index.js +1 -1
  57. package/dist/commands/utils.d.ts +3 -3
  58. package/dist/config/defaults.js +7 -4
  59. package/dist/config/from-env.d.ts +4 -0
  60. package/dist/config/from-env.js +38 -1
  61. package/dist/config/helpers.js +2 -2
  62. package/dist/config/index.d.ts +0 -1
  63. package/dist/config/index.js +1 -4
  64. package/dist/config/loader.js +26 -2
  65. package/dist/config/resolve-env.d.ts +2 -1
  66. package/dist/config/resolve-env.js +28 -12
  67. package/dist/config/runtime.js +1 -1
  68. package/dist/index.d.ts +12 -7
  69. package/dist/index.js +18 -22
  70. package/dist/nav/index.d.ts +2 -2
  71. package/dist/nav/registry.d.ts +3 -3
  72. package/dist/nav/registry.js +1 -1
  73. package/dist/nav/workspace.d.ts +3 -3
  74. package/dist/nav/workspace.js +1 -1
  75. package/dist/pipeline/figma.d.ts +13 -2
  76. package/dist/pipeline/figma.js +86 -46
  77. package/dist/pipeline/styles.js +1 -1
  78. package/dist/pipeline/validation.d.ts +1 -1
  79. package/dist/pipeline/validation.js +3 -3
  80. package/dist/providers.d.ts +10 -0
  81. package/dist/providers.js +13 -0
  82. package/dist/registry/asset-storage/define.d.ts +5 -5
  83. package/dist/registry/asset-storage/define.js +4 -4
  84. package/dist/registry/asset-storage/resolve.d.ts +9 -19
  85. package/dist/registry/asset-storage/resolve.js +33 -21
  86. package/dist/registry/asset-storage/types.d.ts +37 -17
  87. package/dist/registry/asset-storage/types.js +5 -5
  88. package/dist/registry/auth/ai-keys.d.ts +32 -0
  89. package/dist/registry/auth/ai-keys.js +69 -0
  90. package/dist/registry/auth/crypto.d.ts +17 -0
  91. package/dist/registry/auth/crypto.js +61 -1
  92. package/dist/registry/auth/index.d.ts +2 -1
  93. package/dist/registry/auth/index.js +11 -2
  94. package/dist/registry/checkout/index.d.ts +2 -1
  95. package/dist/registry/checkout/index.js +57 -127
  96. package/dist/registry/client.js +53 -17
  97. package/dist/registry/db/driver.js +3 -3
  98. package/dist/registry/db/schema.d.ts +261 -3
  99. package/dist/registry/db/schema.js +29 -4
  100. package/dist/registry/email/define.d.ts +10 -0
  101. package/dist/registry/email/define.js +13 -0
  102. package/dist/registry/email/providers/smtp.d.ts +10 -0
  103. package/dist/registry/email/providers/smtp.js +73 -0
  104. package/dist/registry/email/resolve.d.ts +28 -0
  105. package/dist/registry/email/resolve.js +74 -0
  106. package/dist/registry/email/types.d.ts +24 -0
  107. package/dist/registry/email/types.js +6 -0
  108. package/dist/registry/path.d.ts +2 -0
  109. package/dist/registry/path.js +11 -1
  110. package/dist/registry/publish/package.js +8 -0
  111. package/dist/registry/transfer.d.ts +4 -0
  112. package/dist/store/registry.d.ts +3 -3
  113. package/dist/store/registry.js +4 -4
  114. package/dist/transformers/preview/component/builder.d.ts +1 -9
  115. package/dist/transformers/preview/component/builder.js +17 -17
  116. package/dist/transformers/preview/component/css.d.ts +2 -1
  117. package/dist/transformers/preview/component/css.js +8 -5
  118. package/dist/transformers/preview/component/html.d.ts +3 -1
  119. package/dist/transformers/preview/component/html.js +6 -2
  120. package/dist/transformers/preview/component/javascript.d.ts +2 -1
  121. package/dist/transformers/preview/component/javascript.js +10 -4
  122. package/dist/transformers/utils/build.d.ts +5 -0
  123. package/dist/transformers/utils/build.js +21 -4
  124. package/dist/types/config.d.ts +129 -18
  125. package/dist/utils/path.d.ts +1 -5
  126. package/dist/utils/path.js +6 -23
  127. package/docs/api.md +6 -4
  128. package/docs/cli.md +275 -54
  129. package/drizzle/0007_add_ai_keys.sql +11 -0
  130. package/drizzle/0008_add_source_dir.sql +2 -0
  131. package/drizzle/meta/0007_snapshot.json +2013 -0
  132. package/drizzle/meta/0008_snapshot.json +2025 -0
  133. package/drizzle/meta/_journal.json +14 -0
  134. package/package.json +19 -8
  135. package/src/ai/README.md +42 -0
  136. package/src/ai/connections.ts +189 -0
  137. package/src/ai/define.ts +15 -0
  138. package/src/ai/types.ts +30 -0
  139. package/src/app/components/AiAssistant/AiAssistantPanel.tsx +144 -0
  140. package/src/app/components/AiAssistant/AiAssistantProvider.tsx +215 -0
  141. package/src/app/components/AiAssistant/AiConversation.tsx +737 -0
  142. package/src/app/components/AiAssistant/AiToolResult.tsx +99 -0
  143. package/src/app/components/AiAssistant/dockState.ts +86 -0
  144. package/src/app/components/AiAssistant/toolLabel.ts +24 -0
  145. package/src/app/components/AiAssistant/useAiChat.ts +147 -0
  146. package/src/app/components/AiAssistant/useAiConnections.ts +86 -0
  147. package/src/app/components/AiAssistant/usePageContext.ts +63 -0
  148. package/src/app/components/Auth/AccountLayout.tsx +58 -19
  149. package/src/app/components/Auth/AuthShell.tsx +27 -33
  150. package/src/app/components/Auth/RegistryInstallGate.tsx +4 -8
  151. package/src/app/components/Auth/installStatus.ts +29 -0
  152. package/src/app/components/Component/Preview.tsx +5 -2
  153. package/src/app/components/Foundations/ColorGrid.tsx +13 -14
  154. package/src/app/components/Layout/Header.tsx +21 -15
  155. package/src/app/components/Layout/Main.tsx +2 -2
  156. package/src/app/components/Markdown/MarkdownComponents.tsx +30 -32
  157. package/src/app/components/McpIntegration/McpConfigDialog.tsx +12 -51
  158. package/src/app/components/Navigation/MobileNav.tsx +2 -2
  159. package/src/app/components/Navigation/SideNav.tsx +1 -15
  160. package/src/app/components/context/NavProvider.tsx +7 -3
  161. package/src/app/components/ui/attachment.tsx +50 -0
  162. package/src/app/components/ui/bubble.tsx +57 -0
  163. package/src/app/components/ui/button.tsx +3 -1
  164. package/src/app/components/ui/confirm-dialog.tsx +87 -0
  165. package/src/app/components/ui/dialog.tsx +7 -2
  166. package/src/app/components/ui/marker.tsx +56 -0
  167. package/src/app/components/ui/sidebar.tsx +3 -1
  168. package/src/app/components/util/index.ts +3 -12
  169. package/src/app/css/index.css +64 -0
  170. package/src/app/lib/ai/auth.ts +33 -0
  171. package/src/app/lib/ai/context.ts +99 -0
  172. package/src/app/lib/ai/model.ts +79 -0
  173. package/src/app/lib/ai/prompt.ts +36 -0
  174. package/src/app/lib/ai/resolve.ts +97 -0
  175. package/src/app/lib/ai/tools.ts +120 -0
  176. package/src/app/lib/api/methods.ts +13 -0
  177. package/src/app/lib/asset-storage.ts +35 -36
  178. package/src/app/lib/auth/api.ts +4 -12
  179. package/src/app/lib/auth/email.ts +96 -23
  180. package/src/app/lib/docs-api/page-rendering.ts +1 -1
  181. package/src/app/lib/docs-api/registry.ts +2 -3
  182. package/src/app/lib/docs-api/runtime-config.ts +47 -21
  183. package/src/app/lib/mcp/components.ts +523 -0
  184. package/src/app/lib/mcp/page-url.ts +24 -0
  185. package/src/app/lib/mcp/pages.ts +84 -0
  186. package/src/app/lib/mcp/result.ts +33 -0
  187. package/src/app/lib/mcp/server.ts +39 -318
  188. package/src/app/lib/mcp/tokens.ts +334 -0
  189. package/src/app/lib/mcp/tool.ts +19 -0
  190. package/src/app/lib/registry-api/asset-transfer.ts +119 -11
  191. package/src/app/lib/registry-api/errors.ts +2 -0
  192. package/src/app/lib/registry-api/handler.ts +7 -1
  193. package/src/app/lib/registry-api/transfer.ts +28 -6
  194. package/src/app/lib/server-module.ts +34 -0
  195. package/src/app/lib/utils.ts +20 -0
  196. package/src/app/next.config.mjs +26 -24
  197. package/src/app/pages/[...slug]/index.tsx +2 -1
  198. package/src/app/pages/_app.tsx +6 -2
  199. package/src/app/pages/_document.tsx +6 -0
  200. package/src/app/pages/account/ai.tsx +183 -0
  201. package/src/app/pages/account/index.tsx +68 -68
  202. package/src/app/pages/account/tokens.tsx +37 -7
  203. package/src/app/pages/account/users.tsx +75 -15
  204. package/src/app/pages/api/account/ai/keys/[connectionId].ts +60 -0
  205. package/src/app/pages/api/account/ai/keys/index.ts +20 -0
  206. package/src/app/pages/api/admin/users/[id]/[action].ts +80 -11
  207. package/src/app/pages/api/admin/users/index.ts +2 -2
  208. package/src/app/pages/api/ai/chat.ts +115 -0
  209. package/src/app/pages/api/ai/connections.ts +23 -0
  210. package/src/app/pages/api/auth/request-reset.ts +14 -16
  211. package/src/app/pages/api/docs/nav.json.ts +1 -1
  212. package/src/app/pages/api/install.ts +3 -3
  213. package/src/app/pages/api/mcp.ts +6 -0
  214. package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/complete.ts +11 -0
  215. package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/upload.ts +12 -0
  216. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +1 -1
  217. package/src/app/pages/cli/device.tsx +10 -5
  218. package/src/app/pages/foundations/colors.tsx +9 -8
  219. package/src/app/pages/foundations/index.tsx +3 -3
  220. package/src/app/pages/index.tsx +2 -2
  221. package/src/app/pages/install.tsx +7 -2
  222. package/src/app/pages/login.tsx +32 -27
  223. package/src/app/pages/reset-password.tsx +21 -20
  224. package/src/app/pages/system/component/[component]/index.tsx +2 -5
  225. package/src/app/pages/system/index.tsx +11 -11
  226. package/src/app/pages/system/pattern/[pattern]/index.tsx +2 -2
  227. package/src/app/pages/system/tokens/components/[component]/index.tsx +0 -4
  228. package/src/app-builder/README.md +2 -1
  229. package/src/app-builder/build.ts +218 -73
  230. package/src/app-builder/client-config.ts +3 -2
  231. package/src/app-builder/paths.ts +3 -22
  232. package/src/app-builder/static-export.ts +1 -1
  233. package/src/app-builder/vercel-output.ts +5 -4
  234. package/src/cache/README.md +4 -1
  235. package/src/cache/build-cache.ts +128 -43
  236. package/src/cache/build-inputs.ts +28 -0
  237. package/src/cache/index.ts +4 -0
  238. package/src/cli/create.ts +1 -2
  239. package/src/commands/build/index.ts +1 -1
  240. package/src/config/README.md +10 -9
  241. package/src/config/defaults.ts +7 -4
  242. package/src/config/from-env.ts +33 -0
  243. package/src/config/helpers.ts +1 -3
  244. package/src/config/index.ts +0 -3
  245. package/src/config/loader.ts +25 -3
  246. package/src/config/resolve-env.ts +28 -15
  247. package/src/config/runtime.ts +1 -1
  248. package/src/index.ts +33 -29
  249. package/src/nav/index.ts +2 -2
  250. package/src/nav/registry.ts +3 -3
  251. package/src/nav/workspace.ts +3 -3
  252. package/src/pipeline/README.md +1 -1
  253. package/src/pipeline/figma.ts +98 -49
  254. package/src/pipeline/styles.ts +1 -1
  255. package/src/pipeline/validation.ts +3 -3
  256. package/src/providers.ts +18 -0
  257. package/src/registry/asset-storage/define.ts +6 -6
  258. package/src/registry/asset-storage/resolve.ts +38 -37
  259. package/src/registry/asset-storage/types.ts +39 -17
  260. package/src/registry/auth/ai-keys.ts +62 -0
  261. package/src/registry/auth/crypto.ts +63 -1
  262. package/src/registry/auth/index.ts +11 -1
  263. package/src/registry/checkout/index.ts +60 -138
  264. package/src/registry/client.ts +60 -20
  265. package/src/registry/db/driver.ts +2 -2
  266. package/src/registry/db/schema.ts +49 -22
  267. package/src/registry/email/define.ts +15 -0
  268. package/src/registry/email/providers/smtp.ts +39 -0
  269. package/src/registry/email/resolve.ts +94 -0
  270. package/src/registry/email/types.ts +28 -0
  271. package/src/registry/path.ts +11 -0
  272. package/src/registry/publish/package.ts +9 -0
  273. package/src/registry/transfer.ts +4 -0
  274. package/src/store/registry.ts +7 -7
  275. package/src/transformers/preview/component/builder.ts +19 -25
  276. package/src/transformers/preview/component/css.ts +9 -4
  277. package/src/transformers/preview/component/html.ts +6 -2
  278. package/src/transformers/preview/component/javascript.ts +14 -4
  279. package/src/transformers/utils/build.ts +16 -3
  280. package/src/types/config.ts +142 -20
  281. package/src/utils/README.md +1 -1
  282. package/src/utils/path.ts +6 -29
  283. package/templates/default/handoff.config.js.tpl +10 -2
  284. package/templates/default/handoff.config.ts.tpl +10 -2
  285. package/templates/default/handoff.config.with-examples.js.tpl +10 -2
  286. package/templates/default/handoff.config.with-examples.ts.tpl +10 -2
  287. package/templates/default/package.json.tpl +2 -2
  288. package/.editorconfig +0 -19
  289. package/.eslintrc.json +0 -4
  290. package/.prettierignore +0 -0
  291. package/.prettierrc.json +0 -8
  292. package/.vscode/settings.json +0 -17
  293. package/dist/config/validator.d.ts +0 -9
  294. package/dist/config/validator.js +0 -24
  295. package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +0 -17
  296. package/dist/registry/asset-storage/adapters/vercel-blob.js +0 -87
  297. package/src/app/components/old/Header.tsx +0 -89
  298. package/src/app/lib/mcp/shape.ts +0 -596
  299. package/src/app/pages/assets/fonts.tsx +0 -129
  300. package/src/app/pages/assets/index.tsx +0 -91
  301. package/src/app/pages/assets/logos.tsx +0 -75
  302. package/src/config/validator.ts +0 -22
  303. package/src/registry/asset-storage/adapters/vercel-blob.ts +0 -51
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Server-only AI provider contract for connections that are not OpenAI-compatible.
3
+ *
4
+ * Every built-in connection speaks the OpenAI-compatible `/chat/completions` API and needs no
5
+ * custom module — Ollama, LiteLLM, OpenRouter, vLLM, LM Studio and Azure OpenAI all serve it, and each of
6
+ * them fronts Anthropic, Google and xAI models. A provider that fits nothing else names a
7
+ * {@link defineAiProvider} module instead of a base URL.
8
+ */
9
+ import type { LanguageModel } from 'ai';
10
+ /** Context passed to a custom provider factory. */
11
+ export interface AiProviderContext {
12
+ /** The connection's `options`, with each `fromEnv()` reference replaced by its value. */
13
+ options: Record<string, unknown>;
14
+ env: NodeJS.ProcessEnv;
15
+ /**
16
+ * The resolved key for this request: the deployment's own key for a service connection, or the
17
+ * reader's saved key for a `credential: 'user'` connection. Undefined when neither applies.
18
+ */
19
+ apiKey?: string;
20
+ }
21
+ /** The pluggable provider contract: resolve one of the connection's declared models. */
22
+ export interface AiProvider {
23
+ languageModel(modelId: string): LanguageModel;
24
+ }
25
+ /** A custom provider may export the provider directly or a factory that builds it from its context. */
26
+ export type AiProviderFactory = (context: AiProviderContext) => AiProvider | Promise<AiProvider>;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ /**
3
+ * Server-only AI provider contract for connections that are not OpenAI-compatible.
4
+ *
5
+ * Every built-in connection speaks the OpenAI-compatible `/chat/completions` API and needs no
6
+ * custom module — Ollama, LiteLLM, OpenRouter, vLLM, LM Studio and Azure OpenAI all serve it, and each of
7
+ * them fronts Anthropic, Google and xAI models. A provider that fits nothing else names a
8
+ * {@link defineAiProvider} module instead of a base URL.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -10,7 +10,7 @@ export declare const DEFAULT_BUILD_TARGET: BuildTarget;
10
10
  /**
11
11
  * Resolved packaging axis — orthogonal to {@link BuildTarget}. It
12
12
  * selects *how* the build is packaged, not *what* is built:
13
- * - `standalone` → the existing sites-directory deliverable (`out/<projectId>` for static, the
13
+ * - `standalone` → the sites-directory deliverable (`out/static` for static, the
14
14
  * Node standalone bundle `out/registry` for registry).
15
15
  * - `vercel` → the Vercel Build Output API directory (`.vercel/output`) at the repo root.
16
16
  *
@@ -28,14 +28,21 @@ export type BuildPackage = 'standalone' | 'vercel';
28
28
  export declare class HandoffBuildError extends Error {
29
29
  constructor(message: string);
30
30
  }
31
+ /** Fixed names, not project ids, so deploy paths are the same on every machine. */
32
+ export declare const getStaticBuildOutputPath: (handoff: Handoff) => string;
33
+ export declare const getRegistryBuildOutputPath: (handoff: Handoff) => string;
31
34
  /**
32
- * Output directory for the packaged registry app. Lives under the configurable sites output
33
- * directory (`sitesOutputDirectory`, default `out`) alongside the static target's `out/<projectId>`
34
- * deliverable — both build targets emit to one discoverable, gitignored output root, keeping the
35
- * package's `.handoff` for internal build/staging only. The registry artifact is a single
36
- * deployable, so it is not keyed by project id.
35
+ * Root for Next's file tracing. Every traced path in the standalone bundle is relative to this root.
36
+ *
37
+ * The tracer copies only the files inside this root. It must contain the staged app (see
38
+ * {@link getAppPath}), or Next emits no standalone bundle. It must also contain every
39
+ * `node_modules` that the required runtime dependencies resolve from, or the bundle cannot start
40
+ * (see {@link assertRegistryRuntimeDepsTraced}).
41
+ *
42
+ * `workingPath` satisfies both conditions only when handoff-app is installed in the same package as
43
+ * the config, and nothing is hoisted above it.
37
44
  */
38
- export declare const getRegistryBuildOutputPath: (handoff: Handoff) => string;
45
+ export declare const resolveRegistryTracingRoot: (handoff: Handoff) => string;
39
46
  /**
40
47
  * Build the Next.js documentation application for the resolved target. The `static` target builds
41
48
  * and exports the local workspace (default); the `registry` target packages the deployable dynamic
@@ -35,16 +35,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
35
35
  return (mod && mod.__esModule) ? mod : { "default": mod };
36
36
  };
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
- exports.devApp = exports.watchApp = exports.getRegistryBuildOutputPath = exports.HandoffBuildError = exports.DEFAULT_BUILD_TARGET = void 0;
38
+ exports.devApp = exports.watchApp = exports.resolveRegistryTracingRoot = exports.getRegistryBuildOutputPath = exports.getStaticBuildOutputPath = exports.HandoffBuildError = exports.DEFAULT_BUILD_TARGET = void 0;
39
39
  const p = __importStar(require("@clack/prompts"));
40
+ const nft_1 = require("@vercel/nft");
40
41
  const cross_spawn_1 = __importDefault(require("cross-spawn"));
41
42
  const fs_extra_1 = __importDefault(require("fs-extra"));
42
43
  const path_1 = __importDefault(require("path"));
44
+ const connections_1 = require("../ai/connections");
43
45
  const config_1 = require("../config");
46
+ const from_env_1 = require("../config/from-env");
44
47
  const components_1 = require("../pipeline/components");
45
48
  const patterns_1 = require("../pipeline/patterns");
46
49
  const resolve_1 = require("../registry/asset-storage/resolve");
47
50
  const driver_1 = require("../registry/db/driver");
51
+ const resolve_2 = require("../registry/email/resolve");
48
52
  const builder_1 = __importDefault(require("../transformers/preview/component/builder"));
49
53
  const css_1 = require("../transformers/preview/component/css");
50
54
  const javascript_1 = require("../transformers/preview/component/javascript");
@@ -75,7 +79,7 @@ exports.HandoffBuildError = HandoffBuildError;
75
79
  * Resolve and validate the effective `(target, package)` pair.
76
80
  *
77
81
  * `--package` is optional and never implies a target. When omitted the effective package is
78
- * `standalone` for both targets, preserving today's `out/<projectId>` / `out/registry` deliverables.
82
+ * `standalone` for both targets (`out/static` / `out/registry`).
79
83
  * The one rejected combination is `static + standalone`: a static snapshot has no server to package
80
84
  * as a Node standalone bundle.
81
85
  */
@@ -148,15 +152,50 @@ const runNextBuild = (appPath, target, errorLabel) => __awaiter(void 0, void 0,
148
152
  });
149
153
  });
150
154
  });
151
- /**
152
- * Output directory for the packaged registry app. Lives under the configurable sites output
153
- * directory (`sitesOutputDirectory`, default `out`) alongside the static target's `out/<projectId>`
154
- * deliverable — both build targets emit to one discoverable, gitignored output root, keeping the
155
- * package's `.handoff` for internal build/staging only. The registry artifact is a single
156
- * deployable, so it is not keyed by project id.
157
- */
155
+ /** Fixed names, not project ids, so deploy paths are the same on every machine. */
156
+ const getStaticBuildOutputPath = (handoff) => path_1.default.resolve(handoff.workingPath, handoff.sitesDirectory, 'static');
157
+ exports.getStaticBuildOutputPath = getStaticBuildOutputPath;
158
158
  const getRegistryBuildOutputPath = (handoff) => path_1.default.resolve(handoff.workingPath, handoff.sitesDirectory, 'registry');
159
159
  exports.getRegistryBuildOutputPath = getRegistryBuildOutputPath;
160
+ const commonAncestorPath = (paths) => {
161
+ const [first, ...rest] = paths.map((entry) => path_1.default.resolve(entry).split(path_1.default.sep));
162
+ let shared = first;
163
+ for (const candidate of rest) {
164
+ const next = [];
165
+ for (let index = 0; index < Math.min(shared.length, candidate.length) && shared[index] === candidate[index]; index += 1) {
166
+ next.push(shared[index]);
167
+ }
168
+ shared = next;
169
+ }
170
+ return shared.join(path_1.default.sep) || path_1.default.sep;
171
+ };
172
+ /**
173
+ * Root for Next's file tracing. Every traced path in the standalone bundle is relative to this root.
174
+ *
175
+ * The tracer copies only the files inside this root. It must contain the staged app (see
176
+ * {@link getAppPath}), or Next emits no standalone bundle. It must also contain every
177
+ * `node_modules` that the required runtime dependencies resolve from, or the bundle cannot start
178
+ * (see {@link assertRegistryRuntimeDepsTraced}).
179
+ *
180
+ * `workingPath` satisfies both conditions only when handoff-app is installed in the same package as
181
+ * the config, and nothing is hoisted above it.
182
+ */
183
+ const resolveRegistryTracingRoot = (handoff) => {
184
+ const anchors = [path_1.default.resolve(handoff.workingPath), path_1.default.resolve(handoff.modulePath)];
185
+ // Resolve each module instead of assuming its location. A hoisted or linked dependency otherwise
186
+ // drops out of the trace.
187
+ for (const moduleName of getRequiredRegistryRuntimeModules(handoff)) {
188
+ try {
189
+ const resolved = require.resolve(`${moduleName}/package.json`, { paths: [handoff.modulePath, handoff.workingPath] });
190
+ anchors.push(fs_extra_1.default.realpathSync(path_1.default.dirname(resolved)));
191
+ }
192
+ catch (_a) {
193
+ // The assertion after assembly reports a module that does not resolve.
194
+ }
195
+ }
196
+ return commonAncestorPath(anchors);
197
+ };
198
+ exports.resolveRegistryTracingRoot = resolveRegistryTracingRoot;
160
199
  /**
161
200
  * Performs cleanup of the application directory by removing the existing app directory if it exists.
162
201
  */
@@ -173,7 +212,7 @@ const cleanupAppDirectory = (handoff) => __awaiter(void 0, void 0, void 0, funct
173
212
  * @returns The path to the prepared application directory
174
213
  */
175
214
  const initializeProjectApp = (handoff_1, ...args_1) => __awaiter(void 0, [handoff_1, ...args_1], void 0, function* (handoff, options = {}) {
176
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
215
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
177
216
  const includeWorkspaceArtifacts = (_a = options.includeWorkspaceArtifacts) !== null && _a !== void 0 ? _a : true;
178
217
  const runtimeMode = (_e = (_b = options.runtimeModeOverride) !== null && _b !== void 0 ? _b : (_d = (_c = handoff.config) === null || _c === void 0 ? void 0 : _c.runtime) === null || _d === void 0 ? void 0 : _d.mode) !== null && _e !== void 0 ? _e : 'workspace';
179
218
  const srcPath = path_1.default.resolve(handoff.modulePath, 'src', 'app');
@@ -204,7 +243,7 @@ const initializeProjectApp = (handoff_1, ...args_1) => __awaiter(void 0, [handof
204
243
  const handoffAppBasePath = (_f = handoff.config.app.base_path) !== null && _f !== void 0 ? _f : '';
205
244
  const handoffWorkingPath = path_1.default.resolve(handoff.workingPath);
206
245
  const handoffModulePath = path_1.default.resolve(handoff.modulePath);
207
- const handoffExportPath = path_1.default.resolve(handoff.workingPath, handoff.exportsDirectory, handoff.getProjectId());
246
+ const handoffExportPath = handoff.getOutputPath();
208
247
  const nextConfigPath = path_1.default.resolve(srcPath, 'next.config.mjs');
209
248
  const targetPath = path_1.default.resolve(appPath, 'next.config.mjs');
210
249
  const handoffWebsocketPort = (_h = (_g = handoff.config.app.ports) === null || _g === void 0 ? void 0 : _g.websocket) !== null && _h !== void 0 ? _h : 3001;
@@ -212,6 +251,8 @@ const initializeProjectApp = (handoff_1, ...args_1) => __awaiter(void 0, [handof
212
251
  const escapedProjectId = escapeForSingleQuotedJsString(handoffProjectId);
213
252
  const escapedWorkingPath = escapeForSingleQuotedJsString(handoffWorkingPath);
214
253
  const escapedModulePath = escapeForSingleQuotedJsString(handoffModulePath);
254
+ // Baked here so the template and the assembly step use the same root.
255
+ const escapedTracingRoot = escapeForSingleQuotedJsString((0, exports.resolveRegistryTracingRoot)(handoff));
215
256
  const escapedExportPath = escapeForSingleQuotedJsString(handoffExportPath);
216
257
  const escapedWebsocketPort = escapeForSingleQuotedJsString(String(handoffWebsocketPort));
217
258
  // Resolved runtime mode + registry connection inputs (names only) baked into the Next bundle so the
@@ -220,29 +261,47 @@ const initializeProjectApp = (handoff_1, ...args_1) => __awaiter(void 0, [handof
220
261
  const escapedMcpEnabled = escapeForSingleQuotedJsString(String((0, config_1.isMcpEnabled)(handoff.config)));
221
262
  const escapedRegistryDriver = escapeForSingleQuotedJsString((0, driver_1.resolveRegistryDriver)(handoff.config));
222
263
  const escapedDatabaseUrlEnv = escapeForSingleQuotedJsString((0, driver_1.resolveDatabaseUrlEnv)(handoff.config));
223
- // Asset storage selection baked (provider + module + env-var names + non-secret options JSON).
264
+ // Asset storage selection baked (provider + module + options JSON with `{ $env }` references).
224
265
  const assetStorage = (0, resolve_1.resolveAssetStorageFromConfig)(handoff.config);
225
- const escapedAssetStorageAdapter = escapeForSingleQuotedJsString(assetStorage.adapterKind);
266
+ const escapedAssetStorageProvider = escapeForSingleQuotedJsString(assetStorage.provider);
226
267
  const escapedAssetStorageModule = escapeForSingleQuotedJsString((_j = assetStorage.module) !== null && _j !== void 0 ? _j : '');
227
- const escapedAssetStorageTokenEnv = escapeForSingleQuotedJsString(assetStorage.tokenEnv);
228
268
  const escapedAssetStorageMaxInline = escapeForSingleQuotedJsString(String(assetStorage.maxInlineBytes));
229
269
  const escapedAssetStorageOptions = escapeForSingleQuotedJsString(JSON.stringify((_k = assetStorage.options) !== null && _k !== void 0 ? _k : {}));
270
+ const email = (0, resolve_2.resolveEmailFromConfig)(handoff.config);
271
+ const escapedEmailProvider = escapeForSingleQuotedJsString((_l = email.provider) !== null && _l !== void 0 ? _l : '');
272
+ const escapedEmailFrom = escapeForSingleQuotedJsString((_m = email.from) !== null && _m !== void 0 ? _m : '');
273
+ const escapedEmailModule = escapeForSingleQuotedJsString((_o = email.module) !== null && _o !== void 0 ? _o : '');
274
+ const escapedEmailOptions = escapeForSingleQuotedJsString(JSON.stringify(email.options));
275
+ // AI assistant selection baked (enabled flag + declared connections + default model). Connections
276
+ // carry env-var names and non-secret options only; keys are read from their named env var at
277
+ // request time, and a deployment can extend or replace the list through HANDOFF_AI_CONNECTIONS.
278
+ const ai = (0, connections_1.resolveAiFromConfig)(handoff.config);
279
+ const escapedAiEnabled = escapeForSingleQuotedJsString(String(ai.enabled));
280
+ const escapedAiConnections = escapeForSingleQuotedJsString(JSON.stringify(ai.connections));
281
+ const escapedAiDefaultModel = escapeForSingleQuotedJsString((_p = ai.defaultModel) !== null && _p !== void 0 ? _p : '');
230
282
  const placeholderValues = {
231
283
  '%HANDOFF_PROJECT_ID%': escapedProjectId,
232
284
  '%HANDOFF_APP_BASE_PATH%': escapedAppBasePath,
233
285
  '%HANDOFF_WORKING_PATH%': escapedWorkingPath,
234
286
  '%HANDOFF_MODULE_PATH%': escapedModulePath,
287
+ '%HANDOFF_TRACING_ROOT%': escapedTracingRoot,
235
288
  '%HANDOFF_EXPORT_PATH%': escapedExportPath,
236
289
  '%HANDOFF_WEBSOCKET_PORT%': escapedWebsocketPort,
237
290
  '%HANDOFF_RUNTIME_MODE%': escapedRuntimeMode,
238
291
  '%HANDOFF_MCP_ENABLED%': escapedMcpEnabled,
239
292
  '%HANDOFF_REGISTRY_DRIVER%': escapedRegistryDriver,
240
293
  '%HANDOFF_REGISTRY_DATABASE_URL_ENV%': escapedDatabaseUrlEnv,
241
- '%HANDOFF_ASSET_STORAGE_ADAPTER%': escapedAssetStorageAdapter,
294
+ '%HANDOFF_ASSET_STORAGE_PROVIDER%': escapedAssetStorageProvider,
242
295
  '%HANDOFF_ASSET_STORAGE_MODULE%': escapedAssetStorageModule,
243
- '%HANDOFF_ASSET_STORAGE_TOKEN_ENV%': escapedAssetStorageTokenEnv,
244
296
  '%HANDOFF_ASSET_STORAGE_MAX_INLINE_BYTES%': escapedAssetStorageMaxInline,
245
297
  '%HANDOFF_ASSET_STORAGE_OPTIONS%': escapedAssetStorageOptions,
298
+ '%HANDOFF_EMAIL_PROVIDER%': escapedEmailProvider,
299
+ '%HANDOFF_EMAIL_FROM%': escapedEmailFrom,
300
+ '%HANDOFF_EMAIL_MODULE%': escapedEmailModule,
301
+ '%HANDOFF_EMAIL_OPTIONS%': escapedEmailOptions,
302
+ '%HANDOFF_AI_ENABLED%': escapedAiEnabled,
303
+ '%HANDOFF_AI_BAKED_CONNECTIONS%': escapedAiConnections,
304
+ '%HANDOFF_AI_DEFAULT_MODEL%': escapedAiDefaultModel,
246
305
  };
247
306
  let nextConfigContent = yield fs_extra_1.default.readFile(nextConfigPath, 'utf-8');
248
307
  for (const [placeholder, value] of Object.entries(placeholderValues)) {
@@ -293,20 +352,13 @@ const buildApp = (handoff_1, ...args_1) => __awaiter(void 0, [handoff_1, ...args
293
352
  yield (0, static_export_1.materializeDocsReadModel)(handoff, exportDir);
294
353
  // Final assembly branches on the resolved package. `vercel` lays the materialized export
295
354
  // under `.vercel/output/static/` at the repo root (not the sites directory) — a hard Vercel
296
- // constraint; `standalone` keeps writing the `out/<projectId>` export.
355
+ // constraint; `standalone` keeps writing the `out/static` export.
297
356
  if (resolvedPackage === 'vercel') {
298
357
  yield (0, vercel_output_1.writeStaticVercelOutput)(handoff, exportDir);
299
358
  return;
300
359
  }
301
- // Ensure output root directory exists
302
- const outputRoot = path_1.default.resolve(handoff.workingPath, handoff.sitesDirectory);
303
- yield fs_extra_1.default.ensureDir(outputRoot);
304
- // Clean the project output directory (if exists)
305
- const output = path_1.default.resolve(outputRoot, handoff.getProjectId());
306
- if (fs_extra_1.default.existsSync(output)) {
307
- yield fs_extra_1.default.remove(output);
308
- }
309
- // Copy the build files into the project output directory
360
+ const output = (0, exports.getStaticBuildOutputPath)(handoff);
361
+ yield fs_extra_1.default.remove(output);
310
362
  yield fs_extra_1.default.copy(exportDir, output);
311
363
  });
312
364
  /**
@@ -337,7 +389,28 @@ const findStandaloneServerDir = (root) => {
337
389
  * Write a deployment guide next to the packaged registry app describing how to run it on Vercel,
338
390
  * a custom Node server, or in a container, and which env vars it requires at deploy time.
339
391
  */
340
- const writeRegistryDeploymentReadme = (outputRoot, entryRelativePath, databaseUrlEnv) => __awaiter(void 0, void 0, void 0, function* () {
392
+ const writeRegistryDeploymentReadme = (outputRoot, entryRelativePath, databaseUrlEnv, ai, email, assetStorage) => __awaiter(void 0, void 0, void 0, function* () {
393
+ const serviceKeyEnvs = [...new Set(ai.connections.map((connection) => connection.apiKeyEnv).filter(Boolean))];
394
+ const aiOptionEnvs = [...new Set(ai.connections.flatMap((connection) => { var _a; return (0, from_env_1.envReferenceNames)((_a = connection.options) !== null && _a !== void 0 ? _a : {}); }))];
395
+ const needsKeySecret = ai.connections.some((connection) => connection.credential === 'user');
396
+ const aiSection = !ai.enabled
397
+ ? ''
398
+ : `
399
+ ## AI assistant
400
+
401
+ This build serves the docs assistant at \`/api/ai/*\`.
402
+
403
+ ${serviceKeyEnvs.map((name) => `- \`${name}\` — API key for a declared AI connection, read at request time.`).join('\n') || '- No service-key connection is declared, so no provider key is required here.'}
404
+ ${aiOptionEnvs.map((name) => `- \`${name}\` — read by a custom AI provider at request time.\n`).join('')}${needsKeySecret ? '- `HANDOFF_AI_KEY_SECRET` — a long, random secret that encrypts the API keys readers save for themselves. Readers cannot save a key without it.\n' : ''}
405
+ \`HANDOFF_AI_CONNECTIONS\` optionally carries a JSON array of connections, read at request time and
406
+ merged over the list this build baked in, keyed by \`id\`. Use it to add or repoint a provider
407
+ without rebuilding.
408
+ `;
409
+ const assetStorageEnvs = assetStorage.provider === 'custom' ? (0, from_env_1.envReferenceNames)(assetStorage.options) : [];
410
+ const emailEnvs = (0, from_env_1.envReferenceNames)(email.options);
411
+ const emailSection = !email.from
412
+ ? 'Email delivery is off because `runtime.registry.email.from` is not set. Invitation links are shown once to an administrator for manual delivery.'
413
+ : `Invitation and password-reset emails are sent from \`${email.from}\` through the \`${email.provider}\` provider.${emailEnvs.length ? ` The provider reads ${emailEnvs.map((name) => `\`${name}\``).join(', ')} at request time.` : ''} Until the provider's settings have values, invitation links are shown once to an administrator for manual delivery.`;
341
414
  const readme = `# Handoff Registry App
342
415
 
343
416
  A self-contained, deployable Next.js **standalone** build produced by
@@ -354,10 +427,9 @@ and \`.next/static/\` already copied alongside so the server serves them.
354
427
  - \`${databaseUrlEnv}\` — PostgreSQL/Neon connection string, read at request time.
355
428
  - \`AUTH_SECRET\` — a long, random secret used to sign browser sessions.
356
429
  - \`AUTH_URL\` — the canonical public registry URL, including the configured base path.
357
-
358
- Optional email delivery uses \`RESEND_API_KEY\` and \`AUTH_FROM_EMAIL\`. Without them, invitation
359
- links are shown once to an administrator for manual delivery.
360
-
430
+ ${assetStorageEnvs.map((name) => `- \`${name}\` — read by the custom asset storage provider at request time.\n`).join('')}
431
+ ${emailSection}
432
+ ${aiSection}
361
433
  ## Database migrations
362
434
 
363
435
  Apply migrations from the \`handoff-app\` CLI as a controlled release step (e.g. in your CI/CD
@@ -426,8 +498,8 @@ For multi-instance self-hosting, front \`/_next/static\` with a CDN and configur
426
498
 
427
499
  ## Deploying to Vercel
428
500
 
429
- Vercel does **not** use \`output: 'standalone'\` — it ignores that setting and builds with its own
430
- adapter, so this pre-built bundle is **not** the artifact you deploy there. For Vercel, deploy the
501
+ Vercel does **not** use \`output: 'standalone'\` — it ignores that setting and runs its
502
+ own build, so this pre-built bundle is **not** the artifact you deploy there. For Vercel, deploy the
431
503
  registry app via Vercel's Git/CLI source build (it produces and serves the dynamic app itself) and
432
504
  supply the same environment variables above as project env vars. Use this standalone bundle for
433
505
  containers, custom Node servers, and other non-Vercel hosts.
@@ -438,30 +510,25 @@ containers, custom Node servers, and other non-Vercel hosts.
438
510
  * Resolve the set of npm packages the packaged registry app must be able to `require`/`import` at
439
511
  * runtime, given the configured database driver. `next`/`react`/`react-dom` run the server and React
440
512
  * runtime; `drizzle-orm` backs both the request-time DB client and the migration runner; the MCP SDK
441
- * (and its `zod` peer) is only needed when `/api/mcp/` is enabled; the driver package is
442
- * driver-specific (the Neon serverless driver also needs `ws` for its Node WebSocket transport).
513
+ * is only needed when `/api/mcp/` is enabled; the driver package is
514
+ * driver-specific (the Neon serverless driver also needs `ws` for its Node WebSocket transport); the
515
+ * SMTP email provider needs `nodemailer`.
443
516
  */
444
517
  const getRequiredRegistryRuntimeModules = (handoff) => {
445
- var _a;
446
518
  const base = ['next', 'next-auth', 'react', 'react-dom', 'drizzle-orm'];
447
- if ((0, config_1.isMcpEnabled)(handoff.config)) {
448
- base.push('@modelcontextprotocol/sdk', 'zod');
519
+ const ai = (0, connections_1.resolveAiFromConfig)(handoff.config);
520
+ // The assistant runs the MCP tools in-process, so it needs the MCP SDK whether or not the HTTP
521
+ // endpoint is served.
522
+ if ((0, config_1.isMcpEnabled)(handoff.config) || ai.enabled) {
523
+ base.push('@modelcontextprotocol/sdk');
524
+ }
525
+ if (ai.enabled) {
526
+ base.push('ai', '@ai-sdk/openai-compatible');
449
527
  }
450
528
  const driver = (0, driver_1.resolveRegistryDriver)(handoff.config);
451
529
  const driverModules = driver === 'neon' ? ['@neondatabase/serverless', 'ws'] : ['pg'];
452
- // Asset-storage SDKs the deployed registry must be able to load at request time. The pre-packaged
453
- // Vercel Blob adapter needs `@vercel/blob`; a custom adapter may declare its installed SDK
454
- // package(s) via `assetStorage.options.sdkModules` so the trace/assertion covers them.
455
- const assetStorage = (0, resolve_1.resolveAssetStorageFromConfig)(handoff.config);
456
- const storage = [];
457
- if (assetStorage.adapterKind === 'vercel-blob') {
458
- storage.push('@vercel/blob');
459
- }
460
- const sdkModules = (_a = assetStorage.options) === null || _a === void 0 ? void 0 : _a.sdkModules;
461
- if (Array.isArray(sdkModules)) {
462
- storage.push(...sdkModules.filter((mod) => typeof mod === 'string'));
463
- }
464
- return [...base, ...driverModules, ...storage];
530
+ const email = (0, resolve_2.resolveEmailFromConfig)(handoff.config).provider === 'smtp' ? ['nodemailer'] : [];
531
+ return [...base, ...driverModules, ...email];
465
532
  };
466
533
  /**
467
534
  * Build-time guard against the empty-`node_modules` regression (the artifact would otherwise boot-fail
@@ -488,6 +555,58 @@ const assertRegistryRuntimeDepsTraced = (handoff, entryDir) => {
488
555
  `must be the nearest ancestor of *both* the staged app and the resolved node_modules. Ensure ` +
489
556
  `${missing.join(', ')} are installed and resolvable from "${handoff.workingPath}".`);
490
557
  };
558
+ const getCustomServerModules = (handoff) => {
559
+ const assetStorage = (0, resolve_1.resolveAssetStorageFromConfig)(handoff.config);
560
+ const email = (0, resolve_2.resolveEmailFromConfig)(handoff.config);
561
+ const modules = [
562
+ assetStorage.provider === 'custom' ? assetStorage.module : undefined,
563
+ email.provider === 'custom' ? email.module : undefined,
564
+ ...(0, connections_1.resolveAiFromConfig)(handoff.config).connections.map((connection) => connection.module),
565
+ ];
566
+ return [...new Set(modules.filter((module) => Boolean(module)))];
567
+ };
568
+ /**
569
+ * Copy the custom server modules and every file they import into the bundle. The Next trace cannot
570
+ * follow their variable dynamic import.
571
+ */
572
+ const copyCustomServerModules = (handoff, tracingRoot, entryDir) => __awaiter(void 0, void 0, void 0, function* () {
573
+ const modules = getCustomServerModules(handoff);
574
+ if (modules.length === 0)
575
+ return;
576
+ const missing = modules.filter((module) => !fs_extra_1.default.existsSync(path_1.default.resolve(handoff.workingPath, module)));
577
+ if (missing.length > 0) {
578
+ throw new HandoffBuildError(`A custom server module does not exist: ${missing.map((module) => `"${module}"`).join(', ')}.`);
579
+ }
580
+ const files = new Set();
581
+ for (const module of modules) {
582
+ const { fileList } = yield (0, nft_1.nodeFileTrace)([path_1.default.resolve(handoff.workingPath, module)], { base: tracingRoot });
583
+ const outside = [...fileList].filter((file) => file.startsWith('..'));
584
+ if (outside.length > 0) {
585
+ throw new HandoffBuildError(`The custom server module "${module}" imports files outside the build's tracing root "${tracingRoot}": ${outside.join(', ')}. ` +
586
+ `Install the packages it imports so that they resolve from "${handoff.workingPath}".`);
587
+ }
588
+ if ([...fileList].some((file) => file.split(path_1.default.sep).join('/').endsWith('node_modules/handoff-app/dist/index.js'))) {
589
+ throw new HandoffBuildError(`The custom server module "${module}" imports "handoff-app", which loads the whole CLI. Import the define helpers from "handoff-app/providers".`);
590
+ }
591
+ fileList.forEach((file) => files.add(file));
592
+ }
593
+ for (const file of files) {
594
+ const source = path_1.default.join(tracingRoot, file);
595
+ const target = path_1.default.join(entryDir, file);
596
+ if (yield fs_extra_1.default.lstat(target).then(() => true, () => false))
597
+ continue;
598
+ const stat = yield fs_extra_1.default.lstat(source);
599
+ yield fs_extra_1.default.ensureDir(path_1.default.dirname(target));
600
+ if (stat.isSymbolicLink()) {
601
+ // Point the link at the bundle copy of its target, not at the build machine.
602
+ const linked = path_1.default.join(entryDir, path_1.default.relative(tracingRoot, yield fs_extra_1.default.realpath(source)));
603
+ yield fs_extra_1.default.symlink(path_1.default.relative(path_1.default.dirname(target), linked), target);
604
+ }
605
+ else if (stat.isFile()) {
606
+ yield fs_extra_1.default.copy(source, target);
607
+ }
608
+ }
609
+ });
491
610
  /**
492
611
  * Lay the Next `standalone` build out as the documented standalone layout under `entryDir`: copy the
493
612
  * traced bundle, flatten the package-rooted server entry up to `entryDir`, and copy the untraced
@@ -499,14 +618,11 @@ const assertRegistryRuntimeDepsTraced = (handoff, entryDir) => {
499
618
  */
500
619
  const assembleRegistryStandalone = (handoff, appPath, standaloneRoot, entryDir) => __awaiter(void 0, void 0, void 0, function* () {
501
620
  yield fs_extra_1.default.copy(standaloneRoot, entryDir, { overwrite: true });
502
- // Locate the server entry within the assembled package. Standalone tracing is rooted at the
503
- // consumer project root (`workingPath`, see next.config.mjs), so every traced file is laid out at
504
- // its path *relative to `workingPath`*. The staged app lives at `<workingPath>/node_modules/
505
- // handoff-app/.handoff/<projectId>`, so the server entry lands nested at that same relative path
506
- // *underneath* the traced `node_modules`. Resolve it deterministically; the recursive fallback
507
- // skips `node_modules` (so it cannot find this nested entry) and only covers a hypothetical
508
- // non-nested layout.
509
- const relAppDir = path_1.default.relative(handoff.workingPath, appPath);
621
+ // Locate the server entry in the assembled package. Traced files keep their path relative to the
622
+ // tracing root, so the server entry nests under the traced `node_modules`. The recursive fallback
623
+ // skips `node_modules`, so it covers only a non-nested layout.
624
+ const tracingRoot = (0, exports.resolveRegistryTracingRoot)(handoff);
625
+ const relAppDir = path_1.default.relative(tracingRoot, appPath);
510
626
  let serverDir = path_1.default.resolve(entryDir, relAppDir);
511
627
  if (!fs_extra_1.default.existsSync(path_1.default.join(serverDir, 'server.js'))) {
512
628
  const located = findStandaloneServerDir(entryDir);
@@ -526,7 +642,7 @@ const assembleRegistryStandalone = (handoff, appPath, standaloneRoot, entryDir)
526
642
  // so we must NOT remove the first path segment (`node_modules`) — that would delete every traced
527
643
  // dependency. Remove only the `.handoff` staging container (the parent of the staged app dir),
528
644
  // leaving `node_modules/` and the real `node_modules/handoff-app/` package intact.
529
- const stagingContainer = path_1.default.resolve(entryDir, path_1.default.relative(handoff.workingPath, path_1.default.dirname(appPath)));
645
+ const stagingContainer = path_1.default.resolve(entryDir, path_1.default.relative(tracingRoot, path_1.default.dirname(appPath)));
530
646
  if (path_1.default.basename(stagingContainer) === '.handoff' && fs_extra_1.default.existsSync(stagingContainer)) {
531
647
  yield fs_extra_1.default.remove(stagingContainer);
532
648
  }
@@ -549,6 +665,17 @@ const assembleRegistryStandalone = (handoff, appPath, standaloneRoot, entryDir)
549
665
  yield fs_extra_1.default.copy(src, path_1.default.join(entryDir, configFile), { overwrite: true });
550
666
  }
551
667
  }
668
+ // Next copies the `package.json` of the tracing root into the bundle without change. The generated
669
+ // `server.js` is CommonJS, so an ESM manifest stops the bundle from starting. That manifest also
670
+ // names scripts and dev dependencies that the bundle does not ship.
671
+ const manifestPath = path_1.default.join(entryDir, 'package.json');
672
+ const inheritedManifest = (yield fs_extra_1.default.pathExists(manifestPath))
673
+ ? yield fs_extra_1.default.readJson(manifestPath).catch(() => ({}))
674
+ : {};
675
+ delete inheritedManifest.scripts;
676
+ delete inheritedManifest.devDependencies;
677
+ yield fs_extra_1.default.writeJson(manifestPath, Object.assign(Object.assign({}, inheritedManifest), { type: 'commonjs' }), { spaces: 2 });
678
+ yield copyCustomServerModules(handoff, tracingRoot, entryDir);
552
679
  // Fail loudly at build time if the trace did not capture the runtime deps, rather than shipping a
553
680
  // broken artifact that only crashes on an isolated deploy. Covers both deliverables (this is the
554
681
  // shared assembly step for `standalone` and `vercel`).
@@ -618,8 +745,10 @@ const buildRegistryApp = (handoff_1, ...args_1) => __awaiter(void 0, [handoff_1,
618
745
  // files are not traced and must be copied alongside the server entry.
619
746
  const standaloneRoot = path_1.default.resolve(appPath, '.next', 'standalone');
620
747
  if (!fs_extra_1.default.existsSync(standaloneRoot)) {
621
- throw new Error(`Registry build did not produce a standalone bundle at "${standaloneRoot}". Ensure the registry ` +
622
- 'build target sets Next `output: "standalone"`.');
748
+ throw new Error(`Registry build did not produce a standalone bundle at "${standaloneRoot}". Next emits one only ` +
749
+ `when the app being built sits inside \`outputFileTracingRoot\` (resolved here to ` +
750
+ `"${(0, exports.resolveRegistryTracingRoot)(handoff)}"), so check that the staged app at "${appPath}" is ` +
751
+ 'inside that root, and that the registry build target still sets Next `output: "standalone"`.');
623
752
  }
624
753
  // Final assembly branches on the resolved package. Both lay out
625
754
  // the same traced standalone bundle; they differ only in where it lands and how it is wrapped.
@@ -636,7 +765,7 @@ const buildRegistryApp = (handoff_1, ...args_1) => __awaiter(void 0, [handoff_1,
636
765
  // `handoff-app` source/CLI (`handoff-app db:migrate`) as a controlled release step (e.g. CI/CD),
637
766
  // never from the deployed bundle. See the generated README for the migrate-then-deploy flow.
638
767
  const entryRelativePath = path_1.default.relative(output, serverEntry).split(path_1.default.sep).join('/');
639
- yield writeRegistryDeploymentReadme(output, entryRelativePath, (0, driver_1.resolveDatabaseUrlEnv)(handoff.config));
768
+ yield writeRegistryDeploymentReadme(output, entryRelativePath, (0, driver_1.resolveDatabaseUrlEnv)(handoff.config), (0, connections_1.resolveAiFromConfig)(handoff.config), (0, resolve_2.resolveEmailFromConfig)(handoff.config), (0, resolve_1.resolveAssetStorageFromConfig)(handoff.config));
640
769
  logger_1.Logger.success(`Packaged registry app at ${output} (start: \`node ${entryRelativePath}\`, migrate: \`handoff-app db:migrate\`).`);
641
770
  });
642
771
  /**
@@ -646,11 +775,11 @@ const buildRegistryApp = (handoff_1, ...args_1) => __awaiter(void 0, [handoff_1,
646
775
  const watchApp = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
647
776
  var _a, _b, _c, _d;
648
777
  // Build the shared/global artifacts first so component/pattern preview HTML references them only
649
- // when present, then process components with caching enabled (which skips
650
- // rebuilding components whose source files haven't changed).
778
+ // when present, then process components. The build cache skips the components whose files did not
779
+ // change.
651
780
  yield (0, javascript_1.buildMainJS)(handoff);
652
781
  yield (0, css_1.buildMainCss)(handoff);
653
- yield (0, builder_1.default)(handoff, undefined, undefined, { useCache: true });
782
+ yield (0, builder_1.default)(handoff);
654
783
  // Build patterns after components are ready
655
784
  yield (0, patterns_1.buildPatterns)(handoff);
656
785
  const appPath = yield initializeProjectApp(handoff);
@@ -19,6 +19,7 @@ const config_1 = require("../config");
19
19
  const resolve_1 = require("../registry/asset-storage/resolve");
20
20
  const connection_1 = require("../registry/connection");
21
21
  const driver_1 = require("../registry/db/driver");
22
+ const resolve_2 = require("../registry/email/resolve");
22
23
  const paths_1 = require("./paths");
23
24
  /**
24
25
  * Publishes the tokens API files to the public directory.
@@ -71,12 +72,12 @@ const buildServerRuntimeConfig = (config, modeOverride) => {
71
72
  databaseUrlEnv: (0, driver_1.resolveDatabaseUrlEnv)(config),
72
73
  },
73
74
  assetStorage: {
74
- adapter: assetStorage.adapterKind,
75
+ provider: assetStorage.provider,
75
76
  module: assetStorage.module,
76
- tokenEnv: assetStorage.tokenEnv,
77
77
  maxInlineBytes: assetStorage.maxInlineBytes,
78
78
  options: assetStorage.options,
79
79
  },
80
+ email: (0, resolve_2.resolveEmailFromConfig)(config),
80
81
  };
81
82
  };
82
83
  /**
@@ -1,17 +1,5 @@
1
1
  import Handoff from '..';
2
- /**
3
- * Gets the working public directory path for a given handoff instance.
4
- * Checks for both project-specific and default public directories.
5
- *
6
- * @param handoff - The handoff instance containing working path and figma project configuration
7
- * @returns The resolved path to the public directory if it exists, null otherwise
8
- */
9
2
  export declare const getWorkingPublicPath: (handoff: Handoff) => string | null;
10
- /**
11
- * Gets the application path for a given handoff instance.
12
- * @param handoff - The handoff instance containing module path and figma project configuration
13
- * @returns The resolved path to the application directory
14
- */
15
3
  export declare const getAppPath: (handoff: Handoff) => string;
16
4
  /**
17
5
  * Copy the public dir from the working dir to the module dir.
@@ -15,28 +15,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.syncPublicFiles = exports.getAppPath = exports.getWorkingPublicPath = void 0;
16
16
  const fs_extra_1 = __importDefault(require("fs-extra"));
17
17
  const path_1 = __importDefault(require("path"));
18
- /**
19
- * Gets the working public directory path for a given handoff instance.
20
- * Checks for both project-specific and default public directories.
21
- *
22
- * @param handoff - The handoff instance containing working path and figma project configuration
23
- * @returns The resolved path to the public directory if it exists, null otherwise
24
- */
25
18
  const getWorkingPublicPath = (handoff) => {
26
- const paths = [path_1.default.resolve(handoff.workingPath, `public-${handoff.getProjectId()}`), path_1.default.resolve(handoff.workingPath, `public`)];
27
- for (const p of paths) {
28
- if (fs_extra_1.default.existsSync(p)) {
29
- return p;
30
- }
31
- }
32
- return null;
19
+ const publicPath = path_1.default.resolve(handoff.workingPath, 'public');
20
+ return fs_extra_1.default.existsSync(publicPath) ? publicPath : null;
33
21
  };
34
22
  exports.getWorkingPublicPath = getWorkingPublicPath;
35
- /**
36
- * Gets the application path for a given handoff instance.
37
- * @param handoff - The handoff instance containing module path and figma project configuration
38
- * @returns The resolved path to the application directory
39
- */
40
23
  const getAppPath = (handoff) => {
41
24
  return path_1.default.resolve(handoff.modulePath, '.handoff', `${handoff.getProjectId()}`);
42
25
  };
@@ -51,7 +34,7 @@ const mirrorDirectory = (sourcePath, destinationPath) => __awaiter(void 0, void
51
34
  yield fs_extra_1.default.copy(sourcePath, destinationPath, { overwrite: true });
52
35
  });
53
36
  /**
54
- * Copy the fetch-produced asset bundles (`exported/<id>/{collection}.zip`) into the app's public
37
+ * Copy the fetch-produced asset bundles (`exported/{collection}.zip`) into the app's public
55
38
  * root, where the docs read API serves them at `/api/docs/assets/{collection}/{collection}.zip`.
56
39
  * `fetch` writes these bundles, but `build`/`start` cleans and re-stages `.handoff` without them, so
57
40
  * the download links 404 until the next fetch. Copying them here keeps the route working in workspace
@@ -184,7 +184,7 @@ exports.materializeDocsReadModel = materializeDocsReadModel;
184
184
  * Write the whole-collection asset bundles to the asset route
185
185
  * (`api/docs/assets/{collection}/{collection}.zip`), the same URL the docs read API serves in
186
186
  * registry mode, so the "Download" links work identically on a static host. The bundles come from the
187
- * fetch output (`exported/<id>/{collection}.zip`); `next build` never re-creates them and the
187
+ * fetch output (`exported/{collection}.zip`); `next build` never re-creates them and the
188
188
  * fetch-time copy into `public/` is wiped by the build's clean+restage, so we copy them here again.
189
189
  * Projects with no logos/icons have no bundles, so those are skipped rather than treated as an error.
190
190
  */
@@ -7,7 +7,7 @@ export declare const getVercelOutputPath: (handoff: Handoff) => string;
7
7
  * no functions, no database.
8
8
  *
9
9
  * @param exportDir Absolute path of the materialized static export (the same content the static
10
- * target writes to `out/<projectId>`).
10
+ * target writes to `out/static`).
11
11
  */
12
12
  export declare const writeStaticVercelOutput: (handoff: Handoff, exportDir: string) => Promise<void>;
13
13
  /**