@kyubiware/commit-mint 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -139
- package/dist/cli.mjs +158 -42
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -28,7 +28,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
28
28
|
//#region package.json
|
|
29
29
|
var package_default = {
|
|
30
30
|
name: "@kyubiware/commit-mint",
|
|
31
|
-
version: "0.5.
|
|
31
|
+
version: "0.5.3",
|
|
32
32
|
description: "🌿 A commit tool that actually handles hook failures",
|
|
33
33
|
type: "module",
|
|
34
34
|
bin: { "cmint": "./dist/cli.mjs" },
|
|
@@ -104,11 +104,11 @@ const PROVIDER_CONFIGS = {
|
|
|
104
104
|
defaultModel: "openai/gpt-oss-20b"
|
|
105
105
|
},
|
|
106
106
|
cerebras: {
|
|
107
|
-
baseURL: "https://api.cerebras.ai",
|
|
107
|
+
baseURL: "https://api.cerebras.ai/v1",
|
|
108
108
|
defaultModel: "gpt-oss-120b"
|
|
109
109
|
},
|
|
110
110
|
mistral: {
|
|
111
|
-
baseURL: "https://api.mistral.ai",
|
|
111
|
+
baseURL: "https://api.mistral.ai/v1",
|
|
112
112
|
defaultModel: "mistral-small"
|
|
113
113
|
}
|
|
114
114
|
};
|
|
@@ -124,17 +124,52 @@ function formatProviderName(provider) {
|
|
|
124
124
|
function isValidProvider(name) {
|
|
125
125
|
return ALLOWED_PROVIDERS.includes(name);
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Generic OpenAI-compatible chat completions client using fetch.
|
|
129
|
+
* Used for non-Groq providers where the Groq SDK's hardcoded `/openai/v1/` path
|
|
130
|
+
* prefix doesn't match the provider's actual API path.
|
|
131
|
+
*/
|
|
132
|
+
function createFetchClient(baseURL, apiKey, timeout) {
|
|
133
|
+
return { chat: { completions: { async create(params) {
|
|
134
|
+
const url = `${baseURL}/chat/completions`;
|
|
135
|
+
debug("fetchClient: POST %s, model=%s", url, params.model);
|
|
136
|
+
const controller = new AbortController();
|
|
137
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
138
|
+
try {
|
|
139
|
+
const response = await fetch(url, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: {
|
|
142
|
+
"Content-Type": "application/json",
|
|
143
|
+
Authorization: `Bearer ${apiKey}`
|
|
144
|
+
},
|
|
145
|
+
body: JSON.stringify(params),
|
|
146
|
+
signal: controller.signal
|
|
147
|
+
});
|
|
148
|
+
if (!response.ok) {
|
|
149
|
+
const text = await response.text().catch(() => "");
|
|
150
|
+
throw new Error(`${response.status} ${text}`);
|
|
151
|
+
}
|
|
152
|
+
return await response.json();
|
|
153
|
+
} finally {
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
}
|
|
156
|
+
} } } };
|
|
157
|
+
}
|
|
127
158
|
function createProvider(options) {
|
|
128
159
|
if (!isValidProvider(options.provider)) throw new Error(`Invalid provider "${options.provider}". Allowed values: ${ALLOWED_PROVIDERS.join(", ")}`);
|
|
129
160
|
const providerConfig = PROVIDER_CONFIGS[options.provider];
|
|
130
161
|
const model = options.modelOverride ?? providerConfig.defaultModel;
|
|
131
162
|
const baseURL = options.baseURLOverride ?? providerConfig.baseURL;
|
|
163
|
+
const timeout = options.timeout ?? 6e4;
|
|
164
|
+
let client;
|
|
165
|
+
if (options.provider === "groq") client = new Groq({
|
|
166
|
+
apiKey: options.apiKey,
|
|
167
|
+
baseURL,
|
|
168
|
+
timeout
|
|
169
|
+
});
|
|
170
|
+
else client = createFetchClient(baseURL, options.apiKey, timeout);
|
|
132
171
|
return {
|
|
133
|
-
client
|
|
134
|
-
apiKey: options.apiKey,
|
|
135
|
-
baseURL,
|
|
136
|
-
timeout: options.timeout
|
|
137
|
-
}),
|
|
172
|
+
client,
|
|
138
173
|
model
|
|
139
174
|
};
|
|
140
175
|
}
|
|
@@ -191,8 +226,17 @@ async function getProviderApiKey(provider) {
|
|
|
191
226
|
debug("getProviderApiKey(%s): not found", provider);
|
|
192
227
|
throw new Error(`Please set your ${formatProviderName(provider)} API key via \`cmint config set ${envVar}=<your token>\``);
|
|
193
228
|
}
|
|
229
|
+
/** Check if a model name is the default for a provider OTHER than the given one. */
|
|
230
|
+
function isOtherProviderDefault(model, provider) {
|
|
231
|
+
for (const [name, config] of Object.entries(PROVIDER_CONFIGS)) if (name !== provider && config.defaultModel === model) return true;
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
194
234
|
function getModelForProvider(config, provider, defaultModel) {
|
|
195
|
-
|
|
235
|
+
const providerModel = config[`model_${provider}`];
|
|
236
|
+
if (providerModel) return providerModel;
|
|
237
|
+
const globalModel = config.model;
|
|
238
|
+
if (globalModel && !isOtherProviderDefault(globalModel, provider)) return globalModel;
|
|
239
|
+
return defaultModel;
|
|
196
240
|
}
|
|
197
241
|
//#endregion
|
|
198
242
|
//#region src/services/hooks.ts
|
|
@@ -293,6 +337,50 @@ function parseEslintErrors(output) {
|
|
|
293
337
|
return errors;
|
|
294
338
|
}
|
|
295
339
|
/**
|
|
340
|
+
* Parse cmint check output into structured errors.
|
|
341
|
+
* Check output uses [tool] prefix format (built by callers in auto-group.ts / staging.ts).
|
|
342
|
+
* This is distinct from parseHookErrors which handles raw git hook stderr.
|
|
343
|
+
*/
|
|
344
|
+
function parseCheckErrors(output) {
|
|
345
|
+
if (!output.trim()) return [];
|
|
346
|
+
const errors = [];
|
|
347
|
+
const lines = output.split("\n");
|
|
348
|
+
let currentTool = "";
|
|
349
|
+
let currentLines = [];
|
|
350
|
+
const flush = () => {
|
|
351
|
+
if (currentTool && currentLines.length > 0) {
|
|
352
|
+
const message = currentLines.join("\n").trim();
|
|
353
|
+
if (message) errors.push({
|
|
354
|
+
tool: currentTool,
|
|
355
|
+
message,
|
|
356
|
+
raw: currentLines.join("\n")
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
currentTool = "";
|
|
360
|
+
currentLines = [];
|
|
361
|
+
};
|
|
362
|
+
for (const line of lines) {
|
|
363
|
+
const match = line.match(/^\[(\S+)\]\s*(.*)/);
|
|
364
|
+
if (match) {
|
|
365
|
+
flush();
|
|
366
|
+
currentTool = match[1];
|
|
367
|
+
const rest = match[2].trim();
|
|
368
|
+
if (rest) currentLines.push(rest);
|
|
369
|
+
} else if (currentTool) currentLines.push(line);
|
|
370
|
+
else currentLines.push(line);
|
|
371
|
+
}
|
|
372
|
+
flush();
|
|
373
|
+
if (errors.length === 0) {
|
|
374
|
+
const trimmed = output.trim();
|
|
375
|
+
if (trimmed) errors.push({
|
|
376
|
+
tool: "checks",
|
|
377
|
+
message: trimmed,
|
|
378
|
+
raw: output
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
return errors;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
296
384
|
* Parse lint-staged/hook stderr output to discover which tools ran
|
|
297
385
|
* and whether they succeeded. Used for clean post-commit summary.
|
|
298
386
|
*/
|
|
@@ -651,6 +739,7 @@ function mapGroqError(error, providerLabel) {
|
|
|
651
739
|
if (error instanceof Groq.RateLimitError) return /* @__PURE__ */ new Error(`Rate limited by ${label}. Please wait and try again.`);
|
|
652
740
|
if (error instanceof Groq.APIConnectionTimeoutError) return /* @__PURE__ */ new Error("Request timed out. Check your network or try a smaller diff.");
|
|
653
741
|
if (error instanceof Groq.APIError) return /* @__PURE__ */ new Error(`${label} API error: ${error.message}`);
|
|
742
|
+
if (error instanceof Error && /^4\d{2}\s/.test(error.message)) return /* @__PURE__ */ new Error(`${label} API error: ${error.message}`);
|
|
654
743
|
return /* @__PURE__ */ new Error(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`);
|
|
655
744
|
}
|
|
656
745
|
const CONVENTIONAL_COMMIT_REGEX = /^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(.+\))?!?: .+$/;
|
|
@@ -757,6 +846,7 @@ async function generateCommitMessage(diff, options) {
|
|
|
757
846
|
debug("callAI: %s — model=%s, promptLen=%d, systemLen=%d", !!strictSystemPrompt ? "RETRY (strict)" : "INITIAL", model, userPrompt.length, (strictSystemPrompt ?? systemPrompt).length);
|
|
758
847
|
try {
|
|
759
848
|
const isReasoningModel = /^(o[1-9]|.*gpt-oss.*|.*gpt-5.*)/i.test(model);
|
|
849
|
+
const isGroq = (options.provider ?? "groq") === "groq";
|
|
760
850
|
const completion = await client.chat.completions.create({
|
|
761
851
|
messages: [{
|
|
762
852
|
role: "system",
|
|
@@ -768,7 +858,7 @@ async function generateCommitMessage(diff, options) {
|
|
|
768
858
|
model,
|
|
769
859
|
temperature: .3,
|
|
770
860
|
...isReasoningModel ? { max_completion_tokens: 1024 } : { max_tokens: 1024 },
|
|
771
|
-
reasoning_format: "parsed"
|
|
861
|
+
...isGroq && isReasoningModel ? { reasoning_format: "parsed" } : {}
|
|
772
862
|
});
|
|
773
863
|
const elapsed = Date.now() - callStart;
|
|
774
864
|
const rawContent = completion.choices[0]?.message?.content;
|
|
@@ -1523,35 +1613,44 @@ async function runAutoGroupFlow(changedFiles, flags) {
|
|
|
1523
1613
|
if (excluded.length > 0) {
|
|
1524
1614
|
debug("Committing %d excluded files upfront:", excluded.length, excluded);
|
|
1525
1615
|
const message = buildExcludedFilesMessage(excluded);
|
|
1616
|
+
log.info(excluded.map((f) => ` ${f}`).join("\n"));
|
|
1526
1617
|
await resetStaging();
|
|
1527
1618
|
await stageFiles(excluded);
|
|
1528
1619
|
const headBefore = await getHead();
|
|
1529
1620
|
const commitResult = await attemptCommit(message);
|
|
1530
1621
|
const headAfter = await getHead();
|
|
1531
|
-
if (commitResult.ok || headBefore !== headAfter)
|
|
1532
|
-
|
|
1622
|
+
if (commitResult.ok || headBefore !== headAfter) {
|
|
1623
|
+
debug("Excluded files committed:", message);
|
|
1624
|
+
log.success(dim(message));
|
|
1625
|
+
} else {
|
|
1626
|
+
debug("Excluded files commit failed, continuing without them");
|
|
1627
|
+
log.warn(red("Failed to commit excluded files."));
|
|
1628
|
+
}
|
|
1533
1629
|
}
|
|
1534
1630
|
if (included.length === 0) {
|
|
1535
1631
|
debug("No included files to group, done");
|
|
1536
|
-
outro(green("
|
|
1632
|
+
if (excluded.length > 0) outro(green("Committed excluded files. No other changes to group."));
|
|
1633
|
+
else outro(dim("Nothing to commit."));
|
|
1537
1634
|
return "committed";
|
|
1538
1635
|
}
|
|
1539
1636
|
if (!flags.noCheck) {
|
|
1540
1637
|
const { getRepoRoot } = await Promise.resolve().then(() => git_exports);
|
|
1541
1638
|
const repoRoot = await getRepoRoot();
|
|
1542
1639
|
const allFiles = included.filter((f) => f.status !== "D").map((f) => f.path);
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1640
|
+
if (await detectConfig(repoRoot)) {
|
|
1641
|
+
debug("Running user checks on %d files...", allFiles.length);
|
|
1642
|
+
const ck = spinner();
|
|
1643
|
+
ck.start("Running checks...");
|
|
1644
|
+
const checkResults = await runAllChecks(repoRoot, allFiles, 6e4);
|
|
1645
|
+
debug("Check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
|
|
1646
|
+
if (!checkResults.ok) {
|
|
1647
|
+
ck.stop(`${checkResults.results.filter((r) => !r.ok).length} check(s) failed`);
|
|
1648
|
+
const rawOutput = checkResults.results.filter((r) => !r.ok).map((r) => `[${r.tool}]\n${r.stdout}\n${r.stderr}`.trim()).join("\n\n");
|
|
1649
|
+
if (await showCheckFailureMenu(parseCheckErrors(rawOutput), rawOutput) === "cancelled") return "cancelled";
|
|
1650
|
+
} else {
|
|
1651
|
+
ck.stop("All checks passed");
|
|
1652
|
+
if (checkResults.results.length > 0) log.info(checkResults.results.map((r) => ` ${green("✓")} ${r.tool}`).join("\n"));
|
|
1653
|
+
}
|
|
1555
1654
|
}
|
|
1556
1655
|
}
|
|
1557
1656
|
const config = await readConfig();
|
|
@@ -1750,16 +1849,19 @@ async function handleStaging(changedFiles, flags) {
|
|
|
1750
1849
|
}
|
|
1751
1850
|
if (stagingResult === "checks") {
|
|
1752
1851
|
await stageAll();
|
|
1753
|
-
const
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1852
|
+
const allFiles = currentFiles.filter((f) => f.status !== "D").map((f) => f.path);
|
|
1853
|
+
if (await detectConfig(repoRoot)) {
|
|
1854
|
+
const ckSpinner = spinner();
|
|
1855
|
+
ckSpinner.start("Running checks...");
|
|
1856
|
+
const ckResult = await runAllChecks(repoRoot, allFiles, 6e4);
|
|
1857
|
+
if (ckResult.ok) {
|
|
1858
|
+
ckSpinner.stop("All checks passed");
|
|
1859
|
+
for (const r of ckResult.results) if (r.stdout.trim()) log.info(dim(r.stdout.trim()));
|
|
1860
|
+
} else {
|
|
1861
|
+
const failed = ckResult.results.filter((r) => !r.ok);
|
|
1862
|
+
ckSpinner.stop(`${failed.length} check${failed.length !== 1 ? "s" : ""} failed`);
|
|
1863
|
+
for (const r of failed) log.info(r.stderr?.trim() || r.stdout?.trim() || `Check failed: ${r.command}`);
|
|
1864
|
+
}
|
|
1763
1865
|
}
|
|
1764
1866
|
currentFiles = await getChangedFiles();
|
|
1765
1867
|
continue;
|
|
@@ -1798,8 +1900,8 @@ async function runPreCommitChecks(changedFiles, noCheck) {
|
|
|
1798
1900
|
const checkResults = await runAllChecks(checkRoot, stagedFileList, 6e4);
|
|
1799
1901
|
debug("Check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
|
|
1800
1902
|
if (!checkResults.ok) {
|
|
1801
|
-
const
|
|
1802
|
-
if (await showCheckFailureMenu(
|
|
1903
|
+
const rawOutput = checkResults.results.filter((r) => !r.ok).map((r) => `[${r.tool}]\n${r.stdout}\n${r.stderr}`.trim()).join("\n\n");
|
|
1904
|
+
if (await showCheckFailureMenu(parseCheckErrors(rawOutput), rawOutput) === "cancelled") process.exit(1);
|
|
1803
1905
|
}
|
|
1804
1906
|
}
|
|
1805
1907
|
//#endregion
|
|
@@ -1937,10 +2039,11 @@ function maskKey(key) {
|
|
|
1937
2039
|
function buildConfigDisplay(config) {
|
|
1938
2040
|
const provider = isValidProvider(config.provider ?? "groq") ? config.provider : "groq";
|
|
1939
2041
|
const apiKey = config[PROVIDER_ENV_KEYS[provider]];
|
|
2042
|
+
const effectiveModel = getModelForProvider(config, provider, PROVIDER_CONFIGS[provider].defaultModel);
|
|
1940
2043
|
return [
|
|
1941
2044
|
`Provider: ${bold(formatProviderName(provider))}`,
|
|
1942
2045
|
`API Key: ${maskKey(apiKey)}`,
|
|
1943
|
-
`Model: ${
|
|
2046
|
+
`Model: ${effectiveModel}`,
|
|
1944
2047
|
`Locale: ${config.locale ?? "en"}`,
|
|
1945
2048
|
`Max Length: ${config["max-length"] ?? "100"}`,
|
|
1946
2049
|
`Commit Type: ${config.type || dim("(none)")}`,
|
|
@@ -2007,11 +2110,23 @@ function getSettingHandlers(config) {
|
|
|
2007
2110
|
provider: async () => {
|
|
2008
2111
|
const result = await promptProvider();
|
|
2009
2112
|
if (p.isCancel(result)) return result;
|
|
2010
|
-
|
|
2011
|
-
|
|
2113
|
+
const newProvider = result;
|
|
2114
|
+
const newDefaultModel = PROVIDER_CONFIGS[newProvider].defaultModel;
|
|
2115
|
+
await writeConfig({
|
|
2116
|
+
provider: newProvider,
|
|
2117
|
+
model: newDefaultModel
|
|
2118
|
+
});
|
|
2119
|
+
debug("config: provider set to %s, model set to %s", newProvider, newDefaultModel);
|
|
2120
|
+
const keyName = PROVIDER_ENV_KEYS[newProvider];
|
|
2121
|
+
if (!(await readConfig())[keyName]) {
|
|
2122
|
+
const keyResult = await promptApiKey(newProvider);
|
|
2123
|
+
if (p.isCancel(keyResult)) return keyResult;
|
|
2124
|
+
}
|
|
2012
2125
|
},
|
|
2013
2126
|
apikey: async () => promptApiKey(provider),
|
|
2014
|
-
model: async () =>
|
|
2127
|
+
model: async () => {
|
|
2128
|
+
return promptTextSetting("Model ID:", "model", getModelForProvider(config, provider, PROVIDER_CONFIGS[provider].defaultModel));
|
|
2129
|
+
},
|
|
2015
2130
|
locale: async () => promptTextSetting("Locale (e.g. en, ja, ko):", "locale", config.locale),
|
|
2016
2131
|
maxlen: async () => promptTextSetting("Max commit message length:", "max-length", config["max-length"], requireNumber),
|
|
2017
2132
|
type: async () => promptTextSetting("Commit type prefix (e.g. conventional):", "type", config.type),
|
|
@@ -2030,6 +2145,7 @@ async function editSettingsLoop(initialConfig) {
|
|
|
2030
2145
|
while (true) {
|
|
2031
2146
|
config = await readConfig();
|
|
2032
2147
|
const provider = getProvider(config);
|
|
2148
|
+
const effectiveModel = getModelForProvider(config, provider, PROVIDER_CONFIGS[provider].defaultModel);
|
|
2033
2149
|
const setting = await p.select({
|
|
2034
2150
|
message: "Select a setting to edit:",
|
|
2035
2151
|
options: [
|
|
@@ -2042,7 +2158,7 @@ async function editSettingsLoop(initialConfig) {
|
|
|
2042
2158
|
value: "apikey"
|
|
2043
2159
|
},
|
|
2044
2160
|
{
|
|
2045
|
-
label: `Model ${dim(`(${
|
|
2161
|
+
label: `Model ${dim(`(${effectiveModel})`)}`,
|
|
2046
2162
|
value: "model"
|
|
2047
2163
|
},
|
|
2048
2164
|
{
|