@keywaysh/cli 0.0.21 → 0.1.1

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.
@@ -0,0 +1,12 @@
1
+ import {
2
+ clearAuth,
3
+ getAuthFilePath,
4
+ getStoredAuth,
5
+ saveAuthToken
6
+ } from "./chunk-F4C46224.js";
7
+ export {
8
+ clearAuth,
9
+ getAuthFilePath,
10
+ getStoredAuth,
11
+ saveAuthToken
12
+ };
@@ -0,0 +1,102 @@
1
+ // src/utils/auth.ts
2
+ import Conf from "conf";
3
+ import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "fs";
5
+ import { join } from "path";
6
+ import { homedir } from "os";
7
+ var store = new Conf({
8
+ projectName: "keyway",
9
+ configName: "config",
10
+ fileMode: 384
11
+ });
12
+ var KEY_DIR = join(homedir(), ".keyway");
13
+ var KEY_FILE = join(KEY_DIR, ".key");
14
+ function getOrCreateEncryptionKey() {
15
+ if (!existsSync(KEY_DIR)) {
16
+ mkdirSync(KEY_DIR, { recursive: true, mode: 448 });
17
+ }
18
+ if (existsSync(KEY_FILE)) {
19
+ const keyHex2 = readFileSync(KEY_FILE, "utf-8").trim();
20
+ if (keyHex2.length === 64) {
21
+ return Buffer.from(keyHex2, "hex");
22
+ }
23
+ }
24
+ const key = randomBytes(32);
25
+ const keyHex = key.toString("hex");
26
+ writeFileSync(KEY_FILE, keyHex, { mode: 384 });
27
+ try {
28
+ chmodSync(KEY_FILE, 384);
29
+ } catch {
30
+ }
31
+ return key;
32
+ }
33
+ function encryptToken(token) {
34
+ const key = getOrCreateEncryptionKey();
35
+ const iv = randomBytes(16);
36
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
37
+ const encrypted = Buffer.concat([cipher.update(token, "utf8"), cipher.final()]);
38
+ const authTag = cipher.getAuthTag();
39
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
40
+ }
41
+ function decryptToken(encryptedData) {
42
+ const key = getOrCreateEncryptionKey();
43
+ const parts = encryptedData.split(":");
44
+ if (parts.length !== 3) {
45
+ throw new Error("Invalid encrypted token format");
46
+ }
47
+ const iv = Buffer.from(parts[0], "hex");
48
+ const authTag = Buffer.from(parts[1], "hex");
49
+ const encrypted = Buffer.from(parts[2], "hex");
50
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
51
+ decipher.setAuthTag(authTag);
52
+ const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
53
+ return decrypted.toString("utf8");
54
+ }
55
+ function isExpired(auth) {
56
+ if (!auth.expiresAt) return false;
57
+ const expires = Date.parse(auth.expiresAt);
58
+ if (Number.isNaN(expires)) return false;
59
+ return expires <= Date.now();
60
+ }
61
+ async function getStoredAuth() {
62
+ const encryptedData = store.get("auth");
63
+ if (!encryptedData) {
64
+ return null;
65
+ }
66
+ try {
67
+ const decrypted = decryptToken(encryptedData);
68
+ const auth = JSON.parse(decrypted);
69
+ if (isExpired(auth)) {
70
+ clearAuth();
71
+ return null;
72
+ }
73
+ return auth;
74
+ } catch {
75
+ console.error("Failed to decrypt stored auth, clearing...");
76
+ clearAuth();
77
+ return null;
78
+ }
79
+ }
80
+ async function saveAuthToken(token, meta) {
81
+ const auth = {
82
+ keywayToken: token,
83
+ githubLogin: meta?.githubLogin,
84
+ expiresAt: meta?.expiresAt,
85
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
86
+ };
87
+ const encrypted = encryptToken(JSON.stringify(auth));
88
+ store.set("auth", encrypted);
89
+ }
90
+ function clearAuth() {
91
+ store.delete("auth");
92
+ }
93
+ function getAuthFilePath() {
94
+ return store.path;
95
+ }
96
+
97
+ export {
98
+ getStoredAuth,
99
+ saveAuthToken,
100
+ clearAuth,
101
+ getAuthFilePath
102
+ };
package/dist/cli.js CHANGED
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ clearAuth,
4
+ getAuthFilePath,
5
+ getStoredAuth,
6
+ saveAuthToken
7
+ } from "./chunk-F4C46224.js";
2
8
 
3
9
  // src/cli.ts
4
10
  import { Command } from "commander";
@@ -7,6 +13,7 @@ import pc9 from "picocolors";
7
13
  // src/cmds/init.ts
8
14
  import pc4 from "picocolors";
9
15
  import prompts4 from "prompts";
16
+ import open2 from "open";
10
17
 
11
18
  // src/utils/git.ts
