sabia 1.1.0 → 1.1.3

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 (2) hide show
  1. package/package.json +1 -1
  2. package/sabia.mjs +111 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sabia",
3
- "version": "1.1.0",
3
+ "version": "1.1.3",
4
4
  "description": "Instala, atualiza e desinstala o Sabiá e os toolkits dele no Kiro, no Claude Code e no opencode, com um inventário em ~/.sabia/.",
5
5
  "type": "module",
6
6
  "bin": {
package/sabia.mjs CHANGED
@@ -15,14 +15,73 @@ const VERSAO = JSON.parse(fs.readFileSync(new URL("./package.json", import.meta.
15
15
  const REPO = "https://github.com/dxaraujo/sabia.git";
16
16
  const INDICE = "https://github.com/dxaraujo/sabia-toolkits.git";
17
17
  const ALVOS = ["kiro", "claude", "opencode"];
18
- const BANNER = `
19
- ▄▄▀
20
- ███████╗ █████╗ ██████╗ ██╗ █████╗
21
- ██╔════╝██╔══██╗██╔══██╗██║██╔══██╗
22
- ███████╗███████║██████╔╝██║███████║
23
- ╚════██║██╔══██║██╔══██╗██║██╔══██║
24
- ███████║██║ ██║██████╔╝██║██║ ██║
25
- ╚══════╝╚═╝ ╚═╝╚═════╝ ╚═╝╚═╝ ╚═╝`;
18
+ const NOME = [
19
+ " ▄▄▀",
20
+ "███████╗ █████╗ ██████╗ ██╗ █████╗",
21
+ "██╔════╝██╔══██╗██╔══██╗██║██╔══██╗",
22
+ "███████╗███████║██████╔╝██║███████║",
23
+ "╚════██║██╔══██║██╔══██╗██║██╔══██║",
24
+ "███████║██║ ██║██████╔╝██║██║ ██║",
25
+ "╚══════╝╚═╝ ╚═╝╚═════╝ ╚═╝╚═╝ ╚═╝",
26
+ ];
27
+ // o sabiá do sabia-site/logo.svg: as sete facetas (uma por etapa) e o bico, cinza para aparecer no fundo escuro
28
+ const SABIA = [
29
+ ["#00BBCE", [56, 57, 70, 42, 90, 38, 104, 46, 106, 62, 84, 74, 57, 70]],
30
+ ["#B2D235", [84, 74, 106, 62, 118, 74, 116, 92, 92, 94]],
31
+ ["#0786E3", [57, 70, 84, 74, 92, 94, 86, 122, 70, 112, 60, 92]],
32
+ ["#2C5168", [92, 94, 116, 92, 134, 108, 112, 124, 86, 122]],
33
+ ["#76B043", [86, 122, 112, 124, 132, 152, 118, 170, 94, 146]],
34
+ ["#FAA61A", [134, 108, 150, 128, 150, 154, 132, 152, 112, 124]],
35
+ ["#0094AD", [132, 152, 150, 154, 180, 196, 164, 198]],
36
+ ["#8A8F8C", [36, 64, 56, 57, 57, 70]],
37
+ ];
38
+
39
+ function noSabia(x, y, p) {
40
+ let d = false;
41
+ for (let i = 0, j = p.length - 2; i < p.length; j = i, i += 2)
42
+ if (p[i + 1] > y !== p[j + 1] > y && x < ((p[j] - p[i]) * (y - p[i + 1])) / (p[j + 1] - p[i + 1]) + p[i]) d = !d;
43
+ return d;
44
+ }
45
+
46
+ /** O sabiá em meio-blocos coloridos: cada caractere são dois pixels de `passo` unidades do SVG. */
47
+ function desenharSabia(cor, passo = 13) {
48
+ // pixel: a cor que mais aparece em 4x4 amostras; vazio se menos da metade cai no sabiá
49
+ const pixel = (cx, cy) => {
50
+ if (Math.hypot(cx - 82, cy - 54) < passo * 0.55) return "#0E1210"; // o olho
51
+ const n = {};
52
+ for (let i = 0; i < 16; i++) {
53
+ const x = cx + ((i % 4) + 0.5 - 2) * (passo / 4), y = cy + (Math.floor(i / 4) + 0.5 - 2) * (passo / 4);
54
+ const c = SABIA.find(([, p]) => noSabia(x, y, p))?.[0];
55
+ if (c) n[c] = (n[c] || 0) + 1;
56
+ }
57
+ const total = Object.values(n).reduce((a, b) => a + b, 0);
58
+ return total >= 8 ? Object.entries(n).sort((a, b) => b[1] - a[1])[0][0] : null;
59
+ };
60
+ const linhas = [];
61
+ for (let y = 36; y < 200; y += 2 * passo) {
62
+ let l = "";
63
+ for (let x = 34; x < 182; x += passo) {
64
+ const a = pixel(x + passo / 2, y + passo / 2), b = pixel(x + passo / 2, y + 1.5 * passo);
65
+ l += !a && !b ? " " : !b ? cor(a) + "▀\x1b[0m" : !a ? cor(b) + "▄\x1b[0m" : cor(a) + cor(b, true) + "▀\x1b[0m";
66
+ }
67
+ linhas.push(l);
68
+ }
69
+ while (!linhas.at(-1).trim()) linhas.pop();
70
+ return linhas;
71
+ }
72
+
73
+ /** Sem terminal ou com NO_COLOR, só o nome; senão o sabiá ao lado, em 24 bits ou nos 256 tons. */
74
+ export function banner({ cores = process.stdout.isTTY && !process.env.NO_COLOR, bits24 = /truecolor|24bit/.test(process.env.COLORTERM ?? "") } = {}) {
75
+ if (!cores) return "\n" + NOME.join("\n");
76
+ const cor = (h, fundo) => {
77
+ const [r, g, b] = [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16));
78
+ return `\x1b[${fundo ? 48 : 38};` + (bits24 ? `2;${r};${g};${b}m` : `5;${16 + 36 * Math.round(r / 51) + 6 * Math.round(g / 51) + Math.round(b / 51)}m`);
79
+ };
80
+ const desenho = desenharSabia(cor), largura = [...desenho[0]].filter((c) => " ▀▄".includes(c)).length;
81
+ const texto = [...NOME.map((l) => `\x1b[1m${l}\x1b[0m`), "", `\x1b[2mengenharia agêntica, da ideia ao resultado aceito v${VERSAO}\x1b[0m`];
82
+ return "\n" + texto.map((t, i) => (desenho[i] ?? " ".repeat(largura)) + " " + t).join("\n");
83
+ }
84
+ const BANNER = banner();
26
85
 
27
86
  // trocáveis nos testes
28
87
  export const ctx = {
@@ -301,7 +360,8 @@ export async function instalar(opcoes = {}) {
301
360
  } finally {
302
361
  gravarInventario(inv);
303
362
  }
304
- ctx.log('Pronto. No projeto, peça "configurar o Sabiá" (ou "atualizar os agentes do Sabiá").');
363
+ ctx.log('Pronto: skills e MCPs instalados. Os agentes são de cada projeto: abra a ferramenta na raiz dele, na sessão\n' +
364
+ 'padrão, e peça "configurar o Sabiá" (ou, num projeto já configurado, "atualizar os agentes do Sabiá").');
305
365
  }
306
366
 
307
367
  export async function atualizar(opcoes = {}) {
@@ -367,10 +427,15 @@ export function validarToolkit(pasta, nomeEsperado = null) {
367
427
  let m;
368
428
  try { m = JSON.parse(fs.readFileSync(f, "utf8")); } catch (e) { return [`toolkit.json inválido: ${e.message}`]; }
369
429
  const erros = [];
370
- for (const c of ["nome", "versao", "descricao", "tipos-de-projeto", "skills"]) if (!(c in m)) erros.push(`falta o campo ${c}`);
430
+ for (const c of ["nome", "versao", "descricao", "autor", "tipos-de-projeto", "skills"]) if (!(c in m)) erros.push(`falta o campo ${c}`);
431
+ // quem fez responde pelo toolkit: nome e área aparecem no listar e na setup
432
+ if (m.autor && (typeof m.autor.nome !== "string" || typeof m.autor.area !== "string" || !m.autor.nome.trim() || !m.autor.area.trim()))
433
+ erros.push("autor: precisa de nome e area");
371
434
  const nomeOk = (n) => typeof n === "string" && /^[a-z0-9][a-z0-9-]*$/.test(n) && !n.startsWith("sa-");
372
435
  if (!nomeOk(m.nome)) erros.push(`nome inválido (kebab-case, sem sa-): ${m.nome}`);
373
436
  if (nomeEsperado && m.nome !== nomeEsperado) erros.push(`nome ${m.nome} ≠ ${nomeEsperado} do índice`);
437
+ if (m.customiza && (!nomeOk(m.customiza.toolkit) || m.customiza.toolkit === m.nome || typeof m.customiza.versao !== "string"))
438
+ erros.push("customiza: precisa de toolkit (outro nome, kebab-case) e versao da base");
374
439
  const existe = (c, de) => {
375
440
  try { if (!fs.existsSync(path.join(pasta, dentro(c, de)))) erros.push(`${de}: não existe ${c}`); } catch (e) { erros.push(e.message); }
376
441
  };
@@ -401,7 +466,7 @@ export function lerIndice(fonte) {
401
466
  const m = JSON.parse(fs.readFileSync(path.join(raiz, ".claude-plugin", "marketplace.json"), "utf8"));
402
467
  return (m.plugins || []).map((p) => {
403
468
  const s = p.source;
404
- const base = { nome: p.name, versao: p.version, descricao: p.description, indice: fonte };
469
+ const base = { nome: p.name, versao: p.version, descricao: p.description, customiza: p.customiza, autor: p.autor, indice: fonte };
405
470
  if (typeof s === "string") return { ...base, url: local ? path.resolve(raiz, dentro(s.replace(/^\.\//, ""), fonte)) : fonte, subpasta: local ? null : s.replace(/^\.\//, "") };
406
471
  if (s?.source === "github") return { ...base, url: `https://github.com/${s.repo}.git`, ref: s.ref, sha: s.sha };
407
472
  if (s?.source === "url") return { ...base, url: s.url, ref: s.ref, sha: s.sha };
@@ -453,6 +518,11 @@ export async function instalarToolkit(alvo, opcoes = {}) {
453
518
  const pasta = sabia("toolkits", baixado);
454
519
  const m = JSON.parse(fs.readFileSync(path.join(pasta, "toolkit.json"), "utf8"));
455
520
  const inv = lerInventario();
521
+ // a customização é uma cópia modificada da base: as duas juntas no projeto dão skills e agentes repetidos
522
+ const familia = m.customiza?.toolkit ?? baixado;
523
+ const rival = Object.keys(inv.toolkits).find((n) => n !== baixado && (baseDe(n) ?? n) === familia);
524
+ // mesmos nomes de skill e de agente: as duas no usuário se sobrescreveriam
525
+ if (rival) throw new Error(`${baixado} e ${rival} são versões do mesmo toolkit (base ${familia}); instale um só: sabia toolkits remover ${rival}`);
456
526
  const tk = (inv.toolkits[baixado] ||= { versao: m.versao, alvos: {} });
457
527
  const alvos = opcoes.alvo ? opcoes.alvo.split(",") : Object.keys(inv.alvos).length ? Object.keys(inv.alvos) : await escolherAlvos(opcoes);
458
528
  try {
@@ -504,20 +574,46 @@ export async function atualizarToolkits(nome, opcoes = {}) {
504
574
  }
505
575
  }
506
576
 
577
+ /** A base que o toolkit baixado customiza (do toolkit.json em ~/.sabia/toolkits), ou null. */
578
+ function baseDe(nome) {
579
+ const f = sabia("toolkits", nome, "toolkit.json");
580
+ return fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, "utf8")).customiza?.toolkit ?? null : null;
581
+ }
582
+
507
583
  export function listarToolkits(opcoes = {}) {
508
584
  const inv = lerInventario();
509
585
  const projeto = path.join(process.cwd(), ".sabia", "config", "agentes.json");
510
586
  const doProjeto = fs.existsSync(projeto) ? JSON.parse(fs.readFileSync(projeto, "utf8")).toolkits || {} : {};
511
- const linhas = [];
587
+ // autor e customiza: do toolkit baixado (o toolkit.json) ou, sem ele, da entrada do índice
588
+ const manifesto = (n) => { const f = sabia("toolkits", n, "toolkit.json"); return fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, "utf8")) : {}; };
589
+ const linhas = [], doIndice = {};
512
590
  for (const i of opcoes.indice ? [opcoes.indice] : indices()) {
513
591
  try {
514
- for (const e of lerIndice(i)) linhas.push([e.nome, e.versao || "—", inv.toolkits[e.nome]?.versao || "—", doProjeto[e.nome]?.versao || "—", e.descricao || ""]);
592
+ for (const e of lerIndice(i)) {
593
+ doIndice[e.nome] = e;
594
+ linhas.push([e.nome, "", e.versao || "—", inv.toolkits[e.nome]?.versao || "—", doProjeto[e.nome]?.versao || "—", e.descricao || ""]);
595
+ }
515
596
  } catch (err) {
516
597
  ctx.log(`índice ${i}: ${err.message}`);
517
598
  }
518
599
  }
519
- for (const [n, t] of Object.entries(inv.toolkits)) if (!linhas.some((l) => l[0] === n)) linhas.push([n, "—", t.versao, doProjeto[n]?.versao || "—", "(fora dos índices)"]);
520
- ctx.log(["| Toolkit | No índice | Instalado | Neste projeto | Descrição |", "|---|---|---|---|---|",
600
+ for (const [n, t] of Object.entries(inv.toolkits)) if (!linhas.some((l) => l[0] === n)) linhas.push([n, "", "—", t.versao, doProjeto[n]?.versao || "—", "(fora dos índices)"]);
601
+ for (const l of linhas) {
602
+ const a = manifesto(l[0]).autor ?? doIndice[l[0]]?.autor;
603
+ l[1] = a ? `${a.nome} (${a.area})` : "—";
604
+ }
605
+ // a customização logo abaixo da base, com ↳ e de onde veio
606
+ for (const l of [...linhas]) {
607
+ const c = manifesto(l[0]).customiza ?? doIndice[l[0]]?.customiza;
608
+ if (!c) continue;
609
+ linhas.splice(linhas.indexOf(l), 1);
610
+ const i = linhas.findIndex((b) => b[0] === c.toolkit);
611
+ // só a primeira mudança (quem customizou), como resumo; a lista inteira está no toolkit.json
612
+ l[5] = `customiza ${c.toolkit} ${c.versao}${c.mudancas?.length ? `: ${c.mudancas[0]}${c.mudancas.length > 1 ? "; …" : ""}` : ""}${l[5] ? " — " + l[5] : ""}`;
613
+ l[0] = `↳ ${l[0]}`;
614
+ linhas.splice(i < 0 ? linhas.length : i + 1, 0, l);
615
+ }
616
+ ctx.log(["| Toolkit | Autor | No índice | Instalado | Neste projeto | Descrição |", "|---|---|---|---|---|---|",
521
617
  ...linhas.map((l) => `| ${l.join(" | ")} |`)].join("\n"));
522
618
  return linhas;
523
619
  }