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.
- package/CHANGELOG.md +28 -0
- package/README.md +11 -3
- package/dist/_core/dist/agents/prompt-builder.js +10 -8
- package/dist/_core/dist/audit/schema.js +1 -0
- package/dist/_core/dist/design/asset-normalize.js +37 -0
- package/dist/_core/dist/design/asset-paths.js +23 -0
- package/dist/_core/dist/design/downloader.js +140 -0
- package/dist/_core/dist/design/fetcher.js +203 -0
- package/dist/_core/dist/design/figma-client.js +198 -0
- package/dist/_core/dist/design/figma-credentials.js +41 -0
- package/dist/_core/dist/design/figma-dialects.js +75 -0
- package/dist/_core/dist/design/figma-oauth.js +71 -0
- package/dist/_core/dist/design/figma-rest.js +277 -0
- package/dist/_core/dist/design/font-resolver.js +298 -0
- package/dist/_core/dist/design/index.js +12 -0
- package/dist/_core/dist/design/mapper.js +22 -2
- package/dist/_core/dist/design/scaffold.js +58 -2
- package/dist/_core/dist/design/screens.js +202 -0
- package/dist/_core/dist/design/slug.js +9 -0
- package/dist/_core/dist/design/tokens.js +2 -1
- package/dist/_core/dist/gates/design-gates.js +1 -9
- package/dist/_core/dist/jira/mcp-client.js +15 -131
- package/dist/_core/dist/jira/oauth.js +1 -79
- package/dist/_core/dist/transport/index.js +2 -0
- package/dist/_core/dist/transport/mcp.js +134 -0
- package/dist/_core/dist/transport/oauth.js +83 -0
- package/dist/_core/package.json +1 -1
- package/dist/_core/templates/spec.md.hbs +28 -3
- package/dist/commands/design/connect.js +105 -0
- package/dist/commands/design/disconnect.js +19 -0
- package/dist/commands/design/pull.js +65 -0
- package/dist/commands/design/status.js +76 -0
- package/dist/commands/design/sync.js +55 -0
- package/dist/commands/init.js +29 -240
- package/dist/commands/new.js +36 -2
- package/dist/commands/setup.js +149 -0
- package/dist/shared/design.js +93 -0
- package/dist/shared/init-core.js +260 -0
- package/dist/shared/setup/agent-resolver.js +54 -0
- package/dist/shared/setup/banner.js +19 -0
- package/dist/shared/setup/detect-stack.js +31 -0
- package/dist/shared/setup/onboard.js +18 -0
- package/dist/shared/setup/plan.js +93 -0
- package/dist/shared/setup/wizard.js +164 -0
- package/package.json +2 -1
- package/scripts/prepare-npm.mjs +2 -1
- package/templates/commands/plan.md +5 -3
- package/templates/commands/specify.md +9 -5
- 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
|
|
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
|
-
|
|
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
|
|
37
|
-
"
|
|
38
|
-
"
|
|
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/`)
|
|
71
|
-
"
|
|
71
|
+
"6. Se a feature tiver design (pasta `design/`): se `design/tokens.json` já 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/`)
|
|
101
|
-
"
|
|
102
|
-
"
|
|
102
|
+
"6. Se a feature tiver design (pasta `design/`): consuma os assets já catalogados em",
|
|
103
|
+
" `design/assets-manifest.json` (baixados em `assets/{images,icons,fonts}`) e baixe via",
|
|
104
|
+
" Figma MCP só o que faltar; referencie por caminho concreto (tags `[screen: X]` / `[asset: X]`).",
|
|
103
105
|
].join("\n"),
|
|
104
106
|
},
|
|
105
107
|
implement: {
|
|
@@ -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
|
+
}
|