@envpilot/cli 1.4.1 → 1.6.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/dist/index.js CHANGED
@@ -1,3550 +1,26 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/lib/sentry.ts
4
- import * as Sentry from "@sentry/node";
5
- var initialized = false;
6
- function initSentry() {
7
- const dsn = true ? "" : "";
8
- if (initialized || !dsn) return;
9
- Sentry.init({
10
- dsn,
11
- environment: "cli",
12
- release: true ? "1.4.1" : "0.0.0",
13
- // Free tier: disable performance monitoring
14
- tracesSampleRate: 0,
15
- beforeSend(event) {
16
- if (event.exception?.values) {
17
- for (const exc of event.exception.values) {
18
- if (exc.stacktrace?.frames) {
19
- for (const frame of exc.stacktrace.frames) {
20
- if (frame.filename) {
21
- frame.filename = frame.filename.replace(
22
- /\/Users\/[^/]+/g,
23
- "/~"
24
- );
25
- frame.filename = frame.filename.replace(
26
- /C:\\Users\\[^\\]+/g,
27
- "C:\\~"
28
- );
29
- }
30
- }
31
- }
32
- }
33
- }
34
- if (event.request?.data) {
35
- event.request.data = "[REDACTED]";
36
- }
37
- return event;
38
- }
39
- });
40
- initialized = true;
41
- }
42
- function captureError(error2, context) {
43
- if (!initialized) return;
44
- Sentry.captureException(error2, { tags: context });
45
- }
46
- async function flushSentry() {
47
- if (!initialized) return;
48
- await Sentry.flush(2e3);
49
- }
50
-
51
- // src/index.ts
52
- import { Command as Command12 } from "commander";
53
-
54
- // src/commands/login.ts
55
- import { Command } from "commander";
56
-
57
- // src/lib/ui.ts
58
- import chalk from "chalk";
59
- import ora from "ora";
60
- function createSpinner(text) {
61
- return ora({
62
- text,
63
- color: "cyan"
64
- });
65
- }
66
- async function withSpinner(text, operation, options) {
67
- const spinner = createSpinner(text);
68
- spinner.start();
69
- try {
70
- const result = await operation();
71
- spinner.succeed(options?.successText ?? text);
72
- return result;
73
- } catch (error2) {
74
- spinner.fail(options?.failText ?? text);
75
- throw error2;
76
- }
77
- }
78
- function success(message) {
79
- console.log(chalk.green("\u2713"), message);
80
- }
81
- function info(message) {
82
- console.log(chalk.blue("\u2139"), message);
83
- }
84
- function warning(message) {
85
- console.log(chalk.yellow("\u26A0"), message);
86
- }
87
- function error(message) {
88
- console.log(chalk.red("\u2717"), message);
89
- }
90
- function header(text) {
91
- console.log();
92
- console.log(chalk.bold(text));
93
- console.log(chalk.dim("\u2500".repeat(text.length)));
94
- }
95
- function table(data, columns) {
96
- if (data.length === 0) {
97
- console.log(chalk.dim("No data to display"));
98
- return;
99
- }
100
- const widths = columns.map((col) => {
101
- const headerWidth = col.header.length;
102
- const maxDataWidth = Math.max(
103
- ...data.map((row) => String(row[col.key] ?? "").length)
104
- );
105
- return col.width ?? Math.max(headerWidth, maxDataWidth);
106
- });
107
- const headerLine = columns.map((col, i) => col.header.padEnd(widths[i])).join(" ");
108
- console.log(chalk.bold(headerLine));
109
- console.log(chalk.dim("\u2500".repeat(headerLine.length)));
110
- for (const row of data) {
111
- const line = columns.map((col, i) => String(row[col.key] ?? "").padEnd(widths[i])).join(" ");
112
- console.log(line);
113
- }
114
- }
115
- function keyValue(pairs) {
116
- const maxKeyLength = Math.max(...pairs.map(([key]) => key.length));
117
- for (const [key, value] of pairs) {
118
- const paddedKey = key.padEnd(maxKeyLength);
119
- console.log(`${chalk.dim(paddedKey)} ${value ?? chalk.dim("(not set)")}`);
120
- }
121
- }
122
- function diff(added, removed, changed) {
123
- if (Object.keys(added).length === 0 && Object.keys(removed).length === 0 && Object.keys(changed).length === 0) {
124
- console.log(chalk.dim("No changes"));
125
- return;
126
- }
127
- for (const [key, value] of Object.entries(added)) {
128
- console.log(chalk.green(`+ ${key}=${maskValue(value)}`));
129
- }
130
- for (const [key, value] of Object.entries(removed)) {
131
- console.log(chalk.red(`- ${key}=${maskValue(value)}`));
132
- }
133
- for (const [key, { local, remote }] of Object.entries(changed)) {
134
- console.log(chalk.red(`- ${key}=${maskValue(remote)}`));
135
- console.log(chalk.green(`+ ${key}=${maskValue(local)}`));
136
- }
137
- }
138
- function maskValue(value, showChars = 4) {
139
- if (value.length <= showChars * 2) {
140
- return "*".repeat(value.length);
141
- }
142
- return value.slice(0, showChars) + "****" + value.slice(-showChars);
143
- }
144
- function blank() {
145
- console.log();
146
- }
147
- function formatRole(role) {
148
- switch (role) {
149
- case "admin":
150
- return chalk.green("Admin");
151
- case "team_lead":
152
- return chalk.blue("Team Lead");
153
- case "member":
154
- return chalk.yellow("Member");
155
- default:
156
- return chalk.dim("Unknown");
157
- }
158
- }
159
- function roleNotice(role) {
160
- if (role === "member") {
161
- console.log(
162
- chalk.yellow(
163
- " You have Member access. Write operations will create pending requests for approval."
164
- )
165
- );
166
- }
167
- }
168
- function formatProjectRole(role) {
169
- switch (role) {
170
- case "manager":
171
- return chalk.green("Manager");
172
- case "developer":
173
- return chalk.blue("Developer");
174
- case "viewer":
175
- return chalk.yellow("Viewer");
176
- default:
177
- return chalk.dim("-");
178
- }
179
- }
180
- function projectRoleNotice(projectRole) {
181
- if (projectRole === "viewer") {
182
- console.log(
183
- chalk.yellow(
184
- " You have Viewer access to this project. You can only view variables you have been explicitly granted access to."
185
- )
186
- );
187
- }
188
- }
189
-
190
- // src/lib/errors.ts
191
- import chalk2 from "chalk";
192
- var CLIError = class extends Error {
193
- constructor(message, code, suggestion) {
194
- super(message);
195
- this.code = code;
196
- this.suggestion = suggestion;
197
- this.name = "CLIError";
198
- }
199
- };
200
- var ErrorCodes = {
201
- NOT_AUTHENTICATED: "NOT_AUTHENTICATED",
202
- NOT_INITIALIZED: "NOT_INITIALIZED",
203
- PROJECT_NOT_FOUND: "PROJECT_NOT_FOUND",
204
- ORGANIZATION_NOT_FOUND: "ORGANIZATION_NOT_FOUND",
205
- VARIABLE_NOT_FOUND: "VARIABLE_NOT_FOUND",
206
- INVALID_CONFIG: "INVALID_CONFIG",
207
- NETWORK_ERROR: "NETWORK_ERROR",
208
- PERMISSION_DENIED: "PERMISSION_DENIED",
209
- TIER_LIMIT_EXCEEDED: "TIER_LIMIT_EXCEEDED",
210
- FILE_NOT_FOUND: "FILE_NOT_FOUND",
211
- INVALID_INPUT: "INVALID_INPUT",
212
- UNKNOWN_ERROR: "UNKNOWN_ERROR"
213
- };
214
- function formatError(error2) {
215
- if (error2 instanceof CLIError) {
216
- let message = chalk2.red(`Error: ${error2.message}`);
217
- if (error2.suggestion) {
218
- message += `
219
- ${chalk2.yellow("Suggestion:")} ${error2.suggestion}`;
220
- }
221
- return message;
222
- }
223
- if (error2 instanceof Error) {
224
- return chalk2.red(`Error: ${error2.message}`);
225
- }
226
- return chalk2.red(`Error: ${String(error2)}`);
227
- }
228
- async function handleError(error2) {
229
- console.error(formatError(error2));
230
- const skipCodes = /* @__PURE__ */ new Set([
231
- ErrorCodes.NOT_AUTHENTICATED,
232
- ErrorCodes.INVALID_INPUT,
233
- ErrorCodes.NOT_INITIALIZED
234
- ]);
235
- if (error2 instanceof CLIError) {
236
- if (!skipCodes.has(error2.code)) {
237
- captureError(error2, { errorCode: error2.code });
238
- }
239
- } else {
240
- captureError(error2);
241
- }
242
- await flushSentry();
243
- if (error2 instanceof CLIError) {
244
- switch (error2.code) {
245
- case ErrorCodes.NOT_AUTHENTICATED:
246
- process.exit(2);
247
- case ErrorCodes.PERMISSION_DENIED:
248
- process.exit(3);
249
- case ErrorCodes.TIER_LIMIT_EXCEEDED:
250
- process.exit(4);
251
- default:
252
- process.exit(1);
253
- }
254
- }
255
- process.exit(1);
256
- }
257
- function notAuthenticated() {
258
- return new CLIError(
259
- "You are not authenticated.",
260
- ErrorCodes.NOT_AUTHENTICATED,
261
- "Run `envpilot login` to authenticate."
262
- );
263
- }
264
- function notInitialized() {
265
- return new CLIError(
266
- "This directory is not initialized with Envpilot.",
267
- ErrorCodes.NOT_INITIALIZED,
268
- "Run `envpilot init` to initialize."
269
- );
270
- }
271
- function fileNotFound(path) {
272
- return new CLIError(`File not found: ${path}`, ErrorCodes.FILE_NOT_FOUND);
273
- }
274
-
275
- // src/lib/config.ts
276
- import Conf from "conf";
277
- var DEFAULT_API_URL = "https://www.envpilot.dev";
278
- var config = new Conf({
279
- projectName: "envpilot",
280
- defaults: {
281
- apiUrl: DEFAULT_API_URL
282
- }
283
- });
284
- function getConfig() {
285
- return {
286
- apiUrl: config.get("apiUrl") ?? DEFAULT_API_URL,
287
- accessToken: config.get("accessToken"),
288
- refreshToken: config.get("refreshToken"),
289
- activeProjectId: config.get("activeProjectId"),
290
- activeOrganizationId: config.get("activeOrganizationId"),
291
- user: config.get("user"),
292
- role: config.get("role")
293
- };
294
- }
295
- function getApiUrl() {
296
- return config.get("apiUrl") ?? DEFAULT_API_URL;
297
- }
298
- function setApiUrl(url) {
299
- config.set("apiUrl", url);
300
- }
301
- function getAccessToken() {
302
- return config.get("accessToken");
303
- }
304
- function setAccessToken(token) {
305
- config.set("accessToken", token);
306
- }
307
- function setRefreshToken(token) {
308
- config.set("refreshToken", token);
309
- }
310
- function setActiveProjectId(projectId) {
311
- config.set("activeProjectId", projectId);
312
- }
313
- function getActiveOrganizationId() {
314
- return config.get("activeOrganizationId");
315
- }
316
- function setActiveOrganizationId(organizationId) {
317
- config.set("activeOrganizationId", organizationId);
318
- }
319
- function getUser() {
320
- return config.get("user");
321
- }
322
- function setUser(user) {
323
- config.set("user", user);
324
- }
325
- function getRole() {
326
- return config.get("role");
327
- }
328
- function setRole(role) {
329
- config.set("role", role);
330
- }
331
- function isAuthenticated() {
332
- return !!config.get("accessToken");
333
- }
334
- function clearAuth() {
335
- config.delete("accessToken");
336
- config.delete("refreshToken");
337
- config.delete("user");
338
- config.delete("role");
339
- }
340
- function clearConfig() {
341
- config.clear();
342
- }
343
- function getConfigPath() {
344
- return config.path;
345
- }
346
-
347
- // src/lib/auth-flow.ts
348
- import open from "open";
349
- import chalk3 from "chalk";
350
- import { hostname } from "os";
351
-
352
- // src/lib/api.ts
353
- var APIError = class extends Error {
354
- constructor(message, statusCode, code) {
355
- super(message);
356
- this.statusCode = statusCode;
357
- this.code = code;
358
- this.name = "APIError";
359
- }
360
- };
361
- var APIClient = class {
362
- baseUrl;
363
- accessToken;
364
- constructor(options) {
365
- this.baseUrl = options?.baseUrl ?? getApiUrl();
366
- this.accessToken = options?.accessToken ?? getAccessToken();
367
- }
368
- /**
369
- * Get headers for API requests
370
- */
371
- getHeaders() {
372
- const headers = {
373
- "Content-Type": "application/json"
374
- };
375
- if (this.accessToken) {
376
- headers["Authorization"] = `Bearer ${this.accessToken}`;
377
- }
378
- return headers;
379
- }
380
- /**
381
- * Make a GET request
382
- */
383
- async get(path, params) {
384
- const url = new URL(path, this.baseUrl);
385
- if (params) {
386
- for (const [key, value] of Object.entries(params)) {
387
- url.searchParams.set(key, value);
388
- }
389
- }
390
- const response = await fetch(url.toString(), {
391
- method: "GET",
392
- headers: this.getHeaders()
393
- });
394
- return this.handleResponse(response);
395
- }
396
- /**
397
- * Make a POST request
398
- */
399
- async post(path, body) {
400
- const url = new URL(path, this.baseUrl);
401
- const response = await fetch(url.toString(), {
402
- method: "POST",
403
- headers: this.getHeaders(),
404
- body: body ? JSON.stringify(body) : void 0
405
- });
406
- return this.handleResponse(response);
407
- }
408
- /**
409
- * Make a PUT request
410
- */
411
- async put(path, body) {
412
- const url = new URL(path, this.baseUrl);
413
- const response = await fetch(url.toString(), {
414
- method: "PUT",
415
- headers: this.getHeaders(),
416
- body: body ? JSON.stringify(body) : void 0
417
- });
418
- return this.handleResponse(response);
419
- }
420
- /**
421
- * Make a PATCH request
422
- */
423
- async patch(path, body) {
424
- const url = new URL(path, this.baseUrl);
425
- const response = await fetch(url.toString(), {
426
- method: "PATCH",
427
- headers: this.getHeaders(),
428
- body: body ? JSON.stringify(body) : void 0
429
- });
430
- return this.handleResponse(response);
431
- }
432
- /**
433
- * Make a DELETE request
434
- */
435
- async delete(path) {
436
- const url = new URL(path, this.baseUrl);
437
- const response = await fetch(url.toString(), {
438
- method: "DELETE",
439
- headers: this.getHeaders()
440
- });
441
- if (!response.ok) {
442
- await this.handleError(response);
443
- }
444
- }
445
- /**
446
- * Handle API response
447
- */
448
- async handleResponse(response) {
449
- if (!response.ok) {
450
- await this.handleError(response);
451
- }
452
- const contentType = response.headers.get("content-type") || "";
453
- if (!contentType.includes("application/json")) {
454
- const body2 = await response.text();
455
- const preview = body2.replace(/\s+/g, " ").slice(0, 160);
456
- throw new APIError(
457
- `Expected JSON but got ${contentType || "unknown content type"} from ${response.url}. Response starts with: ${preview}`,
458
- response.status || 500
459
- );
460
- }
461
- const body = await response.text();
462
- try {
463
- const data = JSON.parse(body);
464
- return data;
465
- } catch {
466
- const preview = body.replace(/\s+/g, " ").slice(0, 160);
467
- throw new APIError(
468
- `Failed to parse JSON response from ${response.url}. Response starts with: ${preview}`,
469
- response.status || 500
470
- );
471
- }
472
- }
473
- /**
474
- * Handle API errors
475
- */
476
- async handleError(response) {
477
- let message = `Request failed with status ${response.status}`;
478
- let code;
479
- try {
480
- const data = await response.json();
481
- message = data.error || data.message || message;
482
- code = data.code;
483
- } catch {
484
- }
485
- if (response.status === 401) {
486
- clearAuth();
487
- throw new APIError(
488
- "Authentication required. Please run `envpilot login`.",
489
- 401,
490
- "UNAUTHORIZED"
491
- );
492
- }
493
- if (response.status === 403 && code === "TIER_LIMIT_REACHED") {
494
- throw new APIError(
495
- message || "Tier limit reached. Run `envpilot usage` to see your plan limits.",
496
- 403,
497
- "TIER_LIMIT_REACHED"
498
- );
499
- }
500
- if (response.status === 403) {
501
- throw new APIError(message || "Access denied.", 403, code || "FORBIDDEN");
502
- }
503
- if (response.status === 402) {
504
- throw new APIError(
505
- message || "Tier limit reached. Run `envpilot usage` to see your plan limits.",
506
- 402,
507
- "PAYMENT_REQUIRED"
508
- );
509
- }
510
- throw new APIError(message, response.status, code);
511
- }
512
- // ============================================
513
- // High-level API methods
514
- // ============================================
515
- /**
516
- * Get current user info
517
- */
518
- async getCurrentUser() {
519
- return this.get("/api/cli/auth/me");
520
- }
521
- /**
522
- * Get tier info for the active organization
523
- */
524
- async getTierInfo(organizationId) {
525
- return this.get("/api/cli/tier", { organizationId });
526
- }
527
- /**
528
- * Get usage info for the active organization
529
- */
530
- async getUsage(organizationId) {
531
- return this.get("/api/cli/usage", { organizationId });
532
- }
533
- /**
534
- * List organizations the user has access to
535
- */
536
- async listOrganizations() {
537
- const response = await this.get(
538
- "/api/cli/organizations"
539
- );
540
- return response.data || [];
541
- }
542
- /**
543
- * List projects in an organization
544
- */
545
- async listProjects(organizationId) {
546
- const response = await this.get(
547
- "/api/cli/projects",
548
- { organizationId }
549
- );
550
- return response.data || [];
551
- }
552
- /**
553
- * Get a project by ID
554
- */
555
- async getProject(projectId) {
556
- return this.get(`/api/cli/projects/${projectId}`);
557
- }
558
- /**
559
- * List variables in a project
560
- */
561
- async listVariables(projectId, environment, organizationId) {
562
- const params = { projectId };
563
- if (environment) {
564
- params.environment = environment;
565
- }
566
- if (organizationId) {
567
- params.organizationId = organizationId;
568
- }
569
- const response = await this.get(
570
- "/api/cli/variables",
571
- params
572
- );
573
- return response.data || [];
574
- }
575
- /**
576
- * Get a variable by ID (with decrypted value)
577
- */
578
- async getVariable(variableId) {
579
- return this.get(`/api/cli/variables/${variableId}`);
580
- }
581
- /**
582
- * Create a new variable
583
- */
584
- async createVariable(data) {
585
- return this.post("/api/cli/variables", data);
586
- }
587
- /**
588
- * Update a variable
589
- */
590
- async updateVariable(variableId, data) {
591
- return this.patch(`/api/cli/variables/${variableId}`, data);
592
- }
593
- /**
594
- * Delete a variable
595
- */
596
- async deleteVariable(variableId) {
597
- return this.delete(`/api/cli/variables/${variableId}`);
598
- }
599
- /**
600
- * Bulk create/update variables
601
- */
602
- async bulkUpsertVariables(data) {
603
- return this.post("/api/cli/variables/bulk", data);
604
- }
605
- // ============================================
606
- // Authentication methods
607
- // ============================================
608
- /**
609
- * Initiate CLI authentication flow
610
- */
611
- async initiateAuth(deviceName) {
612
- return this.post("/api/cli/auth/initiate", { deviceName });
613
- }
614
- /**
615
- * Poll for authentication status
616
- */
617
- async pollAuth(code) {
618
- return this.get("/api/cli/auth/poll", { code });
619
- }
620
- /**
621
- * Refresh access token
622
- */
623
- async refreshToken(refreshToken) {
624
- return this.post("/api/cli/auth/refresh", { refreshToken });
625
- }
626
- /**
627
- * Revoke access token (logout)
628
- */
629
- async revokeToken() {
630
- return this.post("/api/cli/auth/revoke", {});
631
- }
632
- };
633
- function createAPIClient() {
634
- return new APIClient();
635
- }
636
-
637
- // src/lib/auth-flow.ts
638
- var POLL_INTERVAL_MS = 2e3;
639
- var MAX_POLL_ATTEMPTS = 150;
640
- function sleep(ms) {
641
- return new Promise((resolve2) => setTimeout(resolve2, ms));
642
- }
643
- async function performLogin(options) {
644
- const api = createAPIClient();
645
- const deviceName = `CLI - ${hostname()}`;
646
- info("Starting authentication flow...");
647
- const spinner = createSpinner("Generating authentication code...");
648
- spinner.start();
649
- const initResponse = await api.post("/api/cli/auth?action=initiate", { deviceName });
650
- spinner.stop();
651
- console.log();
652
- console.log(chalk3.bold("Your authentication code:"));
653
- console.log();
654
- console.log(chalk3.cyan.bold(` ${initResponse.code}`));
655
- console.log();
656
- console.log(`Open this URL to authenticate:`);
657
- console.log(chalk3.dim(initResponse.url));
658
- console.log();
659
- if (options?.browser !== false) {
660
- info("Opening browser...");
661
- await open(initResponse.url);
662
- }
663
- const pollSpinner = createSpinner("Waiting for authentication...");
664
- pollSpinner.start();
665
- for (let attempts = 0; attempts < MAX_POLL_ATTEMPTS; attempts++) {
666
- await sleep(POLL_INTERVAL_MS);
667
- const pollResponse = await api.get("/api/cli/auth", { action: "poll", code: initResponse.code });
668
- if (pollResponse.status === "authenticated") {
669
- pollSpinner.stop();
670
- if (pollResponse.accessToken) {
671
- setAccessToken(pollResponse.accessToken);
672
- }
673
- if (pollResponse.refreshToken) {
674
- setRefreshToken(pollResponse.refreshToken);
675
- }
676
- if (pollResponse.user) {
677
- setUser({
678
- id: pollResponse.user.id,
679
- email: pollResponse.user.email,
680
- name: pollResponse.user.name
681
- });
682
- }
683
- console.log();
684
- success(`Logged in as ${chalk3.bold(pollResponse.user?.email)}`);
685
- return { email: pollResponse.user?.email || "" };
686
- }
687
- if (pollResponse.status === "expired" || pollResponse.status === "not_found") {
688
- pollSpinner.stop();
689
- throw new Error("Authentication code expired. Please try again.");
690
- }
691
- }
692
- pollSpinner.stop();
693
- throw new Error("Authentication timed out. Please try again.");
694
- }
695
-
696
- // src/commands/login.ts
697
- var loginCommand = new Command("login").description("Authenticate with Envpilot").option("--api-url <url>", "API URL (default: http://localhost:3000)").option("--no-browser", "Do not automatically open the browser").action(async (options) => {
698
- try {
699
- if (options.apiUrl) {
700
- setApiUrl(options.apiUrl);
701
- }
702
- await performLogin({
703
- browser: options.browser !== false
704
- });
705
- console.log();
706
- console.log("Next steps:");
707
- info(" envpilot init Initialize a project in the current directory");
708
- info(" envpilot list List your projects and organizations");
709
- info(" envpilot sync Login, select project, and pull \u2014 all at once");
710
- console.log();
711
- } catch (err) {
712
- await handleError(err);
713
- }
714
- });
715
-
716
- // src/commands/init.ts
717
- import { Command as Command2 } from "commander";
718
- import chalk4 from "chalk";
719
- import inquirer from "inquirer";
720
-
721
- // src/lib/project-config.ts
722
- import { readFileSync, writeFileSync, existsSync, unlinkSync } from "fs";
723
- import { execSync } from "child_process";
724
- import { join } from "path";
725
-
726
- // src/types/index.ts
727
- import { z } from "zod";
728
- var userSchema = z.object({
729
- id: z.string(),
730
- email: z.string().email(),
731
- name: z.string().optional()
732
- });
733
- var organizationSchema = z.object({
734
- _id: z.string(),
735
- name: z.string(),
736
- slug: z.string(),
737
- tier: z.enum(["free", "pro"]),
738
- role: z.string().optional()
739
- });
740
- var projectSchema = z.object({
741
- _id: z.string(),
742
- name: z.string(),
743
- slug: z.string(),
744
- organizationId: z.string(),
745
- description: z.string().optional(),
746
- icon: z.string().optional(),
747
- color: z.string().optional(),
748
- userRole: z.string().nullable().optional(),
749
- projectRole: z.string().nullable().optional()
750
- });
751
- var variableTagSchema = z.object({
752
- _id: z.string(),
753
- name: z.string(),
754
- color: z.string()
755
- });
756
- var variableSchema = z.object({
757
- _id: z.string(),
758
- key: z.string(),
759
- value: z.string(),
760
- environment: z.enum(["development", "staging", "production"]),
761
- projectId: z.string(),
762
- description: z.string().optional(),
763
- isSensitive: z.boolean().optional(),
764
- version: z.number().optional(),
765
- tags: z.array(variableTagSchema).optional()
766
- });
767
- var environmentSchema = z.enum([
768
- "development",
769
- "staging",
770
- "production"
771
- ]);
772
- var cliConfigSchema = z.object({
773
- apiUrl: z.string().url(),
774
- accessToken: z.string().optional(),
775
- refreshToken: z.string().optional(),
776
- activeProjectId: z.string().optional(),
777
- activeOrganizationId: z.string().optional(),
778
- user: userSchema.optional(),
779
- role: z.enum(["admin", "team_lead", "member"]).optional()
780
- });
781
- var projectConfigSchema = z.object({
782
- projectId: z.string(),
783
- organizationId: z.string(),
784
- environment: environmentSchema.default("development")
785
- });
786
- var projectEntrySchema = z.object({
787
- projectId: z.string(),
788
- organizationId: z.string(),
789
- projectName: z.string().default(""),
790
- organizationName: z.string().default(""),
791
- environment: environmentSchema.default("development")
792
- });
793
- var projectConfigV2Schema = z.object({
794
- version: z.literal(1),
795
- activeProjectId: z.string(),
796
- projects: z.array(projectEntrySchema).min(1)
797
- });
798
-
799
- // src/lib/project-config.ts
800
- var CONFIG_FILE_NAME = ".envpilot";
801
- function getProjectConfigPath(directory = process.cwd()) {
802
- return join(directory, CONFIG_FILE_NAME);
803
- }
804
- function hasProjectConfig(directory = process.cwd()) {
805
- return existsSync(getProjectConfigPath(directory));
806
- }
807
- function readRawConfig(directory = process.cwd()) {
808
- const configPath = getProjectConfigPath(directory);
809
- if (!existsSync(configPath)) return null;
810
- try {
811
- return JSON.parse(readFileSync(configPath, "utf-8"));
812
- } catch {
813
- return null;
814
- }
815
- }
816
- function migrateV1toV2(v1) {
817
- return {
818
- version: 1,
819
- activeProjectId: v1.projectId,
820
- projects: [
821
- {
822
- projectId: v1.projectId,
823
- organizationId: v1.organizationId,
824
- projectName: "",
825
- organizationName: "",
826
- environment: v1.environment
827
- }
828
- ]
829
- };
830
- }
831
- function readProjectConfigV2(directory = process.cwd()) {
832
- const raw = readRawConfig(directory);
833
- if (!raw || typeof raw !== "object") return null;
834
- const rawVersion = raw.version;
835
- if (rawVersion === 1 || rawVersion === 2) {
836
- try {
837
- const normalized = { ...raw, version: 1 };
838
- const parsed = projectConfigV2Schema.parse(normalized);
839
- if (rawVersion === 2) {
840
- writeProjectConfigV2(parsed, directory);
841
- }
842
- return parsed;
843
- } catch {
844
- return null;
845
- }
846
- }
847
- try {
848
- const v1 = projectConfigSchema.parse(raw);
849
- const v2 = migrateV1toV2(v1);
850
- writeProjectConfigV2(v2, directory);
851
- return v2;
852
- } catch {
853
- return null;
854
- }
855
- }
856
- function writeProjectConfigV2(config2, directory = process.cwd()) {
857
- const configPath = getProjectConfigPath(directory);
858
- writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
859
- }
860
- function getActiveProject(config2) {
861
- return config2.projects.find((p) => p.projectId === config2.activeProjectId) || config2.projects[0] || null;
862
- }
863
- function resolveProject(config2, identifier) {
864
- if (!identifier) return getActiveProject(config2);
865
- return config2.projects.find(
866
- (p) => p.projectId === identifier || p.projectName.toLowerCase() === identifier.toLowerCase()
867
- ) || null;
868
- }
869
- function addProjectToConfig(config2, entry) {
870
- if (config2.projects.some((p) => p.projectId === entry.projectId)) {
871
- throw new Error("Project already linked");
872
- }
873
- return { ...config2, projects: [...config2.projects, entry] };
874
- }
875
- function removeProjectFromConfig(config2, projectId) {
876
- const filtered = config2.projects.filter((p) => p.projectId !== projectId);
877
- if (filtered.length === 0) return null;
878
- const activeId = config2.activeProjectId === projectId ? filtered[0].projectId : config2.activeProjectId;
879
- return { ...config2, activeProjectId: activeId, projects: filtered };
880
- }
881
- function setActiveProjectInConfig(config2, projectId) {
882
- if (!config2.projects.some((p) => p.projectId === projectId)) {
883
- throw new Error("Project not found in config");
884
- }
885
- return { ...config2, activeProjectId: projectId };
886
- }
887
- function updateProjectInConfig(config2, projectId, updates) {
888
- return {
889
- ...config2,
890
- projects: config2.projects.map(
891
- (p) => p.projectId === projectId ? { ...p, ...updates } : p
892
- )
893
- };
894
- }
895
- function readProjectConfig(directory = process.cwd()) {
896
- const v2 = readProjectConfigV2(directory);
897
- if (!v2) return null;
898
- const active = getActiveProject(v2);
899
- if (!active) return null;
900
- return {
901
- projectId: active.projectId,
902
- organizationId: active.organizationId,
903
- environment: active.environment
904
- };
905
- }
906
- function writeProjectConfig(config2, directory = process.cwd()) {
907
- const existing = readProjectConfigV2(directory);
908
- if (existing) {
909
- const updated = updateProjectInConfig(existing, existing.activeProjectId, {
910
- projectId: config2.projectId,
911
- organizationId: config2.organizationId,
912
- environment: config2.environment
913
- });
914
- writeProjectConfigV2(
915
- { ...updated, activeProjectId: config2.projectId },
916
- directory
917
- );
918
- } else {
919
- writeProjectConfigV2(
920
- {
921
- version: 1,
922
- activeProjectId: config2.projectId,
923
- projects: [
924
- {
925
- projectId: config2.projectId,
926
- organizationId: config2.organizationId,
927
- projectName: "",
928
- organizationName: "",
929
- environment: config2.environment
930
- }
931
- ]
932
- },
933
- directory
934
- );
935
- }
936
- }
937
- function updateProjectConfig(updates, directory = process.cwd()) {
938
- const v2 = readProjectConfigV2(directory);
939
- if (!v2) {
940
- throw new Error("No project config found. Run `envpilot init` first.");
941
- }
942
- const active = getActiveProject(v2);
943
- if (!active) {
944
- throw new Error("No active project found.");
945
- }
946
- const updated = updateProjectInConfig(v2, active.projectId, updates);
947
- writeProjectConfigV2(updated, directory);
948
- }
949
- function deleteProjectConfig(directory = process.cwd()) {
950
- const configPath = getProjectConfigPath(directory);
951
- if (!existsSync(configPath)) return false;
952
- unlinkSync(configPath);
953
- return true;
954
- }
955
- function ensureEnvInGitignore(directory = process.cwd()) {
956
- const gitignorePath = join(directory, ".gitignore");
957
- if (!existsSync(gitignorePath)) {
958
- writeFileSync(gitignorePath, ".env\n.env.local\n", "utf-8");
959
- return;
960
- }
961
- const content = readFileSync(gitignorePath, "utf-8");
962
- const lines = content.split("\n");
963
- if (lines.some((line) => line.trim() === ".env")) {
964
- return;
965
- }
966
- const newContent = content.endsWith("\n") ? content + ".env\n" : content + "\n.env\n";
967
- writeFileSync(gitignorePath, newContent, "utf-8");
968
- }
969
- function getTrackedEnvFiles(directory = process.cwd()) {
970
- try {
971
- const result = execSync("git ls-files --cached .env .env.* .env.local", {
972
- cwd: directory,
973
- encoding: "utf-8",
974
- stdio: ["pipe", "pipe", "pipe"]
975
- });
976
- return result.trim().split("\n").filter((f) => f.length > 0);
977
- } catch {
978
- return [];
979
- }
980
- }
981
-
982
- // src/lib/env-file.ts
983
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, chmodSync } from "fs";
984
- import { join as join2 } from "path";
985
- function parseEnvFile(content) {
986
- const result = {};
987
- const lines = content.split("\n");
988
- for (const line of lines) {
989
- const trimmed = line.trim();
990
- if (!trimmed || trimmed.startsWith("#")) {
991
- continue;
992
- }
993
- const equalsIndex = line.indexOf("=");
994
- if (equalsIndex === -1) {
995
- continue;
996
- }
997
- const key = line.substring(0, equalsIndex).trim();
998
- let value = line.substring(equalsIndex + 1);
999
- value = parseValue(value);
1000
- if (isValidEnvKey(key)) {
1001
- result[key] = value;
1002
- }
1003
- }
1004
- return result;
1005
- }
1006
- function parseValue(value) {
1007
- value = value.trim();
1008
- if (value.startsWith('"') && value.endsWith('"')) {
1009
- value = value.slice(1, -1);
1010
- value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
1011
- } else if (value.startsWith("'") && value.endsWith("'")) {
1012
- value = value.slice(1, -1);
1013
- } else {
1014
- const commentIndex = value.indexOf(" #");
1015
- if (commentIndex !== -1) {
1016
- value = value.substring(0, commentIndex).trim();
1017
- }
1018
- }
1019
- return value;
1020
- }
1021
- function isValidEnvKey(key) {
1022
- return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key);
1023
- }
1024
- function stringifyEnv(vars, options) {
1025
- let keys = Object.keys(vars);
1026
- if (options?.sort) {
1027
- keys = keys.sort();
1028
- }
1029
- const lines = [];
1030
- for (const key of keys) {
1031
- const value = vars[key];
1032
- if (options?.comments?.[key]) {
1033
- lines.push(`# ${options.comments[key]}`);
1034
- }
1035
- const formattedValue = formatValue(value);
1036
- lines.push(`${key}=${formattedValue}`);
1037
- }
1038
- return lines.join("\n") + "\n";
1039
- }
1040
- function formatValue(value) {
1041
- const needsQuotes = value.includes("\n") || value.includes("\r") || value.includes('"') || value.includes("'") || value.includes(" ") || value.includes("#") || value.startsWith(" ") || value.endsWith(" ");
1042
- if (!needsQuotes) {
1043
- return value;
1044
- }
1045
- const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
1046
- return `"${escaped}"`;
1047
- }
1048
- function diffEnvVars(local, remote) {
1049
- const added = {};
1050
- const removed = {};
1051
- const changed = {};
1052
- const unchanged = [];
1053
- for (const [key, value] of Object.entries(local)) {
1054
- if (!(key in remote)) {
1055
- added[key] = value;
1056
- } else if (remote[key] !== value) {
1057
- changed[key] = { local: value, remote: remote[key] };
1058
- } else {
1059
- unchanged.push(key);
1060
- }
1061
- }
1062
- for (const [key, value] of Object.entries(remote)) {
1063
- if (!(key in local)) {
1064
- removed[key] = value;
1065
- }
1066
- }
1067
- return { added, removed, changed, unchanged };
1068
- }
1069
- function readEnvFile(filePath) {
1070
- if (!existsSync2(filePath)) {
1071
- return null;
1072
- }
1073
- const content = readFileSync2(filePath, "utf-8");
1074
- return parseEnvFile(content);
1075
- }
1076
- function writeEnvFile(filePath, vars, options) {
1077
- const content = stringifyEnv(vars, options);
1078
- writeFileSync2(filePath, content, "utf-8");
1079
- }
1080
- function getEnvPathForEnvironment(environment, directory = process.cwd()) {
1081
- if (environment === "development") {
1082
- return join2(directory, ".env.local");
1083
- }
1084
- return join2(directory, `.env.${environment}`);
1085
- }
1086
- function applyFileProtection(filePath, role, projectRole) {
1087
- if (!existsSync2(filePath)) return;
1088
- const isWritable = role === "admin" || role === "team_lead" || projectRole === "manager";
1089
- if (isWritable) {
1090
- chmodSync(filePath, 420);
1091
- } else {
1092
- chmodSync(filePath, 292);
1093
- }
1094
- }
1095
-
1096
- // src/commands/init.ts
1097
- var initCommand = new Command2("init").description("Initialize Envpilot in the current directory").option("-o, --organization <id>", "Organization ID").option("-p, --project <id>", "Project ID").option(
1098
- "-e, --environment <env>",
1099
- "Default environment (development, staging, production)"
1100
- ).option("-f, --force", "Overwrite existing configuration").option("--add", "Add another project to existing config").action(async (options) => {
1101
- try {
1102
- if (!isAuthenticated()) {
1103
- throw notAuthenticated();
1104
- }
1105
- const existingConfig = readProjectConfigV2();
1106
- if (options.add) {
1107
- if (!existingConfig) {
1108
- error("No existing config. Run `envpilot init` first.");
1109
- process.exit(1);
1110
- }
1111
- await addProject(existingConfig, options);
1112
- return;
1113
- }
1114
- if (hasProjectConfig() && !options.force) {
1115
- warning("This directory is already initialized with Envpilot.");
1116
- const { proceed } = await inquirer.prompt([
1117
- {
1118
- type: "confirm",
1119
- name: "proceed",
1120
- message: "Do you want to reinitialize?",
1121
- default: false
1122
- }
1123
- ]);
1124
- if (!proceed) {
1125
- info("Initialization cancelled.");
1126
- return;
1127
- }
1128
- }
1129
- const { selectedOrg, selectedProject, selectedEnvironment } = await selectOrgProjectEnv(options);
1130
- writeProjectConfigV2({
1131
- version: 1,
1132
- activeProjectId: selectedProject._id,
1133
- projects: [
1134
- {
1135
- projectId: selectedProject._id,
1136
- organizationId: selectedOrg._id,
1137
- projectName: selectedProject.name,
1138
- organizationName: selectedOrg.name,
1139
- environment: selectedEnvironment
1140
- }
1141
- ]
1142
- });
1143
- setActiveOrganizationId(selectedOrg._id);
1144
- setActiveProjectId(selectedProject._id);
1145
- if (selectedOrg.role) {
1146
- setRole(selectedOrg.role);
1147
- }
1148
- ensureEnvInGitignore();
1149
- warnTrackedFiles();
1150
- console.log();
1151
- success("Project initialized!");
1152
- printPostInit(selectedOrg, selectedProject);
1153
- } catch (err) {
1154
- await handleError(err);
1155
- }
1156
- });
1157
- async function addProject(existingConfig, options) {
1158
- const api = createAPIClient();
1159
- let role = getRole();
1160
- if (role !== "admin" && role !== "team_lead") {
1161
- const orgs = await withSpinner("Checking permissions...", async () => {
1162
- const response = await api.get("/api/cli/organizations");
1163
- return response.data || [];
1164
- });
1165
- const freshRole = orgs.find(
1166
- (o) => o._id === existingConfig.projects[0]?.organizationId
1167
- )?.role;
1168
- if (freshRole) {
1169
- setRole(freshRole);
1170
- role = freshRole;
1171
- }
1172
- if (role !== "admin" && role !== "team_lead") {
1173
- error("Only admins and team leads can link multiple projects.");
1174
- info("Unlink the current project first with `envpilot unlink`.");
1175
- process.exit(1);
1176
- }
1177
- }
1178
- const { selectedOrg, selectedProject, selectedEnvironment } = await selectOrgProjectEnv(options);
1179
- if (existingConfig.projects.some((p) => p.projectId === selectedProject._id)) {
1180
- error(`"${selectedProject.name}" is already linked.`);
1181
- process.exit(1);
1182
- }
1183
- const envFile = getEnvPathForEnvironment(selectedEnvironment);
1184
- const conflicting = existingConfig.projects.find(
1185
- (p) => p.environment === selectedEnvironment
1186
- );
1187
- if (conflicting) {
1188
- warning(
1189
- `"${conflicting.projectName || conflicting.projectId}" already syncs to ${envFile} (${selectedEnvironment}). Both projects will write to the same file \u2014 consider using a different environment.`
1190
- );
1191
- }
1192
- const newEntry = {
1193
- projectId: selectedProject._id,
1194
- organizationId: selectedOrg._id,
1195
- projectName: selectedProject.name,
1196
- organizationName: selectedOrg.name,
1197
- environment: selectedEnvironment
1198
- };
1199
- let updatedConfig = addProjectToConfig(existingConfig, newEntry);
1200
- const { setActive } = await inquirer.prompt([
1201
- {
1202
- type: "confirm",
1203
- name: "setActive",
1204
- message: `Set "${selectedProject.name}" as the active project?`,
1205
- default: false
1206
- }
1207
- ]);
1208
- if (setActive) {
1209
- updatedConfig = {
1210
- ...updatedConfig,
1211
- activeProjectId: selectedProject._id
1212
- };
1213
- setActiveProjectId(selectedProject._id);
1214
- setActiveOrganizationId(selectedOrg._id);
1215
- }
1216
- updatedConfig = backfillNames(updatedConfig);
1217
- writeProjectConfigV2(updatedConfig);
1218
- console.log();
1219
- success(`Added "${selectedProject.name}" to linked projects!`);
1220
- console.log(
1221
- chalk4.dim(` ${existingConfig.projects.length + 1} projects now linked`)
1222
- );
1223
- console.log();
1224
- console.log("Next steps:");
1225
- console.log(
1226
- ` ${chalk4.cyan("envpilot pull --all")} Pull all projects`
1227
- );
1228
- console.log(
1229
- ` ${chalk4.cyan(`envpilot pull --project "${selectedProject.name}"`)} Pull this project`
1230
- );
1231
- console.log(
1232
- ` ${chalk4.cyan("envpilot list linked")} See all linked projects`
1233
- );
1234
- console.log();
1235
- }
1236
- async function selectOrgProjectEnv(options) {
1237
- const api = createAPIClient();
1238
- const organizations = await withSpinner(
1239
- "Fetching organizations...",
1240
- async () => {
1241
- const response = await api.get("/api/cli/organizations");
1242
- return response.data || [];
1243
- }
1244
- );
1245
- if (organizations.length === 0) {
1246
- error("No organizations found. Please create an organization first.");
1247
- process.exit(1);
1248
- }
1249
- let selectedOrg;
1250
- if (options.organization) {
1251
- const org = organizations.find(
1252
- (o) => o._id === options.organization || o.slug === options.organization
1253
- );
1254
- if (!org) {
1255
- error(`Organization not found: ${options.organization}`);
1256
- process.exit(1);
1257
- }
1258
- selectedOrg = org;
1259
- } else if (organizations.length === 1) {
1260
- selectedOrg = organizations[0];
1261
- info(`Using organization: ${chalk4.bold(selectedOrg.name)}`);
1262
- } else {
1263
- const { orgId } = await inquirer.prompt([
1264
- {
1265
- type: "list",
1266
- name: "orgId",
1267
- message: "Select an organization:",
1268
- choices: organizations.map((org) => ({
1269
- name: `${org.name} ${org.tier === "pro" ? chalk4.green("(Pro)") : chalk4.dim("(Free)")}`,
1270
- value: org._id
1271
- }))
1272
- }
1273
- ]);
1274
- selectedOrg = organizations.find((o) => o._id === orgId);
1275
- }
1276
- const projects = await withSpinner("Fetching projects...", async () => {
1277
- const response = await api.get(
1278
- "/api/cli/projects",
1279
- { organizationId: selectedOrg._id }
1280
- );
1281
- return response.data || [];
1282
- });
1283
- if (projects.length === 0) {
1284
- error("No projects found. Please create a project first.");
1285
- process.exit(1);
1286
- }
1287
- let selectedProject;
1288
- if (options.project) {
1289
- const project = projects.find(
1290
- (p) => p._id === options.project || p.slug === options.project
1291
- );
1292
- if (!project) {
1293
- error(`Project not found: ${options.project}`);
1294
- process.exit(1);
1295
- }
1296
- selectedProject = project;
1297
- } else if (projects.length === 1) {
1298
- selectedProject = projects[0];
1299
- info(`Using project: ${chalk4.bold(selectedProject.name)}`);
1300
- } else {
1301
- const { projectId } = await inquirer.prompt([
1302
- {
1303
- type: "list",
1304
- name: "projectId",
1305
- message: "Select a project:",
1306
- choices: projects.map((project) => ({
1307
- name: `${project.icon || "\u{1F4E6}"} ${project.name}`,
1308
- value: project._id
1309
- }))
1310
- }
1311
- ]);
1312
- selectedProject = projects.find((p) => p._id === projectId);
1313
- }
1314
- let selectedEnvironment = "development";
1315
- if (options.environment) {
1316
- if (!["development", "staging", "production"].includes(options.environment)) {
1317
- error(
1318
- "Invalid environment. Must be: development, staging, or production"
1319
- );
1320
- process.exit(1);
1321
- }
1322
- selectedEnvironment = options.environment;
1323
- } else {
1324
- const { environment } = await inquirer.prompt([
1325
- {
1326
- type: "list",
1327
- name: "environment",
1328
- message: "Select default environment:",
1329
- choices: [
1330
- { name: "Development", value: "development" },
1331
- { name: "Staging", value: "staging" },
1332
- { name: "Production", value: "production" }
1333
- ],
1334
- default: "development"
1335
- }
1336
- ]);
1337
- selectedEnvironment = environment;
1338
- }
1339
- return { selectedOrg, selectedProject, selectedEnvironment };
1340
- }
1341
- function backfillNames(config2) {
1342
- return config2;
1343
- }
1344
- function warnTrackedFiles() {
1345
- const trackedFiles = getTrackedEnvFiles();
1346
- if (trackedFiles.length > 0) {
1347
- console.log();
1348
- warning("Security risk: .env files are tracked by git!");
1349
- for (const file of trackedFiles) {
1350
- console.log(chalk4.red(` tracked: ${file}`));
1351
- }
1352
- console.log();
1353
- console.log(
1354
- chalk4.yellow(
1355
- " Run the following to untrack them (without deleting the files):"
1356
- )
1357
- );
1358
- for (const file of trackedFiles) {
1359
- console.log(chalk4.cyan(` git rm --cached ${file}`));
1360
- }
1361
- }
1362
- }
1363
- function printPostInit(selectedOrg, selectedProject) {
1364
- console.log();
1365
- console.log(chalk4.dim("Configuration saved to .envpilot"));
1366
- if (selectedOrg.role) {
1367
- console.log(chalk4.dim(` Org role: ${formatRole(selectedOrg.role)}`));
1368
- roleNotice(selectedOrg.role);
1369
- }
1370
- if (selectedProject.projectRole) {
1371
- console.log(
1372
- chalk4.dim(
1373
- ` Project role: ${formatProjectRole(selectedProject.projectRole)}`
1374
- )
1375
- );
1376
- projectRoleNotice(selectedProject.projectRole);
1377
- }
1378
- console.log();
1379
- console.log("Next steps:");
1380
- console.log(
1381
- ` ${chalk4.cyan("envpilot pull")} Download environment variables`
1382
- );
1383
- console.log(
1384
- ` ${chalk4.cyan("envpilot push")} Upload local .env to cloud`
1385
- );
1386
- console.log();
1387
- }
1388
-
1389
- // src/commands/pull.ts
1390
- import { Command as Command3 } from "commander";
1391
- import chalk5 from "chalk";
1392
- import inquirer2 from "inquirer";
1393
-
1394
- // src/lib/format-converter.ts
1395
- var ALL_FORMATS = [
1396
- "env",
1397
- "json",
1398
- "yaml",
1399
- "docker-compose",
1400
- "aws",
1401
- "vercel",
1402
- "netlify"
1403
- ];
1404
- function serialize(vars, format, meta) {
1405
- switch (format) {
1406
- case "env":
1407
- return serializeEnv(vars, meta);
1408
- case "json":
1409
- return serializeJson(vars);
1410
- case "yaml":
1411
- return serializeYaml(vars, meta);
1412
- case "docker-compose":
1413
- return serializeDockerCompose(vars, meta);
1414
- case "aws":
1415
- return serializeAws(vars, meta);
1416
- case "vercel":
1417
- return serializeVercel(vars);
1418
- case "netlify":
1419
- return serializeNetlify(vars, meta);
1420
- }
1421
- }
1422
- function parse(content, format, options) {
1423
- switch (format) {
1424
- case "env":
1425
- return parseEnvFile(content);
1426
- case "json":
1427
- return parseJson(content);
1428
- case "yaml":
1429
- return parseYaml(content);
1430
- case "docker-compose":
1431
- return parseDockerCompose(content);
1432
- case "aws":
1433
- return parseAws(content, options?.prefix);
1434
- case "vercel":
1435
- return parseVercel(content);
1436
- case "netlify":
1437
- return parseNetlify(content);
1438
- }
1439
- }
1440
- function getDefaultFilename(format, environment) {
1441
- const env = environment || "development";
1442
- switch (format) {
1443
- case "env":
1444
- return env === "development" ? ".env.local" : `.env.${env}`;
1445
- case "json":
1446
- return `${env}.json`;
1447
- case "yaml":
1448
- return `${env}.yaml`;
1449
- case "docker-compose":
1450
- return "docker-compose.yml";
1451
- case "aws":
1452
- return `${env}.aws.json`;
1453
- case "vercel":
1454
- return `${env}.vercel.json`;
1455
- case "netlify":
1456
- return "netlify.toml";
1457
- }
1458
- }
1459
- function serializeEnv(vars, meta) {
1460
- const lines = [];
1461
- if (meta?.environment) {
1462
- lines.push(`# Environment: ${meta.environment}`);
1463
- }
1464
- if (meta?.projectName) {
1465
- lines.push(`# Project: ${meta.projectName}`);
1466
- }
1467
- if (lines.length > 0) {
1468
- lines.push(`# Exported: ${(/* @__PURE__ */ new Date()).toISOString()}`);
1469
- lines.push("");
1470
- }
1471
- return lines.join("\n") + stringifyEnv(vars, { sort: true });
1472
- }
1473
- function serializeJson(vars) {
1474
- const sorted = Object.keys(vars).sort().reduce(
1475
- (obj, key) => {
1476
- obj[key] = vars[key];
1477
- return obj;
1478
- },
1479
- {}
1480
- );
1481
- return JSON.stringify(sorted, null, 2) + "\n";
1482
- }
1483
- function parseJson(content) {
1484
- const data = JSON.parse(content);
1485
- if (typeof data !== "object" || data === null || Array.isArray(data)) {
1486
- throw new Error("Expected a JSON object with string key-value pairs");
1487
- }
1488
- const result = {};
1489
- for (const [key, value] of Object.entries(data)) {
1490
- if (typeof value === "string") {
1491
- result[key] = value;
1492
- } else if (value !== null && value !== void 0) {
1493
- result[key] = String(value);
1494
- }
1495
- }
1496
- return result;
1497
- }
1498
- function yamlQuote(value) {
1499
- if (value === "" || value === "true" || value === "false" || value === "null" || value === "~" || /^[0-9]/.test(value) || /[:#\[\]{}&*!|>'"%@`]/.test(value) || value.includes("\n") || value.startsWith(" ") || value.endsWith(" ")) {
1500
- const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
1501
- return `"${escaped}"`;
1502
- }
1503
- return value;
1504
- }
1505
- function serializeYaml(vars, meta) {
1506
- const lines = [];
1507
- if (meta?.environment) {
1508
- lines.push(`# Environment: ${meta.environment}`);
1509
- }
1510
- if (meta?.projectName) {
1511
- lines.push(`# Project: ${meta.projectName}`);
1512
- }
1513
- if (lines.length > 0) {
1514
- lines.push("");
1515
- }
1516
- for (const key of Object.keys(vars).sort()) {
1517
- lines.push(`${key}: ${yamlQuote(vars[key])}`);
1518
- }
1519
- return lines.join("\n") + "\n";
1520
- }
1521
- function parseYaml(content) {
1522
- const result = {};
1523
- for (const line of content.split("\n")) {
1524
- const trimmed = line.trim();
1525
- if (!trimmed || trimmed.startsWith("#")) continue;
1526
- const colonIndex = trimmed.indexOf(": ");
1527
- if (colonIndex === -1) {
1528
- if (trimmed.endsWith(":")) {
1529
- const key2 = trimmed.slice(0, -1).trim();
1530
- if (key2 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(key2)) {
1531
- result[key2] = "";
1532
- }
1533
- }
1534
- continue;
1535
- }
1536
- const key = trimmed.substring(0, colonIndex).trim();
1537
- let value = trimmed.substring(colonIndex + 2).trim();
1538
- if (line.startsWith(" ") || line.startsWith(" ")) continue;
1539
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1540
- value = value.slice(1, -1);
1541
- if (value.includes("\\")) {
1542
- value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
1543
- }
1544
- }
1545
- if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
1546
- result[key] = value;
1547
- }
1548
- }
1549
- return result;
1550
- }
1551
- function serializeDockerCompose(vars, meta) {
1552
- const lines = [];
1553
- if (meta?.environment || meta?.projectName) {
1554
- lines.push("# Docker Compose environment variables");
1555
- if (meta.projectName) lines.push(`# Project: ${meta.projectName}`);
1556
- if (meta.environment) lines.push(`# Environment: ${meta.environment}`);
1557
- lines.push("");
1558
- }
1559
- lines.push("services:");
1560
- lines.push(" app:");
1561
- lines.push(" environment:");
1562
- for (const key of Object.keys(vars).sort()) {
1563
- lines.push(` ${key}: ${yamlQuote(vars[key])}`);
1564
- }
1565
- return lines.join("\n") + "\n";
1566
- }
1567
- function parseDockerCompose(content) {
1568
- const result = {};
1569
- const lines = content.split("\n");
1570
- let inEnvironment = false;
1571
- let environmentIndent = -1;
1572
- for (const line of lines) {
1573
- const trimmed = line.trim();
1574
- if (!trimmed || trimmed.startsWith("#")) continue;
1575
- const indent = line.length - line.trimStart().length;
1576
- if (trimmed === "environment:") {
1577
- inEnvironment = true;
1578
- environmentIndent = indent;
1579
- continue;
1580
- }
1581
- if (inEnvironment) {
1582
- if (indent <= environmentIndent && trimmed !== "") {
1583
- inEnvironment = false;
1584
- environmentIndent = -1;
1585
- continue;
1586
- }
1587
- if (trimmed.startsWith("- ")) {
1588
- const entry = trimmed.slice(2);
1589
- const eqIdx = entry.indexOf("=");
1590
- if (eqIdx !== -1) {
1591
- const key = entry.substring(0, eqIdx).trim();
1592
- let value = entry.substring(eqIdx + 1).trim();
1593
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1594
- value = value.slice(1, -1);
1595
- }
1596
- if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
1597
- result[key] = value;
1598
- }
1599
- }
1600
- continue;
1601
- }
1602
- const colonIndex = trimmed.indexOf(": ");
1603
- if (colonIndex !== -1) {
1604
- const key = trimmed.substring(0, colonIndex).trim();
1605
- let value = trimmed.substring(colonIndex + 2).trim();
1606
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1607
- value = value.slice(1, -1);
1608
- if (value.includes("\\")) {
1609
- value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
1610
- }
1611
- }
1612
- if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
1613
- result[key] = value;
1614
- }
1615
- } else if (trimmed.endsWith(":")) {
1616
- const key = trimmed.slice(0, -1).trim();
1617
- if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
1618
- result[key] = "";
1619
- }
1620
- }
1621
- }
1622
- }
1623
- return result;
1624
- }
1625
- function serializeAws(vars, meta) {
1626
- const prefix = meta?.prefix || `/${meta?.projectName || "app"}`;
1627
- const parameters = Object.keys(vars).sort().map((key) => ({
1628
- name: `${prefix}/${key}`,
1629
- value: vars[key],
1630
- type: "SecureString"
1631
- }));
1632
- return JSON.stringify({ parameters }, null, 2) + "\n";
1633
- }
1634
- function parseAws(content, prefix) {
1635
- const data = JSON.parse(content);
1636
- const result = {};
1637
- const params = data.parameters || data.Parameters || [];
1638
- if (!Array.isArray(params)) {
1639
- throw new Error(
1640
- "Expected AWS Parameter Store format with 'parameters' array"
1641
- );
1642
- }
1643
- for (const param of params) {
1644
- const name = param.name || param.Name || "";
1645
- const value = param.value || param.Value || "";
1646
- let key;
1647
- if (prefix && name.startsWith(prefix + "/")) {
1648
- key = name.substring(prefix.length + 1);
1649
- } else {
1650
- const lastSlash = name.lastIndexOf("/");
1651
- key = lastSlash !== -1 ? name.substring(lastSlash + 1) : name;
1652
- }
1653
- if (key && /^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
1654
- result[key] = value;
1655
- }
1656
- }
1657
- return result;
1658
- }
1659
- function serializeVercel(vars) {
1660
- const sorted = Object.keys(vars).sort().reduce(
1661
- (obj, key) => {
1662
- obj[key] = vars[key];
1663
- return obj;
1664
- },
1665
- {}
1666
- );
1667
- return JSON.stringify({ env: sorted }, null, 2) + "\n";
1668
- }
1669
- function parseVercel(content) {
1670
- const data = JSON.parse(content);
1671
- const result = {};
1672
- const env = data.env;
1673
- if (!env || typeof env !== "object" || Array.isArray(env)) {
1674
- throw new Error("Expected Vercel format with 'env' object");
1675
- }
1676
- for (const [key, value] of Object.entries(env)) {
1677
- if (typeof value === "string") {
1678
- result[key] = value;
1679
- } else if (value !== null && value !== void 0) {
1680
- result[key] = String(value);
1681
- }
1682
- }
1683
- return result;
1684
- }
1685
- function tomlQuote(value) {
1686
- const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
1687
- return `"${escaped}"`;
1688
- }
1689
- function serializeNetlify(vars, meta) {
1690
- const lines = [];
1691
- if (meta?.environment || meta?.projectName) {
1692
- if (meta.projectName) lines.push(`# Project: ${meta.projectName}`);
1693
- if (meta.environment) lines.push(`# Environment: ${meta.environment}`);
1694
- lines.push("");
1695
- }
1696
- lines.push("[build.environment]");
1697
- for (const key of Object.keys(vars).sort()) {
1698
- lines.push(` ${key} = ${tomlQuote(vars[key])}`);
1699
- }
1700
- return lines.join("\n") + "\n";
1701
- }
1702
- function parseNetlify(content) {
1703
- const result = {};
1704
- const lines = content.split("\n");
1705
- let inBuildEnv = false;
1706
- for (const line of lines) {
1707
- const trimmed = line.trim();
1708
- if (trimmed.startsWith("[")) {
1709
- inBuildEnv = trimmed === "[build.environment]" || trimmed === "[build.environment]";
1710
- continue;
1711
- }
1712
- if (!inBuildEnv) continue;
1713
- if (!trimmed || trimmed.startsWith("#")) continue;
1714
- const eqIndex = trimmed.indexOf("=");
1715
- if (eqIndex === -1) continue;
1716
- const key = trimmed.substring(0, eqIndex).trim();
1717
- let value = trimmed.substring(eqIndex + 1).trim();
1718
- if (value.startsWith('"') && value.endsWith('"')) {
1719
- value = value.slice(1, -1);
1720
- value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
1721
- } else if (value.startsWith("'") && value.endsWith("'")) {
1722
- value = value.slice(1, -1);
1723
- }
1724
- if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
1725
- result[key] = value;
1726
- }
1727
- }
1728
- return result;
1729
- }
1730
-
1731
- // src/commands/pull.ts
1732
- var pullCommand = new Command3("pull").description("Download environment variables to local .env file").option(
1733
- "-e, --env <environment>",
1734
- "Environment (development, staging, production)"
1735
- ).option("-f, --file <path>", "Output file path (default: .env)").option("--force", "Overwrite without confirmation").option(
1736
- "--format <format>",
1737
- "Output format: env, json, yaml, docker-compose, aws, vercel, netlify",
1738
- "env"
1739
- ).option(
1740
- "--prefix <prefix>",
1741
- "AWS Parameter Store path prefix (default: /project-name)"
1742
- ).option("--dry-run", "Show what would be downloaded without writing").option("--project <name-or-id>", "Pull a specific linked project").option("--all", "Pull all linked projects").action(async (options) => {
1743
- try {
1744
- if (!isAuthenticated()) {
1745
- throw notAuthenticated();
1746
- }
1747
- if (options.all) {
1748
- if (options.env || options.file) {
1749
- error("Cannot use --env or --file with --all.");
1750
- process.exit(1);
1751
- }
1752
- await pullAllProjects(options);
1753
- return;
1754
- }
1755
- if (options.project) {
1756
- const configV2 = readProjectConfigV2();
1757
- if (!configV2) throw notInitialized();
1758
- const project = resolveProject(configV2, options.project);
1759
- if (!project) {
1760
- error(`Project not found: ${options.project}`);
1761
- console.log();
1762
- console.log("Linked projects:");
1763
- for (const p of configV2.projects) {
1764
- console.log(` ${p.projectName || p.projectId} (${p.environment})`);
1765
- }
1766
- process.exit(1);
1767
- }
1768
- await pullSingleProject(project, options);
1769
- return;
1770
- }
1771
- const projectConfig = readProjectConfig();
1772
- if (!projectConfig) throw notInitialized();
1773
- checkTrackedFiles();
1774
- const environment = options.env || projectConfig.environment || "development";
1775
- const fmt = options.format || "env";
1776
- if (!ALL_FORMATS.includes(fmt)) {
1777
- error(`Unknown format: ${fmt}. Supported: ${ALL_FORMATS.join(", ")}`);
1778
- process.exit(1);
1779
- }
1780
- const outputPath = options.file || (fmt === "env" ? getEnvPathForEnvironment(environment) : getDefaultFilename(fmt, environment));
1781
- await pullProject(
1782
- {
1783
- projectId: projectConfig.projectId,
1784
- organizationId: projectConfig.organizationId,
1785
- environment
1786
- },
1787
- outputPath,
1788
- options
1789
- );
1790
- } catch (err) {
1791
- await handleError(err);
1792
- }
1793
- });
1794
- async function pullAllProjects(options) {
1795
- const configV2 = readProjectConfigV2();
1796
- if (!configV2) throw notInitialized();
1797
- checkTrackedFiles();
1798
- let totalPulled = 0;
1799
- let totalFailed = 0;
1800
- for (const project of configV2.projects) {
1801
- const outputPath = getEnvPathForEnvironment(project.environment);
1802
- const displayName = project.projectName || project.projectId;
1803
- console.log();
1804
- console.log(
1805
- chalk5.bold(
1806
- `Pulling "${displayName}" (${project.environment}) \u2192 ${outputPath}`
1807
- )
1808
- );
1809
- try {
1810
- await pullProject(
1811
- {
1812
- projectId: project.projectId,
1813
- organizationId: project.organizationId,
1814
- environment: project.environment
1815
- },
1816
- outputPath,
1817
- options
1818
- );
1819
- totalPulled++;
1820
- } catch (err) {
1821
- totalFailed++;
1822
- if (err instanceof Error) {
1823
- error(` Failed: ${err.message}`);
1824
- }
1825
- }
1826
- }
1827
- console.log();
1828
- if (totalFailed === 0) {
1829
- success(`Pulled ${totalPulled} project${totalPulled !== 1 ? "s" : ""}`);
1830
- } else {
1831
- warning(
1832
- `Pulled ${totalPulled}/${totalPulled + totalFailed} projects. ${totalFailed} failed.`
1833
- );
1834
- }
1835
- }
1836
- async function pullSingleProject(project, options) {
1837
- checkTrackedFiles();
1838
- const environment = options.env || project.environment;
1839
- const fmt = options.format || "env";
1840
- const outputPath = options.file || (fmt === "env" ? getEnvPathForEnvironment(environment) : getDefaultFilename(fmt, environment));
1841
- await pullProject(
1842
- {
1843
- projectId: project.projectId,
1844
- organizationId: project.organizationId,
1845
- environment
1846
- },
1847
- outputPath,
1848
- options
1849
- );
1850
- }
1851
- async function pullProject(project, outputPath, options) {
1852
- const api = createAPIClient();
1853
- let metaProjectRole;
1854
- const variables = await withSpinner(
1855
- `Fetching ${chalk5.bold(project.environment)} variables...`,
1856
- async () => {
1857
- const response = await api.get("/api/cli/variables", {
1858
- projectId: project.projectId,
1859
- environment: project.environment,
1860
- ...project.organizationId && {
1861
- organizationId: project.organizationId
1862
- }
1863
- });
1864
- metaProjectRole = response.meta?.projectRole;
1865
- return response.data || [];
1866
- }
1867
- );
1868
- if (variables.length === 0) {
1869
- warning(`No variables found for ${project.environment} environment.`);
1870
- return;
1871
- }
1872
- const remoteVars = {};
1873
- for (const variable of variables) {
1874
- remoteVars[variable.key] = variable.value;
1875
- }
1876
- const localVars = readEnvFile(outputPath) || {};
1877
- const diffResult = diffEnvVars(remoteVars, localVars);
1878
- const hasChanges = Object.keys(diffResult.added).length > 0 || Object.keys(diffResult.removed).length > 0 || Object.keys(diffResult.changed).length > 0;
1879
- if (!hasChanges) {
1880
- success("Local file is up to date.");
1881
- return;
1882
- }
1883
- console.log();
1884
- console.log(chalk5.bold("Changes:"));
1885
- console.log();
1886
- diff(diffResult.added, diffResult.removed, diffResult.changed);
1887
- console.log();
1888
- if (options.dryRun) {
1889
- info("Dry run - no changes written.");
1890
- return;
1891
- }
1892
- if (!options.force && Object.keys(localVars).length > 0) {
1893
- const { proceed } = await inquirer2.prompt([
1894
- {
1895
- type: "confirm",
1896
- name: "proceed",
1897
- message: `Overwrite ${outputPath}?`,
1898
- default: true
1899
- }
1900
- ]);
1901
- if (!proceed) {
1902
- info("Pull cancelled.");
1903
- return;
1904
- }
1905
- }
1906
- try {
1907
- const fs = await import("fs");
1908
- if (fs.existsSync(outputPath)) {
1909
- fs.chmodSync(outputPath, 420);
1910
- }
1911
- } catch {
1912
- }
1913
- const fmt = options.format || "env";
1914
- if (fmt === "env") {
1915
- const comments = {};
1916
- for (const variable of variables) {
1917
- if (variable.description) {
1918
- comments[variable.key] = variable.description;
1919
- }
1920
- }
1921
- writeEnvFile(outputPath, remoteVars, { sort: true, comments });
1922
- } else {
1923
- const fs = await import("fs");
1924
- const output = serialize(remoteVars, fmt, {
1925
- projectName: project.projectId,
1926
- environment: project.environment,
1927
- prefix: options.prefix
1928
- });
1929
- fs.writeFileSync(outputPath, output, "utf-8");
1930
- }
1931
- const role = getRole();
1932
- applyFileProtection(outputPath, role, metaProjectRole);
1933
- success(
1934
- `Downloaded ${variables.length} variables to ${chalk5.bold(outputPath)}`
1935
- );
1936
- if (metaProjectRole === "viewer") {
1937
- info(
1938
- "You have Viewer access to this project. You may only see variables you have been explicitly granted access to."
1939
- );
1940
- }
1941
- const isProtected = role !== "admin" && role !== "team_lead" && metaProjectRole !== "manager";
1942
- if (isProtected) {
1943
- info(
1944
- `File is read-only (your role: ${role || metaProjectRole || "member"}).`
1945
- );
1946
- }
1947
- console.log();
1948
- console.log(chalk5.dim(` Added: ${Object.keys(diffResult.added).length}`));
1949
- console.log(
1950
- chalk5.dim(` Changed: ${Object.keys(diffResult.changed).length}`)
1951
- );
1952
- console.log(
1953
- chalk5.dim(` Removed: ${Object.keys(diffResult.removed).length}`)
1954
- );
1955
- }
1956
- function checkTrackedFiles() {
1957
- const trackedFiles = getTrackedEnvFiles();
1958
- if (trackedFiles.length > 0) {
1959
- error("Security risk: .env files are tracked by git!");
1960
- console.log();
1961
- for (const file of trackedFiles) {
1962
- console.log(chalk5.red(` tracked: ${file}`));
1963
- }
1964
- console.log();
1965
- console.log(
1966
- chalk5.yellow(
1967
- " Run the following to untrack them (without deleting the files):"
1968
- )
1969
- );
1970
- for (const file of trackedFiles) {
1971
- console.log(chalk5.cyan(` git rm --cached ${file}`));
1972
- }
1973
- console.log();
1974
- process.exit(1);
1975
- }
1976
- }
1977
-
1978
- // src/commands/push.ts
1979
- import { Command as Command4 } from "commander";
1980
- import chalk6 from "chalk";
1981
- import inquirer3 from "inquirer";
1982
-
1983
- // src/lib/validators.ts
1984
- import { z as z2 } from "zod";
1985
- var envKeySchema = z2.string().min(1, "Key cannot be empty").max(256, "Key cannot exceed 256 characters").regex(
1986
- /^[A-Za-z_][A-Za-z0-9_]*$/,
1987
- "Key must start with a letter or underscore, followed by letters, numbers, or underscores"
1988
- );
1989
- var envValueSchema = z2.string().max(65536, "Value cannot exceed 64KB");
1990
- var environmentSchema2 = z2.enum([
1991
- "development",
1992
- "staging",
1993
- "production"
1994
- ]);
1995
- var projectSlugSchema = z2.string().min(1, "Slug cannot be empty").max(128, "Slug cannot exceed 128 characters").regex(
1996
- /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/,
1997
- "Slug must be lowercase alphanumeric with hyphens, cannot start or end with hyphen"
1998
- );
1999
- var urlSchema = z2.string().url("Must be a valid URL");
2000
- var tokenSchema = z2.string().min(1, "Token cannot be empty").regex(/^env_[A-Za-z0-9]{48}$/, "Invalid token format");
2001
- var filePathSchema = z2.string().min(1, "File path cannot be empty");
2002
- function validateEnvVars(vars) {
2003
- const valid = {};
2004
- const invalid = [];
2005
- for (const [key, value] of Object.entries(vars)) {
2006
- const keyResult = envKeySchema.safeParse(key);
2007
- const valueResult = envValueSchema.safeParse(value);
2008
- if (!keyResult.success) {
2009
- invalid.push({ key, error: keyResult.error.issues[0].message });
2010
- continue;
2011
- }
2012
- if (!valueResult.success) {
2013
- invalid.push({ key, error: valueResult.error.issues[0].message });
2014
- continue;
2015
- }
2016
- valid[key] = value;
2017
- }
2018
- return { valid, invalid };
2019
- }
2020
-
2021
- // src/commands/push.ts
2022
- var pushCommand = new Command4("push").description("Upload local .env file to cloud").option(
2023
- "-e, --env <environment>",
2024
- "Target environment (development, staging, production)"
2025
- ).option("-f, --file <path>", "Input file path (default: .env)").option("--merge", "Merge with existing variables (default)").option("--replace", "Replace all existing variables").option(
2026
- "--format <format>",
2027
- "Input format: env, json, yaml, docker-compose, aws, vercel, netlify",
2028
- "env"
2029
- ).option(
2030
- "--prefix <prefix>",
2031
- "AWS Parameter Store path prefix (default: /project-name)"
2032
- ).option("--dry-run", "Show what would be uploaded without making changes").option("--force", "Skip confirmation").option("--project <name-or-id>", "Push to a specific linked project").action(async (options) => {
2033
- try {
2034
- if (!isAuthenticated()) {
2035
- throw notAuthenticated();
2036
- }
2037
- let projectId;
2038
- let organizationId;
2039
- let defaultEnvironment;
2040
- if (options.project) {
2041
- const configV2 = readProjectConfigV2();
2042
- if (!configV2) throw notInitialized();
2043
- const resolved = resolveProject(configV2, options.project);
2044
- if (!resolved) {
2045
- error(`Project not found: ${options.project}`);
2046
- console.log();
2047
- console.log("Linked projects:");
2048
- for (const p of configV2.projects) {
2049
- console.log(` ${p.projectName || p.projectId} (${p.environment})`);
2050
- }
2051
- process.exit(1);
2052
- }
2053
- projectId = resolved.projectId;
2054
- organizationId = resolved.organizationId;
2055
- defaultEnvironment = resolved.environment;
2056
- } else {
2057
- const projectConfig = readProjectConfig();
2058
- if (!projectConfig) throw notInitialized();
2059
- projectId = projectConfig.projectId;
2060
- organizationId = projectConfig.organizationId;
2061
- defaultEnvironment = projectConfig.environment;
2062
- }
2063
- const trackedFiles = getTrackedEnvFiles();
2064
- if (trackedFiles.length > 0) {
2065
- error("Security risk: .env files are tracked by git!");
2066
- console.log();
2067
- for (const file of trackedFiles) {
2068
- console.log(chalk6.red(` tracked: ${file}`));
2069
- }
2070
- console.log();
2071
- console.log(
2072
- chalk6.yellow(
2073
- " Run the following to untrack them (without deleting the files):"
2074
- )
2075
- );
2076
- for (const file of trackedFiles) {
2077
- console.log(chalk6.cyan(` git rm --cached ${file}`));
2078
- }
2079
- console.log();
2080
- process.exit(1);
2081
- }
2082
- const api = createAPIClient();
2083
- const projects = await api.get("/api/cli/projects", { organizationId });
2084
- const currentProject = projects.data?.find((p) => p._id === projectId);
2085
- const projectRole = currentProject?.projectRole;
2086
- if (projectRole === "viewer") {
2087
- error(
2088
- "Unfortunately, as a member, you cannot push in any environment. Please access or request access to your team lead."
2089
- );
2090
- process.exit(1);
2091
- }
2092
- const role = getRole();
2093
- if (role === "member") {
2094
- warning(
2095
- "You have Member access. Push will create pending requests that require Admin or Team Lead approval."
2096
- );
2097
- console.log();
2098
- if (!options.force) {
2099
- const { proceed } = await inquirer3.prompt([
2100
- {
2101
- type: "confirm",
2102
- name: "proceed",
2103
- message: "Continue with creating approval requests?",
2104
- default: true
2105
- }
2106
- ]);
2107
- if (!proceed) {
2108
- info("Push cancelled.");
2109
- return;
2110
- }
2111
- }
2112
- }
2113
- const environment = options.env || defaultEnvironment || "development";
2114
- const fmt = options.format || "env";
2115
- if (!ALL_FORMATS.includes(fmt)) {
2116
- error(`Unknown format: ${fmt}. Supported: ${ALL_FORMATS.join(", ")}`);
2117
- process.exit(1);
2118
- }
2119
- const inputPath = options.file || (fmt === "env" ? getEnvPathForEnvironment(environment) : getDefaultFilename(fmt, environment));
2120
- const mode = options.replace ? "replace" : "merge";
2121
- let localVars;
2122
- if (fmt === "env") {
2123
- localVars = readEnvFile(inputPath);
2124
- } else {
2125
- const { readFileSync: readFileSync4, existsSync: existsSync4 } = await import("fs");
2126
- if (!existsSync4(inputPath)) {
2127
- localVars = null;
2128
- } else {
2129
- const content = readFileSync4(inputPath, "utf-8");
2130
- localVars = parse(content, fmt, { prefix: options.prefix });
2131
- }
2132
- }
2133
- if (!localVars) {
2134
- throw fileNotFound(inputPath);
2135
- }
2136
- if (Object.keys(localVars).length === 0) {
2137
- warning(`No variables found in ${inputPath}`);
2138
- return;
2139
- }
2140
- const { valid, invalid } = validateEnvVars(localVars);
2141
- if (invalid.length > 0) {
2142
- warning("Some variables have invalid keys and will be skipped:");
2143
- for (const { key, error: err } of invalid) {
2144
- console.log(chalk6.red(` ${key}: ${err}`));
2145
- }
2146
- console.log();
2147
- }
2148
- if (Object.keys(valid).length === 0) {
2149
- error("No valid variables to push.");
2150
- return;
2151
- }
2152
- const remoteVariables = await withSpinner(
2153
- "Fetching current variables...",
2154
- async () => {
2155
- const params = {
2156
- projectId,
2157
- environment
2158
- };
2159
- if (organizationId) {
2160
- params.organizationId = organizationId;
2161
- }
2162
- const response = await api.get("/api/cli/variables", params);
2163
- return response.data || [];
2164
- }
2165
- );
2166
- const remoteVars = {};
2167
- for (const variable of remoteVariables) {
2168
- remoteVars[variable.key] = variable.value;
2169
- }
2170
- const diffResult = diffEnvVars(valid, remoteVars);
2171
- const hasChanges = Object.keys(diffResult.added).length > 0 || Object.keys(diffResult.changed).length > 0 || mode === "replace" && Object.keys(diffResult.removed).length > 0;
2172
- if (!hasChanges) {
2173
- success("Remote is up to date.");
2174
- return;
2175
- }
2176
- console.log();
2177
- console.log(chalk6.bold("Changes to push:"));
2178
- console.log();
2179
- const removedToShow = mode === "replace" ? diffResult.removed : {};
2180
- diff(diffResult.added, removedToShow, diffResult.changed);
2181
- if (mode === "merge" && Object.keys(diffResult.removed).length > 0) {
2182
- console.log();
2183
- console.log(
2184
- chalk6.dim(
2185
- `Note: ${Object.keys(diffResult.removed).length} remote variables not in local file will be preserved (use --replace to remove them)`
2186
- )
2187
- );
2188
- }
2189
- console.log();
2190
- if (options.dryRun) {
2191
- info("Dry run - no changes made.");
2192
- console.log();
2193
- console.log("Summary:");
2194
- console.log(` Would add: ${Object.keys(diffResult.added).length}`);
2195
- console.log(
2196
- ` Would update: ${Object.keys(diffResult.changed).length}`
2197
- );
2198
- if (mode === "replace") {
2199
- console.log(
2200
- ` Would delete: ${Object.keys(diffResult.removed).length}`
2201
- );
2202
- }
2203
- return;
2204
- }
2205
- if (!options.force) {
2206
- const confirmMessage = mode === "replace" ? `Push ${Object.keys(valid).length} variables and delete ${Object.keys(diffResult.removed).length} remote-only variables?` : `Push ${Object.keys(valid).length} variables to ${environment}?`;
2207
- const { proceed } = await inquirer3.prompt([
2208
- {
2209
- type: "confirm",
2210
- name: "proceed",
2211
- message: confirmMessage,
2212
- default: true
2213
- }
2214
- ]);
2215
- if (!proceed) {
2216
- info("Push cancelled.");
2217
- return;
2218
- }
2219
- }
2220
- const result = await withSpinner(
2221
- `Pushing variables to ${chalk6.bold(environment)}...`,
2222
- async () => {
2223
- const response = await api.post("/api/cli/variables/bulk", {
2224
- projectId,
2225
- environment,
2226
- variables: Object.entries(valid).map(([key, value]) => ({
2227
- key,
2228
- value
2229
- })),
2230
- mode,
2231
- ...organizationId && { organizationId }
2232
- });
2233
- return response.data;
2234
- }
2235
- );
2236
- if (result?.requested && result.requested > 0) {
2237
- success(
2238
- `Created ${result.requested} pending request(s) for ${chalk6.bold(environment)}`
2239
- );
2240
- console.log();
2241
- console.log(
2242
- chalk6.yellow(
2243
- " These changes require approval from an Admin or Team Lead."
2244
- )
2245
- );
2246
- console.log();
2247
- console.log(chalk6.dim(` Requested: ${result.requested}`));
2248
- if (result?.skipped && result.skipped > 0) {
2249
- console.log(chalk6.dim(` Skipped: ${result.skipped}`));
2250
- }
2251
- } else {
2252
- success(
2253
- `Pushed ${result?.total || Object.keys(valid).length} variables to ${chalk6.bold(environment)}`
2254
- );
2255
- console.log();
2256
- console.log(chalk6.dim(` Created: ${result?.created || 0}`));
2257
- console.log(chalk6.dim(` Updated: ${result?.updated || 0}`));
2258
- if (mode === "replace") {
2259
- console.log(chalk6.dim(` Deleted: ${result?.deleted || 0}`));
2260
- }
2261
- }
2262
- } catch (err) {
2263
- await handleError(err);
2264
- }
2265
- });
2266
-
2267
- // src/commands/switch.ts
2268
- import { Command as Command5 } from "commander";
2269
- import chalk7 from "chalk";
2270
- import inquirer4 from "inquirer";
2271
- var switchCommand = new Command5("switch").description("Switch project, environment, or active linked project").argument("[target]", "project slug or environment name").option("-o, --organization <id>", "Switch organization").option("-p, --project <id>", "Switch project").option(
2272
- "-e, --env <environment>",
2273
- "Switch environment (development, staging, production)"
2274
- ).option("--active <name-or-id>", "Set a linked project as active").action(async (target, options) => {
2275
- try {
2276
- if (!isAuthenticated()) {
2277
- throw notAuthenticated();
2278
- }
2279
- const api = createAPIClient();
2280
- const projectConfig = readProjectConfig();
2281
- if (options.active) {
2282
- const configV2 = readProjectConfigV2();
2283
- if (!configV2 || configV2.projects.length < 2) {
2284
- error(
2285
- "No multiple projects linked. Use `envpilot init --add` to link another project."
2286
- );
2287
- process.exit(1);
2288
- }
2289
- const target2 = configV2.projects.find(
2290
- (p) => p.projectId === options.active || p.projectName.toLowerCase() === options.active.toLowerCase()
2291
- );
2292
- if (!target2) {
2293
- error(`Project not found: ${options.active}`);
2294
- console.log();
2295
- console.log("Linked projects:");
2296
- for (const p of configV2.projects) {
2297
- const mark = p.projectId === configV2.activeProjectId ? chalk7.green(" *") : "";
2298
- console.log(
2299
- ` ${p.projectName || p.projectId} (${p.environment})${mark}`
2300
- );
2301
- }
2302
- process.exit(1);
2303
- }
2304
- const updated = setActiveProjectInConfig(configV2, target2.projectId);
2305
- writeProjectConfigV2(updated);
2306
- setActiveProjectId(target2.projectId);
2307
- setActiveOrganizationId(target2.organizationId);
2308
- success(
2309
- `Active project: ${chalk7.bold(target2.projectName || target2.projectId)}`
2310
- );
2311
- return;
2312
- }
2313
- if (options.env || target && ["development", "staging", "production"].includes(target)) {
2314
- const environment = options.env || target;
2315
- if (!projectConfig) {
2316
- error("No project initialized. Run `envpilot init` first.");
2317
- process.exit(1);
2318
- }
2319
- updateProjectConfig({ environment });
2320
- success(`Switched to ${chalk7.bold(environment)} environment`);
2321
- return;
2322
- }
2323
- if (options.organization) {
2324
- const organizations = await withSpinner(
2325
- "Fetching organizations...",
2326
- async () => {
2327
- const response = await api.get("/api/cli/organizations");
2328
- return response.data || [];
2329
- }
2330
- );
2331
- const org = organizations.find(
2332
- (o) => o._id === options.organization || o.slug === options.organization
2333
- );
2334
- if (!org) {
2335
- error(`Organization not found: ${options.organization}`);
2336
- process.exit(1);
2337
- }
2338
- setActiveOrganizationId(org._id);
2339
- if (org.role) {
2340
- setRole(org.role);
2341
- }
2342
- if (projectConfig) {
2343
- updateProjectConfig({ organizationId: org._id });
2344
- }
2345
- success(`Switched to organization: ${chalk7.bold(org.name)}`);
2346
- if (org.role) {
2347
- console.log(chalk7.dim(` Role: ${formatRole(org.role)}`));
2348
- roleNotice(org.role);
2349
- }
2350
- return;
2351
- }
2352
- if (options.project || target) {
2353
- const projectIdentifier = options.project || target;
2354
- const configV2 = readProjectConfigV2();
2355
- if (configV2) {
2356
- const linked = configV2.projects.find(
2357
- (p) => p.projectId === projectIdentifier || p.projectName.toLowerCase() === projectIdentifier.toLowerCase()
2358
- );
2359
- if (linked) {
2360
- const updated = setActiveProjectInConfig(
2361
- configV2,
2362
- linked.projectId
2363
- );
2364
- writeProjectConfigV2(updated);
2365
- setActiveProjectId(linked.projectId);
2366
- setActiveOrganizationId(linked.organizationId);
2367
- success(
2368
- `Switched to project: ${chalk7.bold(linked.projectName || linked.projectId)}`
2369
- );
2370
- return;
2371
- }
2372
- }
2373
- let organizationId = projectConfig?.organizationId;
2374
- if (!organizationId) {
2375
- const organizations = await withSpinner(
2376
- "Fetching organizations...",
2377
- async () => {
2378
- const response = await api.get("/api/cli/organizations");
2379
- return response.data || [];
2380
- }
2381
- );
2382
- if (organizations.length === 0) {
2383
- error("No organizations found.");
2384
- process.exit(1);
2385
- }
2386
- if (organizations.length === 1) {
2387
- organizationId = organizations[0]._id;
2388
- if (organizations[0].role) {
2389
- setRole(organizations[0].role);
2390
- }
2391
- } else {
2392
- const { orgId } = await inquirer4.prompt([
2393
- {
2394
- type: "list",
2395
- name: "orgId",
2396
- message: "Select an organization:",
2397
- choices: organizations.map((org) => ({
2398
- name: `${org.name} ${org.tier === "pro" ? chalk7.green("(Pro)") : chalk7.dim("(Free)")}`,
2399
- value: org._id
2400
- }))
2401
- }
2402
- ]);
2403
- organizationId = orgId;
2404
- const selectedOrg = organizations.find((o) => o._id === orgId);
2405
- if (selectedOrg?.role) {
2406
- setRole(selectedOrg.role);
2407
- }
2408
- }
2409
- }
2410
- const projects = await withSpinner("Fetching projects...", async () => {
2411
- const response = await api.get("/api/cli/projects", { organizationId });
2412
- return response.data || [];
2413
- });
2414
- const project = projects.find(
2415
- (p) => p._id === projectIdentifier || p.slug === projectIdentifier
2416
- );
2417
- if (!project) {
2418
- error(`Project not found: ${projectIdentifier}`);
2419
- console.log();
2420
- console.log("Available projects:");
2421
- for (const p of projects) {
2422
- console.log(` ${p.icon || "\u{1F4E6}"} ${p.name} (${p.slug})`);
2423
- }
2424
- process.exit(1);
2425
- }
2426
- setActiveProjectId(project._id);
2427
- setActiveOrganizationId(organizationId);
2428
- const environment = projectConfig?.environment || "development";
2429
- writeProjectConfig({
2430
- projectId: project._id,
2431
- organizationId,
2432
- environment
2433
- });
2434
- success(`Switched to project: ${chalk7.bold(project.name)}`);
2435
- if (project.projectRole) {
2436
- console.log(
2437
- chalk7.dim(
2438
- ` Project role: ${formatProjectRole(project.projectRole)}`
2439
- )
2440
- );
2441
- projectRoleNotice(project.projectRole);
2442
- }
2443
- return;
2444
- }
2445
- if (!target && !options.project && !options.organization && !options.env && !options.active) {
2446
- const configV2 = readProjectConfigV2();
2447
- const hasMultipleProjects = configV2 && configV2.projects.length > 1;
2448
- const choices = [];
2449
- if (hasMultipleProjects) {
2450
- choices.push({
2451
- name: "Active project",
2452
- value: "active"
2453
- });
2454
- }
2455
- choices.push(
2456
- { name: "Environment", value: "environment" },
2457
- { name: "Project", value: "project" },
2458
- { name: "Organization", value: "organization" }
2459
- );
2460
- const { switchType } = await inquirer4.prompt([
2461
- {
2462
- type: "list",
2463
- name: "switchType",
2464
- message: "What would you like to switch?",
2465
- choices
2466
- }
2467
- ]);
2468
- if (switchType === "active" && configV2) {
2469
- const { projectId } = await inquirer4.prompt([
2470
- {
2471
- type: "list",
2472
- name: "projectId",
2473
- message: "Select active project:",
2474
- choices: configV2.projects.map((p) => {
2475
- const isActive = p.projectId === configV2.activeProjectId;
2476
- return {
2477
- name: `${p.projectName || p.projectId} (${p.environment})${isActive ? chalk7.green(" *current") : ""}`,
2478
- value: p.projectId
2479
- };
2480
- }),
2481
- default: configV2.activeProjectId
2482
- }
2483
- ]);
2484
- const selected = configV2.projects.find(
2485
- (p) => p.projectId === projectId
2486
- );
2487
- const updated = setActiveProjectInConfig(configV2, projectId);
2488
- writeProjectConfigV2(updated);
2489
- setActiveProjectId(projectId);
2490
- setActiveOrganizationId(selected.organizationId);
2491
- success(
2492
- `Active project: ${chalk7.bold(selected.projectName || selected.projectId)}`
2493
- );
2494
- return;
2495
- }
2496
- if (switchType === "environment") {
2497
- if (!projectConfig) {
2498
- error("No project initialized. Run `envpilot init` first.");
2499
- process.exit(1);
2500
- }
2501
- const { environment } = await inquirer4.prompt([
2502
- {
2503
- type: "list",
2504
- name: "environment",
2505
- message: "Select environment:",
2506
- choices: [
2507
- { name: "Development", value: "development" },
2508
- { name: "Staging", value: "staging" },
2509
- { name: "Production", value: "production" }
2510
- ],
2511
- default: projectConfig.environment
2512
- }
2513
- ]);
2514
- updateProjectConfig({ environment });
2515
- success(`Switched to ${chalk7.bold(environment)} environment`);
2516
- return;
2517
- }
2518
- if (switchType === "organization" || switchType === "project") {
2519
- const organizations = await withSpinner(
2520
- "Fetching organizations...",
2521
- async () => {
2522
- const response = await api.get("/api/cli/organizations");
2523
- return response.data || [];
2524
- }
2525
- );
2526
- if (organizations.length === 0) {
2527
- error("No organizations found.");
2528
- process.exit(1);
2529
- }
2530
- const { orgId } = await inquirer4.prompt([
2531
- {
2532
- type: "list",
2533
- name: "orgId",
2534
- message: "Select an organization:",
2535
- choices: organizations.map((org) => ({
2536
- name: `${org.name} ${org.tier === "pro" ? chalk7.green("(Pro)") : chalk7.dim("(Free)")}`,
2537
- value: org._id
2538
- })),
2539
- default: projectConfig?.organizationId
2540
- }
2541
- ]);
2542
- if (switchType === "organization") {
2543
- setActiveOrganizationId(orgId);
2544
- const org = organizations.find((o) => o._id === orgId);
2545
- if (org.role) {
2546
- setRole(org.role);
2547
- }
2548
- success(`Switched to organization: ${chalk7.bold(org.name)}`);
2549
- if (org.role) {
2550
- console.log(chalk7.dim(` Role: ${formatRole(org.role)}`));
2551
- roleNotice(org.role);
2552
- }
2553
- return;
2554
- }
2555
- const selectedOrg = organizations.find((o) => o._id === orgId);
2556
- if (selectedOrg?.role) {
2557
- setRole(selectedOrg.role);
2558
- }
2559
- const projects = await withSpinner(
2560
- "Fetching projects...",
2561
- async () => {
2562
- const response = await api.get("/api/cli/projects", { organizationId: orgId });
2563
- return response.data || [];
2564
- }
2565
- );
2566
- if (projects.length === 0) {
2567
- error("No projects found in this organization.");
2568
- process.exit(1);
2569
- }
2570
- const { projectId } = await inquirer4.prompt([
2571
- {
2572
- type: "list",
2573
- name: "projectId",
2574
- message: "Select a project:",
2575
- choices: projects.map((project2) => ({
2576
- name: `${project2.icon || "\u{1F4E6}"} ${project2.name}`,
2577
- value: project2._id
2578
- })),
2579
- default: projectConfig?.projectId
2580
- }
2581
- ]);
2582
- const project = projects.find((p) => p._id === projectId);
2583
- const environment = projectConfig?.environment || "development";
2584
- setActiveProjectId(projectId);
2585
- setActiveOrganizationId(orgId);
2586
- writeProjectConfig({
2587
- projectId,
2588
- organizationId: orgId,
2589
- environment
2590
- });
2591
- success(`Switched to project: ${chalk7.bold(project.name)}`);
2592
- if (project.projectRole) {
2593
- console.log(
2594
- chalk7.dim(
2595
- ` Project role: ${formatProjectRole(project.projectRole)}`
2596
- )
2597
- );
2598
- projectRoleNotice(project.projectRole);
2599
- }
2600
- }
2601
- }
2602
- } catch (err) {
2603
- await handleError(err);
2604
- }
2605
- });
2606
-
2607
- // src/commands/list.ts
2608
- import { Command as Command6 } from "commander";
2609
- import chalk8 from "chalk";
2610
- var listCommand = new Command6("list").description("List resources").argument(
2611
- "[resource]",
2612
- "Resource type: projects, organizations, variables, linked",
2613
- "projects"
2614
- ).option("-o, --organization <id>", "Organization ID (for projects/variables)").option("-p, --project <id>", "Project ID (for variables)").option("-e, --env <environment>", "Environment filter (for variables)").option("-t, --tag <name>", "Filter by tag name (for variables)").option("--show-values", "Show actual variable values (masked by default)").option("--json", "Output as JSON").action(async (resource, options) => {
2615
- try {
2616
- if (!isAuthenticated()) {
2617
- throw notAuthenticated();
2618
- }
2619
- const api = createAPIClient();
2620
- const projectConfig = readProjectConfig();
2621
- switch (resource) {
2622
- case "orgs":
2623
- case "organizations":
2624
- await listOrganizations(api, options);
2625
- break;
2626
- case "projects":
2627
- await listProjects(api, projectConfig, options);
2628
- break;
2629
- case "vars":
2630
- case "variables":
2631
- await listVariables(api, projectConfig, options);
2632
- break;
2633
- case "linked":
2634
- listLinked();
2635
- break;
2636
- default:
2637
- error(`Unknown resource: ${resource}`);
2638
- console.log();
2639
- console.log("Available resources:");
2640
- console.log(" organizations (orgs) List your organizations");
2641
- console.log(
2642
- " projects List projects in an organization"
2643
- );
2644
- console.log(" variables (vars) List variables in a project");
2645
- console.log(
2646
- " linked List projects linked in this directory"
2647
- );
2648
- process.exit(1);
2649
- }
2650
- } catch (err) {
2651
- await handleError(err);
2652
- }
2653
- });
2654
- function listLinked() {
2655
- const configV2 = readProjectConfigV2();
2656
- if (!configV2) {
2657
- info("No projects linked. Run `envpilot init` to get started.");
2658
- return;
2659
- }
2660
- header(`Linked Projects (${configV2.projects.length})`);
2661
- console.log();
2662
- for (const project of configV2.projects) {
2663
- const isActive = project.projectId === configV2.activeProjectId;
2664
- const marker = isActive ? chalk8.green("*") : " ";
2665
- const envFile = getEnvPathForEnvironment(project.environment);
2666
- console.log(
2667
- ` ${marker} ${chalk8.bold(project.projectName || project.projectId)} ${chalk8.dim(`(${project.organizationName || project.organizationId})`)}`
2668
- );
2669
- console.log(` ${project.environment} ${chalk8.dim("\u2192")} ${envFile}`);
2670
- console.log();
2671
- }
2672
- if (configV2.projects.length > 1) {
2673
- console.log(chalk8.dim(" (* = active project)"));
2674
- console.log();
2675
- console.log(
2676
- chalk8.dim(
2677
- ' Use `envpilot switch --active "<name>"` to change the active project'
2678
- )
2679
- );
2680
- }
2681
- }
2682
- async function listOrganizations(api, options) {
2683
- const organizations = await withSpinner(
2684
- "Fetching organizations...",
2685
- async () => {
2686
- const response = await api.get("/api/cli/organizations");
2687
- return response.data || [];
2688
- }
2689
- );
2690
- if (organizations.length === 0) {
2691
- info("No organizations found.");
2692
- return;
2693
- }
2694
- if (options.json) {
2695
- console.log(JSON.stringify(organizations, null, 2));
2696
- return;
2697
- }
2698
- header("Organizations");
2699
- console.log();
2700
- table(
2701
- organizations.map((org) => ({
2702
- name: org.name,
2703
- slug: org.slug,
2704
- tier: org.tier === "pro" ? chalk8.green("Pro") : chalk8.dim("Free"),
2705
- role: org.role
2706
- })),
2707
- [
2708
- { key: "name", header: "Name" },
2709
- { key: "slug", header: "Slug" },
2710
- { key: "tier", header: "Tier" },
2711
- { key: "role", header: "Role" }
2712
- ]
2713
- );
2714
- }
2715
- async function listProjects(api, projectConfig, options) {
2716
- let organizationId = options.organization || projectConfig?.organizationId;
2717
- if (!organizationId) {
2718
- const organizations = await withSpinner(
2719
- "Fetching organizations...",
2720
- async () => {
2721
- const response = await api.get("/api/cli/organizations");
2722
- return response.data || [];
2723
- }
2724
- );
2725
- if (organizations.length === 0) {
2726
- info("No organizations found.");
2727
- return;
2728
- }
2729
- if (organizations.length === 1) {
2730
- organizationId = organizations[0]._id;
2731
- } else {
2732
- info("Multiple organizations found. Use --organization to specify one.");
2733
- console.log();
2734
- for (const org of organizations) {
2735
- console.log(` ${org.name} (${org.slug}): --organization ${org._id}`);
2736
- }
2737
- return;
2738
- }
2739
- }
2740
- const projects = await withSpinner("Fetching projects...", async () => {
2741
- const response = await api.get(
2742
- "/api/cli/projects",
2743
- { organizationId }
2744
- );
2745
- return response.data || [];
2746
- });
2747
- if (projects.length === 0) {
2748
- info("No projects found.");
2749
- return;
2750
- }
2751
- if (options.json) {
2752
- console.log(JSON.stringify(projects, null, 2));
2753
- return;
2754
- }
2755
- header("Projects");
2756
- console.log();
2757
- table(
2758
- projects.map((project) => ({
2759
- icon: project.icon || "\u{1F4E6}",
2760
- name: project.name,
2761
- slug: project.slug,
2762
- description: project.description || chalk8.dim("-"),
2763
- role: project.userRole === "admin" ? formatRole("admin") : formatProjectRole(project.projectRole),
2764
- active: projectConfig?.projectId === project._id ? chalk8.green("\u2713") : ""
2765
- })),
2766
- [
2767
- { key: "icon", header: "" },
2768
- { key: "name", header: "Name" },
2769
- { key: "slug", header: "Slug" },
2770
- { key: "description", header: "Description", width: 30 },
2771
- { key: "role", header: "Role" },
2772
- { key: "active", header: "" }
2773
- ]
2774
- );
2775
- const role = getRole();
2776
- if (role) {
2777
- console.log();
2778
- console.log(chalk8.dim(`Your org role: ${formatRole(role)}`));
2779
- }
2780
- }
2781
- async function listVariables(api, projectConfig, options) {
2782
- const projectId = options.project || projectConfig?.projectId;
2783
- const environment = options.env || projectConfig?.environment;
2784
- if (!projectId) {
2785
- error("No project specified. Use --project or run `envpilot init` first.");
2786
- process.exit(1);
2787
- }
2788
- let metaProjectRole;
2789
- const variables = await withSpinner("Fetching variables...", async () => {
2790
- const params = { projectId };
2791
- if (environment) {
2792
- params.environment = environment;
2793
- }
2794
- const response = await api.get("/api/cli/variables", params);
2795
- metaProjectRole = response.meta?.projectRole;
2796
- return response.data || [];
2797
- });
2798
- const tagFilter = options.tag?.toLowerCase();
2799
- const filtered = tagFilter ? variables.filter(
2800
- (v) => v.tags?.some((t) => t.name.toLowerCase() === tagFilter)
2801
- ) : variables;
2802
- if (filtered.length === 0) {
2803
- info(
2804
- `No variables found${environment ? ` for ${environment}` : ""}${tagFilter ? ` with tag "${options.tag}"` : ""}.`
2805
- );
2806
- return;
2807
- }
2808
- if (options.json) {
2809
- const output = filtered.map((v) => ({
2810
- ...v,
2811
- value: options.showValues ? v.value : maskValue(v.value)
2812
- }));
2813
- console.log(JSON.stringify(output, null, 2));
2814
- return;
2815
- }
2816
- header(
2817
- `Variables${environment ? ` (${environment})` : ""}${tagFilter ? ` [tag: ${options.tag}]` : ""}`
2818
- );
2819
- console.log();
2820
- const hasTags = filtered.some((v) => v.tags && v.tags.length > 0);
2821
- table(
2822
- filtered.map((variable) => ({
2823
- key: variable.key,
2824
- value: options.showValues ? variable.value : maskValue(variable.value),
2825
- sensitive: variable.isSensitive ? chalk8.yellow("\u25CF") : "",
2826
- tags: hasTags ? variable.tags?.map((t) => t.name).join(", ") || chalk8.dim("-") : "",
2827
- version: `v${variable.version}`
2828
- })),
2829
- [
2830
- { key: "key", header: "Key" },
2831
- { key: "value", header: "Value", width: 40 },
2832
- { key: "sensitive", header: "" },
2833
- ...hasTags ? [{ key: "tags", header: "Tags", width: 25 }] : [],
2834
- { key: "version", header: "Ver" }
2835
- ]
2836
- );
2837
- console.log();
2838
- console.log(chalk8.dim(`Total: ${filtered.length} variables`));
2839
- const role = getRole();
2840
- if (role) {
2841
- console.log(chalk8.dim(`Your org role: ${formatRole(role)}`));
2842
- }
2843
- if (metaProjectRole) {
2844
- console.log(
2845
- chalk8.dim(`Your project role: ${formatProjectRole(metaProjectRole)}`)
2846
- );
2847
- }
2848
- if (role === "member" || metaProjectRole === "viewer") {
2849
- console.log(
2850
- chalk8.dim("You may only see variables you have been granted access to.")
2851
- );
2852
- }
2853
- if (!options.showValues) {
2854
- console.log(chalk8.dim("Use --show-values to see actual values"));
2855
- }
2856
- }
2857
-
2858
- // src/commands/config.ts
2859
- import { Command as Command7 } from "commander";
2860
- import chalk9 from "chalk";
2861
- var configCommand = new Command7("config").description("Manage CLI configuration").argument("[action]", "Action: get, set, list, path, reset").argument("[key]", "Config key (for get/set)").argument("[value]", "Config value (for set)").action(async (action, key, value) => {
2862
- try {
2863
- switch (action) {
2864
- case "get":
2865
- await handleGet(key);
2866
- break;
2867
- case "set":
2868
- await handleSet(key, value);
2869
- break;
2870
- case "list":
2871
- case void 0:
2872
- await handleList();
2873
- break;
2874
- case "path":
2875
- await handlePath();
2876
- break;
2877
- case "reset":
2878
- await handleReset();
2879
- break;
2880
- default:
2881
- error(`Unknown action: ${action}`);
2882
- console.log();
2883
- console.log("Available actions:");
2884
- console.log(" list Show all configuration");
2885
- console.log(" get <key> Get a specific config value");
2886
- console.log(" set <key> <value> Set a config value");
2887
- console.log(" path Show config file locations");
2888
- console.log(" reset Reset all configuration");
2889
- process.exit(1);
2890
- }
2891
- } catch (err) {
2892
- await handleError(err);
2893
- }
2894
- });
2895
- async function handleGet(key) {
2896
- if (!key) {
2897
- error("Missing key. Usage: envpilot config get <key>");
2898
- console.log();
2899
- console.log("Available keys:");
2900
- console.log(" apiUrl API endpoint URL");
2901
- console.log(" user Current authenticated user");
2902
- console.log(" activeProjectId Currently active project");
2903
- console.log(" activeOrganizationId Currently active organization");
2904
- process.exit(1);
2905
- }
2906
- const config2 = getConfig();
2907
- switch (key) {
2908
- case "apiUrl":
2909
- console.log(config2.apiUrl);
2910
- break;
2911
- case "user":
2912
- if (config2.user) {
2913
- console.log(JSON.stringify(config2.user, null, 2));
2914
- } else {
2915
- console.log(chalk9.dim("(not set)"));
2916
- }
2917
- break;
2918
- case "activeProjectId":
2919
- console.log(config2.activeProjectId || chalk9.dim("(not set)"));
2920
- break;
2921
- case "activeOrganizationId":
2922
- console.log(config2.activeOrganizationId || chalk9.dim("(not set)"));
2923
- break;
2924
- default:
2925
- error(`Unknown key: ${key}`);
2926
- process.exit(1);
2927
- }
2928
- }
2929
- async function handleSet(key, value) {
2930
- if (!key || value === void 0) {
2931
- error("Missing key or value. Usage: envpilot config set <key> <value>");
2932
- console.log();
2933
- console.log("Settable keys:");
2934
- console.log(" apiUrl API endpoint URL");
2935
- process.exit(1);
2936
- }
2937
- switch (key) {
2938
- case "apiUrl":
2939
- try {
2940
- new URL(value);
2941
- } catch {
2942
- error("Invalid URL format");
2943
- process.exit(1);
2944
- }
2945
- setApiUrl(value);
2946
- success(`Set apiUrl to ${value}`);
2947
- break;
2948
- default:
2949
- error(`Cannot set key: ${key}`);
2950
- console.log();
2951
- console.log("Settable keys:");
2952
- console.log(" apiUrl API endpoint URL");
2953
- process.exit(1);
2954
- }
2955
- }
2956
- async function handleList() {
2957
- const config2 = getConfig();
2958
- const projectConfig = readProjectConfig();
2959
- header("Global Configuration");
2960
- console.log();
2961
- keyValue([
2962
- ["API URL", config2.apiUrl],
2963
- ["Authenticated", isAuthenticated() ? chalk9.green("Yes") : chalk9.red("No")],
2964
- ["User", config2.user?.email],
2965
- ["Active Organization", config2.activeOrganizationId],
2966
- ["Active Project", config2.activeProjectId]
2967
- ]);
2968
- console.log();
2969
- if (projectConfig) {
2970
- header("Project Configuration (.envpilot)");
2971
- console.log();
2972
- keyValue([
2973
- ["Project ID", projectConfig.projectId],
2974
- ["Organization ID", projectConfig.organizationId],
2975
- ["Environment", projectConfig.environment]
2976
- ]);
2977
- console.log();
2978
- } else {
2979
- info("No project configuration found in current directory.");
2980
- console.log();
2981
- }
2982
- }
2983
- async function handlePath() {
2984
- header("Configuration Paths");
2985
- console.log();
2986
- keyValue([
2987
- ["Global config", getConfigPath()],
2988
- ["Project config", getProjectConfigPath()]
2989
- ]);
2990
- }
2991
- async function handleReset() {
2992
- const inquirer6 = await import("inquirer");
2993
- const { confirm } = await inquirer6.default.prompt([
2994
- {
2995
- type: "confirm",
2996
- name: "confirm",
2997
- message: "Are you sure you want to reset all configuration? This will log you out.",
2998
- default: false
2999
- }
3000
- ]);
3001
- if (!confirm) {
3002
- info("Reset cancelled.");
3003
- return;
3004
- }
3005
- clearConfig();
3006
- success("Configuration reset.");
3007
- }
3008
-
3009
- // src/commands/logout.ts
3010
- import { Command as Command8 } from "commander";
3011
- var logoutCommand = new Command8("logout").description("Log out from Envpilot").action(async () => {
3012
- try {
3013
- if (!isAuthenticated()) {
3014
- info("You are not logged in.");
3015
- return;
3016
- }
3017
- const user = getUser();
3018
- const api = createAPIClient();
3019
- try {
3020
- await api.post("/api/cli/auth?action=revoke", {});
3021
- } catch {
3022
- }
3023
- clearAuth();
3024
- success(`Logged out${user?.email ? ` from ${user.email}` : ""}`);
3025
- } catch (err) {
3026
- await handleError(err);
3027
- }
3028
- });
3029
-
3030
- // src/commands/unlink.ts
3031
- import { Command as Command9 } from "commander";
3032
- import chalk10 from "chalk";
3033
- import inquirer5 from "inquirer";
3034
- var unlinkCommand = new Command9("unlink").description("Remove a linked project from this directory").argument("[project]", "Project name or ID to unlink").option("--force", "Skip confirmation").action(async (projectArg, options) => {
3035
- try {
3036
- if (!isAuthenticated()) {
3037
- throw notAuthenticated();
3038
- }
3039
- const config2 = readProjectConfigV2();
3040
- if (!config2 || config2.projects.length === 0) {
3041
- error("No projects linked. Run `envpilot init` first.");
3042
- process.exit(1);
3043
- }
3044
- let targetProject;
3045
- if (projectArg) {
3046
- targetProject = resolveProject(config2, projectArg);
3047
- if (!targetProject) {
3048
- error(`Project not found: ${projectArg}`);
3049
- console.log();
3050
- console.log("Linked projects:");
3051
- for (const p of config2.projects) {
3052
- console.log(
3053
- ` ${p.projectName || p.projectId} (${p.organizationName || p.organizationId})`
3054
- );
3055
- }
3056
- process.exit(1);
3057
- }
3058
- } else if (config2.projects.length > 1) {
3059
- const { projectId } = await inquirer5.prompt([
3060
- {
3061
- type: "list",
3062
- name: "projectId",
3063
- message: "Select a project to unlink:",
3064
- choices: config2.projects.map((p) => {
3065
- const isActive = p.projectId === config2.activeProjectId;
3066
- return {
3067
- name: `${p.projectName || p.projectId} (${p.organizationName || p.organizationId})${isActive ? chalk10.green(" *active") : ""}`,
3068
- value: p.projectId
3069
- };
3070
- })
3071
- }
3072
- ]);
3073
- targetProject = config2.projects.find((p) => p.projectId === projectId);
3074
- } else {
3075
- targetProject = config2.projects[0];
3076
- }
3077
- const displayName = targetProject.projectName || targetProject.projectId;
3078
- if (!options.force) {
3079
- const { proceed } = await inquirer5.prompt([
3080
- {
3081
- type: "confirm",
3082
- name: "proceed",
3083
- message: `Unlink "${displayName}"? Your .env files won't be deleted.`,
3084
- default: false
3085
- }
3086
- ]);
3087
- if (!proceed) {
3088
- info("Unlink cancelled.");
3089
- return;
3090
- }
3091
- }
3092
- const updated = removeProjectFromConfig(config2, targetProject.projectId);
3093
- if (!updated) {
3094
- deleteProjectConfig();
3095
- success(`Unlinked "${displayName}". No projects remaining.`);
3096
- info("Run `envpilot init` to link a new project.");
3097
- } else {
3098
- writeProjectConfigV2(updated);
3099
- const newActive = getActiveProject(updated);
3100
- if (newActive) {
3101
- setActiveProjectId(newActive.projectId);
3102
- }
3103
- success(`Unlinked "${displayName}".`);
3104
- if (config2.activeProjectId === targetProject.projectId && newActive) {
3105
- info(
3106
- `Active project switched to "${newActive.projectName || newActive.projectId}".`
3107
- );
3108
- }
3109
- console.log(
3110
- chalk10.dim(
3111
- ` ${updated.projects.length} project${updated.projects.length !== 1 ? "s" : ""} remaining`
3112
- )
3113
- );
3114
- }
3115
- } catch (err) {
3116
- await handleError(err);
3117
- }
3118
- });
3119
-
3120
- // src/commands/sync.ts
3121
- import { Command as Command10 } from "commander";
3122
- import chalk11 from "chalk";
3123
-
3124
- // src/lib/commit-guard.ts
3125
2
  import {
3126
- existsSync as existsSync3,
3127
- readFileSync as readFileSync3,
3128
- writeFileSync as writeFileSync3,
3129
- mkdirSync,
3130
- chmodSync as chmodSync2,
3131
- unlinkSync as unlinkSync2,
3132
- statSync
3133
- } from "fs";
3134
- import { join as join3, resolve } from "path";
3135
- import { execSync as execSync2 } from "child_process";
3136
- var HOOK_START_MARKER = "# ENVPILOT_GUARD_START";
3137
- var HOOK_END_MARKER = "# ENVPILOT_GUARD_END";
3138
- var HOOK_BLOCK = `${HOOK_START_MARKER} - Do not remove. Installed by Envpilot CLI.
3139
- ENV_FILES=$(git diff --cached --name-only | grep -E '(^|/)\\.env($|\\.)' || true)
3140
- if [ -n "$ENV_FILES" ]; then
3141
- echo ""
3142
- echo "\\033[1;31mERROR:\\033[0m Envpilot commit guard blocked this commit."
3143
- echo ""
3144
- echo "The following .env files were staged:"
3145
- echo "$ENV_FILES" | while IFS= read -r f; do echo " - $f"; done
3146
- echo ""
3147
- echo "Remove them with: git reset HEAD <file>"
3148
- echo "To bypass (not recommended): git commit --no-verify"
3149
- exit 1
3150
- fi
3151
- ${HOOK_END_MARKER}`;
3152
- function findGitRoot(startDir) {
3153
- let dir = startDir || process.cwd();
3154
- while (true) {
3155
- if (existsSync3(join3(dir, ".git"))) {
3156
- return dir;
3157
- }
3158
- const parent = resolve(dir, "..");
3159
- if (parent === dir) {
3160
- return null;
3161
- }
3162
- dir = parent;
3163
- }
3164
- }
3165
- function resolveGitDir(repoRoot) {
3166
- const gitPath = join3(repoRoot, ".git");
3167
- try {
3168
- const stat = statSync(gitPath);
3169
- if (stat.isDirectory()) {
3170
- return gitPath;
3171
- }
3172
- } catch {
3173
- return gitPath;
3174
- }
3175
- try {
3176
- const content = readFileSync3(gitPath, "utf-8").trim();
3177
- const match = content.match(/^gitdir:\s*(.+)$/);
3178
- if (match) {
3179
- const gitdir = resolve(repoRoot, match[1]);
3180
- const commonDir = resolve(gitdir, "..", "..");
3181
- if (existsSync3(join3(commonDir, "hooks")) || existsSync3(commonDir)) {
3182
- return commonDir;
3183
- }
3184
- return gitdir;
3185
- }
3186
- } catch {
3187
- }
3188
- return gitPath;
3189
- }
3190
- function getHooksDir(repoRoot) {
3191
- try {
3192
- const customPath = execSync2("git config core.hooksPath", {
3193
- cwd: repoRoot,
3194
- encoding: "utf-8",
3195
- stdio: ["pipe", "pipe", "pipe"]
3196
- }).trim();
3197
- if (customPath) {
3198
- return resolve(repoRoot, customPath);
3199
- }
3200
- } catch {
3201
- }
3202
- const gitDir = resolveGitDir(repoRoot);
3203
- return join3(gitDir, "hooks");
3204
- }
3205
- function installCommitGuard(repoRoot) {
3206
- const root = repoRoot || findGitRoot();
3207
- if (!root) {
3208
- return {
3209
- installed: false,
3210
- hookPath: null,
3211
- message: "Not a git repository"
3212
- };
3213
- }
3214
- try {
3215
- const hooksDir = getHooksDir(root);
3216
- const hookPath = join3(hooksDir, "pre-commit");
3217
- mkdirSync(hooksDir, { recursive: true });
3218
- let existingContent = "";
3219
- try {
3220
- existingContent = readFileSync3(hookPath, "utf-8");
3221
- } catch {
3222
- }
3223
- if (existingContent.includes(HOOK_START_MARKER)) {
3224
- const startIdx = existingContent.indexOf(HOOK_START_MARKER);
3225
- const endIdx = existingContent.indexOf(HOOK_END_MARKER) + HOOK_END_MARKER.length;
3226
- const updated = existingContent.substring(0, startIdx) + HOOK_BLOCK + existingContent.substring(endIdx);
3227
- writeFileSync3(hookPath, updated, "utf-8");
3228
- chmodSync2(hookPath, 493);
3229
- return {
3230
- installed: true,
3231
- hookPath,
3232
- message: "Pre-commit hook updated"
3233
- };
3234
- }
3235
- let newContent;
3236
- if (existingContent.trim()) {
3237
- newContent = existingContent.trimEnd() + "\n\n" + HOOK_BLOCK + "\n";
3238
- } else {
3239
- newContent = "#!/bin/sh\n\n" + HOOK_BLOCK + "\n";
3240
- }
3241
- writeFileSync3(hookPath, newContent, "utf-8");
3242
- chmodSync2(hookPath, 493);
3243
- return {
3244
- installed: true,
3245
- hookPath,
3246
- message: "Pre-commit hook installed"
3247
- };
3248
- } catch (err) {
3249
- return {
3250
- installed: false,
3251
- hookPath: null,
3252
- message: `Failed to install hook: ${err instanceof Error ? err.message : String(err)}`
3253
- };
3254
- }
3255
- }
3
+ getApiUrl,
4
+ getTopLevelCommandCatalog,
5
+ initSentry,
6
+ openTUI
7
+ } from "./chunk-RUYGAIH5.js";
3256
8
 
