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.
- package/README.md +21 -0
- package/build/client/_app/immutable/chunks/B_4qwt4o.js +1 -0
- package/build/client/_app/immutable/chunks/B_4qwt4o.js.br +0 -0
- package/build/client/_app/immutable/chunks/B_4qwt4o.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{C4w39LtR.js → DS1jTxvm.js} +1 -1
- package/build/client/_app/immutable/chunks/DS1jTxvm.js.br +2 -0
- package/build/client/_app/immutable/chunks/DS1jTxvm.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{WV4B0d_0.js → p2q3f3Ml.js} +1 -1
- package/build/client/_app/immutable/chunks/p2q3f3Ml.js.br +0 -0
- package/build/client/_app/immutable/chunks/p2q3f3Ml.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.GgnXYpzJ.js → app.DgW6Eg19.js} +2 -2
- package/build/client/_app/immutable/entry/app.DgW6Eg19.js.br +0 -0
- package/build/client/_app/immutable/entry/app.DgW6Eg19.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.D3J5OM6w.js +1 -0
- package/build/client/_app/immutable/entry/start.D3J5OM6w.js.br +1 -0
- package/build/client/_app/immutable/entry/start.D3J5OM6w.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.D1g1plhI.js → 0.DdcKoZu5.js} +1 -1
- package/build/client/_app/immutable/nodes/0.DdcKoZu5.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.DdcKoZu5.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.DzAnL2O6.js → 1.CoM7ng4c.js} +1 -1
- package/build/client/_app/immutable/nodes/1.CoM7ng4c.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.CoM7ng4c.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{2.BMN1YTJ4.js → 2.CV-vWxFI.js} +1 -1
- package/build/client/_app/immutable/nodes/2.CV-vWxFI.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.CV-vWxFI.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{3.DL_TiE5x.js → 3.B9mDsYGS.js} +1 -1
- package/build/client/_app/immutable/nodes/3.B9mDsYGS.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.B9mDsYGS.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{4.D7FHmbgq.js → 4.CFfIs-6D.js} +1 -1
- package/build/client/_app/immutable/nodes/4.CFfIs-6D.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.CFfIs-6D.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/handler.js +7 -7
- package/build/index.js +7 -7
- package/build/server/chunks/chunks/{context.js-4TO_atTF.js → context.js-mE8mFnsg.js} +45 -12
- package/build/server/chunks/chunks/context.js-mE8mFnsg.js.map +1 -0
- package/build/server/chunks/chunks/{exports.js-BAfR8rsb.js → exports.js-CDl9URFG.js} +2 -2
- package/build/server/chunks/chunks/{exports.js-BAfR8rsb.js.map → exports.js-CDl9URFG.js.map} +1 -1
- package/build/server/chunks/chunks/{internal.js-B3hOADoz.js → internal.js-Bz3sw0lq.js} +3 -3
- package/build/server/chunks/chunks/{internal.js-B3hOADoz.js.map → internal.js-Bz3sw0lq.js.map} +1 -1
- package/build/server/chunks/chunks/internal2.js-BlXt2rfE.js.map +1 -1
- package/build/server/chunks/chunks/{shared.js-DN477dAr.js → shared.js-DwJl8ZNw.js} +2 -2
- package/build/server/chunks/chunks/{shared.js-DN477dAr.js.map → shared.js-DwJl8ZNw.js.map} +1 -1
- package/build/server/chunks/chunks/{utils.js-zf3Q16Eh.js → utils.js-DoU5Jolh.js} +3 -3
- package/build/server/chunks/chunks/{utils.js-zf3Q16Eh.js.map → utils.js-DoU5Jolh.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-Aa7zYzUB.js → _server.ts.js-CM6PedpD.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-Aa7zYzUB.js.map → _server.ts.js-CM6PedpD.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-CM73kt1H.js → _server.ts.js-BpfOlmKN.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-CM73kt1H.js.map → _server.ts.js-BpfOlmKN.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-CyiEsjc3.js → _server.ts.js-UvDSeD5O.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-CyiEsjc3.js.map → _server.ts.js-UvDSeD5O.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/_id_/retag/{_server.ts.js-Cpt_vVY0.js → _server.ts.js-D4kq3W1-.js} +2 -2
- 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
- package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-BVdeCRy0.js → _server.ts.js-CcV2BTCH.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-BVdeCRy0.js.map → _server.ts.js-CcV2BTCH.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-QEa0nYZE.js → _server.ts.js-Cb0Dn_a2.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-QEa0nYZE.js.map → _server.ts.js-Cb0Dn_a2.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-Dk0sFdoM.js → _server.ts.js-ChlfFRqR.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-Dk0sFdoM.js.map → _server.ts.js-ChlfFRqR.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/events/{_server.ts.js-CO8RZm3e.js → _server.ts.js-DnrgWGff.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/events/{_server.ts.js-CO8RZm3e.js.map → _server.ts.js-DnrgWGff.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-8RzyUK6g.js → _server.ts.js-CFM83bne.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-8RzyUK6g.js.map → _server.ts.js-CFM83bne.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-YWliM590.js → _server.ts.js-BtJIovEG.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-YWliM590.js.map → _server.ts.js-BtJIovEG.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-CeXmOrwN.js → _server.ts.js-BAHE77r2.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-CeXmOrwN.js.map → _server.ts.js-BAHE77r2.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-DQaLSdGl.js → _server.ts.js-Cypw-CjT.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-DQaLSdGl.js.map → _server.ts.js-Cypw-CjT.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-BE_nYFP_.js → _server.ts.js-FrntF4Gn.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-BE_nYFP_.js.map → _server.ts.js-FrntF4Gn.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-BGxvZ4EV.js → _server.ts.js-BeEHDBep.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-BGxvZ4EV.js.map → _server.ts.js-BeEHDBep.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-Cvwr9a0l.js → _server.ts.js-Di1kfrl2.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-Cvwr9a0l.js.map → _server.ts.js-Di1kfrl2.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-DEMDXHsR.js → _server.ts.js-CrLHlnap.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-DEMDXHsR.js.map → _server.ts.js-CrLHlnap.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-DoeEIUoG.js → _server.ts.js-BcqKQTDk.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-DoeEIUoG.js.map → _server.ts.js-BcqKQTDk.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-0hMtYoB1.js → _server.ts.js-oFUf-T_H.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-0hMtYoB1.js.map → _server.ts.js-oFUf-T_H.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-B4YWmlkC.js → _server.ts.js-D46dsJCB.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-B4YWmlkC.js.map → _server.ts.js-D46dsJCB.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/usage/{_server.ts.js-dNLtLO2U.js → _server.ts.js-BlKjsU-v.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/usage/{_server.ts.js-dNLtLO2U.js.map → _server.ts.js-BlKjsU-v.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-BtCkf-hk.js → _server.ts.js-DafDgMis.js} +2 -2
- package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-BtCkf-hk.js.map → _server.ts.js-DafDgMis.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-CozrPtRv.js → _server.ts.js-6Vn2UuF6.js} +2 -2
- package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-CozrPtRv.js.map → _server.ts.js-6Vn2UuF6.js.map} +1 -1
- package/build/server/chunks/entries/{hooks.server.js-Cnsi5KsE.js → hooks.server.js-CSypLuCa.js} +2 -2
- package/build/server/chunks/entries/{hooks.server.js-Cnsi5KsE.js.map → hooks.server.js-CSypLuCa.js.map} +1 -1
- package/build/server/chunks/{handler-BN27c-Da.js → handler-C0NXVVDu.js} +5 -5
- package/build/server/chunks/{handler-BN27c-Da.js.map → handler-C0NXVVDu.js.map} +1 -1
- package/build/server/chunks/{index.js-C1f-Qrf0.js → index.js-D3rzqgKi.js} +5 -5
- package/build/server/chunks/{index.js-C1f-Qrf0.js.map → index.js-D3rzqgKi.js.map} +1 -1
- package/build/server/chunks/{manifest.js-CFkuYTzu.js → manifest.js-Dw3Fvt_0.js} +29 -29
- package/build/server/chunks/{manifest.js-CFkuYTzu.js.map → manifest.js-Dw3Fvt_0.js.map} +1 -1
- package/build/server/chunks/nodes/{0.js-B1i6yC_e.js → 0.js-D1uRRjFL.js} +2 -2
- package/build/server/chunks/nodes/{0.js-B1i6yC_e.js.map → 0.js-D1uRRjFL.js.map} +1 -1
- package/build/server/chunks/nodes/1.js-sYnc9kZu.js +7 -0
- package/build/server/chunks/nodes/{1.js-Bv64OO0t.js.map → 1.js-sYnc9kZu.js.map} +1 -1
- package/build/server/chunks/nodes/{2.js-DivaYfS1.js → 2.js-B3OCWo8p.js} +2 -2
- package/build/server/chunks/nodes/{2.js-DivaYfS1.js.map → 2.js-B3OCWo8p.js.map} +1 -1
- package/build/server/chunks/nodes/3.js-BWkr9ibc.js +7 -0
- package/build/server/chunks/nodes/{3.js-CUAbHOxb.js.map → 3.js-BWkr9ibc.js.map} +1 -1
- package/build/server/chunks/nodes/{4.js-Ceg0ylPI.js → 4.js-gujLHNff.js} +2 -2
- package/build/server/chunks/nodes/{4.js-Ceg0ylPI.js.map → 4.js-gujLHNff.js.map} +1 -1
- package/dist/cli.js +9 -0
- package/dist/core/figma-tag-script.d.ts +141 -0
- package/dist/core/figma-tag-script.js +236 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +1 -0
- package/dist/server/cli-commands.d.ts +6 -0
- package/dist/server/cli-commands.js +1 -1
- package/dist/server/cli-figma-tags.d.ts +2 -0
- package/dist/server/cli-figma-tags.js +222 -0
- package/dist/server/figma-tags.d.ts +61 -0
- package/dist/server/figma-tags.js +153 -0
- package/dist/server/importers/figma.d.ts +17 -0
- package/dist/server/importers/figma.js +43 -18
- package/package.json +1 -1
- package/build/client/_app/immutable/chunks/C4w39LtR.js.br +0 -1
- package/build/client/_app/immutable/chunks/C4w39LtR.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DGy-bMvO.js +0 -1
- package/build/client/_app/immutable/chunks/DGy-bMvO.js.br +0 -0
- package/build/client/_app/immutable/chunks/DGy-bMvO.js.gz +0 -0
- package/build/client/_app/immutable/chunks/WV4B0d_0.js.br +0 -0
- package/build/client/_app/immutable/chunks/WV4B0d_0.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.GgnXYpzJ.js.br +0 -0
- package/build/client/_app/immutable/entry/app.GgnXYpzJ.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.O9RwuDjf.js +0 -1
- package/build/client/_app/immutable/entry/start.O9RwuDjf.js.br +0 -0
- package/build/client/_app/immutable/entry/start.O9RwuDjf.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.D1g1plhI.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.D1g1plhI.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.DzAnL2O6.js.br +0 -2
- package/build/client/_app/immutable/nodes/1.DzAnL2O6.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.BMN1YTJ4.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.BMN1YTJ4.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.DL_TiE5x.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.DL_TiE5x.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.D7FHmbgq.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.D7FHmbgq.js.gz +0 -0
- package/build/server/chunks/chunks/context.js-4TO_atTF.js.map +0 -1
- package/build/server/chunks/nodes/1.js-Bv64OO0t.js +0 -7
- 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
|
+
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/core/index.js
CHANGED
|
@@ -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>;
|