raptor-aios 0.10.0 → 0.12.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +11 -3
  3. package/dist/_core/dist/agents/prompt-builder.js +10 -8
  4. package/dist/_core/dist/audit/schema.js +1 -0
  5. package/dist/_core/dist/design/asset-normalize.js +37 -0
  6. package/dist/_core/dist/design/asset-paths.js +23 -0
  7. package/dist/_core/dist/design/downloader.js +140 -0
  8. package/dist/_core/dist/design/fetcher.js +203 -0
  9. package/dist/_core/dist/design/figma-client.js +198 -0
  10. package/dist/_core/dist/design/figma-credentials.js +41 -0
  11. package/dist/_core/dist/design/figma-dialects.js +75 -0
  12. package/dist/_core/dist/design/figma-oauth.js +71 -0
  13. package/dist/_core/dist/design/figma-rest.js +277 -0
  14. package/dist/_core/dist/design/font-resolver.js +298 -0
  15. package/dist/_core/dist/design/index.js +12 -0
  16. package/dist/_core/dist/design/mapper.js +22 -2
  17. package/dist/_core/dist/design/scaffold.js +58 -2
  18. package/dist/_core/dist/design/screens.js +202 -0
  19. package/dist/_core/dist/design/slug.js +9 -0
  20. package/dist/_core/dist/design/tokens.js +2 -1
  21. package/dist/_core/dist/gates/design-gates.js +1 -9
  22. package/dist/_core/dist/jira/mcp-client.js +15 -131
  23. package/dist/_core/dist/jira/oauth.js +1 -79
  24. package/dist/_core/dist/transport/index.js +2 -0
  25. package/dist/_core/dist/transport/mcp.js +134 -0
  26. package/dist/_core/dist/transport/oauth.js +83 -0
  27. package/dist/_core/package.json +1 -1
  28. package/dist/_core/templates/spec.md.hbs +28 -3
  29. package/dist/commands/design/connect.js +105 -0
  30. package/dist/commands/design/disconnect.js +19 -0
  31. package/dist/commands/design/pull.js +65 -0
  32. package/dist/commands/design/status.js +76 -0
  33. package/dist/commands/design/sync.js +55 -0
  34. package/dist/commands/init.js +29 -240
  35. package/dist/commands/new.js +36 -2
  36. package/dist/commands/setup.js +149 -0
  37. package/dist/shared/design.js +93 -0
  38. package/dist/shared/init-core.js +260 -0
  39. package/dist/shared/setup/agent-resolver.js +54 -0
  40. package/dist/shared/setup/banner.js +19 -0
  41. package/dist/shared/setup/detect-stack.js +31 -0
  42. package/dist/shared/setup/onboard.js +18 -0
  43. package/dist/shared/setup/plan.js +93 -0
  44. package/dist/shared/setup/wizard.js +164 -0
  45. package/package.json +2 -1
  46. package/scripts/prepare-npm.mjs +2 -1
  47. package/templates/commands/plan.md +5 -3
  48. package/templates/commands/specify.md +9 -5
  49. package/templates/commands/tasks.md +5 -3