3257
- // src/commands/sync.ts
3258
- var syncCommand = new Command10("sync").description(
3259
- "Login, select project, pull variables, and protect files \u2014 all in one command"
3260
- ).option("-o, --organization <id>", "Organization ID").option("-p, --project <id>", "Project ID or name").option(
3261
- "-e, --env <environment>",
3262
- "Environment (development, staging, production)"
3263
- ).option("--force", "Overwrite without confirmation").option("--no-guard", "Skip pre-commit hook installation").action(async (options) => {
3264
- try {
3265
- if (!isAuthenticated()) {
3266
- console.log();
3267
- info("Not logged in. Starting authentication...");
3268
- console.log();
3269
- await performLogin();
3270
- console.log();
3271
- } else {
3272
- info("Already authenticated.");
3273
- }
3274
- ensureEnvInGitignore();
3275
- if (options.guard !== false) {
3276
- const guardResult = installCommitGuard();
3277
- if (guardResult.installed) {
3278
- success(guardResult.message);
3279
- info("Staged .env files will be blocked from commits.");
3280
- } else if (guardResult.message !== "Not a git repository") {
3281
- warning(guardResult.message);
3282
- }
3283
- }
3284
- console.log();
3285
- let projectId;
3286
- let organizationId;
3287
- let environment;
3288
- let projectName;
3289
- let organizationName;
3290
- const existingConfig = readProjectConfigV2();
3291
- if (existingConfig && !options.organization && !options.project && !options.env) {
3292
- const active = getActiveProject(existingConfig);
3293
- if (active) {
3294
- projectId = active.projectId;
3295
- organizationId = active.organizationId;
3296
- environment = active.environment;
3297
- projectName = active.projectName;
3298
- organizationName = active.organizationName;
3299
- info(
3300
- `Using project ${chalk11.bold(projectName || projectId)} (${environment})`
3301
- );
3302
- } else {
3303
- const selection = await selectOrgProjectEnv(options);
3304
- projectId = selection.selectedProject._id;
3305
- organizationId = selection.selectedOrg._id;
3306
- environment = selection.selectedEnvironment;
3307
- projectName = selection.selectedProject.name;
3308
- organizationName = selection.selectedOrg.name;
3309
- }
3310
- } else {
3311
- const selection = await selectOrgProjectEnv({
3312
- organization: options.organization,
3313
- project: options.project,
3314
- environment: options.env
3315
- });
3316
- projectId = selection.selectedProject._id;
3317
- organizationId = selection.selectedOrg._id;
3318
- environment = selection.selectedEnvironment;
3319
- projectName = selection.selectedProject.name;
3320
- organizationName = selection.selectedOrg.name;
3321
- if (selection.selectedOrg.role) {
3322
- setRole(selection.selectedOrg.role);
3323
- }
3324
- writeProjectConfigV2({
3325
- version: 1,
3326
- activeProjectId: projectId,
3327
- projects: existingConfig ? [
3328
- ...existingConfig.projects.filter(
3329
- (p) => p.projectId !== projectId
3330
- ),
3331
- {
3332
- projectId,
3333
- organizationId,
3334
- projectName,
3335
- organizationName,
3336
- environment
3337
- }
3338
- ] : [
3339
- {
3340
- projectId,
3341
- organizationId,
3342
- projectName,
3343
- organizationName,
3344
- environment
3345
- }
3346
- ]
3347
- });
3348
- setActiveOrganizationId(organizationId);
3349
- setActiveProjectId(projectId);
3350
- }
3351
- console.log();
3352
- let metaProjectRole;
3353
- const variables = await withSpinner(
3354
- `Fetching ${chalk11.bold(environment)} variables...`,
3355
- async () => {
3356
- const api = createAPIClient();
3357
- const response = await api.get("/api/cli/variables", {
3358
- projectId,
3359
- environment,
3360
- ...organizationId && { organizationId }
3361
- });
3362
- metaProjectRole = response.meta?.projectRole;
3363
- return response.data || [];
3364
- }
3365
- );
3366
- const outputPath = getEnvPathForEnvironment(environment);
3367
- if (variables.length === 0) {
3368
- warning(`No variables found for ${environment} environment.`);
3369
- console.log();
3370
- return;
3371
- }
3372
- const remoteVars = {};
3373
- for (const variable of variables) {
3374
- remoteVars[variable.key] = variable.value;
3375
- }
3376
- const localVars = readEnvFile(outputPath) || {};
3377
- const diffResult = diffEnvVars(remoteVars, localVars);
3378
- const hasChanges = Object.keys(diffResult.added).length > 0 || Object.keys(diffResult.removed).length > 0 || Object.keys(diffResult.changed).length > 0;
3379
- if (!hasChanges) {
3380
- success(`${chalk11.bold(outputPath)} is up to date.`);
3381
- console.log();
3382
- return;
3383
- }
3384
- console.log();
3385
- console.log(chalk11.bold("Changes:"));
3386
- console.log();
3387
- diff(diffResult.added, diffResult.removed, diffResult.changed);
3388
- console.log();
3389
- try {
3390
- const fs = await import("fs");
3391
- if (fs.existsSync(outputPath)) {
3392
- fs.chmodSync(outputPath, 420);
3393
- }
3394
- } catch {
3395
- }
3396
- const comments = {};
3397
- for (const variable of variables) {
3398
- if (variable.description) {
3399
- comments[variable.key] = variable.description;
3400
- }
3401
- }
3402
- writeEnvFile(outputPath, remoteVars, { sort: true, comments });
3403
- const role = getRole();
3404
- applyFileProtection(outputPath, role, metaProjectRole);
3405
- success(
3406
- `Synced ${variables.length} variables to ${chalk11.bold(outputPath)}`
3407
- );
3408
- const isProtected = role !== "admin" && role !== "team_lead" && metaProjectRole !== "manager";
3409
- if (isProtected) {
3410
- info(
3411
- `File is read-only (your role: ${role || metaProjectRole || "member"}).`
3412
- );
3413
- }
3414
- console.log();
3415
- console.log(
3416
- chalk11.dim(` Added: ${Object.keys(diffResult.added).length}`)
3417
- );
3418
- console.log(
3419
- chalk11.dim(` Changed: ${Object.keys(diffResult.changed).length}`)
3420
- );
3421
- console.log(
3422
- chalk11.dim(` Removed: ${Object.keys(diffResult.removed).length}`)
3423
- );
3424
- console.log();
3425
- } catch (err) {
3426
- await handleError(err);
3427
- }
3428
- });
9
+ // src/lib/program.ts
10
+ import { Command } from "commander";
3429
11
 
