@keywaysh/cli 0.1.3 → 0.1.5

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 +232 -43
  2. package/package.json +17 -13
package/dist/cli.js CHANGED
@@ -102,7 +102,7 @@ var INTERNAL_POSTHOG_HOST = "https://eu.i.posthog.com";
102
102
  // package.json
103
103
  var package_default = {
104
104
  name: "@keywaysh/cli",
105
- version: "0.1.3",
105
+ version: "0.1.5",
106
106
  description: "One link to all your secrets",
107
107
  type: "module",
108
108
  bin: {
@@ -146,6 +146,7 @@ var package_default = {
146
146
  node: ">=18.0.0"
147
147
  },
148
148
  dependencies: {
149
+ "balanced-match": "^3.0.1",
149
150
  commander: "^14.0.0",
150
151
  conf: "^15.0.2",
151
152
  open: "^11.0.0",
@@ -154,6 +155,7 @@ var package_default = {
154
155
  prompts: "^2.4.2"
155
156
  },
156
157
  devDependencies: {
158
+ "@types/balanced-match": "^3.0.2",
157
159
  "@types/node": "^24.2.0",
158
160
  "@types/prompts": "^2.4.9",
159
161
  tsup: "^8.5.0",
@@ -491,6 +493,37 @@ async function executeSync(accessToken, repoFullName, options) {
491
493
  const wrapped = await handleResponse(response);
492
494
  return wrapped.data;
493
495
  }
496
+ async function connectWithToken(accessToken, provider, providerToken) {
497
+ const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations/${provider}/connect`, {
498
+ method: "POST",
499
+ headers: {
500
+ "Content-Type": "application/json",
501
+ "User-Agent": USER_AGENT,
502
+ Authorization: `Bearer ${accessToken}`
503
+ },
504
+ body: JSON.stringify({ token: providerToken })
505
+ });
506
+ const wrapped = await handleResponse(response);
507
+ return wrapped.data;
508
+ }
509
+ async function checkVaultExists(accessToken, repoFullName) {
510
+ const [owner, repo] = repoFullName.split("/");
511
+ try {
512
+ const response = await fetchWithTimeout(
513
+ `${API_BASE_URL}/v1/vaults/${owner}/${repo}`,
514
+ {
515
+ method: "GET",
516
+ headers: {
517
+ "User-Agent": USER_AGENT,
518
+ Authorization: `Bearer ${accessToken}`
519
+ }
520
+ }
521
+ );
522
+ return response.ok;
523
+ } catch {
524
+ return false;
525
+ }
526
+ }
494
527
  async function getVaultEnvironments(accessToken, repoFullName) {
495
528
  const [owner, repo] = repoFullName.split("/");
496
529
  try {
@@ -655,23 +688,76 @@ import fs3 from "fs";
655
688
  import path3 from "path";
656
689
  import prompts from "prompts";
657
690
  import pc2 from "picocolors";
691
+ import balanced from "balanced-match";
658
692
  function generateBadge(repo) {
659
693
  return `[![Keyway Secrets](https://www.keyway.sh/badge.svg?repo=${repo})](https://www.keyway.sh/vaults/${repo})`;
660
694
  }
695
+ var BADGE_PREFIX = /\[!\[[^\]]*\]\([^)]*\)\]\(/g;
696
+ var H1_PATTERN = /^#\s+/;
697
+ var CODE_FENCE = /^```/;
698
+ function findLastBadgeEnd(line) {
699
+ let lastEnd = -1;
700
+ let match;
701
+ BADGE_PREFIX.lastIndex = 0;
702
+ while ((match = BADGE_PREFIX.exec(line)) !== null) {
703
+ const prefixEnd = match.index + match[0].length - 1;
704
+ const remainder = line.substring(prefixEnd);
705
+ const balancedMatch = balanced("(", ")", remainder);
706
+ if (balancedMatch) {
707
+ lastEnd = prefixEnd + balancedMatch.end + 1;
708
+ }
709
+ }
710
+ return lastEnd;
711
+ }
661
712
  function insertBadgeIntoReadme(readmeContent, badge) {
662
713
  if (readmeContent.includes("keyway.sh/badge.svg")) {
663
714
  return readmeContent;
664
715
  }
665
716
  const lines = readmeContent.split(/\r?\n/);
666
- const titleIndex = lines.findIndex((line) => /^#(?!#)\s+/.test(line.trim()));
667
- if (titleIndex !== -1) {
668
- const before = lines.slice(0, titleIndex + 1);
669
- const after = lines.slice(titleIndex + 1);
717
+ let inCodeBlock = false;
718
+ let inHtmlComment = false;
719
+ let lastBadgeLine = -1;
720
+ let lastBadgeEndIndex = -1;
721
+ let firstH1Line = -1;
722
+ for (let i = 0; i < lines.length; i++) {
723
+ const line = lines[i];
724
+ const trimmed = line.trim();
725
+ if (CODE_FENCE.test(trimmed)) {
726
+ inCodeBlock = !inCodeBlock;
727
+ continue;
728
+ }
729
+ if (inCodeBlock) continue;
730
+ if (trimmed.includes("<!--")) inHtmlComment = true;
731
+ if (trimmed.includes("-->")) {
732
+ inHtmlComment = false;
733
+ continue;
734
+ }
735
+ if (inHtmlComment) continue;
736
+ BADGE_PREFIX.lastIndex = 0;
737
+ if (BADGE_PREFIX.test(line)) {
738
+ lastBadgeLine = i;
739
+ lastBadgeEndIndex = findLastBadgeEnd(line);
740
+ }
741
+ if (firstH1Line === -1 && H1_PATTERN.test(line)) {
742
+ firstH1Line = i;
743
+ }
744
+ }
745
+ if (lastBadgeLine >= 0 && lastBadgeEndIndex > 0) {
746
+ const line = lines[lastBadgeLine];
747
+ lines[lastBadgeLine] = line.slice(0, lastBadgeEndIndex) + " " + badge + line.slice(lastBadgeEndIndex);
748
+ return lines.join("\n");
749
+ }
750
+ if (firstH1Line >= 0) {
751
+ const before = lines.slice(0, firstH1Line + 1);
752
+ const after = lines.slice(firstH1Line + 1);
670
753
  while (after.length > 0 && after[0].trim() === "") {
671
754
  after.shift();
672
755
  }
673
- const newLines = [...before, "", badge, "", ...after];
674
- return newLines.join("\n");
756
+ if (after.length > 0) {
757
+ return [...before, "", badge, "", ...after].join("\n");
758
+ } else {
759
+ return [...before, "", badge, ""].join("\n");
760
+ }
675
761
  }
676
762
  return `${badge}
677
763
 
@@ -1315,6 +1401,15 @@ async function initCommand(options = {}) {
1315
1401
  allowPrompt: options.loginPrompt !== false
1316
1402
  });
1317
1403
  trackEvent(AnalyticsEvents.CLI_INIT, { repoFullName, githubAppInstalled: true });
1404
+ const vaultExists = await checkVaultExists(accessToken, repoFullName);
1405
+ if (vaultExists) {
1406
+ console.log(pc5.green("\n\u2713 Already initialized!\n"));
1407
+ console.log(` ${pc5.yellow("\u2192")} Run ${pc5.cyan("keyway push")} to sync your secrets`);
1408
+ console.log(` ${pc5.blue("\u2394")} Dashboard: ${pc5.underline(dashboardLink)}`);
1409
+ console.log("");
1410
+ await shutdownAnalytics();
1411
+ return;
1412
+ }
1318
1413
  await initVault(repoFullName, accessToken);
1319
1414
  console.log(pc5.green("\u2713 Vault created!"));
1320
1415
  try {
@@ -1785,6 +1880,86 @@ Summary: ${formatSummary(results)}`);
1785
1880
  import pc8 from "picocolors";
1786
1881
  import open3 from "open";
1787
1882
  import prompts6 from "prompts";
1883
+ var TOKEN_AUTH_PROVIDERS = ["railway"];
1884
+ function getTokenCreationUrl(provider) {
1885
+ switch (provider) {
1886
+ case "railway":
1887
+ return "https://railway.com/account/tokens";
1888
+ default:
1889
+ return "";
1890
+ }
1891
+ }
1892
+ async function connectWithTokenFlow(accessToken, provider, displayName) {
1893
+ const tokenUrl = getTokenCreationUrl(provider);
1894
+ console.log(pc8.gray(`Create a ${displayName} API Token at:`));
1895
+ console.log(pc8.cyan(`\u2192 ${tokenUrl}
1896
+ `));
1897
+ if (provider === "railway") {
1898
+ console.log(pc8.yellow("Tip: Select the workspace containing your projects."));
1899
+ console.log(pc8.yellow(` Do NOT use "No workspace" - it won't have access to your projects.
1900
+ `));
1901
+ }
1902
+ const { token } = await prompts6({
1903
+ type: "password",
1904
+ name: "token",
1905
+ message: `${displayName} API Token:`
1906
+ });
1907
+ if (!token) {
1908
+ console.log(pc8.gray("Cancelled."));
1909
+ return false;
1910
+ }
1911
+ console.log(pc8.gray("\nValidating token..."));
1912
+ try {
1913
+ const result = await connectWithToken(accessToken, provider, token);
1914
+ if (result.success) {
1915
+ console.log(pc8.green(`
1916
+ \u2713 Connected to ${displayName}!`));
1917
+ console.log(pc8.gray(` Account: ${result.user.username}`));
1918
+ if (result.user.teamName) {
1919
+ console.log(pc8.gray(` Team: ${result.user.teamName}`));
1920
+ }
1921
+ return true;
1922
+ } else {
1923
+ console.log(pc8.red("\n\u2717 Connection failed."));
1924
+ return false;
1925
+ }
1926
+ } catch (error) {
1927
+ const message = error instanceof Error ? error.message : "Token validation failed";
1928
+ console.log(pc8.red(`
1929
+ \u2717 ${message}`));
1930
+ return false;
1931
+ }
1932
+ }
1933
+ async function connectWithOAuthFlow(accessToken, provider, displayName) {
1934
+ const authUrl = getProviderAuthUrl(provider);
1935
+ const startTime = /* @__PURE__ */ new Date();
1936
+ console.log(pc8.gray("Opening browser for authorization..."));
1937
+ console.log(pc8.gray(`If the browser doesn't open, visit: ${authUrl}`));
1938
+ await open3(authUrl).catch(() => {
1939
+ });
1940
+ console.log(pc8.gray("Waiting for authorization..."));
1941
+ const maxAttempts = 60;
1942
+ let attempts = 0;
1943
+ while (attempts < maxAttempts) {
1944
+ await new Promise((resolve) => setTimeout(resolve, 5e3));
1945
+ attempts++;
1946
+ try {
1947
+ const { connections } = await getConnections(accessToken);
1948
+ const newConn = connections.find(
1949
+ (c) => c.provider === provider && new Date(c.createdAt) > startTime
1950
+ );
1951
+ if (newConn) {
1952
+ console.log(pc8.green(`
1953
+ \u2713 Connected to ${displayName}!`));
1954
+ return true;
1955
+ }
1956
+ } catch {
1957
+ }
1958
+ }
1959
+ console.log(pc8.red("\n\u2717 Authorization timeout."));
1960
+ console.log(pc8.gray("Run `keyway connections` to check if the connection was established."));
1961
+ return false;
1962
+ }
1788
1963
  async function connectCommand(provider, options = {}) {
1789
1964
  try {
1790
1965
  const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
@@ -1818,36 +1993,11 @@ async function connectCommand(provider, options = {}) {
1818
1993
  console.log(pc8.blue(`
1819
1994
  Connecting to ${providerInfo.displayName}...
1820
1995
  `));
1821
- const authUrl = getProviderAuthUrl(provider.toLowerCase());
1822
- const startTime = /* @__PURE__ */ new Date();
1823
- console.log(pc8.gray("Opening browser for authorization..."));
1824
- console.log(pc8.gray(`If the browser doesn't open, visit: ${authUrl}`));
1825
- await open3(authUrl).catch(() => {
1826
- });
1827
- console.log(pc8.gray("Waiting for authorization..."));
1828
- const maxAttempts = 60;
1829
- let attempts = 0;
1830
1996
  let connected = false;
1831
- while (attempts < maxAttempts) {
1832
- await new Promise((resolve) => setTimeout(resolve, 5e3));
1833
- attempts++;
1834
- try {
1835
- const { connections: connections2 } = await getConnections(accessToken);
1836
- const newConn = connections2.find(
1837
- (c) => c.provider === provider.toLowerCase() && new Date(c.createdAt) > startTime
1838
- );
1839
- if (newConn) {
1840
- connected = true;
1841
- console.log(pc8.green(`
1842
- \u2713 Connected to ${providerInfo.displayName}!`));
1843
- break;
1844
- }
1845
- } catch {
1846
- }
1847
- }
1848
- if (!connected) {
1849
- console.log(pc8.red("\n\u2717 Authorization timeout."));
1850
- console.log(pc8.gray("Run `keyway connections` to check if the connection was established."));
1997
+ if (TOKEN_AUTH_PROVIDERS.includes(provider.toLowerCase())) {
1998
+ connected = await connectWithTokenFlow(accessToken, provider.toLowerCase(), providerInfo.displayName);
1999
+ } else {
2000
+ connected = await connectWithOAuthFlow(accessToken, provider.toLowerCase(), providerInfo.displayName);
1851
2001
  }
1852
2002
  trackEvent(AnalyticsEvents.CLI_CONNECT, {
1853
2003
  provider: provider.toLowerCase(),
@@ -1871,7 +2021,7 @@ async function connectionsCommand(options = {}) {
1871
2021
  if (connections.length === 0) {
1872
2022
  console.log(pc8.gray("No provider connections found."));
1873
2023
  console.log(pc8.gray("\nConnect to a provider with: keyway connect <provider>"));
1874
- console.log(pc8.gray("Available providers: vercel"));
2024
+ console.log(pc8.gray("Available providers: vercel, railway"));
1875
2025
  return;
1876
2026
  }
1877
2027
  console.log(pc8.blue("\n\u{1F4E1} Provider Connections\n"));
@@ -1941,6 +2091,25 @@ function mapToVercelEnvironment(keywayEnv) {
1941
2091
  };
1942
2092
  return mapping[keywayEnv.toLowerCase()] || "production";
1943
2093
  }
2094
+ function mapToRailwayEnvironment(keywayEnv) {
2095
+ const mapping = {
2096
+ production: "production",
2097
+ staging: "staging",
2098
+ dev: "development",
2099
+ development: "development"
2100
+ };
2101
+ return mapping[keywayEnv.toLowerCase()] || "production";
2102
+ }
2103
+ function mapToProviderEnvironment(provider, keywayEnv) {
2104
+ switch (provider.toLowerCase()) {
2105
+ case "vercel":
2106
+ return mapToVercelEnvironment(keywayEnv);
2107
+ case "railway":
2108
+ return mapToRailwayEnvironment(keywayEnv);
2109
+ default:
2110
+ return keywayEnv;
2111
+ }
2112
+ }
1944
2113
  function findMatchingProject(projects, repoFullName) {
1945
2114
  const repoFullNameLower = repoFullName.toLowerCase();
1946
2115
  const repoName = repoFullName.split("/")[1]?.toLowerCase();
@@ -2018,12 +2187,32 @@ async function syncCommand(provider, options = {}) {
2018
2187
  process.exit(1);
2019
2188
  }
2020
2189
  console.log(pc9.gray(`Repository: ${repoFullName}`));
2021
- const { connections } = await getConnections(accessToken);
2022
- const connection = connections.find((c) => c.provider === provider.toLowerCase());
2190
+ let { connections } = await getConnections(accessToken);
2191
+ let connection = connections.find((c) => c.provider === provider.toLowerCase());
2023
2192
  if (!connection) {
2024
- console.error(pc9.red(`Not connected to ${provider}.`));
2025
- console.log(pc9.gray(`Run: keyway connect ${provider}`));
2026
- process.exit(1);
2193
+ const providerDisplayName = provider.charAt(0).toUpperCase() + provider.slice(1);
2194
+ console.log(pc9.yellow(`
2195
+ Not connected to ${providerDisplayName}.`));
2196
+ const { shouldConnect } = await prompts7({
2197
+ type: "confirm",
2198
+ name: "shouldConnect",
2199
+ message: `Connect to ${providerDisplayName} now?`,
2200
+ initial: true
2201
+ });
2202
+ if (!shouldConnect) {
2203
+ console.log(pc9.gray("Cancelled."));
2204
+ process.exit(0);
2205
+ }
2206
+ await connectCommand(provider, { loginPrompt: false });
2207
+ const refreshed = await getConnections(accessToken);
2208
+ connections = refreshed.connections;
2209
+ connection = connections.find((c) => c.provider === provider.toLowerCase());
2210
+ if (!connection) {
2211
+ console.error(pc9.red(`
2212
+ Connection to ${providerDisplayName} failed.`));
2213
+ process.exit(1);
2214
+ }
2215
+ console.log("");
2027
2216
  }
2028
2217
  const { projects } = await getConnectionProjects(accessToken, connection.id);
2029
2218
  if (projects.length === 0) {
@@ -2151,7 +2340,7 @@ async function syncCommand(provider, options = {}) {
2151
2340
  }
2152
2341
  keywayEnv = selectedEnv;
2153
2342
  if (!options.providerEnv) {
2154
- providerEnv = mapToVercelEnvironment(keywayEnv);
2343
+ providerEnv = mapToProviderEnvironment(provider, keywayEnv);
2155
2344
  }
2156
2345
  }
2157
2346
  if (needsDirectionPrompt) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keywaysh/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "One link to all your secrets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,18 @@
10
10
  "files": [
11
11
  "dist"
12
12
  ],
13
+ "scripts": {
14
+ "dev": "pnpm exec tsx src/cli.ts",
15
+ "dev:local": "NODE_TLS_REJECT_UNAUTHORIZED=0 KEYWAY_API_URL=https://localhost/api pnpm exec tsx src/cli.ts",
16
+ "build": "pnpm exec tsup",
17
+ "build:watch": "pnpm exec tsup --watch",
18
+ "prepublishOnly": "pnpm run build",
19
+ "test": "pnpm exec vitest run",
20
+ "test:watch": "pnpm exec vitest",
21
+ "release": "npm version patch && git push && git push --tags",
22
+ "release:minor": "npm version minor && git push && git push --tags",
23
+ "release:major": "npm version major && git push && git push --tags"
24
+ },
13
25
  "keywords": [
14
26
  "secrets",
15
27
  "env",
@@ -27,10 +39,12 @@
27
39
  "bugs": {
28
40
  "url": "https://github.com/keywaysh/cli/issues"
29
41
  },
42
+ "packageManager": "pnpm@10.6.1",
30
43
  "engines": {
31
44
  "node": ">=18.0.0"
32
45
  },
33
46
  "dependencies": {
47
+ "balanced-match": "^3.0.1",
34
48
  "commander": "^14.0.0",
35
49
  "conf": "^15.0.2",
36
50
  "open": "^11.0.0",
@@ -39,22 +53,12 @@
39
53
  "prompts": "^2.4.2"
40
54
  },
41
55
  "devDependencies": {
56
+ "@types/balanced-match": "^3.0.2",
42
57
  "@types/node": "^24.2.0",
43
58
  "@types/prompts": "^2.4.9",
44
59
  "tsup": "^8.5.0",
45
60
  "tsx": "^4.20.3",
46
61
  "typescript": "^5.9.2",
47
62
  "vitest": "^3.2.4"
48
- },
49
- "scripts": {
50
- "dev": "pnpm exec tsx src/cli.ts",
51
- "dev:local": "NODE_TLS_REJECT_UNAUTHORIZED=0 KEYWAY_API_URL=https://localhost/api pnpm exec tsx src/cli.ts",
52
- "build": "pnpm exec tsup",
53
- "build:watch": "pnpm exec tsup --watch",
54
- "test": "pnpm exec vitest run",
55
- "test:watch": "pnpm exec vitest",
56
- "release": "npm version patch && git push && git push --tags",
57
- "release:minor": "npm version minor && git push && git push --tags",
58
- "release:major": "npm version major && git push && git push --tags"
59
63
  }
60
- }
64
+ }