@akshxy/envgit 0.5.0 → 0.5.1

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
@@ -174,6 +174,15 @@ Supports 100+ services out of the box: OpenAI, Anthropic, Groq, Stripe, Supabase
174
174
  | `envgit run -- node server.js` | Run a command with env vars injected, nothing written to disk |
175
175
  | `envgit import --file .env.local` | Encrypt an existing `.env` file |
176
176
 
177
+ ### Utilities
178
+
179
+ | Command | Description |
180
+ |---------|-------------|
181
+ | `envgit doctor` | Check everything — key, envs, git safety — in one shot |
182
+ | `envgit audit` | Show which keys are missing across environments |
183
+ | `envgit template` | Generate a `.env.example` with all keys, no values |
184
+ | `envgit template --output .env.example --force` | Overwrite existing file |
185
+
177
186
  ### Status
178
187
 
179
188
  | Command | Description |
package/bin/envgit.js CHANGED
@@ -20,6 +20,9 @@ import { verify } from '../src/commands/verify.js';
20
20
  import { rotateKey } from '../src/commands/rotate-key.js';
21
21
  import { share } from '../src/commands/share.js';
22
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';
23
26
 
24
27
  program
25
28
  .name('envgit')
@@ -139,6 +142,23 @@ program
139
142
  .description('Generate a new key and re-encrypt all environments')
140
143
  .action(rotateKey);
141
144
 
145
+ program
146
+ .command('doctor')
147
+ .description('Check project health — key, envs, git safety')
148
+ .action(doctor);
149
+
150
+ program
151
+ .command('audit')
152
+ .description('Show which keys are missing across environments')
153
+ .action(audit);
154
+
155
+ program
156
+ .command('template')
157
+ .description('Generate a .env.example with all keys but no values')
158
+ .option('-o, --output <path>', 'output file path', '.env.example')
159
+ .option('-f, --force', 'overwrite if file already exists')
160
+ .action(template);
161
+
142
162
  program
143
163
  .command('share')
144
164
  .description('Encrypt your key and upload it to a one-time link — send the output to a teammate')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akshxy/envgit",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Encrypted per-project environment variable manager",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,86 @@