12
19
  import { execSync } from "child_process";
@@ -69,7 +76,7 @@ var INTERNAL_POSTHOG_HOST = "https://eu.i.posthog.com";
69
76
  // package.json
70
77
  var package_default = {
71
78
  name: "@keywaysh/cli",
72
- version: "0.0.21",
79
+ version: "0.1.1",
73
80
  description: "One link to all your secrets",
74
81
  type: "module",
75
82
  bin: {
@@ -338,7 +345,8 @@ async function validateToken(token) {
338
345
  },
339
346
  body: JSON.stringify({})
340
347
  });
341
- return handleResponse(response);
348
+ const wrapped = await handleResponse(response);
349
+ return wrapped.data;
342
350
  }
343
351
  async function getProviders() {
344
352
  const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations`, {
@@ -347,7 +355,8 @@ async function getProviders() {
347
355
  "User-Agent": USER_AGENT
348
356
  }
349
357
  });
350
- return handleResponse(response);
358
+ const wrapped = await handleResponse(response);
359
+ return wrapped.data;
351
360
  }
352
361
  async function getConnections(accessToken) {
353
362
  const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations/connections`, {
@@ -357,7 +366,8 @@ async function getConnections(accessToken) {
357
366
  Authorization: `Bearer ${accessToken}`
358
367
  }
359
368
  });
360
- return handleResponse(response);
369
+ const wrapped = await handleResponse(response);
370
+ return wrapped.data;
361
371
  }
