@sabinm677/ccommit 0.1.0 → 0.2.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 CHANGED
@@ -1,11 +1,12 @@
1
1
  # ccommit
2
2
 
3
- Generate Conventional Commit messages from staged Git changes using Claude Code CLI, OpenCode CLI, or Codex CLI.
3
+ Generate Conventional Commit messages from staged Git changes using Claude Code CLI, OpenCode CLI, Codex CLI, Kilo Code CLI, Qwen Code CLI, or Gemini CLI.
4
4
 
5
5
  ## Requirements
6
- - Bun 1.0+
6
+ - Node 18+ or Bun 1.0+
7
7
  - Git 2.0+
8
- - Claude Code CLI, OpenCode CLI, or Codex CLI configured and available on `PATH`
8
+ - One supported provider CLI configured and available on `PATH` (`claude`, `opencode`, `codex`, `kilo`, `qwen`, or `gemini`)
9
+ - Native support for Windows, macOS, and Linux
9
10
 
10
11
  ## Usage
11
12
  ```bash
@@ -31,6 +32,8 @@ ln -sf "$(pwd)/bin/ccommit" ~/.local/bin/ccommit
31
32
 
32
33
  If needed, add `~/.local/bin` to your `PATH`.
33
34
 
35
+ ## Behavior
36
+
34
37
  Default mode:
35
38
  - Generates a commit message from staged changes
36
39
  - Shows the message
@@ -44,33 +47,61 @@ ccommit --show
44
47
  ccommit -s
45
48
  ```
46
49
 
47
- Choose provider explicitly:
50
+ Override generated first line:
51
+ ```bash
52
+ ccommit -m "ABC-1: Some message"
53
+ ccommit --message "ABC-1: Some message"
54
+ ```
55
+
56
+ Choose provider for one run:
48
57
  ```bash
49
- bin/ccommit -p opencode
50
58
  ccommit --provider opencode
51
- ccommit -p opencode
52
59
  ccommit -p codex
53
- ccommit --yes
60
+ ccommit -p kilo
61
+ ccommit -p qwen
62
+ ccommit -p gemini
63
+ ```
64
+
65
+ Persist a default provider:
66
+ ```bash
67
+ ccommit --set-provider codex
68
+ ccommit -sp opencode
69
+ ccommit --set-provider=opencode
54
70
  ```
55
71
 
56
72
  ## Options
57
73
  - `-s`, `--show`: Generate and print message only. Do not prompt or create commit.
58
74
  - `-y`, `--yes`: Create commit without interactive confirmation.
59
- - `-p`, `--provider <claude|opencode|codex>`: Select AI provider for this run.
75
+ - `-m`, `--message <text>`: Replace the first line of the generated commit message. Remaining lines are preserved.
76
+ - `-p`, `--provider <claude|opencode|codex|kilo|qwen|gemini>`: Select AI provider for this run.
77
+ - `-sp`, `--set-provider <claude|opencode|codex|kilo|qwen|gemini>`: Persist default provider to config and exit.
60
78
  - `-h`, `--help`: Show usage help.
61
79
  - `-v`, `--version`: Show CLI version.
62
80
  - `--verbose`: Print debug details to stderr, including provider failure diagnostics.
63
81
 
64
- ccommit prints the selected provider in the header (including `--show`/`-s` mode):
82
+ ## Commit Format
83
+ ccommit enforces Conventional Commits with a required multi-line message:
84
+ - Header format: `<type>[optional scope][optional !]: <description>`
85
+ - At least one non-empty body line is required
86
+ - `BREAKING CHANGE:` footer is supported
87
+
88
+ Example output:
89
+ ```text
90
+ feat(parser): improve prompt normalization
91
+
92
+ Improve normalization to keep body content stable across providers.
93
+ ```
94
+
95
+ ccommit prints the selected provider in the header (including `--show` mode):
65
96
  ```text
66
- Generated commit message (provider):
67
97
  Generated commit message (claude):
68
- Generated commit message (opencode):
69
- Generated commit message (codex):
70
98
  ```
71
99
 
72
100
  ## Config
