fundus 0.25.0 → 0.26.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 (148) hide show
  1. package/README.md +21 -0
  2. package/build/client/_app/immutable/chunks/B_4qwt4o.js +1 -0
  3. package/build/client/_app/immutable/chunks/B_4qwt4o.js.br +0 -0
  4. package/build/client/_app/immutable/chunks/B_4qwt4o.js.gz +0 -0
  5. package/build/client/_app/immutable/chunks/{C4w39LtR.js → DS1jTxvm.js} +1 -1
  6. package/build/client/_app/immutable/chunks/DS1jTxvm.js.br +2 -0
  7. package/build/client/_app/immutable/chunks/DS1jTxvm.js.gz +0 -0
  8. package/build/client/_app/immutable/chunks/{WV4B0d_0.js → p2q3f3Ml.js} +1 -1
  9. package/build/client/_app/immutable/chunks/p2q3f3Ml.js.br +0 -0
  10. package/build/client/_app/immutable/chunks/p2q3f3Ml.js.gz +0 -0
  11. package/build/client/_app/immutable/entry/{app.GgnXYpzJ.js → app.DgW6Eg19.js} +2 -2
  12. package/build/client/_app/immutable/entry/app.DgW6Eg19.js.br +0 -0
  13. package/build/client/_app/immutable/entry/app.DgW6Eg19.js.gz +0 -0
  14. package/build/client/_app/immutable/entry/start.D3J5OM6w.js +1 -0
  15. package/build/client/_app/immutable/entry/start.D3J5OM6w.js.br +1 -0
  16. package/build/client/_app/immutable/entry/start.D3J5OM6w.js.gz +0 -0
  17. package/build/client/_app/immutable/nodes/{0.D1g1plhI.js → 0.DdcKoZu5.js} +1 -1
  18. package/build/client/_app/immutable/nodes/0.DdcKoZu5.js.br +0 -0
  19. package/build/client/_app/immutable/nodes/0.DdcKoZu5.js.gz +0 -0
  20. package/build/client/_app/immutable/nodes/{1.DzAnL2O6.js → 1.CoM7ng4c.js} +1 -1
  21. package/build/client/_app/immutable/nodes/1.CoM7ng4c.js.br +0 -0
  22. package/build/client/_app/immutable/nodes/1.CoM7ng4c.js.gz +0 -0
  23. package/build/client/_app/immutable/nodes/{2.BMN1YTJ4.js → 2.CV-vWxFI.js} +1 -1
  24. package/build/client/_app/immutable/nodes/2.CV-vWxFI.js.br +0 -0
  25. package/build/client/_app/immutable/nodes/2.CV-vWxFI.js.gz +0 -0
  26. package/build/client/_app/immutable/nodes/{3.DL_TiE5x.js → 3.B9mDsYGS.js} +1 -1
  27. package/build/client/_app/immutable/nodes/3.B9mDsYGS.js.br +0 -0
  28. package/build/client/_app/immutable/nodes/3.B9mDsYGS.js.gz +0 -0
  29. package/build/client/_app/immutable/nodes/{4.D7FHmbgq.js → 4.CFfIs-6D.js} +1 -1
  30. package/build/client/_app/immutable/nodes/4.CFfIs-6D.js.br +0 -0
  31. package/build/client/_app/immutable/nodes/4.CFfIs-6D.js.gz +0 -0
  32. package/build/client/_app/version.json +1 -1
  33. package/build/client/_app/version.json.br +0 -0
  34. package/build/client/_app/version.json.gz +0 -0
  35. package/build/handler.js +7 -7
  36. package/build/index.js +7 -7
  37. package/build/server/chunks/chunks/{context.js-4TO_atTF.js → context.js-mE8mFnsg.js} +45 -12
  38. package/build/server/chunks/chunks/context.js-mE8mFnsg.js.map +1 -0
  39. package/build/server/chunks/chunks/{exports.js-BAfR8rsb.js → exports.js-CDl9URFG.js} +2 -2
  40. package/build/server/chunks/chunks/{exports.js-BAfR8rsb.js.map → exports.js-CDl9URFG.js.map} +1 -1
  41. package/build/server/chunks/chunks/{internal.js-B3hOADoz.js → internal.js-Bz3sw0lq.js} +3 -3
  42. package/build/server/chunks/chunks/{internal.js-B3hOADoz.js.map → internal.js-Bz3sw0lq.js.map} +1 -1
  43. package/build/server/chunks/chunks/internal2.js-BlXt2rfE.js.map +1 -1
  44. package/build/server/chunks/chunks/{shared.js-DN477dAr.js → shared.js-DwJl8ZNw.js} +2 -2
  45. package/build/server/chunks/chunks/{shared.js-DN477dAr.js.map → shared.js-DwJl8ZNw.js.map} +1 -1
  46. package/build/server/chunks/chunks/{utils.js-zf3Q16Eh.js → utils.js-DoU5Jolh.js} +3 -3
  47. package/build/server/chunks/chunks/{utils.js-zf3Q16Eh.js.map → utils.js-DoU5Jolh.js.map} +1 -1
  48. package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-Aa7zYzUB.js → _server.ts.js-CM6PedpD.js} +2 -2
  49. package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-Aa7zYzUB.js.map → _server.ts.js-CM6PedpD.js.map} +1 -1
  50. package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-CM73kt1H.js → _server.ts.js-BpfOlmKN.js} +2 -2
  51. package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-CM73kt1H.js.map → _server.ts.js-BpfOlmKN.js.map} +1 -1
  52. package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-CyiEsjc3.js → _server.ts.js-UvDSeD5O.js} +2 -2
  53. package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-CyiEsjc3.js.map → _server.ts.js-UvDSeD5O.js.map} +1 -1
  54. package/build/server/chunks/entries/endpoints/api/assets/_id_/retag/{_server.ts.js-Cpt_vVY0.js → _server.ts.js-D4kq3W1-.js} +2 -2
  55. package/build/server/chunks/entries/endpoints/api/assets/_id_/retag/{_server.ts.js-Cpt_vVY0.js.map → _server.ts.js-D4kq3W1-.js.map} +1 -1
  56. package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-BVdeCRy0.js → _server.ts.js-CcV2BTCH.js} +2 -2
  57. package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-BVdeCRy0.js.map → _server.ts.js-CcV2BTCH.js.map} +1 -1
  58. package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-QEa0nYZE.js → _server.ts.js-Cb0Dn_a2.js} +2 -2
  59. package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-QEa0nYZE.js.map → _server.ts.js-Cb0Dn_a2.js.map} +1 -1
  60. package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-Dk0sFdoM.js → _server.ts.js-ChlfFRqR.js} +2 -2
  61. package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-Dk0sFdoM.js.map → _server.ts.js-ChlfFRqR.js.map} +1 -1
  62. package/build/server/chunks/entries/endpoints/api/events/{_server.ts.js-CO8RZm3e.js → _server.ts.js-DnrgWGff.js} +2 -2
  63. package/build/server/chunks/entries/endpoints/api/events/{_server.ts.js-CO8RZm3e.js.map → _server.ts.js-DnrgWGff.js.map} +1 -1
  64. package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-8RzyUK6g.js → _server.ts.js-CFM83bne.js} +2 -2
  65. package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-8RzyUK6g.js.map → _server.ts.js-CFM83bne.js.map} +1 -1
  66. package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-YWliM590.js → _server.ts.js-BtJIovEG.js} +2 -2
  67. package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-YWliM590.js.map → _server.ts.js-BtJIovEG.js.map} +1 -1
  68. package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-CeXmOrwN.js → _server.ts.js-BAHE77r2.js} +2 -2
  69. package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-CeXmOrwN.js.map → _server.ts.js-BAHE77r2.js.map} +1 -1
  70. package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-DQaLSdGl.js → _server.ts.js-Cypw-CjT.js} +2 -2
  71. package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-DQaLSdGl.js.map → _server.ts.js-Cypw-CjT.js.map} +1 -1
  72. package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-BE_nYFP_.js → _server.ts.js-FrntF4Gn.js} +2 -2
  73. package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-BE_nYFP_.js.map → _server.ts.js-FrntF4Gn.js.map} +1 -1
  74. package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-BGxvZ4EV.js → _server.ts.js-BeEHDBep.js} +2 -2
  75. package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-BGxvZ4EV.js.map → _server.ts.js-BeEHDBep.js.map} +1 -1
  76. package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-Cvwr9a0l.js → _server.ts.js-Di1kfrl2.js} +2 -2
  77. package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-Cvwr9a0l.js.map → _server.ts.js-Di1kfrl2.js.map} +1 -1
  78. package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-DEMDXHsR.js → _server.ts.js-CrLHlnap.js} +2 -2
  79. package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-DEMDXHsR.js.map → _server.ts.js-CrLHlnap.js.map} +1 -1
  80. package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-DoeEIUoG.js → _server.ts.js-BcqKQTDk.js} +2 -2
  81. package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-DoeEIUoG.js.map → _server.ts.js-BcqKQTDk.js.map} +1 -1
  82. package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-0hMtYoB1.js → _server.ts.js-oFUf-T_H.js} +2 -2
  83. package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-0hMtYoB1.js.map → _server.ts.js-oFUf-T_H.js.map} +1 -1
  84. package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-B4YWmlkC.js → _server.ts.js-D46dsJCB.js} +2 -2
  85. package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-B4YWmlkC.js.map → _server.ts.js-D46dsJCB.js.map} +1 -1
  86. package/build/server/chunks/entries/endpoints/api/usage/{_server.ts.js-dNLtLO2U.js → _server.ts.js-BlKjsU-v.js} +2 -2
  87. package/build/server/chunks/entries/endpoints/api/usage/{_server.ts.js-dNLtLO2U.js.map → _server.ts.js-BlKjsU-v.js.map} +1 -1
  88. package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-BtCkf-hk.js → _server.ts.js-DafDgMis.js} +2 -2
  89. package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-BtCkf-hk.js.map → _server.ts.js-DafDgMis.js.map} +1 -1
  90. package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-CozrPtRv.js → _server.ts.js-6Vn2UuF6.js} +2 -2
  91. package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-CozrPtRv.js.map → _server.ts.js-6Vn2UuF6.js.map} +1 -1
  92. package/build/server/chunks/entries/{hooks.server.js-Cnsi5KsE.js → hooks.server.js-CSypLuCa.js} +2 -2
  93. package/build/server/chunks/entries/{hooks.server.js-Cnsi5KsE.js.map → hooks.server.js-CSypLuCa.js.map} +1 -1
  94. package/build/server/chunks/{handler-BN27c-Da.js → handler-C0NXVVDu.js} +5 -5
  95. package/build/server/chunks/{handler-BN27c-Da.js.map → handler-C0NXVVDu.js.map} +1 -1
  96. package/build/server/chunks/{index.js-C1f-Qrf0.js → index.js-D3rzqgKi.js} +5 -5
  97. package/build/server/chunks/{index.js-C1f-Qrf0.js.map → index.js-D3rzqgKi.js.map} +1 -1
  98. package/build/server/chunks/{manifest.js-CFkuYTzu.js → manifest.js-Dw3Fvt_0.js} +29 -29
  99. package/build/server/chunks/{manifest.js-CFkuYTzu.js.map → manifest.js-Dw3Fvt_0.js.map} +1 -1
  100. package/build/server/chunks/nodes/{0.js-B1i6yC_e.js → 0.js-D1uRRjFL.js} +2 -2
  101. package/build/server/chunks/nodes/{0.js-B1i6yC_e.js.map → 0.js-D1uRRjFL.js.map} +1 -1
  102. package/build/server/chunks/nodes/1.js-sYnc9kZu.js +7 -0
  103. package/build/server/chunks/nodes/{1.js-Bv64OO0t.js.map → 1.js-sYnc9kZu.js.map} +1 -1
  104. package/build/server/chunks/nodes/{2.js-DivaYfS1.js → 2.js-B3OCWo8p.js} +2 -2
  105. package/build/server/chunks/nodes/{2.js-DivaYfS1.js.map → 2.js-B3OCWo8p.js.map} +1 -1
  106. package/build/server/chunks/nodes/3.js-BWkr9ibc.js +7 -0
  107. package/build/server/chunks/nodes/{3.js-CUAbHOxb.js.map → 3.js-BWkr9ibc.js.map} +1 -1
  108. package/build/server/chunks/nodes/{4.js-Ceg0ylPI.js → 4.js-gujLHNff.js} +2 -2
  109. package/build/server/chunks/nodes/{4.js-Ceg0ylPI.js.map → 4.js-gujLHNff.js.map} +1 -1
  110. package/dist/cli.js +9 -0
  111. package/dist/core/figma-tag-script.d.ts +141 -0
  112. package/dist/core/figma-tag-script.js +236 -0
  113. package/dist/core/index.d.ts +2 -0
  114. package/dist/core/index.js +1 -0
  115. package/dist/server/cli-commands.d.ts +6 -0
  116. package/dist/server/cli-commands.js +1 -1
  117. package/dist/server/cli-figma-tags.d.ts +2 -0
  118. package/dist/server/cli-figma-tags.js +222 -0
  119. package/dist/server/figma-tags.d.ts +61 -0
  120. package/dist/server/figma-tags.js +153 -0
  121. package/dist/server/importers/figma.d.ts +17 -0
  122. package/dist/server/importers/figma.js +43 -18
  123. package/package.json +1 -1
  124. package/build/client/_app/immutable/chunks/C4w39LtR.js.br +0 -1
  125. package/build/client/_app/immutable/chunks/C4w39LtR.js.gz +0 -0
  126. package/build/client/_app/immutable/chunks/DGy-bMvO.js +0 -1
  127. package/build/client/_app/immutable/chunks/DGy-bMvO.js.br +0 -0
  128. package/build/client/_app/immutable/chunks/DGy-bMvO.js.gz +0 -0
  129. package/build/client/_app/immutable/chunks/WV4B0d_0.js.br +0 -0
  130. package/build/client/_app/immutable/chunks/WV4B0d_0.js.gz +0 -0
  131. package/build/client/_app/immutable/entry/app.GgnXYpzJ.js.br +0 -0
  132. package/build/client/_app/immutable/entry/app.GgnXYpzJ.js.gz +0 -0
  133. package/build/client/_app/immutable/entry/start.O9RwuDjf.js +0 -1
  134. package/build/client/_app/immutable/entry/start.O9RwuDjf.js.br +0 -0
  135. package/build/client/_app/immutable/entry/start.O9RwuDjf.js.gz +0 -0
  136. package/build/client/_app/immutable/nodes/0.D1g1plhI.js.br +0 -0
  137. package/build/client/_app/immutable/nodes/0.D1g1plhI.js.gz +0 -0
  138. package/build/client/_app/immutable/nodes/1.DzAnL2O6.js.br +0 -2
  139. package/build/client/_app/immutable/nodes/1.DzAnL2O6.js.gz +0 -0
  140. package/build/client/_app/immutable/nodes/2.BMN1YTJ4.js.br +0 -0
  141. package/build/client/_app/immutable/nodes/2.BMN1YTJ4.js.gz +0 -0
  142. package/build/client/_app/immutable/nodes/3.DL_TiE5x.js.br +0 -0
  143. package/build/client/_app/immutable/nodes/3.DL_TiE5x.js.gz +0 -0
  144. package/build/client/_app/immutable/nodes/4.D7FHmbgq.js.br +0 -0
  145. package/build/client/_app/immutable/nodes/4.D7FHmbgq.js.gz +0 -0
  146. package/build/server/chunks/chunks/context.js-4TO_atTF.js.map +0 -1
  147. package/build/server/chunks/nodes/1.js-Bv64OO0t.js +0 -7
  148. package/build/server/chunks/nodes/3.js-CUAbHOxb.js +0 -7
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Plugin-API code that reads and writes Fundus tags inside a Figma document.
3
+ * Two runtimes execute it: the Fundus Figma plugin imports these functions
4
+ * directly, and the CLI serializes them (via `Function.prototype.toString`)
5
+ * into a script an agent runs through Figma's MCP `use_figma` tool. The Figma
6
+ * REST API cannot write plugin data, so a script is the only headless write
7
+ * path.
8
+ *
9
+ * Serialization constrains every function listed in `SCRIPT_FUNCTIONS`: it
10
+ * may reference only its parameters, the other listed functions, and the
11
+ * constants in `SCRIPT_CONSTANTS` — never imports or other module scope. The
12
+ * `figma` global is passed in as a parameter so tests can drive the same code
13
+ * against a fake document.
14
+ */
15
+ /** Shared-plugin-data namespace; the REST API returns it under `sharedPluginData`. */
16
+ export const FIGMA_TAG_NAMESPACE = 'fundus';
17
+ /** Key holding the node's Fundus asset id (the tag). */
18
+ export const FIGMA_TAG_KEY = 'assetId';
19
+ /**
20
+ * Key holding the exact label of the annotation Fundus owns on the node; empty
21
+ * when it owns none. Tracking ownership out-of-band keeps the visible label
22
+ * exactly equal to the tag and leaves the user's own annotations untouched.
23
+ */
24
+ export const FIGMA_ANNOTATION_LABEL_KEY = 'annotationLabel';
25
+ /**
26
+ * Visible prefix earlier plugin builds used as the ownership marker. Still
27
+ * recognized so those annotations are migrated or stripped instead of
28
+ * duplicated.
29
+ */
30
+ export const LEGACY_ANNOTATION_PREFIX = 'Fundus asset: ';
31
+ /**
32
+ * Whether the id names a node inside an instance (`I<instance>;<source>`).
33
+ * Such nodes inherit the shared plugin data of the main component's node, so
34
+ * they report its tag without being a separate taggable node.
35
+ */
36
+ export function isInstanceSublayerId(id) {
37
+ return id.startsWith('I');
38
+ }
39
+ export function supportsAnnotations(node) {
40
+ return 'annotations' in node;
41
+ }
42
+ /**
43
+ * The annotation's shown text. Figma reports a plain-label annotation with
44
+ * `labelMarkdown: ""`, so an empty markdown field falls back to `label`.
45
+ */
46
+ export function annotationText(annotation) {
47
+ return annotation.labelMarkdown || annotation.label || '';
48
+ }
49
+ /**
50
+ * Figma returns BOTH `label` and `labelMarkdown` when reading annotations, but
51
+ * the setter rejects an annotation that carries both. Re-emit a single-field
52
+ * copy (non-empty markdown first), preserving `properties`/`categoryId`, so
53
+ * preserved annotations can be written back without failing validation.
54
+ */
55
+ export function writableAnnotation(annotation) {
56
+ const copy = {};
57
+ if (annotation.labelMarkdown)
58
+ copy.labelMarkdown = annotation.labelMarkdown;
59
+ else if (annotation.label !== undefined)
60
+ copy.label = annotation.label;
61
+ if (annotation.properties)
62
+ copy.properties = annotation.properties;
63
+ if (annotation.categoryId)
64
+ copy.categoryId = annotation.categoryId;
65
+ return copy;
66
+ }
67
+ /** Whether this annotation is one Fundus owns — by the stored label or the legacy prefix. */
68
+ export function isFundusAnnotation(annotation, ownedLabel) {
69
+ const text = annotationText(annotation);
70
+ return (ownedLabel !== '' && text === ownedLabel) || text.startsWith(LEGACY_ANNOTATION_PREFIX);
71
+ }
72
+ /**
73
+ * Ensure the node carries exactly one Fundus annotation whose label is the tag,
74
+ * preserving all others. Writes only when not already in sync, so a steady
75
+ * state neither dirties the file nor adds undo steps.
76
+ */
77
+ export function writeFundusAnnotation(node, tag) {
78
+ const owned = node.getSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_ANNOTATION_LABEL_KEY);
79
+ const current = node.annotations;
80
+ const ours = current.filter((annotation) => isFundusAnnotation(annotation, owned));
81
+ const inSync = owned === tag && ours.length === 1 && annotationText(ours[0]) === tag;
82
+ if (inSync)
83
+ return;
84
+ const kept = current
85
+ .filter((annotation) => !isFundusAnnotation(annotation, owned) && annotationText(annotation) !== tag)
86
+ .map(writableAnnotation);
87
+ node.annotations = [...kept, { label: tag }];
88
+ if (owned !== tag) {
89
+ node.setSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_ANNOTATION_LABEL_KEY, tag);
90
+ }
91
+ }
92
+ /** Remove the Fundus annotation (incl. legacy), preserving all others. */
93
+ export function stripFundusAnnotation(node) {
94
+ const owned = node.getSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_ANNOTATION_LABEL_KEY);
95
+ const current = node.annotations;
96
+ const kept = current
97
+ .filter((annotation) => !isFundusAnnotation(annotation, owned))
98
+ .map(writableAnnotation);
99
+ if (kept.length !== current.length)
100
+ node.annotations = kept;
101
+ if (owned !== '')
102
+ node.setSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_ANNOTATION_LABEL_KEY, '');
103
+ }
104
+ export function pageOfNode(node) {
105
+ let current = node;
106
+ while (current && current.type !== 'PAGE')
107
+ current = current.parent;
108
+ return current ? current : null;
109
+ }
110
+ /**
111
+ * Resolve a scene node and load its page. `loadAsync` loads without
112
+ * navigating, so running a script never moves anyone's canvas.
113
+ */
114
+ export async function loadTaggableNode(figma, nodeId) {
115
+ const node = await figma.getNodeByIdAsync(nodeId);
116
+ const page = node ? pageOfNode(node) : null;
117
+ if (!node || !page || node.type === 'PAGE') {
118
+ throw new Error(`Figma node "${nodeId}" does not exist in this file.`);
119
+ }
120
+ await page.loadAsync();
121
+ return node;
122
+ }
123
+ /**
124
+ * The first other node carrying `tag`, across every page. Scripts start with
125
+ * only the first page loaded, and `findAll` on an unloaded page silently
126
+ * misses content, so each page is loaded first.
127
+ */
128
+ export async function findTagHolder(figma, tag, exceptNodeId) {
129
+ for (const page of figma.root.children) {
130
+ await page.loadAsync();
131
+ const [holder] = page.findAll((candidate) => candidate.id !== exceptNodeId &&
132
+ !isInstanceSublayerId(candidate.id) &&
133
+ candidate.getSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_TAG_KEY) === tag);
134
+ if (holder)
135
+ return { node: holder, page };
136
+ }
137
+ return null;
138
+ }
139
+ /**
140
+ * Tag a node, re-checking the live document right before writing: the CLI's
141
+ * REST snapshot can lag behind edits made since it ran.
142
+ */
143
+ export async function setFundusTag(figma, input) {
144
+ const node = await loadTaggableNode(figma, input.nodeId);
145
+ const previous = node.getSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_TAG_KEY);
146
+ if (!input.force && previous === input.tag) {
147
+ throw new Error(`Node "${node.name}" (${node.id}) already carries tag "${input.tag}"; rerun prepare-set-tag with --force to rewrite it.`);
148
+ }
149
+ if (!input.force && previous !== '') {
150
+ throw new Error(`Node "${node.name}" (${node.id}) already carries tag "${previous}"; rerun prepare-set-tag with --force to replace it.`);
151
+ }
152
+ const holder = await findTagHolder(figma, input.tag, node.id);
153
+ if (holder) {
154
+ throw new Error(`Tag "${input.tag}" is already used by "${holder.node.name}" (${holder.node.id}) on page "${holder.page.name}"; choose another tag.`);
155
+ }
156
+ node.setSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_TAG_KEY, input.tag);
157
+ const annotated = supportsAnnotations(node);
158
+ if (annotated)
159
+ writeFundusAnnotation(node, input.tag);
160
+ return {
161
+ ok: true,
162
+ nodeId: node.id,
163
+ nodeName: node.name,
164
+ tag: input.tag,
165
+ previousTag: previous === '' ? null : previous,
166
+ annotated
167
+ };
168
+ }
169
+ export async function deleteFundusTag(figma, input) {
170
+ const node = await loadTaggableNode(figma, input.nodeId);
171
+ const current = node.getSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_TAG_KEY);
172
+ if (current !== input.tag) {
173
+ throw new Error(current === ''
174
+ ? `Node "${node.name}" (${node.id}) carries no Fundus tag.`
175
+ : `Node "${node.name}" (${node.id}) carries tag "${current}", not "${input.tag}"; rerun prepare-delete-tag.`);
176
+ }
177
+ node.setSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_TAG_KEY, '');
178
+ let annotationRemoved = false;
179
+ if (supportsAnnotations(node)) {
180
+ const before = node.annotations.length;
181
+ stripFundusAnnotation(node);
182
+ annotationRemoved = node.annotations.length < before;
183
+ }
184
+ else {
185
+ node.setSharedPluginData(FIGMA_TAG_NAMESPACE, FIGMA_ANNOTATION_LABEL_KEY, '');
186
+ }
187
+ return {
188
+ ok: true,
189
+ nodeId: node.id,
190
+ nodeName: node.name,
191
+ removedTag: input.tag,
192
+ annotationRemoved
193
+ };
194
+ }
195
+ const SCRIPT_CONSTANTS = {
196
+ FIGMA_TAG_NAMESPACE,
197
+ FIGMA_TAG_KEY,
198
+ FIGMA_ANNOTATION_LABEL_KEY,
199
+ LEGACY_ANNOTATION_PREFIX
200
+ };
201
+ const SCRIPT_FUNCTIONS = [
202
+ isInstanceSublayerId,
203
+ supportsAnnotations,
204
+ annotationText,
205
+ writableAnnotation,
206
+ isFundusAnnotation,
207
+ writeFundusAnnotation,
208
+ stripFundusAnnotation,
209
+ pageOfNode,
210
+ loadTaggableNode,
211
+ findTagHolder,
212
+ setFundusTag,
213
+ deleteFundusTag
214
+ ];
215
+ /**
216
+ * A self-contained `use_figma` script body: top-level `await` and `return`,
217
+ * with the `figma` global in scope. It returns the operation's JSON result
218
+ * and throws, before mutating anything, when the live document disagrees.
219
+ */
220
+ function buildScript(summary, call) {
221
+ const constants = Object.entries(SCRIPT_CONSTANTS).map(([name, value]) => `const ${name} = ${JSON.stringify(value)};`);
222
+ return [
223
+ `// ${summary}`,
224
+ '// Generated by the fundus CLI — run it unchanged with the Figma MCP `use_figma` tool.',
225
+ ...constants,
226
+ ...SCRIPT_FUNCTIONS.map(String),
227
+ `return await ${call};`,
228
+ ''
229
+ ].join('\n\n');
230
+ }
231
+ export function buildSetTagScript(input) {
232
+ return buildScript(`Set Fundus tag ${JSON.stringify(input.tag)} on Figma node ${JSON.stringify(input.nodeId)}.`, `setFundusTag(figma, ${JSON.stringify(input)})`);
233
+ }
234
+ export function buildDeleteTagScript(input) {
235
+ return buildScript(`Delete Fundus tag ${JSON.stringify(input.tag)} from Figma node ${JSON.stringify(input.nodeId)}.`, `deleteFundusTag(figma, ${JSON.stringify(input)})`);
236
+ }
@@ -3,6 +3,8 @@ export { createEmptyLibrary, parseLibrary, serializeLibrary } from './library.js
3
3
  export { rawFileNameError, rawFilePathError, portableRawPathKey, isFundusTransactionName } from './raw-path.js';
