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.
Files changed (198) hide show
  1. package/README.md +23 -9
  2. package/lib/api/copilot-adapter.d.ts +14 -5
  3. package/lib/api/copilot-adapter.d.ts.map +1 -1
  4. package/lib/api/copilot-adapter.js +15 -21
  5. package/lib/api/copilot-adapter.js.map +1 -1
  6. package/lib/api/copilot-sdk.d.ts +3 -16
  7. package/lib/api/copilot-sdk.d.ts.map +1 -1
  8. package/lib/api/copilot-sdk.js +166 -456
  9. package/lib/api/copilot-sdk.js.map +1 -1
  10. package/lib/api/copilot.d.ts +3 -4
  11. package/lib/api/copilot.d.ts.map +1 -1
  12. package/lib/api/copilot.js +28 -28
  13. package/lib/api/copilot.js.map +1 -1
  14. package/lib/api/gemini-sdk.d.ts.map +1 -1
  15. package/lib/api/gemini-sdk.js +11 -77
  16. package/lib/api/gemini-sdk.js.map +1 -1
  17. package/lib/api/gemini.d.ts +2 -2
  18. package/lib/api/gemini.d.ts.map +1 -1
  19. package/lib/api/gemini.js +24 -19
  20. package/lib/api/gemini.js.map +1 -1
  21. package/lib/api/gitlab.d.ts +80 -0
  22. package/lib/api/gitlab.d.ts.map +1 -0
  23. package/lib/api/gitlab.js +192 -0
  24. package/lib/api/gitlab.js.map +1 -0
  25. package/lib/api/openrouter-sdk.d.ts.map +1 -1
  26. package/lib/api/openrouter-sdk.js +11 -76
  27. package/lib/api/openrouter-sdk.js.map +1 -1
  28. package/lib/api/openrouter.d.ts.map +1 -1
  29. package/lib/api/openrouter.js +2 -16
  30. package/lib/api/openrouter.js.map +1 -1
  31. package/lib/api/platform.d.ts +78 -0
  32. package/lib/api/platform.d.ts.map +1 -0
  33. package/lib/api/platform.js +218 -0
  34. package/lib/api/platform.js.map +1 -0
  35. package/lib/cli/input.d.ts +2 -2
  36. package/lib/cli/input.d.ts.map +1 -1
  37. package/lib/cli/input.js +23 -27
  38. package/lib/cli/input.js.map +1 -1
  39. package/lib/cli/menu.d.ts +1 -1
  40. package/lib/cli/menu.d.ts.map +1 -1
  41. package/lib/cli/menu.js +123 -100
  42. package/lib/cli/menu.js.map +1 -1
  43. package/lib/core/copilot-setup.d.ts +8 -7
  44. package/lib/core/copilot-setup.d.ts.map +1 -1
  45. package/lib/core/copilot-setup.js +59 -230
  46. package/lib/core/copilot-setup.js.map +1 -1
  47. package/lib/core/gemini-setup.js +7 -7
  48. package/lib/core/gemini-setup.js.map +1 -1
  49. package/lib/core/gitlab-setup.d.ts +5 -0
  50. package/lib/core/gitlab-setup.d.ts.map +1 -0
  51. package/lib/core/gitlab-setup.js +85 -0
  52. package/lib/core/gitlab-setup.js.map +1 -0
  53. package/lib/core/openrouter-setup.d.ts.map +1 -1
  54. package/lib/core/openrouter-setup.js +17 -0
  55. package/lib/core/openrouter-setup.js.map +1 -1
  56. package/lib/index.js +501 -704
  57. package/lib/index.js.map +1 -1
  58. package/lib/types/index.d.ts +10 -6
  59. package/lib/types/index.d.ts.map +1 -1
  60. package/lib/utils/ai-provider-helpers.d.ts +5 -0
  61. package/lib/utils/ai-provider-helpers.d.ts.map +1 -0
  62. package/lib/utils/ai-provider-helpers.js +23 -0
  63. package/lib/utils/ai-provider-helpers.js.map +1 -0
  64. package/lib/utils/ai-text.d.ts +23 -0
  65. package/lib/utils/ai-text.d.ts.map +1 -0
  66. package/lib/utils/ai-text.js +57 -0
  67. package/lib/utils/ai-text.js.map +1 -0
  68. package/lib/utils/ai-workflow.d.ts +18 -0
  69. package/lib/utils/ai-workflow.d.ts.map +1 -0
  70. package/lib/utils/ai-workflow.js +66 -0
  71. package/lib/utils/ai-workflow.js.map +1 -0
  72. package/lib/utils/branch-naming.d.ts.map +1 -1
  73. package/lib/utils/branch-naming.js +1 -3
  74. package/lib/utils/branch-naming.js.map +1 -1
  75. package/lib/utils/config.d.ts +13 -1
  76. package/lib/utils/config.d.ts.map +1 -1
  77. package/lib/utils/config.js +38 -1
  78. package/lib/utils/config.js.map +1 -1
  79. package/lib/utils/display.d.ts.map +1 -1
  80. package/lib/utils/display.js +4 -3
  81. package/lib/utils/display.js.map +1 -1
  82. package/lib/utils/git-ai.js +13 -13
  83. package/lib/utils/git-ai.js.map +1 -1
  84. package/lib/utils/git-errors.d.ts.map +1 -1
  85. package/lib/utils/git-errors.js +2 -6
  86. package/lib/utils/git-errors.js.map +1 -1
  87. package/lib/utils/git.d.ts.map +1 -1
  88. package/lib/utils/git.js +5 -0
  89. package/lib/utils/git.js.map +1 -1
  90. package/lib/utils/github-helpers.d.ts +33 -0
  91. package/lib/utils/github-helpers.d.ts.map +1 -0
  92. package/lib/utils/github-helpers.js +101 -0
  93. package/lib/utils/github-helpers.js.map +1 -0
  94. package/lib/utils/prompt-loader.d.ts +9 -0
  95. package/lib/utils/prompt-loader.d.ts.map +1 -0
  96. package/lib/utils/prompt-loader.js +42 -0
  97. package/lib/utils/prompt-loader.js.map +1 -0
  98. package/lib/utils/prompts-embedded.d.ts +2 -0
  99. package/lib/utils/prompts-embedded.d.ts.map +1 -0
  100. package/lib/utils/prompts-embedded.js +255 -0
  101. package/lib/utils/prompts-embedded.js.map +1 -0
  102. package/lib/utils/scramble.d.ts +9 -86
  103. package/lib/utils/scramble.d.ts.map +1 -1
  104. package/lib/utils/scramble.js +27 -279
  105. package/lib/utils/scramble.js.map +1 -1
  106. package/lib/version.d.ts +1 -1
  107. package/lib/version.js +1 -1
  108. package/lib/workflows/alias.d.ts.map +1 -1
  109. package/lib/workflows/alias.js +1 -0
  110. package/lib/workflows/alias.js.map +1 -1
  111. package/lib/workflows/amend.d.ts.map +1 -1
  112. package/lib/workflows/amend.js +1 -5
  113. package/lib/workflows/amend.js.map +1 -1
  114. package/lib/workflows/branch-helpers.d.ts.map +1 -1
  115. package/lib/workflows/branch-helpers.js +0 -1
  116. package/lib/workflows/branch-helpers.js.map +1 -1
  117. package/lib/workflows/commit.d.ts.map +1 -1
  118. package/lib/workflows/commit.js +160 -187
  119. package/lib/workflows/commit.js.map +1 -1
  120. package/lib/workflows/doctor.d.ts +7 -0
  121. package/lib/workflows/doctor.d.ts.map +1 -0
  122. package/lib/workflows/doctor.js +284 -0
  123. package/lib/workflows/doctor.js.map +1 -0
  124. package/lib/workflows/issue.d.ts +1 -1
  125. package/lib/workflows/issue.d.ts.map +1 -1
  126. package/lib/workflows/issue.js +28 -115
  127. package/lib/workflows/issue.js.map +1 -1
  128. package/lib/workflows/main-helpers.d.ts +34 -0
  129. package/lib/workflows/main-helpers.d.ts.map +1 -0
  130. package/lib/workflows/main-helpers.js +346 -0
  131. package/lib/workflows/main-helpers.js.map +1 -0
  132. package/lib/workflows/main-steps.d.ts.map +1 -1
  133. package/lib/workflows/main-steps.js +9 -134
  134. package/lib/workflows/main-steps.js.map +1 -1
  135. package/lib/workflows/main.d.ts +2 -6
  136. package/lib/workflows/main.d.ts.map +1 -1
  137. package/lib/workflows/main.js +44 -381
  138. package/lib/workflows/main.js.map +1 -1
  139. package/lib/workflows/pr.d.ts +2 -2
  140. package/lib/workflows/pr.d.ts.map +1 -1
  141. package/lib/workflows/pr.js +49 -137
  142. package/lib/workflows/pr.js.map +1 -1
  143. package/lib/workflows/prune.d.ts.map +1 -1
  144. package/lib/workflows/prune.js +2 -10
  145. package/lib/workflows/prune.js.map +1 -1
  146. package/lib/workflows/pull.d.ts.map +1 -1
  147. package/lib/workflows/pull.js +2 -24
  148. package/lib/workflows/pull.js.map +1 -1
  149. package/lib/workflows/release-merge.d.ts +12 -0
  150. package/lib/workflows/release-merge.d.ts.map +1 -0
  151. package/lib/workflows/release-merge.js +569 -0
  152. package/lib/workflows/release-merge.js.map +1 -0
  153. package/lib/workflows/release-notes.d.ts +13 -0
  154. package/lib/workflows/release-notes.d.ts.map +1 -0
  155. package/lib/workflows/release-notes.js +141 -0
  156. package/lib/workflows/release-notes.js.map +1 -0
  157. package/lib/workflows/release-recover.d.ts +5 -0
  158. package/lib/workflows/release-recover.d.ts.map +1 -0
  159. package/lib/workflows/release-recover.js +137 -0
  160. package/lib/workflows/release-recover.js.map +1 -0
  161. package/lib/workflows/release-sync.d.ts +7 -0
  162. package/lib/workflows/release-sync.d.ts.map +1 -0
  163. package/lib/workflows/release-sync.js +321 -0
  164. package/lib/workflows/release-sync.js.map +1 -0
  165. package/lib/workflows/release-utils.d.ts +36 -0
  166. package/lib/workflows/release-utils.d.ts.map +1 -0
  167. package/lib/workflows/release-utils.js +150 -0
  168. package/lib/workflows/release-utils.js.map +1 -0
  169. package/lib/workflows/release.d.ts.map +1 -1
  170. package/lib/workflows/release.js +92 -719
  171. package/lib/workflows/release.js.map +1 -1
  172. package/lib/workflows/repo-settings.d.ts +2 -2
  173. package/lib/workflows/repo-settings.d.ts.map +1 -1
  174. package/lib/workflows/repo-settings.js +33 -24
  175. package/lib/workflows/repo-settings.js.map +1 -1
  176. package/lib/workflows/reword.d.ts.map +1 -1
  177. package/lib/workflows/reword.js +154 -151
  178. package/lib/workflows/reword.js.map +1 -1
  179. package/lib/workflows/security-gate.d.ts.map +1 -1
  180. package/lib/workflows/security-gate.js +15 -75
  181. package/lib/workflows/security-gate.js.map +1 -1
  182. package/lib/workflows/settings.d.ts +2 -1
  183. package/lib/workflows/settings.d.ts.map +1 -1
  184. package/lib/workflows/settings.js +289 -19
  185. package/lib/workflows/settings.js.map +1 -1
  186. package/lib/workflows/trello-menu.d.ts +2 -5
  187. package/lib/workflows/trello-menu.d.ts.map +1 -1
  188. package/lib/workflows/trello-menu.js +67 -228
  189. package/lib/workflows/trello-menu.js.map +1 -1
  190. package/package.json +3 -6
  191. package/prompts/branch-name-prompt.md +4 -0
  192. package/prompts/commit-message-prompt.md +12 -0
  193. package/prompts/issue-prompt.md +19 -0
  194. package/prompts/issue-review.with-context.prompt.yml +77 -0
  195. package/prompts/pr-prompt.md +14 -0
  196. package/prompts/release-notes-prompt.md +35 -0
  197. package/prompts/repo-description-prompt.md +1 -0
  198. package/prompts/security-gate-prompt.md +80 -0
