commitism 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kyubiware
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,191 @@
1
+ # commitism
2
+
3
+ > A commit tool that actually handles hook failures.
4
+
5
+ > **⚠️ WORK IN PROGRESS** — This is an early-stage project. The core commit flow, hook error parsing, and recovery menu work, but AI message generation is still a placeholder (`generateMessage` always returns `"chore: initial commit"`). Expect breaking changes.
6
+
7
+ ## The Problem
8
+
9
+ When `git commit` fails due to pre-commit hooks (lint-staged, biome, eslint, tsc, vitest, jest), you get a wall of raw error output with no clear next step. Your commit message is lost. You fix the errors, try to remember or regenerate the message, and retry manually.
10
+
11
+ Every existing AI commit tool has the same gap — they generate a message, call `git commit`, and if hooks fail, they just die.
12
+
13
+ ## What commitism does differently
14
+
15
+ commitism wraps the entire commit lifecycle — stage, generate, attempt, recover, retry — in one CLI tool:
16
+
17
+ ```
18
+ stage files → generate message → attempt commit → hooks fail?
19
+ ├─ copy errors to clipboard
20
+ ├─ skip hooks & commit
21
+ └─ re-stage & retry
22
+ ```
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ npm install -g commitism
28
+ ```
29
+
30
+ Requires **Node.js 18+**.
31
+
32
+ ## Usage
33
+
34
+ ```bash
35
+ # Normal commit flow
36
+ commitism
37
+
38
+ # Auto-stage all tracked files
39
+ commitism -a
40
+
41
+ # Skip AI, provide your own message
42
+ commitism -m "feat: add dark mode"
43
+
44
+ # Retry last failed commit (uses cached message)
45
+ commitism --retry
46
+ commitism -r
47
+
48
+ # Configuration
49
+ commitism config get GROQ_API_KEY
50
+ commitism config set GROQ_API_KEY=gsk_...
51
+ commitism config set model openai/gpt-oss-20b
52
+ ```
53
+
54
+ ### First run
55
+
56
+ If no `GROQ_API_KEY` is set in `~/.commitism` or `$GROQ_API_KEY`, you'll be prompted to enter one. It's saved to `~/.commitism` for future runs.
57
+
58
+ ## Recovery menu
59
+
60
+ When a pre-commit hook blocks your commit, commitism parses the error output and presents an interactive menu:
61
+
62
+ ```
63
+ ╭─────────────────────────────────────────────────╮
64
+ │ ✘ Pre-commit hook failed │
65
+ │ │
66
+ │ • biome: src/cli.ts — unused variable │
67
+ │ • vitest: 1 test failed in test/cli.test.ts │
68
+ │ │
69
+ │ What do you want to do? │
70
+ │ │
71
+ │ Copy error report to clipboard │
72
+ │ Skip hooks and commit (--no-verify) │
73
+ │ Re-stage files and retry │
74
+ │ Edit commit message │
75
+ │ Cancel │
76
+ ╰─────────────────────────────────────────────────╯
77
+ ```
78
+
79
+ | Option | What it does |
80
+ |--------|-------------|
81
+ | **Copy error report** | Copies parsed, clean error output to clipboard — paste it into another terminal for an AI agent to fix |
82
+ | **Skip hooks** | Re-runs `git commit --no-verify` with the same message — for when hooks are wrong or you'll fix later |
83
+ | **Re-stage & retry** | Runs `git add -A` again (picks up fixes made in another terminal), then retries the commit |
84
+ | **Edit message** | Opens a prompt to modify the commit message, then retries |
85
+ | **Cancel** | Exits. Commit message is cached for `commitism --retry` |
86
+
87
+ ### Supported hook tools
88
+
89
+ commitism parses errors from:
90
+
91
+ - **lint-staged** — task failure detection
92
+ - **biome** — lint/format errors with file:line:col
93
+ - **TypeScript** (`tsc`) — type errors with TS error codes
94
+ - **vitest** / **jest** — test failure detection
95
+ - **ESLint** — lint error/warning detection
96
+
97
+ Unrecognized error output is shown as raw fallback.
98
+
99
+ ## Configuration
100
+
101
+ Stored in `~/.commitism` (INI format):
102
+
103
+ ```ini
104
+ GROQ_API_KEY=gsk_...
105
+ model=openai/gpt-oss-20b
106
+ locale=en
107
+ max-length=100
108
+ type=conventional
109
+ timeout=10000
110
+ ```
111
+
112
+ | Key | Default | Description |
113
+ |-----|---------|-------------|
114
+ | `GROQ_API_KEY` | — | Groq API key for AI message generation |
115
+ | `model` | `openai/gpt-oss-20b` | AI model for commit message generation |
116
+ | `locale` | `en` | Locale for generated messages |
117
+ | `max-length` | `100` | Max commit message length |
118
+ | `type` | — | Commit type prefix (e.g. `conventional`) |
119
+ | `timeout` | `10000` | AI request timeout (ms) |
120
+
121
+ You can also set `GROQ_API_KEY` via environment variable.
122
+
123
+ ## CLI reference
124
+
125
+ ```
126
+ commitism --help
127
+
128
+ commitism
129
+
130
+ A commit tool that actually handles hook failures
131
+
132
+ Options:
133
+ --retry, -r Retry the last failed commit (default: false)
134
+ --all, -a Auto-stage all tracked files (default: false)
135
+ --message, -m Provide a commit message directly (skip AI generation)
136
+ --help, -h Show help
137
+ --version, -v Show version
138
+
139
+ Commands:
140
+ config Get/set configuration values
141
+ ```
142
+
143
+ ## Retry persistence
144
+
145
+ Failed commit messages are cached to `~/.cache/commitism/<repo-hash>.json`. Running `commitism --retry` reuses the last message without regenerating — useful after fixing errors flagged by the recovery menu.
146
+
147
+ ## How it works
148
+
149
+ ```
150
+ commitism/
151
+ ├── src/
152
+ │ ├── cli.ts # Entry point, argument parsing (cleye)
153
+ │ ├── commands/
154
+ │ │ ├── commit.ts # Main commit flow orchestrator
155
+ │ │ └── config.ts # Config get/set subcommand
156
+ │ ├── services/
157
+ │ │ ├── git.ts # Git operations (stage, commit, diff, HEAD)
158
+ │ │ ├── hooks.ts # Hook error parser (lint-staged, biome, tsc, etc.)
159
+ │ │ ├── config.ts # INI config read/write at ~/.commitism
160
+ │ │ └── clipboard.ts # Cross-platform clipboard (xclip/wl-copy/pbcopy)
161
+ │ ├── ui/
162
+ │ │ └── menu.ts # Interactive recovery TUI (@clack/prompts)
163
+ │ └── utils/
164
+ │ └── cache.ts # Commit message persistence at ~/.cache/commitism/
165
+ ```
166
+
167
+ ## Key differentiators
168
+
169
+ 1. **Hook error parsing** — No other commit tool parses lint-staged/biome/eslint output into a clean summary
170
+ 2. **Interactive recovery menu** — Copy/skip/retry as an in-flow choice, not a manual post-mortem
171
+ 3. **Message caching on failure** — `--retry` restores the last message without regenerating
172
+ 4. **Re-stage & retry loop** — Fix errors in another terminal, come back, hit "re-stage & retry"
173
+ 5. **Clipboard integration** — Copy error report and hand it to an AI coding agent for fixes
174
+
175
+ ## Requirements
176
+
177
+ - **Node.js 18+**
178
+ - **Git** (any modern version)
179
+ - **Linux** (primary target; macOS works via `pbcopy`; WSL untested)
180
+ - **xclip**, **wl-copy**, **xsel**, or **pbcopy** for clipboard support
181
+
182
+ ## Non-goals
183
+
184
+ - Not a hook manager — use husky, lefthook
185
+ - Not a linter/formatter — use biome, eslint, prettier
186
+ - Not a git TUI — use lazygit, gitui
187
+ - Not a commitizen replacement — just generates conventional commit messages via AI
188
+
189
+ ## License
190
+
191
+ MIT © kyubiware
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.mjs ADDED
@@ -0,0 +1,606 @@
1
+ #!/usr/bin/env node
2
+ import { cli, command } from "cleye";
3
+ import * as p from "@clack/prompts";
4
+ import { intro, isCancel, outro, spinner } from "@clack/prompts";
5
+ import { bold, cyan, dim, green, red, yellow } from "kolorist";
6
+ import { execa } from "execa";
7
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
8
+ import { join } from "node:path";
9
+ import os from "node:os";
10
+ import { createHash } from "node:crypto";
11
+ import ini from "ini";
12
+ //#region \0rolldown/runtime.js
13
+ var __defProp = Object.defineProperty;
14
+ var __exportAll = (all, no_symbols) => {
15
+ let target = {};
16
+ for (var name in all) __defProp(target, name, {
17
+ get: all[name],
18
+ enumerable: true
19
+ });
20
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
21
+ return target;
22
+ };
23
+ //#endregion
24
+ //#region package.json
25
+ var package_default = {
26
+ name: "commitism",
27
+ version: "0.1.0",
28
+ description: "A commit tool that actually handles hook failures",
29
+ type: "module",
30
+ bin: { "commitism": "./dist/cli.mjs" },
31
+ files: ["dist"],
32
+ scripts: {
33
+ "build": "tsdown src/cli.ts --format esm --dts --clean",
34
+ "dev": "tsx src/cli.ts",
35
+ "lint": "biome check .",
36
+ "lint:fix": "biome check --fix .",
37
+ "typecheck": "tsc --noEmit",
38
+ "test": "vitest run",
39
+ "prepublishOnly": "npm run build"
40
+ },
41
+ keywords: [
42
+ "git",
43
+ "commit",
44
+ "hooks",
45
+ "pre-commit",
46
+ "lint-staged",
47
+ "ai",
48
+ "groq",
49
+ "conventional-commits",
50
+ "cli"
51
+ ],
52
+ author: "kyubiware",
53
+ license: "MIT",
54
+ repository: {
55
+ "type": "git",
56
+ "url": "https://github.com/kyubiware/commitism.git"
57
+ },
58
+ dependencies: {
59
+ "@clack/prompts": "^0.9.1",
60
+ "cleye": "^1.3.4",
61
+ "execa": "^9.6.0",
62
+ "groq-sdk": "^0.32.0",
63
+ "ini": "^5.0.0",
64
+ "kolorist": "^1.8.0"
65
+ },
66
+ devDependencies: {
67
+ "@biomejs/biome": "^2.0.0",
68
+ "tsdown": "^0.22.0",
69
+ "tsx": "^4.22.2",
70
+ "typescript": "^5.9.2",
71
+ "vitest": "^3.2.1"
72
+ }
73
+ };
74
+ //#endregion
75
+ //#region src/services/git.ts
76
+ var git_exports = /* @__PURE__ */ __exportAll({
77
+ KnownError: () => KnownError,
78
+ assertGitRepo: () => assertGitRepo,
79
+ attemptCommit: () => attemptCommit,
80
+ attemptCommitNoVerify: () => attemptCommitNoVerify,
81
+ getHead: () => getHead,
82
+ getRepoRoot: () => getRepoRoot,
83
+ getStagedDiff: () => getStagedDiff,
84
+ getStatusShort: () => getStatusShort,
85
+ stageAll: () => stageAll
86
+ });
87
+ var KnownError = class extends Error {};
88
+ async function assertGitRepo() {
89
+ const { failed } = await execa("git", ["rev-parse", "--show-toplevel"], { reject: false });
90
+ if (failed) throw new KnownError("The current directory must be a Git repository!");
91
+ }
92
+ async function getRepoRoot() {
93
+ const { stdout } = await execa("git", ["rev-parse", "--show-toplevel"]);
94
+ return stdout.trim();
95
+ }
96
+ async function getStagedDiff(exclude) {
97
+ const excludeArgs = (exclude ?? []).map((e) => `:(exclude)${e}`);
98
+ const defaultExcludes = [
99
+ "package-lock.json",
100
+ "node_modules/**",
101
+ "dist/**",
102
+ "build/**",
103
+ ".next/**",
104
+ "coverage/**",
105
+ "*.log",
106
+ "*.min.js",
107
+ "*.min.css",
108
+ "*.lock",
109
+ ".DS_Store"
110
+ ].map((e) => `:(exclude)${e}`);
111
+ const { stdout: files } = await execa("git", [
112
+ "diff",
113
+ "--cached",
114
+ "--name-only",
115
+ ...defaultExcludes,
116
+ ...excludeArgs
117
+ ]);
118
+ if (!files) return null;
119
+ const { stdout: diff } = await execa("git", [
120
+ "diff",
121
+ "--cached",
122
+ "--diff-algorithm=minimal",
123
+ ...defaultExcludes,
124
+ ...excludeArgs
125
+ ]);
126
+ return {
127
+ files: files.split("\n").filter(Boolean),
128
+ diff
129
+ };
130
+ }
131
+ async function stageAll() {
132
+ await execa("git", ["add", "-A"]);
133
+ }
134
+ async function getHead() {
135
+ const { stdout } = await execa("git", ["rev-parse", "HEAD"]);
136
+ return stdout.trim();
137
+ }
138
+ async function getStatusShort() {
139
+ const { stdout } = await execa("git", ["status", "--short"]);
140
+ return stdout.trim();
141
+ }
142
+ async function attemptCommit(message, extraArgs = []) {
143
+ try {
144
+ await execa("git", [
145
+ "commit",
146
+ "-m",
147
+ message,
148
+ ...extraArgs
149
+ ]);
150
+ return { ok: true };
151
+ } catch (error) {
152
+ const e = error;
153
+ return {
154
+ ok: false,
155
+ error: e.message,
156
+ stderr: e.stderr ?? ""
157
+ };
158
+ }
159
+ }
160
+ async function attemptCommitNoVerify(message) {
161
+ return attemptCommit(message, ["--no-verify"]);
162
+ }
163
+ //#endregion
164
+ //#region src/services/hooks.ts
165
+ /**
166
+ * Parse git hook error output into structured, human-readable errors.
167
+ * Handles output from lint-staged, biome, eslint, tsc, vitest, jest.
168
+ */
169
+ function parseHookErrors(stderr) {
170
+ if (!stderr) return [];
171
+ const errors = [];
172
+ stderr.split("\n");
173
+ if (stderr.includes("lint-staged") || stderr.includes("[FAILED]")) errors.push(...parseLintStagedErrors(stderr));
174
+ if (stderr.includes("biome") || stderr.includes("Biome")) errors.push(...parseBiomeErrors(stderr));
175
+ if (stderr.includes("error TS") || stderr.includes("tsc")) errors.push(...parseTscErrors(stderr));
176
+ if (stderr.includes("vitest") || stderr.includes("jest") || stderr.includes("FAIL") || stderr.includes("test failed")) errors.push(...parseTestErrors(stderr));
177
+ if (stderr.includes("eslint") || stderr.includes("ESLint")) errors.push(...parseEslintErrors(stderr));
178
+ if (errors.length === 0) errors.push({
179
+ tool: "git hooks",
180
+ message: stderr.trim(),
181
+ raw: stderr
182
+ });
183
+ return errors;
184
+ }
185
+ function parseLintStagedErrors(output) {
186
+ const errors = [];
187
+ const taskPattern = /\[FAILED\]\s+(.+?)\s+\[FAILED\]/g;
188
+ let match;
189
+ while ((match = taskPattern.exec(output)) !== null) {
190
+ const task = match[1].trim();
191
+ errors.push({
192
+ tool: "lint-staged",
193
+ message: `Task failed: ${task}`,
194
+ raw: match[0]
195
+ });
196
+ }
197
+ return errors;
198
+ }
199
+ function parseBiomeErrors(output) {
200
+ const errors = [];
201
+ const biomePattern = /^(.+?):(\d+):(\d+)\s+(.+)$/gm;
202
+ let match;
203
+ while ((match = biomePattern.exec(output)) !== null) errors.push({
204
+ tool: "biome",
205
+ message: `${match[1]}:${match[2]}:${match[3]} — ${match[4]}`,
206
+ raw: match[0]
207
+ });
208
+ if (errors.length === 0 && output.includes("biome")) errors.push({
209
+ tool: "biome",
210
+ message: "Biome check failed. See raw output for details.",
211
+ raw: output
212
+ });
213
+ return errors;
214
+ }
215
+ function parseTscErrors(output) {
216
+ const errors = [];
217
+ const tscPattern = /^(.+?)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/gm;
218
+ let match;
219
+ while ((match = tscPattern.exec(output)) !== null) errors.push({
220
+ tool: "tsc",
221
+ message: `${match[1]}:${match[2]}:${match[3]} — ${match[4]}: ${match[5]}`,
222
+ raw: match[0]
223
+ });
224
+ return errors;
225
+ }
226
+ function parseTestErrors(output) {
227
+ const errors = [];
228
+ const match = /FAIL\s+(.+\.(test|spec)\..+)/.exec(output);
229
+ if (match) errors.push({
230
+ tool: output.includes("vitest") ? "vitest" : "jest",
231
+ message: `Test file failed: ${match[1]}`,
232
+ raw: output
233
+ });
234
+ if (errors.length === 0 && (output.includes("vitest") || output.includes("jest"))) errors.push({
235
+ tool: output.includes("vitest") ? "vitest" : "jest",
236
+ message: "Tests failed. See raw output for details.",
237
+ raw: output
238
+ });
239
+ return errors;
240
+ }
241
+ function parseEslintErrors(output) {
242
+ const errors = [];
243
+ const eslintPattern = /^\s*\d+:(\d+)\s+(error|warning)\s+(.+?)\s+(.+?)$/gm;
244
+ let match;
245
+ while ((match = eslintPattern.exec(output)) !== null) errors.push({
246
+ tool: "eslint",
247
+ message: `${match[2]}: ${match[3]} (${match[4]})`,
248
+ raw: match[0]
249
+ });
250
+ return errors;
251
+ }
252
+ function formatErrorReport(errors) {
253
+ if (errors.length === 0) return "";
254
+ return errors.map((e) => `[${e.tool}]\n${e.message}`).join("\n\n");
255
+ }
256
+ //#endregion
257
+ //#region src/services/clipboard.ts
258
+ async function copyToClipboard(content) {
259
+ for (const [cmd, ...args] of [
260
+ ["wl-copy"],
261
+ [
262
+ "xclip",
263
+ "-selection",
264
+ "clipboard"
265
+ ],
266
+ [
267
+ "xsel",
268
+ "--clipboard",
269
+ "--input"
270
+ ],
271
+ ["pbcopy"]
272
+ ]) try {
273
+ const { stdout } = await execa("which", [cmd], { reject: false });
274
+ if (!stdout) continue;
275
+ await execa(cmd, args.length > 0 ? args : [], { input: content });
276
+ return true;
277
+ } catch {
278
+ continue;
279
+ }
280
+ return false;
281
+ }
282
+ //#endregion
283
+ //#region src/ui/menu.ts
284
+ async function showRecoveryMenu(errors, onRetry, onSkipHooks, onRestage, message) {
285
+ p.note(errors.map((e) => ` ${red("•")} [${e.tool}] ${e.message}`).join("\n"), red(bold("Pre-commit hook failed")));
286
+ const choice = await p.select({
287
+ message: "What do you want to do?",
288
+ options: [
289
+ {
290
+ label: "Copy error report to clipboard",
291
+ value: "clipboard",
292
+ hint: "Paste into another terminal for an AI agent"
293
+ },
294
+ {
295
+ label: "Skip hooks and commit (--no-verify)",
296
+ value: "skip",
297
+ hint: "Commit anyway, fix later"
298
+ },
299
+ {
300
+ label: "Re-stage files and retry",
301
+ value: "restage",
302
+ hint: "Pick up fixes from another terminal"
303
+ },
304
+ {
305
+ label: "Edit commit message",
306
+ value: "edit",
307
+ hint: "Modify the message before retrying"
308
+ },
309
+ {
310
+ label: "Cancel",
311
+ value: "cancel"
312
+ }
313
+ ]
314
+ });
315
+ if (p.isCancel(choice)) {
316
+ p.outro(yellow("Cancelled. Message cached for --retry."));
317
+ process.exit(1);
318
+ }
319
+ switch (choice) {
320
+ case "clipboard":
321
+ if (await copyToClipboard(formatErrorReport(errors))) p.outro(green("Error report copied to clipboard."));
322
+ else p.outro(red("No clipboard tool found. Install xclip, wl-copy, or xsel."));
323
+ p.log.info(dim("Fix the errors, then run: commitism --retry"));
324
+ process.exit(0);
325
+ break;
326
+ case "skip":
327
+ p.log.info(yellow("Committing with --no-verify..."));
328
+ if (await onSkipHooks(message)) p.outro(green("Committed (hooks skipped)."));
329
+ else p.outro(red("Commit failed even with --no-verify."));
330
+ break;
331
+ case "restage":
332
+ p.log.info(cyan("Re-staging and retrying..."));
333
+ if (await onRestage()) p.outro(green("Committed successfully."));
334
+ else await showRecoveryMenu(errors, onRetry, onSkipHooks, onRestage, message);
335
+ break;
336
+ case "edit": {
337
+ const edited = await p.text({
338
+ message: "Edit commit message:",
339
+ initialValue: message,
340
+ validate: (v) => v.trim() ? void 0 : "Message cannot be empty"
341
+ });
342
+ if (p.isCancel(edited)) {
343
+ p.outro(yellow("Cancelled. Message cached for --retry."));
344
+ process.exit(1);
345
+ }
346
+ if (await onRetry()) p.outro(green("Committed successfully."));
347
+ else p.outro(red("Commit failed again."));
348
+ break;
349
+ }
350
+ case "cancel":
351
+ p.outro(dim("Message cached for --retry."));
352
+ process.exit(1);
353
+ }
354
+ }
355
+ //#endregion
356
+ //#region src/utils/cache.ts
357
+ const CACHE_DIR = join(os.homedir(), ".cache", "commitism");
358
+ function repoHash(repoPath) {
359
+ return createHash("sha256").update(repoPath).digest("hex").slice(0, 12);
360
+ }
361
+ function cachePath(repoPath) {
362
+ return join(CACHE_DIR, `${repoHash(repoPath)}.json`);
363
+ }
364
+ async function saveCachedCommit(repoPath, message) {
365
+ await mkdir(CACHE_DIR, { recursive: true });
366
+ const data = {
367
+ message,
368
+ timestamp: Date.now(),
369
+ repoPath
370
+ };
371
+ await writeFile(cachePath(repoPath), JSON.stringify(data, null, 2), "utf8");
372
+ }
373
+ async function loadCachedCommit(repoPath) {
374
+ try {
375
+ const raw = await readFile(cachePath(repoPath), "utf8");
376
+ return JSON.parse(raw);
377
+ } catch {
378
+ return null;
379
+ }
380
+ }
381
+ //#endregion
382
+ //#region src/services/config.ts
383
+ const CONFIG_PATH = join(os.homedir(), ".commitism");
384
+ const defaults = {
385
+ model: "openai/gpt-oss-20b",
386
+ locale: "en",
387
+ "max-length": "100",
388
+ type: "",
389
+ timeout: "10000"
390
+ };
391
+ async function readConfig() {
392
+ try {
393
+ const raw = await readFile(CONFIG_PATH, "utf8");
394
+ const parsed = ini.parse(raw);
395
+ return {
396
+ ...defaults,
397
+ ...parsed
398
+ };
399
+ } catch {
400
+ return { ...defaults };
401
+ }
402
+ }
403
+ async function writeConfig(updates) {
404
+ const existing = await readConfig();
405
+ Object.assign(existing, updates);
406
+ await writeFile(CONFIG_PATH, ini.stringify(existing), "utf8");
407
+ }
408
+ async function getConfigValue(key) {
409
+ return (await readConfig())[key];
410
+ }
411
+ async function setConfigValue(key, value) {
412
+ await writeConfig({ [key]: value });
413
+ }
414
+ async function getApiKey() {
415
+ const envKey = process.env.GROQ_API_KEY;
416
+ if (envKey) return envKey;
417
+ const config = await readConfig();
418
+ if (config.GROQ_API_KEY) return config.GROQ_API_KEY;
419
+ throw new Error("Please set your Groq API key via `commitism config set GROQ_API_KEY=<your token>`");
420
+ }
421
+ //#endregion
422
+ //#region src/commands/commit.ts
423
+ async function commitCommand(flags) {
424
+ await assertGitRepo();
425
+ if (flags.retry) {
426
+ const { getRepoRoot } = await Promise.resolve().then(() => git_exports);
427
+ const cached = await loadCachedCommit(await getRepoRoot());
428
+ if (!cached) {
429
+ outro(red("No cached commit message found. Run commitism without --retry first."));
430
+ process.exit(1);
431
+ }
432
+ intro("commitism — retry");
433
+ const s = spinner();
434
+ s.start("Retrying commit...");
435
+ const result = await attemptCommit(cached.message);
436
+ s.stop("Attempted commit");
437
+ if (result.ok) outro(green("Committed successfully."));
438
+ else await showRecoveryMenu(parseHookErrors(result.stderr ?? ""), async () => (await attemptCommit(cached.message)).ok, async (msg) => (await attemptCommitNoVerify(msg)).ok, async () => {
439
+ await stageAll();
440
+ return (await attemptCommit(cached.message)).ok;
441
+ }, cached.message);
442
+ return;
443
+ }
444
+ intro("commitism");
445
+ if (!await getStatusShort()) {
446
+ outro(dim("Nothing to commit."));
447
+ return;
448
+ }
449
+ const s = spinner();
450
+ s.start("Staging all changes...");
451
+ await stageAll();
452
+ s.stop("Changes staged");
453
+ const diff = await getStagedDiff();
454
+ if (!diff) {
455
+ outro(red("No staged changes found."));
456
+ process.exit(1);
457
+ }
458
+ console.log(diff.files.map((f) => ` ${f}`).join("\n"));
459
+ let message;
460
+ if (flags.message) message = flags.message;
461
+ else {
462
+ try {
463
+ await getApiKey();
464
+ } catch {
465
+ const { text: promptText } = await import("@clack/prompts");
466
+ const key = await promptText({
467
+ message: "Enter your Groq API key:",
468
+ placeholder: "gsk_...",
469
+ validate: (v) => v.trim() ? void 0 : "API key is required"
470
+ });
471
+ if (isCancel(key)) {
472
+ outro(dim("Cancelled."));
473
+ return;
474
+ }
475
+ await setConfigValue("GROQ_API_KEY", String(key).trim());
476
+ }
477
+ s.start("Generating commit message...");
478
+ try {
479
+ message = await generateMessage(diff.diff, diff.files);
480
+ } catch (err) {
481
+ s.stop(red("Failed to generate message."));
482
+ outro(red(err instanceof Error ? err.message : String(err)));
483
+ return;
484
+ }
485
+ s.stop("Message generated");
486
+ }
487
+ const { select, text } = await import("@clack/prompts");
488
+ const review = await select({
489
+ message: `Review commit message:\n\n ${bold(message)}\n`,
490
+ options: [
491
+ {
492
+ label: "Use as-is",
493
+ value: "use"
494
+ },
495
+ {
496
+ label: "Edit",
497
+ value: "edit"
498
+ },
499
+ {
500
+ label: "Cancel",
501
+ value: "cancel"
502
+ }
503
+ ]
504
+ });
505
+ if (isCancel(review) || review === "cancel") {
506
+ outro(dim("Cancelled."));
507
+ return;
508
+ }
509
+ if (review === "edit") {
510
+ const edited = await text({
511
+ message: "Edit commit message:",
512
+ initialValue: message,
513
+ validate: (v) => v.trim() ? void 0 : "Message cannot be empty"
514
+ });
515
+ if (isCancel(edited)) {
516
+ outro(dim("Cancelled."));
517
+ return;
518
+ }
519
+ message = String(edited).trim();
520
+ }
521
+ const { getRepoRoot } = await Promise.resolve().then(() => git_exports);
522
+ await saveCachedCommit(await getRepoRoot(), message);
523
+ s.start("Committing...");
524
+ const headBefore = await getHead();
525
+ const result = await attemptCommit(message);
526
+ const headAfter = await getHead();
527
+ if (result.ok || headBefore !== headAfter) {
528
+ s.stop("Committed successfully.");
529
+ outro(green("Done."));
530
+ return;
531
+ }
532
+ s.stop("Commit failed.");
533
+ await showRecoveryMenu(parseHookErrors(result.stderr ?? ""), async () => {
534
+ return (await attemptCommit(message)).ok;
535
+ }, async (msg) => {
536
+ return (await attemptCommitNoVerify(msg)).ok;
537
+ }, async () => {
538
+ await stageAll();
539
+ return (await attemptCommit(message)).ok;
540
+ }, message);
541
+ }
542
+ async function generateMessage(_diff, _files) {
543
+ await readConfig();
544
+ await getApiKey();
545
+ return "chore: initial commit";
546
+ }
547
+ //#endregion
548
+ //#region src/commands/config.ts
549
+ const configCommand = command({
550
+ name: "config",
551
+ parameters: ["<mode>", "<key=value...>"]
552
+ }, async (argv) => {
553
+ const { mode, keyValue } = argv._;
554
+ if (mode === "get") {
555
+ for (const kv of keyValue) {
556
+ const key = kv.split("=")[0];
557
+ const value = await getConfigValue(key);
558
+ console.log(`${key}=${value ?? ""}`);
559
+ }
560
+ return;
561
+ }
562
+ if (mode === "set") {
563
+ for (const kv of keyValue) {
564
+ const [key, ...rest] = kv.split("=");
565
+ await setConfigValue(key, rest.join("="));
566
+ }
567
+ console.log("Config updated.");
568
+ return;
569
+ }
570
+ console.error(`Unknown config mode: ${mode}. Use "get" or "set".`);
571
+ process.exit(1);
572
+ });
573
+ //#endregion
574
+ //#region src/cli.ts
575
+ const { version } = package_default;
576
+ cli({
577
+ name: "commitism",
578
+ version,
579
+ description: "A commit tool that actually handles hook failures",
580
+ flags: {
581
+ retry: {
582
+ type: Boolean,
583
+ description: "Retry the last failed commit",
584
+ alias: "r",
585
+ default: false
586
+ },
587
+ all: {
588
+ type: Boolean,
589
+ description: "Auto-stage all tracked files",
590
+ alias: "a",
591
+ default: false
592
+ },
593
+ message: {
594
+ type: String,
595
+ description: "Provide a commit message directly (skip AI generation)",
596
+ alias: "m"
597
+ }
598
+ },
599
+ commands: [configCommand]
600
+ }, (argv) => {
601
+ commitCommand(argv.flags);
602
+ });
603
+ //#endregion
604
+ export {};
605
+
606
+ //# sourceMappingURL=cli.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.mjs","names":["pkg"],"sources":["../package.json","../src/services/git.ts","../src/services/hooks.ts","../src/services/clipboard.ts","../src/ui/menu.ts","../src/utils/cache.ts","../src/services/config.ts","../src/commands/commit.ts","../src/commands/config.ts","../src/cli.ts"],"sourcesContent":["","import type { ExecaError } from \"execa\";\nimport { execa } from \"execa\";\n\nexport class KnownError extends Error {}\n\nexport async function assertGitRepo() {\n\tconst { failed } = await execa(\"git\", [\"rev-parse\", \"--show-toplevel\"], {\n\t\treject: false,\n\t});\n\tif (failed) {\n\t\tthrow new KnownError(\"The current directory must be a Git repository!\");\n\t}\n}\n\nexport async function getRepoRoot() {\n\tconst { stdout } = await execa(\"git\", [\"rev-parse\", \"--show-toplevel\"]);\n\treturn stdout.trim();\n}\n\nexport async function getStagedDiff(exclude?: string[]) {\n\tconst excludeArgs = (exclude ?? []).map((e) => `:(exclude)${e}`);\n\tconst defaultExcludes = [\n\t\t\"package-lock.json\",\n\t\t\"node_modules/**\",\n\t\t\"dist/**\",\n\t\t\"build/**\",\n\t\t\".next/**\",\n\t\t\"coverage/**\",\n\t\t\"*.log\",\n\t\t\"*.min.js\",\n\t\t\"*.min.css\",\n\t\t\"*.lock\",\n\t\t\".DS_Store\",\n\t].map((e) => `:(exclude)${e}`);\n\n\tconst { stdout: files } = await execa(\"git\", [\n\t\t\"diff\",\n\t\t\"--cached\",\n\t\t\"--name-only\",\n\t\t...defaultExcludes,\n\t\t...excludeArgs,\n\t]);\n\tif (!files) return null;\n\n\tconst { stdout: diff } = await execa(\"git\", [\n\t\t\"diff\",\n\t\t\"--cached\",\n\t\t\"--diff-algorithm=minimal\",\n\t\t...defaultExcludes,\n\t\t...excludeArgs,\n\t]);\n\n\treturn { files: files.split(\"\\n\").filter(Boolean), diff };\n}\n\nexport async function stageAll() {\n\tawait execa(\"git\", [\"add\", \"-A\"]);\n}\n\nexport async function getHead() {\n\tconst { stdout } = await execa(\"git\", [\"rev-parse\", \"HEAD\"]);\n\treturn stdout.trim();\n}\n\nexport async function getStatusShort() {\n\tconst { stdout } = await execa(\"git\", [\"status\", \"--short\"]);\n\treturn stdout.trim();\n}\n\nexport interface CommitResult {\n\tok: boolean;\n\terror?: string;\n\tstderr?: string;\n}\n\nexport async function attemptCommit(message: string, extraArgs: string[] = []): Promise<CommitResult> {\n\ttry {\n\t\tawait execa(\"git\", [\"commit\", \"-m\", message, ...extraArgs]);\n\t\treturn { ok: true };\n\t} catch (error) {\n\t\tconst e = error as ExecaError;\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: e.message,\n\t\t\tstderr: e.stderr ?? \"\",\n\t\t};\n\t}\n}\n\nexport async function attemptCommitNoVerify(message: string): Promise<CommitResult> {\n\treturn attemptCommit(message, [\"--no-verify\"]);\n}\n","import type { ExecaError } from \"execa\";\n\nexport interface HookError {\n\ttool: string;\n\tmessage: string;\n\traw: string;\n}\n\n/**\n * Parse git hook error output into structured, human-readable errors.\n * Handles output from lint-staged, biome, eslint, tsc, vitest, jest.\n */\nexport function parseHookErrors(stderr: string): HookError[] {\n\tif (!stderr) return [];\n\n\tconst errors: HookError[] = [];\n\tconst lines = stderr.split(\"\\n\");\n\n\t// Detect lint-staged task failures\n\tif (stderr.includes(\"lint-staged\") || stderr.includes(\"[FAILED]\")) {\n\t\terrors.push(...parseLintStagedErrors(stderr));\n\t}\n\n\t// Detect biome errors\n\tif (stderr.includes(\"biome\") || stderr.includes(\"Biome\")) {\n\t\terrors.push(...parseBiomeErrors(stderr));\n\t}\n\n\t// Detect TypeScript errors\n\tif (stderr.includes(\"error TS\") || stderr.includes(\"tsc\")) {\n\t\terrors.push(...parseTscErrors(stderr));\n\t}\n\n\t// Detect vitest/jest test failures\n\tif (\n\t\tstderr.includes(\"vitest\") ||\n\t\tstderr.includes(\"jest\") ||\n\t\tstderr.includes(\"FAIL\") ||\n\t\tstderr.includes(\"test failed\")\n\t) {\n\t\terrors.push(...parseTestErrors(stderr));\n\t}\n\n\t// Detect ESLint errors\n\tif (stderr.includes(\"eslint\") || stderr.includes(\"ESLint\")) {\n\t\terrors.push(...parseEslintErrors(stderr));\n\t}\n\n\t// Fallback: if nothing parsed, return the raw output\n\tif (errors.length === 0) {\n\t\terrors.push({\n\t\t\ttool: \"git hooks\",\n\t\t\tmessage: stderr.trim(),\n\t\t\traw: stderr,\n\t\t});\n\t}\n\n\treturn errors;\n}\n\nfunction parseLintStagedErrors(output: string): HookError[] {\n\tconst errors: HookError[] = [];\n\tconst taskPattern = /\\[FAILED\\]\\s+(.+?)\\s+\\[FAILED\\]/g;\n\tlet match: RegExpExecArray | null;\n\n\twhile ((match = taskPattern.exec(output)) !== null) {\n\t\tconst task = match[1].trim();\n\t\terrors.push({\n\t\t\ttool: \"lint-staged\",\n\t\t\tmessage: `Task failed: ${task}`,\n\t\t\traw: match[0],\n\t\t});\n\t}\n\n\treturn errors;\n}\n\nfunction parseBiomeErrors(output: string): HookError[] {\n\tconst errors: HookError[] = [];\n\tconst biomePattern = /^(.+?):(\\d+):(\\d+)\\s+(.+)$/gm;\n\tlet match: RegExpExecArray | null;\n\n\twhile ((match = biomePattern.exec(output)) !== null) {\n\t\terrors.push({\n\t\t\ttool: \"biome\",\n\t\t\tmessage: `${match[1]}:${match[2]}:${match[3]} — ${match[4]}`,\n\t\t\traw: match[0],\n\t\t});\n\t}\n\n\tif (errors.length === 0 && output.includes(\"biome\")) {\n\t\terrors.push({\n\t\t\ttool: \"biome\",\n\t\t\tmessage: \"Biome check failed. See raw output for details.\",\n\t\t\traw: output,\n\t\t});\n\t}\n\n\treturn errors;\n}\n\nfunction parseTscErrors(output: string): HookError[] {\n\tconst errors: HookError[] = [];\n\tconst tscPattern = /^(.+?)\\((\\d+),(\\d+)\\):\\s+error\\s+(TS\\d+):\\s+(.+)$/gm;\n\tlet match: RegExpExecArray | null;\n\n\twhile ((match = tscPattern.exec(output)) !== null) {\n\t\terrors.push({\n\t\t\ttool: \"tsc\",\n\t\t\tmessage: `${match[1]}:${match[2]}:${match[3]} — ${match[4]}: ${match[5]}`,\n\t\t\traw: match[0],\n\t\t});\n\t}\n\n\treturn errors;\n}\n\nfunction parseTestErrors(output: string): HookError[] {\n\tconst errors: HookError[] = [];\n\tconst failPattern = /FAIL\\s+(.+\\.(test|spec)\\..+)/;\n\tconst match = failPattern.exec(output);\n\n\tif (match) {\n\t\terrors.push({\n\t\t\ttool: output.includes(\"vitest\") ? \"vitest\" : \"jest\",\n\t\t\tmessage: `Test file failed: ${match[1]}`,\n\t\t\traw: output,\n\t\t});\n\t}\n\n\tif (errors.length === 0 && (output.includes(\"vitest\") || output.includes(\"jest\"))) {\n\t\terrors.push({\n\t\t\ttool: output.includes(\"vitest\") ? \"vitest\" : \"jest\",\n\t\t\tmessage: \"Tests failed. See raw output for details.\",\n\t\t\traw: output,\n\t\t});\n\t}\n\n\treturn errors;\n}\n\nfunction parseEslintErrors(output: string): HookError[] {\n\tconst errors: HookError[] = [];\n\tconst eslintPattern = /^\\s*\\d+:(\\d+)\\s+(error|warning)\\s+(.+?)\\s+(.+?)$/gm;\n\tlet match: RegExpExecArray | null;\n\n\twhile ((match = eslintPattern.exec(output)) !== null) {\n\t\terrors.push({\n\t\t\ttool: \"eslint\",\n\t\t\tmessage: `${match[2]}: ${match[3]} (${match[4]})`,\n\t\t\traw: match[0],\n\t\t});\n\t}\n\n\treturn errors;\n}\n\nexport function formatErrorReport(errors: HookError[]): string {\n\tif (errors.length === 0) return \"\";\n\n\tconst sections = errors.map((e) => `[${e.tool}]\\n${e.message}`);\n\treturn sections.join(\"\\n\\n\");\n}\n","import { execa } from \"execa\";\n\nexport async function copyToClipboard(content: string): Promise<boolean> {\n\tconst commands = [\n\t\t[\"wl-copy\"],\n\t\t[\"xclip\", \"-selection\", \"clipboard\"],\n\t\t[\"xsel\", \"--clipboard\", \"--input\"],\n\t\t[\"pbcopy\"],\n\t];\n\n\tfor (const [cmd, ...args] of commands) {\n\t\ttry {\n\t\t\tconst { stdout } = await execa(\"which\", [cmd], { reject: false });\n\t\t\tif (!stdout) continue;\n\t\t\tawait execa(cmd, args.length > 0 ? args : [], { input: content });\n\t\t\treturn true;\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t}\n\treturn false;\n}\n","import * as p from \"@clack/prompts\";\nimport { red, green, yellow, dim, bold, cyan } from \"kolorist\";\nimport type { HookError } from \"../services/hooks.js\";\nimport { formatErrorReport } from \"../services/hooks.js\";\nimport { copyToClipboard } from \"../services/clipboard.js\";\n\nexport async function showRecoveryMenu(\n\terrors: HookError[],\n\tonRetry: () => Promise<boolean>,\n\tonSkipHooks: (message: string) => Promise<boolean>,\n\tonRestage: () => Promise<boolean>,\n\tmessage: string,\n): Promise<void> {\n\tp.note(\n\t\terrors.map((e) => ` ${red(\"•\")} [${e.tool}] ${e.message}`).join(\"\\n\"),\n\t\tred(bold(\"Pre-commit hook failed\")),\n\t);\n\n\tconst choice = await p.select({\n\t\tmessage: \"What do you want to do?\",\n\t\toptions: [\n\t\t\t{ label: \"Copy error report to clipboard\", value: \"clipboard\", hint: \"Paste into another terminal for an AI agent\" },\n\t\t\t{ label: \"Skip hooks and commit (--no-verify)\", value: \"skip\", hint: \"Commit anyway, fix later\" },\n\t\t\t{ label: \"Re-stage files and retry\", value: \"restage\", hint: \"Pick up fixes from another terminal\" },\n\t\t\t{ label: \"Edit commit message\", value: \"edit\", hint: \"Modify the message before retrying\" },\n\t\t\t{ label: \"Cancel\", value: \"cancel\" },\n\t\t],\n\t});\n\n\tif (p.isCancel(choice)) {\n\t\tp.outro(yellow(\"Cancelled. Message cached for --retry.\"));\n\t\tprocess.exit(1);\n\t}\n\n\tswitch (choice) {\n\t\tcase \"clipboard\": {\n\t\t\tconst report = formatErrorReport(errors);\n\t\t\tconst ok = await copyToClipboard(report);\n\t\t\tif (ok) {\n\t\t\t\tp.outro(green(\"Error report copied to clipboard.\"));\n\t\t\t} else {\n\t\t\t\tp.outro(red(\"No clipboard tool found. Install xclip, wl-copy, or xsel.\"));\n\t\t\t}\n\t\t\tp.log.info(dim(\"Fix the errors, then run: commitism --retry\"));\n\t\t\tprocess.exit(0);\n\t\t\tbreak;\n\t\t}\n\t\tcase \"skip\": {\n\t\t\tp.log.info(yellow(\"Committing with --no-verify...\"));\n\t\t\tconst ok = await onSkipHooks(message);\n\t\t\tif (ok) {\n\t\t\t\tp.outro(green(\"Committed (hooks skipped).\"));\n\t\t\t} else {\n\t\t\t\tp.outro(red(\"Commit failed even with --no-verify.\"));\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase \"restage\": {\n\t\t\tp.log.info(cyan(\"Re-staging and retrying...\"));\n\t\t\tconst ok = await onRestage();\n\t\t\tif (ok) {\n\t\t\t\tp.outro(green(\"Committed successfully.\"));\n\t\t\t} else {\n\t\t\t\t// Recursive — show menu again\n\t\t\t\tawait showRecoveryMenu(errors, onRetry, onSkipHooks, onRestage, message);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase \"edit\": {\n\t\t\tconst edited = await p.text({\n\t\t\t\tmessage: \"Edit commit message:\",\n\t\t\t\tinitialValue: message,\n\t\t\t\tvalidate: (v) => (v.trim() ? undefined : \"Message cannot be empty\"),\n\t\t\t});\n\t\t\tif (p.isCancel(edited)) {\n\t\t\t\tp.outro(yellow(\"Cancelled. Message cached for --retry.\"));\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t\tconst ok = await onRetry();\n\t\t\tif (ok) {\n\t\t\t\tp.outro(green(\"Committed successfully.\"));\n\t\t\t} else {\n\t\t\t\tp.outro(red(\"Commit failed again.\"));\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase \"cancel\": {\n\t\t\tp.outro(dim(\"Message cached for --retry.\"));\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n}\n","import { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport os from \"node:os\";\nimport { createHash } from \"node:crypto\";\n\nconst CACHE_DIR = join(os.homedir(), \".cache\", \"commitism\");\n\nfunction repoHash(repoPath: string): string {\n\treturn createHash(\"sha256\").update(repoPath).digest(\"hex\").slice(0, 12);\n}\n\nfunction cachePath(repoPath: string): string {\n\treturn join(CACHE_DIR, `${repoHash(repoPath)}.json`);\n}\n\nexport interface CachedCommit {\n\tmessage: string;\n\ttimestamp: number;\n\trepoPath: string;\n}\n\nexport async function saveCachedCommit(repoPath: string, message: string) {\n\tawait mkdir(CACHE_DIR, { recursive: true });\n\tconst data: CachedCommit = {\n\t\tmessage,\n\t\ttimestamp: Date.now(),\n\t\trepoPath,\n\t};\n\tawait writeFile(cachePath(repoPath), JSON.stringify(data, null, 2), \"utf8\");\n}\n\nexport async function loadCachedCommit(repoPath: string): Promise<CachedCommit | null> {\n\ttry {\n\t\tconst raw = await readFile(cachePath(repoPath), \"utf8\");\n\t\treturn JSON.parse(raw) as CachedCommit;\n\t} catch {\n\t\treturn null;\n\t}\n}\n","import { execa } from \"execa\";\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport os from \"node:os\";\nimport ini from \"ini\";\n\nconst CONFIG_PATH = join(os.homedir(), \".commitism\");\n\ninterface Config {\n\tGROQ_API_KEY?: string;\n\tmodel?: string;\n\tlocale?: string;\n\t\"max-length\"?: string;\n\ttype?: string;\n\tproxy?: string;\n\ttimeout?: string;\n}\n\nconst defaults: Config = {\n\tmodel: \"openai/gpt-oss-20b\",\n\tlocale: \"en\",\n\t\"max-length\": \"100\",\n\ttype: \"\",\n\ttimeout: \"10000\",\n};\n\nexport async function readConfig(): Promise<Config> {\n\ttry {\n\t\tconst raw = await readFile(CONFIG_PATH, \"utf8\");\n\t\tconst parsed = ini.parse(raw);\n\t\treturn { ...defaults, ...parsed };\n\t} catch {\n\t\treturn { ...defaults };\n\t}\n}\n\nexport async function writeConfig(updates: Record<string, string>) {\n\tconst existing = await readConfig();\n\tObject.assign(existing, updates);\n\tawait writeFile(CONFIG_PATH, ini.stringify(existing), \"utf8\");\n}\n\nexport async function getConfigValue(key: string): Promise<string | undefined> {\n\tconst config = await readConfig();\n\treturn config[key as keyof Config];\n}\n\nexport async function setConfigValue(key: string, value: string) {\n\tawait writeConfig({ [key]: value });\n}\n\nexport async function getApiKey(): Promise<string> {\n\tconst envKey = process.env.GROQ_API_KEY;\n\tif (envKey) return envKey;\n\n\tconst config = await readConfig();\n\tif (config.GROQ_API_KEY) return config.GROQ_API_KEY;\n\n\tthrow new Error(\n\t\t\"Please set your Groq API key via `commitism config set GROQ_API_KEY=<your token>`\",\n\t);\n}\n","import { intro, outro, spinner, isCancel } from \"@clack/prompts\";\nimport { green, red, dim, bold } from \"kolorist\";\nimport { assertGitRepo, getStagedDiff, stageAll, getHead, attemptCommit, attemptCommitNoVerify, getStatusShort } from \"../services/git.js\";\nimport { parseHookErrors } from \"../services/hooks.js\";\nimport { showRecoveryMenu } from \"../ui/menu.js\";\nimport { saveCachedCommit, loadCachedCommit } from \"../utils/cache.js\";\nimport { getApiKey, readConfig, setConfigValue } from \"../services/config.js\";\n\ninterface CommitFlags {\n\tretry: boolean;\n\tall: boolean;\n\tmessage?: string;\n}\n\nexport async function commitCommand(flags: CommitFlags) {\n\tawait assertGitRepo();\n\n\t// ── Retry mode ──────────────────────────────────────────────────\n\tif (flags.retry) {\n\t\tconst { getRepoRoot } = await import(\"../services/git.js\");\n\t\tconst repoRoot = await getRepoRoot();\n\t\tconst cached = await loadCachedCommit(repoRoot);\n\t\tif (!cached) {\n\t\t\toutro(red(\"No cached commit message found. Run commitism without --retry first.\"));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tintro(\"commitism — retry\");\n\t\tconst s = spinner();\n\t\ts.start(\"Retrying commit...\");\n\t\tconst result = await attemptCommit(cached.message);\n\t\ts.stop(\"Attempted commit\");\n\t\tif (result.ok) {\n\t\t\toutro(green(\"Committed successfully.\"));\n\t\t} else {\n\t\t\tconst errors = parseHookErrors(result.stderr ?? \"\");\n\t\t\tawait showRecoveryMenu(\n\t\t\t\terrors,\n\t\t\t\tasync () => (await attemptCommit(cached.message)).ok,\n\t\t\t\tasync (msg) => (await attemptCommitNoVerify(msg)).ok,\n\t\t\t\tasync () => {\n\t\t\t\t\tawait stageAll();\n\t\t\t\t\treturn (await attemptCommit(cached.message)).ok;\n\t\t\t\t},\n\t\t\t\tcached.message,\n\t\t\t);\n\t\t}\n\t\treturn;\n\t}\n\n\t// ── Normal mode ─────────────────────────────────────────────────\n\tintro(\"commitism\");\n\n\tconst status = await getStatusShort();\n\tif (!status) {\n\t\toutro(dim(\"Nothing to commit.\"));\n\t\treturn;\n\t}\n\n\t// Stage all changes\n\tconst s = spinner();\n\ts.start(\"Staging all changes...\");\n\tawait stageAll();\n\ts.stop(\"Changes staged\");\n\n\t// Get diff for AI\n\tconst diff = await getStagedDiff();\n\tif (!diff) {\n\t\toutro(red(\"No staged changes found.\"));\n\t\tprocess.exit(1);\n\t}\n\n\tconsole.log(diff.files.map((f) => ` ${f}`).join(\"\\n\"));\n\n\t// Generate or use provided message\n\tlet message: string;\n\n\tif (flags.message) {\n\t\tmessage = flags.message;\n\t} else {\n\t\t// Ensure API key is available before generating\n\t\ttry {\n\t\t\tawait getApiKey();\n\t\t} catch {\n\t\t\tconst { text: promptText } = await import(\"@clack/prompts\");\n\t\t\tconst key = await promptText({\n\t\t\t\tmessage: \"Enter your Groq API key:\",\n\t\t\t\tplaceholder: \"gsk_...\",\n\t\t\t\tvalidate: (v: string) => (v.trim() ? undefined : \"API key is required\"),\n\t\t\t});\n\t\t\tif (isCancel(key)) {\n\t\t\t\toutro(dim(\"Cancelled.\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait setConfigValue(\"GROQ_API_KEY\", String(key).trim());\n\t\t}\n\n\t\ts.start(\"Generating commit message...\");\n\t\ttry {\n\t\t\tmessage = await generateMessage(diff.diff, diff.files);\n\t\t} catch (err) {\n\t\t\ts.stop(red(\"Failed to generate message.\"));\n\t\t\toutro(red(err instanceof Error ? err.message : String(err)));\n\t\t\treturn;\n\t\t}\n\t\ts.stop(\"Message generated\");\n\t}\n\n\t// Review message\n\tconst { select, text } = await import(\"@clack/prompts\");\n\tconst review = await select({\n\t\tmessage: `Review commit message:\\n\\n ${bold(message)}\\n`,\n\t\toptions: [\n\t\t\t{ label: \"Use as-is\", value: \"use\" },\n\t\t\t{ label: \"Edit\", value: \"edit\" },\n\t\t\t{ label: \"Cancel\", value: \"cancel\" },\n\t\t],\n\t});\n\n\tif (isCancel(review) || review === \"cancel\") {\n\t\toutro(dim(\"Cancelled.\"));\n\t\treturn;\n\t}\n\n\tif (review === \"edit\") {\n\t\tconst edited = await text({\n\t\t\tmessage: \"Edit commit message:\",\n\t\t\tinitialValue: message,\n\t\t\tvalidate: (v: string) => (v.trim() ? undefined : \"Message cannot be empty\"),\n\t\t});\n\t\tif (isCancel(edited)) {\n\t\t\toutro(dim(\"Cancelled.\"));\n\t\t\treturn;\n\t\t}\n\t\tmessage = String(edited).trim();\n\t}\n\n\t// Cache message before attempting commit\n\tconst { getRepoRoot } = await import(\"../services/git.js\");\n\tconst repoRoot = await getRepoRoot();\n\tawait saveCachedCommit(repoRoot, message);\n\n\t// Attempt commit\n\ts.start(\"Committing...\");\n\tconst headBefore = await getHead();\n\tconst result = await attemptCommit(message);\n\tconst headAfter = await getHead();\n\n\tif (result.ok || headBefore !== headAfter) {\n\t\ts.stop(\"Committed successfully.\");\n\t\toutro(green(\"Done.\"));\n\t\treturn;\n\t}\n\n\ts.stop(\"Commit failed.\");\n\n\t// Hook failure — show recovery menu\n\tconst errors = parseHookErrors(result.stderr ?? \"\");\n\tawait showRecoveryMenu(\n\t\terrors,\n\t\tasync () => {\n\t\t\tconst r = await attemptCommit(message);\n\t\t\treturn r.ok;\n\t\t},\n\t\tasync (msg) => {\n\t\t\tconst r = await attemptCommitNoVerify(msg);\n\t\t\treturn r.ok;\n\t\t},\n\t\tasync () => {\n\t\t\tawait stageAll();\n\t\t\tconst r = await attemptCommit(message);\n\t\t\treturn r.ok;\n\t\t},\n\t\tmessage,\n\t);\n}\n\nasync function generateMessage(_diff: string, _files: string[]): Promise<string> {\n\t// TODO: Implement AI message generation via Groq\n\t// For now, return a placeholder\n\tconst config = await readConfig();\n\tvoid config; // used in full implementation\n\tconst apiKey = await getApiKey();\n\tvoid apiKey; // used in full implementation\n\treturn \"chore: initial commit\";\n}\n","import { command } from \"cleye\";\nimport { getConfigValue, setConfigValue } from \"../services/config.js\";\n\nexport const configCommand = command(\n\t{\n\t\tname: \"config\",\n\t\tparameters: [\"<mode>\", \"<key=value...>\"],\n\t},\n\tasync (argv) => {\n\t\tconst { mode, keyValue } = argv._;\n\n\t\tif (mode === \"get\") {\n\t\t\tfor (const kv of keyValue) {\n\t\t\t\tconst key = kv.split(\"=\")[0];\n\t\t\t\tconst value = await getConfigValue(key);\n\t\t\t\tconsole.log(`${key}=${value ?? \"\"}`);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (mode === \"set\") {\n\t\t\tfor (const kv of keyValue) {\n\t\t\t\tconst [key, ...rest] = kv.split(\"=\");\n\t\t\t\tconst value = rest.join(\"=\");\n\t\t\t\tawait setConfigValue(key, value);\n\t\t\t}\n\t\t\tconsole.log(\"Config updated.\");\n\t\t\treturn;\n\t\t}\n\n\t\tconsole.error(`Unknown config mode: ${mode}. Use \"get\" or \"set\".`);\n\t\tprocess.exit(1);\n\t},\n);\n","#!/usr/bin/env node\nimport { cli, command } from \"cleye\";\nimport pkg from \"../package.json\" with { type: \"json\" };\nconst { version } = pkg;\nimport { commitCommand } from \"./commands/commit.js\";\nimport { configCommand } from \"./commands/config.js\";\n\ncli(\n\t{\n\t\tname: \"commitism\",\n\t\tversion,\n\t\tdescription: \"A commit tool that actually handles hook failures\",\n\t\tflags: {\n\t\t\tretry: {\n\t\t\t\ttype: Boolean,\n\t\t\t\tdescription: \"Retry the last failed commit\",\n\t\t\t\talias: \"r\",\n\t\t\t\tdefault: false,\n\t\t\t},\n\t\t\tall: {\n\t\t\t\ttype: Boolean,\n\t\t\t\tdescription: \"Auto-stage all tracked files\",\n\t\t\t\talias: \"a\",\n\t\t\t\tdefault: false,\n\t\t\t},\n\t\t\tmessage: {\n\t\t\t\ttype: String,\n\t\t\t\tdescription: \"Provide a commit message directly (skip AI generation)\",\n\t\t\t\talias: \"m\",\n\t\t\t},\n\t\t},\n\t\tcommands: [configCommand],\n\t},\n\t(argv) => {\n\t\tcommitCommand(argv.flags);\n\t},\n);\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "commitism",
3
+ "version": "0.1.0",
4
+ "description": "A commit tool that actually handles hook failures",
5
+ "type": "module",
6
+ "bin": {
7
+ "commitism": "./dist/cli.mjs"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsdown src/cli.ts --format esm --dts --clean",
14
+ "dev": "tsx src/cli.ts",
15
+ "lint": "biome check .",
16
+ "lint:fix": "biome check --fix .",
17
+ "typecheck": "tsc --noEmit",
18
+ "test": "vitest run",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "keywords": [
22
+ "git",
23
+ "commit",
24
+ "hooks",
25
+ "pre-commit",
26
+ "lint-staged",
27
+ "ai",
28
+ "groq",
29
+ "conventional-commits",
30
+ "cli"
31
+ ],
32
+ "author": "kyubiware",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/kyubiware/commitism.git"
37
+ },
38
+ "dependencies": {
39
+ "@clack/prompts": "^0.9.1",
40
+ "cleye": "^1.3.4",
41
+ "execa": "^9.6.0",
42
+ "groq-sdk": "^0.32.0",
43
+ "ini": "^5.0.0",
44
+ "kolorist": "^1.8.0"
45
+ },
46
+ "devDependencies": {
47
+ "@biomejs/biome": "^2.0.0",
48
+ "tsdown": "^0.22.0",
49
+ "tsx": "^4.22.2",
50
+ "typescript": "^5.9.2",
51
+ "vitest": "^3.2.1"
52
+ }
53
+ }