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
|
-
-
|
|
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
|
|
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 {
|
|
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 {
|
|
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 };
|