ai-cmds 0.2.0 → 0.4.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/README.md CHANGED
@@ -7,9 +7,10 @@ AI-powered CLI tool that uses OpenAI and Google Gemini models to review code cha
7
7
  - Multiple AI models: GPT-5, GPT-5-mini, GPT-4o-mini, Gemini 2.5 Pro, Gemini 2.0 Flash
8
8
  - Configurable review setups from light to heavy
9
9
  - Custom setups with full control over reviewer and validator models
10
- - Four commands: `review-code-changes` for local development, `advanced-review-changes` for guided/customized local review focus, `review-pr` for CI, `create-pr` for PR creation
10
+ - Five commands: `commit` for AI commit messages, `review-code-changes` for local development, `advanced-review-changes` for guided/customized local review focus, `review-pr` for CI, `create-pr` for PR creation
11
11
  - Parallel reviews with a single structured validation pass for higher accuracy
12
12
  - Optional provider-aware concurrency limits for reviewer fan-out
13
+ - AI-generated commit messages with interactive editing
13
14
  - AI-generated PR titles and descriptions
14
15
  - Automatic filtering of import-only changes
15
16
  - Custom review instructions support
@@ -33,6 +34,27 @@ pnpm add ai-cmds
33
34
 
34
35
  ## Commands
35
36
 
37
+ ### `commit` - AI Commit Messages
38
+
39
+ Generate commit messages from staged changes using AI (Gemini primary, GPT-5-mini fallback).
40
+
41
+ ```bash
42
+ # Generate commit message and commit
43
+ ai-cmds commit
44
+
45
+ # Preview message without committing
46
+ ai-cmds commit --dry-run
47
+ ```
48
+
49
+ **Arguments:**
50
+ - `--dry-run` - Preview the generated message without committing
51
+
52
+ **Behavior:**
53
+ - If no files are staged, automatically stages all changes before generating
54
+ - Lockfiles are excluded from the diff sent to AI by default
55
+ - After generation, choose to: **Commit**, **Edit**, **Regenerate**, or **Cancel**
56
+ - If the primary model fails, automatically retries with the fallback model
57
+
36
58
  ### `review-code-changes` - Local Development
37
59
 
38
60
  Review local code changes (staged or all changes vs base branch). Best for local development workflow.
@@ -224,6 +246,11 @@ export default defineConfig({
224
246
  diffExcludePatterns: ['pnpm-lock.yaml'],
225
247
  descriptionInstructions: 'Always mention Jira ticket if present in branch name',
226
248
  },
249
+ commit: {
250
+ maxDiffTokens: 10000,
251
+ excludePatterns: ['dist/**'],
252
+ instructions: 'Always include the Jira ticket number from the branch name',
253
+ },
227
254
  });
