pushai 1.0.1 → 1.0.3

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 +16 -26
  2. package/dist/index.mjs +12 -12
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -16,15 +16,14 @@
16
16
 
17
17
  ### Features
18
18
 
19
- - **Multi‑Provider** – Google Gemini, OpenAI, HuggingFace, and any OpenAI‑compatible local endpoint (Ollama, LM Studio).
20
- - **Privacy First** – Run completely locally by connecting to your own LLM instance.
19
+ - **Multi‑Provider** – Google Gemini, OpenAI, and HuggingFace.
21
20
  - **Conventional Commits** – Generates standardised, readable messages (`feat:`, `fix:`, `docs:`, etc.).
22
21
  - **Auto‑Push** – Stages all changes, commits with the generated message, and pushes to remote in one seamless flow.
23
22
  - **Smart Init** – Automatically detects missing Git repositories and offers to initialise them.
24
23
  - **Interactive Approval** – Review, edit, or regenerate the commit message before anything is pushed.
25
24
  - **Dry‑Run Mode** – Preview the generated message without committing or pushing.
26
-
27
- ---
25
+ - **Test Mode** – Verify your API key and provider configuration with `pai test`.
26
+ - **Progressive Timeouts** – Clear feedback when operations take longer than expected (10s, 60s).
28
27
 
29
28
  ### Installation
30
29
 
@@ -54,8 +53,6 @@ bun install -g pushai
54
53
 
55
54
  After global installation, you can use the shorthand `pai` (recommended).
56
55
 
57
- ---
58
-
59
56
  ### Quick Start
60
57
 
61
58
  1. **Configure your AI provider**
@@ -78,20 +75,16 @@ After global installation, you can use the shorthand `pai` (recommended).
78
75
  - Show you the message and ask for confirmation.
79
76
  - Commit and push (if you approve).
80
77
 
81
- ---
82
-
83
78
  ### Commands
84
79
 
85
- | Command | Description |
86
- | ---------------------- | ---------------------------------------------------------------------- |
87
- | `pai commit` | Stage, generate, approve, commit, and push. |
88
- | `pai commit --dry-run` | Generate a commit message and show it, but do not commit or push. |
89
- | `pai config` | Interactive setup: choose provider, model, and set API key / base URL. |
90
- | `pai reset` | Deletes all local configuration and API keys from your system. |
91
- | `pai --version` / `-v` | Display the installed version. |
92
- | `pai --help` / `-h` | Show help for all commands. |
93
-
94
- ---
80
+ | Command | Description |
81
+ | ---------------------- | ----------------------------------------------------------------- |
82
+ | `pai commit` | Stage, generate, approve, commit, and push. |
83
+ | `pai commit --dry-run` | Generate a commit message and show it, but do not commit or push. |
84
+ | `pai config` | Interactive setup: choose provider, model, and set API key. |
85
+ | `pai reset` | Deletes all local configuration and API keys from your system. |
86
+ | `pai --version` / `-v` | Display the installed version. |
87
+ | `pai --help` / `-h` | Show help for all commands. |
95
88
 
96
89
  ### Configuration
97
90
 
@@ -104,28 +97,25 @@ Example `config.json`:
104
97
  ```json
105
98
  {
106
99
  "provider": "openai",
107
- "model": "gpt-4o",
108
- "baseUrl": "http://localhost:11434/v1" // optional, for local Ollama
100
+ "model": "gpt-4o"
109
101
  }
110
102
  ```
111
103
 
112
104
  > **Note:** The API key is stored securely via `keytar` and never appears in the config file.
113
105
 
114
- ---
115
-
116
106
  ### How It Works
117
107
 
118
108
  1. **`pai` / `pai commit`**
119
109
  - Checks if you are inside a Git repository.
120
- - Stages all changes (`git add .`).
110
+ - Stages all changes (`git add -A`).
121
111
  - Sends the diff to your chosen AI provider with a specialised prompt.
122
112
  - Returns a conventional commit message (`feat(scope): description`).
113
+ - If the operation takes longer than 10 seconds, you’ll see a friendly warning; after 60 seconds, a further alert appears.
123
114
 
124
115
  2. **Interactive approval**
125
- - You see the generated message inside a box.
116
+ - You see the generated message inside a clean note box.
126
117
  - Options: accept, edit, regenerate, or cancel.
127
- - If you accept, the tool commits and pushes to the current remote branch.
118
+ - The “regenerate” action asks for a **different** message (increased temperature + extra prompt instruction).
128
119
 
129
120
  3. **Security**
130
121
  - API keys are stored in the system keychain, not in plain text.
