@keywaysh/cli 0.0.14 → 0.0.16

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 (3) hide show
  1. package/README.md +2 -1
  2. package/dist/cli.js +678 -96
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -5,7 +5,8 @@
5
5
  <br/><br/>
6
6
  <a href="https://keyway.sh">keyway.sh</a> ·
7
7
  <a href="https://github.com/keywaysh/cli">GitHub</a> ·
8
- <a href="https://www.npmjs.com/package/@keywaysh/cli">NPM</a>
8
+ <a href="https://www.npmjs.com/package/@keywaysh/cli">NPM</a> ·
9
+ <a href="https://status.keyway.sh">Status</a>
9
10
  <br/><br/>
10
11
 
11
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
package/dist/cli.js CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { Command } from "commander";
5
- import chalk7 from "chalk";
5
+ import pc9 from "picocolors";
6
6
 
7
7
  // src/cmds/init.ts
8
- import chalk4 from "chalk";
8
+ import pc4 from "picocolors";
9
9
  import prompts4 from "prompts";
10
10
 
11
11
  // src/utils/git.ts
@@ -69,7 +69,7 @@ var INTERNAL_POSTHOG_HOST = "https://eu.i.posthog.com";
69
69
  // package.json
70
70
  var package_default = {
71
71
  name: "@keywaysh/cli",
72
- version: "0.0.14",
72
+ version: "0.0.16",
73
73
  description: "One link to all your secrets",
74
74
  type: "module",
75
75
  bin: {
@@ -81,6 +81,7 @@ var package_default = {
81
81
  ],
82
82
  scripts: {
83
83
  dev: "pnpm exec tsx src/cli.ts",
84
+ "dev:local": "NODE_TLS_REJECT_UNAUTHORIZED=0 KEYWAY_API_URL=https://localhost/api pnpm exec tsx src/cli.ts",
84
85
  build: "pnpm exec tsup",
85
86
  "build:watch": "pnpm exec tsup --watch",
86
87
  prepublishOnly: "pnpm run build",
@@ -112,10 +113,10 @@ var package_default = {
112
113
  node: ">=18.0.0"
113
114
  },
114
115
  dependencies: {
115
- chalk: "^4.1.2",
116
116
  commander: "^14.0.0",
117
117
  conf: "^15.0.2",
118
118
  open: "^11.0.0",
119
+ picocolors: "^1.1.1",
119
120
  "posthog-node": "^3.5.0",
120
121
  prompts: "^2.4.2"
121
122
  },
@@ -132,6 +133,53 @@ var package_default = {
132
133
  // src/utils/api.ts
133
134
  var API_BASE_URL = process.env.KEYWAY_API_URL || INTERNAL_API_URL;
134
135
  var USER_AGENT = `keyway-cli/${package_default.version}`;
136
+ var DEFAULT_TIMEOUT_MS = 3e4;
137
+ function truncateMessage(message, maxLength = 200) {
138
+ if (message.length <= maxLength) return message;
139
+ return message.slice(0, maxLength - 3) + "...";
140
+ }
141
+ var NETWORK_ERROR_MESSAGES = {
142
+ ECONNREFUSED: "Cannot connect to Keyway API server. Is the server running?",
143
+ ECONNRESET: "Connection was reset. Please try again.",
144
+ ENOTFOUND: "DNS lookup failed. Check your internet connection.",
145
+ ETIMEDOUT: "Connection timed out. Check your network connection.",
146
+ ENETUNREACH: "Network is unreachable. Check your internet connection.",
147
+ EHOSTUNREACH: "Host is unreachable. Check your network connection.",
148
+ CERT_HAS_EXPIRED: "SSL certificate has expired. Contact support.",
149
+ UNABLE_TO_VERIFY_LEAF_SIGNATURE: "SSL certificate verification failed.",
150
+ EPROTO: "SSL/TLS protocol error. Try again later."
151
+ };
152
+ function handleNetworkError(error) {
153
+ const errorCode = error.code || error.cause?.code;
154
+ if (errorCode && NETWORK_ERROR_MESSAGES[errorCode]) {
155
+ return new Error(NETWORK_ERROR_MESSAGES[errorCode]);
156
+ }
157
+ const message = error.message.toLowerCase();
158
+ if (message.includes("fetch failed") || message.includes("network")) {
159
+ return new Error("Network error. Check your internet connection and try again.");
160
+ }
161
+ return error;
162
+ }
163
+ async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
164
+ const controller = new AbortController();
165
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
166
+ try {
167
+ return await fetch(url, {
168
+ ...options,
169
+ signal: controller.signal
170
+ });
171
+ } catch (error) {
172
+ if (error instanceof Error) {
173
+ if (error.name === "AbortError") {
174
+ throw new Error(`Request timeout after ${timeoutMs / 1e3}s. Check your network connection.`);
175
+ }
176
+ throw handleNetworkError(error);
177
+ }
178
+ throw error;
179
+ } finally {
180
+ clearTimeout(timeout);
181
+ }
182
+ }
135
183
  function validateApiUrl(url) {
136
184
  const parsed = new URL(url);
137
185
  if (parsed.protocol !== "https:") {
@@ -196,7 +244,7 @@ async function initVault(repoFullName, accessToken) {
196
244
  if (accessToken) {
197
245
  headers.Authorization = `Bearer ${accessToken}`;
198
246
  }
199
- const response = await fetch(`${API_BASE_URL}/v1/vaults`, {
247
+ const response = await fetchWithTimeout(`${API_BASE_URL}/v1/vaults`, {
200
248
  method: "POST",
201
249
  headers,
202
250
  body: JSON.stringify(body)
@@ -231,7 +279,7 @@ async function pushSecrets(repoFullName, environment, content, accessToken) {
231
279
  if (accessToken) {
232
280
  headers.Authorization = `Bearer ${accessToken}`;
233
281
  }
234
- const response = await fetch(`${API_BASE_URL}/v1/secrets/push`, {
282
+ const response = await fetchWithTimeout(`${API_BASE_URL}/v1/secrets/push`, {
235
283
  method: "POST",
236
284
  headers,
237
285
  body: JSON.stringify(body)
@@ -251,7 +299,7 @@ async function pullSecrets(repoFullName, environment, accessToken) {
251
299
  repo: repoFullName,
252
300
  environment
253
301
  });
254
- const response = await fetch(`${API_BASE_URL}/v1/secrets/pull?${params}`, {
302
+ const response = await fetchWithTimeout(`${API_BASE_URL}/v1/secrets/pull?${params}`, {
255
303
  method: "GET",
256
304
  headers
257
305
  });
@@ -259,7 +307,7 @@ async function pullSecrets(repoFullName, environment, accessToken) {
259
307
  return { content: result.data.content };
260
308
  }
261
309
  async function startDeviceLogin(repository) {
262
- const response = await fetch(`${API_BASE_URL}/v1/auth/device/start`, {
310
+ const response = await fetchWithTimeout(`${API_BASE_URL}/v1/auth/device/start`, {
263
311
  method: "POST",
264
312
  headers: {
265
313
  "Content-Type": "application/json",
@@ -270,7 +318,7 @@ async function startDeviceLogin(repository) {
270
318
  return handleResponse(response);
271
319
  }
272
320
  async function pollDeviceLogin(deviceCode) {
273
- const response = await fetch(`${API_BASE_URL}/v1/auth/device/poll`, {
321
+ const response = await fetchWithTimeout(`${API_BASE_URL}/v1/auth/device/poll`, {
274
322
  method: "POST",
275
323
  headers: {
276
324
  "Content-Type": "application/json",
@@ -281,7 +329,7 @@ async function pollDeviceLogin(deviceCode) {
281
329
  return handleResponse(response);
282
330
  }
283
331
  async function validateToken(token) {
284
- const response = await fetch(`${API_BASE_URL}/v1/auth/token/validate`, {
332
+ const response = await fetchWithTimeout(`${API_BASE_URL}/v1/auth/token/validate`, {
285
333
  method: "POST",
286
334
  headers: {
287
335
  "Content-Type": "application/json",
@@ -292,6 +340,117 @@ async function validateToken(token) {
292
340
  });
293
341
  return handleResponse(response);
294
342
  }
343
+ async function getProviders() {
344
+ const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations`, {
345
+ method: "GET",
346
+ headers: {
347
+ "User-Agent": USER_AGENT
348
+ }
349
+ });
350
+ return handleResponse(response);
351
+ }
352
+ async function getConnections(accessToken) {
353
+ const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations/connections`, {
354
+ method: "GET",
355
+ headers: {
356
+ "User-Agent": USER_AGENT,
357
+ Authorization: `Bearer ${accessToken}`
358
+ }
359
+ });
360
+ return handleResponse(response);
361
+ }
362
+ async function deleteConnection(accessToken, connectionId) {
363
+ const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations/connections/${connectionId}`, {
364
+ method: "DELETE",
365
+ headers: {
366
+ "User-Agent": USER_AGENT,
367
+ Authorization: `Bearer ${accessToken}`
368
+ }
369
+ });
370
+ return handleResponse(response);
371
+ }
372
+ function getProviderAuthUrl(provider, redirectUri) {
373
+ const params = redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : "";
374
+ return `${API_BASE_URL}/v1/integrations/${provider}/authorize${params}`;
375
+ }
376
+ async function getConnectionProjects(accessToken, connectionId) {
377
+ const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations/connections/${connectionId}/projects`, {
378
+ method: "GET",
379
+ headers: {
380
+ "User-Agent": USER_AGENT,
381
+ Authorization: `Bearer ${accessToken}`
382
+ }
383
+ });
384
+ return handleResponse(response);
385
+ }
386
+ async function getSyncStatus(accessToken, repoFullName, connectionId, projectId, environment = "production") {
387
+ const [owner, repo] = repoFullName.split("/");
388
+ const params = new URLSearchParams({
389
+ connectionId,
390
+ projectId,
391
+ environment
392
+ });
393
+ const response = await fetchWithTimeout(
394
+ `${API_BASE_URL}/v1/integrations/vaults/${owner}/${repo}/sync/status?${params}`,
395
+ {
396
+ method: "GET",
397
+ headers: {
398
+ "User-Agent": USER_AGENT,
399
+ Authorization: `Bearer ${accessToken}`
400
+ }
401
+ }
402
+ );
403
+ return handleResponse(response);
404
+ }
405
+ async function getSyncPreview(accessToken, repoFullName, options) {
406
+ const [owner, repo] = repoFullName.split("/");
407
+ const params = new URLSearchParams({
408
+ connectionId: options.connectionId,
409
+ projectId: options.projectId,
410
+ keywayEnvironment: options.keywayEnvironment || "production",
411
+ providerEnvironment: options.providerEnvironment || "production",
412
+ direction: options.direction || "push",
413
+ allowDelete: String(options.allowDelete || false)
414
+ });
415
+ const response = await fetchWithTimeout(
416
+ `${API_BASE_URL}/v1/integrations/vaults/${owner}/${repo}/sync/preview?${params}`,
417
+ {
418
+ method: "GET",
419
+ headers: {
420
+ "User-Agent": USER_AGENT,
421
+ Authorization: `Bearer ${accessToken}`
422
+ }
423
+ },
424
+ 6e4
425
+ // 60 seconds for sync operations
426
+ );
427
+ return handleResponse(response);
428
+ }
429
+ async function executeSync(accessToken, repoFullName, options) {
430
+ const [owner, repo] = repoFullName.split("/");
431
+ const response = await fetchWithTimeout(
432
+ `${API_BASE_URL}/v1/integrations/vaults/${owner}/${repo}/sync`,
433
+ {
434
+ method: "POST",
435
+ headers: {
436
+ "Content-Type": "application/json",
437
+ "User-Agent": USER_AGENT,
438
+ Authorization: `Bearer ${accessToken}`
439
+ },
440
+ body: JSON.stringify({
441
+ connectionId: options.connectionId,
442
+ projectId: options.projectId,
443
+ keywayEnvironment: options.keywayEnvironment || "production",
444
+ providerEnvironment: options.providerEnvironment || "production",
445
+ direction: options.direction || "push",
446
+ allowDelete: options.allowDelete || false
447
+ })
448
+ },
449
+ 12e4
450
+ // 2 minutes for sync execution
451
+ );
452
+ return handleResponse(response);
453
+ }
295
454
 
296
455
  // src/utils/analytics.ts
297
456
  import { PostHog } from "posthog-node";
@@ -388,11 +547,14 @@ var AnalyticsEvents = {
388
547
  CLI_PULL: "cli_pull",
389
548
  CLI_ERROR: "cli_error",
390
549
  CLI_LOGIN: "cli_login",
391
- CLI_DOCTOR: "cli_doctor"
550
+ CLI_DOCTOR: "cli_doctor",
551
+ CLI_CONNECT: "cli_connect",
552
+ CLI_DISCONNECT: "cli_disconnect",
553
+ CLI_SYNC: "cli_sync"
392
554
  };
393
555
 
394
556
  // src/cmds/login.ts
395
- import chalk from "chalk";
557
+ import pc from "picocolors";
396
558
  import readline from "readline";
397
559
  import open from "open";
398
560
  import prompts from "prompts";
@@ -511,17 +673,17 @@ async function promptYesNo(question, defaultYes = true) {
511
673
  });
512
674
  }
513
675
  async function runLoginFlow() {
514
- console.log(chalk.blue("\u{1F510} Starting Keyway login...\n"));
676
+ console.log(pc.blue("\u{1F510} Starting Keyway login...\n"));
515
677
  const repoName = detectGitRepo();
516
678
  const start = await startDeviceLogin(repoName);
517
679
  const verifyUrl = start.verificationUriComplete || start.verificationUri;
518
680
  if (!verifyUrl) {
519
681
  throw new Error("Missing verification URL from the auth server.");
520
682
  }
521
- console.log(`Code: ${chalk.green.bold(start.userCode)}`);
683
+ console.log(`Code: ${pc.bold(pc.green(start.userCode))}`);
522
684
  console.log("Waiting for auth...");
523
685
  open(verifyUrl).catch(() => {
524
- console.log(chalk.gray(`Open this URL in your browser: ${verifyUrl}`));
686
+ console.log(pc.gray(`Open this URL in your browser: ${verifyUrl}`));
525
687
  });
526
688
  const pollIntervalMs = (start.interval ?? 5) * 1e3;
527
689
  const maxTimeoutMs = Math.min((start.expiresIn ?? 900) * 1e3, 30 * 60 * 1e3);
@@ -544,9 +706,9 @@ async function runLoginFlow() {
544
706
  method: "device",
545
707
  repo: repoName
546
708
  });
547
- console.log(chalk.green("\n\u2713 Login successful"));
709
+ console.log(pc.green("\n\u2713 Login successful"));
548
710
  if (result.githubLogin) {
549
- console.log(`Authenticated GitHub user: ${chalk.cyan(result.githubLogin)}`);
711
+ console.log(`Authenticated GitHub user: ${pc.cyan(result.githubLogin)}`);
550
712
  }
551
713
  return result.keywayToken;
552
714
  }
@@ -554,10 +716,13 @@ async function runLoginFlow() {
554
716
  }
555
717
  }
556
718
  async function ensureLogin(options = {}) {
557
- const envToken = process.env.KEYWAY_TOKEN || process.env.GITHUB_TOKEN;
719
+ const envToken = process.env.KEYWAY_TOKEN;
558
720
  if (envToken) {
559
721
  return envToken;
560
722
  }
723
+ if (process.env.GITHUB_TOKEN && !process.env.KEYWAY_TOKEN) {
724
+ console.warn(pc.yellow("Note: GITHUB_TOKEN found but not used. Set KEYWAY_TOKEN for Keyway authentication."));
725
+ }
561
726
  const stored = await getStoredAuth();
562
727
  if (stored?.keywayToken) {
563
728
  return stored.keywayToken;
@@ -576,16 +741,16 @@ async function ensureLogin(options = {}) {
576
741
  async function runTokenLogin() {
577
742
  const repoName = detectGitRepo();
578
743
  if (repoName) {
579
- console.log(`\u{1F4C1} Detected: ${chalk.cyan(repoName)}`);
744
+ console.log(`\u{1F4C1} Detected: ${pc.cyan(repoName)}`);
580
745
  }
581
746
  const description = repoName ? `Keyway CLI for ${repoName}` : "Keyway CLI";
582
747
  const url = `https://github.com/settings/personal-access-tokens/new?description=${encodeURIComponent(description)}`;
583
748
  console.log("Opening GitHub...");
584
749
  open(url).catch(() => {
585
- console.log(chalk.gray(`Open this URL in your browser: ${url}`));
750
+ console.log(pc.gray(`Open this URL in your browser: ${url}`));
586
751
  });
587
- console.log(chalk.gray("Select the detected repo (or scope manually)."));
588
- console.log(chalk.gray("Permissions: Metadata \u2192 Read-only; Account permissions: None."));
752
+ console.log(pc.gray("Select the detected repo (or scope manually)."));
753
+ console.log(pc.gray("Permissions: Metadata \u2192 Read-only; Account permissions: None."));
589
754
  const { token } = await prompts(
590
755
  {
591
756
  type: "password",
@@ -618,7 +783,7 @@ async function runTokenLogin() {
618
783
  method: "pat",
619
784
  repo: repoName
620
785
  });
621
- console.log(chalk.green("\u2705 Authenticated"), `as ${chalk.cyan(`@${validation.username}`)}`);
786
+ console.log(pc.green("\u2705 Authenticated"), `as ${pc.cyan(`@${validation.username}`)}`);
622
787
  return trimmedToken;
623
788
  }
624
789
  async function loginCommand(options = {}) {
@@ -632,24 +797,24 @@ async function loginCommand(options = {}) {
632
797
  const message = error instanceof Error ? error.message : "Unexpected login error";
633
798
  trackEvent(AnalyticsEvents.CLI_ERROR, {
634
799
  command: "login",
635
- error: message.slice(0, 200)
800
+ error: truncateMessage(message)
636
801
  });
637
- console.error(chalk.red(`
802
+ console.error(pc.red(`
638
803
  \u2717 ${message}`));
639
804
  process.exit(1);
640
805
  }
641
806
  }
642
807
  async function logoutCommand() {
643
808
  clearAuth();
644
- console.log(chalk.green("\u2713 Logged out of Keyway"));
645
- console.log(chalk.gray(`Auth cache cleared: ${getAuthFilePath()}`));
809
+ console.log(pc.green("\u2713 Logged out of Keyway"));
810
+ console.log(pc.gray(`Auth cache cleared: ${getAuthFilePath()}`));
646
811
  }
647
812
 
648
813
  // src/cmds/readme.ts
649
814
  import fs2 from "fs";
650
815
  import path2 from "path";
651
816
  import prompts2 from "prompts";
652
- import chalk2 from "chalk";
817
+ import pc2 from "picocolors";
653
818
  function generateBadge(repo) {
654
819
  return `[![Keyway Secrets](https://www.keyway.sh/badge.svg?repo=${repo})](https://www.keyway.sh/vaults/${repo})`;
655
820
  }
@@ -687,7 +852,7 @@ async function ensureReadme(repoName, cwd) {
687
852
  if (existing) return existing;
688
853
  const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
689
854
  if (!isInteractive2) {
690
- console.log(chalk2.yellow('No README found. Run "keyway readme add-badge" from a repo with a README.'));
855
+ console.log(pc2.yellow('No README found. Run "keyway readme add-badge" from a repo with a README.'));
691
856
  return null;
692
857
  }
693
858
  const { confirm } = await prompts2(
@@ -702,7 +867,7 @@ async function ensureReadme(repoName, cwd) {
702
867
  }
703
868
  );
704
869
  if (!confirm) {
705
- console.log(chalk2.yellow("Skipping badge insertion (no README)."));
870
+ console.log(pc2.yellow("Skipping badge insertion (no README)."));
706
871
  return null;
707
872
  }
708
873
  const defaultPath = path2.join(cwd, "README.md");
@@ -725,19 +890,19 @@ async function addBadgeToReadme(silent = false) {
725
890
  const updated = insertBadgeIntoReadme(content, badge);
726
891
  if (updated === content) {
727
892
  if (!silent) {
728
- console.log(chalk2.gray("Keyway badge already present in README."));
893
+ console.log(pc2.gray("Keyway badge already present in README."));
729
894
  }
730
895
  return false;
731
896
  }
732
897
  fs2.writeFileSync(readmePath, updated, "utf-8");
733
898
  if (!silent) {
734
- console.log(chalk2.green(`\u2713 Keyway badge added to ${path2.basename(readmePath)}`));
899
+ console.log(pc2.green(`\u2713 Keyway badge added to ${path2.basename(readmePath)}`));
735
900
  }
736
901
  return true;
737
902
  }
738
903
 
739
904
  // src/cmds/push.ts
740
- import chalk3 from "chalk";
905
+ import pc3 from "picocolors";
741
906
  import fs3 from "fs";
742
907
  import path3 from "path";
743
908
  import prompts3 from "prompts";
@@ -754,7 +919,7 @@ function discoverEnvCandidates(cwd) {
754
919
  const entries = fs3.readdirSync(cwd);
755
920
  const hasEnvLocal = entries.includes(".env.local");
756
921
  if (hasEnvLocal) {
757
- console.log(chalk3.gray("\u2139\uFE0F Detected .env.local \u2014 not synced by design (machine-specific secrets)"));
922
+ console.log(pc3.gray("\u2139\uFE0F Detected .env.local \u2014 not synced by design (machine-specific secrets)"));
758
923
  }
759
924
  const candidates = entries.filter((name) => name.startsWith(".env") && name !== ".env.local").map((name) => {
760
925
  const fullPath = path3.join(cwd, name);
@@ -780,7 +945,7 @@ function discoverEnvCandidates(cwd) {
780
945
  }
781
946
  async function pushCommand(options) {
782
947
  try {
783
- console.log(chalk3.blue("\u{1F510} Pushing secrets to Keyway...\n"));
948
+ console.log(pc3.blue("\u{1F510} Pushing secrets to Keyway...\n"));
784
949
  const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
785
950
  let environment = options.env;
786
951
  let envFile = options.file;
@@ -880,11 +1045,11 @@ async function pushCommand(options) {
880
1045
  const trimmed = line.trim();
881
1046
  return trimmed.length > 0 && !trimmed.startsWith("#");
882
1047
  });
883
- console.log(`File: ${chalk3.cyan(envFile)}`);
884
- console.log(`Environment: ${chalk3.cyan(environment)}`);
885
- console.log(`Variables: ${chalk3.cyan(lines.length.toString())}`);
1048
+ console.log(`File: ${pc3.cyan(envFile)}`);
1049
+ console.log(`Environment: ${pc3.cyan(environment)}`);
1050
+ console.log(`Variables: ${pc3.cyan(lines.length.toString())}`);
886
1051
  const repoFullName = getCurrentRepoFullName();
887
- console.log(`Repository: ${chalk3.cyan(repoFullName)}`);
1052
+ console.log(`Repository: ${pc3.cyan(repoFullName)}`);
888
1053
  if (!options.yes) {
889
1054
  const isInteractive3 = process.stdin.isTTY && process.stdout.isTTY;
890
1055
  if (!isInteractive3) {
@@ -904,7 +1069,7 @@ async function pushCommand(options) {
904
1069
  }
905
1070
  );
906
1071
  if (!confirm) {
907
- console.log(chalk3.yellow("Push aborted."));
1072
+ console.log(pc3.yellow("Push aborted."));
908
1073
  return;
909
1074
  }
910
1075
  }
@@ -916,30 +1081,50 @@ async function pushCommand(options) {
916
1081
  });
917
1082
  console.log("\nUploading secrets...");
918
1083
  const response = await pushSecrets(repoFullName, environment, content, accessToken);
919
- console.log(chalk3.green("\n\u2713 " + response.message));
1084
+ console.log(pc3.green("\n\u2713 " + response.message));
920
1085
  if (response.stats) {
921
1086
  const { created, updated, deleted } = response.stats;
922
1087
  const parts = [];
923
- if (created > 0) parts.push(chalk3.green(`+${created} created`));
924
- if (updated > 0) parts.push(chalk3.yellow(`~${updated} updated`));
925
- if (deleted > 0) parts.push(chalk3.red(`-${deleted} deleted`));
1088
+ if (created > 0) parts.push(pc3.green(`+${created} created`));
1089
+ if (updated > 0) parts.push(pc3.yellow(`~${updated} updated`));
1090
+ if (deleted > 0) parts.push(pc3.red(`-${deleted} deleted`));
926
1091
  if (parts.length > 0) {
927
1092
  console.log(`Stats: ${parts.join(", ")}`);
928
1093
  }
929
1094
  }
930
1095
  console.log(`
931
1096
  Your secrets are now encrypted and stored securely.`);
932
- console.log(`To retrieve them, run: ${chalk3.cyan(`keyway pull --env ${environment}`)}`);
1097
+ console.log(`To retrieve them, run: ${pc3.cyan(`keyway pull --env ${environment}`)}`);
933
1098
  await shutdownAnalytics();
934
1099
  } catch (error) {
935
- const message = error instanceof APIError ? `API ${error.statusCode}: ${error.message}` : error instanceof Error ? error.message.slice(0, 200) : "Unknown error";
1100
+ let message;
1101
+ let hint = null;
1102
+ if (error instanceof APIError) {
1103
+ message = error.message || `HTTP ${error.statusCode} - ${error.error}`;
1104
+ const envNotFoundMatch = message.match(/Environment '([^']+)' does not exist.*Available environments: ([^.]+)/);
1105
+ if (envNotFoundMatch) {
1106
+ const requestedEnv = envNotFoundMatch[1];
1107
+ const availableEnvs = envNotFoundMatch[2];
1108
+ message = `Environment '${requestedEnv}' does not exist in this vault.`;
1109
+ hint = `Available environments: ${availableEnvs}
1110
+ Use ${pc3.cyan(`keyway push --env <environment>`)} to specify one, or create '${requestedEnv}' via the dashboard.`;
1111
+ }
1112
+ } else if (error instanceof Error) {
1113
+ message = truncateMessage(error.message);
1114
+ } else {
1115
+ message = "Unknown error";
1116
+ }
936
1117
  trackEvent(AnalyticsEvents.CLI_ERROR, {
937
1118
  command: "push",
938
1119
  error: message
939
1120
  });
940
1121
  await shutdownAnalytics();
941
- console.error(chalk3.red(`
942
- \u2717 Error: ${message}`));
1122
+ console.error(pc3.red(`
1123
+ \u2717 ${message}`));
1124
+ if (hint) {
1125
+ console.error(pc3.gray(`
1126
+ ${hint}`));
1127
+ }
943
1128
  process.exit(1);
944
1129
  }
945
1130
  }
@@ -950,16 +1135,16 @@ async function initCommand(options = {}) {
950
1135
  try {
951
1136
  const repoFullName = getCurrentRepoFullName();
952
1137
  const dashboardLink = `${DASHBOARD_URL}/${repoFullName}`;
953
- console.log(chalk4.blue("\u{1F510} Initializing Keyway vault...\n"));
954
- console.log(` ${chalk4.gray("Repository:")} ${chalk4.white(repoFullName)}`);
1138
+ console.log(pc4.blue("\u{1F510} Initializing Keyway vault...\n"));
1139
+ console.log(` ${pc4.gray("Repository:")} ${pc4.white(repoFullName)}`);
955
1140
  const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
956
1141
  trackEvent(AnalyticsEvents.CLI_INIT, { repoFullName });
957
1142
  const response = await initVault(repoFullName, accessToken);
958
- console.log(chalk4.green("\u2713 Vault created!"));
1143
+ console.log(pc4.green("\u2713 Vault created!"));
959
1144
  try {
960
1145
  const badgeAdded = await addBadgeToReadme(true);
961
1146
  if (badgeAdded) {
962
- console.log(chalk4.green("\u2713 Badge added to README.md"));
1147
+ console.log(pc4.green("\u2713 Badge added to README.md"));
963
1148
  }
964
1149
  } catch {
965
1150
  }
@@ -967,7 +1152,7 @@ async function initCommand(options = {}) {
967
1152
  const envCandidates = discoverEnvCandidates(process.cwd());
968
1153
  const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
969
1154
  if (envCandidates.length > 0 && isInteractive2) {
970
- console.log(chalk4.gray(` Found ${envCandidates.length} env file(s): ${envCandidates.map((c) => c.file).join(", ")}
1155
+ console.log(pc4.gray(` Found ${envCandidates.length} env file(s): ${envCandidates.map((c) => c.file).join(", ")}
971
1156
  `));
972
1157
  const { shouldPush } = await prompts4({
973
1158
  type: "confirm",
@@ -981,60 +1166,59 @@ async function initCommand(options = {}) {
981
1166
  return;
982
1167
  }
983
1168
  }
984
- console.log(chalk4.dim("\u2500".repeat(50)));
1169
+ console.log(pc4.dim("\u2500".repeat(50)));
985
1170
  console.log("");
986
1171
  if (envCandidates.length === 0) {
987
- console.log(` ${chalk4.yellow("\u2192")} Create a ${chalk4.cyan(".env")} file with your secrets`);
988
- console.log(` ${chalk4.yellow("\u2192")} Run ${chalk4.cyan("keyway push")} to sync them
1172
+ console.log(` ${pc4.yellow("\u2192")} Create a ${pc4.cyan(".env")} file with your secrets`);
1173
+ console.log(` ${pc4.yellow("\u2192")} Run ${pc4.cyan("keyway push")} to sync them
989
1174
  `);
990
1175
  } else {
991
- console.log(` ${chalk4.yellow("\u2192")} Run ${chalk4.cyan("keyway push")} to sync your secrets
1176
+ console.log(` ${pc4.yellow("\u2192")} Run ${pc4.cyan("keyway push")} to sync your secrets
992
1177
  `);
993
1178
  }
994
- console.log(` ${chalk4.blue("\u2394")} Dashboard: ${chalk4.underline(dashboardLink)}`);
1179
+ console.log(` ${pc4.blue("\u2394")} Dashboard: ${pc4.underline(dashboardLink)}`);
995
1180
  console.log("");
996
1181
  await shutdownAnalytics();
997
1182
  } catch (error) {
998
1183
  if (error instanceof APIError) {
999
1184
  if (error.statusCode === 409) {
1000
- console.log(chalk4.yellow("\n\u26A0 Vault already exists for this repository.\n"));
1001
- console.log(` ${chalk4.yellow("\u2192")} Run ${chalk4.cyan("keyway push")} to sync your secrets`);
1002
- console.log(` ${chalk4.blue("\u2394")} Dashboard: ${chalk4.underline(`${DASHBOARD_URL}/${getCurrentRepoFullName()}`)}`);
1185
+ console.log(pc4.yellow("\n\u26A0 Vault already exists for this repository.\n"));
1186
+ console.log(` ${pc4.yellow("\u2192")} Run ${pc4.cyan("keyway push")} to sync your secrets`);
1187
+ console.log(` ${pc4.blue("\u2394")} Dashboard: ${pc4.underline(`${DASHBOARD_URL}/${getCurrentRepoFullName()}`)}`);
1003
1188
  console.log("");
1004
1189
  await shutdownAnalytics();
1005
1190
  return;
1006
1191
  }
1007
1192
  if (error.error === "PLAN_LIMIT_REACHED") {
1008
1193
  console.log("");
1009
- console.log(chalk4.dim("\u2500".repeat(50)));
1194
+ console.log(pc4.dim("\u2500".repeat(50)));
1010
1195
  console.log("");
1011
- console.log(` ${chalk4.yellow("\u26A1")} ${chalk4.bold("Upgrade to Pro")}`);
1196
+ console.log(` ${pc4.yellow("\u26A1")} ${pc4.bold("Upgrade Required")}`);
1012
1197
  console.log("");
1013
- console.log(chalk4.gray(" You've reached the limit of your free plan."));
1014
- console.log(chalk4.gray(" Upgrade to Pro for unlimited private repositories."));
1198
+ console.log(pc4.gray(` ${error.message}`));
1015
1199
  console.log("");
1016
- console.log(` ${chalk4.cyan("\u2192")} ${chalk4.underline(error.upgradeUrl || "https://keyway.sh/upgrade")}`);
1200
+ console.log(` ${pc4.cyan("\u2192")} ${pc4.underline(error.upgradeUrl || "https://keyway.sh/upgrade")}`);
1017
1201
  console.log("");
1018
- console.log(chalk4.dim("\u2500".repeat(50)));
1202
+ console.log(pc4.dim("\u2500".repeat(50)));
1019
1203
  console.log("");
1020
1204
  await shutdownAnalytics();
1021
1205
  process.exit(1);
1022
1206
  }
1023
1207
  }
1024
- const message = error instanceof APIError ? error.message : error instanceof Error ? error.message.slice(0, 200) : "Unknown error";
1208
+ const message = error instanceof APIError ? error.message : error instanceof Error ? truncateMessage(error.message) : "Unknown error";
1025
1209
  trackEvent(AnalyticsEvents.CLI_ERROR, {
1026
1210
  command: "init",
1027
1211
  error: message
1028
1212
  });
1029
1213
  await shutdownAnalytics();
1030
- console.error(chalk4.red(`
1214
+ console.error(pc4.red(`
1031
1215
  \u2717 ${message}`));
1032
1216
  process.exit(1);
1033
1217
  }
1034
1218
  }
1035
1219
 
1036
1220
  // src/cmds/pull.ts
1037
- import chalk5 from "chalk";
1221
+ import pc5 from "picocolors";
1038
1222
  import fs4 from "fs";
1039
1223
  import path4 from "path";
1040
1224
  import prompts5 from "prompts";
@@ -1042,10 +1226,10 @@ async function pullCommand(options) {
1042
1226
  try {
1043
1227
  const environment = options.env || "development";
1044
1228
  const envFile = options.file || ".env";
1045
- console.log(chalk5.blue("\u{1F510} Pulling secrets from Keyway...\n"));
1046
- console.log(`Environment: ${chalk5.cyan(environment)}`);
1229
+ console.log(pc5.blue("\u{1F510} Pulling secrets from Keyway...\n"));
1230
+ console.log(`Environment: ${pc5.cyan(environment)}`);
1047
1231
  const repoFullName = getCurrentRepoFullName();
1048
- console.log(`Repository: ${chalk5.cyan(repoFullName)}`);
1232
+ console.log(`Repository: ${pc5.cyan(repoFullName)}`);
1049
1233
  const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
1050
1234
  trackEvent(AnalyticsEvents.CLI_PULL, {
1051
1235
  repoFullName,
@@ -1057,7 +1241,7 @@ async function pullCommand(options) {
1057
1241
  if (fs4.existsSync(envFilePath)) {
1058
1242
  const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
1059
1243
  if (options.yes) {
1060
- console.log(chalk5.yellow(`
1244
+ console.log(pc5.yellow(`
1061
1245
  \u26A0 Overwriting existing file: ${envFile}`));
1062
1246
  } else if (!isInteractive2) {
1063
1247
  throw new Error(`File ${envFile} exists. Re-run with --yes to overwrite or choose a different --file.`);
@@ -1076,7 +1260,7 @@ async function pullCommand(options) {
1076
1260
  }
1077
1261
  );
1078
1262
  if (!confirm) {
1079
- console.log(chalk5.yellow("Pull aborted."));
1263
+ console.log(pc5.yellow("Pull aborted."));
1080
1264
  return;
1081
1265
  }
1082
1266
  }
@@ -1086,27 +1270,27 @@ async function pullCommand(options) {
1086
1270
  const trimmed = line.trim();
1087
1271
  return trimmed.length > 0 && !trimmed.startsWith("#");
1088
1272
  });
1089
- console.log(chalk5.green(`
1273
+ console.log(pc5.green(`
1090
1274
  \u2713 Secrets downloaded successfully`));
1091
1275
  console.log(`
1092
- File: ${chalk5.cyan(envFile)}`);
1093
- console.log(`Variables: ${chalk5.cyan(lines.length.toString())}`);
1276
+ File: ${pc5.cyan(envFile)}`);
1277
+ console.log(`Variables: ${pc5.cyan(lines.length.toString())}`);
1094
1278
  await shutdownAnalytics();
1095
1279
  } catch (error) {
1096
- const message = error instanceof APIError ? `API ${error.statusCode}: ${error.message}` : error instanceof Error ? error.message.slice(0, 200) : "Unknown error";
1280
+ const message = error instanceof APIError ? `API ${error.statusCode}: ${error.message}` : error instanceof Error ? truncateMessage(error.message) : "Unknown error";
1097
1281
  trackEvent(AnalyticsEvents.CLI_ERROR, {
1098
1282
  command: "pull",
1099
1283
  error: message
1100
1284
  });
1101
1285
  await shutdownAnalytics();
1102
- console.error(chalk5.red(`
1103
- \u2717 Error: ${message}`));
1286
+ console.error(pc5.red(`
1287
+ \u2717 ${message}`));
1104
1288
  process.exit(1);
1105
1289
  }
1106
1290
  }
1107
1291
 
1108
1292
  // src/cmds/doctor.ts
1109
- import chalk6 from "chalk";
1293
+ import pc6 from "picocolors";
1110
1294
 
1111
1295
  // src/core/doctor.ts
1112
1296
  import { execSync as execSync2 } from "child_process";
@@ -1360,9 +1544,9 @@ async function runAllChecks(options = {}) {
1360
1544
  // src/cmds/doctor.ts
1361
1545
  function formatSummary(results) {
1362
1546
  const parts = [
1363
- chalk6.green(`${results.summary.pass} passed`),
1364
- results.summary.warn > 0 ? chalk6.yellow(`${results.summary.warn} warnings`) : null,
1365
- results.summary.fail > 0 ? chalk6.red(`${results.summary.fail} failed`) : null
1547
+ pc6.green(`${results.summary.pass} passed`),
1548
+ results.summary.warn > 0 ? pc6.yellow(`${results.summary.warn} warnings`) : null,
1549
+ results.summary.fail > 0 ? pc6.red(`${results.summary.fail} failed`) : null
1366
1550
  ].filter(Boolean);
1367
1551
  return parts.join(", ");
1368
1552
  }
@@ -1379,24 +1563,24 @@ async function doctorCommand(options = {}) {
1379
1563
  process.stdout.write(JSON.stringify(results, null, 0) + "\n");
1380
1564
  process.exit(results.exitCode);
1381
1565
  }
1382
- console.log(chalk6.cyan("\n\u{1F50D} Keyway Doctor - Environment Check\n"));
1566
+ console.log(pc6.cyan("\n\u{1F50D} Keyway Doctor - Environment Check\n"));
1383
1567
  results.checks.forEach((check) => {
1384
- const icon = check.status === "pass" ? chalk6.green("\u2713") : check.status === "warn" ? chalk6.yellow("!") : chalk6.red("\u2717");
1385
- const detail = check.detail ? chalk6.dim(` \u2014 ${check.detail}`) : "";
1568
+ const icon = check.status === "pass" ? pc6.green("\u2713") : check.status === "warn" ? pc6.yellow("!") : pc6.red("\u2717");
1569
+ const detail = check.detail ? pc6.dim(` \u2014 ${check.detail}`) : "";
1386
1570
  console.log(` ${icon} ${check.name}${detail}`);
1387
1571
  });
1388
1572
  console.log(`
1389
1573
  Summary: ${formatSummary(results)}`);
1390
1574
  if (results.summary.fail > 0) {
1391
- console.log(chalk6.red("\u26A0 Some checks failed. Please resolve the issues above before using Keyway."));
1575
+ console.log(pc6.red("\u26A0 Some checks failed. Please resolve the issues above before using Keyway."));
1392
1576
  } else if (results.summary.warn > 0) {
1393
- console.log(chalk6.yellow("\u26A0 Some warnings detected. Keyway should work but consider addressing them."));
1577
+ console.log(pc6.yellow("\u26A0 Some warnings detected. Keyway should work but consider addressing them."));
1394
1578
  } else {
1395
- console.log(chalk6.green("\u2728 All checks passed! Your environment is ready for Keyway."));
1579
+ console.log(pc6.green("\u2728 All checks passed! Your environment is ready for Keyway."));
1396
1580
  }
1397
1581
  process.exit(results.exitCode);
1398
1582
  } catch (error) {
1399
- const message = error instanceof Error ? error.message : "Doctor failed";
1583
+ const message = error instanceof Error ? truncateMessage(error.message) : "Doctor failed";
1400
1584
  trackEvent(AnalyticsEvents.CLI_DOCTOR, {
1401
1585
  pass: 0,
1402
1586
  warn: 0,
@@ -1413,8 +1597,394 @@ Summary: ${formatSummary(results)}`);
1413
1597
  };
1414
1598
  process.stdout.write(JSON.stringify(errorResult, null, 0) + "\n");
1415
1599
  } else {
1416
- console.error(chalk6.red(`\u2716 Doctor check failed: ${message}`));
1600
+ console.error(pc6.red(`
1601
+ \u2717 ${message}`));
1602
+ }
1603
+ process.exit(1);
1604
+ }
1605
+ }
1606
+
1607
+ // src/cmds/connect.ts
1608
+ import pc7 from "picocolors";
1609
+ import open2 from "open";
1610
+ import prompts6 from "prompts";
1611
+ async function connectCommand(provider, options = {}) {
1612
+ try {
1613
+ const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
1614
+ const { providers } = await getProviders();
1615
+ const providerInfo = providers.find((p) => p.name === provider.toLowerCase());
1616
+ if (!providerInfo) {
1617
+ const available = providers.map((p) => p.name).join(", ");
1618
+ console.error(pc7.red(`Unknown provider: ${provider}`));
1619
+ console.log(pc7.gray(`Available providers: ${available || "none"}`));
1620
+ process.exit(1);
1621
+ }
1622
+ if (!providerInfo.configured) {
1623
+ console.error(pc7.red(`Provider ${providerInfo.displayName} is not configured on the server.`));
1624
+ console.log(pc7.gray("Contact your administrator to enable this integration."));
1625
+ process.exit(1);
1626
+ }
1627
+ const { connections } = await getConnections(accessToken);
1628
+ const existingConnection = connections.find((c) => c.provider === provider.toLowerCase());
1629
+ if (existingConnection) {
1630
+ const { reconnect } = await prompts6({
1631
+ type: "confirm",
1632
+ name: "reconnect",
1633
+ message: `You're already connected to ${providerInfo.displayName}. Reconnect?`,
1634
+ initial: false
1635
+ });
1636
+ if (!reconnect) {
1637
+ console.log(pc7.gray("Keeping existing connection."));
1638
+ return;
1639
+ }
1640
+ }
1641
+ console.log(pc7.blue(`
1642
+ Connecting to ${providerInfo.displayName}...
1643
+ `));
1644
+ const authUrl = getProviderAuthUrl(provider.toLowerCase());
1645
+ const startTime = /* @__PURE__ */ new Date();
1646
+ console.log(pc7.gray("Opening browser for authorization..."));
1647
+ console.log(pc7.gray(`If the browser doesn't open, visit: ${authUrl}`));
1648
+ await open2(authUrl).catch(() => {
1649
+ });
1650
+ console.log(pc7.gray("Waiting for authorization..."));
1651
+ const maxAttempts = 60;
1652
+ let attempts = 0;
1653
+ let connected = false;
1654
+ while (attempts < maxAttempts) {
1655
+ await new Promise((resolve) => setTimeout(resolve, 5e3));
1656
+ attempts++;
1657
+ try {
1658
+ const { connections: connections2 } = await getConnections(accessToken);
1659
+ const newConn = connections2.find(
1660
+ (c) => c.provider === provider.toLowerCase() && new Date(c.createdAt) > startTime
1661
+ );
1662
+ if (newConn) {
1663
+ connected = true;
1664
+ console.log(pc7.green(`
1665
+ \u2713 Connected to ${providerInfo.displayName}!`));
1666
+ break;
1667
+ }
1668
+ } catch {
1669
+ }
1670
+ }
1671
+ if (!connected) {
1672
+ console.log(pc7.red("\n\u2717 Authorization timeout."));
1673
+ console.log(pc7.gray("Run `keyway connections` to check if the connection was established."));
1674
+ }
1675
+ trackEvent(AnalyticsEvents.CLI_CONNECT, {
1676
+ provider: provider.toLowerCase(),
1677
+ success: connected
1678
+ });
1679
+ } catch (error) {
1680
+ const message = error instanceof Error ? error.message : "Connection failed";
1681
+ trackEvent(AnalyticsEvents.CLI_ERROR, {
1682
+ command: "connect",
1683
+ error: truncateMessage(message)
1684
+ });
1685
+ console.error(pc7.red(`
1686
+ \u2717 ${message}`));
1687
+ process.exit(1);
1688
+ }
1689
+ }
1690
+ async function connectionsCommand(options = {}) {
1691
+ try {
1692
+ const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
1693
+ const { connections } = await getConnections(accessToken);
1694
+ if (connections.length === 0) {
1695
+ console.log(pc7.gray("No provider connections found."));
1696
+ console.log(pc7.gray("\nConnect to a provider with: keyway connect <provider>"));
1697
+ console.log(pc7.gray("Available providers: vercel"));
1698
+ return;
1699
+ }
1700
+ console.log(pc7.blue("\n\u{1F4E1} Provider Connections\n"));
1701
+ for (const conn of connections) {
1702
+ const providerName = conn.provider.charAt(0).toUpperCase() + conn.provider.slice(1);
1703
+ const teamInfo = conn.providerTeamId ? pc7.gray(` (Team: ${conn.providerTeamId})`) : "";
1704
+ const date = new Date(conn.createdAt).toLocaleDateString();
1705
+ console.log(` ${pc7.green("\u25CF")} ${pc7.bold(providerName)}${teamInfo}`);
1706
+ console.log(pc7.gray(` Connected: ${date}`));
1707
+ console.log(pc7.gray(` ID: ${conn.id}`));
1708
+ console.log("");
1709
+ }
1710
+ } catch (error) {
1711
+ const message = error instanceof Error ? error.message : "Failed to list connections";
1712
+ console.error(pc7.red(`
1713
+ \u2717 ${message}`));
1714
+ process.exit(1);
1715
+ }
1716
+ }
1717
+ async function disconnectCommand(provider, options = {}) {
1718
+ try {
1719
+ const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
1720
+ const { connections } = await getConnections(accessToken);
1721
+ const connection = connections.find((c) => c.provider === provider.toLowerCase());
1722
+ if (!connection) {
1723
+ console.log(pc7.gray(`No connection found for provider: ${provider}`));
1724
+ return;
1725
+ }
1726
+ const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
1727
+ const { confirm } = await prompts6({
1728
+ type: "confirm",
1729
+ name: "confirm",
1730
+ message: `Disconnect from ${providerName}?`,
1731
+ initial: false
1732
+ });
1733
+ if (!confirm) {
1734
+ console.log(pc7.gray("Cancelled."));
1735
+ return;
1736
+ }
1737
+ await deleteConnection(accessToken, connection.id);
1738
+ console.log(pc7.green(`
1739
+ \u2713 Disconnected from ${providerName}`));
1740
+ trackEvent(AnalyticsEvents.CLI_DISCONNECT, {
1741
+ provider: provider.toLowerCase()
1742
+ });
1743
+ } catch (error) {
1744
+ const message = error instanceof Error ? error.message : "Disconnect failed";
1745
+ trackEvent(AnalyticsEvents.CLI_ERROR, {
1746
+ command: "disconnect",
1747
+ error: truncateMessage(message)
1748
+ });
1749
+ console.error(pc7.red(`
1750
+ \u2717 ${message}`));
1751
+ process.exit(1);
1752
+ }
1753
+ }
1754
+
1755
+ // src/cmds/sync.ts
1756
+ import pc8 from "picocolors";
1757
+ import prompts7 from "prompts";
1758
+ function findMatchingProject(projects, repoFullName) {
1759
+ const repoName = repoFullName.split("/")[1]?.toLowerCase();
1760
+ if (!repoName) return void 0;
1761
+ const exact = projects.find((p) => p.name.toLowerCase() === repoName);
1762
+ if (exact) return exact;
1763
+ const partial = projects.filter(
1764
+ (p) => p.name.toLowerCase().includes(repoName) || repoName.includes(p.name.toLowerCase())
1765
+ );
1766
+ return partial.length === 1 ? partial[0] : void 0;
1767
+ }
1768
+ async function syncCommand(provider, options = {}) {
1769
+ try {
1770
+ if (options.pull && options.allowDelete) {
1771
+ console.error(pc8.red("Error: --allow-delete cannot be used with --pull"));
1772
+ console.log(pc8.gray("The --allow-delete flag is only for push operations."));
1773
+ process.exit(1);
1774
+ }
1775
+ const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
1776
+ const repoFullName = detectGitRepo();
1777
+ if (!repoFullName) {
1778
+ console.error(pc8.red("Could not detect Git repository."));
1779
+ console.log(pc8.gray("Run this command from a Git repository directory."));
1780
+ process.exit(1);
1781
+ }
1782
+ console.log(pc8.gray(`Repository: ${repoFullName}`));
1783
+ const { connections } = await getConnections(accessToken);
1784
+ const connection = connections.find((c) => c.provider === provider.toLowerCase());
1785
+ if (!connection) {
1786
+ console.error(pc8.red(`Not connected to ${provider}.`));
1787
+ console.log(pc8.gray(`Run: keyway connect ${provider}`));
1788
+ process.exit(1);
1789
+ }
1790
+ const { projects } = await getConnectionProjects(accessToken, connection.id);
1791
+ if (projects.length === 0) {
1792
+ console.error(pc8.red(`No projects found in your ${provider} account.`));
1793
+ process.exit(1);
1794
+ }
1795
+ let selectedProject;
1796
+ if (options.project) {
1797
+ const found = projects.find(
1798
+ (p) => p.id === options.project || p.name.toLowerCase() === options.project?.toLowerCase()
1799
+ );
1800
+ if (!found) {
1801
+ console.error(pc8.red(`Project not found: ${options.project}`));
1802
+ console.log(pc8.gray("Available projects:"));
1803
+ projects.forEach((p) => console.log(pc8.gray(` - ${p.name}`)));
1804
+ process.exit(1);
1805
+ }
1806
+ selectedProject = found;
1807
+ } else {
1808
+ const autoMatch = findMatchingProject(projects, repoFullName);
1809
+ if (autoMatch && projects.length > 1) {
1810
+ console.log(pc8.gray(`Detected project: ${autoMatch.name}`));
1811
+ const { useDetected } = await prompts7({
1812
+ type: "confirm",
1813
+ name: "useDetected",
1814
+ message: `Use ${autoMatch.name}?`,
1815
+ initial: true
1816
+ });
1817
+ if (useDetected) {
1818
+ selectedProject = autoMatch;
1819
+ } else {
1820
+ const { projectChoice } = await prompts7({
1821
+ type: "select",
1822
+ name: "projectChoice",
1823
+ message: "Select a project:",
1824
+ choices: projects.map((p) => ({ title: p.name, value: p.id }))
1825
+ });
1826
+ selectedProject = projects.find((p) => p.id === projectChoice);
1827
+ }
1828
+ } else if (autoMatch) {
1829
+ selectedProject = autoMatch;
1830
+ } else if (projects.length === 1) {
1831
+ selectedProject = projects[0];
1832
+ } else {
1833
+ const { projectChoice } = await prompts7({
1834
+ type: "select",
1835
+ name: "projectChoice",
1836
+ message: "Select a project:",
1837
+ choices: projects.map((p) => ({ title: p.name, value: p.id }))
1838
+ });
1839
+ if (!projectChoice) {
1840
+ console.log(pc8.gray("Cancelled."));
1841
+ process.exit(0);
1842
+ }
1843
+ selectedProject = projects.find((p) => p.id === projectChoice);
1844
+ }
1845
+ }
1846
+ const keywayEnv = options.environment || "production";
1847
+ const providerEnv = options.providerEnv || "production";
1848
+ const direction = options.pull ? "pull" : "push";
1849
+ const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
1850
+ console.log(pc8.gray(`Project: ${selectedProject.name}`));
1851
+ console.log(pc8.gray(`Direction: ${direction === "push" ? "Keyway \u2192 " + providerName : providerName + " \u2192 Keyway"}`));
1852
+ const status = await getSyncStatus(
1853
+ accessToken,
1854
+ repoFullName,
1855
+ connection.id,
1856
+ selectedProject.id,
1857
+ keywayEnv
1858
+ );
1859
+ if (status.isFirstSync && !options.pull && status.vaultIsEmpty && status.providerHasSecrets) {
1860
+ console.log(pc8.yellow(`
1861
+ \u26A0\uFE0F Your Keyway vault is empty, but ${providerName} has ${status.providerSecretCount} secrets.`));
1862
+ const { importFirst } = await prompts7({
1863
+ type: "confirm",
1864
+ name: "importFirst",
1865
+ message: `Import secrets from ${providerName} first?`,
1866
+ initial: true
1867
+ });
1868
+ if (importFirst) {
1869
+ await executeSyncOperation(
1870
+ accessToken,
1871
+ repoFullName,
1872
+ connection.id,
1873
+ selectedProject,
1874
+ keywayEnv,
1875
+ providerEnv,
1876
+ "pull",
1877
+ false,
1878
+ // Never delete on import
1879
+ options.yes || false,
1880
+ provider
1881
+ );
1882
+ return;
1883
+ }
1884
+ }
1885
+ await executeSyncOperation(
1886
+ accessToken,
1887
+ repoFullName,
1888
+ connection.id,
1889
+ selectedProject,
1890
+ keywayEnv,
1891
+ providerEnv,
1892
+ direction,
1893
+ options.allowDelete || false,
1894
+ options.yes || false,
1895
+ provider
1896
+ );
1897
+ } catch (error) {
1898
+ const message = error instanceof Error ? error.message : "Sync failed";
1899
+ trackEvent(AnalyticsEvents.CLI_ERROR, {
1900
+ command: "sync",
1901
+ error: truncateMessage(message)
1902
+ });
1903
+ console.error(pc8.red(`
1904
+ \u2717 ${message}`));
1905
+ process.exit(1);
1906
+ }
1907
+ }
1908
+ async function executeSyncOperation(accessToken, repoFullName, connectionId, project, keywayEnv, providerEnv, direction, allowDelete, skipConfirm, provider) {
1909
+ const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
1910
+ const preview = await getSyncPreview(accessToken, repoFullName, {
1911
+ connectionId,
1912
+ projectId: project.id,
1913
+ keywayEnvironment: keywayEnv,
1914
+ providerEnvironment: providerEnv,
1915
+ direction,
1916
+ allowDelete
1917
+ });
1918
+ const totalChanges = preview.toCreate.length + preview.toUpdate.length + preview.toDelete.length;
1919
+ if (totalChanges === 0) {
1920
+ console.log(pc8.green("\n\u2713 Already in sync. No changes needed."));
1921
+ return;
1922
+ }
1923
+ console.log(pc8.blue("\n\u{1F4CB} Sync Preview\n"));
1924
+ if (preview.toCreate.length > 0) {
1925
+ console.log(pc8.green(` + ${preview.toCreate.length} to create`));
1926
+ preview.toCreate.slice(0, 5).forEach((key) => console.log(pc8.gray(` ${key}`)));
1927
+ if (preview.toCreate.length > 5) {
1928
+ console.log(pc8.gray(` ... and ${preview.toCreate.length - 5} more`));
1929
+ }
1930
+ }
1931
+ if (preview.toUpdate.length > 0) {
1932
+ console.log(pc8.yellow(` ~ ${preview.toUpdate.length} to update`));
1933
+ preview.toUpdate.slice(0, 5).forEach((key) => console.log(pc8.gray(` ${key}`)));
1934
+ if (preview.toUpdate.length > 5) {
1935
+ console.log(pc8.gray(` ... and ${preview.toUpdate.length - 5} more`));
1936
+ }
1937
+ }
1938
+ if (preview.toDelete.length > 0) {
1939
+ console.log(pc8.red(` - ${preview.toDelete.length} to delete`));
1940
+ preview.toDelete.slice(0, 5).forEach((key) => console.log(pc8.gray(` ${key}`)));
1941
+ if (preview.toDelete.length > 5) {
1942
+ console.log(pc8.gray(` ... and ${preview.toDelete.length - 5} more`));
1417
1943
  }
1944
+ }
1945
+ if (preview.toSkip.length > 0) {
1946
+ console.log(pc8.gray(` \u25CB ${preview.toSkip.length} unchanged`));
1947
+ }
1948
+ console.log("");
1949
+ if (!skipConfirm) {
1950
+ const target = direction === "push" ? providerName : "Keyway";
1951
+ const { confirm } = await prompts7({
1952
+ type: "confirm",
1953
+ name: "confirm",
1954
+ message: `Apply ${totalChanges} changes to ${target}?`,
1955
+ initial: true
1956
+ });
1957
+ if (!confirm) {
1958
+ console.log(pc8.gray("Cancelled."));
1959
+ return;
1960
+ }
1961
+ }
1962
+ console.log(pc8.blue("\n\u23F3 Syncing...\n"));
1963
+ const result = await executeSync(accessToken, repoFullName, {
1964
+ connectionId,
1965
+ projectId: project.id,
1966
+ keywayEnvironment: keywayEnv,
1967
+ providerEnvironment: providerEnv,
1968
+ direction,
1969
+ allowDelete
1970
+ });
1971
+ if (result.success) {
1972
+ console.log(pc8.green("\u2713 Sync complete"));
1973
+ console.log(pc8.gray(` Created: ${result.stats.created}`));
1974
+ console.log(pc8.gray(` Updated: ${result.stats.updated}`));
1975
+ if (result.stats.deleted > 0) {
1976
+ console.log(pc8.gray(` Deleted: ${result.stats.deleted}`));
1977
+ }
1978
+ trackEvent(AnalyticsEvents.CLI_SYNC, {
1979
+ provider,
1980
+ direction,
1981
+ created: result.stats.created,
1982
+ updated: result.stats.updated,
1983
+ deleted: result.stats.deleted
1984
+ });
1985
+ } else {
1986
+ console.error(pc8.red(`
1987
+ \u2717 ${result.error}`));
1418
1988
  process.exit(1);
1419
1989
  }
1420
1990
  }
@@ -1422,8 +1992,8 @@ Summary: ${formatSummary(results)}`);
1422
1992
  // src/cli.ts
1423
1993
  var program = new Command();
1424
1994
  var showBanner = () => {
1425
- const text = chalk7.cyan.bold("Keyway CLI");
1426
- const subtitle = chalk7.gray("GitHub-native secrets manager for dev teams");
1995
+ const text = pc9.bold(pc9.cyan("Keyway CLI"));
1996
+ const subtitle = pc9.gray("GitHub-native secrets manager for dev teams");
1427
1997
  console.log(`
1428
1998
  ${text}
1429
1999
  ${subtitle}
@@ -1449,7 +2019,19 @@ program.command("logout").description("Clear stored Keyway credentials").action(
1449
2019
  program.command("doctor").description("Run environment checks to ensure Keyway runs smoothly").option("--json", "Output results as JSON for machine processing", false).option("--strict", "Treat warnings as failures", false).action(async (options) => {
1450
2020
  await doctorCommand(options);
1451
2021
  });
2022
+ program.command("connect <provider>").description("Connect to an external provider (e.g., vercel)").option("--no-login-prompt", "Fail instead of prompting to login if unauthenticated").action(async (provider, options) => {
2023
+ await connectCommand(provider, options);
2024
+ });
2025
+ program.command("connections").description("List your provider connections").option("--no-login-prompt", "Fail instead of prompting to login if unauthenticated").action(async (options) => {
2026
+ await connectionsCommand(options);
2027
+ });
2028
+ program.command("disconnect <provider>").description("Disconnect from a provider").option("--no-login-prompt", "Fail instead of prompting to login if unauthenticated").action(async (provider, options) => {
2029
+ await disconnectCommand(provider, options);
2030
+ });
2031
+ program.command("sync <provider>").description("Sync secrets with a provider (e.g., vercel)").option("--pull", "Import secrets from provider to Keyway").option("-e, --environment <env>", "Keyway environment", "production").option("--provider-env <env>", "Provider environment", "production").option("--project <project>", "Provider project name or ID").option("--allow-delete", "Allow deleting secrets not in source").option("-y, --yes", "Skip confirmation prompt").option("--no-login-prompt", "Fail instead of prompting to login if unauthenticated").action(async (provider, options) => {
2032
+ await syncCommand(provider, options);
2033
+ });
1452
2034
  program.parseAsync().catch((error) => {
1453
- console.error(chalk7.red("Error:"), error.message || error);
2035
+ console.error(pc9.red("Error:"), error.message || error);
1454
2036
  process.exit(1);
1455
2037
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keywaysh/cli",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "One link to all your secrets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,10 +31,10 @@
31
31
  "node": ">=18.0.0"
32
32
  },
33
33
  "dependencies": {
34
- "chalk": "^4.1.2",
35
34
  "commander": "^14.0.0",
36
35
  "conf": "^15.0.2",
37
36
  "open": "^11.0.0",
37
+ "picocolors": "^1.1.1",
38
38
  "posthog-node": "^3.5.0",
39
39
  "prompts": "^2.4.2"
40
40
  },
@@ -48,6 +48,7 @@
48
48
  },
49
49
  "scripts": {
50
50
  "dev": "pnpm exec tsx src/cli.ts",
51
+ "dev:local": "NODE_TLS_REJECT_UNAUTHORIZED=0 KEYWAY_API_URL=https://localhost/api pnpm exec tsx src/cli.ts",
51
52
  "build": "pnpm exec tsup",
52
53
  "build:watch": "pnpm exec tsup --watch",
53
54
  "test": "pnpm exec vitest run",