@spec-wave/cli 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/bin/spec-wave.mjs +4 -374
  2. package/package.json +1 -1
  3. package/src/api/github-rest.mjs +25 -0
  4. package/src/cli.mjs +400 -0
  5. package/src/commands/decompose.mjs +186 -45
  6. package/src/commands/doctor.mjs +190 -3
  7. package/src/commands/generate-bug.mjs +22 -16
  8. package/src/commands/generate-plan.mjs +72 -23
  9. package/src/commands/generate-spec.mjs +19 -15
  10. package/src/commands/implement.mjs +40 -20
  11. package/src/commands/mode.mjs +16 -5
  12. package/src/commands/run.mjs +156 -40
  13. package/src/commands/validate.mjs +84 -17
  14. package/src/config.mjs +18 -0
  15. package/src/lib/artifact-pr.mjs +272 -0
  16. package/src/lib/artifact-publish.mjs +169 -0
  17. package/src/lib/doc-availability.mjs +23 -1
  18. package/src/lib/doc-source.mjs +162 -0
  19. package/src/lib/execution-mode.mjs +22 -0
  20. package/src/lib/flow-run.mjs +9 -218
  21. package/src/lib/next-step.mjs +87 -8
  22. package/src/lib/pr-branch.mjs +10 -0
  23. package/src/lib/repo-links.mjs +8 -2
  24. package/src/plugin/.claude-plugin/plugin.json +1 -1
  25. package/src/plugin/skills/bug/SKILL.md +2 -2
  26. package/src/plugin/skills/decompose/SKILL.md +4 -4
  27. package/src/plugin/skills/plan/SKILL.md +1 -1
  28. package/src/plugin/skills/run/SKILL.md +4 -2
  29. package/src/plugin/skills/spec/SKILL.md +4 -4
  30. package/src/plugin/skills/workflow/SKILL.md +2 -2
  31. package/src/templates/skill/SKILL.md +7 -7
  32. package/src/templates/workflows/code-review.yml +13 -2
  33. package/src/templates/workflows/critique.yml +1 -1
  34. package/src/templates/workflows/decompose.yml +13 -2
  35. package/src/templates/workflows/generate-bug.yml +17 -6
  36. package/src/templates/workflows/generate-plan.yml +20 -7
  37. package/src/templates/workflows/generate-spec.yml +20 -7
  38. package/src/templates/workflows/qa.yml +13 -0
package/bin/spec-wave.mjs CHANGED
@@ -1,378 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { program } from 'commander';
4
- import { fileURLToPath } from 'node:url';
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
- const __dir = path.dirname(fileURLToPath(import.meta.url));
9
- const pkg = JSON.parse(readFileSync(path.join(__dir, '..', 'package.json'), 'utf-8'));
6
+ import { buildProgram } from '../src/cli.mjs';
10
7
 