package/CHANGELOG.md CHANGED
@@ -3,6 +3,34 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [Unreleased]
7
+
8
+ ## [0.12.0] - 2026-06-13
9
+
10
+ ### Added
11
+
12
+ - **`raptor setup` — menu de instalação interativo + onboarding (`packages/cli/src/commands/setup.ts`).** Uma porta de entrada guiada por setas (via `@clack/prompts`) que conduz, fase a fase: ❶ **onboarding** (a essência do Raptor, pulável com `--skip-intro`); ❷ **ponto de partida** (projeto do zero vs. existente, com detecção de stack do `package.json`); ❸ **ambiente em duas perguntas** — **IDE** (VS Code, Cursor, Antigravity, JetBrains, terminal) → **IA** (Claude Code, Copilot, Gemini, Codex), filtradas em cascata e mapeadas para a *agent key* correta; ❹ **preset** (com a sugestão da stack destacada); ❺ **Jira/Figma "configurar depois"** (grava o bloco pronto no `raptor.yml`, sem disparar OAuth no fluxo); ❻ **revisão e aplicação**.
13
+ - **Coleta separada da execução.** O wizard apenas monta um `SetupPlan`; `applySetupPlan` o executa reusando a lógica de `init` — extraída intacta para `performInit` (`shared/init-core.ts`) —, então o `.raptor/` gerado é byte-equivalente ao de `raptor init`.
14
+ - **Caminho não-interativo** (`--non-interactive --ide --ai --preset …`, ou simplesmente a ausência de TTY) para CI/scripts, compartilhando o MESMO plano do wizard — o que mantém a experiência interativa testável sem dirigir um terminal.
15
+ - **Reconfigure não-destrutivo:** `raptor setup --force` agora preserva os blocos do usuário no `raptor.yml` (`git` com `commit_trailer`/`kind_prefixes`, `gates`, conexões `jira`/`design` — inclusive desconectadas-mas-configuradas, que retêm `cloud_id`/`custom_fields`/token — e o nome do projeto), em vez de revertê-los aos defaults do template.
16
+ - Banner ASCII responsivo (degrada em terminais estreitos, respeita `NO_COLOR`/não-TTY) e exit codes corretos (cancelar ou falhar → não-zero; recusar a aplicação → zero).
17
+ - Endurecido por revisão adversarial multi-dimensão (correção, fidelidade de API `@clack`, paridade da extração de `init`, contrato de config, reuso), com etapa cética por achado e o ciclo repetido até convergir.
18
+
19
+ ## [0.11.0] - 2026-06-12
20
+
21
+ ### Added
22
+
23
+ - **Integração Figma/Design — captura determinística + seed + gate (`packages/core/src/design/`).** Um link do Figma em `raptor new --figma <url>` deixa de ser só uma URL na spec: o Raptor CAPTURA o design de forma determinística e SEMEIA `design/` com os artefatos reais (espelhando o modelo do enriquecimento Jira). A captura usa o provider **`figma-rest`** (REST API + Personal Access Token, determinístico e roda em CI), selecionável via `design.provider` no `raptor.yml`.
24
+ - **Tokens** via `GET /v1/files/:key/variables/local` (com fallback para `/styles` quando o endpoint dá `403` em planos não-Enterprise ou volta vazio), normalizados em `design/tokens.json` (colors/typography/spacing/radii) por uma classificação robusta a nomes inconsistentes do mundo real (o valor decide antes do nome; tipografia decomposta em family/size/weight/lineHeight/letterSpacing — um número nunca vira família).
25
+ - **Telas** via `GET /v1/files/:key/nodes` (nó linkado) ou `?depth=2` (arquivo inteiro) → `design/screens/<slug>.md`.
26
+ - **Assets** escopados ao frame: o render do nó (`/v1/images`) + os image fills que a subárvore realmente pinta (`collectImageRefs` sobre `/nodes`, filtrando o `/v1/files/:key/images` que devolve o arquivo inteiro) — baixados com `sha256`, guarda anti-SSRF e `max_bytes`, catalogados em `design/assets-manifest.json`.
27
+ - **Fontes** (o Figma não serve o binário) resolvidas por precedência repo/brand → Google Fonts → OS (`fc-match`) → `pending`.
28
+ - **Comandos** `raptor design connect | status | pull | sync | disconnect` (o `sync` re-semeia idempotente — nunca sobrescreve refino — e emite `design.synced`).
29
+ - **Flip REDISTRIBUIR**: `spec.md.hbs`, os prompts de comando (`specify`/`plan`/`tasks`) e o prompt canônico bifurcam — quando o design foi semeado, instruem o agente a **REFINAR e redistribuir** os artefatos em disco (referenciar por `[screen: X]`/`[asset: Y]`, refinar os `screens/*.md`, verificar os tokens, declarar as libs); sem captura, mantêm a instrução de **ENXERGAR** via Figma MCP. O `spec.md.hbs` lista o que foi semeado (contagem de tokens, telas, assets, libs).
30
+ - **`gate.design.ready`** cobra a incorporação na promoção da spec (telas/assets referenciados existem e estão em disco; tokens não-vazios).
31
+ - **Auditoria** enriquecida (`feature.created`/`design.imported` com contagens de tokens/telas/assets; novo evento `design.synced`).
32
+ - Validado E2E contra o arquivo Check-in real (`figma-rest`/PAT) e endurecido por revisão adversarial multi-dimensão: escopo de assets (1357 imagens espúrias → 0), classificação de tokens (63 → 93 bem distribuídos, 0 famílias falsas), memoização de `/nodes`. Doc: [`docs/design-figma-sync.md`](docs/design-figma-sync.md).
33
+
6
34
  ## [0.10.0] - 2026-06-11
