@keywaysh/cli 0.0.21 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +376 -171
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import pc9 from "picocolors";
7
7
  // src/cmds/init.ts
8
8
  import pc4 from "picocolors";
9
9
  import prompts4 from "prompts";
10
+ import open2 from "open";
10
11
 
11
12
  // src/utils/git.ts
12
13
  import { execSync } from "child_process";
@@ -69,7 +70,7 @@ var INTERNAL_POSTHOG_HOST = "https://eu.i.posthog.com";
69
70
  // package.json
70
71
  var package_default = {
71
72
  name: "@keywaysh/cli",
72
- version: "0.0.21",
73
+ version: "0.1.0",
73
74
  description: "One link to all your secrets",
74
75
  type: "module",
75
76
  bin: {
@@ -338,7 +339,8 @@ async function validateToken(token) {
338
339
  },
339
340
  body: JSON.stringify({})
340
341
  });
341
- return handleResponse(response);
342
+ const wrapped = await handleResponse(response);
343
+ return wrapped.data;
342
344
  }
343
345
  async function getProviders() {
344
346
  const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations`, {
@@ -347,7 +349,8 @@ async function getProviders() {
347
349
  "User-Agent": USER_AGENT
348
350
  }
349
351
  });
350
- return handleResponse(response);
352
+ const wrapped = await handleResponse(response);
353
+ return wrapped.data;
351
354
  }
352
355
  async function getConnections(accessToken) {
353
356
  const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations/connections`, {
@@ -357,7 +360,8 @@ async function getConnections(accessToken) {
357
360
  Authorization: `Bearer ${accessToken}`
358
361
  }
359
362
  });
360
- return handleResponse(response);
363
+ const wrapped = await handleResponse(response);
364
+ return wrapped.data;
361
365
  }
