sncommit 1.0.0 → 1.0.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 -1
- package/dist/index.js +93 -99
- package/package.json +5 -2
- package/bun.lock +0 -705
- package/eslint.config.mjs +0 -34
- package/src/components/App.tsx +0 -357
- package/src/components/CommitSuggestions.tsx +0 -157
- package/src/components/ConfigApp.tsx +0 -258
- package/src/components/CustomInputPrompt.tsx +0 -82
- package/src/components/StagedFiles.tsx +0 -52
- package/src/components/TuiDialog.tsx +0 -177
- package/src/index.tsx +0 -150
- package/src/services/git.ts +0 -128
- package/src/services/groq.ts +0 -360
- package/src/suppress-warnings.ts +0 -18
- package/src/types/index.ts +0 -36
- package/src/utils/config.ts +0 -66
- package/tsconfig.json +0 -19
package/src/index.tsx
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import "./suppress-warnings";
|
|
4
|
-
|
|
5
|
-
import { Command } from "commander";
|
|
6
|
-
import { render } from "ink";
|
|
7
|
-
import { App as BetterCommitApp } from "./components/App";
|
|
8
|
-
import { ConfigApp } from "./components/ConfigApp";
|
|
9
|
-
|
|
10
|
-
// Handle raw mode errors gracefully
|
|
11
|
-
const handleRawModeError = (error: Error, fallbackMessage: string) => {
|
|
12
|
-
if (error.message.includes("Raw mode is not supported")) {
|
|
13
|
-
console.log(fallbackMessage);
|
|
14
|
-
process.exit(0);
|
|
15
|
-
}
|
|
16
|
-
throw error;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const program = new Command();
|
|
20
|
-
|
|
21
|
-
program
|
|
22
|
-
.name("better-commit")
|
|
23
|
-
.description("AI-powered git commit message generator with beautiful TUI")
|
|
24
|
-
.version("1.0.0");
|
|
25
|
-
|
|
26
|
-
// Add options
|
|
27
|
-
program.option("-a, --all", "stage all files before committing");
|
|
28
|
-
|
|
29
|
-
// Subcommand for config
|
|
30
|
-
program
|
|
31
|
-
.command("config")
|
|
32
|
-
.description("Configure better-commit settings")
|
|
33
|
-
.action(async () => {
|
|
34
|
-
// Check TTY support first before doing anything
|
|
35
|
-
if (!process.stdin.isTTY) {
|
|
36
|
-
console.log("Configuration interface not supported in this terminal.");
|
|
37
|
-
console.log("Please try running in a compatible terminal like:");
|
|
38
|
-
console.log("- Windows Terminal");
|
|
39
|
-
console.log("- PowerShell with proper TTY support");
|
|
40
|
-
console.log("- Git Bash");
|
|
41
|
-
console.log("- WSL terminal");
|
|
42
|
-
process.exit(0);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
let exitMessage = "";
|
|
48
|
-
const { waitUntilExit } = render(
|
|
49
|
-
<ConfigApp
|
|
50
|
-
onExit={(message) => {
|
|
51
|
-
if (message) exitMessage = message;
|
|
52
|
-
}}
|
|
53
|
-
/>,
|
|
54
|
-
{
|
|
55
|
-
stdout: process.stdout,
|
|
56
|
-
stdin: process.stdin,
|
|
57
|
-
patchConsole: true,
|
|
58
|
-
exitOnCtrlC: false,
|
|
59
|
-
},
|
|
60
|
-
);
|
|
61
|
-
await waitUntilExit();
|
|
62
|
-
if (exitMessage) console.log(exitMessage);
|
|
63
|
-
process.exit(0);
|
|
64
|
-
} catch (error) {
|
|
65
|
-
handleRawModeError(
|
|
66
|
-
error as Error,
|
|
67
|
-
"Configuration interface not supported in this terminal.",
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// Default action - commit
|
|
73
|
-
program.action(async () => {
|
|
74
|
-
const options = program.opts();
|
|
75
|
-
|
|
76
|
-
// Check TTY support first before doing any git operations
|
|
77
|
-
if (!process.stdin.isTTY) {
|
|
78
|
-
console.log("Interactive interface not supported in this terminal.");
|
|
79
|
-
console.log("Please try running in a compatible terminal like:");
|
|
80
|
-
console.log("- Windows Terminal");
|
|
81
|
-
console.log("- PowerShell with proper TTY support");
|
|
82
|
-
console.log("- Git Bash");
|
|
83
|
-
console.log("- WSL terminal");
|
|
84
|
-
process.exit(0);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
try {
|
|
89
|
-
// Check git status first to provide fallback behavior
|
|
90
|
-
const gitService = await import("./services/git").then(
|
|
91
|
-
(m) => new m.GitService(),
|
|
92
|
-
);
|
|
93
|
-
const isGitRepo = await gitService.isGitRepository();
|
|
94
|
-
if (!isGitRepo) {
|
|
95
|
-
console.log(
|
|
96
|
-
"Error: Not a git repository. Please run this command from inside a git repository.",
|
|
97
|
-
);
|
|
98
|
-
process.exit(0);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (options.all) {
|
|
103
|
-
const hasUnstaged = await gitService.hasUnstagedChanges();
|
|
104
|
-
if (hasUnstaged) {
|
|
105
|
-
await gitService.stageAll();
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const hasStaged = await gitService.hasStagedChanges();
|
|
110
|
-
if (!hasStaged) {
|
|
111
|
-
const hasUnstaged = await gitService.hasUnstagedChanges();
|
|
112
|
-
if (hasUnstaged) {
|
|
113
|
-
console.log(
|
|
114
|
-
'No staged files. Use "git add ." or run "better-commit -a" to stage all files.',
|
|
115
|
-
);
|
|
116
|
-
} else {
|
|
117
|
-
console.log("No changes to commit. Working tree is clean.");
|
|
118
|
-
}
|
|
119
|
-
process.exit(0);
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Only try to render if we have staged files
|
|
124
|
-
let exitMessage = "";
|
|
125
|
-
const { waitUntilExit } = render(
|
|
126
|
-
<BetterCommitApp
|
|
127
|
-
addAll={options.all || false}
|
|
128
|
-
onExit={(message) => {
|
|
129
|
-
if (message) exitMessage = message;
|
|
130
|
-
}}
|
|
131
|
-
/>,
|
|
132
|
-
{
|
|
133
|
-
stdout: process.stdout,
|
|
134
|
-
stdin: process.stdin,
|
|
135
|
-
patchConsole: true,
|
|
136
|
-
exitOnCtrlC: false,
|
|
137
|
-
},
|
|
138
|
-
);
|
|
139
|
-
await waitUntilExit();
|
|
140
|
-
if (exitMessage) console.log(exitMessage);
|
|
141
|
-
process.exit(0);
|
|
142
|
-
} catch (error) {
|
|
143
|
-
handleRawModeError(
|
|
144
|
-
error as Error,
|
|
145
|
-
"Interactive interface not supported in this terminal. Please try running in a compatible terminal.",
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
program.parse();
|
package/src/services/git.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import simpleGit, { SimpleGit } from "simple-git";
|
|
2
|
-
import { GitFile, GitCommit } from "../types";
|
|
3
|
-
|
|
4
|
-
export class GitService {
|
|
5
|
-
private git: SimpleGit;
|
|
6
|
-
|
|
7
|
-
constructor() {
|
|
8
|
-
this.git = simpleGit();
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
async isGitRepository(): Promise<boolean> {
|
|
12
|
-
try {
|
|
13
|
-
return await this.git.checkIsRepo();
|
|
14
|
-
} catch {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async getStagedFiles(): Promise<GitFile[]> {
|
|
20
|
-
try {
|
|
21
|
-
const status = await this.git.status();
|
|
22
|
-
const stagedFiles: GitFile[] = [];
|
|
23
|
-
|
|
24
|
-
// Add staged files
|
|
25
|
-
status.staged.forEach((file) => {
|
|
26
|
-
stagedFiles.push({
|
|
27
|
-
path: file,
|
|
28
|
-
status: "staged",
|
|
29
|
-
isStaged: true,
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
return stagedFiles;
|
|
34
|
-
} catch (error) {
|
|
35
|
-
throw new Error(`Failed to get staged files: ${error}`);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async getRecentCommits(limit: number = 50): Promise<GitCommit[]> {
|
|
40
|
-
try {
|
|
41
|
-
const log = await this.git.log({ maxCount: limit });
|
|
42
|
-
|
|
43
|
-
return log.all.map((commit) => ({
|
|
44
|
-
hash: commit.hash,
|
|
45
|
-
message: commit.message,
|
|
46
|
-
author: commit.author_name,
|
|
47
|
-
date: commit.date,
|
|
48
|
-
}));
|
|
49
|
-
} catch {
|
|
50
|
-
// Silently handle git history errors - don't show them to user
|
|
51
|
-
// This is common in new repositories with no commits
|
|
52
|
-
return [];
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async commit(message: string): Promise<void> {
|
|
57
|
-
try {
|
|
58
|
-
await this.git.commit(message);
|
|
59
|
-
} catch (error) {
|
|
60
|
-
throw new Error(`Failed to commit: ${error}`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async getDiff(): Promise<string> {
|
|
65
|
-
try {
|
|
66
|
-
const diff = await this.git.diff(["--cached"]);
|
|
67
|
-
return diff;
|
|
68
|
-
} catch {
|
|
69
|
-
// Silently fail - don't interfere with TUI
|
|
70
|
-
return "";
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async getDiffStats(): Promise<{
|
|
75
|
-
added: number;
|
|
76
|
-
deleted: number;
|
|
77
|
-
modified: number;
|
|
78
|
-
renamed: number;
|
|
79
|
-
files: string[];
|
|
80
|
-
}> {
|
|
81
|
-
try {
|
|
82
|
-
const status = await this.git.status();
|
|
83
|
-
const diffStats = await this.git.diffSummary(["--cached"]);
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
added: diffStats.insertions,
|
|
87
|
-
deleted: diffStats.deletions,
|
|
88
|
-
modified: status.modified.length,
|
|
89
|
-
renamed: status.renamed.length,
|
|
90
|
-
files: status.staged,
|
|
91
|
-
};
|
|
92
|
-
} catch {
|
|
93
|
-
// Silently fail - don't interfere with TUI
|
|
94
|
-
return { added: 0, deleted: 0, modified: 0, renamed: 0, files: [] };
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async hasStagedChanges(): Promise<boolean> {
|
|
99
|
-
try {
|
|
100
|
-
const status = await this.git.status();
|
|
101
|
-
return status.staged.length > 0;
|
|
102
|
-
} catch {
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async stageAll(): Promise<void> {
|
|
108
|
-
try {
|
|
109
|
-
await this.git.add(".");
|
|
110
|
-
} catch {
|
|
111
|
-
throw new Error(`Failed to stage all files`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async hasUnstagedChanges(): Promise<boolean> {
|
|
116
|
-
try {
|
|
117
|
-
const status = await this.git.status();
|
|
118
|
-
return (
|
|
119
|
-
status.not_added.length > 0 ||
|
|
120
|
-
status.modified.length > 0 ||
|
|
121
|
-
status.created.length > 0 ||
|
|
122
|
-
status.deleted.length > 0
|
|
123
|
-
);
|
|
124
|
-
} catch {
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
package/src/services/groq.ts
DELETED
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
import Groq from "groq-sdk";
|
|
2
|
-
import { CommitSuggestion, GitFile, GitCommit, Config } from "../types";
|
|
3
|
-
|
|
4
|
-
export class GroqService {
|
|
5
|
-
private client: Groq;
|
|
6
|
-
private config: Config;
|
|
7
|
-
|
|
8
|
-
constructor(apiKey: string, config: Config) {
|
|
9
|
-
this.client = new Groq({ apiKey });
|
|
10
|
-
this.config = config;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async generateCommitSuggestions(
|
|
14
|
-
stagedFiles: GitFile[],
|
|
15
|
-
diff: string,
|
|
16
|
-
recentCommits: GitCommit[] = [],
|
|
17
|
-
_diffStats?: {
|
|
18
|
-
added: number;
|
|
19
|
-
deleted: number;
|
|
20
|
-
modified: number;
|
|
21
|
-
renamed: number;
|
|
22
|
-
files: string[];
|
|
23
|
-
},
|
|
24
|
-
): Promise<CommitSuggestion[]> {
|
|
25
|
-
const prompt = this.buildPrompt(stagedFiles, diff, recentCommits);
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
const response = await this.client.chat.completions.create({
|
|
29
|
-
model: this.config.model || "llama-3.1-8b-instant",
|
|
30
|
-
messages: [
|
|
31
|
-
{
|
|
32
|
-
role: "system",
|
|
33
|
-
content:
|
|
34
|
-
"You are an expert at writing clear, concise, and meaningful git commit messages.\n" +
|
|
35
|
-
"Generate commit messages that follow best practices and are helpful for future developers.\n" +
|
|
36
|
-
"IMPORTANT: Output your response in strict JSON format with the following schema:\n" +
|
|
37
|
-
'{\n "suggestions": [\n {\n "message": "commit message here",\n "type": "feat/fix/etc",\n "description": "brief explanation"\n }\n ]\n}',
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
role: "user",
|
|
41
|
-
content: prompt,
|
|
42
|
-
},
|
|
43
|
-
],
|
|
44
|
-
// response_format: { type: "json_object" }, // Not all models support this, so we rely on system prompt + parsing
|
|
45
|
-
max_tokens: 2048, // Increased for reasoning models
|
|
46
|
-
temperature: 0.7,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const content = response.choices[0]?.message?.content;
|
|
50
|
-
|
|
51
|
-
if (!content) {
|
|
52
|
-
throw new Error("No response from Groq API");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return this.parseSuggestions(content);
|
|
56
|
-
} catch (e) {
|
|
57
|
-
console.error("Error generating suggestions:", e);
|
|
58
|
-
// Fallback to mock suggestions when API fails (silently handle)
|
|
59
|
-
const fallbackSuggestions = this.getFallbackSuggestions(stagedFiles);
|
|
60
|
-
return fallbackSuggestions.map((s) => ({ ...s, isFallback: true }));
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async generateCommitSuggestionsFromCustomInput(
|
|
65
|
-
stagedFiles: GitFile[],
|
|
66
|
-
diff: string,
|
|
67
|
-
userInput: string,
|
|
68
|
-
recentCommits: GitCommit[] = [],
|
|
69
|
-
diffStats?: {
|
|
70
|
-
added: number;
|
|
71
|
-
deleted: number;
|
|
72
|
-
modified: number;
|
|
73
|
-
renamed: number;
|
|
74
|
-
files: string[];
|
|
75
|
-
},
|
|
76
|
-
): Promise<CommitSuggestion[]> {
|
|
77
|
-
const prompt = this.buildPromptFromCustomInput(
|
|
78
|
-
stagedFiles,
|
|
79
|
-
diff,
|
|
80
|
-
userInput,
|
|
81
|
-
recentCommits,
|
|
82
|
-
diffStats,
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
const response = await this.client.chat.completions.create({
|
|
87
|
-
model: this.config.model || "llama-3.1-8b-instant",
|
|
88
|
-
messages: [
|
|
89
|
-
{
|
|
90
|
-
role: "system",
|
|
91
|
-
content:
|
|
92
|
-
"You are an expert at writing clear, concise, and meaningful git commit messages.\n" +
|
|
93
|
-
"Generate commit messages that follow best practices and are helpful for future developers.\n" +
|
|
94
|
-
"IMPORTANT: Output your response in strict JSON format with the following schema:\n" +
|
|
95
|
-
'{\n "suggestions": [\n {\n "message": "commit message here",\n "type": "feat/fix/etc",\n "description": "brief explanation"\n }\n ]\n}',
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
role: "user",
|
|
99
|
-
content: prompt,
|
|
100
|
-
},
|
|
101
|
-
],
|
|
102
|
-
// response_format: { type: "json_object" },
|
|
103
|
-
max_tokens: 2048,
|
|
104
|
-
temperature: 0.7,
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const content = response.choices[0]?.message?.content;
|
|
108
|
-
if (!content) {
|
|
109
|
-
throw new Error("No response from Groq API");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return this.parseSuggestions(content);
|
|
113
|
-
} catch (e) {
|
|
114
|
-
console.error("Error generating suggestions:", e);
|
|
115
|
-
// Fallback to mock suggestions when API fails (silently handle)
|
|
116
|
-
const fallbackSuggestions = this.getFallbackSuggestions(stagedFiles);
|
|
117
|
-
return fallbackSuggestions.map((s) => ({ ...s, isFallback: true }));
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private buildPromptFromCustomInput(
|
|
122
|
-
stagedFiles: GitFile[],
|
|
123
|
-
diff: string,
|
|
124
|
-
userInput: string,
|
|
125
|
-
recentCommits: GitCommit[],
|
|
126
|
-
diffStats?: {
|
|
127
|
-
added: number;
|
|
128
|
-
deleted: number;
|
|
129
|
-
modified: number;
|
|
130
|
-
renamed: number;
|
|
131
|
-
files: string[];
|
|
132
|
-
},
|
|
133
|
-
): string {
|
|
134
|
-
const filesText = stagedFiles.map((f) => `- ${f.path}`).join("\n");
|
|
135
|
-
const recentCommitsText = recentCommits
|
|
136
|
-
.slice(0, 5)
|
|
137
|
-
.map((c) => `- ${c.message}`)
|
|
138
|
-
.join("\n");
|
|
139
|
-
|
|
140
|
-
let styleInstruction = "";
|
|
141
|
-
switch (this.config.commitStyle) {
|
|
142
|
-
case "conventional":
|
|
143
|
-
styleInstruction =
|
|
144
|
-
"Use conventional commit format (feat:, fix:, docs:, etc.)";
|
|
145
|
-
break;
|
|
146
|
-
case "simple":
|
|
147
|
-
styleInstruction = "Keep messages simple and concise";
|
|
148
|
-
break;
|
|
149
|
-
case "detailed":
|
|
150
|
-
styleInstruction = "Include detailed descriptions of changes";
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const customPrompt = this.config.customPrompt
|
|
155
|
-
? `\nAdditional instructions: ${this.config.customPrompt}`
|
|
156
|
-
: "";
|
|
157
|
-
|
|
158
|
-
const statsText = diffStats
|
|
159
|
-
? `
|
|
160
|
-
Change statistics:
|
|
161
|
-
- ${diffStats.added} lines added
|
|
162
|
-
- ${diffStats.deleted} lines deleted
|
|
163
|
-
- ${diffStats.modified} files modified
|
|
164
|
-
- ${diffStats.renamed} files renamed
|
|
165
|
-
- ${diffStats.files.length} total files changed`
|
|
166
|
-
: "";
|
|
167
|
-
|
|
168
|
-
return `The user wants commit messages based on their description: "${userInput}"
|
|
169
|
-
|
|
170
|
-
Analyze the following git changes and generate 4 different commit message suggestions that match the user's intent.
|
|
171
|
-
|
|
172
|
-
IMPORTANT: Carefully analyze the git diff below to understand WHAT changes were actually made:
|
|
173
|
-
- Look for added/removed/modified lines (lines starting with +, -, or @@)
|
|
174
|
-
- Identify if files were added, deleted, or modified
|
|
175
|
-
- Understand the nature of the changes (new features, bug fixes, refactoring, etc.)
|
|
176
|
-
- Match the user's description: "${userInput}"
|
|
177
|
-
|
|
178
|
-
Files staged for commit:
|
|
179
|
-
${filesText}${statsText}
|
|
180
|
-
|
|
181
|
-
${diff
|
|
182
|
-
? `Complete git diff (analyze this carefully to understand the actual changes):\n${diff.slice(
|
|
183
|
-
0,
|
|
184
|
-
2000,
|
|
185
|
-
)}\n${diff.length > 2000 ? "...(truncated)" : ""}`
|
|
186
|
-
: "No diff available"
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
${recentCommitsText
|
|
190
|
-
? `Recent commit history for reference:\n${recentCommitsText}`
|
|
191
|
-
: ""
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
Requirements:
|
|
195
|
-
- Generate exactly 4 different commit messages
|
|
196
|
-
- Each message should be 50-72 characters
|
|
197
|
-
- ${styleInstruction}
|
|
198
|
-
- Make them specific to the actual changes shown AND match the user's intent: "${userInput}"
|
|
199
|
-
- Focus on what changed, not how it changed
|
|
200
|
-
${customPrompt}
|
|
201
|
-
|
|
202
|
-
Output ONLY valid JSON.`;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
private getFallbackSuggestions(stagedFiles: GitFile[]): CommitSuggestion[] {
|
|
206
|
-
const fileNames =
|
|
207
|
-
stagedFiles.length > 0
|
|
208
|
-
? stagedFiles
|
|
209
|
-
.map((f) => f.path)
|
|
210
|
-
.slice(0, 3)
|
|
211
|
-
.join(", ") + (stagedFiles.length > 3 ? "..." : "")
|
|
212
|
-
: "files";
|
|
213
|
-
|
|
214
|
-
const suggestions = [
|
|
215
|
-
{
|
|
216
|
-
message: `feat: add ${fileNames}`,
|
|
217
|
-
type: "feat",
|
|
218
|
-
description: `feat: add ${fileNames}`,
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
message: `fix: update ${fileNames}`,
|
|
222
|
-
type: "fix",
|
|
223
|
-
description: `fix: update ${fileNames}`,
|
|
224
|
-
},
|
|
225
|
-
{
|
|
226
|
-
message: `refactor: improve ${fileNames}`,
|
|
227
|
-
type: "refactor",
|
|
228
|
-
description: `refactor: improve ${fileNames}`,
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
message: `docs: update ${fileNames}`,
|
|
232
|
-
type: "docs",
|
|
233
|
-
description: `docs: update ${fileNames}`,
|
|
234
|
-
},
|
|
235
|
-
];
|
|
236
|
-
|
|
237
|
-
return suggestions;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
private buildPrompt(
|
|
241
|
-
stagedFiles: GitFile[],
|
|
242
|
-
diff: string,
|
|
243
|
-
recentCommits: GitCommit[],
|
|
244
|
-
diffStats?: {
|
|
245
|
-
added: number;
|
|
246
|
-
deleted: number;
|
|
247
|
-
modified: number;
|
|
248
|
-
renamed: number;
|
|
249
|
-
files: string[];
|
|
250
|
-
},
|
|
251
|
-
): string {
|
|
252
|
-
const filesText = stagedFiles.map((f) => `- ${f.path}`).join("\n");
|
|
253
|
-
const recentCommitsText = recentCommits
|
|
254
|
-
.slice(0, 5)
|
|
255
|
-
.map((c) => `- ${c.message}`)
|
|
256
|
-
.join("\n");
|
|
257
|
-
|
|
258
|
-
let styleInstruction = "";
|
|
259
|
-
switch (this.config.commitStyle) {
|
|
260
|
-
case "conventional":
|
|
261
|
-
styleInstruction =
|
|
262
|
-
"Use conventional commit format (feat:, fix:, docs:, etc.)";
|
|
263
|
-
break;
|
|
264
|
-
case "simple":
|
|
265
|
-
styleInstruction = "Keep messages simple and concise";
|
|
266
|
-
break;
|
|
267
|
-
case "detailed":
|
|
268
|
-
styleInstruction = "Include detailed descriptions of changes";
|
|
269
|
-
break;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const customPrompt = this.config.customPrompt
|
|
273
|
-
? `\nAdditional instructions: ${this.config.customPrompt}`
|
|
274
|
-
: "";
|
|
275
|
-
|
|
276
|
-
const statsText = diffStats
|
|
277
|
-
? `
|
|
278
|
-
Change statistics:
|
|
279
|
-
- ${diffStats.added} lines added
|
|
280
|
-
- ${diffStats.deleted} lines deleted
|
|
281
|
-
- ${diffStats.modified} files modified
|
|
282
|
-
- ${diffStats.renamed} files renamed
|
|
283
|
-
- ${diffStats.files.length} total files changed`
|
|
284
|
-
: "";
|
|
285
|
-
|
|
286
|
-
return `Analyze the following git changes and generate 4 different commit message suggestions.
|
|
287
|
-
|
|
288
|
-
IMPORTANT: Carefully analyze the git diff below to understand WHAT changes were actually made:
|
|
289
|
-
- Look for added/removed/modified lines (lines starting with +, -, or @@)
|
|
290
|
-
- Identify if files were added, deleted, or modified
|
|
291
|
-
- Understand the nature of the changes (new features, bug fixes, refactoring, etc.)
|
|
292
|
-
- DO NOT assume all changes are "add" operations - check the diff carefully
|
|
293
|
-
|
|
294
|
-
Files staged for commit:
|
|
295
|
-
${filesText}${statsText}
|
|
296
|
-
|
|
297
|
-
${diff
|
|
298
|
-
? `Complete git diff (analyze this carefully to understand the actual changes):\n${diff.slice(
|
|
299
|
-
0,
|
|
300
|
-
2000,
|
|
301
|
-
)}\n${diff.length > 2000 ? "...(truncated)" : ""}`
|
|
302
|
-
: "No diff available"
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
${recentCommitsText
|
|
306
|
-
? `Recent commit history for reference:\n${recentCommitsText}`
|
|
307
|
-
: ""
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
Requirements:
|
|
311
|
-
- Generate exactly 4 different commit messages
|
|
312
|
-
- Each message should be 50-72 characters
|
|
313
|
-
- ${styleInstruction}
|
|
314
|
-
- Make them specific to the actual changes shown
|
|
315
|
-
- Focus on what changed, not how it changed
|
|
316
|
-
${customPrompt}
|
|
317
|
-
|
|
318
|
-
Output ONLY valid JSON.`;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
private parseSuggestions(content: string): CommitSuggestion[] {
|
|
322
|
-
try {
|
|
323
|
-
// Find the JSON object in the content (it might be wrapped in markdown or have extra text)
|
|
324
|
-
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
325
|
-
if (jsonMatch) {
|
|
326
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
327
|
-
if (parsed.suggestions && Array.isArray(parsed.suggestions)) {
|
|
328
|
-
return parsed.suggestions.map((s: any) => ({
|
|
329
|
-
message: s.message || "",
|
|
330
|
-
type: s.type || this.extractType(s.message || ""),
|
|
331
|
-
description: s.description || s.message || "",
|
|
332
|
-
})).slice(0, 4);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
throw new Error("Invalid JSON structure");
|
|
336
|
-
} catch (e) {
|
|
337
|
-
console.warn("Failed to parse JSON suggestions, falling back to basic parsing:", e);
|
|
338
|
-
// Fallback to basic line parsing if JSON fails
|
|
339
|
-
const lines = content.split("\n").filter((line) => line.trim());
|
|
340
|
-
const suggestions: CommitSuggestion[] = [];
|
|
341
|
-
|
|
342
|
-
for (const line of lines) {
|
|
343
|
-
const match = line.match(/^\d+\.\s*(.+)$/);
|
|
344
|
-
if (match && suggestions.length < 4) {
|
|
345
|
-
suggestions.push({
|
|
346
|
-
message: match[1].trim(),
|
|
347
|
-
type: this.extractType(match[1]),
|
|
348
|
-
description: match[1].trim(),
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
return suggestions;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
private extractType(message: string): string {
|
|
357
|
-
const match = message.match(/^(\w+):/);
|
|
358
|
-
return match ? match[1] : "feat";
|
|
359
|
-
}
|
|
360
|
-
}
|
package/src/suppress-warnings.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// Suppress punycode and url.parse deprecation warnings
|
|
2
|
-
const originalEmit = process.emit;
|
|
3
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
-
process.emit = function (name: any, data: any, ...args: any[]): boolean {
|
|
5
|
-
if (
|
|
6
|
-
name === "warning" &&
|
|
7
|
-
typeof data === "object" &&
|
|
8
|
-
data &&
|
|
9
|
-
data.name === "DeprecationWarning" &&
|
|
10
|
-
data.message &&
|
|
11
|
-
(data.message.includes("punycode") || data.message.includes("url.parse"))
|
|
12
|
-
) {
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
16
|
-
return (originalEmit as Function).apply(process, [name, data, ...args]);
|
|
17
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
-
} as any;
|
package/src/types/index.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
export interface Config {
|
|
2
|
-
groqApiKey?: string;
|
|
3
|
-
model?: string;
|
|
4
|
-
maxHistoryCommits?: number;
|
|
5
|
-
commitStyle?: "conventional" | "simple" | "detailed";
|
|
6
|
-
language?: string;
|
|
7
|
-
customPrompt?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface GitFile {
|
|
11
|
-
path: string;
|
|
12
|
-
status: string;
|
|
13
|
-
isStaged: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface CommitSuggestion {
|
|
17
|
-
message: string;
|
|
18
|
-
type?: string;
|
|
19
|
-
description?: string;
|
|
20
|
-
isFallback?: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface GitCommit {
|
|
24
|
-
hash: string;
|
|
25
|
-
message: string;
|
|
26
|
-
author: string;
|
|
27
|
-
date: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface AppState {
|
|
31
|
-
stagedFiles: GitFile[];
|
|
32
|
-
suggestions: CommitSuggestion[];
|
|
33
|
-
selectedIndex: number;
|
|
34
|
-
isLoading: boolean;
|
|
35
|
-
error?: string;
|
|
36
|
-
}
|