3430
- // src/commands/usage.ts
3431
- import { Command as Command11 } from "commander";
3432
- import chalk12 from "chalk";
3433
- function formatUsage(current, limit) {
3434
- const limitStr = limit === null ? "unlimited" : String(limit);
3435
- const ratio = `${current}/${limitStr}`;
3436
- if (limit === null) return chalk12.green(ratio);
3437
- if (current >= limit) return chalk12.red(ratio);
3438
- if (current >= limit * 0.8) return chalk12.yellow(ratio);
3439
- return chalk12.green(ratio);
3440
- }
3441
- function featureStatus(enabled) {
3442
- return enabled ? chalk12.green("Enabled") : chalk12.dim("Disabled (Pro)");
3443
- }
3444
- var usageCommand = new Command11("usage").description("Show plan usage and limits for the active organization").option("-o, --organization <id>", "Organization ID").option("--json", "Output as JSON").action(async (options) => {
3445
- try {
3446
- if (!isAuthenticated()) {
3447
- throw notAuthenticated();
3448
- }
3449
- const api = createAPIClient();
3450
- const projectConfig = readProjectConfig();
3451
- let organizationId = options.organization || projectConfig?.organizationId || getActiveOrganizationId();
3452
- if (!organizationId) {
3453
- const orgs = await withSpinner(
3454
- "Fetching organizations...",
3455
- async () => {
3456
- const response = await api.get("/api/cli/organizations");
3457
- return response.data || [];
3458
- }
3459
- );
3460
- if (orgs.length === 0) {
3461
- error("No organizations found.");
3462
- process.exit(1);
3463
- }
3464
- if (orgs.length === 1) {
3465
- organizationId = orgs[0]._id;
3466
- } else {
3467
- error(
3468
- "Multiple organizations found. Use --organization to specify one."
3469
- );
3470
- console.log();
3471
- for (const org of orgs) {
3472
- console.log(
3473
- ` ${org.name} (${org.slug}): --organization ${org._id}`
3474
- );
3475
- }
3476
- process.exit(1);
3477
- }
3478
- }
3479
- const usage = await withSpinner(
3480
- "Fetching usage...",
3481
- () => api.getUsage(organizationId)
3482
- );
3483
- if (options.json) {
3484
- console.log(JSON.stringify(usage, null, 2));
3485
- return;
3486
- }
3487
- const tierLabel = usage.tier === "pro" ? chalk12.green("Pro") : chalk12.white("Free");
3488
- header(`Plan: ${tierLabel}`);
3489
- blank();
3490
- if (!usage.enforcementEnabled) {
3491
- info("Pre-alpha mode \u2014 all limits are bypassed. Billing coming soon.");
3492
- blank();
3493
- }
3494
- header("Resource Usage");
3495
- blank();
3496
- keyValue([
3497
- ["Projects", formatUsage(usage.usage.projects, usage.limits.projects)],
3498
- [
3499
- "Team Members",
3500
- formatUsage(usage.usage.teamMembers, usage.limits.teamMembers)
3501
- ],
3502
- ["Pending Invitations", String(usage.usage.pendingInvitations)],
3503
- ["Total Variables", String(usage.usage.totalVariables)]
3504
- ]);
3505
- blank();
3506
- if (usage.usage.variablesPerProject.length > 0) {
3507
- header("Variables per Project");
3508
- blank();
3509
- table(
3510
- usage.usage.variablesPerProject.map((p) => ({
3511
- project: p.projectName,
3512
- variables: formatUsage(p.count, usage.limits.variablesPerProject)
3513
- })),
3514
- [
3515
- { key: "project", header: "Project" },
3516
- { key: "variables", header: "Variables" }
3517
- ]
3518
- );
3519
- blank();
3520
- }
3521
- header("Features");
3522
- blank();
3523
- keyValue([
3524
- ["Version History", featureStatus(usage.features.versionHistory)],
3525
- ["Bulk Import", featureStatus(usage.features.bulkImport)],
3526
- [
3527
- "Granular Permissions",
3528
- featureStatus(usage.features.granularPermissions)
3529
- ],
3530
- ["Extension Access", featureStatus(usage.features.extensionAccess)],
3531
- ["Audit Log Retention", `${usage.features.auditLogRetentionDays} days`]
3532
- ]);
3533
- blank();
3534
- } catch (err) {
3535
- await handleError(err);
3536
- }
3537
- });
12
+ // src/lib/cli-version.ts
13
+ var CLI_VERSION = true ? "1.6.0" : "0.0.0";
3538
14
 
