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
@@ -14,9 +14,10 @@ import { multiSelect, select } from '../cli/menu.js';
14
14
  import { colors } from '../utils/colors.js';
15
15
  import { extractCommitBody, extractCommitTitle, formatCommitBody, normalizeAIOutput, } from '../utils/commit-helpers.js';
16
16
  import { DEFAULT_GEMINI_MODEL } from '../utils/config.js';
17
+ import { BOX_W } from '../utils/display.js';
17
18
  import { isDryRun, logDryRun } from '../utils/dry-run.js';
18
19
  import { execAsync, execSilent } from '../utils/exec.js';
19
- import { chooseModelForProvider, getAIProviderShortName, getModelValue, interactiveAIFallback, isContextLimitFailure, isTransientAIFailure, } from '../utils/git-ai.js';
20
+ import { chooseModelForProvider, generateCommitMessageWithProvider, getAIProviderShortName, getModelValue, interactiveAIFallback, isContextLimitFailure, isTransientAIFailure, } from '../utils/git-ai.js';
20
21
  import { getCurrentBranch } from '../utils/git.js';
21
22
  import { log } from '../utils/logging.js';
22
23
  import { ScrambleProgress } from '../utils/scramble.js';
@@ -255,28 +256,97 @@ const getRepoUrl = () => {
255
256
  };
256
257
  /** Wrap text in OSC 8 hyperlink (clickable in supported terminals). */
257
258
  const hyperlink = (url, text) => `\u001B]8;;${url}\u0007${text}\u001B]8;;\u0007`;
