lazycommitt 1.0.15 → 1.0.16

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.
Files changed (3) hide show
  1. package/README.md +35 -33
  2. package/dist/cli.mjs +132 -89
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -101,45 +101,46 @@ lazycommit --type conventional # or -t conventional
101
101
 
102
102
  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
103
 
104
- #### Exclude files from analysis
104
+ #### Review, edit, and confirm messages
105
105
 
106
- You can exclude specific files from AI analysis using the `--exclude` flag:
106
+ Lazycommit now lets you review the generated message, optionally edit it, and then confirm before it is committed.
107
107
 
108
- ```sh
109
- lazycommit --exclude package-lock.json --exclude dist/
110
- ```
108
+ - You'll see a menu: Use as-is, Edit, or Cancel
109
+ - If you choose Use as-is, it commits immediately without additional prompts
110
+ - If you choose Edit, you can modify the message; then you'll be asked to confirm the final message before committing
111
111
 
112
- #### Automatic multi-commit mode
112
+ Example (single commit):
113
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.
114
+ ```sh
115
+ git add .
116
+ lazycommit
117
+ # Review generated commit message:
118
+ # feat: add lazycommit command
119
+ # → Choose "Use as-is" to commit immediately
120
+ # → Or choose "Edit" to modify, then confirm the final message before commit
121
+ ```
115
122
 
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
123
+ #### Exclude files from analysis
120
124
 
121
- Usage:
125
+ You can exclude specific files from AI analysis using the `--exclude` flag:
122
126
 
123
127
  ```sh
124
- # Just run as usual; grouping triggers automatically when applicable
125
- lazycommit
126
-
127
- # Force grouping even for < 5 files
128
- lazycommit --split
128
+ lazycommit --exclude package-lock.json --exclude dist/
129
129
  ```
130
130
 
131
131
  #### Handling large diffs
132
132
 
133
- For large commits with many files, lazycommit automatically stays within API limits and maintains clean history:
133
+ For large commits with many files, lazycommit automatically stays within API limits and generates relevant commit messages:
134
134
 
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
135
+ - **Smart summarization**: Uses `git diff --cached --numstat` to create compact summaries of all changes
136
+ - **Context snippets**: Includes truncated diff snippets from top changed files for better context
137
+ - **Token-safe processing**: Keeps prompts small while maintaining accuracy for 20+ file changes
138
+ - **Single commit**: Always generates one commit message, no matter how many files are staged
139
+ - **Enhanced analysis**: Uses improved prompts and smart truncation for better commit message quality
139
140
 
140
141
  ### Git hook
141
142
 
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.
143
+ 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
144
 
144
145
  #### Install
145
146
 
@@ -168,7 +169,7 @@ lazycommit hook uninstall
168
169
 
169
170
  > 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
171
 
171
- 2. Lazycommit will generate the commit message for you 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
+ 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
173
 
173
174
  3. Save and close the editor to commit!
174
175
 
@@ -269,10 +270,10 @@ lazycommit config set timeout=20000 # 20s
269
270
 
270
271
  The maximum character length of the generated commit message.
271
272
 
272
- Default: `50`
273
+ Default: `100`
273
274
 
274
275
  ```sh
275
- lazycommit config set max-length=100
276
+ lazycommit config set max-length=150
276
277
  ```
277
278
 
278
279
  #### type
@@ -302,13 +303,14 @@ The tool uses Groq's fast inference API to provide quick and accurate commit mes
302
303
 
303
304
  For large commits that exceed API token limits, lazycommit automatically:
304
305
 
305
- 1. **Detects large/many-file diffs** and switches to a scalable flow
306
- 2. **Groups files** by conventional type/scope; if only one large bucket remains, **auto-splits by second-level directory** (e.g., `app/api/<group>/...`)
307
- 3. **Generates messages per group** using compact `git diff --cached --numstat` summaries (not full diffs)
308
- 4. **Commits sequentially** per group with clear, conventional messages
309
- 5. When a single commit is requested, **uses compact summaries** to generate conventional messages efficiently
306
+ 1. **Detects large/many-file diffs** and switches to enhanced analysis mode
307
+ 2. **Creates compact summaries** using `git diff --cached --numstat` to capture all changes efficiently
308
+ 3. **Includes context snippets** from the most changed files to provide semantic context
309
+ 4. **Generates a single commit message** that accurately reflects all changes without hitting API limits
310
+ 5. **Smart truncation** preserves sentence structure and meaning when messages approach length limits
311
+ 6. **Enhanced prompts** provide better context for AI to generate complete, professional commit messages
310
312
 
