better-commits 1.19.1 → 1.20.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 (53) hide show
  1. package/.better-commits.json +4 -0
  2. package/.github/workflows/test.yml +27 -0
  3. package/dist/branch.js +27 -1
  4. package/dist/chunk-OFJCRS3N.js +4 -0
  5. package/dist/chunk-SIF4LZUS.js +1 -0
  6. package/dist/index.js +44 -19
  7. package/dist/init.js +1 -1
  8. package/package.json +12 -9
  9. package/readme.md +27 -12
  10. package/src/args.test.ts +102 -0
  11. package/src/args.ts +101 -7
  12. package/src/branch-args.test.ts +72 -0
  13. package/src/branch-args.ts +106 -0
  14. package/src/branch-help.ts +114 -0
  15. package/src/branch.ts +67 -238
  16. package/src/help.ts +131 -0
  17. package/src/index.test.ts +7 -0
  18. package/src/index.ts +73 -495
  19. package/src/prompts/branch-checkout.prompt.ts +36 -0
  20. package/src/prompts/branch-confirm.prompt.ts +134 -0
  21. package/src/prompts/branch-description.prompt.ts +37 -0
  22. package/src/prompts/branch-runnable.ts +13 -0
  23. package/src/prompts/branch-ticket.prompt.ts +41 -0
  24. package/src/prompts/branch-type.prompt.ts +43 -0
  25. package/src/prompts/branch-user.prompt.ts +50 -0
  26. package/src/prompts/branch-version.prompt.ts +41 -0
  27. package/src/prompts/commit-body.prompt.ts +57 -0
  28. package/src/prompts/commit-confirm.prompt.ts +119 -0
  29. package/src/prompts/commit-footer.prompt.ts +195 -0
  30. package/src/prompts/commit-scope.prompt.ts +73 -0
  31. package/src/prompts/commit-status.prompt.ts +75 -0
  32. package/src/prompts/commit-ticket.prompt.ts +82 -0
  33. package/src/prompts/commit-title.prompt.ts +98 -0
  34. package/src/prompts/commit-type.prompt.ts +93 -0
  35. package/src/prompts/runnable.ts +13 -0
  36. package/src/utils/build-branch.test.ts +141 -0
  37. package/src/utils/build-branch.ts +46 -0
  38. package/src/utils/build-commit-string.test.ts +253 -0
  39. package/src/utils/build-commit-string.ts +158 -0
  40. package/src/utils/commit-title-size.ts +24 -0
  41. package/src/utils/infer.test.ts +83 -0
  42. package/src/utils/infer.ts +114 -0
  43. package/src/utils/messages.ts +25 -0
  44. package/src/utils/no-interactive-branch-validation.test.ts +170 -0
  45. package/src/utils/no-interactive-validation.test.ts +174 -0
  46. package/src/utils/no-interactive-validation.ts +190 -0
  47. package/src/utils.ts +59 -66
  48. package/src/valibot-consts.ts +2 -2
  49. package/src/valibot-state.test.ts +48 -0
  50. package/src/valibot-state.ts +133 -130
  51. package/tsconfig.json +3 -2
  52. package/vitest.config.ts +8 -0
  53. package/dist/chunk-K2RPF2JY.js +0 -4
@@ -30,6 +30,10 @@
30
30
  "value": "build",
31
31
  "label": "build"
32
32
  },
