@aksp/opencrew 1.10.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +100 -0
- package/README.md +15 -4
- package/package.json +1 -1
- package/src/commands/init.js +4 -5
- package/src/commands/update.js +8 -0
- package/src/lib/resumo.js +5 -1
- package/templates/AGENTS.md +16 -9
- package/templates/_opencrew/.opencrew-version +1 -1
- package/templates/_opencrew/core/architect.agent.yaml +25 -16
- package/templates/_opencrew/core/best-practices/_catalog.yaml +5 -0
- package/templates/_opencrew/core/best-practices/texto-livre.md +23 -0
- package/templates/_opencrew/core/formato-da-crew.md +162 -0
- package/templates/_opencrew/core/prompts/build.prompt.md +35 -59
- package/templates/_opencrew/core/prompts/design.prompt.md +16 -16
- package/templates/_opencrew/core/prompts/discovery.prompt.md +28 -8
- package/templates/_opencrew/core/prompts/documento.prompt.md +9 -5
- package/templates/_opencrew/core/prompts/entrega.prompt.md +7 -2
- package/templates/_opencrew/core/prompts/repair.prompt.md +75 -84
- package/templates/_opencrew/core/runner.pipeline.md +25 -28
- package/templates/_opencrew/core/scripts/caminho/argumentos.mjs +3 -1
- package/templates/_opencrew/core/scripts/caminho/nucleo.mjs +16 -0
- package/templates/_opencrew/core/scripts/caminho.mjs +11 -8
- package/templates/_opencrew/core/scripts/conferir-fontes/relatorio.mjs +4 -2
- package/templates/_opencrew/core/scripts/conferir-fontes.mjs +22 -16
- package/templates/_opencrew/core/scripts/conserto/achados.mjs +158 -0
- package/templates/_opencrew/core/scripts/conserto/aplicar.mjs +156 -0
- package/templates/_opencrew/core/scripts/conserto/argumentos.mjs +51 -0
- package/templates/_opencrew/core/scripts/conserto/crew.mjs +126 -0
- package/templates/_opencrew/core/scripts/conserto/edicoes.mjs +133 -0
- package/templates/_opencrew/core/scripts/conserto/gravar.mjs +55 -0
- package/templates/_opencrew/core/scripts/conserto.mjs +82 -0
- package/templates/_opencrew/core/scripts/documento/markdown.mjs +2 -1
- package/templates/_opencrew/core/scripts/documento/pacote.mjs +1 -0
- package/templates/_opencrew/core/scripts/documento/perfil.mjs +14 -0
- package/templates/_opencrew/core/scripts/entrega/pendencias.mjs +4 -1
- package/templates/_opencrew/core/scripts/entrega/separar.mjs +2 -1
- package/templates/_opencrew/core/scripts/verificar/documento.mjs +20 -0
- package/templates/_opencrew/core/scripts/verificar/medicao.mjs +2 -1
- package/templates/_opencrew/core/scripts/verificar/proibicoes.mjs +30 -5
- package/templates/_opencrew/core/scripts/verificar.mjs +3 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// O diagnóstico do conserto: o que falta numa crew para as melhorias do runtime valerem nela.
|
|
2
|
+
// Cada achado tem um código e as linhas, em PT-BR fixo, que o usuário lê. Só leitura.
|
|
3
|
+
// Spec: fase-u4a-conserto-de-crews.md, §4 e regras 5, 6 e 9 (repositório do OpenCrew).
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { lerFrontmatter, semBom } from '../verificar/leitura.mjs';
|
|
6
|
+
import { itensDeProibicao } from '../verificar/proibicoes.mjs';
|
|
7
|
+
import { idDoAgente, lerBruto, listaDe } from './crew.mjs';
|
|
8
|
+
import { lerCsv } from './edicoes.mjs';
|
|
9
|
+
|
|
10
|
+
const EXPORTACAO = new Set(['pdf', 'csv', 'formatted-post']);
|
|
11
|
+
const plural = (n, um, varios) => (n === 1 ? um : varios);
|
|
12
|
+
const editar = (crew) => `Resolve-se editando a crew: /opencrew edit ${crew.nome}`;
|
|
13
|
+
const irreversivel = (passo) => passo.dados.side_effects === 'irreversible';
|
|
14
|
+
const revisoes = (crew) => crew.passos.filter((p) => p.revisao);
|
|
15
|
+
|
|
16
|
+
function manifesto(crew) {
|
|
17
|
+
if (!crew.agentes.length) return null;
|
|
18
|
+
const { colunas, linhas } = lerCsv(crew.csv);
|
|
19
|
+
const semNome = (l) => !l.displayName || l.displayName.toLowerCase() === (l.title ?? '').toLowerCase();
|
|
20
|
+
if (crew.csv !== null && colunas.includes('displayName') && !linhas.some(semNome)) return null;
|
|
21
|
+
return ['O arquivo de nomes da crew (crew-party.csv) está incompleto: aparece a função no lugar do nome.', 'Para consertar: --aplicar "manifesto"'];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function nomeDeAgente(crew) {
|
|
25
|
+
const semNome = crew.agentes.filter((a) => String(a.dados.name ?? '').trim().split(/\s+/).filter(Boolean).length < 2);
|
|
26
|
+
if (!semNome.length) return null;
|
|
27
|
+
const n = semNome.length;
|
|
28
|
+
return [
|
|
29
|
+
`${n} ${plural(n, 'agente', 'agentes')} sem nome de duas palavras.`,
|
|
30
|
+
...semNome.map((a) => `${a.id} (name: "${a.dados.name ?? ''}")`),
|
|
31
|
+
'Para consertar: --aplicar "nome:<agente>=<Nome Sobrenome>" e depois --aplicar "manifesto"',
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Os passos que o verificador mede: do passo de `on_reject` até o anterior à revisão (regra 6). */
|
|
36
|
+
function medidos(crew) {
|
|
37
|
+
const dentro = new Set();
|
|
38
|
+
for (const revisao of revisoes(crew)) {
|
|
39
|
+
const de = crew.passos.findIndex((p) => p.numero === revisao.volta);
|
|
40
|
+
const ate = crew.passos.indexOf(revisao);
|
|
41
|
+
if (de >= 0) crew.passos.slice(de, ate).forEach((p) => dentro.add(p));
|
|
42
|
+
}
|
|
43
|
+
return crew.passos.filter((p) => dentro.has(p) && p.arquivo && !p.checkpoint && !p.revisao);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const semFormato = (passo) => !passo.dados.format || EXPORTACAO.has(String(passo.dados.format));
|
|
47
|
+
const saidaDe = (passo) => path.posix.basename(String(passo.dados.outputFile ?? passo.citado).replace(/\\/g, '/'));
|
|
48
|
+
|
|
49
|
+
function formato(crew) {
|
|
50
|
+
const passos = medidos(crew).filter(semFormato);
|
|
51
|
+
if (!passos.length) return null;
|
|
52
|
+
const n = passos.length;
|
|
53
|
+
return [
|
|
54
|
+
`${n} ${plural(n, 'passo que a revisão confere não diz', 'passos que a revisão confere não dizem')} o formato do texto.`,
|
|
55
|
+
'Sem o formato, o redator não recebe o guia desse tipo de texto, e o verificador procura no arquivo peças de rede (legenda, post, título de blog).',
|
|
56
|
+
`Passos: ${passos.map((p) => `${p.numero} (${saidaDe(p)})`).join(', ')}`,
|
|
57
|
+
'Para consertar: --aplicar "formato:<passo>=<formato>"',
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fontes(crew) {
|
|
62
|
+
const linhas = semBom(crew.yaml ?? '').split(/\r?\n/);
|
|
63
|
+
const inicio = linhas.findIndex((l) => /^fontes\s*:/.test(l));
|
|
64
|
+
const fim = linhas.findIndex((l, i) => i > inicio && /^[^\s#-]/.test(l));
|
|
65
|
+
const daLista = inicio < 0 ? [] : linhas.slice(inicio + 1, fim < 0 ? linhas.length : fim);
|
|
66
|
+
if (daLista.some((l) => /^\s*(?:-\s*)?caminho\s*:\s*\S/.test(l))) return null;
|
|
67
|
+
return [
|
|
68
|
+
'A crew não registra os arquivos do projeto que ela precisa ler.',
|
|
69
|
+
'Sem isso, ela escreve sem conhecer o que o projeto já decidiu.',
|
|
70
|
+
'Para consertar: --aplicar "fonte:<caminho>=<para que>" (um por arquivo ou pasta)',
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function proibicao(crew) {
|
|
75
|
+
const pendentes = itensDeProibicao(semBom(crew.memoria ?? '')).filter((i) => i.pendente);
|
|
76
|
+
if (!pendentes.length) return null;
|
|
77
|
+
const n = pendentes.length;
|
|
78
|
+
return [
|
|
79
|
+
`${n} ${plural(n, 'proibição', 'proibições')} sem trecho entre aspas: o verificador não consegue barrar.`,
|
|
80
|
+
...pendentes.map((i) => `${i.n}. ${i.texto}`),
|
|
81
|
+
'Para consertar: --aplicar "proibicao:<n>=<trecho>" ou --aplicar "proibicao:<n>=revisao-humana"',
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A skill instalada no projeto declara `side_effects: irreversible`? (regra 9) */
|
|
86
|
+
function publica(raiz, skill) {
|
|
87
|
+
const texto = lerBruto(path.join(raiz, 'skills', skill, 'SKILL.md'));
|
|
88
|
+
return texto !== null && lerFrontmatter(semBom(texto))?.side_effects === 'irreversible';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function semMarca(crew) {
|
|
92
|
+
const doPasso = (passo) => {
|
|
93
|
+
const agente = crew.agentes.find((a) => a.id === idDoAgente(passo));
|
|
94
|
+
const skills = [...listaDe(agente?.bruto, 'skills'), ...listaDe(passo.bruto, 'skills_needed')];
|
|
95
|
+
const skill = skills.find((s) => /^[A-Za-z0-9._-]+$/.test(s) && publica(crew.raiz, s));
|
|
96
|
+
return skill ? `Passo ${passo.numero} — agente ${agente?.id ?? idDoAgente(passo)}, skill ${skill}` : null;
|
|
97
|
+
};
|
|
98
|
+
const linhas = crew.passos.filter((p) => p.arquivo && !p.checkpoint && !irreversivel(p)).map(doPasso).filter(Boolean);
|
|
99
|
+
if (!linhas.length) return null;
|
|
100
|
+
const n = linhas.length;
|
|
101
|
+
return [
|
|
102
|
+
`${n} ${plural(n, 'passo', 'passos')} de agente que tem ferramenta de publicar ou enviar, sem a marca de passo irreversível.`,
|
|
103
|
+
...linhas,
|
|
104
|
+
'Se o passo publica ou envia: --aplicar "irreversivel:<passo>"',
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function semRevisao(crew) {
|
|
109
|
+
if (revisoes(crew).length || !crew.passos.length) return null;
|
|
110
|
+
return ['A crew não tem passo de revisão: nada é conferido antes de chegar a você.', editar(crew)];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Posição da última revisão e do primeiro checkpoint depois dela (-1 quando não há). */
|
|
114
|
+
function fecho(crew) {
|
|
115
|
+
const revisao = crew.passos.indexOf(revisoes(crew).at(-1));
|
|
116
|
+
const aprovacao = crew.passos.findIndex((p, i) => i > revisao && p.checkpoint);
|
|
117
|
+
return { revisao, aprovacao };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function semAprovacaoFinal(crew) {
|
|
121
|
+
const { revisao, aprovacao } = fecho(crew);
|
|
122
|
+
if (revisao < 0 || aprovacao >= 0) return null;
|
|
123
|
+
return ['Depois da revisão não há um ponto de aprovação seu.', editar(crew)];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function publicaAntes(crew) {
|
|
127
|
+
const { revisao, aprovacao } = fecho(crew);
|
|
128
|
+
if (revisao < 0) return null;
|
|
129
|
+
const limite = aprovacao >= 0 ? aprovacao : revisao;
|
|
130
|
+
const cedo = crew.passos.filter((p, i) => i < limite && irreversivel(p)).map((p) => p.numero);
|
|
131
|
+
if (!cedo.length) return null;
|
|
132
|
+
const quem = cedo.length === 1 ? `O passo ${cedo[0]} publica ou envia` : `Os passos ${cedo.join(', ')} publicam ou enviam`;
|
|
133
|
+
return [`${quem} antes da revisão e da sua aprovação final.`, 'Enquanto estiver assim, o que sai não passou pela revisão.', editar(crew)];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function passoFaltando(crew) {
|
|
137
|
+
const semArquivo = crew.passos.filter((p) => !p.arquivo).map((p) => `O passo ${p.numero} cita ${p.citado || 'um arquivo sem nome'}, que não existe.`);
|
|
138
|
+
const existe = (numero) => crew.passos.some((p) => p.numero === numero);
|
|
139
|
+
const semAlvo = revisoes(crew).filter((p) => !existe(p.volta)).map((p) => `O passo ${p.numero} manda voltar ao passo ${p.volta ?? p.dados.on_reject}, que não existe.`);
|
|
140
|
+
const linhas = [...semArquivo, ...semAlvo];
|
|
141
|
+
return linhas.length ? ['A sequência de passos cita o que não existe.', ...linhas, editar(crew)] : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// A ordem em que os achados aparecem (§4 da spec).
|
|
145
|
+
const CONFERENCIAS = [
|
|
146
|
+
['nome-de-agente', nomeDeAgente], ['manifesto', manifesto], ['formato', formato], ['fontes', fontes],
|
|
147
|
+
['proibicao', proibicao], ['irreversivel', semMarca], ['sem-revisao', semRevisao],
|
|
148
|
+
['sem-aprovacao-final', semAprovacaoFinal], ['publica-antes', publicaAntes], ['passo-faltando', passoFaltando],
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Os achados de uma crew, na ordem da spec.
|
|
153
|
+
* @param {object} crew o que `lerCrew` devolveu
|
|
154
|
+
* @returns {Array<{ codigo: string, linhas: string[] }>} a primeira linha é o título do achado
|
|
155
|
+
*/
|
|
156
|
+
export function diagnosticar(crew) {
|
|
157
|
+
return CONFERENCIAS.map(([codigo, conferir]) => ({ codigo, linhas: conferir(crew) })).filter((a) => a.linhas);
|
|
158
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// `--aplicar`: valida todos os itens, monta as mudanças em memória e só então grava — cada arquivo
|
|
2
|
+
// alterado ganha antes uma cópia `.bak` (a que já existe fica). Um item recusado: nada é gravado.
|
|
3
|
+
// Spec: fase-u4a-conserto-de-crews.md, regras 1 a 4 e 7 (repositório do OpenCrew).
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { dentroDoProjeto } from '../comum.mjs';
|
|
7
|
+
import { lerFrontmatter, semAcento, semBom } from '../verificar/leitura.mjs';
|
|
8
|
+
import { ehRevisaoHumana, itensDeProibicao, lerLinha } from '../verificar/proibicoes.mjs';
|
|
9
|
+
import { comChave, comFonte, comSufixo, comTitulo, montarCsv } from './edicoes.mjs';
|
|
10
|
+
import { erroAntesDeGravar } from './gravar.mjs';
|
|
11
|
+
|
|
12
|
+
const MSG = {
|
|
13
|
+
semValor: (item) => `Sobra ou falta valor em --aplicar "${item.escrito}". Veja os itens com --ajuda.`,
|
|
14
|
+
semPasso: (item) => `Passo não encontrado em --aplicar "${item.escrito}".`,
|
|
15
|
+
checkpoint: (n, item) => `O passo ${n} é um ponto de aprovação: --aplicar "${item.escrito}" não vale para ele.`,
|
|
16
|
+
semFrontmatter: (n) => `Não altero o passo ${n}: o arquivo não tem o bloco de campos no começo (entre linhas ---), ou o campo ocupa mais de uma linha. Ajuste pela edição da crew.`,
|
|
17
|
+
fontesNaLinha: 'Não altero o `crew.yaml`: a lista `fontes:` está escrita numa linha só. Ajuste pela edição da crew.',
|
|
18
|
+
trechoIlegivel: 'O verificador não consegue ler esse trecho como trava: use até 200 caracteres, sem crase nem aspas.',
|
|
19
|
+
semFormato: (id) => `Formato "${id}" não encontrado em \`_opencrew/best-practices.local/\` nem em \`_opencrew/core/best-practices/\`.`,
|
|
20
|
+
fonteAbsoluta: (c) => `A fonte tem de ser um caminho relativo à pasta do projeto: ${c}`,
|
|
21
|
+
fonteFora: (c) => `Caminho fora do projeto: ${c}`,
|
|
22
|
+
fonteAusente: (c) => `Não encontrei no projeto: ${c}`,
|
|
23
|
+
semAgentes: 'A crew não tem agentes em `agents/`: não há de onde refazer o crew-party.csv.',
|
|
24
|
+
semAgente: (item) => `Agente não encontrado em --aplicar "${item.escrito}".`,
|
|
25
|
+
nomeCurto: 'O nome do agente tem duas palavras (nome e sobrenome), sem aspas.',
|
|
26
|
+
jaTemNome: (id, nome) => `O agente ${id} já tem nome: ${nome}. Para trocar, use /opencrew edit.`,
|
|
27
|
+
semFrontmatterDoAgente: (id) => `Não altero o agente ${id}: o arquivo não tem o bloco de campos no começo (entre linhas ---), ou o campo name ocupa mais de uma linha.`,
|
|
28
|
+
semItem: (item) => `Item de proibição não encontrado em --aplicar "${item.escrito}".`,
|
|
29
|
+
jaResolvido: (n) => `A proibição ${n} já tem trecho entre aspas ou já está marcada como revisão humana.`,
|
|
30
|
+
comAspas: 'O trecho da trava não pode ter aspas duplas.',
|
|
31
|
+
foraDoItem: (n, trecho) => `O trecho "${trecho}" não aparece na proibição ${n}.`,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** As mudanças em memória: por arquivo, o texto do disco (`antes`) e o texto novo (`depois`). */
|
|
35
|
+
function novoPlano() {
|
|
36
|
+
const arquivos = new Map();
|
|
37
|
+
const plano = {
|
|
38
|
+
arquivos,
|
|
39
|
+
trocas: 0,
|
|
40
|
+
texto: (arquivo, doDisco) => (arquivos.has(arquivo) ? arquivos.get(arquivo).depois : doDisco),
|
|
41
|
+
/** Texto igual ao que já está no plano não conta como troca. Devolve null (não há erro). */
|
|
42
|
+
trocar(arquivo, doDisco, depois) {
|
|
43
|
+
if (depois === plano.texto(arquivo, doDisco)) return null;
|
|
44
|
+
arquivos.set(arquivo, { antes: arquivos.has(arquivo) ? arquivos.get(arquivo).antes : doDisco, depois });
|
|
45
|
+
plano.trocas += 1;
|
|
46
|
+
return null;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
return plano;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function passoDe(crew, item) {
|
|
53
|
+
const passo = /^\d+$/.test(item.alvo) ? crew.passos.find((p) => p.numero === Number(item.alvo)) : null;
|
|
54
|
+
if (!passo?.arquivo) return { erro: MSG.semPasso(item) };
|
|
55
|
+
return passo.checkpoint ? { erro: MSG.checkpoint(passo.numero, item) } : { passo };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const formatoExiste = (raiz, id) => [['best-practices.local'], ['core', 'best-practices']].some((p) => existsSync(path.join(raiz, '_opencrew', ...p, `${id}.md`)));
|
|
59
|
+
|
|
60
|
+
function formato(crew, item, plano) {
|
|
61
|
+
const { passo, erro } = passoDe(crew, item);
|
|
62
|
+
if (erro) return erro;
|
|
63
|
+
if (!/^[a-z0-9-]+$/.test(item.valor) || !formatoExiste(crew.raiz, item.valor)) return MSG.semFormato(item.valor);
|
|
64
|
+
const novo = comChave(plano.texto(passo.arquivo, passo.bruto), 'format', item.valor, 'agent');
|
|
65
|
+
return novo === null ? MSG.semFrontmatter(passo.numero) : plano.trocar(passo.arquivo, passo.bruto, novo);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function irreversivel(crew, item, plano) {
|
|
69
|
+
const { passo, erro } = passoDe(crew, item);
|
|
70
|
+
if (erro) return erro;
|
|
71
|
+
if (item.valor) return MSG.semValor(item);
|
|
72
|
+
const inline = comChave(plano.texto(passo.arquivo, passo.bruto), 'execution', 'inline');
|
|
73
|
+
const marcado = inline === null ? null : comChave(inline, 'side_effects', 'irreversible');
|
|
74
|
+
return marcado === null ? MSG.semFrontmatter(passo.numero) : plano.trocar(passo.arquivo, passo.bruto, marcado);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const ehAbsoluto = (c) => path.isAbsolute(c) || /^[A-Za-z]:/.test(c) || /^[\\/]/.test(c);
|
|
78
|
+
|
|
79
|
+
function fonte(crew, item, plano) {
|
|
80
|
+
if (!item.alvo || !item.valor) return MSG.semValor(item);
|
|
81
|
+
if (ehAbsoluto(item.alvo)) return MSG.fonteAbsoluta(item.alvo);
|
|
82
|
+
if (!dentroDoProjeto(crew.raiz, item.alvo)) return MSG.fonteFora(item.alvo);
|
|
83
|
+
if (!existsSync(path.resolve(crew.raiz, item.alvo))) return MSG.fonteAusente(item.alvo);
|
|
84
|
+
const caminho = item.alvo.replace(/\\/g, '/');
|
|
85
|
+
const novo = comFonte(plano.texto(crew.arquivos.yaml, crew.yaml), caminho, item.valor);
|
|
86
|
+
return novo === null ? MSG.fontesNaLinha : plano.trocar(crew.arquivos.yaml, crew.yaml, novo);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function manifesto(crew, item, plano) {
|
|
90
|
+
if (item.alvo || item.valor) return MSG.semValor(item);
|
|
91
|
+
if (!crew.agentes.length) return MSG.semAgentes;
|
|
92
|
+
const { csv } = crew.arquivos;
|
|
93
|
+
// Os agentes como ficam depois dos itens anteriores desta chamada (um `nome:` antes do `manifesto`).
|
|
94
|
+
const agentes = crew.agentes.map((a) => ({ ...a, dados: lerFrontmatter(semBom(plano.texto(a.arquivo, a.bruto))) ?? {} }));
|
|
95
|
+
return plano.trocar(csv, crew.csv, montarCsv(agentes, plano.texto(csv, crew.csv)));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const palavras = (nome) => String(nome ?? '').trim().split(/\s+/).filter(Boolean);
|
|
99
|
+
|
|
100
|
+
/** O nome de duas palavras no `name:` do agente e no título do arquivo (a primeira linha `# …`). */
|
|
101
|
+
function nome(crew, item, plano) {
|
|
102
|
+
const agente = crew.agentes.find((a) => a.id === item.alvo);
|
|
103
|
+
if (!agente) return MSG.semAgente(item);
|
|
104
|
+
if (palavras(item.valor).length < 2 || /["\r\n]/.test(item.valor)) return MSG.nomeCurto;
|
|
105
|
+
if (agente.dados.name === item.valor) return null;
|
|
106
|
+
if (palavras(agente.dados.name).length >= 2) return MSG.jaTemNome(agente.id, agente.dados.name);
|
|
107
|
+
const comNome = comChave(plano.texto(agente.arquivo, agente.bruto), 'name', JSON.stringify(item.valor));
|
|
108
|
+
if (comNome === null) return MSG.semFrontmatterDoAgente(agente.id);
|
|
109
|
+
return plano.trocar(agente.arquivo, agente.bruto, comTitulo(comNome, item.valor));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Por que o item não pode ganhar a marca pedida, ou null. */
|
|
113
|
+
function recusaDaProibicao(alvo, item, humano) {
|
|
114
|
+
if (!alvo.pendente) return MSG.jaResolvido(alvo.n);
|
|
115
|
+
if (humano) return null;
|
|
116
|
+
if (item.valor.includes('"')) return MSG.comAspas;
|
|
117
|
+
return semAcento(alvo.texto).includes(semAcento(item.valor)) ? null : MSG.foraDoItem(alvo.n, item.valor);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function proibicao(crew, item, plano) {
|
|
121
|
+
const { memoria } = crew.arquivos;
|
|
122
|
+
const texto = plano.texto(memoria, crew.memoria) ?? '';
|
|
123
|
+
const alvo = itensDeProibicao(semBom(texto)).find((i) => String(i.n) === item.alvo);
|
|
124
|
+
if (!alvo || !item.valor) return MSG.semItem(item);
|
|
125
|
+
const humano = item.valor === 'revisao-humana';
|
|
126
|
+
const linha = texto.split(/\r?\n/)[alvo.indice];
|
|
127
|
+
if (humano ? ehRevisaoHumana(linha) : linha.includes(`"${item.valor}"`)) return null;
|
|
128
|
+
const recusa = recusaDaProibicao(alvo, item, humano);
|
|
129
|
+
if (recusa) return recusa;
|
|
130
|
+
const novo = comSufixo(texto, alvo.indice, humano ? ' (revisão humana)' : ` — trava: "${item.valor}"`);
|
|
131
|
+
// A trava só vale se o verificador a lê de volta, igual (crase, aspa solta e trecho longo quebram a leitura).
|
|
132
|
+
const lido = lerLinha(novo.split(/\r?\n/)[alvo.indice]);
|
|
133
|
+
if (!humano && !(lido.proibidos.includes(item.valor) && lido.preferidos.length === 0)) return MSG.trechoIlegivel;
|
|
134
|
+
return plano.trocar(memoria, crew.memoria, novo);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const ACOES = { manifesto, nome, formato, fonte, proibicao, irreversivel };
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Valida os itens e monta as mudanças, sem gravar.
|
|
141
|
+
* @returns {{ erro: string } | { mudancas: Array<{ arquivo, antes, depois }>, iguais: string[] }}
|
|
142
|
+
* `iguais`: os itens que não mudaram nada (já estava assim)
|
|
143
|
+
*/
|
|
144
|
+
export function planejar(crew, itens) {
|
|
145
|
+
const plano = novoPlano();
|
|
146
|
+
const iguais = [];
|
|
147
|
+
for (const item of itens) {
|
|
148
|
+
const antes = plano.trocas;
|
|
149
|
+
const erro = ACOES[item.tipo](crew, item, plano);
|
|
150
|
+
if (erro) return { erro };
|
|
151
|
+
if (plano.trocas === antes) iguais.push(item.escrito);
|
|
152
|
+
}
|
|
153
|
+
const mudancas = [...plano.arquivos].map(([arquivo, m]) => ({ arquivo, ...m })).filter((m) => m.depois !== m.antes);
|
|
154
|
+
const erro = erroAntesDeGravar(crew.raiz, mudancas);
|
|
155
|
+
return erro ? { erro } : { mudancas, iguais };
|
|
156
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Linha de comando do `conserto.mjs`: a crew, os itens de `--aplicar` e a ajuda.
|
|
2
|
+
// Spec: fase-u4a-conserto-de-crews.md, §3 (repositório do OpenCrew).
|
|
3
|
+
|
|
4
|
+
export const USO = 'Uso: node _opencrew/core/scripts/conserto.mjs --crew "crews/<crew>" [--aplicar "<item>" …]';
|
|
5
|
+
export const AJUDA = [
|
|
6
|
+
USO,
|
|
7
|
+
'Sem --aplicar: só lê a crew e mostra o que há para consertar.',
|
|
8
|
+
'Itens de --aplicar (cada gravação deixa uma cópia .bak do arquivo de antes):',
|
|
9
|
+
' manifesto refaz o crew-party.csv a partir dos agentes',
|
|
10
|
+
' nome:<agente>=<Nome Sobrenome> grava o nome de duas palavras no arquivo do agente',
|
|
11
|
+
' formato:<passo>=<formato> grava o formato do texto no passo',
|
|
12
|
+
' fonte:<caminho>=<para que> registra um arquivo ou pasta do projeto em fontes:',
|
|
13
|
+
' proibicao:<n>=<trecho> o trecho do item n vira trava do verificador',
|
|
14
|
+
' proibicao:<n>=revisao-humana o item n fica para o revisor',
|
|
15
|
+
' irreversivel:<passo> marca o passo que publica ou envia',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export const TIPOS = ['manifesto', 'nome', 'formato', 'fonte', 'proibicao', 'irreversivel'];
|
|
19
|
+
const OPCAO = /^--(crew|aplicar|ajuda)(?:=(.*))?$/s;
|
|
20
|
+
const ITEM = /^([a-z-]+)(?::([^=]*))?(?:=(.*))?$/s;
|
|
21
|
+
|
|
22
|
+
/** Texto que veio da linha de comando e volta numa mensagem: uma linha só, até 200 caracteres. */
|
|
23
|
+
export const limpar = (valor) => String(valor).replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* `argv` → `{ crew, itens, ajuda, estranho }`. Opção vale como `--nome valor` e `--nome=valor`;
|
|
27
|
+
* `--aplicar` pode repetir. `estranho`: o primeiro argumento que não é opção conhecida.
|
|
28
|
+
*/
|
|
29
|
+
export function lerArgs(argv) {
|
|
30
|
+
const args = { crew: undefined, itens: [], ajuda: false, estranho: undefined };
|
|
31
|
+
for (let i = 0; i < argv.length; i++) {
|
|
32
|
+
const [, nome, colado] = argv[i].match(OPCAO) ?? [];
|
|
33
|
+
if (!nome) args.estranho ??= argv[i];
|
|
34
|
+
else if (nome === 'ajuda') args.ajuda = true;
|
|
35
|
+
else {
|
|
36
|
+
const valor = colado ?? (i + 1 < argv.length ? argv[++i] : '');
|
|
37
|
+
if (nome === 'crew') args.crew = valor;
|
|
38
|
+
else args.itens.push(valor);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return args;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Um item de `--aplicar`: `tipo[:alvo][=valor]`.
|
|
46
|
+
* @returns {{ tipo: string, alvo: string, valor: string, escrito: string } | null} null = tipo desconhecido
|
|
47
|
+
*/
|
|
48
|
+
export function lerItem(escrito) {
|
|
49
|
+
const [, tipo, alvo = '', valor = ''] = String(escrito).match(ITEM) ?? [];
|
|
50
|
+
return TIPOS.includes(tipo) ? { tipo, alvo: alvo.trim(), valor: valor.trim(), escrito: limpar(escrito) } : null;
|
|
51
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Leitura de uma crew para o conserto: `crew.yaml`, os passos do pipeline, os agentes, o manifesto
|
|
2
|
+
// e a memória. A leitura é tolerante (aceita as formas que versões antigas gravaram) e nunca grava.
|
|
3
|
+
// Spec: fase-u4a-conserto-de-crews.md, regra 5 (repositório do OpenCrew).
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { realDentroDe } from '../comum.mjs';
|
|
7
|
+
import { lerFrontmatter, semBom } from '../verificar/leitura.mjs';
|
|
8
|
+
|
|
9
|
+
/** O texto do arquivo como está no disco (com BOM e fim de linha), ou null quando não existe. */
|
|
10
|
+
export function lerBruto(arquivo) {
|
|
11
|
+
return existsSync(arquivo) && statSync(arquivo).isFile() ? readFileSync(arquivo, 'utf8') : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Valor de uma linha de YAML simples: sem aspas e sem o comentário do fim. */
|
|
15
|
+
const limpo = (valor) => valor.replace(/\s+#.*$/, '').trim().replace(/^(["'])(.*)\1$/, '$2');
|
|
16
|
+
const PAR = /^\s*(?:-\s+)?([A-Za-z_]+)\s*:\s*(.*)$/;
|
|
17
|
+
|
|
18
|
+
/** As entradas de `steps:` do `pipeline.yaml`, na ordem: `{ step, file, on_reject, … }`. */
|
|
19
|
+
export function entradasDoPipeline(texto) {
|
|
20
|
+
const entradas = [];
|
|
21
|
+
let dentro = false;
|
|
22
|
+
for (const linha of semBom(texto ?? '').split(/\r?\n/)) {
|
|
23
|
+
if (!linha.trim() || /^\s*#/.test(linha)) continue; // linha em branco e comentário não mudam a leitura
|
|
24
|
+
if (/^\S/.test(linha) && !/^-\s/.test(linha)) dentro = /^steps\s*:/.test(linha);
|
|
25
|
+
else if (dentro) anotar(entradas, linha);
|
|
26
|
+
}
|
|
27
|
+
return entradas.map(({ coluna: _coluna, ...entrada }) => entrada);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Só valem as chaves da própria entrada (na coluna da primeira): chave de um bloco de dentro não conta. */
|
|
31
|
+
function anotar(entradas, linha) {
|
|
32
|
+
const item = linha.match(/^(\s*-\s+)(.*)$/);
|
|
33
|
+
if (item) entradas.push({ coluna: item[1].length });
|
|
34
|
+
const atual = entradas.at(-1);
|
|
35
|
+
if (!atual || (!item && linha.match(/^\s*/)[0].length !== atual.coluna)) return;
|
|
36
|
+
const par = linha.match(PAR);
|
|
37
|
+
if (par) atual[par[1]] = limpo(par[2]);
|
|
38
|
+
else if (item) atual.file = limpo(item[2]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Onde está o arquivo do passo: com ou sem `steps/` na frente (regra 5). O passo é sempre um
|
|
43
|
+
* arquivo de dentro da crew: caminho absoluto ou com `..` não é lido nem alterado.
|
|
44
|
+
*/
|
|
45
|
+
function arquivoDoPasso(pasta, citado) {
|
|
46
|
+
const partes = citado.split(/[\\/]/);
|
|
47
|
+
if (path.isAbsolute(citado) || /^[A-Za-z]:/.test(citado) || partes.includes('..')) return null;
|
|
48
|
+
const candidatos = [['pipeline', 'steps', citado], ['pipeline', citado], [citado]];
|
|
49
|
+
const achado = candidatos.map((c) => path.join(pasta, ...c)).find((c) => realDentroDe(pasta, c) && lerBruto(c) !== null);
|
|
50
|
+
return achado ?? null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Número inteiro escrito como número ou entre aspas; senão, null. */
|
|
54
|
+
const inteiro = (v) => (/^\d+$/.test(String(v ?? '').trim()) ? Number(v) : null);
|
|
55
|
+
|
|
56
|
+
function passoDe(pasta, entrada, posicao) {
|
|
57
|
+
const arquivo = entrada.file ? arquivoDoPasso(pasta, entrada.file) : null;
|
|
58
|
+
const bruto = arquivo ? lerBruto(arquivo) : null;
|
|
59
|
+
const dados = (bruto && lerFrontmatter(semBom(bruto))) || {};
|
|
60
|
+
const volta = dados.on_reject ?? entrada.on_reject;
|
|
61
|
+
return {
|
|
62
|
+
numero: inteiro(entrada.step) ?? posicao + 1,
|
|
63
|
+
citado: entrada.file ?? '',
|
|
64
|
+
arquivo,
|
|
65
|
+
bruto,
|
|
66
|
+
dados,
|
|
67
|
+
checkpoint: dados.type === 'checkpoint',
|
|
68
|
+
revisao: volta !== undefined && volta !== '',
|
|
69
|
+
volta: inteiro(volta),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Sem `pipeline.yaml` (ou sem entradas), os passos são os arquivos de `pipeline/steps`, em ordem. */
|
|
74
|
+
function entradasDaPasta(pasta) {
|
|
75
|
+
const dir = path.join(pasta, 'pipeline', 'steps');
|
|
76
|
+
if (!existsSync(dir)) return [];
|
|
77
|
+
return readdirSync(dir).filter((f) => f.endsWith('.md')).sort().map((file) => ({ file }));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Lista de um frontmatter: `chave: [a, b]` ou um item `- a` por linha. */
|
|
81
|
+
export function listaDe(bruto, chave) {
|
|
82
|
+
const fm = semBom(bruto ?? '').match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? '';
|
|
83
|
+
const linhas = fm.split(/\r?\n/);
|
|
84
|
+
const i = linhas.findIndex((l) => new RegExp(`^${chave}\\s*:`).test(l));
|
|
85
|
+
if (i < 0) return [];
|
|
86
|
+
const naLinha = linhas[i].slice(linhas[i].indexOf(':') + 1).trim();
|
|
87
|
+
if (naLinha) return naLinha.replace(/^\[|\]$/g, '').split(',').map(limpo).filter(Boolean);
|
|
88
|
+
const itens = [];
|
|
89
|
+
for (let n = i + 1; n < linhas.length && /^\s*-\s+/.test(linhas[n]); n++) itens.push(limpo(linhas[n].replace(/^\s*-\s+/, '')));
|
|
90
|
+
return itens;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function agentesDe(pasta) {
|
|
94
|
+
const dir = path.join(pasta, 'agents');
|
|
95
|
+
if (!existsSync(dir)) return [];
|
|
96
|
+
const arquivos = readdirSync(dir).filter((f) => f.endsWith('.agent.md')).sort();
|
|
97
|
+
return arquivos.map((f) => {
|
|
98
|
+
const bruto = lerBruto(path.join(dir, f));
|
|
99
|
+
return { id: f.slice(0, -'.agent.md'.length), arquivo: path.join(dir, f), bruto, dados: lerFrontmatter(semBom(bruto)) ?? {} };
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** O `id` do agente de um passo: o último segmento do que está em `agent:` (regra 5). */
|
|
104
|
+
export const idDoAgente = (passo) => String(passo.dados.agent ?? '').split(/[\\/]/).at(-1);
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A crew como está no disco.
|
|
108
|
+
* @param {string} raiz pasta do projeto · @param {string} crew `crews/<nome>`, como veio em `--crew`
|
|
109
|
+
*/
|
|
110
|
+
export function lerCrew(raiz, crew) {
|
|
111
|
+
const pasta = path.resolve(raiz, crew);
|
|
112
|
+
const arquivo = (...partes) => path.join(pasta, ...partes);
|
|
113
|
+
const doPipeline = entradasDoPipeline(lerBruto(arquivo('pipeline', 'pipeline.yaml')));
|
|
114
|
+
const entradas = doPipeline.length ? doPipeline : entradasDaPasta(pasta);
|
|
115
|
+
return {
|
|
116
|
+
raiz,
|
|
117
|
+
pasta,
|
|
118
|
+
nome: path.basename(pasta),
|
|
119
|
+
yaml: lerBruto(arquivo('crew.yaml')),
|
|
120
|
+
passos: entradas.map((e, i) => passoDe(pasta, e, i)),
|
|
121
|
+
agentes: agentesDe(pasta),
|
|
122
|
+
csv: lerBruto(arquivo('crew-party.csv')),
|
|
123
|
+
memoria: lerBruto(arquivo('_memory', 'memories.md')),
|
|
124
|
+
arquivos: { yaml: arquivo('crew.yaml'), csv: arquivo('crew-party.csv'), memoria: arquivo('_memory', 'memories.md') },
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// As edições do conserto, como funções de texto: cada uma muda só a linha que o item pede e
|
|
2
|
+
// devolve o resto igual, byte a byte (fim de linha, BOM, comentários e ordem das chaves).
|
|
3
|
+
// Spec: fase-u4a-conserto-de-crews.md, regras 3 e 4 (repositório do OpenCrew).
|
|
4
|
+
|
|
5
|
+
import { lerFrontmatter, semBom } from '../verificar/leitura.mjs';
|
|
6
|
+
|
|
7
|
+
const BOM = String.fromCharCode(0xfeff);
|
|
8
|
+
const emLinhas = (texto) => texto.split(/(?<=\n)/);
|
|
9
|
+
const fimDe = (linha) => linha.match(/\r?\n$/)?.[0] ?? '';
|
|
10
|
+
const eolDe = (texto) => (texto.includes('\r\n') ? '\r\n' : '\n');
|
|
11
|
+
const ehCerca = (linha) => /^\uFEFF?---\s*$/.test(linha);
|
|
12
|
+
|
|
13
|
+
const semAspas = (v) => v.replace(/\r?\n$/, '').replace(/\s+#.*$/, '').trim().replace(/^(["'])(.*)\1$/, '$2');
|
|
14
|
+
|
|
15
|
+
/** A linha da chave tem o valor nela mesma? ("não" quando ele continua nas linhas de baixo) */
|
|
16
|
+
function valorNaLinha(linhas, i, fim) {
|
|
17
|
+
const valor = semAspas(linhas[i].slice(linhas[i].indexOf(':') + 1));
|
|
18
|
+
const continua = i + 1 < fim && /^(?:\s+\S|-\s)/.test(linhas[i + 1]);
|
|
19
|
+
return valor !== '' && !/^[>|][+-]?$/.test(valor) && !continua;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* O frontmatter do arquivo com `chave: valor`. A linha da chave é trocada no lugar; se não existe,
|
|
24
|
+
* entra depois da linha `depoisDe:` ou, na falta dela, no fim do frontmatter. Valor que já é o
|
|
25
|
+
* pedido (com aspas ou comentário): o texto volta igual.
|
|
26
|
+
* @returns {string|null} null quando o arquivo não tem frontmatter, ou quando o valor da chave
|
|
27
|
+
* ocupa mais de uma linha (o script não reescreve um bloco)
|
|
28
|
+
*/
|
|
29
|
+
export function comChave(texto, chave, valor, depoisDe) {
|
|
30
|
+
const linhas = emLinhas(texto);
|
|
31
|
+
const fim = linhas.findIndex((l, i) => i > 0 && ehCerca(l));
|
|
32
|
+
if (!linhas.length || !ehCerca(linhas[0]) || fim < 0 || !lerFrontmatter(semBom(texto))) return null;
|
|
33
|
+
const eol = fimDe(linhas[0]) || eolDe(texto);
|
|
34
|
+
const acha = (nome) => linhas.findIndex((l, i) => i > 0 && i < fim && new RegExp(`^${nome}\\s*:`).test(l));
|
|
35
|
+
const nova = `${chave}: ${valor}${eol}`;
|
|
36
|
+
const atual = acha(chave);
|
|
37
|
+
if (atual < 0) linhas.splice(depoisDe && acha(depoisDe) >= 0 ? acha(depoisDe) + 1 : fim, 0, nova);
|
|
38
|
+
else if (!valorNaLinha(linhas, atual, fim)) return null;
|
|
39
|
+
else if (semAspas(linhas[atual].slice(linhas[atual].indexOf(':') + 1)) === semAspas(valor)) return texto;
|
|
40
|
+
else linhas[atual] = nova;
|
|
41
|
+
return linhas.join('');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** O arquivo com `# titulo` na primeira linha de título de nível 1 depois do frontmatter; sem ela, o texto igual. */
|
|
45
|
+
export function comTitulo(texto, titulo) {
|
|
46
|
+
const linhas = emLinhas(texto);
|
|
47
|
+
const fim = ehCerca(linhas[0] ?? '') ? linhas.findIndex((l, i) => i > 0 && ehCerca(l)) : -1;
|
|
48
|
+
const i = linhas.findIndex((l, n) => n > fim && /^# /.test(l));
|
|
49
|
+
if (i >= 0) linhas[i] = `# ${titulo}${fimDe(linhas[i])}`;
|
|
50
|
+
return linhas.join('');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Valor de YAML: como está quando é simples; entre aspas quando tem `: `, `#` ou aspas. */
|
|
54
|
+
const escrito = (valor) => (/^[^\s"'#[\]{}&*!|>%@`-][^"#\r\n]*$/u.test(valor) && !/:\s|\s$/.test(valor) ? valor : JSON.stringify(valor));
|
|
55
|
+
const normal = (c) => c.replace(/\\/g, '/').replace(/^(?:\.\/)+/, '').replace(/\/+$/, '');
|
|
56
|
+
const mesmoCaminho = (a, b) => normal(a) === normal(b);
|
|
57
|
+
const CAMINHO = /^[ \t]*(?:-[ \t]*)?caminho:[ \t]*(.*)$/;
|
|
58
|
+
|
|
59
|
+
/** Onde começa e acaba a lista de `fontes:` (a linha da chave e a última linha do bloco), ou null. */
|
|
60
|
+
function blocoDeFontes(linhas) {
|
|
61
|
+
const inicio = linhas.findIndex((l) => /^fontes\s*:/.test(l.replace(BOM, '')));
|
|
62
|
+
if (inicio < 0) return null;
|
|
63
|
+
let fim = inicio;
|
|
64
|
+
for (let i = inicio + 1; i < linhas.length && (!linhas[i].trim() || /^(?:\s|-\s)/.test(linhas[i])); i++) {
|
|
65
|
+
if (linhas[i].trim()) fim = i;
|
|
66
|
+
}
|
|
67
|
+
return { inicio, fim };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* O `crew.yaml` com mais uma fonte, no formato que o Build grava. Fonte que já está: texto igual.
|
|
72
|
+
* @returns {string|null} null quando `fontes:` está escrita numa linha só (`fontes: [{…}]`): o
|
|
73
|
+
* script não reescreve essa forma
|
|
74
|
+
*/
|
|
75
|
+
export function comFonte(texto, caminho, paraQue) {
|
|
76
|
+
const eol = eolDe(texto);
|
|
77
|
+
const linhas = emLinhas(texto);
|
|
78
|
+
const bloco = blocoDeFontes(linhas);
|
|
79
|
+
const naLinha = bloco ? semAspas(linhas[bloco.inicio].slice(linhas[bloco.inicio].indexOf(':') + 1)) : '';
|
|
80
|
+
if (naLinha && naLinha !== '[]') return null;
|
|
81
|
+
const daLista = bloco ? linhas.slice(bloco.inicio + 1, bloco.fim + 1).map((l) => l.replace(/\r?\n$/, '')) : [];
|
|
82
|
+
if (daLista.some((l) => CAMINHO.test(l) && mesmoCaminho(semAspas(l.match(CAMINHO)[1]), caminho))) return texto;
|
|
83
|
+
const recuo = daLista.find((l) => /^\s*-\s/.test(l))?.match(/^\s*/)[0] ?? ' ';
|
|
84
|
+
const item = [`${recuo}- caminho: ${escrito(caminho)}${eol}`, `${recuo} para_que: ${escrito(paraQue)}${eol}`];
|
|
85
|
+
if (!bloco) return `${texto}${texto && !fimDe(texto) ? eol : ''}${texto.trim() ? eol : ''}fontes:${eol}${item.join('')}`;
|
|
86
|
+
if (naLinha === '[]') linhas[bloco.inicio] = `fontes:${fimDe(linhas[bloco.inicio]) || eol}`;
|
|
87
|
+
if (!fimDe(linhas[bloco.fim])) linhas[bloco.fim] += eol;
|
|
88
|
+
linhas.splice(bloco.fim + 1, 0, ...item);
|
|
89
|
+
return linhas.join('');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A linha `indice` com `sufixo` no fim, antes do fim de linha. */
|
|
93
|
+
export function comSufixo(texto, indice, sufixo) {
|
|
94
|
+
const linhas = emLinhas(texto);
|
|
95
|
+
const fim = fimDe(linhas[indice]);
|
|
96
|
+
linhas[indice] = `${linhas[indice].slice(0, linhas[indice].length - fim.length).trimEnd()}${sufixo}${fim}`;
|
|
97
|
+
return linhas.join('');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Os campos de uma linha de CSV: aspas duplas protegem vírgula e espaço; `""` é uma aspa. */
|
|
101
|
+
function campos(linha) {
|
|
102
|
+
const saida = [];
|
|
103
|
+
const rx = /(?:^|,)(?:"((?:[^"]|"")*)"|([^,]*))/g;
|
|
104
|
+
for (const m of linha.matchAll(rx)) saida.push(m[1] !== undefined ? m[1].replace(/""/g, '"') : m[2].trim());
|
|
105
|
+
return saida;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** O manifesto lido: as colunas e uma linha por agente (`{ coluna: valor }`). */
|
|
109
|
+
export function lerCsv(texto) {
|
|
110
|
+
const linhas = (texto ?? '').replace(/^\uFEFF/, '').split(/\r?\n/).filter((l) => l.trim());
|
|
111
|
+
const colunas = linhas.length ? campos(linhas[0]) : [];
|
|
112
|
+
return { colunas, linhas: linhas.slice(1).map((l) => Object.fromEntries(campos(l).map((v, i) => [colunas[i] ?? `c${i}`, v]))) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const COLUNAS = ['id', 'displayName', 'title', 'icon', 'path', 'execution'];
|
|
116
|
+
const campo = (v) => (/[\s,"]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* O manifesto refeito a partir dos agentes: as 6 colunas, na ordem do manifesto antigo (agente novo
|
|
120
|
+
* vai para o fim); `execution` vem do agente, depois do manifesto antigo, depois `inline`.
|
|
121
|
+
*/
|
|
122
|
+
export function montarCsv(agentes, antigo) {
|
|
123
|
+
const velho = lerCsv(antigo);
|
|
124
|
+
const ordem = (a) => { const i = velho.linhas.findIndex((l) => l.id === a.id); return i < 0 ? Infinity : i; };
|
|
125
|
+
const emOrdem = [...agentes].sort((a, b) => ordem(a) - ordem(b));
|
|
126
|
+
const eol = antigo ? eolDe(antigo) : '\n';
|
|
127
|
+
const linha = (a) => {
|
|
128
|
+
const antes = velho.linhas.find((l) => l.id === a.id) ?? {};
|
|
129
|
+
const d = a.dados;
|
|
130
|
+
return [a.id, d.name ?? '', d.title ?? antes.title ?? '', d.icon ?? antes.icon ?? '', `./agents/${a.id}.agent.md`, d.execution ?? antes.execution ?? 'inline'].map((v) => campo(String(v))).join(',');
|
|
131
|
+
};
|
|
132
|
+
return [COLUNAS.join(','), ...emOrdem.map(linha), ''].join(eol);
|
|
133
|
+
}
|