@connsoft-tech/claude-init 1.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 +10 -0
- package/LICENSE +21 -0
- package/README.md +193 -0
- package/bin/cli.js +892 -0
- package/commands/claude-init.md +141 -0
- package/package.json +39 -0
- package/templates/.claude/agents/backend-implementer.md.tpl +30 -0
- package/templates/.claude/agents/db-migrator.md.tpl +27 -0
- package/templates/.claude/agents/frontend-implementer.md.tpl +26 -0
- package/templates/.claude/agents/implementer.md.tpl +27 -0
- package/templates/.claude/agents/orchestrator.md.tpl +85 -0
- package/templates/.claude/agents/queue-worker.md.tpl +20 -0
- package/templates/.claude/agents/reviewer.md.tpl +18 -0
- package/templates/.claude/commands/diagrama.md.tpl +20 -0
- package/templates/.claude/commands/finalizar.md.tpl +12 -0
- package/templates/.claude/commands/nova-implementacao.md.tpl +15 -0
- package/templates/.claude/commands/onboarding.md.tpl +25 -0
- package/templates/.claude/commands/registrar-decisao.md.tpl +15 -0
- package/templates/.claude/commands/versao.md.tpl +34 -0
- package/templates/.claude/hooks/guard-git-safety.sh.tpl +33 -0
- package/templates/.claude/hooks/guard-migration-rollback.sh.tpl +35 -0
- package/templates/.claude/layer/CLAUDE.layer.md.tpl +23 -0
- package/templates/.claude/rules/convencoes.md.tpl +7 -0
- package/templates/.claude/rules/registro-decisoes.md.tpl +18 -0
- package/templates/.claude/rules/stack.md.tpl +6 -0
- package/templates/.github/workflows/auto-tag.yml.tpl +31 -0
- package/templates/CLAUDE.root.md.tpl +41 -0
- package/templates/app/CLAUDE.app.md.tpl +21 -0
- package/templates/docs/architecture/README.md.tpl +11 -0
- package/templates/docs/architecture/decisions.md.tpl +23 -0
- package/templates/docs/architecture/visao-geral.md.tpl +17 -0
- package/templates/specs/README.md.tpl +32 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* claude-init
|
|
6
|
+
* Cria a estrutura .claude/ + CLAUDE.md em camadas + docs/architecture
|
|
7
|
+
* dentro do repositório atual, para QUALQUER stack (não depende de Node
|
|
8
|
+
* no projeto alvo — este CLI só usa Node para gerar os arquivos).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const fs = require("fs-extra");
|
|
13
|
+
const prompts = require("prompts");
|
|
14
|
+
const { execSync } = require("child_process");
|
|
15
|
+
|
|
16
|
+
const CWD = process.cwd();
|
|
17
|
+
const TEMPLATES_DIR = path.join(__dirname, "..", "templates");
|
|
18
|
+
|
|
19
|
+
function fillPlaceholders(content, data) {
|
|
20
|
+
return content.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
21
|
+
if (data[key] === undefined) return "";
|
|
22
|
+
return data[key];
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Palavras-chave usadas para detectar quais agentes de camada faz sentido
|
|
27
|
+
// gerar, a partir das respostas livres de stack. Genérico — não assume
|
|
28
|
+
// nenhuma stack fixa.
|
|
29
|
+
const STACK_DETECTORS = [
|
|
30
|
+
{
|
|
31
|
+
template: ".claude/agents/backend-implementer.md.tpl",
|
|
32
|
+
target: "backend-implementer.md",
|
|
33
|
+
field: "language",
|
|
34
|
+
desc: "implementação de lógica de backend e API",
|
|
35
|
+
keywords: [
|
|
36
|
+
"laravel", "php", "nestjs", "node", "express", "django", "flask",
|
|
37
|
+
"python", "rails", "ruby", ".net", "dotnet", "c#", "spring",
|
|
38
|
+
"java", "golang", "fastapi", "symfony",
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
template: ".claude/agents/frontend-implementer.md.tpl",
|
|
43
|
+
target: "frontend-implementer.md",
|
|
44
|
+
field: "language",
|
|
45
|
+
desc: "implementação de UI e integração com API",
|
|
46
|
+
keywords: [
|
|
47
|
+
"react", "vue", "angular", "svelte", "next.js", "nextjs", "nuxt",
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
template: ".claude/agents/db-migrator.md.tpl",
|
|
52
|
+
target: "db-migrator.md",
|
|
53
|
+
field: "database",
|
|
54
|
+
desc: "migrations e mudanças de schema no banco",
|
|
55
|
+
keywords: [
|
|
56
|
+
"postgres", "postgresql", "mysql", "mongodb", "mongo", "sqlite",
|
|
57
|
+
"sql server", "mariadb", "oracle",
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
template: ".claude/agents/queue-worker.md.tpl",
|
|
62
|
+
target: "queue-worker.md",
|
|
63
|
+
field: "messaging",
|
|
64
|
+
desc: "publishers/consumers de fila e contratos de eventos",
|
|
65
|
+
keywords: ["rabbitmq", "kafka", "sqs", "redis", "nats", "activemq"],
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
function detectAgents(answers) {
|
|
70
|
+
const combinedText = [answers.language, answers.database, answers.messaging]
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
.join(" ")
|
|
73
|
+
.toLowerCase();
|
|
74
|
+
|
|
75
|
+
const matched = [];
|
|
76
|
+
for (const detector of STACK_DETECTORS) {
|
|
77
|
+
if (detector.keywords.some((kw) => combinedText.includes(kw))) {
|
|
78
|
+
matched.push(detector);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return matched;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Nome de exibição do framework de backend detectado, por palavra-chave.
|
|
85
|
+
const BACKEND_FRAMEWORK_NAMES = {
|
|
86
|
+
laravel: "Laravel",
|
|
87
|
+
php: "PHP",
|
|
88
|
+
nestjs: "NestJS",
|
|
89
|
+
node: "Node",
|
|
90
|
+
express: "Express",
|
|
91
|
+
django: "Django",
|
|
92
|
+
flask: "Flask",
|
|
93
|
+
python: "Python",
|
|
94
|
+
rails: "Rails",
|
|
95
|
+
ruby: "Ruby",
|
|
96
|
+
".net": ".NET",
|
|
97
|
+
"c#": ".NET",
|
|
98
|
+
spring: "Spring",
|
|
99
|
+
java: "Java",
|
|
100
|
+
golang: "Go",
|
|
101
|
+
fastapi: "FastAPI",
|
|
102
|
+
symfony: "Symfony",
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function detectBackendFramework(combinedText) {
|
|
106
|
+
for (const [kw, name] of Object.entries(BACKEND_FRAMEWORK_NAMES)) {
|
|
107
|
+
if (combinedText.includes(kw)) return name;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Convenções de pastas conhecidas por framework — adicione novas conforme
|
|
113
|
+
// o time usar outras stacks. Fallback genérico cobre o resto.
|
|
114
|
+
const FRAMEWORK_LAYER_PATHS = {
|
|
115
|
+
Laravel: {
|
|
116
|
+
models: "app/Models",
|
|
117
|
+
controllers: "app/Http/Controllers",
|
|
118
|
+
services: "app/Services",
|
|
119
|
+
repositories: "app/Repositories",
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
const GENERIC_LAYER_PATHS = {
|
|
123
|
+
models: "src/models",
|
|
124
|
+
controllers: "src/controllers",
|
|
125
|
+
services: "src/services",
|
|
126
|
+
repositories: "src/repositories",
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
function buildLayersSection(layerPaths) {
|
|
130
|
+
const lines = Object.values(layerPaths).map(
|
|
131
|
+
(relPath) => `- @${relPath}/CLAUDE.md`
|
|
132
|
+
);
|
|
133
|
+
return `## Camadas\n${lines.join("\n")}\n`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Skills populares de terceiros — só sugeridas quando fazem sentido pra
|
|
137
|
+
// stack detectada no projeto (cada projeto/dev pode ter uma diferente).
|
|
138
|
+
const SKILL_CATALOG = [
|
|
139
|
+
{
|
|
140
|
+
id: "grill-me",
|
|
141
|
+
repo: "mattpocock/skills",
|
|
142
|
+
skill: "grill-me",
|
|
143
|
+
desc: "interroga você uma pergunta por vez antes de travar um plano/design",
|
|
144
|
+
universal: true,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "terms",
|
|
148
|
+
repo: "Code-Shock/claude-skills",
|
|
149
|
+
skill: "terms",
|
|
150
|
+
desc: "mantém glossário de domínio (CONTEXT.md) e ADRs",
|
|
151
|
+
universal: true,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "e2e-setup",
|
|
155
|
+
repo: "Code-Shock/claude-skills",
|
|
156
|
+
skill: "e2e-setup",
|
|
157
|
+
desc: "scaffold de testes E2E com Playwright (stacks JS/TS)",
|
|
158
|
+
requiresFrontend: true,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: "code-quality",
|
|
162
|
+
repo: "Code-Shock/claude-skills",
|
|
163
|
+
skill: "code-quality",
|
|
164
|
+
desc: "baseline Prettier/ESLint/husky/CI gate (stacks JS/TS)",
|
|
165
|
+
requiresFrontend: true,
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
function getApplicableSkills(detectedAgents) {
|
|
170
|
+
const hasFrontend = detectedAgents.some(
|
|
171
|
+
(a) => a.target === "frontend-implementer.md"
|
|
172
|
+
);
|
|
173
|
+
return SKILL_CATALOG.filter(
|
|
174
|
+
(s) => s.universal || (s.requiresFrontend && hasFrontend)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Texto do passo final (commit + merge OU commit + PR), conforme escolha
|
|
179
|
+
// do dev. Usado tanto no orchestrator quanto no /finalizar.
|
|
180
|
+
function buildFinalizeBlock(mergeStrategy, devBranch) {
|
|
181
|
+
if (mergeStrategy === "pr") {
|
|
182
|
+
return {
|
|
183
|
+
title: "commit e abrir Pull Request",
|
|
184
|
+
body:
|
|
185
|
+
`com o \`reviewer\` aprovado: marcar as subtarefas concluídas em ` +
|
|
186
|
+
`\`tasks.md\`, commit na branch \`feature/<slug>\` com mensagem clara, ` +
|
|
187
|
+
`\`git push -u origin feature/<slug>\`, e abrir Pull Request pra ` +
|
|
188
|
+
`\`${devBranch}\` via \`gh pr create --base ${devBranch} --title "<título>" ` +
|
|
189
|
+
`--body "<resumo de requirements.md e tasks.md>"\`. **Não fazer merge ` +
|
|
190
|
+
`sozinho** — aguardar aprovação humana do PR antes de entrar em ${devBranch}.`,
|
|
191
|
+
avoidLine: `Não faça merge de um PR sozinho — isso é decisão humana; sua responsabilidade termina em abrir o PR com o \`reviewer\` aprovado.`,
|
|
192
|
+
finalizarSteps:
|
|
193
|
+
`5. Commit na branch atual com mensagem clara resumindo as mudanças.\n` +
|
|
194
|
+
`6. \`git push -u origin feature/<slug>\`.\n` +
|
|
195
|
+
`7. Abrir Pull Request pra \`${devBranch}\`: \`gh pr create --base ${devBranch} ` +
|
|
196
|
+
`--title "<título>" --body "<resumo>"\`. Não fazer merge sozinho.`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
title: "commit e merge",
|
|
201
|
+
body:
|
|
202
|
+
`com o \`reviewer\` aprovado: marcar as subtarefas concluídas em ` +
|
|
203
|
+
`\`tasks.md\`, fazer o commit na branch \`feature/<slug>\` com mensagem ` +
|
|
204
|
+
`clara, depois \`git checkout ${devBranch}\`, \`git pull\`, ` +
|
|
205
|
+
`\`git merge --no-ff feature/<slug>\`, \`git push\`. Deletar a branch ` +
|
|
206
|
+
`\`feature/<slug>\` local após o merge (\`git branch -d\`).`,
|
|
207
|
+
avoidLine: `Não faça merge em ${devBranch} sem o \`reviewer\` ter passado.`,
|
|
208
|
+
finalizarSteps:
|
|
209
|
+
`5. Commit na branch atual com mensagem clara resumindo as mudanças.\n` +
|
|
210
|
+
`6. \`git checkout ${devBranch}\`, \`git pull\`, \`git merge --no-ff ` +
|
|
211
|
+
`feature/<slug>\`, \`git push\`.\n` +
|
|
212
|
+
`7. Deletar a branch \`feature/<slug>\` local (\`git branch -d\`).`,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function writeFromTemplate(templateRelPath, targetAbsPath, data) {
|
|
217
|
+
const templatePath = path.join(TEMPLATES_DIR, templateRelPath);
|
|
218
|
+
const raw = await fs.readFile(templatePath, "utf8");
|
|
219
|
+
const filled = fillPlaceholders(raw, data);
|
|
220
|
+
await fs.ensureDir(path.dirname(targetAbsPath));
|
|
221
|
+
|
|
222
|
+
const exists = await fs.pathExists(targetAbsPath);
|
|
223
|
+
if (exists) {
|
|
224
|
+
console.log(` já existe, pulando: ${path.relative(CWD, targetAbsPath)}`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
await fs.writeFile(targetAbsPath, filled, "utf8");
|
|
228
|
+
console.log(` criado: ${path.relative(CWD, targetAbsPath)}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Detecta sinais de projeto NOVO vs EXISTENTE no diretório atual, pra
|
|
232
|
+
// usar como sugestão nas perguntas — nunca decide sozinho, só pré-preenche.
|
|
233
|
+
async function detectProjectContext(cwd) {
|
|
234
|
+
const ctx = {
|
|
235
|
+
isExisting: false,
|
|
236
|
+
signals: [],
|
|
237
|
+
suggestedLanguage: null,
|
|
238
|
+
suggestedDatabase: null,
|
|
239
|
+
suggestedMonorepo: null,
|
|
240
|
+
suggestedApps: [],
|
|
241
|
+
suggestedDevBranch: null,
|
|
242
|
+
existingClaudeFiles: [],
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const entries = await fs.readdir(cwd).catch(() => []);
|
|
246
|
+
const meaningful = entries.filter((e) => !["/.git", ".git", "node_modules"].includes(e));
|
|
247
|
+
if (meaningful.length > 0) {
|
|
248
|
+
ctx.isExisting = true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// git: branch atual como sugestão de devBranch
|
|
252
|
+
try {
|
|
253
|
+
const branch = execSync("git branch --show-current", { cwd, stdio: ["ignore", "pipe", "ignore"] })
|
|
254
|
+
.toString()
|
|
255
|
+
.trim();
|
|
256
|
+
if (branch) {
|
|
257
|
+
ctx.suggestedDevBranch = branch;
|
|
258
|
+
ctx.signals.push(`branch git atual: ${branch}`);
|
|
259
|
+
}
|
|
260
|
+
} catch (_) {
|
|
261
|
+
/* não é um repo git ainda, ou git indisponível — segue sem sugestão */
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// package.json — Node/JS: tenta adivinhar frontend/backend pelas deps
|
|
265
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
266
|
+
if (await fs.pathExists(pkgPath)) {
|
|
267
|
+
ctx.isExisting = true;
|
|
268
|
+
try {
|
|
269
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf8"));
|
|
270
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
271
|
+
const found = [];
|
|
272
|
+
if (deps.react) found.push("React");
|
|
273
|
+
if (deps.vue) found.push("Vue");
|
|
274
|
+
if (deps["@angular/core"]) found.push("Angular");
|
|
275
|
+
if (deps.next) found.push("Next.js");
|
|
276
|
+
if (deps.express) found.push("Express");
|
|
277
|
+
if (deps["@nestjs/core"]) found.push("NestJS");
|
|
278
|
+
if (found.length) {
|
|
279
|
+
ctx.suggestedLanguage = found.join(" e ");
|
|
280
|
+
ctx.signals.push(`package.json com ${found.join(", ")}`);
|
|
281
|
+
} else {
|
|
282
|
+
ctx.signals.push("package.json (Node) encontrado, sem framework reconhecido nas deps");
|
|
283
|
+
}
|
|
284
|
+
} catch (_) {
|
|
285
|
+
/* package.json inválido — ignora */
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// composer.json — PHP/Laravel
|
|
290
|
+
const composerPath = path.join(cwd, "composer.json");
|
|
291
|
+
if (await fs.pathExists(composerPath)) {
|
|
292
|
+
ctx.isExisting = true;
|
|
293
|
+
try {
|
|
294
|
+
const composer = JSON.parse(await fs.readFile(composerPath, "utf8"));
|
|
295
|
+
const require_ = { ...composer.require, ...composer["require-dev"] };
|
|
296
|
+
if (require_["laravel/framework"]) {
|
|
297
|
+
ctx.suggestedLanguage = ctx.suggestedLanguage
|
|
298
|
+
? `${ctx.suggestedLanguage} e Laravel/PHP`
|
|
299
|
+
: "Laravel/PHP";
|
|
300
|
+
ctx.signals.push("composer.json com laravel/framework");
|
|
301
|
+
} else {
|
|
302
|
+
ctx.signals.push("composer.json (PHP) encontrado, sem Laravel reconhecido");
|
|
303
|
+
}
|
|
304
|
+
} catch (_) {
|
|
305
|
+
/* composer.json inválido — ignora */
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// requirements.txt / pyproject.toml — Python
|
|
310
|
+
if (
|
|
311
|
+
(await fs.pathExists(path.join(cwd, "requirements.txt"))) ||
|
|
312
|
+
(await fs.pathExists(path.join(cwd, "pyproject.toml")))
|
|
313
|
+
) {
|
|
314
|
+
ctx.isExisting = true;
|
|
315
|
+
ctx.suggestedLanguage = ctx.suggestedLanguage ? `${ctx.suggestedLanguage} e Python` : "Python";
|
|
316
|
+
ctx.signals.push("manifesto Python encontrado (requirements.txt/pyproject.toml)");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// go.mod — Go
|
|
320
|
+
if (await fs.pathExists(path.join(cwd, "go.mod"))) {
|
|
321
|
+
ctx.isExisting = true;
|
|
322
|
+
ctx.suggestedLanguage = ctx.suggestedLanguage ? `${ctx.suggestedLanguage} e Go` : "Go";
|
|
323
|
+
ctx.signals.push("go.mod encontrado");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// docker-compose.yml — tenta achar Postgres/MySQL/RabbitMQ nos serviços
|
|
327
|
+
const composePath = path.join(cwd, "docker-compose.yml");
|
|
328
|
+
if (await fs.pathExists(composePath)) {
|
|
329
|
+
try {
|
|
330
|
+
const compose = (await fs.readFile(composePath, "utf8")).toLowerCase();
|
|
331
|
+
const dbFound = [];
|
|
332
|
+
if (compose.includes("postgres")) dbFound.push("PostgreSQL");
|
|
333
|
+
if (compose.includes("mysql") || compose.includes("mariadb")) dbFound.push("MySQL");
|
|
334
|
+
if (compose.includes("mongo")) dbFound.push("MongoDB");
|
|
335
|
+
if (dbFound.length) {
|
|
336
|
+
ctx.suggestedDatabase = dbFound.join(" e ");
|
|
337
|
+
ctx.signals.push(`docker-compose.yml com ${dbFound.join(", ")}`);
|
|
338
|
+
}
|
|
339
|
+
if (compose.includes("rabbitmq")) {
|
|
340
|
+
ctx.signals.push("docker-compose.yml com RabbitMQ");
|
|
341
|
+
}
|
|
342
|
+
} catch (_) {
|
|
343
|
+
/* docker-compose.yml ilegível — ignora */
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// apps/ ou packages/ — sinal de monorepo, com nomes reais das pastas
|
|
348
|
+
for (const dirName of ["apps", "packages"]) {
|
|
349
|
+
const dirPath = path.join(cwd, dirName);
|
|
350
|
+
if (await fs.pathExists(dirPath)) {
|
|
351
|
+
const sub = await fs.readdir(dirPath).catch(() => []);
|
|
352
|
+
const realDirs = [];
|
|
353
|
+
for (const s of sub) {
|
|
354
|
+
const stat = await fs.stat(path.join(dirPath, s)).catch(() => null);
|
|
355
|
+
if (stat && stat.isDirectory()) realDirs.push(s);
|
|
356
|
+
}
|
|
357
|
+
if (realDirs.length) {
|
|
358
|
+
ctx.suggestedMonorepo = true;
|
|
359
|
+
ctx.suggestedApps = realDirs;
|
|
360
|
+
ctx.signals.push(`pasta ${dirName}/ com: ${realDirs.join(", ")}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// .claude/ já existente — lista o que já tem, pra avisar antes de rodar
|
|
366
|
+
const claudeDir = path.join(cwd, ".claude");
|
|
367
|
+
if (await fs.pathExists(claudeDir)) {
|
|
368
|
+
for (const sub of ["agents", "commands", "rules", "hooks"]) {
|
|
369
|
+
const subDir = path.join(claudeDir, sub);
|
|
370
|
+
if (await fs.pathExists(subDir)) {
|
|
371
|
+
const files = await fs.readdir(subDir).catch(() => []);
|
|
372
|
+
if (files.length) ctx.existingClaudeFiles.push(`.claude/${sub}/ (${files.length} arquivo(s))`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (await fs.pathExists(path.join(claudeDir, "settings.json"))) {
|
|
376
|
+
ctx.existingClaudeFiles.push(".claude/settings.json");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (await fs.pathExists(path.join(cwd, "CLAUDE.md"))) {
|
|
380
|
+
ctx.existingClaudeFiles.push("CLAUDE.md");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return ctx;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function main() {
|
|
387
|
+
console.log("\n=== claude-init — setup de ambiente Claude Code ===\n");
|
|
388
|
+
|
|
389
|
+
const projectCtx = await detectProjectContext(CWD);
|
|
390
|
+
|
|
391
|
+
if (projectCtx.isExisting) {
|
|
392
|
+
console.log("Projeto existente detectado:");
|
|
393
|
+
if (projectCtx.signals.length) {
|
|
394
|
+
for (const s of projectCtx.signals) console.log(` - ${s}`);
|
|
395
|
+
} else {
|
|
396
|
+
console.log(" - pasta não vazia, sem manifesto de stack reconhecido");
|
|
397
|
+
}
|
|
398
|
+
if (projectCtx.existingClaudeFiles.length) {
|
|
399
|
+
console.log("\nJá existe configuração Claude Code neste projeto:");
|
|
400
|
+
for (const f of projectCtx.existingClaudeFiles) console.log(` - ${f}`);
|
|
401
|
+
console.log(" (nada será sobrescrito — arquivos existentes são pulados)");
|
|
402
|
+
}
|
|
403
|
+
console.log("\nUsando o que foi detectado como sugestão nas perguntas abaixo — corrija se estiver errado.\n");
|
|
404
|
+
} else {
|
|
405
|
+
console.log("Nenhum sinal de projeto existente — tratando como projeto novo.\n");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const answers = await prompts(
|
|
409
|
+
[
|
|
410
|
+
{
|
|
411
|
+
type: "text",
|
|
412
|
+
name: "projectName",
|
|
413
|
+
message: "Nome do projeto/produto",
|
|
414
|
+
initial: path.basename(CWD),
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
type: "confirm",
|
|
418
|
+
name: "isMonorepo",
|
|
419
|
+
message: "É um monorepo com múltiplos apps/pacotes?",
|
|
420
|
+
initial: projectCtx.suggestedMonorepo !== null ? projectCtx.suggestedMonorepo : true,
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
type: (prev) => (prev ? "list" : null),
|
|
424
|
+
name: "apps",
|
|
425
|
+
message:
|
|
426
|
+
"Liste os apps/pacotes separados por vírgula (ex: api, web, worker)",
|
|
427
|
+
separator: ",",
|
|
428
|
+
initial: projectCtx.suggestedApps.length ? projectCtx.suggestedApps.join(", ") : "",
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
type: "text",
|
|
432
|
+
name: "language",
|
|
433
|
+
message: "Linguagem/framework principal (ex: Laravel/PHP, Go, .NET)",
|
|
434
|
+
initial: projectCtx.suggestedLanguage || "",
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
type: "text",
|
|
438
|
+
name: "database",
|
|
439
|
+
message: "Banco de dados e padrão de arquitetura (ex: PostgreSQL multi-tenant)",
|
|
440
|
+
initial: projectCtx.suggestedDatabase || "",
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
type: "text",
|
|
444
|
+
name: "messaging",
|
|
445
|
+
message: "Mensageria/filas, se houver (ex: RabbitMQ) — deixe em branco se não usar",
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
type: "text",
|
|
449
|
+
name: "deploy",
|
|
450
|
+
message: "Como é feito o deploy (ex: Dokploy/Docker self-hosted)",
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
type: "text",
|
|
454
|
+
name: "devBranch",
|
|
455
|
+
message: "Qual é a branch principal de desenvolvimento (origem e destino do merge das features)?",
|
|
456
|
+
initial: projectCtx.suggestedDevBranch || "develop",
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
type: "text",
|
|
460
|
+
name: "releaseBranch",
|
|
461
|
+
message: "Qual branch dispara o deploy em produção (onde a tag de release deve ser criada)?",
|
|
462
|
+
initial: "main",
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
type: "confirm",
|
|
466
|
+
name: "addVersioning",
|
|
467
|
+
message: "Adicionar automação de versionamento (/versao + GitHub Action de auto-tag na branch de release)?",
|
|
468
|
+
initial: true,
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
type: "confirm",
|
|
472
|
+
name: "planModeDefault",
|
|
473
|
+
message: "Deixar o Claude Code sempre iniciar em Plan Mode neste projeto (defaultMode: \"plan\" em .claude/settings.json)?",
|
|
474
|
+
initial: true,
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
type: "confirm",
|
|
478
|
+
name: "addHooks",
|
|
479
|
+
message: "Adicionar hooks de segurança (bloqueia push forçado, bloqueia push direto na branch de release, bloqueia migration sem rollback)?",
|
|
480
|
+
initial: true,
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
type: "select",
|
|
484
|
+
name: "mergeStrategy",
|
|
485
|
+
message: "Ao finalizar uma implementação aprovada pelo reviewer, o orchestrator deve:",
|
|
486
|
+
choices: [
|
|
487
|
+
{
|
|
488
|
+
title: "Fazer merge direto na branch de desenvolvimento (sem revisão humana)",
|
|
489
|
+
value: "merge",
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
title: "Abrir Pull Request e parar (revisão humana antes de entrar na branch de desenvolvimento)",
|
|
493
|
+
value: "pr",
|
|
494
|
+
},
|
|
495
|
+
],
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
type: "multiselect",
|
|
499
|
+
name: "extras",
|
|
500
|
+
message: "O que mais deseja gerar?",
|
|
501
|
+
choices: [
|
|
502
|
+
{ title: "Subagentes de domínio (.claude/agents)", value: "agents", selected: true },
|
|
503
|
+
{ title: "Regras atômicas (.claude/rules)", value: "rules", selected: true },
|
|
504
|
+
{ title: "Slash commands base (.claude/commands)", value: "commands", selected: true },
|
|
505
|
+
{ title: "Esqueleto de docs/architecture", value: "docs", selected: true },
|
|
506
|
+
],
|
|
507
|
+
},
|
|
508
|
+
],
|
|
509
|
+
{
|
|
510
|
+
onCancel: () => {
|
|
511
|
+
console.log("\nCancelado.");
|
|
512
|
+
process.exit(1);
|
|
513
|
+
},
|
|
514
|
+
}
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
const apps =
|
|
518
|
+
answers.apps && answers.apps.length
|
|
519
|
+
? answers.apps.map((a) => a.trim()).filter(Boolean)
|
|
520
|
+
: [];
|
|
521
|
+
|
|
522
|
+
const combinedText = [answers.language, answers.database, answers.messaging]
|
|
523
|
+
.filter(Boolean)
|
|
524
|
+
.join(" ")
|
|
525
|
+
.toLowerCase();
|
|
526
|
+
const backendFramework = detectBackendFramework(combinedText);
|
|
527
|
+
|
|
528
|
+
// backendAppFolder === null significa "raiz do repo" (projeto não é
|
|
529
|
+
// monorepo, ou é monorepo mas só tem um app informado como app único).
|
|
530
|
+
let backendAppFolder = null;
|
|
531
|
+
let generateLayers = false;
|
|
532
|
+
let layerPaths = null;
|
|
533
|
+
let layersSectionText = "";
|
|
534
|
+
|
|
535
|
+
if (backendFramework && answers.extras.includes("agents")) {
|
|
536
|
+
if (answers.isMonorepo && apps.length > 1) {
|
|
537
|
+
const sel = await prompts(
|
|
538
|
+
{
|
|
539
|
+
type: "select",
|
|
540
|
+
name: "backendApp",
|
|
541
|
+
message: `Qual pasta é o backend (${backendFramework})?`,
|
|
542
|
+
choices: apps.map((a) => ({ title: a, value: a })),
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
onCancel: () => {
|
|
546
|
+
console.log("\nCancelado.");
|
|
547
|
+
process.exit(1);
|
|
548
|
+
},
|
|
549
|
+
}
|
|
550
|
+
);
|
|
551
|
+
backendAppFolder = sel.backendApp;
|
|
552
|
+
} else if (apps.length === 1) {
|
|
553
|
+
backendAppFolder = apps[0];
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const knownConvention = Boolean(FRAMEWORK_LAYER_PATHS[backendFramework]);
|
|
557
|
+
const confirmAnswer = await prompts(
|
|
558
|
+
{
|
|
559
|
+
type: "confirm",
|
|
560
|
+
name: "generateLayers",
|
|
561
|
+
message:
|
|
562
|
+
`Gerar CLAUDE.md por camada (models, controllers, services, repositories)` +
|
|
563
|
+
(knownConvention
|
|
564
|
+
? ` seguindo o padrão de pastas do ${backendFramework}?`
|
|
565
|
+
: ` em pastas genéricas (${backendFramework} ainda não tem convenção mapeada — você pode ajustar os caminhos depois)?`),
|
|
566
|
+
initial: true,
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
onCancel: () => {
|
|
570
|
+
console.log("\nCancelado.");
|
|
571
|
+
process.exit(1);
|
|
572
|
+
},
|
|
573
|
+
}
|
|
574
|
+
);
|
|
575
|
+
generateLayers = confirmAnswer.generateLayers;
|
|
576
|
+
|
|
577
|
+
if (generateLayers) {
|
|
578
|
+
layerPaths = FRAMEWORK_LAYER_PATHS[backendFramework] || GENERIC_LAYER_PATHS;
|
|
579
|
+
layersSectionText = buildLayersSection(layerPaths);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const data = {
|
|
584
|
+
PROJECT_NAME: answers.projectName,
|
|
585
|
+
LANGUAGE: answers.language || "[definir]",
|
|
586
|
+
DATABASE: answers.database || "[definir]",
|
|
587
|
+
MESSAGING: answers.messaging || "[não utilizado]",
|
|
588
|
+
DEPLOY: answers.deploy || "[definir]",
|
|
589
|
+
DEV_BRANCH: answers.devBranch || "develop",
|
|
590
|
+
RELEASE_BRANCH: answers.releaseBranch || "main",
|
|
591
|
+
APPS_LIST: apps.map((a) => `- \`${a}\``).join("\n") || "- (definir apps do monorepo)",
|
|
592
|
+
// Só entra no CLAUDE.md raiz se o backend for a própria raiz (sem apps/).
|
|
593
|
+
LAYERS_SECTION: backendAppFolder === null ? layersSectionText : "",
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
// Detecção de agentes é usada tanto na geração dos agents (mais abaixo)
|
|
597
|
+
// quanto para decidir se o hook de migration faz sentido aqui.
|
|
598
|
+
let detected = [];
|
|
599
|
+
|
|
600
|
+
console.log("\nGerando estrutura...\n");
|
|
601
|
+
|
|
602
|
+
// 1. CLAUDE.md raiz
|
|
603
|
+
await writeFromTemplate("CLAUDE.root.md.tpl", path.join(CWD, "CLAUDE.md"), data);
|
|
604
|
+
|
|
605
|
+
// 1.5 CLAUDE.md por camada do backend (models/controllers/services/repositories)
|
|
606
|
+
if (generateLayers && layerPaths) {
|
|
607
|
+
const baseDir = backendAppFolder ? path.join(CWD, "apps", backendAppFolder) : CWD;
|
|
608
|
+
for (const [layerKey, relPath] of Object.entries(layerPaths)) {
|
|
609
|
+
await writeFromTemplate(
|
|
610
|
+
".claude/layer/CLAUDE.layer.md.tpl",
|
|
611
|
+
path.join(baseDir, relPath, "CLAUDE.md"),
|
|
612
|
+
{ ...data, LAYER_NAME: layerKey, LAYER_PATH: relPath, FRAMEWORK: backendFramework }
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// 2. docs/architecture
|
|
618
|
+
if (answers.extras.includes("docs")) {
|
|
619
|
+
await writeFromTemplate(
|
|
620
|
+
"docs/architecture/README.md.tpl",
|
|
621
|
+
path.join(CWD, "docs", "architecture", "README.md"),
|
|
622
|
+
data
|
|
623
|
+
);
|
|
624
|
+
await writeFromTemplate(
|
|
625
|
+
"docs/architecture/visao-geral.md.tpl",
|
|
626
|
+
path.join(CWD, "docs", "architecture", "visao-geral.md"),
|
|
627
|
+
data
|
|
628
|
+
);
|
|
629
|
+
await writeFromTemplate(
|
|
630
|
+
"docs/architecture/decisions.md.tpl",
|
|
631
|
+
path.join(CWD, "docs", "architecture", "decisions.md"),
|
|
632
|
+
data
|
|
633
|
+
);
|
|
634
|
+
await writeFromTemplate(
|
|
635
|
+
"specs/README.md.tpl",
|
|
636
|
+
path.join(CWD, "specs", "README.md"),
|
|
637
|
+
data
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// 3. rules
|
|
642
|
+
if (answers.extras.includes("rules")) {
|
|
643
|
+
await writeFromTemplate(
|
|
644
|
+
".claude/rules/stack.md.tpl",
|
|
645
|
+
path.join(CWD, ".claude", "rules", "stack.md"),
|
|
646
|
+
data
|
|
647
|
+
);
|
|
648
|
+
await writeFromTemplate(
|
|
649
|
+
".claude/rules/convencoes.md.tpl",
|
|
650
|
+
path.join(CWD, ".claude", "rules", "convencoes.md"),
|
|
651
|
+
data
|
|
652
|
+
);
|
|
653
|
+
await writeFromTemplate(
|
|
654
|
+
".claude/rules/registro-decisoes.md.tpl",
|
|
655
|
+
path.join(CWD, ".claude", "rules", "registro-decisoes.md"),
|
|
656
|
+
data
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// 4. agents — detecta camadas pela stack informada; se nada bater,
|
|
661
|
+
// cai no genérico "implementer".
|
|
662
|
+
if (answers.extras.includes("agents")) {
|
|
663
|
+
detected = detectAgents(answers);
|
|
664
|
+
const agentListEntries = [];
|
|
665
|
+
|
|
666
|
+
if (detected.length === 0) {
|
|
667
|
+
await writeFromTemplate(
|
|
668
|
+
".claude/agents/implementer.md.tpl",
|
|
669
|
+
path.join(CWD, ".claude", "agents", "implementer.md"),
|
|
670
|
+
data
|
|
671
|
+
);
|
|
672
|
+
agentListEntries.push({ name: "implementer", desc: "implementação genérica" });
|
|
673
|
+
console.log(" (nenhuma stack reconhecida — gerado agente genérico 'implementer')");
|
|
674
|
+
} else {
|
|
675
|
+
for (const agent of detected) {
|
|
676
|
+
await writeFromTemplate(agent.template, path.join(CWD, ".claude", "agents", agent.target), data);
|
|
677
|
+
agentListEntries.push({ name: agent.target.replace(".md", ""), desc: agent.desc });
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// reviewer sempre é gerado — revisa contra todas as camadas
|
|
682
|
+
await writeFromTemplate(
|
|
683
|
+
".claude/agents/reviewer.md.tpl",
|
|
684
|
+
path.join(CWD, ".claude", "agents", "reviewer.md"),
|
|
685
|
+
data
|
|
686
|
+
);
|
|
687
|
+
agentListEntries.push({
|
|
688
|
+
name: "reviewer",
|
|
689
|
+
desc: "revisão de código e conformidade com os padrões, antes de finalizar qualquer tarefa",
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// orquestrador — sempre gerado, com a lista de agentes acima já preenchida
|
|
693
|
+
const agentsList = agentListEntries
|
|
694
|
+
.map((e) => `- \`${e.name}\` — ${e.desc}`)
|
|
695
|
+
.join("\n");
|
|
696
|
+
const finalizeBlock = buildFinalizeBlock(answers.mergeStrategy, data.DEV_BRANCH);
|
|
697
|
+
await writeFromTemplate(
|
|
698
|
+
".claude/agents/orchestrator.md.tpl",
|
|
699
|
+
path.join(CWD, ".claude", "agents", "orchestrator.md"),
|
|
700
|
+
{
|
|
701
|
+
...data,
|
|
702
|
+
AGENTS_LIST: agentsList,
|
|
703
|
+
FINALIZE_TITLE: finalizeBlock.title,
|
|
704
|
+
FINALIZE_BODY: finalizeBlock.body,
|
|
705
|
+
FINALIZE_AVOID_LINE: finalizeBlock.avoidLine,
|
|
706
|
+
}
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// 4.5 settings.json — Plan Mode e/ou hooks, combinados no MESMO arquivo
|
|
711
|
+
// (writeFromTemplate não faz merge, então construímos o objeto aqui).
|
|
712
|
+
if (answers.planModeDefault || answers.addHooks) {
|
|
713
|
+
const settingsObj = {};
|
|
714
|
+
if (answers.planModeDefault) {
|
|
715
|
+
settingsObj.defaultMode = "plan";
|
|
716
|
+
}
|
|
717
|
+
if (answers.addHooks) {
|
|
718
|
+
await writeFromTemplate(
|
|
719
|
+
".claude/hooks/guard-git-safety.sh.tpl",
|
|
720
|
+
path.join(CWD, ".claude", "hooks", "guard-git-safety.sh"),
|
|
721
|
+
data
|
|
722
|
+
);
|
|
723
|
+
await fs.chmod(path.join(CWD, ".claude", "hooks", "guard-git-safety.sh"), 0o755);
|
|
724
|
+
|
|
725
|
+
settingsObj.hooks = {
|
|
726
|
+
PreToolUse: [
|
|
727
|
+
{
|
|
728
|
+
matcher: "Bash",
|
|
729
|
+
hooks: [
|
|
730
|
+
{
|
|
731
|
+
type: "command",
|
|
732
|
+
command: "$CLAUDE_PROJECT_DIR/.claude/hooks/guard-git-safety.sh",
|
|
733
|
+
},
|
|
734
|
+
],
|
|
735
|
+
},
|
|
736
|
+
],
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
// hook de migration só faz sentido se detectamos um agente de banco
|
|
740
|
+
const hasDbMigrator = detected.some((a) => a.target === "db-migrator.md");
|
|
741
|
+
if (hasDbMigrator) {
|
|
742
|
+
await writeFromTemplate(
|
|
743
|
+
".claude/hooks/guard-migration-rollback.sh.tpl",
|
|
744
|
+
path.join(CWD, ".claude", "hooks", "guard-migration-rollback.sh"),
|
|
745
|
+
data
|
|
746
|
+
);
|
|
747
|
+
await fs.chmod(
|
|
748
|
+
path.join(CWD, ".claude", "hooks", "guard-migration-rollback.sh"),
|
|
749
|
+
0o755
|
|
750
|
+
);
|
|
751
|
+
settingsObj.hooks.PostToolUse = [
|
|
752
|
+
{
|
|
753
|
+
matcher: "Write|Edit",
|
|
754
|
+
hooks: [
|
|
755
|
+
{
|
|
756
|
+
type: "command",
|
|
757
|
+
command: "$CLAUDE_PROJECT_DIR/.claude/hooks/guard-migration-rollback.sh",
|
|
758
|
+
},
|
|
759
|
+
],
|
|
760
|
+
},
|
|
761
|
+
];
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const settingsPath = path.join(CWD, ".claude", "settings.json");
|
|
766
|
+
if (await fs.pathExists(settingsPath)) {
|
|
767
|
+
console.log(` já existe, pulando: ${path.relative(CWD, settingsPath)}`);
|
|
768
|
+
} else {
|
|
769
|
+
await fs.ensureDir(path.dirname(settingsPath));
|
|
770
|
+
await fs.writeFile(settingsPath, JSON.stringify(settingsObj, null, 2) + "\n", "utf8");
|
|
771
|
+
console.log(` criado: ${path.relative(CWD, settingsPath)}`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// 5. commands
|
|
776
|
+
if (answers.extras.includes("commands")) {
|
|
777
|
+
await writeFromTemplate(
|
|
778
|
+
".claude/commands/nova-implementacao.md.tpl",
|
|
779
|
+
path.join(CWD, ".claude", "commands", "nova-implementacao.md"),
|
|
780
|
+
data
|
|
781
|
+
);
|
|
782
|
+
const finalizeBlockCmd = buildFinalizeBlock(answers.mergeStrategy, data.DEV_BRANCH);
|
|
783
|
+
await writeFromTemplate(
|
|
784
|
+
".claude/commands/finalizar.md.tpl",
|
|
785
|
+
path.join(CWD, ".claude", "commands", "finalizar.md"),
|
|
786
|
+
{
|
|
787
|
+
...data,
|
|
788
|
+
FINALIZE_STEPS: finalizeBlockCmd.finalizarSteps,
|
|
789
|
+
FINALIZE_DESC_SUFFIX:
|
|
790
|
+
answers.mergeStrategy === "pr"
|
|
791
|
+
? "abre um Pull Request pra revisão"
|
|
792
|
+
: `faz merge com ${data.DEV_BRANCH} e limpa a branch`,
|
|
793
|
+
}
|
|
794
|
+
);
|
|
795
|
+
await writeFromTemplate(
|
|
796
|
+
".claude/commands/registrar-decisao.md.tpl",
|
|
797
|
+
path.join(CWD, ".claude", "commands", "registrar-decisao.md"),
|
|
798
|
+
data
|
|
799
|
+
);
|
|
800
|
+
await writeFromTemplate(
|
|
801
|
+
".claude/commands/diagrama.md.tpl",
|
|
802
|
+
path.join(CWD, ".claude", "commands", "diagrama.md"),
|
|
803
|
+
data
|
|
804
|
+
);
|
|
805
|
+
await writeFromTemplate(
|
|
806
|
+
".claude/commands/onboarding.md.tpl",
|
|
807
|
+
path.join(CWD, ".claude", "commands", "onboarding.md"),
|
|
808
|
+
data
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
// versionamento — /versao + GitHub Action de auto-tag, se confirmado
|
|
812
|
+
if (answers.addVersioning) {
|
|
813
|
+
await writeFromTemplate(
|
|
814
|
+
".claude/commands/versao.md.tpl",
|
|
815
|
+
path.join(CWD, ".claude", "commands", "versao.md"),
|
|
816
|
+
data
|
|
817
|
+
);
|
|
818
|
+
await writeFromTemplate(
|
|
819
|
+
".github/workflows/auto-tag.yml.tpl",
|
|
820
|
+
path.join(CWD, ".github", "workflows", "auto-tag.yml"),
|
|
821
|
+
data
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// 6. CLAUDE.md por app (monorepo)
|
|
827
|
+
if (answers.isMonorepo && apps.length) {
|
|
828
|
+
for (const app of apps) {
|
|
829
|
+
await writeFromTemplate(
|
|
830
|
+
"app/CLAUDE.app.md.tpl",
|
|
831
|
+
path.join(CWD, "apps", app, "CLAUDE.md"),
|
|
832
|
+
{
|
|
833
|
+
...data,
|
|
834
|
+
APP_NAME: app,
|
|
835
|
+
LAYERS_SECTION: app === backendAppFolder ? layersSectionText : "",
|
|
836
|
+
}
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
console.log("\nPronto. Revise os placeholders [definir] antes de commitar.\n");
|
|
842
|
+
|
|
843
|
+
// 7. skills complementares — só oferece as que fazem sentido pra stack
|
|
844
|
+
// detectada neste projeto específico.
|
|
845
|
+
if (answers.extras.includes("agents")) {
|
|
846
|
+
const applicableSkills = getApplicableSkills(detected);
|
|
847
|
+
|
|
848
|
+
if (applicableSkills.length) {
|
|
849
|
+
const { skillsToInstall } = await prompts(
|
|
850
|
+
{
|
|
851
|
+
type: "multiselect",
|
|
852
|
+
name: "skillsToInstall",
|
|
853
|
+
message: "Instalar skills complementares populares (aplicáveis à stack deste projeto)?",
|
|
854
|
+
choices: applicableSkills.map((s) => ({
|
|
855
|
+
title: `${s.id} — ${s.desc}`,
|
|
856
|
+
value: s.id,
|
|
857
|
+
selected: true,
|
|
858
|
+
})),
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
onCancel: () => {
|
|
862
|
+
console.log("\nCancelado.");
|
|
863
|
+
process.exit(1);
|
|
864
|
+
},
|
|
865
|
+
}
|
|
866
|
+
);
|
|
867
|
+
|
|
868
|
+
for (const skillId of skillsToInstall || []) {
|
|
869
|
+
const skillDef = applicableSkills.find((s) => s.id === skillId);
|
|
870
|
+
console.log(`\nInstalando ${skillId} via npx skills...\n`);
|
|
871
|
+
try {
|
|
872
|
+
execSync(`npx --yes skills add ${skillDef.repo} --skill ${skillDef.skill}`, {
|
|
873
|
+
cwd: CWD,
|
|
874
|
+
stdio: "inherit",
|
|
875
|
+
});
|
|
876
|
+
console.log(`\n${skillId} instalado em .claude/skills/${skillId}/\n`);
|
|
877
|
+
} catch (err) {
|
|
878
|
+
console.log(
|
|
879
|
+
`\nNão foi possível instalar ${skillId} automaticamente (verifique sua conexão/npm). ` +
|
|
880
|
+
`Para instalar manualmente depois, rode:\n` +
|
|
881
|
+
` npx skills add ${skillDef.repo} --skill ${skillDef.skill}\n`
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
main().catch((err) => {
|
|
890
|
+
console.error("Erro:", err);
|
|
891
|
+
process.exit(1);
|
|
892
|
+
});
|