362
372
  async function deleteConnection(accessToken, connectionId) {
363
373
  const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations/connections/${connectionId}`, {
@@ -367,7 +377,7 @@ async function deleteConnection(accessToken, connectionId) {
367
377
  Authorization: `Bearer ${accessToken}`
368
378
  }
369
379
  });
370
- return handleResponse(response);
380
+ await handleResponse(response);
371
381
  }
372
382
  function getProviderAuthUrl(provider, redirectUri) {
373
383
  const params = redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : "";
@@ -381,7 +391,8 @@ async function getConnectionProjects(accessToken, connectionId) {
381
391
  Authorization: `Bearer ${accessToken}`
382
392
  }
383
393
  });
384
- return handleResponse(response);
394
+ const wrapped = await handleResponse(response);
395
+ return wrapped.data;
385
396
  }
386
397
  async function getSyncStatus(accessToken, repoFullName, connectionId, projectId, environment = "production") {
387
398
  const [owner, repo] = repoFullName.split("/");
@@ -400,7 +411,8 @@ async function getSyncStatus(accessToken, repoFullName, connectionId, projectId,
400
411
  }
401
412
  }
402
413
  );
403
- return handleResponse(response);
414
+ const wrapped = await handleResponse(response);
415
+ return wrapped.data;
404
416
  }
405
417
  async function getSyncPreview(accessToken, repoFullName, options) {
406
418
  const [owner, repo] = repoFullName.split("/");
@@ -424,7 +436,8 @@ async function getSyncPreview(accessToken, repoFullName, options) {
424
436
  6e4
425
437
  // 60 seconds for sync operations
426
438
  );
427
- return handleResponse(response);
439
+ const wrapped = await handleResponse(response);
440
+ return wrapped.data;
428
441
  }
429
442
  async function executeSync(accessToken, repoFullName, options) {
430
443
  const [owner, repo] = repoFullName.split("/");
@@ -449,7 +462,21 @@ async function executeSync(accessToken, repoFullName, options) {
449
462
  12e4
450
463
  // 2 minutes for sync execution
451
464
  );
452
- return handleResponse(response);
465
+ const wrapped = await handleResponse(response);
466
+ return wrapped.data;
467
+ }
468
+ async function checkGitHubAppInstallation(repoOwner, repoName, accessToken) {
469
+ const response = await fetchWithTimeout(`${API_BASE_URL}/v1/github/check-installation`, {
470
+ method: "POST",
471
+ headers: {
472
+ "Content-Type": "application/json",
473
+ "User-Agent": USER_AGENT,
474
+ Authorization: `Bearer ${accessToken}`
475
+ },
476
+ body: JSON.stringify({ repoOwner, repoName })
477
+ });
478
+ const wrapped = await handleResponse(response);
479
+ return wrapped.data;
453
480
  }
454
481
 
455
482
  // src/utils/analytics.ts
@@ -541,6 +568,30 @@ async function shutdownAnalytics() {
541
568
  await posthog.shutdown();
542
569
  }
543
570
  }
571
+ function identifyUser(userId, properties) {
572
+ try {
573
+ if (TELEMETRY_DISABLED) return;
574
+ if (!posthog) initPostHog();
575
+ if (!posthog) return;
576
+ const sanitizedProperties = properties ? sanitizeProperties(properties) : {};
577
+ posthog.identify({
578
+ distinctId: userId,
579
+ properties: {
580
+ ...sanitizedProperties,
581
+ source: "cli"
582
+ }
583
+ });
584
+ const anonId = getDistinctId();
585
+ if (anonId && anonId !== userId) {
586
+ posthog.alias({
587
+ distinctId: userId,
588
+ alias: anonId
589
+ });
590
+ }
591
+ } catch (error) {
592
+ console.debug("Analytics identify error:", error);
593
+ }
594
+ }
544
595
  var AnalyticsEvents = {
545
596
  CLI_INIT: "cli_init",
546
597
  CLI_PUSH: "cli_push",
@@ -554,103 +605,108 @@ var AnalyticsEvents = {
554
605
  CLI_FEEDBACK: "cli_feedback"
555
606
  };
556
607
 
557
- // src/cmds/login.ts
558
- import pc from "picocolors";
559
- import readline from "readline";
560
- import open from "open";
608
+ // src/cmds/readme.ts
609
+ import fs2 from "fs";
610
+ import path2 from "path";
561
611
  import prompts from "prompts";
612
+ import pc from "picocolors";
613
+ function generateBadge(repo) {
614
+ return `[![Keyway Secrets](https://www.keyway.sh/badge.svg?repo=${repo})](https://www.keyway.sh/vaults/${repo})`;
615
+ }
616
+ function insertBadgeIntoReadme(readmeContent, badge) {
617
+ if (readmeContent.includes("keyway.sh/badge.svg")) {
618
+ return readmeContent;
619
+ }
620
+ const lines = readmeContent.split(/\r?\n/);
621
+ const titleIndex = lines.findIndex((line) => /^#(?!#)\s+/.test(line.trim()));
622
+ if (titleIndex !== -1) {
623
+ const before = lines.slice(0, titleIndex + 1);
624
+ const after = lines.slice(titleIndex + 1);
625
+ while (after.length > 0 && after[0].trim() === "") {
626
+ after.shift();
627
+ }
628
+ const newLines = [...before, "", badge, "", ...after];
629
+ return newLines.join("\n");
630
+ }
631
+ return `${badge}
562
632
 
563
- // src/utils/auth.ts
564
- import Conf from "conf";
565
- import { createCipheriv, createDecipheriv, randomBytes, scrypt } from "crypto";
566
- import { promisify } from "util";
567
- var store = new Conf({
568
- projectName: "keyway",
569
- configName: "config",
570
- fileMode: 384
571
- });
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);
581
- return key;
582
- }
583
- async function encryptToken(token) {
584
- const key = await getEncryptionKey();
585
- const iv = randomBytes(16);
586
- const cipher = createCipheriv("aes-256-gcm", key, iv);
587
- const encrypted = Buffer.concat([
588
- cipher.update(token, "utf8"),
589
- cipher.final()
590
- ]);
591
- const authTag = cipher.getAuthTag();
592
- return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
593
- }
594
- async function decryptToken(encryptedData) {
595
- const key = await getEncryptionKey();
596
- const parts = encryptedData.split(":");
597
- if (parts.length !== 3) {
598
- throw new Error("Invalid encrypted token format");
599
- }
600
- const iv = Buffer.from(parts[0], "hex");
601
- const authTag = Buffer.from(parts[1], "hex");
602
- const encrypted = Buffer.from(parts[2], "hex");
603
- const decipher = createDecipheriv("aes-256-gcm", key, iv);
604
- decipher.setAuthTag(authTag);
605
- const decrypted = Buffer.concat([
606
- decipher.update(encrypted),
607
- decipher.final()
608
- ]);
609
- return decrypted.toString("utf8");
633
+ ${readmeContent}`;
610
634
  }
611
- function isExpired(auth) {
612
- if (!auth.expiresAt) return false;
613
- const expires = Date.parse(auth.expiresAt);
614
- if (Number.isNaN(expires)) return false;
615
- return expires <= Date.now();
635
+ function findReadmePath(cwd) {
636
+ const candidates = ["README.md", "readme.md", "Readme.md"];
637
+ for (const candidate of candidates) {
638
+ const candidatePath = path2.join(cwd, candidate);
639
+ if (fs2.existsSync(candidatePath)) {
640
+ return candidatePath;
641
+ }
642
+ }
643
+ return null;
616
644
  }
617
- async function getStoredAuth() {
618
- const encryptedData = store.get("auth");
619
- if (!encryptedData) {
645
+ async function ensureReadme(repoName, cwd) {
646
+ const existing = findReadmePath(cwd);
647
+ if (existing) return existing;
648
+ const isInteractive3 = process.stdin.isTTY && process.stdout.isTTY;
649
+ if (!isInteractive3) {
650
+ console.log(pc.yellow('No README found. Run "keyway readme add-badge" from a repo with a README.'));
620
651
  return null;
621
652
  }
622
- try {
623
- const decrypted = await decryptToken(encryptedData);
624
- const auth = JSON.parse(decrypted);
625
- if (isExpired(auth)) {
626
- clearAuth();
627
- return null;
653
+ const { confirm } = await prompts(
654
+ {
655
+ type: "confirm",
656
+ name: "confirm",
657
+ message: "No README found. Create a default README.md?",
658
+ initial: false
659
+ },
660
+ {
661
+ onCancel: () => ({ confirm: false })
628
662
  }
629
- return auth;
630
- } catch (error) {
631
- console.error("Failed to decrypt stored auth, clearing...");
632
- clearAuth();
663
+ );
664
+ if (!confirm) {
665
+ console.log(pc.yellow("Skipping badge insertion (no README)."));
633
666
  return null;
634
667
  }
668
+ const defaultPath = path2.join(cwd, "README.md");
669
+ const content = `# ${repoName}
670
+
671
+ `;
672
+ fs2.writeFileSync(defaultPath, content, "utf-8");
673
+ return defaultPath;
635
674
  }
636
- async function saveAuthToken(token, meta) {
637
- const auth = {
638
- keywayToken: token,
639
- githubLogin: meta?.githubLogin,
640
- expiresAt: meta?.expiresAt,
641
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
642
- };
643
- const encrypted = await encryptToken(JSON.stringify(auth));
644
- store.set("auth", encrypted);
645
- }
646
- function clearAuth() {
647
- store.delete("auth");
648
- }
649
- function getAuthFilePath() {
650
- return store.path;
675
+ async function addBadgeToReadme(silent = false) {
676
+ const repo = detectGitRepo();
677
+ if (!repo) {
678
+ throw new Error("This directory is not a Git repository.");
679
+ }
680
+ const cwd = process.cwd();
681
+ const readmePath = await ensureReadme(repo, cwd);
682
+ if (!readmePath) return false;
683
+ const badge = generateBadge(repo);
684
+ const content = fs2.readFileSync(readmePath, "utf-8");
685
+ const updated = insertBadgeIntoReadme(content, badge);
686
+ if (updated === content) {
687
+ if (!silent) {
688
+ console.log(pc.gray("Keyway badge already present in README."));
689
+ }
690
+ return false;
691
+ }
692
+ fs2.writeFileSync(readmePath, updated, "utf-8");
693
+ if (!silent) {
694
+ console.log(pc.green(`\u2713 Keyway badge added to ${path2.basename(readmePath)}`));
695
+ }
696
+ return true;
651
697
  }
652
698
 
699
+ // src/cmds/push.ts
700
+ import pc3 from "picocolors";
701
+ import fs3 from "fs";
702
+ import path3 from "path";
703
+ import prompts3 from "prompts";
704
+
653
705
  // src/cmds/login.ts
706
+ import pc2 from "picocolors";
707
+ import readline from "readline";
708
+ import open from "open";
709
+ import prompts2 from "prompts";
654
710
  function sleep(ms) {
655
711
  return new Promise((resolve) => setTimeout(resolve, ms));
656
712
  }
@@ -674,17 +730,17 @@ async function promptYesNo(question, defaultYes = true) {
674
730
  });
675
731
  }
676
732
  async function runLoginFlow() {
677
- console.log(pc.blue("\u{1F510} Starting Keyway login...\n"));
733
+ console.log(pc2.blue("\u{1F510} Starting Keyway login...\n"));
678
734
  const repoName = detectGitRepo();
679
735
  const start = await startDeviceLogin(repoName);
680
736
  const verifyUrl = start.verificationUriComplete || start.verificationUri;
681
737
  if (!verifyUrl) {
682
738
  throw new Error("Missing verification URL from the auth server.");
683
739
  }
684
- console.log(`Code: ${pc.bold(pc.green(start.userCode))}`);
740
+ console.log(`Code: ${pc2.bold(pc2.green(start.userCode))}`);
685
741
  console.log("Waiting for auth...");
686
742
  open(verifyUrl).catch(() => {
687
- console.log(pc.gray(`Open this URL in your browser: ${verifyUrl}`));
743
+ console.log(pc2.gray(`Open this URL in your browser: ${verifyUrl}`));
688
744
  });
689
745
  const pollIntervalMs = (start.interval ?? 5) * 1e3;
690
746
  const maxTimeoutMs = Math.min((start.expiresIn ?? 900) * 1e3, 30 * 60 * 1e3);
@@ -707,9 +763,15 @@ async function runLoginFlow() {
707
763
  method: "device",
708
764
  repo: repoName
709
765
  });
710
- console.log(pc.green("\n\u2713 Login successful"));
711
766
  if (result.githubLogin) {
712
- console.log(`Authenticated GitHub user: ${pc.cyan(result.githubLogin)}`);
767
+ identifyUser(result.githubLogin, {
768
+ github_username: result.githubLogin,
769
+ login_method: "device"
770
+ });
771
+ }
772
+ console.log(pc2.green("\n\u2713 Login successful"));
773
+ if (result.githubLogin) {
774
+ console.log(`Authenticated GitHub user: ${pc2.cyan(result.githubLogin)}`);
713
775
  }
714
776
  return result.keywayToken;
715
777
  }
@@ -722,7 +784,7 @@ async function ensureLogin(options = {}) {
722
784
  return envToken;
723
785
  }
724
786
  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."));
787
+ console.warn(pc2.yellow("Note: GITHUB_TOKEN found but not used. Set KEYWAY_TOKEN for Keyway authentication."));
726
788
  }
727
789
  const stored = await getStoredAuth();
728
790
  if (stored?.keywayToken) {
@@ -742,17 +804,17 @@ async function ensureLogin(options = {}) {
742
804
  async function runTokenLogin() {
743
805
  const repoName = detectGitRepo();
744
806
  if (repoName) {
745
- console.log(`\u{1F4C1} Detected: ${pc.cyan(repoName)}`);
807
+ console.log(`\u{1F4C1} Detected: ${pc2.cyan(repoName)}`);
746
808
  }
747
809
  const description = repoName ? `Keyway CLI for ${repoName}` : "Keyway CLI";
748
810
  const url = `https://github.com/settings/personal-access-tokens/new?description=${encodeURIComponent(description)}`;
749
811
  console.log("Opening GitHub...");
750
812
  open(url).catch(() => {
751
- console.log(pc.gray(`Open this URL in your browser: ${url}`));
813
+ console.log(pc2.gray(`Open this URL in your browser: ${url}`));
752
814
  });
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(
815
+ console.log(pc2.gray("Select the detected repo (or scope manually)."));
816
+ console.log(pc2.gray("Permissions: Metadata \u2192 Read-only; Account permissions: None."));
817
+ const { token } = await prompts2(
756
818
  {
757
819
  type: "password",
758
820
  name: "token",
@@ -784,7 +846,11 @@ async function runTokenLogin() {
784
846
  method: "pat",
785
847
  repo: repoName
786
848
  });
787
- console.log(pc.green("\u2705 Authenticated"), `as ${pc.cyan(`@${validation.username}`)}`);
849
+ identifyUser(validation.username, {
850
+ github_username: validation.username,
851
+ login_method: "pat"
852
+ });
853
+ console.log(pc2.green("\u2705 Authenticated"), `as ${pc2.cyan(`@${validation.username}`)}`);
788
854
  return trimmedToken;
789
855
  }
790
856
  async function loginCommand(options = {}) {
@@ -800,113 +866,18 @@ async function loginCommand(options = {}) {
800
866
  command: "login",
801
867
  error: truncateMessage(message)
802
868
  });
803
- console.error(pc.red(`
869
+ console.error(pc2.red(`
804
870
  \u2717 ${message}`));
805
871
  process.exit(1);
806
872
  }
807
873
  }
808
874
  async function logoutCommand() {
809
875
  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;
876
+ console.log(pc2.green("\u2713 Logged out of Keyway"));
877
+ console.log(pc2.gray(`Auth cache cleared: ${getAuthFilePath()}`));
903
878
  }
904
879
 
905
880
  // 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
881
  function deriveEnvFromFile(file) {
911
882
  const base = path3.basename(file);
912
883
  const match = base.match(/\.env(?:\.(.+))?$/);
@@ -947,7 +918,7 @@ function discoverEnvCandidates(cwd) {
947
918
  async function pushCommand(options) {
948
919
  try {
949
920
  console.log(pc3.blue("\u{1F510} Pushing secrets to Keyway...\n"));
950
- const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
921
+ const isInteractive3 = process.stdin.isTTY && process.stdout.isTTY;
951
922
  let environment = options.env;
952
923
  let envFile = options.file;
953
924
  const candidates = discoverEnvCandidates(process.cwd());
@@ -957,7 +928,7 @@ async function pushCommand(options) {
957
928
  envFile = match.file;
958
929
  }
959
930
  }
960
- if (!environment && !envFile && isInteractive2 && candidates.length > 0) {
931
+ if (!environment && !envFile && isInteractive3 && candidates.length > 0) {
961
932
  const { choice } = await prompts3(
962
933
  {
963
934
  type: "select",
@@ -1011,7 +982,7 @@ async function pushCommand(options) {
1011
982
  }
1012
983
  let envFilePath = path3.resolve(process.cwd(), envFile);
1013
984
  if (!fs3.existsSync(envFilePath)) {
1014
- if (!isInteractive2) {
985
+ if (!isInteractive3) {
1015
986
  throw new Error(`File not found: ${envFile}. Provide --file <path> or run interactively to choose a file.`);
1016
987
  }
1017
988
  const { newPath } = await prompts3(
@@ -1052,8 +1023,8 @@ async function pushCommand(options) {
1052
1023
  const repoFullName = getCurrentRepoFullName();
1053
1024
  console.log(`Repository: ${pc3.cyan(repoFullName)}`);
1054
1025
  if (!options.yes) {
1055
- const isInteractive3 = process.stdin.isTTY && process.stdout.isTTY;
1056
- if (!isInteractive3) {
1026
+ const isInteractive4 = process.stdin.isTTY && process.stdout.isTTY;
1027
+ if (!isInteractive4) {
1057
1028
  throw new Error("Confirmation required. Re-run with --yes in non-interactive environments.");
1058
1029
  }
1059
1030
  const { confirm } = await prompts3(
@@ -1110,6 +1081,12 @@ Your secrets are now encrypted and stored securely.`);
1110
1081
  hint = `Available environments: ${availableEnvs}
1111
1082
  Use ${pc3.cyan(`keyway push --env <environment>`)} to specify one, or create '${requestedEnv}' via the dashboard.`;
1112
1083
  }
1084
+ if (error.statusCode === 403 && error.upgradeUrl) {
1085
+ hint = `${pc3.yellow("\u26A1")} Upgrade to Pro: ${pc3.cyan(error.upgradeUrl)}`;
1086
+ } else if (error.statusCode === 403 && message.toLowerCase().includes("read-only")) {
1087
+ message = "This vault is read-only on your current plan.";
1088
+ hint = `Upgrade to Pro to unlock editing: ${pc3.cyan("https://keyway.sh/settings")}`;
1089
+ }
1113
1090
  } else if (error instanceof Error) {
1114
1091
  message = truncateMessage(error.message);
1115
1092
  } else {
@@ -1132,15 +1109,168 @@ ${hint}`));
1132
1109
 
1133
1110
  // src/cmds/init.ts
1134
1111
  var DASHBOARD_URL = "https://www.keyway.sh/dashboard/vaults";
1112
+ var POLL_INTERVAL_MS = 3e3;
1113
+ var POLL_TIMEOUT_MS = 12e4;
1114
+ function sleep2(ms) {
1115
+ return new Promise((resolve) => setTimeout(resolve, ms));
1116
+ }
1117
+ function isInteractive2() {
1118
+ return Boolean(process.stdout.isTTY && process.stdin.isTTY && !process.env.CI);
1119
+ }
1120
+ async function ensureLoginAndGitHubApp(repoFullName, options = {}) {
1121
+ const [repoOwner, repoName] = repoFullName.split("/");
1122
+ const envToken = process.env.KEYWAY_TOKEN;
1123
+ if (envToken) {
1124
+ const result = await ensureGitHubAppInstalledOnly(repoFullName, envToken);
1125
+ if (result === null) {
1126
+ throw new Error("KEYWAY_TOKEN is invalid or expired. Please update the token.");
1127
+ }
1128
+ return result;
1129
+ }
1130
+ const stored = await getStoredAuth();
1131
+ if (stored?.keywayToken) {
1132
+ const result = await ensureGitHubAppInstalledOnly(repoFullName, stored.keywayToken);
1133
+ if (result !== null) {
1134
+ return result;
1135
+ }
1136
+ }
1137
+ const allowPrompt = options.allowPrompt !== false;
1138
+ if (!allowPrompt || !isInteractive2()) {
1139
+ throw new Error('No Keyway session found. Run "keyway login" to authenticate.');
1140
+ }
1141
+ console.log("");
1142
+ console.log(pc4.gray(" Keyway uses a GitHub App for secure access."));
1143
+ console.log(pc4.gray(" Installing the app will also log you in."));
1144
+ console.log("");
1145
+ const { shouldProceed } = await prompts4({
1146
+ type: "confirm",
1147
+ name: "shouldProceed",
1148
+ message: "Open browser to install Keyway & sign in?",
1149
+ initial: true
1150
+ });
1151
+ if (!shouldProceed) {
1152
+ throw new Error('Setup required. Run "keyway init" when ready.');
1153
+ }
1154
+ const deviceStart = await startDeviceLogin(repoFullName);
1155
+ const installUrl = deviceStart.githubAppInstallUrl || "https://github.com/apps/keyway/installations/new";
1156
+ console.log(pc4.gray("\n Opening browser..."));
1157
+ await open2(installUrl);
1158
+ console.log("");
1159
+ console.log(pc4.blue("\u23F3 Waiting for installation & authorization..."));
1160
+ console.log(pc4.gray(" (Press Ctrl+C to cancel)\n"));
1161
+ const pollIntervalMs = Math.max((deviceStart.interval ?? 5) * 1e3, POLL_INTERVAL_MS);
1162
+ const startTime = Date.now();
1163
+ let accessToken = null;
1164
+ while (Date.now() - startTime < POLL_TIMEOUT_MS) {
1165
+ await sleep2(pollIntervalMs);
1166
+ try {
1167
+ if (!accessToken) {
1168
+ const result = await pollDeviceLogin(deviceStart.deviceCode);
1169
+ if (result.status === "approved" && result.keywayToken) {
1170
+ accessToken = result.keywayToken;
1171
+ await saveAuthToken(result.keywayToken, {
1172
+ githubLogin: result.githubLogin,
1173
+ expiresAt: result.expiresAt
1174
+ });
1175
+ console.log(pc4.green("\u2713 Signed in!"));
1176
+ if (result.githubLogin) {
1177
+ identifyUser(result.githubLogin, {
1178
+ github_username: result.githubLogin,
1179
+ login_method: "github_app"
1180
+ });
1181
+ }
1182
+ }
1183
+ }
1184
+ if (accessToken) {
1185
+ const installStatus = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
1186
+ if (installStatus.installed) {
1187
+ console.log(pc4.green("\u2713 GitHub App installed!"));
1188
+ console.log("");
1189
+ return accessToken;
1190
+ }
1191
+ }
1192
+ process.stdout.write(pc4.gray("."));
1193
+ } catch {
1194
+ }
1195
+ }
1196
+ console.log("");
1197
+ console.log(pc4.yellow("\u26A0 Timed out waiting for setup."));
1198
+ console.log(pc4.gray(` Install the GitHub App: ${installUrl}`));
1199
+ throw new Error("Setup timed out. Please try again.");
1200
+ }
1201
+ async function ensureGitHubAppInstalledOnly(repoFullName, accessToken) {
1202
+ const [repoOwner, repoName] = repoFullName.split("/");
1203
+ let status;
1204
+ try {
1205
+ status = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
1206
+ } catch (error) {
1207
+ if (error instanceof APIError && error.statusCode === 401) {
1208
+ console.log(pc4.yellow("\n\u26A0 Session expired or invalid. Clearing credentials..."));
1209
+ const { clearAuth: clearAuth2 } = await import("./auth-QLPQ24HZ.js");
1210
+ clearAuth2();
1211
+ return null;
1212
+ }
1213
+ throw error;
1214
+ }
1215
+ if (status.installed) {
1216
+ return accessToken;
1217
+ }
1218
+ console.log("");
1219
+ console.log(pc4.yellow("\u26A0 GitHub App not installed for this repository"));
1220
+ console.log("");
1221
+ console.log(pc4.gray(" The Keyway GitHub App is required to securely manage secrets."));
1222
+ console.log(pc4.gray(" It only requests minimal permissions (repository metadata)."));
1223
+ console.log("");
1224
+ if (!isInteractive2()) {
1225
+ console.log(pc4.gray(` Install the Keyway GitHub App: ${status.installUrl}`));
1226
+ throw new Error("GitHub App installation required.");
1227
+ }
1228
+ const { shouldInstall } = await prompts4({
1229
+ type: "confirm",
1230
+ name: "shouldInstall",
1231
+ message: "Open browser to install Keyway GitHub App?",
1232
+ initial: true
1233
+ });
1234
+ if (!shouldInstall) {
1235
+ console.log(pc4.gray(`
1236
+ You can install later: ${status.installUrl}`));
1237
+ throw new Error("GitHub App installation required.");
1238
+ }
1239
+ console.log(pc4.gray("\n Opening browser..."));
1240
+ await open2(status.installUrl);
1241
+ console.log("");
1242
+ console.log(pc4.blue("\u23F3 Waiting for GitHub App installation..."));
1243
+ console.log(pc4.gray(" (Press Ctrl+C to cancel)\n"));
1244
+ const startTime = Date.now();
1245
+ while (Date.now() - startTime < POLL_TIMEOUT_MS) {
1246
+ await sleep2(POLL_INTERVAL_MS);
1247
+ try {
1248
+ const pollStatus = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
1249
+ if (pollStatus.installed) {
1250
+ console.log(pc4.green("\u2713 GitHub App installed!"));
1251
+ console.log("");
1252
+ return accessToken;
1253
+ }
1254
+ process.stdout.write(pc4.gray("."));
1255
+ } catch {
1256
+ }
1257
+ }
1258
+ console.log("");
1259
+ console.log(pc4.yellow("\u26A0 Timed out waiting for installation."));
1260
+ console.log(pc4.gray(` You can install the GitHub App later: ${status.installUrl}`));
1261
+ throw new Error("GitHub App installation timed out.");
1262
+ }
1135
1263
  async function initCommand(options = {}) {
1136
1264
  try {
1137
1265
  const repoFullName = getCurrentRepoFullName();
1138
1266
  const dashboardLink = `${DASHBOARD_URL}/${repoFullName}`;
1139
1267
  console.log(pc4.blue("\u{1F510} Initializing Keyway vault...\n"));
1140
1268
  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);
1269
+ const accessToken = await ensureLoginAndGitHubApp(repoFullName, {
1270
+ allowPrompt: options.loginPrompt !== false
1271
+ });
1272
+ trackEvent(AnalyticsEvents.CLI_INIT, { repoFullName, githubAppInstalled: true });
1273
+ await initVault(repoFullName, accessToken);
1144
1274
  console.log(pc4.green("\u2713 Vault created!"));
1145
1275
  try {
1146
1276
  const badgeAdded = await addBadgeToReadme(true);
@@ -1151,8 +1281,8 @@ async function initCommand(options = {}) {
1151
1281
  }
1152
1282
  console.log("");
1153
1283
  const envCandidates = discoverEnvCandidates(process.cwd());
1154
- const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
1155
- if (envCandidates.length > 0 && isInteractive2) {
1284
+ const isInteractive3 = process.stdin.isTTY && process.stdout.isTTY;
1285
+ if (envCandidates.length > 0 && isInteractive3) {
1156
1286
  console.log(pc4.gray(` Found ${envCandidates.length} env file(s): ${envCandidates.map((c) => c.file).join(", ")}
1157
1287
  `));
1158
1288
  const { shouldPush } = await prompts4({
@@ -1190,15 +1320,16 @@ async function initCommand(options = {}) {
1190
1320
  await shutdownAnalytics();
1191
1321
  return;
1192
1322
  }
1193
- if (error.error === "PLAN_LIMIT_REACHED") {
1323
+ if (error.error === "Plan Limit Reached" || error.upgradeUrl) {
1324
+ const upgradeUrl = error.upgradeUrl || "https://keyway.sh/pricing";
1194
1325
  console.log("");
1195
1326
  console.log(pc4.dim("\u2500".repeat(50)));
1196
1327
  console.log("");
1197
- console.log(` ${pc4.yellow("\u26A1")} ${pc4.bold("Upgrade Required")}`);
1328
+ console.log(` ${pc4.yellow("\u26A1")} ${pc4.bold("Plan Limit Reached")}`);
1198
1329
  console.log("");
1199
- console.log(pc4.gray(` ${error.message}`));
1330
+ console.log(pc4.white(` ${error.message}`));
1200
1331
  console.log("");
1201
- console.log(` ${pc4.cyan("\u2192")} ${pc4.underline(error.upgradeUrl || "https://keyway.sh/upgrade")}`);
1332
+ console.log(` ${pc4.cyan("Upgrade now \u2192")} ${pc4.underline(upgradeUrl)}`);
1202
1333
  console.log("");
1203
1334
  console.log(pc4.dim("\u2500".repeat(50)));
1204
1335
  console.log("");
@@ -1240,11 +1371,11 @@ async function pullCommand(options) {
1240
1371
  const response = await pullSecrets(repoFullName, environment, accessToken);
1241
1372
  const envFilePath = path4.resolve(process.cwd(), envFile);
1242
1373
  if (fs4.existsSync(envFilePath)) {
1243
- const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
1374
+ const isInteractive3 = process.stdin.isTTY && process.stdout.isTTY;
1244
1375
  if (options.yes) {
1245
1376
  console.log(pc5.yellow(`
1246
1377
  \u26A0 Overwriting existing file: ${envFile}`));
1247
- } else if (!isInteractive2) {
1378
+ } else if (!isInteractive3) {
1248
1379
  throw new Error(`File ${envFile} exists. Re-run with --yes to overwrite or choose a different --file.`);
1249
1380
  } else {
1250
1381
  const { confirm } = await prompts5(
@@ -1607,7 +1738,7 @@ Summary: ${formatSummary(results)}`);
1607
1738
 
1608
1739
  // src/cmds/connect.ts
1609
1740
  import pc7 from "picocolors";
1610
- import open2 from "open";
1741
+ import open3 from "open";
1611
1742
  import prompts6 from "prompts";
1612
1743
  async function connectCommand(provider, options = {}) {
1613
1744
  try {
@@ -1646,7 +1777,7 @@ Connecting to ${providerInfo.displayName}...
1646
1777
  const startTime = /* @__PURE__ */ new Date();
1647
1778
  console.log(pc7.gray("Opening browser for authorization..."));
1648
1779
  console.log(pc7.gray(`If the browser doesn't open, visit: ${authUrl}`));
1649
- await open2(authUrl).catch(() => {
1780
+ await open3(authUrl).catch(() => {
1650
1781
  });
1651
1782
  console.log(pc7.gray("Waiting for authorization..."));
1652
1783
  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.1",
4
4
  "description": "One link to all your secrets",
5
5
  "type": "module",
6
6
  "bin": {