geeto 0.6.5 → 0.9.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 +23 -9
- package/lib/api/copilot-adapter.d.ts +14 -5
- package/lib/api/copilot-adapter.d.ts.map +1 -1
- package/lib/api/copilot-adapter.js +15 -21
- package/lib/api/copilot-adapter.js.map +1 -1
- package/lib/api/copilot-sdk.d.ts +3 -16
- package/lib/api/copilot-sdk.d.ts.map +1 -1
- package/lib/api/copilot-sdk.js +166 -441
- package/lib/api/copilot-sdk.js.map +1 -1
- package/lib/api/copilot.d.ts +3 -4
- package/lib/api/copilot.d.ts.map +1 -1
- package/lib/api/copilot.js +34 -31
- package/lib/api/copilot.js.map +1 -1
- package/lib/api/gemini-sdk.d.ts.map +1 -1
- package/lib/api/gemini-sdk.js +11 -77
- package/lib/api/gemini-sdk.js.map +1 -1
- package/lib/api/gemini.d.ts +2 -2
- package/lib/api/gemini.d.ts.map +1 -1
- package/lib/api/gemini.js +30 -22
- package/lib/api/gemini.js.map +1 -1
- package/lib/api/github.d.ts.map +1 -1
- package/lib/api/github.js +12 -6
- package/lib/api/github.js.map +1 -1
- package/lib/api/gitlab.d.ts +80 -0
- package/lib/api/gitlab.d.ts.map +1 -0
- package/lib/api/gitlab.js +192 -0
- package/lib/api/gitlab.js.map +1 -0
- package/lib/api/openrouter-sdk.d.ts +7 -7
- package/lib/api/openrouter-sdk.d.ts.map +1 -1
- package/lib/api/openrouter-sdk.js +17 -79
- package/lib/api/openrouter-sdk.js.map +1 -1
- package/lib/api/openrouter.d.ts.map +1 -1
- package/lib/api/openrouter.js +10 -20
- package/lib/api/openrouter.js.map +1 -1
- package/lib/api/platform.d.ts +78 -0
- package/lib/api/platform.d.ts.map +1 -0
- package/lib/api/platform.js +218 -0
- package/lib/api/platform.js.map +1 -0
- package/lib/api/trello.d.ts.map +1 -1
- package/lib/api/trello.js +8 -4
- package/lib/api/trello.js.map +1 -1
- package/lib/cli/input.d.ts +12 -3
- package/lib/cli/input.d.ts.map +1 -1
- package/lib/cli/input.js +123 -9
- package/lib/cli/input.js.map +1 -1
- package/lib/cli/menu.d.ts +1 -1
- package/lib/cli/menu.d.ts.map +1 -1
- package/lib/cli/menu.js +153 -96
- package/lib/cli/menu.js.map +1 -1
- package/lib/core/copilot-setup.d.ts +8 -7
- package/lib/core/copilot-setup.d.ts.map +1 -1
- package/lib/core/copilot-setup.js +60 -232
- package/lib/core/copilot-setup.js.map +1 -1
- package/lib/core/gemini-setup.js +7 -7
- package/lib/core/gemini-setup.js.map +1 -1
- package/lib/core/gitlab-setup.d.ts +5 -0
- package/lib/core/gitlab-setup.d.ts.map +1 -0
- package/lib/core/gitlab-setup.js +85 -0
- package/lib/core/gitlab-setup.js.map +1 -0
- package/lib/core/openrouter-setup.d.ts.map +1 -1
- package/lib/core/openrouter-setup.js +17 -0
- package/lib/core/openrouter-setup.js.map +1 -1
- package/lib/index.js +501 -704
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +11 -5
- package/lib/types/index.d.ts.map +1 -1
- package/lib/utils/ai-provider-helpers.d.ts +5 -0
- package/lib/utils/ai-provider-helpers.d.ts.map +1 -0
- package/lib/utils/ai-provider-helpers.js +23 -0
- package/lib/utils/ai-provider-helpers.js.map +1 -0
- package/lib/utils/ai-text.d.ts +23 -0
- package/lib/utils/ai-text.d.ts.map +1 -0
- package/lib/utils/ai-text.js +57 -0
- package/lib/utils/ai-text.js.map +1 -0
- package/lib/utils/ai-workflow.d.ts +18 -0
- package/lib/utils/ai-workflow.d.ts.map +1 -0
- package/lib/utils/ai-workflow.js +66 -0
- package/lib/utils/ai-workflow.js.map +1 -0
- package/lib/utils/branch-naming.d.ts.map +1 -1
- package/lib/utils/branch-naming.js +2 -4
- package/lib/utils/branch-naming.js.map +1 -1
- package/lib/utils/config.d.ts +13 -1
- package/lib/utils/config.d.ts.map +1 -1
- package/lib/utils/config.js +38 -1
- package/lib/utils/config.js.map +1 -1
- package/lib/utils/display.d.ts +24 -0
- package/lib/utils/display.d.ts.map +1 -1
- package/lib/utils/display.js +55 -1
- package/lib/utils/display.js.map +1 -1
- package/lib/utils/git-ai.d.ts +2 -0
- package/lib/utils/git-ai.d.ts.map +1 -1
- package/lib/utils/git-ai.js +30 -13
- package/lib/utils/git-ai.js.map +1 -1
- package/lib/utils/git-errors.d.ts.map +1 -1
- package/lib/utils/git-errors.js +4 -10
- package/lib/utils/git-errors.js.map +1 -1
- package/lib/utils/git.d.ts.map +1 -1
- package/lib/utils/git.js +5 -0
- package/lib/utils/git.js.map +1 -1
- package/lib/utils/github-helpers.d.ts +33 -0
- package/lib/utils/github-helpers.d.ts.map +1 -0
- package/lib/utils/github-helpers.js +101 -0
- package/lib/utils/github-helpers.js.map +1 -0
- package/lib/utils/logging.d.ts +8 -0
- package/lib/utils/logging.d.ts.map +1 -1
- package/lib/utils/logging.js +12 -0
- package/lib/utils/logging.js.map +1 -1
- package/lib/utils/prompt-loader.d.ts +9 -0
- package/lib/utils/prompt-loader.d.ts.map +1 -0
- package/lib/utils/prompt-loader.js +42 -0
- package/lib/utils/prompt-loader.js.map +1 -0
- package/lib/utils/prompts-embedded.d.ts +2 -0
- package/lib/utils/prompts-embedded.d.ts.map +1 -0
- package/lib/utils/prompts-embedded.js +255 -0
- package/lib/utils/prompts-embedded.js.map +1 -0
- package/lib/utils/scramble.d.ts +9 -86
- package/lib/utils/scramble.d.ts.map +1 -1
- package/lib/utils/scramble.js +27 -279
- package/lib/utils/scramble.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/workflows/alias.d.ts.map +1 -1
- package/lib/workflows/alias.js +1 -0
- package/lib/workflows/alias.js.map +1 -1
- package/lib/workflows/amend.d.ts.map +1 -1
- package/lib/workflows/amend.js +3 -6
- package/lib/workflows/amend.js.map +1 -1
- package/lib/workflows/branch-helpers.d.ts.map +1 -1
- package/lib/workflows/branch-helpers.js +0 -1
- package/lib/workflows/branch-helpers.js.map +1 -1
- package/lib/workflows/branch-utils.js +1 -1
- package/lib/workflows/branch-utils.js.map +1 -1
- package/lib/workflows/cleanup.js +3 -3
- package/lib/workflows/cleanup.js.map +1 -1
- package/lib/workflows/commit.d.ts.map +1 -1
- package/lib/workflows/commit.js +163 -202
- package/lib/workflows/commit.js.map +1 -1
- package/lib/workflows/doctor.d.ts +7 -0
- package/lib/workflows/doctor.d.ts.map +1 -0
- package/lib/workflows/doctor.js +284 -0
- package/lib/workflows/doctor.js.map +1 -0
- package/lib/workflows/history.d.ts.map +1 -1
- package/lib/workflows/history.js +2 -1
- package/lib/workflows/history.js.map +1 -1
- package/lib/workflows/issue.d.ts +1 -1
- package/lib/workflows/issue.d.ts.map +1 -1
- package/lib/workflows/issue.js +31 -130
- package/lib/workflows/issue.js.map +1 -1
- package/lib/workflows/main-helpers.d.ts +34 -0
- package/lib/workflows/main-helpers.d.ts.map +1 -0
- package/lib/workflows/main-helpers.js +346 -0
- package/lib/workflows/main-helpers.js.map +1 -0
- package/lib/workflows/main-steps.d.ts.map +1 -1
- package/lib/workflows/main-steps.js +11 -139
- package/lib/workflows/main-steps.js.map +1 -1
- package/lib/workflows/main.d.ts +2 -6
- package/lib/workflows/main.d.ts.map +1 -1
- package/lib/workflows/main.js +43 -365
- package/lib/workflows/main.js.map +1 -1
- package/lib/workflows/pr.d.ts +2 -2
- package/lib/workflows/pr.d.ts.map +1 -1
- package/lib/workflows/pr.js +52 -152
- package/lib/workflows/pr.js.map +1 -1
- package/lib/workflows/prune.d.ts.map +1 -1
- package/lib/workflows/prune.js +2 -10
- package/lib/workflows/prune.js.map +1 -1
- package/lib/workflows/pull.d.ts.map +1 -1
- package/lib/workflows/pull.js +2 -24
- package/lib/workflows/pull.js.map +1 -1
- package/lib/workflows/release-merge.d.ts +12 -0
- package/lib/workflows/release-merge.d.ts.map +1 -0
- package/lib/workflows/release-merge.js +569 -0
- package/lib/workflows/release-merge.js.map +1 -0
- package/lib/workflows/release-notes.d.ts +13 -0
- package/lib/workflows/release-notes.d.ts.map +1 -0
- package/lib/workflows/release-notes.js +141 -0
- package/lib/workflows/release-notes.js.map +1 -0
- package/lib/workflows/release-recover.d.ts +5 -0
- package/lib/workflows/release-recover.d.ts.map +1 -0
- package/lib/workflows/release-recover.js +137 -0
- package/lib/workflows/release-recover.js.map +1 -0
- package/lib/workflows/release-sync.d.ts +7 -0
- package/lib/workflows/release-sync.d.ts.map +1 -0
- package/lib/workflows/release-sync.js +321 -0
- package/lib/workflows/release-sync.js.map +1 -0
- package/lib/workflows/release-utils.d.ts +36 -0
- package/lib/workflows/release-utils.d.ts.map +1 -0
- package/lib/workflows/release-utils.js +150 -0
- package/lib/workflows/release-utils.js.map +1 -0
- package/lib/workflows/release.d.ts.map +1 -1
- package/lib/workflows/release.js +97 -723
- package/lib/workflows/release.js.map +1 -1
- package/lib/workflows/repo-settings.d.ts +2 -2
- package/lib/workflows/repo-settings.d.ts.map +1 -1
- package/lib/workflows/repo-settings.js +35 -25
- package/lib/workflows/repo-settings.js.map +1 -1
- package/lib/workflows/reword.d.ts.map +1 -1
- package/lib/workflows/reword.js +159 -167
- package/lib/workflows/reword.js.map +1 -1
- package/lib/workflows/security-gate.d.ts.map +1 -1
- package/lib/workflows/security-gate.js +18 -94
- package/lib/workflows/security-gate.js.map +1 -1
- package/lib/workflows/settings.d.ts +2 -1
- package/lib/workflows/settings.d.ts.map +1 -1
- package/lib/workflows/settings.js +289 -19
- package/lib/workflows/settings.js.map +1 -1
- package/lib/workflows/stash.d.ts.map +1 -1
- package/lib/workflows/stash.js +2 -1
- package/lib/workflows/stash.js.map +1 -1
- package/lib/workflows/trello-menu.d.ts +2 -5
- package/lib/workflows/trello-menu.d.ts.map +1 -1
- package/lib/workflows/trello-menu.js +67 -228
- package/lib/workflows/trello-menu.js.map +1 -1
- package/lib/workflows/undo.d.ts.map +1 -1
- package/lib/workflows/undo.js +2 -1
- package/lib/workflows/undo.js.map +1 -1
- package/package.json +3 -6
- package/prompts/branch-name-prompt.md +4 -0
- package/prompts/commit-message-prompt.md +12 -0
- package/prompts/issue-prompt.md +19 -0
- package/prompts/issue-review.with-context.prompt.yml +77 -0
- package/prompts/pr-prompt.md +14 -0
- package/prompts/release-notes-prompt.md +35 -0
- package/prompts/repo-description-prompt.md +1 -0
- package/prompts/security-gate-prompt.md +80 -0
package/lib/workflows/release.js
CHANGED
|
@@ -4,693 +4,45 @@
|
|
|
4
4
|
* RELEASE.MD (user-friendly) and CHANGELOG.md (developer-facing)
|
|
5
5
|
*/
|
|
6
6
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { handleMergeReleases } from './release-merge.js';
|
|
8
|
+
import { generateChangelogEntry, generateReleaseMd, normalizeReleaseMarkdown, } from './release-notes.js';
|
|
9
|
+
import { handleRecoverTags } from './release-recover.js';
|
|
10
|
+
import { handleDeleteReleases, handleSyncReleases } from './release-sync.js';
|
|
11
|
+
import { bumpPrerelease, categorizeCommits, formatSemver, getCommitsSinceTag, getCurrentVersion, getExistingTags, parseSemver, promoteToStable, updatePackageVersion, } from './release-utils.js';
|
|
7
12
|
import { askQuestion, confirm, editInline } from '../cli/input.js';
|
|
8
13
|
import { select } from '../cli/menu.js';
|
|
9
14
|
import { colors } from '../utils/colors.js';
|
|
15
|
+
import { BOX_W } from '../utils/display.js';
|
|
10
16
|
import { exec, execAsync, execSilent } from '../utils/exec.js';
|
|
11
17
|
import { chooseModelForProvider, generateReleaseNotesWithProvider, getAIProviderShortName, getModelValue, } from '../utils/git-ai.js';
|
|
18
|
+
import { detectPlatformFromRemote, getPlatformCLI } from '../utils/github-helpers.js';
|
|
12
19
|
import { log } from '../utils/logging.js';
|
|
13
20
|
import { ScrambleProgress } from '../utils/scramble.js';
|
|
14
21
|
import { loadState } from '../utils/state.js';
|
|
15
|
-
// ─── Helpers ───
|
|
16
|
-
/**
|
|
17
|
-
* Normalize markdown spacing for consistent markdownlint-friendly output.
|
|
18
|
-
* Ensures: one blank line after ### and #### headings, one blank line between sections,
|
|
19
|
-
* no double blank lines, trailing newline.
|
|
20
|
-
*/
|
|
21
|
-
const normalizeReleaseMarkdown = (md) => {
|
|
22
|
-
const lines = md.split('\n');
|
|
23
|
-
const result = [];
|
|
24
|
-
for (let i = 0; i < lines.length; i++) {
|
|
25
|
-
const line = lines[i] ?? '';
|
|
26
|
-
const nextLine = lines[i + 1] ?? '';
|
|
27
|
-
result.push(line);
|
|
28
|
-
// After a heading (### or ####), ensure exactly one blank line before content
|
|
29
|
-
if ((line.startsWith('###') || line.startsWith('####')) && nextLine.trim() !== '') {
|
|
30
|
-
result.push('');
|
|
31
|
-
}
|
|
32
|
-
// After a bullet line, if next line is a heading, ensure blank line
|
|
33
|
-
if (line.startsWith('-') && (nextLine.startsWith('###') || nextLine.startsWith('####'))) {
|
|
34
|
-
result.push('');
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
// Collapse multiple blank lines into one
|
|
38
|
-
return result
|
|
39
|
-
.join('\n')
|
|
40
|
-
.replaceAll(/\n{3,}/g, '\n\n')
|
|
41
|
-
.trim();
|
|
42
|
-
};
|
|
43
|
-
const parseSemver = (version) => {
|
|
44
|
-
const match = version.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
45
|
-
if (!match)
|
|
46
|
-
return null;
|
|
47
|
-
return {
|
|
48
|
-
major: Number.parseInt(match[1] ?? '0', 10),
|
|
49
|
-
minor: Number.parseInt(match[2] ?? '0', 10),
|
|
50
|
-
patch: Number.parseInt(match[3] ?? '0', 10),
|
|
51
|
-
};
|
|
52
|
-
};
|
|
53
|
-
const getCurrentVersion = () => {
|
|
54
|
-
try {
|
|
55
|
-
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
|
|
56
|
-
return pkg.version ?? '0.0.0';
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return '0.0.0';
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
const getExistingTags = () => {
|
|
63
|
-
try {
|
|
64
|
-
// Sort by creation date (newest first), NOT version number.
|
|
65
|
-
// Version sort breaks when older dummy/test tags have higher semver (e.g. v2.0.0 before v0.3.x).
|
|
66
|
-
const output = execSilent('git tag --list --sort=-creatordate').trim();
|
|
67
|
-
return output ? output.split('\n').filter(Boolean) : [];
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
return [];
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
const getCommitsSinceTag = (tag) => {
|
|
74
|
-
try {
|
|
75
|
-
const sep = '<<GTO>>';
|
|
76
|
-
const range = tag ? `${tag}..HEAD` : 'HEAD';
|
|
77
|
-
const output = execSilent(`git log ${range} --format="%H${sep}%h${sep}%s${sep}%an${sep}%ci" --no-merges`).trim();
|
|
78
|
-
if (!output)
|
|
79
|
-
return [];
|
|
80
|
-
return output
|
|
81
|
-
.split('\n')
|
|
82
|
-
.filter(Boolean)
|
|
83
|
-
.map((line) => {
|
|
84
|
-
const parts = line.split(sep);
|
|
85
|
-
return {
|
|
86
|
-
hash: parts[0] ?? '',
|
|
87
|
-
short: parts[1] ?? '',
|
|
88
|
-
subject: parts[2] ?? '',
|
|
89
|
-
author: parts[3] ?? '',
|
|
90
|
-
date: parts[4] ?? '',
|
|
91
|
-
};
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
return [];
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
const categorizeCommits = (commits) => {
|
|
99
|
-
const result = {
|
|
100
|
-
features: [],
|
|
101
|
-
fixes: [],
|
|
102
|
-
breaking: [],
|
|
103
|
-
other: [],
|
|
104
|
-
};
|
|
105
|
-
for (const c of commits) {
|
|
106
|
-
if (c.subject.includes('BREAKING CHANGE') || c.subject.includes('!:')) {
|
|
107
|
-
result.breaking = [...result.breaking, c];
|
|
108
|
-
}
|
|
109
|
-
else if (c.subject.startsWith('feat')) {
|
|
110
|
-
result.features = [...result.features, c];
|
|
111
|
-
}
|
|
112
|
-
else if (c.subject.startsWith('fix')) {
|
|
113
|
-
result.fixes = [...result.fixes, c];
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
result.other = [...result.other, c];
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return result;
|
|
120
|
-
};
|
|
121
|
-
const getRepoUrl = () => {
|
|
122
|
-
try {
|
|
123
|
-
return execSilent('git config --get remote.origin.url')
|
|
124
|
-
.trim()
|
|
125
|
-
.replace(/\.git$/, '')
|
|
126
|
-
.replace(/^git@github\.com:/, 'https://github.com/');
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
return '';
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
const updatePackageVersion = (newVersion) => {
|
|
133
|
-
const content = readFileSync('package.json', 'utf8');
|
|
134
|
-
const pkg = JSON.parse(content);
|
|
135
|
-
pkg.version = newVersion;
|
|
136
|
-
writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
137
|
-
// Also update the compiled-binary-safe version constant
|
|
138
|
-
try {
|
|
139
|
-
const versionTs = readFileSync('src/version.ts', 'utf8');
|
|
140
|
-
writeFileSync('src/version.ts', versionTs.replace(/VERSION = '[^']*'/, `VERSION = '${newVersion}'`), 'utf8');
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
/* version.ts update is best-effort */
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
// ─── stripConventional helpers ───
|
|
147
|
-
const stripFeatPrefix = (s) => {
|
|
148
|
-
const idx = s.indexOf(': ');
|
|
149
|
-
if (idx !== -1 && s.slice(0, idx).startsWith('feat'))
|
|
150
|
-
return s.slice(idx + 2);
|
|
151
|
-
return s.replace(/^feat:\s*/, '');
|
|
152
|
-
};
|
|
153
|
-
const stripFixPrefix = (s) => {
|
|
154
|
-
const idx = s.indexOf(': ');
|
|
155
|
-
if (idx !== -1 && s.slice(0, idx).startsWith('fix'))
|
|
156
|
-
return s.slice(idx + 2);
|
|
157
|
-
return s.replace(/^fix:\s*/, '');
|
|
158
|
-
};
|
|
159
|
-
const stripBreakingPrefix = (s) => {
|
|
160
|
-
const idx = s.indexOf('!: ');
|
|
161
|
-
if (idx !== -1)
|
|
162
|
-
return s.slice(idx + 3);
|
|
163
|
-
return s.replace(/BREAKING CHANGE:\s*/, '');
|
|
164
|
-
};
|
|
165
|
-
const stripConventionalPrefix = (s) => {
|
|
166
|
-
const idx = s.indexOf(': ');
|
|
167
|
-
if (idx !== -1)
|
|
168
|
-
return s.slice(idx + 2);
|
|
169
|
-
return s;
|
|
170
|
-
};
|
|
171
|
-
// ─── RELEASE.MD generator (user-facing, simple language) ───
|
|
172
|
-
const generateReleaseMd = (version, commits, prevVersion) => {
|
|
173
|
-
const now = new Date();
|
|
174
|
-
const date = now.toLocaleDateString('en-US', {
|
|
175
|
-
year: 'numeric',
|
|
176
|
-
month: 'long',
|
|
177
|
-
day: 'numeric',
|
|
178
|
-
});
|
|
179
|
-
const cat = categorizeCommits(commits);
|
|
180
|
-
// Each version is a ## section so multiple versions stack in a single file
|
|
181
|
-
const header = [
|
|
182
|
-
`## v${version} — ${date}`,
|
|
183
|
-
'',
|
|
184
|
-
`> Previous version: v${prevVersion}`,
|
|
185
|
-
'',
|
|
186
|
-
"### What's New?",
|
|
187
|
-
'',
|
|
188
|
-
];
|
|
189
|
-
const featureSection = cat.features.length > 0
|
|
190
|
-
? ['#### New Features', '', ...cat.features.map((f) => `- ${stripFeatPrefix(f.subject)}`), '']
|
|
191
|
-
: [];
|
|
192
|
-
const fixSection = cat.fixes.length > 0
|
|
193
|
-
? ['#### Bug Fixes', '', ...cat.fixes.map((f) => `- ${stripFixPrefix(f.subject)}`), '']
|
|
194
|
-
: [];
|
|
195
|
-
const breakingSection = cat.breaking.length > 0
|
|
196
|
-
? [
|
|
197
|
-
'#### Important Changes',
|
|
198
|
-
'',
|
|
199
|
-
'> Note: Some changes in this version may require adjustments.',
|
|
200
|
-
'',
|
|
201
|
-
...cat.breaking.map((b) => `- ${stripBreakingPrefix(b.subject)}`),
|
|
202
|
-
'',
|
|
203
|
-
]
|
|
204
|
-
: [];
|
|
205
|
-
const otherSection = cat.other.length > 0
|
|
206
|
-
? [
|
|
207
|
-
'#### Other Improvements',
|
|
208
|
-
'',
|
|
209
|
-
...cat.other.map((o) => `- ${stripConventionalPrefix(o.subject)}`),
|
|
210
|
-
'',
|
|
211
|
-
]
|
|
212
|
-
: [];
|
|
213
|
-
const empty = commits.length === 0 ? ['No significant changes in this version.', ''] : [];
|
|
214
|
-
return [
|
|
215
|
-
...header,
|
|
216
|
-
...featureSection,
|
|
217
|
-
...fixSection,
|
|
218
|
-
...breakingSection,
|
|
219
|
-
...otherSection,
|
|
220
|
-
...empty,
|
|
221
|
-
'---',
|
|
222
|
-
'',
|
|
223
|
-
].join('\n');
|
|
224
|
-
};
|
|
225
|
-
// ─── CHANGELOG.md generator (developer-facing, per-commit) ───
|
|
226
|
-
const generateChangelogEntry = (version, commits, prevVersion) => {
|
|
227
|
-
const repoUrl = getRepoUrl();
|
|
228
|
-
const dateStr = new Date().toISOString().slice(0, 10);
|
|
229
|
-
const cat = categorizeCommits(commits);
|
|
230
|
-
const commitLink = (c) => repoUrl ? `[${c.short}](${repoUrl}/commit/${c.short})` : c.short;
|
|
231
|
-
const versionLink = repoUrl
|
|
232
|
-
? `[${version}](${repoUrl}/compare/v${prevVersion}...v${version})`
|
|
233
|
-
: version;
|
|
234
|
-
const header = [`## ${versionLink} (${dateStr})`, ''];
|
|
235
|
-
const breakingSection = cat.breaking.length > 0
|
|
236
|
-
? [
|
|
237
|
-
'### BREAKING CHANGES',
|
|
238
|
-
'',
|
|
239
|
-
...cat.breaking.map((c) => `* ${c.subject} (${commitLink(c)})`),
|
|
240
|
-
'',
|
|
241
|
-
]
|
|
242
|
-
: [];
|
|
243
|
-
const featureSection = cat.features.length > 0
|
|
244
|
-
? ['### Features', '', ...cat.features.map((c) => `* ${c.subject} (${commitLink(c)})`), '']
|
|
245
|
-
: [];
|
|
246
|
-
const fixSection = cat.fixes.length > 0
|
|
247
|
-
? ['### Bug Fixes', '', ...cat.fixes.map((c) => `* ${c.subject} (${commitLink(c)})`), '']
|
|
248
|
-
: [];
|
|
249
|
-
const otherSection = cat.other.length > 0
|
|
250
|
-
? ['### Other Changes', '', ...cat.other.map((c) => `* ${c.subject} (${commitLink(c)})`), '']
|
|
251
|
-
: [];
|
|
252
|
-
return [...header, ...breakingSection, ...featureSection, ...fixSection, ...otherSection].join('\n');
|
|
253
|
-
};
|
|
254
|
-
// ─── Sync GitHub Releases for existing tags ───
|
|
255
|
-
const getExistingGithubReleases = () => {
|
|
256
|
-
try {
|
|
257
|
-
const output = execSilent('gh release list --limit 100 --json tagName --jq ".[].tagName"').trim();
|
|
258
|
-
return output ? output.split('\n').filter(Boolean) : [];
|
|
259
|
-
}
|
|
260
|
-
catch {
|
|
261
|
-
return [];
|
|
262
|
-
}
|
|
263
|
-
};
|
|
264
|
-
const handleSyncReleases = async () => {
|
|
265
|
-
const line = '─'.repeat(56);
|
|
266
|
-
// Check if gh CLI is available
|
|
267
|
-
try {
|
|
268
|
-
execSilent('gh --version');
|
|
269
|
-
}
|
|
270
|
-
catch {
|
|
271
|
-
log.error('GitHub CLI (gh) is not installed. Install it: https://cli.github.com');
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
console.log('');
|
|
275
|
-
const spinner = new ScrambleProgress();
|
|
276
|
-
spinner.start(['connecting to github...', 'fetching releases...', 'comparing tags...']);
|
|
277
|
-
const localTags = getExistingTags();
|
|
278
|
-
const ghReleases = getExistingGithubReleases();
|
|
279
|
-
const missingTags = localTags.filter((t) => !ghReleases.includes(t));
|
|
280
|
-
spinner.succeed(`Found ${localTags.length} tags, ${ghReleases.length} GitHub releases`);
|
|
281
|
-
if (missingTags.length === 0) {
|
|
282
|
-
console.log('');
|
|
283
|
-
log.success('All tags have GitHub Releases! Nothing to sync.');
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
console.log('');
|
|
287
|
-
log.info(`${colors.bright}${missingTags.length}${colors.reset} tags missing GitHub Releases:`);
|
|
288
|
-
for (const tag of missingTags) {
|
|
289
|
-
console.log(` ${colors.yellow}${tag}${colors.reset}`);
|
|
290
|
-
}
|
|
291
|
-
console.log('');
|
|
292
|
-
const action = await select('What do you want to do?', [
|
|
293
|
-
{ label: 'Create releases for all missing tags', value: 'all' },
|
|
294
|
-
{ label: 'Select which tags to release', value: 'select' },
|
|
295
|
-
{ label: 'Cancel', value: 'cancel' },
|
|
296
|
-
]);
|
|
297
|
-
if (action === 'cancel')
|
|
298
|
-
return;
|
|
299
|
-
let tagsToRelease = missingTags;
|
|
300
|
-
if (action === 'select') {
|
|
301
|
-
const { multiSelect } = await import('../cli/menu.js');
|
|
302
|
-
const choices = missingTags.map((t) => ({ label: t, value: t }));
|
|
303
|
-
const selected = await multiSelect('Select tags to create releases for:', choices);
|
|
304
|
-
if (selected.length === 0) {
|
|
305
|
-
log.info('No tags selected.');
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
tagsToRelease = selected;
|
|
309
|
-
}
|
|
310
|
-
// Choose release notes mode: AI or template
|
|
311
|
-
console.log('');
|
|
312
|
-
const notesMode = await select('How should release notes be generated?', [
|
|
313
|
-
{ label: 'AI-generated (recommended)', value: 'ai' },
|
|
314
|
-
{ label: 'Auto-generate (template-based)', value: 'auto' },
|
|
315
|
-
]);
|
|
316
|
-
// AI setup if needed
|
|
317
|
-
let useAI = notesMode === 'ai';
|
|
318
|
-
let language = 'en';
|
|
319
|
-
let aiProvider = 'copilot';
|
|
320
|
-
let copilotModel;
|
|
321
|
-
let openrouterModel;
|
|
322
|
-
let geminiModel;
|
|
323
|
-
if (useAI) {
|
|
324
|
-
language = (await select('Release notes language:', [
|
|
325
|
-
{ label: 'English', value: 'en' },
|
|
326
|
-
{ label: 'Indonesian (Bahasa Indonesia)', value: 'id' },
|
|
327
|
-
]));
|
|
328
|
-
// Check saved config
|
|
329
|
-
const savedState = loadState();
|
|
330
|
-
if (savedState?.aiProvider &&
|
|
331
|
-
savedState.aiProvider !== 'manual' &&
|
|
332
|
-
(savedState.copilotModel || savedState.openrouterModel || savedState.geminiModel)) {
|
|
333
|
-
aiProvider = savedState.aiProvider;
|
|
334
|
-
copilotModel = savedState.copilotModel;
|
|
335
|
-
openrouterModel = savedState.openrouterModel;
|
|
336
|
-
geminiModel = savedState.geminiModel;
|
|
337
|
-
}
|
|
338
|
-
else {
|
|
339
|
-
let providerChosen = false;
|
|
340
|
-
while (!providerChosen) {
|
|
341
|
-
aiProvider = (await select('Choose AI Provider:', [
|
|
342
|
-
{ label: 'GitHub (Recommended)', value: 'copilot' },
|
|
343
|
-
{ label: 'Gemini', value: 'gemini' },
|
|
344
|
-
{ label: 'OpenRouter', value: 'openrouter' },
|
|
345
|
-
]));
|
|
346
|
-
const chosen = await chooseModelForProvider(aiProvider, undefined, 'Back to AI provider menu');
|
|
347
|
-
if (!chosen || chosen === 'back')
|
|
348
|
-
continue;
|
|
349
|
-
switch (aiProvider) {
|
|
350
|
-
case 'gemini': {
|
|
351
|
-
geminiModel = chosen;
|
|
352
|
-
break;
|
|
353
|
-
}
|
|
354
|
-
case 'copilot': {
|
|
355
|
-
copilotModel = chosen;
|
|
356
|
-
break;
|
|
357
|
-
}
|
|
358
|
-
case 'openrouter': {
|
|
359
|
-
openrouterModel = chosen;
|
|
360
|
-
break;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
providerChosen = true;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
// Preview and confirm
|
|
368
|
-
console.log('');
|
|
369
|
-
console.log(`${colors.cyan}┌${line}┐${colors.reset}`);
|
|
370
|
-
console.log(`${colors.cyan}│${colors.reset} ${colors.bright}Sync Plan${colors.reset}`);
|
|
371
|
-
console.log(`${colors.cyan}├${line}┤${colors.reset}`);
|
|
372
|
-
for (const tag of tagsToRelease) {
|
|
373
|
-
const ver = tag.replace(/^v/, '');
|
|
374
|
-
console.log(`${colors.cyan}│${colors.reset} ${colors.green}+${colors.reset} Create release for ${colors.yellow}${ver}${colors.reset}`);
|
|
375
|
-
}
|
|
376
|
-
console.log(`${colors.cyan}│${colors.reset} ${colors.gray}Mode: ${useAI ? 'AI-generated' : 'Template-based'}${colors.reset}`);
|
|
377
|
-
console.log(`${colors.cyan}└${line}┘${colors.reset}`);
|
|
378
|
-
console.log('');
|
|
379
|
-
const proceed = confirm(`Create ${tagsToRelease.length} GitHub Releases?`);
|
|
380
|
-
if (!proceed)
|
|
381
|
-
return;
|
|
382
|
-
// Create releases one by one
|
|
383
|
-
const allTags = getExistingTags();
|
|
384
|
-
let successCount = 0;
|
|
385
|
-
for (const tag of tagsToRelease) {
|
|
386
|
-
const ver = tag.replace(/^v/, '');
|
|
387
|
-
const tagIdx = allTags.indexOf(tag);
|
|
388
|
-
const prevTag = allTags[tagIdx + 1];
|
|
389
|
-
const commits = getCommitsSinceTag(prevTag);
|
|
390
|
-
const commitList = commits.map((c) => c.subject).join('\n');
|
|
391
|
-
let releaseBody;
|
|
392
|
-
if (useAI && commits.length > 0) {
|
|
393
|
-
console.log('');
|
|
394
|
-
const aiSpinner = new ScrambleProgress();
|
|
395
|
-
const modelDisplay = getModelValue(copilotModel ?? openrouterModel ?? geminiModel ?? '');
|
|
396
|
-
aiSpinner.start([
|
|
397
|
-
'preparing release context...',
|
|
398
|
-
`generating notes with ${getAIProviderShortName(aiProvider)}${modelDisplay ? ` (${modelDisplay})` : ''}...`,
|
|
399
|
-
'processing results...',
|
|
400
|
-
]);
|
|
401
|
-
const aiResult = await generateReleaseNotesWithProvider(aiProvider, commitList, language, undefined, copilotModel, openrouterModel, geminiModel);
|
|
402
|
-
if (aiResult) {
|
|
403
|
-
aiSpinner.succeed(`Notes generated for ${tag}`);
|
|
404
|
-
releaseBody = normalizeReleaseMarkdown(aiResult);
|
|
405
|
-
// Preview notes for user review
|
|
406
|
-
console.log('');
|
|
407
|
-
console.log(`${colors.cyan}┌${'─'.repeat(56)}┐${colors.reset}`);
|
|
408
|
-
console.log(`${colors.cyan}│${colors.reset} ${colors.bright}Release Notes — ${tag}${colors.reset}`);
|
|
409
|
-
console.log(`${colors.cyan}├${'─'.repeat(56)}┤${colors.reset}`);
|
|
410
|
-
for (const noteLine of releaseBody.split('\n')) {
|
|
411
|
-
console.log(`${colors.cyan}│${colors.reset} ${noteLine}`);
|
|
412
|
-
}
|
|
413
|
-
console.log(`${colors.cyan}└${'─'.repeat(56)}┘${colors.reset}`);
|
|
414
|
-
console.log('');
|
|
415
|
-
const reviewAction = await select(`Publish release for ${tag}?`, [
|
|
416
|
-
{ label: 'Yes, publish', value: 'accept' },
|
|
417
|
-
{ label: 'Skip this tag', value: 'skip' },
|
|
418
|
-
]);
|
|
419
|
-
if (reviewAction === 'skip')
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
else {
|
|
423
|
-
aiSpinner.fail(`AI failed for ${tag}, using template`);
|
|
424
|
-
useAI = false;
|
|
425
|
-
releaseBody = generateReleaseMd(ver, commits, prevTag?.replace(/^v/, '') ?? '0.0.0')
|
|
426
|
-
.replace(/^## .*\n+/, '')
|
|
427
|
-
.replace(/\n---\n*$/, '')
|
|
428
|
-
.trim();
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
else {
|
|
432
|
-
releaseBody = generateReleaseMd(ver, commits, prevTag?.replace(/^v/, '') ?? '0.0.0')
|
|
433
|
-
.replace(/^## .*\n+/, '')
|
|
434
|
-
.replace(/\n---\n*$/, '')
|
|
435
|
-
.trim();
|
|
436
|
-
}
|
|
437
|
-
console.log('');
|
|
438
|
-
const releaseSpinner = new ScrambleProgress();
|
|
439
|
-
// Ensure tag exists on remote before creating GitHub Release
|
|
440
|
-
releaseSpinner.start(['connecting to remote...', 'pushing tag...', 'verifying remote refs...']);
|
|
441
|
-
try {
|
|
442
|
-
await execAsync(`git push origin ${tag} --no-verify`, true);
|
|
443
|
-
releaseSpinner.succeed(`Tag ${tag} pushed to remote`);
|
|
444
|
-
}
|
|
445
|
-
catch {
|
|
446
|
-
// Tag might already exist on remote — that's fine, continue
|
|
447
|
-
}
|
|
448
|
-
console.log('');
|
|
449
|
-
const createSpinner = new ScrambleProgress();
|
|
450
|
-
createSpinner.start([
|
|
451
|
-
'preparing release data...',
|
|
452
|
-
'creating github release...',
|
|
453
|
-
'confirming creation...',
|
|
454
|
-
]);
|
|
455
|
-
const os = await import('node:os');
|
|
456
|
-
const tempFile = `${os.tmpdir()}/geeto-sync-${Date.now()}.md`;
|
|
457
|
-
writeFileSync(tempFile, releaseBody, 'utf8');
|
|
458
|
-
try {
|
|
459
|
-
await execAsync(`gh release create ${tag} --title "${tag}" --notes-file "${tempFile}"`, true);
|
|
460
|
-
createSpinner.succeed(`Release ${tag} created`);
|
|
461
|
-
successCount++;
|
|
462
|
-
}
|
|
463
|
-
catch (error) {
|
|
464
|
-
const stderr = error.stderr?.trim();
|
|
465
|
-
createSpinner.fail(`Failed to create release for ${tag}`);
|
|
466
|
-
if (stderr)
|
|
467
|
-
log.error(` ${stderr.split('\n')[0]}`);
|
|
468
|
-
}
|
|
469
|
-
// Cleanup temp file
|
|
470
|
-
try {
|
|
471
|
-
const { unlinkSync } = await import('node:fs');
|
|
472
|
-
unlinkSync(tempFile);
|
|
473
|
-
}
|
|
474
|
-
catch {
|
|
475
|
-
/* ignore */
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
console.log('');
|
|
479
|
-
if (successCount === tagsToRelease.length) {
|
|
480
|
-
log.success(`All ${successCount} GitHub Releases created!`);
|
|
481
|
-
}
|
|
482
|
-
else {
|
|
483
|
-
log.warn(`${successCount}/${tagsToRelease.length} releases created`);
|
|
484
|
-
}
|
|
485
|
-
};
|
|
486
|
-
// ─── Delete GitHub Releases ───
|
|
487
|
-
const handleDeleteReleases = async () => {
|
|
488
|
-
// Check if gh CLI is available
|
|
489
|
-
try {
|
|
490
|
-
execSilent('gh --version');
|
|
491
|
-
}
|
|
492
|
-
catch {
|
|
493
|
-
log.error('GitHub CLI (gh) is not installed. Install it: https://cli.github.com');
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
console.log('');
|
|
497
|
-
const spinner = new ScrambleProgress();
|
|
498
|
-
spinner.start(['connecting to github...', 'fetching releases...', 'processing results...']);
|
|
499
|
-
const ghReleases = getExistingGithubReleases();
|
|
500
|
-
spinner.succeed(`Found ${ghReleases.length} GitHub releases`);
|
|
501
|
-
if (ghReleases.length === 0) {
|
|
502
|
-
console.log('');
|
|
503
|
-
log.info('No GitHub Releases to delete.');
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
console.log('');
|
|
507
|
-
const { multiSelect } = await import('../cli/menu.js');
|
|
508
|
-
const choices = ghReleases.map((t) => ({ label: t, value: t }));
|
|
509
|
-
const selected = await multiSelect('Select releases to delete:', choices);
|
|
510
|
-
if (selected.length === 0) {
|
|
511
|
-
log.info('No releases selected.');
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
console.log('');
|
|
515
|
-
const alsoDeleteTag = confirm('Also delete the associated git tags?');
|
|
516
|
-
console.log('');
|
|
517
|
-
const proceed = confirm(`Delete ${selected.length} GitHub Release(s)${alsoDeleteTag ? ' + tags' : ''}?`);
|
|
518
|
-
if (!proceed)
|
|
519
|
-
return;
|
|
520
|
-
let successCount = 0;
|
|
521
|
-
for (const release of selected) {
|
|
522
|
-
console.log('');
|
|
523
|
-
const releaseSpinner = new ScrambleProgress();
|
|
524
|
-
releaseSpinner.start(['connecting to github...', 'deleting release...', 'cleaning up...']);
|
|
525
|
-
try {
|
|
526
|
-
await execAsync(`gh release delete ${release} --yes`, true);
|
|
527
|
-
if (alsoDeleteTag) {
|
|
528
|
-
try {
|
|
529
|
-
await execAsync(`git tag -d ${release}`, true);
|
|
530
|
-
await execAsync(`git push origin --delete ${release} --no-verify`, true);
|
|
531
|
-
}
|
|
532
|
-
catch {
|
|
533
|
-
/* Tag deletion is best-effort */
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
releaseSpinner.succeed(`Release ${release} deleted${alsoDeleteTag ? ' + tag' : ''}`);
|
|
537
|
-
successCount++;
|
|
538
|
-
}
|
|
539
|
-
catch (error) {
|
|
540
|
-
const stderr = error.stderr?.trim();
|
|
541
|
-
releaseSpinner.fail(`Failed to delete ${release}`);
|
|
542
|
-
if (stderr)
|
|
543
|
-
log.error(` ${stderr.split('\n')[0]}`);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
console.log('');
|
|
547
|
-
if (successCount === selected.length) {
|
|
548
|
-
log.success(`All ${successCount} releases deleted!`);
|
|
549
|
-
}
|
|
550
|
-
else {
|
|
551
|
-
log.warn(`${successCount}/${selected.length} releases deleted`);
|
|
552
|
-
}
|
|
553
|
-
};
|
|
554
|
-
// ─── Recover missing tags ───
|
|
555
|
-
const handleRecoverTags = async () => {
|
|
556
|
-
const line = '─'.repeat(56);
|
|
557
|
-
console.log('');
|
|
558
|
-
const spinner = log.spinner();
|
|
559
|
-
spinner.start('Scanning release commits...');
|
|
560
|
-
// Find all release commits: "chore(release): vX.Y.Z"
|
|
561
|
-
let gitLog;
|
|
562
|
-
try {
|
|
563
|
-
gitLog = exec('git log --all --oneline --grep="^chore(release): v" --format="%H %s"', true);
|
|
564
|
-
}
|
|
565
|
-
catch {
|
|
566
|
-
spinner.fail('Failed to scan git log');
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
const releasePattern = /^([a-f0-9]+) chore\(release\): v(.+)$/;
|
|
570
|
-
const releaseCommits = [];
|
|
571
|
-
for (const logLine of gitLog.split('\n').filter(Boolean)) {
|
|
572
|
-
const match = logLine.match(releasePattern);
|
|
573
|
-
if (match?.[1] && match[2]) {
|
|
574
|
-
releaseCommits.push({
|
|
575
|
-
hash: match[1],
|
|
576
|
-
version: match[2],
|
|
577
|
-
tag: `v${match[2]}`,
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
if (releaseCommits.length === 0) {
|
|
582
|
-
spinner.fail('No release commits found');
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
// Compare with existing tags
|
|
586
|
-
const existingTags = new Set(getExistingTags());
|
|
587
|
-
const missingTags = releaseCommits.filter((rc) => !existingTags.has(rc.tag));
|
|
588
|
-
spinner.succeed(`Found ${releaseCommits.length} release commits, ${existingTags.size} tags`);
|
|
589
|
-
if (missingTags.length === 0) {
|
|
590
|
-
console.log('');
|
|
591
|
-
log.success('All release commits have matching tags! Nothing to recover.');
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
// Show missing tags
|
|
595
|
-
console.log('');
|
|
596
|
-
log.info(`${colors.bright}${missingTags.length}${colors.reset} tags missing (release commit exists but no tag):`);
|
|
597
|
-
for (const mt of missingTags) {
|
|
598
|
-
console.log(` ${colors.yellow}${mt.tag}${colors.reset} ${colors.gray}← ${mt.hash.slice(0, 7)}${colors.reset}`);
|
|
599
|
-
}
|
|
600
|
-
console.log('');
|
|
601
|
-
const action = await select('What do you want to do?', [
|
|
602
|
-
{ label: 'Recover all missing tags', value: 'all' },
|
|
603
|
-
{ label: 'Select which tags to recover', value: 'select' },
|
|
604
|
-
{ label: 'Cancel', value: 'cancel' },
|
|
605
|
-
]);
|
|
606
|
-
if (action === 'cancel')
|
|
607
|
-
return;
|
|
608
|
-
let tagsToRecover = missingTags;
|
|
609
|
-
if (action === 'select') {
|
|
610
|
-
const { multiSelect } = await import('../cli/menu.js');
|
|
611
|
-
const choices = missingTags.map((mt) => ({
|
|
612
|
-
label: `${mt.tag} (${mt.hash.slice(0, 7)})`,
|
|
613
|
-
value: mt.tag,
|
|
614
|
-
}));
|
|
615
|
-
const selected = await multiSelect('Select tags to recover:', choices);
|
|
616
|
-
if (selected.length === 0) {
|
|
617
|
-
log.info('No tags selected.');
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
tagsToRecover = missingTags.filter((mt) => selected.includes(mt.tag));
|
|
621
|
-
}
|
|
622
|
-
// Preview
|
|
623
|
-
console.log('');
|
|
624
|
-
console.log(`${colors.cyan}┌${line}┐${colors.reset}`);
|
|
625
|
-
console.log(`${colors.cyan}│${colors.reset} ${colors.bright}Recovery Plan${colors.reset}`);
|
|
626
|
-
console.log(`${colors.cyan}├${line}┤${colors.reset}`);
|
|
627
|
-
for (const mt of tagsToRecover) {
|
|
628
|
-
console.log(`${colors.cyan}│${colors.reset} ${colors.green}+${colors.reset} ${colors.yellow}${mt.tag}${colors.reset} → commit ${colors.gray}${mt.hash.slice(0, 7)}${colors.reset}`);
|
|
629
|
-
}
|
|
630
|
-
console.log(`${colors.cyan}└${line}┘${colors.reset}`);
|
|
631
|
-
console.log('');
|
|
632
|
-
const proceed = confirm(`Create ${tagsToRecover.length} tags?`);
|
|
633
|
-
if (!proceed)
|
|
634
|
-
return;
|
|
635
|
-
let successCount = 0;
|
|
636
|
-
for (const mt of tagsToRecover) {
|
|
637
|
-
console.log('');
|
|
638
|
-
const tagSpinner = log.spinner();
|
|
639
|
-
tagSpinner.start(`Creating tag ${colors.yellow}${mt.tag}${colors.reset}...`);
|
|
640
|
-
try {
|
|
641
|
-
exec(`git tag -a ${mt.tag} ${mt.hash} -m "Release ${mt.tag}"`, true);
|
|
642
|
-
tagSpinner.succeed(`Tag ${mt.tag} created`);
|
|
643
|
-
successCount++;
|
|
644
|
-
}
|
|
645
|
-
catch (error) {
|
|
646
|
-
const errMsg = error instanceof Error ? error.message : String(error);
|
|
647
|
-
tagSpinner.fail(`Failed to create ${mt.tag}`);
|
|
648
|
-
log.error(` ${errMsg.split('\n')[0]}`);
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
console.log('');
|
|
652
|
-
if (successCount === tagsToRecover.length) {
|
|
653
|
-
log.success(`All ${successCount} tags recovered!`);
|
|
654
|
-
}
|
|
655
|
-
else {
|
|
656
|
-
log.warn(`${successCount}/${tagsToRecover.length} tags recovered`);
|
|
657
|
-
}
|
|
658
|
-
// Offer to push tags to remote
|
|
659
|
-
if (successCount > 0) {
|
|
660
|
-
console.log('');
|
|
661
|
-
const pushTags = confirm('Push recovered tags to remote?');
|
|
662
|
-
if (pushTags) {
|
|
663
|
-
console.log('');
|
|
664
|
-
const pushSpinner = new ScrambleProgress();
|
|
665
|
-
pushSpinner.start(['connecting to remote...', 'pushing tags...', 'verifying remote refs...']);
|
|
666
|
-
try {
|
|
667
|
-
await execAsync('git push --tags --no-verify', true);
|
|
668
|
-
pushSpinner.succeed('Tags pushed to remote');
|
|
669
|
-
}
|
|
670
|
-
catch (error) {
|
|
671
|
-
const stderr = error.stderr?.trim();
|
|
672
|
-
pushSpinner.fail('Failed to push tags');
|
|
673
|
-
if (stderr)
|
|
674
|
-
log.error(` ${stderr.split('\n')[0]}`);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
};
|
|
679
22
|
// ─── Main handler ───
|
|
680
23
|
export const handleRelease = async () => {
|
|
681
24
|
log.banner();
|
|
682
25
|
log.step(`${colors.cyan}Release / Tag Manager${colors.reset}\n`);
|
|
683
26
|
// Main menu: create new release, sync, or manage releases
|
|
27
|
+
// Detect platform for CLI commands
|
|
28
|
+
const platform = detectPlatformFromRemote();
|
|
29
|
+
const cli = platform ? getPlatformCLI(platform) : 'gh';
|
|
30
|
+
const platformName = platform === 'gitlab' ? 'GitLab' : 'GitHub';
|
|
684
31
|
const mode = await select('What do you want to do?', [
|
|
685
32
|
{ label: 'Create a new release', value: 'create' },
|
|
686
|
-
{ label:
|
|
33
|
+
{ label: `Sync ${platformName} Releases for existing tags`, value: 'sync' },
|
|
34
|
+
{ label: 'Merge Releases (consolidate release notes)', value: 'merge' },
|
|
687
35
|
{ label: 'Recover missing tags from release commits', value: 'recover' },
|
|
688
|
-
{ label:
|
|
36
|
+
{ label: `Delete ${platformName} Releases`, value: 'delete' },
|
|
689
37
|
]);
|
|
690
38
|
if (mode === 'sync') {
|
|
691
39
|
await handleSyncReleases();
|
|
692
40
|
return;
|
|
693
41
|
}
|
|
42
|
+
if (mode === 'merge') {
|
|
43
|
+
await handleMergeReleases();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
694
46
|
if (mode === 'recover') {
|
|
695
47
|
await handleRecoverTags();
|
|
696
48
|
return;
|
|
@@ -708,7 +60,7 @@ export const handleRelease = async () => {
|
|
|
708
60
|
const tags = getExistingTags();
|
|
709
61
|
const lastTag = tags[0] ?? '';
|
|
710
62
|
// Show current state
|
|
711
|
-
const line = '─'.repeat(
|
|
63
|
+
const line = '─'.repeat(BOX_W);
|
|
712
64
|
console.log(`${colors.cyan}┌${line}┐${colors.reset}`);
|
|
713
65
|
console.log(`${colors.cyan}│${colors.reset} ${colors.bright}Current version: ${colors.yellow}v${currentVersion}${colors.reset}`);
|
|
714
66
|
if (lastTag) {
|
|
@@ -731,37 +83,70 @@ export const handleRelease = async () => {
|
|
|
731
83
|
}
|
|
732
84
|
// Version bump selection
|
|
733
85
|
console.log('');
|
|
734
|
-
const { major, minor, patch } = semver;
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
{
|
|
745
|
-
label: `
|
|
746
|
-
value: '
|
|
747
|
-
},
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
86
|
+
const { major, minor, patch, prerelease } = semver;
|
|
87
|
+
const nextPatch = `${major}.${minor}.${patch + 1}`;
|
|
88
|
+
// Padded label builder for aligned columns
|
|
89
|
+
const vpad = (name, ver, desc) => `${name.padEnd(12)}${colors.gray}${ver.padEnd(20)}${colors.reset}${desc}`;
|
|
90
|
+
// Build dynamic menu based on whether current version is a prerelease
|
|
91
|
+
const bumpOptions = [];
|
|
92
|
+
if (prerelease) {
|
|
93
|
+
const [preLabel] = prerelease.split('.');
|
|
94
|
+
const bumped = bumpPrerelease(semver);
|
|
95
|
+
const stable = promoteToStable(semver);
|
|
96
|
+
bumpOptions.push({
|
|
97
|
+
label: vpad(`Next ${preLabel}`, formatSemver(bumped), 'bump prerelease'),
|
|
98
|
+
value: 'pre-bump',
|
|
99
|
+
}, {
|
|
100
|
+
label: vpad('Stable', formatSemver(stable), 'promote to stable'),
|
|
101
|
+
value: 'promote',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
bumpOptions.push({
|
|
105
|
+
label: vpad('Patch', nextPatch, 'bug fixes'),
|
|
106
|
+
value: 'patch',
|
|
107
|
+
}, {
|
|
108
|
+
label: vpad('Minor', `${major}.${minor + 1}.0`, 'new features'),
|
|
109
|
+
value: 'minor',
|
|
110
|
+
}, {
|
|
111
|
+
label: vpad('Major', `${major + 1}.0.0`, 'breaking changes'),
|
|
112
|
+
value: 'major',
|
|
113
|
+
});
|
|
114
|
+
if (!prerelease) {
|
|
115
|
+
const nextMinor = `${major}.${minor + 1}.0`;
|
|
116
|
+
bumpOptions.push({
|
|
117
|
+
label: vpad('Alpha', `${nextMinor}-alpha.1`, 'early development'),
|
|
118
|
+
value: 'alpha',
|
|
119
|
+
}, {
|
|
120
|
+
label: vpad('Beta', `${nextMinor}-beta.1`, 'feature testing'),
|
|
121
|
+
value: 'beta',
|
|
122
|
+
}, {
|
|
123
|
+
label: vpad('RC', `${nextMinor}-rc.1`, 'release candidate'),
|
|
124
|
+
value: 'rc',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
bumpOptions.push({ label: 'Cancel', value: 'cancel' });
|
|
128
|
+
const bumpType = await select('Version bump:', bumpOptions);
|
|
751
129
|
if (bumpType === 'cancel')
|
|
752
130
|
return;
|
|
753
131
|
let newVersion;
|
|
132
|
+
let isPreVersion = false;
|
|
754
133
|
switch (bumpType) {
|
|
755
|
-
case '
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
134
|
+
case 'pre-bump': {
|
|
135
|
+
const bumped = bumpPrerelease(semver);
|
|
136
|
+
newVersion = formatSemver(bumped);
|
|
137
|
+
isPreVersion = true;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case 'promote': {
|
|
141
|
+
const stable = promoteToStable(semver);
|
|
142
|
+
newVersion = formatSemver(stable);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case 'alpha':
|
|
146
|
+
case 'beta':
|
|
147
|
+
case 'rc': {
|
|
148
|
+
newVersion = `${major}.${minor + 1}.0-${bumpType}.1`;
|
|
149
|
+
isPreVersion = true;
|
|
765
150
|
break;
|
|
766
151
|
}
|
|
767
152
|
case 'major': {
|
|
@@ -875,9 +260,7 @@ export const handleRelease = async () => {
|
|
|
875
260
|
const spinner = new ScrambleProgress();
|
|
876
261
|
const modelDisplay = getModelValue(copilotModel ?? openrouterModel ?? geminiModel ?? '');
|
|
877
262
|
spinner.start([
|
|
878
|
-
|
|
879
|
-
`generating notes with ${getAIProviderShortName(aiProvider)}${modelDisplay ? ` (${modelDisplay})` : ''}...`,
|
|
880
|
-
'processing results...',
|
|
263
|
+
`Generating release notes with ${getAIProviderShortName(aiProvider)}${modelDisplay ? ` (${modelDisplay})` : ''}`,
|
|
881
264
|
]);
|
|
882
265
|
const result = await generateReleaseNotesWithProvider(aiProvider, commitList, language, correction, copilotModel, openrouterModel, geminiModel);
|
|
883
266
|
spinner.succeed('Release notes generated');
|
|
@@ -888,13 +271,13 @@ export const handleRelease = async () => {
|
|
|
888
271
|
}
|
|
889
272
|
aiReleaseNotes = result;
|
|
890
273
|
// Preview
|
|
891
|
-
console.log(`${colors.cyan}┌${'─'.repeat(
|
|
274
|
+
console.log(`${colors.cyan}┌${'─'.repeat(BOX_W)}┐${colors.reset}`);
|
|
892
275
|
console.log(`${colors.cyan}│${colors.reset} ${colors.bright}Release Notes Preview${colors.reset}`);
|
|
893
|
-
console.log(`${colors.cyan}├${'─'.repeat(
|
|
276
|
+
console.log(`${colors.cyan}├${'─'.repeat(BOX_W)}┤${colors.reset}`);
|
|
894
277
|
for (const line of aiReleaseNotes.split('\n')) {
|
|
895
278
|
console.log(`${colors.cyan}│${colors.reset} ${line}`);
|
|
896
279
|
}
|
|
897
|
-
console.log(`${colors.cyan}└${'─'.repeat(
|
|
280
|
+
console.log(`${colors.cyan}└${'─'.repeat(BOX_W)}┘${colors.reset}`);
|
|
898
281
|
console.log('');
|
|
899
282
|
const action = await select('Accept these release notes?', [
|
|
900
283
|
{ label: 'Yes, use it', value: 'accept' },
|
|
@@ -1110,13 +493,7 @@ export const handleRelease = async () => {
|
|
|
1110
493
|
if (pushChoice === 'both' || pushChoice === 'commit') {
|
|
1111
494
|
console.log('');
|
|
1112
495
|
const pushProgress = new ScrambleProgress();
|
|
1113
|
-
pushProgress.start([
|
|
1114
|
-
'initializing push...',
|
|
1115
|
-
'collecting objects...',
|
|
1116
|
-
{ text: 'compressing deltas', countTo: 100, suffix: '%' },
|
|
1117
|
-
'uploading to remote...',
|
|
1118
|
-
'verifying remote refs...',
|
|
1119
|
-
]);
|
|
496
|
+
pushProgress.start(['Pushing release to remote']);
|
|
1120
497
|
try {
|
|
1121
498
|
await execAsync(`git push`, true);
|
|
1122
499
|
if (pushChoice === 'both') {
|
|
@@ -1131,12 +508,12 @@ export const handleRelease = async () => {
|
|
|
1131
508
|
log.error('Failed to push');
|
|
1132
509
|
}
|
|
1133
510
|
}
|
|
1134
|
-
// 6. Create
|
|
1135
|
-
let
|
|
511
|
+
// 6. Create Release (if tag was pushed and platform CLI is available)
|
|
512
|
+
let releaseCreated = false;
|
|
1136
513
|
if (pushChoice === 'both') {
|
|
1137
514
|
try {
|
|
1138
|
-
execSilent(
|
|
1139
|
-
//
|
|
515
|
+
execSilent(`${cli} --version`);
|
|
516
|
+
// Platform CLI is available — create a Release
|
|
1140
517
|
// Build release body from AI notes or template
|
|
1141
518
|
const releaseBody = aiReleaseNotes
|
|
1142
519
|
? normalizeReleaseMarkdown(aiReleaseNotes)
|
|
@@ -1149,19 +526,16 @@ export const handleRelease = async () => {
|
|
|
1149
526
|
const tempFile = `${os.tmpdir()}/geeto-release-${Date.now()}.md`;
|
|
1150
527
|
writeFileSync(tempFile, releaseBody, 'utf8');
|
|
1151
528
|
const releaseSpinner = new ScrambleProgress();
|
|
1152
|
-
releaseSpinner.start([
|
|
1153
|
-
'preparing release data...',
|
|
1154
|
-
'creating github release...',
|
|
1155
|
-
'confirming creation...',
|
|
1156
|
-
]);
|
|
529
|
+
releaseSpinner.start([`Creating ${platformName} release`]);
|
|
1157
530
|
try {
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
531
|
+
const preFlag = isPreVersion ? ' --prerelease' : '';
|
|
532
|
+
await execAsync(`${cli} release create v${newVersion} --title "v${newVersion}" --notes-file "${tempFile}"${preFlag}`, true);
|
|
533
|
+
releaseSpinner.succeed(`${platformName} Release created`);
|
|
534
|
+
releaseCreated = true;
|
|
1161
535
|
}
|
|
1162
536
|
catch (error) {
|
|
1163
537
|
const stderr = error.stderr?.trim();
|
|
1164
|
-
releaseSpinner.fail(
|
|
538
|
+
releaseSpinner.fail(`Failed to create ${platformName} Release`);
|
|
1165
539
|
if (stderr)
|
|
1166
540
|
log.error(` ${stderr.split('\n')[0]}`);
|
|
1167
541
|
}
|
|
@@ -1175,7 +549,7 @@ export const handleRelease = async () => {
|
|
|
1175
549
|
}
|
|
1176
550
|
}
|
|
1177
551
|
catch {
|
|
1178
|
-
//
|
|
552
|
+
// Platform CLI not available — skip silently
|
|
1179
553
|
}
|
|
1180
554
|
}
|
|
1181
555
|
// Summary
|
|
@@ -1187,8 +561,8 @@ export const handleRelease = async () => {
|
|
|
1187
561
|
console.log(`${colors.cyan}│${colors.reset} ${colors.green}✓${colors.reset} RELEASE.MD generated ${colors.gray}(user-facing)${colors.reset}`);
|
|
1188
562
|
console.log(`${colors.cyan}│${colors.reset} ${colors.green}✓${colors.reset} CHANGELOG.md updated ${colors.gray}(developer-facing)${colors.reset}`);
|
|
1189
563
|
console.log(`${colors.cyan}│${colors.reset} ${colors.green}✓${colors.reset} Tag v${newVersion} created`);
|
|
1190
|
-
if (
|
|
1191
|
-
console.log(`${colors.cyan}│${colors.reset} ${colors.green}✓${colors.reset}
|
|
564
|
+
if (releaseCreated) {
|
|
565
|
+
console.log(`${colors.cyan}│${colors.reset} ${colors.green}✓${colors.reset} ${platformName} Release published`);
|
|
1192
566
|
}
|
|
1193
567
|
console.log(`${colors.cyan}└${line}┘${colors.reset}`);
|
|
1194
568
|
};
|