figma-local 1.8.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 +76 -74
- package/src/index.js +408 -6
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: figma-library
|
|
3
3
|
description: |
|
|
4
|
-
Use this skill when the user wants to access, browse, import, or use components and variables from a Figma team library or external Figma file. Triggers on: "library components", "import component", "library variables", "team library", "use components from another file", "get the button from the library", "what components are available", "design system components", "import from library", "library collections". Also use when building UI and the user references a design system or component library in another Figma file.
|
|
4
|
+
Use this skill when the user wants to access, browse, import, or use components and variables from a Figma team library or external Figma file. Triggers on: "library components", "import component", "library variables", "team library", "use components from another file", "get the button from the library", "what components are available", "design system components", "import from library", "library collections", "index the library", "scan components", "search for a component". Also use when building UI and the user references a design system or component library in another Figma file.
|
|
5
5
|
allowed-tools:
|
|
6
6
|
- Bash(fig library *)
|
|
7
7
|
- Bash(fig library)
|
|
@@ -11,143 +11,145 @@ allowed-tools:
|
|
|
11
11
|
|
|
12
12
|
# Figma Library
|
|
13
13
|
|
|
14
|
-
Access team library components and variables from other Figma files.
|
|
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
|
-
|
|
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
|
-
|
|
25
|
+
### Option A: REST API (recommended for large files)
|
|
27
26
|
|
|
27
|
+
No plugin needed. Works on any file size without hanging Figma.
|
|
28
|
+
|
|
29
|
+
**First time — provide token and file URL:**
|
|
28
30
|
```bash
|
|
29
|
-
fig library
|
|
31
|
+
fig library index --api --token "figd_xxxxx" --file "https://www.figma.com/design/ABC123/MyFile"
|
|
30
32
|
```
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
### List library variables
|
|
35
|
-
|
|
36
|
-
Browse all variables across all linked library collections:
|
|
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
|
|
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)
|
|
42
|
+
|
|
43
|
+
Open the library file in Figma, then scan one page at a time:
|
|
43
44
|
|
|
44
45
|
```bash
|
|
45
|
-
fig library
|
|
46
|
-
fig library
|
|
47
|
-
fig library
|
|
46
|
+
fig library index --page "Buttons"
|
|
47
|
+
fig library index --page "Inputs"
|
|
48
|
+
fig library index --page "Cards"
|
|
48
49
|
```
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
Each page's components are merged into the same index file. This avoids hanging on large files.
|
|
51
52
|
|
|
52
|
-
###
|
|
53
|
+
### Option C: Full scan (small files only)
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
Open the library file in Figma, then:
|
|
55
56
|
|
|
56
57
|
```bash
|
|
57
|
-
fig library
|
|
58
|
+
fig library index
|
|
58
59
|
```
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
**Warning:** This scans ALL pages at once. Only use on small files — large design systems will cause Figma to hang.
|
|
62
|
+
|
|
63
|
+
## After indexing: Search and Import
|
|
64
|
+
|
|
65
|
+
### Search indexed libraries
|
|
61
66
|
|
|
62
67
|
```bash
|
|
63
|
-
fig library
|
|
64
|
-
fig library
|
|
65
|
-
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"
|
|
66
72
|
```
|
|
67
73
|
|
|
68
|
-
Returns: component name, component set,
|
|
74
|
+
Returns: component name, key (for importing), component set, page, and library name.
|
|
69
75
|
|
|
70
|
-
###
|
|
71
|
-
|
|
72
|
-
Import a library component onto the canvas by its key:
|
|
76
|
+
### List indexed libraries
|
|
73
77
|
|
|
74
78
|
```bash
|
|
75
|
-
fig library
|
|
79
|
+
fig library list
|
|
76
80
|
```
|
|
77
81
|
|
|
78
|
-
Import
|
|
82
|
+
### Import by key
|
|
79
83
|
|
|
80
84
|
```bash
|
|
81
|
-
fig library import --key "
|
|
85
|
+
fig library import --key "<key-from-search>"
|
|
86
|
+
fig library import --key "<key>" --name "PrimaryButton"
|
|
82
87
|
```
|
|
83
88
|
|
|
84
|
-
The
|
|
89
|
+
The component is placed at viewport center and selected.
|
|
90
|
+
|
|
91
|
+
### Inspect the imported component
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
fig inspect --deep
|
|
95
|
+
```
|
|
85
96
|
|
|
86
|
-
|
|
97
|
+
## Variables (no indexing needed)
|
|
87
98
|
|
|
88
|
-
|
|
99
|
+
Library variables are available directly via the plugin API:
|
|
89
100
|
|
|
90
101
|
```bash
|
|
91
|
-
fig library collections
|
|
92
|
-
fig library variables
|
|
93
|
-
fig library variables --name "color"
|
|
94
|
-
fig library components --json
|
|
102
|
+
fig library collections # List variable collections
|
|
103
|
+
fig library variables # List all variables
|
|
104
|
+
fig library variables --name "color" # Search by name
|
|
95
105
|
```
|
|
96
106
|
|
|
97
|
-
##
|
|
107
|
+
## Components on current page
|
|
98
108
|
|
|
99
|
-
|
|
100
|
-
```bash
|
|
101
|
-
fig library collections
|
|
102
|
-
fig library variables --name "color"
|
|
103
|
-
fig library components --name "button"
|
|
104
|
-
```
|
|
109
|
+
Find library components already dragged onto the current page:
|
|
105
110
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
```bash
|
|
112
|
+
fig library components
|
|
113
|
+
fig library components --name "button"
|
|
114
|
+
```
|
|
110
115
|
|
|
111
|
-
|
|
112
|
-
```bash
|
|
113
|
-
fig inspect --deep
|
|
114
|
-
```
|
|
116
|
+
## Full workflow
|
|
115
117
|
|
|
116
|
-
|
|
118
|
+
1. **Index** the library (pick one method):
|
|
117
119
|
```bash
|
|
118
|
-
|
|
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"
|
|
119
124
|
```
|
|
120
125
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
## Workflow: Extracting a design system
|
|
124
|
-
|
|
125
|
-
1. List all variable collections:
|
|
126
|
+
2. **Search** for components:
|
|
126
127
|
```bash
|
|
127
|
-
fig library
|
|
128
|
+
fig library search --name "button" --json
|
|
128
129
|
```
|
|
129
130
|
|
|
130
|
-
|
|
131
|
+
3. **Import** into your working file:
|
|
131
132
|
```bash
|
|
132
|
-
fig library
|
|
133
|
-
fig library variables --name "spacing" --json > spacing.json
|
|
134
|
-
fig library variables --name "radius" --json > radii.json
|
|
133
|
+
fig library import --key "<key>"
|
|
135
134
|
```
|
|
136
135
|
|
|
137
|
-
|
|
136
|
+
4. **Inspect** for full specs:
|
|
138
137
|
```bash
|
|
139
|
-
fig
|
|
138
|
+
fig inspect --deep
|
|
140
139
|
```
|
|
141
140
|
|
|
142
|
-
|
|
141
|
+
5. **Get variables** for tokens:
|
|
143
142
|
```bash
|
|
144
|
-
fig library
|
|
145
|
-
fig document --json
|
|
143
|
+
fig library variables --name "primary" --json
|
|
146
144
|
```
|
|
147
145
|
|
|
146
|
+
6. **Replicate** in code.
|
|
147
|
+
|
|
148
148
|
## Tips
|
|
149
149
|
|
|
150
|
-
-
|
|
151
|
-
-
|
|
152
|
-
-
|
|
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.
|
|
153
155
|
- Component keys are stable across file versions — save them for repeated imports.
|
package/src/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import chalk from 'chalk';
|
|
|
5
5
|
import ora from 'ora';
|
|
6
6
|
import { execSync, spawn } from 'child_process';
|
|
7
7
|
import { randomBytes } from 'crypto';
|
|
8
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, readdirSync } from 'fs';
|
|
9
9
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
10
10
|
import { dirname, join } from 'path';
|
|
11
11
|
import { createInterface } from 'readline';
|
|
@@ -10581,21 +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
|
-
components List available library components (use --name to filter)
|
|
10592
|
+
components List available library components on current page (use --name to filter)
|
|
10593
|
+
index Scan and save all components to a local index
|
|
10594
|
+
search Search indexed libraries for components (use --name to filter)
|
|
10589
10595
|
import Import a component by key (use --key)
|
|
10596
|
+
list List all indexed libraries
|
|
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
|
|
10590
10603
|
|
|
10591
10604
|
Examples:
|
|
10592
10605
|
fig library collections List all library variable collections
|
|
10593
|
-
fig library variables List all library variables
|
|
10594
10606
|
fig library variables --name "color" List variables matching "color"
|
|
10595
|
-
fig library components List available library components
|
|
10596
10607
|
fig library components --name "button" Find button components
|
|
10608
|
+
fig library index --api --token "figd_..." --file "https://www.figma.com/design/ABC/..."
|
|
10609
|
+
fig library search --name "button" Search indexed libraries for "button"
|
|
10610
|
+
fig library list List all indexed libraries
|
|
10597
10611
|
fig library import --key "abc123..." Import a component by its key
|
|
10598
|
-
fig library import --key "abc123..." --name "MyButton" Import and rename
|
|
10599
10612
|
`)
|
|
10600
10613
|
.action(async (action, options) => {
|
|
10601
10614
|
checkConnection();
|
|
@@ -10787,8 +10800,397 @@ Examples:
|
|
|
10787
10800
|
spinner.succeed(`Imported "${result.componentName}" as ${result.name} (${result.w}x${result.h})`);
|
|
10788
10801
|
console.log(` ${chalk.gray('id:')} ${result.id}`);
|
|
10789
10802
|
console.log(chalk.gray(' Component is now selected on the canvas.'));
|
|
10803
|
+
} else if (action === 'index') {
|
|
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
|
+
|
|
10929
|
+
try {
|
|
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
|
+
}
|
|
11012
|
+
function scanNode(node, pageName) {
|
|
11013
|
+
if (node.type === 'COMPONENT') {
|
|
11014
|
+
var comp = {
|
|
11015
|
+
name: node.name,
|
|
11016
|
+
key: node.key,
|
|
11017
|
+
id: node.id,
|
|
11018
|
+
page: pageName,
|
|
11019
|
+
description: node.description || '',
|
|
11020
|
+
w: Math.round(node.width),
|
|
11021
|
+
h: Math.round(node.height)
|
|
11022
|
+
};
|
|
11023
|
+
if (node.parent && node.parent.type === 'COMPONENT_SET') {
|
|
11024
|
+
comp.componentSet = node.parent.name;
|
|
11025
|
+
}
|
|
11026
|
+
components.push(comp);
|
|
11027
|
+
}
|
|
11028
|
+
if ('children' in node) {
|
|
11029
|
+
for (var i = 0; i < node.children.length; i++) {
|
|
11030
|
+
scanNode(node.children[i], pageName);
|
|
11031
|
+
}
|
|
11032
|
+
}
|
|
11033
|
+
}
|
|
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.' };
|
|
11044
|
+
}
|
|
11045
|
+
})()`;
|
|
11046
|
+
const result = await daemonExec('eval', { code });
|
|
11047
|
+
if (result.error) {
|
|
11048
|
+
spinner.fail(result.error);
|
|
11049
|
+
process.exit(1);
|
|
11050
|
+
}
|
|
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 ----
|
|
11056
|
+
} else {
|
|
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.' };
|
|
11097
|
+
}
|
|
11098
|
+
})()`;
|
|
11099
|
+
const result = await daemonExec('eval', { code });
|
|
11100
|
+
if (result.error) {
|
|
11101
|
+
spinner.fail(result.error);
|
|
11102
|
+
process.exit(1);
|
|
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);
|
|
11107
|
+
}
|
|
11108
|
+
} else if (action === 'search') {
|
|
11109
|
+
const nameFilter = options.name ? options.name.toLowerCase() : '';
|
|
11110
|
+
if (!nameFilter) {
|
|
11111
|
+
spinner.fail('--name is required for search. Example: fig library search --name "button"');
|
|
11112
|
+
process.exit(1);
|
|
11113
|
+
}
|
|
11114
|
+
spinner.text = `Searching indexed libraries for "${options.name}"...`;
|
|
11115
|
+
const libDir = join(homedir(), '.figma-local', 'libraries');
|
|
11116
|
+
if (!existsSync(libDir)) {
|
|
11117
|
+
spinner.fail('No indexed libraries found. Open a library file in Figma and run: fig library index');
|
|
11118
|
+
process.exit(1);
|
|
11119
|
+
}
|
|
11120
|
+
const files = readdirSync(libDir).filter(f => f.endsWith('.json'));
|
|
11121
|
+
if (files.length === 0) {
|
|
11122
|
+
spinner.fail('No indexed libraries found. Open a library file in Figma and run: fig library index');
|
|
11123
|
+
process.exit(1);
|
|
11124
|
+
}
|
|
11125
|
+
const results = [];
|
|
11126
|
+
for (const file of files) {
|
|
11127
|
+
try {
|
|
11128
|
+
const lib = JSON.parse(readFileSync(join(libDir, file), 'utf8'));
|
|
11129
|
+
for (const comp of lib.components) {
|
|
11130
|
+
const matchName = comp.name.toLowerCase().includes(nameFilter);
|
|
11131
|
+
const matchSet = comp.componentSet && comp.componentSet.toLowerCase().includes(nameFilter);
|
|
11132
|
+
const matchDesc = comp.description && comp.description.toLowerCase().includes(nameFilter);
|
|
11133
|
+
if (matchName || matchSet || matchDesc) {
|
|
11134
|
+
results.push({ ...comp, library: lib.fileName });
|
|
11135
|
+
}
|
|
11136
|
+
}
|
|
11137
|
+
} catch (e) { /* skip corrupt files */ }
|
|
11138
|
+
}
|
|
11139
|
+
spinner.succeed(`Found ${results.length} component${results.length !== 1 ? 's' : ''} matching "${options.name}"`);
|
|
11140
|
+
if (options.json) {
|
|
11141
|
+
console.log(JSON.stringify(results, null, 2));
|
|
11142
|
+
} else {
|
|
11143
|
+
if (results.length === 0) {
|
|
11144
|
+
console.log(chalk.yellow(' No matches. Try a different search term, or index more libraries with: fig library index'));
|
|
11145
|
+
}
|
|
11146
|
+
let currentLib = '';
|
|
11147
|
+
for (const c of results) {
|
|
11148
|
+
if (c.library !== currentLib) {
|
|
11149
|
+
currentLib = c.library;
|
|
11150
|
+
console.log(chalk.cyan(`\n ${currentLib}`));
|
|
11151
|
+
}
|
|
11152
|
+
const setLabel = c.componentSet ? chalk.gray(` (set: ${c.componentSet})`) : '';
|
|
11153
|
+
console.log(` ${chalk.white(c.name)}${setLabel}`);
|
|
11154
|
+
console.log(` ${chalk.gray('key:')} ${c.key} ${chalk.gray('page:')} ${c.page} ${chalk.gray(`${c.w}x${c.h}`)}`);
|
|
11155
|
+
if (c.description) console.log(` ${chalk.gray('desc:')} ${c.description}`);
|
|
11156
|
+
}
|
|
11157
|
+
}
|
|
11158
|
+
} else if (action === 'list') {
|
|
11159
|
+
spinner.text = 'Loading indexed libraries...';
|
|
11160
|
+
const libDir = join(homedir(), '.figma-local', 'libraries');
|
|
11161
|
+
if (!existsSync(libDir)) {
|
|
11162
|
+
spinner.fail('No indexed libraries found. Open a library file in Figma and run: fig library index');
|
|
11163
|
+
process.exit(1);
|
|
11164
|
+
}
|
|
11165
|
+
const files = readdirSync(libDir).filter(f => f.endsWith('.json'));
|
|
11166
|
+
if (files.length === 0) {
|
|
11167
|
+
spinner.fail('No indexed libraries found. Open a library file in Figma and run: fig library index');
|
|
11168
|
+
process.exit(1);
|
|
11169
|
+
}
|
|
11170
|
+
const libs = [];
|
|
11171
|
+
for (const file of files) {
|
|
11172
|
+
try {
|
|
11173
|
+
const lib = JSON.parse(readFileSync(join(libDir, file), 'utf8'));
|
|
11174
|
+
libs.push({
|
|
11175
|
+
fileName: lib.fileName,
|
|
11176
|
+
componentCount: lib.componentCount,
|
|
11177
|
+
pageCount: lib.pageCount,
|
|
11178
|
+
indexedAt: lib.indexedAt,
|
|
11179
|
+
path: join(libDir, file)
|
|
11180
|
+
});
|
|
11181
|
+
} catch (e) { /* skip corrupt files */ }
|
|
11182
|
+
}
|
|
11183
|
+
spinner.succeed(`${libs.length} indexed librar${libs.length !== 1 ? 'ies' : 'y'}`);
|
|
11184
|
+
if (options.json) {
|
|
11185
|
+
console.log(JSON.stringify(libs, null, 2));
|
|
11186
|
+
} else {
|
|
11187
|
+
for (const lib of libs) {
|
|
11188
|
+
console.log(` ${chalk.white(lib.fileName)}`);
|
|
11189
|
+
console.log(` ${chalk.gray('components:')} ${lib.componentCount} ${chalk.gray('pages:')} ${lib.pageCount} ${chalk.gray('indexed:')} ${lib.indexedAt}`);
|
|
11190
|
+
}
|
|
11191
|
+
}
|
|
10790
11192
|
} else {
|
|
10791
|
-
spinner.fail(`Unknown action: ${action}. Use: collections, variables, components, import`);
|
|
11193
|
+
spinner.fail(`Unknown action: ${action}. Use: collections, variables, components, index, search, list, import`);
|
|
10792
11194
|
process.exit(1);
|
|
10793
11195
|
}
|
|
10794
11196
|
} catch (e) {
|