handoff-app 1.2.2-6 → 2.0.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 (349) hide show
  1. package/Changelog.md +37 -15
  2. package/README.md +372 -118
  3. package/dist/app-builder/build.d.ts +41 -2
  4. package/dist/app-builder/build.js +455 -18
  5. package/dist/app-builder/client-config.d.ts +15 -2
  6. package/dist/app-builder/client-config.js +43 -4
  7. package/dist/app-builder/config-diff/snapshot.d.ts +1 -6
  8. package/dist/app-builder/config-diff/snapshot.js +2 -9
  9. package/dist/app-builder/index.d.ts +1 -1
  10. package/dist/app-builder/index.js +3 -1
  11. package/dist/app-builder/nav-shell.d.ts +23 -0
  12. package/dist/app-builder/nav-shell.js +80 -0
  13. package/dist/app-builder/static-export.d.ts +17 -0
  14. package/dist/app-builder/static-export.js +181 -0
  15. package/dist/app-builder/vercel-output.d.ts +39 -0
  16. package/dist/app-builder/vercel-output.js +259 -0
  17. package/dist/app-builder/watchers/utils.d.ts +2 -0
  18. package/dist/app-builder/watchers.d.ts +14 -4
  19. package/dist/app-builder/watchers.js +123 -28
  20. package/dist/app-builder/websocket.js +1 -1
  21. package/dist/artifacts/index.d.ts +2 -0
  22. package/dist/artifacts/index.js +13 -0
  23. package/dist/artifacts/types.d.ts +136 -0
  24. package/dist/artifacts/types.js +24 -0
  25. package/dist/artifacts/url.d.ts +49 -0
  26. package/dist/artifacts/url.js +86 -0
  27. package/dist/cache/build-cache.js +48 -4
  28. package/dist/commands/build/app.js +1 -2
  29. package/dist/commands/build/index.d.ts +17 -0
  30. package/dist/commands/build/index.js +66 -0
  31. package/dist/commands/checkout/index.d.ts +24 -0
  32. package/dist/commands/checkout/index.js +71 -0
  33. package/dist/commands/db/migrate.d.ts +11 -0
  34. package/dist/commands/db/migrate.js +33 -0
  35. package/dist/commands/index.js +8 -0
  36. package/dist/commands/publish/index.d.ts +21 -0
  37. package/dist/commands/publish/index.js +68 -0
  38. package/dist/config/defaults.js +15 -1
  39. package/dist/config/loader.js +6 -1
  40. package/dist/config/normalizers/declaration.js +4 -1
  41. package/dist/config/normalizers/page.d.ts +18 -0
  42. package/dist/config/normalizers/page.js +34 -0
  43. package/dist/config/normalizers/pattern.js +7 -1
  44. package/dist/config/runtime.d.ts +4 -0
  45. package/dist/config/runtime.js +53 -5
  46. package/dist/index.d.ts +65 -1
  47. package/dist/index.js +123 -3
  48. package/dist/nav/index.d.ts +83 -0
  49. package/dist/nav/index.js +9 -0
  50. package/dist/nav/registry.d.ts +15 -0
  51. package/dist/nav/registry.js +42 -0
  52. package/dist/nav/resolver.d.ts +8 -0
  53. package/dist/nav/resolver.js +192 -0
  54. package/dist/nav/workspace.d.ts +18 -0
  55. package/dist/nav/workspace.js +103 -0
  56. package/dist/pipeline/styles.js +7 -35
  57. package/dist/pipeline/token-transformers.d.ts +45 -0
  58. package/dist/pipeline/token-transformers.js +71 -0
  59. package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +17 -0
  60. package/dist/registry/asset-storage/adapters/vercel-blob.js +87 -0
  61. package/dist/registry/asset-storage/define.d.ts +10 -0
  62. package/dist/registry/asset-storage/define.js +13 -0
  63. package/dist/registry/asset-storage/resolve.d.ts +45 -0
  64. package/dist/registry/asset-storage/resolve.js +36 -0
  65. package/dist/registry/asset-storage/types.d.ts +51 -0
  66. package/dist/registry/asset-storage/types.js +9 -0
  67. package/dist/registry/assets/layout.d.ts +22 -0
  68. package/dist/registry/assets/layout.js +30 -0
  69. package/dist/registry/assets/sets.d.ts +18 -0
  70. package/dist/registry/assets/sets.js +44 -0
  71. package/dist/registry/assets/transfer.d.ts +45 -0
  72. package/dist/registry/assets/transfer.js +11 -0
  73. package/dist/registry/checkout/assets.d.ts +16 -0
  74. package/dist/registry/checkout/assets.js +150 -0
  75. package/dist/registry/checkout/index.d.ts +33 -0
  76. package/dist/registry/checkout/index.js +610 -0
  77. package/dist/registry/checkout/tokens.d.ts +15 -0
  78. package/dist/registry/checkout/tokens.js +171 -0
  79. package/dist/registry/client.d.ts +61 -0
  80. package/dist/registry/client.js +211 -0
  81. package/dist/registry/connection.d.ts +30 -0
  82. package/dist/registry/connection.js +36 -0
  83. package/dist/registry/db/adapter.d.ts +43 -0
  84. package/dist/registry/db/adapter.js +59 -0
  85. package/dist/registry/db/client.d.ts +35 -0
  86. package/dist/registry/db/client.js +94 -0
  87. package/dist/registry/db/index.d.ts +5 -0
  88. package/dist/registry/db/index.js +24 -0
  89. package/dist/registry/db/migrate.d.ts +23 -0
  90. package/dist/registry/db/migrate.js +64 -0
  91. package/dist/registry/db/schema.d.ts +4869 -0
  92. package/dist/registry/db/schema.js +347 -0
  93. package/dist/registry/path.d.ts +4 -0
  94. package/dist/registry/path.js +31 -0
  95. package/dist/registry/publish/artifacts.d.ts +16 -0
  96. package/dist/registry/publish/artifacts.js +132 -0
  97. package/dist/registry/publish/assets.d.ts +16 -0
  98. package/dist/registry/publish/assets.js +129 -0
  99. package/dist/registry/publish/errors.d.ts +5 -0
  100. package/dist/registry/publish/errors.js +26 -0
  101. package/dist/registry/publish/index.d.ts +27 -0
  102. package/dist/registry/publish/index.js +126 -0
  103. package/dist/registry/publish/package.d.ts +27 -0
  104. package/dist/registry/publish/package.js +257 -0
  105. package/dist/registry/publish/publish-build.d.ts +12 -0
  106. package/dist/registry/publish/publish-build.js +43 -0
  107. package/dist/registry/publish/tokens.d.ts +18 -0
  108. package/dist/registry/publish/tokens.js +112 -0
  109. package/dist/registry/tokens/sets.d.ts +59 -0
  110. package/dist/registry/tokens/sets.js +109 -0
  111. package/dist/registry/tokens/transfer.d.ts +48 -0
  112. package/dist/registry/tokens/transfer.js +10 -0
  113. package/dist/registry/transfer.d.ts +75 -0
  114. package/dist/registry/transfer.js +11 -0
  115. package/dist/store/filesystem-assets.d.ts +17 -0
  116. package/dist/store/filesystem-assets.js +147 -0
  117. package/dist/store/filesystem.d.ts +76 -0
  118. package/dist/store/filesystem.js +208 -0
  119. package/dist/store/index.d.ts +20 -0
  120. package/dist/store/index.js +38 -0
  121. package/dist/store/registry.d.ts +77 -0
  122. package/dist/store/registry.js +350 -0
  123. package/dist/store/source-files.d.ts +24 -0
  124. package/dist/store/source-files.js +147 -0
  125. package/dist/store/types.d.ts +185 -0
  126. package/dist/store/types.js +12 -0
  127. package/dist/transformers/plugins/csf-render.js +17 -8
  128. package/dist/transformers/plugins/handlebars-previews.js +10 -3
  129. package/dist/transformers/plugins/ssr-render.js +116 -49
  130. package/dist/transformers/preview/component/api.js +3 -1
  131. package/dist/transformers/preview/component/artifacts.js +8 -1
  132. package/dist/transformers/preview/component/builder.js +13 -5
  133. package/dist/transformers/preview/component/css.js +9 -1
  134. package/dist/transformers/preview/component/javascript.d.ts +9 -3
  135. package/dist/transformers/preview/component/javascript.js +45 -17
  136. package/dist/transformers/preview/component/shared-artifacts.d.ts +83 -0
  137. package/dist/transformers/preview/component/shared-artifacts.js +92 -0
  138. package/dist/transformers/preview/component.js +5 -2
  139. package/dist/transformers/preview/pattern/builder.js +28 -9
  140. package/dist/transformers/preview/pattern/html.d.ts +13 -10
  141. package/dist/transformers/preview/pattern/html.js +62 -27
  142. package/dist/transformers/preview/types.d.ts +38 -0
  143. package/dist/transformers/utils/handlebars.d.ts +3 -0
  144. package/dist/transformers/utils/handlebars.js +19 -5
  145. package/dist/types/config.d.ts +118 -25
  146. package/dist/utils/menu-shell.d.ts +60 -0
  147. package/dist/utils/menu-shell.js +221 -0
  148. package/dist/utils/stable-stringify.d.ts +2 -0
  149. package/dist/utils/stable-stringify.js +8 -0
  150. package/docs/cli.md +4 -2
  151. package/drizzle/0000_init_registry_storage.sql +95 -0
  152. package/drizzle/0001_add_review_metadata.sql +2 -0
  153. package/drizzle/0002_add_pages.sql +28 -0
  154. package/drizzle/0003_add_tokens.sql +29 -0
  155. package/drizzle/0004_add_assets.sql +37 -0
  156. package/drizzle/meta/0000_snapshot.json +642 -0
  157. package/drizzle/meta/0001_snapshot.json +654 -0
  158. package/drizzle/meta/0002_snapshot.json +835 -0
  159. package/drizzle/meta/0003_snapshot.json +1023 -0
  160. package/drizzle/meta/0004_snapshot.json +1252 -0
  161. package/drizzle/meta/_journal.json +41 -0
  162. package/drizzle.config.ts +16 -0
  163. package/package.json +10 -1
  164. package/src/app/components/Component/ComponentLists.tsx +102 -102
  165. package/src/app/components/Component/Preview.tsx +3 -2
  166. package/src/app/components/Component/RegistryActions.tsx +150 -0
  167. package/src/app/components/Foundations/DisplayLogo.tsx +5 -3
  168. package/src/app/components/Layout/Header.tsx +21 -3
  169. package/src/app/components/Layout/Main.tsx +11 -3
  170. package/src/app/components/Navigation/MainNav.tsx +4 -2
  171. package/src/app/components/Navigation/MobileNav.tsx +4 -2
  172. package/src/app/components/Navigation/SideNav.tsx +46 -11
  173. package/src/app/components/NotFound.tsx +24 -0
  174. package/src/app/components/context/NavProvider.tsx +71 -0
  175. package/src/app/components/old/Header.tsx +3 -3
  176. package/src/app/components/util/index.ts +203 -380
  177. package/src/app/components/util/useCollectionAssets.ts +125 -0
  178. package/src/app/components/util/useFoundationTokens.ts +67 -0
  179. package/src/app/generated/default-pages.json +1 -0
  180. package/src/app/generated/nav-shell.json +1 -0
  181. package/src/app/lib/api/query.ts +6 -0
  182. package/src/app/lib/api/redact.ts +46 -0
  183. package/src/app/lib/asset-storage.ts +81 -0
  184. package/src/app/lib/docs-api/artifacts.ts +116 -0
  185. package/src/app/lib/docs-api/backend.ts +151 -0
  186. package/src/app/lib/docs-api/errors.ts +52 -0
  187. package/src/app/lib/docs-api/handler.ts +34 -0
  188. package/src/app/lib/docs-api/index.ts +7 -0
  189. package/src/app/lib/docs-api/records.ts +189 -0
  190. package/src/app/lib/docs-api/registry.ts +130 -0
  191. package/src/app/lib/docs-api/runtime-config.ts +173 -0
  192. package/src/app/lib/docs-api/serve.ts +37 -0
  193. package/src/app/lib/docs-api/token-detail.ts +19 -0
  194. package/src/app/lib/registry-api/allowlist.ts +163 -0
  195. package/src/app/lib/registry-api/asset-transfer.ts +363 -0
  196. package/src/app/lib/registry-api/auth.ts +68 -0
  197. package/src/app/lib/registry-api/entity-routes.ts +197 -0
  198. package/src/app/lib/registry-api/errors.ts +51 -0
  199. package/src/app/lib/registry-api/files.ts +135 -0
  200. package/src/app/lib/registry-api/handler.ts +96 -0
  201. package/src/app/lib/registry-api/index.ts +4 -0
  202. package/src/app/lib/registry-api/meta.ts +87 -0
  203. package/src/app/lib/registry-api/revalidate.ts +36 -0
  204. package/src/app/lib/registry-api/store.ts +348 -0
  205. package/src/app/lib/registry-api/token-transfer.ts +231 -0
  206. package/src/app/lib/registry-api/transfer.ts +513 -0
  207. package/src/app/lib/registry-api/validation.ts +74 -0
  208. package/src/app/lib/registry-connection.ts +58 -0
  209. package/src/app/lib/utils.ts +14 -0
  210. package/src/app/next.config.mjs +72 -13
  211. package/src/app/pages/[...slug]/index.tsx +74 -20
  212. package/src/app/pages/_app.tsx +5 -2
  213. package/src/app/pages/api/docs/artifacts/[...artifactPath].ts +16 -0
  214. package/src/app/pages/api/docs/assets/[collection]/[...path].ts +50 -0
  215. package/src/app/pages/api/docs/assets/[collection]/index.ts +22 -0
  216. package/src/app/pages/api/docs/components/[id]/assets/[asset].ts +33 -0
  217. package/src/app/pages/api/docs/components/[id]/inspect/[previewId].ts +20 -0
  218. package/src/app/pages/api/docs/components/[id]/previews/[previewId].ts +21 -0
  219. package/src/app/pages/api/docs/components/[id].ts +24 -0
  220. package/src/app/pages/api/docs/components.json.ts +12 -0
  221. package/src/app/pages/api/docs/nav.json.ts +48 -0
  222. package/src/app/pages/api/docs/patterns/[id]/preview.ts +19 -0
  223. package/src/app/pages/api/docs/patterns/[id].ts +24 -0
  224. package/src/app/pages/api/docs/patterns.json.ts +12 -0
  225. package/src/app/pages/api/docs/tokens/components/[id].ts +30 -0
  226. package/src/app/pages/api/docs/tokens/foundations/[type].ts +32 -0
  227. package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +19 -0
  228. package/src/app/pages/api/registry/components/[id]/files/index.ts +19 -0
  229. package/src/app/pages/api/registry/components/[id].ts +20 -0
  230. package/src/app/pages/api/registry/components/index.ts +19 -0
  231. package/src/app/pages/api/registry/health.ts +20 -0
  232. package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +19 -0
  233. package/src/app/pages/api/registry/patterns/[id]/files/index.ts +19 -0
  234. package/src/app/pages/api/registry/patterns/[id].ts +20 -0
  235. package/src/app/pages/api/registry/patterns/index.ts +19 -0
  236. package/src/app/pages/api/registry/transfer/[kind]/[id].ts +39 -0
  237. package/src/app/pages/api/registry/transfer/assets/[collection].ts +24 -0
  238. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +23 -0
  239. package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +11 -0
  240. package/src/app/pages/api/registry/transfer/assets/index.ts +10 -0
  241. package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +26 -0
  242. package/src/app/pages/api/registry/transfer/tokens/index.ts +11 -0
  243. package/src/app/pages/assets/fonts.tsx +41 -17
  244. package/src/app/pages/assets/index.tsx +2 -2
  245. package/src/app/pages/assets/logos.tsx +2 -2
  246. package/src/app/pages/foundations/colors.tsx +18 -8
  247. package/src/app/pages/foundations/effects.tsx +18 -8
  248. package/src/app/pages/foundations/icons/[name]/index.tsx +26 -14
  249. package/src/app/pages/foundations/icons/index.tsx +13 -9
  250. package/src/app/pages/foundations/index.tsx +1 -1
  251. package/src/app/pages/foundations/logo.tsx +7 -4
  252. package/src/app/pages/foundations/typography.tsx +19 -9
  253. package/src/app/pages/index.tsx +1 -1
  254. package/src/app/pages/system/component/[component]/index.tsx +87 -17
  255. package/src/app/pages/system/index.tsx +13 -4
  256. package/src/app/pages/system/pattern/[pattern]/index.tsx +70 -14
  257. package/src/app/pages/system/tokens/components/[component]/index.tsx +42 -14
  258. package/src/app/pages/system/tokens/foundations/colors.tsx +7 -5
  259. package/src/app/pages/system/tokens/foundations/effects.tsx +7 -5
  260. package/src/app/pages/system/tokens/foundations/typography.tsx +8 -4
  261. package/src/app/tsconfig.json +2 -3
  262. package/src/app-builder/build.ts +555 -11
  263. package/src/app-builder/client-config.ts +61 -4
  264. package/src/app-builder/config-diff/snapshot.ts +1 -11
  265. package/src/app-builder/index.ts +1 -1
  266. package/src/app-builder/nav-shell.ts +74 -0
  267. package/src/app-builder/static-export.ts +215 -0
  268. package/src/app-builder/vercel-output.ts +297 -0
  269. package/src/app-builder/watchers/utils.ts +2 -0
  270. package/src/app-builder/watchers.ts +149 -25
  271. package/src/app-builder/websocket.ts +2 -2
  272. package/src/artifacts/README.md +14 -0
  273. package/src/artifacts/index.ts +17 -0
  274. package/src/artifacts/types.ts +146 -0
  275. package/src/artifacts/url.ts +90 -0
  276. package/src/cache/build-cache.ts +47 -2
  277. package/src/commands/build/app.ts +1 -2
  278. package/src/commands/build/index.ts +61 -0
  279. package/src/commands/checkout/index.ts +66 -0
  280. package/src/commands/db/migrate.ts +25 -0
  281. package/src/commands/index.ts +8 -0
  282. package/src/commands/publish/index.ts +63 -0
  283. package/src/config/defaults.ts +15 -0
  284. package/src/config/loader.ts +10 -1
  285. package/src/config/normalizers/declaration.ts +3 -0
  286. package/src/config/normalizers/page.ts +45 -0
  287. package/src/config/normalizers/pattern.ts +4 -1
  288. package/src/config/runtime.ts +48 -2
  289. package/src/index.ts +126 -3
  290. package/src/nav/index.ts +94 -0
  291. package/src/nav/registry.ts +54 -0
  292. package/src/nav/resolver.ts +217 -0
  293. package/src/nav/workspace.ts +87 -0
  294. package/src/pipeline/styles.ts +4 -35
  295. package/src/pipeline/token-transformers.ts +97 -0
  296. package/src/registry/asset-storage/adapters/vercel-blob.ts +51 -0
  297. package/src/registry/asset-storage/define.ts +15 -0
  298. package/src/registry/asset-storage/resolve.ts +72 -0
  299. package/src/registry/asset-storage/types.ts +46 -0
  300. package/src/registry/assets/layout.ts +38 -0
  301. package/src/registry/assets/sets.ts +48 -0
  302. package/src/registry/assets/transfer.ts +50 -0
  303. package/src/registry/checkout/assets.ts +126 -0
  304. package/src/registry/checkout/index.ts +596 -0
  305. package/src/registry/checkout/tokens.ts +152 -0
  306. package/src/registry/client.ts +234 -0
  307. package/src/registry/connection.ts +50 -0
  308. package/src/registry/db/adapter.ts +74 -0
  309. package/src/registry/db/client.ts +88 -0
  310. package/src/registry/db/index.ts +35 -0
  311. package/src/registry/db/migrate.ts +56 -0
  312. package/src/registry/db/schema.ts +444 -0
  313. package/src/registry/path.ts +26 -0
  314. package/src/registry/publish/artifacts.ts +147 -0
  315. package/src/registry/publish/assets.ts +129 -0
  316. package/src/registry/publish/errors.ts +34 -0
  317. package/src/registry/publish/index.ts +128 -0
  318. package/src/registry/publish/package.ts +272 -0
  319. package/src/registry/publish/publish-build.ts +51 -0
  320. package/src/registry/publish/tokens.ts +119 -0
  321. package/src/registry/tokens/sets.ts +132 -0
  322. package/src/registry/tokens/transfer.ts +53 -0
  323. package/src/registry/transfer.ts +82 -0
  324. package/src/store/filesystem-assets.ts +152 -0
  325. package/src/store/filesystem.ts +237 -0
  326. package/src/store/index.ts +62 -0
  327. package/src/store/registry.ts +334 -0
  328. package/src/store/source-files.ts +155 -0
  329. package/src/store/types.ts +210 -0
  330. package/src/transformers/plugins/csf-render.ts +31 -7
  331. package/src/transformers/plugins/handlebars-previews.ts +22 -3
  332. package/src/transformers/plugins/ssr-render.ts +145 -57
  333. package/src/transformers/preview/component/api.ts +2 -1
  334. package/src/transformers/preview/component/artifacts.ts +10 -1
  335. package/src/transformers/preview/component/builder.ts +9 -2
  336. package/src/transformers/preview/component/css.ts +10 -1
  337. package/src/transformers/preview/component/javascript.ts +55 -20
  338. package/src/transformers/preview/component/shared-artifacts.ts +146 -0
  339. package/src/transformers/preview/component.ts +5 -2
  340. package/src/transformers/preview/pattern/builder.ts +33 -7
  341. package/src/transformers/preview/pattern/html.ts +71 -27
  342. package/src/transformers/preview/types.ts +42 -0
  343. package/src/transformers/utils/handlebars.ts +31 -5
  344. package/src/types/config.ts +123 -25
  345. package/src/utils/menu-shell.ts +241 -0
  346. package/src/utils/stable-stringify.ts +7 -0
  347. package/templates/default/package.json.tpl +1 -1
  348. package/handoff.config.js +0 -90
  349. package/src/app/components/ComponentSearch.tsx +0 -93
