figctl 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/LICENSE +21 -0
- package/README.md +446 -0
- package/bin/figctl.js +41 -0
- package/package.json +60 -0
- package/postinstall.js +266 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tiaan du Plessis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
# figctl
|
|
2
|
+
|
|
3
|
+
One static binary that reads a Figma file and hands a coding agent everything it needs to implement the design.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/tiaanduplessis/figctl/blob/main/LICENSE)
|
|
6
|
+
[](https://github.com/tiaanduplessis/figctl)
|
|
7
|
+
|
|
8
|
+
Create a Figma personal access token with `current_user:read`,
|
|
9
|
+
`file_content:read`, `file_metadata:read`, and `library_content:read`.
|
|
10
|
+
See [Tokens and scopes](#tokens-and-scopes) for optional features.
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install --save-dev figctl
|
|
14
|
+
npx figctl version
|
|
15
|
+
npx figctl auth login personal --default
|
|
16
|
+
npx figctl file tree "https://www.figma.com/design/KEY/Web-App"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Paste the token into the hidden login prompt. Replace the URL with a file your
|
|
20
|
+
account can access. `KEY` is the segment after `/design/` or `/file/`.
|
|
21
|
+
The tree prints node ids and names; choose a frame id and replace `2:2`:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npx figctl node context KEY --node 2:2
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The result includes layout and style data and downloaded asset paths.
|
|
28
|
+
Read any `hints` for optional data unavailable with your token or plan.
|
|
29
|
+
|
|
30
|
+
figctl is a Go binary. This package downloads the release build for your
|
|
31
|
+
platform and runs it through a shim, so there is no toolchain to install, no
|
|
32
|
+
native module to compile, and no native runtime dependency. The npm shim requires Node.js 18 or later.
|
|
33
|
+
|
|
34
|
+
Installing it as a dev dependency pins the version in `package.json`, so every
|
|
35
|
+
machine and CI job in a repository gets the same figctl. That matters more than
|
|
36
|
+
usual here: an agent depends on the shape of the output, so a tool that silently
|
|
37
|
+
changes underneath it changes the agent's behaviour.
|
|
38
|
+
|
|
39
|
+
## Contents
|
|
40
|
+
|
|
41
|
+
- [Why this exists](#why-this-exists)
|
|
42
|
+
- [The workflow](#the-workflow)
|
|
43
|
+
- [Teaching your agent to use it](#teaching-your-agent-to-use-it)
|
|
44
|
+
- [Command surface](#command-surface)
|
|
45
|
+
- [Output contract](#output-contract)
|
|
46
|
+
- [Exit codes and error codes](#exit-codes-and-error-codes)
|
|
47
|
+
- [Tokens and scopes](#tokens-and-scopes)
|
|
48
|
+
- [Working across several Figma accounts](#working-across-several-figma-accounts)
|
|
49
|
+
- [Caching and rate limits](#caching-and-rate-limits)
|
|
50
|
+
- [What this package does on install](#what-this-package-does-on-install)
|
|
51
|
+
- [Other ways to install](#other-ways-to-install)
|
|
52
|
+
- [Documentation](#documentation)
|
|
53
|
+
|
|
54
|
+
## Why this exists
|
|
55
|
+
|
|
56
|
+
An agent that can run a shell can implement a Figma design end to end if it can
|
|
57
|
+
answer six questions cheaply: what is in this file, what does this node look
|
|
58
|
+
like, what are the exact layout and style values, which design token does each
|
|
59
|
+
value come from, where are the assets, and what did the designer say about it.
|
|
60
|
+
figctl answers all six from the public Figma REST API and prints JSON an agent
|
|
61
|
+
can parse.
|
|
62
|
+
|
|
63
|
+
### CLI or MCP
|
|
64
|
+
|
|
65
|
+
figctl exposes explicit shell commands and JSON output for scripts and coding
|
|
66
|
+
agents. It uses a personal access token and can run without the Figma desktop
|
|
67
|
+
app, including in CI when credentials and network access are available.
|
|
68
|
+
|
|
69
|
+
[Figma's MCP server](https://developers.figma.com/docs/figma-mcp-server/) connects
|
|
70
|
+
supported agents to design context, Code Connect, and canvas-writing tools.
|
|
71
|
+
Figma recommends its hosted remote server, which also needs no desktop app;
|
|
72
|
+
a desktop server is available for local workflows.
|
|
73
|
+
|
|
74
|
+
Choose figctl when you want commands you can inspect, pipe, cache, and pin to a
|
|
75
|
+
version. Choose Figma MCP when you want its native agent integration and Figma
|
|
76
|
+
features. Authentication, available tools, and client support differ; consult
|
|
77
|
+
Figma's documentation for current MCP requirements.
|
|
78
|
+
|
|
79
|
+
## The workflow
|
|
80
|
+
|
|
81
|
+
### 1. Outline the file
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
npx figctl file tree KEY -o md
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
- 0:1 CANVAS "Screens" (1 children)
|
|
89
|
+
- 2:1 SECTION "Onboarding" 470x964 @-40,-80 (1 children) [dev=READY_FOR_DEV]
|
|
90
|
+
- 1:2 CANVAS "Design System" (3 children)
|
|
91
|
+
- 3:10 COMPONENT_SET "Button" 368x132 @0,0 (4 children) [component]
|
|
92
|
+
- 3:20 COMPONENT "Input/Text" 342x56 @0,160 (3 children) [component, layout=column]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Default depth is 2, so one call shows the pages and their top-level frames.
|
|
96
|
+
Narrow with `--node`, `--page`, `--depth`, `--type`, `--name`, `--visible-only`.
|
|
97
|
+
Search the whole document with `figctl file find KEY --name "Login*"` or
|
|
98
|
+
`--text "sign in"`.
|
|
99
|
+
|
|
100
|
+
### 2. Get everything needed for one node
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
npx figctl node context KEY --node 2:2
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
One call returns the normalized model, a PNG screenshot written to disk, SVG
|
|
107
|
+
exports of the icon-like layers, the raster image fills, only the design tokens
|
|
108
|
+
that subtree actually uses, the component and variant definitions of every
|
|
109
|
+
instance, the distances the designer pinned in Dev Mode, the comments pinned on
|
|
110
|
+
the node, and the Dev Mode resource links.
|
|
111
|
+
|
|
112
|
+
````
|
|
113
|
+
## Node: Login (2:2, FRAME)
|
|
114
|
+
|
|
115
|
+
- page: Screens
|
|
116
|
+
- path: Screens / Onboarding / Login
|
|
117
|
+
- size: 390 x 844
|
|
118
|
+
|
|
119
|
+
#### 2:2 Login (FRAME)
|
|
120
|
+
|
|
121
|
+
- layout: column; gap 16px [space/4]; padding 24px; w fixed 390px; h fixed 844px; clips
|
|
122
|
+
- fills: #ffffff [bg/surface]
|
|
123
|
+
- effects: box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1) {shadow/md}
|
|
124
|
+
|
|
125
|
+
```css
|
|
126
|
+
background: var(--bg-surface);
|
|
127
|
+
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
|
|
128
|
+
display: flex;
|
|
129
|
+
flex-direction: column;
|
|
130
|
+
gap: var(--space-4);
|
|
131
|
+
padding: 24px;
|
|
132
|
+
```
|
|
133
|
+
````
|
|
134
|
+
|
|
135
|
+
Square brackets are variables, braces are styles. The `token` field has three
|
|
136
|
+
states: a `name` means it is resolved, a bare `variableId` with
|
|
137
|
+
`"unresolved": true` means a variable governs the value but naming it needs an
|
|
138
|
+
Enterprise plan, and `null` means the designer typed it by hand. Only the last
|
|
139
|
+
is safe to hardcode.
|
|
140
|
+
|
|
141
|
+
Trim the payload with `--depth`, `--max-nodes`, `--no-assets`, `--no-comments`,
|
|
142
|
+
`--no-screenshot`, `--no-css`.
|
|
143
|
+
|
|
144
|
+
### 3. Implement it
|
|
145
|
+
|
|
146
|
+
The agent writes the code. figctl deliberately does not generate framework
|
|
147
|
+
code: the agent knows the repository's conventions and figctl does not.
|
|
148
|
+
|
|
149
|
+
### 4. Compare the result
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
npx figctl render KEY --node 2:2 --scale 2 --out ./design
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Renders to PNG, JPG, SVG, or PDF and prints absolute paths, so the image can go
|
|
156
|
+
straight into a vision tool beside a screenshot of the built component.
|
|
157
|
+
|
|
158
|
+
### 5. Wire up the design system once
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
npx figctl tokens export KEY --format css --out ./src/styles
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```css
|
|
165
|
+
:root {
|
|
166
|
+
--color-brand-500: #3366ff;
|
|
167
|
+
--space-4: 16px;
|
|
168
|
+
--bg-surface: var(--neutral-0);
|
|
169
|
+
--shadow-md: 0px 4px 12px rgba(0, 0, 0, 0.1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
[data-theme="dark"] {
|
|
173
|
+
--bg-surface: #111827;
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Also `--format dtcg` (the default, the W3C Design Tokens Format Module 2025.10),
|
|
178
|
+
`--format style-dictionary` (the same bytes, consumed by Style Dictionary v4),
|
|
179
|
+
`--format tailwind`, and `--format json`. Variables resolve across every mode,
|
|
180
|
+
so light and dark come out of one export.
|
|
181
|
+
|
|
182
|
+
## Teaching your agent to use it
|
|
183
|
+
|
|
184
|
+
figctl ships its own instructions, so an agent learns the workflow without you
|
|
185
|
+
writing a prompt:
|
|
186
|
+
|
|
187
|
+
```sh
|
|
188
|
+
npx figctl skill install --agent claude # or: agents, cursor, copilot, codex, all
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
That writes a `SKILL.md` plus a generated command and output-schema reference
|
|
192
|
+
into `.agents/skills/figctl/`, the location Codex and other clients read, and
|
|
193
|
+
links `.claude/skills/figctl` to it. Add `--project` to install into the
|
|
194
|
+
repository instead of your home directory, and `--dry-run` to see what would be
|
|
195
|
+
written first.
|
|
196
|
+
|
|
197
|
+
The skill is also published in the repository, so it can be installed without
|
|
198
|
+
figctl being present yet:
|
|
199
|
+
|
|
200
|
+
```sh
|
|
201
|
+
npx skills add tiaanduplessis/figctl
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Command surface
|
|
205
|
+
|
|
206
|
+
Run `npx figctl <command> --help`, or read the
|
|
207
|
+
[generated reference](https://github.com/tiaanduplessis/figctl/blob/main/docs/commands.md).
|
|
208
|
+
|
|
209
|
+
| Command | What it does |
|
|
210
|
+
| --- | --- |
|
|
211
|
+
| `file info`, `tree`, `find`, `get` | file metadata, sparse outline, search, raw Figma JSON |
|
|
212
|
+
| `node inspect`, `context` | normalized model of nodes; everything needed to implement one node |
|
|
213
|
+
| `render` | nodes to PNG, JPG, SVG, or PDF files |
|
|
214
|
+
| `assets list`, `export` | icons, export-marked layers, and raster image fills |
|
|
215
|
+
| `tokens resolve`, `export` | one variable or style across modes; DTCG, CSS, Tailwind, JSON |
|
|
216
|
+
| `variables list`, `get` | raw variables and collections per mode (Enterprise) |
|
|
217
|
+
| `styles list`, `get` | styles with resolved values, for files and team libraries |
|
|
218
|
+
| `components list`, `get` | components, sets, and variant property definitions |
|
|
219
|
+
| `comments list`, `add` | designer intent, and leaving implementation notes |
|
|
220
|
+
| `devresources list`, `add`, `update`, `remove` | Dev Mode links, for pointing a component at its Storybook story |
|
|
221
|
+
| `versions list` | saved file versions, for use with `--file-version` |
|
|
222
|
+
| `projects`, `folders` | discovery through the projects and folders APIs |
|
|
223
|
+
| `profile add`, `list`, `use`, `show`, `remove` | named accounts |
|
|
224
|
+
| `auth login`, `logout`, `status`, `scopes` | tokens and which scopes to request |
|
|
225
|
+
| `skill install`, `print`, `uninstall` | write the agent skill files |
|
|
226
|
+
| `cache status`, `clear` | inspect and clear the on-disk cache |
|
|
227
|
+
| `me`, `init`, `schema`, `version`, `completion` | account, project setup, output schemas, build metadata, shell completion |
|
|
228
|
+
|
|
229
|
+
`<ref>` is a file key or any Figma URL. A `node-id=1-2` in the URL is converted
|
|
230
|
+
to `1:2` and used as the default `--node`, so an agent can paste the URL from
|
|
231
|
+
the browser unchanged.
|
|
232
|
+
|
|
233
|
+
## Output contract
|
|
234
|
+
|
|
235
|
+
Output is JSON when stdout is not a terminal, which is what an agent gets, and a
|
|
236
|
+
table when it is. `--json` forces JSON, `-o md` produces markdown for agents that
|
|
237
|
+
read tool output as text, `-o plain` produces tab-separated rows.
|
|
238
|
+
|
|
239
|
+
Every success is one envelope:
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{
|
|
243
|
+
"schemaVersion": 1,
|
|
244
|
+
"command": "file.tree",
|
|
245
|
+
"profile": {"name": "acme", "handle": "Jane"},
|
|
246
|
+
"file": {"key": "KEY", "name": "Web App", "version": "2100123456"},
|
|
247
|
+
"data": [],
|
|
248
|
+
"truncated": false,
|
|
249
|
+
"nextCursor": null,
|
|
250
|
+
"hints": ["Next: figctl node context KEY --node <id> to implement a node."]
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Every failure is one envelope too, on stdout in JSON mode:
|
|
255
|
+
|
|
256
|
+
```json
|
|
257
|
+
{
|
|
258
|
+
"schemaVersion": 1,
|
|
259
|
+
"error": {
|
|
260
|
+
"code": "NOT_FOUND",
|
|
261
|
+
"message": "file NOSUCHFILEKEY000000001 not found",
|
|
262
|
+
"hint": "Check the key or id and that the active profile has access.",
|
|
263
|
+
"httpStatus": 404
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Data goes to stdout; progress, warnings, and the `--verbose` HTTP trace go to
|
|
269
|
+
stderr. Read `hints` on every call: they say what was degraded, what was
|
|
270
|
+
truncated, and what to run next. `schemaVersion` changes only on a breaking
|
|
271
|
+
change. `npx figctl schema <command>` prints the JSON Schema of any command's
|
|
272
|
+
payload, so field names never have to be guessed, and a test in the repository
|
|
273
|
+
fails if a payload ever drifts from the schema it advertises.
|
|
274
|
+
|
|
275
|
+
## Exit codes and error codes
|
|
276
|
+
|
|
277
|
+
| Exit | Meaning |
|
|
278
|
+
| --- | --- |
|
|
279
|
+
| 0 | success |
|
|
280
|
+
| 1 | runtime or API error |
|
|
281
|
+
| 2 | usage error |
|
|
282
|
+
| 3 | auth error |
|
|
283
|
+
| 4 | not found |
|
|
284
|
+
| 5 | rate limited |
|
|
285
|
+
| 6 | partial success: `data` is usable, `failures` lists what did not work |
|
|
286
|
+
|
|
287
|
+
Exit 6 is not a failure to retry. The good results are already on stdout.
|
|
288
|
+
|
|
289
|
+
| Error code | Exit | Meaning |
|
|
290
|
+
| --- | --- | --- |
|
|
291
|
+
| `USAGE` | 2 | bad flags or arguments |
|
|
292
|
+
| `AUTH_MISSING` | 3 | no profile or token selected |
|
|
293
|
+
| `AUTH_INVALID` | 3 | the token was rejected or has expired |
|
|
294
|
+
| `AUTH_SCOPE` | 3 | the token lacks a scope; the message names it |
|
|
295
|
+
| `NOT_FOUND` | 4 | file, node, page, or style does not exist |
|
|
296
|
+
| `FORBIDDEN` | 1 | the account cannot see this resource |
|
|
297
|
+
| `RATE_LIMITED` | 5 | carries `retryAfterSeconds` |
|
|
298
|
+
| `PLAN_REQUIRED` | 1 | variables need Enterprise; the hint names the styles fallback |
|
|
299
|
+
| `RENDER_FAILED` | 1 | Figma could not render any requested node |
|
|
300
|
+
| `PARTIAL` | 6 | some items succeeded, some failed |
|
|
301
|
+
| `NETWORK` | 1 | connection or timeout |
|
|
302
|
+
| `INTERNAL` | 1 | a bug in figctl |
|
|
303
|
+
|
|
304
|
+
## Tokens and scopes
|
|
305
|
+
|
|
306
|
+
figctl uses a Figma personal access token, sent in the `X-Figma-Token` header.
|
|
307
|
+
Create one in Figma under Settings, Security, Personal access tokens. Scopes are
|
|
308
|
+
chosen when the token is created and cannot be changed afterwards, and tokens
|
|
309
|
+
expire after at most 90 days.
|
|
310
|
+
|
|
311
|
+
Run `npx figctl auth scopes` for the current list. The ones marked required
|
|
312
|
+
cover the core workflow:
|
|
313
|
+
|
|
314
|
+
| Scope | Required | Used by |
|
|
315
|
+
| --- | --- | --- |
|
|
316
|
+
| `file_content:read` | yes | `file`, `node`, `render`, `assets export` |
|
|
317
|
+
| `file_metadata:read` | yes | `file info`, cache validation |
|
|
318
|
+
| `library_content:read` | yes | `components list`, `styles list` for a file |
|
|
319
|
+
| `current_user:read` | yes | `me`, `auth status` |
|
|
320
|
+
| `library_assets:read` | no | `components get --key`, `styles get --key` |
|
|
321
|
+
| `team_library_content:read` | no | `components`, `styles` with `--team` |
|
|
322
|
+
| `file_variables:read` | no | `variables`, `tokens export`. Enterprise plan only |
|
|
323
|
+
| `file_dev_resources:read` | no | `devresources list`, `node context` |
|
|
324
|
+
| `file_dev_resources:write` | no | `devresources add`, `update`, `remove` |
|
|
325
|
+
| `file_comments:read` | no | `comments list`, `node context` |
|
|
326
|
+
| `file_comments:write` | no | `comments add` only |
|
|
327
|
+
| `file_versions:read` | no | `versions list`, `--file-version` |
|
|
328
|
+
| `projects:read`, `folders:read` | no | project and folder discovery |
|
|
329
|
+
|
|
330
|
+
The old blanket `files:read` scope is deprecated; request the granular scopes.
|
|
331
|
+
|
|
332
|
+
Variables need an Enterprise plan and a full seat. The `file_variables:read`
|
|
333
|
+
scope is not offered on the token screen on other plans, so a token cannot
|
|
334
|
+
carry it at all. `tokens export` still exports the styles and says so in
|
|
335
|
+
`hints` rather than failing.
|
|
336
|
+
|
|
337
|
+
In CI, set `FIGMA_TOKEN` and figctl uses it as an implicit profile:
|
|
338
|
+
|
|
339
|
+
```yaml
|
|
340
|
+
- run: npx figctl tokens export "$FIGMA_FILE" --format css --out ./src/styles
|
|
341
|
+
env:
|
|
342
|
+
FIGMA_TOKEN: ${{ secrets.FIGMA_TOKEN }}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Working across several Figma accounts
|
|
346
|
+
|
|
347
|
+
A consultant has one Figma account per client, often on different plans. A
|
|
348
|
+
profile is a name plus a token; tokens live in the OS keychain and the config
|
|
349
|
+
file holds no secrets.
|
|
350
|
+
|
|
351
|
+
```sh
|
|
352
|
+
npx figctl profile add acme --team 555000111 --default
|
|
353
|
+
npx figctl init --profile acme --file app=KEY1 --file design-system=KEY2 --default app
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
That writes a `.figctl.yaml` naming the profile and the Figma files this
|
|
357
|
+
repository uses, with no secret in it. A repository usually refers to more than
|
|
358
|
+
one, because a design system lives in its own file, and a configured name then
|
|
359
|
+
stands in for a key wherever a command takes a ref:
|
|
360
|
+
|
|
361
|
+
```sh
|
|
362
|
+
npx figctl tokens export design-system --format css --out ./src/styles
|
|
363
|
+
npx figctl node context --node 2:2 # the default file
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Commands
|
|
367
|
+
walk up from the working directory to find it, so an agent working in the Acme
|
|
368
|
+
repository uses the Acme token without being told, and an agent in another
|
|
369
|
+
repository cannot reach Acme's files. Every envelope carries the profile it ran
|
|
370
|
+
as, so the agent can check the account before doing work.
|
|
371
|
+
|
|
372
|
+
The active profile is the first of these that matches: `--profile`,
|
|
373
|
+
`FIGCTL_PROFILE`, `FIGMA_TOKEN`, the nearest `.figctl.yaml`, then the user-level
|
|
374
|
+
default. `npx figctl auth status` reports which was chosen and why. The cache is
|
|
375
|
+
keyed by profile, so one client's data never mixes with another's.
|
|
376
|
+
|
|
377
|
+
## Caching and rate limits
|
|
378
|
+
|
|
379
|
+
Figma's rate limits are tiered per endpoint, and the tier figctl needs most is
|
|
380
|
+
the tightest:
|
|
381
|
+
|
|
382
|
+
| Tier | Endpoints | Limit |
|
|
383
|
+
| --- | --- | --- |
|
|
384
|
+
| 1 | get file, get nodes, render images | 10 to 30 per minute on Dev and Full seats depending on plan; 20 per **month** on View and Collab seats |
|
|
385
|
+
| 2 | variables, versions, dev resources, comments, image fills, folders | 25 to 150 per minute |
|
|
386
|
+
| 3 | components, styles, file meta, users | 50 to 200 per minute |
|
|
387
|
+
|
|
388
|
+
Ten Tier 1 requests a minute is not enough to walk a file node by node, so
|
|
389
|
+
figctl fetches the whole document once per version, caches it per profile, and
|
|
390
|
+
answers `file tree`, `file find`, `node inspect`, and `node context` from the
|
|
391
|
+
local copy. Freshness is checked against the cheap Tier 3 metadata endpoint.
|
|
392
|
+
|
|
393
|
+
Very large files cannot be fetched whole at all: Figma refuses them with a 400.
|
|
394
|
+
figctl degrades to depth-limited and per-node requests rather than failing.
|
|
395
|
+
|
|
396
|
+
`--refresh` re-fetches, `--no-cache` bypasses the cache, and `--file-version`
|
|
397
|
+
pins a version so it never has to be validated. A 429 is retried up to three
|
|
398
|
+
times honouring `Retry-After`.
|
|
399
|
+
|
|
400
|
+
## What this package does on install
|
|
401
|
+
|
|
402
|
+
1. Works out the platform: `darwin`, `linux`, or `win32`, on `x64` or `arm64`.
|
|
403
|
+
Anything else fails with a message naming the supported platforms and the
|
|
404
|
+
other ways to install.
|
|
405
|
+
2. Downloads the matching archive and `checksums.txt` from the GitHub release
|
|
406
|
+
for this package's version.
|
|
407
|
+
3. Verifies the archive against its `sha256` line in `checksums.txt`. A mismatch
|
|
408
|
+
aborts the install and writes nothing.
|
|
409
|
+
4. Unpacks the `figctl` binary next to the shim. No native modules, no
|
|
410
|
+
post-install compilation, no runtime dependencies.
|
|
411
|
+
|
|
412
|
+
| Variable | Effect |
|
|
413
|
+
| --- | --- |
|
|
414
|
+
| `FIGCTL_SKIP_DOWNLOAD=1` | skip the download; set `FIGCTL_BINARY` to an existing executable when running figctl |
|
|
415
|
+
| `FIGCTL_BINARY=/path/to/figctl` | copy an existing binary during install; also select an executable at runtime |
|
|
416
|
+
| `FIGCTL_DOWNLOAD_BASE=URL` | download the assets from somewhere other than GitHub releases |
|
|
417
|
+
|
|
418
|
+
Behind a proxy or on an air-gapped network, download the release archive
|
|
419
|
+
yourself and point `FIGCTL_BINARY` at the extracted binary.
|
|
420
|
+
|
|
421
|
+
The release workflow signs `checksums.txt` with [cosign](https://docs.sigstore.dev/).
|
|
422
|
+
This wrapper checks SHA-256 checksums; it does not verify the cosign signature.
|
|
423
|
+
For signature verification, follow the repository release instructions before
|
|
424
|
+
installing a downloaded binary with `FIGCTL_BINARY`.
|
|
425
|
+
|
|
426
|
+
## Other ways to install
|
|
427
|
+
|
|
428
|
+
```sh
|
|
429
|
+
curl -fsSL https://raw.githubusercontent.com/tiaanduplessis/figctl/main/install.sh | sh
|
|
430
|
+
go install github.com/tiaanduplessis/figctl/cmd/figctl@latest
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
## Documentation
|
|
434
|
+
|
|
435
|
+
- [README](https://github.com/tiaanduplessis/figctl#readme)
|
|
436
|
+
- [Command reference](https://github.com/tiaanduplessis/figctl/blob/main/docs/commands.md)
|
|
437
|
+
- [Wiring figctl into an agent](https://github.com/tiaanduplessis/figctl/blob/main/docs/agents.md)
|
|
438
|
+
- [Design tokens](https://github.com/tiaanduplessis/figctl/blob/main/docs/design-tokens.md)
|
|
439
|
+
- [Profiles](https://github.com/tiaanduplessis/figctl/blob/main/docs/profiles.md)
|
|
440
|
+
- [Caching and rate limits](https://github.com/tiaanduplessis/figctl/blob/main/docs/caching.md)
|
|
441
|
+
- [Changelog](https://github.com/tiaanduplessis/figctl/blob/main/CHANGELOG.md)
|
|
442
|
+
- [Issues](https://github.com/tiaanduplessis/figctl/issues)
|
|
443
|
+
|
|
444
|
+
## License
|
|
445
|
+
|
|
446
|
+
MIT. Copyright Tiaan du Plessis.
|
package/bin/figctl.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Thin shim: run the figctl binary that postinstall.js downloaded next to
|
|
3
|
+
// this file, forwarding arguments, stdio, signals, and the exit code.
|
|
4
|
+
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
const fs = require("node:fs");
|
|
8
|
+
const path = require("node:path");
|
|
9
|
+
const { spawnSync } = require("node:child_process");
|
|
10
|
+
|
|
11
|
+
const binary = process.env.FIGCTL_BINARY || path.join(
|
|
12
|
+
__dirname,
|
|
13
|
+
process.platform === "win32" ? "figctl.exe" : "figctl"
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(binary)) {
|
|
17
|
+
process.stderr.write(
|
|
18
|
+
[
|
|
19
|
+
"",
|
|
20
|
+
"figctl: no executable found; install it or set FIGCTL_BINARY to its path.",
|
|
21
|
+
"",
|
|
22
|
+
"Re-run the install, or get figctl another way:",
|
|
23
|
+
" npm rebuild figctl",
|
|
24
|
+
" go install github.com/tiaanduplessis/figctl/cmd/figctl@latest",
|
|
25
|
+
" https://github.com/tiaanduplessis/figctl/releases",
|
|
26
|
+
"",
|
|
27
|
+
].join("\n") + "\n"
|
|
28
|
+
);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const result = spawnSync(binary, process.argv.slice(2), { stdio: "inherit" });
|
|
33
|
+
|
|
34
|
+
if (result.error) {
|
|
35
|
+
process.stderr.write("figctl: " + result.error.message + "\n");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
if (result.signal) {
|
|
39
|
+
process.kill(process.pid, result.signal);
|
|
40
|
+
}
|
|
41
|
+
process.exit(result.status === null ? 1 : result.status);
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "figctl",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Agent-first Figma CLI: read a Figma design and everything needed to implement it",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"figma",
|
|
7
|
+
"figma-api",
|
|
8
|
+
"cli",
|
|
9
|
+
"design-tokens",
|
|
10
|
+
"dtcg",
|
|
11
|
+
"design-system",
|
|
12
|
+
"design-to-code",
|
|
13
|
+
"ai-agent",
|
|
14
|
+
"agent-skills",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"codex",
|
|
17
|
+
"style-dictionary",
|
|
18
|
+
"tailwind",
|
|
19
|
+
"storybook",
|
|
20
|
+
"figma-export",
|
|
21
|
+
"screenshot"
|
|
22
|
+
],
|
|
23
|
+
"homepage": "https://github.com/tiaanduplessis/figctl#readme",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/tiaanduplessis/figctl/issues"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/tiaanduplessis/figctl.git",
|
|
30
|
+
"directory": "npm"
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"author": "Tiaan du Plessis",
|
|
34
|
+
"type": "commonjs",
|
|
35
|
+
"bin": {
|
|
36
|
+
"figctl": "bin/figctl.js"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"bin/figctl.js",
|
|
40
|
+
"postinstall.js",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"postinstall": "node postinstall.js",
|
|
46
|
+
"test": "node --test"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18"
|
|
50
|
+
},
|
|
51
|
+
"os": [
|
|
52
|
+
"darwin",
|
|
53
|
+
"linux",
|
|
54
|
+
"win32"
|
|
55
|
+
],
|
|
56
|
+
"cpu": [
|
|
57
|
+
"x64",
|
|
58
|
+
"arm64"
|
|
59
|
+
]
|
|
60
|
+
}
|
package/postinstall.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Downloads the figctl release binary for this platform, verifies it against
|
|
3
|
+
// the release checksums.txt, and unpacks it next to this script.
|
|
4
|
+
//
|
|
5
|
+
// No dependencies: node:https, node:fs, node:zlib, and node:crypto only.
|
|
6
|
+
//
|
|
7
|
+
// Environment overrides:
|
|
8
|
+
// FIGCTL_SKIP_DOWNLOAD=1 skip downloading; set FIGCTL_BINARY to run an existing binary
|
|
9
|
+
// FIGCTL_BINARY=/path use an existing binary instead of downloading
|
|
10
|
+
// FIGCTL_DOWNLOAD_BASE=URL download from somewhere other than GitHub releases
|
|
11
|
+
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
const fs = require("node:fs");
|
|
15
|
+
const path = require("node:path");
|
|
16
|
+
const zlib = require("node:zlib");
|
|
17
|
+
const https = require("node:https");
|
|
18
|
+
const crypto = require("node:crypto");
|
|
19
|
+
|
|
20
|
+
const pkg = require("./package.json");
|
|
21
|
+
|
|
22
|
+
// Every GoReleaser target in .goreleaser.yml. Adding a target there means
|
|
23
|
+
// adding it here; a Go test asserts the two agree.
|
|
24
|
+
const PLATFORMS = {
|
|
25
|
+
"darwin-x64": { os: "darwin", arch: "amd64", ext: "tar.gz", bin: "figctl" },
|
|
26
|
+
"darwin-arm64": { os: "darwin", arch: "arm64", ext: "tar.gz", bin: "figctl" },
|
|
27
|
+
"linux-x64": { os: "linux", arch: "amd64", ext: "tar.gz", bin: "figctl" },
|
|
28
|
+
"linux-arm64": { os: "linux", arch: "arm64", ext: "tar.gz", bin: "figctl" },
|
|
29
|
+
"win32-x64": { os: "windows", arch: "amd64", ext: "zip", bin: "figctl.exe" },
|
|
30
|
+
"win32-arm64": { os: "windows", arch: "arm64", ext: "zip", bin: "figctl.exe" },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const REPO = "tiaanduplessis/figctl";
|
|
34
|
+
const VERSION = pkg.version;
|
|
35
|
+
const TAG = "v" + VERSION;
|
|
36
|
+
const BASE =
|
|
37
|
+
process.env.FIGCTL_DOWNLOAD_BASE ||
|
|
38
|
+
`https://github.com/${REPO}/releases/download/${TAG}`;
|
|
39
|
+
|
|
40
|
+
function fail(message, detail) {
|
|
41
|
+
const lines = ["", "figctl: " + message];
|
|
42
|
+
if (detail) {
|
|
43
|
+
lines.push("");
|
|
44
|
+
lines.push(detail);
|
|
45
|
+
}
|
|
46
|
+
lines.push("");
|
|
47
|
+
lines.push("Install it another way instead:");
|
|
48
|
+
lines.push(" go install github.com/tiaanduplessis/figctl/cmd/figctl@latest");
|
|
49
|
+
lines.push(` https://github.com/${REPO}/releases/tag/${TAG}`);
|
|
50
|
+
lines.push("");
|
|
51
|
+
throw new Error(lines.join("\n"));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function platformKey() {
|
|
55
|
+
return `${process.platform}-${process.arch}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function target() {
|
|
59
|
+
const key = platformKey();
|
|
60
|
+
const t = PLATFORMS[key];
|
|
61
|
+
if (!t) {
|
|
62
|
+
fail(
|
|
63
|
+
`no prebuilt binary for ${process.platform} ${process.arch}`,
|
|
64
|
+
"Supported platforms: " + Object.keys(PLATFORMS).sort().join(", ")
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return t;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// get follows redirects and resolves with the response body as a Buffer.
|
|
71
|
+
function get(url, redirects = 0) {
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
if (redirects > 10) {
|
|
74
|
+
reject(new Error("too many redirects for " + url));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const req = https.get(
|
|
78
|
+
url,
|
|
79
|
+
{ headers: { "user-agent": `figctl-npm/${VERSION}` } },
|
|
80
|
+
(res) => {
|
|
81
|
+
const status = res.statusCode || 0;
|
|
82
|
+
if (status >= 300 && status < 400 && res.headers.location) {
|
|
83
|
+
res.resume();
|
|
84
|
+
resolve(get(new URL(res.headers.location, url).toString(), redirects + 1));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (status !== 200) {
|
|
88
|
+
res.resume();
|
|
89
|
+
reject(new Error(`HTTP ${status} for ${url}`));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const chunks = [];
|
|
93
|
+
res.on("data", (c) => chunks.push(c));
|
|
94
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
95
|
+
res.on("error", reject);
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
req.on("error", reject);
|
|
99
|
+
req.setTimeout(120000, () => {
|
|
100
|
+
req.destroy(new Error("timed out after 120s downloading " + url));
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// verify checks the archive against the sha256 line for it in checksums.txt.
|
|
106
|
+
function verify(checksums, assetName, archive) {
|
|
107
|
+
let want = null;
|
|
108
|
+
for (const line of checksums.toString("utf8").split("\n")) {
|
|
109
|
+
const m = line.trim().match(/^([0-9a-f]{64})\s+\*?(.+)$/);
|
|
110
|
+
if (m && m[2] === assetName) {
|
|
111
|
+
want = m[1];
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!want) {
|
|
116
|
+
fail(
|
|
117
|
+
`${assetName} is not listed in checksums.txt`,
|
|
118
|
+
"The release assets look incomplete. This may be a partially published release."
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
const got = crypto.createHash("sha256").update(archive).digest("hex");
|
|
122
|
+
if (got !== want) {
|
|
123
|
+
fail(
|
|
124
|
+
"checksum mismatch for " + assetName,
|
|
125
|
+
`expected sha256 ${want}\nreceived sha256 ${got}\n\nThe download was corrupted or tampered with. Nothing was installed.`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// extractTarGz returns the contents of one file from a gzipped tar archive.
|
|
131
|
+
// Only the ustar fields figctl's archives use are read.
|
|
132
|
+
function extractTarGz(buf, name) {
|
|
133
|
+
const tar = zlib.gunzipSync(buf);
|
|
134
|
+
let offset = 0;
|
|
135
|
+
while (offset + 512 <= tar.length) {
|
|
136
|
+
const header = tar.subarray(offset, offset + 512);
|
|
137
|
+
if (header.every((b) => b === 0)) {
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
const entry = header.subarray(0, 100).toString("utf8").replace(/\0.*$/, "");
|
|
141
|
+
const sizeField = header.subarray(124, 136).toString("utf8").replace(/\0.*$/, "").trim();
|
|
142
|
+
if (!/^[0-7]+$/.test(sizeField)) {
|
|
143
|
+
throw new Error("invalid tar entry size");
|
|
144
|
+
}
|
|
145
|
+
const size = parseInt(sizeField, 8);
|
|
146
|
+
const typeFlag = String.fromCharCode(header[156]);
|
|
147
|
+
const start = offset + 512;
|
|
148
|
+
if (!Number.isSafeInteger(size) || start + size > tar.length) {
|
|
149
|
+
throw new Error("truncated tar entry");
|
|
150
|
+
}
|
|
151
|
+
if ((typeFlag === "0" || typeFlag === "\0") && path.posix.basename(entry) === name) {
|
|
152
|
+
return tar.subarray(start, start + size);
|
|
153
|
+
}
|
|
154
|
+
offset = start + Math.ceil(size / 512) * 512;
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// extractZip returns the contents of one file from a zip archive, reading the
|
|
160
|
+
// end-of-central-directory record and inflating the entry it points at.
|
|
161
|
+
function extractZip(buf, name) {
|
|
162
|
+
let eocd = -1;
|
|
163
|
+
for (let i = buf.length - 22; i >= 0 && i >= buf.length - 65557; i--) {
|
|
164
|
+
if (buf.readUInt32LE(i) === 0x06054b50) {
|
|
165
|
+
eocd = i;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (eocd < 0) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const count = buf.readUInt16LE(eocd + 10);
|
|
173
|
+
let p = buf.readUInt32LE(eocd + 16);
|
|
174
|
+
for (let i = 0; i < count; i++) {
|
|
175
|
+
if (p + 46 > eocd || buf.readUInt32LE(p) !== 0x02014b50) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const method = buf.readUInt16LE(p + 10);
|
|
179
|
+
const compressedSize = buf.readUInt32LE(p + 20);
|
|
180
|
+
const nameLen = buf.readUInt16LE(p + 28);
|
|
181
|
+
const extraLen = buf.readUInt16LE(p + 30);
|
|
182
|
+
const commentLen = buf.readUInt16LE(p + 32);
|
|
183
|
+
const localOffset = buf.readUInt32LE(p + 42);
|
|
184
|
+
if (p + 46 + nameLen + extraLen + commentLen > eocd) {
|
|
185
|
+
throw new Error("truncated zip directory");
|
|
186
|
+
}
|
|
187
|
+
const entry = buf.subarray(p + 46, p + 46 + nameLen).toString("utf8");
|
|
188
|
+
if (path.posix.basename(entry) === name) {
|
|
189
|
+
if (localOffset + 30 > p || buf.readUInt32LE(localOffset) !== 0x04034b50) {
|
|
190
|
+
throw new Error("invalid zip entry");
|
|
191
|
+
}
|
|
192
|
+
const localNameLen = buf.readUInt16LE(localOffset + 26);
|
|
193
|
+
const localExtraLen = buf.readUInt16LE(localOffset + 28);
|
|
194
|
+
const start = localOffset + 30 + localNameLen + localExtraLen;
|
|
195
|
+
if (start + compressedSize > p) {
|
|
196
|
+
throw new Error("truncated zip entry");
|
|
197
|
+
}
|
|
198
|
+
const data = buf.subarray(start, start + compressedSize);
|
|
199
|
+
if (method === 0) {
|
|
200
|
+
return data;
|
|
201
|
+
}
|
|
202
|
+
if (method === 8) {
|
|
203
|
+
return zlib.inflateRawSync(data);
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
p += 46 + nameLen + extraLen + commentLen;
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function main() {
|
|
213
|
+
if (process.env.FIGCTL_SKIP_DOWNLOAD === "1") {
|
|
214
|
+
process.stderr.write(
|
|
215
|
+
"figctl: download skipped; set FIGCTL_BINARY to an existing executable when running figctl\n"
|
|
216
|
+
);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const t = target();
|
|
221
|
+
const dest = path.join(__dirname, "bin", t.bin);
|
|
222
|
+
|
|
223
|
+
if (process.env.FIGCTL_BINARY) {
|
|
224
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
225
|
+
fs.copyFileSync(process.env.FIGCTL_BINARY, dest);
|
|
226
|
+
fs.chmodSync(dest, 0o755);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const assetName = `figctl_${VERSION}_${t.os}_${t.arch}.${t.ext}`;
|
|
231
|
+
let archive;
|
|
232
|
+
let checksums;
|
|
233
|
+
try {
|
|
234
|
+
[archive, checksums] = await Promise.all([
|
|
235
|
+
get(`${BASE}/${assetName}`),
|
|
236
|
+
get(`${BASE}/checksums.txt`),
|
|
237
|
+
]);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
fail("could not download " + assetName, String(err && err.message ? err.message : err));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
verify(checksums, assetName, archive);
|
|
243
|
+
|
|
244
|
+
let binary;
|
|
245
|
+
try {
|
|
246
|
+
binary = t.ext === "zip" ? extractZip(archive, t.bin) : extractTarGz(archive, t.bin);
|
|
247
|
+
} catch (err) {
|
|
248
|
+
fail("could not unpack " + assetName, String(err && err.message ? err.message : err));
|
|
249
|
+
}
|
|
250
|
+
if (!binary || binary.length === 0) {
|
|
251
|
+
fail(`${t.bin} was not found inside ${assetName}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
255
|
+
fs.writeFileSync(dest, binary, { mode: 0o755 });
|
|
256
|
+
fs.chmodSync(dest, 0o755);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (require.main === module) {
|
|
260
|
+
main().catch((err) => {
|
|
261
|
+
process.stderr.write("figctl: installation failed: " + err.message + "\n");
|
|
262
|
+
process.exitCode = 1;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
module.exports = { verify, extractTarGz, extractZip };
|