@keywaysh/cli 0.0.13 → 0.0.15

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