sncommit 1.0.1 → 1.0.3

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/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();
@@ -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
- }
@@ -1,400 +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: 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
71
- const fallbackSuggestions = this.getFallbackSuggestions(stagedFiles);
72
- return fallbackSuggestions.map((s) => ({ ...s, isFallback: true }));
73
- }
74
- }
75
-
76
- async generateCommitSuggestionsFromCustomInput(
77
- stagedFiles: GitFile[],
78
- diff: string,
79
- userInput: string,
80
- recentCommits: GitCommit[] = [],
81
- diffStats?: {
82
- added: number;
83
- deleted: number;
84
- modified: number;
85
- renamed: number;
86
- files: string[];
87
- },
88
- ): Promise<CommitSuggestion[]> {
89
- const prompt = this.buildPromptFromCustomInput(
90
- stagedFiles,
91
- diff,
92
- userInput,
93
- recentCommits,
94
- diffStats,
95
- );
96
-
97
- try {
98
- const response = await this.client.chat.completions.create({
99
- model: this.config.model || "llama-3.1-8b-instant",
100
- messages: [
101
- {
102
- role: "system",
103
- content:
104
- "You are an expert at writing clear, concise, and meaningful git commit messages.\n" +
105
- "Generate commit messages that follow best practices and are helpful for future developers.\n" +
106
- "IMPORTANT: Output your response in strict JSON format with the following schema:\n" +
107
- '{\n "suggestions": [\n {\n "message": "commit message here",\n "type": "feat/fix/etc",\n "description": "brief explanation"\n }\n ]\n}',
108
- },
109
- {
110
- role: "user",
111
- content: prompt,
112
- },
113
- ],
114
- // response_format: { type: "json_object" },
115
- max_tokens: 2048,
116
- temperature: 0.7,
117
- });
118
-
119
- const content = response.choices[0]?.message?.content;
120
- if (!content) {
121
- throw new Error("No response from Groq API");
122
- }
123
-
124
- return this.parseSuggestions(content);
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
- }
138
- // Fallback to mock suggestions when API fails (silently handle)
139
- const fallbackSuggestions = this.getFallbackSuggestions(stagedFiles);
140
- return fallbackSuggestions.map((s) => ({ ...s, isFallback: true }));
141
- }
142
- }
143
-
144
- private buildPromptFromCustomInput(
145
- stagedFiles: GitFile[],
146
- diff: string,
147
- userInput: string,
148
- recentCommits: GitCommit[],
149
- diffStats?: {
150
- added: number;
151
- deleted: number;
152
- modified: number;
153
- renamed: number;
154
- files: string[];
155
- },
156
- ): string {
157
- const filesText = stagedFiles.map((f) => `- ${f.path}`).join("\n");
158
- const recentCommitsText = recentCommits
159
- .slice(0, 5)
160
- .map((c) => `- ${c.message}`)
161
- .join("\n");
162
-
163
- let styleInstruction = "";
164
- switch (this.config.commitStyle) {
165
- case "conventional":
166
- styleInstruction =
167
- "Use conventional commit format (feat:, fix:, docs:, etc.)";
168
- break;
169
- case "simple":
170
- styleInstruction = "Keep messages simple and concise";
171
- break;
172
- case "detailed":
173
- styleInstruction = "Include detailed descriptions of changes";
174
- break;
175
- }
176
-
177
- const customPrompt = this.config.customPrompt
178
- ? `\nAdditional instructions: ${this.config.customPrompt}`
179
- : "";
180
-
181
- const statsText = diffStats
182
- ? `
183
- Change statistics:
184
- - ${diffStats.added} lines added
185
- - ${diffStats.deleted} lines deleted
186
- - ${diffStats.modified} files modified
187
- - ${diffStats.renamed} files renamed
188
- - ${diffStats.files.length} total files changed`
189
- : "";
190
-
191
- return `The user wants commit messages based on their description: "${userInput}"
192
-
193
- Analyze the following git changes and generate 4 different commit message suggestions that match the user's intent.
194
-
195
- IMPORTANT: Carefully analyze the git diff below to understand WHAT changes were actually made:
196
- - Look for added/removed/modified lines (lines starting with +, -, or @@)
197
- - Identify if files were added, deleted, or modified
198
- - Understand the nature of the changes (new features, bug fixes, refactoring, etc.)
199
- - Match the user's description: "${userInput}"
200
-
201
- Files staged for commit:
202
- ${filesText}${statsText}
203
-
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
- }
212
-
213
- ${
214
- recentCommitsText
215
- ? `Recent commit history for reference:\n${recentCommitsText}`
216
- : ""
217
- }
218
-
219
- Requirements:
220
- - Generate exactly 4 different commit messages
221
- - Each message should be 50-72 characters
222
- - ${styleInstruction}
223
- - Make them specific to the actual changes shown AND match the user's intent: "${userInput}"
224
- - Focus on what changed, not how it changed
225
- ${customPrompt}
226
-
227
- Output ONLY valid JSON.`;
228
- }
229
-
230
- private getFallbackSuggestions(stagedFiles: GitFile[]): CommitSuggestion[] {
231
- const fileNames =
232
- stagedFiles.length > 0
233
- ? stagedFiles
234
- .map((f) => f.path)
235
- .slice(0, 3)
236
- .join(", ") + (stagedFiles.length > 3 ? "..." : "")
237
- : "files";
238
-
239
- const suggestions = [
240
- {
241
- message: `feat: add ${fileNames}`,
242
- type: "feat",
243
- description: `feat: add ${fileNames}`,
244
- },
245
- {
246
- message: `fix: update ${fileNames}`,
247
- type: "fix",
248
- description: `fix: update ${fileNames}`,
249
- },
250
- {
251
- message: `refactor: improve ${fileNames}`,
252
- type: "refactor",
253
- description: `refactor: improve ${fileNames}`,
254
- },
255
- {
256
- message: `docs: update ${fileNames}`,
257
- type: "docs",
258
- description: `docs: update ${fileNames}`,
259
- },
260
- ];
261
-
262
- return suggestions;
263
- }
264
-
265
- private buildPrompt(
266
- stagedFiles: GitFile[],
267
- diff: string,
268
- recentCommits: GitCommit[],
269
- diffStats?: {
270
- added: number;
271
- deleted: number;
272
- modified: number;
273
- renamed: number;
274
- files: string[];
275
- },
276
- ): string {
277
- const filesText = stagedFiles.map((f) => `- ${f.path}`).join("\n");
278
- const recentCommitsText = recentCommits
279
- .slice(0, 5)
280
- .map((c) => `- ${c.message}`)
281
- .join("\n");
282
-
283
- let styleInstruction = "";
284
- switch (this.config.commitStyle) {
285
- case "conventional":
286
- styleInstruction =
287
- "Use conventional commit format (feat:, fix:, docs:, etc.)";
288
- break;
289
- case "simple":
290
- styleInstruction = "Keep messages simple and concise";
291
- break;
292
- case "detailed":
293
- styleInstruction = "Include detailed descriptions of changes";
294
- break;
295
- }
296
-
297
- const customPrompt = this.config.customPrompt
298
- ? `\nAdditional instructions: ${this.config.customPrompt}`
299
- : "";
300
-
301
- const statsText = diffStats
302
- ? `
303
- Change statistics:
304
- - ${diffStats.added} lines added
305
- - ${diffStats.deleted} lines deleted
306
- - ${diffStats.modified} files modified
307
- - ${diffStats.renamed} files renamed
308
- - ${diffStats.files.length} total files changed`
309
- : "";
310
-
311
- return `Analyze the following git changes and generate 4 different commit message suggestions.
312
-
313
- IMPORTANT: Carefully analyze the git diff below to understand WHAT changes were actually made:
314
- - Look for added/removed/modified lines (lines starting with +, -, or @@)
315
- - Identify if files were added, deleted, or modified
316
- - Understand the nature of the changes (new features, bug fixes, refactoring, etc.)
317
- - DO NOT assume all changes are "add" operations - check the diff carefully
318
-
319
- Files staged for commit:
320
- ${filesText}${statsText}
321
-
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
- }
330
-
331
- ${
332
- recentCommitsText
333
- ? `Recent commit history for reference:\n${recentCommitsText}`
334
- : ""
335
- }
336
-
337
- Requirements:
338
- - Generate exactly 4 different commit messages
339
- - Each message should be 50-72 characters
340
- - ${styleInstruction}
341
- - Make them specific to the actual changes shown
342
- - Focus on what changed, not how it changed
343
- ${customPrompt}
344
-
345
- Output ONLY valid JSON.`;
346
- }
347
-
348
- private parseSuggestions(content: string): CommitSuggestion[] {
349
- try {
350
- // Find the JSON object in the content (it might be wrapped in markdown or have extra text)
351
- const jsonMatch = content.match(/\{[\s\S]*\}/);
352
- if (jsonMatch) {
353
- const parsed = JSON.parse(jsonMatch[0]);
354
- if (parsed.suggestions && Array.isArray(parsed.suggestions)) {
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
- }
370
- }
371
- }
372
- throw new Error("Invalid JSON structure");
373
- } catch (e) {
374
- console.warn(
375
- "Failed to parse JSON suggestions, falling back to basic parsing:",
376
- e,
377
- );
378
- // Fallback to basic line parsing if JSON fails
379
- const lines = content.split("\n").filter((line) => line.trim());
380
- const suggestions: CommitSuggestion[] = [];
381
-
382
- for (const line of lines) {
383
- const match = line.match(/^\d+\.\s*(.+)$/);
384
- if (match && suggestions.length < 4) {
385
- suggestions.push({
386
- message: match[1].trim(),
387
- type: this.extractType(match[1]),
388
- description: match[1].trim(),
389
- });
390
- }
391
- }
392
- return suggestions;
393
- }
394
- }
395
-
396
- private extractType(message: string): string {
397
- const match = message.match(/^(\w+):/);
398
- return match ? match[1] : "feat";
399
- }
400
- }
@@ -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;