@envpilot/cli 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +282 -69
  2. package/package.json +4 -3
package/dist/index.js CHANGED
@@ -1,11 +1,59 @@
1
1
  #!/usr/bin/env node
2
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.3.0" : "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
+
3
51
  // src/index.ts
4
- import { Command as Command9 } from "commander";
52
+ import { Command as Command10 } from "commander";
5
53
 
6
54
  // src/commands/login.ts
7
55
  import { Command } from "commander";
8
- import chalk2 from "chalk";
56
+ import chalk3 from "chalk";
9
57
  import open from "open";
10
58
 
11
59
  // src/lib/ui.ts
@@ -95,6 +143,9 @@ function maskValue(value, showChars = 4) {
95
143
  }
96
144
  return value.slice(0, showChars) + "****" + value.slice(-showChars);
97
145
  }
146
+ function blank() {
147
+ console.log();
148
+ }
98
149
  function formatRole(role) {
99
150
  switch (role) {
100
151
  case "admin":
@@ -176,6 +227,9 @@ function setRefreshToken(token) {
176
227
  function setActiveProjectId(projectId) {
177
228
  config.set("activeProjectId", projectId);
178
229
  }
230
+ function getActiveOrganizationId() {
231
+ return config.get("activeOrganizationId");
232
+ }
179
233
  function setActiveOrganizationId(organizationId) {
180
234
  config.set("activeOrganizationId", organizationId);
181
235
  }
@@ -348,12 +402,19 @@ var APIClient = class {
348
402
  "UNAUTHORIZED"
349
403
  );
350
404
  }
405
+ if (response.status === 403 && code === "TIER_LIMIT_REACHED") {
406
+ throw new APIError(
407
+ message || "Tier limit reached. Run `envpilot usage` to see your plan limits.",
408
+ 403,
409
+ "TIER_LIMIT_REACHED"
410
+ );
411
+ }
351
412
  if (response.status === 403) {
352
413
  throw new APIError(message || "Access denied.", 403, code || "FORBIDDEN");
353
414
  }
354
415
  if (response.status === 402) {
355
416
  throw new APIError(
356
- message || "Payment is currently disabled for this pre-alpha build.",
417
+ message || "Tier limit reached. Run `envpilot usage` to see your plan limits.",
357
418
  402,
358
419
  "PAYMENT_REQUIRED"
359
420
  );
@@ -375,6 +436,12 @@ var APIClient = class {
375
436
  async getTierInfo(organizationId) {
376
437
  return this.get("/api/cli/tier", { organizationId });
377
438
  }
439
+ /**
440
+ * Get usage info for the active organization
441
+ */
442
+ async getUsage(organizationId) {
443
+ return this.get("/api/cli/usage", { organizationId });
444
+ }
378
445
  /**
379
446
  * List organizations the user has access to
380
447
  */
@@ -479,6 +546,91 @@ function createAPIClient() {
479
546
  return new APIClient();
480
547
  }
481
548
 
549
+ // src/lib/errors.ts
550
+ import chalk2 from "chalk";
551
+ var CLIError = class extends Error {
552
+ constructor(message, code, suggestion) {
553
+ super(message);
554
+ this.code = code;
555
+ this.suggestion = suggestion;
556
+ this.name = "CLIError";
557
+ }
558
+ };
559
+ var ErrorCodes = {
560
+ NOT_AUTHENTICATED: "NOT_AUTHENTICATED",
561
+ NOT_INITIALIZED: "NOT_INITIALIZED",
562
+ PROJECT_NOT_FOUND: "PROJECT_NOT_FOUND",
563
+ ORGANIZATION_NOT_FOUND: "ORGANIZATION_NOT_FOUND",
564
+ VARIABLE_NOT_FOUND: "VARIABLE_NOT_FOUND",
565
+ INVALID_CONFIG: "INVALID_CONFIG",
566
+ NETWORK_ERROR: "NETWORK_ERROR",
567
+ PERMISSION_DENIED: "PERMISSION_DENIED",
568
+ TIER_LIMIT_EXCEEDED: "TIER_LIMIT_EXCEEDED",
569
+ FILE_NOT_FOUND: "FILE_NOT_FOUND",
570
+ INVALID_INPUT: "INVALID_INPUT",
571
+ UNKNOWN_ERROR: "UNKNOWN_ERROR"
572
+ };
573
+ function formatError(error2) {
574
+ if (error2 instanceof CLIError) {
575
+ let message = chalk2.red(`Error: ${error2.message}`);
576
+ if (error2.suggestion) {
577
+ message += `
578
+ ${chalk2.yellow("Suggestion:")} ${error2.suggestion}`;
579
+ }
580
+ return message;
581
+ }
582
+ if (error2 instanceof Error) {
583
+ return chalk2.red(`Error: ${error2.message}`);
584
+ }
585
+ return chalk2.red(`Error: ${String(error2)}`);
586
+ }
587
+ async function handleError(error2) {
588
+ console.error(formatError(error2));
589
+ const skipCodes = /* @__PURE__ */ new Set([
590
+ ErrorCodes.NOT_AUTHENTICATED,
591
+ ErrorCodes.INVALID_INPUT,
592
+ ErrorCodes.NOT_INITIALIZED
593
+ ]);
594
+ if (error2 instanceof CLIError) {
595
+ if (!skipCodes.has(error2.code)) {
596
+ captureError(error2, { errorCode: error2.code });
597
+ }
598
+ } else {
599
+ captureError(error2);
600
+ }
601
+ await flushSentry();
602
+ if (error2 instanceof CLIError) {
603
+ switch (error2.code) {
604
+ case ErrorCodes.NOT_AUTHENTICATED:
605
+ process.exit(2);
606
+ case ErrorCodes.PERMISSION_DENIED:
607
+ process.exit(3);
608
+ case ErrorCodes.TIER_LIMIT_EXCEEDED:
609
+ process.exit(4);
610
+ default:
611
+ process.exit(1);
612
+ }
613
+ }
614
+ process.exit(1);
615
+ }
616
+ function notAuthenticated() {
617
+ return new CLIError(
618
+ "You are not authenticated.",
619
+ ErrorCodes.NOT_AUTHENTICATED,
620
+ "Run `envpilot login` to authenticate."
621
+ );
622
+ }
623
+ function notInitialized() {
624
+ return new CLIError(
625
+ "This directory is not initialized with Envpilot.",
626
+ ErrorCodes.NOT_INITIALIZED,
627
+ "Run `envpilot init` to initialize."
628
+ );
629
+ }
630
+ function fileNotFound(path) {
631
+ return new CLIError(`File not found: ${path}`, ErrorCodes.FILE_NOT_FOUND);
632
+ }
633
+
482
634
  // src/commands/login.ts
483
635
  import { hostname } from "os";
484
636
  var POLL_INTERVAL_MS = 2e3;
@@ -496,12 +648,12 @@ var loginCommand = new Command("login").description("Authenticate with Envpilot"
496
648
  const initResponse = await api.post("/api/cli/auth?action=initiate", { deviceName });
497
649
  spinner.stop();
498
650
  console.log();
499
- console.log(chalk2.bold("Your authentication code:"));
651
+ console.log(chalk3.bold("Your authentication code:"));
500
652
  console.log();
501
- console.log(chalk2.cyan.bold(` ${initResponse.code}`));
653
+ console.log(chalk3.cyan.bold(` ${initResponse.code}`));
502
654
  console.log();
503
655
  console.log(`Open this URL to authenticate:`);
504
- console.log(chalk2.dim(initResponse.url));
656
+ console.log(chalk3.dim(initResponse.url));
505
657
  console.log();
506
658
  if (options.browser !== false) {
507
659
  info("Opening browser...");
@@ -531,14 +683,14 @@ var loginCommand = new Command("login").description("Authenticate with Envpilot"
531
683
  }
532
684
  authenticated = true;
533
685
  console.log();
534
- success(`Logged in as ${chalk2.bold(pollResponse.user?.email)}`);
686
+ success(`Logged in as ${chalk3.bold(pollResponse.user?.email)}`);
535
687
  console.log();
536
688
  console.log("Next steps:");
537
689
  console.log(
538
- ` ${chalk2.cyan("envpilot init")} Initialize a project in the current directory`
690
+ ` ${chalk3.cyan("envpilot init")} Initialize a project in the current directory`
539
691
  );
540
692
  console.log(
541
- ` ${chalk2.cyan("envpilot list")} List your projects and organizations`
693
+ ` ${chalk3.cyan("envpilot list")} List your projects and organizations`
542
694
  );
543
695
  console.log();
544
696
  break;
@@ -556,8 +708,7 @@ var loginCommand = new Command("login").description("Authenticate with Envpilot"
556
708
  process.exit(1);
557
709
  }
558
710
  } catch (err) {
559
- error(err instanceof Error ? err.message : "Authentication failed");
560
- process.exit(1);
711
+ await handleError(err);
561
712
  }
562
713
  });
563
714
  function sleep(ms) {
@@ -690,48 +841,6 @@ function getTrackedEnvFiles(directory = process.cwd()) {
690
841
  }
691
842
  }
692
843
 
693
- // src/lib/errors.ts
694
- import chalk3 from "chalk";
695
- var CLIError = class extends Error {
696
- constructor(message, code, suggestion) {
697
- super(message);
698
- this.code = code;
699
- this.suggestion = suggestion;
700
- this.name = "CLIError";
701
- }
702
- };
703
- var ErrorCodes = {
704
- NOT_AUTHENTICATED: "NOT_AUTHENTICATED",
705
- NOT_INITIALIZED: "NOT_INITIALIZED",
706
- PROJECT_NOT_FOUND: "PROJECT_NOT_FOUND",
707
- ORGANIZATION_NOT_FOUND: "ORGANIZATION_NOT_FOUND",
708
- VARIABLE_NOT_FOUND: "VARIABLE_NOT_FOUND",
709
- INVALID_CONFIG: "INVALID_CONFIG",
710
- NETWORK_ERROR: "NETWORK_ERROR",
711
- PERMISSION_DENIED: "PERMISSION_DENIED",
712
- TIER_LIMIT_EXCEEDED: "TIER_LIMIT_EXCEEDED",
713
- FILE_NOT_FOUND: "FILE_NOT_FOUND",
714
- INVALID_INPUT: "INVALID_INPUT",
715
- UNKNOWN_ERROR: "UNKNOWN_ERROR"
716
- };
717
- function notAuthenticated() {
718
- return new CLIError(
719
- "You are not authenticated.",
720
- ErrorCodes.NOT_AUTHENTICATED,
721
- "Run `envpilot login` to authenticate."
722
- );
723
- }
724
- function notInitialized() {
725
- return new CLIError(
726
- "This directory is not initialized with Envpilot.",
727
- ErrorCodes.NOT_INITIALIZED,
728
- "Run `envpilot init` to initialize."
729
- );
730
- }
731
- function fileNotFound(path) {
732
- return new CLIError(`File not found: ${path}`, ErrorCodes.FILE_NOT_FOUND);
733
- }
734
-
735
844
  // src/commands/init.ts
736
845
  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(
737
846
  "-e, --environment <env>",
@@ -914,8 +1023,7 @@ var initCommand = new Command2("init").description("Initialize Envpilot in the c
914
1023
  );
915
1024
  console.log();
916
1025
  } catch (err) {
917
- error(err instanceof Error ? err.message : "Initialization failed");
918
- process.exit(1);
1026
+ await handleError(err);
919
1027
  }
920
1028
  });
921
1029
 
@@ -1151,8 +1259,7 @@ var pullCommand = new Command3("pull").description("Download environment variabl
1151
1259
  chalk5.dim(` Removed: ${Object.keys(diffResult.removed).length}`)
1152
1260
  );
1153
1261
  } catch (err) {
1154
- error(err instanceof Error ? err.message : "Pull failed");
1155
- process.exit(1);
1262
+ await handleError(err);
1156
1263
  }
