@akshxy/envgit 0.4.3 → 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 +27 -8
- package/bin/envgit.js +33 -0
- package/package.json +1 -1
- package/src/commands/audit.js +86 -0
- package/src/commands/doctor.js +146 -0
- package/src/commands/join.js +50 -0
- package/src/commands/share.js +48 -0
- package/src/commands/template.js +53 -0
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ npm install -g @akshxy/envgit
|
|
|
14
14
|
|
|
15
15
|
- Secrets are encrypted with **AES-256-GCM** and live in `.envgit/` — safe to commit
|
|
16
16
|
- The key lives on **your machine only** (`~/.config/envgit/keys/`) — never touches the repo
|
|
17
|
-
-
|
|
17
|
+
- Onboard a teammate in one command: `envgit share` → send them the link → they run `envgit join`
|
|
18
18
|
- `envgit unpack dev` writes a clean, **beautifully formatted `.env`** grouped by service
|
|
19
19
|
|
|
20
20
|
---
|
|
@@ -49,18 +49,23 @@ envgit init
|
|
|
49
49
|
envgit set DB_URL=postgres://... OPENAI_API_KEY=sk-...
|
|
50
50
|
git add .envgit/ && git commit -m "chore: encrypted env"
|
|
51
51
|
|
|
52
|
-
envgit
|
|
53
|
-
#
|
|
52
|
+
envgit share
|
|
53
|
+
# ✓ Key encrypted and uploaded. Link expires in 24 hours, usable once.
|
|
54
|
+
#
|
|
55
|
+
# envgit join abc123... --code Xk9mP2...==
|
|
56
|
+
#
|
|
57
|
+
# Send that one line to your teammate — nothing else needed.
|
|
54
58
|
|
|
55
59
|
# ── Developer B (teammate, after cloning) ─────────────────
|
|
56
|
-
envgit
|
|
57
|
-
# Key
|
|
58
|
-
# No file paths to think about — it just works
|
|
60
|
+
envgit join abc123... --code Xk9mP2...==
|
|
61
|
+
# ✓ Key saved to ~/.config/envgit/keys/<project-id>.key
|
|
59
62
|
|
|
60
63
|
envgit verify # confirm the key works
|
|
61
64
|
envgit unpack dev # writes .env
|
|
62
65
|
```
|
|
63
66
|
|
|
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.
|
|
68
|
+
|
|
64
69
|
---
|
|
65
70
|
|
|
66
71
|
## Formatted `.env` output
|
|
@@ -127,8 +132,10 @@ Supports 100+ services out of the box: OpenAI, Anthropic, Groq, Stripe, Supabase
|
|
|
127
132
|
|---------|-------------|
|
|
128
133
|
| `envgit init` | Initialize project, generate key, save to `~/.config/envgit/keys/` |
|
|
129
134
|
| `envgit keygen` | Generate a new key for the current project |
|
|
130
|
-
| `envgit keygen --show` | Print current key
|
|
131
|
-
| `envgit keygen --set <key>` | Save a
|
|
135
|
+
| `envgit keygen --show` | Print current key |
|
|
136
|
+
| `envgit keygen --set <key>` | Save a key for the current project |
|
|
137
|
+
| `envgit share` | Upload encrypted key to a one-time relay link |
|
|
138
|
+
| `envgit join <token> --code <passphrase>` | Download and save a key shared via `envgit share` |
|
|
132
139
|
| `envgit rotate-key` | Generate new key and re-encrypt all environments |
|
|
133
140
|
| `envgit verify` | Confirm all environments decrypt with the current key |
|
|
134
141
|
|
|
@@ -167,6 +174,15 @@ Supports 100+ services out of the box: OpenAI, Anthropic, Groq, Stripe, Supabase
|
|
|
167
174
|
| `envgit run -- node server.js` | Run a command with env vars injected, nothing written to disk |
|
|
168
175
|
| `envgit import --file .env.local` | Encrypt an existing `.env` file |
|
|
169
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
|
+
|
|
170
186
|
### Status
|
|
171
187
|
|
|
172
188
|
| Command | Description |
|
|
@@ -216,3 +232,6 @@ ENVGIT_KEY=$(cat ~/.config/envgit/keys/<id>.key) envgit run -- node server.js
|
|
|
216
232
|
- **File permissions enforced** — key files are locked to `0600`, errors if too permissive
|
|
217
233
|
- **Key bytes zeroized** from memory immediately after use
|
|
218
234
|
- **No plaintext ever written** except when you explicitly run `envgit unpack`
|
|
235
|
+
- **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
|
+
- **One-time links** — tokens are deleted on first use via a strongly consistent Durable Object. Replay attacks are impossible.
|
|
237
|
+
- **24-hour TTL** — unclaimed tokens are automatically destroyed
|
package/bin/envgit.js
CHANGED
|
@@ -18,6 +18,11 @@ import { envs } from '../src/commands/envs.js';
|
|
|
18
18
|
import { exportEnv } from '../src/commands/export.js';
|
|
19
19
|
import { verify } from '../src/commands/verify.js';
|
|
20
20
|
import { rotateKey } from '../src/commands/rotate-key.js';
|
|
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';
|
|
21
26
|
|
|
22
27
|
program
|
|
23
28
|
.name('envgit')
|
|
@@ -137,4 +142,32 @@ program
|
|
|
137
142
|
.description('Generate a new key and re-encrypt all environments')
|
|
138
143
|
.action(rotateKey);
|
|
139
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
|
+
|
|
162
|
+
program
|
|
163
|
+
.command('share')
|
|
164
|
+
.description('Encrypt your key and upload it to a one-time link — send the output to a teammate')
|
|
165
|
+
.action(share);
|
|
166
|
+
|
|
167
|
+
program
|
|
168
|
+
.command('join <token>')
|
|
169
|
+
.description('Download and save a key from a link generated by envgit share')
|
|
170
|
+
.requiredOption('--code <passphrase>', 'passphrase printed by envgit share')
|
|
171
|
+
.action(join);
|
|
172
|
+
|
|
140
173
|
program.parse();
|
package/package.json
CHANGED
|
@@ -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,50 @@
|
|
|
1
|
+
import { findProjectRoot, saveKey } from '../keystore.js';
|
|
2
|
+
import { decrypt } from '../crypto.js';
|
|
3
|
+
import { ok, fatal, bold, dim } from '../ui.js';
|
|
4
|
+
|
|
5
|
+
const RELAY = process.env.ENVGIT_RELAY ?? 'https://envgit-relay.akku41809.workers.dev';
|
|
6
|
+
|
|
7
|
+
export async function join(token, options) {
|
|
8
|
+
if (!token) fatal('Token required. Usage: envgit join <token> --code <passphrase>');
|
|
9
|
+
if (!options.code) fatal('Passphrase required. Usage: envgit join <token> --code <passphrase>');
|
|
10
|
+
|
|
11
|
+
const projectRoot = findProjectRoot();
|
|
12
|
+
if (!projectRoot) fatal('No envgit project found. Clone the repo and run envgit init first, or just be inside the project directory.');
|
|
13
|
+
|
|
14
|
+
let blob;
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch(`${RELAY}/join/${token}`);
|
|
17
|
+
|
|
18
|
+
if (res.status === 404) fatal('Token not found or already used. Ask your teammate to run envgit share again.');
|
|
19
|
+
if (res.status === 410) fatal('Token has expired. Ask your teammate to run envgit share again.');
|
|
20
|
+
if (!res.ok) fatal(`Relay error: ${res.status}`);
|
|
21
|
+
|
|
22
|
+
({ blob } = await res.json());
|
|
23
|
+
} catch (e) {
|
|
24
|
+
if (e.cause?.code === 'ENOTFOUND') fatal('Cannot reach relay — check your connection.');
|
|
25
|
+
fatal(`Join failed: ${e.message}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let key;
|
|
29
|
+
try {
|
|
30
|
+
key = decrypt(blob, options.code);
|
|
31
|
+
} catch {
|
|
32
|
+
fatal('Decryption failed — wrong --code, or the blob was tampered with.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Validate it looks like a real key before saving
|
|
36
|
+
const decoded = Buffer.from(key, 'base64');
|
|
37
|
+
if (decoded.length !== 32) {
|
|
38
|
+
fatal('Decrypted value is not a valid envgit key. Wrong --code?');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const keyPath = saveKey(projectRoot, key);
|
|
42
|
+
|
|
43
|
+
console.log('');
|
|
44
|
+
ok('Key saved.');
|
|
45
|
+
console.log(dim(` Stored at: ${keyPath}`));
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log(dim('Run `envgit verify` to confirm it works.'));
|
|
48
|
+
console.log(dim('Run `envgit unpack dev` to write your .env file.'));
|
|
49
|
+
console.log('');
|
|
50
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
import { findProjectRoot, loadKey } from '../keystore.js';
|
|
3
|
+
import { encrypt, generateKey } from '../crypto.js';
|
|
4
|
+
import { ok, fatal, bold, dim } from '../ui.js';
|
|
5
|
+
|
|
6
|
+
const RELAY = process.env.ENVGIT_RELAY ?? 'https://envgit-relay.akku41809.workers.dev';
|
|
7
|
+
|
|
8
|
+
export async function share() {
|
|
9
|
+
const projectRoot = findProjectRoot();
|
|
10
|
+
if (!projectRoot) fatal('No envgit project found. Run envgit init first.');
|
|
11
|
+
|
|
12
|
+
let key;
|
|
13
|
+
try { key = loadKey(projectRoot); }
|
|
14
|
+
catch (e) { fatal(e.message); }
|
|
15
|
+
|
|
16
|
+
// Generate a one-time passphrase — used to encrypt the key before it leaves
|
|
17
|
+
// the machine. The relay stores only ciphertext; it never sees this passphrase.
|
|
18
|
+
const passphrase = generateKey(); // 32 bytes of OS randomness, base64-encoded
|
|
19
|
+
const blob = encrypt(key, passphrase);
|
|
20
|
+
|
|
21
|
+
let token;
|
|
22
|
+
try {
|
|
23
|
+
const res = await fetch(`${RELAY}/share`, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: { 'Content-Type': 'application/json' },
|
|
26
|
+
body: JSON.stringify({ blob }),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (res.status === 429) fatal('Rate limit hit — try again in an hour.');
|
|
30
|
+
if (!res.ok) fatal(`Relay error: ${res.status}`);
|
|
31
|
+
|
|
32
|
+
({ token } = await res.json());
|
|
33
|
+
} catch (e) {
|
|
34
|
+
if (e.cause?.code === 'ENOTFOUND') fatal('Cannot reach relay — check your connection.');
|
|
35
|
+
fatal(`Share failed: ${e.message}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log('');
|
|
39
|
+
ok('Key encrypted and uploaded. Link expires in 24 hours, usable once.');
|
|
40
|
+
console.log('');
|
|
41
|
+
console.log(bold('Send this to your teammate:'));
|
|
42
|
+
console.log('');
|
|
43
|
+
console.log(` envgit join ${token} --code ${passphrase}`);
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log(dim('They can run it directly in their terminal after cloning.'));
|
|
46
|
+
console.log(dim('The link is deleted the moment they use it.'));
|
|
47
|
+
console.log('');
|
|
48
|
+
}
|
|
@@ -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
|
+
}
|