design-lazyyy-cli 0.1.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 ADDED
@@ -0,0 +1,291 @@
1
+ <div align="center">
2
+
3
+ # design-lazyyy-cli
4
+
5
+ **Control Figma Desktop from your terminal. No API key needed.**
6
+
7
+ [![MIT License](https://img.shields.io/badge/license-MIT-7c3aed?style=flat-square)](LICENSE)
8
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-7c3aed?style=flat-square&logo=node.js&logoColor=white)](https://nodejs.org)
9
+ [![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Windows%20%7C%20Linux-7c3aed?style=flat-square)]()
10
+
11
+ ---
12
+
13
+ Create design tokens, render UI with JSX, manage variables, export CSS/Tailwind
14
+ all through a single CLI connected to Figma Desktop via Chrome DevTools Protocol.
15
+
16
+ </div>
17
+
18
+ <br>
19
+
20
+ ## Why design-lazyyy-cli?
21
+
22
+ | Pain Point | Solution |
23
+ |:---|:---|
24
+ | Figma REST API requires tokens & has rate limits | Direct desktop connection, zero config |
25
+ | Manually creating 200+ color variables | `tokens tailwind` generates 242 variables instantly |
26
+ | Switching between Figma and code for tokens | `export css` / `export tailwind` in one command |
27
+ | Building complex layouts click-by-click | JSX-like `render` command creates nested auto-layouts |
28
+ | Repetitive design system setup | `tokens ds` bootstraps a complete system in seconds |
29
+
30
+ <br>
31
+
32
+ ## Quick Start
33
+
34
+ ### Prerequisites
35
+
36
+ - **Node.js** >= 18
37
+ - **Figma Desktop** (free account works)
38
+
39
+ ### Installation
40
+
41
+ ```bash
42
+ # Install globally via npm
43
+ npm install -g design-lazyyy-cli
44
+
45
+ # One-time setup: patch Figma & install figma-use
46
+ design-lazyyy-cli init
47
+
48
+ # Connect to Figma Desktop
49
+ design-lazyyy-cli connect
50
+ ```
51
+
52
+ > **Tip:** Run `design-lazyyy-cli status` to verify the connection is active.
53
+
54
+ <br>
55
+
56
+ ## Usage with Claude Code
57
+
58
+ design-lazyyy-cli ships with a `CLAUDE.md` that teaches Claude every command. No `/init` needed.
59
+
60
+ ```bash
61
+ cd design-lazyyy-cli
62
+ claude
63
+ ```
64
+
65
+ Then just talk naturally:
66
+
67
+ > "Create a complete design system with Tailwind colors"
68
+ > "Render a card component with title and description"
69
+ > "Export all variables as CSS custom properties"
70
+
71
+ Claude handles the rest.
72
+
73
+ <br>
74
+
75
+ ## Core Commands
76
+
77
+ ### Connect & Status
78
+
79
+ ```bash
80
+ node src/index.js connect # Connect to Figma Desktop
81
+ node src/index.js status # Check connection status
82
+ ```
83
+
84
+ ### Design Tokens
85
+
86
+ ```bash
87
+ # Full design system (colors, spacing, typography, radii)
88
+ node src/index.js tokens ds
89
+
90
+ # Tailwind CSS palette (22 families x 11 shades = 242 variables)
91
+ node src/index.js tokens tailwind
92
+
93
+ # Spacing scale (4px base system)
94
+ node src/index.js tokens spacing
95
+ ```
96
+
97
+ ### Create Elements
98
+
99
+ All `create` commands auto-position to avoid overlaps.
100
+
101
+ ```bash
102
+ node src/index.js create rect "Card" -w 320 -h 200 --fill "#fff" --radius 12
103
+ node src/index.js create circle "Avatar" -w 48 --fill "#3b82f6"
104
+ node src/index.js create text "Hello" -s 24 -c "#000" -w bold
105
+ node src/index.js create autolayout "Stack" -d col -g 16 -p 24
106
+ node src/index.js create icon lucide:star -s 24 -c "#f59e0b"
107
+ node src/index.js create component "Button"
108
+ ```
109
+
110
+ ### Render with JSX
111
+
112
+ Build complex nested layouts in a single command:
113
+
114
+ ```bash
115
+ node src/index.js render '<Frame name="Card" w={320} bg="#fff" rounded={16} flex="col" gap={8} p={24}>
116
+ <Text size={20} weight="bold" color="#111">Card Title</Text>
117
+ <Text size={14} color="#666" w="fill">Description goes here</Text>
118
+ </Frame>'
119
+ ```
120
+
121
+ <details>
122
+ <summary><strong>JSX Props Reference</strong></summary>
123
+
124
+ | Prop | Description | Example |
125
+ |:-----|:-----------|:--------|
126
+ | `w` | Width (px or `"fill"`) | `w={320}` |
127
+ | `h` | Height | `h={200}` |
128
+ | `bg` | Background color | `bg="#fff"` |
129
+ | `rounded` | Corner radius | `rounded={16}` |
130
+ | `flex` | Auto-layout direction | `flex="col"` or `flex="row"` |
131
+ | `gap` | Spacing between children | `gap={8}` |
132
+ | `p` | Padding (all sides) | `p={24}` |
133
+ | `x`, `y` | Canvas position | `x={100} y={0}` |
134
+ | `size` | Font size (Text only) | `size={20}` |
135
+ | `weight` | Font weight (Text only) | `weight="bold"` |
136
+ | `color` | Text color (Text only) | `color="#111"` |
137
+
138
+ </details>
139
+
140
+ ### Modify Elements
141
+
142
+ Works on current selection or a specific node with `-n "1:234"`.
143
+
144
+ ```bash
145
+ node src/index.js set fill "#3b82f6"
146
+ node src/index.js set stroke "#e4e4e7" -w 1
147
+ node src/index.js set radius 12
148
+ node src/index.js set size 320 200
149
+ node src/index.js set pos 100 100
150
+ node src/index.js set opacity 0.5
151
+ node src/index.js set autolayout row -g 8 -p 16
152
+ node src/index.js set name "Header"
153
+ ```
154
+
155
+ ### Variable Binding
156
+
157
+ Bind design tokens to elements for theme-responsive designs:
158
+
159
+ ```bash
160
+ node src/index.js bind fill "primary/500"
161
+ node src/index.js bind stroke "border/default"
162
+ node src/index.js bind radius "radius/md"
163
+ node src/index.js bind gap "spacing/md"
164
+ node src/index.js bind padding "spacing/lg"
165
+ node src/index.js bind list # Show all bindable variables
166
+ node src/index.js bind list -t COLOR # Filter by type
167
+ ```
168
+
169
+ ### Sizing & Layout
170
+
171
+ ```bash
172
+ node src/index.js sizing hug # Hug contents
173
+ node src/index.js sizing fill # Fill container
174
+ node src/index.js sizing fixed 320 200 # Fixed dimensions
175
+ node src/index.js padding 16 24 # Vertical, horizontal
176
+ node src/index.js gap 16
177
+ node src/index.js align center
178
+ ```
179
+
180
+ ### Find, Select & Inspect
181
+
182
+ ```bash
183
+ node src/index.js find "Button" # Search by name
184
+ node src/index.js find "Card" -t FRAME # Filter by type
185
+ node src/index.js select "1:234" # Select by node ID
186
+ node src/index.js get # Inspect selection
187
+ node src/index.js canvas info # Overview of canvas
188
+ ```
189
+
190
+ ### Duplicate, Delete & Arrange
191
+
192
+ ```bash
193
+ node src/index.js duplicate # Duplicate selection
194
+ node src/index.js delete "1:234" # Delete node
195
+ node src/index.js arrange -g 100 -c 3 # Arrange in 3-column grid
196
+ ```
197
+
198
+ ### Export
199
+
200
+ ```bash
201
+ node src/index.js export css # CSS custom properties
202
+ node src/index.js export tailwind # Tailwind config
203
+ node src/index.js export screenshot -o shot.png # Screenshot
204
+ ```
205
+
206
+ ### Variables & Collections
207
+
208
+ ```bash
209
+ node src/index.js var list # List all variables
210
+ node src/index.js var create "primary/500" -c "CollectionId" -t COLOR -v "#3b82f6"
211
+ node src/index.js col list # List collections
212
+ node src/index.js col create "Semantic Colors" # Create collection
213
+ ```
214
+
215
+ ### FigJam
216
+
217
+ ```bash
218
+ node src/index.js fj list # List pages
219
+ node src/index.js fj sticky "Note" -x 100 -y 100
220
+ node src/index.js fj shape "Box" -x 200 -y 100 -w 200 -h 100
221
+ node src/index.js fj connect "NODE_1" "NODE_2"
222
+ node src/index.js fj nodes # List elements
223
+ node src/index.js fj eval "figma.currentPage.children.length"
224
+ ```
225
+
226
+ ### Advanced: Run Figma Plugin API
227
+
228
+ ```bash
229
+ node src/index.js eval "figma.currentPage.name"
230
+ node src/index.js raw query "//COMPONENT"
231
+ node src/index.js raw query "//*[contains(@name, 'Button')]"
232
+ ```
233
+
234
+ <br>
235
+
236
+ ## How It Works
237
+
238
+ ```
239
+ ┌──────────────────┐ ┌──────────────────┐
240
+ │ │ Chrome DevTools │ │
241
+ │ design-lazyyy │ ◄────Protocol──────► │ Figma Desktop │
242
+ │ CLI │ (localhost:9222) │ │
243
+ └──────────────────┘ └──────────────────┘
244
+ ```
245
+
246
+ The CLI connects to Figma Desktop through the Chrome DevTools Protocol on port `9222`. This means:
247
+
248
+ - **No API key** required
249
+ - **No rate limits**
250
+ - **Full Plugin API** access (same as writing a Figma plugin)
251
+ - Works with **free Figma accounts**
252
+
253
+ <br>
254
+
255
+ ## Recommended Workflow
256
+
257
+ ```
258
+ init ➜ connect ➜ tokens ds ➜ tokens tailwind ➜ create / render ➜ bind variables ➜ export
259
+ ```
260
+
261
+ 1. **Setup once** with `init` (patches Figma, installs dependencies)
262
+ 2. **Connect** to Figma Desktop
263
+ 3. **Bootstrap tokens** with `tokens ds` for a complete design system
264
+ 4. **Add Tailwind colors** with `tokens tailwind` (242 color variables)
265
+ 5. **Build UI** using `create` commands or `render` JSX
266
+ 6. **Bind variables** to elements for theme support
267
+ 7. **Export** as CSS or Tailwind config
268
+
269
+ <br>
270
+
271
+ ## Tips
272
+
273
+ - Use **`render`** instead of `eval` for anything with text the render command handles font loading automatically
274
+ - **Node IDs** look like `1:234` get them from `find`, `canvas info`, or `raw query`
275
+ - Every **`create` command auto-positions** so elements never overlap
276
+ - Use **`rescale()` not `resize()`** in eval scripts to preserve layer integrity
277
+ - Target specific nodes with **`-n "1:234"`** on any `set` or `bind` command
278
+
279
+ <br>
280
+
281
+ ## Credits
282
+
283
+ Built on top of [figma-use](https://github.com/dannote/figma-use) by dannote.
284
+
285
+ ## Author
286
+
287
+ **plugin87**
288
+
289
+ ## License
290
+
291
+ MIT
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "design-lazyyy-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI for managing Figma design systems. Create variables, components, and more. No API key required.",
5
+ "author": "plugin87",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "bin": {
9
+ "design-lazyyy-cli": "src/index.js"
10
+ },
11
+ "files": [
12
+ "src"
13
+ ],
14
+ "keywords": [
15
+ "figma",
16
+ "design-system",
17
+ "design-tokens",
18
+ "cli",
19
+ "variables"
20
+ ],
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "dependencies": {
25
+ "chalk": "^5.3.0",
26
+ "commander": "^12.0.0",
27
+ "ora": "^8.0.0",
28
+ "ws": "^8.16.0"
29
+ }
30
+ }
@@ -0,0 +1,310 @@
1
+ /**
2
+ * FigJam CDP Client
3
+ *
4
+ * Connects directly to FigJam via Chrome DevTools Protocol,
5
+ * bypassing figma-use which has compatibility issues with FigJam.
6
+ */
7
+
8
+ import WebSocket from 'ws';
9
+
10
+ export class FigJamClient {
11
+ constructor() {
12
+ this.ws = null;
13
+ this.contexts = [];
14
+ this.figmaContextId = null;
15
+ this.msgId = 0;
16
+ this.callbacks = new Map();
17
+ this.pageTitle = null;
18
+ }
19
+
20
+ /**
21
+ * List all available FigJam pages
22
+ */
23
+ static async listPages() {
24
+ const response = await fetch('http://localhost:9222/json');
25
+ const pages = await response.json();
26
+ return pages
27
+ .filter(p => p.title.includes('FigJam'))
28
+ .map(p => ({ title: p.title, id: p.id, url: p.url }));
29
+ }
30
+
31
+ /**
32
+ * Connect to a FigJam page by title (partial match)
33
+ */
34
+ async connect(pageTitle) {
35
+ const response = await fetch('http://localhost:9222/json');
36
+ const pages = await response.json();
37
+ const page = pages.find(p => p.title.includes(pageTitle) && p.title.includes('FigJam'));
38
+
39
+ if (!page) {
40
+ const figjamPages = pages.filter(p => p.title.includes('FigJam'));
41
+ if (figjamPages.length > 0) {
42
+ throw new Error(`Page "${pageTitle}" not found. Available FigJam pages: ${figjamPages.map(p => p.title).join(', ')}`);
43
+ }
44
+ throw new Error('No FigJam pages open. Please open a FigJam file in Figma Desktop.');
45
+ }
46
+
47
+ this.pageTitle = page.title;
48
+
49
+ return new Promise((resolve, reject) => {
50
+ this.ws = new WebSocket(page.webSocketDebuggerUrl);
51
+
52
+ this.ws.on('open', async () => {
53
+ await this.send('Runtime.enable');
54
+
55
+ // Wait for contexts to be discovered
56
+ await new Promise(r => setTimeout(r, 1500));
57
+
58
+ // Find figma context
59
+ for (const ctx of this.contexts) {
60
+ try {
61
+ const result = await this.send('Runtime.evaluate', {
62
+ expression: 'typeof figma !== "undefined"',
63
+ contextId: ctx.id,
64
+ returnByValue: true
65
+ });
66
+
67
+ if (result.result?.result?.value === true) {
68
+ this.figmaContextId = ctx.id;
69
+ break;
70
+ }
71
+ } catch (e) {}
72
+ }
73
+
74
+ if (!this.figmaContextId) {
75
+ reject(new Error('Could not find figma context. Try refreshing the FigJam page.'));
76
+ } else {
77
+ resolve(this);
78
+ }
79
+ });
80
+
81
+ this.ws.on('message', (data) => {
82
+ const msg = JSON.parse(data);
83
+
84
+ if (msg.method === 'Runtime.executionContextCreated') {
85
+ this.contexts.push(msg.params.context);
86
+ }
87
+
88
+ if (msg.id && this.callbacks.has(msg.id)) {
89
+ this.callbacks.get(msg.id)(msg);
90
+ this.callbacks.delete(msg.id);
91
+ }
92
+ });
93
+
94
+ this.ws.on('error', reject);
95
+
96
+ setTimeout(() => reject(new Error('Connection timeout')), 10000);
97
+ });
98
+ }
99
+
100
+ send(method, params = {}) {
101
+ return new Promise((resolve) => {
102
+ const id = ++this.msgId;
103
+ this.callbacks.set(id, resolve);
104
+ this.ws.send(JSON.stringify({ id, method, params }));
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Evaluate JavaScript in the FigJam context
110
+ */
111
+ async eval(expression) {
112
+ if (!this.figmaContextId) {
113
+ throw new Error('Not connected to FigJam');
114
+ }
115
+
116
+ const result = await this.send('Runtime.evaluate', {
117
+ expression,
118
+ contextId: this.figmaContextId,
119
+ returnByValue: true,
120
+ awaitPromise: true
121
+ });
122
+
123
+ if (result.result?.exceptionDetails) {
124
+ const error = result.result.exceptionDetails;
125
+ throw new Error(error.exception?.description || error.text || 'Evaluation error');
126
+ }
127
+
128
+ return result.result?.result?.value;
129
+ }
130
+
131
+ /**
132
+ * Get current page info
133
+ */
134
+ async getPageInfo() {
135
+ return await this.eval(`
136
+ (function() {
137
+ return {
138
+ name: figma.currentPage.name,
139
+ id: figma.currentPage.id,
140
+ childCount: figma.currentPage.children.length,
141
+ editorType: figma.editorType
142
+ };
143
+ })()
144
+ `);
145
+ }
146
+
147
+ /**
148
+ * List all nodes on the current page
149
+ */
150
+ async listNodes(limit = 50) {
151
+ return await this.eval(`
152
+ figma.currentPage.children.slice(0, ${limit}).map(function(n) {
153
+ return {
154
+ id: n.id,
155
+ type: n.type,
156
+ name: n.name || '',
157
+ x: Math.round(n.x),
158
+ y: Math.round(n.y)
159
+ };
160
+ })
161
+ `);
162
+ }
163
+
164
+ /**
165
+ * Create a sticky note
166
+ */
167
+ async createSticky(text, x = 0, y = 0, color) {
168
+ return await this.eval(`
169
+ (async function() {
170
+ var sticky = figma.createSticky();
171
+ sticky.x = ${x};
172
+ sticky.y = ${y};
173
+ ${color ? `sticky.fills = [{type: 'SOLID', color: ${JSON.stringify(hexToRgb(color))}}];` : ''}
174
+ // Load font before setting text
175
+ await figma.loadFontAsync({ family: "Inter", style: "Medium" });
176
+ sticky.text.characters = ${JSON.stringify(text)};
177
+ return { id: sticky.id, x: sticky.x, y: sticky.y };
178
+ })()
179
+ `);
180
+ }
181
+
182
+ /**
183
+ * Create a shape with text
184
+ */
185
+ async createShape(text, x = 0, y = 0, width = 200, height = 100, shapeType = 'ROUNDED_RECTANGLE') {
186
+ return await this.eval(`
187
+ (async function() {
188
+ var shape = figma.createShapeWithText();
189
+ shape.shapeType = ${JSON.stringify(shapeType)};
190
+ shape.x = ${x};
191
+ shape.y = ${y};
192
+ shape.resize(${width}, ${height});
193
+ if (shape.text) {
194
+ await figma.loadFontAsync({ family: "Inter", style: "Medium" });
195
+ shape.text.characters = ${JSON.stringify(text)};
196
+ }
197
+ return { id: shape.id, x: shape.x, y: shape.y };
198
+ })()
199
+ `);
200
+ }
201
+
202
+ /**
203
+ * Create a connector between two nodes
204
+ */
205
+ async createConnector(startNodeId, endNodeId) {
206
+ return await this.eval(`
207
+ (function() {
208
+ var startNode = figma.getNodeById(${JSON.stringify(startNodeId)});
209
+ var endNode = figma.getNodeById(${JSON.stringify(endNodeId)});
210
+ if (!startNode || !endNode) return { error: 'Node not found' };
211
+
212
+ var connector = figma.createConnector();
213
+ connector.connectorStart = { endpointNodeId: startNode.id, magnet: 'AUTO' };
214
+ connector.connectorEnd = { endpointNodeId: endNode.id, magnet: 'AUTO' };
215
+ return { id: connector.id };
216
+ })()
217
+ `);
218
+ }
219
+
220
+ /**
221
+ * Create a text node
222
+ */
223
+ async createText(text, x = 0, y = 0, fontSize = 16) {
224
+ return await this.eval(`
225
+ (async function() {
226
+ var textNode = figma.createText();
227
+ textNode.x = ${x};
228
+ textNode.y = ${y};
229
+ await figma.loadFontAsync({ family: "Inter", style: "Medium" });
230
+ textNode.characters = ${JSON.stringify(text)};
231
+ textNode.fontSize = ${fontSize};
232
+ return { id: textNode.id, x: textNode.x, y: textNode.y };
233
+ })()
234
+ `);
235
+ }
236
+
237
+ /**
238
+ * Delete a node by ID
239
+ */
240
+ async deleteNode(nodeId) {
241
+ return await this.eval(`
242
+ (function() {
243
+ var node = figma.getNodeById(${JSON.stringify(nodeId)});
244
+ if (node) {
245
+ node.remove();
246
+ return { deleted: true };
247
+ }
248
+ return { deleted: false, error: 'Node not found' };
249
+ })()
250
+ `);
251
+ }
252
+
253
+ /**
254
+ * Move a node
255
+ */
256
+ async moveNode(nodeId, x, y) {
257
+ return await this.eval(`
258
+ (function() {
259
+ var node = figma.getNodeById(${JSON.stringify(nodeId)});
260
+ if (node) {
261
+ node.x = ${x};
262
+ node.y = ${y};
263
+ return { id: node.id, x: node.x, y: node.y };
264
+ }
265
+ return { error: 'Node not found' };
266
+ })()
267
+ `);
268
+ }
269
+
270
+ /**
271
+ * Update text content of a node
272
+ */
273
+ async updateText(nodeId, text) {
274
+ return await this.eval(`
275
+ (async function() {
276
+ var node = figma.getNodeById(${JSON.stringify(nodeId)});
277
+ if (!node) return { error: 'Node not found' };
278
+
279
+ await figma.loadFontAsync({ family: "Inter", style: "Medium" });
280
+
281
+ if (node.type === 'STICKY' || node.type === 'SHAPE_WITH_TEXT') {
282
+ node.text.characters = ${JSON.stringify(text)};
283
+ } else if (node.type === 'TEXT') {
284
+ node.characters = ${JSON.stringify(text)};
285
+ } else {
286
+ return { error: 'Node does not support text' };
287
+ }
288
+ return { id: node.id, updated: true };
289
+ })()
290
+ `);
291
+ }
292
+
293
+ close() {
294
+ if (this.ws) {
295
+ this.ws.close();
296
+ this.ws = null;
297
+ }
298
+ }
299
+ }
300
+
301
+ function hexToRgb(hex) {
302
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
303
+ return result ? {
304
+ r: parseInt(result[1], 16) / 255,
305
+ g: parseInt(result[2], 16) / 255,
306
+ b: parseInt(result[3], 16) / 255
307
+ } : { r: 1, g: 0.9, b: 0.5 }; // default yellow
308
+ }
309
+
310
+ export default FigJamClient;