311
- This ensures you can commit large changes (like new features, refactoring, or initial project setup) without hitting API limits, while keeping a clean history.
313
+ 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
314
 
313
315
  ## Troubleshooting
314
316
 
@@ -344,7 +346,7 @@ If you get a 413 error, your diff is too large for the API. Try these solutions:
344
346
 
345
347
  - **Use the GPT-OSS-20B model** (default): `lazycommit config set model "openai/gpt-oss-20b"`
346
348
  - Exclude unnecessary files: `lazycommit --exclude "*.log" --exclude "*.tmp"`
347
- - Use automatic multi-commit mode to split large changes into logical groups
349
+ - Use the built-in large diff handling for better context and accuracy
348
350
  - Lower generate count: `lazycommit config set generate=1` (default)
349
351
  - Reduce timeout: `lazycommit config set timeout=5000` for faster failures
350
352
 
package/dist/cli.mjs CHANGED
@@ -1,72 +1,123 @@
1
1
  #!/usr/bin/env node
2
- var Ce=Object.defineProperty;var l=(t,e)=>Ce(t,"name",{value:e,configurable:!0});import{command as se,cli as Pe}from"cleye";import{readFileSync as _e}from"fs";import{fileURLToPath as ie,pathToFileURL as De}from"url";import N,{dirname as Se,join as We}from"path";import{execa as x}from"execa";import{dim as F,bgCyan as oe,black as ne,green as W,red as T}from"kolorist";import{intro as ae,spinner as E,outro as v,confirm as I,isCancel as P,text as L,select as ue}from"@clack/prompts";import A from"fs/promises";import Ee from"os";import ce from"ini";import re from"groq-sdk";var Ie="1.0.15",Me={version:Ie};const q=class q extends Error{};l(q,"KnownError");let b=q;const U=" ",R=l(t=>{t instanceof Error&&!(t instanceof b)&&(t.stack&&console.error(F(t.stack.split(`
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.16",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
- ${U}${F(`lazycommit v${Me.version}`)}`),console.error(`
6
- ${U}Please open a Bug report with the information above:`),console.error(`${U}https://github.com/KartikLabhshetwar/lazycommit/issues/new/choose`))},"handleCliError"),le=l(async()=>{const{stdout:t,failed:e}=await x("git",["rev-parse","--show-toplevel"],{reject:!1});if(e)throw new b("The current directory must be a Git repository!");return t},"assertGitRepo"),G=l(t=>`:(exclude)${t}`,"excludeFromDiff"),H=["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(G),me=l(async t=>{const e=["diff","--cached","--diff-algorithm=minimal"],{stdout:s}=await x("git",[...e,"--name-only",...H,...t?t.map(G):[]]);if(!s)return;const{stdout:o}=await x("git",[...e,...H,...t?t.map(G):[]]);return{files:s.split(`
7
- `),diff:o}},"getStagedDiff"),de=l(t=>`Detected ${t.length.toLocaleString()} staged file${t.length>1?"s":""}`,"getDetectedMessage"),fe=l(async t=>{const e=["diff","--cached","--diff-algorithm=minimal"],{stdout:s}=await x("git",[...e,"--name-only",...H,...t?t.map(G):[]]);if(!s)return null;const o=s.split(`
8
- `).filter(Boolean),i=await Promise.all(o.map(async n=>{try{const{stdout:a}=await x("git",[...e,"--numstat","--",n]),[c,r]=a.split(" ").slice(0,2).map(Number);return{file:n,additions:c||0,deletions:r||0,changes:(c||0)+(r||0)}}catch{return{file:n,additions:0,deletions:0,changes:0}}}));return{files:o,fileStats:i,totalChanges:i.reduce((n,a)=>n+a.changes,0)}},"getDiffSummary"),ge=l(async(t,e=20)=>{const s=await fe(t);if(!s)return null;const{fileStats:o}=s,i=[...o].sort((g,m)=>m.changes-g.changes),n=i.slice(0,Math.max(1,e)),a=s.files.length,c=s.totalChanges,r=o.reduce((g,m)=>g+(m.additions||0),0),y=o.reduce((g,m)=>g+(m.deletions||0),0),w=[];w.push(`Files changed: ${a}`),w.push(`Additions: ${r}, Deletions: ${y}, Total changes: ${c}`),w.push("Top files by changes:");for(const g of n)w.push(`- ${g.file} (+${g.additions} / -${g.deletions}, ${g.changes} changes)`);return i.length>n.length&&w.push(`\u2026and ${i.length-n.length} more files`),w.join(`
9
- `)},"buildCompactSummary"),pe=l(t=>A.lstat(t).then(()=>!0,()=>!1),"fileExists"),je=["","conventional"],{hasOwnProperty:Oe}=Object.prototype,he=l((t,e)=>Oe.call(t,e),"hasOwn"),k=l((t,e,s)=>{if(!e)throw new b(`Invalid config property ${t}: ${s}`)},"parseAssert"),Y={GROQ_API_KEY(t){if(!t)throw new b("Please set your Groq API key via `lazycommit config set GROQ_API_KEY=<your token>`");return k("GROQ_API_KEY",t.startsWith("gsk_"),'Must start with "gsk_"'),t},locale(t){return t?(k("locale",t,"Cannot be empty"),k("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;k("generate",/^\d+$/.test(t),"Must be an integer");const e=Number(t);return k("generate",e>0,"Must be greater than 0"),k("generate",e<=5,"Must be less or equal to 5"),e},type(t){return t?(k("type",je.includes(t),"Invalid commit type"),t):""},proxy(t){if(!(!t||t.length===0))return k("proxy",/^https?:\/\//.test(t),"Must be a valid URL"),t},model(t){return!t||t.length===0?"openai/gpt-oss-20b":t},timeout(t){if(!t)return 1e4;k("timeout",/^\d+$/.test(t),"Must be an integer");const e=Number(t);return k("timeout",e>=500,"Must be greater than 500ms"),e},"max-length"(t){if(!t)return 50;k("max-length",/^\d+$/.test(t),"Must be an integer");const e=Number(t);return k("max-length",e>=20,"Must be greater than 20 characters"),e}},Q=N.join(Ee.homedir(),".lazycommit"),ye=l(async()=>{if(!await pe(Q))return Object.create(null);const e=await A.readFile(Q,"utf8");return ce.parse(e)},"readConfigFile"),K=l(async(t,e)=>{const s=await ye(),o={};for(const i of Object.keys(Y)){const n=Y[i],a=t?.[i]??s[i];if(e)try{o[i]=n(a)}catch{}else o[i]=n(a)}return o},"getConfig"),Fe=l(async t=>{const e=await ye();for(const[s,o]of t){if(!he(Y,s))throw new b(`Invalid config property: ${s}`);const i=Y[s](o);e[s]=i}await A.writeFile(Q,ce.stringify(e),"utf8")},"setConfigs"),Te={"":"<commit message>",conventional:"<type>(<optional scope>): <commit message>"},we={"":"",conventional:`Choose the most appropriate type from the following categories that best describes the git diff:
10
-
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)}
12
-
13
- IMPORTANT: Use the exact type name from the list above.`},Re=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.
14
-
15
- ## Instructions:
16
- 1. Analyze the provided git diff carefully
17
- 2. Identify the primary purpose and impact of the changes
18
- 3. Generate a commit message that clearly describes what was changed and why
19
- 4. Use present tense, imperative mood (e.g., "feat: Add feature" not "Added feature")
20
- 5. Be specific about what changed, not just how it changed
21
- 6. Focus on the business value or technical improvement
22
-
23
- ## Quality Guidelines:
24
- - Be concise but descriptive
25
- - Use active voice
26
- - Avoid vague terms like "update", "change", "fix stuff"
27
- - Include context when helpful (e.g., "fix: memory leak in user authentication")
28
- - For bug fixes, briefly describe what was broken
29
- - For features, describe what functionality was added
30
- - For refactoring, mention what was improved (performance, readability, etc.)
31
-
32
- ## Language: ${t}
33
- ## Maximum length: ${e} characters
34
- ## Output format: ${Te[s]||"<commit message>"}
35
-
36
- ${we[s]?`
37
- ## Commit Type Guidelines:
38
- ${we[s]}`:""}
39
-
40
- ## Examples of good commit messages:
41
- - feat: Add user authentication with JWT tokens
42
- - fix: Fix memory leak in image processing pipeline
43
- - refactor: Refactor database queries to use prepared statements
44
- - docs: Update README with installation instructions
45
- - style: Remove deprecated API endpoints
46
- - perf: Optimize bundle size by removing unused dependencies
47
-
48
- ## Examples of bad commit messages:
49
- - Update code
50
- - Fix bug
51
- - Changes
52
- - WIP
53
- - Stuff
54
-
55
- Remember: Your response will be used directly as the git commit message. Do NOT wrap the message in quotes. Make it professional and informative.`,"generatePrompt"),Ge=l(async(t,e,s,o,i,n,a,c,r,y,w)=>{const g=new re({apiKey:t,timeout:y});try{return r>1?{choices:(await Promise.all(Array.from({length:r},()=>g.chat.completions.create({model:e,messages:s,temperature:o,top_p:i,frequency_penalty:n,presence_penalty:a,max_tokens:c,n:1})))).flatMap(d=>d.choices)}:await g.chat.completions.create({model:e,messages:s,temperature:o,top_p:i,frequency_penalty:n,presence_penalty:a,max_tokens:c,n:1})}catch(m){if(m instanceof re.APIError){let p=`Groq API Error: ${m.status} - ${m.name}`;throw m.message&&(p+=`
56
-
57
- ${m.message}`),m.status===500&&(p+=`
58
-
59
- Check the API status: https://console.groq.com/status`),(m.status===413||m.message&&m.message.includes("rate_limit_exceeded"))&&(p+=`
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 b(p)}throw m.code==="ENOTFOUND"?new b(`Error connecting to ${m.hostname} (${m.syscall}). Are you connected to the internet?`):m}},"createChatCompletion"),$e=l(t=>t.trim().replace(/^["']|["']\.?$/g,"").replace(/[\n\r]/g,"").replace(/(\w)\.$/,"$1"),"sanitizeMessage"),Ye=l(t=>Array.from(new Set(t)),"deduplicateMessages"),ze=["feat:","fix:","docs:","style:","refactor:","perf:","test:","build:","ci:","chore:","revert:"],Ne=l((t,e)=>{const s=t.replace(/\s+/g," ").trim(),o=s.match(/\b(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)\b\s*:?\s+[^.\n]+/i);let i=o?o[0]:s.split(/[.!?]/)[0];const n=i.toLowerCase();for(const a of ze){const c=a.slice(0,-1);if(n.startsWith(c+" ")&&!n.startsWith(a)){i=c+": "+i.slice(c.length+1);break}}return i=$e(i),i?(i.length>e&&(i=i.slice(0,e)),i):null},"deriveMessageFromReasoning"),M=l(async(t,e,s,o,i,n,a,c,r)=>{const y=`This is a compact summary of staged changes. Generate a single, concise commit message within ${n} characters that reflects the overall intent.
66
-
67
- ${o}`,w=await Ge(t,e,[{role:"system",content:Re(s,n,a)},{role:"user",content:y}],.7,1,0,0,Math.max(200,n*8),i,c),g=(w.choices||[]).map(p=>p.message?.content||"").map(p=>$e(p)).filter(Boolean);if(g.length>0)return Ye(g);const m=w.choices.map(p=>p.message?.reasoning||"").filter(Boolean);for(const p of m){const d=Ne(p,n);if(d)return[d]}return[]},"generateCommitMessageFromSummary"),Le=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"),Ue=l(t=>{const e=t.split("/");if(e.length>1)return e[0]},"topLevelScope"),He=l(t=>{const e=new Map;for(const i of t){const n=Le(i),a=n.scope||Ue(i),c=`${n.type}:${a||"general"}`,r=e.get(c)||{meta:{type:n.type,scope:a},files:[]};r.files.push(i),e.set(c,r)}const s=[];for(const{meta:i,files:n}of e.values()){const{type:a,scope:c}=i;let r="update files";a==="docs"?r="update documentation":a==="ci"?r="update CI workflows":a==="build"?r="update build/configuration":a==="test"?r="update tests":a==="feat"?r="add or update features":a==="chore"&&(r="maintenance updates");const y=c?`${r} in ${c}`:r;s.push({type:a,scope:c,title:y,files:n})}const o={feat:0,fix:1,refactor:2,perf:3,test:4,docs:5,build:6,ci:7,chore:8};return s.sort((i,n)=>(o[i.type]??99)-(o[n.type]??99)),s},"groupFiles"),Qe=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"),Ke=l(async t=>{if(t.length===0)return"";const e=["diff","--cached","--numstat","--",...t],{stdout:s}=await x("git",e),o=[];let i=0,n=0;const a=s.split(`
68
- `).filter(Boolean);for(const r of a){const[y,w,g]=r.split(" "),m=Number(y)||0,p=Number(w)||0;i+=m,n+=p,o.push(`- ${g} (+${m} / -${p})`)}return[`Files: ${t.length}, Additions: ${i}, Deletions: ${n}`,...o.slice(0,25)].join(`
69
- `)},"summarizeFiles"),Be=`\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
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 qe=l(async(t,e,s,o,i,n)=>(async()=>{console.log(Be),console.log(),ae(oe(ne(" lazycommit "))),await le();const a=E();s&&await x("git",["add","--update"]),a.start("Detecting staged files");const c=await me(e);if(!c)throw a.stop("Detecting staged files"),new b("No staged changes found. Stage your changes manually, or automatically stage all changes with the `--all` flag.");const r=await fe(e),y=c.diff.length>5e4,w=c.files.length>=5,g=r&&r.fileStats.some(u=>u.changes>500),m=y||w||g;if(m&&r){let u="Large diff detected";w?u="Many files detected":g&&(u="Large file changes detected"),a.stop(`${de(c.files)} (${r.totalChanges.toLocaleString()} changes):
79
- ${c.files.map(h=>` ${h}`).join(`
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
- \u{1F3AF} ${u} - using smart grouping for better commit history`)}else a.stop(`${de(c.files)}:
83
- ${c.files.map(u=>` ${u}`).join(`
84
- `)}`);const{env:p}=process,d=await K({GROQ_API_KEY:p.GROQ_API_KEY,proxy:p.https_proxy||p.HTTPS_PROXY||p.http_proxy||p.HTTP_PROXY,generate:t?.toString(),type:o?.toString()});if(i||m){let u=He(c.files);if(u.length===1&&u[0].files.length>=5){const h=new Map;for(const $ of u[0].files){const f=Qe($)||"general";h.set(f,[...h.get(f)||[],$])}u=Array.from(h.entries()).map(([$,f])=>({type:u[0].type,scope:$,title:`update files in ${$}`,files:f}))}if(u.length===1)v(`${F("Only one group detected, falling back to single commit...")}`),i=!1;else{const h=E();h.start(`Generating commit messages for ${u.length} groups`);try{for(const f of u){const D=await Ke(f.files),j=`Generate a single conventional commit message for this group of files. Follow commitlint (type(scope): subject). Keep <= ${d["max-length"]} chars.
85
-
86
- ${D}`;try{const C=await M(d.GROQ_API_KEY,d.model,d.locale,j,1,d["max-length"],"conventional",d.timeout,d.proxy);if(C.length>0)f.message=C[0];else{const O=f.scope?`(${f.scope})`:"";f.message=`${f.type}${O}: ${f.title}`}}catch{const C=f.scope?`(${f.scope})`:"";f.message=`${f.type}${C}: ${f.title}`}}}finally{h.stop("Commit messages generated")}console.log(`
87
- \u{1F4CB} Commit Plan:`),u.forEach((f,D)=>{console.log(`
88
- ${D+1}. ${W(f.message)}`),console.log(` Files: ${f.files.join(", ")}`)});const $=await I({message:`
89
- Proceed with ${u.length} commits?`});if(!$||P($)){v("Commits cancelled");return}for(let f=0;f<u.length;f++){const D=u[f],j=E();j.start(`Creating commit ${f+1}/${u.length}: ${D.message}`);try{let C=D.message;const O=await I({message:`Edit commit ${f+1} message before committing?`});if(O&&!P(O)){const ee=await L({message:"Edit commit message:",initialValue:C,validate:l(te=>te&&te.trim().length>0?void 0:"Message cannot be empty","validate")});if(P(ee)){v("Commits cancelled");return}C=String(ee).trim(),D.message=C}const Z=await I({message:`Proceed with commit ${f+1}/${u.length} message?
90
-
91
- ${C}
92
- `});if(!Z||P(Z)){v("Commits cancelled");return}await x("git",["reset","HEAD","--"]),await x("git",["add",...D.files]),await x("git",["commit","-m",C,...n])}finally{j.stop(`Commit ${f+1} created`)}}v(`${W("\u2714")} Successfully created ${u.length} commits!`);return}}const J=E();J.start("The AI is analyzing your changes");let S;try{const u=await ge(e,25);if(u)S=await M(d.GROQ_API_KEY,d.model,d.locale,u,d.generate,d["max-length"],d.type,d.timeout,d.proxy);else{const $=`Generate a commit message for these files: ${c.files.join(", ")}`;S=await M(d.GROQ_API_KEY,d.model,d.locale,$,d.generate,d["max-length"],d.type,d.timeout,d.proxy)}}finally{J.stop("Changes analyzed")}if(S.length===0)throw new b("No commit messages were generated. Try again.");let _,V=!1;if(S.length===1){[_]=S;const u=await ue({message:`Review generated commit message:
93
-
94
- ${_}
95
- `,options:[{label:"Use as-is",value:"use"},{label:"Edit",value:"edit"},{label:"Cancel",value:"cancel"}]});if(P(u)||u==="cancel"){v("Commit cancelled");return}if(u==="edit"){const h=await L({message:"Edit commit message:",initialValue:_,validate:l($=>$&&$.trim().length>0?void 0:"Message cannot be empty","validate")});if(P(h)){v("Commit cancelled");return}_=String(h).trim(),V=!0}}else{const u=await ue({message:`Pick a commit message to use: ${F("(Ctrl+c to exit)")}`,options:S.map(h=>({label:h,value:h}))});if(P(u)){v("Commit cancelled");return}_=u}if(!V){const u=await I({message:"Edit the commit message before committing?"});if(u&&!P(u)){const h=await L({message:"Edit commit message:",initialValue:_,validate:l($=>$&&$.trim().length>0?void 0:"Message cannot be empty","validate")});if(P(h)){v("Commit cancelled");return}_=String(h).trim()}}const X=await I({message:`Proceed with this commit message?
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(De(z))})
108
- `.trim();var et=se({name:"hook",parameters:["<install/uninstall>"]},t=>{(async()=>{const e=await le(),{installUninstall:s}=t._,o=N.join(e,ve),i=await pe(o);if(s==="install"){if(i){if(await A.realpath(o).catch(()=>{})===z){console.warn("The hook is already installed");return}throw new b(`A different ${be} hook seems to be installed. Please remove it before installing lazycommit.`)}await A.mkdir(N.dirname(o),{recursive:!0}),Ae?await A.writeFile(o,ke):(await A.symlink(z,o,"file"),await A.chmod(o,493)),console.log(`${W("\u2714")} Hook installed`);return}if(s==="uninstall"){if(!i){console.warn("Hook is not installed");return}if(Ae){if(await A.readFile(o,"utf8")!==ke){console.warn("Hook is not installed");return}}else if(await A.realpath(o)!==z){console.warn("Hook is not installed");return}await A.rm(o),console.log(`${W("\u2714")} Hook uninstalled`);return}throw new b(`Invalid mode: ${s}`)})().catch(e=>{console.error(`${T("\u2716")} ${e.message}`),R(e),process.exit(1)})});const tt=ie(import.meta.url),st=Se(tt),it=JSON.parse(_e(We(st,"../package.json"),"utf8")),{description:ot,version:nt}=it,xe=process.argv.slice(2);Pe({name:"lazycommit",version:nt,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:[Xe,et],help:{description:ot},ignoreArgv:l(t=>t==="unknown-flag"||t==="argument","ignoreArgv")},t=>{Ze?Ve():qe(t.flags.generate,t.flags.exclude,t.flags.all,t.flags.type,t.flags.split,xe)},xe);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lazycommitt",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "description": "Writes your git commit messages for you with AI using Groq",
5
5
  "main": "index.js",
6
6
  "keywords": [