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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-local",
3
- "version": "1.8.0",
3
+ "version": "2.0.0",
4
4
  "description": "Control Figma Desktop with Claude Code. Smart read, write, and AI-prompt export. No API key required.",
5
5
  "author": "elvke",
6
6
  "license": "MIT",
@@ -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. Import components by key and browse available 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
- ## Usage
21
+ ## IMPORTANT: How to access library components
23
22
 
24
- ### List library variable collections
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
- See what variable collections are available from linked libraries:
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 collections
31
+ fig library index --api --token "figd_xxxxx" --file "https://www.figma.com/design/ABC123/MyFile"
30
32
  ```
31
33
 
32
- Returns: collection name, library name, and collection key for each.
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 variables
38
+ fig library index --api --file "https://www.figma.com/design/ABC123/MyFile"
40
39
  ```
41
40
 
42
- Filter by name:
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 variables --name "color"
46
- fig library variables --name "spacing"
47
- fig library variables --name "radius"
46
+ fig library index --page "Buttons"
47
+ fig library index --page "Inputs"
48
+ fig library index --page "Cards"
48
49
  ```
49
50
 
50
- Returns: variable name, type (COLOR, FLOAT, STRING), key, collection, and library name.
51
+ Each page's components are merged into the same index file. This avoids hanging on large files.
51
52
 
52
- ### List library components
53
+ ### Option C: Full scan (small files only)
53
54
 
54
- Find library components that are already used on the current page:
55
+ Open the library file in Figma, then:
55
56
 
56
57
  ```bash
57
- fig library components
58
+ fig library index
58
59
  ```
59
60
 
60
- Filter by name:
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 components --name "button"
64
- fig library components --name "input"
65
- fig library components --name "card"
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, key, description, and whether it's remote (library) or local.
74
+ Returns: component name, key (for importing), component set, page, and library name.
69
75
 
70
- ### Import a component by key
71
-
72
- Import a library component onto the canvas by its key:
76
+ ### List indexed libraries
73
77
 
74
78
  ```bash
75
- fig library import --key "abc123def456..."
79
+ fig library list
76
80
  ```
77
81
 
78
- Import and rename:
82
+ ### Import by key
79
83
 
80
84
  ```bash
81
- fig library import --key "abc123def456..." --name "PrimaryButton"
85
+ fig library import --key "<key-from-search>"
86
+ fig library import --key "<key>" --name "PrimaryButton"
82
87
  ```
83
88
 
84
- The imported component instance is placed at the viewport center and selected.
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
- ### JSON output
97
+ ## Variables (no indexing needed)
87
98
 
88
- All commands support `--json` for structured output:
99
+ Library variables are available directly via the plugin API:
89
100
 
90
101
  ```bash
91
- fig library collections --json
92
- fig library variables --json
93
- fig library variables --name "color" --json
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
- ## Workflow: Building with library components
107
+ ## Components on current page
98
108
 
99
- 1. **Discover** what's available:
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
- 2. **Import** the components you need:
107
- ```bash
108
- fig library import --key "<key-from-step-1>"
109
- ```
111
+ ```bash
112
+ fig library components
113
+ fig library components --name "button"
114
+ ```
110
115
 
111
- 3. **Inspect** the imported component to get its specs:
112
- ```bash
113
- fig inspect --deep
114
- ```
116
+ ## Full workflow
115
117
 
116
- 4. **Read** variables to get token values:
118
+ 1. **Index** the library (pick one method):
117
119
  ```bash
118
- fig library variables --name "primary" --json
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
- 5. **Replicate** in code using the exact specs and token values.
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 collections --json
128
+ fig library search --name "button" --json
128
129
  ```
129
130
 
130
- 2. Export all variables grouped by type:
131
+ 3. **Import** into your working file:
131
132
  ```bash
132
- fig library variables --name "color" --json > colors.json
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
- 3. Find key components:
136
+ 4. **Inspect** for full specs:
138
137
  ```bash
139
- fig library components --json
138
+ fig inspect --deep
140
139
  ```
141
140
 
142
- 4. Import and document each:
141
+ 5. **Get variables** for tokens:
143
142
  ```bash
144
- fig library import --key "<key>"
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
- - If `fig library components` returns empty, drag some components from the Assets panel onto the page first the command scans instances on the current page.
151
- - Use `fig library variables --json` and pipe to `jq` for filtering: `fig library variables --json | jq '.[] | select(.resolvedType == "COLOR")'`
152
- - After importing a component, use `fig inspect --deep` to get full specs including variable bindings.
150
+ - **REST API is fastest** for large design systemsno 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) {