1157
1264
  });
1158
1265
 
@@ -1400,8 +1507,7 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
1400
1507
  }
1401
1508
  }
1402
1509
  } catch (err) {
1403
- error(err instanceof Error ? err.message : "Push failed");
1404
- process.exit(1);
1510
+ await handleError(err);
1405
1511
  }
1406
1512
  });
1407
1513
 
@@ -1655,8 +1761,7 @@ var switchCommand = new Command5("switch").description("Switch project or enviro
1655
1761
  }
1656
1762
  }
1657
1763
  } catch (err) {
1658
- error(err instanceof Error ? err.message : "Switch failed");
1659
- process.exit(1);
1764
+ await handleError(err);
1660
1765
  }
1661
1766
  });
1662
1767
 
@@ -1698,8 +1803,7 @@ var listCommand = new Command6("list").description("List resources").argument(
1698
1803
  process.exit(1);
1699
1804
  }
1700
1805
  } catch (err) {
1701
- error(err instanceof Error ? err.message : "List failed");
1702
- process.exit(1);
1806
+ await handleError(err);
1703
1807
  }
1704
1808
  });
1705
1809
  async function listOrganizations(api, options) {
@@ -1901,8 +2005,7 @@ var configCommand = new Command7("config").description("Manage CLI configuration
1901
2005
  process.exit(1);
1902
2006
  }
1903
2007
  } catch (err) {
1904
- error(err instanceof Error ? err.message : "Config operation failed");
1905
- process.exit(1);
2008
+ await handleError(err);
1906
2009
  }
