@keywaysh/cli 0.1.5 → 0.1.7

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/cli.js +387 -302
  2. package/package.json +4 -1
package/dist/cli.js CHANGED
@@ -8,12 +8,11 @@ import {
8
8
 
9
9
  // src/cli.ts
10
10
  import { Command } from "commander";
11
- import pc10 from "picocolors";
11
+ import pc11 from "picocolors";
12
12
 
13
13
  // src/cmds/init.ts
14
- import pc5 from "picocolors";
14
+ import pc6 from "picocolors";
15
15
  import prompts4 from "prompts";
16
- import open2 from "open";
17
16
 
18
17
  // src/utils/git.ts
19
18
  import { execSync } from "child_process";
@@ -102,7 +101,7 @@ var INTERNAL_POSTHOG_HOST = "https://eu.i.posthog.com";
102
101
  // package.json
103
102
  var package_default = {
104
103
  name: "@keywaysh/cli",
105
- version: "0.1.5",
104
+ version: "0.1.7",
106
105
  description: "One link to all your secrets",
107
106
  type: "module",
108
107
  bin: {
@@ -120,6 +119,7 @@ var package_default = {
120
119
  prepublishOnly: "pnpm run build",
121
120
  test: "pnpm exec vitest run",
122
121
  "test:watch": "pnpm exec vitest",
122
+ "test:coverage": "pnpm exec vitest run --coverage",
123
123
  release: "npm version patch && git push && git push --tags",
124
124
  "release:minor": "npm version minor && git push && git push --tags",
125
125
  "release:major": "npm version major && git push && git push --tags"
@@ -158,6 +158,8 @@ var package_default = {
158
158
  "@types/balanced-match": "^3.0.2",
159
159
  "@types/node": "^24.2.0",
160
160
  "@types/prompts": "^2.4.9",
161
+ "@vitest/coverage-v8": "^3.0.0",
162
+ msw: "^2.12.4",
161
163
  tsup: "^8.5.0",
162
164
  tsx: "^4.20.3",
163
165
  typescript: "^5.9.2",
@@ -442,6 +444,29 @@ async function getSyncStatus(accessToken, repoFullName, connectionId, projectId,
442
444
  const wrapped = await handleResponse(response);
443
445
  return wrapped.data;
444
446
  }
447
+ async function getSyncDiff(accessToken, repoFullName, options) {
448
+ const [owner, repo] = repoFullName.split("/");
449
+ const params = new URLSearchParams({
450
+ connectionId: options.connectionId,
451
+ projectId: options.projectId,
452
+ keywayEnvironment: options.keywayEnvironment || "production",
453
+ providerEnvironment: options.providerEnvironment || "production"
454
+ });
455
+ const response = await fetchWithTimeout(
456
+ `${API_BASE_URL}/v1/integrations/vaults/${owner}/${repo}/sync/diff?${params}`,
457
+ {
458
+ method: "GET",
459
+ headers: {
460
+ "User-Agent": USER_AGENT,
461
+ Authorization: `Bearer ${accessToken}`
462
+ }
463
+ },
464
+ 6e4
465
+ // 60 seconds
466
+ );
467
+ const wrapped = await handleResponse(response);
468
+ return wrapped.data;
469
+ }
445
470
  async function getSyncPreview(accessToken, repoFullName, options) {
446
471
  const [owner, repo] = repoFullName.split("/");
447
472
  const params = new URLSearchParams({
@@ -776,8 +801,8 @@ function findReadmePath(cwd) {
776
801
  async function ensureReadme(repoName, cwd) {
777
802
  const existing = findReadmePath(cwd);
778
803
  if (existing) return existing;
779
- const isInteractive3 = process.stdin.isTTY && process.stdout.isTTY;
780
- if (!isInteractive3) {
804
+ const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
805
+ if (!isInteractive2) {
781
806
  console.log(pc2.yellow('No README found. Run "keyway readme add-badge" from a repo with a README.'));
782
807
  return null;
783
808
  }
@@ -828,22 +853,49 @@ async function addBadgeToReadme(silent = false) {
828
853
  }
829
854
 
830
855
  // src/cmds/push.ts
831
- import pc4 from "picocolors";
856
+ import pc5 from "picocolors";
832
857
  import fs4 from "fs";
833
858
  import path4 from "path";
834
859
  import prompts3 from "prompts";
835
860
 
836
861
  // src/cmds/login.ts
837
- import pc3 from "picocolors";
862
+ import pc4 from "picocolors";
838
863
  import readline from "readline";
839
- import open from "open";
840
864
  import prompts2 from "prompts";
865
+
866
+ // src/utils/helpers.ts
867
+ import pc3 from "picocolors";
868
+ import open from "open";
841
869
  function sleep(ms) {
842
870
  return new Promise((resolve) => setTimeout(resolve, ms));
843
871
  }
872
+ async function openUrl(url) {
873
+ console.log(pc3.gray(`
874
+ Open this URL in your browser:
875
+ ${url}
876
+ `));
877
+ await open(url).catch(() => {
878
+ });
879
+ }
844
880
  function isInteractive() {
845
881
  return Boolean(process.stdout.isTTY && process.stdin.isTTY && !process.env.CI);
846
882
  }
883
+ function showUpgradePrompt(message, upgradeUrl) {
884
+ console.log("");
885
+ console.log(pc3.dim("\u2500".repeat(50)));
886
+ console.log("");
887
+ console.log(` ${pc3.yellow("\u26A1")} ${pc3.bold("Plan Limit Reached")}`);
888
+ console.log("");
889
+ console.log(pc3.white(` ${message}`));
890
+ console.log("");
891
+ console.log(` ${pc3.cyan("Upgrade now \u2192")} ${pc3.underline(upgradeUrl)}`);
892
+ console.log("");
893
+ console.log(pc3.dim("\u2500".repeat(50)));
894
+ console.log("");
895
+ }
896
+ var MAX_CONSECUTIVE_ERRORS = 5;
897
+
898
+ // src/cmds/login.ts
847
899
  async function promptYesNo(question, defaultYes = true) {
848
900
  return new Promise((resolve) => {
849
901
  const rl = readline.createInterface({
@@ -861,52 +913,60 @@ async function promptYesNo(question, defaultYes = true) {
861
913
  });
862
914
  }
863
915
  async function runLoginFlow() {
864
- console.log(pc3.blue("\u{1F510} Starting Keyway login...\n"));
916
+ console.log(pc4.blue("\u{1F510} Starting Keyway login...\n"));
865
917
  const repoName = detectGitRepo();
866
918
  const start = await startDeviceLogin(repoName);
867
919
  const verifyUrl = start.verificationUriComplete || start.verificationUri;
868
920
  if (!verifyUrl) {
869
921
  throw new Error("Missing verification URL from the auth server.");
870
922
  }
871
- console.log(`Code: ${pc3.bold(pc3.green(start.userCode))}`);
923
+ console.log(`Code: ${pc4.bold(pc4.green(start.userCode))}`);
924
+ await openUrl(verifyUrl);
872
925
  console.log("Waiting for auth...");
873
- open(verifyUrl).catch(() => {
874
- console.log(pc3.gray(`Open this URL in your browser: ${verifyUrl}`));
875
- });
876
926
  const pollIntervalMs = (start.interval ?? 5) * 1e3;
877
927
  const maxTimeoutMs = Math.min((start.expiresIn ?? 900) * 1e3, 30 * 60 * 1e3);
878
928
  const startTime = Date.now();
929
+ let consecutiveErrors = 0;
879
930
  while (true) {
880
931
  if (Date.now() - startTime > maxTimeoutMs) {
881
932
  throw new Error('Login timed out. Please run "keyway login" again.');
882
933
  }
883
934
  await sleep(pollIntervalMs);
884
- const result = await pollDeviceLogin(start.deviceCode);
885
- if (result.status === "pending") {
886
- continue;
887
- }
888
- if (result.status === "approved" && result.keywayToken) {
889
- await saveAuthToken(result.keywayToken, {
890
- githubLogin: result.githubLogin,
891
- expiresAt: result.expiresAt
892
- });
893
- trackEvent(AnalyticsEvents.CLI_LOGIN, {
894
- method: "device",
895
- repo: repoName
896
- });
897
- if (result.githubLogin) {
898
- identifyUser(result.githubLogin, {
899
- github_username: result.githubLogin,
900
- login_method: "device"
935
+ try {
936
+ const result = await pollDeviceLogin(start.deviceCode);
937
+ consecutiveErrors = 0;
938
+ if (result.status === "pending") {
939
+ continue;
940
+ }
941
+ if (result.status === "approved" && result.keywayToken) {
942
+ await saveAuthToken(result.keywayToken, {
943
+ githubLogin: result.githubLogin,
944
+ expiresAt: result.expiresAt
901
945
  });
946
+ trackEvent(AnalyticsEvents.CLI_LOGIN, {
947
+ method: "device",
948
+ repo: repoName
949
+ });
950
+ if (result.githubLogin) {
951
+ identifyUser(result.githubLogin, {
952
+ github_username: result.githubLogin,
953
+ login_method: "device"
954
+ });
955
+ }
956
+ console.log(pc4.green("\n\u2713 Login successful"));
957
+ if (result.githubLogin) {
958
+ console.log(`Authenticated GitHub user: ${pc4.cyan(result.githubLogin)}`);
959
+ }
960
+ return result.keywayToken;
902
961
  }
903
- console.log(pc3.green("\n\u2713 Login successful"));
904
- if (result.githubLogin) {
905
- console.log(`Authenticated GitHub user: ${pc3.cyan(result.githubLogin)}`);
962
+ throw new Error(result.message || "Authentication failed");
963
+ } catch (error) {
964
+ consecutiveErrors++;
965
+ if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
966
+ const errorMsg = error instanceof Error ? error.message : "Unknown error";
967
+ throw new Error(`Login failed after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${errorMsg}`);
906
968
  }
907
- return result.keywayToken;
908
969
  }
909
- throw new Error(result.message || "Authentication failed");
910
970
  }
911
971
  }
912
972
  async function ensureLogin(options = {}) {
@@ -915,7 +975,7 @@ async function ensureLogin(options = {}) {
915
975
  return envToken;
916
976
  }
917
977
  if (process.env.GITHUB_TOKEN && !process.env.KEYWAY_TOKEN) {
918
- console.warn(pc3.yellow("Note: GITHUB_TOKEN found but not used. Set KEYWAY_TOKEN for Keyway authentication."));
978
+ console.warn(pc4.yellow("Note: GITHUB_TOKEN found but not used. Set KEYWAY_TOKEN for Keyway authentication."));
919
979
  }
920
980
  const stored = await getStoredAuth();
921
981
  if (stored?.keywayToken) {
@@ -935,16 +995,13 @@ async function ensureLogin(options = {}) {
935
995
  async function runTokenLogin() {
936
996
  const repoName = detectGitRepo();
937
997
  if (repoName) {
938
- console.log(`\u{1F4C1} Detected: ${pc3.cyan(repoName)}`);
998
+ console.log(`\u{1F4C1} Detected: ${pc4.cyan(repoName)}`);
939
999
  }
940
1000
  const description = repoName ? `Keyway CLI for ${repoName}` : "Keyway CLI";
941
1001
  const url = `https://github.com/settings/personal-access-tokens/new?description=${encodeURIComponent(description)}`;
942
- console.log("Opening GitHub...");
943
- open(url).catch(() => {
944
- console.log(pc3.gray(`Open this URL in your browser: ${url}`));
945
- });
946
- console.log(pc3.gray("Select the detected repo (or scope manually)."));
947
- console.log(pc3.gray("Permissions: Metadata \u2192 Read-only; Account permissions: None."));
1002
+ await openUrl(url);
1003
+ console.log(pc4.gray("Select the detected repo (or scope manually)."));
1004
+ console.log(pc4.gray("Permissions: Metadata \u2192 Read-only; Account permissions: None."));
948
1005
  const { token } = await prompts2(
949
1006
  {
950
1007
  type: "password",
@@ -981,7 +1038,7 @@ async function runTokenLogin() {
981
1038
  github_username: validation.username,
982
1039
  login_method: "pat"
983
1040
  });
984
- console.log(pc3.green("\u2705 Authenticated"), `as ${pc3.cyan(`@${validation.username}`)}`);
1041
+ console.log(pc4.green("\u2705 Authenticated"), `as ${pc4.cyan(`@${validation.username}`)}`);
985
1042
  return trimmedToken;
986
1043
  }
987
1044
  async function loginCommand(options = {}) {
@@ -997,15 +1054,15 @@ async function loginCommand(options = {}) {
997
1054
  command: "login",
998
1055
  error: truncateMessage(message)
999
1056
  });
1000
- console.error(pc3.red(`
1057
+ console.error(pc4.red(`
1001
1058
  \u2717 ${message}`));
1002
1059
  process.exit(1);
1003
1060
  }
1004
1061
  }
1005
1062
  async function logoutCommand() {
1006
1063
  clearAuth();
1007
- console.log(pc3.green("\u2713 Logged out of Keyway"));
1008
- console.log(pc3.gray(`Auth cache cleared: ${getAuthFilePath()}`));
1064
+ console.log(pc4.green("\u2713 Logged out of Keyway"));
1065
+ console.log(pc4.gray(`Auth cache cleared: ${getAuthFilePath()}`));
1009
1066
  }
1010
1067
 
1011
1068
  // src/cmds/push.ts
@@ -1022,7 +1079,7 @@ function discoverEnvCandidates(cwd) {
1022
1079
  const entries = fs4.readdirSync(cwd);
1023
1080
  const hasEnvLocal = entries.includes(".env.local");
1024
1081
  if (hasEnvLocal) {
1025
- console.log(pc4.gray("\u2139\uFE0F Detected .env.local \u2014 not synced by design (machine-specific secrets)"));
1082
+ console.log(pc5.gray("\u2139\uFE0F Detected .env.local \u2014 not synced by design (machine-specific secrets)"));
1026
1083
  }
1027
1084
  const candidates = entries.filter((name) => name.startsWith(".env") && name !== ".env.local").map((name) => {
1028
1085
  const fullPath = path4.join(cwd, name);
@@ -1048,18 +1105,21 @@ function discoverEnvCandidates(cwd) {
1048
1105
  }
1049
1106
  async function pushCommand(options) {
1050
1107
  try {
1051
- console.log(pc4.blue("\u{1F510} Pushing secrets to Keyway...\n"));
1052
- const isInteractive3 = process.stdin.isTTY && process.stdout.isTTY;
1108
+ console.log(pc5.blue("\u{1F510} Pushing secrets to Keyway...\n"));
1109
+ const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
1053
1110
  let environment = options.env;
1054
1111
  let envFile = options.file;
1055
1112
  const candidates = discoverEnvCandidates(process.cwd());
1113
+ if (candidates.length === 0 && !envFile) {
1114
+ throw new Error("No .env file found. Create a .env file first, or use --file <path> to specify one.");
1115
+ }
1056
1116
  if (environment && !envFile) {
1057
1117
  const match = candidates.find((c) => c.env === environment);
1058
1118
  if (match) {
1059
1119
  envFile = match.file;
1060
1120
  }
1061
1121
  }
1062
- if (!environment && !envFile && isInteractive3 && candidates.length > 0) {
1122
+ if (!environment && !envFile && isInteractive2 && candidates.length > 0) {
1063
1123
  const { choice } = await prompts3(
1064
1124
  {
1065
1125
  type: "select",
@@ -1111,34 +1171,9 @@ async function pushCommand(options) {
1111
1171
  if (!envFile) {
1112
1172
  envFile = ".env";
1113
1173
  }
1114
- let envFilePath = path4.resolve(process.cwd(), envFile);
1174
+ const envFilePath = path4.resolve(process.cwd(), envFile);
1115
1175
  if (!fs4.existsSync(envFilePath)) {
1116
- if (!isInteractive3) {
1117
- throw new Error(`File not found: ${envFile}. Provide --file <path> or run interactively to choose a file.`);
1118
- }
1119
- const { newPath } = await prompts3(
1120
- {
1121
- type: "text",
1122
- name: "newPath",
1123
- message: `File not found: ${envFile}. Enter an env file path to use:`,
1124
- validate: (value) => {
1125
- if (!value || typeof value !== "string") return "Path is required";
1126
- const resolved = path4.resolve(process.cwd(), value);
1127
- if (!fs4.existsSync(resolved)) return `File not found: ${value}`;
1128
- return true;
1129
- }
1130
- },
1131
- {
1132
- onCancel: () => {
1133
- throw new Error("Push cancelled (no env file provided).");
1134
- }
1135
- }
1136
- );
1137
- if (!newPath || typeof newPath !== "string") {
1138
- throw new Error("Push cancelled (no env file provided).");
1139
- }
1140
- envFile = newPath.trim();
1141
- envFilePath = path4.resolve(process.cwd(), envFile);
1176
+ throw new Error(`File not found: ${envFile}`);
1142
1177
  }
1143
1178
  const content = fs4.readFileSync(envFilePath, "utf-8");
1144
1179
  if (content.trim().length === 0) {
@@ -1148,14 +1183,14 @@ async function pushCommand(options) {
1148
1183
  const trimmed = line.trim();
1149
1184
  return trimmed.length > 0 && !trimmed.startsWith("#");
1150
1185
  });
1151
- console.log(`File: ${pc4.cyan(envFile)}`);
1152
- console.log(`Environment: ${pc4.cyan(environment)}`);
1153
- console.log(`Variables: ${pc4.cyan(lines.length.toString())}`);
1186
+ console.log(`File: ${pc5.cyan(envFile)}`);
1187
+ console.log(`Environment: ${pc5.cyan(environment)}`);
1188
+ console.log(`Variables: ${pc5.cyan(lines.length.toString())}`);
1154
1189
  const repoFullName = getCurrentRepoFullName();
1155
- console.log(`Repository: ${pc4.cyan(repoFullName)}`);
1190
+ console.log(`Repository: ${pc5.cyan(repoFullName)}`);
1156
1191
  if (!options.yes) {
1157
- const isInteractive4 = process.stdin.isTTY && process.stdout.isTTY;
1158
- if (!isInteractive4) {
1192
+ const isInteractive3 = process.stdin.isTTY && process.stdout.isTTY;
1193
+ if (!isInteractive3) {
1159
1194
  throw new Error("Confirmation required. Re-run with --yes in non-interactive environments.");
1160
1195
  }
1161
1196
  const { confirm } = await prompts3(
@@ -1172,7 +1207,7 @@ async function pushCommand(options) {
1172
1207
  }
1173
1208
  );
1174
1209
  if (!confirm) {
1175
- console.log(pc4.yellow("Push aborted."));
1210
+ console.log(pc5.yellow("Push aborted."));
1176
1211
  return;
1177
1212
  }
1178
1213
  }
@@ -1184,20 +1219,22 @@ async function pushCommand(options) {
1184
1219
  });
1185
1220
  console.log("\nUploading secrets...");
1186
1221
  const response = await pushSecrets(repoFullName, environment, content, accessToken);
1187
- console.log(pc4.green("\n\u2713 " + response.message));
1222
+ console.log(pc5.green("\n\u2713 " + response.message));
1188
1223
  if (response.stats) {
1189
1224
  const { created, updated, deleted } = response.stats;
1190
1225
  const parts = [];
1191
- if (created > 0) parts.push(pc4.green(`+${created} created`));
1192
- if (updated > 0) parts.push(pc4.yellow(`~${updated} updated`));
1193
- if (deleted > 0) parts.push(pc4.red(`-${deleted} deleted`));
1226
+ if (created > 0) parts.push(pc5.green(`+${created} created`));
1227
+ if (updated > 0) parts.push(pc5.yellow(`~${updated} updated`));
1228
+ if (deleted > 0) parts.push(pc5.red(`-${deleted} deleted`));
1194
1229
  if (parts.length > 0) {
1195
1230
  console.log(`Stats: ${parts.join(", ")}`);
1196
1231
  }
1197
1232
  }
1198
1233
  console.log(`
1199
1234
  Your secrets are now encrypted and stored securely.`);
1200
- console.log(`To retrieve them, run: ${pc4.cyan(`keyway pull --env ${environment}`)}`);
1235
+ const dashboardLink = `https://www.keyway.sh/dashboard/vaults/${repoFullName}`;
1236
+ console.log(`
1237
+ ${pc5.blue("\u2394")} Dashboard: ${pc5.underline(dashboardLink)}`);
1201
1238
  await shutdownAnalytics();
1202
1239
  } catch (error) {
1203
1240
  let message;
@@ -1210,13 +1247,18 @@ Your secrets are now encrypted and stored securely.`);
1210
1247
  const availableEnvs = envNotFoundMatch[2];
1211
1248
  message = `Environment '${requestedEnv}' does not exist in this vault.`;
1212
1249
  hint = `Available environments: ${availableEnvs}
1213
- Use ${pc4.cyan(`keyway push --env <environment>`)} to specify one, or create '${requestedEnv}' via the dashboard.`;
1250
+ Use ${pc5.cyan(`keyway push --env <environment>`)} to specify one, or create '${requestedEnv}' via the dashboard.`;
1214
1251
  }
1215
- if (error.statusCode === 403 && error.upgradeUrl) {
1216
- hint = `${pc4.yellow("\u26A1")} Upgrade to Pro: ${pc4.cyan(error.upgradeUrl)}`;
1217
- } else if (error.statusCode === 403 && message.toLowerCase().includes("read-only")) {
1218
- message = "This vault is read-only on your current plan.";
1219
- hint = `Upgrade to Pro to unlock editing: ${pc4.cyan("https://keyway.sh/settings")}`;
1252
+ if (error.statusCode === 403 && (error.upgradeUrl || message.toLowerCase().includes("read-only"))) {
1253
+ const upgradeMessage = message.toLowerCase().includes("read-only") ? "This vault is read-only on your current plan." : message;
1254
+ const upgradeUrl = error.upgradeUrl || "https://keyway.sh/settings";
1255
+ trackEvent(AnalyticsEvents.CLI_ERROR, {
1256
+ command: "push",
1257
+ error: upgradeMessage
1258
+ });
1259
+ await shutdownAnalytics();
1260
+ showUpgradePrompt(upgradeMessage, upgradeUrl);
1261
+ process.exit(1);
1220
1262
  }
1221
1263
  } else if (error instanceof Error) {
1222
1264
  message = truncateMessage(error.message);
@@ -1228,10 +1270,10 @@ Use ${pc4.cyan(`keyway push --env <environment>`)} to specify one, or create '${
1228
1270
  error: message
1229
1271
  });
1230
1272
  await shutdownAnalytics();
1231
- console.error(pc4.red(`
1273
+ console.error(pc5.red(`
1232
1274
  \u2717 ${message}`));
1233
1275
  if (hint) {
1234
- console.error(pc4.gray(`
1276
+ console.error(pc5.gray(`
1235
1277
  ${hint}`));
1236
1278
  }
1237
1279
  process.exit(1);
@@ -1242,12 +1284,6 @@ ${hint}`));
1242
1284
  var DASHBOARD_URL = "https://www.keyway.sh/dashboard/vaults";
1243
1285
  var POLL_INTERVAL_MS = 3e3;
1244
1286
  var POLL_TIMEOUT_MS = 12e4;
1245
- function sleep2(ms) {
1246
- return new Promise((resolve) => setTimeout(resolve, ms));
1247
- }
1248
- function isInteractive2() {
1249
- return Boolean(process.stdout.isTTY && process.stdin.isTTY && !process.env.CI);
1250
- }
1251
1287
  async function ensureLoginAndGitHubApp(repoFullName, options = {}) {
1252
1288
  const [repoOwner, repoName] = repoFullName.split("/");
1253
1289
  const envToken = process.env.KEYWAY_TOKEN;
@@ -1266,12 +1302,12 @@ async function ensureLoginAndGitHubApp(repoFullName, options = {}) {
1266
1302
  }
1267
1303
  }
1268
1304
  const allowPrompt = options.allowPrompt !== false;
1269
- if (!allowPrompt || !isInteractive2()) {
1305
+ if (!allowPrompt || !isInteractive()) {
1270
1306
  throw new Error('No Keyway session found. Run "keyway login" to authenticate.');
1271
1307
  }
1272
1308
  console.log("");
1273
- console.log(pc5.gray(" Keyway uses a GitHub App for secure access."));
1274
- console.log(pc5.gray(" Installing the app will also log you in."));
1309
+ console.log(pc6.gray(" Keyway uses a GitHub App for secure access."));
1310
+ console.log(pc6.gray(" Installing the app will also log you in."));
1275
1311
  console.log("");
1276
1312
  const { shouldProceed } = await prompts4({
1277
1313
  type: "confirm",
@@ -1284,16 +1320,15 @@ async function ensureLoginAndGitHubApp(repoFullName, options = {}) {
1284
1320
  }
1285
1321
  const deviceStart = await startDeviceLogin(repoFullName);
1286
1322
  const installUrl = deviceStart.githubAppInstallUrl || "https://github.com/apps/keyway/installations/new";
1287
- console.log(pc5.gray("\n Opening browser..."));
1288
- await open2(installUrl);
1289
- console.log("");
1290
- console.log(pc5.blue("\u23F3 Waiting for installation & authorization..."));
1291
- console.log(pc5.gray(" (Press Ctrl+C to cancel)\n"));
1323
+ await openUrl(installUrl);
1324
+ console.log(pc6.blue("\u23F3 Waiting for installation & authorization..."));
1325
+ console.log(pc6.gray(" (Press Ctrl+C to cancel)\n"));
1292
1326
  const pollIntervalMs = Math.max((deviceStart.interval ?? 5) * 1e3, POLL_INTERVAL_MS);
1293
1327
  const startTime = Date.now();
1294
1328
  let accessToken = null;
1329
+ let consecutiveErrors = 0;
1295
1330
  while (Date.now() - startTime < POLL_TIMEOUT_MS) {
1296
- await sleep2(pollIntervalMs);
1331
+ await sleep(pollIntervalMs);
1297
1332
  try {
1298
1333
  if (!accessToken) {
1299
1334
  const result = await pollDeviceLogin(deviceStart.deviceCode);
@@ -1303,7 +1338,7 @@ async function ensureLoginAndGitHubApp(repoFullName, options = {}) {
1303
1338
  githubLogin: result.githubLogin,
1304
1339
  expiresAt: result.expiresAt
1305
1340
  });
1306
- console.log(pc5.green("\u2713 Signed in!"));
1341
+ console.log(pc6.green("\u2713 Signed in!"));
1307
1342
  if (result.githubLogin) {
1308
1343
  identifyUser(result.githubLogin, {
1309
1344
  github_username: result.githubLogin,
@@ -1315,18 +1350,24 @@ async function ensureLoginAndGitHubApp(repoFullName, options = {}) {
1315
1350
  if (accessToken) {
1316
1351
  const installStatus = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
1317
1352
  if (installStatus.installed) {
1318
- console.log(pc5.green("\u2713 GitHub App installed!"));
1353
+ console.log(pc6.green("\u2713 GitHub App installed!"));
1319
1354
  console.log("");
1320
1355
  return accessToken;
1321
1356
  }
1322
1357
  }
1323
- process.stdout.write(pc5.gray("."));
1324
- } catch {
1358
+ consecutiveErrors = 0;
1359
+ process.stdout.write(pc6.gray("."));
1360
+ } catch (error) {
1361
+ consecutiveErrors++;
1362
+ if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
1363
+ const errorMsg = error instanceof Error ? error.message : "Unknown error";
1364
+ throw new Error(`Setup failed after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${errorMsg}`);
1365
+ }
1325
1366
  }
1326
1367
  }
1327
1368
  console.log("");
1328
- console.log(pc5.yellow("\u26A0 Timed out waiting for setup."));
1329
- console.log(pc5.gray(` Install the GitHub App: ${installUrl}`));
1369
+ console.log(pc6.yellow("\u26A0 Timed out waiting for setup."));
1370
+ console.log(pc6.gray(` Install the GitHub App: ${installUrl}`));
1330
1371
  throw new Error("Setup timed out. Please try again.");
1331
1372
  }
1332
1373
  async function ensureGitHubAppInstalledOnly(repoFullName, accessToken) {
@@ -1336,7 +1377,7 @@ async function ensureGitHubAppInstalledOnly(repoFullName, accessToken) {
1336
1377
  status = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
1337
1378
  } catch (error) {
1338
1379
  if (error instanceof APIError && error.statusCode === 401) {
1339
- console.log(pc5.yellow("\n\u26A0 Session expired or invalid. Clearing credentials..."));
1380
+ console.log(pc6.yellow("\n\u26A0 Session expired or invalid. Clearing credentials..."));
1340
1381
  const { clearAuth: clearAuth2 } = await import("./auth-QLPQ24HZ.js");
1341
1382
  clearAuth2();
1342
1383
  return null;
@@ -1347,13 +1388,13 @@ async function ensureGitHubAppInstalledOnly(repoFullName, accessToken) {
1347
1388
  return accessToken;
1348
1389
  }
1349
1390
  console.log("");
1350
- console.log(pc5.yellow("\u26A0 GitHub App not installed for this repository"));
1391
+ console.log(pc6.yellow("\u26A0 GitHub App not installed for this repository"));
1351
1392
  console.log("");
1352
- console.log(pc5.gray(" The Keyway GitHub App is required to securely manage secrets."));
1353
- console.log(pc5.gray(" It only requests minimal permissions (repository metadata)."));
1393
+ console.log(pc6.gray(" The Keyway GitHub App is required to securely manage secrets."));
1394
+ console.log(pc6.gray(" It only requests minimal permissions (repository metadata)."));
1354
1395
  console.log("");
1355
- if (!isInteractive2()) {
1356
- console.log(pc5.gray(` Install the Keyway GitHub App: ${status.installUrl}`));
1396
+ if (!isInteractive()) {
1397
+ console.log(pc6.gray(` Install the Keyway GitHub App: ${status.installUrl}`));
1357
1398
  throw new Error("GitHub App installation required.");
1358
1399
  }
1359
1400
  const { shouldInstall } = await prompts4({
@@ -1363,67 +1404,72 @@ async function ensureGitHubAppInstalledOnly(repoFullName, accessToken) {
1363
1404
  initial: true
1364
1405
  });
1365
1406
  if (!shouldInstall) {
1366
- console.log(pc5.gray(`
1407
+ console.log(pc6.gray(`
1367
1408
  You can install later: ${status.installUrl}`));
1368
1409
  throw new Error("GitHub App installation required.");
1369
1410
  }
1370
- console.log(pc5.gray("\n Opening browser..."));
1371
- await open2(status.installUrl);
1372
- console.log("");
1373
- console.log(pc5.blue("\u23F3 Waiting for GitHub App installation..."));
1374
- console.log(pc5.gray(" (Press Ctrl+C to cancel)\n"));
1411
+ await openUrl(status.installUrl);
1412
+ console.log(pc6.blue("\u23F3 Waiting for GitHub App installation..."));
1413
+ console.log(pc6.gray(" (Press Ctrl+C to cancel)\n"));
1375
1414
  const startTime = Date.now();
1415
+ let consecutiveErrors = 0;
1376
1416
  while (Date.now() - startTime < POLL_TIMEOUT_MS) {
1377
- await sleep2(POLL_INTERVAL_MS);
1417
+ await sleep(POLL_INTERVAL_MS);
1378
1418
  try {
1379
1419
  const pollStatus = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
1380
1420
  if (pollStatus.installed) {
1381
- console.log(pc5.green("\u2713 GitHub App installed!"));
1421
+ console.log(pc6.green("\u2713 GitHub App installed!"));
1382
1422
  console.log("");
1383
1423
  return accessToken;
1384
1424
  }
1385
- process.stdout.write(pc5.gray("."));
1386
- } catch {
1425
+ consecutiveErrors = 0;
1426
+ process.stdout.write(pc6.gray("."));
1427
+ } catch (error) {
1428
+ consecutiveErrors++;
1429
+ if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
1430
+ const errorMsg = error instanceof Error ? error.message : "Unknown error";
1431
+ throw new Error(`Installation check failed after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${errorMsg}`);
1432
+ }
1387
1433
  }
1388
1434
  }
1389
1435
  console.log("");
1390
- console.log(pc5.yellow("\u26A0 Timed out waiting for installation."));
1391
- console.log(pc5.gray(` You can install the GitHub App later: ${status.installUrl}`));
1436
+ console.log(pc6.yellow("\u26A0 Timed out waiting for installation."));
1437
+ console.log(pc6.gray(` You can install the GitHub App later: ${status.installUrl}`));
1392
1438
  throw new Error("GitHub App installation timed out.");
1393
1439
  }
1394
1440
  async function initCommand(options = {}) {
1395
1441
  try {
1396
1442
  const repoFullName = getCurrentRepoFullName();
1397
1443
  const dashboardLink = `${DASHBOARD_URL}/${repoFullName}`;
1398
- console.log(pc5.blue("\u{1F510} Initializing Keyway vault...\n"));
1399
- console.log(` ${pc5.gray("Repository:")} ${pc5.white(repoFullName)}`);
1444
+ console.log(pc6.blue("\u{1F510} Initializing Keyway vault...\n"));
1445
+ console.log(` ${pc6.gray("Repository:")} ${pc6.white(repoFullName)}`);
1400
1446
  const accessToken = await ensureLoginAndGitHubApp(repoFullName, {
1401
1447
  allowPrompt: options.loginPrompt !== false
1402
1448
  });
1403
1449
  trackEvent(AnalyticsEvents.CLI_INIT, { repoFullName, githubAppInstalled: true });
1404
1450
  const vaultExists = await checkVaultExists(accessToken, repoFullName);
1405
1451
  if (vaultExists) {
1406
- console.log(pc5.green("\n\u2713 Already initialized!\n"));
1407
- console.log(` ${pc5.yellow("\u2192")} Run ${pc5.cyan("keyway push")} to sync your secrets`);
1408
- console.log(` ${pc5.blue("\u2394")} Dashboard: ${pc5.underline(dashboardLink)}`);
1452
+ console.log(pc6.green("\n\u2713 Already initialized!\n"));
1453
+ console.log(` ${pc6.yellow("\u2192")} Run ${pc6.cyan("keyway push")} to sync your secrets`);
1454
+ console.log(` ${pc6.blue("\u2394")} Dashboard: ${pc6.underline(dashboardLink)}`);
1409
1455
  console.log("");
1410
1456
  await shutdownAnalytics();
1411
1457
  return;
1412
1458
  }
1413
1459
  await initVault(repoFullName, accessToken);
1414
- console.log(pc5.green("\u2713 Vault created!"));
1460
+ console.log(pc6.green("\u2713 Vault created!"));
1415
1461
  try {
1416
1462
  const badgeAdded = await addBadgeToReadme(true);
1417
1463
  if (badgeAdded) {
1418
- console.log(pc5.green("\u2713 Badge added to README.md"));
1464
+ console.log(pc6.green("\u2713 Badge added to README.md"));
1419
1465
  }
1420
1466
  } catch {
1421
1467
  }
1422
1468
  console.log("");
1423
1469
  const envCandidates = discoverEnvCandidates(process.cwd());
1424
- const isInteractive3 = process.stdin.isTTY && process.stdout.isTTY;
1425
- if (envCandidates.length > 0 && isInteractive3) {
1426
- console.log(pc5.gray(` Found ${envCandidates.length} env file(s): ${envCandidates.map((c) => c.file).join(", ")}
1470
+ const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
1471
+ if (envCandidates.length > 0 && isInteractive2) {
1472
+ console.log(pc6.gray(` Found ${envCandidates.length} env file(s): ${envCandidates.map((c) => c.file).join(", ")}
1427
1473
  `));
1428
1474
  const { shouldPush } = await prompts4({
1429
1475
  type: "confirm",
@@ -1437,42 +1483,32 @@ async function initCommand(options = {}) {
1437
1483
  return;
1438
1484
  }
1439
1485
  }
1440
- console.log(pc5.dim("\u2500".repeat(50)));
1486
+ console.log(pc6.dim("\u2500".repeat(50)));
1441
1487
  console.log("");
1442
1488
  if (envCandidates.length === 0) {
1443
- console.log(` ${pc5.yellow("\u2192")} Create a ${pc5.cyan(".env")} file with your secrets`);
1444
- console.log(` ${pc5.yellow("\u2192")} Run ${pc5.cyan("keyway push")} to sync them
1489
+ console.log(`${pc6.yellow("\u26A0")} No .env file found - your vault is empty`);
1490
+ console.log(` Next: Create ${pc6.cyan(".env")} and run ${pc6.cyan("keyway push")}
1445
1491
  `);
1446
1492
  } else {
1447
- console.log(` ${pc5.yellow("\u2192")} Run ${pc5.cyan("keyway push")} to sync your secrets
1493
+ console.log(` ${pc6.yellow("\u2192")} Run ${pc6.cyan("keyway push")} to sync your secrets
1448
1494
  `);
1449
1495
  }
1450
- console.log(` ${pc5.blue("\u2394")} Dashboard: ${pc5.underline(dashboardLink)}`);
1496
+ console.log(` ${pc6.blue("\u2394")} Dashboard: ${pc6.underline(dashboardLink)}`);
1451
1497
  console.log("");
1452
1498
  await shutdownAnalytics();
1453
1499
  } catch (error) {
1454
1500
  if (error instanceof APIError) {
1455
1501
  if (error.statusCode === 409) {
1456
- console.log(pc5.green("\n\u2713 Already initialized!\n"));
1457
- console.log(` ${pc5.yellow("\u2192")} Run ${pc5.cyan("keyway push")} to sync your secrets`);
1458
- console.log(` ${pc5.blue("\u2394")} Dashboard: ${pc5.underline(`${DASHBOARD_URL}/${getCurrentRepoFullName()}`)}`);
1502
+ console.log(pc6.green("\n\u2713 Already initialized!\n"));
1503
+ console.log(` ${pc6.yellow("\u2192")} Run ${pc6.cyan("keyway push")} to sync your secrets`);
1504
+ console.log(` ${pc6.blue("\u2394")} Dashboard: ${pc6.underline(`${DASHBOARD_URL}/${getCurrentRepoFullName()}`)}`);
1459
1505
  console.log("");
1460
1506
  await shutdownAnalytics();
1461
1507
  return;
1462
1508
  }
1463
1509
  if (error.error === "Plan Limit Reached" || error.upgradeUrl) {
1464
1510
  const upgradeUrl = error.upgradeUrl || "https://keyway.sh/pricing";
1465
- console.log("");
1466
- console.log(pc5.dim("\u2500".repeat(50)));
1467
- console.log("");
1468
- console.log(` ${pc5.yellow("\u26A1")} ${pc5.bold("Plan Limit Reached")}`);
1469
- console.log("");
1470
- console.log(pc5.white(` ${error.message}`));
1471
- console.log("");
1472
- console.log(` ${pc5.cyan("Upgrade now \u2192")} ${pc5.underline(upgradeUrl)}`);
1473
- console.log("");
1474
- console.log(pc5.dim("\u2500".repeat(50)));
1475
- console.log("");
1511
+ showUpgradePrompt(error.message, upgradeUrl);
1476
1512
  await shutdownAnalytics();
1477
1513
  process.exit(1);
1478
1514
  }
@@ -1483,14 +1519,14 @@ async function initCommand(options = {}) {
1483
1519
  error: message
1484
1520
  });
1485
1521
  await shutdownAnalytics();
1486
- console.error(pc5.red(`
1522
+ console.error(pc6.red(`
1487
1523
  \u2717 ${message}`));
1488
1524
  process.exit(1);
1489
1525
  }
1490
1526
  }
1491
1527
 
1492
1528
  // src/cmds/pull.ts
1493
- import pc6 from "picocolors";
1529
+ import pc7 from "picocolors";
1494
1530
  import fs5 from "fs";
1495
1531
  import path5 from "path";
1496
1532
  import prompts5 from "prompts";
@@ -1498,10 +1534,10 @@ async function pullCommand(options) {
1498
1534
  try {
1499
1535
  const environment = options.env || "development";
1500
1536
  const envFile = options.file || ".env";
1501
- console.log(pc6.blue("\u{1F510} Pulling secrets from Keyway...\n"));
1502
- console.log(`Environment: ${pc6.cyan(environment)}`);
1537
+ console.log(pc7.blue("\u{1F510} Pulling secrets from Keyway...\n"));
1538
+ console.log(`Environment: ${pc7.cyan(environment)}`);
1503
1539
  const repoFullName = getCurrentRepoFullName();
1504
- console.log(`Repository: ${pc6.cyan(repoFullName)}`);
1540
+ console.log(`Repository: ${pc7.cyan(repoFullName)}`);
1505
1541
  const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
1506
1542
  trackEvent(AnalyticsEvents.CLI_PULL, {
1507
1543
  repoFullName,
@@ -1511,11 +1547,11 @@ async function pullCommand(options) {
1511
1547
  const response = await pullSecrets(repoFullName, environment, accessToken);
1512
1548
  const envFilePath = path5.resolve(process.cwd(), envFile);
1513
1549
  if (fs5.existsSync(envFilePath)) {
1514
- const isInteractive3 = process.stdin.isTTY && process.stdout.isTTY;
1550
+ const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
1515
1551
  if (options.yes) {
1516
- console.log(pc6.yellow(`
1552
+ console.log(pc7.yellow(`
1517
1553
  \u26A0 Overwriting existing file: ${envFile}`));
1518
- } else if (!isInteractive3) {
1554
+ } else if (!isInteractive2) {
1519
1555
  throw new Error(`File ${envFile} exists. Re-run with --yes to overwrite or choose a different --file.`);
1520
1556
  } else {
1521
1557
  const { confirm } = await prompts5(
@@ -1532,7 +1568,7 @@ async function pullCommand(options) {
1532
1568
  }
1533
1569
  );
1534
1570
  if (!confirm) {
1535
- console.log(pc6.yellow("Pull aborted."));
1571
+ console.log(pc7.yellow("Pull aborted."));
1536
1572
  return;
1537
1573
  }
1538
1574
  }
@@ -1542,11 +1578,11 @@ async function pullCommand(options) {
1542
1578
  const trimmed = line.trim();
1543
1579
  return trimmed.length > 0 && !trimmed.startsWith("#");
1544
1580
  });
1545
- console.log(pc6.green(`
1581
+ console.log(pc7.green(`
1546
1582
  \u2713 Secrets downloaded successfully`));
1547
1583
  console.log(`
1548
- File: ${pc6.cyan(envFile)}`);
1549
- console.log(`Variables: ${pc6.cyan(lines.length.toString())}`);
1584
+ File: ${pc7.cyan(envFile)}`);
1585
+ console.log(`Variables: ${pc7.cyan(lines.length.toString())}`);
1550
1586
  await shutdownAnalytics();
1551
1587
  } catch (error) {
1552
1588
  const message = error instanceof APIError ? `API ${error.statusCode}: ${error.message}` : error instanceof Error ? truncateMessage(error.message) : "Unknown error";
@@ -1555,14 +1591,14 @@ File: ${pc6.cyan(envFile)}`);
1555
1591
  error: message
1556
1592
  });
1557
1593
  await shutdownAnalytics();
1558
- console.error(pc6.red(`
1594
+ console.error(pc7.red(`
1559
1595
  \u2717 ${message}`));
1560
1596
  process.exit(1);
1561
1597
  }
1562
1598
  }
1563
1599
 
1564
1600
  // src/cmds/doctor.ts
1565
- import pc7 from "picocolors";
1601
+ import pc8 from "picocolors";
1566
1602
 
1567
1603
  // src/core/doctor.ts
1568
1604
  import { execSync as execSync2 } from "child_process";
@@ -1816,9 +1852,9 @@ async function runAllChecks(options = {}) {
1816
1852
  // src/cmds/doctor.ts
1817
1853
  function formatSummary(results) {
1818
1854
  const parts = [
1819
- pc7.green(`${results.summary.pass} passed`),
1820
- results.summary.warn > 0 ? pc7.yellow(`${results.summary.warn} warnings`) : null,
1821
- results.summary.fail > 0 ? pc7.red(`${results.summary.fail} failed`) : null
1855
+ pc8.green(`${results.summary.pass} passed`),
1856
+ results.summary.warn > 0 ? pc8.yellow(`${results.summary.warn} warnings`) : null,
1857
+ results.summary.fail > 0 ? pc8.red(`${results.summary.fail} failed`) : null
1822
1858
  ].filter(Boolean);
1823
1859
  return parts.join(", ");
1824
1860
  }
@@ -1835,20 +1871,20 @@ async function doctorCommand(options = {}) {
1835
1871
  process.stdout.write(JSON.stringify(results, null, 0) + "\n");
1836
1872
  process.exit(results.exitCode);
1837
1873
  }
1838
- console.log(pc7.cyan("\n\u{1F50D} Keyway Doctor - Environment Check\n"));
1874
+ console.log(pc8.cyan("\n\u{1F50D} Keyway Doctor - Environment Check\n"));
1839
1875
  results.checks.forEach((check) => {
1840
- const icon = check.status === "pass" ? pc7.green("\u2713") : check.status === "warn" ? pc7.yellow("!") : pc7.red("\u2717");
1841
- const detail = check.detail ? pc7.dim(` \u2014 ${check.detail}`) : "";
1876
+ const icon = check.status === "pass" ? pc8.green("\u2713") : check.status === "warn" ? pc8.yellow("!") : pc8.red("\u2717");
1877
+ const detail = check.detail ? pc8.dim(` \u2014 ${check.detail}`) : "";
1842
1878
  console.log(` ${icon} ${check.name}${detail}`);
1843
1879
  });
1844
1880
  console.log(`
1845
1881
  Summary: ${formatSummary(results)}`);
1846
1882
  if (results.summary.fail > 0) {
1847
- console.log(pc7.red("\u26A0 Some checks failed. Please resolve the issues above before using Keyway."));
1883
+ console.log(pc8.red("\u26A0 Some checks failed. Please resolve the issues above before using Keyway."));
1848
1884
  } else if (results.summary.warn > 0) {
1849
- console.log(pc7.yellow("\u26A0 Some warnings detected. Keyway should work but consider addressing them."));
1885
+ console.log(pc8.yellow("\u26A0 Some warnings detected. Keyway should work but consider addressing them."));
1850
1886
  } else {
1851
- console.log(pc7.green("\u2728 All checks passed! Your environment is ready for Keyway."));
1887
+ console.log(pc8.green("\u2728 All checks passed! Your environment is ready for Keyway."));
1852
1888
  }
1853
1889
  process.exit(results.exitCode);
1854
1890
  } catch (error) {
@@ -1869,7 +1905,7 @@ Summary: ${formatSummary(results)}`);
1869
1905
  };
1870
1906
  process.stdout.write(JSON.stringify(errorResult, null, 0) + "\n");
1871
1907
  } else {
1872
- console.error(pc7.red(`
1908
+ console.error(pc8.red(`
1873
1909
  \u2717 ${message}`));
1874
1910
  }
1875
1911
  process.exit(1);
@@ -1877,8 +1913,7 @@ Summary: ${formatSummary(results)}`);
1877
1913
  }
1878
1914
 
1879
1915
  // src/cmds/connect.ts
1880
- import pc8 from "picocolors";
1881
- import open3 from "open";
1916
+ import pc9 from "picocolors";
1882
1917
  import prompts6 from "prompts";
1883
1918
  var TOKEN_AUTH_PROVIDERS = ["railway"];
1884
1919
  function getTokenCreationUrl(provider) {
@@ -1891,41 +1926,38 @@ function getTokenCreationUrl(provider) {
1891
1926
  }
1892
1927
  async function connectWithTokenFlow(accessToken, provider, displayName) {
1893
1928
  const tokenUrl = getTokenCreationUrl(provider);
1894
- console.log(pc8.gray(`Create a ${displayName} API Token at:`));
1895
- console.log(pc8.cyan(`\u2192 ${tokenUrl}
1896
- `));
1897
1929
  if (provider === "railway") {
1898
- console.log(pc8.yellow("Tip: Select the workspace containing your projects."));
1899
- console.log(pc8.yellow(` Do NOT use "No workspace" - it won't have access to your projects.
1900
- `));
1930
+ console.log(pc9.yellow("\nTip: Select the workspace containing your projects."));
1931
+ console.log(pc9.yellow(` Do NOT use "No workspace" - it won't have access to your projects.`));
1901
1932
  }
1933
+ await openUrl(tokenUrl);
1902
1934
  const { token } = await prompts6({
1903
1935
  type: "password",
1904
1936
  name: "token",
1905
1937
  message: `${displayName} API Token:`
1906
1938
  });
1907
1939
  if (!token) {
1908
- console.log(pc8.gray("Cancelled."));
1940
+ console.log(pc9.gray("Cancelled."));
1909
1941
  return false;
1910
1942
  }
1911
- console.log(pc8.gray("\nValidating token..."));
1943
+ console.log(pc9.gray("\nValidating token..."));
1912
1944
  try {
1913
1945
  const result = await connectWithToken(accessToken, provider, token);
1914
1946
  if (result.success) {
1915
- console.log(pc8.green(`
1947
+ console.log(pc9.green(`
1916
1948
  \u2713 Connected to ${displayName}!`));
1917
- console.log(pc8.gray(` Account: ${result.user.username}`));
1949
+ console.log(pc9.gray(` Account: ${result.user.username}`));
1918
1950
  if (result.user.teamName) {
1919
- console.log(pc8.gray(` Team: ${result.user.teamName}`));
1951
+ console.log(pc9.gray(` Team: ${result.user.teamName}`));
1920
1952
  }
1921
1953
  return true;
1922
1954
  } else {
1923
- console.log(pc8.red("\n\u2717 Connection failed."));
1955
+ console.log(pc9.red("\n\u2717 Connection failed."));
1924
1956
  return false;
1925
1957
  }
1926
1958
  } catch (error) {
1927
1959
  const message = error instanceof Error ? error.message : "Token validation failed";
1928
- console.log(pc8.red(`
1960
+ console.log(pc9.red(`
1929
1961
  \u2717 ${message}`));
1930
1962
  return false;
1931
1963
  }
@@ -1933,11 +1965,8 @@ async function connectWithTokenFlow(accessToken, provider, displayName) {
1933
1965
  async function connectWithOAuthFlow(accessToken, provider, displayName) {
1934
1966
  const authUrl = getProviderAuthUrl(provider);
1935
1967
  const startTime = /* @__PURE__ */ new Date();
1936
- console.log(pc8.gray("Opening browser for authorization..."));
1937
- console.log(pc8.gray(`If the browser doesn't open, visit: ${authUrl}`));
1938
- await open3(authUrl).catch(() => {
1939
- });
1940
- console.log(pc8.gray("Waiting for authorization..."));
1968
+ await openUrl(authUrl);
1969
+ console.log(pc9.gray("Waiting for authorization..."));
1941
1970
  const maxAttempts = 60;
1942
1971
  let attempts = 0;
1943
1972
  while (attempts < maxAttempts) {
@@ -1949,15 +1978,15 @@ async function connectWithOAuthFlow(accessToken, provider, displayName) {
1949
1978
  (c) => c.provider === provider && new Date(c.createdAt) > startTime
1950
1979
  );
1951
1980
  if (newConn) {
1952
- console.log(pc8.green(`
1981
+ console.log(pc9.green(`
1953
1982
  \u2713 Connected to ${displayName}!`));
1954
1983
  return true;
1955
1984
  }
1956
1985
  } catch {
1957
1986
  }
1958
1987
  }
1959
- console.log(pc8.red("\n\u2717 Authorization timeout."));
1960
- console.log(pc8.gray("Run `keyway connections` to check if the connection was established."));
1988
+ console.log(pc9.red("\n\u2717 Authorization timeout."));
1989
+ console.log(pc9.gray("Run `keyway connections` to check if the connection was established."));
1961
1990
  return false;
1962
1991
  }
1963
1992
  async function connectCommand(provider, options = {}) {
@@ -1967,13 +1996,13 @@ async function connectCommand(provider, options = {}) {
1967
1996
  const providerInfo = providers.find((p) => p.name === provider.toLowerCase());
1968
1997
  if (!providerInfo) {
1969
1998
  const available = providers.map((p) => p.name).join(", ");
1970
- console.error(pc8.red(`Unknown provider: ${provider}`));
1971
- console.log(pc8.gray(`Available providers: ${available || "none"}`));
1999
+ console.error(pc9.red(`Unknown provider: ${provider}`));
2000
+ console.log(pc9.gray(`Available providers: ${available || "none"}`));
1972
2001
  process.exit(1);
1973
2002
  }
1974
2003
  if (!providerInfo.configured) {
1975
- console.error(pc8.red(`Provider ${providerInfo.displayName} is not configured on the server.`));
1976
- console.log(pc8.gray("Contact your administrator to enable this integration."));
2004
+ console.error(pc9.red(`Provider ${providerInfo.displayName} is not configured on the server.`));
2005
+ console.log(pc9.gray("Contact your administrator to enable this integration."));
1977
2006
  process.exit(1);
1978
2007
  }
1979
2008
  const { connections } = await getConnections(accessToken);
@@ -1986,11 +2015,11 @@ async function connectCommand(provider, options = {}) {
1986
2015
  initial: false
1987
2016
  });
1988
2017
  if (!reconnect) {
1989
- console.log(pc8.gray("Keeping existing connection."));
2018
+ console.log(pc9.gray("Keeping existing connection."));
1990
2019
  return;
1991
2020
  }
1992
2021
  }
1993
- console.log(pc8.blue(`
2022
+ console.log(pc9.blue(`
1994
2023
  Connecting to ${providerInfo.displayName}...
1995
2024
  `));
1996
2025
  let connected = false;
@@ -2009,7 +2038,7 @@ Connecting to ${providerInfo.displayName}...
2009
2038
  command: "connect",
2010
2039
  error: truncateMessage(message)
2011
2040
  });
2012
- console.error(pc8.red(`
2041
+ console.error(pc9.red(`
2013
2042
  \u2717 ${message}`));
2014
2043
  process.exit(1);
2015
2044
  }
@@ -2019,24 +2048,24 @@ async function connectionsCommand(options = {}) {
2019
2048
  const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
2020
2049
  const { connections } = await getConnections(accessToken);
2021
2050
  if (connections.length === 0) {
2022
- console.log(pc8.gray("No provider connections found."));
2023
- console.log(pc8.gray("\nConnect to a provider with: keyway connect <provider>"));
2024
- console.log(pc8.gray("Available providers: vercel, railway"));
2051
+ console.log(pc9.gray("No provider connections found."));
2052
+ console.log(pc9.gray("\nConnect to a provider with: keyway connect <provider>"));
2053
+ console.log(pc9.gray("Available providers: vercel, railway"));
2025
2054
  return;
2026
2055
  }
2027
- console.log(pc8.blue("\n\u{1F4E1} Provider Connections\n"));
2056
+ console.log(pc9.blue("\n\u{1F4E1} Provider Connections\n"));
2028
2057
  for (const conn of connections) {
2029
2058
  const providerName = conn.provider.charAt(0).toUpperCase() + conn.provider.slice(1);
2030
- const teamInfo = conn.providerTeamId ? pc8.gray(` (Team: ${conn.providerTeamId})`) : "";
2059
+ const teamInfo = conn.providerTeamId ? pc9.gray(` (Team: ${conn.providerTeamId})`) : "";
2031
2060
  const date = new Date(conn.createdAt).toLocaleDateString();
2032
- console.log(` ${pc8.green("\u25CF")} ${pc8.bold(providerName)}${teamInfo}`);
2033
- console.log(pc8.gray(` Connected: ${date}`));
2034
- console.log(pc8.gray(` ID: ${conn.id}`));
2061
+ console.log(` ${pc9.green("\u25CF")} ${pc9.bold(providerName)}${teamInfo}`);
2062
+ console.log(pc9.gray(` Connected: ${date}`));
2063
+ console.log(pc9.gray(` ID: ${conn.id}`));
2035
2064
  console.log("");
2036
2065
  }
2037
2066
  } catch (error) {
2038
2067
  const message = error instanceof Error ? error.message : "Failed to list connections";
2039
- console.error(pc8.red(`
2068
+ console.error(pc9.red(`
2040
2069
  \u2717 ${message}`));
2041
2070
  process.exit(1);
2042
2071
  }
@@ -2047,7 +2076,7 @@ async function disconnectCommand(provider, options = {}) {
2047
2076
  const { connections } = await getConnections(accessToken);
2048
2077
  const connection = connections.find((c) => c.provider === provider.toLowerCase());
2049
2078
  if (!connection) {
2050
- console.log(pc8.gray(`No connection found for provider: ${provider}`));
2079
+ console.log(pc9.gray(`No connection found for provider: ${provider}`));
2051
2080
  return;
2052
2081
  }
2053
2082
  const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
@@ -2058,11 +2087,11 @@ async function disconnectCommand(provider, options = {}) {
2058
2087
  initial: false
2059
2088
  });
2060
2089
  if (!confirm) {
2061
- console.log(pc8.gray("Cancelled."));
2090
+ console.log(pc9.gray("Cancelled."));
2062
2091
  return;
2063
2092
  }
2064
2093
  await deleteConnection(accessToken, connection.id);
2065
- console.log(pc8.green(`
2094
+ console.log(pc9.green(`
2066
2095
  \u2713 Disconnected from ${providerName}`));
2067
2096
  trackEvent(AnalyticsEvents.CLI_DISCONNECT, {
2068
2097
  provider: provider.toLowerCase()
@@ -2073,14 +2102,14 @@ async function disconnectCommand(provider, options = {}) {
2073
2102
  command: "disconnect",
2074
2103
  error: truncateMessage(message)
2075
2104
  });
2076
- console.error(pc8.red(`
2105
+ console.error(pc9.red(`
2077
2106
  \u2717 ${message}`));
2078
2107
  process.exit(1);
2079
2108
  }
2080
2109
  }
2081
2110
 
2082
2111
  // src/cmds/sync.ts
2083
- import pc9 from "picocolors";
2112
+ import pc10 from "picocolors";
2084
2113
  import prompts7 from "prompts";
2085
2114
  function mapToVercelEnvironment(keywayEnv) {
2086
2115
  const mapping = {
@@ -2110,6 +2139,42 @@ function mapToProviderEnvironment(provider, keywayEnv) {
2110
2139
  return keywayEnv;
2111
2140
  }
2112
2141
  }
2142
+ function displayDiffSummary(diff, providerName) {
2143
+ const totalDiff = diff.onlyInKeyway.length + diff.onlyInProvider.length + diff.different.length;
2144
+ if (totalDiff === 0 && diff.same.length > 0) {
2145
+ console.log(pc10.green(`
2146
+ \u2713 Already in sync (${diff.same.length} secrets)`));
2147
+ return;
2148
+ }
2149
+ console.log(pc10.blue("\n\u{1F4CA} Comparison Summary\n"));
2150
+ console.log(pc10.gray(` Keyway: ${diff.keywayCount} secrets | ${providerName}: ${diff.providerCount} secrets
2151
+ `));
2152
+ if (diff.onlyInKeyway.length > 0) {
2153
+ console.log(pc10.cyan(` \u2192 ${diff.onlyInKeyway.length} only in Keyway`));
2154
+ diff.onlyInKeyway.slice(0, 3).forEach((key) => console.log(pc10.gray(` ${key}`)));
2155
+ if (diff.onlyInKeyway.length > 3) {
2156
+ console.log(pc10.gray(` ... and ${diff.onlyInKeyway.length - 3} more`));
2157
+ }
2158
+ }
2159
+ if (diff.onlyInProvider.length > 0) {
2160
+ console.log(pc10.magenta(` \u2190 ${diff.onlyInProvider.length} only in ${providerName}`));
2161
+ diff.onlyInProvider.slice(0, 3).forEach((key) => console.log(pc10.gray(` ${key}`)));
2162
+ if (diff.onlyInProvider.length > 3) {
2163
+ console.log(pc10.gray(` ... and ${diff.onlyInProvider.length - 3} more`));
2164
+ }
2165
+ }
2166
+ if (diff.different.length > 0) {
2167
+ console.log(pc10.yellow(` \u2260 ${diff.different.length} with different values`));
2168
+ diff.different.slice(0, 3).forEach((key) => console.log(pc10.gray(` ${key}`)));
2169
+ if (diff.different.length > 3) {
2170
+ console.log(pc10.gray(` ... and ${diff.different.length - 3} more`));
2171
+ }
2172
+ }
2173
+ if (diff.same.length > 0) {
2174
+ console.log(pc10.gray(` = ${diff.same.length} identical`));
2175
+ }
2176
+ console.log("");
2177
+ }
2113
2178
  function findMatchingProject(projects, repoFullName) {
2114
2179
  const repoFullNameLower = repoFullName.toLowerCase();
2115
2180
  const repoName = repoFullName.split("/")[1]?.toLowerCase();
@@ -2149,11 +2214,11 @@ async function promptProjectSelection(projects, repoFullName) {
2149
2214
  let title = p.name;
2150
2215
  const badges = [];
2151
2216
  if (p.linkedRepo?.toLowerCase() === repoFullName.toLowerCase()) {
2152
- badges.push(pc9.green("\u2190 linked"));
2217
+ badges.push(pc10.green("\u2190 linked"));
2153
2218
  } else if (p.name.toLowerCase() === repoName) {
2154
- badges.push(pc9.green("\u2190 same name"));
2219
+ badges.push(pc10.green("\u2190 same name"));
2155
2220
  } else if (p.linkedRepo) {
2156
- badges.push(pc9.gray(`\u2192 ${p.linkedRepo}`));
2221
+ badges.push(pc10.gray(`\u2192 ${p.linkedRepo}`));
2157
2222
  }
2158
2223
  if (badges.length > 0) {
2159
2224
  title = `${p.name} ${badges.join(" ")}`;
@@ -2167,7 +2232,7 @@ async function promptProjectSelection(projects, repoFullName) {
2167
2232
  choices
2168
2233
  });
2169
2234
  if (!projectChoice) {
2170
- console.log(pc9.gray("Cancelled."));
2235
+ console.log(pc10.gray("Cancelled."));
2171
2236
  process.exit(0);
2172
2237
  }
2173
2238
  return projects.find((p) => p.id === projectChoice);
@@ -2175,23 +2240,23 @@ async function promptProjectSelection(projects, repoFullName) {
2175
2240
  async function syncCommand(provider, options = {}) {
2176
2241
  try {
2177
2242
  if (options.pull && options.allowDelete) {
2178
- console.error(pc9.red("Error: --allow-delete cannot be used with --pull"));
2179
- console.log(pc9.gray("The --allow-delete flag is only for push operations."));
2243
+ console.error(pc10.red("Error: --allow-delete cannot be used with --pull"));
2244
+ console.log(pc10.gray("The --allow-delete flag is only for push operations."));
2180
2245
  process.exit(1);
2181
2246
  }
2182
2247
  const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
2183
2248
  const repoFullName = detectGitRepo();
2184
2249
  if (!repoFullName) {
2185
- console.error(pc9.red("Could not detect Git repository."));
2186
- console.log(pc9.gray("Run this command from a Git repository directory."));
2250
+ console.error(pc10.red("Could not detect Git repository."));
2251
+ console.log(pc10.gray("Run this command from a Git repository directory."));
2187
2252
  process.exit(1);
2188
2253
  }
2189
- console.log(pc9.gray(`Repository: ${repoFullName}`));
2254
+ console.log(pc10.gray(`Repository: ${repoFullName}`));
2190
2255
  let { connections } = await getConnections(accessToken);
2191
2256
  let connection = connections.find((c) => c.provider === provider.toLowerCase());
2192
2257
  if (!connection) {
2193
2258
  const providerDisplayName = provider.charAt(0).toUpperCase() + provider.slice(1);
2194
- console.log(pc9.yellow(`
2259
+ console.log(pc10.yellow(`
2195
2260
  Not connected to ${providerDisplayName}.`));
2196
2261
  const { shouldConnect } = await prompts7({
2197
2262
  type: "confirm",
@@ -2200,7 +2265,7 @@ Not connected to ${providerDisplayName}.`));
2200
2265
  initial: true
2201
2266
  });
2202
2267
  if (!shouldConnect) {
2203
- console.log(pc9.gray("Cancelled."));
2268
+ console.log(pc10.gray("Cancelled."));
2204
2269
  process.exit(0);
2205
2270
  }
2206
2271
  await connectCommand(provider, { loginPrompt: false });
@@ -2208,7 +2273,7 @@ Not connected to ${providerDisplayName}.`));
2208
2273
  connections = refreshed.connections;
2209
2274
  connection = connections.find((c) => c.provider === provider.toLowerCase());
2210
2275
  if (!connection) {
2211
- console.error(pc9.red(`
2276
+ console.error(pc10.red(`
2212
2277
  Connection to ${providerDisplayName} failed.`));
2213
2278
  process.exit(1);
2214
2279
  }
@@ -2216,7 +2281,7 @@ Connection to ${providerDisplayName} failed.`));
2216
2281
  }
2217
2282
  const { projects } = await getConnectionProjects(accessToken, connection.id);
2218
2283
  if (projects.length === 0) {
2219
- console.error(pc9.red(`No projects found in your ${provider} account.`));
2284
+ console.error(pc10.red(`No projects found in your ${provider} account.`));
2220
2285
  process.exit(1);
2221
2286
  }
2222
2287
  let selectedProject;
@@ -2225,21 +2290,21 @@ Connection to ${providerDisplayName} failed.`));
2225
2290
  (p) => p.id === options.project || p.name.toLowerCase() === options.project?.toLowerCase()
2226
2291
  );
2227
2292
  if (!found) {
2228
- console.error(pc9.red(`Project not found: ${options.project}`));
2229
- console.log(pc9.gray("Available projects:"));
2230
- projects.forEach((p) => console.log(pc9.gray(` - ${p.name}`)));
2293
+ console.error(pc10.red(`Project not found: ${options.project}`));
2294
+ console.log(pc10.gray("Available projects:"));
2295
+ projects.forEach((p) => console.log(pc10.gray(` - ${p.name}`)));
2231
2296
  process.exit(1);
2232
2297
  }
2233
2298
  selectedProject = found;
2234
2299
  if (!projectMatchesRepo(selectedProject, repoFullName)) {
2235
2300
  console.log("");
2236
- console.log(pc9.yellow("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
2237
- console.log(pc9.yellow("\u2502 \u26A0\uFE0F WARNING: Project does not match current repository \u2502"));
2238
- console.log(pc9.yellow("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
2239
- console.log(pc9.yellow(` Current repo: ${repoFullName}`));
2240
- console.log(pc9.yellow(` Selected project: ${selectedProject.name}`));
2301
+ console.log(pc10.yellow("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
2302
+ console.log(pc10.yellow("\u2502 \u26A0\uFE0F WARNING: Project does not match current repository \u2502"));
2303
+ console.log(pc10.yellow("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
2304
+ console.log(pc10.yellow(` Current repo: ${repoFullName}`));
2305
+ console.log(pc10.yellow(` Selected project: ${selectedProject.name}`));
2241
2306
  if (selectedProject.linkedRepo) {
2242
- console.log(pc9.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
2307
+ console.log(pc10.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
2243
2308
  }
2244
2309
  console.log("");
2245
2310
  }
@@ -2248,9 +2313,9 @@ Connection to ${providerDisplayName} failed.`));
2248
2313
  if (autoMatch && (autoMatch.matchType === "linked_repo" || autoMatch.matchType === "exact_name")) {
2249
2314
  selectedProject = autoMatch.project;
2250
2315
  const matchReason = autoMatch.matchType === "linked_repo" ? `linked to ${repoFullName}` : "exact name match";
2251
- console.log(pc9.green(`\u2713 Auto-selected project: ${selectedProject.name} (${matchReason})`));
2316
+ console.log(pc10.green(`\u2713 Auto-selected project: ${selectedProject.name} (${matchReason})`));
2252
2317
  } else if (autoMatch && autoMatch.matchType === "partial_name") {
2253
- console.log(pc9.yellow(`Detected project: ${autoMatch.project.name} (partial match)`));
2318
+ console.log(pc10.yellow(`Detected project: ${autoMatch.project.name} (partial match)`));
2254
2319
  const { useDetected } = await prompts7({
2255
2320
  type: "confirm",
2256
2321
  name: "useDetected",
@@ -2266,13 +2331,13 @@ Connection to ${providerDisplayName} failed.`));
2266
2331
  selectedProject = projects[0];
2267
2332
  if (!projectMatchesRepo(selectedProject, repoFullName)) {
2268
2333
  console.log("");
2269
- console.log(pc9.yellow("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
2270
- console.log(pc9.yellow("\u2502 \u26A0\uFE0F WARNING: Project does not match current repository \u2502"));
2271
- console.log(pc9.yellow("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
2272
- console.log(pc9.yellow(` Current repo: ${repoFullName}`));
2273
- console.log(pc9.yellow(` Only project: ${selectedProject.name}`));
2334
+ console.log(pc10.yellow("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
2335
+ console.log(pc10.yellow("\u2502 \u26A0\uFE0F WARNING: Project does not match current repository \u2502"));
2336
+ console.log(pc10.yellow("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
2337
+ console.log(pc10.yellow(` Current repo: ${repoFullName}`));
2338
+ console.log(pc10.yellow(` Only project: ${selectedProject.name}`));
2274
2339
  if (selectedProject.linkedRepo) {
2275
- console.log(pc9.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
2340
+ console.log(pc10.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
2276
2341
  }
2277
2342
  console.log("");
2278
2343
  const { continueAnyway } = await prompts7({
@@ -2282,14 +2347,14 @@ Connection to ${providerDisplayName} failed.`));
2282
2347
  initial: false
2283
2348
  });
2284
2349
  if (!continueAnyway) {
2285
- console.log(pc9.gray("Cancelled."));
2350
+ console.log(pc10.gray("Cancelled."));
2286
2351
  process.exit(0);
2287
2352
  }
2288
2353
  }
2289
2354
  } else {
2290
- console.log(pc9.yellow(`
2355
+ console.log(pc10.yellow(`
2291
2356
  \u26A0\uFE0F No matching project found for ${repoFullName}`));
2292
- console.log(pc9.gray("Select a project manually:\n"));
2357
+ console.log(pc10.gray("Select a project manually:\n"));
2293
2358
  selectedProject = await promptProjectSelection(projects, repoFullName);
2294
2359
  }
2295
2360
  }
@@ -2297,13 +2362,13 @@ Connection to ${providerDisplayName} failed.`));
2297
2362
  const autoMatch = findMatchingProject(projects, repoFullName);
2298
2363
  if (autoMatch && autoMatch.project.id !== selectedProject.id) {
2299
2364
  console.log("");
2300
- console.log(pc9.yellow("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
2301
- console.log(pc9.yellow("\u2502 \u26A0\uFE0F WARNING: You selected a different project \u2502"));
2302
- console.log(pc9.yellow("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
2303
- console.log(pc9.yellow(` Current repo: ${repoFullName}`));
2304
- console.log(pc9.yellow(` Selected project: ${selectedProject.name}`));
2365
+ console.log(pc10.yellow("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
2366
+ console.log(pc10.yellow("\u2502 \u26A0\uFE0F WARNING: You selected a different project \u2502"));
2367
+ console.log(pc10.yellow("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
2368
+ console.log(pc10.yellow(` Current repo: ${repoFullName}`));
2369
+ console.log(pc10.yellow(` Selected project: ${selectedProject.name}`));
2305
2370
  if (selectedProject.linkedRepo) {
2306
- console.log(pc9.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
2371
+ console.log(pc10.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
2307
2372
  }
2308
2373
  console.log("");
2309
2374
  const { continueAnyway } = await prompts7({
@@ -2313,7 +2378,7 @@ Connection to ${providerDisplayName} failed.`));
2313
2378
  initial: false
2314
2379
  });
2315
2380
  if (!continueAnyway) {
2316
- console.log(pc9.gray("Cancelled."));
2381
+ console.log(pc10.gray("Cancelled."));
2317
2382
  process.exit(0);
2318
2383
  }
2319
2384
  }
@@ -2335,7 +2400,7 @@ Connection to ${providerDisplayName} failed.`));
2335
2400
  initial: Math.max(0, vaultEnvs.indexOf("production"))
2336
2401
  });
2337
2402
  if (!selectedEnv) {
2338
- console.log(pc9.gray("Cancelled."));
2403
+ console.log(pc10.gray("Cancelled."));
2339
2404
  process.exit(0);
2340
2405
  }
2341
2406
  keywayEnv = selectedEnv;
@@ -2343,6 +2408,22 @@ Connection to ${providerDisplayName} failed.`));
2343
2408
  providerEnv = mapToProviderEnvironment(provider, keywayEnv);
2344
2409
  }
2345
2410
  }
2411
+ if (needsDirectionPrompt) {
2412
+ const effectiveKeywayEnv = keywayEnv || "production";
2413
+ const effectiveProviderEnv = providerEnv || mapToProviderEnvironment(provider, effectiveKeywayEnv);
2414
+ console.log(pc10.gray("\nComparing secrets..."));
2415
+ const diff = await getSyncDiff(accessToken, repoFullName, {
2416
+ connectionId: connection.id,
2417
+ projectId: selectedProject.id,
2418
+ keywayEnvironment: effectiveKeywayEnv,
2419
+ providerEnvironment: effectiveProviderEnv
2420
+ });
2421
+ displayDiffSummary(diff, providerName);
2422
+ const totalDiff = diff.onlyInKeyway.length + diff.onlyInProvider.length + diff.different.length;
2423
+ if (totalDiff === 0) {
2424
+ return;
2425
+ }
2426
+ }
2346
2427
  if (needsDirectionPrompt) {
2347
2428
  const { selectedDirection } = await prompts7({
2348
2429
  type: "select",
@@ -2354,7 +2435,7 @@ Connection to ${providerDisplayName} failed.`));
2354
2435
  ]
2355
2436
  });
2356
2437
  if (!selectedDirection) {
2357
- console.log(pc9.gray("Cancelled."));
2438
+ console.log(pc10.gray("Cancelled."));
2358
2439
  process.exit(0);
2359
2440
  }
2360
2441
  direction = selectedDirection;
@@ -2371,9 +2452,9 @@ Connection to ${providerDisplayName} failed.`));
2371
2452
  keywayEnv
2372
2453
  );
2373
2454
  if (status.isFirstSync && direction === "push" && status.vaultIsEmpty && status.providerHasSecrets) {
2374
- console.log(pc9.yellow(`
2455
+ console.log(pc10.yellow(`
2375
2456
  \u26A0\uFE0F Your Keyway vault is empty for "${keywayEnv}", but ${providerName} has ${status.providerSecretCount} secrets.`));
2376
- console.log(pc9.gray(` (Use --environment to sync a different environment)`));
2457
+ console.log(pc10.gray(` (Use --environment to sync a different environment)`));
2377
2458
  const { importFirst } = await prompts7({
2378
2459
  type: "confirm",
2379
2460
  name: "importFirst",
@@ -2415,7 +2496,7 @@ Connection to ${providerDisplayName} failed.`));
2415
2496
  command: "sync",
2416
2497
  error: truncateMessage(message)
2417
2498
  });
2418
- console.error(pc9.red(`
2499
+ console.error(pc10.red(`
2419
2500
  \u2717 ${message}`));
2420
2501
  process.exit(1);
2421
2502
  }
@@ -2432,33 +2513,33 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
2432
2513
  });
2433
2514
  const totalChanges = preview.toCreate.length + preview.toUpdate.length + preview.toDelete.length;
2434
2515
  if (totalChanges === 0) {
2435
- console.log(pc9.green("\n\u2713 Already in sync. No changes needed."));
2516
+ console.log(pc10.green("\n\u2713 Already in sync. No changes needed."));
2436
2517
  return;
2437
2518
  }
2438
- console.log(pc9.blue("\n\u{1F4CB} Sync Preview\n"));
2519
+ console.log(pc10.blue("\n\u{1F4CB} Sync Preview\n"));
2439
2520
  if (preview.toCreate.length > 0) {
2440
- console.log(pc9.green(` + ${preview.toCreate.length} to create`));
2441
- preview.toCreate.slice(0, 5).forEach((key) => console.log(pc9.gray(` ${key}`)));
2521
+ console.log(pc10.green(` + ${preview.toCreate.length} to create`));
2522
+ preview.toCreate.slice(0, 5).forEach((key) => console.log(pc10.gray(` ${key}`)));
2442
2523
  if (preview.toCreate.length > 5) {
2443
- console.log(pc9.gray(` ... and ${preview.toCreate.length - 5} more`));
2524
+ console.log(pc10.gray(` ... and ${preview.toCreate.length - 5} more`));
2444
2525
  }
2445
2526
  }
2446
2527
  if (preview.toUpdate.length > 0) {
2447
- console.log(pc9.yellow(` ~ ${preview.toUpdate.length} to update`));
2448
- preview.toUpdate.slice(0, 5).forEach((key) => console.log(pc9.gray(` ${key}`)));
2528
+ console.log(pc10.yellow(` ~ ${preview.toUpdate.length} to update`));
2529
+ preview.toUpdate.slice(0, 5).forEach((key) => console.log(pc10.gray(` ${key}`)));
2449
2530
  if (preview.toUpdate.length > 5) {
2450
- console.log(pc9.gray(` ... and ${preview.toUpdate.length - 5} more`));
2531
+ console.log(pc10.gray(` ... and ${preview.toUpdate.length - 5} more`));
2451
2532
  }
2452
2533
  }
2453
2534
  if (preview.toDelete.length > 0) {
2454
- console.log(pc9.red(` - ${preview.toDelete.length} to delete`));
2455
- preview.toDelete.slice(0, 5).forEach((key) => console.log(pc9.gray(` ${key}`)));
2535
+ console.log(pc10.red(` - ${preview.toDelete.length} to delete`));
2536
+ preview.toDelete.slice(0, 5).forEach((key) => console.log(pc10.gray(` ${key}`)));
2456
2537
  if (preview.toDelete.length > 5) {
2457
- console.log(pc9.gray(` ... and ${preview.toDelete.length - 5} more`));
2538
+ console.log(pc10.gray(` ... and ${preview.toDelete.length - 5} more`));
2458
2539
  }
2459
2540
  }
2460
2541
  if (preview.toSkip.length > 0) {
2461
- console.log(pc9.gray(` \u25CB ${preview.toSkip.length} unchanged`));
2542
+ console.log(pc10.gray(` \u25CB ${preview.toSkip.length} unchanged`));
2462
2543
  }
2463
2544
  console.log("");
2464
2545
  if (!skipConfirm) {
@@ -2470,11 +2551,11 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
2470
2551
  initial: true
2471
2552
  });
2472
2553
  if (!confirm) {
2473
- console.log(pc9.gray("Cancelled."));
2554
+ console.log(pc10.gray("Cancelled."));
2474
2555
  return;
2475
2556
  }
2476
2557
  }
2477
- console.log(pc9.blue("\n\u23F3 Syncing...\n"));
2558
+ console.log(pc10.blue("\n\u23F3 Syncing...\n"));
2478
2559
  const result = await executeSync(accessToken, repoFullName, {
2479
2560
  connectionId,
2480
2561
  projectId: project.id,
@@ -2484,11 +2565,11 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
2484
2565
  allowDelete
2485
2566
  });
2486
2567
  if (result.success) {
2487
- console.log(pc9.green("\u2713 Sync complete"));
2488
- console.log(pc9.gray(` Created: ${result.stats.created}`));
2489
- console.log(pc9.gray(` Updated: ${result.stats.updated}`));
2568
+ console.log(pc10.green("\u2713 Sync complete"));
2569
+ console.log(pc10.gray(` Created: ${result.stats.created}`));
2570
+ console.log(pc10.gray(` Updated: ${result.stats.updated}`));
2490
2571
  if (result.stats.deleted > 0) {
2491
- console.log(pc9.gray(` Deleted: ${result.stats.deleted}`));
2572
+ console.log(pc10.gray(` Deleted: ${result.stats.deleted}`));
2492
2573
  }
2493
2574
  trackEvent(AnalyticsEvents.CLI_SYNC, {
2494
2575
  provider,
@@ -2498,20 +2579,24 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
2498
2579
  deleted: result.stats.deleted
2499
2580
  });
2500
2581
  } else {
2501
- console.error(pc9.red(`
2582
+ console.error(pc10.red(`
2502
2583
  \u2717 ${result.error}`));
2503
2584
  process.exit(1);
2504
2585
  }
2505
2586
  }
2506
2587
 
2507
2588
  // src/cli.ts
2589
+ process.on("unhandledRejection", (reason) => {
2590
+ console.error(pc11.red("Unhandled error:"), reason);
2591
+ process.exit(1);
2592
+ });
2508
2593
  var program = new Command();
2509
2594
  var TAGLINE = "Sync secrets with your team and infra";
2510
2595
  var showBanner = () => {
2511
- const text = pc10.bold(pc10.cyan("Keyway CLI"));
2596
+ const text = pc11.bold(pc11.cyan("Keyway CLI"));
2512
2597
  console.log(`
2513
2598
  ${text}
2514
- ${pc10.gray(TAGLINE)}
2599
+ ${pc11.gray(TAGLINE)}
2515
2600
  `);
2516
2601
  };
2517
2602
  showBanner();
@@ -2548,6 +2633,6 @@ program.command("sync <provider>").description("Sync secrets with a provider (e.
2548
2633
  await syncCommand(provider, options);
2549
2634
  });
2550
2635
  program.parseAsync().catch((error) => {
2551
- console.error(pc10.red("Error:"), error.message || error);
2636
+ console.error(pc11.red("Error:"), error.message || error);
2552
2637
  process.exit(1);
2553
2638
  });