@jondotsoy/envs 0.1.7 → 0.1.9-alpha.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  CLI that keeps the `.env` values of every git worktree in a single file (`.envs/values.yml`). Edit all values together and distribute them to each worktree, or collect them from the existing `.env` files.
4
4
 
5
- Requires [Bun](https://bun.com) and git.
5
+ Requires [Bun](https://bun.com) and git. See [docs/features.md](docs/features.md) for a summary of all features.
6
6
 
7
7
  ![Each worktree keeps its own .env; envs edit and envs push sync the shared values across them](docs/assets/images/worktrees-sync.svg)
8
8
 
@@ -36,13 +36,23 @@ The examples below use `envs`; without a global install, replace it with `bunx @
36
36
 
37
37
  Run the commands inside the repo or any of its worktrees. Inside a worktree, `.envs/` always lives in the main repo.
38
38
 
39
+ ![Web editor started with envs edit --ui](docs/assets/images/edit-ui.png)
40
+
41
+ A walkthrough with a screenshot of each action is in [docs/ui.md](docs/ui.md) (in Spanish).
42
+
39
43
  | Command | What it does |
40
44
  | --- | --- |
41
45
  | `envs edit` | Runs `init` if `.envs/values.yml` does not exist yet, then runs `pull`, opens `values.yml` with `code -w` and, when the editor closes, runs `push`. If the editor fails, it does not `push`. |
46
+ | `envs edit --ui` | Same `init` and `pull`, but instead of VS Code it starts a local server (`127.0.0.1`, random port) and opens a React web editor with the values of `values.yml` (worktree profiles, defaults and per-worktree values). The **Save** button in the top right corner writes `values.yml` and runs `push`. Its **History** panel lists the history points (newest first); hovering or clicking a point shows what restoring it would change in the editor (red: removed, green: changed or added), and its **Restore** button, after a confirmation, runs `envs restore` for that point. Open the address it prints (it carries a one-time access token); requests without the token, from another host name or from another site are refused (see [docs/ui.md](docs/ui.md)). Stop the server with Ctrl+C. |
42
47
  | `envs init` | Creates `.envs/`, `.envs/.gitignore` (containing `*`) and an empty `.envs/values.yml`. Does not overwrite existing files. |
43
48
  | `envs pull` | Reads the `.env` of the main repo and of each worktree and writes it to `values.yml`. Variables that are no longer in a `.env` are removed from `values.yml` for that worktree. |
44
- | `envs push` | Writes `values.yml` to the `.env` of each worktree. Updates existing variables in place, appends new ones at the end, and keeps comments and variables that are not in `values.yml`. |
49
+ | `envs push` | Writes `values.yml` to the `.env` of each worktree. Updates existing variables in place, appends new ones at the end, keeps comments, refuses (writing nothing) a variable name that is not valid or a value with a line break, and removes the orphan variables: those `envs` manages (they are, or were, in `values.yml` or its history) but that the worktree no longer resolves, for example after `envs restore`. Variables that `envs` never saw are left alone. |
45
50
  | `envs use [profile]` | Sets the profile of the current worktree (`uses.<worktree>`) and pushes that worktree. Without arguments it lists the profile of each worktree. The profile must already exist in `values.yml`. |
51
+ | `envs restore <hash> [--force]` | Restores `values.yml` to the state it had after the history point `<hash>` (a unique prefix is enough), adds a new point with the difference and runs `push`. Existing points are never removed. The history keeps secrets only as fingerprints, so a restored secret keeps the value it has now (and `restore` says so). It refuses, changing nothing, if the hash chain of the history is broken (an altered, removed or reordered point) unless you pass `--force`. See [docs/history.md](docs/history.md). |
52
+ | `envs history redact` | Rewrites an existing history so it no longer holds any secret in clear (points written before secrets were redacted) and relinks its hash chain. See [docs/history.md](docs/history.md). |
53
+ | `envs config set <key> <value>` | Saves a setting in `.envs/config.yml` (created on demand with mode `600`, along with `.envs/.gitignore` if missing). `true`, `false` and numbers are stored as such; the rest as strings. |
54
+ | `envs config get [key]` | Prints the value of a setting, or every `key=value` when no key is given. Exits with code 1 if the key does not exist. |
55
+ | `envs config delete <key>` | Removes a setting. Exits with code 1 if the key does not exist. |
46
56
  | `envs lint` | Checks security and prints a warning for each problem. Exits with code 1 if there is any (useful in CI). See below. |
47
57
  | `envs help` | Shows the help (also `--help`, `-h`, or no command). |
48
58
 
@@ -83,16 +93,90 @@ It never prints values, only the variable and its location (`envs.DB_PASSWORD.ma
83
93
  | Rule | Detects |
84
94
  | --- | --- |
85
95
  | `tracked-values`, `unignored-values` | `values.yml` is tracked by git or is not ignored. |
96
+ | `tracked-history`, `unignored-history` | `.envs/history.ndjson` (which holds every previous and new value, secrets included) is tracked by git or is not ignored. |
86
97
  | `tracked-dotenv`, `unignored-dotenv` | A worktree's `.env` is tracked or is not ignored. |
87
- | `open-permissions` | `values.yml` is readable by other users (use `chmod 600`). |
88
- | `executable-files` | `values.yml` or a `.env` has the execute permission (use `chmod -x`). |
89
- | `writable-files` | `values.yml` or a `.env` is writable by the group or other users (use `chmod go-w`). |
98
+ | `open-permissions` | `values.yml` or the history is readable by other users (use `chmod 600`). |
99
+ | `executable-files` | `values.yml`, the history or a `.env` has the execute permission (use `chmod -x`). |
100
+ | `writable-files` | `values.yml`, the history or a `.env` is writable by the group or other users (use `chmod go-w`). |
90
101
  | `weak-secret` | Sensitive variable (`PASSWORD`, `TOKEN`, `SECRET`, `API_KEY`…) that is empty or has a typical value (`changeme`, `admin`…). |
91
102
  | `shared-secret` | Sensitive variable in `defaults`, which is written to all worktrees using that profile. |
103
+ | `unsafe-dotenv-value` | A variable name that is not valid, or a value with a line break: `push` would refuse it (it would start another line in the `.env`). |
104
+ | `unknown-worktree` | `values.yml` has values for a worktree that git does not list (deleted, or its branch renamed): they stay forever unless you remove them. `pull` does not delete them, because a worktree can be missing only for a moment. |
92
105
  | `unknown-profile` | `uses` names a profile that is not in `profiles`. |
93
106
  | `secret-pattern` | Value with a known secret format (AWS, GitHub, Slack, `sk-…`, private key). |
94
107
  | `url-credentials`, `insecure-url` | Remote URL with an embedded password, or using `http://`, `ws://` or `ftp://`. |
95
108
 
109
+ ## History
110
+
111
+ See [docs/history.md](docs/history.md) for the full documentation (in Spanish).
112
+
113
+ `envs` keeps a record of how `values.yml` evolved. It works with two separate concepts:
114
+
115
+ | Concept | Where | What it is |
116
+ | --- | --- | --- |
117
+ | **Snapshot** | `.envs/values.yml` | The complete current state: every profile, default and per-worktree value. It is the file you edit, and it is the only thing `push` reads. |
118
+ | **Changes** | `.envs/history.ndjson` | An append-only log. Each line is a *point* holding only what changed between two states, never a full copy of the file. |
119
+
120
+ The snapshot answers "what are the values now?"; the changes answer "what changed, when, and from what?". The history never replaces `values.yml`: if you delete `history.ndjson`, `envs` keeps working and the next `pull` starts a new history.
121
+
122
+ ### A point
123
+
124
+ One JSON object per line:
125
+
126
+ ```json
127
+ {"hash":"3f9a1c0b7d2e","prev":"e7c8ebe462f1","timestamp":"2026-01-01T00:02:00.000Z","changes":[{"path":["profiles","default","envs","PORT","feature-x"],"from":3001,"to":3002}]}
128
+ ```
129
+
130
+ - `changes` lists the differences. `path` locates the value inside the snapshot (`uses.<worktree>`, `profiles.<profile>.defaults.<VAR>` or `profiles.<profile>.envs.<VAR>.<worktree>`). `from` is absent when the entry was added and `to` is absent when it was removed.
131
+ - `prev` is the hash of the previous point (`null` for the first one) and `hash` is the first 12 hex characters of the sha256 of `prev`, `timestamp` and `changes`. The file is a hash chain: altering or removing a point breaks every later one.
132
+
133
+ ### When a point is added
134
+
135
+ A point is written only when `values.yml` actually changed.
136
+
137
+ - The **first** point of a history (whichever command writes it: `pull`, `edit`, Save in the web editor or `use`) describes the whole snapshot, as if it had been built from nothing. So a project that predates the history, or whose `history.ndjson` was deleted, still gets a complete base.
138
+ - Any later `envs pull` adds a point with what it changed, for example after you edited a worktree's `.env` by hand.
139
+ - `envs edit` adds a point with what you changed in the editor, and so does Save in `envs edit --ui`.
140
+ - `envs use <profile>` adds a point with the profile change.
141
+
142
+ ### Reading it
143
+
144
+ Applying the points in order, from the first one, rebuilds the snapshot as it was after each of them. Because the first point contains the whole initial state, the result after the last point equals `values.yml`. A change made outside the history (a manual edit of `values.yml`, or a period with the history off) is recorded by the next command that writes a point, which first adds a *resync point* (`"resync": true`) holding the difference.
145
+
146
+ ### Secrets
147
+
148
+ The history never holds a secret. A variable whose name looks sensitive (`PASSWORD`, `TOKEN`, `SECRET`, `API_KEY`…) or whose value is a URL with a password (or a sensitive query parameter) is stored as `envs:redacted:<fingerprint>`: the fingerprint is keyed with `.envs/history.key`, changes when the secret changes (so the history still shows *that* it was rotated) and cannot be reversed. Consequently, `envs restore` cannot bring back an old secret: it keeps the current one (and says so). Set `envs config set history.secrets true` to keep secrets in clear in the history instead. On an existing history, run `envs history redact` once. The same masking applies to the console: `push` and `restore` show `postgres://user:********@host/db`.
149
+
150
+ ### Concurrency and permissions
151
+
152
+ Commands that write take `.envs/lock`, so two `envs` processes (or the web editor and a terminal) never interleave: the history stays one linear chain. A lock left by a process that died is taken over. `.envs/` is created `700` and its files (`values.yml`, the history, its key and the config) `600`, and any command that writes tightens a project created with looser modes; a `.env` that `push` creates is `600` too.
153
+
154
+ ### Damaged history
155
+
156
+ A damaged `history.ndjson` never stops a command. An incomplete last line (an interrupted write) is ignored and dropped by the next point; an unreadable line in the middle is skipped with a warning naming its line number. `envs restore` refuses, changing nothing, until those lines are repaired or removed, and the web editor flags them. See [docs/history.md](docs/history.md).
157
+
158
+ ### Turning it off
159
+
160
+ The history is active by default. It reads the `history` setting of `.envs/config.yml`: only an explicit `false` turns it off, and `values.yml` keeps being updated as usual.
161
+
162
+ ```sh
163
+ envs config set history false # stop recording
164
+ envs config set history true # record again (or `envs config delete history`)
165
+ ```
166
+
167
+ Changes made while it is off get no point at that moment, but they are not lost: when you turn it on again, the next recorded point is preceded by a resync point with what changed meanwhile.
168
+
169
+ ### In the web editor
170
+
171
+ The **History** panel of `envs edit --ui` lists the points, newest first, with their short hash, time and number of changes. Hovering a point, or clicking it to keep it selected (a click never restores), compares the state after that point with the values in the editor, grouped by worktree profiles and by profile (`defaults`, `envs`):
172
+
173
+ - **red**: the entry exists now and would be removed;
174
+ - **green**: the entry would change (current value → value at that point) or would be added.
175
+
176
+ When they are equal it says "No differences". **Restore** asks for confirmation (and warns that unsaved editor changes will be lost), then restores the point like `envs restore`: a new point is appended, none is removed, and the editor and the panel reload. With the history off the panel shows an empty state and offers no restore.
177
+
178
+ The file is created with mode `600` because it holds variable values. Like the rest of `.envs/`, it is ignored by git.
179
+
96
180
  ## `values.yml` format
97
181
 
98
182
  ```yaml
package/SECURITY.md CHANGED
@@ -27,8 +27,12 @@ I will reply as soon as possible and we will coordinate the fix and the disclosu
27
27
  | --- | --- | --- |
28
28
  | `envs init` | — | Creates `<root>/.envs/`, `<root>/.envs/.gitignore` and `<root>/.envs/values.yml`, only if they do not exist. Never overwrites. |
29
29
  | `envs pull` | `<worktree>/.env` of each worktree | Rewrites `<root>/.envs/values.yml`. The file is serialized again, so YAML comments are lost. |
30
- | `envs push` | `<root>/.envs/values.yml` | Creates or updates `<worktree>/.env` of each worktree that has values. Updates existing variables in place, appends new ones at the end and keeps the rest of the file. If there are `defaults`, this includes all worktrees. |
31
- | `envs edit` | Same as `pull` and `push` | Same as `pull` and `push`. It also runs `code -w <root>/.envs/values.yml`. |
30
+ | `envs push` | `<root>/.envs/values.yml` | Creates or updates `<worktree>/.env` of each worktree that has values. Updates existing variables in place, appends new ones at the end and keeps the rest of the file, except the orphan variables: those `envs` manages (they are or were in `values.yml` or its history) that the worktree no longer resolves are removed. If there are `defaults`, this includes all worktrees. |
31
+ | `envs edit` | Same as `pull` and `push` | Same as `pull` and `push`. It also runs `code -w <root>/.envs/values.yml`. With `--ui` it serves the web editor instead (see below). |
32
+ | `envs use`, `envs restore` | `<root>/.envs/values.yml`, `<root>/.envs/history.ndjson` | Rewrite `values.yml` and then act as `push`. |
33
+ | `envs config` | `<root>/.envs/config.yml` | Creates or updates `<root>/.envs/config.yml` (mode `600`). |
34
+
35
+ Every command that changes `values.yml` also appends to `<root>/.envs/history.ndjson` (mode `600`, append-only) unless the history is turned off with `envs config set history false`.
32
36
 
33
37
  Additionally:
34
38
 
@@ -45,12 +49,20 @@ Additionally:
45
49
  - `envs push` writes the values to the `.env` files of each worktree. Make sure `.env` is in your project's `.gitignore`.
46
50
  - Do not share `values.yml` or paste it in issues or logs.
47
51
 
52
+ ## Web editor (`envs edit --ui`)
53
+
54
+ The server listens on `127.0.0.1` only and exposes the values of every `.env`, secrets included, so each request must pass these checks: a random per-run access token (in the printed URL, then an `HttpOnly`, `SameSite=Strict` cookie), a `Host` of this server (no DNS rebinding), an `Origin` of this server, `application/json` bodies and a body of at most 5 MiB; it never answers CORS preflights. Open only the address printed by the command and do not share it: it carries the token. See [docs/ui.md](docs/ui.md).
55
+
56
+ ## The history
57
+
58
+ `.envs/history.ndjson` keeps the previous and the new value of every variable that changes. Secrets (a sensitive variable name, or a URL with a password or a sensitive query parameter) are stored only as keyed fingerprints (`envs:redacted:…`, key in `.envs/history.key`), never in clear, unless you set `envs config set history.secrets true`. It is append-only: a value that was recorded cannot be removed without rewriting the file; on a history written before this, run `envs history redact`. `.envs/` is `700` and its files `600`, and, like the rest of `.envs/`, it is ignored by git. `envs lint` checks its permissions and that git ignores it. Turn it off with `envs config set history false` if you do not want it. Writing commands take `.envs/lock`, so concurrent processes cannot corrupt it.
59
+
48
60
  ## Console output
49
61
 
50
62
  `envs push` prints one line for each variable whose value changes, in the form `↻ NAME=VALUE → branch, ...`, and `envs pull` prints `↓ pulling branch, ... - N variables`. To keep secrets out of the terminal and CI logs:
51
63
 
52
64
  - The value is replaced by `********` when the variable name contains a sensitive word: `SECRET`, `PASSWORD`, `PASSWD`, `PWD`, `TOKEN`, `KEY` (including `API_KEY` and `PRIVATE_KEY`), `CREDENTIAL`, `AUTH`, `SALT` or `SIGNING`. The match is case-insensitive and applies to whole `_`-separated words, so `KEY_FOO` and `AWS_BUCKET_SECRET` are masked but `MONKEY` is not.
53
- - Masking is based on the name only. A secret stored in a variable with an innocuous name (for example `DATABASE_URL` with a password inside) is printed as is.
65
+ - A URL keeps its host and loses its secrets: `postgres://user:********@host/db?token=********` (the password and the values of sensitive query parameters are masked). A secret stored in a variable with an innocuous name and no URL form is still printed as is.
54
66
  - `envs pull` never prints values, only branch names and the number of variables.
55
67
  - `envs lint` messages never include the matched value.
56
68
  - Set `NO_COLOR` to a non-empty value to print the output without colors.