@keywaysh/cli 0.1.15 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -4,22 +4,92 @@ import {
4
4
  getAuthFilePath,
5
5
  getStoredAuth,
6
6
  saveAuthToken
7
- } from "./chunk-F4C46224.js";
7
+ } from "./chunk-IVZM2JTT.js";
8
8
 
9
9
  // src/cli.ts
10
10
  import { Command } from "commander";
11
- import pc13 from "picocolors";
12
11
 
13
- // src/cmds/init.ts
14
- import pc7 from "picocolors";
15
- import prompts6 from "prompts";
12
+ // src/utils/ui.ts
13
+ import * as p from "@clack/prompts";
14
+ import pc from "picocolors";
15
+ function intro2(command2) {
16
+ p.intro(pc.bgCyan(pc.black(` keyway ${command2} `)));
17
+ }
18
+ function outro2(message2) {
19
+ p.outro(message2);
20
+ }
21
+ function spinner2() {
22
+ return p.spinner();
23
+ }
24
+ function success(message2) {
25
+ p.log.success(message2);
26
+ }
27
+ function error(message2) {
28
+ p.log.error(message2);
29
+ }
30
+ function warn(message2) {
31
+ p.log.warn(message2);
32
+ }
33
+ function info(message2) {
34
+ p.log.info(message2);
35
+ }
36
+ function step(message2) {
37
+ p.log.step(message2);
38
+ }
39
+ function message(message2) {
40
+ p.log.message(message2);
41
+ }
42
+ function note2(message2, title) {
43
+ p.note(message2, title);
44
+ }
45
+ function cancel2(message2 = "Operation cancelled.") {
46
+ p.cancel(message2);
47
+ process.exit(0);
48
+ }
49
+ async function text2(options) {
50
+ const result = await p.text(options);
51
+ if (p.isCancel(result)) {
52
+ cancel2();
53
+ }
54
+ return result;
55
+ }
56
+ async function confirm2(options) {
57
+ const result = await p.confirm(options);
58
+ if (p.isCancel(result)) {
59
+ cancel2();
60
+ }
61
+ return result;
62
+ }
63
+ async function select2(options) {
64
+ const result = await p.select(options);
65
+ if (p.isCancel(result)) {
66
+ cancel2();
67
+ }
68
+ return result;
69
+ }
70
+ function link(url) {
71
+ return pc.underline(pc.cyan(url));
72
+ }
73
+ function command(cmd) {
74
+ return pc.cyan(cmd);
75
+ }
76
+ function file(path7) {
77
+ return pc.cyan(path7);
78
+ }
79
+ function value(val) {
80
+ return pc.cyan(String(val));
81
+ }
82
+ function dim(text3) {
83
+ return pc.dim(text3);
84
+ }
85
+ function bold(text3) {
86
+ return pc.bold(text3);
87
+ }
16
88
 
17
89
  // src/utils/git.ts
18
90
  import { execSync } from "child_process";
19
91
  import fs from "fs";
20
92
  import path from "path";
21
- import pc from "picocolors";
22
- import prompts from "prompts";
23
93
  function getCurrentRepoFullName() {
24
94
  try {
25
95
  if (!isGitRepository()) {
@@ -29,7 +99,7 @@ function getCurrentRepoFullName() {
29
99
  encoding: "utf-8"
30
100
  }).trim();
31
101
  return parseGitHubUrl(remoteUrl);
32
- } catch (error) {
102
+ } catch (error2) {
33
103
  throw new Error("Failed to get repository name. Make sure you are in a git repository with a GitHub remote.");
34
104
  }
35
105
  }
