@akshxy/envgit 0.2.0 → 0.4.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/bin/envgit.js +3 -2
- package/package.json +1 -1
- package/src/commands/init.js +15 -8
- package/src/commands/keygen.js +21 -19
- package/src/commands/set.js +27 -8
- package/src/commands/status.js +14 -6
- package/src/envfile.js +417 -8
- package/src/keystore.js +54 -19
package/bin/envgit.js
CHANGED
|
@@ -37,9 +37,10 @@ program
|
|
|
37
37
|
.action(status);
|
|
38
38
|
|
|
39
39
|
program
|
|
40
|
-
.command('set
|
|
41
|
-
.description('Set
|
|
40
|
+
.command('set [assignments...]')
|
|
41
|
+
.description('Set KEY=VALUE pairs, or load from a file with -f')
|
|
42
42
|
.option('--env <name>', 'target environment')
|
|
43
|
+
.option('-f, --file <path>', 'read variables from a .env file')
|
|
43
44
|
.action(set);
|
|
44
45
|
|
|
45
46
|
program
|
package/package.json
CHANGED
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/set.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
1
3
|
import { requireProjectRoot, loadKey } from '../keystore.js';
|
|
2
4
|
import { resolveEnv } from '../config.js';
|
|
3
5
|
import { readEncEnv, writeEncEnv } from '../enc.js';
|
|
6
|
+
import { readEnvFile } from '../envfile.js';
|
|
4
7
|
import { getCurrentEnv } from '../state.js';
|
|
5
8
|
import { ok, fatal, label } from '../ui.js';
|
|
6
9
|
|
|
@@ -11,15 +14,31 @@ export async function set(assignments, options) {
|
|
|
11
14
|
|
|
12
15
|
const vars = readEncEnv(projectRoot, envName, key);
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
if (
|
|
17
|
-
fatal(`
|
|
17
|
+
if (options.file) {
|
|
18
|
+
const filePath = join(projectRoot, options.file);
|
|
19
|
+
if (!existsSync(filePath)) {
|
|
20
|
+
fatal(`File not found: ${options.file}`);
|
|
21
|
+
}
|
|
22
|
+
const fileVars = readEnvFile(filePath);
|
|
23
|
+
const entries = Object.entries(fileVars);
|
|
24
|
+
if (entries.length === 0) {
|
|
25
|
+
fatal(`No variables found in ${options.file}`);
|
|
26
|
+
}
|
|
27
|
+
for (const [k, v] of entries) {
|
|
28
|
+
vars[k] = v;
|
|
29
|
+
ok(`Set ${k} in ${label(envName)}`);
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
for (const assignment of assignments) {
|
|
33
|
+
const eqIdx = assignment.indexOf('=');
|
|
34
|
+
if (eqIdx === -1) {
|
|
35
|
+
fatal(`Invalid assignment '${assignment}'. Expected KEY=VALUE format.`);
|
|
36
|
+
}
|
|
37
|
+
const k = assignment.slice(0, eqIdx).trim();
|
|
38
|
+
const v = assignment.slice(eqIdx + 1);
|
|
39
|
+
vars[k] = v;
|
|
40
|
+
ok(`Set ${k} in ${label(envName)}`);
|
|
18
41
|
}
|
|
19
|
-
const k = assignment.slice(0, eqIdx).trim();
|
|
20
|
-
const v = assignment.slice(eqIdx + 1);
|
|
21
|
-
vars[k] = v;
|
|
22
|
-
ok(`Set ${k} in ${label(envName)}`);
|
|
23
42
|
}
|
|
24
43
|
|
|
25
44
|
writeEncEnv(projectRoot, envName, key, vars);
|
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/envfile.js
CHANGED
|
@@ -1,5 +1,338 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
2
2
|
|
|
3
|
+
// ─── Section label mappings (prefix → section name) ──────────────────────────
|
|
4
|
+
const SECTION_LABELS = {
|
|
5
|
+
// ── App / Runtime ──────────────────────────────────────────────────────────
|
|
6
|
+
NODE: 'App / Runtime',
|
|
7
|
+
APP: 'App / Config',
|
|
8
|
+
SERVER: 'App / Server',
|
|
9
|
+
SERVICE: 'App / Service',
|
|
10
|
+
WORKER: 'App / Worker',
|
|
11
|
+
|
|
12
|
+
// ── Frontend Frameworks ────────────────────────────────────────────────────
|
|
13
|
+
NEXT: 'Next.js',
|
|
14
|
+
NEXT_PUBLIC: 'Next.js / Public (client-side)',
|
|
15
|
+
NUXT: 'Nuxt',
|
|
16
|
+
NUXT_PUBLIC: 'Nuxt / Public (client-side)',
|
|
17
|
+
VITE: 'Vite',
|
|
18
|
+
VITE_APP: 'Vite / Public (client-side)',
|
|
19
|
+
REACT: 'React',
|
|
20
|
+
REACT_APP: 'React / Public (client-side)',
|
|
21
|
+
GATSBY: 'Gatsby',
|
|
22
|
+
SVELTE: 'SvelteKit',
|
|
23
|
+
PUBLIC: 'Public (client-side)',
|
|
24
|
+
|
|
25
|
+
// ── Databases ──────────────────────────────────────────────────────────────
|
|
26
|
+
DB: 'Database',
|
|
27
|
+
DATABASE: 'Database',
|
|
28
|
+
POSTGRES: 'Database / PostgreSQL',
|
|
29
|
+
POSTGRESQL: 'Database / PostgreSQL',
|
|
30
|
+
PG: 'Database / PostgreSQL',
|
|
31
|
+
PGHOST: 'Database / PostgreSQL',
|
|
32
|
+
PGPORT: 'Database / PostgreSQL',
|
|
33
|
+
PGUSER: 'Database / PostgreSQL',
|
|
34
|
+
PGPASSWORD: 'Database / PostgreSQL',
|
|
35
|
+
PGDATABASE: 'Database / PostgreSQL',
|
|
36
|
+
MYSQL: 'Database / MySQL',
|
|
37
|
+
MARIADB: 'Database / MariaDB',
|
|
38
|
+
MONGO: 'Database / MongoDB',
|
|
39
|
+
MONGODB: 'Database / MongoDB',
|
|
40
|
+
SQLITE: 'Database / SQLite',
|
|
41
|
+
COCKROACH: 'Database / CockroachDB',
|
|
42
|
+
COCKROACHDB: 'Database / CockroachDB',
|
|
43
|
+
NEON: 'Database / Neon',
|
|
44
|
+
PLANETSCALE: 'Database / PlanetScale',
|
|
45
|
+
TURSO: 'Database / Turso',
|
|
46
|
+
SUPABASE: 'Supabase',
|
|
47
|
+
XATA: 'Database / Xata',
|
|
48
|
+
CONVEX: 'Database / Convex',
|
|
49
|
+
|
|
50
|
+
// ── Cache / Queues ─────────────────────────────────────────────────────────
|
|
51
|
+
REDIS: 'Cache / Redis',
|
|
52
|
+
UPSTASH: 'Cache / Upstash Redis',
|
|
53
|
+
MEMCACHED: 'Cache / Memcached',
|
|
54
|
+
KAFKA: 'Queue / Kafka',
|
|
55
|
+
RABBITMQ: 'Queue / RabbitMQ',
|
|
56
|
+
SQS: 'Queue / AWS SQS',
|
|
57
|
+
BULL: 'Queue / Bull',
|
|
58
|
+
INNGEST: 'Queue / Inngest',
|
|
59
|
+
TRIGGER: 'Queue / Trigger.dev',
|
|
60
|
+
QSTASH: 'Queue / QStash',
|
|
61
|
+
|
|
62
|
+
// ── Auth ───────────────────────────────────────────────────────────────────
|
|
63
|
+
AUTH: 'Auth',
|
|
64
|
+
AUTH0: 'Auth / Auth0',
|
|
65
|
+
CLERK: 'Auth / Clerk',
|
|
66
|
+
NEXTAUTH: 'Auth / NextAuth',
|
|
67
|
+
NEXT_AUTH: 'Auth / NextAuth',
|
|
68
|
+
LUCIA: 'Auth / Lucia',
|
|
69
|
+
BETTER_AUTH: 'Auth / Better Auth',
|
|
70
|
+
SUPABASE_AUTH: 'Auth / Supabase Auth',
|
|
71
|
+
FIREBASE_AUTH: 'Auth / Firebase Auth',
|
|
72
|
+
COGNITO: 'Auth / AWS Cognito',
|
|
73
|
+
OKTA: 'Auth / Okta',
|
|
74
|
+
WORKOS: 'Auth / WorkOS',
|
|
75
|
+
PROPELAUTH: 'Auth / PropelAuth',
|
|
76
|
+
STYTCH: 'Auth / Stytch',
|
|
77
|
+
MAGIC: 'Auth / Magic',
|
|
78
|
+
OAUTH: 'Auth / OAuth',
|
|
79
|
+
JWT: 'Auth / JWT',
|
|
80
|
+
SESSION: 'Auth / Session',
|
|
81
|
+
COOKIE: 'Auth / Cookie',
|
|
82
|
+
CSRF: 'Auth / CSRF',
|
|
83
|
+
TOTP: 'Auth / 2FA',
|
|
84
|
+
MFA: 'Auth / 2FA',
|
|
85
|
+
|
|
86
|
+
// ── AWS ────────────────────────────────────────────────────────────────────
|
|
87
|
+
AWS: 'AWS',
|
|
88
|
+
S3: 'AWS / S3',
|
|
89
|
+
EC2: 'AWS / EC2',
|
|
90
|
+
ECS: 'AWS / ECS',
|
|
91
|
+
EKS: 'AWS / EKS',
|
|
92
|
+
LAMBDA: 'AWS / Lambda',
|
|
93
|
+
CLOUDFRONT: 'AWS / CloudFront',
|
|
94
|
+
CLOUDWATCH: 'AWS / CloudWatch',
|
|
95
|
+
COGNITO: 'AWS / Cognito',
|
|
96
|
+
DYNAMODB: 'AWS / DynamoDB',
|
|
97
|
+
SES: 'AWS / SES',
|
|
98
|
+
SNS: 'AWS / SNS',
|
|
99
|
+
SQS: 'AWS / SQS',
|
|
100
|
+
ROUTE53: 'AWS / Route 53',
|
|
101
|
+
ECR: 'AWS / ECR',
|
|
102
|
+
SECRETS: 'AWS / Secrets Manager',
|
|
103
|
+
|
|
104
|
+
// ── Google Cloud ───────────────────────────────────────────────────────────
|
|
105
|
+
GCP: 'Google Cloud',
|
|
106
|
+
GOOGLE: 'Google',
|
|
107
|
+
GOOGLE_CLOUD: 'Google Cloud',
|
|
108
|
+
FIREBASE: 'Firebase',
|
|
109
|
+
FIRESTORE: 'Firebase / Firestore',
|
|
110
|
+
GCS: 'Google Cloud / Storage',
|
|
111
|
+
BIGQUERY: 'Google Cloud / BigQuery',
|
|
112
|
+
PUBSUB: 'Google Cloud / Pub/Sub',
|
|
113
|
+
GCLOUD: 'Google Cloud',
|
|
114
|
+
|
|
115
|
+
// ── Azure ──────────────────────────────────────────────────────────────────
|
|
116
|
+
AZURE: 'Azure',
|
|
117
|
+
AZURE_AD: 'Azure / Active Directory',
|
|
118
|
+
COSMOS: 'Azure / Cosmos DB',
|
|
119
|
+
COSMOSDB: 'Azure / Cosmos DB',
|
|
120
|
+
BLOB: 'Azure / Blob Storage',
|
|
121
|
+
|
|
122
|
+
// ── AI / LLMs ──────────────────────────────────────────────────────────────
|
|
123
|
+
OPENAI: 'AI / OpenAI',
|
|
124
|
+
ANTHROPIC: 'AI / Anthropic',
|
|
125
|
+
GEMINI: 'AI / Google Gemini',
|
|
126
|
+
COHERE: 'AI / Cohere',
|
|
127
|
+
REPLICATE: 'AI / Replicate',
|
|
128
|
+
HUGGINGFACE: 'AI / HuggingFace',
|
|
129
|
+
HF: 'AI / HuggingFace',
|
|
130
|
+
TOGETHER: 'AI / Together AI',
|
|
131
|
+
GROQ: 'AI / Groq',
|
|
132
|
+
MISTRAL: 'AI / Mistral',
|
|
133
|
+
PERPLEXITY: 'AI / Perplexity',
|
|
134
|
+
FIREWORKS: 'AI / Fireworks AI',
|
|
135
|
+
ANYSCALE: 'AI / Anyscale',
|
|
136
|
+
AI21: 'AI / AI21 Labs',
|
|
137
|
+
STABILITY: 'AI / Stability AI',
|
|
138
|
+
DEEPINFRA: 'AI / DeepInfra',
|
|
139
|
+
ELEVENLABS: 'AI / ElevenLabs',
|
|
140
|
+
ASSEMBLYAI: 'AI / AssemblyAI',
|
|
141
|
+
DEEPGRAM: 'AI / Deepgram',
|
|
142
|
+
|
|
143
|
+
// ── Vector DBs ─────────────────────────────────────────────────────────────
|
|
144
|
+
PINECONE: 'Vector DB / Pinecone',
|
|
145
|
+
WEAVIATE: 'Vector DB / Weaviate',
|
|
146
|
+
QDRANT: 'Vector DB / Qdrant',
|
|
147
|
+
CHROMA: 'Vector DB / Chroma',
|
|
148
|
+
MILVUS: 'Vector DB / Milvus',
|
|
149
|
+
|
|
150
|
+
// ── Payments ───────────────────────────────────────────────────────────────
|
|
151
|
+
STRIPE: 'Payments / Stripe',
|
|
152
|
+
PAYPAL: 'Payments / PayPal',
|
|
153
|
+
BRAINTREE: 'Payments / Braintree',
|
|
154
|
+
SQUARE: 'Payments / Square',
|
|
155
|
+
LEMON: 'Payments / Lemon Squeezy',
|
|
156
|
+
LEMONSQUEEZY: 'Payments / Lemon Squeezy',
|
|
157
|
+
PADDLE: 'Payments / Paddle',
|
|
158
|
+
COINBASE: 'Payments / Coinbase Commerce',
|
|
159
|
+
RAZORPAY: 'Payments / Razorpay',
|
|
160
|
+
|
|
161
|
+
// ── Email ──────────────────────────────────────────────────────────────────
|
|
162
|
+
SMTP: 'Email / SMTP',
|
|
163
|
+
MAIL: 'Email',
|
|
164
|
+
EMAIL: 'Email',
|
|
165
|
+
SENDGRID: 'Email / SendGrid',
|
|
166
|
+
RESEND: 'Email / Resend',
|
|
167
|
+
MAILGUN: 'Email / Mailgun',
|
|
168
|
+
POSTMARK: 'Email / Postmark',
|
|
169
|
+
MAILCHIMP: 'Email / Mailchimp',
|
|
170
|
+
MANDRILL: 'Email / Mandrill',
|
|
171
|
+
SES: 'Email / AWS SES',
|
|
172
|
+
SPARKPOST: 'Email / SparkPost',
|
|
173
|
+
CONVERTKIT: 'Email / ConvertKit',
|
|
174
|
+
LOOPS: 'Email / Loops',
|
|
175
|
+
|
|
176
|
+
// ── SMS / Communications ───────────────────────────────────────────────────
|
|
177
|
+
TWILIO: 'SMS / Twilio',
|
|
178
|
+
VONAGE: 'SMS / Vonage',
|
|
179
|
+
MESSAGEBIRD: 'SMS / MessageBird',
|
|
180
|
+
TELNYX: 'SMS / Telnyx',
|
|
181
|
+
SINCH: 'SMS / Sinch',
|
|
182
|
+
BANDWIDTH: 'SMS / Bandwidth',
|
|
183
|
+
|
|
184
|
+
// ── Search ─────────────────────────────────────────────────────────────────
|
|
185
|
+
ALGOLIA: 'Search / Algolia',
|
|
186
|
+
MEILISEARCH: 'Search / Meilisearch',
|
|
187
|
+
TYPESENSE: 'Search / Typesense',
|
|
188
|
+
ELASTICSEARCH: 'Search / Elasticsearch',
|
|
189
|
+
OPENSEARCH: 'Search / OpenSearch',
|
|
190
|
+
|
|
191
|
+
// ── Storage / CDN ──────────────────────────────────────────────────────────
|
|
192
|
+
CLOUDINARY: 'Storage / Cloudinary',
|
|
193
|
+
IMAGEKIT: 'Storage / ImageKit',
|
|
194
|
+
UPLOADTHING: 'Storage / UploadThing',
|
|
195
|
+
BUNNY: 'Storage / Bunny CDN',
|
|
196
|
+
BACKBLAZE: 'Storage / Backblaze B2',
|
|
197
|
+
R2: 'Storage / Cloudflare R2',
|
|
198
|
+
TIGRIS: 'Storage / Tigris',
|
|
199
|
+
|
|
200
|
+
// ── Observability ──────────────────────────────────────────────────────────
|
|
201
|
+
SENTRY: 'Observability / Sentry',
|
|
202
|
+
DATADOG: 'Observability / Datadog',
|
|
203
|
+
NEWRELIC: 'Observability / New Relic',
|
|
204
|
+
NEW_RELIC: 'Observability / New Relic',
|
|
205
|
+
GRAFANA: 'Observability / Grafana',
|
|
206
|
+
PROMETHEUS: 'Observability / Prometheus',
|
|
207
|
+
LOGTAIL: 'Observability / Logtail',
|
|
208
|
+
AXIOM: 'Observability / Axiom',
|
|
209
|
+
BETTERSTACK: 'Observability / Better Stack',
|
|
210
|
+
LOGFLARE: 'Observability / Logflare',
|
|
211
|
+
HIGHLIGHT: 'Observability / Highlight',
|
|
212
|
+
BASELIME: 'Observability / Baselime',
|
|
213
|
+
|
|
214
|
+
// ── Analytics ──────────────────────────────────────────────────────────────
|
|
215
|
+
POSTHOG: 'Analytics / PostHog',
|
|
216
|
+
SEGMENT: 'Analytics / Segment',
|
|
217
|
+
MIXPANEL: 'Analytics / Mixpanel',
|
|
218
|
+
AMPLITUDE: 'Analytics / Amplitude',
|
|
219
|
+
HEAP: 'Analytics / Heap',
|
|
220
|
+
HOTJAR: 'Analytics / Hotjar',
|
|
221
|
+
GA: 'Analytics / Google Analytics',
|
|
222
|
+
GOOGLE_ANALYTICS: 'Analytics / Google Analytics',
|
|
223
|
+
PLAUSIBLE: 'Analytics / Plausible',
|
|
224
|
+
FATHOM: 'Analytics / Fathom',
|
|
225
|
+
PIRSCH: 'Analytics / Pirsch',
|
|
226
|
+
UMAMI: 'Analytics / Umami',
|
|
227
|
+
JUNE: 'Analytics / June',
|
|
228
|
+
OPENPANEL: 'Analytics / OpenPanel',
|
|
229
|
+
|
|
230
|
+
// ── Feature Flags ──────────────────────────────────────────────────────────
|
|
231
|
+
LAUNCHDARKLY: 'Feature Flags / LaunchDarkly',
|
|
232
|
+
GROWTHBOOK: 'Feature Flags / GrowthBook',
|
|
233
|
+
FLAGSMITH: 'Feature Flags / Flagsmith',
|
|
234
|
+
STATSIG: 'Feature Flags / Statsig',
|
|
235
|
+
UNLEASH: 'Feature Flags / Unleash',
|
|
236
|
+
HYPERTUNE: 'Feature Flags / Hypertune',
|
|
237
|
+
|
|
238
|
+
// ── CMS ────────────────────────────────────────────────────────────────────
|
|
239
|
+
CONTENTFUL: 'CMS / Contentful',
|
|
240
|
+
SANITY: 'CMS / Sanity',
|
|
241
|
+
STRAPI: 'CMS / Strapi',
|
|
242
|
+
DIRECTUS: 'CMS / Directus',
|
|
243
|
+
PAYLOAD: 'CMS / Payload',
|
|
244
|
+
PRISMIC: 'CMS / Prismic',
|
|
245
|
+
GHOST: 'CMS / Ghost',
|
|
246
|
+
STORYBLOK: 'CMS / Storyblok',
|
|
247
|
+
BUILDER: 'CMS / Builder.io',
|
|
248
|
+
|
|
249
|
+
// ── Hosting / Deploy ───────────────────────────────────────────────────────
|
|
250
|
+
VERCEL: 'Hosting / Vercel',
|
|
251
|
+
NETLIFY: 'Hosting / Netlify',
|
|
252
|
+
RAILWAY: 'Hosting / Railway',
|
|
253
|
+
RENDER: 'Hosting / Render',
|
|
254
|
+
FLY: 'Hosting / Fly.io',
|
|
255
|
+
COOLIFY: 'Hosting / Coolify',
|
|
256
|
+
DOKKU: 'Hosting / Dokku',
|
|
257
|
+
HEROKU: 'Hosting / Heroku',
|
|
258
|
+
DENO: 'Hosting / Deno Deploy',
|
|
259
|
+
CLOUDFLARE: 'Hosting / Cloudflare',
|
|
260
|
+
|
|
261
|
+
// ── Social / OAuth Providers ───────────────────────────────────────────────
|
|
262
|
+
GITHUB: 'OAuth / GitHub',
|
|
263
|
+
GITLAB: 'OAuth / GitLab',
|
|
264
|
+
TWITTER: 'OAuth / Twitter / X',
|
|
265
|
+
TWITTER_X: 'OAuth / Twitter / X',
|
|
266
|
+
X: 'OAuth / Twitter / X',
|
|
267
|
+
FACEBOOK: 'OAuth / Facebook',
|
|
268
|
+
INSTAGRAM: 'OAuth / Instagram',
|
|
269
|
+
LINKEDIN: 'OAuth / LinkedIn',
|
|
270
|
+
DISCORD: 'OAuth / Discord',
|
|
271
|
+
SPOTIFY: 'OAuth / Spotify',
|
|
272
|
+
APPLE: 'OAuth / Apple',
|
|
273
|
+
MICROSOFT: 'OAuth / Microsoft',
|
|
274
|
+
|
|
275
|
+
// ── Collaboration / Productivity ───────────────────────────────────────────
|
|
276
|
+
SLACK: 'Integrations / Slack',
|
|
277
|
+
NOTION: 'Integrations / Notion',
|
|
278
|
+
LINEAR: 'Integrations / Linear',
|
|
279
|
+
JIRA: 'Integrations / Jira',
|
|
280
|
+
AIRTABLE: 'Integrations / Airtable',
|
|
281
|
+
ZAPIER: 'Integrations / Zapier',
|
|
282
|
+
MAKE: 'Integrations / Make',
|
|
283
|
+
|
|
284
|
+
// ── Maps / Location ────────────────────────────────────────────────────────
|
|
285
|
+
MAPBOX: 'Maps / Mapbox',
|
|
286
|
+
GOOGLE_MAPS: 'Maps / Google Maps',
|
|
287
|
+
MAPS: 'Maps / Google Maps',
|
|
288
|
+
HERE: 'Maps / HERE',
|
|
289
|
+
IPINFO: 'Maps / IPinfo',
|
|
290
|
+
MAXMIND: 'Maps / MaxMind',
|
|
291
|
+
|
|
292
|
+
// ── Crypto / Web3 ──────────────────────────────────────────────────────────
|
|
293
|
+
ALCHEMY: 'Web3 / Alchemy',
|
|
294
|
+
INFURA: 'Web3 / Infura',
|
|
295
|
+
MORALIS: 'Web3 / Moralis',
|
|
296
|
+
THIRDWEB: 'Web3 / Thirdweb',
|
|
297
|
+
WALLET: 'Web3 / Wallet',
|
|
298
|
+
|
|
299
|
+
// ── Testing / Dev Tools ────────────────────────────────────────────────────
|
|
300
|
+
PLAYWRIGHT: 'Testing / Playwright',
|
|
301
|
+
CYPRESS: 'Testing / Cypress',
|
|
302
|
+
BROWSERSTACK: 'Testing / BrowserStack',
|
|
303
|
+
SAUCE: 'Testing / Sauce Labs',
|
|
304
|
+
STORYBOOK: 'Dev / Storybook',
|
|
305
|
+
CHROMATIC: 'Dev / Chromatic',
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const STANDALONE_LABELS = {
|
|
309
|
+
PORT: 'App / Config',
|
|
310
|
+
HOST: 'App / Config',
|
|
311
|
+
NODE_ENV: 'App / Config',
|
|
312
|
+
APP_ENV: 'App / Config',
|
|
313
|
+
ENVIRONMENT: 'App / Config',
|
|
314
|
+
BASE_URL: 'App / Config',
|
|
315
|
+
API_URL: 'App / Config',
|
|
316
|
+
SITE_URL: 'App / Config',
|
|
317
|
+
FRONTEND_URL: 'App / Config',
|
|
318
|
+
BACKEND_URL: 'App / Config',
|
|
319
|
+
PUBLIC_URL: 'App / Config',
|
|
320
|
+
LOG_LEVEL: 'App / Config',
|
|
321
|
+
DEBUG: 'App / Config',
|
|
322
|
+
TZ: 'App / Config',
|
|
323
|
+
TIMEZONE: 'App / Config',
|
|
324
|
+
LOCALE: 'App / Config',
|
|
325
|
+
LANG: 'App / Config',
|
|
326
|
+
SECRET: 'Secrets',
|
|
327
|
+
SECRET_KEY: 'Secrets',
|
|
328
|
+
ENCRYPTION_KEY: 'Secrets',
|
|
329
|
+
API_KEY: 'API Keys',
|
|
330
|
+
API_SECRET: 'API Keys',
|
|
331
|
+
ACCESS_TOKEN: 'API Keys',
|
|
332
|
+
PRIVATE_KEY: 'Secrets',
|
|
333
|
+
PUBLIC_KEY: 'Secrets',
|
|
334
|
+
};
|
|
335
|
+
|
|
3
336
|
export function parseEnv(content) {
|
|
4
337
|
const vars = {};
|
|
5
338
|
for (const line of content.split('\n')) {
|
|
@@ -9,7 +342,6 @@ export function parseEnv(content) {
|
|
|
9
342
|
if (eqIdx === -1) continue;
|
|
10
343
|
const key = trimmed.slice(0, eqIdx).trim();
|
|
11
344
|
let value = trimmed.slice(eqIdx + 1).trim();
|
|
12
|
-
// Strip surrounding quotes
|
|
13
345
|
if (
|
|
14
346
|
(value.startsWith('"') && value.endsWith('"')) ||
|
|
15
347
|
(value.startsWith("'") && value.endsWith("'"))
|
|
@@ -21,6 +353,7 @@ export function parseEnv(content) {
|
|
|
21
353
|
return vars;
|
|
22
354
|
}
|
|
23
355
|
|
|
356
|
+
// Used internally for encryption — keeps insertion order, no formatting
|
|
24
357
|
export function stringifyEnv(vars) {
|
|
25
358
|
const lines = Object.entries(vars).map(([k, v]) => {
|
|
26
359
|
const needsQuotes = /[\s"'\\#]/.test(v) || v === '';
|
|
@@ -30,19 +363,95 @@ export function stringifyEnv(vars) {
|
|
|
30
363
|
return lines.join('\n') + (lines.length ? '\n' : '');
|
|
31
364
|
}
|
|
32
365
|
|
|
366
|
+
function formatValue(v) {
|
|
367
|
+
const needsQuotes = /[\s"'\\#]/.test(v) || v === '';
|
|
368
|
+
const escaped = v.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
369
|
+
return needsQuotes ? `"${escaped}"` : v;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function getSectionForKey(key) {
|
|
373
|
+
if (STANDALONE_LABELS[key]) return STANDALONE_LABELS[key];
|
|
374
|
+
|
|
375
|
+
// Try progressively shorter prefixes — longest match wins
|
|
376
|
+
const parts = key.split('_');
|
|
377
|
+
for (let len = parts.length - 1; len >= 1; len--) {
|
|
378
|
+
const prefix = parts.slice(0, len).join('_');
|
|
379
|
+
if (SECTION_LABELS[prefix]) return SECTION_LABELS[prefix];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return 'General';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function groupAndSort(vars) {
|
|
386
|
+
const sections = {};
|
|
387
|
+
|
|
388
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
389
|
+
const section = getSectionForKey(key);
|
|
390
|
+
if (!sections[section]) sections[section] = [];
|
|
391
|
+
sections[section].push([key, value]);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
for (const section of Object.keys(sections)) {
|
|
395
|
+
sections[section].sort(([a], [b]) => a.localeCompare(b));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// App/Config first, Secrets near top, General at bottom
|
|
399
|
+
const priority = {
|
|
400
|
+
'App / Config': 0,
|
|
401
|
+
'App / Runtime': 1,
|
|
402
|
+
'App / Server': 2,
|
|
403
|
+
'Secrets': 3,
|
|
404
|
+
'API Keys': 4,
|
|
405
|
+
'General': 999,
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
return Object.entries(sections).sort(([a], [b]) => {
|
|
409
|
+
const pa = priority[a] ?? 50;
|
|
410
|
+
const pb = priority[b] ?? 50;
|
|
411
|
+
if (pa !== pb) return pa - pb;
|
|
412
|
+
return a.localeCompare(b);
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
33
416
|
export function readEnvFile(filePath) {
|
|
34
417
|
if (!existsSync(filePath)) return {};
|
|
35
418
|
return parseEnv(readFileSync(filePath, 'utf8'));
|
|
36
419
|
}
|
|
37
420
|
|
|
38
421
|
export function writeEnvFile(filePath, vars, { envName, projectRoot } = {}) {
|
|
39
|
-
|
|
422
|
+
const entries = Object.entries(vars);
|
|
423
|
+
const projectName = projectRoot ? projectRoot.split('/').pop() : null;
|
|
424
|
+
const lines = [];
|
|
425
|
+
|
|
426
|
+
// Header
|
|
40
427
|
if (envName) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
428
|
+
const width = 50;
|
|
429
|
+
const bar = '─'.repeat(width);
|
|
430
|
+
lines.push(`# ${bar}`);
|
|
431
|
+
lines.push(`# envgit — encrypted environment manager`);
|
|
432
|
+
if (projectName) lines.push(`# Project : ${projectName}`);
|
|
433
|
+
lines.push(`# Env : ${envName}`);
|
|
434
|
+
lines.push(`# Edit : envgit set KEY=VALUE --env ${envName}`);
|
|
435
|
+
lines.push(`# ${bar}`);
|
|
436
|
+
lines.push('');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (entries.length === 0) {
|
|
440
|
+
writeFileSync(filePath, lines.join('\n'), 'utf8');
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const grouped = groupAndSort(vars);
|
|
445
|
+
const maxKeyLen = Math.max(...entries.map(([k]) => k.length));
|
|
446
|
+
|
|
447
|
+
for (const [section, sectionVars] of grouped) {
|
|
448
|
+
lines.push(`# ── ${section} ${'─'.repeat(Math.max(0, 44 - section.length))}`);
|
|
449
|
+
for (const [k, v] of sectionVars) {
|
|
450
|
+
const padding = ' '.repeat(maxKeyLen - k.length);
|
|
451
|
+
lines.push(`${k}${padding} = ${formatValue(v)}`);
|
|
452
|
+
}
|
|
453
|
+
lines.push('');
|
|
45
454
|
}
|
|
46
|
-
|
|
47
|
-
writeFileSync(filePath,
|
|
455
|
+
|
|
456
|
+
writeFileSync(filePath, lines.join('\n'), 'utf8');
|
|
48
457
|
}
|
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
|
}
|