@fontdata/ai-harness 3.3.1 → 4.0.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +22 -2
- package/dist/cli.js +6 -0
- package/dist/doctor/checks.js +12 -0
- package/dist/update/update.js +3 -1
- package/dist/versao/command.js +424 -0
- package/dist/versao/config.js +325 -0
- package/dist/versao/detectar.js +332 -0
- package/dist/versao/diff.js +58 -0
- package/dist/versao/extratores/comum.js +41 -0
- package/dist/versao/extratores/csproj.js +28 -0
- package/dist/versao/extratores/dproj.js +149 -0
- package/dist/versao/extratores/index.js +26 -0
- package/dist/versao/extratores/json.js +118 -0
- package/dist/versao/extratores/pubspec.js +15 -0
- package/dist/versao/extratores/pyproject.js +26 -0
- package/dist/versao/extratores/regex.js +20 -0
- package/dist/versao/extratores/versoes-txt.js +29 -0
- package/dist/versao/formato.js +67 -0
- package/dist/versao/git.js +91 -0
- package/dist/versao/texto.js +16 -0
- package/dist/versao/tipos.js +1 -0
- package/package.json +1 -1
- package/templates/catalog.json +2 -1
- package/templates/commands/publicar-release.md +93 -0
- package/templates/commands/versao.md +153 -116
- package/templates/skills/evoluir-harness/SKILL.md +3 -2
- package/templates/skills/evoluir-harness/references/versao.md +70 -0
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
{
|
|
12
12
|
"name": "fontdata",
|
|
13
13
|
"source": "./",
|
|
14
|
-
"description": "
|
|
14
|
+
"description": "20 slash commands para conduzir spec, contexto, harness, revisão e manutenção de projetos com Claude Code."
|
|
15
15
|
}
|
|
16
16
|
]
|
|
17
17
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fontdata",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"mcpServers": {
|
|
5
|
-
"banco_dados": { "command": "npx", "args": ["-y", "@fontdata/ai-harness@
|
|
5
|
+
"banco_dados": { "command": "npx", "args": ["-y", "@fontdata/ai-harness@4.0.0", "banco-dados-mcp"] }
|
|
6
6
|
},
|
|
7
7
|
"description": "Harness de engenharia com IA da Fontdata para Claude Code",
|
|
8
8
|
"author": { "name": "Fontdata" },
|
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ carregado como plugin **só daquela sessão** — nenhum arquivo do harness é e
|
|
|
32
32
|
# npm / npx
|
|
33
33
|
npx @fontdata/ai-harness harness
|
|
34
34
|
npx @fontdata/ai-harness resolver-bug "login 500 no refresh token"
|
|
35
|
-
npx @fontdata/ai-harness versao minor
|
|
35
|
+
npx @fontdata/ai-harness run versao minor
|
|
36
36
|
|
|
37
37
|
# yarn
|
|
38
38
|
yarn dlx @fontdata/ai-harness harness
|
|
@@ -41,7 +41,7 @@ yarn dlx @fontdata/ai-harness harness
|
|
|
41
41
|
pnpm dlx @fontdata/ai-harness harness
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
Durante a sessão, todos os
|
|
44
|
+
Durante a sessão, todos os 20 comandos ficam disponíveis com o prefixo do plugin — `/fontdata:nova-tarefa`,
|
|
45
45
|
`/fontdata:resolver-bug` etc. —, junto com os hooks e os agents do harness.
|
|
46
46
|
É isso que faz o `/harness`, que encadeia 4 comandos, funcionar sem instalação prévia.
|
|
47
47
|
|
|
@@ -167,8 +167,27 @@ projeto, cadastro, limites do filtro, produção, mascaramento e dúvidas freque
|
|
|
167
167
|
| `banco-dados <subcomando>` | Conexões de banco somente leitura: `adicionar`, `listar`, `testar`, `usar`, `remover` |
|
|
168
168
|
| `banco-dados-mcp` | Servidor MCP de leitura do banco, iniciado pelo Claude Code |
|
|
169
169
|
| `comentarios [caminho]` | Instala o hook do padrão de comentários com a catraca do legado (`--yes`) |
|
|
170
|
+
| `versao detectar\|calcular\|aplicar` | Fontes de versão do projeto, próxima versão e gravação nos arquivos (veja abaixo) |
|
|
170
171
|
| `list` | Lista o catálogo |
|
|
171
172
|
|
|
173
|
+
### Subcomando `versao`
|
|
174
|
+
|
|
175
|
+
`npx @fontdata/ai-harness versao detectar|calcular|aplicar` é a parte determinística do `/versao`, para qualquer
|
|
176
|
+
stack. Não faz commit, tag nem push.
|
|
177
|
+
|
|
178
|
+
- `detectar` lista as fontes da versão (arquivo, extrator, linha e valor), o modelo da tag e as divergências.
|
|
179
|
+
- `calcular` infere o próximo bump pelos commits desde a última tag (nunca major) ou aplica `major|minor|patch|build`.
|
|
180
|
+
- `aplicar <versao>` grava a versão em todas as fontes; com `--dry-run` só mostra o diff.
|
|
181
|
+
- `--json` devolve a saída para máquinas (é o que o `/versao` e o `/publicar-release` leem); `--app <nome>` escolhe
|
|
182
|
+
o app num monorepo.
|
|
183
|
+
|
|
184
|
+
Onde a versão mora, o modelo da tag (`v{versao}`, `{app}-v{versao}`…), os segmentos, os changelogs, o que nunca
|
|
185
|
+
tocar e o comando de validação ficam no `.ai-harness/versao.json`, opcional e versionado. Sem ele, a CLI detecta as
|
|
186
|
+
fontes por stack e propõe o arquivo. O formato está em [`docs/contratos/VERSAO.md`](docs/contratos/VERSAO.md).
|
|
187
|
+
|
|
188
|
+
Para abrir o fluxo `/versao` no Claude Code, use `npx @fontdata/ai-harness run versao minor`: `versao` é subcomando
|
|
189
|
+
do CLI.
|
|
190
|
+
|
|
172
191
|
## Modelo de arquivos
|
|
173
192
|
|
|
174
193
|
| Arquivo | Papel |
|
|
@@ -247,6 +266,7 @@ também acionado pelo modelo quando o pedido combina.
|
|
|
247
266
|
### Utilitário
|
|
248
267
|
- `/git-commit-push` ⭐ — gera a mensagem (PT-BR, Conventional Commits) e faz add, commit e push
|
|
249
268
|
- `/versao` ⭐ — detecta o versionamento da stack e aplica bump e changelog, com aprovação e sem push
|
|
269
|
+
- `/publicar-release` — depois do `/versao`, cria a tag (exceto com auto-tag), faz o push após confirmação e cria o GitHub Release
|
|
250
270
|
- `/revisar-pr` — revisa um PR ou o diff atual, classifica os achados e, se pedido, organiza as correções num trabalho
|
|
251
271
|
|
|
252
272
|
### Setup guiado
|
package/dist/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ const RESERVED = new Set([
|
|
|
20
20
|
'memoria',
|
|
21
21
|
'banco-dados',
|
|
22
22
|
'banco-dados-mcp',
|
|
23
|
+
'versao',
|
|
23
24
|
'list',
|
|
24
25
|
'run',
|
|
25
26
|
'help',
|
|
@@ -66,6 +67,7 @@ Gerenciar:
|
|
|
66
67
|
--antes-de <dias> muda o corte; --remover apaga do disco os que o git recupera
|
|
67
68
|
banco-dados <subcomando> Conexões de banco somente leitura: adicionar, listar, testar, usar, remover
|
|
68
69
|
banco-dados-mcp Servidor MCP de leitura do banco, usado pelo Claude Code (stdio)
|
|
70
|
+
versao detectar|calcular|aplicar Fontes de versão, próxima versão e gravação (--app, --json; aplicar --dry-run)
|
|
69
71
|
list Lista todos os comandos disponíveis no catálogo
|
|
70
72
|
|
|
71
73
|
Init sem perguntas (CI):
|
|
@@ -177,6 +179,10 @@ async function main() {
|
|
|
177
179
|
const { runUpdateCommand } = await import('./update/update.js');
|
|
178
180
|
process.exit(runUpdateCommand(argv.slice(1)));
|
|
179
181
|
}
|
|
182
|
+
if (cmd === 'versao') {
|
|
183
|
+
const { runVersaoCommand } = await import('./versao/command.js');
|
|
184
|
+
process.exit(runVersaoCommand(argv.slice(1)));
|
|
185
|
+
}
|
|
180
186
|
if (cmd === 'run') {
|
|
181
187
|
const id = argv[1];
|
|
182
188
|
if (!id) {
|
package/dist/doctor/checks.js
CHANGED
|
@@ -9,6 +9,7 @@ import { CONFIG_PATH, readConfig } from '../config/config.js';
|
|
|
9
9
|
import { ARCHIVE_DIR_NAME, ARTIFACT_TYPES, listWorkFiles, readArtifact, validateAllWorks, WORKS_DIR } from '../contratos/artefatos.js';
|
|
10
10
|
import { validateEventLog } from '../contratos/eventos.js';
|
|
11
11
|
import { hashContent, LOCK_PATH, LOCK_VERSION } from '../install/lock.js';
|
|
12
|
+
import { itemSources } from '../install/plan.js';
|
|
12
13
|
import { SETTINGS_PATH } from '../install/settings.js';
|
|
13
14
|
import { planArchive } from '../trabalhos/arquivar.js';
|
|
14
15
|
import { ARCHIVE_DEFAULT_DAYS } from '../trabalhos/command.js';
|
|
@@ -102,7 +103,9 @@ function checkLock(root) {
|
|
|
102
103
|
}
|
|
103
104
|
function checkFiles(root, lock) {
|
|
104
105
|
const findings = [];
|
|
106
|
+
const catalog = loadCatalog();
|
|
105
107
|
for (const item of lock.itens) {
|
|
108
|
+
const sourceByPath = new Map((itemSources(catalog, item.tipo, item.id) ?? []).map((entry) => [entry.path, entry.source]));
|
|
106
109
|
for (const entry of item.arquivos) {
|
|
107
110
|
const file = join(root, entry.caminho);
|
|
108
111
|
if (!existsSync(file)) {
|
|
@@ -120,6 +123,15 @@ function checkFiles(root, lock) {
|
|
|
120
123
|
mensagem: `Arquivo alterado localmente desde a instalação: ${entry.caminho}.`,
|
|
121
124
|
correcao: 'Se a alteração é intencional, mantenha: atualizações não sobrescrevem arquivos alterados sem confirmação.',
|
|
122
125
|
});
|
|
126
|
+
const source = sourceByPath.get(entry.caminho);
|
|
127
|
+
if (source && existsSync(source) && hashContent(readFileSync(source)) !== entry.sha256) {
|
|
128
|
+
findings.push({
|
|
129
|
+
id: 'comando-customizado-defasado',
|
|
130
|
+
severidade: 'aviso',
|
|
131
|
+
mensagem: `Arquivo customizado preso numa versão antiga do template, que mudou desde a instalação: ${entry.caminho}.`,
|
|
132
|
+
correcao: `Compare ${entry.caminho} com o template atual do pacote; se não houver customização a manter, rode \`npx @fontdata/ai-harness update --forcar\`.`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
123
135
|
}
|
|
124
136
|
}
|
|
125
137
|
}
|
package/dist/update/update.js
CHANGED
|
@@ -103,7 +103,9 @@ export function checkUpdate(root, catalog = loadCatalog(), legacy, home, { semBa
|
|
|
103
103
|
for (const { path, source } of sources) {
|
|
104
104
|
if (trackedPaths.has(path))
|
|
105
105
|
continue;
|
|
106
|
-
|
|
106
|
+
const target = join(root, path);
|
|
107
|
+
const same = existsSync(target) && hashContent(readFileSync(target)) === hashContent(readFileSync(source));
|
|
108
|
+
changes.push({ tipo: !existsSync(target) || same ? 'novo' : 'conflito', caminho: path, item: key, source });
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
111
|
// Itens que o lock não tem. Arquivo igual ao do pacote só entra no lock; `protect` mantém o do projeto mesmo com --forcar.
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { EOL } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { suggestFlag } from '../install/flags.js';
|
|
5
|
+
import { acharRaiz } from '../raiz.js';
|
|
6
|
+
import { carregarVersaoConfig } from './config.js';
|
|
7
|
+
import { detectarFontes, extratorParaTexto } from './detectar.js';
|
|
8
|
+
import { diffLinhas } from './diff.js';
|
|
9
|
+
import { criarExtrator } from './extratores/index.js';
|
|
10
|
+
import { formatarVersao, incrementar, parseVersao } from './formato.js';
|
|
11
|
+
import { acharTagBase, classificarBump, listarCommits } from './git.js';
|
|
12
|
+
const SUBCOMANDOS = ['detectar', 'calcular', 'aplicar'];
|
|
13
|
+
const INCREMENTOS = ['major', 'minor', 'patch', 'build'];
|
|
14
|
+
const STATUS_IGNORADA = 'ignorada (naoTocar)';
|
|
15
|
+
const STATUS_CHANGELOG = 'changelog, gravado pelo /versao';
|
|
16
|
+
function parseOpcoes(sub, argv) {
|
|
17
|
+
const booleanas = sub === 'aplicar' ? ['--json', '--dry-run'] : ['--json'];
|
|
18
|
+
const opcoes = { json: false, dryRun: false, posicionais: [] };
|
|
19
|
+
for (let i = 0; i < argv.length; i++) {
|
|
20
|
+
const token = argv[i];
|
|
21
|
+
if (token === '--app') {
|
|
22
|
+
const valor = argv[++i];
|
|
23
|
+
if (valor === undefined || valor.startsWith('--'))
|
|
24
|
+
throw new Error('--app precisa do nome do app.');
|
|
25
|
+
opcoes.app = valor;
|
|
26
|
+
}
|
|
27
|
+
else if (booleanas.includes(token)) {
|
|
28
|
+
if (token === '--json')
|
|
29
|
+
opcoes.json = true;
|
|
30
|
+
else
|
|
31
|
+
opcoes.dryRun = true;
|
|
32
|
+
}
|
|
33
|
+
else if (token.startsWith('--')) {
|
|
34
|
+
const sugestao = suggestFlag(token, ['--app', ...booleanas]);
|
|
35
|
+
throw new Error(`Flag desconhecida: ${token}.${sugestao ? ` Você quis dizer ${sugestao}?` : ''}`);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
opcoes.posicionais.push(token);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const maximo = sub === 'detectar' ? 0 : 1;
|
|
42
|
+
if (opcoes.posicionais.length > maximo)
|
|
43
|
+
throw new Error(`Argumento inesperado: ${opcoes.posicionais[maximo]}`);
|
|
44
|
+
if (sub === 'aplicar' && opcoes.posicionais.length === 0)
|
|
45
|
+
throw new Error('Informe a versão: versao aplicar <versao> [--dry-run].');
|
|
46
|
+
return opcoes;
|
|
47
|
+
}
|
|
48
|
+
function normalizarCaminho(caminho) {
|
|
49
|
+
return caminho.replaceAll('\\', '/').replace(/^\.\//, '').replace(/\/+$/, '');
|
|
50
|
+
}
|
|
51
|
+
/** Entrada de `naoTocar` que é caminho bloqueia o arquivo; nome de campo bloqueia o `json:` que termina nele. */
|
|
52
|
+
function bloqueada(fonte, naoTocar) {
|
|
53
|
+
const caminho = normalizarCaminho(fonte.caminho);
|
|
54
|
+
return naoTocar.some((entrada) => {
|
|
55
|
+
if (normalizarCaminho(entrada) === caminho)
|
|
56
|
+
return true;
|
|
57
|
+
if (fonte.extrator.tipo !== 'json')
|
|
58
|
+
return false;
|
|
59
|
+
const campo = entrada.split('.');
|
|
60
|
+
return campo.length <= fonte.extrator.caminho.length && fonte.extrator.caminho.slice(-campo.length).join('\u0000') === campo.join('\u0000');
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function fonteJson(fonte, naoTocar) {
|
|
64
|
+
return {
|
|
65
|
+
caminho: fonte.caminho,
|
|
66
|
+
extrator: extratorParaTexto(fonte.extrator),
|
|
67
|
+
formato: fonte.formato ?? null,
|
|
68
|
+
valores: [...new Set(fonte.ocorrencias.map((o) => o.valor))],
|
|
69
|
+
ocorrencias: fonte.ocorrencias.map((o) => ({ linha: o.linha, valor: o.valor, grupo: o.grupo ?? null })),
|
|
70
|
+
somenteLeitura: fonte.extrator.tipo === 'versoes-txt',
|
|
71
|
+
naoTocar: bloqueada(fonte, naoTocar),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function mensagemSemFonte(d) {
|
|
75
|
+
const apps = Object.keys(d.apps);
|
|
76
|
+
if (d.app === null && apps.length > 0) {
|
|
77
|
+
return `nenhuma fonte de versão na raiz; informe --app <nome> (apps: ${apps.join(', ')})`;
|
|
78
|
+
}
|
|
79
|
+
return d.origem === 'config'
|
|
80
|
+
? 'nenhuma fonte de versão no .ai-harness/versao.json; declare os arquivos em "arquivos" (ou nos apps)'
|
|
81
|
+
: 'nenhuma fonte de versão detectada; crie o .ai-harness/versao.json declarando os arquivos (docs/contratos/VERSAO.md)';
|
|
82
|
+
}
|
|
83
|
+
function exigirFontes(d) {
|
|
84
|
+
if (d.fontes.length === 0)
|
|
85
|
+
throw new Error(mensagemSemFonte(d));
|
|
86
|
+
}
|
|
87
|
+
function tentarParse(texto, formato) {
|
|
88
|
+
try {
|
|
89
|
+
return parseVersao(texto, formato);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function segmentosDe(texto, formato) {
|
|
96
|
+
const comFormato = formato ? tentarParse(texto, formato) : null;
|
|
97
|
+
if (comFormato)
|
|
98
|
+
return comFormato;
|
|
99
|
+
if (/^\d+(?:\.\d+)*$/.test(texto))
|
|
100
|
+
return parseVersao(texto);
|
|
101
|
+
throw new Error(`versão inválida: "${texto}"; use números separados por ponto${formato ? ` ou o formato ${formato}` : ''}`);
|
|
102
|
+
}
|
|
103
|
+
function comparar(a, b) {
|
|
104
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
105
|
+
const d = (a[i] ?? 0) - (b[i] ?? 0);
|
|
106
|
+
if (d !== 0)
|
|
107
|
+
return d;
|
|
108
|
+
}
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
function tentarBase(raiz, d) {
|
|
112
|
+
if (d.tag.includes('{app}') && d.app === null)
|
|
113
|
+
return null;
|
|
114
|
+
try {
|
|
115
|
+
return acharTagBase(raiz, d.tag, d.app ?? undefined);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function baseJson(base) {
|
|
122
|
+
return base ? { tag: base.tag, versao: base.versao, ref: base.base, primeiroRelease: base.primeiroRelease } : null;
|
|
123
|
+
}
|
|
124
|
+
function montarTag(modelo, versao, app) {
|
|
125
|
+
if (versao === null || (modelo.includes('{app}') && app === null))
|
|
126
|
+
return null;
|
|
127
|
+
return modelo.replaceAll('{versao}', versao).replaceAll('{app}', app ?? '');
|
|
128
|
+
}
|
|
129
|
+
function detectar({ raiz, deteccao: d, config }) {
|
|
130
|
+
const naoTocar = config?.naoTocar ?? [];
|
|
131
|
+
const problemas = [...d.problemas];
|
|
132
|
+
if (d.fontes.length === 0)
|
|
133
|
+
problemas.unshift(mensagemSemFonte(d));
|
|
134
|
+
return {
|
|
135
|
+
ok: true,
|
|
136
|
+
comando: 'detectar',
|
|
137
|
+
raiz,
|
|
138
|
+
origem: d.origem,
|
|
139
|
+
app: d.app,
|
|
140
|
+
versaoAtual: d.versaoAtual,
|
|
141
|
+
tag: d.tag,
|
|
142
|
+
base: baseJson(tentarBase(raiz, d)),
|
|
143
|
+
segmentos: d.segmentos,
|
|
144
|
+
formato: d.formato,
|
|
145
|
+
fontes: d.fontes.map((f) => fonteJson(f, naoTocar)),
|
|
146
|
+
divergencias: d.divergencias,
|
|
147
|
+
apps: Object.fromEntries(Object.entries(d.apps).map(([nome, a]) => [
|
|
148
|
+
nome,
|
|
149
|
+
{ caminhos: a.caminhos, versaoAtual: a.versaoAtual, tag: a.tag, fontes: a.fontes.map((f) => fonteJson(f, naoTocar)), divergencias: a.divergencias },
|
|
150
|
+
])),
|
|
151
|
+
naoTocar,
|
|
152
|
+
problemas,
|
|
153
|
+
versaoJson: d.versaoJson,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function calcular({ raiz, deteccao: d, config, opcoes }) {
|
|
157
|
+
exigirFontes(d);
|
|
158
|
+
if (d.versaoAtual === null) {
|
|
159
|
+
throw new Error(`versão atual não encontrada nas fontes${d.problemas.length ? `:${EOL}- ${d.problemas.join(`${EOL}- `)}` : ''}`);
|
|
160
|
+
}
|
|
161
|
+
const formato = d.formato ?? undefined;
|
|
162
|
+
const atual = segmentosDe(d.versaoAtual, formato);
|
|
163
|
+
const caminhos = d.app !== null ? (d.apps[d.app]?.caminhos ?? null) : null;
|
|
164
|
+
const avisos = [];
|
|
165
|
+
let base = null;
|
|
166
|
+
let commits = [];
|
|
167
|
+
let erroGit = null;
|
|
168
|
+
try {
|
|
169
|
+
base = acharTagBase(raiz, d.tag, d.app ?? undefined);
|
|
170
|
+
commits = listarCommits(raiz, base.base, caminhos ?? undefined, base.primeiroRelease);
|
|
171
|
+
}
|
|
172
|
+
catch (erro) {
|
|
173
|
+
erroGit = erro.message;
|
|
174
|
+
}
|
|
175
|
+
const argumento = opcoes.posicionais[0];
|
|
176
|
+
let bump = null;
|
|
177
|
+
let proximos = null;
|
|
178
|
+
if (argumento !== undefined) {
|
|
179
|
+
if (INCREMENTOS.includes(argumento))
|
|
180
|
+
bump = { tipo: argumento, origem: 'argumento' };
|
|
181
|
+
else {
|
|
182
|
+
proximos = segmentosDe(argumento, formato);
|
|
183
|
+
bump = { tipo: 'versao', origem: 'argumento' };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else if (config && config.bump.padrao !== 'auto') {
|
|
187
|
+
bump = { tipo: config.bump.padrao, origem: 'config' };
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
if (erroGit !== null)
|
|
191
|
+
throw new Error(`não foi possível ler os commits para inferir o bump: ${erroGit}`);
|
|
192
|
+
const inferido = classificarBump(commits);
|
|
193
|
+
avisos.push(...inferido.avisos);
|
|
194
|
+
if (inferido.tipo === 'nenhum') {
|
|
195
|
+
const desde = base?.tag ?? 'o primeiro commit';
|
|
196
|
+
avisos.push(`nenhum commit com tipo semântico (feat, fix, chore...) desde ${desde}${caminhos ? ` em ${caminhos.join(', ')}` : ''}; ` +
|
|
197
|
+
'nada a versionar. Informe o bump (major|minor|patch|build) ou a versão.');
|
|
198
|
+
}
|
|
199
|
+
else
|
|
200
|
+
bump = { tipo: inferido.tipo, origem: 'inferido' };
|
|
201
|
+
}
|
|
202
|
+
if (erroGit !== null)
|
|
203
|
+
avisos.push(`commits não lidos: ${erroGit}`);
|
|
204
|
+
if (bump && bump.tipo !== 'versao') {
|
|
205
|
+
proximos = incrementar(atual, bump.tipo);
|
|
206
|
+
while (d.segmentos !== null && proximos.length < d.segmentos)
|
|
207
|
+
proximos.push(0);
|
|
208
|
+
}
|
|
209
|
+
let proxima = null;
|
|
210
|
+
if (proximos) {
|
|
211
|
+
proxima = formato ? formatarVersao(proximos, formato) : bump?.tipo === 'versao' ? argumento : formatarVersao(proximos);
|
|
212
|
+
if (comparar(proximos, atual) <= 0)
|
|
213
|
+
avisos.push(`a próxima versão ${proxima} não é maior que a atual ${d.versaoAtual}`);
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
ok: true,
|
|
217
|
+
comando: 'calcular',
|
|
218
|
+
raiz,
|
|
219
|
+
app: d.app,
|
|
220
|
+
versaoAtual: d.versaoAtual,
|
|
221
|
+
proxima,
|
|
222
|
+
tag: d.tag,
|
|
223
|
+
tagProxima: montarTag(d.tag, proxima, d.app),
|
|
224
|
+
bump,
|
|
225
|
+
base: baseJson(base),
|
|
226
|
+
caminhos,
|
|
227
|
+
commits,
|
|
228
|
+
divergencias: d.divergencias,
|
|
229
|
+
avisos,
|
|
230
|
+
problemas: d.problemas,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function textoParaFonte(fonte, segmentos, versao, formatoRaiz) {
|
|
234
|
+
if (fonte.extrator.tipo === 'dproj')
|
|
235
|
+
return formatarVersao(segmentos);
|
|
236
|
+
const formato = fonte.formato ?? fonte.padding?.map((p) => `{${p}}`).join('.') ?? formatoRaiz ?? undefined;
|
|
237
|
+
return formato ? formatarVersao(segmentos, formato) : versao;
|
|
238
|
+
}
|
|
239
|
+
function statusDoArquivo(fontes, mudou) {
|
|
240
|
+
if (mudou)
|
|
241
|
+
return 'alterado';
|
|
242
|
+
if (fontes.every((f) => f.status === STATUS_IGNORADA))
|
|
243
|
+
return 'ignorado (naoTocar)';
|
|
244
|
+
if (fontes.every((f) => f.status === STATUS_CHANGELOG || f.status === STATUS_IGNORADA))
|
|
245
|
+
return STATUS_CHANGELOG;
|
|
246
|
+
return 'sem alteração';
|
|
247
|
+
}
|
|
248
|
+
function aplicar({ raiz, deteccao: d, config, opcoes }) {
|
|
249
|
+
exigirFontes(d);
|
|
250
|
+
const versao = opcoes.posicionais[0];
|
|
251
|
+
const segmentos = segmentosDe(versao, d.formato ?? undefined);
|
|
252
|
+
const naoTocar = config?.naoTocar ?? [];
|
|
253
|
+
const porArquivo = new Map();
|
|
254
|
+
for (const f of d.fontes)
|
|
255
|
+
porArquivo.set(f.caminho, [...(porArquivo.get(f.caminho) ?? []), f]);
|
|
256
|
+
const erros = [];
|
|
257
|
+
const planos = [];
|
|
258
|
+
for (const [caminho, fontes] of porArquivo) {
|
|
259
|
+
const absoluto = join(raiz, caminho);
|
|
260
|
+
const original = existsSync(absoluto) ? readFileSync(absoluto) : null;
|
|
261
|
+
let atual = original;
|
|
262
|
+
const aplicadas = [];
|
|
263
|
+
for (const fonte of fontes) {
|
|
264
|
+
const extrator = extratorParaTexto(fonte.extrator);
|
|
265
|
+
if (bloqueada(fonte, naoTocar)) {
|
|
266
|
+
aplicadas.push({ extrator, status: STATUS_IGNORADA });
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (fonte.extrator.tipo === 'versoes-txt') {
|
|
270
|
+
aplicadas.push({ extrator, status: STATUS_CHANGELOG });
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
if (atual === null)
|
|
275
|
+
throw new Error('arquivo não encontrado');
|
|
276
|
+
const texto = textoParaFonte(fonte, segmentos, versao, d.formato);
|
|
277
|
+
const novo = criarExtrator(fonte.extrator).gravar(caminho, atual, texto);
|
|
278
|
+
aplicadas.push({ extrator, status: novo.equals(atual) ? 'sem alteração' : 'alterada', versao: texto });
|
|
279
|
+
atual = novo;
|
|
280
|
+
}
|
|
281
|
+
catch (erro) {
|
|
282
|
+
const mensagem = erro.message;
|
|
283
|
+
erros.push(`${caminho} (${extrator}): ${mensagem}`);
|
|
284
|
+
aplicadas.push({ extrator, status: 'erro', mensagem });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const mudou = original !== null && atual !== null && !atual.equals(original);
|
|
288
|
+
planos.push({
|
|
289
|
+
arquivo: { caminho, status: statusDoArquivo(aplicadas, mudou), gravado: false, fontes: aplicadas, diff: mudou ? diffLinhas(original, atual) : [] },
|
|
290
|
+
novo: mudou ? atual : null,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (erros.length > 0)
|
|
294
|
+
throw new Error(`nada foi gravado:${EOL}- ${erros.join(`${EOL}- `)}`);
|
|
295
|
+
if (!opcoes.dryRun) {
|
|
296
|
+
for (const plano of planos) {
|
|
297
|
+
if (!plano.novo)
|
|
298
|
+
continue;
|
|
299
|
+
writeFileSync(join(raiz, plano.arquivo.caminho), plano.novo);
|
|
300
|
+
plano.arquivo.gravado = true;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
ok: true,
|
|
305
|
+
comando: 'aplicar',
|
|
306
|
+
raiz,
|
|
307
|
+
app: d.app,
|
|
308
|
+
versao,
|
|
309
|
+
dryRun: opcoes.dryRun,
|
|
310
|
+
arquivos: planos.map((p) => p.arquivo),
|
|
311
|
+
divergencias: d.divergencias,
|
|
312
|
+
problemas: d.problemas,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function linhasDeFonte(f) {
|
|
316
|
+
const marcas = [f.somenteLeitura ? 'só leitura' : '', f.naoTocar ? 'naoTocar' : ''].filter(Boolean);
|
|
317
|
+
return ` ${f.caminho} (${f.extrator}): ${f.valores.join(', ') || '—'}${marcas.length ? ` [${marcas.join(', ')}]` : ''}`;
|
|
318
|
+
}
|
|
319
|
+
function linhasDeLista(titulo, itens) {
|
|
320
|
+
return itens.length ? [`${titulo}:`, ...itens.map((i) => ` - ${i}`)] : [];
|
|
321
|
+
}
|
|
322
|
+
function divergenciasTexto(divergencias) {
|
|
323
|
+
return linhasDeLista('Divergências', divergencias.map((d) => d.mensagem));
|
|
324
|
+
}
|
|
325
|
+
function textoDetectar(s) {
|
|
326
|
+
const base = s.base ? ` (base: ${s.base.tag ?? 'nenhuma tag; primeiro release'})` : '';
|
|
327
|
+
const linhas = [
|
|
328
|
+
`Raiz: ${s.raiz}`,
|
|
329
|
+
`Origem: ${s.origem === 'config' ? '.ai-harness/versao.json' : 'detecção por stack (sem .ai-harness/versao.json)'}`,
|
|
330
|
+
...(s.app ? [`App: ${s.app}`] : []),
|
|
331
|
+
`Versão atual: ${s.versaoAtual ?? '—'}`,
|
|
332
|
+
`Tag: ${s.tag}${base}`,
|
|
333
|
+
];
|
|
334
|
+
if (s.fontes.length)
|
|
335
|
+
linhas.push('Fontes:', ...s.fontes.map(linhasDeFonte));
|
|
336
|
+
for (const [nome, a] of Object.entries(s.apps)) {
|
|
337
|
+
linhas.push(`App ${nome} (${a.caminhos.join(', ')}): versão ${a.versaoAtual ?? '—'}, tag ${a.tag}`, ...a.fontes.map((f) => ` ${linhasDeFonte(f)}`));
|
|
338
|
+
}
|
|
339
|
+
linhas.push(...divergenciasTexto(s.divergencias), ...linhasDeLista('Problemas', s.problemas));
|
|
340
|
+
if (s.versaoJson)
|
|
341
|
+
linhas.push('versao.json proposto (.ai-harness/versao.json):', JSON.stringify(s.versaoJson, null, 2));
|
|
342
|
+
return linhas.join(EOL);
|
|
343
|
+
}
|
|
344
|
+
function textoCalcular(s) {
|
|
345
|
+
const bump = s.bump ? `${s.bump.tipo} (${s.bump.origem})` : 'nenhum';
|
|
346
|
+
const base = s.base ? (s.base.tag ?? `primeiro release (desde ${s.base.ref.slice(0, 7)})`) : '—';
|
|
347
|
+
return [
|
|
348
|
+
...(s.app ? [`App: ${s.app}`] : []),
|
|
349
|
+
`Versão atual: ${s.versaoAtual}`,
|
|
350
|
+
`Base: ${base}`,
|
|
351
|
+
`Commits considerados (${s.commits.length})${s.caminhos ? ` em ${s.caminhos.join(', ')}` : ''}:`,
|
|
352
|
+
...s.commits.map((c) => ` ${c.hash.slice(0, 7)} ${c.titulo}`),
|
|
353
|
+
`Bump: ${bump}`,
|
|
354
|
+
`Próxima versão: ${s.proxima ?? '— (nada a versionar)'}${s.tagProxima ? ` (tag ${s.tagProxima})` : ''}`,
|
|
355
|
+
...divergenciasTexto(s.divergencias),
|
|
356
|
+
...linhasDeLista('Avisos', s.avisos),
|
|
357
|
+
...linhasDeLista('Problemas', s.problemas),
|
|
358
|
+
].join(EOL);
|
|
359
|
+
}
|
|
360
|
+
function textoAplicar(s) {
|
|
361
|
+
const linhas = [`Versão ${s.versao}${s.dryRun ? ' (simulação: nada foi gravado)' : ''}`];
|
|
362
|
+
for (const a of s.arquivos) {
|
|
363
|
+
linhas.push(`${a.caminho}: ${a.status}${a.gravado ? ', gravado' : ''}`);
|
|
364
|
+
for (const f of a.fontes)
|
|
365
|
+
if (f.status === STATUS_IGNORADA && a.status !== 'ignorado (naoTocar)')
|
|
366
|
+
linhas.push(` ${f.extrator}: ${f.status}`);
|
|
367
|
+
for (const d of a.diff) {
|
|
368
|
+
if (d.antes !== null)
|
|
369
|
+
linhas.push(` - ${d.linha}: ${d.antes}`);
|
|
370
|
+
if (d.depois !== null)
|
|
371
|
+
linhas.push(` + ${d.linha}: ${d.depois}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
linhas.push(...divergenciasTexto(s.divergencias), ...linhasDeLista('Problemas', s.problemas));
|
|
375
|
+
return linhas.join(EOL);
|
|
376
|
+
}
|
|
377
|
+
function erroDeUso(argv) {
|
|
378
|
+
const sub = argv[0];
|
|
379
|
+
return [
|
|
380
|
+
sub === undefined ? 'Informe o subcomando do versao.' : `Subcomando desconhecido: versao ${sub}.`,
|
|
381
|
+
'Uso: versao detectar|calcular|aplicar [--app <nome>] [--json] (aplicar aceita --dry-run).',
|
|
382
|
+
`Para abrir o /versao no Claude Code, use: npx @fontdata/ai-harness run versao${argv.length ? ` ${argv.join(' ')}` : ''}`,
|
|
383
|
+
].join(EOL);
|
|
384
|
+
}
|
|
385
|
+
/** `versao detectar|calcular|aplicar`; devolve o código de saída. Não faz commit, tag nem push. */
|
|
386
|
+
export function runVersaoCommand(argv) {
|
|
387
|
+
const sub = argv[0];
|
|
388
|
+
if (!SUBCOMANDOS.includes(sub)) {
|
|
389
|
+
console.error(erroDeUso(argv));
|
|
390
|
+
return 1;
|
|
391
|
+
}
|
|
392
|
+
const subcomando = sub;
|
|
393
|
+
const json = argv.includes('--json');
|
|
394
|
+
try {
|
|
395
|
+
const opcoes = parseOpcoes(subcomando, argv.slice(1));
|
|
396
|
+
const raiz = acharRaiz();
|
|
397
|
+
const config = carregarVersaoConfig(raiz);
|
|
398
|
+
const contexto = { raiz, config, deteccao: detectarFontes(raiz, config, opcoes.app), opcoes };
|
|
399
|
+
let saida;
|
|
400
|
+
let texto;
|
|
401
|
+
if (subcomando === 'detectar') {
|
|
402
|
+
const s = detectar(contexto);
|
|
403
|
+
[saida, texto] = [s, textoDetectar(s)];
|
|
404
|
+
}
|
|
405
|
+
else if (subcomando === 'calcular') {
|
|
406
|
+
const s = calcular(contexto);
|
|
407
|
+
[saida, texto] = [s, textoCalcular(s)];
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
const s = aplicar(contexto);
|
|
411
|
+
[saida, texto] = [s, textoAplicar(s)];
|
|
412
|
+
}
|
|
413
|
+
console.log(json ? JSON.stringify(saida, null, 2) : texto);
|
|
414
|
+
return 0;
|
|
415
|
+
}
|
|
416
|
+
catch (erro) {
|
|
417
|
+
const mensagem = erro instanceof Error ? erro.message : String(erro);
|
|
418
|
+
if (json)
|
|
419
|
+
console.log(JSON.stringify({ ok: false, comando: subcomando, erro: mensagem }, null, 2));
|
|
420
|
+
else
|
|
421
|
+
console.error(mensagem);
|
|
422
|
+
return 1;
|
|
423
|
+
}
|
|
424
|
+
}
|