@keywaysh/cli 0.1.15 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -227
- package/dist/{auth-QLPQ24HZ.js → auth-64V3RWUK.js} +1 -1
- package/dist/{chunk-F4C46224.js → chunk-IVZM2JTT.js} +0 -1
- package/dist/cli.js +857 -868
- package/package.json +3 -4
package/dist/cli.js
CHANGED
|
@@ -4,22 +4,92 @@ import {
|
|
|
4
4
|
getAuthFilePath,
|
|
5
5
|
getStoredAuth,
|
|
6
6
|
saveAuthToken
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-IVZM2JTT.js";
|
|
8
8
|
|
|
9
9
|
// src/cli.ts
|
|
10
10
|
import { Command } from "commander";
|
|
11
|
-
import pc13 from "picocolors";
|
|
12
11
|
|
|
13
|
-
// src/
|
|
14
|
-
import
|
|
15
|
-
import
|
|
12
|
+
// src/utils/ui.ts
|
|
13
|
+
import * as p from "@clack/prompts";
|
|
14
|
+
import pc from "picocolors";
|
|
15
|
+
function intro2(command2) {
|
|
16
|
+
p.intro(pc.bgCyan(pc.black(` keyway ${command2} `)));
|
|
17
|
+
}
|
|
18
|
+
function outro2(message2) {
|
|
19
|
+
p.outro(message2);
|
|
20
|
+
}
|
|
21
|
+
function spinner2() {
|
|
22
|
+
return p.spinner();
|
|
23
|
+
}
|
|
24
|
+
function success(message2) {
|
|
25
|
+
p.log.success(message2);
|
|
26
|
+
}
|
|
27
|
+
function error(message2) {
|
|
28
|
+
p.log.error(message2);
|
|
29
|
+
}
|
|
30
|
+
function warn(message2) {
|
|
31
|
+
p.log.warn(message2);
|
|
32
|
+
}
|
|
33
|
+
function info(message2) {
|
|
34
|
+
p.log.info(message2);
|
|
35
|
+
}
|
|
36
|
+
function step(message2) {
|
|
37
|
+
p.log.step(message2);
|
|
38
|
+
}
|
|
39
|
+
function message(message2) {
|
|
40
|
+
p.log.message(message2);
|
|
41
|
+
}
|
|
42
|
+
function note2(message2, title) {
|
|
43
|
+
p.note(message2, title);
|
|
44
|
+
}
|
|
45
|
+
function cancel2(message2 = "Operation cancelled.") {
|
|
46
|
+
p.cancel(message2);
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
async function text2(options) {
|
|
50
|
+
const result = await p.text(options);
|
|
51
|
+
if (p.isCancel(result)) {
|
|
52
|
+
cancel2();
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
async function confirm2(options) {
|
|
57
|
+
const result = await p.confirm(options);
|
|
58
|
+
if (p.isCancel(result)) {
|
|
59
|
+
cancel2();
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
async function select2(options) {
|
|
64
|
+
const result = await p.select(options);
|
|
65
|
+
if (p.isCancel(result)) {
|
|
66
|
+
cancel2();
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
function link(url) {
|
|
71
|
+
return pc.underline(pc.cyan(url));
|
|
72
|
+
}
|
|
73
|
+
function command(cmd) {
|
|
74
|
+
return pc.cyan(cmd);
|
|
75
|
+
}
|
|
76
|
+
function file(path7) {
|
|
77
|
+
return pc.cyan(path7);
|
|
78
|
+
}
|
|
79
|
+
function value(val) {
|
|
80
|
+
return pc.cyan(String(val));
|
|
81
|
+
}
|
|
82
|
+
function dim(text3) {
|
|
83
|
+
return pc.dim(text3);
|
|
84
|
+
}
|
|
85
|
+
function bold(text3) {
|
|
86
|
+
return pc.bold(text3);
|
|
87
|
+
}
|
|
16
88
|
|
|
17
89
|
// src/utils/git.ts
|
|
18
90
|
import { execSync } from "child_process";
|
|
19
91
|
import fs from "fs";
|
|
20
92
|
import path from "path";
|
|
21
|
-
import pc from "picocolors";
|
|
22
|
-
import prompts from "prompts";
|
|
23
93
|
function getCurrentRepoFullName() {
|
|
24
94
|
try {
|
|
25
95
|
if (!isGitRepository()) {
|
|
@@ -29,7 +99,7 @@ function getCurrentRepoFullName() {
|
|
|
29
99
|
encoding: "utf-8"
|
|
30
100
|
}).trim();
|
|
31
101
|
return parseGitHubUrl(remoteUrl);
|
|
32
|
-
} catch (
|
|
102
|
+
} catch (error2) {
|
|
33
103
|
throw new Error("Failed to get repository name. Make sure you are in a git repository with a GitHub remote.");
|
|
34
104
|
}
|
|
35
105
|
}
|
|
@@ -116,18 +186,16 @@ async function warnIfEnvNotGitignored() {
|
|
|
116
186
|
if (checkEnvGitignore()) {
|
|
117
187
|
return;
|
|
118
188
|
}
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
type: "confirm",
|
|
122
|
-
name: "addToGitignore",
|
|
189
|
+
warn(".env files are not in .gitignore - secrets may be committed");
|
|
190
|
+
const addToGitignore = await confirm2({
|
|
123
191
|
message: "Add .env* to .gitignore?",
|
|
124
|
-
|
|
192
|
+
initialValue: true
|
|
125
193
|
});
|
|
126
194
|
if (addToGitignore) {
|
|
127
195
|
if (addEnvToGitignore()) {
|
|
128
|
-
|
|
196
|
+
success("Added .env* to .gitignore");
|
|
129
197
|
} else {
|
|
130
|
-
|
|
198
|
+
error("Failed to update .gitignore");
|
|
131
199
|
}
|
|
132
200
|
}
|
|
133
201
|
}
|
|
@@ -140,8 +208,8 @@ var INTERNAL_POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
|
140
208
|
// package.json
|
|
141
209
|
var package_default = {
|
|
142
210
|
name: "@keywaysh/cli",
|
|
143
|
-
version: "0.
|
|
144
|
-
description: "
|
|
211
|
+
version: "0.2.0",
|
|
212
|
+
description: "Env vars that sync like code.",
|
|
145
213
|
type: "module",
|
|
146
214
|
bin: {
|
|
147
215
|
keyway: "./dist/cli.js"
|
|
@@ -193,13 +261,12 @@ var package_default = {
|
|
|
193
261
|
open: "^11.0.0",
|
|
194
262
|
picocolors: "^1.1.1",
|
|
195
263
|
"posthog-node": "^3.5.0",
|
|
196
|
-
prompts: "^
|
|
264
|
+
"@clack/prompts": "^0.9.1"
|
|
197
265
|
},
|
|
198
266
|
devDependencies: {
|
|
199
267
|
"@types/balanced-match": "^3.0.2",
|
|
200
268
|
"@types/libsodium-wrappers": "^0.7.14",
|
|
201
269
|
"@types/node": "^24.2.0",
|
|
202
|
-
"@types/prompts": "^2.4.9",
|
|
203
270
|
"@vitest/coverage-v8": "^3.0.0",
|
|
204
271
|
msw: "^2.12.4",
|
|
205
272
|
tsup: "^8.5.0",
|
|
@@ -213,9 +280,9 @@ var package_default = {
|
|
|
213
280
|
var API_BASE_URL = process.env.KEYWAY_API_URL || INTERNAL_API_URL;
|
|
214
281
|
var USER_AGENT = `keyway-cli/${package_default.version}`;
|
|
215
282
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
216
|
-
function truncateMessage(
|
|
217
|
-
if (
|
|
218
|
-
return
|
|
283
|
+
function truncateMessage(message2, maxLength = 200) {
|
|
284
|
+
if (message2.length <= maxLength) return message2;
|
|
285
|
+
return message2.slice(0, maxLength - 3) + "...";
|
|
219
286
|
}
|
|
220
287
|
var NETWORK_ERROR_MESSAGES = {
|
|
221
288
|
ECONNREFUSED: "Cannot connect to Keyway API server. Is the server running?",
|
|
@@ -228,16 +295,16 @@ var NETWORK_ERROR_MESSAGES = {
|
|
|
228
295
|
UNABLE_TO_VERIFY_LEAF_SIGNATURE: "SSL certificate verification failed.",
|
|
229
296
|
EPROTO: "SSL/TLS protocol error. Try again later."
|
|
230
297
|
};
|
|
231
|
-
function handleNetworkError(
|
|
232
|
-
const errorCode =
|
|
298
|
+
function handleNetworkError(error2) {
|
|
299
|
+
const errorCode = error2.code || error2.cause?.code;
|
|
233
300
|
if (errorCode && NETWORK_ERROR_MESSAGES[errorCode]) {
|
|
234
301
|
return new Error(NETWORK_ERROR_MESSAGES[errorCode]);
|
|
235
302
|
}
|
|
236
|
-
const
|
|
237
|
-
if (
|
|
303
|
+
const message2 = error2.message.toLowerCase();
|
|
304
|
+
if (message2.includes("fetch failed") || message2.includes("network")) {
|
|
238
305
|
return new Error("Network error. Check your internet connection and try again.");
|
|
239
306
|
}
|
|
240
|
-
return
|
|
307
|
+
return error2;
|
|
241
308
|
}
|
|
242
309
|
async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
243
310
|
const controller = new AbortController();
|
|
@@ -247,14 +314,14 @@ async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_M
|
|
|
247
314
|
...options,
|
|
248
315
|
signal: controller.signal
|
|
249
316
|
});
|
|
250
|
-
} catch (
|
|
251
|
-
if (
|
|
252
|
-
if (
|
|
317
|
+
} catch (error2) {
|
|
318
|
+
if (error2 instanceof Error) {
|
|
319
|
+
if (error2.name === "AbortError") {
|
|
253
320
|
throw new Error(`Request timeout after ${timeoutMs / 1e3}s. Check your network connection.`);
|
|
254
321
|
}
|
|
255
|
-
throw handleNetworkError(
|
|
322
|
+
throw handleNetworkError(error2);
|
|
256
323
|
}
|
|
257
|
-
throw
|
|
324
|
+
throw error2;
|
|
258
325
|
} finally {
|
|
259
326
|
clearTimeout(timeout);
|
|
260
327
|
}
|
|
@@ -280,39 +347,39 @@ Set KEYWAY_DISABLE_SECURITY_WARNINGS=1 to suppress this warning.`
|
|
|
280
347
|
}
|
|
281
348
|
validateApiUrl(API_BASE_URL);
|
|
282
349
|
var APIError = class extends Error {
|
|
283
|
-
constructor(statusCode,
|
|
284
|
-
super(
|
|
350
|
+
constructor(statusCode, error2, message2, upgradeUrl) {
|
|
351
|
+
super(message2);
|
|
285
352
|
this.statusCode = statusCode;
|
|
286
|
-
this.error =
|
|
353
|
+
this.error = error2;
|
|
287
354
|
this.upgradeUrl = upgradeUrl;
|
|
288
355
|
this.name = "APIError";
|
|
289
356
|
}
|
|
290
357
|
};
|
|
291
358
|
async function handleResponse(response) {
|
|
292
359
|
const contentType = response.headers.get("content-type") || "";
|
|
293
|
-
const
|
|
360
|
+
const text3 = await response.text();
|
|
294
361
|
if (!response.ok) {
|
|
295
362
|
if (contentType.includes("application/json")) {
|
|
296
363
|
try {
|
|
297
|
-
const
|
|
298
|
-
throw new APIError(response.status,
|
|
364
|
+
const error2 = JSON.parse(text3);
|
|
365
|
+
throw new APIError(response.status, error2.title || "Error", error2.detail || `HTTP ${response.status}`, error2.upgradeUrl);
|
|
299
366
|
} catch (e) {
|
|
300
367
|
if (e instanceof APIError) throw e;
|
|
301
|
-
throw new APIError(response.status, "Error",
|
|
368
|
+
throw new APIError(response.status, "Error", text3 || `HTTP ${response.status}`);
|
|
302
369
|
}
|
|
303
370
|
}
|
|
304
|
-
throw new APIError(response.status, "Error",
|
|
371
|
+
throw new APIError(response.status, "Error", text3 || `HTTP ${response.status}`);
|
|
305
372
|
}
|
|
306
|
-
if (!
|
|
373
|
+
if (!text3) {
|
|
307
374
|
return {};
|
|
308
375
|
}
|
|
309
376
|
if (contentType.includes("application/json")) {
|
|
310
377
|
try {
|
|
311
|
-
return JSON.parse(
|
|
378
|
+
return JSON.parse(text3);
|
|
312
379
|
} catch {
|
|
313
380
|
}
|
|
314
381
|
}
|
|
315
|
-
return { content:
|
|
382
|
+
return { content: text3 };
|
|
316
383
|
}
|
|
317
384
|
async function initVault(repoFullName, accessToken) {
|
|
318
385
|
const body = { repoFullName };
|
|
@@ -340,11 +407,11 @@ function parseEnvContent(content) {
|
|
|
340
407
|
const eqIndex = trimmed.indexOf("=");
|
|
341
408
|
if (eqIndex === -1) continue;
|
|
342
409
|
const key = trimmed.substring(0, eqIndex).trim();
|
|
343
|
-
let
|
|
344
|
-
if (
|
|
345
|
-
|
|
410
|
+
let value2 = trimmed.substring(eqIndex + 1);
|
|
411
|
+
if (value2.startsWith('"') && value2.endsWith('"') || value2.startsWith("'") && value2.endsWith("'")) {
|
|
412
|
+
value2 = value2.slice(1, -1);
|
|
346
413
|
}
|
|
347
|
-
if (key) result[key] =
|
|
414
|
+
if (key) result[key] = value2;
|
|
348
415
|
}
|
|
349
416
|
return result;
|
|
350
417
|
}
|
|
@@ -664,7 +731,7 @@ function getDistinctId() {
|
|
|
664
731
|
} catch {
|
|
665
732
|
}
|
|
666
733
|
return distinctId;
|
|
667
|
-
} catch (
|
|
734
|
+
} catch (error2) {
|
|
668
735
|
console.warn("Failed to persist distinct ID, using session-based ID");
|
|
669
736
|
distinctId = `session-${crypto.randomUUID()}`;
|
|
670
737
|
return distinctId;
|
|
@@ -698,21 +765,21 @@ function trackEvent(event, properties) {
|
|
|
698
765
|
ci: CI
|
|
699
766
|
}
|
|
700
767
|
});
|
|
701
|
-
} catch (
|
|
702
|
-
console.debug("Analytics error:",
|
|
768
|
+
} catch (error2) {
|
|
769
|
+
console.debug("Analytics error:", error2);
|
|
703
770
|
}
|
|
704
771
|
}
|
|
705
772
|
function sanitizeProperties(properties) {
|
|
706
773
|
const sanitized = {};
|
|
707
|
-
for (const [key,
|
|
774
|
+
for (const [key, value2] of Object.entries(properties)) {
|
|
708
775
|
if (key.toLowerCase().includes("secret") || key.toLowerCase().includes("token") || key.toLowerCase().includes("password") || key.toLowerCase().includes("content") || key.toLowerCase().includes("key") || key.toLowerCase().includes("value")) {
|
|
709
776
|
continue;
|
|
710
777
|
}
|
|
711
|
-
if (
|
|
712
|
-
sanitized[key] = `${
|
|
778
|
+
if (value2 && typeof value2 === "string" && value2.length > 500) {
|
|
779
|
+
sanitized[key] = `${value2.slice(0, 200)}...`;
|
|
713
780
|
continue;
|
|
714
781
|
}
|
|
715
|
-
sanitized[key] =
|
|
782
|
+
sanitized[key] = value2;
|
|
716
783
|
}
|
|
717
784
|
return sanitized;
|
|
718
785
|
}
|
|
@@ -741,8 +808,8 @@ function identifyUser(userId, properties) {
|
|
|
741
808
|
alias: anonId
|
|
742
809
|
});
|
|
743
810
|
}
|
|
744
|
-
} catch (
|
|
745
|
-
console.debug("Analytics identify error:",
|
|
811
|
+
} catch (error2) {
|
|
812
|
+
console.debug("Analytics identify error:", error2);
|
|
746
813
|
}
|
|
747
814
|
}
|
|
748
815
|
var AnalyticsEvents = {
|
|
@@ -761,8 +828,6 @@ var AnalyticsEvents = {
|
|
|
761
828
|
// src/cmds/readme.ts
|
|
762
829
|
import fs3 from "fs";
|
|
763
830
|
import path3 from "path";
|
|
764
|
-
import prompts2 from "prompts";
|
|
765
|
-
import pc2 from "picocolors";
|
|
766
831
|
import balanced from "balanced-match";
|
|
767
832
|
function generateBadge(repo) {
|
|
768
833
|
return `[](https://www.keyway.sh/vaults/${repo})`;
|
|
@@ -853,22 +918,15 @@ async function ensureReadme(repoName, cwd) {
|
|
|
853
918
|
if (existing) return existing;
|
|
854
919
|
const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
|
|
855
920
|
if (!isInteractive2) {
|
|
856
|
-
|
|
921
|
+
warn('No README found. Run "keyway readme add-badge" from a repo with a README.');
|
|
857
922
|
return null;
|
|
858
923
|
}
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
},
|
|
866
|
-
{
|
|
867
|
-
onCancel: () => ({ confirm: false })
|
|
868
|
-
}
|
|
869
|
-
);
|
|
870
|
-
if (!confirm) {
|
|
871
|
-
console.log(pc2.yellow("Skipping badge insertion (no README)."));
|
|
924
|
+
const confirm3 = await confirm2({
|
|
925
|
+
message: "No README found. Create a default README.md?",
|
|
926
|
+
initialValue: false
|
|
927
|
+
});
|
|
928
|
+
if (!confirm3) {
|
|
929
|
+
warn("Skipping badge insertion (no README).");
|
|
872
930
|
return null;
|
|
873
931
|
}
|
|
874
932
|
const defaultPath = path3.join(cwd, "README.md");
|
|
@@ -891,94 +949,66 @@ async function addBadgeToReadme(silent = false) {
|
|
|
891
949
|
const updated = insertBadgeIntoReadme(content, badge);
|
|
892
950
|
if (updated === content) {
|
|
893
951
|
if (!silent) {
|
|
894
|
-
|
|
952
|
+
info("Keyway badge already present in README.");
|
|
895
953
|
}
|
|
896
954
|
return false;
|
|
897
955
|
}
|
|
898
956
|
fs3.writeFileSync(readmePath, updated, "utf-8");
|
|
899
957
|
if (!silent) {
|
|
900
|
-
|
|
958
|
+
success(`Keyway badge added to ${path3.basename(readmePath)}`);
|
|
901
959
|
}
|
|
902
960
|
return true;
|
|
903
961
|
}
|
|
904
962
|
|
|
905
963
|
// src/cmds/push.ts
|
|
906
|
-
import
|
|
964
|
+
import pc3 from "picocolors";
|
|
907
965
|
import fs5 from "fs";
|
|
908
966
|
import path5 from "path";
|
|
909
|
-
import prompts5 from "prompts";
|
|
910
967
|
|
|
911
968
|
// src/cmds/login.ts
|
|
912
|
-
import
|
|
913
|
-
import
|
|
914
|
-
import prompts3 from "prompts";
|
|
969
|
+
import pc2 from "picocolors";
|
|
970
|
+
import * as p2 from "@clack/prompts";
|
|
915
971
|
|
|
916
972
|
// src/utils/helpers.ts
|
|
917
|
-
import pc3 from "picocolors";
|
|
918
973
|
import open from "open";
|
|
919
974
|
function sleep(ms) {
|
|
920
975
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
921
976
|
}
|
|
922
977
|
async function openUrl(url) {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
${url}
|
|
926
|
-
`));
|
|
978
|
+
message(dim(`Open this URL in your browser:
|
|
979
|
+
${url}`));
|
|
927
980
|
await open(url).catch(() => {
|
|
928
981
|
});
|
|
929
982
|
}
|
|
930
983
|
function isInteractive() {
|
|
931
984
|
return Boolean(process.stdout.isTTY && process.stdin.isTTY && !process.env.CI);
|
|
932
985
|
}
|
|
933
|
-
function showUpgradePrompt(
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
console.log(` ${pc3.yellow("\u26A1")} ${pc3.bold("Plan Limit Reached")}`);
|
|
938
|
-
console.log("");
|
|
939
|
-
console.log(pc3.white(` ${message}`));
|
|
940
|
-
console.log("");
|
|
941
|
-
console.log(` ${pc3.cyan("Upgrade now \u2192")} ${pc3.underline(upgradeUrl)}`);
|
|
942
|
-
console.log("");
|
|
943
|
-
console.log(pc3.dim("\u2500".repeat(50)));
|
|
944
|
-
console.log("");
|
|
986
|
+
function showUpgradePrompt(message2, upgradeUrl) {
|
|
987
|
+
note2(`${message2}
|
|
988
|
+
|
|
989
|
+
Upgrade: ${link(upgradeUrl)}`, "Plan Limit Reached");
|
|
945
990
|
}
|
|
946
991
|
var MAX_CONSECUTIVE_ERRORS = 5;
|
|
947
992
|
|
|
948
993
|
// src/cmds/login.ts
|
|
949
|
-
async function promptYesNo(question, defaultYes = true) {
|
|
950
|
-
return new Promise((resolve) => {
|
|
951
|
-
const rl = readline.createInterface({
|
|
952
|
-
input: process.stdin,
|
|
953
|
-
output: process.stdout
|
|
954
|
-
});
|
|
955
|
-
rl.question(question, (answer) => {
|
|
956
|
-
rl.close();
|
|
957
|
-
const normalized = answer.trim().toLowerCase();
|
|
958
|
-
if (!normalized) return resolve(defaultYes);
|
|
959
|
-
if (["y", "yes"].includes(normalized)) return resolve(true);
|
|
960
|
-
if (["n", "no"].includes(normalized)) return resolve(false);
|
|
961
|
-
return resolve(defaultYes);
|
|
962
|
-
});
|
|
963
|
-
});
|
|
964
|
-
}
|
|
965
994
|
async function runLoginFlow() {
|
|
966
|
-
console.log(pc4.blue("\u{1F510} Starting Keyway login...\n"));
|
|
967
995
|
const repoName = detectGitRepo();
|
|
968
996
|
const start = await startDeviceLogin(repoName);
|
|
969
997
|
const verifyUrl = start.verificationUriComplete || start.verificationUri;
|
|
970
998
|
if (!verifyUrl) {
|
|
971
999
|
throw new Error("Missing verification URL from the auth server.");
|
|
972
1000
|
}
|
|
973
|
-
|
|
1001
|
+
step(`Code: ${pc2.bold(pc2.green(start.userCode))}`);
|
|
974
1002
|
await openUrl(verifyUrl);
|
|
975
|
-
|
|
1003
|
+
const s = spinner2();
|
|
1004
|
+
s.start("Waiting for authorization...");
|
|
976
1005
|
const pollIntervalMs = (start.interval ?? 5) * 1e3;
|
|
977
1006
|
const maxTimeoutMs = Math.min((start.expiresIn ?? 900) * 1e3, 30 * 60 * 1e3);
|
|
978
1007
|
const startTime = Date.now();
|
|
979
1008
|
let consecutiveErrors = 0;
|
|
980
1009
|
while (true) {
|
|
981
1010
|
if (Date.now() - startTime > maxTimeoutMs) {
|
|
1011
|
+
s.stop("Login timed out");
|
|
982
1012
|
throw new Error('Login timed out. Please run "keyway login" again.');
|
|
983
1013
|
}
|
|
984
1014
|
await sleep(pollIntervalMs);
|
|
@@ -1003,17 +1033,17 @@ async function runLoginFlow() {
|
|
|
1003
1033
|
login_method: "device"
|
|
1004
1034
|
});
|
|
1005
1035
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
console.log(`Authenticated GitHub user: ${pc4.cyan(result.githubLogin)}`);
|
|
1009
|
-
}
|
|
1036
|
+
s.stop("Authorized");
|
|
1037
|
+
success(`Logged in as ${value(`@${result.githubLogin}`)}`);
|
|
1010
1038
|
return result.keywayToken;
|
|
1011
1039
|
}
|
|
1040
|
+
s.stop("Authorization failed");
|
|
1012
1041
|
throw new Error(result.message || "Authentication failed");
|
|
1013
|
-
} catch (
|
|
1042
|
+
} catch (error2) {
|
|
1014
1043
|
consecutiveErrors++;
|
|
1015
1044
|
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
1016
|
-
const errorMsg =
|
|
1045
|
+
const errorMsg = error2 instanceof Error ? error2.message : "Unknown error";
|
|
1046
|
+
s.stop("Login failed");
|
|
1017
1047
|
throw new Error(`Login failed after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${errorMsg}`);
|
|
1018
1048
|
}
|
|
1019
1049
|
}
|
|
@@ -1025,7 +1055,7 @@ async function ensureLogin(options = {}) {
|
|
|
1025
1055
|
return envToken;
|
|
1026
1056
|
}
|
|
1027
1057
|
if (process.env.GITHUB_TOKEN && !process.env.KEYWAY_TOKEN) {
|
|
1028
|
-
|
|
1058
|
+
warn("GITHUB_TOKEN found but not used. Set KEYWAY_TOKEN for Keyway authentication.");
|
|
1029
1059
|
}
|
|
1030
1060
|
const stored = await getStoredAuth();
|
|
1031
1061
|
if (stored?.keywayToken) {
|
|
@@ -1036,7 +1066,10 @@ async function ensureLogin(options = {}) {
|
|
|
1036
1066
|
if (!canPrompt) {
|
|
1037
1067
|
throw new Error('No Keyway session found. Run "keyway login" to authenticate.');
|
|
1038
1068
|
}
|
|
1039
|
-
const proceed = await
|
|
1069
|
+
const proceed = await confirm2({
|
|
1070
|
+
message: "No Keyway session found. Open browser to sign in?",
|
|
1071
|
+
initialValue: true
|
|
1072
|
+
});
|
|
1040
1073
|
if (!proceed) {
|
|
1041
1074
|
throw new Error("Login required. Aborting.");
|
|
1042
1075
|
}
|
|
@@ -1045,30 +1078,24 @@ async function ensureLogin(options = {}) {
|
|
|
1045
1078
|
async function runTokenLogin() {
|
|
1046
1079
|
const repoName = detectGitRepo();
|
|
1047
1080
|
if (repoName) {
|
|
1048
|
-
|
|
1081
|
+
step(`Detected repository: ${value(repoName)}`);
|
|
1049
1082
|
}
|
|
1050
1083
|
const description = repoName ? `Keyway CLI for ${repoName}` : "Keyway CLI";
|
|
1051
1084
|
const url = `https://github.com/settings/personal-access-tokens/new?description=${encodeURIComponent(description)}`;
|
|
1052
1085
|
await openUrl(url);
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
if (!value || typeof value !== "string") return "Token is required";
|
|
1062
|
-
if (!value.startsWith("github_pat_")) return "Token must start with github_pat_";
|
|
1063
|
-
return true;
|
|
1064
|
-
}
|
|
1065
|
-
},
|
|
1066
|
-
{
|
|
1067
|
-
onCancel: () => {
|
|
1068
|
-
throw new Error("Login cancelled.");
|
|
1069
|
-
}
|
|
1086
|
+
info("Select the detected repo (or scope manually).");
|
|
1087
|
+
message(dim("Permissions: Metadata \u2192 Read-only; Account permissions: None."));
|
|
1088
|
+
const token = await p2.password({
|
|
1089
|
+
message: "Paste your GitHub PAT:",
|
|
1090
|
+
validate: (value2) => {
|
|
1091
|
+
if (!value2 || typeof value2 !== "string") return "Token is required";
|
|
1092
|
+
if (!value2.startsWith("github_pat_")) return "Token must start with github_pat_";
|
|
1093
|
+
return void 0;
|
|
1070
1094
|
}
|
|
1071
|
-
);
|
|
1095
|
+
});
|
|
1096
|
+
if (p2.isCancel(token)) {
|
|
1097
|
+
cancel2("Login cancelled.");
|
|
1098
|
+
}
|
|
1072
1099
|
if (!token || typeof token !== "string") {
|
|
1073
1100
|
throw new Error("Token is required.");
|
|
1074
1101
|
}
|
|
@@ -1076,6 +1103,8 @@ async function runTokenLogin() {
|
|
|
1076
1103
|
if (!trimmedToken.startsWith("github_pat_")) {
|
|
1077
1104
|
throw new Error("Token must start with github_pat_.");
|
|
1078
1105
|
}
|
|
1106
|
+
const s = spinner2();
|
|
1107
|
+
s.start("Validating token...");
|
|
1079
1108
|
const validation = await validateToken(trimmedToken);
|
|
1080
1109
|
await saveAuthToken(trimmedToken, {
|
|
1081
1110
|
githubLogin: validation.username
|
|
@@ -1088,61 +1117,56 @@ async function runTokenLogin() {
|
|
|
1088
1117
|
github_username: validation.username,
|
|
1089
1118
|
login_method: "pat"
|
|
1090
1119
|
});
|
|
1091
|
-
|
|
1120
|
+
s.stop("Token validated");
|
|
1121
|
+
success(`Logged in as ${value(`@${validation.username}`)}`);
|
|
1092
1122
|
return trimmedToken;
|
|
1093
1123
|
}
|
|
1094
1124
|
async function loginCommand(options = {}) {
|
|
1125
|
+
intro2("login");
|
|
1095
1126
|
try {
|
|
1096
1127
|
if (options.token) {
|
|
1097
1128
|
await runTokenLogin();
|
|
1098
1129
|
} else {
|
|
1099
1130
|
await runLoginFlow();
|
|
1100
1131
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1132
|
+
outro2("Ready to sync secrets!");
|
|
1133
|
+
} catch (error2) {
|
|
1134
|
+
const message2 = error2 instanceof Error ? error2.message : "Unexpected login error";
|
|
1103
1135
|
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
1104
1136
|
command: "login",
|
|
1105
|
-
error: truncateMessage(
|
|
1137
|
+
error: truncateMessage(message2)
|
|
1106
1138
|
});
|
|
1107
|
-
|
|
1108
|
-
\u2717 ${message}`));
|
|
1139
|
+
error(message2);
|
|
1109
1140
|
process.exit(1);
|
|
1110
1141
|
}
|
|
1111
1142
|
}
|
|
1112
1143
|
async function logoutCommand() {
|
|
1144
|
+
intro2("logout");
|
|
1113
1145
|
clearAuth();
|
|
1114
|
-
|
|
1115
|
-
|
|
1146
|
+
success("Logged out of Keyway");
|
|
1147
|
+
outro2(dim(`Auth cache cleared: ${getAuthFilePath()}`));
|
|
1116
1148
|
}
|
|
1117
1149
|
|
|
1118
1150
|
// src/utils/env.ts
|
|
1119
1151
|
import fs4 from "fs";
|
|
1120
1152
|
import path4 from "path";
|
|
1121
|
-
import pc5 from "picocolors";
|
|
1122
|
-
import prompts4 from "prompts";
|
|
1123
1153
|
async function promptCreateEnvFile() {
|
|
1124
|
-
const
|
|
1125
|
-
type: "confirm",
|
|
1126
|
-
name: "createEnv",
|
|
1154
|
+
const createEnv = await confirm2({
|
|
1127
1155
|
message: "No .env file found. Create one?",
|
|
1128
|
-
|
|
1129
|
-
}, {
|
|
1130
|
-
onCancel: () => {
|
|
1131
|
-
throw new Error("Cancelled by user.");
|
|
1132
|
-
}
|
|
1156
|
+
initialValue: true
|
|
1133
1157
|
});
|
|
1134
1158
|
if (!createEnv) {
|
|
1135
1159
|
return false;
|
|
1136
1160
|
}
|
|
1137
1161
|
const envFilePath = path4.join(process.cwd(), ".env");
|
|
1138
1162
|
fs4.writeFileSync(envFilePath, "# Add your environment variables here\n# Example: API_KEY=your-api-key\n");
|
|
1139
|
-
|
|
1163
|
+
success("Created .env file");
|
|
1140
1164
|
return true;
|
|
1141
1165
|
}
|
|
1142
1166
|
|
|
1143
1167
|
// src/cmds/push.ts
|
|
1144
|
-
function deriveEnvFromFile(
|
|
1145
|
-
const base = path5.basename(
|
|
1168
|
+
function deriveEnvFromFile(file2) {
|
|
1169
|
+
const base = path5.basename(file2);
|
|
1146
1170
|
const match = base.match(/\.env(?:\.(.+))?$/);
|
|
1147
1171
|
if (match) {
|
|
1148
1172
|
return match[1] || "development";
|
|
@@ -1154,7 +1178,7 @@ function discoverEnvCandidates(cwd) {
|
|
|
1154
1178
|
const entries = fs5.readdirSync(cwd);
|
|
1155
1179
|
const hasEnvLocal = entries.includes(".env.local");
|
|
1156
1180
|
if (hasEnvLocal) {
|
|
1157
|
-
|
|
1181
|
+
info("Detected .env.local \u2014 not synced by design (machine-specific secrets)");
|
|
1158
1182
|
}
|
|
1159
1183
|
const candidates = entries.filter((name) => name.startsWith(".env") && name !== ".env.local").map((name) => {
|
|
1160
1184
|
const fullPath = path5.join(cwd, name);
|
|
@@ -1179,8 +1203,9 @@ function discoverEnvCandidates(cwd) {
|
|
|
1179
1203
|
}
|
|
1180
1204
|
}
|
|
1181
1205
|
async function pushCommand(options) {
|
|
1206
|
+
intro2("push");
|
|
1207
|
+
await warnIfEnvNotGitignored();
|
|
1182
1208
|
try {
|
|
1183
|
-
console.log(pc6.blue("\u{1F510} Pushing secrets to Keyway...\n"));
|
|
1184
1209
|
const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
|
|
1185
1210
|
let environment = options.env;
|
|
1186
1211
|
let envFile = options.file;
|
|
@@ -1193,7 +1218,7 @@ async function pushCommand(options) {
|
|
|
1193
1218
|
if (!created) {
|
|
1194
1219
|
throw new Error("No .env file found.");
|
|
1195
1220
|
}
|
|
1196
|
-
|
|
1221
|
+
message(dim("Add your variables and run keyway push again"));
|
|
1197
1222
|
return;
|
|
1198
1223
|
}
|
|
1199
1224
|
if (environment && !envFile) {
|
|
@@ -1203,47 +1228,30 @@ async function pushCommand(options) {
|
|
|
1203
1228
|
}
|
|
1204
1229
|
}
|
|
1205
1230
|
if (!environment && !envFile && isInteractive2 && candidates.length > 0) {
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
} else if (choice === "custom") {
|
|
1229
|
-
const { fileInput } = await prompts5(
|
|
1230
|
-
{
|
|
1231
|
-
type: "text",
|
|
1232
|
-
name: "fileInput",
|
|
1233
|
-
message: "Path to env file:",
|
|
1234
|
-
validate: (value) => {
|
|
1235
|
-
if (!value) return "Path is required";
|
|
1236
|
-
const resolved = path5.resolve(process.cwd(), value);
|
|
1237
|
-
if (!fs5.existsSync(resolved)) return `File not found: ${value}`;
|
|
1238
|
-
return true;
|
|
1239
|
-
}
|
|
1240
|
-
},
|
|
1241
|
-
{
|
|
1242
|
-
onCancel: () => {
|
|
1243
|
-
throw new Error("Push cancelled by user.");
|
|
1244
|
-
}
|
|
1231
|
+
const choice = await select2({
|
|
1232
|
+
message: "Select an env file to push:",
|
|
1233
|
+
options: [
|
|
1234
|
+
...candidates.map((c) => ({
|
|
1235
|
+
label: `${c.file} (env: ${c.env})`,
|
|
1236
|
+
value: c.file
|
|
1237
|
+
})),
|
|
1238
|
+
{ label: "Enter a different file...", value: "__custom__" }
|
|
1239
|
+
]
|
|
1240
|
+
});
|
|
1241
|
+
if (choice && choice !== "__custom__") {
|
|
1242
|
+
envFile = choice;
|
|
1243
|
+
const matched = candidates.find((c) => c.file === envFile);
|
|
1244
|
+
environment = matched?.env;
|
|
1245
|
+
} else if (choice === "__custom__") {
|
|
1246
|
+
const fileInput = await text2({
|
|
1247
|
+
message: "Path to env file:",
|
|
1248
|
+
validate: (value2) => {
|
|
1249
|
+
if (!value2) return "Path is required";
|
|
1250
|
+
const resolved = path5.resolve(process.cwd(), value2);
|
|
1251
|
+
if (!fs5.existsSync(resolved)) return `File not found: ${value2}`;
|
|
1252
|
+
return void 0;
|
|
1245
1253
|
}
|
|
1246
|
-
);
|
|
1254
|
+
});
|
|
1247
1255
|
envFile = fileInput;
|
|
1248
1256
|
environment = deriveEnvFromFile(fileInput);
|
|
1249
1257
|
}
|
|
@@ -1266,31 +1274,21 @@ async function pushCommand(options) {
|
|
|
1266
1274
|
const trimmed = line.trim();
|
|
1267
1275
|
return trimmed.length > 0 && !trimmed.startsWith("#");
|
|
1268
1276
|
});
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1277
|
+
step(`File: ${file(envFile)}`);
|
|
1278
|
+
step(`Environment: ${value(environment)}`);
|
|
1279
|
+
step(`Variables: ${value(lines.length)}`);
|
|
1272
1280
|
const repoFullName = getCurrentRepoFullName();
|
|
1273
|
-
|
|
1281
|
+
step(`Repository: ${value(repoFullName)}`);
|
|
1274
1282
|
if (!options.yes) {
|
|
1275
|
-
|
|
1276
|
-
if (!isInteractive3) {
|
|
1283
|
+
if (!isInteractive2) {
|
|
1277
1284
|
throw new Error("Confirmation required. Re-run with --yes in non-interactive environments.");
|
|
1278
1285
|
}
|
|
1279
|
-
const
|
|
1280
|
-
{
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
},
|
|
1286
|
-
{
|
|
1287
|
-
onCancel: () => {
|
|
1288
|
-
throw new Error("Push cancelled by user.");
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
);
|
|
1292
|
-
if (!confirm) {
|
|
1293
|
-
console.log(pc6.yellow("Push aborted."));
|
|
1286
|
+
const confirm3 = await confirm2({
|
|
1287
|
+
message: `Push ${lines.length} secrets from ${envFile} to ${repoFullName}?`,
|
|
1288
|
+
initialValue: true
|
|
1289
|
+
});
|
|
1290
|
+
if (!confirm3) {
|
|
1291
|
+
warn("Push aborted.");
|
|
1294
1292
|
return;
|
|
1295
1293
|
}
|
|
1296
1294
|
}
|
|
@@ -1300,41 +1298,40 @@ async function pushCommand(options) {
|
|
|
1300
1298
|
environment,
|
|
1301
1299
|
variableCount: lines.length
|
|
1302
1300
|
});
|
|
1303
|
-
|
|
1301
|
+
const s = spinner2();
|
|
1302
|
+
s.start("Uploading secrets...");
|
|
1304
1303
|
const response = await pushSecrets(repoFullName, environment, content, accessToken);
|
|
1305
|
-
|
|
1304
|
+
s.stop("Uploaded");
|
|
1305
|
+
success(response.message);
|
|
1306
1306
|
if (response.stats) {
|
|
1307
1307
|
const { created, updated, deleted } = response.stats;
|
|
1308
1308
|
const parts = [];
|
|
1309
|
-
if (created > 0) parts.push(
|
|
1310
|
-
if (updated > 0) parts.push(
|
|
1311
|
-
if (deleted > 0) parts.push(
|
|
1309
|
+
if (created > 0) parts.push(pc3.green(`+${created} created`));
|
|
1310
|
+
if (updated > 0) parts.push(pc3.yellow(`~${updated} updated`));
|
|
1311
|
+
if (deleted > 0) parts.push(pc3.red(`-${deleted} deleted`));
|
|
1312
1312
|
if (parts.length > 0) {
|
|
1313
|
-
|
|
1313
|
+
message(`Stats: ${parts.join(", ")}`);
|
|
1314
1314
|
}
|
|
1315
1315
|
}
|
|
1316
|
-
console.log(`
|
|
1317
|
-
Your secrets are now encrypted and stored securely.`);
|
|
1318
1316
|
const dashboardLink = `https://www.keyway.sh/dashboard/vaults/${repoFullName}`;
|
|
1319
|
-
|
|
1320
|
-
${pc6.blue("\u2394")} Dashboard: ${pc6.underline(dashboardLink)}`);
|
|
1317
|
+
outro2(`Dashboard: ${link(dashboardLink)}`);
|
|
1321
1318
|
await shutdownAnalytics();
|
|
1322
|
-
} catch (
|
|
1323
|
-
let
|
|
1319
|
+
} catch (error2) {
|
|
1320
|
+
let message2;
|
|
1324
1321
|
let hint = null;
|
|
1325
|
-
if (
|
|
1326
|
-
|
|
1327
|
-
const envNotFoundMatch =
|
|
1322
|
+
if (error2 instanceof APIError) {
|
|
1323
|
+
message2 = error2.message || `HTTP ${error2.statusCode} - ${error2.error}`;
|
|
1324
|
+
const envNotFoundMatch = message2.match(/Environment '([^']+)' does not exist.*Available environments: ([^.]+)/);
|
|
1328
1325
|
if (envNotFoundMatch) {
|
|
1329
1326
|
const requestedEnv = envNotFoundMatch[1];
|
|
1330
1327
|
const availableEnvs = envNotFoundMatch[2];
|
|
1331
|
-
|
|
1328
|
+
message2 = `Environment '${requestedEnv}' does not exist in this vault.`;
|
|
1332
1329
|
hint = `Available environments: ${availableEnvs}
|
|
1333
|
-
Use ${
|
|
1330
|
+
Use ${command(`keyway push --env <environment>`)} to specify one.`;
|
|
1334
1331
|
}
|
|
1335
|
-
if (
|
|
1336
|
-
const upgradeMessage =
|
|
1337
|
-
const upgradeUrl =
|
|
1332
|
+
if (error2.statusCode === 403 && (error2.upgradeUrl || message2.toLowerCase().includes("read-only"))) {
|
|
1333
|
+
const upgradeMessage = message2.toLowerCase().includes("read-only") ? "This vault is read-only on your current plan." : message2;
|
|
1334
|
+
const upgradeUrl = error2.upgradeUrl || "https://keyway.sh/settings";
|
|
1338
1335
|
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
1339
1336
|
command: "push",
|
|
1340
1337
|
error: upgradeMessage
|
|
@@ -1343,21 +1340,19 @@ Use ${pc6.cyan(`keyway push --env <environment>`)} to specify one, or create '${
|
|
|
1343
1340
|
showUpgradePrompt(upgradeMessage, upgradeUrl);
|
|
1344
1341
|
process.exit(1);
|
|
1345
1342
|
}
|
|
1346
|
-
} else if (
|
|
1347
|
-
|
|
1343
|
+
} else if (error2 instanceof Error) {
|
|
1344
|
+
message2 = truncateMessage(error2.message);
|
|
1348
1345
|
} else {
|
|
1349
|
-
|
|
1346
|
+
message2 = "Unknown error";
|
|
1350
1347
|
}
|
|
1351
1348
|
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
1352
1349
|
command: "push",
|
|
1353
|
-
error:
|
|
1350
|
+
error: message2
|
|
1354
1351
|
});
|
|
1355
1352
|
await shutdownAnalytics();
|
|
1356
|
-
|
|
1357
|
-
\u2717 ${message}`));
|
|
1353
|
+
error(message2);
|
|
1358
1354
|
if (hint) {
|
|
1359
|
-
|
|
1360
|
-
${hint}`));
|
|
1355
|
+
message(dim(hint));
|
|
1361
1356
|
}
|
|
1362
1357
|
process.exit(1);
|
|
1363
1358
|
}
|
|
@@ -1390,19 +1385,16 @@ async function ensureLoginAndGitHubApp(repoFullName, options = {}) {
|
|
|
1390
1385
|
}
|
|
1391
1386
|
const deviceStart = await startDeviceLogin(repoFullName);
|
|
1392
1387
|
const installUrl = deviceStart.githubAppInstallUrl || "https://github.com/apps/keyway/installations/new";
|
|
1393
|
-
|
|
1394
|
-
const { shouldProceed } = await prompts6({
|
|
1395
|
-
type: "confirm",
|
|
1396
|
-
name: "shouldProceed",
|
|
1388
|
+
const shouldProceed = await confirm2({
|
|
1397
1389
|
message: "Open browser to sign in?",
|
|
1398
|
-
|
|
1390
|
+
initialValue: true
|
|
1399
1391
|
});
|
|
1400
1392
|
if (!shouldProceed) {
|
|
1401
1393
|
throw new Error('Setup required. Run "keyway init" when ready.');
|
|
1402
1394
|
}
|
|
1403
1395
|
await openUrl(deviceStart.verificationUriComplete);
|
|
1404
|
-
|
|
1405
|
-
|
|
1396
|
+
const loginSpinner = spinner2();
|
|
1397
|
+
loginSpinner.start("Waiting for authorization...");
|
|
1406
1398
|
const pollIntervalMs = Math.max((deviceStart.interval ?? 5) * 1e3, POLL_INTERVAL_MS);
|
|
1407
1399
|
const startTime = Date.now();
|
|
1408
1400
|
let accessToken = null;
|
|
@@ -1417,7 +1409,7 @@ async function ensureLoginAndGitHubApp(repoFullName, options = {}) {
|
|
|
1417
1409
|
githubLogin: result.githubLogin,
|
|
1418
1410
|
expiresAt: result.expiresAt
|
|
1419
1411
|
});
|
|
1420
|
-
|
|
1412
|
+
loginSpinner.stop("Signed in!");
|
|
1421
1413
|
if (result.githubLogin) {
|
|
1422
1414
|
identifyUser(result.githubLogin, {
|
|
1423
1415
|
github_username: result.githubLogin,
|
|
@@ -1427,46 +1419,38 @@ async function ensureLoginAndGitHubApp(repoFullName, options = {}) {
|
|
|
1427
1419
|
break;
|
|
1428
1420
|
}
|
|
1429
1421
|
consecutiveErrors = 0;
|
|
1430
|
-
|
|
1431
|
-
} catch (error) {
|
|
1422
|
+
} catch (error2) {
|
|
1432
1423
|
consecutiveErrors++;
|
|
1433
1424
|
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
1434
|
-
|
|
1425
|
+
loginSpinner.stop("Failed");
|
|
1426
|
+
const errorMsg = error2 instanceof Error ? error2.message : "Unknown error";
|
|
1435
1427
|
throw new Error(`Login failed after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${errorMsg}`);
|
|
1436
1428
|
}
|
|
1437
1429
|
}
|
|
1438
1430
|
}
|
|
1439
1431
|
if (!accessToken) {
|
|
1440
|
-
|
|
1441
|
-
|
|
1432
|
+
loginSpinner.stop("Timeout");
|
|
1433
|
+
warn("Timed out waiting for sign in.");
|
|
1442
1434
|
throw new Error("Sign in timed out. Please try again.");
|
|
1443
1435
|
}
|
|
1444
1436
|
const installStatus = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
|
|
1445
1437
|
if (installStatus.installed) {
|
|
1446
|
-
|
|
1447
|
-
console.log("");
|
|
1438
|
+
success("GitHub App installed");
|
|
1448
1439
|
return accessToken;
|
|
1449
1440
|
}
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
console.log("");
|
|
1454
|
-
const { shouldInstall } = await prompts6({
|
|
1455
|
-
type: "confirm",
|
|
1456
|
-
name: "shouldInstall",
|
|
1441
|
+
warn("GitHub App not installed on this repository");
|
|
1442
|
+
message(dim("The Keyway GitHub App is required for secure access."));
|
|
1443
|
+
const shouldInstall = await confirm2({
|
|
1457
1444
|
message: "Open browser to install GitHub App?",
|
|
1458
|
-
|
|
1445
|
+
initialValue: true
|
|
1459
1446
|
});
|
|
1460
1447
|
if (!shouldInstall) {
|
|
1461
|
-
|
|
1462
|
-
Install later: ${installUrl}`));
|
|
1448
|
+
message(dim(`Install later: ${installUrl}`));
|
|
1463
1449
|
throw new Error("GitHub App installation required.");
|
|
1464
1450
|
}
|
|
1465
1451
|
await openUrl(installUrl);
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
console.log(pc7.gray(" Then return here - the CLI will detect it automatically"));
|
|
1469
|
-
console.log(pc7.gray(" (Press Ctrl+C to cancel)\n"));
|
|
1452
|
+
const installSpinner = spinner2();
|
|
1453
|
+
installSpinner.start("Waiting for GitHub App installation...");
|
|
1470
1454
|
const installStartTime = Date.now();
|
|
1471
1455
|
consecutiveErrors = 0;
|
|
1472
1456
|
while (Date.now() - installStartTime < POLL_TIMEOUT_MS) {
|
|
@@ -1474,23 +1458,22 @@ async function ensureLoginAndGitHubApp(repoFullName, options = {}) {
|
|
|
1474
1458
|
try {
|
|
1475
1459
|
const pollStatus = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
|
|
1476
1460
|
if (pollStatus.installed) {
|
|
1477
|
-
|
|
1478
|
-
console.log("");
|
|
1461
|
+
installSpinner.stop("GitHub App installed!");
|
|
1479
1462
|
return accessToken;
|
|
1480
1463
|
}
|
|
1481
1464
|
consecutiveErrors = 0;
|
|
1482
|
-
|
|
1483
|
-
} catch (error) {
|
|
1465
|
+
} catch (error2) {
|
|
1484
1466
|
consecutiveErrors++;
|
|
1485
1467
|
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
1486
|
-
|
|
1468
|
+
installSpinner.stop("Failed");
|
|
1469
|
+
const errorMsg = error2 instanceof Error ? error2.message : "Unknown error";
|
|
1487
1470
|
throw new Error(`Installation check failed after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${errorMsg}`);
|
|
1488
1471
|
}
|
|
1489
1472
|
}
|
|
1490
1473
|
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1474
|
+
installSpinner.stop("Timeout");
|
|
1475
|
+
warn("Timed out waiting for installation.");
|
|
1476
|
+
message(dim(`Install the GitHub App: ${installUrl}`));
|
|
1494
1477
|
throw new Error("GitHub App installation timed out.");
|
|
1495
1478
|
}
|
|
1496
1479
|
async function ensureGitHubAppInstalledOnly(repoFullName, accessToken) {
|
|
@@ -1498,42 +1481,36 @@ async function ensureGitHubAppInstalledOnly(repoFullName, accessToken) {
|
|
|
1498
1481
|
let status;
|
|
1499
1482
|
try {
|
|
1500
1483
|
status = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
|
|
1501
|
-
} catch (
|
|
1502
|
-
if (
|
|
1503
|
-
|
|
1504
|
-
const { clearAuth: clearAuth2 } = await import("./auth-
|
|
1484
|
+
} catch (error2) {
|
|
1485
|
+
if (error2 instanceof APIError && error2.statusCode === 401) {
|
|
1486
|
+
warn("Session expired or invalid. Clearing credentials...");
|
|
1487
|
+
const { clearAuth: clearAuth2 } = await import("./auth-64V3RWUK.js");
|
|
1505
1488
|
clearAuth2();
|
|
1506
1489
|
return null;
|
|
1507
1490
|
}
|
|
1508
|
-
throw
|
|
1491
|
+
throw error2;
|
|
1509
1492
|
}
|
|
1510
1493
|
if (status.installed) {
|
|
1511
1494
|
return accessToken;
|
|
1512
1495
|
}
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
console.log(pc7.gray(" The Keyway GitHub App is required to securely manage secrets."));
|
|
1517
|
-
console.log(pc7.gray(" It only requests minimal permissions (repository metadata)."));
|
|
1518
|
-
console.log("");
|
|
1496
|
+
warn("GitHub App not installed for this repository");
|
|
1497
|
+
message(dim("The Keyway GitHub App is required to securely manage secrets."));
|
|
1498
|
+
message(dim("It only requests minimal permissions (repository metadata)."));
|
|
1519
1499
|
if (!isInteractive()) {
|
|
1520
|
-
|
|
1500
|
+
message(dim(`Install the Keyway GitHub App: ${status.installUrl}`));
|
|
1521
1501
|
throw new Error("GitHub App installation required.");
|
|
1522
1502
|
}
|
|
1523
|
-
const
|
|
1524
|
-
type: "confirm",
|
|
1525
|
-
name: "shouldInstall",
|
|
1503
|
+
const shouldInstall = await confirm2({
|
|
1526
1504
|
message: "Open browser to install Keyway GitHub App?",
|
|
1527
|
-
|
|
1505
|
+
initialValue: true
|
|
1528
1506
|
});
|
|
1529
1507
|
if (!shouldInstall) {
|
|
1530
|
-
|
|
1531
|
-
You can install later: ${status.installUrl}`));
|
|
1508
|
+
message(dim(`You can install later: ${status.installUrl}`));
|
|
1532
1509
|
throw new Error("GitHub App installation required.");
|
|
1533
1510
|
}
|
|
1534
1511
|
await openUrl(status.installUrl);
|
|
1535
|
-
|
|
1536
|
-
|
|
1512
|
+
const s = spinner2();
|
|
1513
|
+
s.start("Waiting for GitHub App installation...");
|
|
1537
1514
|
const startTime = Date.now();
|
|
1538
1515
|
let consecutiveErrors = 0;
|
|
1539
1516
|
while (Date.now() - startTime < POLL_TIMEOUT_MS) {
|
|
@@ -1541,251 +1518,266 @@ async function ensureGitHubAppInstalledOnly(repoFullName, accessToken) {
|
|
|
1541
1518
|
try {
|
|
1542
1519
|
const pollStatus = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
|
|
1543
1520
|
if (pollStatus.installed) {
|
|
1544
|
-
|
|
1545
|
-
console.log("");
|
|
1521
|
+
s.stop("GitHub App installed!");
|
|
1546
1522
|
return accessToken;
|
|
1547
1523
|
}
|
|
1548
1524
|
consecutiveErrors = 0;
|
|
1549
|
-
|
|
1550
|
-
} catch (error) {
|
|
1525
|
+
} catch (error2) {
|
|
1551
1526
|
consecutiveErrors++;
|
|
1552
1527
|
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
1553
|
-
|
|
1528
|
+
s.stop("Failed");
|
|
1529
|
+
const errorMsg = error2 instanceof Error ? error2.message : "Unknown error";
|
|
1554
1530
|
throw new Error(`Installation check failed after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${errorMsg}`);
|
|
1555
1531
|
}
|
|
1556
1532
|
}
|
|
1557
1533
|
}
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1534
|
+
s.stop("Timeout");
|
|
1535
|
+
warn("Timed out waiting for installation.");
|
|
1536
|
+
message(dim(`You can install the GitHub App later: ${status.installUrl}`));
|
|
1561
1537
|
throw new Error("GitHub App installation timed out.");
|
|
1562
1538
|
}
|
|
1563
1539
|
async function initCommand(options = {}) {
|
|
1564
1540
|
try {
|
|
1565
1541
|
const repoFullName = getCurrentRepoFullName();
|
|
1566
1542
|
const dashboardLink = `${DASHBOARD_URL}/${repoFullName}`;
|
|
1567
|
-
|
|
1568
|
-
|
|
1543
|
+
intro2("init");
|
|
1544
|
+
await warnIfEnvNotGitignored();
|
|
1545
|
+
step(`Repository: ${value(repoFullName)}`);
|
|
1569
1546
|
const accessToken = await ensureLoginAndGitHubApp(repoFullName, {
|
|
1570
1547
|
allowPrompt: options.loginPrompt !== false
|
|
1571
1548
|
});
|
|
1572
1549
|
trackEvent(AnalyticsEvents.CLI_INIT, { repoFullName, githubAppInstalled: true });
|
|
1573
1550
|
const vaultExists = await checkVaultExists(accessToken, repoFullName);
|
|
1574
1551
|
if (vaultExists) {
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
console.log("");
|
|
1552
|
+
success("Already initialized!");
|
|
1553
|
+
message(dim(`Run ${command("keyway push")} to sync your secrets`));
|
|
1554
|
+
outro2(`Dashboard: ${link(dashboardLink)}`);
|
|
1579
1555
|
await shutdownAnalytics();
|
|
1580
1556
|
return;
|
|
1581
1557
|
}
|
|
1582
1558
|
await initVault(repoFullName, accessToken);
|
|
1583
|
-
|
|
1559
|
+
success("Vault created!");
|
|
1584
1560
|
try {
|
|
1585
1561
|
const badgeAdded = await addBadgeToReadme(true);
|
|
1586
1562
|
if (badgeAdded) {
|
|
1587
|
-
|
|
1563
|
+
success("Badge added to README.md");
|
|
1588
1564
|
}
|
|
1589
1565
|
} catch {
|
|
1590
1566
|
}
|
|
1591
|
-
console.log("");
|
|
1592
1567
|
const envCandidates = discoverEnvCandidates(process.cwd());
|
|
1593
|
-
const
|
|
1594
|
-
if (envCandidates.length > 0 &&
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
const { shouldPush } = await prompts6({
|
|
1598
|
-
type: "confirm",
|
|
1599
|
-
name: "shouldPush",
|
|
1568
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|
|
1569
|
+
if (envCandidates.length > 0 && interactive) {
|
|
1570
|
+
message(dim(`Found ${envCandidates.length} env file(s): ${envCandidates.map((c) => c.file).join(", ")}`));
|
|
1571
|
+
const shouldPush = await confirm2({
|
|
1600
1572
|
message: "Push secrets now?",
|
|
1601
|
-
|
|
1573
|
+
initialValue: true
|
|
1602
1574
|
});
|
|
1603
1575
|
if (shouldPush) {
|
|
1604
|
-
console.log("");
|
|
1605
1576
|
await pushCommand({ loginPrompt: false, yes: false });
|
|
1606
1577
|
return;
|
|
1607
1578
|
}
|
|
1608
1579
|
}
|
|
1609
|
-
console.log(pc7.dim("\u2500".repeat(50)));
|
|
1610
|
-
console.log("");
|
|
1611
1580
|
if (envCandidates.length === 0) {
|
|
1612
|
-
if (
|
|
1581
|
+
if (interactive) {
|
|
1613
1582
|
const created = await promptCreateEnvFile();
|
|
1614
1583
|
if (created) {
|
|
1615
|
-
|
|
1616
|
-
`);
|
|
1584
|
+
message(dim(`Add your variables and run ${command("keyway push")}`));
|
|
1617
1585
|
} else {
|
|
1618
|
-
|
|
1619
|
-
`);
|
|
1586
|
+
message(dim(`Next: Create ${file(".env")} and run ${command("keyway push")}`));
|
|
1620
1587
|
}
|
|
1621
1588
|
} else {
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
`);
|
|
1589
|
+
warn("No .env file found - your vault is empty");
|
|
1590
|
+
message(dim(`Next: Create ${file(".env")} and run ${command("keyway push")}`));
|
|
1625
1591
|
}
|
|
1626
1592
|
} else {
|
|
1627
|
-
|
|
1628
|
-
`);
|
|
1593
|
+
message(dim(`Run ${command("keyway push")} to sync your secrets`));
|
|
1629
1594
|
}
|
|
1630
|
-
|
|
1631
|
-
console.log("");
|
|
1595
|
+
outro2(`Dashboard: ${link(dashboardLink)}`);
|
|
1632
1596
|
await shutdownAnalytics();
|
|
1633
|
-
} catch (
|
|
1634
|
-
if (
|
|
1635
|
-
if (
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
console.log("");
|
|
1597
|
+
} catch (error2) {
|
|
1598
|
+
if (error2 instanceof APIError) {
|
|
1599
|
+
if (error2.statusCode === 409) {
|
|
1600
|
+
success("Already initialized!");
|
|
1601
|
+
message(dim(`Run ${command("keyway push")} to sync your secrets`));
|
|
1602
|
+
outro2(`Dashboard: ${link(`${DASHBOARD_URL}/${getCurrentRepoFullName()}`)}`);
|
|
1640
1603
|
await shutdownAnalytics();
|
|
1641
1604
|
return;
|
|
1642
1605
|
}
|
|
1643
|
-
if (
|
|
1644
|
-
const upgradeUrl =
|
|
1645
|
-
showUpgradePrompt(
|
|
1606
|
+
if (error2.error === "Plan Limit Reached" || error2.upgradeUrl) {
|
|
1607
|
+
const upgradeUrl = error2.upgradeUrl || "https://keyway.sh/pricing";
|
|
1608
|
+
showUpgradePrompt(error2.message, upgradeUrl);
|
|
1646
1609
|
await shutdownAnalytics();
|
|
1647
1610
|
process.exit(1);
|
|
1648
1611
|
}
|
|
1649
1612
|
}
|
|
1650
|
-
const
|
|
1613
|
+
const message2 = error2 instanceof APIError ? error2.message : error2 instanceof Error ? truncateMessage(error2.message) : "Unknown error";
|
|
1651
1614
|
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
1652
1615
|
command: "init",
|
|
1653
|
-
error:
|
|
1616
|
+
error: message2
|
|
1654
1617
|
});
|
|
1655
1618
|
await shutdownAnalytics();
|
|
1656
|
-
|
|
1657
|
-
\u2717 ${message}`));
|
|
1619
|
+
error(message2);
|
|
1658
1620
|
process.exit(1);
|
|
1659
1621
|
}
|
|
1660
1622
|
}
|
|
1661
1623
|
|
|
1662
1624
|
// src/cmds/pull.ts
|
|
1663
|
-
import pc8 from "picocolors";
|
|
1664
1625
|
import fs6 from "fs";
|
|
1665
1626
|
import path6 from "path";
|
|
1666
|
-
import prompts7 from "prompts";
|
|
1667
1627
|
async function pullCommand(options) {
|
|
1628
|
+
intro2("pull");
|
|
1629
|
+
await warnIfEnvNotGitignored();
|
|
1668
1630
|
try {
|
|
1669
1631
|
const environment = options.env || "development";
|
|
1670
1632
|
const envFile = options.file || ".env";
|
|
1671
|
-
|
|
1672
|
-
console.log(`Environment: ${pc8.cyan(environment)}`);
|
|
1633
|
+
step(`Environment: ${value(environment)}`);
|
|
1673
1634
|
const repoFullName = getCurrentRepoFullName();
|
|
1674
|
-
|
|
1635
|
+
step(`Repository: ${value(repoFullName)}`);
|
|
1675
1636
|
const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
|
|
1676
1637
|
trackEvent(AnalyticsEvents.CLI_PULL, {
|
|
1677
1638
|
repoFullName,
|
|
1678
1639
|
environment
|
|
1679
1640
|
});
|
|
1680
|
-
|
|
1641
|
+
const s = spinner2();
|
|
1642
|
+
s.start("Downloading secrets...");
|
|
1681
1643
|
const response = await pullSecrets(repoFullName, environment, accessToken);
|
|
1682
1644
|
const envFilePath = path6.resolve(process.cwd(), envFile);
|
|
1683
1645
|
if (fs6.existsSync(envFilePath)) {
|
|
1646
|
+
s.stop("Downloaded");
|
|
1684
1647
|
const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
|
|
1685
1648
|
if (options.yes) {
|
|
1686
|
-
|
|
1687
|
-
\u26A0 Overwriting existing file: ${envFile}`));
|
|
1649
|
+
warn(`Overwriting existing file: ${envFile}`);
|
|
1688
1650
|
} else if (!isInteractive2) {
|
|
1689
1651
|
throw new Error(`File ${envFile} exists. Re-run with --yes to overwrite or choose a different --file.`);
|
|
1690
1652
|
} else {
|
|
1691
|
-
const
|
|
1692
|
-
{
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
},
|
|
1698
|
-
{
|
|
1699
|
-
onCancel: () => {
|
|
1700
|
-
throw new Error("Pull cancelled by user.");
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
);
|
|
1704
|
-
if (!confirm) {
|
|
1705
|
-
console.log(pc8.yellow("Pull aborted."));
|
|
1653
|
+
const confirm3 = await confirm2({
|
|
1654
|
+
message: `${envFile} exists. Overwrite with secrets from ${environment}?`,
|
|
1655
|
+
initialValue: false
|
|
1656
|
+
});
|
|
1657
|
+
if (!confirm3) {
|
|
1658
|
+
warn("Pull aborted.");
|
|
1706
1659
|
return;
|
|
1707
1660
|
}
|
|
1708
1661
|
}
|
|
1662
|
+
} else {
|
|
1663
|
+
s.stop("Downloaded");
|
|
1709
1664
|
}
|
|
1710
1665
|
fs6.writeFileSync(envFilePath, response.content, "utf-8");
|
|
1711
1666
|
const lines = response.content.split("\n").filter((line) => {
|
|
1712
1667
|
const trimmed = line.trim();
|
|
1713
1668
|
return trimmed.length > 0 && !trimmed.startsWith("#");
|
|
1714
1669
|
});
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
File: ${pc8.cyan(envFile)}`);
|
|
1719
|
-
console.log(`Variables: ${pc8.cyan(lines.length.toString())}`);
|
|
1670
|
+
success(`Secrets downloaded to ${file(envFile)}`);
|
|
1671
|
+
message(`Variables: ${value(lines.length)}`);
|
|
1672
|
+
outro2("Secrets synced!");
|
|
1720
1673
|
await shutdownAnalytics();
|
|
1721
|
-
} catch (
|
|
1722
|
-
const
|
|
1674
|
+
} catch (error2) {
|
|
1675
|
+
const message2 = error2 instanceof APIError ? `API ${error2.statusCode}: ${error2.message}` : error2 instanceof Error ? truncateMessage(error2.message) : "Unknown error";
|
|
1723
1676
|
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
1724
1677
|
command: "pull",
|
|
1725
|
-
error:
|
|
1678
|
+
error: message2
|
|
1726
1679
|
});
|
|
1727
1680
|
await shutdownAnalytics();
|
|
1728
|
-
|
|
1729
|
-
\u2717 ${message}`));
|
|
1681
|
+
error(message2);
|
|
1730
1682
|
process.exit(1);
|
|
1731
1683
|
}
|
|
1732
1684
|
}
|
|
1733
1685
|
|
|
1734
1686
|
// src/cmds/doctor.ts
|
|
1735
|
-
import
|
|
1687
|
+
import pc4 from "picocolors";
|
|
1736
1688
|
|
|
1737
1689
|
// src/core/doctor.ts
|
|
1738
1690
|
import { execSync as execSync2 } from "child_process";
|
|
1739
|
-
import {
|
|
1740
|
-
import { tmpdir } from "os";
|
|
1741
|
-
import { join } from "path";
|
|
1691
|
+
import { readFileSync, existsSync } from "fs";
|
|
1742
1692
|
var API_HEALTH_URL = `${process.env.KEYWAY_API_URL || INTERNAL_API_URL}/v1/health`;
|
|
1743
|
-
async function
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1693
|
+
async function checkAuth() {
|
|
1694
|
+
try {
|
|
1695
|
+
const auth = await getStoredAuth();
|
|
1696
|
+
if (!auth) {
|
|
1697
|
+
return {
|
|
1698
|
+
id: "auth",
|
|
1699
|
+
name: "Authentication",
|
|
1700
|
+
status: "warn",
|
|
1701
|
+
detail: "Not logged in. Run: keyway login"
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
try {
|
|
1705
|
+
const result = await validateToken(auth.keywayToken);
|
|
1706
|
+
return {
|
|
1707
|
+
id: "auth",
|
|
1708
|
+
name: "Authentication",
|
|
1709
|
+
status: "pass",
|
|
1710
|
+
detail: `Logged in as ${result.login || auth.githubLogin || "user"}`
|
|
1711
|
+
};
|
|
1712
|
+
} catch {
|
|
1713
|
+
return {
|
|
1714
|
+
id: "auth",
|
|
1715
|
+
name: "Authentication",
|
|
1716
|
+
status: "warn",
|
|
1717
|
+
detail: "Token expired or invalid. Run: keyway login"
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
} catch {
|
|
1747
1721
|
return {
|
|
1748
|
-
id: "
|
|
1749
|
-
name: "
|
|
1750
|
-
status: "
|
|
1751
|
-
detail:
|
|
1722
|
+
id: "auth",
|
|
1723
|
+
name: "Authentication",
|
|
1724
|
+
status: "warn",
|
|
1725
|
+
detail: "Unable to check authentication status"
|
|
1752
1726
|
};
|
|
1753
1727
|
}
|
|
1754
|
-
return {
|
|
1755
|
-
id: "node",
|
|
1756
|
-
name: "Node.js version",
|
|
1757
|
-
status: "fail",
|
|
1758
|
-
detail: `v${nodeVersion} (<18.0.0, please upgrade)`
|
|
1759
|
-
};
|
|
1760
1728
|
}
|
|
1761
|
-
async function
|
|
1729
|
+
async function checkGitHubRemote() {
|
|
1762
1730
|
try {
|
|
1763
|
-
const gitVersion = execSync2("git --version", { encoding: "utf-8" }).trim();
|
|
1764
1731
|
try {
|
|
1765
1732
|
execSync2("git rev-parse --is-inside-work-tree", {
|
|
1766
1733
|
encoding: "utf-8",
|
|
1767
1734
|
stdio: ["pipe", "pipe", "ignore"]
|
|
1768
1735
|
});
|
|
1736
|
+
} catch {
|
|
1769
1737
|
return {
|
|
1770
|
-
id: "
|
|
1771
|
-
name: "
|
|
1772
|
-
status: "
|
|
1773
|
-
detail:
|
|
1738
|
+
id: "github",
|
|
1739
|
+
name: "GitHub repository",
|
|
1740
|
+
status: "warn",
|
|
1741
|
+
detail: "Not in a git repository"
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
try {
|
|
1745
|
+
const remoteUrl = execSync2("git remote get-url origin", {
|
|
1746
|
+
encoding: "utf-8",
|
|
1747
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
1748
|
+
}).trim();
|
|
1749
|
+
const sshMatch = remoteUrl.match(/git@github\.com:(.+)\/(.+?)(\.git)?$/);
|
|
1750
|
+
const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/(.+)\/(.+?)(\.git)?$/);
|
|
1751
|
+
if (sshMatch || httpsMatch) {
|
|
1752
|
+
const match = sshMatch || httpsMatch;
|
|
1753
|
+
const repoName = `${match[1]}/${match[2]}`;
|
|
1754
|
+
return {
|
|
1755
|
+
id: "github",
|
|
1756
|
+
name: "GitHub repository",
|
|
1757
|
+
status: "pass",
|
|
1758
|
+
detail: repoName
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
return {
|
|
1762
|
+
id: "github",
|
|
1763
|
+
name: "GitHub repository",
|
|
1764
|
+
status: "warn",
|
|
1765
|
+
detail: "Remote is not a GitHub URL"
|
|
1774
1766
|
};
|
|
1775
1767
|
} catch {
|
|
1776
1768
|
return {
|
|
1777
|
-
id: "
|
|
1778
|
-
name: "
|
|
1769
|
+
id: "github",
|
|
1770
|
+
name: "GitHub repository",
|
|
1779
1771
|
status: "warn",
|
|
1780
|
-
detail:
|
|
1772
|
+
detail: "No remote origin configured"
|
|
1781
1773
|
};
|
|
1782
1774
|
}
|
|
1783
1775
|
} catch {
|
|
1784
1776
|
return {
|
|
1785
|
-
id: "
|
|
1786
|
-
name: "
|
|
1777
|
+
id: "github",
|
|
1778
|
+
name: "GitHub repository",
|
|
1787
1779
|
status: "warn",
|
|
1788
|
-
detail: "
|
|
1780
|
+
detail: "Unable to detect repository"
|
|
1789
1781
|
};
|
|
1790
1782
|
}
|
|
1791
1783
|
}
|
|
@@ -1821,8 +1813,8 @@ async function checkNetwork() {
|
|
|
1821
1813
|
status: "warn",
|
|
1822
1814
|
detail: `Server returned ${response.status}`
|
|
1823
1815
|
};
|
|
1824
|
-
} catch (
|
|
1825
|
-
if (
|
|
1816
|
+
} catch (error2) {
|
|
1817
|
+
if (error2.name === "AbortError") {
|
|
1826
1818
|
return {
|
|
1827
1819
|
id: "network",
|
|
1828
1820
|
name: "API connectivity",
|
|
@@ -1830,7 +1822,7 @@ async function checkNetwork() {
|
|
|
1830
1822
|
detail: "Connection timeout (>2s)"
|
|
1831
1823
|
};
|
|
1832
1824
|
}
|
|
1833
|
-
if (
|
|
1825
|
+
if (error2.code === "ENOTFOUND") {
|
|
1834
1826
|
return {
|
|
1835
1827
|
id: "network",
|
|
1836
1828
|
name: "API connectivity",
|
|
@@ -1838,7 +1830,7 @@ async function checkNetwork() {
|
|
|
1838
1830
|
detail: "DNS resolution failed"
|
|
1839
1831
|
};
|
|
1840
1832
|
}
|
|
1841
|
-
if (
|
|
1833
|
+
if (error2.code === "CERT_HAS_EXPIRED" || error2.code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") {
|
|
1842
1834
|
return {
|
|
1843
1835
|
id: "network",
|
|
1844
1836
|
name: "API connectivity",
|
|
@@ -1850,27 +1842,103 @@ async function checkNetwork() {
|
|
|
1850
1842
|
id: "network",
|
|
1851
1843
|
name: "API connectivity",
|
|
1852
1844
|
status: "warn",
|
|
1853
|
-
detail:
|
|
1845
|
+
detail: error2.message || "Connection failed"
|
|
1854
1846
|
};
|
|
1855
1847
|
}
|
|
1856
1848
|
}
|
|
1857
|
-
async function
|
|
1858
|
-
const
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1849
|
+
async function checkEnvFile() {
|
|
1850
|
+
const envFiles = [".env", ".env.local", ".env.development"];
|
|
1851
|
+
const found = [];
|
|
1852
|
+
for (const file2 of envFiles) {
|
|
1853
|
+
if (existsSync(file2)) {
|
|
1854
|
+
found.push(file2);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
if (found.length === 0) {
|
|
1862
1858
|
return {
|
|
1863
|
-
id: "
|
|
1864
|
-
name: "
|
|
1865
|
-
status: "
|
|
1866
|
-
detail: "
|
|
1859
|
+
id: "envfile",
|
|
1860
|
+
name: "Environment file",
|
|
1861
|
+
status: "warn",
|
|
1862
|
+
detail: "No .env file found. Run: keyway pull"
|
|
1867
1863
|
};
|
|
1868
|
-
}
|
|
1864
|
+
}
|
|
1865
|
+
return {
|
|
1866
|
+
id: "envfile",
|
|
1867
|
+
name: "Environment file",
|
|
1868
|
+
status: "pass",
|
|
1869
|
+
detail: found.join(", ")
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
async function checkSyncs(repoFullName) {
|
|
1873
|
+
if (!repoFullName) {
|
|
1869
1874
|
return {
|
|
1870
|
-
id: "
|
|
1871
|
-
name: "
|
|
1872
|
-
status: "
|
|
1873
|
-
detail:
|
|
1875
|
+
id: "syncs",
|
|
1876
|
+
name: "Provider syncs",
|
|
1877
|
+
status: "warn",
|
|
1878
|
+
detail: "Not in a GitHub repository"
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
try {
|
|
1882
|
+
const auth = await getStoredAuth();
|
|
1883
|
+
if (!auth) {
|
|
1884
|
+
return {
|
|
1885
|
+
id: "syncs",
|
|
1886
|
+
name: "Provider syncs",
|
|
1887
|
+
status: "warn",
|
|
1888
|
+
detail: "Login required to check"
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
try {
|
|
1892
|
+
const { connections } = await getConnections(auth.keywayToken);
|
|
1893
|
+
if (connections.length === 0) {
|
|
1894
|
+
return {
|
|
1895
|
+
id: "syncs",
|
|
1896
|
+
name: "Provider syncs",
|
|
1897
|
+
status: "pass",
|
|
1898
|
+
detail: "No integrations connected"
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
const providers = [...new Set(connections.map((c) => c.provider))];
|
|
1902
|
+
const linkedProviders = [];
|
|
1903
|
+
const repoLower = repoFullName.toLowerCase();
|
|
1904
|
+
for (const provider of providers) {
|
|
1905
|
+
try {
|
|
1906
|
+
const { projects } = await getAllProviderProjects(auth.keywayToken, provider);
|
|
1907
|
+
const hasLinked = projects.some((p5) => p5.linkedRepo?.toLowerCase() === repoLower);
|
|
1908
|
+
if (hasLinked) {
|
|
1909
|
+
linkedProviders.push(provider);
|
|
1910
|
+
}
|
|
1911
|
+
} catch {
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
if (linkedProviders.length === 0) {
|
|
1915
|
+
return {
|
|
1916
|
+
id: "syncs",
|
|
1917
|
+
name: "Provider syncs",
|
|
1918
|
+
status: "pass",
|
|
1919
|
+
detail: "None for this repo"
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
return {
|
|
1923
|
+
id: "syncs",
|
|
1924
|
+
name: "Provider syncs",
|
|
1925
|
+
status: "pass",
|
|
1926
|
+
detail: linkedProviders.join(", ")
|
|
1927
|
+
};
|
|
1928
|
+
} catch {
|
|
1929
|
+
return {
|
|
1930
|
+
id: "syncs",
|
|
1931
|
+
name: "Provider syncs",
|
|
1932
|
+
status: "warn",
|
|
1933
|
+
detail: "Unable to check"
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
} catch {
|
|
1937
|
+
return {
|
|
1938
|
+
id: "syncs",
|
|
1939
|
+
name: "Provider syncs",
|
|
1940
|
+
status: "warn",
|
|
1941
|
+
detail: "Unable to check"
|
|
1874
1942
|
};
|
|
1875
1943
|
}
|
|
1876
1944
|
}
|
|
@@ -1910,59 +1978,17 @@ async function checkGitignore() {
|
|
|
1910
1978
|
};
|
|
1911
1979
|
}
|
|
1912
1980
|
}
|
|
1913
|
-
async function checkSystemClock() {
|
|
1914
|
-
try {
|
|
1915
|
-
const controller = new AbortController();
|
|
1916
|
-
const timeout = setTimeout(() => controller.abort(), 2e3);
|
|
1917
|
-
const response = await fetch(API_HEALTH_URL, {
|
|
1918
|
-
method: "HEAD",
|
|
1919
|
-
signal: controller.signal
|
|
1920
|
-
});
|
|
1921
|
-
clearTimeout(timeout);
|
|
1922
|
-
const serverDate = response.headers.get("date");
|
|
1923
|
-
if (!serverDate) {
|
|
1924
|
-
return {
|
|
1925
|
-
id: "clock",
|
|
1926
|
-
name: "System clock",
|
|
1927
|
-
status: "pass",
|
|
1928
|
-
detail: "Unable to verify (no server date)"
|
|
1929
|
-
};
|
|
1930
|
-
}
|
|
1931
|
-
const serverTime = new Date(serverDate).getTime();
|
|
1932
|
-
const localTime = Date.now();
|
|
1933
|
-
const diffMinutes = Math.abs(serverTime - localTime) / 1e3 / 60;
|
|
1934
|
-
if (diffMinutes < 5) {
|
|
1935
|
-
return {
|
|
1936
|
-
id: "clock",
|
|
1937
|
-
name: "System clock",
|
|
1938
|
-
status: "pass",
|
|
1939
|
-
detail: `Synchronized (drift: ${Math.round(diffMinutes * 60)}s)`
|
|
1940
|
-
};
|
|
1941
|
-
}
|
|
1942
|
-
return {
|
|
1943
|
-
id: "clock",
|
|
1944
|
-
name: "System clock",
|
|
1945
|
-
status: "warn",
|
|
1946
|
-
detail: `Clock drift: ${Math.round(diffMinutes)} minutes`
|
|
1947
|
-
};
|
|
1948
|
-
} catch {
|
|
1949
|
-
return {
|
|
1950
|
-
id: "clock",
|
|
1951
|
-
name: "System clock",
|
|
1952
|
-
status: "pass",
|
|
1953
|
-
detail: "Unable to verify"
|
|
1954
|
-
};
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
1981
|
async function runAllChecks(options = {}) {
|
|
1958
|
-
const
|
|
1959
|
-
|
|
1960
|
-
|
|
1982
|
+
const githubResult = await checkGitHubRemote();
|
|
1983
|
+
const repoFullName = githubResult.status === "pass" ? githubResult.detail || null : null;
|
|
1984
|
+
const [authResult, networkResult, syncsResult, envFileResult, gitignoreResult] = await Promise.all([
|
|
1985
|
+
checkAuth(),
|
|
1961
1986
|
checkNetwork(),
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1987
|
+
checkSyncs(repoFullName),
|
|
1988
|
+
checkEnvFile(),
|
|
1989
|
+
checkGitignore()
|
|
1965
1990
|
]);
|
|
1991
|
+
const checks = [authResult, githubResult, networkResult, syncsResult, envFileResult, gitignoreResult];
|
|
1966
1992
|
if (options.strict) {
|
|
1967
1993
|
checks.forEach((check) => {
|
|
1968
1994
|
if (check.status === "warn") {
|
|
@@ -1986,9 +2012,9 @@ async function runAllChecks(options = {}) {
|
|
|
1986
2012
|
// src/cmds/doctor.ts
|
|
1987
2013
|
function formatSummary(results) {
|
|
1988
2014
|
const parts = [
|
|
1989
|
-
|
|
1990
|
-
results.summary.warn > 0 ?
|
|
1991
|
-
results.summary.fail > 0 ?
|
|
2015
|
+
pc4.green(`${results.summary.pass} passed`),
|
|
2016
|
+
results.summary.warn > 0 ? pc4.yellow(`${results.summary.warn} warnings`) : null,
|
|
2017
|
+
results.summary.fail > 0 ? pc4.red(`${results.summary.fail} failed`) : null
|
|
1992
2018
|
].filter(Boolean);
|
|
1993
2019
|
return parts.join(", ");
|
|
1994
2020
|
}
|
|
@@ -2005,24 +2031,28 @@ async function doctorCommand(options = {}) {
|
|
|
2005
2031
|
process.stdout.write(JSON.stringify(results, null, 0) + "\n");
|
|
2006
2032
|
process.exit(results.exitCode);
|
|
2007
2033
|
}
|
|
2008
|
-
|
|
2034
|
+
intro2("doctor");
|
|
2009
2035
|
results.checks.forEach((check) => {
|
|
2010
|
-
const
|
|
2011
|
-
|
|
2012
|
-
|
|
2036
|
+
const detail = check.detail ? dim(` \u2014 ${check.detail}`) : "";
|
|
2037
|
+
if (check.status === "pass") {
|
|
2038
|
+
success(`${check.name}${detail}`);
|
|
2039
|
+
} else if (check.status === "warn") {
|
|
2040
|
+
warn(`${check.name}${detail}`);
|
|
2041
|
+
} else {
|
|
2042
|
+
error(`${check.name}${detail}`);
|
|
2043
|
+
}
|
|
2013
2044
|
});
|
|
2014
|
-
|
|
2015
|
-
Summary: ${formatSummary(results)}`);
|
|
2045
|
+
message(`Summary: ${formatSummary(results)}`);
|
|
2016
2046
|
if (results.summary.fail > 0) {
|
|
2017
|
-
|
|
2047
|
+
outro2("Some checks failed. Please resolve the issues above.");
|
|
2018
2048
|
} else if (results.summary.warn > 0) {
|
|
2019
|
-
|
|
2049
|
+
outro2("Some warnings detected. Keyway should work but consider addressing them.");
|
|
2020
2050
|
} else {
|
|
2021
|
-
|
|
2051
|
+
outro2("All checks passed! Your environment is ready.");
|
|
2022
2052
|
}
|
|
2023
2053
|
process.exit(results.exitCode);
|
|
2024
|
-
} catch (
|
|
2025
|
-
const
|
|
2054
|
+
} catch (error2) {
|
|
2055
|
+
const message2 = error2 instanceof Error ? truncateMessage(error2.message) : "Doctor failed";
|
|
2026
2056
|
trackEvent(AnalyticsEvents.CLI_DOCTOR, {
|
|
2027
2057
|
pass: 0,
|
|
2028
2058
|
warn: 0,
|
|
@@ -2035,12 +2065,11 @@ Summary: ${formatSummary(results)}`);
|
|
|
2035
2065
|
checks: [],
|
|
2036
2066
|
summary: { pass: 0, warn: 0, fail: 1 },
|
|
2037
2067
|
exitCode: 1,
|
|
2038
|
-
error:
|
|
2068
|
+
error: message2
|
|
2039
2069
|
};
|
|
2040
2070
|
process.stdout.write(JSON.stringify(errorResult, null, 0) + "\n");
|
|
2041
2071
|
} else {
|
|
2042
|
-
|
|
2043
|
-
\u2717 ${message}`));
|
|
2072
|
+
error(message2);
|
|
2044
2073
|
}
|
|
2045
2074
|
process.exit(1);
|
|
2046
2075
|
}
|
|
@@ -2049,8 +2078,7 @@ Summary: ${formatSummary(results)}`);
|
|
|
2049
2078
|
// src/cmds/ci.ts
|
|
2050
2079
|
import { execSync as execSync3 } from "child_process";
|
|
2051
2080
|
import { Octokit } from "@octokit/rest";
|
|
2052
|
-
import
|
|
2053
|
-
import prompts8 from "prompts";
|
|
2081
|
+
import * as p3 from "@clack/prompts";
|
|
2054
2082
|
function isGhAvailable() {
|
|
2055
2083
|
try {
|
|
2056
2084
|
execSync3("gh auth status", { stdio: "ignore" });
|
|
@@ -2068,89 +2096,82 @@ function addSecretWithGh(repo, secretName, secretValue) {
|
|
|
2068
2096
|
async function ciSetupCommand(options) {
|
|
2069
2097
|
const repo = options.repo || detectGitRepo();
|
|
2070
2098
|
if (!repo) {
|
|
2071
|
-
|
|
2099
|
+
error("Not in a git repository. Use --repo owner/repo");
|
|
2072
2100
|
process.exit(1);
|
|
2073
2101
|
}
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
console.log(pc10.dim("Step 1: Keyway Authentication"));
|
|
2102
|
+
intro2("ci setup");
|
|
2103
|
+
step(`Setting up GitHub Actions for ${value(repo)}`);
|
|
2104
|
+
message(dim("Step 1: Keyway Authentication"));
|
|
2078
2105
|
let keywayToken;
|
|
2079
2106
|
try {
|
|
2080
2107
|
keywayToken = await ensureLogin({ allowPrompt: true });
|
|
2081
|
-
|
|
2108
|
+
success("Authenticated with Keyway");
|
|
2082
2109
|
} catch {
|
|
2083
|
-
|
|
2084
|
-
|
|
2110
|
+
error("Failed to authenticate with Keyway");
|
|
2111
|
+
message(dim("Run `keyway login` first"));
|
|
2085
2112
|
process.exit(1);
|
|
2086
2113
|
}
|
|
2087
2114
|
const useGh = isGhAvailable();
|
|
2088
2115
|
if (useGh) {
|
|
2089
|
-
|
|
2116
|
+
message(dim("Step 2: Adding secret via GitHub CLI"));
|
|
2090
2117
|
try {
|
|
2091
2118
|
addSecretWithGh(repo, "KEYWAY_TOKEN", keywayToken);
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
console.error(pc10.dim(" Try running: gh auth login"));
|
|
2119
|
+
success(`Secret KEYWAY_TOKEN added to ${repo}`);
|
|
2120
|
+
} catch (error2) {
|
|
2121
|
+
const message2 = error2 instanceof Error ? error2.message : String(error2);
|
|
2122
|
+
error(`Failed to add secret: ${message2}`);
|
|
2123
|
+
message(dim("Try running: gh auth login"));
|
|
2098
2124
|
process.exit(1);
|
|
2099
2125
|
}
|
|
2100
2126
|
} else {
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2127
|
+
message(dim("Step 2: Temporary GitHub PAT"));
|
|
2128
|
+
info("gh CLI not found. We need a one-time GitHub PAT.");
|
|
2129
|
+
message(dim("You can delete it immediately after setup."));
|
|
2104
2130
|
const patUrl = "https://github.com/settings/tokens/new?scopes=repo&description=Keyway%20CI%20Setup%20(temporary)";
|
|
2105
2131
|
await openUrl(patUrl);
|
|
2106
|
-
const
|
|
2107
|
-
type: "password",
|
|
2108
|
-
name: "githubToken",
|
|
2132
|
+
const githubToken = await p3.password({
|
|
2109
2133
|
message: "Paste your GitHub PAT:"
|
|
2110
2134
|
});
|
|
2111
|
-
if (!githubToken) {
|
|
2112
|
-
|
|
2135
|
+
if (p3.isCancel(githubToken) || !githubToken) {
|
|
2136
|
+
error("GitHub PAT is required");
|
|
2113
2137
|
process.exit(1);
|
|
2114
2138
|
}
|
|
2115
2139
|
const octokit = new Octokit({ auth: githubToken });
|
|
2116
2140
|
try {
|
|
2117
2141
|
await octokit.users.getAuthenticated();
|
|
2118
|
-
|
|
2142
|
+
success("GitHub PAT validated");
|
|
2119
2143
|
} catch {
|
|
2120
|
-
|
|
2144
|
+
error("Invalid GitHub PAT");
|
|
2121
2145
|
process.exit(1);
|
|
2122
2146
|
}
|
|
2123
|
-
|
|
2147
|
+
message(dim("Step 3: Adding secret to repository"));
|
|
2124
2148
|
const [owner, repoName] = repo.split("/");
|
|
2125
2149
|
try {
|
|
2126
2150
|
await addRepoSecret(octokit, owner, repoName, "KEYWAY_TOKEN", keywayToken);
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
console.error(pc10.dim(" Make sure the PAT has access to this repository"));
|
|
2151
|
+
success(`Secret KEYWAY_TOKEN added to ${repo}`);
|
|
2152
|
+
} catch (error2) {
|
|
2153
|
+
const message2 = error2 instanceof Error ? error2.message : String(error2);
|
|
2154
|
+
if (message2.includes("Not Found")) {
|
|
2155
|
+
error(`Repository not found or no access: ${repo}`);
|
|
2156
|
+
message(dim("Make sure the PAT has access to this repository"));
|
|
2134
2157
|
} else {
|
|
2135
|
-
|
|
2158
|
+
error(`Failed to add secret: ${message2}`);
|
|
2136
2159
|
}
|
|
2137
2160
|
process.exit(1);
|
|
2138
2161
|
}
|
|
2139
2162
|
}
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2163
|
+
success("Setup complete!");
|
|
2164
|
+
note2(
|
|
2165
|
+
`- uses: keywaysh/keyway-action@v1
|
|
2166
|
+
with:
|
|
2167
|
+
token: \${{ secrets.KEYWAY_TOKEN }}
|
|
2168
|
+
environment: production`,
|
|
2169
|
+
"Add this to your workflow"
|
|
2147
2170
|
);
|
|
2148
|
-
console.log();
|
|
2149
2171
|
if (!useGh) {
|
|
2150
|
-
|
|
2172
|
+
message(`Delete the temporary PAT: ${link("https://github.com/settings/tokens")}`);
|
|
2151
2173
|
}
|
|
2152
|
-
|
|
2153
|
-
`));
|
|
2174
|
+
outro2(`Docs: ${link("https://docs.keyway.sh/ci")}`);
|
|
2154
2175
|
}
|
|
2155
2176
|
async function addRepoSecret(octokit, owner, repo, secretName, secretValue) {
|
|
2156
2177
|
const { data: publicKey } = await octokit.rest.actions.getRepoPublicKey({
|
|
@@ -2177,8 +2198,7 @@ async function encryptSecret(publicKey, secret) {
|
|
|
2177
2198
|
}
|
|
2178
2199
|
|
|
2179
2200
|
// src/cmds/connect.ts
|
|
2180
|
-
import
|
|
2181
|
-
import prompts9 from "prompts";
|
|
2201
|
+
import * as p4 from "@clack/prompts";
|
|
2182
2202
|
var TOKEN_AUTH_PROVIDERS = ["railway"];
|
|
2183
2203
|
function getTokenCreationUrl(provider) {
|
|
2184
2204
|
switch (provider) {
|
|
@@ -2191,38 +2211,37 @@ function getTokenCreationUrl(provider) {
|
|
|
2191
2211
|
async function connectWithTokenFlow(accessToken, provider, displayName) {
|
|
2192
2212
|
const tokenUrl = getTokenCreationUrl(provider);
|
|
2193
2213
|
if (provider === "railway") {
|
|
2194
|
-
|
|
2195
|
-
|
|
2214
|
+
warn("Tip: Select the workspace containing your projects.");
|
|
2215
|
+
message(dim(`Do NOT use "No workspace" - it won't have access to your projects.`));
|
|
2196
2216
|
}
|
|
2197
2217
|
await openUrl(tokenUrl);
|
|
2198
|
-
const
|
|
2199
|
-
type: "password",
|
|
2200
|
-
name: "token",
|
|
2218
|
+
const token = await p4.password({
|
|
2201
2219
|
message: `${displayName} API Token:`
|
|
2202
2220
|
});
|
|
2203
|
-
if (!token) {
|
|
2204
|
-
|
|
2221
|
+
if (p4.isCancel(token) || !token) {
|
|
2222
|
+
message(dim("Cancelled."));
|
|
2205
2223
|
return false;
|
|
2206
2224
|
}
|
|
2207
|
-
|
|
2225
|
+
const s = spinner2();
|
|
2226
|
+
s.start("Validating token...");
|
|
2208
2227
|
try {
|
|
2209
2228
|
const result = await connectWithToken(accessToken, provider, token);
|
|
2229
|
+
s.stop("Validated");
|
|
2210
2230
|
if (result.success) {
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
console.log(pc11.gray(` Account: ${result.user.username}`));
|
|
2231
|
+
success(`Connected to ${displayName}!`);
|
|
2232
|
+
message(dim(`Account: ${result.user.username}`));
|
|
2214
2233
|
if (result.user.teamName) {
|
|
2215
|
-
|
|
2234
|
+
message(dim(`Team: ${result.user.teamName}`));
|
|
2216
2235
|
}
|
|
2217
2236
|
return true;
|
|
2218
2237
|
} else {
|
|
2219
|
-
|
|
2238
|
+
error("Connection failed.");
|
|
2220
2239
|
return false;
|
|
2221
2240
|
}
|
|
2222
|
-
} catch (
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2241
|
+
} catch (error2) {
|
|
2242
|
+
s.stop("Failed");
|
|
2243
|
+
const message2 = error2 instanceof Error ? error2.message : "Token validation failed";
|
|
2244
|
+
error(message2);
|
|
2226
2245
|
return false;
|
|
2227
2246
|
}
|
|
2228
2247
|
}
|
|
@@ -2230,7 +2249,8 @@ async function connectWithOAuthFlow(accessToken, provider, displayName) {
|
|
|
2230
2249
|
const authUrl = getProviderAuthUrl(provider, accessToken);
|
|
2231
2250
|
const startTime = /* @__PURE__ */ new Date();
|
|
2232
2251
|
await openUrl(authUrl);
|
|
2233
|
-
|
|
2252
|
+
const s = spinner2();
|
|
2253
|
+
s.start("Waiting for authorization...");
|
|
2234
2254
|
const maxAttempts = 60;
|
|
2235
2255
|
let attempts = 0;
|
|
2236
2256
|
while (attempts < maxAttempts) {
|
|
@@ -2242,60 +2262,55 @@ async function connectWithOAuthFlow(accessToken, provider, displayName) {
|
|
|
2242
2262
|
(c) => c.provider === provider && new Date(c.createdAt) > startTime
|
|
2243
2263
|
);
|
|
2244
2264
|
if (newConn) {
|
|
2245
|
-
|
|
2246
|
-
|
|
2265
|
+
s.stop("Authorized");
|
|
2266
|
+
success(`Connected to ${displayName}!`);
|
|
2247
2267
|
return true;
|
|
2248
2268
|
}
|
|
2249
2269
|
} catch {
|
|
2250
2270
|
}
|
|
2251
2271
|
}
|
|
2252
|
-
|
|
2253
|
-
|
|
2272
|
+
s.stop("Timeout");
|
|
2273
|
+
error("Authorization timeout.");
|
|
2274
|
+
message(dim("Run `keyway connections` to check if the connection was established."));
|
|
2254
2275
|
return false;
|
|
2255
2276
|
}
|
|
2256
2277
|
async function connectCommand(provider, options = {}) {
|
|
2257
2278
|
try {
|
|
2258
2279
|
const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
|
|
2259
2280
|
const { providers } = await getProviders();
|
|
2260
|
-
const providerInfo = providers.find((
|
|
2281
|
+
const providerInfo = providers.find((p5) => p5.name === provider.toLowerCase());
|
|
2261
2282
|
if (!providerInfo) {
|
|
2262
|
-
const available = providers.map((
|
|
2263
|
-
|
|
2264
|
-
|
|
2283
|
+
const available = providers.map((p5) => p5.name).join(", ");
|
|
2284
|
+
error(`Unknown provider: ${provider}`);
|
|
2285
|
+
message(dim(`Available providers: ${available || "none"}`));
|
|
2265
2286
|
process.exit(1);
|
|
2266
2287
|
}
|
|
2267
2288
|
if (!providerInfo.configured) {
|
|
2268
|
-
|
|
2269
|
-
|
|
2289
|
+
error(`Provider ${providerInfo.displayName} is not configured on the server.`);
|
|
2290
|
+
message(dim("Contact your administrator to enable this integration."));
|
|
2270
2291
|
process.exit(1);
|
|
2271
2292
|
}
|
|
2272
2293
|
const { connections } = await getConnections(accessToken);
|
|
2273
2294
|
const existingConnections = connections.filter((c) => c.provider === provider.toLowerCase());
|
|
2274
2295
|
if (existingConnections.length > 0) {
|
|
2275
|
-
|
|
2276
|
-
You have ${existingConnections.length} ${providerInfo.displayName} connection(s):`));
|
|
2296
|
+
message(dim(`You have ${existingConnections.length} ${providerInfo.displayName} connection(s):`));
|
|
2277
2297
|
for (const conn of existingConnections) {
|
|
2278
2298
|
const teamInfo = conn.providerTeamId ? `(Team: ${conn.providerTeamId})` : "(Personal)";
|
|
2279
|
-
|
|
2299
|
+
message(dim(` - ${teamInfo}`));
|
|
2280
2300
|
}
|
|
2281
|
-
|
|
2282
|
-
const { action } = await prompts9({
|
|
2283
|
-
type: "select",
|
|
2284
|
-
name: "action",
|
|
2301
|
+
const action = await select2({
|
|
2285
2302
|
message: "What would you like to do?",
|
|
2286
|
-
|
|
2287
|
-
{
|
|
2288
|
-
{
|
|
2303
|
+
options: [
|
|
2304
|
+
{ label: "Add another account/team", value: "add" },
|
|
2305
|
+
{ label: "Cancel", value: "cancel" }
|
|
2289
2306
|
]
|
|
2290
2307
|
});
|
|
2291
2308
|
if (action !== "add") {
|
|
2292
|
-
|
|
2309
|
+
message(dim("Keeping existing connections."));
|
|
2293
2310
|
return;
|
|
2294
2311
|
}
|
|
2295
2312
|
}
|
|
2296
|
-
|
|
2297
|
-
Connecting to ${providerInfo.displayName}...
|
|
2298
|
-
`));
|
|
2313
|
+
step(`Connecting to ${providerInfo.displayName}...`);
|
|
2299
2314
|
let connected = false;
|
|
2300
2315
|
if (TOKEN_AUTH_PROVIDERS.includes(provider.toLowerCase())) {
|
|
2301
2316
|
connected = await connectWithTokenFlow(accessToken, provider.toLowerCase(), providerInfo.displayName);
|
|
@@ -2306,14 +2321,13 @@ Connecting to ${providerInfo.displayName}...
|
|
|
2306
2321
|
provider: provider.toLowerCase(),
|
|
2307
2322
|
success: connected
|
|
2308
2323
|
});
|
|
2309
|
-
} catch (
|
|
2310
|
-
const
|
|
2324
|
+
} catch (error2) {
|
|
2325
|
+
const message2 = error2 instanceof Error ? error2.message : "Connection failed";
|
|
2311
2326
|
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
2312
2327
|
command: "connect",
|
|
2313
|
-
error: truncateMessage(
|
|
2328
|
+
error: truncateMessage(message2)
|
|
2314
2329
|
});
|
|
2315
|
-
|
|
2316
|
-
\u2717 ${message}`));
|
|
2330
|
+
error(message2);
|
|
2317
2331
|
process.exit(1);
|
|
2318
2332
|
}
|
|
2319
2333
|
}
|
|
@@ -2322,25 +2336,24 @@ async function connectionsCommand(options = {}) {
|
|
|
2322
2336
|
const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
|
|
2323
2337
|
const { connections } = await getConnections(accessToken);
|
|
2324
2338
|
if (connections.length === 0) {
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2339
|
+
info("No provider connections found.");
|
|
2340
|
+
message(dim("Connect to a provider with: keyway connect <provider>"));
|
|
2341
|
+
message(dim("Available providers: vercel, railway"));
|
|
2328
2342
|
return;
|
|
2329
2343
|
}
|
|
2330
|
-
|
|
2344
|
+
intro2("connections");
|
|
2331
2345
|
for (const conn of connections) {
|
|
2332
2346
|
const providerName = conn.provider.charAt(0).toUpperCase() + conn.provider.slice(1);
|
|
2333
|
-
const teamInfo = conn.providerTeamId ?
|
|
2347
|
+
const teamInfo = conn.providerTeamId ? dim(` (Team: ${conn.providerTeamId})`) : "";
|
|
2334
2348
|
const date = new Date(conn.createdAt).toLocaleDateString();
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
} catch (
|
|
2341
|
-
const
|
|
2342
|
-
|
|
2343
|
-
\u2717 ${message}`));
|
|
2349
|
+
success(`${bold(providerName)}${teamInfo}`);
|
|
2350
|
+
message(dim(` Connected: ${date}`));
|
|
2351
|
+
message(dim(` ID: ${conn.id}`));
|
|
2352
|
+
}
|
|
2353
|
+
outro2("");
|
|
2354
|
+
} catch (error2) {
|
|
2355
|
+
const message2 = error2 instanceof Error ? error2.message : "Failed to list connections";
|
|
2356
|
+
error(message2);
|
|
2344
2357
|
process.exit(1);
|
|
2345
2358
|
}
|
|
2346
2359
|
}
|
|
@@ -2350,41 +2363,36 @@ async function disconnectCommand(provider, options = {}) {
|
|
|
2350
2363
|
const { connections } = await getConnections(accessToken);
|
|
2351
2364
|
const connection = connections.find((c) => c.provider === provider.toLowerCase());
|
|
2352
2365
|
if (!connection) {
|
|
2353
|
-
|
|
2366
|
+
info(`No connection found for provider: ${provider}`);
|
|
2354
2367
|
return;
|
|
2355
2368
|
}
|
|
2356
2369
|
const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
2357
|
-
const
|
|
2358
|
-
type: "confirm",
|
|
2359
|
-
name: "confirm",
|
|
2370
|
+
const confirm3 = await confirm2({
|
|
2360
2371
|
message: `Disconnect from ${providerName}?`,
|
|
2361
|
-
|
|
2372
|
+
initialValue: false
|
|
2362
2373
|
});
|
|
2363
|
-
if (!
|
|
2364
|
-
|
|
2374
|
+
if (!confirm3) {
|
|
2375
|
+
message(dim("Cancelled."));
|
|
2365
2376
|
return;
|
|
2366
2377
|
}
|
|
2367
2378
|
await deleteConnection(accessToken, connection.id);
|
|
2368
|
-
|
|
2369
|
-
\u2713 Disconnected from ${providerName}`));
|
|
2379
|
+
success(`Disconnected from ${providerName}`);
|
|
2370
2380
|
trackEvent(AnalyticsEvents.CLI_DISCONNECT, {
|
|
2371
2381
|
provider: provider.toLowerCase()
|
|
2372
2382
|
});
|
|
2373
|
-
} catch (
|
|
2374
|
-
const
|
|
2383
|
+
} catch (error2) {
|
|
2384
|
+
const message2 = error2 instanceof Error ? error2.message : "Disconnect failed";
|
|
2375
2385
|
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
2376
2386
|
command: "disconnect",
|
|
2377
|
-
error: truncateMessage(
|
|
2387
|
+
error: truncateMessage(message2)
|
|
2378
2388
|
});
|
|
2379
|
-
|
|
2380
|
-
\u2717 ${message}`));
|
|
2389
|
+
error(message2);
|
|
2381
2390
|
process.exit(1);
|
|
2382
2391
|
}
|
|
2383
2392
|
}
|
|
2384
2393
|
|
|
2385
2394
|
// src/cmds/sync.ts
|
|
2386
|
-
import
|
|
2387
|
-
import prompts10 from "prompts";
|
|
2395
|
+
import pc5 from "picocolors";
|
|
2388
2396
|
function mapToVercelEnvironment(keywayEnv) {
|
|
2389
2397
|
const mapping = {
|
|
2390
2398
|
production: "production",
|
|
@@ -2428,38 +2436,35 @@ function mapToProviderEnvironment(provider, keywayEnv) {
|
|
|
2428
2436
|
function displayDiffSummary(diff, providerName) {
|
|
2429
2437
|
const totalDiff = diff.onlyInKeyway.length + diff.onlyInProvider.length + diff.different.length;
|
|
2430
2438
|
if (totalDiff === 0 && diff.same.length > 0) {
|
|
2431
|
-
|
|
2432
|
-
\u2713 Already in sync (${diff.same.length} secrets)`));
|
|
2439
|
+
success(`Already in sync (${diff.same.length} secrets)`);
|
|
2433
2440
|
return;
|
|
2434
2441
|
}
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
`));
|
|
2442
|
+
step("Comparison Summary");
|
|
2443
|
+
message(dim(`Keyway: ${diff.keywayCount} secrets | ${providerName}: ${diff.providerCount} secrets`));
|
|
2438
2444
|
if (diff.onlyInKeyway.length > 0) {
|
|
2439
|
-
|
|
2440
|
-
diff.onlyInKeyway.slice(0, 3).forEach((key) =>
|
|
2445
|
+
message(pc5.cyan(`\u2192 ${diff.onlyInKeyway.length} only in Keyway`));
|
|
2446
|
+
diff.onlyInKeyway.slice(0, 3).forEach((key) => message(dim(` ${key}`)));
|
|
2441
2447
|
if (diff.onlyInKeyway.length > 3) {
|
|
2442
|
-
|
|
2448
|
+
message(dim(` ... and ${diff.onlyInKeyway.length - 3} more`));
|
|
2443
2449
|
}
|
|
2444
2450
|
}
|
|
2445
2451
|
if (diff.onlyInProvider.length > 0) {
|
|
2446
|
-
|
|
2447
|
-
diff.onlyInProvider.slice(0, 3).forEach((key) =>
|
|
2452
|
+
message(pc5.magenta(`\u2190 ${diff.onlyInProvider.length} only in ${providerName}`));
|
|
2453
|
+
diff.onlyInProvider.slice(0, 3).forEach((key) => message(dim(` ${key}`)));
|
|
2448
2454
|
if (diff.onlyInProvider.length > 3) {
|
|
2449
|
-
|
|
2455
|
+
message(dim(` ... and ${diff.onlyInProvider.length - 3} more`));
|
|
2450
2456
|
}
|
|
2451
2457
|
}
|
|
2452
2458
|
if (diff.different.length > 0) {
|
|
2453
|
-
|
|
2454
|
-
diff.different.slice(0, 3).forEach((key) =>
|
|
2459
|
+
message(pc5.yellow(`\u2260 ${diff.different.length} with different values`));
|
|
2460
|
+
diff.different.slice(0, 3).forEach((key) => message(dim(` ${key}`)));
|
|
2455
2461
|
if (diff.different.length > 3) {
|
|
2456
|
-
|
|
2462
|
+
message(dim(` ... and ${diff.different.length - 3} more`));
|
|
2457
2463
|
}
|
|
2458
2464
|
}
|
|
2459
2465
|
if (diff.same.length > 0) {
|
|
2460
|
-
|
|
2466
|
+
message(dim(`= ${diff.same.length} identical`));
|
|
2461
2467
|
}
|
|
2462
|
-
console.log("");
|
|
2463
2468
|
}
|
|
2464
2469
|
function getProjectDisplayName(project) {
|
|
2465
2470
|
return project.serviceName || project.name;
|
|
@@ -2469,17 +2474,17 @@ function findMatchingProject(projects, repoFullName) {
|
|
|
2469
2474
|
const repoName = repoFullName.split("/")[1]?.toLowerCase();
|
|
2470
2475
|
if (!repoName) return void 0;
|
|
2471
2476
|
const linkedMatch = projects.find(
|
|
2472
|
-
(
|
|
2477
|
+
(p5) => p5.linkedRepo?.toLowerCase() === repoFullNameLower
|
|
2473
2478
|
);
|
|
2474
2479
|
if (linkedMatch) {
|
|
2475
2480
|
return { project: linkedMatch, matchType: "linked_repo" };
|
|
2476
2481
|
}
|
|
2477
|
-
const exactNameMatch = projects.find((
|
|
2482
|
+
const exactNameMatch = projects.find((p5) => p5.name.toLowerCase() === repoName);
|
|
2478
2483
|
if (exactNameMatch) {
|
|
2479
2484
|
return { project: exactNameMatch, matchType: "exact_name" };
|
|
2480
2485
|
}
|
|
2481
2486
|
const partialMatches = projects.filter(
|
|
2482
|
-
(
|
|
2487
|
+
(p5) => p5.name.toLowerCase().includes(repoName) || repoName.includes(p5.name.toLowerCase())
|
|
2483
2488
|
);
|
|
2484
2489
|
if (partialMatches.length === 1) {
|
|
2485
2490
|
return { project: partialMatches[0], matchType: "partial_name" };
|
|
@@ -2497,102 +2502,129 @@ function projectMatchesRepo(project, repoFullName) {
|
|
|
2497
2502
|
}
|
|
2498
2503
|
return false;
|
|
2499
2504
|
}
|
|
2500
|
-
async function
|
|
2505
|
+
async function selectProjectWithConnectOption(accessToken, provider, providerDisplayName, repoFullName, initialProjects) {
|
|
2506
|
+
let projects = initialProjects;
|
|
2507
|
+
while (true) {
|
|
2508
|
+
const result = await promptProjectSelection(projects, repoFullName, providerDisplayName);
|
|
2509
|
+
if (result === "connect_new") {
|
|
2510
|
+
await connectCommand(provider, { loginPrompt: false });
|
|
2511
|
+
const { projects: allProjects } = await getAllProviderProjects(accessToken, provider.toLowerCase());
|
|
2512
|
+
projects = allProjects.map((p5) => ({
|
|
2513
|
+
id: p5.id,
|
|
2514
|
+
name: p5.name,
|
|
2515
|
+
serviceId: p5.serviceId,
|
|
2516
|
+
serviceName: p5.serviceName,
|
|
2517
|
+
linkedRepo: p5.linkedRepo,
|
|
2518
|
+
environments: p5.environments,
|
|
2519
|
+
connectionId: p5.connectionId,
|
|
2520
|
+
teamId: p5.teamId,
|
|
2521
|
+
teamName: p5.teamName
|
|
2522
|
+
}));
|
|
2523
|
+
if (projects.length === 0) {
|
|
2524
|
+
error("No projects found after connecting.");
|
|
2525
|
+
process.exit(1);
|
|
2526
|
+
}
|
|
2527
|
+
success(`Found ${projects.length} projects. Select one:`);
|
|
2528
|
+
continue;
|
|
2529
|
+
}
|
|
2530
|
+
return { project: result, projects };
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
async function promptProjectSelection(projects, repoFullName, providerDisplayName) {
|
|
2501
2534
|
const repoName = repoFullName.split("/")[1]?.toLowerCase() || "";
|
|
2502
|
-
const uniqueTeams = new Set(projects.map((
|
|
2535
|
+
const uniqueTeams = new Set(projects.map((p5) => p5.teamId || "personal"));
|
|
2503
2536
|
const hasMultipleAccounts = uniqueTeams.size > 1;
|
|
2504
|
-
const
|
|
2505
|
-
const displayName = getProjectDisplayName(
|
|
2506
|
-
let
|
|
2537
|
+
const options = projects.map((p5) => {
|
|
2538
|
+
const displayName = getProjectDisplayName(p5);
|
|
2539
|
+
let label = displayName;
|
|
2507
2540
|
const badges = [];
|
|
2508
2541
|
if (hasMultipleAccounts) {
|
|
2509
|
-
if (
|
|
2510
|
-
badges.push(
|
|
2511
|
-
} else if (
|
|
2512
|
-
const shortTeamId =
|
|
2513
|
-
badges.push(
|
|
2542
|
+
if (p5.teamName) {
|
|
2543
|
+
badges.push(pc5.cyan(`[${p5.teamName}]`));
|
|
2544
|
+
} else if (p5.teamId) {
|
|
2545
|
+
const shortTeamId = p5.teamId.length > 12 ? p5.teamId.slice(0, 12) + "..." : p5.teamId;
|
|
2546
|
+
badges.push(pc5.cyan(`[team:${shortTeamId}]`));
|
|
2514
2547
|
} else {
|
|
2515
|
-
badges.push(
|
|
2548
|
+
badges.push(pc5.cyan("[personal]"));
|
|
2516
2549
|
}
|
|
2517
2550
|
}
|
|
2518
|
-
if (
|
|
2519
|
-
badges.push(
|
|
2520
|
-
} else if (
|
|
2521
|
-
badges.push(
|
|
2522
|
-
} else if (
|
|
2523
|
-
badges.push(
|
|
2551
|
+
if (p5.linkedRepo?.toLowerCase() === repoFullName.toLowerCase()) {
|
|
2552
|
+
badges.push(pc5.green("\u2190 linked"));
|
|
2553
|
+
} else if (p5.name.toLowerCase() === repoName || p5.serviceName?.toLowerCase() === repoName) {
|
|
2554
|
+
badges.push(pc5.green("\u2190 same name"));
|
|
2555
|
+
} else if (p5.linkedRepo) {
|
|
2556
|
+
badges.push(pc5.gray(`\u2192 ${p5.linkedRepo}`));
|
|
2524
2557
|
}
|
|
2525
2558
|
if (badges.length > 0) {
|
|
2526
|
-
|
|
2559
|
+
label = `${displayName} ${badges.join(" ")}`;
|
|
2527
2560
|
}
|
|
2528
|
-
return {
|
|
2561
|
+
return { label, value: p5.id };
|
|
2562
|
+
});
|
|
2563
|
+
options.push({
|
|
2564
|
+
label: pc5.blue(`+ Connect another ${providerDisplayName} account`),
|
|
2565
|
+
value: "__connect_new__"
|
|
2529
2566
|
});
|
|
2530
|
-
const
|
|
2531
|
-
type: "select",
|
|
2532
|
-
name: "projectChoice",
|
|
2567
|
+
const projectChoice = await select2({
|
|
2533
2568
|
message: "Select a project:",
|
|
2534
|
-
|
|
2569
|
+
options
|
|
2535
2570
|
});
|
|
2536
2571
|
if (!projectChoice) {
|
|
2537
|
-
|
|
2572
|
+
message(dim("Cancelled."));
|
|
2538
2573
|
process.exit(0);
|
|
2539
2574
|
}
|
|
2540
|
-
|
|
2575
|
+
if (projectChoice === "__connect_new__") {
|
|
2576
|
+
return "connect_new";
|
|
2577
|
+
}
|
|
2578
|
+
return projects.find((p5) => p5.id === projectChoice);
|
|
2541
2579
|
}
|
|
2542
2580
|
async function syncCommand(provider, options = {}) {
|
|
2543
2581
|
try {
|
|
2544
2582
|
if (options.pull && options.allowDelete) {
|
|
2545
|
-
|
|
2546
|
-
|
|
2583
|
+
error("--allow-delete cannot be used with --pull");
|
|
2584
|
+
message(dim("The --allow-delete flag is only for push operations."));
|
|
2547
2585
|
process.exit(1);
|
|
2548
2586
|
}
|
|
2549
2587
|
const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
|
|
2550
2588
|
const repoFullName = detectGitRepo();
|
|
2551
2589
|
if (!repoFullName) {
|
|
2552
|
-
|
|
2553
|
-
|
|
2590
|
+
error("Could not detect Git repository.");
|
|
2591
|
+
message(dim("Run this command from a Git repository directory."));
|
|
2554
2592
|
process.exit(1);
|
|
2555
2593
|
}
|
|
2556
|
-
|
|
2594
|
+
step(`Repository: ${value(repoFullName)}`);
|
|
2557
2595
|
const vaultExists = await checkVaultExists(accessToken, repoFullName);
|
|
2558
2596
|
if (!vaultExists) {
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
const { shouldCreate } = await prompts10({
|
|
2562
|
-
type: "confirm",
|
|
2563
|
-
name: "shouldCreate",
|
|
2597
|
+
warn(`No vault found for ${repoFullName}.`);
|
|
2598
|
+
const shouldCreate = await confirm2({
|
|
2564
2599
|
message: "Create vault now?",
|
|
2565
|
-
|
|
2600
|
+
initialValue: true
|
|
2566
2601
|
});
|
|
2567
2602
|
if (!shouldCreate) {
|
|
2568
|
-
|
|
2603
|
+
message(dim("Cancelled. Run `keyway init` to create a vault first."));
|
|
2569
2604
|
process.exit(0);
|
|
2570
2605
|
}
|
|
2571
|
-
|
|
2606
|
+
const s = spinner2();
|
|
2607
|
+
s.start("Creating vault...");
|
|
2572
2608
|
try {
|
|
2573
2609
|
await initVault(repoFullName, accessToken);
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
const
|
|
2578
|
-
|
|
2579
|
-
\u2717 ${message}`));
|
|
2610
|
+
s.stop(`Vault created for ${repoFullName}`);
|
|
2611
|
+
} catch (error2) {
|
|
2612
|
+
s.stop("Failed");
|
|
2613
|
+
const message2 = error2 instanceof Error ? error2.message : "Failed to create vault";
|
|
2614
|
+
error(message2);
|
|
2580
2615
|
process.exit(1);
|
|
2581
2616
|
}
|
|
2582
2617
|
}
|
|
2583
2618
|
const providerDisplayName = provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
2584
2619
|
let { projects: allProjects, connections } = await getAllProviderProjects(accessToken, provider.toLowerCase());
|
|
2585
2620
|
if (connections.length === 0) {
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
const { shouldConnect } = await prompts10({
|
|
2589
|
-
type: "confirm",
|
|
2590
|
-
name: "shouldConnect",
|
|
2621
|
+
warn(`Not connected to ${providerDisplayName}.`);
|
|
2622
|
+
const shouldConnect = await confirm2({
|
|
2591
2623
|
message: `Connect to ${providerDisplayName} now?`,
|
|
2592
|
-
|
|
2624
|
+
initialValue: true
|
|
2593
2625
|
});
|
|
2594
2626
|
if (!shouldConnect) {
|
|
2595
|
-
|
|
2627
|
+
message(dim("Cancelled."));
|
|
2596
2628
|
process.exit(0);
|
|
2597
2629
|
}
|
|
2598
2630
|
await connectCommand(provider, { loginPrompt: false });
|
|
@@ -2600,77 +2632,71 @@ Not connected to ${providerDisplayName}.`));
|
|
|
2600
2632
|
allProjects = refreshed.projects;
|
|
2601
2633
|
connections = refreshed.connections;
|
|
2602
2634
|
if (connections.length === 0) {
|
|
2603
|
-
|
|
2604
|
-
Connection to ${providerDisplayName} failed.`));
|
|
2635
|
+
error(`Connection to ${providerDisplayName} failed.`);
|
|
2605
2636
|
process.exit(1);
|
|
2606
2637
|
}
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
teamName: p.teamName
|
|
2638
|
+
}
|
|
2639
|
+
let projects = allProjects.map((p5) => ({
|
|
2640
|
+
id: p5.id,
|
|
2641
|
+
name: p5.name,
|
|
2642
|
+
serviceId: p5.serviceId,
|
|
2643
|
+
serviceName: p5.serviceName,
|
|
2644
|
+
linkedRepo: p5.linkedRepo,
|
|
2645
|
+
environments: p5.environments,
|
|
2646
|
+
connectionId: p5.connectionId,
|
|
2647
|
+
teamId: p5.teamId,
|
|
2648
|
+
teamName: p5.teamName
|
|
2619
2649
|
}));
|
|
2620
2650
|
if (options.team) {
|
|
2621
2651
|
const teamFilter = options.team.toLowerCase();
|
|
2622
2652
|
const filteredProjects = projects.filter(
|
|
2623
|
-
(
|
|
2624
|
-
teamFilter === "personal" && !
|
|
2653
|
+
(p5) => p5.teamId?.toLowerCase() === teamFilter || p5.teamName?.toLowerCase() === teamFilter || // Match "personal" for null teamId
|
|
2654
|
+
teamFilter === "personal" && !p5.teamId
|
|
2625
2655
|
);
|
|
2626
2656
|
if (filteredProjects.length === 0) {
|
|
2627
|
-
|
|
2628
|
-
|
|
2657
|
+
error(`No projects found for team: ${options.team}`);
|
|
2658
|
+
message(dim("Available teams:"));
|
|
2629
2659
|
const teams = /* @__PURE__ */ new Set();
|
|
2630
|
-
projects.forEach((
|
|
2631
|
-
if (
|
|
2632
|
-
else if (
|
|
2660
|
+
projects.forEach((p5) => {
|
|
2661
|
+
if (p5.teamName) teams.add(p5.teamName);
|
|
2662
|
+
else if (p5.teamId) teams.add(p5.teamId);
|
|
2633
2663
|
else teams.add("personal");
|
|
2634
2664
|
});
|
|
2635
|
-
teams.forEach((t) =>
|
|
2665
|
+
teams.forEach((t) => message(dim(` - ${t}`)));
|
|
2636
2666
|
process.exit(1);
|
|
2637
2667
|
}
|
|
2638
2668
|
projects = filteredProjects;
|
|
2639
|
-
|
|
2669
|
+
message(dim(`Filtered to ${projects.length} projects in team: ${options.team}`));
|
|
2640
2670
|
}
|
|
2641
2671
|
if (projects.length === 0) {
|
|
2642
|
-
|
|
2672
|
+
error(`No projects found in your ${providerDisplayName} account(s).`);
|
|
2643
2673
|
if (connections.length > 1) {
|
|
2644
|
-
|
|
2674
|
+
message(dim(`Checked ${connections.length} connected accounts.`));
|
|
2645
2675
|
}
|
|
2646
2676
|
process.exit(1);
|
|
2647
2677
|
}
|
|
2648
2678
|
if (connections.length > 1 && !options.team) {
|
|
2649
|
-
|
|
2679
|
+
message(dim(`Searching ${projects.length} projects across ${connections.length} ${providerDisplayName} accounts...`));
|
|
2650
2680
|
}
|
|
2651
2681
|
let selectedProject;
|
|
2652
2682
|
if (options.project) {
|
|
2653
2683
|
const found = projects.find(
|
|
2654
|
-
(
|
|
2684
|
+
(p5) => p5.id === options.project || p5.name.toLowerCase() === options.project?.toLowerCase() || p5.serviceName?.toLowerCase() === options.project?.toLowerCase()
|
|
2655
2685
|
);
|
|
2656
2686
|
if (!found) {
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
projects.forEach((
|
|
2687
|
+
error(`Project not found: ${options.project}`);
|
|
2688
|
+
message(dim("Available projects:"));
|
|
2689
|
+
projects.forEach((p5) => message(dim(` - ${getProjectDisplayName(p5)}`)));
|
|
2660
2690
|
process.exit(1);
|
|
2661
2691
|
}
|
|
2662
2692
|
selectedProject = found;
|
|
2663
2693
|
if (!projectMatchesRepo(selectedProject, repoFullName)) {
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
console.log(pc12.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"));
|
|
2668
|
-
console.log(pc12.yellow(` Current repo: ${repoFullName}`));
|
|
2669
|
-
console.log(pc12.yellow(` Selected project: ${getProjectDisplayName(selectedProject)}`));
|
|
2694
|
+
warn("Project does not match current repository");
|
|
2695
|
+
message(pc5.yellow(`Current repo: ${repoFullName}`));
|
|
2696
|
+
message(pc5.yellow(`Selected project: ${getProjectDisplayName(selectedProject)}`));
|
|
2670
2697
|
if (selectedProject.linkedRepo) {
|
|
2671
|
-
|
|
2698
|
+
message(pc5.yellow(`Project linked to: ${selectedProject.linkedRepo}`));
|
|
2672
2699
|
}
|
|
2673
|
-
console.log("");
|
|
2674
2700
|
}
|
|
2675
2701
|
} else {
|
|
2676
2702
|
const autoMatch = findMatchingProject(projects, repoFullName);
|
|
@@ -2679,78 +2705,67 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2679
2705
|
const matchReason = autoMatch.matchType === "linked_repo" ? `linked to ${repoFullName}` : "exact name match";
|
|
2680
2706
|
let teamInfo = "";
|
|
2681
2707
|
if (selectedProject.teamName) {
|
|
2682
|
-
teamInfo =
|
|
2708
|
+
teamInfo = dim(` (${selectedProject.teamName})`);
|
|
2683
2709
|
} else if (selectedProject.teamId && connections.length > 1) {
|
|
2684
2710
|
const shortTeamId = selectedProject.teamId.length > 12 ? selectedProject.teamId.slice(0, 12) + "..." : selectedProject.teamId;
|
|
2685
|
-
teamInfo =
|
|
2711
|
+
teamInfo = dim(` (team:${shortTeamId})`);
|
|
2686
2712
|
}
|
|
2687
|
-
|
|
2713
|
+
success(`Auto-selected project: ${getProjectDisplayName(selectedProject)}${teamInfo} (${matchReason})`);
|
|
2688
2714
|
} else if (autoMatch && autoMatch.matchType === "partial_name") {
|
|
2689
2715
|
const partialDisplayName = getProjectDisplayName(autoMatch.project);
|
|
2690
|
-
|
|
2691
|
-
const
|
|
2692
|
-
type: "confirm",
|
|
2693
|
-
name: "useDetected",
|
|
2716
|
+
info(`Detected project: ${partialDisplayName} (partial match)`);
|
|
2717
|
+
const useDetected = await confirm2({
|
|
2694
2718
|
message: `Use ${partialDisplayName}?`,
|
|
2695
|
-
|
|
2719
|
+
initialValue: true
|
|
2696
2720
|
});
|
|
2697
2721
|
if (useDetected) {
|
|
2698
2722
|
selectedProject = autoMatch.project;
|
|
2699
2723
|
} else {
|
|
2700
|
-
|
|
2724
|
+
const result = await selectProjectWithConnectOption(accessToken, provider, providerDisplayName, repoFullName, projects);
|
|
2725
|
+
selectedProject = result.project;
|
|
2726
|
+
projects = result.projects;
|
|
2701
2727
|
}
|
|
2702
2728
|
} else if (projects.length === 1) {
|
|
2703
2729
|
selectedProject = projects[0];
|
|
2704
2730
|
if (!projectMatchesRepo(selectedProject, repoFullName)) {
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
console.log(pc12.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"));
|
|
2709
|
-
console.log(pc12.yellow(` Current repo: ${repoFullName}`));
|
|
2710
|
-
console.log(pc12.yellow(` Only project: ${getProjectDisplayName(selectedProject)}`));
|
|
2731
|
+
warn("Project does not match current repository");
|
|
2732
|
+
message(pc5.yellow(`Current repo: ${repoFullName}`));
|
|
2733
|
+
message(pc5.yellow(`Only project: ${getProjectDisplayName(selectedProject)}`));
|
|
2711
2734
|
if (selectedProject.linkedRepo) {
|
|
2712
|
-
|
|
2735
|
+
message(pc5.yellow(`Project linked to: ${selectedProject.linkedRepo}`));
|
|
2713
2736
|
}
|
|
2714
|
-
|
|
2715
|
-
const { continueAnyway } = await prompts10({
|
|
2716
|
-
type: "confirm",
|
|
2717
|
-
name: "continueAnyway",
|
|
2737
|
+
const continueAnyway = await confirm2({
|
|
2718
2738
|
message: "Continue anyway?",
|
|
2719
|
-
|
|
2739
|
+
initialValue: false
|
|
2720
2740
|
});
|
|
2721
2741
|
if (!continueAnyway) {
|
|
2722
|
-
|
|
2742
|
+
message(dim("Cancelled."));
|
|
2723
2743
|
process.exit(0);
|
|
2724
2744
|
}
|
|
2725
2745
|
}
|
|
2726
2746
|
} else {
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
selectedProject =
|
|
2747
|
+
warn(`No matching project found for ${repoFullName}`);
|
|
2748
|
+
message(dim("Select a project manually:"));
|
|
2749
|
+
const result = await selectProjectWithConnectOption(accessToken, provider, providerDisplayName, repoFullName, projects);
|
|
2750
|
+
selectedProject = result.project;
|
|
2751
|
+
projects = result.projects;
|
|
2731
2752
|
}
|
|
2732
2753
|
}
|
|
2733
2754
|
if (!options.project && !projectMatchesRepo(selectedProject, repoFullName)) {
|
|
2734
2755
|
const autoMatch = findMatchingProject(projects, repoFullName);
|
|
2735
2756
|
if (autoMatch && autoMatch.project.id !== selectedProject.id) {
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
console.log(pc12.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"));
|
|
2740
|
-
console.log(pc12.yellow(` Current repo: ${repoFullName}`));
|
|
2741
|
-
console.log(pc12.yellow(` Selected project: ${getProjectDisplayName(selectedProject)}`));
|
|
2757
|
+
warn("You selected a different project");
|
|
2758
|
+
message(pc5.yellow(`Current repo: ${repoFullName}`));
|
|
2759
|
+
message(pc5.yellow(`Selected project: ${getProjectDisplayName(selectedProject)}`));
|
|
2742
2760
|
if (selectedProject.linkedRepo) {
|
|
2743
|
-
|
|
2761
|
+
message(pc5.yellow(`Project linked to: ${selectedProject.linkedRepo}`));
|
|
2744
2762
|
}
|
|
2745
|
-
|
|
2746
|
-
const { continueAnyway } = await prompts10({
|
|
2747
|
-
type: "confirm",
|
|
2748
|
-
name: "continueAnyway",
|
|
2763
|
+
const continueAnyway = await confirm2({
|
|
2749
2764
|
message: "Are you sure you want to sync with this project?",
|
|
2750
|
-
|
|
2765
|
+
initialValue: false
|
|
2751
2766
|
});
|
|
2752
2767
|
if (!continueAnyway) {
|
|
2753
|
-
|
|
2768
|
+
message(dim("Cancelled."));
|
|
2754
2769
|
process.exit(0);
|
|
2755
2770
|
}
|
|
2756
2771
|
}
|
|
@@ -2764,15 +2779,13 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2764
2779
|
if (needsEnvPrompt || needsDirectionPrompt) {
|
|
2765
2780
|
if (needsEnvPrompt) {
|
|
2766
2781
|
const vaultEnvs = await getVaultEnvironments(accessToken, repoFullName);
|
|
2767
|
-
const
|
|
2768
|
-
type: "select",
|
|
2769
|
-
name: "selectedEnv",
|
|
2782
|
+
const selectedEnv = await select2({
|
|
2770
2783
|
message: "Keyway environment:",
|
|
2771
|
-
|
|
2772
|
-
|
|
2784
|
+
options: vaultEnvs.map((e) => ({ label: e, value: e })),
|
|
2785
|
+
initialValue: vaultEnvs.includes("production") ? "production" : vaultEnvs[0]
|
|
2773
2786
|
});
|
|
2774
2787
|
if (!selectedEnv) {
|
|
2775
|
-
|
|
2788
|
+
message(dim("Cancelled."));
|
|
2776
2789
|
process.exit(0);
|
|
2777
2790
|
}
|
|
2778
2791
|
keywayEnv = selectedEnv;
|
|
@@ -2786,19 +2799,15 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2786
2799
|
providerEnv = mappedEnv;
|
|
2787
2800
|
} else if (selectedProject.environments.length === 1) {
|
|
2788
2801
|
providerEnv = selectedProject.environments[0];
|
|
2789
|
-
|
|
2802
|
+
message(dim(`Using ${providerName} environment: ${providerEnv}`));
|
|
2790
2803
|
} else {
|
|
2791
|
-
const
|
|
2792
|
-
type: "select",
|
|
2793
|
-
name: "selectedProviderEnv",
|
|
2804
|
+
const selectedProviderEnv = await select2({
|
|
2794
2805
|
message: `${providerName} environment:`,
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
(e) => e.toLowerCase() === "production"
|
|
2798
|
-
))
|
|
2806
|
+
options: selectedProject.environments.map((e) => ({ label: e, value: e })),
|
|
2807
|
+
initialValue: selectedProject.environments.includes("production") ? "production" : selectedProject.environments[0]
|
|
2799
2808
|
});
|
|
2800
2809
|
if (!selectedProviderEnv) {
|
|
2801
|
-
|
|
2810
|
+
message(dim("Cancelled."));
|
|
2802
2811
|
process.exit(0);
|
|
2803
2812
|
}
|
|
2804
2813
|
providerEnv = selectedProviderEnv;
|
|
@@ -2812,7 +2821,8 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2812
2821
|
if (needsDirectionPrompt) {
|
|
2813
2822
|
const effectiveKeywayEnv = keywayEnv || "production";
|
|
2814
2823
|
const effectiveProviderEnv = providerEnv || mapToProviderEnvironment(provider, effectiveKeywayEnv);
|
|
2815
|
-
|
|
2824
|
+
const s = spinner2();
|
|
2825
|
+
s.start("Comparing secrets...");
|
|
2816
2826
|
diff = await getSyncDiff(accessToken, repoFullName, {
|
|
2817
2827
|
connectionId: selectedProject.connectionId,
|
|
2818
2828
|
projectId: selectedProject.id,
|
|
@@ -2821,6 +2831,7 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2821
2831
|
keywayEnvironment: effectiveKeywayEnv,
|
|
2822
2832
|
providerEnvironment: effectiveProviderEnv
|
|
2823
2833
|
});
|
|
2834
|
+
s.stop("Compared");
|
|
2824
2835
|
displayDiffSummary(diff, providerName);
|
|
2825
2836
|
const totalDiff = diff.onlyInKeyway.length + diff.onlyInProvider.length + diff.different.length;
|
|
2826
2837
|
if (totalDiff === 0) {
|
|
@@ -2828,24 +2839,17 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2828
2839
|
}
|
|
2829
2840
|
}
|
|
2830
2841
|
if (needsDirectionPrompt && diff) {
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
defaultDirection = 1;
|
|
2834
|
-
} else if (diff.providerCount === 0 && diff.keywayCount > 0) {
|
|
2835
|
-
defaultDirection = 0;
|
|
2836
|
-
}
|
|
2837
|
-
const { selectedDirection } = await prompts10({
|
|
2838
|
-
type: "select",
|
|
2839
|
-
name: "selectedDirection",
|
|
2842
|
+
const defaultDirection = diff.keywayCount === 0 && diff.providerCount > 0 ? "pull" : "push";
|
|
2843
|
+
const selectedDirection = await select2({
|
|
2840
2844
|
message: "Sync direction:",
|
|
2841
|
-
|
|
2842
|
-
{
|
|
2843
|
-
{
|
|
2845
|
+
options: [
|
|
2846
|
+
{ label: `Keyway \u2192 ${providerName}`, value: "push" },
|
|
2847
|
+
{ label: `${providerName} \u2192 Keyway`, value: "pull" }
|
|
2844
2848
|
],
|
|
2845
|
-
|
|
2849
|
+
initialValue: defaultDirection
|
|
2846
2850
|
});
|
|
2847
2851
|
if (!selectedDirection) {
|
|
2848
|
-
|
|
2852
|
+
message(dim("Cancelled."));
|
|
2849
2853
|
process.exit(0);
|
|
2850
2854
|
}
|
|
2851
2855
|
direction = selectedDirection;
|
|
@@ -2862,14 +2866,11 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2862
2866
|
keywayEnv
|
|
2863
2867
|
);
|
|
2864
2868
|
if (status.isFirstSync && direction === "push" && status.vaultIsEmpty && status.providerHasSecrets) {
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
const { importFirst } = await prompts10({
|
|
2869
|
-
type: "confirm",
|
|
2870
|
-
name: "importFirst",
|
|
2869
|
+
warn(`Your Keyway vault is empty for "${keywayEnv}", but ${providerName} has ${status.providerSecretCount} secrets.`);
|
|
2870
|
+
message(dim("(Use --environment to sync a different environment)"));
|
|
2871
|
+
const importFirst = await confirm2({
|
|
2871
2872
|
message: `Import secrets from ${providerName} first?`,
|
|
2872
|
-
|
|
2873
|
+
initialValue: true
|
|
2873
2874
|
});
|
|
2874
2875
|
if (importFirst) {
|
|
2875
2876
|
await executeSyncOperation(
|
|
@@ -2900,14 +2901,13 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2900
2901
|
options.yes || false,
|
|
2901
2902
|
provider
|
|
2902
2903
|
);
|
|
2903
|
-
} catch (
|
|
2904
|
-
const
|
|
2904
|
+
} catch (error2) {
|
|
2905
|
+
const message2 = error2 instanceof Error ? error2.message : "Sync failed";
|
|
2905
2906
|
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
2906
2907
|
command: "sync",
|
|
2907
|
-
error: truncateMessage(
|
|
2908
|
+
error: truncateMessage(message2)
|
|
2908
2909
|
});
|
|
2909
|
-
|
|
2910
|
-
\u2717 ${message}`));
|
|
2910
|
+
error(message2);
|
|
2911
2911
|
process.exit(1);
|
|
2912
2912
|
}
|
|
2913
2913
|
}
|
|
@@ -2925,49 +2925,47 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
|
|
|
2925
2925
|
});
|
|
2926
2926
|
const totalChanges = preview.toCreate.length + preview.toUpdate.length + preview.toDelete.length;
|
|
2927
2927
|
if (totalChanges === 0) {
|
|
2928
|
-
|
|
2928
|
+
success("Already in sync. No changes needed.");
|
|
2929
2929
|
return;
|
|
2930
2930
|
}
|
|
2931
|
-
|
|
2931
|
+
step("Sync Preview");
|
|
2932
2932
|
if (preview.toCreate.length > 0) {
|
|
2933
|
-
|
|
2934
|
-
preview.toCreate.slice(0, 5).forEach((key) =>
|
|
2933
|
+
message(pc5.green(`+ ${preview.toCreate.length} to create`));
|
|
2934
|
+
preview.toCreate.slice(0, 5).forEach((key) => message(dim(` ${key}`)));
|
|
2935
2935
|
if (preview.toCreate.length > 5) {
|
|
2936
|
-
|
|
2936
|
+
message(dim(` ... and ${preview.toCreate.length - 5} more`));
|
|
2937
2937
|
}
|
|
2938
2938
|
}
|
|
2939
2939
|
if (preview.toUpdate.length > 0) {
|
|
2940
|
-
|
|
2941
|
-
preview.toUpdate.slice(0, 5).forEach((key) =>
|
|
2940
|
+
message(pc5.yellow(`~ ${preview.toUpdate.length} to update`));
|
|
2941
|
+
preview.toUpdate.slice(0, 5).forEach((key) => message(dim(` ${key}`)));
|
|
2942
2942
|
if (preview.toUpdate.length > 5) {
|
|
2943
|
-
|
|
2943
|
+
message(dim(` ... and ${preview.toUpdate.length - 5} more`));
|
|
2944
2944
|
}
|
|
2945
2945
|
}
|
|
2946
2946
|
if (preview.toDelete.length > 0) {
|
|
2947
|
-
|
|
2948
|
-
preview.toDelete.slice(0, 5).forEach((key) =>
|
|
2947
|
+
message(pc5.red(`- ${preview.toDelete.length} to delete`));
|
|
2948
|
+
preview.toDelete.slice(0, 5).forEach((key) => message(dim(` ${key}`)));
|
|
2949
2949
|
if (preview.toDelete.length > 5) {
|
|
2950
|
-
|
|
2950
|
+
message(dim(` ... and ${preview.toDelete.length - 5} more`));
|
|
2951
2951
|
}
|
|
2952
2952
|
}
|
|
2953
2953
|
if (preview.toSkip.length > 0) {
|
|
2954
|
-
|
|
2954
|
+
message(dim(`\u25CB ${preview.toSkip.length} unchanged`));
|
|
2955
2955
|
}
|
|
2956
|
-
console.log("");
|
|
2957
2956
|
if (!skipConfirm) {
|
|
2958
2957
|
const target = direction === "push" ? providerName : "Keyway";
|
|
2959
|
-
const
|
|
2960
|
-
type: "confirm",
|
|
2961
|
-
name: "confirm",
|
|
2958
|
+
const confirm3 = await confirm2({
|
|
2962
2959
|
message: `Apply ${totalChanges} changes to ${target}?`,
|
|
2963
|
-
|
|
2960
|
+
initialValue: true
|
|
2964
2961
|
});
|
|
2965
|
-
if (!
|
|
2966
|
-
|
|
2962
|
+
if (!confirm3) {
|
|
2963
|
+
message(dim("Cancelled."));
|
|
2967
2964
|
return;
|
|
2968
2965
|
}
|
|
2969
2966
|
}
|
|
2970
|
-
|
|
2967
|
+
const s = spinner2();
|
|
2968
|
+
s.start("Syncing...");
|
|
2971
2969
|
const result = await executeSync(accessToken, repoFullName, {
|
|
2972
2970
|
connectionId,
|
|
2973
2971
|
projectId: project.id,
|
|
@@ -2979,11 +2977,11 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
|
|
|
2979
2977
|
allowDelete
|
|
2980
2978
|
});
|
|
2981
2979
|
if (result.success) {
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2980
|
+
s.stop("Sync complete");
|
|
2981
|
+
message(dim(`Created: ${result.stats.created}`));
|
|
2982
|
+
message(dim(`Updated: ${result.stats.updated}`));
|
|
2985
2983
|
if (result.stats.deleted > 0) {
|
|
2986
|
-
|
|
2984
|
+
message(dim(`Deleted: ${result.stats.deleted}`));
|
|
2987
2985
|
}
|
|
2988
2986
|
trackEvent(AnalyticsEvents.CLI_SYNC, {
|
|
2989
2987
|
provider,
|
|
@@ -2993,27 +2991,19 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
|
|
|
2993
2991
|
deleted: result.stats.deleted
|
|
2994
2992
|
});
|
|
2995
2993
|
} else {
|
|
2996
|
-
|
|
2997
|
-
|
|
2994
|
+
s.stop("Failed");
|
|
2995
|
+
error(result.error || "Sync failed");
|
|
2998
2996
|
process.exit(1);
|
|
2999
2997
|
}
|
|
3000
2998
|
}
|
|
3001
2999
|
|
|
3002
3000
|
// src/cli.ts
|
|
3003
3001
|
process.on("unhandledRejection", (reason) => {
|
|
3004
|
-
|
|
3002
|
+
error(`Unhandled error: ${reason}`);
|
|
3005
3003
|
process.exit(1);
|
|
3006
3004
|
});
|
|
3007
3005
|
var program = new Command();
|
|
3008
3006
|
var TAGLINE = "Sync secrets with your team and infra";
|
|
3009
|
-
var showBanner = () => {
|
|
3010
|
-
const text = pc13.bold(pc13.cyan("Keyway CLI"));
|
|
3011
|
-
console.log(`
|
|
3012
|
-
${text}
|
|
3013
|
-
${pc13.gray(TAGLINE)}
|
|
3014
|
-
`);
|
|
3015
|
-
};
|
|
3016
|
-
showBanner();
|
|
3017
3007
|
program.name("keyway").description(TAGLINE).version(package_default.version);
|
|
3018
3008
|
program.command("init").description("Initialize a vault for the current repository").option("--no-login-prompt", "Fail instead of prompting to login if unauthenticated").action(async (options) => {
|
|
3019
3009
|
await initCommand(options);
|
|
@@ -3050,9 +3040,8 @@ ci.command("setup").description("Setup GitHub Actions integration (adds KEYWAY_T
|
|
|
3050
3040
|
await ciSetupCommand(options);
|
|
3051
3041
|
});
|
|
3052
3042
|
(async () => {
|
|
3053
|
-
await warnIfEnvNotGitignored();
|
|
3054
3043
|
await program.parseAsync();
|
|
3055
|
-
})().catch((
|
|
3056
|
-
|
|
3044
|
+
})().catch((error2) => {
|
|
3045
|
+
error(error2.message || error2);
|
|
3057
3046
|
process.exit(1);
|
|
3058
3047
|
});
|