131
- - Local endpoints (`baseUrl`) keep all data on your machine.
package/dist/index.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import{Command as Ce}from"commander";import C from"chalk";import{confirm as se,intro as me,isCancel as ce,outro as A}from"@clack/prompts";import Q from"os";import O from"path";import f from"fs";import F from"keytar";var L="pushai",U="api-key";async function K(){let t=E(),o={};if(f.existsSync(t))try{o=JSON.parse(f.readFileSync(t,"utf-8"))}catch{}let r=await F.getPassword(L,U);return r&&(o.apiKey=r),o}async function z(t){let o=E(),{apiKey:r,...a}=t;r&&await F.setPassword(L,U,r);let c={};if(f.existsSync(o))try{c=JSON.parse(f.readFileSync(o,"utf-8"))}catch{}let d={...c,...a};f.writeFileSync(o,JSON.stringify(d,null,2),"utf-8")}async function V(){try{let t=Q.homedir(),o=O.join(t,".config","pushai"),r=!1;return f.existsSync(o)&&(f.rmSync(o,{recursive:!0,force:!0}),r=!0),await F.deletePassword(L,U),r}catch(t){return console.error("Failed to reset config:",t),!1}}function E(){let t=O.join(Q.homedir(),".config","pushai");return f.existsSync(t)||f.mkdirSync(t,{recursive:!0}),O.join(t,"config.json")}import h from"chalk";var e={common:{operationCancelled:"Operation cancelled.",interrupted:"Request interrupted.",dryRun:{header:"[DRY RUN] No changes were committed or pushed.",proposed:t=>`Here's the generated commit message:
3
- ${t}`},noRemote:{header:"There's no Git remote repository configured.",instruction:"Try adding a remote repository (e.g. `git remote add origin <url>`) and run the command again."},success:{committed:"Changes committed successfully.",pushed:"Changes pushed successfully."}},commit:{intro:(t,o)=>`${h.cyan("\u25CF")} ${h.bgCyan.black.bold(` ${t.toUpperCase()} `)} ${h.dim("\u2022")} ${h.white(o)}
4
- `,outro:"Successfully synced with the remote repository.",noteTitle:"Commit message preview",gitRepoMissing:"There's no Git repository in this directory.",initConfirm:"Would you like to initialize a new Git repository?",initSuccess:"Git repository initialized successfully.",abortNoRepo:"A Git repository is required to continue.",noChanges:"No changes were found to commit.",generating:"Putting together a commit message",generated:"Commit message ready.",generationFailed:"Couldn't generate a commit message.",generationCancelled:"Commit message generation cancelled.",actionPrompt:"What would you like to do next?",actions:{accept:"Commit and push changes",edit:"Edit commit message",regenerate:"Generate another message",cancel:"Cancel operation"},editPrompt:"Update the commit message:",regenerating:"Regenerating a new commit message",regenerated:"New commit message ready.",regenerationFailed:t=>`Couldn't generate another message: ${t}`,processStopped:"Process stopped. No changes were committed or pushed.",committing:"Creating commit",pushing:"Pushing changes to remote",operationFailed:"Something went wrong while completing the operation.",gitError:t=>`Git reported an error: ${t}`},config:{intro:"Welcome to PushAI. Let's get things set up.",outro:"Configuration complete. You're ready to go!",providerPrompt:"Choose from the available AI providers:",apiKeyPrompt:t=>`Enter your ${h.cyan.bold(t)} API key:`,apiKeyRequired:"You'll need an API key to continue. Simply copy and paste to proceed.",modelPrompt:"Which model would you like to use?",customModelSeparator:"Use a custom model ID",customModelPrompt:"Enter the custom model ID:",customModelRequired:"A model ID is required.",baseUrlPrompt:"Enter a base URL",baseUrlPromptPlaceholder:"Optional \u2014 press Enter to skip",saved:"Configuration saved successfully.",providerLabel:t=>`${h.white.bold("Provider:")} ${t}`,modelLabel:t=>`${h.white.bold("Model: ")} ${t}`,apiKeyLabel:t=>`${h.white.bold("API Key: ")} ****${t}`,configFile:t=>`
5
- File: ${t}`,commandsHint:"Here are a few commands to try:",hintCommit:"Generate AI-powered commit messages",hintConfig:"Update provider or API settings",hintReset:"Clear saved configuration"},reset:{intro:"Resetting PushAI configuration",confirm:"Remove all PushAI configurations and API keys?",outro:"PushAI configuration removed successfully.",nothingToDelete:"There's no configuration data to remove."},errors:{authInvalid:"Authentication failed. The API key or token appears to be invalid.",authFix:"Try running `pai reset` to update your credentials.",unknown:t=>`
6
- ${t||"Something unexpected went wrong."}`}};async function W(){me(C.yellow.bold(e.reset.intro));try{let t=await se({message:C.red(e.reset.confirm),initialValue:!1});if(ce(t)){A(C.red(e.common.operationCancelled));return}if(!t){A(C.red(e.common.operationCancelled));return}await V()?A(C.green.bold(e.reset.outro)):A(C.dim(e.reset.nothingToDelete))}catch(t){if(t.name==="ExitPromptError"||t.name==="AbortError"){A(C.red(e.common.operationCancelled));return}throw t}}import l from"chalk";import{select as X,password as de,isCancel as S,text as ee,intro as ge,note as te,outro as I}from"@clack/prompts";var y=class{apiKey;model;constructor(o,r){this.apiKey=o,this.model=r}};var x=(t,o=!1)=>{let r=`You are a Senior Software Engineer. Your task is to write a concise, technical, and impactful commit message based on a git diff.
2
+ import{Command as Ie}from"commander";import C from"chalk";import{confirm as de,intro as ge,isCancel as ue,outro as T}from"@clack/prompts";import Z from"os";import _ from"path";import h from"fs";import B from"keytar";var D="pushai",q="api-key";async function U(){let t=M(),o={};if(h.existsSync(t))try{o=JSON.parse(h.readFileSync(t,"utf-8"))}catch{}let i=await B.getPassword(D,q);return i&&(o.apiKey=i),o}async function X(t){let o=M(),{apiKey:i,...l}=t;i&&await B.setPassword(D,q,i);let s={};if(h.existsSync(o))try{s=JSON.parse(h.readFileSync(o,"utf-8"))}catch{}let f={...s,...l};h.writeFileSync(o,JSON.stringify(f,null,2),"utf-8")}async function ee(){try{let t=Z.homedir(),o=_.join(t,".config","pushai"),i=!1;return h.existsSync(o)&&(h.rmSync(o,{recursive:!0,force:!0}),i=!0),await B.deletePassword(D,q),i}catch(t){return console.error("Failed to reset config:",t),!1}}function M(){let t=_.join(Z.homedir(),".config","pushai");return h.existsSync(t)||h.mkdirSync(t,{recursive:!0}),_.join(t,"config.json")}import y from"chalk";var e={common:{operationCancelled:"Operation cancelled.",interrupted:"Request interrupted.",dryRun:{header:"[DRY RUN] No changes were committed or pushed.",proposed:t=>`Here's the generated commit message:
3
+ ${t}`},noRemote:{header:"There's no Git remote repository configured.",instruction:"Try adding a remote repository (e.g. `git remote add origin <url>`) and run the command again."},success:{committed:"Changes committed successfully.",pushed:"Changes pushed successfully."}},commit:{intro:(t,o)=>`${y.cyan("\u25CF")} ${y.bgCyan.black.bold(` ${t.toUpperCase()} `)} ${y.dim("\u2022")} ${y.white(o)}
4
+ `,outro:"Successfully synced with the remote repository.",noteTitle:"Commit message preview",gitRepoMissing:"There's no Git repository in this directory.",initConfirm:"Would you like to initialize a new Git repository?",initSuccess:"Git repository initialized successfully.",abortNoRepo:"A Git repository is required to continue.",noChanges:"No changes were found to commit.",generating:"Putting together a commit message..",generatingSlow10:"Please wait.. This might take a moment.",generatingSlow60:"This is taking longer than usual..",generated:"Commit message generated.",generationFailed:"Couldn't generate a commit message.",generationCancelled:"Commit message generation cancelled.",actionPrompt:"What would you like to do next?",actions:{accept:"Commit and push changes",edit:"Edit commit message",regenerate:"Generate another message",cancel:"Cancel operation"},editPrompt:"Update the commit message:",regenerating:"Regenerating a new commit message..",regenerated:"New commit message ready.",regenerationFailed:t=>`Couldn't generate another message: ${t}`,processStopped:"Process stopped. No changes were committed or pushed.",committing:"Creating commit..",committingSlow10:"Almost there..",committingSlow60:"Still processing your request..",pushing:"Pushing changes to remote..",pushingSlow10:"Syncing changes..",pushingSlow60:"Still syncing changes.. Please wait.",operationFailed:"Something went wrong while completing the operation.",gitError:t=>`Git reported an error: ${t}`},config:{intro:"Welcome to PushAI. Let's get things set up.",outro:"Configuration complete. You're ready to go!",providerPrompt:"Choose from the available AI providers:",apiKeyPrompt:t=>`Enter your ${y.cyan.bold(t)} API key:`,apiKeyRequired:"You'll need an API key to continue. Simply copy and paste to proceed.",modelPrompt:"Which model would you like to use?",customModelSeparator:"Use a custom model ID",customModelPrompt:"Enter the custom model ID:",customModelRequired:"A model ID is required.",baseUrlPrompt:"Enter a base URL",baseUrlPromptPlaceholder:"Optional \u2014 press Enter to skip",saved:"Configuration saved successfully.",providerLabel:t=>`${y.white.bold("Provider:")} ${t}`,modelLabel:t=>`${y.white.bold("Model: ")} ${t}`,apiKeyLabel:t=>`${y.white.bold("API Key: ")} ****${t}`,configFile:t=>`
5
+ File: ${t}`,commandsHint:"Here are a few commands to try:",hintCommit:"Generate AI-powered commit messages",hintConfig:"Update provider or API settings",hintReset:"Clear saved configuration"},reset:{intro:"Resetting PushAI configuration",confirm:"Remove all PushAI configurations and API keys?",outro:"PushAI configuration removed successfully.",nothingToDelete:"There's no configuration data to remove."},test:{intro:"Testing API connection...",success:"API connection successful! Your configuration is working.",failure:"API connection failed. Please check your API key and network.",generating:"Sending test request...",generated:"Test request completed successfully."},errors:{authInvalid:"Authentication failed. The API key or token appears to be invalid.",authFix:"Try running `pai reset` to update your credentials.",unknown:t=>`
6
+ ${t||"Something unexpected went wrong."}`}};async function te(){ge(C.yellow.bold(e.reset.intro));try{let t=await de({message:C.red(e.reset.confirm),initialValue:!1});if(ue(t)){T(C.red(e.common.operationCancelled));return}if(!t){T(C.red(e.common.operationCancelled));return}await ee()?T(C.green.bold(e.reset.outro)):T(C.dim(e.reset.nothingToDelete))}catch(t){if(t.name==="ExitPromptError"||t.name==="AbortError"){T(C.red(e.common.operationCancelled));return}throw t}}import c from"chalk";import{select as re,password as he,isCancel as F,text as ye,intro as we,note as ne,outro as E}from"@clack/prompts";var w=class{apiKey;model;constructor(o,i){this.apiKey=o,this.model=i}};var P=(t,o=!1)=>{let i=`You are a Senior Software Engineer. Your task is to write a concise, technical, and impactful commit message based on a git diff.
7
7
 
8
8
  ### CONSTRAINTS
9
9
  - Format: <type>(<scope>): <description>
@@ -19,12 +19,12 @@ ${t||"Something unexpected went wrong."}`}};async function W(){me(C.yellow.bold(
19
19
  - If multiple changes are present, focus on the most significant one.
20
20
 
21
21
  ### GIT DIFF TO ANALYZE:
22
- ${t}`;return o&&(r+=`
22
+ ${t}`;return o&&(i+=`
23
23
 
24
24
  ### ADDITIONAL INSTRUCTION FOR REGENERATION:
25
- You already generated a commit message for this diff. Now please provide a **different, alternative** commit message. It should still follow the same constraints but approach the change from a slightly different perspective or emphasize a different aspect of the changes.`),r};var T=class extends y{async generateCommitMessage(o,r,a){let c=a?.regenerate||!1,d=`https://generativelanguage.googleapis.com/v1beta/models/${this.model}:generateContent?key=${this.apiKey}`,s={contents:[{parts:[{text:x(o,c)}]}],generationConfig:{temperature:c?.8:.2,maxOutputTokens:100}},p=await fetch(d,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s),signal:r});if(!p.ok){let m=await p.text(),u=`Gemini API error (${p.status})`;try{let n=JSON.parse(m);n.error?.message&&(u=n.error.message)}catch{u=m}throw new Error(u)}return((await p.json()).candidates?.[0]?.content?.parts?.[0]?.text||"").trim().replace(/['"]/g,"")}};import{InferenceClient as le}from"@huggingface/inference";var k=class extends y{async generateCommitMessage(o,r,a){let c=a?.regenerate||!1;return((await new le(this.apiKey,{endpointUrl:"https://router.huggingface.co/v1"}).chatCompletion({model:this.model,messages:[{role:"user",content:x(o,c)}],max_tokens:100,temperature:c?.8:.2},{signal:r})).choices[0]?.message.content||"").trim().replace(/['"]/g,"").replace(/^commit:\s*/i,"")}};import pe from"openai";var M=class extends y{baseUrl;constructor(o,r,a){super(o,r),this.baseUrl=a}async generateCommitMessage(o,r,a){let c=a?.regenerate||!1;return(await new pe({apiKey:this.apiKey,baseURL:this.baseUrl||void 0}).chat.completions.create({model:this.model,messages:[{role:"user",content:x(o,c)}],temperature:c?.8:.7,max_tokens:100},{signal:r})).choices[0]?.message.content?.trim().replace(/['"]/g,"")||""}};var G=[{name:"Google Gemini",value:"gemini",models:[{name:"Gemini 3.1 Flash Lite",value:"gemini-3.1-flash-lite",hint:"free \u2022 recommended"},{name:"Gemini Flash Lite Latest",value:"gemini-flash-lite-latest",hint:"free"},{name:"Gemini 2.0 Flash Lite",value:"gemini-2.0-flash-lite",hint:"free"},{name:"Gemini 2.5 Flash",value:"gemini-2.5-flash",hint:"free \u2022 fast"},{name:"Gemini 2.5 Pro",value:"gemini-2.5-pro",hint:"paid \u2022 best quality"},{name:"Gemini 1.5 Flash",value:"gemini-1.5-flash",hint:"free \u2022 stable"},{name:"Gemini 1.5 Pro",value:"gemini-1.5-pro",hint:"paid"}]},{name:"OpenAI",value:"openai",models:[{name:"GPT-4.1 Mini",value:"gpt-4.1-mini",hint:"paid \u2022 recommended"},{name:"GPT-4.1",value:"gpt-4.1",hint:"paid \u2022 best quality"},{name:"GPT-4.1 Nano",value:"gpt-4.1-nano",hint:"paid \u2022 lightweight"},{name:"GPT-4o",value:"gpt-4o",hint:"paid \u2022 multimodal"},{name:"GPT-4o Mini",value:"gpt-4o-mini",hint:"free \u2022 fast"},{name:"GPT-4 Turbo",value:"gpt-4-turbo",hint:"paid"}]},{name:"Hugging Face",value:"huggingface",models:[{name:"Llama 3 8B Instruct",value:"meta-llama/Meta-Llama-3-8B-Instruct",hint:"free \u2022 recommended"},{name:"Llama 3.1 8B Instruct",value:"meta-llama/Llama-3.1-8B-Instruct",hint:"free"},{name:"Llama 3.1 70B Instruct",value:"meta-llama/Llama-3.1-70B-Instruct",hint:"free \u2022 high quality"},{name:"Mistral 7B Instruct",value:"mistralai/Mistral-7B-Instruct-v0.3",hint:"free \u2022 lightweight"},{name:"Mixtral 8x7B Instruct",value:"mistralai/Mixtral-8x7B-Instruct-v0.1",hint:"free \u2022 powerful"},{name:"Qwen 2 7B Instruct",value:"Qwen/Qwen2-7B-Instruct",hint:"free"},{name:"Qwen 2.5 7B Instruct",value:"Qwen/Qwen2.5-7B-Instruct",hint:"free \u2022 improved"},{name:"DeepSeek Coder 33B",value:"deepseek-ai/deepseek-coder-33b-instruct",hint:"free \u2022 coding"},{name:"Phi-3 Mini 4K",value:"microsoft/Phi-3-mini-4k-instruct",hint:"free \u2022 compact"}]}];function Z(t){switch(t.provider){case"gemini":return Promise.resolve(new T(t.apiKey,t.model));case"huggingface":return Promise.resolve(new k(t.apiKey,t.model));case"openai":case"custom":return Promise.resolve(new M(t.apiKey,t.model,t.baseUrl));default:throw new Error(`Provider ${t.provider} is not supported.`)}}async function N(){ge(l.blue.bold(e.config.intro));try{let t=await X({message:e.config.providerPrompt,options:G.map(n=>({label:n.name,value:n.value}))});if(S(t)){I(l.red(e.common.operationCancelled));return}let o=t,r=G.find(n=>n.value===o),a=await de({message:e.config.apiKeyPrompt(o),mask:"*",validate:n=>{if(!n||n.trim()==="")return e.config.apiKeyRequired}});if(S(a)){I(l.red(e.common.operationCancelled));return}let c=a,d=await X({message:e.config.modelPrompt,options:[...r?.models.map(n=>({label:n.name,value:n.value,hint:n.hint}))||[],{label:e.config.customModelSeparator,value:"custom_id"}]});if(S(d)){I(l.red(e.common.operationCancelled));return}let s=d;if(s==="custom_id"){let n=await ee({message:e.config.customModelPrompt,placeholder:"...",validate:J=>{if(!J||J.trim()==="")return e.config.customModelRequired}});if(S(n)){I(l.red(e.common.operationCancelled));return}s=n}let p=await ee({message:e.config.baseUrlPrompt,defaultValue:"...",placeholder:e.config.baseUrlPromptPlaceholder});if(S(p)){I(l.red(e.common.operationCancelled));return}let v=p;await z({provider:o,apiKey:c,model:s,baseUrl:v==="..."?void 0:v});let P=E(),m=`${e.config.providerLabel(G.find(n=>o===n.value)?.name||o)}
26
- ${e.config.modelLabel(s)}
27
- ${e.config.apiKeyLabel(c.slice(-4))}
28
- ${l.dim(e.config.configFile(P))}`;te(m,l.cyan(e.config.saved));let u=`${l.white.bold("pai commit:")} ${l.white(e.config.hintCommit)}
29
- ${l.white.bold("pai config:")} ${l.white(e.config.hintConfig)}
30
- ${l.white.bold("pai reset:")} ${l.white(e.config.hintReset)}`;te(u,l.cyan(e.config.commandsHint)),I(l.green.bold(e.config.outro))}catch(t){if(t.name==="ExitPromptError"){console.log(l.dim(e.common.operationCancelled));return}throw t}}import fe from"ora";import i from"chalk";import he from"simple-git";import{confirm as ye,select as we,text as ve,isCancel as q,note as re,outro as g}from"@clack/prompts";import B from"chalk";function D(t){if(t.name==="ExitPromptError")return;t.message?.includes("API key not valid")||t.message?.includes("Authorization header")||[400,401,403].includes(t.status)?(console.log(B.red(e.errors.authInvalid)),console.log(B.yellow(e.errors.authFix))):console.log(B.red(e.errors.unknown(t.message)))}import{simpleGit as ue}from"simple-git";var R=ue();async function _(){if(!await R.checkIsRepo())return{isRepo:!1};let o=await R.status();return(o.not_added.length>0||o.modified.length>0||o.deleted.length>0)&&await R.add("--all"),{isRepo:!0,diff:await R.diff(["--cached"])}}async function oe(){await R.init()}async function ie(){try{return(await R.getRemotes()).length>0}catch{return!1}}var ne=he();async function $(t=!1,o){let r=new AbortController;o?.(r);try{let a=await K();if(!a.apiKey||!a.provider||!a.model){await N(),Object.assign(a,await K());return}else{let m=a.provider.toUpperCase();console.log(e.commit.intro(m,a.model))}let c=await _();if(!c.isRepo){console.log(i.yellow(e.commit.gitRepoMissing));let m=await ye({message:e.commit.initConfirm,initialValue:!0});q(m)&&(g(i.red(e.common.operationCancelled)),process.exit(0)),m?(await oe(),console.log(i.green(e.commit.initSuccess)),c=await _()):(g(i.dim(e.commit.abortNoRepo)),process.exit(0))}let d=c.diff;d||(g(i.red(e.commit.noChanges)),process.exit(0));let s=fe({color:"cyan"}),p="",v;try{s.start(i.blue(e.commit.generating)),v=await Z(a),p=await v.generateCommitMessage(d,r.signal),s.succeed(i.green(e.commit.generated))}catch(m){s.fail(i.red.bold(e.commit.generationFailed)),m.name==="AbortError"&&(g(i.yellow(e.commit.generationCancelled)),process.exit(0)),D(m),process.exit(1)}let P=!1;for(;!P;){re(i.bold(p),e.commit.noteTitle);let m=await we({message:e.commit.actionPrompt,options:[{label:e.commit.actions.accept,value:"accept"},{label:e.commit.actions.edit,value:"edit"},{label:e.commit.actions.regenerate,value:"regenerate"},{label:e.commit.actions.cancel,value:"cancel"}]});q(m)&&(g(i.dim(e.commit.processStopped)),process.exit(0));let u=m;if(u==="accept")P=!0;else if(u==="edit"){let n=await ve({message:e.commit.editPrompt,initialValue:p});q(n)&&(g(i.dim(e.commit.processStopped)),process.exit(0)),p=n,P=!0}else if(u==="regenerate"){s.start(i.blue(e.commit.regenerating));try{p=await v.generateCommitMessage(d,r.signal,{regenerate:!0}),s.succeed(i.green(e.commit.regenerated))}catch(n){s.fail(i.red(e.commit.regenerationFailed(n.message))),n.name==="AbortError"&&(g(i.yellow(e.commit.generationCancelled)),process.exit(0))}}else g(i.dim(e.commit.processStopped)),process.exit(0)}if(t&&(re(i.dim(e.common.dryRun.proposed(p)),i.yellow(e.common.dryRun.header)),process.exit(0)),!t){await ie()||(s.fail(i.red.bold(e.common.noRemote.header)),g(i.yellow(e.common.noRemote.instruction)),process.exit(1));try{s.start(i.blue(e.commit.committing)),await ne.commit(p),await new Promise(m=>setTimeout(m,2e3)),s.succeed(i.green(e.common.success.committed)),s.start(i.blue(e.commit.pushing)),await ne.push(),s.succeed(i.green.bold(e.common.success.pushed)),g(i.green(e.commit.outro))}catch(m){s.fail(i.red.bold(e.commit.operationFailed)),g(i.red(e.commit.gitError(m.message))),process.exit(1)}}}catch(a){(a.name==="ExitPromptError"||a.name==="AbortError")&&(g(i.red(e.common.operationCancelled)),process.exit(0)),D(a),process.exit(1)}finally{o?.(null)}}var Y="pushai",ae="1.0.0",H="Stop writing manual commit messages.";import be from"chalk";var w=null,b=new Ce;b.name(Y).description(H).option("--dry-run","Generate commit message but do not commit or push").action(async t=>{await $(t.dryRun,o=>{w=o}),w=null});b.name(Y).description(H).version(ae,"-v, --version","output the version number");b.action(async()=>{await $(!1,t=>{w=t}),w=null});b.command("commit").description("Stage changes, generate a message, and push").option("--dry-run","Generate commit message but do not commit or push").action(async t=>{await $(t.dryRun,o=>{w=o}),w=null});b.command("config").description("Configure AI providers and API keys").action(N);b.command("reset").description("Delete the local config.json file").action(W);process.on("SIGINT",()=>{console.log(be.yellow(e.common.interrupted)),w?w.abort():process.exit(0)});b.parse();
25
+ You already generated a commit message for this diff. Now please provide a **different, alternative** commit message. It should still follow the same constraints but approach the change from a slightly different perspective or emphasize a different aspect of the changes.`),i};var G=class extends w{async generateCommitMessage(o,i,l){let s=l?.regenerate||!1,f=`https://generativelanguage.googleapis.com/v1beta/models/${this.model}:generateContent?key=${this.apiKey}`,n={contents:[{parts:[{text:P(o,s)}]}],generationConfig:{temperature:s?.8:.2,maxOutputTokens:100}},u=await fetch(f,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n),signal:i});if(!u.ok){let a=await u.text(),p=`Gemini API error (${u.status})`;try{let x=JSON.parse(a);x.error?.message&&(p=x.error.message)}catch{p=a}throw new Error(p)}return((await u.json()).candidates?.[0]?.content?.parts?.[0]?.text||"").trim().replace(/['"]/g,"")}};import{InferenceClient as oe}from"@huggingface/inference";var N=class extends w{async generateCommitMessage(o,i,l){let s=l?.regenerate||!1;try{return((await new oe(this.apiKey,{endpointUrl:"https://router.huggingface.co/v1"}).chatCompletion({model:this.model,messages:[{role:"user",content:P(o,s)}],max_tokens:100,temperature:s?.8:.2},{signal:i})).choices[0]?.message.content||"").trim().replace(/['"]/g,"").replace(/^commit:\s*/i,"")}catch{try{return(await new oe(this.apiKey).textGeneration({model:this.model,inputs:P(o,s),parameters:{max_new_tokens:100,temperature:s?.8:.2,return_full_text:!1}},{signal:i})).generated_text.trim().replace(/['"]/g,"").replace(/^commit:\s*/i,"")}catch{throw new Error(`Model "${this.model}" does not support chat or text generation. For Hugging Face, please use a model that supports "text-generation" or "conversational", e.g., "mistralai/Mistral-7B-Instruct-v0.3" or "microsoft/Phi-3-mini-4k-instruct".`)}}}};import fe from"openai";var $=class extends w{constructor(o,i){super(o,i)}async generateCommitMessage(o,i,l){let s=l?.regenerate||!1;return(await new fe({apiKey:this.apiKey}).chat.completions.create({model:this.model,messages:[{role:"user",content:P(o,s)}],temperature:s?.8:.7,max_tokens:100},{signal:i})).choices[0]?.message.content?.trim().replace(/['"]/g,"")||""}};var O=[{name:"Google Gemini",value:"gemini",models:[{name:"Gemini 3.1 Flash Lite",value:"gemini-3.1-flash-lite",hint:"free \u2022 recommended"},{name:"Gemini Flash Lite Latest",value:"gemini-flash-lite-latest",hint:"free"},{name:"Gemini 2.0 Flash Lite",value:"gemini-2.0-flash-lite",hint:"free"},{name:"Gemini 2.5 Flash",value:"gemini-2.5-flash",hint:"free \u2022 fast"},{name:"Gemini 2.5 Pro",value:"gemini-2.5-pro",hint:"paid \u2022 best quality"},{name:"Gemini 1.5 Flash",value:"gemini-1.5-flash",hint:"free \u2022 stable"},{name:"Gemini 1.5 Pro",value:"gemini-1.5-pro",hint:"paid"}]},{name:"OpenAI",value:"openai",models:[{name:"GPT-4.1 Mini",value:"gpt-4.1-mini",hint:"paid \u2022 recommended"},{name:"GPT-4.1",value:"gpt-4.1",hint:"paid \u2022 best quality"},{name:"GPT-4.1 Nano",value:"gpt-4.1-nano",hint:"paid \u2022 lightweight"},{name:"GPT-4o",value:"gpt-4o",hint:"paid \u2022 multimodal"},{name:"GPT-4o Mini",value:"gpt-4o-mini",hint:"free \u2022 fast"},{name:"GPT-4 Turbo",value:"gpt-4-turbo",hint:"paid"}]},{name:"Hugging Face",value:"huggingface",models:[{name:"Llama 3 8B Instruct",value:"meta-llama/Meta-Llama-3-8B-Instruct",hint:"free \u2022 recommended"},{name:"Llama 3.1 8B Instruct",value:"meta-llama/Llama-3.1-8B-Instruct",hint:"free"},{name:"Llama 3.1 70B Instruct",value:"meta-llama/Llama-3.1-70B-Instruct",hint:"free \u2022 high quality"},{name:"Mistral 7B Instruct",value:"mistralai/Mistral-7B-Instruct-v0.3",hint:"free \u2022 lightweight"},{name:"Mixtral 8x7B Instruct",value:"mistralai/Mixtral-8x7B-Instruct-v0.1",hint:"free \u2022 powerful"},{name:"Qwen 2 7B Instruct",value:"Qwen/Qwen2-7B-Instruct",hint:"free"},{name:"Qwen 2.5 7B Instruct",value:"Qwen/Qwen2.5-7B-Instruct",hint:"free \u2022 improved"},{name:"DeepSeek Coder 33B",value:"deepseek-ai/deepseek-coder-33b-instruct",hint:"free \u2022 coding"},{name:"Phi-3 Mini 4K",value:"microsoft/Phi-3-mini-4k-instruct",hint:"free \u2022 compact"}]}];function ie(t){switch(t.provider){case"gemini":return Promise.resolve(new G(t.apiKey,t.model));case"huggingface":return Promise.resolve(new N(t.apiKey,t.model));case"openai":return Promise.resolve(new $(t.apiKey,t.model));default:throw new Error(`Provider ${t.provider} is not supported.`)}}async function L(){we(c.blue.bold(e.config.intro));try{let t=await re({message:e.config.providerPrompt,options:O.map(a=>({label:a.name,value:a.value}))});if(F(t)){E(c.red(e.common.operationCancelled));return}let o=t,i=O.find(a=>a.value===o),l=await he({message:e.config.apiKeyPrompt(o),mask:"*",validate:a=>{if(!a||a.trim()==="")return e.config.apiKeyRequired}});if(F(l)){E(c.red(e.common.operationCancelled));return}let s=l,f=await re({message:e.config.modelPrompt,options:[...i?.models.map(a=>({label:a.name,value:a.value,hint:a.hint}))||[],{label:e.config.customModelSeparator,value:"custom_id"}]});if(F(f)){E(c.red(e.common.operationCancelled));return}let n=f;if(n==="custom_id"){let a=await ye({message:e.config.customModelPrompt,placeholder:"...",validate:p=>{if(!p||p.trim()==="")return e.config.customModelRequired}});if(F(a)){E(c.red(e.common.operationCancelled));return}n=a}await X({provider:o,apiKey:s,model:n});let u=M(),S=`${e.config.providerLabel(O.find(a=>o===a.value)?.name||o)}
26
+ ${e.config.modelLabel(n)}
27
+ ${e.config.apiKeyLabel(s.slice(-4))}
28
+ ${c.dim(e.config.configFile(u))}`;ne(S,c.cyan(e.config.saved));let m=`${c.white.bold("pai commit:")} ${c.white(e.config.hintCommit)}
29
+ ${c.white.bold("pai config:")} ${c.white(e.config.hintConfig)}
30
+ ${c.white.bold("pai reset:")} ${c.white(e.config.hintReset)}`;ne(m,c.cyan(e.config.commandsHint)),E(c.green.bold(e.config.outro))}catch(t){if(t.name==="ExitPromptError"){console.log(c.dim(e.common.operationCancelled));return}throw t}}import Ce from"ora";import r from"chalk";import Pe from"simple-git";import{confirm as be,select as Se,text as xe,isCancel as J,note as ce,outro as g}from"@clack/prompts";import j from"chalk";function Y(t){if(t.name==="ExitPromptError")return;t.message?.includes("API key not valid")||t.message?.includes("Authorization header")||[400,401,403].includes(t.status)?(console.log(j.red(e.errors.authInvalid)),console.log(j.yellow(e.errors.authFix))):console.log(j.red(e.errors.unknown(t.message)))}import{simpleGit as ve}from"simple-git";var I=ve();async function H(){if(!await I.checkIsRepo())return{isRepo:!1};let o=await I.status();return(o.not_added.length>0||o.modified.length>0||o.deleted.length>0)&&await I.add("--all"),{isRepo:!0,diff:await I.diff(["--cached"])}}async function ae(){await I.init()}async function se(){try{return(await I.getRemotes()).length>0}catch{return!1}}import me from"chalk";function k(t,o,i){let l=setTimeout(()=>{t.isSpinning&&(t.color="yellow",t.text=me.yellow(o))},1e4),s=setTimeout(()=>{t.isSpinning&&(t.color="red",t.text=me.red(i))},6e4);return()=>{clearTimeout(l),clearTimeout(s),t.color="cyan"}}var le=Pe();async function K(t=!1,o){let i=new AbortController;process.stdin.setRawMode?.(!0),process.stdin.resume(),process.stdin.setEncoding("utf8");let l=()=>{i.abort(),g(r.red(e.common.operationCancelled)),process.exit(0)},s=n=>{n===""&&l()};process.stdin.on("data",s),o?.(i);let f=()=>{i.abort(),g(r.red(e.common.operationCancelled)),process.exit(0)};process.once("SIGINT",f);try{let n=await U();if(!n.apiKey||!n.provider||!n.model){await L(),Object.assign(n,await U());return}console.log(e.commit.intro(n.provider.toUpperCase(),n.model));let u=await H();if(!u.isRepo){console.log(r.yellow(e.commit.gitRepoMissing));let d=await be({message:e.commit.initConfirm,initialValue:!0});J(d)&&(g(r.red(e.common.operationCancelled)),process.exit(0)),d?(await ae(),console.log(r.green(e.commit.initSuccess)),u=await H()):(g(r.dim(e.commit.abortNoRepo)),process.exit(0))}let S=u.diff;S||(g(r.red(e.commit.noChanges)),process.exit(0));let m=Ce({color:"cyan"}),a,p="";try{m.start(r.cyan(e.commit.generating));let d=k(m,e.commit.generatingSlow10,e.commit.generatingSlow60);a=await ie(n),p=await a.generateCommitMessage(S,i.signal),d(),m.succeed(r.green(e.commit.generated))}catch(d){m.fail(r.red.bold(e.commit.generationFailed)),d.name==="AbortError"&&(g(r.yellow(e.commit.generationCancelled)),process.exit(0)),Y(d),process.exit(1)}let x=!1;for(;!x;){ce(r.bold(p),e.commit.noteTitle);let d=await Se({message:e.commit.actionPrompt,options:[{label:e.commit.actions.accept,value:"accept"},{label:e.commit.actions.edit,value:"edit"},{label:e.commit.actions.regenerate,value:"regenerate"},{label:e.commit.actions.cancel,value:"cancel"}]});J(d)&&(g(r.dim(e.commit.processStopped)),process.exit(0));let A=d;if(A==="accept")x=!0;else if(A==="edit"){let R=await xe({message:e.commit.editPrompt,initialValue:p});J(R)&&(g(r.dim(e.commit.processStopped)),process.exit(0)),p=R,x=!0}else if(A==="regenerate"){m.start(r.blue(e.commit.regenerating));let R=k(m,e.commit.generatingSlow10,e.commit.generatingSlow60);try{p=await a.generateCommitMessage(S,i.signal,{regenerate:!0}),R(),m.succeed(r.green(e.commit.regenerated))}catch(V){R(),m.fail(r.red(e.commit.regenerationFailed(V.message))),V.name==="AbortError"&&(g(r.yellow(e.commit.generationCancelled)),process.exit(0))}}else g(r.dim(e.commit.processStopped)),process.exit(0)}t&&(ce(r.dim(e.common.dryRun.proposed(p)),r.yellow(e.common.dryRun.header)),process.exit(0)),await se()||(m.fail(r.red.bold(e.common.noRemote.header)),g(r.yellow(e.common.noRemote.instruction)),process.exit(1));try{m.start(r.blue(e.commit.committing));let d=k(m,e.commit.committingSlow10,e.commit.committingSlow60);await le.commit(p),d(),m.succeed(r.green(e.common.success.committed)),m.start(r.blue(e.commit.pushing));let A=k(m,e.commit.pushingSlow10,e.commit.pushingSlow60);await le.push(),A(),m.succeed(r.green.bold(e.common.success.pushed)),g(r.green(e.commit.outro))}catch(d){m.fail(r.red.bold(e.commit.operationFailed)),g(r.red(e.commit.gitError(d.message))),process.exit(1)}}catch(n){(n.name==="ExitPromptError"||n.name==="AbortError")&&(g(r.red(e.common.operationCancelled)),process.exit(0)),Y(n),process.exit(1)}finally{process.stdin.removeListener("data",s),process.stdin.setRawMode?.(!1),o?.(null)}}var z="pushai",pe="1.0.1",W="Stop writing manual commit messages.";import Ae from"chalk";var v=null,b=new Ie;b.name(z).description(W).option("--dry-run","Generate commit message but do not commit or push").action(async t=>{await K(t.dryRun,o=>{v=o}),v=null});b.name(z).description(W).version(pe,"-v, --version","output the version number");b.action(async()=>{await K(!1,t=>{v=t}),v=null});b.command("commit").description("Stage changes, generate a message, and push").option("--dry-run","Generate commit message but do not commit or push").action(async t=>{await K(t.dryRun,o=>{v=o}),v=null});b.command("config").description("Configure AI providers and API keys").action(L);b.command("reset").description("Delete the local config.json file").action(te);process.on("SIGINT",()=>{console.log(Ae.yellow(e.common.interrupted)),v?v.abort():process.exit(0)});b.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pushai",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Stop writing manual commit messages.",
5
5
  "bin": {
6
6
  "pai": "./dist/index.mjs"