envd-cli 0.1.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/package.json +34 -0
- package/src/index.ts +426 -0
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "envd-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Environment variables management CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"envd": "./src/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"dev": "bun run src/index.ts",
|
|
14
|
+
"build": "bun build src/index.ts --compile --outfile dist/envd",
|
|
15
|
+
"typecheck": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"env",
|
|
19
|
+
"environment",
|
|
20
|
+
"dotenv",
|
|
21
|
+
"cli",
|
|
22
|
+
"sync"
|
|
23
|
+
],
|
|
24
|
+
"author": "sunneydev",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/sunneydev/envd.git"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/bun": "latest",
|
|
32
|
+
"typescript": "^5.0.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { join, basename } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
|
|
6
|
+
// Config management
|
|
7
|
+
const CONFIG_DIR = join(homedir(), ".envd");
|
|
8
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
9
|
+
|
|
10
|
+
interface Config {
|
|
11
|
+
apiUrl: string;
|
|
12
|
+
apiKey: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function loadConfig(): Config | null {
|
|
16
|
+
if (!existsSync(CONFIG_FILE)) return null;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function saveConfig(config: Config): void {
|
|
25
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
26
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function requireConfig(): Config {
|
|
32
|
+
const config = loadConfig();
|
|
33
|
+
if (!config) {
|
|
34
|
+
console.error("Not configured. Run: envd config");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
return config;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// API client
|
|
41
|
+
async function api<T>(
|
|
42
|
+
config: Config,
|
|
43
|
+
method: string,
|
|
44
|
+
path: string,
|
|
45
|
+
body?: unknown
|
|
46
|
+
): Promise<T> {
|
|
47
|
+
const url = `${config.apiUrl}${path}`;
|
|
48
|
+
const res = await fetch(url, {
|
|
49
|
+
method,
|
|
50
|
+
headers: {
|
|
51
|
+
"Content-Type": "application/json",
|
|
52
|
+
"X-API-Key": config.apiKey,
|
|
53
|
+
},
|
|
54
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
const text = await res.text();
|
|
59
|
+
throw new Error(`API error ${res.status}: ${text}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return res.json();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// .env file parsing
|
|
66
|
+
function parseEnvFile(content: string): Record<string, string> {
|
|
67
|
+
const vars: Record<string, string> = {};
|
|
68
|
+
for (const line of content.split("\n")) {
|
|
69
|
+
const trimmed = line.trim();
|
|
70
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
71
|
+
const eq = trimmed.indexOf("=");
|
|
72
|
+
if (eq === -1) continue;
|
|
73
|
+
const key = trimmed.slice(0, eq).trim();
|
|
74
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
75
|
+
// Remove surrounding quotes
|
|
76
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
77
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
78
|
+
value = value.slice(1, -1);
|
|
79
|
+
}
|
|
80
|
+
vars[key] = value;
|
|
81
|
+
}
|
|
82
|
+
return vars;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function formatEnvFile(vars: Record<string, string>): string {
|
|
86
|
+
return Object.entries(vars)
|
|
87
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
88
|
+
.map(([k, v]) => {
|
|
89
|
+
// Quote if contains spaces or special chars
|
|
90
|
+
if (v.includes(" ") || v.includes("=") || v.includes("#")) {
|
|
91
|
+
return `${k}="${v}"`;
|
|
92
|
+
}
|
|
93
|
+
return `${k}=${v}`;
|
|
94
|
+
})
|
|
95
|
+
.join("\n") + "\n";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Get current project name from directory
|
|
99
|
+
function getProjectName(): string {
|
|
100
|
+
return basename(process.cwd());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Commands
|
|
104
|
+
async function cmdConfig(): Promise<void> {
|
|
105
|
+
const readline = await import("readline");
|
|
106
|
+
const rl = readline.createInterface({
|
|
107
|
+
input: process.stdin,
|
|
108
|
+
output: process.stdout,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const question = (q: string): Promise<string> =>
|
|
112
|
+
new Promise((resolve) => rl.question(q, resolve));
|
|
113
|
+
|
|
114
|
+
const existing = loadConfig();
|
|
115
|
+
|
|
116
|
+
console.log("envd configuration\n");
|
|
117
|
+
|
|
118
|
+
const apiUrl = await question(
|
|
119
|
+
`API URL [${existing?.apiUrl || "https://envd.example.com"}]: `
|
|
120
|
+
);
|
|
121
|
+
const apiKey = await question("API Key: ");
|
|
122
|
+
|
|
123
|
+
rl.close();
|
|
124
|
+
|
|
125
|
+
const config: Config = {
|
|
126
|
+
apiUrl: apiUrl || existing?.apiUrl || "https://envd.example.com",
|
|
127
|
+
apiKey: apiKey || existing?.apiKey || "",
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
saveConfig(config);
|
|
131
|
+
console.log(`\nConfig saved to ${CONFIG_FILE}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function cmdInit(): Promise<void> {
|
|
135
|
+
const config = requireConfig();
|
|
136
|
+
const projectName = getProjectName();
|
|
137
|
+
|
|
138
|
+
console.log(`Initializing project: ${projectName}`);
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// Create project
|
|
142
|
+
await api(config, "POST", "/api/projects", { name: projectName });
|
|
143
|
+
console.log("Created project");
|
|
144
|
+
} catch (e: any) {
|
|
145
|
+
if (e.message.includes("409") || e.message.includes("already exists")) {
|
|
146
|
+
console.log("Project already exists");
|
|
147
|
+
} else {
|
|
148
|
+
throw e;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Create default environment
|
|
153
|
+
try {
|
|
154
|
+
const project = await api<{ id: string }>(
|
|
155
|
+
config,
|
|
156
|
+
"GET",
|
|
157
|
+
`/api/projects/${encodeURIComponent(projectName)}`
|
|
158
|
+
);
|
|
159
|
+
await api(config, "POST", `/api/projects/${project.id}/environments`, {
|
|
160
|
+
name: "default",
|
|
161
|
+
});
|
|
162
|
+
console.log("Created default environment");
|
|
163
|
+
} catch (e: any) {
|
|
164
|
+
if (e.message.includes("409") || e.message.includes("already exists")) {
|
|
165
|
+
console.log("Default environment already exists");
|
|
166
|
+
} else {
|
|
167
|
+
throw e;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log("\nProject initialized. Use 'envd push' to upload variables.");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function cmdPush(envName: string = "default"): Promise<void> {
|
|
175
|
+
const config = requireConfig();
|
|
176
|
+
const projectName = getProjectName();
|
|
177
|
+
|
|
178
|
+
// Read local .env
|
|
179
|
+
const envPath = join(process.cwd(), ".env");
|
|
180
|
+
if (!existsSync(envPath)) {
|
|
181
|
+
console.error("No .env file found in current directory");
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const content = readFileSync(envPath, "utf-8");
|
|
186
|
+
const vars = parseEnvFile(content);
|
|
187
|
+
const count = Object.keys(vars).length;
|
|
188
|
+
|
|
189
|
+
if (count === 0) {
|
|
190
|
+
console.log("No variables found in .env");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
console.log(`Pushing ${count} variables to ${projectName}/${envName}...`);
|
|
195
|
+
|
|
196
|
+
// Get project and environment
|
|
197
|
+
const project = await api<{ id: string }>(
|
|
198
|
+
config,
|
|
199
|
+
"GET",
|
|
200
|
+
`/api/projects/${encodeURIComponent(projectName)}`
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const envs = await api<{ id: string; name: string }[]>(
|
|
204
|
+
config,
|
|
205
|
+
"GET",
|
|
206
|
+
`/api/projects/${project.id}/environments`
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const env = envs.find((e) => e.name === envName);
|
|
210
|
+
if (!env) {
|
|
211
|
+
console.error(`Environment '${envName}' not found. Create it with: envd env create ${envName}`);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Push variables
|
|
216
|
+
await api(config, "PUT", `/api/environments/${env.id}/variables`, { variables: vars });
|
|
217
|
+
|
|
218
|
+
console.log("Done");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function cmdPull(envName: string = "default"): Promise<void> {
|
|
222
|
+
const config = requireConfig();
|
|
223
|
+
const projectName = getProjectName();
|
|
224
|
+
|
|
225
|
+
console.log(`Pulling ${projectName}/${envName}...`);
|
|
226
|
+
|
|
227
|
+
// Get project and environment
|
|
228
|
+
const project = await api<{ id: string }>(
|
|
229
|
+
config,
|
|
230
|
+
"GET",
|
|
231
|
+
`/api/projects/${encodeURIComponent(projectName)}`
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const envs = await api<{ id: string; name: string }[]>(
|
|
235
|
+
config,
|
|
236
|
+
"GET",
|
|
237
|
+
`/api/projects/${project.id}/environments`
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const env = envs.find((e) => e.name === envName);
|
|
241
|
+
if (!env) {
|
|
242
|
+
console.error(`Environment '${envName}' not found`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Get variables
|
|
247
|
+
const variables = await api<{ key: string; value: string }[]>(
|
|
248
|
+
config,
|
|
249
|
+
"GET",
|
|
250
|
+
`/api/environments/${env.id}/variables`
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
if (variables.length === 0) {
|
|
254
|
+
console.log("No variables found");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Convert to record and write
|
|
259
|
+
const vars: Record<string, string> = {};
|
|
260
|
+
for (const v of variables) {
|
|
261
|
+
vars[v.key] = v.value;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const content = formatEnvFile(vars);
|
|
265
|
+
const envPath = join(process.cwd(), ".env");
|
|
266
|
+
writeFileSync(envPath, content);
|
|
267
|
+
|
|
268
|
+
console.log(`Wrote ${variables.length} variables to .env`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function cmdEnvList(): Promise<void> {
|
|
272
|
+
const config = requireConfig();
|
|
273
|
+
const projectName = getProjectName();
|
|
274
|
+
|
|
275
|
+
const project = await api<{ id: string }>(
|
|
276
|
+
config,
|
|
277
|
+
"GET",
|
|
278
|
+
`/api/projects/${encodeURIComponent(projectName)}`
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const envs = await api<{ id: string; name: string; created_at: number }[]>(
|
|
282
|
+
config,
|
|
283
|
+
"GET",
|
|
284
|
+
`/api/projects/${project.id}/environments`
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
if (envs.length === 0) {
|
|
288
|
+
console.log("No environments found");
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
console.log("Environments:\n");
|
|
293
|
+
for (const env of envs) {
|
|
294
|
+
console.log(` ${env.name}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function cmdEnvCreate(name: string): Promise<void> {
|
|
299
|
+
const config = requireConfig();
|
|
300
|
+
const projectName = getProjectName();
|
|
301
|
+
|
|
302
|
+
const project = await api<{ id: string }>(
|
|
303
|
+
config,
|
|
304
|
+
"GET",
|
|
305
|
+
`/api/projects/${encodeURIComponent(projectName)}`
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
await api(config, "POST", `/api/projects/${project.id}/environments`, { name });
|
|
309
|
+
|
|
310
|
+
console.log(`Created environment: ${name}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function cmdEnvDelete(name: string): Promise<void> {
|
|
314
|
+
const config = requireConfig();
|
|
315
|
+
const projectName = getProjectName();
|
|
316
|
+
|
|
317
|
+
const project = await api<{ id: string }>(
|
|
318
|
+
config,
|
|
319
|
+
"GET",
|
|
320
|
+
`/api/projects/${encodeURIComponent(projectName)}`
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const envs = await api<{ id: string; name: string }[]>(
|
|
324
|
+
config,
|
|
325
|
+
"GET",
|
|
326
|
+
`/api/projects/${project.id}/environments`
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const env = envs.find((e) => e.name === name);
|
|
330
|
+
if (!env) {
|
|
331
|
+
console.error(`Environment '${name}' not found`);
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
await api(config, "DELETE", `/api/environments/${env.id}`, {});
|
|
336
|
+
|
|
337
|
+
console.log(`Deleted environment: ${name}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function printHelp(): void {
|
|
341
|
+
console.log(`envd - environment variables management
|
|
342
|
+
|
|
343
|
+
USAGE:
|
|
344
|
+
envd <command> [args]
|
|
345
|
+
|
|
346
|
+
COMMANDS:
|
|
347
|
+
config Configure API endpoint and key
|
|
348
|
+
init Initialize project (creates default environment)
|
|
349
|
+
push [env] Push .env to remote (default: default)
|
|
350
|
+
pull [env] Pull remote env to local .env (default: default)
|
|
351
|
+
env list List environments for current project
|
|
352
|
+
env create <name> Create new environment
|
|
353
|
+
env delete <name> Delete environment
|
|
354
|
+
help Show this help
|
|
355
|
+
|
|
356
|
+
EXAMPLES:
|
|
357
|
+
envd config # Set up API connection
|
|
358
|
+
envd init # Initialize current directory as project
|
|
359
|
+
envd push # Push .env to default environment
|
|
360
|
+
envd push production # Push to production environment
|
|
361
|
+
envd pull staging # Pull staging environment to .env
|
|
362
|
+
envd env create staging # Create staging environment
|
|
363
|
+
`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Main
|
|
367
|
+
async function main(): Promise<void> {
|
|
368
|
+
const args = process.argv.slice(2);
|
|
369
|
+
const cmd = args[0];
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
switch (cmd) {
|
|
373
|
+
case "config":
|
|
374
|
+
await cmdConfig();
|
|
375
|
+
break;
|
|
376
|
+
case "init":
|
|
377
|
+
await cmdInit();
|
|
378
|
+
break;
|
|
379
|
+
case "push":
|
|
380
|
+
await cmdPush(args[1]);
|
|
381
|
+
break;
|
|
382
|
+
case "pull":
|
|
383
|
+
await cmdPull(args[1]);
|
|
384
|
+
break;
|
|
385
|
+
case "env":
|
|
386
|
+
switch (args[1]) {
|
|
387
|
+
case "list":
|
|
388
|
+
await cmdEnvList();
|
|
389
|
+
break;
|
|
390
|
+
case "create":
|
|
391
|
+
if (!args[2]) {
|
|
392
|
+
console.error("Usage: envd env create <name>");
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
await cmdEnvCreate(args[2]);
|
|
396
|
+
break;
|
|
397
|
+
case "delete":
|
|
398
|
+
if (!args[2]) {
|
|
399
|
+
console.error("Usage: envd env delete <name>");
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
await cmdEnvDelete(args[2]);
|
|
403
|
+
break;
|
|
404
|
+
default:
|
|
405
|
+
console.error("Unknown env command. Use: list, create, delete");
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
break;
|
|
409
|
+
case "help":
|
|
410
|
+
case "--help":
|
|
411
|
+
case "-h":
|
|
412
|
+
case undefined:
|
|
413
|
+
printHelp();
|
|
414
|
+
break;
|
|
415
|
+
default:
|
|
416
|
+
console.error(`Unknown command: ${cmd}`);
|
|
417
|
+
printHelp();
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
} catch (e: any) {
|
|
421
|
+
console.error("Error:", e.message);
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
main();
|