@@ -0,0 +1,596 @@
1
+ /**
2
+ * Connected-workspace checkout orchestration.
3
+ *
4
+ * `checkout <component|pattern> <id>` reads a normalized registry record and its registry-safe
5
+ * source files from the connected registry through the shared registry client, then writes them
6
+ * into the local workspace in standard authoring form. The local **declaration is synthesized
7
+ * locally** in the configured `runtime.workspace.declarationFormat` — declarations are never read
8
+ * from the registry (they are workspace-only). Identity is matched by stable `id`, so a checked-out
9
+ * entity maps to the correct logical component/pattern and can be re-authored and re-published.
10
+ *
11
+ * Available only from a connected workspace (`runtime.mode: workspace` + a configured
12
+ * `registryConnection`). Overwriting existing local files is explicit: a `--force` flag or an
13
+ * interactive confirmation.
14
+ */
15
+
16
+ import * as p from '@clack/prompts';
17
+ import fs from 'fs-extra';
18
+ import path from 'path';
19
+ import { resolveComponentDeclaration } from '../../config/runtime';
20
+ import Handoff from '../../index';
21
+ import type { DeclarationFormat } from '../../types/config';
22
+ import { Logger } from '../../utils/logger';
23
+ import { createRegistryClient, RegistryClientError } from '../client';
24
+ import { resolveRegistryConnection } from '../connection';
25
+ import { isSafePathSegment, isSafeRelativePath, resolvePathWithin } from '../path';
26
+ import type { CheckoutPayload, TransferEntityKind, TransferFile } from '../transfer';
27
+
28
+ /** A connected-workspace configuration or precondition failure surfaced to the CLI. */
29
+ export class CheckoutError extends Error {
30
+ constructor(message: string) {
31
+ super(message);
32
+ this.name = 'CheckoutError';
33
+ }
34
+ }
35
+
36
+ /** Default authoring format synthesized when no existing file or configured preference applies. */
37
+ const DEFAULT_DECLARATION_FORMAT: DeclarationFormat = 'ts';
38
+
39
+ /** Default workspace subdirectory an entity is written into when none is configured. */
40
+ const DEFAULT_ENTITY_DIR: Record<TransferEntityKind, string> = {
41
+ component: 'components',
42
+ pattern: 'patterns',
43
+ page: 'pages',
44
+ };
45
+
46
+ /** Source-file extensions stripped to form a module specifier for a React component import. */
47
+ const COMPONENT_EXTENSION = /\.(tsx|jsx|ts|js|cjs|mjs)$/i;
48
+
49
+ /**
50
+ * Ensure the workspace is a connected workspace able to checkout: workspace runtime mode (a registry
51
+ * host has no local workspace to write into) and a resolved registry URL + access token. Throws an
52
+ * actionable {@link CheckoutError} naming the exact misconfiguration.
53
+ */
54
+ export const resolveConnectionOrThrow = (handoff: Handoff) => {
55
+ const mode = handoff.config?.runtime?.mode ?? 'workspace';
56
+ if (mode !== 'workspace') {
57
+ throw new CheckoutError(
58
+ `checkout is only available from a connected workspace (runtime.mode: "workspace"); this project is "${mode}". ` +
59
+ 'A registry host stores and serves entities; it has no local workspace to check them out into.'
60
+ );
61
+ }
62
+
63
+ const connection = resolveRegistryConnection(handoff.config);
64
+ if (!connection.url) {
65
+ throw new CheckoutError(
66
+ `No registry URL is configured. Set runtime.registryConnection.url, or the "${connection.urlEnv}" environment variable, ` +
67
+ 'to the base URL of the registry to checkout from.'
68
+ );
69
+ }
70
+ if (!connection.accessToken) {
71
+ throw new CheckoutError(
72
+ `No registry access token is configured. Set the "${connection.accessTokenEnv}" environment variable to the ` +
73
+ "registry's bearer token to authorize checkout."
74
+ );
75
+ }
76
+ return connection;
77
+ };
78
+
79
+ /** Map a registry client error to an actionable checkout message. */
80
+ const describeFetchFailure = (error: RegistryClientError, kind: TransferEntityKind, id: string, registryUrl: string): string => {
81
+ switch (error.code) {
82
+ case 'not_found':
83
+ return `No ${kind} "${id}" exists in the registry at ${registryUrl}.`;
84
+ case 'runtime_mode_conflict':
85
+ return `The registry at ${registryUrl} is not running in registry mode, so it cannot serve a checkout: ${error.message}`;
86
+ case 'unauthorized':
87
+ return `The registry rejected the access token (401). Check the configured access token matches the registry's token.`;
88
+ default:
89
+ return error.message;
90
+ }
91
+ };
92
+
93
+ /**
94
+ * Resolve the local directory the entity is written into: an already-checked-out / locally-declared
95
+ * entity keeps its existing source directory (matched by stable `id`); otherwise it lands under the
96
+ * first configured `entries.{components|patterns}` root (falling back to `components`/`patterns`).
97
+ */
98
+ const resolveTargetDir = async (handoff: Handoff, kind: TransferEntityKind, id: string): Promise<string> => {
99
+ const store = kind === 'component' ? handoff.store.components : handoff.store.patterns;
100
+ const existing = await store.get(id);
101
+ const existingDir = (existing as { path?: string } | null)?.path;
102
+ if (existingDir) {
103
+ return existingDir;
104
+ }
105
+
106
+ const configuredRoots = kind === 'component' ? handoff.config?.entries?.components : handoff.config?.entries?.patterns;
107
+ const root = configuredRoots?.length
108
+ ? path.resolve(handoff.workingPath, configuredRoots[0])
109
+ : path.resolve(handoff.workingPath, DEFAULT_ENTITY_DIR[kind]);
110
+ const targetDir = resolvePathWithin(root, id);
111
+ if (!targetDir) {
112
+ throw new CheckoutError(`Cannot checkout ${kind} with unsafe id "${id}".`);
113
+ }
114
+ return targetDir;
115
+ };
116
+
117
+ /** Read the extension of an existing local declaration in `dir`, if any maps to a known format. */
118
+ const existingDeclarationFormat = (dir: string): DeclarationFormat | undefined => {
119
+ if (!fs.existsSync(dir)) {
120
+ return undefined;
121
+ }
122
+ const declaration = resolveComponentDeclaration(dir, path.basename(dir));
123
+ if (!declaration) {
124
+ return undefined;
125
+ }
126
+ const ext = path.extname(declaration.fileName).slice(1).toLowerCase();
127
+ return (['ts', 'js', 'cjs', 'json'] as const).includes(ext as DeclarationFormat) ? (ext as DeclarationFormat) : undefined;
128
+ };
129
+
130
+ /**
131
+ * Resolve the declaration format: an existing local declaration's extension wins, else the
132
+ * configured `runtime.workspace.declarationFormat`, else {@link DEFAULT_DECLARATION_FORMAT}.
133
+ */
134
+ const resolveDeclarationFormat = (handoff: Handoff, dir: string): DeclarationFormat => {
135
+ const configured = handoff.config?.runtime?.workspace?.declarationFormat;
136
+ return existingDeclarationFormat(dir) ?? configured ?? DEFAULT_DECLARATION_FORMAT;
137
+ };
138
+
139
+ /** The declaration filename to write: reuse an existing local declaration, else `<id>.handoff.<format>`. */
140
+ const resolveDeclarationFileName = (dir: string, id: string, format: DeclarationFormat): string => {
141
+ if (fs.existsSync(dir)) {
142
+ const existing = resolveComponentDeclaration(dir, path.basename(dir));
143
+ if (existing) {
144
+ return existing.fileName;
145
+ }
146
+ }
147
+ return `${id}.handoff.${format}`;
148
+ };
149
+
150
+ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
151
+ typeof value === 'object' && value !== null && !Array.isArray(value);
152
+
153
+ /**
154
+ * The single authored "primary" entry key per renderer. The normalizer derives the others (e.g. it
155
+ * mirrors a React `component` into `template`, and a CSF `story` into `template`), so the stored
156
+ * record carries duplicates that must not be re-authored. React authors `component`; handlebars and
157
+ * CSF author `template` (a `.stories.*` template is what marks a CSF component).
158
+ */
159
+ const PRIMARY_ENTRY_BY_RENDERER: Record<string, string> = {
160
+ react: 'component',
161
+ handlebars: 'template',
162
+ csf: 'template',
163
+ };
164
+
165
+ /** Primary entry keys, in preference order, for an unknown renderer. */
166
+ const PRIMARY_ENTRY_KEYS = ['component', 'template', 'story'] as const;
167
+
168
+ /** Supporting entry keys kept verbatim regardless of renderer. */
169
+ const SUPPORTING_ENTRY_KEYS = ['js', 'scss', 'schema', 'templates'] as const;
170
+
171
+ const asString = (value: unknown): string | undefined => (typeof value === 'string' && value ? value : undefined);
172
+ const asStringArray = (value: unknown): string[] | undefined =>
173
+ Array.isArray(value) && value.every((item) => typeof item === 'string') ? (value as string[]) : undefined;
174
+
175
+ /**
176
+ * Rebuild the authored `entries` for a renderer: keep only the renderer's primary entry key (so the
177
+ * normalizer-duplicated keys are dropped) plus the supporting source entries (`js`/`scss`/`schema`/
178
+ * `templates`). An unknown renderer keeps the first available primary key by preference order.
179
+ */
180
+ const buildEntries = (entries: unknown, renderer: string | undefined): Record<string, unknown> | undefined => {
181
+ if (!isPlainObject(entries)) {
182
+ return undefined;
183
+ }
184
+ const result: Record<string, unknown> = {};
185
+
186
+ const primaryKey = renderer ? PRIMARY_ENTRY_BY_RENDERER[renderer] : undefined;
187
+ if (primaryKey && typeof entries[primaryKey] === 'string') {
188
+ result[primaryKey] = entries[primaryKey];
189
+ } else if (!primaryKey) {
190
+ const fallback = PRIMARY_ENTRY_KEYS.find((key) => typeof entries[key] === 'string');
191
+ if (fallback) {
192
+ result[fallback] = entries[fallback];
193
+ }
194
+ }
195
+
196
+ for (const key of SUPPORTING_ENTRY_KEYS) {
197
+ if (entries[key] !== undefined) {
198
+ result[key] = entries[key];
199
+ }
200
+ }
201
+
202
+ return Object.keys(result).length > 0 ? result : undefined;
203
+ };
204
+
205
+ /**
206
+ * Rebuild the authored `previews` map (`DeclarationPreview`) from the stored previews, keeping only
207
+ * authored fields (`title`, `args`) and dropping build-derived ones (rendered `url`, `usage`,
208
+ * `sourcePreview`). The component normalizer maps authored `args` to `values`, so the stored record
209
+ * carries `values`; checkout re-emits it as `args` to match the authoring convention (the form
210
+ * `make`/`scaffold` generate). Both rebuild identically.
211
+ */
212
+ const buildPreviews = (previews: unknown): Record<string, unknown> | undefined => {
213
+ if (!isPlainObject(previews)) {
214
+ return undefined;
215
+ }
216
+ const result: Record<string, unknown> = {};
217
+ for (const [key, raw] of Object.entries(previews)) {
218
+ if (!isPlainObject(raw)) {
219
+ continue;
220
+ }
221
+ const preview: Record<string, unknown> = {};
222
+ if (typeof raw.title === 'string') preview.title = raw.title;
223
+ const args = raw.args !== undefined ? raw.args : raw.values;
224
+ if (args !== undefined) preview.args = args;
225
+ result[key] = preview;
226
+ }
227
+ return Object.keys(result).length > 0 ? result : undefined;
228
+ };
229
+
230
+ /**
231
+ * Synthesize an authored component declaration (`GenericDeclarationConfig`) from the normalized
232
+ * record. Only authored fields are kept; registry-/build-derived data (`path`, `image`,
233
+ * `properties`/docgen, rendered preview URLs, `internalPatternPreviews`, `validations`, `variant`,
234
+ * review `metadata`) is dropped so the local build regenerates it. `name` carries the record title
235
+ * (the authored contract uses `name`; normalization maps it back to `title`); `shouldDo`/
236
+ * `shouldNotDo` are emitted in their camelCase authored form.
237
+ */
238
+ const buildComponentDeclaration = (item: Record<string, unknown>): Record<string, unknown> => {
239
+ const declaration: Record<string, unknown> = {};
240
+ const id = asString(item.id);
241
+ if (id) declaration.id = id;
242
+ declaration.name = asString(item.title) ?? asString(item.name) ?? id ?? '';
243
+
244
+ const description = asString(item.description);
245
+ if (description) declaration.description = description;
246
+ const group = asString(item.group);
247
+ if (group) declaration.group = group;
248
+ const type = asString(item.type);
249
+ if (type) declaration.type = type;
250
+ const renderer = asString(item.renderer);
251
+ if (renderer) declaration.renderer = renderer;
252
+ const entries = buildEntries(item.entries, renderer);
253
+ if (entries) declaration.entries = entries;
254
+
255
+ const previews = buildPreviews(item.previews);
256
+ if (previews) declaration.previews = previews;
257
+
258
+ const categories = asStringArray(item.categories);
259
+ if (categories) declaration.categories = categories;
260
+ const tags = asStringArray(item.tags);
261
+ if (tags) declaration.tags = tags;
262
+
263
+ const shouldDo = asStringArray(item.shouldDo) ?? asStringArray(item.should_do);
264
+ if (shouldDo) declaration.shouldDo = shouldDo;
265
+ const shouldNotDo = asStringArray(item.shouldNotDo) ?? asStringArray(item.should_not_do);
266
+ if (shouldNotDo) declaration.shouldNotDo = shouldNotDo;
267
+
268
+ const figma = asString(item.figma);
269
+ if (figma) declaration.figma = figma;
270
+ const figmaComponentId = asString(item.figmaComponentId);
271
+ if (figmaComponentId) declaration.figmaComponentId = figmaComponentId;
272
+ if (isPlainObject(item.page)) declaration.page = item.page;
273
+ if (isPlainObject(item.options)) declaration.options = item.options;
274
+
275
+ return declaration;
276
+ };
277
+
278
+ /**
279
+ * Rebuild the authored pattern component refs (`PatternComponentRef`) from the stored entries,
280
+ * keeping only `id`/`preview`/`args` and dropping build-time resolution fields (`resolvedPreview`,
281
+ * `resolved`).
282
+ */
283
+ const buildPatternComponents = (components: unknown): Record<string, unknown>[] => {
284
+ if (!Array.isArray(components)) {
285
+ return [];
286
+ }
287
+ return components.filter(isPlainObject).map((entry) => {
288
+ const ref: Record<string, unknown> = {};
289
+ const id = asString(entry.id);
290
+ if (id) ref.id = id;
291
+ const preview = asString(entry.preview);
292
+ if (preview) ref.preview = preview;
293
+ if (isPlainObject(entry.args)) ref.args = entry.args;
294
+ return ref;
295
+ });
296
+ };
297
+
298
+ /**
299
+ * Synthesize an authored pattern declaration (`GenericPatternDeclarationConfig`) from the normalized
300
+ * record. Only the authored fields (`id`, `name`, `description`, `group`, `tags`, `components`) are
301
+ * kept; derived fields (`path`, rendered `url`, review `metadata`) are dropped.
302
+ */
303
+ const buildPatternDeclaration = (item: Record<string, unknown>): Record<string, unknown> => {
304
+ const declaration: Record<string, unknown> = {};
305
+ const id = asString(item.id);
306
+ if (id) declaration.id = id;
307
+ declaration.name = asString(item.title) ?? asString(item.name) ?? id ?? '';
308
+
309
+ const description = asString(item.description);
310
+ if (description) declaration.description = description;
311
+ const group = asString(item.group);
312
+ if (group) declaration.group = group;
313
+ const tags = asStringArray(item.tags);
314
+ if (tags) declaration.tags = tags;
315
+
316
+ declaration.components = buildPatternComponents(item.components);
317
+ return declaration;
318
+ };
319
+
320
+ /**
321
+ * Pick the renderer-specific `handoff-app` component factory from the record's renderer so the
322
+ * synthesized declaration matches the authored contract for that renderer. `react`/`handlebars`/`csf`
323
+ * each have a dedicated factory that stamps the renderer itself (so the renderer is dropped from the
324
+ * authored config); an unknown/absent renderer falls back to the generic `defineComponent`, which
325
+ * keeps `renderer` in the config.
326
+ */
327
+ const resolveComponentFactory = (renderer: string | undefined): { name: string; stampsRenderer: boolean } => {
328
+ switch (renderer) {
329
+ case 'react':
330
+ return { name: 'defineReactComponent', stampsRenderer: true };
331
+ case 'handlebars':
332
+ return { name: 'defineHandlebarsComponent', stampsRenderer: true };
333
+ case 'csf':
334
+ return { name: 'defineCsfComponent', stampsRenderer: true };
335
+ default:
336
+ return { name: 'defineComponent', stampsRenderer: false };
337
+ }
338
+ };
339
+
340
+ /** Drop a key from an object without mutating it. */
341
+ const omit = (object: Record<string, unknown>, key: string): Record<string, unknown> => {
342
+ const { [key]: _removed, ...rest } = object;
343
+ return rest;
344
+ };
345
+
346
+ /** Turn a relative entry path into a module specifier (extension stripped, `./`-prefixed). */
347
+ const toImportSpecifier = (entryPath: string): string => {
348
+ const withoutExt = entryPath.replace(COMPONENT_EXTENSION, '').split(path.sep).join('/');
349
+ return withoutExt.startsWith('.') ? withoutExt : `./${withoutExt}`;
350
+ };
351
+
352
+ /** Derive a safe PascalCase identifier for the imported React component. */
353
+ const toComponentIdentifier = (entryPath: string, fallback: string): string => {
354
+ const base = path.basename(entryPath).replace(COMPONENT_EXTENSION, '');
355
+ const pascal = (base || fallback)
356
+ .replace(/[^a-zA-Z0-9]+/g, ' ')
357
+ .trim()
358
+ .split(/\s+/)
359
+ .filter(Boolean)
360
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
361
+ .join('');
362
+ return /^[A-Za-z_$]/.test(pascal) ? pascal : `Component${pascal}`;
363
+ };
364
+
365
+ /** Keys matching this are emitted unquoted in JS/TS object literals; others are single-quoted. */
366
+ const JS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
367
+
368
+ /** Serialize a string as a single-quoted JS literal (repo style), escaping safely. */
369
+ const singleQuote = (value: string): string => {
370
+ let out = "'";
371
+ for (const char of value) {
372
+ const code = char.codePointAt(0) ?? 0;
373
+ if (char === '\\') out += '\\\\';
374
+ else if (char === "'") out += "\\'";
375
+ else if (char === '\n') out += '\\n';
376
+ else if (char === '\r') out += '\\r';
377
+ else if (char === '\t') out += '\\t';
378
+ else if (code < 0x20) out += `\\u${code.toString(16).padStart(4, '0')}`;
379
+ else out += char;
380
+ }
381
+ return `${out}'`;
382
+ };
383
+
384
+ /**
385
+ * Serialize a value as a JS/TS object literal mirroring `JSON.stringify(value, null, 2)` formatting,
386
+ * except identifier-safe object keys are emitted unquoted (`id: 'button'` rather than
387
+ * `"id": "button"`) and strings are single-quoted to match the repo's authoring style. `undefined`
388
+ * entries are dropped.
389
+ */
390
+ const toJsLiteral = (value: unknown, indentLevel = 0): string => {
391
+ const pad = ' '.repeat(indentLevel);
392
+ const padInner = ' '.repeat(indentLevel + 1);
393
+
394
+ if (value === null) {
395
+ return 'null';
396
+ }
397
+ if (typeof value === 'string') {
398
+ return singleQuote(value);
399
+ }
400
+ if (typeof value !== 'object') {
401
+ return JSON.stringify(value);
402
+ }
403
+
404
+ if (Array.isArray(value)) {
405
+ if (value.length === 0) {
406
+ return '[]';
407
+ }
408
+ const items = value.map((item) => `${padInner}${toJsLiteral(item, indentLevel + 1)}`);
409
+ return `[\n${items.join(',\n')}\n${pad}]`;
410
+ }
411
+
412
+ const entries = Object.entries(value as Record<string, unknown>).filter(([, v]) => v !== undefined);
413
+ if (entries.length === 0) {
414
+ return '{}';
415
+ }
416
+ const lines = entries.map(([key, v]) => {
417
+ const renderedKey = JS_IDENTIFIER.test(key) ? key : singleQuote(key);
418
+ return `${padInner}${renderedKey}: ${toJsLiteral(v, indentLevel + 1)}`;
419
+ });
420
+ return `{\n${lines.join(',\n')}\n${pad}}`;
421
+ };
422
+
423
+ /** Wrap a config object in a factory call for the given code format (unquoted identifier keys). */
424
+ const wrapFactory = (factory: string, config: Record<string, unknown>, format: DeclarationFormat): string => {
425
+ const literal = toJsLiteral(config);
426
+ return format === 'ts'
427
+ ? `import { ${factory} } from 'handoff-app';\n\nexport default ${factory}(${literal});\n`
428
+ : `const { ${factory} } = require('handoff-app');\n\nmodule.exports = ${factory}(${literal});\n`;
429
+ };
430
+
431
+ /**
432
+ * Synthesize the component declaration file. JSON declarations are the bare object (carrying
433
+ * `renderer`/`entries`, which the loader reads directly). Code declarations use the renderer-specific
434
+ * factory; React additionally imports the component from its `entries.component` source and passes it
435
+ * as the factory's first argument (matching the authored `defineReactComponent` contract). When the
436
+ * renderer is `react` but no component entry is available, it falls back to the generic factory.
437
+ */
438
+ const renderComponentDeclaration = (declaration: Record<string, unknown>, format: DeclarationFormat): string => {
439
+ if (format === 'json') {
440
+ return `${JSON.stringify(declaration, null, 2)}\n`;
441
+ }
442
+
443
+ const renderer = asString(declaration.renderer);
444
+ const factory = resolveComponentFactory(renderer);
445
+ const componentEntry = isPlainObject(declaration.entries) ? asString(declaration.entries.component) : undefined;
446
+
447
+ if (factory.name === 'defineReactComponent' && componentEntry) {
448
+ const specifier = toImportSpecifier(componentEntry);
449
+ const identifier = toComponentIdentifier(componentEntry, asString(declaration.id) ?? 'Component');
450
+ const literal = toJsLiteral(omit(declaration, 'renderer'));
451
+ return format === 'ts'
452
+ ? `import { defineReactComponent } from 'handoff-app';\nimport ${identifier} from '${specifier}';\n\nexport default defineReactComponent(${identifier}, ${literal});\n`
453
+ : `const { defineReactComponent } = require('handoff-app');\nconst ${identifier} = require('${specifier}').default;\n\nmodule.exports = defineReactComponent(${identifier}, ${literal});\n`;
454
+ }
455
+
456
+ // React without a resolvable component entry can't use defineReactComponent; keep renderer and use
457
+ // the generic factory so the build still infers the renderer.
458
+ const effective = factory.name === 'defineReactComponent' ? { name: 'defineComponent', stampsRenderer: false } : factory;
459
+ const config = effective.stampsRenderer ? omit(declaration, 'renderer') : declaration;
460
+ return wrapFactory(effective.name, config, format);
461
+ };
462
+
463
+ /** Synthesize the pattern declaration file via `definePattern` (or a bare object for JSON). */
464
+ const renderPatternDeclaration = (declaration: Record<string, unknown>, format: DeclarationFormat): string => {
465
+ if (format === 'json') {
466
+ return `${JSON.stringify(declaration, null, 2)}\n`;
467
+ }
468
+ return wrapFactory('definePattern', declaration, format);
469
+ };
470
+
471
+ /** Synthesize the declaration file contents for an entity, faithful to its renderer/kind contract. */
472
+ const synthesizeDeclaration = (kind: TransferEntityKind, item: Record<string, unknown>, format: DeclarationFormat): string =>
473
+ kind === 'component'
474
+ ? renderComponentDeclaration(buildComponentDeclaration(item), format)
475
+ : renderPatternDeclaration(buildPatternDeclaration(item), format);
476
+
477
+ /**
478
+ * Confirm overwriting any local files the checkout would replace. Returns `true` to proceed. With
479
+ * `--force` (or when nothing would be overwritten) it proceeds without prompting; otherwise it asks
480
+ * for explicit confirmation and treats a cancel/decline as an abort.
481
+ */
482
+ const confirmOverwrite = async (handoff: Handoff, conflicts: string[]): Promise<boolean> => {
483
+ if (conflicts.length === 0 || handoff.force) {
484
+ return true;
485
+ }
486
+ const relative = conflicts.map((file) => ` - ${path.relative(handoff.workingPath, file)}`).join('\n');
487
+ const proceed = await p.confirm({
488
+ message: `Checkout will overwrite ${conflicts.length} existing local file(s):\n${relative}\nOverwrite?`,
489
+ initialValue: false,
490
+ });
491
+ return !p.isCancel(proceed) && proceed === true;
492
+ };
493
+
494
+ /** Write a registry source file at its relative path under the target directory. */
495
+ const writeSourceFile = async (targetDir: string, file: TransferFile): Promise<string> => {
496
+ const absolutePath = resolvePathWithin(targetDir, file.path);
497
+ if (!absolutePath) {
498
+ throw new CheckoutError(`Registry returned an unsafe source file path "${file.path}".`);
499
+ }
500
+ await fs.ensureDir(path.dirname(absolutePath));
501
+ await fs.writeFile(absolutePath, file.content, 'utf8');
502
+ return absolutePath;
503
+ };
504
+
505
+ /**
506
+ * Checkout a page: write its single verbatim `.md` (transfer path `<id>.md`) back under
507
+ * `<workingPath>/pages/`. Unlike components/patterns there is no declaration to synthesize — the
508
+ * markdown file is itself the authored source — so the page round-trips byte-for-byte.
509
+ */
510
+ const checkoutPage = async (handoff: Handoff, id: string, payload: CheckoutPayload): Promise<void> => {
511
+ const pagesRoot = path.resolve(handoff.workingPath, DEFAULT_ENTITY_DIR.page);
512
+ const targets = payload.files.map((file) => {
513
+ const target = resolvePathWithin(pagesRoot, file.path);
514
+ if (!target) {
515
+ throw new CheckoutError(`Registry returned an unsafe page source path "${file.path}".`);
516
+ }
517
+ return target;
518
+ });
519
+ const conflicts = targets.filter((file) => fs.existsSync(file));
520
+ if (!(await confirmOverwrite(handoff, conflicts))) {
521
+ Logger.warn(`Checkout of page "${id}" cancelled; no files were written.`);
522
+ return;
523
+ }
524
+ for (const file of payload.files) {
525
+ await writeSourceFile(pagesRoot, file);
526
+ }
527
+ Logger.success(
528
+ `Checked out page "${id}" into ${path.relative(handoff.workingPath, pagesRoot) || '.'} ` + `(${payload.files.length} markdown file(s)).`
529
+ );
530
+ };
531
+
532
+ /**
533
+ * Checkout a single component or pattern from a connected workspace: precondition checks → fetch the
534
+ * normalized record + source files → resolve the local target → explicit-overwrite guard → write
535
+ * source files and synthesize the local declaration. Throws {@link CheckoutError} with actionable
536
+ * messaging on any precondition or fetch failure.
537
+ */
538
+ export const checkoutEntity = async (handoff: Handoff, kind: TransferEntityKind, id: string): Promise<void> => {
539
+ const validId = kind === 'page' ? isSafeRelativePath(id) : isSafePathSegment(id);
540
+ if (!validId) {
541
+ throw new CheckoutError(`Cannot checkout ${kind} with unsafe id "${id}".`);
542
+ }
543
+ const connection = resolveConnectionOrThrow(handoff);
544
+
545
+ Logger.info(`Fetching ${kind} "${id}" from ${connection.url}…`);
546
+ const client = createRegistryClient({ baseUrl: connection.url, accessToken: connection.accessToken });
547
+ let payload: CheckoutPayload;
548
+ try {
549
+ payload = await client.checkout(kind, id);
550
+ } catch (error) {
551
+ if (error instanceof RegistryClientError) {
552
+ throw new CheckoutError(describeFetchFailure(error, kind, id, connection.url));
553
+ }
554
+ throw error;
555
+ }
556
+
557
+ // Pages round-trip as a single verbatim `.md` with no declaration synthesis.
558
+ if (kind === 'page') {
559
+ await checkoutPage(handoff, id, payload);
560
+ return;
561
+ }
562
+
563
+ const targetDir = await resolveTargetDir(handoff, kind, id);
564
+ const format = resolveDeclarationFormat(handoff, targetDir);
565
+ const declarationFileName = resolveDeclarationFileName(targetDir, id, format);
566
+ const declarationPath = path.join(targetDir, declarationFileName);
567
+
568
+ // Compute everything that would be written, then gate overwrite of any pre-existing file.
569
+ const sourceTargets = payload.files.map((file) => {
570
+ const absolutePath = resolvePathWithin(targetDir, file.path);
571
+ if (!absolutePath) {
572
+ throw new CheckoutError(`Registry returned an unsafe source file path "${file.path}".`);
573
+ }
574
+ return { file, absolutePath };
575
+ });
576
+ const conflicts = [...sourceTargets.map((entry) => entry.absolutePath), declarationPath].filter((file) => fs.existsSync(file));
577
+ if (!(await confirmOverwrite(handoff, conflicts))) {
578
+ Logger.warn(`Checkout of ${kind} "${id}" cancelled; no files were written.`);
579
+ return;
580
+ }
581
+
582
+ await fs.ensureDir(targetDir);
583
+ const written: string[] = [];
584
+ for (const { file } of sourceTargets) {
585
+ written.push(await writeSourceFile(targetDir, file));
586
+ }
587
+
588
+ const declaration = synthesizeDeclaration(kind, payload.item, format);
589
+ await fs.writeFile(declarationPath, declaration, 'utf8');
590
+ written.push(declarationPath);
591
+
592
+ Logger.success(
593
+ `Checked out ${kind} "${id}" into ${path.relative(handoff.workingPath, targetDir) || '.'} ` +
594
+ `(${payload.files.length} source file(s) + ${declarationFileName}).`
595
+ );
596
+ };