1907
2010
  });
1908
2011
  async function handleGet(key) {
@@ -2036,14 +2139,123 @@ var logoutCommand = new Command8("logout").description("Log out from Envpilot").
2036
2139
  clearAuth();
2037
2140
  success(`Logged out${user?.email ? ` from ${user.email}` : ""}`);
2038
2141
  } catch (err) {
2039
- error(err instanceof Error ? err.message : "Logout failed");
2040
- process.exit(1);
2142
+ await handleError(err);
2143
+ }
2144
+ });
2145
+
2146
+ // src/commands/usage.ts
2147
+ import { Command as Command9 } from "commander";
2148
+ import chalk10 from "chalk";
2149
+ function formatUsage(current, limit) {
2150
+ const limitStr = limit === null ? "unlimited" : String(limit);
2151
+ const ratio = `${current}/${limitStr}`;
2152
+ if (limit === null) return chalk10.green(ratio);
2153
+ if (current >= limit) return chalk10.red(ratio);
2154
+ if (current >= limit * 0.8) return chalk10.yellow(ratio);
2155
+ return chalk10.green(ratio);
2156
+ }
2157
+ function featureStatus(enabled) {
2158
+ return enabled ? chalk10.green("Enabled") : chalk10.dim("Disabled (Pro)");
2159
+ }
2160
+ var usageCommand = new Command9("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) => {
2161
+ try {
2162
+ if (!isAuthenticated()) {
2163
+ throw notAuthenticated();
2164
+ }
2165
+ const api = createAPIClient();
2166
+ const projectConfig = readProjectConfig();
2167
+ let organizationId = options.organization || projectConfig?.organizationId || getActiveOrganizationId();
2168
+ if (!organizationId) {
2169
+ const orgs = await withSpinner(
2170
+ "Fetching organizations...",
2171
+ async () => {
2172
+ const response = await api.get("/api/cli/organizations");
2173
+ return response.data || [];
2174
+ }
2175
+ );
2176
+ if (orgs.length === 0) {
2177
+ error("No organizations found.");
2178
+ process.exit(1);
2179
+ }
2180
+ if (orgs.length === 1) {
2181
+ organizationId = orgs[0]._id;
2182
+ } else {
2183
+ error(
2184
+ "Multiple organizations found. Use --organization to specify one."
2185
+ );
2186
+ console.log();
2187
+ for (const org of orgs) {
2188
+ console.log(
2189
+ ` ${org.name} (${org.slug}): --organization ${org._id}`
2190
+ );
2191
+ }
2192
+ process.exit(1);
2193
+ }
2194
+ }
2195
+ const usage = await withSpinner(
2196
+ "Fetching usage...",
2197
+ () => api.getUsage(organizationId)
2198
+ );
2199
+ if (options.json) {
2200
+ console.log(JSON.stringify(usage, null, 2));
2201
+ return;
2202
+ }
2203
+ const tierLabel = usage.tier === "pro" ? chalk10.green("Pro") : chalk10.white("Free");
2204
+ header(`Plan: ${tierLabel}`);
2205
+ blank();
2206
+ if (!usage.enforcementEnabled) {
2207
+ info("Pre-alpha mode \u2014 all limits are bypassed. Billing coming soon.");
2208
+ blank();
2209
+ }
2210
+ header("Resource Usage");
2211
+ blank();
2212
+ keyValue([
2213
+ ["Projects", formatUsage(usage.usage.projects, usage.limits.projects)],
2214
+ [
2215
+ "Team Members",
2216
+ formatUsage(usage.usage.teamMembers, usage.limits.teamMembers)
2217
+ ],
2218
+ ["Pending Invitations", String(usage.usage.pendingInvitations)],
2219
+ ["Total Variables", String(usage.usage.totalVariables)]
2220
+ ]);
2221
+ blank();
2222
+ if (usage.usage.variablesPerProject.length > 0) {
2223
+ header("Variables per Project");
2224
+ blank();
2225
+ table(
2226
+ usage.usage.variablesPerProject.map((p) => ({
2227
+ project: p.projectName,
2228
+ variables: formatUsage(p.count, usage.limits.variablesPerProject)
2229
+ })),
2230
+ [
2231
+ { key: "project", header: "Project" },
2232
+ { key: "variables", header: "Variables" }
2233
+ ]
2234
+ );
2235
+ blank();
2236
+ }
2237
+ header("Features");
2238
+ blank();
2239
+ keyValue([
2240
+ ["Version History", featureStatus(usage.features.versionHistory)],
2241
+ ["Bulk Import", featureStatus(usage.features.bulkImport)],
2242
+ [
2243
+ "Granular Permissions",
2244
+ featureStatus(usage.features.granularPermissions)
2245
+ ],
2246
+ ["Extension Access", featureStatus(usage.features.extensionAccess)],
2247
+ ["Audit Log Retention", `${usage.features.auditLogRetentionDays} days`]
2248
+ ]);
2249
+ blank();
2250
+ } catch (err) {
2251
+ await handleError(err);
2041
2252
  }
2042
2253
  });
