@akshxy/envgit 0.1.0 → 0.3.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 +128 -0
- package/package.json +4 -3
- package/src/commands/add-env.js +3 -3
- package/src/commands/get.js +2 -2
- package/src/commands/import.js +3 -5
- package/src/commands/init.js +15 -8
- package/src/commands/keygen.js +21 -19
- package/src/commands/status.js +14 -6
- package/src/keystore.js +54 -19
- package/src/commands/switch.js +0 -26
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# envgit
|
|
2
|
+
|
|
3
|
+
Encrypted per-project environment variable manager. Store secrets safely alongside your code — encrypted at rest, never in plaintext.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npm install -g @akshxy/envgit
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
`.env` files get committed by accident. `envgit` keeps your secrets encrypted in `.envgit/` so you can safely commit them to git and share them with your team — only people with the key can decrypt them.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# 1. Initialize in your project
|
|
17
|
+
envgit init
|
|
18
|
+
|
|
19
|
+
# 2. Set some variables
|
|
20
|
+
envgit set API_KEY=abc123 DB_PASS=secret
|
|
21
|
+
|
|
22
|
+
# 3. Commit the encrypted store (not the key!)
|
|
23
|
+
git add .envgit/
|
|
24
|
+
git commit -m "add encrypted env"
|
|
25
|
+
|
|
26
|
+
# 4. Share your key with teammates
|
|
27
|
+
envgit keygen --show
|
|
28
|
+
# → teammate runs: envgit keygen --set <key>
|
|
29
|
+
|
|
30
|
+
# 5. Write .env when you need it locally
|
|
31
|
+
envgit unpack dev
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
### Key management
|
|
37
|
+
|
|
38
|
+
| Command | Description |
|
|
39
|
+
|---------|-------------|
|
|
40
|
+
| `envgit init` | Initialize in current project, generates key |
|
|
41
|
+
| `envgit keygen` | Generate a new key and save it |
|
|
42
|
+
| `envgit keygen --show` | Print current key for sharing with teammates |
|
|
43
|
+
| `envgit keygen --set <key>` | Save a key received from a teammate |
|
|
44
|
+
| `envgit rotate-key` | Generate new key, re-encrypt all environments |
|
|
45
|
+
| `envgit verify` | Check all environments decrypt correctly |
|
|
46
|
+
|
|
47
|
+
### Variables
|
|
48
|
+
|
|
49
|
+
| Command | Description |
|
|
50
|
+
|---------|-------------|
|
|
51
|
+
| `envgit set KEY=VALUE ...` | Set one or more variables |
|
|
52
|
+
| `envgit get KEY` | Print a single value |
|
|
53
|
+
| `envgit delete KEY` | Remove a variable |
|
|
54
|
+
| `envgit rename OLD NEW` | Rename a variable |
|
|
55
|
+
| `envgit list` | List all keys in active environment |
|
|
56
|
+
| `envgit list --show-values` | List keys and values |
|
|
57
|
+
|
|
58
|
+
### Environments
|
|
59
|
+
|
|
60
|
+
| Command | Description |
|
|
61
|
+
|---------|-------------|
|
|
62
|
+
| `envgit envs` | List all environments with variable counts |
|
|
63
|
+
| `envgit add-env <name>` | Create a new environment |
|
|
64
|
+
| `envgit unpack <env>` | Decrypt env and write `.env` file |
|
|
65
|
+
| `envgit copy KEY --from dev --to prod` | Copy a variable between environments |
|
|
66
|
+
| `envgit diff dev prod` | Show differences between two environments |
|
|
67
|
+
| `envgit diff dev prod --show-values` | Include values in diff |
|
|
68
|
+
|
|
69
|
+
### Export & run
|
|
70
|
+
|
|
71
|
+
| Command | Description |
|
|
72
|
+
|---------|-------------|
|
|
73
|
+
| `envgit export` | Print as `KEY=VALUE` lines (pipeable) |
|
|
74
|
+
| `envgit export --format json` | Print as JSON |
|
|
75
|
+
| `envgit export --format shell` | Print as `export KEY="VALUE"` (eval-able) |
|
|
76
|
+
| `envgit run -- node server.js` | Run a command with env vars injected |
|
|
77
|
+
| `envgit import --file .env.local` | Encrypt an existing `.env` file |
|
|
78
|
+
|
|
79
|
+
### Status
|
|
80
|
+
|
|
81
|
+
| Command | Description |
|
|
82
|
+
|---------|-------------|
|
|
83
|
+
| `envgit status` | Show project, active env, key source, .env state |
|
|
84
|
+
|
|
85
|
+
## All options support `--env <name>`
|
|
86
|
+
|
|
87
|
+
Most commands default to the active environment. Pass `--env` to target a specific one:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
envgit set API_KEY=prod-key --env prod
|
|
91
|
+
envgit list --env staging
|
|
92
|
+
envgit export --env prod --format json
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Team workflow
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Developer A (project owner)
|
|
99
|
+
envgit init
|
|
100
|
+
envgit set DB_URL=postgres://... API_KEY=...
|
|
101
|
+
git add .envgit/ && git commit -m "encrypted env"
|
|
102
|
+
envgit keygen --show # share this key securely with teammates
|
|
103
|
+
|
|
104
|
+
# Developer B (teammate)
|
|
105
|
+
git clone <repo>
|
|
106
|
+
envgit keygen --set <key-from-teammate>
|
|
107
|
+
envgit unpack dev # writes .env
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Security
|
|
111
|
+
|
|
112
|
+
- **AES-256-GCM** — authenticated encryption, tamper-proof
|
|
113
|
+
- **32-byte random key** from OS cryptographic RNG
|
|
114
|
+
- **Fresh random IV** per encryption — same value encrypts differently each time
|
|
115
|
+
- **Key file locked to `0o600`** — unreadable by other users
|
|
116
|
+
- **Permission check on load** — errors if `.envgit.key` is world-readable
|
|
117
|
+
- **Key bytes zeroized** from memory immediately after use
|
|
118
|
+
|
|
119
|
+
The key (`~/.envgit.key`) is gitignored automatically. Never commit it.
|
|
120
|
+
|
|
121
|
+
## Environment variable
|
|
122
|
+
|
|
123
|
+
Set `ENVGIT_KEY` instead of using a key file — useful in CI:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
export ENVGIT_KEY=$(cat .envgit.key)
|
|
127
|
+
envgit export --format shell | source /dev/stdin
|
|
128
|
+
```
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akshxy/envgit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Encrypted per-project environment variable manager",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"envgit": "
|
|
7
|
+
"envgit": "bin/envgit.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
|
11
|
-
"src"
|
|
11
|
+
"src",
|
|
12
|
+
"README.md"
|
|
12
13
|
],
|
|
13
14
|
"license": "MIT",
|
|
14
15
|
"dependencies": {
|
package/src/commands/add-env.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { requireProjectRoot, loadKey } from '../keystore.js';
|
|
2
2
|
import { loadConfig, saveConfig } from '../config.js';
|
|
3
3
|
import { writeEncEnv } from '../enc.js';
|
|
4
|
+
import { ok, fatal } from '../ui.js';
|
|
4
5
|
|
|
5
6
|
export async function addEnv(name) {
|
|
6
7
|
const projectRoot = requireProjectRoot();
|
|
@@ -8,13 +9,12 @@ export async function addEnv(name) {
|
|
|
8
9
|
const config = loadConfig(projectRoot);
|
|
9
10
|
|
|
10
11
|
if (config.envs.includes(name)) {
|
|
11
|
-
|
|
12
|
-
process.exit(1);
|
|
12
|
+
fatal(`Environment '${name}' already exists.`);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
config.envs.push(name);
|
|
16
16
|
saveConfig(projectRoot, config);
|
|
17
17
|
writeEncEnv(projectRoot, name, key, {});
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
ok(`Added environment '${name}'`);
|
|
20
20
|
}
|
package/src/commands/get.js
CHANGED
|
@@ -2,6 +2,7 @@ import { requireProjectRoot, loadKey } from '../keystore.js';
|
|
|
2
2
|
import { resolveEnv } from '../config.js';
|
|
3
3
|
import { readEncEnv } from '../enc.js';
|
|
4
4
|
import { getCurrentEnv } from '../state.js';
|
|
5
|
+
import { fatal, label } from '../ui.js';
|
|
5
6
|
|
|
6
7
|
export async function get(key, options) {
|
|
7
8
|
const projectRoot = requireProjectRoot();
|
|
@@ -11,8 +12,7 @@ export async function get(key, options) {
|
|
|
11
12
|
const vars = readEncEnv(projectRoot, envName, encKey);
|
|
12
13
|
|
|
13
14
|
if (!(key in vars)) {
|
|
14
|
-
|
|
15
|
-
process.exit(1);
|
|
15
|
+
fatal(`Key '${key}' not found in ${label(envName)}`);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
console.log(vars[key]);
|
package/src/commands/import.js
CHANGED
|
@@ -5,6 +5,7 @@ import { resolveEnv } from '../config.js';
|
|
|
5
5
|
import { writeEncEnv } from '../enc.js';
|
|
6
6
|
import { readEnvFile } from '../envfile.js';
|
|
7
7
|
import { getCurrentEnv } from '../state.js';
|
|
8
|
+
import { ok, fatal, label, dim } from '../ui.js';
|
|
8
9
|
|
|
9
10
|
export async function importEnv(options) {
|
|
10
11
|
const projectRoot = requireProjectRoot();
|
|
@@ -13,15 +14,12 @@ export async function importEnv(options) {
|
|
|
13
14
|
|
|
14
15
|
const filePath = join(projectRoot, options.file);
|
|
15
16
|
if (!existsSync(filePath)) {
|
|
16
|
-
|
|
17
|
-
process.exit(1);
|
|
17
|
+
fatal(`File not found: ${options.file}`);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const vars = readEnvFile(filePath);
|
|
21
21
|
const count = Object.keys(vars).length;
|
|
22
22
|
|
|
23
23
|
writeEncEnv(projectRoot, envName, key, vars);
|
|
24
|
-
|
|
25
|
-
`Imported ${count} variable${count !== 1 ? 's' : ''} from ${options.file} into [${envName}]`
|
|
26
|
-
);
|
|
24
|
+
ok(`Imported ${count} variable${count !== 1 ? 's' : ''} from ${dim(options.file)} into ${label(envName)}`);
|
|
27
25
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, appendFileSync, readFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
3
4
|
import { generateKey } from '../crypto.js';
|
|
4
|
-
import { saveKey } from '../keystore.js';
|
|
5
|
+
import { saveKey, globalKeyPath } from '../keystore.js';
|
|
5
6
|
import { saveConfig, getEnvctlDir } from '../config.js';
|
|
6
7
|
import { writeEncEnv } from '../enc.js';
|
|
7
|
-
import { ok,
|
|
8
|
+
import { ok, fatal, bold, dim, label } from '../ui.js';
|
|
8
9
|
|
|
9
10
|
export async function init(options) {
|
|
10
11
|
const projectRoot = process.cwd();
|
|
@@ -15,18 +16,21 @@ export async function init(options) {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
const defaultEnv = options.env;
|
|
19
|
+
const keyId = randomUUID();
|
|
18
20
|
|
|
19
21
|
mkdirSync(envgitDir, { recursive: true });
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
saveKey(projectRoot, key);
|
|
23
|
-
|
|
23
|
+
// Save config first (saveKey needs key_id from config)
|
|
24
24
|
saveConfig(projectRoot, {
|
|
25
25
|
version: 1,
|
|
26
26
|
default_env: defaultEnv,
|
|
27
27
|
envs: [defaultEnv],
|
|
28
|
+
key_id: keyId,
|
|
28
29
|
});
|
|
29
30
|
|
|
31
|
+
const key = generateKey();
|
|
32
|
+
const keyPath = saveKey(projectRoot, key, keyId);
|
|
33
|
+
|
|
30
34
|
writeEncEnv(projectRoot, defaultEnv, key, {});
|
|
31
35
|
|
|
32
36
|
updateGitignore(projectRoot);
|
|
@@ -35,16 +39,19 @@ export async function init(options) {
|
|
|
35
39
|
console.log(bold('envgit initialized'));
|
|
36
40
|
console.log('');
|
|
37
41
|
ok(`Default environment: ${label(defaultEnv)}`);
|
|
38
|
-
ok(
|
|
42
|
+
ok(`Key stored at ${dim(keyPath)}`);
|
|
39
43
|
console.log('');
|
|
40
|
-
console.log(dim('Keep .envgit.key secret and do not commit it.'));
|
|
41
44
|
console.log(dim('Commit .envgit/ to share encrypted environments with your team.'));
|
|
45
|
+
console.log(dim('Your key never touches the repo — it lives only on your machine.'));
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log(`Share your key with teammates: ${bold('envgit keygen --show')}`);
|
|
48
|
+
console.log(`Teammates save it with: ${bold('envgit keygen --set <key>')}`);
|
|
42
49
|
console.log('');
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
function updateGitignore(projectRoot) {
|
|
46
53
|
const gitignorePath = join(projectRoot, '.gitignore');
|
|
47
|
-
const entries = ['.env'
|
|
54
|
+
const entries = ['.env'];
|
|
48
55
|
|
|
49
56
|
let existing = '';
|
|
50
57
|
if (existsSync(gitignorePath)) {
|
package/src/commands/keygen.js
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import { generateKey } from '../crypto.js';
|
|
2
|
-
import { findProjectRoot, saveKey, loadKey } from '../keystore.js';
|
|
3
|
-
import {
|
|
2
|
+
import { findProjectRoot, saveKey, loadKey, globalKeyPath } from '../keystore.js';
|
|
3
|
+
import { loadConfig } from '../config.js';
|
|
4
|
+
import { ok, warn, fatal, bold, dim } from '../ui.js';
|
|
4
5
|
|
|
5
6
|
export async function keygen(options) {
|
|
6
7
|
const projectRoot = findProjectRoot();
|
|
7
8
|
|
|
8
9
|
if (options.show) {
|
|
9
10
|
if (!projectRoot) {
|
|
10
|
-
|
|
11
|
-
process.exit(1);
|
|
11
|
+
fatal('No envgit project found — cannot show key.');
|
|
12
12
|
}
|
|
13
13
|
let key;
|
|
14
14
|
try {
|
|
15
15
|
key = loadKey(projectRoot);
|
|
16
16
|
} catch (e) {
|
|
17
|
-
|
|
18
|
-
process.exit(1);
|
|
17
|
+
fatal(e.message);
|
|
19
18
|
}
|
|
20
19
|
const hint = key.slice(0, 8);
|
|
21
20
|
console.log('');
|
|
@@ -23,8 +22,8 @@ export async function keygen(options) {
|
|
|
23
22
|
console.log(` ${key}`);
|
|
24
23
|
console.log('');
|
|
25
24
|
console.log(dim(`Hint (first 8 chars): ${hint}`));
|
|
26
|
-
console.log(dim('Share
|
|
27
|
-
console.log(dim('
|
|
25
|
+
console.log(dim('Share via a secure channel (not git, not chat).'));
|
|
26
|
+
console.log(dim('Teammate saves it with: envgit keygen --set <key>'));
|
|
28
27
|
console.log('');
|
|
29
28
|
return;
|
|
30
29
|
}
|
|
@@ -33,16 +32,18 @@ export async function keygen(options) {
|
|
|
33
32
|
const key = options.set;
|
|
34
33
|
const decoded = Buffer.from(key, 'base64');
|
|
35
34
|
if (decoded.length !== 32) {
|
|
36
|
-
|
|
37
|
-
process.exit(1);
|
|
35
|
+
fatal(`Invalid key — must decode to exactly 32 bytes (got ${decoded.length}). Generate one with: envgit keygen`);
|
|
38
36
|
}
|
|
39
37
|
if (!projectRoot) {
|
|
40
|
-
|
|
41
|
-
process.exit(1);
|
|
38
|
+
fatal('No envgit project found. Run envgit init first, or clone a repo that uses envgit.');
|
|
42
39
|
}
|
|
43
|
-
saveKey(projectRoot, key);
|
|
44
|
-
ok(`Key saved
|
|
45
|
-
console.log(dim(`
|
|
40
|
+
const keyPath = saveKey(projectRoot, key);
|
|
41
|
+
ok(`Key saved for this project`);
|
|
42
|
+
console.log(dim(` Stored at: ${keyPath}`));
|
|
43
|
+
console.log(dim(` Hint: ${key.slice(0, 8)}`));
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log(dim('Run `envgit verify` to confirm it works.'));
|
|
46
|
+
console.log('');
|
|
46
47
|
return;
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -51,8 +52,9 @@ export async function keygen(options) {
|
|
|
51
52
|
const hint = key.slice(0, 8);
|
|
52
53
|
|
|
53
54
|
if (projectRoot) {
|
|
54
|
-
saveKey(projectRoot, key);
|
|
55
|
-
ok(
|
|
55
|
+
const keyPath = saveKey(projectRoot, key);
|
|
56
|
+
ok(`New key generated`);
|
|
57
|
+
console.log(dim(` Stored at: ${keyPath}`));
|
|
56
58
|
} else {
|
|
57
59
|
console.log('');
|
|
58
60
|
console.log(bold('Generated key (no project found — not saved):'));
|
|
@@ -63,7 +65,7 @@ export async function keygen(options) {
|
|
|
63
65
|
console.log(` ${key}`);
|
|
64
66
|
console.log('');
|
|
65
67
|
console.log(dim(`Hint (first 8 chars): ${hint}`));
|
|
66
|
-
console.log(dim('Share
|
|
67
|
-
console.log(dim('
|
|
68
|
+
console.log(dim('Share via a secure channel (not git, not chat).'));
|
|
69
|
+
console.log(dim('Teammate saves it with: envgit keygen --set <key>'));
|
|
68
70
|
console.log('');
|
|
69
71
|
}
|
package/src/commands/status.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
import { requireProjectRoot } from '../keystore.js';
|
|
3
|
+
import { requireProjectRoot, globalKeyPath } from '../keystore.js';
|
|
4
4
|
import { loadConfig } from '../config.js';
|
|
5
5
|
import { getCurrentEnv } from '../state.js';
|
|
6
6
|
import chalk from 'chalk';
|
|
@@ -11,11 +11,19 @@ export async function status() {
|
|
|
11
11
|
const config = loadConfig(projectRoot);
|
|
12
12
|
const current = getCurrentEnv(projectRoot);
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
let keySource;
|
|
15
|
+
if (process.env.ENVGIT_KEY) {
|
|
16
|
+
keySource = chalk.green('ENVGIT_KEY') + dim(' (env var)');
|
|
17
|
+
} else if (config.key_id) {
|
|
18
|
+
const keyPath = globalKeyPath(config.key_id);
|
|
19
|
+
keySource = existsSync(keyPath)
|
|
20
|
+
? chalk.green('~/.config/envgit/keys/') + dim(config.key_id.slice(0, 8) + '…')
|
|
21
|
+
: chalk.red('not found on this machine') + dim(' — run: envgit keygen --set <key>');
|
|
22
|
+
} else {
|
|
23
|
+
// Legacy
|
|
24
|
+
const legacyPath = join(projectRoot, '.envgit.key');
|
|
25
|
+
keySource = existsSync(legacyPath) ? dim('.envgit.key (legacy)') : chalk.red('(not found)');
|
|
26
|
+
}
|
|
19
27
|
|
|
20
28
|
const dotenvExists = existsSync(join(projectRoot, '.env'));
|
|
21
29
|
|
package/src/keystore.js
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, chmodSync, existsSync, statSync } from 'fs';
|
|
1
|
+
import { readFileSync, writeFileSync, chmodSync, existsSync, statSync, mkdirSync } from 'fs';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { loadConfig } from './config.js';
|
|
3
5
|
|
|
4
|
-
const KEY_FILE = '.envgit.key';
|
|
5
6
|
const ENV_VAR = 'ENVGIT_KEY';
|
|
6
7
|
const SECURE_MODE = 0o600;
|
|
7
8
|
|
|
9
|
+
function globalKeysDir() {
|
|
10
|
+
return join(homedir(), '.config', 'envgit', 'keys');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function globalKeyPath(keyId) {
|
|
14
|
+
return join(globalKeysDir(), `${keyId}.key`);
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
export function findProjectRoot(startDir = process.cwd()) {
|
|
9
18
|
let dir = startDir;
|
|
10
19
|
while (true) {
|
|
11
20
|
if (existsSync(join(dir, '.envgit'))) return dir;
|
|
12
21
|
const parent = dirname(dir);
|
|
13
|
-
if (parent === dir) return null;
|
|
22
|
+
if (parent === dir) return null;
|
|
14
23
|
dir = parent;
|
|
15
24
|
}
|
|
16
25
|
}
|
|
@@ -26,27 +35,53 @@ export function requireProjectRoot() {
|
|
|
26
35
|
|
|
27
36
|
export function loadKey(projectRoot) {
|
|
28
37
|
if (process.env[ENV_VAR]) return process.env[ENV_VAR];
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
);
|
|
37
|
-
process.exit(1);
|
|
38
|
+
|
|
39
|
+
const config = loadConfig(projectRoot);
|
|
40
|
+
|
|
41
|
+
if (config.key_id) {
|
|
42
|
+
const keyPath = globalKeyPath(config.key_id);
|
|
43
|
+
if (existsSync(keyPath)) {
|
|
44
|
+
enforcePermissions(keyPath);
|
|
45
|
+
return readFileSync(keyPath, 'utf8').trim();
|
|
38
46
|
}
|
|
39
|
-
|
|
47
|
+
// Project found, but this machine doesn't have the key yet
|
|
48
|
+
console.error(
|
|
49
|
+
`No key found for this project. Ask your team for the key, then run:\n envgit keygen --set <key>`
|
|
50
|
+
);
|
|
51
|
+
process.exit(1);
|
|
40
52
|
}
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
|
|
54
|
+
// Legacy fallback: .envgit.key in project root
|
|
55
|
+
const legacyPath = join(projectRoot, '.envgit.key');
|
|
56
|
+
if (existsSync(legacyPath)) {
|
|
57
|
+
enforcePermissions(legacyPath);
|
|
58
|
+
return readFileSync(legacyPath, 'utf8').trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.error(
|
|
62
|
+
`No key found for this project. Ask your team for the key, then run:\n envgit keygen --set <key>`
|
|
43
63
|
);
|
|
64
|
+
process.exit(1);
|
|
44
65
|
}
|
|
45
66
|
|
|
46
|
-
export function saveKey(projectRoot, key) {
|
|
47
|
-
const
|
|
67
|
+
export function saveKey(projectRoot, key, keyId) {
|
|
68
|
+
const dir = globalKeysDir();
|
|
69
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
70
|
+
|
|
71
|
+
const id = keyId ?? loadConfig(projectRoot).key_id;
|
|
72
|
+
const keyPath = globalKeyPath(id);
|
|
48
73
|
writeFileSync(keyPath, key + '\n', { mode: SECURE_MODE });
|
|
49
|
-
// Explicitly tighten permissions in case the file already existed —
|
|
50
|
-
// writeFileSync's mode option only applies when creating a new file.
|
|
51
74
|
chmodSync(keyPath, SECURE_MODE);
|
|
75
|
+
return keyPath;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function enforcePermissions(keyPath) {
|
|
79
|
+
const stat = statSync(keyPath);
|
|
80
|
+
const mode = stat.mode & 0o777;
|
|
81
|
+
if (mode & 0o077) {
|
|
82
|
+
console.error(
|
|
83
|
+
`Error: key file has insecure permissions (${mode.toString(8)}). Run: chmod 600 ${keyPath}`
|
|
84
|
+
);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
52
87
|
}
|
package/src/commands/switch.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { join } from 'path';
|
|
2
|
-
import { existsSync } from 'fs';
|
|
3
|
-
import { requireProjectRoot, loadKey } from '../keystore.js';
|
|
4
|
-
import { getEncPath } from '../config.js';
|
|
5
|
-
import { readEncEnv } from '../enc.js';
|
|
6
|
-
import { writeEnvFile } from '../envfile.js';
|
|
7
|
-
|
|
8
|
-
export async function switchEnv(envName) {
|
|
9
|
-
const projectRoot = requireProjectRoot();
|
|
10
|
-
const key = loadKey(projectRoot);
|
|
11
|
-
|
|
12
|
-
const encPath = getEncPath(projectRoot, envName);
|
|
13
|
-
if (!existsSync(encPath)) {
|
|
14
|
-
console.error(
|
|
15
|
-
`Error: Environment '${envName}' does not exist. Use 'envgit add-env ${envName}' to create it.`
|
|
16
|
-
);
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const vars = readEncEnv(projectRoot, envName, key);
|
|
21
|
-
const dotenvPath = join(projectRoot, '.env');
|
|
22
|
-
writeEnvFile(dotenvPath, vars);
|
|
23
|
-
|
|
24
|
-
const count = Object.keys(vars).length;
|
|
25
|
-
console.log(`Switched to [${envName}] — wrote ${count} variable${count !== 1 ? 's' : ''} to .env`);
|
|
26
|
-
}
|