@jondotsoy/envs 0.1.3 → 0.1.4

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
@@ -33,6 +33,26 @@ Run the commands inside the repo or any of its worktrees. Inside a worktree, `.e
33
33
 
34
34
  Worktrees are identified by their branch name (or their folder name if they are in detached HEAD). The main repo uses its branch name, for example `main`.
35
35
 
36
+ ### Output
37
+
38
+ `envs pull` prints one green line with the pulled branches and the number of variables:
39
+
40
+ ```
41
+ ↓ pulling main, feature/biz - 3 variables
42
+ ```
43
+
44
+ `envs push` prints one yellow line for each variable whose value changes, listing the branches where it changed. Variables that already have the same value are not shown:
45
+
46
+ ```
47
+ ↻ LOG_LEVEL=info → main, feature/biz
48
+ ```
49
+
50
+ Colors are disabled when the [`NO_COLOR`](https://no-color.org) environment variable is set to a non-empty value:
51
+
52
+ ```sh
53
+ NO_COLOR=1 envs push
54
+ ```
55
+
36
56
  ### `envs lint`
37
57
 
38
58
  It never prints values, only the variable and its location (`envs.DB_PASSWORD.main`).
package/SECURITY.md CHANGED
@@ -44,3 +44,13 @@ Additionally:
44
44
  - `envs init` creates `.envs/.gitignore` with `*`, so the `.envs/` folder is not committed to git. Do not remove that file.
45
45
  - `envs push` writes the values to the `.env` files of each worktree. Make sure `.env` is in your project's `.gitignore`.
46
46
  - Do not share `values.yml` or paste it in issues or logs.
47
+
48
+ ## Console output
49
+
50
+ `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
+
52
+ - 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.
54
+ - `envs pull` never prints values, only branch names and the number of variables.
55
+ - `envs lint` messages never include the matched value.
56
+ - Set `NO_COLOR` to a non-empty value to print the output without colors.
package/envs.js CHANGED
@@ -229,6 +229,12 @@ Commands:
229
229
  edit Pull, open .envs/values.yml in VS Code (code -w), then push
230
230
  help Show this help message
231
231
  `;
232
+ var colorsDisabled = () => Boolean(process.env.NO_COLOR);
233
+ var MASK = "********";
234
+ var isMaskedKey = (key) => isSensitiveKey(key) || /(^|_)KEY(_|$)/i.test(key);
235
+ var paint = (code, text) => colorsDisabled() ? text : `\x1B[${code}m${text}\x1B[0m`;
236
+ var green = (text) => paint(32, text);
237
+ var yellow = (text) => paint(33, text);
232
238
  var defaultContext = () => ({
233
239
  cwd: process.cwd(),
234
240
  log: (message) => console.log(message),
@@ -348,6 +354,7 @@ async function push(ctx) {
348
354
  if (!read)
349
355
  return 1;
350
356
  const { values } = read;
357
+ const changes = new Map;
351
358
  for (const { name, path } of await listWorktrees(ctx.cwd)) {
352
359
  const entries = {};
353
360
  for (const [key, value] of Object.entries(values.defaults ?? {})) {
@@ -361,8 +368,18 @@ async function push(ctx) {
361
368
  continue;
362
369
  const dotenv = Bun.file(join(path, ".env"));
363
370
  const current = await dotenv.exists() ? await dotenv.text() : "";
371
+ const existing = parseDotenv(current);
372
+ for (const [key, value] of Object.entries(entries)) {
373
+ if (existing[key] === value)
374
+ continue;
375
+ const shown = isMaskedKey(key) ? MASK : formatDotenvValue(value);
376
+ const line = `${key}=${shown}`;
377
+ changes.set(line, [...changes.get(line) ?? [], name]);
378
+ }
364
379
  await Bun.write(dotenv, updateDotenv(current, entries));
365
- ctx.log(`Pushed ${name}`);
380
+ }
381
+ for (const [line, names] of changes) {
382
+ ctx.log(yellow(`\u21BB ${line} \u2192 ${names.join(", ")}`));
366
383
  }
367
384
  return 0;
368
385
  }
@@ -373,11 +390,16 @@ async function pull(ctx) {
373
390
  const { path: valuesPath, values } = read;
374
391
  const envs = values.envs ??= {};
375
392
  const defaults = values.defaults ?? {};
393
+ const pulledNames = [];
394
+ const pulledKeys = new Set;
376
395
  for (const { name, path } of await listWorktrees(ctx.cwd)) {
377
396
  const dotenv = Bun.file(join(path, ".env"));
378
397
  if (!await dotenv.exists())
379
398
  continue;
380
399
  const parsed = parseDotenv(await dotenv.text());
400
+ pulledNames.push(name);
401
+ for (const key of Object.keys(parsed))
402
+ pulledKeys.add(key);
381
403
  for (const [key, byWorktree] of Object.entries(envs)) {
382
404
  if (!(key in parsed))
383
405
  delete byWorktree[name];
@@ -392,13 +414,15 @@ async function pull(ctx) {
392
414
  (envs[key] ??= {})[name] = parsedValue;
393
415
  }
394
416
  }
395
- ctx.log(`Pulled ${name}`);
396
417
  }
397
418
  for (const [key, byWorktree] of Object.entries(envs)) {
398
419
  if (Object.keys(byWorktree).length === 0)
399
420
  delete envs[key];
400
421
  }
401
422
  await Bun.write(valuesPath, Bun.YAML.stringify(values, null, 2));
423
+ if (pulledNames.length > 0) {
424
+ ctx.log(green(`\u2193 pulling ${pulledNames.join(", ")} - ${pulledKeys.size} variables`));
425
+ }
402
426
  return 0;
403
427
  }
404
428
  async function isIgnored(dir, file) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jondotsoy/envs",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "license": "MIT",
5
5
  "author": {
6
6
  "name": "Jonathan Delgado",