nocommit 0.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 +15 -0
- package/dist/ai.d.ts +10 -0
- package/dist/ai.js +97 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +33 -0
- package/dist/error.d.ts +5 -0
- package/dist/error.js +18 -0
- package/dist/git.d.ts +13 -0
- package/dist/git.js +136 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +191 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# nocommit
|
|
2
|
+
|
|
3
|
+
To install dependencies:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun install
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
To run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun run index.ts
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This project was created using `bun init` in bun v1.3.0. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
package/dist/ai.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Options for generating commit messages */
|
|
2
|
+
export interface GenerateOptions {
|
|
3
|
+
diffSnippets: string;
|
|
4
|
+
files: string[];
|
|
5
|
+
maxLength?: number;
|
|
6
|
+
count?: number;
|
|
7
|
+
timeout?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare const generateCommitMessages: (options: GenerateOptions) => Promise<string[]>;
|
|
10
|
+
export declare const generateCommitMessage: (diff: string) => Promise<string>;
|
package/dist/ai.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { GoogleGenAI } from "@google/genai";
|
|
2
|
+
import { getApiKey, getConfig } from "./config.js";
|
|
3
|
+
import { KnownError } from "./error.js";
|
|
4
|
+
const buildPrompt = (diffSnippets, fileList, maxLength, count) => `You are an expert developer writing git commit messages.
|
|
5
|
+
|
|
6
|
+
CHANGES:
|
|
7
|
+
Files: ${fileList.join(", ")}
|
|
8
|
+
|
|
9
|
+
${diffSnippets ? `CODE DIFF:\n${diffSnippets}` : ""}
|
|
10
|
+
|
|
11
|
+
TASK: Generate ${count} different commit message${count > 1 ? "s" : ""} for these changes.
|
|
12
|
+
|
|
13
|
+
FORMAT RULES:
|
|
14
|
+
- Use Conventional Commits: <type>: <subject>
|
|
15
|
+
- NO scope (use "feat: add login" NOT "feat(auth): add login")
|
|
16
|
+
- Maximum ${maxLength} characters per message
|
|
17
|
+
- Use imperative mood ("add" not "added")
|
|
18
|
+
- Be specific but concise
|
|
19
|
+
|
|
20
|
+
TYPES:
|
|
21
|
+
- feat: NEW user-facing feature
|
|
22
|
+
- fix: bug fix
|
|
23
|
+
- docs: documentation only
|
|
24
|
+
- style: formatting, missing semi-colons
|
|
25
|
+
- refactor: code change without new feature or fix
|
|
26
|
+
- perf: performance improvement
|
|
27
|
+
- test: adding/correcting tests
|
|
28
|
+
- build: build system, dependencies
|
|
29
|
+
- ci: CI configuration
|
|
30
|
+
- chore: maintenance, config updates
|
|
31
|
+
|
|
32
|
+
EXAMPLES (correct):
|
|
33
|
+
- feat: add user authentication with JWT
|
|
34
|
+
- fix: resolve memory leak in image processor
|
|
35
|
+
- refactor: simplify error handling logic
|
|
36
|
+
|
|
37
|
+
WRONG (do not use scope):
|
|
38
|
+
- feat(auth): add login
|
|
39
|
+
|
|
40
|
+
Output ${count > 1 ? `exactly ${count} commit messages, one per line` : "only the commit message"}, no explanations or numbering.`;
|
|
41
|
+
// Generates commit messages using Gemini
|
|
42
|
+
export const generateCommitMessages = async (options) => {
|
|
43
|
+
const apiKey = getApiKey();
|
|
44
|
+
const model = getConfig("model");
|
|
45
|
+
const maxLength = options.maxLength ?? getConfig("maxLength");
|
|
46
|
+
const count = options.count ?? getConfig("generate");
|
|
47
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
48
|
+
const prompt = buildPrompt(options.diffSnippets, options.files, maxLength, count);
|
|
49
|
+
try {
|
|
50
|
+
const response = await ai.models.generateContent({
|
|
51
|
+
model,
|
|
52
|
+
contents: prompt,
|
|
53
|
+
config: {
|
|
54
|
+
maxOutputTokens: 256,
|
|
55
|
+
temperature: 0.7,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
const text = response.text?.trim();
|
|
59
|
+
if (!text) {
|
|
60
|
+
throw new KnownError("AI returned empty response. Please try again");
|
|
61
|
+
}
|
|
62
|
+
// Parse response: split by newlines, remove empty lines
|
|
63
|
+
const messages = text
|
|
64
|
+
.split("\n")
|
|
65
|
+
.map((line) => line.trim())
|
|
66
|
+
.filter((line) => line.length > 0 && !line.match(/^\d+\./))
|
|
67
|
+
.map((line) => line.replace(/^[-*]\s*/, ""));
|
|
68
|
+
if (messages.length === 0) {
|
|
69
|
+
throw new KnownError("AI returned no valid commit messages.");
|
|
70
|
+
}
|
|
71
|
+
return messages.slice(0, count);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (error instanceof KnownError) {
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
if (error.message?.includes("API key")) {
|
|
78
|
+
throw new KnownError("Invalid Gemini API Key. Run: nc config set GEMINI_API_KEY=<key>");
|
|
79
|
+
}
|
|
80
|
+
if (error.message?.includes("Quota")) {
|
|
81
|
+
throw new KnownError("API quota exceeded. Please check your Gemini API usage limits.");
|
|
82
|
+
}
|
|
83
|
+
if (error.message?.includes("network") || error.code === "ENOTFOUND") {
|
|
84
|
+
throw new KnownError("Network error. Please check your internet connection.");
|
|
85
|
+
}
|
|
86
|
+
throw new KnownError(`AI error: ${error.message}`);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
// generate a single commit message
|
|
90
|
+
export const generateCommitMessage = async (diff) => {
|
|
91
|
+
const messages = await generateCommitMessages({
|
|
92
|
+
diffSnippets: diff,
|
|
93
|
+
files: [],
|
|
94
|
+
count: 1,
|
|
95
|
+
});
|
|
96
|
+
return messages[0] || "";
|
|
97
|
+
};
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ConfigSchema {
|
|
2
|
+
GEMINI_API_KEY: string;
|
|
3
|
+
model: string;
|
|
4
|
+
maxLength: number;
|
|
5
|
+
timeout: number;
|
|
6
|
+
generate: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const getConfig: <K extends keyof ConfigSchema>(key: K) => ConfigSchema[K];
|
|
9
|
+
export declare const setConfig: <K extends keyof ConfigSchema>(key: K, value: ConfigSchema[K]) => void;
|
|
10
|
+
export declare const getAllConfig: () => ConfigSchema;
|
|
11
|
+
export declare const getApiKey: () => string;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import Conf from "conf";
|
|
2
|
+
import { KnownError } from "./error.js";
|
|
3
|
+
const config = new Conf({
|
|
4
|
+
projectName: "nocommit",
|
|
5
|
+
defaults: {
|
|
6
|
+
GEMINI_API_KEY: "",
|
|
7
|
+
model: "gemini-2.5-flash",
|
|
8
|
+
maxLength: 72,
|
|
9
|
+
timeout: 30000,
|
|
10
|
+
generate: 3,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
// export const getConfig = (key: string) => {
|
|
14
|
+
// return config.get(key as any);
|
|
15
|
+
// };
|
|
16
|
+
// get Config
|
|
17
|
+
export const getConfig = (key) => {
|
|
18
|
+
return config.get(key);
|
|
19
|
+
};
|
|
20
|
+
// set Config
|
|
21
|
+
export const setConfig = (key, value) => {
|
|
22
|
+
config.set(key, value);
|
|
23
|
+
};
|
|
24
|
+
export const getAllConfig = () => {
|
|
25
|
+
return config.store;
|
|
26
|
+
};
|
|
27
|
+
export const getApiKey = () => {
|
|
28
|
+
const key = config.get("GEMINI_API_KEY");
|
|
29
|
+
if (!key) {
|
|
30
|
+
throw new KnownError("Missing API Key. Run: nc config set GEMINI_API_KEY=<Key Your API Key>");
|
|
31
|
+
}
|
|
32
|
+
return key;
|
|
33
|
+
};
|
package/dist/error.d.ts
ADDED
package/dist/error.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class KnownError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "KnownError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export const isKnownError = (error) => {
|
|
8
|
+
return error instanceof KnownError;
|
|
9
|
+
};
|
|
10
|
+
export const handleCliError = (error) => {
|
|
11
|
+
if (isKnownError(error)) {
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
if (error instanceof Error) {
|
|
15
|
+
console.error("\nUnexpected error", error.stack);
|
|
16
|
+
}
|
|
17
|
+
process.exit(1);
|
|
18
|
+
};
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const isGitRepo: () => Promise<boolean>;
|
|
2
|
+
export declare const assertGitRepo: () => Promise<void>;
|
|
3
|
+
export declare const stageAllChanges: () => Promise<void>;
|
|
4
|
+
export declare const getStagedDiff: () => Promise<string>;
|
|
5
|
+
export declare const hasStagedChanges: () => Promise<boolean>;
|
|
6
|
+
export declare const getStagedFiles: () => Promise<string[]>;
|
|
7
|
+
export declare const commitChanges: (message: string) => Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Extracts only changed lines from diff for token efficiency.
|
|
10
|
+
* Limits: 5 files max, 30 lines per file, 4000 chars total.
|
|
11
|
+
*/
|
|
12
|
+
export declare const buildDiffSnippets: (files: string[], perFileMaxLines?: number, totalMaxChars?: number) => Promise<string>;
|
|
13
|
+
export declare const getStagedSummary: () => Promise<string>;
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import { KnownError } from "./error.js";
|
|
3
|
+
// Exclude from diff to save API Token
|
|
4
|
+
const EXCLUDE = [
|
|
5
|
+
"package-lock.json",
|
|
6
|
+
"pnpm-lock.yaml",
|
|
7
|
+
"yarn.lock",
|
|
8
|
+
"bun.lock",
|
|
9
|
+
"node_modules/**",
|
|
10
|
+
"dist/**",
|
|
11
|
+
"build/**",
|
|
12
|
+
".next/**",
|
|
13
|
+
"*.min.js",
|
|
14
|
+
"*.map",
|
|
15
|
+
"*.log",
|
|
16
|
+
].flatMap((file) => [":(exclude)" + file]);
|
|
17
|
+
// Check git init or not
|
|
18
|
+
export const isGitRepo = async () => {
|
|
19
|
+
try {
|
|
20
|
+
await execa("git", ["rev-parse", "--is-inside-work-tree"]);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
export const assertGitRepo = async () => {
|
|
28
|
+
if (!(await isGitRepo())) {
|
|
29
|
+
throw new KnownError("Not a git repository. Run this command inside 'git init'");
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
// Check Stages everything (git add .)
|
|
33
|
+
export const stageAllChanges = async () => {
|
|
34
|
+
await execa("git", ["add", "--update"]);
|
|
35
|
+
};
|
|
36
|
+
// gets the actual code changes
|
|
37
|
+
export const getStagedDiff = async () => {
|
|
38
|
+
try {
|
|
39
|
+
const { stdout } = await execa("git", ["diff", "--staged", ...EXCLUDE]);
|
|
40
|
+
return stdout;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
export const hasStagedChanges = async () => {
|
|
47
|
+
const { stdout } = await execa("git", [
|
|
48
|
+
"diff",
|
|
49
|
+
"--staged",
|
|
50
|
+
"--name-only",
|
|
51
|
+
...EXCLUDE,
|
|
52
|
+
]);
|
|
53
|
+
return stdout.trim().length > 0;
|
|
54
|
+
};
|
|
55
|
+
// gets the list of filenames
|
|
56
|
+
export const getStagedFiles = async () => {
|
|
57
|
+
const { stdout } = await execa("git", [
|
|
58
|
+
"diff",
|
|
59
|
+
"--staged",
|
|
60
|
+
"--name-only",
|
|
61
|
+
...EXCLUDE,
|
|
62
|
+
]);
|
|
63
|
+
return stdout.split("\n").filter(Boolean);
|
|
64
|
+
};
|
|
65
|
+
// git commit -m "your message here"
|
|
66
|
+
export const commitChanges = async (message) => {
|
|
67
|
+
await execa("git", ["commit", "-m", message], {
|
|
68
|
+
stdio: "inherit",
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Extracts only changed lines from diff for token efficiency.
|
|
73
|
+
* Limits: 5 files max, 30 lines per file, 4000 chars total.
|
|
74
|
+
*/
|
|
75
|
+
export const buildDiffSnippets = async (files, perFileMaxLines = 30, totalMaxChars = 4000) => {
|
|
76
|
+
try {
|
|
77
|
+
const targetFiles = files.slice(0, 5);
|
|
78
|
+
const parts = [];
|
|
79
|
+
let remaining = totalMaxChars;
|
|
80
|
+
for (const file of targetFiles) {
|
|
81
|
+
const { stdout } = await execa("git", [
|
|
82
|
+
"diff",
|
|
83
|
+
"--cached",
|
|
84
|
+
"--unified=0",
|
|
85
|
+
"--",
|
|
86
|
+
file,
|
|
87
|
+
]);
|
|
88
|
+
if (!stdout)
|
|
89
|
+
continue;
|
|
90
|
+
const lines = stdout.split("\n").filter(Boolean);
|
|
91
|
+
const picked = [];
|
|
92
|
+
let count = 0;
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
const isHunk = line.startsWith("@@");
|
|
95
|
+
const isChange = (line.startsWith("+") || line.startsWith("-")) &&
|
|
96
|
+
!line.startsWith("+++") &&
|
|
97
|
+
!line.startsWith("---");
|
|
98
|
+
if (isHunk || isChange) {
|
|
99
|
+
picked.push(line);
|
|
100
|
+
count++;
|
|
101
|
+
if (count >= perFileMaxLines)
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (picked.length > 0) {
|
|
106
|
+
const block = [`# ${file}`, ...picked].join("\n");
|
|
107
|
+
if (block.length <= remaining) {
|
|
108
|
+
parts.push(block);
|
|
109
|
+
remaining -= block.length;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
parts.push(block.slice(0, Math.max(0, remaining)));
|
|
113
|
+
remaining = 0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (remaining <= 0)
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
if (parts.length === 0)
|
|
120
|
+
return "";
|
|
121
|
+
return parts.join("\n\n");
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return "";
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
// gets a high-level statistical summary
|
|
128
|
+
export const getStagedSummary = async () => {
|
|
129
|
+
const { stdout } = await execa("git", [
|
|
130
|
+
"diff",
|
|
131
|
+
"--staged",
|
|
132
|
+
"--stat",
|
|
133
|
+
...EXCLUDE,
|
|
134
|
+
]);
|
|
135
|
+
return stdout;
|
|
136
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { intro, outro, spinner, select, text, isCancel } from "@clack/prompts";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import { setConfig, getConfig } from "./config.js";
|
|
6
|
+
import { assertGitRepo, hasStagedChanges, stageAllChanges, commitChanges, getStagedFiles, buildDiffSnippets, } from "./git.js";
|
|
7
|
+
import { generateCommitMessages } from "./ai.js";
|
|
8
|
+
import { KnownError, handleCliError } from "./error.js";
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name("nocommit")
|
|
12
|
+
.version("0.0.0")
|
|
13
|
+
.description("AI-powered git commit message generator")
|
|
14
|
+
.option("-a, --all", "Stage all tracked changes before committing")
|
|
15
|
+
.option("-y, --yes", "Skip confirmation and commit with first suggestion");
|
|
16
|
+
// Main command execution logic for generating and committing messages
|
|
17
|
+
program.action(async (options) => {
|
|
18
|
+
intro(pc.bgCyan(pc.black(" nocommit ")));
|
|
19
|
+
try {
|
|
20
|
+
await assertGitRepo();
|
|
21
|
+
if (options.all) {
|
|
22
|
+
await stageAllChanges();
|
|
23
|
+
}
|
|
24
|
+
if (!(await hasStagedChanges())) {
|
|
25
|
+
throw new KnownError('No staged changes found. Stage files with "git add ." or use --all flag.');
|
|
26
|
+
}
|
|
27
|
+
const s = spinner();
|
|
28
|
+
s.start("Detecting staged files...");
|
|
29
|
+
const files = await getStagedFiles();
|
|
30
|
+
s.stop(`Found ${files.length} staged file(s):`);
|
|
31
|
+
console.log(pc.dim(files.map((f) => ` ${f}`).join("\n")));
|
|
32
|
+
s.start("Analyzing changes with AI...");
|
|
33
|
+
const diffSnippets = await buildDiffSnippets(files);
|
|
34
|
+
const messages = await generateCommitMessages({
|
|
35
|
+
diffSnippets,
|
|
36
|
+
files,
|
|
37
|
+
});
|
|
38
|
+
s.stop("Generated commit message");
|
|
39
|
+
if (options.yes) {
|
|
40
|
+
const message = messages[0];
|
|
41
|
+
console.log(pc.dim(`Using: ${message}`));
|
|
42
|
+
await commitChanges(message);
|
|
43
|
+
outro(pc.green("✔ Committed!"));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
let selectedMessage = messages[0];
|
|
47
|
+
while (true) {
|
|
48
|
+
const action = await select({
|
|
49
|
+
message: `${pc.green(selectedMessage)}`,
|
|
50
|
+
options: [
|
|
51
|
+
{ value: "commit", label: "✔ Commit" },
|
|
52
|
+
{ value: "edit", label: "✎ Edit" },
|
|
53
|
+
{ value: "retry", label: "↻ Regenerate" },
|
|
54
|
+
{ value: "cancel", label: "✖ Cancel" },
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
if (isCancel(action) || action === "cancel") {
|
|
58
|
+
outro("Cancelled");
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
if (action === "commit") {
|
|
62
|
+
await commitChanges(selectedMessage);
|
|
63
|
+
outro(pc.green("✔ Committed!"));
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
if (action === "edit") {
|
|
67
|
+
const newMessage = await text({
|
|
68
|
+
message: "Edit message:",
|
|
69
|
+
initialValue: selectedMessage,
|
|
70
|
+
validate: (value) => {
|
|
71
|
+
if (!value || value.trim().length === 0) {
|
|
72
|
+
return "Message cannot be empty";
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
if (!isCancel(newMessage)) {
|
|
78
|
+
selectedMessage = newMessage.trim();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (action === "retry") {
|
|
82
|
+
s.start("Regenerating...");
|
|
83
|
+
const newMessages = await generateCommitMessages({
|
|
84
|
+
diffSnippets,
|
|
85
|
+
files,
|
|
86
|
+
});
|
|
87
|
+
s.stop("New message generated");
|
|
88
|
+
selectedMessage = newMessages[0];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
outro(pc.red(error.message));
|
|
94
|
+
handleCliError(error);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
// Command to manage application configuration, including API key and model settings
|
|
98
|
+
const configCmd = program.command("config").description("Manage configuration");
|
|
99
|
+
const VALID_CONFIG_KEYS = [
|
|
100
|
+
"GEMINI_API_KEY",
|
|
101
|
+
"model",
|
|
102
|
+
"maxLength",
|
|
103
|
+
"timeout",
|
|
104
|
+
"generate",
|
|
105
|
+
];
|
|
106
|
+
configCmd
|
|
107
|
+
.command("set <key=value>")
|
|
108
|
+
.description("Set a config value")
|
|
109
|
+
.action((str) => {
|
|
110
|
+
const eqIndex = str.indexOf("=");
|
|
111
|
+
if (eqIndex === -1) {
|
|
112
|
+
console.log(pc.red("Usage: config set KEY=VALUE"));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
const key = str.slice(0, eqIndex);
|
|
116
|
+
const val = str.slice(eqIndex + 1);
|
|
117
|
+
if (!key || !val) {
|
|
118
|
+
console.log(pc.red("Usage: config set KEY=VALUE"));
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
if (!VALID_CONFIG_KEYS.includes(key)) {
|
|
122
|
+
console.log(pc.red(`Invalid key: ${key}`));
|
|
123
|
+
console.log(pc.yellow(`Valid keys: ${VALID_CONFIG_KEYS.join(", ")}`));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
if (key === "maxLength") {
|
|
127
|
+
const num = parseInt(val, 10);
|
|
128
|
+
if (isNaN(num) || num < 20 || num > 500) {
|
|
129
|
+
console.log(pc.red("maxLength must be a number between 20 and 500"));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
setConfig("maxLength", num);
|
|
133
|
+
}
|
|
134
|
+
else if (key === "timeout") {
|
|
135
|
+
const num = parseInt(val, 10);
|
|
136
|
+
if (isNaN(num) || num < 5000 || num > 120000) {
|
|
137
|
+
console.log(pc.red("timeout must be a number between 5000 and 120000 (ms)"));
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
setConfig("timeout", num);
|
|
141
|
+
}
|
|
142
|
+
else if (key === "generate") {
|
|
143
|
+
const num = parseInt(val, 10);
|
|
144
|
+
if (isNaN(num) || num < 1 || num > 5) {
|
|
145
|
+
console.log(pc.red("generate must be a number between 1 and 5"));
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
setConfig("generate", num);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
setConfig(key, val);
|
|
152
|
+
}
|
|
153
|
+
console.log(pc.green(`✔ Set ${key}`));
|
|
154
|
+
});
|
|
155
|
+
configCmd
|
|
156
|
+
.command("get [key]")
|
|
157
|
+
.description("Get config value(s)")
|
|
158
|
+
.action((key) => {
|
|
159
|
+
if (key) {
|
|
160
|
+
if (!VALID_CONFIG_KEYS.includes(key)) {
|
|
161
|
+
console.log(pc.red(`Invalid key: ${key}`));
|
|
162
|
+
console.log(pc.yellow(`Valid keys: ${VALID_CONFIG_KEYS.join(", ")}`));
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
const val = getConfig(key);
|
|
166
|
+
// Mask API key for security
|
|
167
|
+
if (key === "GEMINI_API_KEY" &&
|
|
168
|
+
typeof val === "string" &&
|
|
169
|
+
val.length > 8) {
|
|
170
|
+
console.log(`${key}: ${val.slice(0, 4)}...${val.slice(-4)}`);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
console.log(`${key}: ${val}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
console.log(pc.bold("Current configuration:"));
|
|
178
|
+
for (const k of VALID_CONFIG_KEYS) {
|
|
179
|
+
const val = getConfig(k);
|
|
180
|
+
if (k === "GEMINI_API_KEY" &&
|
|
181
|
+
typeof val === "string" &&
|
|
182
|
+
val.length > 8) {
|
|
183
|
+
console.log(` ${k}: ${val.slice(0, 4)}...${val.slice(-4)}`);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
console.log(` ${k}: ${val}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nocommit",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Writes your git commit messages for you with AI using Gemini",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"nocommit": "dist/index.js",
|
|
9
|
+
"nc": "dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"dev": "tsc --watch",
|
|
14
|
+
"start": "node dist/index.js",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"git",
|
|
19
|
+
"commit",
|
|
20
|
+
"ai",
|
|
21
|
+
"gemini",
|
|
22
|
+
"cli"
|
|
23
|
+
],
|
|
24
|
+
"author": "Asim Sk",
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^25.0.10"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"typescript": "^5.9.3"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@clack/prompts": "^0.11.0",
|
|
39
|
+
"@google/genai": "^1.38.0",
|
|
40
|
+
"commander": "^14.0.2",
|
|
41
|
+
"conf": "^15.0.2",
|
|
42
|
+
"execa": "^9.6.1",
|
|
43
|
+
"picocolors": "^1.1.1"
|
|
44
|
+
}
|
|
45
|
+
}
|