handoff-app 1.2.2-7 → 2.0.0-1

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 (351) 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/paths.js +25 -1
  14. package/dist/app-builder/static-export.d.ts +17 -0
  15. package/dist/app-builder/static-export.js +206 -0
  16. package/dist/app-builder/vercel-output.d.ts +39 -0
  17. package/dist/app-builder/vercel-output.js +259 -0
  18. package/dist/app-builder/watchers/utils.d.ts +2 -0
  19. package/dist/app-builder/watchers.d.ts +14 -4
  20. package/dist/app-builder/watchers.js +123 -28
  21. package/dist/app-builder/websocket.js +1 -1
  22. package/dist/artifacts/index.d.ts +2 -0
  23. package/dist/artifacts/index.js +16 -0
  24. package/dist/artifacts/types.d.ts +136 -0
  25. package/dist/artifacts/types.js +24 -0
  26. package/dist/artifacts/url.d.ts +67 -0
  27. package/dist/artifacts/url.js +116 -0
  28. package/dist/cache/build-cache.js +48 -4
  29. package/dist/cli.js +0 -0
  30. package/dist/commands/build/app.js +1 -2
  31. package/dist/commands/build/index.d.ts +17 -0
  32. package/dist/commands/build/index.js +66 -0
  33. package/dist/commands/checkout/index.d.ts +24 -0
  34. package/dist/commands/checkout/index.js +71 -0
  35. package/dist/commands/db/migrate.d.ts +11 -0
  36. package/dist/commands/db/migrate.js +33 -0
  37. package/dist/commands/index.js +8 -0
  38. package/dist/commands/publish/index.d.ts +21 -0
  39. package/dist/commands/publish/index.js +68 -0
  40. package/dist/config/defaults.js +15 -1
  41. package/dist/config/loader.js +6 -1
  42. package/dist/config/normalizers/declaration.js +4 -1
  43. package/dist/config/normalizers/page.d.ts +18 -0
  44. package/dist/config/normalizers/page.js +34 -0
  45. package/dist/config/normalizers/pattern.js +7 -1
  46. package/dist/config/runtime.d.ts +4 -0
  47. package/dist/config/runtime.js +53 -5
  48. package/dist/index.d.ts +65 -1
  49. package/dist/index.js +123 -3
  50. package/dist/nav/index.d.ts +83 -0
  51. package/dist/nav/index.js +9 -0
  52. package/dist/nav/registry.d.ts +15 -0
  53. package/dist/nav/registry.js +42 -0
  54. package/dist/nav/resolver.d.ts +8 -0
  55. package/dist/nav/resolver.js +192 -0
  56. package/dist/nav/workspace.d.ts +18 -0
  57. package/dist/nav/workspace.js +103 -0
  58. package/dist/pipeline/styles.js +7 -35
  59. package/dist/pipeline/token-transformers.d.ts +45 -0
  60. package/dist/pipeline/token-transformers.js +71 -0
  61. package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +17 -0
  62. package/dist/registry/asset-storage/adapters/vercel-blob.js +87 -0
  63. package/dist/registry/asset-storage/define.d.ts +10 -0
  64. package/dist/registry/asset-storage/define.js +13 -0
  65. package/dist/registry/asset-storage/resolve.d.ts +45 -0
  66. package/dist/registry/asset-storage/resolve.js +36 -0
  67. package/dist/registry/asset-storage/types.d.ts +51 -0
  68. package/dist/registry/asset-storage/types.js +9 -0
  69. package/dist/registry/assets/layout.d.ts +22 -0
  70. package/dist/registry/assets/layout.js +30 -0
  71. package/dist/registry/assets/sets.d.ts +18 -0
  72. package/dist/registry/assets/sets.js +44 -0
  73. package/dist/registry/assets/transfer.d.ts +45 -0
  74. package/dist/registry/assets/transfer.js +11 -0
  75. package/dist/registry/checkout/assets.d.ts +16 -0
  76. package/dist/registry/checkout/assets.js +150 -0
  77. package/dist/registry/checkout/index.d.ts +33 -0
  78. package/dist/registry/checkout/index.js +610 -0
  79. package/dist/registry/checkout/tokens.d.ts +15 -0
  80. package/dist/registry/checkout/tokens.js +171 -0
  81. package/dist/registry/client.d.ts +61 -0
  82. package/dist/registry/client.js +211 -0
  83. package/dist/registry/connection.d.ts +30 -0
  84. package/dist/registry/connection.js +36 -0
  85. package/dist/registry/db/adapter.d.ts +43 -0
  86. package/dist/registry/db/adapter.js +59 -0
  87. package/dist/registry/db/client.d.ts +35 -0
  88. package/dist/registry/db/client.js +94 -0
  89. package/dist/registry/db/index.d.ts +5 -0
  90. package/dist/registry/db/index.js +24 -0
  91. package/dist/registry/db/migrate.d.ts +23 -0
  92. package/dist/registry/db/migrate.js +64 -0
  93. package/dist/registry/db/schema.d.ts +4869 -0
  94. package/dist/registry/db/schema.js +347 -0
  95. package/dist/registry/path.d.ts +4 -0
  96. package/dist/registry/path.js +31 -0
  97. package/dist/registry/publish/artifacts.d.ts +16 -0
  98. package/dist/registry/publish/artifacts.js +132 -0
  99. package/dist/registry/publish/assets.d.ts +16 -0
  100. package/dist/registry/publish/assets.js +129 -0
  101. package/dist/registry/publish/errors.d.ts +5 -0
  102. package/dist/registry/publish/errors.js +26 -0
  103. package/dist/registry/publish/index.d.ts +27 -0
  104. package/dist/registry/publish/index.js +126 -0
  105. package/dist/registry/publish/package.d.ts +27 -0
  106. package/dist/registry/publish/package.js +257 -0
  107. package/dist/registry/publish/publish-build.d.ts +12 -0
  108. package/dist/registry/publish/publish-build.js +43 -0
  109. package/dist/registry/publish/tokens.d.ts +18 -0
  110. package/dist/registry/publish/tokens.js +112 -0
  111. package/dist/registry/tokens/sets.d.ts +59 -0
  112. package/dist/registry/tokens/sets.js +109 -0
  113. package/dist/registry/tokens/transfer.d.ts +48 -0
  114. package/dist/registry/tokens/transfer.js +10 -0
  115. package/dist/registry/transfer.d.ts +75 -0
  116. package/dist/registry/transfer.js +11 -0
  117. package/dist/store/filesystem-assets.d.ts +17 -0
  118. package/dist/store/filesystem-assets.js +147 -0
  119. package/dist/store/filesystem.d.ts +76 -0
  120. package/dist/store/filesystem.js +208 -0
  121. package/dist/store/index.d.ts +20 -0
  122. package/dist/store/index.js +38 -0
  123. package/dist/store/registry.d.ts +77 -0
  124. package/dist/store/registry.js +350 -0
  125. package/dist/store/source-files.d.ts +24 -0
  126. package/dist/store/source-files.js +147 -0
  127. package/dist/store/types.d.ts +185 -0
  128. package/dist/store/types.js +12 -0
  129. package/dist/transformers/plugins/csf-render.js +17 -8
  130. package/dist/transformers/plugins/handlebars-previews.js +10 -3
  131. package/dist/transformers/plugins/ssr-render.js +116 -49
  132. package/dist/transformers/preview/component/api.js +3 -1
  133. package/dist/transformers/preview/component/artifacts.js +8 -1
  134. package/dist/transformers/preview/component/builder.js +13 -5
  135. package/dist/transformers/preview/component/css.js +9 -1
  136. package/dist/transformers/preview/component/javascript.d.ts +9 -3
  137. package/dist/transformers/preview/component/javascript.js +45 -17
  138. package/dist/transformers/preview/component/shared-artifacts.d.ts +83 -0
  139. package/dist/transformers/preview/component/shared-artifacts.js +92 -0
  140. package/dist/transformers/preview/component.js +5 -2
  141. package/dist/transformers/preview/pattern/builder.js +28 -9
  142. package/dist/transformers/preview/pattern/html.d.ts +13 -10
  143. package/dist/transformers/preview/pattern/html.js +62 -27
  144. package/dist/transformers/preview/types.d.ts +38 -0
  145. package/dist/transformers/utils/handlebars.d.ts +3 -0
  146. package/dist/transformers/utils/handlebars.js +19 -5
  147. package/dist/types/config.d.ts +124 -31
  148. package/dist/utils/menu-shell.d.ts +60 -0
  149. package/dist/utils/menu-shell.js +221 -0
  150. package/dist/utils/stable-stringify.d.ts +2 -0
  151. package/dist/utils/stable-stringify.js +8 -0
  152. package/docs/cli.md +4 -2
  153. package/drizzle/0000_init_registry_storage.sql +95 -0
  154. package/drizzle/0001_add_review_metadata.sql +2 -0
  155. package/drizzle/0002_add_pages.sql +28 -0
  156. package/drizzle/0003_add_tokens.sql +29 -0
  157. package/drizzle/0004_add_assets.sql +37 -0
  158. package/drizzle/meta/0000_snapshot.json +642 -0
  159. package/drizzle/meta/0001_snapshot.json +654 -0
  160. package/drizzle/meta/0002_snapshot.json +835 -0
  161. package/drizzle/meta/0003_snapshot.json +1023 -0
  162. package/drizzle/meta/0004_snapshot.json +1252 -0
  163. package/drizzle/meta/_journal.json +41 -0
  164. package/drizzle.config.ts +16 -0
  165. package/package.json +10 -1
  166. package/src/app/components/Component/Preview.tsx +3 -2
  167. package/src/app/components/Component/RegistryActions.tsx +150 -0
  168. package/src/app/components/Foundations/DisplayLogo.tsx +11 -3
  169. package/src/app/components/Layout/Header.tsx +21 -3
  170. package/src/app/components/Layout/Main.tsx +11 -3
  171. package/src/app/components/Navigation/MainNav.tsx +4 -2
  172. package/src/app/components/Navigation/MobileNav.tsx +4 -2
  173. package/src/app/components/Navigation/SideNav.tsx +46 -11
  174. package/src/app/components/NotFound.tsx +24 -0
  175. package/src/app/components/context/NavProvider.tsx +71 -0
  176. package/src/app/components/old/Header.tsx +3 -3
  177. package/src/app/components/util/index.ts +203 -380
  178. package/src/app/components/util/useCollectionAssets.ts +125 -0
  179. package/src/app/components/util/useFoundationTokens.ts +67 -0
  180. package/src/app/generated/default-pages.json +1 -0
  181. package/src/app/generated/nav-shell.json +1 -0
  182. package/src/app/lib/api/query.ts +6 -0
  183. package/src/app/lib/api/redact.ts +46 -0
  184. package/src/app/lib/asset-storage.ts +81 -0
  185. package/src/app/lib/docs-api/artifacts.ts +116 -0
  186. package/src/app/lib/docs-api/backend.ts +151 -0
  187. package/src/app/lib/docs-api/errors.ts +52 -0
  188. package/src/app/lib/docs-api/handler.ts +34 -0
  189. package/src/app/lib/docs-api/index.ts +7 -0
  190. package/src/app/lib/docs-api/records.ts +189 -0
  191. package/src/app/lib/docs-api/registry.ts +130 -0
  192. package/src/app/lib/docs-api/runtime-config.ts +173 -0
  193. package/src/app/lib/docs-api/serve.ts +37 -0
  194. package/src/app/lib/docs-api/token-detail.ts +19 -0
  195. package/src/app/lib/registry-api/allowlist.ts +163 -0
  196. package/src/app/lib/registry-api/asset-transfer.ts +363 -0
  197. package/src/app/lib/registry-api/auth.ts +68 -0
  198. package/src/app/lib/registry-api/entity-routes.ts +197 -0
  199. package/src/app/lib/registry-api/errors.ts +51 -0
  200. package/src/app/lib/registry-api/files.ts +135 -0
  201. package/src/app/lib/registry-api/handler.ts +96 -0
  202. package/src/app/lib/registry-api/index.ts +4 -0
  203. package/src/app/lib/registry-api/meta.ts +87 -0
  204. package/src/app/lib/registry-api/revalidate.ts +36 -0
  205. package/src/app/lib/registry-api/store.ts +348 -0
  206. package/src/app/lib/registry-api/token-transfer.ts +231 -0
  207. package/src/app/lib/registry-api/transfer.ts +513 -0
  208. package/src/app/lib/registry-api/validation.ts +74 -0
  209. package/src/app/lib/registry-connection.ts +58 -0
  210. package/src/app/lib/utils.ts +14 -0
  211. package/src/app/next.config.mjs +72 -13
  212. package/src/app/pages/[...slug]/index.tsx +74 -20
  213. package/src/app/pages/_app.tsx +5 -2
  214. package/src/app/pages/api/docs/artifacts/[...artifactPath].ts +16 -0
  215. package/src/app/pages/api/docs/assets/[collection]/[...path].ts +50 -0
  216. package/src/app/pages/api/docs/assets/[collection]/index.ts +22 -0
  217. package/src/app/pages/api/docs/components/[id]/assets/[asset].ts +33 -0
  218. package/src/app/pages/api/docs/components/[id]/inspect/[previewId].ts +20 -0
  219. package/src/app/pages/api/docs/components/[id]/previews/[previewId].ts +21 -0
  220. package/src/app/pages/api/docs/components/[id].ts +24 -0
  221. package/src/app/pages/api/docs/components.json.ts +12 -0
  222. package/src/app/pages/api/docs/nav.json.ts +48 -0
  223. package/src/app/pages/api/docs/patterns/[id]/preview.ts +19 -0
  224. package/src/app/pages/api/docs/patterns/[id].ts +24 -0
  225. package/src/app/pages/api/docs/patterns.json.ts +12 -0
  226. package/src/app/pages/api/docs/tokens/components/[id].ts +30 -0
  227. package/src/app/pages/api/docs/tokens/foundations/[type].ts +32 -0
  228. package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +19 -0
  229. package/src/app/pages/api/registry/components/[id]/files/index.ts +19 -0
  230. package/src/app/pages/api/registry/components/[id].ts +20 -0
  231. package/src/app/pages/api/registry/components/index.ts +19 -0
  232. package/src/app/pages/api/registry/health.ts +20 -0
  233. package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +19 -0
  234. package/src/app/pages/api/registry/patterns/[id]/files/index.ts +19 -0
  235. package/src/app/pages/api/registry/patterns/[id].ts +20 -0
  236. package/src/app/pages/api/registry/patterns/index.ts +19 -0
  237. package/src/app/pages/api/registry/transfer/[kind]/[id].ts +39 -0
  238. package/src/app/pages/api/registry/transfer/assets/[collection].ts +24 -0
  239. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +23 -0
  240. package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +11 -0
  241. package/src/app/pages/api/registry/transfer/assets/index.ts +10 -0
  242. package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +26 -0
  243. package/src/app/pages/api/registry/transfer/tokens/index.ts +11 -0
  244. package/src/app/pages/assets/fonts.tsx +41 -17
  245. package/src/app/pages/assets/index.tsx +2 -2
  246. package/src/app/pages/assets/logos.tsx +6 -3
  247. package/src/app/pages/foundations/colors.tsx +18 -8
  248. package/src/app/pages/foundations/effects.tsx +18 -8
  249. package/src/app/pages/foundations/icons/[name]/index.tsx +28 -16
  250. package/src/app/pages/foundations/icons/index.tsx +18 -12
  251. package/src/app/pages/foundations/index.tsx +1 -1
  252. package/src/app/pages/foundations/logo.tsx +12 -8
  253. package/src/app/pages/foundations/typography.tsx +19 -9
  254. package/src/app/pages/index.tsx +1 -1
  255. package/src/app/pages/system/component/[component]/index.tsx +87 -17
  256. package/src/app/pages/system/index.tsx +13 -4
  257. package/src/app/pages/system/pattern/[pattern]/index.tsx +70 -14
  258. package/src/app/pages/system/tokens/components/[component]/index.tsx +42 -14
  259. package/src/app/pages/system/tokens/foundations/colors.tsx +7 -5
  260. package/src/app/pages/system/tokens/foundations/effects.tsx +7 -5
  261. package/src/app/pages/system/tokens/foundations/typography.tsx +8 -4
  262. package/src/app/tsconfig.json +2 -3
  263. package/src/app-builder/build.ts +555 -11
  264. package/src/app-builder/client-config.ts +61 -4
  265. package/src/app-builder/config-diff/snapshot.ts +1 -11
  266. package/src/app-builder/index.ts +1 -1
  267. package/src/app-builder/nav-shell.ts +74 -0
  268. package/src/app-builder/paths.ts +27 -1
  269. package/src/app-builder/static-export.ts +242 -0
  270. package/src/app-builder/vercel-output.ts +297 -0
  271. package/src/app-builder/watchers/utils.ts +2 -0
  272. package/src/app-builder/watchers.ts +149 -25
  273. package/src/app-builder/websocket.ts +2 -2
  274. package/src/artifacts/README.md +14 -0
  275. package/src/artifacts/index.ts +24 -0
  276. package/src/artifacts/types.ts +146 -0
  277. package/src/artifacts/url.ts +122 -0
  278. package/src/cache/build-cache.ts +47 -2
  279. package/src/commands/build/app.ts +1 -2
  280. package/src/commands/build/index.ts +61 -0
  281. package/src/commands/checkout/index.ts +66 -0
  282. package/src/commands/db/migrate.ts +25 -0
  283. package/src/commands/index.ts +8 -0
  284. package/src/commands/publish/index.ts +63 -0
  285. package/src/config/defaults.ts +15 -0
  286. package/src/config/loader.ts +10 -1
  287. package/src/config/normalizers/declaration.ts +3 -0
  288. package/src/config/normalizers/page.ts +45 -0
  289. package/src/config/normalizers/pattern.ts +4 -1
  290. package/src/config/runtime.ts +48 -2
  291. package/src/index.ts +126 -3
  292. package/src/nav/index.ts +94 -0
  293. package/src/nav/registry.ts +54 -0
  294. package/src/nav/resolver.ts +217 -0
  295. package/src/nav/workspace.ts +87 -0
  296. package/src/pipeline/styles.ts +4 -35
  297. package/src/pipeline/token-transformers.ts +97 -0
  298. package/src/registry/asset-storage/adapters/vercel-blob.ts +51 -0
  299. package/src/registry/asset-storage/define.ts +15 -0
  300. package/src/registry/asset-storage/resolve.ts +72 -0
  301. package/src/registry/asset-storage/types.ts +46 -0
  302. package/src/registry/assets/layout.ts +38 -0
  303. package/src/registry/assets/sets.ts +48 -0
  304. package/src/registry/assets/transfer.ts +50 -0
  305. package/src/registry/checkout/assets.ts +126 -0
  306. package/src/registry/checkout/index.ts +596 -0
  307. package/src/registry/checkout/tokens.ts +152 -0
  308. package/src/registry/client.ts +234 -0
  309. package/src/registry/connection.ts +50 -0
  310. package/src/registry/db/adapter.ts +74 -0
  311. package/src/registry/db/client.ts +88 -0
  312. package/src/registry/db/index.ts +35 -0
  313. package/src/registry/db/migrate.ts +56 -0
  314. package/src/registry/db/schema.ts +444 -0
  315. package/src/registry/path.ts +26 -0
  316. package/src/registry/publish/artifacts.ts +147 -0
  317. package/src/registry/publish/assets.ts +129 -0
  318. package/src/registry/publish/errors.ts +34 -0
  319. package/src/registry/publish/index.ts +128 -0
  320. package/src/registry/publish/package.ts +272 -0
  321. package/src/registry/publish/publish-build.ts +51 -0
  322. package/src/registry/publish/tokens.ts +119 -0
  323. package/src/registry/tokens/sets.ts +132 -0
  324. package/src/registry/tokens/transfer.ts +53 -0
  325. package/src/registry/transfer.ts +82 -0
  326. package/src/store/filesystem-assets.ts +152 -0
  327. package/src/store/filesystem.ts +237 -0
  328. package/src/store/index.ts +62 -0
  329. package/src/store/registry.ts +334 -0
  330. package/src/store/source-files.ts +155 -0
  331. package/src/store/types.ts +210 -0
  332. package/src/transformers/plugins/csf-render.ts +31 -7
  333. package/src/transformers/plugins/handlebars-previews.ts +22 -3
  334. package/src/transformers/plugins/ssr-render.ts +145 -57
  335. package/src/transformers/preview/component/api.ts +2 -1
  336. package/src/transformers/preview/component/artifacts.ts +10 -1
  337. package/src/transformers/preview/component/builder.ts +9 -2
  338. package/src/transformers/preview/component/css.ts +10 -1
  339. package/src/transformers/preview/component/javascript.ts +55 -20
  340. package/src/transformers/preview/component/shared-artifacts.ts +146 -0
  341. package/src/transformers/preview/component.ts +5 -2
  342. package/src/transformers/preview/pattern/builder.ts +33 -7
  343. package/src/transformers/preview/pattern/html.ts +71 -27
  344. package/src/transformers/preview/types.ts +42 -0
  345. package/src/transformers/utils/handlebars.ts +31 -5
  346. package/src/types/config.ts +129 -31
  347. package/src/utils/menu-shell.ts +241 -0
  348. package/src/utils/stable-stringify.ts +7 -0
  349. package/templates/default/package.json.tpl +1 -1
  350. package/handoff.config.js +0 -90
  351. package/src/app/components/ComponentSearch.tsx +0 -93
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Structured artifact and build-metadata model for handoff-app v2.
3
+ *
4
+ * These types are the contract the rest of v2 reads from instead of parsing generated HTML.
5
+ * The workspace (filesystem) and registry (database) tracks both produce and consume this
6
+ * shape, so dependency validation, serving, publish, ingestion, and pattern composition all
7
+ * rely on structured data rather than HTML inspection.
8
+ *
9
+ * This module is a pure prefactor: types and constants only. No artifact is read or written
10
+ * here — consumers (the store, docs read API, build, publish/ingest) come in later issues.
11
+ */
12
+
13
+ /**
14
+ * Logical artifact kind. Mirrors the docs read-model artifact record's `artifactKind` — the
15
+ * broad class an artifact belongs to, independent of how it is stored or served.
16
+ */
17
+ export type ArtifactKind = 'json' | 'html' | 'css' | 'javascript' | 'other';
18
+
19
+ /**
20
+ * What an artifact (or reference) logically belongs to. Shared/global artifacts are owned by
21
+ * `asset` with a `null` owner id; entity-owned artifacts name their component or pattern.
22
+ */
23
+ export type ArtifactOwnerKind = 'component' | 'pattern' | 'asset';
24
+
25
+ /**
26
+ * Kind of a structured artifact reference carried by an HTML artifact. Drives load order,
27
+ * validation, and pattern dedup without ever parsing the HTML.
28
+ */
29
+ export type ArtifactReferenceKind = 'client' | 'style' | 'script' | 'shared' | 'other';
30
+
31
+ /**
32
+ * Build status for an artifact/entity. A successful publish sets `current` with `builtAt`,
33
+ * `builderVersion`, and `artifactHash` all present; metadata-only records report `missing`.
34
+ */
35
+ export type ArtifactBuildStatus = 'current' | 'stale' | 'missing' | 'error';
36
+
37
+ /**
38
+ * A structured reference from one artifact (typically preview/inspect HTML) to another
39
+ * artifact it depends on. References are the single source of truth for dependency
40
+ * resolution — HTML is never parsed for dependencies.
41
+ */
42
+ export interface ArtifactReference {
43
+ /** Logical path of the referenced artifact (e.g. `component/main.css`). */
44
+ path: string;
45
+ /** Reference kind, used for load ordering and dedup. */
46
+ kind: ArtifactReferenceKind;
47
+ /** Whether the referencing artifact cannot render correctly without this reference. */
48
+ required: boolean;
49
+ /** What the referenced artifact belongs to, when known. */
50
+ ownerKind?: ArtifactOwnerKind;
51
+ /** Owner entity id; `null` for shared/global artifacts. */
52
+ ownerId?: string | null;
53
+ /** Optional content type of the referenced artifact. */
54
+ contentType?: string;
55
+ /** Optional format/version tag of the referenced artifact. */
56
+ formatVersion?: string;
57
+ /** Optional build identity the reference was resolved against. */
58
+ buildId?: string;
59
+ /** Optional content hash of the referenced artifact. */
60
+ hash?: string;
61
+ /** Optional byte size of the referenced artifact. */
62
+ size?: number;
63
+ }
64
+
65
+ /**
66
+ * Build metadata for an artifact or entity. Captures enough to validate, serve, and debug docs
67
+ * output.
68
+ */
69
+ export interface ArtifactBuildMetadata {
70
+ /** Current build state for the artifact/entity. */
71
+ status: ArtifactBuildStatus;
72
+ /** ISO timestamp of the build that produced the current artifact, when built. */
73
+ builtAt?: string;
74
+ /** Version of the builder that produced the artifact. */
75
+ builderVersion?: string;
76
+ /** Content hash of the produced artifact. */
77
+ artifactHash?: string;
78
+ /** Optional hash of the source that produced the artifact, for staleness detection. */
79
+ sourceHash?: string;
80
+ /** Optional non-fatal build warnings. */
81
+ warnings?: string[];
82
+ /** Optional fatal build error message. */
83
+ error?: string;
84
+ }
85
+
86
+ /**
87
+ * Size diagnostics for an artifact, used for publish/build reporting.
88
+ */
89
+ export interface ArtifactSizeDiagnostics {
90
+ /** Raw byte size of the artifact content. */
91
+ bytes: number;
92
+ /** Optional gzipped byte size, when measured. */
93
+ gzipBytes?: number;
94
+ }
95
+
96
+ /**
97
+ * Structured description of a single docs read-model artifact. This is the shape the docs read
98
+ * API serves, the static build materializes, and registry publish/ingest moves over the wire —
99
+ * carrying enough to validate, serve, and debug an artifact.
100
+ *
101
+ * `content` and `storageRef` are intentionally both optional so the model leaves room for
102
+ * future object storage of large/binary assets rather than assuming inline content for every
103
+ * kind.
104
+ */
105
+ export interface ArtifactDescriptor {
106
+ /** Logical artifact path (e.g. `component/badge-primary.html`). Stable key for serving. */
107
+ path: string;
108
+ /** Broad artifact kind. */
109
+ artifactKind: ArtifactKind;
110
+ /** Content type to serve the artifact with (e.g. `text/html; charset=utf-8`). */
111
+ contentType: string;
112
+ /** Inline artifact content, when stored inline. */
113
+ content?: string;
114
+ /** Reference to externally stored content (future object storage), when not inline. */
115
+ storageRef?: string;
116
+ /** What the artifact belongs to. */
117
+ ownerKind: ArtifactOwnerKind;
118
+ /** Owner entity id; `null` for shared/global artifacts. */
119
+ ownerId: string | null;
120
+ /** Structured references this artifact depends on (required and optional). */
121
+ references: ArtifactReference[];
122
+ /** Optional format/version tag (e.g. preview/inspect HTML format version). */
123
+ formatVersion?: string;
124
+ /** Build identity that produced this artifact. */
125
+ buildId?: string;
126
+ /** Content hash of the artifact. */
127
+ hash?: string;
128
+ /** Byte size of the artifact. */
129
+ size?: number;
130
+ /** Build metadata for the artifact. */
131
+ build?: ArtifactBuildMetadata;
132
+ /** Optional size diagnostics. */
133
+ diagnostics?: ArtifactSizeDiagnostics;
134
+ }
135
+
136
+ /** Logical path of the shared global stylesheet artifact. */
137
+ export const SHARED_MAIN_CSS_ARTIFACT_PATH = 'component/main.css';
138
+ /** Logical path of the shared global script artifact. */
139
+ export const SHARED_MAIN_JS_ARTIFACT_PATH = 'component/main.js';
140
+ /** Logical path of the shared component styles artifact. */
141
+ export const SHARED_STYLES_CSS_ARTIFACT_PATH = 'component/shared.css';
142
+
143
+ /** Logical path of the per-build artifact metadata manifest. */
144
+ export const ARTIFACT_METADATA_PATH = 'artifact-metadata.json';
145
+ /** Format version tag carried by generated React preview/inspect HTML. */
146
+ export const REACT_PREVIEW_FORMAT_VERSION = 'handoff-react-preview-v1';
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Centralized, basePath-aware canonical artifact URL builder.
3
+ *
4
+ * Every artifact reference emitted in generated HTML and docs read data flows through this one
5
+ * builder. URLs are always of the canonical form:
6
+ *
7
+ * {basePath}/api/docs/artifacts/{artifactPath}
8
+ *
9
+ * intended for use directly in native `href`/`src` attributes. By contract these URLs never carry
10
+ * a query string and never depend on artifact-specific data attributes — artifact references come
11
+ * from structured metadata, not the URL or the HTML.
12
+ *
13
+ * This module is pure and runtime-agnostic (no Node or browser globals) so the build and the
14
+ * Next app can both import it.
15
+ */
16
+
17
+ /** Canonical route prefix all artifact URLs are served under. */
18
+ export const ARTIFACTS_ROUTE_SEGMENT = 'api/docs/artifacts';
19
+
20
+ /** Route prefix for docs read API metadata (`.json`) reads. */
21
+ export const DOCS_ROUTE_SEGMENT = 'api/docs';
22
+
23
+ /** Canonical route prefix all asset content URLs are served under (mode-independent). */
24
+ export const ASSETS_ROUTE_SEGMENT = 'api/docs/assets';
25
+
26
+ /**
27
+ * Normalize a configured basePath to either an empty string or a single-leading-slash,
28
+ * no-trailing-slash form. Mirrors the Next app's `resolveBasePath` so builder output composes
29
+ * correctly with the configured Next `basePath`.
30
+ */
31
+ export const normalizeBasePath = (basePath?: string | null): string => {
32
+ if (!basePath) {
33
+ return '';
34
+ }
35
+ const trimmed = basePath.replace(/^\/+|\/+$/g, '');
36
+ return trimmed ? `/${trimmed}` : '';
37
+ };
38
+
39
+ /**
40
+ * Split a logical artifact path into its non-empty segments. Leading/trailing/duplicate slashes
41
+ * are ignored; backslashes are treated as separators so Windows-style inputs normalize too.
42
+ */
43
+ const toArtifactSegments = (artifactPath: string): string[] =>
44
+ artifactPath
45
+ .split(/[\\/]+/)
46
+ .filter((segment) => segment.length > 0);
47
+
48
+ /**
49
+ * Whether a logical path segment is a relative-traversal marker (`.` or `..`). These survive
50
+ * `encodeURIComponent` (dots are unreserved) and so must be rejected explicitly to keep emitted
51
+ * URLs canonical and contained — mirrors the server resolver's segment validation as defense in
52
+ * depth.
53
+ */
54
+ const isTraversalSegment = (segment: string): boolean => segment === '.' || segment === '..';
55
+
56
+ /**
57
+ * Build a canonical artifact URL for a logical artifact path.
58
+ *
59
+ * Each path segment is individually `encodeURIComponent`-encoded (so a slash separating logical
60
+ * segments is preserved while reserved characters within a segment are escaped), and the
61
+ * configured `basePath` is preserved ahead of the canonical route.
62
+ *
63
+ * @param artifactPath Logical artifact path, e.g. `component/badge-primary.html`.
64
+ * @param basePath Optional configured base path; normalized like the Next app's basePath.
65
+ * @returns A canonical `{basePath}/api/docs/artifacts/{encodedPath}` URL.
66
+ * @throws If `artifactPath` has no usable segments, or contains a `.`/`..` traversal segment.
67
+ */
68
+ export const buildArtifactUrl = (artifactPath: string, basePath?: string | null): string => {
69
+ const segments = toArtifactSegments(artifactPath);
70
+ if (segments.length === 0) {
71
+ throw new Error(`Cannot build artifact URL: empty artifact path "${artifactPath}".`);
72
+ }
73
+ if (segments.some(isTraversalSegment)) {
74
+ throw new Error(`Cannot build artifact URL: path traversal is not allowed in "${artifactPath}".`);
75
+ }
76
+ const encoded = segments.map((segment) => encodeURIComponent(segment)).join('/');
77
+ return `${normalizeBasePath(basePath)}/${ARTIFACTS_ROUTE_SEGMENT}/${encoded}`;
78
+ };
79
+
80
+ /**
81
+ * Build the canonical docs read API URL for a component's detail metadata
82
+ * (`{basePath}/api/docs/components/{id}.json`). The id is URL-encoded; `basePath` is preserved.
83
+ * This is the single source of the metadata-read URL that the build records as an entity's `path`.
84
+ */
85
+ export const buildComponentDetailUrl = (id: string, basePath?: string | null): string =>
86
+ `${normalizeBasePath(basePath)}/${DOCS_ROUTE_SEGMENT}/components/${encodeURIComponent(id)}.json`;
87
+
88
+ /**
89
+ * Build the canonical docs read API URL for a pattern's detail metadata
90
+ * (`{basePath}/api/docs/patterns/{id}.json`). The id is URL-encoded; `basePath` is preserved.
91
+ */
92
+ export const buildPatternDetailUrl = (id: string, basePath?: string | null): string =>
93
+ `${normalizeBasePath(basePath)}/${DOCS_ROUTE_SEGMENT}/patterns/${encodeURIComponent(id)}.json`;
94
+
95
+ /**
96
+ * Build the canonical, basePath-aware content URL for one asset within a collection
97
+ * (`{basePath}/api/docs/assets/{collection}/{logicalPath}`). This is the same route the docs read API
98
+ * serves in every runtime mode: from the database in registry mode, and from statically materialized
99
+ * files in a static export. Each segment is individually encoded and traversal segments are rejected,
100
+ * mirroring {@link buildArtifactUrl}.
101
+ *
102
+ * @throws If the collection or logical path has no usable segments, or contains a `.`/`..` segment.
103
+ */
104
+ export const buildAssetUrl = (collection: string, logicalPath: string, basePath?: string | null): string => {
105
+ const segments = [...toArtifactSegments(collection), ...toArtifactSegments(logicalPath)];
106
+ if (segments.length < 2) {
107
+ throw new Error(`Cannot build asset URL: empty collection ("${collection}") or path ("${logicalPath}").`);
108
+ }
109
+ if (segments.some(isTraversalSegment)) {
110
+ throw new Error(`Cannot build asset URL: path traversal is not allowed in "${collection}/${logicalPath}".`);
111
+ }
112
+ const encoded = segments.map((segment) => encodeURIComponent(segment)).join('/');
113
+ return `${normalizeBasePath(basePath)}/${ASSETS_ROUTE_SEGMENT}/${encoded}`;
114
+ };
115
+
116
+ /**
117
+ * Build the canonical download URL for a collection's whole-collection zip bundle
118
+ * (`{basePath}/api/docs/assets/{collection}/{collection}.zip`) — the default target for the docs
119
+ * "Download" links, overridable via `assets_zip_links`.
120
+ */
121
+ export const buildAssetDownloadUrl = (collection: string, basePath?: string | null): string =>
122
+ buildAssetUrl(collection, `${collection}.zip`, basePath);
@@ -226,6 +226,23 @@ export function getComponentFilePaths(handoff: Handoff, componentId: string): {
226
226
  files.push(matchingConfigPath);
227
227
  }
