fundus 0.16.0 → 0.17.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 (194) hide show
  1. package/README.md +37 -1
  2. package/build/client/_app/immutable/assets/{AssetSourceMenu.B5fzYfym.css → AssetSourceMenu.Buj2UxUg.css} +1 -1
  3. package/build/client/_app/immutable/assets/AssetSourceMenu.Buj2UxUg.css.br +0 -0
  4. package/build/client/_app/immutable/assets/{AssetSourceMenu.B5fzYfym.css.gz → AssetSourceMenu.Buj2UxUg.css.gz} +0 -0
  5. package/build/client/_app/immutable/chunks/{D6c2mfMB.js → 5fq8ySvR.js} +1 -1
  6. package/build/client/_app/immutable/chunks/5fq8ySvR.js.br +0 -0
  7. package/build/client/_app/immutable/chunks/5fq8ySvR.js.gz +0 -0
  8. package/build/client/_app/immutable/chunks/BcVZHIBD.js +1 -0
  9. package/build/client/_app/immutable/chunks/BcVZHIBD.js.br +0 -0
  10. package/build/client/_app/immutable/chunks/BcVZHIBD.js.gz +0 -0
  11. package/build/client/_app/immutable/chunks/DXiUbZ9S.js +2 -0
  12. package/build/client/_app/immutable/chunks/DXiUbZ9S.js.br +0 -0
  13. package/build/client/_app/immutable/chunks/DXiUbZ9S.js.gz +0 -0
  14. package/build/client/_app/immutable/chunks/H5KnxQLQ.js +108 -0
  15. package/build/client/_app/immutable/chunks/H5KnxQLQ.js.br +0 -0
  16. package/build/client/_app/immutable/chunks/H5KnxQLQ.js.gz +0 -0
  17. package/build/client/_app/immutable/chunks/{B97y-eFA.js → jEVVu5G9.js} +1 -1
  18. package/build/client/_app/immutable/chunks/jEVVu5G9.js.br +0 -0
  19. package/build/client/_app/immutable/chunks/jEVVu5G9.js.gz +0 -0
  20. package/build/client/_app/immutable/entry/{app.V1MmmICK.js → app.Z9n2k33I.js} +2 -2
  21. package/build/client/_app/immutable/entry/app.Z9n2k33I.js.br +0 -0
  22. package/build/client/_app/immutable/entry/app.Z9n2k33I.js.gz +0 -0
  23. package/build/client/_app/immutable/entry/start.DGo1uhI2.js +1 -0
  24. package/build/client/_app/immutable/entry/start.DGo1uhI2.js.br +0 -0
  25. package/build/client/_app/immutable/entry/start.DGo1uhI2.js.gz +0 -0
  26. package/build/client/_app/immutable/nodes/0.DiZpkhxP.js +1 -0
  27. package/build/client/_app/immutable/nodes/0.DiZpkhxP.js.br +0 -0
  28. package/build/client/_app/immutable/nodes/0.DiZpkhxP.js.gz +0 -0
  29. package/build/client/_app/immutable/nodes/{1.BqoPRvUU.js → 1.rgoSlmPu.js} +1 -1
  30. package/build/client/_app/immutable/nodes/1.rgoSlmPu.js.br +2 -0
  31. package/build/client/_app/immutable/nodes/1.rgoSlmPu.js.gz +0 -0
  32. package/build/client/_app/immutable/nodes/2.Dzp3a3be.js +3 -0
  33. package/build/client/_app/immutable/nodes/2.Dzp3a3be.js.br +0 -0
  34. package/build/client/_app/immutable/nodes/2.Dzp3a3be.js.gz +0 -0
  35. package/build/client/_app/immutable/nodes/3.mSsa2TDI.js +1 -0
  36. package/build/client/_app/immutable/nodes/3.mSsa2TDI.js.br +0 -0
  37. package/build/client/_app/immutable/nodes/3.mSsa2TDI.js.gz +0 -0
  38. package/build/client/_app/immutable/nodes/4.4oRGPYr4.js +2 -0
  39. package/build/client/_app/immutable/nodes/4.4oRGPYr4.js.br +0 -0
  40. package/build/client/_app/immutable/nodes/4.4oRGPYr4.js.gz +0 -0
  41. package/build/client/_app/version.json +1 -1
  42. package/build/client/_app/version.json.br +0 -0
  43. package/build/client/_app/version.json.gz +0 -0
  44. package/build/handler.js +7 -7
  45. package/build/index.js +7 -7
  46. package/build/server/chunks/chunks/{context.js-BXaBgcZT.js → context.js-Dh0ibRIF.js} +196 -7
  47. package/build/server/chunks/chunks/context.js-Dh0ibRIF.js.map +1 -0
  48. package/build/server/chunks/chunks/{contract.js-IY1F-HE_.js → contract.js-_LTMZBo2.js} +68 -6
  49. package/build/server/chunks/chunks/contract.js-_LTMZBo2.js.map +1 -0
  50. package/build/server/chunks/chunks/{core.js-D1sYC_78.js → core.js-CTyKR4bu.js} +2 -14
  51. package/build/server/chunks/chunks/{core.js-D1sYC_78.js.map → core.js-CTyKR4bu.js.map} +1 -1
  52. package/build/server/chunks/chunks/{exports.js-B5DLeNXr.js → exports.js-gz2aNXzz.js} +2 -2
  53. package/build/server/chunks/chunks/{exports.js-B5DLeNXr.js.map → exports.js-gz2aNXzz.js.map} +1 -1
  54. package/build/server/chunks/chunks/{http.js-CzB9Igb0.js → http.js-BiqUcAuu.js} +3 -2
  55. package/build/server/chunks/chunks/http.js-BiqUcAuu.js.map +1 -0
  56. package/build/server/chunks/chunks/{internal.js-HE35Pm1C.js → internal.js-DoH0cN5N.js} +3 -3
  57. package/build/server/chunks/chunks/{internal.js-HE35Pm1C.js.map → internal.js-DoH0cN5N.js.map} +1 -1
  58. package/build/server/chunks/chunks/internal2.js-BlXt2rfE.js.map +1 -1
  59. package/build/server/chunks/chunks/{manifest-membership.js-Cm_2fU-Z.js → manifest-membership.js-at1ixQdW.js} +10 -3
  60. package/build/server/chunks/chunks/manifest-membership.js-at1ixQdW.js.map +1 -0
  61. package/build/server/chunks/chunks/{raw-reconciliation.js-1_IwPhKb.js → raw-reconciliation.js-CUH8IIJx.js} +3 -3
  62. package/build/server/chunks/chunks/raw-reconciliation.js-CUH8IIJx.js.map +1 -0
  63. package/build/server/chunks/chunks/{raw-reconciliation2.js-CcO9li5n.js → raw-reconciliation2.js-z4xl5zw2.js} +2 -2
  64. package/build/server/chunks/chunks/{raw-reconciliation2.js-CcO9li5n.js.map → raw-reconciliation2.js-z4xl5zw2.js.map} +1 -1
  65. package/build/server/chunks/chunks/{shared.js-MR8sPPTu.js → shared.js-CI-ZkPQn.js} +2 -2
  66. package/build/server/chunks/chunks/{shared.js-MR8sPPTu.js.map → shared.js-CI-ZkPQn.js.map} +1 -1
  67. package/build/server/chunks/chunks/{utils.js-C2UFBITa.js → utils.js-gXYLxRG9.js} +3 -3
  68. package/build/server/chunks/chunks/{utils.js-C2UFBITa.js.map → utils.js-gXYLxRG9.js.map} +1 -1
  69. package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-BnRJ5Qnd.js → _server.ts.js-CdDjuW0r.js} +6 -6
  70. package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-BnRJ5Qnd.js.map → _server.ts.js-CdDjuW0r.js.map} +1 -1
  71. package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-BZPR6n5n.js → _server.ts.js-OWM1eORG.js} +7 -7
  72. package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-BZPR6n5n.js.map → _server.ts.js-OWM1eORG.js.map} +1 -1
  73. package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-DiBS3A0C.js → _server.ts.js-DoG-7ZpZ.js} +7 -7
  74. package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-DiBS3A0C.js.map → _server.ts.js-DoG-7ZpZ.js.map} +1 -1
  75. package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-LOO6qm2-.js → _server.ts.js-CAWjVeGd.js} +7 -7
  76. package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-LOO6qm2-.js.map → _server.ts.js-CAWjVeGd.js.map} +1 -1
  77. package/build/server/chunks/entries/endpoints/api/assets/_id_/retag/_server.ts.js-CqS4aALY.js +38 -0
  78. package/build/server/chunks/entries/endpoints/api/assets/_id_/retag/_server.ts.js-CqS4aALY.js.map +1 -0
  79. package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-CRtJJoSs.js → _server.ts.js-Bo_EsYXJ.js} +7 -7
  80. package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-CRtJJoSs.js.map → _server.ts.js-Bo_EsYXJ.js.map} +1 -1
  81. package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-Ds4zCZ9H.js → _server.ts.js-DW4w0XAr.js} +7 -7
  82. package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-Ds4zCZ9H.js.map → _server.ts.js-DW4w0XAr.js.map} +1 -1
  83. package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-DD4N8pcj.js → _server.ts.js-_MiNPnSy.js} +7 -7
  84. package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-DD4N8pcj.js.map → _server.ts.js-_MiNPnSy.js.map} +1 -1
  85. package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-CZev9aYz.js → _server.ts.js-DwbrQnBc.js} +7 -7
  86. package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-CZev9aYz.js.map → _server.ts.js-DwbrQnBc.js.map} +1 -1
  87. package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-MBDVW0q1.js → _server.ts.js-CEnHQf3b.js} +7 -7
  88. package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-MBDVW0q1.js.map → _server.ts.js-CEnHQf3b.js.map} +1 -1
  89. package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-C7naXnsR.js → _server.ts.js-BZDuJZtF.js} +7 -7
  90. package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-C7naXnsR.js.map → _server.ts.js-BZDuJZtF.js.map} +1 -1
  91. package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-DuGyRdf3.js → _server.ts.js-DRgdpKKd.js} +7 -7
  92. package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-DuGyRdf3.js.map → _server.ts.js-DRgdpKKd.js.map} +1 -1
  93. package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-0-ZCt0qH.js → _server.ts.js-CP5QyIhz.js} +7 -7
  94. package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-0-ZCt0qH.js.map → _server.ts.js-CP5QyIhz.js.map} +1 -1
  95. package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-DFYU_sZL.js → _server.ts.js-BKEvt065.js} +7 -7
  96. package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-DFYU_sZL.js.map → _server.ts.js-BKEvt065.js.map} +1 -1
  97. package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-BqJpK0sJ.js → _server.ts.js-C_zihkDS.js} +7 -7
  98. package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-BqJpK0sJ.js.map → _server.ts.js-C_zihkDS.js.map} +1 -1
  99. package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js--LSqADJp.js → _server.ts.js-watkBmy6.js} +7 -7
  100. package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js--LSqADJp.js.map → _server.ts.js-watkBmy6.js.map} +1 -1
  101. package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-DcL8nYDM.js → _server.ts.js-YtumLIou.js} +7 -7
  102. package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-DcL8nYDM.js.map → _server.ts.js-YtumLIou.js.map} +1 -1
  103. package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-72OcyLfP.js → _server.ts.js-Dkouuu-M.js} +7 -7
  104. package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-72OcyLfP.js.map → _server.ts.js-Dkouuu-M.js.map} +1 -1
  105. package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-D0bOxmG0.js → _server.ts.js-DwsEQeuf.js} +7 -7
  106. package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-D0bOxmG0.js.map → _server.ts.js-DwsEQeuf.js.map} +1 -1
  107. package/build/server/chunks/entries/endpoints/api/usage/{_server.ts.js-BjDVCRNV.js → _server.ts.js-CdQxfhV3.js} +7 -7
  108. package/build/server/chunks/entries/endpoints/api/usage/{_server.ts.js-BjDVCRNV.js.map → _server.ts.js-CdQxfhV3.js.map} +1 -1
  109. package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-CndfoXXd.js → _server.ts.js-DDdMocWj.js} +6 -6
  110. package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-CndfoXXd.js.map → _server.ts.js-DDdMocWj.js.map} +1 -1
  111. package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-Ck1mgbk5.js → _server.ts.js-DbzjbH32.js} +7 -7
  112. package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-Ck1mgbk5.js.map → _server.ts.js-DbzjbH32.js.map} +1 -1
  113. package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-lZ9oP8tz.js → _server.ts.js-K_JbXZsU.js} +7 -7
  114. package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-lZ9oP8tz.js.map → _server.ts.js-K_JbXZsU.js.map} +1 -1
  115. package/build/server/chunks/entries/{hooks.server.js-DQ6l2RSK.js → hooks.server.js-DgZAVFbQ.js} +6 -6
  116. package/build/server/chunks/entries/{hooks.server.js-DQ6l2RSK.js.map → hooks.server.js-DgZAVFbQ.js.map} +1 -1
  117. package/build/server/chunks/{handler-Dfk_4a-r.js → handler-CFkKak8t.js} +5 -5
  118. package/build/server/chunks/{handler-Dfk_4a-r.js.map → handler-CFkKak8t.js.map} +1 -1
  119. package/build/server/chunks/{index.js-O3Wl1zok.js → index.js-B1IroJH5.js} +5 -5
  120. package/build/server/chunks/{index.js-O3Wl1zok.js.map → index.js-B1IroJH5.js.map} +1 -1
  121. package/build/server/chunks/{manifest.js-51nEZnKQ.js → manifest.js-DNDBsrEU.js} +36 -29
  122. package/build/server/chunks/manifest.js-DNDBsrEU.js.map +1 -0
  123. package/build/server/chunks/nodes/{0.js-DjqMfDtB.js → 0.js-CZ3mZUZl.js} +2 -2
  124. package/build/server/chunks/nodes/{0.js-DjqMfDtB.js.map → 0.js-CZ3mZUZl.js.map} +1 -1
  125. package/build/server/chunks/nodes/1.js-BmDzLKxV.js +7 -0
  126. package/build/server/chunks/nodes/{1.js-BwfSLYJH.js.map → 1.js-BmDzLKxV.js.map} +1 -1
  127. package/build/server/chunks/nodes/2.js-Dirm-bY-.js +7 -0
  128. package/build/server/chunks/nodes/2.js-Dirm-bY-.js.map +1 -0
  129. package/build/server/chunks/nodes/3.js-DOqHmSWt.js +7 -0
  130. package/build/server/chunks/nodes/{3.js-D7_hF0qp.js.map → 3.js-DOqHmSWt.js.map} +1 -1
  131. package/build/server/chunks/nodes/4.js-C8pmF8IP.js +7 -0
  132. package/build/server/chunks/nodes/{4.js-D58M2chZ.js.map → 4.js-C8pmF8IP.js.map} +1 -1
  133. package/dist/core/api-types.d.ts +2 -1
  134. package/dist/core/config.d.ts +2 -0
  135. package/dist/core/ingest.d.ts +1 -6
  136. package/dist/core/ingest.js +4 -21
  137. package/dist/core/propose-asset-id.d.ts +10 -0
  138. package/dist/core/propose-asset-id.js +25 -0
  139. package/dist/server/cli-commands.d.ts +2 -2
  140. package/dist/server/cli-commands.js +213 -29
  141. package/dist/server/config-loader.d.ts +1 -0
  142. package/dist/server/config-loader.js +5 -0
  143. package/dist/server/http.js +1 -0
  144. package/dist/server/importers/figma.d.ts +26 -3
  145. package/dist/server/importers/figma.js +114 -1
  146. package/dist/server/library-store.d.ts +9 -0
  147. package/dist/server/library-store.js +79 -0
  148. package/dist/server/operations.d.ts +12 -0
  149. package/dist/server/operations.js +121 -1
  150. package/package.json +1 -1
  151. package/build/client/_app/immutable/assets/AssetSourceMenu.B5fzYfym.css.br +0 -0
  152. package/build/client/_app/immutable/chunks/B06eqbax.js +0 -108
  153. package/build/client/_app/immutable/chunks/B06eqbax.js.br +0 -0
  154. package/build/client/_app/immutable/chunks/B06eqbax.js.gz +0 -0
  155. package/build/client/_app/immutable/chunks/B97y-eFA.js.br +0 -0
  156. package/build/client/_app/immutable/chunks/B97y-eFA.js.gz +0 -0
  157. package/build/client/_app/immutable/chunks/C9MzfQz6.js +0 -2
  158. package/build/client/_app/immutable/chunks/C9MzfQz6.js.br +0 -0
  159. package/build/client/_app/immutable/chunks/C9MzfQz6.js.gz +0 -0
  160. package/build/client/_app/immutable/chunks/CcuyGBqj.js +0 -1
  161. package/build/client/_app/immutable/chunks/CcuyGBqj.js.br +0 -0
  162. package/build/client/_app/immutable/chunks/CcuyGBqj.js.gz +0 -0
  163. package/build/client/_app/immutable/chunks/D6c2mfMB.js.br +0 -2
  164. package/build/client/_app/immutable/chunks/D6c2mfMB.js.gz +0 -0
  165. package/build/client/_app/immutable/entry/app.V1MmmICK.js.br +0 -0
  166. package/build/client/_app/immutable/entry/app.V1MmmICK.js.gz +0 -0
  167. package/build/client/_app/immutable/entry/start.BbyflZUw.js +0 -1
  168. package/build/client/_app/immutable/entry/start.BbyflZUw.js.br +0 -0
  169. package/build/client/_app/immutable/entry/start.BbyflZUw.js.gz +0 -0
  170. package/build/client/_app/immutable/nodes/0.BJOUVkow.js +0 -1
  171. package/build/client/_app/immutable/nodes/0.BJOUVkow.js.br +0 -0
  172. package/build/client/_app/immutable/nodes/0.BJOUVkow.js.gz +0 -0
  173. package/build/client/_app/immutable/nodes/1.BqoPRvUU.js.br +0 -0
  174. package/build/client/_app/immutable/nodes/1.BqoPRvUU.js.gz +0 -0
  175. package/build/client/_app/immutable/nodes/2.BSUVFDfW.js +0 -3
  176. package/build/client/_app/immutable/nodes/2.BSUVFDfW.js.br +0 -0
  177. package/build/client/_app/immutable/nodes/2.BSUVFDfW.js.gz +0 -0
  178. package/build/client/_app/immutable/nodes/3.B6beFiTQ.js +0 -1
  179. package/build/client/_app/immutable/nodes/3.B6beFiTQ.js.br +0 -0
  180. package/build/client/_app/immutable/nodes/3.B6beFiTQ.js.gz +0 -0
  181. package/build/client/_app/immutable/nodes/4.Dt96q8V4.js +0 -2
  182. package/build/client/_app/immutable/nodes/4.Dt96q8V4.js.br +0 -0
  183. package/build/client/_app/immutable/nodes/4.Dt96q8V4.js.gz +0 -0
  184. package/build/server/chunks/chunks/context.js-BXaBgcZT.js.map +0 -1
  185. package/build/server/chunks/chunks/contract.js-IY1F-HE_.js.map +0 -1
  186. package/build/server/chunks/chunks/http.js-CzB9Igb0.js.map +0 -1
  187. package/build/server/chunks/chunks/manifest-membership.js-Cm_2fU-Z.js.map +0 -1
  188. package/build/server/chunks/chunks/raw-reconciliation.js-1_IwPhKb.js.map +0 -1
  189. package/build/server/chunks/manifest.js-51nEZnKQ.js.map +0 -1
  190. package/build/server/chunks/nodes/1.js-BwfSLYJH.js +0 -7
  191. package/build/server/chunks/nodes/2.js-buHbEytf.js +0 -7
  192. package/build/server/chunks/nodes/2.js-buHbEytf.js.map +0 -1
  193. package/build/server/chunks/nodes/3.js-D7_hF0qp.js +0 -7
  194. package/build/server/chunks/nodes/4.js-D58M2chZ.js +0 -7
