@kud/ai-conventional-commit-cli 0.11.0 → 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/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 chalk2 from "chalk";
25
+ import chalk from "chalk";
11
26
  import ora from "ora";
12
27
 
13
28
  // src/git.ts
@@ -146,265 +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: <type>(<optional-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("Scope Rules: optional; if present, lowercase kebab-case; omit when unclear.");
205
- specLines.push(
206
- "Subject Rules: imperative mood, present tense, no leading capital unless proper noun, no trailing period."
207
- );
208
- specLines.push(
209
- "Length Rule: Keep titles concise; prefer 50 or fewer chars; MUST be <=72 including type/scope."
210
- );
211
- specLines.push(
212
- "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.")
213
- );
214
- specLines.push(
215
- "Forbidden: breaking changes notation, exclamation mark after type unless truly semver-major (avoid unless diff clearly indicates)."
216
- );
217
- specLines.push("Fallback Type: use chore when no other type clearly fits.");
218
- specLines.push("Consistency: prefer existing top prefixes: " + style.topPrefixes.join(", "));
219
- specLines.push("Provide score (0-100) measuring clarity & specificity (higher is better).");
220
- specLines.push(
221
- "Provide reasons array citing concrete diff elements: filenames, functions, tests, metrics."
222
- );
223
- specLines.push(
224
- '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).'
225
- );
226
- specLines.push("Return ONLY the JSON object. No surrounding text or markdown.");
227
- specLines.push("Do not add fields not listed in schema.");
228
- specLines.push("Never fabricate content not present or implied by the diff.");
229
- specLines.push(
230
- "If mode is split and multiple logical changes exist, set meta.splitRecommended=true."
231
- );
232
- return [
233
- {
234
- role: "system",
235
- content: specLines.join("\n")
236
- },
237
- {
238
- role: "user",
239
- content: `Mode: ${mode}
240
- RequestedCommitCount: ${desiredCommits || (mode === "split" ? "2-6" : 1)}
241
- StyleFingerprint: ${JSON.stringify(style)}
242
- Diff:
243
- ${diff}
244
- Generate commit candidates now.`
245
- }
246
- ];
247
- };
248
- var buildRefineMessages = (opts) => {
249
- const { originalPlan, index, instructions, config } = opts;
250
- const target = originalPlan.commits[index];
251
- const spec = [];
252
- spec.push("Purpose: Refine a single Conventional Commit message while preserving intent.");
253
- spec.push("Locale: en");
254
- spec.push("Input: one existing commit JSON object.");
255
- spec.push(
256
- 'Output JSON Schema: { "commits": [ { "title": string, "body": string, "score": 0-100, "reasons": string[] } ] }'
257
- );
258
- spec.push("Title Format: <type>(<optional-scope>): <subject> (<=72 chars)");
259
- spec.push("Subject: imperative, present tense, no trailing period.");
260
- spec.push(
261
- "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.")
262
- );
263
- spec.push("Preserve semantic meaning; only improve clarity, scope, brevity, conformity.");
264
- spec.push("If instructions request scope or emoji, incorporate only if justified by content.");
265
- spec.push("Return ONLY JSON (commits array length=1).");
266
- return [
267
- { role: "system", content: spec.join("\n") },
268
- {
269
- role: "user",
270
- content: `Current commit object:
271
- ${JSON.stringify(target, null, 2)}
272
- Instructions:
273
- ${instructions.join("\n") || "None"}
274
- Refine now.`
275
- }
276
- ];
277
- };
278
-
279
- // src/model/provider.ts
280
- import { z } from "zod";
281
- import { execa } from "execa";
282
- var OpenCodeProvider = class {
283
- constructor(model = "github-copilot/gpt-4.1") {
284
- this.model = model;
285
- }
286
- name() {
287
- return "opencode";
288
- }
289
- async chat(messages, _opts) {
290
- const debug = process.env.AICC_DEBUG === "true";
291
- const mockMode = process.env.AICC_DEBUG_PROVIDER === "mock";
292
- const timeoutMs = parseInt(process.env.AICC_MODEL_TIMEOUT_MS || "120000", 10);
293
- const eager = process.env.AICC_EAGER_PARSE !== "false";
294
- const userAggregate = messages.map((m) => `${m.role.toUpperCase()}: ${m.content}`).join("\n\n");
295
- const command = `Generate high-quality commit message candidates based on the staged git diff.`;
296
- const fullPrompt = `${command}
297
-
298
- Context:
299
- ${userAggregate}`;
300
- if (mockMode) {
301
- if (debug) console.error("[ai-cc][mock] Returning deterministic mock response");
302
- return JSON.stringify({
303
- commits: [
304
- {
305
- title: "chore: mock commit from provider",
306
- body: "",
307
- score: 80,
308
- reasons: ["mock mode"]
309
- }
310
- ],
311
- meta: { splitRecommended: false }
312
- });
313
- }
314
- const start = Date.now();
315
- return await new Promise((resolve2, reject) => {
316
- let resolved = false;
317
- let acc = "";
318
- const includeLogs = process.env.AICC_PRINT_LOGS === "true";
319
- const args = ["run", fullPrompt, "--model", this.model];
320
- if (includeLogs) args.push("--print-logs");
321
- const subprocess = execa("opencode", args, {
322
- timeout: timeoutMs,
323
- input: ""
324
- // immediately close stdin in case CLI waits for it
325
- });
326
- const finish = (value) => {
327
- if (resolved) return;
328
- resolved = true;
329
- const elapsed = Date.now() - start;
330
- if (debug) {
331
- console.error(
332
- `[ai-cc][provider] model=${this.model} elapsedMs=${elapsed} promptChars=${fullPrompt.length} bytesOut=${value.length}`
333
- );
334
- }
335
- resolve2(value);
336
- };
337
- const tryEager = () => {
338
- if (!eager) return;
339
- const first = acc.indexOf("{");
340
- const last = acc.lastIndexOf("}");
341
- if (first !== -1 && last !== -1 && last > first) {
342
- const candidate = acc.slice(first, last + 1).trim();
343
- try {
344
- JSON.parse(candidate);
345
- if (debug) console.error("[ai-cc][provider] eager JSON detected, terminating process");
346
- subprocess.kill("SIGTERM");
347
- finish(candidate);
348
- } catch {
349
- }
350
- }
351
- };
352
- subprocess.stdout?.on("data", (chunk) => {
353
- const text = chunk.toString();
354
- acc += text;
355
- tryEager();
356
- });
357
- subprocess.stderr?.on("data", (chunk) => {
358
- if (debug) console.error("[ai-cc][provider][stderr]", chunk.toString().trim());
359
- });
360
- subprocess.then(({ stdout }) => {
361
- if (!resolved) finish(stdout);
362
- }).catch((e) => {
363
- if (resolved) return;
364
- const elapsed = Date.now() - start;
365
- if (e.timedOut) {
366
- return reject(
367
- new Error(`Model call timed out after ${timeoutMs}ms (elapsed=${elapsed}ms)`)
368
- );
369
- }
370
- if (debug) console.error("[ai-cc][provider] failure", e.stderr || e.message);
371
- reject(new Error(e.stderr || e.message || "opencode invocation failed"));
372
- });
373
- });
374
- }
375
- };
376
- var CommitSchema = z.object({
377
- title: z.string().min(5).max(150),
378
- body: z.string().optional().default(""),
379
- score: z.number().min(0).max(100),
380
- reasons: z.array(z.string()).optional().default([]),
381
- files: z.array(z.string()).optional().default([])
382
- });
383
- var PlanSchema = z.object({
384
- commits: z.array(CommitSchema).min(1),
385
- meta: z.object({
386
- splitRecommended: z.boolean().optional()
387
- }).optional()
388
- });
389
- var extractJSON = (raw) => {
390
- const trimmed = raw.trim();
391
- let jsonText = null;
392
- if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
393
- jsonText = trimmed;
394
- } else {
395
- const match = raw.match(/\{[\s\S]*\}$/m);
396
- if (match) jsonText = match[0];
397
- }
398
- if (!jsonText) throw new Error("No JSON object detected.");
399
- let parsed;
400
- try {
401
- parsed = JSON.parse(jsonText);
402
- } catch (e) {
403
- throw new Error("Invalid JSON parse");
404
- }
405
- return PlanSchema.parse(parsed);
406
- };
407
-
408
164
  // src/plugins.ts
