lazycommitt 1.0.10 → 1.0.11
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 +50 -35
- package/dist/cli.mjs +41 -51
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -109,14 +109,33 @@ You can exclude specific files from AI analysis using the `--exclude` flag:
|
|
|
109
109
|
lazycommit --exclude package-lock.json --exclude dist/
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
+
#### Automatic multi-commit mode
|
|
113
|
+
|
|
114
|
+
When you stage many files, `lazycommit` can automatically split your changes into logical groups and create multiple commits with proper Conventional Commit messages.
|
|
115
|
+
|
|
116
|
+
- Auto-trigger: when staged files ≥ 5, or when the diff is large
|
|
117
|
+
- Grouping: buckets by type/scope (e.g., `feat(api)`, `docs`, `ci`, `build`, `test`, `chore`)
|
|
118
|
+
- Deep split: if everything falls into one big bucket (e.g., `app/api/*`), it auto-splits by second-level directory (like `analytics`, `projects`, `sessions`)
|
|
119
|
+
- Token-safe AI: each group uses a compact `git diff --cached --numstat` summary (not full diffs) to generate the commit line
|
|
120
|
+
|
|
121
|
+
Usage:
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
# Just run as usual; grouping triggers automatically when applicable
|
|
125
|
+
lazycommit
|
|
126
|
+
|
|
127
|
+
# Force grouping even for < 5 files
|
|
128
|
+
lazycommit --split
|
|
129
|
+
```
|
|
130
|
+
|
|
112
131
|
#### Handling large diffs
|
|
113
132
|
|
|
114
|
-
For large commits with many files, lazycommit automatically stays within API limits:
|
|
133
|
+
For large commits with many files, lazycommit automatically stays within API limits and maintains clean history:
|
|
115
134
|
|
|
116
|
-
- **Automatic detection**: Large diffs are detected
|
|
117
|
-
- **
|
|
118
|
-
- **
|
|
119
|
-
- **
|
|
135
|
+
- **Automatic detection**: Large diffs and many-file changes are detected
|
|
136
|
+
- **Logical grouping**: Files are grouped into conventional buckets; single huge buckets are auto-split by second-level directory (e.g., `app/api/<group>/...`)
|
|
137
|
+
- **Token-safe summaries**: Each group sends a small `--numstat` summary to AI instead of full diffs
|
|
138
|
+
- **Sequential commits**: In multi-commit mode, groups are committed one-by-one with their own messages
|
|
120
139
|
|
|
121
140
|
### Git hook
|
|
122
141
|
|
|
@@ -229,7 +248,14 @@ lazycommit config set proxy=
|
|
|
229
248
|
|
|
230
249
|
#### model
|
|
231
250
|
|
|
232
|
-
Default: `
|
|
251
|
+
Default: `llama-3.1-8b-instant`
|
|
252
|
+
|
|
253
|
+
The Groq model to use for generating commit messages. Available models include:
|
|
254
|
+
- `llama-3.1-8b-instant` (default) - Fast, efficient for conventional commits
|
|
255
|
+
- `llama-3.1-70b-versatile` - More detailed but slower
|
|
256
|
+
- `llama-3.1-120b-versatile` - Most capable but slowest
|
|
257
|
+
|
|
258
|
+
For conventional commit generation, the 8B instant model provides the best balance of speed and quality.
|
|
233
259
|
|
|
234
260
|
#### timeout
|
|
235
261
|
|
|
@@ -267,17 +293,6 @@ You can clear this option by setting it to an empty string:
|
|
|
267
293
|
lazycommit config set type=
|
|
268
294
|
```
|
|
269
295
|
|
|
270
|
-
#### chunk-size
|
|
271
|
-
|
|
272
|
-
Default: `4000`
|
|
273
|
-
|
|
274
|
-
The maximum number of tokens per chunk when processing large diffs. This helps avoid API limits and keeps requests fast:
|
|
275
|
-
|
|
276
|
-
```sh
|
|
277
|
-
lazycommit config set chunk-size 4000
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
**Note**: Must be between 1000-8000 tokens (Groq API limit).
|
|
281
296
|
|
|
282
297
|
## How it works
|
|
283
298
|
|
|
@@ -289,12 +304,13 @@ The tool uses Groq's fast inference API to provide quick and accurate commit mes
|
|
|
289
304
|
|
|
290
305
|
For large commits that exceed API token limits, lazycommit automatically:
|
|
291
306
|
|
|
292
|
-
1. **
|
|
293
|
-
2. **
|
|
294
|
-
3. **
|
|
295
|
-
4. **
|
|
307
|
+
1. **Detects large/many-file diffs** and switches to a scalable flow
|
|
308
|
+
2. **Groups files** by conventional type/scope; if only one large bucket remains, **auto-splits by second-level directory** (e.g., `app/api/<group>/...`)
|
|
309
|
+
3. **Generates messages per group** using compact `git diff --cached --numstat` summaries (not full diffs)
|
|
310
|
+
4. **Commits sequentially** per group with clear, conventional messages
|
|
311
|
+
5. When a single commit is requested, **uses compact summaries** to generate conventional messages efficiently
|
|
296
312
|
|
|
297
|
-
This ensures you can commit large changes (like new features, refactoring, or initial project setup) without hitting API limits.
|
|
313
|
+
This ensures you can commit large changes (like new features, refactoring, or initial project setup) without hitting API limits, while keeping a clean history.
|
|
298
314
|
|
|
299
315
|
## Troubleshooting
|
|
300
316
|
|
|
@@ -307,17 +323,12 @@ If you get a 413 error, your diff is too large for the API. Try these solutions:
|
|
|
307
323
|
lazycommit --exclude "dist/**" --exclude "node_modules/**" --exclude ".next/**"
|
|
308
324
|
```
|
|
309
325
|
|
|
310
|
-
2. **
|
|
311
|
-
```sh
|
|
312
|
-
lazycommit config set chunk-size 4000
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
3. **Use a different model**:
|
|
326
|
+
2. **Use a different model**:
|
|
316
327
|
```sh
|
|
317
328
|
lazycommit config set model "llama-3.1-70b-versatile"
|
|
318
329
|
```
|
|
319
330
|
|
|
320
|
-
|
|
331
|
+
3. **Commit in smaller batches**:
|
|
321
332
|
```sh
|
|
322
333
|
git add src/ # Stage only source files
|
|
323
334
|
lazycommit
|
|
@@ -329,19 +340,23 @@ If you get a 413 error, your diff is too large for the API. Try these solutions:
|
|
|
329
340
|
|
|
330
341
|
- Check your API key: `lazycommit config get GROQ_API_KEY`
|
|
331
342
|
- Verify you have staged changes: `git status`
|
|
332
|
-
- Try
|
|
343
|
+
- Try excluding large files or using a different model
|
|
333
344
|
|
|
334
345
|
### Slow performance with large diffs
|
|
335
346
|
|
|
336
|
-
-
|
|
347
|
+
- **Use the 8B instant model** (default): `lazycommit config set model "llama-3.1-8b-instant"`
|
|
337
348
|
- Exclude unnecessary files: `lazycommit --exclude "*.log" --exclude "*.tmp"`
|
|
349
|
+
- Use automatic multi-commit mode to split large changes into logical groups
|
|
350
|
+
- Lower generate count: `lazycommit config set generate=1` (default)
|
|
351
|
+
- Reduce timeout: `lazycommit config set timeout=5000` for faster failures
|
|
338
352
|
|
|
339
353
|
## Why Groq?
|
|
340
354
|
|
|
341
|
-
- **Fast**: Groq provides ultra-fast inference speeds
|
|
342
|
-
-
|
|
343
|
-
-
|
|
344
|
-
-
|
|
355
|
+
- **Fast**: Groq provides ultra-fast inference speeds, especially with the 8B instant model
|
|
356
|
+
- **Cost-effective**: More affordable than traditional AI APIs
|
|
357
|
+
- **Open source models**: Uses leading open-source language models
|
|
358
|
+
- **Reliable**: High uptime and consistent performance
|
|
359
|
+
- **Optimized for commits**: The 8B instant model is perfectly sized for conventional commit generation
|
|
345
360
|
|
|
346
361
|
## Maintainers
|
|
347
362
|
|
package/dist/cli.mjs
CHANGED
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
2
|
+
var we=Object.defineProperty;var l=(t,e)=>we(t,"name",{value:e,configurable:!0});import{command as B,cli as $e}from"cleye";import{readFileSync as be}from"fs";import{fileURLToPath as q,pathToFileURL as ve}from"url";import G,{dirname as ke,join as Ae}from"path";import{execa as k}from"execa";import{dim as O,bgCyan as J,black as X,green as _,red as F}from"kolorist";import{intro as V,spinner as D,outro as x,confirm as Z,isCancel as Y,select as xe}from"@clack/prompts";import $ from"fs/promises";import Pe from"os";import ee from"ini";import te from"groq-sdk";var Ce="1.0.11",_e={version:Ce};const Q=class Q extends Error{};l(Q,"KnownError");let w=Q;const z=" ",M=l(t=>{t instanceof Error&&!(t instanceof w)&&(t.stack&&console.error(O(t.stack.split(`
|
|
3
3
|
`).slice(1).join(`
|
|
4
4
|
`))),console.error(`
|
|
5
|
-
${
|
|
6
|
-
${
|
|
7
|
-
`),diff:
|
|
8
|
-
`)
|
|
9
|
-
|
|
10
|
-
`,r+=f)}return o.trim().length>0&&n.push(o.trim()),n},"chunkDiff"),re=g(async e=>{const t=["diff","--cached","--diff-algorithm=minimal"],{stdout:s}=await A("git",[...t,"--name-only",...N,...e?e.map(O):[]]);if(!s)return null;const n=s.split(`
|
|
11
|
-
`).filter(Boolean),u=await Promise.all(n.map(async o=>{try{const{stdout:r}=await A("git",[...t,"--numstat","--",o]),[l,f]=r.split(" ").slice(0,2).map(Number);return{file:o,additions:l||0,deletions:f||0,changes:(l||0)+(f||0)}}catch{return{file:o,additions:0,deletions:0,changes:0}}}));return{files:n,fileStats:u,totalChanges:u.reduce((o,r)=>o+r.changes,0)}},"getDiffSummary"),ce=g(e=>{const t=[];let s="";const n=e.split(`
|
|
12
|
-
`);for(const u of n)u.startsWith("diff --git ")?(s.trim().length>0&&t.push(s.trim()),s=u+`
|
|
13
|
-
`):s+=u+`
|
|
14
|
-
`;return s.trim().length>0&&t.push(s.trim()),t},"splitDiffByFile"),Oe=g(async(e,t=20)=>{const s=await re(e);if(!s)return null;const{fileStats:n}=s,u=[...n].sort((h,i)=>i.changes-h.changes),o=u.slice(0,Math.max(1,t)),r=s.files.length,l=s.totalChanges,f=n.reduce((h,i)=>h+(i.additions||0),0),d=n.reduce((h,i)=>h+(i.deletions||0),0),a=[];a.push(`Files changed: ${r}`),a.push(`Additions: ${f}, Deletions: ${d}, Total changes: ${l}`),a.push("Top files by changes:");for(const h of o)a.push(`- ${h.file} (+${h.additions} / -${h.deletions}, ${h.changes} changes)`);return u.length>o.length&&a.push(`\u2026and ${u.length-o.length} more files`),a.join(`
|
|
15
|
-
`)},"buildCompactSummary"),le=g(e=>w.lstat(e).then(()=>!0,()=>!1),"fileExists"),Ee=["","conventional"],{hasOwnProperty:Fe}=Object.prototype,me=g((e,t)=>Fe.call(e,t),"hasOwn"),y=g((e,t,s)=>{if(!t)throw new p(`Invalid config property ${e}: ${s}`)},"parseAssert"),E={GROQ_API_KEY(e){if(!e)throw new p("Please set your Groq API key via `lazycommit config set GROQ_API_KEY=<your token>`");return y("GROQ_API_KEY",e.startsWith("gsk_"),'Must start with "gsk_"'),e},locale(e){return e?(y("locale",e,"Cannot be empty"),y("locale",/^[a-z-]+$/i.test(e),"Must be a valid locale (letters and dashes/underscores). You can consult the list of codes in: https://wikipedia.org/wiki/List_of_ISO_639-1_codes"),e):"en"},generate(e){if(!e)return 1;y("generate",/^\d+$/.test(e),"Must be an integer");const t=Number(e);return y("generate",t>0,"Must be greater than 0"),y("generate",t<=5,"Must be less or equal to 5"),t},type(e){return e?(y("type",Ee.includes(e),"Invalid commit type"),e):""},proxy(e){if(!(!e||e.length===0))return y("proxy",/^https?:\/\//.test(e),"Must be a valid URL"),e},model(e){return!e||e.length===0?"openai/gpt-oss-120b":e},timeout(e){if(!e)return 1e4;y("timeout",/^\d+$/.test(e),"Must be an integer");const t=Number(e);return y("timeout",t>=500,"Must be greater than 500ms"),t},"max-length"(e){if(!e)return 50;y("max-length",/^\d+$/.test(e),"Must be an integer");const t=Number(e);return y("max-length",t>=20,"Must be greater than 20 characters"),t},"chunk-size"(e){if(!e)return 4e3;y("chunk-size",/^\d+$/.test(e),"Must be an integer");const t=Number(e);return y("chunk-size",t>=1e3,"Must be at least 1000 tokens"),y("chunk-size",t<=8e3,"Must be at most 8000 tokens (Groq limit)"),t}},B=z.join(_e.homedir(),".lazycommit"),fe=g(async()=>{if(!await le(B))return Object.create(null);const t=await w.readFile(B,"utf8");return se.parse(t)},"readConfigFile"),H=g(async(e,t)=>{const s=await fe(),n={};for(const u of Object.keys(E)){const o=E[u],r=e?.[u]??s[u];if(t)try{n[u]=o(r)}catch{}else n[u]=o(r)}return n},"getConfig"),Se=g(async e=>{const t=await fe();for(const[s,n]of e){if(!me(E,s))throw new p(`Invalid config property: ${s}`);const u=E[s](n);t[s]=u}await w.writeFile(B,se.stringify(t),"utf8")},"setConfigs"),Re={"":"<commit message>",conventional:"<type>(<optional scope>): <commit message>"},ge={"":"",conventional:`Choose the most appropriate type from the following categories that best describes the git diff:
|
|
5
|
+
${z}${O(`lazycommit v${_e.version}`)}`),console.error(`
|
|
6
|
+
${z}Please open a Bug report with the information above:`),console.error(`${z}https://github.com/KartikLabhshetwar/lazycommit/issues/new/choose`))},"handleCliError"),se=l(async()=>{const{stdout:t,failed:e}=await k("git",["rev-parse","--show-toplevel"],{reject:!1});if(e)throw new w("The current directory must be a Git repository!");return t},"assertGitRepo"),T=l(t=>`:(exclude)${t}`,"excludeFromDiff"),L=["package-lock.json","pnpm-lock.yaml","node_modules/**","dist/**","build/**",".next/**","coverage/**",".nyc_output/**","*.log","*.tmp","*.temp","*.cache",".DS_Store","Thumbs.db","*.min.js","*.min.css","*.bundle.js","*.bundle.css","*.lock"].map(T),oe=l(async t=>{const e=["diff","--cached","--diff-algorithm=minimal"],{stdout:s}=await k("git",[...e,"--name-only",...L,...t?t.map(T):[]]);if(!s)return;const{stdout:i}=await k("git",[...e,...L,...t?t.map(T):[]]);return{files:s.split(`
|
|
7
|
+
`),diff:i}},"getStagedDiff"),ie=l(t=>`Detected ${t.length.toLocaleString()} staged file${t.length>1?"s":""}`,"getDetectedMessage"),ne=l(async t=>{const e=["diff","--cached","--diff-algorithm=minimal"],{stdout:s}=await k("git",[...e,"--name-only",...L,...t?t.map(T):[]]);if(!s)return null;const i=s.split(`
|
|
8
|
+
`).filter(Boolean),o=await Promise.all(i.map(async n=>{try{const{stdout:u}=await k("git",[...e,"--numstat","--",n]),[a,c]=u.split(" ").slice(0,2).map(Number);return{file:n,additions:a||0,deletions:c||0,changes:(a||0)+(c||0)}}catch{return{file:n,additions:0,deletions:0,changes:0}}}));return{files:i,fileStats:o,totalChanges:o.reduce((n,u)=>n+u.changes,0)}},"getDiffSummary"),ue=l(async(t,e=20)=>{const s=await ne(t);if(!s)return null;const{fileStats:i}=s,o=[...i].sort((p,m)=>m.changes-p.changes),n=o.slice(0,Math.max(1,e)),u=s.files.length,a=s.totalChanges,c=i.reduce((p,m)=>p+(m.additions||0),0),h=i.reduce((p,m)=>p+(m.deletions||0),0),y=[];y.push(`Files changed: ${u}`),y.push(`Additions: ${c}, Deletions: ${h}, Total changes: ${a}`),y.push("Top files by changes:");for(const p of n)y.push(`- ${p.file} (+${p.additions} / -${p.deletions}, ${p.changes} changes)`);return o.length>n.length&&y.push(`\u2026and ${o.length-n.length} more files`),y.join(`
|
|
9
|
+
`)},"buildCompactSummary"),ae=l(t=>$.lstat(t).then(()=>!0,()=>!1),"fileExists"),De=["","conventional"],{hasOwnProperty:We}=Object.prototype,ce=l((t,e)=>We.call(t,e),"hasOwn"),v=l((t,e,s)=>{if(!e)throw new w(`Invalid config property ${t}: ${s}`)},"parseAssert"),E={GROQ_API_KEY(t){if(!t)throw new w("Please set your Groq API key via `lazycommit config set GROQ_API_KEY=<your token>`");return v("GROQ_API_KEY",t.startsWith("gsk_"),'Must start with "gsk_"'),t},locale(t){return t?(v("locale",t,"Cannot be empty"),v("locale",/^[a-z-]+$/i.test(t),"Must be a valid locale (letters and dashes/underscores). You can consult the list of codes in: https://wikipedia.org/wiki/List_of_ISO_639-1_codes"),t):"en"},generate(t){if(!t)return 1;v("generate",/^\d+$/.test(t),"Must be an integer");const e=Number(t);return v("generate",e>0,"Must be greater than 0"),v("generate",e<=5,"Must be less or equal to 5"),e},type(t){return t?(v("type",De.includes(t),"Invalid commit type"),t):""},proxy(t){if(!(!t||t.length===0))return v("proxy",/^https?:\/\//.test(t),"Must be a valid URL"),t},model(t){return!t||t.length===0?"llama-3.1-8b-instant":t},timeout(t){if(!t)return 1e4;v("timeout",/^\d+$/.test(t),"Must be an integer");const e=Number(t);return v("timeout",e>=500,"Must be greater than 500ms"),e},"max-length"(t){if(!t)return 50;v("max-length",/^\d+$/.test(t),"Must be an integer");const e=Number(t);return v("max-length",e>=20,"Must be greater than 20 characters"),e}},N=G.join(Pe.homedir(),".lazycommit"),re=l(async()=>{if(!await ae(N))return Object.create(null);const e=await $.readFile(N,"utf8");return ee.parse(e)},"readConfigFile"),U=l(async(t,e)=>{const s=await re(),i={};for(const o of Object.keys(E)){const n=E[o],u=t?.[o]??s[o];if(e)try{i[o]=n(u)}catch{}else i[o]=n(u)}return i},"getConfig"),Se=l(async t=>{const e=await re();for(const[s,i]of t){if(!ce(E,s))throw new w(`Invalid config property: ${s}`);const o=E[s](i);e[s]=o}await $.writeFile(N,ee.stringify(e),"utf8")},"setConfigs"),Ie={"":"<commit message>",conventional:"<type>(<optional scope>): <commit message>"},le={"":"",conventional:`Choose the most appropriate type from the following categories that best describes the git diff:
|
|
16
10
|
|
|
17
11
|
${JSON.stringify({feat:"A new feature for the user",fix:"A bug fix",docs:"Documentation only changes",style:"Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)",refactor:"A code change that neither fixes a bug nor adds a feature",perf:"A code change that improves performance",test:"Adding missing tests or correcting existing tests",build:"Changes that affect the build system or external dependencies",ci:"Changes to our CI configuration files and scripts",chore:"Other changes that don't modify src or test files",revert:"Reverts a previous commit"},null,2)}
|
|
18
12
|
|
|
19
|
-
IMPORTANT: Use the exact type name from the list above.`},
|
|
13
|
+
IMPORTANT: Use the exact type name from the list above.`},je=l((t,e,s)=>`You are an expert software engineer and git commit message writer. Your task is to analyze git diffs and generate clear, concise, and professional commit messages.
|
|
20
14
|
|
|
21
15
|
## Instructions:
|
|
22
16
|
1. Analyze the provided git diff carefully
|
|
@@ -35,13 +29,13 @@ IMPORTANT: Use the exact type name from the list above.`},F=g((e,t,s)=>`You are
|
|
|
35
29
|
- For features, describe what functionality was added
|
|
36
30
|
- For refactoring, mention what was improved (performance, readability, etc.)
|
|
37
31
|
|
|
38
|
-
## Language: ${
|
|
39
|
-
## Maximum length: ${
|
|
40
|
-
## Output format: ${
|
|
32
|
+
## Language: ${t}
|
|
33
|
+
## Maximum length: ${e} characters
|
|
34
|
+
## Output format: ${Ie[s]||"<commit message>"}
|
|
41
35
|
|
|
42
|
-
${
|
|
36
|
+
${le[s]?`
|
|
43
37
|
## Commit Type Guidelines:
|
|
44
|
-
${
|
|
38
|
+
${le[s]}`:""}
|
|
45
39
|
|
|
46
40
|
## Examples of good commit messages:
|
|
47
41
|
- "feat: Add user authentication with JWT tokens"
|
|
@@ -58,30 +52,21 @@ ${ge[s]}`:""}
|
|
|
58
52
|
- "WIP"
|
|
59
53
|
- "Stuff"
|
|
60
54
|
|
|
61
|
-
Remember: Your response will be used directly as the git commit message. Make it professional and informative.`,"generatePrompt"),
|
|
55
|
+
Remember: Your response will be used directly as the git commit message. Make it professional and informative.`,"generatePrompt"),Oe=l(async(t,e,s,i,o,n,u,a,c,h,y)=>{const p=new te({apiKey:t,timeout:h});try{return c>1?{choices:(await Promise.all(Array.from({length:c},()=>p.chat.completions.create({model:e,messages:s,temperature:i,top_p:o,frequency_penalty:n,presence_penalty:u,max_tokens:a,n:1})))).flatMap(d=>d.choices)}:await p.chat.completions.create({model:e,messages:s,temperature:i,top_p:o,frequency_penalty:n,presence_penalty:u,max_tokens:a,n:1})}catch(m){if(m instanceof te.APIError){let g=`Groq API Error: ${m.status} - ${m.name}`;throw m.message&&(g+=`
|
|
62
56
|
|
|
63
|
-
${
|
|
57
|
+
${m.message}`),m.status===500&&(g+=`
|
|
64
58
|
|
|
65
|
-
Check the API status: https://console.groq.com/status`),(
|
|
59
|
+
Check the API status: https://console.groq.com/status`),(m.status===413||m.message&&m.message.includes("rate_limit_exceeded"))&&(g+=`
|
|
66
60
|
|
|
67
61
|
\u{1F4A1} Tip: Your diff is too large. Try:
|
|
68
62
|
1. Commit files in smaller batches
|
|
69
63
|
2. Exclude large files with --exclude
|
|
70
64
|
3. Use a different model with --model
|
|
71
|
-
4. Check if you have build artifacts staged (dist/, .next/, etc.)`),new
|
|
65
|
+
4. Check if you have build artifacts staged (dist/, .next/, etc.)`),new w(g)}throw m.code==="ENOTFOUND"?new w(`Error connecting to ${m.hostname} (${m.syscall}). Are you connected to the internet?`):m}},"createChatCompletion"),me=l(t=>t.trim().replace(/[\n\r]/g,"").replace(/(\w)\.$/,"$1"),"sanitizeMessage"),Fe=l(t=>Array.from(new Set(t)),"deduplicateMessages"),Me=["feat:","fix:","docs:","style:","refactor:","perf:","test:","build:","ci:","chore:","revert:"],Te=l((t,e)=>{const s=t.replace(/\s+/g," ").trim(),i=s.match(/\b(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)\b\s*:?\s+[^.\n]+/i);let o=i?i[0]:s.split(/[.!?]/)[0];const n=o.toLowerCase();for(const u of Me){const a=u.slice(0,-1);if(n.startsWith(a+" ")&&!n.startsWith(u)){o=a+": "+o.slice(a.length+1);break}}return o=me(o),o?(o.length>e&&(o=o.slice(0,e)),o):null},"deriveMessageFromReasoning"),W=l(async(t,e,s,i,o,n,u,a,c)=>{const h=`This is a compact summary of staged changes. Generate a single, concise commit message within ${n} characters that reflects the overall intent.
|
|
72
66
|
|
|
73
|
-
${
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
`)}`;try{const k=((await S(e,t,[{role:"system",content:F(s,o,r)},{role:"user",content:$}],.7,1,0,0,Math.max(200,o*8),1,l,f)).choices||[]).map(x=>x.message?.content).filter(Boolean);if(k.length>0)return[P(k[0])]}catch{}throw new p("Failed to generate commit messages for any chunks")}if(m.length>1){const c=`I have ${m.length} commit messages for different parts of a large change:
|
|
77
|
-
|
|
78
|
-
${m.map(($,b)=>`${b+1}. ${$}`).join(`
|
|
79
|
-
`)}
|
|
80
|
-
|
|
81
|
-
Please generate a single, comprehensive commit message that captures the overall changes.
|
|
82
|
-
The message should be concise but cover the main aspects of all the changes.`;try{return await W(e,t,s,c,u,o,r,l,f)}catch{return m}}return m},"generateCommitMessageFromChunks"),Ge=g(async(e,t,s,n,u,o,r,l,f)=>{const d=`This is a compact summary of staged changes. Generate a single, concise commit message within ${o} characters that reflects the overall intent.
|
|
83
|
-
|
|
84
|
-
${n}`,a=await S(e,t,[{role:"system",content:F(s,o,r)},{role:"user",content:d}],.7,1,0,0,Math.max(200,o*8),u,l),h=(a.choices||[]).map(m=>m.message?.content||"").map(m=>P(m)).filter(Boolean);if(h.length>0)return he(h);const i=a.choices.map(m=>m.message?.reasoning||"").filter(Boolean);for(const m of i){const c=Q(m,o);if(c)return[c]}return[]},"generateCommitMessageFromSummary"),je=`\u2554\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2557
|
|
67
|
+
${i}`,y=await Oe(t,e,[{role:"system",content:je(s,n,u)},{role:"user",content:h}],.7,1,0,0,Math.max(200,n*8),o,a),p=(y.choices||[]).map(g=>g.message?.content||"").map(g=>me(g)).filter(Boolean);if(p.length>0)return Fe(p);const m=y.choices.map(g=>g.message?.reasoning||"").filter(Boolean);for(const g of m){const d=Te(g,n);if(d)return[d]}return[]},"generateCommitMessageFromSummary"),Ee=l(t=>{const e=t.toLowerCase();return t.split("/"),e.endsWith(".md")||e.startsWith("docs/")||e.includes("/docs/")||e.includes("readme")||e.includes("changelog")||e.includes("license")||e.endsWith(".rst")||e.endsWith(".adoc")?{type:"docs",scope:void 0}:e.startsWith(".github/")||e.startsWith(".gitlab/")||(e.endsWith(".yml")||e.endsWith(".yaml"))&&(e.includes("workflow")||e.includes("pipeline")||e.includes("action")||e.includes("ci")||e.includes("deploy"))?{type:"ci",scope:void 0}:e.endsWith("package.json")||e.endsWith("pnpm-lock.yaml")||e.endsWith("yarn.lock")||e.endsWith("package-lock.json")||e.endsWith("tsconfig.json")||e.endsWith("jsconfig.json")||e.endsWith("vite.config.js")||e.endsWith("webpack.config.js")||e.endsWith("rollup.config.js")||e.endsWith("babel.config.js")||e.endsWith(".config.js")||e.endsWith(".config.ts")||e.endsWith("dockerfile")||e.endsWith("docker-compose.yml")||e.endsWith("makefile")||e.endsWith(".mk")||e.includes("webpack")||e.includes("rollup")||e.includes("vite")||e.includes("babel")||e.startsWith("config/")?{type:"build",scope:void 0}:e.includes("/test/")||e.includes("__tests__")||e.includes("/tests/")||e.match(/\.(test|spec)\.[jt]sx?$/)||e.includes(".test.")||e.includes(".spec.")||e.startsWith("test/")||e.startsWith("tests/")||e.includes("cypress/")||e.includes("jest/")||e.includes("vitest/")?{type:"test",scope:void 0}:e.includes("analytics")||e.includes("metrics")||e.includes("tracking")?{type:"feat",scope:"analytics"}:e.includes("auth")||e.includes("login")||e.includes("security")||e.includes("permission")||e.includes("role")?{type:"feat",scope:"auth"}:e.startsWith("app/api/")||e.includes("/api/")||e.includes("routes/")||e.includes("endpoint")||e.includes("handler")?{type:"feat",scope:"api"}:e.includes("database")||e.includes("/db/")||e.includes("migration")||e.includes("schema")||e.includes("model")||e.includes("entity")||e.endsWith(".sql")||e.includes("prisma/")?{type:"feat",scope:"db"}:e.includes("component")||e.includes("ui/")||e.includes("style")||e.endsWith(".css")||e.endsWith(".scss")||e.endsWith(".less")||e.endsWith(".styl")||e.includes("theme")?{type:"feat",scope:"ui"}:e.includes("util")||e.includes("helper")||e.includes("lib/")||e.includes("common/")||e.includes("shared/")?{type:"refactor",scope:"utils"}:e.startsWith("app/")||e.startsWith("src/app/")||e.includes("page")||e.includes("layout")||e.includes("route")?{type:"feat",scope:"app"}:e.startsWith("src/")?{type:"feat",scope:"core"}:{type:"chore",scope:void 0}},"classifyFile"),Re=l(t=>{const e=t.split("/");if(e.length>1)return e[0]},"topLevelScope"),Ge=l(t=>{const e=new Map;for(const o of t){const n=Ee(o),u=n.scope||Re(o),a=`${n.type}:${u||"general"}`,c=e.get(a)||{meta:{type:n.type,scope:u},files:[]};c.files.push(o),e.set(a,c)}const s=[];for(const{meta:o,files:n}of e.values()){const{type:u,scope:a}=o;let c="update files";u==="docs"?c="update documentation":u==="ci"?c="update CI workflows":u==="build"?c="update build/configuration":u==="test"?c="update tests":u==="feat"?c="add or update features":u==="chore"&&(c="maintenance updates");const h=a?`${c} in ${a}`:c;s.push({type:u,scope:a,title:h,files:n})}const i={feat:0,fix:1,refactor:2,perf:3,test:4,docs:5,build:6,ci:7,chore:8};return s.sort((o,n)=>(i[o.type]??99)-(i[n.type]??99)),s},"groupFiles"),Ye=l(t=>{const e=t.split("/");if(e.length>=4&&e[0]==="app"&&e[1]==="api")return e[2];if(e.length>=2)return e[1]},"getSecondLevelDir"),ze=l(async t=>{if(t.length===0)return"";const e=["diff","--cached","--numstat","--",...t],{stdout:s}=await k("git",e),i=[];let o=0,n=0;const u=s.split(`
|
|
68
|
+
`).filter(Boolean);for(const c of u){const[h,y,p]=c.split(" "),m=Number(h)||0,g=Number(y)||0;o+=m,n+=g,i.push(`- ${p} (+${m} / -${g})`)}return[`Files: ${t.length}, Additions: ${o}, Deletions: ${n}`,...i.slice(0,25)].join(`
|
|
69
|
+
`)},"summarizeFiles"),Le=`\u2554\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2557
|
|
85
70
|
\u2502 \u2502
|
|
86
71
|
\u2502 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2502
|
|
87
72
|
\u2502 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D \u2502
|
|
@@ -90,23 +75,28 @@ ${n}`,a=await S(e,t,[{role:"system",content:F(s,o,r)},{role:"user",content:d}],.
|
|
|
90
75
|
\u2502 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2502
|
|
91
76
|
\u2502 \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u2502
|
|
92
77
|
\u2502 \u2502
|
|
93
|
-
\u255A\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u255D`;var
|
|
94
|
-
${
|
|
78
|
+
\u255A\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u255D`;var Ne=l(async(t,e,s,i,o,n)=>(async()=>{console.log(Le),console.log(),V(J(X(" lazycommit "))),await se();const u=D();s&&await k("git",["add","--update"]),u.start("Detecting staged files");const a=await oe(e);if(!a)throw u.stop("Detecting staged files"),new w("No staged changes found. Stage your changes manually, or automatically stage all changes with the `--all` flag.");const c=await ne(e),h=a.diff.length>5e4,y=a.files.length>=5,p=c&&c.fileStats.some(r=>r.changes>500),m=h||y||p;if(m&&c){let r="Large diff detected";y?r="Many files detected":p&&(r="Large file changes detected"),u.stop(`${ie(a.files)} (${c.totalChanges.toLocaleString()} changes):
|
|
79
|
+
${a.files.map(b=>` ${b}`).join(`
|
|
95
80
|
`)}
|
|
96
81
|
|
|
97
|
-
\
|
|
98
|
-
${
|
|
99
|
-
`)}`);const{env:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
`),
|
|
104
|
-
`),f
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
`),
|
|
108
|
-
|
|
109
|
-
`),
|
|
82
|
+
\u{1F3AF} ${r} - using smart grouping for better commit history`)}else u.stop(`${ie(a.files)}:
|
|
83
|
+
${a.files.map(r=>` ${r}`).join(`
|
|
84
|
+
`)}`);const{env:g}=process,d=await U({GROQ_API_KEY:g.GROQ_API_KEY,proxy:g.https_proxy||g.HTTPS_PROXY||g.http_proxy||g.HTTP_PROXY,generate:t?.toString(),type:i?.toString()});if(o||m){let r=Ge(a.files);if(r.length===1&&r[0].files.length>=5){const b=new Map;for(const A of r[0].files){const f=Ye(A)||"general";b.set(f,[...b.get(f)||[],A])}r=Array.from(b.entries()).map(([A,f])=>({type:r[0].type,scope:A,title:`update files in ${A}`,files:f}))}if(r.length===1)x(`${O("Only one group detected, falling back to single commit...")}`),o=!1;else{const b=D();b.start(`Generating commit messages for ${r.length} groups`);try{for(const f of r){const P=await ze(f.files),I=`Generate a single conventional commit message for this group of files. Follow commitlint (type(scope): subject). Keep <= ${d["max-length"]} chars.
|
|
85
|
+
|
|
86
|
+
${P}`;try{const j=await W(d.GROQ_API_KEY,d.model,d.locale,I,1,d["max-length"],"conventional",d.timeout,d.proxy);if(j.length>0)f.message=j[0];else{const ye=f.scope?`(${f.scope})`:"";f.message=`${f.type}${ye}: ${f.title}`}}catch{const j=f.scope?`(${f.scope})`:"";f.message=`${f.type}${j}: ${f.title}`}}}finally{b.stop("Commit messages generated")}console.log(`
|
|
87
|
+
\u{1F4CB} Commit Plan:`),r.forEach((f,P)=>{console.log(`
|
|
88
|
+
${P+1}. ${_(f.message)}`),console.log(` Files: ${f.files.join(", ")}`)});const A=await Z({message:`
|
|
89
|
+
Proceed with ${r.length} commits?`});if(!A||Y(A)){x("Commits cancelled");return}for(let f=0;f<r.length;f++){const P=r[f],I=D();I.start(`Creating commit ${f+1}/${r.length}: ${P.message}`);try{await k("git",["reset","HEAD","--"]),await k("git",["add",...P.files]),await k("git",["commit","-m",P.message,...n])}finally{I.stop(`Commit ${f+1} created`)}}x(`${_("\u2714")} Successfully created ${r.length} commits!`);return}}const K=D();K.start("The AI is analyzing your changes");let C;try{const r=await ue(e,25);if(r)C=await W(d.GROQ_API_KEY,d.model,d.locale,r,d.generate,d["max-length"],d.type,d.timeout,d.proxy);else{const A=`Generate a commit message for these files: ${a.files.join(", ")}`;C=await W(d.GROQ_API_KEY,d.model,d.locale,A,d.generate,d["max-length"],d.type,d.timeout,d.proxy)}}finally{K.stop("Changes analyzed")}if(C.length===0)throw new w("No commit messages were generated. Try again.");let S;if(C.length===1){[S]=C;const r=await Z({message:`Use this commit message?
|
|
90
|
+
|
|
91
|
+
${S}
|
|
92
|
+
`});if(!r||Y(r)){x("Commit cancelled");return}}else{const r=await xe({message:`Pick a commit message to use: ${O("(Ctrl+c to exit)")}`,options:C.map(b=>({label:b,value:b}))});if(Y(r)){x("Commit cancelled");return}S=r}await k("git",["commit","-m",S,...n]),x(`${_("\u2714")} Successfully committed!`)})().catch(u=>{x(`${F("\u2716")} ${u.message}`),M(u),process.exit(1)}),"lazycommit");const[H,Ue]=process.argv.slice(2);var He=l(()=>(async()=>{if(!H)throw new w('Commit message file path is missing. This file should be called from the "prepare-commit-msg" git hook');if(Ue)return;const t=await oe();if(!t)return;V(J(X(" lazycommit ")));const{env:e}=process,s=await U({GROQ_API_KEY:e.GROQ_API_KEY,proxy:e.https_proxy||e.HTTPS_PROXY||e.http_proxy||e.HTTP_PROXY}),i=D();i.start("The AI is analyzing your changes");let o;try{const h=await ue();if(h)o=await W(s.GROQ_API_KEY,s.model,s.locale,h,s.generate,s["max-length"],s.type,s.timeout,s.proxy);else{const p=`Generate a commit message for these files: ${t.files.join(", ")}`;o=await W(s.GROQ_API_KEY,s.model,s.locale,p,s.generate,s["max-length"],s.type,s.timeout,s.proxy)}}finally{i.stop("Changes analyzed")}const u=await $.readFile(H,"utf8")!=="",a=o.length>1;let c="";u&&(c=`# \u{1F916} AI generated commit${a?"s":""}
|
|
93
|
+
`),a?(u&&(c+=`# Select one of the following messages by uncommeting:
|
|
94
|
+
`),c+=`
|
|
95
|
+
${o.map(h=>`# ${h}`).join(`
|
|
96
|
+
`)}`):(u&&(c+=`# Edit the message below and commit:
|
|
97
|
+
`),c+=`
|
|
98
|
+
${o[0]}
|
|
99
|
+
`),await $.appendFile(H,c),x(`${_("\u2714")} Saved commit message!`)})().catch(t=>{x(`${F("\u2716")} ${t.message}`),M(t),process.exit(1)}),"prepareCommitMessageHook"),Qe=B({name:"config",parameters:["<mode>","<key=value...>"]},t=>{(async()=>{const{mode:e,keyValue:s}=t._;if(e==="get"){const i=await U({},!0);for(const o of s)ce(i,o)&&console.log(`${o}=${i[o]}`);return}if(e==="set"){await Se(s.map(i=>i.split("=")));return}throw new w(`Invalid mode: ${e}`)})().catch(e=>{console.error(`${F("\u2716")} ${e.message}`),M(e),process.exit(1)})});const de="prepare-commit-msg",fe=`.git/hooks/${de}`,R=q(new URL("cli.mjs",import.meta.url)),Ke=process.argv[1].replace(/\\/g,"/").endsWith(`/${fe}`),pe=process.platform==="win32",ge=`
|
|
110
100
|
#!/usr/bin/env node
|
|
111
|
-
import(${JSON.stringify(
|
|
112
|
-
`.trim();var
|
|
101
|
+
import(${JSON.stringify(ve(R))})
|
|
102
|
+
`.trim();var Be=B({name:"hook",parameters:["<install/uninstall>"]},t=>{(async()=>{const e=await se(),{installUninstall:s}=t._,i=G.join(e,fe),o=await ae(i);if(s==="install"){if(o){if(await $.realpath(i).catch(()=>{})===R){console.warn("The hook is already installed");return}throw new w(`A different ${de} hook seems to be installed. Please remove it before installing lazycommit.`)}await $.mkdir(G.dirname(i),{recursive:!0}),pe?await $.writeFile(i,ge):(await $.symlink(R,i,"file"),await $.chmod(i,493)),console.log(`${_("\u2714")} Hook installed`);return}if(s==="uninstall"){if(!o){console.warn("Hook is not installed");return}if(pe){if(await $.readFile(i,"utf8")!==ge){console.warn("Hook is not installed");return}}else if(await $.realpath(i)!==R){console.warn("Hook is not installed");return}await $.rm(i),console.log(`${_("\u2714")} Hook uninstalled`);return}throw new w(`Invalid mode: ${s}`)})().catch(e=>{console.error(`${F("\u2716")} ${e.message}`),M(e),process.exit(1)})});const qe=q(import.meta.url),Je=ke(qe),Xe=JSON.parse(be(Ae(Je,"../package.json"),"utf8")),{description:Ve,version:Ze}=Xe,he=process.argv.slice(2);$e({name:"lazycommit",version:Ze,flags:{generate:{type:Number,description:"Number of messages to generate (Warning: generating multiple costs more) (default: 1)",alias:"g"},exclude:{type:[String],description:"Files to exclude from AI analysis",alias:"x"},all:{type:Boolean,description:"Automatically stage changes in tracked files for the commit",alias:"a",default:!1},type:{type:String,description:"Type of commit message to generate",alias:"t"},split:{type:Boolean,description:"Create multiple commits by grouping files logically",alias:"s",default:!1}},commands:[Qe,Be],help:{description:Ve},ignoreArgv:l(t=>t==="unknown-flag"||t==="argument","ignoreArgv")},t=>{Ke?He():Ne(t.flags.generate,t.flags.exclude,t.flags.all,t.flags.type,t.flags.split,he)},he);
|