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.
Files changed (2) hide show
  1. package/package.json +34 -0
  2. 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();