up-cc 0.16.0 → 2.0.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 +87 -577
- package/package.json +5 -3
- package/up/CHANGELOG.md +110 -0
- package/up/agents/up-arquiteto.md +95 -39
- package/up/agents/up-auditor.md +218 -0
- package/up/agents/up-executor.md +94 -31
- package/up/agents/up-mapeador-codigo.md +63 -10
- package/up/agents/up-pesquisador.md +278 -0
- package/up/agents/up-revisor.md +249 -0
- package/up/agents/up-sintetizador.md +156 -179
- package/up/agents/up-tester.md +280 -0
- package/up/agents/up-verificador.md +95 -11
- package/up/bin/install.js +190 -21
- package/up/bin/lib/core.cjs +17 -43
- package/up/bin/lib/github.cjs +495 -0
- package/up/bin/lib/multica.cjs +424 -0
- package/up/bin/up-tools.cjs +167 -46
- package/up/commands/auditar.md +66 -0
- package/up/commands/build.md +54 -43
- package/up/commands/depurar.md +1 -1
- package/up/commands/plan.md +52 -38
- package/up/commands/rapido.md +15 -9
- package/up/commands/testar.md +81 -122
- package/up/commands/up.md +106 -0
- package/up/hooks/up-session-start.js +107 -0
- package/up/references/engineering-principles.md +1 -1
- package/up/references/governance-rules.md +5 -5
- package/up/references/production-requirements.md +1 -1
- package/up/references/severity-levels.md +2 -2
- package/up/references/tdd-evidence-types.md +81 -0
- package/up/skills/up-brainstorm/SKILL.md +39 -0
- package/up/skills/up-tdd/SKILL.md +39 -0
- package/up/skills/up-verificar-antes-de-concluir/SKILL.md +49 -0
- package/up/skills/usando-up/SKILL.md +26 -0
- package/up/templates/audit-plan.md +3 -3
- package/up/templates/audit-report.md +2 -2
- package/up/templates/design-tokens.md +2 -2
- package/up/workflows/auditar.md +255 -0
- package/up/workflows/build.md +600 -386
- package/up/workflows/dcrv.md +183 -99
- package/up/workflows/governance.md +112 -220
- package/up/workflows/plan.md +169 -399
- package/up/workflows/rapido.md +7 -1
- package/up/workflows/up.md +447 -0
- package/up/agents/up-analista-codigo.md +0 -446
- package/up/agents/up-api-tester.md +0 -405
- package/up/agents/up-architecture-supervisor.md +0 -126
- package/up/agents/up-audit-supervisor.md +0 -83
- package/up/agents/up-auditor-modernidade.md +0 -378
- package/up/agents/up-auditor-performance.md +0 -426
- package/up/agents/up-auditor-ux.md +0 -396
- package/up/agents/up-backend-specialist.md +0 -175
- package/up/agents/up-blind-validator.md +0 -259
- package/up/agents/up-chief-architect.md +0 -184
- package/up/agents/up-chief-engineer.md +0 -202
- package/up/agents/up-chief-operations.md +0 -123
- package/up/agents/up-chief-product.md +0 -103
- package/up/agents/up-chief-quality.md +0 -211
- package/up/agents/up-clone-crawler.md +0 -234
- package/up/agents/up-clone-design-extractor.md +0 -227
- package/up/agents/up-clone-feature-mapper.md +0 -225
- package/up/agents/up-clone-prd-writer.md +0 -169
- package/up/agents/up-clone-verifier.md +0 -227
- package/up/agents/up-code-reviewer.md +0 -229
- package/up/agents/up-consolidador-ideias.md +0 -493
- package/up/agents/up-database-specialist.md +0 -169
- package/up/agents/up-delivery-auditor.md +0 -247
- package/up/agents/up-devops-agent.md +0 -203
- package/up/agents/up-execution-supervisor.md +0 -315
- package/up/agents/up-exhaustive-tester.md +0 -348
- package/up/agents/up-frontend-specialist.md +0 -152
- package/up/agents/up-operations-supervisor.md +0 -94
- package/up/agents/up-pesquisador-mercado.md +0 -350
- package/up/agents/up-pesquisador-projeto.md +0 -358
- package/up/agents/up-planning-auditor.md +0 -284
- package/up/agents/up-planning-supervisor.md +0 -260
- package/up/agents/up-product-analyst.md +0 -192
- package/up/agents/up-product-supervisor.md +0 -83
- package/up/agents/up-project-ceo.md +0 -352
- package/up/agents/up-qa-agent.md +0 -171
- package/up/agents/up-quality-supervisor.md +0 -178
- package/up/agents/up-requirements-validator.md +0 -230
- package/up/agents/up-security-reviewer.md +0 -137
- package/up/agents/up-sintetizador-melhorias.md +0 -407
- package/up/agents/up-system-designer.md +0 -332
- package/up/agents/up-technical-writer.md +0 -188
- package/up/agents/up-verification-supervisor.md +0 -111
- package/up/agents/up-visual-critic.md +0 -358
- package/up/commands/adicionar-fase.md +0 -47
- package/up/commands/adicionar-testes.md +0 -145
- package/up/commands/ajuda.md +0 -176
- package/up/commands/atualizar.md +0 -103
- package/up/commands/clone-builder.md +0 -67
- package/up/commands/configurar.md +0 -219
- package/up/commands/custos.md +0 -67
- package/up/commands/dashboard.md +0 -48
- package/up/commands/discutir-fase.md +0 -35
- package/up/commands/executar-fase.md +0 -40
- package/up/commands/ideias.md +0 -49
- package/up/commands/iniciar.md +0 -31
- package/up/commands/mapear-codigo.md +0 -63
- package/up/commands/melhorias.md +0 -45
- package/up/commands/mobile-first.md +0 -71
- package/up/commands/modo-builder.md +0 -186
- package/up/commands/novo-projeto.md +0 -40
- package/up/commands/onboard.md +0 -69
- package/up/commands/pausar.md +0 -33
- package/up/commands/planejar-fase.md +0 -45
- package/up/commands/progresso.md +0 -33
- package/up/commands/remover-fase.md +0 -34
- package/up/commands/resetar.md +0 -27
- package/up/commands/retomar.md +0 -35
- package/up/commands/saude.md +0 -103
- package/up/commands/ux-tester.md +0 -63
- package/up/commands/verificar-trabalho.md +0 -35
- package/up/workflows/adicionar-fase.md +0 -112
- package/up/workflows/builder-e2e.md +0 -501
- package/up/workflows/builder.md +0 -3419
- package/up/workflows/ceo-intake.md +0 -305
- package/up/workflows/ceo-updates.md +0 -183
- package/up/workflows/clone-builder.md +0 -320
- package/up/workflows/discutir-fase.md +0 -336
- package/up/workflows/executar-fase.md +0 -358
- package/up/workflows/executar-plano.md +0 -659
- package/up/workflows/ideias.md +0 -381
- package/up/workflows/iniciar.md +0 -235
- package/up/workflows/melhorias.md +0 -409
- package/up/workflows/mobile-first.md +0 -692
- package/up/workflows/novo-projeto.md +0 -778
- package/up/workflows/planejar-fase.md +0 -293
- package/up/workflows/progresso.md +0 -226
- package/up/workflows/retomar.md +0 -231
- package/up/workflows/ux-tester.md +0 -526
- package/up/workflows/verificar-trabalho.md +0 -308
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* github.cjs — GitHub-native execution layer for UP (worktree -> issue -> PR -> merge)
|
|
3
|
+
*
|
|
4
|
+
* Construido sobre execGit (core.cjs). FAIL-OPEN em tudo: se faltar `gh` ou
|
|
5
|
+
* remote, degrada para git local (branch + merge local, issue/pr=null) com aviso,
|
|
6
|
+
* nunca crasha. git worktree e sempre local (funciona offline).
|
|
7
|
+
*
|
|
8
|
+
* Modelo de estado: .plano/git-map.json (canonico no working dir principal).
|
|
9
|
+
* { github_native, merge_strategy, phases: { N: {branch, worktree, issue, issue_url, pr, pr_url, status} } }
|
|
10
|
+
*
|
|
11
|
+
* Branch: up/fase-NN-slug (NN zero-pad)
|
|
12
|
+
* Worktree: <repoParent>/.up-worktrees/<repoName>/fase-NN-slug (FORA do repo)
|
|
13
|
+
*
|
|
14
|
+
* Exporta: startPhase, finishPhase, status.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { execSync } = require('child_process');
|
|
20
|
+
const { execGit, loadConfig } = require('./core.cjs');
|
|
21
|
+
|
|
22
|
+
// =====================================================================
|
|
23
|
+
// Detecção de ambiente (gh + remote) — FAIL-OPEN
|
|
24
|
+
// =====================================================================
|
|
25
|
+
|
|
26
|
+
/** Existe remote `origin`? Decide local-only vs GitHub. */
|
|
27
|
+
function gitHasRemote(cwd) {
|
|
28
|
+
const res = execGit(cwd, ['remote', 'get-url', 'origin']);
|
|
29
|
+
return res.exitCode === 0 && res.stdout.trim().length > 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getRemoteUrl(cwd) {
|
|
33
|
+
const res = execGit(cwd, ['remote', 'get-url', 'origin']);
|
|
34
|
+
return res.exitCode === 0 ? res.stdout.trim() : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** `gh` instalado E autenticado? */
|
|
38
|
+
function ghAvailable(cwd) {
|
|
39
|
+
try {
|
|
40
|
+
execSync('gh auth status', { cwd, stdio: 'pipe' });
|
|
41
|
+
return true;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Executa `gh <args>` no cwd. Retorna {exitCode, stdout, stderr}.
|
|
49
|
+
* Nunca lança: erro de gh ausente/falha vira exitCode != 0.
|
|
50
|
+
*/
|
|
51
|
+
function execGh(cwd, args) {
|
|
52
|
+
try {
|
|
53
|
+
const escaped = args.map(a => {
|
|
54
|
+
if (/^[a-zA-Z0-9._\-/=:@,]+$/.test(a)) return a;
|
|
55
|
+
return "'" + String(a).replace(/'/g, "'\\''") + "'";
|
|
56
|
+
});
|
|
57
|
+
const stdout = execSync('gh ' + escaped.join(' '), {
|
|
58
|
+
cwd,
|
|
59
|
+
stdio: 'pipe',
|
|
60
|
+
encoding: 'utf-8',
|
|
61
|
+
});
|
|
62
|
+
return { exitCode: 0, stdout: stdout.trim(), stderr: '' };
|
|
63
|
+
} catch (err) {
|
|
64
|
+
return {
|
|
65
|
+
exitCode: err.status ?? 1,
|
|
66
|
+
stdout: (err.stdout ?? '').toString().trim(),
|
|
67
|
+
stderr: (err.stderr ?? '').toString().trim(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** github-native ativo? (config.github_native default true) AND gh+remote disponiveis. */
|
|
73
|
+
function githubMode(cwd, solo) {
|
|
74
|
+
if (solo) return false;
|
|
75
|
+
const config = loadConfig(cwd);
|
|
76
|
+
if (config.github_native === false) return false;
|
|
77
|
+
return ghAvailable(cwd) && gitHasRemote(cwd);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// =====================================================================
|
|
81
|
+
// Helpers de fase / branch / worktree
|
|
82
|
+
// =====================================================================
|
|
83
|
+
|
|
84
|
+
/** Zero-pad o número da fase para 2 dígitos, preservando letra/decimal. */
|
|
85
|
+
function padPhase(phase) {
|
|
86
|
+
const m = String(phase).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
|
87
|
+
if (!m) return String(phase);
|
|
88
|
+
const padded = m[1].padStart(2, '0');
|
|
89
|
+
const letter = m[2] ? m[2].toUpperCase() : '';
|
|
90
|
+
const decimal = m[3] || '';
|
|
91
|
+
return padded + letter + decimal;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function slugify(text) {
|
|
95
|
+
if (!text) return '';
|
|
96
|
+
return String(text).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** up/fase-NN-slug */
|
|
100
|
+
function branchName(phase, slug) {
|
|
101
|
+
const nn = padPhase(phase);
|
|
102
|
+
const s = slugify(slug);
|
|
103
|
+
return s ? `up/fase-${nn}-${s}` : `up/fase-${nn}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Caminho do toplevel do repo principal (resolve worktrees aninhadas). */
|
|
107
|
+
function repoToplevel(cwd) {
|
|
108
|
+
const res = execGit(cwd, ['rev-parse', '--show-toplevel']);
|
|
109
|
+
return res.exitCode === 0 && res.stdout ? res.stdout.trim() : cwd;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Worktree FORA do repo: <repoParent>/.up-worktrees/<repoName>/fase-NN-slug
|
|
114
|
+
* Espelha a estrutura do nome de branch, mas com `/` -> `-` (1 nivel de pasta).
|
|
115
|
+
*/
|
|
116
|
+
function worktreePath(cwd, phase, slug) {
|
|
117
|
+
const top = repoToplevel(cwd);
|
|
118
|
+
const repoName = path.basename(top);
|
|
119
|
+
const repoParent = path.dirname(top);
|
|
120
|
+
const nn = padPhase(phase);
|
|
121
|
+
const s = slugify(slug);
|
|
122
|
+
const dirName = s ? `fase-${nn}-${s}` : `fase-${nn}`;
|
|
123
|
+
return path.join(repoParent, '.up-worktrees', repoName, dirName);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Garante que .up-worktrees nao polua git status do repo principal (exclude local + gitignore best-effort). */
|
|
127
|
+
function shieldWorktreeDir(cwd) {
|
|
128
|
+
const top = repoToplevel(cwd);
|
|
129
|
+
// 1) .git/info/exclude (nao versionado, sempre seguro)
|
|
130
|
+
try {
|
|
131
|
+
const excludePath = path.join(top, '.git', 'info', 'exclude');
|
|
132
|
+
let content = fs.existsSync(excludePath) ? fs.readFileSync(excludePath, 'utf-8') : '';
|
|
133
|
+
if (!content.split('\n').some(l => l.trim() === '.up-worktrees/' || l.trim() === '.up-worktrees')) {
|
|
134
|
+
if (content.length && !content.endsWith('\n')) content += '\n';
|
|
135
|
+
content += '.up-worktrees/\n';
|
|
136
|
+
fs.writeFileSync(excludePath, content, 'utf-8');
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// best-effort, ignora
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// =====================================================================
|
|
144
|
+
// git-map.json — leitura/escrita (canonico no working dir principal)
|
|
145
|
+
// =====================================================================
|
|
146
|
+
|
|
147
|
+
function gitMapPath(cwd) {
|
|
148
|
+
return path.join(repoToplevel(cwd), '.plano', 'git-map.json');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function readGitMap(cwd) {
|
|
152
|
+
const config = loadConfig(cwd);
|
|
153
|
+
const defaults = {
|
|
154
|
+
github_native: config.github_native !== false,
|
|
155
|
+
merge_strategy: config.merge_strategy || 'squash',
|
|
156
|
+
phases: {},
|
|
157
|
+
};
|
|
158
|
+
try {
|
|
159
|
+
const p = gitMapPath(cwd);
|
|
160
|
+
if (!fs.existsSync(p)) return defaults;
|
|
161
|
+
const parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
162
|
+
return {
|
|
163
|
+
github_native: parsed.github_native ?? defaults.github_native,
|
|
164
|
+
merge_strategy: parsed.merge_strategy ?? defaults.merge_strategy,
|
|
165
|
+
phases: parsed.phases ?? {},
|
|
166
|
+
};
|
|
167
|
+
} catch {
|
|
168
|
+
return defaults;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function writeGitMap(cwd, map) {
|
|
173
|
+
const p = gitMapPath(cwd);
|
|
174
|
+
try {
|
|
175
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
176
|
+
fs.writeFileSync(p, JSON.stringify(map, null, 2), 'utf-8');
|
|
177
|
+
return true;
|
|
178
|
+
} catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function phaseKey(phase) {
|
|
184
|
+
return padPhase(phase).replace(/^0+(\d)/, '$1'); // "03" -> "3", "02.1" -> "2.1"
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// =====================================================================
|
|
188
|
+
// startPhase — worktree + branch + (opcional) issue
|
|
189
|
+
// =====================================================================
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* startPhase({cwd, phase, slug, solo})
|
|
193
|
+
* - Cria branch up/fase-NN-slug e worktree FORA do repo.
|
|
194
|
+
* - Se github_native e gh+remote disponiveis: cria gh issue.
|
|
195
|
+
* - Com --solo: degrada (sem worktree, sem issue) — trabalha na branch atual.
|
|
196
|
+
* - Sem gh/remote: cria worktree+branch local, issue=null (fail-open, nunca crasha).
|
|
197
|
+
* - Escreve .plano/git-map.json. Retorna {branch, worktree, issue, issue_url, mode, warnings}.
|
|
198
|
+
*/
|
|
199
|
+
function startPhase({ cwd, phase, slug, solo = false }) {
|
|
200
|
+
const warnings = [];
|
|
201
|
+
const key = phaseKey(phase);
|
|
202
|
+
const branch = branchName(phase, slug);
|
|
203
|
+
const ghOn = githubMode(cwd, solo);
|
|
204
|
+
|
|
205
|
+
const map = readGitMap(cwd);
|
|
206
|
+
if (solo) map.github_native = false;
|
|
207
|
+
|
|
208
|
+
// --- modo SOLO: nada de worktree/issue, trabalha na branch atual ---
|
|
209
|
+
if (solo) {
|
|
210
|
+
const cur = execGit(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
211
|
+
const currentBranch = cur.exitCode === 0 ? cur.stdout.trim() : null;
|
|
212
|
+
map.phases[key] = {
|
|
213
|
+
...(map.phases[key] || {}),
|
|
214
|
+
branch: currentBranch,
|
|
215
|
+
worktree: null,
|
|
216
|
+
issue: null,
|
|
217
|
+
issue_url: null,
|
|
218
|
+
pr: null,
|
|
219
|
+
pr_url: null,
|
|
220
|
+
status: 'in_progress',
|
|
221
|
+
};
|
|
222
|
+
writeGitMap(cwd, map);
|
|
223
|
+
return {
|
|
224
|
+
branch: currentBranch,
|
|
225
|
+
worktree: null,
|
|
226
|
+
issue: null,
|
|
227
|
+
issue_url: null,
|
|
228
|
+
mode: 'solo',
|
|
229
|
+
warnings,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// --- modo GitHub-native ou git-local: criar worktree+branch ---
|
|
234
|
+
const wt = worktreePath(cwd, phase, slug);
|
|
235
|
+
shieldWorktreeDir(cwd);
|
|
236
|
+
|
|
237
|
+
// Worktree e branch existem? (idempotente / recovery)
|
|
238
|
+
const existing = (map.phases[key] && map.phases[key].worktree) || null;
|
|
239
|
+
let worktreeCreated = false;
|
|
240
|
+
|
|
241
|
+
if (!fs.existsSync(wt)) {
|
|
242
|
+
fs.mkdirSync(path.dirname(wt), { recursive: true });
|
|
243
|
+
// Branch ja existe? Anexa sem -b. Senao cria com -b.
|
|
244
|
+
const branchExists = execGit(cwd, ['rev-parse', '--verify', '--quiet', branch]).exitCode === 0;
|
|
245
|
+
const wtArgs = branchExists
|
|
246
|
+
? ['worktree', 'add', wt, branch]
|
|
247
|
+
: ['worktree', 'add', '-b', branch, wt];
|
|
248
|
+
const wtRes = execGit(cwd, wtArgs);
|
|
249
|
+
if (wtRes.exitCode !== 0) {
|
|
250
|
+
warnings.push('worktree_failed: ' + (wtRes.stderr || wtRes.stdout));
|
|
251
|
+
// FAIL-OPEN: cai pra branch local sem worktree
|
|
252
|
+
const co = execGit(cwd, branchExists ? ['checkout', branch] : ['checkout', '-b', branch]);
|
|
253
|
+
if (co.exitCode !== 0) warnings.push('branch_checkout_failed: ' + (co.stderr || co.stdout));
|
|
254
|
+
} else {
|
|
255
|
+
worktreeCreated = true;
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
worktreeCreated = true; // ja existia
|
|
259
|
+
}
|
|
260
|
+
const worktree = worktreeCreated || existing ? wt : null;
|
|
261
|
+
|
|
262
|
+
// --- issue (so em github-native) ---
|
|
263
|
+
let issue = null;
|
|
264
|
+
let issueUrl = null;
|
|
265
|
+
if (ghOn) {
|
|
266
|
+
const title = `[fase ${padPhase(phase)}] ${slug || branch}`;
|
|
267
|
+
const body = buildIssueBody(cwd, phase, slug);
|
|
268
|
+
const res = execGh(cwd, ['issue', 'create', '--title', title, '--body', body]);
|
|
269
|
+
if (res.exitCode === 0) {
|
|
270
|
+
issueUrl = res.stdout.trim().split('\n').pop().trim() || null;
|
|
271
|
+
const m = issueUrl && issueUrl.match(/\/issues\/(\d+)/);
|
|
272
|
+
issue = m ? parseInt(m[1], 10) : null;
|
|
273
|
+
} else {
|
|
274
|
+
warnings.push('issue_create_failed: ' + (res.stderr || res.stdout));
|
|
275
|
+
}
|
|
276
|
+
} else if (!solo) {
|
|
277
|
+
if (!ghAvailable(cwd)) warnings.push('gh indisponivel ou nao autenticado: degradando para git local (sem issue/PR)');
|
|
278
|
+
else if (!gitHasRemote(cwd)) warnings.push('sem remote origin: degradando para git local (sem issue/PR)');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
map.phases[key] = {
|
|
282
|
+
...(map.phases[key] || {}),
|
|
283
|
+
branch,
|
|
284
|
+
worktree,
|
|
285
|
+
issue,
|
|
286
|
+
issue_url: issueUrl,
|
|
287
|
+
pr: (map.phases[key] && map.phases[key].pr) || null,
|
|
288
|
+
pr_url: (map.phases[key] && map.phases[key].pr_url) || null,
|
|
289
|
+
status: 'in_progress',
|
|
290
|
+
};
|
|
291
|
+
writeGitMap(cwd, map);
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
branch,
|
|
295
|
+
worktree,
|
|
296
|
+
issue,
|
|
297
|
+
issue_url: issueUrl,
|
|
298
|
+
mode: ghOn ? 'github' : 'git-local',
|
|
299
|
+
warnings,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Body da issue: goal + criterios do ROADMAP, se disponiveis. */
|
|
304
|
+
function buildIssueBody(cwd, phase, slug) {
|
|
305
|
+
let body = `Fase ${padPhase(phase)}` + (slug ? ` — ${slug}` : '') + '\n\nGerado por UP (github-native).';
|
|
306
|
+
try {
|
|
307
|
+
const { getRoadmapPhaseInternal } = require('./core.cjs');
|
|
308
|
+
const rp = getRoadmapPhaseInternal(repoToplevel(cwd), phase);
|
|
309
|
+
if (rp && rp.goal) body = `**Objetivo:** ${rp.goal}\n\n${body}`;
|
|
310
|
+
} catch {
|
|
311
|
+
// best-effort
|
|
312
|
+
}
|
|
313
|
+
return body;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// =====================================================================
|
|
317
|
+
// finishPhase — solo | menu | auto/pr
|
|
318
|
+
// =====================================================================
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* finishPhase({cwd, phase, mode, strategy})
|
|
322
|
+
* - solo: nao faz nada (ja committado na branch atual). Marca status=done.
|
|
323
|
+
* - menu: imprime as 4 opcoes pro orquestrador perguntar (nao age).
|
|
324
|
+
* - auto (a.k.a. pr): gh pr create (body "Closes #<issue>") -> merge (squash default)
|
|
325
|
+
* -> cleanup worktree+branch. Sem remote: merge LOCAL da branch na base, cleanup.
|
|
326
|
+
* Atualiza git-map.json. Retorna estado da operacao.
|
|
327
|
+
*/
|
|
328
|
+
function finishPhase({ cwd, phase, mode = 'menu', strategy }) {
|
|
329
|
+
const warnings = [];
|
|
330
|
+
const key = phaseKey(phase);
|
|
331
|
+
const map = readGitMap(cwd);
|
|
332
|
+
const entry = map.phases[key] || {};
|
|
333
|
+
const mergeStrategy = strategy || map.merge_strategy || loadConfig(cwd).merge_strategy || 'squash';
|
|
334
|
+
|
|
335
|
+
// --- SOLO: nada a fazer, ja committado ---
|
|
336
|
+
if (mode === 'solo') {
|
|
337
|
+
entry.status = 'done';
|
|
338
|
+
map.phases[key] = entry;
|
|
339
|
+
writeGitMap(cwd, map);
|
|
340
|
+
return { mode: 'solo', action: 'none', status: 'done', warnings };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// --- MENU: imprime as 4 opcoes, nao age ---
|
|
344
|
+
if (mode === 'menu') {
|
|
345
|
+
return {
|
|
346
|
+
mode: 'menu',
|
|
347
|
+
action: 'prompt',
|
|
348
|
+
options: [
|
|
349
|
+
{ id: 1, label: 'merge local', detail: 'Mescla a branch na base localmente (sem PR).' },
|
|
350
|
+
{ id: 2, label: 'abrir PR', detail: 'Cria PR no GitHub (Closes #issue) e para pra revisao.' },
|
|
351
|
+
{ id: 3, label: 'deixa a branch', detail: 'Mantem a branch/worktree como esta.' },
|
|
352
|
+
{ id: 4, label: 'descarta', detail: 'Remove worktree e branch sem mesclar.' },
|
|
353
|
+
],
|
|
354
|
+
branch: entry.branch || null,
|
|
355
|
+
issue: entry.issue || null,
|
|
356
|
+
warnings,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// --- AUTO / PR ---
|
|
361
|
+
if (mode === 'auto' || mode === 'pr') {
|
|
362
|
+
const branch = entry.branch || branchName(phase, '');
|
|
363
|
+
const worktree = entry.worktree || null;
|
|
364
|
+
const ghOn = ghAvailable(cwd) && gitHasRemote(cwd) && map.github_native !== false;
|
|
365
|
+
|
|
366
|
+
// Base de merge (default: branch ativa na main do repo)
|
|
367
|
+
const baseRes = execGit(cwd, ['symbolic-ref', '--short', 'HEAD']);
|
|
368
|
+
const base = baseRes.exitCode === 0 ? baseRes.stdout.trim() : 'main';
|
|
369
|
+
|
|
370
|
+
let pr = entry.pr || null;
|
|
371
|
+
let prUrl = entry.pr_url || null;
|
|
372
|
+
|
|
373
|
+
if (ghOn) {
|
|
374
|
+
// push da branch (necessario pro PR). Worktree ja tem a branch checada.
|
|
375
|
+
const pushCwd = worktree && fs.existsSync(worktree) ? worktree : cwd;
|
|
376
|
+
const push = execGit(pushCwd, ['push', '-u', 'origin', branch]);
|
|
377
|
+
if (push.exitCode !== 0) warnings.push('push_failed: ' + (push.stderr || push.stdout));
|
|
378
|
+
|
|
379
|
+
// gh pr create (Closes #issue no body)
|
|
380
|
+
const issueRef = entry.issue ? `\n\nCloses #${entry.issue}` : '';
|
|
381
|
+
const prTitle = `Fase ${padPhase(phase)}` + (entry.branch ? `: ${entry.branch.replace(/^up\/fase-\d+-?/, '')}` : '');
|
|
382
|
+
const prBody = `Entrega da fase ${padPhase(phase)} (UP github-native).${issueRef}`;
|
|
383
|
+
const create = execGh(cwd, ['pr', 'create', '--base', base, '--head', branch, '--title', prTitle, '--body', prBody]);
|
|
384
|
+
if (create.exitCode === 0) {
|
|
385
|
+
prUrl = create.stdout.trim().split('\n').pop().trim() || null;
|
|
386
|
+
const m = prUrl && prUrl.match(/\/pull\/(\d+)/);
|
|
387
|
+
pr = m ? parseInt(m[1], 10) : null;
|
|
388
|
+
} else {
|
|
389
|
+
warnings.push('pr_create_failed: ' + (create.stderr || create.stdout));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// merge via gh
|
|
393
|
+
const stratFlag = mergeStrategy === 'merge' ? '--merge' : mergeStrategy === 'rebase' ? '--rebase' : '--squash';
|
|
394
|
+
const mergeTarget = pr ? String(pr) : branch;
|
|
395
|
+
const merge = execGh(cwd, ['pr', 'merge', mergeTarget, stratFlag, '--delete-branch']);
|
|
396
|
+
if (merge.exitCode !== 0) {
|
|
397
|
+
warnings.push('pr_merge_failed: ' + (merge.stderr || merge.stdout));
|
|
398
|
+
} else {
|
|
399
|
+
entry.status = 'merged';
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
// FAIL-OPEN: merge LOCAL (sem remote/gh)
|
|
403
|
+
warnings.push('sem gh/remote: merge local da branch ' + branch + ' em ' + base);
|
|
404
|
+
// Garante base checada no repo principal
|
|
405
|
+
const co = execGit(cwd, ['checkout', base]);
|
|
406
|
+
if (co.exitCode !== 0) warnings.push('checkout_base_failed: ' + (co.stderr || co.stdout));
|
|
407
|
+
const stratArgs = mergeStrategy === 'squash'
|
|
408
|
+
? ['merge', '--squash', branch]
|
|
409
|
+
: ['merge', '--no-ff', branch];
|
|
410
|
+
const merge = execGit(cwd, stratArgs);
|
|
411
|
+
if (merge.exitCode !== 0) {
|
|
412
|
+
warnings.push('local_merge_failed: ' + (merge.stderr || merge.stdout));
|
|
413
|
+
} else {
|
|
414
|
+
if (mergeStrategy === 'squash') {
|
|
415
|
+
const c = execGit(cwd, ['commit', '-m', `feat(fase-${padPhase(phase)}): merge da fase`]);
|
|
416
|
+
if (c.exitCode !== 0 && !/nothing to commit/.test(c.stderr + c.stdout)) {
|
|
417
|
+
warnings.push('local_squash_commit_failed: ' + (c.stderr || c.stdout));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
entry.status = 'merged';
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// cleanup: remove worktree e branch local
|
|
425
|
+
if (worktree && fs.existsSync(worktree)) {
|
|
426
|
+
const rm = execGit(cwd, ['worktree', 'remove', worktree]);
|
|
427
|
+
if (rm.exitCode !== 0) {
|
|
428
|
+
const rmF = execGit(cwd, ['worktree', 'remove', '--force', worktree]);
|
|
429
|
+
if (rmF.exitCode !== 0) warnings.push('worktree_remove_failed: ' + (rmF.stderr || rmF.stdout));
|
|
430
|
+
}
|
|
431
|
+
// best-effort: remove a casca vazia que git deixa, e o dir-pai .up-worktrees/<repo> se vazio
|
|
432
|
+
try { if (fs.existsSync(worktree)) fs.rmdirSync(worktree); } catch (e) { /* nao-vazio: ignora */ }
|
|
433
|
+
try {
|
|
434
|
+
const parent = path.dirname(worktree);
|
|
435
|
+
if (fs.existsSync(parent) && fs.readdirSync(parent).length === 0) fs.rmdirSync(parent);
|
|
436
|
+
} catch (e) { /* ignora */ }
|
|
437
|
+
}
|
|
438
|
+
// remove branch local (se ainda existir e nao for a branch ativa)
|
|
439
|
+
const delBranch = execGit(cwd, ['branch', '-D', branch]);
|
|
440
|
+
if (delBranch.exitCode !== 0 && !/not found|não encontrad/i.test(delBranch.stderr)) {
|
|
441
|
+
// silencioso: gh --delete-branch ja pode ter removido
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
entry.pr = pr;
|
|
445
|
+
entry.pr_url = prUrl;
|
|
446
|
+
if (!entry.status || entry.status === 'in_progress') entry.status = ghOn ? 'merged' : entry.status;
|
|
447
|
+
map.phases[key] = entry;
|
|
448
|
+
writeGitMap(cwd, map);
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
mode: mode,
|
|
452
|
+
action: 'merged',
|
|
453
|
+
pr,
|
|
454
|
+
pr_url: prUrl,
|
|
455
|
+
branch,
|
|
456
|
+
strategy: mergeStrategy,
|
|
457
|
+
status: entry.status,
|
|
458
|
+
warnings,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
warnings.push('modo desconhecido: ' + mode + ' (use solo|menu|auto)');
|
|
463
|
+
return { mode, action: 'noop', warnings };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// =====================================================================
|
|
467
|
+
// status — mostra git-map.json
|
|
468
|
+
// =====================================================================
|
|
469
|
+
|
|
470
|
+
/** status({cwd}) — retorna o git-map.json atual (ou defaults se nao existir). Nunca crasha. */
|
|
471
|
+
function status({ cwd }) {
|
|
472
|
+
const map = readGitMap(cwd);
|
|
473
|
+
return {
|
|
474
|
+
github_native: map.github_native,
|
|
475
|
+
merge_strategy: map.merge_strategy,
|
|
476
|
+
gh_available: ghAvailable(cwd),
|
|
477
|
+
has_remote: gitHasRemote(cwd),
|
|
478
|
+
remote: getRemoteUrl(cwd),
|
|
479
|
+
phases: map.phases,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
module.exports = {
|
|
484
|
+
startPhase,
|
|
485
|
+
finishPhase,
|
|
486
|
+
status,
|
|
487
|
+
// helpers expostos para teste/reuso
|
|
488
|
+
branchName,
|
|
489
|
+
worktreePath,
|
|
490
|
+
gitHasRemote,
|
|
491
|
+
ghAvailable,
|
|
492
|
+
githubMode,
|
|
493
|
+
readGitMap,
|
|
494
|
+
writeGitMap,
|
|
495
|
+
};
|