@keywaysh/cli 0.0.14 → 0.0.16
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 +2 -1
- package/dist/cli.js +678 -96
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
<br/><br/>
|
|
6
6
|
<a href="https://keyway.sh">keyway.sh</a> ·
|
|
7
7
|
<a href="https://github.com/keywaysh/cli">GitHub</a> ·
|
|
8
|
-
<a href="https://www.npmjs.com/package/@keywaysh/cli">NPM</a>
|
|
8
|
+
<a href="https://www.npmjs.com/package/@keywaysh/cli">NPM</a> ·
|
|
9
|
+
<a href="https://status.keyway.sh">Status</a>
|
|
9
10
|
<br/><br/>
|
|
10
11
|
|
|
11
12
|
[](https://opensource.org/licenses/MIT)
|
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
|
|
@@ -69,7 +69,7 @@ var INTERNAL_POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
|
69
69
|
// package.json
|
|
70
70
|
var package_default = {
|
|
71
71
|
name: "@keywaysh/cli",
|
|
72
|
-
version: "0.0.
|
|
72
|
+
version: "0.0.16",
|
|
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,53 @@ 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
|
+
}
|
|
135
183
|
function validateApiUrl(url) {
|
|
136
184
|
const parsed = new URL(url);
|
|
137
185
|
if (parsed.protocol !== "https:") {
|
|
@@ -196,7 +244,7 @@ async function initVault(repoFullName, accessToken) {
|
|
|
196
244
|
if (accessToken) {
|
|
197
245
|
headers.Authorization = `Bearer ${accessToken}`;
|
|
198
246
|
}
|
|
199
|
-
const response = await
|
|
247
|
+
const response = await fetchWithTimeout(`${API_BASE_URL}/v1/vaults`, {
|
|
200
248
|
method: "POST",
|
|
201
249
|
headers,
|
|
202
250
|
body: JSON.stringify(body)
|
|
@@ -231,7 +279,7 @@ async function pushSecrets(repoFullName, environment, content, accessToken) {
|
|
|
231
279
|
if (accessToken) {
|
|
232
280
|
headers.Authorization = `Bearer ${accessToken}`;
|
|
233
281
|
}
|
|
234
|
-
const response = await
|
|
282
|
+
const response = await fetchWithTimeout(`${API_BASE_URL}/v1/secrets/push`, {
|
|
235
283
|
method: "POST",
|
|
236
284
|
headers,
|
|
237
285
|
body: JSON.stringify(body)
|
|
@@ -251,7 +299,7 @@ async function pullSecrets(repoFullName, environment, accessToken) {
|
|
|
251
299
|
repo: repoFullName,
|
|
252
300
|
environment
|
|
253
301
|
});
|
|
254
|
-
const response = await
|
|
302
|
+
const response = await fetchWithTimeout(`${API_BASE_URL}/v1/secrets/pull?${params}`, {
|
|
255
303
|
method: "GET",
|
|
256
304
|
headers
|
|
257
305
|
});
|
|
@@ -259,7 +307,7 @@ async function pullSecrets(repoFullName, environment, accessToken) {
|
|
|
259
307
|
return { content: result.data.content };
|
|
260
308
|
}
|
|
261
309
|
async function startDeviceLogin(repository) {
|
|
262
|
-
const response = await
|
|
310
|
+
const response = await fetchWithTimeout(`${API_BASE_URL}/v1/auth/device/start`, {
|
|
263
311
|
method: "POST",
|
|
264
312
|
headers: {
|
|
265
313
|
"Content-Type": "application/json",
|
|
@@ -270,7 +318,7 @@ async function startDeviceLogin(repository) {
|
|
|
270
318
|
return handleResponse(response);
|
|
271
319
|
}
|
|
272
320
|
async function pollDeviceLogin(deviceCode) {
|
|
273
|
-
const response = await
|
|
321
|
+
const response = await fetchWithTimeout(`${API_BASE_URL}/v1/auth/device/poll`, {
|
|
274
322
|
method: "POST",
|
|
275
323
|
headers: {
|
|
276
324
|
"Content-Type": "application/json",
|
|
@@ -281,7 +329,7 @@ async function pollDeviceLogin(deviceCode) {
|
|
|
281
329
|
return handleResponse(response);
|
|
282
330
|
}
|
|
283
331
|
async function validateToken(token) {
|
|
284
|
-
const response = await
|
|
332
|
+
const response = await fetchWithTimeout(`${API_BASE_URL}/v1/auth/token/validate`, {
|
|
285
333
|
method: "POST",
|
|
286
334
|
headers: {
|
|
287
335
|
"Content-Type": "application/json",
|
|
@@ -292,6 +340,117 @@ async function validateToken(token) {
|
|
|
292
340
|
});
|
|
293
341
|
return handleResponse(response);
|
|
294
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
|
+
}
|
|
295
454
|
|
|
296
455
|
// src/utils/analytics.ts
|
|
297
456
|
import { PostHog } from "posthog-node";
|
|
@@ -388,11 +547,14 @@ var AnalyticsEvents = {
|
|
|
388
547
|
CLI_PULL: "cli_pull",
|
|
389
548
|
CLI_ERROR: "cli_error",
|
|
390
549
|
CLI_LOGIN: "cli_login",
|
|
391
|
-
CLI_DOCTOR: "cli_doctor"
|
|
550
|
+
CLI_DOCTOR: "cli_doctor",
|
|
551
|
+
CLI_CONNECT: "cli_connect",
|
|
552
|
+
CLI_DISCONNECT: "cli_disconnect",
|
|
553
|
+
CLI_SYNC: "cli_sync"
|
|
392
554
|
};
|
|
393
555
|
|
|
394
556
|
// src/cmds/login.ts
|
|
395
|
-
import
|
|
557
|
+
import pc from "picocolors";
|
|
396
558
|
import readline from "readline";
|
|
397
559
|
import open from "open";
|
|
398
560
|
import prompts from "prompts";
|
|
@@ -511,17 +673,17 @@ async function promptYesNo(question, defaultYes = true) {
|
|
|
511
673
|
});
|
|
512
674
|
}
|
|
513
675
|
async function runLoginFlow() {
|
|
514
|
-
console.log(
|
|
676
|
+
console.log(pc.blue("\u{1F510} Starting Keyway login...\n"));
|
|
515
677
|
const repoName = detectGitRepo();
|
|
516
678
|
const start = await startDeviceLogin(repoName);
|
|
517
679
|
const verifyUrl = start.verificationUriComplete || start.verificationUri;
|
|
518
680
|
if (!verifyUrl) {
|
|
519
681
|
throw new Error("Missing verification URL from the auth server.");
|
|
520
682
|
}
|
|
521
|
-
console.log(`Code: ${
|
|
683
|
+
console.log(`Code: ${pc.bold(pc.green(start.userCode))}`);
|
|
522
684
|
console.log("Waiting for auth...");
|
|
523
685
|
open(verifyUrl).catch(() => {
|
|
524
|
-
console.log(
|
|
686
|
+
console.log(pc.gray(`Open this URL in your browser: ${verifyUrl}`));
|
|
525
687
|
});
|
|
526
688
|
const pollIntervalMs = (start.interval ?? 5) * 1e3;
|
|
527
689
|
const maxTimeoutMs = Math.min((start.expiresIn ?? 900) * 1e3, 30 * 60 * 1e3);
|
|
@@ -544,9 +706,9 @@ async function runLoginFlow() {
|
|
|
544
706
|
method: "device",
|
|
545
707
|
repo: repoName
|
|
546
708
|
});
|
|
547
|
-
console.log(
|
|
709
|
+
console.log(pc.green("\n\u2713 Login successful"));
|
|
548
710
|
if (result.githubLogin) {
|
|
549
|
-
console.log(`Authenticated GitHub user: ${
|
|
711
|
+
console.log(`Authenticated GitHub user: ${pc.cyan(result.githubLogin)}`);
|
|
550
712
|
}
|
|
551
713
|
return result.keywayToken;
|
|
552
714
|
}
|
|
@@ -554,10 +716,13 @@ async function runLoginFlow() {
|
|
|
554
716
|
}
|
|
555
717
|
}
|
|
556
718
|
async function ensureLogin(options = {}) {
|
|
557
|
-
const envToken = process.env.KEYWAY_TOKEN
|
|
719
|
+
const envToken = process.env.KEYWAY_TOKEN;
|
|
558
720
|
if (envToken) {
|
|
559
721
|
return envToken;
|
|
560
722
|
}
|
|
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
|
+
}
|
|
561
726
|
const stored = await getStoredAuth();
|
|
562
727
|
if (stored?.keywayToken) {
|
|
563
728
|
return stored.keywayToken;
|
|
@@ -576,16 +741,16 @@ async function ensureLogin(options = {}) {
|
|
|
576
741
|
async function runTokenLogin() {
|
|
577
742
|
const repoName = detectGitRepo();
|
|
578
743
|
if (repoName) {
|
|
579
|
-
console.log(`\u{1F4C1} Detected: ${
|
|
744
|
+
console.log(`\u{1F4C1} Detected: ${pc.cyan(repoName)}`);
|
|
580
745
|
}
|
|
581
746
|
const description = repoName ? `Keyway CLI for ${repoName}` : "Keyway CLI";
|
|
582
747
|
const url = `https://github.com/settings/personal-access-tokens/new?description=${encodeURIComponent(description)}`;
|
|
583
748
|
console.log("Opening GitHub...");
|
|
584
749
|
open(url).catch(() => {
|
|
585
|
-
console.log(
|
|
750
|
+
console.log(pc.gray(`Open this URL in your browser: ${url}`));
|
|
586
751
|
});
|
|
587
|
-
console.log(
|
|
588
|
-
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."));
|
|
589
754
|
const { token } = await prompts(
|
|
590
755
|
{
|
|
591
756
|
type: "password",
|
|
@@ -618,7 +783,7 @@ async function runTokenLogin() {
|
|
|
618
783
|
method: "pat",
|
|
619
784
|
repo: repoName
|
|
620
785
|
});
|
|
621
|
-
console.log(
|
|
786
|
+
console.log(pc.green("\u2705 Authenticated"), `as ${pc.cyan(`@${validation.username}`)}`);
|
|
622
787
|
return trimmedToken;
|
|
623
788
|
}
|
|
624
789
|
async function loginCommand(options = {}) {
|
|
@@ -632,24 +797,24 @@ async function loginCommand(options = {}) {
|
|
|
632
797
|
const message = error instanceof Error ? error.message : "Unexpected login error";
|
|
633
798
|
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
634
799
|
command: "login",
|
|
635
|
-
error: message
|
|
800
|
+
error: truncateMessage(message)
|
|
636
801
|
});
|
|
637
|
-
console.error(
|
|
802
|
+
console.error(pc.red(`
|
|
638
803
|
\u2717 ${message}`));
|
|
639
804
|
process.exit(1);
|
|
640
805
|
}
|
|
641
806
|
}
|
|
642
807
|
async function logoutCommand() {
|
|
643
808
|
clearAuth();
|
|
644
|
-
console.log(
|
|
645
|
-
console.log(
|
|
809
|
+
console.log(pc.green("\u2713 Logged out of Keyway"));
|
|
810
|
+
console.log(pc.gray(`Auth cache cleared: ${getAuthFilePath()}`));
|
|
646
811
|
}
|
|
647
812
|
|
|
648
813
|
// src/cmds/readme.ts
|
|
649
814
|
import fs2 from "fs";
|
|
650
815
|
import path2 from "path";
|
|
651
816
|
import prompts2 from "prompts";
|
|
652
|
-
import
|
|
817
|
+
import pc2 from "picocolors";
|
|
653
818
|
function generateBadge(repo) {
|
|
654
819
|
return `[](https://www.keyway.sh/vaults/${repo})`;
|
|
655
820
|
}
|
|
@@ -687,7 +852,7 @@ async function ensureReadme(repoName, cwd) {
|
|
|
687
852
|
if (existing) return existing;
|
|
688
853
|
const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
|
|
689
854
|
if (!isInteractive2) {
|
|
690
|
-
console.log(
|
|
855
|
+
console.log(pc2.yellow('No README found. Run "keyway readme add-badge" from a repo with a README.'));
|
|
691
856
|
return null;
|
|
692
857
|
}
|
|
693
858
|
const { confirm } = await prompts2(
|
|
@@ -702,7 +867,7 @@ async function ensureReadme(repoName, cwd) {
|
|
|
702
867
|
}
|
|
703
868
|
);
|
|
704
869
|
if (!confirm) {
|
|
705
|
-
console.log(
|
|
870
|
+
console.log(pc2.yellow("Skipping badge insertion (no README)."));
|
|
706
871
|
return null;
|
|
707
872
|
}
|
|
708
873
|
const defaultPath = path2.join(cwd, "README.md");
|
|
@@ -725,19 +890,19 @@ async function addBadgeToReadme(silent = false) {
|
|
|
725
890
|
const updated = insertBadgeIntoReadme(content, badge);
|
|
726
891
|
if (updated === content) {
|
|
727
892
|
if (!silent) {
|
|
728
|
-
console.log(
|
|
893
|
+
console.log(pc2.gray("Keyway badge already present in README."));
|
|
729
894
|
}
|
|
730
895
|
return false;
|
|
731
896
|
}
|
|
732
897
|
fs2.writeFileSync(readmePath, updated, "utf-8");
|
|
733
898
|
if (!silent) {
|
|
734
|
-
console.log(
|
|
899
|
+
console.log(pc2.green(`\u2713 Keyway badge added to ${path2.basename(readmePath)}`));
|
|
735
900
|
}
|
|
736
901
|
return true;
|
|
737
902
|
}
|
|
738
903
|
|
|
739
904
|
// src/cmds/push.ts
|
|
740
|
-
import
|
|
905
|
+
import pc3 from "picocolors";
|
|
741
906
|
import fs3 from "fs";
|
|
742
907
|
import path3 from "path";
|
|
743
908
|
import prompts3 from "prompts";
|
|
@@ -754,7 +919,7 @@ function discoverEnvCandidates(cwd) {
|
|
|
754
919
|
const entries = fs3.readdirSync(cwd);
|
|
755
920
|
const hasEnvLocal = entries.includes(".env.local");
|
|
756
921
|
if (hasEnvLocal) {
|
|
757
|
-
console.log(
|
|
922
|
+
console.log(pc3.gray("\u2139\uFE0F Detected .env.local \u2014 not synced by design (machine-specific secrets)"));
|
|
758
923
|
}
|
|
759
924
|
const candidates = entries.filter((name) => name.startsWith(".env") && name !== ".env.local").map((name) => {
|
|
760
925
|
const fullPath = path3.join(cwd, name);
|
|
@@ -780,7 +945,7 @@ function discoverEnvCandidates(cwd) {
|
|
|
780
945
|
}
|
|
781
946
|
async function pushCommand(options) {
|
|
782
947
|
try {
|
|
783
|
-
console.log(
|
|
948
|
+
console.log(pc3.blue("\u{1F510} Pushing secrets to Keyway...\n"));
|
|
784
949
|
const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
|
|
785
950
|
let environment = options.env;
|
|
786
951
|
let envFile = options.file;
|
|
@@ -880,11 +1045,11 @@ async function pushCommand(options) {
|
|
|
880
1045
|
const trimmed = line.trim();
|
|
881
1046
|
return trimmed.length > 0 && !trimmed.startsWith("#");
|
|
882
1047
|
});
|
|
883
|
-
console.log(`File: ${
|
|
884
|
-
console.log(`Environment: ${
|
|
885
|
-
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())}`);
|
|
886
1051
|
const repoFullName = getCurrentRepoFullName();
|
|
887
|
-
console.log(`Repository: ${
|
|
1052
|
+
console.log(`Repository: ${pc3.cyan(repoFullName)}`);
|
|
888
1053
|
if (!options.yes) {
|
|
889
1054
|
const isInteractive3 = process.stdin.isTTY && process.stdout.isTTY;
|
|
890
1055
|
if (!isInteractive3) {
|
|
@@ -904,7 +1069,7 @@ async function pushCommand(options) {
|
|
|
904
1069
|
}
|
|
905
1070
|
);
|
|
906
1071
|
if (!confirm) {
|
|
907
|
-
console.log(
|
|
1072
|
+
console.log(pc3.yellow("Push aborted."));
|
|
908
1073
|
return;
|
|
909
1074
|
}
|
|
910
1075
|
}
|
|
@@ -916,30 +1081,50 @@ async function pushCommand(options) {
|
|
|
916
1081
|
});
|
|
917
1082
|
console.log("\nUploading secrets...");
|
|
918
1083
|
const response = await pushSecrets(repoFullName, environment, content, accessToken);
|
|
919
|
-
console.log(
|
|
1084
|
+
console.log(pc3.green("\n\u2713 " + response.message));
|
|
920
1085
|
if (response.stats) {
|
|
921
1086
|
const { created, updated, deleted } = response.stats;
|
|
922
1087
|
const parts = [];
|
|
923
|
-
if (created > 0) parts.push(
|
|
924
|
-
if (updated > 0) parts.push(
|
|
925
|
-
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`));
|
|
926
1091
|
if (parts.length > 0) {
|
|
927
1092
|
console.log(`Stats: ${parts.join(", ")}`);
|
|
928
1093
|
}
|
|
929
1094
|
}
|
|
930
1095
|
console.log(`
|
|
931
1096
|
Your secrets are now encrypted and stored securely.`);
|
|
932
|
-
console.log(`To retrieve them, run: ${
|
|
1097
|
+
console.log(`To retrieve them, run: ${pc3.cyan(`keyway pull --env ${environment}`)}`);
|
|
933
1098
|
await shutdownAnalytics();
|
|
934
1099
|
} catch (error) {
|
|
935
|
-
|
|
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
|
+
}
|
|
936
1117
|
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
937
1118
|
command: "push",
|
|
938
1119
|
error: message
|
|
939
1120
|
});
|
|
940
1121
|
await shutdownAnalytics();
|
|
941
|
-
console.error(
|
|
942
|
-
\u2717
|
|
1122
|
+
console.error(pc3.red(`
|
|
1123
|
+
\u2717 ${message}`));
|
|
1124
|
+
if (hint) {
|
|
1125
|
+
console.error(pc3.gray(`
|
|
1126
|
+
${hint}`));
|
|
1127
|
+
}
|
|
943
1128
|
process.exit(1);
|
|
944
1129
|
}
|
|
945
1130
|
}
|
|
@@ -950,16 +1135,16 @@ async function initCommand(options = {}) {
|
|
|
950
1135
|
try {
|
|
951
1136
|
const repoFullName = getCurrentRepoFullName();
|
|
952
1137
|
const dashboardLink = `${DASHBOARD_URL}/${repoFullName}`;
|
|
953
|
-
console.log(
|
|
954
|
-
console.log(` ${
|
|
1138
|
+
console.log(pc4.blue("\u{1F510} Initializing Keyway vault...\n"));
|
|
1139
|
+
console.log(` ${pc4.gray("Repository:")} ${pc4.white(repoFullName)}`);
|
|
955
1140
|
const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
|
|
956
1141
|
trackEvent(AnalyticsEvents.CLI_INIT, { repoFullName });
|
|
957
1142
|
const response = await initVault(repoFullName, accessToken);
|
|
958
|
-
console.log(
|
|
1143
|
+
console.log(pc4.green("\u2713 Vault created!"));
|
|
959
1144
|
try {
|
|
960
1145
|
const badgeAdded = await addBadgeToReadme(true);
|
|
961
1146
|
if (badgeAdded) {
|
|
962
|
-
console.log(
|
|
1147
|
+
console.log(pc4.green("\u2713 Badge added to README.md"));
|
|
963
1148
|
}
|
|
964
1149
|
} catch {
|
|
965
1150
|
}
|
|
@@ -967,7 +1152,7 @@ async function initCommand(options = {}) {
|
|
|
967
1152
|
const envCandidates = discoverEnvCandidates(process.cwd());
|
|
968
1153
|
const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
|
|
969
1154
|
if (envCandidates.length > 0 && isInteractive2) {
|
|
970
|
-
console.log(
|
|
1155
|
+
console.log(pc4.gray(` Found ${envCandidates.length} env file(s): ${envCandidates.map((c) => c.file).join(", ")}
|
|
971
1156
|
`));
|
|
972
1157
|
const { shouldPush } = await prompts4({
|
|
973
1158
|
type: "confirm",
|
|
@@ -981,60 +1166,59 @@ async function initCommand(options = {}) {
|
|
|
981
1166
|
return;
|
|
982
1167
|
}
|
|
983
1168
|
}
|
|
984
|
-
console.log(
|
|
1169
|
+
console.log(pc4.dim("\u2500".repeat(50)));
|
|
985
1170
|
console.log("");
|
|
986
1171
|
if (envCandidates.length === 0) {
|
|
987
|
-
console.log(` ${
|
|
988
|
-
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
|
|
989
1174
|
`);
|
|
990
1175
|
} else {
|
|
991
|
-
console.log(` ${
|
|
1176
|
+
console.log(` ${pc4.yellow("\u2192")} Run ${pc4.cyan("keyway push")} to sync your secrets
|
|
992
1177
|
`);
|
|
993
1178
|
}
|
|
994
|
-
console.log(` ${
|
|
1179
|
+
console.log(` ${pc4.blue("\u2394")} Dashboard: ${pc4.underline(dashboardLink)}`);
|
|
995
1180
|
console.log("");
|
|
996
1181
|
await shutdownAnalytics();
|
|
997
1182
|
} catch (error) {
|
|
998
1183
|
if (error instanceof APIError) {
|
|
999
1184
|
if (error.statusCode === 409) {
|
|
1000
|
-
console.log(
|
|
1001
|
-
console.log(` ${
|
|
1002
|
-
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()}`)}`);
|
|
1003
1188
|
console.log("");
|
|
1004
1189
|
await shutdownAnalytics();
|
|
1005
1190
|
return;
|
|
1006
1191
|
}
|
|
1007
1192
|
if (error.error === "PLAN_LIMIT_REACHED") {
|
|
1008
1193
|
console.log("");
|
|
1009
|
-
console.log(
|
|
1194
|
+
console.log(pc4.dim("\u2500".repeat(50)));
|
|
1010
1195
|
console.log("");
|
|
1011
|
-
console.log(` ${
|
|
1196
|
+
console.log(` ${pc4.yellow("\u26A1")} ${pc4.bold("Upgrade Required")}`);
|
|
1012
1197
|
console.log("");
|
|
1013
|
-
console.log(
|
|
1014
|
-
console.log(chalk4.gray(" Upgrade to Pro for unlimited private repositories."));
|
|
1198
|
+
console.log(pc4.gray(` ${error.message}`));
|
|
1015
1199
|
console.log("");
|
|
1016
|
-
console.log(` ${
|
|
1200
|
+
console.log(` ${pc4.cyan("\u2192")} ${pc4.underline(error.upgradeUrl || "https://keyway.sh/upgrade")}`);
|
|
1017
1201
|
console.log("");
|
|
1018
|
-
console.log(
|
|
1202
|
+
console.log(pc4.dim("\u2500".repeat(50)));
|
|
1019
1203
|
console.log("");
|
|
1020
1204
|
await shutdownAnalytics();
|
|
1021
1205
|
process.exit(1);
|
|
1022
1206
|
}
|
|
1023
1207
|
}
|
|
1024
|
-
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";
|
|
1025
1209
|
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
1026
1210
|
command: "init",
|
|
1027
1211
|
error: message
|
|
1028
1212
|
});
|
|
1029
1213
|
await shutdownAnalytics();
|
|
1030
|
-
console.error(
|
|
1214
|
+
console.error(pc4.red(`
|
|
1031
1215
|
\u2717 ${message}`));
|
|
1032
1216
|
process.exit(1);
|
|
1033
1217
|
}
|
|
1034
1218
|
}
|
|
1035
1219
|
|
|
1036
1220
|
// src/cmds/pull.ts
|
|
1037
|
-
import
|
|
1221
|
+
import pc5 from "picocolors";
|
|
1038
1222
|
import fs4 from "fs";
|
|
1039
1223
|
import path4 from "path";
|
|
1040
1224
|
import prompts5 from "prompts";
|
|
@@ -1042,10 +1226,10 @@ async function pullCommand(options) {
|
|
|
1042
1226
|
try {
|
|
1043
1227
|
const environment = options.env || "development";
|
|
1044
1228
|
const envFile = options.file || ".env";
|
|
1045
|
-
console.log(
|
|
1046
|
-
console.log(`Environment: ${
|
|
1229
|
+
console.log(pc5.blue("\u{1F510} Pulling secrets from Keyway...\n"));
|
|
1230
|
+
console.log(`Environment: ${pc5.cyan(environment)}`);
|
|
1047
1231
|
const repoFullName = getCurrentRepoFullName();
|
|
1048
|
-
console.log(`Repository: ${
|
|
1232
|
+
console.log(`Repository: ${pc5.cyan(repoFullName)}`);
|
|
1049
1233
|
const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
|
|
1050
1234
|
trackEvent(AnalyticsEvents.CLI_PULL, {
|
|
1051
1235
|
repoFullName,
|
|
@@ -1057,7 +1241,7 @@ async function pullCommand(options) {
|
|
|
1057
1241
|
if (fs4.existsSync(envFilePath)) {
|
|
1058
1242
|
const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
|
|
1059
1243
|
if (options.yes) {
|
|
1060
|
-
console.log(
|
|
1244
|
+
console.log(pc5.yellow(`
|
|
1061
1245
|
\u26A0 Overwriting existing file: ${envFile}`));
|
|
1062
1246
|
} else if (!isInteractive2) {
|
|
1063
1247
|
throw new Error(`File ${envFile} exists. Re-run with --yes to overwrite or choose a different --file.`);
|
|
@@ -1076,7 +1260,7 @@ async function pullCommand(options) {
|
|
|
1076
1260
|
}
|
|
1077
1261
|
);
|
|
1078
1262
|
if (!confirm) {
|
|
1079
|
-
console.log(
|
|
1263
|
+
console.log(pc5.yellow("Pull aborted."));
|
|
1080
1264
|
return;
|
|
1081
1265
|
}
|
|
1082
1266
|
}
|
|
@@ -1086,27 +1270,27 @@ async function pullCommand(options) {
|
|
|
1086
1270
|
const trimmed = line.trim();
|
|
1087
1271
|
return trimmed.length > 0 && !trimmed.startsWith("#");
|
|
1088
1272
|
});
|
|
1089
|
-
console.log(
|
|
1273
|
+
console.log(pc5.green(`
|
|
1090
1274
|
\u2713 Secrets downloaded successfully`));
|
|
1091
1275
|
console.log(`
|
|
1092
|
-
File: ${
|
|
1093
|
-
console.log(`Variables: ${
|
|
1276
|
+
File: ${pc5.cyan(envFile)}`);
|
|
1277
|
+
console.log(`Variables: ${pc5.cyan(lines.length.toString())}`);
|
|
1094
1278
|
await shutdownAnalytics();
|
|
1095
1279
|
} catch (error) {
|
|
1096
|
-
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";
|
|
1097
1281
|
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
1098
1282
|
command: "pull",
|
|
1099
1283
|
error: message
|
|
1100
1284
|
});
|
|
1101
1285
|
await shutdownAnalytics();
|
|
1102
|
-
console.error(
|
|
1103
|
-
\u2717
|
|
1286
|
+
console.error(pc5.red(`
|
|
1287
|
+
\u2717 ${message}`));
|
|
1104
1288
|
process.exit(1);
|
|
1105
1289
|
}
|
|
1106
1290
|
}
|
|
1107
1291
|
|
|
1108
1292
|
// src/cmds/doctor.ts
|
|
1109
|
-
import
|
|
1293
|
+
import pc6 from "picocolors";
|
|
1110
1294
|
|
|
1111
1295
|
// src/core/doctor.ts
|
|
1112
1296
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -1360,9 +1544,9 @@ async function runAllChecks(options = {}) {
|
|
|
1360
1544
|
// src/cmds/doctor.ts
|
|
1361
1545
|
function formatSummary(results) {
|
|
1362
1546
|
const parts = [
|
|
1363
|
-
|
|
1364
|
-
results.summary.warn > 0 ?
|
|
1365
|
-
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
|
|
1366
1550
|
].filter(Boolean);
|
|
1367
1551
|
return parts.join(", ");
|
|
1368
1552
|
}
|
|
@@ -1379,24 +1563,24 @@ async function doctorCommand(options = {}) {
|
|
|
1379
1563
|
process.stdout.write(JSON.stringify(results, null, 0) + "\n");
|
|
1380
1564
|
process.exit(results.exitCode);
|
|
1381
1565
|
}
|
|
1382
|
-
console.log(
|
|
1566
|
+
console.log(pc6.cyan("\n\u{1F50D} Keyway Doctor - Environment Check\n"));
|
|
1383
1567
|
results.checks.forEach((check) => {
|
|
1384
|
-
const icon = check.status === "pass" ?
|
|
1385
|
-
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}`) : "";
|
|
1386
1570
|
console.log(` ${icon} ${check.name}${detail}`);
|
|
1387
1571
|
});
|
|
1388
1572
|
console.log(`
|
|
1389
1573
|
Summary: ${formatSummary(results)}`);
|
|
1390
1574
|
if (results.summary.fail > 0) {
|
|
1391
|
-
console.log(
|
|
1575
|
+
console.log(pc6.red("\u26A0 Some checks failed. Please resolve the issues above before using Keyway."));
|
|
1392
1576
|
} else if (results.summary.warn > 0) {
|
|
1393
|
-
console.log(
|
|
1577
|
+
console.log(pc6.yellow("\u26A0 Some warnings detected. Keyway should work but consider addressing them."));
|
|
1394
1578
|
} else {
|
|
1395
|
-
console.log(
|
|
1579
|
+
console.log(pc6.green("\u2728 All checks passed! Your environment is ready for Keyway."));
|
|
1396
1580
|
}
|
|
1397
1581
|
process.exit(results.exitCode);
|
|
1398
1582
|
} catch (error) {
|
|
1399
|
-
const message = error instanceof Error ? error.message : "Doctor failed";
|
|
1583
|
+
const message = error instanceof Error ? truncateMessage(error.message) : "Doctor failed";
|
|
1400
1584
|
trackEvent(AnalyticsEvents.CLI_DOCTOR, {
|
|
1401
1585
|
pass: 0,
|
|
1402
1586
|
warn: 0,
|
|
@@ -1413,8 +1597,394 @@ Summary: ${formatSummary(results)}`);
|
|
|
1413
1597
|
};
|
|
1414
1598
|
process.stdout.write(JSON.stringify(errorResult, null, 0) + "\n");
|
|
1415
1599
|
} else {
|
|
1416
|
-
console.error(
|
|
1600
|
+
console.error(pc6.red(`
|
|
1601
|
+
\u2717 ${message}`));
|
|
1602
|
+
}
|
|
1603
|
+
process.exit(1);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
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`));
|
|
1417
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}`));
|
|
1418
1988
|
process.exit(1);
|
|
1419
1989
|
}
|
|
1420
1990
|
}
|
|
@@ -1422,8 +1992,8 @@ Summary: ${formatSummary(results)}`);
|
|
|
1422
1992
|
// src/cli.ts
|
|
1423
1993
|
var program = new Command();
|
|
1424
1994
|
var showBanner = () => {
|
|
1425
|
-
const text =
|
|
1426
|
-
const subtitle =
|
|
1995
|
+
const text = pc9.bold(pc9.cyan("Keyway CLI"));
|
|
1996
|
+
const subtitle = pc9.gray("GitHub-native secrets manager for dev teams");
|
|
1427
1997
|
console.log(`
|
|
1428
1998
|
${text}
|
|
1429
1999
|
${subtitle}
|
|
@@ -1449,7 +2019,19 @@ program.command("logout").description("Clear stored Keyway credentials").action(
|
|
|
1449
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) => {
|
|
1450
2020
|
await doctorCommand(options);
|
|
1451
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
|
+
});
|
|
1452
2034
|
program.parseAsync().catch((error) => {
|
|
1453
|
-
console.error(
|
|
2035
|
+
console.error(pc9.red("Error:"), error.message || error);
|
|
1454
2036
|
process.exit(1);
|
|
1455
2037
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keywaysh/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"description": "One link to all your secrets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"node": ">=18.0.0"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"chalk": "^4.1.2",
|
|
35
34
|
"commander": "^14.0.0",
|
|
36
35
|
"conf": "^15.0.2",
|
|
37
36
|
"open": "^11.0.0",
|
|
37
|
+
"picocolors": "^1.1.1",
|
|
38
38
|
"posthog-node": "^3.5.0",
|
|
39
39
|
"prompts": "^2.4.2"
|
|
40
40
|
},
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"dev": "pnpm exec tsx src/cli.ts",
|
|
51
|
+
"dev:local": "NODE_TLS_REJECT_UNAUTHORIZED=0 KEYWAY_API_URL=https://localhost/api pnpm exec tsx src/cli.ts",
|
|
51
52
|
"build": "pnpm exec tsup",
|
|
52
53
|
"build:watch": "pnpm exec tsup --watch",
|
|
53
54
|
"test": "pnpm exec vitest run",
|