geeto 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/lib/api/copilot-adapter.d.ts +11 -0
- package/lib/api/copilot-adapter.d.ts.map +1 -0
- package/lib/api/copilot-adapter.js +41 -0
- package/lib/api/copilot-adapter.js.map +1 -0
- package/lib/api/copilot-sdk.d.ts +48 -0
- package/lib/api/copilot-sdk.d.ts.map +1 -0
- package/lib/api/copilot-sdk.js +451 -0
- package/lib/api/copilot-sdk.js.map +1 -0
- package/lib/api/copilot.d.ts +21 -0
- package/lib/api/copilot.d.ts.map +1 -0
- package/lib/api/copilot.js +87 -0
- package/lib/api/copilot.js.map +1 -0
- package/lib/api/gemini-sdk.d.ts +24 -0
- package/lib/api/gemini-sdk.d.ts.map +1 -0
- package/lib/api/gemini-sdk.js +245 -0
- package/lib/api/gemini-sdk.js.map +1 -0
- package/lib/api/gemini.d.ts +21 -0
- package/lib/api/gemini.d.ts.map +1 -0
- package/lib/api/gemini.js +87 -0
- package/lib/api/gemini.js.map +1 -0
- package/lib/api/openrouter-sdk.d.ts +58 -0
- package/lib/api/openrouter-sdk.d.ts.map +1 -0
- package/lib/api/openrouter-sdk.js +341 -0
- package/lib/api/openrouter-sdk.js.map +1 -0
- package/lib/api/openrouter.d.ts +17 -0
- package/lib/api/openrouter.d.ts.map +1 -0
- package/lib/api/openrouter.js +111 -0
- package/lib/api/openrouter.js.map +1 -0
- package/lib/api/trello.d.ts +17 -0
- package/lib/api/trello.d.ts.map +1 -0
- package/lib/api/trello.js +72 -0
- package/lib/api/trello.js.map +1 -0
- package/lib/cli/input.d.ts +39 -0
- package/lib/cli/input.d.ts.map +1 -0
- package/lib/cli/input.js +119 -0
- package/lib/cli/input.js.map +1 -0
- package/lib/cli/menu.d.ts +9 -0
- package/lib/cli/menu.d.ts.map +1 -0
- package/lib/cli/menu.js +201 -0
- package/lib/cli/menu.js.map +1 -0
- package/lib/core/constants.d.ts +22 -0
- package/lib/core/constants.d.ts.map +1 -0
- package/lib/core/constants.js +24 -0
- package/lib/core/constants.js.map +1 -0
- package/lib/core/copilot-setup.d.ts +13 -0
- package/lib/core/copilot-setup.d.ts.map +1 -0
- package/lib/core/copilot-setup.js +447 -0
- package/lib/core/copilot-setup.js.map +1 -0
- package/lib/core/gemini-setup.d.ts +8 -0
- package/lib/core/gemini-setup.d.ts.map +1 -0
- package/lib/core/gemini-setup.js +84 -0
- package/lib/core/gemini-setup.js.map +1 -0
- package/lib/core/menu-constants.d.ts +24 -0
- package/lib/core/menu-constants.d.ts.map +1 -0
- package/lib/core/menu-constants.js +22 -0
- package/lib/core/menu-constants.js.map +1 -0
- package/lib/core/openrouter-setup.d.ts +8 -0
- package/lib/core/openrouter-setup.d.ts.map +1 -0
- package/lib/core/openrouter-setup.js +73 -0
- package/lib/core/openrouter-setup.js.map +1 -0
- package/lib/core/setup.d.ts +21 -0
- package/lib/core/setup.d.ts.map +1 -0
- package/lib/core/setup.js +88 -0
- package/lib/core/setup.js.map +1 -0
- package/lib/core/trello-setup.d.ts +8 -0
- package/lib/core/trello-setup.d.ts.map +1 -0
- package/lib/core/trello-setup.js +107 -0
- package/lib/core/trello-setup.js.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +250 -0
- package/lib/index.js.map +1 -0
- package/lib/types/index.d.ts +78 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +2 -0
- package/lib/types/index.js.map +1 -0
- package/lib/utils/branch-naming.d.ts +11 -0
- package/lib/utils/branch-naming.d.ts.map +1 -0
- package/lib/utils/branch-naming.js +396 -0
- package/lib/utils/branch-naming.js.map +1 -0
- package/lib/utils/colors.d.ts +14 -0
- package/lib/utils/colors.d.ts.map +1 -0
- package/lib/utils/colors.js +14 -0
- package/lib/utils/colors.js.map +1 -0
- package/lib/utils/commit-helpers.d.ts +53 -0
- package/lib/utils/commit-helpers.d.ts.map +1 -0
- package/lib/utils/commit-helpers.js +177 -0
- package/lib/utils/commit-helpers.js.map +1 -0
- package/lib/utils/config.d.ts +87 -0
- package/lib/utils/config.d.ts.map +1 -0
- package/lib/utils/config.js +326 -0
- package/lib/utils/config.js.map +1 -0
- package/lib/utils/display.d.ts +27 -0
- package/lib/utils/display.d.ts.map +1 -0
- package/lib/utils/display.js +116 -0
- package/lib/utils/display.js.map +1 -0
- package/lib/utils/error-helpers.d.ts +27 -0
- package/lib/utils/error-helpers.d.ts.map +1 -0
- package/lib/utils/error-helpers.js +102 -0
- package/lib/utils/error-helpers.js.map +1 -0
- package/lib/utils/exec.d.ts +18 -0
- package/lib/utils/exec.d.ts.map +1 -0
- package/lib/utils/exec.js +96 -0
- package/lib/utils/exec.js.map +1 -0
- package/lib/utils/git-ai-errors.d.ts +10 -0
- package/lib/utils/git-ai-errors.d.ts.map +1 -0
- package/lib/utils/git-ai-errors.js +71 -0
- package/lib/utils/git-ai-errors.js.map +1 -0
- package/lib/utils/git-ai.d.ts +26 -0
- package/lib/utils/git-ai.d.ts.map +1 -0
- package/lib/utils/git-ai.js +603 -0
- package/lib/utils/git-ai.js.map +1 -0
- package/lib/utils/git-commands.d.ts +21 -0
- package/lib/utils/git-commands.d.ts.map +1 -0
- package/lib/utils/git-commands.js +58 -0
- package/lib/utils/git-commands.js.map +1 -0
- package/lib/utils/git-errors.d.ts +76 -0
- package/lib/utils/git-errors.d.ts.map +1 -0
- package/lib/utils/git-errors.js +565 -0
- package/lib/utils/git-errors.js.map +1 -0
- package/lib/utils/git.d.ts +61 -0
- package/lib/utils/git.d.ts.map +1 -0
- package/lib/utils/git.js +245 -0
- package/lib/utils/git.js.map +1 -0
- package/lib/utils/logging.d.ts +25 -0
- package/lib/utils/logging.d.ts.map +1 -0
- package/lib/utils/logging.js +71 -0
- package/lib/utils/logging.js.map +1 -0
- package/lib/utils/menu-builders.d.ts +47 -0
- package/lib/utils/menu-builders.d.ts.map +1 -0
- package/lib/utils/menu-builders.js +34 -0
- package/lib/utils/menu-builders.js.map +1 -0
- package/lib/utils/platform.d.ts +13 -0
- package/lib/utils/platform.d.ts.map +1 -0
- package/lib/utils/platform.js +32 -0
- package/lib/utils/platform.js.map +1 -0
- package/lib/utils/spinner-wrapper.d.ts +16 -0
- package/lib/utils/spinner-wrapper.d.ts.map +1 -0
- package/lib/utils/spinner-wrapper.js +56 -0
- package/lib/utils/spinner-wrapper.js.map +1 -0
- package/lib/utils/state.d.ts +22 -0
- package/lib/utils/state.d.ts.map +1 -0
- package/lib/utils/state.js +75 -0
- package/lib/utils/state.js.map +1 -0
- package/lib/utils/time.d.ts +4 -0
- package/lib/utils/time.d.ts.map +1 -0
- package/lib/utils/time.js +29 -0
- package/lib/utils/time.js.map +1 -0
- package/lib/utils/type-guards.d.ts +10 -0
- package/lib/utils/type-guards.d.ts.map +1 -0
- package/lib/utils/type-guards.js +29 -0
- package/lib/utils/type-guards.js.map +1 -0
- package/lib/workflows/ai-provider.d.ts +13 -0
- package/lib/workflows/ai-provider.d.ts.map +1 -0
- package/lib/workflows/ai-provider.js +55 -0
- package/lib/workflows/ai-provider.js.map +1 -0
- package/lib/workflows/author.d.ts +4 -0
- package/lib/workflows/author.d.ts.map +1 -0
- package/lib/workflows/author.js +80 -0
- package/lib/workflows/author.js.map +1 -0
- package/lib/workflows/branch-helpers.d.ts +9 -0
- package/lib/workflows/branch-helpers.d.ts.map +1 -0
- package/lib/workflows/branch-helpers.js +406 -0
- package/lib/workflows/branch-helpers.js.map +1 -0
- package/lib/workflows/branch-utils.d.ts +9 -0
- package/lib/workflows/branch-utils.d.ts.map +1 -0
- package/lib/workflows/branch-utils.js +61 -0
- package/lib/workflows/branch-utils.js.map +1 -0
- package/lib/workflows/branch.d.ts +12 -0
- package/lib/workflows/branch.d.ts.map +1 -0
- package/lib/workflows/branch.js +555 -0
- package/lib/workflows/branch.js.map +1 -0
- package/lib/workflows/cleanup.d.ts +10 -0
- package/lib/workflows/cleanup.d.ts.map +1 -0
- package/lib/workflows/cleanup.js +287 -0
- package/lib/workflows/cleanup.js.map +1 -0
- package/lib/workflows/commit.d.ts +10 -0
- package/lib/workflows/commit.d.ts.map +1 -0
- package/lib/workflows/commit.js +771 -0
- package/lib/workflows/commit.js.map +1 -0
- package/lib/workflows/main-steps.d.ts +12 -0
- package/lib/workflows/main-steps.d.ts.map +1 -0
- package/lib/workflows/main-steps.js +407 -0
- package/lib/workflows/main-steps.js.map +1 -0
- package/lib/workflows/main.d.ts +8 -0
- package/lib/workflows/main.d.ts.map +1 -0
- package/lib/workflows/main.js +720 -0
- package/lib/workflows/main.js.map +1 -0
- package/lib/workflows/security-gate.d.ts +8 -0
- package/lib/workflows/security-gate.d.ts.map +1 -0
- package/lib/workflows/security-gate.js +447 -0
- package/lib/workflows/security-gate.js.map +1 -0
- package/lib/workflows/settings.d.ts +15 -0
- package/lib/workflows/settings.d.ts.map +1 -0
- package/lib/workflows/settings.js +438 -0
- package/lib/workflows/settings.js.map +1 -0
- package/lib/workflows/trello-menu.d.ts +16 -0
- package/lib/workflows/trello-menu.d.ts.map +1 -0
- package/lib/workflows/trello-menu.js +361 -0
- package/lib/workflows/trello-menu.js.map +1 -0
- package/package.json +112 -0
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commit workflow - handles commit-related operations
|
|
3
|
+
*/
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { askQuestion, confirm, editInEditor } from '../cli/input.js';
|
|
6
|
+
import { select } from '../cli/menu.js';
|
|
7
|
+
import { colors } from '../utils/colors.js';
|
|
8
|
+
import { extractCommitTitle, getCommitTypes, normalizeAIOutput } from '../utils/commit-helpers.js';
|
|
9
|
+
import { DEFAULT_GEMINI_MODEL } from '../utils/config.js';
|
|
10
|
+
import { execGit } from '../utils/exec.js';
|
|
11
|
+
import { chooseModelForProvider, getAIProviderShortName, getModelValue, interactiveAIFallback, isContextLimitFailure, isTransientAIFailure, } from '../utils/git-ai.js';
|
|
12
|
+
import { log } from '../utils/logging.js';
|
|
13
|
+
import { saveState } from '../utils/state.js';
|
|
14
|
+
export const getDefaultCommitTool = (aiProvider) => {
|
|
15
|
+
switch (aiProvider) {
|
|
16
|
+
case 'gemini': {
|
|
17
|
+
return 'gemini';
|
|
18
|
+
}
|
|
19
|
+
case 'copilot': {
|
|
20
|
+
return 'copilot';
|
|
21
|
+
}
|
|
22
|
+
case 'openrouter': {
|
|
23
|
+
return 'openrouter';
|
|
24
|
+
}
|
|
25
|
+
default: {
|
|
26
|
+
return 'manual';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const extractCommitBody = (text, title) => {
|
|
31
|
+
const lines = text.split('\n').map((l) => l.trim());
|
|
32
|
+
const titleIndex = lines.indexOf(title);
|
|
33
|
+
if (titleIndex === -1) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
// Get lines after the title
|
|
37
|
+
const bodyLines = lines.slice(titleIndex + 1).filter(Boolean);
|
|
38
|
+
if (bodyLines.length === 0) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return bodyLines.join('\n');
|
|
42
|
+
};
|
|
43
|
+
const isConventionalLine = (line) => {
|
|
44
|
+
const types = new Set([
|
|
45
|
+
'feat',
|
|
46
|
+
'fix',
|
|
47
|
+
'docs',
|
|
48
|
+
'style',
|
|
49
|
+
'refactor',
|
|
50
|
+
'test',
|
|
51
|
+
'chore',
|
|
52
|
+
'perf',
|
|
53
|
+
'ci',
|
|
54
|
+
'build',
|
|
55
|
+
'revert',
|
|
56
|
+
]);
|
|
57
|
+
const trimmed = line.trim();
|
|
58
|
+
if (!trimmed) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const colonIndex = trimmed.indexOf(':');
|
|
62
|
+
if (colonIndex === -1) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
const left = trimmed.slice(0, colonIndex).trim();
|
|
66
|
+
const type = (left.split('(')[0] ?? '').trim();
|
|
67
|
+
return types.has(type);
|
|
68
|
+
};
|
|
69
|
+
const formatCommitBody = (rawBody) => {
|
|
70
|
+
if (!rawBody) {
|
|
71
|
+
return '';
|
|
72
|
+
}
|
|
73
|
+
const lines = rawBody
|
|
74
|
+
.split('\n')
|
|
75
|
+
.map((l) => l.trim())
|
|
76
|
+
.filter(Boolean);
|
|
77
|
+
const conv = [];
|
|
78
|
+
const others = [];
|
|
79
|
+
for (const l of lines) {
|
|
80
|
+
if (isConventionalLine(l)) {
|
|
81
|
+
conv.push(l);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
others.push(l);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// If conventional lines exist, list them under "Other suggested commits"; otherwise join others.
|
|
88
|
+
if (conv.length > 0 && others.length > 0) {
|
|
89
|
+
return `${others.join('\n')}
|
|
90
|
+
|
|
91
|
+
Other suggested commits:\n- ${conv.join('\n- ')}`;
|
|
92
|
+
}
|
|
93
|
+
if (conv.length > 0) {
|
|
94
|
+
return conv.join('\n');
|
|
95
|
+
}
|
|
96
|
+
return others.join('\n');
|
|
97
|
+
};
|
|
98
|
+
export const handleCommitWorkflow = async (state, opts) => {
|
|
99
|
+
if (!opts?.suppressStep) {
|
|
100
|
+
log.step('Step 3: Commit');
|
|
101
|
+
}
|
|
102
|
+
const aiProvider = (state.aiProvider ?? 'gemini');
|
|
103
|
+
let selectedTool = getDefaultCommitTool(aiProvider);
|
|
104
|
+
// Helper: attempt to run git commit using a temporary file to avoid shell quoting issues
|
|
105
|
+
const attemptCommit = async (titleStr, bodyStr) => {
|
|
106
|
+
// Compose full commit message
|
|
107
|
+
const msg = bodyStr ? `${titleStr}\n\n${bodyStr}\n` : `${titleStr}\n`;
|
|
108
|
+
// Use spawnSync to avoid shell quoting pitfalls
|
|
109
|
+
const tempDir = await import('node:os');
|
|
110
|
+
const fs = await import('node:fs');
|
|
111
|
+
const { spawnSync } = await import('node:child_process');
|
|
112
|
+
const tmpFile = path.join(tempDir.tmpdir(), `geeto-commit-${Date.now()}.txt`);
|
|
113
|
+
try {
|
|
114
|
+
fs.writeFileSync(tmpFile, msg, 'utf8');
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
118
|
+
log.error(`Failed to write temporary commit message: ${errMsg}`);
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const res = spawnSync('git', ['commit', '-F', tmpFile], { stdio: 'inherit' });
|
|
123
|
+
// cleanup
|
|
124
|
+
try {
|
|
125
|
+
fs.unlinkSync(tmpFile);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
/* ignore cleanup errors */
|
|
129
|
+
}
|
|
130
|
+
if (res.status === 0) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
log.error('Commit failed due to commit hook or invalid message.');
|
|
134
|
+
const action = await select('Commit failed. Choose an action:', [
|
|
135
|
+
{ label: "I've fixed it, retry", value: 'retry' },
|
|
136
|
+
{ label: 'Edit commit message and retry', value: 'edit' },
|
|
137
|
+
{ label: 'Abort', value: 'abort' },
|
|
138
|
+
]);
|
|
139
|
+
if (action === 'edit') {
|
|
140
|
+
const edited = editInEditor(`${titleStr}\n\n${bodyStr ?? ''}`, 'geeto-commit.txt');
|
|
141
|
+
if (!edited?.trim()) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
const normalized = normalizeAIOutput(edited.trim());
|
|
145
|
+
const newTitle = extractCommitTitle(normalized) ?? edited.split('\n').find((l) => l.trim()) ?? '';
|
|
146
|
+
const newBody = newTitle ? extractCommitBody(normalized, newTitle) : null;
|
|
147
|
+
return attemptCommit(newTitle, newBody);
|
|
148
|
+
}
|
|
149
|
+
if (action === 'retry') {
|
|
150
|
+
// User fixed issues outside of this tool; try committing again
|
|
151
|
+
return attemptCommit(titleStr, bodyStr);
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
157
|
+
log.error(`Failed to run git commit: ${errMsg}`);
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const aiTools = [
|
|
162
|
+
{ label: 'Gemini', value: 'gemini' },
|
|
163
|
+
{ label: 'GitHub Copilot (Recommended)', value: 'copilot' },
|
|
164
|
+
{ label: 'OpenRouter', value: 'openrouter' },
|
|
165
|
+
{ label: 'Manual commit', value: 'manual' },
|
|
166
|
+
];
|
|
167
|
+
// Log provider detected; if manual, skip AI prompts and go straight to conventional commit flow
|
|
168
|
+
if (aiProvider === 'manual') {
|
|
169
|
+
selectedTool = 'manual';
|
|
170
|
+
}
|
|
171
|
+
let modelName = '';
|
|
172
|
+
if (aiProvider === 'copilot' && state.copilotModel) {
|
|
173
|
+
modelName = state.copilotModel;
|
|
174
|
+
}
|
|
175
|
+
else if (aiProvider === 'openrouter' && state.openrouterModel) {
|
|
176
|
+
modelName = state.openrouterModel;
|
|
177
|
+
}
|
|
178
|
+
else if (aiProvider === 'gemini') {
|
|
179
|
+
// prefer persisted state selection, otherwise fall back to default
|
|
180
|
+
modelName = state.geminiModel ?? DEFAULT_GEMINI_MODEL;
|
|
181
|
+
}
|
|
182
|
+
// If not manual, ask whether to use AI provider for commit; otherwise skip to manual flow
|
|
183
|
+
let useAutoTool = false;
|
|
184
|
+
if (aiProvider !== 'manual') {
|
|
185
|
+
if (opts?.suppressConfirm) {
|
|
186
|
+
useAutoTool = true;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
useAutoTool = confirm(`\nUse ${getAIProviderShortName(aiProvider)}${modelName ? ` (${modelName})` : ''} for commit? (recommended)`);
|
|
190
|
+
}
|
|
191
|
+
if (!useAutoTool) {
|
|
192
|
+
selectedTool = await select('Choose commit method:', aiTools);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const commitSuccess = false;
|
|
196
|
+
const diff = execGit('git diff --cached', true);
|
|
197
|
+
if (!diff?.trim()) {
|
|
198
|
+
log.warn('No staged changes found. Cannot generate a commit message from empty diff. Aborting.');
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
log.info(`Git diff size: ${diff.length} chars`);
|
|
202
|
+
console.log('');
|
|
203
|
+
// Use chosen provider; prompt model and allow going back to provider selection.
|
|
204
|
+
let effectiveProvider = aiProvider === 'manual' ? 'gemini' : aiProvider;
|
|
205
|
+
if (selectedTool !== 'manual') {
|
|
206
|
+
// Determine if model prompt is needed (skip if default & persisted)
|
|
207
|
+
const defaultTool = getDefaultCommitTool(aiProvider);
|
|
208
|
+
const choseAutoDefault = useAutoTool && selectedTool === defaultTool;
|
|
209
|
+
const hasPersistedModel = (tool) => {
|
|
210
|
+
switch (tool) {
|
|
211
|
+
case 'copilot': {
|
|
212
|
+
return !!state.copilotModel;
|
|
213
|
+
}
|
|
214
|
+
case 'openrouter': {
|
|
215
|
+
return !!state.openrouterModel;
|
|
216
|
+
}
|
|
217
|
+
case 'gemini': {
|
|
218
|
+
return !!state.geminiModel;
|
|
219
|
+
}
|
|
220
|
+
default: {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
const needModelPrompt = !choseAutoDefault || !hasPersistedModel(selectedTool);
|
|
226
|
+
if (needModelPrompt) {
|
|
227
|
+
// loop until a model is chosen or user returns to manual
|
|
228
|
+
let providerPick = selectedTool;
|
|
229
|
+
while (true) {
|
|
230
|
+
effectiveProvider = providerPick;
|
|
231
|
+
state.aiProvider = effectiveProvider;
|
|
232
|
+
saveState(state);
|
|
233
|
+
// Prompt model for chosen provider (centralized helper)
|
|
234
|
+
log.info(`Selected AI Provider: ${getAIProviderShortName(effectiveProvider)}`);
|
|
235
|
+
const chosenModel = await chooseModelForProvider(effectiveProvider, 'Choose model:', 'Back to suggested commit selection');
|
|
236
|
+
if (!chosenModel) {
|
|
237
|
+
// setup failed; allow user to reselect provider
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (chosenModel === 'back') {
|
|
241
|
+
providerPick = (await select('Choose commit method:', aiTools));
|
|
242
|
+
if (providerPick === 'manual') {
|
|
243
|
+
selectedTool = 'manual';
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
// Persist chosen model
|
|
249
|
+
switch (effectiveProvider) {
|
|
250
|
+
case 'copilot': {
|
|
251
|
+
state.copilotModel = chosenModel;
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
case 'openrouter': {
|
|
255
|
+
state.openrouterModel = chosenModel;
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
case 'gemini': {
|
|
259
|
+
state.geminiModel = chosenModel;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
default: {
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
saveState(state);
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
// end while
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
// no interactive model prompt required — persist chosen provider and continue
|
|
273
|
+
state.aiProvider = selectedTool;
|
|
274
|
+
saveState(state);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (selectedTool !== 'manual') {
|
|
278
|
+
let correction = '';
|
|
279
|
+
// Try generating commit message via AI
|
|
280
|
+
let initialAiResult = null;
|
|
281
|
+
let currentModel;
|
|
282
|
+
const spinner = log.spinner();
|
|
283
|
+
try {
|
|
284
|
+
let currentProvider;
|
|
285
|
+
if (state.aiProvider && state.aiProvider !== 'manual') {
|
|
286
|
+
currentProvider = state.aiProvider;
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
currentProvider = aiProvider;
|
|
290
|
+
}
|
|
291
|
+
if (currentProvider === 'copilot') {
|
|
292
|
+
currentModel = state.copilotModel;
|
|
293
|
+
}
|
|
294
|
+
else if (currentProvider === 'openrouter') {
|
|
295
|
+
currentModel = state.openrouterModel;
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
currentModel = state.geminiModel ?? DEFAULT_GEMINI_MODEL;
|
|
299
|
+
}
|
|
300
|
+
spinner.start(`Generating commit message with ${getAIProviderShortName(currentProvider)}${currentModel ? ` (${currentModel})` : ''}...`);
|
|
301
|
+
if (currentProvider === 'copilot') {
|
|
302
|
+
const { generateCommitMessage } = await import('../api/copilot.js');
|
|
303
|
+
initialAiResult = await generateCommitMessage(diff, correction, state.copilotModel);
|
|
304
|
+
}
|
|
305
|
+
else if (currentProvider === 'openrouter') {
|
|
306
|
+
const { generateCommitMessage } = await import('../api/openrouter.js');
|
|
307
|
+
initialAiResult = await generateCommitMessage(diff, correction, state.openrouterModel);
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
const { generateCommitMessage } = await import('../api/gemini.js');
|
|
311
|
+
initialAiResult = await generateCommitMessage(diff, correction, state.geminiModel);
|
|
312
|
+
}
|
|
313
|
+
spinner.stop();
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
spinner.stop();
|
|
317
|
+
log.warn('Initial AI generation attempt failed, will enter interactive fallback');
|
|
318
|
+
initialAiResult = null;
|
|
319
|
+
}
|
|
320
|
+
// Loop AI generation/user choices — pass initial result only on first iteration
|
|
321
|
+
let firstAttempt = true;
|
|
322
|
+
let forceDirect = false;
|
|
323
|
+
// allow returning from model/provider menus to the suggested-commit prompt
|
|
324
|
+
let skipRegenerate = false;
|
|
325
|
+
let previousAiResult = initialAiResult;
|
|
326
|
+
while (true) {
|
|
327
|
+
// Obtain AI result: initial -> direct regenerate -> interactive fallback
|
|
328
|
+
let aiResult = null;
|
|
329
|
+
if (skipRegenerate) {
|
|
330
|
+
// reuse the previous AI result and show the accept menu again
|
|
331
|
+
aiResult = previousAiResult;
|
|
332
|
+
skipRegenerate = false;
|
|
333
|
+
}
|
|
334
|
+
else if (firstAttempt &&
|
|
335
|
+
initialAiResult &&
|
|
336
|
+
!isTransientAIFailure(initialAiResult) &&
|
|
337
|
+
!isContextLimitFailure(initialAiResult)) {
|
|
338
|
+
aiResult = initialAiResult;
|
|
339
|
+
}
|
|
340
|
+
else if (forceDirect) {
|
|
341
|
+
// Try direct generation with the currently selected provider/model
|
|
342
|
+
// Retry once automatically if the provider returns no suggestion to reduce
|
|
343
|
+
// the chance of immediately falling back to the interactive menu after
|
|
344
|
+
// the user provided a correction.
|
|
345
|
+
let directAttempt = 0;
|
|
346
|
+
const maxDirectAttempts = 2;
|
|
347
|
+
while (directAttempt < maxDirectAttempts && !aiResult) {
|
|
348
|
+
// Log which provider/model we're attempting for regenerate
|
|
349
|
+
let directModelName = '';
|
|
350
|
+
if (state.aiProvider === 'copilot' && state.copilotModel) {
|
|
351
|
+
directModelName = state.copilotModel;
|
|
352
|
+
}
|
|
353
|
+
else if (state.aiProvider === 'openrouter' && state.openrouterModel) {
|
|
354
|
+
directModelName = state.openrouterModel;
|
|
355
|
+
}
|
|
356
|
+
else if (state.aiProvider === 'gemini') {
|
|
357
|
+
directModelName = state.geminiModel ?? DEFAULT_GEMINI_MODEL;
|
|
358
|
+
}
|
|
359
|
+
if (correction) {
|
|
360
|
+
console.log('');
|
|
361
|
+
}
|
|
362
|
+
const spinner = log.spinner();
|
|
363
|
+
spinner.start(`Regenerating commit message with ${getAIProviderShortName(state.aiProvider ?? 'gemini')}${directModelName ? ` (${directModelName})` : ''}...`);
|
|
364
|
+
try {
|
|
365
|
+
switch (state.aiProvider) {
|
|
366
|
+
case 'copilot': {
|
|
367
|
+
const { generateCommitMessage } = await import('../api/copilot.js');
|
|
368
|
+
aiResult = await generateCommitMessage(diff, correction, state.copilotModel);
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
case 'openrouter': {
|
|
372
|
+
const { generateCommitMessage } = await import('../api/openrouter.js');
|
|
373
|
+
aiResult = await generateCommitMessage(diff, correction, state.openrouterModel);
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
case 'gemini': {
|
|
377
|
+
const { generateCommitMessage } = await import('../api/gemini.js');
|
|
378
|
+
aiResult = await generateCommitMessage(diff, correction, state.geminiModel);
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
default: {
|
|
382
|
+
aiResult = null;
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
spinner.stop();
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
spinner.stop();
|
|
390
|
+
aiResult = null;
|
|
391
|
+
}
|
|
392
|
+
directAttempt += 1;
|
|
393
|
+
if (!aiResult && directAttempt < maxDirectAttempts) {
|
|
394
|
+
log.ai('Regenerate returned no suggestion; retrying once...');
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
const currentProv = (state.aiProvider ?? 'gemini');
|
|
400
|
+
let modelChoice;
|
|
401
|
+
if (currentProv === 'copilot') {
|
|
402
|
+
modelChoice = state.copilotModel;
|
|
403
|
+
}
|
|
404
|
+
else if (currentProv === 'openrouter') {
|
|
405
|
+
modelChoice = state.openrouterModel;
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
modelChoice = state.geminiModel ?? DEFAULT_GEMINI_MODEL;
|
|
409
|
+
}
|
|
410
|
+
aiResult = await interactiveAIFallback(firstAttempt ? initialAiResult : null, currentProv, modelChoice, diff, correction, state.currentBranch, (provider, model) => {
|
|
411
|
+
log.info(`AI provider switched to: ${getAIProviderShortName(provider)}`);
|
|
412
|
+
state.aiProvider = provider;
|
|
413
|
+
switch (provider) {
|
|
414
|
+
case 'copilot': {
|
|
415
|
+
state.copilotModel = model;
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
case 'openrouter': {
|
|
419
|
+
state.openrouterModel = model;
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
case 'gemini': {
|
|
423
|
+
// persist gemini model selection if provided
|
|
424
|
+
if (model && typeof model === 'string') {
|
|
425
|
+
state.geminiModel = model;
|
|
426
|
+
}
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
default: {
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
saveState(state);
|
|
434
|
+
}, true);
|
|
435
|
+
}
|
|
436
|
+
// Remember last AI result before any user-driven continues/regenerates
|
|
437
|
+
previousAiResult = aiResult;
|
|
438
|
+
// After first interactive attempt, clear seed to force fresh suggestions
|
|
439
|
+
firstAttempt = false;
|
|
440
|
+
// reset forceDirect unless explicitly set again by 'regenerate'
|
|
441
|
+
forceDirect = false;
|
|
442
|
+
const commitMessage = aiResult ?? '';
|
|
443
|
+
if (!commitMessage) {
|
|
444
|
+
log.warn('Could not generate commit message from AI provider');
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
const contextLimitDetected = isContextLimitFailure(commitMessage);
|
|
448
|
+
// Persist AI suggestion for commit so user can inspect/raw and we can show a short suggested line
|
|
449
|
+
try {
|
|
450
|
+
const fs = await import('node:fs/promises');
|
|
451
|
+
const outDir = path.join(process.cwd(), '.geeto');
|
|
452
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
453
|
+
let modelParam;
|
|
454
|
+
if (state.aiProvider === 'copilot') {
|
|
455
|
+
modelParam = state.copilotModel;
|
|
456
|
+
}
|
|
457
|
+
else if (state.aiProvider === 'openrouter') {
|
|
458
|
+
modelParam = state.openrouterModel;
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
modelParam = state.geminiModel ?? DEFAULT_GEMINI_MODEL;
|
|
462
|
+
}
|
|
463
|
+
const payload = {
|
|
464
|
+
provider: state.aiProvider ?? aiProvider,
|
|
465
|
+
model: modelParam,
|
|
466
|
+
raw: commitMessage,
|
|
467
|
+
timestamp: new Date().toISOString(),
|
|
468
|
+
};
|
|
469
|
+
try {
|
|
470
|
+
const existing = await fs.readFile(path.join(outDir, 'last-ai-suggestion.json'), 'utf8');
|
|
471
|
+
const parsed = JSON.parse(existing || '{}');
|
|
472
|
+
if (parsed && typeof parsed === 'object' && 'content' in parsed) {
|
|
473
|
+
payload.content = parsed.content;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
/* ignore read errors */
|
|
478
|
+
}
|
|
479
|
+
await fs.writeFile(path.join(outDir, 'last-ai-suggestion.json'), JSON.stringify(payload, null, 2));
|
|
480
|
+
// Show the suggested commit: subject and full body if present
|
|
481
|
+
const lines = commitMessage.split('\n');
|
|
482
|
+
const subject = lines.find((l) => l.trim()) ?? commitMessage;
|
|
483
|
+
const body = lines
|
|
484
|
+
.slice(lines.indexOf(subject) + 1)
|
|
485
|
+
.join('\n')
|
|
486
|
+
.trim();
|
|
487
|
+
log.ai(`Suggested Commit:\n\n${colors.cyan}${colors.bright}${subject}`);
|
|
488
|
+
if (body) {
|
|
489
|
+
console.log('\n' + body + `${colors.reset}\n`);
|
|
490
|
+
}
|
|
491
|
+
log.info('Incorrect Suggestion? check .geeto/last-ai-suggestion.json (possible AI/context limit).');
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
/* ignore file write failures */
|
|
495
|
+
}
|
|
496
|
+
// If we have a short subject line in the suggestion, allow accepting
|
|
497
|
+
// the suggested commit message even when a context limit was detected.
|
|
498
|
+
const subjectLine = commitMessage.split('\n').find((l) => l.trim()) ?? '';
|
|
499
|
+
let acceptAi;
|
|
500
|
+
if (contextLimitDetected && !subjectLine) {
|
|
501
|
+
// No usable suggestion present: force the user to change model/provider or edit
|
|
502
|
+
const editorName = process.env.EDITOR ?? (process.platform === 'win32' ? 'notepad' : 'vi');
|
|
503
|
+
acceptAi = await select('This model cannot process the input due to token/context limits. Please choose a different model or provider:', [
|
|
504
|
+
{
|
|
505
|
+
label: `Try again with ${getAIProviderShortName(aiProvider)}${getModelValue(currentModel) ? ` (${getModelValue(currentModel)})` : ''} model`,
|
|
506
|
+
value: 'try-same',
|
|
507
|
+
},
|
|
508
|
+
{ label: 'Change model', value: 'change-model' },
|
|
509
|
+
{ label: 'Change AI provider', value: 'change-provider' },
|
|
510
|
+
{ label: `Edit in editor (${editorName})`, value: 'edit' },
|
|
511
|
+
]);
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
// Either no context limits, or we have a usable suggestion (allow accepting)
|
|
515
|
+
const editorName = process.env.EDITOR ?? (process.platform === 'win32' ? 'notepad' : 'vi');
|
|
516
|
+
acceptAi = await select('Accept this commit message?', [
|
|
517
|
+
{ label: 'Yes, use it', value: 'accept' },
|
|
518
|
+
{ label: 'Regenerate', value: 'regenerate' },
|
|
519
|
+
{ label: `Edit in editor (${editorName})`, value: 'edit' },
|
|
520
|
+
{ label: 'Correct AI (give feedback)', value: 'correct' },
|
|
521
|
+
{ label: 'Change model', value: 'change-model' },
|
|
522
|
+
{ label: 'Change AI provider', value: 'change-provider' },
|
|
523
|
+
]);
|
|
524
|
+
}
|
|
525
|
+
switch (acceptAi) {
|
|
526
|
+
case 'accept': {
|
|
527
|
+
log.info('User accepted AI suggestion');
|
|
528
|
+
// Extract and clean commit message using helpers
|
|
529
|
+
const normalizedOutput = normalizeAIOutput(commitMessage);
|
|
530
|
+
const extractedTitle = extractCommitTitle(normalizedOutput);
|
|
531
|
+
let title;
|
|
532
|
+
let body = null;
|
|
533
|
+
if (extractedTitle) {
|
|
534
|
+
title = extractedTitle;
|
|
535
|
+
body = extractCommitBody(normalizedOutput, title);
|
|
536
|
+
if (body) {
|
|
537
|
+
body = formatCommitBody(body);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
// Fallback: use first non-empty line
|
|
542
|
+
const firstLine = normalizedOutput.split('\n').find((line) => line.trim());
|
|
543
|
+
title = firstLine?.trim() ?? normalizedOutput;
|
|
544
|
+
}
|
|
545
|
+
const committed = await attemptCommit(title, body);
|
|
546
|
+
if (committed) {
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
// If commit did not complete (user aborted or editing cancelled), continue loop
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
case 'regenerate': {
|
|
553
|
+
correction = '';
|
|
554
|
+
// next loop should try direct generation with the currently selected model
|
|
555
|
+
forceDirect = true;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
case 'try-same': {
|
|
559
|
+
// User chose to attempt the same model again — try a direct regenerate
|
|
560
|
+
forceDirect = true;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
case 'change-provider': {
|
|
564
|
+
const prov = await select('Choose AI provider:', [
|
|
565
|
+
{ label: 'Gemini', value: 'gemini' },
|
|
566
|
+
{ label: 'GitHub Copilot (Recommended)', value: 'copilot' },
|
|
567
|
+
{ label: 'OpenRouter', value: 'openrouter' },
|
|
568
|
+
{ label: 'Back to suggested commit selection', value: 'back' },
|
|
569
|
+
]);
|
|
570
|
+
if (prov === 'back') {
|
|
571
|
+
// return to the accept-suggestion prompt reusing previous AI result
|
|
572
|
+
skipRegenerate = true;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
// Use centralized helper to choose model for the provider
|
|
576
|
+
const chosenModel = await chooseModelForProvider(prov, 'Choose model:', 'Back to suggested commit selection');
|
|
577
|
+
if (!chosenModel) {
|
|
578
|
+
// setup failed; re-prompt later
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (chosenModel === 'back') {
|
|
582
|
+
// user chose to go back to selection
|
|
583
|
+
skipRegenerate = true;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
state.aiProvider = prov;
|
|
587
|
+
switch (prov) {
|
|
588
|
+
case 'copilot': {
|
|
589
|
+
state.copilotModel = chosenModel;
|
|
590
|
+
state.openrouterModel = undefined;
|
|
591
|
+
state.geminiModel = undefined;
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
case 'openrouter': {
|
|
595
|
+
state.openrouterModel = chosenModel;
|
|
596
|
+
state.copilotModel = undefined;
|
|
597
|
+
state.geminiModel = undefined;
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
case 'gemini': {
|
|
601
|
+
state.geminiModel = chosenModel;
|
|
602
|
+
state.copilotModel = undefined;
|
|
603
|
+
state.openrouterModel = undefined;
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
default: {
|
|
607
|
+
state.geminiModel = chosenModel;
|
|
608
|
+
state.copilotModel = undefined;
|
|
609
|
+
state.openrouterModel = undefined;
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
saveState(state);
|
|
614
|
+
// force direct regenerate with new model
|
|
615
|
+
forceDirect = true;
|
|
616
|
+
correction = '';
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
case 'change-model': {
|
|
620
|
+
const currentProv = (state.aiProvider ?? 'gemini');
|
|
621
|
+
const providerKey = (currentProv === 'manual' ? 'gemini' : currentProv);
|
|
622
|
+
const chosen = await chooseModelForProvider(providerKey, 'Choose model:', 'Back to suggested commit selection');
|
|
623
|
+
if (!chosen) {
|
|
624
|
+
skipRegenerate = true;
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
if (chosen === 'back') {
|
|
628
|
+
skipRegenerate = true;
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
switch (currentProv) {
|
|
632
|
+
case 'copilot': {
|
|
633
|
+
state.copilotModel = chosen;
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
case 'openrouter': {
|
|
637
|
+
state.openrouterModel = chosen;
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
case 'gemini': {
|
|
641
|
+
state.geminiModel = chosen;
|
|
642
|
+
state.copilotModel = undefined;
|
|
643
|
+
state.openrouterModel = undefined;
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
default: {
|
|
647
|
+
state.geminiModel = chosen;
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
saveState(state);
|
|
652
|
+
forceDirect = true;
|
|
653
|
+
correction = '';
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
case 'correct': {
|
|
657
|
+
correction = askQuestion('Provide corrections for the AI (e.g., shorten header, clarify scope): ');
|
|
658
|
+
// Immediately force a regenerate using the provided correction
|
|
659
|
+
forceDirect = true;
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
case 'edit': {
|
|
663
|
+
// Open user's editor for multi-line editing
|
|
664
|
+
const initial = commitMessage;
|
|
665
|
+
const edited = editInEditor(initial, 'geeto-commit.txt');
|
|
666
|
+
if (edited?.trim()) {
|
|
667
|
+
const editedMessage = edited.trim();
|
|
668
|
+
// Process the edited message
|
|
669
|
+
const normalizedOutput = normalizeAIOutput(editedMessage);
|
|
670
|
+
const extractedTitle = extractCommitTitle(normalizedOutput);
|
|
671
|
+
let title;
|
|
672
|
+
let body = null;
|
|
673
|
+
if (extractedTitle) {
|
|
674
|
+
title = extractedTitle;
|
|
675
|
+
body = extractCommitBody(normalizedOutput, title);
|
|
676
|
+
if (body) {
|
|
677
|
+
body = formatCommitBody(body);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
// Fallback: use first non-empty line
|
|
682
|
+
const firstLine = normalizedOutput.split('\n').find((line) => line.trim());
|
|
683
|
+
title = firstLine?.trim() ?? normalizedOutput;
|
|
684
|
+
}
|
|
685
|
+
const committed = await attemptCommit(title, body);
|
|
686
|
+
if (committed) {
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
689
|
+
// If commit didn't happen, continue the loop to allow later actions
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
// If no edit provided, continue the loop
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (!commitSuccess || selectedTool === 'manual') {
|
|
700
|
+
log.info('Falling back to manual commit flow');
|
|
701
|
+
const mode = await select('Choose commit mode:', [
|
|
702
|
+
{ label: 'Conventional commit (structured)', value: 'conventional' },
|
|
703
|
+
{ label: 'Manual commit (freeform)', value: 'manual' },
|
|
704
|
+
{ label: 'Cancel', value: 'cancel' },
|
|
705
|
+
]);
|
|
706
|
+
if (mode === 'cancel') {
|
|
707
|
+
log.warn('Commit cancelled.');
|
|
708
|
+
process.exit(0);
|
|
709
|
+
}
|
|
710
|
+
if (mode === 'manual') {
|
|
711
|
+
// Freeform manual commit: prompt for a non-empty commit message
|
|
712
|
+
let message = '';
|
|
713
|
+
while (!message) {
|
|
714
|
+
message = askQuestion('Commit message: ').trim();
|
|
715
|
+
if (!message) {
|
|
716
|
+
log.error('Commit message cannot be empty!');
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const committed = await attemptCommit(message);
|
|
720
|
+
if (committed) {
|
|
721
|
+
log.success(`Committed: ${colors.cyan}${colors.bright}${message}${colors.reset}`);
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
log.error('Commit failed or aborted.');
|
|
725
|
+
process.exit(1);
|
|
726
|
+
}
|
|
727
|
+
const commitType = await select('Select commit type:', getCommitTypes());
|
|
728
|
+
if (commitType === 'cancel') {
|
|
729
|
+
log.warn('Commit cancelled.');
|
|
730
|
+
process.exit(0);
|
|
731
|
+
}
|
|
732
|
+
const scope = askQuestion('Scope (optional, press Enter to skip): ').trim();
|
|
733
|
+
let description = '';
|
|
734
|
+
let suggestedDescription = state.workingBranch;
|
|
735
|
+
const slashIndex = state.workingBranch.indexOf('/');
|
|
736
|
+
const hashIndex = state.workingBranch.indexOf('#');
|
|
737
|
+
if (slashIndex > 0) {
|
|
738
|
+
suggestedDescription = state.workingBranch.slice(slashIndex + 1);
|
|
739
|
+
}
|
|
740
|
+
else if (hashIndex > 0) {
|
|
741
|
+
suggestedDescription = state.workingBranch.slice(hashIndex + 1);
|
|
742
|
+
}
|
|
743
|
+
suggestedDescription = suggestedDescription.replaceAll('-', ' ').replaceAll('_', ' ').trim();
|
|
744
|
+
const useSuggested = confirm(`Use suggested description: "${suggestedDescription}"?`);
|
|
745
|
+
if (useSuggested) {
|
|
746
|
+
description = suggestedDescription;
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
while (!description) {
|
|
750
|
+
description = askQuestion('Commit message: ').trim();
|
|
751
|
+
if (!description) {
|
|
752
|
+
log.error('Commit message cannot be empty!');
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
// No prefix selection — keep description as entered
|
|
757
|
+
const commitMsg = scope
|
|
758
|
+
? `${commitType}(${scope}): ${description}`
|
|
759
|
+
: `${commitType}: ${description}`;
|
|
760
|
+
const committed = await attemptCommit(commitMsg);
|
|
761
|
+
if (committed) {
|
|
762
|
+
log.success(`Committed: ${colors.cyan}${colors.bright}${commitMsg}${colors.reset}`);
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
log.error('Commit failed or aborted.');
|
|
766
|
+
process.exit(1);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return true;
|
|
770
|
+
};
|
|
771
|
+
//# sourceMappingURL=commit.js.map
|