4
4
  export { ROOT_FOLDER, FOLDER_SENTINEL, folderNameError, folderPathError, normalizeFolderPath, portableFolderPathKey, parentFolderPath, folderNameOf, joinFolderPath, folderOfRawPath, rawFileNameOfPath, isFolderOrDescendant, replaceFolderPrefix } from './folder-path.js';
5
5
  export { RESERVED_ASSET_IDS, isValidAssetId, assetIdError, manifestNameError } from './asset-id.js';
6
+ export type { SetFigmaTagInput, SetFigmaTagResult, DeleteFigmaTagInput, DeleteFigmaTagResult } from './figma-tag-script.js';
7
+ export { buildSetTagScript, buildDeleteTagScript, isInstanceSublayerId } from './figma-tag-script.js';
6
8
  export type { ManifestColor } from './manifest-appearance.js';
7
9
  export { MANIFEST_COLORS, MANIFEST_EMOJIS, isManifestColor, isManifestEmoji, resolveManifestColor, manifestColorError, manifestEmojiError } from './manifest-appearance.js';
8
10
  export { acceptedExtensions, acceptedExtensionsForType, typesForFileName, proposeAssetId } from './ingest.js';
@@ -2,6 +2,7 @@ export { createEmptyLibrary, parseLibrary, serializeLibrary } from './library.js
2
2
  export { rawFileNameError, rawFilePathError, portableRawPathKey, isFundusTransactionName } from './raw-path.js';
