@guilhermefsousa/open-spec-kit 0.0.9 → 0.0.11

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 (28) hide show
  1. package/README.md +1 -1
  2. package/bin/open-spec-kit.js +7 -0
  3. package/package.json +1 -1
  4. package/src/commands/doctor.js +107 -197
  5. package/src/commands/init.js +112 -347
  6. package/src/commands/install.js +393 -0
  7. package/src/commands/update.js +117 -165
  8. package/src/schemas/spec.schema.js +3 -3
  9. package/src/utils/global-path.js +73 -0
  10. package/templates/agents/agents/spec-hub.agent.md +13 -13
  11. package/templates/agents/rules/hub_structure.instructions.md +1 -1
  12. package/templates/agents/rules/ownership.instructions.md +39 -39
  13. package/templates/agents/skills/dev-orchestrator/SKILL.md +17 -17
  14. package/templates/agents/skills/discovery/SKILL.md +17 -17
  15. package/templates/agents/skills/setup-project/SKILL.md +15 -15
  16. package/templates/agents/skills/specifying-features/SKILL.md +28 -28
  17. package/templates/github/agents/spec-hub.agent.md +5 -5
  18. package/templates/github/copilot-instructions.md +9 -9
  19. package/templates/github/instructions/hub_structure.instructions.md +1 -1
  20. package/templates/github/instructions/ownership.instructions.md +9 -9
  21. package/templates/github/skills/dev-orchestrator/SKILL.md +619 -5
  22. package/templates/github/skills/discovery/SKILL.md +419 -5
  23. package/templates/github/skills/setup-project/SKILL.md +496 -5
  24. package/templates/github/skills/specifying-features/SKILL.md +417 -5
  25. /package/templates/github/prompts/{dev.prompt.md → osk-build.prompt.md} +0 -0
  26. /package/templates/github/prompts/{discovery.prompt.md → osk-discover.prompt.md} +0 -0
  27. /package/templates/github/prompts/{setup.prompt.md → osk-init.prompt.md} +0 -0
  28. /package/templates/github/prompts/{nova-feature.prompt.md → osk-spec.prompt.md} +0 -0
@@ -1,20 +1,15 @@
1
1
  import inquirer from 'inquirer';
2
2
  import chalk from 'chalk';
3
3
  import ora from 'ora';
4
- import { writeFile, mkdir, access, cp } from 'fs/promises';
4
+ import { writeFile, mkdir, access } from 'fs/promises';
5
5
  import { join, dirname } from 'path';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { execSync } from 'child_process';
8
- import { detectMcpRunner, detectNpxRunner, tryInstallMcpAtlassian, getInstallInstructions } from '../utils/mcp-detect.js';
9
- import { confluenceRequest, figmaRequest, isSslError } from '../utils/http.js';
8
+ import { detectMcpRunner, detectNpxRunner } from '../utils/mcp-detect.js';
9
+ import { confluenceRequest, isSslError } from '../utils/http.js';
10
+ import { GLOBAL_DIR, getGlobalEnvPath, readGlobalEnv } from '../utils/global-path.js';
10
11
 
11
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
- const TEMPLATES_DIR = join(__dirname, '..', '..', 'templates');
13
-
14
- const AGENTS = {
15
- claude: { name: 'Claude Code' },
16
- copilot: { name: 'GitHub Copilot' },
17
- };
18
13
 
19
14
  const FIGMA_MCP_PACKAGE = 'figma-developer-mcp';
20
15
 
@@ -147,8 +142,6 @@ async function autoDetectFromConfluence(confluenceUrl, confluenceRef, user, toke
147
142
  p.title && p.title.toLowerCase().includes('demandas')
148
143
  );
149
144
 
