sabia 1.1.3 → 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 +224 -38
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,7 +424,7 @@ 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;
|
|
@@ -440,9 +443,17 @@ export function validarToolkit(pasta, nomeEsperado = null) {
|
|
|
440
443
|
try { if (!fs.existsSync(path.join(pasta, dentro(c, de)))) erros.push(`${de}: não existe ${c}`); } catch (e) { erros.push(e.message); }
|
|
441
444
|
};
|
|
442
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;
|
|
443
448
|
for (const s of m.skills || []) {
|
|
444
449
|
existe(s.caminho, "skills");
|
|
445
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
|
+
}
|
|
446
457
|
}
|
|
447
458
|
for (const s of m.steering || []) existe(s.caminho, "steering");
|
|
448
459
|
for (const a of m.agentes || []) {
|
|
@@ -488,7 +499,7 @@ export async function baixarToolkit(alvo, opcoes = {}) {
|
|
|
488
499
|
const [nomeOuUrl, versaoOuRef] = separarRef(alvo);
|
|
489
500
|
const e = ehUrl ? { nome: null, url: nomeOuUrl, ref: versaoOuRef } : acharNoIndice(nomeOuUrl, versaoOuRef, opcoes);
|
|
490
501
|
const { pasta, sha } = baixar(e.url, e);
|
|
491
|
-
const erros = validarToolkit(pasta, e.nome);
|
|
502
|
+
const erros = validarToolkit(pasta, e.nome, sabia("framework"));
|
|
492
503
|
if (erros.length) throw new Error(`toolkit recusado:\n- ${erros.join("\n- ")}`);
|
|
493
504
|
const m = JSON.parse(fs.readFileSync(path.join(pasta, "toolkit.json"), "utf8"));
|
|
494
505
|
const destino = sabia("toolkits", m.nome);
|
|
@@ -524,7 +535,8 @@ export async function instalarToolkit(alvo, opcoes = {}) {
|
|
|
524
535
|
// mesmos nomes de skill e de agente: as duas no usuário se sobrescreveriam
|
|
525
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}`);
|
|
526
537
|
const tk = (inv.toolkits[baixado] ||= { versao: m.versao, alvos: {} });
|
|
527
|
-
const
|
|
538
|
+
const doInv = Object.keys(inv.alvos).filter((a) => ALVOS.includes(a));
|
|
539
|
+
const alvos = opcoes.alvo || !doInv.length ? await escolherAlvos(opcoes) : doInv;
|
|
528
540
|
try {
|
|
529
541
|
for (const a of alvos.filter((x) => x !== "kiro")) {
|
|
530
542
|
const reg = (tk.alvos[a] ||= { copias: {} });
|
|
@@ -570,7 +582,7 @@ export async function atualizarToolkits(nome, opcoes = {}) {
|
|
|
570
582
|
const origem = JSON.parse(fs.readFileSync(sabia("toolkits", n, ".origem.json"), "utf8"));
|
|
571
583
|
const alvo = origem.indice ? n : `${origem.url}${origem.ref ? "@" + origem.ref : ""}`;
|
|
572
584
|
await instalarToolkit(alvo, { ...opcoes, atualizar: true, indice: origem.indice || opcoes.indice,
|
|
573
|
-
alvo: Object.keys(inv.toolkits[n]?.alvos || {}).join(",") || "kiro" });
|
|
585
|
+
alvo: Object.keys(inv.toolkits[n]?.alvos || {}).filter((a) => ALVOS.includes(a)).join(",") || "kiro" });
|
|
574
586
|
}
|
|
575
587
|
}
|
|
576
588
|
|
|
@@ -618,13 +630,178 @@ export function listarToolkits(opcoes = {}) {
|
|
|
618
630
|
return linhas;
|
|
619
631
|
}
|
|
620
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
|
+
|
|
621
798
|
// ---------- linha de comando ----------
|
|
622
799
|
|
|
623
800
|
const AJUDA = `${BANNER}
|
|
624
801
|
|
|
625
|
-
sabia ${VERSAO} — instala o Sabiá e os toolkits no Kiro
|
|
802
|
+
sabia ${VERSAO} — instala o Sabiá e os toolkits no Kiro e no opencode
|
|
626
803
|
|
|
627
|
-
sabia instalar [--alvo kiro,
|
|
804
|
+
sabia instalar [--alvo kiro,opencode] [--versao X] [--fonte <pasta|url-git>[@ref]]
|
|
628
805
|
sabia atualizar [--versao X]
|
|
629
806
|
sabia desinstalar [--alvo …]
|
|
630
807
|
sabia toolkits listar [--indice <url-git|pasta>]
|
|
@@ -632,6 +809,10 @@ sabia ${VERSAO} — instala o Sabiá e os toolkits no Kiro, no Claude Code e no
|
|
|
632
809
|
sabia toolkits instalar <nome>[@versao] | <url-git>[@ref] [--alvo …] (sem argumento, no projeto: as versões dele)
|
|
633
810
|
sabia toolkits atualizar [<nome>]
|
|
634
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)
|
|
635
816
|
|
|
636
817
|
--sim responde "sim" às confirmações (cópia alterada à mão continua perguntando, salvo --forcar)
|
|
637
818
|
--forcar sobrescreve o que não foi instalado pelo Sabiá ou foi alterado à mão
|
|
@@ -646,14 +827,17 @@ export function lerArgs(argv) {
|
|
|
646
827
|
const a = argv[i];
|
|
647
828
|
if (!a.startsWith("--")) { posicionais.push(a); continue; }
|
|
648
829
|
const chave = a.slice(2).replace(/-(\w)/g, (_, c) => c.toUpperCase());
|
|
649
|
-
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`);
|
|
650
832
|
else opcoes[chave] = argv[++i];
|
|
651
833
|
}
|
|
652
834
|
return { posicionais, opcoes };
|
|
653
835
|
}
|
|
654
836
|
|
|
655
837
|
export async function main(argv = process.argv.slice(2)) {
|
|
656
|
-
|
|
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;
|
|
657
841
|
if (opcoes.sim) { // cópia alterada à mão continua perguntando: só o --forcar passa por cima
|
|
658
842
|
const antes = ctx.perguntar;
|
|
659
843
|
ctx.perguntar = async (texto, padrao) => (/alterado à mão/.test(texto) ? antes(texto, padrao) : /\[s\/N\]/.test(texto) ? "s" : padrao);
|
|
@@ -667,6 +851,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
667
851
|
else if (cmd === "toolkits" && sub === "instalar") await (arg ? instalarToolkit(arg, opcoes) : instalarDoProjeto(opcoes));
|
|
668
852
|
else if (cmd === "toolkits" && sub === "atualizar") await atualizarToolkits(arg, opcoes);
|
|
669
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));
|
|
670
856
|
else { ctx.log(AJUDA); return cmd && !opcoes.ajuda && !opcoes.help ? 1 : 0; }
|
|
671
857
|
return 0;
|
|
672
858
|
} catch (e) {
|