@@ -14,7 +14,7 @@ import { runBuild } from './build.js';
14
14
  import { formatCheckFinding, runCheck } from './check.js';
15
15
  import { loadConfig } from './config-loader.js';
16
16
  import { FundusOperations } from './operations.js';
17
- import { validateFigmaSourceParameters } from './importers/figma.js';
17
+ import { parseFigmaFileKey, validateFigmaSourceParameters } from './importers/figma.js';
18
18
  import { pluginsByType, serverPluginForType } from './plugins.js';
19
19
  import { CliUsageError, formatBytes, parseArguments, parseJsonFlag, parseManifestList, printJsonDocument, requirePositionals } from './cli-io.js';
20
20
  const COMMON_FLAGS = {
@@ -114,7 +114,9 @@ function printHumanAssetDetail(asset) {
114
114
  console.log(` rawHash: ${asset.rawHash}`);
115
115
  if (asset.importSource?.importer === 'figma') {
116
116
  const source = validateFigmaSourceParameters(asset.importSource.parameters);
117
- console.log(` source: Figma node ${source.nodeId} at ${source.scale}x`);
117
+ console.log(source.mode === 'tag'
118
+ ? ` source: Figma tag "${source.fundusTag}" → node ${source.nodeId} at ${source.scale}x`
119
+ : ` source: Figma node ${source.nodeId} at ${source.scale}x`);
118
120
  }
119
121
  console.log(` manifests: ${asset.manifests.length > 0 ? asset.manifests.join(', ') : '(none)'}`);
120
122
  for (const membership of asset.passiveManifests) {
@@ -374,19 +376,34 @@ Usage: fundus asset ingest <file> [--type <type>] [--id <id>] [--folder <path|ro
374
376
  [--scale <1|2|3|4>] [--folder <path|root>]
375
377
  [--manifests <a,b>] [--parameters <json>]
376
378
  [--compression-x <0-100>] [--compression-y <0-100>] [--json]
379
+ fundus asset ingest --from figma --figma-tag <tag> --type <image|slice>
380
+ [--figma-file <key|link>] [--scale <1|2|3|4>] [--folder <path|root>]
381
+ [--manifests <a,b>] [--parameters <json>]
382
+ [--compression-x <0-100>] [--compression-y <0-100>] [--json]
377
383
 
378
384
  File ingest remains the default and behaves like an editor upload. Figma
379
385
  ingest materializes one selection as PNG through the same importer-neutral
380
386
  operations pipeline. Both run the full autosave chain.
381
387
 
388
+ Figma has two modes. Node mode takes the selection link as the positional and
389
+ requires --id. Tag mode takes --figma-tag <tag> instead of a link: the tag IS
390
+ the asset id, so a positional link and --id are both forbidden with --figma-tag.
391
+
382
392
  Flags:
383
393
  --from <file|figma> acquisition source; default: file. URLs are never
384
394
  detected heuristically
385
395
  --type <type> required for Figma (image or slice) and when a file
386
396
  extension is ambiguous; known types:
387
397
  ${Object.keys(pluginsByType).sort().join(', ')}
388
- --id <id> required for Figma; for files defaults to a proposal
389
- from the file name
398
+ --id <id> required for Figma node mode; for files defaults to a
399
+ proposal from the file name; forbidden with --figma-tag
400
+ --figma-tag <tag> Figma tag mode: resolve the node tagged with this Fundus
401
+ asset id; the tag becomes the asset id (mutually
402
+ exclusive with a node selection link and --id)
403
+ --figma-file <key|link>
404
+ fileKey for tag mode as a raw key or a Figma link;
405
+ defaults to import.figma.defaultFileKey from
406
+ fundus.config.ts
390
407
  --folder <path|root> destination folder; default: root
391
408
  --manifests <a,b> explicit manifest membership (every name must exist)
392
409
  --parameters <json> shallow parameter patch merged after ingest seeding
@@ -408,20 +425,37 @@ Example:
408
425
  --compression-x 75 --compression-y 25 --json
409
426
  fundus asset ingest 'https://www.figma.com/design/abc/UI?node-id=12-34' \\
410
427
  --from figma --type slice --id navigationPanel --scale 3 --json
428
+ fundus asset ingest --from figma --figma-tag navigationPanel --type slice \\
429
+ --figma-file abc --scale 3 --json
411
430
  `;
412
431
  export const ASSET_REIMPORT_HELP = `fundus asset reimport — refresh an asset from its saved source
413
432
 
414
433
  Usage: fundus asset reimport <id> [--json]
415
434
  fundus asset reimport <id> --figma-link <selection-link> [--scale <1|2|3|4>] [--json]
435
+ fundus asset reimport <id> --figma-tag <newTag> [--figma-file <key|link>] [--scale <1|2|3|4>] [--json]
436
+ fundus asset reimport <id> --figma-file <key|link> [--scale <1|2|3|4>] [--json]
416
437
  fundus asset reimport <id> --scale <1|2|3|4> [--json]
417
438
 
418
- Without overrides, re-materializes the persisted importer source. Figma
419
- overrides may update the link, scale, or both; omitted values are preserved.
420
- Asset type, parameters, folder, manifests, and id remain unchanged. Raw-file
421
- drift or concurrent changes abort without overwriting local edits.
439
+ Without overrides, re-materializes the persisted importer source (a Figma tag
440
+ asset re-scans its file for the tagged node, self-healing after node moves).
441
+ A Figma override may update the source or scale; omitted values are preserved.
442
+ Asset type, parameters, folder, and manifests remain unchanged. Raw-file drift
443
+ or concurrent changes abort without overwriting local edits.
444
+
445
+ Node vs tag assets accept different overrides. A node asset takes --figma-link
446
+ (and --scale); --figma-tag and --figma-file are rejected. A tag asset takes
447
+ --figma-tag to change its tag, --figma-file to re-point in place to another
448
+ document, and --scale; --figma-link is rejected. Because the tag IS the asset
449
+ id, changing it with --figma-tag renames the asset to the new tag (the new id
450
+ must be valid and unused).
422
451
 
423
452
  Flags:
424
- --figma-link <link> replace the persisted Figma file/node selection
453
+ --figma-link <link> node asset only: replace the persisted file/node selection
454
+ --figma-tag <newTag> tag asset only: change the tag, renaming the asset to the
455
+ new tag (the tag is the asset id)
456
+ --figma-file <key|link>
457
+ tag asset only: re-point in place to another file (raw
458
+ key or a Figma link)
425
459
  --scale <1|2|3|4> replace the persisted Figma export scale
426
460
  --json print {"ok": true, "asset": <AssetStateDto>, "issues": […]}
427
461
  --help this help
@@ -429,6 +463,8 @@ Flags:
429
463
  Example:
430
464
  fundus asset reimport navigationPanel
431
465
  fundus asset reimport navigationPanel --figma-link '<new-selection-link>'
466
+ fundus asset reimport navigationPanel --figma-tag mainNavigationPanel
467
+ fundus asset reimport navigationPanel --figma-file def456
432
468
  fundus asset reimport navigationPanel --scale 4
433
469
  fundus asset reimport navigationPanel --figma-link '<new-selection-link>' --scale 4 --json
434
470
  `;
@@ -436,16 +472,26 @@ export const ASSET_REPLACE_HELP = `fundus asset replace — replace an asset's r
436
472
 
437
473
  Usage: fundus asset replace <id> <file|figma-selection-link>
438
474
  [--from <file|figma>] [--scale <1|2|3|4>] [--json]
475
+ fundus asset replace <id> --from figma --figma-tag <tag>
476
+ [--figma-file <key|link>] [--scale <1|2|3|4>] [--json]
439
477
 
440
478
  Replaces the stored raw file and content hash while preserving the asset's
441
479
  id, type, folder, parameters, references, and manifest memberships. The new
442
480
  source must be accepted by the existing asset type. File replacement remains
443
481
  the default and disconnects any saved source. Figma replacement stores the new
444
- selection as a repeatable source. Runs the full autosave chain, so delivered
445
- assets are reprocessed and manifests regenerated.
482
+ selection (node mode) or tag (tag mode) as a repeatable source. A tag
483
+ replacement must resolve to the asset's own id, so it can only re-point the
484
+ same asset's own tag. Runs the full autosave chain, so delivered assets are
485
+ reprocessed and manifests regenerated.
446
486
 
447
487
  Flags:
448
488
  --from <file|figma> replacement source; default: file
489
+ --figma-tag <tag> Figma tag mode: replace from the node tagged with this
490
+ Fundus asset id (takes no positional link)
491
+ --figma-file <key|link>
492
+ fileKey for tag mode as a raw key or a Figma link;
493
+ defaults to import.figma.defaultFileKey from
494
+ fundus.config.ts
449
495
  --scale <1|2|3|4> Figma export scale; default:
450
496
  import.figma.defaults.scale from fundus.config.ts
451
497
  --json print {"ok": true, "asset": <AssetStateDto>, "issues": […]}
@@ -455,6 +501,8 @@ Example:
455
501
  fundus asset replace navigationPanel ./exports/navigation_panel_v2.png --json
456
502
  fundus asset replace navigationPanel \\
457
503
  'https://www.figma.com/design/abc/UI?node-id=12-34' --from figma --scale 3 --json
504
+ fundus asset replace navigationPanel --from figma \\
505
+ --figma-tag navigationPanel --figma-file abc --scale 3 --json
458
506
  `;
459
507
  export const ASSET_SET_HELP = `fundus asset set — update an asset's parameters and/or memberships
460
508
 
@@ -701,9 +749,10 @@ async function runAssetIngest(argumentList) {
701
749
  'compression-y': { takesValue: true },
702
750
  folder: { takesValue: true },
703
751
  from: { takesValue: true },
704
- scale: { takesValue: true }
752
+ scale: { takesValue: true },
753
+ 'figma-tag': { takesValue: true },
754
+ 'figma-file': { takesValue: true }
705
755
  }, 'fundus asset ingest');
706
- const [source] = requirePositionals(parsed, ['<file|figma-selection-link>'], 'fundus asset ingest');
707
756
  const from = parsed.flags.from === undefined ? 'file' : parsed.flags.from;
708
757
  if (from !== 'file' && from !== 'figma') {
709
758
  throw new CliUsageError('Flag "--from" must be "file" or "figma" — run `fundus asset ingest --help`.');
@@ -714,6 +763,11 @@ async function runAssetIngest(argumentList) {
714
763
  if (from === 'file' && scale !== undefined) {
715
764
  throw new CliUsageError('Flag "--scale" is only valid with `--from figma` — run `fundus asset ingest --help`.');
716
765
  }
766
+ const figmaTag = typeof parsed.flags['figma-tag'] === 'string' ? parsed.flags['figma-tag'] : undefined;
767
+ const figmaFile = typeof parsed.flags['figma-file'] === 'string' ? parsed.flags['figma-file'] : undefined;
768
+ if (from === 'file' && (figmaTag !== undefined || figmaFile !== undefined)) {
769
+ throw new CliUsageError('Flags "--figma-tag" and "--figma-file" are only valid with `--from figma` — run `fundus asset ingest --help`.');
770
+ }
717
771
  let parametersPatch = typeof parsed.flags.parameters === 'string'
718
772
  ? parseJsonFlag('parameters', parsed.flags.parameters)
719
773
  : undefined;
@@ -723,15 +777,56 @@ async function runAssetIngest(argumentList) {
723
777
  : undefined;
724
778
  const folder = typeof parsed.flags.folder === 'string' ? parseFolderToken(parsed.flags.folder) : '';
725
779
  if (from === 'figma') {
780
+ const type = parsed.flags.type;
781
+ if (typeof type !== 'string' || (type !== 'image' && type !== 'slice')) {
782
+ throw new CliUsageError('Figma ingest requires --type <image|slice> — run `fundus asset ingest --help`.');
783
+ }
784
+ if (figmaTag !== undefined) {
785
+ if (parsed.positionals.length > 0) {
786
+ throw new CliUsageError('Figma tag ingest takes no positional link — `--figma-tag` and a node selection link are mutually exclusive. Run `fundus asset ingest --help`.');
787
+ }
788
+ if (parsed.flags.id !== undefined) {
789
+ throw new CliUsageError('Figma tag ingest forbids --id: the tag is the asset id. Run `fundus asset ingest --help`.');
790
+ }
791
+ const fileKey = figmaFile === undefined ? undefined : resolveFigmaFileKeyFlag(figmaFile);
792
+ const parametersPatchForTag = applySliceCompressionFlags({
793
+ type,
794
+ flags: compressionFlags,
795
+ parametersPatch,
796
+ commandPath: 'fundus asset ingest'
797
+ });
798
+ const operations = await createOperations();
799
+ const state = await operations.importAsset({
800
+ importer: 'figma',
801
+ input: {
802
+ mode: 'tag',
803
+ tag: figmaTag,
804
+ format: 'png',
805
+ ...(fileKey !== undefined ? { fileKey } : {}),
806
+ ...(scale !== undefined ? { scale } : {})
807
+ },
808
+ type,
809
+ id: figmaTag,
810
+ folder,
811
+ parametersPatch: parametersPatchForTag,
812
+ manifests
813
+ });
814
+ const asset = assetById(state, figmaTag);
815
+ if (parsed.flags.json) {
816
+ printJsonDocument({ ok: true, asset, issues: state.issues });
817
+ return;
818
+ }
819
+ printMutationResult(`Imported asset "${figmaTag}" from Figma tag as ${type}.`, asset, state);
820
+ return;
821
+ }
822
+ const [source] = requirePositionals(parsed, ['<figma-selection-link>'], 'fundus asset ingest');
726
823
  if (typeof parsed.flags.id !== 'string') {
727
824
  throw new CliUsageError('Figma ingest requires --id <id> — run `fundus asset ingest --help`.');
728
825
  }
729
- if (typeof parsed.flags.type !== 'string' ||
730
- (parsed.flags.type !== 'image' && parsed.flags.type !== 'slice')) {
731
- throw new CliUsageError('Figma ingest requires --type <image|slice> — run `fundus asset ingest --help`.');
826
+ if (figmaFile !== undefined) {
827
+ throw new CliUsageError('Flag "--figma-file" is only valid in tag mode (with `--figma-tag`) — run `fundus asset ingest --help`.');
732
828
  }
733
829
  const id = parsed.flags.id;
734
- const type = parsed.flags.type;
735
830
  parametersPatch = applySliceCompressionFlags({
736
831
  type,
737
832
  flags: compressionFlags,
@@ -756,6 +851,7 @@ async function runAssetIngest(argumentList) {
756
851
  printMutationResult(`Imported asset "${id}" from Figma as ${type}.`, asset, state);
757
852
  return;
758
853
  }
854
+ const [source] = requirePositionals(parsed, ['<file>'], 'fundus asset ingest');
759
855
  const filePath = source;
760
856
  const absolutePath = resolve(process.cwd(), filePath);
761
857
  let bytes;
@@ -797,6 +893,20 @@ function parseFigmaScale(value, commandPath) {
797
893
  }
798
894
  throw new CliUsageError(`Flag "--scale" must be 1, 2, 3, or 4 — run \`${commandPath} --help\`.`);
799
895
  }
896
+ /**
897
+ * `--figma-file` accepts either a raw Figma file key or a full design/file link.
898
+ * A value that parses as a URL is treated as a link (fileKey extracted via
899
+ * `parseFigmaFileKey`); anything else is passed through as a raw key.
900
+ */
901
+ function resolveFigmaFileKeyFlag(value) {
902
+ try {
903
+ new URL(value);
904
+ }
905
+ catch {
906
+ return value;
907
+ }
908
+ return parseFigmaFileKey(value);
909
+ }
800
910
  function canonicalFigmaSelectionLink(source) {
801
911
  const url = new URL(`https://www.figma.com/design/${encodeURIComponent(source.fileKey)}/fundus`);
802
912
  url.searchParams.set('node-id', source.nodeId.replace(':', '-'));
@@ -806,6 +916,8 @@ async function runAssetReimport(argumentList) {
806
916
  const parsed = parseArguments(argumentList, {
807
917
  ...COMMON_FLAGS,
808
918
  'figma-link': { takesValue: true },
919
+ 'figma-tag': { takesValue: true },
920
+ 'figma-file': { takesValue: true },
809
921
  scale: { takesValue: true }
810
922
  }, 'fundus asset reimport');
811
923
  const [id] = requirePositionals(parsed, ['<id>'], 'fundus asset reimport');
@@ -813,9 +925,20 @@ async function runAssetReimport(argumentList) {
813
925
  ? parseFigmaScale(parsed.flags.scale, 'fundus asset reimport')
814
926
  : undefined;
815
927
  const figmaLink = typeof parsed.flags['figma-link'] === 'string' ? parsed.flags['figma-link'] : undefined;
928
+ const figmaTag = typeof parsed.flags['figma-tag'] === 'string' ? parsed.flags['figma-tag'] : undefined;
929
+ const figmaFile = typeof parsed.flags['figma-file'] === 'string' ? parsed.flags['figma-file'] : undefined;
930
+ if (figmaTag !== undefined && figmaTag === '') {
931
+ throw new CliUsageError('Flag "--figma-tag" needs a non-empty tag — run `fundus asset reimport --help`.');
932
+ }
933
+ const hasOverride = figmaLink !== undefined ||
934
+ figmaTag !== undefined ||
935
+ figmaFile !== undefined ||
936
+ scale !== undefined;
816
937
  const operations = await createOperations();
817
938
  let state;
818
- if (figmaLink === undefined && scale === undefined) {
939
+ // A tag rename changes the id; look the asset up under its effective new id.
940
+ let effectiveId = id;
941
+ if (!hasOverride) {
819
942
  state = await operations.reimportAsset(id);
820
943
  }
821
944
  else {
@@ -825,28 +948,58 @@ async function runAssetReimport(argumentList) {
825
948
  throw new Error(`Asset "${id}" does not have a Figma source.`);
826
949
  }
827
950
  const source = validateFigmaSourceParameters(currentAsset.importSource.parameters);
828
- state = await operations.reimportAsset(id, {
829
- url: figmaLink ?? canonicalFigmaSelectionLink(source),
830
- format: 'png',
831
- scale: scale ?? source.scale
832
- });
951
+ if (source.mode === 'tag') {
952
+ if (figmaLink !== undefined) {
953
+ throw new CliUsageError('Flag "--figma-link" is invalid for a Figma tag asset — change the tag with --figma-tag or re-point with --figma-file and/or --scale. Run `fundus asset reimport --help`.');
954
+ }
955
+ const fileKey = figmaFile === undefined ? undefined : resolveFigmaFileKeyFlag(figmaFile);
956
+ state = await operations.retagFigmaAsset(id, {
957
+ ...(figmaTag !== undefined ? { tag: figmaTag } : {}),
958
+ ...(fileKey !== undefined ? { fileKey } : {}),
959
+ ...(scale !== undefined ? { scale } : {})
960
+ });
961
+ // A different tag renames the asset (the tag is the id); else the id is unchanged.
962
+ effectiveId = figmaTag ?? id;
963
+ }
964
+ else {
965
+ if (figmaFile !== undefined) {
966
+ throw new CliUsageError('Flag "--figma-file" is invalid for a Figma node asset — it applies only to tag assets. Run `fundus asset reimport --help`.');
967
+ }
968
+ if (figmaTag !== undefined) {
969
+ throw new CliUsageError('Flag "--figma-tag" is invalid for a Figma node asset — it applies only to tag assets. Run `fundus asset reimport --help`.');
970
+ }
971
+ state = await operations.reimportAsset(id, {
972
+ url: figmaLink ?? canonicalFigmaSelectionLink(source),
973
+ format: 'png',
974
+ scale: scale ?? source.scale
975
+ });
976
+ }
833
977
  }
834
- const asset = assetById(state, id);
978
+ const asset = assetById(state, effectiveId);
835
979
  if (parsed.flags.json) {
836
980
  printJsonDocument({ ok: true, asset, issues: state.issues });
837
981
  return;
838
982
  }
839
- printMutationResult(figmaLink === undefined && scale === undefined
840
- ? `Refreshed asset "${id}" from its saved source.`
841
- : `Reimported asset "${id}" with updated Figma source.`, asset, state);
983
+ let headline;
984
+ if (!hasOverride) {
985
+ headline = `Refreshed asset "${id}" from its saved source.`;
986
+ }
987
+ else if (effectiveId !== id) {
988
+ headline = `Reimported asset "${id}" as "${effectiveId}" with updated Figma tag.`;
989
+ }
990
+ else {
991
+ headline = `Reimported asset "${effectiveId}" with updated Figma source.`;
992
+ }
993
+ printMutationResult(headline, asset, state);
842
994
  }
843
995
  async function runAssetReplace(argumentList) {
844
996
  const parsed = parseArguments(argumentList, {
845
997
  ...COMMON_FLAGS,
846
998
  from: { takesValue: true },
847
- scale: { takesValue: true }
999
+ scale: { takesValue: true },
1000
+ 'figma-tag': { takesValue: true },
1001
+ 'figma-file': { takesValue: true }
848
1002
  }, 'fundus asset replace');
849
- const [id, source] = requirePositionals(parsed, ['<id>', '<file|figma-selection-link>'], 'fundus asset replace');
850
1003
  const from = parsed.flags.from === undefined ? 'file' : parsed.flags.from;
851
1004
  if (from !== 'file' && from !== 'figma') {
852
1005
  throw new CliUsageError('Flag "--from" must be "file" or "figma" — run `fundus asset replace --help`.');
@@ -857,8 +1010,39 @@ async function runAssetReplace(argumentList) {
857
1010
  if (from === 'file' && scale !== undefined) {
858
1011
  throw new CliUsageError('Flag "--scale" is only valid with `--from figma` — run `fundus asset replace --help`.');
859
1012
  }
1013
+ const figmaTag = typeof parsed.flags['figma-tag'] === 'string' ? parsed.flags['figma-tag'] : undefined;
1014
+ const figmaFile = typeof parsed.flags['figma-file'] === 'string' ? parsed.flags['figma-file'] : undefined;
1015
+ if (from === 'file' && (figmaTag !== undefined || figmaFile !== undefined)) {
1016
+ throw new CliUsageError('Flags "--figma-tag" and "--figma-file" are only valid with `--from figma` — run `fundus asset replace --help`.');
1017
+ }
1018
+ if (from === 'figma' && figmaTag !== undefined) {
1019
+ const [id] = requirePositionals(parsed, ['<id>'], 'fundus asset replace');
1020
+ const fileKey = figmaFile === undefined ? undefined : resolveFigmaFileKeyFlag(figmaFile);
1021
+ const operations = await createOperations();
1022
+ const state = await operations.replaceRawFromSource(id, {
1023
+ importer: 'figma',
1024
+ input: {
1025
+ mode: 'tag',
1026
+ tag: figmaTag,
1027
+ format: 'png',
1028
+ ...(fileKey !== undefined ? { fileKey } : {}),
1029
+ ...(scale !== undefined ? { scale } : {})
1030
+ }
1031
+ });
1032
+ const asset = assetById(state, id);
1033
+ if (parsed.flags.json) {
1034
+ printJsonDocument({ ok: true, asset, issues: state.issues });
1035
+ return;
1036
+ }
1037
+ printMutationResult(`Replaced raw file for asset "${id}" from Figma tag.`, asset, state);
1038
+ return;
1039
+ }
1040
+ const [id, source] = requirePositionals(parsed, ['<id>', from === 'figma' ? '<figma-selection-link>' : '<file>'], 'fundus asset replace');
860
1041
  const operations = await createOperations();
861
1042
  if (from === 'figma') {
1043
+ if (figmaFile !== undefined) {
1044
+ throw new CliUsageError('Flag "--figma-file" is only valid in tag mode (with `--figma-tag`) — run `fundus asset replace --help`.');
1045
+ }
862
1046
  const state = await operations.replaceRawFromSource(id, {
863
1047
  importer: 'figma',
864
1048
  input: { url: source, format: 'png', ...(scale !== undefined ? { scale } : {}) }
@@ -54,6 +54,7 @@ export interface ResolvedFundusConfig {
54
54
  import: {
55
55
  figma: {
56
56
  token?: string;
57
+ defaultFileKey?: string;
57
58
  defaults: {
58
59
  scale: FigmaImportScale;
59
60
  };
@@ -149,6 +149,10 @@ export function resolveConfig(projectRoot, value, registry = serverPluginsByType
149
149
  if (figma.token !== undefined && (typeof figma.token !== 'string' || figma.token.length === 0)) {
150
150
  throw new Error('fundus.config: "import.figma.token" must be a non-empty string.');
151
151
  }
152
+ if (figma.defaultFileKey !== undefined &&
153
+ (typeof figma.defaultFileKey !== 'string' || figma.defaultFileKey.length === 0)) {
154
+ throw new Error('fundus.config: "import.figma.defaultFileKey" must be a non-empty string.');
155
+ }
152
156
  const defaults = figma.defaults === undefined ? {} : figma.defaults;
153
157
  if (!isRecord(defaults)) {
154
158
  throw new Error('fundus.config: "import.figma.defaults" must be an object.');
@@ -178,6 +182,7 @@ export function resolveConfig(projectRoot, value, registry = serverPluginsByType
178
182
  import: {
179
183
  figma: {
180
184
  ...(figma.token !== undefined ? { token: figma.token } : {}),
185
+ ...(figma.defaultFileKey !== undefined ? { defaultFileKey: figma.defaultFileKey } : {}),
181
186
  defaults: { scale }
182
187
  }
183
188
  },
@@ -44,6 +44,7 @@ const SOURCE_ERROR_STATUS = {
44
44
  unauthorized: 401,
45
45
  forbidden: 403,
46
46
  'not-found': 404,
47
+ 'ambiguous-tag': 409,
47
48
  'rate-limited': 429,
48
49
  'upstream-failed': 502,
49
50
  'invalid-response': 502,
@@ -1,21 +1,44 @@
1
- import { type ServerSourceImporter } from './contract.js';
1
+ import { type ImporterContext, type ServerSourceImporter } from './contract.js';
2
2
  import { type PinnedHttpsTarget } from './pinned-https.js';
3
- export interface FigmaSourceParameters {
3
+ export interface FigmaNodeSourceParameters {
4
+ mode?: 'node';
4
5
  fileKey: string;
5
6
  nodeId: string;
6
7
  format: 'png';
7
8
  scale: 1 | 2 | 3 | 4;
8
9
  }
9
- export interface FigmaImportInput {
10
+ export interface FigmaTagSourceParameters {
11
+ mode: 'tag';
12
+ fileKey: string;
13
+ /** The Fundus asset id carried in Figma as `sharedPluginData.fundus.assetId`. */
14
+ fundusTag: string;
15
+ /** Resolved node id (colon-form), cached from the last tag resolution. */
16
+ nodeId: string;
17
+ format: 'png';
18
+ scale: 1 | 2 | 3 | 4;
19
+ }
20
+ export type FigmaSourceParameters = FigmaNodeSourceParameters | FigmaTagSourceParameters;
21
+ export interface FigmaNodeImportInput {
22
+ mode?: 'node';
10
23
  url: string;
11
24
  format?: 'png';
12
25
  scale?: 1 | 2 | 3 | 4;
13
26
  }
27
+ export interface FigmaTagImportInput {
28
+ mode: 'tag';
29
+ tag: string;
30
+ fileKey?: string;
31
+ format?: 'png';
32
+ scale?: 1 | 2 | 3 | 4;
33
+ }
34
+ export type FigmaImportInput = FigmaNodeImportInput | FigmaTagImportInput;
14
35
  export declare function parseFigmaSelectionUrl(value: string): {
15
36
  fileKey: string;
16
37
  nodeId: string;
17
38
  };
39
+ export declare function parseFigmaFileKey(value: string): string;
18
40
  export declare function validateFigmaSourceParameters(value: unknown): FigmaSourceParameters;
19
41
  export declare function resolveSafeDownloadTarget(value: string | URL): Promise<PinnedHttpsTarget>;
20
42
  export declare function assertSafeDownloadUrl(value: string | URL): Promise<URL>;
43
+ export declare function resolveTagToNode(fileKey: string, tag: string, context: ImporterContext): Promise<string>;
21
44
  export declare const figmaSourceImporter: ServerSourceImporter<FigmaImportInput, FigmaSourceParameters>;
@@ -1,6 +1,7 @@
1
1
  import { lookup } from 'node:dns/promises';
2
2
  import { isIP } from 'node:net';
3
3
  import sharp from 'sharp';
4
+ import { isValidAssetId } from '../../core/index.js';
4
5
  import { SourceImportError, invalidInput } from './contract.js';
5
6
  import { requestPinnedHttps } from './pinned-https.js';
6
7
  const MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024;
@@ -36,6 +37,24 @@ export function parseFigmaSelectionUrl(value) {
36
37
  const [left, right] = nodeIds[0].split(/[-:]/);
37
38
  return { fileKey: segments[1], nodeId: `${left}:${right}` };
38
39
  }
40
+ export function parseFigmaFileKey(value) {
41
+ let url;
42
+ try {
43
+ url = new URL(value);
44
+ }
45
+ catch {
46
+ return invalidInput('Enter a valid Figma file link.');
47
+ }
48
+ if (url.protocol !== 'https:' ||
49
+ (url.hostname !== 'figma.com' && url.hostname !== 'www.figma.com')) {
50
+ return invalidInput('Figma links must use HTTPS on figma.com.');
51
+ }
52
+ const segments = url.pathname.split('/').filter(Boolean);
53
+ if (!['design', 'file', 'proto'].includes(segments[0] ?? '') || !segments[1]) {
54
+ return invalidInput('The Figma link must point to a design, file, or prototype.');
55
+ }
56
+ return segments[1];
57
+ }
39
58
  function validateScale(value) {
40
59
  if (value !== 1 && value !== 2 && value !== 3 && value !== 4) {
41
60
  return invalidInput('Figma export scale must be 1, 2, 3, or 4.');
@@ -44,12 +63,31 @@ function validateScale(value) {
44
63
  }
45
64
  function validateInput(value) {
46
65
  const input = record(value);
47
- if (!input || typeof input.url !== 'string')
66
+ if (!input)
48
67
  return invalidInput('Figma import requires a URL.');
49
68
  if (input.format !== undefined && input.format !== 'png') {
50
69
  return invalidInput('Figma import currently supports PNG only.');
51
70
  }
71
+ if (input.mode === 'tag') {
72
+ if (typeof input.tag !== 'string' || !isValidAssetId(input.tag)) {
73
+ return invalidInput('Figma tag import requires a valid Fundus asset id as the tag.');
74
+ }
75
+ if (input.fileKey !== undefined &&
76
+ (typeof input.fileKey !== 'string' || input.fileKey.length === 0)) {
77
+ return invalidInput('Figma tag import fileKey must be a non-empty string.');
78
+ }
79
+ return {
80
+ mode: 'tag',
81
+ tag: input.tag,
82
+ format: 'png',
83
+ ...(input.fileKey !== undefined ? { fileKey: input.fileKey } : {}),
84
+ ...(input.scale !== undefined ? { scale: validateScale(input.scale) } : {})
85
+ };
86
+ }
87
+ if (typeof input.url !== 'string')
88
+ return invalidInput('Figma import requires a URL.');
52
89
  return {
90
+ mode: 'node',
53
91
  url: input.url,
54
92
  format: 'png',
55
93
  ...(input.scale !== undefined ? { scale: validateScale(input.scale) } : {})
@@ -65,6 +103,19 @@ export function validateFigmaSourceParameters(value) {
65
103
  source.format !== 'png') {
66
104
  return invalidInput('Stored Figma source parameters are invalid.');
67
105
  }
106
+ if (source.mode === 'tag') {
107
+ if (typeof source.fundusTag !== 'string' || !isValidAssetId(source.fundusTag)) {
108
+ return invalidInput('Stored Figma source parameters are invalid.');
109
+ }
110
+ return {
111
+ mode: 'tag',
112
+ fileKey: source.fileKey,
113
+ fundusTag: source.fundusTag,
114
+ nodeId: source.nodeId,
115
+ format: 'png',
116
+ scale: validateScale(source.scale)
117
+ };
118
+ }
68
119
  return {
69
120
  fileKey: source.fileKey,
70
121
  nodeId: source.nodeId,
@@ -332,6 +383,49 @@ async function validatePng(bytes) {
332
383
  throw new SourceImportError('image-too-large', 'Figma export exceeds the 32 megapixel limit.');
333
384
  }
334
385
  }
386
+ function collectTaggedNodeIds(node, tag, matches) {
387
+ const current = record(node);
388
+ if (!current)
389
+ return;
390
+ const shared = record(current.sharedPluginData);
391
+ const fundus = shared ? record(shared.fundus) : null;
392
+ if (fundus && fundus.assetId === tag && typeof current.id === 'string') {
393
+ matches.push(current.id);
394
+ }
395
+ if (Array.isArray(current.children)) {
396
+ for (const child of current.children)
397
+ collectTaggedNodeIds(child, tag, matches);
398
+ }
399
+ }
400
+ export async function resolveTagToNode(fileKey, tag, context) {
401
+ const token = context.config.import.figma.token;
402
+ if (!token) {
403
+ throw new SourceImportError('not-configured', 'Figma access is not configured.');
404
+ }
405
+ const endpoint = new URL(`https://api.figma.com/v1/files/${encodeURIComponent(fileKey)}`);
406
+ endpoint.searchParams.set('plugin_data', 'shared');
407
+ const jsonBytes = await fetchWithDeadline(context.fetch, endpoint, { redirect: 'error', headers: { 'X-Figma-Token': token } }, async (response) => new Uint8Array(await response.arrayBuffer()));
408
+ let payload;
409
+ try {
410
+ payload = JSON.parse(new TextDecoder().decode(jsonBytes));
411
+ }
412
+ catch {
413
+ throw new SourceImportError('invalid-response', 'Figma returned an invalid response.');
414
+ }
415
+ const body = record(payload);
416
+ if (!body || body.err || (typeof body.status === 'number' && body.status >= 400)) {
417
+ throw new SourceImportError('invalid-response', 'Figma returned an invalid response.');
418
+ }
419
+ const matches = [];
420
+ collectTaggedNodeIds(body.document, tag, matches);
421
+ if (matches.length === 0) {
422
+ throw new SourceImportError('not-found', `No Figma node is tagged "${tag}" in this file.`);
423
+ }
424
+ if (matches.length > 1) {
425
+ throw new SourceImportError('ambiguous-tag', `More than one Figma node is tagged "${tag}" in this file.`);
426
+ }
427
+ return matches[0];
428
+ }
335
429
  async function materialize(source, context) {
336
430
  const token = context.config.import.figma.token;
337
431
  if (!token) {
@@ -371,6 +465,21 @@ export const figmaSourceImporter = {
371
465
  label: 'Figma',
372
466
  validateInput,
373
467
  async importRaw(input, context) {
468
+ if (input.mode === 'tag') {
469
+ const fileKey = input.fileKey ?? context.config.import.figma.defaultFileKey;
470
+ if (!fileKey) {
471
+ throw new SourceImportError('not-configured', 'Figma tag import needs a fileKey or import.figma.defaultFileKey in fundus.config.');
472
+ }
473
+ const nodeId = await resolveTagToNode(fileKey, input.tag, context);
474
+ return materialize({
475
+ mode: 'tag',
476
+ fileKey,
477
+ fundusTag: input.tag,
478
+ nodeId,
479
+ format: 'png',
480
+ scale: input.scale ?? context.config.import.figma.defaults.scale
481
+ }, context);
482
+ }
374
483
  const parsed = parseFigmaSelectionUrl(input.url);
375
484
  return materialize({
376
485
  fileKey: parsed.fileKey,
@@ -381,6 +490,10 @@ export const figmaSourceImporter = {
381
490
  },
382
491
  validateSourceParameters: validateFigmaSourceParameters,
383
492
  async reimportRaw(source, context) {
493
+ if (source.mode === 'tag') {
494
+ const nodeId = await resolveTagToNode(source.fileKey, source.fundusTag, context);
495
+ return materialize({ ...source, nodeId }, context);
496
+ }
384
497
  return materialize(source, context);
385
498
  }
386
499
  };