sabia 1.1.1 → 2.0.0-alpha.1
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/compilar.mjs +373 -0
- package/package.json +4 -4
- package/sabia.mjs +244 -47
package/compilar.mjs
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
// Compilador as-built do Sabiá: fluxo/ + padrao/ + skills/ (a fonte, no framework) → skills comuns (SKILL.md +
|
|
2
|
+
// references/) numa pasta de staging, uma por alvo. Seção 11 do plano da 2.0.
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const FM = /^---\n([\s\S]*?)\n---\n/;
|
|
7
|
+
const ID = /`(sa-[a-z]+(?:\.[a-z][a-z-]*){1,2})`/g;
|
|
8
|
+
const LINK = /\]\(([^)\s]+)\)/g;
|
|
9
|
+
|
|
10
|
+
const ler = (f) => fs.readFileSync(f, "utf8");
|
|
11
|
+
const json = (f) => JSON.parse(ler(f));
|
|
12
|
+
const skillsDe = (fw) => fs.readdirSync(path.join(fw, "skills")).filter((d) => d === "sabia" || d.startsWith("sa-")).sort();
|
|
13
|
+
const mds = (dir) => (fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => f.endsWith(".md")).sort() : []);
|
|
14
|
+
|
|
15
|
+
/** Front-matter JSON (fluxo/, padrao/) e o corpo, com as linhas do front-matter preservadas para o número de linha. */
|
|
16
|
+
export function partes(texto) {
|
|
17
|
+
const m = texto.match(FM);
|
|
18
|
+
return m ? { dados: JSON.parse(m[1]), corpo: texto.slice(m[0].length), pulo: m[0].split("\n").length - 1 } : { dados: null, corpo: texto, pulo: 0 };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Slug de âncora no estilo do GitHub: minúsculas, letras e números Unicode mantidos, espaço vira "-". */
|
|
22
|
+
export const slug = (titulo) => titulo.trim().toLowerCase().replace(/`/g, "").replace(/[^\p{L}\p{N} _-]/gu, "").replace(/ /g, "-");
|
|
23
|
+
const ancoras = (texto) => new Set([...texto.matchAll(/^#+ (.+)$/gm)].map((m) => slug(m[1])));
|
|
24
|
+
|
|
25
|
+
/** Lê a fonte inteira: fluxos, passos, regras, padrões, travas e papéis. */
|
|
26
|
+
export function carregar(fw) {
|
|
27
|
+
const m = { fw, fluxos: {}, regras: {}, padroes: {}, travas: {}, subag: {}, skillsAgente: {} };
|
|
28
|
+
const dirFluxo = path.join(fw, "fluxo");
|
|
29
|
+
if (fs.existsSync(path.join(dirFluxo, "travas.json"))) m.travas = json(path.join(dirFluxo, "travas.json")).travas;
|
|
30
|
+
const papeis = path.join(fw, "skills/sa-setup/assets/agents/papeis.json");
|
|
31
|
+
if (fs.existsSync(papeis))
|
|
32
|
+
for (const [k, v] of Object.entries(json(papeis).papeis)) {
|
|
33
|
+
m.subag[`sa-${k}`] = new Set(v.subagentes || []);
|
|
34
|
+
m.skillsAgente[`sa-${k}`] = new Set(v.skills || []);
|
|
35
|
+
}
|
|
36
|
+
for (const s of fs.existsSync(dirFluxo) ? fs.readdirSync(dirFluxo).sort() : []) {
|
|
37
|
+
const ds = path.join(dirFluxo, s);
|
|
38
|
+
if (!fs.statSync(ds).isDirectory()) continue;
|
|
39
|
+
if (fs.existsSync(path.join(ds, "regras.md"))) m.regras[s] = { arq: path.join(ds, "regras.md"), ...partes(ler(path.join(ds, "regras.md"))) };
|
|
40
|
+
for (const f of fs.readdirSync(ds).sort()) {
|
|
41
|
+
const mestre = path.join(ds, f, `${f}.md`);
|
|
42
|
+
if (!fs.existsSync(mestre)) continue;
|
|
43
|
+
const p = partes(ler(mestre));
|
|
44
|
+
const passos = (p.dados?.passos || []).map((x) => {
|
|
45
|
+
const arq = path.join(ds, f, "passos", `${x.id}.md`);
|
|
46
|
+
return { ...x, arq, ...(fs.existsSync(arq) ? partes(ler(arq)) : { dados: null, corpo: "", pulo: 0 }) };
|
|
47
|
+
});
|
|
48
|
+
m.fluxos[`${s}.${f}`] = { skill: s, fluxo: f, arq: mestre, d: p.dados || {}, corpo: p.corpo, pulo: p.pulo, passos };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const dirPadrao = path.join(fw, "padrao");
|
|
52
|
+
for (const s of fs.existsSync(dirPadrao) ? fs.readdirSync(dirPadrao).sort() : [])
|
|
53
|
+
m.padroes[s] = mds(path.join(dirPadrao, s)).map((n) => ({ nome: n, arq: path.join(dirPadrao, s, n), ...partes(ler(path.join(dirPadrao, s, n))) }));
|
|
54
|
+
return m;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const fluxosDa = (m, s) => Object.values(m.fluxos).filter((f) => f.skill === s);
|
|
58
|
+
|
|
59
|
+
/** "sa-x.f" ou "sa-x.f.p" existe? Devolve o fluxo e o passo (ou null). */
|
|
60
|
+
function resolverId(m, q) {
|
|
61
|
+
const [s, f, p] = q.split(".");
|
|
62
|
+
const fl = m.fluxos[`${s}.${f}`];
|
|
63
|
+
if (!fl) return null;
|
|
64
|
+
if (p === undefined) return { fl, passo: null };
|
|
65
|
+
const passo = fl.passos.find((x) => x.id === p);
|
|
66
|
+
return passo ? { fl, passo } : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Como o agente roda um fluxo chamado: "em linha" (mesmo dono ou skill do agente), "subagente", ou null (não pode). */
|
|
70
|
+
function modoChamada(m, dono, alvo) {
|
|
71
|
+
if (alvo.d.dono === dono || m.skillsAgente[dono]?.has(alvo.skill)) return "em linha";
|
|
72
|
+
if (m.subag[dono]?.has(alvo.d.dono)) return "subagente";
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------- validação
|
|
77
|
+
|
|
78
|
+
/** Erros "arquivo:linha: mensagem" da fonte (as regras do conferidor da F2.0). Lista vazia = pronta para compilar. */
|
|
79
|
+
export function validar(m) {
|
|
80
|
+
const erros = [];
|
|
81
|
+
const rel = (f) => path.relative(m.fw, f);
|
|
82
|
+
const e = (f, n, msg) => erros.push(`${rel(f)}:${n}: ${msg}`);
|
|
83
|
+
const porSkill = {};
|
|
84
|
+
for (const fl of Object.values(m.fluxos)) (porSkill[fl.skill] ||= []).push(fl);
|
|
85
|
+
|
|
86
|
+
for (const [q, fl] of Object.entries(m.fluxos)) {
|
|
87
|
+
const { d } = fl;
|
|
88
|
+
if (!d.passos?.length || d.fluxo !== fl.fluxo || !d.dono) e(fl.arq, 2, "mestre sem fluxo/dono/passos (ou fluxo ≠ nome da pasta)");
|
|
89
|
+
if (porSkill[fl.skill].length > 1 && !(Array.isArray(d.quando) ? d.quando.length : String(d.quando || "").trim()))
|
|
90
|
+
e(fl.arq, 2, `falta "quando" (a skill ${fl.skill} tem ${porSkill[fl.skill].length} fluxos)`);
|
|
91
|
+
const ids = fl.passos.map((x) => x.id);
|
|
92
|
+
const orfaos = fs.existsSync(path.join(path.dirname(fl.arq), "passos"))
|
|
93
|
+
? mds(path.join(path.dirname(fl.arq), "passos")).map((f) => f.slice(0, -3)).filter((x) => !ids.includes(x)) : [];
|
|
94
|
+
for (const o of orfaos) e(fl.arq, 2, `passo ${o} órfão (tem arquivo, não está no mestre)`);
|
|
95
|
+
for (const t of d.travas_gerais || []) if (!m.travas[t]) e(fl.arq, 2, `trava ${t} fora do catálogo`);
|
|
96
|
+
for (const x of fl.passos) {
|
|
97
|
+
if (!x.dados) { e(x.arq, 1, `passo ${x.id} sem arquivo ou sem front-matter`); continue; }
|
|
98
|
+
if (!x.dados.resumo) e(x.arq, 2, "passo sem resumo");
|
|
99
|
+
for (const sec of ["## Entrada", "## Faça", "## Saída", "## Pare se"]) if (!x.corpo.includes(sec)) e(x.arq, 1, `falta ${sec}`);
|
|
100
|
+
for (const t of x.travas || []) if (!m.travas[t]) e(fl.arq, 2, `passo ${x.id}: trava ${t} fora do catálogo`);
|
|
101
|
+
for (const c of [].concat(x.chama || [])) {
|
|
102
|
+
const r = resolverId(m, c);
|
|
103
|
+
if (!r) e(fl.arq, 2, `passo ${x.id}: chama ${c} inexistente`);
|
|
104
|
+
else if (!modoChamada(m, d.dono, r.fl)) e(fl.arq, 2, `passo ${x.id}: chama ${c} (dono ${r.fl.d.dono}), mas ${d.dono} não tem a skill nem esse subagente → use passagem (→)`);
|
|
105
|
+
}
|
|
106
|
+
for (const [cond, dest] of Object.entries(x.saidas || {})) {
|
|
107
|
+
if (dest === "fim" || ids.includes(dest) || (dest.startsWith("→ ") && resolverId(m, dest.slice(2).trim()))) continue;
|
|
108
|
+
e(fl.arq, 2, `passo ${x.id}: saída "${cond}" → "${dest}" (use id do fluxo, fim ou "→ skill.fluxo[.passo]" existente)`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const textos = [];
|
|
114
|
+
for (const fl of Object.values(m.fluxos)) {
|
|
115
|
+
textos.push({ arq: fl.arq, corpo: fl.corpo, pulo: fl.pulo, skill: fl.skill, fl });
|
|
116
|
+
for (const x of fl.passos) textos.push({ arq: x.arq, corpo: x.corpo, pulo: x.pulo, skill: fl.skill, fl });
|
|
117
|
+
}
|
|
118
|
+
for (const [s, r] of Object.entries(m.regras)) textos.push({ arq: r.arq, corpo: r.corpo, pulo: r.pulo, skill: s });
|
|
119
|
+
for (const [s, ps] of Object.entries(m.padroes)) for (const p of ps) textos.push({ arq: p.arq, corpo: p.corpo, pulo: p.pulo, skill: s });
|
|
120
|
+
for (const s of skillsDe(m.fw)) {
|
|
121
|
+
const dir = path.join(m.fw, "skills", s);
|
|
122
|
+
for (const f of [path.join(dir, "SKILL.md"), ...mds(path.join(dir, "references")).map((n) => path.join(dir, "references", n))]) {
|
|
123
|
+
if (!fs.existsSync(f)) continue;
|
|
124
|
+
const t = ler(f);
|
|
125
|
+
const fmYaml = t.match(FM);
|
|
126
|
+
const corpo = fmYaml ? t.slice(fmYaml[0].length) : t;
|
|
127
|
+
const pulo = fmYaml ? fmYaml[0].split("\n").length - 1 : 0;
|
|
128
|
+
if (path.basename(f) === "SKILL.md" && porSkill[s]) {
|
|
129
|
+
const resto = corpo.split("\n").filter((l) => l.trim() && !l.startsWith("# "));
|
|
130
|
+
if (resto.length) e(f, pulo + 1, `skill de fluxo com corpo além do H1 (${resto.length} linhas): vai para "quando" ou regras.md`);
|
|
131
|
+
}
|
|
132
|
+
textos.push({ arq: f, corpo, pulo, skill: s });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const passoRx = /\bpassos? ((?:`[a-z][a-z-]*`(?:, | e | ou )?)+)/g;
|
|
137
|
+
const fluxoRx = /\bfluxos? ((?:`[a-z][a-z-]*`(?:, | e | ou )?)+)/g;
|
|
138
|
+
const legado = [/`?\bsa-[a-z]+`?\*{0,2},? \(?(?:fluxo|passo)s? `/, /\b(?:fluxo|passo)s? `[a-z-]+`(?: \([^)]*\))? d[ao] \*{0,2}`?sa-[a-z]+/];
|
|
139
|
+
for (const t of textos) {
|
|
140
|
+
let cerca = false;
|
|
141
|
+
t.corpo.split("\n").forEach((linha, i) => {
|
|
142
|
+
const n = t.pulo + i + 1;
|
|
143
|
+
if (linha.trimStart().startsWith("```")) { cerca = !cerca; return; }
|
|
144
|
+
if (/~\/\.sabia\/(fluxo|padrao)\//.test(linha)) e(t.arq, n, "prefixo ~/.sabia/fluxo|padrao/ → ~/.sabia/framework/fluxo|padrao/");
|
|
145
|
+
if (cerca) return;
|
|
146
|
+
for (const [, q] of linha.matchAll(ID)) if (!/\.(md|json|sh|py)$/.test(q) && !resolverId(m, q)) e(t.arq, n, `id inexistente \`${q}\``);
|
|
147
|
+
const semId = linha.replace(ID, "ID");
|
|
148
|
+
for (const rx of legado) if (rx.test(semId)) e(t.arq, n, `referência em prosa → use o id: …${semId.match(rx)[0]}…`);
|
|
149
|
+
for (const [, grupo] of semId.matchAll(passoRx))
|
|
150
|
+
for (const [, x] of grupo.matchAll(/`([a-z][a-z-]*)`/g)) {
|
|
151
|
+
if (t.fl) { if (!t.fl.passos.some((p) => p.id === x)) e(t.arq, n, `passo \`${x}\` não é do fluxo ${t.skill}.${t.fl.fluxo} → use o id`); }
|
|
152
|
+
else if (porSkill[t.skill]) { if (porSkill[t.skill].filter((f) => f.passos.some((p) => p.id === x)).length !== 1) e(t.arq, n, `passo \`${x}\` sem fluxo → use o id`); }
|
|
153
|
+
else e(t.arq, n, `passo \`${x}\` fora de fluxo → use o id`);
|
|
154
|
+
}
|
|
155
|
+
for (const [, grupo] of semId.matchAll(fluxoRx))
|
|
156
|
+
for (const [, x] of grupo.matchAll(/`([a-z][a-z-]*)`/g)) if (!m.fluxos[`${t.skill}.${x}`]) e(t.arq, n, `fluxo \`${x}\` não é da skill ${t.skill} → use o id`);
|
|
157
|
+
for (const mi of semId.matchAll(/\bitens? \d+/g))
|
|
158
|
+
if (/^[^.;]*?(\bd[ao] `?sa-|ID|\bd[ao] passo `)/.test(semId.slice(mi.index + mi[0].length, mi.index + mi[0].length + 70)))
|
|
159
|
+
e(t.arq, n, `"${mi[0]}" de outro arquivo → use id ou âncora`);
|
|
160
|
+
for (const f of ["em `fluxo/`", "abra-os pelo caminho", "abra pelo caminho", "ficam fora do projeto"]) if (linha.includes(f)) e(t.arq, n, `frase de localização "${f}"`);
|
|
161
|
+
if (/`sa-[a-z]+\/[a-z-]+\/passos\/|`sa-[a-z]+\/regras\.md`/.test(linha)) e(t.arq, n, "caminho de fluxo/ em code-span → id do passo");
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return erros;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------- reescrita
|
|
168
|
+
|
|
169
|
+
/** Caminho na fonte (absoluto) → caminho relativo à pasta skills/ da saída, ou null se não for do framework. */
|
|
170
|
+
export function mapear(fw, src) {
|
|
171
|
+
const r = path.relative(fw, src).split(path.sep);
|
|
172
|
+
if (r[0] === "skills" && r.length >= 2) return r.slice(1).join("/");
|
|
173
|
+
if (r[0] === "padrao" && r.length === 3) return `${r[1]}/references/padrao/${r[2]}`;
|
|
174
|
+
if (r[0] === "fluxo" && r.length === 3 && r[2] === "regras.md") return `${r[1]}/SKILL.md`;
|
|
175
|
+
if (r[0] === "fluxo" && r.length === 4 && r[3] === `${r[2]}.md`) return `${r[1]}/SKILL.md`;
|
|
176
|
+
if (r[0] === "fluxo" && r.length === 5 && r[3] === "passos") return `${r[1]}/references/${r[2]}/${r[4]}`;
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Reescreve links e caminhos do framework para o caminho absoluto do alvo (`base`) e liga ids e padrões. */
|
|
181
|
+
export function reescrever(texto, origem, { fw, base, m }) {
|
|
182
|
+
const destino = (alvo) => {
|
|
183
|
+
const [cam, anc = ""] = alvo.split("#");
|
|
184
|
+
let src;
|
|
185
|
+
if (cam.startsWith("~/.sabia/framework/")) src = path.join(fw, cam.slice("~/.sabia/framework/".length));
|
|
186
|
+
else if (!cam) src = origem;
|
|
187
|
+
else if (cam.startsWith("../") || cam.startsWith("references/") || cam.endsWith("SKILL.md")) src = path.resolve(path.dirname(origem), cam);
|
|
188
|
+
else return null;
|
|
189
|
+
const r = mapear(fw, src);
|
|
190
|
+
return r ? `${base}/${r}${anc ? "#" + anc : ""}` : null;
|
|
191
|
+
};
|
|
192
|
+
let cerca = false;
|
|
193
|
+
return texto.split("\n").map((linha) => {
|
|
194
|
+
if (linha.trimStart().startsWith("```")) { cerca = !cerca; return linha; }
|
|
195
|
+
linha = linha.replace(LINK, (todo, alvo) => {
|
|
196
|
+
if (/^(https?:|mailto:)/.test(alvo) || alvo.includes("<") || alvo.includes("…")) return todo;
|
|
197
|
+
const d = destino(alvo);
|
|
198
|
+
return d ? `](${d})` : todo;
|
|
199
|
+
});
|
|
200
|
+
linha = linha.replace(/~\/\.sabia\/framework\/[^\s)`'"]+/g, (cam) => destino(cam) || cam);
|
|
201
|
+
if (cerca || !m) return linha;
|
|
202
|
+
linha = linha.replace(/(\[)?`(sa-[a-z]+(?:\.[a-z][a-z-]*){1,2})`(\]\()?/g, (todo, abre, q, fecha) => {
|
|
203
|
+
if (abre || fecha || /\.(md|json|sh|py)$/.test(q)) return todo;
|
|
204
|
+
const r = resolverId(m, q);
|
|
205
|
+
if (!r) return todo;
|
|
206
|
+
const alvo = destinoId(base, r);
|
|
207
|
+
return `[\`${q}\`](${alvo})`;
|
|
208
|
+
});
|
|
209
|
+
return linha.replace(/o padrão `([a-z-]+)\/([a-z0-9.-]+\.md)`(?!\])/g, (todo, s, n) =>
|
|
210
|
+
m.padroes[s]?.some((p) => p.nome === n) ? `o padrão [\`${s}/${n}\`](${base}/${s}/references/padrao/${n})` : todo);
|
|
211
|
+
}).join("\n");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Desce os títulos `n` níveis (fora das cercas de código). */
|
|
215
|
+
function rebaixar(texto, n) {
|
|
216
|
+
let cerca = false;
|
|
217
|
+
return texto.split("\n").map((l) => {
|
|
218
|
+
if (l.trimStart().startsWith("```")) cerca = !cerca;
|
|
219
|
+
return !cerca && /^#{1,5} /.test(l) ? "#".repeat(n) + l : l;
|
|
220
|
+
}).join("\n");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Tira o H1 do corpo; devolve o título e o resto. */
|
|
224
|
+
function semH1(corpo) {
|
|
225
|
+
const m = corpo.match(/^\s*# (.+)\n/);
|
|
226
|
+
return m ? { titulo: m[1].trim(), resto: corpo.slice(m[0].length).replace(/^\n+/, "") } : { titulo: "", resto: corpo.replace(/^\n+/, "") };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Link de um id: o arquivo do passo, ou a seção do fluxo no SKILL.md. */
|
|
230
|
+
const destinoId = (base, r) => (r.passo ? `${base}/${r.fl.skill}/references/${r.fl.fluxo}/${r.passo.id}.md` : `${base}/${r.fl.skill}/SKILL.md#${slug(`Fluxo ${r.fl.fluxo}`)}`);
|
|
231
|
+
|
|
232
|
+
const cel = (t) => String(t).replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
233
|
+
const italico = (resumo) => (resumo ? `_${resumo}_\n\n` : "");
|
|
234
|
+
|
|
235
|
+
const NO_PROJETO = `## No projeto
|
|
236
|
+
|
|
237
|
+
Antes de ler o arquivo de um passo (\`…/skills/<skill>/references/<fluxo>/<passo>.md\`, de qualquer skill), tente abrir
|
|
238
|
+
direto \`.sabia/fluxo/<skill>/<fluxo>/passos/<passo>.md\` na raiz do projeto: se existir, vale ele; senão, o da skill.
|
|
239
|
+
O mesmo para um padrão: \`.sabia/padrao/<skill>/<nome>.md\` no lugar de \`…/references/padrao/<nome>.md\`. Abra pelo
|
|
240
|
+
caminho; não liste a pasta (a busca não vê pastas ocultas). Com agente do Sabiá, o prompt lista também os passos inseridos
|
|
241
|
+
e removidos nos fluxos de que ele é dono.
|
|
242
|
+
`;
|
|
243
|
+
|
|
244
|
+
/** O SKILL.md compilado de uma skill de fluxo. */
|
|
245
|
+
function skillMd(m, s, fmYaml, h1, ctx) {
|
|
246
|
+
const { base } = ctx;
|
|
247
|
+
const fls = fluxosDa(m, s);
|
|
248
|
+
const out = [fmYaml.trimEnd(), "", h1, ""];
|
|
249
|
+
if (m.regras[s]) out.push("## Regras", "", rebaixar(reescrever(semH1(m.regras[s].corpo).resto, m.regras[s].arq, ctx), 1).trimEnd(), "");
|
|
250
|
+
if (fls.length > 1) {
|
|
251
|
+
out.push("## Fluxos", "", "| Fluxo | Quando |", "|---|---|");
|
|
252
|
+
for (const fl of fls) out.push(`| [${fl.fluxo}](#${slug(`Fluxo ${fl.fluxo}`)}) | ${cel([].concat(fl.d.quando || []).join("; "))} |`);
|
|
253
|
+
out.push("");
|
|
254
|
+
}
|
|
255
|
+
const usadas = new Set();
|
|
256
|
+
for (const fl of fls) {
|
|
257
|
+
const { titulo, resto } = semH1(fl.corpo);
|
|
258
|
+
out.push(`## Fluxo ${fl.fluxo}`, "");
|
|
259
|
+
if (titulo) out.push(`**${titulo}**`, "");
|
|
260
|
+
out.push(`Agente: \`${fl.d.dono}\`. Sem ele, sugira à pessoa usá-lo; seguindo aqui, os passos inseridos pelo projeto e os`,
|
|
261
|
+
"desvios de toolkit (⇢) não entram.", "");
|
|
262
|
+
if (resto.trim()) out.push(rebaixar(reescrever(resto, fl.arq, ctx), 1).trimEnd(), "");
|
|
263
|
+
out.push("| # | Passo | Arquivo | Chama | Travas | Saídas |", "|---|---|---|---|---|---|");
|
|
264
|
+
(fl.d.travas_gerais || []).forEach((t) => usadas.add(t));
|
|
265
|
+
fl.passos.forEach((x, i) => {
|
|
266
|
+
(x.travas || []).forEach((t) => usadas.add(t));
|
|
267
|
+
const arq = `${base}/${s}/references/${fl.fluxo}/${x.id}.md`;
|
|
268
|
+
const chama = [].concat(x.chama || []).map((c) => {
|
|
269
|
+
const r = resolverId(m, c);
|
|
270
|
+
const modo = modoChamada(m, fl.d.dono, r.fl);
|
|
271
|
+
const alvo = destinoId(base, r);
|
|
272
|
+
return `[\`${c}\`](${alvo}) (${modo === "subagente" ? `${r.fl.d.dono}, subagente` : "em linha"})`;
|
|
273
|
+
}).join("<br>") || "—";
|
|
274
|
+
const saidas = Object.entries(x.saidas || {}).map(([cond, dest]) => {
|
|
275
|
+
if (dest === "fim") return `${cond} → fim`;
|
|
276
|
+
if (!dest.startsWith("→ ")) return `${cond} → \`${dest}\``;
|
|
277
|
+
const q = dest.slice(2).trim();
|
|
278
|
+
const r = resolverId(m, q);
|
|
279
|
+
const alvo = destinoId(base, r);
|
|
280
|
+
return `${cond} → passa a [\`${q}\`](${alvo})`;
|
|
281
|
+
}).join("<br>") || "—";
|
|
282
|
+
out.push(`| ${i + 1} | \`${x.id}\`${x.desvio ? " ⇢" : ""} | [${x.id}.md](${arq}) | ${cel(chama)} | ${cel((x.travas || []).join(", ") || "—")} | ${cel(saidas)} |`);
|
|
283
|
+
});
|
|
284
|
+
out.push("", "Ao começar um passo, leia o arquivo dele (não os dos outros passos) e o que ele mandar ler. Sem Saídas, segue para o",
|
|
285
|
+
`próximo da lista. Travas do fluxo: ${(fl.d.travas_gerais || []).map((t) => `\`${t}\``).join(", ") || "—"}.`, "");
|
|
286
|
+
}
|
|
287
|
+
const ordem = Object.keys(m.travas).filter((t) => usadas.has(t));
|
|
288
|
+
if (ordem.length) out.push("## Travas", "", "Valem sobre o corpo de qualquer passo.", "", ...ordem.map((t) => `- **${t}** — ${m.travas[t].texto}`), "");
|
|
289
|
+
const padroes = m.padroes[s] || [];
|
|
290
|
+
const refs = mds(path.join(m.fw, "skills", s, "references"));
|
|
291
|
+
if (padroes.length || refs.length) {
|
|
292
|
+
out.push("## Arquivos", "");
|
|
293
|
+
for (const p of padroes) out.push(`- padrão [\`${s}/${p.nome}\`](${base}/${s}/references/padrao/${p.nome})${p.dados?.resumo ? ` — ${p.dados.resumo}` : ""}`);
|
|
294
|
+
for (const r of refs) out.push(`- [\`references/${r}\`](${base}/${s}/references/${r})`);
|
|
295
|
+
out.push("");
|
|
296
|
+
}
|
|
297
|
+
out.push(NO_PROJETO);
|
|
298
|
+
return out.join("\n");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const comVersao = (fmYaml, versao) => (versao ? fmYaml.replace(/^(\s+version:\s*)"[^"]*"/m, `$1"${versao}"`) : fmYaml);
|
|
302
|
+
|
|
303
|
+
function copiarArvore(origem, destino, transformar) {
|
|
304
|
+
fs.mkdirSync(destino, { recursive: true });
|
|
305
|
+
for (const n of fs.readdirSync(origem)) {
|
|
306
|
+
if (n === "__pycache__" || n === ".DS_Store") continue;
|
|
307
|
+
const o = path.join(origem, n), d = path.join(destino, n);
|
|
308
|
+
if (fs.statSync(o).isDirectory()) copiarArvore(o, d, transformar);
|
|
309
|
+
else if (n.endsWith(".md")) fs.writeFileSync(d, transformar(ler(o), o));
|
|
310
|
+
else fs.copyFileSync(o, d);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Confere a saída: todo link para `base` resolve num arquivo do staging (e a âncora existe); nada do layout da fonte sobra. */
|
|
315
|
+
function conferirSaida(staging, base) {
|
|
316
|
+
const erros = [];
|
|
317
|
+
const raiz = path.join(staging, "skills");
|
|
318
|
+
const cache = {};
|
|
319
|
+
const andar = (dir) => fs.readdirSync(dir).flatMap((n) => (fs.statSync(path.join(dir, n)).isDirectory() ? andar(path.join(dir, n)) : n.endsWith(".md") ? [path.join(dir, n)] : []));
|
|
320
|
+
for (const f of andar(raiz)) {
|
|
321
|
+
const t = ler(f);
|
|
322
|
+
const rel = path.relative(staging, f);
|
|
323
|
+
t.split("\n").forEach((l, i) => {
|
|
324
|
+
if (/~\/\.sabia\/(framework\/(skills|fluxo|padrao)|fluxo|padrao)\//.test(l)) erros.push(`${rel}:${i + 1}: caminho da fonte na saída`);
|
|
325
|
+
for (const [, alvo] of l.matchAll(LINK)) {
|
|
326
|
+
if (!alvo.startsWith(base + "/")) continue;
|
|
327
|
+
const [cam, anc] = alvo.slice(base.length + 1).split("#");
|
|
328
|
+
const arq = path.join(raiz, cam);
|
|
329
|
+
if (!fs.existsSync(arq)) { erros.push(`${rel}:${i + 1}: link para ${cam} inexistente`); continue; }
|
|
330
|
+
if (anc && !(cache[arq] ||= ancoras(ler(arq))).has(anc)) erros.push(`${rel}:${i + 1}: âncora #${anc} não existe em ${cam}`);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
return erros;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Compila o framework `fw` no `staging` (recriado do zero): staging/skills/<s>/… e, com `raiz`, plugin.json, mcp.json,
|
|
339
|
+
* README.md e LICENSE (o power do Kiro). `base` é o caminho absoluto com ~ onde as skills ficam no alvo.
|
|
340
|
+
*/
|
|
341
|
+
export function compilar(fw, staging, { base, raiz = false, versao = null } = {}) {
|
|
342
|
+
const m = carregar(fw);
|
|
343
|
+
const erros = validar(m);
|
|
344
|
+
if (erros.length) throw new Error(`a fonte do Sabiá tem ${erros.length} erro(s):\n${erros.slice(0, 40).join("\n")}`);
|
|
345
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
346
|
+
const ctx = { fw, base, m };
|
|
347
|
+
const comFluxo = new Set(Object.values(m.fluxos).map((f) => f.skill));
|
|
348
|
+
for (const s of skillsDe(fw)) {
|
|
349
|
+
const dir = path.join(fw, "skills", s), out = path.join(staging, "skills", s);
|
|
350
|
+
copiarArvore(dir, out, (t, o) => reescrever(t, o, ctx));
|
|
351
|
+
const fonte = ler(path.join(dir, "SKILL.md"));
|
|
352
|
+
const fmYaml = (fonte.match(FM) || [""])[0];
|
|
353
|
+
if (comFluxo.has(s)) {
|
|
354
|
+
const h1 = (fonte.slice(fmYaml.length).match(/^# .+$/m) || [`# ${s}`])[0];
|
|
355
|
+
fs.writeFileSync(path.join(out, "SKILL.md"), skillMd(m, s, comVersao(fmYaml, versao), h1, ctx));
|
|
356
|
+
for (const fl of fluxosDa(m, s))
|
|
357
|
+
for (const x of fl.passos) {
|
|
358
|
+
fs.mkdirSync(path.join(out, "references", fl.fluxo), { recursive: true });
|
|
359
|
+
fs.writeFileSync(path.join(out, "references", fl.fluxo, `${x.id}.md`), italico(x.dados.resumo) + reescrever(x.corpo, x.arq, ctx).replace(/^\n+/, ""));
|
|
360
|
+
}
|
|
361
|
+
} else if (fmYaml) {
|
|
362
|
+
fs.writeFileSync(path.join(out, "SKILL.md"), comVersao(fmYaml, versao) + reescrever(fonte.slice(fmYaml.length), path.join(dir, "SKILL.md"), ctx));
|
|
363
|
+
}
|
|
364
|
+
for (const p of m.padroes[s] || []) {
|
|
365
|
+
fs.mkdirSync(path.join(out, "references", "padrao"), { recursive: true });
|
|
366
|
+
fs.writeFileSync(path.join(out, "references", "padrao", p.nome), italico(p.dados?.resumo) + reescrever(p.corpo, p.arq, ctx).replace(/^\n+/, ""));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (raiz) for (const f of ["plugin.json", "mcp.json", "README.md", "LICENSE"]) if (fs.existsSync(path.join(fw, f))) fs.copyFileSync(path.join(fw, f), path.join(staging, f));
|
|
370
|
+
const ruins = conferirSaida(staging, base);
|
|
371
|
+
if (ruins.length) throw new Error(`a skill compilada tem ${ruins.length} erro(s):\n${ruins.slice(0, 40).join("\n")}`);
|
|
372
|
+
return staging;
|
|
373
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sabia",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Instala, atualiza e desinstala o Sabiá e os toolkits dele no Kiro
|
|
3
|
+
"version": "2.0.0-alpha.1",
|
|
4
|
+
"description": "Instala, atualiza e desinstala o Sabiá e os toolkits dele no Kiro e no opencode, com um inventário em ~/.sabia/, e explica a rota dos fluxos (sabia rota).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"sabia": "sabia.mjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"sabia.mjs"
|
|
10
|
+
"sabia.mjs",
|
|
11
|
+
"compilar.mjs"
|
|
11
12
|
],
|
|
12
13
|
"engines": {
|
|
13
14
|
"node": ">=18"
|
|
@@ -25,7 +26,6 @@
|
|
|
25
26
|
"keywords": [
|
|
26
27
|
"sabia",
|
|
27
28
|
"kiro",
|
|
28
|
-
"claude-code",
|
|
29
29
|
"opencode",
|
|
30
30
|
"skills",
|
|
31
31
|
"agentes"
|
package/sabia.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Sabiá CLI — instala, atualiza e desinstala o Sabiá e os toolkits no Kiro
|
|
2
|
+
// Sabiá CLI — instala, atualiza e desinstala o Sabiá e os toolkits no Kiro e no opencode, e explica a rota (`sabia rota`).
|
|
3
3
|
// ~/.sabia/ é a fonte (framework/ e toolkits/), cada ferramenta recebe uma cópia no padrão dela, e o
|
|
4
4
|
// ~/.sabia/inventario.json registra cada cópia (com o hash de cada arquivo) e cada MCP: atualizar e desinstalar
|
|
5
5
|
// mexem só no que está lá. Só a biblioteca padrão do Node 18+.
|
|
@@ -10,11 +10,12 @@ import os from "node:os";
|
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import readline from "node:readline/promises";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { carregar, compilar, partes } from "./compilar.mjs";
|
|
13
14
|
|
|
14
15
|
const VERSAO = JSON.parse(fs.readFileSync(new URL("./package.json", import.meta.url), "utf8")).version;
|
|
15
16
|
const REPO = "https://github.com/dxaraujo/sabia.git";
|
|
16
17
|
const INDICE = "https://github.com/dxaraujo/sabia-toolkits.git";
|
|
17
|
-
const ALVOS = ["kiro", "
|
|
18
|
+
const ALVOS = ["kiro", "opencode"];
|
|
18
19
|
const NOME = [
|
|
19
20
|
" ▄▄▀",
|
|
20
21
|
"███████╗ █████╗ ██████╗ ██╗ █████╗",
|
|
@@ -105,7 +106,6 @@ export const curto = (p) => (p.startsWith(home() + path.sep) ? "~/" + path.relat
|
|
|
105
106
|
export const longo = (p) => (p.startsWith("~/") ? path.join(home(), ...p.slice(2).split("/")) : p);
|
|
106
107
|
|
|
107
108
|
export const PASTA_SKILLS = {
|
|
108
|
-
claude: () => path.join(home(), ".claude", "skills"),
|
|
109
109
|
opencode: () => path.join(home(), ".config", "opencode", "skills"),
|
|
110
110
|
};
|
|
111
111
|
|
|
@@ -228,7 +228,7 @@ function temComando(cmd) {
|
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
export function detectarAlvos() {
|
|
231
|
-
return ALVOS.filter((a) => temComando({ kiro: "kiro-cli",
|
|
231
|
+
return ALVOS.filter((a) => temComando({ kiro: "kiro-cli", opencode: "opencode" }[a]));
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
async function escolherAlvos(opcoes) {
|
|
@@ -276,28 +276,29 @@ function tirarMcpDoOpencode(nome) {
|
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
|
|
279
|
+
/** Os MCPs do framework no opencode (no Kiro, vão no mcp.json do power). */
|
|
280
|
+
async function registrarMcps(fw, reg) {
|
|
280
281
|
reg.mcp ||= [];
|
|
281
|
-
const ja =
|
|
282
|
+
const ja = mcpsDoOpencode();
|
|
282
283
|
for (const [nome, s] of Object.entries(servidoresMcp(fw))) {
|
|
284
|
+
if (ja.includes(nome)) { ctx.log(`MCP ${nome} já existe no opencode — mantido`); continue; }
|
|
283
285
|
const env = Object.entries(s.env || {}).map(([k, v]) => `${k}=${v}`);
|
|
284
|
-
|
|
285
|
-
if (
|
|
286
|
-
if (ctx.exec("claude", ["mcp", "get", nome]).status === 0) { ctx.log(`MCP ${nome} já existe no Claude Code — mantido`); continue; }
|
|
287
|
-
// o nome vem antes do -e, que aceita vários valores e engoliria o nome
|
|
288
|
-
r = ctx.exec("claude", ["mcp", "add", "--scope", "user", nome, ...env.flatMap((e) => ["-e", e]), "--", s.command, ...s.args]);
|
|
289
|
-
} else {
|
|
290
|
-
if (ja.includes(nome)) { ctx.log(`MCP ${nome} já existe no opencode — mantido`); continue; }
|
|
291
|
-
r = ctx.exec("opencode", ["mcp", "add", "--global", ...env.flatMap((e) => ["--env", e]), nome, "--", s.command, ...s.args]);
|
|
292
|
-
}
|
|
293
|
-
if (r.status !== 0) throw new Error(`MCP ${nome} no ${alvo}: ${(r.stderr || r.error?.message || "").trim()}`);
|
|
286
|
+
const r = ctx.exec("opencode", ["mcp", "add", "--global", ...env.flatMap((e) => ["--env", e]), nome, "--", s.command, ...s.args]);
|
|
287
|
+
if (r.status !== 0) throw new Error(`MCP ${nome} no opencode: ${(r.stderr || r.error?.message || "").trim()}`);
|
|
294
288
|
if (!reg.mcp.includes(nome)) reg.mcp.push(nome);
|
|
295
|
-
ctx.log(`MCP ${nome} registrado no
|
|
289
|
+
ctx.log(`MCP ${nome} registrado no opencode`);
|
|
296
290
|
}
|
|
297
291
|
}
|
|
298
292
|
|
|
293
|
+
/** Compila o framework para o alvo em ~/.sabia/compilado/<alvo>/ (seção 11 do plano da 2.0). */
|
|
294
|
+
function compilarPara(alvo, fw) {
|
|
295
|
+
const versao = JSON.parse(fs.readFileSync(path.join(fw, "plugin.json"), "utf8")).version;
|
|
296
|
+
const base = alvo === "kiro" ? "~/.kiro/powers/installed/sabia/skills" : curto(PASTA_SKILLS[alvo]());
|
|
297
|
+
return compilar(fw, sabia("compilado", alvo), { base, raiz: alvo === "kiro", versao });
|
|
298
|
+
}
|
|
299
|
+
|
|
299
300
|
async function instalarAlvo(alvo, inv, opcoes) {
|
|
300
|
-
const fw = sabia("framework");
|
|
301
|
+
const fw = compilarPara(alvo, sabia("framework"));
|
|
301
302
|
if (alvo === "kiro") {
|
|
302
303
|
const nosso = inv.alvos.kiro?.power;
|
|
303
304
|
// o painel Powers instala em ~/.kiro/powers/installed/sabia: fora do inventário, é de outra instalação
|
|
@@ -314,11 +315,9 @@ async function instalarAlvo(alvo, inv, opcoes) {
|
|
|
314
315
|
}
|
|
315
316
|
const reg = (inv.alvos[alvo] ||= { copias: {}, mcp: [] });
|
|
316
317
|
for (const s of skillsDoFluxo(fw)) await copiar(path.join(fw, "skills", s), path.join(PASTA_SKILLS[alvo](), s), reg.copias, opcoes);
|
|
317
|
-
if (!opcoes.semMcp) await registrarMcps(
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (velhas.length) ctx.log(`aviso: skills antigas no opencode concorrem com as sa-*: ${velhas.join(", ")} (remova se não usa mais)`);
|
|
321
|
-
}
|
|
318
|
+
if (!opcoes.semMcp) await registrarMcps(sabia("framework"), reg);
|
|
319
|
+
const velhas = (fs.existsSync(PASTA_SKILLS.opencode()) ? fs.readdirSync(PASTA_SKILLS.opencode()) : []).filter((d) => /^sdd/.test(d));
|
|
320
|
+
if (velhas.length) ctx.log(`aviso: skills antigas no opencode concorrem com as sa-*: ${velhas.join(", ")} (remova se não usa mais)`);
|
|
322
321
|
}
|
|
323
322
|
|
|
324
323
|
/** Baixa o framework para uma pasta temporária e diz o que muda em relação a ~/.sabia/framework/. */
|
|
@@ -353,7 +352,7 @@ export async function instalar(opcoes = {}) {
|
|
|
353
352
|
fs.rmSync(sabia("framework"), { recursive: true, force: true });
|
|
354
353
|
fs.mkdirSync(sabia(), { recursive: true });
|
|
355
354
|
fs.cpSync(novo.pasta, sabia("framework"), { recursive: true });
|
|
356
|
-
inv.framework = { versao: novo.versao, sha: novo.sha, fonte: novo.fonte, instalado_em: new Date().toISOString() };
|
|
355
|
+
inv.framework = { versao: novo.versao, sha: novo.sha, fonte: novo.fonte, compilador: VERSAO, instalado_em: new Date().toISOString() };
|
|
357
356
|
ctx.log(`Sabiá ${novo.versao} em ~/.sabia/framework`);
|
|
358
357
|
try {
|
|
359
358
|
for (const alvo of await escolherAlvos(opcoes)) await instalarAlvo(alvo, inv, opcoes);
|
|
@@ -369,13 +368,18 @@ export async function atualizar(opcoes = {}) {
|
|
|
369
368
|
if (!inv.framework) throw new Error("o Sabiá não está instalado: rode `sabia instalar`");
|
|
370
369
|
const novo = baixarFramework({ fonte: inv.framework.fonte === REPO ? undefined : inv.framework.fonte, ...opcoes });
|
|
371
370
|
ctx.log(`Sabiá ${inv.framework.versao} → ${novo.versao}`);
|
|
372
|
-
|
|
371
|
+
// o compilador também conta: a mesma fonte compilada por outro CLI pode dar outra skill
|
|
372
|
+
const compilador = inv.framework.compilador === VERSAO ? null : `compilador ${inv.framework.compilador || "—"} → ${VERSAO}`;
|
|
373
|
+
if (!mostrarDiferencas(diferencas(sabia("framework"), novo.pasta)) && !compilador) return ctx.log("nada mudou");
|
|
374
|
+
if (compilador) ctx.log(` ${compilador}`);
|
|
373
375
|
if (!/^s/i.test(await ctx.perguntar("Atualizar? [s/N]", "n"))) return ctx.log("nada foi alterado");
|
|
374
376
|
fs.rmSync(sabia("framework"), { recursive: true, force: true });
|
|
375
377
|
fs.cpSync(novo.pasta, sabia("framework"), { recursive: true });
|
|
376
|
-
inv.framework = { ...inv.framework, versao: novo.versao, sha: novo.sha, instalado_em: new Date().toISOString() };
|
|
378
|
+
inv.framework = { ...inv.framework, versao: novo.versao, sha: novo.sha, compilador: VERSAO, instalado_em: new Date().toISOString() };
|
|
377
379
|
try {
|
|
378
380
|
for (const alvo of Object.keys(inv.alvos)) {
|
|
381
|
+
// alvo que saiu do Sabiá (o claude, na 2.0): fica como está; `sabia desinstalar --alvo <ele>` apaga as cópias
|
|
382
|
+
if (!ALVOS.includes(alvo)) { ctx.log(`${alvo}: não é mais alvo do Sabiá (${ALVOS.join(", ")}) — ignorado; tire com sabia desinstalar --alvo ${alvo}`); continue; }
|
|
379
383
|
if (alvo !== "kiro") { // skill que saiu do framework sai da ferramenta
|
|
380
384
|
const copias = inv.alvos[alvo].copias;
|
|
381
385
|
const fica = new Set(skillsDoFluxo(sabia("framework")).map((s) => curto(path.join(PASTA_SKILLS[alvo](), s))));
|
|
@@ -401,16 +405,15 @@ export async function desinstalar(opcoes = {}) {
|
|
|
401
405
|
for (const tk of Object.values(inv.toolkits)) if (tk.alvos?.[alvo]) { await apagar(tk.alvos[alvo].copias, opcoes); delete tk.alvos[alvo]; }
|
|
402
406
|
if (alvo === "kiro") ctx.exec("kiro-cli", ["powers", "uninstall", reg.power]);
|
|
403
407
|
else await apagar(reg.copias, opcoes);
|
|
404
|
-
for (const nome of reg.mcp || [])
|
|
405
|
-
|
|
406
|
-
else tirarMcpDoOpencode(nome);
|
|
407
|
-
}
|
|
408
|
+
if (alvo === "opencode") for (const nome of reg.mcp || []) tirarMcpDoOpencode(nome);
|
|
409
|
+
else if (reg.mcp?.length) ctx.log(`${alvo}: os MCPs ${reg.mcp.join(", ")} ficam (o Sabiá não mexe mais no ${alvo}); tire à mão se não usa`);
|
|
408
410
|
if (alvo !== "kiro" && Object.keys(reg.copias).length) continue; // ficou cópia alterada à mão
|
|
409
411
|
delete inv.alvos[alvo];
|
|
410
412
|
ctx.log(`${alvo}: Sabiá desinstalado`);
|
|
411
413
|
}
|
|
412
414
|
if (!Object.keys(inv.alvos).length) {
|
|
413
415
|
fs.rmSync(sabia("framework"), { recursive: true, force: true });
|
|
416
|
+
fs.rmSync(sabia("compilado"), { recursive: true, force: true });
|
|
414
417
|
inv.framework = null;
|
|
415
418
|
}
|
|
416
419
|
} finally {
|
|
@@ -421,13 +424,16 @@ export async function desinstalar(opcoes = {}) {
|
|
|
421
424
|
// ---------- toolkits ----------
|
|
422
425
|
|
|
423
426
|
/** Erros do toolkit.json: campos, nome, caminhos presos à pasta e arquivos que existem (o schema completo é o da setup). */
|
|
424
|
-
export function validarToolkit(pasta, nomeEsperado = null) {
|
|
427
|
+
export function validarToolkit(pasta, nomeEsperado = null, fw = null) {
|
|
425
428
|
const f = path.join(pasta, "toolkit.json");
|
|
426
429
|
if (!fs.existsSync(f)) return ["falta o toolkit.json"];
|
|
427
430
|
let m;
|
|
428
431
|
try { m = JSON.parse(fs.readFileSync(f, "utf8")); } catch (e) { return [`toolkit.json inválido: ${e.message}`]; }
|
|
429
432
|
const erros = [];
|
|
430
|
-
for (const c of ["nome", "versao", "descricao", "tipos-de-projeto", "skills"]) if (!(c in m)) erros.push(`falta o campo ${c}`);
|
|
433
|
+
for (const c of ["nome", "versao", "descricao", "autor", "tipos-de-projeto", "skills"]) if (!(c in m)) erros.push(`falta o campo ${c}`);
|
|
434
|
+
// quem fez responde pelo toolkit: nome e área aparecem no listar e na setup
|
|
435
|
+
if (m.autor && (typeof m.autor.nome !== "string" || typeof m.autor.area !== "string" || !m.autor.nome.trim() || !m.autor.area.trim()))
|
|
436
|
+
erros.push("autor: precisa de nome e area");
|
|
431
437
|
const nomeOk = (n) => typeof n === "string" && /^[a-z0-9][a-z0-9-]*$/.test(n) && !n.startsWith("sa-");
|
|
432
438
|
if (!nomeOk(m.nome)) erros.push(`nome inválido (kebab-case, sem sa-): ${m.nome}`);
|
|
433
439
|
if (nomeEsperado && m.nome !== nomeEsperado) erros.push(`nome ${m.nome} ≠ ${nomeEsperado} do índice`);
|
|
@@ -437,9 +443,17 @@ export function validarToolkit(pasta, nomeEsperado = null) {
|
|
|
437
443
|
try { if (!fs.existsSync(path.join(pasta, dentro(c, de)))) erros.push(`${de}: não existe ${c}`); } catch (e) { erros.push(e.message); }
|
|
438
444
|
};
|
|
439
445
|
const agentes = new Set((m.agentes || []).map((a) => a.nome));
|
|
446
|
+
// sem o framework (toolkits baixar antes do instalar), o gancho é conferido na setup
|
|
447
|
+
const ganchos = fw && fs.existsSync(path.join(fw, "fluxo")) ? carregar(fw) : null;
|
|
440
448
|
for (const s of m.skills || []) {
|
|
441
449
|
existe(s.caminho, "skills");
|
|
442
450
|
if (s.passo?.agente && !agentes.has(s.passo.agente)) erros.push(`passo de ${s.caminho}: agente ${s.passo.agente} não está em agentes`);
|
|
451
|
+
if (s.passo?.passo && ganchos) {
|
|
452
|
+
const [sk, fl, id] = s.passo.passo.split(".");
|
|
453
|
+
const x = ganchos.fluxos[`${sk}.${fl}`]?.passos.find((p) => p.id === id);
|
|
454
|
+
if (!x) erros.push(`passo de ${s.caminho}: ${s.passo.passo} não existe no Sabiá instalado`);
|
|
455
|
+
else if (!x.desvio) erros.push(`passo de ${s.caminho}: ${s.passo.passo} não aceita desvio de toolkit`);
|
|
456
|
+
}
|
|
443
457
|
}
|
|
444
458
|
for (const s of m.steering || []) existe(s.caminho, "steering");
|
|
445
459
|
for (const a of m.agentes || []) {
|
|
@@ -463,7 +477,7 @@ export function lerIndice(fonte) {
|
|
|
463
477
|
const m = JSON.parse(fs.readFileSync(path.join(raiz, ".claude-plugin", "marketplace.json"), "utf8"));
|
|
464
478
|
return (m.plugins || []).map((p) => {
|
|
465
479
|
const s = p.source;
|
|
466
|
-
const base = { nome: p.name, versao: p.version, descricao: p.description, indice: fonte };
|
|
480
|
+
const base = { nome: p.name, versao: p.version, descricao: p.description, customiza: p.customiza, autor: p.autor, indice: fonte };
|
|
467
481
|
if (typeof s === "string") return { ...base, url: local ? path.resolve(raiz, dentro(s.replace(/^\.\//, ""), fonte)) : fonte, subpasta: local ? null : s.replace(/^\.\//, "") };
|
|
468
482
|
if (s?.source === "github") return { ...base, url: `https://github.com/${s.repo}.git`, ref: s.ref, sha: s.sha };
|
|
469
483
|
if (s?.source === "url") return { ...base, url: s.url, ref: s.ref, sha: s.sha };
|
|
@@ -485,7 +499,7 @@ export async function baixarToolkit(alvo, opcoes = {}) {
|
|
|
485
499
|
const [nomeOuUrl, versaoOuRef] = separarRef(alvo);
|
|
486
500
|
const e = ehUrl ? { nome: null, url: nomeOuUrl, ref: versaoOuRef } : acharNoIndice(nomeOuUrl, versaoOuRef, opcoes);
|
|
487
501
|
const { pasta, sha } = baixar(e.url, e);
|
|
488
|
-
const erros = validarToolkit(pasta, e.nome);
|
|
502
|
+
const erros = validarToolkit(pasta, e.nome, sabia("framework"));
|
|
489
503
|
if (erros.length) throw new Error(`toolkit recusado:\n- ${erros.join("\n- ")}`);
|
|
490
504
|
const m = JSON.parse(fs.readFileSync(path.join(pasta, "toolkit.json"), "utf8"));
|
|
491
505
|
const destino = sabia("toolkits", m.nome);
|
|
@@ -521,7 +535,8 @@ export async function instalarToolkit(alvo, opcoes = {}) {
|
|
|
521
535
|
// mesmos nomes de skill e de agente: as duas no usuário se sobrescreveriam
|
|
522
536
|
if (rival) throw new Error(`${baixado} e ${rival} são versões do mesmo toolkit (base ${familia}); instale um só: sabia toolkits remover ${rival}`);
|
|
523
537
|
const tk = (inv.toolkits[baixado] ||= { versao: m.versao, alvos: {} });
|
|
524
|
-
const
|
|
538
|
+
const doInv = Object.keys(inv.alvos).filter((a) => ALVOS.includes(a));
|
|
539
|
+
const alvos = opcoes.alvo || !doInv.length ? await escolherAlvos(opcoes) : doInv;
|
|
525
540
|
try {
|
|
526
541
|
for (const a of alvos.filter((x) => x !== "kiro")) {
|
|
527
542
|
const reg = (tk.alvos[a] ||= { copias: {} });
|
|
@@ -567,7 +582,7 @@ export async function atualizarToolkits(nome, opcoes = {}) {
|
|
|
567
582
|
const origem = JSON.parse(fs.readFileSync(sabia("toolkits", n, ".origem.json"), "utf8"));
|
|
568
583
|
const alvo = origem.indice ? n : `${origem.url}${origem.ref ? "@" + origem.ref : ""}`;
|
|
569
584
|
await instalarToolkit(alvo, { ...opcoes, atualizar: true, indice: origem.indice || opcoes.indice,
|
|
570
|
-
alvo: Object.keys(inv.toolkits[n]?.alvos || {}).join(",") || "kiro" });
|
|
585
|
+
alvo: Object.keys(inv.toolkits[n]?.alvos || {}).filter((a) => ALVOS.includes(a)).join(",") || "kiro" });
|
|
571
586
|
}
|
|
572
587
|
}
|
|
573
588
|
|
|
@@ -581,39 +596,212 @@ export function listarToolkits(opcoes = {}) {
|
|
|
581
596
|
const inv = lerInventario();
|
|
582
597
|
const projeto = path.join(process.cwd(), ".sabia", "config", "agentes.json");
|
|
583
598
|
const doProjeto = fs.existsSync(projeto) ? JSON.parse(fs.readFileSync(projeto, "utf8")).toolkits || {} : {};
|
|
584
|
-
|
|
599
|
+
// autor e customiza: do toolkit baixado (o toolkit.json) ou, sem ele, da entrada do índice
|
|
600
|
+
const manifesto = (n) => { const f = sabia("toolkits", n, "toolkit.json"); return fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, "utf8")) : {}; };
|
|
601
|
+
const linhas = [], doIndice = {};
|
|
585
602
|
for (const i of opcoes.indice ? [opcoes.indice] : indices()) {
|
|
586
603
|
try {
|
|
587
|
-
for (const e of lerIndice(i))
|
|
604
|
+
for (const e of lerIndice(i)) {
|
|
605
|
+
doIndice[e.nome] = e;
|
|
606
|
+
linhas.push([e.nome, "", e.versao || "—", inv.toolkits[e.nome]?.versao || "—", doProjeto[e.nome]?.versao || "—", e.descricao || ""]);
|
|
607
|
+
}
|
|
588
608
|
} catch (err) {
|
|
589
609
|
ctx.log(`índice ${i}: ${err.message}`);
|
|
590
610
|
}
|
|
591
611
|
}
|
|
592
|
-
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)"]);
|
|
612
|
+
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)"]);
|
|
613
|
+
for (const l of linhas) {
|
|
614
|
+
const a = manifesto(l[0]).autor ?? doIndice[l[0]]?.autor;
|
|
615
|
+
l[1] = a ? `${a.nome} (${a.area})` : "—";
|
|
616
|
+
}
|
|
593
617
|
// a customização logo abaixo da base, com ↳ e de onde veio
|
|
594
618
|
for (const l of [...linhas]) {
|
|
595
|
-
const
|
|
596
|
-
const c = fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, "utf8")).customiza : null;
|
|
619
|
+
const c = manifesto(l[0]).customiza ?? doIndice[l[0]]?.customiza;
|
|
597
620
|
if (!c) continue;
|
|
598
621
|
linhas.splice(linhas.indexOf(l), 1);
|
|
599
622
|
const i = linhas.findIndex((b) => b[0] === c.toolkit);
|
|
600
623
|
// só a primeira mudança (quem customizou), como resumo; a lista inteira está no toolkit.json
|
|
601
|
-
l[
|
|
624
|
+
l[5] = `customiza ${c.toolkit} ${c.versao}${c.mudancas?.length ? `: ${c.mudancas[0]}${c.mudancas.length > 1 ? "; …" : ""}` : ""}${l[5] ? " — " + l[5] : ""}`;
|
|
602
625
|
l[0] = `↳ ${l[0]}`;
|
|
603
626
|
linhas.splice(i < 0 ? linhas.length : i + 1, 0, l);
|
|
604
627
|
}
|
|
605
|
-
ctx.log(["| Toolkit | No índice | Instalado | Neste projeto | Descrição |", "
|
|
628
|
+
ctx.log(["| Toolkit | Autor | No índice | Instalado | Neste projeto | Descrição |", "|---|---|---|---|---|---|",
|
|
606
629
|
...linhas.map((l) => `| ${l.join(" | ")} |`)].join("\n"));
|
|
607
630
|
return linhas;
|
|
608
631
|
}
|
|
609
632
|
|
|
633
|
+
// ---------- rota (diagnóstico, para pessoas) ----------
|
|
634
|
+
|
|
635
|
+
// toolkit 1.x (passo.papel, e "codificador" antes do renome): o mesmo mapa do gerar_agentes.py
|
|
636
|
+
const GANCHO_1X = { desenvolvedor: "sa-codificar.codificar.tarefa", codificador: "sa-codificar.codificar.tarefa", especificador: "sa-prd.prd.escrever" };
|
|
637
|
+
const lerJson = (f, padrao) => (fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, "utf8")) : padrao);
|
|
638
|
+
const lerFm = (f) => (fs.existsSync(f) ? partes(fs.readFileSync(f, "utf8")).dados || {} : null);
|
|
639
|
+
|
|
640
|
+
/** Glob do `passo.arquivos`: `**` atravessa `/`; `*` e `?` não. */
|
|
641
|
+
export function casaGlob(glob, arquivo) {
|
|
642
|
+
const rx = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\/|\*\*|\*|\?/g, (t) => ({ "**/": "(?:.*/)?", "**": ".*", "*": "[^/]*", "?": "[^/]" })[t]);
|
|
643
|
+
return new RegExp(`^${rx}$`).test(arquivo);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** Colunas alinhadas; a última sem preenchimento. */
|
|
647
|
+
const colunas = (linhas) => {
|
|
648
|
+
const w = [];
|
|
649
|
+
for (const l of linhas) l.slice(0, -1).forEach((c, i) => { w[i] = Math.max(w[i] || 0, c.length); });
|
|
650
|
+
return linhas.map((l) => l.map((c, i) => (i < l.length - 1 ? c.padEnd(w[i]) : c)).join(" ").trimEnd()).join("\n");
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
/** Framework instalado (ou --framework), projeto (ou --projeto) e os desvios ligados nele, como o gerador os resolve. */
|
|
654
|
+
function contextoRota(opcoes) {
|
|
655
|
+
const fw = path.resolve(opcoes.framework || sabia("framework"));
|
|
656
|
+
if (!fs.existsSync(path.join(fw, "fluxo"))) throw new Error(`${curto(fw)} não tem fluxo/: rode \`sabia instalar\` ou passe --framework`);
|
|
657
|
+
const projeto = path.resolve(opcoes.projeto || process.cwd());
|
|
658
|
+
const cfg = lerJson(path.join(projeto, ".sabia", "config", "agentes.json"), {});
|
|
659
|
+
const m = carregar(fw);
|
|
660
|
+
const desvios = (cfg.passos || []).flatMap((e) => {
|
|
661
|
+
const pasta = cfg.toolkits?.[e.toolkit]?.pasta;
|
|
662
|
+
const dir = !pasta ? sabia("toolkits", e.toolkit) : pasta.startsWith("~/") ? longo(pasta) : path.resolve(projeto, pasta);
|
|
663
|
+
const man = lerJson(path.join(dir, "toolkit.json"), null);
|
|
664
|
+
const ext = man?.skills?.find((x) => x.caminho === e.skill && x.passo)?.passo;
|
|
665
|
+
if (!ext) { ctx.log(`aviso: toolkit ${e.toolkit} sem passo em ${e.skill} (${curto(dir)})`); return []; }
|
|
666
|
+
// como o gancho() do gerador: só passo do framework com "desvio": true (senão o gerador recusa e o prompt não o tem)
|
|
667
|
+
const passo = ext.passo || GANCHO_1X[ext.papel], [sk, f, p] = (passo || "").split(".");
|
|
668
|
+
if (!m.fluxos[`${sk}.${f}`]?.passos.find((x) => x.id === p)?.desvio) {
|
|
669
|
+
ctx.log(`aviso: toolkit ${e.toolkit}: ${passo ? `o passo ${passo}` : `o papel ${ext.papel}`} não aceita desvio de toolkit (o gerador recusa) — ignorado`);
|
|
670
|
+
return [];
|
|
671
|
+
}
|
|
672
|
+
const agente = e.agente && ext.agente ? ext.agente : null;
|
|
673
|
+
const skills = agente ? (man.agentes || []).find((a) => a.nome === agente)?.skills || [] : [e.skill];
|
|
674
|
+
return [{ toolkit: e.toolkit, versao: man.versao, passo, para: ext.para, arquivos: ext.arquivos || [],
|
|
675
|
+
agente, skill: path.basename(path.dirname(e.skill)), skills: skills.map((x) => path.basename(path.dirname(x))) }];
|
|
676
|
+
});
|
|
677
|
+
return { fw, projeto, m, versao: lerJson(path.join(fw, "plugin.json"), {}).version || "?", desvios };
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/** Os passos do fluxo `q` na ordem, com o que o projeto mudou: origem sabia | projeto (corpo trocado) | inserido | removido. */
|
|
681
|
+
function resolverFluxo(c, q) {
|
|
682
|
+
const fl = c.m.fluxos[q.replace("/", ".")];
|
|
683
|
+
if (!fl) throw new Error(`fluxo ${q} não existe em ${curto(c.fw)} (use skill.fluxo, ex.: sa-codificar.codificar)`);
|
|
684
|
+
const dir = path.join(c.projeto, ".sabia", "fluxo", fl.skill, fl.fluxo);
|
|
685
|
+
const ops = lerFm(path.join(dir, `${fl.fluxo}.md`)) || {};
|
|
686
|
+
const doProjeto = (id) => path.join(dir, "passos", `${id}.md`);
|
|
687
|
+
const passos = fl.passos.map((x) => {
|
|
688
|
+
const base = lerFm(doProjeto(x.id));
|
|
689
|
+
const origem = (ops.remover || []).includes(x.id) ? "removido" : base ? "projeto" : "sabia";
|
|
690
|
+
return { ...x, origem, base: base?.base, arq: base ? doProjeto(x.id) : x.arq };
|
|
691
|
+
});
|
|
692
|
+
let falta = [...(ops.inserir || [])];
|
|
693
|
+
while (falta.length) { // em qualquer ordem, inclusive depois de outro inserido
|
|
694
|
+
const prontos = falta.filter((o) => passos.some((x) => x.id === o.depois));
|
|
695
|
+
if (!prontos.length) { ctx.log(`aviso: ${q}: inserir ${falta.map((o) => `${o.id} depois de ${o.depois}`).join(", ")}: o 'depois' não existe`); break; }
|
|
696
|
+
for (const o of prontos) {
|
|
697
|
+
let i = passos.findIndex((x) => x.id === o.depois);
|
|
698
|
+
while (passos[i + 1]?.depois === o.depois) i++;
|
|
699
|
+
passos.splice(i + 1, 0, { id: o.id, depois: o.depois, origem: "inserido", arq: doProjeto(o.id) });
|
|
700
|
+
}
|
|
701
|
+
falta = falta.filter((o) => !prontos.includes(o));
|
|
702
|
+
}
|
|
703
|
+
return { fl, q: `${fl.skill}.${fl.fluxo}`, passos };
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const travasDe = (fl, x) => [...new Set([...(fl.d.travas_gerais || []), ...(x?.travas || [])])].join(", ") || "—";
|
|
707
|
+
|
|
708
|
+
/** `sabia rota <arquivo>`: o passo com gancho cujo desvio casa com o arquivo, o dono, o toolkit, o motivo e as travas. */
|
|
709
|
+
export function rota(arquivo, opcoes = {}) {
|
|
710
|
+
const c = contextoRota(opcoes);
|
|
711
|
+
let rel = path.relative(c.projeto, path.resolve(arquivo));
|
|
712
|
+
if (rel.startsWith("..")) rel = arquivo; // fora do projeto: o caminho como veio
|
|
713
|
+
rel = rel.split(path.sep).join("/");
|
|
714
|
+
const ganchos = Object.entries(c.m.fluxos).flatMap(([q, fl]) => fl.passos.filter((x) => x.desvio).map((x) => `${q}.${x.id}`));
|
|
715
|
+
let casou = null;
|
|
716
|
+
for (const d of c.desvios) { const g = d.arquivos.find((a) => casaGlob(a, rel)); if (g) { casou = { d, g }; break; } }
|
|
717
|
+
// sem casar: o passo dos desvios por arquivo do projeto (o de código), senão o primeiro gancho do framework
|
|
718
|
+
const pid = casou?.d.passo || c.desvios.find((d) => d.arquivos.length)?.passo || ganchos[0];
|
|
719
|
+
if (!pid) throw new Error(`${curto(c.fw)} não tem passo com desvio de toolkit`);
|
|
720
|
+
const { fl, passos } = resolverFluxo(c, pid.split(".").slice(0, 2).join("."));
|
|
721
|
+
const x = passos.find((y) => y.id === pid.split(".")[2]);
|
|
722
|
+
if (!x) throw new Error(`o desvio aponta para ${pid}, que não é passo do Sabiá instalado`);
|
|
723
|
+
const agentes = [`.opencode/agents/${fl.d.dono}.md`, `.kiro/agents/${fl.d.dono}.json`].filter((f) => fs.existsSync(path.join(c.projeto, f)));
|
|
724
|
+
const linhas = [
|
|
725
|
+
["passo", pid, x.origem === "projeto" ? `${path.relative(c.projeto, x.arq)} (projeto)` : `${curto(x.arq)} (Sabiá ${c.versao})`],
|
|
726
|
+
["dono", fl.d.dono, agentes.join(", ") || "agente não gerado neste projeto"],
|
|
727
|
+
];
|
|
728
|
+
if (casou) linhas.push(["desvio", casou.d.agente || `skill ${casou.d.skill}`, `toolkit ${casou.d.toolkit} ${casou.d.versao}`],
|
|
729
|
+
["motivo", `casou ${casou.g}`], ["skills", [fl.skill, ...casou.d.skills].join(", ")]);
|
|
730
|
+
else {
|
|
731
|
+
linhas.push(["desvio", "— fluxo padrão", "nenhum padrão casou"]);
|
|
732
|
+
for (const d of c.desvios.filter((y) => y.passo === pid)) linhas.push(["pelo texto", `"${d.para}" → ${d.agente || `skill ${d.skill}`}`, "o modelo decide pela tarefa"]);
|
|
733
|
+
}
|
|
734
|
+
linhas.push(["travas", travasDe(fl, x)]);
|
|
735
|
+
return colunas(linhas);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const sid = (t) => t.replace(/[^A-Za-z0-9_]/g, "_");
|
|
739
|
+
const rotulo = (t) => t.replace(/[;:]/g, ",").replace(/[`"#]/g, "");
|
|
740
|
+
|
|
741
|
+
/** `sabia rota --fluxo <skill.fluxo>`: a tabela resolvida (com o que o projeto mudou) e o diagrama de estados. */
|
|
742
|
+
export function rotaFluxo(q, opcoes = {}) {
|
|
743
|
+
const c = contextoRota(opcoes);
|
|
744
|
+
const r = resolverFluxo(c, q), { fl } = r;
|
|
745
|
+
const desvios = c.desvios.filter((d) => d.passo?.startsWith(`${r.q}.`));
|
|
746
|
+
const origem = (x) => ({ sabia: "Sabiá", projeto: `projeto${x.base ? ` (base ${x.base})` : ""}`,
|
|
747
|
+
inserido: `projeto, inserido depois de ${x.depois}`, removido: "removido no projeto" })[x.origem];
|
|
748
|
+
let n = 0;
|
|
749
|
+
const tabela = [["#", "passo", "origem", "chama", "travas", "saídas"], ...r.passos.map((x) => [
|
|
750
|
+
x.origem === "removido" ? "-" : String(++n), x.id + (x.desvio ? " ⇢" : ""), origem(x), [].concat(x.chama || []).join(", ") || "—",
|
|
751
|
+
(x.travas || []).join(", ") || "—", Object.entries(x.saidas || {}).map(([k, v]) => `${k} → ${v.replace(/^→ /, "passa a ")}`).join("; ") || "—"])];
|
|
752
|
+
const cab = [["fluxo", r.q, `dono ${fl.d.dono}`, `Sabiá ${c.versao}`], ["travas", (fl.d.travas_gerais || []).join(", ") || "—"],
|
|
753
|
+
...desvios.map((d) => ["desvio ⇢", `${d.passo.split(".")[2]}: ${d.arquivos.join(", ") || `"${d.para}"`}`, `→ ${d.agente || `skill ${d.skill}`}`, `toolkit ${d.toolkit} ${d.versao}`])];
|
|
754
|
+
|
|
755
|
+
const out = ["stateDiagram-v2"], declarados = new Set();
|
|
756
|
+
const estado = (id, nome = id) => {
|
|
757
|
+
const s = sid(id);
|
|
758
|
+
if (s !== nome && !declarados.has(s)) { declarados.add(s); out.push(` state "${nome}" as ${s}`); }
|
|
759
|
+
return s;
|
|
760
|
+
};
|
|
761
|
+
const ativos = r.passos.filter((x) => x.origem !== "removido");
|
|
762
|
+
ativos.forEach((x, i) => {
|
|
763
|
+
const a = estado(x.id), prox = ativos[i + 1];
|
|
764
|
+
if (!i) out.push(` [*] --> ${a}`);
|
|
765
|
+
for (const [cond, dest] of Object.entries(x.saidas || {})) {
|
|
766
|
+
if (dest === "fim") out.push(` ${a} --> [*]: ${rotulo(cond)}`);
|
|
767
|
+
else if (dest.startsWith("→ ")) { const q2 = dest.slice(2).trim(); out.push(` ${a} --> ${estado(`x_${q2}`, q2)}: ${rotulo(cond)} (passagem)`); }
|
|
768
|
+
else if (ativos.some((y) => y.id === dest)) out.push(` ${a} --> ${estado(dest)}: ${rotulo(cond)}`);
|
|
769
|
+
}
|
|
770
|
+
out.push(prox ? ` ${a} --> ${estado(prox.id)}${prox.origem === "inserido" ? ": projeto (inserido)" : ""}` : ` ${a} --> [*]`);
|
|
771
|
+
const nota = [x.chama && `chama ${[].concat(x.chama).join(", ")}`, x.origem === "projeto" && `corpo do projeto${x.base ? ` (base ${x.base})` : ""}`,
|
|
772
|
+
x.desvio && desvios.length && `desvio ⇢ ${desvios.filter((d) => d.passo === `${r.q}.${x.id}`).map((d) => d.agente || d.skill).join(", ")}`].filter(Boolean);
|
|
773
|
+
if (nota.length) out.push(` note right of ${a}: ${rotulo(nota.join(" · "))}`);
|
|
774
|
+
});
|
|
775
|
+
return [colunas(cab), "", colunas(tabela), "", "```mermaid", ...out, "```"].join("\n");
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/** `sabia rota --mapa`: as arestas entre fluxos de todo o framework (chama e passagens), em Mermaid. */
|
|
779
|
+
export function rotaMapa(opcoes = {}) {
|
|
780
|
+
const c = contextoRota(opcoes);
|
|
781
|
+
const out = ["flowchart LR"], nos = new Set(), vistas = new Set();
|
|
782
|
+
const no = (q) => { const s = sid(q); if (!nos.has(s)) { nos.add(s); out.push(` ${s}["${q}"]`); } return s; };
|
|
783
|
+
for (const [q, fl] of Object.entries(c.m.fluxos))
|
|
784
|
+
for (const x of fl.passos) {
|
|
785
|
+
const arestas = [...[].concat(x.chama || []).map((d) => [d, "-->"]),
|
|
786
|
+
...Object.values(x.saidas || {}).filter((d) => d.startsWith("→ ")).map((d) => [d.slice(2).trim(), "-.->"])];
|
|
787
|
+
for (const [d, seta] of arestas) {
|
|
788
|
+
const q2 = d.split(".").slice(0, 2).join(".");
|
|
789
|
+
if (vistas.has(`${q} ${seta} ${q2} ${x.id}`)) continue;
|
|
790
|
+
vistas.add(`${q} ${seta} ${q2} ${x.id}`);
|
|
791
|
+
out.push(` ${no(q)} ${seta}|${x.id}| ${no(q2)}`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return [`Fluxos do Sabiá ${c.versao}: --> chama (volta ao passo) · -.-> passagem (o fluxo termina e segue no outro); rótulo = passo de origem`,
|
|
795
|
+
"", "```mermaid", ...out, "```"].join("\n");
|
|
796
|
+
}
|
|
797
|
+
|
|
610
798
|
// ---------- linha de comando ----------
|
|
611
799
|
|
|
612
800
|
const AJUDA = `${BANNER}
|
|
613
801
|
|
|
614
|
-
sabia ${VERSAO} — instala o Sabiá e os toolkits no Kiro
|
|
802
|
+
sabia ${VERSAO} — instala o Sabiá e os toolkits no Kiro e no opencode
|
|
615
803
|
|
|
616
|
-
sabia instalar [--alvo kiro,
|
|
804
|
+
sabia instalar [--alvo kiro,opencode] [--versao X] [--fonte <pasta|url-git>[@ref]]
|
|
617
805
|
sabia atualizar [--versao X]
|
|
618
806
|
sabia desinstalar [--alvo …]
|
|
619
807
|
sabia toolkits listar [--indice <url-git|pasta>]
|
|
@@ -621,6 +809,10 @@ sabia ${VERSAO} — instala o Sabiá e os toolkits no Kiro, no Claude Code e no
|
|
|
621
809
|
sabia toolkits instalar <nome>[@versao] | <url-git>[@ref] [--alvo …] (sem argumento, no projeto: as versões dele)
|
|
622
810
|
sabia toolkits atualizar [<nome>]
|
|
623
811
|
sabia toolkits remover <nome>
|
|
812
|
+
sabia rota <arquivo> o passo com desvio de toolkit, o dono, o toolkit, o motivo e as travas
|
|
813
|
+
sabia rota --fluxo <skill.fluxo> o fluxo resolvido no projeto (tabela e diagrama Mermaid)
|
|
814
|
+
sabia rota --mapa as arestas entre fluxos (chama e passagens), em Mermaid
|
|
815
|
+
[--projeto <pasta>] (padrão: a atual) [--framework <pasta>] (padrão: ~/.sabia/framework)
|
|
624
816
|
|
|
625
817
|
--sim responde "sim" às confirmações (cópia alterada à mão continua perguntando, salvo --forcar)
|
|
626
818
|
--forcar sobrescreve o que não foi instalado pelo Sabiá ou foi alterado à mão
|
|
@@ -635,14 +827,17 @@ export function lerArgs(argv) {
|
|
|
635
827
|
const a = argv[i];
|
|
636
828
|
if (!a.startsWith("--")) { posicionais.push(a); continue; }
|
|
637
829
|
const chave = a.slice(2).replace(/-(\w)/g, (_, c) => c.toUpperCase());
|
|
638
|
-
if (["sim", "forcar", "semMcp", "ajuda", "help"].includes(chave)) opcoes[chave] = true;
|
|
830
|
+
if (["sim", "forcar", "semMcp", "mapa", "ajuda", "help"].includes(chave)) opcoes[chave] = true;
|
|
831
|
+
else if (argv[i + 1] === undefined || argv[i + 1].startsWith("--")) throw new Error(`--${a.slice(2)} precisa de um valor`);
|
|
639
832
|
else opcoes[chave] = argv[++i];
|
|
640
833
|
}
|
|
641
834
|
return { posicionais, opcoes };
|
|
642
835
|
}
|
|
643
836
|
|
|
644
837
|
export async function main(argv = process.argv.slice(2)) {
|
|
645
|
-
|
|
838
|
+
let args;
|
|
839
|
+
try { args = lerArgs(argv); } catch (e) { console.error(`sabia: ${e.message}`); return 1; }
|
|
840
|
+
const { posicionais: [cmd, sub, arg], opcoes } = args;
|
|
646
841
|
if (opcoes.sim) { // cópia alterada à mão continua perguntando: só o --forcar passa por cima
|
|
647
842
|
const antes = ctx.perguntar;
|
|
648
843
|
ctx.perguntar = async (texto, padrao) => (/alterado à mão/.test(texto) ? antes(texto, padrao) : /\[s\/N\]/.test(texto) ? "s" : padrao);
|
|
@@ -656,6 +851,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
656
851
|
else if (cmd === "toolkits" && sub === "instalar") await (arg ? instalarToolkit(arg, opcoes) : instalarDoProjeto(opcoes));
|
|
657
852
|
else if (cmd === "toolkits" && sub === "atualizar") await atualizarToolkits(arg, opcoes);
|
|
658
853
|
else if (cmd === "toolkits" && sub === "remover" && arg) await removerToolkit(arg, opcoes);
|
|
854
|
+
else if (cmd === "rota" && (sub || opcoes.fluxo || opcoes.mapa))
|
|
855
|
+
ctx.log(opcoes.mapa ? rotaMapa(opcoes) : opcoes.fluxo ? rotaFluxo(opcoes.fluxo, opcoes) : rota(sub, opcoes));
|
|
659
856
|
else { ctx.log(AJUDA); return cmd && !opcoes.ajuda && !opcoes.help ? 1 : 0; }
|
|
660
857
|
return 0;
|
|
661
858
|
} catch (e) {
|