7
35
 
8
36
  ### Added
package/README.md CHANGED
@@ -22,7 +22,7 @@
22
22
  ```bash
23
23
  npm install -g raptor-aios
24
24
  cd meu-app-react-native
25
- raptor init --ai=claude-code --preset=mobile-opinionated
25
+ raptor setup # 🧭 assistente guiado: IA/IDE, preset, Jira/Figma (ou 'raptor init' direto)
26
26
  raptor new login-biometrico -d "Permitir login com Face ID / digital"
27
27
  # → cria a feature E já troca para a branch feat/001-login-biometrico
28
28
  ```
@@ -95,11 +95,19 @@ raptor doctor # 🩺 checagem de saúde do ambiente e do projeto
95
95
 
96
96
  ### 1️⃣ Inicialize o Raptor
97
97
 
98
+ A forma **guiada** — um assistente interativo (navegação por setas) que apresenta o Raptor e coleta IA/IDE, preset e integrações Jira/Figma:
99
+
100
+ ```bash
101
+ raptor setup
102
+ ```
103
+
104
+ Ou, **direto e não-interativo** (ideal para CI/scripts):
105
+
98
106
  ```bash
99
107
  raptor init --ai=claude-code --preset=mobile-opinionated
100
108
  ```
101
109
 
102
- Isso cria a pasta `.raptor/`, a **constituição** do projeto, e materializa os **slash commands** em `.claude/commands/` (ou `.cursor/`, etc., conforme o agente).
110
+ Qualquer um cria a pasta `.raptor/`, a **constituição** do projeto, e materializa os **slash commands** em `.claude/commands/` (ou `.cursor/`, etc., conforme o agente).
103
111
 
104
112
  | Flag | Para que serve |
105
113
  | ----------------------------- | ------------------------------------------------------------------------------------------------------------ |
@@ -481,7 +489,7 @@ raptor new login --jira APP-1234 # semeia a spec a partir da issue
481
489
  ## 📖 Referência de comandos CLI
482
490
 
483
491
  ```text
484
- 🔄 Ciclo init · new · clarify · plan · tasks · analyze · checklist · implement · approve · taskstoissues
492
+ 🔄 Ciclo setup · init · new · clarify · plan · tasks · analyze · checklist · implement · approve · taskstoissues
485
493
  🌿 Branch/commit commit · scan
486
494
  🔬 Verificação verify · verify a11y · verify perf · verify stores · verify os-matrix
487
495
  verify architecture · verify audit · verify constitution
@@ -33,9 +33,10 @@ const PHASE_MAP = {
33
33
  " e todo id de AC é espelhado no frontmatter `acceptance.ids`.",
34
34
  "6. Success Criteria mensuráveis e tecnologia-agnósticos (foco no usuário).",
35
35
  "7. Auto-valide contra o 'Review & Acceptance Checklist' do template antes de entregar.",
36
- "8. Se a feature tiver design anexado (pasta `design/` presente ou seção `## Design Reference`",
37
- " na spec), use suas ferramentas de design (Figma MCP) para ENXERGAR o design: derive",
38
- " telas, fluxos e critérios das telas reais e crie `design/screens/<nome>.md` por tela.",
36
+ "8. Se a feature tiver design (pasta `design/` ou seção `## Design Reference`): se já houver",
37
+ " artefatos semeados (`design/tokens.json` populado, `design/screens/*.md`), REFINE e",
38
+ " redistribua esses artefatos em vez de re-buscar; senão use o Figma MCP para ENXERGAR o",
39
+ " design. Derive telas/fluxos/critérios das telas reais e refine (ou crie) `design/screens/<nome>.md`.",
39
40
  "",
