@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.
- package/README.md +1 -1
- package/bin/open-spec-kit.js +7 -0
- package/package.json +1 -1
- package/src/commands/doctor.js +107 -197
- package/src/commands/init.js +112 -347
- package/src/commands/install.js +393 -0
- package/src/commands/update.js +117 -165
- package/src/schemas/spec.schema.js +3 -3
- package/src/utils/global-path.js +73 -0
- package/templates/agents/agents/spec-hub.agent.md +13 -13
- package/templates/agents/rules/hub_structure.instructions.md +1 -1
- package/templates/agents/rules/ownership.instructions.md +39 -39
- package/templates/agents/skills/dev-orchestrator/SKILL.md +17 -17
- package/templates/agents/skills/discovery/SKILL.md +17 -17
- package/templates/agents/skills/setup-project/SKILL.md +15 -15
- package/templates/agents/skills/specifying-features/SKILL.md +28 -28
- package/templates/github/agents/spec-hub.agent.md +5 -5
- package/templates/github/copilot-instructions.md +9 -9
- package/templates/github/instructions/hub_structure.instructions.md +1 -1
- package/templates/github/instructions/ownership.instructions.md +9 -9
- package/templates/github/skills/dev-orchestrator/SKILL.md +619 -5
- package/templates/github/skills/discovery/SKILL.md +419 -5
- package/templates/github/skills/setup-project/SKILL.md +496 -5
- package/templates/github/skills/specifying-features/SKILL.md +417 -5
- /package/templates/github/prompts/{dev.prompt.md → osk-build.prompt.md} +0 -0
- /package/templates/github/prompts/{discovery.prompt.md → osk-discover.prompt.md} +0 -0
- /package/templates/github/prompts/{setup.prompt.md → osk-init.prompt.md} +0 -0
- /package/templates/github/prompts/{nova-feature.prompt.md → osk-spec.prompt.md} +0 -0
package/src/commands/init.js
CHANGED
|
@@ -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
|
|
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
|
|
9
|
-
import { confluenceRequest,
|
|
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 = ['
|
|
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(
|
|
329
|
-
|
|
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 /
|
|
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 /
|
|
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
|
|
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 —
|
|
432
|
+
// Phase 1 — Projeto
|
|
467
433
|
// ════════════════════════════════════════════
|
|
468
|
-
console.log(chalk.bold.blue('\n ── Fase 1:
|
|
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
|
-
//
|
|
534
|
-
|
|
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
|
-
|
|
570
|
-
|
|
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 ──
|
|
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
|
-
|
|
599
|
-
|
|
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
|
|
507
|
+
detectSpinner.info('Nenhuma stack detectada automaticamente.');
|
|
616
508
|
}
|
|
617
509
|
} catch {
|
|
618
|
-
detectSpinner.info('Auto-detecção não disponível
|
|
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 ──
|
|
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('
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
702
|
-
if (!trimmed.includes('figma.com')) return 'URL deve ser do Figma
|
|
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
|
|
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
|
|
782
|
-
confluenceToken
|
|
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:
|
|
620
|
+
gchatWebhookUrl: globalEnv.GCHAT_WEBHOOK_URL || '',
|
|
792
621
|
hasFigma: phase3.hasFigma,
|
|
793
622
|
figmaFileUrl: (phase3.figmaFileUrl || '').trim(),
|
|
794
|
-
figmaToken:
|
|
623
|
+
figmaToken: globalEnv.FIGMA_API_KEY || '',
|
|
795
624
|
initGit: initGitFinal,
|
|
796
625
|
};
|
|
797
626
|
|
|
798
627
|
// ════════════════════════════════════════════
|
|
799
|
-
//
|
|
628
|
+
// Revisão & Confirmação
|
|
800
629
|
// ════════════════════════════════════════════
|
|
801
|
-
console.log(chalk.bold.blue('\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)}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
|
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
|
|
847
|
-
const
|
|
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
|
-
//
|
|
853
|
-
genSpinner.text = '.
|
|
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
|
-
//
|
|
865
|
-
genSpinner.text = '.gitignore
|
|
866
|
-
await writeFile(join(cwd, '.gitignore'), generateGitignore(
|
|
680
|
+
// .gitignore
|
|
681
|
+
genSpinner.text = '.gitignore';
|
|
682
|
+
await writeFile(join(cwd, '.gitignore'), generateGitignore());
|
|
867
683
|
|
|
868
|
-
//
|
|
869
|
-
genSpinner.text = '
|
|
870
|
-
await
|
|
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
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
704
|
+
console.log(chalk.green('\n Repositório Git inicializado'));
|
|
918
705
|
} catch {
|
|
919
|
-
console.log(chalk.yellow('\n
|
|
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(' .
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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 /
|
|
954
|
-
console.log(' → Execute /
|
|
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
|
};
|