fundus 0.24.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 (154) hide show
  1. package/README.md +42 -20
  2. package/build/client/_app/immutable/chunks/{CCIxsMRK.js → B_4qwt4o.js} +1 -1
  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/{CIZHDrUF.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/{CnnMN4wM.js → p2q3f3Ml.js} +1 -1
  9. package/build/client/_app/immutable/chunks/p2q3f3Ml.js.br +0 -0
  10. package/build/client/_app/immutable/chunks/{CnnMN4wM.js.gz → p2q3f3Ml.js.gz} +0 -0
  11. package/build/client/_app/immutable/entry/{app.BPBGxhbo.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.CquXHiCP.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.xSi5hUiK.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.CgzHO6l0.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.B-HZ9QO3.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._rTlKXDo.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-CcnW-JJJ.js → exports.js-CDl9URFG.js} +2 -2
  40. package/build/server/chunks/chunks/{exports.js-CcnW-JJJ.js.map → exports.js-CDl9URFG.js.map} +1 -1
  41. package/build/server/chunks/chunks/{internal.js-ByBTomBV.js → internal.js-Bz3sw0lq.js} +3 -3
  42. package/build/server/chunks/chunks/{internal.js-ByBTomBV.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-CCqNepI3.js → shared.js-DwJl8ZNw.js} +2 -2
  45. package/build/server/chunks/chunks/{shared.js-CCqNepI3.js.map → shared.js-DwJl8ZNw.js.map} +1 -1
  46. package/build/server/chunks/chunks/{utils.js-GaDYRoLP.js → utils.js-DoU5Jolh.js} +3 -3
  47. package/build/server/chunks/chunks/{utils.js-GaDYRoLP.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-DegMVxhC.js → handler-C0NXVVDu.js} +5 -5
  95. package/build/server/chunks/{handler-DegMVxhC.js.map → handler-C0NXVVDu.js.map} +1 -1
  96. package/build/server/chunks/{index.js-BrCnc8Gq.js → index.js-D3rzqgKi.js} +5 -5
  97. package/build/server/chunks/{index.js-BrCnc8Gq.js.map → index.js-D3rzqgKi.js.map} +1 -1
  98. package/build/server/chunks/{manifest.js-mKxXvxuB.js → manifest.js-Dw3Fvt_0.js} +29 -29
  99. package/build/server/chunks/{manifest.js-mKxXvxuB.js.map → manifest.js-Dw3Fvt_0.js.map} +1 -1
  100. package/build/server/chunks/nodes/{0.js-Bnw_8E4c.js → 0.js-D1uRRjFL.js} +2 -2
  101. package/build/server/chunks/nodes/{0.js-Bnw_8E4c.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-DpvNtg1z.js.map → 1.js-sYnc9kZu.js.map} +1 -1
  104. package/build/server/chunks/nodes/{2.js-h7fLpCKT.js → 2.js-B3OCWo8p.js} +2 -2
  105. package/build/server/chunks/nodes/{2.js-h7fLpCKT.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-XlL1_Sjt.js.map → 3.js-BWkr9ibc.js.map} +1 -1
  108. package/build/server/chunks/nodes/{4.js-BRkm0s7e.js → 4.js-gujLHNff.js} +2 -2
  109. package/build/server/chunks/nodes/{4.js-BRkm0s7e.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/runtime/canvas.d.ts +14 -9
  116. package/dist/runtime/canvas.js +20 -8
  117. package/dist/runtime/index.d.ts +3 -1
  118. package/dist/runtime/index.js +1 -0
  119. package/dist/runtime/retained-entry.d.ts +3 -3
  120. package/dist/runtime/retained-entry.js +4 -4
  121. package/dist/runtime/retained-entry.svelte.d.ts +26 -0
  122. package/dist/runtime/retained-entry.svelte.js +62 -0
  123. package/dist/server/cli-commands.d.ts +6 -0
  124. package/dist/server/cli-commands.js +1 -1
  125. package/dist/server/cli-figma-tags.d.ts +2 -0
  126. package/dist/server/cli-figma-tags.js +222 -0
  127. package/dist/server/figma-tags.d.ts +61 -0
  128. package/dist/server/figma-tags.js +153 -0
  129. package/dist/server/importers/figma.d.ts +17 -0
  130. package/dist/server/importers/figma.js +43 -18
  131. package/package.json +1 -1
  132. package/build/client/_app/immutable/chunks/CCIxsMRK.js.br +0 -0
  133. package/build/client/_app/immutable/chunks/CCIxsMRK.js.gz +0 -0
  134. package/build/client/_app/immutable/chunks/CIZHDrUF.js.br +0 -0
  135. package/build/client/_app/immutable/chunks/CIZHDrUF.js.gz +0 -0
  136. package/build/client/_app/immutable/chunks/CnnMN4wM.js.br +0 -0
  137. package/build/client/_app/immutable/entry/app.BPBGxhbo.js.br +0 -0
  138. package/build/client/_app/immutable/entry/app.BPBGxhbo.js.gz +0 -0
  139. package/build/client/_app/immutable/entry/start.CTftTL5f.js +0 -1
  140. package/build/client/_app/immutable/entry/start.CTftTL5f.js.br +0 -0
  141. package/build/client/_app/immutable/entry/start.CTftTL5f.js.gz +0 -0
  142. package/build/client/_app/immutable/nodes/0.CquXHiCP.js.br +0 -0
  143. package/build/client/_app/immutable/nodes/0.CquXHiCP.js.gz +0 -0
  144. package/build/client/_app/immutable/nodes/1.xSi5hUiK.js.br +0 -1
  145. package/build/client/_app/immutable/nodes/1.xSi5hUiK.js.gz +0 -0
  146. package/build/client/_app/immutable/nodes/2.CgzHO6l0.js.br +0 -0
  147. package/build/client/_app/immutable/nodes/2.CgzHO6l0.js.gz +0 -0
  148. package/build/client/_app/immutable/nodes/3.B-HZ9QO3.js.br +0 -0
  149. package/build/client/_app/immutable/nodes/3.B-HZ9QO3.js.gz +0 -0
  150. package/build/client/_app/immutable/nodes/4._rTlKXDo.js.br +0 -0
  151. package/build/client/_app/immutable/nodes/4._rTlKXDo.js.gz +0 -0
  152. package/build/server/chunks/chunks/context.js-4TO_atTF.js.map +0 -1
  153. package/build/server/chunks/nodes/1.js-DpvNtg1z.js +0 -7
  154. package/build/server/chunks/nodes/3.js-XlL1_Sjt.js +0 -7
@@ -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>;
@@ -0,0 +1,153 @@
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 { assetIdError, buildDeleteTagScript, buildSetTagScript } from '../core/index.js';
9
+ import { fetchFigmaDocument, figmaDocumentNodes, validateFigmaSourceParameters } from './importers/figma.js';
10
+ import { LibraryStore } from './library-store.js';
11
+ /** An expected refusal that carries the file's current tags so the caller can pick another. */
12
+ export class FigmaTagConflictError extends Error {
13
+ existingTags;
14
+ constructor(message, existingTags) {
15
+ super(message);
16
+ this.existingTags = existingTags;
17
+ }
18
+ }
19
+ /** Normalize a `12:34` / `12-34` node id; Fundus imports only this id form. */
20
+ export function parseFigmaNodeId(value) {
21
+ if (!/^\d+[-:]\d+$/.test(value)) {
22
+ throw new Error(`"${value}" is not a taggable Figma node id — use the form 12:34 (or 12-34 as in a node-id link parameter). Nodes inside instances (ids starting with "I") inherit their tag from the main component; tag the node there.`);
23
+ }
24
+ return value.replace('-', ':');
25
+ }
26
+ function tagListing(nodes) {
27
+ return nodes
28
+ .filter((node) => node.tag !== null)
29
+ .map((node) => ({
30
+ tag: node.tag,
31
+ nodeId: node.nodeId,
32
+ nodeName: node.nodeName,
33
+ page: node.page
34
+ }))
35
+ .sort((a, b) => a.tag.localeCompare(b.tag));
36
+ }
37
+ function describeNode(node) {
38
+ return `"${node.nodeName}" (${node.nodeId}) on page "${node.page}"`;
39
+ }
40
+ /** The library asset imported from `tag` in `fileKey`, if any. */
41
+ function tagImportedAsset(library, fileKey, tag) {
42
+ const record = library.assets[tag];
43
+ if (record?.importSource?.importer !== 'figma')
44
+ return null;
45
+ const source = validateFigmaSourceParameters(record.importSource.parameters);
46
+ return source.mode === 'tag' && source.fundusTag === tag && source.fileKey === fileKey
47
+ ? tag
48
+ : null;
49
+ }
50
+ export function planSetFigmaTag(request, { library, document }) {
51
+ const { fileKey, nodeId, tag, force } = request;
52
+ const problem = assetIdError(tag);
53
+ if (problem)
54
+ throw new Error(problem);
55
+ const nodes = figmaDocumentNodes(document);
56
+ const existingTags = tagListing(nodes);
57
+ const warnings = [];
58
+ const holder = nodes.find((node) => node.tag === tag && node.nodeId !== nodeId);
59
+ if (holder) {
60
+ throw new FigmaTagConflictError(`Tag "${tag}" is already used by ${describeNode(holder)} — choose another tag (existing tags are listed).`, existingTags);
61
+ }
62
+ if (Object.hasOwn(library.assets, tag) && tagImportedAsset(library, fileKey, tag) === null) {
63
+ throw new FigmaTagConflictError(`A Fundus asset "${tag}" already exists and is not imported from this tag in file "${fileKey}" — \`fundus asset ingest --figma-tag ${tag}\` would collide; choose another tag.`, existingTags);
64
+ }
65
+ const node = nodes.find((candidate) => candidate.nodeId === nodeId) ?? null;
66
+ if (!node) {
67
+ warnings.push(`Node ${nodeId} is not in the Figma REST snapshot yet (new or unsynced node); the script verifies it exists before writing.`);
68
+ }
69
+ const previousTag = node?.tag ?? null;
70
+ if (previousTag === tag) {
71
+ if (!force) {
72
+ throw new Error(`Node ${describeNode(node)} already carries tag "${tag}" — nothing to do. Pass --force to regenerate the script anyway (e.g. to repair its annotation).`);
73
+ }
74
+ warnings.push(`Node ${nodeId} already carries tag "${tag}"; the script rewrites it.`);
75
+ }
76
+ else if (previousTag !== null) {
77
+ const orphaned = tagImportedAsset(library, fileKey, previousTag);
78
+ const followUp = orphaned
79
+ ? ` Fundus asset "${orphaned}" is imported from that tag — afterwards run \`fundus asset reimport ${orphaned} --figma-tag ${tag}\` (renames it to "${tag}").`
80
+ : '';
81
+ if (!force) {
82
+ throw new Error(`Node ${describeNode(node)} already carries tag "${previousTag}". Pass --force to replace it.${followUp}`);
83
+ }
84
+ warnings.push(`Replaces tag "${previousTag}" on node ${nodeId}.${followUp}`);
85
+ }
86
+ if (tagImportedAsset(library, fileKey, tag) !== null && !nodes.some((n) => n.tag === tag)) {
87
+ warnings.push(`Fundus asset "${tag}" is imported from this tag; its next \`fundus asset reimport ${tag}\` resolves to this node.`);
88
+ }
89
+ return {
90
+ fileKey,
91
+ nodeId,
92
+ tag,
93
+ previousTag,
94
+ warnings,
95
+ code: buildSetTagScript({ nodeId, tag, force })
96
+ };
97
+ }
98
+ export function planDeleteFigmaTag(request, { library, document }) {
99
+ const { fileKey, force } = request;
100
+ if (request.tag !== undefined) {
101
+ const problem = assetIdError(request.tag);
102
+ if (problem)
103
+ throw new Error(problem);
104
+ }
105
+ const nodes = figmaDocumentNodes(document);
106
+ const existingTags = tagListing(nodes);
107
+ let node;
108
+ if (request.nodeId !== undefined) {
109
+ const found = nodes.find((candidate) => candidate.nodeId === request.nodeId);
110
+ if (!found) {
111
+ throw new Error(`Node ${request.nodeId} is not in Figma file "${fileKey}".`);
112
+ }
113
+ if (found.tag === null) {
114
+ throw new FigmaTagConflictError(`Node ${describeNode(found)} carries no Fundus tag (existing tags are listed).`, existingTags);
115
+ }
116
+ if (request.tag !== undefined && found.tag !== request.tag) {
117
+ throw new FigmaTagConflictError(`Node ${describeNode(found)} carries tag "${found.tag}", not "${request.tag}".`, existingTags);
118
+ }
119
+ node = found;
120
+ }
121
+ else {
122
+ const matches = nodes.filter((candidate) => candidate.tag === request.tag);
123
+ if (matches.length === 0) {
124
+ throw new FigmaTagConflictError(`No node carries tag "${request.tag}" in Figma file "${fileKey}" (existing tags are listed).`, existingTags);
125
+ }
126
+ if (matches.length > 1) {
127
+ throw new FigmaTagConflictError(`Tag "${request.tag}" is on ${matches.length} nodes (${matches.map((match) => match.nodeId).join(', ')}) — pass --node-id to pick one.`, existingTags);
128
+ }
129
+ node = matches[0];
130
+ }
131
+ const tag = node.tag;
132
+ const warnings = [];
133
+ const imported = tagImportedAsset(library, fileKey, tag);
134
+ if (imported) {
135
+ const consequence = `Fundus asset "${imported}" is imported from this tag; without it, \`fundus asset reimport ${imported}\` fails until the tag is set again.`;
136
+ if (!force)
137
+ throw new Error(`${consequence} Pass --force to delete the tag anyway.`);
138
+ warnings.push(consequence);
139
+ }
140
+ return {
141
+ fileKey,
142
+ nodeId: node.nodeId,
143
+ tag,
144
+ warnings,
145
+ code: buildDeleteTagScript({ nodeId: node.nodeId, tag })
146
+ };
147
+ }
148
+ /** Load the library and the file's REST document for planning. */
149
+ export async function loadFigmaTagPlanContext(config, fileKey) {
150
+ const library = await new LibraryStore(config).read();
151
+ const document = await fetchFigmaDocument(fileKey, { config, fetch: globalThis.fetch });
152
+ return { library, document };
153
+ }
@@ -40,5 +40,22 @@ export declare function parseFigmaFileKey(value: string): string;
40
40
  export declare function validateFigmaSourceParameters(value: unknown): FigmaSourceParameters;
41
41
  export declare function resolveSafeDownloadTarget(value: string | URL): Promise<PinnedHttpsTarget>;
42
42
  export declare function assertSafeDownloadUrl(value: string | URL): Promise<URL>;
43
+ /** One node in a Figma REST document tree, as the tag commands report it. */
44
+ export interface FigmaDocumentNode {
45
+ nodeId: string;
46
+ nodeName: string;
47
+ /** Name of the page (canvas) containing the node. */
48
+ page: string;
49
+ /** The node's Fundus tag, or null when untagged. */
50
+ tag: string | null;
51
+ }
52
+ /**
53
+ * Every taggable node in a REST document tree (`plugin_data=shared`), in
54
+ * document order. Nodes inside instances are skipped: they echo the main
55
+ * component node's tag, and Fundus imports only plain `12:34` node ids.
56
+ */
57
+ export declare function figmaDocumentNodes(document: unknown): FigmaDocumentNode[];
58
+ /** The whole document tree of a Figma file, including every node's shared plugin data. */
59
+ export declare function fetchFigmaDocument(fileKey: string, context: ImporterContext): Promise<unknown>;
43
60
  export declare function resolveTagToNode(fileKey: string, tag: string, context: ImporterContext): Promise<string>;
44
61
  export declare const figmaSourceImporter: ServerSourceImporter<FigmaImportInput, FigmaSourceParameters>;
@@ -1,7 +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
+ import { isInstanceSublayerId, isValidAssetId } from '../../core/index.js';
5
5
  import { SourceImportError, invalidInput } from './contract.js';
6
6
  import { requestPinnedHttps } from './pinned-https.js';
7
7
  const MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024;
@@ -383,21 +383,43 @@ async function validatePng(bytes) {
383
383
  throw new SourceImportError('image-too-large', 'Figma export exceeds the 32 megapixel limit.');
384
384
  }
385
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
- }
386
+ /**
387
+ * Every taggable node in a REST document tree (`plugin_data=shared`), in
388
+ * document order. Nodes inside instances are skipped: they echo the main
389
+ * component node's tag, and Fundus imports only plain `12:34` node ids.
390
+ */
391
+ export function figmaDocumentNodes(document) {
392
+ const nodes = [];
393
+ const visit = (value, page) => {
394
+ const current = record(value);
395
+ if (!current)
396
+ return;
397
+ const name = typeof current.name === 'string' ? current.name : '';
398
+ const pageName = current.type === 'CANVAS' ? name : page;
399
+ if (typeof current.id === 'string' &&
400
+ current.type !== 'DOCUMENT' &&
401
+ current.type !== 'CANVAS' &&
402
+ !isInstanceSublayerId(current.id)) {
403
+ const shared = record(current.sharedPluginData);
404
+ const fundus = shared ? record(shared.fundus) : null;
405
+ const tag = fundus && typeof fundus.assetId === 'string' ? fundus.assetId : '';
406
+ nodes.push({
407
+ nodeId: current.id,
408
+ nodeName: name,
409
+ page: pageName,
410
+ tag: tag === '' ? null : tag
411
+ });
412
+ }
413
+ if (Array.isArray(current.children)) {
414
+ for (const child of current.children)
415
+ visit(child, pageName);
416
+ }
417
+ };
418
+ visit(record(document), '');
419
+ return nodes;
399
420
  }
400
- export async function resolveTagToNode(fileKey, tag, context) {
421
+ /** The whole document tree of a Figma file, including every node's shared plugin data. */
422
+ export async function fetchFigmaDocument(fileKey, context) {
401
423
  const token = context.config.import.figma.token;
402
424
  if (!token) {
403
425
  throw new SourceImportError('not-configured', 'Figma access is not configured.');
@@ -416,15 +438,18 @@ export async function resolveTagToNode(fileKey, tag, context) {
416
438
  if (!body || body.err || (typeof body.status === 'number' && body.status >= 400)) {
417
439
  throw new SourceImportError('invalid-response', 'Figma returned an invalid response.');
418
440
  }
419
- const matches = [];
420
- collectTaggedNodeIds(body.document, tag, matches);
441
+ return body.document;
442
+ }
443
+ export async function resolveTagToNode(fileKey, tag, context) {
444
+ const document = await fetchFigmaDocument(fileKey, context);
445
+ const matches = figmaDocumentNodes(document).filter((node) => node.tag === tag);
421
446
  if (matches.length === 0) {
422
447
  throw new SourceImportError('not-found', `No Figma node is tagged "${tag}" in this file.`);
423
448
  }
424
449
  if (matches.length > 1) {
425
450
  throw new SourceImportError('ambiguous-tag', `More than one Figma node is tagged "${tag}" in this file.`);
426
451
  }
427
- return matches[0];
452
+ return matches[0].nodeId;
428
453
  }
429
454
  async function materialize(source, context) {
430
455
  const token = context.config.import.figma.token;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fundus",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1 +0,0 @@
1
- import{c as e,r as t}from"../chunks/CCIxsMRK.js";export{e as load_css,t as start};
@@ -1 +0,0 @@
1
- w`�ۦ�M�uEE