envhound 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michel Salib
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # envhound
2
+
3
+ Find which startup file sets each environment variable, audit your `PATH`, and change them safely.
4
+
5
+ ```
6
+ $ envhound blame PATH
7
+ (initial) +/usr/local/sbin +/usr/local/bin +/usr/sbin +/usr/bin +/sbin +/bin
8
+ /etc/profile.d/apps-bin-path.sh:5 +/snap/bin
9
+ ~/.profile:33 pathprepend() +~/.tfenv/bin
10
+ ~/.profile:35 pathprepend() +~/.bun/bin
11
+ ~/.profile:37 pathprepend() +~/.local/bin
12
+
13
+ pathprepend() is defined in ~/.profile
14
+
15
+ ✓ a fresh login shell ends with this value
16
+ ```
17
+
18
+ ## Install
19
+
20
+ envhound is a single JavaScript file that runs on Node ≥ 20 or Bun.
21
+
22
+ ```sh
23
+ npx envhound blame PATH # or: bunx envhound blame PATH, nothing to install
24
+ npm install -g envhound # or: bun add -g envhound
25
+ curl -fsSL https://github.com/michelsalib/envhound/releases/latest/download/install.sh | sh
26
+ ```
27
+
28
+ `install.sh` puts `envhound` in `~/.local/bin` (`ENVHOUND_INSTALL_DIR` to change it, `ENVHOUND_VERSION=0.2.0` to pin
29
+ a version), checks it against the release's `SHA256SUMS`, and runs it on Node, or on Bun when there is no
30
+ Node ≥ 20. It also installs bash completion where bash-completion loads it, and fish completion when fish
31
+ is installed; it never edits your startup files, so for zsh it prints the line to add.
32
+
33
+ **Updates.** Once a week envhound asks the npm registry, in the background, whether a newer version exists,
34
+ and when there is one says so after a command, at most once a week, with the update command for how you
35
+ installed it: `envhound upgrade` for `install.sh`, `npm install -g envhound@latest`, `npx envhound@latest`, and so on.
36
+ It stays quiet in scripts, CI, with `--json` or `--home`, and with `ENVHOUND_NO_UPDATE_CHECK=1`.
37
+
38
+ ## Commands
39
+
40
+ | Command | What it shows |
41
+ |---|---|
42
+ | `envhound` / `envhound list` | every exported variable, with the startup `file:line` that sets it |
43
+ | `envhound blame VAR` | every startup `file:line` that assigns `VAR`, in order; for `PATH`, what each step added or removed |
44
+ | `envhound path` | the current `PATH` one entry per line, who added each entry, missing directories and duplicates |
45
+ | `envhound dotenv [FILE…]` | for each key of a `.env` file (default `./.env`): new, same as your shell, or in conflict (with the startup line behind the shell's value); plus syntax problems, duplicates and `$VAR` expansion. Exits 1 if there are problems |
46
+ | `envhound set NAME=value…` | set variables for future login shells (see below) |
47
+ | `envhound unset NAME…` | remove variables `envhound set` added |
48
+ | `envhound path add DIR [--append]` / `envhound path remove DIR` | put a directory in `PATH` (front by default), or take it out |
49
+ | `envhound upgrade` | update an `install.sh` install; for others, prints the update command |
50
+ | `envhound edit [FILE…]` | interactive editor for variables, `PATH` and `.env` files (see below) |
51
+ | `envhound completion bash\|zsh\|fish` | a shell completion script (commands, flags, live variable names) |
52
+
53
+ Options: `--json`, `--show-secrets` (values of `*_TOKEN`, `*_KEY`, … are masked by default), `--home DIR`,
54
+ and for changes `--file FILE`, `--yes`, `--dry-run`, `--append`.
55
+
56
+ `blame` also tells you when an assignment doesn't stick:
57
+
58
+ - **not effective**: a fresh shell ends with a different value, e.g. a one-command prefix like `SHELL=/bin/sh lesspipe`
59
+ - **not exported**: set as a shell variable only, so programs started from the shell don't see it
60
+ - **different in this shell**: the current shell's value differs from what a fresh login shell gets
61
+
62
+ ## Changing variables
63
+
64
+ ```
65
+ $ envhound set EDITOR=vim
66
+ note: EDITOR is also set at ~/.bashrc:12; envhound's line runs last, so it wins in login shells
67
+ note: ~/.bashrc runs again in every non-login shell and will set EDITOR back there; consider removing that line
68
+
69
+ ~/.config/envhound/env.sh
70
+ # Hand edits are fine: envhound keeps lines it does not recognise.
71
+ + export EDITOR=vim
72
+
73
+ Write these changes? [y/N] y
74
+ wrote ~/.config/envhound/env.sh
75
+ ✓ a fresh login shell now gets EDITOR
76
+
77
+ This shell is unchanged. To apply it here too, run:
78
+ export EDITOR=vim
79
+ ```
80
+
81
+ - envhound writes **its own file**, `~/.config/envhound/env.sh` (mode 600), and never rewrites your startup
82
+ files. The first time, it appends one line to your login file (`~/.bash_profile`, `~/.bash_login` or
83
+ `~/.profile`, whichever bash reads) to load it. Being last, envhound's values win.
84
+ - Every change is shown as a **diff** (secrets masked) and confirmed; `--yes` skips the question,
85
+ `--dry-run` only shows it. Changed files are **backed up** to `~/.local/state/envhound/backups/`.
86
+ - After writing, envhound **re-traces** a fresh login shell to check the change took effect, and names the
87
+ line that overrides it if not.
88
+ - With `--file .env`, `set` and `unset` edit a `.env` file instead, changing only that key's line and keeping
89
+ comments, `export`, quote style and inline comments.
90
+ - A program can't change the shell that started it, so envhound prints the commands to apply the change in
91
+ the current shell.
92
+
93
+ ## Interactive editor
94
+
95
+ `envhound edit [FILE…]` opens a full-screen editor with tabs for **Variables**, **PATH** and each `.env` file:
96
+
97
+ ```
98
+ envhound edit 1 Variables 2 PATH 2 staged · w to write
99
+ ──────────────────────────────────────────────────────────────────────────────
100
+ NAME SET BY VALUE
101
+ * EDITOR ~/.bash_profile:1 vim → code
102
+ NVM_DIR ~/.profile:23 ~/.nvm
103
+ + MY_TOKEN envhound ********
104
+ ──────────────────────────────────────────────────────────────────────────────
105
+ EDITOR: set at ~/.bash_profile:1, staged: set
106
+ ↑↓ move / filter enter edit n new d unset o open u undo w write tab PATH ? help q quit
107
+ ```
108
+
109
+ Changes are staged, never written directly: <kbd>w</kbd> leaves the editor and goes through the same diff,
110
+ confirmation, backup and verification as `envhound set`. <kbd>o</kbd> opens `$VISUAL`/`$EDITOR` at the line that sets
111
+ the selected variable or adds the selected directory, then reloads. Lines in your own startup files are
112
+ yours to change there; envhound only writes its own file. <kbd>?</kbd> lists every key.
113
+
114
+ `.env` files get a tab each: `./.env` when it exists, or the files you name (which then open first). Each key
115
+ shows its line, its value and how it compares with your shell (same, or the shell's value when they differ),
116
+ plus syntax problems and duplicates. Edits there go through the same diff and confirmation, but only change
117
+ that key's line, as `envhound set --file` does. One session can stage both: on <kbd>w</kbd>, shell variables go
118
+ to envhound's file and `.env` keys to their file.
119
+
120
+ ## Shell completion
121
+
122
+ `install.sh` sets up bash and fish completion for you. Otherwise:
123
+
124
+ ```sh
125
+ eval "$(envhound completion bash)" # in ~/.bashrc
126
+ eval "$(envhound completion zsh)" # in ~/.zshrc, after compinit
127
+ envhound completion fish > ~/.config/fish/completions/envhound.fish
128
+ ```
129
+
130
+ Completion is dynamic: on each <kbd>Tab</kbd> the script asks `envhound` for candidates, so `envhound blame <Tab>`
131
+ offers the variables of the shell you are typing in.
132
+
133
+ ## How it works
134
+
135
+ envhound starts `bash -lixc` with a clean environment and a custom `PS4`, so bash's own trace reports the
136
+ file, line and function of every command its startup files run. envhound parses the assignments from that
137
+ trace and compares them with the environment the shell ends up with.
138
+
139
+ This runs your startup files once, as opening a terminal would. Startup files that `exec` another
140
+ program or wait for input stop the trace (envhound gives up after 15 seconds).
141
+
142
+ Values labelled `(inherited)` come from whatever launched your shell (terminal, WSL, IDE), not from startup files.
143
+
144
+ ## Status
145
+
146
+ bash only for now. Planned: zsh and fish, and a `.deb`.
147
+
148
+ ## Development
149
+
150
+ ```sh
151
+ bun install
152
+ bun run dev -- blame PATH # run from source
153
+ bun test # unit tests + real bash on test/fixtures/home + built CLI on node
154
+ bun run typecheck
155
+ bun run build # dist/envhound.js, plain JS for node >= 20
156
+ ```
157
+
158
+ Source uses only `node:` APIs so the build runs on both Node and Bun.
159
+
160
+ ## Releasing
161
+
162
+ ```sh
163
+ npm version patch # or minor/major: bumps package.json, commits, tags vX.Y.Z
164
+ git push --follow-tags
165
+ ```
166
+
167
+ The tag starts [`.github/workflows/release.yml`](.github/workflows/release.yml): tests, then `npm publish`
168
+ with provenance, then a GitHub release with `envhound.js`, `install.sh` and `SHA256SUMS`. npm needs either
169
+ an `NPM_TOKEN` repository secret or trusted publishing configured for this workflow on npmjs.com.
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ import{spawnSync as vr}from"node:child_process";import{existsSync as br,readFileSync as Mn,realpathSync as yr}from"node:fs";import{dirname as wr,resolve as L}from"node:path";import{fileURLToPath as xr}from"node:url";import{parseArgs as $r}from"node:util";var te={name:"envhound",version:"0.1.0",description:"Find which startup file sets each environment variable, and audit your PATH",keywords:["environment","env","path","bashrc","profile","dotfiles","shell"],license:"MIT",author:"Michel Salib",repository:{type:"git",url:"git+https://github.com/michelsalib/envhound.git"},homepage:"https://github.com/michelsalib/envhound#readme",bugs:"https://github.com/michelsalib/envhound/issues",type:"module",bin:{envhound:"dist/envhound.js"},files:["dist"],engines:{node:">=20"},scripts:{dev:"bun src/cli.ts",build:"bun build src/cli.ts --target=node --minify --outfile dist/envhound.js",test:"bun test",typecheck:"tsc",prepublishOnly:"bun run typecheck && bun run build && bun test"},devDependencies:{"@types/bun":"^1.4.2","@types/node":"^26.6.4",typescript:"^7.0.2"}};import{statSync as _n}from"node:fs";function _(e,n){return e.assignments.filter((t)=>t.name===n&&!t.local)}function Re(e,n){let t=_(e,n).at(-1);if(!t)return;let i=e.final[n];if(t.result===void 0)return i===void 0?"unset":"not-effective";if(i===void 0)return"not-exported";return t.result===i?"effective":"not-effective"}function Z(e){return e.via?.kind==="function"?e.via.at:e.at}var Hn=new Set(["PWD","OLDPWD","SHLVL","_"]);function re(e,n){return Object.keys(n).filter((t)=>t!=="_"&&n[t]!==void 0).sort().map((t)=>{let i=_(e,t),r=n[t];return{name:t,value:r,kind:i.length?"startup":(t in e.final)?"shell":"inherited",last:i.at(-1),count:i.length,status:Re(e,t),differsFromFresh:!Hn.has(t)&&t in e.final&&e.final[t]!==r}})}function he(e,n,t){return{name:n,assignments:_(e,n),status:Re(e,n),fresh:e.final[n],current:t[n],pathSteps:n==="PATH"?Me(e):void 0}}var F=(e)=>e===void 0?[]:e.split(":"),pe=(e)=>[...new Set(e)];function Me(e){let n=e.initial.PATH??"",t=[{value:n,added:pe(F(n)),removed:[],reordered:!1}];for(let i of _(e,"PATH")){let r=i.result??"",s=new Set(F(n)),a=new Set(F(r)),l=pe(F(r).filter((o)=>!s.has(o))),c=pe(F(n).filter((o)=>!a.has(o))),u=!l.length&&!c.length&&[...s].join(":")!==[...a].join(":");t.push({assignment:i,value:r,added:l,removed:c,reordered:u}),n=r}return t}function Nn(e){try{return _n(e).isDirectory()}catch{return!1}}function z(e,n){let t=new Map;for(let r of Me(e)){for(let s of r.removed)t.delete(s);for(let s of r.added)t.set(s,r.assignment??"initial")}let i=new Map;return F(n).map((r,s)=>{let a=t.get(r),l=s+1,c=i.get(r);if(c===void 0)i.set(r,l);return{index:l,dir:r,exists:r!==""&&Nn(r),duplicateOf:c,source:a===void 0?"inherited":a==="initial"?"initial":"startup",addedBy:a==="initial"?void 0:a}})}var Vn={list:"every exported variable and where it is set",blame:"every startup line that assigns a variable",path:"PATH entries, who added them, missing and duplicates",dotenv:"check .env files against your shell",set:"set variables for future login shells (or a .env file)",unset:"remove variables envhound set",edit:"interactive editor for variables, PATH and .env files",completion:"print a shell completion script",upgrade:"update envhound to the latest release"},Cn={"--json":"machine-readable output","--show-secrets":"don't mask secret values","--home":"act as if HOME were DIR","--file":"set/unset: edit this .env file","--yes":"write without asking","--dry-run":"show the diff, write nothing","--append":"path add: put DIR at the end","--help":"show help","--version":"show the version"},ie=["bash","zsh","fish"],Le=new Set(["--home","--file","-f"]),jn={add:"put a directory in PATH",remove:"remove a directory envhound added"};function _e(e,n){let t=e.at(-1)??"",i=e.slice(0,-1);if(Le.has(i.at(-1)??""))return[];let r=i.filter((a,l)=>!a.startsWith("-")&&!Le.has(i[l-1]??"")),s=[];if(t.startsWith("-"))s=Object.entries(Cn);else if(r.length===0)s=Object.entries(Vn);else if(r[0]==="blame"&&r.length===1)s=Object.keys(n).sort().map((a)=>[a]);else if(r[0]==="unset")s=Object.keys(n).sort().map((a)=>[a]);else if(r[0]==="set"&&!t.includes("="))s=Object.keys(n).filter((a)=>a!=="PATH").sort().map((a)=>[`${a}=`]);else if(r[0]==="path"&&r.length===1)s=Object.entries(jn);else if(r.length===1&&r[0]==="completion")s=ie.map((a)=>[a]);return s.filter(([a])=>a.startsWith(t)).map(([a,l])=>l?`${a} ${l}`:a)}function He(e){switch(e){case"bash":return`# envhound completion for bash. Add to ~/.bashrc: eval "$(envhound completion bash)"
3
+ _envhound() {
4
+ local cur=\${COMP_WORDS[COMP_CWORD]}
5
+ case \${COMP_WORDS[COMP_CWORD-1]} in
6
+ --home) COMPREPLY=($(compgen -d -- "$cur")); return ;;
7
+ --file|-f) COMPREPLY=($(compgen -f -- "$cur")); return ;;
8
+ esac
9
+ local IFS=$'\\n'
10
+ COMPREPLY=($(envhound __complete "\${COMP_WORDS[@]:1:COMP_CWORD}" 2>/dev/null | cut -f1))
11
+ [[ \${COMPREPLY[0]} == *= ]] && compopt -o nospace
12
+ }
13
+ complete -o default -F _envhound envhound
14
+ `;case"zsh":return`# envhound completion for zsh. Add to ~/.zshrc (after compinit): eval "$(envhound completion zsh)"
15
+ _envhound() {
16
+ case \${words[CURRENT-1]} in
17
+ --home) _directories; return ;;
18
+ --file|-f) _files; return ;;
19
+ esac
20
+ local -a lines candidates
21
+ local line name
22
+ lines=("\${(@f)$(envhound __complete "\${(@)words[2,CURRENT]}" 2>/dev/null)}")
23
+ for line in $lines; do
24
+ [[ -n $line ]] || continue
25
+ name=\${\${line%%$'\\t'*}//:/\\\\:}
26
+ if [[ $line == *$'\\t'* ]]; then candidates+=("$name:\${line#*$'\\t'}"); else candidates+=("$name"); fi
27
+ done
28
+ if (( \${#candidates} )); then _describe envhound candidates; else _files; fi
29
+ }
30
+ compdef _envhound envhound
31
+ `;case"fish":return`# envhound completion for fish. Save as ~/.config/fish/completions/envhound.fish:
32
+ # envhound completion fish > ~/.config/fish/completions/envhound.fish
33
+ complete -c envhound -f -a '(envhound __complete (commandline -opc)[2..-1] (commandline -ct) 2>/dev/null)'
34
+ complete -c envhound -n '__fish_seen_subcommand_from dotenv' -F
35
+ complete -c envhound -l file -s f -r -F
36
+ complete -c envhound -l home -x -a '(__fish_complete_directories (commandline -ct))'
37
+ `}}var Ne=/^[A-Za-z0-9_./:@%+,=-]+$/,In=/[\x00-\x1f\x7f]/g,Kn=(e)=>({"\n":"\\n","\t":"\\t","\r":"\\r"})[e]??"\\x"+e.charCodeAt(0).toString(16).padStart(2,"0");function B(e){if(Ne.test(e))return e;if(/[\x00-\x1f\x7f]/.test(e))return"$'"+e.replace(/[\\']/g,"\\$&").replace(In,Kn)+"'";return"'"+e.replace(/'/g,"'\\''")+"'"}function Ve(e,n=""){let t=!e.includes("'")&&!/[\r\n]/.test(e);if(n==="'"&&t)return`'${e}'`;if(n!=='"'){if(Ne.test(e))return e;if(t)return`'${e}'`}return'"'+e.replace(/[\\"$]/g,"\\$&").replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/\t/g,"\\t")+'"'}var Wn=/^(\s*)(export\s+)?([^=\s#]+)(\s*)=(.*)$/,Fn=/^[A-Za-z_][A-Za-z0-9_.-]*$/,zn=/\$\{?[A-Za-z_]/;function Ce(e,n,t){for(let i=n;i<e.length;i++)if(t==='"'&&e[i]==="\\")i++;else if(e[i]===t)return i;return-1}var Bn=(e)=>e.replace(/\\([nrt"\\$])/g,(n,t)=>({n:`
38
+ `,r:"\r",t:"\t"})[t]??t);function H(e){let n=e.includes(`\r
39
+ `)?`\r
40
+ `:`
41
+ `,t=e.endsWith(`
42
+ `),i=e===""?[]:e.split(/\r?\n/);if(t)i.pop();let r=[];for(let s=0;s<i.length;s++){let a=i[s],l={line:s+1,endLine:s+1,raw:a},c=a.trim();if(!c){r.push({...l,kind:"blank"});continue}if(c.startsWith("#")){r.push({...l,kind:"comment"});continue}let u=Wn.exec(a);if(!u){r.push({...l,kind:"invalid",problem:"not a KEY=value line"});continue}let[,o,d,f,,h]=u;if(!Fn.test(f)){r.push({...l,kind:"invalid",key:f,problem:`invalid key '${f}'`});continue}let p=h.trimStart(),g=["'",'"',"`"].find((x)=>p.startsWith(x)),m,y="",w,S,K=s+1,ne=a;if(g){let x=p.slice(1),P=Ce(x,0,g),W=s;while(P<0&&W+1<i.length)W++,x+=`
43
+ `+i[W],P=Ce(x,0,g);if(P<0){r.push({...l,kind:"invalid",key:f,problem:`unterminated ${g} quote`});continue}let fe=x.slice(P+1);if(fe.trim().startsWith("#"))S=fe;else if(fe.trim())w="text after the closing quote is ignored";m=g==='"'?Bn(x.slice(0,P)):x.slice(0,P),y=g,ne=i.slice(s,W+1).join(`
44
+ `),K=W+1,s=W}else{let x=/(^|\s+)#.*$/.exec(p);if(x)S=x[0];m=p.slice(0,x?.index??p.length).trim()}r.push({line:l.line,endLine:K,raw:ne,kind:"entry",key:f,value:m,exported:!!d,quote:y,trailing:S,indent:o,expands:y!=="'"&&zn.test(m),problem:w})}return{lines:r,eol:n,trailingNewline:t}}function je(e){return e.lines.map((t)=>t.raw.replace(/\n/g,e.eol)).join(e.eol)+(e.trailingNewline?e.eol:"")}function se(e){let n=[],t=new Map;for(let i of e.lines){if(i.problem)n.push({line:i.line,key:i.key,message:i.problem});if(i.kind!=="entry")continue;let r=t.get(i.key);if(r!==void 0)n.push({line:i.line,key:i.key,message:`duplicate of line ${r}; most loaders keep the last one`});else t.set(i.key,i.line);if(i.expands)n.push({line:i.line,key:i.key,message:"uses $VAR expansion, which not every loader supports"})}return n.sort((i,r)=>i.line-r.line)}function oe(e,n){let t=new Map;for(let i of e.lines)if(i.kind==="entry")t.set(i.key,i);return[...t.values()].sort((i,r)=>i.line-r.line).map((i)=>{let r=n[i.key],s=r===void 0?"new":r===i.value?"same":"conflict";return{key:i.key,line:i.line,value:i.value,status:s,current:r}})}function Ie(e,n,t){let i=e.lines.findLastIndex((u)=>u.kind==="entry"&&u.key===n),r=e.lines[i],s=r?.quote??"",a=`${r?.indent??""}${r?.exported?"export ":""}${n}=${Ve(t,s)}${r?.trailing??""}`,c={...H(a).lines[0],line:r?.line??0,endLine:r?.endLine??0};if(r)return{...e,lines:e.lines.map((u,o)=>o===i?c:u)};return{...e,lines:[...e.lines,c],trailingNewline:!0}}function Ke(e,n){return{...e,lines:e.lines.filter((t)=>!(t.kind==="entry"&&t.key===n))}}import{chmodSync as Jn,copyFileSync as Gn,mkdirSync as Je,readFileSync as Xn,writeFileSync as et}from"node:fs";import{basename as nt,dirname as tt,join as rt}from"node:path";import{createInterface as it}from"node:readline/promises";function We(e,n){let t=Array.from({length:e.length+1},()=>Array(n.length+1).fill(0));for(let a=e.length-1;a>=0;a--)for(let l=n.length-1;l>=0;l--)t[a][l]=e[a]===n[l]?t[a+1][l+1]+1:Math.max(t[a+1][l],t[a][l+1]);let i=[],r=0,s=0;while(r<e.length||s<n.length)if(r<e.length&&s<n.length&&e[r]===n[s])i.push({op:" ",text:e[r]}),r++,s++;else if(r<e.length&&(s===n.length||t[r+1][s]>=t[r][s+1]))i.push({op:"-",text:e[r++]});else i.push({op:"+",text:n[s++]});return i}function Fe(e,n=2){let t=e.map((r,s)=>e.slice(Math.max(0,s-n),s+n+1).some((a)=>a.op!==" ")),i=[];if(e.forEach((r,s)=>{if(t[s])i.push(r);else if(i.at(-1)!==null)i.push(null)}),i[0]===null)i.shift();if(i.at(-1)===null)i.pop();return i}var Un=/TOKEN|SECRET|PASSW(OR)?D|API_?KEY|PRIVATE_?KEY|CREDENTIAL|_KEY$|^KEY$/i,T=(e)=>Un.test(e);function E(e,n,t){return n===void 0||t.showSecrets||!T(e)?n:"********"}function U(e){let n=(t)=>(i)=>e.color&&i?`\x1B[${t}m${i}\x1B[0m`:i;return{bold:n("1"),dim:n("2"),red:n("31"),green:n("32"),yellow:n("33"),cyan:n("36")}}function v(e,n){if(!n||n==="/")return e;let t=n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return e.replace(new RegExp(`(^|[:=\\s])${t}(?=/|:|$|\\s)`,"g"),"$1~")}var ve=(e)=>e.replace(/\n/g,"\\n").replace(/[\x00-\x1f\x7f]/g,(n)=>`\\x${n.charCodeAt(0).toString(16).padStart(2,"0")}`);function qn(e,n){return n!==void 0&&n>0&&e.length>n?e.slice(0,Math.max(0,n-1))+"…":e}function me(e,n){return e.length>n?"…"+e.slice(e.length-n+1):e}var ze=(e,n)=>`${v(e.file,n)}:${e.line}`;function D(e,n){return ze(Z(e),n)+(e.via?.kind==="function"?` ${e.via.name}()`:"")}function N(e,n,t){return e.map((i)=>i.map((r,s)=>{let a=s===i.length-1,l=qn(r,n[s]);return(t[s]??((c)=>c))(a?l:l.padEnd(n[s]??0))}).join(" ").trimEnd()).join(`
45
+ `)}function Be(e,n){let t=U(n),i=(o)=>{if(o.kind==="shell")return"(login/shell)";if(o.kind==="inherited")return"(inherited)";let d=[o.count>1?`×${o.count}`:"",o.status==="not-effective"?"not effective":""].filter(Boolean);return D(o.last,n.home)+(d.length?` (${d.join(", ")})`:"")},r=e.map((o)=>[o.name,i(o),v(ve(E(o.name,o.value,n)),n.home)]),s=Math.min(28,Math.max(8,...r.map((o)=>o[0].length))),a=Math.min(44,Math.max(6,...r.map((o)=>o[1].length)));for(let o of r)o[1]=me(o[1],a);let l=n.width===void 0?void 0:Math.max(10,n.width-s-a-4),c=N([["VARIABLE","SET BY","VALUE"]],[s,a,0],[t.bold,t.bold,t.bold]),u=e.map((o,d)=>{let f=o.kind!=="startup"?t.dim:o.status==="not-effective"?t.yellow:(p)=>p,h=o.differsFromFresh?t.cyan:(p)=>p;return N([r[d]],[s,a,l??0],[(p)=>p,f,h])});if(n.color&&e.some((o)=>o.differsFromFresh))u.push("",t.dim("values in ")+t.cyan("cyan")+t.dim(" differ from what a fresh login shell gets"));return[c,...u].join(`
46
+ `)}function Yn(e,n){let t=[...e.added.map((i)=>`+${v(i,n)||"(empty)"}`),...e.removed.map((i)=>`-${v(i,n)||"(empty)"}`)];if(t.length)return t.join(" ");return e.reordered?"(reordered)":"(reassigned, nothing new)"}function Ue(e,n){let t=U(n),i=(c)=>v(ve(E(e.name,c,n)??"<unset>"),n.home);if(!e.assignments.length){if(e.current===void 0)return`${e.name} is not set anywhere.`;let c=e.fresh!==void 0?"set by login or bash itself":"inherited from whatever launched this shell";return`${e.name} is not set by any startup file (${c}).
47
+ Current value: ${i(e.current)}`}let r;if(e.pathSteps)r=e.pathSteps.map((c)=>[c.assignment?D(c.assignment,n.home):"(initial)",Yn(c,n.home)]);else{let c=(u)=>u===""||/^\s|\s$/.test(u)?JSON.stringify(u):u;r=e.assignments.map((u)=>[D(u,n.home),u.op==="unset"?"unset":`${u.op} ${c(i(u.value))}`])}let s=Math.min(48,Math.max(...r.map((c)=>c[0].length))),a=[N(r,[s,n.width===void 0?0:Math.max(10,n.width-s-2)],[t.dim,(c)=>c])],l=e.assignments.flatMap((c)=>c.via?.kind==="function"?[`${c.via.name}() is defined in ${v(c.at.file,n.home)}`]:c.via?.kind==="source"?[`${v(c.at.file,n.home)} is sourced from ${ze(c.via.at,n.home)}`]:[]);if(l.length)a.push("",...Zn(l).map((c)=>t.dim(c)));switch(a.push(""),e.status){case"effective":a.push(t.green("✓ a fresh login shell ends with this value"));break;case"unset":a.push(t.green("✓ a fresh login shell ends with this variable unset"));break;case"not-exported":a.push(t.yellow("! set as a shell variable but never exported: programs started from the shell don't see it"));break;case"not-effective":a.push(t.yellow(`! not effective: a fresh shell ends with ${e.name}=${i(e.fresh)}
48
+ so the last line above is a one-command prefix (${e.name}=x cmd), or something envhound can't trace undid it`));break}if(e.current===void 0&&e.fresh!==void 0)a.push(t.cyan("~ not set in this shell: it started before this was added, or something unset it"));else if(e.current!==e.fresh&&e.current!==void 0)a.push(t.cyan(`~ this shell has a different value: ${e.pathSteps?Qn(e.fresh,e.current,n.home):i(e.current)}`));return a.join(`
49
+ `)}var Zn=(e)=>[...new Set(e)];function Qn(e,n,t){let i=new Set((e??"").split(":")),r=new Set((n??"").split(":")),s=[...r].filter((u)=>!i.has(u)),a=[...i].filter((u)=>!r.has(u)),l=(u)=>u.length>3?`${u.slice(0,3).map((o)=>v(o,t)).join(", ")}, …`:u.map((o)=>v(o,t)).join(", ");return([s.length?`${s.length} extra (${l(s)})`:"",a.length?`${a.length} missing (${l(a)})`:""].filter(Boolean).join("; ")||"same entries, different order")+`
50
+ run 'envhound path' to see each entry`}function qe(e,n){let t=U(n),i=(o)=>o.source==="startup"?D(o.addedBy,n.home):o.source==="initial"?"(initial)":"(inherited)",r=(o)=>o.dir===""?"empty entry: searches the current directory":o.duplicateOf?`duplicate of #${o.duplicateOf}`:!o.exists?"missing":"",s=e.map((o)=>[String(o.index).padStart(2),v(o.dir,n.home)||"(empty)",i(o),r(o)]),a=Math.min(50,Math.max(9,...s.map((o)=>o[1].length))),l=Math.min(40,Math.max(8,...s.map((o)=>o[2].length)));for(let o of s)o[1]=me(o[1],a),o[2]=me(o[2],l);let c=N([[" #","DIRECTORY","ADDED BY",""]],[2,a,l,0],[t.bold,t.bold,t.bold]),u=s.map((o,d)=>N([o],[2,a,l,0],[t.dim,(f)=>f,e[d].source==="startup"?(f)=>f:t.dim,t.red]));return[c,...u].join(`
51
+ `)}var ge=(e,n)=>({...e,value:E(e.name,e.value,n),previous:E(e.name,e.previous,n),result:E(e.name,e.result,n)}),Ye=(e,n)=>e.map((t)=>({...t,value:E(t.name,t.value,n),last:t.last&&ge(t.last,n)})),Ze=(e,n)=>({...e,fresh:E(e.name,e.fresh,n),current:E(e.name,e.current,n),assignments:e.assignments.map((t)=>ge(t,n)),pathSteps:e.pathSteps?.map((t)=>({...t,assignment:t.assignment&&ge(t.assignment,n)}))});function Qe(e,n){let t=U(n),i=(s,a)=>v(ve(E(s,a,n)??""),n.home),r=[t.bold(`${v(e.file,n.home)}: ${e.keys.length} key${e.keys.length===1?"":"s"}`)];if(e.keys.length){let s=e.keys.map((o)=>{if(o.status==="new")return[o.key,String(o.line),"new",""];if(o.status==="same")return[o.key,String(o.line),"same as shell",""];let d=e.shellOrigin[o.key];return[o.key,String(o.line),"conflict",`shell has ${i(o.key,o.current)}${d?` (set at ${D(d,n.home)})`:""}`]}),a=Math.min(32,Math.max(3,...s.map((o)=>o[0].length))),l=Math.max(4,...s.map((o)=>o[1].length)),c=Math.max(6,...s.map((o)=>o[2].length)),u=(o)=>o.startsWith("conflict")?t.yellow(o):o.startsWith("same")?t.dim(o):t.green(o);r.push(N([["KEY","LINE","STATUS",""]],[a,l,c,0],[t.bold,t.bold,t.bold]));for(let o of s)r.push(N([o],[a,l,c,0],[(d)=>d,t.dim,u,t.dim]));if(s.some((o)=>o[2]==="conflict"))r.push("",t.dim("conflict: loaders such as dotenv keep the shell's value unless told to override; check yours"))}if(e.problems.length){r.push("",t.bold("Problems"));for(let s of e.problems)r.push(`${t.red(`line ${s.line}`)}${s.key?` ${s.key}`:""}: ${s.message}`)}return r.join(`
52
+ `)}function R(e){try{return Xn(e,"utf8")}catch(n){if(n.code==="ENOENT")return;throw n}}var st=/^(\s*(?:export\s+)?)([A-Za-z_][A-Za-z0-9_.-]*)(\s*=\s*)(.*)$/;function ot(e,n){if(n.showSecrets)return e;return e.replace(st,(t,i,r,s)=>T(r)?`${i}${r}${s}********`:t)}function Ge(e,n){let t=(o)=>(d)=>n.color?`\x1B[${o}m${d}\x1B[0m`:d,[i,r,s,a]=[t("1"),t("31"),t("32"),t("2")],l=(o)=>o===void 0||o===""?[]:o.replace(/\n$/,"").split(`
53
+ `),c=`${v(e.path,n.home)}${e.before===void 0?" (new file)":""}`,u=Fe(We(l(e.before),l(e.after))).map((o)=>{if(o===null)return a(" …");let d=`${o.op} ${ot(o.text,n)}`;return o.op==="+"?s(d):o.op==="-"?r(d):a(d)});return[i(c),...u].join(`
54
+ `)}async function Xe(e){if(!process.stdin.isTTY)return!1;let n=it({input:process.stdin,output:process.stdout});try{return/^y(es)?$/i.test((await n.question(e)).trim())}catch{return!1}finally{n.close()}}function en(e,n){for(let r of e)if(R(r.path)!==r.before)throw Error(`${r.path} changed while envhound was running; nothing was written`);let t=new Date().toISOString().replace(/[:.]/g,"-"),i=[];for(let r of e){if(r.before!==void 0){Je(n,{recursive:!0,mode:448});let s=rt(n,`${t}-${nt(r.path)}`);Gn(r.path,s),i.push(s)}if(Je(tt(r.path),{recursive:!0}),et(r.path,r.after,{mode:r.mode}),r.mode!==void 0)Jn(r.path,r.mode)}return i}import{existsSync as lt}from"node:fs";import{join as V}from"node:path";import{Buffer as nn}from"node:buffer";var tn={n:10,t:9,r:13,a:7,b:8,e:27,E:27,f:12,v:11,"\\":92,"'":39,'"':34,"?":63};function at(e,n){let t=[],i=(r)=>t.push(...nn.from(r,"utf8"));while(n<e.length&&e[n]!=="'"){if(e[n]!=="\\"||n+1>=e.length){let l=String.fromCodePoint(e.codePointAt(n));i(l),n+=l.length;continue}let r=e[n+1],s=e.slice(n+2),a;if(r in tn)t.push(tn[r]),n+=2;else if(a=/^[0-7]{1,3}/.exec(e.slice(n+1))?.[0])t.push(parseInt(a,8)&255),n+=1+a.length;else if(r==="x"&&(a=/^[0-9a-fA-F]{1,2}/.exec(s)?.[0]))t.push(parseInt(a,16)),n+=2+a.length;else if((r==="u"||r==="U")&&(a=(r==="u"?/^[0-9a-fA-F]{1,4}/:/^[0-9a-fA-F]{1,8}/).exec(s)?.[0])){let l=parseInt(a,16);i(l<=1114111?String.fromCodePoint(l):"�"),n+=2+a.length}else if(r==="c"&&s.length>0)t.push(s.charCodeAt(0)&31),n+=3;else i("\\"+r),n+=2}return[nn.from(t).toString("utf8"),n+1]}function Q(e){let n=[],t="",i=!1,r=0;while(r<e.length){let s=e[r];if(s===" "||s==="\t"||s===`
55
+ `){if(i)n.push(t);t="",i=!1,r++;continue}if(i=!0,s==="'"){let a=e.indexOf("'",r+1),l=a<0?e.length:a;t+=e.slice(r+1,l),r=l+1}else if(s==="$"&&e[r+1]==="'"){let[a,l]=at(e,r+2);t+=a,r=l}else if(s==='"'){r++;while(r<e.length&&e[r]!=='"'){if(e[r]==="\\"&&'"\\$`'.includes(e[r+1]??"x"))r++;t+=e[r]??"",r++}r++}else if(s==="\\"&&r+1<e.length)t+=e[r+1],r+=2;else t+=s,r++}if(i)n.push(t);return n}function ae(e,n,t){let i=!t&&n.XDG_CONFIG_HOME||V(e,".config"),r=!t&&n.XDG_STATE_HOME||V(e,".local","state");return{home:e,managed:V(i,"envhound","env.sh"),backups:V(r,"envhound","backups"),updateState:V(r,"envhound","update.json")}}var sn="# Managed by envhound: environment variables for your login shell.\n# Change them with `envhound set NAME=value`, `envhound unset NAME` and `envhound path add DIR`.\n# Hand edits are fine: envhound keeps lines it does not recognise.\n",rn='"${XDG_CONFIG_HOME:-$HOME/.config}/envhound/env.sh"',on=`
56
+ # Added by envhound: load variables managed with \`envhound set\`
57
+ [ -f ${rn} ] && . ${rn}
58
+ `,an=(e)=>e.includes("envhound/env.sh");function ln(e){return[".bash_profile",".bash_login",".profile"].map((t)=>V(e,t)).find((t)=>lt(t))??V(e,".profile")}var ct=/^export ([A-Za-z_][A-Za-z0-9_]*)=(.*)$/,dt=/^case ":\$PATH:" in \*":(.*):"\*\) ;; \*\) export PATH="(?:\$PATH:(.*)|(.*):\$PATH)" ;; esac$/;function ut(e,n){let t=(i)=>i.replace(/[\\"$`]/g,"\\$&");return e===n||e.startsWith(n+"/")?"$HOME"+t(e.slice(n.length)):t(e)}function ft(e,n){return e.replace(/^\$HOME(?=\/|$)/,n).replace(/\\([\\"$`])/g,"$1")}function pt(e,n,t){let i=ut(e,t),r=n==="front"?`${i}:$PATH`:`$PATH:${i}`;return`case ":$PATH:" in *":${i}:"*) ;; *) export PATH="${r}" ;; esac`}function le(e,n){let t=e===""?[]:e.split(`
59
+ `);if(e.endsWith(`
60
+ `))t.pop();return t.map((i)=>{let r=ct.exec(i);if(r)return{raw:i,kind:"var",name:r[1],value:Q(r[2])[0]??""};let s=dt.exec(i);if(s&&s[1]===(s[2]??s[3]))return{raw:i,kind:"path",dir:ft(s[1],n),position:s[2]!==void 0?"back":"front"};return{raw:i,kind:"other"}})}var cn=(e)=>e.map((n)=>n.raw).join(`
61
+ `)+`
62
+ `;function dn(e,n,t){let i={raw:`export ${n}=${B(t)}`,kind:"var",name:n,value:t},r=e.findIndex((s)=>s.kind==="var"&&s.name===n);if(r<0)return[...e,i];return e.flatMap((s,a)=>a===r?[i]:s.kind==="var"&&s.name===n?[]:[s])}function un(e,n){return e.filter((t)=>!(t.kind==="var"&&t.name===n))}function fn(e,n,t,i){if(e.some((s)=>s.kind==="path"&&s.dir===n&&s.position===t))return e;let r={raw:pt(n,t,i),kind:"path",dir:n,position:t};return[...be(e,n),r]}function be(e,n){return e.filter((t)=>!(t.kind==="path"&&t.dir===n))}import{join as ht}from"node:path";var ye=(e,n)=>e.at.file===n.managed;function pn(e,n,t){let i=R(n.managed),r=le(i??sn,n.home),s=[],a=ht(n.home,".bashrc"),l=(d)=>_(t,d).filter((f)=>!ye(f,n)),c=(d)=>[...new Set(d.map((f)=>D(f,n.home)))].join(", ");for(let d of e)switch(d.kind){case"set":{r=dn(r,d.name,d.value);let f=l(d.name);if(f.length)s.push(`${d.name} is also set at ${c(f)}; envhound's line runs last, so it wins in login shells`);if(f.some((h)=>h.at.file===a))s.push(`~/.bashrc runs again in every non-login shell and will set ${d.name} back there; consider removing that line`);break}case"unset":{if(!r.some((h)=>h.kind==="var"&&h.name===d.name))s.push(`${d.name} is not set by envhound`);r=un(r,d.name);let f=l(d.name);if(f.length)s.push(`${d.name} is still set at ${c(f)}; envhound only edits its own file, remove that line yourself`);break}case"path-add":r=fn(r,d.dir,d.position,n.home);break;case"path-remove":{if(!r.some((h)=>h.kind==="path"&&h.dir===d.dir))s.push(`${d.dir} is not added by envhound`);r=be(r,d.dir);let f=z(t,t.final.PATH??"").find((h)=>h.dir===d.dir&&h.addedBy&&!ye(h.addedBy,n));if(f)s.push(`${d.dir} is also added at ${D(f.addedBy,n.home)}; remove that line yourself`);break}}let u=[],o=cn(r);if(o!==i)u.push({path:n.managed,before:i,after:o,mode:384});if(u.length){let d=ln(n.home),f=R(d);if(!an(f??"")){let h=f&&!f.endsWith(`
63
+ `)?`
64
+ `:"";u.push({path:d,before:f,after:(f??"")+h+on})}}return{changes:u,notes:s}}function hn(e,n,t){let i=R(n),r=H(i??""),s=[];for(let l of e)if(l.kind==="set"){r=Ie(r,l.name,l.value);let c=t[l.name];if(c!==void 0&&c!==l.value)s.push(`your shell already has ${l.name} with another value; loaders such as dotenv keep the shell's value`)}else if(l.kind==="unset"){if(!r.lines.some((c)=>c.kind==="entry"&&c.key===l.name))s.push(`${l.name} is not in this file`);r=Ke(r,l.name)}else throw Error("PATH changes only apply to the shell, not to a .env file");let a=je(r);return{changes:a===(i??"")?[]:[{path:n,before:i,after:a}],notes:s}}function mn(e,n,t){let i=(s)=>{let a=_(n,s),l=a.findLastIndex((c)=>ye(c,t));return a.slice(l+1).map((c)=>D(c,t.home))},r=(n.final.PATH??"").split(":");return e.map((s)=>{switch(s.kind){case"set":{if(n.final[s.name]===s.value)return{ok:!0,message:`a fresh login shell now gets ${s.name}`};let a=i(s.name);return{ok:!1,message:`a fresh login shell does not get ${s.name}${a.length?`: ${a.join(", ")} sets it again after envhound`:""}`}}case"unset":if(n.final[s.name]===void 0)return{ok:!0,message:`${s.name} is no longer set in a fresh login shell`};return{ok:!1,message:`${s.name} is still set in a fresh login shell (envhound blame ${s.name} shows where)`};case"path-add":return r.includes(s.dir)?{ok:!0,message:`${s.dir} is now in PATH for fresh login shells`}:{ok:!1,message:`${s.dir} is not in PATH of a fresh login shell (envhound blame PATH shows why)`};case"path-remove":return r.includes(s.dir)?{ok:!1,message:`${s.dir} is still in PATH (envhound path shows who adds it)`}:{ok:!0,message:`${s.dir} is no longer in PATH for fresh login shells`}}})}function gn(e){return e.flatMap((n)=>{switch(n.kind){case"set":return[`export ${n.name}=${B(n.value)}`];case"unset":return[`unset ${n.name}`];case"path-add":return[n.position==="front"?`export PATH=${B(n.dir)}:"$PATH"`:`export PATH="$PATH":${B(n.dir)}`];case"path-remove":return[]}})}import{spawnSync as mt}from"node:child_process";import{mkdtempSync as gt,readFileSync as vt,rmSync as bt}from"node:fs";import{tmpdir as yt,userInfo as vn}from"node:os";import{join as bn,resolve as wt}from"node:path";var xt="\x1F",ce="\x1E",$t=xt+["${BASH_SOURCE[0]:-}","${LINENO}","${FUNCNAME[0]:-}","${BASH_SOURCE[1]:-}","${BASH_LINENO[0]:-}"].join(ce)+ce,St="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";function M(e={}){let n=wt(e.home??process.env.HOME??vn().homedir),t=process.env.USER??vn().username,i={HOME:n,USER:t,LOGNAME:t,SHELL:process.env.SHELL??"/bin/bash",TERM:process.env.TERM??"xterm",PATH:St},r=gt(bn(yt(),"envhound-")),s=bn(r,"env");try{let a=mt("bash",["-lixc",`env -0 > '${s}'`],{env:{...i,PS4:$t},cwd:n,stdio:["ignore","ignore","pipe"],timeout:e.timeoutMs??15000,maxBuffer:268435456,encoding:"utf8"});if(a.error){if(a.error.code==="ETIMEDOUT")throw Error("bash startup files did not finish in time (is something waiting for input?)");throw a.error}let l="";try{l=vt(s,"utf8")}catch{}if(!l)throw Error(`bash startup files did not finish (exit status ${a.status}); an 'exec' or 'exit' in a startup file stops the trace`);let c=kt(l);return delete c.PS4,Ot(a.stderr,i,c)}finally{bt(r,{recursive:!0,force:!0})}}function kt(e){let n={};for(let t of e.split("\x00")){let i=t.indexOf("=");if(i>0)n[t.slice(0,i)]=t.slice(i+1)}return n}var Et=/^([A-Za-z_][A-Za-z0-9_]*)(\+?=)/,At=/^([A-Za-z_][A-Za-z0-9_]*)(?:(\+?=)([\s\S]*))?$/;function Dt(e){let n=Et.exec(e);if(n){let a=e.slice(n[0].length);if(a.startsWith("("))return[];return[{name:n[1],op:n[2],value:Q(a)[0]??"",local:!1,bare:!1}]}let[t,...i]=Q(e),r=i.filter((a)=>a.startsWith("-")).join(""),s=i.filter((a)=>!a.startsWith("-"));if(t==="unset"){if(r.includes("f"))return[];return s.map((a)=>({name:a,op:"unset",value:"",local:!1,bare:!1}))}if(t==="local"||t==="declare"||t==="typeset"){if(/[aAfF]/.test(r))return[];let a=t==="local"||!r.includes("g");return s.flatMap((l)=>{let c=At.exec(l);if(!c)return[];return[{name:c[1],op:c[2]??"=",value:c[3]??"",local:a,bare:c[2]===void 0}]})}return[]}function Ot(e,n,t){let i=[],r=new Map(Object.entries(n)),s=new Map;for(let a of e.split(/(?:^|\n)\x1f+/).slice(1)){let l=a.split(ce);if(l.length<6)continue;let[c,u,o,d,f]=l;if(!c)continue;let h=l.slice(5).join(ce).replace(/\n$/,""),p=o?{kind:o==="source"||o==="."?"source":"function",name:o,at:{file:d,line:Number(f)}}:void 0,g=p?.kind==="function"?`${p.name}@${p.at.file}:${p.at.line}`:void 0;for(let m of Dt(h)){let y=!1;if(g){let P=s.get(g)??new Set;if(s.set(g,P),m.local)P.add(m.name);y=P.has(m.name)}if(m.bare)continue;let w=y?void 0:r.get(m.name),S=m.op==="unset"?void 0:m.op==="+="?(w??"")+m.value:m.value;if(!y)r.set(m.name,S);let{name:K,op:ne,value:x}=m;i.push({name:K,op:ne,value:x,previous:w,result:S,at:{file:c,line:Number(u)},via:p,local:y})}}return{shell:"bash",initial:n,final:t,assignments:i}}function Pt(e,n){let t=R(e),i=H(t??""),r=se(i),s=new Map;for(let l of i.lines)if(l.kind==="entry")s.set(l.key,(s.get(l.key)??0)+1);let a=oe(i,n).map((l)=>({key:l.key,value:l.value,line:l.line,status:l.status,current:l.current,count:s.get(l.key)??1,problem:r.find((c)=>c.key===l.key)?.message}));return{file:e,exists:t!==void 0,rows:a,problems:r.length}}function yn(e,n,t,i=[]){let r=le(R(t.managed)??"",t.home),s=new Set(r.flatMap((o)=>o.kind==="var"?[o.name]:[])),a=new Set(r.flatMap((o)=>o.kind==="path"?[o.dir]:[])),l=(o)=>o.at.file===t.managed?"envhound":D(o,t.home),c=re(e,n).filter((o)=>o.name!=="PATH").map((o)=>({name:o.name,value:o.value,by:o.last?l(o.last):o.kind==="shell"?"(login/shell)":"(inherited)",source:o.last&&Z(o.last),managed:s.has(o.name)}));for(let o of s)if(!c.some((d)=>d.name===o)&&o!=="PATH")c.push({name:o,value:e.final[o],by:"envhound (new shells)",managed:!0});let u=z(e,n.PATH??"").map((o)=>({dir:o.dir,by:o.addedBy?l(o.addedBy):`(${o.source})`,source:o.addedBy&&Z(o.addedBy),managed:a.has(o.dir),exists:o.exists,duplicateOf:o.duplicateOf}));for(let o of a)if(!u.some((d)=>d.dir===o))u.push({dir:o,by:"envhound (new shells)",managed:!0,exists:!0});return{home:t.home,vars:c,path:u,dotenv:i.map((o)=>Pt(o,n))}}import{spawnSync as er}from"node:child_process";import{emitKeypressEvents as nr}from"node:readline";import{isAbsolute as Tt,resolve as wn}from"node:path";function ue(e){return[{kind:"vars"},{kind:"path"},...e.data.dotenv.map((n)=>({kind:"dotenv",file:n.file}))]}var we=(e)=>e.kind==="dotenv"?`dotenv:${e.file}`:e.kind,j=(e)=>ue(e)[e.tab]??{kind:"vars"},k=(e)=>e.cursor[we(j(e))]??0;function xn(e,n={}){return{data:e,tab:n.tab??0,ops:[],cursor:{},filter:"",confirmQuit:!1,help:!1,showSecrets:n.showSecrets??!1,pageSize:10}}function $n(e,...n){let t=e.filter.toLowerCase();return!t||n.some((i)=>i?.toLowerCase().includes(t))}function G(e){let n=e.data.vars.map((t)=>({...t}));for(let t of e.ops){if(t.kind!=="set"&&t.kind!=="unset"||t.file)continue;let i=n.find((r)=>r.name===t.name);if(t.kind==="unset"){if(i)i.pending="unset"}else if(i)i.pending="set",i.newValue=t.value;else n.push({name:t.name,value:void 0,by:"envhound",managed:!0,pending:"new",newValue:t.value})}return n.sort((t,i)=>t.name.localeCompare(i.name)),n.filter((t)=>$n(e,t.name,t.newValue??t.value))}function X(e){let n=e.data.path.map((t)=>({...t}));for(let t of e.ops)if(t.kind==="path-remove"){for(let i of n)if(i.dir===t.dir)i.pending="remove"}else if(t.kind==="path-add"){let i={dir:t.dir,by:"envhound",managed:!0,exists:!0,pending:"add"};if(t.position==="front")n.unshift(i);else n.push(i)}return n}function ee(e,n){let i=(e.data.dotenv.find((r)=>r.file===n)?.rows??[]).map((r)=>({...r}));for(let r of e.ops){if(r.kind!=="set"&&r.kind!=="unset"||r.file!==n)continue;let s=i.find((a)=>a.key===r.name);if(r.kind==="unset"){if(s)s.pending="unset"}else if(s)s.pending="set",s.newValue=r.value;else i.push({key:r.name,value:"",line:0,status:"new",count:0,pending:"new",newValue:r.value})}return i.filter((r)=>$n(e,r.key,r.newValue??r.value))}function Rt(e){let n=j(e);return n.kind==="vars"?G(e).length:n.kind==="path"?X(e).length:ee(e,n.file).length}function de(e){if(e.kind==="set"||e.kind==="unset")return`${e.file??"shell"}:${e.name}`;return`path:${e.dir}`}function J(e,n,t){let i=e.ops.filter((r)=>de(r)!==de(n));return{...e,ops:[...i,n],message:{text:t}}}function xe(e,n,t){return{...e,ops:e.ops.filter((i)=>de(i)!==de(n)),message:{text:t}}}var Mt=/^[A-Za-z_][A-Za-z0-9_]*$/,Lt=/^[A-Za-z_][A-Za-z0-9_.-]*$/;function _t(e,n){let t=e.trim().replace(/^~(?=\/|$)/,n);return Tt(t)?wn(t):wn(process.cwd(),t)}var C=(e,n)=>v(n,e.data.home);function Ht(e){let n=e.prompt,t={...e,prompt:void 0};switch(n.for.kind){case"filter":return{...t,filter:n.value};case"edit":case"new-value":{let{name:i,file:r}=n.for;return J(t,{kind:"set",name:i,value:n.value,file:r},`${i} will be set${r?` in ${C(e,r)}`:""}`)}case"new-name":{let i=n.value.trim(),r=n.for.file;if(!(r?Lt:Mt).test(i))return{...e,message:{text:`'${i}' is not a valid ${r?"key":"variable name"}`,error:!0}};if(i==="PATH"&&!r)return{...t,message:{text:"use the PATH tab to change PATH",error:!0}};let s=r?e.data.dotenv.find((a)=>a.file===r)?.rows.find((a)=>a.key===i)?.value:e.data.vars.find((a)=>a.name===i)?.value;return{...t,prompt:{for:{kind:"new-value",name:i,file:r},label:`${i}=`,value:s??"",secret:T(i)}}}case"path-add":{if(!n.value.trim())return t;let i=_t(n.value,e.data.home);if(e.data.path.some((r)=>r.dir===i))return{...t,message:{text:`${C(e,i)} is already in PATH`,error:!0}};return J(t,{kind:"path-add",dir:i,position:n.for.position},`${C(e,i)} will be added`)}}}function Nt(e,n){let t=e.prompt,i=(r)=>{let s={...e,prompt:{...t,value:r}};return t.for.kind==="filter"?{...s,filter:r,cursor:{...e.cursor,[we(j(e))]:0}}:s};if(n.name==="escape")return{...e,prompt:void 0,filter:t.for.kind==="filter"?"":e.filter};if(n.name==="return"||n.name==="enter")return Ht(e);if(n.name==="backspace")return i([...t.value].slice(0,-1).join(""));if(n.ctrl&&n.name==="u")return i("");if(n.ch&&!n.ctrl)return i(t.value+n.ch);return e}function A(e,n){let t=Math.max(0,Rt(e)-1);return{...e,cursor:{...e.cursor,[we(j(e))]:Math.min(t,Math.max(0,n))}}}function Sn(e,n,t,i,r){switch(n.ch){case"/":return[{...e,prompt:{for:{kind:"filter"},label:"filter: ",value:e.filter}}];case"n":return[{...e,prompt:{for:{kind:"new-name",file:i},label:i?"new key: ":"new variable name: ",value:""}}];case"s":return[{...e,showSecrets:!e.showSecrets}]}if(!t)return[e];if(n.ch==="e"||n.name==="return"||n.name==="enter")return[{...e,prompt:{for:{kind:"edit",name:t.name,file:i},label:`${t.name}=`,value:t.value,secret:T(t.name)}}];if(n.ch==="d")return[r()];if(n.ch==="o"){if(!t.source)return[{...e,message:{text:i?"not in the file yet: write first":"not set by a startup file, nothing to open",error:!0}}];return[e,{kind:"open",at:t.source}]}return[e]}function Vt(e,n){let t=G(e)[k(e)],i=t&&{name:t.name,value:t.newValue??t.value??"",source:t.source};return Sn(e,n,i,void 0,()=>{let r=t,s={kind:"unset",name:r.name};if(r.pending==="new"||r.pending==="unset"||r.pending==="set"&&!r.managed)return xe(e,s,`change to ${r.name} dropped`);if(r.managed)return J(e,s,`${r.name} will be removed from envhound's file`);return{...e,message:{text:kn(r.name,r),error:!0}}})}function Ct(e,n,t){let i=ee(e,t)[k(e)],r=i&&{name:i.key,value:i.newValue??i.value,source:i.line?{file:t,line:i.line}:void 0};return Sn(e,n,r,t,()=>{let s=i,a={kind:"unset",name:s.key,file:t};if(s.pending)return xe(e,a,`change to ${s.key} dropped`);return J(e,a,`${s.key} will be deleted from ${C(e,t)}`)})}function jt(e,n){if(n.ch==="a"||n.ch==="A"){let i=n.ch==="a"?"front":"back";return[{...e,prompt:{for:{kind:"path-add",position:i},label:`add to the ${i} of PATH: `,value:""}}]}let t=X(e)[k(e)];if(!t)return[e];if(n.ch==="d"){let i={kind:"path-remove",dir:t.dir};if(t.pending)return[xe(e,i,`change to ${C(e,t.dir)} dropped`)];if(t.managed)return[J(e,i,`${C(e,t.dir)} will be removed`)];return[{...e,message:{text:kn(C(e,t.dir),t),error:!0}}]}if(n.ch==="o"){if(!t.source)return[{...e,message:{text:"not added by a startup file, nothing to open",error:!0}}];return[e,{kind:"open",at:t.source}]}return[e]}function kn(e,n){if(n.source)return`${e} comes from ${n.by}, not envhound: press o to open that line`;return`${e} ${n.by==="(inherited)"?"comes from the program that started this shell":"is set by login or bash itself"}`}function En(e,n){if(n.ctrl&&n.name==="c")return[e,{kind:"quit"}];if(e.prompt)return[Nt(e,n)];let t={...e,message:void 0,confirmQuit:!1};if(e.help)return[{...t,help:!1}];if(n.ch==="q"||n.name==="escape"&&!e.filter){if(e.ops.length&&!e.confirmQuit)return[{...t,confirmQuit:!0,message:{text:`${e.ops.length} staged change(s) will be lost: q again to quit, w to write`,error:!0}}];return[t,{kind:"quit"}]}if(n.name==="escape")return[{...t,filter:""}];let i=ue(t).length;if(n.name==="tab")return[{...t,tab:(t.tab+1)%i}];if(n.ch&&/^[1-9]$/.test(n.ch)&&Number(n.ch)<=i)return[{...t,tab:Number(n.ch)-1}];if(n.ch==="?")return[{...t,help:!0}];if(n.ch==="w")return t.ops.length?[t,{kind:"write"}]:[{...t,message:{text:"nothing staged yet"}}];if(n.ch==="u"){if(!t.ops.length)return[{...t,message:{text:"nothing to undo"}}];return[A({...t,ops:t.ops.slice(0,-1),message:{text:"undone"}},k(t))]}let r=k(t);switch(n.name){case"up":return[A(t,r-1)];case"down":return[A(t,r+1)];case"pageup":return[A(t,r-t.pageSize)];case"pagedown":return[A(t,r+t.pageSize)];case"home":return[A(t,0)];case"end":return[A(t,1/0)]}if(n.ch==="k")return[A(t,r-1)];if(n.ch==="j")return[A(t,r+1)];if(n.ch==="g")return[A(t,0)];if(n.ch==="G")return[A(t,1/0)];let s=j(t),[a,l]=s.kind==="vars"?Vt(t,n):s.kind==="path"?jt(t,n):Ct(t,n,s.file);return[A(a,k(a)),l]}import{basename as It,relative as Kt}from"node:path";var I=(e,n)=>(t)=>n&&t?`\x1B[${e}m${t}\x1B[0m`:t;function Wt(e){return{bold:I("1",e),dim:I("2",e),inverse:I("7",e),red:I("31",e),green:I("32",e),yellow:I("33",e),cyan:I("36",e)}}function b(e,n){if(n<=0)return"";let t=e.replace(/[\x00-\x1f\x7f]/g," "),i=[...t];return i.length>n?i.slice(0,n-1).join("")+"…":t.padEnd(n+(t.length-i.length))}function Y(e,n){return[...e].length>n?b(e,n):e}function $e(e,n){let t=[...e];return t.length>n?"…"+t.slice(t.length-n+1).join(""):b(e,n)}var Ft={vars:"↑↓ move / filter enter edit n new d unset o open u undo w write tab next ? help q quit",path:"↑↓ move a add front A add end d remove o open u undo w write tab next ? help q quit",dotenv:"↑↓ move / filter enter edit n new d delete o open u undo w write tab next ? help q quit"},zt=["envhound edit: stage changes, then w to review them as a diff and write.","","Variables tab"," enter / e edit the value (envhound's line comes last, so it wins in login shells)"," n new variable"," d unset a variable envhound set, or drop a staged change"," o open your editor at the line that sets it, then reload"," / filter by name or value; esc clears"," s show or hide secret values","","PATH tab"," a / A add a directory at the front / end of PATH"," d remove a directory envhound added, or drop a staged change"," o open the line that adds it","",".env tabs (envhound edit FILE...; ./.env is opened when it exists)"," enter / e edit a value: only that line changes, comments and quoting are kept"," n / d add a key / delete every line for a key"," o open the file at the key's line","","Anywhere"," u undo · w write · tab or 1-9 switch tabs · q or esc quit · ctrl-c quit now","","Press any key to go back."];function Se(e,n,t){return Math.max(0,Math.min(e-Math.floor(t/2),n-t))}function Bt(e,n){if(e.kind==="vars")return"Variables";if(e.kind==="path")return"PATH";let t=Kt(process.cwd(),e.file),i=t&&!t.startsWith("..")?t:v(e.file,n);return i.length>24?It(e.file):i}function Ut(e,n,t){let r=ue(e).map((u,o)=>` ${o+1} ${Bt(u,e.data.home)} `),s=r.map((u,o)=>o===e.tab?t.inverse(u):t.dim(u)).join(" "),a=r.join(" ").length,l=e.ops.length?`${e.ops.length} staged · w to write`:"nothing staged",c=e.ops.length?t.yellow(l):t.dim(l);if(15+a+3+l.length<=n)return`${t.bold("envhound edit")} ${s} ${c}`;if(a+1+l.length<=n)return`${s} ${c}`;if(a<=n)return s;return t.inverse(b(r[e.tab].trim(),n))}function An(e,n,t,i){let r=Wt(i.color),s=[Ut(e,n,r),r.dim("─".repeat(Math.max(0,n)))],a=j(e),l=[r.dim(b(Ft[a.kind],n))];if(e.prompt){let d=e.prompt.secret&&!e.showSecrets?"•".repeat([...e.prompt.value].length):e.prompt.value,f=Math.max(1,n-e.prompt.label.length-1),h=[...d],p=h.length>f?"…"+h.slice(h.length-f+1).join(""):d;l.push(r.bold(e.prompt.label)+p+(i.color?"\x1B[7m \x1B[0m":"_"))}else if(e.message)l.push(e.message.error?r.red(b(e.message.text,n)):r.green(b(e.message.text,n)));else l.push(r.dim(b(e.filter?`filter: ${e.filter} (esc clears)`:"",n)));let c=Math.max(0,t-s.length-l.length-2),u,o;if(e.help)u=zt.map((d)=>b(d,n)),o="";else if(a.kind==="vars")u=qt(e,n,c,r),o=Yt(e);else if(a.kind==="path")u=Zt(e,n,c,r),o=Qt(e);else u=Gt(e,a.file,n,c,r),o=Xt(e,a.file);for(let d=0;d<c;d++)s.push(u[d]??"");return s.push(r.dim("─".repeat(Math.max(0,n)))),s.push(b(o,n)),s.push(...l),s.slice(0,t)}function q(e,n,t){if(n===void 0)return"";let i=n.replace(/\n/g,"\\n");return T(e)&&!t.showSecrets?"********":v(i,t.data.home)}function Dn(e,n,t,i){if(n.pending==="unset")return`${q(e,n.value,t)} (${i})`;if(n.pending==="new")return q(e,n.newValue,t);if(n.pending==="set")return`${q(e,n.value,t)} → ${q(e,n.newValue,t)}`;return q(e,n.value,t)}var ke=(e)=>e==="new"||e==="add"?"+":e==="unset"||e==="remove"?"-":e?"*":" ";function qt(e,n,t,i){let r=G(e);if(!r.length)return[i.dim(e.filter?"no variable matches the filter":"no variables")];let s=Math.min(28,Math.max(8,...r.map((f)=>f.name.length))),a=Math.min(32,Math.max(6,...r.map((f)=>f.by.length))),l=Math.max(10,n-s-a-6),c=t-1,u=k(e),o=Se(u,r.length,c),d=[i.bold(Y(` ${b("NAME",s)} ${b("SET BY",a)} VALUE`,n))];return r.slice(o,o+c).forEach((f,h)=>{let p=Y(`${ke(f.pending)} ${b(f.name,s)} ${$e(f.by,a)} ${b(Dn(f.name,f,e,"unset"),l)}`,n),g=f.pending==="unset"?i.red:f.pending?i.yellow:f.managed?i.cyan:f.by.startsWith("(")?i.dim:(m)=>m;d.push(o+h===u?i.inverse(p):g(p))}),d}function Yt(e){let n=G(e)[k(e)];if(!n)return"";let t=n.managed?"set by envhound":n.source?`set at ${n.by}`:n.by==="(inherited)"?"inherited from the program that started this shell":"set by login or bash";return`${n.name}: ${t}${n.pending?`, staged: ${n.pending}`:""}`}function Zt(e,n,t,i){let r=X(e);if(!r.length)return[i.dim("PATH is empty")];let s=Math.min(60,Math.max(9,...r.map((d)=>v(d.dir,e.data.home).length))),a=Math.min(32,Math.max(8,...r.map((d)=>d.by.length))),l=t-1,c=k(e),u=Se(c,r.length,l),o=[i.bold(Y(` # ${b("DIRECTORY",s)} ${b("ADDED BY",a)}`,n))];return r.slice(u,u+l).forEach((d,f)=>{let h=u+f,p=d.pending?`staged: ${d.pending}`:d.duplicateOf?`duplicate of #${d.duplicateOf}`:!d.exists?"missing":"",g=Y(`${ke(d.pending)} ${String(h+1).padStart(2)} ${$e(v(d.dir,e.data.home),s)} ${$e(d.by,a)} ${b(p,Math.max(0,n-s-a-10))}`,n),m=d.pending==="remove"?i.red:d.pending?i.yellow:p?i.red:d.managed?i.cyan:d.by.startsWith("(")?i.dim:(y)=>y;o.push(h===c?i.inverse(g):m(g))}),o}function Qt(e){let n=X(e)[k(e)];if(!n)return"";let t=n.managed?"added by envhound":n.source?`added at ${n.by}`:n.by==="(initial)"?"in the initial PATH bash starts with":"inherited from the program that started this shell";return`${v(n.dir,e.data.home)}: ${t}`}function Jt(e,n){if(e.pending)return`staged: ${e.pending==="unset"?"delete":e.pending}`;if(e.problem)return e.problem;if(e.count>1)return`defined ${e.count} times`;if(e.status==="conflict")return`shell has ${q(e.key,e.current,n)}`;return e.status==="same"?"same as shell":""}function Gt(e,n,t,i,r){let s=ee(e,n),a=e.data.dotenv.find((p)=>p.file===n);if(!s.length){if(e.filter)return[r.dim("no key matches the filter")];return[r.dim(a?.exists?"no keys in this file: n adds one":"this file does not exist yet: n adds a key, w creates it")]}let l=Math.min(28,Math.max(3,...s.map((p)=>p.key.length))),c=Math.max(10,Math.min(48,t-l-32)),u=Math.max(0,t-l-c-12),o=i-1,d=k(e),f=Se(d,s.length,o),h=[r.bold(Y(` ${b("KEY",l)} LINE ${b("VALUE",c)} `,t))];return s.slice(f,f+o).forEach((p,g)=>{let m=Jt(p,e),y=p.line?String(p.line).padStart(4):" ",w=Y(`${ke(p.pending)} ${b(p.key,l)} ${y} ${b(Dn(p.key,p,e,"delete"),c)} ${b(m,u)}`,t),S=p.pending==="unset"?r.red:p.pending?r.yellow:p.problem||p.count>1?r.red:p.status==="conflict"?r.yellow:p.status==="same"?r.dim:(K)=>K;h.push(f+g===d?r.inverse(w):S(w))}),h}function Xt(e,n){let t=e.data.dotenv.find((c)=>c.file===n),i=v(n,e.data.home),r=t?.problems?` · ${t.problems} problem(s), see envhound dotenv`:"",s=ee(e,n)[k(e)];if(!s)return`${i}${t?.exists?"":" (new file)"}${r}`;let a=s.line?`line ${s.line} of ${i}`:`new in ${i}`,l=s.status==="conflict"?": loaders such as dotenv keep the shell's value":"";return`${s.key}: ${a}${l}${r}`}var tr="\x1B[?1049h\x1B[?25l",rr="\x1B[?25h\x1B[?1049l";function ir(e,n){let[t="vi",...i]=e.trim().split(/\s+/),r=t.split("/").pop()??t;if(/^(code|code-insiders|codium|cursor|windsurf)$/.test(r))return[t,[...i.filter((s)=>s!=="--wait"),"--wait","-g",`${n.file}:${n.line}`]];return[t,[...i,`+${n.line}`,n.file]]}function On(e,n){let t=process.stdin,i=process.stdout,r=xn(e(),{showSecrets:n.showSecrets,tab:n.tab});return new Promise((s,a)=>{let l=()=>{let h=i.columns||80,p=i.rows||24;r={...r,pageSize:Math.max(1,p-8)};let g=An(r,h,p,{color:n.color});i.write("\x1B[H"+g.map((m)=>m+"\x1B[K").join(`\r
65
+ `)+"\x1B[J")},c=()=>{i.write(tr),t.setRawMode(!0),t.resume(),l()},u=()=>{t.setRawMode(!1),t.pause(),i.write(rr)},o=(h)=>{t.off("keypress",f),i.off("resize",l),u(),s(h)},d=(h)=>{u();let[p,g]=ir(process.env.VISUAL||process.env.EDITOR||"vi",h),m=er(p,g,{stdio:"inherit"});if(c(),m.error)r={...r,message:{text:`could not run ${p}: ${m.error.message}`,error:!0}};else i.write("\x1B[H\x1B[Jreloading…"),r={...r,data:e(),message:{text:"reloaded after editing"}};l()},f=(h,p)=>{try{let g=h&&!p?.ctrl&&!p?.meta&&[...h].length===1&&h>=" "&&h!==""?h:void 0,m={name:p?.name,ch:g,ctrl:p?.ctrl},[y,w]=En(r,m);if(r=y,w?.kind==="quit")return o(void 0);if(w?.kind==="write")return o(r.ops);if(w?.kind==="open")return d(w.at);l()}catch(g){t.off("keypress",f),u(),a(g)}};nr(t),t.on("keypress",f),i.on("resize",l),c()})}import{spawn as sr}from"node:child_process";import{mkdirSync as or,readFileSync as ar,realpathSync as lr,renameSync as cr,writeFileSync as dr}from"node:fs";import{dirname as ur}from"node:path";var fr="michelsalib/envhound",pr="https://registry.npmjs.org/envhound/latest",hr=604800000;function De(e){let n=e.replaceAll("\\","/");if(n.includes("/_npx/"))return"npx";if(n.includes("/bunx-"))return"bunx";if(n.includes("/.bun/install/global/"))return"bun";if(n.includes("/node_modules/"))return"npm";if(/\.[cm]?[jt]s$/.test(n))return"dev";return"standalone"}function Oe(e){switch(e){case"npx":return"npx envhound@latest";case"bunx":return"bunx envhound@latest";case"bun":return"bun add -g envhound@latest";case"npm":return"npm install -g envhound@latest";case"standalone":return"envhound upgrade";case"dev":return"git pull && bun run build"}}function mr(e,n){let t=(s)=>(s.match(/^v?(\d+)\.(\d+)\.(\d+)/)??[]).slice(1).map(Number),[i,r]=[t(e),t(n)];if(i.length!==3||r.length!==3)return!1;for(let s=0;s<3;s++)if(i[s]!==r[s])return i[s]>r[s];return!1}function gr(e,n,t){let i=(r)=>r===void 0||t-r>=hr||r>t;return{check:i(e.checkedAt),notify:e.latest!==void 0&&mr(e.latest,n)&&i(e.notifiedAt)}}function Ee(e){try{let n=JSON.parse(ar(e,"utf8"));return typeof n==="object"&&n?n:{}}catch{return{}}}function Ae(e,n){or(ur(e),{recursive:!0});let t=`${e}.${process.pid}.tmp`;dr(t,JSON.stringify(n)+`
66
+ `),cr(t,e)}function Pn(e,n,t,i=Date.now()){try{let r=De(lr(n));if(r==="dev")return;let s=Ee(t),{check:a,notify:l}=gr(s,e,i);if(a)Ae(t,{...s,checkedAt:i}),sr(process.execPath,[n,"__update-check",t],{detached:!0,stdio:"ignore"}).unref();if(!l)return;return Ae(t,{...Ee(t),notifiedAt:i}),`envhound ${s.latest} is available (you have ${e}). Update with: ${Oe(r)}`}catch{return}}async function Tn(e){let n=await fetch(pr,{signal:AbortSignal.timeout(1e4),headers:{accept:"application/json"}});if(!n.ok)return;let{version:t}=await n.json();if(typeof t!=="string")return;Ae(e,{...Ee(e),checkedAt:Date.now(),latest:t})}function Rn(e){return`${e.ENVHOUND_BASE_URL??`https://github.com/${fr}/releases/latest/download`}/install.sh`}var Sr=`envhound ${te.version}: find which startup file sets each environment variable, and change it
67
+
68
+ Usage:
69
+ envhound [list] every exported variable, with the startup file:line that sets it
70
+ envhound blame VAR every startup file:line that assigns VAR, in order
71
+ (for PATH: what each step added or removed)
72
+ envhound path current PATH, one entry per line, with who added it,
73
+ flagging missing directories and duplicates
74
+ envhound dotenv [FILE...]
75
+ check .env files (default ./.env): keys that are new,
76
+ already set, or conflicting with your shell, and syntax
77
+ problems; exits 1 if there are problems
78
+
79
+ envhound set NAME=value... set variables for future login shells
80
+ envhound unset NAME... remove variables envhound set
81
+ envhound path add DIR [--append] put DIR in PATH (at the front unless --append)
82
+ envhound path remove DIR remove a directory envhound added
83
+ These write ~/.config/envhound/env.sh, loaded by one line
84
+ envhound adds to your login file. With --file FILE, set and
85
+ unset edit a .env file instead. Every change is shown as
86
+ a diff and confirmed first; changed files are backed up.
87
+
88
+ envhound edit [FILE...]
89
+ interactive editor for variables, PATH and .env files
90
+ (./.env is included when it exists): stage changes,
91
+ then review them as a diff and write
92
+
93
+ envhound completion SHELL
94
+ print a completion script for bash, zsh or fish, e.g.
95
+ eval "$(envhound completion bash)" in ~/.bashrc
96
+ envhound upgrade update envhound (installed with install.sh; otherwise
97
+ shows the command for npm, bun, npx or bunx)
98
+
99
+ Options:
100
+ --json machine-readable output (list, blame, path, dotenv)
101
+ --show-secrets don't mask values of variables like *_TOKEN or *_KEY
102
+ --home DIR act as if HOME were DIR
103
+ -f, --file FILE set/unset: edit this .env file instead of the shell
104
+ -y, --yes set/unset/path: write without asking
105
+ --dry-run set/unset/path: show the diff, write nothing
106
+ --append path add: put DIR at the end of PATH
107
+ -h, --help show this help
108
+ -v, --version show the version
109
+
110
+ envhound replays a bash login shell from a clean environment with tracing on,
111
+ so it only sees what startup files do. That runs your startup files once.
112
+
113
+ Once a week envhound asks npm in the background whether a newer version exists,
114
+ and says so after a command. ENVHOUND_NO_UPDATE_CHECK=1 turns this off.
115
+ `,Te,kr=/^[A-Za-z_][A-Za-z0-9_]*$/,Er=/^[A-Za-z_][A-Za-z0-9_.-]*$/;async function Ar(e){if(e[0]==="__complete"){for(let u of _e(e.slice(1),process.env))console.log(u);return 0}if(e[0]==="__update-check")return await Tn(e[1]),0;let{values:n,positionals:t}=$r({args:e,allowPositionals:!0,options:{json:{type:"boolean"},"show-secrets":{type:"boolean"},home:{type:"string"},file:{type:"string",short:"f"},yes:{type:"boolean",short:"y"},"dry-run":{type:"boolean"},append:{type:"boolean"},help:{type:"boolean",short:"h"},version:{type:"boolean",short:"v"}}});if(n.help)return process.stdout.write(Sr),0;if(n.version)return console.log(te.version),0;let[i="list",...r]=t,s=process.env;if(process.stderr.isTTY&&n.home===void 0&&!n.json&&!s.CI&&!s.ENVHOUND_NO_UPDATE_CHECK&&!s.NO_UPDATE_NOTIFIER&&!["completion","upgrade"].includes(i))Te=Pn(te.version,process.argv[1],ae(L(s.HOME??""),s,!1).updateState);let a=process.stdout.isTTY,l={home:L(n.home??process.env.HOME??""),color:a&&!process.env.NO_COLOR,width:a?process.stdout.columns:void 0,showSecrets:n["show-secrets"]??!1},c=(u,o)=>console.log(n.json?JSON.stringify(o(),null,2):u());switch(i){case"list":{let u=re(M({home:n.home}),process.env);return c(()=>Be(u,l),()=>Ye(u,l)),0}case"blame":{let u=r[0];if(!u)return O("blame needs a variable name, e.g. envhound blame PATH");let o=he(M({home:n.home}),u,process.env);return c(()=>Ue(o,l),()=>Ze(o,l)),0}case"path":{let[u,...o]=r;if(u==="add"||u==="remove"){if(!o.length)return O(`path ${u} needs a directory`);let f=o.map((h)=>u==="add"?{kind:"path-add",dir:L(h),position:n.append?"back":"front"}:{kind:"path-remove",dir:L(h)});if(n.file)return O("--file only applies to set and unset");return Pe(f,n,l)}if(u!==void 0)return O(`unknown path command '${u}' (add, remove)`);let d=z(M({home:n.home}),process.env.PATH??"");return c(()=>qe(d,l),()=>d),0}case"set":case"unset":{if(!r.length)return O(i==="set"?"set needs NAME=value":"unset needs a NAME");let u=n.file?Er:kr,o=[];for(let d of r){let f=d.indexOf("="),h=i==="set"?d.slice(0,f):d;if(i==="set"&&f<0)return O(`expected NAME=value, got '${d}'`);if(!u.test(h))return O(`'${h}' is not a valid variable name`);if(h==="PATH"&&!n.file)return O("use 'envhound path add DIR' to change PATH");let p=n.file&&L(n.file);o.push(i==="set"?{kind:"set",name:h,value:d.slice(f+1),file:p}:{kind:"unset",name:h,file:p})}return Pe(o,n,l)}case"edit":{if(!process.stdin.isTTY||!process.stdout.isTTY)return O("edit needs a terminal");let u=ae(l.home,process.env,n.home!==void 0),o=[...r,...n.file?[n.file]:[]].map((p)=>L(p)),d=o.length?[...new Set(o)]:br(".env")?[L(".env")]:[];process.stderr.write("tracing startup files…\r");let h=await On(()=>yn(M({home:n.home}),process.env,u,d),{...l,tab:o.length?2:0});if(process.stderr.write("\x1B[K"),!h?.length)return 0;return Pe(h,n,l)}case"dotenv":{let u=r.length?r:[".env"],o,d=u.map((h)=>{let p=L(h),g=H(Mn(p,"utf8")),m=oe(g,process.env),y=m.filter((S)=>S.status==="conflict");if(y.length)o??=M({home:n.home});let w=Object.fromEntries(y.map((S)=>[S.key,he(o,S.key,process.env).assignments.at(-1)]));return{file:p,keys:m,problems:se(g),shellOrigin:w}}),f=d.map((h)=>({...h,keys:h.keys.map((p)=>({...p,value:E(p.key,p.value,l),current:E(p.key,p.current,l)}))}));return c(()=>d.map((h)=>Qe(h,l)).join(`
116
+
117
+ `),()=>f),d.some((h)=>h.problems.length)?1:0}case"completion":{let u=r[0];if(!ie.includes(u))return O(`completion needs a shell: ${ie.join(", ")}`);return process.stdout.write(He(u)),0}case"upgrade":return Dr();default:return O(`unknown command '${i}'`)}}async function Pe(e,n,t){let i=U(t),r=ae(t.home,process.env,n.home!==void 0),s=e.filter((f)=>!(("file"in f)&&f.file)),a=[...new Set(e.flatMap((f)=>("file"in f)&&f.file?[f.file]:[]))],l=[...s.length?[pn(s,r,M({home:n.home}))]:[],...a.map((f)=>hn(e.filter((h)=>("file"in h)&&h.file===f),f,process.env))],c={changes:l.flatMap((f)=>f.changes),notes:l.flatMap((f)=>f.notes)};for(let f of c.notes)console.log(i.yellow(`note: ${f}`));if(!c.changes.length)return console.log("Nothing to change."),0;if(c.notes.length)console.log();if(console.log(c.changes.map((f)=>Ge(f,t)).join(`
118
+
119
+ `)+`
120
+ `),n["dry-run"])return 0;if(!n.yes){if(!process.stdin.isTTY)return console.error("envhound: not a terminal, so envhound can't ask; pass --yes to write without asking"),1;if(!await Xe("Write these changes? [y/N] "))return console.log("Nothing written."),1}let u=en(c.changes,r.backups);for(let f of c.changes)console.log(`wrote ${v(f.path,t.home)}`);if(u.length)console.log(i.dim(`backups in ${v(r.backups,t.home)}`));if(!s.length)return 0;let o=mn(s,M({home:n.home}),r);for(let f of o)console.log(f.ok?i.green(`✓ ${f.message}`):i.yellow(`! ${f.message}`));let d=gn(s);if(d.length)console.log(i.dim(`
121
+ This shell is unchanged. To apply it here too, run:
122
+ `)+d.map((f)=>` ${f}`).join(`
123
+ `));return o.every((f)=>f.ok)?0:1}async function Dr(){let e=yr(process.argv[1]),n=De(e);if(n!=="standalone")return console.log(`This envhound was not installed with install.sh. Update it with:
124
+ ${Oe(n)}`),0;let t=Rn(process.env),i;try{if(t.startsWith("file:"))i=Mn(xr(t),"utf8");else{let s=await fetch(t,{signal:AbortSignal.timeout(30000)});if(!s.ok)throw Error(`HTTP ${s.status}`);i=await s.text()}}catch(s){return console.error(`envhound: could not download ${t}: ${s instanceof Error?s.message:s}`),1}return vr("sh",["-s"],{input:i,stdio:["pipe","inherit","inherit"],env:{...process.env,ENVHOUND_INSTALL_DIR:wr(e)}}).status??1}function O(e){return console.error(`envhound: ${e}
125
+ Run 'envhound --help' for usage.`),2}Ar(process.argv.slice(2)).then((e)=>{if(process.exitCode=e,Te)process.stderr.write(`
126
+ ${Te}
127
+ `)},(e)=>{console.error(`envhound: ${e instanceof Error?e.message:e}`),process.exitCode=1});
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "envhound",
3
+ "version": "0.1.0",
4
+ "description": "Find which startup file sets each environment variable, and audit your PATH",
5
+ "keywords": [
6
+ "environment",
7
+ "env",
8
+ "path",
9
+ "bashrc",
10
+ "profile",
11
+ "dotfiles",
12
+ "shell"
13
+ ],
14
+ "license": "MIT",
15
+ "author": "Michel Salib",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/michelsalib/envhound.git"
19
+ },
20
+ "homepage": "https://github.com/michelsalib/envhound#readme",
21
+ "bugs": "https://github.com/michelsalib/envhound/issues",
22
+ "type": "module",
23
+ "bin": {
24
+ "envhound": "dist/envhound.js"
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "scripts": {
33
+ "dev": "bun src/cli.ts",
34
+ "build": "bun build src/cli.ts --target=node --minify --outfile dist/envhound.js",
35
+ "test": "bun test",
36
+ "typecheck": "tsc",
37
+ "prepublishOnly": "bun run typecheck && bun run build && bun test"
38
+ },
39
+ "devDependencies": {
40
+ "@types/bun": "^1.4.2",
41
+ "@types/node": "^26.6.4",
42
+ "typescript": "^7.0.2"
43
+ }
44
+ }