synthesisui 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SynthesisUI
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,54 @@
1
+ # synthesisui
2
+
3
+ CLI para trazer design systems publicados no [SynthesisUI](https://www.synthesisui.com)
4
+ para dentro de qualquer projeto. Materializa o sistema em `_local/ds/<slug>/` e injeta
5
+ um bloco gerenciado no `CLAUDE.md` da raiz, de forma que o Claude Code construa
6
+ componentes seguindo o design system.
7
+
8
+ ## Uso
9
+
10
+ Sem instalar nada:
11
+
12
+ ```bash
13
+ npx synthesisui login # conecta o CLI à sua conta (device-flow no browser)
14
+ npx synthesisui list # lista os design systems disponíveis
15
+ npx synthesisui add <slug> # traz um DS para _local/ds/<slug>/
16
+ ```
17
+
18
+ Ou instale globalmente:
19
+
20
+ ```bash
21
+ npm install -g synthesisui
22
+ synthesisui add halogen
23
+ ```
24
+
25
+ ### O que o `add` materializa
26
+
27
+ Em `_local/ds/<slug>/`:
28
+
29
+ - `design-system.json` — a verdade canônica do design system
30
+ - `tokens.css` — CSS custom properties escopadas por `data-ds`
31
+ - `GUIDE.md` — instruções para o agente (papéis semânticos, mood, recipes, como adicionar componentes)
32
+ - `.lock` — slug + versão pinada (reproduzível)
33
+
34
+ E injeta um bloco idempotente `<!-- synthesisui:start/end -->` no `CLAUDE.md` da raiz,
35
+ refletindo todos os DSs instalados.
36
+
37
+ ## Autenticação
38
+
39
+ `synthesisui login` usa device-flow (RFC 8628): abre o browser, você confirma um código,
40
+ e o token é salvo em `~/.synthesisui/credentials.json` (por máquina). Logout = apagar esse arquivo.
41
+
42
+ ## Registry
43
+
44
+ Por padrão aponta para `https://www.synthesisui.com`. Sobrescreva com:
45
+
46
+ ```bash
47
+ synthesisui list --registry http://localhost:3000
48
+ # ou
49
+ SYNTHESISUI_REGISTRY_URL=http://localhost:3000 synthesisui list
50
+ ```
51
+
52
+ ## Licença
53
+
54
+ MIT
@@ -0,0 +1,83 @@
1
+ import { readdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ const START = "<!-- synthesisui:start -->";
4
+ const END = "<!-- synthesisui:end -->";
5
+ /** Lê os DSs instalados a partir dos .lock em _local/ds/<slug>/. */
6
+ async function readInstalled(projectRoot) {
7
+ const dsDir = join(projectRoot, "_local", "ds");
8
+ let entries = [];
9
+ try {
10
+ const dirents = await readdir(dsDir, { withFileTypes: true });
11
+ entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
12
+ }
13
+ catch {
14
+ return [];
15
+ }
16
+ const locks = [];
17
+ for (const slug of entries.sort()) {
18
+ try {
19
+ const raw = await readFile(join(dsDir, slug, ".lock"), "utf8");
20
+ const lock = JSON.parse(raw);
21
+ locks.push({ slug: lock.slug, name: lock.name, version: lock.version });
22
+ }
23
+ catch {
24
+ // pasta sem .lock válido — ignora
25
+ }
26
+ }
27
+ return locks;
28
+ }
29
+ function renderRegion(installed) {
30
+ if (installed.length === 0) {
31
+ return `${START}\n${END}`;
32
+ }
33
+ const lines = installed
34
+ .map((ds) => `- **${ds.name}** (\`${ds.slug}\`, v${ds.version}) — guia: \`_local/ds/${ds.slug}/GUIDE.md\``)
35
+ .join("\n");
36
+ const body = `## Design Systems (via SynthesisUI)
37
+
38
+ Este projeto usa design system(s) trazido(s) pelo \`synthesisui\` CLI. **Ao criar ou editar
39
+ componentes, leia o GUIDE.md do sistema e siga-o:** use apenas tokens semânticos
40
+ (\`var(--ds-color-semantic-*)\`, \`--ds-spacing-*\`, etc.), escope a UI com \`data-ds="<slug>"\`,
41
+ e reaproveite as classes \`.ds-*\`. Não use valores crus fora da escala do sistema.
42
+
43
+ ${lines}
44
+
45
+ _Bloco gerenciado pelo CLI — não edite à mão; rode \`synthesisui add <slug>\` para atualizar._`;
46
+ return `${START}\n${body}\n${END}`;
47
+ }
48
+ /**
49
+ * Regenera o bloco gerenciado no CLAUDE.md da raiz refletindo todos os DSs
50
+ * instalados. Idempotente: substitui o trecho entre os marcadores se existir,
51
+ * senão cria o arquivo / anexa o bloco. Retorna se o arquivo foi criado.
52
+ */
53
+ export async function syncClaudeMd(projectRoot) {
54
+ const installed = await readInstalled(projectRoot);
55
+ const region = renderRegion(installed);
56
+ const path = join(projectRoot, "CLAUDE.md");
57
+ let existing = null;
58
+ try {
59
+ existing = await readFile(path, "utf8");
60
+ }
61
+ catch {
62
+ existing = null;
63
+ }
64
+ if (existing === null) {
65
+ await writeFile(path, `${region}\n`, "utf8");
66
+ return { created: true, count: installed.length };
67
+ }
68
+ const startIdx = existing.indexOf(START);
69
+ const endIdx = existing.indexOf(END);
70
+ let next;
71
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
72
+ next =
73
+ existing.slice(0, startIdx) +
74
+ region +
75
+ existing.slice(endIdx + END.length);
76
+ }
77
+ else {
78
+ const sep = existing.endsWith("\n") ? "\n" : "\n\n";
79
+ next = `${existing}${sep}${region}\n`;
80
+ }
81
+ await writeFile(path, next, "utf8");
82
+ return { created: false, count: installed.length };
83
+ }
@@ -0,0 +1,50 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { syncClaudeMd } from "../claude-md.js";
4
+ import { resolveRegistry } from "../config.js";
5
+ import { buildGuide } from "../guide.js";
6
+ import { fetchDesignSystem } from "../registry.js";
7
+ /**
8
+ * Materializa um DS publicado em `_local/ds/<slug>/` e atualiza o CLAUDE.md.
9
+ */
10
+ export async function add(slug, opts) {
11
+ const base = resolveRegistry(opts.registry);
12
+ const projectRoot = opts.dir ?? process.cwd();
13
+ console.log(`→ buscando "${slug}" em ${base} …`);
14
+ const payload = await fetchDesignSystem(base, slug);
15
+ const targetDir = join(projectRoot, "_local", "ds", payload.slug);
16
+ await mkdir(targetDir, { recursive: true });
17
+ // 1. artifacts compilados pelo servidor (tokens.css, e futuros theme.css…)
18
+ for (const [filename, content] of Object.entries(payload.artifacts)) {
19
+ await writeFile(join(targetDir, filename), content, "utf8");
20
+ }
21
+ // 2. verdade canônica
22
+ await writeFile(join(targetDir, "design-system.json"), `${JSON.stringify(payload.document, null, 2)}\n`, "utf8");
23
+ // 3. guia para o agente (gerado client-side a partir do document)
24
+ await writeFile(join(targetDir, "GUIDE.md"), buildGuide(payload), "utf8");
25
+ // 4. lock reproduzível
26
+ const lock = {
27
+ slug: payload.slug,
28
+ name: payload.name,
29
+ version: payload.version,
30
+ registry: base,
31
+ fetchedAt: new Date().toISOString(),
32
+ };
33
+ await writeFile(join(targetDir, ".lock"), `${JSON.stringify(lock, null, 2)}\n`, "utf8");
34
+ // 5. descoberta pelo agente
35
+ const claudeMd = await syncClaudeMd(projectRoot);
36
+ const files = [
37
+ ...Object.keys(payload.artifacts),
38
+ "design-system.json",
39
+ "GUIDE.md",
40
+ ".lock",
41
+ ];
42
+ console.log(`✓ ${payload.name} v${payload.version} → _local/ds/${payload.slug}/`);
43
+ console.log(` ${files.join(", ")}`);
44
+ console.log(` CLAUDE.md ${claudeMd.created ? "criado" : "atualizado"} (${claudeMd.count} sistema(s) instalado(s))`);
45
+ console.log("");
46
+ console.log("Próximos passos:");
47
+ console.log(` • @import "_local/ds/${payload.slug}/tokens.css" no seu CSS global`);
48
+ console.log(` • escope sua UI com data-ds="${payload.slug}"`);
49
+ console.log(` • detalhes e regras em _local/ds/${payload.slug}/GUIDE.md`);
50
+ }
@@ -0,0 +1,17 @@
1
+ import { resolveRegistry } from "../config.js";
2
+ import { fetchList } from "../registry.js";
3
+ /** Lista os design systems publicados disponíveis no registry. */
4
+ export async function list(opts) {
5
+ const base = resolveRegistry(opts.registry);
6
+ const systems = await fetchList(base);
7
+ if (systems.length === 0) {
8
+ console.log(`Nenhum design system publicado em ${base}.`);
9
+ return;
10
+ }
11
+ console.log(`Design systems disponíveis (${base}):\n`);
12
+ const width = Math.max(...systems.map((s) => s.slug.length));
13
+ for (const s of systems) {
14
+ console.log(` ${s.slug.padEnd(width)} ${s.name} (v${s.version})`);
15
+ }
16
+ console.log(`\nTraga um com: synthesisui add <slug>`);
17
+ }
@@ -0,0 +1,79 @@
1
+ import { spawn } from "node:child_process";
2
+ import { resolveRegistry, writeToken } from "../config.js";
3
+ import { RegistryError } from "../registry.js";
4
+ const CLIENT_ID = "synthesisui-cli";
5
+ const GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
6
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
7
+ /** Tenta abrir o browser no SO; silencioso se não der. */
8
+ function openBrowser(url) {
9
+ const [cmd, args] = process.platform === "darwin"
10
+ ? ["open", [url]]
11
+ : process.platform === "win32"
12
+ ? ["cmd", ["/c", "start", "", url]]
13
+ : ["xdg-open", [url]];
14
+ try {
15
+ const child = spawn(cmd, args, {
16
+ stdio: "ignore",
17
+ detached: true,
18
+ });
19
+ child.on("error", () => { });
20
+ child.unref();
21
+ }
22
+ catch {
23
+ // sem browser disponível — o usuário abre manualmente
24
+ }
25
+ }
26
+ /** Device authorization (RFC 8628): abre o browser, espera a aprovação. */
27
+ export async function login(opts) {
28
+ const base = resolveRegistry(opts.registry);
29
+ const codeRes = await fetch(`${base}/api/auth/device/code`, {
30
+ method: "POST",
31
+ headers: { "content-type": "application/json" },
32
+ body: JSON.stringify({ client_id: CLIENT_ID }),
33
+ }).catch(() => null);
34
+ if (!codeRes || !codeRes.ok) {
35
+ throw new RegistryError(`Não consegui iniciar o login em ${base}` +
36
+ (codeRes
37
+ ? ` (HTTP ${codeRes.status}).`
38
+ : ". Confira a URL e a conexão."));
39
+ }
40
+ const code = (await codeRes.json());
41
+ console.log("\nPara conectar o CLI à sua conta:");
42
+ console.log(` 1. abra: ${code.verification_uri}`);
43
+ console.log(` 2. confirme o código: ${code.user_code}\n`);
44
+ console.log("(tentando abrir o navegador…)");
45
+ openBrowser(code.verification_uri_complete);
46
+ let interval = (code.interval || 5) * 1000;
47
+ const deadline = Date.now() + (code.expires_in || 900) * 1000;
48
+ process.stdout.write("aguardando aprovação");
49
+ while (Date.now() < deadline) {
50
+ await sleep(interval);
51
+ process.stdout.write(".");
52
+ const tokenRes = await fetch(`${base}/api/auth/device/token`, {
53
+ method: "POST",
54
+ headers: { "content-type": "application/json" },
55
+ body: JSON.stringify({
56
+ grant_type: GRANT_TYPE,
57
+ device_code: code.device_code,
58
+ client_id: CLIENT_ID,
59
+ }),
60
+ }).catch(() => null);
61
+ const data = tokenRes
62
+ ? (await tokenRes.json().catch(() => ({})))
63
+ : {};
64
+ if (tokenRes?.ok && typeof data.access_token === "string") {
65
+ await writeToken(data.access_token, base);
66
+ console.log("\n✓ Login concluído. Token salvo em ~/.synthesisui/credentials.json");
67
+ return;
68
+ }
69
+ const err = data.error;
70
+ if (err === "authorization_pending")
71
+ continue;
72
+ if (err === "slow_down") {
73
+ interval += 5000;
74
+ continue;
75
+ }
76
+ throw new RegistryError(`\nLogin falhou: ${data.error_description ?? err ?? tokenRes?.status ?? "erro desconhecido"}`);
77
+ }
78
+ throw new RegistryError("\nO código expirou antes da aprovação. Rode `synthesisui login` de novo.");
79
+ }
package/dist/config.js ADDED
@@ -0,0 +1,37 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ /**
5
+ * Registry padrão (domínio canônico de produção). Sobrescrevível por
6
+ * `--registry <url>` ou `SYNTHESISUI_REGISTRY_URL` (ex.: http://localhost:3000
7
+ * em dev).
8
+ */
9
+ export const DEFAULT_REGISTRY = "https://www.synthesisui.com";
10
+ export function resolveRegistry(flag) {
11
+ const base = flag || process.env.SYNTHESISUI_REGISTRY_URL || DEFAULT_REGISTRY;
12
+ return base.replace(/\/+$/, ""); // sem barra final
13
+ }
14
+ /** Onde o token do device-flow (passo 3) vive — por máquina, na home. */
15
+ export const credentialsPath = join(homedir(), ".synthesisui", "credentials.json");
16
+ /**
17
+ * Lê o token salvo, se existir. Hoje opcional (portão aberto); o device-flow
18
+ * (passo 3) é quem vai gravá-lo. Mandamos como Bearer quando presente.
19
+ */
20
+ export async function readToken() {
21
+ try {
22
+ const raw = await readFile(credentialsPath, "utf8");
23
+ const parsed = JSON.parse(raw);
24
+ return parsed.token ?? null;
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ /** Persiste o token do device-flow (chmod 600, dir só do usuário). */
31
+ export async function writeToken(token, registry) {
32
+ await mkdir(dirname(credentialsPath), { recursive: true, mode: 0o700 });
33
+ const payload = { token, registry, savedAt: new Date().toISOString() };
34
+ await writeFile(credentialsPath, `${JSON.stringify(payload, null, 2)}\n`, {
35
+ mode: 0o600,
36
+ });
37
+ }
package/dist/guide.js ADDED
@@ -0,0 +1,97 @@
1
+ const kebab = (v) => v.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
2
+ const list = (items) => items.length ? items.map((i) => `\`${i}\``).join(", ") : "_(nenhum)_";
3
+ /**
4
+ * Gera o GUIDE.md — instruções *para o agente* sobre como construir
5
+ * componentes seguindo o DS. É a peça que faz "com claude-code eu crio os
6
+ * componentes" funcionar: não basta os tokens, o agente precisa das regras
7
+ * e do vocabulário real (nomes de tokens semânticos e de recipes).
8
+ */
9
+ export function buildGuide(payload) {
10
+ const { document: doc, slug, name, version } = payload;
11
+ const { meta, foundations, motion, components } = doc;
12
+ const semanticRoles = Object.keys(foundations.color.semantic);
13
+ const hasAlt = foundations.color.semanticAlt &&
14
+ Object.keys(foundations.color.semanticAlt).length > 0;
15
+ const altScheme = meta.scheme === "light" ? "dark" : "light";
16
+ const componentLines = Object.entries(components).map(([cname, recipe]) => {
17
+ const cls = `.ds-${kebab(cname)}`;
18
+ const axes = Object.entries(recipe.variants).map(([axis, opts]) => {
19
+ const options = Object.keys(opts);
20
+ return `\`data-${kebab(axis)}="${options.join("|")}"\``;
21
+ });
22
+ const variantsText = axes.length ? ` — variantes: ${axes.join(", ")}` : "";
23
+ return `- **${cname}** (\`${cls}\`)${variantsText}\n ${recipe.description}`;
24
+ });
25
+ const artifactList = Object.keys(payload.artifacts)
26
+ .map((f) => `\`${f}\``)
27
+ .join(", ");
28
+ return `# Design System: ${name}
29
+
30
+ > Gerado por \`synthesisui add ${slug}\` (v${version}). **Não edite à mão** —
31
+ > rode \`synthesisui add ${slug}\` de novo para atualizar.
32
+
33
+ ${meta.tagline}
34
+
35
+ **Mood:** ${meta.mood.join(" · ")}
36
+ **Modo padrão:** ${meta.scheme}${hasAlt ? ` (suporta toggle para ${altScheme})` : ""}
37
+ ${meta.sourceUrl ? `**Releitura de:** ${meta.sourceUrl}` : "**Sistema autoral.**"}
38
+
39
+ ${meta.narrative}
40
+
41
+ ---
42
+
43
+ ## Como aplicar
44
+
45
+ 1. Importe os tokens uma vez no CSS global do projeto:
46
+ \`\`\`css
47
+ @import "./_local/ds/${slug}/tokens.css";
48
+ \`\`\`
49
+ (ajuste o caminho relativo conforme a localização do seu CSS.)
50
+
51
+ 2. Envolva a árvore que deve usar o sistema com o atributo de escopo:
52
+ \`\`\`html
53
+ <div data-ds="${slug}">…sua UI aqui…</div>
54
+ \`\`\`
55
+ Todas as custom properties \`--ds-*\` e as classes \`.ds-*\` só valem dentro desse escopo.
56
+ ${hasAlt
57
+ ? `
58
+ 3. Light/dark: um ancestral com \`data-scheme="${altScheme}"\` troca os papéis neutros para o modo oposto.
59
+ \`\`\`html
60
+ <div data-scheme="${altScheme}"><div data-ds="${slug}">…</div></div>
61
+ \`\`\`
62
+ `
63
+ : ""}
64
+ Artefatos disponíveis em \`_local/ds/${slug}/\`: ${artifactList}, \`design-system.json\` (verdade canônica), \`GUIDE.md\` (este arquivo).
65
+
66
+ ---
67
+
68
+ ## Regras (siga ao criar componentes)
69
+
70
+ - **Use SEMPRE tokens semânticos**, nunca valores crus nem primitivas diretas.
71
+ Cor: \`var(--ds-color-semantic-<papel>)\`. Os papéis são: ${list(semanticRoles)}.
72
+ - Primitivas (\`--ds-color-<paleta>-<step>\`) existem mas **não** devem ser referenciadas direto —
73
+ elas alimentam os papéis semânticos.
74
+ - Espaçamento → \`var(--ds-spacing-<key>)\`: ${list(Object.keys(foundations.spacing))}.
75
+ - Raio → \`var(--ds-radius-<key>)\`: ${list(Object.keys(foundations.radius))}.
76
+ - Sombra → \`var(--ds-shadow-<key>)\`: ${list(Object.keys(foundations.shadow))}.
77
+ - Tipografia: famílias \`--ds-typography-families-{display,body,mono}\` (${foundations.typography.families.display}, ${foundations.typography.families.body}, ${foundations.typography.families.mono});
78
+ escala \`--ds-typography-scale-<key>-font-size\` etc.: ${list(Object.keys(foundations.typography.scale))}.
79
+ - Motion: durações \`--ds-motion-durations-<key>\` (${list(Object.keys(motion.durations))}) e
80
+ easings \`--ds-motion-easings-<key>\` (${list(Object.keys(motion.easings))}).
81
+ - Ao **criar um componente novo** que o DS ainda não cobre: componha a partir desses tokens
82
+ semânticos para herdar a identidade do sistema; não invente cores/medidas fora da escala.
83
+
84
+ ---
85
+
86
+ ## Componentes prontos
87
+
88
+ Cada recipe vira uma classe \`.ds-<nome>\` (dentro do escopo \`[data-ds="${slug}"]\`).
89
+ Variantes são atributos \`data-<eixo>="<opção>"\`; estados (hover/focus/active/disabled) já vêm no CSS.
90
+
91
+ ${componentLines.join("\n\n")}
92
+
93
+ ---
94
+
95
+ _Verdade canônica completa (incluindo valores e keyframes) em \`design-system.json\`._
96
+ `;
97
+ }
package/dist/index.js ADDED
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ import { add } from "./commands/add.js";
3
+ import { list } from "./commands/list.js";
4
+ import { login } from "./commands/login.js";
5
+ import { RegistryError } from "./registry.js";
6
+ const HELP = `synthesisui — traz design systems do SynthesisUI para o seu projeto
7
+
8
+ Uso:
9
+ synthesisui login [opções] conecta o CLI à sua conta (device-flow)
10
+ synthesisui list [opções] lista os DSs publicados
11
+ synthesisui add <slug> [opções] materializa um DS em _local/ds/<slug>/
12
+
13
+ Opções:
14
+ --registry <url> URL do registry (ou env SYNTHESISUI_REGISTRY_URL)
15
+ --dir <path> raiz do projeto consumidor (default: diretório atual)
16
+ -h, --help esta ajuda
17
+
18
+ Exemplos:
19
+ synthesisui login
20
+ synthesisui list
21
+ synthesisui add halogen
22
+ synthesisui add halogen --registry http://localhost:3737
23
+ `;
24
+ /** Extrai `--flag value` simples e os posicionais restantes. */
25
+ function parseFlags(argv) {
26
+ const positionals = [];
27
+ const flags = {};
28
+ for (let i = 0; i < argv.length; i++) {
29
+ const arg = argv[i];
30
+ if (arg === "-h" || arg === "--help") {
31
+ flags.help = true;
32
+ }
33
+ else if (arg.startsWith("--")) {
34
+ const key = arg.slice(2);
35
+ const next = argv[i + 1];
36
+ if (next !== undefined && !next.startsWith("--")) {
37
+ flags[key] = next;
38
+ i++;
39
+ }
40
+ else {
41
+ flags[key] = true;
42
+ }
43
+ }
44
+ else {
45
+ positionals.push(arg);
46
+ }
47
+ }
48
+ return { positionals, flags };
49
+ }
50
+ async function main() {
51
+ const { positionals, flags } = parseFlags(process.argv.slice(2));
52
+ const [command, ...args] = positionals;
53
+ if (!command || flags.help || command === "help") {
54
+ console.log(HELP);
55
+ return;
56
+ }
57
+ const registry = typeof flags.registry === "string" ? flags.registry : undefined;
58
+ const dir = typeof flags.dir === "string" ? flags.dir : undefined;
59
+ switch (command) {
60
+ case "list":
61
+ await list({ registry });
62
+ break;
63
+ case "add": {
64
+ const slug = args[0];
65
+ if (!slug) {
66
+ console.error("erro: informe o slug — `synthesisui add <slug>`");
67
+ process.exitCode = 1;
68
+ return;
69
+ }
70
+ await add(slug, { registry, dir });
71
+ break;
72
+ }
73
+ case "login":
74
+ await login({ registry });
75
+ break;
76
+ default:
77
+ console.error(`comando desconhecido: "${command}"\n`);
78
+ console.log(HELP);
79
+ process.exitCode = 1;
80
+ }
81
+ }
82
+ main().catch((err) => {
83
+ if (err instanceof RegistryError) {
84
+ console.error(`erro: ${err.message}`);
85
+ }
86
+ else {
87
+ console.error(err);
88
+ }
89
+ process.exitCode = 1;
90
+ });
@@ -0,0 +1,39 @@
1
+ import { readToken } from "./config.js";
2
+ export class RegistryError extends Error {
3
+ }
4
+ async function authHeaders() {
5
+ const token = await readToken();
6
+ return token ? { Authorization: `Bearer ${token}` } : {};
7
+ }
8
+ async function request(url) {
9
+ let res;
10
+ try {
11
+ res = await fetch(url, { headers: await authHeaders() });
12
+ }
13
+ catch {
14
+ throw new RegistryError(`Não consegui falar com o registry em ${url}. ` +
15
+ `Confira a URL (--registry / SYNTHESISUI_REGISTRY_URL) e a conexão.`);
16
+ }
17
+ return res;
18
+ }
19
+ /** Lista os design systems publicados disponíveis. */
20
+ export async function fetchList(base) {
21
+ const res = await request(`${base}/api/registry/ds`);
22
+ if (!res.ok) {
23
+ throw new RegistryError(`Registry respondeu ${res.status} ao listar.`);
24
+ }
25
+ const body = (await res.json());
26
+ return body.designSystems ?? [];
27
+ }
28
+ /** Busca um DS publicado já compilado (document + artifacts). */
29
+ export async function fetchDesignSystem(base, slug) {
30
+ const res = await request(`${base}/api/registry/ds/${encodeURIComponent(slug)}`);
31
+ if (res.status === 404) {
32
+ throw new RegistryError(`Nenhum design system publicado com slug "${slug}". ` +
33
+ `Rode \`synthesisui list\` para ver os disponíveis.`);
34
+ }
35
+ if (!res.ok) {
36
+ throw new RegistryError(`Registry respondeu ${res.status} ao buscar "${slug}".`);
37
+ }
38
+ return (await res.json());
39
+ }
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Espelho mínimo do contrato do registry — o CLI é standalone e NÃO importa
3
+ * `@synthesisui-hub/ds-contracts` (só consome o JSON do endpoint). Tipamos
4
+ * apenas o que o CLI lê para gerar o GUIDE.md e o .lock.
5
+ */
6
+ export {};
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "synthesisui",
3
+ "version": "0.1.0",
4
+ "description": "Traz design systems do SynthesisUI para qualquer projeto (materializa em _local/ds/).",
5
+ "type": "module",
6
+ "bin": {
7
+ "synthesisui": "dist/index.js"
8
+ },
9
+ "files": ["dist"],
10
+ "keywords": [
11
+ "synthesisui",
12
+ "design-system",
13
+ "design-tokens",
14
+ "cli",
15
+ "claude-code"
16
+ ],
17
+ "homepage": "https://www.synthesisui.com",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/SynthesisUI/synthesisui-hub.git",
21
+ "directory": "packages/cli"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/SynthesisUI/synthesisui-hub/issues"
25
+ },
26
+ "author": "SynthesisUI",
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "scripts": {
31
+ "build": "tsc -p tsconfig.json",
32
+ "dev": "tsx src/index.ts",
33
+ "prepublishOnly": "npm run build"
34
+ },
35
+ "license": "MIT"
36
+ }