@spec-wave/cli 0.24.0 → 0.25.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 +35 -2
- package/package.json +1 -1
- package/src/api/github-rest.mjs +63 -1
- package/src/commands/decompose.mjs +3 -10
- 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 +169 -0
- package/src/commands/run.mjs +396 -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 +110 -0
- package/src/lib/next-step.mjs +356 -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
|
@@ -128,6 +128,35 @@ program
|
|
|
128
128
|
.catch(err => { console.error(err.message); process.exit(1); });
|
|
129
129
|
});
|
|
130
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
|
+
|
|
131
160
|
program
|
|
132
161
|
.command('update')
|
|
133
162
|
.description('Detecta o que está desatualizado (skill, .spec-wave.json, workflows/labels do repo) e atualiza só o que mudou')
|
|
@@ -205,7 +234,7 @@ program
|
|
|
205
234
|
|
|
206
235
|
program
|
|
207
236
|
.command('generate-bug')
|
|
208
|
-
.description('Gera bug.md para um Bug
|
|
237
|
+
.description('Gera bug.md para um Bug — roda no GitHub Action ou localmente')
|
|
209
238
|
.requiredOption('--issue-number <n>', 'Número da issue no GitHub')
|
|
210
239
|
.action(async (options) => {
|
|
211
240
|
const { generateBug } = await import('../src/commands/generate-bug.mjs');
|
|
@@ -218,7 +247,11 @@ program
|
|
|
218
247
|
.requiredOption('--issue-number <n>', 'Número da issue no GitHub')
|
|
219
248
|
.action(async (options) => {
|
|
220
249
|
const { validate } = await import('../src/commands/validate.mjs');
|
|
221
|
-
|
|
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);
|
|
222
255
|
});
|
|
223
256
|
|
|
224
257
|
program
|
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
|
+
}
|
|
@@ -33,7 +33,8 @@ import {
|
|
|
33
33
|
import { recordUsage } from '../lib/usage-report.mjs';
|
|
34
34
|
import { formatDependencyLine, orderStories, renderOrderComment } from '../lib/dependencies.mjs';
|
|
35
35
|
import { lintLanguage } from '../lib/output-lint.mjs';
|
|
36
|
-
import {
|
|
36
|
+
import { resolveDocDir } from '../lib/doc-paths.mjs';
|
|
37
|
+
import { docBlobUrl } from '../lib/repo-links.mjs';
|
|
37
38
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
38
39
|
import { loadConfig } from '../lib/project-root.mjs';
|
|
39
40
|
import { resolveFlowContext, commitGenerated } from '../lib/flow-run.mjs';
|
|
@@ -222,14 +223,6 @@ export function resolveInheritedMilestone(issue) {
|
|
|
222
223
|
return Number.isInteger(number) && number > 0 ? number : undefined;
|
|
223
224
|
}
|
|
224
225
|
|
|
225
|
-
// Diretório do documento por tipo. Feature usa o mesmo docs/features/<slug> da
|
|
226
|
-
// spec/plan; RFC ganha o seu, já que não passa por spec/plan.
|
|
227
|
-
function resolveDocDir(root, issue, type) {
|
|
228
|
-
const slug = slugify(issue.title);
|
|
229
|
-
const rel = type === 'RFC' ? `docs/rfcs/${slug}` : `docs/features/${slug}`;
|
|
230
|
-
return { slug, rel, dir: path.resolve(root || process.cwd(), rel) };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
226
|
// Lint de idioma sobre títulos+corpos gerados; retorna aviso pronto para
|
|
234
227
|
// anexar ao comentário final ('' se limpo).
|
|
235
228
|
function formatItemsLintWarning(texts) {
|
|
@@ -262,7 +255,7 @@ async function draftDecomposition(ctx) {
|
|
|
262
255
|
const { token, owner, repo, issue, issueNumber, type, labels, usage, root, runMode, docDir, docPath, docRel } = ctx;
|
|
263
256
|
const number = parseInt(issueNumber, 10);
|
|
264
257
|
const kind = DECOMPOSE_TARGETS[type]; // Feature → 'stories'; RFC → 'tasks'
|
|
265
|
-
const blobUrl =
|
|
258
|
+
const blobUrl = docBlobUrl({ owner, repo, pathRel: docRel, mode: runMode, root });
|
|
266
259
|
|
|
267
260
|
const specPath = path.join(docDir, 'spec.md');
|
|
268
261
|
const planPath = path.join(docDir, 'plan.md');
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
tokenMismatchWarning, parseActiveAccount,
|
|
14
14
|
} from '../api/auth.mjs';
|
|
15
15
|
import { getProjectSnapshot, listSubIssues } from '../api/github-graphql.mjs';
|
|
16
|
+
import { getRepoVariable } from '../api/github-rest.mjs';
|
|
16
17
|
import {
|
|
17
18
|
CONFIG_FILE, WORKFLOW_FILES, getProvider, DEFAULT_PROVIDER, AI_PROVIDERS, STATUS_OPTIONS,
|
|
18
19
|
RETIRED_STAGES, ALL_LABELS, allLabelsFor, LABEL_NEEDS_HUMAN, MODEL_LABEL_PREFIX,
|
|
@@ -20,6 +21,8 @@ import {
|
|
|
20
21
|
modelLabels,
|
|
21
22
|
} from '../config.mjs';
|
|
22
23
|
import { findConfigPath } from '../lib/project-root.mjs';
|
|
24
|
+
import { configuredMode, describeModeState, EXECUTION_VARIABLE } from '../lib/execution-mode.mjs';
|
|
25
|
+
import { unguardedWorkflows } from './mode.mjs';
|
|
23
26
|
import {
|
|
24
27
|
DEFAULT_MAX_TOKENS, supportsStrictSchema, resolveAiConfig,
|
|
25
28
|
} from '../lib/claude.mjs';
|
|
@@ -936,6 +939,34 @@ export function checkSpecKit(ctx) {
|
|
|
936
939
|
};
|
|
937
940
|
}
|
|
938
941
|
|
|
942
|
+
// Modo de execução: o config e a variável do repositório precisam concordar.
|
|
943
|
+
// Discordar não é detalhe — é o usuário achando que desligou os workflows e
|
|
944
|
+
// continuando a pagar minutos (ou o contrário: tudo pulado e nada rodando).
|
|
945
|
+
async function checkExecutionMode(ctx) {
|
|
946
|
+
const name = 'Modo de execução (Actions × local)';
|
|
947
|
+
const configured = configuredMode(ctx.cfg);
|
|
948
|
+
|
|
949
|
+
let variable; // undefined = não verificável
|
|
950
|
+
if (ctx.token && ctx.cfg?.owner && ctx.cfg?.repo) {
|
|
951
|
+
try {
|
|
952
|
+
variable = await getRepoVariable(ctx.token, ctx.cfg.owner, ctx.cfg.repo, EXECUTION_VARIABLE);
|
|
953
|
+
} catch (err) {
|
|
954
|
+
variable = undefined;
|
|
955
|
+
if (err.status !== 403 && err.status !== 404) {
|
|
956
|
+
return { name, status: 'warn', detail: `Variável não verificável agora: ${err.message}` };
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const estado = describeModeState({
|
|
962
|
+
configured,
|
|
963
|
+
variable,
|
|
964
|
+
unguardedWorkflows: unguardedWorkflows(ctx.root || ctx.cwd),
|
|
965
|
+
});
|
|
966
|
+
const status = estado.status === 'problem' ? 'fail' : estado.status;
|
|
967
|
+
return { name, status, detail: [estado.summary, ...estado.notes, ...estado.fixes].join('\n') };
|
|
968
|
+
}
|
|
969
|
+
|
|
939
970
|
async function checkWorkflows(ctx) {
|
|
940
971
|
const name = 'Workflows do Actions';
|
|
941
972
|
// Ancorado na raiz do projeto, não no cwd: rodar o doctor de um subdiretório
|
|
@@ -1037,6 +1068,7 @@ export async function doctor() {
|
|
|
1037
1068
|
checkDecompositions,
|
|
1038
1069
|
checkSpecKit,
|
|
1039
1070
|
checkWorkflows,
|
|
1071
|
+
checkExecutionMode,
|
|
1040
1072
|
];
|
|
1041
1073
|
const results = [];
|
|
1042
1074
|
const spinner = p.spinner();
|
|
@@ -15,11 +15,11 @@ import {
|
|
|
15
15
|
import { generateDocument } from '../lib/claude.mjs';
|
|
16
16
|
import { unwrapGeneratedDoc } from '../lib/unwrap-doc.mjs';
|
|
17
17
|
import { recordUsage } from '../lib/usage-report.mjs';
|
|
18
|
-
import { loadConfig } from '../lib/project-root.mjs';
|
|
19
18
|
import { loadPrompt, systemPromptWithTools } from '../lib/prompt-loader.mjs';
|
|
20
19
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
21
20
|
import { bugDocPaths } from '../lib/bug-doc.mjs';
|
|
22
|
-
import { commitGenerated,
|
|
21
|
+
import { commitGenerated, resolveFlowContext } from '../lib/flow-run.mjs';
|
|
22
|
+
import { docBlobUrl } from '../lib/repo-links.mjs';
|
|
23
23
|
import {
|
|
24
24
|
runCritique, resolveCritiqueAttempt, renderNeedsHumanComment,
|
|
25
25
|
} from '../lib/critique.mjs';
|
|
@@ -48,18 +48,12 @@ function isSpecWaveComment(body) {
|
|
|
48
48
|
|
|
49
49
|
export async function generateBug({ issueNumber }) {
|
|
50
50
|
const token = await resolveToken();
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
// Mesmo contexto dos outros geradores: GITHUB_REPOSITORY quando existe, o
|
|
52
|
+
// `.spec-wave.json` quando não. Este comando exigia a env crua e morria fora
|
|
53
|
+
// do runner mandando exportá-la — dentro de um repo que já sabe seu owner/repo.
|
|
54
|
+
const { owner, repo, root, config, mode } = resolveFlowContext({ command: 'generate-bug' });
|
|
53
55
|
const n = parseInt(issueNumber, 10);
|
|
54
56
|
|
|
55
|
-
if (!owner || !repo) {
|
|
56
|
-
throw new Error(
|
|
57
|
-
'GITHUB_REPOSITORY env var não definida.\n' +
|
|
58
|
-
'Este comando roda no GitHub Actions. Para testar localmente:\n' +
|
|
59
|
-
' GITHUB_REPOSITORY=owner/repo spec-wave generate-bug --issue-number 1'
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
57
|
console.log(`Buscando issue #${n}...`);
|
|
64
58
|
const issue = await getIssue(token, owner, repo, n);
|
|
65
59
|
|
|
@@ -126,7 +120,7 @@ export async function generateBug({ issueNumber }) {
|
|
|
126
120
|
filePath: fileAbs,
|
|
127
121
|
content,
|
|
128
122
|
message: `docs: generate bug.md for ${slug} [spec-wave]`,
|
|
129
|
-
mode
|
|
123
|
+
mode,
|
|
130
124
|
});
|
|
131
125
|
if (published.warning) console.warn(`⚠️ ${published.warning}`);
|
|
132
126
|
|
|
@@ -142,7 +136,7 @@ export async function generateBug({ issueNumber }) {
|
|
|
142
136
|
await commentOnIssue(
|
|
143
137
|
token, owner, repo, n,
|
|
144
138
|
'🐞 **bug.md gerado automaticamente!**\n\n' +
|
|
145
|
-
`📄 Arquivo: [\`${fileRel}\`](
|
|
139
|
+
`📄 Arquivo: [\`${fileRel}\`](${docBlobUrl({ owner, repo, pathRel: fileRel, mode, root, config })})\n\n` +
|
|
146
140
|
'Revise a **causa raiz** e o **teste de regressão** — são as duas seções que decidem se ' +
|
|
147
141
|
'a correção ataca o defeito ou o sintoma. Quando estiver pronto, valide com:\n' +
|
|
148
142
|
`\`\`\`\ngh issue edit ${n} --add-label "spec-wave:ready"\n\`\`\`` +
|
|
@@ -20,6 +20,7 @@ import { recordUsage } from '../lib/usage-report.mjs';
|
|
|
20
20
|
import { slugify } from '../lib/slugify.mjs';
|
|
21
21
|
import { resolveFromRoot } from '../lib/project-root.mjs';
|
|
22
22
|
import { resolveFlowContext, commitGenerated } from '../lib/flow-run.mjs';
|
|
23
|
+
import { docBlobUrl } from '../lib/repo-links.mjs';
|
|
23
24
|
import { buildTechContext } from '../lib/tech-context.mjs';
|
|
24
25
|
import { loadPrompt, systemPromptWithTools } from '../lib/prompt-loader.mjs';
|
|
25
26
|
|
|
@@ -213,7 +214,7 @@ export async function generatePlan({ issueNumber }) {
|
|
|
213
214
|
await commentOnIssue(
|
|
214
215
|
token, owner, repo, parseInt(issueNumber, 10),
|
|
215
216
|
`📋 **plan.md gerado automaticamente!**\n\n` +
|
|
216
|
-
`📄 Arquivo: [\`${fileRel}\`](
|
|
217
|
+
`📄 Arquivo: [\`${fileRel}\`](${docBlobUrl({ owner, repo, pathRel: fileRel, mode, root, config })})\n\n` +
|
|
217
218
|
`Revise o plano e, quando estiver pronto, valide a Feature: mova o card para **✅ Ready** ou use:\n` +
|
|
218
219
|
`\`\`\`\ngh issue edit ${issueNumber} --add-label "spec-wave:ready"\n\`\`\`` +
|
|
219
220
|
formatLintWarning(lintFindings)
|
|
@@ -7,6 +7,7 @@ import { recordUsage } from '../lib/usage-report.mjs';
|
|
|
7
7
|
import { slugify } from '../lib/slugify.mjs';
|
|
8
8
|
import { resolveFromRoot } from '../lib/project-root.mjs';
|
|
9
9
|
import { resolveFlowContext, commitGenerated } from '../lib/flow-run.mjs';
|
|
10
|
+
import { docBlobUrl } from '../lib/repo-links.mjs';
|
|
10
11
|
import { loadPrompt, systemPromptWithTools } from '../lib/prompt-loader.mjs';
|
|
11
12
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
12
13
|
import {
|
|
@@ -110,7 +111,7 @@ export async function generateSpec({ issueNumber }) {
|
|
|
110
111
|
await commentOnIssue(
|
|
111
112
|
token, owner, repo, parseInt(issueNumber, 10),
|
|
112
113
|
`📋 **spec.md gerado automaticamente!**\n\n` +
|
|
113
|
-
`📄 Arquivo: [\`${fileRel}\`](
|
|
114
|
+
`📄 Arquivo: [\`${fileRel}\`](${docBlobUrl({ owner, repo, pathRel: fileRel, mode, root })})\n\n` +
|
|
114
115
|
`Revise a especificação e, quando estiver pronto, gere o plano técnico: mova o card para **📋 Plan** ou use:\n` +
|
|
115
116
|
`\`\`\`\ngh issue edit ${issueNumber} --add-label "spec-wave:plan"\n\`\`\`` +
|
|
116
117
|
formatLintWarning(lintFindings)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Alterna entre rodar o fluxo no GitHub Actions e rodar nesta máquina.
|
|
2
|
+
//
|
|
3
|
+
// O comando escreve os DOIS lados do interruptor — o `.spec-wave.json`, que a
|
|
4
|
+
// CLI e a skill leem, e a variável de repositório, que o `if:` de cada job
|
|
5
|
+
// avalia. Escrever só um deles é o defeito que o comando existe para evitar:
|
|
6
|
+
// config em "local" com a variável ausente significa workflow disparando e
|
|
7
|
+
// minuto sendo cobrado enquanto o usuário acha que desligou.
|
|
8
|
+
//
|
|
9
|
+
// A variável exige permissão de administração. Sem ela o comando NÃO finge que
|
|
10
|
+
// deu certo: grava o config, diz o que falta e aponta a alternativa manual.
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
import * as p from '@clack/prompts';
|
|
16
|
+
import chalk from 'chalk';
|
|
17
|
+
|
|
18
|
+
import { resolveToken } from '../api/auth.mjs';
|
|
19
|
+
import { getRepoVariable, setRepoVariable, deleteRepoVariable } from '../api/github-rest.mjs';
|
|
20
|
+
import { CONFIG_FILE, WORKFLOW_FILES } from '../config.mjs';
|
|
21
|
+
import { updateConfig } from '../lib/config-file.mjs';
|
|
22
|
+
import {
|
|
23
|
+
EXECUTION_MODES, EXECUTION_VARIABLE, EXECUTION_GUARD,
|
|
24
|
+
configuredMode, variableValueFor, describeModeState,
|
|
25
|
+
} from '../lib/execution-mode.mjs';
|
|
26
|
+
import { loadConfig } from '../lib/project-root.mjs';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Workflows instalados que NÃO carregam a guarda (função quase pura — só lê o fs).
|
|
30
|
+
*
|
|
31
|
+
* @param {string|null} root
|
|
32
|
+
* @returns {string[]}
|
|
33
|
+
*/
|
|
34
|
+
export function unguardedWorkflows(root) {
|
|
35
|
+
const dir = path.join(root || process.cwd(), '.github', 'workflows');
|
|
36
|
+
if (!existsSync(dir)) return [];
|
|
37
|
+
const presentes = new Set(readdirSync(dir));
|
|
38
|
+
return WORKFLOW_FILES
|
|
39
|
+
.filter(file => presentes.has(file))
|
|
40
|
+
.filter(file => !readFileSync(path.join(dir, file), 'utf-8').includes(EXECUTION_GUARD));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// A variável só é legível por quem administra o repo. 403 não é falha: é "não
|
|
44
|
+
// verificável", e quem chama distingue isso de "não existe" (null).
|
|
45
|
+
async function readVariable(token, owner, repo) {
|
|
46
|
+
try {
|
|
47
|
+
return await getRepoVariable(token, owner, repo, EXECUTION_VARIABLE);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (err.status === 403 || err.status === 404) return undefined;
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function mode({ target, dryRun = false } = {}) {
|
|
55
|
+
p.intro(chalk.bold('spec-wave mode'));
|
|
56
|
+
|
|
57
|
+
const { config, root, configPath } = loadConfig();
|
|
58
|
+
if (!config) {
|
|
59
|
+
p.log.error(`${CONFIG_FILE} não encontrado — rode \`npx @spec-wave/cli@latest init\` antes.`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const alvo = target ? String(target).toLowerCase() : null;
|
|
64
|
+
if (alvo && !EXECUTION_MODES.includes(alvo)) {
|
|
65
|
+
p.log.error(`Modo inválido: ${alvo}. Use um de: ${EXECUTION_MODES.join(', ')}.`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const atual = configuredMode(config);
|
|
70
|
+
const owner = config.owner;
|
|
71
|
+
const repo = config.repo;
|
|
72
|
+
|
|
73
|
+
let token = null;
|
|
74
|
+
let variavel;
|
|
75
|
+
if (owner && repo) {
|
|
76
|
+
try {
|
|
77
|
+
token = await resolveToken();
|
|
78
|
+
variavel = await readVariable(token, owner, repo);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
p.log.warn(`Não foi possível consultar a variável do repositório: ${err.message}`);
|
|
81
|
+
variavel = undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Sem argumento: só relatório.
|
|
86
|
+
if (!alvo) {
|
|
87
|
+
const estado = describeModeState({
|
|
88
|
+
configured: atual, variable: variavel, unguardedWorkflows: unguardedWorkflows(root),
|
|
89
|
+
});
|
|
90
|
+
p.log.info(estado.summary);
|
|
91
|
+
for (const nota of estado.notes) p.log.message(`• ${nota}`);
|
|
92
|
+
for (const fix of estado.fixes) p.log.warn(fix);
|
|
93
|
+
p.outro(
|
|
94
|
+
atual === 'local'
|
|
95
|
+
? `Próximo passo de uma issue: ${chalk.cyan('spec-wave run <issue>')}`
|
|
96
|
+
: `Para desligar o CI: ${chalk.cyan('spec-wave mode local')}`
|
|
97
|
+
);
|
|
98
|
+
return { mode: atual, variable: variavel, changed: false };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const esperado = variableValueFor(alvo);
|
|
102
|
+
const mudaConfig = atual !== alvo;
|
|
103
|
+
const mudaVariavel = variavel !== undefined && variavel !== esperado;
|
|
104
|
+
|
|
105
|
+
if (!mudaConfig && !mudaVariavel) {
|
|
106
|
+
p.log.success(`Já está em ${chalk.bold(alvo)} — config e variável do repositório coincidem.`);
|
|
107
|
+
p.outro('Nada a fazer.');
|
|
108
|
+
return { mode: alvo, variable: variavel, changed: false };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (dryRun) {
|
|
112
|
+
if (mudaConfig) p.log.info(`${CONFIG_FILE}: execution.mode ${atual} → ${alvo}`);
|
|
113
|
+
if (mudaVariavel) {
|
|
114
|
+
p.log.info(esperado === null
|
|
115
|
+
? `Variável ${EXECUTION_VARIABLE}: remover (valor atual: ${variavel})`
|
|
116
|
+
: `Variável ${EXECUTION_VARIABLE}: definir como "${esperado}"`);
|
|
117
|
+
}
|
|
118
|
+
p.outro('Dry-run: nada foi alterado.');
|
|
119
|
+
return { mode: atual, variable: variavel, changed: false };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (mudaConfig) {
|
|
123
|
+
const { changed } = updateConfig(cfg => {
|
|
124
|
+
cfg.execution = { ...(cfg.execution || {}), mode: alvo };
|
|
125
|
+
}, { cwd: root || process.cwd() });
|
|
126
|
+
if (changed) p.log.success(`${CONFIG_FILE} atualizado: execution.mode = ${alvo}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let variavelOk = true;
|
|
130
|
+
if (token && owner && repo) {
|
|
131
|
+
try {
|
|
132
|
+
if (esperado === null) {
|
|
133
|
+
const removida = await deleteRepoVariable(token, owner, repo, EXECUTION_VARIABLE);
|
|
134
|
+
p.log.success(removida
|
|
135
|
+
? `Variável ${EXECUTION_VARIABLE} removida — os workflows voltam a disparar.`
|
|
136
|
+
: `Variável ${EXECUTION_VARIABLE} já não existia.`);
|
|
137
|
+
} else {
|
|
138
|
+
await setRepoVariable(token, owner, repo, EXECUTION_VARIABLE, esperado);
|
|
139
|
+
p.log.success(`Variável ${EXECUTION_VARIABLE}=${esperado} — os jobs passam a ser pulados (0 minutos).`);
|
|
140
|
+
}
|
|
141
|
+
} catch (err) {
|
|
142
|
+
variavelOk = false;
|
|
143
|
+
p.log.error(
|
|
144
|
+
`Não foi possível escrever a variável ${EXECUTION_VARIABLE} (${err.status || ''} ${err.message}).\n` +
|
|
145
|
+
'Ela exige administração no repositório — peça a um admin, ou defina em ' +
|
|
146
|
+
'Settings → Secrets and variables → Actions → Variables.'
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
variavelOk = false;
|
|
151
|
+
p.log.warn(`Sem owner/repo ou token: só o ${CONFIG_FILE} foi atualizado.`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const semGuarda = unguardedWorkflows(root);
|
|
155
|
+
if (alvo === 'local' && semGuarda.length > 0) {
|
|
156
|
+
p.log.warn(
|
|
157
|
+
`Estes workflows instalados ainda não têm a guarda \`${EXECUTION_GUARD}\` e vão rodar mesmo assim: ` +
|
|
158
|
+
`${semGuarda.join(', ')}. Rode \`npx @spec-wave/cli@latest update\`.`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
p.log.message(chalk.dim(`Commite o ${configPath ? path.basename(configPath) : CONFIG_FILE} — quem clona o repo lê a versão versionada.`));
|
|
163
|
+
p.outro(
|
|
164
|
+
alvo === 'local'
|
|
165
|
+
? `Modo local. Conduza o fluxo com ${chalk.cyan('spec-wave run <issue>')}.`
|
|
166
|
+
: 'Modo actions. As labels de gatilho voltam a disparar os workflows.'
|
|
167
|
+
);
|
|
168
|
+
return { mode: alvo, variable: esperado, changed: true, variableApplied: variavelOk };
|
|
169
|
+
}
|