@akshxy/envgit 0.5.1 → 0.6.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 +73 -14
- package/bin/envgit.js +117 -92
- package/package.json +2 -1
- package/src/commands/add-env.js +4 -0
- package/src/commands/audit.js +4 -1
- package/src/commands/copy.js +15 -13
- package/src/commands/delete.js +11 -5
- package/src/commands/doctor.js +3 -6
- package/src/commands/envs.js +16 -10
- package/src/commands/fix.js +130 -0
- package/src/commands/get.js +6 -5
- package/src/commands/init.js +2 -2
- package/src/commands/join.js +1 -1
- package/src/commands/rename-key.js +8 -9
- package/src/commands/rotate-key.js +2 -5
- package/src/commands/scan.js +207 -0
- package/src/commands/set.js +13 -12
- package/src/commands/status.js +25 -13
- package/src/commands/unpack.js +19 -13
- package/src/commands/use.js +29 -0
- package/src/interactive.js +83 -0
- package/src/keystore.js +2 -2
- package/src/ui.js +30 -5
- package/src/commands/keygen.js +0 -71
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@ envgit join abc123... --code Xk9mP2...==
|
|
|
61
61
|
# ✓ Key saved to ~/.config/envgit/keys/<project-id>.key
|
|
62
62
|
|
|
63
63
|
envgit verify # confirm the key works
|
|
64
|
-
envgit unpack
|
|
64
|
+
envgit unpack # writes .env (picks up active env)
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
The relay is **cryptographically blind** — it stores only AES-256-GCM ciphertext and never sees the passphrase. The link is deleted the moment your teammate uses it.
|
|
@@ -131,16 +131,15 @@ Supports 100+ services out of the box: OpenAI, Anthropic, Groq, Stripe, Supabase
|
|
|
131
131
|
| Command | Description |
|
|
132
132
|
|---------|-------------|
|
|
133
133
|
| `envgit init` | Initialize project, generate key, save to `~/.config/envgit/keys/` |
|
|
134
|
-
| `envgit keygen` | Generate a new key for the current project |
|
|
135
|
-
| `envgit keygen --show` | Print current key |
|
|
136
|
-
| `envgit keygen --set <key>` | Save a key for the current project |
|
|
137
134
|
| `envgit share` | Upload encrypted key to a one-time relay link |
|
|
138
135
|
| `envgit join <token> --code <passphrase>` | Download and save a key shared via `envgit share` |
|
|
139
|
-
| `envgit rotate-key` | Generate new key and re-encrypt all environments |
|
|
136
|
+
| `envgit rotate-key` | Generate new key and re-encrypt all environments, then run `envgit share` |
|
|
140
137
|
| `envgit verify` | Confirm all environments decrypt with the current key |
|
|
141
138
|
|
|
142
139
|
### Variables
|
|
143
140
|
|
|
141
|
+
All variable commands are **interactive** — run them without arguments to get a fuzzy search picker.
|
|
142
|
+
|
|
144
143
|
| Command | Description |
|
|
145
144
|
|---------|-------------|
|
|
146
145
|
| `envgit set KEY=VALUE ...` | Set one or more variables |
|
|
@@ -149,6 +148,7 @@ Supports 100+ services out of the box: OpenAI, Anthropic, Groq, Stripe, Supabase
|
|
|
149
148
|
| `envgit get KEY` | Print a single value |
|
|
150
149
|
| `envgit delete KEY` | Remove a variable |
|
|
151
150
|
| `envgit rename OLD NEW` | Rename a variable |
|
|
151
|
+
| `envgit copy KEY --from dev --to prod` | Copy a variable between environments |
|
|
152
152
|
| `envgit list` | List all keys in the active environment |
|
|
153
153
|
| `envgit list --show-values` | List keys and their values |
|
|
154
154
|
|
|
@@ -157,9 +157,9 @@ Supports 100+ services out of the box: OpenAI, Anthropic, Groq, Stripe, Supabase
|
|
|
157
157
|
| Command | Description |
|
|
158
158
|
|---------|-------------|
|
|
159
159
|
| `envgit envs` | List all environments with variable counts |
|
|
160
|
+
| `envgit use <env>` | Switch active environment (like `git checkout`) |
|
|
160
161
|
| `envgit add-env <name>` | Create a new environment |
|
|
161
|
-
| `envgit unpack
|
|
162
|
-
| `envgit copy KEY --from dev --to prod` | Copy a variable between environments |
|
|
162
|
+
| `envgit unpack [env]` | Decrypt and write a formatted `.env` file |
|
|
163
163
|
| `envgit diff dev prod` | Show what's different between two environments |
|
|
164
164
|
| `envgit diff dev prod --show-values` | Include values in the diff |
|
|
165
165
|
|
|
@@ -178,16 +178,12 @@ Supports 100+ services out of the box: OpenAI, Anthropic, Groq, Stripe, Supabase
|
|
|
178
178
|
|
|
179
179
|
| Command | Description |
|
|
180
180
|
|---------|-------------|
|
|
181
|
+
| `envgit status` | Show project root, active env, key location, `.env` state |
|
|
181
182
|
| `envgit doctor` | Check everything — key, envs, git safety — in one shot |
|
|
182
183
|
| `envgit audit` | Show which keys are missing across environments |
|
|
184
|
+
| `envgit scan` | Scan codebase for hardcoded secrets using patterns and entropy analysis |
|
|
183
185
|
| `envgit template` | Generate a `.env.example` with all keys, no values |
|
|
184
|
-
| `envgit
|
|
185
|
-
|
|
186
|
-
### Status
|
|
187
|
-
|
|
188
|
-
| Command | Description |
|
|
189
|
-
|---------|-------------|
|
|
190
|
-
| `envgit status` | Show project root, active env, key location, `.env` state |
|
|
186
|
+
| `envgit fix` | Post-upgrade repair: migrate config, fix permissions, update `.gitignore` |
|
|
191
187
|
|
|
192
188
|
---
|
|
193
189
|
|
|
@@ -235,3 +231,66 @@ ENVGIT_KEY=$(cat ~/.config/envgit/keys/<id>.key) envgit run -- node server.js
|
|
|
235
231
|
- **Relay is blind** — `envgit share` encrypts your key with a one-time passphrase before upload. The relay stores only ciphertext and never sees the passphrase. Even a full relay compromise leaks nothing.
|
|
236
232
|
- **One-time links** — tokens are deleted on first use via a strongly consistent Durable Object. Replay attacks are impossible.
|
|
237
233
|
- **24-hour TTL** — unclaimed tokens are automatically destroyed
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Threat model
|
|
238
|
+
|
|
239
|
+
**What envgit protects against**
|
|
240
|
+
|
|
241
|
+
| Threat | Protection |
|
|
242
|
+
|--------|-----------|
|
|
243
|
+
| `.env` accidentally committed to git | Encrypted files are safe to commit — plaintext never touches the repo |
|
|
244
|
+
| Secrets shared over Slack, email, chat | `envgit share` uses a blind relay and one-time encrypted links |
|
|
245
|
+
| Teammate's machine is compromised | Key is per-machine — compromise one machine, not all |
|
|
246
|
+
| Relay is breached | Relay stores only AES-256-GCM ciphertext. Passphrase never sent to relay. Useless without the passphrase. |
|
|
247
|
+
| Secrets hardcoded in source | `envgit scan` detects these using pattern matching and entropy analysis |
|
|
248
|
+
|
|
249
|
+
**What envgit does NOT protect against**
|
|
250
|
+
|
|
251
|
+
- A compromised machine where the key file is readable — if your machine is owned, the key is accessible
|
|
252
|
+
- Secrets that have already been committed in plaintext to git history — use `git filter-repo` to scrub those
|
|
253
|
+
- An attacker who intercepts both the relay token AND the passphrase at the same time — treat the passphrase like a password
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## How it works
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
Your machine Relay Teammate's machine
|
|
261
|
+
──────────────── ────────────────── ──────────────────────
|
|
262
|
+
(Cloudflare Worker)
|
|
263
|
+
project.key (32 bytes)
|
|
264
|
+
│
|
|
265
|
+
▼
|
|
266
|
+
encrypt with ┌──────────────────┐
|
|
267
|
+
one-time passphrase ──────► │ ciphertext only │ ──────► fetch + decrypt
|
|
268
|
+
│ deleted on read │ with passphrase
|
|
269
|
+
│ │ TTL: 24 hours │ │
|
|
270
|
+
▼ └──────────────────┘ ▼
|
|
271
|
+
envgit share envgit join <token>
|
|
272
|
+
prints: --code <passphrase>
|
|
273
|
+
token + passphrase │
|
|
274
|
+
▼
|
|
275
|
+
key saved to
|
|
276
|
+
~/.config/envgit/keys/
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The relay is stateless and blind. It cannot reconstruct the key. Only the machine with the passphrase can decrypt.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Crypto decisions
|
|
284
|
+
|
|
285
|
+
**Why AES-256-GCM?**
|
|
286
|
+
GCM is an authenticated encryption mode — it detects tampering. If anyone modifies the ciphertext (even one byte), decryption fails loudly rather than silently returning corrupt data. This matters for secrets: you want to know immediately if something has been tampered with.
|
|
287
|
+
|
|
288
|
+
**Why 32 bytes of entropy?**
|
|
289
|
+
NIST recommends 128-bit minimum for symmetric keys. envgit uses 256-bit (32 bytes) from `crypto.randomBytes`, which reads from the OS CSPRNG. This is the same source used by OpenSSL and Node's TLS stack.
|
|
290
|
+
|
|
291
|
+
**Why a fresh IV per write?**
|
|
292
|
+
AES-GCM requires a unique IV per (key, message) pair. Reusing an IV with the same key catastrophically breaks confidentiality — an attacker can XOR two ciphertexts to cancel out the keystream. envgit generates a new random IV on every write, making this impossible.
|
|
293
|
+
|
|
294
|
+
**Why zeroize key bytes after use?**
|
|
295
|
+
Node.js buffers live in V8's heap. Without explicit zeroization, key bytes can persist in memory until GC runs — and could potentially be read from a core dump or swap file. envgit calls `buffer.fill(0)` immediately after the crypto operation completes.
|
|
296
|
+
|
package/bin/envgit.js
CHANGED
|
@@ -1,35 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'module';
|
|
2
3
|
import { program } from 'commander';
|
|
3
|
-
import { init }
|
|
4
|
-
import { set }
|
|
5
|
-
import { get }
|
|
6
|
-
import { unpack }
|
|
7
|
-
import { list }
|
|
4
|
+
import { init } from '../src/commands/init.js';
|
|
5
|
+
import { set } from '../src/commands/set.js';
|
|
6
|
+
import { get } from '../src/commands/get.js';
|
|
7
|
+
import { unpack } from '../src/commands/unpack.js';
|
|
8
|
+
import { list } from '../src/commands/list.js';
|
|
8
9
|
import { importEnv } from '../src/commands/import.js';
|
|
9
|
-
import { addEnv }
|
|
10
|
-
import { status }
|
|
11
|
-
import { keygen } from '../src/commands/keygen.js';
|
|
10
|
+
import { addEnv } from '../src/commands/add-env.js';
|
|
11
|
+
import { status } from '../src/commands/status.js';
|
|
12
12
|
import { deleteKey } from '../src/commands/delete.js';
|
|
13
|
-
import { copy }
|
|
13
|
+
import { copy } from '../src/commands/copy.js';
|
|
14
14
|
import { renameKey } from '../src/commands/rename-key.js';
|
|
15
|
-
import { diff }
|
|
16
|
-
import { run }
|
|
17
|
-
import { envs }
|
|
15
|
+
import { diff } from '../src/commands/diff.js';
|
|
16
|
+
import { run } from '../src/commands/run.js';
|
|
17
|
+
import { envs } from '../src/commands/envs.js';
|
|
18
18
|
import { exportEnv } from '../src/commands/export.js';
|
|
19
|
-
import { verify }
|
|
19
|
+
import { verify } from '../src/commands/verify.js';
|
|
20
20
|
import { rotateKey } from '../src/commands/rotate-key.js';
|
|
21
|
-
import { share }
|
|
22
|
-
import { join }
|
|
23
|
-
import { doctor }
|
|
24
|
-
import { audit }
|
|
25
|
-
import { template }
|
|
21
|
+
import { share } from '../src/commands/share.js';
|
|
22
|
+
import { join } from '../src/commands/join.js';
|
|
23
|
+
import { doctor } from '../src/commands/doctor.js';
|
|
24
|
+
import { audit } from '../src/commands/audit.js';
|
|
25
|
+
import { template } from '../src/commands/template.js';
|
|
26
|
+
import { scan } from '../src/commands/scan.js';
|
|
27
|
+
import { use } from '../src/commands/use.js';
|
|
28
|
+
import { fix } from '../src/commands/fix.js';
|
|
29
|
+
|
|
30
|
+
const { version } = createRequire(import.meta.url)('../package.json');
|
|
26
31
|
|
|
27
32
|
program
|
|
28
33
|
.name('envgit')
|
|
29
34
|
.description('Encrypted per-project environment variable manager')
|
|
30
|
-
.version(
|
|
35
|
+
.version(version)
|
|
31
36
|
.enablePositionalOptions();
|
|
32
37
|
|
|
38
|
+
// ── Setup ────────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
33
40
|
program
|
|
34
41
|
.command('init')
|
|
35
42
|
.description('Initialize envgit in the current project')
|
|
@@ -37,33 +44,77 @@ program
|
|
|
37
44
|
.action(init);
|
|
38
45
|
|
|
39
46
|
program
|
|
40
|
-
.command('
|
|
41
|
-
.description('
|
|
42
|
-
.action(
|
|
47
|
+
.command('fix')
|
|
48
|
+
.description('Fix everything after an upgrade — .gitignore, permissions, config migration')
|
|
49
|
+
.action(fix);
|
|
50
|
+
|
|
51
|
+
// ── Environments ──────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
program
|
|
54
|
+
.command('use [env]')
|
|
55
|
+
.description('Switch active environment — omit to pick interactively')
|
|
56
|
+
.action(use);
|
|
57
|
+
|
|
58
|
+
program
|
|
59
|
+
.command('envs')
|
|
60
|
+
.description('List all environments with variable counts')
|
|
61
|
+
.action(envs);
|
|
62
|
+
|
|
63
|
+
program
|
|
64
|
+
.command('add-env <name>')
|
|
65
|
+
.alias('new')
|
|
66
|
+
.description('Create a new environment')
|
|
67
|
+
.action(addEnv);
|
|
68
|
+
|
|
69
|
+
program
|
|
70
|
+
.command('unpack [env]')
|
|
71
|
+
.alias('pull')
|
|
72
|
+
.description('Write .env for the active env — or specify one explicitly')
|
|
73
|
+
.action(unpack);
|
|
74
|
+
|
|
75
|
+
program
|
|
76
|
+
.command('diff [env1] [env2]')
|
|
77
|
+
.description('Show differences between two environments')
|
|
78
|
+
.option('--show-values', 'reveal values in diff output')
|
|
79
|
+
.action(diff);
|
|
80
|
+
|
|
81
|
+
// ── Variables ─────────────────────────────────────────────────────────────────
|
|
43
82
|
|
|
44
83
|
program
|
|
45
84
|
.command('set [assignments...]')
|
|
46
|
-
.description('Set KEY=VALUE
|
|
85
|
+
.description('Set KEY=VALUE — omit args to pick interactively')
|
|
47
86
|
.option('--env <name>', 'target environment')
|
|
48
|
-
.option('-f, --file <path>', '
|
|
87
|
+
.option('-f, --file <path>', 'import from a .env file')
|
|
49
88
|
.action(set);
|
|
50
89
|
|
|
51
90
|
program
|
|
52
|
-
.command('get
|
|
53
|
-
.description('Print a value
|
|
91
|
+
.command('get [key]')
|
|
92
|
+
.description('Print a value — omit key to pick interactively')
|
|
54
93
|
.option('--env <name>', 'target environment')
|
|
55
94
|
.action(get);
|
|
56
95
|
|
|
57
96
|
program
|
|
58
|
-
.command('
|
|
59
|
-
.
|
|
60
|
-
.
|
|
61
|
-
.
|
|
62
|
-
|
|
97
|
+
.command('delete [key]')
|
|
98
|
+
.description('Remove a key — omit to pick interactively')
|
|
99
|
+
.option('--env <name>', 'target environment')
|
|
100
|
+
.action(deleteKey);
|
|
101
|
+
|
|
102
|
+
program
|
|
103
|
+
.command('rename [old-key] [new-key]')
|
|
104
|
+
.description('Rename a key — omit args to pick interactively')
|
|
105
|
+
.option('--env <name>', 'target environment')
|
|
106
|
+
.action(renameKey);
|
|
107
|
+
|
|
108
|
+
program
|
|
109
|
+
.command('copy [key]')
|
|
110
|
+
.description('Copy a key between environments — omit args to pick interactively')
|
|
111
|
+
.option('--from <env>', 'source environment')
|
|
112
|
+
.option('--to <env>', 'destination environment')
|
|
113
|
+
.action(copy);
|
|
63
114
|
|
|
64
115
|
program
|
|
65
116
|
.command('list')
|
|
66
|
-
.description('List keys in
|
|
117
|
+
.description('List all keys in the active environment')
|
|
67
118
|
.option('--env <name>', 'target environment')
|
|
68
119
|
.option('--show-values', 'print values alongside keys')
|
|
69
120
|
.action(list);
|
|
@@ -72,75 +123,50 @@ program
|
|
|
72
123
|
.command('import')
|
|
73
124
|
.description('Encrypt an existing .env file into an environment')
|
|
74
125
|
.option('--env <name>', 'target environment')
|
|
75
|
-
.option('--file <path>', 'source file
|
|
126
|
+
.option('--file <path>', 'source file', '.env')
|
|
76
127
|
.action(importEnv);
|
|
77
128
|
|
|
78
|
-
|
|
79
|
-
.command('add-env <name>')
|
|
80
|
-
.description('Add a new environment')
|
|
81
|
-
.action(addEnv);
|
|
129
|
+
// ── Key management ───────────────────────────────────────────────────────────
|
|
82
130
|
|
|
83
131
|
program
|
|
84
|
-
.command('
|
|
85
|
-
.description('
|
|
86
|
-
.
|
|
87
|
-
.option('--set <key>', 'save a received key to .envgit.key')
|
|
88
|
-
.action(keygen);
|
|
132
|
+
.command('share')
|
|
133
|
+
.description('Upload encrypted key as a one-time link to send to a teammate')
|
|
134
|
+
.action(share);
|
|
89
135
|
|
|
90
136
|
program
|
|
91
|
-
.command('
|
|
92
|
-
.description('
|
|
93
|
-
.
|
|
94
|
-
.action(
|
|
137
|
+
.command('join <token>')
|
|
138
|
+
.description('Download and save a key shared via envgit share')
|
|
139
|
+
.requiredOption('--code <passphrase>', 'passphrase from the share output')
|
|
140
|
+
.action(join);
|
|
95
141
|
|
|
96
142
|
program
|
|
97
|
-
.command('
|
|
98
|
-
.description('
|
|
99
|
-
.
|
|
100
|
-
.requiredOption('--to <env>', 'destination environment')
|
|
101
|
-
.action(copy);
|
|
143
|
+
.command('rotate-key')
|
|
144
|
+
.description('Generate a new key and re-encrypt all environments')
|
|
145
|
+
.action(rotateKey);
|
|
102
146
|
|
|
103
|
-
|
|
104
|
-
.command('rename <old-key> <new-key>')
|
|
105
|
-
.description('Rename a key within an environment')
|
|
106
|
-
.option('--env <name>', 'target environment')
|
|
107
|
-
.action(renameKey);
|
|
147
|
+
// ── Export & run ─────────────────────────────────────────────────────────────
|
|
108
148
|
|
|
109
149
|
program
|
|
110
|
-
.command('
|
|
111
|
-
.description('
|
|
112
|
-
.option('--
|
|
113
|
-
.
|
|
150
|
+
.command('export')
|
|
151
|
+
.description('Print decrypted vars to stdout')
|
|
152
|
+
.option('--env <name>', 'target environment')
|
|
153
|
+
.option('--format <fmt>', 'dotenv | json | shell', 'dotenv')
|
|
154
|
+
.action(exportEnv);
|
|
114
155
|
|
|
115
156
|
program
|
|
116
157
|
.command('run [args...]')
|
|
117
|
-
.description('
|
|
158
|
+
.description('Run a command with decrypted env vars injected — nothing written to disk')
|
|
118
159
|
.option('--env <name>', 'environment to use')
|
|
119
160
|
.allowUnknownOption()
|
|
120
161
|
.passThroughOptions()
|
|
121
162
|
.action(run);
|
|
122
163
|
|
|
123
|
-
|
|
124
|
-
.command('envs')
|
|
125
|
-
.description('List all environments with variable counts')
|
|
126
|
-
.action(envs);
|
|
127
|
-
|
|
128
|
-
program
|
|
129
|
-
.command('export')
|
|
130
|
-
.description('Print decrypted vars to stdout (dotenv, json, or shell format)')
|
|
131
|
-
.option('--env <name>', 'target environment')
|
|
132
|
-
.option('--format <fmt>', 'output format: dotenv, json, shell', 'dotenv')
|
|
133
|
-
.action(exportEnv);
|
|
134
|
-
|
|
135
|
-
program
|
|
136
|
-
.command('verify')
|
|
137
|
-
.description('Attempt to decrypt every .enc file with the current key')
|
|
138
|
-
.action(verify);
|
|
164
|
+
// ── Health & safety ──────────────────────────────────────────────────────────
|
|
139
165
|
|
|
140
166
|
program
|
|
141
|
-
.command('
|
|
142
|
-
.description('
|
|
143
|
-
.action(
|
|
167
|
+
.command('status')
|
|
168
|
+
.description('Show active environment, key status, and project info')
|
|
169
|
+
.action(status);
|
|
144
170
|
|
|
145
171
|
program
|
|
146
172
|
.command('doctor')
|
|
@@ -153,21 +179,20 @@ program
|
|
|
153
179
|
.action(audit);
|
|
154
180
|
|
|
155
181
|
program
|
|
156
|
-
.command('
|
|
157
|
-
.description('
|
|
158
|
-
.
|
|
159
|
-
.option('-f, --force', 'overwrite if file already exists')
|
|
160
|
-
.action(template);
|
|
182
|
+
.command('verify')
|
|
183
|
+
.description('Confirm all environments decrypt correctly with the current key')
|
|
184
|
+
.action(verify);
|
|
161
185
|
|
|
162
186
|
program
|
|
163
|
-
.command('
|
|
164
|
-
.description('
|
|
165
|
-
.action(
|
|
187
|
+
.command('scan')
|
|
188
|
+
.description('Scan codebase for hardcoded secrets')
|
|
189
|
+
.action(scan);
|
|
166
190
|
|
|
167
191
|
program
|
|
168
|
-
.command('
|
|
169
|
-
.description('
|
|
170
|
-
.
|
|
171
|
-
.
|
|
192
|
+
.command('template')
|
|
193
|
+
.description('Generate a .env.example with all keys, no values')
|
|
194
|
+
.option('-o, --output <path>', 'output path', '.env.example')
|
|
195
|
+
.option('-f, --force', 'overwrite existing file')
|
|
196
|
+
.action(template);
|
|
172
197
|
|
|
173
198
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akshxy/envgit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Encrypted per-project environment variable manager",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"dependencies": {
|
|
16
|
+
"@inquirer/prompts": "^8.3.0",
|
|
16
17
|
"chalk": "^5.6.2",
|
|
17
18
|
"commander": "^12.0.0",
|
|
18
19
|
"js-yaml": "^4.1.0"
|
package/src/commands/add-env.js
CHANGED
|
@@ -8,6 +8,10 @@ export async function addEnv(name) {
|
|
|
8
8
|
const key = loadKey(projectRoot);
|
|
9
9
|
const config = loadConfig(projectRoot);
|
|
10
10
|
|
|
11
|
+
if (!/^[a-z0-9_-]+$/i.test(name)) {
|
|
12
|
+
fatal(`Invalid environment name '${name}' — use only letters, numbers, hyphens, and underscores.`);
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
if (config.envs.includes(name)) {
|
|
12
16
|
fatal(`Environment '${name}' already exists.`);
|
|
13
17
|
}
|
package/src/commands/audit.js
CHANGED
|
@@ -10,7 +10,10 @@ export async function audit() {
|
|
|
10
10
|
const config = loadConfig(projectRoot);
|
|
11
11
|
|
|
12
12
|
if (config.envs.length < 2) {
|
|
13
|
-
console.log(
|
|
13
|
+
console.log('');
|
|
14
|
+
console.log(dim(' Only one environment — nothing to compare.'));
|
|
15
|
+
console.log(dim(' Add more with: envgit add-env <name>'));
|
|
16
|
+
console.log('');
|
|
14
17
|
return;
|
|
15
18
|
}
|
|
16
19
|
|
package/src/commands/copy.js
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
import { requireProjectRoot, loadKey } from '../keystore.js';
|
|
2
|
+
import { loadConfig } from '../config.js';
|
|
2
3
|
import { readEncEnv, writeEncEnv } from '../enc.js';
|
|
3
|
-
import { ok, fatal, label } from '../ui.js';
|
|
4
|
+
import { ok, fatal, label, envLabel } from '../ui.js';
|
|
5
|
+
import { pickKey, pickEnv } from '../interactive.js';
|
|
4
6
|
|
|
5
7
|
export async function copy(keyName, options) {
|
|
6
|
-
if (!options.from || !options.to) {
|
|
7
|
-
fatal('Both --from and --to environments are required.');
|
|
8
|
-
}
|
|
9
|
-
|
|
10
8
|
const projectRoot = requireProjectRoot();
|
|
11
9
|
const key = loadKey(projectRoot);
|
|
10
|
+
const config = loadConfig(projectRoot);
|
|
11
|
+
|
|
12
|
+
const from = options.from ?? await pickEnv(config.envs, 'Copy from environment');
|
|
13
|
+
const to = options.to ?? await pickEnv(config.envs.filter(e => e !== from), 'Copy to environment');
|
|
14
|
+
|
|
15
|
+
const srcVars = readEncEnv(projectRoot, from, key);
|
|
12
16
|
|
|
13
|
-
const
|
|
17
|
+
const name = keyName ?? await pickKey(srcVars, `Key to copy from [${from}]`);
|
|
14
18
|
|
|
15
|
-
if (!(
|
|
16
|
-
fatal(`Key '${keyName}' not found in ${label(options.from)}`);
|
|
17
|
-
}
|
|
19
|
+
if (!(name in srcVars)) fatal(`Key '${name}' not found in ${envLabel(from)}`);
|
|
18
20
|
|
|
19
|
-
const dstVars = readEncEnv(projectRoot,
|
|
20
|
-
dstVars[
|
|
21
|
-
writeEncEnv(projectRoot,
|
|
21
|
+
const dstVars = readEncEnv(projectRoot, to, key);
|
|
22
|
+
dstVars[name] = srcVars[name];
|
|
23
|
+
writeEncEnv(projectRoot, to, key, dstVars);
|
|
22
24
|
|
|
23
|
-
ok(`Copied ${
|
|
25
|
+
ok(`Copied ${name} from ${envLabel(from)} → ${envLabel(to)}`);
|
|
24
26
|
}
|
package/src/commands/delete.js
CHANGED
|
@@ -2,7 +2,8 @@ import { requireProjectRoot, loadKey } from '../keystore.js';
|
|
|
2
2
|
import { resolveEnv } from '../config.js';
|
|
3
3
|
import { readEncEnv, writeEncEnv } from '../enc.js';
|
|
4
4
|
import { getCurrentEnv } from '../state.js';
|
|
5
|
-
import { ok, fatal, label } from '../ui.js';
|
|
5
|
+
import { ok, fatal, label, envLabel } from '../ui.js';
|
|
6
|
+
import { pickKey, promptConfirm } from '../interactive.js';
|
|
6
7
|
|
|
7
8
|
export async function deleteKey(keyName, options) {
|
|
8
9
|
const projectRoot = requireProjectRoot();
|
|
@@ -11,11 +12,16 @@ export async function deleteKey(keyName, options) {
|
|
|
11
12
|
|
|
12
13
|
const vars = readEncEnv(projectRoot, envName, key);
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
const name = keyName ?? await pickKey(vars, `Key to delete from [${envName}]`);
|
|
16
|
+
|
|
17
|
+
if (!(name in vars)) fatal(`Key '${name}' not found in ${envLabel(envName)}`);
|
|
18
|
+
|
|
19
|
+
if (!keyName) {
|
|
20
|
+
const confirmed = await promptConfirm(`Delete ${name} from [${envName}]?`);
|
|
21
|
+
if (!confirmed) { process.exit(0); }
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
delete vars[
|
|
24
|
+
delete vars[name];
|
|
19
25
|
writeEncEnv(projectRoot, envName, key, vars);
|
|
20
|
-
ok(`Deleted ${
|
|
26
|
+
ok(`Deleted ${name} from ${envLabel(envName)}`);
|
|
21
27
|
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -5,11 +5,8 @@ import chalk from 'chalk';
|
|
|
5
5
|
import { findProjectRoot, globalKeyPath } from '../keystore.js';
|
|
6
6
|
import { loadConfig } from '../config.js';
|
|
7
7
|
import { readEncEnv } from '../enc.js';
|
|
8
|
-
import { bold, dim } from '../ui.js';
|
|
8
|
+
import { ok as pass, fail, warn, bold, dim } from '../ui.js';
|
|
9
9
|
|
|
10
|
-
function pass(msg) { console.log(chalk.green(` ✓ ${msg}`)); }
|
|
11
|
-
function fail(msg) { console.log(chalk.red(` ✗ ${msg}`)); }
|
|
12
|
-
function warn(msg) { console.log(chalk.yellow(` ⚠ ${msg}`)); }
|
|
13
10
|
function section(title) { console.log(`\n${bold(title)}`); }
|
|
14
11
|
|
|
15
12
|
export async function doctor() {
|
|
@@ -48,7 +45,7 @@ export async function doctor() {
|
|
|
48
45
|
pass(`Key file found ${dim(keyPath)}`);
|
|
49
46
|
key = readFileSync(keyPath, 'utf8').trim();
|
|
50
47
|
} else {
|
|
51
|
-
fail('Key file missing —
|
|
48
|
+
fail('Key file missing — get it from a teammate: envgit join <token> --code <passphrase>');
|
|
52
49
|
issues++;
|
|
53
50
|
}
|
|
54
51
|
} else {
|
|
@@ -57,7 +54,7 @@ export async function doctor() {
|
|
|
57
54
|
warn(`Legacy key file at project root ${dim('(consider migrating)')}`);
|
|
58
55
|
key = readFileSync(legacyPath, 'utf8').trim();
|
|
59
56
|
} else {
|
|
60
|
-
fail('No key found — run: envgit
|
|
57
|
+
fail('No key found — ask a teammate to run: envgit share');
|
|
61
58
|
issues++;
|
|
62
59
|
}
|
|
63
60
|
}
|
package/src/commands/envs.js
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
1
|
import { requireProjectRoot, loadKey } from '../keystore.js';
|
|
3
2
|
import { loadConfig } from '../config.js';
|
|
4
3
|
import { readEncEnv } from '../enc.js';
|
|
5
4
|
import { getCurrentEnv } from '../state.js';
|
|
6
|
-
import { dim } from '../ui.js';
|
|
5
|
+
import { dim, envLabel } from '../ui.js';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
7
|
|
|
8
8
|
export async function envs() {
|
|
9
9
|
const projectRoot = requireProjectRoot();
|
|
10
|
-
const key
|
|
11
|
-
const config
|
|
10
|
+
const key = loadKey(projectRoot);
|
|
11
|
+
const config = loadConfig(projectRoot);
|
|
12
12
|
const current = getCurrentEnv(projectRoot);
|
|
13
13
|
|
|
14
14
|
console.log('');
|
|
15
15
|
for (const envName of config.envs) {
|
|
16
16
|
const isActive = envName === current;
|
|
17
|
-
const bullet
|
|
18
|
-
const vars
|
|
19
|
-
const count
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
17
|
+
const bullet = isActive ? chalk.green('●') : chalk.dim('○');
|
|
18
|
+
const vars = readEncEnv(projectRoot, envName, key);
|
|
19
|
+
const count = Object.keys(vars).length;
|
|
20
|
+
const label = isActive ? envLabel(envName) : chalk.dim(`[${envName}]`);
|
|
21
|
+
const countStr = dim(`${count} var${count !== 1 ? 's' : ''}`);
|
|
22
|
+
const hint = isActive ? chalk.dim(' ← active') : '';
|
|
23
|
+
console.log(` ${bullet} ${label} ${countStr}${hint}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!current) {
|
|
27
|
+
console.log('');
|
|
28
|
+
console.log(dim(' No active environment. Run: envgit use <env>'));
|
|
23
29
|
}
|
|
24
30
|
console.log('');
|
|
25
31
|
}
|