commit-agent-cli 0.2.0 → 0.2.2
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 -3
- package/dist/index.js +241 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# commit-agent-cli
|
|
2
2
|
|
|
3
|
-
> AI-powered git commit message generator using Claude Sonnet 4.5 / Opus 4.5 or Google Gemini and LangGraph
|
|
4
|
-
|
|
5
3
|
Generate intelligent, context-aware commit messages by simply typing `commit`.
|
|
6
4
|
|
|
7
5
|
## Quick Start
|
|
@@ -27,7 +25,7 @@ You'll be prompted to:
|
|
|
27
25
|
|
|
28
26
|
## Features
|
|
29
27
|
|
|
30
|
-
Powered by Claude
|
|
28
|
+
Powered by Claude and Gemini, this tool intelligently explores your codebase to generate clear, context-aware commit messages with full transparency. Enjoy customizable commit styles, secure local configuration, and seamless switching between AI providers.
|
|
31
29
|
|
|
32
30
|
## Documentation
|
|
33
31
|
|
package/dist/index.js
CHANGED
|
@@ -100,7 +100,7 @@ var require_picocolors = __commonJS({
|
|
|
100
100
|
|
|
101
101
|
// src/index.ts
|
|
102
102
|
import "dotenv/config";
|
|
103
|
-
import { intro, outro, text, spinner, isCancel, cancel, note, select } from "@clack/prompts";
|
|
103
|
+
import { intro, outro, text, spinner, isCancel, cancel, note, select, multiselect } from "@clack/prompts";
|
|
104
104
|
|
|
105
105
|
// src/git.ts
|
|
106
106
|
import { execa } from "execa";
|
|
@@ -154,6 +154,50 @@ async function isGitRepository() {
|
|
|
154
154
|
return false;
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
+
async function getStagedFiles() {
|
|
158
|
+
const { stdout } = await execa("git", ["diff", "--cached", "--name-only"], {
|
|
159
|
+
reject: false
|
|
160
|
+
});
|
|
161
|
+
return stdout ? stdout.split("\n").filter(Boolean) : [];
|
|
162
|
+
}
|
|
163
|
+
async function getUnstagedFiles() {
|
|
164
|
+
const { stdout } = await execa("git", ["diff", "--name-only"], {
|
|
165
|
+
reject: false
|
|
166
|
+
});
|
|
167
|
+
return stdout ? stdout.split("\n").filter(Boolean) : [];
|
|
168
|
+
}
|
|
169
|
+
async function getUntrackedFiles() {
|
|
170
|
+
const { stdout } = await execa("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
171
|
+
reject: false
|
|
172
|
+
});
|
|
173
|
+
return stdout ? stdout.split("\n").filter(Boolean) : [];
|
|
174
|
+
}
|
|
175
|
+
async function getRecentCommits(count = 5) {
|
|
176
|
+
const { stdout } = await execa("git", ["log", `-${count}`, "--pretty=format:%h|%s"], {
|
|
177
|
+
reject: false
|
|
178
|
+
});
|
|
179
|
+
if (!stdout) return [];
|
|
180
|
+
return stdout.split("\n").map((line) => {
|
|
181
|
+
const [hash, ...messageParts] = line.split("|");
|
|
182
|
+
return { hash, message: messageParts.join("|") };
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
async function getStagedStats() {
|
|
186
|
+
const { stdout: stats } = await execa("git", ["diff", "--cached", "--stat"], {
|
|
187
|
+
reject: false
|
|
188
|
+
});
|
|
189
|
+
if (!stats) return { files: 0, insertions: 0, deletions: 0 };
|
|
190
|
+
const match = stats.match(/(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?/);
|
|
191
|
+
return {
|
|
192
|
+
files: match ? parseInt(match[1]) : 0,
|
|
193
|
+
insertions: match && match[2] ? parseInt(match[2]) : 0,
|
|
194
|
+
deletions: match && match[3] ? parseInt(match[3]) : 0
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
async function stageFiles(files) {
|
|
198
|
+
if (files.length === 0) return;
|
|
199
|
+
await execa("git", ["add", ...files]);
|
|
200
|
+
}
|
|
157
201
|
|
|
158
202
|
// src/agent.ts
|
|
159
203
|
import { ChatAnthropic } from "@langchain/anthropic";
|
|
@@ -445,6 +489,7 @@ import updateNotifier from "update-notifier";
|
|
|
445
489
|
import { readFileSync } from "fs";
|
|
446
490
|
import { fileURLToPath } from "url";
|
|
447
491
|
import { dirname, join as joinPath } from "path";
|
|
492
|
+
import { execa as execa3 } from "execa";
|
|
448
493
|
import { homedir } from "os";
|
|
449
494
|
import { join as join2 } from "path";
|
|
450
495
|
import { readFile as readFile2, writeFile } from "fs/promises";
|
|
@@ -455,32 +500,30 @@ var packageJson = JSON.parse(
|
|
|
455
500
|
);
|
|
456
501
|
var notifier = updateNotifier({
|
|
457
502
|
pkg: packageJson,
|
|
458
|
-
updateCheckInterval:
|
|
459
|
-
//
|
|
503
|
+
updateCheckInterval: 0
|
|
504
|
+
// Always check for updates (no cache)
|
|
460
505
|
});
|
|
461
506
|
if (notifier.update && notifier.update.current !== notifier.update.latest) {
|
|
462
507
|
const currentVersion = notifier.update.current;
|
|
463
508
|
const latestVersion = notifier.update.latest;
|
|
464
|
-
const
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
509
|
+
const line1Text = `${packageJson.name} update available! ${currentVersion} \u2192 ${latestVersion}`;
|
|
510
|
+
const line2Text = `Run npm install -g ${packageJson.name}@latest to update.`;
|
|
511
|
+
const maxLength = Math.max(line1Text.length, line2Text.length);
|
|
512
|
+
const boxWidth = maxLength + 2;
|
|
513
|
+
const padLine = (text2, visibleLength) => {
|
|
514
|
+
const padding = boxWidth - visibleLength;
|
|
515
|
+
return text2 + " ".repeat(Math.max(0, padding));
|
|
469
516
|
};
|
|
517
|
+
const line1Colored = ` ${packageJson.name} update available! ${import_picocolors.default.cyan(currentVersion)} \u2192 ${import_picocolors.default.green(import_picocolors.default.bold(latestVersion))}`;
|
|
518
|
+
const line2Colored = ` Run ${import_picocolors.default.cyan(import_picocolors.default.bold(`npm install -g ${packageJson.name}@latest`))} to update.`;
|
|
519
|
+
const horizontalBorder = "\u2500".repeat(boxWidth);
|
|
470
520
|
console.log("");
|
|
471
|
-
console.log(import_picocolors.default.yellow("\u256D\
|
|
472
|
-
console.log(import_picocolors.default.yellow("\u2502") + padLine(
|
|
473
|
-
console.log(import_picocolors.default.yellow("\u2502") + padLine(
|
|
474
|
-
console.log(import_picocolors.default.yellow("\
|
|
475
|
-
console.log(import_picocolors.default.yellow("\u2502") + padLine(" Current: " + import_picocolors.default.dim(currentVersion) + " \u2192 Latest: " + import_picocolors.default.green(import_picocolors.default.bold(latestVersion)), boxWidth) + import_picocolors.default.yellow("\u2502"));
|
|
476
|
-
console.log(import_picocolors.default.yellow("\u2502") + padLine("", boxWidth) + import_picocolors.default.yellow("\u2502"));
|
|
477
|
-
console.log(import_picocolors.default.yellow("\u2502") + padLine(" " + import_picocolors.default.dim("Update after you finish by running:"), boxWidth) + import_picocolors.default.yellow("\u2502"));
|
|
478
|
-
console.log(import_picocolors.default.yellow("\u2502") + padLine(" " + import_picocolors.default.cyan(import_picocolors.default.bold(`npm install -g ${packageJson.name}@latest`)), boxWidth) + import_picocolors.default.yellow("\u2502"));
|
|
479
|
-
console.log(import_picocolors.default.yellow("\u2502") + padLine("", boxWidth) + import_picocolors.default.yellow("\u2502"));
|
|
480
|
-
console.log(import_picocolors.default.yellow("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
|
|
521
|
+
console.log(import_picocolors.default.yellow("\u256D" + horizontalBorder + "\u256E"));
|
|
522
|
+
console.log(import_picocolors.default.yellow("\u2502") + padLine(line1Colored, line1Text.length + 1) + import_picocolors.default.yellow("\u2502"));
|
|
523
|
+
console.log(import_picocolors.default.yellow("\u2502") + padLine(line2Colored, line2Text.length + 1) + import_picocolors.default.yellow("\u2502"));
|
|
524
|
+
console.log(import_picocolors.default.yellow("\u2570" + horizontalBorder + "\u256F"));
|
|
481
525
|
console.log("");
|
|
482
526
|
}
|
|
483
|
-
notifier.notify({ defer: false, isGlobal: true });
|
|
484
527
|
var CONFIG_PATH = join2(homedir(), ".commit-cli.json");
|
|
485
528
|
var ANTHROPIC_MODELS = {
|
|
486
529
|
"claude-sonnet-4-20250514": "Claude Sonnet 4.5",
|
|
@@ -580,6 +623,43 @@ async function promptForApiKey(provider) {
|
|
|
580
623
|
return key;
|
|
581
624
|
}
|
|
582
625
|
}
|
|
626
|
+
async function showGuidedStaging() {
|
|
627
|
+
const s = spinner();
|
|
628
|
+
s.start("Checking for unstaged and untracked files...");
|
|
629
|
+
const [unstaged, untracked] = await Promise.all([
|
|
630
|
+
getUnstagedFiles(),
|
|
631
|
+
getUntrackedFiles()
|
|
632
|
+
]);
|
|
633
|
+
s.stop("Files found.");
|
|
634
|
+
const allFiles = [
|
|
635
|
+
...unstaged.map((f) => ({ file: f, status: "modified" })),
|
|
636
|
+
...untracked.map((f) => ({ file: f, status: "untracked" }))
|
|
637
|
+
];
|
|
638
|
+
if (allFiles.length === 0) {
|
|
639
|
+
note("No changes to stage. Make some changes first!", "No Changes");
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
note(`Found ${allFiles.length} file(s) with changes`, "Available Files");
|
|
643
|
+
const selectedFiles = await multiselect({
|
|
644
|
+
message: "Select files to stage (Space to select, Enter to continue):",
|
|
645
|
+
options: allFiles.map(({ file, status }) => ({
|
|
646
|
+
value: file,
|
|
647
|
+
label: `${file} (${status})`
|
|
648
|
+
})),
|
|
649
|
+
required: false
|
|
650
|
+
});
|
|
651
|
+
if (isCancel(selectedFiles)) {
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
if (!selectedFiles || selectedFiles.length === 0) {
|
|
655
|
+
note('No files selected. You can run "git add <files>" manually.', "Skipped");
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
s.start("Staging selected files...");
|
|
659
|
+
await stageFiles(selectedFiles);
|
|
660
|
+
s.stop(`Staged ${selectedFiles.length} file(s).`);
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
583
663
|
async function showSettingsMenu(currentConfig2) {
|
|
584
664
|
const providerLabel = currentConfig2.provider === "anthropic" ? "Anthropic (Claude)" : "Google (Gemini)";
|
|
585
665
|
const modelName = currentConfig2.provider === "anthropic" ? ANTHROPIC_MODELS[currentConfig2.model] || currentConfig2.model : GOOGLE_MODELS[currentConfig2.model] || currentConfig2.model;
|
|
@@ -774,14 +854,116 @@ async function main() {
|
|
|
774
854
|
note(`Preferences saved! You can change these anytime in the settings menu.`, "Setup Complete");
|
|
775
855
|
}
|
|
776
856
|
const s = spinner();
|
|
777
|
-
s.start("
|
|
778
|
-
|
|
857
|
+
s.start("Checking staged changes...");
|
|
858
|
+
let diff = await getStagedDiffSmart();
|
|
779
859
|
if (!diff) {
|
|
780
860
|
s.stop("No staged changes found.");
|
|
781
|
-
|
|
782
|
-
|
|
861
|
+
const shouldStage = await select({
|
|
862
|
+
message: "No staged changes. Would you like to stage files now?",
|
|
863
|
+
options: [
|
|
864
|
+
{ value: true, label: "Yes, show me files to stage" },
|
|
865
|
+
{ value: false, label: "No, I'll stage manually" }
|
|
866
|
+
],
|
|
867
|
+
initialValue: true
|
|
868
|
+
});
|
|
869
|
+
if (isCancel(shouldStage) || !shouldStage) {
|
|
870
|
+
cancel('Please stage your changes using "git add" first.');
|
|
871
|
+
process.exit(0);
|
|
872
|
+
}
|
|
873
|
+
const staged = await showGuidedStaging();
|
|
874
|
+
if (!staged) {
|
|
875
|
+
cancel("No files were staged. Exiting.");
|
|
876
|
+
process.exit(0);
|
|
877
|
+
}
|
|
878
|
+
s.start("Analyzing staged changes...");
|
|
879
|
+
diff = await getStagedDiffSmart();
|
|
880
|
+
if (!diff) {
|
|
881
|
+
s.stop("Still no staged changes.");
|
|
882
|
+
cancel("Something went wrong. Please try again.");
|
|
883
|
+
process.exit(0);
|
|
884
|
+
}
|
|
783
885
|
}
|
|
784
886
|
s.stop("Changes detected.");
|
|
887
|
+
const stats = await getStagedStats();
|
|
888
|
+
const stagedFiles = await getStagedFiles();
|
|
889
|
+
let previewMessage = import_picocolors.default.cyan(`${stats.files} file(s) changed`);
|
|
890
|
+
if (stats.insertions > 0) previewMessage += import_picocolors.default.green(`, ${stats.insertions} insertion(s)`);
|
|
891
|
+
if (stats.deletions > 0) previewMessage += import_picocolors.default.red(`, ${stats.deletions} deletion(s)`);
|
|
892
|
+
previewMessage += "\n" + stagedFiles.map((f) => ` \u2022 ${f}`).join("\n");
|
|
893
|
+
note(previewMessage, "Changes Summary");
|
|
894
|
+
const recentCommits = await getRecentCommits(5);
|
|
895
|
+
if (recentCommits.length > 0) {
|
|
896
|
+
const commitsMessage = recentCommits.map((c) => ` ${import_picocolors.default.dim(c.hash)} ${c.message}`).join("\n");
|
|
897
|
+
note(commitsMessage, "Recent Commits");
|
|
898
|
+
}
|
|
899
|
+
const [unstaged, untracked] = await Promise.all([
|
|
900
|
+
getUnstagedFiles(),
|
|
901
|
+
getUntrackedFiles()
|
|
902
|
+
]);
|
|
903
|
+
if (unstaged.length > 0 || untracked.length > 0) {
|
|
904
|
+
const unstagedCount = unstaged.length + untracked.length;
|
|
905
|
+
const warningMessage = `You have ${unstagedCount} unstaged file(s):
|
|
906
|
+
` + [...unstaged.map((f) => ` \u2022 ${f} (modified)`), ...untracked.map((f) => ` \u2022 ${f} (untracked)`)].join("\n");
|
|
907
|
+
note(import_picocolors.default.yellow(warningMessage), import_picocolors.default.yellow("\u26A0 Unstaged Changes"));
|
|
908
|
+
const includeUnstaged = await select({
|
|
909
|
+
message: "Include these files in this commit?",
|
|
910
|
+
options: [
|
|
911
|
+
{ value: false, label: "No, continue with current staging" },
|
|
912
|
+
{ value: true, label: "Yes, let me select which ones" }
|
|
913
|
+
],
|
|
914
|
+
initialValue: false
|
|
915
|
+
});
|
|
916
|
+
if (!isCancel(includeUnstaged) && includeUnstaged) {
|
|
917
|
+
const additionalFiles = await multiselect({
|
|
918
|
+
message: "Select additional files to stage:",
|
|
919
|
+
options: [
|
|
920
|
+
...unstaged.map((f) => ({ value: f, label: `${f} (modified)` })),
|
|
921
|
+
...untracked.map((f) => ({ value: f, label: `${f} (untracked)` }))
|
|
922
|
+
],
|
|
923
|
+
required: false
|
|
924
|
+
});
|
|
925
|
+
if (!isCancel(additionalFiles) && additionalFiles && additionalFiles.length > 0) {
|
|
926
|
+
s.start("Staging additional files...");
|
|
927
|
+
await stageFiles(additionalFiles);
|
|
928
|
+
diff = await getStagedDiffSmart();
|
|
929
|
+
s.stop("Additional files staged.");
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
const totalChanges = stats.insertions + stats.deletions;
|
|
934
|
+
if (stats.files >= 10 || totalChanges >= 500) {
|
|
935
|
+
const largeChangesetMessage = import_picocolors.default.yellow(
|
|
936
|
+
`\u26A0 Large changeset detected (${stats.files} files, ${totalChanges} changes)
|
|
937
|
+
Consider splitting into smaller, focused commits for better review.`
|
|
938
|
+
);
|
|
939
|
+
note(largeChangesetMessage, import_picocolors.default.yellow("Large Changeset Warning"));
|
|
940
|
+
const proceedWithLarge = await select({
|
|
941
|
+
message: "How would you like to proceed?",
|
|
942
|
+
options: [
|
|
943
|
+
{ value: "continue", label: "Continue with all changes" },
|
|
944
|
+
{ value: "reselect", label: "Let me re-select files to commit" },
|
|
945
|
+
{ value: "cancel", label: "Cancel and stage manually" }
|
|
946
|
+
],
|
|
947
|
+
initialValue: "continue"
|
|
948
|
+
});
|
|
949
|
+
if (isCancel(proceedWithLarge) || proceedWithLarge === "cancel") {
|
|
950
|
+
cancel('Cancelled. Use "git add" to stage specific files.');
|
|
951
|
+
process.exit(0);
|
|
952
|
+
}
|
|
953
|
+
if (proceedWithLarge === "reselect") {
|
|
954
|
+
await execa3("git", ["reset", "HEAD"]);
|
|
955
|
+
const restaged = await showGuidedStaging();
|
|
956
|
+
if (!restaged) {
|
|
957
|
+
cancel("No files were staged. Exiting.");
|
|
958
|
+
process.exit(0);
|
|
959
|
+
}
|
|
960
|
+
diff = await getStagedDiffSmart();
|
|
961
|
+
if (!diff) {
|
|
962
|
+
cancel("No staged changes. Exiting.");
|
|
963
|
+
process.exit(0);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
785
967
|
let commitMessage = "";
|
|
786
968
|
let confirmed = false;
|
|
787
969
|
let userFeedback = void 0;
|
|
@@ -856,13 +1038,16 @@ async function main() {
|
|
|
856
1038
|
}
|
|
857
1039
|
const formattedMessage = wrappedLines.map((line) => import_picocolors.default.cyan(line)).join("\n");
|
|
858
1040
|
note(formattedMessage, import_picocolors.default.bold("Proposed Commit Message"));
|
|
1041
|
+
const isMultiLine = commitMessage.includes("\n");
|
|
1042
|
+
const actionOptions = [
|
|
1043
|
+
{ value: "commit", label: "Yes, commit" },
|
|
1044
|
+
...isMultiLine ? [] : [{ value: "edit", label: "Edit message" }],
|
|
1045
|
+
{ value: "regenerate", label: "Regenerate" },
|
|
1046
|
+
{ value: "settings", label: "Change settings" }
|
|
1047
|
+
];
|
|
859
1048
|
const action = await select({
|
|
860
1049
|
message: "Do you want to use this message?",
|
|
861
|
-
options:
|
|
862
|
-
{ value: "commit", label: "Yes, commit" },
|
|
863
|
-
{ value: "regenerate", label: "No, regenerate" },
|
|
864
|
-
{ value: "settings", label: "Change settings" }
|
|
865
|
-
]
|
|
1050
|
+
options: actionOptions
|
|
866
1051
|
});
|
|
867
1052
|
if (isCancel(action)) {
|
|
868
1053
|
cancel("Operation cancelled.");
|
|
@@ -870,6 +1055,34 @@ async function main() {
|
|
|
870
1055
|
}
|
|
871
1056
|
if (action === "commit") {
|
|
872
1057
|
confirmed = true;
|
|
1058
|
+
} else if (action === "edit") {
|
|
1059
|
+
const editedMessage = await text({
|
|
1060
|
+
message: "Edit your commit message:",
|
|
1061
|
+
initialValue: commitMessage,
|
|
1062
|
+
validate: (value) => {
|
|
1063
|
+
if (!value || value.trim() === "") return "Commit message cannot be empty";
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
if (isCancel(editedMessage)) {
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
commitMessage = editedMessage;
|
|
1070
|
+
const editedFormattedMessage = editedMessage.split("\n").map((line) => import_picocolors.default.cyan(line)).join("\n");
|
|
1071
|
+
note(editedFormattedMessage, import_picocolors.default.bold("Edited Commit Message"));
|
|
1072
|
+
const confirmEdited = await select({
|
|
1073
|
+
message: "Use this edited message?",
|
|
1074
|
+
options: [
|
|
1075
|
+
{ value: true, label: "Yes, commit" },
|
|
1076
|
+
{ value: false, label: "No, go back" }
|
|
1077
|
+
],
|
|
1078
|
+
initialValue: true
|
|
1079
|
+
});
|
|
1080
|
+
if (isCancel(confirmEdited)) {
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
if (confirmEdited) {
|
|
1084
|
+
confirmed = true;
|
|
1085
|
+
}
|
|
873
1086
|
} else if (action === "settings") {
|
|
874
1087
|
const settingsResult = await showSettingsMenu(config);
|
|
875
1088
|
if (settingsResult) {
|