@spec-wave/cli 0.13.0 → 0.15.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 +39 -6
- package/bin/spec-wave.mjs +17 -1
- package/package.json +1 -1
- package/src/api/github-rest.mjs +198 -2
- package/src/commands/code-review.mjs +5 -8
- package/src/commands/decompose.mjs +412 -130
- package/src/commands/dev-agent.mjs +3 -2
- package/src/commands/doctor.mjs +239 -9
- package/src/commands/generate-plan.mjs +105 -30
- package/src/commands/generate-spec.mjs +17 -5
- package/src/commands/implement.mjs +39 -15
- package/src/commands/info.mjs +4 -3
- package/src/commands/issue.mjs +4 -4
- package/src/commands/move.mjs +162 -0
- package/src/commands/order.mjs +1 -12
- package/src/commands/qa.mjs +5 -8
- package/src/commands/refresh.mjs +4 -3
- package/src/commands/story.mjs +1 -12
- package/src/commands/task.mjs +1 -11
- package/src/commands/update.mjs +372 -71
- package/src/commands/validate.mjs +37 -22
- package/src/config.mjs +40 -1
- package/src/lib/board.mjs +88 -26
- package/src/lib/claude.mjs +315 -70
- package/src/lib/critique.mjs +391 -91
- package/src/lib/decomposition-doc.mjs +451 -0
- package/src/lib/implement-board.mjs +14 -1
- package/src/lib/pr-branch.mjs +267 -0
- package/src/lib/project-root.mjs +93 -0
- package/src/lib/templates.mjs +53 -0
- package/src/setup/files.mjs +3 -10
- package/src/templates/skill/SKILL.md +158 -30
- package/src/templates/workflows/code-review.yml +1 -1
- package/src/templates/workflows/decompose.yml +20 -6
- package/src/templates/workflows/generate-plan.yml +1 -1
- package/src/templates/workflows/generate-spec.yml +1 -1
- package/src/templates/workflows/qa.yml +1 -1
- package/src/templates/workflows/validate.yml +1 -1
|
@@ -16,6 +16,7 @@ import { parseDependencies, orderStories, formatDependencyLine } from '../lib/de
|
|
|
16
16
|
import { loadProjectConfig, resolveField } from '../lib/board.mjs';
|
|
17
17
|
import { planBoardMoves, applyBoardMoves } from '../lib/implement-board.mjs';
|
|
18
18
|
import { extractPathsFromPlan, buildCodeDigest } from '../lib/code-digest.mjs';
|
|
19
|
+
import { findConfigPath, resolveFromRoot } from '../lib/project-root.mjs';
|
|
19
20
|
|
|
20
21
|
// Diretório onde montamos o arquivo de contexto entregue ao spec-kit.
|
|
21
22
|
const WORK_DIR = '.spec-wave';
|
|
@@ -359,7 +360,7 @@ function renderCommand(template, vars) {
|
|
|
359
360
|
// Modo Feature: avalia as Stories da Feature (dependências + Etapa no board),
|
|
360
361
|
// pula as já implementadas (Code Review+) e monta UM contexto único com todas
|
|
361
362
|
// as pendentes em ordem topológica — spec-kit acionado uma vez.
|
|
362
|
-
async function implementFeature({ token, owner, repo, config, feature, featureDirOpt, dryRun }) {
|
|
363
|
+
async function implementFeature({ token, owner, repo, config, feature, featureDirOpt, dryRun, repoRoot }) {
|
|
363
364
|
// F1. Stories (sub-issues) da Feature.
|
|
364
365
|
const subs = await listSubIssues(token, feature.node_id).catch(() => []);
|
|
365
366
|
const stories = subs.filter(s => detectIssueType({ title: s.title, labels: s.labels }) === 'Story');
|
|
@@ -374,7 +375,7 @@ async function implementFeature({ token, owner, repo, config, feature, featureDi
|
|
|
374
375
|
p.log.info(`Feature com ${stories.length} story(ies): ${stories.map(s => `#${s.number}`).join(', ')}`);
|
|
375
376
|
|
|
376
377
|
// F1-board. Feature → Desenvolvimento (In Progress) já no início.
|
|
377
|
-
await applyBoardMoves({ token, moves: planBoardMoves('start', {
|
|
378
|
+
await applyBoardMoves({ token, dryRun, moves: planBoardMoves('start', {
|
|
378
379
|
feature: { nodeId: feature.node_id, number: feature.number },
|
|
379
380
|
}) });
|
|
380
381
|
|
|
@@ -391,7 +392,15 @@ async function implementFeature({ token, owner, repo, config, feature, featureDi
|
|
|
391
392
|
// F3. Etapa de cada Story no board (best-effort — sem board, nada é pulado).
|
|
392
393
|
const { project, error: projectError } = loadProjectConfig();
|
|
393
394
|
const stageOf = new Map();
|
|
394
|
-
if (
|
|
395
|
+
if (dryRun) {
|
|
396
|
+
// A leitura da Etapa passa por addProjectItem, que é MUTAÇÃO (adiciona a
|
|
397
|
+
// Story ao Project se ainda não estiver lá). Num dry-run isso é proibido, então
|
|
398
|
+
// a consulta é pulada inteira — o planejamento segue com stage null.
|
|
399
|
+
p.log.warn(
|
|
400
|
+
'Dry-run: Etapas do board não consultadas (a consulta adicionaria itens ao Project); ' +
|
|
401
|
+
'nenhuma Story será considerada implementada.'
|
|
402
|
+
);
|
|
403
|
+
} else if (projectError) {
|
|
395
404
|
p.log.warn(`${projectError} — Etapas do board não consultadas; nenhuma Story será considerada implementada.`);
|
|
396
405
|
} else {
|
|
397
406
|
const etapaField = await resolveField(token, project, 'Etapa').catch(() => null);
|
|
@@ -451,7 +460,8 @@ async function implementFeature({ token, owner, repo, config, feature, featureDi
|
|
|
451
460
|
}
|
|
452
461
|
|
|
453
462
|
// F6. spec.md/plan.md — a issue-alvo JÁ é a Feature (sem resolveFeature).
|
|
454
|
-
const featureDir = featureDirOpt
|
|
463
|
+
const featureDir = featureDirOpt
|
|
464
|
+
|| resolveFromRoot(repoRoot, 'docs', 'features', slugify(feature.title));
|
|
455
465
|
let specPlan = { spec: null, plan: null, specPath: null, planPath: null };
|
|
456
466
|
if (existsSync(featureDir)) {
|
|
457
467
|
specPlan = readSpecPlan(featureDir);
|
|
@@ -556,12 +566,15 @@ export async function implement({ issue: issueArg, featureDir: featureDirOpt, dr
|
|
|
556
566
|
}
|
|
557
567
|
|
|
558
568
|
// 1. Config local (.spec-wave.json) — owner/repo e bloco opcional specKit.
|
|
559
|
-
|
|
560
|
-
|
|
569
|
+
// Procurado subindo na árvore, como o git faz com o .git: rodar de dentro de
|
|
570
|
+
// apps/web precisa achar o config da raiz. `repoRoot` ancora docs/features.
|
|
571
|
+
const configPath = findConfigPath();
|
|
572
|
+
if (!configPath) {
|
|
561
573
|
p.log.error(`Repositório não inicializado (sem ${CONFIG_FILE}). Rode \`spec-wave init\` primeiro.`);
|
|
562
574
|
process.exitCode = 1;
|
|
563
575
|
return;
|
|
564
576
|
}
|
|
577
|
+
const repoRoot = path.dirname(configPath);
|
|
565
578
|
let config;
|
|
566
579
|
try {
|
|
567
580
|
config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
@@ -615,7 +628,7 @@ export async function implement({ issue: issueArg, featureDir: featureDirOpt, dr
|
|
|
615
628
|
p.log.info(`Task única #${issueNumber}.`);
|
|
616
629
|
} else if (type === 'Feature') {
|
|
617
630
|
// Modo Feature: Stories pendentes em ordem de dependência, contexto único.
|
|
618
|
-
await implementFeature({ token, owner, repo, config, feature: issue, featureDirOpt, dryRun });
|
|
631
|
+
await implementFeature({ token, owner, repo, config, feature: issue, featureDirOpt, dryRun, repoRoot });
|
|
619
632
|
return;
|
|
620
633
|
} else {
|
|
621
634
|
p.log.error(
|
|
@@ -630,7 +643,9 @@ export async function implement({ issue: issueArg, featureDir: featureDirOpt, dr
|
|
|
630
643
|
const feature = await resolveFeature(token, issue.node_id);
|
|
631
644
|
let featureDir = featureDirOpt;
|
|
632
645
|
if (!featureDir && feature?.title) {
|
|
633
|
-
|
|
646
|
+
// Ancorado na RAIZ do repo: o config é encontrado subindo na árvore, e os
|
|
647
|
+
// documentos precisam ser procurados no mesmo lugar.
|
|
648
|
+
featureDir = resolveFromRoot(repoRoot, 'docs', 'features', slugify(feature.title));
|
|
634
649
|
}
|
|
635
650
|
let specPlan = { spec: null, plan: null, specPath: null, planPath: null };
|
|
636
651
|
if (featureDir && existsSync(featureDir)) {
|
|
@@ -644,7 +659,7 @@ export async function implement({ issue: issueArg, featureDir: featureDirOpt, dr
|
|
|
644
659
|
// 4a-board. Board determinístico: Feature/Story → Desenvolvimento (In
|
|
645
660
|
// Progress) já no início — a UI mostra o desenvolvimento em andamento sem
|
|
646
661
|
// depender de o LLM mover cards. Best-effort: falha vira warn.
|
|
647
|
-
await applyBoardMoves({ token, moves: planBoardMoves('start', {
|
|
662
|
+
await applyBoardMoves({ token, dryRun, moves: planBoardMoves('start', {
|
|
648
663
|
feature: feature ? { nodeId: feature.nodeId, number: feature.number } : null,
|
|
649
664
|
story: type === 'Story' ? { nodeId: issue.node_id, number: issue.number } : null,
|
|
650
665
|
tasks: tasks.map(t => ({ nodeId: t.nodeId, number: t.number })),
|
|
@@ -753,6 +768,21 @@ async function writeContextAndRunSpecKit({ config, issueNumber, type, title, spe
|
|
|
753
768
|
title,
|
|
754
769
|
};
|
|
755
770
|
|
|
771
|
+
// O dry-run é decidido ANTES do early return de "spec-kit não configurado":
|
|
772
|
+
// sem isso, com o comando ausente os dois caminhos ficavam indistinguíveis e a
|
|
773
|
+
// saída não dizia que estava em dry-run.
|
|
774
|
+
if (dryRun) {
|
|
775
|
+
if (template) {
|
|
776
|
+
p.note(renderCommand(template, vars), 'Comando que seria executado (--dry-run)');
|
|
777
|
+
} else {
|
|
778
|
+
p.log.warn(`Comando do spec-kit não configurado em ${CONFIG_FILE} — nada a imprimir.`);
|
|
779
|
+
}
|
|
780
|
+
p.outro(
|
|
781
|
+
`Dry-run: nada executado e nada escrito no GitHub. Contexto montado em ${tasksFile}.`
|
|
782
|
+
);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
|
|
756
786
|
if (!template) {
|
|
757
787
|
p.log.warn('Comando do spec-kit não configurado.');
|
|
758
788
|
p.note(
|
|
@@ -768,12 +798,6 @@ async function writeContextAndRunSpecKit({ config, issueNumber, type, title, spe
|
|
|
768
798
|
|
|
769
799
|
const command = renderCommand(template, vars);
|
|
770
800
|
|
|
771
|
-
if (dryRun) {
|
|
772
|
-
p.note(command, 'Comando que seria executado (--dry-run)');
|
|
773
|
-
p.outro(`Dry-run: nada executado. Contexto em ${tasksFile}.`);
|
|
774
|
-
return;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
801
|
p.log.step(`Executando: ${chalk.dim(command)}`);
|
|
778
802
|
try {
|
|
779
803
|
execSync(command, { stdio: 'inherit' });
|
package/src/commands/info.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import { readFileSync
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { CONFIG_FILE, PORTAL_URL } from '../config.mjs';
|
|
6
|
+
import { findConfigPath } from '../lib/project-root.mjs';
|
|
6
7
|
import { skillStatus } from './install-skill.mjs';
|
|
7
8
|
|
|
8
9
|
// Resume o estado da skill para a saída JSON. `null` = não foi possível checar.
|
|
@@ -43,10 +44,10 @@ function reportSkill(status) {
|
|
|
43
44
|
// as informações ou oferecer rodar o `init`. Também valida se a skill instalada
|
|
44
45
|
// nos agentes detectados está em dia com a versão empacotada na CLI.
|
|
45
46
|
export async function info(options = {}) {
|
|
46
|
-
const configPath =
|
|
47
|
+
const configPath = findConfigPath();
|
|
47
48
|
const skill = skillStatus();
|
|
48
49
|
|
|
49
|
-
if (!
|
|
50
|
+
if (!configPath) {
|
|
50
51
|
if (options.json) {
|
|
51
52
|
console.log(JSON.stringify({ initialized: false, skill: skillJson(skill) }));
|
|
52
53
|
return;
|
package/src/commands/issue.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import { readFileSync
|
|
4
|
-
import path from 'node:path';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
5
4
|
import { resolveToken } from '../api/auth.mjs';
|
|
6
5
|
import { CONFIG_FILE, STATUS_OPTIONS, PRIORITY_LABELS, WORK_ITEM_TYPES } from '../config.mjs';
|
|
6
|
+
import { findConfigPath } from '../lib/project-root.mjs';
|
|
7
7
|
import { createIssue, getIssue } from '../api/github-rest.mjs';
|
|
8
8
|
import { addProjectItem, setItemSingleSelect, getSingleSelectField, addSubIssue } from '../api/github-graphql.mjs';
|
|
9
9
|
|
|
@@ -70,8 +70,8 @@ export async function issue(options) {
|
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
const configPath =
|
|
74
|
-
if (!
|
|
73
|
+
const configPath = findConfigPath();
|
|
74
|
+
if (!configPath) {
|
|
75
75
|
p.log.error(`Repositório não inicializado (sem ${CONFIG_FILE}). Rode \`spec-wave init\` primeiro.`);
|
|
76
76
|
process.exitCode = 1;
|
|
77
77
|
return;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Move QUALQUER item do board para uma Etapa (Feature, Story, Task, Bug, RFC…).
|
|
2
|
+
//
|
|
3
|
+
// Existia um buraco no fluxo: `story` só aceita `review` e `task` só
|
|
4
|
+
// `start`/`done`, então mover uma Feature pela CLI era impossível — o único
|
|
5
|
+
// caminho era mutação GraphQL manual, justamente o que a skill instrui a evitar
|
|
6
|
+
// ("prefira sempre os comandos da CLI a mutações manuais no board").
|
|
7
|
+
//
|
|
8
|
+
// O comando se chama `move` e não `feature <ação>` porque `feature` já está
|
|
9
|
+
// registrado no bin CRIANDO issues; reaproveitar o nome colidiria.
|
|
10
|
+
//
|
|
11
|
+
// As guardas do board são as mesmas dos outros comandos — reusa advanceToStage,
|
|
12
|
+
// logo a Etapa NUNCA retrocede e não há escape hatch.
|
|
13
|
+
import * as p from '@clack/prompts';
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import { resolveToken } from '../api/auth.mjs';
|
|
16
|
+
import { getIssue } from '../api/github-rest.mjs';
|
|
17
|
+
import { addProjectItem, getItemSingleSelectValue } from '../api/github-graphql.mjs';
|
|
18
|
+
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
19
|
+
import { loadProjectConfig, resolveField, advanceToStage, resolveStageName } from '../lib/board.mjs';
|
|
20
|
+
import { loadConfig } from '../lib/project-root.mjs';
|
|
21
|
+
import {
|
|
22
|
+
CONFIG_FILE, PROGRESS_TODO, PROGRESS_IN_PROGRESS, PROGRESS_DONE,
|
|
23
|
+
isManualStageType, MANUAL_STAGE_TYPES,
|
|
24
|
+
} from '../config.mjs';
|
|
25
|
+
|
|
26
|
+
const PROGRESS_VALUES = [PROGRESS_TODO, PROGRESS_IN_PROGRESS, PROGRESS_DONE];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve o valor do campo Status a partir da entrada do usuário (função PURA).
|
|
30
|
+
* Sem `--status`, o destino é "Todo": ao avançar de Etapa o progresso reinicia
|
|
31
|
+
* (ver a distinção Etapa × Status em config.mjs).
|
|
32
|
+
*
|
|
33
|
+
* @param {string} [input]
|
|
34
|
+
* @returns {{ status: string|null, error: string|null }}
|
|
35
|
+
*/
|
|
36
|
+
export function resolveProgressName(input) {
|
|
37
|
+
if (input === undefined || input === null || String(input).trim() === '') {
|
|
38
|
+
return { status: PROGRESS_TODO, error: null };
|
|
39
|
+
}
|
|
40
|
+
const key = String(input).trim().toLowerCase();
|
|
41
|
+
const match = PROGRESS_VALUES.find(v => v.toLowerCase() === key)
|
|
42
|
+
|| PROGRESS_VALUES.find(v => v.toLowerCase().replace(/\s+/g, '') === key.replace(/[\s_-]+/g, ''));
|
|
43
|
+
return match
|
|
44
|
+
? { status: match, error: null }
|
|
45
|
+
: {
|
|
46
|
+
status: null,
|
|
47
|
+
error:
|
|
48
|
+
`Status "${input}" não existe. Use um destes: ${PROGRESS_VALUES.map(v => `"${v}"`).join(', ')}.`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function move({ issue: issueArg, stage: stageArg, status: statusArg }) {
|
|
53
|
+
const issueNumber = parseInt(String(issueArg).replace('#', ''), 10);
|
|
54
|
+
if (!Number.isInteger(issueNumber) || issueNumber <= 0) {
|
|
55
|
+
p.log.error(`Issue inválida: "${issueArg}". Use o número da issue, ex.: 12 ou #12.`);
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { stage, error: stageError } = resolveStageName(stageArg);
|
|
61
|
+
if (stageError) {
|
|
62
|
+
p.log.error(stageError);
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const { status, error: statusError } = resolveProgressName(statusArg);
|
|
67
|
+
if (statusError) {
|
|
68
|
+
p.log.error(statusError);
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// owner/repo: env GITHUB_REPOSITORY (padrão dos comandos de Action) com
|
|
74
|
+
// fallback no .spec-wave.json, procurado subindo na árvore.
|
|
75
|
+
const [envOwner, envRepo] = (process.env.GITHUB_REPOSITORY || '').split('/');
|
|
76
|
+
const { config, root } = loadConfig();
|
|
77
|
+
const owner = envOwner || config?.owner;
|
|
78
|
+
const repo = envRepo || config?.repo;
|
|
79
|
+
if (!owner || !repo) {
|
|
80
|
+
p.log.error(
|
|
81
|
+
'Não foi possível determinar owner/repo.\n' +
|
|
82
|
+
`Rode dentro de um repositório com ${CONFIG_FILE} (\`spec-wave init\`) ou defina GITHUB_REPOSITORY=owner/repo.`
|
|
83
|
+
);
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let token;
|
|
89
|
+
try {
|
|
90
|
+
token = await resolveToken();
|
|
91
|
+
} catch (err) {
|
|
92
|
+
p.log.error(err.message);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
p.intro(chalk.bold(`spec-wave move #${issueNumber} → ${stage}`));
|
|
98
|
+
|
|
99
|
+
let issue;
|
|
100
|
+
try {
|
|
101
|
+
issue = await getIssue(token, owner, repo, issueNumber);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
p.log.error(`Não foi possível ler a issue #${issueNumber}: ${err.message}`);
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Spike tem avanço decidido pelo usuário no board, não por comando.
|
|
109
|
+
const type = detectIssueType(issue);
|
|
110
|
+
if (isManualStageType(type)) {
|
|
111
|
+
p.log.error(
|
|
112
|
+
`Issue #${issueNumber} é do tipo ${type}, cuja Etapa é movida à mão no board ` +
|
|
113
|
+
`(tipos manuais: ${MANUAL_STAGE_TYPES.join(', ')}).`
|
|
114
|
+
);
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const { project, error: projectError } = loadProjectConfig({ cwd: root || process.cwd() });
|
|
120
|
+
if (projectError) {
|
|
121
|
+
p.log.error(`${projectError} — board não atualizado. Rode \`spec-wave init\` (ou \`spec-wave refresh --config\`).`);
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const etapaField = await resolveField(token, project, 'Etapa').catch(() => null);
|
|
126
|
+
const statusField = await resolveField(token, project, 'Status').catch(() => null);
|
|
127
|
+
|
|
128
|
+
let moved;
|
|
129
|
+
try {
|
|
130
|
+
moved = await advanceToStage(
|
|
131
|
+
token, project, etapaField, statusField, issue.node_id, stage, status);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
p.log.error(`Falha ao atualizar o board: ${err.message}`);
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (moved) {
|
|
139
|
+
p.log.success(
|
|
140
|
+
`${type || 'Issue'} #${issueNumber} → Etapa ${chalk.bold(stage)} / Status ${chalk.bold(status)}.`
|
|
141
|
+
);
|
|
142
|
+
p.outro(`${chalk.green('✓')} ${issue.title}`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// false = já está nessa Etapa ou adiante (ou numa coluna fora da ordem
|
|
147
|
+
// canônica). A Etapa NUNCA retrocede — lê a atual só para informar.
|
|
148
|
+
let current = null;
|
|
149
|
+
if (etapaField?.id) {
|
|
150
|
+
try {
|
|
151
|
+
const itemId = await addProjectItem(token, project.id, issue.node_id);
|
|
152
|
+
current = await getItemSingleSelectValue(token, itemId, etapaField.id);
|
|
153
|
+
} catch {
|
|
154
|
+
// sem leitura da Etapa — segue com o aviso genérico
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
p.log.info(
|
|
158
|
+
`#${issueNumber} não foi movida: a Etapa nunca retrocede, e ela já está em ` +
|
|
159
|
+
`${chalk.bold(current || `"${stage}" ou etapa posterior`)}.`
|
|
160
|
+
);
|
|
161
|
+
p.outro('Nada a fazer.');
|
|
162
|
+
}
|
package/src/commands/order.mjs
CHANGED
|
@@ -7,26 +7,15 @@
|
|
|
7
7
|
// GITHUB_REPOSITORY quando existir, senão do .spec-wave.json (gravado pelo init).
|
|
8
8
|
import * as p from '@clack/prompts';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
-
import path from 'node:path';
|
|
12
10
|
import { resolveToken } from '../api/auth.mjs';
|
|
13
11
|
import { getIssue, listBlockedBy } from '../api/github-rest.mjs';
|
|
14
12
|
import { addProjectItem, listSubIssues, getItemSingleSelectValue } from '../api/github-graphql.mjs';
|
|
15
13
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
16
14
|
import { parseDependencies, orderStories } from '../lib/dependencies.mjs';
|
|
17
15
|
import { loadProjectConfig, resolveField } from '../lib/board.mjs';
|
|
16
|
+
import { resolveRepoContext } from '../lib/project-root.mjs';
|
|
18
17
|
import { CONFIG_FILE, STAGE_ORDER, STAGE_DEVELOPMENT, STAGE_DONE } from '../config.mjs';
|
|
19
18
|
|
|
20
|
-
// Resolve owner/repo: env GITHUB_REPOSITORY (padrão dos comandos de Action) com
|
|
21
|
-
// fallback no .spec-wave.json — comandos locais rodam sem essa env.
|
|
22
|
-
function resolveRepoContext() {
|
|
23
|
-
const [envOwner, envRepo] = (process.env.GITHUB_REPOSITORY || '').split('/');
|
|
24
|
-
let cfg = {};
|
|
25
|
-
const cfgPath = path.join(process.cwd(), CONFIG_FILE);
|
|
26
|
-
try { if (existsSync(cfgPath)) cfg = JSON.parse(readFileSync(cfgPath, 'utf-8')); } catch {}
|
|
27
|
-
return { owner: envOwner || cfg.owner, repo: envRepo || cfg.repo };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
19
|
export async function order({ feature: featureArg }) {
|
|
31
20
|
const featureNumber = parseInt(String(featureArg).replace('#', ''), 10);
|
|
32
21
|
if (!Number.isInteger(featureNumber) || featureNumber <= 0) {
|
package/src/commands/qa.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
1
|
import { resolveToken } from '../api/auth.mjs';
|
|
4
2
|
import { getIssue, getPR, commentOnIssue } from '../api/github-rest.mjs';
|
|
5
3
|
import { getIssueParent } from '../api/github-graphql.mjs';
|
|
6
4
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
7
5
|
import { loadProjectConfig, resolveField, advanceToStage } from '../lib/board.mjs';
|
|
8
|
-
import {
|
|
6
|
+
import { loadConfig } from '../lib/project-root.mjs';
|
|
7
|
+
import { STATUS_OPTIONS, PROGRESS_TODO } from '../config.mjs';
|
|
9
8
|
|
|
10
9
|
const QA_STAGE = STATUS_OPTIONS.find(s => s.name.includes('QA'))?.name;
|
|
11
10
|
const TODO_STATUS = PROGRESS_TODO;
|
|
@@ -47,11 +46,9 @@ export async function qa({ prNumber }) {
|
|
|
47
46
|
const token = await resolveToken();
|
|
48
47
|
const projectToken = process.env.PROJECT_TOKEN || token;
|
|
49
48
|
const [envOwner, envRepo] = (process.env.GITHUB_REPOSITORY || '').split('/');
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const owner = envOwner || cfg.owner;
|
|
54
|
-
const repo = envRepo || cfg.repo;
|
|
49
|
+
const { config: cfg } = loadConfig();
|
|
50
|
+
const owner = envOwner || cfg?.owner;
|
|
51
|
+
const repo = envRepo || cfg?.repo;
|
|
55
52
|
|
|
56
53
|
if (!owner || !repo) {
|
|
57
54
|
throw new Error(
|
package/src/commands/refresh.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import { readFileSync, writeFileSync
|
|
3
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { resolveToken } from '../api/auth.mjs';
|
|
7
7
|
import { CONFIG_FILE } from '../config.mjs';
|
|
8
|
+
import { findConfigPath } from '../lib/project-root.mjs';
|
|
8
9
|
import { getProjectSnapshot } from '../api/github-graphql.mjs';
|
|
9
10
|
|
|
10
11
|
const __dir = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -21,8 +22,8 @@ export async function refresh(options = {}) {
|
|
|
21
22
|
return;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
const configPath =
|
|
25
|
-
if (!
|
|
25
|
+
const configPath = findConfigPath();
|
|
26
|
+
if (!configPath) {
|
|
26
27
|
p.log.error(`Repositório não inicializado (sem ${CONFIG_FILE}). Rode \`spec-wave init\` primeiro.`);
|
|
27
28
|
process.exitCode = 1;
|
|
28
29
|
return;
|
package/src/commands/story.mjs
CHANGED
|
@@ -7,25 +7,14 @@
|
|
|
7
7
|
// estiver em Code Review ou adiante, o comando apenas informa a Etapa atual.
|
|
8
8
|
import * as p from '@clack/prompts';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
-
import path from 'node:path';
|
|
12
10
|
import { resolveToken } from '../api/auth.mjs';
|
|
13
11
|
import { getIssue } from '../api/github-rest.mjs';
|
|
14
12
|
import { addProjectItem, getItemSingleSelectValue } from '../api/github-graphql.mjs';
|
|
15
13
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
16
14
|
import { loadProjectConfig, resolveField, advanceToStage } from '../lib/board.mjs';
|
|
15
|
+
import { resolveRepoContext } from '../lib/project-root.mjs';
|
|
17
16
|
import { CONFIG_FILE, STAGE_CODE_REVIEW, PROGRESS_TODO } from '../config.mjs';
|
|
18
17
|
|
|
19
|
-
// Resolve owner/repo: env GITHUB_REPOSITORY (padrão dos comandos de Action) com
|
|
20
|
-
// fallback no .spec-wave.json — comandos locais rodam sem essa env.
|
|
21
|
-
function resolveRepoContext() {
|
|
22
|
-
const [envOwner, envRepo] = (process.env.GITHUB_REPOSITORY || '').split('/');
|
|
23
|
-
let cfg = {};
|
|
24
|
-
const cfgPath = path.join(process.cwd(), CONFIG_FILE);
|
|
25
|
-
try { if (existsSync(cfgPath)) cfg = JSON.parse(readFileSync(cfgPath, 'utf-8')); } catch {}
|
|
26
|
-
return { owner: envOwner || cfg.owner, repo: envRepo || cfg.repo };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
18
|
export async function story({ action, issue: issueArg }) {
|
|
30
19
|
if (action !== 'review') {
|
|
31
20
|
p.log.error(
|
package/src/commands/task.mjs
CHANGED
|
@@ -10,13 +10,12 @@
|
|
|
10
10
|
// (regra pura canStartTask, testada em test/board-rules.test.mjs).
|
|
11
11
|
import * as p from '@clack/prompts';
|
|
12
12
|
import chalk from 'chalk';
|
|
13
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
-
import path from 'node:path';
|
|
15
13
|
import { resolveToken } from '../api/auth.mjs';
|
|
16
14
|
import { getIssue } from '../api/github-rest.mjs';
|
|
17
15
|
import { addProjectItem, getIssueParent, listSubIssues, getItemSingleSelectValue } from '../api/github-graphql.mjs';
|
|
18
16
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
19
17
|
import { loadProjectConfig, resolveField, advanceToStage, setItemStatus } from '../lib/board.mjs';
|
|
18
|
+
import { resolveRepoContext } from '../lib/project-root.mjs';
|
|
20
19
|
import {
|
|
21
20
|
CONFIG_FILE, STAGE_DEVELOPMENT, STAGE_DONE,
|
|
22
21
|
PROGRESS_IN_PROGRESS, PROGRESS_DONE,
|
|
@@ -36,15 +35,6 @@ export function canStartTask({ siblings } = {}) {
|
|
|
36
35
|
return busy ? { ok: false, blocker: busy.number } : { ok: true, blocker: null };
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
// Resolve owner/repo: env GITHUB_REPOSITORY (padrão dos comandos de Action) com
|
|
40
|
-
// fallback no .spec-wave.json — comandos locais rodam sem essa env.
|
|
41
|
-
function resolveRepoContext() {
|
|
42
|
-
const [envOwner, envRepo] = (process.env.GITHUB_REPOSITORY || '').split('/');
|
|
43
|
-
let cfg = {};
|
|
44
|
-
const cfgPath = path.join(process.cwd(), CONFIG_FILE);
|
|
45
|
-
try { if (existsSync(cfgPath)) cfg = JSON.parse(readFileSync(cfgPath, 'utf-8')); } catch {}
|
|
46
|
-
return { owner: envOwner || cfg.owner, repo: envRepo || cfg.repo };
|
|
47
|
-
}
|
|
48
38
|
|
|
49
39
|
export async function task({ action, issue: issueArg }) {
|
|
50
40
|
if (action !== 'start' && action !== 'done') {
|