40
41
  "### Feature a especificar:",
41
42
  "",
@@ -67,8 +68,9 @@ const PHASE_MAP = {
67
68
  "3. Inclua Data Model, Contracts e Research quando aplicável.",
68
69
  "4. Gere Complexity Tracking se algum gate for violado.",
69
70
  "5. Não invente onde a spec tem `[NEEDS CLARIFICATION]`.",
70
- "6. Se a feature tiver design (pasta `design/`), use o Figma MCP para extrair tokens em",
71
- " `design/tokens.json` e declarar as bibliotecas/design-system necessárias no plan.",
71
+ "6. Se a feature tiver design (pasta `design/`): se `design/tokens.json` estiver populado,",
72
+ " VERIFIQUE/corrija os tokens semeados; senão extraia via Figma MCP. Declare as",
73
+ " bibliotecas/design-system necessárias no plan.",
72
74
  ].join("\n"),
73
75
  },
74
76
  tasks: {
@@ -97,9 +99,9 @@ const PHASE_MAP = {
97
99
  "3. Inclua critério de aceite técnico por task.",
98
100
  "4. Ordene pela dependência natural (build-order).",
99
101
  "5. Toda task deve ser completável em ≤4h.",
100
- "6. Se a feature tiver design (pasta `design/`), gere tasks que baixem os assets via Figma",
101
- " MCP para `assets/{images,icons,fonts}`, os cataloguem em `design/assets-manifest.json`",
102
- " e os referenciem por caminho concreto (tags `[screen: X]` / `[asset: X]`).",
102
+ "6. Se a feature tiver design (pasta `design/`): consuma os assets catalogados em",
103
+ " `design/assets-manifest.json` (baixados em `assets/{images,icons,fonts}`) e baixe via",
104
+ " Figma MCP o que faltar; referencie por caminho concreto (tags `[screen: X]` / `[asset: X]`).",
103
105
  ].join("\n"),
104
106
  },