3539
15
  // src/lib/version-check.ts
3540
- import chalk13 from "chalk";
3541
- import Conf2 from "conf";
3542
- var CLI_VERSION = true ? "1.4.1" : "0.0.0";
16
+ import chalk from "chalk";
17
+ import Conf from "conf";
18
+ var CLI_VERSION2 = true ? "1.6.0" : "0.0.0";
3543
19
  var CHECK_INTERVAL = 60 * 60 * 1e3;
3544
20
  var _cache = null;
3545
21
  function getCache() {
3546
22
  if (!_cache) {
3547
- _cache = new Conf2({
23
+ _cache = new Conf({
3548
24
  projectName: "envpilot",
3549
25
  configName: "version-cache"
3550
26
  });
@@ -3562,18 +38,18 @@ function checkForUpdate() {
3562
38
  }).then((data) => {
3563
39
  if (!data?.cli) return;
3564
40
  cache.set("lastVersionCheck", Date.now());
3565
- if (data.cli !== CLI_VERSION) {
41
+ if (data.cli !== CLI_VERSION2) {
3566
42
  console.log();
3567
43
  console.log(
3568
- chalk13.yellow(" Update available:"),
3569
- chalk13.dim(CLI_VERSION),
3570
- chalk13.yellow("\u2192"),
3571
- chalk13.green(data.cli)
44
+ chalk.yellow(" Update available:"),
45
+ chalk.dim(CLI_VERSION2),
46
+ chalk.yellow("\u2192"),
47
+ chalk.green(data.cli)
3572
48
  );
3573
49
  console.log(
3574
- chalk13.dim(" Run"),
3575
- chalk13.cyan("npm update -g @envpilot/cli"),
3576
- chalk13.dim("to update")
50
+ chalk.dim(" Run"),
51
+ chalk.cyan("npm update -g @envpilot/cli"),
52
+ chalk.dim("to update")
3577
53
  );
3578
54
  console.log();
3579
55
  }
@@ -3581,22 +57,28 @@ function checkForUpdate() {
3581
57
  });
3582
58
  }
3583
59
 
3584
- // src/index.ts
60
+ // src/lib/program.ts
61
+ function createProgram() {
62
+ const program = new Command();
63
+ program.name("envpilot").description("Envpilot CLI - Sync, secure, and share environment variables").version(CLI_VERSION).enablePositionalOptions();
64
+ for (const command of getTopLevelCommandCatalog()) {
65
+ if (command.createCommand) {
66
+ program.addCommand(command.createCommand());
67
+ }
68
+ }
69
+ program.hook("postAction", () => {
70
+ checkForUpdate();
71
+ });
72
+ return program;
73
+ }
74
+
75
+ // src/index.tsx
3585
76
  initSentry();
3586
- var program = new Command12();
3587
- program.name("envpilot").description("Envpilot CLI - Sync, secure, and share environment variables").version("1.3.2");
3588
- program.addCommand(loginCommand);
3589
- program.addCommand(logoutCommand);
3590
- program.addCommand(initCommand);
3591
- program.addCommand(pullCommand);
3592
- program.addCommand(pushCommand);
3593
- program.addCommand(switchCommand);
3594
- program.addCommand(listCommand);
3595
- program.addCommand(configCommand);
3596
- program.addCommand(unlinkCommand);
3597
- program.addCommand(syncCommand);
3598
- program.addCommand(usageCommand);
3599
- program.hook("postAction", () => {
3600
- checkForUpdate();
3601
- });
3602
- program.parse();
77
+ var args = process.argv.slice(2);
78
+ var shouldOpenTUI = process.env.ENVPILOT_TUI_CHILD !== "1" && args.length === 0;
79
+ if (shouldOpenTUI) {
80
+ await openTUI();
81
+ } else {
82
+ const program = createProgram();
83
+ await program.parseAsync();
84
+ }