@thankarezos/git-ai-commit 0.1.3 → 0.1.4
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 +17 -3
- package/bin/git-aic.js +38 -5
- package/bin/presets.js +8 -4
- package/bin/run-provider.js +41 -13
- package/bin/spawn.js +97 -0
- package/bin/wizard.js +5 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# git-aic
|
|
2
2
|
|
|
3
|
-
Generate a git commit message from your staged diff using any AI CLI you already have on your machine (Claude Code,
|
|
3
|
+
Generate a git commit message from your staged diff using any AI CLI you already have on your machine (Claude Code, Antigravity CLI, Codex, `llm`, Ollama, or a custom command). The message opens in your editor — save to commit, quit without saving to abort.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
$ git aic -ap "mention the migration"
|
|
@@ -44,7 +44,7 @@ First run launches an **interactive wizard** that asks you to pick a provider, e
|
|
|
44
44
|
| `-a`, `--add` | Run `git add -A` before generating |
|
|
45
45
|
| `-p "..."`, `--prompt "..."` | Append extra instructions to the AI prompt |
|
|
46
46
|
| `-ap "..."`, `-pa "..."` | Combine `-a` and `-p` |
|
|
47
|
-
| `--provider <name>`, `-P <name>` | One-off provider override. `<name>` matches a preset command (`claude`, `
|
|
47
|
+
| `--provider <name>`, `-P <name>` | One-off provider override. `<name>` matches a preset command (`claude`, `agy`, `codex`, `opencode`, `llm`, `ollama`). |
|
|
48
48
|
| `--no-commit` | Generate and edit, but skip the `git commit` |
|
|
49
49
|
| `--commit` | Force commit (overrides `commit: false` in config) |
|
|
50
50
|
| `--require-edit` | Refuse to commit if the saved message is byte-identical to what the AI produced |
|
|
@@ -100,7 +100,7 @@ Each field in your user config **replaces** the default if present. If you delet
|
|
|
100
100
|
The wizard ships with presets for:
|
|
101
101
|
|
|
102
102
|
- **Claude Code** — `claude -p "{prompt}"` (reads diff from stdin)
|
|
103
|
-
- **
|
|
103
|
+
- **Antigravity CLI** — `agy -p "{prompt}"`
|
|
104
104
|
- **Codex CLI** — `codex exec --skip-git-repo-check "{prompt}"`
|
|
105
105
|
- **OpenCode** — `opencode run "{prompt}"`
|
|
106
106
|
- **llm** (Simon Willison) — `llm "{prompt}"`
|
|
@@ -109,6 +109,20 @@ The wizard ships with presets for:
|
|
|
109
109
|
|
|
110
110
|
You can edit `provider` in the config file at any time, or rerun `git aic --init`.
|
|
111
111
|
|
|
112
|
+
### Windows note
|
|
113
|
+
|
|
114
|
+
Most CLIs work the same on Windows. The exception is a CLI that renders its output to the console instead of stdout (like `agy`): to capture it, `git-aic` must spawn it with no console, which briefly flashes a console window. A provider can opt into this with a `windows` field carrying extra spawn options:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
"provider": {
|
|
118
|
+
"command": "agy",
|
|
119
|
+
"args": ["-p", "{prompt}"],
|
|
120
|
+
"windows": { "detached": true }
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
CLIs that write to stdout (e.g. `claude`) don't need this and run without any window.
|
|
125
|
+
|
|
112
126
|
## Customizing the prompt
|
|
113
127
|
|
|
114
128
|
The base prompt template, the `<commit_message>` tag, and the cleanup patterns live in `bin/prompts.json`. Edit that file to:
|
package/bin/git-aic.js
CHANGED
|
@@ -16,6 +16,7 @@ import { runInitWizard } from "./wizard.js";
|
|
|
16
16
|
import { cleanCommitMessage } from "./clean.js";
|
|
17
17
|
import { extractCommitMessage } from "./extract.js";
|
|
18
18
|
import { runProvider } from "./run-provider.js";
|
|
19
|
+
import { spawnCross } from "./spawn.js";
|
|
19
20
|
import { applyUserOverrides, getConfiguration } from "./prompts.js";
|
|
20
21
|
import { getConfigPath, loadConfig } from "./get-config-path.js";
|
|
21
22
|
import { PROVIDER_PRESETS } from "./presets.js";
|
|
@@ -28,6 +29,9 @@ function fail(message, code = 1) {
|
|
|
28
29
|
function run(command, args, options = {}) {
|
|
29
30
|
return spawnSync(command, args, {
|
|
30
31
|
encoding: "utf8",
|
|
32
|
+
// Suppress the transient console window Windows flashes for piped child
|
|
33
|
+
// processes (no effect on other platforms).
|
|
34
|
+
windowsHide: true,
|
|
31
35
|
...options,
|
|
32
36
|
});
|
|
33
37
|
}
|
|
@@ -41,6 +45,7 @@ function parseArgs(argv) {
|
|
|
41
45
|
let runInit = false;
|
|
42
46
|
let shouldAdd = false;
|
|
43
47
|
let providerName = null;
|
|
48
|
+
let verbose = false;
|
|
44
49
|
|
|
45
50
|
for (let i = 0; i < argv.length; i++) {
|
|
46
51
|
const arg = argv[i];
|
|
@@ -105,6 +110,11 @@ function parseArgs(argv) {
|
|
|
105
110
|
continue;
|
|
106
111
|
}
|
|
107
112
|
|
|
113
|
+
if (arg === "-V" || arg === "--verbose") {
|
|
114
|
+
verbose = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
108
118
|
if (arg === "--init") {
|
|
109
119
|
runInit = true;
|
|
110
120
|
continue;
|
|
@@ -117,11 +127,12 @@ Usage:
|
|
|
117
127
|
git aic -a # stage all changes first (git add -A)
|
|
118
128
|
git aic -p "extra prompt"
|
|
119
129
|
git aic -ap "extra prompt" # combine -a and -p
|
|
120
|
-
git aic --provider
|
|
130
|
+
git aic --provider agy # one-off provider override (also -P)
|
|
121
131
|
git aic --provider # list available providers
|
|
122
132
|
git aic --no-commit
|
|
123
133
|
git aic --config
|
|
124
134
|
git aic -v, --version
|
|
135
|
+
git aic -V, --verbose # print the prompt and raw provider output
|
|
125
136
|
git aic --init
|
|
126
137
|
|
|
127
138
|
Examples:
|
|
@@ -154,6 +165,7 @@ Environment:
|
|
|
154
165
|
runInit,
|
|
155
166
|
shouldAdd,
|
|
156
167
|
providerName,
|
|
168
|
+
verbose,
|
|
157
169
|
};
|
|
158
170
|
}
|
|
159
171
|
|
|
@@ -181,6 +193,7 @@ function readStagedDiff() {
|
|
|
181
193
|
{
|
|
182
194
|
encoding: "utf8",
|
|
183
195
|
maxBuffer: 50 * 1024 * 1024,
|
|
196
|
+
windowsHide: true,
|
|
184
197
|
}
|
|
185
198
|
);
|
|
186
199
|
} catch (error) {
|
|
@@ -228,7 +241,11 @@ if (args.providerName) {
|
|
|
228
241
|
const names = PROVIDER_PRESETS.map((p) => p.command).join(", ");
|
|
229
242
|
fail(`Unknown provider: ${args.providerName}. Available: ${names}`);
|
|
230
243
|
}
|
|
231
|
-
config.provider = {
|
|
244
|
+
config.provider = {
|
|
245
|
+
command: preset.command,
|
|
246
|
+
args: preset.args,
|
|
247
|
+
...(preset.windows ? { windows: preset.windows } : {}),
|
|
248
|
+
};
|
|
232
249
|
}
|
|
233
250
|
|
|
234
251
|
if (args.printConfig) {
|
|
@@ -270,13 +287,25 @@ const prompt = buildPrompt(args.extraPrompt);
|
|
|
270
287
|
|
|
271
288
|
console.log(`Using provider: ${config.provider.command}`);
|
|
272
289
|
|
|
290
|
+
if (args.verbose) {
|
|
291
|
+
console.error("[verbose] ----- prompt -----");
|
|
292
|
+
console.error(prompt);
|
|
293
|
+
console.error("[verbose] ----- end prompt -----");
|
|
294
|
+
}
|
|
295
|
+
|
|
273
296
|
let raw;
|
|
274
297
|
try {
|
|
275
|
-
raw = runProvider(config.provider, prompt, safeDiff);
|
|
298
|
+
raw = runProvider(config.provider, prompt, safeDiff, { verbose: args.verbose });
|
|
276
299
|
} catch (error) {
|
|
277
300
|
fail(error.message);
|
|
278
301
|
}
|
|
279
302
|
|
|
303
|
+
if (args.verbose) {
|
|
304
|
+
console.error("[verbose] ----- raw provider output -----");
|
|
305
|
+
console.error(raw);
|
|
306
|
+
console.error("[verbose] ----- end raw provider output -----");
|
|
307
|
+
}
|
|
308
|
+
|
|
280
309
|
const message = extractCommitMessage(raw);
|
|
281
310
|
|
|
282
311
|
if (!message) {
|
|
@@ -291,8 +320,12 @@ writeFileSync(file, `${message.trim()}\n`);
|
|
|
291
320
|
const before = sha256(readFileSync(file, "utf8"));
|
|
292
321
|
const mtimeBefore = statSync(file).mtimeMs;
|
|
293
322
|
|
|
294
|
-
const
|
|
295
|
-
const
|
|
323
|
+
const defaultEditor = process.platform === "win32" ? "notepad" : "nano";
|
|
324
|
+
const editor = process.env.EDITOR || config.editor || defaultEditor;
|
|
325
|
+
// Editor configs can carry flags (e.g. "code --wait"), so split the command
|
|
326
|
+
// from its arguments before spawning.
|
|
327
|
+
const [editorCommand, ...editorArgs] = editor.trim().split(/\s+/);
|
|
328
|
+
const editorResult = spawnCross(editorCommand, [...editorArgs, file], {
|
|
296
329
|
stdio: "inherit",
|
|
297
330
|
});
|
|
298
331
|
|
package/bin/presets.js
CHANGED
|
@@ -5,9 +5,13 @@ export const PROVIDER_PRESETS = [
|
|
|
5
5
|
args: ["-p", "{prompt}"]
|
|
6
6
|
},
|
|
7
7
|
{
|
|
8
|
-
name: "
|
|
9
|
-
command: "
|
|
10
|
-
args: ["-p", "{prompt}"]
|
|
8
|
+
name: "Antigravity CLI",
|
|
9
|
+
command: "agy",
|
|
10
|
+
args: ["-p", "{prompt}"],
|
|
11
|
+
// agy renders to the Windows console device instead of stdout, so it can
|
|
12
|
+
// only be captured when spawned with no console (detached). This pops a
|
|
13
|
+
// brief console window on Windows; CLIs that write to stdout don't need it.
|
|
14
|
+
windows: { detached: true }
|
|
11
15
|
},
|
|
12
16
|
{
|
|
13
17
|
name: "Codex CLI",
|
|
@@ -31,4 +35,4 @@ export const PROVIDER_PRESETS = [
|
|
|
31
35
|
}
|
|
32
36
|
];
|
|
33
37
|
|
|
34
|
-
export const EDITOR_PRESETS = ["nano", "vim", "nvim", "code --wait", "micro", "hx"];
|
|
38
|
+
export const EDITOR_PRESETS = ["nano", "vim", "nvim", "code --wait", "micro", "hx", "notepad"];
|
package/bin/run-provider.js
CHANGED
|
@@ -1,30 +1,58 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnCross, isWindowsShim } from "./spawn.js";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Keep verbose output readable: show short args verbatim, summarise long ones
|
|
4
|
+
// (like the full prompt) by byte size instead of dumping them.
|
|
5
|
+
function previewArg(arg) {
|
|
6
|
+
return arg.length > 60 ? `<${Buffer.byteLength(arg)} bytes>` : arg;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function runProvider(provider, prompt, diff, { verbose = false } = {}) {
|
|
4
10
|
if (!provider?.command) {
|
|
5
11
|
throw new Error("Config error: provider.command is required.");
|
|
6
12
|
}
|
|
7
13
|
|
|
8
14
|
const args = provider.args ?? [];
|
|
9
|
-
|
|
10
|
-
const finalArgs = args.map((arg) =>
|
|
11
|
-
arg
|
|
12
|
-
.replaceAll("{prompt}", prompt)
|
|
13
|
-
.replaceAll("{diff}", diff)
|
|
14
|
-
);
|
|
15
|
-
|
|
16
15
|
const usesPromptPlaceholder = args.some((arg) => arg.includes("{prompt}"));
|
|
17
16
|
const usesDiffPlaceholder = args.some((arg) => arg.includes("{diff}"));
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
let finalArgs;
|
|
19
|
+
let input;
|
|
20
|
+
|
|
21
|
+
if (isWindowsShim(provider.command) && !usesDiffPlaceholder) {
|
|
22
|
+
// Batch shims (e.g. claude.cmd) run through cmd.exe, which truncates a
|
|
23
|
+
// command-line argument at the first newline — so the multi-line prompt
|
|
24
|
+
// can't be passed inline. Send the prompt and diff via stdin and drop the
|
|
25
|
+
// inline {prompt}; CLIs like claude read the prompt from stdin when no
|
|
26
|
+
// inline prompt is given.
|
|
27
|
+
finalArgs = args.filter((arg) => arg.trim() !== "{prompt}");
|
|
28
|
+
input = `${prompt}\n\n${diff}`;
|
|
29
|
+
} else {
|
|
30
|
+
finalArgs = args.map((arg) =>
|
|
31
|
+
arg.replaceAll("{prompt}", prompt).replaceAll("{diff}", diff)
|
|
32
|
+
);
|
|
33
|
+
input = usesDiffPlaceholder ? undefined : diff;
|
|
34
|
+
if (!usesPromptPlaceholder) {
|
|
35
|
+
finalArgs.push(prompt);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
20
38
|
|
|
21
|
-
if (
|
|
22
|
-
finalArgs.
|
|
39
|
+
if (verbose) {
|
|
40
|
+
const preview = finalArgs.map(previewArg).join(" ");
|
|
41
|
+
console.error(`[verbose] command: ${provider.command} ${preview}`);
|
|
42
|
+
console.error(
|
|
43
|
+
`[verbose] stdin: ${input ? `${Buffer.byteLength(input)} bytes` : "(none)"}`
|
|
44
|
+
);
|
|
23
45
|
}
|
|
24
46
|
|
|
25
|
-
|
|
47
|
+
// Per-provider Windows spawn options (e.g. { detached: true } for agy).
|
|
48
|
+
// Applied only on Windows; ignored elsewhere.
|
|
49
|
+
const windowsOptions =
|
|
50
|
+
process.platform === "win32" ? provider.windows ?? {} : {};
|
|
51
|
+
|
|
52
|
+
const result = spawnCross(provider.command, finalArgs, {
|
|
26
53
|
input,
|
|
27
54
|
encoding: "utf8",
|
|
55
|
+
...windowsOptions,
|
|
28
56
|
});
|
|
29
57
|
|
|
30
58
|
if (result.status !== 0) {
|
package/bin/spawn.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { delimiter, extname, isAbsolute, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const isWindows = process.platform === "win32";
|
|
6
|
+
|
|
7
|
+
// On Windows, a CLI on PATH may be a native executable (`agy.exe`) or a batch
|
|
8
|
+
// shim that npm generates for global installs (`claude.cmd`). Native exes can
|
|
9
|
+
// be spawned directly and accept multi-line arguments; batch shims must be run
|
|
10
|
+
// through cmd.exe, which mangles multi-line arguments. We need to tell them
|
|
11
|
+
// apart, so resolve the command to a real file and inspect its extension.
|
|
12
|
+
function resolveWindowsCommand(command) {
|
|
13
|
+
if (isAbsolute(command) || command.includes("\\") || command.includes("/")) {
|
|
14
|
+
return { file: command, ext: extname(command).toLowerCase() };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const pathext = (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD")
|
|
18
|
+
.split(";")
|
|
19
|
+
.filter(Boolean);
|
|
20
|
+
const hasExt = extname(command) !== "";
|
|
21
|
+
const candidates = hasExt ? [command] : pathext.map((ext) => command + ext);
|
|
22
|
+
const dirs = (process.env.PATH || "").split(delimiter).filter(Boolean);
|
|
23
|
+
|
|
24
|
+
for (const dir of dirs) {
|
|
25
|
+
for (const candidate of candidates) {
|
|
26
|
+
const full = join(dir, candidate);
|
|
27
|
+
if (existsSync(full)) {
|
|
28
|
+
return { file: full, ext: extname(full).toLowerCase() };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Not found on PATH — hand it back unchanged and let spawn surface the error.
|
|
34
|
+
return { file: command, ext: extname(command).toLowerCase() };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Whether a command resolves to a batch/PowerShell shim on Windows (e.g. the
|
|
38
|
+
// `claude.cmd` npm installs). Such shims run through cmd.exe, which truncates
|
|
39
|
+
// arguments at the first newline — so callers must avoid passing multi-line
|
|
40
|
+
// arguments to them. Always false off Windows.
|
|
41
|
+
export function isWindowsShim(command) {
|
|
42
|
+
if (!isWindows) return false;
|
|
43
|
+
const { ext } = resolveWindowsCommand(command);
|
|
44
|
+
return ext === ".cmd" || ext === ".bat" || ext === ".ps1";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Quoting for the cmd.exe shell path. With `shell: true` Node concatenates argv
|
|
48
|
+
// verbatim instead of quoting it, so we wrap anything containing whitespace or
|
|
49
|
+
// shell metacharacters ourselves.
|
|
50
|
+
function quoteWinArg(arg) {
|
|
51
|
+
if (arg === "") return '""';
|
|
52
|
+
if (!/[\s"&|<>^()]/.test(arg)) return arg;
|
|
53
|
+
const escaped = arg
|
|
54
|
+
.replace(/(\\*)"/g, '$1$1\\"') // double backslashes before a quote, then escape the quote
|
|
55
|
+
.replace(/(\\*)$/, "$1$1"); // double trailing backslashes so they don't escape the closing quote
|
|
56
|
+
return `"${escaped}"`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Cross-platform spawnSync. Non-Windows spawns the command directly. On Windows
|
|
60
|
+
// native executables are also spawned directly (preserving multi-line args),
|
|
61
|
+
// while batch/PowerShell shims are routed through the shell so PATHEXT resolves
|
|
62
|
+
// them.
|
|
63
|
+
export function spawnCross(command, args = [], options = {}) {
|
|
64
|
+
if (!isWindows) {
|
|
65
|
+
return spawnSync(command, args, options);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const { file, ext } = resolveWindowsCommand(command);
|
|
69
|
+
const needsShell = ext === ".cmd" || ext === ".bat" || ext === ".ps1";
|
|
70
|
+
// Interactive spawns (the editor) inherit stdio and need a real, visible
|
|
71
|
+
// console; only capturing spawns get the window-suppression flags below.
|
|
72
|
+
// detached and windowsHide map to mutually exclusive console-creation flags
|
|
73
|
+
// (DETACHED_PROCESS vs CREATE_NO_WINDOW), so each path picks exactly one.
|
|
74
|
+
const capturing = options.stdio !== "inherit";
|
|
75
|
+
|
|
76
|
+
if (!needsShell) {
|
|
77
|
+
// Native exe. A caller can pass `detached: true` for a CLI that renders to
|
|
78
|
+
// the console device instead of stdout (e.g. agy): detached gives it no
|
|
79
|
+
// console so it falls back to stdout — at the cost of a brief console
|
|
80
|
+
// window. Everything else just gets a hidden console (no window).
|
|
81
|
+
const wantsDetached = capturing && options.detached === true;
|
|
82
|
+
const extra = capturing && !wantsDetached ? { windowsHide: true } : {};
|
|
83
|
+
return spawnSync(file, args, { ...extra, ...options });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Batch/PowerShell shim — route through cmd.exe so PATHEXT resolves it.
|
|
87
|
+
// windowsHide (CREATE_NO_WINDOW) gives cmd.exe a hidden console: no popup,
|
|
88
|
+
// and shims like claude write to stdout normally so capture still works.
|
|
89
|
+
// detached would pop a cmd.exe window and shims never need it, so drop it.
|
|
90
|
+
const { detached, ...shimOptions } = options;
|
|
91
|
+
const extra = capturing ? { windowsHide: true } : {};
|
|
92
|
+
return spawnSync(file, args.map(quoteWinArg), {
|
|
93
|
+
...extra,
|
|
94
|
+
...shimOptions,
|
|
95
|
+
shell: true,
|
|
96
|
+
});
|
|
97
|
+
}
|
package/bin/wizard.js
CHANGED
|
@@ -75,7 +75,11 @@ async function pickProvider(rl) {
|
|
|
75
75
|
|
|
76
76
|
if (index >= 0 && index < PROVIDER_PRESETS.length) {
|
|
77
77
|
const preset = PROVIDER_PRESETS[index];
|
|
78
|
-
return {
|
|
78
|
+
return {
|
|
79
|
+
command: preset.command,
|
|
80
|
+
args: preset.args,
|
|
81
|
+
...(preset.windows ? { windows: preset.windows } : {}),
|
|
82
|
+
};
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
if (index === PROVIDER_PRESETS.length) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thankarezos/git-ai-commit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Generate editable git commit messages using a configurable AI CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"commit",
|
|
33
33
|
"cli",
|
|
34
34
|
"claude",
|
|
35
|
-
"
|
|
35
|
+
"antigravity",
|
|
36
36
|
"conventional-commits"
|
|
37
37
|
],
|
|
38
38
|
"license": "MIT"
|