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 +21 -0
- package/README.md +54 -0
- package/dist/claude-md.js +83 -0
- package/dist/commands/add.js +50 -0
- package/dist/commands/list.js +17 -0
- package/dist/commands/login.js +79 -0
- package/dist/config.js +37 -0
- package/dist/guide.js +97 -0
- package/dist/index.js +90 -0
- package/dist/registry.js +39 -0
- package/dist/types.js +6 -0
- package/package.json +36 -0
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
|
+
});
|
package/dist/registry.js
ADDED
|
@@ -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
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
|
+
}
|