409
165
  import { resolve } from "path";
410
166
  async function loadPlugins(config, cwd = process.cwd()) {
@@ -443,283 +199,10 @@ async function runValidations(candidate, plugins, ctx) {
443
199
  return errors;
444
200
  }
445
201
 
446
- // src/guardrails.ts
447
- var SECRET_PATTERNS = [
448
- /AWS_[A-Z0-9_]+/i,
449
- /BEGIN RSA PRIVATE KEY/,
450
- /-----BEGIN PRIVATE KEY-----/,
451
- /ssh-rsa AAAA/
452
- ];
453
- 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;
454
- var sanitizeTitle = (title, allowEmoji) => {
455
- let t = title.trim();
456
- if (allowEmoji) {
457
- const multi = t.match(/^((?:[\p{Emoji}\p{So}\p{Sk}]+)[\p{Emoji}\p{So}\p{Sk}\s]*)+/u);
458
- if (multi) {
459
- const first = Array.from(multi[0].trim())[0];
460
- t = first + " " + t.slice(multi[0].length).trimStart();
461
- }
462
- } else {
463
- t = t.replace(/^([\p{Emoji}\p{So}\p{Sk}\p{P}]+\s*)+/u, "").trimStart();
464
- }
465
- return t;
466
- };
467
- var normalizeConventionalTitle = (title) => {
468
- let original = title.trim();
469
- let leadingEmoji = "";
470
- const emojiCluster = original.match(/^[\p{Emoji}\p{So}\p{Sk}]+/u);
471
- if (emojiCluster) {
472
- leadingEmoji = Array.from(emojiCluster[0])[0];
473
- }
474
- let t = original.replace(/^([\p{Emoji}\p{So}\p{Sk}\p{P}]+\s*)+/u, "").trim();
475
- const m = t.match(/^(\w+)(\(.+\))?:\s+(.*)$/);
476
- let result;
477
- if (m) {
478
- const type = m[1].toLowerCase();
479
- const scope = m[2] || "";
480
- let subject = m[3].trim();
481
- subject = subject.replace(/\.$/, "");
482
- subject = subject.charAt(0).toLowerCase() + subject.slice(1);
483
- result = `${type}${scope}: ${subject}`;
484
- } else if (!/^\w+\(.+\)?: /.test(t)) {
485
- t = t.replace(/\.$/, "");
486
- t = t.charAt(0).toLowerCase() + t.slice(1);
487
- result = `chore: ${t}`;
488
- } else {
489
- result = t;
490
- }
491
- if (leadingEmoji) {
492
- result = `${leadingEmoji} ${result}`;
493
- }
494
- return result;
495
- };
496
- var checkCandidate = (candidate) => {
497
- const errs = [];
498
- if (!CONVENTIONAL_RE.test(candidate.title)) {
499
- errs.push("Not a valid conventional commit title.");
500
- }
501
- if (/^[A-Z]/.test(candidate.title)) {
502
- }
503
- const body = candidate.body || "";
504
- for (const pat of SECRET_PATTERNS) {
505
- if (pat.test(body)) {
506
- errs.push("Potential secret detected.");
507
- break;
508
- }
509
- }
510
- return errs;
511
- };
512
-
513
- // src/title-format.ts
514
- var EMOJI_MAP = {
515
- feat: "\u2728",
516
- fix: "\u{1F41B}",
517
- chore: "\u{1F9F9}",
518
- docs: "\u{1F4DD}",
519
- refactor: "\u267B\uFE0F",
520
- test: "\u2705",
521
- ci: "\u{1F916}",
522
- perf: "\u26A1\uFE0F",
523
- style: "\u{1F3A8}",
524
- build: "\u{1F3D7}\uFE0F",
525
- revert: "\u23EA",
526
- merge: "\u{1F500}",
527
- security: "\u{1F512}",
528
- release: "\u{1F3F7}\uFE0F"
529
- };
530
- var EMOJI_TYPE_RE = /^([\p{Emoji}\p{So}\p{Sk}])\s+(\w+)(\(.+\))?:\s+(.*)$/u;
531
- var TYPE_RE = /^(\w+)(\(.+\))?:\s+(.*)$/;
532
- var formatCommitTitle = (raw, opts) => {
533
- const { allowGitmoji, mode = "standard" } = opts;
534
- let norm = normalizeConventionalTitle(sanitizeTitle(raw, allowGitmoji));
535
- if (!allowGitmoji || mode !== "gitmoji" && mode !== "gitmoji-pure") {
536
- return norm;
537
- }
538
- if (mode === "gitmoji-pure") {
539
- let m2 = norm.match(EMOJI_TYPE_RE);
540
- if (m2) {
541
- const emoji = m2[1];
542
- const subject = m2[4];
543
- norm = `${emoji}: ${subject}`;
544
- } else if (m2 = norm.match(TYPE_RE)) {
545
- const type = m2[1];
546
- const subject = m2[3];
547
- const em = EMOJI_MAP[type] || "\u{1F527}";
548
- norm = `${em}: ${subject}`;
549
- } else if (!/^([\p{Emoji}\p{So}\p{Sk}])+:/u.test(norm)) {
550
- norm = `\u{1F527}: ${norm}`;
551
- }
552
- return norm;
553
- }
554
- let m = norm.match(EMOJI_TYPE_RE);
555
- if (m) {
556
- return norm;
557
- }
558
- if (m = norm.match(TYPE_RE)) {
559
- const type = m[1];
560
- const scope = m[2] || "";
561
- const subject = m[3];
562
- const em = EMOJI_MAP[type] || "\u{1F527}";
563
- norm = `${em} ${type}${scope}: ${subject}`;
564
- } else if (!/^([\p{Emoji}\p{So}\p{Sk}])+\s+\w+.*:/u.test(norm)) {
565
- norm = `\u{1F527} chore: ${norm}`;
566
- }
567
- return norm;
568
- };
569
-
570
202
  // src/workflow/generate.ts
571
203
  import { writeFileSync, mkdirSync, existsSync } from "fs";
572
204
  import { join } from "path";
573
205
  import inquirer from "inquirer";
574
-
575
- // src/workflow/ui.ts
576
- import chalk from "chalk";
577
- function animateHeaderBase(text = "ai-conventional-commit", modelSegment) {
578
- const mainText = text;
579
- const modelSeg = modelSegment ? ` (using ${modelSegment})` : "";
580
- if (!process.stdout.isTTY || process.env.AICC_NO_ANIMATION) {
581
- if (modelSeg) console.log("\n\u250C " + chalk.bold(mainText) + chalk.dim(modelSeg));
582
- else console.log("\n\u250C " + chalk.bold(mainText));
583
- return Promise.resolve();
584
- }
585
- const palette = [
586
- "#3a0d6d",
587
- "#5a1ea3",
588
- "#7a32d6",
589
- "#9a4dff",
590
- "#b267ff",
591
- "#c37dff",
592
- "#b267ff",
593
- "#9a4dff",
594
- "#7a32d6",
595
- "#5a1ea3"
596
- ];
597
- process.stdout.write("\n");
598
- return palette.reduce(async (p, color) => {
599
- await p;
600
- const frame = chalk.bold.hex(color)(mainText);
601
- if (modelSeg) process.stdout.write("\r\u250C " + frame + chalk.dim(modelSeg));
602
- else process.stdout.write("\r\u250C " + frame);
603
- await new Promise((r) => setTimeout(r, 60));
604
- }, Promise.resolve()).then(() => process.stdout.write("\n"));
605
- }
606
- function borderLine(content) {
607
- if (!content) console.log("\u2502");
608
- else console.log("\u2502 " + content);
609
- }
610
- function sectionTitle(label) {
611
- console.log("\u2299 " + chalk.bold(label));
612
- }
613
- function abortMessage() {
614
- console.log("\u2514 \u{1F645}\u200D\u2640\uFE0F No commit created.");
615
- console.log();
616
- }
617
- function finalSuccess(opts) {
618
- const elapsedMs = Date.now() - opts.startedAt;
619
- const seconds = elapsedMs / 1e3;
620
- const dur = seconds >= 0.1 ? seconds.toFixed(1) + "s" : elapsedMs + "ms";
621
- const plural = opts.count !== 1;
622
- if (plural) console.log(`\u2514 \u2728 ${opts.count} commits created in ${dur}.`);
623
- else console.log(`\u2514 \u2728 commit created in ${dur}.`);
624
- console.log();
625
- }
626
- function createPhasedSpinner(oraLib) {
627
- const useAnim = process.stdout.isTTY && !process.env.AICC_NO_ANIMATION && !process.env.AICC_NO_SPINNER_ANIM;
628
- const palette = [
629
- "#3a0d6d",
630
- "#5a1ea3",
631
- "#7a32d6",
632
- "#9a4dff",
633
- "#b267ff",
634
- "#c37dff",
635
- "#b267ff",
636
- "#9a4dff",
637
- "#7a32d6",
638
- "#5a1ea3"
639
- ];
640
- let label = "Starting";
641
- let i = 0;
642
- const spinner = oraLib({ text: chalk.bold(label), spinner: "dots" }).start();
643
- let interval = null;
644
- function frame() {
645
- if (!useAnim) return;
646
- spinner.text = chalk.bold.hex(palette[i])(label);
647
- i = (i + 1) % palette.length;
648
- }
649
- if (useAnim) {
650
- frame();
651
- interval = setInterval(frame, 80);
652
- }
653
- function setLabel(next) {
654
- label = next;
655
- if (useAnim) {
656
- i = 0;
657
- frame();
658
- } else {
659
- spinner.text = chalk.bold(label);
660
- }
661
- }
662
- function stopAnim() {
663
- if (interval) {
664
- clearInterval(interval);
665
- interval = null;
666
- }
667
- }
668
- return {
669
- spinner,
670
- async step(l, fn) {
671
- setLabel(l);
672
- try {
673
- return await fn();
674
- } catch (e) {
675
- stopAnim();
676
- const msg = `${l} failed: ${e?.message || e}`.replace(/^\s+/, "");
677
- spinner.fail(msg);
678
- throw e;
679
- }
680
- },
681
- phase(l) {
682
- setLabel(l);
683
- },
684
- stop() {
685
- stopAnim();
686
- spinner.stop();
687
- }
688
- };
689
- }
690
- function renderCommitBlock(opts) {
691
- const dim = (s) => chalk.dim(s);
692
- const white = (s) => chalk.white(s);
693
- const msgColor = opts.messageLabelColor || dim;
694
- const descColor = opts.descriptionLabelColor || dim;
695
- const titleColor = opts.titleColor || white;
696
- const bodyFirst = opts.bodyFirstLineColor || white;
697
- const bodyRest = opts.bodyLineColor || white;
698
- if (opts.fancy) {
699
- const heading = opts.heading ? chalk.hex("#9a4dff").bold(opts.heading) : void 0;
700
- if (heading) borderLine(heading);
701
- borderLine(msgColor("Title:") + " " + titleColor(`${opts.indexPrefix || ""}${opts.title}`));
702
- } else {
703
- if (opts.heading) borderLine(chalk.bold(opts.heading));
704
- if (!opts.hideMessageLabel)
705
- borderLine(msgColor("Message:") + " " + titleColor(`${opts.indexPrefix || ""}${opts.title}`));
706
- else
707
- borderLine(msgColor("Title:") + " " + titleColor(`${opts.indexPrefix || ""}${opts.title}`));
708
- }
709
- borderLine();
710
- if (opts.body) {
711
- const lines = opts.body.split("\n");
712
- lines.forEach((line, i) => {
713
- if (line.trim().length === 0) borderLine();
714
- else if (i === 0) {
715
- borderLine(descColor("Description:"));
716
- borderLine(bodyFirst(line));
717
- } else borderLine(bodyRest(line));
718
- });
719
- }
720
- }
721
-
722
- // src/workflow/generate.ts
723
206
  async function runGenerate(config) {
724
207
  const startedAt = Date.now();
725
208
  if (!await ensureStagedChanges()) {
@@ -743,7 +226,7 @@ async function runGenerate(config) {
743
226
  const deltas = files.map((f) => (f.additions || 0) + (f.deletions || 0));
744
227
  const maxDelta = Math.max(...deltas, 1);
745
228
  borderLine(
746
- chalk2.dim(`Detected ${files.length} staged ${files.length === 1 ? "file" : "files"}:`)
229
+ chalk.dim(`Detected ${files.length} staged ${files.length === 1 ? "file" : "files"}:`)
747
230
  );
748
231
  let totalAdd = 0;
749
232
  let totalDel = 0;
@@ -760,13 +243,13 @@ async function runGenerate(config) {
760
243
  const barLen = Math.max(1, Math.round(delta / maxDelta * BAR_WIDTH));
761
244
  const addPortion = Math.min(barLen, Math.round(barLen * (add / (delta || 1))));
762
245
  const delPortion = barLen - addPortion;
763
- const bar = chalk2.green("+".repeat(addPortion)) + chalk2.red("-".repeat(delPortion));
764
- const counts = chalk2.green("+" + add) + " " + chalk2.red("-" + del);
246
+ const bar = chalk.green("+".repeat(addPortion)) + chalk.red("-".repeat(delPortion));
247
+ const counts = chalk.green("+" + add) + " " + chalk.red("-" + del);
765
248
  const name = f.file.length > maxName ? f.file.slice(0, maxName - 1) + "\u2026" : f.file;
766
249
  borderLine(name.padEnd(maxName) + " | " + counts.padEnd(12) + " " + bar);
767
250
  });
768
251
  borderLine(
769
- chalk2.dim(
252
+ chalk.dim(
770
253
  `${files.length} file${files.length === 1 ? "" : "s"} changed, ${totalAdd} insertion${totalAdd === 1 ? "" : "s"}(+), ${totalDel} deletion${totalDel === 1 ? "" : "s"}(-)`
771
254
  )
772
255
  );
@@ -819,8 +302,8 @@ async function runGenerate(config) {
819
302
  const errors = [...pluginErrors, ...guardErrors];
820
303
  if (errors.length) {
821
304
  borderLine();
822
- console.log("\u2299 " + chalk2.bold("Checks"));
823
- const errorLines = ["Validation issues:", ...errors.map((e) => chalk2.red("\u2022 " + e))];
305
+ console.log("\u2299 " + chalk.bold("Checks"));
306
+ const errorLines = ["Validation issues:", ...errors.map((e) => chalk.red("\u2022 " + e))];
824
307
  errorLines.forEach((l) => borderLine(l));
825
308
  }
826
309
  borderLine();
@@ -857,7 +340,7 @@ async function selectYesNo() {
857
340
  }
858
341
 
859
342
  // src/workflow/split.ts
860
- import chalk3 from "chalk";
343
+ import chalk2 from "chalk";
861
344
  import ora2 from "ora";
862
345
 
863
346
  // src/cluster.ts
@@ -926,7 +409,7 @@ async function runSplit(config, desired) {
926
409
  const deltas = files.map((f) => (f.additions || 0) + (f.deletions || 0));
927
410
  const maxDelta = Math.max(...deltas, 1);
928
411
  borderLine(
929
- chalk3.dim(`Detected ${files.length} staged ${files.length === 1 ? "file" : "files"}:`)
412
+ chalk2.dim(`Detected ${files.length} staged ${files.length === 1 ? "file" : "files"}:`)
930
413
  );
931
414
  let totalAdd = 0;
932
415
  let totalDel = 0;
@@ -943,13 +426,13 @@ async function runSplit(config, desired) {
943
426
  const barLen = Math.max(1, Math.round(delta / maxDelta * BAR_WIDTH));
944
427
  const addPortion = Math.min(barLen, Math.round(barLen * (add / (delta || 1))));
945
428
  const delPortion = barLen - addPortion;
946
- const bar = chalk3.green("+".repeat(addPortion)) + chalk3.red("-".repeat(delPortion));
947
- const counts = chalk3.green("+" + add) + " " + chalk3.red("-" + del);
429
+ const bar = chalk2.green("+".repeat(addPortion)) + chalk2.red("-".repeat(delPortion));
430
+ const counts = chalk2.green("+" + add) + " " + chalk2.red("-" + del);
948
431
  const name = f.file.length > maxName ? f.file.slice(0, maxName - 1) + "\u2026" : f.file;
949
432
  borderLine(name.padEnd(maxName) + " | " + counts.padEnd(12) + " " + bar);
950
433
  });
951
434
  borderLine(
952
- chalk3.dim(
435
+ chalk2.dim(
953
436
  `${files.length} file${files.length === 1 ? "" : "s"} changed, ${totalAdd} insertion${totalAdd === 1 ? "" : "s"}(+), ${totalDel} deletion${totalDel === 1 ? "" : "s"}(-)`
954
437
  )
955
438
  );
@@ -1061,7 +544,7 @@ function saveSession2(data) {
1061
544
  }
1062
545
 
1063
546
  // src/workflow/refine.ts
1064
- import chalk4 from "chalk";
547
+ import chalk3 from "chalk";
1065
548
  import ora3 from "ora";
1066
549
  import inquirer3 from "inquirer";
1067
550
  import { readFileSync, existsSync as existsSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
@@ -1114,11 +597,11 @@ async function runRefine(config, options) {
1114
597
  }
1115
598
  sectionTitle("Original");
1116
599
  const original = plan.commits[index];
1117
- const originalLines = [chalk4.yellow(original.title)];
600
+ const originalLines = [chalk3.yellow(original.title)];
1118
601
  if (original.body) {
1119
602
  original.body.split("\n").forEach((line) => {
1120
603
  if (line.trim().length === 0) originalLines.push("");
1121
- else originalLines.push(chalk4.white(line));
604
+ else originalLines.push(chalk3.white(line));
1122
605
  });
1123
606
  }
1124
607
  originalLines.forEach((l) => l.trim().length === 0 ? borderLine() : borderLine(l));
@@ -1157,7 +640,7 @@ async function runRefine(config, options) {
1157
640
  renderCommitBlock({
1158
641
  title: refinedPlan.commits[0].title,
1159
642
  body: refinedPlan.commits[0].body,
1160
- titleColor: (s) => chalk4.yellow(s)
643
+ titleColor: (s) => chalk3.yellow(s)
1161
644
  });
1162
645
  borderLine();
1163
646
  const { ok } = await inquirer3.prompt([
@@ -1186,7 +669,7 @@ async function runRefine(config, options) {
1186
669
  // package.json
1187
670
  var package_default = {
1188
671
  name: "@kud/ai-conventional-commit-cli",
1189
- version: "0.11.0",
672
+ version: "0.12.0",
1190
673
  type: "module",
1191
674
  description: "Opinionated, style-aware AI assistant for crafting and splitting git commits (opencode-based, provider-agnostic).",
1192
675
  bin: {
@@ -1256,7 +739,7 @@ var package_default = {
1256
739
  };
1257
740
 
1258
741
  // src/index.ts
1259
- import { execa as execa2 } from "execa";
742
+ import { execa } from "execa";
1260
743
  import inquirer4 from "inquirer";
1261
744
  var pkgVersion = package_default.version || "0.0.0";
1262
745
  var RootCommand = class extends Command {
@@ -1436,7 +919,7 @@ var ModelsCommand = class extends Command {
1436
919
  return;
1437
920
  }
1438
921
  try {
1439
- const { stdout } = await execa2("opencode", ["models"]).catch(async (err) => {
922
+ const { stdout } = await execa("opencode", ["models"]).catch(async (err) => {
1440
923
  if (err.shortMessage && /ENOENT/.test(err.shortMessage)) {
1441
924
  this.context.stderr.write(
1442
925
  "opencode CLI not found in PATH. Install it from https://github.com/opencodejs/opencode or ensure the binary is available.\n"
@@ -1587,6 +1070,58 @@ var ConfigSetCommand = class extends Command {
1587
1070
  `);
1588
1071
  }
1589
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
+ };
1590
1125
  var VersionCommand = class extends Command {
1591
1126
  static paths = [[`--version`], [`-V`]];
1592
1127
  async execute() {
@@ -1607,6 +1142,7 @@ cli.register(ModelsCommand);
1607
1142
  cli.register(ConfigShowCommand);
1608
1143
  cli.register(ConfigGetCommand);
1609
1144
  cli.register(ConfigSetCommand);
1145
+ cli.register(RewordCommand);
1610
1146
  cli.register(VersionCommand);
1611
1147
  cli.runExit(process.argv.slice(2), {
1612
1148
  stdin: process.stdin,