3
3
  export { ROOT_FOLDER, FOLDER_SENTINEL, folderNameError, folderPathError, normalizeFolderPath, portableFolderPathKey, parentFolderPath, folderNameOf, joinFolderPath, folderOfRawPath, rawFileNameOfPath, isFolderOrDescendant, replaceFolderPrefix } from './folder-path.js';
4
4
  export { RESERVED_ASSET_IDS, isValidAssetId, assetIdError, manifestNameError } from './asset-id.js';
5
+ export { buildSetTagScript, buildDeleteTagScript, isInstanceSublayerId } from './figma-tag-script.js';
5
6
  export { MANIFEST_COLORS, MANIFEST_EMOJIS, isManifestColor, isManifestEmoji, resolveManifestColor, manifestColorError, manifestEmojiError } from './manifest-appearance.js';
6
7
  export { acceptedExtensions, acceptedExtensionsForType, typesForFileName, proposeAssetId } from './ingest.js';
7
8
  export { canonicalizeParameters, compareCodeUnits } from './canonicalize.js';
@@ -12,6 +12,12 @@ export declare const ASSET_SET_HELP = "fundus asset set \u2014 update an asset's
12
12
  export declare const ASSET_RENAME_HELP = "fundus asset rename \u2014 rename an asset id\n\nUsage: fundus asset rename <id> <newId> [--json]\n\nRenames the asset's id (the raw file keeps its name \u2014 ids are the\nstable key) and re-hashes its proxy under the new id via the autosave\nchain. Fails if another asset references <id> \u2014 update the referrers\nfirst \u2014 or if <newId> is taken or invalid.\n\nFlags:\n --json print {\"ok\": true, \"asset\": <AssetStateDto>, \"issues\": [\u2026]}\n --help this help\n\nExample:\n fundus asset rename sidebarPanel navigationPanel --json\n";
