@keywaysh/cli 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +387 -302
- package/package.json +4 -1
package/dist/cli.js
CHANGED
|
@@ -8,12 +8,11 @@ import {
|
|
|
8
8
|
|
|
9
9
|
// src/cli.ts
|
|
10
10
|
import { Command } from "commander";
|
|
11
|
-
import
|
|
11
|
+
import pc11 from "picocolors";
|
|
12
12
|
|
|
13
13
|
// src/cmds/init.ts
|
|
14
|
-
import
|
|
14
|
+
import pc6 from "picocolors";
|
|
15
15
|
import prompts4 from "prompts";
|
|
16
|
-
import open2 from "open";
|
|
17
16
|
|
|
18
17
|
// src/utils/git.ts
|
|
19
18
|
import { execSync } from "child_process";
|
|
@@ -102,7 +101,7 @@ var INTERNAL_POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
|
102
101
|
// package.json
|
|
103
102
|
var package_default = {
|
|
104
103
|
name: "@keywaysh/cli",
|
|
105
|
-
version: "0.1.
|
|
104
|
+
version: "0.1.7",
|
|
106
105
|
description: "One link to all your secrets",
|
|
107
106
|
type: "module",
|
|
108
107
|
bin: {
|
|
@@ -120,6 +119,7 @@ var package_default = {
|
|
|
120
119
|
prepublishOnly: "pnpm run build",
|
|
121
120
|
test: "pnpm exec vitest run",
|
|
122
121
|
"test:watch": "pnpm exec vitest",
|
|
122
|
+
"test:coverage": "pnpm exec vitest run --coverage",
|
|
123
123
|
release: "npm version patch && git push && git push --tags",
|
|
124
124
|
"release:minor": "npm version minor && git push && git push --tags",
|
|
125
125
|
"release:major": "npm version major && git push && git push --tags"
|
|
@@ -158,6 +158,8 @@ var package_default = {
|
|
|
158
158
|
"@types/balanced-match": "^3.0.2",
|
|
159
159
|
"@types/node": "^24.2.0",
|
|
160
160
|
"@types/prompts": "^2.4.9",
|
|
161
|
+
"@vitest/coverage-v8": "^3.0.0",
|
|
162
|
+
msw: "^2.12.4",
|
|
161
163
|
tsup: "^8.5.0",
|
|
162
164
|
tsx: "^4.20.3",
|
|
163
165
|
typescript: "^5.9.2",
|
|
@@ -442,6 +444,29 @@ async function getSyncStatus(accessToken, repoFullName, connectionId, projectId,
|
|
|
442
444
|
const wrapped = await handleResponse(response);
|
|
443
445
|
return wrapped.data;
|
|
444
446
|
}
|
|
447
|
+
async function getSyncDiff(accessToken, repoFullName, options) {
|
|
448
|
+
const [owner, repo] = repoFullName.split("/");
|
|
449
|
+
const params = new URLSearchParams({
|
|
450
|
+
connectionId: options.connectionId,
|
|
451
|
+
projectId: options.projectId,
|
|
452
|
+
keywayEnvironment: options.keywayEnvironment || "production",
|
|
453
|
+
providerEnvironment: options.providerEnvironment || "production"
|
|
454
|
+
});
|
|
455
|
+
const response = await fetchWithTimeout(
|
|
456
|
+
`${API_BASE_URL}/v1/integrations/vaults/${owner}/${repo}/sync/diff?${params}`,
|
|
457
|
+
{
|
|
458
|
+
method: "GET",
|
|
459
|
+
headers: {
|
|
460
|
+
"User-Agent": USER_AGENT,
|
|
461
|
+
Authorization: `Bearer ${accessToken}`
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
6e4
|
|
465
|
+
// 60 seconds
|
|
466
|
+
);
|
|
467
|
+
const wrapped = await handleResponse(response);
|
|
468
|
+
return wrapped.data;
|
|
469
|
+
}
|
|
445
470
|
async function getSyncPreview(accessToken, repoFullName, options) {
|
|
446
471
|
const [owner, repo] = repoFullName.split("/");
|
|
447
472
|
const params = new URLSearchParams({
|
|
@@ -776,8 +801,8 @@ function findReadmePath(cwd) {
|
|
|
776
801
|
async function ensureReadme(repoName, cwd) {
|
|
777
802
|
const existing = findReadmePath(cwd);
|
|
778
803
|
if (existing) return existing;
|
|
779
|
-
const
|
|
780
|
-
if (!
|
|
804
|
+
const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
|
|
805
|
+
if (!isInteractive2) {
|
|
781
806
|
console.log(pc2.yellow('No README found. Run "keyway readme add-badge" from a repo with a README.'));
|
|
782
807
|
return null;
|
|
783
808
|
}
|
|
@@ -828,22 +853,49 @@ async function addBadgeToReadme(silent = false) {
|
|
|
828
853
|
}
|
|
829
854
|
|
|
830
855
|
// src/cmds/push.ts
|
|
831
|
-
import
|
|
856
|
+
import pc5 from "picocolors";
|
|
832
857
|
import fs4 from "fs";
|
|
833
858
|
import path4 from "path";
|
|
834
859
|
import prompts3 from "prompts";
|
|
835
860
|
|
|
836
861
|
// src/cmds/login.ts
|
|
837
|
-
import
|
|
862
|
+
import pc4 from "picocolors";
|
|
838
863
|
import readline from "readline";
|
|
839
|
-
import open from "open";
|
|
840
864
|
import prompts2 from "prompts";
|
|
865
|
+
|
|
866
|
+
// src/utils/helpers.ts
|
|
867
|
+
import pc3 from "picocolors";
|
|
868
|
+
import open from "open";
|
|
841
869
|
function sleep(ms) {
|
|
842
870
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
843
871
|
}
|
|
872
|
+
async function openUrl(url) {
|
|
873
|
+
console.log(pc3.gray(`
|
|
874
|
+
Open this URL in your browser:
|
|
875
|
+
${url}
|
|
876
|
+
`));
|
|
877
|
+
await open(url).catch(() => {
|
|
878
|
+
});
|
|
879
|
+
}
|
|
844
880
|
function isInteractive() {
|
|
845
881
|
return Boolean(process.stdout.isTTY && process.stdin.isTTY && !process.env.CI);
|
|
846
882
|
}
|
|
883
|
+
function showUpgradePrompt(message, upgradeUrl) {
|
|
884
|
+
console.log("");
|
|
885
|
+
console.log(pc3.dim("\u2500".repeat(50)));
|
|
886
|
+
console.log("");
|
|
887
|
+
console.log(` ${pc3.yellow("\u26A1")} ${pc3.bold("Plan Limit Reached")}`);
|
|
888
|
+
console.log("");
|
|
889
|
+
console.log(pc3.white(` ${message}`));
|
|
890
|
+
console.log("");
|
|
891
|
+
console.log(` ${pc3.cyan("Upgrade now \u2192")} ${pc3.underline(upgradeUrl)}`);
|
|
892
|
+
console.log("");
|
|
893
|
+
console.log(pc3.dim("\u2500".repeat(50)));
|
|
894
|
+
console.log("");
|
|
895
|
+
}
|
|
896
|
+
var MAX_CONSECUTIVE_ERRORS = 5;
|
|
897
|
+
|
|
898
|
+
// src/cmds/login.ts
|
|
847
899
|
async function promptYesNo(question, defaultYes = true) {
|
|
848
900
|
return new Promise((resolve) => {
|
|
849
901
|
const rl = readline.createInterface({
|
|
@@ -861,52 +913,60 @@ async function promptYesNo(question, defaultYes = true) {
|
|
|
861
913
|
});
|
|
862
914
|
}
|
|
863
915
|
async function runLoginFlow() {
|
|
864
|
-
console.log(
|
|
916
|
+
console.log(pc4.blue("\u{1F510} Starting Keyway login...\n"));
|
|
865
917
|
const repoName = detectGitRepo();
|
|
866
918
|
const start = await startDeviceLogin(repoName);
|
|
867
919
|
const verifyUrl = start.verificationUriComplete || start.verificationUri;
|
|
868
920
|
if (!verifyUrl) {
|
|
869
921
|
throw new Error("Missing verification URL from the auth server.");
|
|
870
922
|
}
|
|
871
|
-
console.log(`Code: ${
|
|
923
|
+
console.log(`Code: ${pc4.bold(pc4.green(start.userCode))}`);
|
|
924
|
+
await openUrl(verifyUrl);
|
|
872
925
|
console.log("Waiting for auth...");
|
|
873
|
-
open(verifyUrl).catch(() => {
|
|
874
|
-
console.log(pc3.gray(`Open this URL in your browser: ${verifyUrl}`));
|
|
875
|
-
});
|
|
876
926
|
const pollIntervalMs = (start.interval ?? 5) * 1e3;
|
|
877
927
|
const maxTimeoutMs = Math.min((start.expiresIn ?? 900) * 1e3, 30 * 60 * 1e3);
|
|
878
928
|
const startTime = Date.now();
|
|
929
|
+
let consecutiveErrors = 0;
|
|
879
930
|
while (true) {
|
|
880
931
|
if (Date.now() - startTime > maxTimeoutMs) {
|
|
881
932
|
throw new Error('Login timed out. Please run "keyway login" again.');
|
|
882
933
|
}
|
|
883
934
|
await sleep(pollIntervalMs);
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
method: "device",
|
|
895
|
-
repo: repoName
|
|
896
|
-
});
|
|
897
|
-
if (result.githubLogin) {
|
|
898
|
-
identifyUser(result.githubLogin, {
|
|
899
|
-
github_username: result.githubLogin,
|
|
900
|
-
login_method: "device"
|
|
935
|
+
try {
|
|
936
|
+
const result = await pollDeviceLogin(start.deviceCode);
|
|
937
|
+
consecutiveErrors = 0;
|
|
938
|
+
if (result.status === "pending") {
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
if (result.status === "approved" && result.keywayToken) {
|
|
942
|
+
await saveAuthToken(result.keywayToken, {
|
|
943
|
+
githubLogin: result.githubLogin,
|
|
944
|
+
expiresAt: result.expiresAt
|
|
901
945
|
});
|
|
946
|
+
trackEvent(AnalyticsEvents.CLI_LOGIN, {
|
|
947
|
+
method: "device",
|
|
948
|
+
repo: repoName
|
|
949
|
+
});
|
|
950
|
+
if (result.githubLogin) {
|
|
951
|
+
identifyUser(result.githubLogin, {
|
|
952
|
+
github_username: result.githubLogin,
|
|
953
|
+
login_method: "device"
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
console.log(pc4.green("\n\u2713 Login successful"));
|
|
957
|
+
if (result.githubLogin) {
|
|
958
|
+
console.log(`Authenticated GitHub user: ${pc4.cyan(result.githubLogin)}`);
|
|
959
|
+
}
|
|
960
|
+
return result.keywayToken;
|
|
902
961
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
962
|
+
throw new Error(result.message || "Authentication failed");
|
|
963
|
+
} catch (error) {
|
|
964
|
+
consecutiveErrors++;
|
|
965
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
966
|
+
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
967
|
+
throw new Error(`Login failed after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${errorMsg}`);
|
|
906
968
|
}
|
|
907
|
-
return result.keywayToken;
|
|
908
969
|
}
|
|
909
|
-
throw new Error(result.message || "Authentication failed");
|
|
910
970
|
}
|
|
911
971
|
}
|
|
912
972
|
async function ensureLogin(options = {}) {
|
|
@@ -915,7 +975,7 @@ async function ensureLogin(options = {}) {
|
|
|
915
975
|
return envToken;
|
|
916
976
|
}
|
|
917
977
|
if (process.env.GITHUB_TOKEN && !process.env.KEYWAY_TOKEN) {
|
|
918
|
-
console.warn(
|
|
978
|
+
console.warn(pc4.yellow("Note: GITHUB_TOKEN found but not used. Set KEYWAY_TOKEN for Keyway authentication."));
|
|
919
979
|
}
|
|
920
980
|
const stored = await getStoredAuth();
|
|
921
981
|
if (stored?.keywayToken) {
|
|
@@ -935,16 +995,13 @@ async function ensureLogin(options = {}) {
|
|
|
935
995
|
async function runTokenLogin() {
|
|
936
996
|
const repoName = detectGitRepo();
|
|
937
997
|
if (repoName) {
|
|
938
|
-
console.log(`\u{1F4C1} Detected: ${
|
|
998
|
+
console.log(`\u{1F4C1} Detected: ${pc4.cyan(repoName)}`);
|
|
939
999
|
}
|
|
940
1000
|
const description = repoName ? `Keyway CLI for ${repoName}` : "Keyway CLI";
|
|
941
1001
|
const url = `https://github.com/settings/personal-access-tokens/new?description=${encodeURIComponent(description)}`;
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
});
|
|
946
|
-
console.log(pc3.gray("Select the detected repo (or scope manually)."));
|
|
947
|
-
console.log(pc3.gray("Permissions: Metadata \u2192 Read-only; Account permissions: None."));
|
|
1002
|
+
await openUrl(url);
|
|
1003
|
+
console.log(pc4.gray("Select the detected repo (or scope manually)."));
|
|
1004
|
+
console.log(pc4.gray("Permissions: Metadata \u2192 Read-only; Account permissions: None."));
|
|
948
1005
|
const { token } = await prompts2(
|
|
949
1006
|
{
|
|
950
1007
|
type: "password",
|
|
@@ -981,7 +1038,7 @@ async function runTokenLogin() {
|
|
|
981
1038
|
github_username: validation.username,
|
|
982
1039
|
login_method: "pat"
|
|
983
1040
|
});
|
|
984
|
-
console.log(
|
|
1041
|
+
console.log(pc4.green("\u2705 Authenticated"), `as ${pc4.cyan(`@${validation.username}`)}`);
|
|
985
1042
|
return trimmedToken;
|
|
986
1043
|
}
|
|
987
1044
|
async function loginCommand(options = {}) {
|
|
@@ -997,15 +1054,15 @@ async function loginCommand(options = {}) {
|
|
|
997
1054
|
command: "login",
|
|
998
1055
|
error: truncateMessage(message)
|
|
999
1056
|
});
|
|
1000
|
-
console.error(
|
|
1057
|
+
console.error(pc4.red(`
|
|
1001
1058
|
\u2717 ${message}`));
|
|
1002
1059
|
process.exit(1);
|
|
1003
1060
|
}
|
|
1004
1061
|
}
|
|
1005
1062
|
async function logoutCommand() {
|
|
1006
1063
|
clearAuth();
|
|
1007
|
-
console.log(
|
|
1008
|
-
console.log(
|
|
1064
|
+
console.log(pc4.green("\u2713 Logged out of Keyway"));
|
|
1065
|
+
console.log(pc4.gray(`Auth cache cleared: ${getAuthFilePath()}`));
|
|
1009
1066
|
}
|
|
1010
1067
|
|
|
1011
1068
|
// src/cmds/push.ts
|
|
@@ -1022,7 +1079,7 @@ function discoverEnvCandidates(cwd) {
|
|
|
1022
1079
|
const entries = fs4.readdirSync(cwd);
|
|
1023
1080
|
const hasEnvLocal = entries.includes(".env.local");
|
|
1024
1081
|
if (hasEnvLocal) {
|
|
1025
|
-
console.log(
|
|
1082
|
+
console.log(pc5.gray("\u2139\uFE0F Detected .env.local \u2014 not synced by design (machine-specific secrets)"));
|
|
1026
1083
|
}
|
|
1027
1084
|
const candidates = entries.filter((name) => name.startsWith(".env") && name !== ".env.local").map((name) => {
|
|
1028
1085
|
const fullPath = path4.join(cwd, name);
|
|
@@ -1048,18 +1105,21 @@ function discoverEnvCandidates(cwd) {
|
|
|
1048
1105
|
}
|
|
1049
1106
|
async function pushCommand(options) {
|
|
1050
1107
|
try {
|
|
1051
|
-
console.log(
|
|
1052
|
-
const
|
|
1108
|
+
console.log(pc5.blue("\u{1F510} Pushing secrets to Keyway...\n"));
|
|
1109
|
+
const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
|
|
1053
1110
|
let environment = options.env;
|
|
1054
1111
|
let envFile = options.file;
|
|
1055
1112
|
const candidates = discoverEnvCandidates(process.cwd());
|
|
1113
|
+
if (candidates.length === 0 && !envFile) {
|
|
1114
|
+
throw new Error("No .env file found. Create a .env file first, or use --file <path> to specify one.");
|
|
1115
|
+
}
|
|
1056
1116
|
if (environment && !envFile) {
|
|
1057
1117
|
const match = candidates.find((c) => c.env === environment);
|
|
1058
1118
|
if (match) {
|
|
1059
1119
|
envFile = match.file;
|
|
1060
1120
|
}
|
|
1061
1121
|
}
|
|
1062
|
-
if (!environment && !envFile &&
|
|
1122
|
+
if (!environment && !envFile && isInteractive2 && candidates.length > 0) {
|
|
1063
1123
|
const { choice } = await prompts3(
|
|
1064
1124
|
{
|
|
1065
1125
|
type: "select",
|
|
@@ -1111,34 +1171,9 @@ async function pushCommand(options) {
|
|
|
1111
1171
|
if (!envFile) {
|
|
1112
1172
|
envFile = ".env";
|
|
1113
1173
|
}
|
|
1114
|
-
|
|
1174
|
+
const envFilePath = path4.resolve(process.cwd(), envFile);
|
|
1115
1175
|
if (!fs4.existsSync(envFilePath)) {
|
|
1116
|
-
|
|
1117
|
-
throw new Error(`File not found: ${envFile}. Provide --file <path> or run interactively to choose a file.`);
|
|
1118
|
-
}
|
|
1119
|
-
const { newPath } = await prompts3(
|
|
1120
|
-
{
|
|
1121
|
-
type: "text",
|
|
1122
|
-
name: "newPath",
|
|
1123
|
-
message: `File not found: ${envFile}. Enter an env file path to use:`,
|
|
1124
|
-
validate: (value) => {
|
|
1125
|
-
if (!value || typeof value !== "string") return "Path is required";
|
|
1126
|
-
const resolved = path4.resolve(process.cwd(), value);
|
|
1127
|
-
if (!fs4.existsSync(resolved)) return `File not found: ${value}`;
|
|
1128
|
-
return true;
|
|
1129
|
-
}
|
|
1130
|
-
},
|
|
1131
|
-
{
|
|
1132
|
-
onCancel: () => {
|
|
1133
|
-
throw new Error("Push cancelled (no env file provided).");
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
);
|
|
1137
|
-
if (!newPath || typeof newPath !== "string") {
|
|
1138
|
-
throw new Error("Push cancelled (no env file provided).");
|
|
1139
|
-
}
|
|
1140
|
-
envFile = newPath.trim();
|
|
1141
|
-
envFilePath = path4.resolve(process.cwd(), envFile);
|
|
1176
|
+
throw new Error(`File not found: ${envFile}`);
|
|
1142
1177
|
}
|
|
1143
1178
|
const content = fs4.readFileSync(envFilePath, "utf-8");
|
|
1144
1179
|
if (content.trim().length === 0) {
|
|
@@ -1148,14 +1183,14 @@ async function pushCommand(options) {
|
|
|
1148
1183
|
const trimmed = line.trim();
|
|
1149
1184
|
return trimmed.length > 0 && !trimmed.startsWith("#");
|
|
1150
1185
|
});
|
|
1151
|
-
console.log(`File: ${
|
|
1152
|
-
console.log(`Environment: ${
|
|
1153
|
-
console.log(`Variables: ${
|
|
1186
|
+
console.log(`File: ${pc5.cyan(envFile)}`);
|
|
1187
|
+
console.log(`Environment: ${pc5.cyan(environment)}`);
|
|
1188
|
+
console.log(`Variables: ${pc5.cyan(lines.length.toString())}`);
|
|
1154
1189
|
const repoFullName = getCurrentRepoFullName();
|
|
1155
|
-
console.log(`Repository: ${
|
|
1190
|
+
console.log(`Repository: ${pc5.cyan(repoFullName)}`);
|
|
1156
1191
|
if (!options.yes) {
|
|
1157
|
-
const
|
|
1158
|
-
if (!
|
|
1192
|
+
const isInteractive3 = process.stdin.isTTY && process.stdout.isTTY;
|
|
1193
|
+
if (!isInteractive3) {
|
|
1159
1194
|
throw new Error("Confirmation required. Re-run with --yes in non-interactive environments.");
|
|
1160
1195
|
}
|
|
1161
1196
|
const { confirm } = await prompts3(
|
|
@@ -1172,7 +1207,7 @@ async function pushCommand(options) {
|
|
|
1172
1207
|
}
|
|
1173
1208
|
);
|
|
1174
1209
|
if (!confirm) {
|
|
1175
|
-
console.log(
|
|
1210
|
+
console.log(pc5.yellow("Push aborted."));
|
|
1176
1211
|
return;
|
|
1177
1212
|
}
|
|
1178
1213
|
}
|
|
@@ -1184,20 +1219,22 @@ async function pushCommand(options) {
|
|
|
1184
1219
|
});
|
|
1185
1220
|
console.log("\nUploading secrets...");
|
|
1186
1221
|
const response = await pushSecrets(repoFullName, environment, content, accessToken);
|
|
1187
|
-
console.log(
|
|
1222
|
+
console.log(pc5.green("\n\u2713 " + response.message));
|
|
1188
1223
|
if (response.stats) {
|
|
1189
1224
|
const { created, updated, deleted } = response.stats;
|
|
1190
1225
|
const parts = [];
|
|
1191
|
-
if (created > 0) parts.push(
|
|
1192
|
-
if (updated > 0) parts.push(
|
|
1193
|
-
if (deleted > 0) parts.push(
|
|
1226
|
+
if (created > 0) parts.push(pc5.green(`+${created} created`));
|
|
1227
|
+
if (updated > 0) parts.push(pc5.yellow(`~${updated} updated`));
|
|
1228
|
+
if (deleted > 0) parts.push(pc5.red(`-${deleted} deleted`));
|
|
1194
1229
|
if (parts.length > 0) {
|
|
1195
1230
|
console.log(`Stats: ${parts.join(", ")}`);
|
|
1196
1231
|
}
|
|
1197
1232
|
}
|
|
1198
1233
|
console.log(`
|
|
1199
1234
|
Your secrets are now encrypted and stored securely.`);
|
|
1200
|
-
|
|
1235
|
+
const dashboardLink = `https://www.keyway.sh/dashboard/vaults/${repoFullName}`;
|
|
1236
|
+
console.log(`
|
|
1237
|
+
${pc5.blue("\u2394")} Dashboard: ${pc5.underline(dashboardLink)}`);
|
|
1201
1238
|
await shutdownAnalytics();
|
|
1202
1239
|
} catch (error) {
|
|
1203
1240
|
let message;
|
|
@@ -1210,13 +1247,18 @@ Your secrets are now encrypted and stored securely.`);
|
|
|
1210
1247
|
const availableEnvs = envNotFoundMatch[2];
|
|
1211
1248
|
message = `Environment '${requestedEnv}' does not exist in this vault.`;
|
|
1212
1249
|
hint = `Available environments: ${availableEnvs}
|
|
1213
|
-
Use ${
|
|
1250
|
+
Use ${pc5.cyan(`keyway push --env <environment>`)} to specify one, or create '${requestedEnv}' via the dashboard.`;
|
|
1214
1251
|
}
|
|
1215
|
-
if (error.statusCode === 403 && error.upgradeUrl) {
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1252
|
+
if (error.statusCode === 403 && (error.upgradeUrl || message.toLowerCase().includes("read-only"))) {
|
|
1253
|
+
const upgradeMessage = message.toLowerCase().includes("read-only") ? "This vault is read-only on your current plan." : message;
|
|
1254
|
+
const upgradeUrl = error.upgradeUrl || "https://keyway.sh/settings";
|
|
1255
|
+
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
1256
|
+
command: "push",
|
|
1257
|
+
error: upgradeMessage
|
|
1258
|
+
});
|
|
1259
|
+
await shutdownAnalytics();
|
|
1260
|
+
showUpgradePrompt(upgradeMessage, upgradeUrl);
|
|
1261
|
+
process.exit(1);
|
|
1220
1262
|
}
|
|
1221
1263
|
} else if (error instanceof Error) {
|
|
1222
1264
|
message = truncateMessage(error.message);
|
|
@@ -1228,10 +1270,10 @@ Use ${pc4.cyan(`keyway push --env <environment>`)} to specify one, or create '${
|
|
|
1228
1270
|
error: message
|
|
1229
1271
|
});
|
|
1230
1272
|
await shutdownAnalytics();
|
|
1231
|
-
console.error(
|
|
1273
|
+
console.error(pc5.red(`
|
|
1232
1274
|
\u2717 ${message}`));
|
|
1233
1275
|
if (hint) {
|
|
1234
|
-
console.error(
|
|
1276
|
+
console.error(pc5.gray(`
|
|
1235
1277
|
${hint}`));
|
|
1236
1278
|
}
|
|
1237
1279
|
process.exit(1);
|
|
@@ -1242,12 +1284,6 @@ ${hint}`));
|
|
|
1242
1284
|
var DASHBOARD_URL = "https://www.keyway.sh/dashboard/vaults";
|
|
1243
1285
|
var POLL_INTERVAL_MS = 3e3;
|
|
1244
1286
|
var POLL_TIMEOUT_MS = 12e4;
|
|
1245
|
-
function sleep2(ms) {
|
|
1246
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1247
|
-
}
|
|
1248
|
-
function isInteractive2() {
|
|
1249
|
-
return Boolean(process.stdout.isTTY && process.stdin.isTTY && !process.env.CI);
|
|
1250
|
-
}
|
|
1251
1287
|
async function ensureLoginAndGitHubApp(repoFullName, options = {}) {
|
|
1252
1288
|
const [repoOwner, repoName] = repoFullName.split("/");
|
|
1253
1289
|
const envToken = process.env.KEYWAY_TOKEN;
|
|
@@ -1266,12 +1302,12 @@ async function ensureLoginAndGitHubApp(repoFullName, options = {}) {
|
|
|
1266
1302
|
}
|
|
1267
1303
|
}
|
|
1268
1304
|
const allowPrompt = options.allowPrompt !== false;
|
|
1269
|
-
if (!allowPrompt || !
|
|
1305
|
+
if (!allowPrompt || !isInteractive()) {
|
|
1270
1306
|
throw new Error('No Keyway session found. Run "keyway login" to authenticate.');
|
|
1271
1307
|
}
|
|
1272
1308
|
console.log("");
|
|
1273
|
-
console.log(
|
|
1274
|
-
console.log(
|
|
1309
|
+
console.log(pc6.gray(" Keyway uses a GitHub App for secure access."));
|
|
1310
|
+
console.log(pc6.gray(" Installing the app will also log you in."));
|
|
1275
1311
|
console.log("");
|
|
1276
1312
|
const { shouldProceed } = await prompts4({
|
|
1277
1313
|
type: "confirm",
|
|
@@ -1284,16 +1320,15 @@ async function ensureLoginAndGitHubApp(repoFullName, options = {}) {
|
|
|
1284
1320
|
}
|
|
1285
1321
|
const deviceStart = await startDeviceLogin(repoFullName);
|
|
1286
1322
|
const installUrl = deviceStart.githubAppInstallUrl || "https://github.com/apps/keyway/installations/new";
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
console.log("");
|
|
1290
|
-
console.log(pc5.blue("\u23F3 Waiting for installation & authorization..."));
|
|
1291
|
-
console.log(pc5.gray(" (Press Ctrl+C to cancel)\n"));
|
|
1323
|
+
await openUrl(installUrl);
|
|
1324
|
+
console.log(pc6.blue("\u23F3 Waiting for installation & authorization..."));
|
|
1325
|
+
console.log(pc6.gray(" (Press Ctrl+C to cancel)\n"));
|
|
1292
1326
|
const pollIntervalMs = Math.max((deviceStart.interval ?? 5) * 1e3, POLL_INTERVAL_MS);
|
|
1293
1327
|
const startTime = Date.now();
|
|
1294
1328
|
let accessToken = null;
|
|
1329
|
+
let consecutiveErrors = 0;
|
|
1295
1330
|
while (Date.now() - startTime < POLL_TIMEOUT_MS) {
|
|
1296
|
-
await
|
|
1331
|
+
await sleep(pollIntervalMs);
|
|
1297
1332
|
try {
|
|
1298
1333
|
if (!accessToken) {
|
|
1299
1334
|
const result = await pollDeviceLogin(deviceStart.deviceCode);
|
|
@@ -1303,7 +1338,7 @@ async function ensureLoginAndGitHubApp(repoFullName, options = {}) {
|
|
|
1303
1338
|
githubLogin: result.githubLogin,
|
|
1304
1339
|
expiresAt: result.expiresAt
|
|
1305
1340
|
});
|
|
1306
|
-
console.log(
|
|
1341
|
+
console.log(pc6.green("\u2713 Signed in!"));
|
|
1307
1342
|
if (result.githubLogin) {
|
|
1308
1343
|
identifyUser(result.githubLogin, {
|
|
1309
1344
|
github_username: result.githubLogin,
|
|
@@ -1315,18 +1350,24 @@ async function ensureLoginAndGitHubApp(repoFullName, options = {}) {
|
|
|
1315
1350
|
if (accessToken) {
|
|
1316
1351
|
const installStatus = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
|
|
1317
1352
|
if (installStatus.installed) {
|
|
1318
|
-
console.log(
|
|
1353
|
+
console.log(pc6.green("\u2713 GitHub App installed!"));
|
|
1319
1354
|
console.log("");
|
|
1320
1355
|
return accessToken;
|
|
1321
1356
|
}
|
|
1322
1357
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1358
|
+
consecutiveErrors = 0;
|
|
1359
|
+
process.stdout.write(pc6.gray("."));
|
|
1360
|
+
} catch (error) {
|
|
1361
|
+
consecutiveErrors++;
|
|
1362
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
1363
|
+
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
1364
|
+
throw new Error(`Setup failed after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${errorMsg}`);
|
|
1365
|
+
}
|
|
1325
1366
|
}
|
|
1326
1367
|
}
|
|
1327
1368
|
console.log("");
|
|
1328
|
-
console.log(
|
|
1329
|
-
console.log(
|
|
1369
|
+
console.log(pc6.yellow("\u26A0 Timed out waiting for setup."));
|
|
1370
|
+
console.log(pc6.gray(` Install the GitHub App: ${installUrl}`));
|
|
1330
1371
|
throw new Error("Setup timed out. Please try again.");
|
|
1331
1372
|
}
|
|
1332
1373
|
async function ensureGitHubAppInstalledOnly(repoFullName, accessToken) {
|
|
@@ -1336,7 +1377,7 @@ async function ensureGitHubAppInstalledOnly(repoFullName, accessToken) {
|
|
|
1336
1377
|
status = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
|
|
1337
1378
|
} catch (error) {
|
|
1338
1379
|
if (error instanceof APIError && error.statusCode === 401) {
|
|
1339
|
-
console.log(
|
|
1380
|
+
console.log(pc6.yellow("\n\u26A0 Session expired or invalid. Clearing credentials..."));
|
|
1340
1381
|
const { clearAuth: clearAuth2 } = await import("./auth-QLPQ24HZ.js");
|
|
1341
1382
|
clearAuth2();
|
|
1342
1383
|
return null;
|
|
@@ -1347,13 +1388,13 @@ async function ensureGitHubAppInstalledOnly(repoFullName, accessToken) {
|
|
|
1347
1388
|
return accessToken;
|
|
1348
1389
|
}
|
|
1349
1390
|
console.log("");
|
|
1350
|
-
console.log(
|
|
1391
|
+
console.log(pc6.yellow("\u26A0 GitHub App not installed for this repository"));
|
|
1351
1392
|
console.log("");
|
|
1352
|
-
console.log(
|
|
1353
|
-
console.log(
|
|
1393
|
+
console.log(pc6.gray(" The Keyway GitHub App is required to securely manage secrets."));
|
|
1394
|
+
console.log(pc6.gray(" It only requests minimal permissions (repository metadata)."));
|
|
1354
1395
|
console.log("");
|
|
1355
|
-
if (!
|
|
1356
|
-
console.log(
|
|
1396
|
+
if (!isInteractive()) {
|
|
1397
|
+
console.log(pc6.gray(` Install the Keyway GitHub App: ${status.installUrl}`));
|
|
1357
1398
|
throw new Error("GitHub App installation required.");
|
|
1358
1399
|
}
|
|
1359
1400
|
const { shouldInstall } = await prompts4({
|
|
@@ -1363,67 +1404,72 @@ async function ensureGitHubAppInstalledOnly(repoFullName, accessToken) {
|
|
|
1363
1404
|
initial: true
|
|
1364
1405
|
});
|
|
1365
1406
|
if (!shouldInstall) {
|
|
1366
|
-
console.log(
|
|
1407
|
+
console.log(pc6.gray(`
|
|
1367
1408
|
You can install later: ${status.installUrl}`));
|
|
1368
1409
|
throw new Error("GitHub App installation required.");
|
|
1369
1410
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
console.log("");
|
|
1373
|
-
console.log(pc5.blue("\u23F3 Waiting for GitHub App installation..."));
|
|
1374
|
-
console.log(pc5.gray(" (Press Ctrl+C to cancel)\n"));
|
|
1411
|
+
await openUrl(status.installUrl);
|
|
1412
|
+
console.log(pc6.blue("\u23F3 Waiting for GitHub App installation..."));
|
|
1413
|
+
console.log(pc6.gray(" (Press Ctrl+C to cancel)\n"));
|
|
1375
1414
|
const startTime = Date.now();
|
|
1415
|
+
let consecutiveErrors = 0;
|
|
1376
1416
|
while (Date.now() - startTime < POLL_TIMEOUT_MS) {
|
|
1377
|
-
await
|
|
1417
|
+
await sleep(POLL_INTERVAL_MS);
|
|
1378
1418
|
try {
|
|
1379
1419
|
const pollStatus = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
|
|
1380
1420
|
if (pollStatus.installed) {
|
|
1381
|
-
console.log(
|
|
1421
|
+
console.log(pc6.green("\u2713 GitHub App installed!"));
|
|
1382
1422
|
console.log("");
|
|
1383
1423
|
return accessToken;
|
|
1384
1424
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1425
|
+
consecutiveErrors = 0;
|
|
1426
|
+
process.stdout.write(pc6.gray("."));
|
|
1427
|
+
} catch (error) {
|
|
1428
|
+
consecutiveErrors++;
|
|
1429
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
1430
|
+
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
1431
|
+
throw new Error(`Installation check failed after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${errorMsg}`);
|
|
1432
|
+
}
|
|
1387
1433
|
}
|
|
1388
1434
|
}
|
|
1389
1435
|
console.log("");
|
|
1390
|
-
console.log(
|
|
1391
|
-
console.log(
|
|
1436
|
+
console.log(pc6.yellow("\u26A0 Timed out waiting for installation."));
|
|
1437
|
+
console.log(pc6.gray(` You can install the GitHub App later: ${status.installUrl}`));
|
|
1392
1438
|
throw new Error("GitHub App installation timed out.");
|
|
1393
1439
|
}
|
|
1394
1440
|
async function initCommand(options = {}) {
|
|
1395
1441
|
try {
|
|
1396
1442
|
const repoFullName = getCurrentRepoFullName();
|
|
1397
1443
|
const dashboardLink = `${DASHBOARD_URL}/${repoFullName}`;
|
|
1398
|
-
console.log(
|
|
1399
|
-
console.log(` ${
|
|
1444
|
+
console.log(pc6.blue("\u{1F510} Initializing Keyway vault...\n"));
|
|
1445
|
+
console.log(` ${pc6.gray("Repository:")} ${pc6.white(repoFullName)}`);
|
|
1400
1446
|
const accessToken = await ensureLoginAndGitHubApp(repoFullName, {
|
|
1401
1447
|
allowPrompt: options.loginPrompt !== false
|
|
1402
1448
|
});
|
|
1403
1449
|
trackEvent(AnalyticsEvents.CLI_INIT, { repoFullName, githubAppInstalled: true });
|
|
1404
1450
|
const vaultExists = await checkVaultExists(accessToken, repoFullName);
|
|
1405
1451
|
if (vaultExists) {
|
|
1406
|
-
console.log(
|
|
1407
|
-
console.log(` ${
|
|
1408
|
-
console.log(` ${
|
|
1452
|
+
console.log(pc6.green("\n\u2713 Already initialized!\n"));
|
|
1453
|
+
console.log(` ${pc6.yellow("\u2192")} Run ${pc6.cyan("keyway push")} to sync your secrets`);
|
|
1454
|
+
console.log(` ${pc6.blue("\u2394")} Dashboard: ${pc6.underline(dashboardLink)}`);
|
|
1409
1455
|
console.log("");
|
|
1410
1456
|
await shutdownAnalytics();
|
|
1411
1457
|
return;
|
|
1412
1458
|
}
|
|
1413
1459
|
await initVault(repoFullName, accessToken);
|
|
1414
|
-
console.log(
|
|
1460
|
+
console.log(pc6.green("\u2713 Vault created!"));
|
|
1415
1461
|
try {
|
|
1416
1462
|
const badgeAdded = await addBadgeToReadme(true);
|
|
1417
1463
|
if (badgeAdded) {
|
|
1418
|
-
console.log(
|
|
1464
|
+
console.log(pc6.green("\u2713 Badge added to README.md"));
|
|
1419
1465
|
}
|
|
1420
1466
|
} catch {
|
|
1421
1467
|
}
|
|
1422
1468
|
console.log("");
|
|
1423
1469
|
const envCandidates = discoverEnvCandidates(process.cwd());
|
|
1424
|
-
const
|
|
1425
|
-
if (envCandidates.length > 0 &&
|
|
1426
|
-
console.log(
|
|
1470
|
+
const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
|
|
1471
|
+
if (envCandidates.length > 0 && isInteractive2) {
|
|
1472
|
+
console.log(pc6.gray(` Found ${envCandidates.length} env file(s): ${envCandidates.map((c) => c.file).join(", ")}
|
|
1427
1473
|
`));
|
|
1428
1474
|
const { shouldPush } = await prompts4({
|
|
1429
1475
|
type: "confirm",
|
|
@@ -1437,42 +1483,32 @@ async function initCommand(options = {}) {
|
|
|
1437
1483
|
return;
|
|
1438
1484
|
}
|
|
1439
1485
|
}
|
|
1440
|
-
console.log(
|
|
1486
|
+
console.log(pc6.dim("\u2500".repeat(50)));
|
|
1441
1487
|
console.log("");
|
|
1442
1488
|
if (envCandidates.length === 0) {
|
|
1443
|
-
console.log(
|
|
1444
|
-
console.log(` ${
|
|
1489
|
+
console.log(`${pc6.yellow("\u26A0")} No .env file found - your vault is empty`);
|
|
1490
|
+
console.log(` Next: Create ${pc6.cyan(".env")} and run ${pc6.cyan("keyway push")}
|
|
1445
1491
|
`);
|
|
1446
1492
|
} else {
|
|
1447
|
-
console.log(` ${
|
|
1493
|
+
console.log(` ${pc6.yellow("\u2192")} Run ${pc6.cyan("keyway push")} to sync your secrets
|
|
1448
1494
|
`);
|
|
1449
1495
|
}
|
|
1450
|
-
console.log(` ${
|
|
1496
|
+
console.log(` ${pc6.blue("\u2394")} Dashboard: ${pc6.underline(dashboardLink)}`);
|
|
1451
1497
|
console.log("");
|
|
1452
1498
|
await shutdownAnalytics();
|
|
1453
1499
|
} catch (error) {
|
|
1454
1500
|
if (error instanceof APIError) {
|
|
1455
1501
|
if (error.statusCode === 409) {
|
|
1456
|
-
console.log(
|
|
1457
|
-
console.log(` ${
|
|
1458
|
-
console.log(` ${
|
|
1502
|
+
console.log(pc6.green("\n\u2713 Already initialized!\n"));
|
|
1503
|
+
console.log(` ${pc6.yellow("\u2192")} Run ${pc6.cyan("keyway push")} to sync your secrets`);
|
|
1504
|
+
console.log(` ${pc6.blue("\u2394")} Dashboard: ${pc6.underline(`${DASHBOARD_URL}/${getCurrentRepoFullName()}`)}`);
|
|
1459
1505
|
console.log("");
|
|
1460
1506
|
await shutdownAnalytics();
|
|
1461
1507
|
return;
|
|
1462
1508
|
}
|
|
1463
1509
|
if (error.error === "Plan Limit Reached" || error.upgradeUrl) {
|
|
1464
1510
|
const upgradeUrl = error.upgradeUrl || "https://keyway.sh/pricing";
|
|
1465
|
-
|
|
1466
|
-
console.log(pc5.dim("\u2500".repeat(50)));
|
|
1467
|
-
console.log("");
|
|
1468
|
-
console.log(` ${pc5.yellow("\u26A1")} ${pc5.bold("Plan Limit Reached")}`);
|
|
1469
|
-
console.log("");
|
|
1470
|
-
console.log(pc5.white(` ${error.message}`));
|
|
1471
|
-
console.log("");
|
|
1472
|
-
console.log(` ${pc5.cyan("Upgrade now \u2192")} ${pc5.underline(upgradeUrl)}`);
|
|
1473
|
-
console.log("");
|
|
1474
|
-
console.log(pc5.dim("\u2500".repeat(50)));
|
|
1475
|
-
console.log("");
|
|
1511
|
+
showUpgradePrompt(error.message, upgradeUrl);
|
|
1476
1512
|
await shutdownAnalytics();
|
|
1477
1513
|
process.exit(1);
|
|
1478
1514
|
}
|
|
@@ -1483,14 +1519,14 @@ async function initCommand(options = {}) {
|
|
|
1483
1519
|
error: message
|
|
1484
1520
|
});
|
|
1485
1521
|
await shutdownAnalytics();
|
|
1486
|
-
console.error(
|
|
1522
|
+
console.error(pc6.red(`
|
|
1487
1523
|
\u2717 ${message}`));
|
|
1488
1524
|
process.exit(1);
|
|
1489
1525
|
}
|
|
1490
1526
|
}
|
|
1491
1527
|
|
|
1492
1528
|
// src/cmds/pull.ts
|
|
1493
|
-
import
|
|
1529
|
+
import pc7 from "picocolors";
|
|
1494
1530
|
import fs5 from "fs";
|
|
1495
1531
|
import path5 from "path";
|
|
1496
1532
|
import prompts5 from "prompts";
|
|
@@ -1498,10 +1534,10 @@ async function pullCommand(options) {
|
|
|
1498
1534
|
try {
|
|
1499
1535
|
const environment = options.env || "development";
|
|
1500
1536
|
const envFile = options.file || ".env";
|
|
1501
|
-
console.log(
|
|
1502
|
-
console.log(`Environment: ${
|
|
1537
|
+
console.log(pc7.blue("\u{1F510} Pulling secrets from Keyway...\n"));
|
|
1538
|
+
console.log(`Environment: ${pc7.cyan(environment)}`);
|
|
1503
1539
|
const repoFullName = getCurrentRepoFullName();
|
|
1504
|
-
console.log(`Repository: ${
|
|
1540
|
+
console.log(`Repository: ${pc7.cyan(repoFullName)}`);
|
|
1505
1541
|
const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
|
|
1506
1542
|
trackEvent(AnalyticsEvents.CLI_PULL, {
|
|
1507
1543
|
repoFullName,
|
|
@@ -1511,11 +1547,11 @@ async function pullCommand(options) {
|
|
|
1511
1547
|
const response = await pullSecrets(repoFullName, environment, accessToken);
|
|
1512
1548
|
const envFilePath = path5.resolve(process.cwd(), envFile);
|
|
1513
1549
|
if (fs5.existsSync(envFilePath)) {
|
|
1514
|
-
const
|
|
1550
|
+
const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
|
|
1515
1551
|
if (options.yes) {
|
|
1516
|
-
console.log(
|
|
1552
|
+
console.log(pc7.yellow(`
|
|
1517
1553
|
\u26A0 Overwriting existing file: ${envFile}`));
|
|
1518
|
-
} else if (!
|
|
1554
|
+
} else if (!isInteractive2) {
|
|
1519
1555
|
throw new Error(`File ${envFile} exists. Re-run with --yes to overwrite or choose a different --file.`);
|
|
1520
1556
|
} else {
|
|
1521
1557
|
const { confirm } = await prompts5(
|
|
@@ -1532,7 +1568,7 @@ async function pullCommand(options) {
|
|
|
1532
1568
|
}
|
|
1533
1569
|
);
|
|
1534
1570
|
if (!confirm) {
|
|
1535
|
-
console.log(
|
|
1571
|
+
console.log(pc7.yellow("Pull aborted."));
|
|
1536
1572
|
return;
|
|
1537
1573
|
}
|
|
1538
1574
|
}
|
|
@@ -1542,11 +1578,11 @@ async function pullCommand(options) {
|
|
|
1542
1578
|
const trimmed = line.trim();
|
|
1543
1579
|
return trimmed.length > 0 && !trimmed.startsWith("#");
|
|
1544
1580
|
});
|
|
1545
|
-
console.log(
|
|
1581
|
+
console.log(pc7.green(`
|
|
1546
1582
|
\u2713 Secrets downloaded successfully`));
|
|
1547
1583
|
console.log(`
|
|
1548
|
-
File: ${
|
|
1549
|
-
console.log(`Variables: ${
|
|
1584
|
+
File: ${pc7.cyan(envFile)}`);
|
|
1585
|
+
console.log(`Variables: ${pc7.cyan(lines.length.toString())}`);
|
|
1550
1586
|
await shutdownAnalytics();
|
|
1551
1587
|
} catch (error) {
|
|
1552
1588
|
const message = error instanceof APIError ? `API ${error.statusCode}: ${error.message}` : error instanceof Error ? truncateMessage(error.message) : "Unknown error";
|
|
@@ -1555,14 +1591,14 @@ File: ${pc6.cyan(envFile)}`);
|
|
|
1555
1591
|
error: message
|
|
1556
1592
|
});
|
|
1557
1593
|
await shutdownAnalytics();
|
|
1558
|
-
console.error(
|
|
1594
|
+
console.error(pc7.red(`
|
|
1559
1595
|
\u2717 ${message}`));
|
|
1560
1596
|
process.exit(1);
|
|
1561
1597
|
}
|
|
1562
1598
|
}
|
|
1563
1599
|
|
|
1564
1600
|
// src/cmds/doctor.ts
|
|
1565
|
-
import
|
|
1601
|
+
import pc8 from "picocolors";
|
|
1566
1602
|
|
|
1567
1603
|
// src/core/doctor.ts
|
|
1568
1604
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -1816,9 +1852,9 @@ async function runAllChecks(options = {}) {
|
|
|
1816
1852
|
// src/cmds/doctor.ts
|
|
1817
1853
|
function formatSummary(results) {
|
|
1818
1854
|
const parts = [
|
|
1819
|
-
|
|
1820
|
-
results.summary.warn > 0 ?
|
|
1821
|
-
results.summary.fail > 0 ?
|
|
1855
|
+
pc8.green(`${results.summary.pass} passed`),
|
|
1856
|
+
results.summary.warn > 0 ? pc8.yellow(`${results.summary.warn} warnings`) : null,
|
|
1857
|
+
results.summary.fail > 0 ? pc8.red(`${results.summary.fail} failed`) : null
|
|
1822
1858
|
].filter(Boolean);
|
|
1823
1859
|
return parts.join(", ");
|
|
1824
1860
|
}
|
|
@@ -1835,20 +1871,20 @@ async function doctorCommand(options = {}) {
|
|
|
1835
1871
|
process.stdout.write(JSON.stringify(results, null, 0) + "\n");
|
|
1836
1872
|
process.exit(results.exitCode);
|
|
1837
1873
|
}
|
|
1838
|
-
console.log(
|
|
1874
|
+
console.log(pc8.cyan("\n\u{1F50D} Keyway Doctor - Environment Check\n"));
|
|
1839
1875
|
results.checks.forEach((check) => {
|
|
1840
|
-
const icon = check.status === "pass" ?
|
|
1841
|
-
const detail = check.detail ?
|
|
1876
|
+
const icon = check.status === "pass" ? pc8.green("\u2713") : check.status === "warn" ? pc8.yellow("!") : pc8.red("\u2717");
|
|
1877
|
+
const detail = check.detail ? pc8.dim(` \u2014 ${check.detail}`) : "";
|
|
1842
1878
|
console.log(` ${icon} ${check.name}${detail}`);
|
|
1843
1879
|
});
|
|
1844
1880
|
console.log(`
|
|
1845
1881
|
Summary: ${formatSummary(results)}`);
|
|
1846
1882
|
if (results.summary.fail > 0) {
|
|
1847
|
-
console.log(
|
|
1883
|
+
console.log(pc8.red("\u26A0 Some checks failed. Please resolve the issues above before using Keyway."));
|
|
1848
1884
|
} else if (results.summary.warn > 0) {
|
|
1849
|
-
console.log(
|
|
1885
|
+
console.log(pc8.yellow("\u26A0 Some warnings detected. Keyway should work but consider addressing them."));
|
|
1850
1886
|
} else {
|
|
1851
|
-
console.log(
|
|
1887
|
+
console.log(pc8.green("\u2728 All checks passed! Your environment is ready for Keyway."));
|
|
1852
1888
|
}
|
|
1853
1889
|
process.exit(results.exitCode);
|
|
1854
1890
|
} catch (error) {
|
|
@@ -1869,7 +1905,7 @@ Summary: ${formatSummary(results)}`);
|
|
|
1869
1905
|
};
|
|
1870
1906
|
process.stdout.write(JSON.stringify(errorResult, null, 0) + "\n");
|
|
1871
1907
|
} else {
|
|
1872
|
-
console.error(
|
|
1908
|
+
console.error(pc8.red(`
|
|
1873
1909
|
\u2717 ${message}`));
|
|
1874
1910
|
}
|
|
1875
1911
|
process.exit(1);
|
|
@@ -1877,8 +1913,7 @@ Summary: ${formatSummary(results)}`);
|
|
|
1877
1913
|
}
|
|
1878
1914
|
|
|
1879
1915
|
// src/cmds/connect.ts
|
|
1880
|
-
import
|
|
1881
|
-
import open3 from "open";
|
|
1916
|
+
import pc9 from "picocolors";
|
|
1882
1917
|
import prompts6 from "prompts";
|
|
1883
1918
|
var TOKEN_AUTH_PROVIDERS = ["railway"];
|
|
1884
1919
|
function getTokenCreationUrl(provider) {
|
|
@@ -1891,41 +1926,38 @@ function getTokenCreationUrl(provider) {
|
|
|
1891
1926
|
}
|
|
1892
1927
|
async function connectWithTokenFlow(accessToken, provider, displayName) {
|
|
1893
1928
|
const tokenUrl = getTokenCreationUrl(provider);
|
|
1894
|
-
console.log(pc8.gray(`Create a ${displayName} API Token at:`));
|
|
1895
|
-
console.log(pc8.cyan(`\u2192 ${tokenUrl}
|
|
1896
|
-
`));
|
|
1897
1929
|
if (provider === "railway") {
|
|
1898
|
-
console.log(
|
|
1899
|
-
console.log(
|
|
1900
|
-
`));
|
|
1930
|
+
console.log(pc9.yellow("\nTip: Select the workspace containing your projects."));
|
|
1931
|
+
console.log(pc9.yellow(` Do NOT use "No workspace" - it won't have access to your projects.`));
|
|
1901
1932
|
}
|
|
1933
|
+
await openUrl(tokenUrl);
|
|
1902
1934
|
const { token } = await prompts6({
|
|
1903
1935
|
type: "password",
|
|
1904
1936
|
name: "token",
|
|
1905
1937
|
message: `${displayName} API Token:`
|
|
1906
1938
|
});
|
|
1907
1939
|
if (!token) {
|
|
1908
|
-
console.log(
|
|
1940
|
+
console.log(pc9.gray("Cancelled."));
|
|
1909
1941
|
return false;
|
|
1910
1942
|
}
|
|
1911
|
-
console.log(
|
|
1943
|
+
console.log(pc9.gray("\nValidating token..."));
|
|
1912
1944
|
try {
|
|
1913
1945
|
const result = await connectWithToken(accessToken, provider, token);
|
|
1914
1946
|
if (result.success) {
|
|
1915
|
-
console.log(
|
|
1947
|
+
console.log(pc9.green(`
|
|
1916
1948
|
\u2713 Connected to ${displayName}!`));
|
|
1917
|
-
console.log(
|
|
1949
|
+
console.log(pc9.gray(` Account: ${result.user.username}`));
|
|
1918
1950
|
if (result.user.teamName) {
|
|
1919
|
-
console.log(
|
|
1951
|
+
console.log(pc9.gray(` Team: ${result.user.teamName}`));
|
|
1920
1952
|
}
|
|
1921
1953
|
return true;
|
|
1922
1954
|
} else {
|
|
1923
|
-
console.log(
|
|
1955
|
+
console.log(pc9.red("\n\u2717 Connection failed."));
|
|
1924
1956
|
return false;
|
|
1925
1957
|
}
|
|
1926
1958
|
} catch (error) {
|
|
1927
1959
|
const message = error instanceof Error ? error.message : "Token validation failed";
|
|
1928
|
-
console.log(
|
|
1960
|
+
console.log(pc9.red(`
|
|
1929
1961
|
\u2717 ${message}`));
|
|
1930
1962
|
return false;
|
|
1931
1963
|
}
|
|
@@ -1933,11 +1965,8 @@ async function connectWithTokenFlow(accessToken, provider, displayName) {
|
|
|
1933
1965
|
async function connectWithOAuthFlow(accessToken, provider, displayName) {
|
|
1934
1966
|
const authUrl = getProviderAuthUrl(provider);
|
|
1935
1967
|
const startTime = /* @__PURE__ */ new Date();
|
|
1936
|
-
|
|
1937
|
-
console.log(
|
|
1938
|
-
await open3(authUrl).catch(() => {
|
|
1939
|
-
});
|
|
1940
|
-
console.log(pc8.gray("Waiting for authorization..."));
|
|
1968
|
+
await openUrl(authUrl);
|
|
1969
|
+
console.log(pc9.gray("Waiting for authorization..."));
|
|
1941
1970
|
const maxAttempts = 60;
|
|
1942
1971
|
let attempts = 0;
|
|
1943
1972
|
while (attempts < maxAttempts) {
|
|
@@ -1949,15 +1978,15 @@ async function connectWithOAuthFlow(accessToken, provider, displayName) {
|
|
|
1949
1978
|
(c) => c.provider === provider && new Date(c.createdAt) > startTime
|
|
1950
1979
|
);
|
|
1951
1980
|
if (newConn) {
|
|
1952
|
-
console.log(
|
|
1981
|
+
console.log(pc9.green(`
|
|
1953
1982
|
\u2713 Connected to ${displayName}!`));
|
|
1954
1983
|
return true;
|
|
1955
1984
|
}
|
|
1956
1985
|
} catch {
|
|
1957
1986
|
}
|
|
1958
1987
|
}
|
|
1959
|
-
console.log(
|
|
1960
|
-
console.log(
|
|
1988
|
+
console.log(pc9.red("\n\u2717 Authorization timeout."));
|
|
1989
|
+
console.log(pc9.gray("Run `keyway connections` to check if the connection was established."));
|
|
1961
1990
|
return false;
|
|
1962
1991
|
}
|
|
1963
1992
|
async function connectCommand(provider, options = {}) {
|
|
@@ -1967,13 +1996,13 @@ async function connectCommand(provider, options = {}) {
|
|
|
1967
1996
|
const providerInfo = providers.find((p) => p.name === provider.toLowerCase());
|
|
1968
1997
|
if (!providerInfo) {
|
|
1969
1998
|
const available = providers.map((p) => p.name).join(", ");
|
|
1970
|
-
console.error(
|
|
1971
|
-
console.log(
|
|
1999
|
+
console.error(pc9.red(`Unknown provider: ${provider}`));
|
|
2000
|
+
console.log(pc9.gray(`Available providers: ${available || "none"}`));
|
|
1972
2001
|
process.exit(1);
|
|
1973
2002
|
}
|
|
1974
2003
|
if (!providerInfo.configured) {
|
|
1975
|
-
console.error(
|
|
1976
|
-
console.log(
|
|
2004
|
+
console.error(pc9.red(`Provider ${providerInfo.displayName} is not configured on the server.`));
|
|
2005
|
+
console.log(pc9.gray("Contact your administrator to enable this integration."));
|
|
1977
2006
|
process.exit(1);
|
|
1978
2007
|
}
|
|
1979
2008
|
const { connections } = await getConnections(accessToken);
|
|
@@ -1986,11 +2015,11 @@ async function connectCommand(provider, options = {}) {
|
|
|
1986
2015
|
initial: false
|
|
1987
2016
|
});
|
|
1988
2017
|
if (!reconnect) {
|
|
1989
|
-
console.log(
|
|
2018
|
+
console.log(pc9.gray("Keeping existing connection."));
|
|
1990
2019
|
return;
|
|
1991
2020
|
}
|
|
1992
2021
|
}
|
|
1993
|
-
console.log(
|
|
2022
|
+
console.log(pc9.blue(`
|
|
1994
2023
|
Connecting to ${providerInfo.displayName}...
|
|
1995
2024
|
`));
|
|
1996
2025
|
let connected = false;
|
|
@@ -2009,7 +2038,7 @@ Connecting to ${providerInfo.displayName}...
|
|
|
2009
2038
|
command: "connect",
|
|
2010
2039
|
error: truncateMessage(message)
|
|
2011
2040
|
});
|
|
2012
|
-
console.error(
|
|
2041
|
+
console.error(pc9.red(`
|
|
2013
2042
|
\u2717 ${message}`));
|
|
2014
2043
|
process.exit(1);
|
|
2015
2044
|
}
|
|
@@ -2019,24 +2048,24 @@ async function connectionsCommand(options = {}) {
|
|
|
2019
2048
|
const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
|
|
2020
2049
|
const { connections } = await getConnections(accessToken);
|
|
2021
2050
|
if (connections.length === 0) {
|
|
2022
|
-
console.log(
|
|
2023
|
-
console.log(
|
|
2024
|
-
console.log(
|
|
2051
|
+
console.log(pc9.gray("No provider connections found."));
|
|
2052
|
+
console.log(pc9.gray("\nConnect to a provider with: keyway connect <provider>"));
|
|
2053
|
+
console.log(pc9.gray("Available providers: vercel, railway"));
|
|
2025
2054
|
return;
|
|
2026
2055
|
}
|
|
2027
|
-
console.log(
|
|
2056
|
+
console.log(pc9.blue("\n\u{1F4E1} Provider Connections\n"));
|
|
2028
2057
|
for (const conn of connections) {
|
|
2029
2058
|
const providerName = conn.provider.charAt(0).toUpperCase() + conn.provider.slice(1);
|
|
2030
|
-
const teamInfo = conn.providerTeamId ?
|
|
2059
|
+
const teamInfo = conn.providerTeamId ? pc9.gray(` (Team: ${conn.providerTeamId})`) : "";
|
|
2031
2060
|
const date = new Date(conn.createdAt).toLocaleDateString();
|
|
2032
|
-
console.log(` ${
|
|
2033
|
-
console.log(
|
|
2034
|
-
console.log(
|
|
2061
|
+
console.log(` ${pc9.green("\u25CF")} ${pc9.bold(providerName)}${teamInfo}`);
|
|
2062
|
+
console.log(pc9.gray(` Connected: ${date}`));
|
|
2063
|
+
console.log(pc9.gray(` ID: ${conn.id}`));
|
|
2035
2064
|
console.log("");
|
|
2036
2065
|
}
|
|
2037
2066
|
} catch (error) {
|
|
2038
2067
|
const message = error instanceof Error ? error.message : "Failed to list connections";
|
|
2039
|
-
console.error(
|
|
2068
|
+
console.error(pc9.red(`
|
|
2040
2069
|
\u2717 ${message}`));
|
|
2041
2070
|
process.exit(1);
|
|
2042
2071
|
}
|
|
@@ -2047,7 +2076,7 @@ async function disconnectCommand(provider, options = {}) {
|
|
|
2047
2076
|
const { connections } = await getConnections(accessToken);
|
|
2048
2077
|
const connection = connections.find((c) => c.provider === provider.toLowerCase());
|
|
2049
2078
|
if (!connection) {
|
|
2050
|
-
console.log(
|
|
2079
|
+
console.log(pc9.gray(`No connection found for provider: ${provider}`));
|
|
2051
2080
|
return;
|
|
2052
2081
|
}
|
|
2053
2082
|
const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
@@ -2058,11 +2087,11 @@ async function disconnectCommand(provider, options = {}) {
|
|
|
2058
2087
|
initial: false
|
|
2059
2088
|
});
|
|
2060
2089
|
if (!confirm) {
|
|
2061
|
-
console.log(
|
|
2090
|
+
console.log(pc9.gray("Cancelled."));
|
|
2062
2091
|
return;
|
|
2063
2092
|
}
|
|
2064
2093
|
await deleteConnection(accessToken, connection.id);
|
|
2065
|
-
console.log(
|
|
2094
|
+
console.log(pc9.green(`
|
|
2066
2095
|
\u2713 Disconnected from ${providerName}`));
|
|
2067
2096
|
trackEvent(AnalyticsEvents.CLI_DISCONNECT, {
|
|
2068
2097
|
provider: provider.toLowerCase()
|
|
@@ -2073,14 +2102,14 @@ async function disconnectCommand(provider, options = {}) {
|
|
|
2073
2102
|
command: "disconnect",
|
|
2074
2103
|
error: truncateMessage(message)
|
|
2075
2104
|
});
|
|
2076
|
-
console.error(
|
|
2105
|
+
console.error(pc9.red(`
|
|
2077
2106
|
\u2717 ${message}`));
|
|
2078
2107
|
process.exit(1);
|
|
2079
2108
|
}
|
|
2080
2109
|
}
|
|
2081
2110
|
|
|
2082
2111
|
// src/cmds/sync.ts
|
|
2083
|
-
import
|
|
2112
|
+
import pc10 from "picocolors";
|
|
2084
2113
|
import prompts7 from "prompts";
|
|
2085
2114
|
function mapToVercelEnvironment(keywayEnv) {
|
|
2086
2115
|
const mapping = {
|
|
@@ -2110,6 +2139,42 @@ function mapToProviderEnvironment(provider, keywayEnv) {
|
|
|
2110
2139
|
return keywayEnv;
|
|
2111
2140
|
}
|
|
2112
2141
|
}
|
|
2142
|
+
function displayDiffSummary(diff, providerName) {
|
|
2143
|
+
const totalDiff = diff.onlyInKeyway.length + diff.onlyInProvider.length + diff.different.length;
|
|
2144
|
+
if (totalDiff === 0 && diff.same.length > 0) {
|
|
2145
|
+
console.log(pc10.green(`
|
|
2146
|
+
\u2713 Already in sync (${diff.same.length} secrets)`));
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
console.log(pc10.blue("\n\u{1F4CA} Comparison Summary\n"));
|
|
2150
|
+
console.log(pc10.gray(` Keyway: ${diff.keywayCount} secrets | ${providerName}: ${diff.providerCount} secrets
|
|
2151
|
+
`));
|
|
2152
|
+
if (diff.onlyInKeyway.length > 0) {
|
|
2153
|
+
console.log(pc10.cyan(` \u2192 ${diff.onlyInKeyway.length} only in Keyway`));
|
|
2154
|
+
diff.onlyInKeyway.slice(0, 3).forEach((key) => console.log(pc10.gray(` ${key}`)));
|
|
2155
|
+
if (diff.onlyInKeyway.length > 3) {
|
|
2156
|
+
console.log(pc10.gray(` ... and ${diff.onlyInKeyway.length - 3} more`));
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
if (diff.onlyInProvider.length > 0) {
|
|
2160
|
+
console.log(pc10.magenta(` \u2190 ${diff.onlyInProvider.length} only in ${providerName}`));
|
|
2161
|
+
diff.onlyInProvider.slice(0, 3).forEach((key) => console.log(pc10.gray(` ${key}`)));
|
|
2162
|
+
if (diff.onlyInProvider.length > 3) {
|
|
2163
|
+
console.log(pc10.gray(` ... and ${diff.onlyInProvider.length - 3} more`));
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
if (diff.different.length > 0) {
|
|
2167
|
+
console.log(pc10.yellow(` \u2260 ${diff.different.length} with different values`));
|
|
2168
|
+
diff.different.slice(0, 3).forEach((key) => console.log(pc10.gray(` ${key}`)));
|
|
2169
|
+
if (diff.different.length > 3) {
|
|
2170
|
+
console.log(pc10.gray(` ... and ${diff.different.length - 3} more`));
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
if (diff.same.length > 0) {
|
|
2174
|
+
console.log(pc10.gray(` = ${diff.same.length} identical`));
|
|
2175
|
+
}
|
|
2176
|
+
console.log("");
|
|
2177
|
+
}
|
|
2113
2178
|
function findMatchingProject(projects, repoFullName) {
|
|
2114
2179
|
const repoFullNameLower = repoFullName.toLowerCase();
|
|
2115
2180
|
const repoName = repoFullName.split("/")[1]?.toLowerCase();
|
|
@@ -2149,11 +2214,11 @@ async function promptProjectSelection(projects, repoFullName) {
|
|
|
2149
2214
|
let title = p.name;
|
|
2150
2215
|
const badges = [];
|
|
2151
2216
|
if (p.linkedRepo?.toLowerCase() === repoFullName.toLowerCase()) {
|
|
2152
|
-
badges.push(
|
|
2217
|
+
badges.push(pc10.green("\u2190 linked"));
|
|
2153
2218
|
} else if (p.name.toLowerCase() === repoName) {
|
|
2154
|
-
badges.push(
|
|
2219
|
+
badges.push(pc10.green("\u2190 same name"));
|
|
2155
2220
|
} else if (p.linkedRepo) {
|
|
2156
|
-
badges.push(
|
|
2221
|
+
badges.push(pc10.gray(`\u2192 ${p.linkedRepo}`));
|
|
2157
2222
|
}
|
|
2158
2223
|
if (badges.length > 0) {
|
|
2159
2224
|
title = `${p.name} ${badges.join(" ")}`;
|
|
@@ -2167,7 +2232,7 @@ async function promptProjectSelection(projects, repoFullName) {
|
|
|
2167
2232
|
choices
|
|
2168
2233
|
});
|
|
2169
2234
|
if (!projectChoice) {
|
|
2170
|
-
console.log(
|
|
2235
|
+
console.log(pc10.gray("Cancelled."));
|
|
2171
2236
|
process.exit(0);
|
|
2172
2237
|
}
|
|
2173
2238
|
return projects.find((p) => p.id === projectChoice);
|
|
@@ -2175,23 +2240,23 @@ async function promptProjectSelection(projects, repoFullName) {
|
|
|
2175
2240
|
async function syncCommand(provider, options = {}) {
|
|
2176
2241
|
try {
|
|
2177
2242
|
if (options.pull && options.allowDelete) {
|
|
2178
|
-
console.error(
|
|
2179
|
-
console.log(
|
|
2243
|
+
console.error(pc10.red("Error: --allow-delete cannot be used with --pull"));
|
|
2244
|
+
console.log(pc10.gray("The --allow-delete flag is only for push operations."));
|
|
2180
2245
|
process.exit(1);
|
|
2181
2246
|
}
|
|
2182
2247
|
const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
|
|
2183
2248
|
const repoFullName = detectGitRepo();
|
|
2184
2249
|
if (!repoFullName) {
|
|
2185
|
-
console.error(
|
|
2186
|
-
console.log(
|
|
2250
|
+
console.error(pc10.red("Could not detect Git repository."));
|
|
2251
|
+
console.log(pc10.gray("Run this command from a Git repository directory."));
|
|
2187
2252
|
process.exit(1);
|
|
2188
2253
|
}
|
|
2189
|
-
console.log(
|
|
2254
|
+
console.log(pc10.gray(`Repository: ${repoFullName}`));
|
|
2190
2255
|
let { connections } = await getConnections(accessToken);
|
|
2191
2256
|
let connection = connections.find((c) => c.provider === provider.toLowerCase());
|
|
2192
2257
|
if (!connection) {
|
|
2193
2258
|
const providerDisplayName = provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
2194
|
-
console.log(
|
|
2259
|
+
console.log(pc10.yellow(`
|
|
2195
2260
|
Not connected to ${providerDisplayName}.`));
|
|
2196
2261
|
const { shouldConnect } = await prompts7({
|
|
2197
2262
|
type: "confirm",
|
|
@@ -2200,7 +2265,7 @@ Not connected to ${providerDisplayName}.`));
|
|
|
2200
2265
|
initial: true
|
|
2201
2266
|
});
|
|
2202
2267
|
if (!shouldConnect) {
|
|
2203
|
-
console.log(
|
|
2268
|
+
console.log(pc10.gray("Cancelled."));
|
|
2204
2269
|
process.exit(0);
|
|
2205
2270
|
}
|
|
2206
2271
|
await connectCommand(provider, { loginPrompt: false });
|
|
@@ -2208,7 +2273,7 @@ Not connected to ${providerDisplayName}.`));
|
|
|
2208
2273
|
connections = refreshed.connections;
|
|
2209
2274
|
connection = connections.find((c) => c.provider === provider.toLowerCase());
|
|
2210
2275
|
if (!connection) {
|
|
2211
|
-
console.error(
|
|
2276
|
+
console.error(pc10.red(`
|
|
2212
2277
|
Connection to ${providerDisplayName} failed.`));
|
|
2213
2278
|
process.exit(1);
|
|
2214
2279
|
}
|
|
@@ -2216,7 +2281,7 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2216
2281
|
}
|
|
2217
2282
|
const { projects } = await getConnectionProjects(accessToken, connection.id);
|
|
2218
2283
|
if (projects.length === 0) {
|
|
2219
|
-
console.error(
|
|
2284
|
+
console.error(pc10.red(`No projects found in your ${provider} account.`));
|
|
2220
2285
|
process.exit(1);
|
|
2221
2286
|
}
|
|
2222
2287
|
let selectedProject;
|
|
@@ -2225,21 +2290,21 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2225
2290
|
(p) => p.id === options.project || p.name.toLowerCase() === options.project?.toLowerCase()
|
|
2226
2291
|
);
|
|
2227
2292
|
if (!found) {
|
|
2228
|
-
console.error(
|
|
2229
|
-
console.log(
|
|
2230
|
-
projects.forEach((p) => console.log(
|
|
2293
|
+
console.error(pc10.red(`Project not found: ${options.project}`));
|
|
2294
|
+
console.log(pc10.gray("Available projects:"));
|
|
2295
|
+
projects.forEach((p) => console.log(pc10.gray(` - ${p.name}`)));
|
|
2231
2296
|
process.exit(1);
|
|
2232
2297
|
}
|
|
2233
2298
|
selectedProject = found;
|
|
2234
2299
|
if (!projectMatchesRepo(selectedProject, repoFullName)) {
|
|
2235
2300
|
console.log("");
|
|
2236
|
-
console.log(
|
|
2237
|
-
console.log(
|
|
2238
|
-
console.log(
|
|
2239
|
-
console.log(
|
|
2240
|
-
console.log(
|
|
2301
|
+
console.log(pc10.yellow("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
|
|
2302
|
+
console.log(pc10.yellow("\u2502 \u26A0\uFE0F WARNING: Project does not match current repository \u2502"));
|
|
2303
|
+
console.log(pc10.yellow("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
|
|
2304
|
+
console.log(pc10.yellow(` Current repo: ${repoFullName}`));
|
|
2305
|
+
console.log(pc10.yellow(` Selected project: ${selectedProject.name}`));
|
|
2241
2306
|
if (selectedProject.linkedRepo) {
|
|
2242
|
-
console.log(
|
|
2307
|
+
console.log(pc10.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
|
|
2243
2308
|
}
|
|
2244
2309
|
console.log("");
|
|
2245
2310
|
}
|
|
@@ -2248,9 +2313,9 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2248
2313
|
if (autoMatch && (autoMatch.matchType === "linked_repo" || autoMatch.matchType === "exact_name")) {
|
|
2249
2314
|
selectedProject = autoMatch.project;
|
|
2250
2315
|
const matchReason = autoMatch.matchType === "linked_repo" ? `linked to ${repoFullName}` : "exact name match";
|
|
2251
|
-
console.log(
|
|
2316
|
+
console.log(pc10.green(`\u2713 Auto-selected project: ${selectedProject.name} (${matchReason})`));
|
|
2252
2317
|
} else if (autoMatch && autoMatch.matchType === "partial_name") {
|
|
2253
|
-
console.log(
|
|
2318
|
+
console.log(pc10.yellow(`Detected project: ${autoMatch.project.name} (partial match)`));
|
|
2254
2319
|
const { useDetected } = await prompts7({
|
|
2255
2320
|
type: "confirm",
|
|
2256
2321
|
name: "useDetected",
|
|
@@ -2266,13 +2331,13 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2266
2331
|
selectedProject = projects[0];
|
|
2267
2332
|
if (!projectMatchesRepo(selectedProject, repoFullName)) {
|
|
2268
2333
|
console.log("");
|
|
2269
|
-
console.log(
|
|
2270
|
-
console.log(
|
|
2271
|
-
console.log(
|
|
2272
|
-
console.log(
|
|
2273
|
-
console.log(
|
|
2334
|
+
console.log(pc10.yellow("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
|
|
2335
|
+
console.log(pc10.yellow("\u2502 \u26A0\uFE0F WARNING: Project does not match current repository \u2502"));
|
|
2336
|
+
console.log(pc10.yellow("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
|
|
2337
|
+
console.log(pc10.yellow(` Current repo: ${repoFullName}`));
|
|
2338
|
+
console.log(pc10.yellow(` Only project: ${selectedProject.name}`));
|
|
2274
2339
|
if (selectedProject.linkedRepo) {
|
|
2275
|
-
console.log(
|
|
2340
|
+
console.log(pc10.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
|
|
2276
2341
|
}
|
|
2277
2342
|
console.log("");
|
|
2278
2343
|
const { continueAnyway } = await prompts7({
|
|
@@ -2282,14 +2347,14 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2282
2347
|
initial: false
|
|
2283
2348
|
});
|
|
2284
2349
|
if (!continueAnyway) {
|
|
2285
|
-
console.log(
|
|
2350
|
+
console.log(pc10.gray("Cancelled."));
|
|
2286
2351
|
process.exit(0);
|
|
2287
2352
|
}
|
|
2288
2353
|
}
|
|
2289
2354
|
} else {
|
|
2290
|
-
console.log(
|
|
2355
|
+
console.log(pc10.yellow(`
|
|
2291
2356
|
\u26A0\uFE0F No matching project found for ${repoFullName}`));
|
|
2292
|
-
console.log(
|
|
2357
|
+
console.log(pc10.gray("Select a project manually:\n"));
|
|
2293
2358
|
selectedProject = await promptProjectSelection(projects, repoFullName);
|
|
2294
2359
|
}
|
|
2295
2360
|
}
|
|
@@ -2297,13 +2362,13 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2297
2362
|
const autoMatch = findMatchingProject(projects, repoFullName);
|
|
2298
2363
|
if (autoMatch && autoMatch.project.id !== selectedProject.id) {
|
|
2299
2364
|
console.log("");
|
|
2300
|
-
console.log(
|
|
2301
|
-
console.log(
|
|
2302
|
-
console.log(
|
|
2303
|
-
console.log(
|
|
2304
|
-
console.log(
|
|
2365
|
+
console.log(pc10.yellow("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
|
|
2366
|
+
console.log(pc10.yellow("\u2502 \u26A0\uFE0F WARNING: You selected a different project \u2502"));
|
|
2367
|
+
console.log(pc10.yellow("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
|
|
2368
|
+
console.log(pc10.yellow(` Current repo: ${repoFullName}`));
|
|
2369
|
+
console.log(pc10.yellow(` Selected project: ${selectedProject.name}`));
|
|
2305
2370
|
if (selectedProject.linkedRepo) {
|
|
2306
|
-
console.log(
|
|
2371
|
+
console.log(pc10.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
|
|
2307
2372
|
}
|
|
2308
2373
|
console.log("");
|
|
2309
2374
|
const { continueAnyway } = await prompts7({
|
|
@@ -2313,7 +2378,7 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2313
2378
|
initial: false
|
|
2314
2379
|
});
|
|
2315
2380
|
if (!continueAnyway) {
|
|
2316
|
-
console.log(
|
|
2381
|
+
console.log(pc10.gray("Cancelled."));
|
|
2317
2382
|
process.exit(0);
|
|
2318
2383
|
}
|
|
2319
2384
|
}
|
|
@@ -2335,7 +2400,7 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2335
2400
|
initial: Math.max(0, vaultEnvs.indexOf("production"))
|
|
2336
2401
|
});
|
|
2337
2402
|
if (!selectedEnv) {
|
|
2338
|
-
console.log(
|
|
2403
|
+
console.log(pc10.gray("Cancelled."));
|
|
2339
2404
|
process.exit(0);
|
|
2340
2405
|
}
|
|
2341
2406
|
keywayEnv = selectedEnv;
|
|
@@ -2343,6 +2408,22 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2343
2408
|
providerEnv = mapToProviderEnvironment(provider, keywayEnv);
|
|
2344
2409
|
}
|
|
2345
2410
|
}
|
|
2411
|
+
if (needsDirectionPrompt) {
|
|
2412
|
+
const effectiveKeywayEnv = keywayEnv || "production";
|
|
2413
|
+
const effectiveProviderEnv = providerEnv || mapToProviderEnvironment(provider, effectiveKeywayEnv);
|
|
2414
|
+
console.log(pc10.gray("\nComparing secrets..."));
|
|
2415
|
+
const diff = await getSyncDiff(accessToken, repoFullName, {
|
|
2416
|
+
connectionId: connection.id,
|
|
2417
|
+
projectId: selectedProject.id,
|
|
2418
|
+
keywayEnvironment: effectiveKeywayEnv,
|
|
2419
|
+
providerEnvironment: effectiveProviderEnv
|
|
2420
|
+
});
|
|
2421
|
+
displayDiffSummary(diff, providerName);
|
|
2422
|
+
const totalDiff = diff.onlyInKeyway.length + diff.onlyInProvider.length + diff.different.length;
|
|
2423
|
+
if (totalDiff === 0) {
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2346
2427
|
if (needsDirectionPrompt) {
|
|
2347
2428
|
const { selectedDirection } = await prompts7({
|
|
2348
2429
|
type: "select",
|
|
@@ -2354,7 +2435,7 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2354
2435
|
]
|
|
2355
2436
|
});
|
|
2356
2437
|
if (!selectedDirection) {
|
|
2357
|
-
console.log(
|
|
2438
|
+
console.log(pc10.gray("Cancelled."));
|
|
2358
2439
|
process.exit(0);
|
|
2359
2440
|
}
|
|
2360
2441
|
direction = selectedDirection;
|
|
@@ -2371,9 +2452,9 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2371
2452
|
keywayEnv
|
|
2372
2453
|
);
|
|
2373
2454
|
if (status.isFirstSync && direction === "push" && status.vaultIsEmpty && status.providerHasSecrets) {
|
|
2374
|
-
console.log(
|
|
2455
|
+
console.log(pc10.yellow(`
|
|
2375
2456
|
\u26A0\uFE0F Your Keyway vault is empty for "${keywayEnv}", but ${providerName} has ${status.providerSecretCount} secrets.`));
|
|
2376
|
-
console.log(
|
|
2457
|
+
console.log(pc10.gray(` (Use --environment to sync a different environment)`));
|
|
2377
2458
|
const { importFirst } = await prompts7({
|
|
2378
2459
|
type: "confirm",
|
|
2379
2460
|
name: "importFirst",
|
|
@@ -2415,7 +2496,7 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2415
2496
|
command: "sync",
|
|
2416
2497
|
error: truncateMessage(message)
|
|
2417
2498
|
});
|
|
2418
|
-
console.error(
|
|
2499
|
+
console.error(pc10.red(`
|
|
2419
2500
|
\u2717 ${message}`));
|
|
2420
2501
|
process.exit(1);
|
|
2421
2502
|
}
|
|
@@ -2432,33 +2513,33 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
|
|
|
2432
2513
|
});
|
|
2433
2514
|
const totalChanges = preview.toCreate.length + preview.toUpdate.length + preview.toDelete.length;
|
|
2434
2515
|
if (totalChanges === 0) {
|
|
2435
|
-
console.log(
|
|
2516
|
+
console.log(pc10.green("\n\u2713 Already in sync. No changes needed."));
|
|
2436
2517
|
return;
|
|
2437
2518
|
}
|
|
2438
|
-
console.log(
|
|
2519
|
+
console.log(pc10.blue("\n\u{1F4CB} Sync Preview\n"));
|
|
2439
2520
|
if (preview.toCreate.length > 0) {
|
|
2440
|
-
console.log(
|
|
2441
|
-
preview.toCreate.slice(0, 5).forEach((key) => console.log(
|
|
2521
|
+
console.log(pc10.green(` + ${preview.toCreate.length} to create`));
|
|
2522
|
+
preview.toCreate.slice(0, 5).forEach((key) => console.log(pc10.gray(` ${key}`)));
|
|
2442
2523
|
if (preview.toCreate.length > 5) {
|
|
2443
|
-
console.log(
|
|
2524
|
+
console.log(pc10.gray(` ... and ${preview.toCreate.length - 5} more`));
|
|
2444
2525
|
}
|
|
2445
2526
|
}
|
|
2446
2527
|
if (preview.toUpdate.length > 0) {
|
|
2447
|
-
console.log(
|
|
2448
|
-
preview.toUpdate.slice(0, 5).forEach((key) => console.log(
|
|
2528
|
+
console.log(pc10.yellow(` ~ ${preview.toUpdate.length} to update`));
|
|
2529
|
+
preview.toUpdate.slice(0, 5).forEach((key) => console.log(pc10.gray(` ${key}`)));
|
|
2449
2530
|
if (preview.toUpdate.length > 5) {
|
|
2450
|
-
console.log(
|
|
2531
|
+
console.log(pc10.gray(` ... and ${preview.toUpdate.length - 5} more`));
|
|
2451
2532
|
}
|
|
2452
2533
|
}
|
|
2453
2534
|
if (preview.toDelete.length > 0) {
|
|
2454
|
-
console.log(
|
|
2455
|
-
preview.toDelete.slice(0, 5).forEach((key) => console.log(
|
|
2535
|
+
console.log(pc10.red(` - ${preview.toDelete.length} to delete`));
|
|
2536
|
+
preview.toDelete.slice(0, 5).forEach((key) => console.log(pc10.gray(` ${key}`)));
|
|
2456
2537
|
if (preview.toDelete.length > 5) {
|
|
2457
|
-
console.log(
|
|
2538
|
+
console.log(pc10.gray(` ... and ${preview.toDelete.length - 5} more`));
|
|
2458
2539
|
}
|
|
2459
2540
|
}
|
|
2460
2541
|
if (preview.toSkip.length > 0) {
|
|
2461
|
-
console.log(
|
|
2542
|
+
console.log(pc10.gray(` \u25CB ${preview.toSkip.length} unchanged`));
|
|
2462
2543
|
}
|
|
2463
2544
|
console.log("");
|
|
2464
2545
|
if (!skipConfirm) {
|
|
@@ -2470,11 +2551,11 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
|
|
|
2470
2551
|
initial: true
|
|
2471
2552
|
});
|
|
2472
2553
|
if (!confirm) {
|
|
2473
|
-
console.log(
|
|
2554
|
+
console.log(pc10.gray("Cancelled."));
|
|
2474
2555
|
return;
|
|
2475
2556
|
}
|
|
2476
2557
|
}
|
|
2477
|
-
console.log(
|
|
2558
|
+
console.log(pc10.blue("\n\u23F3 Syncing...\n"));
|
|
2478
2559
|
const result = await executeSync(accessToken, repoFullName, {
|
|
2479
2560
|
connectionId,
|
|
2480
2561
|
projectId: project.id,
|
|
@@ -2484,11 +2565,11 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
|
|
|
2484
2565
|
allowDelete
|
|
2485
2566
|
});
|
|
2486
2567
|
if (result.success) {
|
|
2487
|
-
console.log(
|
|
2488
|
-
console.log(
|
|
2489
|
-
console.log(
|
|
2568
|
+
console.log(pc10.green("\u2713 Sync complete"));
|
|
2569
|
+
console.log(pc10.gray(` Created: ${result.stats.created}`));
|
|
2570
|
+
console.log(pc10.gray(` Updated: ${result.stats.updated}`));
|
|
2490
2571
|
if (result.stats.deleted > 0) {
|
|
2491
|
-
console.log(
|
|
2572
|
+
console.log(pc10.gray(` Deleted: ${result.stats.deleted}`));
|
|
2492
2573
|
}
|
|
2493
2574
|
trackEvent(AnalyticsEvents.CLI_SYNC, {
|
|
2494
2575
|
provider,
|
|
@@ -2498,20 +2579,24 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
|
|
|
2498
2579
|
deleted: result.stats.deleted
|
|
2499
2580
|
});
|
|
2500
2581
|
} else {
|
|
2501
|
-
console.error(
|
|
2582
|
+
console.error(pc10.red(`
|
|
2502
2583
|
\u2717 ${result.error}`));
|
|
2503
2584
|
process.exit(1);
|
|
2504
2585
|
}
|
|
2505
2586
|
}
|
|
2506
2587
|
|
|
2507
2588
|
// src/cli.ts
|
|
2589
|
+
process.on("unhandledRejection", (reason) => {
|
|
2590
|
+
console.error(pc11.red("Unhandled error:"), reason);
|
|
2591
|
+
process.exit(1);
|
|
2592
|
+
});
|
|
2508
2593
|
var program = new Command();
|
|
2509
2594
|
var TAGLINE = "Sync secrets with your team and infra";
|
|
2510
2595
|
var showBanner = () => {
|
|
2511
|
-
const text =
|
|
2596
|
+
const text = pc11.bold(pc11.cyan("Keyway CLI"));
|
|
2512
2597
|
console.log(`
|
|
2513
2598
|
${text}
|
|
2514
|
-
${
|
|
2599
|
+
${pc11.gray(TAGLINE)}
|
|
2515
2600
|
`);
|
|
2516
2601
|
};
|
|
2517
2602
|
showBanner();
|
|
@@ -2548,6 +2633,6 @@ program.command("sync <provider>").description("Sync secrets with a provider (e.
|
|
|
2548
2633
|
await syncCommand(provider, options);
|
|
2549
2634
|
});
|
|
2550
2635
|
program.parseAsync().catch((error) => {
|
|
2551
|
-
console.error(
|
|
2636
|
+
console.error(pc11.red("Error:"), error.message || error);
|
|
2552
2637
|
process.exit(1);
|
|
2553
2638
|
});
|