@planu/cli 0.78.0 → 0.79.1
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/dist/cli/commands/serve.d.ts.map +1 -1
- package/dist/cli/commands/serve.js +1 -1
- package/dist/cli/commands/serve.js.map +1 -1
- package/dist/config/contradiction-patterns.json +83 -0
- package/dist/config/estimation-tables.json +49 -0
- package/dist/config/license-plans.json +5 -1
- package/dist/engine/ci-generator/local-script.d.ts +8 -0
- package/dist/engine/ci-generator/local-script.d.ts.map +1 -0
- package/dist/engine/ci-generator/local-script.js +177 -0
- package/dist/engine/ci-generator/local-script.js.map +1 -0
- package/dist/engine/ci-generator/planu-steps.d.ts.map +1 -1
- package/dist/engine/ci-generator/planu-steps.js +37 -22
- package/dist/engine/ci-generator/planu-steps.js.map +1 -1
- package/dist/engine/config-loader.d.ts +73 -0
- package/dist/engine/config-loader.d.ts.map +1 -0
- package/dist/engine/config-loader.js +246 -0
- package/dist/engine/config-loader.js.map +1 -0
- package/dist/engine/config-schemas.d.ts +64 -0
- package/dist/engine/config-schemas.d.ts.map +1 -0
- package/dist/engine/config-schemas.js +55 -0
- package/dist/engine/config-schemas.js.map +1 -0
- package/dist/engine/spec-summary-html.d.ts.map +1 -1
- package/dist/engine/spec-summary-html.js +5 -0
- package/dist/engine/spec-summary-html.js.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/storage/ideas-store.d.ts +15 -0
- package/dist/storage/ideas-store.d.ts.map +1 -0
- package/dist/storage/ideas-store.js +37 -0
- package/dist/storage/ideas-store.js.map +1 -0
- package/dist/tools/capture-idea-handler.d.ts +4 -0
- package/dist/tools/capture-idea-handler.d.ts.map +1 -0
- package/dist/tools/capture-idea-handler.js +39 -0
- package/dist/tools/capture-idea-handler.js.map +1 -0
- package/dist/tools/ci-planu-handler.d.ts +2 -2
- package/dist/tools/ci-planu-handler.d.ts.map +1 -1
- package/dist/tools/ci-planu-handler.js +55 -22
- package/dist/tools/ci-planu-handler.js.map +1 -1
- package/dist/tools/discard-idea-handler.d.ts +4 -0
- package/dist/tools/discard-idea-handler.d.ts.map +1 -0
- package/dist/tools/discard-idea-handler.js +27 -0
- package/dist/tools/discard-idea-handler.js.map +1 -0
- package/dist/tools/git/cleanup-ops.d.ts +15 -0
- package/dist/tools/git/cleanup-ops.d.ts.map +1 -0
- package/dist/tools/git/cleanup-ops.js +417 -0
- package/dist/tools/git/cleanup-ops.js.map +1 -0
- package/dist/tools/license-gate.d.ts +6 -0
- package/dist/tools/license-gate.d.ts.map +1 -1
- package/dist/tools/license-gate.js +14 -0
- package/dist/tools/license-gate.js.map +1 -1
- package/dist/tools/list-backlog-handler.d.ts +4 -0
- package/dist/tools/list-backlog-handler.d.ts.map +1 -0
- package/dist/tools/list-backlog-handler.js +60 -0
- package/dist/tools/list-backlog-handler.js.map +1 -0
- package/dist/tools/manage-git.d.ts.map +1 -1
- package/dist/tools/manage-git.js +3 -0
- package/dist/tools/manage-git.js.map +1 -1
- package/dist/tools/promote-idea-handler.d.ts +4 -0
- package/dist/tools/promote-idea-handler.d.ts.map +1 -0
- package/dist/tools/promote-idea-handler.js +51 -0
- package/dist/tools/promote-idea-handler.js.map +1 -0
- package/dist/tools/register-backlog-tools.d.ts +3 -0
- package/dist/tools/register-backlog-tools.d.ts.map +1 -0
- package/dist/tools/register-backlog-tools.js +81 -0
- package/dist/tools/register-backlog-tools.js.map +1 -0
- package/dist/tools/register-ci-tools.d.ts.map +1 -1
- package/dist/tools/register-ci-tools.js +8 -0
- package/dist/tools/register-ci-tools.js.map +1 -1
- package/dist/tools/schemas/infra.d.ts +1 -1
- package/dist/tools/schemas/lifecycle.d.ts +1 -0
- package/dist/tools/schemas/lifecycle.d.ts.map +1 -1
- package/dist/tools/schemas/lifecycle.js +2 -1
- package/dist/tools/schemas/lifecycle.js.map +1 -1
- package/dist/tools/update-status-actions.d.ts.map +1 -1
- package/dist/tools/update-status-actions.js +12 -0
- package/dist/tools/update-status-actions.js.map +1 -1
- package/dist/types/ci.d.ts +16 -1
- package/dist/types/ci.d.ts.map +1 -1
- package/dist/types/common/tech-enums.d.ts +1 -1
- package/dist/types/common/tech-enums.d.ts.map +1 -1
- package/dist/types/estimation.d.ts +34 -0
- package/dist/types/estimation.d.ts.map +1 -1
- package/dist/types/git.d.ts +24 -0
- package/dist/types/git.d.ts.map +1 -1
- package/dist/types/ideas.d.ts +37 -0
- package/dist/types/ideas.d.ts.map +1 -0
- package/dist/types/ideas.js +3 -0
- package/dist/types/ideas.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
- package/src/config/contradiction-patterns.json +83 -0
- package/src/config/estimation-tables.json +49 -0
- package/src/config/license-plans.json +5 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { loadIdeas, saveIdea } from '../storage/ideas-store.js';
|
|
2
|
+
const EXPIRY_DAYS = 90;
|
|
3
|
+
/** Generate a unique idea ID based on date and sequence number. */
|
|
4
|
+
async function generateIdeaId(projectPath) {
|
|
5
|
+
const ideas = await loadIdeas(projectPath);
|
|
6
|
+
const today = new Date();
|
|
7
|
+
const dateStr = today.toISOString().slice(0, 10).replace(/-/g, '');
|
|
8
|
+
const todayPrefix = `idea-${dateStr}-`;
|
|
9
|
+
const todayCount = ideas.filter((i) => i.id.startsWith(todayPrefix)).length;
|
|
10
|
+
const seq = String(todayCount + 1).padStart(3, '0');
|
|
11
|
+
return `${todayPrefix}${seq}`;
|
|
12
|
+
}
|
|
13
|
+
/** Calculate expiry date 90 days from now. */
|
|
14
|
+
function calculateExpiry() {
|
|
15
|
+
const expiry = new Date();
|
|
16
|
+
expiry.setDate(expiry.getDate() + EXPIRY_DAYS);
|
|
17
|
+
return expiry.toISOString();
|
|
18
|
+
}
|
|
19
|
+
/** Handle capture_idea tool call. */
|
|
20
|
+
export async function handleCaptureIdea(input) {
|
|
21
|
+
const { projectPath, title, description, tags = [] } = input;
|
|
22
|
+
const id = await generateIdeaId(projectPath);
|
|
23
|
+
const now = new Date().toISOString();
|
|
24
|
+
const idea = {
|
|
25
|
+
id,
|
|
26
|
+
title,
|
|
27
|
+
description,
|
|
28
|
+
tags,
|
|
29
|
+
capturedAt: now,
|
|
30
|
+
capturedFrom: 'tool',
|
|
31
|
+
status: 'new',
|
|
32
|
+
expiresAt: calculateExpiry(),
|
|
33
|
+
};
|
|
34
|
+
await saveIdea(projectPath, idea);
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: 'text', text: `Idea guardada: [${id}] ${title}` }],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=capture-idea-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture-idea-handler.js","sourceRoot":"","sources":["../../src/tools/capture-idea-handler.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAEhE,MAAM,WAAW,GAAG,EAAE,CAAC;AAEvB,mEAAmE;AACnE,KAAK,UAAU,cAAc,CAAC,WAAmB;IAC/C,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,WAAW,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAG,IAAI,IAAI,EAAE,CAAC;IACzB,MAAM,OAAO,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACnE,MAAM,WAAW,GAAG,QAAQ,OAAO,GAAG,CAAC;IACvC,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC;IAC5E,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACpD,OAAO,GAAG,WAAW,GAAG,GAAG,EAAE,CAAC;AAChC,CAAC;AAED,8CAA8C;AAC9C,SAAS,eAAe;IACtB,MAAM,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;IAC1B,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,GAAG,WAAW,CAAC,CAAC;IAC/C,OAAO,MAAM,CAAC,WAAW,EAAE,CAAC;AAC9B,CAAC;AAED,qCAAqC;AACrC,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,KAAuB;IAC7D,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,GAAG,EAAE,EAAE,GAAG,KAAK,CAAC;IAE7D,MAAM,EAAE,GAAG,MAAM,cAAc,CAAC,WAAW,CAAC,CAAC;IAC7C,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAErC,MAAM,IAAI,GAAa;QACrB,EAAE;QACF,KAAK;QACL,WAAW;QACX,IAAI;QACJ,UAAU,EAAE,GAAG;QACf,YAAY,EAAE,MAAM;QACpB,MAAM,EAAE,KAAK;QACb,SAAS,EAAE,eAAe,EAAE;KAC7B,CAAC;IAEF,MAAM,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;IAElC,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,EAAE,KAAK,KAAK,EAAE,EAAE,CAAC;KACrE,CAAC;AACJ,CAAC"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { GeneratePlanuCIInput, ToolResult } from '../types/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Handle the generate_planu_ci tool.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Always generates a local planu-check.sh script (no GitHub billing required).
|
|
5
|
+
* Optionally generates a GitHub Actions workflow YAML for cloud CI.
|
|
6
6
|
*/
|
|
7
7
|
export declare function handleGeneratePlanuCI(args: GeneratePlanuCIInput): ToolResult;
|
|
8
8
|
//# sourceMappingURL=ci-planu-handler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ci-planu-handler.d.ts","sourceRoot":"","sources":["../../src/tools/ci-planu-handler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAG1E;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,oBAAoB,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"ci-planu-handler.d.ts","sourceRoot":"","sources":["../../src/tools/ci-planu-handler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAG1E;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,oBAAoB,GAAG,UAAU,CAqF5E"}
|
|
@@ -1,42 +1,75 @@
|
|
|
1
|
-
// tools/ci-planu-handler.ts — Handle generate_planu_ci tool calls (SPEC-066 AC-02..07)
|
|
1
|
+
// tools/ci-planu-handler.ts — Handle generate_planu_ci tool calls (SPEC-066 AC-02..07, SPEC-201)
|
|
2
2
|
import { generatePlanuCI } from '../engine/ci-generator/planu-steps.js';
|
|
3
3
|
/**
|
|
4
4
|
* Handle the generate_planu_ci tool.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Always generates a local planu-check.sh script (no GitHub billing required).
|
|
6
|
+
* Optionally generates a GitHub Actions workflow YAML for cloud CI.
|
|
7
7
|
*/
|
|
8
8
|
export function handleGeneratePlanuCI(args) {
|
|
9
|
+
const includeGithubActions = args.includeGithubActions ?? true;
|
|
9
10
|
const result = generatePlanuCI(args);
|
|
10
11
|
const lines = [
|
|
11
12
|
'## Planu CI Workflow Generated',
|
|
12
13
|
'',
|
|
13
14
|
`**Features enabled**: ${result.features.join(', ')}`,
|
|
14
|
-
`**Workflow path**: \`${result.workflowPath}\``,
|
|
15
15
|
'',
|
|
16
|
-
'
|
|
16
|
+
'---',
|
|
17
17
|
'',
|
|
18
|
-
'
|
|
19
|
-
|
|
18
|
+
'### Option 1 — Local script (no GitHub billing required)',
|
|
19
|
+
'',
|
|
20
|
+
'Save `planu-check.sh` to your project root and run it directly:',
|
|
21
|
+
'',
|
|
22
|
+
'```bash',
|
|
23
|
+
'chmod +x planu-check.sh',
|
|
24
|
+
'./planu-check.sh',
|
|
25
|
+
'```',
|
|
26
|
+
'',
|
|
27
|
+
'```bash',
|
|
28
|
+
result.localScript.trimEnd(),
|
|
29
|
+
'```',
|
|
30
|
+
'',
|
|
31
|
+
'**Optional: run as git pre-push hook** (catches issues before pushing):',
|
|
32
|
+
'',
|
|
33
|
+
'```bash',
|
|
34
|
+
'ln -s ../../planu-check.sh .git/hooks/pre-push',
|
|
35
|
+
'```',
|
|
36
|
+
'',
|
|
37
|
+
'---',
|
|
38
|
+
'',
|
|
39
|
+
'### Option 2 — Run GitHub Actions locally with act (no billing)',
|
|
40
|
+
'',
|
|
41
|
+
'[act](https://github.com/nektos/act) runs GitHub Actions workflows on your machine.',
|
|
42
|
+
'The generated workflow is fully compatible with act:',
|
|
43
|
+
'',
|
|
44
|
+
'```bash',
|
|
45
|
+
'# Install act (macOS)',
|
|
46
|
+
'brew install act',
|
|
47
|
+
'',
|
|
48
|
+
'# Run the Planu check locally',
|
|
49
|
+
'act -j planu-check',
|
|
20
50
|
'```',
|
|
21
51
|
'',
|
|
22
52
|
];
|
|
53
|
+
if (includeGithubActions && result.workflowYaml) {
|
|
54
|
+
lines.push('---', '', `### Option 3 — GitHub Actions (requires billing for private repos)`, '', `**Workflow path**: \`${result.workflowPath}\``, '', '```yaml', result.workflowYaml.trimEnd(), '```', '');
|
|
55
|
+
}
|
|
23
56
|
// .planu.yml config
|
|
24
|
-
lines.push('### Generated .planu.yml Configuration');
|
|
25
|
-
lines.push('');
|
|
26
|
-
lines.push('```yaml');
|
|
57
|
+
lines.push('---', '', '### Generated .planu.yml Configuration', '', '```yaml');
|
|
27
58
|
lines.push(result.planuConfigYaml.trimEnd());
|
|
28
|
-
lines.push('```');
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
lines.push(
|
|
38
|
-
|
|
39
|
-
|
|
59
|
+
lines.push('```', '');
|
|
60
|
+
// Badge (only if GitHub Actions enabled)
|
|
61
|
+
if (includeGithubActions && result.badgeMarkdown) {
|
|
62
|
+
lines.push('### Badge for README', '');
|
|
63
|
+
lines.push('Add this to your README.md (replace `{owner}` and `{repo}` with your values):');
|
|
64
|
+
lines.push('', '```markdown');
|
|
65
|
+
lines.push(result.badgeMarkdown);
|
|
66
|
+
lines.push('```', '');
|
|
67
|
+
}
|
|
68
|
+
lines.push(`> **Save**: \`${result.localScriptPath}\` (local script, always)`);
|
|
69
|
+
if (includeGithubActions && result.workflowPath) {
|
|
70
|
+
lines.push(`> **Save**: \`${result.workflowPath}\` (GitHub Actions, optional)`);
|
|
71
|
+
}
|
|
72
|
+
lines.push('> **Save**: `.planu.yml` (shared config for both modes)');
|
|
40
73
|
return {
|
|
41
74
|
content: [{ type: 'text', text: lines.join('\n') }],
|
|
42
75
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ci-planu-handler.js","sourceRoot":"","sources":["../../src/tools/ci-planu-handler.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"ci-planu-handler.js","sourceRoot":"","sources":["../../src/tools/ci-planu-handler.ts"],"names":[],"mappings":"AAAA,iGAAiG;AAGjG,OAAO,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAC;AAExE;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAA0B;IAC9D,MAAM,oBAAoB,GAAG,IAAI,CAAC,oBAAoB,IAAI,IAAI,CAAC;IAC/D,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;IAErC,MAAM,KAAK,GAAa;QACtB,gCAAgC;QAChC,EAAE;QACF,yBAAyB,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QACrD,EAAE;QACF,KAAK;QACL,EAAE;QACF,0DAA0D;QAC1D,EAAE;QACF,iEAAiE;QACjE,EAAE;QACF,SAAS;QACT,yBAAyB;QACzB,kBAAkB;QAClB,KAAK;QACL,EAAE;QACF,SAAS;QACT,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE;QAC5B,KAAK;QACL,EAAE;QACF,yEAAyE;QACzE,EAAE;QACF,SAAS;QACT,gDAAgD;QAChD,KAAK;QACL,EAAE;QACF,KAAK;QACL,EAAE;QACF,iEAAiE;QACjE,EAAE;QACF,qFAAqF;QACrF,sDAAsD;QACtD,EAAE;QACF,SAAS;QACT,uBAAuB;QACvB,kBAAkB;QAClB,EAAE;QACF,+BAA+B;QAC/B,oBAAoB;QACpB,KAAK;QACL,EAAE;KACH,CAAC;IAEF,IAAI,oBAAoB,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QAChD,KAAK,CAAC,IAAI,CACR,KAAK,EACL,EAAE,EACF,oEAAoE,EACpE,EAAE,EACF,wBAAwB,MAAM,CAAC,YAAY,IAAI,EAC/C,EAAE,EACF,SAAS,EACT,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,EAC7B,KAAK,EACL,EAAE,CACH,CAAC;IACJ,CAAC;IAED,oBAAoB;IACpB,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,EAAE,wCAAwC,EAAE,EAAE,EAAE,SAAS,CAAC,CAAC;IAC/E,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,OAAO,EAAE,CAAC,CAAC;IAC7C,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAEtB,yCAAyC;IACzC,IAAI,oBAAoB,IAAI,MAAM,CAAC,aAAa,EAAE,CAAC;QACjD,KAAK,CAAC,IAAI,CAAC,sBAAsB,EAAE,EAAE,CAAC,CAAC;QACvC,KAAK,CAAC,IAAI,CAAC,+EAA+E,CAAC,CAAC;QAC5F,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,aAAa,CAAC,CAAC;QAC9B,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;QACjC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACxB,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,iBAAiB,MAAM,CAAC,eAAe,2BAA2B,CAAC,CAAC;IAC/E,IAAI,oBAAoB,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QAChD,KAAK,CAAC,IAAI,CAAC,iBAAiB,MAAM,CAAC,YAAY,+BAA+B,CAAC,CAAC;IAClF,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,yDAAyD,CAAC,CAAC;IAEtE,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;KAC7D,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ToolResult, DiscardIdeaInput } from '../types/index.js';
|
|
2
|
+
/** Handle discard_idea tool call. Idempotent if already discarded. */
|
|
3
|
+
export declare function handleDiscardIdea(input: DiscardIdeaInput): Promise<ToolResult>;
|
|
4
|
+
//# sourceMappingURL=discard-idea-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discard-idea-handler.d.ts","sourceRoot":"","sources":["../../src/tools/discard-idea-handler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAGtE,sEAAsE;AACtE,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CA4BpF"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { loadIdeas, updateIdea } from '../storage/ideas-store.js';
|
|
2
|
+
/** Handle discard_idea tool call. Idempotent if already discarded. */
|
|
3
|
+
export async function handleDiscardIdea(input) {
|
|
4
|
+
const { projectPath, ideaId, reason } = input;
|
|
5
|
+
const ideas = await loadIdeas(projectPath);
|
|
6
|
+
const idea = ideas.find((i) => i.id === ideaId);
|
|
7
|
+
if (!idea) {
|
|
8
|
+
return {
|
|
9
|
+
content: [{ type: 'text', text: `Idea not found: ${ideaId}` }],
|
|
10
|
+
isError: true,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
// Idempotent: already discarded
|
|
14
|
+
if (idea.status === 'discarded') {
|
|
15
|
+
return {
|
|
16
|
+
content: [{ type: 'text', text: `Idea "${idea.title}" was already discarded.` }],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
await updateIdea(projectPath, ideaId, {
|
|
20
|
+
status: 'discarded',
|
|
21
|
+
discardedReason: reason,
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: 'text', text: `Idea "${idea.title}" discarded. Reason: ${reason}` }],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=discard-idea-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discard-idea-handler.js","sourceRoot":"","sources":["../../src/tools/discard-idea-handler.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAElE,sEAAsE;AACtE,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,KAAuB;IAC7D,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;IAE9C,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,WAAW,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,CAAC,CAAC;IAEhD,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,MAAM,EAAE,EAAE,CAAC;YAC9D,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,gCAAgC;IAChC,IAAI,IAAI,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;QAChC,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,IAAI,CAAC,KAAK,0BAA0B,EAAE,CAAC;SACjF,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE;QACpC,MAAM,EAAE,WAAW;QACnB,eAAe,EAAE,MAAM;KACxB,CAAC,CAAC;IAEH,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,IAAI,CAAC,KAAK,wBAAwB,MAAM,EAAE,EAAE,CAAC;KACvF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ToolResult, GitConfig, SpecDoneCleanupResult } from '../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* AC-02: Eliminate gone branches + remote prune.
|
|
4
|
+
* AC-03: List stale stashes (>7d); drop if force=true.
|
|
5
|
+
* AC-05: Idempotent.
|
|
6
|
+
* AC-06: Never deletes active branch or current HEAD.
|
|
7
|
+
* AC-07: Returns detailed report.
|
|
8
|
+
*/
|
|
9
|
+
export declare function handleCleanup(projectId: string, config?: GitConfig): Promise<ToolResult>;
|
|
10
|
+
/**
|
|
11
|
+
* AC-01: When a spec is marked done, clean its worktree, local branch, and remote branch.
|
|
12
|
+
* Best-effort — never throws. Caller should fire-and-forget or log result.
|
|
13
|
+
*/
|
|
14
|
+
export declare function cleanupSpecOnDone(projectPath: string, specId: string, gitBranch: string | undefined): Promise<SpecDoneCleanupResult>;
|
|
15
|
+
//# sourceMappingURL=cleanup-ops.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleanup-ops.d.ts","sourceRoot":"","sources":["../../../src/tools/git/cleanup-ops.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,UAAU,EAGV,SAAS,EACT,qBAAqB,EACtB,MAAM,sBAAsB,CAAC;AAgR9B;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CA8E9F;AAsBD;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,OAAO,CAAC,qBAAqB,CAAC,CAqFhC"}
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
// tools/git/cleanup-ops.ts — Git cleanup: worktrees, branches, remote prune, stashes (SPEC-203)
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { readdir } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { git, resolveProjectPath } from './git-helpers.js';
|
|
6
|
+
const WORKTREES_DIR = '.claude/worktrees';
|
|
7
|
+
const STALE_DAYS_THRESHOLD = 7;
|
|
8
|
+
const MILLISECONDS_PER_DAY = 86_400_000;
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Worktree helpers
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
/** Parse `git worktree list --porcelain` output into {path, branch} records. */
|
|
13
|
+
function parseWorktreeList(output) {
|
|
14
|
+
const entries = [];
|
|
15
|
+
let current = {};
|
|
16
|
+
for (const line of output.split('\n')) {
|
|
17
|
+
if (line.startsWith('worktree ')) {
|
|
18
|
+
current = { path: line.slice('worktree '.length).trim() };
|
|
19
|
+
}
|
|
20
|
+
else if (line.startsWith('branch ')) {
|
|
21
|
+
current.branch = line.slice('branch '.length).trim().replace('refs/heads/', '');
|
|
22
|
+
}
|
|
23
|
+
else if (line === '' && current.path) {
|
|
24
|
+
if (current.branch) {
|
|
25
|
+
entries.push({ path: current.path, branch: current.branch });
|
|
26
|
+
}
|
|
27
|
+
current = {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return entries;
|
|
31
|
+
}
|
|
32
|
+
/** Returns the set of branches already merged into develop (or main as fallback). */
|
|
33
|
+
async function getMergedBranches(projectPath) {
|
|
34
|
+
const bases = ['develop', 'main', 'master'];
|
|
35
|
+
for (const base of bases) {
|
|
36
|
+
try {
|
|
37
|
+
const { stdout } = await git(projectPath, ['branch', '--merged', base]);
|
|
38
|
+
return new Set(stdout
|
|
39
|
+
.split('\n')
|
|
40
|
+
.map((b) => b.replace(/^\*?\s+/, '').trim())
|
|
41
|
+
.filter(Boolean));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// try next base
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return new Set();
|
|
48
|
+
}
|
|
49
|
+
/** Remove a worktree — idempotent (ignores "not a worktree" errors). */
|
|
50
|
+
async function removeWorktree(projectPath, worktreePath) {
|
|
51
|
+
try {
|
|
52
|
+
await git(projectPath, ['worktree', 'remove', '--force', worktreePath]);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
56
|
+
// Already gone — not an error
|
|
57
|
+
if (!msg.includes('not a working tree') && !msg.includes('does not exist')) {
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function cleanupOrphanWorktrees(projectPath, mergedBranches) {
|
|
63
|
+
const removed = [];
|
|
64
|
+
const errors = [];
|
|
65
|
+
let worktreeOutput;
|
|
66
|
+
try {
|
|
67
|
+
const { stdout } = await git(projectPath, ['worktree', 'list', '--porcelain']);
|
|
68
|
+
worktreeOutput = stdout;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return { removed, errors };
|
|
72
|
+
}
|
|
73
|
+
const worktrees = parseWorktreeList(worktreeOutput);
|
|
74
|
+
const worktreesSubdir = join(projectPath, WORKTREES_DIR);
|
|
75
|
+
for (const wt of worktrees) {
|
|
76
|
+
// Never touch the main worktree (same path as projectPath)
|
|
77
|
+
if (wt.path === projectPath) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const isInWorktreesDir = wt.path.startsWith(worktreesSubdir);
|
|
81
|
+
const isMergedBranch = wt.branch && mergedBranches.has(wt.branch);
|
|
82
|
+
if (isInWorktreesDir || isMergedBranch) {
|
|
83
|
+
// Never remove if it's the current HEAD
|
|
84
|
+
try {
|
|
85
|
+
const { stdout: headBranch } = await git(projectPath, [
|
|
86
|
+
'rev-parse',
|
|
87
|
+
'--abbrev-ref',
|
|
88
|
+
'HEAD',
|
|
89
|
+
]);
|
|
90
|
+
if (headBranch.trim() === wt.branch) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Skip safety check failure — do not remove
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
await removeWorktree(projectPath, wt.path);
|
|
100
|
+
removed.push(wt.path);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
errors.push(`worktree ${wt.path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Also prune stale worktree admin dirs (git worktree prune)
|
|
108
|
+
try {
|
|
109
|
+
await git(projectPath, ['worktree', 'prune']);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// best-effort
|
|
113
|
+
}
|
|
114
|
+
return { removed, errors };
|
|
115
|
+
}
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Branch helpers
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
async function cleanupGoneBranches(projectPath) {
|
|
120
|
+
const removed = [];
|
|
121
|
+
const errors = [];
|
|
122
|
+
let vvOutput;
|
|
123
|
+
try {
|
|
124
|
+
const { stdout } = await git(projectPath, ['branch', '-vv']);
|
|
125
|
+
vvOutput = stdout;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return { removed, errors };
|
|
129
|
+
}
|
|
130
|
+
// Lines containing ": gone]" mean upstream was deleted (git branch -vv format: [origin/...: gone])
|
|
131
|
+
const goneBranches = vvOutput
|
|
132
|
+
.split('\n')
|
|
133
|
+
.filter((line) => line.includes(': gone]'))
|
|
134
|
+
.map((line) => line
|
|
135
|
+
.replace(/^\*?\s+/, '')
|
|
136
|
+
.split(/\s+/)[0]
|
|
137
|
+
?.trim() ?? '')
|
|
138
|
+
.filter(Boolean);
|
|
139
|
+
// Resolve current branch only when needed to avoid an extra git call (AC-05)
|
|
140
|
+
let currentBranch = '';
|
|
141
|
+
if (goneBranches.length > 0) {
|
|
142
|
+
try {
|
|
143
|
+
const { stdout } = await git(projectPath, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
144
|
+
currentBranch = stdout.trim();
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// ignore
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const branch of goneBranches) {
|
|
151
|
+
if (branch === currentBranch) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
await git(projectPath, ['branch', '-d', branch]);
|
|
156
|
+
removed.push(branch);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Force-delete if branch is not fully merged (upstream gone = safe)
|
|
160
|
+
try {
|
|
161
|
+
await git(projectPath, ['branch', '-D', branch]);
|
|
162
|
+
removed.push(branch);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
errors.push(`branch ${branch}: ${err instanceof Error ? err.message : String(err)}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { removed, errors };
|
|
170
|
+
}
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Stash helpers
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
function parseDaysOld(stashMessage) {
|
|
175
|
+
// git stash list format: stash@{0}: WIP on branch: hash message
|
|
176
|
+
// We detect age from the stash index timestamp via git log on the stash ref
|
|
177
|
+
// Since we can't get date from the message directly, we return 0 here
|
|
178
|
+
// and use git log to get the actual date per stash ref in detectStaleStashes.
|
|
179
|
+
void stashMessage;
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
async function detectStaleStashes(projectPath) {
|
|
183
|
+
const stale = [];
|
|
184
|
+
let listOutput;
|
|
185
|
+
try {
|
|
186
|
+
const { stdout } = await git(projectPath, ['stash', 'list']);
|
|
187
|
+
listOutput = stdout;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return stale;
|
|
191
|
+
}
|
|
192
|
+
const lines = listOutput.split('\n').filter(Boolean);
|
|
193
|
+
const now = Date.now();
|
|
194
|
+
for (const line of lines) {
|
|
195
|
+
const match = /^(stash@\{(\d+)\}):\s+(.+)$/.exec(line);
|
|
196
|
+
if (!match) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const [, ref, indexStr, message] = match;
|
|
200
|
+
if (!ref || !indexStr || !message) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const index = parseInt(indexStr, 10);
|
|
204
|
+
// Get commit date for this stash ref
|
|
205
|
+
let daysOld = parseDaysOld(message);
|
|
206
|
+
try {
|
|
207
|
+
const { stdout: dateOut } = await git(projectPath, ['log', '-1', '--format=%ct', ref]);
|
|
208
|
+
const epochSeconds = parseInt(dateOut.trim(), 10);
|
|
209
|
+
if (!isNaN(epochSeconds)) {
|
|
210
|
+
daysOld = Math.floor((now - epochSeconds * 1000) / MILLISECONDS_PER_DAY);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// Can't determine age — skip this stash
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (daysOld >= STALE_DAYS_THRESHOLD) {
|
|
218
|
+
stale.push({ index, ref, message, daysOld });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return stale;
|
|
222
|
+
}
|
|
223
|
+
async function dropStaleStashes(projectPath, stale) {
|
|
224
|
+
const errors = [];
|
|
225
|
+
// Drop in reverse index order so indices stay valid
|
|
226
|
+
const sorted = [...stale].sort((a, b) => b.index - a.index);
|
|
227
|
+
let dropped = 0;
|
|
228
|
+
for (const stash of sorted) {
|
|
229
|
+
try {
|
|
230
|
+
await git(projectPath, ['stash', 'drop', stash.ref]);
|
|
231
|
+
dropped++;
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
errors.push(`stash ${stash.ref}: ${err instanceof Error ? err.message : String(err)}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { dropped, errors };
|
|
238
|
+
}
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// Public handler
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
/**
|
|
243
|
+
* AC-02: Eliminate gone branches + remote prune.
|
|
244
|
+
* AC-03: List stale stashes (>7d); drop if force=true.
|
|
245
|
+
* AC-05: Idempotent.
|
|
246
|
+
* AC-06: Never deletes active branch or current HEAD.
|
|
247
|
+
* AC-07: Returns detailed report.
|
|
248
|
+
*/
|
|
249
|
+
export async function handleCleanup(projectId, config) {
|
|
250
|
+
const force = config?.force ?? false;
|
|
251
|
+
const errors = [];
|
|
252
|
+
let projectPath;
|
|
253
|
+
try {
|
|
254
|
+
projectPath = await resolveProjectPath(projectId);
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
return {
|
|
258
|
+
content: [
|
|
259
|
+
{
|
|
260
|
+
type: 'text',
|
|
261
|
+
text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) }, null, 2),
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
isError: true,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
// 1. Remote prune
|
|
268
|
+
let remotePruned = false;
|
|
269
|
+
try {
|
|
270
|
+
await git(projectPath, ['remote', 'prune', 'origin']);
|
|
271
|
+
remotePruned = true;
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
errors.push(`remote prune: ${err instanceof Error ? err.message : String(err)}`);
|
|
275
|
+
}
|
|
276
|
+
// 2. Get merged branches (used for worktree and branch cleanup)
|
|
277
|
+
const mergedBranches = await getMergedBranches(projectPath);
|
|
278
|
+
// 3. Remove orphan worktrees
|
|
279
|
+
const { removed: worktreesRemoved, errors: wtErrors } = await cleanupOrphanWorktrees(projectPath, mergedBranches);
|
|
280
|
+
errors.push(...wtErrors);
|
|
281
|
+
// 4. Remove branches with gone upstream
|
|
282
|
+
const { removed: branchesRemoved, errors: branchErrors } = await cleanupGoneBranches(projectPath);
|
|
283
|
+
errors.push(...branchErrors);
|
|
284
|
+
// 5. Detect / drop stale stashes
|
|
285
|
+
const staleStashes = await detectStaleStashes(projectPath);
|
|
286
|
+
let stashesDropped = 0;
|
|
287
|
+
if (force && staleStashes.length > 0) {
|
|
288
|
+
const { dropped, errors: stashErrors } = await dropStaleStashes(projectPath, staleStashes);
|
|
289
|
+
stashesDropped = dropped;
|
|
290
|
+
errors.push(...stashErrors);
|
|
291
|
+
}
|
|
292
|
+
const messageParts = [
|
|
293
|
+
`Worktrees removed: ${worktreesRemoved.length}`,
|
|
294
|
+
`Branches removed: ${branchesRemoved.length}`,
|
|
295
|
+
`Remote pruned: ${remotePruned ? 'yes' : 'no'}`,
|
|
296
|
+
`Stale stashes (>${STALE_DAYS_THRESHOLD}d): ${staleStashes.length}`,
|
|
297
|
+
...(force ? [`Stashes dropped: ${stashesDropped}`] : ['Use force:true to drop stale stashes']),
|
|
298
|
+
...(errors.length > 0 ? [`Errors: ${errors.length}`] : []),
|
|
299
|
+
];
|
|
300
|
+
const report = {
|
|
301
|
+
action: 'cleanup',
|
|
302
|
+
worktreesRemoved,
|
|
303
|
+
branchesRemoved,
|
|
304
|
+
remotePruned,
|
|
305
|
+
stalesStashes: staleStashes,
|
|
306
|
+
stashesDropped,
|
|
307
|
+
errors,
|
|
308
|
+
message: messageParts.join(' | '),
|
|
309
|
+
};
|
|
310
|
+
return {
|
|
311
|
+
content: [{ type: 'text', text: JSON.stringify(report, null, 2) }],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
// Spec-done cleanup (AC-01) — called from update-status-actions.ts
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
/** Checks whether a worktree path for a given spec exists in .claude/worktrees/ */
|
|
318
|
+
async function findSpecWorktree(projectPath, specId) {
|
|
319
|
+
const worktreesPath = join(projectPath, WORKTREES_DIR);
|
|
320
|
+
if (!existsSync(worktreesPath)) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const entries = await readdir(worktreesPath);
|
|
325
|
+
const match = entries.find((e) => e.toLowerCase().includes(specId.toLowerCase()));
|
|
326
|
+
return match ? join(worktreesPath, match) : null;
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* AC-01: When a spec is marked done, clean its worktree, local branch, and remote branch.
|
|
334
|
+
* Best-effort — never throws. Caller should fire-and-forget or log result.
|
|
335
|
+
*/
|
|
336
|
+
export async function cleanupSpecOnDone(projectPath, specId, gitBranch) {
|
|
337
|
+
const result = {
|
|
338
|
+
worktreeRemoved: null,
|
|
339
|
+
localBranchRemoved: null,
|
|
340
|
+
remoteBranchRemoved: null,
|
|
341
|
+
errors: [],
|
|
342
|
+
};
|
|
343
|
+
// 1. Remove worktree if it exists
|
|
344
|
+
const worktreePath = await findSpecWorktree(projectPath, specId);
|
|
345
|
+
if (worktreePath) {
|
|
346
|
+
try {
|
|
347
|
+
await removeWorktree(projectPath, worktreePath);
|
|
348
|
+
result.worktreeRemoved = worktreePath;
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
result.errors.push(`worktree: ${err instanceof Error ? err.message : String(err)}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (!gitBranch) {
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
// 2. Remove local branch if merged
|
|
358
|
+
try {
|
|
359
|
+
const { stdout } = await git(projectPath, ['branch', '--merged', 'develop']);
|
|
360
|
+
const merged = new Set(stdout
|
|
361
|
+
.split('\n')
|
|
362
|
+
.map((b) => b.replace(/^\*?\s+/, '').trim())
|
|
363
|
+
.filter(Boolean));
|
|
364
|
+
const isMerged = merged.has(gitBranch);
|
|
365
|
+
// Also check main as fallback
|
|
366
|
+
let isMergedMain = false;
|
|
367
|
+
if (!isMerged) {
|
|
368
|
+
try {
|
|
369
|
+
const { stdout: mainOut } = await git(projectPath, ['branch', '--merged', 'main']);
|
|
370
|
+
const mergedMain = new Set(mainOut
|
|
371
|
+
.split('\n')
|
|
372
|
+
.map((b) => b.replace(/^\*?\s+/, '').trim())
|
|
373
|
+
.filter(Boolean));
|
|
374
|
+
isMergedMain = mergedMain.has(gitBranch);
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
// ignore
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (isMerged || isMergedMain) {
|
|
381
|
+
// Never delete current branch
|
|
382
|
+
const { stdout: headOut } = await git(projectPath, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
383
|
+
if (headOut.trim() !== gitBranch) {
|
|
384
|
+
try {
|
|
385
|
+
await git(projectPath, ['branch', '-d', gitBranch]);
|
|
386
|
+
result.localBranchRemoved = gitBranch;
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// Force-delete if needed
|
|
390
|
+
try {
|
|
391
|
+
await git(projectPath, ['branch', '-D', gitBranch]);
|
|
392
|
+
result.localBranchRemoved = gitBranch;
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
result.errors.push(`local branch: ${err instanceof Error ? err.message : String(err)}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
result.errors.push(`merged check: ${err instanceof Error ? err.message : String(err)}`);
|
|
403
|
+
}
|
|
404
|
+
// 3. Remove remote branch if it exists
|
|
405
|
+
try {
|
|
406
|
+
const { stdout } = await git(projectPath, ['ls-remote', '--heads', 'origin', gitBranch]);
|
|
407
|
+
if (stdout.trim()) {
|
|
408
|
+
await git(projectPath, ['push', 'origin', '--delete', gitBranch]);
|
|
409
|
+
result.remoteBranchRemoved = `origin/${gitBranch}`;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
catch (err) {
|
|
413
|
+
result.errors.push(`remote branch: ${err instanceof Error ? err.message : String(err)}`);
|
|
414
|
+
}
|
|
415
|
+
return result;
|
|
416
|
+
}
|
|
417
|
+
//# sourceMappingURL=cleanup-ops.js.map
|