@@ -116,18 +186,16 @@ async function warnIfEnvNotGitignored() {
116
186
  if (checkEnvGitignore()) {
117
187
  return;
118
188
  }
119
- console.log(pc.yellow("\u26A0\uFE0F .env files are not in .gitignore - secrets may be committed"));
120
- const { addToGitignore } = await prompts({
121
- type: "confirm",
122
- name: "addToGitignore",
189
+ warn(".env files are not in .gitignore - secrets may be committed");
190
+ const addToGitignore = await confirm2({
123
191
  message: "Add .env* to .gitignore?",
124
- initial: true
192
+ initialValue: true
125
193
  });
126
194
  if (addToGitignore) {
127
195
  if (addEnvToGitignore()) {
128
- console.log(pc.green("\u2713 Added .env* to .gitignore"));
196
+ success("Added .env* to .gitignore");
129
197
  } else {
130
- console.log(pc.red("\u2717 Failed to update .gitignore"));
198
+ error("Failed to update .gitignore");
131
199
  }
132
200
  }
133
201
  }
@@ -140,8 +208,8 @@ var INTERNAL_POSTHOG_HOST = "https://eu.i.posthog.com";
140
208
  // package.json
141
209
  var package_default = {
142
210
  name: "@keywaysh/cli",
143
- version: "0.1.15",
144
- description: "One link to all your secrets",
211
+ version: "0.2.0",
212
+ description: "Env vars that sync like code.",
145
213
  type: "module",
146
214
  bin: {
147
215
  keyway: "./dist/cli.js"
@@ -193,13 +261,12 @@ var package_default = {
193
261
  open: "^11.0.0",
194
262
  picocolors: "^1.1.1",
195
263
  "posthog-node": "^3.5.0",
196
- prompts: "^2.4.2"
264
+ "@clack/prompts": "^0.9.1"
197
265
  },
198
266
  devDependencies: {
199
267
  "@types/balanced-match": "^3.0.2",
200
268
  "@types/libsodium-wrappers": "^0.7.14",
201
269
  "@types/node": "^24.2.0",
202
- "@types/prompts": "^2.4.9",
203
270
  "@vitest/coverage-v8": "^3.0.0",
204
271
  msw: "^2.12.4",
205
272
  tsup: "^8.5.0",
@@ -213,9 +280,9 @@ var package_default = {
213
280
  var API_BASE_URL = process.env.KEYWAY_API_URL || INTERNAL_API_URL;
214
281
  var USER_AGENT = `keyway-cli/${package_default.version}`;
215
282
  var DEFAULT_TIMEOUT_MS = 3e4;
216
- function truncateMessage(message, maxLength = 200) {
217
- if (message.length <= maxLength) return message;
218
- return message.slice(0, maxLength - 3) + "...";
283
+ function truncateMessage(message2, maxLength = 200) {
284
+ if (message2.length <= maxLength) return message2;
285
+ return message2.slice(0, maxLength - 3) + "...";
219
286
  }
220
287
  var NETWORK_ERROR_MESSAGES = {
221
288
  ECONNREFUSED: "Cannot connect to Keyway API server. Is the server running?",
@@ -228,16 +295,16 @@ var NETWORK_ERROR_MESSAGES = {
228
295
  UNABLE_TO_VERIFY_LEAF_SIGNATURE: "SSL certificate verification failed.",
229
296
  EPROTO: "SSL/TLS protocol error. Try again later."
230
297
  };
231
- function handleNetworkError(error) {
232
- const errorCode = error.code || error.cause?.code;
298
+ function handleNetworkError(error2) {
299
+ const errorCode = error2.code || error2.cause?.code;
233
300
  if (errorCode && NETWORK_ERROR_MESSAGES[errorCode]) {
234
301
  return new Error(NETWORK_ERROR_MESSAGES[errorCode]);
235
302
  }
236
- const message = error.message.toLowerCase();
237
- if (message.includes("fetch failed") || message.includes("network")) {
303
+ const message2 = error2.message.toLowerCase();
304
+ if (message2.includes("fetch failed") || message2.includes("network")) {
238
305
  return new Error("Network error. Check your internet connection and try again.");
239
306
  }
240
- return error;
307
+ return error2;
241
308
  }
242
309
  async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
243
310
  const controller = new AbortController();
@@ -247,14 +314,14 @@ async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_M
247
314
  ...options,
248
315
  signal: controller.signal
249
316
  });
250
- } catch (error) {
251
- if (error instanceof Error) {
252
- if (error.name === "AbortError") {
317
+ } catch (error2) {
318
+ if (error2 instanceof Error) {
319
+ if (error2.name === "AbortError") {
253
320
  throw new Error(`Request timeout after ${timeoutMs / 1e3}s. Check your network connection.`);
254
321
  }
255
- throw handleNetworkError(error);
322
+ throw handleNetworkError(error2);
256
323
  }
257
- throw error;
324
+ throw error2;
258
325
  } finally {
259
326
  clearTimeout(timeout);
260
327
  }
@@ -280,39 +347,39 @@ Set KEYWAY_DISABLE_SECURITY_WARNINGS=1 to suppress this warning.`
280
347
  }
281
348
  validateApiUrl(API_BASE_URL);
282
349
  var APIError = class extends Error {
283
- constructor(statusCode, error, message, upgradeUrl) {
284
- super(message);
350
+ constructor(statusCode, error2, message2, upgradeUrl) {
351
+ super(message2);
285
352
  this.statusCode = statusCode;
286
- this.error = error;
353
+ this.error = error2;
287
354
  this.upgradeUrl = upgradeUrl;
288
355
  this.name = "APIError";
289
356
  }
290
357
  };
291
358
  async function handleResponse(response) {
292
359
  const contentType = response.headers.get("content-type") || "";
293
- const text = await response.text();
360
+ const text3 = await response.text();
294
361
  if (!response.ok) {
295
362
  if (contentType.includes("application/json")) {
296
363
  try {
297
- const error = JSON.parse(text);
298
- throw new APIError(response.status, error.title || "Error", error.detail || `HTTP ${response.status}`, error.upgradeUrl);
364
+ const error2 = JSON.parse(text3);
365
+ throw new APIError(response.status, error2.title || "Error", error2.detail || `HTTP ${response.status}`, error2.upgradeUrl);
299
366
  } catch (e) {
300
367
  if (e instanceof APIError) throw e;
301
- throw new APIError(response.status, "Error", text || `HTTP ${response.status}`);
368
+ throw new APIError(response.status, "Error", text3 || `HTTP ${response.status}`);
302
369
  }
303
370
  }
304
- throw new APIError(response.status, "Error", text || `HTTP ${response.status}`);
371
+ throw new APIError(response.status, "Error", text3 || `HTTP ${response.status}`);
305
372
  }
306
- if (!text) {
373
+ if (!text3) {
307
374
  return {};
308
375
  }
309
376
  if (contentType.includes("application/json")) {
310
377
  try {
311
- return JSON.parse(text);
378
+ return JSON.parse(text3);
312
379
  } catch {
313
380
  }
314
381
  }
315
- return { content: text };
382
+ return { content: text3 };
316
383
  }
317
384
  async function initVault(repoFullName, accessToken) {
318
385
  const body = { repoFullName };
@@ -340,11 +407,11 @@ function parseEnvContent(content) {
340
407
  const eqIndex = trimmed.indexOf("=");
341
408
  if (eqIndex === -1) continue;
342
409
  const key = trimmed.substring(0, eqIndex).trim();
343
- let value = trimmed.substring(eqIndex + 1);
344
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
345
- value = value.slice(1, -1);
410
+ let value2 = trimmed.substring(eqIndex + 1);
411
+ if (value2.startsWith('"') && value2.endsWith('"') || value2.startsWith("'") && value2.endsWith("'")) {
412
+ value2 = value2.slice(1, -1);
346
413
  }
347
- if (key) result[key] = value;
414
+ if (key) result[key] = value2;
348
415
  }
349
416
  return result;
350
417
  }
@@ -664,7 +731,7 @@ function getDistinctId() {
664
731
  } catch {
665
732
  }
666
733
  return distinctId;
667
- } catch (error) {
734
+ } catch (error2) {
668
735
  console.warn("Failed to persist distinct ID, using session-based ID");
669
736
  distinctId = `session-${crypto.randomUUID()}`;
670
737
  return distinctId;
@@ -698,21 +765,21 @@ function trackEvent(event, properties) {
698
765
  ci: CI
699
766
  }
700
767
  });
701
- } catch (error) {
702
- console.debug("Analytics error:", error);
768
+ } catch (error2) {
769
+ console.debug("Analytics error:", error2);
703
770
  }
704
771
  }
705
772
  function sanitizeProperties(properties) {
706
773
  const sanitized = {};
707
- for (const [key, value] of Object.entries(properties)) {
774
+ for (const [key, value2] of Object.entries(properties)) {
708
775
  if (key.toLowerCase().includes("secret") || key.toLowerCase().includes("token") || key.toLowerCase().includes("password") || key.toLowerCase().includes("content") || key.toLowerCase().includes("key") || key.toLowerCase().includes("value")) {
709
776
  continue;
710
777
  }
711
- if (value && typeof value === "string" && value.length > 500) {
712
- sanitized[key] = `${value.slice(0, 200)}...`;
778
+ if (value2 && typeof value2 === "string" && value2.length > 500) {
779
+ sanitized[key] = `${value2.slice(0, 200)}...`;
713
780
  continue;
714
781
  }
715
- sanitized[key] = value;
782
+ sanitized[key] = value2;
716
783
  }
717
784
  return sanitized;
718
785
  }
@@ -741,8 +808,8 @@ function identifyUser(userId, properties) {
741
808
  alias: anonId
742
809
  });
743
810
  }
744
- } catch (error) {
745
- console.debug("Analytics identify error:", error);
811
+ } catch (error2) {
812
+ console.debug("Analytics identify error:", error2);
746
813
  }
747
814
  }
748
815
  var AnalyticsEvents = {
@@ -761,8 +828,6 @@ var AnalyticsEvents = {
761
828
  // src/cmds/readme.ts
762
829
  import fs3 from "fs";
763
830
  import path3 from "path";
764
- import prompts2 from "prompts";
765
- import pc2 from "picocolors";
766
831
  import balanced from "balanced-match";
767
832
  function generateBadge(repo) {
768
833
  return `[![Keyway Secrets](https://www.keyway.sh/badge.svg?repo=${repo})](https://www.keyway.sh/vaults/${repo})`;
@@ -853,22 +918,15 @@ async function ensureReadme(repoName, cwd) {
853
918
  if (existing) return existing;
854
919
  const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
855
920
  if (!isInteractive2) {
856
- console.log(pc2.yellow('No README found. Run "keyway readme add-badge" from a repo with a README.'));
921
+ warn('No README found. Run "keyway readme add-badge" from a repo with a README.');
857
922
  return null;
858
923
  }
859
- const { confirm } = await prompts2(
860
- {
861
- type: "confirm",
862
- name: "confirm",
863
- message: "No README found. Create a default README.md?",
864
- initial: false
865
- },
866
- {
867
- onCancel: () => ({ confirm: false })
868
- }
869
- );
870
- if (!confirm) {
871
- console.log(pc2.yellow("Skipping badge insertion (no README)."));
924
+ const confirm3 = await confirm2({
925
+ message: "No README found. Create a default README.md?",
926
+ initialValue: false
927
+ });
928
+ if (!confirm3) {
929
+ warn("Skipping badge insertion (no README).");
872
930
  return null;
873
931
  }
874
932
  const defaultPath = path3.join(cwd, "README.md");
@@ -891,94 +949,66 @@ async function addBadgeToReadme(silent = false) {
891
949
  const updated = insertBadgeIntoReadme(content, badge);
892
950
  if (updated === content) {
893
951
  if (!silent) {
894
- console.log(pc2.gray("Keyway badge already present in README."));
952
+ info("Keyway badge already present in README.");
895
953
  }
896
954
  return false;
897
955
  }
898
956
  fs3.writeFileSync(readmePath, updated, "utf-8");
899
957
  if (!silent) {
900
- console.log(pc2.green(`\u2713 Keyway badge added to ${path3.basename(readmePath)}`));
958
+ success(`Keyway badge added to ${path3.basename(readmePath)}`);
901
959
  }
902
960
  return true;
903
961
  }
904
962
 
905
963
  // src/cmds/push.ts
906
- import pc6 from "picocolors";
964
+ import pc3 from "picocolors";
907
965
  import fs5 from "fs";
908
966
  import path5 from "path";
909
- import prompts5 from "prompts";
910
967
 
911
968
  // src/cmds/login.ts
912
- import pc4 from "picocolors";
913
- import readline from "readline";
914
- import prompts3 from "prompts";
969
+ import pc2 from "picocolors";
970
+ import * as p2 from "@clack/prompts";
915
971
 
916
972
  // src/utils/helpers.ts
917
- import pc3 from "picocolors";
918
973
  import open from "open";
919
974
  function sleep(ms) {
920
975
  return new Promise((resolve) => setTimeout(resolve, ms));
921
976
  }
922
977
  async function openUrl(url) {
923
- console.log(pc3.gray(`
924
- Open this URL in your browser:
925
- ${url}
926
- `));
978
+ message(dim(`Open this URL in your browser:
979
+ ${url}`));
927
980
  await open(url).catch(() => {
928
981
  });
929
982
  }
930
983
  function isInteractive() {
931
984
  return Boolean(process.stdout.isTTY && process.stdin.isTTY && !process.env.CI);
932
985
  }
933
- function showUpgradePrompt(message, upgradeUrl) {
934
- console.log("");
935
- console.log(pc3.dim("\u2500".repeat(50)));
936
- console.log("");
937
- console.log(` ${pc3.yellow("\u26A1")} ${pc3.bold("Plan Limit Reached")}`);
938
- console.log("");
939
- console.log(pc3.white(` ${message}`));
940
- console.log("");
941
- console.log(` ${pc3.cyan("Upgrade now \u2192")} ${pc3.underline(upgradeUrl)}`);
942
- console.log("");
943
- console.log(pc3.dim("\u2500".repeat(50)));
944
- console.log("");
986
+ function showUpgradePrompt(message2, upgradeUrl) {
987
+ note2(`${message2}
988
+
989
+ Upgrade: ${link(upgradeUrl)}`, "Plan Limit Reached");
945
990
  }
946
991
  var MAX_CONSECUTIVE_ERRORS = 5;
947
992
 
948
993
  // src/cmds/login.ts
949
- async function promptYesNo(question, defaultYes = true) {
950
- return new Promise((resolve) => {
951
- const rl = readline.createInterface({
952
- input: process.stdin,
953
- output: process.stdout
954
- });
955
- rl.question(question, (answer) => {
956
- rl.close();
957
- const normalized = answer.trim().toLowerCase();
958
- if (!normalized) return resolve(defaultYes);
959
- if (["y", "yes"].includes(normalized)) return resolve(true);
960
- if (["n", "no"].includes(normalized)) return resolve(false);
961
- return resolve(defaultYes);
962
- });
963
- });
964
- }
965
994
  async function runLoginFlow() {
966
- console.log(pc4.blue("\u{1F510} Starting Keyway login...\n"));
967
995
  const repoName = detectGitRepo();
968
996
  const start = await startDeviceLogin(repoName);
969
997
  const verifyUrl = start.verificationUriComplete || start.verificationUri;
970
998
  if (!verifyUrl) {
971
999
  throw new Error("Missing verification URL from the auth server.");
972
1000
  }
973
- console.log(`Code: ${pc4.bold(pc4.green(start.userCode))}`);
1001
+ step(`Code: ${pc2.bold(pc2.green(start.userCode))}`);
974
1002
  await openUrl(verifyUrl);
975
- console.log("Waiting for auth...");
1003
+ const s = spinner2();
1004
+ s.start("Waiting for authorization...");
976
1005
  const pollIntervalMs = (start.interval ?? 5) * 1e3;
977
1006
  const maxTimeoutMs = Math.min((start.expiresIn ?? 900) * 1e3, 30 * 60 * 1e3);
978
1007
  const startTime = Date.now();
979
1008
  let consecutiveErrors = 0;
980
1009
  while (true) {
981
1010
  if (Date.now() - startTime > maxTimeoutMs) {
1011
+ s.stop("Login timed out");
982
1012
  throw new Error('Login timed out. Please run "keyway login" again.');
983
1013
  }
984
1014
  await sleep(pollIntervalMs);
@@ -1003,17 +1033,17 @@ async function runLoginFlow() {
1003
1033
  login_method: "device"
1004
1034
  });
1005
1035
  }
1006
- console.log(pc4.green("\n\u2713 Login successful"));
1007
- if (result.githubLogin) {
1008
- console.log(`Authenticated GitHub user: ${pc4.cyan(result.githubLogin)}`);
1009
- }
1036
+ s.stop("Authorized");
1037
+ success(`Logged in as ${value(`@${result.githubLogin}`)}`);
1010
1038
  return result.keywayToken;
1011
1039
  }
1040
+ s.stop("Authorization failed");
1012
1041
  throw new Error(result.message || "Authentication failed");
1013
- } catch (error) {
1042
+ } catch (error2) {
1014
1043
  consecutiveErrors++;
1015
1044
  if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
1016
- const errorMsg = error instanceof Error ? error.message : "Unknown error";
1045
+ const errorMsg = error2 instanceof Error ? error2.message : "Unknown error";
1046
+ s.stop("Login failed");
1017
1047
  throw new Error(`Login failed after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${errorMsg}`);
1018
1048
  }
1019
1049
  }
@@ -1025,7 +1055,7 @@ async function ensureLogin(options = {}) {
1025
1055
  return envToken;
1026
1056
  }
1027
1057
  if (process.env.GITHUB_TOKEN && !process.env.KEYWAY_TOKEN) {
1028
- console.warn(pc4.yellow("Note: GITHUB_TOKEN found but not used. Set KEYWAY_TOKEN for Keyway authentication."));
1058
+ warn("GITHUB_TOKEN found but not used. Set KEYWAY_TOKEN for Keyway authentication.");
1029
1059
  }
1030
1060
  const stored = await getStoredAuth();
1031
1061
  if (stored?.keywayToken) {
@@ -1036,7 +1066,10 @@ async function ensureLogin(options = {}) {
1036
1066
  if (!canPrompt) {
1037
1067
  throw new Error('No Keyway session found. Run "keyway login" to authenticate.');
1038
1068
  }
1039
- const proceed = await promptYesNo("No Keyway session found. Open the browser to sign in now? (Y/n) ");
1069
+ const proceed = await confirm2({
1070
+ message: "No Keyway session found. Open browser to sign in?",
1071
+ initialValue: true
1072
+ });
1040
1073
  if (!proceed) {
1041
1074
  throw new Error("Login required. Aborting.");
1042
1075
  }
@@ -1045,30 +1078,24 @@ async function ensureLogin(options = {}) {
1045
1078
  async function runTokenLogin() {
1046
1079
  const repoName = detectGitRepo();
1047
1080
  if (repoName) {
1048
- console.log(`\u{1F4C1} Detected: ${pc4.cyan(repoName)}`);
1081
+ step(`Detected repository: ${value(repoName)}`);
1049
1082
  }
1050
1083
  const description = repoName ? `Keyway CLI for ${repoName}` : "Keyway CLI";
1051
1084
  const url = `https://github.com/settings/personal-access-tokens/new?description=${encodeURIComponent(description)}`;
1052
1085
  await openUrl(url);
1053
- console.log(pc4.gray("Select the detected repo (or scope manually)."));
1054
- console.log(pc4.gray("Permissions: Metadata \u2192 Read-only; Account permissions: None."));
1055
- const { token } = await prompts3(
1056
- {
1057
- type: "password",
1058
- name: "token",
1059
- message: "Paste token:",
1060
- validate: (value) => {
1061
- if (!value || typeof value !== "string") return "Token is required";
1062
- if (!value.startsWith("github_pat_")) return "Token must start with github_pat_";
1063
- return true;
1064
- }
1065
- },
1066
- {
1067
- onCancel: () => {
1068
- throw new Error("Login cancelled.");
1069
- }
1086
+ info("Select the detected repo (or scope manually).");
1087
+ message(dim("Permissions: Metadata \u2192 Read-only; Account permissions: None."));
1088
+ const token = await p2.password({
1089
+ message: "Paste your GitHub PAT:",
1090
+ validate: (value2) => {
1091
+ if (!value2 || typeof value2 !== "string") return "Token is required";
1092
+ if (!value2.startsWith("github_pat_")) return "Token must start with github_pat_";
1093
+ return void 0;
1070
1094
  }
1071
- );
1095
+ });
1096
+ if (p2.isCancel(token)) {
1097
+ cancel2("Login cancelled.");
1098
+ }
1072
1099
  if (!token || typeof token !== "string") {
1073
1100
  throw new Error("Token is required.");
1074
1101
  }
@@ -1076,6 +1103,8 @@ async function runTokenLogin() {
1076
1103
  if (!trimmedToken.startsWith("github_pat_")) {
1077
1104
  throw new Error("Token must start with github_pat_.");
1078
1105
  }
1106
+ const s = spinner2();
1107
+ s.start("Validating token...");
1079
1108
  const validation = await validateToken(trimmedToken);
1080
1109
  await saveAuthToken(trimmedToken, {
1081
1110
  githubLogin: validation.username
@@ -1088,61 +1117,56 @@ async function runTokenLogin() {
1088
1117
  github_username: validation.username,
1089
1118
  login_method: "pat"
1090
1119
  });
1091
- console.log(pc4.green("\u2705 Authenticated"), `as ${pc4.cyan(`@${validation.username}`)}`);
1120
+ s.stop("Token validated");
1121
+ success(`Logged in as ${value(`@${validation.username}`)}`);
1092
1122
  return trimmedToken;
1093
1123
  }
1094
1124
  async function loginCommand(options = {}) {
1125
+ intro2("login");
1095
1126
  try {
1096
1127
  if (options.token) {
1097
1128
  await runTokenLogin();
1098
1129
  } else {
1099
1130
  await runLoginFlow();
1100
1131
  }
1101
- } catch (error) {
1102
- const message = error instanceof Error ? error.message : "Unexpected login error";
1132
+ outro2("Ready to sync secrets!");
1133
+ } catch (error2) {
1134
+ const message2 = error2 instanceof Error ? error2.message : "Unexpected login error";
1103
1135
  trackEvent(AnalyticsEvents.CLI_ERROR, {
1104
1136
  command: "login",
1105
- error: truncateMessage(message)
1137
+ error: truncateMessage(message2)
1106
1138
  });
1107
- console.error(pc4.red(`
1108
- \u2717 ${message}`));
1139
+ error(message2);
1109
1140
  process.exit(1);
1110
1141
  }
1111
1142
  }
1112
1143
  async function logoutCommand() {
1144
+ intro2("logout");
1113
1145
  clearAuth();
1114
- console.log(pc4.green("\u2713 Logged out of Keyway"));
1115
- console.log(pc4.gray(`Auth cache cleared: ${getAuthFilePath()}`));
1146
+ success("Logged out of Keyway");
1147
+ outro2(dim(`Auth cache cleared: ${getAuthFilePath()}`));
1116
1148
  }
1117
1149
 
1118
1150
  // src/utils/env.ts
1119
1151
  import fs4 from "fs";
1120
1152
  import path4 from "path";
1121
- import pc5 from "picocolors";
1122
- import prompts4 from "prompts";
1123
1153
  async function promptCreateEnvFile() {
1124
- const { createEnv } = await prompts4({
1125
- type: "confirm",
1126
- name: "createEnv",
1154
+ const createEnv = await confirm2({
1127
1155
  message: "No .env file found. Create one?",
1128
- initial: true
1129
- }, {
1130
- onCancel: () => {
1131
- throw new Error("Cancelled by user.");
1132
- }
1156
+ initialValue: true
1133
1157
  });
1134
1158
  if (!createEnv) {
1135
1159
  return false;
1136
1160
  }
1137
1161
  const envFilePath = path4.join(process.cwd(), ".env");
1138
1162
  fs4.writeFileSync(envFilePath, "# Add your environment variables here\n# Example: API_KEY=your-api-key\n");
1139
- console.log(pc5.green("\u2713 Created .env file"));
1163
+ success("Created .env file");
1140
1164
  return true;
1141
1165
  }
1142
1166
 
1143
1167
  // src/cmds/push.ts
1144
- function deriveEnvFromFile(file) {
1145
- const base = path5.basename(file);
1168
+ function deriveEnvFromFile(file2) {
1169
+ const base = path5.basename(file2);
1146
1170
  const match = base.match(/\.env(?:\.(.+))?$/);
1147
1171
  if (match) {
1148
1172
  return match[1] || "development";
@@ -1154,7 +1178,7 @@ function discoverEnvCandidates(cwd) {
1154
1178
  const entries = fs5.readdirSync(cwd);
1155
1179
  const hasEnvLocal = entries.includes(".env.local");
1156
1180
  if (hasEnvLocal) {
1157
- console.log(pc6.gray("\u2139\uFE0F Detected .env.local \u2014 not synced by design (machine-specific secrets)"));
1181
+ info("Detected .env.local \u2014 not synced by design (machine-specific secrets)");
1158
1182
  }
1159
1183
  const candidates = entries.filter((name) => name.startsWith(".env") && name !== ".env.local").map((name) => {
1160
1184
  const fullPath = path5.join(cwd, name);
@@ -1179,8 +1203,9 @@ function discoverEnvCandidates(cwd) {
1179
1203
  }
1180
1204
  }
1181
1205
  async function pushCommand(options) {
1206
+ intro2("push");
1207
+ await warnIfEnvNotGitignored();
1182
1208
  try {
1183
- console.log(pc6.blue("\u{1F510} Pushing secrets to Keyway...\n"));
1184
1209
  const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
1185
1210
  let environment = options.env;
1186
1211
  let envFile = options.file;
@@ -1193,7 +1218,7 @@ async function pushCommand(options) {
1193
1218
  if (!created) {
1194
1219
  throw new Error("No .env file found.");
1195
1220
  }
1196
- console.log(pc6.gray(" Add your variables and run keyway push again\n"));
1221
+ message(dim("Add your variables and run keyway push again"));
1197
1222
  return;
1198
1223
  }
1199
1224
  if (environment && !envFile) {
@@ -1203,47 +1228,30 @@ async function pushCommand(options) {
1203
1228
  }
1204
1229
  }
1205
1230
  if (!environment && !envFile && isInteractive2 && candidates.length > 0) {
1206
- const { choice } = await prompts5(
1207
- {
1208
- type: "select",
1209
- name: "choice",
1210
- message: "Select an env file to push:",
1211
- choices: [
1212
- ...candidates.map((c) => ({
1213
- title: `${c.file} (env: ${c.env})`,
1214
- value: c
1215
- })),
1216
- { title: "Enter a different file...", value: "custom" }
1217
- ]
1218
- },
1219
- {
1220
- onCancel: () => {
1221
- throw new Error("Push cancelled by user.");
1222
- }
1223
- }
1224
- );
1225
- if (choice && choice !== "custom") {
1226
- envFile = choice.file;
1227
- environment = choice.env;
1228
- } else if (choice === "custom") {
1229
- const { fileInput } = await prompts5(
1230
- {
1231
- type: "text",
1232
- name: "fileInput",
1233
- message: "Path to env file:",
1234
- validate: (value) => {
1235
- if (!value) return "Path is required";
1236
- const resolved = path5.resolve(process.cwd(), value);
1237
- if (!fs5.existsSync(resolved)) return `File not found: ${value}`;
1238
- return true;
1239
- }
1240
- },
1241
- {
1242
- onCancel: () => {
1243
- throw new Error("Push cancelled by user.");
1244
- }
1231
+ const choice = await select2({
1232
+ message: "Select an env file to push:",
1233
+ options: [
1234
+ ...candidates.map((c) => ({
1235
+ label: `${c.file} (env: ${c.env})`,
1236
+ value: c.file
1237
+ })),
1238
+ { label: "Enter a different file...", value: "__custom__" }
1239
+ ]
1240
+ });
1241
+ if (choice && choice !== "__custom__") {
1242
+ envFile = choice;
1243
+ const matched = candidates.find((c) => c.file === envFile);
1244
+ environment = matched?.env;
1245
+ } else if (choice === "__custom__") {
1246
+ const fileInput = await text2({
1247
+ message: "Path to env file:",
1248
+ validate: (value2) => {
1249
+ if (!value2) return "Path is required";
1250
+ const resolved = path5.resolve(process.cwd(), value2);
1251
+ if (!fs5.existsSync(resolved)) return `File not found: ${value2}`;
1252
+ return void 0;
1245
1253
  }
1246
- );
1254
+ });
1247
1255
  envFile = fileInput;
1248
1256
  environment = deriveEnvFromFile(fileInput);
1249
1257
  }
@@ -1266,31 +1274,21 @@ async function pushCommand(options) {
1266
1274
  const trimmed = line.trim();
1267
1275
  return trimmed.length > 0 && !trimmed.startsWith("#");
1268
1276
  });
1269
- console.log(`File: ${pc6.cyan(envFile)}`);
1270
- console.log(`Environment: ${pc6.cyan(environment)}`);
1271
- console.log(`Variables: ${pc6.cyan(lines.length.toString())}`);
1277
+ step(`File: ${file(envFile)}`);
1278
+ step(`Environment: ${value(environment)}`);
1279
+ step(`Variables: ${value(lines.length)}`);
1272
1280
  const repoFullName = getCurrentRepoFullName();
1273
- console.log(`Repository: ${pc6.cyan(repoFullName)}`);
1281
+ step(`Repository: ${value(repoFullName)}`);
1274
1282
  if (!options.yes) {
1275
- const isInteractive3 = process.stdin.isTTY && process.stdout.isTTY;
1276
- if (!isInteractive3) {
1283
+ if (!isInteractive2) {
1277
1284
  throw new Error("Confirmation required. Re-run with --yes in non-interactive environments.");
1278
1285
  }
1279
- const { confirm } = await prompts5(
1280
- {
1281
- type: "confirm",
1282
- name: "confirm",
1283
- message: `Send ${lines.length} secrets from ${envFile} (env: ${environment}) to ${repoFullName}?`,
1284
- initial: true
1285
- },
1286
- {
1287
- onCancel: () => {
1288
- throw new Error("Push cancelled by user.");
1289
- }
1290
- }
1291
- );
1292
- if (!confirm) {
1293
- console.log(pc6.yellow("Push aborted."));
1286
+ const confirm3 = await confirm2({
1287
+ message: `Push ${lines.length} secrets from ${envFile} to ${repoFullName}?`,
1288
+ initialValue: true
1289
+ });
1290
+ if (!confirm3) {
1291
+ warn("Push aborted.");
1294
1292
  return;
1295
1293
  }
1296
1294
  }
@@ -1300,41 +1298,40 @@ async function pushCommand(options) {
1300
1298
  environment,
1301
1299
  variableCount: lines.length
1302
1300
  });
1303
- console.log("\nUploading secrets...");
1301
+ const s = spinner2();
1302
+ s.start("Uploading secrets...");
1304
1303
  const response = await pushSecrets(repoFullName, environment, content, accessToken);
1305
- console.log(pc6.green("\n\u2713 " + response.message));
1304
+ s.stop("Uploaded");
1305
+ success(response.message);
1306
1306
  if (response.stats) {
1307
1307
  const { created, updated, deleted } = response.stats;
1308
1308
  const parts = [];
1309
- if (created > 0) parts.push(pc6.green(`+${created} created`));
1310
- if (updated > 0) parts.push(pc6.yellow(`~${updated} updated`));
1311
- if (deleted > 0) parts.push(pc6.red(`-${deleted} deleted`));
1309
+ if (created > 0) parts.push(pc3.green(`+${created} created`));
1310
+ if (updated > 0) parts.push(pc3.yellow(`~${updated} updated`));
1311
+ if (deleted > 0) parts.push(pc3.red(`-${deleted} deleted`));
1312
1312
  if (parts.length > 0) {
1313
- console.log(`Stats: ${parts.join(", ")}`);
1313
+ message(`Stats: ${parts.join(", ")}`);
1314
1314
  }
1315
1315
  }
1316
- console.log(`
1317
- Your secrets are now encrypted and stored securely.`);
1318
1316
  const dashboardLink = `https://www.keyway.sh/dashboard/vaults/${repoFullName}`;
1319
- console.log(`
1320
- ${pc6.blue("\u2394")} Dashboard: ${pc6.underline(dashboardLink)}`);
1317
+ outro2(`Dashboard: ${link(dashboardLink)}`);
1321
1318
  await shutdownAnalytics();
1322
- } catch (error) {
1323
- let message;
1319
+ } catch (error2) {
1320
+ let message2;
1324
1321
  let hint = null;
1325
- if (error instanceof APIError) {
1326
- message = error.message || `HTTP ${error.statusCode} - ${error.error}`;
1327
- const envNotFoundMatch = message.match(/Environment '([^']+)' does not exist.*Available environments: ([^.]+)/);
1322
+ if (error2 instanceof APIError) {
1323
+ message2 = error2.message || `HTTP ${error2.statusCode} - ${error2.error}`;
1324
+ const envNotFoundMatch = message2.match(/Environment '([^']+)' does not exist.*Available environments: ([^.]+)/);
1328
1325
  if (envNotFoundMatch) {
1329
1326
  const requestedEnv = envNotFoundMatch[1];
1330
1327
  const availableEnvs = envNotFoundMatch[2];
1331
- message = `Environment '${requestedEnv}' does not exist in this vault.`;
1328
+ message2 = `Environment '${requestedEnv}' does not exist in this vault.`;
1332
1329
  hint = `Available environments: ${availableEnvs}
1333
- Use ${pc6.cyan(`keyway push --env <environment>`)} to specify one, or create '${requestedEnv}' via the dashboard.`;
1330
+ Use ${command(`keyway push --env <environment>`)} to specify one.`;
1334
1331
  }
1335
- if (error.statusCode === 403 && (error.upgradeUrl || message.toLowerCase().includes("read-only"))) {
1336
- const upgradeMessage = message.toLowerCase().includes("read-only") ? "This vault is read-only on your current plan." : message;
1337
- const upgradeUrl = error.upgradeUrl || "https://keyway.sh/settings";
1332
+ if (error2.statusCode === 403 && (error2.upgradeUrl || message2.toLowerCase().includes("read-only"))) {
1333
+ const upgradeMessage = message2.toLowerCase().includes("read-only") ? "This vault is read-only on your current plan." : message2;
1334
+ const upgradeUrl = error2.upgradeUrl || "https://keyway.sh/settings";
1338
1335
  trackEvent(AnalyticsEvents.CLI_ERROR, {
1339
1336
  command: "push",
1340
1337
  error: upgradeMessage
@@ -1343,21 +1340,19 @@ Use ${pc6.cyan(`keyway push --env <environment>`)} to specify one, or create '${
1343
1340
  showUpgradePrompt(upgradeMessage, upgradeUrl);
1344
1341
  process.exit(1);
1345
1342
  }
1346
- } else if (error instanceof Error) {
1347
- message = truncateMessage(error.message);
1343
+ } else if (error2 instanceof Error) {
1344
+ message2 = truncateMessage(error2.message);
1348
1345
  } else {
1349
- message = "Unknown error";
1346
+ message2 = "Unknown error";
1350
1347
  }
1351
1348
  trackEvent(AnalyticsEvents.CLI_ERROR, {
1352
1349
  command: "push",
1353
- error: message
1350
+ error: message2
1354
1351
  });
1355
1352
  await shutdownAnalytics();
1356
- console.error(pc6.red(`
1357
- \u2717 ${message}`));
1353
+ error(message2);
1358
1354
  if (hint) {
1359
- console.error(pc6.gray(`
1360
- ${hint}`));
1355
+ message(dim(hint));
1361
1356
  }
1362
1357
  process.exit(1);
1363
1358
  }
@@ -1390,19 +1385,16 @@ async function ensureLoginAndGitHubApp(repoFullName, options = {}) {
1390
1385
  }
1391
1386
  const deviceStart = await startDeviceLogin(repoFullName);
1392
1387
  const installUrl = deviceStart.githubAppInstallUrl || "https://github.com/apps/keyway/installations/new";
1393
- console.log("");
1394
- const { shouldProceed } = await prompts6({
1395
- type: "confirm",
1396
- name: "shouldProceed",
1388
+ const shouldProceed = await confirm2({
1397
1389
  message: "Open browser to sign in?",
1398
- initial: true
1390
+ initialValue: true
1399
1391
  });
1400
1392
  if (!shouldProceed) {
1401
1393
  throw new Error('Setup required. Run "keyway init" when ready.');
1402
1394
  }
1403
1395
  await openUrl(deviceStart.verificationUriComplete);
1404
- console.log(pc7.blue("\u23F3 Waiting for authorization..."));
1405
- console.log(pc7.gray(" (Press Ctrl+C to cancel)\n"));
1396
+ const loginSpinner = spinner2();
1397
+ loginSpinner.start("Waiting for authorization...");
1406
1398
  const pollIntervalMs = Math.max((deviceStart.interval ?? 5) * 1e3, POLL_INTERVAL_MS);
1407
1399
  const startTime = Date.now();
1408
1400
  let accessToken = null;
@@ -1417,7 +1409,7 @@ async function ensureLoginAndGitHubApp(repoFullName, options = {}) {
1417
1409
  githubLogin: result.githubLogin,
1418
1410
  expiresAt: result.expiresAt
1419
1411
  });
1420
- console.log(pc7.green("\u2713 Signed in!"));
1412
+ loginSpinner.stop("Signed in!");
1421
1413
  if (result.githubLogin) {
1422
1414
  identifyUser(result.githubLogin, {
1423
1415
  github_username: result.githubLogin,
@@ -1427,46 +1419,38 @@ async function ensureLoginAndGitHubApp(repoFullName, options = {}) {
1427
1419
  break;
1428
1420
  }
1429
1421
  consecutiveErrors = 0;
1430
- process.stdout.write(pc7.gray("."));
1431
- } catch (error) {
1422
+ } catch (error2) {
1432
1423
  consecutiveErrors++;
1433
1424
  if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
1434
- const errorMsg = error instanceof Error ? error.message : "Unknown error";
1425
+ loginSpinner.stop("Failed");
1426
+ const errorMsg = error2 instanceof Error ? error2.message : "Unknown error";
1435
1427
  throw new Error(`Login failed after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${errorMsg}`);
1436
1428
  }
1437
1429
  }
1438
1430
  }
1439
1431
  if (!accessToken) {
1440
- console.log("");
1441
- console.log(pc7.yellow("\u26A0 Timed out waiting for sign in."));
1432
+ loginSpinner.stop("Timeout");
1433
+ warn("Timed out waiting for sign in.");
1442
1434
  throw new Error("Sign in timed out. Please try again.");
1443
1435
  }
1444
1436
  const installStatus = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
1445
1437
  if (installStatus.installed) {
1446
- console.log(pc7.green("\u2713 GitHub App installed"));
1447
- console.log("");
1438
+ success("GitHub App installed");
1448
1439
  return accessToken;
1449
1440
  }
1450
- console.log("");
1451
- console.log(pc7.yellow("\u26A0 GitHub App not installed on this repository"));
1452
- console.log(pc7.gray(" The Keyway GitHub App is required for secure access."));
1453
- console.log("");
1454
- const { shouldInstall } = await prompts6({
1455
- type: "confirm",
1456
- name: "shouldInstall",
1441
+ warn("GitHub App not installed on this repository");
1442
+ message(dim("The Keyway GitHub App is required for secure access."));
1443
+ const shouldInstall = await confirm2({
1457
1444
  message: "Open browser to install GitHub App?",
1458
- initial: true
1445
+ initialValue: true
1459
1446
  });
1460
1447
  if (!shouldInstall) {
1461
- console.log(pc7.gray(`
1462
- Install later: ${installUrl}`));
1448
+ message(dim(`Install later: ${installUrl}`));
1463
1449
  throw new Error("GitHub App installation required.");
1464
1450
  }
1465
1451
  await openUrl(installUrl);
1466
- console.log(pc7.blue("\u23F3 Waiting for GitHub App installation..."));
1467
- console.log(pc7.gray(' Add this repository and click "Install"'));
1468
- console.log(pc7.gray(" Then return here - the CLI will detect it automatically"));
1469
- console.log(pc7.gray(" (Press Ctrl+C to cancel)\n"));
1452
+ const installSpinner = spinner2();
1453
+ installSpinner.start("Waiting for GitHub App installation...");
1470
1454
  const installStartTime = Date.now();
1471
1455
  consecutiveErrors = 0;
1472
1456
  while (Date.now() - installStartTime < POLL_TIMEOUT_MS) {
@@ -1474,23 +1458,22 @@ async function ensureLoginAndGitHubApp(repoFullName, options = {}) {
1474
1458
  try {
1475
1459
  const pollStatus = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
1476
1460
  if (pollStatus.installed) {
1477
- console.log(pc7.green("\u2713 GitHub App installed!"));
1478
- console.log("");
1461
+ installSpinner.stop("GitHub App installed!");
1479
1462
  return accessToken;
1480
1463
  }
1481
1464
  consecutiveErrors = 0;
1482
- process.stdout.write(pc7.gray("."));
1483
- } catch (error) {
1465
+ } catch (error2) {
1484
1466
  consecutiveErrors++;
1485
1467
  if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
1486
- const errorMsg = error instanceof Error ? error.message : "Unknown error";
1468
+ installSpinner.stop("Failed");
1469
+ const errorMsg = error2 instanceof Error ? error2.message : "Unknown error";
1487
1470
  throw new Error(`Installation check failed after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${errorMsg}`);
1488
1471
  }
1489
1472
  }
1490
1473
  }
1491
- console.log("");
1492
- console.log(pc7.yellow("\u26A0 Timed out waiting for installation."));
1493
- console.log(pc7.gray(` Install the GitHub App: ${installUrl}`));
1474
+ installSpinner.stop("Timeout");
1475
+ warn("Timed out waiting for installation.");
1476
+ message(dim(`Install the GitHub App: ${installUrl}`));
1494
1477
  throw new Error("GitHub App installation timed out.");
1495
1478
  }
1496
1479
  async function ensureGitHubAppInstalledOnly(repoFullName, accessToken) {
@@ -1498,42 +1481,36 @@ async function ensureGitHubAppInstalledOnly(repoFullName, accessToken) {
1498
1481
  let status;
1499
1482
  try {
1500
1483
  status = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
1501
- } catch (error) {
1502
- if (error instanceof APIError && error.statusCode === 401) {
1503
- console.log(pc7.yellow("\n\u26A0 Session expired or invalid. Clearing credentials..."));
1504
- const { clearAuth: clearAuth2 } = await import("./auth-QLPQ24HZ.js");
1484
+ } catch (error2) {
1485
+ if (error2 instanceof APIError && error2.statusCode === 401) {
1486
+ warn("Session expired or invalid. Clearing credentials...");
1487
+ const { clearAuth: clearAuth2 } = await import("./auth-64V3RWUK.js");
1505
1488
  clearAuth2();
1506
1489
  return null;
1507
1490
  }
1508
- throw error;
1491
+ throw error2;
1509
1492
  }
1510
1493
  if (status.installed) {
1511
1494
  return accessToken;
1512
1495
  }
1513
- console.log("");
1514
- console.log(pc7.yellow("\u26A0 GitHub App not installed for this repository"));
1515
- console.log("");
1516
- console.log(pc7.gray(" The Keyway GitHub App is required to securely manage secrets."));
1517
- console.log(pc7.gray(" It only requests minimal permissions (repository metadata)."));
1518
- console.log("");
1496
+ warn("GitHub App not installed for this repository");
1497
+ message(dim("The Keyway GitHub App is required to securely manage secrets."));
1498
+ message(dim("It only requests minimal permissions (repository metadata)."));
1519
1499
  if (!isInteractive()) {
1520
- console.log(pc7.gray(` Install the Keyway GitHub App: ${status.installUrl}`));
1500
+ message(dim(`Install the Keyway GitHub App: ${status.installUrl}`));
1521
1501
  throw new Error("GitHub App installation required.");
1522
1502
  }
1523
- const { shouldInstall } = await prompts6({
1524
- type: "confirm",
1525
- name: "shouldInstall",
1503
+ const shouldInstall = await confirm2({
1526
1504
  message: "Open browser to install Keyway GitHub App?",
1527
- initial: true
1505
+ initialValue: true
1528
1506
  });
1529
1507
  if (!shouldInstall) {
1530
- console.log(pc7.gray(`
1531
- You can install later: ${status.installUrl}`));
1508
+ message(dim(`You can install later: ${status.installUrl}`));
1532
1509
  throw new Error("GitHub App installation required.");
1533
1510
  }
1534
1511
  await openUrl(status.installUrl);
1535
- console.log(pc7.blue("\u23F3 Waiting for GitHub App installation..."));
1536
- console.log(pc7.gray(" (Press Ctrl+C to cancel)\n"));
1512
+ const s = spinner2();
1513
+ s.start("Waiting for GitHub App installation...");
1537
1514
  const startTime = Date.now();
1538
1515
  let consecutiveErrors = 0;
1539
1516
  while (Date.now() - startTime < POLL_TIMEOUT_MS) {
@@ -1541,251 +1518,266 @@ async function ensureGitHubAppInstalledOnly(repoFullName, accessToken) {
1541
1518
  try {
1542
1519
  const pollStatus = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
1543
1520
  if (pollStatus.installed) {
1544
- console.log(pc7.green("\u2713 GitHub App installed!"));
1545
- console.log("");
1521
+ s.stop("GitHub App installed!");
1546
1522
  return accessToken;
1547
1523
  }
1548
1524
  consecutiveErrors = 0;
1549
- process.stdout.write(pc7.gray("."));
1550
- } catch (error) {
1525
+ } catch (error2) {
1551
1526
  consecutiveErrors++;
1552
1527
  if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
1553
- const errorMsg = error instanceof Error ? error.message : "Unknown error";
1528
+ s.stop("Failed");
1529
+ const errorMsg = error2 instanceof Error ? error2.message : "Unknown error";
1554
1530
  throw new Error(`Installation check failed after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${errorMsg}`);
1555
1531
  }
1556
1532
  }
1557
1533
  }
1558
- console.log("");
1559
- console.log(pc7.yellow("\u26A0 Timed out waiting for installation."));
1560
- console.log(pc7.gray(` You can install the GitHub App later: ${status.installUrl}`));
1534
+ s.stop("Timeout");
1535
+ warn("Timed out waiting for installation.");
1536
+ message(dim(`You can install the GitHub App later: ${status.installUrl}`));
1561
1537
  throw new Error("GitHub App installation timed out.");
1562
1538
  }
1563
1539
  async function initCommand(options = {}) {
1564
1540
  try {
1565
1541
  const repoFullName = getCurrentRepoFullName();
1566
1542
  const dashboardLink = `${DASHBOARD_URL}/${repoFullName}`;
1567
- console.log(pc7.blue("\u{1F510} Initializing Keyway vault...\n"));
1568
- console.log(` ${pc7.gray("Repository:")} ${pc7.white(repoFullName)}`);
1543
+ intro2("init");
1544
+ await warnIfEnvNotGitignored();
1545
+ step(`Repository: ${value(repoFullName)}`);
1569
1546
  const accessToken = await ensureLoginAndGitHubApp(repoFullName, {
1570
1547
  allowPrompt: options.loginPrompt !== false
1571
1548
  });
1572
1549
  trackEvent(AnalyticsEvents.CLI_INIT, { repoFullName, githubAppInstalled: true });
1573
1550
  const vaultExists = await checkVaultExists(accessToken, repoFullName);
1574
1551
  if (vaultExists) {
1575
- console.log(pc7.green("\n\u2713 Already initialized!\n"));
1576
- console.log(` ${pc7.yellow("\u2192")} Run ${pc7.cyan("keyway push")} to sync your secrets`);
1577
- console.log(` ${pc7.blue("\u2394")} Dashboard: ${pc7.underline(dashboardLink)}`);
1578
- console.log("");
1552
+ success("Already initialized!");
1553
+ message(dim(`Run ${command("keyway push")} to sync your secrets`));
1554
+ outro2(`Dashboard: ${link(dashboardLink)}`);
1579
1555
  await shutdownAnalytics();
1580
1556
  return;
1581
1557
  }
1582
1558
  await initVault(repoFullName, accessToken);
1583
- console.log(pc7.green("\u2713 Vault created!"));
1559
+ success("Vault created!");
1584
1560
  try {
1585
1561
  const badgeAdded = await addBadgeToReadme(true);
1586
1562
  if (badgeAdded) {
1587
- console.log(pc7.green("\u2713 Badge added to README.md"));
1563
+ success("Badge added to README.md");
1588
1564
  }
1589
1565
  } catch {
1590
1566
  }
1591
- console.log("");
1592
1567
  const envCandidates = discoverEnvCandidates(process.cwd());
1593
- const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
1594
- if (envCandidates.length > 0 && isInteractive2) {
1595
- console.log(pc7.gray(` Found ${envCandidates.length} env file(s): ${envCandidates.map((c) => c.file).join(", ")}
1596
- `));
1597
- const { shouldPush } = await prompts6({
1598
- type: "confirm",
1599
- name: "shouldPush",
1568
+ const interactive = process.stdin.isTTY && process.stdout.isTTY;
1569
+ if (envCandidates.length > 0 && interactive) {
1570
+ message(dim(`Found ${envCandidates.length} env file(s): ${envCandidates.map((c) => c.file).join(", ")}`));
1571
+ const shouldPush = await confirm2({
1600
1572
  message: "Push secrets now?",
1601
- initial: true
1573
+ initialValue: true
1602
1574
  });
1603
1575
  if (shouldPush) {
1604
- console.log("");
1605
1576
  await pushCommand({ loginPrompt: false, yes: false });
1606
1577
  return;
1607
1578
  }
1608
1579
  }
1609
- console.log(pc7.dim("\u2500".repeat(50)));
1610
- console.log("");
1611
1580
  if (envCandidates.length === 0) {
1612
- if (isInteractive2) {
1581
+ if (interactive) {
1613
1582
  const created = await promptCreateEnvFile();
1614
1583
  if (created) {
1615
- console.log(` Add your variables and run ${pc7.cyan("keyway push")}
1616
- `);
1584
+ message(dim(`Add your variables and run ${command("keyway push")}`));
1617
1585
  } else {
1618
- console.log(` Next: Create ${pc7.cyan(".env")} and run ${pc7.cyan("keyway push")}
1619
- `);
1586
+ message(dim(`Next: Create ${file(".env")} and run ${command("keyway push")}`));
1620
1587
  }
1621
1588
  } else {
1622
- console.log(`${pc7.yellow("\u26A0")} No .env file found - your vault is empty`);
1623
- console.log(` Next: Create ${pc7.cyan(".env")} and run ${pc7.cyan("keyway push")}
1624
- `);
1589
+ warn("No .env file found - your vault is empty");
1590
+ message(dim(`Next: Create ${file(".env")} and run ${command("keyway push")}`));
1625
1591
  }
1626
1592
  } else {
1627
- console.log(` ${pc7.yellow("\u2192")} Run ${pc7.cyan("keyway push")} to sync your secrets
1628
- `);
1593
+ message(dim(`Run ${command("keyway push")} to sync your secrets`));
1629
1594
  }
1630
- console.log(` ${pc7.blue("\u2394")} Dashboard: ${pc7.underline(dashboardLink)}`);
1631
- console.log("");
1595
+ outro2(`Dashboard: ${link(dashboardLink)}`);
1632
1596
  await shutdownAnalytics();
1633
- } catch (error) {
1634
- if (error instanceof APIError) {
1635
- if (error.statusCode === 409) {
1636
- console.log(pc7.green("\n\u2713 Already initialized!\n"));
1637
- console.log(` ${pc7.yellow("\u2192")} Run ${pc7.cyan("keyway push")} to sync your secrets`);
1638
- console.log(` ${pc7.blue("\u2394")} Dashboard: ${pc7.underline(`${DASHBOARD_URL}/${getCurrentRepoFullName()}`)}`);
1639
- console.log("");
1597
+ } catch (error2) {
1598
+ if (error2 instanceof APIError) {
1599
+ if (error2.statusCode === 409) {
1600
+ success("Already initialized!");
1601
+ message(dim(`Run ${command("keyway push")} to sync your secrets`));
1602
+ outro2(`Dashboard: ${link(`${DASHBOARD_URL}/${getCurrentRepoFullName()}`)}`);
1640
1603
  await shutdownAnalytics();
1641
1604
  return;
1642
1605
  }
1643
- if (error.error === "Plan Limit Reached" || error.upgradeUrl) {
1644
- const upgradeUrl = error.upgradeUrl || "https://keyway.sh/pricing";
1645
- showUpgradePrompt(error.message, upgradeUrl);
1606
+ if (error2.error === "Plan Limit Reached" || error2.upgradeUrl) {
1607
+ const upgradeUrl = error2.upgradeUrl || "https://keyway.sh/pricing";
1608
+ showUpgradePrompt(error2.message, upgradeUrl);
1646
1609
  await shutdownAnalytics();
1647
1610
  process.exit(1);
1648
1611
  }
1649
1612
  }
1650
- const message = error instanceof APIError ? error.message : error instanceof Error ? truncateMessage(error.message) : "Unknown error";
1613
+ const message2 = error2 instanceof APIError ? error2.message : error2 instanceof Error ? truncateMessage(error2.message) : "Unknown error";
1651
1614
  trackEvent(AnalyticsEvents.CLI_ERROR, {
1652
1615
  command: "init",
1653
- error: message
1616
+ error: message2
1654
1617
  });
1655
1618
  await shutdownAnalytics();
1656
- console.error(pc7.red(`
1657
- \u2717 ${message}`));
1619
+ error(message2);
1658
1620
  process.exit(1);
1659
1621
  }
1660
1622
  }
1661
1623
 
1662
1624
  // src/cmds/pull.ts
1663
- import pc8 from "picocolors";
1664
1625
  import fs6 from "fs";
1665
1626
  import path6 from "path";
1666
- import prompts7 from "prompts";
1667
1627
  async function pullCommand(options) {
1628
+ intro2("pull");
1629
+ await warnIfEnvNotGitignored();
1668
1630
  try {
1669
1631
  const environment = options.env || "development";
1670
1632
  const envFile = options.file || ".env";
1671
- console.log(pc8.blue("\u{1F510} Pulling secrets from Keyway...\n"));
1672
- console.log(`Environment: ${pc8.cyan(environment)}`);
1633
+ step(`Environment: ${value(environment)}`);
1673
1634
  const repoFullName = getCurrentRepoFullName();
1674
- console.log(`Repository: ${pc8.cyan(repoFullName)}`);
1635
+ step(`Repository: ${value(repoFullName)}`);
1675
1636
  const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
1676
1637
  trackEvent(AnalyticsEvents.CLI_PULL, {
1677
1638
  repoFullName,
1678
1639
  environment
1679
1640
  });
1680
- console.log("\nDownloading secrets...");
1641
+ const s = spinner2();
1642
+ s.start("Downloading secrets...");
1681
1643
  const response = await pullSecrets(repoFullName, environment, accessToken);
1682
1644
  const envFilePath = path6.resolve(process.cwd(), envFile);
1683
1645
  if (fs6.existsSync(envFilePath)) {
1646
+ s.stop("Downloaded");
1684
1647
  const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
1685
1648
  if (options.yes) {
1686
- console.log(pc8.yellow(`
1687
- \u26A0 Overwriting existing file: ${envFile}`));
1649
+ warn(`Overwriting existing file: ${envFile}`);
1688
1650
  } else if (!isInteractive2) {
1689
1651
  throw new Error(`File ${envFile} exists. Re-run with --yes to overwrite or choose a different --file.`);
1690
1652
  } else {
1691
- const { confirm } = await prompts7(
1692
- {
1693
- type: "confirm",
1694
- name: "confirm",
1695
- message: `${envFile} exists. Overwrite with secrets from ${environment}?`,
1696
- initial: false
1697
- },
1698
- {
1699
- onCancel: () => {
1700
- throw new Error("Pull cancelled by user.");
1701
- }
1702
- }
1703
- );
1704
- if (!confirm) {
1705
- console.log(pc8.yellow("Pull aborted."));
1653
+ const confirm3 = await confirm2({
1654
+ message: `${envFile} exists. Overwrite with secrets from ${environment}?`,
1655
+ initialValue: false
1656
+ });
1657
+ if (!confirm3) {
1658
+ warn("Pull aborted.");
1706
1659
  return;
1707
1660
  }
1708
1661
  }
1662
+ } else {
1663
+ s.stop("Downloaded");
1709
1664
  }
1710
1665
  fs6.writeFileSync(envFilePath, response.content, "utf-8");
1711
1666
  const lines = response.content.split("\n").filter((line) => {
1712
1667
  const trimmed = line.trim();
1713
1668
  return trimmed.length > 0 && !trimmed.startsWith("#");
1714
1669
  });
1715
- console.log(pc8.green(`
1716
- \u2713 Secrets downloaded successfully`));
1717
- console.log(`
1718
- File: ${pc8.cyan(envFile)}`);
1719
- console.log(`Variables: ${pc8.cyan(lines.length.toString())}`);
1670
+ success(`Secrets downloaded to ${file(envFile)}`);
1671
+ message(`Variables: ${value(lines.length)}`);
1672
+ outro2("Secrets synced!");
1720
1673
  await shutdownAnalytics();
1721
- } catch (error) {
1722
- const message = error instanceof APIError ? `API ${error.statusCode}: ${error.message}` : error instanceof Error ? truncateMessage(error.message) : "Unknown error";
1674
+ } catch (error2) {
1675
+ const message2 = error2 instanceof APIError ? `API ${error2.statusCode}: ${error2.message}` : error2 instanceof Error ? truncateMessage(error2.message) : "Unknown error";
1723
1676
  trackEvent(AnalyticsEvents.CLI_ERROR, {
1724
1677
  command: "pull",
1725
- error: message
1678
+ error: message2
1726
1679
  });
1727
1680
  await shutdownAnalytics();
1728
- console.error(pc8.red(`
1729
- \u2717 ${message}`));
1681
+ error(message2);
1730
1682
  process.exit(1);
1731
1683
  }
1732
1684
  }
1733
1685
 
1734
1686
  // src/cmds/doctor.ts
1735
- import pc9 from "picocolors";
1687
+ import pc4 from "picocolors";
1736
1688
 
1737
1689
  // src/core/doctor.ts
1738
1690
  import { execSync as execSync2 } from "child_process";
1739
- import { writeFileSync, unlinkSync, readFileSync, existsSync } from "fs";
1740
- import { tmpdir } from "os";
1741
- import { join } from "path";
1691
+ import { readFileSync, existsSync } from "fs";
1742
1692
  var API_HEALTH_URL = `${process.env.KEYWAY_API_URL || INTERNAL_API_URL}/v1/health`;
1743
- async function checkNode() {
1744
- const nodeVersion = process.versions.node;
1745
- const [major] = nodeVersion.split(".").map(Number);
1746
- if (major >= 18) {
1693
+ async function checkAuth() {
1694
+ try {
1695
+ const auth = await getStoredAuth();
1696
+ if (!auth) {
1697
+ return {
1698
+ id: "auth",
1699
+ name: "Authentication",
1700
+ status: "warn",
1701
+ detail: "Not logged in. Run: keyway login"
1702
+ };
1703
+ }
1704
+ try {
1705
+ const result = await validateToken(auth.keywayToken);
1706
+ return {
1707
+ id: "auth",
1708
+ name: "Authentication",
1709
+ status: "pass",
1710
+ detail: `Logged in as ${result.login || auth.githubLogin || "user"}`
1711
+ };
1712
+ } catch {
1713
+ return {
1714
+ id: "auth",
1715
+ name: "Authentication",
1716
+ status: "warn",
1717
+ detail: "Token expired or invalid. Run: keyway login"
1718
+ };
1719
+ }
1720
+ } catch {
1747
1721
  return {
1748
- id: "node",
1749
- name: "Node.js version",
1750
- status: "pass",
1751
- detail: `v${nodeVersion} (>=18.0.0 required)`
1722
+ id: "auth",
1723
+ name: "Authentication",
1724
+ status: "warn",
1725
+ detail: "Unable to check authentication status"
1752
1726
  };
1753
1727
  }
1754
- return {
1755
- id: "node",
1756
- name: "Node.js version",
1757
- status: "fail",
1758
- detail: `v${nodeVersion} (<18.0.0, please upgrade)`
1759
- };
1760
1728
  }
1761
- async function checkGit() {
1729
+ async function checkGitHubRemote() {
1762
1730
  try {
1763
- const gitVersion = execSync2("git --version", { encoding: "utf-8" }).trim();
1764
1731
  try {
1765
1732
  execSync2("git rev-parse --is-inside-work-tree", {
1766
1733
  encoding: "utf-8",
1767
1734
  stdio: ["pipe", "pipe", "ignore"]
1768
1735
  });
1736
+ } catch {
1769
1737
  return {
1770
- id: "git",
1771
- name: "Git repository",
1772
- status: "pass",
1773
- detail: `${gitVersion} - inside repository`
1738
+ id: "github",
1739
+ name: "GitHub repository",
1740
+ status: "warn",
1741
+ detail: "Not in a git repository"
1742
+ };
1743
+ }
1744
+ try {
1745
+ const remoteUrl = execSync2("git remote get-url origin", {
1746
+ encoding: "utf-8",
1747
+ stdio: ["pipe", "pipe", "ignore"]
1748
+ }).trim();
1749
+ const sshMatch = remoteUrl.match(/git@github\.com:(.+)\/(.+?)(\.git)?$/);
1750
+ const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/(.+)\/(.+?)(\.git)?$/);
1751
+ if (sshMatch || httpsMatch) {
1752
+ const match = sshMatch || httpsMatch;
1753
+ const repoName = `${match[1]}/${match[2]}`;
1754
+ return {
1755
+ id: "github",
1756
+ name: "GitHub repository",
1757
+ status: "pass",
1758
+ detail: repoName
1759
+ };
1760
+ }
1761
+ return {
1762
+ id: "github",
1763
+ name: "GitHub repository",
1764
+ status: "warn",
1765
+ detail: "Remote is not a GitHub URL"
1774
1766
  };
1775
1767
  } catch {
1776
1768
  return {
1777
- id: "git",
1778
- name: "Git repository",
1769
+ id: "github",
1770
+ name: "GitHub repository",
1779
1771
  status: "warn",
1780
- detail: `${gitVersion} - not in a repository`
1772
+ detail: "No remote origin configured"
1781
1773
  };
1782
1774
  }
1783
1775
  } catch {
1784
1776
  return {
1785
- id: "git",
1786
- name: "Git repository",
1777
+ id: "github",
1778
+ name: "GitHub repository",
1787
1779
  status: "warn",
1788
- detail: "Git not installed"
1780
+ detail: "Unable to detect repository"
1789
1781
  };
1790
1782
  }
1791
1783
  }
@@ -1821,8 +1813,8 @@ async function checkNetwork() {
1821
1813
  status: "warn",
1822
1814
  detail: `Server returned ${response.status}`
1823
1815
  };
1824
- } catch (error) {
1825
- if (error.name === "AbortError") {
1816
+ } catch (error2) {
1817
+ if (error2.name === "AbortError") {
1826
1818
  return {
1827
1819
  id: "network",
1828
1820
  name: "API connectivity",
@@ -1830,7 +1822,7 @@ async function checkNetwork() {
1830
1822
  detail: "Connection timeout (>2s)"
1831
1823
  };
1832
1824
  }
1833
- if (error.code === "ENOTFOUND") {
1825
+ if (error2.code === "ENOTFOUND") {
1834
1826
  return {
1835
1827
  id: "network",
1836
1828
  name: "API connectivity",
@@ -1838,7 +1830,7 @@ async function checkNetwork() {
1838
1830
  detail: "DNS resolution failed"
1839
1831
  };
1840
1832
  }
1841
- if (error.code === "CERT_HAS_EXPIRED" || error.code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") {
1833
+ if (error2.code === "CERT_HAS_EXPIRED" || error2.code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") {
1842
1834
  return {
1843
1835
  id: "network",
1844
1836
  name: "API connectivity",
@@ -1850,27 +1842,103 @@ async function checkNetwork() {
1850
1842
  id: "network",
1851
1843
  name: "API connectivity",
1852
1844
  status: "warn",
1853
- detail: error.message || "Connection failed"
1845
+ detail: error2.message || "Connection failed"
1854
1846
  };
1855
1847
  }
1856
1848
  }
1857
- async function checkFileSystem() {
1858
- const testFile = join(tmpdir(), `.keyway-test-${Date.now()}.tmp`);
1859
- try {
1860
- writeFileSync(testFile, "test");
1861
- unlinkSync(testFile);
1849
+ async function checkEnvFile() {
1850
+ const envFiles = [".env", ".env.local", ".env.development"];
1851
+ const found = [];
1852
+ for (const file2 of envFiles) {
1853
+ if (existsSync(file2)) {
1854
+ found.push(file2);
1855
+ }
1856
+ }
1857
+ if (found.length === 0) {
1862
1858
  return {
1863
- id: "filesystem",
1864
- name: "File system permissions",
1865
- status: "pass",
1866
- detail: "Write permissions verified"
1859
+ id: "envfile",
1860
+ name: "Environment file",
1861
+ status: "warn",
1862
+ detail: "No .env file found. Run: keyway pull"
1867
1863
  };
1868
- } catch (error) {
1864
+ }
1865
+ return {
1866
+ id: "envfile",
1867
+ name: "Environment file",
1868
+ status: "pass",
1869
+ detail: found.join(", ")
1870
+ };
1871
+ }
1872
+ async function checkSyncs(repoFullName) {
1873
+ if (!repoFullName) {
1869
1874
  return {
1870
- id: "filesystem",
1871
- name: "File system permissions",
1872
- status: "fail",
1873
- detail: `Cannot write to temp directory: ${error.message}`
1875
+ id: "syncs",
1876
+ name: "Provider syncs",
1877
+ status: "warn",
1878
+ detail: "Not in a GitHub repository"
1879
+ };
1880
+ }
1881
+ try {
1882
+ const auth = await getStoredAuth();
1883
+ if (!auth) {
1884
+ return {
1885
+ id: "syncs",
1886
+ name: "Provider syncs",
1887
+ status: "warn",
1888
+ detail: "Login required to check"
1889
+ };
1890
+ }
1891
+ try {
1892
+ const { connections } = await getConnections(auth.keywayToken);
1893
+ if (connections.length === 0) {
1894
+ return {
1895
+ id: "syncs",
1896
+ name: "Provider syncs",
1897
+ status: "pass",
1898
+ detail: "No integrations connected"
1899
+ };
1900
+ }
1901
+ const providers = [...new Set(connections.map((c) => c.provider))];
1902
+ const linkedProviders = [];
1903
+ const repoLower = repoFullName.toLowerCase();
1904
+ for (const provider of providers) {
1905
+ try {
1906
+ const { projects } = await getAllProviderProjects(auth.keywayToken, provider);
1907
+ const hasLinked = projects.some((p5) => p5.linkedRepo?.toLowerCase() === repoLower);
1908
+ if (hasLinked) {
1909
+ linkedProviders.push(provider);
1910
+ }
1911
+ } catch {
1912
+ }
1913
+ }
1914
+ if (linkedProviders.length === 0) {
1915
+ return {
1916
+ id: "syncs",
1917
+ name: "Provider syncs",
1918
+ status: "pass",
1919
+ detail: "None for this repo"
1920
+ };
1921
+ }
1922
+ return {
1923
+ id: "syncs",
1924
+ name: "Provider syncs",
1925
+ status: "pass",
1926
+ detail: linkedProviders.join(", ")
1927
+ };
1928
+ } catch {
1929
+ return {
1930
+ id: "syncs",
1931
+ name: "Provider syncs",
1932
+ status: "warn",
1933
+ detail: "Unable to check"
1934
+ };
1935
+ }
1936
+ } catch {
1937
+ return {
1938
+ id: "syncs",
1939
+ name: "Provider syncs",
1940
+ status: "warn",
1941
+ detail: "Unable to check"
1874
1942
  };
1875
1943
  }
1876
1944
  }
@@ -1910,59 +1978,17 @@ async function checkGitignore() {
1910
1978
  };
1911
1979
  }
1912
1980
  }
1913
- async function checkSystemClock() {
1914
- try {
1915
- const controller = new AbortController();
1916
- const timeout = setTimeout(() => controller.abort(), 2e3);
1917
- const response = await fetch(API_HEALTH_URL, {
1918
- method: "HEAD",
1919
- signal: controller.signal
1920
- });
1921
- clearTimeout(timeout);
1922
- const serverDate = response.headers.get("date");
1923
- if (!serverDate) {
1924
- return {
1925
- id: "clock",
1926
- name: "System clock",
1927
- status: "pass",
1928
- detail: "Unable to verify (no server date)"
1929
- };
1930
- }
1931
- const serverTime = new Date(serverDate).getTime();
1932
- const localTime = Date.now();
1933
- const diffMinutes = Math.abs(serverTime - localTime) / 1e3 / 60;
1934
- if (diffMinutes < 5) {
1935
- return {
1936
- id: "clock",
1937
- name: "System clock",
1938
- status: "pass",
1939
- detail: `Synchronized (drift: ${Math.round(diffMinutes * 60)}s)`
1940
- };
1941
- }
1942
- return {
1943
- id: "clock",
1944
- name: "System clock",
1945
- status: "warn",
1946
- detail: `Clock drift: ${Math.round(diffMinutes)} minutes`
1947
- };
1948
- } catch {
1949
- return {
1950
- id: "clock",
1951
- name: "System clock",
1952
- status: "pass",
1953
- detail: "Unable to verify"
1954
- };
1955
- }
1956
- }
1957
1981
  async function runAllChecks(options = {}) {
1958
- const checks = await Promise.all([
1959
- checkNode(),
1960
- checkGit(),
1982
+ const githubResult = await checkGitHubRemote();
1983
+ const repoFullName = githubResult.status === "pass" ? githubResult.detail || null : null;
1984
+ const [authResult, networkResult, syncsResult, envFileResult, gitignoreResult] = await Promise.all([
1985
+ checkAuth(),
1961
1986
  checkNetwork(),
1962
- checkFileSystem(),
1963
- checkGitignore(),
1964
- checkSystemClock()
1987
+ checkSyncs(repoFullName),
1988
+ checkEnvFile(),
1989
+ checkGitignore()
1965
1990
  ]);
1991
+ const checks = [authResult, githubResult, networkResult, syncsResult, envFileResult, gitignoreResult];
1966
1992
  if (options.strict) {
1967
1993
  checks.forEach((check) => {
1968
1994
  if (check.status === "warn") {
@@ -1986,9 +2012,9 @@ async function runAllChecks(options = {}) {
1986
2012
  // src/cmds/doctor.ts
1987
2013
  function formatSummary(results) {
1988
2014
  const parts = [
1989
- pc9.green(`${results.summary.pass} passed`),
1990
- results.summary.warn > 0 ? pc9.yellow(`${results.summary.warn} warnings`) : null,
1991
- results.summary.fail > 0 ? pc9.red(`${results.summary.fail} failed`) : null
2015
+ pc4.green(`${results.summary.pass} passed`),
2016
+ results.summary.warn > 0 ? pc4.yellow(`${results.summary.warn} warnings`) : null,
2017
+ results.summary.fail > 0 ? pc4.red(`${results.summary.fail} failed`) : null
1992
2018
  ].filter(Boolean);
1993
2019
  return parts.join(", ");
1994
2020
  }
@@ -2005,24 +2031,28 @@ async function doctorCommand(options = {}) {
2005
2031
  process.stdout.write(JSON.stringify(results, null, 0) + "\n");
2006
2032
  process.exit(results.exitCode);
2007
2033
  }
2008
- console.log(pc9.cyan("\n\u{1F50D} Keyway Doctor - Environment Check\n"));
2034
+ intro2("doctor");
2009
2035
  results.checks.forEach((check) => {
2010
- const icon = check.status === "pass" ? pc9.green("\u2713") : check.status === "warn" ? pc9.yellow("!") : pc9.red("\u2717");
2011
- const detail = check.detail ? pc9.dim(` \u2014 ${check.detail}`) : "";
2012
- console.log(` ${icon} ${check.name}${detail}`);
2036
+ const detail = check.detail ? dim(` \u2014 ${check.detail}`) : "";
2037
+ if (check.status === "pass") {
2038
+ success(`${check.name}${detail}`);
2039
+ } else if (check.status === "warn") {
2040
+ warn(`${check.name}${detail}`);
2041
+ } else {
2042
+ error(`${check.name}${detail}`);
2043
+ }
2013
2044
  });
2014
- console.log(`
2015
- Summary: ${formatSummary(results)}`);
2045
+ message(`Summary: ${formatSummary(results)}`);
2016
2046
  if (results.summary.fail > 0) {
2017
- console.log(pc9.red("\u26A0 Some checks failed. Please resolve the issues above before using Keyway."));
2047
+ outro2("Some checks failed. Please resolve the issues above.");
2018
2048
  } else if (results.summary.warn > 0) {
2019
- console.log(pc9.yellow("\u26A0 Some warnings detected. Keyway should work but consider addressing them."));
2049
+ outro2("Some warnings detected. Keyway should work but consider addressing them.");
2020
2050
  } else {
2021
- console.log(pc9.green("\u2728 All checks passed! Your environment is ready for Keyway."));
2051
+ outro2("All checks passed! Your environment is ready.");
2022
2052
  }
2023
2053
  process.exit(results.exitCode);
2024
- } catch (error) {
2025
- const message = error instanceof Error ? truncateMessage(error.message) : "Doctor failed";
2054
+ } catch (error2) {
2055
+ const message2 = error2 instanceof Error ? truncateMessage(error2.message) : "Doctor failed";
2026
2056
  trackEvent(AnalyticsEvents.CLI_DOCTOR, {
2027
2057
  pass: 0,
2028
2058
  warn: 0,
@@ -2035,12 +2065,11 @@ Summary: ${formatSummary(results)}`);
2035
2065
  checks: [],
2036
2066
  summary: { pass: 0, warn: 0, fail: 1 },
2037
2067
  exitCode: 1,
2038
- error: message
2068
+ error: message2
2039
2069
  };
2040
2070
  process.stdout.write(JSON.stringify(errorResult, null, 0) + "\n");
2041
2071
  } else {
2042
- console.error(pc9.red(`
2043
- \u2717 ${message}`));
2072
+ error(message2);
2044
2073
  }
2045
2074
  process.exit(1);
2046
2075
  }
@@ -2049,8 +2078,7 @@ Summary: ${formatSummary(results)}`);
2049
2078
  // src/cmds/ci.ts
2050
2079
  import { execSync as execSync3 } from "child_process";
2051
2080
  import { Octokit } from "@octokit/rest";
2052
- import pc10 from "picocolors";
2053
- import prompts8 from "prompts";
2081
+ import * as p3 from "@clack/prompts";
2054
2082
  function isGhAvailable() {
2055
2083
  try {
2056
2084
  execSync3("gh auth status", { stdio: "ignore" });
@@ -2068,89 +2096,82 @@ function addSecretWithGh(repo, secretName, secretValue) {
2068
2096
  async function ciSetupCommand(options) {
2069
2097
  const repo = options.repo || detectGitRepo();
2070
2098
  if (!repo) {
2071
- console.error(pc10.red("Not in a git repository. Use --repo owner/repo"));
2099
+ error("Not in a git repository. Use --repo owner/repo");
2072
2100
  process.exit(1);
2073
2101
  }
2074
- console.log(pc10.bold(`
2075
- \u{1F510} Setting up GitHub Actions for ${repo}
2076
- `));
2077
- console.log(pc10.dim("Step 1: Keyway Authentication"));
2102
+ intro2("ci setup");
2103
+ step(`Setting up GitHub Actions for ${value(repo)}`);
2104
+ message(dim("Step 1: Keyway Authentication"));
2078
2105
  let keywayToken;
2079
2106
  try {
2080
2107
  keywayToken = await ensureLogin({ allowPrompt: true });
2081
- console.log(pc10.green(" \u2713 Authenticated with Keyway\n"));
2108
+ success("Authenticated with Keyway");
2082
2109
  } catch {
2083
- console.error(pc10.red(" \u2717 Failed to authenticate with Keyway"));
2084
- console.error(pc10.dim(" Run `keyway login` first"));
2110
+ error("Failed to authenticate with Keyway");
2111
+ message(dim("Run `keyway login` first"));
2085
2112
  process.exit(1);
2086
2113
  }
2087
2114
  const useGh = isGhAvailable();
2088
2115
  if (useGh) {
2089
- console.log(pc10.dim("Step 2: Adding secret via GitHub CLI"));
2116
+ message(dim("Step 2: Adding secret via GitHub CLI"));
2090
2117
  try {
2091
2118
  addSecretWithGh(repo, "KEYWAY_TOKEN", keywayToken);
2092
- console.log(pc10.green(` \u2713 Secret KEYWAY_TOKEN added to ${repo}
2093
- `));
2094
- } catch (error) {
2095
- const message = error instanceof Error ? error.message : String(error);
2096
- console.error(pc10.red(` \u2717 Failed to add secret: ${message}`));
2097
- console.error(pc10.dim(" Try running: gh auth login"));
2119
+ success(`Secret KEYWAY_TOKEN added to ${repo}`);
2120
+ } catch (error2) {
2121
+ const message2 = error2 instanceof Error ? error2.message : String(error2);
2122
+ error(`Failed to add secret: ${message2}`);
2123
+ message(dim("Try running: gh auth login"));
2098
2124
  process.exit(1);
2099
2125
  }
2100
2126
  } else {
2101
- console.log(pc10.dim("Step 2: Temporary GitHub PAT"));
2102
- console.log(" gh CLI not found. We need a one-time GitHub PAT.");
2103
- console.log(pc10.dim(" You can delete it immediately after setup.\n"));
2127
+ message(dim("Step 2: Temporary GitHub PAT"));
2128
+ info("gh CLI not found. We need a one-time GitHub PAT.");
2129
+ message(dim("You can delete it immediately after setup."));
2104
2130
  const patUrl = "https://github.com/settings/tokens/new?scopes=repo&description=Keyway%20CI%20Setup%20(temporary)";
2105
2131
  await openUrl(patUrl);
2106
- const { githubToken } = await prompts8({
2107
- type: "password",
2108
- name: "githubToken",
2132
+ const githubToken = await p3.password({
2109
2133
  message: "Paste your GitHub PAT:"
2110
2134
  });
2111
- if (!githubToken) {
2112
- console.error(pc10.red("\n \u2717 GitHub PAT is required"));
2135
+ if (p3.isCancel(githubToken) || !githubToken) {
2136
+ error("GitHub PAT is required");
2113
2137
  process.exit(1);
2114
2138
  }
2115
2139
  const octokit = new Octokit({ auth: githubToken });
2116
2140
  try {
2117
2141
  await octokit.users.getAuthenticated();
2118
- console.log(pc10.green(" \u2713 GitHub PAT validated\n"));
2142
+ success("GitHub PAT validated");
2119
2143
  } catch {
2120
- console.error(pc10.red(" \u2717 Invalid GitHub PAT"));
2144
+ error("Invalid GitHub PAT");
2121
2145
  process.exit(1);
2122
2146
  }
2123
- console.log(pc10.dim("Step 3: Adding secret to repository"));
2147
+ message(dim("Step 3: Adding secret to repository"));
2124
2148
  const [owner, repoName] = repo.split("/");
2125
2149
  try {
2126
2150
  await addRepoSecret(octokit, owner, repoName, "KEYWAY_TOKEN", keywayToken);
2127
- console.log(pc10.green(` \u2713 Secret KEYWAY_TOKEN added to ${repo}
2128
- `));
2129
- } catch (error) {
2130
- const message = error instanceof Error ? error.message : String(error);
2131
- if (message.includes("Not Found")) {
2132
- console.error(pc10.red(` \u2717 Repository not found or no access: ${repo}`));
2133
- console.error(pc10.dim(" Make sure the PAT has access to this repository"));
2151
+ success(`Secret KEYWAY_TOKEN added to ${repo}`);
2152
+ } catch (error2) {
2153
+ const message2 = error2 instanceof Error ? error2.message : String(error2);
2154
+ if (message2.includes("Not Found")) {
2155
+ error(`Repository not found or no access: ${repo}`);
2156
+ message(dim("Make sure the PAT has access to this repository"));
2134
2157
  } else {
2135
- console.error(pc10.red(` \u2717 Failed to add secret: ${message}`));
2158
+ error(`Failed to add secret: ${message2}`);
2136
2159
  }
2137
2160
  process.exit(1);
2138
2161
  }
2139
2162
  }
2140
- console.log(pc10.green(pc10.bold("\u2713 Setup complete!\n")));
2141
- console.log("Add this to your workflow (.github/workflows/*.yml):\n");
2142
- console.log(
2143
- pc10.cyan(` - uses: keywaysh/keyway-action@v1
2144
- with:
2145
- token: \${{ secrets.KEYWAY_TOKEN }}
2146
- environment: production`)
2163
+ success("Setup complete!");
2164
+ note2(
2165
+ `- uses: keywaysh/keyway-action@v1
2166
+ with:
2167
+ token: \${{ secrets.KEYWAY_TOKEN }}
2168
+ environment: production`,
2169
+ "Add this to your workflow"
2147
2170
  );
2148
- console.log();
2149
2171
  if (!useGh) {
2150
- console.log(`\u{1F5D1}\uFE0F Delete the temporary PAT: ${pc10.underline("https://github.com/settings/tokens")}`);
2172
+ message(`Delete the temporary PAT: ${link("https://github.com/settings/tokens")}`);
2151
2173
  }
2152
- console.log(pc10.dim(`\u{1F4D6} Docs: ${pc10.underline("https://docs.keyway.sh/ci")}
2153
- `));
2174
+ outro2(`Docs: ${link("https://docs.keyway.sh/ci")}`);
2154
2175
  }
2155
2176
  async function addRepoSecret(octokit, owner, repo, secretName, secretValue) {
2156
2177
  const { data: publicKey } = await octokit.rest.actions.getRepoPublicKey({
@@ -2177,8 +2198,7 @@ async function encryptSecret(publicKey, secret) {
2177
2198
  }
2178
2199
 
2179
2200
  // src/cmds/connect.ts
2180
- import pc11 from "picocolors";
2181
- import prompts9 from "prompts";
2201
+ import * as p4 from "@clack/prompts";
2182
2202
  var TOKEN_AUTH_PROVIDERS = ["railway"];
2183
2203
  function getTokenCreationUrl(provider) {
2184
2204
  switch (provider) {
@@ -2191,38 +2211,37 @@ function getTokenCreationUrl(provider) {
2191
2211
  async function connectWithTokenFlow(accessToken, provider, displayName) {
2192
2212
  const tokenUrl = getTokenCreationUrl(provider);
2193
2213
  if (provider === "railway") {
2194
- console.log(pc11.yellow("\nTip: Select the workspace containing your projects."));
2195
- console.log(pc11.yellow(` Do NOT use "No workspace" - it won't have access to your projects.`));
2214
+ warn("Tip: Select the workspace containing your projects.");
2215
+ message(dim(`Do NOT use "No workspace" - it won't have access to your projects.`));
2196
2216
  }
2197
2217
  await openUrl(tokenUrl);
2198
- const { token } = await prompts9({
2199
- type: "password",
2200
- name: "token",
2218
+ const token = await p4.password({
2201
2219
  message: `${displayName} API Token:`
2202
2220
  });
2203
- if (!token) {
2204
- console.log(pc11.gray("Cancelled."));
2221
+ if (p4.isCancel(token) || !token) {
2222
+ message(dim("Cancelled."));
2205
2223
  return false;
2206
2224
  }
2207
- console.log(pc11.gray("\nValidating token..."));
2225
+ const s = spinner2();
2226
+ s.start("Validating token...");
2208
2227
  try {
2209
2228
  const result = await connectWithToken(accessToken, provider, token);
2229
+ s.stop("Validated");
2210
2230
  if (result.success) {
2211
- console.log(pc11.green(`
2212
- \u2713 Connected to ${displayName}!`));
2213
- console.log(pc11.gray(` Account: ${result.user.username}`));
2231
+ success(`Connected to ${displayName}!`);
2232
+ message(dim(`Account: ${result.user.username}`));
2214
2233
  if (result.user.teamName) {
2215
- console.log(pc11.gray(` Team: ${result.user.teamName}`));
2234
+ message(dim(`Team: ${result.user.teamName}`));
2216
2235
  }
2217
2236
  return true;
2218
2237
  } else {
2219
- console.log(pc11.red("\n\u2717 Connection failed."));
2238
+ error("Connection failed.");
2220
2239
  return false;
2221
2240
  }
2222
- } catch (error) {
2223
- const message = error instanceof Error ? error.message : "Token validation failed";
2224
- console.log(pc11.red(`
2225
- \u2717 ${message}`));
2241
+ } catch (error2) {
2242
+ s.stop("Failed");
2243
+ const message2 = error2 instanceof Error ? error2.message : "Token validation failed";
2244
+ error(message2);
2226
2245
  return false;
2227
2246
  }
2228
2247
  }
@@ -2230,7 +2249,8 @@ async function connectWithOAuthFlow(accessToken, provider, displayName) {
2230
2249
  const authUrl = getProviderAuthUrl(provider, accessToken);
2231
2250
  const startTime = /* @__PURE__ */ new Date();
2232
2251
  await openUrl(authUrl);
2233
- console.log(pc11.gray("Waiting for authorization..."));
2252
+ const s = spinner2();
2253
+ s.start("Waiting for authorization...");
2234
2254
  const maxAttempts = 60;
2235
2255
  let attempts = 0;
2236
2256
  while (attempts < maxAttempts) {
@@ -2242,60 +2262,55 @@ async function connectWithOAuthFlow(accessToken, provider, displayName) {
2242
2262
  (c) => c.provider === provider && new Date(c.createdAt) > startTime
2243
2263
  );
2244
2264
  if (newConn) {
2245
- console.log(pc11.green(`
2246
- \u2713 Connected to ${displayName}!`));
2265
+ s.stop("Authorized");
2266
+ success(`Connected to ${displayName}!`);
2247
2267
  return true;
2248
2268
  }
2249
2269
  } catch {
2250
2270
  }
2251
2271
  }
2252
- console.log(pc11.red("\n\u2717 Authorization timeout."));
2253
- console.log(pc11.gray("Run `keyway connections` to check if the connection was established."));
2272
+ s.stop("Timeout");
2273
+ error("Authorization timeout.");
2274
+ message(dim("Run `keyway connections` to check if the connection was established."));
2254
2275
  return false;
2255
2276
  }
2256
2277
  async function connectCommand(provider, options = {}) {
2257
2278
  try {
2258
2279
  const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
2259
2280
  const { providers } = await getProviders();
2260
- const providerInfo = providers.find((p) => p.name === provider.toLowerCase());
2281
+ const providerInfo = providers.find((p5) => p5.name === provider.toLowerCase());
2261
2282
  if (!providerInfo) {
2262
- const available = providers.map((p) => p.name).join(", ");
2263
- console.error(pc11.red(`Unknown provider: ${provider}`));
2264
- console.log(pc11.gray(`Available providers: ${available || "none"}`));
2283
+ const available = providers.map((p5) => p5.name).join(", ");
2284
+ error(`Unknown provider: ${provider}`);
2285
+ message(dim(`Available providers: ${available || "none"}`));
2265
2286
  process.exit(1);
2266
2287
  }
2267
2288
  if (!providerInfo.configured) {
2268
- console.error(pc11.red(`Provider ${providerInfo.displayName} is not configured on the server.`));
2269
- console.log(pc11.gray("Contact your administrator to enable this integration."));
2289
+ error(`Provider ${providerInfo.displayName} is not configured on the server.`);
2290
+ message(dim("Contact your administrator to enable this integration."));
2270
2291
  process.exit(1);
2271
2292
  }
2272
2293
  const { connections } = await getConnections(accessToken);
2273
2294
  const existingConnections = connections.filter((c) => c.provider === provider.toLowerCase());
2274
2295
  if (existingConnections.length > 0) {
2275
- console.log(pc11.gray(`
2276
- You have ${existingConnections.length} ${providerInfo.displayName} connection(s):`));
2296
+ message(dim(`You have ${existingConnections.length} ${providerInfo.displayName} connection(s):`));
2277
2297
  for (const conn of existingConnections) {
2278
2298
  const teamInfo = conn.providerTeamId ? `(Team: ${conn.providerTeamId})` : "(Personal)";
2279
- console.log(pc11.gray(` - ${teamInfo}`));
2299
+ message(dim(` - ${teamInfo}`));
2280
2300
  }
2281
- console.log("");
2282
- const { action } = await prompts9({
2283
- type: "select",
2284
- name: "action",
2301
+ const action = await select2({
2285
2302
  message: "What would you like to do?",
2286
- choices: [
2287
- { title: "Add another account/team", value: "add" },
2288
- { title: "Cancel", value: "cancel" }
2303
+ options: [
2304
+ { label: "Add another account/team", value: "add" },
2305
+ { label: "Cancel", value: "cancel" }
2289
2306
  ]
2290
2307
  });
2291
2308
  if (action !== "add") {
2292
- console.log(pc11.gray("Keeping existing connections."));
2309
+ message(dim("Keeping existing connections."));
2293
2310
  return;
2294
2311
  }
2295
2312
  }
2296
- console.log(pc11.blue(`
2297
- Connecting to ${providerInfo.displayName}...
2298
- `));
2313
+ step(`Connecting to ${providerInfo.displayName}...`);
2299
2314
  let connected = false;
2300
2315
  if (TOKEN_AUTH_PROVIDERS.includes(provider.toLowerCase())) {
2301
2316
  connected = await connectWithTokenFlow(accessToken, provider.toLowerCase(), providerInfo.displayName);
@@ -2306,14 +2321,13 @@ Connecting to ${providerInfo.displayName}...
2306
2321
  provider: provider.toLowerCase(),
2307
2322
  success: connected
2308
2323
  });
2309
- } catch (error) {
2310
- const message = error instanceof Error ? error.message : "Connection failed";
2324
+ } catch (error2) {
2325
+ const message2 = error2 instanceof Error ? error2.message : "Connection failed";
2311
2326
  trackEvent(AnalyticsEvents.CLI_ERROR, {
2312
2327
  command: "connect",
2313
- error: truncateMessage(message)
2328
+ error: truncateMessage(message2)
2314
2329
  });
2315
- console.error(pc11.red(`
2316
- \u2717 ${message}`));
2330
+ error(message2);
2317
2331
  process.exit(1);
2318
2332
  }
2319
2333
  }
@@ -2322,25 +2336,24 @@ async function connectionsCommand(options = {}) {
2322
2336
  const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
2323
2337
  const { connections } = await getConnections(accessToken);
2324
2338
  if (connections.length === 0) {
2325
- console.log(pc11.gray("No provider connections found."));
2326
- console.log(pc11.gray("\nConnect to a provider with: keyway connect <provider>"));
2327
- console.log(pc11.gray("Available providers: vercel, railway"));
2339
+ info("No provider connections found.");
2340
+ message(dim("Connect to a provider with: keyway connect <provider>"));
2341
+ message(dim("Available providers: vercel, railway"));
2328
2342
  return;
2329
2343
  }
2330
- console.log(pc11.blue("\n\u{1F4E1} Provider Connections\n"));
2344
+ intro2("connections");
2331
2345
  for (const conn of connections) {
2332
2346
  const providerName = conn.provider.charAt(0).toUpperCase() + conn.provider.slice(1);
2333
- const teamInfo = conn.providerTeamId ? pc11.gray(` (Team: ${conn.providerTeamId})`) : "";
2347
+ const teamInfo = conn.providerTeamId ? dim(` (Team: ${conn.providerTeamId})`) : "";
2334
2348
  const date = new Date(conn.createdAt).toLocaleDateString();
2335
- console.log(` ${pc11.green("\u25CF")} ${pc11.bold(providerName)}${teamInfo}`);
2336
- console.log(pc11.gray(` Connected: ${date}`));
2337
- console.log(pc11.gray(` ID: ${conn.id}`));
2338
- console.log("");
2339
- }
2340
- } catch (error) {
2341
- const message = error instanceof Error ? error.message : "Failed to list connections";
2342
- console.error(pc11.red(`
2343
- \u2717 ${message}`));
2349
+ success(`${bold(providerName)}${teamInfo}`);
2350
+ message(dim(` Connected: ${date}`));
2351
+ message(dim(` ID: ${conn.id}`));
2352
+ }
2353
+ outro2("");
2354
+ } catch (error2) {
2355
+ const message2 = error2 instanceof Error ? error2.message : "Failed to list connections";
2356
+ error(message2);
2344
2357
  process.exit(1);
2345
2358
  }
2346
2359
  }
@@ -2350,41 +2363,36 @@ async function disconnectCommand(provider, options = {}) {
2350
2363
  const { connections } = await getConnections(accessToken);
2351
2364
  const connection = connections.find((c) => c.provider === provider.toLowerCase());
2352
2365
  if (!connection) {
2353
- console.log(pc11.gray(`No connection found for provider: ${provider}`));
2366
+ info(`No connection found for provider: ${provider}`);
2354
2367
  return;
2355
2368
  }
2356
2369
  const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
2357
- const { confirm } = await prompts9({
2358
- type: "confirm",
2359
- name: "confirm",
2370
+ const confirm3 = await confirm2({
2360
2371
  message: `Disconnect from ${providerName}?`,
2361
- initial: false
2372
+ initialValue: false
2362
2373
  });
2363
- if (!confirm) {
2364
- console.log(pc11.gray("Cancelled."));
2374
+ if (!confirm3) {
2375
+ message(dim("Cancelled."));
2365
2376
  return;
2366
2377
  }
2367
2378
  await deleteConnection(accessToken, connection.id);
2368
- console.log(pc11.green(`
2369
- \u2713 Disconnected from ${providerName}`));
2379
+ success(`Disconnected from ${providerName}`);
2370
2380
  trackEvent(AnalyticsEvents.CLI_DISCONNECT, {
2371
2381
  provider: provider.toLowerCase()
2372
2382
  });
2373
- } catch (error) {
2374
- const message = error instanceof Error ? error.message : "Disconnect failed";
2383
+ } catch (error2) {
2384
+ const message2 = error2 instanceof Error ? error2.message : "Disconnect failed";
2375
2385
  trackEvent(AnalyticsEvents.CLI_ERROR, {
2376
2386
  command: "disconnect",
2377
- error: truncateMessage(message)
2387
+ error: truncateMessage(message2)
2378
2388
  });
2379
- console.error(pc11.red(`
2380
- \u2717 ${message}`));
2389
+ error(message2);
2381
2390
  process.exit(1);
2382
2391
  }
2383
2392
  }
2384
2393
 
2385
2394
  // src/cmds/sync.ts
2386
- import pc12 from "picocolors";
2387
- import prompts10 from "prompts";
2395
+ import pc5 from "picocolors";
2388
2396
  function mapToVercelEnvironment(keywayEnv) {
2389
2397
  const mapping = {
2390
2398
  production: "production",
@@ -2428,38 +2436,35 @@ function mapToProviderEnvironment(provider, keywayEnv) {
2428
2436
  function displayDiffSummary(diff, providerName) {
2429
2437
  const totalDiff = diff.onlyInKeyway.length + diff.onlyInProvider.length + diff.different.length;
2430
2438
  if (totalDiff === 0 && diff.same.length > 0) {
2431
- console.log(pc12.green(`
2432
- \u2713 Already in sync (${diff.same.length} secrets)`));
2439
+ success(`Already in sync (${diff.same.length} secrets)`);
2433
2440
  return;
2434
2441
  }
2435
- console.log(pc12.blue("\n\u{1F4CA} Comparison Summary\n"));
2436
- console.log(pc12.gray(` Keyway: ${diff.keywayCount} secrets | ${providerName}: ${diff.providerCount} secrets
2437
- `));
2442
+ step("Comparison Summary");
2443
+ message(dim(`Keyway: ${diff.keywayCount} secrets | ${providerName}: ${diff.providerCount} secrets`));
2438
2444
  if (diff.onlyInKeyway.length > 0) {
2439
- console.log(pc12.cyan(` \u2192 ${diff.onlyInKeyway.length} only in Keyway`));
2440
- diff.onlyInKeyway.slice(0, 3).forEach((key) => console.log(pc12.gray(` ${key}`)));
2445
+ message(pc5.cyan(`\u2192 ${diff.onlyInKeyway.length} only in Keyway`));
2446
+ diff.onlyInKeyway.slice(0, 3).forEach((key) => message(dim(` ${key}`)));
2441
2447
  if (diff.onlyInKeyway.length > 3) {
2442
- console.log(pc12.gray(` ... and ${diff.onlyInKeyway.length - 3} more`));
2448
+ message(dim(` ... and ${diff.onlyInKeyway.length - 3} more`));
2443
2449
  }
2444
2450
  }
2445
2451
  if (diff.onlyInProvider.length > 0) {
2446
- console.log(pc12.magenta(` \u2190 ${diff.onlyInProvider.length} only in ${providerName}`));
2447
- diff.onlyInProvider.slice(0, 3).forEach((key) => console.log(pc12.gray(` ${key}`)));
2452
+ message(pc5.magenta(`\u2190 ${diff.onlyInProvider.length} only in ${providerName}`));
2453
+ diff.onlyInProvider.slice(0, 3).forEach((key) => message(dim(` ${key}`)));
2448
2454
  if (diff.onlyInProvider.length > 3) {
2449
- console.log(pc12.gray(` ... and ${diff.onlyInProvider.length - 3} more`));
2455
+ message(dim(` ... and ${diff.onlyInProvider.length - 3} more`));
2450
2456
  }
2451
2457
  }
2452
2458
  if (diff.different.length > 0) {
2453
- console.log(pc12.yellow(` \u2260 ${diff.different.length} with different values`));
2454
- diff.different.slice(0, 3).forEach((key) => console.log(pc12.gray(` ${key}`)));
2459
+ message(pc5.yellow(`\u2260 ${diff.different.length} with different values`));
2460
+ diff.different.slice(0, 3).forEach((key) => message(dim(` ${key}`)));
2455
2461
  if (diff.different.length > 3) {
2456
- console.log(pc12.gray(` ... and ${diff.different.length - 3} more`));
2462
+ message(dim(` ... and ${diff.different.length - 3} more`));
2457
2463
  }
2458
2464
  }
2459
2465
  if (diff.same.length > 0) {
2460
- console.log(pc12.gray(` = ${diff.same.length} identical`));
2466
+ message(dim(`= ${diff.same.length} identical`));
2461
2467
  }
2462
- console.log("");
2463
2468
  }
2464
2469
  function getProjectDisplayName(project) {
2465
2470
  return project.serviceName || project.name;
@@ -2469,17 +2474,17 @@ function findMatchingProject(projects, repoFullName) {
2469
2474
  const repoName = repoFullName.split("/")[1]?.toLowerCase();
2470
2475
  if (!repoName) return void 0;
2471
2476
  const linkedMatch = projects.find(
2472
- (p) => p.linkedRepo?.toLowerCase() === repoFullNameLower
2477
+ (p5) => p5.linkedRepo?.toLowerCase() === repoFullNameLower
2473
2478
  );
2474
2479
  if (linkedMatch) {
2475
2480
  return { project: linkedMatch, matchType: "linked_repo" };
2476
2481
  }
2477
- const exactNameMatch = projects.find((p) => p.name.toLowerCase() === repoName);
2482
+ const exactNameMatch = projects.find((p5) => p5.name.toLowerCase() === repoName);
2478
2483
  if (exactNameMatch) {
2479
2484
  return { project: exactNameMatch, matchType: "exact_name" };
2480
2485
  }
2481
2486
  const partialMatches = projects.filter(
2482
- (p) => p.name.toLowerCase().includes(repoName) || repoName.includes(p.name.toLowerCase())
2487
+ (p5) => p5.name.toLowerCase().includes(repoName) || repoName.includes(p5.name.toLowerCase())
2483
2488
  );
2484
2489
  if (partialMatches.length === 1) {
2485
2490
  return { project: partialMatches[0], matchType: "partial_name" };
@@ -2497,102 +2502,129 @@ function projectMatchesRepo(project, repoFullName) {
2497
2502
  }
2498
2503
  return false;
2499
2504
  }
2500
- async function promptProjectSelection(projects, repoFullName) {
2505
+ async function selectProjectWithConnectOption(accessToken, provider, providerDisplayName, repoFullName, initialProjects) {
2506
+ let projects = initialProjects;
2507
+ while (true) {
2508
+ const result = await promptProjectSelection(projects, repoFullName, providerDisplayName);
2509
+ if (result === "connect_new") {
2510
+ await connectCommand(provider, { loginPrompt: false });
2511
+ const { projects: allProjects } = await getAllProviderProjects(accessToken, provider.toLowerCase());
2512
+ projects = allProjects.map((p5) => ({
2513
+ id: p5.id,
2514
+ name: p5.name,
2515
+ serviceId: p5.serviceId,
2516
+ serviceName: p5.serviceName,
2517
+ linkedRepo: p5.linkedRepo,
2518
+ environments: p5.environments,
2519
+ connectionId: p5.connectionId,
2520
+ teamId: p5.teamId,
2521
+ teamName: p5.teamName
2522
+ }));
2523
+ if (projects.length === 0) {
2524
+ error("No projects found after connecting.");
2525
+ process.exit(1);
2526
+ }
2527
+ success(`Found ${projects.length} projects. Select one:`);
2528
+ continue;
2529
+ }
2530
+ return { project: result, projects };
2531
+ }
2532
+ }
2533
+ async function promptProjectSelection(projects, repoFullName, providerDisplayName) {
2501
2534
  const repoName = repoFullName.split("/")[1]?.toLowerCase() || "";
2502
- const uniqueTeams = new Set(projects.map((p) => p.teamId || "personal"));
2535
+ const uniqueTeams = new Set(projects.map((p5) => p5.teamId || "personal"));
2503
2536
  const hasMultipleAccounts = uniqueTeams.size > 1;
2504
- const choices = projects.map((p) => {
2505
- const displayName = getProjectDisplayName(p);
2506
- let title = displayName;
2537
+ const options = projects.map((p5) => {
2538
+ const displayName = getProjectDisplayName(p5);
2539
+ let label = displayName;
2507
2540
  const badges = [];
2508
2541
  if (hasMultipleAccounts) {
2509
- if (p.teamName) {
2510
- badges.push(pc12.cyan(`[${p.teamName}]`));
2511
- } else if (p.teamId) {
2512
- const shortTeamId = p.teamId.length > 12 ? p.teamId.slice(0, 12) + "..." : p.teamId;
2513
- badges.push(pc12.cyan(`[team:${shortTeamId}]`));
2542
+ if (p5.teamName) {
2543
+ badges.push(pc5.cyan(`[${p5.teamName}]`));
2544
+ } else if (p5.teamId) {
2545
+ const shortTeamId = p5.teamId.length > 12 ? p5.teamId.slice(0, 12) + "..." : p5.teamId;
2546
+ badges.push(pc5.cyan(`[team:${shortTeamId}]`));
2514
2547
  } else {
2515
- badges.push(pc12.cyan("[personal]"));
2548
+ badges.push(pc5.cyan("[personal]"));
2516
2549
  }
2517
2550
  }
2518
- if (p.linkedRepo?.toLowerCase() === repoFullName.toLowerCase()) {
2519
- badges.push(pc12.green("\u2190 linked"));
2520
- } else if (p.name.toLowerCase() === repoName || p.serviceName?.toLowerCase() === repoName) {
2521
- badges.push(pc12.green("\u2190 same name"));
2522
- } else if (p.linkedRepo) {
2523
- badges.push(pc12.gray(`\u2192 ${p.linkedRepo}`));
2551
+ if (p5.linkedRepo?.toLowerCase() === repoFullName.toLowerCase()) {
2552
+ badges.push(pc5.green("\u2190 linked"));
2553
+ } else if (p5.name.toLowerCase() === repoName || p5.serviceName?.toLowerCase() === repoName) {
2554
+ badges.push(pc5.green("\u2190 same name"));
2555
+ } else if (p5.linkedRepo) {
2556
+ badges.push(pc5.gray(`\u2192 ${p5.linkedRepo}`));
2524
2557
  }
2525
2558
  if (badges.length > 0) {
2526
- title = `${displayName} ${badges.join(" ")}`;
2559
+ label = `${displayName} ${badges.join(" ")}`;
2527
2560
  }
2528
- return { title, value: p.id };
2561
+ return { label, value: p5.id };
2562
+ });
2563
+ options.push({
2564
+ label: pc5.blue(`+ Connect another ${providerDisplayName} account`),
2565
+ value: "__connect_new__"
2529
2566
  });
2530
- const { projectChoice } = await prompts10({
2531
- type: "select",
2532
- name: "projectChoice",
2567
+ const projectChoice = await select2({
2533
2568
  message: "Select a project:",
2534
- choices
2569
+ options
2535
2570
  });
2536
2571
  if (!projectChoice) {
2537
- console.log(pc12.gray("Cancelled."));
2572
+ message(dim("Cancelled."));
2538
2573
  process.exit(0);
2539
2574
  }
2540
- return projects.find((p) => p.id === projectChoice);
2575
+ if (projectChoice === "__connect_new__") {
2576
+ return "connect_new";
2577
+ }
2578
+ return projects.find((p5) => p5.id === projectChoice);
2541
2579
  }
2542
2580
  async function syncCommand(provider, options = {}) {
2543
2581
  try {
2544
2582
  if (options.pull && options.allowDelete) {
2545
- console.error(pc12.red("Error: --allow-delete cannot be used with --pull"));
2546
- console.log(pc12.gray("The --allow-delete flag is only for push operations."));
2583
+ error("--allow-delete cannot be used with --pull");
2584
+ message(dim("The --allow-delete flag is only for push operations."));
2547
2585
  process.exit(1);
2548
2586
  }
2549
2587
  const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
2550
2588
  const repoFullName = detectGitRepo();
2551
2589
  if (!repoFullName) {
2552
- console.error(pc12.red("Could not detect Git repository."));
2553
- console.log(pc12.gray("Run this command from a Git repository directory."));
2590
+ error("Could not detect Git repository.");
2591
+ message(dim("Run this command from a Git repository directory."));
2554
2592
  process.exit(1);
2555
2593
  }
2556
- console.log(pc12.gray(`Repository: ${repoFullName}`));
2594
+ step(`Repository: ${value(repoFullName)}`);
2557
2595
  const vaultExists = await checkVaultExists(accessToken, repoFullName);
2558
2596
  if (!vaultExists) {
2559
- console.log(pc12.yellow(`
2560
- No vault found for ${repoFullName}.`));
2561
- const { shouldCreate } = await prompts10({
2562
- type: "confirm",
2563
- name: "shouldCreate",
2597
+ warn(`No vault found for ${repoFullName}.`);
2598
+ const shouldCreate = await confirm2({
2564
2599
  message: "Create vault now?",
2565
- initial: true
2600
+ initialValue: true
2566
2601
  });
2567
2602
  if (!shouldCreate) {
2568
- console.log(pc12.gray("Cancelled. Run `keyway init` to create a vault first."));
2603
+ message(dim("Cancelled. Run `keyway init` to create a vault first."));
2569
2604
  process.exit(0);
2570
2605
  }
2571
- console.log(pc12.gray("\nCreating vault..."));
2606
+ const s = spinner2();
2607
+ s.start("Creating vault...");
2572
2608
  try {
2573
2609
  await initVault(repoFullName, accessToken);
2574
- console.log(pc12.green(`\u2713 Vault created for ${repoFullName}
2575
- `));
2576
- } catch (error) {
2577
- const message = error instanceof Error ? error.message : "Failed to create vault";
2578
- console.error(pc12.red(`
2579
- \u2717 ${message}`));
2610
+ s.stop(`Vault created for ${repoFullName}`);
2611
+ } catch (error2) {
2612
+ s.stop("Failed");
2613
+ const message2 = error2 instanceof Error ? error2.message : "Failed to create vault";
2614
+ error(message2);
2580
2615
  process.exit(1);
2581
2616
  }
2582
2617
  }
2583
2618
  const providerDisplayName = provider.charAt(0).toUpperCase() + provider.slice(1);
2584
2619
  let { projects: allProjects, connections } = await getAllProviderProjects(accessToken, provider.toLowerCase());
2585
2620
  if (connections.length === 0) {
2586
- console.log(pc12.yellow(`
2587
- Not connected to ${providerDisplayName}.`));
2588
- const { shouldConnect } = await prompts10({
2589
- type: "confirm",
2590
- name: "shouldConnect",
2621
+ warn(`Not connected to ${providerDisplayName}.`);
2622
+ const shouldConnect = await confirm2({
2591
2623
  message: `Connect to ${providerDisplayName} now?`,
2592
- initial: true
2624
+ initialValue: true
2593
2625
  });
2594
2626
  if (!shouldConnect) {
2595
- console.log(pc12.gray("Cancelled."));
2627
+ message(dim("Cancelled."));
2596
2628
  process.exit(0);
2597
2629
  }
2598
2630
  await connectCommand(provider, { loginPrompt: false });
@@ -2600,77 +2632,71 @@ Not connected to ${providerDisplayName}.`));
2600
2632
  allProjects = refreshed.projects;
2601
2633
  connections = refreshed.connections;
2602
2634
  if (connections.length === 0) {
2603
- console.error(pc12.red(`
2604
- Connection to ${providerDisplayName} failed.`));
2635
+ error(`Connection to ${providerDisplayName} failed.`);
2605
2636
  process.exit(1);
2606
2637
  }
2607
- console.log("");
2608
- }
2609
- let projects = allProjects.map((p) => ({
2610
- id: p.id,
2611
- name: p.name,
2612
- serviceId: p.serviceId,
2613
- serviceName: p.serviceName,
2614
- linkedRepo: p.linkedRepo,
2615
- environments: p.environments,
2616
- connectionId: p.connectionId,
2617
- teamId: p.teamId,
2618
- teamName: p.teamName
2638
+ }
2639
+ let projects = allProjects.map((p5) => ({
2640
+ id: p5.id,
2641
+ name: p5.name,
2642
+ serviceId: p5.serviceId,
2643
+ serviceName: p5.serviceName,
2644
+ linkedRepo: p5.linkedRepo,
2645
+ environments: p5.environments,
2646
+ connectionId: p5.connectionId,
2647
+ teamId: p5.teamId,
2648
+ teamName: p5.teamName
2619
2649
  }));
2620
2650
  if (options.team) {
2621
2651
  const teamFilter = options.team.toLowerCase();
2622
2652
  const filteredProjects = projects.filter(
2623
- (p) => p.teamId?.toLowerCase() === teamFilter || p.teamName?.toLowerCase() === teamFilter || // Match "personal" for null teamId
2624
- teamFilter === "personal" && !p.teamId
2653
+ (p5) => p5.teamId?.toLowerCase() === teamFilter || p5.teamName?.toLowerCase() === teamFilter || // Match "personal" for null teamId
2654
+ teamFilter === "personal" && !p5.teamId
2625
2655
  );
2626
2656
  if (filteredProjects.length === 0) {
2627
- console.error(pc12.red(`No projects found for team: ${options.team}`));
2628
- console.log(pc12.gray("Available teams:"));
2657
+ error(`No projects found for team: ${options.team}`);
2658
+ message(dim("Available teams:"));
2629
2659
  const teams = /* @__PURE__ */ new Set();
2630
- projects.forEach((p) => {
2631
- if (p.teamName) teams.add(p.teamName);
2632
- else if (p.teamId) teams.add(p.teamId);
2660
+ projects.forEach((p5) => {
2661
+ if (p5.teamName) teams.add(p5.teamName);
2662
+ else if (p5.teamId) teams.add(p5.teamId);
2633
2663
  else teams.add("personal");
2634
2664
  });
2635
- teams.forEach((t) => console.log(pc12.gray(` - ${t}`)));
2665
+ teams.forEach((t) => message(dim(` - ${t}`)));
2636
2666
  process.exit(1);
2637
2667
  }
2638
2668
  projects = filteredProjects;
2639
- console.log(pc12.gray(`Filtered to ${projects.length} projects in team: ${options.team}`));
2669
+ message(dim(`Filtered to ${projects.length} projects in team: ${options.team}`));
2640
2670
  }
2641
2671
  if (projects.length === 0) {
2642
- console.error(pc12.red(`No projects found in your ${providerDisplayName} account(s).`));
2672
+ error(`No projects found in your ${providerDisplayName} account(s).`);
2643
2673
  if (connections.length > 1) {
2644
- console.log(pc12.gray(`Checked ${connections.length} connected accounts.`));
2674
+ message(dim(`Checked ${connections.length} connected accounts.`));
2645
2675
  }
2646
2676
  process.exit(1);
2647
2677
  }
2648
2678
  if (connections.length > 1 && !options.team) {
2649
- console.log(pc12.gray(`Searching ${projects.length} projects across ${connections.length} ${providerDisplayName} accounts...`));
2679
+ message(dim(`Searching ${projects.length} projects across ${connections.length} ${providerDisplayName} accounts...`));
2650
2680
  }
2651
2681
  let selectedProject;
2652
2682
  if (options.project) {
2653
2683
  const found = projects.find(
2654
- (p) => p.id === options.project || p.name.toLowerCase() === options.project?.toLowerCase() || p.serviceName?.toLowerCase() === options.project?.toLowerCase()
2684
+ (p5) => p5.id === options.project || p5.name.toLowerCase() === options.project?.toLowerCase() || p5.serviceName?.toLowerCase() === options.project?.toLowerCase()
2655
2685
  );
2656
2686
  if (!found) {
2657
- console.error(pc12.red(`Project not found: ${options.project}`));
2658
- console.log(pc12.gray("Available projects:"));
2659
- projects.forEach((p) => console.log(pc12.gray(` - ${getProjectDisplayName(p)}`)));
2687
+ error(`Project not found: ${options.project}`);
2688
+ message(dim("Available projects:"));
2689
+ projects.forEach((p5) => message(dim(` - ${getProjectDisplayName(p5)}`)));
2660
2690
  process.exit(1);
2661
2691
  }
2662
2692
  selectedProject = found;
2663
2693
  if (!projectMatchesRepo(selectedProject, repoFullName)) {
2664
- console.log("");
2665
- console.log(pc12.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"));
2666
- console.log(pc12.yellow("\u2502 \u26A0\uFE0F WARNING: Project does not match current repository \u2502"));
2667
- console.log(pc12.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"));
2668
- console.log(pc12.yellow(` Current repo: ${repoFullName}`));
2669
- console.log(pc12.yellow(` Selected project: ${getProjectDisplayName(selectedProject)}`));
2694
+ warn("Project does not match current repository");
2695
+ message(pc5.yellow(`Current repo: ${repoFullName}`));
2696
+ message(pc5.yellow(`Selected project: ${getProjectDisplayName(selectedProject)}`));
2670
2697
  if (selectedProject.linkedRepo) {
2671
- console.log(pc12.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
2698
+ message(pc5.yellow(`Project linked to: ${selectedProject.linkedRepo}`));
2672
2699
  }
2673
- console.log("");
2674
2700
  }
2675
2701
  } else {
2676
2702
  const autoMatch = findMatchingProject(projects, repoFullName);
@@ -2679,78 +2705,67 @@ Connection to ${providerDisplayName} failed.`));
2679
2705
  const matchReason = autoMatch.matchType === "linked_repo" ? `linked to ${repoFullName}` : "exact name match";
2680
2706
  let teamInfo = "";
2681
2707
  if (selectedProject.teamName) {
2682
- teamInfo = pc12.gray(` (${selectedProject.teamName})`);
2708
+ teamInfo = dim(` (${selectedProject.teamName})`);
2683
2709
  } else if (selectedProject.teamId && connections.length > 1) {
2684
2710
  const shortTeamId = selectedProject.teamId.length > 12 ? selectedProject.teamId.slice(0, 12) + "..." : selectedProject.teamId;
2685
- teamInfo = pc12.gray(` (team:${shortTeamId})`);
2711
+ teamInfo = dim(` (team:${shortTeamId})`);
2686
2712
  }
2687
- console.log(pc12.green(`\u2713 Auto-selected project: ${getProjectDisplayName(selectedProject)}${teamInfo} (${matchReason})`));
2713
+ success(`Auto-selected project: ${getProjectDisplayName(selectedProject)}${teamInfo} (${matchReason})`);
2688
2714
  } else if (autoMatch && autoMatch.matchType === "partial_name") {
2689
2715
  const partialDisplayName = getProjectDisplayName(autoMatch.project);
2690
- console.log(pc12.yellow(`Detected project: ${partialDisplayName} (partial match)`));
2691
- const { useDetected } = await prompts10({
2692
- type: "confirm",
2693
- name: "useDetected",
2716
+ info(`Detected project: ${partialDisplayName} (partial match)`);
2717
+ const useDetected = await confirm2({
2694
2718
  message: `Use ${partialDisplayName}?`,
2695
- initial: true
2719
+ initialValue: true
2696
2720
  });
2697
2721
  if (useDetected) {
2698
2722
  selectedProject = autoMatch.project;
2699
2723
  } else {
2700
- selectedProject = await promptProjectSelection(projects, repoFullName);
2724
+ const result = await selectProjectWithConnectOption(accessToken, provider, providerDisplayName, repoFullName, projects);
2725
+ selectedProject = result.project;
2726
+ projects = result.projects;
2701
2727
  }
2702
2728
  } else if (projects.length === 1) {
2703
2729
  selectedProject = projects[0];
2704
2730
  if (!projectMatchesRepo(selectedProject, repoFullName)) {
2705
- console.log("");
2706
- console.log(pc12.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"));
2707
- console.log(pc12.yellow("\u2502 \u26A0\uFE0F WARNING: Project does not match current repository \u2502"));
2708
- console.log(pc12.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"));
2709
- console.log(pc12.yellow(` Current repo: ${repoFullName}`));
2710
- console.log(pc12.yellow(` Only project: ${getProjectDisplayName(selectedProject)}`));
2731
+ warn("Project does not match current repository");
2732
+ message(pc5.yellow(`Current repo: ${repoFullName}`));
2733
+ message(pc5.yellow(`Only project: ${getProjectDisplayName(selectedProject)}`));
2711
2734
  if (selectedProject.linkedRepo) {
2712
- console.log(pc12.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
2735
+ message(pc5.yellow(`Project linked to: ${selectedProject.linkedRepo}`));
2713
2736
  }
2714
- console.log("");
2715
- const { continueAnyway } = await prompts10({
2716
- type: "confirm",
2717
- name: "continueAnyway",
2737
+ const continueAnyway = await confirm2({
2718
2738
  message: "Continue anyway?",
2719
- initial: false
2739
+ initialValue: false
2720
2740
  });
2721
2741
  if (!continueAnyway) {
2722
- console.log(pc12.gray("Cancelled."));
2742
+ message(dim("Cancelled."));
2723
2743
  process.exit(0);
2724
2744
  }
2725
2745
  }
2726
2746
  } else {
2727
- console.log(pc12.yellow(`
2728
- \u26A0\uFE0F No matching project found for ${repoFullName}`));
2729
- console.log(pc12.gray("Select a project manually:\n"));
2730
- selectedProject = await promptProjectSelection(projects, repoFullName);
2747
+ warn(`No matching project found for ${repoFullName}`);
2748
+ message(dim("Select a project manually:"));
2749
+ const result = await selectProjectWithConnectOption(accessToken, provider, providerDisplayName, repoFullName, projects);
2750
+ selectedProject = result.project;
2751
+ projects = result.projects;
2731
2752
  }
2732
2753
  }
2733
2754
  if (!options.project && !projectMatchesRepo(selectedProject, repoFullName)) {
2734
2755
  const autoMatch = findMatchingProject(projects, repoFullName);
2735
2756
  if (autoMatch && autoMatch.project.id !== selectedProject.id) {
2736
- console.log("");
2737
- console.log(pc12.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"));
2738
- console.log(pc12.yellow("\u2502 \u26A0\uFE0F WARNING: You selected a different project \u2502"));
2739
- console.log(pc12.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"));
2740
- console.log(pc12.yellow(` Current repo: ${repoFullName}`));
2741
- console.log(pc12.yellow(` Selected project: ${getProjectDisplayName(selectedProject)}`));
2757
+ warn("You selected a different project");
2758
+ message(pc5.yellow(`Current repo: ${repoFullName}`));
2759
+ message(pc5.yellow(`Selected project: ${getProjectDisplayName(selectedProject)}`));
2742
2760
  if (selectedProject.linkedRepo) {
2743
- console.log(pc12.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
2761
+ message(pc5.yellow(`Project linked to: ${selectedProject.linkedRepo}`));
2744
2762
  }
2745
- console.log("");
2746
- const { continueAnyway } = await prompts10({
2747
- type: "confirm",
2748
- name: "continueAnyway",
2763
+ const continueAnyway = await confirm2({
2749
2764
  message: "Are you sure you want to sync with this project?",
2750
- initial: false
2765
+ initialValue: false
2751
2766
  });
2752
2767
  if (!continueAnyway) {
2753
- console.log(pc12.gray("Cancelled."));
2768
+ message(dim("Cancelled."));
2754
2769
  process.exit(0);
2755
2770
  }
2756
2771
  }
@@ -2764,15 +2779,13 @@ Connection to ${providerDisplayName} failed.`));
2764
2779
  if (needsEnvPrompt || needsDirectionPrompt) {
2765
2780
  if (needsEnvPrompt) {
2766
2781
  const vaultEnvs = await getVaultEnvironments(accessToken, repoFullName);
2767
- const { selectedEnv } = await prompts10({
2768
- type: "select",
2769
- name: "selectedEnv",
2782
+ const selectedEnv = await select2({
2770
2783
  message: "Keyway environment:",
2771
- choices: vaultEnvs.map((e) => ({ title: e, value: e })),
2772
- initial: Math.max(0, vaultEnvs.indexOf("production"))
2784
+ options: vaultEnvs.map((e) => ({ label: e, value: e })),
2785
+ initialValue: vaultEnvs.includes("production") ? "production" : vaultEnvs[0]
2773
2786
  });
2774
2787
  if (!selectedEnv) {
2775
- console.log(pc12.gray("Cancelled."));
2788
+ message(dim("Cancelled."));
2776
2789
  process.exit(0);
2777
2790
  }
2778
2791
  keywayEnv = selectedEnv;
@@ -2786,19 +2799,15 @@ Connection to ${providerDisplayName} failed.`));
2786
2799
  providerEnv = mappedEnv;
2787
2800
  } else if (selectedProject.environments.length === 1) {
2788
2801
  providerEnv = selectedProject.environments[0];
2789
- console.log(pc12.gray(`Using ${providerName} environment: ${providerEnv}`));
2802
+ message(dim(`Using ${providerName} environment: ${providerEnv}`));
2790
2803
  } else {
2791
- const { selectedProviderEnv } = await prompts10({
2792
- type: "select",
2793
- name: "selectedProviderEnv",
2804
+ const selectedProviderEnv = await select2({
2794
2805
  message: `${providerName} environment:`,
2795
- choices: selectedProject.environments.map((e) => ({ title: e, value: e })),
2796
- initial: Math.max(0, selectedProject.environments.findIndex(
2797
- (e) => e.toLowerCase() === "production"
2798
- ))
2806
+ options: selectedProject.environments.map((e) => ({ label: e, value: e })),
2807
+ initialValue: selectedProject.environments.includes("production") ? "production" : selectedProject.environments[0]
2799
2808
  });
2800
2809
  if (!selectedProviderEnv) {
2801
- console.log(pc12.gray("Cancelled."));
2810
+ message(dim("Cancelled."));
2802
2811
  process.exit(0);
2803
2812
  }
2804
2813
  providerEnv = selectedProviderEnv;
@@ -2812,7 +2821,8 @@ Connection to ${providerDisplayName} failed.`));
2812
2821
  if (needsDirectionPrompt) {
2813
2822
  const effectiveKeywayEnv = keywayEnv || "production";
2814
2823
  const effectiveProviderEnv = providerEnv || mapToProviderEnvironment(provider, effectiveKeywayEnv);
2815
- console.log(pc12.gray("\nComparing secrets..."));
2824
+ const s = spinner2();
2825
+ s.start("Comparing secrets...");
2816
2826
  diff = await getSyncDiff(accessToken, repoFullName, {
2817
2827
  connectionId: selectedProject.connectionId,
2818
2828
  projectId: selectedProject.id,
@@ -2821,6 +2831,7 @@ Connection to ${providerDisplayName} failed.`));
2821
2831
  keywayEnvironment: effectiveKeywayEnv,
2822
2832
  providerEnvironment: effectiveProviderEnv
2823
2833
  });
2834
+ s.stop("Compared");
2824
2835
  displayDiffSummary(diff, providerName);
2825
2836
  const totalDiff = diff.onlyInKeyway.length + diff.onlyInProvider.length + diff.different.length;
2826
2837
  if (totalDiff === 0) {
@@ -2828,24 +2839,17 @@ Connection to ${providerDisplayName} failed.`));
2828
2839
  }
2829
2840
  }
2830
2841
  if (needsDirectionPrompt && diff) {
2831
- let defaultDirection = 0;
2832
- if (diff.keywayCount === 0 && diff.providerCount > 0) {
2833
- defaultDirection = 1;
2834
- } else if (diff.providerCount === 0 && diff.keywayCount > 0) {
2835
- defaultDirection = 0;
2836
- }
2837
- const { selectedDirection } = await prompts10({
2838
- type: "select",
2839
- name: "selectedDirection",
2842
+ const defaultDirection = diff.keywayCount === 0 && diff.providerCount > 0 ? "pull" : "push";
2843
+ const selectedDirection = await select2({
2840
2844
  message: "Sync direction:",
2841
- choices: [
2842
- { title: `Keyway \u2192 ${providerName}`, value: "push" },
2843
- { title: `${providerName} \u2192 Keyway`, value: "pull" }
2845
+ options: [
2846
+ { label: `Keyway \u2192 ${providerName}`, value: "push" },
2847
+ { label: `${providerName} \u2192 Keyway`, value: "pull" }
2844
2848
  ],
2845
- initial: defaultDirection
2849
+ initialValue: defaultDirection
2846
2850
  });
2847
2851
  if (!selectedDirection) {
2848
- console.log(pc12.gray("Cancelled."));
2852
+ message(dim("Cancelled."));
2849
2853
  process.exit(0);
2850
2854
  }
2851
2855
  direction = selectedDirection;
@@ -2862,14 +2866,11 @@ Connection to ${providerDisplayName} failed.`));
2862
2866
  keywayEnv
2863
2867
  );
2864
2868
  if (status.isFirstSync && direction === "push" && status.vaultIsEmpty && status.providerHasSecrets) {
2865
- console.log(pc12.yellow(`
2866
- \u26A0\uFE0F Your Keyway vault is empty for "${keywayEnv}", but ${providerName} has ${status.providerSecretCount} secrets.`));
2867
- console.log(pc12.gray(` (Use --environment to sync a different environment)`));
2868
- const { importFirst } = await prompts10({
2869
- type: "confirm",
2870
- name: "importFirst",
2869
+ warn(`Your Keyway vault is empty for "${keywayEnv}", but ${providerName} has ${status.providerSecretCount} secrets.`);
2870
+ message(dim("(Use --environment to sync a different environment)"));
2871
+ const importFirst = await confirm2({
2871
2872
  message: `Import secrets from ${providerName} first?`,
2872
- initial: true
2873
+ initialValue: true
2873
2874
  });
2874
2875
  if (importFirst) {
2875
2876
  await executeSyncOperation(
@@ -2900,14 +2901,13 @@ Connection to ${providerDisplayName} failed.`));
2900
2901
  options.yes || false,
2901
2902
  provider
2902
2903
  );
2903
- } catch (error) {
2904
- const message = error instanceof Error ? error.message : "Sync failed";
2904
+ } catch (error2) {
2905
+ const message2 = error2 instanceof Error ? error2.message : "Sync failed";
2905
2906
  trackEvent(AnalyticsEvents.CLI_ERROR, {
2906
2907
  command: "sync",
2907
- error: truncateMessage(message)
2908
+ error: truncateMessage(message2)
2908
2909
  });
2909
- console.error(pc12.red(`
2910
- \u2717 ${message}`));
2910
+ error(message2);
2911
2911
  process.exit(1);
2912
2912
  }
2913
2913
  }
@@ -2925,49 +2925,47 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
2925
2925
  });
2926
2926
  const totalChanges = preview.toCreate.length + preview.toUpdate.length + preview.toDelete.length;
2927
2927
  if (totalChanges === 0) {
2928
- console.log(pc12.green("\n\u2713 Already in sync. No changes needed."));
2928
+ success("Already in sync. No changes needed.");
2929
2929
  return;
2930
2930
  }
2931
- console.log(pc12.blue("\n\u{1F4CB} Sync Preview\n"));
2931
+ step("Sync Preview");
2932
2932
  if (preview.toCreate.length > 0) {
2933
- console.log(pc12.green(` + ${preview.toCreate.length} to create`));
2934
- preview.toCreate.slice(0, 5).forEach((key) => console.log(pc12.gray(` ${key}`)));
2933
+ message(pc5.green(`+ ${preview.toCreate.length} to create`));
2934
+ preview.toCreate.slice(0, 5).forEach((key) => message(dim(` ${key}`)));
2935
2935
  if (preview.toCreate.length > 5) {
2936
- console.log(pc12.gray(` ... and ${preview.toCreate.length - 5} more`));
2936
+ message(dim(` ... and ${preview.toCreate.length - 5} more`));
2937
2937
  }
2938
2938
  }
2939
2939
  if (preview.toUpdate.length > 0) {
2940
- console.log(pc12.yellow(` ~ ${preview.toUpdate.length} to update`));
2941
- preview.toUpdate.slice(0, 5).forEach((key) => console.log(pc12.gray(` ${key}`)));
2940
+ message(pc5.yellow(`~ ${preview.toUpdate.length} to update`));
2941
+ preview.toUpdate.slice(0, 5).forEach((key) => message(dim(` ${key}`)));
2942
2942
  if (preview.toUpdate.length > 5) {
2943
- console.log(pc12.gray(` ... and ${preview.toUpdate.length - 5} more`));
2943
+ message(dim(` ... and ${preview.toUpdate.length - 5} more`));
2944
2944
  }
2945
2945
  }
2946
2946
  if (preview.toDelete.length > 0) {
2947
- console.log(pc12.red(` - ${preview.toDelete.length} to delete`));
2948
- preview.toDelete.slice(0, 5).forEach((key) => console.log(pc12.gray(` ${key}`)));
2947
+ message(pc5.red(`- ${preview.toDelete.length} to delete`));
2948
+ preview.toDelete.slice(0, 5).forEach((key) => message(dim(` ${key}`)));
2949
2949
  if (preview.toDelete.length > 5) {
2950
- console.log(pc12.gray(` ... and ${preview.toDelete.length - 5} more`));
2950
+ message(dim(` ... and ${preview.toDelete.length - 5} more`));
2951
2951
  }
2952
2952
  }
2953
2953
  if (preview.toSkip.length > 0) {
2954
- console.log(pc12.gray(` \u25CB ${preview.toSkip.length} unchanged`));
2954
+ message(dim(`\u25CB ${preview.toSkip.length} unchanged`));
2955
2955
  }
2956
- console.log("");
2957
2956
  if (!skipConfirm) {
2958
2957
  const target = direction === "push" ? providerName : "Keyway";
2959
- const { confirm } = await prompts10({
2960
- type: "confirm",
2961
- name: "confirm",
2958
+ const confirm3 = await confirm2({
2962
2959
  message: `Apply ${totalChanges} changes to ${target}?`,
2963
- initial: true
2960
+ initialValue: true
2964
2961
  });
2965
- if (!confirm) {
2966
- console.log(pc12.gray("Cancelled."));
2962
+ if (!confirm3) {
2963
+ message(dim("Cancelled."));
2967
2964
  return;
2968
2965
  }
2969
2966
  }
2970
- console.log(pc12.blue("\n\u23F3 Syncing...\n"));
2967
+ const s = spinner2();
2968
+ s.start("Syncing...");
2971
2969
  const result = await executeSync(accessToken, repoFullName, {
2972
2970
  connectionId,
2973
2971
  projectId: project.id,
@@ -2979,11 +2977,11 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
2979
2977
  allowDelete
2980
2978
  });
2981
2979
  if (result.success) {
2982
- console.log(pc12.green("\u2713 Sync complete"));
2983
- console.log(pc12.gray(` Created: ${result.stats.created}`));
2984
- console.log(pc12.gray(` Updated: ${result.stats.updated}`));
2980
+ s.stop("Sync complete");
2981
+ message(dim(`Created: ${result.stats.created}`));
2982
+ message(dim(`Updated: ${result.stats.updated}`));
2985
2983
  if (result.stats.deleted > 0) {
2986
- console.log(pc12.gray(` Deleted: ${result.stats.deleted}`));
2984
+ message(dim(`Deleted: ${result.stats.deleted}`));
2987
2985
  }
2988
2986
  trackEvent(AnalyticsEvents.CLI_SYNC, {
2989
2987
  provider,
@@ -2993,27 +2991,19 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
2993
2991
  deleted: result.stats.deleted
2994
2992
  });
2995
2993
  } else {
2996
- console.error(pc12.red(`
2997
- \u2717 ${result.error}`));
2994
+ s.stop("Failed");
2995
+ error(result.error || "Sync failed");
2998
2996
  process.exit(1);
2999
2997
  }
3000
2998
  }
3001
2999
 
3002
3000
  // src/cli.ts
3003
3001
  process.on("unhandledRejection", (reason) => {
3004
- console.error(pc13.red("Unhandled error:"), reason);
3002
+ error(`Unhandled error: ${reason}`);
3005
3003
  process.exit(1);
3006
3004
  });
3007
3005
  var program = new Command();
3008
3006
  var TAGLINE = "Sync secrets with your team and infra";
3009
- var showBanner = () => {
3010
- const text = pc13.bold(pc13.cyan("Keyway CLI"));
3011
- console.log(`
3012
- ${text}
3013
- ${pc13.gray(TAGLINE)}
3014
- `);
3015
- };
3016
- showBanner();
3017
3007
  program.name("keyway").description(TAGLINE).version(package_default.version);
3018
3008
  program.command("init").description("Initialize a vault for the current repository").option("--no-login-prompt", "Fail instead of prompting to login if unauthenticated").action(async (options) => {
3019
3009
  await initCommand(options);
@@ -3050,9 +3040,8 @@ ci.command("setup").description("Setup GitHub Actions integration (adds KEYWAY_T
3050
3040
  await ciSetupCommand(options);
3051
3041
  });
3052
3042
  (async () => {
3053
- await warnIfEnvNotGitignored();
3054
3043
  await program.parseAsync();
3055
- })().catch((error) => {
3056
- console.error(pc13.red("Error:"), error.message || error);
3044
+ })().catch((error2) => {
3045
+ error(error2.message || error2);
3057
3046
  process.exit(1);
3058
3047
  });