@spec-wave/cli 0.24.0 → 0.26.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.
- package/README.md +84 -0
- package/bin/spec-wave.mjs +4 -341
- package/package.json +1 -1
- package/src/api/github-rest.mjs +63 -1
- package/src/cli.mjs +400 -0
- package/src/commands/decompose.mjs +23 -16
- package/src/commands/doctor.mjs +32 -0
- package/src/commands/generate-bug.mjs +8 -14
- package/src/commands/generate-plan.mjs +2 -1
- package/src/commands/generate-spec.mjs +2 -1
- package/src/commands/mode.mjs +180 -0
- package/src/commands/run.mjs +491 -0
- package/src/commands/validate.mjs +22 -6
- package/src/config.mjs +4 -1
- package/src/lib/config-file.mjs +62 -0
- package/src/lib/doc-paths.mjs +51 -0
- package/src/lib/execution-mode.mjs +132 -0
- package/src/lib/next-step.mjs +412 -0
- package/src/lib/pr-step.mjs +102 -0
- package/src/lib/repo-links.mjs +84 -0
- package/src/plugin/.claude-plugin/plugin.json +1 -1
- package/src/plugin/skills/doctor/SKILL.md +1 -0
- package/src/plugin/skills/run/SKILL.md +76 -0
- package/src/plugin/skills/spec/SKILL.md +1 -1
- package/src/templates/skill/SKILL.md +24 -1
- package/src/templates/workflows/code-review.yml +5 -1
- package/src/templates/workflows/critique.yml +4 -0
- package/src/templates/workflows/decompose.yml +4 -0
- package/src/templates/workflows/generate-bug.yml +4 -0
- package/src/templates/workflows/generate-plan.yml +4 -0
- package/src/templates/workflows/generate-spec.yml +4 -0
- package/src/templates/workflows/qa.yml +6 -1
- package/src/templates/workflows/validate.yml +4 -0
package/README.md
CHANGED
|
@@ -113,6 +113,24 @@ Arquivo em `.github/config/tech_context.yml` que descreve a stack tecnológica d
|
|
|
113
113
|
|
|
114
114
|
---
|
|
115
115
|
|
|
116
|
+
## Modo de execução: GitHub Actions ou local
|
|
117
|
+
|
|
118
|
+
Todo passo do fluxo roda nos dois lugares — os workflows só instalam a CLI e
|
|
119
|
+
chamam um comando. Para conduzir tudo sem consumir minutos de Actions:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npx @spec-wave/cli@latest mode local # desarma os workflows (variável SPEC_WAVE_EXECUTION)
|
|
123
|
+
npx @spec-wave/cli@latest run <issue> # executa aqui o passo que a label dispararia
|
|
124
|
+
npx @spec-wave/cli@latest mode actions # volta ao CI
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Com a variável setada, cada job é pulado e o run aparece como *skipped* — job que
|
|
128
|
+
não roda não é faturado. `run <issue> --dry-run` explica o próximo passo (e o
|
|
129
|
+
porquê) sem executar nada. Detalhes em
|
|
130
|
+
[`packages/spec-wave/README.md`](packages/spec-wave/README.md#modo-de-execução-actions-ou-local).
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
116
134
|
## Instalação da CLI
|
|
117
135
|
|
|
118
136
|
Não é necessário instalar globalmente — use `npx`:
|
|
@@ -130,6 +148,24 @@ spec-wave --help
|
|
|
130
148
|
|
|
131
149
|
---
|
|
132
150
|
|
|
151
|
+
## Modo de execução: GitHub Actions ou local
|
|
152
|
+
|
|
153
|
+
Todo passo do fluxo roda nos dois lugares — os workflows só instalam a CLI e
|
|
154
|
+
chamam um comando. Para conduzir tudo sem consumir minutos de Actions:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
npx @spec-wave/cli@latest mode local # desarma os workflows (variável SPEC_WAVE_EXECUTION)
|
|
158
|
+
npx @spec-wave/cli@latest run <issue> # executa aqui o passo que a label dispararia
|
|
159
|
+
npx @spec-wave/cli@latest mode actions # volta ao CI
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Com a variável setada, cada job é pulado e o run aparece como *skipped* — job que
|
|
163
|
+
não roda não é faturado. `run <issue> --dry-run` explica o próximo passo (e o
|
|
164
|
+
porquê) sem executar nada. Detalhes em
|
|
165
|
+
[`packages/spec-wave/README.md`](packages/spec-wave/README.md#modo-de-execução-actions-ou-local).
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
133
169
|
## Instalação da Skill
|
|
134
170
|
|
|
135
171
|
A skill permite usar o fluxo diretamente no seu agente via `/spec-wave`.
|
|
@@ -438,6 +474,54 @@ job falha por indisponibilidade, não por erro de configuração.
|
|
|
438
474
|
|
|
439
475
|
---
|
|
440
476
|
|
|
477
|
+
## Modo de execução: Actions ou local
|
|
478
|
+
|
|
479
|
+
Os workflows nunca fizeram o trabalho — eles instalam a CLI e chamam um comando.
|
|
480
|
+
Por isso o fluxo inteiro roda igual na sua máquina, e dá para **desligar o CI**
|
|
481
|
+
quando o que incomoda é o consumo de minutos.
|
|
482
|
+
|
|
483
|
+
```bash
|
|
484
|
+
npx @spec-wave/cli@latest mode # estado atual
|
|
485
|
+
npx @spec-wave/cli@latest mode local # desarma os workflows
|
|
486
|
+
npx @spec-wave/cli@latest run <issue> --dry-run # explica o próximo passo
|
|
487
|
+
npx @spec-wave/cli@latest run <issue> # executa aqui
|
|
488
|
+
npx @spec-wave/cli@latest mode actions # volta ao CI
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
`mode` escreve os **dois** lados do interruptor: `execution.mode` no
|
|
492
|
+
`.spec-wave.json` (o que a CLI e as skills leem) e a variável de repositório
|
|
493
|
+
`SPEC_WAVE_EXECUTION` (o que o `if:` de cada job avalia). Com ela setada, o run
|
|
494
|
+
aparece como *skipped* — **job que não roda não é faturado**. A variável exige
|
|
495
|
+
admin no repositório; sem permissão o comando avisa em vez de fingir que aplicou.
|
|
496
|
+
|
|
497
|
+
`run <issue>` executa o passo que a label dispararia, decidido pelo estado da
|
|
498
|
+
issue: documentos que existem + labels presentes.
|
|
499
|
+
|
|
500
|
+
| Estado | Passo |
|
|
501
|
+
|---|---|
|
|
502
|
+
| Feature sem `spec.md` | `generate-spec` |
|
|
503
|
+
| spec pronta, sem `plan.md` | `generate-plan` (crítica embutida) |
|
|
504
|
+
| sem `spec-wave:plan-approved` | `validate` |
|
|
505
|
+
| validada, sem `decomposition.md` | `decompose` (rascunho) |
|
|
506
|
+
| `spec-wave:decompose-ready` | `decompose-apply` — exige `--apply` |
|
|
507
|
+
| Bug | `generate-bug` → `validate` → triagem (humana) |
|
|
508
|
+
|
|
509
|
+
`run --pr <n>` cobre o lado do PR: `code-review` e, havendo review aprovada,
|
|
510
|
+
também `qa`.
|
|
511
|
+
|
|
512
|
+
Ele **se recusa** a rodar (saindo com código 2) quando há label de gatilho
|
|
513
|
+
pendente na issue — Action em voo, e rodar por cima duplicaria o documento —,
|
|
514
|
+
quando um portão humano da crítica está aplicado (`needs-human`,
|
|
515
|
+
`critique-failed`), quando o documento existe no repositório mas não no seu
|
|
516
|
+
clone (`git pull` primeiro) e quando o passo cria issues sem confirmação
|
|
517
|
+
explícita. `--dry-run` mostra a decisão sem executar nada; `--force`, `--yes`,
|
|
518
|
+
`--apply` e `--step` cobrem os casos em que você sabe o que está fazendo.
|
|
519
|
+
|
|
520
|
+
O `doctor` tem um check para o par config × variável: divergir é o estado
|
|
521
|
+
perigoso — é achar que desligou o CI e continuar pagando por ele.
|
|
522
|
+
|
|
523
|
+
---
|
|
524
|
+
|
|
441
525
|
## Licença
|
|
442
526
|
|
|
443
527
|
MIT
|
package/bin/spec-wave.mjs
CHANGED
|
@@ -1,345 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
+
// Ponto de entrada: a montagem do programa vive em ../src/cli.mjs para que ela
|
|
4
|
+
// possa ser importada (doc gerada, testes de superfície) sem executar a CLI.
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
const pkg = JSON.parse(readFileSync(path.join(__dir, '..', 'package.json'), 'utf-8'));
|
|
6
|
+
import { buildProgram } from '../src/cli.mjs';
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
.name('spec-wave')
|
|
13
|
-
.description('Setup spec-driven GitHub workflow with Projects v2')
|
|
14
|
-
.version(pkg.version)
|
|
15
|
-
// Conta do `gh` a usar nesta execução. Existe porque um GH_TOKEN exportado no
|
|
16
|
-
// shell (um `.envrc` na raiz de um diretório de projetos, por exemplo) vale
|
|
17
|
-
// para repositórios de QUALQUER org abaixo dele — e o GraphQL do GitHub
|
|
18
|
-
// mascara o 403 resultante como "Could not resolve to a Repository".
|
|
19
|
-
.option('--account <login>', 'Conta do gh a usar (vence GITHUB_TOKEN/GH_TOKEN fora do CI)')
|
|
20
|
-
.hook('preAction', async (thisCommand) => {
|
|
21
|
-
const { account } = thisCommand.opts();
|
|
22
|
-
if (account) {
|
|
23
|
-
const { setAccountOverride } = await import('../src/api/auth.mjs');
|
|
24
|
-
setAccountOverride(account);
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
program
|
|
29
|
-
.command('init')
|
|
30
|
-
.description('Configura spec-wave em um repositório GitHub')
|
|
31
|
-
.option('--dry-run', 'Simula a configuração sem fazer alterações')
|
|
32
|
-
.option('--repo <owner/repo>', 'Repositório GitHub (ignora o wizard interativo)')
|
|
33
|
-
.option('--project-title <title>', 'Nome do GitHub Project (padrão: "<repo> — Spec Wave")')
|
|
34
|
-
.option('--skip-project', 'Pula a criação do GitHub Project (use se já foi criado)')
|
|
35
|
-
.option('--skip-labels', 'Pula a criação das labels')
|
|
36
|
-
.option('--skip-files', 'Pula a criação dos arquivos de workflow')
|
|
37
|
-
.option('--provider <provider>', 'Provider de IA dos workflows: anthropic ou openrouter')
|
|
38
|
-
.option('--model <model>', 'Modelo de IA usado pelos workflows (ex.: anthropic/claude-3.7-sonnet)')
|
|
39
|
-
.action(async (options) => {
|
|
40
|
-
const { init } = await import('../src/commands/init.mjs');
|
|
41
|
-
await init(options);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
program
|
|
45
|
-
.command('info')
|
|
46
|
-
.description('Mostra se o repositório atual foi inicializado e os dados do .spec-wave.json')
|
|
47
|
-
.option('--json', 'Saída em JSON (para uso programático)')
|
|
48
|
-
.action(async (options) => {
|
|
49
|
-
const { info } = await import('../src/commands/info.mjs');
|
|
50
|
-
await info(options);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
program
|
|
54
|
-
.command('refresh')
|
|
55
|
-
.description('Atualiza o .spec-wave.json local com os dados atuais do GitHub Project')
|
|
56
|
-
.option('--config', 'Re-consulta o Project e reescreve o .spec-wave.json')
|
|
57
|
-
.option('--stages', 'Acrescenta ao campo "Etapa" as colunas canônicas que faltam (nunca remove)')
|
|
58
|
-
.option('--dry-run', 'Com --stages: mostra o que seria enviado e não escreve nada')
|
|
59
|
-
.option('--yes', 'Com --stages: não pede confirmação')
|
|
60
|
-
.action(async (options) => {
|
|
61
|
-
const { refresh } = await import('../src/commands/refresh.mjs');
|
|
62
|
-
await refresh(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
program
|
|
66
|
-
.command('issue')
|
|
67
|
-
.description('Cria um work item (initiative/epic/feature/story/task...), opcionalmente como sub-issue, e adiciona ao board')
|
|
68
|
-
.requiredOption('--title <title>', 'Título (sem o prefixo de tipo, ex.: [FEATURE])')
|
|
69
|
-
.option('--type <type>', 'Tipo: initiative, epic, feature, story, task, bug, spike ou rfc', 'feature')
|
|
70
|
-
.option('--parent <n>', 'Número da issue pai (cria como sub-issue dela)')
|
|
71
|
-
.option('--body <text>', 'Descrição')
|
|
72
|
-
.option('--priority <p>', 'Prioridade: P0, P1, P2 ou P3')
|
|
73
|
-
.option('--area <area>', 'Área: Frontend, Backend, Mobile, Infra, DevOps ou Data')
|
|
74
|
-
.action(async (options) => {
|
|
75
|
-
const { issue } = await import('../src/commands/issue.mjs');
|
|
76
|
-
await issue(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
program
|
|
80
|
-
.command('initiative')
|
|
81
|
-
.description('Atalho de `issue --type initiative` (nó raiz que agrupa Epics)')
|
|
82
|
-
.requiredOption('--title <title>', 'Título da initiative (sem o prefixo [INITIATIVE])')
|
|
83
|
-
.option('--body <text>', 'Descrição da initiative')
|
|
84
|
-
.option('--priority <p>', 'Prioridade: P0, P1, P2 ou P3 (adiciona label)')
|
|
85
|
-
.option('--area <area>', 'Área: Frontend, Backend, Mobile, Infra, DevOps ou Data')
|
|
86
|
-
.action(async (options) => {
|
|
87
|
-
const { initiative } = await import('../src/commands/initiative.mjs');
|
|
88
|
-
await initiative(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
program
|
|
92
|
-
.command('feature')
|
|
93
|
-
.description('Atalho de `issue --type feature`')
|
|
94
|
-
.requiredOption('--title <title>', 'Título da feature (sem o prefixo [FEATURE])')
|
|
95
|
-
.option('--parent <n>', 'Número da Epic pai (cria como sub-issue dela)')
|
|
96
|
-
.option('--body <text>', 'Descrição da feature')
|
|
97
|
-
.option('--priority <p>', 'Prioridade: P0, P1, P2 ou P3 (adiciona label)')
|
|
98
|
-
.option('--area <area>', 'Área: Frontend, Backend, Mobile, Infra, DevOps ou Data')
|
|
99
|
-
.action(async (options) => {
|
|
100
|
-
const { feature } = await import('../src/commands/feature.mjs');
|
|
101
|
-
await feature(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
program
|
|
105
|
-
.command('bug')
|
|
106
|
-
.description('Cria um Bug (atalho de `issue --type bug`) — nasce em 🐞 Triagem, ou ✅ Ready se P0')
|
|
107
|
-
.requiredOption('--title <title>', 'Título (sem o prefixo [BUG])')
|
|
108
|
-
.option('--parent <n>', 'Feature ou Story afetada (cria como sub-issue dela)')
|
|
109
|
-
.option('--body <text>', 'Descrição: passos, esperado e obtido')
|
|
110
|
-
.option('--priority <p>', 'Severidade: P0, P1, P2 ou P3')
|
|
111
|
-
.option('--area <area>', 'Área: Frontend, Backend, Mobile, Infra, DevOps ou Data')
|
|
112
|
-
.action(async (options) => {
|
|
113
|
-
const { bug } = await import('../src/commands/bug.mjs');
|
|
114
|
-
await bug(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
program
|
|
118
|
-
.command('triage')
|
|
119
|
-
.description('Tria um Bug: accept (→ ✅ Ready), reject (fecha) ou duplicate (fecha)')
|
|
120
|
-
.argument('<action>', 'accept | reject | duplicate')
|
|
121
|
-
.argument('<issue>', 'Número da issue do Bug')
|
|
122
|
-
.option('--reason <texto>', 'Motivo da rejeição (obrigatório em reject)')
|
|
123
|
-
.option('--of <n>', 'Número da issue original (obrigatório em duplicate)')
|
|
124
|
-
.option('--severity <p>', 'Reclassifica a severidade ao aceitar: P0–P3')
|
|
125
|
-
.action(async (action, issue, options) => {
|
|
126
|
-
const { triage } = await import('../src/commands/triage.mjs');
|
|
127
|
-
await triage({ action, issue, ...options })
|
|
128
|
-
.catch(err => { console.error(err.message); process.exit(1); });
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
program
|
|
132
|
-
.command('update')
|
|
133
|
-
.description('Detecta o que está desatualizado (skill, .spec-wave.json, workflows/labels do repo) e atualiza só o que mudou')
|
|
134
|
-
.option('--global', 'Verifica a skill no escopo do usuário (padrão: projeto)')
|
|
135
|
-
.option('--skip-skill', 'Não verifica/atualiza a skill instalada')
|
|
136
|
-
.option('--skip-config', 'Não verifica/atualiza o .spec-wave.json local')
|
|
137
|
-
.option('--skip-repo', 'Não verifica/atualiza workflows e labels do repo')
|
|
138
|
-
.option('--branch [nome]', 'Envia os arquivos do repo como Pull Request numa branch, em um único commit (sem valor: spec-wave/update-v<versão>)')
|
|
139
|
-
.option('--config-in-pr', 'Força incluir o .spec-wave.json no Pull Request')
|
|
140
|
-
.option('--no-config-in-pr', 'Força manter o .spec-wave.json fora do Pull Request')
|
|
141
|
-
.option('--dry-run', 'Mostra o que seria atualizado sem alterar nada')
|
|
142
|
-
.option('--yes', 'Aplica sem pedir confirmação')
|
|
143
|
-
.action(async (options) => {
|
|
144
|
-
const { update } = await import('../src/commands/update.mjs');
|
|
145
|
-
await update(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
program
|
|
149
|
-
.command('install-skill')
|
|
150
|
-
.description('Instala a skill spec-wave no(s) agente(s) detectado(s): Claude Code, Codex, Cursor, opencode, Cline, Kilo, Antigravity, AGENTS.md')
|
|
151
|
-
.option('--agent <names>', 'Agente(s) alvo, separados por vírgula (pula a detecção)')
|
|
152
|
-
.option('--all', 'Instala em todos os agentes detectados')
|
|
153
|
-
.option('--global', 'Instala no escopo do usuário (padrão: projeto)')
|
|
154
|
-
.option('--dry-run', 'Mostra o que seria instalado sem gravar')
|
|
155
|
-
.option('--force', 'Sobrescreve arquivos existentes sem confirmar')
|
|
156
|
-
.option('--yes', 'Modo não-interativo')
|
|
157
|
-
.action(async (options) => {
|
|
158
|
-
const { installSkill } = await import('../src/commands/install-skill.mjs');
|
|
159
|
-
await installSkill(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
program
|
|
163
|
-
.command('uninstall')
|
|
164
|
-
.description('Remove labels, arquivos .github e o .spec-wave.json (mantém o GitHub Project)')
|
|
165
|
-
.option('--repo <owner/repo>', 'Repositório (padrão: lê do .spec-wave.json)')
|
|
166
|
-
.option('--skip-labels', 'Não remove as labels')
|
|
167
|
-
.option('--skip-files', 'Não remove os arquivos .github')
|
|
168
|
-
.option('--keep-config', 'Mantém o .spec-wave.json local')
|
|
169
|
-
.option('--dry-run', 'Mostra o que seria removido sem alterar nada')
|
|
170
|
-
.option('--yes', 'Não pede confirmação')
|
|
171
|
-
.action(async (options) => {
|
|
172
|
-
const { uninstall } = await import('../src/commands/uninstall.mjs');
|
|
173
|
-
await uninstall(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
program
|
|
177
|
-
.command('generate-plan')
|
|
178
|
-
.description('Gera plan.md para uma Feature — roda no GitHub Action ou localmente')
|
|
179
|
-
.requiredOption('--issue-number <n>', 'Número da issue no GitHub')
|
|
180
|
-
.action(async (options) => {
|
|
181
|
-
const { generatePlan } = await import('../src/commands/generate-plan.mjs');
|
|
182
|
-
await generatePlan(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
program
|
|
186
|
-
.command('critique')
|
|
187
|
-
.description('Critica um documento COMO ESTÁ, sem regerar — para depois de corrigi-lo à mão')
|
|
188
|
-
.option('--issue-number <n>', 'Número da issue: critica o plan.md dela e comenta (fluxo canônico)')
|
|
189
|
-
.option('--file <caminho>', 'Critica ESTE arquivo e imprime o resultado — sem issue, sem label, sem contar tentativa')
|
|
190
|
-
.option('--kind <tipo>', 'plan | spec | stories | bug (default: inferido do nome do arquivo)')
|
|
191
|
-
.option('--fail-on-grave', 'Sai com código 1 se houver finding grave (útil em script)')
|
|
192
|
-
.action(async (options) => {
|
|
193
|
-
const { critique } = await import('../src/commands/generate-plan.mjs');
|
|
194
|
-
await critique(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
program
|
|
198
|
-
.command('generate-spec')
|
|
199
|
-
.description('Gera spec.md para uma Feature — roda no GitHub Action ou localmente')
|
|
200
|
-
.requiredOption('--issue-number <n>', 'Número da issue no GitHub')
|
|
201
|
-
.action(async (options) => {
|
|
202
|
-
const { generateSpec } = await import('../src/commands/generate-spec.mjs');
|
|
203
|
-
await generateSpec(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
program
|
|
207
|
-
.command('generate-bug')
|
|
208
|
-
.description('Gera bug.md para um Bug (usado pelo GitHub Action)')
|
|
209
|
-
.requiredOption('--issue-number <n>', 'Número da issue no GitHub')
|
|
210
|
-
.action(async (options) => {
|
|
211
|
-
const { generateBug } = await import('../src/commands/generate-bug.mjs');
|
|
212
|
-
await generateBug(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
program
|
|
216
|
-
.command('validate')
|
|
217
|
-
.description('Valida os documentos de uma issue: spec.md+plan.md de Feature, bug.md de Bug (usado pelo GitHub Action)')
|
|
218
|
-
.requiredOption('--issue-number <n>', 'Número da issue no GitHub')
|
|
219
|
-
.action(async (options) => {
|
|
220
|
-
const { validate } = await import('../src/commands/validate.mjs');
|
|
221
|
-
await validate(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
program
|
|
225
|
-
.command('decompose')
|
|
226
|
-
.description('Gera o rascunho da decomposição em decomposition.md; com --apply, cria as Stories/Tasks a partir do rascunho revisado — roda no Action ou localmente')
|
|
227
|
-
.requiredOption('--issue-number <n>', 'Número da issue no GitHub')
|
|
228
|
-
.option('--apply', 'Aplica o decomposition.md já revisado: cria as issues (sem esta flag, apenas gera/critica o rascunho)')
|
|
229
|
-
.action(async (options) => {
|
|
230
|
-
const { decompose } = await import('../src/commands/decompose.mjs');
|
|
231
|
-
await decompose(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
program
|
|
235
|
-
.command('code-review')
|
|
236
|
-
.description('Move Feature para Code Review ao abrir um PR (usado pelo GitHub Action)')
|
|
237
|
-
.requiredOption('--pr-number <n>', 'Número do Pull Request')
|
|
238
|
-
.option('--resolve-only', 'Só descobre a Feature-alvo e imprime feature=<n>, sem tocar no board')
|
|
239
|
-
.action(async (options) => {
|
|
240
|
-
const { codeReview } = await import('../src/commands/code-review.mjs');
|
|
241
|
-
await codeReview(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
program
|
|
245
|
-
.command('qa')
|
|
246
|
-
.description('Move Feature para QA ao aprovar um PR (usado pelo GitHub Action)')
|
|
247
|
-
.requiredOption('--pr-number <n>', 'Número do Pull Request')
|
|
248
|
-
.action(async (options) => {
|
|
249
|
-
const { qa } = await import('../src/commands/qa.mjs');
|
|
250
|
-
await qa(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
program
|
|
254
|
-
.command('implement')
|
|
255
|
-
.description('Aciona o spec-kit implement para uma Feature (Stories pendentes em ordem de dependência), uma Story (todas as tasks) ou uma Task')
|
|
256
|
-
.argument('<issue>', 'Número da issue (Feature, Story ou Task), ex.: 12 ou #12')
|
|
257
|
-
.option('--feature-dir <path>', 'Caminho do docs/features/<slug> (sobrescreve a resolução automática)')
|
|
258
|
-
.option('--dry-run', 'Monta o contexto e imprime o comando sem executar o spec-kit')
|
|
259
|
-
.action(async (issue, options) => {
|
|
260
|
-
const { implement } = await import('../src/commands/implement.mjs');
|
|
261
|
-
await implement({ issue, ...options }).catch(err => { console.error(err.message); process.exit(1); });
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
program
|
|
265
|
-
.command('order')
|
|
266
|
-
.description('Ordena as Stories pelas dependências (topológica). Sem argumento, o mapa de todas as Features com trabalho')
|
|
267
|
-
.argument('[feature]', 'Número da issue da Feature, ex.: 12 ou #12. Omitido: todas as Features abertas fora de 🎉 Done')
|
|
268
|
-
.action(async (feature) => {
|
|
269
|
-
const { order } = await import('../src/commands/order.mjs');
|
|
270
|
-
await order({ feature }).catch(err => { console.error(err.message); process.exit(1); });
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
program
|
|
274
|
-
.command('task')
|
|
275
|
-
.description('Gerencia uma Task no board: start (Status "In Progress") ou done (Done)')
|
|
276
|
-
.argument('<action>', 'Ação: start ou done')
|
|
277
|
-
.argument('<n>', 'Número da issue da Task, ex.: 12 ou #12')
|
|
278
|
-
.action(async (action, n) => {
|
|
279
|
-
const { task } = await import('../src/commands/task.mjs');
|
|
280
|
-
await task({ action, issue: n }).catch(err => { console.error(err.message); process.exit(1); });
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
program
|
|
284
|
-
.command('move')
|
|
285
|
-
.description('Move qualquer item do board (Feature, Story, Task, Bug, RFC) para uma Etapa — a Etapa nunca retrocede')
|
|
286
|
-
.argument('<n>', 'Número da issue, ex.: 8 ou #8')
|
|
287
|
-
.argument('<etapa>', 'Etapa de destino, com ou sem emoji, ex.: "code review", "Homologação", "🎉 Done"')
|
|
288
|
-
.option('--status <valor>', 'Valor do campo Status no destino: Todo, In Progress ou Done (default: Todo)')
|
|
289
|
-
.action(async (n, etapa, options) => {
|
|
290
|
-
const { move } = await import('../src/commands/move.mjs');
|
|
291
|
-
await move({ issue: n, stage: etapa, ...options })
|
|
292
|
-
.catch(err => { console.error(err.message); process.exit(1); });
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
program
|
|
296
|
-
.command('repair-stage')
|
|
297
|
-
.description('Corrige a Etapa de itens que a automação errou — inclusive retrocedendo. Exige --yes e --reason, e registra o reparo na issue')
|
|
298
|
-
.argument('<issues>', 'Número(s) da(s) issue(s), ex.: 529 ou 529,530,531')
|
|
299
|
-
.argument('<etapa>', 'Etapa correta, com ou sem emoji, ex.: "ready", "✅ Ready"')
|
|
300
|
-
.option('--reason <motivo>', 'Por que o reparo é necessário (vai para o comentário de auditoria)')
|
|
301
|
-
.option('--status <valor>', 'Também corrige o Status: Todo, In Progress ou Done (default: não mexe)')
|
|
302
|
-
.option('--yes', 'Confirma o reparo (obrigatório)')
|
|
303
|
-
.option('--dry-run', 'Mostra o que seria reparado sem alterar nada')
|
|
304
|
-
.action(async (issues, etapa, options) => {
|
|
305
|
-
const { repairStage } = await import('../src/commands/repair-stage.mjs');
|
|
306
|
-
await repairStage(issues, etapa, options)
|
|
307
|
-
.catch(err => { console.error(err.message); process.exit(1); });
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
program
|
|
311
|
-
.command('story')
|
|
312
|
-
.description('Gerencia uma Story no board: review (move para Code Review)')
|
|
313
|
-
.argument('<action>', 'Ação: review')
|
|
314
|
-
.argument('<n>', 'Número da issue da Story, ex.: 12 ou #12')
|
|
315
|
-
.action(async (action, n) => {
|
|
316
|
-
const { story } = await import('../src/commands/story.mjs');
|
|
317
|
-
await story({ action, issue: n }).catch(err => { console.error(err.message); process.exit(1); });
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
program
|
|
321
|
-
.command('dev-agent')
|
|
322
|
-
.description('Instala (--install/--build) ou executa (--run) o spec-wave-agent nesta máquina')
|
|
323
|
-
.option('--install', 'Baixa o binário da release, gera a config e (com --service) o serviço')
|
|
324
|
-
.option('--build', 'Clona o repo do agente e compila com cargo (alternativa ao --install)')
|
|
325
|
-
.option('--run', 'Executa o agente em foreground (Ctrl+C encerra com checkpoint)')
|
|
326
|
-
.option('--service', 'No --install/--build: também instala e habilita systemd/launchd')
|
|
327
|
-
.option('--tag <tag>', 'Release (--install) ou branch/tag (--build); padrão: última release / main')
|
|
328
|
-
.option('--debug', 'No --run: RUST_LOG=debug')
|
|
329
|
-
.option('--dry-run', 'Mostra o que seria instalado sem gravar')
|
|
330
|
-
.option('--force', 'Reinstala o binário e regrava a config')
|
|
331
|
-
.option('--yes', 'Modo não-interativo')
|
|
332
|
-
.action(async (options) => {
|
|
333
|
-
const { devAgent } = await import('../src/commands/dev-agent.mjs');
|
|
334
|
-
await devAgent(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
program
|
|
338
|
-
.command('doctor')
|
|
339
|
-
.description('Diagnostica a configuração do spec-wave no repositório atual')
|
|
340
|
-
.action(async () => {
|
|
341
|
-
const { doctor } = await import('../src/commands/doctor.mjs');
|
|
342
|
-
await doctor().catch(err => { console.error(err.message); process.exit(1); });
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
program.parse();
|
|
8
|
+
buildProgram().parse();
|
package/package.json
CHANGED
package/src/api/github-rest.mjs
CHANGED
|
@@ -445,7 +445,9 @@ export async function getPR(token, owner, repo, prNumber) {
|
|
|
445
445
|
// `ref` é opcional (compatível com os chamadores de 4 argumentos): o modo PR do
|
|
446
446
|
// `update` precisa ler o .spec-wave.json na BASE, não no que a API escolher.
|
|
447
447
|
export async function getFileContent(token, owner, repo, path, ref) {
|
|
448
|
-
|
|
448
|
+
// Quiet: 404 é resultado esperado aqui (o arquivo pode não existir), e a linha
|
|
449
|
+
// `GET ... - 404` no stderr parecia falha no meio da saída do `run`.
|
|
450
|
+
const octokit = makeQuietOctokit(token);
|
|
449
451
|
try {
|
|
450
452
|
const res = await octokit.rest.repos.getContent({
|
|
451
453
|
owner, repo, path, ...(ref ? { ref } : {}),
|
|
@@ -456,3 +458,63 @@ export async function getFileContent(token, owner, repo, path, ref) {
|
|
|
456
458
|
throw err;
|
|
457
459
|
}
|
|
458
460
|
}
|
|
461
|
+
|
|
462
|
+
export async function listPullRequestReviews(token, owner, repo, prNumber) {
|
|
463
|
+
const octokit = makeOctokit(token);
|
|
464
|
+
return await octokit.paginate(octokit.rest.pulls.listReviews, {
|
|
465
|
+
owner, repo, pull_number: prNumber, per_page: 100,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ---------------------------------------------------------------------------
|
|
470
|
+
// Variables do Actions — a chave que desarma os workflows.
|
|
471
|
+
//
|
|
472
|
+
// O `if:` de cada job consulta `vars.SPEC_WAVE_EXECUTION`, e é aqui que a CLI
|
|
473
|
+
// escreve esse valor. Exigem permissão de administração no repositório: um 403
|
|
474
|
+
// não é falha do comando, é informação para o usuário (e o `doctor` a repete).
|
|
475
|
+
// ---------------------------------------------------------------------------
|
|
476
|
+
|
|
477
|
+
/** Valor da variável, ou null se ela não existe. */
|
|
478
|
+
export async function getRepoVariable(token, owner, repo, name) {
|
|
479
|
+
const octokit = makeQuietOctokit(token);
|
|
480
|
+
try {
|
|
481
|
+
const res = await octokit.request('GET /repos/{owner}/{repo}/actions/variables/{name}', {
|
|
482
|
+
owner, repo, name,
|
|
483
|
+
});
|
|
484
|
+
return res.data?.value ?? null;
|
|
485
|
+
} catch (err) {
|
|
486
|
+
if (err.status === 404) return null;
|
|
487
|
+
throw err;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Cria ou atualiza a variável. Devolve 'created' | 'updated'. */
|
|
492
|
+
export async function setRepoVariable(token, owner, repo, name, value) {
|
|
493
|
+
const octokit = makeQuietOctokit(token);
|
|
494
|
+
try {
|
|
495
|
+
await octokit.request('POST /repos/{owner}/{repo}/actions/variables', { owner, repo, name, value });
|
|
496
|
+
return 'created';
|
|
497
|
+
} catch (err) {
|
|
498
|
+
// 409 = já existe. Criar-ou-atualizar em duas chamadas é o contrato da API:
|
|
499
|
+
// não há PUT idempotente para variables.
|
|
500
|
+
if (err.status !== 409) throw err;
|
|
501
|
+
await octokit.request('PATCH /repos/{owner}/{repo}/actions/variables/{name}', {
|
|
502
|
+
owner, repo, name, value,
|
|
503
|
+
});
|
|
504
|
+
return 'updated';
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** Remove a variável. Ausente já é o estado desejado: 404 é no-op. */
|
|
509
|
+
export async function deleteRepoVariable(token, owner, repo, name) {
|
|
510
|
+
const octokit = makeQuietOctokit(token);
|
|
511
|
+
try {
|
|
512
|
+
await octokit.request('DELETE /repos/{owner}/{repo}/actions/variables/{name}', {
|
|
513
|
+
owner, repo, name,
|
|
514
|
+
});
|
|
515
|
+
return true;
|
|
516
|
+
} catch (err) {
|
|
517
|
+
if (err.status === 404) return false;
|
|
518
|
+
throw err;
|
|
519
|
+
}
|
|
520
|
+
}
|