spec-first-copilot 0.7.0-beta.1 → 0.7.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 (55) hide show
  1. package/README.md +252 -167
  2. package/bin/cli.js +70 -70
  3. package/lib/init.js +92 -92
  4. package/lib/update.js +132 -132
  5. package/package.json +1 -1
  6. package/templates/.ai/memory/napkin.md +68 -68
  7. package/templates/.github/CHANGELOG.md +560 -533
  8. package/templates/.github/adapters/SETUP.md +314 -314
  9. package/templates/.github/adapters/confluence.md +295 -295
  10. package/templates/.github/adapters/errors.md +234 -234
  11. package/templates/.github/adapters/filesystem.md +353 -353
  12. package/templates/.github/adapters/interface.md +301 -301
  13. package/templates/.github/adapters/naming.md +241 -241
  14. package/templates/.github/adapters/registry.md +244 -244
  15. package/templates/.github/agents/backend-coder.md +215 -215
  16. package/templates/.github/agents/db-coder.md +165 -165
  17. package/templates/.github/agents/doc-writer.md +66 -66
  18. package/templates/.github/agents/frontend-coder.md +222 -222
  19. package/templates/.github/agents/infra-coder.md +341 -341
  20. package/templates/.github/agents/reviewer.md +99 -99
  21. package/templates/.github/agents/security-reviewer.md +153 -153
  22. package/templates/.github/copilot-instructions.md +272 -272
  23. package/templates/.github/instructions/docs.instructions.md +147 -145
  24. package/templates/.github/instructions/sensitive-files.instructions.md +32 -32
  25. package/templates/.github/rules.md +229 -229
  26. package/templates/.github/scripts/bootstrap-confluence.js +289 -289
  27. package/templates/.github/skills/sf-design/SKILL.md +161 -161
  28. package/templates/.github/skills/sf-dev/SKILL.md +204 -204
  29. package/templates/.github/skills/sf-discovery/SKILL.md +415 -415
  30. package/templates/.github/skills/sf-extract/SKILL.md +225 -225
  31. package/templates/.github/skills/sf-load/SKILL.md +296 -296
  32. package/templates/.github/skills/sf-mcp/SKILL.md +386 -386
  33. package/templates/.github/skills/sf-merge-docs/SKILL.md +152 -152
  34. package/templates/.github/skills/sf-plan/SKILL.md +152 -152
  35. package/templates/.github/skills/sf-publish/SKILL.md +144 -144
  36. package/templates/.github/skills/sf-session-finish/SKILL.md +93 -93
  37. package/templates/.github/skills/sf-start/SKILL.md +192 -192
  38. package/templates/.github/templates/estrutura/apiContracts.template.md +160 -159
  39. package/templates/.github/templates/estrutura/architecture.template.md +169 -168
  40. package/templates/.github/templates/estrutura/conventions.template.md +214 -212
  41. package/templates/.github/templates/estrutura/decisions.template.md +107 -107
  42. package/templates/.github/templates/estrutura/domain.template.md +161 -160
  43. package/templates/.github/templates/feature/PRD.template.md +279 -279
  44. package/templates/.github/templates/feature/Progresso.template.md +141 -141
  45. package/templates/.github/templates/feature/TRD.template.md +358 -358
  46. package/templates/.github/templates/feature/context.template.md +89 -89
  47. package/templates/.github/templates/feature/extract-log.template.md +49 -49
  48. package/templates/.github/templates/feature/projetos.template.yaml +79 -79
  49. package/templates/.github/templates/global/progresso_global.template.md +59 -57
  50. package/templates/.github/templates/specs/brief.template.md +66 -66
  51. package/templates/.github/templates/specs/contracts.template.md +147 -147
  52. package/templates/.github/templates/specs/scenarios.template.md +125 -125
  53. package/templates/.github/templates/specs/tasks.template.md +65 -65
  54. package/templates/_gitignore +35 -35
  55. package/templates/sfw.config.yml.example +147 -147