13
13
  export declare const ASSET_DELETE_HELP = "fundus asset delete \u2014 delete an asset\n\nUsage: fundus asset delete <id> [--json]\n\nDeletes the library record, its raw file, and its proxies, then\nregenerates the affected manifest modules. Fails if another asset\nreferences it \u2014 remove the references first.\n\nFlags:\n --json print {\"ok\": true, \"deletedAssetId\": \"<id>\", \"issues\": [\u2026]}\n --help this help\n\nExample:\n fundus asset delete obsoleteBanner --json\n";
14
14
  export declare const ASSET_MOVE_HELP = "fundus asset move \u2014 move an asset between folders\n\nUsage: fundus asset move <id> <folder|root> [--json]\n\nMoves the raw file and its current proxy without changing the stable asset id,\nthen runs the autosave chain. The destination folder must already exist; use\nthe literal \"root\" for the implicit root.\n\nFlags:\n --json print {\"ok\": true, \"asset\": <AssetStateDto>, \"issues\": [\u2026]}\n --help this help\n\nExample:\n fundus asset move navigationPanel \"UI/Buttons\" --json\n";
15
+ /**
16
+ * `--figma-file` accepts either a raw Figma file key or a full design/file link.
17
+ * A value that parses as a URL is treated as a link (fileKey extracted via
18
+ * `parseFigmaFileKey`); anything else is passed through as a raw key.
19
+ */
20
+ export declare function resolveFigmaFileKeyFlag(value: string): string;
15
21
  export declare function runAssetCommand(argumentList: string[]): Promise<void>;