73
- Optional config file: `~/.config/ccommit/config.json`
101
+ Optional config file locations:
102
+ - Windows: `%APPDATA%\\ccommit\\config.json`
103
+ - macOS/Linux: `$XDG_CONFIG_HOME/ccommit/config.json` or `~/.config/ccommit/config.json`
104
+ - Override on any OS: `CCOMMIT_CONFIG_PATH=/custom/path/config.json`
74
105
 
75
106
  ```json
76
107
  {
@@ -78,13 +109,17 @@ Optional config file: `~/.config/ccommit/config.json`
78
109
  "providers": {
79
110
  "claude": { "command": "claude", "args": ["-p"] },
80
111
  "opencode": { "command": "opencode", "args": ["run"] },
81
- "codex": { "command": "codex", "args": ["exec"] }
112
+ "codex": { "command": "codex", "args": ["exec"] },
113
+ "kilo": { "command": "kilo", "args": ["run"] },
114
+ "qwen": { "command": "qwen", "args": ["run"] },
115
+ "gemini": { "command": "gemini", "args": ["-p"] }
82
116
  }
83
117
  }
84
118
  ```
85
119
 
86
120
  Precedence for provider/command settings: `CLI > env > config > defaults`.
87
121
  Default provider: `claude`.
122
+ `--set-provider` updates the config file and can run outside a Git repository.
88
123
 
89
124
  ## Exit Codes
90
125
  - `0`: success
@@ -96,6 +131,21 @@ Run tests:
96
131
  bun test