33
+ {
34
+ "value": "help",
35
+ "label": "help"
36
+ },
33
37
  {
34
38
  "value": "",
35
39
  "label": "none"
@@ -0,0 +1,27 @@
1
+ name: Test
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches:
7
+ - main
8
+
9
+ jobs:
10
+ test:
11
+ name: npm test
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - name: Checkout
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Setup Node.js
18
+ uses: actions/setup-node@v4
19
+ with:
20
+ node-version: "lts/*"
21
+ cache: npm
22
+
23
+ - name: Install dependencies
24
+ run: npm ci
25
+
26
+ - name: Run tests
27
+ run: npm run test
package/dist/branch.js CHANGED
@@ -1,2 +1,28 @@
1
1
  #! /usr/bin/env node
2
- import{d as f,e as g,h as _,i as b,q as w,s as y,u as h}from"./chunk-K2RPF2JY.js";import*as r from"@clack/prompts";import{execSync as l}from"child_process";import C from"configstore";import s from"picocolors";import{chdir as k}from"process";import{parse as $}from"valibot";O(y(" better-branch "));async function O(e){let o=$(f,{});k(h());let a="branch";if(e.worktrees.enable){let t=await r.select({message:"Checkout a branch or create a worktree?",initialValue:e.branch_action_default,options:w});r.isCancel(t)&&process.exit(),a=t}if(e.branch_user.enable){let t=N(),n=e.branch_user.required,p=await r.text({message:`Type your git username ${n?"":_} ${b}`.trim(),placeholder:"",initialValue:t,validate:x=>{if(n&&!x)return"Please enter a username"}});r.isCancel(p)&&process.exit(0),o.user=p?.replace(/\s+/g,"-")?.toLowerCase()??"",T(o.user)}if(e.branch_type.enable){let t=e.commit_type.initial_value,n=await r.select({message:"Select a branch type",initialValue:t,options:e.commit_type.options});r.isCancel(n)&&process.exit(0),o.type=n}if(e.branch_ticket.enable){let t=e.branch_ticket.required,n=await r.text({message:`Type ticket / issue number ${t?"":_}`.trim(),placeholder:"",validate:p=>{if(t&&!p)return"Please enter a ticket / issue"}});r.isCancel(n)&&process.exit(0),o.ticket=n}if(e.branch_version.enable){let t=e.branch_version.required,n=await r.text({message:`Type version number ${t?"":_}`.trim(),placeholder:"",validate:p=>{if(t&&!p)return"Please enter a version"}});r.isCancel(n)&&process.exit(0),o.version=n}let c=e.branch_description.max_length,i=await r.text({message:"Type a short description",placeholder:"",validate:t=>{if(!t)return"Please enter a description";if(t.length>c)return`Exceeded max length. Description max [${c}]`}});r.isCancel(i)&&process.exit(0),o.description=i?.replace(/\s+/g,"-")?.toLowerCase()??"",(a==="worktree"?e.worktree_pre_commands:e.branch_pre_commands).forEach(t=>{try{l(t,{stdio:"inherit"})}catch(n){r.log.error("Something went wrong when executing pre-commands: "+n),process.exit(0)}});let m=v(o,e),d=A(m);if(a==="branch")try{l(`git ${g.git_args} checkout ${d} ${m}`,{stdio:"inherit"}),r.log.info(`Switched to a new branch '${s.bgGreen(" "+s.black(m)+" ")}'`)}catch{process.exit(0)}else try{let t=S(o,e);l(`git worktree add ${t} ${d} ${m}`,{stdio:"inherit"}),r.log.info(`Created a new worktree ${s.bgGreen(+" "+s.black(t)+" ")}, checked out branch ${s.bgGreen(" "+s.black(m)+" ")}`),r.log.info(s.bgMagenta(s.black(` cd ${t} `))+" to navigate to your new worktree"),k(t)}catch{process.exit(0)}(a==="worktree"?e.worktree_post_commands:e.branch_post_commands).forEach(t=>{try{l(t,{stdio:"inherit"})}catch(n){r.log.error("Something went wrong when executing post-commands: "+n),process.exit(0)}})}function v(e,o){let a="";return o.branch_order.forEach(c=>{let i=`branch_${c}`;e[c]&&(a+=e[c]+o[i].separator)}),a.endsWith("-")||a.endsWith("/")||a.endsWith("_")?a.slice(0,-1).trim():a.trim()}function S(e,o){let c=h().split("/").pop()||"repo",i=o.worktrees.folder_template;i=i.replace("{{repo_name}}",c).replace("{{branch_description}}",e.description).replace("{{user}}",e.user||"").replace("{{type}}",e.type||"").replace("{{ticket}}",e.ticket||"").replace("{{version}}",e.version||""),i=i.replace(/\s/g,"").replace(/--+/g,"-").replace(/^-+|-+$/g,"");let u=o.worktrees.base_path;return`${u}${u.endsWith("/")?"":"/"}${i}`}function N(){try{return new C("better-commits").get("username")??""}catch{r.log.warn('There was an issue accessing username from cache. Check that the folder "~/.config" exists')}return""}function A(e){let o="";try{l(`git show-ref ${e}`,{encoding:"utf-8"}),r.log.warning(s.yellow(`${e} already exists! Checking out existing branch.`))}catch{o="-b"}return o}function T(e){try{new C("better-commits").set("username",e)}catch{}}
2
+ import{c as V,e as H,f as D,i as h}from"./chunk-SIF4LZUS.js";import{f as T,j as A,k as F,l as E,m as u,n as _,q as G,r as L}from"./chunk-OFJCRS3N.js";import at from"configstore";import{chdir as ct}from"process";import{ValiError as pt,parse as U}from"valibot";var a=class{constructor(t,r,n){this.config=t;this.branch_state=r;this.prompt_cache=n}};import*as l from"@clack/prompts";var f=class extends a{async run(){if(this.#e){let t=await l.select({message:this.#t,initialValue:this.#r,options:A});l.isCancel(t)&&process.exit(),this.#n(t)}}get#t(){return"Checkout a branch or create a worktree?"}get#e(){return this.config.worktrees.enable}get#r(){return this.branch_state.checkout||this.config.branch_action_default}#n(t){this.branch_state.checkout=t}};import*as b from"@clack/prompts";var d=class extends a{async run(){if(!this.#t)return;let t=await b.text({message:this.#r,placeholder:"",initialValue:this.#n,validate:r=>this.#i(r)});b.isCancel(t)&&process.exit(0),this.#o(t??"")}get#t(){return this.config.branch_user.enable}get#e(){return this.config.branch_user.required}get#r(){return this.#e?"Type your git username":h("Type your git username")}get#n(){return this.branch_state.user||G(this.prompt_cache,"username")}#i(t){if(this.#e&&!t)return"Please enter a username"}#o(t){this.branch_state.user=t.replace(/\s+/g,"-").toLowerCase(),L(this.prompt_cache,"username",this.branch_state.user)}};import*as v from"@clack/prompts";var y=class extends a{async run(){if(!this.#t)return;let t=await v.select({message:this.#e,initialValue:this.#r,options:this.#n});v.isCancel(t)&&process.exit(0),this.#i(t)}get#t(){return this.config.branch_type.enable}get#e(){return"Select a branch type"}get#r(){return this.branch_state.type||this.config.commit_type.initial_value}get#n(){return this.config.commit_type.options}#i(t){this.branch_state.type=t}};import*as w from"@clack/prompts";var k=class extends a{async run(){if(!this.#t)return;let t=await w.text({message:this.#r,placeholder:"",validate:r=>this.#n(r),initialValue:this.branch_state.ticket});w.isCancel(t)&&process.exit(0),this.#i(t??"")}get#t(){return this.config.branch_ticket.enable}get#e(){return this.config.branch_ticket.required}get#r(){return this.#e?"Type ticket / issue number":h("Type ticket / issue number")}#n(t){if(this.#e&&!t)return"Please enter a ticket / issue"}#i(t){this.branch_state.ticket=t}};import*as C from"@clack/prompts";var x=class extends a{async run(){if(!this.#t)return;let t=await C.text({message:this.#r,placeholder:"",validate:r=>this.#n(r),initialValue:this.branch_state.version});C.isCancel(t)&&process.exit(0),this.#i(t??"")}get#t(){return this.config.branch_version.enable}get#e(){return this.config.branch_version.required}get#r(){return this.#e?"Type version number":h("Type version number")}#n(t){if(this.#e&&!t)return"Please enter a version"}#i(t){this.branch_state.version=t}};import*as I from"@clack/prompts";var $=class extends a{async run(){let t=await I.text({message:this.#t,placeholder:"",validate:r=>this.#r(r),initialValue:this.branch_state.description});I.isCancel(t)&&process.exit(0),this.#n(t??"")}get#t(){return"Type a short description"}get#e(){return this.config.branch_description.max_length}#r(t){if(!t)return"Please enter a description";if(t.length>this.#e)return`Exceeded max length. Description max [${this.#e}]`}#n(t){this.branch_state.description=t.replace(/\s+/g,"-").toLowerCase()}};import*as m from"@clack/prompts";import{execSync as S}from"child_process";import p from"picocolors";import{chdir as rt}from"process";import{parse as X}from"@bomb.sh/args";var q=["user","type","description","ticket","branch-version","checkout"],Y=["git-dir","work-tree"],Z=["interactive","dry-run","help","version"],P=class{#t;constructor(t){this.#t=t}get interactive(){return!this.#t.no_interactive}get dry_run(){return this.#t.dry_run}get help(){return this.#t.help}get version(){return this.#t.version}get git_args(){return this.#t.git_args}get branch_state(){return this.#t.branch_state}},s=new P(tt(process.argv.slice(2)));function tt(e){let t=X(e,{alias:{h:"help",v:"version"},boolean:Z,string:[...q,...Y]}),r={};return q.forEach(n=>{let i=t[n];if(i){let o=n==="branch-version"?"version":n.replace("-","_");o==="checkout"?r[o]=i??"branch":r[o]=i}}),{help:t.help===!0,version:t.version===!0,git_args:et(t["git-dir"],t["work-tree"]),no_interactive:t.interactive===!1,dry_run:t["dry-run"]===!0,branch_state:r}}function et(e,t){return`${e?`--git-dir=${e}`:""} ${t?`--work-tree=${t}`:""}`.trim()}function j(e,t){let r="";return t.branch_order.forEach(n=>{let i=`branch_${n}`;e[n]&&(r+=e[n]+t[i].separator)}),r.endsWith("-")||r.endsWith("/")||r.endsWith("_")?r.slice(0,-1).trim():r.trim()}function W(e,t,r){let n=r.split("/").pop()||"repo",i=t.worktrees.folder_template;i=i.replace("{{repo_name}}",n).replace("{{branch_description}}",e.description).replace("{{user}}",e.user||"").replace("{{type}}",e.type||"").replace("{{ticket}}",e.ticket||"").replace("{{version}}",e.version||""),i=i.replace(/\s/g,"").replace(/--+/g,"-").replace(/^-+|-+$/g,"");let o=t.worktrees.base_path;return`${o}${o.endsWith("/")?"":"/"}${i}`}var g=class extends a{async run(){this.#i(),this.#o(),this.#a()}get#t(){return this.branch_state.checkout==="worktree"}get#e(){return this.#t?this.config.worktree_pre_commands:this.config.branch_pre_commands}get#r(){return this.#t?this.config.worktree_post_commands:this.config.branch_post_commands}get#n(){return j(this.branch_state,this.config)}#i(){this.#s(this.#e,"Something went wrong when executing pre-commands: ")}#o(){let t=this.#n,r=this.#c(t);if(!this.#t){try{S(`git ${s.git_args} checkout ${r} ${t}`,{stdio:"inherit"}),m.log.info(`Switched to a new branch '${p.bgGreen(" "+p.black(t)+" ")}'`)}catch{process.exit(0)}return}try{let n=W(this.branch_state,this.config,u(s.git_args));S(`git ${s.git_args} worktree add ${n} ${r} ${t}`,{stdio:"inherit"}),m.log.info(`Created a new worktree ${p.bgGreen(" "+p.black(n)+" ")}, checked out branch ${p.bgGreen(" "+p.black(t)+" ")}`),m.log.info(p.bgMagenta(p.black(` cd ${n} `))+" to navigate to your new worktree"),rt(n)}catch{process.exit(0)}}#a(){this.#s(this.#r,"Something went wrong when executing post-commands: ")}#s(t,r){t.forEach(n=>{try{S(n,{stdio:"inherit"})}catch(i){m.log.error(r+i),process.exit(0)}})}#c(t){let r="";try{S(`git ${s.git_args} show-ref ${t}`,{encoding:"utf-8"}),m.log.warning(p.yellow(`${t} already exists! Checking out existing branch.`))}catch{r="-b"}return r}};import{execSync as nt}from"child_process";import c from"picocolors";var it={"--interactive":"Run in interactive prompt mode (default behavior).","--dry-run":"Print branch commands without creating a branch or worktree.","--help":"Show help information and exit."},ot={"--user":"Set branch username segment.","--type":"Set branch type (for example feat, fix, docs).","--description":"Set branch description segment.","--ticket":"Set branch ticket/issue segment.","--branch-version":"Set branch version segment.","--checkout":"Choose branch or worktree checkout mode."},st={"--git-dir":"Set the path to the .git directory.","--work-tree":"Set the path to the working tree root."};function N(e){let n=" ";return Object.entries(e).map(([i,o])=>{let B=Math.max(2,26-i.length);return`${n}${i}${" ".repeat(B)}${o}`}).join(`
3
+ `)}function M(e,t){let r=_(),n="(none)";try{n=nt(`git ${s.git_args} branch --show-current`,{stdio:"pipe"}).toString().trim()||"(none)"}catch{}let i=H(e.commit_type.options,s.git_args)||"Unknown",o=e.check_ticket.infer_ticket?D({append_hashtag:e.check_ticket.append_hashtag,prepend_hashtag:e.check_ticket.prepend_hashtag},s.git_args)||"Unknown":"Infer Disabled",B=e.commit_type.options.map(R=>R.value).join(", ").trim(),z=e.commit_scope.options.map(R=>R.value).join(", ").trim(),J=N(it),K=N(st),Q=N(ot);console.log(`
4
+ ${c.green("\uF489 better-branch")} ${c.gray("v"+r)}
5
+
6
+ ${c.gray("BRANCH")}
7
+ ${n}
8
+ ${c.gray("Type")} ${c.blue(i)} ${c.gray("\xB7")} ${c.gray("Ticket")} ${c.magenta(o)}
9
+
10
+ ${c.gray("CONFIGURATION")}
11
+ ${t}
12
+
13
+ ${c.gray("Types")}
14
+ ${B}
15
+
16
+ ${c.gray("Scopes")}
17
+ ${z}
18
+
19
+ ${c.gray("CLI FLAGS")}
20
+ ${J}
21
+
22
+ ${c.gray("Branch Flags")}
23
+ ${Q}
24
+
25
+ ${c.gray("Git Flags (Advanced)")}
26
+ ${K}
27
+
28
+ `)}import*as O from"@clack/prompts";var ht=[f,d,y,k,x,$,g],{config:mt,config_source:gt}=E(" better-branch ",s.git_args);ut(mt,gt);async function ut(e,t){if(ct(u(s.git_args)),s.version){let o=_();O.log.step("Better Commits v"+o);return}if(s.help){M(e,t);return}let r=U(T,s.branch_state);if(!s.interactive)try{U(V(e),r)}catch(o){o instanceof pt?O.log.error(`Invalid branch input: ${o.message}`):O.log.error(`Failed to validate branch input: ${o}`),process.exit(0)}let n=e.cache_last_value?new at("better-commits"):F,i=s.interactive?ht:[g];for(let o of i)await new o(e,r,n).run()}
@@ -0,0 +1,4 @@
1
+ import*as t from"valibot";import*as s from"valibot";var p="custom",d=["closes","trailer","breaking-change","deprecated","custom"],_=s.picklist(["branch","worktree"]),g=s.picklist(["closes","trailer","breaking-change","deprecated","custom"]),O=s.picklist(["user","version","type","ticket","description"]),U=s.picklist(["branch_user","branch_version","branch_type","branch_ticket","branch_description"]),C=["user","version","type","ticket","description"],y=[{value:"app",label:"app"},{value:"shared",label:"shared"},{value:"server",label:"server"},{value:"tools",label:"tools"},{value:"",label:"none"}],S=[{value:"feat",label:"feat",hint:"A new feature",emoji:"\u{1F31F}",trailer:"Changelog: feature"},{value:"fix",label:"fix",hint:"A bug fix",emoji:"\u{1F41B}",trailer:"Changelog: fix"},{value:"docs",label:"docs",hint:"Documentation only changes",emoji:"\u{1F4DA}",trailer:"Changelog: documentation"},{value:"refactor",label:"refactor",hint:"A code change that neither fixes a bug nor adds a feature",emoji:"\u{1F528}",trailer:"Changelog: refactor"},{value:"perf",label:"perf",hint:"A code change that improves performance",emoji:"\u{1F680}",trailer:"Changelog: performance"},{value:"test",label:"test",hint:"Adding missing tests or correcting existing tests",emoji:"\u{1F6A8}",trailer:"Changelog: test"},{value:"build",label:"build",hint:"Changes that affect the build system or external dependencies",emoji:"\u{1F6A7}",trailer:"Changelog: build"},{value:"ci",label:"ci",hint:"Changes to our CI configuration files and scripts",emoji:"\u{1F916}",trailer:"Changelog: ci"},{value:"chore",label:"chore",hint:"Other changes that do not modify src or test files",emoji:"\u{1F9F9}",trailer:"Changelog: chore"},{value:"",label:"none"}];var E=t.pipe(t.optional(t.object({enable:t.optional(t.boolean(),!0),initial_value:t.optional(t.string(),"feat"),max_items:t.optional(t.pipe(t.number(),t.minValue(1)),20),infer_type_from_branch:t.optional(t.boolean(),!0),append_emoji_to_label:t.optional(t.boolean(),!1),append_emoji_to_commit:t.optional(t.boolean(),!1),emoji_commit_position:t.optional(t.picklist(["Start","After-Colon"]),"Start"),options:t.optional(t.array(t.object({value:t.string(),label:t.optional(t.string()),hint:t.optional(t.string()),emoji:t.optional(t.pipe(t.string(),t.emoji())),trailer:t.optional(t.string())})),S)}),{}),t.rawCheck(({dataset:o,addIssue:e})=>{o.typed&&!o.value.options.some(n=>n.value===o.value.initial_value)&&e({message:`Type: initial_value "${o.value.initial_value}" must exist in options`})}),t.transform(o=>({...o,options:o.options.map(e=>({...e,label:e.emoji&&o.append_emoji_to_label?`${e.emoji} ${e.label}`:e.label}))}))),N=t.pipe(t.optional(t.object({enable:t.optional(t.boolean(),!0),custom_scope:t.optional(t.boolean(),!1),max_items:t.optional(t.pipe(t.number(),t.minValue(1)),20),initial_value:t.optional(t.string(),"app"),options:t.optional(t.array(t.object({value:t.string(),label:t.optional(t.string()),hint:t.optional(t.string())})),y)}),{}),t.rawCheck(({dataset:o,addIssue:e})=>{if(!o.typed)return;let n=o.value.options.map(i=>i.value);o.value.custom_scope&&n.push(p),n.includes(o.value.initial_value)||e({message:`Scope: initial_value "${o.value.initial_value}" must exist in options`})}),t.transform(o=>{let e=o.options.map(n=>n.value);return o.custom_scope&&!e.includes(p)?{...o,options:[...o.options,{label:p,value:p,hint:"Write a custom scope"}]}:o})),m=t.object({check_status:t.optional(t.boolean(),!0),commit_type:E,commit_scope:N,check_ticket:t.optional(t.object({infer_ticket:t.optional(t.boolean(),!0),confirm_ticket:t.optional(t.boolean(),!0),add_to_title:t.optional(t.boolean(),!0),append_hashtag:t.optional(t.boolean(),!1),prepend_hashtag:t.optional(t.picklist(["Never","Always","Prompt"]),"Never"),surround:t.optional(t.picklist(["","()","[]","{}"]),""),title_position:t.optional(t.picklist(["start","end","before-colon","beginning"]),"start")}),{}),commit_title:t.optional(t.object({max_size:t.optional(t.pipe(t.number(),t.minValue(1)),70)}),{}),commit_body:t.optional(t.object({enable:t.optional(t.boolean(),!0),required:t.optional(t.boolean(),!1),split_by_period:t.optional(t.boolean(),!1)}),{}),commit_footer:t.optional(t.object({enable:t.optional(t.boolean(),!0),initial_value:t.optional(t.array(g),[]),options:t.optional(t.array(g),d)}),{}),breaking_change:t.optional(t.object({add_exclamation_to_title:t.optional(t.boolean(),!0)}),{}),cache_last_value:t.optional(t.boolean(),!0),confirm_with_editor:t.optional(t.boolean(),!1),confirm_commit:t.optional(t.boolean(),!0),print_commit_output:t.optional(t.boolean(),!0),branch_pre_commands:t.optional(t.array(t.string()),[]),branch_post_commands:t.optional(t.array(t.string()),[]),worktree_pre_commands:t.optional(t.array(t.string()),[]),worktree_post_commands:t.optional(t.array(t.string()),[]),branch_user:t.optional(t.object({enable:t.optional(t.boolean(),!0),required:t.optional(t.boolean(),!1),separator:t.optional(t.picklist(["/","-","_"]),"/")}),{}),branch_type:t.optional(t.object({enable:t.optional(t.boolean(),!0),separator:t.optional(t.picklist(["/","-","_"]),"/")}),{}),branch_version:t.optional(t.object({enable:t.optional(t.boolean(),!1),required:t.optional(t.boolean(),!1),separator:t.optional(t.picklist(["/","-","_"]),"/")}),{}),branch_ticket:t.optional(t.object({enable:t.optional(t.boolean(),!0),required:t.optional(t.boolean(),!1),separator:t.optional(t.picklist(["/","-","_"]),"-")}),{}),branch_description:t.optional(t.object({max_length:t.optional(t.pipe(t.number(),t.minValue(1)),70),separator:t.optional(t.picklist(["","/","-","_"]),"")}),{}),branch_action_default:t.optional(_,"branch"),branch_order:t.optional(t.array(O),C),enable_worktrees:t.optional(t.boolean(),!0),worktrees:t.optional(t.object({enable:t.optional(t.boolean(),!0),base_path:t.optional(t.string(),".."),folder_template:t.optional(t.string(),"{{repo_name}}-{{ticket}}-{{branch_description}}")}),{}),overrides:t.optional(t.object({shell:t.optional(t.string())}),{})}),I={type:t.optional(t.string(),""),scope:t.optional(t.string(),""),title:t.optional(t.string(),""),body:t.optional(t.string(),""),closes:t.optional(t.string(),""),ticket:t.optional(t.string(),""),breaking_title:t.optional(t.string(),""),breaking_body:t.optional(t.string(),""),deprecates:t.optional(t.string(),""),deprecates_title:t.optional(t.string(),""),deprecates_body:t.optional(t.string(),""),custom_footer:t.optional(t.string(),""),trailer:t.optional(t.string(),"")},J=t.optional(t.object(I),{}),j={user:t.optional(t.string(),""),type:t.optional(t.string(),""),ticket:t.optional(t.string(),""),description:t.optional(t.string(),""),version:t.optional(t.string(),""),checkout:t.optional(_,"branch")},Y=t.optional(t.object(j),{});import{parse as R}from"@bomb.sh/args";var k=["type","scope","title","body","closes","ticket","trailer","deprecates","breaking-title","breaking-body","deprecates-title","deprecates-body","custom-footer"],P=["git-dir","work-tree"],w=["interactive","dry-run","help","version"],f=class{#t;constructor(e){this.#t=e}get git_args(){return this.#t.git_args}get interactive(){return!this.#t.no_interactive}get dry_run(){return this.#t.dry_run}get help(){return this.#t.help}get version(){return this.#t.version}get commit_state(){return this.#t.commit_state}},b=new f(F(process.argv.slice(2)));function F(o){let e=R(o,{alias:{h:"help",v:"version"},boolean:w,string:[...k,...P]}),n={};return k.forEach(i=>{let a=e[i];if(a){let v=i.replace("-","_");n[v]=a}}),{help:e.help===!0,version:e.version===!0,git_args:L(e["git-dir"],e["work-tree"]),no_interactive:e.interactive===!1,dry_run:e["dry-run"]===!0,commit_state:n}}function L(o,e){return`${o?`--git-dir=${o}`:""} ${e?`--work-tree=${e}`:""}`.trim()}import*as r from"@clack/prompts";import{execSync as $}from"child_process";import c from"fs";import{homedir as V}from"os";import l from"picocolors";import{ValiError as B,parse as T}from"valibot";var A=".better-commits.json",rt=`${l.dim("(<space> to select)")}`,at=`${l.dim("(<space> to select, <a> to select all)")}`,lt=`${l.dim("(optional)")}`,st=[{value:"closes",label:"closes <issue/ticket>",hint:"Attempts to infer ticket from branch"},{value:"trailer",label:"trailer",hint:"Appends trailer based on commit type"},{value:"breaking-change",label:"breaking change",hint:"Add breaking change"},{value:"deprecated",label:"deprecated",hint:"Add deprecated change"},{value:"custom",label:"custom",hint:"Add a custom footer"}],pt=[{value:"branch",label:"Branch"},{value:"worktree",label:"Worktree"}],ct={get:()=>"",set:(o,e)=>{},clear:()=>{}};function vt(o=" better-commits ",e=b.git_args){console.clear(),r.intro(`${l.bgCyan(l.black(o))}`);let n=null,i=M();c.existsSync(i)&&(n=x(i));let v=`${D(e)}/${A}`;if(c.existsSync(v)){r.log.step("Reading from Repository Config");let u=x(v);return{config:n?{...u,overrides:n.overrides.shell?n.overrides:u.overrides,confirm_with_editor:n.confirm_with_editor,cache_last_value:n.cache_last_value}:u,config_source:"repository"}}if(n)return r.log.step("Reading from Global Config"),{config:n,config_source:"global"};let h=T(m,{});return r.log.step("Config not found. Generating default .better-commit.json at $HOME"),c.writeFileSync(i,JSON.stringify(h,null,4)),{config:h,config_source:"none"}}function x(o){let e=null;try{e=JSON.parse(c.readFileSync(o,"utf8"))}catch(n){r.log.error(`Invalid JSON file. Exiting.
2
+ `+n),process.exit(0)}return H(e)}function H(o){try{return T(m,o)}catch(e){if(e instanceof B){let i=(e.issues[0].path??[]).map(a=>a.key).filter(a=>typeof a=="string"||typeof a=="number").join(".");r.log.error(`Invalid Configuration: ${l.red(i)}
3
+ `+e.message)}process.exit(0)}}function D(o=b.git_args){let e=".";try{e=$(`git ${o} rev-parse --show-toplevel`).toString().trim()}catch{r.log.warn("Could not find git root. If in a --bare repository, ignore this warning.")}return e}function M(){return V()+"/"+A}function ut(){try{return JSON.parse(c.readFileSync(new URL("../package.json",import.meta.url),"utf8")).version??"unknown"}catch{return"unknown"}}function _t(o,e){return e===o.length-1?"":`
4
+ `}function gt(o){let e=o.trim();return e.endsWith(".")?e.substring(0,e.length-1).trim():o.trim()}function mt(o,e){try{return o.get(e)??""}catch{r.log.warn(`Could not access ${e} from cache. Check that "~/.config" exists. Set "cache_last_value" to false to disable.`)}return""}function ft(o,e,n){try{o.set(e,n)}catch{r.log.warn(`Could not access ${e} from cache. Check that "~/.config" exists. Set "cache_last_value" to false to disable.`)}}export{p as a,m as b,I as c,J as d,j as e,Y as f,b as g,A as h,st as i,pt as j,ct as k,vt as l,D as m,ut as n,_t as o,gt as p,mt as q,ft as r};
@@ -0,0 +1 @@
1
+ import{c as m,e as u,g as p}from"./chunk-OFJCRS3N.js";import*as s from"valibot";function _(t,n){let i=t.scope?t.scope.length+2:0,e=t.type?.length??0,r=n.include_ticket?t.ticket?.length??0:0,c=t.title?.length??0;return i+e+r+c}function a(t){return t.map(i=>i===""?'"" (none)':`"${i}"`).join(", ")}function R(t){let n=t.commit_type.options.map(e=>e.value),i=t.commit_scope.options.map(e=>e.value);return s.pipe(s.object(m),s.rawCheck(({dataset:e,addIssue:r})=>{if(!e.typed)return;let c=e.value.type?`"${e.value.type}"`:"(empty)";e.value.type&&!n.includes(e.value.type)&&r({message:`Invalid --type ${c}. Valid types: ${a(n)}.`})}),s.rawCheck(({dataset:e,addIssue:r})=>{if(!e.typed)return;let c=e.value.scope?`"${e.value.scope}"`:"(empty)";e.value.scope&&!t.commit_scope.custom_scope&&!i.includes(e.value.scope)&&r({message:`Invalid --scope ${c}. Valid scopes: ${a(i)}.`})}),s.rawCheck(({dataset:e,addIssue:r})=>{!e.typed||e.value.title.trim()||r({message:"Missing --title. Provide a non-empty commit title."})}),s.rawCheck(({dataset:e,addIssue:r})=>{if(!e.typed)return;let c=_(e.value,{include_ticket:t.check_ticket.add_to_title});c>t.commit_title.max_size&&r({message:`Title exceeds max width. Current size is ${c}, max is ${t.commit_title.max_size} (includes type, scope, and ticket when enabled).`})}),s.rawCheck(({dataset:e,addIssue:r})=>{e.typed&&t.commit_body.required&&!e.value.body.trim()&&r({message:"Missing --body. commit_body.required is enabled in config."})}),s.rawCheck(({dataset:e,addIssue:r})=>{e.typed&&e.value.closes&&!e.value.ticket&&r({message:'Invalid footer values: --closes requires --ticket (for example: --ticket "ABC-123").'})}),s.rawCheck(({dataset:e,addIssue:r})=>{e.typed&&e.value.breaking_body&&!e.value.breaking_title&&r({message:"Invalid breaking change values: --breaking-body requires --breaking-title."})}),s.rawCheck(({dataset:e,addIssue:r})=>{e.typed&&e.value.deprecates_body&&!e.value.deprecates_title&&r({message:"Invalid deprecation values: --deprecates-body requires --deprecates-title."})}))}function q(t){let n=t.commit_type.options.map(i=>i.value);return s.pipe(s.object(u),s.rawCheck(({dataset:i,addIssue:e})=>{if(!i.typed)return;let r=i.value.type?`"${i.value.type}"`:"(empty)";i.value.type&&!n.includes(i.value.type)&&e({message:`Invalid --type ${r}. Valid types: ${a(n)}.`})}),s.rawCheck(({dataset:i,addIssue:e})=>{!i.typed||i.value.description.trim()||e({message:"Missing --description. Provide a non-empty branch description."})}),s.rawCheck(({dataset:i,addIssue:e})=>{if(!i.typed)return;let r=i.value.description.trim();r.length>t.branch_description.max_length&&e({message:`Description exceeds max length. Current length is ${r.length}, max is ${t.branch_description.max_length}.`})}),s.rawCheck(({dataset:i,addIssue:e})=>{i.typed&&t.branch_user.required&&!i.value.user.trim()&&e({message:"Missing --user. branch_user.required is enabled in config."})}),s.rawCheck(({dataset:i,addIssue:e})=>{i.typed&&t.branch_ticket.required&&!i.value.ticket.trim()&&e({message:"Missing --ticket. branch_ticket.required is enabled in config."})}),s.rawCheck(({dataset:i,addIssue:e})=>{i.typed&&t.branch_version.required&&!i.value.version.trim()&&e({message:"Missing --branch-version. branch_version.required is enabled in config."})}))}import{execSync as h}from"child_process";var f=/\/(\w+-\d+)/,v=/^(\w+-\d+)/,y=/^([A-Z]+-[\[a-zA-Z\]\d]+)_/,k=/\/([A-Z]+-[\[a-zA-Z\]\d]+)_/,b=/\/(\d+)/,$=/^(\d+)/;function H(t){if(p.interactive)return;let n={ticket:"",type:""};if(t.check_ticket.infer_ticket){let e=w({append_hashtag:t.check_ticket.append_hashtag,prepend_hashtag:t.check_ticket.prepend_hashtag},p.git_args);n.ticket=e}let i=C(t.commit_type.options,p.git_args);return n.type=i,n}function C(t,n){let i=l(n);return i?E(i,t.map(e=>e.value)):""}function w(t,n){let i=l(n);return i?x(i,t):""}function x(t,n){let i=[t.match(y),t.match(k),t.match(f),t.match(b),t.match(v),t.match($)].filter(e=>e!=null).map(e=>e&&e.length>=2?e[1]:"");return!i.length||!i[0]?"":n.append_hashtag||n.prepend_hashtag==="Always"?`#${i[0]}`:i[0]}function E(t,n){return n.find(e=>{let r=new RegExp(`^${e}-`),c=new RegExp(`-${e}-`),g=new RegExp(`${e}/`);return[t.match(r),t.match(c),t.match(g)].filter(d=>d!=null).length>0})??""}function l(t){try{return h(`git ${t} branch --show-current`,{stdio:"pipe"}).toString().trim()}catch{return""}}import o from"picocolors";function X(t){return`${t} ${o.dim("\xB7 restored from cache")}`}function U(t){return`${t} ${o.dim("\xB7 inferred from branch")}`}function Z(t){return`${t} ${o.dim("\xB7 optional")}`}function j(t){return`${t} ${o.dim("\xB7 <space> to select")}`}function D(t){return`${t} ${o.dim("\xB7 dry run - changes will not be committed")}`}export{_ as a,R as b,q as c,H as d,C as e,w as f,X as g,U as h,Z as i,j,D as k};
package/dist/index.js CHANGED
@@ -1,32 +1,57 @@
1
1
  #! /usr/bin/env node
2
- import{a as A,c as T,e as g,g as x,h as b,j as R,k as v,l as P,m as j,n as G,o as N,p as D,r as I,s as W,t as M,u as V,v as C,w as H,x as k,y}from"./chunk-K2RPF2JY.js";import*as s from"@clack/prompts";import l from"picocolors";import{execSync as w}from"child_process";import{chdir as q}from"process";import{parse as K}from"valibot";import{execSync as X}from"child_process";import*as $ from"@clack/prompts";import E from"picocolors";var L=["M","T","R","D","A","C"];function S(){let e="";try{e=X(`git ${g.git_args} status --porcelain`,{stdio:"pipe"}).toString()}catch(u){return $.log.error(E.red("Failed to git status"+u)),{index:[],work_tree:[]}}let t=e.split(`
3
- `),o=[],f=[];return t.forEach(u=>{let a=u.trimEnd();if(!a)return;let p=a.substring(2).trim(),h=a.charAt(0).trim(),i=a.charAt(1).trim();(h==="?"||i==="?")&&o.push(p),L.includes(h)&&f.push(p),L.includes(i)&&o.push(p)}),{index:f,work_tree:o}}function F(e){let t=e.join(" ");if(t)try{X(`git ${g.git_args} add ${t}`,{stdio:"pipe"}).toString(),$.log.success(E.green("Changes successfully staged"))}catch{$.log.error(E.red("Failed to stage changes"))}}import Y from"configstore";J(W());async function J(e){let t=K(T,{});q(V());let o=e.cache_last_value?new Y("better-commits"):I;if(e.check_status){let{index:i,work_tree:r}=S();s.log.step(l.black(l.bgGreen(" Checking Git Status ")));let n=i.reduce((c,m,d)=>l.green(c+m+C(i,d)),"");if(s.log.success(`Changes to be committed:
4
- `+n),r.length){let c=r.reduce((d,U,B)=>l.red(d+U+C(r,B)),"");s.log.error(`Changes not staged for commit:
5
- `+c);let m=await s.multiselect({message:`Some files have not been staged, would you like to add them now? ${x}`,options:[{value:".",label:"."},...r.map(d=>({value:d,label:d}))],required:!1});s.isCancel(m)&&process.exit(0),F(m)}S().index.length||(s.log.error(l.red('no changes added to commit (use "git add" and/or "git commit -a")')),process.exit(0))}let f=e.commit_type.options.reduce((i,r)=>({...i,[r.value]:{emoji:r.emoji??"",trailer:r.trailer??""}}),{});if(e.commit_type.enable){let i="Select a commit type",r=e.commit_type.initial_value;if(e.commit_type.infer_type_from_branch){let _=e.commit_type.options.map(m=>m.value),c=M(_);c&&(i=`Commit type inferred from branch ${l.dim("(confirm / edit)")}`,r=c)}let n=await s.select({message:i,initialValue:k(o,"commit_type")||r,maxItems:e.commit_type.max_items,options:e.commit_type.options});s.isCancel(n)&&process.exit(0),y(o,"commit_type",n),t.trailer=f[n].trailer,t.type=e.commit_type.append_emoji_to_commit&&e.commit_type.emoji_commit_position==="Start"?`${f[n].emoji} ${n}`.trim():n}if(e.commit_scope.enable){let i=await s.select({message:"Select a commit scope",initialValue:k(o,"commit_scope")||e.commit_scope.initial_value,maxItems:e.commit_scope.max_items,options:e.commit_scope.options});s.isCancel(i)&&process.exit(0),y(o,"commit_scope",i),i===A&&e.commit_scope.custom_scope&&(i=await s.text({message:"Write a custom scope",placeholder:""}),s.isCancel(i)&&process.exit(0)),t.scope=i}if(e.check_ticket.infer_ticket)try{let i=w(`git ${g.git_args} branch --show-current`,{stdio:"pipe"}).toString(),r=[i.match(P),i.match(j),i.match(R),i.match(G),i.match(v),i.match(N)].filter(n=>n!=null).map(n=>n&&n.length>=2?n[1]:"");r.length&&r[0]&&(t.ticket=e.check_ticket.append_hashtag||e.check_ticket.prepend_hashtag==="Prompt"?"#"+r[0]:r[0])}catch{}if(e.check_ticket.confirm_ticket){let i=await s.text({message:t.ticket?`Ticket / issue inferred from branch ${l.dim("(confirm / edit)")}`:`Add ticket / issue ${b}`,placeholder:"",initialValue:k(o,"commit_ticket")||t.ticket});s.isCancel(i)&&process.exit(0),y(o,"commit_ticket",i),t.ticket=i??""}e.check_ticket.prepend_hashtag==="Always"&&t.ticket&&!t.ticket.startsWith("#")&&(t.ticket="#"+t.ticket);let u=await s.text({message:"Write a brief title describing the commit",initialValue:k(o,"commit_title")||"",placeholder:"",validate:i=>{if(!i)return"Please enter a title";let r=t.scope?t.scope.length+2:0,n=t.type.length,_=e.check_ticket.add_to_title?t.ticket.length:0;if(r+n+_+i.length>e.commit_title.max_size)return`Exceeded max length. Title max [${e.commit_title.max_size}]`}});s.isCancel(u)&&process.exit(0),y(o,"commit_title",u);let a=u;if(e.commit_type.append_emoji_to_commit&&e.commit_type.emoji_commit_position==="After-Colon"&&(a=`${f[t.type].emoji} ${u}`),t.title=H(a),e.commit_body.enable){let i=await s.text({message:`Write a detailed description of the changes ${b}`,initialValue:k(o,"commit_body")||"",placeholder:"",validate:r=>{if(e.commit_body.required&&!r)return"Please enter a description"}});if(s.isCancel(i)&&process.exit(0),t.body=i??"",e.commit_body.split_by_period){let r=t.body.split(/\.\s+/).map(n=>n.trim());t.body=r.join(`.
6
- `)}y(o,"commit_body",i)}if(e.commit_footer.enable){let i=k(o,"commit_footer").split(","),r=await s.multiselect({message:`Select optional footers ${x}`,initialValues:i||e.commit_footer.initial_value,options:D,required:!1});if(s.isCancel(r)&&process.exit(0),y(o,"commit_footer",r.join(",")),r.includes("breaking-change")){let n=await s.text({message:"Breaking changes: Write a short title / summary",placeholder:"",validate:c=>{if(!c)return"Please enter a title / summary"}});s.isCancel(n)&&process.exit(0);let _=await s.text({message:`Breaking Changes: Write a description & migration instructions ${b}`,placeholder:""});s.isCancel(_)&&process.exit(0),t.breaking_title=n,t.breaking_body=_}if(r.includes("deprecated")){let n=await s.text({message:"Deprecated: Write a short title / summary",placeholder:"",validate:c=>{if(!c)return"Please enter a title / summary"}});s.isCancel(n)&&process.exit(0);let _=await s.text({message:`Deprecated: Write a description ${b}`,placeholder:""});s.isCancel(_)&&process.exit(0),t.deprecates_body=_,t.deprecates_title=n}if(r.includes("closes")&&(t.closes="Closes:"),r.includes("custom")){let n=await s.text({message:"Write a custom footer",placeholder:""});s.isCancel(n)&&process.exit(0),t.custom_footer=n}r.includes("trailer")||(t.trailer="")}if(e.confirm_with_editor){let i=e.overrides.shell?{shell:e.overrides.shell,stdio:"inherit"}:{stdio:"inherit"},r=t.trailer?`--trailer="${t.trailer}"`:"";w(`git ${g.git_args} commit -m "${O(t,e,!1,!0,!1)}" ${r} --edit`,i),process.exit(0)}let p=!0;e.print_commit_output&&s.note(O(t,e,!0,!1,!0),"Commit Preview"),e.confirm_commit&&(p=await s.confirm({message:"Confirm Commit?"}),s.isCancel(p)&&process.exit(0)),p||(s.log.info("Exiting without commit"),process.exit(0));try{s.log.info("Committing changes...");let i=e.overrides.shell?{shell:e.overrides.shell,stdio:"inherit"}:{stdio:"inherit"},r=t.trailer?`--trailer="${t.trailer}"`:"";w(`git ${g.git_args} commit -m "${O(t,e,!1,!0,!1)}" ${r}`,i)}catch(i){s.log.error("Something went wrong when committing: "+i);return}s.log.success("Commit Complete");let h=o.get("username");o.clear(),h&&o.set("username",h)}function O(e,t,o=!1,f=!1,u=!1){let a="";if(e.type&&(a+=o?l.blue(e.type):e.type),e.scope){let c=o?l.cyan(e.scope):e.scope;a+=`(${c})`}let p=e.ticket,h=t.check_ticket.surround;if(e.ticket&&h){let c=h.charAt(0),m=h.charAt(1);p=`${c}${e.ticket}${m}`}let i=t.check_ticket.title_position==="beginning";p&&t.check_ticket.add_to_title&&i&&(a=`${o?l.magenta(p):p} ${a}`);let r=t.check_ticket.title_position==="before-colon";if(p&&t.check_ticket.add_to_title&&r){let c=e.scope||e.type&&!t.check_ticket.surround?" ":"";a+=o?l.magenta(c+p):c+p}e.breaking_title&&t.breaking_change.add_exclamation_to_title&&(a+=o?l.red("!"):"!"),(e.scope||e.type||p&&r)&&(a+=": ");let n=t.check_ticket.title_position==="start",_=t.check_ticket.title_position==="end";if(p&&t.check_ticket.add_to_title&&n&&(a+=o?l.magenta(p)+" ":p+" "),e.title&&(a+=o?l.reset(e.title):e.title),p&&t.check_ticket.add_to_title&&_&&(a+=" "+(o?l.magenta(p):p)),e.body){let m=e.body.split("\\n").map(d=>o?l.reset(d.trim()):d.trim()).join(`
7
- `);a+=o?`
2
+ import{a as nt,b as at,d as ct,e as F,f as P,g as f,h as A,i as y,j as $,k as Y}from"./chunk-SIF4LZUS.js";import{a as Z,d as z,g as s,i as tt,k as et,l as it,m as ot,n as I,o as rt,p as st,q as g,r as u}from"./chunk-OFJCRS3N.js";import{chdir as kt}from"process";import*as H from"@clack/prompts";import{ValiError as vt,parse as ut}from"valibot";import Ct from"configstore";import*as T from"@clack/prompts";var m=class{constructor(t,e,o){this.config=t;this.commit_state=e;this.prompt_cache=o}};var R=class extends m{async run(){if(this.#o){let{initial_value:t,message:e}=this.#e,o=await T.select({message:e,initialValue:t,maxItems:this.#r,options:this.#t});T.isCancel(o)&&process.exit(0),this.#s(o)}}get#o(){return this.config.commit_type.enable}get#e(){let t=g(this.prompt_cache,"commit_type");if(t)return{initial_value:t,message:f("Commit type")};if(this.config.commit_type.infer_type_from_branch){let e=F(this.#t,s.git_args);if(e)return{message:A("Commit type"),initial_value:e}}return{initial_value:this.config.commit_type.initial_value,message:"Select a commit type"}}get#t(){return this.config.commit_type.options}get#i(){return this.#t.reduce((t,e)=>({...t,[e.value]:{emoji:e.emoji??"",trailer:e.trailer??""}}),{})}get#r(){return this.config.commit_type.max_items}#s(t){u(this.prompt_cache,"commit_type",t);let e=this.#i;this.commit_state.trailer=e[t].trailer,this.commit_state.type=this.config.commit_type.append_emoji_to_commit&&this.config.commit_type.emoji_commit_position==="Start"?`${e[t].emoji} ${t}`.trim():t}};import*as k from"@clack/prompts";var j=class extends m{async run(){if(!this.#o)return;let{initial_value:t,message:e}=this.#e(),o=await k.select({message:e,initialValue:t,maxItems:this.#t,options:this.#i});k.isCancel(o)&&process.exit(0),await this.#s(o)}get#o(){return this.config.commit_scope.enable}#e(){let t=g(this.prompt_cache,"commit_scope");return t?{initial_value:t,message:f("Commit scope")}:{initial_value:this.config.commit_scope.initial_value,message:"Select a commit scope"}}get#t(){return this.config.commit_scope.max_items}get#i(){return this.config.commit_scope.options}get#r(){return this.config.commit_scope.custom_scope}async#s(t){u(this.prompt_cache,"commit_scope",t);let e=t;if(e===Z&&this.#r){let o=await k.text({message:"Write a custom scope",placeholder:""});k.isCancel(o)&&process.exit(0),e=o??""}this.commit_state.scope=e}};import*as N from"@clack/prompts";var E=class extends m{async run(){let{initial_value:t,message:e}=this.#i();if(this.commit_state.ticket=t,this.#e){let o=await N.text({message:e,placeholder:"",initialValue:t});N.isCancel(o)&&process.exit(0),u(this.prompt_cache,"commit_ticket",o),this.commit_state.ticket=o??""}this.#t&&this.commit_state.ticket&&!this.commit_state.ticket.startsWith("#")&&(this.commit_state.ticket="#"+this.commit_state.ticket)}get#o(){return this.config.check_ticket.infer_ticket}get#e(){return this.config.check_ticket.confirm_ticket}get#t(){return this.config.check_ticket.prepend_hashtag==="Always"}#i(){let t=g(this.prompt_cache,"commit_ticket");if(t)return{initial_value:t,message:f("Ticket / issue")};if(this.#o){let e=P({append_hashtag:this.config.check_ticket.append_hashtag,prepend_hashtag:this.config.check_ticket.prepend_hashtag},s.git_args);if(e)return{initial_value:e,message:A("Ticket / issue")}}return{initial_value:this.commit_state.ticket,message:y("Add ticket / issue")}}};import*as G from"@clack/prompts";var D=class extends m{async run(){let{initial_value:t,message:e}=this.#o(),o=await G.text({message:e,initialValue:t,placeholder:"",validate:a=>this.#e(a)});G.isCancel(o)&&process.exit(0),this.#n(o??"")}#o(){let t=g(this.prompt_cache,"commit_title");return t?{initial_value:t,message:f("Commit title")}:{initial_value:this.commit_state.title,message:"Write a brief title describing the commit"}}#e(t){if(!t)return"Please enter a title";if(this.#i(t)>this.#t)return`Exceeded max length. Title max [${this.#t}]`}get#t(){return this.config.commit_title.max_size}#i(t){return nt({type:this.commit_state.type,scope:this.commit_state.scope,ticket:this.commit_state.ticket,title:t},{include_ticket:this.config.check_ticket.add_to_title})}get#r(){return this.config.commit_type.options.reduce((t,e)=>({...t,[e.value]:{emoji:e.emoji??""}}),{})}#s(t){return this.config.commit_type.append_emoji_to_commit&&this.config.commit_type.emoji_commit_position==="After-Colon"?`${this.#r[this.commit_state.type]?.emoji??""} ${t}`.trim():t}#n(t){u(this.prompt_cache,"commit_title",t),this.commit_state.title=st(this.#s(t))}};import*as L from"@clack/prompts";var M=class extends m{async run(){if(!this.#o)return;let{initial_value:t,message:e}=this.#e(),o=await L.text({message:e,initialValue:t,placeholder:"",validate:a=>this.#t(a)});L.isCancel(o)&&process.exit(0),this.#r(o??"")}get#o(){return this.config.commit_body.enable}#e(){let t=g(this.prompt_cache,"commit_body");return t?{initial_value:t,message:f("Commit body")}:{initial_value:"",message:y("Write a detailed description of the changes")}}#t(t){if(this.config.commit_body.required&&!t)return"Please enter a description"}#i(t){return this.config.commit_body.split_by_period?t.split(/\.\s+/).map(o=>o.trim()).join(`.
3
+ `):t}#r(t){u(this.prompt_cache,"commit_body",t),this.commit_state.body=this.#i(t)}};import*as h from"@clack/prompts";var V=class extends m{async run(){if(!this.#o)return;let{initial_values:t,message:e}=this.#i(),o=await h.multiselect({message:e,initialValues:t,options:this.#e,required:!1});h.isCancel(o)&&process.exit(0);let a=this.#s(o),r=await this.#n(a);this.#m(o,a,r)}get#o(){return this.config.commit_footer.enable}get#e(){let t=new Set(this.config.commit_footer.options);return tt.filter(e=>t.has(e.value))}get#t(){return this.#e.map(t=>t.value)}#i(){let t=g(this.prompt_cache,"commit_footer");return t?{initial_values:this.#r(t),message:$(f("Commit footers"))}:{initial_values:this.config.commit_footer.initial_value.filter(o=>this.#t.includes(o)),message:$(y("Select optional footers"))}}#r(t){return t.split(",").map(e=>e.trim()).filter(e=>this.#t.includes(e))}#s(t){return{includes_breaking_change:t.includes("breaking-change"),includes_deprecated:t.includes("deprecated"),includes_closes:t.includes("closes"),includes_custom:t.includes("custom"),includes_trailer:t.includes("trailer")}}async#n(t){let e={breaking_title:"",breaking_body:"",deprecated_title:"",deprecated_body:"",custom_footer:""};return t.includes_breaking_change&&(e.breaking_title=await this.#c("Breaking changes: Write a short title / summary"),e.breaking_body=await this.#a(y("Breaking Changes: Write a description & migration instructions"))),t.includes_deprecated&&(e.deprecated_title=await this.#c("Deprecated: Write a short title / summary"),e.deprecated_body=await this.#a(y("Deprecated: Write a description"))),t.includes_custom&&(e.custom_footer=await this.#a("Write a custom footer")),e}async#c(t){let e=await h.text({message:t,placeholder:"",validate:o=>{if(!o)return"Please enter a title / summary"}});return h.isCancel(e)&&process.exit(0),e??""}async#a(t){let e=await h.text({message:t,placeholder:""});return h.isCancel(e)&&process.exit(0),e??""}#m(t,e,o){u(this.prompt_cache,"commit_footer",t.join(",")),this.commit_state.breaking_title=o.breaking_title,this.commit_state.breaking_body=o.breaking_body,this.commit_state.deprecates_title=o.deprecated_title,this.commit_state.deprecates_body=o.deprecated_body,this.commit_state.custom_footer=o.custom_footer,this.commit_state.closes=e.includes_closes?"Closes:":"",e.includes_trailer||(this.commit_state.trailer="")}};import*as d from"@clack/prompts";import{execSync as mt}from"child_process";import p from"picocolors";function J({commit_state:i,config:t,colorize:e=!1,escape_quotes:o=!1,include_trailer:a=!1}){let r="";if(i.type&&(r+=e?p.blue(i.type):i.type),i.scope){let c=e?p.cyan(i.scope):i.scope;r+=`(${c})`}let n=i.ticket,l=t.check_ticket.surround;if(i.ticket&&l){let c=l.charAt(0),C=l.charAt(1);n=`${c}${i.ticket}${C}`}let v=t.check_ticket.title_position==="beginning";n&&t.check_ticket.add_to_title&&v&&(r=`${e?p.magenta(n):n} ${r}`);let w=t.check_ticket.title_position==="before-colon";if(n&&t.check_ticket.add_to_title&&w){let c=i.scope||i.type&&!t.check_ticket.surround?" ":"";r+=e?p.magenta(c+n):c+n}i.breaking_title&&t.breaking_change.add_exclamation_to_title&&(r+=e?p.red("!"):"!"),(i.scope||i.type||n&&w)&&(r+=": ");let U=t.check_ticket.title_position==="start",K=t.check_ticket.title_position==="end";if(n&&t.check_ticket.add_to_title&&U&&(r+=e?p.magenta(n)+" ":n+" "),i.title&&(r+=e?p.reset(i.title):i.title),n&&t.check_ticket.add_to_title&&K&&(r+=" "+(e?p.magenta(n):n)),i.body){let C=i.body.split("\\n").map(x=>e?p.reset(x.trim()):x.trim()).join(`
4
+ `);r+=`
8
5
 
9
- ${m}`:`
6
+ ${C}`}if(i.breaking_title){let c=e?p.red(`BREAKING CHANGE: ${i.breaking_title}`):`BREAKING CHANGE: ${i.breaking_title}`;r+=`
10
7
 
11
- ${m}`}if(e.breaking_title){let c=o?l.red(`BREAKING CHANGE: ${e.breaking_title}`):`BREAKING CHANGE: ${e.breaking_title}`;a+=`
8
+ ${c}`}if(i.breaking_body){let c=e?p.red(i.breaking_body):i.breaking_body;r+=`
12
9
 
13
- ${c}`}if(e.breaking_body){let c=o?l.red(e.breaking_body):e.breaking_body;a+=`
10
+ ${c}`}if(i.deprecates_title){let c=e?p.yellow(`DEPRECATED: ${i.deprecates_title}`):`DEPRECATED: ${i.deprecates_title}`;r+=`
14
11
 
15
- ${c}`}if(e.deprecates_title){let c=o?l.yellow(`DEPRECATED: ${e.deprecates_title}`):`DEPRECATED: ${e.deprecates_title}`;a+=`
12
+ ${c}`}if(i.deprecates_body){let c=e?p.yellow(i.deprecates_body):i.deprecates_body;r+=`
16
13
 
17
- ${c}`}if(e.deprecates_body){let c=o?l.yellow(e.deprecates_body):e.deprecates_body;a+=`
14
+ ${c}`}if(i.custom_footer){let C=i.custom_footer.split("\\n").map(x=>e?p.reset(x.trim()):x.trim()).join(`
15
+ `);r+=`
18
16
 
19
- ${c}`}if(e.custom_footer){let m=e.custom_footer.split("\\n").map(d=>o?l.reset(d.trim()):d.trim()).join(`
20
- `);a+=o?`
17
+ ${C}`}return i.closes&&i.ticket&&(r+=e?`
21
18
 
22
- ${m}`:`
19
+ ${p.reset(i.closes)} ${p.magenta(i.ticket)}`:`
23
20
 
24
- ${m}`}return e.closes&&e.ticket&&(a+=o?`
21
+ ${i.closes} ${i.ticket}`),a&&i.trailer&&(r+=e?`
25
22
 
26
- ${l.reset(e.closes)} ${l.magenta(e.ticket)}`:`
23
+ ${p.dim(i.trailer)}`:`
27
24
 
28
- ${e.closes} ${e.ticket}`),u&&e.trailer&&(a+=o?`
25
+ ${i.trailer}`),o&&(r=r.replaceAll('"','\\"').replaceAll("`","\\`")),r}var O=class extends m{async run(){this.#o&&(mt(`${this.#n} --edit`,this.#i),process.exit(0)),this.#e&&d.note(J({commit_state:this.commit_state,config:this.config,colorize:!0,escape_quotes:!1,include_trailer:!0}),"Commit Preview"),await this.#a()||(d.log.info("Exiting without commit"),process.exit(0));try{d.log.info(s.dry_run?Y("Committing changes..."):"Committing changes..."),mt(this.#n,s.dry_run?this.#r:this.#i)}catch(e){d.log.error("Something went wrong when committing: "+e);return}this.#m()}get#o(){return s.interactive&&this.config.confirm_with_editor}get#e(){return this.config.print_commit_output}get#t(){return this.config.confirm_commit}get#i(){return this.config.overrides.shell?{shell:this.config.overrides.shell,stdio:"inherit"}:{stdio:"inherit"}}get#r(){return this.config.overrides.shell?{shell:this.config.overrides.shell,stdio:"pipe"}:{stdio:"pipe"}}get#s(){return this.commit_state.trailer?`--trailer="${this.commit_state.trailer}"`:""}get#n(){return`git ${s.git_args} commit -m "${J({commit_state:this.commit_state,config:this.config,colorize:!1,escape_quotes:!0,include_trailer:!1})}" ${this.#s} ${this.#c}`.trim()}get#c(){return s.dry_run?"--dry-run --porcelain --untracked-files=no":""}async#a(){if(!s.interactive||!this.#t)return!0;let t=await d.confirm({message:s.dry_run?Y("Confirm Commit?"):"Confirm Commit?"});return d.isCancel(t)&&process.exit(0),t}#m(){d.log.success("Commit Complete");let t=this.prompt_cache.get("username");this.prompt_cache.clear(),t&&this.prompt_cache.set("username",t)}};import*as b from"@clack/prompts";import S from"picocolors";import{execSync as lt}from"child_process";import*as W from"@clack/prompts";import Q from"picocolors";var pt=["M","T","R","D","A","C"];function X(){let i="";try{i=lt(`git ${s.git_args} status --porcelain`,{stdio:"pipe"}).toString()}catch(a){return W.log.error(Q.red("Failed to git status"+a)),{index:[],work_tree:[]}}let t=i.split(`
26
+ `),e=[],o=[];return t.forEach(a=>{let r=a.trimEnd();if(!r)return;let n=r.substring(2).trim(),l=r.charAt(0).trim(),v=r.charAt(1).trim();(l==="?"||v==="?")&&e.push(n),pt.includes(l)&&o.push(n),pt.includes(v)&&e.push(n)}),{index:o,work_tree:e}}function _t(i){let t=i.join(" ");if(t)try{lt(`git ${s.git_args} add ${t}`,{stdio:"pipe"}).toString(),W.log.success(Q.green("Changes successfully staged"))}catch{W.log.error(Q.red("Failed to stage changes"))}}var q=class extends m{async run(){if(!this.#o)return;let t=X();if(this.#e(t),t.work_tree.length){let e=await this.#i(t.work_tree);e.length&&_t(e)}this.#r()}get#o(){return this.config.check_status}#e(t){b.log.step(S.black(S.bgGreen(" Checking Git Status ")));let e=this.#t(t.index,S.green);if(b.log.success(`Changes to be committed:
27
+ `+e),!t.work_tree.length)return;let o=this.#t(t.work_tree,S.red);b.log.error(`Changes not staged for commit:
28
+ `+o)}#t(t,e){return t.reduce((o,a,r)=>e(o+a+rt(t,r)),"")}async#i(t){let e=await b.multiselect({message:$("Some files have not been staged, would you like to add them now?"),options:[{value:".",label:"."},...t.map(o=>({value:o,label:o}))],required:!1});return b.isCancel(e)&&process.exit(0),e}#r(){X().index.length||(b.log.error(S.red('no changes added to commit (use "git add" and/or "git commit -a")')),process.exit(0))}};import{execSync as ft}from"child_process";import _ from"picocolors";var dt={"better-branch":"Create a branch or worktree from a guided prompt flow.","better-commits-init":"Create a .better-commits.json config in this repository."},ht={"--interactive":"Run in interactive prompt mode (default behavior).","--dry-run":"Print the commit command without creating a commit.","--help":"Show help information and exit."},bt={"--type":"Set commit type (for example feat, fix, docs).","--scope":"Set commit scope.","--title":"Set commit title/description.","--body":"Set commit body text.","--closes":"Set issue/ticket id for a closes footer.","--ticket":"Set ticket value used in the title.","--trailer":"Set trailer footer value.","--deprecates":"Set issue/ticket id for a deprecates footer.","--breaking-title":"Set breaking-change title footer.","--breaking-body":"Set breaking-change body footer.","--deprecates-title":"Set deprecates footer title text.","--deprecates-body":"Set deprecates footer body text.","--custom-footer":"Set a custom footer line."},yt={"--git-dir":"Set the path to the .git directory.","--work-tree":"Set the path to the working tree root."};function B(i){let o=" ";return Object.entries(i).map(([a,r])=>{let n=Math.max(2,26-a.length);return`${o}${a}${" ".repeat(n)}${r}`}).join(`
29
+ `)}function gt(i,t){let e=I(),o="(none)";try{o=ft(`git ${s.git_args} branch --show-current`,{stdio:"pipe"}).toString().trim()||"(none)"}catch{}let a=F(i.commit_type.options,s.git_args)||"Unknown",r=i.check_ticket.infer_ticket?P({append_hashtag:i.check_ticket.append_hashtag,prepend_hashtag:i.check_ticket.prepend_hashtag},s.git_args)||"Unknown":"Infer Disabled",n=i.commit_type.options.map(c=>c.value).join(", ").trim(),l=i.commit_scope.options.map(c=>c.value).join(", ").trim(),v=B(ht),w=B(yt),U=B(bt),K=B(dt);console.log(`
30
+ ${_.green("\uF489 better-commits")} ${_.gray("v"+e)}
29
31
 
30
- ${l.dim(e.trailer)}`:`
32
+ ${_.gray("BRANCH")}
33
+ ${o}
34
+ ${_.gray("Type")} ${_.blue(a)} ${_.gray("\xB7")} ${_.gray("Ticket")} ${_.magenta(r)}
31
35
 
32
- ${e.trailer}`),f&&(a=a.replaceAll('"','\\"').replaceAll("`","\\`")),a}export{J as main};
36
+ ${_.gray("CONFIGURATION")}
37
+ ${t}
38
+
39
+ ${_.gray("Types")}
40
+ ${n}
41
+
42
+ ${_.gray("Scopes")}
43
+ ${l}
44
+
45
+ ${_.gray("CLI FLAGS")}
46
+ ${v}
47
+
48
+ ${_.gray("Commit Flags")}
49
+ ${U}
50
+
51
+ ${_.gray("Git Flags (Advanced)")}
52
+ ${w}
53
+
54
+ ${_.gray("ADDITIONAL COMMANDS")}
55
+ ${K}
56
+
57
+ `)}var xt=[q,R,j,E,D,M,V,O],{config:$t,config_source:Ot}=it();St($t,Ot);async function St(i,t){if(kt(ot()),s.version){let l=I();H.log.step("Better Commits v"+l);return}if(s.help){gt(i,t);return}let e=ct(i),o={...s.commit_state,type:(s.commit_state.type||e?.type)??"",ticket:(s.commit_state.ticket||e?.ticket)??""},a=ut(z,o);if(!s.interactive)try{ut(at(i),a)}catch(l){l instanceof vt?H.log.error(`Invalid --no-interactive commit input: ${l.message}`):H.log.error(`Failed to validate --no-interactive commit input: ${l}`),process.exit(0)}let r=i.cache_last_value?new Ct("better-commits"):et,n=s.interactive?xt:[O];for(let l of n)await new l(i,a,r).run()}export{St as main};
package/dist/init.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #! /usr/bin/env node
2
- import{b as r,f as e,u as i}from"./chunk-K2RPF2JY.js";import*as t from"@clack/prompts";import c from"fs";import o from"picocolors";import{parse as l}from"valibot";try{console.clear(),t.intro(`${o.bgCyan(o.black(" better-commits-init "))}`);let s=`${i()}/${e}`,m=l(r,{});c.writeFileSync(s,JSON.stringify(m,null,4)),t.log.success(`${o.green("Successfully created .better-commits.json")}`),t.outro(`Run ${o.bgBlack(o.white("better-commits"))} to start the CLI`)}catch{t.log.error(`${o.red("Could not determine git root folder. better-commits-init must be used in a git repository")}`)}
2
+ import{b as r,h as e,m as i}from"./chunk-OFJCRS3N.js";import*as t from"@clack/prompts";import c from"fs";import o from"picocolors";import{parse as l}from"valibot";try{console.clear(),t.intro(`${o.bgCyan(o.black(" better-commits-init "))}`);let s=`${i()}/${e}`,m=l(r,{});c.writeFileSync(s,JSON.stringify(m,null,4)),t.log.success(`${o.green("Successfully created .better-commits.json")}`),t.outro(`Run ${o.bgBlack(o.white("better-commits"))} to start the CLI`)}catch{t.log.error(`${o.red("Could not determine git root folder. better-commits-init must be used in a git repository")}`)}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "better-commits",
3
3
  "private": false,
4
- "version": "1.19.1",
4
+ "version": "1.20.0",
5
5
  "description": "A CLI for creating better commits following the conventional commits specification",
6
6
  "author": "Erik Verduin (https://github.com/everduin94)",
7
7
  "type": "module",
@@ -26,18 +26,20 @@
26
26
  "url": "https://github.com/Everduin94/better-commits"
27
27
  },
28
28
  "dependencies": {
29
- "@clack/core": "^0.3.1",
30
- "@clack/prompts": "^0.7.0",
29
+ "@bomb.sh/args": "^0.3.1",
30
+ "@clack/core": "^1.2.0",
31
+ "@clack/prompts": "^1.2.0",
31
32
  "configstore": "^5.0.1",
32
33
  "picocolors": "^1.0.0",
33
- "valibot": "^0.30.0"
34
+ "valibot": "^1.3.1"
34
35
  },
35
36
  "scripts": {
36
- "start": "jiti ./src/index.ts",
37
- "branch": "jiti ./src/branch.ts",
38
- "init": "jiti ./src/init.ts",
37
+ "start": "tsx ./src/index.ts",
38
+ "branch": "tsx ./src/branch.ts",
39
+ "init": "tsx ./src/init.ts",
39
40
  "build": "tsup",
40
- "commit": "jiti ./src/index.ts"
41
+ "commit": "tsx ./src/index.ts",
42
+ "test": "vitest run"
41
43
  },
42
44
  "devDependencies": {
43
45
  "@semantic-release/git": "^10.0.1",
@@ -49,7 +51,8 @@
49
51
  "semantic-release": "^25.0.2",
50
52
  "tsup": "^8.0.2",
51
53
  "tsx": "^3.12.3",
52
- "typescript": "^5.4.5"
54
+ "typescript": "^5.4.5",
55
+ "vitest": "^3.2.4"
53
56
  },
54
57
  "release": {
55
58
  "branches": [
package/readme.md CHANGED
@@ -49,10 +49,16 @@ better-commits # Create a new commit
49
49
  better-branch # Create a new branch
50
50
  ```
51
51
 
52
- `better-commits` will prompt a series of questions. These prompts will build a commit message, which you can preview, before confirming the commit.
52
+ `better-commits` will prompt a series of questions. These prompts will build a commit message, which you can preview, before confirming the commit. - To better understand these prompts and their intention, read [Conventional Commits Summary](https://www.conventionalcommits.org/en/v1.0.0-beta.4/#summary)
53
+
53
54
  Some of the values in these prompts will be inferred by your branch name and auto populated. You can adjust this in your `.better-commits.json` configuration file.
54
55
 
55
- To better understand these prompts and their intention, read [Conventional Commits Summary](https://www.conventionalcommits.org/en/v1.0.0-beta.4/#summary)
56
+ For documentation on passing commit values to `better-commits` via the CLI, see [CLI Flags](#cli-flags).
57
+
58
+ > [!TIP]
59
+ > The `--no-interactive` flag, allows automated workflows or AI agents like OpenCode and Claude Code, to use better-commits to generate consistent commit messages using less tokens.
60
+ >
61
+ > Run `better-commits --help` / `better-branch --help` for more information.
56
62
 
57
63
  ## ⚙️ Configuration
58
64
 
@@ -402,7 +408,7 @@ Optionally configure pre and post checkout commands, for example:
402
408
 
403
409
  See _branch_pre_commands_ and _branch_post_commands_ in default config. (or _worktree_pre_commands_ and _worktree_post_commands_ for creating worktrees)
404
410
 
405
- ## 🌌 Mildly Interesting
411
+ ## 💡 Tips & Tricks
406
412
 
407
413
  ### Building / Versioning
408
414
 
@@ -423,7 +429,7 @@ If you're using Github issues to track your work, and select the `closes` footer
423
429
 
424
430
  `better-commits` can append a commit trailer per commit type. This allows you to [automate change logs](https://docs.gitlab.com/ee/user/project/changelogs.html) with tools like Gitlab.
425
431
 
426
- ### Misc
432
+ ### Git
427
433
 
428
434
  `better-commits` uses native `git` commands under the hood. So any hooks, tools, or staging should work as if it was a normal commit.
429
435
 
@@ -432,14 +438,6 @@ Setting `confirm_with_editor=true` will allow you to edit/confirm a commit with
432
438
  - For example, to edit with Neovim: `git config --global core.editor "nvim"`
433
439
  - For VS Code, `git config --global core.editor "code -n --wait"`
434
440
 
435
- You can add this badge to your repository to display that you're using a better-commits repository config
436
-
437
- | Markdown | Result |
438
- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
439
- | `[![better commits is enabled](https://img.shields.io/badge/better--commits-enabled?style=for-the-badge&logo=git&color=a6e3a1&logoColor=D9E0EE&labelColor=302D41)](https://github.com/Everduin94/better-commits)` | [![better commits is enabled](https://img.shields.io/badge/better--commits-enabled?style=for-the-badge&logo=git&color=a6e3a1&logoColor=D9E0EE&labelColor=302D41)](https://github.com/Everduin94/better-commits) |
440
-
441
- ### Git Arguments
442
-
443
441
  You can pass arguments to `git` through `better-commits` like so:
444
442
 
445
443
  ```sh
@@ -448,6 +446,23 @@ better-commits --git-dir="$HOME/.config" --work-tree="$HOME"
448
446
 
449
447
  A practical example of this would be managing dotfiles, as described in this [Atlassian Article](https://www.atlassian.com/git/tutorials/dotfiles)
450
448
 
449
+ ### CLI Flags
450
+
451
+ Use CLI flags to pass commit values directly instead of answering prompts.
452
+
453
+ - Use `--no-interactive` to skip prompts, confirmation, and editor flows. This is the recommended mode for OpenCode, Claude Code, and other coding agents.
454
+ - Use `--dry-run` to validate the generated `git commit` command without creating a commit.
455
+ - Supported commit field flags: `--type`, `--scope`, `--title`, `--body`, `--ticket`, `--closes`, `--deprecates`, `--breaking-title`, `--breaking-body`, `--deprecates-title`, `--deprecates-body`, `--custom-footer`, `--trailer`.
456
+ - Supported branch field flags: `--user`, `--type`, `--description`, `--ticket`, `--branch-version`, `--checkout`.
457
+
458
+ **Examples**
459
+
460
+ ```sh
461
+ better-commits --no-interactive --dry-run --type feat --scope cli --title "add parser"
462
+
463
+ better-branch --no-interactive --type feat --ticket TAC-123 --description "add parser" --checkout worktree
464
+ ```
465
+
451
466
  ---
452
467
 
453
468
  ### 🪟 Troubleshooting Windows
@@ -0,0 +1,102 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parse_runtime_flags } from "./args";
3
+
4
+ describe("parse_runtime_flags", () => {
5
+ it("uses interactive mode by default", () => {
6
+ const parsed = parse_runtime_flags([]);
7
+
8
+ expect(parsed.no_interactive).toBe(false);
9
+ expect(parsed.dry_run).toBe(false);
10
+ expect(parsed.git_args).toBe("");
11
+ expect(parsed.commit_state).toEqual({});
12
+ });
13
+
14
+ it("parses --no-interactive and --dry-run", () => {
15
+ const parsed = parse_runtime_flags(["--no-interactive", "--dry-run"]);
16
+
17
+ expect(parsed.no_interactive).toBe(true);
18
+ expect(parsed.dry_run).toBe(true);
19
+ });
20
+
21
+ it("parses --version and -v", () => {
22
+ const long_flag = parse_runtime_flags(["--version"]);
23
+ const short_flag = parse_runtime_flags(["-v"]);
24
+
25
+ expect(long_flag.version).toBe(true);
26
+ expect(short_flag.version).toBe(true);
27
+ });
28
+
29
+ it("maps commit flags into commit_state keys", () => {
30
+ const parsed = parse_runtime_flags([
31
+ "--type",
32
+ "feat",
33
+ "--title",
34
+ "ship feature",
35
+ "--breaking-title",
36
+ "api changed",
37
+ "--custom-footer",
38
+ "Reviewed-by: Jane",
39
+ ]);
40
+
41
+ expect(parsed.commit_state).toEqual({
42
+ type: "feat",
43
+ title: "ship feature",
44
+ breaking_title: "api changed",
45
+ custom_footer: "Reviewed-by: Jane",
46
+ });
47
+ });
48
+
49
+ it("builds git args from --git-dir and --work-tree", () => {
50
+ const parsed = parse_runtime_flags([
51
+ "--git-dir",
52
+ "/tmp/repo/.git",
53
+ "--work-tree",
54
+ "/tmp/repo",
55
+ ]);
56
+
57
+ expect(parsed.git_args).toBe(
58
+ "--git-dir=/tmp/repo/.git --work-tree=/tmp/repo",
59
+ );
60
+ });
61
+
62
+ it("builds git args when only one git location flag is provided", () => {
63
+ const with_git_dir = parse_runtime_flags(["--git-dir", "/tmp/repo/.git"]);
64
+ const with_work_tree = parse_runtime_flags(["--work-tree", "/tmp/repo"]);
65
+
66
+ expect(with_git_dir.git_args).toBe("--git-dir=/tmp/repo/.git");
67
+ expect(with_work_tree.git_args).toBe("--work-tree=/tmp/repo");
68
+ });
69
+
70
+ it("maps dashed commit flags to snake_case commit_state keys", () => {
71
+ const parsed = parse_runtime_flags([
72
+ "--breaking-body",
73
+ "major impact",
74
+ "--deprecates-title",
75
+ "legacy endpoint",
76
+ "--deprecates-body",
77
+ "use v2 endpoint",
78
+ "--custom-footer",
79
+ "Reviewed-by: Alex",
80
+ "--breaking-title",
81
+ "v1 removed",
82
+ ]);
83
+
84
+ expect(parsed.commit_state).toEqual({
85
+ breaking_body: "major impact",
86
+ deprecates_title: "legacy endpoint",
87
+ deprecates_body: "use v2 endpoint",
88
+ custom_footer: "Reviewed-by: Alex",
89
+ breaking_title: "v1 removed",
90
+ });
91
+ });
92
+
93
+ it("honors interactive flag semantics", () => {
94
+ const default_flags = parse_runtime_flags([]);
95
+ const explicit_interactive = parse_runtime_flags(["--interactive"]);
96
+ const no_interactive = parse_runtime_flags(["--no-interactive"]);
97
+
98
+ expect(default_flags.no_interactive).toBe(false);
99
+ expect(explicit_interactive.no_interactive).toBe(false);
100
+ expect(no_interactive.no_interactive).toBe(true);
101
+ });
102
+ });