362
366
  async function deleteConnection(accessToken, connectionId) {
363
367
  const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations/connections/${connectionId}`, {
@@ -367,7 +371,7 @@ async function deleteConnection(accessToken, connectionId) {
367
371
  Authorization: `Bearer ${accessToken}`
368
372
  }
369
373
  });
370
- return handleResponse(response);
374
+ await handleResponse(response);
371
375
  }
372
376
  function getProviderAuthUrl(provider, redirectUri) {
373
377
  const params = redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : "";
@@ -381,7 +385,8 @@ async function getConnectionProjects(accessToken, connectionId) {
381
385
  Authorization: `Bearer ${accessToken}`
382
386
  }
383
387
  });
384
- return handleResponse(response);
388
+ const wrapped = await handleResponse(response);
389
+ return wrapped.data;
385
390
  }
386
391
  async function getSyncStatus(accessToken, repoFullName, connectionId, projectId, environment = "production") {
387
392
  const [owner, repo] = repoFullName.split("/");
@@ -400,7 +405,8 @@ async function getSyncStatus(accessToken, repoFullName, connectionId, projectId,
400
405
  }
401
406
  }
402
407
  );
403
- return handleResponse(response);
408
+ const wrapped = await handleResponse(response);
409
+ return wrapped.data;
404
410
  }
405
411
  async function getSyncPreview(accessToken, repoFullName, options) {
406
412
  const [owner, repo] = repoFullName.split("/");
@@ -424,7 +430,8 @@ async function getSyncPreview(accessToken, repoFullName, options) {
424
430
  6e4
425
431
  // 60 seconds for sync operations
426
432
  );
427
- return handleResponse(response);
433
+ const wrapped = await handleResponse(response);
434
+ return wrapped.data;
428
435
  }
429
436
  async function executeSync(accessToken, repoFullName, options) {
430
437
  const [owner, repo] = repoFullName.split("/");
@@ -449,7 +456,21 @@ async function executeSync(accessToken, repoFullName, options) {
449
456
  12e4
450
457
  // 2 minutes for sync execution
451
458
  );
452
- return handleResponse(response);
459
+ const wrapped = await handleResponse(response);
460
+ return wrapped.data;
461
+ }
462
+ async function checkGitHubAppInstallation(repoOwner, repoName, accessToken) {
463
+ const response = await fetchWithTimeout(`${API_BASE_URL}/v1/github/check-installation`, {
464
+ method: "POST",
465
+ headers: {
466
+ "Content-Type": "application/json",
467
+ "User-Agent": USER_AGENT,
468
+ Authorization: `Bearer ${accessToken}`
469
+ },
470
+ body: JSON.stringify({ repoOwner, repoName })
471
+ });
472
+ const wrapped = await handleResponse(response);
473
+ return wrapped.data;
453
474
  }
454
475
 
455
476
  // src/utils/analytics.ts
@@ -541,6 +562,30 @@ async function shutdownAnalytics() {
541
562
  await posthog.shutdown();
542
563
  }
543
564
  }
565
+ function identifyUser(userId, properties) {
566
+ try {
567
+ if (TELEMETRY_DISABLED) return;
568
+ if (!posthog) initPostHog();
569
+ if (!posthog) return;
570
+ const sanitizedProperties = properties ? sanitizeProperties(properties) : {};
571
+ posthog.identify({
572
+ distinctId: userId,
573
+ properties: {
574
+ ...sanitizedProperties,
575
+ source: "cli"
576
+ }
577
+ });
578
+ const anonId = getDistinctId();
579
+ if (anonId && anonId !== userId) {
580
+ posthog.alias({
581
+ distinctId: userId,
582
+ alias: anonId
583
+ });
584
+ }
585
+ } catch (error) {
586
+ console.debug("Analytics identify error:", error);
587
+ }
588
+ }
544
589
  var AnalyticsEvents = {
545
590
  CLI_INIT: "cli_init",
546
591
  CLI_PUSH: "cli_push",
@@ -554,45 +599,151 @@ var AnalyticsEvents = {
554
599
  CLI_FEEDBACK: "cli_feedback"
555
600
  };
556
601
 
557
- // src/cmds/login.ts
602
+ // src/cmds/readme.ts
603
+ import fs2 from "fs";
604
+ import path2 from "path";
605
+ import prompts from "prompts";
558
606
  import pc from "picocolors";
607
+ function generateBadge(repo) {
608
+ return `[![Keyway Secrets](https://www.keyway.sh/badge.svg?repo=${repo})](https://www.keyway.sh/vaults/${repo})`;
609
+ }
610
+ function insertBadgeIntoReadme(readmeContent, badge) {
611
+ if (readmeContent.includes("keyway.sh/badge.svg")) {
612
+ return readmeContent;
613
+ }
614
+ const lines = readmeContent.split(/\r?\n/);
615
+ const titleIndex = lines.findIndex((line) => /^#(?!#)\s+/.test(line.trim()));
616
+ if (titleIndex !== -1) {
617
+ const before = lines.slice(0, titleIndex + 1);
618
+ const after = lines.slice(titleIndex + 1);
619
+ while (after.length > 0 && after[0].trim() === "") {
620
+ after.shift();
621
+ }
622
+ const newLines = [...before, "", badge, "", ...after];
623
+ return newLines.join("\n");
624
+ }
625
+ return `${badge}
626
+
627
+ ${readmeContent}`;
628
+ }
629
+ function findReadmePath(cwd) {
630
+ const candidates = ["README.md", "readme.md", "Readme.md"];
631
+ for (const candidate of candidates) {
632
+ const candidatePath = path2.join(cwd, candidate);
633
+ if (fs2.existsSync(candidatePath)) {
634
+ return candidatePath;
635
+ }
636
+ }
637
+ return null;
638
+ }
639
+ async function ensureReadme(repoName, cwd) {
640
+ const existing = findReadmePath(cwd);
641
+ if (existing) return existing;
642
+ const isInteractive3 = process.stdin.isTTY && process.stdout.isTTY;
643
+ if (!isInteractive3) {
644
+ console.log(pc.yellow('No README found. Run "keyway readme add-badge" from a repo with a README.'));
645
+ return null;
646
+ }
647
+ const { confirm } = await prompts(
648
+ {
649
+ type: "confirm",
650
+ name: "confirm",
651
+ message: "No README found. Create a default README.md?",
652
+ initial: false
653
+ },
654
+ {
655
+ onCancel: () => ({ confirm: false })
656
+ }
657
+ );
658
+ if (!confirm) {
659
+ console.log(pc.yellow("Skipping badge insertion (no README)."));
660
+ return null;
661
+ }
662
+ const defaultPath = path2.join(cwd, "README.md");
663
+ const content = `# ${repoName}
664
+
665
+ `;
666
+ fs2.writeFileSync(defaultPath, content, "utf-8");
667
+ return defaultPath;
668
+ }
669
+ async function addBadgeToReadme(silent = false) {
670
+ const repo = detectGitRepo();
671
+ if (!repo) {
672
+ throw new Error("This directory is not a Git repository.");
673
+ }
674
+ const cwd = process.cwd();
675
+ const readmePath = await ensureReadme(repo, cwd);
676
+ if (!readmePath) return false;
677
+ const badge = generateBadge(repo);
678
+ const content = fs2.readFileSync(readmePath, "utf-8");
679
+ const updated = insertBadgeIntoReadme(content, badge);
680
+ if (updated === content) {
681
+ if (!silent) {
682
+ console.log(pc.gray("Keyway badge already present in README."));
683
+ }
684
+ return false;
685
+ }
686
+ fs2.writeFileSync(readmePath, updated, "utf-8");
687
+ if (!silent) {
688
+ console.log(pc.green(`\u2713 Keyway badge added to ${path2.basename(readmePath)}`));
689
+ }
690
+ return true;
691
+ }
692
+
693
+ // src/cmds/push.ts
694
+ import pc3 from "picocolors";
695
+ import fs3 from "fs";
696
+ import path3 from "path";
697
+ import prompts3 from "prompts";
698
+
699
+ // src/cmds/login.ts
700
+ import pc2 from "picocolors";
559
701
  import readline from "readline";
560
702
  import open from "open";
561
- import prompts from "prompts";
703
+ import prompts2 from "prompts";
562
704
 
563
705
  // src/utils/auth.ts
564
706
  import Conf from "conf";
565
- import { createCipheriv, createDecipheriv, randomBytes, scrypt } from "crypto";
566
- import { promisify } from "util";
707
+ import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
708
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "fs";
709
+ import { join } from "path";
710
+ import { homedir } from "os";
567
711
  var store = new Conf({
568
712
  projectName: "keyway",
569
713
  configName: "config",
570
714
  fileMode: 384
571
715
  });
572
- var scryptAsync = promisify(scrypt);
573
- async function getEncryptionKey() {
574
- const machineId = process.env.USER || process.env.USERNAME || "keyway-user";
575
- let salt = store.get("salt");
576
- if (!salt) {
577
- salt = randomBytes(16).toString("hex");
578
- store.set("salt", salt);
579
- }
580
- const key = await scryptAsync(machineId, salt, 32);
716
+ var KEY_DIR = join(homedir(), ".keyway");
717
+ var KEY_FILE = join(KEY_DIR, ".key");
718
+ function getOrCreateEncryptionKey() {
719
+ if (!existsSync(KEY_DIR)) {
720
+ mkdirSync(KEY_DIR, { recursive: true, mode: 448 });
721
+ }
722
+ if (existsSync(KEY_FILE)) {
723
+ const keyHex2 = readFileSync(KEY_FILE, "utf-8").trim();
724
+ if (keyHex2.length === 64) {
725
+ return Buffer.from(keyHex2, "hex");
726
+ }
727
+ }
728
+ const key = randomBytes(32);
729
+ const keyHex = key.toString("hex");
730
+ writeFileSync(KEY_FILE, keyHex, { mode: 384 });
731
+ try {
732
+ chmodSync(KEY_FILE, 384);
733
+ } catch {
734
+ }
581
735
  return key;
582
736
  }
583
- async function encryptToken(token) {
584
- const key = await getEncryptionKey();
737
+ function encryptToken(token) {
738
+ const key = getOrCreateEncryptionKey();
585
739
  const iv = randomBytes(16);
586
740
  const cipher = createCipheriv("aes-256-gcm", key, iv);
587
- const encrypted = Buffer.concat([
588
- cipher.update(token, "utf8"),
589
- cipher.final()
590
- ]);
741
+ const encrypted = Buffer.concat([cipher.update(token, "utf8"), cipher.final()]);
591
742
  const authTag = cipher.getAuthTag();
592
743
  return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
593
744
  }
594
- async function decryptToken(encryptedData) {
595
- const key = await getEncryptionKey();
745
+ function decryptToken(encryptedData) {
746
+ const key = getOrCreateEncryptionKey();
596
747
  const parts = encryptedData.split(":");
597
748
  if (parts.length !== 3) {
598
749
  throw new Error("Invalid encrypted token format");
@@ -602,10 +753,7 @@ async function decryptToken(encryptedData) {
602
753
  const encrypted = Buffer.from(parts[2], "hex");
603
754
  const decipher = createDecipheriv("aes-256-gcm", key, iv);
604
755
  decipher.setAuthTag(authTag);
605
- const decrypted = Buffer.concat([
606
- decipher.update(encrypted),
607
- decipher.final()
608
- ]);
756
+ const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
609
757
  return decrypted.toString("utf8");
610
758
  }
611
759
  function isExpired(auth) {
@@ -620,14 +768,14 @@ async function getStoredAuth() {
620
768
  return null;
621
769
  }
622
770
  try {
623
- const decrypted = await decryptToken(encryptedData);
771
+ const decrypted = decryptToken(encryptedData);
624
772
  const auth = JSON.parse(decrypted);
625
773
  if (isExpired(auth)) {
626
774
  clearAuth();
627
775
  return null;
628
776
  }
629
777
  return auth;
630
- } catch (error) {
778
+ } catch {
631
779
  console.error("Failed to decrypt stored auth, clearing...");
632
780
  clearAuth();
633
781
  return null;
@@ -640,7 +788,7 @@ async function saveAuthToken(token, meta) {
640
788
  expiresAt: meta?.expiresAt,
641
789
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
642
790
  };
643
- const encrypted = await encryptToken(JSON.stringify(auth));
791
+ const encrypted = encryptToken(JSON.stringify(auth));
644
792
  store.set("auth", encrypted);
645
793
  }
646
794
  function clearAuth() {
@@ -674,17 +822,17 @@ async function promptYesNo(question, defaultYes = true) {
674
822
  });
675
823
  }
676
824
  async function runLoginFlow() {
677
- console.log(pc.blue("\u{1F510} Starting Keyway login...\n"));
825
+ console.log(pc2.blue("\u{1F510} Starting Keyway login...\n"));
678
826
  const repoName = detectGitRepo();
679
827
  const start = await startDeviceLogin(repoName);
680
828
  const verifyUrl = start.verificationUriComplete || start.verificationUri;
681
829
  if (!verifyUrl) {
682
830
  throw new Error("Missing verification URL from the auth server.");
683
831
  }
684
- console.log(`Code: ${pc.bold(pc.green(start.userCode))}`);
832
+ console.log(`Code: ${pc2.bold(pc2.green(start.userCode))}`);
685
833
  console.log("Waiting for auth...");
686
834
  open(verifyUrl).catch(() => {
687
- console.log(pc.gray(`Open this URL in your browser: ${verifyUrl}`));
835
+ console.log(pc2.gray(`Open this URL in your browser: ${verifyUrl}`));
688
836
  });
689
837
  const pollIntervalMs = (start.interval ?? 5) * 1e3;
690
838
  const maxTimeoutMs = Math.min((start.expiresIn ?? 900) * 1e3, 30 * 60 * 1e3);
@@ -707,9 +855,15 @@ async function runLoginFlow() {
707
855
  method: "device",
708
856
  repo: repoName
709
857
  });
710
- console.log(pc.green("\n\u2713 Login successful"));
711
858
  if (result.githubLogin) {
712
- console.log(`Authenticated GitHub user: ${pc.cyan(result.githubLogin)}`);
859
+ identifyUser(result.githubLogin, {
860
+ github_username: result.githubLogin,
861
+ login_method: "device"
862
+ });
863
+ }
864
+ console.log(pc2.green("\n\u2713 Login successful"));
865
+ if (result.githubLogin) {
866
+ console.log(`Authenticated GitHub user: ${pc2.cyan(result.githubLogin)}`);
713
867
  }
714
868
  return result.keywayToken;
715
869
  }
@@ -722,7 +876,7 @@ async function ensureLogin(options = {}) {
722
876
  return envToken;
723
877
  }
724
878
  if (process.env.GITHUB_TOKEN && !process.env.KEYWAY_TOKEN) {
725
- console.warn(pc.yellow("Note: GITHUB_TOKEN found but not used. Set KEYWAY_TOKEN for Keyway authentication."));
879
+ console.warn(pc2.yellow("Note: GITHUB_TOKEN found but not used. Set KEYWAY_TOKEN for Keyway authentication."));
726
880
  }
727
881
  const stored = await getStoredAuth();
728
882
  if (stored?.keywayToken) {
@@ -742,17 +896,17 @@ async function ensureLogin(options = {}) {
742
896
  async function runTokenLogin() {
743
897
  const repoName = detectGitRepo();
744
898
  if (repoName) {
745
- console.log(`\u{1F4C1} Detected: ${pc.cyan(repoName)}`);
899
+ console.log(`\u{1F4C1} Detected: ${pc2.cyan(repoName)}`);
746
900
  }
747
901
  const description = repoName ? `Keyway CLI for ${repoName}` : "Keyway CLI";
748
902
  const url = `https://github.com/settings/personal-access-tokens/new?description=${encodeURIComponent(description)}`;
749
903
  console.log("Opening GitHub...");
750
904
  open(url).catch(() => {
751
- console.log(pc.gray(`Open this URL in your browser: ${url}`));
905
+ console.log(pc2.gray(`Open this URL in your browser: ${url}`));
752
906
  });
753
- console.log(pc.gray("Select the detected repo (or scope manually)."));
754
- console.log(pc.gray("Permissions: Metadata \u2192 Read-only; Account permissions: None."));
755
- const { token } = await prompts(
907
+ console.log(pc2.gray("Select the detected repo (or scope manually)."));
908
+ console.log(pc2.gray("Permissions: Metadata \u2192 Read-only; Account permissions: None."));
909
+ const { token } = await prompts2(
756
910
  {
757
911
  type: "password",
758
912
  name: "token",
@@ -784,7 +938,11 @@ async function runTokenLogin() {
784
938
  method: "pat",
785
939
  repo: repoName
786
940
  });
787
- console.log(pc.green("\u2705 Authenticated"), `as ${pc.cyan(`@${validation.username}`)}`);
941
+ identifyUser(validation.username, {
942
+ github_username: validation.username,
943
+ login_method: "pat"
944
+ });
945
+ console.log(pc2.green("\u2705 Authenticated"), `as ${pc2.cyan(`@${validation.username}`)}`);
788
946
  return trimmedToken;
789
947
  }
790
948
  async function loginCommand(options = {}) {
@@ -800,113 +958,18 @@ async function loginCommand(options = {}) {
800
958
  command: "login",
801
959
  error: truncateMessage(message)
802
960
  });
803
- console.error(pc.red(`
961
+ console.error(pc2.red(`
804
962
  \u2717 ${message}`));
805
963
  process.exit(1);
806
964
  }
807
965
  }
808
966
  async function logoutCommand() {
809
967
  clearAuth();
810
- console.log(pc.green("\u2713 Logged out of Keyway"));
811
- console.log(pc.gray(`Auth cache cleared: ${getAuthFilePath()}`));
812
- }
813
-
814
- // src/cmds/readme.ts
815
- import fs2 from "fs";
816
- import path2 from "path";
817
- import prompts2 from "prompts";
818
- import pc2 from "picocolors";
819
- function generateBadge(repo) {
820
- return `[![Keyway Secrets](https://www.keyway.sh/badge.svg?repo=${repo})](https://www.keyway.sh/vaults/${repo})`;
821
- }
822
- function insertBadgeIntoReadme(readmeContent, badge) {
823
- if (readmeContent.includes("keyway.sh/badge.svg")) {
824
- return readmeContent;
825
- }
826
- const lines = readmeContent.split(/\r?\n/);
827
- const titleIndex = lines.findIndex((line) => /^#(?!#)\s+/.test(line.trim()));
828
- if (titleIndex !== -1) {
829
- const before = lines.slice(0, titleIndex + 1);
830
- const after = lines.slice(titleIndex + 1);
831
- while (after.length > 0 && after[0].trim() === "") {
832
- after.shift();
833
- }
834
- const newLines = [...before, "", badge, "", ...after];
835
- return newLines.join("\n");
836
- }
837
- return `${badge}
838
-
839
- ${readmeContent}`;
840
- }
841
- function findReadmePath(cwd) {
842
- const candidates = ["README.md", "readme.md", "Readme.md"];
843
- for (const candidate of candidates) {
844
- const candidatePath = path2.join(cwd, candidate);
845
- if (fs2.existsSync(candidatePath)) {
846
- return candidatePath;
847
- }
848
- }
849
- return null;
850
- }
851
- async function ensureReadme(repoName, cwd) {
852
- const existing = findReadmePath(cwd);
853
- if (existing) return existing;
854
- const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
855
- if (!isInteractive2) {
856
- console.log(pc2.yellow('No README found. Run "keyway readme add-badge" from a repo with a README.'));
857
- return null;
858
- }
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)."));
872
- return null;
873
- }
874
- const defaultPath = path2.join(cwd, "README.md");
875
- const content = `# ${repoName}
876
-
877
- `;
878
- fs2.writeFileSync(defaultPath, content, "utf-8");
879
- return defaultPath;
880
- }
881
- async function addBadgeToReadme(silent = false) {
882
- const repo = detectGitRepo();
883
- if (!repo) {
884
- throw new Error("This directory is not a Git repository.");
885
- }
886
- const cwd = process.cwd();
887
- const readmePath = await ensureReadme(repo, cwd);
888
- if (!readmePath) return false;
889
- const badge = generateBadge(repo);
890
- const content = fs2.readFileSync(readmePath, "utf-8");
891
- const updated = insertBadgeIntoReadme(content, badge);
892
- if (updated === content) {
893
- if (!silent) {
894
- console.log(pc2.gray("Keyway badge already present in README."));
895
- }
896
- return false;
897
- }
898
- fs2.writeFileSync(readmePath, updated, "utf-8");
899
- if (!silent) {
900
- console.log(pc2.green(`\u2713 Keyway badge added to ${path2.basename(readmePath)}`));
901
- }
902
- return true;
968
+ console.log(pc2.green("\u2713 Logged out of Keyway"));
969
+ console.log(pc2.gray(`Auth cache cleared: ${getAuthFilePath()}`));
903
970
  }
904
971
 
905
972
  // src/cmds/push.ts
906
- import pc3 from "picocolors";
907
- import fs3 from "fs";
908
- import path3 from "path";
909
- import prompts3 from "prompts";
910
973
  function deriveEnvFromFile(file) {
911
974
  const base = path3.basename(file);
912
975
  const match = base.match(/\.env(?:\.(.+))?$/);
@@ -947,7 +1010,7 @@ function discoverEnvCandidates(cwd) {
947
1010
  async function pushCommand(options) {
948
1011
  try {
949
1012
  console.log(pc3.blue("\u{1F510} Pushing secrets to Keyway...\n"));
950
- const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
1013
+ const isInteractive3 = process.stdin.isTTY && process.stdout.isTTY;
951
1014
  let environment = options.env;
952
1015
  let envFile = options.file;
953
1016
  const candidates = discoverEnvCandidates(process.cwd());
@@ -957,7 +1020,7 @@ async function pushCommand(options) {
957
1020
  envFile = match.file;
958
1021
  }
959
1022
  }
960
- if (!environment && !envFile && isInteractive2 && candidates.length > 0) {
1023
+ if (!environment && !envFile && isInteractive3 && candidates.length > 0) {
961
1024
  const { choice } = await prompts3(
962
1025
  {
963
1026
  type: "select",
@@ -1011,7 +1074,7 @@ async function pushCommand(options) {
1011
1074
  }
1012
1075
  let envFilePath = path3.resolve(process.cwd(), envFile);
1013
1076
  if (!fs3.existsSync(envFilePath)) {
1014
- if (!isInteractive2) {
1077
+ if (!isInteractive3) {
1015
1078
  throw new Error(`File not found: ${envFile}. Provide --file <path> or run interactively to choose a file.`);
1016
1079
  }
1017
1080
  const { newPath } = await prompts3(
@@ -1052,8 +1115,8 @@ async function pushCommand(options) {
1052
1115
  const repoFullName = getCurrentRepoFullName();
1053
1116
  console.log(`Repository: ${pc3.cyan(repoFullName)}`);
1054
1117
  if (!options.yes) {
1055
- const isInteractive3 = process.stdin.isTTY && process.stdout.isTTY;
1056
- if (!isInteractive3) {
1118
+ const isInteractive4 = process.stdin.isTTY && process.stdout.isTTY;
1119
+ if (!isInteractive4) {
1057
1120
  throw new Error("Confirmation required. Re-run with --yes in non-interactive environments.");
1058
1121
  }
1059
1122
  const { confirm } = await prompts3(
@@ -1110,6 +1173,12 @@ Your secrets are now encrypted and stored securely.`);
1110
1173
  hint = `Available environments: ${availableEnvs}
1111
1174
  Use ${pc3.cyan(`keyway push --env <environment>`)} to specify one, or create '${requestedEnv}' via the dashboard.`;
1112
1175
  }
1176
+ if (error.statusCode === 403 && error.upgradeUrl) {
1177
+ hint = `${pc3.yellow("\u26A1")} Upgrade to Pro: ${pc3.cyan(error.upgradeUrl)}`;
1178
+ } else if (error.statusCode === 403 && message.toLowerCase().includes("read-only")) {
1179
+ message = "This vault is read-only on your current plan.";
1180
+ hint = `Upgrade to Pro to unlock editing: ${pc3.cyan("https://keyway.sh/settings")}`;
1181
+ }
1113
1182
  } else if (error instanceof Error) {
1114
1183
  message = truncateMessage(error.message);
1115
1184
  } else {
@@ -1132,15 +1201,150 @@ ${hint}`));
1132
1201
 
1133
1202
  // src/cmds/init.ts
1134
1203
  var DASHBOARD_URL = "https://www.keyway.sh/dashboard/vaults";
1204
+ var POLL_INTERVAL_MS = 3e3;
1205
+ var POLL_TIMEOUT_MS = 12e4;
1206
+ function sleep2(ms) {
1207
+ return new Promise((resolve) => setTimeout(resolve, ms));
1208
+ }
1209
+ function isInteractive2() {
1210
+ return Boolean(process.stdout.isTTY && process.stdin.isTTY && !process.env.CI);
1211
+ }
1212
+ async function ensureLoginAndGitHubApp(repoFullName, options = {}) {
1213
+ const [repoOwner, repoName] = repoFullName.split("/");
1214
+ const envToken = process.env.KEYWAY_TOKEN;
1215
+ if (envToken) {
1216
+ return ensureGitHubAppInstalledOnly(repoFullName, envToken);
1217
+ }
1218
+ const stored = await getStoredAuth();
1219
+ if (stored?.keywayToken) {
1220
+ return ensureGitHubAppInstalledOnly(repoFullName, stored.keywayToken);
1221
+ }
1222
+ const allowPrompt = options.allowPrompt !== false;
1223
+ if (!allowPrompt || !isInteractive2()) {
1224
+ throw new Error('No Keyway session found. Run "keyway login" to authenticate.');
1225
+ }
1226
+ console.log("");
1227
+ console.log(pc4.gray(" Keyway uses a GitHub App for secure access."));
1228
+ console.log(pc4.gray(" Installing the app will also log you in."));
1229
+ console.log("");
1230
+ const { shouldProceed } = await prompts4({
1231
+ type: "confirm",
1232
+ name: "shouldProceed",
1233
+ message: "Open browser to install Keyway & sign in?",
1234
+ initial: true
1235
+ });
1236
+ if (!shouldProceed) {
1237
+ throw new Error('Setup required. Run "keyway init" when ready.');
1238
+ }
1239
+ const deviceStart = await startDeviceLogin(repoFullName);
1240
+ const installUrl = deviceStart.githubAppInstallUrl || "https://github.com/apps/keyway/installations/new";
1241
+ console.log(pc4.gray("\n Opening browser..."));
1242
+ await open2(installUrl);
1243
+ console.log("");
1244
+ console.log(pc4.blue("\u23F3 Waiting for installation & authorization..."));
1245
+ console.log(pc4.gray(" (Press Ctrl+C to cancel)\n"));
1246
+ const pollIntervalMs = Math.max((deviceStart.interval ?? 5) * 1e3, POLL_INTERVAL_MS);
1247
+ const startTime = Date.now();
1248
+ let accessToken = null;
1249
+ while (Date.now() - startTime < POLL_TIMEOUT_MS) {
1250
+ await sleep2(pollIntervalMs);
1251
+ try {
1252
+ if (!accessToken) {
1253
+ const result = await pollDeviceLogin(deviceStart.deviceCode);
1254
+ if (result.status === "approved" && result.keywayToken) {
1255
+ accessToken = result.keywayToken;
1256
+ await saveAuthToken(result.keywayToken, {
1257
+ githubLogin: result.githubLogin,
1258
+ expiresAt: result.expiresAt
1259
+ });
1260
+ console.log(pc4.green("\u2713 Signed in!"));
1261
+ if (result.githubLogin) {
1262
+ identifyUser(result.githubLogin, {
1263
+ github_username: result.githubLogin,
1264
+ login_method: "github_app"
1265
+ });
1266
+ }
1267
+ }
1268
+ }
1269
+ if (accessToken) {
1270
+ const installStatus = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
1271
+ if (installStatus.installed) {
1272
+ console.log(pc4.green("\u2713 GitHub App installed!"));
1273
+ console.log("");
1274
+ return accessToken;
1275
+ }
1276
+ }
1277
+ process.stdout.write(pc4.gray("."));
1278
+ } catch {
1279
+ }
1280
+ }
1281
+ console.log("");
1282
+ console.log(pc4.yellow("\u26A0 Timed out waiting for setup."));
1283
+ console.log(pc4.gray(` Install the GitHub App: ${installUrl}`));
1284
+ throw new Error("Setup timed out. Please try again.");
1285
+ }
1286
+ async function ensureGitHubAppInstalledOnly(repoFullName, accessToken) {
1287
+ const [repoOwner, repoName] = repoFullName.split("/");
1288
+ const status = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
1289
+ if (status.installed) {
1290
+ return accessToken;
1291
+ }
1292
+ console.log("");
1293
+ console.log(pc4.yellow("\u26A0 GitHub App not installed for this repository"));
1294
+ console.log("");
1295
+ console.log(pc4.gray(" The Keyway GitHub App is required to securely manage secrets."));
1296
+ console.log(pc4.gray(" It only requests minimal permissions (repository metadata)."));
1297
+ console.log("");
1298
+ if (!isInteractive2()) {
1299
+ console.log(pc4.gray(` Install the Keyway GitHub App: ${status.installUrl}`));
1300
+ throw new Error("GitHub App installation required.");
1301
+ }
1302
+ const { shouldInstall } = await prompts4({
1303
+ type: "confirm",
1304
+ name: "shouldInstall",
1305
+ message: "Open browser to install Keyway GitHub App?",
1306
+ initial: true
1307
+ });
1308
+ if (!shouldInstall) {
1309
+ console.log(pc4.gray(`
1310
+ You can install later: ${status.installUrl}`));
1311
+ throw new Error("GitHub App installation required.");
1312
+ }
1313
+ console.log(pc4.gray("\n Opening browser..."));
1314
+ await open2(status.installUrl);
1315
+ console.log("");
1316
+ console.log(pc4.blue("\u23F3 Waiting for GitHub App installation..."));
1317
+ console.log(pc4.gray(" (Press Ctrl+C to cancel)\n"));
1318
+ const startTime = Date.now();
1319
+ while (Date.now() - startTime < POLL_TIMEOUT_MS) {
1320
+ await sleep2(POLL_INTERVAL_MS);
1321
+ try {
1322
+ const pollStatus = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
1323
+ if (pollStatus.installed) {
1324
+ console.log(pc4.green("\u2713 GitHub App installed!"));
1325
+ console.log("");
1326
+ return accessToken;
1327
+ }
1328
+ process.stdout.write(pc4.gray("."));
1329
+ } catch {
1330
+ }
1331
+ }
1332
+ console.log("");
1333
+ console.log(pc4.yellow("\u26A0 Timed out waiting for installation."));
1334
+ console.log(pc4.gray(` You can install the GitHub App later: ${status.installUrl}`));
1335
+ throw new Error("GitHub App installation timed out.");
1336
+ }
1135
1337
  async function initCommand(options = {}) {
1136
1338
  try {
1137
1339
  const repoFullName = getCurrentRepoFullName();
1138
1340
  const dashboardLink = `${DASHBOARD_URL}/${repoFullName}`;
1139
1341
  console.log(pc4.blue("\u{1F510} Initializing Keyway vault...\n"));
1140
1342
  console.log(` ${pc4.gray("Repository:")} ${pc4.white(repoFullName)}`);
1141
- const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
1142
- trackEvent(AnalyticsEvents.CLI_INIT, { repoFullName });
1143
- const response = await initVault(repoFullName, accessToken);
1343
+ const accessToken = await ensureLoginAndGitHubApp(repoFullName, {
1344
+ allowPrompt: options.loginPrompt !== false
1345
+ });
1346
+ trackEvent(AnalyticsEvents.CLI_INIT, { repoFullName, githubAppInstalled: true });
1347
+ await initVault(repoFullName, accessToken);
1144
1348
  console.log(pc4.green("\u2713 Vault created!"));
1145
1349
  try {
1146
1350
  const badgeAdded = await addBadgeToReadme(true);
@@ -1151,8 +1355,8 @@ async function initCommand(options = {}) {
1151
1355
  }
1152
1356
  console.log("");
1153
1357
  const envCandidates = discoverEnvCandidates(process.cwd());
1154
- const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
1155
- if (envCandidates.length > 0 && isInteractive2) {
1358
+ const isInteractive3 = process.stdin.isTTY && process.stdout.isTTY;
1359
+ if (envCandidates.length > 0 && isInteractive3) {
1156
1360
  console.log(pc4.gray(` Found ${envCandidates.length} env file(s): ${envCandidates.map((c) => c.file).join(", ")}
1157
1361
  `));
1158
1362
  const { shouldPush } = await prompts4({
@@ -1190,15 +1394,16 @@ async function initCommand(options = {}) {
1190
1394
  await shutdownAnalytics();
1191
1395
  return;
1192
1396
  }
1193
- if (error.error === "PLAN_LIMIT_REACHED") {
1397
+ if (error.error === "Plan Limit Reached" || error.upgradeUrl) {
1398
+ const upgradeUrl = error.upgradeUrl || "https://keyway.sh/pricing";
1194
1399
  console.log("");
1195
1400
  console.log(pc4.dim("\u2500".repeat(50)));
1196
1401
  console.log("");
1197
- console.log(` ${pc4.yellow("\u26A1")} ${pc4.bold("Upgrade Required")}`);
1402
+ console.log(` ${pc4.yellow("\u26A1")} ${pc4.bold("Plan Limit Reached")}`);
1198
1403
  console.log("");
1199
- console.log(pc4.gray(` ${error.message}`));
1404
+ console.log(pc4.white(` ${error.message}`));
1200
1405
  console.log("");
1201
- console.log(` ${pc4.cyan("\u2192")} ${pc4.underline(error.upgradeUrl || "https://keyway.sh/upgrade")}`);
1406
+ console.log(` ${pc4.cyan("Upgrade now \u2192")} ${pc4.underline(upgradeUrl)}`);
1202
1407
  console.log("");
1203
1408
  console.log(pc4.dim("\u2500".repeat(50)));
1204
1409
  console.log("");
@@ -1240,11 +1445,11 @@ async function pullCommand(options) {
1240
1445
  const response = await pullSecrets(repoFullName, environment, accessToken);
1241
1446
  const envFilePath = path4.resolve(process.cwd(), envFile);
1242
1447
  if (fs4.existsSync(envFilePath)) {
1243
- const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
1448
+ const isInteractive3 = process.stdin.isTTY && process.stdout.isTTY;
1244
1449
  if (options.yes) {
1245
1450
  console.log(pc5.yellow(`
1246
1451
  \u26A0 Overwriting existing file: ${envFile}`));
1247
- } else if (!isInteractive2) {
1452
+ } else if (!isInteractive3) {
1248
1453
  throw new Error(`File ${envFile} exists. Re-run with --yes to overwrite or choose a different --file.`);
1249
1454
  } else {
1250
1455
  const { confirm } = await prompts5(
@@ -1295,9 +1500,9 @@ import pc6 from "picocolors";
1295
1500
 
1296
1501
  // src/core/doctor.ts
1297
1502
  import { execSync as execSync2 } from "child_process";
1298
- import { writeFileSync, unlinkSync, readFileSync, existsSync } from "fs";
1503
+ import { writeFileSync as writeFileSync2, unlinkSync, readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
1299
1504
  import { tmpdir } from "os";
1300
- import { join } from "path";
1505
+ import { join as join2 } from "path";
1301
1506
  var API_HEALTH_URL = `${process.env.KEYWAY_API_URL || INTERNAL_API_URL}/v1/health`;
1302
1507
  async function checkNode() {
1303
1508
  const nodeVersion = process.versions.node;
@@ -1414,9 +1619,9 @@ async function checkNetwork() {
1414
1619
  }
1415
1620
  }
1416
1621
  async function checkFileSystem() {
1417
- const testFile = join(tmpdir(), `.keyway-test-${Date.now()}.tmp`);
1622
+ const testFile = join2(tmpdir(), `.keyway-test-${Date.now()}.tmp`);
1418
1623
  try {
1419
- writeFileSync(testFile, "test");
1624
+ writeFileSync2(testFile, "test");
1420
1625
  unlinkSync(testFile);
1421
1626
  return {
1422
1627
  id: "filesystem",
@@ -1435,7 +1640,7 @@ async function checkFileSystem() {
1435
1640
  }
1436
1641
  async function checkGitignore() {
1437
1642
  try {
1438
- if (!existsSync(".gitignore")) {
1643
+ if (!existsSync2(".gitignore")) {
1439
1644
  return {
1440
1645
  id: "gitignore",
1441
1646
  name: ".gitignore configuration",
@@ -1443,7 +1648,7 @@ async function checkGitignore() {
1443
1648
  detail: "No .gitignore file found"
1444
1649
  };
1445
1650
  }
1446
- const gitignoreContent = readFileSync(".gitignore", "utf-8");
1651
+ const gitignoreContent = readFileSync2(".gitignore", "utf-8");
1447
1652
  const hasEnvPattern = gitignoreContent.includes("*.env") || gitignoreContent.includes(".env*");
1448
1653
  const hasDotEnv = gitignoreContent.includes(".env");
1449
1654
  if (hasEnvPattern || hasDotEnv) {
@@ -1607,7 +1812,7 @@ Summary: ${formatSummary(results)}`);
1607
1812
 
1608
1813
  // src/cmds/connect.ts
1609
1814
  import pc7 from "picocolors";
1610
- import open2 from "open";
1815
+ import open3 from "open";
1611
1816
  import prompts6 from "prompts";
1612
1817
  async function connectCommand(provider, options = {}) {
1613
1818
  try {
@@ -1646,7 +1851,7 @@ Connecting to ${providerInfo.displayName}...
1646
1851
  const startTime = /* @__PURE__ */ new Date();
1647
1852
  console.log(pc7.gray("Opening browser for authorization..."));
1648
1853
  console.log(pc7.gray(`If the browser doesn't open, visit: ${authUrl}`));
1649
- await open2(authUrl).catch(() => {
1854
+ await open3(authUrl).catch(() => {
1650
1855
  });
1651
1856
  console.log(pc7.gray("Waiting for authorization..."));
1652
1857
  const maxAttempts = 60;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keywaysh/cli",
3
- "version": "0.0.21",
3
+ "version": "0.1.0",
4
4
  "description": "One link to all your secrets",
5
5
  "type": "module",
6
6
  "bin": {