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,119 @@
1
+ /**
2
+ * Connected-workspace token publish orchestration.
3
+ *
4
+ * `publish tokens [setId]` runs a **fresh token build** (`handoff.fetch()` — Figma extract +
5
+ * `buildStyles`) so the local `tokens.json` and generated token outputs are current, discovers the
6
+ * logical sets from that document, then uploads each selected set (its extracted record + generated
7
+ * artifacts) to the connected registry. Each set is one atomic upsert; a set whose deterministic
8
+ * `sourceHash` matches the registry's is skipped. Absent sets are never deleted (explicit deletion is
9
+ * a separate management action).
10
+ */
11
+
12
+ import Handoff from '../../index';
13
+ import type { TokenSetRecord } from '../../store/types';
14
+ import { Logger } from '../../utils/logger';
15
+ import { stableStringify } from '../../utils/stable-stringify';
16
+ import { createRegistryClient } from '../client';
17
+ import type { TokenSetSummary, TokenSetTransferArtifact, TokenSetTransferPackage } from '../tokens/transfer';
18
+ import { describePublishError } from './errors';
19
+ import { PublishError, resolveConnectionOrThrow } from './index';
20
+ import { createCurrentBuild, hashPathValues, sha256 } from './publish-build';
21
+
22
+ /**
23
+ * Deterministic content hash for a token set: the key-order-independent record serialization plus the
24
+ * path-sorted artifact bytes. Independent of JSON key insertion order, so an unchanged set hashes the
25
+ * same across rebuilds and skip-unchanged works reliably.
26
+ */
27
+ const hashTokenSet = (record: unknown, artifacts: TokenSetTransferArtifact[]): string => {
28
+ return hashPathValues(
29
+ artifacts.map((artifact) => ({ path: artifact.path, value: artifact.content })),
30
+ stableStringify(record)
31
+ );
32
+ };
33
+
34
+ /** Assemble one set's publish package from the freshly generated local token output. */
35
+ const buildTokenSetPackage = async (handoff: Handoff, set: TokenSetRecord): Promise<TokenSetTransferPackage> => {
36
+ const artifacts: TokenSetTransferArtifact[] = (await handoff.store.tokens.getArtifacts(set.id)).map((artifact) => ({
37
+ path: artifact.path,
38
+ format: artifact.format,
39
+ content: artifact.content,
40
+ contentType: artifact.contentType,
41
+ hash: sha256(artifact.content),
42
+ size: Buffer.byteLength(artifact.content, 'utf8'),
43
+ }));
44
+
45
+ return {
46
+ id: set.id,
47
+ kind: set.kind,
48
+ record: set.record,
49
+ artifacts,
50
+ build: createCurrentBuild({ sourceHash: hashTokenSet(set.record, artifacts) }),
51
+ };
52
+ };
53
+
54
+ /**
55
+ * Publish all discovered token sets, or a single set when `setId` is given. Runs a fresh token build,
56
+ * skips sets whose content is unchanged on the registry, and reports published/unchanged/failed
57
+ * counts. A per-set upload failure is collected and never leaves a partially written set (each set is
58
+ * committed atomically server-side); the run throws at the end if any set failed.
59
+ */
60
+ export const publishTokens = async (handoff: Handoff, setId?: string): Promise<void> => {
61
+ const connection = resolveConnectionOrThrow(handoff);
62
+
63
+ Logger.info(setId ? `Building tokens to publish "${setId}"…` : 'Building tokens for publish…');
64
+ await handoff.fetch();
65
+
66
+ const allSets = await handoff.store.tokens.listSets();
67
+ let targetSets = allSets;
68
+ if (setId) {
69
+ const found = allSets.find((set) => set.id === setId);
70
+ if (!found) {
71
+ throw new PublishError(
72
+ `Token set "${setId}" was not found in the generated tokens. Discovered sets: ${allSets.map((set) => set.id).join(', ') || '(none)'}.`
73
+ );
74
+ }
75
+ targetSets = [found];
76
+ }
77
+
78
+ const packages = await Promise.all(targetSets.map((set) => buildTokenSetPackage(handoff, set)));
79
+
80
+ const client = createRegistryClient({ baseUrl: connection.url, accessToken: connection.accessToken });
81
+ let remote: TokenSetSummary[] = [];
82
+ try {
83
+ remote = await client.listTokenSets();
84
+ } catch (error) {
85
+ // If the summary listing fails we simply publish everything (the server still no-ops unchanged sets).
86
+ Logger.info('Could not read current registry token sets; publishing all selected sets.');
87
+ }
88
+ const remoteHashById = new Map(remote.map((entry) => [entry.id, entry.sourceHash]));
89
+
90
+ let published = 0;
91
+ let unchanged = 0;
92
+ const failed: { id: string; message: string }[] = [];
93
+
94
+ for (const pkg of packages) {
95
+ const remoteHash = remoteHashById.get(pkg.id);
96
+ if (remoteHash && remoteHash === pkg.build.sourceHash) {
97
+ unchanged += 1;
98
+ Logger.info(`Unchanged: ${pkg.id}`);
99
+ continue;
100
+ }
101
+ try {
102
+ await client.publishTokens(pkg);
103
+ published += 1;
104
+ Logger.info(`Published: ${pkg.id} (${pkg.artifacts.length} artifact(s))`);
105
+ } catch (error) {
106
+ failed.push({ id: pkg.id, message: describePublishError(error, connection.url, 'token set') });
107
+ }
108
+ }
109
+
110
+ Logger.success(
111
+ `Tokens publish complete — ${published} published, ${unchanged} unchanged${failed.length ? `, ${failed.length} failed` : ''}.`
112
+ );
113
+ if (failed.length > 0) {
114
+ for (const failure of failed) {
115
+ Logger.error(` - ${failure.id}: ${failure.message}`);
116
+ }
117
+ throw new PublishError(`${failed.length} token set(s) failed to publish.`);
118
+ }
119
+ };
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Logical token-set derivation — the pure, dependency-light boundary between the generated token
3
+ * document and the registry token entity.
4
+ *
5
+ * A **logical token set** is the registry entity boundary for tokens. Its stable ids are:
6
+ *
7
+ * ```text
8
+ * foundation/colors
9
+ * foundation/typography
10
+ * foundation/effects
11
+ * component/<componentId>
12
+ * ```
13
+ *
14
+ * The set `record` is the exact slice extracted from the generated `tokens.json`
15
+ * ({@link CoreTypes.IDocumentationObject}) — no lossy normalization, so new token properties flow
16
+ * through without a database migration. This module is imported by the store, publish, checkout, and
17
+ * server ingestion, so it stays free of fs/DB/React dependencies.
18
+ */
19
+
20
+ import type { Types as CoreTypes } from 'handoff-core';
21
+ import { isSafePathSegment } from '../path';
22
+
23
+ /** Kind of a logical token set. */
24
+ export type TokenSetKind = 'foundation' | 'component';
25
+
26
+ /** A logical token set: its stable id, kind, and the exact extracted token slice. */
27
+ export interface DerivedTokenSet {
28
+ id: string;
29
+ kind: TokenSetKind;
30
+ /** The exact slice from `tokens.json` — `IColorObject[]`/`ITypographyObject[]`/`IEffectObject[]` for a foundation, `IFileComponentObject` for a component. */
31
+ record: unknown;
32
+ }
33
+
34
+ /** The three stable foundation set ids, in deterministic order. */
35
+ export const FOUNDATION_SET_IDS = ['foundation/colors', 'foundation/typography', 'foundation/effects'] as const;
36
+
37
+ /**
38
+ * Map a foundation set name (the segment after `foundation/`, and the generated-file base name) to
39
+ * its `localStyles` key. Note the plural set names (`colors`/`effects`) map to the core's singular
40
+ * `localStyles.color`/`localStyles.effect`.
41
+ */
42
+ const LOCAL_STYLES_KEY_BY_FOUNDATION: Record<string, keyof CoreTypes.IDocumentationObject['localStyles']> = {
43
+ colors: 'color',
44
+ typography: 'typography',
45
+ effects: 'effect',
46
+ };
47
+
48
+ /** Whether a set id refers to a component token set. */
49
+ export const isComponentSet = (id: string): boolean => id.startsWith('component/');
50
+
51
+ export const isTokenSetId = (id: string): boolean => {
52
+ if ((FOUNDATION_SET_IDS as readonly string[]).includes(id)) {
53
+ return true;
54
+ }
55
+ return isComponentSet(id) && isSafePathSegment(id.slice('component/'.length));
56
+ };
57
+
58
+ /** The kind of a set id. */
59
+ export const kindForSetId = (id: string): TokenSetKind => (isComponentSet(id) ? 'component' : 'foundation');
60
+
61
+ /**
62
+ * The generated-file base name for a set: `colors`/`typography`/`effects` for a foundation, the
63
+ * component id for a component set. This is the name the style transformers write files under
64
+ * (`<outDir>/<name>.<format>`).
65
+ */
66
+ export const setNameForId = (id: string): string => (isComponentSet(id) ? id.slice('component/'.length) : (id.split('/')[1] ?? id));
67
+
68
+ /** The `foundations/<type>` page type (`colors`/`typography`/`effects`) for a foundation set id, else null. */
69
+ export const foundationTypeForId = (id: string): string | null => (isComponentSet(id) ? null : (id.split('/')[1] ?? null));
70
+
71
+ /**
72
+ * Split the generated token document into logical sets: the three foundation sets (each possibly an
73
+ * empty array) plus one set per component. Foundation sets are always emitted so a set can carry zero
74
+ * tokens and so a token removed from a set is reflected as a shrunk `record` on republish.
75
+ */
76
+ export const deriveTokenSets = (doc: CoreTypes.IDocumentationObject | null | undefined): DerivedTokenSet[] => {
77
+ const localStyles = doc?.localStyles ?? ({} as CoreTypes.IDocumentationObject['localStyles']);
78
+ const sets: DerivedTokenSet[] = [
79
+ { id: 'foundation/colors', kind: 'foundation', record: localStyles.color ?? [] },
80
+ { id: 'foundation/typography', kind: 'foundation', record: localStyles.typography ?? [] },
81
+ { id: 'foundation/effects', kind: 'foundation', record: localStyles.effect ?? [] },
82
+ ];
83
+
84
+ const components = doc?.components ?? {};
85
+ for (const componentId of Object.keys(components)) {
86
+ sets.push({ id: `component/${componentId}`, kind: 'component', record: components[componentId] });
87
+ }
88
+
89
+ return sets;
90
+ };
91
+
92
+ /** A minimal default token document, used when no local `tokens.json` exists yet on checkout. */
93
+ export const emptyTokenDocument = (): CoreTypes.IDocumentationObject =>
94
+ ({
95
+ localStyles: { color: [], typography: [], effect: [] },
96
+ components: {},
97
+ assets: {},
98
+ }) as CoreTypes.IDocumentationObject;
99
+
100
+ /**
101
+ * Splice a set of logical token sets into a token document, returning the merged document. Untouched
102
+ * sets are preserved (single-set checkout never clobbers other sets); a foundation set replaces its
103
+ * `localStyles` slice, a component set replaces `components[<id>]`.
104
+ */
105
+ export const mergeTokenSetsIntoDocument = (
106
+ existing: CoreTypes.IDocumentationObject | null | undefined,
107
+ sets: DerivedTokenSet[]
108
+ ): CoreTypes.IDocumentationObject => {
109
+ const base = existing ?? emptyTokenDocument();
110
+ const doc: CoreTypes.IDocumentationObject = {
111
+ ...base,
112
+ localStyles: { ...(base.localStyles ?? emptyTokenDocument().localStyles) },
113
+ components: { ...(base.components ?? {}) },
114
+ } as CoreTypes.IDocumentationObject;
115
+
116
+ for (const set of sets) {
117
+ if (isComponentSet(set.id)) {
118
+ doc.components[setNameForId(set.id)] = set.record as CoreTypes.IFileComponentObject;
119
+ continue;
120
+ }
121
+ const localStylesKey = LOCAL_STYLES_KEY_BY_FOUNDATION[setNameForId(set.id)];
122
+ if (localStylesKey) {
123
+ (doc.localStyles as Record<string, unknown>)[localStylesKey] = set.record;
124
+ }
125
+ }
126
+
127
+ return doc;
128
+ };
129
+
130
+ /** The `localStyles` key backing a `foundations/<type>` page (`colors`→`color`, `effects`→`effect`). */
131
+ export const localStylesKeyForFoundationType = (type: string): keyof CoreTypes.IDocumentationObject['localStyles'] | null =>
132
+ LOCAL_STYLES_KEY_BY_FOUNDATION[type] ?? null;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Transfer payload contract for token sets, shared by the connected-workspace publish/checkout client
3
+ * and the registry-side token ingestion endpoint.
4
+ *
5
+ * Tokens are their own transfer concern (a `record` slice + byte-for-byte generated artifacts, no
6
+ * source files and no artifact reference graph), so they use this dedicated shape rather than the
7
+ * component/pattern/page {@link import('../transfer').TransferPackage}. Types only — no behavior.
8
+ */
9
+
10
+ import type { TransferBuild } from '../transfer';
11
+ import type { ArtifactBuildStatus } from '../../artifacts/types';
12
+ import type { TokenSetKind } from './sets';
13
+
14
+ /** A generated token artifact in a transfer package (CSS/SCSS/Style Dictionary/types/custom output). */
15
+ export interface TokenSetTransferArtifact {
16
+ /** Registry-safe relative output path under the tokens dir (e.g. `css/colors.css`). */
17
+ path: string;
18
+ /** Logical format label (`css`|`scss`|`types`|`styleDictionary`|custom outDir). */
19
+ format: string;
20
+ content: string;
21
+ contentType: string;
22
+ hash?: string;
23
+ size?: number;
24
+ }
25
+
26
+ /**
27
+ * The full token set publish package uploaded to `PUT /api/registry/transfer/tokens/:setId`: the
28
+ * extracted record slice, the generated artifacts, and build metadata keyed by `sourceHash`.
29
+ */
30
+ export interface TokenSetTransferPackage {
31
+ id: string;
32
+ kind: TokenSetKind;
33
+ /** The exact extracted token slice (`IColorObject[]`/…/`IFileComponentObject`). */
34
+ record: unknown;
35
+ artifacts: TokenSetTransferArtifact[];
36
+ build: TransferBuild;
37
+ }
38
+
39
+ /** The checkout payload returned by `GET /api/registry/transfer/tokens/:setId`. */
40
+ export interface TokenSetCheckoutPayload {
41
+ id: string;
42
+ kind: TokenSetKind;
43
+ record: unknown;
44
+ artifacts: TokenSetTransferArtifact[];
45
+ }
46
+
47
+ /** A lightweight token-set summary (`GET /api/registry/transfer/tokens`) driving skip-unchanged + bulk checkout. */
48
+ export interface TokenSetSummary {
49
+ id: string;
50
+ kind: TokenSetKind;
51
+ sourceHash: string | null;
52
+ status: ArtifactBuildStatus | null;
53
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Transfer payload contract shared by the connected-workspace publish client and the registry-side
3
+ * ingestion endpoint.
4
+ *
5
+ * Publish is the only path allowed to set render/build-defining fields, source files, rendered
6
+ * artifacts, and build metadata. This module is the single shared description of that wire shape so
7
+ * the CLI (which builds and uploads the package) and the registry API (which validates and ingests
8
+ * it) never drift. Types only — no runtime behavior.
9
+ */
10
+
11
+ import type { ArtifactBuildStatus, ArtifactKind, ArtifactOwnerKind, ArtifactReference } from '../artifacts/types';
12
+ import type { RegistryTextFileKind } from '../store/types';
13
+
14
+ /** Entity kinds that can be transferred (published/checked out). */
15
+ export type TransferEntityKind = 'component' | 'pattern' | 'page';
16
+
17
+ /**
18
+ * A registry-safe source file in a transfer package. Declarations are workspace-only (synthesized on
19
+ * checkout) and are never present here — `kind` excludes `declaration` by type.
20
+ */
21
+ export interface TransferFile {
22
+ /** Registry-safe relative path within the entity (e.g. `Badge.tsx`). */
23
+ path: string;
24
+ kind: RegistryTextFileKind;
25
+ content: string;
26
+ contentType: string;
27
+ }
28
+
29
+ /**
30
+ * A rendered docs read-model artifact in a transfer package. Carries the structured ownership and
31
+ * references the registry stores so it can validate, serve, and dedup the artifact without ever
32
+ * parsing HTML.
33
+ */
34
+ export interface TransferArtifact {
35
+ /** Logical artifact path (e.g. `component/badge-primary.html`, `component/main.css`). */
36
+ path: string;
37
+ artifactKind: ArtifactKind;
38
+ content: string;
39
+ contentType: string;
40
+ /** What the artifact belongs to — shared/global artifacts are `asset` with a `null` owner id. */
41
+ ownerKind: ArtifactOwnerKind;
42
+ ownerId: string | null;
43
+ /** Structured references this artifact depends on (drives required-reference validation). */
44
+ references?: ArtifactReference[];
45
+ formatVersion?: string;
46
+ hash?: string;
47
+ size?: number;
48
+ }
49
+
50
+ /** Build metadata for the published entity. */
51
+ export interface TransferBuild {
52
+ status: ArtifactBuildStatus;
53
+ builtAt?: string;
54
+ builderVersion?: string;
55
+ artifactHash?: string;
56
+ sourceHash?: string;
57
+ warnings?: string[];
58
+ error?: string;
59
+ }
60
+
61
+ /**
62
+ * The full publish package uploaded to `PUT /api/registry/transfer/{component|pattern|page}/:id`.
63
+ * Rendered artifacts are included when the entity has a render pipeline.
64
+ */
65
+ export interface TransferPackage {
66
+ /** Normalized component, pattern, or page record stored as the served record. */
67
+ item: Record<string, unknown>;
68
+ files: TransferFile[];
69
+ artifacts: TransferArtifact[];
70
+ build: TransferBuild;
71
+ }
72
+
73
+ /**
74
+ * The checkout payload returned by `GET /api/registry/transfer/{component|pattern|page}/:id`.
75
+ * Declarations are omitted because the consuming workspace creates them locally.
76
+ */
77
+ export interface CheckoutPayload {
78
+ kind: TransferEntityKind;
79
+ /** Normalized record used to reconstruct the entity. */
80
+ item: Record<string, unknown>;
81
+ files: TransferFile[];
82
+ }
@@ -0,0 +1,152 @@
1
+ import crypto from 'crypto';
2
+ import fs from 'fs-extra';
3
+ import { Types as CoreTypes } from 'handoff-core';
4
+ import path from 'path';
5
+ import { resolveAssetPhysicalPath, type AssetPhysicalRoots } from '../registry/assets/layout';
6
+ import { ASSET_COLLECTIONS, assetContentType } from '../registry/assets/sets';
7
+ import type { FilesystemStoreContext } from './filesystem';
8
+ import type { AssetContentResource, AssetMetadata, AssetStore } from './types';
9
+
10
+ const sha256 = (bytes: Buffer): string => crypto.createHash('sha256').update(bytes).digest('hex');
11
+
12
+ interface AssetFileEntry {
13
+ logicalPath: string;
14
+ name: string;
15
+ metadata?: Record<string, unknown> | null;
16
+ absolutePath: string;
17
+ }
18
+
19
+ const isFile = (absolutePath: string): boolean => {
20
+ try {
21
+ return fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile();
22
+ } catch {
23
+ return false;
24
+ }
25
+ };
26
+
27
+ /**
28
+ * Filesystem backed asset store. Enumerates each collection from the generated `public/api` tree
29
+ * plus the downloadable ZIP and font archives.
30
+ */
31
+ export class FilesystemAssetStore implements AssetStore {
32
+ constructor(private readonly context: FilesystemStoreContext) {}
33
+
34
+ private roots(): AssetPhysicalRoots | null {
35
+ const apiPath = this.context.getAssetsApiPath?.();
36
+ const iconsZip = this.context.getIconsZipFilePath?.();
37
+ const logosZip = this.context.getLogosZipFilePath?.();
38
+ const workingPath = this.context.workingPath;
39
+ if (!apiPath || !iconsZip || !logosZip || !workingPath) {
40
+ return null;
41
+ }
42
+ return { apiPath, iconsZip, logosZip, workingPath };
43
+ }
44
+
45
+ private enumerate(collection: string): AssetFileEntry[] {
46
+ const roots = this.roots();
47
+ if (!roots) {
48
+ return [];
49
+ }
50
+
51
+ if (collection === 'icons' || collection === 'logos') {
52
+ const entries: AssetFileEntry[] = [];
53
+ const collectionJson = `assets/${collection}.json`;
54
+ let assetObjects: CoreTypes.IAssetObject[] = [];
55
+ try {
56
+ const raw = fs.readFileSync(resolveAssetPhysicalPath(collectionJson, roots), 'utf8');
57
+ const parsed = JSON.parse(raw);
58
+ if (Array.isArray(parsed)) assetObjects = parsed as CoreTypes.IAssetObject[];
59
+ } catch {
60
+ assetObjects = [];
61
+ }
62
+ for (const asset of assetObjects) {
63
+ const logicalPath = `assets/${collection}/${asset.path}`;
64
+ entries.push({
65
+ logicalPath,
66
+ name: asset.name,
67
+ metadata: { icon: asset.icon, index: asset.index, description: asset.description ?? null },
68
+ absolutePath: resolveAssetPhysicalPath(logicalPath, roots),
69
+ });
70
+ }
71
+ const extras =
72
+ collection === 'icons'
73
+ ? [collectionJson, 'icons-sprite.svg', 'icons-sprite-manifest.json', 'icons.zip']
74
+ : [collectionJson, 'logos.zip'];
75
+ for (const logicalPath of extras) {
76
+ entries.push({ logicalPath, name: path.basename(logicalPath), absolutePath: resolveAssetPhysicalPath(logicalPath, roots) });
77
+ }
78
+ return entries.filter((entry) => isFile(entry.absolutePath));
79
+ }
80
+
81
+ if (collection === 'fonts') {
82
+ const fontsDir = path.resolve(roots.workingPath, 'fonts');
83
+ if (!fs.existsSync(fontsDir)) {
84
+ return [];
85
+ }
86
+ try {
87
+ return fs
88
+ .readdirSync(fontsDir)
89
+ .filter((file) => file.toLowerCase().endsWith('.zip'))
90
+ .map((file) => ({ logicalPath: `fonts/${file}`, name: file, absolutePath: path.join(fontsDir, file) }))
91
+ .filter((entry) => isFile(entry.absolutePath));
92
+ } catch {
93
+ return [];
94
+ }
95
+ }
96
+
97
+ return [];
98
+ }
99
+
100
+ private toMetadata(collection: string, entry: AssetFileEntry, bytes: Buffer): AssetMetadata {
101
+ return {
102
+ collection,
103
+ path: entry.logicalPath,
104
+ name: entry.name,
105
+ contentType: assetContentType(entry.logicalPath),
106
+ size: bytes.length,
107
+ contentHash: sha256(bytes),
108
+ metadata: entry.metadata ?? null,
109
+ };
110
+ }
111
+
112
+ listCollections(): string[] {
113
+ return ASSET_COLLECTIONS.filter((collection) => this.enumerate(collection).length > 0);
114
+ }
115
+
116
+ listAssets(collection: string): AssetMetadata[] {
117
+ const out: AssetMetadata[] = [];
118
+ for (const entry of this.enumerate(collection)) {
119
+ try {
120
+ out.push(this.toMetadata(collection, entry, fs.readFileSync(entry.absolutePath)));
121
+ } catch {
122
+ // Skip files that vanished between enumeration and read.
123
+ }
124
+ }
125
+ return out;
126
+ }
127
+
128
+ getAsset(collection: string, assetPath: string): AssetMetadata | null {
129
+ const entry = this.enumerate(collection).find((candidate) => candidate.logicalPath === assetPath);
130
+ if (!entry) {
131
+ return null;
132
+ }
133
+ try {
134
+ return this.toMetadata(collection, entry, fs.readFileSync(entry.absolutePath));
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+
140
+ getAssetContent(collection: string, assetPath: string): AssetContentResource | null {
141
+ const entry = this.enumerate(collection).find((candidate) => candidate.logicalPath === assetPath);
142
+ if (!entry) {
143
+ return null;
144
+ }
145
+ try {
146
+ const bytes = fs.readFileSync(entry.absolutePath);
147
+ return { ...this.toMetadata(collection, entry, bytes), body: bytes };
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+ }