@@ -1,289 +1,289 @@
1
- #!/usr/bin/env node
2
- // bootstrap-confluence.js — prepara o ambiente para o backend Confluence do SFW.
3
- //
4
- // Propósito: tirar fricção ANTES de abrir Claude Code. Depois de rodar este script,
5
- // o /sf-mcp confluence vai só descobrir a árvore do projeto no Confluence — credenciais
6
- // e dependências já estarão prontas.
7
- //
8
- // Uso:
9
- // node .github/scripts/bootstrap-confluence.js
10
- //
11
- // O que faz:
12
- // 1. Verifica se `uvx` está instalado (roda o MCP Atlassian)
13
- // 2. Coleta credenciais Atlassian interativamente (email + token + URL)
14
- // 3. Gera/mescla `.mcp.json` com entry `atlassian` (preserva outras entries)
15
- // 4. Garante que `.mcp.json` está no `.gitignore`
16
- // 5. Pré-cacheia `uvx mcp-atlassian` (evita delay no primeiro boot do Claude)
17
- //
18
- // O que NÃO faz (fica pro /sf-mcp confluence dentro do Claude):
19
- // - Validar credenciais contra o servidor
20
- // - Descobrir páginas root do projeto
21
- // - Gerar `sfw.config.yml`
22
-
23
- const fs = require("fs");
24
- const path = require("path");
25
- const readline = require("readline");
26
- const { execSync, spawn } = require("child_process");
27
-
28
- const CWD = process.cwd();
29
-
30
- function log(msg) {
31
- console.log(msg);
32
- }
33
-
34
- function err(msg) {
35
- console.error(`✗ ${msg}`);
36
- }
37
-
38
- function ok(msg) {
39
- console.log(`✓ ${msg}`);
40
- }
41
-
42
- function hr() {
43
- console.log("─".repeat(60));
44
- }
45
-
46
- function prompt(question) {
47
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
48
- return new Promise((resolve) => {
49
- rl.question(question, (answer) => {
50
- rl.close();
51
- resolve(answer.trim());
52
- });
53
- });
54
- }
55
-
56
- // Pergunta yes/no com default "y". Aceita ENTER, y, yes, s, sim (não case-sensitive).
57
- async function confirm(question) {
58
- const answer = (await prompt(`${question} [S/n]: `)).toLowerCase();
59
- if (answer === "" || answer === "s" || answer === "sim" || answer === "y" || answer === "yes") {
60
- return true;
61
- }
62
- return false;
63
- }
64
-
65
- function maskToken(token) {
66
- if (!token || token.length < 12) return "****";
67
- return token.slice(0, 4) + "…" + token.slice(-4);
68
- }
69
-
70
- function hasCommand(cmd) {
71
- try {
72
- execSync(`${cmd} --version`, { stdio: "ignore" });
73
- return true;
74
- } catch {
75
- return false;
76
- }
77
- }
78
-
79
- function loadJson(filePath) {
80
- if (!fs.existsSync(filePath)) return null;
81
- try {
82
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
83
- } catch (e) {
84
- err(`JSON inválido em ${filePath}: ${e.message}`);
85
- process.exit(1);
86
- }
87
- }
88
-
89
- function saveJson(filePath, data) {
90
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
91
- }
92
-
93
- function ensureGitignore(entry) {
94
- const giPath = path.join(CWD, ".gitignore");
95
- let content = "";
96
- if (fs.existsSync(giPath)) {
97
- content = fs.readFileSync(giPath, "utf8");
98
- const lines = content.split(/\r?\n/).map((l) => l.trim());
99
- if (lines.includes(entry)) return false; // já tem
100
- }
101
- const sep = content && !content.endsWith("\n") ? "\n" : "";
102
- fs.appendFileSync(giPath, `${sep}${entry}\n`, "utf8");
103
- return true;
104
- }
105
-
106
- function validateUrl(url) {
107
- return /^https:\/\/[\w.-]+\.atlassian\.net\/wiki\/?$/.test(url);
108
- }
109
-
110
- function validateEmail(email) {
111
- return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email);
112
- }
113
-
114
- // ---- Main flow ----
115
-
116
- (async function main() {
117
- log("");
118
- log("SFW — Bootstrap Confluence");
119
- hr();
120
-
121
- // 1. Verificar uvx
122
- log("1. Verificando dependências...");
123
- if (!hasCommand("uvx")) {
124
- err("`uvx` não encontrado no PATH.");
125
- log("");
126
- log(" Instale com:");
127
- log(" pip install uv (Python 3.10+ necessário)");
128
- log("");
129
- log(" Ou via Homebrew (macOS):");
130
- log(" brew install uv");
131
- log("");
132
- log(" Depois rode este script novamente.");
133
- process.exit(1);
134
- }
135
- ok("uvx disponível");
136
-
137
- // 2. Detectar .mcp.json existente e reusar credenciais válidas
138
- log("");
139
- log("2. Credenciais Atlassian");
140
- log("");
141
-
142
- const mcpPath = path.join(CWD, ".mcp.json");
143
- const existing = loadJson(mcpPath) || {};
144
- if (!existing.mcpServers) existing.mcpServers = {};
145
- const existingAtlassian = existing.mcpServers.atlassian;
146
- const existingEnv = (existingAtlassian && existingAtlassian.env) || {};
147
-
148
- if (existingAtlassian) {
149
- log(" Detectei entry `atlassian` existente em .mcp.json — vou reusar o que já está válido.");
150
- log("");
151
- }
152
-
153
- // URL
154
- let url = existingEnv.CONFLUENCE_URL || "";
155
- if (url && validateUrl(url)) {
156
- log(` URL atual: ${url}`);
157
- if (await confirm(" Manter esta URL?")) {
158
- ok("URL mantida");
159
- } else {
160
- url = "";
161
- }
162
- log("");
163
- }
164
- if (!url) {
165
- log(" → URL do Confluence: abra seu Confluence no browser e copie a URL base.");
166
- log(" Ex: https://empresa.atlassian.net/wiki");
167
- log("");
168
- while (!validateUrl(url)) {
169
- url = await prompt(" URL: ");
170
- if (!validateUrl(url)) {
171
- err(`URL inválida. Formato esperado: https://{empresa}.atlassian.net/wiki`);
172
- }
173
- }
174
- if (url.endsWith("/")) url = url.slice(0, -1);
175
- }
176
-
177
- // Email
178
- let email = existingEnv.CONFLUENCE_USERNAME || "";
179
- log("");
180
- if (email && validateEmail(email)) {
181
- log(` Email atual: ${email}`);
182
- if (await confirm(" Manter este email?")) {
183
- ok("Email mantido");
184
- } else {
185
- email = "";
186
- }
187
- log("");
188
- }
189
- if (!email) {
190
- log(" → Email da conta Atlassian (o que você usa pra logar).");
191
- log("");
192
- while (!validateEmail(email)) {
193
- email = await prompt(" Email: ");
194
- if (!validateEmail(email)) err("Email inválido.");
195
- }
196
- }
197
-
198
- // Token
199
- let token = existingEnv.CONFLUENCE_API_TOKEN || "";
200
- log("");
201
- if (token) {
202
- log(` Token atual: ${maskToken(token)}`);
203
- if (await confirm(" Manter este token?")) {
204
- ok("Token mantido");
205
- } else {
206
- token = "";
207
- }
208
- log("");
209
- }
210
- if (!token) {
211
- log(" → API Token.");
212
- log(" Gere em: https://id.atlassian.com/manage-profile/security/api-tokens");
213
- log(" Clique em 'Create API token' → dê um nome (ex: 'sfw-local') → copie agora");
214
- log(" (o token só aparece uma vez, depois some).");
215
- log("");
216
- while (!token) {
217
- token = await prompt(" Token: ");
218
- if (!token) err("Token não pode estar vazio.");
219
- }
220
- }
221
-
222
- // 3. Gerar/mesclar .mcp.json (só escreve se algo mudou)
223
- log("");
224
- log("3. Atualizando .mcp.json...");
225
- const nextEntry = {
226
- command: "uvx",
227
- args: ["mcp-atlassian"],
228
- env: {
229
- CONFLUENCE_URL: url,
230
- CONFLUENCE_USERNAME: email,
231
- CONFLUENCE_API_TOKEN: token,
232
- },
233
- };
234
- const unchanged =
235
- existingAtlassian &&
236
- JSON.stringify(existingAtlassian) === JSON.stringify(nextEntry);
237
-
238
- if (unchanged) {
239
- ok(".mcp.json já estava correto (entry: atlassian) — nada a escrever");
240
- } else {
241
- existing.mcpServers.atlassian = nextEntry;
242
- saveJson(mcpPath, existing);
243
- ok(`.mcp.json ${existingAtlassian ? "atualizado" : "criado"} (entry: atlassian)`);
244
- }
245
-
246
- // 4. Garantir .gitignore
247
- const added = ensureGitignore(".mcp.json");
248
- if (added) ok(".mcp.json adicionado ao .gitignore");
249
- else ok(".mcp.json já estava no .gitignore");
250
-
251
- // 5. Pré-cachear MCP
252
- log("");
253
- log("4. Pré-cacheando mcp-atlassian (baixa 100+ deps na primeira vez, pode demorar 1-3min)...");
254
- await new Promise((resolve) => {
255
- const child = spawn("uvx", ["mcp-atlassian", "--help"], { stdio: "ignore", shell: true });
256
- const timeout = setTimeout(() => {
257
- child.kill();
258
- log(" (timeout após 5min — segue adiante, Claude pode retomar o download)");
259
- resolve();
260
- }, 5 * 60 * 1000);
261
- child.on("close", (code) => {
262
- clearTimeout(timeout);
263
- if (code === 0) ok("mcp-atlassian em cache local");
264
- else log(` (uvx retornou código ${code} — pode ser normal, --help às vezes sai != 0)`);
265
- resolve();
266
- });
267
- child.on("error", (e) => {
268
- clearTimeout(timeout);
269
- err(`Falha ao rodar uvx: ${e.message}`);
270
- resolve();
271
- });
272
- });
273
-
274
- // Final
275
- log("");
276
- hr();
277
- ok("Bootstrap concluído.");
278
- log("");
279
- log("Próximos passos:");
280
- log(" 1. Abra o Claude Code neste diretório");
281
- log(" 2. Rode: /sf-mcp confluence");
282
- log(" → vai descobrir a árvore do seu projeto no Confluence e gerar sfw.config.yml");
283
- log("");
284
- log("Se reiniciar o Claude Code for necessário (após este setup), ele vai pedir.");
285
- log("");
286
- })().catch((e) => {
287
- err(`Erro inesperado: ${e.message}`);
288
- process.exit(1);
289
- });
1
+ #!/usr/bin/env node
2
+ // bootstrap-confluence.js — prepara o ambiente para o backend Confluence do SFW.
3
+ //
4
+ // Propósito: tirar fricção ANTES de abrir Claude Code. Depois de rodar este script,
5
+ // o /sf-mcp confluence vai só descobrir a árvore do projeto no Confluence — credenciais
6
+ // e dependências já estarão prontas.
7
+ //
8
+ // Uso:
9
+ // node .github/scripts/bootstrap-confluence.js
10
+ //
11
+ // O que faz:
12
+ // 1. Verifica se `uvx` está instalado (roda o MCP Atlassian)
13
+ // 2. Coleta credenciais Atlassian interativamente (email + token + URL)
14
+ // 3. Gera/mescla `.mcp.json` com entry `atlassian` (preserva outras entries)
15
+ // 4. Garante que `.mcp.json` está no `.gitignore`
16
+ // 5. Pré-cacheia `uvx mcp-atlassian` (evita delay no primeiro boot do Claude)
17
+ //
18
+ // O que NÃO faz (fica pro /sf-mcp confluence dentro do Claude):
19
+ // - Validar credenciais contra o servidor
20
+ // - Descobrir páginas root do projeto
21
+ // - Gerar `sfw.config.yml`
22
+
23
+ const fs = require("fs");
24
+ const path = require("path");
25
+ const readline = require("readline");
26
+ const { execSync, spawn } = require("child_process");
27
+
28
+ const CWD = process.cwd();
29
+
30
+ function log(msg) {
31
+ console.log(msg);
32
+ }
33
+
34
+ function err(msg) {
35
+ console.error(`✗ ${msg}`);
36
+ }
37
+
38
+ function ok(msg) {
39
+ console.log(`✓ ${msg}`);
40
+ }
41
+
42
+ function hr() {
43
+ console.log("─".repeat(60));
44
+ }
45
+
46
+ function prompt(question) {
47
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
48
+ return new Promise((resolve) => {
49
+ rl.question(question, (answer) => {
50
+ rl.close();
51
+ resolve(answer.trim());
52
+ });
53
+ });
54
+ }
55
+
56
+ // Pergunta yes/no com default "y". Aceita ENTER, y, yes, s, sim (não case-sensitive).
57
+ async function confirm(question) {
58
+ const answer = (await prompt(`${question} [S/n]: `)).toLowerCase();
59
+ if (answer === "" || answer === "s" || answer === "sim" || answer === "y" || answer === "yes") {
60
+ return true;
61
+ }
62
+ return false;
63
+ }
64
+
65
+ function maskToken(token) {
66
+ if (!token || token.length < 12) return "****";
67
+ return token.slice(0, 4) + "…" + token.slice(-4);
68
+ }
69
+
70
+ function hasCommand(cmd) {
71
+ try {
72
+ execSync(`${cmd} --version`, { stdio: "ignore" });
73
+ return true;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ function loadJson(filePath) {
80
+ if (!fs.existsSync(filePath)) return null;
81
+ try {
82
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
83
+ } catch (e) {
84
+ err(`JSON inválido em ${filePath}: ${e.message}`);
85
+ process.exit(1);
86
+ }
87
+ }
88
+
89
+ function saveJson(filePath, data) {
90
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
91
+ }
92
+
93
+ function ensureGitignore(entry) {
94
+ const giPath = path.join(CWD, ".gitignore");
95
+ let content = "";
96
+ if (fs.existsSync(giPath)) {
97
+ content = fs.readFileSync(giPath, "utf8");
98
+ const lines = content.split(/\r?\n/).map((l) => l.trim());
99
+ if (lines.includes(entry)) return false; // já tem
100
+ }
101
+ const sep = content && !content.endsWith("\n") ? "\n" : "";
102
+ fs.appendFileSync(giPath, `${sep}${entry}\n`, "utf8");
103
+ return true;
104
+ }
105
+
106
+ function validateUrl(url) {
107
+ return /^https:\/\/[\w.-]+\.atlassian\.net\/wiki\/?$/.test(url);
108
+ }
109
+
110
+ function validateEmail(email) {
111
+ return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email);
112
+ }
113
+
114
+ // ---- Main flow ----
115
+
116
+ (async function main() {
117
+ log("");
118
+ log("SFW — Bootstrap Confluence");
119
+ hr();
120
+
121
+ // 1. Verificar uvx
122
+ log("1. Verificando dependências...");
123
+ if (!hasCommand("uvx")) {
124
+ err("`uvx` não encontrado no PATH.");
125
+ log("");
126
+ log(" Instale com:");
127
+ log(" pip install uv (Python 3.10+ necessário)");
128
+ log("");
129
+ log(" Ou via Homebrew (macOS):");
130
+ log(" brew install uv");
131
+ log("");
132
+ log(" Depois rode este script novamente.");
133
+ process.exit(1);
134
+ }
135
+ ok("uvx disponível");
136
+
137
+ // 2. Detectar .mcp.json existente e reusar credenciais válidas
138
+ log("");
139
+ log("2. Credenciais Atlassian");
140
+ log("");
141
+
142
+ const mcpPath = path.join(CWD, ".mcp.json");
143
+ const existing = loadJson(mcpPath) || {};
144
+ if (!existing.mcpServers) existing.mcpServers = {};
145
+ const existingAtlassian = existing.mcpServers.atlassian;
146
+ const existingEnv = (existingAtlassian && existingAtlassian.env) || {};
147
+
148
+ if (existingAtlassian) {
149
+ log(" Detectei entry `atlassian` existente em .mcp.json — vou reusar o que já está válido.");
150
+ log("");
151
+ }
152
+
153
+ // URL
154
+ let url = existingEnv.CONFLUENCE_URL || "";
155
+ if (url && validateUrl(url)) {
156
+ log(` URL atual: ${url}`);
157
+ if (await confirm(" Manter esta URL?")) {
158
+ ok("URL mantida");
159
+ } else {
160
+ url = "";
161
+ }
162
+ log("");
163
+ }
164
+ if (!url) {
165
+ log(" → URL do Confluence: abra seu Confluence no browser e copie a URL base.");
166
+ log(" Ex: https://empresa.atlassian.net/wiki");
167
+ log("");
168
+ while (!validateUrl(url)) {
169
+ url = await prompt(" URL: ");
170
+ if (!validateUrl(url)) {
171
+ err(`URL inválida. Formato esperado: https://{empresa}.atlassian.net/wiki`);
172
+ }
173
+ }
174
+ if (url.endsWith("/")) url = url.slice(0, -1);
175
+ }
176
+
177
+ // Email
178
+ let email = existingEnv.CONFLUENCE_USERNAME || "";
179
+ log("");
180
+ if (email && validateEmail(email)) {
181
+ log(` Email atual: ${email}`);
182
+ if (await confirm(" Manter este email?")) {
183
+ ok("Email mantido");
184
+ } else {
185
+ email = "";
186
+ }
187
+ log("");
188
+ }
189
+ if (!email) {
190
+ log(" → Email da conta Atlassian (o que você usa pra logar).");
191
+ log("");
192
+ while (!validateEmail(email)) {
193
+ email = await prompt(" Email: ");
194
+ if (!validateEmail(email)) err("Email inválido.");
195
+ }
196
+ }
197
+
198
+ // Token
199
+ let token = existingEnv.CONFLUENCE_API_TOKEN || "";
200
+ log("");
201
+ if (token) {
202
+ log(` Token atual: ${maskToken(token)}`);
203
+ if (await confirm(" Manter este token?")) {
204
+ ok("Token mantido");
205
+ } else {
206
+ token = "";
207
+ }
208
+ log("");
209
+ }
210
+ if (!token) {
211
+ log(" → API Token.");
212
+ log(" Gere em: https://id.atlassian.com/manage-profile/security/api-tokens");
213
+ log(" Clique em 'Create API token' → dê um nome (ex: 'sfw-local') → copie agora");
214
+ log(" (o token só aparece uma vez, depois some).");
215
+ log("");
216
+ while (!token) {
217
+ token = await prompt(" Token: ");
218
+ if (!token) err("Token não pode estar vazio.");
219
+ }
220
+ }
221
+
222
+ // 3. Gerar/mesclar .mcp.json (só escreve se algo mudou)
223
+ log("");
224
+ log("3. Atualizando .mcp.json...");
225
+ const nextEntry = {
226
+ command: "uvx",
227
+ args: ["mcp-atlassian"],
228
+ env: {
229
+ CONFLUENCE_URL: url,
230
+ CONFLUENCE_USERNAME: email,
231
+ CONFLUENCE_API_TOKEN: token,
232
+ },
233
+ };
234
+ const unchanged =
235
+ existingAtlassian &&
236
+ JSON.stringify(existingAtlassian) === JSON.stringify(nextEntry);
237
+
238
+ if (unchanged) {
239
+ ok(".mcp.json já estava correto (entry: atlassian) — nada a escrever");
240
+ } else {
241
+ existing.mcpServers.atlassian = nextEntry;
242
+ saveJson(mcpPath, existing);
243
+ ok(`.mcp.json ${existingAtlassian ? "atualizado" : "criado"} (entry: atlassian)`);
244
+ }
245
+
246
+ // 4. Garantir .gitignore
247
+ const added = ensureGitignore(".mcp.json");
248
+ if (added) ok(".mcp.json adicionado ao .gitignore");
249
+ else ok(".mcp.json já estava no .gitignore");
250
+
251
+ // 5. Pré-cachear MCP
252
+ log("");
253
+ log("4. Pré-cacheando mcp-atlassian (baixa 100+ deps na primeira vez, pode demorar 1-3min)...");
254
+ await new Promise((resolve) => {
255
+ const child = spawn("uvx", ["mcp-atlassian", "--help"], { stdio: "ignore", shell: true });
256
+ const timeout = setTimeout(() => {
257
+ child.kill();
258
+ log(" (timeout após 5min — segue adiante, Claude pode retomar o download)");
259
+ resolve();
260
+ }, 5 * 60 * 1000);
261
+ child.on("close", (code) => {
262
+ clearTimeout(timeout);
263
+ if (code === 0) ok("mcp-atlassian em cache local");
264
+ else log(` (uvx retornou código ${code} — pode ser normal, --help às vezes sai != 0)`);
265
+ resolve();
266
+ });
267
+ child.on("error", (e) => {
268
+ clearTimeout(timeout);
269
+ err(`Falha ao rodar uvx: ${e.message}`);
270
+ resolve();
271
+ });
272
+ });
273
+
274
+ // Final
275
+ log("");
276
+ hr();
277
+ ok("Bootstrap concluído.");
278
+ log("");
279
+ log("Próximos passos:");
280
+ log(" 1. Abra o Claude Code neste diretório");
281
+ log(" 2. Rode: /sf-mcp confluence");
282
+ log(" → vai descobrir a árvore do seu projeto no Confluence e gerar sfw.config.yml");
283
+ log("");
284
+ log("Se reiniciar o Claude Code for necessário (após este setup), ele vai pedir.");
285
+ log("");
286
+ })().catch((e) => {
287
+ err(`Erro inesperado: ${e.message}`);
288
+ process.exit(1);
289
+ });