150
- // If "Demandas" not found among children, look UP the ancestors tree.
151
- // The configured page may itself live INSIDE a "Demandas" folder.
152
145
  if (!demandasPage) {
153
146
  spinner.text = 'Buscando "Demandas" nos ancestrais...';
154
147
  try {
@@ -229,7 +222,7 @@ async function checkGitExists() {
229
222
 
230
223
  async function checkExistingSetup() {
231
224
  const cwd = process.cwd();
232
- const check = ['.agents', '.github/agents', 'projects.yml', '.mcp.json', '.vscode/mcp.json', '.env'];
225
+ const check = ['projects.yml', '.mcp.json', '.vscode/mcp.json'];
233
226
  const found = [];
234
227
  for (const f of check) {
235
228
  try {
@@ -240,32 +233,12 @@ async function checkExistingSetup() {
240
233
  return found;
241
234
  }
242
235
 
243
- /**
244
- * Copy a template directory tree to the target.
245
- * Templates are bundled with the npm package under cli/templates/.
246
- */
247
- async function copyTemplateDir(templateName, targetRoot) {
248
- const sourceDir = join(TEMPLATES_DIR, templateName);
249
- try {
250
- await access(sourceDir);
251
- } catch {
252
- throw new Error(`Template directory not found: ${sourceDir}`);
253
- }
254
- const targetMap = {
255
- agents: '.agents',
256
- github: '.github',
257
- };
258
- const targetDir = join(targetRoot, targetMap[templateName] || templateName);
259
- await cp(sourceDir, targetDir, { recursive: true, force: true });
260
- }
261
-
262
236
  // ──────────────────────────────────────────────────────
263
237
  // File generators
264
238
  // ──────────────────────────────────────────────────────
265
239
  function generateProjectsYml(config) {
266
240
  const isNumeric = /^\d+$/.test(config.confluenceRef);
267
241
  const stackYaml = config.stack.map(s => ` - ${s}`).join('\n');
268
- const agentsYaml = config.agents.map(a => ` - ${a}`).join('\n');
269
242
 
270
243
  let yml = `project:
271
244
  name: ${config.projectName}
@@ -276,8 +249,6 @@ function generateProjectsYml(config) {
276
249
  preset: ${config.preset}
277
250
  stack:
278
251
  ${stackYaml}
279
- agents:
280
- ${agentsYaml}
281
252
 
282
253
  ${config.vcs !== 'none'
283
254
  ? `vcs: ${config.vcs}\nvcs_org: ${config.vcsOrg}`
@@ -285,11 +256,13 @@ ${config.vcs !== 'none'
285
256
 
286
257
  repos:
287
258
  # Liste os repositórios de código do projeto.
259
+ # O /osk-build usa 'status' e 'url' pra saber se precisa criar o repo.
260
+ #
288
261
  # - name: MeuProjeto.Api
289
262
  # type: api
290
263
  # purpose: Endpoints REST
291
- # status: planned
292
- # url: null
264
+ # status: planned # planned | active | deprecated
265
+ # url: null # preenchido pelo /osk-build ao criar o repo
293
266
 
294
267
  # Agent configuration
295
268
  agents:
@@ -325,9 +298,8 @@ confluence:
325
298
  return yml;
326
299
  }
327
300
 
328
- function generateGitignore(config) {
329
- let content = `# Secrets (nunca comitar)
330
- .env
301
+ function generateGitignore() {
302
+ return `# Secrets (nunca comitar)
331
303
  .mcp.json
332
304
 
333
305
  # Claude Code local files
@@ -340,47 +312,8 @@ Thumbs.db
340
312
  # IDE
341
313
  .idea/
342
314
  *.swp
315
+ .vscode/
343
316
  `;
344
-
345
- content += '\n.vscode/\n';
346
-
347
- return content;
348
- }
349
-
350
- function generateEnvFile(config) {
351
- let content = `CONFLUENCE_URL=${config.confluenceUrl}
352
- CONFLUENCE_USERNAME=${config.confluenceUser}
353
- CONFLUENCE_API_TOKEN=${config.confluenceToken}
354
- GCHAT_WEBHOOK_URL=${config.gchatWebhookUrl || ''}
355
- `;
356
-
357
- if (config.hasFigma && config.figmaFileUrl) {
358
- content += `FIGMA_API_KEY=${config.figmaToken}\n`;
359
- }
360
-
361
- return content;
362
- }
363
-
364
- function generateEnvExample(config) {
365
- let content = `# Confluence credentials
366
- # Obtenha o token em: https://id.atlassian.com/manage-profile/security/api-tokens
367
- CONFLUENCE_URL=https://seu-dominio.atlassian.net/wiki
368
- CONFLUENCE_USERNAME=seu-email@empresa.com
369
- CONFLUENCE_API_TOKEN=seu-token-aqui
370
-
371
- # Google Chat webhook (opcional)
372
- GCHAT_WEBHOOK_URL=
373
- `;
374
-
375
- if (config.hasFigma && config.figmaFileUrl) {
376
- content += `
377
- # Figma API key
378
- # Obtenha em: https://www.figma.com/settings
379
- FIGMA_API_KEY=seu-figma-api-key
380
- `;
381
- }
382
-
383
- return content;
384
317
  }
385
318
 
386
319
  function buildMcpServers(config, { stdio = false } = {}) {
@@ -426,7 +359,7 @@ ${config.stack.map(s => `- ${s}`).join('\n')}
426
359
 
427
360
  ## Repositórios
428
361
 
429
- (Preenchido pelo /setup)
362
+ (Preenchido pelo /osk-init)
430
363
 
431
364
  ## Decisões em Aberto
432
365
 
@@ -435,7 +368,7 @@ ${config.stack.map(s => `- ${s}`).join('\n')}
435
368
 
436
369
  ## Diagrama
437
370
 
438
- (Gerado pelo /spec ou /dev)
371
+ (Gerado pelo /osk-spec ou /osk-build)
439
372
  `;
440
373
  }
441
374
 
@@ -445,7 +378,40 @@ ${config.stack.map(s => `- ${s}`).join('\n')}
445
378
  export async function initCommand() {
446
379
  console.log(chalk.bold('\n open-spec-kit init\n'));
447
380
 
448
- // Pre-flight: check existing setup
381
+ // Pre-flight: check global installation
382
+ try {
383
+ await access(GLOBAL_DIR);
384
+ await access(getGlobalEnvPath());
385
+ } catch {
386
+ console.log(chalk.red(' Skills globais não encontrados.'));
387
+ console.log(chalk.yellow(` Execute ${chalk.bold('open-spec-kit install')} primeiro.\n`));
388
+ return;
389
+ }
390
+
391
+ // Read credentials from global .env
392
+ let globalEnv;
393
+ try {
394
+ globalEnv = await readGlobalEnv();
395
+ } catch {
396
+ console.log(chalk.red(' Não foi possível ler ~/.open-spec-kit/.env'));
397
+ console.log(chalk.yellow(` Execute ${chalk.bold('open-spec-kit install')} para configurar credenciais.\n`));
398
+ return;
399
+ }
400
+
401
+ const confluenceUrl = (globalEnv.CONFLUENCE_URL || '').replace(/\/$/, '');
402
+ const confluenceUser = globalEnv.CONFLUENCE_USERNAME || '';
403
+ const confluenceToken = globalEnv.CONFLUENCE_API_TOKEN || '';
404
+
405
+ if (!confluenceUrl || !confluenceUser || !confluenceToken) {
406
+ console.log(chalk.red(' Credenciais Confluence incompletas em ~/.open-spec-kit/.env'));
407
+ console.log(chalk.yellow(` Execute ${chalk.bold('open-spec-kit install --force')} para reconfigurar.\n`));
408
+ return;
409
+ }
410
+
411
+ console.log(chalk.dim(` Credenciais: ${getGlobalEnvPath()}`));
412
+ console.log(chalk.dim(` Skills: ${GLOBAL_DIR}\n`));
413
+
414
+ // Pre-flight: check existing project setup
449
415
  const existing = await checkExistingSetup();
450
416
  if (existing.length > 0) {
451
417
  console.log(chalk.yellow(' Estrutura existente detectada:'));
@@ -463,21 +429,11 @@ export async function initCommand() {
463
429
  }
464
430
 
465
431
  // ════════════════════════════════════════════
466
- // Phase 1 — Conexão
432
+ // Phase 1 — Projeto
467
433
  // ════════════════════════════════════════════
468
- console.log(chalk.bold.blue('\n ── Fase 1: Conexão ──\n'));
434
+ console.log(chalk.bold.blue('\n ── Fase 1: Projeto ──\n'));
469
435
 
470
436
  const phase1 = await inquirer.prompt([
471
- {
472
- type: 'checkbox',
473
- name: 'agents',
474
- message: 'Quais AI tools você usa?',
475
- choices: [
476
- { name: 'Claude Code', value: 'claude', checked: true },
477
- { name: 'GitHub Copilot', value: 'copilot', checked: true },
478
- ],
479
- validate: v => v.length > 0 || 'Selecione pelo menos uma tool'
480
- },
481
437
  {
482
438
  type: 'list',
483
439
  name: 'preset',
@@ -503,103 +459,39 @@ export async function initCommand() {
503
459
  message: 'Space key ou page ID (ex: TT ou 1234567890):',
504
460
  validate: v => v.trim().length > 0 || 'Obrigatório'
505
461
  },
506
- {
507
- type: 'input',
508
- name: 'confluenceUrl',
509
- message: 'Confluence URL (ex: https://seu-dominio.atlassian.net/wiki):',
510
- validate: v => {
511
- const trimmed = v.trim();
512
- if (!trimmed.startsWith('http://') && !trimmed.startsWith('https://')) {
513
- return 'URL deve começar com http:// ou https://';
514
- }
515
- return true;
516
- }
517
- },
518
- {
519
- type: 'input',
520
- name: 'confluenceUser',
521
- message: 'Confluence username/email (ex: voce@empresa.com):',
522
- validate: v => v.trim().length > 0 || 'Obrigatório'
523
- },
524
- {
525
- type: 'password',
526
- name: 'confluenceToken',
527
- message: 'Confluence API token:',
528
- mask: '*',
529
- validate: v => v.trim().length > 0 || 'Obrigatório'
530
- }
531
462
  ]);
532
463
 
533
- // Validate credentials
534
- const spinner = ora('Validando credenciais do Confluence...').start();
464
+ // ════════════════════════════════════════════
465
+ // Phase 2 Auto-detecção
466
+ // ════════════════════════════════════════════
467
+ let detected = { projectName: null, stack: [], repos: [], spaceKey: null, success: false };
468
+ const isNumericRef = /^\d+$/.test(phase1.confluenceRef.trim());
535
469
  let credentialsValid = false;
536
470
  let allowInsecure = false;
537
- const confluenceUrl = phase1.confluenceUrl.trim().replace(/\/$/, '');
538
471
 
472
+ // Validate credentials silently
539
473
  try {
540
- await confluenceRequest(
541
- confluenceUrl,
542
- '/rest/api/space?limit=1',
543
- phase1.confluenceUser.trim(),
544
- phase1.confluenceToken.trim()
545
- );
546
- spinner.succeed('Credenciais validadas com sucesso!');
474
+ await confluenceRequest(confluenceUrl, '/rest/api/space?limit=1', confluenceUser, confluenceToken);
547
475
  credentialsValid = true;
548
476
  } catch (err) {
549
- if (err.status === 401 || err.status === 403) {
550
- spinner.fail(`Autenticação falhou (HTTP ${err.status}). Verifique suas credenciais.`);
551
- console.log(chalk.red(' Não é possível continuar sem acesso ao Confluence.'));
552
- console.log(chalk.dim(' Gere um token em: https://id.atlassian.com/manage-profile/security/api-tokens'));
553
- return;
554
- }
555
477
  if (isSslError(err)) {
556
- spinner.text = 'Certificado SSL corporativo detectado — tentando sem verificação...';
557
478
  try {
558
- await confluenceRequest(
559
- confluenceUrl,
560
- '/rest/api/space?limit=1',
561
- phase1.confluenceUser.trim(),
562
- phase1.confluenceToken.trim(),
563
- 15000,
564
- true
565
- );
566
- allowInsecure = true;
567
- spinner.succeed('Credenciais validadas (SSL corporativo ignorado — ambiente interno).');
479
+ await confluenceRequest(confluenceUrl, '/rest/api/space?limit=1', confluenceUser, confluenceToken, 15000, true);
568
480
  credentialsValid = true;
569
- } catch (retryErr) {
570
- if (retryErr.status === 401 || retryErr.status === 403) {
571
- spinner.fail(`Autenticação falhou (HTTP ${retryErr.status}). Verifique suas credenciais.`);
572
- console.log(chalk.red(' Não é possível continuar sem acesso ao Confluence.'));
573
- console.log(chalk.dim(' Gere um token em: https://id.atlassian.com/manage-profile/security/api-tokens'));
574
- return;
575
- }
576
- spinner.warn(`Não foi possível conectar ao Confluence: ${retryErr.message}`);
577
- console.log(chalk.yellow(' Continuando sem auto-detecção...\n'));
578
- }
579
- } else {
580
- spinner.warn(`Não foi possível conectar ao Confluence: ${err.message}`);
581
- console.log(chalk.yellow(' Continuando sem auto-detecção...\n'));
481
+ allowInsecure = true;
482
+ } catch { /* ignore */ }
582
483
  }
583
484
  }
584
485
 
585
- // ════════════════════════════════════════════
586
- // Phase 2 — Auto-detecção
587
- // ════════════════════════════════════════════
588
- let detected = { projectName: null, stack: [], repos: [], spaceKey: null, success: false };
589
- const isNumericRef = /^\d+$/.test(phase1.confluenceRef.trim());
590
-
591
486
  if (credentialsValid && isNumericRef) {
592
- console.log(chalk.bold.blue('\n ── Fase 2: Auto-detecção ──\n'));
487
+ console.log(chalk.bold.blue('\n ── Auto-detecção ──\n'));
593
488
  const detectSpinner = ora('Analisando páginas do Confluence...').start();
594
489
 
595
490
  try {
596
491
  detected = await autoDetectFromConfluence(
597
- confluenceUrl,
598
- phase1.confluenceRef.trim(),
599
- phase1.confluenceUser.trim(),
600
- phase1.confluenceToken.trim(),
601
- detectSpinner,
602
- allowInsecure
492
+ confluenceUrl, phase1.confluenceRef.trim(),
493
+ confluenceUser, confluenceToken,
494
+ detectSpinner, allowInsecure
603
495
  );
604
496
 
605
497
  if (detected.success && detected.stack.length > 0) {
@@ -612,24 +504,21 @@ export async function initCommand() {
612
504
  console.log(chalk.green(` Projeto: ${detected.projectName}`));
613
505
  }
614
506
  } else {
615
- detectSpinner.info('Nenhuma stack detectada automaticamente — preencha manualmente.');
507
+ detectSpinner.info('Nenhuma stack detectada automaticamente.');
616
508
  }
617
509
  } catch {
618
- detectSpinner.info('Auto-detecção não disponível — preencha manualmente.');
510
+ detectSpinner.info('Auto-detecção não disponível.');
619
511
  }
620
- } else if (credentialsValid && !isNumericRef) {
621
- console.log(chalk.dim('\n Auto-detecção disponível apenas com page ID numérico.\n'));
622
512
  }
623
513
 
624
514
  // ════════════════════════════════════════════
625
515
  // Phase 3 — Dados do projeto
626
516
  // ════════════════════════════════════════════
627
- console.log(chalk.bold.blue('\n ── Fase 3: Dados do projeto ──\n'));
517
+ console.log(chalk.bold.blue('\n ── Dados do projeto ──\n'));
628
518
 
629
- // B3: verificar se .git já existe antes de oferecer git init
630
519
  const gitAlreadyExists = await checkGitExists();
631
520
  if (gitAlreadyExists) {
632
- console.log(chalk.dim(' .git já existe neste diretório etapa de git init será pulada automaticamente.\n'));
521
+ console.log(chalk.dim(' .git já existe — git init será pulado.\n'));
633
522
  }
634
523
 
635
524
  const defaultStack = detected.stack.length > 0 ? detected.stack.join(', ') : '';
@@ -645,7 +534,7 @@ export async function initCommand() {
645
534
  {
646
535
  type: 'input',
647
536
  name: 'sigla',
648
- message: 'Sigla (2-4 letras maiúsculas, usada como prefixo):',
537
+ message: 'Sigla (2-4 letras maiúsculas):',
649
538
  default: (answers) => deriveSigla(answers.projectName || detected.projectName || ''),
650
539
  filter: v => v.toUpperCase().replace(/[^A-Z]/g, '').slice(0, 4),
651
540
  validate: v => {
@@ -657,7 +546,7 @@ export async function initCommand() {
657
546
  {
658
547
  type: 'input',
659
548
  name: 'stack',
660
- message: 'Stack principal (separado por vírgula, ex: Node.js, React, PostgreSQL):',
549
+ message: 'Stack principal (separado por vírgula):',
661
550
  default: defaultStack || undefined,
662
551
  filter: v => v.split(',').map(s => s.trim()).filter(Boolean),
663
552
  when: () => detected.stack.length === 0
@@ -675,16 +564,10 @@ export async function initCommand() {
675
564
  {
676
565
  type: 'input',
677
566
  name: 'vcsOrg',
678
- message: 'Organização/grupo no VCS (ex: minha-empresa ou meu-usuario):',
567
+ message: 'Organização/grupo no VCS:',
679
568
  when: (answers) => answers.vcs !== 'none',
680
569
  validate: v => v.trim().length > 0 || 'Obrigatório'
681
570
  },
682
- {
683
- type: 'input',
684
- name: 'gchatWebhookUrl',
685
- message: 'Google Chat Webhook URL (opcional — deixe vazio para pular notificações):',
686
- default: ''
687
- },
688
571
  {
689
572
  type: 'confirm',
690
573
  name: 'hasFigma',
@@ -694,23 +577,15 @@ export async function initCommand() {
694
577
  {
695
578
  type: 'input',
696
579
  name: 'figmaFileUrl',
697
- message: 'URL do arquivo Figma (ex: https://www.figma.com/design/ABC123/Nome):',
580
+ message: 'URL do arquivo Figma:',
698
581
  when: (answers) => answers.hasFigma,
699
582
  validate: v => {
700
583
  const trimmed = v.trim();
701
- if (!trimmed) return 'Obrigatório — se não tem URL, responda "Não" na pergunta anterior';
702
- if (!trimmed.includes('figma.com')) return 'URL deve ser do Figma (figma.com)';
584
+ if (!trimmed) return 'Obrigatório';
585
+ if (!trimmed.includes('figma.com')) return 'URL deve ser do Figma';
703
586
  return true;
704
587
  }
705
588
  },
706
- {
707
- type: 'password',
708
- name: 'figmaToken',
709
- message: 'Figma API token (Personal Access Token):',
710
- mask: '*',
711
- when: (answers) => answers.hasFigma && answers.figmaFileUrl?.trim(),
712
- validate: v => v.trim().length > 0 || 'Obrigatório — gere em: Figma > Account Settings > Personal Access Tokens'
713
- },
714
589
  {
715
590
  type: 'confirm',
716
591
  name: 'initGit',
@@ -720,86 +595,39 @@ export async function initCommand() {
720
595
  }
721
596
  ]);
722
597
 
723
- // If Figma URL is empty, treat as no Figma (BUG-026 fix)
724
598
  if (phase3.hasFigma && (!phase3.figmaFileUrl || !phase3.figmaFileUrl.trim())) {
725
599
  phase3.hasFigma = false;
726
600
  phase3.figmaFileUrl = '';
727
601
  }
728
602
 
729
- // Validate Figma token
730
- let figmaTokenValidated = false;
731
- if (phase3.hasFigma && phase3.figmaToken) {
732
- const figmaSpinner = ora('Validando token do Figma...').start();
733
- try {
734
- const result = await figmaRequest(phase3.figmaToken.trim());
735
- figmaSpinner.succeed(`Token Figma validado — usuário: ${result.data.handle || result.data.email || 'OK'}`);
736
- figmaTokenValidated = true;
737
- } catch (err) {
738
- if (err.status === 401 || err.status === 403) {
739
- figmaSpinner.fail(`Token Figma inválido (HTTP ${err.status}). Verifique o Personal Access Token.`);
740
- console.log(chalk.dim(' Gere em: Figma > Account Settings > Personal Access Tokens'));
741
- console.log(chalk.yellow(' Continuando sem Figma...\n'));
742
- phase3.hasFigma = false;
743
- phase3.figmaFileUrl = '';
744
- phase3.figmaToken = '';
745
- } else if (isSslError(err)) {
746
- figmaSpinner.text = 'Certificado SSL corporativo detectado — tentando sem verificação...';
747
- try {
748
- const result = await figmaRequest(phase3.figmaToken.trim(), 15000, true);
749
- figmaSpinner.succeed(`Token Figma validado (SSL corporativo ignorado) — usuário: ${result.data.handle || result.data.email || 'OK'}`);
750
- figmaTokenValidated = true;
751
- } catch (retryErr) {
752
- if (retryErr.status === 401 || retryErr.status === 403) {
753
- figmaSpinner.fail(`Token Figma inválido (HTTP ${retryErr.status}). Verifique o Personal Access Token.`);
754
- console.log(chalk.dim(' Gere em: Figma > Account Settings > Personal Access Tokens'));
755
- console.log(chalk.yellow(' Continuando sem Figma...\n'));
756
- phase3.hasFigma = false;
757
- phase3.figmaFileUrl = '';
758
- phase3.figmaToken = '';
759
- } else {
760
- figmaSpinner.warn(`Não foi possível validar o token Figma: ${retryErr.message}`);
761
- console.log(chalk.yellow(' Token será salvo, mas verifique com "open-spec-kit doctor".\n'));
762
- }
763
- }
764
- } else {
765
- figmaSpinner.warn(`Não foi possível validar o token Figma: ${err.message}`);
766
- console.log(chalk.yellow(' Token será salvo, mas verifique com "open-spec-kit doctor".\n'));
767
- }
768
- }
769
- }
770
-
771
- // B3: se .git já existe, git init é false independente do prompt (que foi pulado)
772
603
  const initGitFinal = gitAlreadyExists ? false : (phase3.initGit ?? false);
773
604
 
774
- // Build unified config object
605
+ // Build config (credentials from global .env)
775
606
  const config = {
776
- agents: phase1.agents,
777
607
  preset: phase1.preset,
778
608
  confluenceLayout: phase1.confluenceLayout,
779
609
  confluenceRef: phase1.confluenceRef.trim(),
780
610
  confluenceUrl,
781
- confluenceUser: phase1.confluenceUser.trim(),
782
- confluenceToken: phase1.confluenceToken.trim(),
611
+ confluenceUser,
612
+ confluenceToken,
783
613
  spaceKey: detected.spaceKey || null,
784
614
  projectName: phase3.projectName.trim(),
785
615
  sigla: phase3.sigla,
786
616
  stack: phase3.stack ?? detected.stack,
787
617
  vcs: phase3.vcs,
788
- // B2: vcsOrg é undefined quando vcs='none' (prompt foi pulado)
789
618
  vcsOrg: phase3.vcs !== 'none' ? (phase3.vcsOrg || '').trim() : '',
790
619
  teamSize: '5',
791
- gchatWebhookUrl: (phase3.gchatWebhookUrl || '').trim(),
620
+ gchatWebhookUrl: globalEnv.GCHAT_WEBHOOK_URL || '',
792
621
  hasFigma: phase3.hasFigma,
793
622
  figmaFileUrl: (phase3.figmaFileUrl || '').trim(),
794
- figmaToken: (phase3.figmaToken || '').trim(),
623
+ figmaToken: globalEnv.FIGMA_API_KEY || '',
795
624
  initGit: initGitFinal,
796
625
  };
797
626
 
798
627
  // ════════════════════════════════════════════
799
- // Phase 4 — Revisão & Confirmação (B4)
628
+ // Revisão & Confirmação
800
629
  // ════════════════════════════════════════════
801
- console.log(chalk.bold.blue('\n ── Fase 4: Revisão ──\n'));
802
- console.log(chalk.bold(' Confira todos os dados antes de gerar os arquivos:\n'));
630
+ console.log(chalk.bold.blue('\n ── Revisão ──\n'));
803
631
 
804
632
  const vcsDisplay = config.vcs === 'none'
805
633
  ? chalk.dim('Nenhum (configurar depois)')
@@ -809,14 +637,11 @@ export async function initCommand() {
809
637
  console.log(` Sigla: ${chalk.cyan(config.sigla)}`);
810
638
  console.log(` Stack: ${chalk.cyan(config.stack.length ? config.stack.join(', ') : '(não definida)')}`);
811
639
  console.log(` Preset: ${chalk.cyan(PRESETS[config.preset].name)}`);
812
- console.log(` AI tools: ${chalk.cyan(config.agents.map(a => AGENTS[a].name).join(', '))}`);
813
640
  console.log(` VCS: ${vcsDisplay}`);
814
- console.log(` Confluence: ${chalk.cyan(config.confluenceRef)} (${config.confluenceLayout === 'space' ? 'space key' : 'page ID'}) @ ${chalk.cyan(config.confluenceUrl)}`);
815
- console.log(` GChat: ${chalk.cyan(config.gchatWebhookUrl ? '(webhook configurado)' : chalk.dim('Não configurado'))}`);
816
- const figmaStatus = config.hasFigma
817
- ? `${config.figmaFileUrl} (${figmaTokenValidated ? 'token validado' : 'token salvo, não validado'})`
818
- : 'Não';
819
- console.log(` Figma: ${chalk.cyan(figmaStatus)}`);
641
+ console.log(` Confluence: ${chalk.cyan(config.confluenceRef)} @ ${chalk.cyan(config.confluenceUrl)}`);
642
+ if (config.hasFigma) {
643
+ console.log(` Figma: ${chalk.cyan(config.figmaFileUrl)}`);
644
+ }
820
645
  if (gitAlreadyExists) {
821
646
  console.log(` Git init: ${chalk.dim('Pulado (.git já existe)')}`);
822
647
  } else {
@@ -832,81 +657,43 @@ export async function initCommand() {
832
657
  }]);
833
658
 
834
659
  if (!confirmed) {
835
- console.log(chalk.yellow('\n Cancelado. Execute open-spec-kit init novamente para recomeçar.\n'));
660
+ console.log(chalk.yellow('\n Cancelado.\n'));
836
661
  return;
837
662
  }
838
663
 
839
664
  // ════════════════════════════════════════════
840
- // File generation
665
+ // File generation — projeto limpo
841
666
  // ════════════════════════════════════════════
842
667
  const genSpinner = ora('Gerando estrutura...').start();
843
668
  const cwd = process.cwd();
844
669
 
845
670
  try {
846
- // Create common directories
847
- const commonDirs = ['specs', 'docs/decisions', 'docs/lessons', 'scripts'];
848
- for (const dir of commonDirs) {
671
+ // Create directories
672
+ for (const dir of ['specs', 'docs/decisions', 'docs/lessons']) {
849
673
  await mkdir(join(cwd, dir), { recursive: true });
850
674
  }
851
675
 
852
- // Generate .env with real credentials (GAP-03 fix)
853
- genSpinner.text = '.env criado';
854
- await writeFile(join(cwd, '.env'), generateEnvFile(config));
855
-
856
- // Generate .env.example
857
- genSpinner.text = '.env.example criado';
858
- await writeFile(join(cwd, '.env.example'), generateEnvExample(config));
859
-
860
- // Generate projects.yml
861
- genSpinner.text = 'projects.yml criado';
676
+ // projects.yml
677
+ genSpinner.text = 'projects.yml';
862
678
  await writeFile(join(cwd, 'projects.yml'), generateProjectsYml(config));
863
679
 
864
- // Generate .gitignore
865
- genSpinner.text = '.gitignore criado';
866
- await writeFile(join(cwd, '.gitignore'), generateGitignore(config));
680
+ // .gitignore
681
+ genSpinner.text = '.gitignore';
682
+ await writeFile(join(cwd, '.gitignore'), generateGitignore());
867
683
 
868
- // Copy shared templates (agents source of truth)
869
- genSpinner.text = 'Copiando skills e rules...';
870
- await copyTemplateDir('agents', cwd);
684
+ // MCP configs (credentials from global .env)
685
+ genSpinner.text = '.mcp.json (Claude Code)';
686
+ await writeFile(join(cwd, '.mcp.json'), generateClaudeMcp(config));
871
687
 
872
- // Copy notify scripts to scripts/ (cross-platform)
873
- genSpinner.text = 'Copiando scripts de notificação...';
874
- const scriptsSource = join(TEMPLATES_DIR, 'agents', 'scripts');
875
- const scriptsTarget = join(cwd, 'scripts');
876
- try {
877
- await access(scriptsSource);
878
- await cp(scriptsSource, scriptsTarget, { recursive: true, force: true });
879
- } catch { /* scripts dir may not exist in older templates */ }
880
-
881
- // Generate per-agent files
882
- if (config.agents.includes('claude')) {
883
- genSpinner.text = 'Configurando Claude Code...';
884
- await writeFile(join(cwd, '.mcp.json'), generateClaudeMcp(config));
885
- }
886
-
887
- if (config.agents.includes('copilot')) {
888
- genSpinner.text = 'Configurando GitHub Copilot...';
889
- await copyTemplateDir('github', cwd);
890
- await mkdir(join(cwd, '.vscode'), { recursive: true });
891
- await writeFile(join(cwd, '.vscode/mcp.json'), generateCopilotMcp(config));
892
- }
688
+ genSpinner.text = '.vscode/mcp.json (Copilot)';
689
+ await mkdir(join(cwd, '.vscode'), { recursive: true });
690
+ await writeFile(join(cwd, '.vscode/mcp.json'), generateCopilotMcp(config));
893
691
 
894
- // Generate docs/architecture.md skeleton
692
+ // docs/architecture.md
693
+ genSpinner.text = 'docs/architecture.md';
895
694
  await writeFile(join(cwd, 'docs/architecture.md'), generateArchitectureSkeleton(config));
896
695
 
897
- genSpinner.succeed('Estrutura gerada com sucesso!');
898
-
899
- // Install mcp-atlassian — cross-platform detection (uvx > pipx > pip)
900
- const mcpSpinner = ora('Verificando mcp-atlassian...').start();
901
- const installResult = tryInstallMcpAtlassian();
902
- if (installResult.installed) {
903
- mcpSpinner.succeed(`mcp-atlassian disponível via ${installResult.method}`);
904
- } else {
905
- mcpSpinner.warn(`${installResult.message}. Instale manualmente:`);
906
- for (const line of getInstallInstructions()) {
907
- console.log(chalk.dim(` ${line}`));
908
- }
909
- }
696
+ genSpinner.succeed('Estrutura gerada!');
910
697
 
911
698
  // Git init
912
699
  if (config.initGit) {
@@ -914,44 +701,24 @@ export async function initCommand() {
914
701
  execSync('git init', { cwd, stdio: 'pipe' });
915
702
  execSync('git add .', { cwd, stdio: 'pipe' });
916
703
  execSync('git commit -m "chore: init spec repo via open-spec-kit"', { cwd, stdio: 'pipe' });
917
- console.log(chalk.green('\n Repositório Git inicializado e commit inicial criado'));
704
+ console.log(chalk.green('\n Repositório Git inicializado'));
918
705
  } catch {
919
- console.log(chalk.yellow('\n Git init falhou — inicialize manualmente:'));
920
- console.log(chalk.dim(' git init && git add . && git commit -m "chore: init spec repo"'));
706
+ console.log(chalk.yellow('\n Git init falhou — inicialize manualmente'));
921
707
  }
922
708
  }
923
709
 
924
710
  // Summary
925
- console.log(chalk.bold('\n Resumo:\n'));
926
- console.log(` Projeto: ${chalk.cyan(config.projectName)}`);
927
- console.log(` Sigla: ${chalk.cyan(config.sigla)}`);
928
- console.log(` Stack: ${chalk.cyan(config.stack.join(', ') || '(não definida)')}`);
929
- console.log(` Preset: ${chalk.cyan(PRESETS[config.preset].name)}`);
930
- console.log(` AI tools: ${chalk.cyan(config.agents.map(a => AGENTS[a].name).join(', '))}`);
931
- const vcsSummary = config.vcs === 'none'
932
- ? chalk.dim('Nenhum (configurar depois)')
933
- : `${chalk.cyan(config.vcs)} (${config.vcsOrg})`;
934
- console.log(` VCS: ${vcsSummary}`);
935
- console.log(` Confluence: ${chalk.cyan(config.confluenceRef)} (${config.confluenceLayout})`);
936
- if (config.hasFigma && config.figmaFileUrl) {
937
- console.log(` Figma: ${chalk.cyan(config.figmaFileUrl)}`);
938
- }
939
-
940
711
  console.log(chalk.bold('\n Arquivos gerados:\n'));
941
- console.log(chalk.dim(' .env — credenciais reais (NÃO comitar)'));
942
- console.log(chalk.dim(' .env.example — template para o time'));
943
712
  console.log(chalk.dim(' projects.yml — configuração do projeto'));
944
- console.log(chalk.dim(' .gitignore ignora .env e .mcp.json'));
945
- if (config.agents.includes('claude')) {
946
- console.log(chalk.dim(' .mcp.json — MCP config para Claude Code'));
947
- }
948
- if (config.agents.includes('copilot')) {
949
- console.log(chalk.dim(' .vscode/mcp.json — MCP config para GitHub Copilot'));
950
- }
713
+ console.log(chalk.dim(' .mcp.json MCP config (Claude Code)'));
714
+ console.log(chalk.dim(' .vscode/mcp.json — MCP config (Copilot)'));
715
+ console.log(chalk.dim(' .gitignore'));
716
+ console.log(chalk.dim(' docs/ — arquitetura, decisões, lições'));
717
+ console.log(chalk.dim(' specs/ — (vazio, preenchido pelo /osk-spec)'));
951
718
 
952
719
  console.log(chalk.bold('\n Próximos passos:\n'));
953
- console.log(' → Execute /setup para fazer o bootstrap no Confluence');
954
- console.log(' → Execute /discovery para analisar a primeira demanda');
720
+ console.log(' → Execute /osk-init para fazer o bootstrap no Confluence');
721
+ console.log(' → Execute /osk-discover para analisar a primeira demanda');
955
722
  console.log('');
956
723
 
957
724
  } catch (err) {
@@ -967,8 +734,6 @@ export {
967
734
  detectRepoNames,
968
735
  deriveSigla,
969
736
  generateProjectsYml,
970
- generateEnvFile,
971
- generateEnvExample,
972
737
  generateGitignore,
973
738
  generateArchitectureSkeleton,
974
739
  };