228
255
  ```
229
256
 
@@ -255,6 +282,7 @@ By default, `.env` is loaded automatically before the config file is imported, a
255
282
  | `loadDotEnv` | Controls `.env` file loading. `true` (default): load `.env`, `false`: skip, `string`: additional file path, `string[]`: multiple files (later override earlier) |
256
283
  | `codeReview` | Configuration for the review commands (see below) |
257
284
  | `createPR` | Configuration for the create-pr command (see below) |
285
+ | `commit` | Configuration for the commit command (see below) |
258
286
 
259
287
  #### `codeReview` Options
260
288
 
@@ -282,6 +310,16 @@ By default, `.env` is loaded automatically before the config file is imported, a
282
310
  | `diffExcludePatterns` | Glob patterns for files to exclude from diff |
283
311
  | `maxDiffTokens` | Maximum tokens from diff to include in AI prompt (default: 50000) |
284
312
 
313
+ #### `commit` Options
314
+
315
+ | Option | Description |
316
+ |--------|-------------|
317
+ | `primaryModel` | Custom AI model for commit message generation (default: Gemini 2.5 Flash) |
318
+ | `fallbackModel` | Fallback AI model if primary fails (default: GPT-5-mini) |
319
+ | `maxDiffTokens` | Maximum tokens from diff to include in AI prompt (default: 10000) |
320
+ | `excludePatterns` | Additional glob patterns to exclude from diff (merged with default lockfile patterns) |
321
+ | `instructions` | Custom instructions for AI commit message generation |
322
+
285
323
  When `codeReview.logsDir` (or `AI_CLI_LOGS_DIR`) is set, each review run stores artifacts under:
286
324
 
287
325
  - `<logsDir>/advanced-review-changes/<run-id>/...` for advanced local reviews
@@ -171,6 +171,34 @@ type CreatePRConfig = {
171
171
  */
172
172
  maxDiffTokens?: number;
173
173
  };
174
+ type CommitConfig = {
175
+ /**
176
+ * Custom AI model for generating commit messages.
177
+ * Defaults to Google Gemini 2.5 Flash.
178
+ */
179
+ primaryModel?: CustomModelConfig;
180
+ /**
181
+ * Fallback AI model if the primary model fails.
182
+ * Defaults to OpenAI GPT-5-mini.
183
+ */
184
+ fallbackModel?: CustomModelConfig;
185
+ /**
186
+ * Maximum number of tokens from the diff to include in the AI prompt.
187
+ * @default 10000
188
+ */
189
+ maxDiffTokens?: number;
190
+ /**
191
+ * Additional glob patterns to exclude from the diff sent to AI.
192
+ * Merged with default lockfile patterns (package-lock.json, yarn.lock, etc.).
193
+ * @example ['dist/**', '*.generated.ts']
194
+ */
195
+ excludePatterns?: string[];
196
+ /**
197
+ * Custom instructions to include in the AI prompt for generating commit messages.
198
+ * @example 'Always include the Jira ticket number from the branch name'
199
+ */
200
+ instructions?: string;
201
+ };
174
202
  type Config = {
175
203
  /**
176
204
  * Configuration for the review-code-changes, advanced-review-changes, and
@@ -181,6 +209,10 @@ type Config = {
181
209
  * Configuration for the create-pr command.
182
210
  */
183
211
  createPR?: CreatePRConfig;
212
+ /**
213
+ * Configuration for the commit command.
214
+ */
215
+ commit?: CommitConfig;
184
216
  /**
185
217
  * Controls loading of environment variables from `.env` files.
186
218
  *
@@ -322,6 +354,33 @@ declare const advancedReviewChangesCommand: {
322
354
  }[] | undefined;
323
355
  };
324
356
  //#endregion
357
+ //#region src/commands/commit/commit.d.ts
358
+ declare const commitCommand: {
359
+ short: string | undefined;
360
+ description: string;
361
+ run: (cmdArgs: {
362
+ dryRun: boolean;
363
+ yes: boolean;
364
+ }) => Promise<void> | void;
365
+ args: {
366
+ dryRun: {
367
+ type: "flag";
368
+ name: string;
369
+ description: string;
370
+ };
371
+ yes: {
372
+ type: "flag";
373
+ name: string;
374
+ description: string;
375
+ short: string;
376
+ };
377
+ } | undefined;
378
+ examples: {
379
+ args: string[];
380
+ description: string;
381
+ }[] | undefined;
382
+ };
383
+ //#endregion
325
384
  //#region src/commands/create-pr/create-pr.d.ts
326
385
  declare const createPRCommand: {
327
386
  short: string | undefined;
@@ -436,4 +495,4 @@ declare const reviewPRCommand: {
436
495
  }[] | undefined;
437
496
  };
438
497
  //#endregion
439
- export { BUILT_IN_SCOPE_OPTIONS, BUILT_IN_SETUP_OPTIONS, type Config, type CreatePRConfig, type CustomModelConfig, DEFAULT_SCOPES, type ReviewCodeChangesConfig, type ReviewConcurrencyConfig, type ScopeConfig, type ScopeContext, type SetupConfig, advancedReviewChangesCommand, createPRCommand, defineConfig, reviewCodeChangesCommand, reviewPRCommand };
498
+ export { BUILT_IN_SCOPE_OPTIONS, BUILT_IN_SETUP_OPTIONS, type CommitConfig, type Config, type CreatePRConfig, type CustomModelConfig, DEFAULT_SCOPES, type ReviewCodeChangesConfig, type ReviewConcurrencyConfig, type ScopeConfig, type ScopeContext, type SetupConfig, advancedReviewChangesCommand, commitCommand, createPRCommand, defineConfig, reviewCodeChangesCommand, reviewPRCommand };
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { a as BUILT_IN_SETUP_OPTIONS, c as defineConfig, i as reviewCodeChangesCommand, n as createPRCommand, o as BUILT_IN_SCOPE_OPTIONS, r as advancedReviewChangesCommand, s as DEFAULT_SCOPES, t as reviewPRCommand } from "./review-pr-MkD-Pu9b.js";
1
+ import { a as reviewCodeChangesCommand, c as DEFAULT_SCOPES, i as advancedReviewChangesCommand, l as defineConfig, n as createPRCommand, o as BUILT_IN_SETUP_OPTIONS, r as commitCommand, s as BUILT_IN_SCOPE_OPTIONS, t as reviewPRCommand } from "./review-pr-abLkWpeM.js";
2
2
 
3
- export { BUILT_IN_SCOPE_OPTIONS, BUILT_IN_SETUP_OPTIONS, DEFAULT_SCOPES, advancedReviewChangesCommand, createPRCommand, defineConfig, reviewCodeChangesCommand, reviewPRCommand };
3
+ export { BUILT_IN_SCOPE_OPTIONS, BUILT_IN_SETUP_OPTIONS, DEFAULT_SCOPES, advancedReviewChangesCommand, commitCommand, createPRCommand, defineConfig, reviewCodeChangesCommand, reviewPRCommand };
package/dist/main.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { i as reviewCodeChangesCommand, n as createPRCommand, r as advancedReviewChangesCommand, t as reviewPRCommand } from "./review-pr-MkD-Pu9b.js";
2
+ import { a as reviewCodeChangesCommand, i as advancedReviewChangesCommand, n as createPRCommand, r as commitCommand, t as reviewPRCommand } from "./review-pr-abLkWpeM.js";
3
3
  import { createCLI } from "@ls-stack/cli";
4
4
 
5
5
  //#region src/main.ts
@@ -7,6 +7,7 @@ await createCLI({
7
7
  name: "✨ ai-cmds",
8
8
  baseCmd: "ai-cmds"
9
9
  }, {
10
+ commit: commitCommand,
10
11
  "review-code-changes": reviewCodeChangesCommand,
11
12
  "review-pr": reviewPRCommand,
12
13
  "create-pr": createPRCommand,
@@ -243,18 +243,65 @@ async function getRepoInfo() {
243
243
  repo: match[2].replace(/\.git$/, "")
244
244
  };
245
245
  }
246
+ async function getUnstagedDiff(options = {}) {
247
+ const { includeFiles, silent = true } = options;
248
+ const gitArgs = ["git", "diff"];
249
+ if (includeFiles && includeFiles.length > 0) gitArgs.push("--", ...includeFiles);
250
+ return runCmdUnwrap(gitArgs, { silent });
251
+ }
252
+ async function getChangedFilesUnstaged() {
253
+ const modifiedOutput = await runCmdSilentUnwrap([
254
+ "git",
255
+ "diff",
256
+ "--name-only"
257
+ ]);
258
+ const untrackedOutput = await runCmdSilentUnwrap([
259
+ "git",
260
+ "ls-files",
261
+ "--others",
262
+ "--exclude-standard"
263
+ ]);
264
+ return [...modifiedOutput.trim().split("\n"), ...untrackedOutput.trim().split("\n")].filter(Boolean);
265
+ }
266
+ async function stageAll() {
267
+ await runCmdUnwrap([
268
+ "git",
269
+ "add",
270
+ "-A"
271
+ ], { silent: true });
272
+ }
273
+ async function commit(message) {
274
+ return runCmdUnwrap([
275
+ "git",
276
+ "commit",
277
+ "-m",
278
+ message
279
+ ], { silent: true });
280
+ }
281
+ async function hasChanges() {
282
+ return (await runCmdSilentUnwrap([
283
+ "git",
284
+ "status",
285
+ "--porcelain"
286
+ ])).trim().length > 0;
287
+ }
246
288
  const git = {
247
289
  getCurrentBranch,
248
290
  getGitRoot,
249
291
  getDiffToBranch,
250
292
  getStagedDiff,
293
+ getUnstagedDiff,
251
294
  getChangedFiles: getChangedFiles$1,
295
+ getChangedFilesUnstaged,
252
296
  getStagedFiles,
253
297
  fetchBranch,
254
298
  getCommitHash,
255
299
  getRemoteUrl,
256
300
  getRepoInfo,
257
- getLocalBranches
301
+ getLocalBranches,
302
+ stageAll,
303
+ commit,
304
+ hasChanges
258
305
  };
259
306
 
260
307
  //#endregion
@@ -1707,7 +1754,7 @@ function getModelId(model) {
1707
1754
  if (typeof model === "string") return model;
1708
1755
  return model.modelId;
1709
1756
  }
1710
- function getProviderId(model) {
1757
+ function getProviderId$1(model) {
1711
1758
  if (typeof model === "string") return "unknown";
1712
1759
  return model.provider;
1713
1760
  }
@@ -1744,7 +1791,7 @@ function createDebugTrace(params) {
1744
1791
  durationMs: endedAt.getTime() - startedAt.getTime(),
1745
1792
  model: {
1746
1793
  id: getModelId(model),
1747
- provider: getProviderId(model)
1794
+ provider: getProviderId$1(model)
1748
1795
  },
1749
1796
  config,
1750
1797
  result
@@ -1766,7 +1813,7 @@ async function runSingleReview(context, prData, changedFiles, prDiff, reviewerId
1766
1813
  ripgrep: createRipgrepTool(reviewerId)
1767
1814
  },
1768
1815
  ...config?.topP !== false && { topP: config?.topP ?? .9 },
1769
- providerOptions: config?.providerOptions ? { [getProviderId(model)]: config.providerOptions } : void 0
1816
+ providerOptions: config?.providerOptions ? { [getProviderId$1(model)]: config.providerOptions } : void 0
1770
1817
  }));
1771
1818
  const modelId = getModelId(model);
1772
1819
  if (result.error) {
@@ -1820,7 +1867,7 @@ async function reviewValidator(context, reviews, prData, changedFiles, prDiff, h
1820
1867
  },
1821
1868
  experimental_output: Output.object({ schema: validatedReviewSchema }),
1822
1869
  ...config?.topP !== false && { topP: config?.topP ?? .7 },
1823
- providerOptions: config?.providerOptions ? { [getProviderId(model)]: config.providerOptions } : void 0
1870
+ providerOptions: config?.providerOptions ? { [getProviderId$1(model)]: config.providerOptions } : void 0
1824
1871
  }));
1825
1872
  if (result.error) {
1826
1873
  console.error(`❌ Validator failed with model ${getModelId(model)}`, result.error);
@@ -1884,7 +1931,7 @@ async function runPreviousReviewCheck(context, prData, changedFiles, prDiff, { m
1884
1931
  ripgrep: createRipgrepTool("previous-review-checker")
1885
1932
  },
1886
1933
  ...config?.topP !== false && { topP: config?.topP ?? .7 },
1887
- providerOptions: config?.providerOptions ? { [getProviderId(model)]: config.providerOptions } : void 0
1934
+ providerOptions: config?.providerOptions ? { [getProviderId$1(model)]: config.providerOptions } : void 0
1888
1935
  }));
1889
1936
  const modelId = getModelId(model);
1890
1937
  if (result.error) {
@@ -2497,6 +2544,252 @@ const advancedReviewChangesCommand = createCmd({
2497
2544
  }
2498
2545
  });
2499
2546
 
2547
+ //#endregion
2548
+ //#region src/commands/commit/commit-message-generator.ts
2549
+ const commitMessageSchema = z.object({
2550
+ subject: z.string().describe("Concise commit subject line (50 chars ideal, 72 max). Imperative mood. No period at end."),
2551
+ body: z.string().optional().describe("Optional commit body explaining what and why (not how). Wrap at 72 chars.")
2552
+ });
2553
+ const systemPrompt$1 = `You are a helpful assistant that generates clear, professional git commit messages following conventional commit best practices.
2554
+
2555
+ Guidelines for the subject line:
2556
+ - Use imperative mood ("Add feature" not "Added feature")
2557
+ - Keep it concise: 50 characters is ideal, 72 is the hard max
2558
+ - Do not end with a period
2559
+ - Capitalize the first letter
2560
+ - Focus on WHAT was done and WHY, not HOW
2561
+ - If changes span multiple areas, summarize the overall intent
2562
+
2563
+ Guidelines for the body (optional):
2564
+ - Only include a body if the subject alone doesn't fully explain the change
2565
+ - Explain the motivation for the change
2566
+ - Contrast the new behavior with the old behavior if relevant
2567
+ - Wrap lines at 72 characters
2568
+ - Do not repeat the subject line`;
2569
+ function buildUserPrompt$1(changedFiles, diff, instructions) {
2570
+ let prompt = `Generate a commit message for the following staged changes.
2571
+
2572
+ <changed-files>
2573
+ ${changedFiles.map((f) => `- ${f}`).join("\n")}
2574
+ </changed-files>
2575
+
2576
+ <diff>
2577
+ ${diff}
2578
+ </diff>`;
2579
+ if (instructions) prompt += `\n\n<instructions>\n${instructions}\n</instructions>`;
2580
+ return prompt;
2581
+ }
2582
+ function getProviderId(model) {
2583
+ if (typeof model === "string") return "unknown";
2584
+ return model.provider;
2585
+ }
2586
+ async function resolveModel(customModel, fallbackProvider) {
2587
+ if (customModel) return {
2588
+ model: customModel.model,
2589
+ label: customModel.label ?? "custom model",
2590
+ providerOptions: customModel.providerOptions ? { [getProviderId(customModel.model)]: customModel.providerOptions } : void 0
2591
+ };
2592
+ if (fallbackProvider === "google") {
2593
+ const { google } = await import("@ai-sdk/google");
2594
+ return {
2595
+ model: google("gemini-2.5-flash"),
2596
+ label: "gemini-2.5-flash",
2597
+ providerOptions: void 0
2598
+ };
2599
+ }
2600
+ const { openai } = await import("@ai-sdk/openai");
2601
+ return {
2602
+ model: openai("gpt-5-mini"),
2603
+ label: "gpt-5-mini",
2604
+ providerOptions: void 0
2605
+ };
2606
+ }
2607
+ async function generateCommitMessage(changedFiles, diff, config) {
2608
+ const userPrompt = buildUserPrompt$1(changedFiles, diff, config.instructions);
2609
+ const primary = await resolveModel(config.primaryModel, "google");
2610
+ try {
2611
+ console.log(`🤖 Generating commit message with ${primary.label}...`);
2612
+ const primaryStart = performance.now();
2613
+ const result = await generateObject({
2614
+ model: primary.model,
2615
+ schema: commitMessageSchema,
2616
+ system: systemPrompt$1,
2617
+ prompt: userPrompt,
2618
+ providerOptions: primary.providerOptions
2619
+ });
2620
+ const primaryDuration = performance.now() - primaryStart;
2621
+ logTokenUsage(primary.label, result.usage, primaryDuration);
2622
+ return formatCommitMessage(result.object);
2623
+ } catch {
2624
+ console.warn(`⚠️ Primary model (${primary.label}) failed, trying fallback...`);
2625
+ const fallback = await resolveModel(config.fallbackModel, "openai");
2626
+ console.log(`🤖 Generating commit message with ${fallback.label}...`);
2627
+ const fallbackStart = performance.now();
2628
+ const result = await generateObject({
2629
+ model: fallback.model,
2630
+ schema: commitMessageSchema,
2631
+ system: systemPrompt$1,
2632
+ prompt: userPrompt,
2633
+ providerOptions: fallback.providerOptions
2634
+ });
2635
+ const fallbackDuration = performance.now() - fallbackStart;
2636
+ logTokenUsage(fallback.label, result.usage, fallbackDuration);
2637
+ return formatCommitMessage(result.object);
2638
+ }
2639
+ }
2640
+ function logTokenUsage(label, usage, durationMs) {
2641
+ const input = formatNum$1(usage.inputTokens ?? 0);
2642
+ const output = formatNum$1(usage.outputTokens ?? 0);
2643
+ const total = formatNum$1(usage.totalTokens ?? 0);
2644
+ const seconds = (durationMs / 1e3).toFixed(1);
2645
+ console.log(`📊 ${label} — ${seconds}s, tokens: ${input} in / ${output} out / ${total} total`);
2646
+ }
2647
+ function formatCommitMessage(message) {
2648
+ if (message.body) return `${message.subject}\n\n${message.body}`;
2649
+ return message.subject;
2650
+ }
2651
+
2652
+ //#endregion
2653
+ //#region src/commands/commit/commit.ts
2654
+ const DEFAULT_MAX_DIFF_TOKENS$1 = 1e4;
2655
+ const DEFAULT_EXCLUDE_PATTERNS = [
2656
+ "package-lock.json",
2657
+ "yarn.lock",
2658
+ "pnpm-lock.yaml",
2659
+ "bun.lockb",
2660
+ "bun.lock"
2661
+ ];
2662
+ function truncateDiff$1(diff, maxTokens) {
2663
+ const tokenCount = estimateTokenCount(diff);
2664
+ if (tokenCount <= maxTokens) return diff;
2665
+ return `${sliceByTokens(diff, 0, maxTokens)}\n\n... (diff truncated, ${tokenCount - maxTokens} tokens omitted)`;
2666
+ }
2667
+ const commitCommand = createCmd({
2668
+ description: "Generate an AI-powered commit message and commit staged changes",
2669
+ short: "c",
2670
+ args: {
2671
+ dryRun: {
2672
+ type: "flag",
2673
+ name: "dry-run",
2674
+ description: "Preview generated message without committing"
2675
+ },
2676
+ yes: {
2677
+ type: "flag",
2678
+ name: "yes",
2679
+ description: "Skip confirmation and commit immediately",
2680
+ short: "y"
2681
+ }
2682
+ },
2683
+ examples: [
2684
+ {
2685
+ args: [],
2686
+ description: "Generate commit message and commit"
2687
+ },
2688
+ {
2689
+ args: ["--dry-run"],
2690
+ description: "Preview commit message without committing"
2691
+ },
2692
+ {
2693
+ args: ["--yes"],
2694
+ description: "Generate and commit without confirmation"
2695
+ }
2696
+ ],
2697
+ run: async ({ dryRun, yes }) => {
2698
+ const config = (await loadConfig()).commit ?? {};
2699
+ if (!await git.hasChanges()) showErrorAndExit("No changes to commit.");
2700
+ const stagedFiles = await git.getStagedFiles();
2701
+ const needsStaging = stagedFiles.length === 0;
2702
+ let filesToReview;
2703
+ let diff;
2704
+ if (needsStaging) {
2705
+ console.log("📂 No staged changes found. Will stage all changes on commit.");
2706
+ const changedFiles = await git.getChangedFilesUnstaged();
2707
+ if (changedFiles.length === 0) showErrorAndExit("No changes found to commit.");
2708
+ filesToReview = changedFiles;
2709
+ const excludePatterns = [...DEFAULT_EXCLUDE_PATTERNS, ...config.excludePatterns ?? []];
2710
+ const filteredFiles = applyExcludePatterns(filesToReview, excludePatterns);
2711
+ console.log(`\n📊 ${filesToReview.length} file(s) changed:`);
2712
+ for (const file of filesToReview) console.log(` ${file}`);
2713
+ console.log();
2714
+ diff = await git.getUnstagedDiff({
2715
+ includeFiles: filteredFiles.length > 0 ? filteredFiles : void 0,
2716
+ silent: true
2717
+ });
2718
+ } else {
2719
+ filesToReview = stagedFiles;
2720
+ const excludePatterns = [...DEFAULT_EXCLUDE_PATTERNS, ...config.excludePatterns ?? []];
2721
+ const filteredFiles = applyExcludePatterns(filesToReview, excludePatterns);
2722
+ console.log(`\n📊 ${filesToReview.length} file(s) staged for commit:`);
2723
+ for (const file of filesToReview) console.log(` ${file}`);
2724
+ console.log();
2725
+ diff = await git.getStagedDiff({
2726
+ includeFiles: filteredFiles.length > 0 ? filteredFiles : void 0,
2727
+ silent: true
2728
+ });
2729
+ }
2730
+ if (!diff.trim()) showErrorAndExit("No diff content available. All changes may be in excluded files.");
2731
+ const maxTokens = config.maxDiffTokens ?? DEFAULT_MAX_DIFF_TOKENS$1;
2732
+ const diffTokens = estimateTokenCount(diff);
2733
+ console.log(`📝 Diff: ${formatNum$1(diffTokens)} tokens`);
2734
+ const truncatedDiff = truncateDiff$1(diff, maxTokens);
2735
+ let message = await generateCommitMessage(filesToReview.slice(0, 30), truncatedDiff, config);
2736
+ while (true) {
2737
+ const separator = "─".repeat(60);
2738
+ console.log(`\n${separator}`);
2739
+ console.log(message);
2740
+ console.log(separator);
2741
+ if (dryRun) {
2742
+ console.log("\n🔍 Dry run mode — commit not created.\n");
2743
+ return;
2744
+ }
2745
+ if (yes) {
2746
+ if (needsStaging) await git.stageAll();
2747
+ await git.commit(message);
2748
+ console.log("\n✅ Changes committed successfully:");
2749
+ for (const file of filesToReview) console.log(` ${file}`);
2750
+ console.log();
2751
+ return;
2752
+ }
2753
+ const action = await cliInput.select("What would you like to do?", { options: [
2754
+ {
2755
+ value: "commit",
2756
+ label: "Commit"
2757
+ },
2758
+ {
2759
+ value: "edit",
2760
+ label: "Edit message"
2761
+ },
2762
+ {
2763
+ value: "regenerate",
2764
+ label: "Regenerate"
2765
+ },
2766
+ {
2767
+ value: "cancel",
2768
+ label: "Cancel"
2769
+ }
2770
+ ] });
2771
+ if (action === "cancel") {
2772
+ console.log("\n🚫 Commit cancelled.\n");
2773
+ return;
2774
+ }
2775
+ if (action === "edit") {
2776
+ message = await cliInput.text("Edit commit message:", { initial: message });
2777
+ continue;
2778
+ }
2779
+ if (action === "regenerate") {
2780
+ message = await generateCommitMessage(filesToReview.slice(0, 30), truncatedDiff, config);
2781
+ continue;
2782
+ }
2783
+ if (needsStaging) await git.stageAll();
2784
+ await git.commit(message);
2785
+ console.log("\n✅ Changes committed successfully:");
2786
+ for (const file of filesToReview) console.log(` ${file}`);
2787
+ console.log();
2788
+ return;
2789
+ }
2790
+ }
2791
+ });
2792
+
2500
2793
  //#endregion
2501
2794
  //#region src/commands/create-pr/pr-generator.ts
2502
2795
  const DEFAULT_MAX_DIFF_TOKENS = 5e4;
@@ -3078,4 +3371,4 @@ const reviewPRCommand = createCmd({
3078
3371
  });
3079
3372
 
3080
3373
  //#endregion
3081
- export { BUILT_IN_SETUP_OPTIONS as a, defineConfig as c, reviewCodeChangesCommand as i, createPRCommand as n, BUILT_IN_SCOPE_OPTIONS as o, advancedReviewChangesCommand as r, DEFAULT_SCOPES as s, reviewPRCommand as t };
3374
+ export { reviewCodeChangesCommand as a, DEFAULT_SCOPES as c, advancedReviewChangesCommand as i, defineConfig as l, createPRCommand as n, BUILT_IN_SETUP_OPTIONS as o, commitCommand as r, BUILT_IN_SCOPE_OPTIONS as s, reviewPRCommand as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-cmds",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "dist"