@fontdata/ai-harness 2.0.0 → 2.2.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 +9 -2
- package/LICENSE +21 -0
- package/README.md +175 -140
- package/{src → dist}/catalog.js +5 -12
- package/dist/cli.js +180 -0
- package/dist/comentarios/command.js +74 -0
- package/dist/comentarios/instalar.js +33 -0
- package/dist/config/config.js +99 -0
- package/dist/contratos/artefatos.js +284 -0
- package/dist/contratos/eventos.js +103 -0
- package/dist/doctor/checks.js +424 -0
- package/dist/init.js +240 -0
- package/dist/install/atomic.js +51 -0
- package/dist/install/flags.js +99 -0
- package/dist/install/hooks.js +47 -0
- package/dist/install/lock.js +39 -0
- package/dist/install/plan.js +209 -0
- package/dist/install/selection.js +77 -0
- package/dist/install/settings.js +112 -0
- package/dist/memoria/indice.js +195 -0
- package/dist/migrar/migrar.js +426 -0
- package/dist/rastro/command.js +46 -0
- package/dist/run.js +86 -0
- package/dist/trabalhos/conflitos.js +115 -0
- package/dist/types.js +1 -0
- package/dist/update/legado.js +74 -0
- package/dist/update/update.js +284 -0
- package/package.json +17 -8
- package/templates/CLAUDE.md +59 -30
- package/templates/agents/auditor-plano.md +44 -0
- package/templates/agents/investigador.md +32 -0
- package/templates/agents/qa.md +44 -0
- package/templates/agents/revisor-testes.md +40 -0
- package/templates/catalog.json +24 -61
- package/templates/commands/atualizar-deps.md +43 -9
- package/templates/commands/diagrama.md +35 -10
- package/templates/commands/gerar-env.md +39 -10
- package/templates/commands/gerar-testes.md +42 -10
- package/templates/commands/git-commit-push.md +0 -0
- package/templates/commands/onboarding.md +44 -15
- package/templates/commands/refatorar.md +50 -10
- package/templates/commands/revisar-pr.md +56 -12
- package/templates/commands/sincronizar-claude-md.md +177 -9
- package/templates/commands/versao.md +0 -0
- package/templates/hooks/comentarios/check.mjs +210 -0
- package/templates/hooks/hooks.json +34 -0
- package/templates/hooks/lembrete.mjs +29 -0
- package/templates/hooks/lib/config.mjs +40 -0
- package/templates/hooks/lib/hook.mjs +85 -0
- package/templates/hooks/lib/rastro.mjs +144 -0
- package/templates/hooks/lib/segredos.mjs +91 -0
- package/templates/hooks/lib/temas.mjs +111 -0
- package/templates/hooks/lib/trabalho.mjs +94 -0
- package/templates/hooks/proteger-contratos.mjs +80 -0
- package/templates/hooks/segredo-no-commit.mjs +76 -0
- package/templates/hooks/sem-jargao.mjs +58 -0
- package/templates/hooks/task-so-fecha-verde.mjs +33 -0
- package/templates/legado/manifesto.json +1267 -0
- package/templates/rules/comentarios.md +166 -0
- package/templates/rules/harness-projeto.md +41 -0
- package/templates/skills/auditar-performance/SKILL.md +53 -0
- package/templates/skills/auditar-performance/references/pontos-quentes.md +35 -0
- package/templates/skills/auditar-performance/templates/relatorios/CLIENTE.md +26 -0
- package/templates/skills/auditar-performance/templates/relatorios/TECNICO.md +27 -0
- package/templates/skills/auditar-seguranca/SKILL.md +67 -0
- package/templates/skills/auditar-seguranca/references/seguranca-tecnica.md +23 -0
- package/templates/skills/auditar-seguranca/templates/relatorios/CLIENTE.md +30 -0
- package/templates/skills/auditar-seguranca/templates/relatorios/TECNICO.md +38 -0
- package/templates/skills/auditar-ui/SKILL.md +64 -0
- package/templates/skills/auditar-ui/references/lentes.md +33 -0
- package/templates/skills/auditar-ui/templates/relatorios/CLIENTE.md +26 -0
- package/templates/skills/auditar-ui/templates/relatorios/TECNICO.md +31 -0
- package/templates/skills/evoluir-harness/SKILL.md +81 -0
- package/templates/skills/evoluir-harness/references/agent.md +83 -0
- package/templates/skills/evoluir-harness/references/auditoria.md +107 -0
- package/templates/skills/evoluir-harness/references/code-reviewer.md +68 -0
- package/templates/skills/evoluir-harness/references/design-system.md +56 -0
- package/templates/skills/evoluir-harness/references/hook.md +95 -0
- package/templates/skills/evoluir-harness/references/rule.md +67 -0
- package/templates/skills/evoluir-harness/references/skill.md +63 -0
- package/templates/skills/executar-plan/SKILL.md +130 -0
- package/templates/skills/executar-plan/references/delegacao.md +43 -0
- package/templates/skills/executar-plan/references/retomada.md +33 -0
- package/templates/skills/executar-plan/templates/BLOQUEIOS.md +30 -0
- package/templates/skills/harness/SKILL.md +75 -0
- package/templates/skills/harness/references/auto-tag.md +118 -0
- package/templates/skills/harness/references/ignores.md +40 -0
- package/templates/skills/harness/references/scaffold.md +29 -0
- package/templates/skills/harness/references/validar-ideia.md +42 -0
- package/templates/skills/memoria/SKILL.md +52 -0
- package/templates/skills/nova-tarefa/SKILL.md +99 -0
- package/templates/skills/nova-tarefa/references/isso-ja-existe.md +24 -0
- package/templates/skills/nova-tarefa/references/regras-do-plano.md +35 -0
- package/templates/skills/nova-tarefa/templates/DECISOES.md +20 -0
- package/templates/skills/nova-tarefa/templates/PLAN.md +58 -0
- package/templates/skills/nova-tarefa/templates/SPEC.md +37 -0
- package/templates/skills/resolver-bug/SKILL.md +104 -0
- package/templates/skills/resolver-bug/references/relatorio-para-cliente.md +26 -0
- package/templates/skills/resolver-bug/templates/PLAN.md +62 -0
- package/templates/skills/resolver-bug/templates/relatorios/CLIENTE.md +29 -0
- package/templates/skills/resolver-bug/templates/relatorios/TECNICO.md +32 -0
- package/bin/cli.js +0 -122
- package/src/init.js +0 -317
- package/src/installer.js +0 -92
- package/src/run.js +0 -99
- package/templates/commands/auditar-performance.md +0 -27
- package/templates/commands/auditar-seguranca.md +0 -64
- package/templates/commands/auditar-ui.md +0 -38
- package/templates/commands/codigo-morto.md +0 -22
- package/templates/commands/configurar-auto-tag.md +0 -185
- package/templates/commands/configurar-comentarios.md +0 -543
- package/templates/commands/configurar-orquestrador.md +0 -13
- package/templates/commands/corrigir-review.md +0 -11
- package/templates/commands/criar-agents.md +0 -42
- package/templates/commands/criar-code-reviewer.md +0 -30
- package/templates/commands/criar-hooks.md +0 -32
- package/templates/commands/criar-rag.md +0 -25
- package/templates/commands/criar-rules.md +0 -31
- package/templates/commands/criar-skill.md +0 -22
- package/templates/commands/design-system.md +0 -27
- package/templates/commands/executar-plan.md +0 -36
- package/templates/commands/gerar-claude-md.md +0 -66
- package/templates/commands/gerar-ignore.md +0 -37
- package/templates/commands/gerar-spec.md +0 -33
- package/templates/commands/harness.md +0 -34
- package/templates/commands/nova-tarefa.md +0 -60
- package/templates/commands/resolver-bug.md +0 -25
- package/templates/commands/review-por-task.md +0 -16
- package/templates/commands/scaffold.md +0 -25
- package/templates/commands/validar-commands.md +0 -24
- package/templates/commands/validar-ideia.md +0 -36
package/dist/init.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { EOL } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import * as p from '@clack/prompts';
|
|
5
|
+
import { allCommands, loadCatalog, recommendedCommands } from './catalog.js';
|
|
6
|
+
import { readConfig, syncGitignore, writeDefaultConfig } from './config/config.js';
|
|
7
|
+
import { parseInitArgs } from './install/flags.js';
|
|
8
|
+
import { isHarnessPluginEnabled } from './install/hooks.js';
|
|
9
|
+
import { readLock } from './install/lock.js';
|
|
10
|
+
import { describePlan, executePlan, packageInfo, planInstall, resolveInstallOptions, } from './install/plan.js';
|
|
11
|
+
import { detectProjectKind } from './install/selection.js';
|
|
12
|
+
export { detectProjectKind, neededSkillsFor, resolveSelection } from './install/selection.js';
|
|
13
|
+
const PLUGIN_HOOKS_NOTE = 'O plugin fontdata@ai-harness está habilitado: os hooks já rodam por ele e não foram instalados no projeto, para não rodarem duas vezes.';
|
|
14
|
+
/** Encerra o fluxo com mensagem amigável quando o usuário cancela (Ctrl+C). */
|
|
15
|
+
function bail(value) {
|
|
16
|
+
if (p.isCancel(value)) {
|
|
17
|
+
p.cancel('Operação cancelada.');
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Quantos itens a lista pode mostrar de uma vez sem estourar a altura do terminal.
|
|
24
|
+
* Calcula a partir das linhas reais do terminal (process.stdout.rows), deixando
|
|
25
|
+
* margem para a mensagem e os indicadores de rolagem do clack. Sem isso, em um
|
|
26
|
+
* terminal baixo a lista sobrepõe as renderizações ("itens duplicados").
|
|
27
|
+
*/
|
|
28
|
+
function viewportItems(min = 3, reserve = 6) {
|
|
29
|
+
const rows = process.stdout.rows || 20;
|
|
30
|
+
return Math.max(min, rows - reserve);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Seleção interativa em dois níveis: menu de grupos → submenu de itens do grupo
|
|
34
|
+
* → volta ao menu de grupos. Mantém cada lista pequena (cabe em terminais baixos)
|
|
35
|
+
* e mostra o quanto já foi marcado em cada grupo. Retorna os comandos escolhidos.
|
|
36
|
+
*
|
|
37
|
+
* `initialIds` define os comandos já marcados ao abrir (ex.: recomendados em
|
|
38
|
+
* projeto novo). Em projeto existente vem vazio — o usuário marca só os avulsos.
|
|
39
|
+
*/
|
|
40
|
+
async function selectByGroups(catalog, initialIds = new Set()) {
|
|
41
|
+
const selectedIds = new Set(initialIds);
|
|
42
|
+
const DONE = '__done__';
|
|
43
|
+
for (;;) {
|
|
44
|
+
const groupOptions = catalog.groups.map((g) => {
|
|
45
|
+
const chosen = g.commands.filter((c) => selectedIds.has(c.id)).length;
|
|
46
|
+
return {
|
|
47
|
+
value: g.id,
|
|
48
|
+
label: `${g.title} (${chosen}/${g.commands.length})`,
|
|
49
|
+
...(chosen ? { hint: `${chosen} marcado(s)` } : {}),
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
groupOptions.push({
|
|
53
|
+
value: DONE,
|
|
54
|
+
label: `✓ Concluir e instalar (${selectedIds.size} comando(s) marcado(s))`,
|
|
55
|
+
});
|
|
56
|
+
const groupChoice = bail(await p.select({
|
|
57
|
+
message: 'Escolha um grupo para marcar comandos — ou Concluir:',
|
|
58
|
+
options: groupOptions,
|
|
59
|
+
maxItems: viewportItems(),
|
|
60
|
+
}));
|
|
61
|
+
if (groupChoice === DONE)
|
|
62
|
+
break;
|
|
63
|
+
const group = catalog.groups.find((g) => g.id === groupChoice);
|
|
64
|
+
if (!group)
|
|
65
|
+
continue;
|
|
66
|
+
const picked = bail(await p.multiselect({
|
|
67
|
+
message: `${group.title} — espaço marca, enter volta ao menu de grupos:`,
|
|
68
|
+
options: group.commands.map((c) => ({ value: c.id, label: c.title, hint: c.description })),
|
|
69
|
+
initialValues: group.commands.filter((c) => selectedIds.has(c.id)).map((c) => c.id),
|
|
70
|
+
required: false,
|
|
71
|
+
}));
|
|
72
|
+
// Reconcilia só este grupo: limpa os ids dele e re-adiciona os que ficaram marcados.
|
|
73
|
+
for (const c of group.commands)
|
|
74
|
+
selectedIds.delete(c.id);
|
|
75
|
+
for (const id of picked)
|
|
76
|
+
selectedIds.add(id);
|
|
77
|
+
}
|
|
78
|
+
return allCommands(catalog).filter((c) => selectedIds.has(c.id));
|
|
79
|
+
}
|
|
80
|
+
/** Config com os padrões (sem sobrescrever a existente) e o bloco gerenciado do .gitignore. */
|
|
81
|
+
function prepareProjectFiles(repoPath) {
|
|
82
|
+
writeDefaultConfig(repoPath);
|
|
83
|
+
syncGitignore(repoPath, readConfig(repoPath).config);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Sem terminal interativo, ou com escopo dado por flag, o init não pergunta nada.
|
|
87
|
+
* Sem `--yes` ele só mostra o que faria — o que protege execuções em CI.
|
|
88
|
+
*/
|
|
89
|
+
function runNonInteractive(args) {
|
|
90
|
+
const catalog = loadCatalog();
|
|
91
|
+
const repoPath = resolve(process.cwd(), args.path ?? '.');
|
|
92
|
+
if (!existsSync(repoPath))
|
|
93
|
+
throw new Error(`Pasta não encontrada: ${repoPath}`);
|
|
94
|
+
readLock(repoPath);
|
|
95
|
+
const options = resolveInstallOptions(args);
|
|
96
|
+
const skippedForPlugin = options.hooks && isHarnessPluginEnabled(repoPath);
|
|
97
|
+
if (skippedForPlugin)
|
|
98
|
+
options.hooks = false;
|
|
99
|
+
const plan = planInstall(catalog, repoPath, options);
|
|
100
|
+
const summary = [describePlan(plan), ...(skippedForPlugin ? [PLUGIN_HOOKS_NOTE] : [])].join(EOL + EOL);
|
|
101
|
+
if (!args.yes) {
|
|
102
|
+
console.log(`Simulação — nada foi gravado em ${repoPath}.\n\n${summary}\n\nRode de novo com --yes para aplicar.`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const lock = executePlan(repoPath, plan, packageInfo());
|
|
106
|
+
prepareProjectFiles(repoPath);
|
|
107
|
+
console.log(`Instalação concluída em ${repoPath}.\n\n${summary}\n\nLock: .ai-harness/lock.json (${lock.itens.length} itens).`);
|
|
108
|
+
}
|
|
109
|
+
async function runWizard(args) {
|
|
110
|
+
const catalog = loadCatalog();
|
|
111
|
+
p.intro('Fontdata · ai-harness — instalador do harness para Claude Code');
|
|
112
|
+
// 1. Diretório alvo
|
|
113
|
+
const repoInput = bail(await p.text({
|
|
114
|
+
message: 'Em qual pasta do projeto deseja instalar?',
|
|
115
|
+
placeholder: '. (pasta atual)',
|
|
116
|
+
initialValue: args.path || '.',
|
|
117
|
+
}));
|
|
118
|
+
const repoPath = resolve(process.cwd(), repoInput.trim() || '.');
|
|
119
|
+
if (!existsSync(repoPath)) {
|
|
120
|
+
p.cancel(`Pasta não encontrada: ${repoPath}`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
readLock(repoPath);
|
|
124
|
+
// Detecta o tipo de projeto: muda como a seleção "Manual" começa.
|
|
125
|
+
const projectKind = detectProjectKind(repoPath);
|
|
126
|
+
p.note(projectKind === 'existing'
|
|
127
|
+
? 'Projeto existente detectado. Na opção "Escolher manualmente" nada vem marcado — ideal para cadastrar comandos avulsos.'
|
|
128
|
+
: 'Projeto novo detectado. Na opção "Escolher manualmente" os comandos recomendados já vêm marcados.', projectKind === 'existing' ? 'Projeto existente' : 'Projeto novo');
|
|
129
|
+
// 2. O que instalar
|
|
130
|
+
const scope = bail(await p.select({
|
|
131
|
+
message: 'O que deseja instalar?',
|
|
132
|
+
options: [
|
|
133
|
+
{ value: 'recommended', label: 'Conjunto recomendado', hint: 'núcleo de setup + dia a dia' },
|
|
134
|
+
{ value: 'all', label: 'Tudo', hint: `${allCommands(catalog).length} comandos` },
|
|
135
|
+
{
|
|
136
|
+
value: 'manual',
|
|
137
|
+
label: 'Escolher manualmente',
|
|
138
|
+
hint: projectKind === 'existing' ? 'seleção por categoria (nada marcado)' : 'seleção por categoria (recomendados marcados)',
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
}));
|
|
142
|
+
let commands = [];
|
|
143
|
+
if (scope === 'manual') {
|
|
144
|
+
// Projeto novo começa com os recomendados marcados; existente começa vazio.
|
|
145
|
+
const initialIds = projectKind === 'new'
|
|
146
|
+
? new Set(recommendedCommands(catalog).map((c) => c.id))
|
|
147
|
+
: new Set();
|
|
148
|
+
commands = (await selectByGroups(catalog, initialIds)).map((c) => c.id);
|
|
149
|
+
}
|
|
150
|
+
// 3. Comando-mestre /harness
|
|
151
|
+
const includeMaster = bail(await p.confirm({
|
|
152
|
+
message: 'Incluir também o comando-mestre /harness (conduz a sequência de setup em ordem)?',
|
|
153
|
+
initialValue: true,
|
|
154
|
+
}));
|
|
155
|
+
// 4. CLAUDE.md base
|
|
156
|
+
const claudeExists = existsSync(resolve(repoPath, catalog.claudeMd.target));
|
|
157
|
+
let claudeMd = 'keep';
|
|
158
|
+
const wantClaude = bail(await p.confirm({
|
|
159
|
+
message: claudeExists
|
|
160
|
+
? 'Já existe um CLAUDE.md na raiz. Deseja tratá-lo?'
|
|
161
|
+
: 'Criar o CLAUDE.md base na raiz (filosofia de orquestração/TDD)?',
|
|
162
|
+
initialValue: !claudeExists,
|
|
163
|
+
}));
|
|
164
|
+
if (wantClaude) {
|
|
165
|
+
claudeMd = claudeExists
|
|
166
|
+
? bail(await p.select({
|
|
167
|
+
message: 'O que fazer com o CLAUDE.md existente?',
|
|
168
|
+
options: [
|
|
169
|
+
{ value: 'backup', label: 'Sobrescrever criando .bak', hint: 'recomendado' },
|
|
170
|
+
{ value: 'overwrite', label: 'Sobrescrever sem backup' },
|
|
171
|
+
{ value: 'keep', label: 'Manter o existente' },
|
|
172
|
+
],
|
|
173
|
+
}))
|
|
174
|
+
: 'create';
|
|
175
|
+
}
|
|
176
|
+
// 5. Hooks: pelo plugin habilitado eles já rodam; no projeto, perguntamos.
|
|
177
|
+
let hooks = false;
|
|
178
|
+
if (isHarnessPluginEnabled(repoPath)) {
|
|
179
|
+
p.note(PLUGIN_HOOKS_NOTE, 'Hooks');
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
hooks = bail(await p.confirm({
|
|
183
|
+
message: 'Instalar os hooks do harness (lembrete de fluxos, bloqueio de segredos no commit e avisos de método)?',
|
|
184
|
+
initialValue: true,
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
const agents = bail(await p.confirm({
|
|
188
|
+
message: 'Instalar os agents somente leitura do harness (revisor-testes, qa, investigador, auditor-plano)?',
|
|
189
|
+
initialValue: true,
|
|
190
|
+
}));
|
|
191
|
+
const comments = bail(await p.confirm({
|
|
192
|
+
message: 'Instalar o padrão de comentários (rule, hook que bloqueia a violação e catraca do legado)?',
|
|
193
|
+
initialValue: true,
|
|
194
|
+
}));
|
|
195
|
+
const options = {
|
|
196
|
+
scope: scope === 'all' ? 'all' : scope === 'manual' ? 'manual' : 'recommended',
|
|
197
|
+
commands,
|
|
198
|
+
includeMaster,
|
|
199
|
+
claudeMd,
|
|
200
|
+
conflict: 'overwrite',
|
|
201
|
+
hooks,
|
|
202
|
+
agents,
|
|
203
|
+
comments,
|
|
204
|
+
};
|
|
205
|
+
let plan = planInstall(catalog, repoPath, options);
|
|
206
|
+
// 6. Confirmação final
|
|
207
|
+
if (plan.files.length === 0) {
|
|
208
|
+
p.outro('Nada selecionado. Nenhuma alteração feita.');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// Política global de conflito: perguntamos UMA vez se houver risco de sobrescrever.
|
|
212
|
+
const anyConflict = plan.files.some((file) => file.path !== catalog.claudeMd.target && existsSync(join(repoPath, file.path)));
|
|
213
|
+
if (anyConflict) {
|
|
214
|
+
const conflict = bail(await p.select({
|
|
215
|
+
message: 'Alguns itens já existem no destino. O que fazer com os conflitos?',
|
|
216
|
+
options: [
|
|
217
|
+
{ value: 'overwrite', label: 'Sobrescrever os existentes' },
|
|
218
|
+
{ value: 'skip', label: 'Pular os que já existem' },
|
|
219
|
+
],
|
|
220
|
+
}));
|
|
221
|
+
if (conflict === 'skip')
|
|
222
|
+
plan = planInstall(catalog, repoPath, { ...options, conflict });
|
|
223
|
+
}
|
|
224
|
+
// Execução
|
|
225
|
+
const s = p.spinner();
|
|
226
|
+
s.start('Instalando...');
|
|
227
|
+
executePlan(repoPath, plan, packageInfo());
|
|
228
|
+
prepareProjectFiles(repoPath);
|
|
229
|
+
s.stop('Instalação concluída.');
|
|
230
|
+
p.note(describePlan(plan), `Resumo · ${repoPath}`);
|
|
231
|
+
p.outro('Abra o projeto no Claude Code e digite "/" para ver os comandos instalados.');
|
|
232
|
+
}
|
|
233
|
+
export async function runInit(argv = []) {
|
|
234
|
+
const args = parseInitArgs(argv);
|
|
235
|
+
const interactive = Boolean(process.stdin.isTTY) && !args.hasSelectionFlags && !args.yes;
|
|
236
|
+
if (interactive)
|
|
237
|
+
await runWizard(args);
|
|
238
|
+
else
|
|
239
|
+
runNonInteractive(args);
|
|
240
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { mkdirSync, readdirSync, renameSync, rmdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
/** Resolve um caminho relativo à raiz do projeto, recusando o que escapar dela. */
|
|
4
|
+
export function resolveInside(root, relPath) {
|
|
5
|
+
const target = resolve(root, relPath);
|
|
6
|
+
const rel = relative(root, target);
|
|
7
|
+
if (isAbsolute(relPath) || rel === '' || rel === '..' || rel.startsWith(`..${sep}`)) {
|
|
8
|
+
throw new Error(`Caminho fora da raiz do projeto: ${relPath}`);
|
|
9
|
+
}
|
|
10
|
+
return target;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Grava um conjunto de arquivos no projeto de forma que uma falha não deixe a
|
|
14
|
+
* instalação pela metade: tudo é montado antes numa pasta temporária dentro do
|
|
15
|
+
* próprio projeto (mesmo sistema de arquivos) e só então movido com `rename`.
|
|
16
|
+
* Arquivos do projeto fora do conjunto não são tocados.
|
|
17
|
+
*/
|
|
18
|
+
export function applyAtomically(root, files) {
|
|
19
|
+
const planned = files.map((file) => ({
|
|
20
|
+
target: resolveInside(root, file.path),
|
|
21
|
+
rel: relative(root, resolve(root, file.path)),
|
|
22
|
+
content: file.content,
|
|
23
|
+
}));
|
|
24
|
+
const harnessDir = join(root, '.ai-harness');
|
|
25
|
+
const stage = join(harnessDir, `.tmp-${process.pid}-${Date.now()}`);
|
|
26
|
+
try {
|
|
27
|
+
for (const file of planned) {
|
|
28
|
+
const staged = join(stage, file.rel);
|
|
29
|
+
mkdirSync(dirname(staged), { recursive: true });
|
|
30
|
+
writeFileSync(staged, file.content);
|
|
31
|
+
}
|
|
32
|
+
for (const file of planned) {
|
|
33
|
+
mkdirSync(dirname(file.target), { recursive: true });
|
|
34
|
+
renameSync(join(stage, file.rel), file.target);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
rmSync(stage, { recursive: true, force: true });
|
|
39
|
+
removeIfEmpty(harnessDir);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** A pasta .ai-harness criada só para a montagem não deve sobrar num projeto sem lock nem config. */
|
|
43
|
+
function removeIfEmpty(dir) {
|
|
44
|
+
try {
|
|
45
|
+
if (readdirSync(dir).length === 0)
|
|
46
|
+
rmdirSync(dir);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// não existe ou não pôde ser lida — nada a limpar.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const SCOPES = { recomendado: 'recommended', tudo: 'all' };
|
|
2
|
+
const CLAUDE_MD = {
|
|
3
|
+
criar: 'create',
|
|
4
|
+
manter: 'keep',
|
|
5
|
+
backup: 'backup',
|
|
6
|
+
sobrescrever: 'overwrite',
|
|
7
|
+
};
|
|
8
|
+
const CONFLICTS = { sobrescrever: 'overwrite', pular: 'skip' };
|
|
9
|
+
const VALUE_FLAGS = ['--escopo', '--comandos', '--claude-md', '--conflito'];
|
|
10
|
+
const BOOLEAN_FLAGS = ['--sem-harness', '--sem-hooks', '--sem-agents', '--sem-comentarios', '--yes', '-y', '--sim'];
|
|
11
|
+
function pick(flag, value, options) {
|
|
12
|
+
const chosen = options[value];
|
|
13
|
+
if (chosen === undefined) {
|
|
14
|
+
throw new Error(`Valor inválido para ${flag}: ${value}. Use: ${Object.keys(options).join(', ')}.`);
|
|
15
|
+
}
|
|
16
|
+
return chosen;
|
|
17
|
+
}
|
|
18
|
+
function distance(a, b) {
|
|
19
|
+
const row = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
20
|
+
for (let i = 1; i <= a.length; i++) {
|
|
21
|
+
let diagonal = row[0] ?? 0;
|
|
22
|
+
row[0] = i;
|
|
23
|
+
for (let j = 1; j <= b.length; j++) {
|
|
24
|
+
const above = row[j] ?? 0;
|
|
25
|
+
row[j] = Math.min(above + 1, (row[j - 1] ?? 0) + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
26
|
+
diagonal = above;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return row[b.length] ?? 0;
|
|
30
|
+
}
|
|
31
|
+
/** Flag conhecida mais parecida com a digitada, para sugerir em mensagens de erro. */
|
|
32
|
+
export function suggestFlag(token, flags) {
|
|
33
|
+
const [best] = flags
|
|
34
|
+
.map((flag) => ({ flag, d: distance(token, flag) }))
|
|
35
|
+
.filter(({ d }) => d <= 2)
|
|
36
|
+
.sort((x, y) => x.d - y.d);
|
|
37
|
+
return best?.flag;
|
|
38
|
+
}
|
|
39
|
+
/** Lê os argumentos do `init`; valores e flags inválidos viram erro com orientação. */
|
|
40
|
+
export function parseInitArgs(argv) {
|
|
41
|
+
const args = {
|
|
42
|
+
path: undefined,
|
|
43
|
+
scope: undefined,
|
|
44
|
+
commands: [],
|
|
45
|
+
includeMaster: undefined,
|
|
46
|
+
claudeMd: undefined,
|
|
47
|
+
conflict: undefined,
|
|
48
|
+
hooks: undefined,
|
|
49
|
+
agents: undefined,
|
|
50
|
+
comments: undefined,
|
|
51
|
+
yes: false,
|
|
52
|
+
hasSelectionFlags: false,
|
|
53
|
+
};
|
|
54
|
+
for (let i = 0; i < argv.length; i++) {
|
|
55
|
+
const token = argv[i] ?? '';
|
|
56
|
+
if (!token.startsWith('-')) {
|
|
57
|
+
if (args.path !== undefined)
|
|
58
|
+
throw new Error(`Argumento inesperado: ${token}`);
|
|
59
|
+
args.path = token;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (BOOLEAN_FLAGS.includes(token)) {
|
|
63
|
+
if (token === '--sem-harness')
|
|
64
|
+
args.includeMaster = false;
|
|
65
|
+
else if (token === '--sem-hooks')
|
|
66
|
+
args.hooks = false;
|
|
67
|
+
else if (token === '--sem-agents')
|
|
68
|
+
args.agents = false;
|
|
69
|
+
else if (token === '--sem-comentarios')
|
|
70
|
+
args.comments = false;
|
|
71
|
+
else
|
|
72
|
+
args.yes = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (!VALUE_FLAGS.includes(token)) {
|
|
76
|
+
const suggestion = suggestFlag(token, [...VALUE_FLAGS, ...BOOLEAN_FLAGS]);
|
|
77
|
+
throw new Error(`Flag desconhecida: ${token}.${suggestion ? ` Você quis dizer ${suggestion}?` : ''}`);
|
|
78
|
+
}
|
|
79
|
+
const value = argv[i + 1];
|
|
80
|
+
if (value === undefined || value.startsWith('-'))
|
|
81
|
+
throw new Error(`${token} precisa de um valor.`);
|
|
82
|
+
i++;
|
|
83
|
+
if (token === '--escopo')
|
|
84
|
+
args.scope = pick(token, value, SCOPES);
|
|
85
|
+
else if (token === '--claude-md')
|
|
86
|
+
args.claudeMd = pick(token, value, CLAUDE_MD);
|
|
87
|
+
else if (token === '--conflito')
|
|
88
|
+
args.conflict = pick(token, value, CONFLICTS);
|
|
89
|
+
else {
|
|
90
|
+
args.commands = value.split(',').map((id) => id.trim()).filter(Boolean);
|
|
91
|
+
args.scope = 'manual';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (argv.includes('--escopo') && argv.includes('--comandos')) {
|
|
95
|
+
throw new Error('Use --escopo ou --comandos, não os dois.');
|
|
96
|
+
}
|
|
97
|
+
args.hasSelectionFlags = args.scope !== undefined;
|
|
98
|
+
return args;
|
|
99
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { templatesDir } from '../catalog.js';
|
|
5
|
+
/** Id do item do lock que agrupa os scripts de hook copiados para o projeto. */
|
|
6
|
+
export const HOOKS_ITEM_ID = 'ai-harness';
|
|
7
|
+
export const PROJECT_HOOKS_DIR = '.claude/hooks/ai-harness';
|
|
8
|
+
export const PLUGIN_ID = 'fontdata@ai-harness';
|
|
9
|
+
const PLUGIN_PREFIX = '${CLAUDE_PLUGIN_ROOT}/templates/hooks/';
|
|
10
|
+
const PROJECT_PREFIX = `$CLAUDE_PROJECT_DIR/${PROJECT_HOOKS_DIR}/`;
|
|
11
|
+
/** Hooks do plugin (templates/hooks/hooks.json), fonte única também para a instalação no projeto. */
|
|
12
|
+
export function pluginHooksConfig() {
|
|
13
|
+
const file = JSON.parse(readFileSync(join(templatesDir, 'hooks', 'hooks.json'), 'utf8'));
|
|
14
|
+
return file.hooks;
|
|
15
|
+
}
|
|
16
|
+
/** Os mesmos hooks, apontando para os scripts copiados em .claude/hooks/ai-harness/. */
|
|
17
|
+
export function projectHooksConfig() {
|
|
18
|
+
return Object.fromEntries(Object.entries(pluginHooksConfig()).map(([event, groups]) => [
|
|
19
|
+
event,
|
|
20
|
+
groups.map((group) => ({
|
|
21
|
+
...group,
|
|
22
|
+
hooks: group.hooks.map((hook) => ({ ...hook, command: hook.command.replaceAll(PLUGIN_PREFIX, PROJECT_PREFIX) })),
|
|
23
|
+
})),
|
|
24
|
+
]));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* O plugin está habilitado no projeto ou para o usuário? Nesse caso os hooks já rodam
|
|
28
|
+
* pelo plugin, e instalá-los também no projeto faria cada um rodar duas vezes (D-24).
|
|
29
|
+
*/
|
|
30
|
+
export function isHarnessPluginEnabled(root, home = homedir()) {
|
|
31
|
+
const files = [
|
|
32
|
+
join(root, '.claude', 'settings.json'),
|
|
33
|
+
join(root, '.claude', 'settings.local.json'),
|
|
34
|
+
join(home, '.claude', 'settings.json'),
|
|
35
|
+
];
|
|
36
|
+
return files.some((file) => {
|
|
37
|
+
try {
|
|
38
|
+
const plugins = JSON.parse(readFileSync(file, 'utf8')).enabledPlugins;
|
|
39
|
+
if (Array.isArray(plugins))
|
|
40
|
+
return plugins.includes(PLUGIN_ID);
|
|
41
|
+
return typeof plugins === 'object' && plugins !== null && plugins[PLUGIN_ID] === true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
/** Versão do formato do lock; o CLI recusa ler um lock de formato mais novo que o seu. */
|
|
5
|
+
export const LOCK_VERSION = 1;
|
|
6
|
+
export const LOCK_PATH = join('.ai-harness', 'lock.json');
|
|
7
|
+
export function hashContent(content) {
|
|
8
|
+
return createHash('sha256').update(content).digest('hex');
|
|
9
|
+
}
|
|
10
|
+
/** Monta o item do lock com o hash de cada arquivo já instalado, em ordem estável. */
|
|
11
|
+
export function buildLockItem(root, tipo, id, relPaths) {
|
|
12
|
+
const arquivos = [...relPaths]
|
|
13
|
+
.sort()
|
|
14
|
+
.map((caminho) => ({ caminho, sha256: hashContent(readFileSync(join(root, caminho))) }));
|
|
15
|
+
return { tipo, id, arquivos };
|
|
16
|
+
}
|
|
17
|
+
export function readLock(root) {
|
|
18
|
+
const file = join(root, LOCK_PATH);
|
|
19
|
+
if (!existsSync(file))
|
|
20
|
+
return null;
|
|
21
|
+
let lock;
|
|
22
|
+
try {
|
|
23
|
+
lock = JSON.parse(readFileSync(file, 'utf8'));
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
27
|
+
throw new Error(`O ${LOCK_PATH} está com JSON inválido (${reason}).`);
|
|
28
|
+
}
|
|
29
|
+
if (lock.lock_versao > LOCK_VERSION) {
|
|
30
|
+
throw new Error(`O ${LOCK_PATH} foi gerado por uma versão mais nova do ai-harness (lock_versao ${lock.lock_versao}). ` +
|
|
31
|
+
'Atualize o pacote antes de continuar.');
|
|
32
|
+
}
|
|
33
|
+
return lock;
|
|
34
|
+
}
|
|
35
|
+
export function writeLock(root, lock) {
|
|
36
|
+
const file = join(root, LOCK_PATH);
|
|
37
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
38
|
+
writeFileSync(file, `${JSON.stringify(lock, null, 2)}\n`);
|
|
39
|
+
}
|