@spec-wave/cli 0.1.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.
@@ -0,0 +1,116 @@
1
+ import * as p from '@clack/prompts';
2
+ import chalk from 'chalk';
3
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import path from 'node:path';
6
+ import { resolveToken } from '../api/auth.mjs';
7
+ import { CONFIG_FILE } from '../config.mjs';
8
+ import { getProjectSnapshot } from '../api/github-graphql.mjs';
9
+
10
+ const __dir = path.dirname(fileURLToPath(import.meta.url));
11
+ const pkg = JSON.parse(readFileSync(path.join(__dir, '..', '..', 'package.json'), 'utf-8'));
12
+
13
+ // Re-consulta o GitHub Project e reescreve o .spec-wave.json local com os dados
14
+ // atuais (id/number/url/title do Project, IDs do campo Etapa e das opções, e a
15
+ // versão da CLI). Útil para repositórios inicializados antes do enriquecimento,
16
+ // ou quando o Project foi renomeado/teve campos alterados.
17
+ export async function refresh(options = {}) {
18
+ if (!options.config) {
19
+ p.log.error('Nada a fazer. Use `spec-wave refresh --config` para atualizar o .spec-wave.json.');
20
+ process.exitCode = 1;
21
+ return;
22
+ }
23
+
24
+ const configPath = path.join(process.cwd(), CONFIG_FILE);
25
+ if (!existsSync(configPath)) {
26
+ p.log.error(`Repositório não inicializado (sem ${CONFIG_FILE}). Rode \`spec-wave init\` primeiro.`);
27
+ process.exitCode = 1;
28
+ return;
29
+ }
30
+
31
+ let config;
32
+ try {
33
+ config = JSON.parse(readFileSync(configPath, 'utf-8'));
34
+ } catch (err) {
35
+ p.log.error(`${CONFIG_FILE} corrompido: ${err.message}`);
36
+ process.exitCode = 1;
37
+ return;
38
+ }
39
+
40
+ const projectId = config.project?.id;
41
+ if (!projectId) {
42
+ p.log.error(
43
+ `${CONFIG_FILE} não tem o id do Project (project.id). ` +
44
+ 'Re-rode `spec-wave init` (sem --skip-project) para registrá-lo.'
45
+ );
46
+ process.exitCode = 1;
47
+ return;
48
+ }
49
+
50
+ let token;
51
+ try {
52
+ token = await resolveToken();
53
+ } catch (err) {
54
+ p.log.error(err.message);
55
+ process.exitCode = 1;
56
+ return;
57
+ }
58
+
59
+ p.intro(chalk.bold('spec-wave refresh --config'));
60
+
61
+ const spinner = p.spinner();
62
+ spinner.start('Consultando o GitHub Project...');
63
+ let snapshot;
64
+ try {
65
+ snapshot = await getProjectSnapshot(token, projectId);
66
+ } catch (err) {
67
+ spinner.stop('');
68
+ p.log.error(`Erro ao consultar o Project: ${err.message}`);
69
+ process.exitCode = 1;
70
+ return;
71
+ }
72
+ if (!snapshot) {
73
+ spinner.stop('');
74
+ p.log.error(`Project ${projectId} não encontrado. Ele pode ter sido excluído.`);
75
+ process.exitCode = 1;
76
+ return;
77
+ }
78
+ spinner.stop('Project consultado.');
79
+
80
+ // Remove campos legados (versões anteriores gravavam etapaFieldId/stageOptions soltos).
81
+ const { etapaFieldId: _e, stageOptions: _s, ...projectRest } = config.project;
82
+ const updated = {
83
+ ...config,
84
+ version: pkg.version,
85
+ project: {
86
+ ...projectRest,
87
+ title: snapshot.title,
88
+ url: snapshot.url,
89
+ id: snapshot.id,
90
+ number: snapshot.number,
91
+ fields: snapshot.fields,
92
+ },
93
+ refreshedAt: new Date().toISOString(),
94
+ };
95
+
96
+ try {
97
+ writeFileSync(configPath, JSON.stringify(updated, null, 2) + '\n');
98
+ } catch (err) {
99
+ p.log.error(`Falha ao gravar ${CONFIG_FILE}: ${err.message}`);
100
+ process.exitCode = 1;
101
+ return;
102
+ }
103
+
104
+ const fieldNames = Object.keys(snapshot.fields || {});
105
+ if (!fieldNames.includes('Etapa')) {
106
+ p.log.warn('Campo "Etapa" não encontrado no Project.');
107
+ }
108
+
109
+ p.note(
110
+ `${chalk.dim('Project:')} ${snapshot.title} (#${snapshot.number})\n` +
111
+ `${chalk.dim('Campos:')} ${fieldNames.length ? fieldNames.join(', ') : '—'}\n` +
112
+ `${chalk.dim('Versão CLI:')} ${pkg.version}`,
113
+ 'Configuração atualizada'
114
+ );
115
+ p.outro(`${CONFIG_FILE} atualizado. Faça commit do arquivo para versioná-lo.`);
116
+ }
@@ -0,0 +1,132 @@
1
+ import * as p from '@clack/prompts';
2
+ import chalk from 'chalk';
3
+ import { readFileSync, existsSync, unlinkSync } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { resolveToken } from '../api/auth.mjs';
6
+ import { CONFIG_FILE, ALL_LABELS, WORKFLOW_FILES, ISSUE_TEMPLATE_FILES } from '../config.mjs';
7
+ import { deleteLabel, deleteFile } from '../api/github-rest.mjs';
8
+
9
+ const REPO_FILES = [
10
+ ...WORKFLOW_FILES.map(f => `.github/workflows/${f}`),
11
+ ...ISSUE_TEMPLATE_FILES.map(f => `.github/ISSUE_TEMPLATE/${f}`),
12
+ ];
13
+
14
+ // Reverte o que o `init` criou — EXCETO o GitHub Project, que nunca é apagado
15
+ // (perderia todo o histórico do board). Remove labels, arquivos .github e o
16
+ // marcador .spec-wave.json local.
17
+ export async function uninstall(options = {}) {
18
+ const configPath = path.join(process.cwd(), CONFIG_FILE);
19
+ const hasConfig = existsSync(configPath);
20
+
21
+ let owner, repo;
22
+ if (options.repo) {
23
+ if (!options.repo.includes('/')) {
24
+ p.log.error('Formato inválido para --repo. Use: owner/repo');
25
+ process.exitCode = 1;
26
+ return;
27
+ }
28
+ [owner, repo] = options.repo.split('/');
29
+ } else if (hasConfig) {
30
+ try {
31
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
32
+ owner = config.owner;
33
+ repo = config.repo;
34
+ } catch (err) {
35
+ p.log.error(`${CONFIG_FILE} corrompido: ${err.message}. Use --repo owner/repo.`);
36
+ process.exitCode = 1;
37
+ return;
38
+ }
39
+ }
40
+
41
+ if (!owner || !repo) {
42
+ p.log.error(`Não encontrei o repositório. Rode dentro de um repo com ${CONFIG_FILE} ou use --repo owner/repo.`);
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+
47
+ const doLabels = !options.skipLabels;
48
+ const doFiles = !options.skipFiles;
49
+ const doConfig = hasConfig && !options.keepConfig;
50
+
51
+ p.intro(chalk.bold('spec-wave uninstall'));
52
+ p.note(
53
+ `${chalk.dim('Repositório:')} ${owner}/${repo}\n\n` +
54
+ `${doLabels ? '✓' : '○'} Labels do repo (${ALL_LABELS.length})\n` +
55
+ `${doFiles ? '✓' : '○'} Arquivos .github (${REPO_FILES.length})\n` +
56
+ `${doConfig ? '✓' : '○'} ${CONFIG_FILE} (local)\n\n` +
57
+ `${chalk.yellow('O GitHub Project NÃO será apagado')} — remova-o manualmente se desejar.`,
58
+ options.dryRun ? 'Dry-run — nada será alterado' : 'Será removido'
59
+ );
60
+
61
+ if (options.dryRun) {
62
+ p.outro('Dry-run concluído.');
63
+ return;
64
+ }
65
+
66
+ if (!options.yes) {
67
+ const ok = await p.confirm({ message: `Remover a configuração do spec-wave de ${owner}/${repo}?`, initialValue: false });
68
+ if (p.isCancel(ok) || !ok) {
69
+ p.cancel('Cancelado. Nada foi alterado.');
70
+ return;
71
+ }
72
+ }
73
+
74
+ let token;
75
+ try {
76
+ token = await resolveToken();
77
+ } catch (err) {
78
+ p.log.error(err.message);
79
+ process.exitCode = 1;
80
+ return;
81
+ }
82
+
83
+ // --- Labels ---
84
+ if (doLabels) {
85
+ const spinner = p.spinner();
86
+ spinner.start('Removendo labels...');
87
+ let removed = 0;
88
+ try {
89
+ for (let i = 0; i < ALL_LABELS.length; i++) {
90
+ spinner.message(`Removendo label ${i + 1}/${ALL_LABELS.length}: ${ALL_LABELS[i].name}`);
91
+ if (await deleteLabel(token, owner, repo, ALL_LABELS[i].name)) removed++;
92
+ }
93
+ spinner.stop(`Labels removidas (${removed}/${ALL_LABELS.length}).`);
94
+ } catch (err) {
95
+ spinner.stop('');
96
+ p.log.warn(`Erro ao remover labels: ${err.message}`);
97
+ }
98
+ }
99
+
100
+ // --- Arquivos .github (cada remoção é um commit) ---
101
+ if (doFiles) {
102
+ const spinner = p.spinner();
103
+ spinner.start('Removendo arquivos...');
104
+ let removed = 0;
105
+ try {
106
+ for (let i = 0; i < REPO_FILES.length; i++) {
107
+ const filePath = REPO_FILES[i];
108
+ spinner.message(`Removendo ${i + 1}/${REPO_FILES.length}: ${filePath}`);
109
+ if (await deleteFile(token, owner, repo, filePath, `chore: remove ${filePath} [spec-wave]`)) removed++;
110
+ }
111
+ spinner.stop(`Arquivos removidos (${removed}/${REPO_FILES.length}).`);
112
+ } catch (err) {
113
+ spinner.stop('');
114
+ p.log.warn(`Erro ao remover arquivos: ${err.message}`);
115
+ }
116
+ }
117
+
118
+ // --- Marcador local ---
119
+ if (doConfig) {
120
+ try {
121
+ unlinkSync(configPath);
122
+ p.log.success(`${CONFIG_FILE} removido.`);
123
+ } catch (err) {
124
+ p.log.warn(`Não foi possível remover ${CONFIG_FILE}: ${err.message}`);
125
+ }
126
+ }
127
+
128
+ p.outro(
129
+ `${chalk.green('✓')} spec-wave removido de ${owner}/${repo}.\n` +
130
+ ' Lembre-se de excluir o GitHub Project manualmente, se quiser.'
131
+ );
132
+ }
@@ -0,0 +1,77 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { resolveToken } from '../api/auth.mjs';
3
+ import { getIssue, removeLabel, addLabel, commentOnIssue } from '../api/github-rest.mjs';
4
+ import { slugify } from '../lib/slugify.mjs';
5
+ import { REQUIRED_PLAN_SECTIONS, REQUIRED_SPEC_SECTIONS } from '../config.mjs';
6
+
7
+ export async function validate({ issueNumber }) {
8
+ const token = await resolveToken();
9
+ const [owner, repo] = (process.env.GITHUB_REPOSITORY || '').split('/');
10
+
11
+ if (!owner || !repo) {
12
+ throw new Error(
13
+ 'GITHUB_REPOSITORY env var não definida.\n' +
14
+ 'Este comando roda no GitHub Actions. Para testar localmente:\n' +
15
+ ' GITHUB_REPOSITORY=owner/repo spec-wave validate --issue-number 1'
16
+ );
17
+ }
18
+
19
+ const issue = await getIssue(token, owner, repo, parseInt(issueNumber, 10));
20
+ const slug = slugify(issue.title);
21
+ const featureDir = `docs/features/${slug}`;
22
+
23
+ const errors = [];
24
+
25
+ // Check plan.md
26
+ const planPath = `${featureDir}/plan.md`;
27
+ if (!existsSync(planPath)) {
28
+ errors.push('❌ `plan.md` não encontrado em `' + planPath + '`');
29
+ } else {
30
+ const planContent = readFileSync(planPath, 'utf-8');
31
+ for (const section of REQUIRED_PLAN_SECTIONS) {
32
+ if (!planContent.includes(`# ${section}`)) {
33
+ errors.push(`❌ Seção obrigatória ausente no plan.md: **${section}**`);
34
+ }
35
+ }
36
+ }
37
+
38
+ // Check spec.md
39
+ const specPath = `${featureDir}/spec.md`;
40
+ if (!existsSync(specPath)) {
41
+ errors.push('❌ `spec.md` não encontrado em `' + specPath + '`');
42
+ } else {
43
+ const specContent = readFileSync(specPath, 'utf-8');
44
+ for (const section of REQUIRED_SPEC_SECTIONS) {
45
+ if (!specContent.includes(`# ${section}`)) {
46
+ errors.push(`❌ Seção obrigatória ausente no spec.md: **${section}**`);
47
+ }
48
+ }
49
+ }
50
+
51
+ // Remove trigger label
52
+ await removeLabel(token, owner, repo, parseInt(issueNumber, 10), 'spec-wave:ready');
53
+
54
+ if (errors.length > 0) {
55
+ await commentOnIssue(
56
+ token, owner, repo, parseInt(issueNumber, 10),
57
+ `⚠️ **Validação falhou — Feature não está pronta.**\n\n` +
58
+ errors.join('\n') +
59
+ `\n\nCorreija os problemas e adicione novamente a label \`spec-wave:ready\`.`
60
+ );
61
+ // Send back to spec stage
62
+ await addLabel(token, owner, repo, parseInt(issueNumber, 10), 'spec-wave:spec');
63
+ console.error('Validação falhou:', errors.join(', '));
64
+ process.exit(1);
65
+ }
66
+
67
+ await commentOnIssue(
68
+ token, owner, repo, parseInt(issueNumber, 10),
69
+ `✅ **Validação concluída com sucesso!**\n\n` +
70
+ `- [\`${planPath}\`](${planPath}) ✓\n` +
71
+ `- [\`${specPath}\`](${specPath}) ✓\n\n` +
72
+ `A Feature está pronta para decomposição. Mova o card para **📋 Backlog Técnico** ou use:\n` +
73
+ `\`\`\`\ngh issue edit ${issueNumber} --add-label "spec-wave:decompose"\n\`\`\``
74
+ );
75
+
76
+ console.log('Validação OK. Feature pronta para decomposição.');
77
+ }
package/src/config.mjs ADDED
@@ -0,0 +1,131 @@
1
+ // Single source of truth for all RFC-001 data
2
+
3
+ // Marcador de configuração gravado pelo `init` na raiz do repo-alvo e lido
4
+ // pelo comando `info` (e pela skill) para detectar se o spec-wave já foi configurado.
5
+ export const CONFIG_FILE = '.spec-wave.json';
6
+
7
+ export const STATUS_OPTIONS = [
8
+ { name: '📥 Backlog', color: 'GRAY' },
9
+ { name: '🎯 Priorizado', color: 'BLUE' },
10
+ { name: '📋 Plan', color: 'YELLOW' },
11
+ { name: '📋 Spec', color: 'YELLOW' },
12
+ { name: '✅ Ready', color: 'GREEN' },
13
+ { name: '📋 Backlog Técnico', color: 'BLUE' },
14
+ { name: '🚧 Desenvolvimento', color: 'ORANGE' },
15
+ { name: '👀 Code Review', color: 'PURPLE' },
16
+ { name: '🧪 QA', color: 'PINK' },
17
+ { name: '📋 Homologação', color: 'YELLOW' },
18
+ { name: '🚀 Deploy', color: 'ORANGE' },
19
+ { name: '🎉 Done', color: 'GREEN' },
20
+ ];
21
+
22
+ export const CUSTOM_FIELDS = [
23
+ {
24
+ name: 'Work Item Type',
25
+ dataType: 'SINGLE_SELECT',
26
+ options: [
27
+ { name: 'Epic', color: 'PURPLE', description: 'Objetivo estratégico' },
28
+ { name: 'Feature', color: 'BLUE', description: 'Capacidade funcional' },
29
+ { name: 'Story', color: 'GREEN', description: 'Necessidade do usuário' },
30
+ { name: 'Task', color: 'YELLOW', description: 'Atividade técnica' },
31
+ { name: 'Bug', color: 'RED', description: 'Defeito a corrigir' },
32
+ { name: 'Spike', color: 'ORANGE', description: 'Investigação técnica' },
33
+ { name: 'RFC', color: 'GRAY', description: 'Proposta de processo' },
34
+ ],
35
+ },
36
+ {
37
+ name: 'Priority',
38
+ dataType: 'SINGLE_SELECT',
39
+ options: [
40
+ { name: 'P0', color: 'RED', description: 'Crítico' },
41
+ { name: 'P1', color: 'ORANGE', description: 'Alta' },
42
+ { name: 'P2', color: 'YELLOW', description: 'Média' },
43
+ { name: 'P3', color: 'GRAY', description: 'Baixa' },
44
+ ],
45
+ },
46
+ {
47
+ name: 'Story Points',
48
+ dataType: 'SINGLE_SELECT',
49
+ options: [
50
+ { name: '1', color: 'GREEN', description: '' },
51
+ { name: '2', color: 'GREEN', description: '' },
52
+ { name: '3', color: 'BLUE', description: '' },
53
+ { name: '5', color: 'BLUE', description: '' },
54
+ { name: '8', color: 'YELLOW', description: '' },
55
+ { name: '13', color: 'ORANGE', description: '' },
56
+ { name: '21', color: 'RED', description: '' },
57
+ ],
58
+ },
59
+ {
60
+ name: 'Area',
61
+ dataType: 'SINGLE_SELECT',
62
+ options: [
63
+ { name: 'Frontend', color: 'BLUE', description: '' },
64
+ { name: 'Backend', color: 'GREEN', description: '' },
65
+ { name: 'Mobile', color: 'PURPLE', description: '' },
66
+ { name: 'Infra', color: 'ORANGE', description: '' },
67
+ { name: 'DevOps', color: 'YELLOW', description: '' },
68
+ { name: 'Data', color: 'PINK', description: '' },
69
+ ],
70
+ },
71
+ {
72
+ name: 'Release',
73
+ dataType: 'TEXT',
74
+ },
75
+ ];
76
+
77
+ // Tipos de work item (Epic, Feature, Story, Task, Bug, Spike, RFC) — derivados do
78
+ // campo "Work Item Type". Usados pelo comando `issue` para validar --type.
79
+ export const WORK_ITEM_TYPES = CUSTOM_FIELDS
80
+ .find(f => f.name === 'Work Item Type')
81
+ .options.map(o => o.name);
82
+
83
+ export const TYPE_LABELS = [
84
+ { name: '[EPIC]', color: '7B61FF', description: 'Objetivo estratégico' },
85
+ { name: '[FEATURE]', color: '0075CA', description: 'Capacidade funcional' },
86
+ { name: '[STORY]', color: '0E8A16', description: 'Necessidade do usuário' },
87
+ { name: '[TASK]', color: 'E4E669', description: 'Atividade técnica' },
88
+ { name: '[BUG]', color: 'D93F0B', description: 'Defeito a corrigir' },
89
+ { name: '[SPIKE]', color: 'E99695', description: 'Investigação técnica' },
90
+ { name: '[RFC]', color: 'EDEDED', description: 'Proposta de processo' },
91
+ ];
92
+
93
+ export const PRIORITY_LABELS = [
94
+ { name: 'P0', color: 'B60205', description: 'Crítico' },
95
+ { name: 'P1', color: 'D93F0B', description: 'Alta' },
96
+ { name: 'P2', color: 'E4E669', description: 'Média' },
97
+ { name: 'P3', color: 'EDEDED', description: 'Baixa' },
98
+ ];
99
+
100
+ export const TRIGGER_LABELS = [
101
+ { name: 'spec-wave:plan', color: 'BFD4F2', description: 'Gerar plan.md via GitHub Action' },
102
+ { name: 'spec-wave:spec', color: 'BFD4F2', description: 'Gerar spec.md via GitHub Action' },
103
+ { name: 'spec-wave:ready', color: '0E8A16', description: 'Validar spec+plan e mover para Ready' },
104
+ { name: 'spec-wave:decompose', color: 'BFD4F2', description: 'Decompor em Stories e Tasks' },
105
+ ];
106
+
107
+ export const ALL_LABELS = [...TYPE_LABELS, ...PRIORITY_LABELS, ...TRIGGER_LABELS];
108
+
109
+ export const WORKFLOW_FILES = [
110
+ 'generate-plan.yml',
111
+ 'generate-spec.yml',
112
+ 'validate.yml',
113
+ 'decompose.yml',
114
+ ];
115
+
116
+ export const ISSUE_TEMPLATE_FILES = [
117
+ 'plan-template.md',
118
+ 'spec-template.md',
119
+ ];
120
+
121
+ export const REQUIRED_PLAN_SECTIONS = [
122
+ 'Frontend',
123
+ 'Backend',
124
+ 'Banco de dados',
125
+ 'Testes',
126
+ ];
127
+
128
+ export const REQUIRED_SPEC_SECTIONS = [
129
+ 'Objetivo',
130
+ 'Critérios de Aceite',
131
+ ];
@@ -0,0 +1,25 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+
3
+ const DEFAULT_MODEL = 'claude-sonnet-4-6';
4
+
5
+ export async function generateDocument(systemPrompt, userContent) {
6
+ const apiKey = process.env.ANTHROPIC_API_KEY;
7
+ if (!apiKey) {
8
+ throw new Error(
9
+ 'ANTHROPIC_API_KEY not set.\n' +
10
+ 'Add it as a GitHub Actions secret or set it in your environment.'
11
+ );
12
+ }
13
+
14
+ const client = new Anthropic({ apiKey });
15
+ const model = process.env.ANTHROPIC_MODEL || DEFAULT_MODEL;
16
+
17
+ const message = await client.messages.create({
18
+ model,
19
+ max_tokens: 4096,
20
+ messages: [{ role: 'user', content: userContent }],
21
+ system: systemPrompt,
22
+ });
23
+
24
+ return message.content[0].text;
25
+ }
@@ -0,0 +1,13 @@
1
+ // Converts an issue title like "[FEATURE] Cadastro de Pedidos com PIX"
2
+ // into a filesystem slug like "cadastro-de-pedidos-com-pix"
3
+ export function slugify(title) {
4
+ return title
5
+ .replace(/^\[.*?\]\s*/, '') // strip [PREFIX] at start
6
+ .toLowerCase()
7
+ .normalize('NFD')
8
+ .replace(/[̀-ͯ]/g, '') // remove diacritics
9
+ .replace(/[^a-z0-9\s-]/g, '') // remove non-alphanumeric
10
+ .trim()
11
+ .replace(/\s+/g, '-') // spaces to hyphens
12
+ .replace(/-+/g, '-'); // collapse multiple hyphens
13
+ }
@@ -0,0 +1,63 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import path from 'node:path';
4
+ import { upsertFile, isRepoInitialized } from '../api/github-rest.mjs';
5
+
6
+ const __dir = path.dirname(fileURLToPath(import.meta.url));
7
+ const TEMPLATES_DIR = path.join(__dir, '..', 'templates');
8
+
9
+ function readTemplate(...parts) {
10
+ return readFileSync(path.join(TEMPLATES_DIR, ...parts), 'utf-8');
11
+ }
12
+
13
+ export async function setupFiles(token, owner, repo, spinner) {
14
+ spinner.message('Verificando repositório...');
15
+ const initialized = await isRepoInitialized(token, owner, repo);
16
+ if (!initialized) {
17
+ throw new Error(
18
+ `O repositório ${owner}/${repo} está vazio (sem commits).\n` +
19
+ `Inicialize-o com um commit antes de continuar:\n` +
20
+ ` gh repo clone ${owner}/${repo} && cd ${repo}\n` +
21
+ ` git commit --allow-empty -m "chore: initial commit" && git push`
22
+ );
23
+ }
24
+
25
+ const filesToCreate = [
26
+ {
27
+ path: '.github/ISSUE_TEMPLATE/plan-template.md',
28
+ content: readTemplate('issue', 'plan-template.md'),
29
+ message: 'chore: add plan.md issue template [spec-wave]',
30
+ },
31
+ {
32
+ path: '.github/ISSUE_TEMPLATE/spec-template.md',
33
+ content: readTemplate('issue', 'spec-template.md'),
34
+ message: 'chore: add spec.md issue template [spec-wave]',
35
+ },
36
+ {
37
+ path: '.github/workflows/generate-plan.yml',
38
+ content: readTemplate('workflows', 'generate-plan.yml'),
39
+ message: 'chore: add generate-plan workflow [spec-wave]',
40
+ },
41
+ {
42
+ path: '.github/workflows/generate-spec.yml',
43
+ content: readTemplate('workflows', 'generate-spec.yml'),
44
+ message: 'chore: add generate-spec workflow [spec-wave]',
45
+ },
46
+ {
47
+ path: '.github/workflows/validate.yml',
48
+ content: readTemplate('workflows', 'validate.yml'),
49
+ message: 'chore: add validate workflow [spec-wave]',
50
+ },
51
+ {
52
+ path: '.github/workflows/decompose.yml',
53
+ content: readTemplate('workflows', 'decompose.yml'),
54
+ message: 'chore: add decompose workflow [spec-wave]',
55
+ },
56
+ ];
57
+
58
+ for (let i = 0; i < filesToCreate.length; i++) {
59
+ const file = filesToCreate[i];
60
+ spinner.message(`Criando arquivo ${i + 1}/${filesToCreate.length}: ${file.path}`);
61
+ await upsertFile(token, owner, repo, file.path, file.content, file.message);
62
+ }
63
+ }
@@ -0,0 +1,17 @@
1
+ import { createLabel } from '../api/github-rest.mjs';
2
+ import { ALL_LABELS } from '../config.mjs';
3
+
4
+ const DELAY_MS = 120;
5
+
6
+ function sleep(ms) {
7
+ return new Promise(r => setTimeout(r, ms));
8
+ }
9
+
10
+ export async function setupLabels(token, owner, repo, spinner) {
11
+ for (let i = 0; i < ALL_LABELS.length; i++) {
12
+ const label = ALL_LABELS[i];
13
+ spinner.message(`Criando label ${i + 1}/${ALL_LABELS.length}: ${label.name}`);
14
+ await createLabel(token, owner, repo, label);
15
+ if (i < ALL_LABELS.length - 1) await sleep(DELAY_MS);
16
+ }
17
+ }
@@ -0,0 +1,62 @@
1
+ import {
2
+ createProject,
3
+ createSingleSelectField,
4
+ createTextField,
5
+ linkProjectToRepo,
6
+ } from '../api/github-graphql.mjs';
7
+ import { getOwnerNodeId, getRepoNodeId } from '../api/github-rest.mjs';
8
+ import { STATUS_OPTIONS, CUSTOM_FIELDS } from '../config.mjs';
9
+
10
+ // The GitHub Projects v2 API does not allow replacing the built-in Status field
11
+ // options atomically ("Position has already been taken" error). We create a custom
12
+ // "Etapa" field with the RFC-001 kanban columns instead, and configure the board
13
+ // view to group by it.
14
+ const ETAPA_FIELD = {
15
+ name: 'Etapa',
16
+ dataType: 'SINGLE_SELECT',
17
+ options: STATUS_OPTIONS,
18
+ };
19
+
20
+ export async function setupProject(token, owner, repo, projectTitle, spinner) {
21
+ spinner.message('Buscando IDs do owner e repositório...');
22
+ const [ownerId, repositoryId] = await Promise.all([
23
+ getOwnerNodeId(token, owner),
24
+ getRepoNodeId(token, owner, repo),
25
+ ]);
26
+
27
+ spinner.message('Criando GitHub Project...');
28
+ const { projectId, projectNumber, projectUrl } = await createProject(token, ownerId, projectTitle);
29
+
30
+ spinner.message('Criando campos customizados...');
31
+ const selectFields = CUSTOM_FIELDS.filter(f => f.dataType === 'SINGLE_SELECT');
32
+ const textFields = CUSTOM_FIELDS.filter(f => f.dataType === 'TEXT');
33
+ const allFields = [ETAPA_FIELD, ...selectFields];
34
+
35
+ // Sequential creation: the GitHub Projects v2 API raises "Position has already
36
+ // been taken" when multiple SINGLE_SELECT fields are created concurrently.
37
+ // Capturamos id + opções de cada campo num mapa `fields` (nome → {id, options})
38
+ // para o .spec-wave.json — o comando `issue` usa isso para setar Etapa e Work Item Type.
39
+ const fields = {};
40
+ for (const f of allFields) {
41
+ spinner.message(`Criando campo "${f.name}"...`);
42
+ fields[f.name] = await createSingleSelectField(token, projectId, f.name, f.options);
43
+ }
44
+ for (const f of textFields) {
45
+ spinner.message(`Criando campo "${f.name}"...`);
46
+ await createTextField(token, projectId, f.name);
47
+ }
48
+
49
+ const result = { projectId, projectNumber, projectUrl, fields };
50
+
51
+ spinner.message('Vinculando projeto ao repositório...');
52
+ try {
53
+ await linkProjectToRepo(token, projectId, repositoryId);
54
+ } catch (err) {
55
+ // Linking is cosmetic (shows project in repo's Projects tab).
56
+ // It requires `repo` scope in addition to `project`. Skip gracefully.
57
+ spinner.message('');
58
+ return { ...result, linkWarning: err.message };
59
+ }
60
+
61
+ return result;
62
+ }
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: "Plano Técnico (plan.md)"
3
+ about: "Plano técnico de uma Feature. Gerado automaticamente ao mover para a coluna 📋 Plan."
4
+ title: "[FEATURE] "
5
+ labels: "[FEATURE]"
6
+ assignees: ""
7
+ ---
8
+
9
+ # Frontend
10
+
11
+ <!-- Componentes, rotas, estados, integrações de UI -->
12
+
13
+ # Backend
14
+
15
+ <!-- Endpoints, serviços, regras de negócio -->
16
+
17
+ # Banco de dados
18
+
19
+ <!-- Migrations, tabelas, índices, queries relevantes -->
20
+
21
+ # Infraestrutura
22
+
23
+ <!-- Configurações de ambiente, variáveis, serviços externos -->
24
+
25
+ # Segurança
26
+
27
+ <!-- Autenticação, autorização, validações críticas -->
28
+
29
+ # Testes
30
+
31
+ <!-- Estratégia de testes: unitários, integração, E2E -->
32
+
33
+ # Estimativa (Story Points)
34
+
35
+ <!-- 1 / 2 / 3 / 5 / 8 / 13 / 21 -->