@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.
Files changed (33) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/.claude-plugin/plugin.json +10 -0
  3. package/LICENSE +21 -0
  4. package/README.md +193 -0
  5. package/bin/cli.js +892 -0
  6. package/commands/claude-init.md +141 -0
  7. package/package.json +39 -0
  8. package/templates/.claude/agents/backend-implementer.md.tpl +30 -0
  9. package/templates/.claude/agents/db-migrator.md.tpl +27 -0
  10. package/templates/.claude/agents/frontend-implementer.md.tpl +26 -0
  11. package/templates/.claude/agents/implementer.md.tpl +27 -0
  12. package/templates/.claude/agents/orchestrator.md.tpl +85 -0
  13. package/templates/.claude/agents/queue-worker.md.tpl +20 -0
  14. package/templates/.claude/agents/reviewer.md.tpl +18 -0
  15. package/templates/.claude/commands/diagrama.md.tpl +20 -0
  16. package/templates/.claude/commands/finalizar.md.tpl +12 -0
  17. package/templates/.claude/commands/nova-implementacao.md.tpl +15 -0
  18. package/templates/.claude/commands/onboarding.md.tpl +25 -0
  19. package/templates/.claude/commands/registrar-decisao.md.tpl +15 -0
  20. package/templates/.claude/commands/versao.md.tpl +34 -0
  21. package/templates/.claude/hooks/guard-git-safety.sh.tpl +33 -0
  22. package/templates/.claude/hooks/guard-migration-rollback.sh.tpl +35 -0
  23. package/templates/.claude/layer/CLAUDE.layer.md.tpl +23 -0
  24. package/templates/.claude/rules/convencoes.md.tpl +7 -0
  25. package/templates/.claude/rules/registro-decisoes.md.tpl +18 -0
  26. package/templates/.claude/rules/stack.md.tpl +6 -0
  27. package/templates/.github/workflows/auto-tag.yml.tpl +31 -0
  28. package/templates/CLAUDE.root.md.tpl +41 -0
  29. package/templates/app/CLAUDE.app.md.tpl +21 -0
  30. package/templates/docs/architecture/README.md.tpl +11 -0
  31. package/templates/docs/architecture/decisions.md.tpl +23 -0
  32. package/templates/docs/architecture/visao-geral.md.tpl +17 -0
  33. 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
+ });