105
107
  implement: {
@@ -64,6 +64,7 @@ export const auditEventSchema = {
64
64
  "hotfix.reconciled",
65
65
  "jira.issues.created",
66
66
  "design.imported",
67
+ "design.synced",
67
68
  "branch.created",
68
69
  "branch.switched",
69
70
  "commit.guarded",
@@ -0,0 +1,37 @@
1
+ import { slugify } from "./slug.js";
2
+ import { assetDirFor, classifyAssetType, extForFormat, extFromUrl } from "./asset-paths.js";
3
+ export function normalizeImagesToAssets(renders, fills, opts = {}) {
4
+ const out = [];
5
+ const seen = new Set();
6
+ const fmt = opts.format ?? "png";
7
+ const renderType = classifyAssetType(fmt);
8
+ const renderExt = extForFormat(fmt);
9
+ const renderDir = assetDirFor(renderType);
10
+ for (const [nodeId, url] of Object.entries(renders)) {
11
+ if (!url)
12
+ continue;
13
+ const human = opts.names?.[nodeId];
14
+ let base = slugify(human || `node ${nodeId}`) || slugify(`node ${nodeId}`);
15
+ let path = `${renderDir}/${base}.${renderExt}`;
16
+ if (seen.has(path)) {
17
+ base = `${base}-${slugify(nodeId)}`;
18
+ path = `${renderDir}/${base}.${renderExt}`;
19
+ }
20
+ seen.add(path);
21
+ out.push({ name: human || base, type: renderType, path, url, figma_node_id: nodeId });
22
+ }
23
+ for (const [ref, url] of Object.entries(fills)) {
24
+ if (!url)
25
+ continue;
26
+ let base = slugify(`fill ${ref}`).slice(0, 40) || slugify(`fill ${ref}`);
27
+ const ext = extFromUrl(url) ?? "png";
28
+ let path = `assets/images/${base}.${ext}`;
29
+ if (seen.has(path)) {
30
+ base = `${base}-${slugify(ref)}`.slice(0, 60);
31
+ path = `assets/images/${base}.${ext}`;
32
+ }
33
+ seen.add(path);
34
+ out.push({ name: base, type: "image", path, url });
35
+ }
36
+ return out;
37
+ }
@@ -0,0 +1,23 @@
1
+ export function classifyAssetType(format) {
2
+ return format.trim().toLowerCase() === "svg" ? "svg" : "image";
3
+ }
4
+ export function assetDirFor(type) {
5
+ if (type === "svg" || type === "icon")
6
+ return "assets/icons";
7
+ if (type === "font")
8
+ return "assets/fonts";
9
+ return "assets/images";
10
+ }
11
+ export function extForFormat(format) {
12
+ const f = format.trim().toLowerCase();
13
+ if (!/^[a-z0-9]{2,5}$/.test(f))
14
+ return "png";
15
+ return f === "jpeg" ? "jpg" : f;
16
+ }
17
+ export function extFromUrl(url) {
18
+ const m = url.match(/\.([A-Za-z0-9]{2,5})(?:[?#]|$)/);
19
+ if (!m)
20
+ return null;
21
+ const ext = m[1].toLowerCase();
22
+ return ext === "jpeg" ? "jpg" : ext;
23
+ }
@@ -0,0 +1,140 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdirSync, writeFileSync } from "node:fs";
3
+ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
4
+ import { normalizePath } from "./slug.js";
5
+ export class DownloadError extends Error {
6
+ status;
7
+ constructor(message, status) {
8
+ super(message);
9
+ this.status = status;
10
+ this.name = "DownloadError";
11
+ }
12
+ }
13
+ export async function downloadAsset(url, destAbs, opts = {}) {
14
+ const doFetch = opts.fetchImpl ?? globalThis.fetch;
15
+ const max = opts.maxBytes && opts.maxBytes > 0 ? opts.maxBytes : 0;
16
+ assertSafeAssetUrl(url, opts.allowHttp ?? false);
17
+ let res;
18
+ try {
19
+ res = await doFetch(url, opts.headers ? { headers: opts.headers } : {});
20
+ }
21
+ catch (err) {
22
+ throw new DownloadError(`download failed for ${url}: ${err instanceof Error ? err.message : String(err)}`, 0);
23
+ }
24
+ if (!res.ok) {
25
+ throw new DownloadError(`download ${url} → ${res.status} ${res.statusText}`, res.status);
26
+ }
27
+ const contentType = res.headers.get("content-type") ?? undefined;
28
+ const declared = Number(res.headers.get("content-length") ?? "");
29
+ if (max && Number.isFinite(declared) && declared > max) {
30
+ throw new DownloadError(`download ${url} exceeds max_bytes (${declared} > ${max})`, res.status);
31
+ }
32
+ const buf = Buffer.from(await res.arrayBuffer());
33
+ if (max && buf.length > max) {
34
+ throw new DownloadError(`download ${url} exceeds max_bytes (${buf.length} > ${max})`, res.status);
35
+ }
36
+ mkdirSync(dirname(destAbs), { recursive: true });
37
+ writeFileSync(destAbs, buf);
38
+ const hash = createHash("sha256").update(buf).digest("hex");
39
+ return { hash, bytes: buf.length, ...(contentType ? { contentType } : {}) };
40
+ }
41
+ export async function downloadAll(pending, root, opts = {}) {
42
+ const concurrency = Math.max(1, opts.concurrency ?? 4);
43
+ const results = new Array(pending.length);
44
+ let next = 0;
45
+ const worker = async () => {
46
+ for (;;) {
47
+ const i = next++;
48
+ if (i >= pending.length)
49
+ return;
50
+ const a = pending[i];
51
+ if (!a)
52
+ return;
53
+ const base = {
54
+ name: a.name,
55
+ type: a.type,
56
+ path: a.path,
57
+ ...(a.figma_node_id ? { figma_node_id: a.figma_node_id } : {}),
58
+ ...(a.screen ? { screen: a.screen } : {}),
59
+ };
60
+ try {
61
+ const rel = normalizePath(a.path);
62
+ const rootAbs = resolve(root);
63
+ const within = resolve(rootAbs, rel);
64
+ if (isAbsolute(rel) || (within !== rootAbs && !within.startsWith(rootAbs + sep))) {
65
+ throw new DownloadError(`asset path escapes project root: ${a.path}`, 0);
66
+ }
67
+ const destAbs = join(root, rel);
68
+ const { hash } = await downloadAsset(a.url, destAbs, opts);
69
+ results[i] = { ...base, hash, status: "downloaded" };
70
+ }
71
+ catch (err) {
72
+ const msg = err instanceof Error ? err.message : String(err);
73
+ if (opts.onWarn)
74
+ opts.onWarn(`asset "${a.name}" not downloaded: ${msg}`);
75
+ else
76
+ process.stderr.write(`Warning: asset "${a.name}" not downloaded: ${msg}\n`);
77
+ results[i] = { ...base, status: "pending" };
78
+ }
79
+ }
80
+ };
81
+ await Promise.all(Array.from({ length: Math.min(concurrency, pending.length) }, worker));
82
+ return results;
83
+ }
84
+ function assertSafeAssetUrl(url, allowHttp) {
85
+ let u;
86
+ try {
87
+ u = new URL(url);
88
+ }
89
+ catch {
90
+ throw new DownloadError(`invalid asset URL: ${url}`, 0);
91
+ }
92
+ if (u.protocol !== "https:" && !(allowHttp && u.protocol === "http:")) {
93
+ throw new DownloadError(`refusing non-https asset URL (${u.protocol}//…)`, 0);
94
+ }
95
+ const host = u.hostname.toLowerCase().replace(/^\[|\]$/g, "");
96
+ if (isInternalHost(host)) {
97
+ throw new DownloadError(`refusing asset URL to internal host: ${host}`, 0);
98
+ }
99
+ }
100
+ function isInternalHost(host) {
101
+ if (host === "localhost" || host.endsWith(".localhost"))
102
+ return true;
103
+ if (host === "metadata.google.internal")
104
+ return true;
105
+ if (host.includes(":")) {
106
+ if (host === "::1" || host === "::")
107
+ return true;
108
+ if (host.startsWith("fe80:"))
109
+ return true;
110
+ if (/^f[cd][0-9a-f]{2}:/.test(host))
111
+ return true;
112
+ if (host.startsWith("::ffff:")) {
113
+ const rest = host.slice("::ffff:".length);
114
+ if (rest.includes("."))
115
+ return isInternalHost(rest);
116
+ const hx = rest.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
117
+ if (hx) {
118
+ const hi = parseInt(hx[1], 16);
119
+ const lo = parseInt(hx[2], 16);
120
+ return isInternalHost(`${hi >> 8}.${hi & 255}.${lo >> 8}.${lo & 255}`);
121
+ }
122
+ }
123
+ return false;
124
+ }
125
+ const m = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
126
+ if (m) {
127
+ const a = Number(m[1]);
128
+ const b = Number(m[2]);
129
+ if (a === 0 || a === 10 || a === 127)
130
+ return true;
131
+ if (a === 169 && b === 254)
132
+ return true;
133
+ if (a === 172 && b >= 16 && b <= 31)
134
+ return true;
135
+ if (a === 192 && b === 168)
136
+ return true;
137
+ return false;
138
+ }
139
+ return false;
140
+ }
@@ -0,0 +1,203 @@
1
+ import { downloadAll } from "./downloader.js";
2
+ import { resolveFonts } from "./font-resolver.js";
3
+ function warn(opts, message) {
4
+ if (opts.onWarn)
5
+ opts.onWarn(message);
6
+ else
7
+ process.stderr.write(`Warning: ${message}\n`);
8
+ }
9
+ export async function fetchDesignData(refs, client, opts = {}) {
10
+ const varMap = {};
11
+ for (const ref of refs) {
12
+ try {
13
+ const m = await client.getVariableDefs(ref.fileKey, ref.nodeId);
14
+ Object.assign(varMap, m);
15
+ }
16
+ catch (err) {
17
+ warn(opts, `Figma variables read failed for ${ref.url}: ${errMsg(err)}`);
18
+ }
19
+ }
20
+ const libraries = new Set();
21
+ if (client.getLibraries) {
22
+ for (const ref of refs) {
23
+ try {
24
+ for (const lib of await client.getLibraries(ref.fileKey))
25
+ libraries.add(lib);
26
+ }
27
+ catch (err) {
28
+ warn(opts, `Figma libraries read failed for ${ref.url}: ${errMsg(err)}`);
29
+ }
30
+ }
31
+ }
32
+ const tokens = normalizeVariablesToTokens(varMap, opts.source);
33
+ const screens = await captureScreens(refs, client, opts);
34
+ const assets = [];
35
+ if (opts.root) {
36
+ assets.push(...(await captureAssets(refs, client, tokens.typography, opts)));
37
+ }
38
+ return {
39
+ tokens,
40
+ screens,
41
+ assets,
42
+ libraries: [...libraries],
43
+ };
44
+ }
45
+ async function captureScreens(refs, client, opts) {
46
+ if (!client.captureScreens)
47
+ return [];
48
+ const out = [];
49
+ const seen = new Set();
50
+ for (const ref of refs) {
51
+ try {
52
+ for (const s of await client.captureScreens(ref.fileKey, ref.nodeId)) {
53
+ const key = s.nodeId
54
+ ? `id:${s.file ?? ""}#${s.nodeId}`
55
+ : `nm:${s.file ?? ""}#${s.name}#${s.summary ?? ""}`;
56
+ if (seen.has(key))
57
+ continue;
58
+ seen.add(key);
59
+ out.push(s);
60
+ }
61
+ }
62
+ catch (err) {
63
+ warn(opts, `Figma screens read failed for ${ref.url}: ${errMsg(err)}`);
64
+ }
65
+ }
66
+ return out;
67
+ }
68
+ async function captureAssets(refs, client, typography, opts) {
69
+ const root = opts.root;
70
+ const format = opts.assets?.format ?? "svg";
71
+ const scale = opts.assets?.scale ?? 2;
72
+ const maxBytes = opts.assets?.maxBytes ?? 5_000_000;
73
+ const fetchImpl = opts.deps?.fetchImpl;
74
+ const pending = [];
75
+ const seenPaths = new Set();
76
+ if (client.captureAssets) {
77
+ for (const ref of refs) {
78
+ try {
79
+ const found = await client.captureAssets(ref.fileKey, ref.nodeId, { format, scale });
80
+ for (const p of found) {
81
+ if (seenPaths.has(p.path))
82
+ continue;
83
+ seenPaths.add(p.path);
84
+ pending.push(p);
85
+ }
86
+ }
87
+ catch (err) {
88
+ warn(opts, `Figma asset read failed for ${ref.url}: ${errMsg(err)}`);
89
+ }
90
+ }
91
+ }
92
+ const out = [];
93
+ if (pending.length) {
94
+ const entries = await downloadAll(pending, root, {
95
+ ...(fetchImpl ? { fetchImpl } : {}),
96
+ maxBytes,
97
+ ...(opts.concurrency ? { concurrency: opts.concurrency } : {}),
98
+ ...(opts.onWarn ? { onWarn: opts.onWarn } : {}),
99
+ });
100
+ for (const e of entries) {
101
+ if (e.status === "downloaded")
102
+ out.push(e);
103
+ else
104
+ warn(opts, `asset "${e.name}" could not be downloaded — left pending (not catalogued)`);
105
+ }
106
+ }
107
+ const fontEntries = await resolveFonts(typography, {
108
+ root,
109
+ brandDir: opts.fonts?.brandDir ?? "assets/fonts",
110
+ google: opts.fonts?.google !== false,
111
+ os: opts.fonts?.os !== false,
112
+ maxBytes,
113
+ deps: {
114
+ ...(fetchImpl ? { fetchImpl } : {}),
115
+ ...(opts.deps?.fcMatch ? { fcMatch: opts.deps.fcMatch } : {}),
116
+ ...(opts.deps?.osFontDirs ? { osFontDirs: opts.deps.osFontDirs } : {}),
117
+ },
118
+ ...(opts.onWarn ? { onWarn: opts.onWarn } : {}),
119
+ });
120
+ out.push(...fontEntries);
121
+ return out;
122
+ }
123
+ export function isColorValue(value) {
124
+ const v = value.trim().toLowerCase();
125
+ return /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(v) || /^rgba?\(/.test(v);
126
+ }
127
+ export function isNumericValue(value) {
128
+ return /^-?\d+(\.\d+)?(px|rem|em|pt|%)?$/.test(value.trim());
129
+ }
130
+ export function classifyToken(name, value) {
131
+ const lname = name.toLowerCase();
132
+ if (isColorValue(value))
133
+ return "color";
134
+ if (value === "true" || value === "false")
135
+ return "unknown";
136
+ if (/(radius|radii|corner|rounded)/.test(lname))
137
+ return "radius";
138
+ if (/(font|typograph|leading|line-?height|letter-?spacing|\btext\b|\btype\b)/.test(lname)) {
139
+ return "typography";
140
+ }
141
+ if (!isNumericValue(value) && /(colou?r|fill|bg|background|surface|border|stroke|brand|shadow)/.test(lname)) {
142
+ return "color";
143
+ }
144
+ if (/(spac|sizing|gap|pad|margin|inset|\bsize\b|dimension|width|height)/.test(lname)) {
145
+ return "spacing";
146
+ }
147
+ return "unknown";
148
+ }
149
+ function typographyField(name, value) {
150
+ const ln = name.toLowerCase();
151
+ if (/weight/.test(ln))
152
+ return { fontWeight: value };
153
+ if (/(line-?height|leading)/.test(ln))
154
+ return { lineHeight: value };
155
+ if (/(letter-?spacing|tracking)/.test(ln))
156
+ return { letterSpacing: value };
157
+ if (/size/.test(ln) || isNumericValue(value))
158
+ return { fontSize: value };
159
+ return { fontFamily: value };
160
+ }
161
+ export function normalizeVariablesToTokens(map, source) {
162
+ const colors = [];
163
+ const spacing = [];
164
+ const radii = [];
165
+ const typography = [];
166
+ for (const [rawName, rawValue] of Object.entries(map)) {
167
+ const name = rawName.trim();
168
+ const value = String(rawValue).trim();
169
+ if (!name || !value)
170
+ continue;
171
+ switch (classifyToken(name, value)) {
172
+ case "color":
173
+ colors.push({ name, value });
174
+ break;
175
+ case "radius":
176
+ radii.push({ name, value });
177
+ break;
178
+ case "spacing":
179
+ spacing.push({ name, value });
180
+ break;
181
+ case "typography":
182
+ typography.push({ name, ...typographyField(name, value) });
183
+ break;
184
+ }
185
+ }
186
+ const tokens = { colors, typography, spacing };
187
+ if (radii.length)
188
+ tokens.radii = radii;
189
+ if (source) {
190
+ tokens.source = {
191
+ provider: source.provider,
192
+ ...(source.fileKey ? { fileKey: source.fileKey } : {}),
193
+ ...(source.generatedAt ? { generatedAt: source.generatedAt } : {}),
194
+ };
195
+ }
196
+ return tokens;
197
+ }
198
+ export function seededTokensCount(t) {
199
+ return t.colors.length + t.typography.length + t.spacing.length + (t.radii?.length ?? 0);
200
+ }
201
+ function errMsg(err) {
202
+ return err instanceof Error ? err.message : String(err);
203
+ }