figma-local 1.9.0 → 2.0.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/package.json +1 -1
- package/skills/figma-library/SKILL.md +63 -80
- package/src/index.js +287 -66
- package/src/.figma-client-1774014578401.9038.mjs +0 -4198
package/package.json
CHANGED
|
@@ -11,162 +11,145 @@ allowed-tools:
|
|
|
11
11
|
|
|
12
12
|
# Figma Library
|
|
13
13
|
|
|
14
|
-
Access team library components and variables from other Figma files. Index libraries, search components by name, import by key, and browse design tokens.
|
|
14
|
+
Access team library components and variables from other Figma files. Index libraries via REST API or plugin, search components by name, import by key, and browse design tokens.
|
|
15
15
|
|
|
16
16
|
## Prerequisites
|
|
17
17
|
|
|
18
18
|
- The `fig` CLI must be connected: `fig daemon status`. If not: `fig connect --safe`.
|
|
19
19
|
- The Figma plugin must have `teamlibrary` permission in its manifest. If library commands fail with a permission error, re-import the plugin in Figma (Plugins → Development → Import from manifest).
|
|
20
|
-
- Libraries must be enabled in the current Figma file (check Assets panel → team library icon).
|
|
21
20
|
|
|
22
21
|
## IMPORTANT: How to access library components
|
|
23
22
|
|
|
24
|
-
Figma's plugin API does **not** allow browsing library components directly. To work with library components, you must **index** them first:
|
|
23
|
+
Figma's plugin API does **not** allow browsing library components directly. To work with library components, you must **index** them first. There are three ways to index:
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
2. Run `fig library index` — this scans all pages and saves every component with its key
|
|
28
|
-
3. Switch to your **working file**
|
|
29
|
-
4. Run `fig library search --name "button"` — finds components from indexed libraries
|
|
30
|
-
5. Run `fig library import --key "<key>"` — imports the component
|
|
25
|
+
### Option A: REST API (recommended for large files)
|
|
31
26
|
|
|
32
|
-
|
|
27
|
+
No plugin needed. Works on any file size without hanging Figma.
|
|
33
28
|
|
|
34
|
-
|
|
29
|
+
**First time — provide token and file URL:**
|
|
30
|
+
```bash
|
|
31
|
+
fig library index --api --token "figd_xxxxx" --file "https://www.figma.com/design/ABC123/MyFile"
|
|
32
|
+
```
|
|
35
33
|
|
|
36
|
-
|
|
34
|
+
The token is saved for future use. Get one from: Figma → Settings → Personal Access Tokens.
|
|
37
35
|
|
|
36
|
+
**After first time — just provide the file URL:**
|
|
38
37
|
```bash
|
|
39
|
-
fig library index
|
|
38
|
+
fig library index --api --file "https://www.figma.com/design/ABC123/MyFile"
|
|
40
39
|
```
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
### Option B: Page-by-page (for large files via plugin)
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
### Search indexed libraries
|
|
47
|
-
|
|
48
|
-
Search across all indexed libraries by component name:
|
|
43
|
+
Open the library file in Figma, then scan one page at a time:
|
|
49
44
|
|
|
50
45
|
```bash
|
|
51
|
-
fig library
|
|
52
|
-
fig library
|
|
53
|
-
fig library
|
|
54
|
-
fig library search --name "checkbox"
|
|
46
|
+
fig library index --page "Buttons"
|
|
47
|
+
fig library index --page "Inputs"
|
|
48
|
+
fig library index --page "Cards"
|
|
55
49
|
```
|
|
56
50
|
|
|
57
|
-
|
|
51
|
+
Each page's components are merged into the same index file. This avoids hanging on large files.
|
|
52
|
+
|
|
53
|
+
### Option C: Full scan (small files only)
|
|
58
54
|
|
|
59
|
-
|
|
55
|
+
Open the library file in Figma, then:
|
|
60
56
|
|
|
61
57
|
```bash
|
|
62
|
-
fig library
|
|
58
|
+
fig library index
|
|
63
59
|
```
|
|
64
60
|
|
|
65
|
-
|
|
61
|
+
**Warning:** This scans ALL pages at once. Only use on small files — large design systems will cause Figma to hang.
|
|
66
62
|
|
|
67
|
-
|
|
63
|
+
## After indexing: Search and Import
|
|
64
|
+
|
|
65
|
+
### Search indexed libraries
|
|
68
66
|
|
|
69
67
|
```bash
|
|
70
|
-
fig library
|
|
68
|
+
fig library search --name "button"
|
|
69
|
+
fig library search --name "input"
|
|
70
|
+
fig library search --name "card"
|
|
71
|
+
fig library search --name "checkbox"
|
|
71
72
|
```
|
|
72
73
|
|
|
73
|
-
Returns:
|
|
74
|
-
|
|
75
|
-
### Import a component by key
|
|
74
|
+
Returns: component name, key (for importing), component set, page, and library name.
|
|
76
75
|
|
|
77
|
-
|
|
76
|
+
### List indexed libraries
|
|
78
77
|
|
|
79
78
|
```bash
|
|
80
|
-
fig library
|
|
79
|
+
fig library list
|
|
81
80
|
```
|
|
82
81
|
|
|
83
|
-
Import
|
|
82
|
+
### Import by key
|
|
84
83
|
|
|
85
84
|
```bash
|
|
86
|
-
fig library import --key "
|
|
85
|
+
fig library import --key "<key-from-search>"
|
|
86
|
+
fig library import --key "<key>" --name "PrimaryButton"
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
The
|
|
89
|
+
The component is placed at viewport center and selected.
|
|
90
90
|
|
|
91
|
-
###
|
|
92
|
-
|
|
93
|
-
See what variable collections are available from linked libraries:
|
|
91
|
+
### Inspect the imported component
|
|
94
92
|
|
|
95
93
|
```bash
|
|
96
|
-
fig
|
|
94
|
+
fig inspect --deep
|
|
97
95
|
```
|
|
98
96
|
|
|
99
|
-
|
|
97
|
+
## Variables (no indexing needed)
|
|
100
98
|
|
|
101
|
-
|
|
99
|
+
Library variables are available directly via the plugin API:
|
|
102
100
|
|
|
103
101
|
```bash
|
|
104
|
-
fig library
|
|
105
|
-
fig library variables
|
|
106
|
-
fig library variables --name "
|
|
102
|
+
fig library collections # List variable collections
|
|
103
|
+
fig library variables # List all variables
|
|
104
|
+
fig library variables --name "color" # Search by name
|
|
107
105
|
```
|
|
108
106
|
|
|
109
|
-
|
|
107
|
+
## Components on current page
|
|
110
108
|
|
|
111
|
-
Find library components
|
|
109
|
+
Find library components already dragged onto the current page:
|
|
112
110
|
|
|
113
111
|
```bash
|
|
114
112
|
fig library components
|
|
115
113
|
fig library components --name "button"
|
|
116
114
|
```
|
|
117
115
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
All commands support `--json` for structured output.
|
|
116
|
+
## Full workflow
|
|
121
117
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
1. **Index** the library (one-time, in the library file):
|
|
118
|
+
1. **Index** the library (pick one method):
|
|
125
119
|
```bash
|
|
126
|
-
|
|
120
|
+
# Best for large files:
|
|
121
|
+
fig library index --api --token "figd_..." --file "https://..."
|
|
122
|
+
# Or page by page:
|
|
123
|
+
fig library index --page "Buttons"
|
|
127
124
|
```
|
|
128
125
|
|
|
129
|
-
2. **
|
|
130
|
-
|
|
131
|
-
3. **Search** for the components you need:
|
|
126
|
+
2. **Search** for components:
|
|
132
127
|
```bash
|
|
133
|
-
fig library search --name "button"
|
|
134
|
-
fig library search --name "input"
|
|
128
|
+
fig library search --name "button" --json
|
|
135
129
|
```
|
|
136
130
|
|
|
137
|
-
|
|
131
|
+
3. **Import** into your working file:
|
|
138
132
|
```bash
|
|
139
|
-
fig library import --key "<key
|
|
133
|
+
fig library import --key "<key>"
|
|
140
134
|
```
|
|
141
135
|
|
|
142
|
-
|
|
136
|
+
4. **Inspect** for full specs:
|
|
143
137
|
```bash
|
|
144
138
|
fig inspect --deep
|
|
145
139
|
```
|
|
146
140
|
|
|
147
|
-
|
|
141
|
+
5. **Get variables** for tokens:
|
|
148
142
|
```bash
|
|
149
143
|
fig library variables --name "primary" --json
|
|
150
144
|
```
|
|
151
145
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
## Workflow: Extracting a full design system
|
|
155
|
-
|
|
156
|
-
1. Open the design system file → `fig library index`
|
|
157
|
-
2. Get all components: `fig library search --name "" --json`
|
|
158
|
-
3. Get all variables: `fig library variables --json`
|
|
159
|
-
4. Import key components one by one and document them:
|
|
160
|
-
```bash
|
|
161
|
-
fig library import --key "<key>"
|
|
162
|
-
fig document --json
|
|
163
|
-
```
|
|
146
|
+
6. **Replicate** in code.
|
|
164
147
|
|
|
165
148
|
## Tips
|
|
166
149
|
|
|
167
|
-
- **
|
|
168
|
-
-
|
|
169
|
-
-
|
|
170
|
-
-
|
|
150
|
+
- **REST API is fastest** for large design systems — no file opening needed.
|
|
151
|
+
- Token is saved after first use in `~/.figma-local/figma-token`.
|
|
152
|
+
- Page-by-page indexing merges results — run multiple times to build up the index.
|
|
153
|
+
- Re-index when the library is updated to get new components.
|
|
154
|
+
- `fig library search --name "" --json` returns ALL indexed components.
|
|
171
155
|
- Component keys are stable across file versions — save them for repeated imports.
|
|
172
|
-
- Use `fig library search --name "button" --json | jq '.[].key'` to extract just the keys.
|
package/src/index.js
CHANGED
|
@@ -10581,27 +10581,34 @@ program
|
|
|
10581
10581
|
.option('--key <key>', 'Component key for importing')
|
|
10582
10582
|
.option('--name <name>', 'Filter by name (partial match)')
|
|
10583
10583
|
.option('--json', 'Output raw JSON')
|
|
10584
|
+
.option('--api', 'Use Figma REST API instead of plugin (for index)')
|
|
10585
|
+
.option('--token <token>', 'Figma personal access token (for --api)')
|
|
10586
|
+
.option('--file <url>', 'Figma file URL (for --api)')
|
|
10587
|
+
.option('--page <pageName>', 'Index only a specific page (for index)')
|
|
10584
10588
|
.addHelpText('after', `
|
|
10585
10589
|
Actions:
|
|
10586
10590
|
collections List all available library variable collections
|
|
10587
10591
|
variables List variables from a library collection (use --name to filter)
|
|
10588
10592
|
components List available library components on current page (use --name to filter)
|
|
10589
|
-
index Scan
|
|
10593
|
+
index Scan and save all components to a local index
|
|
10590
10594
|
search Search indexed libraries for components (use --name to filter)
|
|
10591
10595
|
import Import a component by key (use --key)
|
|
10592
10596
|
list List all indexed libraries
|
|
10593
10597
|
|
|
10598
|
+
Index modes:
|
|
10599
|
+
fig library index Scan current file via plugin (small files)
|
|
10600
|
+
fig library index --page "Buttons" Scan only one page (large files)
|
|
10601
|
+
fig library index --api --token "figd_..." --file "URL" Use REST API (best for large files)
|
|
10602
|
+
fig library index --api --file "URL" Use saved token
|
|
10603
|
+
|
|
10594
10604
|
Examples:
|
|
10595
10605
|
fig library collections List all library variable collections
|
|
10596
|
-
fig library variables List all library variables
|
|
10597
10606
|
fig library variables --name "color" List variables matching "color"
|
|
10598
|
-
fig library components List available library components
|
|
10599
10607
|
fig library components --name "button" Find button components
|
|
10600
|
-
fig library index
|
|
10608
|
+
fig library index --api --token "figd_..." --file "https://www.figma.com/design/ABC/..."
|
|
10601
10609
|
fig library search --name "button" Search indexed libraries for "button"
|
|
10602
10610
|
fig library list List all indexed libraries
|
|
10603
10611
|
fig library import --key "abc123..." Import a component by its key
|
|
10604
|
-
fig library import --key "abc123..." --name "MyButton" Import and rename
|
|
10605
10612
|
`)
|
|
10606
10613
|
.action(async (action, options) => {
|
|
10607
10614
|
checkConnection();
|
|
@@ -10794,13 +10801,214 @@ Examples:
|
|
|
10794
10801
|
console.log(` ${chalk.gray('id:')} ${result.id}`);
|
|
10795
10802
|
console.log(chalk.gray(' Component is now selected on the canvas.'));
|
|
10796
10803
|
} else if (action === 'index') {
|
|
10797
|
-
|
|
10798
|
-
|
|
10804
|
+
// Helper to save index data and print summary
|
|
10805
|
+
function saveIndexAndPrint(result, opts) {
|
|
10806
|
+
const libDir = join(homedir(), '.figma-local', 'libraries');
|
|
10807
|
+
mkdirSync(libDir, { recursive: true });
|
|
10808
|
+
const safeName = result.fileName.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
|
|
10809
|
+
const libFile = join(libDir, `${safeName}.json`);
|
|
10810
|
+
|
|
10811
|
+
// If file exists and we're doing page-by-page, merge
|
|
10812
|
+
let existingComponents = [];
|
|
10813
|
+
let existingPages = new Set();
|
|
10814
|
+
if (opts.page && existsSync(libFile)) {
|
|
10815
|
+
try {
|
|
10816
|
+
const existing = JSON.parse(readFileSync(libFile, 'utf8'));
|
|
10817
|
+
// Keep components from OTHER pages
|
|
10818
|
+
existingComponents = (existing.components || []).filter(c => c.page !== opts.page);
|
|
10819
|
+
existing.components.forEach(c => existingPages.add(c.page));
|
|
10820
|
+
} catch (e) { /* fresh start */ }
|
|
10821
|
+
}
|
|
10822
|
+
const mergedComponents = [...existingComponents, ...result.components];
|
|
10823
|
+
result.components.forEach(c => existingPages.add(c.page));
|
|
10824
|
+
|
|
10825
|
+
const indexData = {
|
|
10826
|
+
fileName: result.fileName,
|
|
10827
|
+
fileKey: result.fileKey || '',
|
|
10828
|
+
indexedAt: new Date().toISOString(),
|
|
10829
|
+
pageCount: opts.page ? existingPages.size : result.pageCount,
|
|
10830
|
+
componentCount: mergedComponents.length,
|
|
10831
|
+
components: mergedComponents
|
|
10832
|
+
};
|
|
10833
|
+
writeFileSync(libFile, JSON.stringify(indexData, null, 2));
|
|
10834
|
+
return { indexData, libFile };
|
|
10835
|
+
}
|
|
10836
|
+
|
|
10837
|
+
function printIndexSummary(indexData, libFile, opts) {
|
|
10838
|
+
console.log(` ${chalk.gray('Saved to:')} ${libFile}`);
|
|
10839
|
+
if (opts.json) {
|
|
10840
|
+
console.log(JSON.stringify(indexData, null, 2));
|
|
10841
|
+
} else {
|
|
10842
|
+
const sets = {};
|
|
10843
|
+
const standalone = [];
|
|
10844
|
+
for (const c of indexData.components) {
|
|
10845
|
+
if (c.componentSet) {
|
|
10846
|
+
if (!sets[c.componentSet]) sets[c.componentSet] = [];
|
|
10847
|
+
sets[c.componentSet].push(c);
|
|
10848
|
+
} else {
|
|
10849
|
+
standalone.push(c);
|
|
10850
|
+
}
|
|
10851
|
+
}
|
|
10852
|
+
if (Object.keys(sets).length > 0) {
|
|
10853
|
+
console.log(chalk.cyan('\n Component Sets:'));
|
|
10854
|
+
for (const [setName, variants] of Object.entries(sets)) {
|
|
10855
|
+
console.log(` ${chalk.white(setName)} ${chalk.gray(`(${variants.length} variant${variants.length !== 1 ? 's' : ''})`)}`);
|
|
10856
|
+
}
|
|
10857
|
+
}
|
|
10858
|
+
if (standalone.length > 0) {
|
|
10859
|
+
console.log(chalk.cyan('\n Standalone Components:'));
|
|
10860
|
+
for (const c of standalone.slice(0, 50)) {
|
|
10861
|
+
console.log(` ${chalk.white(c.name)} ${chalk.gray(`[${c.page}]`)}`);
|
|
10862
|
+
}
|
|
10863
|
+
if (standalone.length > 50) {
|
|
10864
|
+
console.log(chalk.gray(` ... and ${standalone.length - 50} more`));
|
|
10865
|
+
}
|
|
10866
|
+
}
|
|
10867
|
+
}
|
|
10868
|
+
}
|
|
10869
|
+
|
|
10870
|
+
// ---- REST API mode ----
|
|
10871
|
+
if (options.api) {
|
|
10872
|
+
// Get or save token
|
|
10873
|
+
const configDir = join(homedir(), '.figma-local');
|
|
10874
|
+
mkdirSync(configDir, { recursive: true });
|
|
10875
|
+
const tokenFile = join(configDir, 'figma-token');
|
|
10876
|
+
let token = options.token || '';
|
|
10877
|
+
if (!token && existsSync(tokenFile)) {
|
|
10878
|
+
token = readFileSync(tokenFile, 'utf8').trim();
|
|
10879
|
+
}
|
|
10880
|
+
if (!token) {
|
|
10881
|
+
spinner.fail('Figma token required. Get one from: Figma → Settings → Personal Access Tokens\nThen run: fig library index --api --token "figd_..." --file "URL"');
|
|
10882
|
+
process.exit(1);
|
|
10883
|
+
}
|
|
10884
|
+
// Save token for future use
|
|
10885
|
+
if (options.token) {
|
|
10886
|
+
writeFileSync(tokenFile, token);
|
|
10887
|
+
}
|
|
10888
|
+
|
|
10889
|
+
if (!options.file) {
|
|
10890
|
+
spinner.fail('--file is required with --api. Provide a Figma file URL.\nExample: fig library index --api --file "https://www.figma.com/design/ABC123/MyFile"');
|
|
10891
|
+
process.exit(1);
|
|
10892
|
+
}
|
|
10893
|
+
|
|
10894
|
+
// Extract file key from URL
|
|
10895
|
+
const fileKeyMatch = options.file.match(/(?:file|design)\/([a-zA-Z0-9]+)/);
|
|
10896
|
+
if (!fileKeyMatch) {
|
|
10897
|
+
spinner.fail('Could not extract file key from URL. Use a URL like: https://www.figma.com/design/ABC123/MyFile');
|
|
10898
|
+
process.exit(1);
|
|
10899
|
+
}
|
|
10900
|
+
const fileKey = fileKeyMatch[1];
|
|
10901
|
+
|
|
10902
|
+
spinner.text = `Fetching file metadata from Figma API...`;
|
|
10903
|
+
|
|
10904
|
+
// Fetch the file from the REST API
|
|
10905
|
+
const https = await import('https');
|
|
10906
|
+
function figmaApiGet(endpoint) {
|
|
10907
|
+
return new Promise((resolve, reject) => {
|
|
10908
|
+
const url = `https://api.figma.com/v1${endpoint}`;
|
|
10909
|
+
const req = https.get(url, { headers: { 'X-Figma-Token': token } }, (res) => {
|
|
10910
|
+
let data = '';
|
|
10911
|
+
res.on('data', chunk => data += chunk);
|
|
10912
|
+
res.on('end', () => {
|
|
10913
|
+
if (res.statusCode === 403) {
|
|
10914
|
+
reject(new Error('Access denied. Check your token has read access to this file.'));
|
|
10915
|
+
} else if (res.statusCode === 404) {
|
|
10916
|
+
reject(new Error('File not found. Check the URL is correct.'));
|
|
10917
|
+
} else if (res.statusCode !== 200) {
|
|
10918
|
+
reject(new Error(`Figma API returned ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
10919
|
+
} else {
|
|
10920
|
+
try { resolve(JSON.parse(data)); }
|
|
10921
|
+
catch (e) { reject(new Error('Invalid JSON response from Figma API')); }
|
|
10922
|
+
}
|
|
10923
|
+
});
|
|
10924
|
+
});
|
|
10925
|
+
req.on('error', reject);
|
|
10926
|
+
});
|
|
10927
|
+
}
|
|
10928
|
+
|
|
10799
10929
|
try {
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
|
|
10930
|
+
// Get file components via the components endpoint (lightweight)
|
|
10931
|
+
spinner.text = 'Fetching components from Figma API...';
|
|
10932
|
+
const compData = await figmaApiGet(`/files/${fileKey}/components`);
|
|
10933
|
+
|
|
10934
|
+
const components = [];
|
|
10935
|
+
if (compData.meta && compData.meta.components) {
|
|
10936
|
+
for (const comp of compData.meta.components) {
|
|
10937
|
+
const c = {
|
|
10938
|
+
name: comp.name,
|
|
10939
|
+
key: comp.key,
|
|
10940
|
+
id: comp.node_id,
|
|
10941
|
+
page: comp.containing_frame ? comp.containing_frame.pageName || '' : '',
|
|
10942
|
+
description: comp.description || '',
|
|
10943
|
+
};
|
|
10944
|
+
if (comp.containing_frame && comp.containing_frame.name) {
|
|
10945
|
+
// The containing_frame for component set variants shows the set name
|
|
10946
|
+
c.componentSet = comp.containing_frame.name !== comp.name ? comp.containing_frame.name : undefined;
|
|
10947
|
+
}
|
|
10948
|
+
components.push(c);
|
|
10949
|
+
}
|
|
10950
|
+
}
|
|
10951
|
+
|
|
10952
|
+
// Also get component sets
|
|
10953
|
+
spinner.text = 'Fetching component sets from Figma API...';
|
|
10954
|
+
try {
|
|
10955
|
+
const setsData = await figmaApiGet(`/files/${fileKey}/component_sets`);
|
|
10956
|
+
if (setsData.meta && setsData.meta.component_sets) {
|
|
10957
|
+
for (const set of setsData.meta.component_sets) {
|
|
10958
|
+
// Mark components that belong to this set
|
|
10959
|
+
for (const comp of components) {
|
|
10960
|
+
if (comp.id && set.node_id && comp.componentSet === undefined) {
|
|
10961
|
+
// Check if node is child of this set by matching containing_frame
|
|
10962
|
+
}
|
|
10963
|
+
}
|
|
10964
|
+
}
|
|
10965
|
+
}
|
|
10966
|
+
} catch (e) { /* component_sets endpoint may not exist for all plans */ }
|
|
10967
|
+
|
|
10968
|
+
// Get file name
|
|
10969
|
+
spinner.text = 'Fetching file info...';
|
|
10970
|
+
let fileName = fileKey;
|
|
10971
|
+
try {
|
|
10972
|
+
const fileInfo = await figmaApiGet(`/files/${fileKey}?depth=1`);
|
|
10973
|
+
if (fileInfo.name) fileName = fileInfo.name;
|
|
10974
|
+
} catch (e) { /* use fileKey as fallback */ }
|
|
10975
|
+
|
|
10976
|
+
const result = {
|
|
10977
|
+
fileName,
|
|
10978
|
+
fileKey,
|
|
10979
|
+
pageCount: 0,
|
|
10980
|
+
componentCount: components.length,
|
|
10981
|
+
components
|
|
10982
|
+
};
|
|
10983
|
+
|
|
10984
|
+
const { indexData, libFile } = saveIndexAndPrint(result, options);
|
|
10985
|
+
spinner.succeed(`Indexed ${components.length} components from "${fileName}" via API`);
|
|
10986
|
+
printIndexSummary(indexData, libFile, options);
|
|
10987
|
+
|
|
10988
|
+
} catch (e) {
|
|
10989
|
+
spinner.fail(`Figma API error: ${e.message}`);
|
|
10990
|
+
process.exit(1);
|
|
10991
|
+
}
|
|
10992
|
+
|
|
10993
|
+
// ---- Plugin mode with --page filter ----
|
|
10994
|
+
} else if (options.page) {
|
|
10995
|
+
const pageName = options.page;
|
|
10996
|
+
spinner.text = `Scanning page "${pageName}" for components...`;
|
|
10997
|
+
const safePageName = pageName.replace(/'/g, "\\'");
|
|
10998
|
+
const code = `(function() {
|
|
10999
|
+
try {
|
|
11000
|
+
var components = [];
|
|
11001
|
+
var targetPage = null;
|
|
11002
|
+
var pages = figma.root.children;
|
|
11003
|
+
for (var p = 0; p < pages.length; p++) {
|
|
11004
|
+
if (pages[p].name === '${safePageName}' || pages[p].name.toLowerCase().indexOf('${safePageName.toLowerCase()}') !== -1) {
|
|
11005
|
+
targetPage = pages[p];
|
|
11006
|
+
break;
|
|
11007
|
+
}
|
|
11008
|
+
}
|
|
11009
|
+
if (!targetPage) {
|
|
11010
|
+
return { error: 'Page "${safePageName}" not found. Available pages: ' + pages.map(function(p) { return p.name; }).join(', ') };
|
|
11011
|
+
}
|
|
10804
11012
|
function scanNode(node, pageName) {
|
|
10805
11013
|
if (node.type === 'COMPONENT') {
|
|
10806
11014
|
var comp = {
|
|
@@ -10823,66 +11031,79 @@ Examples:
|
|
|
10823
11031
|
}
|
|
10824
11032
|
}
|
|
10825
11033
|
}
|
|
10826
|
-
scanNode(
|
|
11034
|
+
scanNode(targetPage, targetPage.name);
|
|
11035
|
+
return {
|
|
11036
|
+
fileName: figma.root.name,
|
|
11037
|
+
fileKey: figma.fileKey || '',
|
|
11038
|
+
pageCount: 1,
|
|
11039
|
+
componentCount: components.length,
|
|
11040
|
+
components: components
|
|
11041
|
+
};
|
|
11042
|
+
} catch(e) {
|
|
11043
|
+
return { error: e.message || 'Failed to scan page for components.' };
|
|
10827
11044
|
}
|
|
10828
|
-
|
|
10829
|
-
|
|
10830
|
-
|
|
10831
|
-
|
|
10832
|
-
|
|
10833
|
-
components: components
|
|
10834
|
-
};
|
|
10835
|
-
} catch(e) {
|
|
10836
|
-
return { error: e.message || 'Failed to scan file for components.' };
|
|
11045
|
+
})()`;
|
|
11046
|
+
const result = await daemonExec('eval', { code });
|
|
11047
|
+
if (result.error) {
|
|
11048
|
+
spinner.fail(result.error);
|
|
11049
|
+
process.exit(1);
|
|
10837
11050
|
}
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
10841
|
-
|
|
10842
|
-
|
|
10843
|
-
}
|
|
10844
|
-
// Save to ~/.figma-local/libraries/
|
|
10845
|
-
const libDir = join(homedir(), '.figma-local', 'libraries');
|
|
10846
|
-
mkdirSync(libDir, { recursive: true });
|
|
10847
|
-
const safeName = result.fileName.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
|
|
10848
|
-
const libFile = join(libDir, `${safeName}.json`);
|
|
10849
|
-
const indexData = {
|
|
10850
|
-
fileName: result.fileName,
|
|
10851
|
-
fileKey: result.fileKey,
|
|
10852
|
-
indexedAt: new Date().toISOString(),
|
|
10853
|
-
pageCount: result.pageCount,
|
|
10854
|
-
componentCount: result.componentCount,
|
|
10855
|
-
components: result.components
|
|
10856
|
-
};
|
|
10857
|
-
writeFileSync(libFile, JSON.stringify(indexData, null, 2));
|
|
10858
|
-
spinner.succeed(`Indexed ${result.componentCount} components from "${result.fileName}" (${result.pageCount} pages)`);
|
|
10859
|
-
console.log(` ${chalk.gray('Saved to:')} ${libFile}`);
|
|
10860
|
-
if (options.json) {
|
|
10861
|
-
console.log(JSON.stringify(indexData, null, 2));
|
|
11051
|
+
const { indexData, libFile } = saveIndexAndPrint(result, { page: pageName });
|
|
11052
|
+
spinner.succeed(`Indexed ${result.componentCount} components from page "${pageName}" (${indexData.componentCount} total in library)`);
|
|
11053
|
+
printIndexSummary(indexData, libFile, options);
|
|
11054
|
+
|
|
11055
|
+
// ---- Plugin mode full scan ----
|
|
10862
11056
|
} else {
|
|
10863
|
-
|
|
10864
|
-
const
|
|
10865
|
-
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
|
|
10870
|
-
|
|
10871
|
-
|
|
10872
|
-
|
|
10873
|
-
|
|
10874
|
-
|
|
10875
|
-
|
|
10876
|
-
|
|
10877
|
-
|
|
10878
|
-
|
|
10879
|
-
|
|
10880
|
-
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
|
|
11057
|
+
spinner.text = 'Scanning all pages for components...';
|
|
11058
|
+
const code = `(function() {
|
|
11059
|
+
try {
|
|
11060
|
+
var components = [];
|
|
11061
|
+
var pages = figma.root.children;
|
|
11062
|
+
for (var p = 0; p < pages.length; p++) {
|
|
11063
|
+
var page = pages[p];
|
|
11064
|
+
function scanNode(node, pageName) {
|
|
11065
|
+
if (node.type === 'COMPONENT') {
|
|
11066
|
+
var comp = {
|
|
11067
|
+
name: node.name,
|
|
11068
|
+
key: node.key,
|
|
11069
|
+
id: node.id,
|
|
11070
|
+
page: pageName,
|
|
11071
|
+
description: node.description || '',
|
|
11072
|
+
w: Math.round(node.width),
|
|
11073
|
+
h: Math.round(node.height)
|
|
11074
|
+
};
|
|
11075
|
+
if (node.parent && node.parent.type === 'COMPONENT_SET') {
|
|
11076
|
+
comp.componentSet = node.parent.name;
|
|
11077
|
+
}
|
|
11078
|
+
components.push(comp);
|
|
11079
|
+
}
|
|
11080
|
+
if ('children' in node) {
|
|
11081
|
+
for (var i = 0; i < node.children.length; i++) {
|
|
11082
|
+
scanNode(node.children[i], pageName);
|
|
11083
|
+
}
|
|
11084
|
+
}
|
|
11085
|
+
}
|
|
11086
|
+
scanNode(page, page.name);
|
|
11087
|
+
}
|
|
11088
|
+
return {
|
|
11089
|
+
fileName: figma.root.name,
|
|
11090
|
+
fileKey: figma.fileKey || '',
|
|
11091
|
+
pageCount: pages.length,
|
|
11092
|
+
componentCount: components.length,
|
|
11093
|
+
components: components
|
|
11094
|
+
};
|
|
11095
|
+
} catch(e) {
|
|
11096
|
+
return { error: e.message || 'Failed to scan file for components.' };
|
|
10884
11097
|
}
|
|
11098
|
+
})()`;
|
|
11099
|
+
const result = await daemonExec('eval', { code });
|
|
11100
|
+
if (result.error) {
|
|
11101
|
+
spinner.fail(result.error);
|
|
11102
|
+
process.exit(1);
|
|
10885
11103
|
}
|
|
11104
|
+
const { indexData, libFile } = saveIndexAndPrint(result, {});
|
|
11105
|
+
spinner.succeed(`Indexed ${result.componentCount} components from "${result.fileName}" (${result.pageCount} pages)`);
|
|
11106
|
+
printIndexSummary(indexData, libFile, options);
|
|
10886
11107
|
}
|
|
10887
11108
|
} else if (action === 'search') {
|
|
10888
11109
|
const nameFilter = options.name ? options.name.toLowerCase() : '';
|