@snelusha/noto 1.3.3 → 1.3.4-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +8 -0
  2. package/dist/index.js +23 -22
  3. package/package.json +4 -1
package/README.md CHANGED
@@ -118,6 +118,14 @@ Create and switch to a new branch:
118
118
  noto checkout -b new-branch-name
119
119
  ```
120
120
 
121
+ Update noto to the latest version:
122
+
123
+ ```bash
124
+ noto upgrade
125
+ ```
126
+
127
+ > noto will automatically detect your installation method and update itself accordingly.
128
+
121
129
  ## Pro Tips
122
130
 
123
131
  - 🚀 Get fast commits on the fly with `noto -a` to streamline your workflow!
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import{createCli as pt}from"trpc-cli";import qe from"node:fs/promises";import{initTRPC as Ve}from"@trpc/server";import*as $ from"@clack/prompts";import b from"picocolors";import H from"dedent";import Oe from"simple-git";var S=Oe(),oe=async()=>{return S.checkIsRepo()},D=async()=>{try{return await S.revparse(["--show-toplevel"])}catch{return null}};var re=async(t=20,e=!1)=>{try{let i=e?{maxCount:t,"--no-merges":null}:{maxCount:t};return(await S.log(i)).all.map((o)=>o.message)}catch{return null}};var ae=async()=>{try{return S.diff(["--cached","--",":!*.lock"])}catch{return null}},E=async(t,e)=>{try{let i=e?{"--amend":null}:void 0,{summary:{changes:a}}=await S.commit(t,void 0,i);return Boolean(a)}catch{return!1}},ie=async()=>{try{let t=await S.push();return t.update||t.pushed&&t.pushed.length>0}catch{return!1}},se=async()=>{try{return(await S.branch()).current}catch{return null}},ne=async(t)=>{try{let e=await S.branch();return t?e.all:Object.keys(e.branches).filter((i)=>!i.startsWith("remotes/"))}catch{return null}},_=async(t)=>{try{return await S.checkout(t,{}),!0}catch{return!1}},W=async(t)=>{try{return await S.checkoutLocalBranch(t),!0}catch{return!1}};import ze from"os";import{join as De,resolve as Ue}from"path";import{z as O}from"zod";import{promises as U}from"fs";import{dirname as Fe}from"path";function L(t){let{schema:e,path:i}=t;return class{static storagePath=i;static storage={};static async load(){try{await U.access(this.storagePath);let o=await U.readFile(this.storagePath,"utf-8"),n=o?JSON.parse(o):{},s=e.safeParse(n);this.storage=s.success?s.data:{}}catch{this.storage={}}return this.storage}static async save(){try{let o=Fe(this.storagePath);await U.mkdir(o,{recursive:!0});let n=JSON.stringify(this.storage,null,2);await U.writeFile(this.storagePath,n,"utf-8")}catch{}}static async update(o){try{await this.load();let n=await o(this.storage),s=e.safeParse(n);if(s.success)this.storage=s.data,await this.save()}catch{}return this.storage}static async get(){return await this.load(),JSON.parse(JSON.stringify(this.storage))}static async clear(){this.storage={},await this.save()}static get path(){return this.storagePath}static set path(o){this.storagePath=o}static get raw(){return this.storage}}}import{z as Ne}from"zod";var ce=Ne.enum(["gemini-1.5-flash","gemini-1.5-flash-latest","gemini-1.5-flash-8b","gemini-1.5-flash-8b-latest","gemini-1.5-pro","gemini-1.5-pro-latest","gemini-2.0-flash-001","gemini-2.0-flash","gemini-2.0-flash-lite-preview-02-05","gemini-2.5-flash-preview-04-17","gemini-2.5-pro-preview-05-06"]);var Le=O.object({llm:O.object({apiKey:O.string().optional(),model:ce.optional().or(O.string())}).optional(),lastGeneratedMessage:O.string().optional()}),p=L({path:Ue(De(ze.homedir(),".config","noto"),".notorc"),schema:Le});import Be from"node:fs/promises";import T from"node:path";import{fileURLToPath as je}from"node:url";var me=(t)=>t instanceof URL?je(t):t;async function le(t,e={}){let i=T.resolve(me(e.cwd??"")),{root:a}=T.parse(i);e.stopAt=T.resolve(me(e.stopAt??a));let o=T.isAbsolute(t);while(i){let n=o?t:T.join(i,t);try{let s=await Be.stat(n);if(e.type===void 0||e.type==="file"&&s.isFile()||e.type==="directory"&&s.isDirectory())return n}catch{}if(i===e.stopAt||i===a)break;i=T.dirname(i)}}var B=async()=>{let t=await D();return await le(".noto/commit-prompt.md",{stopAt:t||process.cwd(),type:"file"})};var r=async(t)=>{await new Promise((e)=>setTimeout(e,1)),console.log(),process.exit(t)};var A=Ve.meta().create({defaultMeta:{intro:!0,authRequired:!0,repoRequired:!0,diffRequired:!1,promptRequired:!1}}),pe=A.middleware(async(t)=>{let{meta:e,next:i}=t,a=await p.get(),o=process.env.NOTO_API_KEY||a.llm?.apiKey;if(e?.authRequired&&!o)return $.log.error(H`${b.red("noto api key is missing.")}
3
- ${b.dim(`run ${b.cyan("`noto config key`")} to set it up.`)}`),await r(1);return i()}),de=A.middleware(async(t)=>{let{meta:e,next:i}=t,a=await oe();if(e?.repoRequired&&!a)return $.log.error(H`${b.red("no git repository found in cwd.")}
4
- ${b.dim(`run ${b.cyan("`git init`")} to initialize a new repository.`)}`),await r(1);let o=a&&await ae();if(e?.diffRequired&&!o)return $.log.error(H`${b.red("no staged changes found.")}
5
- ${b.dim(`run ${b.cyan("`git add <file>`")} or ${b.cyan("`git add .`")} to stage changes.`)}`),await r(1);let n=null;if(e?.promptRequired){let s=await B();if(s)try{n=await qe.readFile(s,"utf-8")}catch{}}return i({ctx:{noto:{prompt:n},git:{isRepository:a,diff:o}}})}),k=A.procedure.use((t)=>{let{meta:e,next:i}=t;if(e?.intro)console.log(),$.intro(`${b.bgCyan(b.black(" @snelusha/noto "))}`);return i()}),Bt=k.use(pe),j=k.use(de),q=k.use(pe).use(de);import{z as G}from"zod";import*as c from"@clack/prompts";import u from"picocolors";import Ke from"clipboardy";var ue=j.meta({description:"checkout a branch"}).input(G.object({copy:G.boolean().meta({description:"copy the selected branch to clipboard",alias:"c"}),create:G.union([G.boolean(),G.string()]).optional().meta({description:"create a new branch",alias:"b"}),branch:G.string().optional().meta({positional:!0})})).mutation(async(t)=>{let{input:e}=t,i=await ne();if(!i)return c.log.error("failed to fetch branches"),await r(1);let a=await se(),o=typeof e.create==="string"?e.create:e.branch;if((e.create===!0||typeof e.create==="string")&&o){if(i.includes(o))return c.log.error(`branch ${u.red(o)} already exists in the repository`),await r(1);if(!await W(o))return c.log.error(`failed to create and checkout ${u.bold(o)}`),await r(1);return c.log.success(`created and checked out ${u.green(o)}`),await r(0)}if(o){if(!i.includes(o)){c.log.error(`branch ${u.red(o)} does not exist in the repository`);let I=await c.confirm({message:`do you want to create branch ${u.green(o)}?`});if(c.isCancel(I))return c.log.error("aborted"),await r(1);if(I){if(!await W(o))return c.log.error(`failed to create and checkout ${u.bold(o)}`),await r(1);return c.log.success(`created and checked out ${u.green(o)}`),await r(0)}return await r(1)}if(o===a)return c.log.error(`${u.red("already on branch")} ${u.green(o)}`),await r(1);if(!await _(o))return c.log.error(`failed to checkout ${u.bold(o)}`),await r(1);return c.log.success(`checked out ${u.green(o)}`),await r(0)}if(i.length===0)return c.log.error("no branches found in the repository"),await r(1);let s=await c.select({message:"select a branch to checkout",options:i.map((m)=>({value:m,label:u.bold(m===a?u.green(m):m),hint:m===a?"current branch":void 0})),initialValue:a});if(c.isCancel(s))return c.log.error("nothing selected!"),await r(1);if(!s)return c.log.error("no branch selected"),await r(1);if(e.copy)return Ke.writeSync(s),c.log.success(`copied ${u.green(s)} to clipboard`),await r(0);if(s===a)return c.log.error(`${u.red("already on branch")}`),await r(1);if(!await _(s))return c.log.error(`failed to checkout ${u.bold(s)}`),await r(1);c.log.success(`checked out ${u.green(s)}`),await r(0)});import{z as Ye}from"zod";import*as x from"@clack/prompts";import J from"picocolors";var ge=k.meta({description:"configure noto api key"}).input(Ye.string().optional().describe("apiKey")).mutation(async(t)=>{let{input:e}=t,i=e;if((await p.get()).llm?.apiKey){let a=await x.confirm({message:"noto api key already configured, do you want to update it?"});if(x.isCancel(a)||!a)return x.log.error(J.red("nothing changed!")),await r(1)}if(!i){let a=await x.text({message:"enter your noto api key"});if(x.isCancel(a))return x.log.error(J.red("nothing changed!")),await r(1);i=a}await p.update((a)=>({...a,llm:{...a.llm,apiKey:i}})),x.log.success(J.green("noto api key configured!")),await r(0)});import*as C from"@clack/prompts";import X from"picocolors";import{createGoogleGenerativeAI as _e}from"@ai-sdk/google";var v=_e({apiKey:process.env.NOTO_API_KEY||(await p.get()).llm?.apiKey||"api-key"}),fe="gemini-2.0-flash",V={"gemini-1.5-flash":v("gemini-1.5-flash"),"gemini-1.5-flash-latest":v("gemini-1.5-flash-latest"),"gemini-1.5-flash-8b":v("gemini-1.5-flash-8b"),"gemini-1.5-flash-8b-latest":v("gemini-1.5-flash-8b-latest"),"gemini-1.5-pro":v("gemini-1.5-pro"),"gemini-1.5-pro-latest":v("gemini-1.5-pro-latest"),"gemini-2.0-flash-001":v("gemini-2.0-flash-001"),"gemini-2.0-flash":v("gemini-2.0-flash"),"gemini-2.0-flash-lite-preview-02-05":v("gemini-2.0-flash-lite-preview-02-05"),"gemini-2.5-flash-preview-04-17":v("gemini-2.5-flash-preview-04-17"),"gemini-2.5-pro-preview-05-06":v("gemini-2.5-pro-preview-05-06")},We=Object.keys(V),Q=async()=>{let t=(await p.get()).llm?.model;if(!t||!We.includes(t))t=fe,await p.update((e)=>({...e,llm:{...e.llm,model:fe}}));return V[t]};var he=k.meta({description:"configure model"}).mutation(async()=>{let t=await C.select({message:"select a model",initialValue:(await p.get()).llm?.model,options:Object.keys(V).map((e)=>({label:e,value:e}))});if(C.isCancel(t))return C.log.error(X.red("nothing changed!")),await r(1);if(t==="gemini-2.5-pro-preview-05-06"){let e=await C.confirm({message:"this model does not have free quota tier, do you want to continue?"});if(C.isCancel(e)||!e)return C.log.error(X.red("nothing changed!")),await r(1)}await p.update((e)=>({...e,llm:{...e.llm,model:t}})),C.log.success(X.green("model configured!")),await r(0)});import*as P from"@clack/prompts";import ye from"picocolors";var we=k.meta({description:"reset the configuration"}).mutation(async()=>{let t=await P.confirm({message:"are you sure you want to reset the configuration?"});if(P.isCancel(t)||!t)return P.log.error(ye.red("nothing changed!")),await r(1);await p.clear(),P.log.success(ye.green("configuration reset!")),await r(0)});var be=A.router({key:ge,model:he,reset:we});import{z as F}from"zod";import*as f from"@clack/prompts";import g from"picocolors";import xe from"dedent";import He from"clipboardy";var ve=j.meta({description:"access the last generated commit",repoRequired:!1}).input(F.object({copy:F.boolean().meta({description:"copy the last commit to clipboard",alias:"c"}),apply:F.boolean().meta({description:"commit the last generated message",alias:"a"}),edit:F.boolean().meta({description:"edit the last generated commit message",alias:"e"}),amend:F.boolean().meta({description:"amend the last commit with the last message"})})).mutation(async(t)=>{let{input:e,ctx:i}=t,a=(await p.get()).lastGeneratedMessage;if(!a)return f.log.error(g.red("no previous commit message found")),await r(1);let{edit:o,amend:n}=e;if(n&&!o)return f.log.error(g.red("the --amend option requires the --edit option")),await r(1);if(f.log.step(o?g.white(a):g.green(a)),o){let s=await f.text({message:"edit the last generated commit message",initialValue:a,placeholder:a});if(f.isCancel(s))return f.log.error(g.red("nothing changed!")),await r(1);a=s,await p.update((w)=>({...w,lastGeneratedMessage:s})),f.log.step(g.green(a))}if(e.copy)He.writeSync(a),f.log.step(g.dim("copied last generated commit message to clipboard"));if(e.apply||n){if(!i.git.isRepository)return f.log.error(xe`${g.red("no git repository found in cwd.")}
6
- ${g.dim(`run ${g.cyan("`git init`")} to initialize a new repository.`)}`),await r(1);if(!i.git.diff&&!n)return f.log.error(xe`${g.red("no staged changes found.")}
7
- ${g.dim(`run ${g.cyan("`git add <file>`")} or ${g.cyan("`git add .`")} to stage changes.`)}`),await r(1);if(await E(a,n))f.log.step(g.dim("commit successful"));else f.log.error(g.red("failed to commit changes"))}return await r(0)});import Ae from"node:fs/promises";import{z as ee}from"zod";import*as d from"@clack/prompts";import M from"picocolors";import z from"dedent";import{generateObject as Me,wrapLanguageModel as tt}from"ai";import Y from"zod";import N from"dedent";import Se from"superjson";import{createHash as Je}from"crypto";function Ce(t){let e=Buffer.from(t,"utf-8"),i=Buffer.from(`blob ${e.length}\x00`,"utf-8");return Je("sha1").update(i).update(e).digest("hex")}import Qe from"os";import{join as Xe,resolve as Ze}from"path";import{z as K}from"zod";var et=K.object({commitGenerationCache:K.record(K.string(),K.string()).default({})}),Z=L({path:Ze(Xe(Qe.homedir(),".cache","noto"),"cache"),schema:et});var ot=N`
2
+ import{createCli as Pt}from"trpc-cli";var ge="@snelusha/noto",U="1.3.4-beta.0";import ft from"node:fs/promises";import{initTRPC as ht}from"@trpc/server";import*as D from"@clack/prompts";import b from"picocolors";import ne from"dedent";import Qe from"simple-git";var A=Qe(),Y=async()=>{return A.checkIsRepo()},K=async()=>{try{return await A.revparse(["--show-toplevel"])}catch{return null}};var fe=async(e=20,t=!1)=>{try{let o=t?{maxCount:e,"--no-merges":null}:{maxCount:e};return(await A.log(o)).all.map((r)=>r.message)}catch{return null}};var he=async()=>{try{return A.diff(["--cached","--",":!*.lock"])}catch{return null}},L=async(e,t)=>{try{let o=t?{"--amend":null}:void 0,{summary:{changes:a}}=await A.commit(e,void 0,o);return Boolean(a)}catch{return!1}},ye=async()=>{try{let e=await A.push();return e.update||e.pushed&&e.pushed.length>0}catch{return!1}},we=async()=>{try{return(await A.branch()).current}catch{return null}},be=async(e)=>{try{let t=await A.branch();return e?t.all:Object.keys(t.branches).filter((o)=>!o.startsWith("remotes/"))}catch{return null}},re=async(e)=>{try{return await A.checkout(e,{}),!0}catch{return!1}},ae=async(e)=>{try{return await A.checkoutLocalBranch(e),!0}catch{return!1}};import et from"os";import{join as tt,resolve as ot}from"path";import{z as j}from"zod";import{promises as _}from"fs";import{dirname as Xe}from"path";function H(e){let{schema:t,path:o}=e;return class{static storagePath=o;static storage={};static async load(){try{await _.access(this.storagePath);let r=await _.readFile(this.storagePath,"utf-8"),n=r?JSON.parse(r):{},s=t.safeParse(n);this.storage=s.success?s.data:{}}catch{this.storage={}}return this.storage}static async save(){try{let r=Xe(this.storagePath);await _.mkdir(r,{recursive:!0});let n=JSON.stringify(this.storage,null,2);await _.writeFile(this.storagePath,n,"utf-8")}catch{}}static async update(r){try{await this.load();let n=await r(this.storage),s=t.safeParse(n);if(s.success)this.storage=s.data,await this.save()}catch{}return this.storage}static async get(){return await this.load(),JSON.parse(JSON.stringify(this.storage))}static async clear(){this.storage={},await this.save()}static get path(){return this.storagePath}static set path(r){this.storagePath=r}static get raw(){return this.storage}}}import{z as Ze}from"zod";var ve=Ze.enum(["gemini-1.5-flash","gemini-1.5-flash-latest","gemini-1.5-flash-8b","gemini-1.5-flash-8b-latest","gemini-1.5-pro","gemini-1.5-pro-latest","gemini-2.0-flash-001","gemini-2.0-flash","gemini-2.0-flash-lite-preview-02-05","gemini-2.5-flash-preview-04-17","gemini-2.5-pro-preview-05-06"]);var rt=j.object({llm:j.object({apiKey:j.string().optional(),model:ve.optional().or(j.string())}).optional(),lastGeneratedMessage:j.string().optional()}),p=H({path:ot(tt(et.homedir(),".config","noto"),".notorc"),schema:rt});async function xe(){try{let e=await p.get();await p.update(()=>e)}catch{}}import at from"node:fs/promises";import P from"node:path";import{fileURLToPath as it}from"node:url";var Ce=(e)=>e instanceof URL?it(e):e;async function Se(e,t={}){let o=P.resolve(Ce(t.cwd??"")),{root:a}=P.parse(o);t.stopAt=P.resolve(Ce(t.stopAt??a));let r=P.isAbsolute(e);while(o){let n=r?e:P.join(o,e);try{let s=await at.stat(n);if(t.type===void 0||t.type==="file"&&s.isFile()||t.type==="directory"&&s.isDirectory())return n}catch{}if(o===t.stopAt||o===a)break;o=P.dirname(o)}}var W=async()=>{let e=await K();return await Se(".noto/commit-prompt.md",{stopAt:e||process.cwd(),type:"file"})};import*as ke from"@clack/prompts";import Ae from"picocolors";import ut from"dedent";import v from"semver";import lt from"latest-version";import st from"os";import{join as nt,resolve as ct}from"path";import{z as I}from"zod";var mt=I.object({commitGenerationCache:I.record(I.string(),I.string()).optional(),update:I.object({timestamp:I.number(),current:I.string(),latest:I.string()}).optional()}),O=H({path:ct(nt(st.homedir(),".cache","noto"),"cache"),schema:mt});var pt=43200000;async function ie(e=!1,t=!1){let o=(await O.get()).update;if(!t&&o){if(v.valid(o.current)&&v.valid(o.latest)){let r=v.gte(o.current,o.latest),n=Date.now()-o.timestamp<pt;if(r||n)return{latest:o.latest,current:o.current,timestamp:o.timestamp}}}try{let r={latest:await lt(ge),current:U,timestamp:Date.now()};if(e)await dt(r);return r}catch{if(o)return{latest:o.latest,current:o.current,timestamp:o.timestamp};return{latest:U,current:U,timestamp:Date.now()}}}async function dt(e){if(!e)return;await O.update((t)=>({...t,update:e&&{timestamp:e.timestamp,latest:e.latest,current:e.current}}))}async function J(e=!1,t=!1){let o=await ie(e,t);if(v.valid(o.current)&&v.valid(o.latest)){if(!v.gte(o.current,o.latest))return o}return null}import*as T from"node:fs";import*as z from"node:path";import*as Me from"node:child_process";import se from"node:process";async function Q(){let e=se.argv[1];if(!e)return{packageManager:"unknown",isGlobal:!1};let t=se.cwd();try{let o=T.realpathSync(e).replace(/\\/g,"/"),a=t.replace(/\\/g,"/");if(await Y()&&a&&o.startsWith(a)&&!o.includes("/node_modules/"))return{packageManager:"unknown",isGlobal:!1,updateMessage:'Running from a local git clone. Please update with "git pull".'};if(o.includes("/.npm/_npx")||o.includes("/npm/_npx"))return{packageManager:"npx",isGlobal:!1,updateMessage:"Running via npx, update not applicable."};if(o.includes("/.pnpm/_pnpx"))return{packageManager:"pnpx",isGlobal:!1,updateMessage:"Running via pnpx, update not applicable."};if(se.platform==="darwin")try{return Me.execSync('brew list -1 | grep -q "^noto$"',{stdio:"ignore"}),{packageManager:"homebrew",isGlobal:!0,updateMessage:'Installed via Homebrew. Please update with "brew upgrade".'}}catch{}if(o.includes("/.pnpm/global"))return{packageManager:"pnpm",isGlobal:!0,updateCommand:"pnpm add -g @snelusha/noto@latest",updateMessage:"Please run pnpm add -g @snelusha/noto@latest to update"};if(o.includes("/.yarn/global"))return{packageManager:"yarn",isGlobal:!0,updateCommand:"yarn global add @snelusha/noto@latest",updateMessage:"Please run yarn global add @snelusha/noto@latest to update"};if(o.includes("/.bun/install/cache"))return{packageManager:"bunx",isGlobal:!1,updateMessage:"Running via bunx, update not applicable."};if(o.includes("/.bun/install/global"))return{packageManager:"bun",isGlobal:!0,updateCommand:"bun add -g @snelusha/noto@latest",updateMessage:"Please run bun add -g @snelusha/noto@latest to update"};if(a&&o.startsWith(`${a}/node_modules`)){let s="npm";if(T.existsSync(z.join(t,"yarn.lock")))s="yarn";else if(T.existsSync(z.join(t,"pnpm-lock.yaml")))s="pnpm";else if(T.existsSync(z.join(t,"bun.lockb"))||T.existsSync(z.join(t,"bun.lock")))s="bun";return{packageManager:s,isGlobal:!1,updateMessage:"Locally installed. Please update via your project's package.json."}}let n="npm install -g @snelusha/noto@latest";return{packageManager:"npm",isGlobal:!0,updateCommand:n,updateMessage:`Please run ${n} to update`}}catch{return{packageManager:"unknown",isGlobal:!1}}}async function gt(){let e=await J();if(e){let o=(await Q()).updateMessage;ke.log.warn(ut`A new version of noto is available: ${Ae.dim(e.current)} → ${Ae.green(e.latest)}
3
+ ${o??""}`.trim())}}var i=async(e,t=!0)=>{if(t)await gt();await new Promise((o)=>setTimeout(o,1)),console.log(),process.exit(e)};var $=ht.meta().create({defaultMeta:{intro:!0,authRequired:!0,repoRequired:!0,diffRequired:!1,promptRequired:!1}}),Ie=$.middleware(async(e)=>{let{meta:t,next:o}=e,a=await p.get(),r=process.env.NOTO_API_KEY||a.llm?.apiKey;if(t?.authRequired&&!r)return D.log.error(ne`${b.red("noto api key is missing.")}
4
+ ${b.dim(`run ${b.cyan("`noto config key`")} to set it up.`)}`),await i(1);return o()}),Re=$.middleware(async(e)=>{let{meta:t,next:o}=e,a=await Y();if(t?.repoRequired&&!a)return D.log.error(ne`${b.red("no git repository found in cwd.")}
5
+ ${b.dim(`run ${b.cyan("`git init`")} to initialize a new repository.`)}`),await i(1);let r=a&&await he();if(t?.diffRequired&&!r)return D.log.error(ne`${b.red("no staged changes found.")}
6
+ ${b.dim(`run ${b.cyan("`git add <file>`")} or ${b.cyan("`git add .`")} to stage changes.`)}`),await i(1);let n=null;if(t?.promptRequired){let s=await W();if(s)try{n=await ft.readFile(s,"utf-8")}catch{}}return o({ctx:{noto:{prompt:n},git:{isRepository:a,diff:r}}})}),x=$.procedure.use((e)=>{let{meta:t,next:o}=e;if(t?.intro)console.log(),D.intro(`${b.bgCyan(b.black(" @snelusha/noto "))}`);return o()}),To=x.use(Ie),X=x.use(Re),Z=x.use(Ie).use(Re);import{z as F}from"zod";import*as c from"@clack/prompts";import u from"picocolors";import yt from"clipboardy";var Te=X.meta({description:"checkout a branch"}).input(F.object({copy:F.boolean().meta({description:"copy the selected branch to clipboard",alias:"c"}),create:F.union([F.boolean(),F.string()]).optional().meta({description:"create a new branch",alias:"b"}),branch:F.string().optional().meta({positional:!0})})).mutation(async(e)=>{let{input:t}=e,o=await be();if(!o)return c.log.error("failed to fetch branches"),await i(1);let a=await we(),r=typeof t.create==="string"?t.create:t.branch;if((t.create===!0||typeof t.create==="string")&&r){if(o.includes(r))return c.log.error(`branch ${u.red(r)} already exists in the repository`),await i(1);if(!await ae(r))return c.log.error(`failed to create and checkout ${u.bold(r)}`),await i(1);return c.log.success(`created and checked out ${u.green(r)}`),await i(0)}if(r){if(!o.includes(r)){c.log.error(`branch ${u.red(r)} does not exist in the repository`);let G=await c.confirm({message:`do you want to create branch ${u.green(r)}?`});if(c.isCancel(G))return c.log.error("aborted"),await i(1);if(G){if(!await ae(r))return c.log.error(`failed to create and checkout ${u.bold(r)}`),await i(1);return c.log.success(`created and checked out ${u.green(r)}`),await i(0)}return await i(1)}if(r===a)return c.log.error(`${u.red("already on branch")} ${u.green(r)}`),await i(1);if(!await re(r))return c.log.error(`failed to checkout ${u.bold(r)}`),await i(1);return c.log.success(`checked out ${u.green(r)}`),await i(0)}if(o.length===0)return c.log.error("no branches found in the repository"),await i(1);let s=await c.select({message:"select a branch to checkout",options:o.map((m)=>({value:m,label:u.bold(m===a?u.green(m):m),hint:m===a?"current branch":void 0})),initialValue:a});if(c.isCancel(s))return c.log.error("nothing selected!"),await i(1);if(!s)return c.log.error("no branch selected"),await i(1);if(t.copy)return yt.writeSync(s),c.log.success(`copied ${u.green(s)} to clipboard`),await i(0);if(s===a)return c.log.error(`${u.red("already on branch")}`),await i(1);if(!await re(s))return c.log.error(`failed to checkout ${u.bold(s)}`),await i(1);c.log.success(`checked out ${u.green(s)}`),await i(0)});import{z as wt}from"zod";import*as C from"@clack/prompts";import ce from"picocolors";var $e=x.meta({description:"configure noto api key"}).input(wt.string().optional().describe("apiKey")).mutation(async(e)=>{let{input:t}=e,o=t;if((await p.get()).llm?.apiKey){let a=await C.confirm({message:"noto api key already configured, do you want to update it?"});if(C.isCancel(a)||!a)return C.log.error(ce.red("nothing changed!")),await i(1)}if(!o){let a=await C.text({message:"enter your noto api key"});if(C.isCancel(a))return C.log.error(ce.red("nothing changed!")),await i(1);o=a}await p.update((a)=>({...a,llm:{...a.llm,apiKey:o}})),C.log.success(ce.green("noto api key configured!")),await i(0)});import*as M from"@clack/prompts";import le from"picocolors";import{createGoogleGenerativeAI as bt}from"@ai-sdk/google";var S=bt({apiKey:process.env.NOTO_API_KEY||(await p.get()).llm?.apiKey||"api-key"}),Ee="gemini-2.0-flash",ee={"gemini-1.5-flash":S("gemini-1.5-flash"),"gemini-1.5-flash-latest":S("gemini-1.5-flash-latest"),"gemini-1.5-flash-8b":S("gemini-1.5-flash-8b"),"gemini-1.5-flash-8b-latest":S("gemini-1.5-flash-8b-latest"),"gemini-1.5-pro":S("gemini-1.5-pro"),"gemini-1.5-pro-latest":S("gemini-1.5-pro-latest"),"gemini-2.0-flash-001":S("gemini-2.0-flash-001"),"gemini-2.0-flash":S("gemini-2.0-flash"),"gemini-2.0-flash-lite-preview-02-05":S("gemini-2.0-flash-lite-preview-02-05"),"gemini-2.5-flash-preview-04-17":S("gemini-2.5-flash-preview-04-17"),"gemini-2.5-pro-preview-05-06":S("gemini-2.5-pro-preview-05-06")},vt=Object.keys(ee),me=async()=>{let e=(await p.get()).llm?.model;if(!e||!vt.includes(e))e=Ee,await p.update((t)=>({...t,llm:{...t.llm,model:Ee}}));return ee[e]};var Ge=x.meta({description:"configure model"}).mutation(async()=>{let e=await M.select({message:"select a model",initialValue:(await p.get()).llm?.model,options:Object.keys(ee).map((t)=>({label:t,value:t}))});if(M.isCancel(e))return M.log.error(le.red("nothing changed!")),await i(1);if(e==="gemini-2.5-pro-preview-05-06"){let t=await M.confirm({message:"this model does not have free quota tier, do you want to continue?"});if(M.isCancel(t)||!t)return M.log.error(le.red("nothing changed!")),await i(1)}await p.update((t)=>({...t,llm:{...t.llm,model:e}})),M.log.success(le.green("model configured!")),await i(0)});import*as E from"@clack/prompts";import Ue from"picocolors";var Pe=x.meta({description:"reset the configuration"}).mutation(async()=>{let e=await E.confirm({message:"are you sure you want to reset the configuration?"});if(E.isCancel(e)||!e)return E.log.error(Ue.red("nothing changed!")),await i(1);await p.clear(),E.log.success(Ue.green("configuration reset!")),await i(0)});var Oe=$.router({key:$e,model:Ge,reset:Pe});import{z as B}from"zod";import*as f from"@clack/prompts";import g from"picocolors";import De from"dedent";import xt from"clipboardy";var Fe=X.meta({description:"access the last generated commit",repoRequired:!1}).input(B.object({copy:B.boolean().meta({description:"copy the last commit to clipboard",alias:"c"}),apply:B.boolean().meta({description:"commit the last generated message",alias:"a"}),edit:B.boolean().meta({description:"edit the last generated commit message",alias:"e"}),amend:B.boolean().meta({description:"amend the last commit with the last message"})})).mutation(async(e)=>{let{input:t,ctx:o}=e,a=(await p.get()).lastGeneratedMessage;if(!a)return f.log.error(g.red("no previous commit message found")),await i(1);let{edit:r,amend:n}=t;if(n&&!r)return f.log.error(g.red("the --amend option requires the --edit option")),await i(1);if(f.log.step(r?g.white(a):g.green(a)),r){let s=await f.text({message:"edit the last generated commit message",initialValue:a,placeholder:a});if(f.isCancel(s))return f.log.error(g.red("nothing changed!")),await i(1);a=s,await p.update((w)=>({...w,lastGeneratedMessage:s})),f.log.step(g.green(a))}if(t.copy)xt.writeSync(a),f.log.step(g.dim("copied last generated commit message to clipboard"));if(t.apply||n){if(!o.git.isRepository)return f.log.error(De`${g.red("no git repository found in cwd.")}
7
+ ${g.dim(`run ${g.cyan("`git init`")} to initialize a new repository.`)}`),await i(1);if(!o.git.diff&&!n)return f.log.error(De`${g.red("no staged changes found.")}
8
+ ${g.dim(`run ${g.cyan("`git add <file>`")} or ${g.cyan("`git add .`")} to stage changes.`)}`),await i(1);if(await L(a,n))f.log.step(g.dim("commit successful"));else f.log.error(g.red("failed to commit changes"))}return await i(0)});import qe from"node:fs/promises";import{z as pe}from"zod";import*as d from"@clack/prompts";import k from"picocolors";import V from"dedent";import{generateObject as je,wrapLanguageModel as St}from"ai";import te from"zod";import q from"dedent";import Le from"superjson";import{createHash as Ct}from"crypto";function Ne(e){let t=Buffer.from(e,"utf-8"),o=Buffer.from(`blob ${t.length}\x00`,"utf-8");return Ct("sha1").update(o).update(t).digest("hex")}var Mt=q`
8
9
  # System Instruction for Noto
9
10
 
10
11
  You are a Git commit message generator for the \`noto\` CLI tool. Your role is to analyze staged code changes and generate clear, single-line commit messages that follow the user's established style.
@@ -75,7 +76,7 @@ Or whatever format matches the user's guidelines. **Must be single-line only.**
75
76
  - Don't use generic or vague descriptions
76
77
 
77
78
  **Remember:** Your output becomes permanent git history. Generate commit messages that are clear, accurate, and consistent with the user's established patterns.
78
- `,rt=N`
79
+ `,At=q`
79
80
  # Commit Message Guidelines
80
81
 
81
82
  ## Format
@@ -147,7 +148,7 @@ For breaking changes, add \`!\` after the type/scope:
147
148
  ## Additional Notes
148
149
  - If a commit addresses a specific issue, you can reference it in the description (e.g., \`fix: resolve memory leak (fixes #123)\`)
149
150
  - Each commit should represent a single logical change
150
- - Write commits as if completing the sentence: "If applied, this commit will..."`,at=N`
151
+ - Write commits as if completing the sentence: "If applied, this commit will..."`,kt=q`
151
152
  You are a commit style analyzer. Analyze the provided commit history and generate a personalized style guide that will be used to generate future commit messages.
152
153
 
153
154
  ## Task
@@ -303,27 +304,27 @@ Start with action verb (add, implement, resolve, update, simplify). Be specific
303
304
  - The output will be stored as \`.noto/commit-prompt.md\` and used by an AI to generate commits
304
305
 
305
306
  Generate the markdown guidelines now based on the commit history provided.
306
- `,it={wrapGenerate:async({doGenerate:t,params:e})=>{let i=Ce(JSON.stringify(e)),a=(await Z.get()).commitGenerationCache;if(a&&i in a)return Se.parse(a[i]);let o=await t();return await Z.update((n)=>({...n,commitGenerationCache:{[i]:Se.stringify(o)}})),o}},ke=async(t,e,i,a=!1)=>{let o=await Q(),{object:n}=await Me({model:!a?tt({model:o,middleware:it}):o,schema:Y.object({message:Y.string()}),messages:[{role:"system",content:ot},{role:"user",content:N`
307
+ `,It={wrapGenerate:async({doGenerate:e,params:t})=>{let o=Ne(JSON.stringify(t)),a=(await O.get()).commitGenerationCache;if(a&&o in a)return Le.parse(a[o]);let r=await e();return await O.update((n)=>({...n,commitGenerationCache:{[o]:Le.stringify(r)}})),r}},ze=async(e,t,o,a=!1)=>{let r=await me(),{object:n}=await je({model:!a?St({model:r,middleware:It}):r,schema:te.object({message:te.string()}),messages:[{role:"system",content:Mt},{role:"user",content:q`
307
308
  USER GUIDELINES:
308
- ${e??rt}
309
- ${i?`
309
+ ${t??At}
310
+ ${o?`
310
311
  USER CONTEXT:
311
- ${i}`:""}
312
+ ${o}`:""}
312
313
 
313
314
  GIT DIFF:
314
- ${t}
315
- `}]});return n.message.trim()},Re=async(t)=>{let e=await Q(),{object:i}=await Me({model:e,schema:Y.object({prompt:Y.string()}),messages:[{role:"system",content:at},{role:"user",content:N`
315
+ ${e}
316
+ `}]});return n.message.trim()},Be=async(e)=>{let t=await me(),{object:o}=await je({model:t,schema:te.object({prompt:te.string()}),messages:[{role:"system",content:kt},{role:"user",content:q`
316
317
  COMMIT HISTORY:
317
- ${t.join(`
318
- `)}`}]});return i.prompt.trim()};var st=z`
318
+ ${e.join(`
319
+ `)}`}]});return o.prompt.trim()};var Rt=V`
319
320
  # Commit Message Guidelines
320
321
 
321
322
  # Add your custom guidelines here.
322
- # When no guidelines are present, noto will use conventional commits format by default.`,Pe=q.meta({description:"initialize noto in the repository"}).input(ee.object({root:ee.boolean().meta({description:"create the prompt file in the git root"}),generate:ee.boolean().meta({description:"generate a prompt file based on existing commits"})})).mutation(async(t)=>{let{input:e}=t,i=await D(),a=i,o=process.cwd(),n=await B(),s=null;if(n)if(!n.startsWith(o)){d.log.warn(z`${M.yellow("a prompt file already exists!")}
323
- ${M.gray(n)}`);let h=await d.confirm({message:"do you want to create in the current directory instead?",initialValue:!0});if(d.isCancel(h)||!h)return d.log.error("aborted"),await r(1);a=o}else return d.log.error(z`${M.red("a prompt file already exists.")}
324
- ${M.gray(n)}`),await r(1);if(i!==o&&!e.root){let h=await d.confirm({message:"do you want to create the prompt file in the git root?",initialValue:!0});if(d.isCancel(h))return d.log.error("aborted"),await r(1);if(!h)a=o}let w=await re(20,!0),m=e.generate;if(m){if(!w||w.length<5)return d.log.error(z`${M.red("not enough commits to generate a prompt file.")}
325
- ${M.gray("at least 5 commits are required.")}`),await r(1)}else if(w&&w.length>=5){let h=await d.confirm({message:"do you want to generate a prompt file based on existing commits?",initialValue:!0});if(d.isCancel(h))return d.log.error("aborted"),await r(1);m=h}let I=d.spinner();if(w&&m)I.start("generating commit message guidelines"),s=await Re(w),I.stop(M.green("generated commit message guidelines!"));else s=st;try{let h=`${a}/.noto`;await Ae.mkdir(h,{recursive:!0});let te=`${h}/commit-prompt.md`;return await Ae.writeFile(te,s,"utf-8"),d.log.success(z`${M.green("prompt file created!")}
326
- ${M.gray(te)}`),await r(0)}catch{d.log.error(M.red("failed to create the prompt file!"))}});import{z as R}from"zod";import*as l from"@clack/prompts";import y from"picocolors";import nt from"clipboardy";import{APICallError as ct,RetryError as mt}from"ai";var Ie=q.meta({description:"generate a commit message",default:!0,diffRequired:!0,promptRequired:!0}).input(R.object({message:R.string().or(R.boolean()).meta({description:"provide context for commit message",alias:"m"}),copy:R.boolean().meta({description:"copy the generated message to clipboard",alias:"c"}),apply:R.boolean().meta({description:"commit the generated message",alias:"a"}),push:R.boolean().meta({description:"commit and push the changes",alias:"p"}),force:R.boolean().meta({description:"bypass cache and force regeneration of commit message",alias:"f"}),manual:R.boolean().meta({description:"custom commit message"})})).mutation(async(t)=>{let{input:e,ctx:i}=t,a=l.spinner();try{if(e.manual){let m=await l.text({message:"edit the generated commit message",placeholder:"chore: init repo"});if(l.isCancel(m))return l.log.error(y.red("nothing changed!")),await r(1);if(l.log.step(y.green(m)),await p.update((h)=>({...h,lastGeneratedMessage:m})),await E(m))l.log.step(y.dim("commit successful"));else l.log.error(y.red("failed to commit changes"));return await r(0)}let n=e.message;if(typeof n==="string")n=n.trim();else if(typeof n==="boolean"&&n===!0){let m=await l.text({message:"provide context for the commit message",placeholder:"describe the changes"});if(l.isCancel(m))return l.log.error(y.red("nothing changed!")),await r(1);n=m}a.start("generating commit message");let s=null;s=await ke(i.git.diff,i.noto.prompt,typeof n==="string"?n:void 0,e.force),a.stop(y.white(s));let w=await l.text({message:"edit the generated commit message",initialValue:s,placeholder:s});if(l.isCancel(w))return l.log.error(y.red("nothing changed!")),await r(1);if(s=w,l.log.step(y.green(s)),await p.update((m)=>({...m,lastGeneratedMessage:s})),e.copy)nt.writeSync(s),l.log.step(y.dim("copied commit message to clipboard"));if(e.apply)if(await E(s))l.log.step(y.dim("commit successful"));else l.log.error(y.red("failed to commit changes"));if(e.push)if(await ie())l.log.step(y.dim("push successful"));else l.log.error(y.red("failed to push changes"));return await r(0)}catch(o){let n;if(mt.isInstance(o)&&ct.isInstance(o.lastError))n=lt(o.lastError.responseBody);let s=n?`
327
- ${n}`:"";a.stop(y.red(`failed to generate commit message${s}`),1),await r(1)}});function lt(t){if(typeof t!=="string")return;try{let e=JSON.parse(t);return e?.error?.message??e?.message}catch{return}}var Te={checkout:ue,config:be,prev:ve,init:Pe,noto:Ie};var $e=A.router(Te);var Ge=process.argv.slice(2),Ee="1.3.3";if(Ge.includes("--version")||Ge.includes("-v"))console.log(Ee),process.exit(0);pt({name:"noto",router:$e,version:Ee}).run();
323
+ # When no guidelines are present, noto will use conventional commits format by default.`,Ve=Z.meta({description:"initialize noto in the repository"}).input(pe.object({root:pe.boolean().meta({description:"create the prompt file in the git root"}),generate:pe.boolean().meta({description:"generate a prompt file based on existing commits"})})).mutation(async(e)=>{let{input:t}=e,o=await K(),a=o,r=process.cwd(),n=await W(),s=null;if(n)if(!n.startsWith(r)){d.log.warn(V`${k.yellow("a prompt file already exists!")}
324
+ ${k.gray(n)}`);let h=await d.confirm({message:"do you want to create in the current directory instead?",initialValue:!0});if(d.isCancel(h)||!h)return d.log.error("aborted"),await i(1);a=r}else return d.log.error(V`${k.red("a prompt file already exists.")}
325
+ ${k.gray(n)}`),await i(1);if(o!==r&&!t.root){let h=await d.confirm({message:"do you want to create the prompt file in the git root?",initialValue:!0});if(d.isCancel(h))return d.log.error("aborted"),await i(1);if(!h)a=r}let w=await fe(20,!0),m=t.generate;if(m){if(!w||w.length<5)return d.log.error(V`${k.red("not enough commits to generate a prompt file.")}
326
+ ${k.gray("at least 5 commits are required.")}`),await i(1)}else if(w&&w.length>=5){let h=await d.confirm({message:"do you want to generate a prompt file based on existing commits?",initialValue:!0});if(d.isCancel(h))return d.log.error("aborted"),await i(1);m=h}let G=d.spinner();if(w&&m)G.start("generating commit message guidelines"),s=await Be(w),G.stop(k.green("generated commit message guidelines!"));else s=Rt;try{let h=`${a}/.noto`;await qe.mkdir(h,{recursive:!0});let de=`${h}/commit-prompt.md`;return await qe.writeFile(de,s,"utf-8"),d.log.success(V`${k.green("prompt file created!")}
327
+ ${k.gray(de)}`),await i(0)}catch{d.log.error(k.red("failed to create the prompt file!"))}});import{z as R}from"zod";import*as l from"@clack/prompts";import y from"picocolors";import Tt from"clipboardy";import{APICallError as $t,RetryError as Et}from"ai";var Ye=Z.meta({description:"generate a commit message",default:!0,diffRequired:!0,promptRequired:!0}).input(R.object({message:R.string().or(R.boolean()).meta({description:"provide context for commit message",alias:"m"}),copy:R.boolean().meta({description:"copy the generated message to clipboard",alias:"c"}),apply:R.boolean().meta({description:"commit the generated message",alias:"a"}),push:R.boolean().meta({description:"commit and push the changes",alias:"p"}),force:R.boolean().meta({description:"bypass cache and force regeneration of commit message",alias:"f"}),manual:R.boolean().meta({description:"custom commit message"})})).mutation(async(e)=>{let{input:t,ctx:o}=e,a=l.spinner();try{if(t.manual){let m=await l.text({message:"edit the generated commit message",placeholder:"chore: init repo"});if(l.isCancel(m))return l.log.error(y.red("nothing changed!")),await i(1);if(l.log.step(y.green(m)),await p.update((h)=>({...h,lastGeneratedMessage:m})),await L(m))l.log.step(y.dim("commit successful"));else l.log.error(y.red("failed to commit changes"));return await i(0)}let n=t.message;if(typeof n==="string")n=n.trim();else if(typeof n==="boolean"&&n===!0){let m=await l.text({message:"provide context for the commit message",placeholder:"describe the changes"});if(l.isCancel(m))return l.log.error(y.red("nothing changed!")),await i(1);n=m}a.start("generating commit message");let s=null;s=await ze(o.git.diff,o.noto.prompt,typeof n==="string"?n:void 0,t.force),a.stop(y.white(s));let w=await l.text({message:"edit the generated commit message",initialValue:s,placeholder:s});if(l.isCancel(w))return l.log.error(y.red("nothing changed!")),await i(1);if(s=w,l.log.step(y.green(s)),await p.update((m)=>({...m,lastGeneratedMessage:s})),t.copy)Tt.writeSync(s),l.log.step(y.dim("copied commit message to clipboard"));if(t.apply)if(await L(s))l.log.step(y.dim("commit successful"));else l.log.error(y.red("failed to commit changes"));if(t.push)if(await ye())l.log.step(y.dim("push successful"));else l.log.error(y.red("failed to push changes"));return await i(0)}catch(r){let n;if(Et.isInstance(r)&&$t.isInstance(r.lastError))n=Gt(r.lastError.responseBody);let s=n?`
328
+ ${n}`:"";a.stop(y.red(`failed to generate commit message${s}`),1),await i(1)}});function Gt(e){if(typeof e!=="string")return;try{let t=JSON.parse(e);return t?.error?.message??t?.message}catch{return}}import{spawn as Ut}from"node:child_process";import*as N from"@clack/prompts";import oe from"picocolors";var Ke=x.meta({description:"upgrade noto"}).mutation(async()=>{let e=N.spinner();e.start("fetching latest version");let t=await J(!0,!0);if(!t)return e.stop(`You're already on the latest version of noto (${oe.dim(`which is ${U}`)})`),await i(0,!1);e.stop(`noto ${oe.green(t.latest)} is out! You are on ${oe.dim(t.current)}.`);let o=await Q();if(!o.updateCommand){if(o.updateMessage)return N.log.warn(o.updateMessage),await i(0,!1);return N.log.error("unable to determine update command for your installation."),await i(1,!1)}let a=Ut(o.updateCommand,{stdio:"pipe",shell:!0});e.start("upgrading noto");try{await new Promise((r,n)=>{a.on("close",(s)=>{if(s===0)r();else n()})}),e.stop(oe.green("noto has been updated successfully!"))}catch{return N.log.error(`automatic update failed. please try updating manually by running: ${o.updateCommand}`),await i(1,!1)}return await i(0,!1)});var _e={checkout:Te,config:Oe,prev:Fe,init:Ve,noto:Ye,upgrade:Ke};var He=$.router(_e);var We=process.argv.slice(2),Je="1.3.4-beta.0";if(We.includes("--version")||We.includes("-v"))console.log(Je),process.exit(0);xe();ie(!0);Pt({name:"noto",router:He,version:Je}).run();
328
329
 
329
330
  // Made by a human on earth!
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snelusha/noto",
3
- "version": "1.3.3",
3
+ "version": "1.3.4-beta.0",
4
4
  "description": "Generate clean commit messages in a snap! ✨",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -43,6 +43,7 @@
43
43
  ],
44
44
  "devDependencies": {
45
45
  "@types/node": "^24.10.1",
46
+ "@types/semver": "^7.7.1",
46
47
  "bunup": "^0.15.7",
47
48
  "typescript": "^5.9.3",
48
49
  "vitest": "^4.0.3"
@@ -55,7 +56,9 @@
55
56
  "ai": "^5.0.80",
56
57
  "clipboardy": "^5.0.0",
57
58
  "dedent": "^1.7.0",
59
+ "latest-version": "^9.0.0",
58
60
  "picocolors": "^1.1.1",
61
+ "semver": "^7.7.3",
59
62
  "simple-git": "^3.28.0",
60
63
  "superjson": "^2.2.3",
61
64
  "tinyexec": "^1.0.1",