11
- program
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('run')
133
- .description('Executa LOCALMENTE o próximo passo do fluxo (o que a label dispararia no Actions)')
134
- .argument('[issue]', 'Número da issue (Feature, Bug ou RFC)')
135
- .option('--pr <n>', 'Modo PR: decide entre code-review e qa pelo estado das reviews')
136
- .option('--dry-run', 'Decide e explica sem executar nada')
137
- .option('--yes', 'Confirma o passo que exige confirmação')
138
- .option('--apply', 'Autoriza especificamente o decompose-apply (erra se o passo pendente for outro)')
139
- .option('--step <nome>', 'Força um passo: spec | plan | critique | validate | decompose | decompose-apply | bug')
140
- .option('--max-steps <n>', 'Encadeia até N passos (padrão: 1)', '1')
141
- .option('--force', 'Ignora o portão de label de gatilho pendente')
142
- .option('--no-remote-check', 'Não consulta o remoto pelos documentos ausentes (offline)')
143
- .option('--only <passo>', 'No modo --pr: roda só code-review ou só qa')
144
- .option('--json', 'Imprime a decisão em JSON')
145
- .action(async (issue, options) => {
146
- const { run } = await import('../src/commands/run.mjs');
147
- await run(issue, options).catch(err => { console.error(err.message); process.exit(1); });
148
- });
149
-
150
- program
151
- .command('mode')
152
- .description('Mostra ou alterna o modo de execução: `actions` (workflows) ou `local` (esta máquina)')
153
- .argument('[modo]', 'actions | local (sem argumento: só mostra o estado)')
154
- .option('--dry-run', 'Mostra o que mudaria sem alterar nada')
155
- .action(async (target, options) => {
156
- const { mode } = await import('../src/commands/mode.mjs');
157
- await mode({ target, ...options }).catch(err => { console.error(err.message); process.exit(1); });
158
- });
159
-
160
- program
161
- .command('update')
162
- .description('Detecta o que está desatualizado (skill, .spec-wave.json, workflows/labels do repo) e atualiza só o que mudou')
163
- .option('--global', 'Verifica a skill no escopo do usuário (padrão: projeto)')
164
- .option('--skip-skill', 'Não verifica/atualiza a skill instalada')
165
- .option('--skip-config', 'Não verifica/atualiza o .spec-wave.json local')
166
- .option('--skip-repo', 'Não verifica/atualiza workflows e labels do repo')
167
- .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>)')
168
- .option('--config-in-pr', 'Força incluir o .spec-wave.json no Pull Request')
169
- .option('--no-config-in-pr', 'Força manter o .spec-wave.json fora do Pull Request')
170
- .option('--dry-run', 'Mostra o que seria atualizado sem alterar nada')
171
- .option('--yes', 'Aplica sem pedir confirmação')
172
- .action(async (options) => {
173
- const { update } = await import('../src/commands/update.mjs');
174
- await update(options).catch(err => { console.error(err.message); process.exit(1); });
175
- });
176
-
177
- program
178
- .command('install-skill')
179
- .description('Instala a skill spec-wave no(s) agente(s) detectado(s): Claude Code, Codex, Cursor, opencode, Cline, Kilo, Antigravity, AGENTS.md')
180
- .option('--agent <names>', 'Agente(s) alvo, separados por vírgula (pula a detecção)')
181
- .option('--all', 'Instala em todos os agentes detectados')
182
- .option('--global', 'Instala no escopo do usuário (padrão: projeto)')
183
- .option('--dry-run', 'Mostra o que seria instalado sem gravar')
184
- .option('--force', 'Sobrescreve arquivos existentes sem confirmar')
185
- .option('--yes', 'Modo não-interativo')
186
- .action(async (options) => {
187
- const { installSkill } = await import('../src/commands/install-skill.mjs');
188
- await installSkill(options).catch(err => { console.error(err.message); process.exit(1); });
189
- });
190
-
191
- program
192
- .command('uninstall')
193
- .description('Remove labels, arquivos .github e o .spec-wave.json (mantém o GitHub Project)')
194
- .option('--repo <owner/repo>', 'Repositório (padrão: lê do .spec-wave.json)')
195
- .option('--skip-labels', 'Não remove as labels')
196
- .option('--skip-files', 'Não remove os arquivos .github')
197
- .option('--keep-config', 'Mantém o .spec-wave.json local')
198
- .option('--dry-run', 'Mostra o que seria removido sem alterar nada')
199
- .option('--yes', 'Não pede confirmação')
200
- .action(async (options) => {
201
- const { uninstall } = await import('../src/commands/uninstall.mjs');
202
- await uninstall(options).catch(err => { console.error(err.message); process.exit(1); });
203
- });
204
-
205
- program
206
- .command('generate-plan')
207
- .description('Gera plan.md para uma Feature — roda no GitHub Action ou localmente')
208
- .requiredOption('--issue-number <n>', 'Número da issue no GitHub')
209
- .action(async (options) => {
210
- const { generatePlan } = await import('../src/commands/generate-plan.mjs');
211
- await generatePlan(options).catch(err => { console.error(err.message); process.exit(1); });
212
- });
213
-
214
- program
215
- .command('critique')
216
- .description('Critica um documento COMO ESTÁ, sem regerar — para depois de corrigi-lo à mão')
217
- .option('--issue-number <n>', 'Número da issue: critica o plan.md dela e comenta (fluxo canônico)')
218
- .option('--file <caminho>', 'Critica ESTE arquivo e imprime o resultado — sem issue, sem label, sem contar tentativa')
219
- .option('--kind <tipo>', 'plan | spec | stories | bug (default: inferido do nome do arquivo)')
220
- .option('--fail-on-grave', 'Sai com código 1 se houver finding grave (útil em script)')
221
- .action(async (options) => {
222
- const { critique } = await import('../src/commands/generate-plan.mjs');
223
- await critique(options).catch(err => { console.error(err.message); process.exit(1); });
224
- });
225
-
226
- program
227
- .command('generate-spec')
228
- .description('Gera spec.md para uma Feature — roda no GitHub Action ou localmente')
229
- .requiredOption('--issue-number <n>', 'Número da issue no GitHub')
230
- .action(async (options) => {
231
- const { generateSpec } = await import('../src/commands/generate-spec.mjs');
232
- await generateSpec(options).catch(err => { console.error(err.message); process.exit(1); });
233
- });
234
-
235
- program
236
- .command('generate-bug')
237
- .description('Gera bug.md para um Bug — roda no GitHub Action ou localmente')
238
- .requiredOption('--issue-number <n>', 'Número da issue no GitHub')
239
- .action(async (options) => {
240
- const { generateBug } = await import('../src/commands/generate-bug.mjs');
241
- await generateBug(options).catch(err => { console.error(err.message); process.exit(1); });
242
- });
243
-
244
- program
245
- .command('validate')
246
- .description('Valida os documentos de uma issue: spec.md+plan.md de Feature, bug.md de Bug (usado pelo GitHub Action)')
247
- .requiredOption('--issue-number <n>', 'Número da issue no GitHub')
248
- .action(async (options) => {
249
- const { validate } = await import('../src/commands/validate.mjs');
250
- // A reprova é um desfecho esperado do comando, não uma exceção — mas o exit
251
- // code precisa continuar 1 para o job do Actions ficar vermelho.
252
- const result = await validate(options)
253
- .catch(err => { console.error(err.message); process.exit(1); });
254
- if (result?.ok === false) process.exit(1);
255
- });
256
-
257
- program
258
- .command('decompose')
259
- .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')
260
- .requiredOption('--issue-number <n>', 'Número da issue no GitHub')
261
- .option('--apply', 'Aplica o decomposition.md já revisado: cria as issues (sem esta flag, apenas gera/critica o rascunho)')
262
- .action(async (options) => {
263
- const { decompose } = await import('../src/commands/decompose.mjs');
264
- await decompose(options).catch(err => { console.error(err.message); process.exit(1); });
265
- });
266
-
267
- program
268
- .command('code-review')
269
- .description('Move Feature para Code Review ao abrir um PR (usado pelo GitHub Action)')
270
- .requiredOption('--pr-number <n>', 'Número do Pull Request')
271
- .option('--resolve-only', 'Só descobre a Feature-alvo e imprime feature=<n>, sem tocar no board')
272
- .action(async (options) => {
273
- const { codeReview } = await import('../src/commands/code-review.mjs');
274
- await codeReview(options).catch(err => { console.error(err.message); process.exit(1); });
275
- });
276
-
277
- program
278
- .command('qa')
279
- .description('Move Feature para QA ao aprovar um PR (usado pelo GitHub Action)')
280
- .requiredOption('--pr-number <n>', 'Número do Pull Request')
281
- .action(async (options) => {
282
- const { qa } = await import('../src/commands/qa.mjs');
283
- await qa(options).catch(err => { console.error(err.message); process.exit(1); });
284
- });
285
-
286
- program
287
- .command('implement')
288
- .description('Aciona o spec-kit implement para uma Feature (Stories pendentes em ordem de dependência), uma Story (todas as tasks) ou uma Task')
289
- .argument('<issue>', 'Número da issue (Feature, Story ou Task), ex.: 12 ou #12')
290
- .option('--feature-dir <path>', 'Caminho do docs/features/<slug> (sobrescreve a resolução automática)')
291
- .option('--dry-run', 'Monta o contexto e imprime o comando sem executar o spec-kit')
292
- .action(async (issue, options) => {
293
- const { implement } = await import('../src/commands/implement.mjs');
294
- await implement({ issue, ...options }).catch(err => { console.error(err.message); process.exit(1); });
295
- });
296
-
297
- program
298
- .command('order')
299
- .description('Ordena as Stories pelas dependências (topológica). Sem argumento, o mapa de todas as Features com trabalho')
300
- .argument('[feature]', 'Número da issue da Feature, ex.: 12 ou #12. Omitido: todas as Features abertas fora de 🎉 Done')
301
- .action(async (feature) => {
302
- const { order } = await import('../src/commands/order.mjs');
303
- await order({ feature }).catch(err => { console.error(err.message); process.exit(1); });
304
- });
305
-
306
- program
307
- .command('task')
308
- .description('Gerencia uma Task no board: start (Status "In Progress") ou done (Done)')
309
- .argument('<action>', 'Ação: start ou done')
310
- .argument('<n>', 'Número da issue da Task, ex.: 12 ou #12')
311
- .action(async (action, n) => {
312
- const { task } = await import('../src/commands/task.mjs');
313
- await task({ action, issue: n }).catch(err => { console.error(err.message); process.exit(1); });
314
- });
315
-
316
- program
317
- .command('move')
318
- .description('Move qualquer item do board (Feature, Story, Task, Bug, RFC) para uma Etapa — a Etapa nunca retrocede')
319
- .argument('<n>', 'Número da issue, ex.: 8 ou #8')
320
- .argument('<etapa>', 'Etapa de destino, com ou sem emoji, ex.: "code review", "Homologação", "🎉 Done"')
321
- .option('--status <valor>', 'Valor do campo Status no destino: Todo, In Progress ou Done (default: Todo)')
322
- .action(async (n, etapa, options) => {
323
- const { move } = await import('../src/commands/move.mjs');
324
- await move({ issue: n, stage: etapa, ...options })
325
- .catch(err => { console.error(err.message); process.exit(1); });
326
- });
327
-
328
- program
329
- .command('repair-stage')
330
- .description('Corrige a Etapa de itens que a automação errou — inclusive retrocedendo. Exige --yes e --reason, e registra o reparo na issue')
331
- .argument('<issues>', 'Número(s) da(s) issue(s), ex.: 529 ou 529,530,531')
332
- .argument('<etapa>', 'Etapa correta, com ou sem emoji, ex.: "ready", "✅ Ready"')
333
- .option('--reason <motivo>', 'Por que o reparo é necessário (vai para o comentário de auditoria)')
334
- .option('--status <valor>', 'Também corrige o Status: Todo, In Progress ou Done (default: não mexe)')
335
- .option('--yes', 'Confirma o reparo (obrigatório)')
336
- .option('--dry-run', 'Mostra o que seria reparado sem alterar nada')
337
- .action(async (issues, etapa, options) => {
338
- const { repairStage } = await import('../src/commands/repair-stage.mjs');
339
- await repairStage(issues, etapa, options)
340
- .catch(err => { console.error(err.message); process.exit(1); });
341
- });
342
-
343
- program
344
- .command('story')
345
- .description('Gerencia uma Story no board: review (move para Code Review)')
346
- .argument('<action>', 'Ação: review')
347
- .argument('<n>', 'Número da issue da Story, ex.: 12 ou #12')
348
- .action(async (action, n) => {
349
- const { story } = await import('../src/commands/story.mjs');
350
- await story({ action, issue: n }).catch(err => { console.error(err.message); process.exit(1); });
351
- });
352
-
353
- program
354
- .command('dev-agent')
355
- .description('Instala (--install/--build) ou executa (--run) o spec-wave-agent nesta máquina')
356
- .option('--install', 'Baixa o binário da release, gera a config e (com --service) o serviço')
357
- .option('--build', 'Clona o repo do agente e compila com cargo (alternativa ao --install)')
358
- .option('--run', 'Executa o agente em foreground (Ctrl+C encerra com checkpoint)')
359
- .option('--service', 'No --install/--build: também instala e habilita systemd/launchd')
360
- .option('--tag <tag>', 'Release (--install) ou branch/tag (--build); padrão: última release / main')
361
- .option('--debug', 'No --run: RUST_LOG=debug')
362
- .option('--dry-run', 'Mostra o que seria instalado sem gravar')
363
- .option('--force', 'Reinstala o binário e regrava a config')
364
- .option('--yes', 'Modo não-interativo')
365
- .action(async (options) => {
366
- const { devAgent } = await import('../src/commands/dev-agent.mjs');
367
- await devAgent(options).catch(err => { console.error(err.message); process.exit(1); });
368
- });
369
-
370
- program
371
- .command('doctor')
372
- .description('Diagnostica a configuração do spec-wave no repositório atual')
373
- .action(async () => {
374
- const { doctor } = await import('../src/commands/doctor.mjs');
375
- await doctor().catch(err => { console.error(err.message); process.exit(1); });
376
- });
377
-
378
- program.parse();
8
+ buildProgram().parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spec-wave/cli",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "Setup spec-driven GitHub workflow with Projects v2, labels, issue templates, and AI-powered Actions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -311,6 +311,31 @@ export async function deleteLabel(token, owner, repo, name) {
311
311
  }