16
22
  export declare const FOLDER_HELP = "fundus folder \u2014 manage the mirrored raw/proxy folder tree\n\nUsage: fundus folder <subcommand> \u2026\n\nSubcommands:\n list list registered folders and their direct assets\n create create a persisted folder (fundus folder create <path>)\n rename rename one folder segment and its subtree\n move move a complete folder subtree below another folder\n delete recursively delete a folder subtree and its assets\n\nAll mutations update rawDir and proxyDir together, maintain .gitkeep files,\nand run the full autosave chain. All subcommands accept --json and --help.\n";
17
23
  export declare function runFolderCommand(argumentList: string[]): Promise<void>;
@@ -898,7 +898,7 @@ function parseFigmaScale(value, commandPath) {
898
898
  * A value that parses as a URL is treated as a link (fileKey extracted via
899
899
  * `parseFigmaFileKey`); anything else is passed through as a raw key.
900
900
  */
901
- function resolveFigmaFileKeyFlag(value) {
901
+ export function resolveFigmaFileKeyFlag(value) {
902
902
  try {
903
903
  new URL(value);
904
904
  }
@@ -0,0 +1,2 @@
1
+ export declare const FIGMA_HELP = "fundus figma \u2014 prepare Fundus tag writes in Figma\n\nUsage: fundus figma <subcommand> \u2026 [--json]\n\nThe Figma REST API cannot write plugin data, so Fundus cannot tag nodes by\nitself. These commands check the request against the file (via REST) and the\nlibrary, then print a script. Run that script unchanged with the Figma MCP\n`use_figma` tool on the same file: it re-checks the live document (the set\nscript loads every page, which takes a few seconds on large files), writes\nthe tag and its Dev Mode annotation exactly like the Fundus Figma plugin, and\nreturns the result. It throws without writing when the document changed.\n\nSubcommands:\n prepare-set-tag tag a node (--node-id, --tag)\n prepare-delete-tag remove a node's tag (--tag or --node-id)\n\nRequires import.figma.token in fundus.config.ts.\n";
2
+ export declare function runFigmaCommand(argumentList: string[]): Promise<void>;
@@ -0,0 +1,222 @@
1
+ /**
2
+ * The `fundus figma` command group: prepare Fundus tag writes for Figma. The
3
+ * REST API cannot write plugin data, so these commands only validate (tag
4
+ * grammar, uniqueness in the file, library collisions) and print a script the
5
+ * caller runs with Figma's MCP `use_figma` tool.
6
+ */
7
+ import { assetIdError } from '../core/index.js';
8
+ import { loadConfig } from './config-loader.js';
9
+ import { FigmaTagConflictError, loadFigmaTagPlanContext, parseFigmaNodeId, planDeleteFigmaTag, planSetFigmaTag } from './figma-tags.js';
10
+ import { resolveFigmaFileKeyFlag } from './cli-commands.js';
11
+ import { CliUsageError, parseArguments, printJsonDocument, requirePositionals } from './cli-io.js';
12
+ export const FIGMA_HELP = `fundus figma — prepare Fundus tag writes in Figma
13
+
14
+ Usage: fundus figma <subcommand> … [--json]
15
+
16
+ The Figma REST API cannot write plugin data, so Fundus cannot tag nodes by
17
+ itself. These commands check the request against the file (via REST) and the
18
+ library, then print a script. Run that script unchanged with the Figma MCP
19
+ \`use_figma\` tool on the same file: it re-checks the live document (the set
20
+ script loads every page, which takes a few seconds on large files), writes
21
+ the tag and its Dev Mode annotation exactly like the Fundus Figma plugin, and
22
+ returns the result. It throws without writing when the document changed.
23
+
24
+ Subcommands:
25
+ prepare-set-tag tag a node (--node-id, --tag)
26
+ prepare-delete-tag remove a node's tag (--tag or --node-id)
27
+
28
+ Requires import.figma.token in fundus.config.ts.
29
+ `;
30
+ const PREPARE_SET_TAG_HELP = `fundus figma prepare-set-tag — prepare a use_figma script that tags a node
31
+
32
+ Usage: fundus figma prepare-set-tag --node-id <id> --tag <tag>
33
+ [--file <key|link>] [--force] [--json]
34
+
35
+ Refuses, listing the file's existing tags, when another node already carries
36
+ the tag or a Fundus asset with that id exists from another source. Refuses
37
+ when the node already carries a tag (the same one or another) unless --force.
38
+
39
+ Flags:
40
+ --node-id <id> the node to tag: 12:34 or 12-34
41
+ --tag <tag> the Fundus asset id to store on the node (camelCase)
42
+ --file <key|link> Figma file key or link; default:
43
+ import.figma.defaultFileKey from fundus.config.ts
44
+ --force replace the node's current tag, or rewrite the same tag
45
+ --json print {"ok": true, "fileKey", "nodeId", "tag",
46
+ "previousTag", "warnings", "next", "code"}; a refusal
47
+ prints {"ok": false, "error": {"message"}, "existingTags"}
48
+ when the file's tags help choose another
49
+ --help this help
50
+
51
+ Without --json the script goes to stdout, everything else to stderr.
52
+ The script returns {"ok": true, "nodeId", "nodeName", "tag", "previousTag",
53
+ "annotated"}. Afterwards: \`fundus asset ingest --from figma --figma-tag <tag>\`.
54
+
55
+ Example:
56
+ fundus figma prepare-set-tag --file 'https://www.figma.com/design/abc/UI' \\
57
+ --node-id 12-34 --tag navigationPanel --json
58
+ `;
59
+ const PREPARE_DELETE_TAG_HELP = `fundus figma prepare-delete-tag — prepare a use_figma script that removes a tag
60
+
61
+ Usage: fundus figma prepare-delete-tag (--tag <tag> | --node-id <id>)
62
+ [--file <key|link>] [--force] [--json]
63
+
64
+ Removes the tag and its Dev Mode annotation; other annotations stay. Refuses
65
+ unless --force when a Fundus asset is imported from the tag (its reimport
66
+ would fail afterwards).
67
+
68
+ Flags:
69
+ --tag <tag> the tag to remove (must be on exactly one node)
70
+ --node-id <id> the node whose tag to remove: 12:34 or 12-34; with --tag,
71
+ both must match
72
+ --file <key|link> Figma file key or link; default:
73
+ import.figma.defaultFileKey from fundus.config.ts
74
+ --force delete even when a Fundus asset is imported from the tag
75
+ --json print {"ok": true, "fileKey", "nodeId", "tag",
76
+ "warnings", "next", "code"}; refusals as for
77
+ prepare-set-tag
78
+ --help this help
79
+
80
+ The script returns {"ok": true, "nodeId", "nodeName", "removedTag",
81
+ "annotationRemoved"}.
82
+
83
+ Example:
84
+ fundus figma prepare-delete-tag --tag navigationPanel --json
85
+ `;
86
+ const FIGMA_SUBCOMMAND_HELP = {
87
+ 'prepare-set-tag': PREPARE_SET_TAG_HELP,
88
+ 'prepare-delete-tag': PREPARE_DELETE_TAG_HELP
89
+ };
90
+ const TAG_FLAGS = {
91
+ json: { takesValue: false },
92
+ help: { takesValue: false },
93
+ file: { takesValue: true },
94
+ 'node-id': { takesValue: true },
95
+ tag: { takesValue: true },
96
+ force: { takesValue: false }
97
+ };
98
+ /** Fail on a malformed tag before any Figma request. */
99
+ function assertTag(tag) {
100
+ const problem = tag === undefined ? null : assetIdError(tag);
101
+ if (problem)
102
+ throw new Error(problem);
103
+ }
104
+ function stringFlag(parsed, name) {
105
+ const value = parsed.flags[name];
106
+ return typeof value === 'string' ? value : undefined;
107
+ }
108
+ async function resolvePlanInputs(parsed, commandPath) {
109
+ const config = await loadConfig(process.cwd());
110
+ const fileFlag = stringFlag(parsed, 'file');
111
+ const fileKey = fileFlag !== undefined ? resolveFigmaFileKeyFlag(fileFlag) : config.import.figma.defaultFileKey;
112
+ if (!fileKey) {
113
+ throw new CliUsageError(`\`${commandPath}\` needs --file <key|link> or import.figma.defaultFileKey in fundus.config.ts.`);
114
+ }
115
+ return { fileKey, context: await loadFigmaTagPlanContext(config, fileKey) };
116
+ }
117
+ function nextStep(fileKey, subject = '"code"') {
118
+ return `Run ${subject} unchanged with the Figma MCP use_figma tool on fileKey "${fileKey}". It returns the result as JSON, or throws without writing when the live document disagrees.`;
119
+ }
120
+ /** Refusals that list the file's tags get their own document; others take the standard error path. */
121
+ function reportConflict(cause, json) {
122
+ if (!(cause instanceof FigmaTagConflictError))
123
+ return false;
124
+ if (json) {
125
+ printJsonDocument({
126
+ ok: false,
127
+ error: { message: cause.message },
128
+ existingTags: cause.existingTags
129
+ });
130
+ }
131
+ else {
132
+ console.error(`fundus: ${cause.message}`);
133
+ console.error(cause.existingTags.length === 0 ? 'No tags in this file yet.' : 'Existing tags in this file:');
134
+ for (const entry of cause.existingTags) {
135
+ console.error(` ${entry.tag} ${entry.nodeId} "${entry.nodeName}" (page "${entry.page}")`);
136
+ }
137
+ }
138
+ process.exitCode = 1;
139
+ return true;
140
+ }
141
+ function printPlan(plan, document, json) {
142
+ if (json) {
143
+ printJsonDocument({ ok: true, ...document, next: nextStep(plan.fileKey), code: plan.code });
144
+ return;
145
+ }
146
+ for (const warning of plan.warnings)
147
+ console.error(`⚠ ${warning}`);
148
+ console.error(`${nextStep(plan.fileKey, 'the script below')}\n`);
149
+ process.stdout.write(plan.code);
150
+ }
151
+ async function runPrepareSetTag(argumentList) {
152
+ const commandPath = 'fundus figma prepare-set-tag';
153
+ const parsed = parseArguments(argumentList, TAG_FLAGS, commandPath);
154
+ requirePositionals(parsed, [], commandPath);
155
+ const nodeIdFlag = stringFlag(parsed, 'node-id');
156
+ const tag = stringFlag(parsed, 'tag');
157
+ if (nodeIdFlag === undefined || tag === undefined) {
158
+ throw new CliUsageError(`\`${commandPath}\` needs --node-id and --tag — run \`${commandPath} --help\`.`);
159
+ }
160
+ const nodeId = parseFigmaNodeId(nodeIdFlag);
161
+ assertTag(tag);
162
+ const json = parsed.flags.json === true;
163
+ const { fileKey, context } = await resolvePlanInputs(parsed, commandPath);
164
+ try {
165
+ const plan = planSetFigmaTag({ fileKey, nodeId, tag, force: parsed.flags.force === true }, context);
166
+ printPlan(plan, {
167
+ fileKey,
168
+ nodeId: plan.nodeId,
169
+ tag: plan.tag,
170
+ previousTag: plan.previousTag,
171
+ warnings: plan.warnings
172
+ }, json);
173
+ }
174
+ catch (cause) {
175
+ if (!reportConflict(cause, json))
176
+ throw cause;
177
+ }
178
+ }
179
+ async function runPrepareDeleteTag(argumentList) {
180
+ const commandPath = 'fundus figma prepare-delete-tag';
181
+ const parsed = parseArguments(argumentList, TAG_FLAGS, commandPath);
182
+ requirePositionals(parsed, [], commandPath);
183
+ const nodeIdFlag = stringFlag(parsed, 'node-id');
184
+ const tag = stringFlag(parsed, 'tag');
185
+ if (nodeIdFlag === undefined && tag === undefined) {
186
+ throw new CliUsageError(`\`${commandPath}\` needs --tag or --node-id — run \`${commandPath} --help\`.`);
187
+ }
188
+ const nodeId = nodeIdFlag === undefined ? undefined : parseFigmaNodeId(nodeIdFlag);
189
+ assertTag(tag);
190
+ const json = parsed.flags.json === true;
191
+ const { fileKey, context } = await resolvePlanInputs(parsed, commandPath);
192
+ try {
193
+ const plan = planDeleteFigmaTag({ fileKey, nodeId, tag, force: parsed.flags.force === true }, context);
194
+ printPlan(plan, { fileKey, nodeId: plan.nodeId, tag: plan.tag, warnings: plan.warnings }, json);
195
+ }
196
+ catch (cause) {
197
+ if (!reportConflict(cause, json))
198
+ throw cause;
199
+ }
200
+ }
201
+ export async function runFigmaCommand(argumentList) {
202
+ const [subcommand, ...rest] = argumentList;
203
+ if (subcommand === undefined || subcommand === '--help') {
204
+ console.log(FIGMA_HELP);
205
+ if (subcommand === undefined) {
206
+ throw new CliUsageError('`fundus figma` needs a subcommand: prepare-set-tag, prepare-delete-tag.');
207
+ }
208
+ return;
209
+ }
210
+ if (rest.includes('--help')) {
211
+ console.log(FIGMA_SUBCOMMAND_HELP[subcommand] ?? FIGMA_HELP);
212
+ return;
213
+ }
214
+ switch (subcommand) {
215
+ case 'prepare-set-tag':
216
+ return runPrepareSetTag(rest);
217
+ case 'prepare-delete-tag':
218
+ return runPrepareDeleteTag(rest);
219
+ default:
220
+ throw new CliUsageError(`Unknown subcommand "figma ${subcommand}" — run \`fundus figma --help\`.`);
221
+ }
222
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Planning for the `fundus figma prepare-*-tag` commands. The Figma REST API
3
+ * can read shared plugin data but not write it, so Fundus validates against a
4
+ * REST snapshot of the file plus the local library, then hands back a
5
+ * `use_figma` script (figma-tag-script.ts) that performs the write inside
6
+ * Figma and re-checks the live document first.
7
+ */
8
+ import { type Library } from '../core/index.js';
9
+ import type { ResolvedFundusConfig } from './config-loader.js';
10
+ /** A tagged node in the file, as reported alongside conflicts. */
11
+ export interface FigmaTagListing {
12
+ tag: string;
13
+ nodeId: string;
14
+ nodeName: string;
15
+ page: string;
16
+ }
17
+ /** An expected refusal that carries the file's current tags so the caller can pick another. */
18
+ export declare class FigmaTagConflictError extends Error {
19
+ readonly existingTags: FigmaTagListing[];
20
+ constructor(message: string, existingTags: FigmaTagListing[]);
21
+ }
22
+ export interface FigmaTagPlan {
23
+ fileKey: string;
24
+ nodeId: string;
25
+ /** Non-fatal findings the caller should relay (e.g. a Fundus asset to reimport). */
26
+ warnings: string[];
27
+ /** The `use_figma` script body. */
28
+ code: string;
29
+ }
30
+ export interface SetFigmaTagPlan extends FigmaTagPlan {
31
+ tag: string;
32
+ /** The node's tag per the REST snapshot; null when untagged or not yet visible. */
33
+ previousTag: string | null;
34
+ }
35
+ export interface DeleteFigmaTagPlan extends FigmaTagPlan {
36
+ tag: string;
37
+ }
38
+ /** Normalize a `12:34` / `12-34` node id; Fundus imports only this id form. */
39
+ export declare function parseFigmaNodeId(value: string): string;
40
+ export interface FigmaTagPlanContext {
41
+ library: Library;
42
+ /** REST document tree of the file, with shared plugin data. */
43
+ document: unknown;
44
+ }
45
+ export interface SetFigmaTagRequest {
46
+ fileKey: string;
47
+ nodeId: string;
48
+ tag: string;
49
+ force: boolean;
50
+ }
51
+ export declare function planSetFigmaTag(request: SetFigmaTagRequest, { library, document }: FigmaTagPlanContext): SetFigmaTagPlan;
52
+ export interface DeleteFigmaTagRequest {
53
+ fileKey: string;
54
+ /** Exactly one of nodeId / tag identifies the node; both must agree when given. */
55
+ nodeId?: string;
56
+ tag?: string;
57
+ force: boolean;
58
+ }
59
+ export declare function planDeleteFigmaTag(request: DeleteFigmaTagRequest, { library, document }: FigmaTagPlanContext): DeleteFigmaTagPlan;
60
+ /** Load the library and the file's REST document for planning. */
61
+ export declare function loadFigmaTagPlanContext(config: ResolvedFundusConfig, fileKey: string): Promise<FigmaTagPlanContext>;