258
- // ── Main handler ───────────────────────────────────────────────────
259
- export const handleReword = async () => {
260
- log.banner();
261
- log.step(`${colors.cyan}Edit Commit Messages${colors.reset}\n`);
262
- const branch = getCurrentBranch();
263
- const isProtected = ['main', 'master', 'develop', 'production', 'staging'].includes(branch);
264
- console.log(` ${colors.gray}Branch: ${branch}${colors.reset}`);
265
- if (isProtected) {
266
- console.log(` ${colors.red}⚠${colors.reset} ${colors.bright}Protected branch${colors.reset} rewriting history here affects all collaborators.`);
267
- }
268
- // Check working tree
269
- if (!isWorkingTreeClean()) {
270
- log.error('Working tree has uncommitted changes.');
271
- log.info('Please commit or stash changes before rewording.');
272
- return;
273
- }
274
- // Load commits
275
- const commits = getRecentCommits(30);
276
- if (commits.length === 0) {
277
- log.warn('No commits found.');
278
- return;
259
+ // ── Small helpers ──────────────────────────────────────────────────
260
+ /** Resolve current AI provider and model from persisted state. */
261
+ const resolveAIProvider = (state) => {
262
+ const provider = (state.aiProvider === 'manual' ? undefined : state.aiProvider) ?? 'gemini';
263
+ if (provider === 'copilot')
264
+ return { provider, model: state.copilotModel };
265
+ if (provider === 'openrouter')
266
+ return { provider, model: state.openrouterModel };
267
+ return { provider, model: state.geminiModel ?? DEFAULT_GEMINI_MODEL };
268
+ };
269
+ /** Normalize AI output and extract title + body into final commit message. */
270
+ const buildFinalMessage = (raw) => {
271
+ const normalized = normalizeAIOutput(raw);
272
+ const extracted = extractCommitTitle(normalized);
273
+ let title;
274
+ let bodyText = null;
275
+ if (extracted) {
276
+ title = extracted;
277
+ bodyText = extractCommitBody(normalized, title);
278
+ if (bodyText)
279
+ bodyText = formatCommitBody(bodyText);
280
+ }
281
+ else {
282
+ const first = normalized.split('\n').find((l) => l.trim());
283
+ title = first?.trim() ?? normalized;
284
+ }
285
+ return bodyText ? `${title}\n\n${bodyText}` : title;
286
+ };
287
+ /** Regenerate commit message directly via the current provider (with retry). */
288
+ const regenerateDirect = async (state, diff, correction) => {
289
+ const maxAttempts = 2;
290
+ let aiResult = null;
291
+ for (let attempt = 0; attempt < maxAttempts && !aiResult; attempt++) {
292
+ let modelName = '';
293
+ if (state.aiProvider === 'copilot' && state.copilotModel) {
294
+ modelName = state.copilotModel;
295
+ }
296
+ else if (state.aiProvider === 'openrouter' && state.openrouterModel) {
297
+ modelName = state.openrouterModel;
298
+ }
299
+ else if (state.aiProvider === 'gemini') {
300
+ modelName = state.geminiModel ?? DEFAULT_GEMINI_MODEL;
301
+ }
302
+ if (correction)
303
+ console.log('');
304
+ const sp = new ScrambleProgress();
305
+ sp.start([
306
+ `Regenerating with ${getAIProviderShortName(state.aiProvider ?? 'gemini')}${modelName ? ` (${modelName})` : ''}`,
307
+ ]);
308
+ try {
309
+ switch (state.aiProvider) {
310
+ case 'copilot': {
311
+ const { generateCommitMessage } = await import('../api/copilot.js');
312
+ aiResult = await generateCommitMessage(diff, correction, state.copilotModel);
313
+ break;
314
+ }
315
+ case 'openrouter': {
316
+ const { generateCommitMessage } = await import('../api/openrouter.js');
317
+ aiResult = await generateCommitMessage(diff, correction, state.openrouterModel);
318
+ break;
319
+ }
320
+ case 'gemini': {
321
+ const { generateCommitMessage } = await import('../api/gemini.js');
322
+ aiResult = await generateCommitMessage(diff, correction, state.geminiModel);
323
+ break;
324
+ }
325
+ default: {
326
+ aiResult = null;
327
+ break;
328
+ }
329
+ }
330
+ sp.stop();
331
+ }
332
+ catch {
333
+ sp.stop();
334
+ aiResult = null;
335
+ }
336
+ if (!aiResult && attempt < maxAttempts - 1) {
337
+ log.ai('Regenerate returned no suggestion; retrying...');
338
+ }
279
339
  }
340
+ return aiResult;
341
+ };
342
+ // ── Extracted workflow steps ───────────────────────────────────────
343
+ /**
344
+ * Show commit log with format consistency indicators and let user
345
+ * pick which commits to reword. Returns sorted (oldest-first) array
346
+ * or null when nothing was selected.
347
+ */
348
+ const selectCommitsToReword = async (ctx) => {
349
+ const { commits, repoUrl } = ctx;
280
350
  // Detect commit format consistency (skip merge commits)
281
351
  const isMerge = (subject) => /^Merge\s/.test(subject);
282
352
  const formats = commits.map((c) => (isMerge(c.subject) ? null : detectCommitFormat(c.subject)));
@@ -298,20 +368,16 @@ export const handleReword = async () => {
298
368
  }
299
369
  // Priority-based inconsistency detection:
300
370
  // conventional (1st) > bracket-tag (2nd) > no-prefix / other (3rd)
301
- // - conventional: NEVER gets ⚠ (gold standard)
302
- // - bracket-tag: ⚠ only when conventional is the majority
303
- // - no-prefix/other: ⚠ when ANY conventional or bracket-tag exists
304
371
  const hasConventional = (formatCounts.get('conventional') ?? 0) > 0;
305
372
  const hasBracketTag = (formatCounts.get('bracket-tag') ?? 0) > 0;
306
373
  const hasStructured = hasConventional || hasBracketTag;
307
374
  const isInconsistent = (fmt) => {
308
375
  if (fmt === null)
309
- return false; // merge commits — always skip
376
+ return false;
310
377
  if (fmt === 'conventional')
311
- return false; // gold standard — never flag
378
+ return false;
312
379
  if (fmt === 'bracket-tag')
313
- return majorityFormat === 'conventional'; // only flag if conventional is majority
314
- // no-prefix / other: flag if any structured format exists
380
+ return majorityFormat === 'conventional';
315
381
  return hasStructured;
316
382
  };
317
383
  const inconsistentCount = formats.filter((f) => isInconsistent(f)).length;
@@ -319,35 +385,30 @@ export const handleReword = async () => {
319
385
  if (inconsistentCount > 0) {
320
386
  console.log(` ${colors.yellow}⚠${colors.reset} ${inconsistentCount} inconsistent commit(s) detected`);
321
387
  console.log(` ${colors.gray}Team pattern: ${formatLabel(majorityFormat)} (${majorityCount}/${totalNonMerge})${colors.reset}`);
322
- // If bracket-tag is majority, gently suggest conventional commits
323
388
  if (majorityFormat === 'bracket-tag' && !hasConventional) {
324
389
  console.log(` ${colors.gray}💡 Tip: conventional commits is the recommended project standard${colors.reset}`);
325
390
  }
326
391
  console.log('');
327
392
  }
328
393
  else if (majorityFormat !== 'conventional' && totalNonMerge > 0) {
329
- // No inconsistencies but not using conventional — soft tip
330
394
  console.log(` ${colors.gray}💡 Tip: conventional commits is the recommended project standard${colors.reset}`);
331
395
  console.log('');
332
396
  }
333
397
  // Compute column widths
334
398
  const maxHash = Math.max(...commits.map((c) => c.shortHash.length));
335
- const repoUrl = getRepoUrl();
336
399
  // Build multi-select options
337
400
  const options = commits.map((c, i) => {
338
401
  const hashCol = c.shortHash.padEnd(maxHash);
339
402
  const refs = formatRefs(c.refs);
340
403
  const subj = c.subject.length > 60 ? c.subject.slice(0, 57) + '...' : c.subject;
341
- // Show indicator only when inconsistencies exist
342
404
  const fmt = formats[i] ?? null;
343
405
  const indicator = inconsistentCount > 0
344
406
  ? isInconsistent(fmt)
345
407
  ? `${colors.red}⚠${colors.reset} `
346
408
  : fmt === null
347
- ? ' ' // merge commit — no indicator
409
+ ? ' '
348
410
  : `${colors.green}✓${colors.reset} `
349
411
  : '';
350
- // Wrap hash in OSC 8 hyperlink when repo URL is available
351
412
  const hashDisplay = repoUrl
352
413
  ? hyperlink(`${repoUrl}/commit/${c.hash}`, `${colors.yellow}${hashCol}${colors.reset}`)
353
414
  : `${colors.yellow}${hashCol}${colors.reset}`;
@@ -374,7 +435,7 @@ export const handleReword = async () => {
374
435
  const selected = await multiSelect('Select commits to edit:', options);
375
436
  if (selected.length === 0) {
376
437
  log.info('No commits selected.');
377
- return;
438
+ return null;
378
439
  }
379
440
  // Sort selected: oldest first (for rebase ordering)
380
441
  const filtered = commits.filter((c) => selected.includes(c.hash));
@@ -384,11 +445,18 @@ export const handleReword = async () => {
384
445
  if (item)
385
446
  selectedCommits.push(item);
386
447
  }
387
- // Collect new messages for each commit
448
+ return selectedCommits;
449
+ };
450
+ /**
451
+ * For each selected commit, let the user choose AI / manual / skip
452
+ * and collect new commit messages. Returns a Map of hash → new message.
453
+ */
454
+ const generateNewMessages = async (selectedCommits, ctx) => {
455
+ const { repoUrl, branch } = ctx;
456
+ const newMessages = new Map();
388
457
  console.log('');
389
458
  log.info(`Editing ${selectedCommits.length} commit message(s)...`);
390
459
  console.log('');
391
- const newMessages = new Map();
392
460
  // Load provider/model state for AI generation
393
461
  const state = loadState() ?? {
394
462
  step: 0,
@@ -451,44 +519,20 @@ export const handleReword = async () => {
451
519
  continue;
452
520
  }
453
521
  // Determine current provider/model
454
- let currentProvider = (state.aiProvider === 'manual' ? undefined : state.aiProvider) ?? 'gemini';
455
- let currentModel;
456
- if (currentProvider === 'copilot') {
457
- currentModel = state.copilotModel;
458
- }
459
- else if (currentProvider === 'openrouter') {
460
- currentModel = state.openrouterModel;
461
- }
462
- else {
463
- currentModel = state.geminiModel ?? DEFAULT_GEMINI_MODEL;
464
- }
522
+ let { provider: currentProvider, model: currentModel } = resolveAIProvider(state);
465
523
  // Initial AI generation
466
524
  let correction = editGuidance.trim();
467
525
  let initialAiResult = null;
468
526
  const spinner = new ScrambleProgress();
469
527
  try {
470
528
  spinner.start([
471
- 'analyzing commit diff...',
472
- `generating commit message with ${getAIProviderShortName(currentProvider)}${currentModel ? ` (${currentModel})` : ''}...`,
473
- 'formatting conventional commit...',
529
+ `Generating commit message with ${getAIProviderShortName(currentProvider)}${currentModel ? ` (${currentModel})` : ''}`,
474
530
  ]);
475
- if (currentProvider === 'copilot') {
476
- const { generateCommitMessage } = await import('../api/copilot.js');
477
- initialAiResult = await generateCommitMessage(diff, correction, state.copilotModel);
478
- }
479
- else if (currentProvider === 'openrouter') {
480
- const { generateCommitMessage } = await import('../api/openrouter.js');
481
- initialAiResult = await generateCommitMessage(diff, correction, state.openrouterModel);
482
- }
483
- else {
484
- const { generateCommitMessage } = await import('../api/gemini.js');
485
- initialAiResult = await generateCommitMessage(diff, correction, state.geminiModel);
486
- }
531
+ initialAiResult = await generateCommitMessageWithProvider(currentProvider, diff, correction, state.copilotModel, state.openrouterModel, state.geminiModel);
487
532
  spinner.stop();
488
533
  }
489
534
  catch {
490
- spinner.stop();
491
- log.warn('AI generation failed, falling back to manual edit');
535
+ spinner.fail('AI generation failed, falling back to manual edit');
492
536
  const edited = await editInline(currentMsg, `Edit: ${commit.shortHash} ${commit.subject}`);
493
537
  if (edited && edited.trim() !== currentMsg.trim()) {
494
538
  newMessages.set(commit.hash, edited.trim());
@@ -515,60 +559,7 @@ export const handleReword = async () => {
515
559
  aiResult = initialAiResult;
516
560
  }
517
561
  else if (forceDirect) {
518
- let directAttempt = 0;
519
- const maxDirectAttempts = 2;
520
- while (directAttempt < maxDirectAttempts && !aiResult) {
521
- let directModelName = '';
522
- if (state.aiProvider === 'copilot' && state.copilotModel) {
523
- directModelName = state.copilotModel;
524
- }
525
- else if (state.aiProvider === 'openrouter' && state.openrouterModel) {
526
- directModelName = state.openrouterModel;
527
- }
528
- else if (state.aiProvider === 'gemini') {
529
- directModelName = state.geminiModel ?? DEFAULT_GEMINI_MODEL;
530
- }
531
- if (correction)
532
- console.log('');
533
- const sp = new ScrambleProgress();
534
- sp.start([
535
- 'reviewing feedback...',
536
- `regenerating with ${getAIProviderShortName(state.aiProvider ?? 'gemini')}${directModelName ? ` (${directModelName})` : ''}...`,
537
- 'formatting conventional commit...',
538
- ]);
539
- try {
540
- switch (state.aiProvider) {
541
- case 'copilot': {
542
- const { generateCommitMessage } = await import('../api/copilot.js');
543
- aiResult = await generateCommitMessage(diff, correction, state.copilotModel);
544
- break;
545
- }
546
- case 'openrouter': {
547
- const { generateCommitMessage } = await import('../api/openrouter.js');
548
- aiResult = await generateCommitMessage(diff, correction, state.openrouterModel);
549
- break;
550
- }
551
- case 'gemini': {
552
- const { generateCommitMessage } = await import('../api/gemini.js');
553
- aiResult = await generateCommitMessage(diff, correction, state.geminiModel);
554
- break;
555
- }
556
- default: {
557
- aiResult = null;
558
- break;
559
- }
560
- }
561
- sp.stop();
562
- }
563
- catch {
564
- sp.stop();
565
- aiResult = null;
566
- }
567
- directAttempt += 1;
568
- if (!aiResult && directAttempt < maxDirectAttempts) {
569
- log.ai('Regenerate returned no suggestion; retrying...');
570
- }
571
- }
562
+ aiResult = await regenerateDirect(state, diff, correction);
572
563
  }
573
564
  else {
574
565
  const provForFallback = (state.aiProvider ?? 'gemini');
@@ -655,22 +646,7 @@ export const handleReword = async () => {
655
646
  }
656
647
  switch (acceptChoice) {
657
648
  case 'accept': {
658
- const normalized = normalizeAIOutput(commitMessage);
659
- const extracted = extractCommitTitle(normalized);
660
- let title;
661
- let bodyText = null;
662
- if (extracted) {
663
- title = extracted;
664
- bodyText = extractCommitBody(normalized, title);
665
- if (bodyText)
666
- bodyText = formatCommitBody(bodyText);
667
- }
668
- else {
669
- const first = normalized.split('\n').find((l) => l.trim());
670
- title = first?.trim() ?? normalized;
671
- }
672
- const finalMsg = bodyText ? `${title}\n\n${bodyText}` : title;
673
- newMessages.set(commit.hash, finalMsg.trim());
649
+ newMessages.set(commit.hash, buildFinalMessage(commitMessage).trim());
674
650
  log.success(`Message set for ${commit.shortHash}`);
675
651
  commitDone = true;
676
652
  break;
@@ -776,22 +752,7 @@ export const handleReword = async () => {
776
752
  case 'edit': {
777
753
  const edited = await editInline(commitMessage, `Edit: ${commit.shortHash}`);
778
754
  if (edited?.trim()) {
779
- const editedNorm = normalizeAIOutput(edited.trim());
780
- const editedTitle = extractCommitTitle(editedNorm);
781
- let title;
782
- let bodyText = null;
783
- if (editedTitle) {
784
- title = editedTitle;
785
- bodyText = extractCommitBody(editedNorm, title);
786
- if (bodyText)
787
- bodyText = formatCommitBody(bodyText);
788
- }
789
- else {
790
- const first = editedNorm.split('\n').find((l) => l.trim());
791
- title = first?.trim() ?? editedNorm;
792
- }
793
- const finalMsg = bodyText ? `${title}\n\n${bodyText}` : title;
794
- newMessages.set(commit.hash, finalMsg.trim());
755
+ newMessages.set(commit.hash, buildFinalMessage(edited.trim()).trim());
795
756
  log.success(`Message set for ${commit.shortHash}`);
796
757
  commitDone = true;
797
758
  }
@@ -804,12 +765,16 @@ export const handleReword = async () => {
804
765
  }
805
766
  console.log('');
806
767
  }
807
- if (newMessages.size === 0) {
808
- log.info('No messages changed.');
809
- return;
810
- }
768
+ return newMessages;
769
+ };
770
+ /**
771
+ * Preview proposed changes, confirm with the user, then execute
772
+ * git rebase -i with auto-generated editor scripts.
773
+ */
774
+ const executeRebase = async (selectedCommits, newMessages, ctx) => {
775
+ const { commits, repoUrl, branch, isProtected } = ctx;
811
776
  // Preview changes — complete before/after summary
812
- const line = '─'.repeat(56);
777
+ const line = '─'.repeat(BOX_W);
813
778
  console.log('');
814
779
  console.log(` ${colors.cyan}┌${line}┐${colors.reset}`);
815
780
  console.log(` ${colors.cyan}│${colors.reset} ${colors.bright}Changes to apply (${newMessages.size} commit${newMessages.size > 1 ? 's' : ''})${colors.reset}`);
@@ -900,7 +865,6 @@ export const handleReword = async () => {
900
865
  parentRef = execSilent(`git rev-parse ${oldestHash}^`).trim();
901
866
  }
902
867
  catch {
903
- // Oldest commit might be the root commit
904
868
  parentRef = '--root';
905
869
  }
906
870
  // Execute rebase
@@ -915,7 +879,6 @@ export const handleReword = async () => {
915
879
  const result = spawnSync('git', ['rebase', '-i', rebaseTarget], { stdio: 'inherit', env });
916
880
  if (result.status === 0) {
917
881
  log.success(`Reworded ${newMessages.size} commit(s)!`);
918
- // Show updated commits
919
882
  for (const [hash] of newMessages) {
920
883
  const short = hash.slice(0, 7);
921
884
  const newMsg = getCommitMessage(hash)?.split('\n')[0];
@@ -923,7 +886,7 @@ export const handleReword = async () => {
923
886
  console.log(` ${colors.green}✓${colors.reset} ${colors.yellow}${short}${colors.reset} ${newMsg}`);
924
887
  }
925
888
  }
926
- // Offer force push (reword requires force push to update remote)
889
+ // Offer force push
927
890
  console.log('');
928
891
  const shouldPush = confirm('Force push to update remote? (recommended)');
929
892
  if (shouldPush) {
@@ -933,11 +896,7 @@ export const handleReword = async () => {
933
896
  else {
934
897
  console.log('');
935
898
  const pushProgress = new ScrambleProgress();
936
- pushProgress.start([
937
- 'preparing force push...',
938
- 'pushing to remote...',
939
- 'confirming remote state...',
940
- ]);
899
+ pushProgress.start([`Force pushing to origin/${branch}`]);
941
900
  try {
942
901
  await execAsync(`git push --force-with-lease origin "${branch}"`, true);
943
902
  pushProgress.succeed(`Force pushed ${branch} to remote`);
@@ -950,7 +909,7 @@ export const handleReword = async () => {
950
909
  }
951
910
  }
952
911
  }
953
- // Team warning — history has been rewritten
912
+ // Team warning
954
913
  console.log('');
955
914
  console.log(` ${colors.yellow}⚠${colors.reset} ${colors.bright}History rewritten!${colors.reset} All commit hashes on ${colors.cyan}${branch}${colors.reset} have changed.`);
956
915
  console.log(` ${colors.gray}If this branch is shared, inform your team to run:${colors.reset}`);
@@ -974,7 +933,6 @@ export const handleReword = async () => {
974
933
  log.info('Run: git rebase --abort to undo');
975
934
  }
976
935
  finally {
977
- // Cleanup temp files
978
936
  try {
979
937
  fs.rmSync(tmpDir, { recursive: true, force: true });
980
938
  }
@@ -983,4 +941,38 @@ export const handleReword = async () => {
983
941
  }
984
942
  }
985
943
  };
944
+ // ── Main handler ───────────────────────────────────────────────────
945
+ export const handleReword = async () => {
946
+ log.banner();
947
+ log.step(`${colors.cyan}Edit Commit Messages${colors.reset}\n`);
948
+ const branch = getCurrentBranch();
949
+ const isProtected = ['main', 'master', 'develop', 'production', 'staging'].includes(branch);
950
+ console.log(` ${colors.gray}Branch: ${branch}${colors.reset}`);
951
+ if (isProtected) {
952
+ console.log(` ${colors.red}⚠${colors.reset} ${colors.bright}Protected branch${colors.reset} — rewriting history here affects all collaborators.`);
953
+ }
954
+ // Check working tree
955
+ if (!isWorkingTreeClean()) {
956
+ log.error('Working tree has uncommitted changes.');
957
+ log.info('Please commit or stash changes before rewording.');
958
+ return;
959
+ }
960
+ // Load commits
961
+ const commits = getRecentCommits(30);
962
+ if (commits.length === 0) {
963
+ log.warn('No commits found.');
964
+ return;
965
+ }
966
+ const repoUrl = getRepoUrl();
967
+ const ctx = { branch, isProtected, commits, repoUrl };
968
+ const selectedCommits = await selectCommitsToReword(ctx);
969
+ if (!selectedCommits)
970
+ return;
971
+ const newMessages = await generateNewMessages(selectedCommits, ctx);
972
+ if (newMessages.size === 0) {
973
+ log.info('No messages changed.');
974
+ return;
975
+ }
976
+ await executeRebase(selectedCommits, newMessages, ctx);
977
+ };
986
978
  //# sourceMappingURL=reword.js.map