@thankarezos/git-ai-commit 0.1.3 → 0.1.5

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,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, Gemini CLI, Codex, `llm`, Ollama, or a custom command). The message opens in your editor — save to commit, quit without saving to abort.
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`, `gemini`, `codex`, `opencode`, `llm`, `ollama`). |
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
- - **Gemini CLI** — `gemini -p "{prompt}"`
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,9 +109,23 @@ 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
- The base prompt template, the `<commit_message>` tag, and the cleanup patterns live in `bin/prompts.json`. Edit that file to:
128
+ `git aic --init` copies the prompt template, the `<commit_message>` tag, and the cleanup patterns into your **config file** (run `git aic --config` to see its path and contents). Edit the config file to customize them — these values override the built-in defaults:
115
129
 
116
130
  - Change tone, rules, or allowed Conventional Commit types in `prompt`.
117
131
  - Add line patterns to `removeLinesMatching` to strip more AI boilerplate.
@@ -119,6 +133,8 @@ The base prompt template, the `<commit_message>` tag, and the cleanup patterns l
119
133
 
120
134
  Patterns are case-insensitive JS regex strings.
121
135
 
136
+ The built-in defaults live in `bin/prompts.json`; editing that only changes the fallback for fields your config doesn't set, and is overwritten on reinstall — so prefer the config file.
137
+
122
138
  ## Tests
123
139
 
124
140
  ```sh
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 gemini # one-off provider override (also -P)
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 = { command: preset.command, args: preset.args };
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 editor = process.env.EDITOR || config.editor || "nano";
295
- const editorResult = spawnSync(editor, [file], {
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: "Gemini CLI",
9
- command: "gemini",
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"];
@@ -1,30 +1,58 @@
1
- import { spawnSync } from "node:child_process";
1
+ import { spawnCross, isWindowsShim } from "./spawn.js";
2
2
 
3
- export function runProvider(provider, prompt, diff) {
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
- const input = usesDiffPlaceholder ? undefined : diff;
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 (!usesPromptPlaceholder) {
22
- finalArgs.push(prompt);
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
- const result = spawnSync(provider.command, finalArgs, {
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 { command: preset.command, args: preset.args };
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",
3
+ "version": "0.1.5",
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
- "gemini",
35
+ "antigravity",
36
36
  "conventional-commits"
37
37
  ],
38
38
  "license": "MIT"