@@ -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: 'Sync GitHub Releases for existing tags', value: 'sync' },
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: 'Delete GitHub Releases', value: 'delete' },
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 bumpType = await select('Version bump:', [
737
- {
738
- label: `Patch ${colors.gray}${major}.${minor}.${patch + 1}${colors.reset} — bug fixes`,
739
- value: 'patch',
740
- },
741
- {
742
- label: `Minor ${colors.gray}${major}.${minor + 1}.0${colors.reset} — new features`,
743
- value: 'minor',
744
- },
745
- {
746
- label: `Major ${colors.gray}${major + 1}.0.0${colors.reset} breaking changes`,
747
- value: 'major',
748
- },
749
- { label: 'Custom enter version manually', value: 'custom' },
750
- { label: 'Cancel', value: 'cancel' },
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 'custom': {
757
- if (process.stdin.isTTY) {
758
- process.stdin.setRawMode(false);
759
- }
760
- const input = askQuestion('Enter version (e.g. 1.2.3): ').trim();
761
- if (!parseSemver(input)) {
762
- log.error('Invalid semver format.');
763
- return;
764
- }
765
- newVersion = input;
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
- 'preparing release context...',
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 GitHub Release (if tag was pushed and gh CLI is available)
1136
- let ghReleaseCreated = false;
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('gh --version');
1140
- // gh CLI is available — create a GitHub Release
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
- await execAsync(`gh release create v${newVersion} --title "v${newVersion}" --notes-file "${tempFile}"`, true);
1160
- releaseSpinner.succeed('GitHub Release created');
1161
- ghReleaseCreated = true;
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('Failed to create GitHub Release');
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
- // gh CLI not available — skip silently
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 (ghReleaseCreated) {
1192
- console.log(`${colors.cyan}│${colors.reset} ${colors.green}✓${colors.reset} GitHub Release published`);
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
  };