1
+ import chalk from 'chalk';
2
+ import { requireProjectRoot, loadKey } from '../keystore.js';
3
+ import { loadConfig } from '../config.js';
4
+ import { readEncEnv } from '../enc.js';
5
+ import { bold, dim, ok } from '../ui.js';
6
+
7
+ export async function audit() {
8
+ const projectRoot = requireProjectRoot();
9
+ const key = loadKey(projectRoot);
10
+ const config = loadConfig(projectRoot);
11
+
12
+ if (config.envs.length < 2) {
13
+ console.log(dim('\n Need at least 2 environments to audit.\n'));
14
+ return;
15
+ }
16
+
17
+ // Load all envs
18
+ const envVars = {};
19
+ for (const envName of config.envs) {
20
+ envVars[envName] = readEncEnv(projectRoot, envName, key);
21
+ }
22
+
23
+ // Collect every key across all envs
24
+ const allKeys = [...new Set(config.envs.flatMap(e => Object.keys(envVars[e])))].sort();
25
+
26
+ if (allKeys.length === 0) {
27
+ console.log(dim('\n No variables found across any environment.\n'));
28
+ return;
29
+ }
30
+
31
+ // Find keys that are missing from at least one env
32
+ const missing = {}; // key → [envs it's missing from]
33
+ const present = {}; // key → [envs it's in]
34
+
35
+ for (const key of allKeys) {
36
+ missing[key] = config.envs.filter(e => !(key in envVars[e]));
37
+ present[key] = config.envs.filter(e => (key in envVars[e]));
38
+ }
39
+
40
+ const problemKeys = allKeys.filter(k => missing[k].length > 0);
41
+ const cleanKeys = allKeys.filter(k => missing[k].length === 0);
42
+
43
+ // ── Header ────────────────────────────────────────────────────────────────
44
+ console.log('');
45
+ console.log(bold('Audit') + dim(` — ${config.envs.join(', ')}`));
46
+ console.log('');
47
+
48
+ // ── Missing keys ──────────────────────────────────────────────────────────
49
+ if (problemKeys.length === 0) {
50
+ ok(`All ${allKeys.length} keys are present in every environment.`);
51
+ console.log('');
52
+ return;
53
+ }
54
+
55
+ // Column header
56
+ const pad = Math.max(...allKeys.map(k => k.length), 4) + 2;
57
+ const envHeaders = config.envs.map(e => e.padEnd(8)).join(' ');
58
+ console.log(dim(' ' + 'KEY'.padEnd(pad) + envHeaders));
59
+ console.log(dim(' ' + '─'.repeat(pad + config.envs.length * 10)));
60
+
61
+ for (const key of problemKeys) {
62
+ const cols = config.envs.map(e => {
63
+ if (key in envVars[e]) return chalk.green(' ✓'.padEnd(10));
64
+ return chalk.red(' ✗'.padEnd(10));
65
+ }).join('');
66
+ console.log(` ${chalk.bold(key.padEnd(pad))}${cols}`);
67
+ }
68
+
69
+ if (cleanKeys.length > 0) {
70
+ console.log(dim(`\n ${cleanKeys.length} key${cleanKeys.length !== 1 ? 's' : ''} present in all envs (hidden)`));
71
+ }
72
+
73
+ console.log('');
74
+ console.log(chalk.yellow(bold(`${problemKeys.length} key${problemKeys.length !== 1 ? 's' : ''} missing from one or more environments.`)));
75
+
76
+ // Per-env fix hints
77
+ console.log('');
78
+ for (const envName of config.envs) {
79
+ const needed = problemKeys.filter(k => !(k in envVars[envName]));
80
+ if (needed.length > 0) {
81
+ console.log(dim(` Fix ${envName}: envgit set ${needed.join('=... ')}=... --env ${envName}`));
82
+ }
83
+ }
84
+
85
+ console.log('');
86
+ }
@@ -0,0 +1,146 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import chalk from 'chalk';
5
+ import { findProjectRoot, globalKeyPath } from '../keystore.js';
6
+ import { loadConfig } from '../config.js';
7
+ import { readEncEnv } from '../enc.js';
8
+ import { bold, dim } from '../ui.js';
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
+ function section(title) { console.log(`\n${bold(title)}`); }
14
+
15
+ export async function doctor() {
16
+ let issues = 0;
17
+
18
+ // ── Project ───────────────────────────────────────────────────────────────
19
+ section('Project');
20
+
21
+ const projectRoot = findProjectRoot();
22
+ if (!projectRoot) {
23
+ fail('No envgit project found — run envgit init first.');
24
+ console.log('');
25
+ process.exit(1);
26
+ }
27
+ pass(`Project root: ${dim(projectRoot)}`);
28
+
29
+ let config;
30
+ try {
31
+ config = loadConfig(projectRoot);
32
+ pass(`Config loaded ${dim(`(${config.envs.length} env${config.envs.length !== 1 ? 's' : ''}: ${config.envs.join(', ')})`)}`)
33
+ } catch (e) {
34
+ fail(`Config unreadable — ${e.message}`);
35
+ issues++;
36
+ }
37
+
38
+ // ── Key ───────────────────────────────────────────────────────────────────
39
+ section('Key');
40
+
41
+ let key = null;
42
+ if (process.env.ENVGIT_KEY) {
43
+ pass('Key loaded from ENVGIT_KEY environment variable');
44
+ key = process.env.ENVGIT_KEY;
45
+ } else if (config?.key_id) {
46
+ const keyPath = globalKeyPath(config.key_id);
47
+ if (existsSync(keyPath)) {
48
+ pass(`Key file found ${dim(keyPath)}`);
49
+ key = readFileSync(keyPath, 'utf8').trim();
50
+ } else {
51
+ fail('Key file missing — run: envgit share / envgit join');
52
+ issues++;
53
+ }
54
+ } else {
55
+ const legacyPath = join(projectRoot, '.envgit.key');
56
+ if (existsSync(legacyPath)) {
57
+ warn(`Legacy key file at project root ${dim('(consider migrating)')}`);
58
+ key = readFileSync(legacyPath, 'utf8').trim();
59
+ } else {
60
+ fail('No key found — run: envgit keygen');
61
+ issues++;
62
+ }
63
+ }
64
+
65
+ if (key) {
66
+ const decoded = Buffer.from(key, 'base64');
67
+ if (decoded.length === 32) {
68
+ pass('Key length valid (256-bit)');
69
+ } else {
70
+ fail(`Key length invalid — got ${decoded.length} bytes, expected 32`);
71
+ issues++;
72
+ }
73
+ }
74
+
75
+ // ── Environments ──────────────────────────────────────────────────────────
76
+ if (config && key) {
77
+ section('Environments');
78
+ for (const envName of config.envs) {
79
+ try {
80
+ const vars = readEncEnv(projectRoot, envName, key);
81
+ const count = Object.keys(vars).length;
82
+ pass(`${envName} decrypts OK ${dim(`(${count} var${count !== 1 ? 's' : ''})`)}`);
83
+ } catch (e) {
84
+ fail(`${envName} failed to decrypt — ${e.message}`);
85
+ issues++;
86
+ }
87
+ }
88
+ }
89
+
90
+ // ── Git safety ────────────────────────────────────────────────────────────
91
+ section('Git safety');
92
+
93
+ const gitignorePath = join(projectRoot, '.gitignore');
94
+ if (existsSync(gitignorePath)) {
95
+ const gitignore = readFileSync(gitignorePath, 'utf8');
96
+ const lines = gitignore.split('\n').map(l => l.trim());
97
+
98
+ const envIgnored = lines.some(l => l === '.env' || l === '.env.*' || l === '*.env');
99
+ if (envIgnored) {
100
+ pass('.env is in .gitignore');
101
+ } else {
102
+ warn('.env is not in .gitignore — add it to prevent accidental commits');
103
+ issues++;
104
+ }
105
+
106
+ if (existsSync(join(projectRoot, '.envgit.key'))) {
107
+ const keyIgnored = lines.some(l => l === '.envgit.key');
108
+ if (!keyIgnored) {
109
+ fail('.envgit.key is not in .gitignore — this would expose your key!');
110
+ issues++;
111
+ }
112
+ }
113
+ } else {
114
+ warn('No .gitignore found');
115
+ issues++;
116
+ }
117
+
118
+ // Check for .env files tracked by git (no shell, no injection)
119
+ try {
120
+ const tracked = execFileSync('git', ['ls-files', '.env', '.env.local', '.env.production'], {
121
+ cwd: projectRoot,
122
+ stdio: ['pipe', 'pipe', 'pipe'],
123
+ }).toString().trim();
124
+
125
+ if (tracked) {
126
+ const files = tracked.split('\n').map(f => ` ${f}`).join('\n');
127
+ fail(`Plaintext .env file tracked by git:\n${files}`);
128
+ fail('Remove with: git rm --cached .env');
129
+ issues++;
130
+ } else {
131
+ pass('No plaintext .env files tracked by git');
132
+ }
133
+ } catch {
134
+ warn('Not a git repo — skipping git tracking check');
135
+ }
136
+
137
+ // ── Summary ───────────────────────────────────────────────────────────────
138
+ console.log('');
139
+ if (issues === 0) {
140
+ console.log(chalk.green(bold('All checks passed.')));
141
+ } else {
142
+ console.log(chalk.red(bold(`${issues} issue${issues !== 1 ? 's' : ''} found.`)));
143
+ process.exit(1);
144
+ }
145
+ console.log('');
146
+ }
@@ -0,0 +1,53 @@
1
+ import { writeFileSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { requireProjectRoot, loadKey } from '../keystore.js';
4
+ import { loadConfig } from '../config.js';
5
+ import { readEncEnv } from '../enc.js';
6
+ import { ok, warn, fatal, bold, dim } from '../ui.js';
7
+
8
+ export async function template(options) {
9
+ const projectRoot = requireProjectRoot();
10
+ const key = loadKey(projectRoot);
11
+ const config = loadConfig(projectRoot);
12
+
13
+ // Merge keys from all envs (union), sorted
14
+ const allKeys = [...new Set(
15
+ config.envs.flatMap(envName => Object.keys(readEncEnv(projectRoot, envName, key)))
16
+ )].sort();
17
+
18
+ if (allKeys.length === 0) {
19
+ warn('No variables found — nothing to template.');
20
+ return;
21
+ }
22
+
23
+ const outPath = join(projectRoot, options.output ?? '.env.example');
24
+
25
+ if (existsSync(outPath) && !options.force) {
26
+ fatal(`${outPath} already exists. Use --force to overwrite.`);
27
+ }
28
+
29
+ const projectName = config.project ?? 'your-project';
30
+ const envList = config.envs.join(', ');
31
+
32
+ const lines = [
33
+ `# .env.example — generated by envgit`,
34
+ `# Project : ${projectName}`,
35
+ `# Envs : ${envList}`,
36
+ `#`,
37
+ `# Do not put real values here. This file is safe to commit.`,
38
+ `# To get the real values: envgit join <token> --code <passphrase>`,
39
+ `# then: envgit unpack dev`,
40
+ ``,
41
+ ...allKeys.map(k => `${k}=`),
42
+ ``,
43
+ ].join('\n');
44
+
45
+ writeFileSync(outPath, lines, 'utf8');
46
+
47
+ console.log('');
48
+ ok(`Generated ${dim(outPath)} with ${allKeys.length} key${allKeys.length !== 1 ? 's' : ''}`);
49
+ console.log(dim(` Envs scanned: ${envList}`));
50
+ console.log('');
51
+ console.log(dim(` Commit it: git add ${options.output ?? '.env.example'} && git commit -m "chore: update .env.example"`));
52
+ console.log('');
53
+ }