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 +21 -0
- package/README.md +169 -0
- package/dist/envhound.js +127 -0
- package/package.json +44 -0
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.
|
package/dist/envhound.js
ADDED
|
@@ -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
|
+
}
|