@fontdata/ai-harness 2.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 +17 -0
- package/.claude-plugin/plugin.json +8 -0
- package/README.md +239 -0
- package/bin/cli.js +122 -0
- package/package.json +44 -0
- package/src/catalog.js +26 -0
- package/src/init.js +317 -0
- package/src/installer.js +92 -0
- package/src/run.js +99 -0
- package/templates/CLAUDE.md +49 -0
- package/templates/catalog.json +124 -0
- package/templates/commands/atualizar-deps.md +15 -0
- package/templates/commands/auditar-performance.md +27 -0
- package/templates/commands/auditar-seguranca.md +64 -0
- package/templates/commands/auditar-ui.md +38 -0
- package/templates/commands/codigo-morto.md +22 -0
- package/templates/commands/configurar-auto-tag.md +185 -0
- package/templates/commands/configurar-comentarios.md +543 -0
- package/templates/commands/configurar-orquestrador.md +13 -0
- package/templates/commands/corrigir-review.md +11 -0
- package/templates/commands/criar-agents.md +42 -0
- package/templates/commands/criar-code-reviewer.md +30 -0
- package/templates/commands/criar-hooks.md +32 -0
- package/templates/commands/criar-rag.md +25 -0
- package/templates/commands/criar-rules.md +31 -0
- package/templates/commands/criar-skill.md +22 -0
- package/templates/commands/design-system.md +27 -0
- package/templates/commands/diagrama.md +17 -0
- package/templates/commands/executar-plan.md +36 -0
- package/templates/commands/gerar-claude-md.md +66 -0
- package/templates/commands/gerar-env.md +16 -0
- package/templates/commands/gerar-ignore.md +37 -0
- package/templates/commands/gerar-spec.md +33 -0
- package/templates/commands/gerar-testes.md +17 -0
- package/templates/commands/git-commit-push.md +38 -0
- package/templates/commands/harness.md +34 -0
- package/templates/commands/nova-tarefa.md +60 -0
- package/templates/commands/onboarding.md +21 -0
- package/templates/commands/refatorar.md +17 -0
- package/templates/commands/resolver-bug.md +25 -0
- package/templates/commands/review-por-task.md +16 -0
- package/templates/commands/revisar-pr.md +18 -0
- package/templates/commands/scaffold.md +25 -0
- package/templates/commands/sincronizar-claude-md.md +15 -0
- package/templates/commands/validar-commands.md +24 -0
- package/templates/commands/validar-ideia.md +36 -0
- package/templates/commands/versao.md +159 -0
- package/templates/skills/lgpd-checker/ORIGEM.md +21 -0
- package/templates/skills/lgpd-checker/SKILL.md +152 -0
- package/templates/skills/lgpd-checker/references/checklist.md +155 -0
- package/templates/skills/lgpd-checker/references/lgpd-artigos.md +178 -0
- package/templates/skills/lgpd-checker/references/report-template.md +117 -0
package/src/init.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import * as p from '@clack/prompts';
|
|
4
|
+
import { loadCatalog, allCommands, recommendedCommands } from './catalog.js';
|
|
5
|
+
import { installCommand, installClaudeMd, installMaster, installSkill } from './installer.js';
|
|
6
|
+
|
|
7
|
+
/** Encerra o fluxo com mensagem amigável quando o usuário cancela (Ctrl+C). */
|
|
8
|
+
function bail(value) {
|
|
9
|
+
if (p.isCancel(value)) {
|
|
10
|
+
p.cancel('Operação cancelada.');
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Quantos itens a lista pode mostrar de uma vez sem estourar a altura do terminal.
|
|
18
|
+
* Calcula a partir das linhas reais do terminal (process.stdout.rows), deixando
|
|
19
|
+
* margem para a mensagem e os indicadores de rolagem do clack. Sem isso, em um
|
|
20
|
+
* terminal baixo a lista sobrepõe as renderizações ("itens duplicados").
|
|
21
|
+
*/
|
|
22
|
+
function viewportItems(min = 3, reserve = 6) {
|
|
23
|
+
const rows = process.stdout.rows || 20;
|
|
24
|
+
return Math.max(min, rows - reserve);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Manifestos de stack: se algum existir na raiz, é um projeto já existente.
|
|
29
|
+
*/
|
|
30
|
+
const PROJECT_MANIFESTS = [
|
|
31
|
+
'package.json', 'pyproject.toml', 'requirements.txt', 'go.mod',
|
|
32
|
+
'Cargo.toml', 'pom.xml', 'build.gradle', 'build.gradle.kts',
|
|
33
|
+
'composer.json', 'Gemfile', 'Makefile', 'CMakeLists.txt',
|
|
34
|
+
'tsconfig.json', 'pubspec.yaml', 'mix.exs',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/** Pastas de código-fonte que, se tiverem conteúdo, indicam projeto existente. */
|
|
38
|
+
const SOURCE_DIRS = ['src', 'app', 'lib', 'source'];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Detecta se a pasta alvo já é um projeto com código ('existing') ou está
|
|
42
|
+
* essencialmente vazia/recém-criada ('new'). Ignora .git e .claude — a presença
|
|
43
|
+
* deles sozinha não caracteriza um projeto existente.
|
|
44
|
+
*/
|
|
45
|
+
function detectProjectKind(repoPath) {
|
|
46
|
+
// 1. Manifesto de stack na raiz.
|
|
47
|
+
if (PROJECT_MANIFESTS.some((f) => existsSync(resolve(repoPath, f)))) return 'existing';
|
|
48
|
+
|
|
49
|
+
// 2. Arquivo de solução/projeto .NET na raiz.
|
|
50
|
+
let rootEntries = [];
|
|
51
|
+
try {
|
|
52
|
+
rootEntries = readdirSync(repoPath);
|
|
53
|
+
} catch {
|
|
54
|
+
return 'new';
|
|
55
|
+
}
|
|
56
|
+
if (rootEntries.some((f) => /\.(sln|csproj|fsproj|vbproj)$/i.test(f))) return 'existing';
|
|
57
|
+
|
|
58
|
+
// 3. Pasta de código-fonte com algum conteúdo.
|
|
59
|
+
for (const dir of SOURCE_DIRS) {
|
|
60
|
+
const dirPath = resolve(repoPath, dir);
|
|
61
|
+
try {
|
|
62
|
+
if (statSync(dirPath).isDirectory() && readdirSync(dirPath).length) return 'existing';
|
|
63
|
+
} catch {
|
|
64
|
+
// pasta não existe — segue.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return 'new';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Seleção interativa em dois níveis: menu de grupos → submenu de itens do grupo
|
|
73
|
+
* → volta ao menu de grupos. Mantém cada lista pequena (cabe em terminais baixos)
|
|
74
|
+
* e mostra o quanto já foi marcado em cada grupo. Retorna os comandos escolhidos.
|
|
75
|
+
*
|
|
76
|
+
* `initialIds` define os comandos já marcados ao abrir (ex.: recomendados em
|
|
77
|
+
* projeto novo). Em projeto existente vem vazio — o usuário marca só os avulsos.
|
|
78
|
+
*/
|
|
79
|
+
async function selectByGroups(catalog, initialIds = new Set()) {
|
|
80
|
+
const selectedIds = new Set(initialIds);
|
|
81
|
+
const DONE = '__done__';
|
|
82
|
+
|
|
83
|
+
for (;;) {
|
|
84
|
+
const groupOptions = catalog.groups.map((g) => {
|
|
85
|
+
const chosen = g.commands.filter((c) => selectedIds.has(c.id)).length;
|
|
86
|
+
return {
|
|
87
|
+
value: g.id,
|
|
88
|
+
label: `${g.title} (${chosen}/${g.commands.length})`,
|
|
89
|
+
hint: chosen ? `${chosen} marcado(s)` : undefined,
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
groupOptions.push({
|
|
93
|
+
value: DONE,
|
|
94
|
+
label: `✓ Concluir e instalar (${selectedIds.size} comando(s) marcado(s))`,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const groupChoice = bail(
|
|
98
|
+
await p.select({
|
|
99
|
+
message: 'Escolha um grupo para marcar comandos — ou Concluir:',
|
|
100
|
+
options: groupOptions,
|
|
101
|
+
maxItems: viewportItems(),
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (groupChoice === DONE) break;
|
|
106
|
+
|
|
107
|
+
const group = catalog.groups.find((g) => g.id === groupChoice);
|
|
108
|
+
const picked = bail(
|
|
109
|
+
await p.multiselect({
|
|
110
|
+
message: `${group.title} — espaço marca, enter volta ao menu de grupos:`,
|
|
111
|
+
options: group.commands.map((c) => ({ value: c.id, label: c.title, hint: c.description })),
|
|
112
|
+
initialValues: group.commands.filter((c) => selectedIds.has(c.id)).map((c) => c.id),
|
|
113
|
+
required: false,
|
|
114
|
+
maxItems: viewportItems(),
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Reconcilia só este grupo: limpa os ids dele e re-adiciona os que ficaram marcados.
|
|
119
|
+
for (const c of group.commands) selectedIds.delete(c.id);
|
|
120
|
+
for (const id of picked) selectedIds.add(id);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return allCommands(catalog).filter((c) => selectedIds.has(c.id));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function runInit(initialPath) {
|
|
127
|
+
const catalog = loadCatalog();
|
|
128
|
+
|
|
129
|
+
p.intro('Fontdata · ai-harness — instalador do harness para Claude Code');
|
|
130
|
+
|
|
131
|
+
// 1. Diretório alvo
|
|
132
|
+
const repoInput = bail(
|
|
133
|
+
await p.text({
|
|
134
|
+
message: 'Em qual pasta do projeto deseja instalar?',
|
|
135
|
+
placeholder: '. (pasta atual)',
|
|
136
|
+
initialValue: initialPath || '.',
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
const repoPath = resolve(process.cwd(), repoInput.trim() || '.');
|
|
140
|
+
|
|
141
|
+
if (!existsSync(repoPath)) {
|
|
142
|
+
p.cancel(`Pasta não encontrada: ${repoPath}`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Detecta o tipo de projeto: muda como a seleção "Manual" começa.
|
|
147
|
+
const projectKind = detectProjectKind(repoPath);
|
|
148
|
+
p.note(
|
|
149
|
+
projectKind === 'existing'
|
|
150
|
+
? 'Projeto existente detectado. Na opção "Escolher manualmente" nada vem marcado — ideal para cadastrar comandos avulsos.'
|
|
151
|
+
: 'Projeto novo detectado. Na opção "Escolher manualmente" os comandos recomendados já vêm marcados.',
|
|
152
|
+
projectKind === 'existing' ? 'Projeto existente' : 'Projeto novo'
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// 2. O que instalar
|
|
156
|
+
const scope = bail(
|
|
157
|
+
await p.select({
|
|
158
|
+
message: 'O que deseja instalar?',
|
|
159
|
+
options: [
|
|
160
|
+
{ value: 'recommended', label: 'Conjunto recomendado', hint: 'núcleo de setup + dia a dia' },
|
|
161
|
+
{ value: 'all', label: 'Tudo', hint: `${allCommands(catalog).length} comandos` },
|
|
162
|
+
{
|
|
163
|
+
value: 'manual',
|
|
164
|
+
label: 'Escolher manualmente',
|
|
165
|
+
hint: projectKind === 'existing' ? 'seleção por categoria (nada marcado)' : 'seleção por categoria (recomendados marcados)',
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
})
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
let selected;
|
|
172
|
+
if (scope === 'all') {
|
|
173
|
+
selected = allCommands(catalog);
|
|
174
|
+
} else if (scope === 'recommended') {
|
|
175
|
+
selected = recommendedCommands(catalog);
|
|
176
|
+
} else {
|
|
177
|
+
// Projeto novo começa com os recomendados marcados; existente começa vazio.
|
|
178
|
+
const initialIds =
|
|
179
|
+
projectKind === 'new'
|
|
180
|
+
? new Set(recommendedCommands(catalog).map((c) => c.id))
|
|
181
|
+
: new Set();
|
|
182
|
+
selected = await selectByGroups(catalog, initialIds);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 3. Comando-mestre /harness
|
|
186
|
+
const wantMaster = bail(
|
|
187
|
+
await p.confirm({
|
|
188
|
+
message: 'Incluir também o comando-mestre /harness (conduz a sequência de setup em ordem)?',
|
|
189
|
+
initialValue: true,
|
|
190
|
+
})
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// Se o /harness foi escolhido, garante que os comandos que ele invoca também sejam instalados.
|
|
194
|
+
let autoAdded = [];
|
|
195
|
+
if (wantMaster) {
|
|
196
|
+
const required = catalog.master.requires || [];
|
|
197
|
+
const selectedIds = new Set(selected.map((c) => c.id));
|
|
198
|
+
const missing = allCommands(catalog).filter(
|
|
199
|
+
(c) => required.includes(c.id) && !selectedIds.has(c.id)
|
|
200
|
+
);
|
|
201
|
+
if (missing.length) {
|
|
202
|
+
selected = [...selected, ...missing];
|
|
203
|
+
autoAdded = missing.map((c) => c.title);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Resolve dependências por comando (campo `requires`): se um comando selecionado
|
|
208
|
+
// depende de outro, o dependente entra junto, mesmo que não tenha sido marcado —
|
|
209
|
+
// como acontece com o /harness.
|
|
210
|
+
{
|
|
211
|
+
const selectedIds = new Set(selected.map((c) => c.id));
|
|
212
|
+
const deps = allCommands(catalog).filter(
|
|
213
|
+
(c) =>
|
|
214
|
+
!selectedIds.has(c.id) &&
|
|
215
|
+
selected.some((sel) => (sel.requires || []).includes(c.id))
|
|
216
|
+
);
|
|
217
|
+
if (deps.length) {
|
|
218
|
+
selected = [...selected, ...deps];
|
|
219
|
+
autoAdded = [...autoAdded, ...deps.map((c) => c.title)];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 4. CLAUDE.md base
|
|
224
|
+
const claudeExists = existsSync(resolve(repoPath, catalog.claudeMd.target));
|
|
225
|
+
let claudeMode = 'create';
|
|
226
|
+
const wantClaude = bail(
|
|
227
|
+
await p.confirm({
|
|
228
|
+
message: claudeExists
|
|
229
|
+
? 'Já existe um CLAUDE.md na raiz. Deseja tratá-lo?'
|
|
230
|
+
: 'Criar o CLAUDE.md base na raiz (filosofia de orquestração/TDD)?',
|
|
231
|
+
initialValue: !claudeExists,
|
|
232
|
+
})
|
|
233
|
+
);
|
|
234
|
+
if (wantClaude && claudeExists) {
|
|
235
|
+
claudeMode = bail(
|
|
236
|
+
await p.select({
|
|
237
|
+
message: 'O que fazer com o CLAUDE.md existente?',
|
|
238
|
+
options: [
|
|
239
|
+
{ value: 'backup', label: 'Sobrescrever criando .bak', hint: 'recomendado' },
|
|
240
|
+
{ value: 'overwrite', label: 'Sobrescrever sem backup' },
|
|
241
|
+
{ value: 'skip', label: 'Manter o existente' },
|
|
242
|
+
],
|
|
243
|
+
})
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 5. Confirmação final
|
|
248
|
+
if (selected.length === 0 && !wantMaster && !wantClaude) {
|
|
249
|
+
p.outro('Nada selecionado. Nenhuma alteração feita.');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Skills exigidas pelos comandos selecionados (campo `requiresSkills`).
|
|
254
|
+
const skillCatalog = catalog.skills || [];
|
|
255
|
+
const neededSkillIds = new Set(selected.flatMap((c) => c.requiresSkills || []));
|
|
256
|
+
const neededSkills = skillCatalog.filter((sk) => neededSkillIds.has(sk.id));
|
|
257
|
+
|
|
258
|
+
// Política global de conflito: perguntamos UMA vez se houver risco de sobrescrever.
|
|
259
|
+
const anyConflict =
|
|
260
|
+
selected.some((c) => existsSync(resolve(repoPath, '.claude', 'commands', `${c.id}.md`))) ||
|
|
261
|
+
(wantMaster && existsSync(resolve(repoPath, '.claude', 'commands', `${catalog.master.id}.md`))) ||
|
|
262
|
+
neededSkills.some((sk) => existsSync(resolve(repoPath, '.claude', 'skills', sk.id)));
|
|
263
|
+
|
|
264
|
+
let conflictPolicy = 'overwrite';
|
|
265
|
+
if (anyConflict) {
|
|
266
|
+
conflictPolicy = bail(
|
|
267
|
+
await p.select({
|
|
268
|
+
message: 'Alguns itens já existem no destino. O que fazer com os conflitos?',
|
|
269
|
+
options: [
|
|
270
|
+
{ value: 'overwrite', label: 'Sobrescrever os existentes' },
|
|
271
|
+
{ value: 'skip', label: 'Pular os que já existem' },
|
|
272
|
+
],
|
|
273
|
+
})
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Execução
|
|
278
|
+
const s = p.spinner();
|
|
279
|
+
s.start('Instalando...');
|
|
280
|
+
|
|
281
|
+
const results = { created: [], overwritten: [], skipped: [] };
|
|
282
|
+
for (const cmd of selected) {
|
|
283
|
+
const status = installCommand(cmd, repoPath, () => conflictPolicy);
|
|
284
|
+
results[status].push(cmd.title);
|
|
285
|
+
}
|
|
286
|
+
if (wantMaster) {
|
|
287
|
+
const status = installMaster(catalog, repoPath, () => conflictPolicy);
|
|
288
|
+
results[status].push(catalog.master.title);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const skillResults = { created: [], overwritten: [], skipped: [] };
|
|
292
|
+
for (const skill of neededSkills) {
|
|
293
|
+
const status = installSkill(skill, repoPath, () => conflictPolicy);
|
|
294
|
+
skillResults[status].push(skill.title);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let claudeResult = null;
|
|
298
|
+
if (wantClaude) {
|
|
299
|
+
claudeResult = installClaudeMd(catalog, repoPath, claudeExists ? claudeMode : 'create');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
s.stop('Instalação concluída.');
|
|
303
|
+
|
|
304
|
+
// Resumo
|
|
305
|
+
const lines = [];
|
|
306
|
+
if (results.created.length) lines.push(`Criados (${results.created.length}): ${results.created.join(', ')}`);
|
|
307
|
+
if (results.overwritten.length) lines.push(`Sobrescritos (${results.overwritten.length}): ${results.overwritten.join(', ')}`);
|
|
308
|
+
if (results.skipped.length) lines.push(`Pulados (${results.skipped.length}): ${results.skipped.join(', ')}`);
|
|
309
|
+
const skillsInstalled = [...skillResults.created, ...skillResults.overwritten];
|
|
310
|
+
if (skillsInstalled.length) lines.push(`Skills (${skillsInstalled.length}): ${skillsInstalled.join(', ')}`);
|
|
311
|
+
if (skillResults.skipped.length) lines.push(`Skills puladas (${skillResults.skipped.length}): ${skillResults.skipped.join(', ')}`);
|
|
312
|
+
if (autoAdded.length) lines.push(`Adicionados como dependência (${autoAdded.length}): ${autoAdded.join(', ')}`);
|
|
313
|
+
if (claudeResult) lines.push(`CLAUDE.md: ${claudeResult}`);
|
|
314
|
+
p.note(lines.join('\n') || 'Nada a instalar.', `Resumo · ${repoPath}`);
|
|
315
|
+
|
|
316
|
+
p.outro('Abra o projeto no Claude Code e digite "/" para ver os comandos instalados.');
|
|
317
|
+
}
|
package/src/installer.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { cpSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { templatesDir } from './catalog.js';
|
|
4
|
+
|
|
5
|
+
/** Garante que o diretório de um arquivo exista. */
|
|
6
|
+
function ensureDir(filePath) {
|
|
7
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Copia um comando (template) para <repo>/.claude/commands/<id>.md.
|
|
12
|
+
* Retorna o status da operação: 'created' | 'overwritten' | 'skipped'.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} cmd Item do catálogo (precisa de id e file).
|
|
15
|
+
* @param {string} repoPath Raiz do repositório alvo.
|
|
16
|
+
* @param {function} resolveConflict (destPath) => 'overwrite' | 'skip'. Chamado só se o destino existir.
|
|
17
|
+
*/
|
|
18
|
+
export function installCommand(cmd, repoPath, resolveConflict) {
|
|
19
|
+
const source = join(templatesDir, cmd.file);
|
|
20
|
+
const dest = join(repoPath, '.claude', 'commands', `${cmd.id}.md`);
|
|
21
|
+
|
|
22
|
+
if (existsSync(dest)) {
|
|
23
|
+
const decision = resolveConflict ? resolveConflict(dest) : 'overwrite';
|
|
24
|
+
if (decision === 'skip') return 'skipped';
|
|
25
|
+
ensureDir(dest);
|
|
26
|
+
copyFileSync(source, dest);
|
|
27
|
+
return 'overwritten';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
ensureDir(dest);
|
|
31
|
+
copyFileSync(source, dest);
|
|
32
|
+
return 'created';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Instala/atualiza o CLAUDE.md base na raiz do repositório.
|
|
37
|
+
* mode: 'overwrite' | 'skip' | 'backup' (cria CLAUDE.md.bak antes de sobrescrever).
|
|
38
|
+
* Retorna 'created' | 'overwritten' | 'skipped' | 'backed-up'.
|
|
39
|
+
*/
|
|
40
|
+
export function installClaudeMd(catalog, repoPath, mode = 'overwrite') {
|
|
41
|
+
const source = join(templatesDir, catalog.claudeMd.file);
|
|
42
|
+
const dest = join(repoPath, catalog.claudeMd.target);
|
|
43
|
+
|
|
44
|
+
if (!existsSync(dest)) {
|
|
45
|
+
writeFileSync(dest, readFileSync(source, 'utf8'));
|
|
46
|
+
return 'created';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (mode === 'skip') return 'skipped';
|
|
50
|
+
|
|
51
|
+
if (mode === 'backup') {
|
|
52
|
+
copyFileSync(dest, `${dest}.bak`);
|
|
53
|
+
writeFileSync(dest, readFileSync(source, 'utf8'));
|
|
54
|
+
return 'backed-up';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
writeFileSync(dest, readFileSync(source, 'utf8'));
|
|
58
|
+
return 'overwritten';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Copia uma skill (pasta com SKILL.md e references/) para
|
|
63
|
+
* <repo>/.claude/skills/<id>/. Retorna 'created' | 'overwritten' | 'skipped'.
|
|
64
|
+
*
|
|
65
|
+
* @param {object} skill Item do catálogo (precisa de id e dir).
|
|
66
|
+
* @param {string} repoPath Raiz do repositório alvo.
|
|
67
|
+
* @param {function} resolveConflict (destPath) => 'overwrite' | 'skip'. Chamado só se o destino existir.
|
|
68
|
+
*/
|
|
69
|
+
export function installSkill(skill, repoPath, resolveConflict) {
|
|
70
|
+
const source = join(templatesDir, skill.dir);
|
|
71
|
+
const dest = join(repoPath, '.claude', 'skills', skill.id);
|
|
72
|
+
|
|
73
|
+
if (existsSync(dest)) {
|
|
74
|
+
const decision = resolveConflict ? resolveConflict(dest) : 'overwrite';
|
|
75
|
+
if (decision === 'skip') return 'skipped';
|
|
76
|
+
cpSync(source, dest, { recursive: true });
|
|
77
|
+
return 'overwritten';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
81
|
+
cpSync(source, dest, { recursive: true });
|
|
82
|
+
return 'created';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Instala o comando-mestre /harness (mesmo mecanismo de installCommand). */
|
|
86
|
+
export function installMaster(catalog, repoPath, resolveConflict) {
|
|
87
|
+
return installCommand(
|
|
88
|
+
{ id: catalog.master.id, file: catalog.master.file },
|
|
89
|
+
repoPath,
|
|
90
|
+
resolveConflict
|
|
91
|
+
);
|
|
92
|
+
}
|
package/src/run.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { loadCatalog, allCommands } from './catalog.js';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
/** Raiz do pacote — é ela que vira o plugin carregado via `claude --plugin-dir`. */
|
|
10
|
+
export const pluginRoot = join(__dirname, '..');
|
|
11
|
+
|
|
12
|
+
/** Nome do plugin, lido do manifesto para não duplicar a string. */
|
|
13
|
+
export function pluginName() {
|
|
14
|
+
const manifest = JSON.parse(
|
|
15
|
+
readFileSync(join(pluginRoot, '.claude-plugin', 'plugin.json'), 'utf8')
|
|
16
|
+
);
|
|
17
|
+
return manifest.name;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Ids executáveis: os comandos dos grupos mais o comando-mestre, que fica fora
|
|
22
|
+
* de `groups` no catálogo.
|
|
23
|
+
*/
|
|
24
|
+
export function runnableIds(catalog = loadCatalog()) {
|
|
25
|
+
const ids = allCommands(catalog).map((cmd) => cmd.id);
|
|
26
|
+
if (catalog.master?.id) ids.unshift(catalog.master.id);
|
|
27
|
+
return [...new Set(ids)];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Ids mais parecidos com o que foi digitado, para sugerir em caso de erro. */
|
|
31
|
+
export function suggestIds(input, ids) {
|
|
32
|
+
const needle = input.toLowerCase();
|
|
33
|
+
return ids
|
|
34
|
+
.filter((id) => id.includes(needle) || needle.includes(id) || id.startsWith(needle.slice(0, 4)))
|
|
35
|
+
.slice(0, 5);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SYSTEM_NOTE =
|
|
39
|
+
'Os slash commands do harness Fontdata estão disponíveis nesta sessão com o prefixo ' +
|
|
40
|
+
'`/PLUGIN:` (ex.: `/PLUGIN:gerar-spec`). Quando uma instrução referenciar um comando ' +
|
|
41
|
+
'como `/nome`, entenda que se trata de `/PLUGIN:nome`.';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Executa um comando do catálogo abrindo o Claude Code na pasta atual, com o
|
|
45
|
+
* pacote carregado como plugin da sessão — sem instalar nada no projeto.
|
|
46
|
+
*/
|
|
47
|
+
export function runCommand(id, args = [], claudeFlags = []) {
|
|
48
|
+
const catalog = loadCatalog();
|
|
49
|
+
const ids = runnableIds(catalog);
|
|
50
|
+
const wanted = id.replace(/^\//, '');
|
|
51
|
+
|
|
52
|
+
if (!ids.includes(wanted)) {
|
|
53
|
+
const near = suggestIds(wanted, ids);
|
|
54
|
+
console.error(`Comando desconhecido: ${wanted}`);
|
|
55
|
+
if (near.length) console.error(`\nVocê quis dizer: ${near.join(', ')}`);
|
|
56
|
+
console.error('\nRode `npx @fontdata/ai-harness list` para ver o catálogo completo.');
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const name = pluginName();
|
|
61
|
+
const prompt = [`/${name}:${wanted}`, ...args].join(' ').trim();
|
|
62
|
+
const argv = [
|
|
63
|
+
'--plugin-dir',
|
|
64
|
+
pluginRoot,
|
|
65
|
+
'--append-system-prompt',
|
|
66
|
+
SYSTEM_NOTE.replaceAll('PLUGIN', name),
|
|
67
|
+
...claudeFlags,
|
|
68
|
+
'--',
|
|
69
|
+
prompt,
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const result = spawn(argv);
|
|
73
|
+
|
|
74
|
+
if (result.error?.code === 'ENOENT') {
|
|
75
|
+
console.error(
|
|
76
|
+
'Claude Code não encontrado no PATH.\n' +
|
|
77
|
+
'Instale com: npm install -g @anthropic-ai/claude-code'
|
|
78
|
+
);
|
|
79
|
+
return 127;
|
|
80
|
+
}
|
|
81
|
+
if (result.error) {
|
|
82
|
+
console.error('Erro ao executar o Claude Code:', result.error.message);
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
return result.status ?? 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* No Windows o `claude` costuma ser um `.cmd`, que o spawn sem shell não
|
|
90
|
+
* resolve; nesse caso repetimos a chamada pelo shell, com os argumentos citados.
|
|
91
|
+
*/
|
|
92
|
+
function spawn(argv) {
|
|
93
|
+
const direct = spawnSync('claude', argv, { stdio: 'inherit', cwd: process.cwd() });
|
|
94
|
+
if (direct.error?.code === 'ENOENT' && process.platform === 'win32') {
|
|
95
|
+
const quoted = argv.map((arg) => `"${String(arg).replaceAll('"', '\\"')}"`).join(' ');
|
|
96
|
+
return spawnSync(`claude ${quoted}`, { stdio: 'inherit', cwd: process.cwd(), shell: true });
|
|
97
|
+
}
|
|
98
|
+
return direct;
|
|
99
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# [NOME DO PROJETO]
|
|
2
|
+
> Substitua esta seção rodando `/gerar-claude-md` depois de gerar o `SPEC.md`.
|
|
3
|
+
|
|
4
|
+
Este projeto usa o **harness de engenharia com IA da Fontdata** para Claude Code.
|
|
5
|
+
Os slash commands ficam em `.claude/commands/`. Comece por `/validar-ideia` (ou `/harness`).
|
|
6
|
+
|
|
7
|
+
## Filosofia de trabalho
|
|
8
|
+
|
|
9
|
+
- **Documentação como contrato.** `SPEC.md` (raiz), `CLAUDE.md` (raiz) e os planos em `docs/plans/`
|
|
10
|
+
são a fonte da verdade. Agents não os reescrevem por conta própria.
|
|
11
|
+
- **TDD obrigatório.** Toda task escreve os testes primeiro (RED), confirma que falham, e só então
|
|
12
|
+
implementa o mínimo para passar (GREEN). Mínimo de 2 testes críticos por task.
|
|
13
|
+
- **Orquestração com sub-agents.** Tasks independentes da mesma fase rodam em paralelo. O orquestrador
|
|
14
|
+
delega, não implementa. Gates entre fases: só avança quando os testes da fase passam.
|
|
15
|
+
- **Sempre relatar qual agent está ativo** no chat, principalmente nos gates entre agents.
|
|
16
|
+
|
|
17
|
+
## Modelo de arquivos
|
|
18
|
+
|
|
19
|
+
| Arquivo | Local | Papel |
|
|
20
|
+
|--------|-------|-------|
|
|
21
|
+
| `SPEC.md` | raiz | Definição **principal** e completa do projeto (única). |
|
|
22
|
+
| `CLAUDE.md` | raiz | Este context file, lido em toda sessão. |
|
|
23
|
+
| `docs/specs/AAAA-MM-DD-*.md` | `docs/specs/` | Specs **complementares**: o start inicial e cada nova tarefa. |
|
|
24
|
+
| `docs/plans/AAAA-MM-DD-*.md` | `docs/plans/` | Plano de implementação correspondente a cada spec complementar. |
|
|
25
|
+
| `docs/bugs/AAAA-MM-DD-*.md` | `docs/bugs/` | Correção de defeito: sintoma, causa-raiz e fix (via `/resolver-bug`). |
|
|
26
|
+
| `docs/investigacoes/AAAA-MM-DD-*.md` | `docs/investigacoes/` | Investigação, medição, auditoria, paridade e comparação (via `/auditar-*`, `/codigo-morto`). |
|
|
27
|
+
|
|
28
|
+
## Fluxo recomendado (projeto novo)
|
|
29
|
+
|
|
30
|
+
1. `/validar-ideia` — confere se cabe em ~4h com multi-agents
|
|
31
|
+
2. `/gerar-spec` — gera o `SPEC.md` principal na raiz
|
|
32
|
+
3. `/gerar-claude-md` — gera/atualiza este arquivo
|
|
33
|
+
4. `/gerar-ignore` — `.claudeignore` e `.gitignore`
|
|
34
|
+
5. `/nova-tarefa` — spec do start (derivada do SPEC) + plano em `docs/`
|
|
35
|
+
6. `/scaffold` — estrutura inicial + `/health` + testes de smoke
|
|
36
|
+
7. `/criar-agents`, `/criar-code-reviewer`, `/criar-rules`, `/criar-hooks` — monta o harness
|
|
37
|
+
8. `/executar-plan` — orquestra a implementação
|
|
38
|
+
|
|
39
|
+
No dia a dia: `/nova-tarefa`, `/resolver-bug`, `/git-commit-push`.
|
|
40
|
+
|
|
41
|
+
## Nunca fazer
|
|
42
|
+
|
|
43
|
+
- Nunca implementar sem teste escrito antes.
|
|
44
|
+
- Nunca avançar de fase com gate (testes) falhando.
|
|
45
|
+
- Nunca reescrever `SPEC.md`/`CLAUDE.md`/planos sem pedido explícito.
|
|
46
|
+
- Nunca usar `git push --force` ou comandos destrutivos sem confirmação.
|
|
47
|
+
- Nunca comentar o passo a passo do código, decorar arquivo com banner de seção, nem escrever
|
|
48
|
+
task/fase/plano/data/hash de commit dentro de comentário — isso vai na mensagem de commit e em `docs/`.
|
|
49
|
+
Rode `/configurar-comentarios` para instalar a rule, o hook e a catraca.
|