geeto 0.6.5 → 0.9.0

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