2043
2254
 
2044
2255
  // src/index.ts
2045
- var program = new Command9();
2046
- program.name("envpilot").description("Envpilot CLI - Sync, secure, and share environment variables").version("1.0.0");
2256
+ initSentry();
2257
+ var program = new Command10();
2258
+ program.name("envpilot").description("Envpilot CLI - Sync, secure, and share environment variables").version("1.3.0");
2047
2259
  program.addCommand(loginCommand);
2048
2260
  program.addCommand(logoutCommand);
2049
2261
  program.addCommand(initCommand);
@@ -2052,4 +2264,5 @@ program.addCommand(pushCommand);
2052
2264
  program.addCommand(switchCommand);
2053
2265
  program.addCommand(listCommand);
2054
2266
  program.addCommand(configCommand);
2267
+ program.addCommand(usageCommand);
2055
2268
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envpilot/cli",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Envpilot CLI — sync and manage environment variables from the terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,8 +12,8 @@
12
12
  "README.md"
13
13
  ],
14
14
  "scripts": {
15
- "build": "tsup src/index.ts --format esm --dts --clean",
16
- "dev": "tsup src/index.ts --format esm --watch",
15
+ "build": "tsup",
16
+ "dev": "tsup --watch",
17
17
  "lint": "eslint \"src/**/*.ts\"",
18
18
  "typecheck": "tsc --noEmit",
19
19
  "test": "vitest run",
@@ -21,6 +21,7 @@
21
21
  "prepublishOnly": "npm run build"
22
22
  },
23
23
  "dependencies": {
24
+ "@sentry/node": "^10.43.0",
24
25
  "chalk": "^5.3.0",
25
26
  "commander": "^12.1.0",
26
27
  "conf": "^13.0.1",