@figtools/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/LICENSE +21 -0
- package/README.es.md +89 -0
- package/README.md +89 -0
- package/dist/cli.d.ts +35 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +222 -0
- package/dist/output/json-writer.d.ts +6 -0
- package/dist/output/json-writer.d.ts.map +1 -0
- package/dist/output/markdown-writer.d.ts +6 -0
- package/dist/output/markdown-writer.d.ts.map +1 -0
- package/dist/output/slugify.d.ts +2 -0
- package/dist/output/slugify.d.ts.map +1 -0
- package/dist/resolve-all.d.ts +7 -0
- package/dist/resolve-all.d.ts.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dryor
|
|
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.es.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
*[Read in English](./README.md)*
|
|
2
|
+
|
|
3
|
+
# @figtools/cli
|
|
4
|
+
|
|
5
|
+
`@figtools/cli` resuelve una o varias URLs de Figma desde la línea de comandos y escribe el resultado en JSON o en un árbol de Markdown navegable, pensado para que un LLM lo use como fuente de datos sin tener que procesar un único JSON gigante. Usa [`@figtools/core`](../core) por debajo, así que no depende de la REST API oficial de Figma ni de una cuenta con Dev Mode.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @figtools/cli
|
|
11
|
+
npx playwright install chromium
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
También puedes instalarlo como dependencia local del proyecto y correrlo con `npx figtools`.
|
|
15
|
+
|
|
16
|
+
## Examples
|
|
17
|
+
|
|
18
|
+
### Iniciar sesión una sola vez
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
figtools login
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Abre una ventana de Chromium en el login de Figma. Completa el login manualmente (soporta el flujo de Google SSO); al terminar, la sesión queda guardada en `~/.figma-scraper/session.json` y los comandos siguientes la reutilizan sin volver a pedir login.
|
|
25
|
+
|
|
26
|
+
### Resolver una URL a JSON (por defecto, a stdout)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
figtools "https://www.figma.com/design/ABC123/Mi-Diseno?node-id=1-23"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Si la URL no trae `node-id`, se resuelve la página activa completa del archivo.
|
|
33
|
+
|
|
34
|
+
### Guardar el resultado en un archivo o carpeta
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Un archivo .json puntual
|
|
38
|
+
figtools "https://www.figma.com/design/ABC123/Mi-Diseno?node-id=1-23" --output resultado.json
|
|
39
|
+
|
|
40
|
+
# Una carpeta: escribe "<carpeta>/<fileKey>-<nodeId>.json"
|
|
41
|
+
figtools "https://www.figma.com/design/ABC123/Mi-Diseno?node-id=1-23" --output ./salida
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Exportar como árbol de Markdown navegable
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
figtools "https://www.figma.com/design/ABC123/Mi-Diseno" --format markdown --output ./docs
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Genera `./docs/<fileKey>/index.md` con un link por cada nodo de nivel superior, y una carpeta por cada nodo que tiene hijos (con su propio `index.md`), hasta llegar a los nodos hoja como `<slug>.md`. Este formato está pensado para que un LLM navegue el árbol de a un archivo por vez, en vez de recibir el diseño completo en un solo bloque de JSON.
|
|
51
|
+
|
|
52
|
+
### Resolver varias URLs en una sola corrida
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
figtools "https://www.figma.com/design/ABC123/A?node-id=1-1" "https://www.figma.com/design/ABC123/A?node-id=1-2" --format markdown --output ./docs
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Cada URL se resuelve en paralelo tras autenticar una sola vez. Si alguna falla, el proceso sigue con las demás y termina con código de salida `1`; los errores se listan en stderr al final.
|
|
59
|
+
|
|
60
|
+
### Silenciar el progreso
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
figtools "https://www.figma.com/design/ABC123/Mi-Diseno" --quiet
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Omite el mensaje `Resolving N URL(s)...` en stderr — útil si estás capturando stdout en un script.
|
|
67
|
+
|
|
68
|
+
## Flags
|
|
69
|
+
|
|
70
|
+
| Flag | Valores | Default | Descripción |
|
|
71
|
+
| --- | --- | --- | --- |
|
|
72
|
+
| `--format` | `json`, `markdown` | `json` | Formato de salida |
|
|
73
|
+
| `--output` | ruta de archivo o carpeta | stdout (json) / `.` (markdown) | Dónde escribir el resultado |
|
|
74
|
+
| `--quiet` | — | `false` | Omite el mensaje de progreso en stderr |
|
|
75
|
+
|
|
76
|
+
## Troubleshooting
|
|
77
|
+
|
|
78
|
+
- **`Error: unsupported extension "<ext>"`**: con `--format markdown`, `--output` siempre se trata como una carpeta. Con `--format json`, solo se acepta una ruta terminada en `.json` o sin extensión (tratada como carpeta); cualquier otra extensión falla explícitamente.
|
|
79
|
+
- **El proceso termina con código `1` pero imprimió resultados**: significa que al menos una de las URLs falló — revisa el bloque `URLs with errors:` al final de stderr para ver el código (`FigmaScraperErrorCode`) y mensaje de cada una. Ver la tabla de errores en el [README de `@figtools/core`](../core/README.es.md#errores-posibles).
|
|
80
|
+
- **`figtools login` no avanza**: la ventana de Chromium espera indefinidamente a que termines el login manual; confirma que llegaste a `https://www.figma.com/files/...` antes de cerrar la ventana.
|
|
81
|
+
|
|
82
|
+
## Additional resources
|
|
83
|
+
|
|
84
|
+
- [`@figtools/core`](../core) — la librería subyacente, útil si prefieres integrar la resolución de URLs directamente en tu propio código en vez de invocar un binario.
|
|
85
|
+
- [README del monorepo](../../README.md) — visión general de `figtools`, ADRs y specs de aceptación.
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT — ver [LICENSE](./LICENSE).
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
*[Leer en español](./README.es.md)*
|
|
2
|
+
|
|
3
|
+
# @figtools/cli
|
|
4
|
+
|
|
5
|
+
`@figtools/cli` resolves one or more Figma URLs from the command line and writes the result as JSON or as a navigable Markdown tree, meant to be used by an LLM as a data source without having to parse one giant JSON blob. It uses [`@figtools/core`](../core) under the hood, so it doesn't depend on the official Figma REST API or a Dev Mode account.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @figtools/cli
|
|
11
|
+
npx playwright install chromium
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
You can also install it as a local project dependency and run it with `npx figtools`.
|
|
15
|
+
|
|
16
|
+
## Examples
|
|
17
|
+
|
|
18
|
+
### Log in once
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
figtools login
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Opens a Chromium window at the Figma login page. Complete the login manually (the Google SSO flow is supported); once done, the session is saved to `~/.figma-scraper/session.json` and later commands reuse it without asking to log in again.
|
|
25
|
+
|
|
26
|
+
### Resolve a URL to JSON (stdout by default)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
figtools "https://www.figma.com/design/ABC123/My-Design?node-id=1-23"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
If the URL doesn't include `node-id`, the entire active page of the file is resolved.
|
|
33
|
+
|
|
34
|
+
### Save the result to a file or folder
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# A single .json file
|
|
38
|
+
figtools "https://www.figma.com/design/ABC123/My-Design?node-id=1-23" --output result.json
|
|
39
|
+
|
|
40
|
+
# A folder: writes "<folder>/<fileKey>-<nodeId>.json"
|
|
41
|
+
figtools "https://www.figma.com/design/ABC123/My-Design?node-id=1-23" --output ./output
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Export as a navigable Markdown tree
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
figtools "https://www.figma.com/design/ABC123/My-Design" --format markdown --output ./docs
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Generates `./docs/<fileKey>/index.md` with a link for each top-level node, and a folder per node that has children (with its own `index.md`), down to leaf nodes as `<slug>.md`. This format is meant for an LLM to navigate the tree one file at a time, instead of receiving the entire design in a single JSON blob.
|
|
51
|
+
|
|
52
|
+
### Resolve several URLs in a single run
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
figtools "https://www.figma.com/design/ABC123/A?node-id=1-1" "https://www.figma.com/design/ABC123/A?node-id=1-2" --format markdown --output ./docs
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Each URL is resolved in parallel after authenticating once. If any of them fail, the process continues with the rest and exits with code `1`; the errors are listed on stderr at the end.
|
|
59
|
+
|
|
60
|
+
### Silence progress output
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
figtools "https://www.figma.com/design/ABC123/My-Design" --quiet
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Omits the `Resolving N URL(s)...` message on stderr — useful if you're capturing stdout in a script.
|
|
67
|
+
|
|
68
|
+
## Flags
|
|
69
|
+
|
|
70
|
+
| Flag | Values | Default | Description |
|
|
71
|
+
| --- | --- | --- | --- |
|
|
72
|
+
| `--format` | `json`, `markdown` | `json` | Output format |
|
|
73
|
+
| `--output` | file or folder path | stdout (json) / `.` (markdown) | Where to write the result |
|
|
74
|
+
| `--quiet` | — | `false` | Omits the progress message on stderr |
|
|
75
|
+
|
|
76
|
+
## Troubleshooting
|
|
77
|
+
|
|
78
|
+
- **`Error: unsupported extension "<ext>"`**: with `--format markdown`, `--output` is always treated as a folder. With `--format json`, only a path ending in `.json` or with no extension (treated as a folder) is accepted; any other extension fails explicitly.
|
|
79
|
+
- **The process exits with code `1` but printed results**: at least one of the URLs failed — check the `URLs with errors:` block at the end of stderr for each one's code (`FigmaScraperErrorCode`) and message. See the error table in the [`@figtools/core` README](../core#possible-errors).
|
|
80
|
+
- **`figtools login` doesn't progress**: the Chromium window waits indefinitely for you to finish the manual login; confirm you reached `https://www.figma.com/files/...` before closing the window.
|
|
81
|
+
|
|
82
|
+
## Additional resources
|
|
83
|
+
|
|
84
|
+
- [`@figtools/core`](../core) — the underlying library, useful if you'd rather integrate URL resolution directly into your own code instead of invoking a binary.
|
|
85
|
+
- [Monorepo README](../../README.md) — overview of `figtools`, ADRs, and acceptance specs.
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT — see [LICENSE](./LICENSE).
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type OutputFormat = "json" | "markdown";
|
|
2
|
+
export interface ParsedArgs {
|
|
3
|
+
urls: string[];
|
|
4
|
+
format: OutputFormat;
|
|
5
|
+
outputPath?: string;
|
|
6
|
+
quiet: boolean;
|
|
7
|
+
command?: "login";
|
|
8
|
+
}
|
|
9
|
+
type ParseArgsError = {
|
|
10
|
+
code: "VALIDATION_NO_URLS" | "VALIDATION_UNSUPPORTED_EXTENSION";
|
|
11
|
+
message: string;
|
|
12
|
+
};
|
|
13
|
+
export type ParseArgsResult = {
|
|
14
|
+
ok: true;
|
|
15
|
+
value: ParsedArgs;
|
|
16
|
+
} | {
|
|
17
|
+
ok: false;
|
|
18
|
+
error: ParseArgsError;
|
|
19
|
+
};
|
|
20
|
+
export type OutputTarget = {
|
|
21
|
+
kind: "stdout";
|
|
22
|
+
} | {
|
|
23
|
+
kind: "file";
|
|
24
|
+
path: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: "directory";
|
|
27
|
+
path: string;
|
|
28
|
+
} | {
|
|
29
|
+
kind: "unsupported-extension";
|
|
30
|
+
extension: string;
|
|
31
|
+
};
|
|
32
|
+
export declare function parseArgs(argv: string[]): ParseArgsResult;
|
|
33
|
+
export declare function decideOutputTarget(outputPath: string | undefined, format: OutputFormat): OutputTarget;
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,UAAU,CAAC;AAE/C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,YAAY,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,oBAAoB,GAAG,kCAAkC,CAAC;IAChE,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,eAAe,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,GAC/B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,cAAc,CAAA;CAAE,CAAC;AAEzC,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,uBAAuB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,CA+BzD;AAED,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,MAAM,EAAE,YAAY,GACnB,YAAY,CAad"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { extname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { CookieSessionStore, PlaywrightFigmaGateway, PlaywrightLogin, createFigmaScraperCore } from "@figtools/core";
|
|
5
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
6
|
+
async function resolveAll(core, urls) {
|
|
7
|
+
const sessionResult = await core.ensureSession();
|
|
8
|
+
if (!sessionResult.ok) return urls.map((url)=>({
|
|
9
|
+
url,
|
|
10
|
+
result: sessionResult
|
|
11
|
+
}));
|
|
12
|
+
const settled = await Promise.allSettled(urls.map((url)=>core.resolveUrl(url)));
|
|
13
|
+
return urls.map((url, i)=>{
|
|
14
|
+
const outcome = settled[i];
|
|
15
|
+
if ("fulfilled" === outcome.status) return {
|
|
16
|
+
url,
|
|
17
|
+
result: outcome.value
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
url,
|
|
21
|
+
result: {
|
|
22
|
+
ok: false,
|
|
23
|
+
error: {
|
|
24
|
+
code: "AUTHENTICATION_FAILED",
|
|
25
|
+
message: "Unexpected error while resolving the URL"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async function writeAsJson(fileKey, result, options) {
|
|
32
|
+
const json = JSON.stringify(result, null, 2);
|
|
33
|
+
if (!options.outputPath) return void console.log(json);
|
|
34
|
+
const { outputPath } = options;
|
|
35
|
+
if (".json" === extname(outputPath)) return void await writeFile(outputPath, json, "utf8");
|
|
36
|
+
await mkdir(outputPath, {
|
|
37
|
+
recursive: true
|
|
38
|
+
});
|
|
39
|
+
const nodeId = result.id.replace(/:/g, "-");
|
|
40
|
+
await writeFile(join(outputPath, `${fileKey}-${nodeId}.json`), json, "utf8");
|
|
41
|
+
}
|
|
42
|
+
function slugify(name) {
|
|
43
|
+
return name.replace(/\s*\([^)]*\)/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
44
|
+
}
|
|
45
|
+
function slugifyWithCollisions(names) {
|
|
46
|
+
const slugs = names.map(slugify);
|
|
47
|
+
const count = new Map();
|
|
48
|
+
return slugs.map((slug)=>{
|
|
49
|
+
const n = (count.get(slug) ?? 0) + 1;
|
|
50
|
+
count.set(slug, n);
|
|
51
|
+
return 1 === n ? slug : `${slug}-[${n}]`;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function isFigmaPage(result) {
|
|
55
|
+
return "nodes" in result;
|
|
56
|
+
}
|
|
57
|
+
function leafContent(node) {
|
|
58
|
+
return `# ${node.name}\n\ntype: ${node.type}\n`;
|
|
59
|
+
}
|
|
60
|
+
function containerContent(name, children, childSlugs) {
|
|
61
|
+
const links = children.map((child, i)=>{
|
|
62
|
+
const slug = childSlugs[i];
|
|
63
|
+
const href = 0 === child.children.length ? `${slug}.md` : `${slug}/index.md`;
|
|
64
|
+
return `- [${child.name}](${href})`;
|
|
65
|
+
}).join("\n");
|
|
66
|
+
return `# ${name}\n\n${links}\n`;
|
|
67
|
+
}
|
|
68
|
+
async function writeChildNode(node, parentDir, slug) {
|
|
69
|
+
if (0 === node.children.length) return void await writeFile(join(parentDir, `${slug}.md`), leafContent(node), "utf8");
|
|
70
|
+
const nodeDir = join(parentDir, slug);
|
|
71
|
+
await mkdir(nodeDir, {
|
|
72
|
+
recursive: true
|
|
73
|
+
});
|
|
74
|
+
const childSlugs = slugifyWithCollisions(node.children.map((c)=>c.name));
|
|
75
|
+
await writeFile(join(nodeDir, "index.md"), containerContent(node.name, node.children, childSlugs), "utf8");
|
|
76
|
+
for(let i = 0; i < node.children.length; i++)await writeChildNode(node.children[i], nodeDir, childSlugs[i]);
|
|
77
|
+
}
|
|
78
|
+
async function writeAsMarkdownTree(fileKey, result, options) {
|
|
79
|
+
const fileKeyDir = join(options.outputDir, fileKey);
|
|
80
|
+
await mkdir(fileKeyDir, {
|
|
81
|
+
recursive: true
|
|
82
|
+
});
|
|
83
|
+
if (isFigmaPage(result)) {
|
|
84
|
+
const childSlugs = slugifyWithCollisions(result.nodes.map((n)=>n.name));
|
|
85
|
+
await writeFile(join(fileKeyDir, "index.md"), containerContent(result.name, result.nodes, childSlugs), "utf8");
|
|
86
|
+
for(let i = 0; i < result.nodes.length; i++)await writeChildNode(result.nodes[i], fileKeyDir, childSlugs[i]);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (0 === result.children.length) {
|
|
90
|
+
const [slug] = slugifyWithCollisions([
|
|
91
|
+
result.name
|
|
92
|
+
]);
|
|
93
|
+
await writeFile(join(fileKeyDir, `${slug}.md`), leafContent(result), "utf8");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const childSlugs = slugifyWithCollisions(result.children.map((c)=>c.name));
|
|
97
|
+
await writeFile(join(fileKeyDir, "index.md"), containerContent(result.name, result.children, childSlugs), "utf8");
|
|
98
|
+
for(let i = 0; i < result.children.length; i++)await writeChildNode(result.children[i], fileKeyDir, childSlugs[i]);
|
|
99
|
+
}
|
|
100
|
+
function parseArgs(argv) {
|
|
101
|
+
if ("login" === argv[0]) return {
|
|
102
|
+
ok: true,
|
|
103
|
+
value: {
|
|
104
|
+
command: "login",
|
|
105
|
+
urls: [],
|
|
106
|
+
format: "json",
|
|
107
|
+
quiet: false
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
let format = "json";
|
|
111
|
+
let outputPath;
|
|
112
|
+
let quiet = false;
|
|
113
|
+
const urls = [];
|
|
114
|
+
for(let i = 0; i < argv.length; i++){
|
|
115
|
+
const arg = argv[i];
|
|
116
|
+
if ("--format" === arg) format = argv[++i];
|
|
117
|
+
else if ("--output" === arg) outputPath = argv[++i];
|
|
118
|
+
else if ("--quiet" === arg) quiet = true;
|
|
119
|
+
else if (!arg.startsWith("--")) urls.push(arg);
|
|
120
|
+
}
|
|
121
|
+
if (0 === urls.length) return {
|
|
122
|
+
ok: false,
|
|
123
|
+
error: {
|
|
124
|
+
code: "VALIDATION_NO_URLS",
|
|
125
|
+
message: "At least one Figma URL is required"
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
ok: true,
|
|
130
|
+
value: {
|
|
131
|
+
urls,
|
|
132
|
+
format,
|
|
133
|
+
outputPath,
|
|
134
|
+
quiet
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function decideOutputTarget(outputPath, format) {
|
|
139
|
+
if (!outputPath) return "markdown" === format ? {
|
|
140
|
+
kind: "directory",
|
|
141
|
+
path: "."
|
|
142
|
+
} : {
|
|
143
|
+
kind: "stdout"
|
|
144
|
+
};
|
|
145
|
+
if ("markdown" === format) return {
|
|
146
|
+
kind: "directory",
|
|
147
|
+
path: outputPath
|
|
148
|
+
};
|
|
149
|
+
const ext = extname(outputPath);
|
|
150
|
+
if (!ext) return {
|
|
151
|
+
kind: "directory",
|
|
152
|
+
path: outputPath
|
|
153
|
+
};
|
|
154
|
+
if (".json" === ext) return {
|
|
155
|
+
kind: "file",
|
|
156
|
+
path: outputPath
|
|
157
|
+
};
|
|
158
|
+
return {
|
|
159
|
+
kind: "unsupported-extension",
|
|
160
|
+
extension: ext
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function extractFileKey(url) {
|
|
164
|
+
const match = url.match(/\/(?:file|design|proto|board)\/([^/?]+)/);
|
|
165
|
+
return match ? match[1] : url;
|
|
166
|
+
}
|
|
167
|
+
async function main(argv) {
|
|
168
|
+
const parsed = parseArgs(argv);
|
|
169
|
+
if (!parsed.ok) {
|
|
170
|
+
process.stderr.write(`Error: ${parsed.error.message}\n`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
const { command, urls, format, outputPath, quiet } = parsed.value;
|
|
174
|
+
const core = createFigmaScraperCore({
|
|
175
|
+
sessionStore: new CookieSessionStore(),
|
|
176
|
+
interactiveLogin: new PlaywrightLogin(),
|
|
177
|
+
gateway: new PlaywrightFigmaGateway()
|
|
178
|
+
});
|
|
179
|
+
if ("login" === command) {
|
|
180
|
+
const result = await core.reauthenticate();
|
|
181
|
+
if (!result.ok) {
|
|
182
|
+
process.stderr.write(`Authentication error: ${result.error.message}\n`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
process.stderr.write("Logged in successfully.\n");
|
|
186
|
+
process.exit(0);
|
|
187
|
+
}
|
|
188
|
+
const outputTarget = decideOutputTarget(outputPath, format);
|
|
189
|
+
if ("unsupported-extension" === outputTarget.kind) {
|
|
190
|
+
process.stderr.write(`Error: unsupported extension "${outputTarget.extension}"\n`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
if (!quiet) process.stderr.write(`Resolving ${urls.length} URL(s)...\n`);
|
|
194
|
+
const resolutions = await resolveAll(core, urls);
|
|
195
|
+
let hadFailure = false;
|
|
196
|
+
for (const { url, result } of resolutions){
|
|
197
|
+
if (!result.ok) {
|
|
198
|
+
hadFailure = true;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const fileKey = extractFileKey(url);
|
|
202
|
+
if ("markdown" === format) {
|
|
203
|
+
const dir = "directory" === outputTarget.kind ? outputTarget.path : ".";
|
|
204
|
+
await writeAsMarkdownTree(fileKey, result.value, {
|
|
205
|
+
outputDir: dir
|
|
206
|
+
});
|
|
207
|
+
} else {
|
|
208
|
+
const writePath = "file" === outputTarget.kind ? outputTarget.path : "directory" === outputTarget.kind ? outputTarget.path : void 0;
|
|
209
|
+
await writeAsJson(fileKey, result.value, {
|
|
210
|
+
outputPath: writePath
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const failures = resolutions.filter((r)=>!r.result.ok);
|
|
215
|
+
if (failures.length > 0) {
|
|
216
|
+
process.stderr.write("\nURLs with errors:\n");
|
|
217
|
+
for (const { url, result } of failures)if (!result.ok) process.stderr.write(` ${url}: ${result.error.code} — ${result.error.message}\n`);
|
|
218
|
+
}
|
|
219
|
+
process.exit(hadFailure ? 1 : 0);
|
|
220
|
+
}
|
|
221
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) main(process.argv.slice(2));
|
|
222
|
+
export { decideOutputTarget, parseArgs };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FigmaScrapeResult } from "@figtools/core";
|
|
2
|
+
export interface JsonWriterOptions {
|
|
3
|
+
outputPath?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function writeAsJson(fileKey: string, result: FigmaScrapeResult, options: JsonWriterOptions): Promise<void>;
|
|
6
|
+
//# sourceMappingURL=json-writer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-writer.d.ts","sourceRoot":"","sources":["../../src/output/json-writer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAsB,WAAW,CAC/B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,IAAI,CAAC,CAkBf"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FigmaScrapeResult } from "@figtools/core";
|
|
2
|
+
export interface MarkdownWriterOptions {
|
|
3
|
+
outputDir: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function writeAsMarkdownTree(fileKey: string, result: FigmaScrapeResult, options: MarkdownWriterOptions): Promise<void>;
|
|
6
|
+
//# sourceMappingURL=markdown-writer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown-writer.d.ts","sourceRoot":"","sources":["../../src/output/markdown-writer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAwB,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAG9E,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;CACnB;AAoCD,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAgCf"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slugify.d.ts","sourceRoot":"","sources":["../../src/output/slugify.ts"],"names":[],"mappings":"AAQA,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAQ/D"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FigmaScraperCore, FigmaScrapeResult, FigmaScraperError, Result } from "@figtools/core";
|
|
2
|
+
export interface UrlResolution {
|
|
3
|
+
url: string;
|
|
4
|
+
result: Result<FigmaScrapeResult, FigmaScraperError>;
|
|
5
|
+
}
|
|
6
|
+
export declare function resolveAll(core: FigmaScraperCore, urls: string[]): Promise<UrlResolution[]>;
|
|
7
|
+
//# sourceMappingURL=resolve-all.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-all.d.ts","sourceRoot":"","sources":["../src/resolve-all.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAErG,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,CAAC;CACtD;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,gBAAgB,EACtB,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,aAAa,EAAE,CAAC,CAsB1B"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@figtools/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Resolves one or more Figma URLs from the command line and writes the result as JSON or as a navigable markdown tree meant to be used as a data source by an LLM.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"figma",
|
|
7
|
+
"figma-api",
|
|
8
|
+
"cli",
|
|
9
|
+
"scraper",
|
|
10
|
+
"markdown",
|
|
11
|
+
"llm"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/dryor/figtools#readme",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/dryor/figtools.git",
|
|
17
|
+
"directory": "packages/cli"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/dryor/figtools/issues"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=24"
|
|
26
|
+
},
|
|
27
|
+
"bin": {
|
|
28
|
+
"figtools": "./dist/cli.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"playwright": "^1.61.1",
|
|
38
|
+
"@figtools/core": "0.1.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@rslib/core": "^0.23.2",
|
|
42
|
+
"@types/node": "^26.1.1",
|
|
43
|
+
"typescript": "^7.0.2",
|
|
44
|
+
"vitest": "^4.1.10"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "rslib build",
|
|
48
|
+
"test": "vitest run --project unit",
|
|
49
|
+
"test:watch": "vitest --project unit",
|
|
50
|
+
"typecheck": "tsc --noEmit"
|
|
51
|
+
}
|
|
52
|
+
}
|