sncommit 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +93 -99
- package/package.json +2 -2
- package/src/components/App.tsx +47 -79
- package/src/components/CommitSuggestions.tsx +1 -1
- package/src/components/ConfigApp.tsx +69 -36
- package/src/services/groq.ts +76 -36
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -58323,7 +58323,15 @@ class GroqService {
|
|
|
58323
58323
|
}
|
|
58324
58324
|
return this.parseSuggestions(content);
|
|
58325
58325
|
} catch (e2) {
|
|
58326
|
-
|
|
58326
|
+
const error = e2;
|
|
58327
|
+
if (error?.status === 401) {
|
|
58328
|
+
console.warn(`
|
|
58329
|
+
\x1B[33m⚠️ Groq API Key is invalid.\x1B[0m`);
|
|
58330
|
+
console.warn(` Using static backup suggestions. Run \x1B[36mbetter-commit config\x1B[0m to set your key.
|
|
58331
|
+
`);
|
|
58332
|
+
} else {
|
|
58333
|
+
console.error("Error generation suggestions:", error?.message || String(e2));
|
|
58334
|
+
}
|
|
58327
58335
|
const fallbackSuggestions = this.getFallbackSuggestions(stagedFiles);
|
|
58328
58336
|
return fallbackSuggestions.map((s2) => ({ ...s2, isFallback: true }));
|
|
58329
58337
|
}
|
|
@@ -58363,7 +58371,15 @@ class GroqService {
|
|
|
58363
58371
|
}
|
|
58364
58372
|
return this.parseSuggestions(content);
|
|
58365
58373
|
} catch (e2) {
|
|
58366
|
-
|
|
58374
|
+
const error = e2;
|
|
58375
|
+
if (error?.status === 401) {
|
|
58376
|
+
console.warn(`
|
|
58377
|
+
\x1B[33m⚠️ Groq API Key is invalid.\x1B[0m`);
|
|
58378
|
+
console.warn(` Using static backup suggestions. Run \x1B[36mbetter-commit config\x1B[0m to set your key.
|
|
58379
|
+
`);
|
|
58380
|
+
} else {
|
|
58381
|
+
console.error("Error generation suggestions:", error?.message || String(e2));
|
|
58382
|
+
}
|
|
58367
58383
|
const fallbackSuggestions = this.getFallbackSuggestions(stagedFiles);
|
|
58368
58384
|
return fallbackSuggestions.map((s2) => ({ ...s2, isFallback: true }));
|
|
58369
58385
|
}
|
|
@@ -58510,11 +58526,13 @@ Output ONLY valid JSON.`;
|
|
|
58510
58526
|
if (jsonMatch) {
|
|
58511
58527
|
const parsed = JSON.parse(jsonMatch[0]);
|
|
58512
58528
|
if (parsed.suggestions && Array.isArray(parsed.suggestions)) {
|
|
58513
|
-
|
|
58514
|
-
|
|
58515
|
-
|
|
58516
|
-
|
|
58517
|
-
|
|
58529
|
+
if (parsed.suggestions && Array.isArray(parsed.suggestions)) {
|
|
58530
|
+
return parsed.suggestions.map((s2) => ({
|
|
58531
|
+
message: s2.message || "",
|
|
58532
|
+
type: s2.type || this.extractType(s2.message || ""),
|
|
58533
|
+
description: s2.description || s2.message || ""
|
|
58534
|
+
})).slice(0, 4);
|
|
58535
|
+
}
|
|
58518
58536
|
}
|
|
58519
58537
|
}
|
|
58520
58538
|
throw new Error("Invalid JSON structure");
|
|
@@ -58689,7 +58707,7 @@ var CommitSuggestions = ({
|
|
|
58689
58707
|
}, 80);
|
|
58690
58708
|
return () => clearInterval(timer);
|
|
58691
58709
|
}
|
|
58692
|
-
}, [isLoading]);
|
|
58710
|
+
}, [isLoading, spinnerFrames.length]);
|
|
58693
58711
|
use_input_default((input, key) => {
|
|
58694
58712
|
if (key.upArrow) {
|
|
58695
58713
|
const newIndex = selectedIndex > 0 ? selectedIndex - 1 : totalOptions - 1;
|
|
@@ -58933,12 +58951,16 @@ var BetterCommitApp = ({
|
|
|
58933
58951
|
if (successMessage) {
|
|
58934
58952
|
write("\x1B[2J\x1B[0f");
|
|
58935
58953
|
const timer = setTimeout(() => {
|
|
58936
|
-
onExit(successMessage);
|
|
58937
58954
|
exit();
|
|
58938
58955
|
}, 1500);
|
|
58939
58956
|
return () => clearTimeout(timer);
|
|
58940
58957
|
}
|
|
58941
|
-
}, [successMessage,
|
|
58958
|
+
}, [successMessage, exit, write]);
|
|
58959
|
+
import_react24.useEffect(() => {
|
|
58960
|
+
if (state.error) {
|
|
58961
|
+
exit();
|
|
58962
|
+
}
|
|
58963
|
+
}, [state.error, exit]);
|
|
58942
58964
|
use_input_default((input, key) => {
|
|
58943
58965
|
if (key.escape || key.ctrl && input === "c") {
|
|
58944
58966
|
onExit("Operation cancelled");
|
|
@@ -58952,7 +58974,7 @@ var BetterCommitApp = ({
|
|
|
58952
58974
|
try {
|
|
58953
58975
|
const stagedFiles = await gitService.getStagedFiles();
|
|
58954
58976
|
await gitService.getDiff();
|
|
58955
|
-
await
|
|
58977
|
+
await fetchSuggestions(stagedFiles);
|
|
58956
58978
|
setState((prev) => ({
|
|
58957
58979
|
...prev,
|
|
58958
58980
|
stagedFiles
|
|
@@ -58966,11 +58988,11 @@ var BetterCommitApp = ({
|
|
|
58966
58988
|
};
|
|
58967
58989
|
initializeApp();
|
|
58968
58990
|
}, []);
|
|
58969
|
-
const
|
|
58991
|
+
const fetchSuggestions = async (stagedFiles, customPrompt) => {
|
|
58970
58992
|
if (!config.groqApiKey || config.groqApiKey.trim() === "") {
|
|
58971
58993
|
setState((prev) => ({
|
|
58972
58994
|
...prev,
|
|
58973
|
-
error: 'Groq API key not configured. Run "better-commit
|
|
58995
|
+
error: 'Groq API key not configured. Run "better-commit config" to set it up.',
|
|
58974
58996
|
isLoading: false
|
|
58975
58997
|
}));
|
|
58976
58998
|
return;
|
|
@@ -58980,7 +59002,12 @@ var BetterCommitApp = ({
|
|
|
58980
59002
|
const diff2 = await gitService.getDiff();
|
|
58981
59003
|
const diffStats = await gitService.getDiffStats();
|
|
58982
59004
|
const recentCommits = await gitService.getRecentCommits(config.maxHistoryCommits || 50);
|
|
58983
|
-
|
|
59005
|
+
let suggestions;
|
|
59006
|
+
if (customPrompt) {
|
|
59007
|
+
suggestions = await groqService.generateCommitSuggestionsFromCustomInput(stagedFiles, diff2, customPrompt, recentCommits, diffStats);
|
|
59008
|
+
} else {
|
|
59009
|
+
suggestions = await groqService.generateCommitSuggestions(stagedFiles, diff2, recentCommits, diffStats);
|
|
59010
|
+
}
|
|
58984
59011
|
const hasFallback = suggestions.some((s2) => s2.isFallback);
|
|
58985
59012
|
setState((prev) => ({
|
|
58986
59013
|
...prev,
|
|
@@ -59016,7 +59043,7 @@ var BetterCommitApp = ({
|
|
|
59016
59043
|
};
|
|
59017
59044
|
const handleTryAgain = () => {
|
|
59018
59045
|
setState((prev) => ({ ...prev, isLoading: true, suggestions: [] }));
|
|
59019
|
-
|
|
59046
|
+
fetchSuggestions(state.stagedFiles);
|
|
59020
59047
|
};
|
|
59021
59048
|
const handleCustomInput = () => {
|
|
59022
59049
|
setIsCustomInputMode(true);
|
|
@@ -59034,82 +59061,26 @@ var BetterCommitApp = ({
|
|
|
59034
59061
|
}
|
|
59035
59062
|
setIsCustomInputMode(false);
|
|
59036
59063
|
setState((prev) => ({ ...prev, isLoading: true, suggestions: [] }));
|
|
59037
|
-
await
|
|
59038
|
-
};
|
|
59039
|
-
const generateSuggestionsFromCustomInput = async (userInput) => {
|
|
59040
|
-
if (!config.groqApiKey || config.groqApiKey.trim() === "") {
|
|
59041
|
-
setState((prev) => ({
|
|
59042
|
-
...prev,
|
|
59043
|
-
error: 'Groq API key not configured. Run "better-commit --config" to set it up.',
|
|
59044
|
-
isLoading: false
|
|
59045
|
-
}));
|
|
59046
|
-
return;
|
|
59047
|
-
}
|
|
59048
|
-
try {
|
|
59049
|
-
const groqService = new GroqService(config.groqApiKey, config);
|
|
59050
|
-
const diff2 = await gitService.getDiff();
|
|
59051
|
-
const diffStats = await gitService.getDiffStats();
|
|
59052
|
-
const recentCommits = await gitService.getRecentCommits(config.maxHistoryCommits || 50);
|
|
59053
|
-
const suggestions = await groqService.generateCommitSuggestionsFromCustomInput(state.stagedFiles, diff2, userInput, recentCommits, diffStats);
|
|
59054
|
-
const hasFallback = suggestions.some((s2) => s2.isFallback);
|
|
59055
|
-
setState((prev) => ({
|
|
59056
|
-
...prev,
|
|
59057
|
-
suggestions,
|
|
59058
|
-
isLoading: false,
|
|
59059
|
-
error: undefined
|
|
59060
|
-
}));
|
|
59061
|
-
setIsUsingFallback(hasFallback);
|
|
59062
|
-
} catch (error) {
|
|
59063
|
-
setState((prev) => ({
|
|
59064
|
-
...prev,
|
|
59065
|
-
error: `Failed to generate suggestions: ${error instanceof Error ? error.message : String(error)}`,
|
|
59066
|
-
isLoading: false
|
|
59067
|
-
}));
|
|
59068
|
-
}
|
|
59064
|
+
await fetchSuggestions(state.stagedFiles, customInput.trim());
|
|
59069
59065
|
};
|
|
59070
59066
|
if (state.error) {
|
|
59071
59067
|
return /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
|
|
59072
|
-
|
|
59068
|
+
borderStyle: "single",
|
|
59069
|
+
borderColor: "#ef4444",
|
|
59073
59070
|
padding: 2,
|
|
59071
|
+
marginBottom: 1,
|
|
59074
59072
|
children: [
|
|
59073
|
+
/* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
|
|
59074
|
+
color: "#ef4444",
|
|
59075
|
+
bold: true,
|
|
59076
|
+
children: "Error"
|
|
59077
|
+
}, undefined, false, undefined, this),
|
|
59075
59078
|
/* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
|
|
59076
|
-
|
|
59077
|
-
borderColor: "#ef4444",
|
|
59078
|
-
padding: 2,
|
|
59079
|
-
marginBottom: 1,
|
|
59080
|
-
children: [
|
|
59081
|
-
/* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
|
|
59082
|
-
color: "#ef4444",
|
|
59083
|
-
bold: true,
|
|
59084
|
-
children: "Error"
|
|
59085
|
-
}, undefined, false, undefined, this),
|
|
59086
|
-
/* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
|
|
59087
|
-
marginTop: 1,
|
|
59088
|
-
children: /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
|
|
59089
|
-
color: "#e5e7eb",
|
|
59090
|
-
children: state.error
|
|
59091
|
-
}, undefined, false, undefined, this)
|
|
59092
|
-
}, undefined, false, undefined, this)
|
|
59093
|
-
]
|
|
59094
|
-
}, undefined, true, undefined, this),
|
|
59095
|
-
/* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
|
|
59079
|
+
marginTop: 1,
|
|
59096
59080
|
children: /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
|
|
59097
|
-
color: "#
|
|
59098
|
-
children:
|
|
59099
|
-
|
|
59100
|
-
/* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
|
|
59101
|
-
color: "#ef4444",
|
|
59102
|
-
children: "Esc"
|
|
59103
|
-
}, undefined, false, undefined, this),
|
|
59104
|
-
" or",
|
|
59105
|
-
" ",
|
|
59106
|
-
/* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
|
|
59107
|
-
color: "#ef4444",
|
|
59108
|
-
children: "Ctrl+C"
|
|
59109
|
-
}, undefined, false, undefined, this),
|
|
59110
|
-
" to exit"
|
|
59111
|
-
]
|
|
59112
|
-
}, undefined, true, undefined, this)
|
|
59081
|
+
color: "#e5e7eb",
|
|
59082
|
+
children: state.error
|
|
59083
|
+
}, undefined, false, undefined, this)
|
|
59113
59084
|
}, undefined, false, undefined, this)
|
|
59114
59085
|
]
|
|
59115
59086
|
}, undefined, true, undefined, this);
|
|
@@ -59171,7 +59142,7 @@ var BetterCommitApp = ({
|
|
|
59171
59142
|
/* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
|
|
59172
59143
|
bold: true,
|
|
59173
59144
|
color: "#8b5cf6",
|
|
59174
|
-
children: "Better
|
|
59145
|
+
children: "Better-Commit"
|
|
59175
59146
|
}, undefined, false, undefined, this),
|
|
59176
59147
|
/* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
|
|
59177
59148
|
marginTop: 1,
|
|
@@ -59193,7 +59164,7 @@ var BetterCommitApp = ({
|
|
|
59193
59164
|
/* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
|
|
59194
59165
|
bold: true,
|
|
59195
59166
|
color: "#8b5cf6",
|
|
59196
|
-
children: "Better
|
|
59167
|
+
children: "Better-Commit"
|
|
59197
59168
|
}, undefined, false, undefined, this),
|
|
59198
59169
|
/* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
|
|
59199
59170
|
color: "#6b7280",
|
|
@@ -59477,7 +59448,18 @@ var ConfigApp = ({ onExit }) => {
|
|
|
59477
59448
|
const [config, setConfig] = import_react26.useState(configManager.getConfig());
|
|
59478
59449
|
const [activeDialog, setActiveDialog] = import_react26.useState(null);
|
|
59479
59450
|
const [menuIndex, setMenuIndex] = import_react26.useState(0);
|
|
59480
|
-
const
|
|
59451
|
+
const modelOptions = import_react26.useMemo(() => [
|
|
59452
|
+
{
|
|
59453
|
+
display: "llama-3.1-8b-instant (fastest)",
|
|
59454
|
+
value: "llama-3.1-8b-instant"
|
|
59455
|
+
},
|
|
59456
|
+
{
|
|
59457
|
+
display: "llama-3.3-70b-versatile (most capable)",
|
|
59458
|
+
value: "llama-3.3-70b-versatile"
|
|
59459
|
+
},
|
|
59460
|
+
{ display: "openai/gpt-oss-20b (balanced)", value: "openai/gpt-oss-20b" }
|
|
59461
|
+
], []);
|
|
59462
|
+
const menuItems = import_react26.useMemo(() => [
|
|
59481
59463
|
{
|
|
59482
59464
|
key: "groqApiKey",
|
|
59483
59465
|
label: "Groq API Key",
|
|
@@ -59487,11 +59469,7 @@ var ConfigApp = ({ onExit }) => {
|
|
|
59487
59469
|
key: "model",
|
|
59488
59470
|
label: "AI Model",
|
|
59489
59471
|
type: "select",
|
|
59490
|
-
options:
|
|
59491
|
-
"llama-3.1-8b-instant",
|
|
59492
|
-
"llama-3.3-70b-versatile",
|
|
59493
|
-
"openai/gpt-oss-20b"
|
|
59494
|
-
]
|
|
59472
|
+
options: modelOptions.map((m2) => m2.display)
|
|
59495
59473
|
},
|
|
59496
59474
|
{
|
|
59497
59475
|
key: "commitStyle",
|
|
@@ -59504,8 +59482,8 @@ var ConfigApp = ({ onExit }) => {
|
|
|
59504
59482
|
label: "Custom Prompt",
|
|
59505
59483
|
type: "textarea"
|
|
59506
59484
|
}
|
|
59507
|
-
];
|
|
59508
|
-
const saveAndExit = () => {
|
|
59485
|
+
], [modelOptions]);
|
|
59486
|
+
const saveAndExit = import_react26.useCallback(() => {
|
|
59509
59487
|
const finalConfig = {
|
|
59510
59488
|
...config,
|
|
59511
59489
|
maxHistoryCommits: 40,
|
|
@@ -59514,11 +59492,11 @@ var ConfigApp = ({ onExit }) => {
|
|
|
59514
59492
|
configManager.updateConfig(finalConfig);
|
|
59515
59493
|
onExit("Configuration saved");
|
|
59516
59494
|
exit();
|
|
59517
|
-
};
|
|
59518
|
-
const cancelAndExit = () => {
|
|
59495
|
+
}, [config, onExit, exit]);
|
|
59496
|
+
const cancelAndExit = import_react26.useCallback(() => {
|
|
59519
59497
|
onExit("Configuration cancelled");
|
|
59520
59498
|
exit();
|
|
59521
|
-
};
|
|
59499
|
+
}, [onExit, exit]);
|
|
59522
59500
|
const handleInput = import_react26.useCallback((input, key) => {
|
|
59523
59501
|
if (activeDialog)
|
|
59524
59502
|
return;
|
|
@@ -59543,11 +59521,18 @@ var ConfigApp = ({ onExit }) => {
|
|
|
59543
59521
|
} else if (key.escape || key.ctrl && input === "c") {
|
|
59544
59522
|
cancelAndExit();
|
|
59545
59523
|
}
|
|
59546
|
-
}, [activeDialog, menuIndex, menuItems]);
|
|
59524
|
+
}, [activeDialog, menuIndex, menuItems, saveAndExit, cancelAndExit]);
|
|
59547
59525
|
use_input_default(handleInput, { isActive: !activeDialog });
|
|
59548
59526
|
const handleDialogSubmit = (value) => {
|
|
59549
59527
|
if (activeDialog) {
|
|
59550
|
-
|
|
59528
|
+
let finalValue = value;
|
|
59529
|
+
if (activeDialog.key === "model") {
|
|
59530
|
+
const modelOption = modelOptions.find((m2) => m2.display === value);
|
|
59531
|
+
if (modelOption) {
|
|
59532
|
+
finalValue = modelOption.value;
|
|
59533
|
+
}
|
|
59534
|
+
}
|
|
59535
|
+
setConfig((prev) => ({ ...prev, [activeDialog.key]: finalValue }));
|
|
59551
59536
|
setActiveDialog(null);
|
|
59552
59537
|
}
|
|
59553
59538
|
};
|
|
@@ -59573,6 +59558,15 @@ var ConfigApp = ({ onExit }) => {
|
|
|
59573
59558
|
backgroundColor: bgColor,
|
|
59574
59559
|
children: "••••••••"
|
|
59575
59560
|
}, undefined, false, undefined, this);
|
|
59561
|
+
if (key === "model") {
|
|
59562
|
+
const modelOption = modelOptions.find((m2) => m2.value === val);
|
|
59563
|
+
const displayValue = modelOption ? modelOption.display : val;
|
|
59564
|
+
return /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(Text, {
|
|
59565
|
+
color: textColor,
|
|
59566
|
+
backgroundColor: bgColor,
|
|
59567
|
+
children: displayValue
|
|
59568
|
+
}, undefined, false, undefined, this);
|
|
59569
|
+
}
|
|
59576
59570
|
if (key === "customPrompt")
|
|
59577
59571
|
return /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(Text, {
|
|
59578
59572
|
color: textColor,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sncommit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "AI-powered git commit message generator with beautiful TUI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -53,4 +53,4 @@
|
|
|
53
53
|
"node": ">=18.0.0",
|
|
54
54
|
"bun": ">=1.0.0"
|
|
55
55
|
}
|
|
56
|
-
}
|
|
56
|
+
}
|
package/src/components/App.tsx
CHANGED
|
@@ -40,12 +40,18 @@ export const BetterCommitApp: React.FC<AppProps> = ({
|
|
|
40
40
|
if (successMessage) {
|
|
41
41
|
write("\x1B[2J\x1B[0f");
|
|
42
42
|
const timer = setTimeout(() => {
|
|
43
|
-
onExit(successMessage);
|
|
44
43
|
exit();
|
|
45
44
|
}, 1500);
|
|
46
45
|
return () => clearTimeout(timer);
|
|
47
46
|
}
|
|
48
|
-
}, [successMessage,
|
|
47
|
+
}, [successMessage, exit, write]);
|
|
48
|
+
|
|
49
|
+
// Auto-exit when error occurs
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (state.error) {
|
|
52
|
+
exit();
|
|
53
|
+
}
|
|
54
|
+
}, [state.error, exit]);
|
|
49
55
|
|
|
50
56
|
// Handle global exit keys (disabled when custom input is active)
|
|
51
57
|
useInput(
|
|
@@ -69,7 +75,7 @@ export const BetterCommitApp: React.FC<AppProps> = ({
|
|
|
69
75
|
|
|
70
76
|
await gitService.getDiff();
|
|
71
77
|
|
|
72
|
-
await
|
|
78
|
+
await fetchSuggestions(stagedFiles);
|
|
73
79
|
|
|
74
80
|
setState((prev) => ({
|
|
75
81
|
...prev,
|
|
@@ -87,12 +93,15 @@ export const BetterCommitApp: React.FC<AppProps> = ({
|
|
|
87
93
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
88
94
|
}, []);
|
|
89
95
|
|
|
90
|
-
const
|
|
96
|
+
const fetchSuggestions = async (
|
|
97
|
+
stagedFiles: GitFile[],
|
|
98
|
+
customPrompt?: string,
|
|
99
|
+
) => {
|
|
91
100
|
if (!config.groqApiKey || config.groqApiKey.trim() === "") {
|
|
92
101
|
setState((prev) => ({
|
|
93
102
|
...prev,
|
|
94
103
|
error:
|
|
95
|
-
'Groq API key not configured. Run "better-commit
|
|
104
|
+
'Groq API key not configured. Run "better-commit config" to set it up.',
|
|
96
105
|
isLoading: false,
|
|
97
106
|
}));
|
|
98
107
|
return;
|
|
@@ -106,12 +115,24 @@ export const BetterCommitApp: React.FC<AppProps> = ({
|
|
|
106
115
|
config.maxHistoryCommits || 50,
|
|
107
116
|
);
|
|
108
117
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
let suggestions;
|
|
119
|
+
if (customPrompt) {
|
|
120
|
+
suggestions =
|
|
121
|
+
await groqService.generateCommitSuggestionsFromCustomInput(
|
|
122
|
+
stagedFiles,
|
|
123
|
+
diff,
|
|
124
|
+
customPrompt,
|
|
125
|
+
recentCommits,
|
|
126
|
+
diffStats,
|
|
127
|
+
);
|
|
128
|
+
} else {
|
|
129
|
+
suggestions = await groqService.generateCommitSuggestions(
|
|
130
|
+
stagedFiles,
|
|
131
|
+
diff,
|
|
132
|
+
recentCommits,
|
|
133
|
+
diffStats,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
115
136
|
|
|
116
137
|
const hasFallback = suggestions.some((s) => s.isFallback);
|
|
117
138
|
setState((prev) => ({
|
|
@@ -151,7 +172,7 @@ export const BetterCommitApp: React.FC<AppProps> = ({
|
|
|
151
172
|
|
|
152
173
|
const handleTryAgain = () => {
|
|
153
174
|
setState((prev) => ({ ...prev, isLoading: true, suggestions: [] }));
|
|
154
|
-
|
|
175
|
+
fetchSuggestions(state.stagedFiles);
|
|
155
176
|
};
|
|
156
177
|
|
|
157
178
|
const handleCustomInput = () => {
|
|
@@ -173,76 +194,23 @@ export const BetterCommitApp: React.FC<AppProps> = ({
|
|
|
173
194
|
|
|
174
195
|
setIsCustomInputMode(false);
|
|
175
196
|
setState((prev) => ({ ...prev, isLoading: true, suggestions: [] }));
|
|
176
|
-
await
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
const generateSuggestionsFromCustomInput = async (userInput: string) => {
|
|
180
|
-
if (!config.groqApiKey || config.groqApiKey.trim() === "") {
|
|
181
|
-
setState((prev) => ({
|
|
182
|
-
...prev,
|
|
183
|
-
error:
|
|
184
|
-
'Groq API key not configured. Run "better-commit --config" to set it up.',
|
|
185
|
-
isLoading: false,
|
|
186
|
-
}));
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
const groqService = new GroqService(config.groqApiKey, config);
|
|
192
|
-
const diff = await gitService.getDiff();
|
|
193
|
-
const diffStats = await gitService.getDiffStats();
|
|
194
|
-
const recentCommits = await gitService.getRecentCommits(
|
|
195
|
-
config.maxHistoryCommits || 50,
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
const suggestions =
|
|
199
|
-
await groqService.generateCommitSuggestionsFromCustomInput(
|
|
200
|
-
state.stagedFiles,
|
|
201
|
-
diff,
|
|
202
|
-
userInput,
|
|
203
|
-
recentCommits,
|
|
204
|
-
diffStats,
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
const hasFallback = suggestions.some((s) => s.isFallback);
|
|
208
|
-
setState((prev) => ({
|
|
209
|
-
...prev,
|
|
210
|
-
suggestions,
|
|
211
|
-
isLoading: false,
|
|
212
|
-
error: undefined,
|
|
213
|
-
}));
|
|
214
|
-
setIsUsingFallback(hasFallback);
|
|
215
|
-
} catch (error) {
|
|
216
|
-
setState((prev) => ({
|
|
217
|
-
...prev,
|
|
218
|
-
error: `Failed to generate suggestions: ${error instanceof Error ? error.message : String(error)}`,
|
|
219
|
-
isLoading: false,
|
|
220
|
-
}));
|
|
221
|
-
}
|
|
197
|
+
await fetchSuggestions(state.stagedFiles, customInput.trim());
|
|
222
198
|
};
|
|
223
199
|
|
|
224
200
|
// Error State
|
|
225
201
|
if (state.error) {
|
|
226
202
|
return (
|
|
227
|
-
<Box
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
>
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
<
|
|
238
|
-
<Text color="#e5e7eb">{state.error}</Text>
|
|
239
|
-
</Box>
|
|
240
|
-
</Box>
|
|
241
|
-
<Box>
|
|
242
|
-
<Text color="#6b7280">
|
|
243
|
-
Press <Text color="#ef4444">Esc</Text> or{" "}
|
|
244
|
-
<Text color="#ef4444">Ctrl+C</Text> to exit
|
|
245
|
-
</Text>
|
|
203
|
+
<Box
|
|
204
|
+
borderStyle="single"
|
|
205
|
+
borderColor="#ef4444"
|
|
206
|
+
padding={2}
|
|
207
|
+
marginBottom={1}
|
|
208
|
+
>
|
|
209
|
+
<Text color="#ef4444" bold>
|
|
210
|
+
Error
|
|
211
|
+
</Text>
|
|
212
|
+
<Box marginTop={1}>
|
|
213
|
+
<Text color="#e5e7eb">{state.error}</Text>
|
|
246
214
|
</Box>
|
|
247
215
|
</Box>
|
|
248
216
|
);
|
|
@@ -290,7 +258,7 @@ export const BetterCommitApp: React.FC<AppProps> = ({
|
|
|
290
258
|
return (
|
|
291
259
|
<Box flexDirection="column" padding={2} justifyContent="center">
|
|
292
260
|
<Text bold color="#8b5cf6">
|
|
293
|
-
Better
|
|
261
|
+
Better-Commit
|
|
294
262
|
</Text>
|
|
295
263
|
<Box marginTop={1}>
|
|
296
264
|
<Text color="#6b7280">Loading staged files...</Text>
|
|
@@ -304,7 +272,7 @@ export const BetterCommitApp: React.FC<AppProps> = ({
|
|
|
304
272
|
{/* Header */}
|
|
305
273
|
<Box marginBottom={1}>
|
|
306
274
|
<Text bold color="#8b5cf6">
|
|
307
|
-
Better
|
|
275
|
+
Better-Commit
|
|
308
276
|
</Text>
|
|
309
277
|
<Text color="#6b7280"> • AI-Powered Commit Suggestions</Text>
|
|
310
278
|
</Box>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useCallback } from "react";
|
|
1
|
+
import React, { useState, useCallback, useMemo } from "react";
|
|
2
2
|
import { Box, Text, useInput, useApp, Key } from "ink";
|
|
3
3
|
import { configManager } from "../utils/config";
|
|
4
4
|
import { Config } from "../types";
|
|
@@ -21,36 +21,50 @@ export const ConfigApp: React.FC<ConfigAppProps> = ({ onExit }) => {
|
|
|
21
21
|
|
|
22
22
|
const [menuIndex, setMenuIndex] = useState(0);
|
|
23
23
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
],
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
key: "commitStyle" as keyof Config,
|
|
42
|
-
label: "Commit Style",
|
|
43
|
-
type: "select" as const,
|
|
44
|
-
options: ["conventional", "simple", "detailed"],
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
key: "customPrompt" as keyof Config,
|
|
48
|
-
label: "Custom Prompt",
|
|
49
|
-
type: "textarea" as const,
|
|
50
|
-
},
|
|
51
|
-
];
|
|
24
|
+
const modelOptions = useMemo(
|
|
25
|
+
() => [
|
|
26
|
+
{
|
|
27
|
+
display: "llama-3.1-8b-instant (fastest)",
|
|
28
|
+
value: "llama-3.1-8b-instant",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
display: "llama-3.3-70b-versatile (most capable)",
|
|
32
|
+
value: "llama-3.3-70b-versatile",
|
|
33
|
+
},
|
|
34
|
+
{ display: "openai/gpt-oss-20b (balanced)", value: "openai/gpt-oss-20b" },
|
|
35
|
+
],
|
|
36
|
+
[],
|
|
37
|
+
);
|
|
52
38
|
|
|
53
|
-
const
|
|
39
|
+
const menuItems = useMemo(
|
|
40
|
+
() => [
|
|
41
|
+
{
|
|
42
|
+
key: "groqApiKey" as keyof Config,
|
|
43
|
+
label: "Groq API Key",
|
|
44
|
+
type: "password" as const,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: "model" as keyof Config,
|
|
48
|
+
label: "AI Model",
|
|
49
|
+
type: "select" as const,
|
|
50
|
+
options: modelOptions.map((m) => m.display),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
key: "commitStyle" as keyof Config,
|
|
54
|
+
label: "Commit Style",
|
|
55
|
+
type: "select" as const,
|
|
56
|
+
options: ["conventional", "simple", "detailed"],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: "customPrompt" as keyof Config,
|
|
60
|
+
label: "Custom Prompt",
|
|
61
|
+
type: "textarea" as const,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
[modelOptions],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const saveAndExit = useCallback(() => {
|
|
54
68
|
const finalConfig = {
|
|
55
69
|
...config,
|
|
56
70
|
// model was hardcoded here previously, now we let user select it
|
|
@@ -61,12 +75,12 @@ export const ConfigApp: React.FC<ConfigAppProps> = ({ onExit }) => {
|
|
|
61
75
|
configManager.updateConfig(finalConfig);
|
|
62
76
|
onExit("Configuration saved");
|
|
63
77
|
exit();
|
|
64
|
-
};
|
|
78
|
+
}, [config, onExit, exit]);
|
|
65
79
|
|
|
66
|
-
const cancelAndExit = () => {
|
|
80
|
+
const cancelAndExit = useCallback(() => {
|
|
67
81
|
onExit("Configuration cancelled");
|
|
68
82
|
exit();
|
|
69
|
-
};
|
|
83
|
+
}, [onExit, exit]);
|
|
70
84
|
|
|
71
85
|
const handleInput = useCallback(
|
|
72
86
|
(input: string, key: Key) => {
|
|
@@ -93,16 +107,25 @@ export const ConfigApp: React.FC<ConfigAppProps> = ({ onExit }) => {
|
|
|
93
107
|
} else if (key.escape || (key.ctrl && input === "c")) {
|
|
94
108
|
cancelAndExit();
|
|
95
109
|
}
|
|
96
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
97
110
|
},
|
|
98
|
-
[activeDialog, menuIndex, menuItems],
|
|
111
|
+
[activeDialog, menuIndex, menuItems, saveAndExit, cancelAndExit],
|
|
99
112
|
);
|
|
100
113
|
|
|
101
114
|
useInput(handleInput, { isActive: !activeDialog });
|
|
102
115
|
|
|
103
116
|
const handleDialogSubmit = (value: string) => {
|
|
104
117
|
if (activeDialog) {
|
|
105
|
-
|
|
118
|
+
let finalValue = value;
|
|
119
|
+
|
|
120
|
+
// Convert display label back to actual model ID
|
|
121
|
+
if (activeDialog.key === "model") {
|
|
122
|
+
const modelOption = modelOptions.find((m) => m.display === value);
|
|
123
|
+
if (modelOption) {
|
|
124
|
+
finalValue = modelOption.value;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
setConfig((prev) => ({ ...prev, [activeDialog.key]: finalValue }));
|
|
106
129
|
setActiveDialog(null);
|
|
107
130
|
}
|
|
108
131
|
};
|
|
@@ -129,6 +152,16 @@ export const ConfigApp: React.FC<ConfigAppProps> = ({ onExit }) => {
|
|
|
129
152
|
••••••••
|
|
130
153
|
</Text>
|
|
131
154
|
);
|
|
155
|
+
if (key === "model") {
|
|
156
|
+
// Display the friendly label instead of the raw model ID
|
|
157
|
+
const modelOption = modelOptions.find((m) => m.value === val);
|
|
158
|
+
const displayValue = modelOption ? modelOption.display : val;
|
|
159
|
+
return (
|
|
160
|
+
<Text color={textColor} backgroundColor={bgColor}>
|
|
161
|
+
{displayValue}
|
|
162
|
+
</Text>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
132
165
|
if (key === "customPrompt")
|
|
133
166
|
return (
|
|
134
167
|
<Text color={textColor} backgroundColor={bgColor}>
|
package/src/services/groq.ts
CHANGED
|
@@ -53,9 +53,21 @@ export class GroqService {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
return this.parseSuggestions(content);
|
|
56
|
-
} catch (e) {
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
} catch (e: unknown) {
|
|
57
|
+
const error = e as { status?: number; message?: string };
|
|
58
|
+
if (error?.status === 401) {
|
|
59
|
+
console.warn("\n\x1b[33m⚠️ Groq API Key is invalid.\x1b[0m");
|
|
60
|
+
console.warn(
|
|
61
|
+
" Using static backup suggestions. Run \x1b[36mbetter-commit config\x1b[0m to set your key.\n",
|
|
62
|
+
);
|
|
63
|
+
} else {
|
|
64
|
+
console.error(
|
|
65
|
+
"Error generation suggestions:",
|
|
66
|
+
error?.message || String(e),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Fallback to mock suggestions when API fails
|
|
59
71
|
const fallbackSuggestions = this.getFallbackSuggestions(stagedFiles);
|
|
60
72
|
return fallbackSuggestions.map((s) => ({ ...s, isFallback: true }));
|
|
61
73
|
}
|
|
@@ -110,8 +122,19 @@ export class GroqService {
|
|
|
110
122
|
}
|
|
111
123
|
|
|
112
124
|
return this.parseSuggestions(content);
|
|
113
|
-
} catch (e) {
|
|
114
|
-
|
|
125
|
+
} catch (e: unknown) {
|
|
126
|
+
const error = e as { status?: number; message?: string };
|
|
127
|
+
if (error?.status === 401) {
|
|
128
|
+
console.warn("\n\x1b[33m⚠️ Groq API Key is invalid.\x1b[0m");
|
|
129
|
+
console.warn(
|
|
130
|
+
" Using static backup suggestions. Run \x1b[36mbetter-commit config\x1b[0m to set your key.\n",
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
console.error(
|
|
134
|
+
"Error generation suggestions:",
|
|
135
|
+
error?.message || String(e),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
115
138
|
// Fallback to mock suggestions when API fails (silently handle)
|
|
116
139
|
const fallbackSuggestions = this.getFallbackSuggestions(stagedFiles);
|
|
117
140
|
return fallbackSuggestions.map((s) => ({ ...s, isFallback: true }));
|
|
@@ -178,18 +201,20 @@ IMPORTANT: Carefully analyze the git diff below to understand WHAT changes were
|
|
|
178
201
|
Files staged for commit:
|
|
179
202
|
${filesText}${statsText}
|
|
180
203
|
|
|
181
|
-
${
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
204
|
+
${
|
|
205
|
+
diff
|
|
206
|
+
? `Complete git diff (analyze this carefully to understand the actual changes):\n${diff.slice(
|
|
207
|
+
0,
|
|
208
|
+
2000,
|
|
209
|
+
)}\n${diff.length > 2000 ? "...(truncated)" : ""}`
|
|
210
|
+
: "No diff available"
|
|
211
|
+
}
|
|
188
212
|
|
|
189
|
-
${
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
213
|
+
${
|
|
214
|
+
recentCommitsText
|
|
215
|
+
? `Recent commit history for reference:\n${recentCommitsText}`
|
|
216
|
+
: ""
|
|
217
|
+
}
|
|
193
218
|
|
|
194
219
|
Requirements:
|
|
195
220
|
- Generate exactly 4 different commit messages
|
|
@@ -206,9 +231,9 @@ Output ONLY valid JSON.`;
|
|
|
206
231
|
const fileNames =
|
|
207
232
|
stagedFiles.length > 0
|
|
208
233
|
? stagedFiles
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
234
|
+
.map((f) => f.path)
|
|
235
|
+
.slice(0, 3)
|
|
236
|
+
.join(", ") + (stagedFiles.length > 3 ? "..." : "")
|
|
212
237
|
: "files";
|
|
213
238
|
|
|
214
239
|
const suggestions = [
|
|
@@ -294,18 +319,20 @@ IMPORTANT: Carefully analyze the git diff below to understand WHAT changes were
|
|
|
294
319
|
Files staged for commit:
|
|
295
320
|
${filesText}${statsText}
|
|
296
321
|
|
|
297
|
-
${
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
322
|
+
${
|
|
323
|
+
diff
|
|
324
|
+
? `Complete git diff (analyze this carefully to understand the actual changes):\n${diff.slice(
|
|
325
|
+
0,
|
|
326
|
+
2000,
|
|
327
|
+
)}\n${diff.length > 2000 ? "...(truncated)" : ""}`
|
|
328
|
+
: "No diff available"
|
|
329
|
+
}
|
|
304
330
|
|
|
305
|
-
${
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
331
|
+
${
|
|
332
|
+
recentCommitsText
|
|
333
|
+
? `Recent commit history for reference:\n${recentCommitsText}`
|
|
334
|
+
: ""
|
|
335
|
+
}
|
|
309
336
|
|
|
310
337
|
Requirements:
|
|
311
338
|
- Generate exactly 4 different commit messages
|
|
@@ -325,16 +352,29 @@ Output ONLY valid JSON.`;
|
|
|
325
352
|
if (jsonMatch) {
|
|
326
353
|
const parsed = JSON.parse(jsonMatch[0]);
|
|
327
354
|
if (parsed.suggestions && Array.isArray(parsed.suggestions)) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
355
|
+
if (parsed.suggestions && Array.isArray(parsed.suggestions)) {
|
|
356
|
+
return parsed.suggestions
|
|
357
|
+
.map(
|
|
358
|
+
(s: {
|
|
359
|
+
message?: string;
|
|
360
|
+
type?: string;
|
|
361
|
+
description?: string;
|
|
362
|
+
}) => ({
|
|
363
|
+
message: s.message || "",
|
|
364
|
+
type: s.type || this.extractType(s.message || ""),
|
|
365
|
+
description: s.description || s.message || "",
|
|
366
|
+
}),
|
|
367
|
+
)
|
|
368
|
+
.slice(0, 4);
|
|
369
|
+
}
|
|
333
370
|
}
|
|
334
371
|
}
|
|
335
372
|
throw new Error("Invalid JSON structure");
|
|
336
373
|
} catch (e) {
|
|
337
|
-
console.warn(
|
|
374
|
+
console.warn(
|
|
375
|
+
"Failed to parse JSON suggestions, falling back to basic parsing:",
|
|
376
|
+
e,
|
|
377
|
+
);
|
|
338
378
|
// Fallback to basic line parsing if JSON fails
|
|
339
379
|
const lines = content.split("\n").filter((line) => line.trim());
|
|
340
380
|
const suggestions: CommitSuggestion[] = [];
|