228
228
 
229
+ // Include every pattern config file whose declaration references this component.
230
+ // When a pattern is added or modified, injectPatternPreviews injects new
231
+ // __pattern_* synthetic preview keys onto the referenced component, changing
232
+ // what processComponents must render. Without tracking these files here, the
233
+ // cache would incorrectly consider the component unchanged and skip the preview
234
+ // rebuild, leaving buildPatterns unable to find the required HTML fragments.
235
+ const runtimePatterns = handoff.runtimeConfig?.entries?.patterns ?? {};
236
+ for (const configPath of configPaths) {
237
+ const entry = handoff.getConfigFileEntry(configPath);
238
+ if (entry?.kind !== 'pattern') continue;
239
+ const pattern = runtimePatterns[entry.entityId];
240
+ if (!pattern) continue;
241
+ if (pattern.components?.some((ref) => ref.id === componentId)) {
242
+ files.push(configPath);
243
+ }
244
+ }
245
+
229
246
  return { files, templateDir };
230
247
  }
231
248
 
@@ -302,8 +319,36 @@ export function hasComponentChanged(
302
319
  * Checks if the component output files exist
303
320
  */
304
321
  export async function checkOutputExists(handoff: Handoff, componentId: string): Promise<boolean> {
305
- const outputPath = path.resolve(handoff.workingPath, 'public/api/component', `${componentId}.json`);
306
- return fs.pathExists(outputPath);
322
+ const outputDirectory = path.resolve(handoff.workingPath, 'public/api/component');
323
+ const componentPath = path.resolve(outputDirectory, `${componentId}.json`);
324
+ if (!(await fs.pathExists(componentPath))) {
325
+ return false;
326
+ }
327
+
328
+ const runtimeComponent = handoff.runtimeConfig?.entries?.components?.[componentId];
329
+ const previewIds = new Set([
330
+ ...Object.keys(runtimeComponent?.previews ?? {}),
331
+ ...Object.keys(runtimeComponent?.internalPatternPreviews ?? {}),
332
+ ]);
333
+
334
+ try {
335
+ const component = await fs.readJson(componentPath);
336
+ for (const previewId of Object.keys(component?.previews ?? {})) {
337
+ previewIds.add(previewId);
338
+ }
339
+ } catch {
340
+ return false;
341
+ }
342
+
343
+ for (const previewId of previewIds) {
344
+ const previewPath = path.resolve(outputDirectory, `${componentId}-${previewId}.html`);
345
+ const inspectPath = path.resolve(outputDirectory, `${componentId}-${previewId}-inspect.html`);
346
+ if (!(await fs.pathExists(previewPath)) || !(await fs.pathExists(inspectPath))) {
347
+ return false;
348
+ }
349
+ }
350
+
351
+ return true;
307
352
  }
308
353
 
309
354
  /**
@@ -16,11 +16,10 @@ const command: CommandModule<{}, BuildAppArgs> = {
16
16
  type: 'boolean',
17
17
  default: false,
18
18
  });
19
- return getSharedOptions(yargs);
20
19
  },
21
20
  handler: async (args: BuildAppArgs) => {
22
21
  const handoff = new Handoff(args.debug, args.force);
23
- await handoff.build(args.skipComponents ?? false);
22
+ await handoff.build('static', args.skipComponents ?? false);
24
23
  },
25
24
  };
26
25
 
@@ -0,0 +1,61 @@
1
+ import { CommandModule } from 'yargs';
2
+ import Handoff from '../../';
3
+ import { DEFAULT_BUILD_TARGET, HandoffBuildError, type BuildPackage, type BuildTarget } from '../../app-builder';
4
+ import { Logger } from '../../utils/logger';
5
+ import { SharedArgs } from '../types';
6
+ import { getSharedOptions } from '../utils';
7
+
8
+ const BUILD_TARGETS: BuildTarget[] = ['static', 'registry'];
9
+ const BUILD_PACKAGES: BuildPackage[] = ['standalone', 'vercel'];
10
+
11
+ export interface BuildArgs extends SharedArgs {
12
+ target?: BuildTarget;
13
+ package?: BuildPackage;
14
+ skipComponents?: boolean;
15
+ }
16
+
17
+ /**
18
+ * `handoff-app build [--target static|registry] [--package standalone|vercel]` — builds the
19
+ * documentation site for the resolved target and packages it for the resolved deliverable.
20
+ * `--target` selects *what* is built; the optional, additive `--package` selects *how* it is
21
+ * packaged. Bare `build` resolves to the static target with the `out/<projectId>` export;
22
+ * `--package` never implies a `--target`.
23
+ */
24
+ const command: CommandModule<{}, BuildArgs> = {
25
+ command: 'build',
26
+ describe: 'Build the design system for the given target (default: static export)',
27
+ builder: (yargs) => {
28
+ return getSharedOptions(yargs)
29
+ .option('target', {
30
+ describe: 'Build target (what is built)',
31
+ choices: BUILD_TARGETS,
32
+ default: DEFAULT_BUILD_TARGET,
33
+ })
34
+ .option('package', {
35
+ describe: 'Packaging format (how it is delivered). Omit for the default per-target deliverable.',
36
+ choices: BUILD_PACKAGES,
37
+ })
38
+ .option('skip-components', {
39
+ describe: 'Skip building components before building the app',
40
+ type: 'boolean',
41
+ default: false,
42
+ });
43
+ },
44
+ handler: async (args: BuildArgs) => {
45
+ const handoff = new Handoff(args.debug, args.force);
46
+ try {
47
+ await handoff.build(args.target ?? DEFAULT_BUILD_TARGET, args.skipComponents ?? false, args.package);
48
+ } catch (error) {
49
+ // Expected business/flow failures (invalid combinations, unsupported runtime modes) print as a
50
+ // single actionable line; unexpected errors re-throw so their stack trace is preserved.
51
+ if (error instanceof HandoffBuildError) {
52
+ Logger.error(error.message);
53
+ process.exitCode = 1;
54
+ return;
55
+ }
56
+ throw error;
57
+ }
58
+ },
59
+ };
60
+
61
+ export default command;
@@ -0,0 +1,66 @@
1
+ import { CommandModule } from 'yargs';
2
+ import Handoff from '../../';
3
+ import { Logger } from '../../utils/logger';
4
+ import { SharedArgs } from '../types';
5
+ import { getSharedOptions } from '../utils';
6
+
7
+ /** Entity kinds checkout-able from a connected workspace. */
8
+ const ENTITY_KINDS = ['component', 'pattern', 'page', 'tokens', 'assets'] as const;
9
+
10
+ export interface CheckoutArgs extends SharedArgs {
11
+ type: (typeof ENTITY_KINDS)[number];
12
+ id?: string;
13
+ }
14
+
15
+ /**
16
+ * `handoff-app checkout <component|pattern|page> <id>` — pull an entity from the connected registry
17
+ * into this workspace: writes its source files in standard authoring form and synthesizes a local
18
+ * declaration.
19
+ *
20
+ * `handoff-app checkout tokens [setId]` — pull every published token set (or only the named set) into
21
+ * this workspace: reconstruct `tokens.json` and restore the generated token files.
22
+ *
23
+ * `handoff-app checkout assets [collection]` pulls every published asset collection (or only the
24
+ * named one) into this workspace, recreating the standard asset files, sprite/manifest, and archives.
25
+ * Available only from a connected workspace (`runtime.mode: workspace` + a configured
26
+ * `registryConnection`). Overwriting existing local files requires `--force` or an interactive
27
+ * confirmation.
28
+ */
29
+ const command: CommandModule<{}, CheckoutArgs> = {
30
+ command: 'checkout <type> [id]',
31
+ describe: 'Pull a component, pattern, page, design tokens, or assets from the connected registry into this workspace',
32
+ builder: (yargs) => {
33
+ return getSharedOptions(yargs)
34
+ .positional('type', {
35
+ describe: 'The kind of entity to checkout',
36
+ choices: ENTITY_KINDS,
37
+ type: 'string',
38
+ })
39
+ .positional('id', {
40
+ describe: 'The stable id of the entity (component/pattern/page id, token set id, or asset collection); optional for tokens/assets',
41
+ type: 'string',
42
+ });
43
+ },
44
+ handler: async (args: CheckoutArgs) => {
45
+ const handoff = new Handoff(args.debug, args.force);
46
+ try {
47
+ if (args.type === 'tokens') {
48
+ await handoff.checkoutTokens(args.id);
49
+ return;
50
+ }
51
+ if (args.type === 'assets') {
52
+ await handoff.checkoutAssets(args.id);
53
+ return;
54
+ }
55
+ if (!args.id) {
56
+ throw new Error(`An id is required to checkout a ${args.type} (e.g. "handoff-app checkout ${args.type} <id>").`);
57
+ }
58
+ await handoff.checkout(args.type, args.id);
59
+ } catch (error) {
60
+ Logger.error(error instanceof Error ? error.message : String(error));
61
+ process.exitCode = 1;
62
+ }
63
+ },
64
+ };
65
+
66
+ export default command;
@@ -0,0 +1,25 @@
1
+ import { CommandModule } from 'yargs';
2
+ import Handoff from '../../';
3
+ import { SharedArgs } from '../types';
4
+ import { getSharedOptions } from '../utils';
5
+
6
+ export interface DbMigrateArgs extends SharedArgs {}
7
+
8
+ /**
9
+ * `handoff-app db:migrate` — runs the package-owned registry database migrations against the
10
+ * configured PostgreSQL/Neon database. Reads the consuming project's config + DB env vars and runs
11
+ * independently of `build`.
12
+ */
13
+ const command: CommandModule<{}, DbMigrateArgs> = {
14
+ command: 'db:migrate',
15
+ describe: 'Run registry database migrations (Drizzle / PostgreSQL)',
16
+ builder: (yargs) => {
17
+ return getSharedOptions(yargs);
18
+ },
19
+ handler: async (args: DbMigrateArgs) => {
20
+ const handoff = new Handoff(args.debug, args.force);
21
+ await handoff.dbMigrate();
22
+ },
23
+ };
24
+
25
+ export default command;
@@ -1,5 +1,8 @@
1
+ import buildCommand from './build';
1
2
  import buildAppCommand from './build/app';
2
3
  import buildComponentsCommand from './build/components';
4
+ import checkoutCommand from './checkout';
5
+ import dbMigrateCommand from './db/migrate';
3
6
  import devCommand from './dev';
4
7
  import ejectConfigCommand from './eject/config';
5
8
  import ejectPagesCommand from './eject/pages';
@@ -9,13 +12,17 @@ import initCommand from './init';
9
12
  import makeComponentCommand from './make/component';
10
13
  import makePageCommand from './make/page';
11
14
  import makeTemplateCommand from './make/template';
15
+ import publishCommand from './publish';
12
16
  import scaffoldCommand from './scaffold';
13
17
  import startCommand from './start';
14
18
  import validateComponentsCommand from './validate/components';
15
19
 
16
20
  export const commands = [
21
+ buildCommand,
17
22
  buildAppCommand,
18
23
  buildComponentsCommand,
24
+ checkoutCommand,
25
+ dbMigrateCommand,
19
26
  devCommand,
20
27
  ejectConfigCommand,
21
28
  ejectPagesCommand,
@@ -25,6 +32,7 @@ export const commands = [
25
32
  makePageCommand,
26
33
  makeComponentCommand,
27
34
  makeTemplateCommand,
35
+ publishCommand,
28
36
  scaffoldCommand,
29
37
  startCommand,
30
38
  validateComponentsCommand,
@@ -0,0 +1,63 @@
1
+ import { CommandModule } from 'yargs';
2
+ import Handoff from '../../';
3
+ import { Logger } from '../../utils/logger';
4
+ import { SharedArgs } from '../types';
5
+ import { getSharedOptions } from '../utils';
6
+
7
+ /** Entity kinds publishable from a connected workspace. */
8
+ const ENTITY_KINDS = ['component', 'pattern', 'page', 'tokens', 'assets'] as const;
9
+
10
+ export interface PublishArgs extends SharedArgs {
11
+ type: (typeof ENTITY_KINDS)[number];
12
+ id?: string;
13
+ }
14
+
15
+ /**
16
+ * `handoff-app publish <component|pattern|page> <id>` — fresh targeted local build + upload of the
17
+ * selected entity to the connected registry.
18
+ *
19
+ * `handoff-app publish tokens [setId]` — fresh token build + upload of every logical token set, or
20
+ * only the named set (`foundation/colors`, `component/button`).
21
+ *
22
+ * `handoff-app publish assets [collection]` runs a fresh build and uploads every asset collection, or
23
+ * only the named one (`icons`, `logos`, `fonts`). Available only from a connected workspace
24
+ * (`runtime.mode: workspace` + a configured `registryConnection`).
25
+ */
26
+ const command: CommandModule<{}, PublishArgs> = {
27
+ command: 'publish <type> [id]',
28
+ describe: 'Build and publish a component, pattern, page, design tokens, or assets to the connected registry',
29
+ builder: (yargs) => {
30
+ return getSharedOptions(yargs)
31
+ .positional('type', {
32
+ describe: 'The kind of entity to publish',
33
+ choices: ENTITY_KINDS,
34
+ type: 'string',
35
+ })
36
+ .positional('id', {
37
+ describe: 'The stable id of the entity (component/pattern/page id, token set id, or asset collection); optional for tokens/assets',
38
+ type: 'string',
39
+ });
40
+ },
41
+ handler: async (args: PublishArgs) => {
42
+ const handoff = new Handoff(args.debug, args.force);
43
+ try {
44
+ if (args.type === 'tokens') {
45
+ await handoff.publishTokens(args.id);
46
+ return;
47
+ }
48
+ if (args.type === 'assets') {
49
+ await handoff.publishAssets(args.id);
50
+ return;
51
+ }
52
+ if (!args.id) {
53
+ throw new Error(`An id is required to publish a ${args.type} (e.g. "handoff-app publish ${args.type} <id>").`);
54
+ }
55
+ await handoff.publish(args.type, args.id);
56
+ } catch (error) {
57
+ Logger.error(error instanceof Error ? error.message : String(error));
58
+ process.exitCode = 1;
59
+ }
60
+ },
61
+ };
62
+
63
+ export default command;
@@ -1,3 +1,4 @@
1
+ import { resolveRegistryConnection } from '../registry/connection';
1
2
  import { ClientConfig, Config } from '../types/config';
2
3
 
3
4
  export interface ImageStyle {
@@ -11,6 +12,9 @@ export interface ImageStyle {
11
12
  export const defaultConfig = (): Config => ({
12
13
  dev_access_token: process.env.HANDOFF_DEV_ACCESS_TOKEN ?? null,
13
14
  figma_project_id: process.env.HANDOFF_FIGMA_PROJECT_ID ?? null,
15
+ runtime: {
16
+ mode: 'workspace',
17
+ },
14
18
  exportsOutputDirectory: process.env.HANDOFF_OUTPUT_DIR ?? 'exported',
15
19
  sitesOutputDirectory: process.env.HANDOFF_SITES_DIR ?? 'out',
16
20
  useVariables: process.env.HANDOFF_USE_VARIABLES === 'true',
@@ -66,13 +70,24 @@ export const getClientConfig = (config: Config): ClientConfig => {
66
70
  sitesOutputDirectory,
67
71
  assets_zip_links = { icons: null, logos: null },
68
72
  useVariables,
73
+ runtime,
69
74
  } = { ...defaultConfig(), ...config };
70
75
 
76
+ const mode = runtime?.mode ?? 'workspace';
77
+ // Connected-workspace affordance: only a workspace with a resolvable registry URL is "connected".
78
+ // URL-only threshold (no token required) so the publish hint surfaces before the access token is
79
+ // set. Never project the URL/token value itself — only this boolean crosses to the browser.
80
+ const connected = mode === 'workspace' && Boolean(resolveRegistryConnection(config).url);
81
+
71
82
  return {
72
83
  app,
73
84
  exportsOutputDirectory,
74
85
  sitesOutputDirectory,
75
86
  assets_zip_links,
76
87
  useVariables,
88
+ runtime: {
89
+ mode,
90
+ connected,
91
+ },
77
92
  };
78
93
  };
@@ -1,5 +1,6 @@
1
1
  import esbuild from 'esbuild';
2
2
  import fs from 'fs-extra';
3
+ import merge from 'lodash/merge';
3
4
  import { createRequire } from 'module';
4
5
  import path from 'path';
5
6
  import { Config } from '../types/config';
@@ -109,7 +110,15 @@ export const initConfigWithMetadata = (configOverride?: Partial<Config>): Config
109
110
  });
110
111
  }
111
112
 
112
- const resolvedConfig = { ...defaultConfig(), ...config } as Config;
113
+ const defaults = defaultConfig();
114
+ // Top-level merge stays shallow to preserve 1.x semantics, except for the `runtime`
115
+ // block which is deep-merged so a partial user `runtime` (e.g. only `mode`) does not
116
+ // wipe the defaults supplied for the rest of the block.
117
+ const resolvedConfig = {
118
+ ...defaults,
119
+ ...config,
120
+ runtime: merge({}, defaults.runtime, config.runtime),
121
+ } as Config;
113
122
  return { config: resolvedConfig, configPath };
114
123
  };
115
124
 
@@ -172,5 +172,8 @@ export const normalizeComponentDeclaration = (raw: RawDeclaration, options: Norm
172
172
  renderer: resolvedRenderer,
173
173
  entries,
174
174
  previews: normalizedPreviews,
175
+ // Source location of the entity (declaration directory), retained so the store can expose a
176
+ // stable source reference for checkout/publish.
177
+ path: path.dirname(options.declarationPath),
175
178
  };
176
179
  };