@kud/ai-conventional-commit-cli 0.11.1 → 0.12.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/dist/chunk-H4W6AMGZ.js +549 -0
- package/dist/chunk-YIXP5EWA.js +545 -0
- package/dist/index.js +87 -555
- package/dist/reword-CZDYMQEV.js +150 -0
- package/dist/reword-FE5N4MGV.js +150 -0
- package/package.json +1 -1
- package/dist/index.cjs +0 -1200
- package/dist/index.d.cts +0 -1
package/dist/index.js
CHANGED
|
@@ -2,12 +2,27 @@
|
|
|
2
2
|
import {
|
|
3
3
|
loadConfig
|
|
4
4
|
} from "./chunk-DCGUX6KW.js";
|
|
5
|
+
import {
|
|
6
|
+
OpenCodeProvider,
|
|
7
|
+
abortMessage,
|
|
8
|
+
animateHeaderBase,
|
|
9
|
+
borderLine,
|
|
10
|
+
buildGenerationMessages,
|
|
11
|
+
buildRefineMessages,
|
|
12
|
+
checkCandidate,
|
|
13
|
+
createPhasedSpinner,
|
|
14
|
+
extractJSON,
|
|
15
|
+
finalSuccess,
|
|
16
|
+
formatCommitTitle,
|
|
17
|
+
renderCommitBlock,
|
|
18
|
+
sectionTitle
|
|
19
|
+
} from "./chunk-H4W6AMGZ.js";
|
|
5
20
|
|
|
6
21
|
// src/index.ts
|
|
7
22
|
import { Cli, Command, Option } from "clipanion";
|
|
8
23
|
|
|
9
24
|
// src/workflow/generate.ts
|
|
10
|
-
import
|
|
25
|
+
import chalk from "chalk";
|
|
11
26
|
import ora from "ora";
|
|
12
27
|
|
|
13
28
|
// src/git.ts
|
|
@@ -146,269 +161,6 @@ var buildStyleProfile = (messages) => {
|
|
|
146
161
|
};
|
|
147
162
|
};
|
|
148
163
|
|
|
149
|
-
// src/prompt.ts
|
|
150
|
-
var summarizeDiffForPrompt = (files, privacy) => {
|
|
151
|
-
if (privacy === "high") {
|
|
152
|
-
return files.map((f) => `file: ${f.file} (+${f.additions} -${f.deletions}) hunks:${f.hunks.length}`).join("\n");
|
|
153
|
-
}
|
|
154
|
-
if (privacy === "medium") {
|
|
155
|
-
return files.map(
|
|
156
|
-
(f) => `file: ${f.file}
|
|
157
|
-
` + f.hunks.map(
|
|
158
|
-
(h) => ` hunk ${h.hash} context:${h.functionContext || ""} +${h.added} -${h.removed}`
|
|
159
|
-
).join("\n")
|
|
160
|
-
).join("\n");
|
|
161
|
-
}
|
|
162
|
-
return files.map(
|
|
163
|
-
(f) => `file: ${f.file}
|
|
164
|
-
` + f.hunks.map(
|
|
165
|
-
(h) => `${h.header}
|
|
166
|
-
${h.lines.slice(0, 40).join("\n")}${h.lines.length > 40 ? "\n[truncated]" : ""}`
|
|
167
|
-
).join("\n")
|
|
168
|
-
).join("\n");
|
|
169
|
-
};
|
|
170
|
-
var buildGenerationMessages = (opts) => {
|
|
171
|
-
const { files, style, config, mode, desiredCommits } = opts;
|
|
172
|
-
const diff = summarizeDiffForPrompt(files, config.privacy);
|
|
173
|
-
const TYPE_MAP = {
|
|
174
|
-
feat: "A new feature or capability added for the user",
|
|
175
|
-
fix: "A bug fix resolving incorrect behavior",
|
|
176
|
-
chore: "Internal change with no user-facing impact",
|
|
177
|
-
docs: "Documentation-only changes",
|
|
178
|
-
refactor: "Code change that neither fixes a bug nor adds a feature",
|
|
179
|
-
test: "Adding or improving tests only",
|
|
180
|
-
ci: "Changes to CI configuration or scripts",
|
|
181
|
-
perf: "Performance improvement",
|
|
182
|
-
style: "Formatting or stylistic change (no logic)",
|
|
183
|
-
build: "Build system or dependency changes",
|
|
184
|
-
revert: "Revert a previous commit",
|
|
185
|
-
merge: "Merge branches (rare; only if truly a merge commit)",
|
|
186
|
-
security: "Security-related change or hardening",
|
|
187
|
-
release: "Version bump or release meta change"
|
|
188
|
-
};
|
|
189
|
-
const specLines = [];
|
|
190
|
-
specLines.push(
|
|
191
|
-
"Purpose: Generate high-quality Conventional Commit messages for the provided git diff."
|
|
192
|
-
);
|
|
193
|
-
specLines.push("Locale: en");
|
|
194
|
-
specLines.push(
|
|
195
|
-
'Output JSON Schema: { "commits": [ { "title": string, "body": string, "score": 0-100, "reasons": string[], "files"?: string[] } ], "meta": { "splitRecommended": boolean } }'
|
|
196
|
-
);
|
|
197
|
-
specLines.push("Primary Output Field: commits[ ].title");
|
|
198
|
-
specLines.push("Title Format (REQUIRED): <type>(<scope>): <subject>");
|
|
199
|
-
specLines.push(
|
|
200
|
-
"Title Length Guidance: Aim for <=50 chars ideal; absolute max 72 (do not exceed)."
|
|
201
|
-
);
|
|
202
|
-
specLines.push("Types (JSON mapping follows on next line)");
|
|
203
|
-
specLines.push("TypeMap: " + JSON.stringify(TYPE_MAP));
|
|
204
|
-
specLines.push(
|
|
205
|
-
"Scope Rules: ALWAYS include a concise lowercase kebab-case scope (derive from dominant directory, package, or feature); never omit."
|
|
206
|
-
);
|
|
207
|
-
specLines.push(
|
|
208
|
-
"Subject Rules: imperative mood, present tense, no leading capital unless proper noun, no trailing period."
|
|
209
|
-
);
|
|
210
|
-
specLines.push(
|
|
211
|
-
"Length Rule: Keep titles concise; prefer 50 or fewer chars; MUST be <=72 including type/scope."
|
|
212
|
-
);
|
|
213
|
-
specLines.push(
|
|
214
|
-
"Emoji Rule: " + (config.style === "gitmoji" || config.style === "gitmoji-pure" ? "OPTIONAL single leading gitmoji BEFORE the type only if confidently adds clarity; do not invent or stack; omit if unsure." : "Disallow all emojis and gitmoji codes; output must start directly with the type.")
|
|
215
|
-
);
|
|
216
|
-
specLines.push(
|
|
217
|
-
"Forbidden: breaking changes notation, exclamation mark after type unless truly semver-major (avoid unless diff clearly indicates)."
|
|
218
|
-
);
|
|
219
|
-
specLines.push("Fallback Type: use chore when no other type clearly fits.");
|
|
220
|
-
specLines.push("Consistency: prefer existing top prefixes: " + style.topPrefixes.join(", "));
|
|
221
|
-
specLines.push("Provide score (0-100) measuring clarity & specificity (higher is better).");
|
|
222
|
-
specLines.push(
|
|
223
|
-
"Provide reasons array citing concrete diff elements: filenames, functions, tests, metrics."
|
|
224
|
-
);
|
|
225
|
-
specLines.push(
|
|
226
|
-
'When mode is split, WHERE POSSIBLE add a "files" array per commit listing the most relevant changed file paths (1-6, minimize overlap across commits).'
|
|
227
|
-
);
|
|
228
|
-
specLines.push("Return ONLY the JSON object. No surrounding text or markdown.");
|
|
229
|
-
specLines.push("Do not add fields not listed in schema.");
|
|
230
|
-
specLines.push("Never fabricate content not present or implied by the diff.");
|
|
231
|
-
specLines.push(
|
|
232
|
-
"If mode is split and multiple logical changes exist, set meta.splitRecommended=true."
|
|
233
|
-
);
|
|
234
|
-
return [
|
|
235
|
-
{
|
|
236
|
-
role: "system",
|
|
237
|
-
content: specLines.join("\n")
|
|
238
|
-
},
|
|
239
|
-
{
|
|
240
|
-
role: "user",
|
|
241
|
-
content: `Mode: ${mode}
|
|
242
|
-
RequestedCommitCount: ${desiredCommits || (mode === "split" ? "2-6" : 1)}
|
|
243
|
-
StyleFingerprint: ${JSON.stringify(style)}
|
|
244
|
-
Diff:
|
|
245
|
-
${diff}
|
|
246
|
-
Generate commit candidates now.`
|
|
247
|
-
}
|
|
248
|
-
];
|
|
249
|
-
};
|
|
250
|
-
var buildRefineMessages = (opts) => {
|
|
251
|
-
const { originalPlan, index, instructions, config } = opts;
|
|
252
|
-
const target = originalPlan.commits[index];
|
|
253
|
-
const spec = [];
|
|
254
|
-
spec.push("Purpose: Refine a single Conventional Commit message while preserving intent.");
|
|
255
|
-
spec.push("Locale: en");
|
|
256
|
-
spec.push("Input: one existing commit JSON object.");
|
|
257
|
-
spec.push(
|
|
258
|
-
'Output JSON Schema: { "commits": [ { "title": string, "body": string, "score": 0-100, "reasons": string[] } ] }'
|
|
259
|
-
);
|
|
260
|
-
spec.push("Title Format (REQUIRED): <type>(<scope>): <subject> (<=72 chars)");
|
|
261
|
-
spec.push("Subject: imperative, present tense, no trailing period.");
|
|
262
|
-
spec.push(
|
|
263
|
-
"Emoji Rule: " + (config.style === "gitmoji" || config.style === "gitmoji-pure" ? "OPTIONAL single leading gitmoji BEFORE type if it adds clarity; omit if unsure." : "Disallow all emojis; start directly with the type.")
|
|
264
|
-
);
|
|
265
|
-
spec.push(
|
|
266
|
-
"Preserve semantic meaning; ensure a scope is present (infer one if missing); only improve clarity, brevity, conformity."
|
|
267
|
-
);
|
|
268
|
-
spec.push("If instructions request scope or emoji, incorporate only if justified by content.");
|
|
269
|
-
spec.push("Return ONLY JSON (commits array length=1).");
|
|
270
|
-
return [
|
|
271
|
-
{ role: "system", content: spec.join("\n") },
|
|
272
|
-
{
|
|
273
|
-
role: "user",
|
|
274
|
-
content: `Current commit object:
|
|
275
|
-
${JSON.stringify(target, null, 2)}
|
|
276
|
-
Instructions:
|
|
277
|
-
${instructions.join("\n") || "None"}
|
|
278
|
-
Refine now.`
|
|
279
|
-
}
|
|
280
|
-
];
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
// src/model/provider.ts
|
|
284
|
-
import { z } from "zod";
|
|
285
|
-
import { execa } from "execa";
|
|
286
|
-
var OpenCodeProvider = class {
|
|
287
|
-
constructor(model = "github-copilot/gpt-4.1") {
|
|
288
|
-
this.model = model;
|
|
289
|
-
}
|
|
290
|
-
name() {
|
|
291
|
-
return "opencode";
|
|
292
|
-
}
|
|
293
|
-
async chat(messages, _opts) {
|
|
294
|
-
const debug = process.env.AICC_DEBUG === "true";
|
|
295
|
-
const mockMode = process.env.AICC_DEBUG_PROVIDER === "mock";
|
|
296
|
-
const timeoutMs = parseInt(process.env.AICC_MODEL_TIMEOUT_MS || "120000", 10);
|
|
297
|
-
const eager = process.env.AICC_EAGER_PARSE !== "false";
|
|
298
|
-
const userAggregate = messages.map((m) => `${m.role.toUpperCase()}: ${m.content}`).join("\n\n");
|
|
299
|
-
const command = `Generate high-quality commit message candidates based on the staged git diff.`;
|
|
300
|
-
const fullPrompt = `${command}
|
|
301
|
-
|
|
302
|
-
Context:
|
|
303
|
-
${userAggregate}`;
|
|
304
|
-
if (mockMode) {
|
|
305
|
-
if (debug) console.error("[ai-cc][mock] Returning deterministic mock response");
|
|
306
|
-
return JSON.stringify({
|
|
307
|
-
commits: [
|
|
308
|
-
{
|
|
309
|
-
title: "chore: mock commit from provider",
|
|
310
|
-
body: "",
|
|
311
|
-
score: 80,
|
|
312
|
-
reasons: ["mock mode"]
|
|
313
|
-
}
|
|
314
|
-
],
|
|
315
|
-
meta: { splitRecommended: false }
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
const start = Date.now();
|
|
319
|
-
return await new Promise((resolve2, reject) => {
|
|
320
|
-
let resolved = false;
|
|
321
|
-
let acc = "";
|
|
322
|
-
const includeLogs = process.env.AICC_PRINT_LOGS === "true";
|
|
323
|
-
const args = ["run", fullPrompt, "--model", this.model];
|
|
324
|
-
if (includeLogs) args.push("--print-logs");
|
|
325
|
-
const subprocess = execa("opencode", args, {
|
|
326
|
-
timeout: timeoutMs,
|
|
327
|
-
input: ""
|
|
328
|
-
// immediately close stdin in case CLI waits for it
|
|
329
|
-
});
|
|
330
|
-
const finish = (value) => {
|
|
331
|
-
if (resolved) return;
|
|
332
|
-
resolved = true;
|
|
333
|
-
const elapsed = Date.now() - start;
|
|
334
|
-
if (debug) {
|
|
335
|
-
console.error(
|
|
336
|
-
`[ai-cc][provider] model=${this.model} elapsedMs=${elapsed} promptChars=${fullPrompt.length} bytesOut=${value.length}`
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
resolve2(value);
|
|
340
|
-
};
|
|
341
|
-
const tryEager = () => {
|
|
342
|
-
if (!eager) return;
|
|
343
|
-
const first = acc.indexOf("{");
|
|
344
|
-
const last = acc.lastIndexOf("}");
|
|
345
|
-
if (first !== -1 && last !== -1 && last > first) {
|
|
346
|
-
const candidate = acc.slice(first, last + 1).trim();
|
|
347
|
-
try {
|
|
348
|
-
JSON.parse(candidate);
|
|
349
|
-
if (debug) console.error("[ai-cc][provider] eager JSON detected, terminating process");
|
|
350
|
-
subprocess.kill("SIGTERM");
|
|
351
|
-
finish(candidate);
|
|
352
|
-
} catch {
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
subprocess.stdout?.on("data", (chunk) => {
|
|
357
|
-
const text = chunk.toString();
|
|
358
|
-
acc += text;
|
|
359
|
-
tryEager();
|
|
360
|
-
});
|
|
361
|
-
subprocess.stderr?.on("data", (chunk) => {
|
|
362
|
-
if (debug) console.error("[ai-cc][provider][stderr]", chunk.toString().trim());
|
|
363
|
-
});
|
|
364
|
-
subprocess.then(({ stdout }) => {
|
|
365
|
-
if (!resolved) finish(stdout);
|
|
366
|
-
}).catch((e) => {
|
|
367
|
-
if (resolved) return;
|
|
368
|
-
const elapsed = Date.now() - start;
|
|
369
|
-
if (e.timedOut) {
|
|
370
|
-
return reject(
|
|
371
|
-
new Error(`Model call timed out after ${timeoutMs}ms (elapsed=${elapsed}ms)`)
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
if (debug) console.error("[ai-cc][provider] failure", e.stderr || e.message);
|
|
375
|
-
reject(new Error(e.stderr || e.message || "opencode invocation failed"));
|
|
376
|
-
});
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
};
|
|
380
|
-
var CommitSchema = z.object({
|
|
381
|
-
title: z.string().min(5).max(150),
|
|
382
|
-
body: z.string().optional().default(""),
|
|
383
|
-
score: z.number().min(0).max(100),
|
|
384
|
-
reasons: z.array(z.string()).optional().default([]),
|
|
385
|
-
files: z.array(z.string()).optional().default([])
|
|
386
|
-
});
|
|
387
|
-
var PlanSchema = z.object({
|
|
388
|
-
commits: z.array(CommitSchema).min(1),
|
|
389
|
-
meta: z.object({
|
|
390
|
-
splitRecommended: z.boolean().optional()
|
|
391
|
-
}).optional()
|
|
392
|
-
});
|
|
393
|
-
var extractJSON = (raw) => {
|
|
394
|
-
const trimmed = raw.trim();
|
|
395
|
-
let jsonText = null;
|
|
396
|
-
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
397
|
-
jsonText = trimmed;
|
|
398
|
-
} else {
|
|
399
|
-
const match = raw.match(/\{[\s\S]*\}$/m);
|
|
400
|
-
if (match) jsonText = match[0];
|
|
401
|
-
}
|
|
402
|
-
if (!jsonText) throw new Error("No JSON object detected.");
|
|
403
|
-
let parsed;
|
|
404
|
-
try {
|
|
405
|
-
parsed = JSON.parse(jsonText);
|
|
406
|
-
} catch (e) {
|
|
407
|
-
throw new Error("Invalid JSON parse");
|
|
408
|
-
}
|
|
409
|
-
return PlanSchema.parse(parsed);
|
|
410
|
-
};
|
|
411
|
-
|
|
412
164
|
// src/plugins.ts
|
|
413
165
|
import { resolve } from "path";
|
|
414
166
|
async function loadPlugins(config, cwd = process.cwd()) {
|
|
@@ -447,283 +199,10 @@ async function runValidations(candidate, plugins, ctx) {
|
|
|
447
199
|
return errors;
|
|
448
200
|
}
|
|
449
201
|
|
|
450
|
-
// src/guardrails.ts
|
|
451
|
-
var SECRET_PATTERNS = [
|
|
452
|
-
/AWS_[A-Z0-9_]+/i,
|
|
453
|
-
/BEGIN RSA PRIVATE KEY/,
|
|
454
|
-
/-----BEGIN PRIVATE KEY-----/,
|
|
455
|
-
/ssh-rsa AAAA/
|
|
456
|
-
];
|
|
457
|
-
var CONVENTIONAL_RE = /^(?:([\p{Emoji}\p{So}\p{Sk}]+)\s+(feat|fix|chore|docs|refactor|test|ci|perf|style|build|revert|merge|security|release)(\(.+\))?:\s|([\p{Emoji}\p{So}\p{Sk}]+):\s.*|([\p{Emoji}\p{So}\p{Sk}]+):\s*$|(feat|fix|chore|docs|refactor|test|ci|perf|style|build|revert|merge|security|release)(\(.+\))?:\s)/u;
|
|
458
|
-
var sanitizeTitle = (title, allowEmoji) => {
|
|
459
|
-
let t = title.trim();
|
|
460
|
-
if (allowEmoji) {
|
|
461
|
-
const multi = t.match(/^((?:[\p{Emoji}\p{So}\p{Sk}]+)[\p{Emoji}\p{So}\p{Sk}\s]*)+/u);
|
|
462
|
-
if (multi) {
|
|
463
|
-
const first = Array.from(multi[0].trim())[0];
|
|
464
|
-
t = first + " " + t.slice(multi[0].length).trimStart();
|
|
465
|
-
}
|
|
466
|
-
} else {
|
|
467
|
-
t = t.replace(/^([\p{Emoji}\p{So}\p{Sk}\p{P}]+\s*)+/u, "").trimStart();
|
|
468
|
-
}
|
|
469
|
-
return t;
|
|
470
|
-
};
|
|
471
|
-
var normalizeConventionalTitle = (title) => {
|
|
472
|
-
let original = title.trim();
|
|
473
|
-
let leadingEmoji = "";
|
|
474
|
-
const emojiCluster = original.match(/^[\p{Emoji}\p{So}\p{Sk}]+/u);
|
|
475
|
-
if (emojiCluster) {
|
|
476
|
-
leadingEmoji = Array.from(emojiCluster[0])[0];
|
|
477
|
-
}
|
|
478
|
-
let t = original.replace(/^([\p{Emoji}\p{So}\p{Sk}\p{P}]+\s*)+/u, "").trim();
|
|
479
|
-
const m = t.match(/^(\w+)(\(.+\))?:\s+(.*)$/);
|
|
480
|
-
let result;
|
|
481
|
-
if (m) {
|
|
482
|
-
const type = m[1].toLowerCase();
|
|
483
|
-
const scope = m[2] || "";
|
|
484
|
-
let subject = m[3].trim();
|
|
485
|
-
subject = subject.replace(/\.$/, "");
|
|
486
|
-
subject = subject.charAt(0).toLowerCase() + subject.slice(1);
|
|
487
|
-
result = `${type}${scope}: ${subject}`;
|
|
488
|
-
} else if (!/^\w+\(.+\)?: /.test(t)) {
|
|
489
|
-
t = t.replace(/\.$/, "");
|
|
490
|
-
t = t.charAt(0).toLowerCase() + t.slice(1);
|
|
491
|
-
result = `chore: ${t}`;
|
|
492
|
-
} else {
|
|
493
|
-
result = t;
|
|
494
|
-
}
|
|
495
|
-
if (leadingEmoji) {
|
|
496
|
-
result = `${leadingEmoji} ${result}`;
|
|
497
|
-
}
|
|
498
|
-
return result;
|
|
499
|
-
};
|
|
500
|
-
var checkCandidate = (candidate) => {
|
|
501
|
-
const errs = [];
|
|
502
|
-
if (!CONVENTIONAL_RE.test(candidate.title)) {
|
|
503
|
-
errs.push("Not a valid conventional commit title.");
|
|
504
|
-
}
|
|
505
|
-
if (/^[A-Z]/.test(candidate.title)) {
|
|
506
|
-
}
|
|
507
|
-
const body = candidate.body || "";
|
|
508
|
-
for (const pat of SECRET_PATTERNS) {
|
|
509
|
-
if (pat.test(body)) {
|
|
510
|
-
errs.push("Potential secret detected.");
|
|
511
|
-
break;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
return errs;
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
// src/title-format.ts
|
|
518
|
-
var EMOJI_MAP = {
|
|
519
|
-
feat: "\u2728",
|
|
520
|
-
fix: "\u{1F41B}",
|
|
521
|
-
chore: "\u{1F9F9}",
|
|
522
|
-
docs: "\u{1F4DD}",
|
|
523
|
-
refactor: "\u267B\uFE0F",
|
|
524
|
-
test: "\u2705",
|
|
525
|
-
ci: "\u{1F916}",
|
|
526
|
-
perf: "\u26A1\uFE0F",
|
|
527
|
-
style: "\u{1F3A8}",
|
|
528
|
-
build: "\u{1F3D7}\uFE0F",
|
|
529
|
-
revert: "\u23EA",
|
|
530
|
-
merge: "\u{1F500}",
|
|
531
|
-
security: "\u{1F512}",
|
|
532
|
-
release: "\u{1F3F7}\uFE0F"
|
|
533
|
-
};
|
|
534
|
-
var EMOJI_TYPE_RE = /^([\p{Emoji}\p{So}\p{Sk}])\s+(\w+)(\(.+\))?:\s+(.*)$/u;
|
|
535
|
-
var TYPE_RE = /^(\w+)(\(.+\))?:\s+(.*)$/;
|
|
536
|
-
var formatCommitTitle = (raw, opts) => {
|
|
537
|
-
const { allowGitmoji, mode = "standard" } = opts;
|
|
538
|
-
let norm = normalizeConventionalTitle(sanitizeTitle(raw, allowGitmoji));
|
|
539
|
-
if (!allowGitmoji || mode !== "gitmoji" && mode !== "gitmoji-pure") {
|
|
540
|
-
return norm;
|
|
541
|
-
}
|
|
542
|
-
if (mode === "gitmoji-pure") {
|
|
543
|
-
let m2 = norm.match(EMOJI_TYPE_RE);
|
|
544
|
-
if (m2) {
|
|
545
|
-
const emoji = m2[1];
|
|
546
|
-
const subject = m2[4];
|
|
547
|
-
norm = `${emoji}: ${subject}`;
|
|
548
|
-
} else if (m2 = norm.match(TYPE_RE)) {
|
|
549
|
-
const type = m2[1];
|
|
550
|
-
const subject = m2[3];
|
|
551
|
-
const em = EMOJI_MAP[type] || "\u{1F527}";
|
|
552
|
-
norm = `${em}: ${subject}`;
|
|
553
|
-
} else if (!/^([\p{Emoji}\p{So}\p{Sk}])+:/u.test(norm)) {
|
|
554
|
-
norm = `\u{1F527}: ${norm}`;
|
|
555
|
-
}
|
|
556
|
-
return norm;
|
|
557
|
-
}
|
|
558
|
-
let m = norm.match(EMOJI_TYPE_RE);
|
|
559
|
-
if (m) {
|
|
560
|
-
return norm;
|
|
561
|
-
}
|
|
562
|
-
if (m = norm.match(TYPE_RE)) {
|
|
563
|
-
const type = m[1];
|
|
564
|
-
const scope = m[2] || "";
|
|
565
|
-
const subject = m[3];
|
|
566
|
-
const em = EMOJI_MAP[type] || "\u{1F527}";
|
|
567
|
-
norm = `${em} ${type}${scope}: ${subject}`;
|
|
568
|
-
} else if (!/^([\p{Emoji}\p{So}\p{Sk}])+\s+\w+.*:/u.test(norm)) {
|
|
569
|
-
norm = `\u{1F527} chore: ${norm}`;
|
|
570
|
-
}
|
|
571
|
-
return norm;
|
|
572
|
-
};
|
|
573
|
-
|
|
574
202
|
// src/workflow/generate.ts
|
|
575
203
|
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
576
204
|
import { join } from "path";
|
|
577
205
|
import inquirer from "inquirer";
|
|
578
|
-
|
|
579
|
-
// src/workflow/ui.ts
|
|
580
|
-
import chalk from "chalk";
|
|
581
|
-
function animateHeaderBase(text = "ai-conventional-commit", modelSegment) {
|
|
582
|
-
const mainText = text;
|
|
583
|
-
const modelSeg = modelSegment ? ` (using ${modelSegment})` : "";
|
|
584
|
-
if (!process.stdout.isTTY || process.env.AICC_NO_ANIMATION) {
|
|
585
|
-
if (modelSeg) console.log("\n\u250C " + chalk.bold(mainText) + chalk.dim(modelSeg));
|
|
586
|
-
else console.log("\n\u250C " + chalk.bold(mainText));
|
|
587
|
-
return Promise.resolve();
|
|
588
|
-
}
|
|
589
|
-
const palette = [
|
|
590
|
-
"#3a0d6d",
|
|
591
|
-
"#5a1ea3",
|
|
592
|
-
"#7a32d6",
|
|
593
|
-
"#9a4dff",
|
|
594
|
-
"#b267ff",
|
|
595
|
-
"#c37dff",
|
|
596
|
-
"#b267ff",
|
|
597
|
-
"#9a4dff",
|
|
598
|
-
"#7a32d6",
|
|
599
|
-
"#5a1ea3"
|
|
600
|
-
];
|
|
601
|
-
process.stdout.write("\n");
|
|
602
|
-
return palette.reduce(async (p, color) => {
|
|
603
|
-
await p;
|
|
604
|
-
const frame = chalk.bold.hex(color)(mainText);
|
|
605
|
-
if (modelSeg) process.stdout.write("\r\u250C " + frame + chalk.dim(modelSeg));
|
|
606
|
-
else process.stdout.write("\r\u250C " + frame);
|
|
607
|
-
await new Promise((r) => setTimeout(r, 60));
|
|
608
|
-
}, Promise.resolve()).then(() => process.stdout.write("\n"));
|
|
609
|
-
}
|
|
610
|
-
function borderLine(content) {
|
|
611
|
-
if (!content) console.log("\u2502");
|
|
612
|
-
else console.log("\u2502 " + content);
|
|
613
|
-
}
|
|
614
|
-
function sectionTitle(label) {
|
|
615
|
-
console.log("\u2299 " + chalk.bold(label));
|
|
616
|
-
}
|
|
617
|
-
function abortMessage() {
|
|
618
|
-
console.log("\u2514 \u{1F645}\u200D\u2640\uFE0F No commit created.");
|
|
619
|
-
console.log();
|
|
620
|
-
}
|
|
621
|
-
function finalSuccess(opts) {
|
|
622
|
-
const elapsedMs = Date.now() - opts.startedAt;
|
|
623
|
-
const seconds = elapsedMs / 1e3;
|
|
624
|
-
const dur = seconds >= 0.1 ? seconds.toFixed(1) + "s" : elapsedMs + "ms";
|
|
625
|
-
const plural = opts.count !== 1;
|
|
626
|
-
if (plural) console.log(`\u2514 \u2728 ${opts.count} commits created in ${dur}.`);
|
|
627
|
-
else console.log(`\u2514 \u2728 commit created in ${dur}.`);
|
|
628
|
-
console.log();
|
|
629
|
-
}
|
|
630
|
-
function createPhasedSpinner(oraLib) {
|
|
631
|
-
const useAnim = process.stdout.isTTY && !process.env.AICC_NO_ANIMATION && !process.env.AICC_NO_SPINNER_ANIM;
|
|
632
|
-
const palette = [
|
|
633
|
-
"#3a0d6d",
|
|
634
|
-
"#5a1ea3",
|
|
635
|
-
"#7a32d6",
|
|
636
|
-
"#9a4dff",
|
|
637
|
-
"#b267ff",
|
|
638
|
-
"#c37dff",
|
|
639
|
-
"#b267ff",
|
|
640
|
-
"#9a4dff",
|
|
641
|
-
"#7a32d6",
|
|
642
|
-
"#5a1ea3"
|
|
643
|
-
];
|
|
644
|
-
let label = "Starting";
|
|
645
|
-
let i = 0;
|
|
646
|
-
const spinner = oraLib({ text: chalk.bold(label), spinner: "dots" }).start();
|
|
647
|
-
let interval = null;
|
|
648
|
-
function frame() {
|
|
649
|
-
if (!useAnim) return;
|
|
650
|
-
spinner.text = chalk.bold.hex(palette[i])(label);
|
|
651
|
-
i = (i + 1) % palette.length;
|
|
652
|
-
}
|
|
653
|
-
if (useAnim) {
|
|
654
|
-
frame();
|
|
655
|
-
interval = setInterval(frame, 80);
|
|
656
|
-
}
|
|
657
|
-
function setLabel(next) {
|
|
658
|
-
label = next;
|
|
659
|
-
if (useAnim) {
|
|
660
|
-
i = 0;
|
|
661
|
-
frame();
|
|
662
|
-
} else {
|
|
663
|
-
spinner.text = chalk.bold(label);
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
function stopAnim() {
|
|
667
|
-
if (interval) {
|
|
668
|
-
clearInterval(interval);
|
|
669
|
-
interval = null;
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
return {
|
|
673
|
-
spinner,
|
|
674
|
-
async step(l, fn) {
|
|
675
|
-
setLabel(l);
|
|
676
|
-
try {
|
|
677
|
-
return await fn();
|
|
678
|
-
} catch (e) {
|
|
679
|
-
stopAnim();
|
|
680
|
-
const msg = `${l} failed: ${e?.message || e}`.replace(/^\s+/, "");
|
|
681
|
-
spinner.fail(msg);
|
|
682
|
-
throw e;
|
|
683
|
-
}
|
|
684
|
-
},
|
|
685
|
-
phase(l) {
|
|
686
|
-
setLabel(l);
|
|
687
|
-
},
|
|
688
|
-
stop() {
|
|
689
|
-
stopAnim();
|
|
690
|
-
spinner.stop();
|
|
691
|
-
}
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
function renderCommitBlock(opts) {
|
|
695
|
-
const dim = (s) => chalk.dim(s);
|
|
696
|
-
const white = (s) => chalk.white(s);
|
|
697
|
-
const msgColor = opts.messageLabelColor || dim;
|
|
698
|
-
const descColor = opts.descriptionLabelColor || dim;
|
|
699
|
-
const titleColor = opts.titleColor || white;
|
|
700
|
-
const bodyFirst = opts.bodyFirstLineColor || white;
|
|
701
|
-
const bodyRest = opts.bodyLineColor || white;
|
|
702
|
-
if (opts.fancy) {
|
|
703
|
-
const heading = opts.heading ? chalk.hex("#9a4dff").bold(opts.heading) : void 0;
|
|
704
|
-
if (heading) borderLine(heading);
|
|
705
|
-
borderLine(msgColor("Title:") + " " + titleColor(`${opts.indexPrefix || ""}${opts.title}`));
|
|
706
|
-
} else {
|
|
707
|
-
if (opts.heading) borderLine(chalk.bold(opts.heading));
|
|
708
|
-
if (!opts.hideMessageLabel)
|
|
709
|
-
borderLine(msgColor("Message:") + " " + titleColor(`${opts.indexPrefix || ""}${opts.title}`));
|
|
710
|
-
else
|
|
711
|
-
borderLine(msgColor("Title:") + " " + titleColor(`${opts.indexPrefix || ""}${opts.title}`));
|
|
712
|
-
}
|
|
713
|
-
borderLine();
|
|
714
|
-
if (opts.body) {
|
|
715
|
-
const lines = opts.body.split("\n");
|
|
716
|
-
lines.forEach((line, i) => {
|
|
717
|
-
if (line.trim().length === 0) borderLine();
|
|
718
|
-
else if (i === 0) {
|
|
719
|
-
borderLine(descColor("Description:"));
|
|
720
|
-
borderLine(bodyFirst(line));
|
|
721
|
-
} else borderLine(bodyRest(line));
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// src/workflow/generate.ts
|
|
727
206
|
async function runGenerate(config) {
|
|
728
207
|
const startedAt = Date.now();
|
|
729
208
|
if (!await ensureStagedChanges()) {
|
|
@@ -747,7 +226,7 @@ async function runGenerate(config) {
|
|
|
747
226
|
const deltas = files.map((f) => (f.additions || 0) + (f.deletions || 0));
|
|
748
227
|
const maxDelta = Math.max(...deltas, 1);
|
|
749
228
|
borderLine(
|
|
750
|
-
|
|
229
|
+
chalk.dim(`Detected ${files.length} staged ${files.length === 1 ? "file" : "files"}:`)
|
|
751
230
|
);
|
|
752
231
|
let totalAdd = 0;
|
|
753
232
|
let totalDel = 0;
|
|
@@ -764,13 +243,13 @@ async function runGenerate(config) {
|
|
|
764
243
|
const barLen = Math.max(1, Math.round(delta / maxDelta * BAR_WIDTH));
|
|
765
244
|
const addPortion = Math.min(barLen, Math.round(barLen * (add / (delta || 1))));
|
|
766
245
|
const delPortion = barLen - addPortion;
|
|
767
|
-
const bar =
|
|
768
|
-
const counts =
|
|
246
|
+
const bar = chalk.green("+".repeat(addPortion)) + chalk.red("-".repeat(delPortion));
|
|
247
|
+
const counts = chalk.green("+" + add) + " " + chalk.red("-" + del);
|
|
769
248
|
const name = f.file.length > maxName ? f.file.slice(0, maxName - 1) + "\u2026" : f.file;
|
|
770
249
|
borderLine(name.padEnd(maxName) + " | " + counts.padEnd(12) + " " + bar);
|
|
771
250
|
});
|
|
772
251
|
borderLine(
|
|
773
|
-
|
|
252
|
+
chalk.dim(
|
|
774
253
|
`${files.length} file${files.length === 1 ? "" : "s"} changed, ${totalAdd} insertion${totalAdd === 1 ? "" : "s"}(+), ${totalDel} deletion${totalDel === 1 ? "" : "s"}(-)`
|
|
775
254
|
)
|
|
776
255
|
);
|
|
@@ -823,8 +302,8 @@ async function runGenerate(config) {
|
|
|
823
302
|
const errors = [...pluginErrors, ...guardErrors];
|
|
824
303
|
if (errors.length) {
|
|
825
304
|
borderLine();
|
|
826
|
-
console.log("\u2299 " +
|
|
827
|
-
const errorLines = ["Validation issues:", ...errors.map((e) =>
|
|
305
|
+
console.log("\u2299 " + chalk.bold("Checks"));
|
|
306
|
+
const errorLines = ["Validation issues:", ...errors.map((e) => chalk.red("\u2022 " + e))];
|
|
828
307
|
errorLines.forEach((l) => borderLine(l));
|
|
829
308
|
}
|
|
830
309
|
borderLine();
|
|
@@ -861,7 +340,7 @@ async function selectYesNo() {
|
|
|
861
340
|
}
|
|
862
341
|
|
|
863
342
|
// src/workflow/split.ts
|
|
864
|
-
import
|
|
343
|
+
import chalk2 from "chalk";
|
|
865
344
|
import ora2 from "ora";
|
|
866
345
|
|
|
867
346
|
// src/cluster.ts
|
|
@@ -930,7 +409,7 @@ async function runSplit(config, desired) {
|
|
|
930
409
|
const deltas = files.map((f) => (f.additions || 0) + (f.deletions || 0));
|
|
931
410
|
const maxDelta = Math.max(...deltas, 1);
|
|
932
411
|
borderLine(
|
|
933
|
-
|
|
412
|
+
chalk2.dim(`Detected ${files.length} staged ${files.length === 1 ? "file" : "files"}:`)
|
|
934
413
|
);
|
|
935
414
|
let totalAdd = 0;
|
|
936
415
|
let totalDel = 0;
|
|
@@ -947,13 +426,13 @@ async function runSplit(config, desired) {
|
|
|
947
426
|
const barLen = Math.max(1, Math.round(delta / maxDelta * BAR_WIDTH));
|
|
948
427
|
const addPortion = Math.min(barLen, Math.round(barLen * (add / (delta || 1))));
|
|
949
428
|
const delPortion = barLen - addPortion;
|
|
950
|
-
const bar =
|
|
951
|
-
const counts =
|
|
429
|
+
const bar = chalk2.green("+".repeat(addPortion)) + chalk2.red("-".repeat(delPortion));
|
|
430
|
+
const counts = chalk2.green("+" + add) + " " + chalk2.red("-" + del);
|
|
952
431
|
const name = f.file.length > maxName ? f.file.slice(0, maxName - 1) + "\u2026" : f.file;
|
|
953
432
|
borderLine(name.padEnd(maxName) + " | " + counts.padEnd(12) + " " + bar);
|
|
954
433
|
});
|
|
955
434
|
borderLine(
|
|
956
|
-
|
|
435
|
+
chalk2.dim(
|
|
957
436
|
`${files.length} file${files.length === 1 ? "" : "s"} changed, ${totalAdd} insertion${totalAdd === 1 ? "" : "s"}(+), ${totalDel} deletion${totalDel === 1 ? "" : "s"}(-)`
|
|
958
437
|
)
|
|
959
438
|
);
|
|
@@ -1065,7 +544,7 @@ function saveSession2(data) {
|
|
|
1065
544
|
}
|
|
1066
545
|
|
|
1067
546
|
// src/workflow/refine.ts
|
|
1068
|
-
import
|
|
547
|
+
import chalk3 from "chalk";
|
|
1069
548
|
import ora3 from "ora";
|
|
1070
549
|
import inquirer3 from "inquirer";
|
|
1071
550
|
import { readFileSync, existsSync as existsSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
@@ -1118,11 +597,11 @@ async function runRefine(config, options) {
|
|
|
1118
597
|
}
|
|
1119
598
|
sectionTitle("Original");
|
|
1120
599
|
const original = plan.commits[index];
|
|
1121
|
-
const originalLines = [
|
|
600
|
+
const originalLines = [chalk3.yellow(original.title)];
|
|
1122
601
|
if (original.body) {
|
|
1123
602
|
original.body.split("\n").forEach((line) => {
|
|
1124
603
|
if (line.trim().length === 0) originalLines.push("");
|
|
1125
|
-
else originalLines.push(
|
|
604
|
+
else originalLines.push(chalk3.white(line));
|
|
1126
605
|
});
|
|
1127
606
|
}
|
|
1128
607
|
originalLines.forEach((l) => l.trim().length === 0 ? borderLine() : borderLine(l));
|
|
@@ -1161,7 +640,7 @@ async function runRefine(config, options) {
|
|
|
1161
640
|
renderCommitBlock({
|
|
1162
641
|
title: refinedPlan.commits[0].title,
|
|
1163
642
|
body: refinedPlan.commits[0].body,
|
|
1164
|
-
titleColor: (s) =>
|
|
643
|
+
titleColor: (s) => chalk3.yellow(s)
|
|
1165
644
|
});
|
|
1166
645
|
borderLine();
|
|
1167
646
|
const { ok } = await inquirer3.prompt([
|
|
@@ -1190,7 +669,7 @@ async function runRefine(config, options) {
|
|
|
1190
669
|
// package.json
|
|
1191
670
|
var package_default = {
|
|
1192
671
|
name: "@kud/ai-conventional-commit-cli",
|
|
1193
|
-
version: "0.
|
|
672
|
+
version: "0.12.0",
|
|
1194
673
|
type: "module",
|
|
1195
674
|
description: "Opinionated, style-aware AI assistant for crafting and splitting git commits (opencode-based, provider-agnostic).",
|
|
1196
675
|
bin: {
|
|
@@ -1260,7 +739,7 @@ var package_default = {
|
|
|
1260
739
|
};
|
|
1261
740
|
|
|
1262
741
|
// src/index.ts
|
|
1263
|
-
import { execa
|
|
742
|
+
import { execa } from "execa";
|
|
1264
743
|
import inquirer4 from "inquirer";
|
|
1265
744
|
var pkgVersion = package_default.version || "0.0.0";
|
|
1266
745
|
var RootCommand = class extends Command {
|
|
@@ -1440,7 +919,7 @@ var ModelsCommand = class extends Command {
|
|
|
1440
919
|
return;
|
|
1441
920
|
}
|
|
1442
921
|
try {
|
|
1443
|
-
const { stdout } = await
|
|
922
|
+
const { stdout } = await execa("opencode", ["models"]).catch(async (err) => {
|
|
1444
923
|
if (err.shortMessage && /ENOENT/.test(err.shortMessage)) {
|
|
1445
924
|
this.context.stderr.write(
|
|
1446
925
|
"opencode CLI not found in PATH. Install it from https://github.com/opencodejs/opencode or ensure the binary is available.\n"
|
|
@@ -1591,6 +1070,58 @@ var ConfigSetCommand = class extends Command {
|
|
|
1591
1070
|
`);
|
|
1592
1071
|
}
|
|
1593
1072
|
};
|
|
1073
|
+
var RewordCommand = class extends Command {
|
|
1074
|
+
static paths = [[`reword`]];
|
|
1075
|
+
static usage = Command.Usage({
|
|
1076
|
+
description: "AI-assisted reword of an existing commit (by hash).",
|
|
1077
|
+
details: "Generate an improved Conventional Commit message for the given commit hash. If the hash is HEAD the commit is amended; otherwise rebase instructions are shown. If no hash is provided, an interactive picker of recent commits appears.",
|
|
1078
|
+
examples: [
|
|
1079
|
+
["Interactive pick", "ai-conventional-commit reword"],
|
|
1080
|
+
["Reword HEAD", "ai-conventional-commit reword HEAD"],
|
|
1081
|
+
["Reword older commit", "ai-conventional-commit reword d30fd1b"]
|
|
1082
|
+
]
|
|
1083
|
+
});
|
|
1084
|
+
hash = Option.String({ required: false });
|
|
1085
|
+
async execute() {
|
|
1086
|
+
const { runReword } = await import("./reword-FE5N4MGV.js");
|
|
1087
|
+
const config = await loadConfig();
|
|
1088
|
+
let target = this.hash;
|
|
1089
|
+
if (!target) {
|
|
1090
|
+
try {
|
|
1091
|
+
const { simpleGit: simpleGit2 } = await import("simple-git");
|
|
1092
|
+
const git2 = simpleGit2();
|
|
1093
|
+
const log = await git2.log({ maxCount: 20 });
|
|
1094
|
+
if (!log.all.length) {
|
|
1095
|
+
this.context.stderr.write("No commits available to select.\n");
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
const choices = log.all.map((c) => ({
|
|
1099
|
+
name: `${c.hash.slice(0, 7)} ${c.message.split("\n")[0]}`.slice(0, 80),
|
|
1100
|
+
value: c.hash
|
|
1101
|
+
}));
|
|
1102
|
+
choices.push({ name: "Cancel", value: "__CANCEL__" });
|
|
1103
|
+
const { picked } = await inquirer4.prompt([
|
|
1104
|
+
{
|
|
1105
|
+
type: "list",
|
|
1106
|
+
name: "picked",
|
|
1107
|
+
message: "Select a commit to reword",
|
|
1108
|
+
choices,
|
|
1109
|
+
pageSize: Math.min(choices.length, 15)
|
|
1110
|
+
}
|
|
1111
|
+
]);
|
|
1112
|
+
if (picked === "__CANCEL__") {
|
|
1113
|
+
this.context.stdout.write("Aborted.\n");
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
target = picked;
|
|
1117
|
+
} catch (e) {
|
|
1118
|
+
this.context.stderr.write("Failed to list commits: " + (e?.message || e) + "\n");
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
await runReword(config, target);
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1594
1125
|
var VersionCommand = class extends Command {
|
|
1595
1126
|
static paths = [[`--version`], [`-V`]];
|
|
1596
1127
|
async execute() {
|
|
@@ -1611,6 +1142,7 @@ cli.register(ModelsCommand);
|
|
|
1611
1142
|
cli.register(ConfigShowCommand);
|
|
1612
1143
|
cli.register(ConfigGetCommand);
|
|
1613
1144
|
cli.register(ConfigSetCommand);
|
|
1145
|
+
cli.register(RewordCommand);
|
|
1614
1146
|
cli.register(VersionCommand);
|
|
1615
1147
|
cli.runExit(process.argv.slice(2), {
|
|
1616
1148
|
stdin: process.stdin,
|