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