@sabinm677/ccommit 0.1.0
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/LICENSE +21 -0
- package/README.md +113 -0
- package/bin/ccommit +5 -0
- package/package.json +40 -0
- package/src/ai/generate.js +185 -0
- package/src/ai/prompt-builder.js +63 -0
- package/src/ai/provider-resolution.js +77 -0
- package/src/ai/providers.js +5 -0
- package/src/ai/response-parser.js +14 -0
- package/src/cli/args.js +82 -0
- package/src/cli/confirm.js +24 -0
- package/src/cli/exit-codes.js +4 -0
- package/src/commit/normalizer.js +36 -0
- package/src/commit/types.js +12 -0
- package/src/commit/validator.js +30 -0
- package/src/config/loader.js +90 -0
- package/src/errors/app-error.js +9 -0
- package/src/errors/messages.js +27 -0
- package/src/git/create-commit.js +17 -0
- package/src/git/repo-check.js +14 -0
- package/src/git/staged-diff.js +23 -0
- package/src/main.js +102 -0
- package/src/output/writer.js +7 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Sabin Maharjan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# ccommit
|
|
2
|
+
|
|
3
|
+
Generate Conventional Commit messages from staged Git changes using Claude Code CLI, OpenCode CLI, or Codex CLI.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
- Bun 1.0+
|
|
7
|
+
- Git 2.0+
|
|
8
|
+
- Claude Code CLI, OpenCode CLI, or Codex CLI configured and available on `PATH`
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
```bash
|
|
12
|
+
ccommit
|
|
13
|
+
# or from repo root
|
|
14
|
+
bin/ccommit
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
### Via npm (recommended)
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g @sabinm677/ccommit
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### From source
|
|
25
|
+
```bash
|
|
26
|
+
git clone https://github.com/sabinm677/ccommit.git
|
|
27
|
+
cd ccommit
|
|
28
|
+
chmod +x bin/ccommit
|
|
29
|
+
ln -sf "$(pwd)/bin/ccommit" ~/.local/bin/ccommit
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
If needed, add `~/.local/bin` to your `PATH`.
|
|
33
|
+
|
|
34
|
+
Default mode:
|
|
35
|
+
- Generates a commit message from staged changes
|
|
36
|
+
- Shows the message
|
|
37
|
+
- Prompts `Create commit? [y/N]`
|
|
38
|
+
- Creates commit only when you answer `y` or `yes`
|
|
39
|
+
- In non-interactive mode, commit is auto-cancelled unless `--yes` is used
|
|
40
|
+
|
|
41
|
+
Show-only mode (no commit created):
|
|
42
|
+
```bash
|
|
43
|
+
ccommit --show
|
|
44
|
+
ccommit -s
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Choose provider explicitly:
|
|
48
|
+
```bash
|
|
49
|
+
bin/ccommit -p opencode
|
|
50
|
+
ccommit --provider opencode
|
|
51
|
+
ccommit -p opencode
|
|
52
|
+
ccommit -p codex
|
|
53
|
+
ccommit --yes
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Options
|
|
57
|
+
- `-s`, `--show`: Generate and print message only. Do not prompt or create commit.
|
|
58
|
+
- `-y`, `--yes`: Create commit without interactive confirmation.
|
|
59
|
+
- `-p`, `--provider <claude|opencode|codex>`: Select AI provider for this run.
|
|
60
|
+
- `-h`, `--help`: Show usage help.
|
|
61
|
+
- `-v`, `--version`: Show CLI version.
|
|
62
|
+
- `--verbose`: Print debug details to stderr, including provider failure diagnostics.
|
|
63
|
+
|
|
64
|
+
ccommit prints the selected provider in the header (including `--show`/`-s` mode):
|
|
65
|
+
```text
|
|
66
|
+
Generated commit message (provider):
|
|
67
|
+
Generated commit message (claude):
|
|
68
|
+
Generated commit message (opencode):
|
|
69
|
+
Generated commit message (codex):
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Config
|
|
73
|
+
Optional config file: `~/.config/ccommit/config.json`
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"provider": "codex",
|
|
78
|
+
"providers": {
|
|
79
|
+
"claude": { "command": "claude", "args": ["-p"] },
|
|
80
|
+
"opencode": { "command": "opencode", "args": ["run"] },
|
|
81
|
+
"codex": { "command": "codex", "args": ["exec"] }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Precedence for provider/command settings: `CLI > env > config > defaults`.
|
|
87
|
+
Default provider: `claude`.
|
|
88
|
+
|
|
89
|
+
## Exit Codes
|
|
90
|
+
- `0`: success
|
|
91
|
+
- `1`: expected error (for example not a git repo, no staged changes, provider CLI/auth/network failure)
|
|
92
|
+
|
|
93
|
+
## Development
|
|
94
|
+
Run tests:
|
|
95
|
+
```bash
|
|
96
|
+
bun test
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Node compatibility:
|
|
100
|
+
- `ccommit` is Bun-first, but basic CLI invocation with Node remains supported (for example `node bin/ccommit --version`).
|
|
101
|
+
|
|
102
|
+
## Test Helpers
|
|
103
|
+
For local testing without provider API calls:
|
|
104
|
+
- `CCOMMIT_MOCK_MESSAGE="feat: add feature"` to bypass AI provider call.
|
|
105
|
+
- `CCOMMIT_MOCK_ERROR=AUTH` to force auth error.
|
|
106
|
+
- `CCOMMIT_MOCK_ERROR=NETWORK` to force network error.
|
|
107
|
+
|
|
108
|
+
Provider env overrides:
|
|
109
|
+
- `CCOMMIT_PROVIDER=claude|opencode|codex`
|
|
110
|
+
- `CCOMMIT_CLAUDE_CMD`, `CCOMMIT_CLAUDE_ARGS`
|
|
111
|
+
- `CCOMMIT_OPENCODE_CMD`, `CCOMMIT_OPENCODE_ARGS`
|
|
112
|
+
- `CCOMMIT_CODEX_CMD`, `CCOMMIT_CODEX_ARGS`
|
|
113
|
+
- `CCOMMIT_PROVIDER_TIMEOUT_MS` (optional, default `60000`)
|
package/bin/ccommit
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sabinm677/ccommit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI-powered Git commit message generator with Conventional Commits output.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Sabin Maharjan",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/sabinm677/ccommit.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/sabinm677/ccommit",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/sabinm677/ccommit/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"git",
|
|
18
|
+
"commit",
|
|
19
|
+
"ai",
|
|
20
|
+
"conventional-commits",
|
|
21
|
+
"cli",
|
|
22
|
+
"claude",
|
|
23
|
+
"opencode",
|
|
24
|
+
"codex"
|
|
25
|
+
],
|
|
26
|
+
"files": [
|
|
27
|
+
"bin",
|
|
28
|
+
"src"
|
|
29
|
+
],
|
|
30
|
+
"bin": {
|
|
31
|
+
"ccommit": "./bin/ccommit"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "bun test"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"bun": ">=1.0.0",
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { AppError } from "../errors/app-error.js";
|
|
3
|
+
import { ERROR_MESSAGES } from "../errors/messages.js";
|
|
4
|
+
import { resolveCommand, resolveProvider } from "./provider-resolution.js";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_PROVIDER_TIMEOUT_MS = 60_000;
|
|
7
|
+
|
|
8
|
+
function getProviderErrors() {
|
|
9
|
+
return {
|
|
10
|
+
notFound: ERROR_MESSAGES.PROVIDER_NOT_FOUND,
|
|
11
|
+
auth: ERROR_MESSAGES.PROVIDER_AUTH_FAILURE,
|
|
12
|
+
network: ERROR_MESSAGES.PROVIDER_NETWORK_FAILURE,
|
|
13
|
+
timeout: ERROR_MESSAGES.PROVIDER_TIMEOUT,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveProviderTimeoutMs() {
|
|
18
|
+
const raw = process.env.CCOMMIT_PROVIDER_TIMEOUT_MS;
|
|
19
|
+
if (!raw) {
|
|
20
|
+
return DEFAULT_PROVIDER_TIMEOUT_MS;
|
|
21
|
+
}
|
|
22
|
+
const parsed = Number.parseInt(raw, 10);
|
|
23
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
24
|
+
return DEFAULT_PROVIDER_TIMEOUT_MS;
|
|
25
|
+
}
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function classifyCliFailure(stderr = "", stdout = "") {
|
|
30
|
+
const errors = getProviderErrors();
|
|
31
|
+
const output = `${stderr}\n${stdout}`.toLowerCase();
|
|
32
|
+
if (
|
|
33
|
+
output.includes("unauthorized") ||
|
|
34
|
+
output.includes("authentication") ||
|
|
35
|
+
output.includes("api key")
|
|
36
|
+
) {
|
|
37
|
+
return new AppError(errors.auth);
|
|
38
|
+
}
|
|
39
|
+
if (
|
|
40
|
+
output.includes("network") ||
|
|
41
|
+
output.includes("unable to connect") ||
|
|
42
|
+
output.includes("failed to fetch") ||
|
|
43
|
+
output.includes("connect") ||
|
|
44
|
+
output.includes("timed out") ||
|
|
45
|
+
output.includes("connection") ||
|
|
46
|
+
output.includes("econn")
|
|
47
|
+
) {
|
|
48
|
+
return new AppError(errors.network);
|
|
49
|
+
}
|
|
50
|
+
return new AppError(ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function summarizeOutput(text = "") {
|
|
54
|
+
if (typeof text !== "string") {
|
|
55
|
+
text = text == null ? "" : String(text);
|
|
56
|
+
}
|
|
57
|
+
const trimmed = text.trim();
|
|
58
|
+
if (!trimmed) {
|
|
59
|
+
return "<empty>";
|
|
60
|
+
}
|
|
61
|
+
const max = 600;
|
|
62
|
+
return trimmed.length > max ? `${trimmed.slice(0, max)}...` : trimmed;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function writeVerboseFailure(options, details) {
|
|
66
|
+
if (!options.verbose) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
process.stderr.write(
|
|
70
|
+
[
|
|
71
|
+
"Provider execution failed:",
|
|
72
|
+
`provider=${details.provider}`,
|
|
73
|
+
`command=${details.command} ${details.args.join(" ")}`.trim(),
|
|
74
|
+
`timeout=${details.timeout}ms`,
|
|
75
|
+
`status=${details.status ?? "null"}`,
|
|
76
|
+
`error_code=${details.errorCode || "<none>"}`,
|
|
77
|
+
`error_message=${details.errorMessage || "<none>"}`,
|
|
78
|
+
`stdout=${summarizeOutput(details.stdout)}`,
|
|
79
|
+
`stderr=${summarizeOutput(details.stderr)}`,
|
|
80
|
+
"",
|
|
81
|
+
].join("\n"),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function generateCommitMessage(prompt, options = {}, config = {}) {
|
|
86
|
+
const provider = resolveProvider(options, config);
|
|
87
|
+
const errors = getProviderErrors();
|
|
88
|
+
|
|
89
|
+
const mockMessage = process.env.CCOMMIT_MOCK_MESSAGE;
|
|
90
|
+
if (mockMessage) {
|
|
91
|
+
return mockMessage;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const mockError = process.env.CCOMMIT_MOCK_ERROR;
|
|
95
|
+
if (mockError === "AUTH") {
|
|
96
|
+
throw new AppError(errors.auth);
|
|
97
|
+
}
|
|
98
|
+
if (mockError === "NETWORK") {
|
|
99
|
+
throw new AppError(errors.network);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const { command, args } = resolveCommand(provider, config);
|
|
103
|
+
const timeout = resolveProviderTimeoutMs();
|
|
104
|
+
let result;
|
|
105
|
+
try {
|
|
106
|
+
result = spawnSync(command, [...args, prompt], {
|
|
107
|
+
encoding: "utf8",
|
|
108
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
109
|
+
timeout,
|
|
110
|
+
});
|
|
111
|
+
} catch (error) {
|
|
112
|
+
writeVerboseFailure(options, {
|
|
113
|
+
provider,
|
|
114
|
+
command,
|
|
115
|
+
args,
|
|
116
|
+
timeout,
|
|
117
|
+
status: null,
|
|
118
|
+
errorCode: error?.code,
|
|
119
|
+
errorMessage: error?.message,
|
|
120
|
+
stdout: "",
|
|
121
|
+
stderr: "",
|
|
122
|
+
});
|
|
123
|
+
if (error?.code === "ENOENT") {
|
|
124
|
+
throw new AppError(errors.notFound);
|
|
125
|
+
}
|
|
126
|
+
if (error?.code === "ETIMEDOUT") {
|
|
127
|
+
throw new AppError(errors.timeout);
|
|
128
|
+
}
|
|
129
|
+
throw new AppError(ERROR_MESSAGES.UNKNOWN_ERROR);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (result.error && result.error.code === "ETIMEDOUT") {
|
|
133
|
+
writeVerboseFailure(options, {
|
|
134
|
+
provider,
|
|
135
|
+
command,
|
|
136
|
+
args,
|
|
137
|
+
timeout,
|
|
138
|
+
status: result.status,
|
|
139
|
+
errorCode: result.error.code,
|
|
140
|
+
errorMessage: result.error.message,
|
|
141
|
+
stdout: result.stdout,
|
|
142
|
+
stderr: result.stderr,
|
|
143
|
+
});
|
|
144
|
+
throw new AppError(errors.timeout);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (result.error && result.error.code === "ENOENT") {
|
|
148
|
+
writeVerboseFailure(options, {
|
|
149
|
+
provider,
|
|
150
|
+
command,
|
|
151
|
+
args,
|
|
152
|
+
timeout,
|
|
153
|
+
status: result.status,
|
|
154
|
+
errorCode: result.error.code,
|
|
155
|
+
errorMessage: result.error.message,
|
|
156
|
+
stdout: result.stdout,
|
|
157
|
+
stderr: result.stderr,
|
|
158
|
+
});
|
|
159
|
+
throw new AppError(errors.notFound);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (result.status !== 0) {
|
|
163
|
+
const classified = classifyCliFailure(result.stderr, result.stdout);
|
|
164
|
+
writeVerboseFailure(options, {
|
|
165
|
+
provider,
|
|
166
|
+
command,
|
|
167
|
+
args,
|
|
168
|
+
timeout,
|
|
169
|
+
status: result.status,
|
|
170
|
+
errorCode: result.error?.code,
|
|
171
|
+
errorMessage: classified.message,
|
|
172
|
+
stdout: result.stdout,
|
|
173
|
+
stderr: result.stderr,
|
|
174
|
+
});
|
|
175
|
+
throw classified;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (options.verbose) {
|
|
179
|
+
process.stderr.write(
|
|
180
|
+
`Provider: ${provider}\nCommand: ${command} ${args.join(" ")}\nTimeout: ${timeout}ms\n`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return (result.stdout || "").trim();
|
|
185
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { COMMIT_TYPES } from "../commit/types.js";
|
|
2
|
+
|
|
3
|
+
const MAX_DIFF_CHARS = 120_000;
|
|
4
|
+
|
|
5
|
+
function extractStagedPaths(diff) {
|
|
6
|
+
const paths = [];
|
|
7
|
+
for (const line of diff.split("\n")) {
|
|
8
|
+
if (!line.startsWith("diff --git ")) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
const match = line.match(/^diff --git a\/(.+) b\/(.+)$/);
|
|
12
|
+
if (!match) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
paths.push(match[2]);
|
|
16
|
+
}
|
|
17
|
+
return paths;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isDocsOnlyPath(path) {
|
|
21
|
+
const lower = path.toLowerCase();
|
|
22
|
+
if (lower.startsWith("docs/")) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return (
|
|
26
|
+
lower.endsWith(".md") ||
|
|
27
|
+
lower.endsWith(".mdx") ||
|
|
28
|
+
lower.endsWith(".rst") ||
|
|
29
|
+
lower.endsWith(".txt") ||
|
|
30
|
+
lower.endsWith(".adoc")
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildPrompt(diff) {
|
|
35
|
+
const clippedDiff =
|
|
36
|
+
diff.length > MAX_DIFF_CHARS
|
|
37
|
+
? `${diff.slice(0, MAX_DIFF_CHARS)}\n\n[diff truncated for length]`
|
|
38
|
+
: diff;
|
|
39
|
+
const stagedPaths = extractStagedPaths(diff);
|
|
40
|
+
const docsOnly =
|
|
41
|
+
stagedPaths.length > 0 && stagedPaths.every((path) => isDocsOnlyPath(path));
|
|
42
|
+
|
|
43
|
+
const typeGuidance = docsOnly
|
|
44
|
+
? "- This change is documentation-only. Use type docs."
|
|
45
|
+
: "- Choose the type strictly from actual staged changes.";
|
|
46
|
+
|
|
47
|
+
return `You are generating a git commit message from a staged diff.
|
|
48
|
+
|
|
49
|
+
Rules:
|
|
50
|
+
- Use Conventional Commits v1.0.0.
|
|
51
|
+
- Allowed types: ${COMMIT_TYPES.join(", ")}.
|
|
52
|
+
- Format header exactly: <type>[optional scope][optional !]: <description>
|
|
53
|
+
- Description must be concise and imperative.
|
|
54
|
+
- Prefer a single-line commit message. Add body/footer only if essential.
|
|
55
|
+
- Do not claim changes that are not present in the staged diff.
|
|
56
|
+
- Do not use markdown bullets in the commit body.
|
|
57
|
+
- ${typeGuidance}
|
|
58
|
+
- Use BREAKING CHANGE: footer when required.
|
|
59
|
+
- Return only the commit message text with no explanation.
|
|
60
|
+
|
|
61
|
+
Staged diff:
|
|
62
|
+
${clippedDiff}`;
|
|
63
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { AppError } from "../errors/app-error.js";
|
|
2
|
+
import { ERROR_MESSAGES } from "../errors/messages.js";
|
|
3
|
+
import { isValidProvider } from "./providers.js";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_PROVIDER = "claude";
|
|
6
|
+
|
|
7
|
+
function splitArgs(raw) {
|
|
8
|
+
if (!raw) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
return raw
|
|
12
|
+
.split(" ")
|
|
13
|
+
.map((value) => value.trim())
|
|
14
|
+
.filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function resolveProvider(options = {}, config = {}) {
|
|
18
|
+
const provider =
|
|
19
|
+
options.provider ||
|
|
20
|
+
process.env.CCOMMIT_PROVIDER ||
|
|
21
|
+
config.provider ||
|
|
22
|
+
DEFAULT_PROVIDER;
|
|
23
|
+
|
|
24
|
+
if (!isValidProvider(provider)) {
|
|
25
|
+
throw new AppError(ERROR_MESSAGES.INVALID_PROVIDER);
|
|
26
|
+
}
|
|
27
|
+
return provider;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveCommand(provider, config = {}) {
|
|
31
|
+
const defaults =
|
|
32
|
+
provider === "claude"
|
|
33
|
+
? { command: "claude", args: ["-p"] }
|
|
34
|
+
: provider === "opencode"
|
|
35
|
+
? { command: "opencode", args: ["run"] }
|
|
36
|
+
: { command: "codex", args: ["exec"] };
|
|
37
|
+
|
|
38
|
+
if (provider === "claude") {
|
|
39
|
+
if (process.env.CCOMMIT_CLAUDE_CMD || process.env.CCOMMIT_CLAUDE_ARGS) {
|
|
40
|
+
return {
|
|
41
|
+
command: process.env.CCOMMIT_CLAUDE_CMD || defaults.command,
|
|
42
|
+
args: splitArgs(process.env.CCOMMIT_CLAUDE_ARGS || defaults.args.join(" ")),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
} else if (provider === "opencode") {
|
|
46
|
+
if (process.env.CCOMMIT_OPENCODE_CMD || process.env.CCOMMIT_OPENCODE_ARGS) {
|
|
47
|
+
return {
|
|
48
|
+
command: process.env.CCOMMIT_OPENCODE_CMD || defaults.command,
|
|
49
|
+
args: splitArgs(
|
|
50
|
+
process.env.CCOMMIT_OPENCODE_ARGS || defaults.args.join(" "),
|
|
51
|
+
),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
} else if (provider === "codex") {
|
|
55
|
+
if (process.env.CCOMMIT_CODEX_CMD || process.env.CCOMMIT_CODEX_ARGS) {
|
|
56
|
+
return {
|
|
57
|
+
command: process.env.CCOMMIT_CODEX_CMD || defaults.command,
|
|
58
|
+
args: splitArgs(process.env.CCOMMIT_CODEX_ARGS || defaults.args.join(" ")),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const providerConfig = config.providers?.[provider];
|
|
64
|
+
if (providerConfig?.command !== undefined || providerConfig?.args !== undefined) {
|
|
65
|
+
const command = providerConfig.command || defaults.command;
|
|
66
|
+
const args = providerConfig.args || defaults.args;
|
|
67
|
+
if (!command || typeof command !== "string") {
|
|
68
|
+
throw new AppError(ERROR_MESSAGES.INVALID_CONFIG);
|
|
69
|
+
}
|
|
70
|
+
if (!Array.isArray(args) || args.some((arg) => typeof arg !== "string")) {
|
|
71
|
+
throw new AppError(ERROR_MESSAGES.INVALID_CONFIG);
|
|
72
|
+
}
|
|
73
|
+
return { command, args };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return defaults;
|
|
77
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function stripCodeFence(text) {
|
|
2
|
+
const trimmed = text.trim();
|
|
3
|
+
if (trimmed.startsWith("```") && trimmed.endsWith("```")) {
|
|
4
|
+
return trimmed.replace(/^```[a-zA-Z]*\n?/, "").replace(/\n?```$/, "");
|
|
5
|
+
}
|
|
6
|
+
return text;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function parseCommitMessage(rawOutput) {
|
|
10
|
+
let text = stripCodeFence(rawOutput).trim();
|
|
11
|
+
text = text.replace(/^commit message:\s*/i, "").trim();
|
|
12
|
+
text = text.replace(/^message:\s*/i, "").trim();
|
|
13
|
+
return text;
|
|
14
|
+
}
|
package/src/cli/args.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { AppError } from "../errors/app-error.js";
|
|
2
|
+
import { ERROR_MESSAGES } from "../errors/messages.js";
|
|
3
|
+
import { isValidProvider } from "../ai/providers.js";
|
|
4
|
+
|
|
5
|
+
export function parseArgs(args) {
|
|
6
|
+
const options = {
|
|
7
|
+
help: false,
|
|
8
|
+
show: false,
|
|
9
|
+
yes: false,
|
|
10
|
+
version: false,
|
|
11
|
+
verbose: false,
|
|
12
|
+
provider: undefined,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
16
|
+
const arg = args[index];
|
|
17
|
+
if (arg === "--help" || arg === "-h") {
|
|
18
|
+
options.help = true;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (arg === "--version" || arg === "-v") {
|
|
22
|
+
options.version = true;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (arg === "--show" || arg === "-s") {
|
|
26
|
+
options.show = true;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (arg === "--yes" || arg === "-y") {
|
|
30
|
+
options.yes = true;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (arg === "--verbose") {
|
|
34
|
+
options.verbose = true;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (arg === "--provider" || arg === "-p") {
|
|
38
|
+
const value = args[index + 1];
|
|
39
|
+
if (!value || value.startsWith("-")) {
|
|
40
|
+
throw new AppError("Missing value for --provider/-p.");
|
|
41
|
+
}
|
|
42
|
+
if (!isValidProvider(value)) {
|
|
43
|
+
throw new AppError(ERROR_MESSAGES.INVALID_PROVIDER);
|
|
44
|
+
}
|
|
45
|
+
options.provider = value;
|
|
46
|
+
index += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (arg.startsWith("--provider=") || arg.startsWith("-p=")) {
|
|
50
|
+
const value = arg.includes("--provider=")
|
|
51
|
+
? arg.slice("--provider=".length)
|
|
52
|
+
: arg.slice("-p=".length);
|
|
53
|
+
if (!isValidProvider(value)) {
|
|
54
|
+
throw new AppError(ERROR_MESSAGES.INVALID_PROVIDER);
|
|
55
|
+
}
|
|
56
|
+
options.provider = value;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
throw new AppError(`Unknown option: ${arg}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return options;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getHelpText() {
|
|
66
|
+
return `Usage: ccommit [options]
|
|
67
|
+
|
|
68
|
+
Generate a Conventional Commit message from staged Git changes.
|
|
69
|
+
|
|
70
|
+
Options:
|
|
71
|
+
-s, --show Show generated message only (do not commit)
|
|
72
|
+
-y, --yes Create commit without interactive confirmation
|
|
73
|
+
-p, --provider AI provider: claude | opencode | codex
|
|
74
|
+
-h, --help Show help
|
|
75
|
+
-v, --version Show version
|
|
76
|
+
--verbose Print debug information to stderr
|
|
77
|
+
|
|
78
|
+
Config:
|
|
79
|
+
~/.config/ccommit/config.json
|
|
80
|
+
Precedence: CLI > env > config > defaults
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
|
|
4
|
+
export function isInteractiveTerminal() {
|
|
5
|
+
return Boolean(input.isTTY && output.isTTY);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function askForConfirmation(promptText) {
|
|
9
|
+
const rl = createInterface({ input, output });
|
|
10
|
+
try {
|
|
11
|
+
const answer = await new Promise((resolve) => {
|
|
12
|
+
rl.question(promptText, (value) => {
|
|
13
|
+
resolve(value ?? "");
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
} finally {
|
|
20
|
+
rl.close();
|
|
21
|
+
// Bun can keep stdin alive after readline close; pause to allow clean exit.
|
|
22
|
+
input.pause();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { COMMIT_TYPES } from "./types.js";
|
|
2
|
+
|
|
3
|
+
function compactWhitespace(text) {
|
|
4
|
+
return text.replace(/\s+/g, " ").trim();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function sanitizeDescription(raw) {
|
|
8
|
+
const cleaned = compactWhitespace(raw).replace(/[.]+$/, "");
|
|
9
|
+
if (!cleaned) {
|
|
10
|
+
return "update project files";
|
|
11
|
+
}
|
|
12
|
+
return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function normalizeCommitMessage(message) {
|
|
16
|
+
const normalized = message.trim();
|
|
17
|
+
if (!normalized) {
|
|
18
|
+
return "chore: update project files";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const [header, ...rest] = normalized.split("\n");
|
|
22
|
+
const cleanedHeader = compactWhitespace(header);
|
|
23
|
+
const matchedType = COMMIT_TYPES.find((type) =>
|
|
24
|
+
cleanedHeader.toLowerCase().startsWith(`${type}:`),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (matchedType) {
|
|
28
|
+
return [cleanedHeader, ...rest].join("\n").trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (/^[a-z]+(\([^)]+\))?!?:/i.test(cleanedHeader)) {
|
|
32
|
+
return `chore: ${sanitizeDescription(cleanedHeader.split(":").slice(1).join(":"))}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return `chore: ${sanitizeDescription(cleanedHeader)}`;
|
|
36
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { COMMIT_TYPES } from "./types.js";
|
|
2
|
+
|
|
3
|
+
const typeGroup = COMMIT_TYPES.join("|");
|
|
4
|
+
const HEADER_REGEX = new RegExp(
|
|
5
|
+
`^(${typeGroup})(\\([a-z0-9._-]+\\))?(!)?: .+`,
|
|
6
|
+
"i",
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
export function isValidCommitMessage(message) {
|
|
10
|
+
const normalized = message.trim();
|
|
11
|
+
if (!normalized) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const [header] = normalized.split("\n");
|
|
16
|
+
if (!HEADER_REGEX.test(header.trim())) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isBreakingChange(message) {
|
|
24
|
+
const normalized = message.trim();
|
|
25
|
+
const [header] = normalized.split("\n");
|
|
26
|
+
if (header.includes("!:")) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return /\nBREAKING CHANGE:/i.test(`\n${normalized}`);
|
|
30
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { AppError } from "../errors/app-error.js";
|
|
5
|
+
import { ERROR_MESSAGES } from "../errors/messages.js";
|
|
6
|
+
import { isValidProvider } from "../ai/providers.js";
|
|
7
|
+
|
|
8
|
+
const defaultConfig = Object.freeze({
|
|
9
|
+
provider: undefined,
|
|
10
|
+
providers: {
|
|
11
|
+
claude: { command: undefined, args: undefined },
|
|
12
|
+
opencode: { command: undefined, args: undefined },
|
|
13
|
+
codex: { command: undefined, args: undefined },
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export function getConfigPath() {
|
|
18
|
+
return join(homedir(), ".config", "ccommit", "config.json");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function loadConfig() {
|
|
22
|
+
const configPath = getConfigPath();
|
|
23
|
+
if (!existsSync(configPath)) {
|
|
24
|
+
return structuredClone(defaultConfig);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let raw;
|
|
28
|
+
try {
|
|
29
|
+
raw = JSON.parse(readFileSync(configPath, "utf8"));
|
|
30
|
+
} catch {
|
|
31
|
+
throw new AppError(ERROR_MESSAGES.INVALID_CONFIG_JSON);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return normalizeConfig(raw);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeConfig(raw) {
|
|
38
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
39
|
+
throw new AppError(ERROR_MESSAGES.INVALID_CONFIG);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const provider = raw.provider;
|
|
43
|
+
if (provider !== undefined) {
|
|
44
|
+
if (typeof provider !== "string" || !isValidProvider(provider.trim())) {
|
|
45
|
+
throw new AppError(ERROR_MESSAGES.INVALID_PROVIDER);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const providers = raw.providers;
|
|
50
|
+
if (
|
|
51
|
+
providers !== undefined &&
|
|
52
|
+
(!providers || typeof providers !== "object" || Array.isArray(providers))
|
|
53
|
+
) {
|
|
54
|
+
throw new AppError(ERROR_MESSAGES.INVALID_CONFIG);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const normalized = structuredClone(defaultConfig);
|
|
58
|
+
if (provider !== undefined) {
|
|
59
|
+
normalized.provider = provider.trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const providerName of ["claude", "opencode", "codex"]) {
|
|
63
|
+
const value = providers?.[providerName];
|
|
64
|
+
if (value === undefined) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
68
|
+
throw new AppError(ERROR_MESSAGES.INVALID_CONFIG);
|
|
69
|
+
}
|
|
70
|
+
if (value.command !== undefined) {
|
|
71
|
+
if (typeof value.command !== "string" || !value.command.trim()) {
|
|
72
|
+
throw new AppError(ERROR_MESSAGES.INVALID_CONFIG);
|
|
73
|
+
}
|
|
74
|
+
normalized.providers[providerName].command = value.command.trim();
|
|
75
|
+
}
|
|
76
|
+
if (value.args !== undefined) {
|
|
77
|
+
if (
|
|
78
|
+
!Array.isArray(value.args) ||
|
|
79
|
+
value.args.some((arg) => typeof arg !== "string" || !arg.trim())
|
|
80
|
+
) {
|
|
81
|
+
throw new AppError(ERROR_MESSAGES.INVALID_CONFIG);
|
|
82
|
+
}
|
|
83
|
+
normalized.providers[providerName].args = value.args.map((arg) =>
|
|
84
|
+
arg.trim(),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return normalized;
|
|
90
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const ERROR_MESSAGES = {
|
|
2
|
+
NO_STAGED_CHANGES:
|
|
3
|
+
"No staged changes found. Use 'git add' to stage changes first.",
|
|
4
|
+
NOT_GIT_REPO:
|
|
5
|
+
"Not a git repository. Please run this command inside a git repository.",
|
|
6
|
+
PROVIDER_NOT_FOUND:
|
|
7
|
+
"AI provider CLI not found. Please install and configure the selected provider CLI.",
|
|
8
|
+
PROVIDER_AUTH_FAILURE:
|
|
9
|
+
"Failed to authenticate with AI provider API. Please check your API key and provider login.",
|
|
10
|
+
PROVIDER_NETWORK_FAILURE:
|
|
11
|
+
"Unable to connect to AI provider API. Please check your internet connection.",
|
|
12
|
+
PROVIDER_TIMEOUT:
|
|
13
|
+
"AI provider request timed out. Please retry or increase CCOMMIT_PROVIDER_TIMEOUT_MS.",
|
|
14
|
+
COMMIT_CANCELLED: "Commit cancelled.",
|
|
15
|
+
COMMIT_CANCELLED_NON_INTERACTIVE:
|
|
16
|
+
"Commit cancelled in non-interactive mode. Use --yes to create commit automatically or --show to print message only.",
|
|
17
|
+
COMMIT_FAILED: "Failed to create git commit with generated message.",
|
|
18
|
+
INVALID_GENERATED_MESSAGE:
|
|
19
|
+
"Generated commit message is invalid. Please retry or commit manually.",
|
|
20
|
+
INVALID_PROVIDER:
|
|
21
|
+
"Invalid provider. Supported providers: claude, opencode, codex.",
|
|
22
|
+
INVALID_CONFIG:
|
|
23
|
+
"Invalid config file at ~/.config/ccommit/config.json.",
|
|
24
|
+
INVALID_CONFIG_JSON:
|
|
25
|
+
"Failed to parse config file at ~/.config/ccommit/config.json.",
|
|
26
|
+
UNKNOWN_ERROR: "An unexpected error occurred while generating commit message.",
|
|
27
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { AppError } from "../errors/app-error.js";
|
|
3
|
+
import { ERROR_MESSAGES } from "../errors/messages.js";
|
|
4
|
+
|
|
5
|
+
export function createCommit(message, cwd = process.cwd()) {
|
|
6
|
+
try {
|
|
7
|
+
execFileSync("git", ["commit", "-F", "-"], {
|
|
8
|
+
cwd,
|
|
9
|
+
input: `${message.trim()}\n`,
|
|
10
|
+
encoding: "utf8",
|
|
11
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
12
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
13
|
+
});
|
|
14
|
+
} catch {
|
|
15
|
+
throw new AppError(ERROR_MESSAGES.COMMIT_FAILED);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export function isGitRepository(cwd = process.cwd()) {
|
|
4
|
+
try {
|
|
5
|
+
const output = execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
6
|
+
cwd,
|
|
7
|
+
encoding: "utf8",
|
|
8
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
9
|
+
}).trim();
|
|
10
|
+
return output === "true";
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { AppError } from "../errors/app-error.js";
|
|
3
|
+
import { ERROR_MESSAGES } from "../errors/messages.js";
|
|
4
|
+
|
|
5
|
+
export function getStagedDiff(cwd = process.cwd()) {
|
|
6
|
+
let diff = "";
|
|
7
|
+
try {
|
|
8
|
+
diff = execFileSync("git", ["diff", "--cached", "--no-ext-diff"], {
|
|
9
|
+
cwd,
|
|
10
|
+
encoding: "utf8",
|
|
11
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
12
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
13
|
+
});
|
|
14
|
+
} catch {
|
|
15
|
+
throw new AppError(ERROR_MESSAGES.NOT_GIT_REPO);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!diff.trim()) {
|
|
19
|
+
throw new AppError(ERROR_MESSAGES.NO_STAGED_CHANGES);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return diff;
|
|
23
|
+
}
|
package/src/main.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { parseArgs, getHelpText } from "./cli/args.js";
|
|
5
|
+
import { EXIT_CODES } from "./cli/exit-codes.js";
|
|
6
|
+
import { isGitRepository } from "./git/repo-check.js";
|
|
7
|
+
import { getStagedDiff } from "./git/staged-diff.js";
|
|
8
|
+
import { createCommit } from "./git/create-commit.js";
|
|
9
|
+
import { ERROR_MESSAGES } from "./errors/messages.js";
|
|
10
|
+
import { AppError } from "./errors/app-error.js";
|
|
11
|
+
import { writeStdout, writeStderr } from "./output/writer.js";
|
|
12
|
+
import { askForConfirmation, isInteractiveTerminal } from "./cli/confirm.js";
|
|
13
|
+
import { buildPrompt } from "./ai/prompt-builder.js";
|
|
14
|
+
import { generateCommitMessage } from "./ai/generate.js";
|
|
15
|
+
import { resolveProvider } from "./ai/provider-resolution.js";
|
|
16
|
+
import { parseCommitMessage } from "./ai/response-parser.js";
|
|
17
|
+
import { isValidCommitMessage } from "./commit/validator.js";
|
|
18
|
+
import { normalizeCommitMessage } from "./commit/normalizer.js";
|
|
19
|
+
import { loadConfig } from "./config/loader.js";
|
|
20
|
+
|
|
21
|
+
function getVersion() {
|
|
22
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
23
|
+
const root = join(dirname(currentFile), "..");
|
|
24
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
25
|
+
return pkg.version;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function run(args = []) {
|
|
29
|
+
try {
|
|
30
|
+
const options = parseArgs(args);
|
|
31
|
+
|
|
32
|
+
if (options.help) {
|
|
33
|
+
writeStdout(getHelpText());
|
|
34
|
+
process.exitCode = EXIT_CODES.SUCCESS;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (options.version) {
|
|
39
|
+
writeStdout(`${getVersion()}\n`);
|
|
40
|
+
process.exitCode = EXIT_CODES.SUCCESS;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!isGitRepository()) {
|
|
45
|
+
throw new AppError(ERROR_MESSAGES.NOT_GIT_REPO);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const config = loadConfig();
|
|
49
|
+
const provider = resolveProvider(options, config);
|
|
50
|
+
const diff = getStagedDiff();
|
|
51
|
+
const prompt = buildPrompt(diff);
|
|
52
|
+
const rawMessage = generateCommitMessage(prompt, options, config);
|
|
53
|
+
let commitMessage = parseCommitMessage(rawMessage);
|
|
54
|
+
|
|
55
|
+
if (!isValidCommitMessage(commitMessage)) {
|
|
56
|
+
commitMessage = normalizeCommitMessage(commitMessage);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!isValidCommitMessage(commitMessage)) {
|
|
60
|
+
throw new AppError(ERROR_MESSAGES.INVALID_GENERATED_MESSAGE);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (options.show) {
|
|
64
|
+
writeStdout(
|
|
65
|
+
`Generated commit message (${provider}):\n\n${commitMessage.trim()}\n`,
|
|
66
|
+
);
|
|
67
|
+
process.exitCode = EXIT_CODES.SUCCESS;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
writeStdout(
|
|
72
|
+
`Generated commit message (${provider}):\n\n${commitMessage.trim()}\n\n`,
|
|
73
|
+
);
|
|
74
|
+
let confirmed = false;
|
|
75
|
+
|
|
76
|
+
if (options.yes) {
|
|
77
|
+
confirmed = true;
|
|
78
|
+
} else if (!isInteractiveTerminal()) {
|
|
79
|
+
writeStderr(`${ERROR_MESSAGES.COMMIT_CANCELLED_NON_INTERACTIVE}\n`);
|
|
80
|
+
process.exitCode = EXIT_CODES.SUCCESS;
|
|
81
|
+
return;
|
|
82
|
+
} else {
|
|
83
|
+
confirmed = await askForConfirmation("Create commit? [y/N] ");
|
|
84
|
+
writeStdout("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!confirmed) {
|
|
88
|
+
writeStderr(`${ERROR_MESSAGES.COMMIT_CANCELLED}\n`);
|
|
89
|
+
process.exitCode = EXIT_CODES.SUCCESS;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
createCommit(commitMessage);
|
|
94
|
+
writeStdout("Commit created.\n");
|
|
95
|
+
process.exitCode = EXIT_CODES.SUCCESS;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
const message =
|
|
98
|
+
error instanceof AppError ? error.message : ERROR_MESSAGES.UNKNOWN_ERROR;
|
|
99
|
+
writeStderr(`${message}\n`);
|
|
100
|
+
process.exitCode = EXIT_CODES.ERROR;
|
|
101
|
+
}
|
|
102
|
+
}
|