lazycommitt 1.0.15 → 1.0.17
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 +41 -33
- package/dist/cli.mjs +132 -89
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
+
<div align="center">
|
|
16
|
+
<a href="https://peerlist.io/code_kartik/project/lazycommit" ><img width="400" height="128" alt="LazyCommit" src="https://github.com/user-attachments/assets/5d54d238-38f4-44f2-b253-3959b2487987" /></a>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
15
21
|
## Setup
|
|
16
22
|
|
|
17
23
|
> The minimum supported version of Node.js is v18. Check your Node.js version with `node --version`.
|
|
@@ -101,45 +107,46 @@ lazycommit --type conventional # or -t conventional
|
|
|
101
107
|
|
|
102
108
|
This feature can be useful if your project follows the Conventional Commits standard or if you're using tools that rely on this commit format.
|
|
103
109
|
|
|
104
|
-
####
|
|
110
|
+
#### Review, edit, and confirm messages
|
|
105
111
|
|
|
106
|
-
|
|
112
|
+
Lazycommit now lets you review the generated message, optionally edit it, and then confirm before it is committed.
|
|
107
113
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
114
|
+
- You'll see a menu: Use as-is, Edit, or Cancel
|
|
115
|
+
- If you choose Use as-is, it commits immediately without additional prompts
|
|
116
|
+
- If you choose Edit, you can modify the message; then you'll be asked to confirm the final message before committing
|
|
111
117
|
|
|
112
|
-
|
|
118
|
+
Example (single commit):
|
|
113
119
|
|
|
114
|
-
|
|
120
|
+
```sh
|
|
121
|
+
git add .
|
|
122
|
+
lazycommit
|
|
123
|
+
# Review generated commit message:
|
|
124
|
+
# feat: add lazycommit command
|
|
125
|
+
# → Choose "Use as-is" to commit immediately
|
|
126
|
+
# → Or choose "Edit" to modify, then confirm the final message before commit
|
|
127
|
+
```
|
|
115
128
|
|
|
116
|
-
|
|
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
|
|
129
|
+
#### Exclude files from analysis
|
|
120
130
|
|
|
121
|
-
|
|
131
|
+
You can exclude specific files from AI analysis using the `--exclude` flag:
|
|
122
132
|
|
|
123
133
|
```sh
|
|
124
|
-
|
|
125
|
-
lazycommit
|
|
126
|
-
|
|
127
|
-
# Force grouping even for < 5 files
|
|
128
|
-
lazycommit --split
|
|
134
|
+
lazycommit --exclude package-lock.json --exclude dist/
|
|
129
135
|
```
|
|
130
136
|
|
|
131
137
|
#### Handling large diffs
|
|
132
138
|
|
|
133
|
-
For large commits with many files, lazycommit automatically stays within API limits and
|
|
139
|
+
For large commits with many files, lazycommit automatically stays within API limits and generates relevant commit messages:
|
|
134
140
|
|
|
135
|
-
- **
|
|
136
|
-
- **
|
|
137
|
-
- **Token-safe
|
|
138
|
-
- **
|
|
141
|
+
- **Smart summarization**: Uses `git diff --cached --numstat` to create compact summaries of all changes
|
|
142
|
+
- **Context snippets**: Includes truncated diff snippets from top changed files for better context
|
|
143
|
+
- **Token-safe processing**: Keeps prompts small while maintaining accuracy for 20+ file changes
|
|
144
|
+
- **Single commit**: Always generates one commit message, no matter how many files are staged
|
|
145
|
+
- **Enhanced analysis**: Uses improved prompts and smart truncation for better commit message quality
|
|
139
146
|
|
|
140
147
|
### Git hook
|
|
141
148
|
|
|
142
|
-
You can also integrate _lazycommit_ with Git via the [`prepare-commit-msg`](https://git-scm.com/docs/githooks#_prepare_commit_msg) hook. This lets you use Git like you normally would, and edit the commit message before committing.
|
|
149
|
+
You can also integrate _lazycommit_ with Git via the [`prepare-commit-msg`](https://git-scm.com/docs/githooks#_prepare_commit_msg) hook. This lets you use Git like you normally would, and edit the commit message before committing. The hook uses the same enhanced analysis and quality improvements as the CLI mode.
|
|
143
150
|
|
|
144
151
|
#### Install
|
|
145
152
|
|
|
@@ -168,7 +175,7 @@ lazycommit hook uninstall
|
|
|
168
175
|
|
|
169
176
|
> If you ever want to write your own message instead of generating one, you can simply pass one in: `git commit -m "My message"`
|
|
170
177
|
|
|
171
|
-
2. Lazycommit will generate
|
|
178
|
+
2. Lazycommit will generate a high-quality commit message using the same enhanced analysis as the CLI mode and pass it back to Git. Git will open it with the [configured editor](https://docs.github.com/en/get-started/getting-started-with-git/associating-text-editors-with-git) for you to review/edit it.
|
|
172
179
|
|
|
173
180
|
3. Save and close the editor to commit!
|
|
174
181
|
|
|
@@ -269,10 +276,10 @@ lazycommit config set timeout=20000 # 20s
|
|
|
269
276
|
|
|
270
277
|
The maximum character length of the generated commit message.
|
|
271
278
|
|
|
272
|
-
Default: `
|
|
279
|
+
Default: `100`
|
|
273
280
|
|
|
274
281
|
```sh
|
|
275
|
-
lazycommit config set max-length=
|
|
282
|
+
lazycommit config set max-length=150
|
|
276
283
|
```
|
|
277
284
|
|
|
278
285
|
#### type
|
|
@@ -302,13 +309,14 @@ The tool uses Groq's fast inference API to provide quick and accurate commit mes
|
|
|
302
309
|
|
|
303
310
|
For large commits that exceed API token limits, lazycommit automatically:
|
|
304
311
|
|
|
305
|
-
1. **Detects large/many-file diffs** and switches to
|
|
306
|
-
2. **
|
|
307
|
-
3. **
|
|
308
|
-
4. **
|
|
309
|
-
5.
|
|
312
|
+
1. **Detects large/many-file diffs** and switches to enhanced analysis mode
|
|
313
|
+
2. **Creates compact summaries** using `git diff --cached --numstat` to capture all changes efficiently
|
|
314
|
+
3. **Includes context snippets** from the most changed files to provide semantic context
|
|
315
|
+
4. **Generates a single commit message** that accurately reflects all changes without hitting API limits
|
|
316
|
+
5. **Smart truncation** preserves sentence structure and meaning when messages approach length limits
|
|
317
|
+
6. **Enhanced prompts** provide better context for AI to generate complete, professional commit messages
|
|
310
318
|
|
|
311
|
-
This ensures you can commit large changes (like new features, refactoring, or initial project setup) without hitting API limits, while
|
|
319
|
+
This ensures you can commit large changes (like new features, refactoring, or initial project setup) without hitting API limits, while maintaining accuracy, relevance, and high-quality commit messages.
|
|
312
320
|
|
|
313
321
|
## Troubleshooting
|
|
314
322
|
|
|
@@ -344,7 +352,7 @@ If you get a 413 error, your diff is too large for the API. Try these solutions:
|
|
|
344
352
|
|
|
345
353
|
- **Use the GPT-OSS-20B model** (default): `lazycommit config set model "openai/gpt-oss-20b"`
|
|
346
354
|
- Exclude unnecessary files: `lazycommit --exclude "*.log" --exclude "*.tmp"`
|
|
347
|
-
- Use
|
|
355
|
+
- Use the built-in large diff handling for better context and accuracy
|
|
348
356
|
- Lower generate count: `lazycommit config set generate=1` (default)
|
|
349
357
|
- Reduce timeout: `lazycommit config set timeout=5000` for faster failures
|
|
350
358
|
|
package/dist/cli.mjs
CHANGED
|
@@ -1,72 +1,123 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
2
|
+
var ve=Object.defineProperty;var c=(e,t)=>ve(e,"name",{value:t,configurable:!0});import{command as K,cli as $e}from"cleye";import{readFileSync as Ae}from"fs";import{fileURLToPath as B,pathToFileURL as xe}from"url";import j,{dirname as Ce,join as Ee}from"path";import{execa as x}from"execa";import{dim as Y,bgCyan as J,black as X,green as I,red as P}from"kolorist";import{intro as q,spinner as G,select as V,isCancel as k,outro as A,text as Z,confirm as ee}from"@clack/prompts";import b from"fs/promises";import ke from"os";import te from"ini";import se from"groq-sdk";var Oe="1.0.17",Ie={version:Oe};const H=class H extends Error{};c(H,"KnownError");let y=H;const F=" ",S=c(e=>{e instanceof Error&&!(e instanceof y)&&(e.stack&&console.error(Y(e.stack.split(`
|
|
3
3
|
`).slice(1).join(`
|
|
4
4
|
`))),console.error(`
|
|
5
|
-
${
|
|
6
|
-
${
|
|
7
|
-
`),diff:
|
|
8
|
-
`).filter(Boolean),
|
|
9
|
-
`)},"buildCompactSummary"),
|
|
10
|
-
|
|
11
|
-
${JSON.stringify({feat:"A
|
|
12
|
-
|
|
13
|
-
IMPORTANT:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- Be
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
5
|
+
${F}${Y(`lazycommit v${Ie.version}`)}`),console.error(`
|
|
6
|
+
${F}Please open a Bug report with the information above:`),console.error(`${F}https://github.com/KartikLabhshetwar/lazycommit/issues/new/choose`))},"handleCliError"),ne=c(async()=>{const{stdout:e,failed:t}=await x("git",["rev-parse","--show-toplevel"],{reject:!1});if(t)throw new y("The current directory must be a Git repository!");return e},"assertGitRepo"),D=c(e=>`:(exclude)${e}`,"excludeFromDiff"),U=["package-lock.json","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(D),ae=c(async e=>{const t=["diff","--cached","--diff-algorithm=minimal"],{stdout:s}=await x("git",[...t,"--name-only",...U,...e?e.map(D):[]]);if(!s)return;const{stdout:a}=await x("git",[...t,...U,...e?e.map(D):[]]);return{files:s.split(`
|
|
7
|
+
`),diff:a}},"getStagedDiff"),oe=c(e=>`Detected ${e.length.toLocaleString()} staged file${e.length>1?"s":""}`,"getDetectedMessage"),ue=c(async e=>{const t=["diff","--cached","--diff-algorithm=minimal"],{stdout:s}=await x("git",[...t,"--name-only",...U,...e?e.map(D):[]]);if(!s)return null;const a=s.split(`
|
|
8
|
+
`).filter(Boolean),n=await Promise.all(a.map(async o=>{try{const{stdout:u}=await x("git",[...t,"--numstat","--",o]),[r,d]=u.split(" ").slice(0,2).map(Number);return{file:o,additions:r||0,deletions:d||0,changes:(r||0)+(d||0)}}catch{return{file:o,additions:0,deletions:0,changes:0}}}));return{files:a,fileStats:n,totalChanges:n.reduce((o,u)=>o+u.changes,0)}},"getDiffSummary"),ie=c(async(e,t=20)=>{const s=await ue(e);if(!s)return null;const{fileStats:a}=s,n=[...a].sort((i,f)=>f.changes-i.changes),o=n.slice(0,Math.max(1,t)),u=s.files.length,r=s.totalChanges,d=a.reduce((i,f)=>i+(f.additions||0),0),h=a.reduce((i,f)=>i+(f.deletions||0),0),p=[];p.push(`Files changed: ${u}`),p.push(`Additions: ${d}, Deletions: ${h}, Total changes: ${r}`),p.push("Top files by changes:");for(const i of o)p.push(`- ${i.file} (+${i.additions} / -${i.deletions}, ${i.changes} changes)`);return n.length>o.length&&p.push(`\u2026and ${n.length-o.length} more files`),p.join(`
|
|
9
|
+
`)},"buildCompactSummary"),re=c(e=>b.lstat(e).then(()=>!0,()=>!1),"fileExists"),Pe=["","conventional"],{hasOwnProperty:Se}=Object.prototype,ce=c((e,t)=>Se.call(e,t),"hasOwn"),v=c((e,t,s)=>{if(!t)throw new y(`Invalid config property ${e}: ${s}`)},"parseAssert"),M={GROQ_API_KEY(e){if(!e)throw new y("Please set your Groq API key via `lazycommit config set GROQ_API_KEY=<your token>`");return v("GROQ_API_KEY",e.startsWith("gsk_"),'Must start with "gsk_"'),e},locale(e){return e?(v("locale",e,"Cannot be empty"),v("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;v("generate",/^\d+$/.test(e),"Must be an integer");const t=Number(e);return v("generate",t>0,"Must be greater than 0"),v("generate",t<=5,"Must be less or equal to 5"),t},type(e){return e?(v("type",Pe.includes(e),"Invalid commit type"),e):""},proxy(e){if(!(!e||e.length===0))return v("proxy",/^https?:\/\//.test(e),"Must be a valid URL"),e},model(e){return!e||e.length===0?"openai/gpt-oss-20b":e},timeout(e){if(!e)return 1e4;v("timeout",/^\d+$/.test(e),"Must be an integer");const t=Number(e);return v("timeout",t>=500,"Must be greater than 500ms"),t},"max-length"(e){if(!e)return 100;v("max-length",/^\d+$/.test(e),"Must be an integer");const t=Number(e);return v("max-length",t>=20,"Must be greater than 20 characters"),v("max-length",t<=200,"Must be less than or equal to 200 characters"),t}},W=j.join(ke.homedir(),".lazycommit"),le=c(async()=>{if(!await re(W))return Object.create(null);const t=await b.readFile(W,"utf8");return te.parse(t)},"readConfigFile"),L=c(async(e,t)=>{const s=await le(),a={};for(const n of Object.keys(M)){const o=M[n],u=e?.[n]??s[n];if(t)try{a[n]=o(u)}catch{}else a[n]=o(u)}return a},"getConfig"),De=c(async e=>{const t=await le();for(const[s,a]of e){if(!ce(M,s))throw new y(`Invalid config property: ${s}`);const n=M[s](a);t[s]=n}await b.writeFile(W,te.stringify(t),"utf8")},"setConfigs"),Me={"":"<commit message>",conventional:"<type>(<optional scope>): <commit message>"},me={"":"",conventional:`Choose the most appropriate type from the following categories that best describes the git diff:
|
|
10
|
+
|
|
11
|
+
${JSON.stringify({feat:"A NEW user-facing feature or functionality that adds capabilities",fix:"A bug fix that resolves an existing issue",docs:"Documentation only changes (README, comments, etc)",style:"Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)",refactor:"Code restructuring, improvements, or internal changes that enhance existing functionality",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:"Maintenance tasks, config updates, dependency updates, or internal tooling changes",revert:"Reverts a previous commit"},null,2)}
|
|
12
|
+
|
|
13
|
+
IMPORTANT:
|
|
14
|
+
- Use 'feat' ONLY for NEW user-facing features
|
|
15
|
+
- Use 'refactor' for code improvements, restructuring, or internal changes
|
|
16
|
+
- Use 'chore' for config updates, maintenance, or internal tooling
|
|
17
|
+
- Use the exact type name from the list above.`},fe=c((e,t,s)=>`You are a professional git commit message generator. Generate ONLY conventional commit messages.
|
|
18
|
+
|
|
19
|
+
CRITICAL RULES:
|
|
20
|
+
- Return ONLY the commit message line, nothing else
|
|
21
|
+
- Use format: type: subject (NO scope, just type and subject)
|
|
22
|
+
- Maximum ${t} characters (be concise but complete)
|
|
23
|
+
- Imperative mood, present tense
|
|
24
|
+
- Be specific and descriptive
|
|
25
|
+
- NO explanations, questions, or meta-commentary
|
|
26
|
+
- ALWAYS complete the message - never truncate mid-sentence
|
|
27
|
+
|
|
28
|
+
COMMIT TYPES:
|
|
29
|
+
- feat: NEW user-facing feature or functionality
|
|
30
|
+
- fix: bug fix that resolves an issue
|
|
31
|
+
- docs: documentation changes only
|
|
32
|
+
- style: formatting, no logic change
|
|
33
|
+
- refactor: code restructuring, improvements, or internal changes
|
|
34
|
+
- perf: performance improvements
|
|
35
|
+
- test: adding/updating tests
|
|
36
|
+
- build: build system changes
|
|
37
|
+
- ci: CI/CD changes
|
|
38
|
+
- chore: maintenance tasks, dependencies, config updates
|
|
39
|
+
|
|
40
|
+
QUALITY GUIDELINES:
|
|
41
|
+
- Start with the most important change
|
|
42
|
+
- Use specific, descriptive language
|
|
43
|
+
- Include the main component/area affected
|
|
44
|
+
- Be clear about what was done, not just what files changed
|
|
45
|
+
- Use proper grammar and punctuation
|
|
46
|
+
|
|
47
|
+
EXAMPLES (correct format - NO scope, just type and subject):
|
|
48
|
+
- feat: add user login with OAuth integration
|
|
49
|
+
- fix: resolve memory leak in image processing service
|
|
50
|
+
- refactor: improve message generation with better prompts
|
|
51
|
+
- refactor: increase default max-length from 50 to 100
|
|
52
|
+
- docs: update installation and configuration guide
|
|
53
|
+
- test: add unit tests for JWT token validation
|
|
54
|
+
- chore: update axios to v1.6.0 for security patches
|
|
55
|
+
|
|
56
|
+
WRONG FORMAT (do not use):
|
|
57
|
+
- feat(auth): add user login
|
|
58
|
+
- refactor(commit): improve prompts
|
|
59
|
+
|
|
60
|
+
${me[s]?`
|
|
61
|
+
DETAILED TYPE GUIDELINES:
|
|
62
|
+
${me[s]}`:""}
|
|
63
|
+
|
|
64
|
+
Language: ${e}
|
|
65
|
+
Output format: ${Me[s]||"type: subject"}
|
|
66
|
+
|
|
67
|
+
Generate a single, complete, professional commit message that accurately describes the changes.`,"generatePrompt"),Te=c(async(e,t,s,a,n,o,u,r,d,h,p)=>{const i=new se({apiKey:e,timeout:h});try{return d>1?{choices:(await Promise.all(Array.from({length:d},()=>i.chat.completions.create({model:t,messages:s,temperature:a,top_p:n,frequency_penalty:o,presence_penalty:u,max_tokens:r,n:1})))).flatMap(m=>m.choices)}:await i.chat.completions.create({model:t,messages:s,temperature:a,top_p:n,frequency_penalty:o,presence_penalty:u,max_tokens:r,n:1})}catch(f){if(f instanceof se.APIError){let l=`Groq API Error: ${f.status} - ${f.name}`;throw f.message&&(l+=`
|
|
68
|
+
|
|
69
|
+
${f.message}`),f.status===500&&(l+=`
|
|
70
|
+
|
|
71
|
+
Check the API status: https://console.groq.com/status`),(f.status===413||f.message&&f.message.includes("rate_limit_exceeded"))&&(l+=`
|
|
60
72
|
|
|
61
73
|
\u{1F4A1} Tip: Your diff is too large. Try:
|
|
62
74
|
1. Commit files in smaller batches
|
|
63
75
|
2. Exclude large files with --exclude
|
|
64
76
|
3. Use a different model with --model
|
|
65
|
-
4. Check if you have build artifacts staged (dist/, .next/, etc.)`),new
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
`)
|
|
69
|
-
|
|
77
|
+
4. Check if you have build artifacts staged (dist/, .next/, etc.)`),new y(l)}throw f.code==="ENOTFOUND"?new y(`Error connecting to ${f.hostname} (${f.syscall}). Are you connected to the internet?`):f}},"createChatCompletion"),ge=c(e=>e.trim().replace(/^["']|["']\.?$/g,"").replace(/[\n\r]/g,"").replace(/(\w)\.$/,"$1"),"sanitizeMessage"),de=c((e,t)=>{if(e.length<=t)return e;const s=e.slice(0,t),a=Math.max(s.lastIndexOf(". "),s.lastIndexOf("! "),s.lastIndexOf("? "));if(a>t*.7)return s.slice(0,a+1);const n=Math.max(s.lastIndexOf(", "),s.lastIndexOf("; "));if(n>t*.6)return s.slice(0,n+1);const o=s.lastIndexOf(" ");return o>t*.5?s.slice(0,o):e.length>t+10?s+"...":s},"enforceMaxLength"),_e=c(e=>Array.from(new Set(e)),"deduplicateMessages"),Re=["feat:","fix:","docs:","style:","refactor:","perf:","test:","build:","ci:","chore:","revert:"],Ne=c((e,t)=>{const s=e.replace(/\s+/g," ").trim(),a=s.match(/\b(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)\b\s*:?\s+[^.\n]+/i);let n=a?a[0]:s.split(/[.!?]/)[0];if(!a&&n.length<10){const u=s.split(/[.!?]/).filter(r=>r.trim().length>10);u.length>0&&(n=u[0].trim())}const o=n.toLowerCase();for(const u of Re){const r=u.slice(0,-1);if(o.startsWith(r+" ")&&!o.startsWith(u)){n=r+": "+n.slice(r.length+1);break}}return n=ge(n),!n||n.length<5?null:(n.length>t*1.2&&(n=de(n,t)),n)},"deriveMessageFromReasoning"),T=c(async(e,t,s,a,n,o,u,r,d)=>{const h=a,p=await Te(e,t,[{role:"system",content:fe(s,o,u)},{role:"user",content:h}],.3,1,0,0,Math.max(300,o*12),n,r),i=(p.choices||[]).map(l=>l.message?.content||"").map(l=>ge(l)).filter(Boolean).map(l=>l.length>o*1.1?de(l,o):l).filter(l=>l.length>=10);if(i.length>0)return _e(i);const f=p.choices.map(l=>l.message?.reasoning||"").filter(Boolean);for(const l of f){const m=Ne(l,o);if(m)return[m]}return[]},"generateCommitMessageFromSummary"),je=c(async(e,t=30,s=4e3)=>{try{const a=e.slice(0,5),n=[];let o=s;for(const u of a){const{stdout:r}=await x("git",["diff","--cached","--unified=0","--",u]);if(!r)continue;const d=r.split(`
|
|
78
|
+
`).filter(Boolean),h=[];let p=0;for(const i of d){const f=i.startsWith("@@"),l=(i.startsWith("+")||i.startsWith("-"))&&!i.startsWith("+++")&&!i.startsWith("---");if((f||l)&&(h.push(i),p++,p>=t))break}if(h.length>0){const i=[`# ${u}`,...h].join(`
|
|
79
|
+
`);i.length<=o?(n.push(i),o-=i.length):(n.push(i.slice(0,Math.max(0,o))),o=0)}if(o<=0)break}return n.length===0?"":["Context snippets (truncated):",...n].join(`
|
|
80
|
+
`)}catch{return""}},"buildDiffSnippets"),_=c(async(e,t,s)=>{const a=await je(e,30,3e3);return`Analyze the following git changes and generate a single, complete conventional commit message.
|
|
81
|
+
|
|
82
|
+
CHANGES SUMMARY:
|
|
83
|
+
${t}
|
|
84
|
+
|
|
85
|
+
${a?`
|
|
86
|
+
CODE CONTEXT:
|
|
87
|
+
${a}
|
|
88
|
+
`:""}
|
|
89
|
+
|
|
90
|
+
TASK: Write ONE conventional commit message that accurately describes what was changed.
|
|
91
|
+
|
|
92
|
+
REQUIREMENTS:
|
|
93
|
+
- Format: type: subject (NO scope, just type and subject)
|
|
94
|
+
- Maximum ${s} characters
|
|
95
|
+
- Be specific and descriptive
|
|
96
|
+
- Use imperative mood, present tense
|
|
97
|
+
- Include the main component/area affected
|
|
98
|
+
- Complete the message - never truncate mid-sentence
|
|
99
|
+
|
|
100
|
+
COMMIT TYPE GUIDELINES:
|
|
101
|
+
- feat: NEW user-facing features only
|
|
102
|
+
- refactor: code improvements, restructuring, internal changes
|
|
103
|
+
- fix: bug fixes that resolve issues
|
|
104
|
+
- docs: documentation changes only
|
|
105
|
+
- chore: config updates, maintenance, dependencies
|
|
106
|
+
|
|
107
|
+
EXAMPLES (correct format - NO scope, just type and subject):
|
|
108
|
+
- feat: add user login with OAuth integration
|
|
109
|
+
- fix: resolve memory leak in image processing service
|
|
110
|
+
- refactor: improve message generation with better prompts
|
|
111
|
+
- refactor: increase default max-length from 50 to 100
|
|
112
|
+
- docs: update installation and configuration guide
|
|
113
|
+
- test: add unit tests for JWT token validation
|
|
114
|
+
- chore: update axios to v1.6.0 for security patches
|
|
115
|
+
|
|
116
|
+
WRONG FORMAT (do not use):
|
|
117
|
+
- feat(auth): add user login
|
|
118
|
+
- refactor(commit): improve prompts
|
|
119
|
+
|
|
120
|
+
Return only the commit message line, no explanations.`},"buildSingleCommitPrompt"),Ye=`\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
|
|
70
121
|
\u2502 \u2502
|
|
71
122
|
\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
|
|
72
123
|
\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
|
|
@@ -75,34 +126,26 @@ ${o}`,w=await Ge(t,e,[{role:"system",content:Re(s,n,a)},{role:"user",content:y}]
|
|
|
75
126
|
\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
|
|
76
127
|
\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
|
|
77
128
|
\u2502 \u2502
|
|
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
|
|
79
|
-
${
|
|
129
|
+
\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 Ge=c(async(e,t,s,a,n,o)=>(async()=>{console.log(Ye),console.log(),q(J(X(" lazycommit "))),await ne();const u=G();s&&await x("git",["add","--update"]),u.start("Detecting staged files");const r=await ae(t);if(!r)throw u.stop("Detecting staged files"),new y("No staged changes found. Stage your changes manually, or automatically stage all changes with the `--all` flag.");const d=await ue(t),h=r.diff.length>5e4,p=r.files.length>=5,i=d&&d.fileStats.some(g=>g.changes>500);if((h||p||i)&&d){let g="Large diff detected";p?g="Many files detected":i&&(g="Large file changes detected"),u.stop(`${oe(r.files)} (${d.totalChanges.toLocaleString()} changes):
|
|
130
|
+
${r.files.map(w=>` ${w}`).join(`
|
|
80
131
|
`)}
|
|
81
132
|
|
|
82
|
-
|
|
83
|
-
${
|
|
84
|
-
`)}`);const{env:
|
|
85
|
-
|
|
86
|
-
${
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
`
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
`});if(!X||P(X)){v("Commit cancelled");return}await x("git",["commit","-m",_,...n]),v(`${W("\u2714")} Successfully committed!`)})().catch(a=>{v(`${T("\u2716")} ${a.message}`),R(a),process.exit(1)}),"lazycommit");const[B,Je]=process.argv.slice(2);var Ve=l(()=>(async()=>{if(!B)throw new b('Commit message file path is missing. This file should be called from the "prepare-commit-msg" git hook');if(Je)return;const t=await me();if(!t)return;ae(oe(ne(" lazycommit ")));const{env:e}=process,s=await K({GROQ_API_KEY:e.GROQ_API_KEY,proxy:e.https_proxy||e.HTTPS_PROXY||e.http_proxy||e.HTTP_PROXY}),o=E();o.start("The AI is analyzing your changes");let i;try{const y=await ge();if(y)i=await M(s.GROQ_API_KEY,s.model,s.locale,y,s.generate,s["max-length"],s.type,s.timeout,s.proxy);else{const g=`Generate a commit message for these files: ${t.files.join(", ")}`;i=await M(s.GROQ_API_KEY,s.model,s.locale,g,s.generate,s["max-length"],s.type,s.timeout,s.proxy)}}finally{o.stop("Changes analyzed")}const a=await A.readFile(B,"utf8")!=="",c=i.length>1;let r="";a&&(r=`# \u{1F916} AI generated commit${c?"s":""}
|
|
99
|
-
`),c?(a&&(r+=`# Select one of the following messages by uncommeting:
|
|
100
|
-
`),r+=`
|
|
101
|
-
${i.map(y=>`# ${y}`).join(`
|
|
102
|
-
`)}`):(a&&(r+=`# Edit the message below and commit:
|
|
103
|
-
`),r+=`
|
|
104
|
-
${i[0]}
|
|
105
|
-
`),await A.appendFile(B,r),v(`${W("\u2714")} Saved commit message!`)})().catch(t=>{v(`${T("\u2716")} ${t.message}`),R(t),process.exit(1)}),"prepareCommitMessageHook"),Xe=se({name:"config",parameters:["<mode>","<key=value...>"]},t=>{(async()=>{const{mode:e,keyValue:s}=t._;if(e==="get"){const o=await K({},!0);for(const i of s)he(o,i)&&console.log(`${i}=${o[i]}`);return}if(e==="set"){await Fe(s.map(o=>o.split("=")));return}throw new b(`Invalid mode: ${e}`)})().catch(e=>{console.error(`${T("\u2716")} ${e.message}`),R(e),process.exit(1)})});const be="prepare-commit-msg",ve=`.git/hooks/${be}`,z=ie(new URL("cli.mjs",import.meta.url)),Ze=process.argv[1].replace(/\\/g,"/").endsWith(`/${ve}`),Ae=process.platform==="win32",ke=`
|
|
133
|
+
${g} - using enhanced analysis for better commit message`)}else u.stop(`${oe(r.files)}:
|
|
134
|
+
${r.files.map(g=>` ${g}`).join(`
|
|
135
|
+
`)}`);const{env:l}=process,m=await L({GROQ_API_KEY:l.GROQ_API_KEY,proxy:l.https_proxy||l.HTTPS_PROXY||l.http_proxy||l.HTTP_PROXY,generate:e?.toString(),type:a?.toString()}),Q=G();Q.start("The AI is analyzing your changes");let E;try{const g=await ie(t,25);if(g){const w=await _(r.files,g,m["max-length"]);E=await T(m.GROQ_API_KEY,m.model,m.locale,w,m.generate,m["max-length"],m.type,m.timeout,m.proxy)}else{const w=r.files.join(", "),C=await _(r.files,`Files: ${w}`,m["max-length"]),Xe=fe(m.locale,m["max-length"],m.type);E=await T(m.GROQ_API_KEY,m.model,m.locale,C,m.generate,m["max-length"],m.type,m.timeout,m.proxy)}}finally{Q.stop("Changes analyzed")}if(E.length===0)throw new y("No commit messages were generated. Try again.");let $,N=!1,O=!1;if(E.length===1){[$]=E;const g=await V({message:`Review generated commit message:
|
|
136
|
+
|
|
137
|
+
${$}
|
|
138
|
+
`,options:[{label:"Use as-is",value:"use"},{label:"Edit",value:"edit"},{label:"Cancel",value:"cancel"}]});if(k(g)||g==="cancel"){A("Commit cancelled");return}if(g==="use")O=!0;else if(g==="edit"){const w=await Z({message:"Edit commit message:",initialValue:$,validate:c(C=>C&&C.trim().length>0?void 0:"Message cannot be empty","validate")});if(k(w)){A("Commit cancelled");return}$=String(w).trim(),N=!0}}else{const g=await V({message:`Pick a commit message to use: ${Y("(Ctrl+c to exit)")}`,options:E.map(w=>({label:w,value:w}))});if(k(g)){A("Commit cancelled");return}$=g,O=!0}if(!O&&!N){const g=await ee({message:"Edit the commit message before committing?"});if(g&&!k(g)){const w=await Z({message:"Edit commit message:",initialValue:$,validate:c(C=>C&&C.trim().length>0?void 0:"Message cannot be empty","validate")});if(k(w)){A("Commit cancelled");return}$=String(w).trim(),N=!0}}if(!O){const g=await ee({message:`Proceed with this commit message?
|
|
139
|
+
|
|
140
|
+
${$}
|
|
141
|
+
`});if(!g||k(g)){A("Commit cancelled");return}}await x("git",["commit","-m",$,...o]),A(`${I("\u2714")} Successfully committed!`)})().catch(u=>{A(`${P("\u2716")} ${u.message}`),S(u),process.exit(1)}),"lazycommit");const[z,Fe]=process.argv.slice(2);var Ue=c(()=>(async()=>{if(!z)throw new y('Commit message file path is missing. This file should be called from the "prepare-commit-msg" git hook');if(Fe)return;const e=await ae();if(!e)return;q(J(X(" lazycommit ")));const{env:t}=process,s=await L({GROQ_API_KEY:t.GROQ_API_KEY,proxy:t.https_proxy||t.HTTPS_PROXY||t.http_proxy||t.HTTP_PROXY}),a=G();a.start("The AI is analyzing your changes");let n;try{const h=await ie();if(h){const p=await _(e.files,h,s["max-length"]);n=await T(s.GROQ_API_KEY,s.model,s.locale,p,s.generate,s["max-length"],s.type,s.timeout,s.proxy)}else{const p=e.files.join(", "),i=await _(e.files,`Files: ${p}`,s["max-length"]);n=await T(s.GROQ_API_KEY,s.model,s.locale,i,s.generate,s["max-length"],s.type,s.timeout,s.proxy)}}finally{a.stop("Changes analyzed")}const u=await b.readFile(z,"utf8")!=="",r=n.length>1;let d="";u&&(d=`# \u{1F916} AI generated commit${r?"s":""}
|
|
142
|
+
`),r?(u&&(d+=`# Select one of the following messages by uncommeting:
|
|
143
|
+
`),d+=`
|
|
144
|
+
${n.map(h=>`# ${h}`).join(`
|
|
145
|
+
`)}`):(u&&(d+=`# Edit the message below and commit:
|
|
146
|
+
`),d+=`
|
|
147
|
+
${n[0]}
|
|
148
|
+
`),await b.appendFile(z,d),A(`${I("\u2714")} Saved commit message!`)})().catch(e=>{A(`${P("\u2716")} ${e.message}`),S(e),process.exit(1)}),"prepareCommitMessageHook"),We=K({name:"config",parameters:["<mode>","<key=value...>"]},e=>{(async()=>{const{mode:t,keyValue:s}=e._;if(t==="get"){const a=await L({},!0);for(const n of s)ce(a,n)&&console.log(`${n}=${a[n]}`);return}if(t==="set"){await De(s.map(a=>a.split("=")));return}throw new y(`Invalid mode: ${t}`)})().catch(t=>{console.error(`${P("\u2716")} ${t.message}`),S(t),process.exit(1)})});const pe="prepare-commit-msg",he=`.git/hooks/${pe}`,R=B(new URL("cli.mjs",import.meta.url)),Le=process.argv[1].replace(/\\/g,"/").endsWith(`/${he}`),ye=process.platform==="win32",we=`
|
|
106
149
|
#!/usr/bin/env node
|
|
107
|
-
import(${JSON.stringify(
|
|
108
|
-
`.trim();var
|
|
150
|
+
import(${JSON.stringify(xe(R))})
|
|
151
|
+
`.trim();var ze=K({name:"hook",parameters:["<install/uninstall>"]},e=>{(async()=>{const t=await ne(),{installUninstall:s}=e._,a=j.join(t,he),n=await re(a);if(s==="install"){if(n){if(await b.realpath(a).catch(()=>{})===R){console.warn("The hook is already installed");return}throw new y(`A different ${pe} hook seems to be installed. Please remove it before installing lazycommit.`)}await b.mkdir(j.dirname(a),{recursive:!0}),ye?await b.writeFile(a,we):(await b.symlink(R,a,"file"),await b.chmod(a,493)),console.log(`${I("\u2714")} Hook installed`);return}if(s==="uninstall"){if(!n){console.warn("Hook is not installed");return}if(ye){if(await b.readFile(a,"utf8")!==we){console.warn("Hook is not installed");return}}else if(await b.realpath(a)!==R){console.warn("Hook is not installed");return}await b.rm(a),console.log(`${I("\u2714")} Hook uninstalled`);return}throw new y(`Invalid mode: ${s}`)})().catch(t=>{console.error(`${P("\u2716")} ${t.message}`),S(t),process.exit(1)})});const He=B(import.meta.url),Qe=Ce(He),Ke=JSON.parse(Ae(Ee(Qe,"../package.json"),"utf8")),{description:Be,version:Je}=Ke,be=process.argv.slice(2);$e({name:"lazycommit",version:Je,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:[We,ze],help:{description:Be},ignoreArgv:c(e=>e==="unknown-flag"||e==="argument","ignoreArgv")},e=>{Le?Ue():Ge(e.flags.generate,e.flags.exclude,e.flags.all,e.flags.type,e.flags.split,be)},be);
|