@keywaysh/cli 0.0.13 → 0.0.15
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 +7 -5
- package/dist/cli.js +763 -173
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import pc9 from "picocolors";
|
|
6
6
|
|
|
7
7
|
// src/cmds/init.ts
|
|
8
|
-
import
|
|
8
|
+
import pc4 from "picocolors";
|
|
9
9
|
import prompts4 from "prompts";
|
|
10
10
|
|
|
11
11
|
// src/utils/git.ts
|
|
@@ -66,10 +66,10 @@ var INTERNAL_API_URL = "https://api.keyway.sh";
|
|
|
66
66
|
var INTERNAL_POSTHOG_KEY = "phc_duG0qqI5z8LeHrS9pNxR5KaD4djgD0nmzUxuD3zP0ov";
|
|
67
67
|
var INTERNAL_POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
68
68
|
|
|
69
|
-
// package.json
|
|
69
|
+
// package.json
|
|
70
70
|
var package_default = {
|
|
71
71
|
name: "@keywaysh/cli",
|
|
72
|
-
version: "0.0.
|
|
72
|
+
version: "0.0.15",
|
|
73
73
|
description: "One link to all your secrets",
|
|
74
74
|
type: "module",
|
|
75
75
|
bin: {
|
|
@@ -81,6 +81,7 @@ var package_default = {
|
|
|
81
81
|
],
|
|
82
82
|
scripts: {
|
|
83
83
|
dev: "pnpm exec tsx src/cli.ts",
|
|
84
|
+
"dev:local": "NODE_TLS_REJECT_UNAUTHORIZED=0 KEYWAY_API_URL=https://localhost/api pnpm exec tsx src/cli.ts",
|
|
84
85
|
build: "pnpm exec tsup",
|
|
85
86
|
"build:watch": "pnpm exec tsup --watch",
|
|
86
87
|
prepublishOnly: "pnpm run build",
|
|
@@ -112,10 +113,10 @@ var package_default = {
|
|
|
112
113
|
node: ">=18.0.0"
|
|
113
114
|
},
|
|
114
115
|
dependencies: {
|
|
115
|
-
chalk: "^4.1.2",
|
|
116
116
|
commander: "^14.0.0",
|
|
117
117
|
conf: "^15.0.2",
|
|
118
118
|
open: "^11.0.0",
|
|
119
|
+
picocolors: "^1.1.1",
|
|
119
120
|
"posthog-node": "^3.5.0",
|
|
120
121
|
prompts: "^2.4.2"
|
|
121
122
|
},
|
|
@@ -132,6 +133,73 @@ var package_default = {
|
|
|
132
133
|
// src/utils/api.ts
|
|
133
134
|
var API_BASE_URL = process.env.KEYWAY_API_URL || INTERNAL_API_URL;
|
|
134
135
|
var USER_AGENT = `keyway-cli/${package_default.version}`;
|
|
136
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
137
|
+
function truncateMessage(message, maxLength = 200) {
|
|
138
|
+
if (message.length <= maxLength) return message;
|
|
139
|
+
return message.slice(0, maxLength - 3) + "...";
|
|
140
|
+
}
|
|
141
|
+
var NETWORK_ERROR_MESSAGES = {
|
|
142
|
+
ECONNREFUSED: "Cannot connect to Keyway API server. Is the server running?",
|
|
143
|
+
ECONNRESET: "Connection was reset. Please try again.",
|
|
144
|
+
ENOTFOUND: "DNS lookup failed. Check your internet connection.",
|
|
145
|
+
ETIMEDOUT: "Connection timed out. Check your network connection.",
|
|
146
|
+
ENETUNREACH: "Network is unreachable. Check your internet connection.",
|
|
147
|
+
EHOSTUNREACH: "Host is unreachable. Check your network connection.",
|
|
148
|
+
CERT_HAS_EXPIRED: "SSL certificate has expired. Contact support.",
|
|
149
|
+
UNABLE_TO_VERIFY_LEAF_SIGNATURE: "SSL certificate verification failed.",
|
|
150
|
+
EPROTO: "SSL/TLS protocol error. Try again later."
|
|
151
|
+
};
|
|
152
|
+
function handleNetworkError(error) {
|
|
153
|
+
const errorCode = error.code || error.cause?.code;
|
|
154
|
+
if (errorCode && NETWORK_ERROR_MESSAGES[errorCode]) {
|
|
155
|
+
return new Error(NETWORK_ERROR_MESSAGES[errorCode]);
|
|
156
|
+
}
|
|
157
|
+
const message = error.message.toLowerCase();
|
|
158
|
+
if (message.includes("fetch failed") || message.includes("network")) {
|
|
159
|
+
return new Error("Network error. Check your internet connection and try again.");
|
|
160
|
+
}
|
|
161
|
+
return error;
|
|
162
|
+
}
|
|
163
|
+
async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
164
|
+
const controller = new AbortController();
|
|
165
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
166
|
+
try {
|
|
167
|
+
return await fetch(url, {
|
|
168
|
+
...options,
|
|
169
|
+
signal: controller.signal
|
|
170
|
+
});
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (error instanceof Error) {
|
|
173
|
+
if (error.name === "AbortError") {
|
|
174
|
+
throw new Error(`Request timeout after ${timeoutMs / 1e3}s. Check your network connection.`);
|
|
175
|
+
}
|
|
176
|
+
throw handleNetworkError(error);
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
} finally {
|
|
180
|
+
clearTimeout(timeout);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function validateApiUrl(url) {
|
|
184
|
+
const parsed = new URL(url);
|
|
185
|
+
if (parsed.protocol !== "https:") {
|
|
186
|
+
const isLocalhost = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "0.0.0.0";
|
|
187
|
+
if (!isLocalhost) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Insecure API URL detected: ${url}
|
|
190
|
+
HTTPS is required for security. If this is a development server, use localhost or configure HTTPS.`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
if (!process.env.KEYWAY_DISABLE_SECURITY_WARNINGS) {
|
|
194
|
+
console.warn(
|
|
195
|
+
`\u26A0\uFE0F WARNING: Using insecure HTTP connection to ${url}
|
|
196
|
+
This should only be used for local development.
|
|
197
|
+
Set KEYWAY_DISABLE_SECURITY_WARNINGS=1 to suppress this warning.`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
validateApiUrl(API_BASE_URL);
|
|
135
203
|
var APIError = class extends Error {
|
|
136
204
|
constructor(statusCode, error, message, upgradeUrl) {
|
|
137
205
|
super(message);
|
|
@@ -176,7 +244,7 @@ async function initVault(repoFullName, accessToken) {
|
|
|
176
244
|
if (accessToken) {
|
|
177
245
|
headers.Authorization = `Bearer ${accessToken}`;
|
|
178
246
|
}
|
|
179
|
-
const response = await
|
|
247
|
+
const response = await fetchWithTimeout(`${API_BASE_URL}/v1/vaults`, {
|
|
180
248
|
method: "POST",
|
|
181
249
|
headers,
|
|
182
250
|
body: JSON.stringify(body)
|
|
@@ -211,7 +279,7 @@ async function pushSecrets(repoFullName, environment, content, accessToken) {
|
|
|
211
279
|
if (accessToken) {
|
|
212
280
|
headers.Authorization = `Bearer ${accessToken}`;
|
|
213
281
|
}
|
|
214
|
-
const response = await
|
|
282
|
+
const response = await fetchWithTimeout(`${API_BASE_URL}/v1/secrets/push`, {
|
|
215
283
|
method: "POST",
|
|
216
284
|
headers,
|
|
217
285
|
body: JSON.stringify(body)
|
|
@@ -231,7 +299,7 @@ async function pullSecrets(repoFullName, environment, accessToken) {
|
|
|
231
299
|
repo: repoFullName,
|
|
232
300
|
environment
|
|
233
301
|
});
|
|
234
|
-
const response = await
|
|
302
|
+
const response = await fetchWithTimeout(`${API_BASE_URL}/v1/secrets/pull?${params}`, {
|
|
235
303
|
method: "GET",
|
|
236
304
|
headers
|
|
237
305
|
});
|
|
@@ -239,7 +307,7 @@ async function pullSecrets(repoFullName, environment, accessToken) {
|
|
|
239
307
|
return { content: result.data.content };
|
|
240
308
|
}
|
|
241
309
|
async function startDeviceLogin(repository) {
|
|
242
|
-
const response = await
|
|
310
|
+
const response = await fetchWithTimeout(`${API_BASE_URL}/v1/auth/device/start`, {
|
|
243
311
|
method: "POST",
|
|
244
312
|
headers: {
|
|
245
313
|
"Content-Type": "application/json",
|
|
@@ -250,7 +318,7 @@ async function startDeviceLogin(repository) {
|
|
|
250
318
|
return handleResponse(response);
|
|
251
319
|
}
|
|
252
320
|
async function pollDeviceLogin(deviceCode) {
|
|
253
|
-
const response = await
|
|
321
|
+
const response = await fetchWithTimeout(`${API_BASE_URL}/v1/auth/device/poll`, {
|
|
254
322
|
method: "POST",
|
|
255
323
|
headers: {
|
|
256
324
|
"Content-Type": "application/json",
|
|
@@ -261,7 +329,7 @@ async function pollDeviceLogin(deviceCode) {
|
|
|
261
329
|
return handleResponse(response);
|
|
262
330
|
}
|
|
263
331
|
async function validateToken(token) {
|
|
264
|
-
const response = await
|
|
332
|
+
const response = await fetchWithTimeout(`${API_BASE_URL}/v1/auth/token/validate`, {
|
|
265
333
|
method: "POST",
|
|
266
334
|
headers: {
|
|
267
335
|
"Content-Type": "application/json",
|
|
@@ -272,6 +340,117 @@ async function validateToken(token) {
|
|
|
272
340
|
});
|
|
273
341
|
return handleResponse(response);
|
|
274
342
|
}
|
|
343
|
+
async function getProviders() {
|
|
344
|
+
const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations`, {
|
|
345
|
+
method: "GET",
|
|
346
|
+
headers: {
|
|
347
|
+
"User-Agent": USER_AGENT
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
return handleResponse(response);
|
|
351
|
+
}
|
|
352
|
+
async function getConnections(accessToken) {
|
|
353
|
+
const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations/connections`, {
|
|
354
|
+
method: "GET",
|
|
355
|
+
headers: {
|
|
356
|
+
"User-Agent": USER_AGENT,
|
|
357
|
+
Authorization: `Bearer ${accessToken}`
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
return handleResponse(response);
|
|
361
|
+
}
|
|
362
|
+
async function deleteConnection(accessToken, connectionId) {
|
|
363
|
+
const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations/connections/${connectionId}`, {
|
|
364
|
+
method: "DELETE",
|
|
365
|
+
headers: {
|
|
366
|
+
"User-Agent": USER_AGENT,
|
|
367
|
+
Authorization: `Bearer ${accessToken}`
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
return handleResponse(response);
|
|
371
|
+
}
|
|
372
|
+
function getProviderAuthUrl(provider, redirectUri) {
|
|
373
|
+
const params = redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : "";
|
|
374
|
+
return `${API_BASE_URL}/v1/integrations/${provider}/authorize${params}`;
|
|
375
|
+
}
|
|
376
|
+
async function getConnectionProjects(accessToken, connectionId) {
|
|
377
|
+
const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations/connections/${connectionId}/projects`, {
|
|
378
|
+
method: "GET",
|
|
379
|
+
headers: {
|
|
380
|
+
"User-Agent": USER_AGENT,
|
|
381
|
+
Authorization: `Bearer ${accessToken}`
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
return handleResponse(response);
|
|
385
|
+
}
|
|
386
|
+
async function getSyncStatus(accessToken, repoFullName, connectionId, projectId, environment = "production") {
|
|
387
|
+
const [owner, repo] = repoFullName.split("/");
|
|
388
|
+
const params = new URLSearchParams({
|
|
389
|
+
connectionId,
|
|
390
|
+
projectId,
|
|
391
|
+
environment
|
|
392
|
+
});
|
|
393
|
+
const response = await fetchWithTimeout(
|
|
394
|
+
`${API_BASE_URL}/v1/integrations/vaults/${owner}/${repo}/sync/status?${params}`,
|
|
395
|
+
{
|
|
396
|
+
method: "GET",
|
|
397
|
+
headers: {
|
|
398
|
+
"User-Agent": USER_AGENT,
|
|
399
|
+
Authorization: `Bearer ${accessToken}`
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
);
|
|
403
|
+
return handleResponse(response);
|
|
404
|
+
}
|
|
405
|
+
async function getSyncPreview(accessToken, repoFullName, options) {
|
|
406
|
+
const [owner, repo] = repoFullName.split("/");
|
|
407
|
+
const params = new URLSearchParams({
|
|
408
|
+
connectionId: options.connectionId,
|
|
409
|
+
projectId: options.projectId,
|
|
410
|
+
keywayEnvironment: options.keywayEnvironment || "production",
|
|
411
|
+
providerEnvironment: options.providerEnvironment || "production",
|
|
412
|
+
direction: options.direction || "push",
|
|
413
|
+
allowDelete: String(options.allowDelete || false)
|
|
414
|
+
});
|
|
415
|
+
const response = await fetchWithTimeout(
|
|
416
|
+
`${API_BASE_URL}/v1/integrations/vaults/${owner}/${repo}/sync/preview?${params}`,
|
|
417
|
+
{
|
|
418
|
+
method: "GET",
|
|
419
|
+
headers: {
|
|
420
|
+
"User-Agent": USER_AGENT,
|
|
421
|
+
Authorization: `Bearer ${accessToken}`
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
6e4
|
|
425
|
+
// 60 seconds for sync operations
|
|
426
|
+
);
|
|
427
|
+
return handleResponse(response);
|
|
428
|
+
}
|
|
429
|
+
async function executeSync(accessToken, repoFullName, options) {
|
|
430
|
+
const [owner, repo] = repoFullName.split("/");
|
|
431
|
+
const response = await fetchWithTimeout(
|
|
432
|
+
`${API_BASE_URL}/v1/integrations/vaults/${owner}/${repo}/sync`,
|
|
433
|
+
{
|
|
434
|
+
method: "POST",
|
|
435
|
+
headers: {
|
|
436
|
+
"Content-Type": "application/json",
|
|
437
|
+
"User-Agent": USER_AGENT,
|
|
438
|
+
Authorization: `Bearer ${accessToken}`
|
|
439
|
+
},
|
|
440
|
+
body: JSON.stringify({
|
|
441
|
+
connectionId: options.connectionId,
|
|
442
|
+
projectId: options.projectId,
|
|
443
|
+
keywayEnvironment: options.keywayEnvironment || "production",
|
|
444
|
+
providerEnvironment: options.providerEnvironment || "production",
|
|
445
|
+
direction: options.direction || "push",
|
|
446
|
+
allowDelete: options.allowDelete || false
|
|
447
|
+
})
|
|
448
|
+
},
|
|
449
|
+
12e4
|
|
450
|
+
// 2 minutes for sync execution
|
|
451
|
+
);
|
|
452
|
+
return handleResponse(response);
|
|
453
|
+
}
|
|
275
454
|
|
|
276
455
|
// src/utils/analytics.ts
|
|
277
456
|
import { PostHog } from "posthog-node";
|
|
@@ -279,71 +458,6 @@ import crypto from "crypto";
|
|
|
279
458
|
import path from "path";
|
|
280
459
|
import os from "os";
|
|
281
460
|
import fs from "fs";
|
|
282
|
-
|
|
283
|
-
// package.json
|
|
284
|
-
var package_default2 = {
|
|
285
|
-
name: "@keywaysh/cli",
|
|
286
|
-
version: "0.0.13",
|
|
287
|
-
description: "One link to all your secrets",
|
|
288
|
-
type: "module",
|
|
289
|
-
bin: {
|
|
290
|
-
keyway: "./dist/cli.js"
|
|
291
|
-
},
|
|
292
|
-
main: "./dist/cli.js",
|
|
293
|
-
files: [
|
|
294
|
-
"dist"
|
|
295
|
-
],
|
|
296
|
-
scripts: {
|
|
297
|
-
dev: "pnpm exec tsx src/cli.ts",
|
|
298
|
-
build: "pnpm exec tsup",
|
|
299
|
-
"build:watch": "pnpm exec tsup --watch",
|
|
300
|
-
prepublishOnly: "pnpm run build",
|
|
301
|
-
test: "pnpm exec vitest run",
|
|
302
|
-
"test:watch": "pnpm exec vitest",
|
|
303
|
-
release: "npm version patch && git push && git push --tags",
|
|
304
|
-
"release:minor": "npm version minor && git push && git push --tags",
|
|
305
|
-
"release:major": "npm version major && git push && git push --tags"
|
|
306
|
-
},
|
|
307
|
-
keywords: [
|
|
308
|
-
"secrets",
|
|
309
|
-
"env",
|
|
310
|
-
"keyway",
|
|
311
|
-
"cli",
|
|
312
|
-
"devops"
|
|
313
|
-
],
|
|
314
|
-
author: "Nicolas Ritouet",
|
|
315
|
-
license: "MIT",
|
|
316
|
-
homepage: "https://keyway.sh",
|
|
317
|
-
repository: {
|
|
318
|
-
type: "git",
|
|
319
|
-
url: "https://github.com/keywaysh/cli.git"
|
|
320
|
-
},
|
|
321
|
-
bugs: {
|
|
322
|
-
url: "https://github.com/keywaysh/cli/issues"
|
|
323
|
-
},
|
|
324
|
-
packageManager: "pnpm@10.6.1",
|
|
325
|
-
engines: {
|
|
326
|
-
node: ">=18.0.0"
|
|
327
|
-
},
|
|
328
|
-
dependencies: {
|
|
329
|
-
chalk: "^4.1.2",
|
|
330
|
-
commander: "^14.0.0",
|
|
331
|
-
conf: "^15.0.2",
|
|
332
|
-
open: "^11.0.0",
|
|
333
|
-
"posthog-node": "^3.5.0",
|
|
334
|
-
prompts: "^2.4.2"
|
|
335
|
-
},
|
|
336
|
-
devDependencies: {
|
|
337
|
-
"@types/node": "^24.2.0",
|
|
338
|
-
"@types/prompts": "^2.4.9",
|
|
339
|
-
tsup: "^8.5.0",
|
|
340
|
-
tsx: "^4.20.3",
|
|
341
|
-
typescript: "^5.9.2",
|
|
342
|
-
vitest: "^3.2.4"
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
// src/utils/analytics.ts
|
|
347
461
|
var posthog = null;
|
|
348
462
|
var distinctId = null;
|
|
349
463
|
var CONFIG_DIR = path.join(os.homedir(), ".config", "keyway");
|
|
@@ -400,7 +514,7 @@ function trackEvent(event, properties) {
|
|
|
400
514
|
source: "cli",
|
|
401
515
|
platform: process.platform,
|
|
402
516
|
nodeVersion: process.version,
|
|
403
|
-
version:
|
|
517
|
+
version: package_default.version,
|
|
404
518
|
ci: CI
|
|
405
519
|
}
|
|
406
520
|
});
|
|
@@ -433,44 +547,100 @@ var AnalyticsEvents = {
|
|
|
433
547
|
CLI_PULL: "cli_pull",
|
|
434
548
|
CLI_ERROR: "cli_error",
|
|
435
549
|
CLI_LOGIN: "cli_login",
|
|
436
|
-
CLI_DOCTOR: "cli_doctor"
|
|
550
|
+
CLI_DOCTOR: "cli_doctor",
|
|
551
|
+
CLI_CONNECT: "cli_connect",
|
|
552
|
+
CLI_DISCONNECT: "cli_disconnect",
|
|
553
|
+
CLI_SYNC: "cli_sync"
|
|
437
554
|
};
|
|
438
555
|
|
|
439
556
|
// src/cmds/login.ts
|
|
440
|
-
import
|
|
557
|
+
import pc from "picocolors";
|
|
441
558
|
import readline from "readline";
|
|
442
559
|
import open from "open";
|
|
443
560
|
import prompts from "prompts";
|
|
444
561
|
|
|
445
562
|
// src/utils/auth.ts
|
|
446
563
|
import Conf from "conf";
|
|
564
|
+
import { createCipheriv, createDecipheriv, randomBytes, scrypt } from "crypto";
|
|
565
|
+
import { promisify } from "util";
|
|
447
566
|
var store = new Conf({
|
|
448
567
|
projectName: "keyway",
|
|
449
568
|
configName: "config",
|
|
450
569
|
fileMode: 384
|
|
451
570
|
});
|
|
571
|
+
var scryptAsync = promisify(scrypt);
|
|
572
|
+
async function getEncryptionKey() {
|
|
573
|
+
const machineId = process.env.USER || process.env.USERNAME || "keyway-user";
|
|
574
|
+
let salt = store.get("salt");
|
|
575
|
+
if (!salt) {
|
|
576
|
+
salt = randomBytes(16).toString("hex");
|
|
577
|
+
store.set("salt", salt);
|
|
578
|
+
}
|
|
579
|
+
const key = await scryptAsync(machineId, salt, 32);
|
|
580
|
+
return key;
|
|
581
|
+
}
|
|
582
|
+
async function encryptToken(token) {
|
|
583
|
+
const key = await getEncryptionKey();
|
|
584
|
+
const iv = randomBytes(16);
|
|
585
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
586
|
+
const encrypted = Buffer.concat([
|
|
587
|
+
cipher.update(token, "utf8"),
|
|
588
|
+
cipher.final()
|
|
589
|
+
]);
|
|
590
|
+
const authTag = cipher.getAuthTag();
|
|
591
|
+
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
|
|
592
|
+
}
|
|
593
|
+
async function decryptToken(encryptedData) {
|
|
594
|
+
const key = await getEncryptionKey();
|
|
595
|
+
const parts = encryptedData.split(":");
|
|
596
|
+
if (parts.length !== 3) {
|
|
597
|
+
throw new Error("Invalid encrypted token format");
|
|
598
|
+
}
|
|
599
|
+
const iv = Buffer.from(parts[0], "hex");
|
|
600
|
+
const authTag = Buffer.from(parts[1], "hex");
|
|
601
|
+
const encrypted = Buffer.from(parts[2], "hex");
|
|
602
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
603
|
+
decipher.setAuthTag(authTag);
|
|
604
|
+
const decrypted = Buffer.concat([
|
|
605
|
+
decipher.update(encrypted),
|
|
606
|
+
decipher.final()
|
|
607
|
+
]);
|
|
608
|
+
return decrypted.toString("utf8");
|
|
609
|
+
}
|
|
452
610
|
function isExpired(auth) {
|
|
453
611
|
if (!auth.expiresAt) return false;
|
|
454
612
|
const expires = Date.parse(auth.expiresAt);
|
|
455
613
|
if (Number.isNaN(expires)) return false;
|
|
456
614
|
return expires <= Date.now();
|
|
457
615
|
}
|
|
458
|
-
function getStoredAuth() {
|
|
459
|
-
const
|
|
460
|
-
if (
|
|
616
|
+
async function getStoredAuth() {
|
|
617
|
+
const encryptedData = store.get("auth");
|
|
618
|
+
if (!encryptedData) {
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
const decrypted = await decryptToken(encryptedData);
|
|
623
|
+
const auth = JSON.parse(decrypted);
|
|
624
|
+
if (isExpired(auth)) {
|
|
625
|
+
clearAuth();
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
return auth;
|
|
629
|
+
} catch (error) {
|
|
630
|
+
console.error("Failed to decrypt stored auth, clearing...");
|
|
461
631
|
clearAuth();
|
|
462
632
|
return null;
|
|
463
633
|
}
|
|
464
|
-
return auth ?? null;
|
|
465
634
|
}
|
|
466
|
-
function saveAuthToken(token, meta) {
|
|
635
|
+
async function saveAuthToken(token, meta) {
|
|
467
636
|
const auth = {
|
|
468
637
|
keywayToken: token,
|
|
469
638
|
githubLogin: meta?.githubLogin,
|
|
470
639
|
expiresAt: meta?.expiresAt,
|
|
471
640
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
472
641
|
};
|
|
473
|
-
|
|
642
|
+
const encrypted = await encryptToken(JSON.stringify(auth));
|
|
643
|
+
store.set("auth", encrypted);
|
|
474
644
|
}
|
|
475
645
|
function clearAuth() {
|
|
476
646
|
store.delete("auth");
|
|
@@ -503,17 +673,17 @@ async function promptYesNo(question, defaultYes = true) {
|
|
|
503
673
|
});
|
|
504
674
|
}
|
|
505
675
|
async function runLoginFlow() {
|
|
506
|
-
console.log(
|
|
676
|
+
console.log(pc.blue("\u{1F510} Starting Keyway login...\n"));
|
|
507
677
|
const repoName = detectGitRepo();
|
|
508
678
|
const start = await startDeviceLogin(repoName);
|
|
509
679
|
const verifyUrl = start.verificationUriComplete || start.verificationUri;
|
|
510
680
|
if (!verifyUrl) {
|
|
511
681
|
throw new Error("Missing verification URL from the auth server.");
|
|
512
682
|
}
|
|
513
|
-
console.log(`Code: ${
|
|
683
|
+
console.log(`Code: ${pc.green.bold(start.userCode)}`);
|
|
514
684
|
console.log("Waiting for auth...");
|
|
515
685
|
open(verifyUrl).catch(() => {
|
|
516
|
-
console.log(
|
|
686
|
+
console.log(pc.gray(`Open this URL in your browser: ${verifyUrl}`));
|
|
517
687
|
});
|
|
518
688
|
const pollIntervalMs = (start.interval ?? 5) * 1e3;
|
|
519
689
|
const maxTimeoutMs = Math.min((start.expiresIn ?? 900) * 1e3, 30 * 60 * 1e3);
|
|
@@ -528,7 +698,7 @@ async function runLoginFlow() {
|
|
|
528
698
|
continue;
|
|
529
699
|
}
|
|
530
700
|
if (result.status === "approved" && result.keywayToken) {
|
|
531
|
-
saveAuthToken(result.keywayToken, {
|
|
701
|
+
await saveAuthToken(result.keywayToken, {
|
|
532
702
|
githubLogin: result.githubLogin,
|
|
533
703
|
expiresAt: result.expiresAt
|
|
534
704
|
});
|
|
@@ -536,9 +706,9 @@ async function runLoginFlow() {
|
|
|
536
706
|
method: "device",
|
|
537
707
|
repo: repoName
|
|
538
708
|
});
|
|
539
|
-
console.log(
|
|
709
|
+
console.log(pc.green("\n\u2713 Login successful"));
|
|
540
710
|
if (result.githubLogin) {
|
|
541
|
-
console.log(`Authenticated GitHub user: ${
|
|
711
|
+
console.log(`Authenticated GitHub user: ${pc.cyan(result.githubLogin)}`);
|
|
542
712
|
}
|
|
543
713
|
return result.keywayToken;
|
|
544
714
|
}
|
|
@@ -546,11 +716,14 @@ async function runLoginFlow() {
|
|
|
546
716
|
}
|
|
547
717
|
}
|
|
548
718
|
async function ensureLogin(options = {}) {
|
|
549
|
-
const envToken = process.env.KEYWAY_TOKEN
|
|
719
|
+
const envToken = process.env.KEYWAY_TOKEN;
|
|
550
720
|
if (envToken) {
|
|
551
721
|
return envToken;
|
|
552
722
|
}
|
|
553
|
-
|
|
723
|
+
if (process.env.GITHUB_TOKEN && !process.env.KEYWAY_TOKEN) {
|
|
724
|
+
console.warn(pc.yellow("Note: GITHUB_TOKEN found but not used. Set KEYWAY_TOKEN for Keyway authentication."));
|
|
725
|
+
}
|
|
726
|
+
const stored = await getStoredAuth();
|
|
554
727
|
if (stored?.keywayToken) {
|
|
555
728
|
return stored.keywayToken;
|
|
556
729
|
}
|
|
@@ -568,16 +741,16 @@ async function ensureLogin(options = {}) {
|
|
|
568
741
|
async function runTokenLogin() {
|
|
569
742
|
const repoName = detectGitRepo();
|
|
570
743
|
if (repoName) {
|
|
571
|
-
console.log(`\u{1F4C1} Detected: ${
|
|
744
|
+
console.log(`\u{1F4C1} Detected: ${pc.cyan(repoName)}`);
|
|
572
745
|
}
|
|
573
746
|
const description = repoName ? `Keyway CLI for ${repoName}` : "Keyway CLI";
|
|
574
747
|
const url = `https://github.com/settings/personal-access-tokens/new?description=${encodeURIComponent(description)}`;
|
|
575
748
|
console.log("Opening GitHub...");
|
|
576
749
|
open(url).catch(() => {
|
|
577
|
-
console.log(
|
|
750
|
+
console.log(pc.gray(`Open this URL in your browser: ${url}`));
|
|
578
751
|
});
|
|
579
|
-
console.log(
|
|
580
|
-
console.log(
|
|
752
|
+
console.log(pc.gray("Select the detected repo (or scope manually)."));
|
|
753
|
+
console.log(pc.gray("Permissions: Metadata \u2192 Read-only; Account permissions: None."));
|
|
581
754
|
const { token } = await prompts(
|
|
582
755
|
{
|
|
583
756
|
type: "password",
|
|
@@ -603,14 +776,14 @@ async function runTokenLogin() {
|
|
|
603
776
|
throw new Error("Token must start with github_pat_.");
|
|
604
777
|
}
|
|
605
778
|
const validation = await validateToken(trimmedToken);
|
|
606
|
-
saveAuthToken(trimmedToken, {
|
|
779
|
+
await saveAuthToken(trimmedToken, {
|
|
607
780
|
githubLogin: validation.username
|
|
608
781
|
});
|
|
609
782
|
trackEvent(AnalyticsEvents.CLI_LOGIN, {
|
|
610
783
|
method: "pat",
|
|
611
784
|
repo: repoName
|
|
612
785
|
});
|
|
613
|
-
console.log(
|
|
786
|
+
console.log(pc.green("\u2705 Authenticated"), `as ${pc.cyan(`@${validation.username}`)}`);
|
|
614
787
|
return trimmedToken;
|
|
615
788
|
}
|
|
616
789
|
async function loginCommand(options = {}) {
|
|
@@ -624,24 +797,24 @@ async function loginCommand(options = {}) {
|
|
|
624
797
|
const message = error instanceof Error ? error.message : "Unexpected login error";
|
|
625
798
|
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
626
799
|
command: "login",
|
|
627
|
-
error: message
|
|
800
|
+
error: truncateMessage(message)
|
|
628
801
|
});
|
|
629
|
-
console.error(
|
|
802
|
+
console.error(pc.red(`
|
|
630
803
|
\u2717 ${message}`));
|
|
631
804
|
process.exit(1);
|
|
632
805
|
}
|
|
633
806
|
}
|
|
634
807
|
async function logoutCommand() {
|
|
635
808
|
clearAuth();
|
|
636
|
-
console.log(
|
|
637
|
-
console.log(
|
|
809
|
+
console.log(pc.green("\u2713 Logged out of Keyway"));
|
|
810
|
+
console.log(pc.gray(`Auth cache cleared: ${getAuthFilePath()}`));
|
|
638
811
|
}
|
|
639
812
|
|
|
640
813
|
// src/cmds/readme.ts
|
|
641
814
|
import fs2 from "fs";
|
|
642
815
|
import path2 from "path";
|
|
643
816
|
import prompts2 from "prompts";
|
|
644
|
-
import
|
|
817
|
+
import pc2 from "picocolors";
|
|
645
818
|
function generateBadge(repo) {
|
|
646
819
|
return `[](https://www.keyway.sh/vaults/${repo})`;
|
|
647
820
|
}
|
|
@@ -679,7 +852,7 @@ async function ensureReadme(repoName, cwd) {
|
|
|
679
852
|
if (existing) return existing;
|
|
680
853
|
const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
|
|
681
854
|
if (!isInteractive2) {
|
|
682
|
-
console.log(
|
|
855
|
+
console.log(pc2.yellow('No README found. Run "keyway readme add-badge" from a repo with a README.'));
|
|
683
856
|
return null;
|
|
684
857
|
}
|
|
685
858
|
const { confirm } = await prompts2(
|
|
@@ -694,7 +867,7 @@ async function ensureReadme(repoName, cwd) {
|
|
|
694
867
|
}
|
|
695
868
|
);
|
|
696
869
|
if (!confirm) {
|
|
697
|
-
console.log(
|
|
870
|
+
console.log(pc2.yellow("Skipping badge insertion (no README)."));
|
|
698
871
|
return null;
|
|
699
872
|
}
|
|
700
873
|
const defaultPath = path2.join(cwd, "README.md");
|
|
@@ -717,19 +890,19 @@ async function addBadgeToReadme(silent = false) {
|
|
|
717
890
|
const updated = insertBadgeIntoReadme(content, badge);
|
|
718
891
|
if (updated === content) {
|
|
719
892
|
if (!silent) {
|
|
720
|
-
console.log(
|
|
893
|
+
console.log(pc2.gray("Keyway badge already present in README."));
|
|
721
894
|
}
|
|
722
895
|
return false;
|
|
723
896
|
}
|
|
724
897
|
fs2.writeFileSync(readmePath, updated, "utf-8");
|
|
725
898
|
if (!silent) {
|
|
726
|
-
console.log(
|
|
899
|
+
console.log(pc2.green(`\u2713 Keyway badge added to ${path2.basename(readmePath)}`));
|
|
727
900
|
}
|
|
728
901
|
return true;
|
|
729
902
|
}
|
|
730
903
|
|
|
731
904
|
// src/cmds/push.ts
|
|
732
|
-
import
|
|
905
|
+
import pc3 from "picocolors";
|
|
733
906
|
import fs3 from "fs";
|
|
734
907
|
import path3 from "path";
|
|
735
908
|
import prompts3 from "prompts";
|
|
@@ -746,7 +919,7 @@ function discoverEnvCandidates(cwd) {
|
|
|
746
919
|
const entries = fs3.readdirSync(cwd);
|
|
747
920
|
const hasEnvLocal = entries.includes(".env.local");
|
|
748
921
|
if (hasEnvLocal) {
|
|
749
|
-
console.log(
|
|
922
|
+
console.log(pc3.gray("\u2139\uFE0F Detected .env.local \u2014 not synced by design (machine-specific secrets)"));
|
|
750
923
|
}
|
|
751
924
|
const candidates = entries.filter((name) => name.startsWith(".env") && name !== ".env.local").map((name) => {
|
|
752
925
|
const fullPath = path3.join(cwd, name);
|
|
@@ -772,7 +945,7 @@ function discoverEnvCandidates(cwd) {
|
|
|
772
945
|
}
|
|
773
946
|
async function pushCommand(options) {
|
|
774
947
|
try {
|
|
775
|
-
console.log(
|
|
948
|
+
console.log(pc3.blue("\u{1F510} Pushing secrets to Keyway...\n"));
|
|
776
949
|
const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
|
|
777
950
|
let environment = options.env;
|
|
778
951
|
let envFile = options.file;
|
|
@@ -872,11 +1045,11 @@ async function pushCommand(options) {
|
|
|
872
1045
|
const trimmed = line.trim();
|
|
873
1046
|
return trimmed.length > 0 && !trimmed.startsWith("#");
|
|
874
1047
|
});
|
|
875
|
-
console.log(`File: ${
|
|
876
|
-
console.log(`Environment: ${
|
|
877
|
-
console.log(`Variables: ${
|
|
1048
|
+
console.log(`File: ${pc3.cyan(envFile)}`);
|
|
1049
|
+
console.log(`Environment: ${pc3.cyan(environment)}`);
|
|
1050
|
+
console.log(`Variables: ${pc3.cyan(lines.length.toString())}`);
|
|
878
1051
|
const repoFullName = getCurrentRepoFullName();
|
|
879
|
-
console.log(`Repository: ${
|
|
1052
|
+
console.log(`Repository: ${pc3.cyan(repoFullName)}`);
|
|
880
1053
|
if (!options.yes) {
|
|
881
1054
|
const isInteractive3 = process.stdin.isTTY && process.stdout.isTTY;
|
|
882
1055
|
if (!isInteractive3) {
|
|
@@ -896,7 +1069,7 @@ async function pushCommand(options) {
|
|
|
896
1069
|
}
|
|
897
1070
|
);
|
|
898
1071
|
if (!confirm) {
|
|
899
|
-
console.log(
|
|
1072
|
+
console.log(pc3.yellow("Push aborted."));
|
|
900
1073
|
return;
|
|
901
1074
|
}
|
|
902
1075
|
}
|
|
@@ -908,30 +1081,50 @@ async function pushCommand(options) {
|
|
|
908
1081
|
});
|
|
909
1082
|
console.log("\nUploading secrets...");
|
|
910
1083
|
const response = await pushSecrets(repoFullName, environment, content, accessToken);
|
|
911
|
-
console.log(
|
|
1084
|
+
console.log(pc3.green("\n\u2713 " + response.message));
|
|
912
1085
|
if (response.stats) {
|
|
913
1086
|
const { created, updated, deleted } = response.stats;
|
|
914
1087
|
const parts = [];
|
|
915
|
-
if (created > 0) parts.push(
|
|
916
|
-
if (updated > 0) parts.push(
|
|
917
|
-
if (deleted > 0) parts.push(
|
|
1088
|
+
if (created > 0) parts.push(pc3.green(`+${created} created`));
|
|
1089
|
+
if (updated > 0) parts.push(pc3.yellow(`~${updated} updated`));
|
|
1090
|
+
if (deleted > 0) parts.push(pc3.red(`-${deleted} deleted`));
|
|
918
1091
|
if (parts.length > 0) {
|
|
919
1092
|
console.log(`Stats: ${parts.join(", ")}`);
|
|
920
1093
|
}
|
|
921
1094
|
}
|
|
922
1095
|
console.log(`
|
|
923
1096
|
Your secrets are now encrypted and stored securely.`);
|
|
924
|
-
console.log(`To retrieve them, run: ${
|
|
1097
|
+
console.log(`To retrieve them, run: ${pc3.cyan(`keyway pull --env ${environment}`)}`);
|
|
925
1098
|
await shutdownAnalytics();
|
|
926
1099
|
} catch (error) {
|
|
927
|
-
|
|
1100
|
+
let message;
|
|
1101
|
+
let hint = null;
|
|
1102
|
+
if (error instanceof APIError) {
|
|
1103
|
+
message = error.message || `HTTP ${error.statusCode} - ${error.error}`;
|
|
1104
|
+
const envNotFoundMatch = message.match(/Environment '([^']+)' does not exist.*Available environments: ([^.]+)/);
|
|
1105
|
+
if (envNotFoundMatch) {
|
|
1106
|
+
const requestedEnv = envNotFoundMatch[1];
|
|
1107
|
+
const availableEnvs = envNotFoundMatch[2];
|
|
1108
|
+
message = `Environment '${requestedEnv}' does not exist in this vault.`;
|
|
1109
|
+
hint = `Available environments: ${availableEnvs}
|
|
1110
|
+
Use ${pc3.cyan(`keyway push --env <environment>`)} to specify one, or create '${requestedEnv}' via the dashboard.`;
|
|
1111
|
+
}
|
|
1112
|
+
} else if (error instanceof Error) {
|
|
1113
|
+
message = truncateMessage(error.message);
|
|
1114
|
+
} else {
|
|
1115
|
+
message = "Unknown error";
|
|
1116
|
+
}
|
|
928
1117
|
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
929
1118
|
command: "push",
|
|
930
1119
|
error: message
|
|
931
1120
|
});
|
|
932
1121
|
await shutdownAnalytics();
|
|
933
|
-
console.error(
|
|
934
|
-
\u2717
|
|
1122
|
+
console.error(pc3.red(`
|
|
1123
|
+
\u2717 ${message}`));
|
|
1124
|
+
if (hint) {
|
|
1125
|
+
console.error(pc3.gray(`
|
|
1126
|
+
${hint}`));
|
|
1127
|
+
}
|
|
935
1128
|
process.exit(1);
|
|
936
1129
|
}
|
|
937
1130
|
}
|
|
@@ -942,16 +1135,16 @@ async function initCommand(options = {}) {
|
|
|
942
1135
|
try {
|
|
943
1136
|
const repoFullName = getCurrentRepoFullName();
|
|
944
1137
|
const dashboardLink = `${DASHBOARD_URL}/${repoFullName}`;
|
|
945
|
-
console.log(
|
|
946
|
-
console.log(` ${
|
|
1138
|
+
console.log(pc4.blue("\u{1F510} Initializing Keyway vault...\n"));
|
|
1139
|
+
console.log(` ${pc4.gray("Repository:")} ${pc4.white(repoFullName)}`);
|
|
947
1140
|
const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
|
|
948
1141
|
trackEvent(AnalyticsEvents.CLI_INIT, { repoFullName });
|
|
949
1142
|
const response = await initVault(repoFullName, accessToken);
|
|
950
|
-
console.log(
|
|
1143
|
+
console.log(pc4.green("\u2713 Vault created!"));
|
|
951
1144
|
try {
|
|
952
1145
|
const badgeAdded = await addBadgeToReadme(true);
|
|
953
1146
|
if (badgeAdded) {
|
|
954
|
-
console.log(
|
|
1147
|
+
console.log(pc4.green("\u2713 Badge added to README.md"));
|
|
955
1148
|
}
|
|
956
1149
|
} catch {
|
|
957
1150
|
}
|
|
@@ -959,7 +1152,7 @@ async function initCommand(options = {}) {
|
|
|
959
1152
|
const envCandidates = discoverEnvCandidates(process.cwd());
|
|
960
1153
|
const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
|
|
961
1154
|
if (envCandidates.length > 0 && isInteractive2) {
|
|
962
|
-
console.log(
|
|
1155
|
+
console.log(pc4.gray(` Found ${envCandidates.length} env file(s): ${envCandidates.map((c) => c.file).join(", ")}
|
|
963
1156
|
`));
|
|
964
1157
|
const { shouldPush } = await prompts4({
|
|
965
1158
|
type: "confirm",
|
|
@@ -973,60 +1166,59 @@ async function initCommand(options = {}) {
|
|
|
973
1166
|
return;
|
|
974
1167
|
}
|
|
975
1168
|
}
|
|
976
|
-
console.log(
|
|
1169
|
+
console.log(pc4.dim("\u2500".repeat(50)));
|
|
977
1170
|
console.log("");
|
|
978
1171
|
if (envCandidates.length === 0) {
|
|
979
|
-
console.log(` ${
|
|
980
|
-
console.log(` ${
|
|
1172
|
+
console.log(` ${pc4.yellow("\u2192")} Create a ${pc4.cyan(".env")} file with your secrets`);
|
|
1173
|
+
console.log(` ${pc4.yellow("\u2192")} Run ${pc4.cyan("keyway push")} to sync them
|
|
981
1174
|
`);
|
|
982
1175
|
} else {
|
|
983
|
-
console.log(` ${
|
|
1176
|
+
console.log(` ${pc4.yellow("\u2192")} Run ${pc4.cyan("keyway push")} to sync your secrets
|
|
984
1177
|
`);
|
|
985
1178
|
}
|
|
986
|
-
console.log(` ${
|
|
1179
|
+
console.log(` ${pc4.blue("\u2394")} Dashboard: ${pc4.underline(dashboardLink)}`);
|
|
987
1180
|
console.log("");
|
|
988
1181
|
await shutdownAnalytics();
|
|
989
1182
|
} catch (error) {
|
|
990
1183
|
if (error instanceof APIError) {
|
|
991
1184
|
if (error.statusCode === 409) {
|
|
992
|
-
console.log(
|
|
993
|
-
console.log(` ${
|
|
994
|
-
console.log(` ${
|
|
1185
|
+
console.log(pc4.yellow("\n\u26A0 Vault already exists for this repository.\n"));
|
|
1186
|
+
console.log(` ${pc4.yellow("\u2192")} Run ${pc4.cyan("keyway push")} to sync your secrets`);
|
|
1187
|
+
console.log(` ${pc4.blue("\u2394")} Dashboard: ${pc4.underline(`${DASHBOARD_URL}/${getCurrentRepoFullName()}`)}`);
|
|
995
1188
|
console.log("");
|
|
996
1189
|
await shutdownAnalytics();
|
|
997
1190
|
return;
|
|
998
1191
|
}
|
|
999
1192
|
if (error.error === "PLAN_LIMIT_REACHED") {
|
|
1000
1193
|
console.log("");
|
|
1001
|
-
console.log(
|
|
1194
|
+
console.log(pc4.dim("\u2500".repeat(50)));
|
|
1002
1195
|
console.log("");
|
|
1003
|
-
console.log(` ${
|
|
1196
|
+
console.log(` ${pc4.yellow("\u26A1")} ${pc4.bold("Upgrade Required")}`);
|
|
1004
1197
|
console.log("");
|
|
1005
|
-
console.log(
|
|
1006
|
-
console.log(chalk4.gray(" Upgrade to Pro for unlimited private repositories."));
|
|
1198
|
+
console.log(pc4.gray(` ${error.message}`));
|
|
1007
1199
|
console.log("");
|
|
1008
|
-
console.log(` ${
|
|
1200
|
+
console.log(` ${pc4.cyan("\u2192")} ${pc4.underline(error.upgradeUrl || "https://keyway.sh/upgrade")}`);
|
|
1009
1201
|
console.log("");
|
|
1010
|
-
console.log(
|
|
1202
|
+
console.log(pc4.dim("\u2500".repeat(50)));
|
|
1011
1203
|
console.log("");
|
|
1012
1204
|
await shutdownAnalytics();
|
|
1013
1205
|
process.exit(1);
|
|
1014
1206
|
}
|
|
1015
1207
|
}
|
|
1016
|
-
const message = error instanceof APIError ? error.message : error instanceof Error ? error.message
|
|
1208
|
+
const message = error instanceof APIError ? error.message : error instanceof Error ? truncateMessage(error.message) : "Unknown error";
|
|
1017
1209
|
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
1018
1210
|
command: "init",
|
|
1019
1211
|
error: message
|
|
1020
1212
|
});
|
|
1021
1213
|
await shutdownAnalytics();
|
|
1022
|
-
console.error(
|
|
1214
|
+
console.error(pc4.red(`
|
|
1023
1215
|
\u2717 ${message}`));
|
|
1024
1216
|
process.exit(1);
|
|
1025
1217
|
}
|
|
1026
1218
|
}
|
|
1027
1219
|
|
|
1028
1220
|
// src/cmds/pull.ts
|
|
1029
|
-
import
|
|
1221
|
+
import pc5 from "picocolors";
|
|
1030
1222
|
import fs4 from "fs";
|
|
1031
1223
|
import path4 from "path";
|
|
1032
1224
|
import prompts5 from "prompts";
|
|
@@ -1034,10 +1226,10 @@ async function pullCommand(options) {
|
|
|
1034
1226
|
try {
|
|
1035
1227
|
const environment = options.env || "development";
|
|
1036
1228
|
const envFile = options.file || ".env";
|
|
1037
|
-
console.log(
|
|
1038
|
-
console.log(`Environment: ${
|
|
1229
|
+
console.log(pc5.blue("\u{1F510} Pulling secrets from Keyway...\n"));
|
|
1230
|
+
console.log(`Environment: ${pc5.cyan(environment)}`);
|
|
1039
1231
|
const repoFullName = getCurrentRepoFullName();
|
|
1040
|
-
console.log(`Repository: ${
|
|
1232
|
+
console.log(`Repository: ${pc5.cyan(repoFullName)}`);
|
|
1041
1233
|
const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
|
|
1042
1234
|
trackEvent(AnalyticsEvents.CLI_PULL, {
|
|
1043
1235
|
repoFullName,
|
|
@@ -1049,7 +1241,7 @@ async function pullCommand(options) {
|
|
|
1049
1241
|
if (fs4.existsSync(envFilePath)) {
|
|
1050
1242
|
const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
|
|
1051
1243
|
if (options.yes) {
|
|
1052
|
-
console.log(
|
|
1244
|
+
console.log(pc5.yellow(`
|
|
1053
1245
|
\u26A0 Overwriting existing file: ${envFile}`));
|
|
1054
1246
|
} else if (!isInteractive2) {
|
|
1055
1247
|
throw new Error(`File ${envFile} exists. Re-run with --yes to overwrite or choose a different --file.`);
|
|
@@ -1068,7 +1260,7 @@ async function pullCommand(options) {
|
|
|
1068
1260
|
}
|
|
1069
1261
|
);
|
|
1070
1262
|
if (!confirm) {
|
|
1071
|
-
console.log(
|
|
1263
|
+
console.log(pc5.yellow("Pull aborted."));
|
|
1072
1264
|
return;
|
|
1073
1265
|
}
|
|
1074
1266
|
}
|
|
@@ -1078,27 +1270,27 @@ async function pullCommand(options) {
|
|
|
1078
1270
|
const trimmed = line.trim();
|
|
1079
1271
|
return trimmed.length > 0 && !trimmed.startsWith("#");
|
|
1080
1272
|
});
|
|
1081
|
-
console.log(
|
|
1273
|
+
console.log(pc5.green(`
|
|
1082
1274
|
\u2713 Secrets downloaded successfully`));
|
|
1083
1275
|
console.log(`
|
|
1084
|
-
File: ${
|
|
1085
|
-
console.log(`Variables: ${
|
|
1276
|
+
File: ${pc5.cyan(envFile)}`);
|
|
1277
|
+
console.log(`Variables: ${pc5.cyan(lines.length.toString())}`);
|
|
1086
1278
|
await shutdownAnalytics();
|
|
1087
1279
|
} catch (error) {
|
|
1088
|
-
const message = error instanceof APIError ? `API ${error.statusCode}: ${error.message}` : error instanceof Error ? error.message
|
|
1280
|
+
const message = error instanceof APIError ? `API ${error.statusCode}: ${error.message}` : error instanceof Error ? truncateMessage(error.message) : "Unknown error";
|
|
1089
1281
|
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
1090
1282
|
command: "pull",
|
|
1091
1283
|
error: message
|
|
1092
1284
|
});
|
|
1093
1285
|
await shutdownAnalytics();
|
|
1094
|
-
console.error(
|
|
1095
|
-
\u2717
|
|
1286
|
+
console.error(pc5.red(`
|
|
1287
|
+
\u2717 ${message}`));
|
|
1096
1288
|
process.exit(1);
|
|
1097
1289
|
}
|
|
1098
1290
|
}
|
|
1099
1291
|
|
|
1100
1292
|
// src/cmds/doctor.ts
|
|
1101
|
-
import
|
|
1293
|
+
import pc6 from "picocolors";
|
|
1102
1294
|
|
|
1103
1295
|
// src/core/doctor.ts
|
|
1104
1296
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -1280,7 +1472,7 @@ async function checkSystemClock() {
|
|
|
1280
1472
|
try {
|
|
1281
1473
|
const controller = new AbortController();
|
|
1282
1474
|
const timeout = setTimeout(() => controller.abort(), 2e3);
|
|
1283
|
-
const response = await fetch(
|
|
1475
|
+
const response = await fetch(API_HEALTH_URL, {
|
|
1284
1476
|
method: "HEAD",
|
|
1285
1477
|
signal: controller.signal
|
|
1286
1478
|
});
|
|
@@ -1352,9 +1544,9 @@ async function runAllChecks(options = {}) {
|
|
|
1352
1544
|
// src/cmds/doctor.ts
|
|
1353
1545
|
function formatSummary(results) {
|
|
1354
1546
|
const parts = [
|
|
1355
|
-
|
|
1356
|
-
results.summary.warn > 0 ?
|
|
1357
|
-
results.summary.fail > 0 ?
|
|
1547
|
+
pc6.green(`${results.summary.pass} passed`),
|
|
1548
|
+
results.summary.warn > 0 ? pc6.yellow(`${results.summary.warn} warnings`) : null,
|
|
1549
|
+
results.summary.fail > 0 ? pc6.red(`${results.summary.fail} failed`) : null
|
|
1358
1550
|
].filter(Boolean);
|
|
1359
1551
|
return parts.join(", ");
|
|
1360
1552
|
}
|
|
@@ -1371,24 +1563,24 @@ async function doctorCommand(options = {}) {
|
|
|
1371
1563
|
process.stdout.write(JSON.stringify(results, null, 0) + "\n");
|
|
1372
1564
|
process.exit(results.exitCode);
|
|
1373
1565
|
}
|
|
1374
|
-
console.log(
|
|
1566
|
+
console.log(pc6.cyan("\n\u{1F50D} Keyway Doctor - Environment Check\n"));
|
|
1375
1567
|
results.checks.forEach((check) => {
|
|
1376
|
-
const icon = check.status === "pass" ?
|
|
1377
|
-
const detail = check.detail ?
|
|
1568
|
+
const icon = check.status === "pass" ? pc6.green("\u2713") : check.status === "warn" ? pc6.yellow("!") : pc6.red("\u2717");
|
|
1569
|
+
const detail = check.detail ? pc6.dim(` \u2014 ${check.detail}`) : "";
|
|
1378
1570
|
console.log(` ${icon} ${check.name}${detail}`);
|
|
1379
1571
|
});
|
|
1380
1572
|
console.log(`
|
|
1381
1573
|
Summary: ${formatSummary(results)}`);
|
|
1382
1574
|
if (results.summary.fail > 0) {
|
|
1383
|
-
console.log(
|
|
1575
|
+
console.log(pc6.red("\u26A0 Some checks failed. Please resolve the issues above before using Keyway."));
|
|
1384
1576
|
} else if (results.summary.warn > 0) {
|
|
1385
|
-
console.log(
|
|
1577
|
+
console.log(pc6.yellow("\u26A0 Some warnings detected. Keyway should work but consider addressing them."));
|
|
1386
1578
|
} else {
|
|
1387
|
-
console.log(
|
|
1579
|
+
console.log(pc6.green("\u2728 All checks passed! Your environment is ready for Keyway."));
|
|
1388
1580
|
}
|
|
1389
1581
|
process.exit(results.exitCode);
|
|
1390
1582
|
} catch (error) {
|
|
1391
|
-
const message = error instanceof Error ? error.message : "Doctor failed";
|
|
1583
|
+
const message = error instanceof Error ? truncateMessage(error.message) : "Doctor failed";
|
|
1392
1584
|
trackEvent(AnalyticsEvents.CLI_DOCTOR, {
|
|
1393
1585
|
pass: 0,
|
|
1394
1586
|
warn: 0,
|
|
@@ -1405,17 +1597,403 @@ Summary: ${formatSummary(results)}`);
|
|
|
1405
1597
|
};
|
|
1406
1598
|
process.stdout.write(JSON.stringify(errorResult, null, 0) + "\n");
|
|
1407
1599
|
} else {
|
|
1408
|
-
console.error(
|
|
1600
|
+
console.error(pc6.red(`
|
|
1601
|
+
\u2717 ${message}`));
|
|
1409
1602
|
}
|
|
1410
1603
|
process.exit(1);
|
|
1411
1604
|
}
|
|
1412
1605
|
}
|
|
1413
1606
|
|
|
1607
|
+
// src/cmds/connect.ts
|
|
1608
|
+
import pc7 from "picocolors";
|
|
1609
|
+
import open2 from "open";
|
|
1610
|
+
import prompts6 from "prompts";
|
|
1611
|
+
async function connectCommand(provider, options = {}) {
|
|
1612
|
+
try {
|
|
1613
|
+
const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
|
|
1614
|
+
const { providers } = await getProviders();
|
|
1615
|
+
const providerInfo = providers.find((p) => p.name === provider.toLowerCase());
|
|
1616
|
+
if (!providerInfo) {
|
|
1617
|
+
const available = providers.map((p) => p.name).join(", ");
|
|
1618
|
+
console.error(pc7.red(`Unknown provider: ${provider}`));
|
|
1619
|
+
console.log(pc7.gray(`Available providers: ${available || "none"}`));
|
|
1620
|
+
process.exit(1);
|
|
1621
|
+
}
|
|
1622
|
+
if (!providerInfo.configured) {
|
|
1623
|
+
console.error(pc7.red(`Provider ${providerInfo.displayName} is not configured on the server.`));
|
|
1624
|
+
console.log(pc7.gray("Contact your administrator to enable this integration."));
|
|
1625
|
+
process.exit(1);
|
|
1626
|
+
}
|
|
1627
|
+
const { connections } = await getConnections(accessToken);
|
|
1628
|
+
const existingConnection = connections.find((c) => c.provider === provider.toLowerCase());
|
|
1629
|
+
if (existingConnection) {
|
|
1630
|
+
const { reconnect } = await prompts6({
|
|
1631
|
+
type: "confirm",
|
|
1632
|
+
name: "reconnect",
|
|
1633
|
+
message: `You're already connected to ${providerInfo.displayName}. Reconnect?`,
|
|
1634
|
+
initial: false
|
|
1635
|
+
});
|
|
1636
|
+
if (!reconnect) {
|
|
1637
|
+
console.log(pc7.gray("Keeping existing connection."));
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
console.log(pc7.blue(`
|
|
1642
|
+
Connecting to ${providerInfo.displayName}...
|
|
1643
|
+
`));
|
|
1644
|
+
const authUrl = getProviderAuthUrl(provider.toLowerCase());
|
|
1645
|
+
const startTime = /* @__PURE__ */ new Date();
|
|
1646
|
+
console.log(pc7.gray("Opening browser for authorization..."));
|
|
1647
|
+
console.log(pc7.gray(`If the browser doesn't open, visit: ${authUrl}`));
|
|
1648
|
+
await open2(authUrl).catch(() => {
|
|
1649
|
+
});
|
|
1650
|
+
console.log(pc7.gray("Waiting for authorization..."));
|
|
1651
|
+
const maxAttempts = 60;
|
|
1652
|
+
let attempts = 0;
|
|
1653
|
+
let connected = false;
|
|
1654
|
+
while (attempts < maxAttempts) {
|
|
1655
|
+
await new Promise((resolve) => setTimeout(resolve, 5e3));
|
|
1656
|
+
attempts++;
|
|
1657
|
+
try {
|
|
1658
|
+
const { connections: connections2 } = await getConnections(accessToken);
|
|
1659
|
+
const newConn = connections2.find(
|
|
1660
|
+
(c) => c.provider === provider.toLowerCase() && new Date(c.createdAt) > startTime
|
|
1661
|
+
);
|
|
1662
|
+
if (newConn) {
|
|
1663
|
+
connected = true;
|
|
1664
|
+
console.log(pc7.green(`
|
|
1665
|
+
\u2713 Connected to ${providerInfo.displayName}!`));
|
|
1666
|
+
break;
|
|
1667
|
+
}
|
|
1668
|
+
} catch {
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
if (!connected) {
|
|
1672
|
+
console.log(pc7.red("\n\u2717 Authorization timeout."));
|
|
1673
|
+
console.log(pc7.gray("Run `keyway connections` to check if the connection was established."));
|
|
1674
|
+
}
|
|
1675
|
+
trackEvent(AnalyticsEvents.CLI_CONNECT, {
|
|
1676
|
+
provider: provider.toLowerCase(),
|
|
1677
|
+
success: connected
|
|
1678
|
+
});
|
|
1679
|
+
} catch (error) {
|
|
1680
|
+
const message = error instanceof Error ? error.message : "Connection failed";
|
|
1681
|
+
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
1682
|
+
command: "connect",
|
|
1683
|
+
error: truncateMessage(message)
|
|
1684
|
+
});
|
|
1685
|
+
console.error(pc7.red(`
|
|
1686
|
+
\u2717 ${message}`));
|
|
1687
|
+
process.exit(1);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
async function connectionsCommand(options = {}) {
|
|
1691
|
+
try {
|
|
1692
|
+
const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
|
|
1693
|
+
const { connections } = await getConnections(accessToken);
|
|
1694
|
+
if (connections.length === 0) {
|
|
1695
|
+
console.log(pc7.gray("No provider connections found."));
|
|
1696
|
+
console.log(pc7.gray("\nConnect to a provider with: keyway connect <provider>"));
|
|
1697
|
+
console.log(pc7.gray("Available providers: vercel"));
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
console.log(pc7.blue("\n\u{1F4E1} Provider Connections\n"));
|
|
1701
|
+
for (const conn of connections) {
|
|
1702
|
+
const providerName = conn.provider.charAt(0).toUpperCase() + conn.provider.slice(1);
|
|
1703
|
+
const teamInfo = conn.providerTeamId ? pc7.gray(` (Team: ${conn.providerTeamId})`) : "";
|
|
1704
|
+
const date = new Date(conn.createdAt).toLocaleDateString();
|
|
1705
|
+
console.log(` ${pc7.green("\u25CF")} ${pc7.bold(providerName)}${teamInfo}`);
|
|
1706
|
+
console.log(pc7.gray(` Connected: ${date}`));
|
|
1707
|
+
console.log(pc7.gray(` ID: ${conn.id}`));
|
|
1708
|
+
console.log("");
|
|
1709
|
+
}
|
|
1710
|
+
} catch (error) {
|
|
1711
|
+
const message = error instanceof Error ? error.message : "Failed to list connections";
|
|
1712
|
+
console.error(pc7.red(`
|
|
1713
|
+
\u2717 ${message}`));
|
|
1714
|
+
process.exit(1);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
async function disconnectCommand(provider, options = {}) {
|
|
1718
|
+
try {
|
|
1719
|
+
const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
|
|
1720
|
+
const { connections } = await getConnections(accessToken);
|
|
1721
|
+
const connection = connections.find((c) => c.provider === provider.toLowerCase());
|
|
1722
|
+
if (!connection) {
|
|
1723
|
+
console.log(pc7.gray(`No connection found for provider: ${provider}`));
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
1727
|
+
const { confirm } = await prompts6({
|
|
1728
|
+
type: "confirm",
|
|
1729
|
+
name: "confirm",
|
|
1730
|
+
message: `Disconnect from ${providerName}?`,
|
|
1731
|
+
initial: false
|
|
1732
|
+
});
|
|
1733
|
+
if (!confirm) {
|
|
1734
|
+
console.log(pc7.gray("Cancelled."));
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
await deleteConnection(accessToken, connection.id);
|
|
1738
|
+
console.log(pc7.green(`
|
|
1739
|
+
\u2713 Disconnected from ${providerName}`));
|
|
1740
|
+
trackEvent(AnalyticsEvents.CLI_DISCONNECT, {
|
|
1741
|
+
provider: provider.toLowerCase()
|
|
1742
|
+
});
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
const message = error instanceof Error ? error.message : "Disconnect failed";
|
|
1745
|
+
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
1746
|
+
command: "disconnect",
|
|
1747
|
+
error: truncateMessage(message)
|
|
1748
|
+
});
|
|
1749
|
+
console.error(pc7.red(`
|
|
1750
|
+
\u2717 ${message}`));
|
|
1751
|
+
process.exit(1);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// src/cmds/sync.ts
|
|
1756
|
+
import pc8 from "picocolors";
|
|
1757
|
+
import prompts7 from "prompts";
|
|
1758
|
+
function findMatchingProject(projects, repoFullName) {
|
|
1759
|
+
const repoName = repoFullName.split("/")[1]?.toLowerCase();
|
|
1760
|
+
if (!repoName) return void 0;
|
|
1761
|
+
const exact = projects.find((p) => p.name.toLowerCase() === repoName);
|
|
1762
|
+
if (exact) return exact;
|
|
1763
|
+
const partial = projects.filter(
|
|
1764
|
+
(p) => p.name.toLowerCase().includes(repoName) || repoName.includes(p.name.toLowerCase())
|
|
1765
|
+
);
|
|
1766
|
+
return partial.length === 1 ? partial[0] : void 0;
|
|
1767
|
+
}
|
|
1768
|
+
async function syncCommand(provider, options = {}) {
|
|
1769
|
+
try {
|
|
1770
|
+
if (options.pull && options.allowDelete) {
|
|
1771
|
+
console.error(pc8.red("Error: --allow-delete cannot be used with --pull"));
|
|
1772
|
+
console.log(pc8.gray("The --allow-delete flag is only for push operations."));
|
|
1773
|
+
process.exit(1);
|
|
1774
|
+
}
|
|
1775
|
+
const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
|
|
1776
|
+
const repoFullName = detectGitRepo();
|
|
1777
|
+
if (!repoFullName) {
|
|
1778
|
+
console.error(pc8.red("Could not detect Git repository."));
|
|
1779
|
+
console.log(pc8.gray("Run this command from a Git repository directory."));
|
|
1780
|
+
process.exit(1);
|
|
1781
|
+
}
|
|
1782
|
+
console.log(pc8.gray(`Repository: ${repoFullName}`));
|
|
1783
|
+
const { connections } = await getConnections(accessToken);
|
|
1784
|
+
const connection = connections.find((c) => c.provider === provider.toLowerCase());
|
|
1785
|
+
if (!connection) {
|
|
1786
|
+
console.error(pc8.red(`Not connected to ${provider}.`));
|
|
1787
|
+
console.log(pc8.gray(`Run: keyway connect ${provider}`));
|
|
1788
|
+
process.exit(1);
|
|
1789
|
+
}
|
|
1790
|
+
const { projects } = await getConnectionProjects(accessToken, connection.id);
|
|
1791
|
+
if (projects.length === 0) {
|
|
1792
|
+
console.error(pc8.red(`No projects found in your ${provider} account.`));
|
|
1793
|
+
process.exit(1);
|
|
1794
|
+
}
|
|
1795
|
+
let selectedProject;
|
|
1796
|
+
if (options.project) {
|
|
1797
|
+
const found = projects.find(
|
|
1798
|
+
(p) => p.id === options.project || p.name.toLowerCase() === options.project?.toLowerCase()
|
|
1799
|
+
);
|
|
1800
|
+
if (!found) {
|
|
1801
|
+
console.error(pc8.red(`Project not found: ${options.project}`));
|
|
1802
|
+
console.log(pc8.gray("Available projects:"));
|
|
1803
|
+
projects.forEach((p) => console.log(pc8.gray(` - ${p.name}`)));
|
|
1804
|
+
process.exit(1);
|
|
1805
|
+
}
|
|
1806
|
+
selectedProject = found;
|
|
1807
|
+
} else {
|
|
1808
|
+
const autoMatch = findMatchingProject(projects, repoFullName);
|
|
1809
|
+
if (autoMatch && projects.length > 1) {
|
|
1810
|
+
console.log(pc8.gray(`Detected project: ${autoMatch.name}`));
|
|
1811
|
+
const { useDetected } = await prompts7({
|
|
1812
|
+
type: "confirm",
|
|
1813
|
+
name: "useDetected",
|
|
1814
|
+
message: `Use ${autoMatch.name}?`,
|
|
1815
|
+
initial: true
|
|
1816
|
+
});
|
|
1817
|
+
if (useDetected) {
|
|
1818
|
+
selectedProject = autoMatch;
|
|
1819
|
+
} else {
|
|
1820
|
+
const { projectChoice } = await prompts7({
|
|
1821
|
+
type: "select",
|
|
1822
|
+
name: "projectChoice",
|
|
1823
|
+
message: "Select a project:",
|
|
1824
|
+
choices: projects.map((p) => ({ title: p.name, value: p.id }))
|
|
1825
|
+
});
|
|
1826
|
+
selectedProject = projects.find((p) => p.id === projectChoice);
|
|
1827
|
+
}
|
|
1828
|
+
} else if (autoMatch) {
|
|
1829
|
+
selectedProject = autoMatch;
|
|
1830
|
+
} else if (projects.length === 1) {
|
|
1831
|
+
selectedProject = projects[0];
|
|
1832
|
+
} else {
|
|
1833
|
+
const { projectChoice } = await prompts7({
|
|
1834
|
+
type: "select",
|
|
1835
|
+
name: "projectChoice",
|
|
1836
|
+
message: "Select a project:",
|
|
1837
|
+
choices: projects.map((p) => ({ title: p.name, value: p.id }))
|
|
1838
|
+
});
|
|
1839
|
+
if (!projectChoice) {
|
|
1840
|
+
console.log(pc8.gray("Cancelled."));
|
|
1841
|
+
process.exit(0);
|
|
1842
|
+
}
|
|
1843
|
+
selectedProject = projects.find((p) => p.id === projectChoice);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
const keywayEnv = options.environment || "production";
|
|
1847
|
+
const providerEnv = options.providerEnv || "production";
|
|
1848
|
+
const direction = options.pull ? "pull" : "push";
|
|
1849
|
+
const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
1850
|
+
console.log(pc8.gray(`Project: ${selectedProject.name}`));
|
|
1851
|
+
console.log(pc8.gray(`Direction: ${direction === "push" ? "Keyway \u2192 " + providerName : providerName + " \u2192 Keyway"}`));
|
|
1852
|
+
const status = await getSyncStatus(
|
|
1853
|
+
accessToken,
|
|
1854
|
+
repoFullName,
|
|
1855
|
+
connection.id,
|
|
1856
|
+
selectedProject.id,
|
|
1857
|
+
keywayEnv
|
|
1858
|
+
);
|
|
1859
|
+
if (status.isFirstSync && !options.pull && status.vaultIsEmpty && status.providerHasSecrets) {
|
|
1860
|
+
console.log(pc8.yellow(`
|
|
1861
|
+
\u26A0\uFE0F Your Keyway vault is empty, but ${providerName} has ${status.providerSecretCount} secrets.`));
|
|
1862
|
+
const { importFirst } = await prompts7({
|
|
1863
|
+
type: "confirm",
|
|
1864
|
+
name: "importFirst",
|
|
1865
|
+
message: `Import secrets from ${providerName} first?`,
|
|
1866
|
+
initial: true
|
|
1867
|
+
});
|
|
1868
|
+
if (importFirst) {
|
|
1869
|
+
await executeSyncOperation(
|
|
1870
|
+
accessToken,
|
|
1871
|
+
repoFullName,
|
|
1872
|
+
connection.id,
|
|
1873
|
+
selectedProject,
|
|
1874
|
+
keywayEnv,
|
|
1875
|
+
providerEnv,
|
|
1876
|
+
"pull",
|
|
1877
|
+
false,
|
|
1878
|
+
// Never delete on import
|
|
1879
|
+
options.yes || false,
|
|
1880
|
+
provider
|
|
1881
|
+
);
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
await executeSyncOperation(
|
|
1886
|
+
accessToken,
|
|
1887
|
+
repoFullName,
|
|
1888
|
+
connection.id,
|
|
1889
|
+
selectedProject,
|
|
1890
|
+
keywayEnv,
|
|
1891
|
+
providerEnv,
|
|
1892
|
+
direction,
|
|
1893
|
+
options.allowDelete || false,
|
|
1894
|
+
options.yes || false,
|
|
1895
|
+
provider
|
|
1896
|
+
);
|
|
1897
|
+
} catch (error) {
|
|
1898
|
+
const message = error instanceof Error ? error.message : "Sync failed";
|
|
1899
|
+
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
1900
|
+
command: "sync",
|
|
1901
|
+
error: truncateMessage(message)
|
|
1902
|
+
});
|
|
1903
|
+
console.error(pc8.red(`
|
|
1904
|
+
\u2717 ${message}`));
|
|
1905
|
+
process.exit(1);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
async function executeSyncOperation(accessToken, repoFullName, connectionId, project, keywayEnv, providerEnv, direction, allowDelete, skipConfirm, provider) {
|
|
1909
|
+
const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
1910
|
+
const preview = await getSyncPreview(accessToken, repoFullName, {
|
|
1911
|
+
connectionId,
|
|
1912
|
+
projectId: project.id,
|
|
1913
|
+
keywayEnvironment: keywayEnv,
|
|
1914
|
+
providerEnvironment: providerEnv,
|
|
1915
|
+
direction,
|
|
1916
|
+
allowDelete
|
|
1917
|
+
});
|
|
1918
|
+
const totalChanges = preview.toCreate.length + preview.toUpdate.length + preview.toDelete.length;
|
|
1919
|
+
if (totalChanges === 0) {
|
|
1920
|
+
console.log(pc8.green("\n\u2713 Already in sync. No changes needed."));
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
console.log(pc8.blue("\n\u{1F4CB} Sync Preview\n"));
|
|
1924
|
+
if (preview.toCreate.length > 0) {
|
|
1925
|
+
console.log(pc8.green(` + ${preview.toCreate.length} to create`));
|
|
1926
|
+
preview.toCreate.slice(0, 5).forEach((key) => console.log(pc8.gray(` ${key}`)));
|
|
1927
|
+
if (preview.toCreate.length > 5) {
|
|
1928
|
+
console.log(pc8.gray(` ... and ${preview.toCreate.length - 5} more`));
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
if (preview.toUpdate.length > 0) {
|
|
1932
|
+
console.log(pc8.yellow(` ~ ${preview.toUpdate.length} to update`));
|
|
1933
|
+
preview.toUpdate.slice(0, 5).forEach((key) => console.log(pc8.gray(` ${key}`)));
|
|
1934
|
+
if (preview.toUpdate.length > 5) {
|
|
1935
|
+
console.log(pc8.gray(` ... and ${preview.toUpdate.length - 5} more`));
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
if (preview.toDelete.length > 0) {
|
|
1939
|
+
console.log(pc8.red(` - ${preview.toDelete.length} to delete`));
|
|
1940
|
+
preview.toDelete.slice(0, 5).forEach((key) => console.log(pc8.gray(` ${key}`)));
|
|
1941
|
+
if (preview.toDelete.length > 5) {
|
|
1942
|
+
console.log(pc8.gray(` ... and ${preview.toDelete.length - 5} more`));
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
if (preview.toSkip.length > 0) {
|
|
1946
|
+
console.log(pc8.gray(` \u25CB ${preview.toSkip.length} unchanged`));
|
|
1947
|
+
}
|
|
1948
|
+
console.log("");
|
|
1949
|
+
if (!skipConfirm) {
|
|
1950
|
+
const target = direction === "push" ? providerName : "Keyway";
|
|
1951
|
+
const { confirm } = await prompts7({
|
|
1952
|
+
type: "confirm",
|
|
1953
|
+
name: "confirm",
|
|
1954
|
+
message: `Apply ${totalChanges} changes to ${target}?`,
|
|
1955
|
+
initial: true
|
|
1956
|
+
});
|
|
1957
|
+
if (!confirm) {
|
|
1958
|
+
console.log(pc8.gray("Cancelled."));
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
console.log(pc8.blue("\n\u23F3 Syncing...\n"));
|
|
1963
|
+
const result = await executeSync(accessToken, repoFullName, {
|
|
1964
|
+
connectionId,
|
|
1965
|
+
projectId: project.id,
|
|
1966
|
+
keywayEnvironment: keywayEnv,
|
|
1967
|
+
providerEnvironment: providerEnv,
|
|
1968
|
+
direction,
|
|
1969
|
+
allowDelete
|
|
1970
|
+
});
|
|
1971
|
+
if (result.success) {
|
|
1972
|
+
console.log(pc8.green("\u2713 Sync complete"));
|
|
1973
|
+
console.log(pc8.gray(` Created: ${result.stats.created}`));
|
|
1974
|
+
console.log(pc8.gray(` Updated: ${result.stats.updated}`));
|
|
1975
|
+
if (result.stats.deleted > 0) {
|
|
1976
|
+
console.log(pc8.gray(` Deleted: ${result.stats.deleted}`));
|
|
1977
|
+
}
|
|
1978
|
+
trackEvent(AnalyticsEvents.CLI_SYNC, {
|
|
1979
|
+
provider,
|
|
1980
|
+
direction,
|
|
1981
|
+
created: result.stats.created,
|
|
1982
|
+
updated: result.stats.updated,
|
|
1983
|
+
deleted: result.stats.deleted
|
|
1984
|
+
});
|
|
1985
|
+
} else {
|
|
1986
|
+
console.error(pc8.red(`
|
|
1987
|
+
\u2717 ${result.error}`));
|
|
1988
|
+
process.exit(1);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1414
1992
|
// src/cli.ts
|
|
1415
1993
|
var program = new Command();
|
|
1416
1994
|
var showBanner = () => {
|
|
1417
|
-
const text =
|
|
1418
|
-
const subtitle =
|
|
1995
|
+
const text = pc9.cyan.bold("Keyway CLI");
|
|
1996
|
+
const subtitle = pc9.gray("GitHub-native secrets manager for dev teams");
|
|
1419
1997
|
console.log(`
|
|
1420
1998
|
${text}
|
|
1421
1999
|
${subtitle}
|
|
@@ -1441,7 +2019,19 @@ program.command("logout").description("Clear stored Keyway credentials").action(
|
|
|
1441
2019
|
program.command("doctor").description("Run environment checks to ensure Keyway runs smoothly").option("--json", "Output results as JSON for machine processing", false).option("--strict", "Treat warnings as failures", false).action(async (options) => {
|
|
1442
2020
|
await doctorCommand(options);
|
|
1443
2021
|
});
|
|
2022
|
+
program.command("connect <provider>").description("Connect to an external provider (e.g., vercel)").option("--no-login-prompt", "Fail instead of prompting to login if unauthenticated").action(async (provider, options) => {
|
|
2023
|
+
await connectCommand(provider, options);
|
|
2024
|
+
});
|
|
2025
|
+
program.command("connections").description("List your provider connections").option("--no-login-prompt", "Fail instead of prompting to login if unauthenticated").action(async (options) => {
|
|
2026
|
+
await connectionsCommand(options);
|
|
2027
|
+
});
|
|
2028
|
+
program.command("disconnect <provider>").description("Disconnect from a provider").option("--no-login-prompt", "Fail instead of prompting to login if unauthenticated").action(async (provider, options) => {
|
|
2029
|
+
await disconnectCommand(provider, options);
|
|
2030
|
+
});
|
|
2031
|
+
program.command("sync <provider>").description("Sync secrets with a provider (e.g., vercel)").option("--pull", "Import secrets from provider to Keyway").option("-e, --environment <env>", "Keyway environment", "production").option("--provider-env <env>", "Provider environment", "production").option("--project <project>", "Provider project name or ID").option("--allow-delete", "Allow deleting secrets not in source").option("-y, --yes", "Skip confirmation prompt").option("--no-login-prompt", "Fail instead of prompting to login if unauthenticated").action(async (provider, options) => {
|
|
2032
|
+
await syncCommand(provider, options);
|
|
2033
|
+
});
|
|
1444
2034
|
program.parseAsync().catch((error) => {
|
|
1445
|
-
console.error(
|
|
2035
|
+
console.error(pc9.red("Error:"), error.message || error);
|
|
1446
2036
|
process.exit(1);
|
|
1447
2037
|
});
|