97
132
  ```
98
133
 
134
+ Build standalone executables:
135
+ ```bash
136
+ bun run build:bin
137
+ # or only current platform/arch
138
+ bun run build:bin:current
139
+ ```
140
+
141
+ Build outputs are written to `dist/`:
142
+ - `ccommit-darwin-x64`
143
+ - `ccommit-darwin-arm64`
144
+ - `ccommit-linux-x64`
145
+ - `ccommit-linux-arm64`
146
+ - `ccommit-windows-x64.exe`
147
+ - `ccommit-windows-arm64.exe`
148
+
99
149
  Node compatibility:
100
150
  - `ccommit` is Bun-first, but basic CLI invocation with Node remains supported (for example `node bin/ccommit --version`).
101
151
 
@@ -106,8 +156,11 @@ For local testing without provider API calls:
106
156
  - `CCOMMIT_MOCK_ERROR=NETWORK` to force network error.
107
157
 
108
158
  Provider env overrides:
109
- - `CCOMMIT_PROVIDER=claude|opencode|codex`
159
+ - `CCOMMIT_PROVIDER=claude|opencode|codex|kilo|qwen|gemini`
110
160
  - `CCOMMIT_CLAUDE_CMD`, `CCOMMIT_CLAUDE_ARGS`
111
161
  - `CCOMMIT_OPENCODE_CMD`, `CCOMMIT_OPENCODE_ARGS`
112
162
  - `CCOMMIT_CODEX_CMD`, `CCOMMIT_CODEX_ARGS`
163
+ - `CCOMMIT_KILO_CMD`, `CCOMMIT_KILO_ARGS`
164
+ - `CCOMMIT_QWEN_CMD`, `CCOMMIT_QWEN_ARGS`
165
+ - `CCOMMIT_GEMINI_CMD`, `CCOMMIT_GEMINI_ARGS`
113
166
  - `CCOMMIT_PROVIDER_TIMEOUT_MS` (optional, default `60000`)
package/bin/ccommit CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env node
2
2
  import { run } from "../src/main.js";
3
3
 
4
4
  await run(process.argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabinm677/ccommit",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "AI-powered Git commit message generator with Conventional Commits output.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -31,7 +31,9 @@
31
31
  "ccommit": "./bin/ccommit"
32
32
  },
33
33
  "scripts": {
34
- "test": "bun test"
34
+ "test": "bun test",
35
+ "build:bin": "node scripts/build-binaries.mjs",
36
+ "build:bin:current": "node scripts/build-binaries.mjs --current"
35
37
  },
36
38
  "engines": {
37
39
  "bun": ">=1.0.0",
@@ -82,6 +82,20 @@ function writeVerboseFailure(options, details) {
82
82
  );
83
83
  }
84
84
 
85
+ export function getProviderSpawnOptions(
86
+ timeout,
87
+ { platform = process.platform } = {},
88
+ ) {
89
+ const isWindows = platform === "win32";
90
+ return {
91
+ encoding: "utf8",
92
+ maxBuffer: 10 * 1024 * 1024,
93
+ timeout,
94
+ shell: isWindows,
95
+ windowsHide: isWindows,
96
+ };
97
+ }
98
+
85
99
  export function generateCommitMessage(prompt, options = {}, config = {}) {
86
100
  const provider = resolveProvider(options, config);
87
101
  const errors = getProviderErrors();
@@ -103,11 +117,11 @@ export function generateCommitMessage(prompt, options = {}, config = {}) {
103
117
  const timeout = resolveProviderTimeoutMs();
104
118
  let result;
105
119
  try {
106
- result = spawnSync(command, [...args, prompt], {
107
- encoding: "utf8",
108
- maxBuffer: 10 * 1024 * 1024,
109
- timeout,
110
- });
120
+ result = spawnSync(
121
+ command,
122
+ [...args, prompt],
123
+ getProviderSpawnOptions(timeout),
124
+ );
111
125
  } catch (error) {
112
126
  writeVerboseFailure(options, {
113
127
  provider,
@@ -51,7 +51,7 @@ Rules:
51
51
  - Allowed types: ${COMMIT_TYPES.join(", ")}.
52
52
  - Format header exactly: <type>[optional scope][optional !]: <description>
53
53
  - Description must be concise and imperative.
54
- - Prefer a single-line commit message. Add body/footer only if essential.
54
+ - Use a multi-line commit message with a meaningful body.
55
55
  - Do not claim changes that are not present in the staged diff.
56
56
  - Do not use markdown bullets in the commit body.
57
57
  - ${typeGuidance}
@@ -33,7 +33,13 @@ export function resolveCommand(provider, config = {}) {
33
33
  ? { command: "claude", args: ["-p"] }
34
34
  : provider === "opencode"
35
35
  ? { command: "opencode", args: ["run"] }
36
- : { command: "codex", args: ["exec"] };
36
+ : provider === "codex"
37
+ ? { command: "codex", args: ["exec"] }
38
+ : provider === "kilo"
39
+ ? { command: "kilo", args: ["run"] }
40
+ : provider === "qwen"
41
+ ? { command: "qwen", args: ["run"] }
42
+ : { command: "gemini", args: ["-p"] };
37
43
 
38
44
  if (provider === "claude") {
39
45
  if (process.env.CCOMMIT_CLAUDE_CMD || process.env.CCOMMIT_CLAUDE_ARGS) {
@@ -58,6 +64,29 @@ export function resolveCommand(provider, config = {}) {
58
64
  args: splitArgs(process.env.CCOMMIT_CODEX_ARGS || defaults.args.join(" ")),
59
65
  };
60
66
  }
67
+ } else if (provider === "kilo") {
68
+ if (process.env.CCOMMIT_KILO_CMD || process.env.CCOMMIT_KILO_ARGS) {
69
+ return {
70
+ command: process.env.CCOMMIT_KILO_CMD || defaults.command,
71
+ args: splitArgs(process.env.CCOMMIT_KILO_ARGS || defaults.args.join(" ")),
72
+ };
73
+ }
74
+ } else if (provider === "qwen") {
75
+ if (process.env.CCOMMIT_QWEN_CMD || process.env.CCOMMIT_QWEN_ARGS) {
76
+ return {
77
+ command: process.env.CCOMMIT_QWEN_CMD || defaults.command,
78
+ args: splitArgs(process.env.CCOMMIT_QWEN_ARGS || defaults.args.join(" ")),
79
+ };
80
+ }
81
+ } else if (provider === "gemini") {
82
+ if (process.env.CCOMMIT_GEMINI_CMD || process.env.CCOMMIT_GEMINI_ARGS) {
83
+ return {
84
+ command: process.env.CCOMMIT_GEMINI_CMD || defaults.command,
85
+ args: splitArgs(
86
+ process.env.CCOMMIT_GEMINI_ARGS || defaults.args.join(" "),
87
+ ),
88
+ };
89
+ }
61
90
  }
62
91
 
63
92
  const providerConfig = config.providers?.[provider];
@@ -1,4 +1,11 @@
1
- export const AI_PROVIDERS = ["claude", "opencode", "codex"];
1
+ export const AI_PROVIDERS = [
2
+ "claude",
3
+ "opencode",
4
+ "codex",
5
+ "kilo",
6
+ "qwen",
7
+ "gemini",
8
+ ];
2
9
 
3
10
  export function isValidProvider(provider) {
4
11
  return AI_PROVIDERS.includes(provider);
@@ -6,9 +6,18 @@ function stripCodeFence(text) {
6
6
  return text;
7
7
  }
8
8
 
9
+ function stripCommitTrailers(text) {
10
+ return text
11
+ .split("\n")
12
+ .filter((line) => !/^co-authored-by:\s*/i.test(line.trim()))
13
+ .join("\n")
14
+ .trim();
15
+ }
16
+
9
17
  export function parseCommitMessage(rawOutput) {
10
18
  let text = stripCodeFence(rawOutput).trim();
11
19
  text = text.replace(/^commit message:\s*/i, "").trim();
12
20
  text = text.replace(/^message:\s*/i, "").trim();
21
+ text = stripCommitTrailers(text);
13
22
  return text;
14
23
  }
package/src/cli/args.js CHANGED
@@ -10,6 +10,8 @@ export function parseArgs(args) {
10
10
  version: false,
11
11
  verbose: false,
12
12
  provider: undefined,
13
+ setDefaultProvider: undefined,
14
+ message: undefined,
13
15
  };
14
16
 
15
17
  for (let index = 0; index < args.length; index += 1) {
@@ -34,6 +36,25 @@ export function parseArgs(args) {
34
36
  options.verbose = true;
35
37
  continue;
36
38
  }
39
+ if (arg === "--message" || arg === "-m") {
40
+ const value = args[index + 1];
41
+ if (!value || value.startsWith("-") || !value.trim()) {
42
+ throw new AppError("Missing value for --message/-m.");
43
+ }
44
+ options.message = value.trim();
45
+ index += 1;
46
+ continue;
47
+ }
48
+ if (arg.startsWith("--message=") || arg.startsWith("-m=")) {
49
+ const value = arg.startsWith("--message=")
50
+ ? arg.slice("--message=".length)
51
+ : arg.slice("-m=".length);
52
+ if (!value.trim()) {
53
+ throw new AppError("Missing value for --message/-m.");
54
+ }
55
+ options.message = value.trim();
56
+ continue;
57
+ }
37
58
  if (arg === "--provider" || arg === "-p") {
38
59
  const value = args[index + 1];
39
60
  if (!value || value.startsWith("-")) {
@@ -56,9 +77,37 @@ export function parseArgs(args) {
56
77
  options.provider = value;
57
78
  continue;
58
79
  }
80
+ if (arg === "--set-provider" || arg === "-sp") {
81
+ const value = args[index + 1];
82
+ if (!value || value.startsWith("-")) {
83
+ throw new AppError("Missing value for --set-provider/-sp.");
84
+ }
85
+ if (!isValidProvider(value)) {
86
+ throw new AppError(ERROR_MESSAGES.INVALID_PROVIDER);
87
+ }
88
+ options.setDefaultProvider = value;
89
+ index += 1;
90
+ continue;
91
+ }
92
+ if (arg.startsWith("--set-provider=") || arg.startsWith("-sp=")) {
93
+ const value = arg.startsWith("--set-provider=")
94
+ ? arg.slice("--set-provider=".length)
95
+ : arg.slice("-sp=".length);
96
+ if (!isValidProvider(value)) {
97
+ throw new AppError(ERROR_MESSAGES.INVALID_PROVIDER);
98
+ }
99
+ options.setDefaultProvider = value;
100
+ continue;
101
+ }
59
102
  throw new AppError(`Unknown option: ${arg}`);
60
103
  }
61
104
 
105
+ if (options.provider && options.setDefaultProvider) {
106
+ throw new AppError(
107
+ "Cannot use --provider/-p together with --set-provider/-sp.",
108
+ );
109
+ }
110
+
62
111
  return options;
63
112
  }
64
113
 
@@ -70,7 +119,9 @@ Generate a Conventional Commit message from staged Git changes.
70
119
  Options:
71
120
  -s, --show Show generated message only (do not commit)
72
121
  -y, --yes Create commit without interactive confirmation
73
- -p, --provider AI provider: claude | opencode | codex
122
+ -m, --message Override first line of generated message
123
+ -p, --provider AI provider: claude | opencode | codex | kilo | qwen | gemini
124
+ -sp, --set-provider Persist default provider: claude | opencode | codex | kilo | qwen | gemini
74
125
  -h, --help Show help
75
126
  -v, --version Show version
76
127
  --verbose Print debug information to stderr
@@ -0,0 +1,6 @@
1
+ import { run } from "../main.js";
2
+
3
+ await run(process.argv.slice(2), {
4
+ version: process.env.CCOMMIT_VERSION,
5
+ });
6
+ process.exit(process.exitCode ?? 0);
@@ -12,10 +12,23 @@ function sanitizeDescription(raw) {
12
12
  return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
13
13
  }
14
14
 
15
+ function toBodyLine(description) {
16
+ const sentence = sanitizeDescription(description);
17
+ return `${sentence.charAt(0).toUpperCase()}${sentence.slice(1)}.`;
18
+ }
19
+
20
+ function withBody(header, rest) {
21
+ if (rest.some((line) => line.trim().length > 0)) {
22
+ return [header, ...rest].join("\n").trim();
23
+ }
24
+ const description = header.split(":").slice(1).join(":").trim();
25
+ return `${header}\n\n${toBodyLine(description)}`;
26
+ }
27
+
15
28
  export function normalizeCommitMessage(message) {
16
29
  const normalized = message.trim();
17
30
  if (!normalized) {
18
- return "chore: update project files";
31
+ return "chore: update project files\n\nUpdate project files.";
19
32
  }
20
33
 
21
34
  const [header, ...rest] = normalized.split("\n");
@@ -25,12 +38,16 @@ export function normalizeCommitMessage(message) {
25
38
  );
26
39
 
27
40
  if (matchedType) {
28
- return [cleanedHeader, ...rest].join("\n").trim();
41
+ return withBody(cleanedHeader, rest);
29
42
  }
30
43
 
31
44
  if (/^[a-z]+(\([^)]+\))?!?:/i.test(cleanedHeader)) {
32
- return `chore: ${sanitizeDescription(cleanedHeader.split(":").slice(1).join(":"))}`;
45
+ const normalizedHeader = `chore: ${sanitizeDescription(
46
+ cleanedHeader.split(":").slice(1).join(":"),
47
+ )}`;
48
+ return withBody(normalizedHeader, rest);
33
49
  }
34
50
 
35
- return `chore: ${sanitizeDescription(cleanedHeader)}`;
51
+ const normalizedHeader = `chore: ${sanitizeDescription(cleanedHeader)}`;
52
+ return withBody(normalizedHeader, rest);
36
53
  }
@@ -12,11 +12,15 @@ export function isValidCommitMessage(message) {
12
12
  return false;
13
13
  }
14
14
 
15
- const [header] = normalized.split("\n");
15
+ const [header, ...rest] = normalized.split("\n");
16
16
  if (!HEADER_REGEX.test(header.trim())) {
17
17
  return false;
18
18
  }
19
19
 
20
+ if (!rest.some((line) => line.trim().length > 0)) {
21
+ return false;
22
+ }
23
+
20
24
  return true;
21
25
  }
22
26
 
@@ -1,6 +1,6 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { join } from "node:path";
3
+ import { dirname, join, resolve } from "node:path";
4
4
  import { AppError } from "../errors/app-error.js";
5
5
  import { ERROR_MESSAGES } from "../errors/messages.js";
6
6
  import { isValidProvider } from "../ai/providers.js";
@@ -11,15 +11,69 @@ const defaultConfig = Object.freeze({
11
11
  claude: { command: undefined, args: undefined },
12
12
  opencode: { command: undefined, args: undefined },
13
13
  codex: { command: undefined, args: undefined },
14
+ kilo: { command: undefined, args: undefined },
15
+ qwen: { command: undefined, args: undefined },
16
+ gemini: { command: undefined, args: undefined },
14
17
  },
15
18
  });
16
19
 
20
+ export function getLegacyConfigPath(homeDir = homedir()) {
21
+ return join(homeDir, ".config", "ccommit", "config.json");
22
+ }
23
+
24
+ export function getPrimaryConfigPath({
25
+ env = process.env,
26
+ platform = process.platform,
27
+ homeDir = homedir(),
28
+ cwd = process.cwd(),
29
+ } = {}) {
30
+ const overridePath = env.CCOMMIT_CONFIG_PATH?.trim();
31
+ if (overridePath) {
32
+ return resolve(cwd, overridePath);
33
+ }
34
+
35
+ if (platform === "win32") {
36
+ const appData = env.APPDATA?.trim();
37
+ if (appData) {
38
+ return join(appData, "ccommit", "config.json");
39
+ }
40
+ return getLegacyConfigPath(homeDir);
41
+ }
42
+
43
+ const xdgConfigHome = env.XDG_CONFIG_HOME?.trim();
44
+ if (xdgConfigHome) {
45
+ return join(xdgConfigHome, "ccommit", "config.json");
46
+ }
47
+
48
+ return getLegacyConfigPath(homeDir);
49
+ }
50
+
51
+ export function getReadableConfigPath({
52
+ exists = existsSync,
53
+ env = process.env,
54
+ platform = process.platform,
55
+ homeDir = homedir(),
56
+ cwd = process.cwd(),
57
+ } = {}) {
58
+ const primaryPath = getPrimaryConfigPath({ env, platform, homeDir, cwd });
59
+ if (exists(primaryPath)) {
60
+ return primaryPath;
61
+ }
62
+
63
+ const legacyPath = getLegacyConfigPath(homeDir);
64
+ if (legacyPath !== primaryPath && exists(legacyPath)) {
65
+ return legacyPath;
66
+ }
67
+
68
+ return primaryPath;
69
+ }
70
+
17
71
  export function getConfigPath() {
18
- return join(homedir(), ".config", "ccommit", "config.json");
72
+ return getPrimaryConfigPath();
19
73
  }
20
74
 
21
75
  export function loadConfig() {
22
- const configPath = getConfigPath();
76
+ const configPath = getReadableConfigPath();
23
77
  if (!existsSync(configPath)) {
24
78
  return structuredClone(defaultConfig);
25
79
  }
@@ -34,6 +88,30 @@ export function loadConfig() {
34
88
  return normalizeConfig(raw);
35
89
  }
36
90
 
91
+ export function setDefaultProvider(provider) {
92
+ const normalizedProvider = provider?.trim();
93
+ if (!normalizedProvider || !isValidProvider(normalizedProvider)) {
94
+ throw new AppError(ERROR_MESSAGES.INVALID_PROVIDER);
95
+ }
96
+
97
+ const configPath = getConfigPath();
98
+ const readablePath = getReadableConfigPath();
99
+ let raw = {};
100
+
101
+ if (existsSync(readablePath)) {
102
+ try {
103
+ raw = JSON.parse(readFileSync(readablePath, "utf8"));
104
+ } catch {
105
+ throw new AppError(ERROR_MESSAGES.INVALID_CONFIG_JSON);
106
+ }
107
+ normalizeConfig(raw);
108
+ }
109
+
110
+ raw.provider = normalizedProvider;
111
+ mkdirSync(dirname(configPath), { recursive: true });
112
+ writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\n`, "utf8");
113
+ }
114
+
37
115
  function normalizeConfig(raw) {
38
116
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
39
117
  throw new AppError(ERROR_MESSAGES.INVALID_CONFIG);
@@ -59,7 +137,14 @@ function normalizeConfig(raw) {
59
137
  normalized.provider = provider.trim();
60
138
  }
61
139
 
62
- for (const providerName of ["claude", "opencode", "codex"]) {
140
+ for (const providerName of [
141
+ "claude",
142
+ "opencode",
143
+ "codex",
144
+ "kilo",
145
+ "qwen",
146
+ "gemini",
147
+ ]) {
63
148
  const value = providers?.[providerName];
64
149
  if (value === undefined) {
65
150
  continue;
@@ -18,7 +18,7 @@ export const ERROR_MESSAGES = {
18
18
  INVALID_GENERATED_MESSAGE:
19
19
  "Generated commit message is invalid. Please retry or commit manually.",
20
20
  INVALID_PROVIDER:
21
- "Invalid provider. Supported providers: claude, opencode, codex.",
21
+ "Invalid provider. Supported providers: claude, opencode, codex, kilo, qwen, gemini.",
22
22
  INVALID_CONFIG:
23
23
  "Invalid config file at ~/.config/ccommit/config.json.",
24
24
  INVALID_CONFIG_JSON:
package/src/main.js CHANGED
@@ -16,16 +16,32 @@ import { resolveProvider } from "./ai/provider-resolution.js";
16
16
  import { parseCommitMessage } from "./ai/response-parser.js";
17
17
  import { isValidCommitMessage } from "./commit/validator.js";
18
18
  import { normalizeCommitMessage } from "./commit/normalizer.js";
19
- import { loadConfig } from "./config/loader.js";
19
+ import { getConfigPath, loadConfig, setDefaultProvider } from "./config/loader.js";
20
+
21
+ export function resolveVersion(runtime = {}) {
22
+ if (runtime.version) {
23
+ return runtime.version;
24
+ }
25
+
26
+ if (process.env.CCOMMIT_VERSION) {
27
+ return process.env.CCOMMIT_VERSION;
28
+ }
20
29
 
21
- function getVersion() {
22
30
  const currentFile = fileURLToPath(import.meta.url);
23
31
  const root = join(dirname(currentFile), "..");
24
32
  const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
25
33
  return pkg.version;
26
34
  }
27
35
 
28
- export async function run(args = []) {
36
+ function replaceHeader(message, header) {
37
+ const [_, ...rest] = message.split("\n");
38
+ if (rest.length === 0) {
39
+ return header;
40
+ }
41
+ return [header, ...rest].join("\n");
42
+ }
43
+
44
+ export async function run(args = [], runtime = {}) {
29
45
  try {
30
46
  const options = parseArgs(args);
31
47
 
@@ -36,7 +52,16 @@ export async function run(args = []) {
36
52
  }
37
53
 
38
54
  if (options.version) {
39
- writeStdout(`${getVersion()}\n`);
55
+ writeStdout(`${resolveVersion(runtime)}\n`);
56
+ process.exitCode = EXIT_CODES.SUCCESS;
57
+ return;
58
+ }
59
+
60
+ if (options.setDefaultProvider) {
61
+ setDefaultProvider(options.setDefaultProvider);
62
+ writeStdout(
63
+ `Default provider saved to ${getConfigPath()} as ${options.setDefaultProvider}.\n`,
64
+ );
40
65
  process.exitCode = EXIT_CODES.SUCCESS;
41
66
  return;
42
67
  }
@@ -60,6 +85,10 @@ export async function run(args = []) {
60
85
  throw new AppError(ERROR_MESSAGES.INVALID_GENERATED_MESSAGE);
61
86
  }
62
87
 
88
+ if (options.message) {
89
+ commitMessage = replaceHeader(commitMessage, options.message);
90
+ }
91
+
63
92
  if (options.show) {
64
93
  writeStdout(
65
94
  `Generated commit message (${provider}):\n\n${commitMessage.trim()}\n`,