@kyubiware/commit-mint 0.1.0 → 0.2.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.
Files changed (3) hide show
  1. package/dist/cli.mjs +559 -183
  2. package/dist/cli.mjs.map +1 -1
  3. package/package.json +65 -54
package/dist/cli.mjs CHANGED
@@ -5,8 +5,8 @@ import { intro, isCancel, log, outro, spinner } from "@clack/prompts";
5
5
  import { bold, cyan, dim, green, red, yellow } from "kolorist";
6
6
  import Groq from "groq-sdk";
7
7
  import { mkdir, readFile, writeFile } from "node:fs/promises";
8
- import { join } from "node:path";
9
8
  import os from "node:os";
9
+ import { join } from "node:path";
10
10
  import ini from "ini";
11
11
  import { execa } from "execa";
12
12
  import { createHash } from "node:crypto";
@@ -25,7 +25,7 @@ var __exportAll = (all, no_symbols) => {
25
25
  //#region package.json
26
26
  var package_default = {
27
27
  name: "@kyubiware/commit-mint",
28
- version: "0.1.0",
28
+ version: "0.2.0",
29
29
  description: "A commit tool that actually handles hook failures",
30
30
  type: "module",
31
31
  bin: { "cmint": "./dist/cli.mjs" },
@@ -33,14 +33,20 @@ var package_default = {
33
33
  scripts: {
34
34
  "build": "tsdown src/cli.ts --format esm --dts --clean",
35
35
  "dev": "tsx src/cli.ts",
36
+ "dev:debug": "tsx src/cli.ts --debug",
36
37
  "lint": "biome check .",
37
38
  "lint:fix": "biome check --fix .",
38
39
  "typecheck": "tsc --noEmit",
39
40
  "test": "vitest run",
40
41
  "test:coverage": "vitest run --coverage",
41
42
  "test:watch": "vitest --watch",
42
- "prepublishOnly": "npm run build"
43
+ "release:patch": "bash scripts/release.sh patch",
44
+ "release:minor": "bash scripts/release.sh minor",
45
+ "release:major": "bash scripts/release.sh major",
46
+ "prepublishOnly": "npm run build",
47
+ "prepare": "simple-git-hooks"
43
48
  },
49
+ "simple-git-hooks": { "pre-commit": "npx lint-staged" },
44
50
  keywords: [
45
51
  "git",
46
52
  "commit",
@@ -68,7 +74,10 @@ var package_default = {
68
74
  },
69
75
  devDependencies: {
70
76
  "@biomejs/biome": "^2.0.0",
77
+ "@types/ini": "^4.1.1",
71
78
  "@vitest/coverage-v8": "^3.2.4",
79
+ "lint-staged": "^17.0.5",
80
+ "simple-git-hooks": "^2.13.1",
72
81
  "tsdown": "^0.22.0",
73
82
  "tsx": "^4.22.2",
74
83
  "typescript": "^5.9.2",
@@ -76,75 +85,174 @@ var package_default = {
76
85
  }
77
86
  };
78
87
  //#endregion
88
+ //#region src/utils/debug.ts
89
+ let enabled = false;
90
+ function setDebug(value) {
91
+ enabled = value;
92
+ }
93
+ function debug(...args) {
94
+ if (!enabled) return;
95
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
96
+ console.error(dim(`[debug ${timestamp}]`), ...args);
97
+ }
98
+ //#endregion
79
99
  //#region src/services/ai.ts
100
+ const MAX_DIFF_CHARS = 2e4;
80
101
  const CONVENTIONAL_COMMIT_REGEX = /^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(.+\))?!?: .+$/;
102
+ function stripThinkTags(text) {
103
+ return text.replace(/<think[\s\S]*?<\/think>/gi, "").trim();
104
+ }
105
+ function deriveMessageFromReasoning(reasoning) {
106
+ const match = reasoning.match(/(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(.+\))?!?: .+/i);
107
+ if (match) return match[0].trim();
108
+ const first = reasoning.split(/[.!?]/).find((s) => s.trim().length >= 10);
109
+ return first ? first.trim() : null;
110
+ }
111
+ function stripContextLines(diff) {
112
+ return diff.split("\n").filter((line) => !line.startsWith(" ")).join("\n");
113
+ }
81
114
  function compressDiff(diff) {
82
- if (diff.length <= 4e4) return diff;
83
- let result = diff.split("\n").map((line) => line.length > 256 ? `${line.slice(0, 256)}...` : line).join("\n");
84
- if (result.length <= 4e4) return result;
85
- const files = result.split(/(?=diff --git)/).filter(Boolean).map((fd) => ({
86
- diff: fd,
87
- parts: fd.split(/(?=\n@@)/)
88
- }));
89
- while (result.length > 4e4) {
90
- files.sort((a, b) => b.parts.length - a.parts.length);
91
- const longest = files[0];
92
- if (!longest || longest.parts.length <= 1) break;
93
- longest.parts.pop();
94
- longest.diff = longest.parts.join("");
95
- result = files.map((f) => f.diff).join("");
96
- }
97
- if (result.length <= 4e4) return result;
98
- return `Summary of changes:\n${(result.match(/^diff --git a\/(.+) b\/(.+)$/gm) || []).map((f) => {
115
+ if (diff.length <= MAX_DIFF_CHARS) return diff;
116
+ let result = stripContextLines(diff);
117
+ if (result.length <= MAX_DIFF_CHARS) return result;
118
+ result = result.split(/(?=diff --git)/).filter(Boolean).map((fd) => {
119
+ return fd.split(/(?=\n@@)/).map((part, idx) => {
120
+ if (idx === 0) return part;
121
+ const lines = part.split("\n");
122
+ return [lines[0], ...lines.slice(1).filter((l) => l.startsWith("+") || l.startsWith("-")).slice(0, 10)].join("\n");
123
+ }).join("");
124
+ }).join("");
125
+ if (result.length <= MAX_DIFF_CHARS) return result;
126
+ return `Summary of changes:\n${(diff.match(/^diff --git a\/(.+) b\/(.+)$/gm) || []).map((f) => {
99
127
  const match = f.match(/^diff --git a\/(.+) b\/(.+)$/);
100
128
  return match && match[1] === match[2] ? `${match[1]} | changed` : "";
101
129
  }).filter(Boolean).join("\n")}`;
102
130
  }
131
+ function buildStatSummary(diff) {
132
+ const files = [];
133
+ let currentFile = "";
134
+ let adds = 0;
135
+ let dels = 0;
136
+ for (const line of diff.split("\n")) {
137
+ const match = line.match(/^diff --git a\/.+ b\/(.+)$/);
138
+ if (match) {
139
+ if (currentFile) files.push({
140
+ name: currentFile,
141
+ adds,
142
+ dels
143
+ });
144
+ currentFile = match[1];
145
+ adds = 0;
146
+ dels = 0;
147
+ } else if (line.startsWith("+") && !line.startsWith("+++")) adds++;
148
+ else if (line.startsWith("-") && !line.startsWith("---")) dels++;
149
+ }
150
+ if (currentFile) files.push({
151
+ name: currentFile,
152
+ adds,
153
+ dels
154
+ });
155
+ const totalAdds = files.reduce((s, f) => s + f.adds, 0);
156
+ const totalDels = files.reduce((s, f) => s + f.dels, 0);
157
+ const lines = files.map((f) => ` ${f.name} | +${f.adds} -${f.dels}`);
158
+ lines.push(` ${files.length} files changed, ${totalAdds} insertions(+), ${totalDels} deletions(-)`);
159
+ return lines.join("\n");
160
+ }
103
161
  function buildSystemPrompt(type) {
104
162
  let prompt = "You are a commit message generator. Follow the Conventional Commits specification.\nValid types: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test.\nFormat: type(scope): description\nUse imperative mood, lowercase, no trailing period.\nOutput ONLY the commit message, no markdown fences, no explanation.";
105
163
  if (type && type.trim().length > 0) prompt += `\nYou MUST use type: ${type}`;
106
164
  return prompt;
107
165
  }
108
- function buildUserPrompt(diff, hint) {
109
- return `${hint ? `Context: ${hint}\n\n` : ""}Generate a conventional commit for:\n\n${diff}`;
166
+ function buildUserPrompt(diff, hint, statSummary) {
167
+ const parts = [];
168
+ if (hint) parts.push(`Context: ${hint}`);
169
+ if (statSummary) parts.push(`Change summary:\n${statSummary}`);
170
+ parts.push(`Generate a conventional commit for:\n\n${diff}`);
171
+ return parts.join("\n\n");
110
172
  }
111
173
  function isValidConventionalCommit(message) {
112
174
  return CONVENTIONAL_COMMIT_REGEX.test(message);
113
175
  }
114
- function enforceMaxLength(message, maxLength) {
115
- if (!maxLength || message.length <= maxLength) return message;
116
- return `${message.slice(0, maxLength - 3)}...`;
176
+ function extractContentText(content) {
177
+ if (content == null) return "";
178
+ if (typeof content === "string") return content.trim();
179
+ if (Array.isArray(content)) return content.filter((part) => part.type === "text" && typeof part.text === "string").map((part) => stripThinkTags(part.text)).join("").trim();
180
+ return "";
117
181
  }
118
182
  async function generateCommitMessage(diff, options) {
183
+ debug("generateCommitMessage: model=%s, type=%s, hint=%s", options.model ?? "default", options.type ?? "none", options.hint ?? "none");
184
+ const timeoutMs = options.timeout ?? 6e4;
185
+ debug("Timeout: %d ms", timeoutMs);
119
186
  const client = new Groq({
120
187
  apiKey: options.apiKey,
121
- timeout: options.timeout ?? 6e4
188
+ timeout: timeoutMs
122
189
  });
123
190
  const compressedDiff = compressDiff(diff);
191
+ const statSummary = buildStatSummary(diff);
124
192
  const systemPrompt = buildSystemPrompt(options.type);
125
- const userPrompt = buildUserPrompt(compressedDiff, options.hint);
193
+ const userPrompt = buildUserPrompt(compressedDiff, options.hint, statSummary);
194
+ debug("Diff: %d chars → compressed to %d chars", diff.length, compressedDiff.length);
195
+ debug("Stat summary:\n%s", statSummary);
196
+ debug("User prompt length: %d chars", userPrompt.length);
126
197
  async function callAI(strictSystemPrompt) {
127
- return ((await client.chat.completions.create({
128
- messages: [{
129
- role: "system",
130
- content: strictSystemPrompt ?? systemPrompt
131
- }, {
132
- role: "user",
133
- content: userPrompt
134
- }],
135
- model: options.model ?? "openai/gpt-oss-20b",
136
- temperature: .3,
137
- max_tokens: 300
138
- })).choices[0]?.message?.content)?.trim() ?? "";
198
+ const callStart = Date.now();
199
+ debug("callAI: %s — model=%s, promptLen=%d, systemLen=%d", !!strictSystemPrompt ? "RETRY (strict)" : "INITIAL", options.model ?? "openai/gpt-oss-20b", userPrompt.length, (strictSystemPrompt ?? systemPrompt).length);
200
+ try {
201
+ const isReasoningModel = /^(o[1-9]|.*gpt-oss.*|.*gpt-5.*)/i.test(options.model ?? "");
202
+ const completion = await client.chat.completions.create({
203
+ messages: [{
204
+ role: "system",
205
+ content: strictSystemPrompt ?? systemPrompt
206
+ }, {
207
+ role: "user",
208
+ content: userPrompt
209
+ }],
210
+ model: options.model ?? "openai/gpt-oss-20b",
211
+ temperature: .3,
212
+ ...isReasoningModel ? { max_completion_tokens: 1024 } : { max_tokens: 1024 },
213
+ reasoning_format: "parsed"
214
+ });
215
+ const elapsed = Date.now() - callStart;
216
+ const rawContent = completion.choices[0]?.message?.content;
217
+ const content = extractContentText(typeof rawContent === "string" ? stripThinkTags(rawContent) : rawContent);
218
+ debug("callAI response (%d ms): choices=%d, finishReason=%s, contentLen=%d, rawType=%s", elapsed, completion.choices.length, completion.choices[0]?.finish_reason ?? "(none)", content.length, typeof rawContent);
219
+ debug("callAI raw content: %s", content.slice(0, 300) || "(empty)");
220
+ if (!content) {
221
+ const reasoning = completion.choices[0]?.message?.reasoning;
222
+ debug("callAI: content empty, attempting reasoning fallback (reasoningLen=%d)", reasoning?.length ?? 0);
223
+ if (reasoning) {
224
+ const derived = deriveMessageFromReasoning(reasoning);
225
+ if (derived) {
226
+ debug("callAI: derived message from reasoning: %s", derived.slice(0, 100));
227
+ return stripThinkTags(derived);
228
+ }
229
+ debug("callAI: could not derive message from reasoning");
230
+ }
231
+ throw new Error("AI returned an empty commit message");
232
+ }
233
+ return content;
234
+ } catch (error) {
235
+ debug("callAI FAILED after %d ms: %s", Date.now() - callStart, error instanceof Error ? `${error.name}: ${error.message}` : String(error));
236
+ throw error;
237
+ }
139
238
  }
140
239
  try {
240
+ const totalStart = Date.now();
141
241
  let message = await callAI();
242
+ debug("Validation: message=%s, isValid=%s", message.slice(0, 100), isValidConventionalCommit(message));
142
243
  if (!isValidConventionalCommit(message)) {
244
+ debug("Initial message failed conventional commit validation, retrying with strict prompt (elapsed: %d ms)", Date.now() - totalStart);
143
245
  const retryMessage = await callAI("You MUST output ONLY a valid conventional commit message. Format: type(scope): description. If you output anything else your response will be rejected.\nValid types: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test.");
144
- if (isValidConventionalCommit(retryMessage)) message = retryMessage;
246
+ debug("Retry validation: message=%s, isValid=%s", retryMessage.slice(0, 100), isValidConventionalCommit(retryMessage));
247
+ if (isValidConventionalCommit(retryMessage)) {
248
+ debug("Retry produced valid conventional commit");
249
+ message = retryMessage;
250
+ } else debug("Retry also failed validation, using original message");
145
251
  }
146
- return enforceMaxLength(message, options.maxLength);
252
+ debug("Final message (%d ms total): %s", Date.now() - totalStart, message);
253
+ return message;
147
254
  } catch (error) {
255
+ debug("AI error: %s", error instanceof Error ? error.message : String(error));
148
256
  if (error instanceof Groq.AuthenticationError) throw new Error("Invalid GROQ_API_KEY. Run: cmint config set GROQ_API_KEY=<key>");
149
257
  if (error instanceof Groq.RateLimitError) throw new Error("Rate limited by Groq. Please wait and try again.");
150
258
  if (error instanceof Groq.APIConnectionTimeoutError) throw new Error("Request timed out. Check your network or try a smaller diff.");
@@ -163,14 +271,18 @@ const defaults = {
163
271
  timeout: "10000"
164
272
  };
165
273
  async function readConfig() {
274
+ debug("readConfig: loading from %s", CONFIG_PATH);
166
275
  try {
167
276
  const raw = await readFile(CONFIG_PATH, "utf8");
168
277
  const parsed = ini.parse(raw);
169
- return {
278
+ const merged = {
170
279
  ...defaults,
171
280
  ...parsed
172
281
  };
282
+ debug("readConfig: loaded keys: %s", Object.keys(merged).join(", "));
283
+ return merged;
173
284
  } catch {
285
+ debug("readConfig: no config file, using defaults");
174
286
  return { ...defaults };
175
287
  }
176
288
  }
@@ -187,9 +299,16 @@ async function setConfigValue(key, value) {
187
299
  }
188
300
  async function getApiKey() {
189
301
  const envKey = process.env.GROQ_API_KEY;
190
- if (envKey) return envKey;
302
+ if (envKey) {
303
+ debug("getApiKey: found in env");
304
+ return envKey;
305
+ }
191
306
  const config = await readConfig();
192
- if (config.GROQ_API_KEY) return config.GROQ_API_KEY;
307
+ if (config.GROQ_API_KEY) {
308
+ debug("getApiKey: found in config");
309
+ return config.GROQ_API_KEY;
310
+ }
311
+ debug("getApiKey: not found");
193
312
  throw new Error("Please set your Groq API key via `cmint config set GROQ_API_KEY=<your token>`");
194
313
  }
195
314
  //#endregion
@@ -199,57 +318,81 @@ var git_exports = /* @__PURE__ */ __exportAll({
199
318
  assertGitRepo: () => assertGitRepo,
200
319
  attemptCommit: () => attemptCommit,
201
320
  attemptCommitNoVerify: () => attemptCommitNoVerify,
321
+ getChangedFiles: () => getChangedFiles,
322
+ getDefaultExcludes: () => getDefaultExcludes,
202
323
  getHead: () => getHead,
203
324
  getRepoRoot: () => getRepoRoot,
204
325
  getStagedDiff: () => getStagedDiff,
205
326
  getStatusShort: () => getStatusShort,
206
- stageAll: () => stageAll
327
+ stageAll: () => stageAll,
328
+ stageFiles: () => stageFiles
207
329
  });
208
330
  var KnownError = class extends Error {};
209
331
  async function assertGitRepo() {
332
+ debug("assertGitRepo");
210
333
  const { failed } = await execa("git", ["rev-parse", "--show-toplevel"], { reject: false });
211
334
  if (failed) throw new KnownError("The current directory must be a Git repository!");
212
335
  }
213
336
  async function getRepoRoot() {
214
337
  const { stdout } = await execa("git", ["rev-parse", "--show-toplevel"]);
338
+ debug("getRepoRoot:", stdout.trim());
215
339
  return stdout.trim();
216
340
  }
341
+ const DEFAULT_EXCLUDES = [
342
+ "package-lock.json",
343
+ "node_modules/**",
344
+ "dist/**",
345
+ "build/**",
346
+ ".next/**",
347
+ "coverage/**",
348
+ "*.log",
349
+ "*.min.js",
350
+ "*.min.css",
351
+ "*.lock",
352
+ ".DS_Store"
353
+ ];
354
+ function getDefaultExcludes() {
355
+ return [...DEFAULT_EXCLUDES];
356
+ }
217
357
  async function getStagedDiff(exclude) {
218
358
  const excludeArgs = (exclude ?? []).map((e) => `:(exclude)${e}`);
219
- const defaultExcludes = [
220
- "package-lock.json",
221
- "node_modules/**",
222
- "dist/**",
223
- "build/**",
224
- ".next/**",
225
- "coverage/**",
226
- "*.log",
227
- "*.min.js",
228
- "*.min.css",
229
- "*.lock",
230
- ".DS_Store"
231
- ].map((e) => `:(exclude)${e}`);
359
+ const defaultExcludeArgs = DEFAULT_EXCLUDES.map((e) => `:(exclude)${e}`);
360
+ const { stdout: allFiles } = await execa("git", [
361
+ "diff",
362
+ "--cached",
363
+ "--name-only"
364
+ ]);
365
+ if (!allFiles) {
366
+ debug("getStagedDiff: no staged files");
367
+ return null;
368
+ }
232
369
  const { stdout: files } = await execa("git", [
233
370
  "diff",
234
371
  "--cached",
235
372
  "--name-only",
236
- ...defaultExcludes,
373
+ ...defaultExcludeArgs,
237
374
  ...excludeArgs
238
375
  ]);
239
- if (!files) return null;
376
+ if (!files) {
377
+ const excludedFiles = allFiles.split("\n").filter(Boolean);
378
+ debug("getStagedDiff: all files excluded:", excludedFiles);
379
+ return { excludedFiles };
380
+ }
240
381
  const { stdout: diff } = await execa("git", [
241
382
  "diff",
242
383
  "--cached",
243
384
  "--diff-algorithm=minimal",
244
- ...defaultExcludes,
385
+ ...defaultExcludeArgs,
245
386
  ...excludeArgs
246
387
  ]);
388
+ debug("getStagedDiff:", files.split("\n").filter(Boolean).length, "files,", diff.length, "chars");
247
389
  return {
248
390
  files: files.split("\n").filter(Boolean),
249
391
  diff
250
392
  };
251
393
  }
252
394
  async function stageAll() {
395
+ debug("stageAll: git add -A");
253
396
  await execa("git", ["add", "-A"]);
254
397
  }
255
398
  async function getHead() {
@@ -260,25 +403,51 @@ async function getStatusShort() {
260
403
  const { stdout } = await execa("git", ["status", "--short"]);
261
404
  return stdout.trim();
262
405
  }
406
+ async function getChangedFiles() {
407
+ const { stdout } = await execa("git", ["status", "--short"]);
408
+ if (!stdout.trim()) return [];
409
+ const files = stdout.split("\n").filter(Boolean).map((line) => ({
410
+ status: line.slice(0, 2).trim(),
411
+ path: line.slice(3)
412
+ }));
413
+ debug("getChangedFiles:", files.length, "files");
414
+ return files;
415
+ }
416
+ async function stageFiles(paths) {
417
+ debug("stageFiles:", paths);
418
+ await execa("git", ["add", ...paths]);
419
+ }
263
420
  async function attemptCommit(message, extraArgs = []) {
421
+ debug("attemptCommit:", message, extraArgs.length ? extraArgs : "(no extra args)");
264
422
  try {
265
- await execa("git", [
423
+ const subprocess = execa("git", [
266
424
  "commit",
267
425
  "-m",
268
426
  message,
269
427
  ...extraArgs
270
428
  ]);
271
- return { ok: true };
429
+ const stderrChunks = [];
430
+ subprocess.stderr?.on("data", (chunk) => {
431
+ stderrChunks.push(chunk.toString());
432
+ });
433
+ await subprocess;
434
+ debug("attemptCommit: success");
435
+ return {
436
+ ok: true,
437
+ stderr: stderrChunks.join("")
438
+ };
272
439
  } catch (error) {
273
440
  const e = error;
441
+ debug("attemptCommit: failed —", e.message?.slice(0, 200));
274
442
  return {
275
443
  ok: false,
276
444
  error: e.message,
277
- stderr: e.stderr ?? ""
445
+ stderr: typeof e.stderr === "string" ? e.stderr : ""
278
446
  };
279
447
  }
280
448
  }
281
449
  async function attemptCommitNoVerify(message) {
450
+ debug("attemptCommitNoVerify:", message);
282
451
  return attemptCommit(message, ["--no-verify"]);
283
452
  }
284
453
  //#endregion
@@ -289,25 +458,27 @@ async function attemptCommitNoVerify(message) {
289
458
  */
290
459
  function parseHookErrors(stderr) {
291
460
  if (!stderr) return [];
461
+ debug("parseHookErrors: stderr length=%d", stderr.length);
292
462
  const errors = [];
293
- stderr.split("\n");
294
463
  if (stderr.includes("lint-staged") || stderr.includes("[FAILED]")) errors.push(...parseLintStagedErrors(stderr));
295
464
  if (stderr.includes("biome") || stderr.includes("Biome")) errors.push(...parseBiomeErrors(stderr));
296
465
  if (stderr.includes("error TS") || stderr.includes("tsc")) errors.push(...parseTscErrors(stderr));
297
466
  if (stderr.includes("vitest") || stderr.includes("jest") || stderr.includes("FAIL") || stderr.includes("test failed")) errors.push(...parseTestErrors(stderr));
298
467
  if (stderr.includes("eslint") || stderr.includes("ESLint")) errors.push(...parseEslintErrors(stderr));
299
- if (errors.length === 0) errors.push({
300
- tool: "git hooks",
301
- message: stderr.trim(),
302
- raw: stderr
303
- });
468
+ if (errors.length === 0) {
469
+ debug("parseHookErrors: no patterns matched, using raw fallback");
470
+ errors.push({
471
+ tool: "git hooks",
472
+ message: stderr.trim(),
473
+ raw: stderr
474
+ });
475
+ }
476
+ debug("parseHookErrors: found %d errors", errors.length);
304
477
  return errors;
305
478
  }
306
479
  function parseLintStagedErrors(output) {
307
480
  const errors = [];
308
- const taskPattern = /\[FAILED\]\s+(.+?)\s+\[FAILED\]/g;
309
- let match;
310
- while ((match = taskPattern.exec(output)) !== null) {
481
+ for (const match of output.matchAll(/\[FAILED\]\s+(.+?)\s+\[FAILED\]/g)) {
311
482
  const task = match[1].trim();
312
483
  errors.push({
313
484
  tool: "lint-staged",
@@ -319,9 +490,7 @@ function parseLintStagedErrors(output) {
319
490
  }
320
491
  function parseBiomeErrors(output) {
321
492
  const errors = [];
322
- const biomePattern = /^(.+?):(\d+):(\d+)\s+(.+)$/gm;
323
- let match;
324
- while ((match = biomePattern.exec(output)) !== null) errors.push({
493
+ for (const match of output.matchAll(/^(.+?):(\d+):(\d+)\s+(.+)$/gm)) errors.push({
325
494
  tool: "biome",
326
495
  message: `${match[1]}:${match[2]}:${match[3]} — ${match[4]}`,
327
496
  raw: match[0]
@@ -335,9 +504,7 @@ function parseBiomeErrors(output) {
335
504
  }
336
505
  function parseTscErrors(output) {
337
506
  const errors = [];
338
- const tscPattern = /^(.+?)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/gm;
339
- let match;
340
- while ((match = tscPattern.exec(output)) !== null) errors.push({
507
+ for (const match of output.matchAll(/^(.+?)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/gm)) errors.push({
341
508
  tool: "tsc",
342
509
  message: `${match[1]}:${match[2]}:${match[3]} — ${match[4]}: ${match[5]}`,
343
510
  raw: match[0]
@@ -361,71 +528,103 @@ function parseTestErrors(output) {
361
528
  }
362
529
  function parseEslintErrors(output) {
363
530
  const errors = [];
364
- const eslintPattern = /^\s*\d+:(\d+)\s+(error|warning)\s+(.+?)\s+(.+?)$/gm;
365
- let match;
366
- while ((match = eslintPattern.exec(output)) !== null) errors.push({
531
+ for (const match of output.matchAll(/^\s*\d+:(\d+)\s+(error|warning)\s+(.+?)\s+(.+?)$/gm)) errors.push({
367
532
  tool: "eslint",
368
533
  message: `${match[2]}: ${match[3]} (${match[4]})`,
369
534
  raw: match[0]
370
535
  });
371
536
  return errors;
372
537
  }
373
- function formatErrorReport(errors) {
374
- if (errors.length === 0) return "";
375
- return errors.map((e) => `[${e.tool}]\n${e.message}`).join("\n\n");
538
+ /**
539
+ * Parse lint-staged/hook stderr output to discover which tools ran
540
+ * and whether they succeeded. Used for clean post-commit summary.
541
+ */
542
+ function parseToolChecks(stderr) {
543
+ if (!stderr) return [];
544
+ const checks = [];
545
+ for (const match of stderr.matchAll(/\[(COMPLETED|FAILED)\]\s+(.+)/g)) {
546
+ const status = match[1];
547
+ const command = match[2].trim();
548
+ if (isLintStagedMeta(command)) continue;
549
+ const tool = extractToolName(command);
550
+ if (!tool) continue;
551
+ checks.push({
552
+ tool,
553
+ ok: status === "COMPLETED"
554
+ });
555
+ }
556
+ const seen = /* @__PURE__ */ new Map();
557
+ for (const c of checks) seen.set(c.tool, c);
558
+ return [...seen.values()];
559
+ }
560
+ /** Heuristic: skip lint-staged internal metadata lines */
561
+ function isLintStagedMeta(command) {
562
+ if (/[*{}[\]]/.test(command)) return true;
563
+ if (/\s[-–—]\s(\d+\s)?files?$/.test(command)) return true;
564
+ if (/\s[-–—]\sno\s files$/.test(command)) return true;
565
+ if (/^(Running tasks|Applying modifications|Cleaning up|Backing up|Backed up|Updating Git)/.test(command)) return true;
566
+ if (/\.{3}$/.test(command)) return true;
567
+ return false;
568
+ }
569
+ /** Extract a display-friendly tool name from a lint-staged command */
570
+ function extractToolName(command) {
571
+ const tokens = command.split(/\s+/);
572
+ const first = tokens[0];
573
+ if ([
574
+ "npm",
575
+ "yarn",
576
+ "pnpm",
577
+ "bun"
578
+ ].includes(first)) {
579
+ const script = tokens[tokens[1] === "run" ? 2 : 1];
580
+ if (!script) return null;
581
+ return {
582
+ typecheck: "tsc",
583
+ lint: "eslint",
584
+ format: "prettier"
585
+ }[script] ?? script;
586
+ }
587
+ if (first === "npx") return tokens[1] ?? null;
588
+ return first;
376
589
  }
377
590
  //#endregion
378
591
  //#region src/services/clipboard.ts
379
592
  async function copyToClipboard(content) {
380
- for (const [cmd, ...args] of [
381
- ["wl-copy"],
382
- [
383
- "xclip",
384
- "-selection",
385
- "clipboard"
386
- ],
387
- [
388
- "xsel",
389
- "--clipboard",
390
- "--input"
391
- ],
392
- ["pbcopy"]
593
+ for (const [cmd, args] of [
594
+ ["wl-copy", []],
595
+ ["xclip", ["-selection", "clipboard"]],
596
+ ["xsel", ["--clipboard", "--input"]],
597
+ ["pbcopy", []]
393
598
  ]) try {
394
- const { stdout } = await execa("which", [cmd], { reject: false });
395
- if (!stdout) continue;
396
- await execa(cmd, args.length > 0 ? args : [], { input: content });
599
+ await execa(cmd, args, { input: content });
397
600
  return true;
398
- } catch {
399
- continue;
400
- }
601
+ } catch {}
401
602
  return false;
402
603
  }
403
604
  //#endregion
404
605
  //#region src/ui/menu.ts
405
- async function showRecoveryMenu(errors, onRetry, onSkipHooks, onRestage, message) {
406
- p.note(errors.map((e) => ` ${red("•")} [${e.tool}] ${e.message}`).join("\n"), red(bold("Pre-commit hook failed")));
606
+ async function showStagingMenu(files) {
607
+ debug("showStagingMenu: %d files", files.length);
608
+ const statusLabel = (status) => {
609
+ switch (status) {
610
+ case "M": return yellow("M");
611
+ case "A": return green("A");
612
+ case "D": return red("D");
613
+ case "?": return dim("?");
614
+ default: return dim(status);
615
+ }
616
+ };
407
617
  const choice = await p.select({
408
- message: "What do you want to do?",
618
+ message: "Stage files for commit:",
409
619
  options: [
410
620
  {
411
- label: "Copy error report to clipboard",
412
- value: "clipboard",
413
- hint: "Paste into another terminal for an AI agent"
621
+ label: "Stage all files",
622
+ value: "all",
623
+ hint: `${files.length} file${files.length !== 1 ? "s" : ""}`
414
624
  },
415
625
  {
416
- label: "Skip hooks and commit (--no-verify)",
417
- value: "skip",
418
- hint: "Commit anyway, fix later"
419
- },
420
- {
421
- label: "Re-stage files and retry",
422
- value: "restage",
423
- hint: "Pick up fixes from another terminal"
424
- },
425
- {
426
- label: "Edit commit message",
427
- value: "edit",
428
- hint: "Modify the message before retrying"
626
+ label: "Select files...",
627
+ value: "select"
429
628
  },
430
629
  {
431
630
  label: "Cancel",
@@ -433,44 +632,102 @@ async function showRecoveryMenu(errors, onRetry, onSkipHooks, onRestage, message
433
632
  }
434
633
  ]
435
634
  });
436
- if (p.isCancel(choice)) {
437
- p.outro(yellow("Cancelled. Message cached for --retry."));
438
- process.exit(1);
439
- }
440
- switch (choice) {
441
- case "clipboard":
442
- if (await copyToClipboard(formatErrorReport(errors))) p.outro(green("Error report copied to clipboard."));
443
- else p.outro(red("No clipboard tool found. Install xclip, wl-copy, or xsel."));
444
- p.log.info(dim("Fix the errors, then run: cmint --retry"));
445
- process.exit(0);
446
- break;
447
- case "skip":
448
- p.log.info(yellow("Committing with --no-verify..."));
449
- if (await onSkipHooks(message)) p.outro(green("Committed (hooks skipped)."));
450
- else p.outro(red("Commit failed even with --no-verify."));
451
- break;
452
- case "restage":
453
- p.log.info(cyan("Re-staging and retrying..."));
454
- if (await onRestage()) p.outro(green("Committed successfully."));
455
- else await showRecoveryMenu(errors, onRetry, onSkipHooks, onRestage, message);
456
- break;
457
- case "edit": {
458
- const edited = await p.text({
459
- message: "Edit commit message:",
460
- initialValue: message,
461
- validate: (v) => v.trim() ? void 0 : "Message cannot be empty"
462
- });
463
- if (p.isCancel(edited)) {
464
- p.outro(yellow("Cancelled. Message cached for --retry."));
465
- process.exit(1);
635
+ if (p.isCancel(choice) || choice === "cancel") return null;
636
+ if (choice === "all") return {
637
+ files: files.map((f) => f.path),
638
+ all: true
639
+ };
640
+ const selected = await p.multiselect({
641
+ message: "Select files to stage:",
642
+ options: files.map((f) => ({
643
+ label: `${statusLabel(f.status)} ${f.path}`,
644
+ value: f.path
645
+ })),
646
+ required: true
647
+ });
648
+ if (p.isCancel(selected)) return null;
649
+ return {
650
+ files: selected,
651
+ all: false
652
+ };
653
+ }
654
+ async function showRecoveryMenu(errors, onRetry, onSkipHooks, onRestage, message, rawStderr) {
655
+ debug("showRecoveryMenu: %d errors", errors.length);
656
+ while (true) {
657
+ p.note(errors.map((e) => ` ${red("•")} [${e.tool}] ${e.message}`).join("\n"), red(bold("Pre-commit hook failed")));
658
+ const choice = await p.select({
659
+ message: "What do you want to do?",
660
+ options: [
661
+ {
662
+ label: "Copy error report to clipboard",
663
+ value: "clipboard",
664
+ hint: "Paste into another terminal for an AI agent"
665
+ },
666
+ {
667
+ label: "Skip hooks and commit (--no-verify)",
668
+ value: "skip",
669
+ hint: "Commit anyway, fix later"
670
+ },
671
+ {
672
+ label: "Re-stage files and retry",
673
+ value: "restage",
674
+ hint: "Pick up fixes from another terminal"
675
+ },
676
+ {
677
+ label: "Edit commit message",
678
+ value: "edit",
679
+ hint: "Modify the message before retrying"
680
+ },
681
+ {
682
+ label: "Cancel",
683
+ value: "cancel"
684
+ }
685
+ ]
686
+ });
687
+ if (p.isCancel(choice)) {
688
+ debug("showRecoveryMenu: user cancelled");
689
+ p.outro(yellow("Cancelled. Message cached for --retry."));
690
+ process.exit(1);
691
+ return;
692
+ }
693
+ debug("showRecoveryMenu: user chose %s", choice);
694
+ switch (choice) {
695
+ case "clipboard":
696
+ if (await copyToClipboard(rawStderr)) p.log.step(green("Errors copied"));
697
+ else p.log.warn(red("No clipboard tool found. Install xclip, wl-copy, or xsel."));
698
+ continue;
699
+ case "skip":
700
+ p.log.info(yellow("Committing with --no-verify..."));
701
+ if (await onSkipHooks(message)) p.outro(green("Committed (hooks skipped)."));
702
+ else p.outro(red("Commit failed even with --no-verify."));
703
+ return;
704
+ case "restage":
705
+ p.log.info(cyan("Re-staging and retrying..."));
706
+ if (await onRestage()) {
707
+ p.outro(green("Committed successfully."));
708
+ return;
709
+ }
710
+ continue;
711
+ case "edit": {
712
+ const edited = await p.text({
713
+ message: "Edit commit message:",
714
+ initialValue: message,
715
+ validate: (v) => v.trim() ? void 0 : "Message cannot be empty"
716
+ });
717
+ if (p.isCancel(edited)) {
718
+ p.outro(yellow("Cancelled. Message cached for --retry."));
719
+ process.exit(1);
720
+ return;
721
+ }
722
+ if (await onRetry()) p.outro(green("Committed successfully."));
723
+ else p.outro(red("Commit failed again."));
724
+ return;
466
725
  }
467
- if (await onRetry()) p.outro(green("Committed successfully."));
468
- else p.outro(red("Commit failed again."));
469
- break;
726
+ case "cancel":
727
+ p.outro(dim("Message cached for --retry."));
728
+ process.exit(1);
729
+ return;
470
730
  }
471
- case "cancel":
472
- p.outro(dim("Message cached for --retry."));
473
- process.exit(1);
474
731
  }
475
732
  }
476
733
  //#endregion
@@ -489,60 +746,141 @@ async function saveCachedCommit(repoPath, message) {
489
746
  timestamp: Date.now(),
490
747
  repoPath
491
748
  };
492
- await writeFile(cachePath(repoPath), JSON.stringify(data, null, 2), "utf8");
749
+ const path = cachePath(repoPath);
750
+ debug("saveCachedCommit: saving to %s", path);
751
+ await writeFile(path, JSON.stringify(data, null, 2), "utf8");
493
752
  }
494
753
  async function loadCachedCommit(repoPath) {
754
+ const path = cachePath(repoPath);
755
+ debug("loadCachedCommit: loading from %s", path);
495
756
  try {
496
- const raw = await readFile(cachePath(repoPath), "utf8");
497
- return JSON.parse(raw);
757
+ const raw = await readFile(path, "utf8");
758
+ const data = JSON.parse(raw);
759
+ debug("loadCachedCommit: found message from %s", new Date(data.timestamp).toISOString());
760
+ return data;
498
761
  } catch {
762
+ debug("loadCachedCommit: no cached commit found");
499
763
  return null;
500
764
  }
501
765
  }
502
766
  //#endregion
503
767
  //#region src/commands/commit.ts
504
768
  async function commitCommand(flags) {
769
+ debug("commitCommand called", { flags });
505
770
  await assertGitRepo();
506
771
  if (flags.retry) {
772
+ debug("Entering retry mode");
507
773
  const { getRepoRoot } = await Promise.resolve().then(() => git_exports);
508
- const cached = await loadCachedCommit(await getRepoRoot());
774
+ const repoRoot = await getRepoRoot();
775
+ debug("Repo root:", repoRoot);
776
+ const cached = await loadCachedCommit(repoRoot);
509
777
  if (!cached) {
778
+ debug("No cached commit found");
510
779
  outro(red("No cached commit message found. Run cmint without --retry first."));
511
780
  process.exit(1);
512
781
  }
782
+ debug("Loaded cached message:", cached.message);
513
783
  intro("commit-mint — retry");
514
784
  const s = spinner();
515
785
  s.start("Retrying commit...");
516
786
  const result = await attemptCommit(cached.message);
517
787
  s.stop("Attempted commit");
518
- if (result.ok) outro(green("Committed successfully."));
519
- else await showRecoveryMenu(parseHookErrors(result.stderr ?? ""), async () => (await attemptCommit(cached.message)).ok, async (msg) => (await attemptCommitNoVerify(msg)).ok, async () => {
520
- await stageAll();
521
- return (await attemptCommit(cached.message)).ok;
522
- }, cached.message);
788
+ debug("Retry commit result:", result);
789
+ if (result.ok) {
790
+ const checks = parseToolChecks(result.stderr ?? "");
791
+ if (checks.length > 0) {
792
+ const lines = checks.map((c) => ` ${c.ok ? green("✓") : red("✗")} ${c.tool}`);
793
+ log.info(lines.join("\n"));
794
+ }
795
+ outro(green("Committed successfully."));
796
+ } else {
797
+ const errors = parseHookErrors(result.stderr ?? "");
798
+ debug("Hook errors on retry:", errors.length);
799
+ await showRecoveryMenu(errors, async () => (await attemptCommit(cached.message)).ok, async (msg) => (await attemptCommitNoVerify(msg)).ok, async () => {
800
+ await stageAll();
801
+ return (await attemptCommit(cached.message)).ok;
802
+ }, cached.message, result.stderr ?? "");
803
+ }
523
804
  return;
524
805
  }
525
806
  intro("commit-mint");
526
- if (!await getStatusShort()) {
807
+ const status = await getStatusShort();
808
+ debug("Git status:", status || "(empty)");
809
+ if (!status) {
527
810
  outro(dim("Nothing to commit."));
528
811
  return;
529
812
  }
813
+ const changedFiles = await getChangedFiles();
814
+ debug("Changed files:", changedFiles.length);
530
815
  const s = spinner();
531
- s.start("Staging all changes...");
532
- await stageAll();
533
- s.stop("Changes staged");
534
- const diff = await getStagedDiff();
535
- if (!diff) {
816
+ try {
817
+ if (flags.all) {
818
+ s.start("Staging all changes...");
819
+ await stageAll();
820
+ s.stop("Changes staged");
821
+ } else if (changedFiles.length === 1) {
822
+ s.start(`Staging ${changedFiles[0].path}...`);
823
+ await stageFiles([changedFiles[0].path]);
824
+ s.stop("File staged");
825
+ } else {
826
+ const stagingResult = await showStagingMenu(changedFiles);
827
+ if (!stagingResult) {
828
+ outro(dim("Cancelled."));
829
+ return;
830
+ }
831
+ s.start(`Staging ${stagingResult.files.length} file${stagingResult.files.length !== 1 ? "s" : ""}...`);
832
+ if (stagingResult.all) await stageAll();
833
+ else await stageFiles(stagingResult.files);
834
+ s.stop("Files staged");
835
+ }
836
+ } catch (err) {
837
+ s.stop(red("Staging failed."));
838
+ const msg = err instanceof Error ? err.message : String(err);
839
+ debug("Staging error:", msg);
840
+ outro(red(`Failed to stage files: ${msg}`));
841
+ process.exit(1);
842
+ }
843
+ const diffResult = await getStagedDiff();
844
+ if (!diffResult) {
845
+ debug("No staged changes found after staging");
536
846
  outro(red("No staged changes found."));
537
847
  process.exit(1);
538
848
  }
539
- log.info(diff.files.map((f) => ` ${f}`).join("\n"));
849
+ if ("excludedFiles" in diffResult) {
850
+ debug("All staged files are excluded:", diffResult.excludedFiles);
851
+ const message = buildExcludedFilesMessage(diffResult.excludedFiles);
852
+ log.info(diffResult.excludedFiles.map((f) => ` ${f}`).join("\n"));
853
+ const { getRepoRoot } = await Promise.resolve().then(() => git_exports);
854
+ await saveCachedCommit(await getRepoRoot(), message);
855
+ s.start("Committing...");
856
+ const headBefore = await getHead();
857
+ const result = await attemptCommit(message);
858
+ const headAfter = await getHead();
859
+ if (result.ok || headBefore !== headAfter) {
860
+ s.stop("Committed successfully.");
861
+ outro(green("Done."));
862
+ return;
863
+ }
864
+ s.stop("Commit failed.");
865
+ await showRecoveryMenu(parseHookErrors(result.stderr ?? ""), async () => (await attemptCommit(message)).ok, async (msg) => (await attemptCommitNoVerify(msg)).ok, async () => {
866
+ await stageAll();
867
+ return (await attemptCommit(message)).ok;
868
+ }, message, result.stderr ?? "");
869
+ return;
870
+ }
871
+ debug("Staged files:", diffResult.files);
872
+ debug("Diff length:", diffResult.diff.length, "chars");
873
+ log.info(diffResult.files.map((f) => ` ${f}`).join("\n"));
540
874
  let message;
541
- if (flags.message) message = flags.message;
542
- else {
875
+ if (flags.message) {
876
+ debug("Using provided message:", flags.message);
877
+ message = flags.message;
878
+ } else {
543
879
  try {
544
880
  await getApiKey();
881
+ debug("API key found");
545
882
  } catch {
883
+ debug("No API key found, prompting user");
546
884
  const { text: promptText } = await import("@clack/prompts");
547
885
  const key = await promptText({
548
886
  message: "Enter your Groq API key:",
@@ -554,12 +892,17 @@ async function commitCommand(flags) {
554
892
  return;
555
893
  }
556
894
  await setConfigValue("GROQ_API_KEY", String(key).trim());
895
+ debug("API key saved to config");
557
896
  }
558
897
  s.start("Generating commit message...");
559
898
  try {
560
- message = await generateMessage(diff.diff, flags.hint);
899
+ const genStart = Date.now();
900
+ message = await generateMessage(diffResult.diff, flags.hint);
901
+ debug("generateMessage took %d ms", Date.now() - genStart);
902
+ debug("Generated message:", message);
561
903
  } catch (err) {
562
904
  s.stop(red("Failed to generate message."));
905
+ debug("Message generation failed:", err instanceof Error ? err.message : String(err));
563
906
  outro(red(err instanceof Error ? err.message : String(err)));
564
907
  return;
565
908
  }
@@ -584,10 +927,12 @@ async function commitCommand(flags) {
584
927
  ]
585
928
  });
586
929
  if (isCancel(review) || review === "cancel") {
930
+ debug("User cancelled at review step");
587
931
  outro(dim("Cancelled."));
588
932
  return;
589
933
  }
590
934
  if (review === "edit") {
935
+ debug("User chose to edit message");
591
936
  const edited = await text({
592
937
  message: "Edit commit message:",
593
938
  initialValue: message,
@@ -598,39 +943,63 @@ async function commitCommand(flags) {
598
943
  return;
599
944
  }
600
945
  message = String(edited).trim();
946
+ debug("Edited message:", message);
601
947
  }
602
948
  const { getRepoRoot } = await Promise.resolve().then(() => git_exports);
603
- await saveCachedCommit(await getRepoRoot(), message);
949
+ const repoRoot = await getRepoRoot();
950
+ await saveCachedCommit(repoRoot, message);
951
+ debug("Message cached for repo:", repoRoot);
604
952
  s.start("Committing...");
605
953
  const headBefore = await getHead();
954
+ debug("HEAD before commit:", headBefore);
606
955
  const result = await attemptCommit(message);
607
956
  const headAfter = await getHead();
957
+ debug("HEAD after commit:", headAfter);
958
+ debug("Commit result:", result);
608
959
  if (result.ok || headBefore !== headAfter) {
609
960
  s.stop("Committed successfully.");
961
+ const checks = parseToolChecks(result.stderr ?? "");
962
+ if (checks.length > 0) {
963
+ const lines = checks.map((c) => ` ${c.ok ? green("✓") : red("✗")} ${c.tool}`);
964
+ log.info(lines.join("\n"));
965
+ }
610
966
  outro(green("Done."));
611
967
  return;
612
968
  }
613
969
  s.stop("Commit failed.");
614
- await showRecoveryMenu(parseHookErrors(result.stderr ?? ""), async () => {
970
+ debug("Commit failed, showing recovery menu");
971
+ const errors = parseHookErrors(result.stderr ?? "");
972
+ debug("Parsed hook errors:", errors.length, "errors");
973
+ await showRecoveryMenu(errors, async () => {
615
974
  return (await attemptCommit(message)).ok;
616
975
  }, async (msg) => {
617
976
  return (await attemptCommitNoVerify(msg)).ok;
618
977
  }, async () => {
619
978
  await stageAll();
620
979
  return (await attemptCommit(message)).ok;
621
- }, message);
980
+ }, message, result.stderr ?? "");
622
981
  }
623
982
  async function generateMessage(diff, hint) {
624
983
  const config = await readConfig();
984
+ const apiKey = await getApiKey();
985
+ debug("Generating message with model:", config.model, "type:", config.type);
625
986
  return generateCommitMessage(diff, {
626
- apiKey: await getApiKey(),
987
+ apiKey,
627
988
  model: config.model,
628
- maxLength: config["max-length"] ? parseInt(config["max-length"], 10) : void 0,
629
989
  type: config.type,
630
990
  timeout: config.timeout ? parseInt(config.timeout, 10) : void 0,
631
991
  hint
632
992
  });
633
993
  }
994
+ function buildExcludedFilesMessage(files) {
995
+ const excludes = getDefaultExcludes();
996
+ const isLockfile = (f) => excludes.some((pattern) => {
997
+ if (pattern.endsWith(".lock") || pattern.endsWith(".json")) return f === pattern || f.endsWith(pattern.replace("*.", "."));
998
+ return false;
999
+ });
1000
+ if (files.every(isLockfile)) return "chore: update lockfile";
1001
+ return "chore: update generated files";
1002
+ }
634
1003
  //#endregion
635
1004
  //#region src/commands/config.ts
636
1005
  const configCommand = command({
@@ -686,10 +1055,17 @@ cli({
686
1055
  type: String,
687
1056
  description: "Add context hint for AI commit message generation",
688
1057
  alias: "H"
1058
+ },
1059
+ debug: {
1060
+ type: Boolean,
1061
+ description: "Enable debug output",
1062
+ alias: "d",
1063
+ default: false
689
1064
  }
690
1065
  },
691
1066
  commands: [configCommand]
692
1067
  }, (argv) => {
1068
+ setDebug(argv.flags.debug);
693
1069
  commitCommand(argv.flags);
694
1070
  });
695
1071
  //#endregion