312
312
  }
313
313
 
314
+ /**
315
+ * Apaga a ref de uma branch.
316
+ *
317
+ * Existe para UM caso, e só ele: a branch de artefato que sobrou de um PR
318
+ * mergeado com squash. Nesse merge a ponta da branch não é ancestral da base, e
319
+ * empilhar o próximo commit nela produziria um PR que reintroduz estado antigo.
320
+ * Sem PR aberto e sem commits à frente da base, a branch não guarda nada — pode
321
+ * ser recriada a partir da base.
322
+ *
323
+ * NUNCA use isto para resolver conflito: apagar uma branch com PR aberto
324
+ * descartaria revisão humana.
325
+ *
326
+ * @returns {Promise<boolean>} false quando a branch já não existia
327
+ */
328
+ export async function deleteBranch(token, owner, repo, branch) {
329
+ const octokit = makeQuietOctokit(token); // 404 = já não existe, não é erro
330
+ try {
331
+ await octokit.rest.git.deleteRef({ owner, repo, ref: `heads/${branch}` });
332
+ return true;
333
+ } catch (err) {
334
+ if (err.status === 404 || err.status === 422) return false;
335
+ throw err;
336
+ }
337
+ }
338
+
314
339
  export async function deleteFile(token, owner, repo, filePath, message) {
315
340
  const octokit = makeOctokit(token);
316
341
  let sha;