@keywaysh/cli 0.2.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js DELETED
@@ -1,3047 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- clearAuth,
4
- getAuthFilePath,
5
- getStoredAuth,
6
- saveAuthToken
7
- } from "./chunk-IVZM2JTT.js";
8
-
9
- // src/cli.ts
10
- import { Command } from "commander";
11
-
12
- // src/utils/ui.ts
13
- import * as p from "@clack/prompts";
14
- import pc from "picocolors";
15
- function intro2(command2) {
16
- p.intro(pc.bgCyan(pc.black(` keyway ${command2} `)));
17
- }
18
- function outro2(message2) {
19
- p.outro(message2);
20
- }
21
- function spinner2() {
22
- return p.spinner();
23
- }
24
- function success(message2) {
25
- p.log.success(message2);
26
- }
27
- function error(message2) {
28
- p.log.error(message2);
29
- }
30
- function warn(message2) {
31
- p.log.warn(message2);
32
- }
33
- function info(message2) {
34
- p.log.info(message2);
35
- }
36
- function step(message2) {
37
- p.log.step(message2);
38
- }
39
- function message(message2) {
40
- p.log.message(message2);
41
- }
42
- function note2(message2, title) {
43
- p.note(message2, title);
44
- }
45
- function cancel2(message2 = "Operation cancelled.") {
46
- p.cancel(message2);
47
- process.exit(0);
48
- }
49
- async function text2(options) {
50
- const result = await p.text(options);
51
- if (p.isCancel(result)) {
52
- cancel2();
53
- }
54
- return result;
55
- }
56
- async function confirm2(options) {
57
- const result = await p.confirm(options);
58
- if (p.isCancel(result)) {
59
- cancel2();
60
- }
61
- return result;
62
- }
63
- async function select2(options) {
64
- const result = await p.select(options);
65
- if (p.isCancel(result)) {
66
- cancel2();
67
- }
68
- return result;
69
- }
70
- function link(url) {
71
- return pc.underline(pc.cyan(url));
72
- }
73
- function command(cmd) {
74
- return pc.cyan(cmd);
75
- }
76
- function file(path7) {
77
- return pc.cyan(path7);
78
- }
79
- function value(val) {
80
- return pc.cyan(String(val));
81
- }
82
- function dim(text3) {
83
- return pc.dim(text3);
84
- }
85
- function bold(text3) {
86
- return pc.bold(text3);
87
- }
88
-
89
- // src/utils/git.ts
90
- import { execSync } from "child_process";
91
- import fs from "fs";
92
- import path from "path";
93
- function getCurrentRepoFullName() {
94
- try {
95
- if (!isGitRepository()) {
96
- throw new Error("Not in a git repository");
97
- }
98
- const remoteUrl = execSync("git config --get remote.origin.url", {
99
- encoding: "utf-8"
100
- }).trim();
101
- return parseGitHubUrl(remoteUrl);
102
- } catch (error2) {
103
- throw new Error("Failed to get repository name. Make sure you are in a git repository with a GitHub remote.");
104
- }
105
- }
106
- function isGitRepository() {
107
- try {
108
- execSync("git rev-parse --is-inside-work-tree", {
109
- encoding: "utf-8",
110
- stdio: "pipe"
111
- });
112
- return true;
113
- } catch {
114
- return false;
115
- }
116
- }
117
- function detectGitRepo() {
118
- try {
119
- const remoteUrl = execSync("git remote get-url origin", {
120
- encoding: "utf-8",
121
- stdio: "pipe"
122
- }).trim();
123
- return parseGitHubUrl(remoteUrl);
124
- } catch {
125
- return null;
126
- }
127
- }
128
- function parseGitHubUrl(url) {
129
- const sshMatch = url.match(/git@github\.com:(.+)\/(.+)\.git/);
130
- if (sshMatch) {
131
- return `${sshMatch[1]}/${sshMatch[2]}`;
132
- }
133
- const httpsMatch = url.match(/https:\/\/github\.com\/(.+)\/(.+)\.git/);
134
- if (httpsMatch) {
135
- return `${httpsMatch[1]}/${httpsMatch[2]}`;
136
- }
137
- const httpsMatch2 = url.match(/https:\/\/github\.com\/(.+)\/(.+)/);
138
- if (httpsMatch2) {
139
- return `${httpsMatch2[1]}/${httpsMatch2[2]}`;
140
- }
141
- throw new Error(`Invalid GitHub URL: ${url}`);
142
- }
143
- function checkEnvGitignore() {
144
- try {
145
- const gitRoot = execSync("git rev-parse --show-toplevel", {
146
- encoding: "utf-8",
147
- stdio: "pipe"
148
- }).trim();
149
- const gitignorePath = path.join(gitRoot, ".gitignore");
150
- if (!fs.existsSync(gitignorePath)) {
151
- return false;
152
- }
153
- const content = fs.readFileSync(gitignorePath, "utf-8");
154
- const lines = content.split("\n").map((l) => l.trim());
155
- const envPatterns = [".env", ".env*", ".env.*", "*.env"];
156
- return envPatterns.some((pattern) => lines.includes(pattern));
157
- } catch {
158
- return true;
159
- }
160
- }
161
- function addEnvToGitignore() {
162
- try {
163
- const gitRoot = execSync("git rev-parse --show-toplevel", {
164
- encoding: "utf-8",
165
- stdio: "pipe"
166
- }).trim();
167
- const gitignorePath = path.join(gitRoot, ".gitignore");
168
- const envEntry = ".env*";
169
- if (fs.existsSync(gitignorePath)) {
170
- const content = fs.readFileSync(gitignorePath, "utf-8");
171
- const newContent = content.endsWith("\n") ? `${content}${envEntry}
172
- ` : `${content}
173
- ${envEntry}
174
- `;
175
- fs.writeFileSync(gitignorePath, newContent);
176
- } else {
177
- fs.writeFileSync(gitignorePath, `${envEntry}
178
- `);
179
- }
180
- return true;
181
- } catch {
182
- return false;
183
- }
184
- }
185
- async function warnIfEnvNotGitignored() {
186
- if (checkEnvGitignore()) {
187
- return;
188
- }
189
- warn(".env files are not in .gitignore - secrets may be committed");
190
- const addToGitignore = await confirm2({
191
- message: "Add .env* to .gitignore?",
192
- initialValue: true
193
- });
194
- if (addToGitignore) {
195
- if (addEnvToGitignore()) {
196
- success("Added .env* to .gitignore");
197
- } else {
198
- error("Failed to update .gitignore");
199
- }
200
- }
201
- }
202
-
203
- // src/config/internal.ts
204
- var INTERNAL_API_URL = "https://api.keyway.sh";
205
- var INTERNAL_POSTHOG_KEY = "phc_duG0qqI5z8LeHrS9pNxR5KaD4djgD0nmzUxuD3zP0ov";
206
- var INTERNAL_POSTHOG_HOST = "https://eu.i.posthog.com";
207
-
208
- // package.json
209
- var package_default = {
210
- name: "@keywaysh/cli",
211
- version: "0.2.0",
212
- description: "Env vars that sync like code.",
213
- type: "module",
214
- bin: {
215
- keyway: "./dist/cli.js"
216
- },
217
- main: "./dist/cli.js",
218
- files: [
219
- "dist"
220
- ],
221
- scripts: {
222
- dev: "pnpm exec tsx src/cli.ts",
223
- "dev:local": "NODE_TLS_REJECT_UNAUTHORIZED=0 KEYWAY_API_URL=https://localhost/api pnpm exec tsx src/cli.ts",
224
- build: "pnpm exec tsup",
225
- "build:watch": "pnpm exec tsup --watch",
226
- prepublishOnly: "pnpm run build",
227
- test: "pnpm exec vitest run",
228
- "test:watch": "pnpm exec vitest",
229
- "test:coverage": "pnpm exec vitest run --coverage",
230
- release: "npm version patch && git push && git push --tags",
231
- "release:minor": "npm version minor && git push && git push --tags",
232
- "release:major": "npm version major && git push && git push --tags"
233
- },
234
- keywords: [
235
- "secrets",
236
- "env",
237
- "keyway",
238
- "cli",
239
- "devops"
240
- ],
241
- author: "Nicolas Ritouet",
242
- license: "MIT",
243
- homepage: "https://keyway.sh",
244
- repository: {
245
- type: "git",
246
- url: "https://github.com/keywaysh/cli.git"
247
- },
248
- bugs: {
249
- url: "https://github.com/keywaysh/cli/issues"
250
- },
251
- packageManager: "pnpm@10.6.1",
252
- engines: {
253
- node: ">=18.0.0"
254
- },
255
- dependencies: {
256
- "@octokit/rest": "^22.0.1",
257
- "balanced-match": "^3.0.1",
258
- commander: "^14.0.0",
259
- conf: "^15.0.2",
260
- "libsodium-wrappers": "^0.7.15",
261
- open: "^11.0.0",
262
- picocolors: "^1.1.1",
263
- "posthog-node": "^3.5.0",
264
- "@clack/prompts": "^0.9.1"
265
- },
266
- devDependencies: {
267
- "@types/balanced-match": "^3.0.2",
268
- "@types/libsodium-wrappers": "^0.7.14",
269
- "@types/node": "^24.2.0",
270
- "@vitest/coverage-v8": "^3.0.0",
271
- msw: "^2.12.4",
272
- tsup: "^8.5.0",
273
- tsx: "^4.20.3",
274
- typescript: "^5.9.2",
275
- vitest: "^3.2.4"
276
- }
277
- };
278
-
279
- // src/utils/api.ts
280
- var API_BASE_URL = process.env.KEYWAY_API_URL || INTERNAL_API_URL;
281
- var USER_AGENT = `keyway-cli/${package_default.version}`;
282
- var DEFAULT_TIMEOUT_MS = 3e4;
283
- function truncateMessage(message2, maxLength = 200) {
284
- if (message2.length <= maxLength) return message2;
285
- return message2.slice(0, maxLength - 3) + "...";
286
- }
287
- var NETWORK_ERROR_MESSAGES = {
288
- ECONNREFUSED: "Cannot connect to Keyway API server. Is the server running?",
289
- ECONNRESET: "Connection was reset. Please try again.",
290
- ENOTFOUND: "DNS lookup failed. Check your internet connection.",
291
- ETIMEDOUT: "Connection timed out. Check your network connection.",
292
- ENETUNREACH: "Network is unreachable. Check your internet connection.",
293
- EHOSTUNREACH: "Host is unreachable. Check your network connection.",
294
- CERT_HAS_EXPIRED: "SSL certificate has expired. Contact support.",
295
- UNABLE_TO_VERIFY_LEAF_SIGNATURE: "SSL certificate verification failed.",
296
- EPROTO: "SSL/TLS protocol error. Try again later."
297
- };
298
- function handleNetworkError(error2) {
299
- const errorCode = error2.code || error2.cause?.code;
300
- if (errorCode && NETWORK_ERROR_MESSAGES[errorCode]) {
301
- return new Error(NETWORK_ERROR_MESSAGES[errorCode]);
302
- }
303
- const message2 = error2.message.toLowerCase();
304
- if (message2.includes("fetch failed") || message2.includes("network")) {
305
- return new Error("Network error. Check your internet connection and try again.");
306
- }
307
- return error2;
308
- }
309
- async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
310
- const controller = new AbortController();
311
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
312
- try {
313
- return await fetch(url, {
314
- ...options,
315
- signal: controller.signal
316
- });
317
- } catch (error2) {
318
- if (error2 instanceof Error) {
319
- if (error2.name === "AbortError") {
320
- throw new Error(`Request timeout after ${timeoutMs / 1e3}s. Check your network connection.`);
321
- }
322
- throw handleNetworkError(error2);
323
- }
324
- throw error2;
325
- } finally {
326
- clearTimeout(timeout);
327
- }
328
- }
329
- function validateApiUrl(url) {
330
- const parsed = new URL(url);
331
- if (parsed.protocol !== "https:") {
332
- const isLocalhost = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "0.0.0.0";
333
- if (!isLocalhost) {
334
- throw new Error(
335
- `Insecure API URL detected: ${url}
336
- HTTPS is required for security. If this is a development server, use localhost or configure HTTPS.`
337
- );
338
- }
339
- if (!process.env.KEYWAY_DISABLE_SECURITY_WARNINGS) {
340
- console.warn(
341
- `\u26A0\uFE0F WARNING: Using insecure HTTP connection to ${url}
342
- This should only be used for local development.
343
- Set KEYWAY_DISABLE_SECURITY_WARNINGS=1 to suppress this warning.`
344
- );
345
- }
346
- }
347
- }
348
- validateApiUrl(API_BASE_URL);
349
- var APIError = class extends Error {
350
- constructor(statusCode, error2, message2, upgradeUrl) {
351
- super(message2);
352
- this.statusCode = statusCode;
353
- this.error = error2;
354
- this.upgradeUrl = upgradeUrl;
355
- this.name = "APIError";
356
- }
357
- };
358
- async function handleResponse(response) {
359
- const contentType = response.headers.get("content-type") || "";
360
- const text3 = await response.text();
361
- if (!response.ok) {
362
- if (contentType.includes("application/json")) {
363
- try {
364
- const error2 = JSON.parse(text3);
365
- throw new APIError(response.status, error2.title || "Error", error2.detail || `HTTP ${response.status}`, error2.upgradeUrl);
366
- } catch (e) {
367
- if (e instanceof APIError) throw e;
368
- throw new APIError(response.status, "Error", text3 || `HTTP ${response.status}`);
369
- }
370
- }
371
- throw new APIError(response.status, "Error", text3 || `HTTP ${response.status}`);
372
- }
373
- if (!text3) {
374
- return {};
375
- }
376
- if (contentType.includes("application/json")) {
377
- try {
378
- return JSON.parse(text3);
379
- } catch {
380
- }
381
- }
382
- return { content: text3 };
383
- }
384
- async function initVault(repoFullName, accessToken) {
385
- const body = { repoFullName };
386
- const headers = {
387
- "Content-Type": "application/json",
388
- "User-Agent": USER_AGENT
389
- };
390
- if (accessToken) {
391
- headers.Authorization = `Bearer ${accessToken}`;
392
- }
393
- const response = await fetchWithTimeout(`${API_BASE_URL}/v1/vaults`, {
394
- method: "POST",
395
- headers,
396
- body: JSON.stringify(body)
397
- });
398
- const result = await handleResponse(response);
399
- return result.data;
400
- }
401
- function parseEnvContent(content) {
402
- const result = {};
403
- const lines = content.split("\n");
404
- for (const line of lines) {
405
- const trimmed = line.trim();
406
- if (!trimmed || trimmed.startsWith("#")) continue;
407
- const eqIndex = trimmed.indexOf("=");
408
- if (eqIndex === -1) continue;
409
- const key = trimmed.substring(0, eqIndex).trim();
410
- let value2 = trimmed.substring(eqIndex + 1);
411
- if (value2.startsWith('"') && value2.endsWith('"') || value2.startsWith("'") && value2.endsWith("'")) {
412
- value2 = value2.slice(1, -1);
413
- }
414
- if (key) result[key] = value2;
415
- }
416
- return result;
417
- }
418
- async function pushSecrets(repoFullName, environment, content, accessToken) {
419
- const secrets = parseEnvContent(content);
420
- const body = { repoFullName, environment, secrets };
421
- const headers = {
422
- "Content-Type": "application/json",
423
- "User-Agent": USER_AGENT
424
- };
425
- if (accessToken) {
426
- headers.Authorization = `Bearer ${accessToken}`;
427
- }
428
- const response = await fetchWithTimeout(`${API_BASE_URL}/v1/secrets/push`, {
429
- method: "POST",
430
- headers,
431
- body: JSON.stringify(body)
432
- });
433
- const result = await handleResponse(response);
434
- return result.data;
435
- }
436
- async function pullSecrets(repoFullName, environment, accessToken) {
437
- const headers = {
438
- "Content-Type": "application/json",
439
- "User-Agent": USER_AGENT
440
- };
441
- if (accessToken) {
442
- headers.Authorization = `Bearer ${accessToken}`;
443
- }
444
- const params = new URLSearchParams({
445
- repo: repoFullName,
446
- environment
447
- });
448
- const response = await fetchWithTimeout(`${API_BASE_URL}/v1/secrets/pull?${params}`, {
449
- method: "GET",
450
- headers
451
- });
452
- const result = await handleResponse(response);
453
- return { content: result.data.content };
454
- }
455
- async function startDeviceLogin(repository) {
456
- const response = await fetchWithTimeout(`${API_BASE_URL}/v1/auth/device/start`, {
457
- method: "POST",
458
- headers: {
459
- "Content-Type": "application/json",
460
- "User-Agent": USER_AGENT
461
- },
462
- body: JSON.stringify(repository ? { repository } : {})
463
- });
464
- return handleResponse(response);
465
- }
466
- async function pollDeviceLogin(deviceCode) {
467
- const response = await fetchWithTimeout(`${API_BASE_URL}/v1/auth/device/poll`, {
468
- method: "POST",
469
- headers: {
470
- "Content-Type": "application/json",
471
- "User-Agent": USER_AGENT
472
- },
473
- body: JSON.stringify({ deviceCode })
474
- });
475
- return handleResponse(response);
476
- }
477
- async function validateToken(token) {
478
- const response = await fetchWithTimeout(`${API_BASE_URL}/v1/auth/token/validate`, {
479
- method: "POST",
480
- headers: {
481
- "Content-Type": "application/json",
482
- "User-Agent": USER_AGENT,
483
- Authorization: `Bearer ${token}`
484
- },
485
- body: JSON.stringify({})
486
- });
487
- const wrapped = await handleResponse(response);
488
- return wrapped.data;
489
- }
490
- async function getProviders() {
491
- const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations`, {
492
- method: "GET",
493
- headers: {
494
- "User-Agent": USER_AGENT
495
- }
496
- });
497
- const wrapped = await handleResponse(response);
498
- return wrapped.data;
499
- }
500
- async function getConnections(accessToken) {
501
- const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations/connections`, {
502
- method: "GET",
503
- headers: {
504
- "User-Agent": USER_AGENT,
505
- Authorization: `Bearer ${accessToken}`
506
- }
507
- });
508
- const wrapped = await handleResponse(response);
509
- return wrapped.data;
510
- }
511
- async function deleteConnection(accessToken, connectionId) {
512
- const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations/connections/${connectionId}`, {
513
- method: "DELETE",
514
- headers: {
515
- "User-Agent": USER_AGENT,
516
- Authorization: `Bearer ${accessToken}`
517
- }
518
- });
519
- await handleResponse(response);
520
- }
521
- function getProviderAuthUrl(provider, accessToken, redirectUri) {
522
- const params = new URLSearchParams({ token: accessToken });
523
- if (redirectUri) params.set("redirect_uri", redirectUri);
524
- return `${API_BASE_URL}/v1/integrations/${provider}/authorize?${params}`;
525
- }
526
- async function getAllProviderProjects(accessToken, provider) {
527
- const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations/providers/${provider}/all-projects`, {
528
- method: "GET",
529
- headers: {
530
- "User-Agent": USER_AGENT,
531
- Authorization: `Bearer ${accessToken}`
532
- }
533
- });
534
- const wrapped = await handleResponse(response);
535
- return wrapped.data;
536
- }
537
- async function getSyncStatus(accessToken, repoFullName, connectionId, projectId, environment = "production") {
538
- const [owner, repo] = repoFullName.split("/");
539
- const params = new URLSearchParams({
540
- connectionId,
541
- projectId,
542
- environment
543
- });
544
- const response = await fetchWithTimeout(
545
- `${API_BASE_URL}/v1/integrations/vaults/${owner}/${repo}/sync/status?${params}`,
546
- {
547
- method: "GET",
548
- headers: {
549
- "User-Agent": USER_AGENT,
550
- Authorization: `Bearer ${accessToken}`
551
- }
552
- }
553
- );
554
- const wrapped = await handleResponse(response);
555
- return wrapped.data;
556
- }
557
- async function getSyncDiff(accessToken, repoFullName, options) {
558
- const [owner, repo] = repoFullName.split("/");
559
- const params = new URLSearchParams({
560
- connectionId: options.connectionId,
561
- projectId: options.projectId,
562
- keywayEnvironment: options.keywayEnvironment || "production",
563
- providerEnvironment: options.providerEnvironment || "production"
564
- });
565
- if (options.serviceId) {
566
- params.set("serviceId", options.serviceId);
567
- }
568
- const response = await fetchWithTimeout(
569
- `${API_BASE_URL}/v1/integrations/vaults/${owner}/${repo}/sync/diff?${params}`,
570
- {
571
- method: "GET",
572
- headers: {
573
- "User-Agent": USER_AGENT,
574
- Authorization: `Bearer ${accessToken}`
575
- }
576
- },
577
- 6e4
578
- // 60 seconds
579
- );
580
- const wrapped = await handleResponse(response);
581
- return wrapped.data;
582
- }
583
- async function getSyncPreview(accessToken, repoFullName, options) {
584
- const [owner, repo] = repoFullName.split("/");
585
- const params = new URLSearchParams({
586
- connectionId: options.connectionId,
587
- projectId: options.projectId,
588
- keywayEnvironment: options.keywayEnvironment || "production",
589
- providerEnvironment: options.providerEnvironment || "production",
590
- direction: options.direction || "push",
591
- allowDelete: String(options.allowDelete || false)
592
- });
593
- if (options.serviceId) {
594
- params.set("serviceId", options.serviceId);
595
- }
596
- const response = await fetchWithTimeout(
597
- `${API_BASE_URL}/v1/integrations/vaults/${owner}/${repo}/sync/preview?${params}`,
598
- {
599
- method: "GET",
600
- headers: {
601
- "User-Agent": USER_AGENT,
602
- Authorization: `Bearer ${accessToken}`
603
- }
604
- },
605
- 6e4
606
- // 60 seconds for sync operations
607
- );
608
- const wrapped = await handleResponse(response);
609
- return wrapped.data;
610
- }
611
- async function executeSync(accessToken, repoFullName, options) {
612
- const [owner, repo] = repoFullName.split("/");
613
- const response = await fetchWithTimeout(
614
- `${API_BASE_URL}/v1/integrations/vaults/${owner}/${repo}/sync`,
615
- {
616
- method: "POST",
617
- headers: {
618
- "Content-Type": "application/json",
619
- "User-Agent": USER_AGENT,
620
- Authorization: `Bearer ${accessToken}`
621
- },
622
- body: JSON.stringify({
623
- connectionId: options.connectionId,
624
- projectId: options.projectId,
625
- serviceId: options.serviceId,
626
- keywayEnvironment: options.keywayEnvironment || "production",
627
- providerEnvironment: options.providerEnvironment || "production",
628
- direction: options.direction || "push",
629
- allowDelete: options.allowDelete || false
630
- })
631
- },
632
- 12e4
633
- // 2 minutes for sync execution
634
- );
635
- const wrapped = await handleResponse(response);
636
- return wrapped.data;
637
- }
638
- async function connectWithToken(accessToken, provider, providerToken) {
639
- const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations/${provider}/connect`, {
640
- method: "POST",
641
- headers: {
642
- "Content-Type": "application/json",
643
- "User-Agent": USER_AGENT,
644
- Authorization: `Bearer ${accessToken}`
645
- },
646
- body: JSON.stringify({ token: providerToken })
647
- });
648
- const wrapped = await handleResponse(response);
649
- return wrapped.data;
650
- }
651
- async function checkVaultExists(accessToken, repoFullName) {
652
- const [owner, repo] = repoFullName.split("/");
653
- try {
654
- const response = await fetchWithTimeout(
655
- `${API_BASE_URL}/v1/vaults/${owner}/${repo}`,
656
- {
657
- method: "GET",
658
- headers: {
659
- "User-Agent": USER_AGENT,
660
- Authorization: `Bearer ${accessToken}`
661
- }
662
- }
663
- );
664
- return response.ok;
665
- } catch {
666
- return false;
667
- }
668
- }
669
- async function getVaultEnvironments(accessToken, repoFullName) {
670
- const [owner, repo] = repoFullName.split("/");
671
- try {
672
- const response = await fetchWithTimeout(
673
- `${API_BASE_URL}/v1/vaults/${owner}/${repo}`,
674
- {
675
- method: "GET",
676
- headers: {
677
- "User-Agent": USER_AGENT,
678
- Authorization: `Bearer ${accessToken}`
679
- }
680
- }
681
- );
682
- const wrapped = await handleResponse(response);
683
- return wrapped.data.environments || ["production"];
684
- } catch {
685
- return ["production"];
686
- }
687
- }
688
- async function checkGitHubAppInstallation(repoOwner, repoName, accessToken) {
689
- const response = await fetchWithTimeout(`${API_BASE_URL}/v1/github/check-installation`, {
690
- method: "POST",
691
- headers: {
692
- "Content-Type": "application/json",
693
- "User-Agent": USER_AGENT,
694
- Authorization: `Bearer ${accessToken}`
695
- },
696
- body: JSON.stringify({ repoOwner, repoName })
697
- });
698
- const wrapped = await handleResponse(response);
699
- return wrapped.data;
700
- }
701
-
702
- // src/utils/analytics.ts
703
- import { PostHog } from "posthog-node";
704
- import crypto from "crypto";
705
- import path2 from "path";
706
- import os from "os";
707
- import fs2 from "fs";
708
- var posthog = null;
709
- var distinctId = null;
710
- var CONFIG_DIR = path2.join(os.homedir(), ".config", "keyway");
711
- var ID_FILE = path2.join(CONFIG_DIR, "id.json");
712
- var TELEMETRY_DISABLED = process.env.KEYWAY_DISABLE_TELEMETRY === "1";
713
- var CI = process.env.CI === "true" || process.env.CI === "1";
714
- function getDistinctId() {
715
- if (distinctId) return distinctId;
716
- try {
717
- if (!fs2.existsSync(CONFIG_DIR)) {
718
- fs2.mkdirSync(CONFIG_DIR, { recursive: true });
719
- }
720
- if (fs2.existsSync(ID_FILE)) {
721
- const content = fs2.readFileSync(ID_FILE, "utf-8");
722
- const config2 = JSON.parse(content);
723
- distinctId = config2.distinctId;
724
- return distinctId;
725
- }
726
- distinctId = crypto.randomUUID();
727
- const config = { distinctId };
728
- fs2.writeFileSync(ID_FILE, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
729
- try {
730
- fs2.chmodSync(ID_FILE, 384);
731
- } catch {
732
- }
733
- return distinctId;
734
- } catch (error2) {
735
- console.warn("Failed to persist distinct ID, using session-based ID");
736
- distinctId = `session-${crypto.randomUUID()}`;
737
- return distinctId;
738
- }
739
- }
740
- function initPostHog() {
741
- if (posthog) return;
742
- if (TELEMETRY_DISABLED) return;
743
- const apiKey = process.env.KEYWAY_POSTHOG_KEY || INTERNAL_POSTHOG_KEY;
744
- if (!apiKey) return;
745
- posthog = new PostHog(apiKey, {
746
- host: process.env.KEYWAY_POSTHOG_HOST || INTERNAL_POSTHOG_HOST
747
- });
748
- }
749
- function trackEvent(event, properties) {
750
- try {
751
- if (TELEMETRY_DISABLED) return;
752
- if (!posthog) initPostHog();
753
- if (!posthog) return;
754
- const id = getDistinctId();
755
- const sanitizedProperties = properties ? sanitizeProperties(properties) : {};
756
- posthog.capture({
757
- distinctId: id,
758
- event,
759
- properties: {
760
- ...sanitizedProperties,
761
- source: "cli",
762
- platform: process.platform,
763
- nodeVersion: process.version,
764
- version: package_default.version,
765
- ci: CI
766
- }
767
- });
768
- } catch (error2) {
769
- console.debug("Analytics error:", error2);
770
- }
771
- }
772
- function sanitizeProperties(properties) {
773
- const sanitized = {};
774
- for (const [key, value2] of Object.entries(properties)) {
775
- if (key.toLowerCase().includes("secret") || key.toLowerCase().includes("token") || key.toLowerCase().includes("password") || key.toLowerCase().includes("content") || key.toLowerCase().includes("key") || key.toLowerCase().includes("value")) {
776
- continue;
777
- }
778
- if (value2 && typeof value2 === "string" && value2.length > 500) {
779
- sanitized[key] = `${value2.slice(0, 200)}...`;
780
- continue;
781
- }
782
- sanitized[key] = value2;
783
- }
784
- return sanitized;
785
- }
786
- async function shutdownAnalytics() {
787
- if (posthog) {
788
- await posthog.shutdown();
789
- }
790
- }
791
- function identifyUser(userId, properties) {
792
- try {
793
- if (TELEMETRY_DISABLED) return;
794
- if (!posthog) initPostHog();
795
- if (!posthog) return;
796
- const sanitizedProperties = properties ? sanitizeProperties(properties) : {};
797
- posthog.identify({
798
- distinctId: userId,
799
- properties: {
800
- ...sanitizedProperties,
801
- source: "cli"
802
- }
803
- });
804
- const anonId = getDistinctId();
805
- if (anonId && anonId !== userId) {
806
- posthog.alias({
807
- distinctId: userId,
808
- alias: anonId
809
- });
810
- }
811
- } catch (error2) {
812
- console.debug("Analytics identify error:", error2);
813
- }
814
- }
815
- var AnalyticsEvents = {
816
- CLI_INIT: "cli_init",
817
- CLI_PUSH: "cli_push",
818
- CLI_PULL: "cli_pull",
819
- CLI_ERROR: "cli_error",
820
- CLI_LOGIN: "cli_login",
821
- CLI_DOCTOR: "cli_doctor",
822
- CLI_CONNECT: "cli_connect",
823
- CLI_DISCONNECT: "cli_disconnect",
824
- CLI_SYNC: "cli_sync",
825
- CLI_FEEDBACK: "cli_feedback"
826
- };
827
-
828
- // src/cmds/readme.ts
829
- import fs3 from "fs";
830
- import path3 from "path";
831
- import balanced from "balanced-match";
832
- function generateBadge(repo) {
833
- return `[![Keyway Secrets](https://www.keyway.sh/badge.svg?repo=${repo})](https://www.keyway.sh/vaults/${repo})`;
834
- }
835
- var BADGE_PREFIX = /\[!\[[^\]]*\]\([^)]*\)\]\(/g;
836
- var H1_PATTERN = /^#\s+/;
837
- var CODE_FENCE = /^```/;
838
- function findLastBadgeEnd(line) {
839
- let lastEnd = -1;
840
- let match;
841
- BADGE_PREFIX.lastIndex = 0;
842
- while ((match = BADGE_PREFIX.exec(line)) !== null) {
843
- const prefixEnd = match.index + match[0].length - 1;
844
- const remainder = line.substring(prefixEnd);
845
- const balancedMatch = balanced("(", ")", remainder);
846
- if (balancedMatch) {
847
- lastEnd = prefixEnd + balancedMatch.end + 1;
848
- }
849
- }
850
- return lastEnd;
851
- }
852
- function insertBadgeIntoReadme(readmeContent, badge) {
853
- if (readmeContent.includes("keyway.sh/badge.svg")) {
854
- return readmeContent;
855
- }
856
- const lines = readmeContent.split(/\r?\n/);
857
- let inCodeBlock = false;
858
- let inHtmlComment = false;
859
- let lastBadgeLine = -1;
860
- let lastBadgeEndIndex = -1;
861
- let firstH1Line = -1;
862
- for (let i = 0; i < lines.length; i++) {
863
- const line = lines[i];
864
- const trimmed = line.trim();
865
- if (CODE_FENCE.test(trimmed)) {
866
- inCodeBlock = !inCodeBlock;
867
- continue;
868
- }
869
- if (inCodeBlock) continue;
870
- if (trimmed.includes("<!--")) inHtmlComment = true;
871
- if (trimmed.includes("-->")) {
872
- inHtmlComment = false;
873
- continue;
874
- }
875
- if (inHtmlComment) continue;
876
- BADGE_PREFIX.lastIndex = 0;
877
- if (BADGE_PREFIX.test(line)) {
878
- lastBadgeLine = i;
879
- lastBadgeEndIndex = findLastBadgeEnd(line);
880
- }
881
- if (firstH1Line === -1 && H1_PATTERN.test(line)) {
882
- firstH1Line = i;
883
- }
884
- }
885
- if (lastBadgeLine >= 0 && lastBadgeEndIndex > 0) {
886
- const line = lines[lastBadgeLine];
887
- lines[lastBadgeLine] = line.slice(0, lastBadgeEndIndex) + " " + badge + line.slice(lastBadgeEndIndex);
888
- return lines.join("\n");
889
- }
890
- if (firstH1Line >= 0) {
891
- const before = lines.slice(0, firstH1Line + 1);
892
- const after = lines.slice(firstH1Line + 1);
893
- while (after.length > 0 && after[0].trim() === "") {
894
- after.shift();
895
- }
896
- if (after.length > 0) {
897
- return [...before, "", badge, "", ...after].join("\n");
898
- } else {
899
- return [...before, "", badge, ""].join("\n");
900
- }
901
- }
902
- return `${badge}
903
-
904
- ${readmeContent}`;
905
- }
906
- function findReadmePath(cwd) {
907
- const candidates = ["README.md", "readme.md", "Readme.md"];
908
- for (const candidate of candidates) {
909
- const candidatePath = path3.join(cwd, candidate);
910
- if (fs3.existsSync(candidatePath)) {
911
- return candidatePath;
912
- }
913
- }
914
- return null;
915
- }
916
- async function ensureReadme(repoName, cwd) {
917
- const existing = findReadmePath(cwd);
918
- if (existing) return existing;
919
- const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
920
- if (!isInteractive2) {
921
- warn('No README found. Run "keyway readme add-badge" from a repo with a README.');
922
- return null;
923
- }
924
- const confirm3 = await confirm2({
925
- message: "No README found. Create a default README.md?",
926
- initialValue: false
927
- });
928
- if (!confirm3) {
929
- warn("Skipping badge insertion (no README).");
930
- return null;
931
- }
932
- const defaultPath = path3.join(cwd, "README.md");
933
- const content = `# ${repoName}
934
-
935
- `;
936
- fs3.writeFileSync(defaultPath, content, "utf-8");
937
- return defaultPath;
938
- }
939
- async function addBadgeToReadme(silent = false) {
940
- const repo = detectGitRepo();
941
- if (!repo) {
942
- throw new Error("This directory is not a Git repository.");
943
- }
944
- const cwd = process.cwd();
945
- const readmePath = await ensureReadme(repo, cwd);
946
- if (!readmePath) return false;
947
- const badge = generateBadge(repo);
948
- const content = fs3.readFileSync(readmePath, "utf-8");
949
- const updated = insertBadgeIntoReadme(content, badge);
950
- if (updated === content) {
951
- if (!silent) {
952
- info("Keyway badge already present in README.");
953
- }
954
- return false;
955
- }
956
- fs3.writeFileSync(readmePath, updated, "utf-8");
957
- if (!silent) {
958
- success(`Keyway badge added to ${path3.basename(readmePath)}`);
959
- }
960
- return true;
961
- }
962
-
963
- // src/cmds/push.ts
964
- import pc3 from "picocolors";
965
- import fs5 from "fs";
966
- import path5 from "path";
967
-
968
- // src/cmds/login.ts
969
- import pc2 from "picocolors";
970
- import * as p2 from "@clack/prompts";
971
-
972
- // src/utils/helpers.ts
973
- import open from "open";
974
- function sleep(ms) {
975
- return new Promise((resolve) => setTimeout(resolve, ms));
976
- }
977
- async function openUrl(url) {
978
- message(dim(`Open this URL in your browser:
979
- ${url}`));
980
- await open(url).catch(() => {
981
- });
982
- }
983
- function isInteractive() {
984
- return Boolean(process.stdout.isTTY && process.stdin.isTTY && !process.env.CI);
985
- }
986
- function showUpgradePrompt(message2, upgradeUrl) {
987
- note2(`${message2}
988
-
989
- Upgrade: ${link(upgradeUrl)}`, "Plan Limit Reached");
990
- }
991
- var MAX_CONSECUTIVE_ERRORS = 5;
992
-
993
- // src/cmds/login.ts
994
- async function runLoginFlow() {
995
- const repoName = detectGitRepo();
996
- const start = await startDeviceLogin(repoName);
997
- const verifyUrl = start.verificationUriComplete || start.verificationUri;
998
- if (!verifyUrl) {
999
- throw new Error("Missing verification URL from the auth server.");
1000
- }
1001
- step(`Code: ${pc2.bold(pc2.green(start.userCode))}`);
1002
- await openUrl(verifyUrl);
1003
- const s = spinner2();
1004
- s.start("Waiting for authorization...");
1005
- const pollIntervalMs = (start.interval ?? 5) * 1e3;
1006
- const maxTimeoutMs = Math.min((start.expiresIn ?? 900) * 1e3, 30 * 60 * 1e3);
1007
- const startTime = Date.now();
1008
- let consecutiveErrors = 0;
1009
- while (true) {
1010
- if (Date.now() - startTime > maxTimeoutMs) {
1011
- s.stop("Login timed out");
1012
- throw new Error('Login timed out. Please run "keyway login" again.');
1013
- }
1014
- await sleep(pollIntervalMs);
1015
- try {
1016
- const result = await pollDeviceLogin(start.deviceCode);
1017
- consecutiveErrors = 0;
1018
- if (result.status === "pending") {
1019
- continue;
1020
- }
1021
- if (result.status === "approved" && result.keywayToken) {
1022
- await saveAuthToken(result.keywayToken, {
1023
- githubLogin: result.githubLogin,
1024
- expiresAt: result.expiresAt
1025
- });
1026
- trackEvent(AnalyticsEvents.CLI_LOGIN, {
1027
- method: "device",
1028
- repo: repoName
1029
- });
1030
- if (result.githubLogin) {
1031
- identifyUser(result.githubLogin, {
1032
- github_username: result.githubLogin,
1033
- login_method: "device"
1034
- });
1035
- }
1036
- s.stop("Authorized");
1037
- success(`Logged in as ${value(`@${result.githubLogin}`)}`);
1038
- return result.keywayToken;
1039
- }
1040
- s.stop("Authorization failed");
1041
- throw new Error(result.message || "Authentication failed");
1042
- } catch (error2) {
1043
- consecutiveErrors++;
1044
- if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
1045
- const errorMsg = error2 instanceof Error ? error2.message : "Unknown error";
1046
- s.stop("Login failed");
1047
- throw new Error(`Login failed after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${errorMsg}`);
1048
- }
1049
- }
1050
- }
1051
- }
1052
- async function ensureLogin(options = {}) {
1053
- const envToken = process.env.KEYWAY_TOKEN;
1054
- if (envToken) {
1055
- return envToken;
1056
- }
1057
- if (process.env.GITHUB_TOKEN && !process.env.KEYWAY_TOKEN) {
1058
- warn("GITHUB_TOKEN found but not used. Set KEYWAY_TOKEN for Keyway authentication.");
1059
- }
1060
- const stored = await getStoredAuth();
1061
- if (stored?.keywayToken) {
1062
- return stored.keywayToken;
1063
- }
1064
- const allowPrompt = options.allowPrompt !== false;
1065
- const canPrompt = allowPrompt && isInteractive();
1066
- if (!canPrompt) {
1067
- throw new Error('No Keyway session found. Run "keyway login" to authenticate.');
1068
- }
1069
- const proceed = await confirm2({
1070
- message: "No Keyway session found. Open browser to sign in?",
1071
- initialValue: true
1072
- });
1073
- if (!proceed) {
1074
- throw new Error("Login required. Aborting.");
1075
- }
1076
- return runLoginFlow();
1077
- }
1078
- async function runTokenLogin() {
1079
- const repoName = detectGitRepo();
1080
- if (repoName) {
1081
- step(`Detected repository: ${value(repoName)}`);
1082
- }
1083
- const description = repoName ? `Keyway CLI for ${repoName}` : "Keyway CLI";
1084
- const url = `https://github.com/settings/personal-access-tokens/new?description=${encodeURIComponent(description)}`;
1085
- await openUrl(url);
1086
- info("Select the detected repo (or scope manually).");
1087
- message(dim("Permissions: Metadata \u2192 Read-only; Account permissions: None."));
1088
- const token = await p2.password({
1089
- message: "Paste your GitHub PAT:",
1090
- validate: (value2) => {
1091
- if (!value2 || typeof value2 !== "string") return "Token is required";
1092
- if (!value2.startsWith("github_pat_")) return "Token must start with github_pat_";
1093
- return void 0;
1094
- }
1095
- });
1096
- if (p2.isCancel(token)) {
1097
- cancel2("Login cancelled.");
1098
- }
1099
- if (!token || typeof token !== "string") {
1100
- throw new Error("Token is required.");
1101
- }
1102
- const trimmedToken = token.trim();
1103
- if (!trimmedToken.startsWith("github_pat_")) {
1104
- throw new Error("Token must start with github_pat_.");
1105
- }
1106
- const s = spinner2();
1107
- s.start("Validating token...");
1108
- const validation = await validateToken(trimmedToken);
1109
- await saveAuthToken(trimmedToken, {
1110
- githubLogin: validation.username
1111
- });
1112
- trackEvent(AnalyticsEvents.CLI_LOGIN, {
1113
- method: "pat",
1114
- repo: repoName
1115
- });
1116
- identifyUser(validation.username, {
1117
- github_username: validation.username,
1118
- login_method: "pat"
1119
- });
1120
- s.stop("Token validated");
1121
- success(`Logged in as ${value(`@${validation.username}`)}`);
1122
- return trimmedToken;
1123
- }
1124
- async function loginCommand(options = {}) {
1125
- intro2("login");
1126
- try {
1127
- if (options.token) {
1128
- await runTokenLogin();
1129
- } else {
1130
- await runLoginFlow();
1131
- }
1132
- outro2("Ready to sync secrets!");
1133
- } catch (error2) {
1134
- const message2 = error2 instanceof Error ? error2.message : "Unexpected login error";
1135
- trackEvent(AnalyticsEvents.CLI_ERROR, {
1136
- command: "login",
1137
- error: truncateMessage(message2)
1138
- });
1139
- error(message2);
1140
- process.exit(1);
1141
- }
1142
- }
1143
- async function logoutCommand() {
1144
- intro2("logout");
1145
- clearAuth();
1146
- success("Logged out of Keyway");
1147
- outro2(dim(`Auth cache cleared: ${getAuthFilePath()}`));
1148
- }
1149
-
1150
- // src/utils/env.ts
1151
- import fs4 from "fs";
1152
- import path4 from "path";
1153
- async function promptCreateEnvFile() {
1154
- const createEnv = await confirm2({
1155
- message: "No .env file found. Create one?",
1156
- initialValue: true
1157
- });
1158
- if (!createEnv) {
1159
- return false;
1160
- }
1161
- const envFilePath = path4.join(process.cwd(), ".env");
1162
- fs4.writeFileSync(envFilePath, "# Add your environment variables here\n# Example: API_KEY=your-api-key\n");
1163
- success("Created .env file");
1164
- return true;
1165
- }
1166
-
1167
- // src/cmds/push.ts
1168
- function deriveEnvFromFile(file2) {
1169
- const base = path5.basename(file2);
1170
- const match = base.match(/\.env(?:\.(.+))?$/);
1171
- if (match) {
1172
- return match[1] || "development";
1173
- }
1174
- return "development";
1175
- }
1176
- function discoverEnvCandidates(cwd) {
1177
- try {
1178
- const entries = fs5.readdirSync(cwd);
1179
- const hasEnvLocal = entries.includes(".env.local");
1180
- if (hasEnvLocal) {
1181
- info("Detected .env.local \u2014 not synced by design (machine-specific secrets)");
1182
- }
1183
- const candidates = entries.filter((name) => name.startsWith(".env") && name !== ".env.local").map((name) => {
1184
- const fullPath = path5.join(cwd, name);
1185
- try {
1186
- const stat = fs5.statSync(fullPath);
1187
- if (!stat.isFile()) return null;
1188
- return { file: name, env: deriveEnvFromFile(name) };
1189
- } catch {
1190
- return null;
1191
- }
1192
- }).filter((c) => Boolean(c));
1193
- const seen = /* @__PURE__ */ new Set();
1194
- const unique = [];
1195
- for (const c of candidates) {
1196
- if (seen.has(c.file)) continue;
1197
- seen.add(c.file);
1198
- unique.push(c);
1199
- }
1200
- return unique;
1201
- } catch {
1202
- return [];
1203
- }
1204
- }
1205
- async function pushCommand(options) {
1206
- intro2("push");
1207
- await warnIfEnvNotGitignored();
1208
- try {
1209
- const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
1210
- let environment = options.env;
1211
- let envFile = options.file;
1212
- const candidates = discoverEnvCandidates(process.cwd());
1213
- if (candidates.length === 0 && !envFile) {
1214
- if (!isInteractive2) {
1215
- throw new Error("No .env file found. Create a .env file first, or use --file <path> to specify one.");
1216
- }
1217
- const created = await promptCreateEnvFile();
1218
- if (!created) {
1219
- throw new Error("No .env file found.");
1220
- }
1221
- message(dim("Add your variables and run keyway push again"));
1222
- return;
1223
- }
1224
- if (environment && !envFile) {
1225
- const match = candidates.find((c) => c.env === environment);
1226
- if (match) {
1227
- envFile = match.file;
1228
- }
1229
- }
1230
- if (!environment && !envFile && isInteractive2 && candidates.length > 0) {
1231
- const choice = await select2({
1232
- message: "Select an env file to push:",
1233
- options: [
1234
- ...candidates.map((c) => ({
1235
- label: `${c.file} (env: ${c.env})`,
1236
- value: c.file
1237
- })),
1238
- { label: "Enter a different file...", value: "__custom__" }
1239
- ]
1240
- });
1241
- if (choice && choice !== "__custom__") {
1242
- envFile = choice;
1243
- const matched = candidates.find((c) => c.file === envFile);
1244
- environment = matched?.env;
1245
- } else if (choice === "__custom__") {
1246
- const fileInput = await text2({
1247
- message: "Path to env file:",
1248
- validate: (value2) => {
1249
- if (!value2) return "Path is required";
1250
- const resolved = path5.resolve(process.cwd(), value2);
1251
- if (!fs5.existsSync(resolved)) return `File not found: ${value2}`;
1252
- return void 0;
1253
- }
1254
- });
1255
- envFile = fileInput;
1256
- environment = deriveEnvFromFile(fileInput);
1257
- }
1258
- }
1259
- if (!environment) {
1260
- environment = "development";
1261
- }
1262
- if (!envFile) {
1263
- envFile = ".env";
1264
- }
1265
- const envFilePath = path5.resolve(process.cwd(), envFile);
1266
- if (!fs5.existsSync(envFilePath)) {
1267
- throw new Error(`File not found: ${envFile}`);
1268
- }
1269
- const content = fs5.readFileSync(envFilePath, "utf-8");
1270
- if (content.trim().length === 0) {
1271
- throw new Error(`File is empty: ${envFile}`);
1272
- }
1273
- const lines = content.split("\n").filter((line) => {
1274
- const trimmed = line.trim();
1275
- return trimmed.length > 0 && !trimmed.startsWith("#");
1276
- });
1277
- step(`File: ${file(envFile)}`);
1278
- step(`Environment: ${value(environment)}`);
1279
- step(`Variables: ${value(lines.length)}`);
1280
- const repoFullName = getCurrentRepoFullName();
1281
- step(`Repository: ${value(repoFullName)}`);
1282
- if (!options.yes) {
1283
- if (!isInteractive2) {
1284
- throw new Error("Confirmation required. Re-run with --yes in non-interactive environments.");
1285
- }
1286
- const confirm3 = await confirm2({
1287
- message: `Push ${lines.length} secrets from ${envFile} to ${repoFullName}?`,
1288
- initialValue: true
1289
- });
1290
- if (!confirm3) {
1291
- warn("Push aborted.");
1292
- return;
1293
- }
1294
- }
1295
- const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
1296
- trackEvent(AnalyticsEvents.CLI_PUSH, {
1297
- repoFullName,
1298
- environment,
1299
- variableCount: lines.length
1300
- });
1301
- const s = spinner2();
1302
- s.start("Uploading secrets...");
1303
- const response = await pushSecrets(repoFullName, environment, content, accessToken);
1304
- s.stop("Uploaded");
1305
- success(response.message);
1306
- if (response.stats) {
1307
- const { created, updated, deleted } = response.stats;
1308
- const parts = [];
1309
- if (created > 0) parts.push(pc3.green(`+${created} created`));
1310
- if (updated > 0) parts.push(pc3.yellow(`~${updated} updated`));
1311
- if (deleted > 0) parts.push(pc3.red(`-${deleted} deleted`));
1312
- if (parts.length > 0) {
1313
- message(`Stats: ${parts.join(", ")}`);
1314
- }
1315
- }
1316
- const dashboardLink = `https://www.keyway.sh/dashboard/vaults/${repoFullName}`;
1317
- outro2(`Dashboard: ${link(dashboardLink)}`);
1318
- await shutdownAnalytics();
1319
- } catch (error2) {
1320
- let message2;
1321
- let hint = null;
1322
- if (error2 instanceof APIError) {
1323
- message2 = error2.message || `HTTP ${error2.statusCode} - ${error2.error}`;
1324
- const envNotFoundMatch = message2.match(/Environment '([^']+)' does not exist.*Available environments: ([^.]+)/);
1325
- if (envNotFoundMatch) {
1326
- const requestedEnv = envNotFoundMatch[1];
1327
- const availableEnvs = envNotFoundMatch[2];
1328
- message2 = `Environment '${requestedEnv}' does not exist in this vault.`;
1329
- hint = `Available environments: ${availableEnvs}
1330
- Use ${command(`keyway push --env <environment>`)} to specify one.`;
1331
- }
1332
- if (error2.statusCode === 403 && (error2.upgradeUrl || message2.toLowerCase().includes("read-only"))) {
1333
- const upgradeMessage = message2.toLowerCase().includes("read-only") ? "This vault is read-only on your current plan." : message2;
1334
- const upgradeUrl = error2.upgradeUrl || "https://keyway.sh/settings";
1335
- trackEvent(AnalyticsEvents.CLI_ERROR, {
1336
- command: "push",
1337
- error: upgradeMessage
1338
- });
1339
- await shutdownAnalytics();
1340
- showUpgradePrompt(upgradeMessage, upgradeUrl);
1341
- process.exit(1);
1342
- }
1343
- } else if (error2 instanceof Error) {
1344
- message2 = truncateMessage(error2.message);
1345
- } else {
1346
- message2 = "Unknown error";
1347
- }
1348
- trackEvent(AnalyticsEvents.CLI_ERROR, {
1349
- command: "push",
1350
- error: message2
1351
- });
1352
- await shutdownAnalytics();
1353
- error(message2);
1354
- if (hint) {
1355
- message(dim(hint));
1356
- }
1357
- process.exit(1);
1358
- }
1359
- }
1360
-
1361
- // src/cmds/init.ts
1362
- var DASHBOARD_URL = "https://www.keyway.sh/dashboard/vaults";
1363
- var POLL_INTERVAL_MS = 3e3;
1364
- var POLL_TIMEOUT_MS = 12e4;
1365
- async function ensureLoginAndGitHubApp(repoFullName, options = {}) {
1366
- const [repoOwner, repoName] = repoFullName.split("/");
1367
- const envToken = process.env.KEYWAY_TOKEN;
1368
- if (envToken) {
1369
- const result = await ensureGitHubAppInstalledOnly(repoFullName, envToken);
1370
- if (result === null) {
1371
- throw new Error("KEYWAY_TOKEN is invalid or expired. Please update the token.");
1372
- }
1373
- return result;
1374
- }
1375
- const stored = await getStoredAuth();
1376
- if (stored?.keywayToken) {
1377
- const result = await ensureGitHubAppInstalledOnly(repoFullName, stored.keywayToken);
1378
- if (result !== null) {
1379
- return result;
1380
- }
1381
- }
1382
- const allowPrompt = options.allowPrompt !== false;
1383
- if (!allowPrompt || !isInteractive()) {
1384
- throw new Error('No Keyway session found. Run "keyway login" to authenticate.');
1385
- }
1386
- const deviceStart = await startDeviceLogin(repoFullName);
1387
- const installUrl = deviceStart.githubAppInstallUrl || "https://github.com/apps/keyway/installations/new";
1388
- const shouldProceed = await confirm2({
1389
- message: "Open browser to sign in?",
1390
- initialValue: true
1391
- });
1392
- if (!shouldProceed) {
1393
- throw new Error('Setup required. Run "keyway init" when ready.');
1394
- }
1395
- await openUrl(deviceStart.verificationUriComplete);
1396
- const loginSpinner = spinner2();
1397
- loginSpinner.start("Waiting for authorization...");
1398
- const pollIntervalMs = Math.max((deviceStart.interval ?? 5) * 1e3, POLL_INTERVAL_MS);
1399
- const startTime = Date.now();
1400
- let accessToken = null;
1401
- let consecutiveErrors = 0;
1402
- while (Date.now() - startTime < POLL_TIMEOUT_MS) {
1403
- await sleep(pollIntervalMs);
1404
- try {
1405
- const result = await pollDeviceLogin(deviceStart.deviceCode);
1406
- if (result.status === "approved" && result.keywayToken) {
1407
- accessToken = result.keywayToken;
1408
- await saveAuthToken(result.keywayToken, {
1409
- githubLogin: result.githubLogin,
1410
- expiresAt: result.expiresAt
1411
- });
1412
- loginSpinner.stop("Signed in!");
1413
- if (result.githubLogin) {
1414
- identifyUser(result.githubLogin, {
1415
- github_username: result.githubLogin,
1416
- login_method: "device_flow"
1417
- });
1418
- }
1419
- break;
1420
- }
1421
- consecutiveErrors = 0;
1422
- } catch (error2) {
1423
- consecutiveErrors++;
1424
- if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
1425
- loginSpinner.stop("Failed");
1426
- const errorMsg = error2 instanceof Error ? error2.message : "Unknown error";
1427
- throw new Error(`Login failed after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${errorMsg}`);
1428
- }
1429
- }
1430
- }
1431
- if (!accessToken) {
1432
- loginSpinner.stop("Timeout");
1433
- warn("Timed out waiting for sign in.");
1434
- throw new Error("Sign in timed out. Please try again.");
1435
- }
1436
- const installStatus = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
1437
- if (installStatus.installed) {
1438
- success("GitHub App installed");
1439
- return accessToken;
1440
- }
1441
- warn("GitHub App not installed on this repository");
1442
- message(dim("The Keyway GitHub App is required for secure access."));
1443
- const shouldInstall = await confirm2({
1444
- message: "Open browser to install GitHub App?",
1445
- initialValue: true
1446
- });
1447
- if (!shouldInstall) {
1448
- message(dim(`Install later: ${installUrl}`));
1449
- throw new Error("GitHub App installation required.");
1450
- }
1451
- await openUrl(installUrl);
1452
- const installSpinner = spinner2();
1453
- installSpinner.start("Waiting for GitHub App installation...");
1454
- const installStartTime = Date.now();
1455
- consecutiveErrors = 0;
1456
- while (Date.now() - installStartTime < POLL_TIMEOUT_MS) {
1457
- await sleep(POLL_INTERVAL_MS);
1458
- try {
1459
- const pollStatus = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
1460
- if (pollStatus.installed) {
1461
- installSpinner.stop("GitHub App installed!");
1462
- return accessToken;
1463
- }
1464
- consecutiveErrors = 0;
1465
- } catch (error2) {
1466
- consecutiveErrors++;
1467
- if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
1468
- installSpinner.stop("Failed");
1469
- const errorMsg = error2 instanceof Error ? error2.message : "Unknown error";
1470
- throw new Error(`Installation check failed after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${errorMsg}`);
1471
- }
1472
- }
1473
- }
1474
- installSpinner.stop("Timeout");
1475
- warn("Timed out waiting for installation.");
1476
- message(dim(`Install the GitHub App: ${installUrl}`));
1477
- throw new Error("GitHub App installation timed out.");
1478
- }
1479
- async function ensureGitHubAppInstalledOnly(repoFullName, accessToken) {
1480
- const [repoOwner, repoName] = repoFullName.split("/");
1481
- let status;
1482
- try {
1483
- status = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
1484
- } catch (error2) {
1485
- if (error2 instanceof APIError && error2.statusCode === 401) {
1486
- warn("Session expired or invalid. Clearing credentials...");
1487
- const { clearAuth: clearAuth2 } = await import("./auth-64V3RWUK.js");
1488
- clearAuth2();
1489
- return null;
1490
- }
1491
- throw error2;
1492
- }
1493
- if (status.installed) {
1494
- return accessToken;
1495
- }
1496
- warn("GitHub App not installed for this repository");
1497
- message(dim("The Keyway GitHub App is required to securely manage secrets."));
1498
- message(dim("It only requests minimal permissions (repository metadata)."));
1499
- if (!isInteractive()) {
1500
- message(dim(`Install the Keyway GitHub App: ${status.installUrl}`));
1501
- throw new Error("GitHub App installation required.");
1502
- }
1503
- const shouldInstall = await confirm2({
1504
- message: "Open browser to install Keyway GitHub App?",
1505
- initialValue: true
1506
- });
1507
- if (!shouldInstall) {
1508
- message(dim(`You can install later: ${status.installUrl}`));
1509
- throw new Error("GitHub App installation required.");
1510
- }
1511
- await openUrl(status.installUrl);
1512
- const s = spinner2();
1513
- s.start("Waiting for GitHub App installation...");
1514
- const startTime = Date.now();
1515
- let consecutiveErrors = 0;
1516
- while (Date.now() - startTime < POLL_TIMEOUT_MS) {
1517
- await sleep(POLL_INTERVAL_MS);
1518
- try {
1519
- const pollStatus = await checkGitHubAppInstallation(repoOwner, repoName, accessToken);
1520
- if (pollStatus.installed) {
1521
- s.stop("GitHub App installed!");
1522
- return accessToken;
1523
- }
1524
- consecutiveErrors = 0;
1525
- } catch (error2) {
1526
- consecutiveErrors++;
1527
- if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
1528
- s.stop("Failed");
1529
- const errorMsg = error2 instanceof Error ? error2.message : "Unknown error";
1530
- throw new Error(`Installation check failed after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${errorMsg}`);
1531
- }
1532
- }
1533
- }
1534
- s.stop("Timeout");
1535
- warn("Timed out waiting for installation.");
1536
- message(dim(`You can install the GitHub App later: ${status.installUrl}`));
1537
- throw new Error("GitHub App installation timed out.");
1538
- }
1539
- async function initCommand(options = {}) {
1540
- try {
1541
- const repoFullName = getCurrentRepoFullName();
1542
- const dashboardLink = `${DASHBOARD_URL}/${repoFullName}`;
1543
- intro2("init");
1544
- await warnIfEnvNotGitignored();
1545
- step(`Repository: ${value(repoFullName)}`);
1546
- const accessToken = await ensureLoginAndGitHubApp(repoFullName, {
1547
- allowPrompt: options.loginPrompt !== false
1548
- });
1549
- trackEvent(AnalyticsEvents.CLI_INIT, { repoFullName, githubAppInstalled: true });
1550
- const vaultExists = await checkVaultExists(accessToken, repoFullName);
1551
- if (vaultExists) {
1552
- success("Already initialized!");
1553
- message(dim(`Run ${command("keyway push")} to sync your secrets`));
1554
- outro2(`Dashboard: ${link(dashboardLink)}`);
1555
- await shutdownAnalytics();
1556
- return;
1557
- }
1558
- await initVault(repoFullName, accessToken);
1559
- success("Vault created!");
1560
- try {
1561
- const badgeAdded = await addBadgeToReadme(true);
1562
- if (badgeAdded) {
1563
- success("Badge added to README.md");
1564
- }
1565
- } catch {
1566
- }
1567
- const envCandidates = discoverEnvCandidates(process.cwd());
1568
- const interactive = process.stdin.isTTY && process.stdout.isTTY;
1569
- if (envCandidates.length > 0 && interactive) {
1570
- message(dim(`Found ${envCandidates.length} env file(s): ${envCandidates.map((c) => c.file).join(", ")}`));
1571
- const shouldPush = await confirm2({
1572
- message: "Push secrets now?",
1573
- initialValue: true
1574
- });
1575
- if (shouldPush) {
1576
- await pushCommand({ loginPrompt: false, yes: false });
1577
- return;
1578
- }
1579
- }
1580
- if (envCandidates.length === 0) {
1581
- if (interactive) {
1582
- const created = await promptCreateEnvFile();
1583
- if (created) {
1584
- message(dim(`Add your variables and run ${command("keyway push")}`));
1585
- } else {
1586
- message(dim(`Next: Create ${file(".env")} and run ${command("keyway push")}`));
1587
- }
1588
- } else {
1589
- warn("No .env file found - your vault is empty");
1590
- message(dim(`Next: Create ${file(".env")} and run ${command("keyway push")}`));
1591
- }
1592
- } else {
1593
- message(dim(`Run ${command("keyway push")} to sync your secrets`));
1594
- }
1595
- outro2(`Dashboard: ${link(dashboardLink)}`);
1596
- await shutdownAnalytics();
1597
- } catch (error2) {
1598
- if (error2 instanceof APIError) {
1599
- if (error2.statusCode === 409) {
1600
- success("Already initialized!");
1601
- message(dim(`Run ${command("keyway push")} to sync your secrets`));
1602
- outro2(`Dashboard: ${link(`${DASHBOARD_URL}/${getCurrentRepoFullName()}`)}`);
1603
- await shutdownAnalytics();
1604
- return;
1605
- }
1606
- if (error2.error === "Plan Limit Reached" || error2.upgradeUrl) {
1607
- const upgradeUrl = error2.upgradeUrl || "https://keyway.sh/pricing";
1608
- showUpgradePrompt(error2.message, upgradeUrl);
1609
- await shutdownAnalytics();
1610
- process.exit(1);
1611
- }
1612
- }
1613
- const message2 = error2 instanceof APIError ? error2.message : error2 instanceof Error ? truncateMessage(error2.message) : "Unknown error";
1614
- trackEvent(AnalyticsEvents.CLI_ERROR, {
1615
- command: "init",
1616
- error: message2
1617
- });
1618
- await shutdownAnalytics();
1619
- error(message2);
1620
- process.exit(1);
1621
- }
1622
- }
1623
-
1624
- // src/cmds/pull.ts
1625
- import fs6 from "fs";
1626
- import path6 from "path";
1627
- async function pullCommand(options) {
1628
- intro2("pull");
1629
- await warnIfEnvNotGitignored();
1630
- try {
1631
- const environment = options.env || "development";
1632
- const envFile = options.file || ".env";
1633
- step(`Environment: ${value(environment)}`);
1634
- const repoFullName = getCurrentRepoFullName();
1635
- step(`Repository: ${value(repoFullName)}`);
1636
- const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
1637
- trackEvent(AnalyticsEvents.CLI_PULL, {
1638
- repoFullName,
1639
- environment
1640
- });
1641
- const s = spinner2();
1642
- s.start("Downloading secrets...");
1643
- const response = await pullSecrets(repoFullName, environment, accessToken);
1644
- const envFilePath = path6.resolve(process.cwd(), envFile);
1645
- if (fs6.existsSync(envFilePath)) {
1646
- s.stop("Downloaded");
1647
- const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
1648
- if (options.yes) {
1649
- warn(`Overwriting existing file: ${envFile}`);
1650
- } else if (!isInteractive2) {
1651
- throw new Error(`File ${envFile} exists. Re-run with --yes to overwrite or choose a different --file.`);
1652
- } else {
1653
- const confirm3 = await confirm2({
1654
- message: `${envFile} exists. Overwrite with secrets from ${environment}?`,
1655
- initialValue: false
1656
- });
1657
- if (!confirm3) {
1658
- warn("Pull aborted.");
1659
- return;
1660
- }
1661
- }
1662
- } else {
1663
- s.stop("Downloaded");
1664
- }
1665
- fs6.writeFileSync(envFilePath, response.content, "utf-8");
1666
- const lines = response.content.split("\n").filter((line) => {
1667
- const trimmed = line.trim();
1668
- return trimmed.length > 0 && !trimmed.startsWith("#");
1669
- });
1670
- success(`Secrets downloaded to ${file(envFile)}`);
1671
- message(`Variables: ${value(lines.length)}`);
1672
- outro2("Secrets synced!");
1673
- await shutdownAnalytics();
1674
- } catch (error2) {
1675
- const message2 = error2 instanceof APIError ? `API ${error2.statusCode}: ${error2.message}` : error2 instanceof Error ? truncateMessage(error2.message) : "Unknown error";
1676
- trackEvent(AnalyticsEvents.CLI_ERROR, {
1677
- command: "pull",
1678
- error: message2
1679
- });
1680
- await shutdownAnalytics();
1681
- error(message2);
1682
- process.exit(1);
1683
- }
1684
- }
1685
-
1686
- // src/cmds/doctor.ts
1687
- import pc4 from "picocolors";
1688
-
1689
- // src/core/doctor.ts
1690
- import { execSync as execSync2 } from "child_process";
1691
- import { readFileSync, existsSync } from "fs";
1692
- var API_HEALTH_URL = `${process.env.KEYWAY_API_URL || INTERNAL_API_URL}/v1/health`;
1693
- async function checkAuth() {
1694
- try {
1695
- const auth = await getStoredAuth();
1696
- if (!auth) {
1697
- return {
1698
- id: "auth",
1699
- name: "Authentication",
1700
- status: "warn",
1701
- detail: "Not logged in. Run: keyway login"
1702
- };
1703
- }
1704
- try {
1705
- const result = await validateToken(auth.keywayToken);
1706
- return {
1707
- id: "auth",
1708
- name: "Authentication",
1709
- status: "pass",
1710
- detail: `Logged in as ${result.login || auth.githubLogin || "user"}`
1711
- };
1712
- } catch {
1713
- return {
1714
- id: "auth",
1715
- name: "Authentication",
1716
- status: "warn",
1717
- detail: "Token expired or invalid. Run: keyway login"
1718
- };
1719
- }
1720
- } catch {
1721
- return {
1722
- id: "auth",
1723
- name: "Authentication",
1724
- status: "warn",
1725
- detail: "Unable to check authentication status"
1726
- };
1727
- }
1728
- }
1729
- async function checkGitHubRemote() {
1730
- try {
1731
- try {
1732
- execSync2("git rev-parse --is-inside-work-tree", {
1733
- encoding: "utf-8",
1734
- stdio: ["pipe", "pipe", "ignore"]
1735
- });
1736
- } catch {
1737
- return {
1738
- id: "github",
1739
- name: "GitHub repository",
1740
- status: "warn",
1741
- detail: "Not in a git repository"
1742
- };
1743
- }
1744
- try {
1745
- const remoteUrl = execSync2("git remote get-url origin", {
1746
- encoding: "utf-8",
1747
- stdio: ["pipe", "pipe", "ignore"]
1748
- }).trim();
1749
- const sshMatch = remoteUrl.match(/git@github\.com:(.+)\/(.+?)(\.git)?$/);
1750
- const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/(.+)\/(.+?)(\.git)?$/);
1751
- if (sshMatch || httpsMatch) {
1752
- const match = sshMatch || httpsMatch;
1753
- const repoName = `${match[1]}/${match[2]}`;
1754
- return {
1755
- id: "github",
1756
- name: "GitHub repository",
1757
- status: "pass",
1758
- detail: repoName
1759
- };
1760
- }
1761
- return {
1762
- id: "github",
1763
- name: "GitHub repository",
1764
- status: "warn",
1765
- detail: "Remote is not a GitHub URL"
1766
- };
1767
- } catch {
1768
- return {
1769
- id: "github",
1770
- name: "GitHub repository",
1771
- status: "warn",
1772
- detail: "No remote origin configured"
1773
- };
1774
- }
1775
- } catch {
1776
- return {
1777
- id: "github",
1778
- name: "GitHub repository",
1779
- status: "warn",
1780
- detail: "Unable to detect repository"
1781
- };
1782
- }
1783
- }
1784
- async function checkNetwork() {
1785
- const fetchFn = globalThis.fetch;
1786
- if (!fetchFn) {
1787
- return {
1788
- id: "network",
1789
- name: "API connectivity",
1790
- status: "warn",
1791
- detail: "Fetch API not available in this Node.js runtime"
1792
- };
1793
- }
1794
- try {
1795
- const controller = new AbortController();
1796
- const timeout = setTimeout(() => controller.abort(), 2e3);
1797
- const response = await fetchFn(API_HEALTH_URL, {
1798
- method: "HEAD",
1799
- signal: controller.signal
1800
- });
1801
- clearTimeout(timeout);
1802
- if (response.ok || response.status < 500) {
1803
- return {
1804
- id: "network",
1805
- name: "API connectivity",
1806
- status: "pass",
1807
- detail: `Connected to ${API_HEALTH_URL}`
1808
- };
1809
- }
1810
- return {
1811
- id: "network",
1812
- name: "API connectivity",
1813
- status: "warn",
1814
- detail: `Server returned ${response.status}`
1815
- };
1816
- } catch (error2) {
1817
- if (error2.name === "AbortError") {
1818
- return {
1819
- id: "network",
1820
- name: "API connectivity",
1821
- status: "warn",
1822
- detail: "Connection timeout (>2s)"
1823
- };
1824
- }
1825
- if (error2.code === "ENOTFOUND") {
1826
- return {
1827
- id: "network",
1828
- name: "API connectivity",
1829
- status: "fail",
1830
- detail: "DNS resolution failed"
1831
- };
1832
- }
1833
- if (error2.code === "CERT_HAS_EXPIRED" || error2.code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") {
1834
- return {
1835
- id: "network",
1836
- name: "API connectivity",
1837
- status: "fail",
1838
- detail: "SSL certificate error"
1839
- };
1840
- }
1841
- return {
1842
- id: "network",
1843
- name: "API connectivity",
1844
- status: "warn",
1845
- detail: error2.message || "Connection failed"
1846
- };
1847
- }
1848
- }
1849
- async function checkEnvFile() {
1850
- const envFiles = [".env", ".env.local", ".env.development"];
1851
- const found = [];
1852
- for (const file2 of envFiles) {
1853
- if (existsSync(file2)) {
1854
- found.push(file2);
1855
- }
1856
- }
1857
- if (found.length === 0) {
1858
- return {
1859
- id: "envfile",
1860
- name: "Environment file",
1861
- status: "warn",
1862
- detail: "No .env file found. Run: keyway pull"
1863
- };
1864
- }
1865
- return {
1866
- id: "envfile",
1867
- name: "Environment file",
1868
- status: "pass",
1869
- detail: found.join(", ")
1870
- };
1871
- }
1872
- async function checkSyncs(repoFullName) {
1873
- if (!repoFullName) {
1874
- return {
1875
- id: "syncs",
1876
- name: "Provider syncs",
1877
- status: "warn",
1878
- detail: "Not in a GitHub repository"
1879
- };
1880
- }
1881
- try {
1882
- const auth = await getStoredAuth();
1883
- if (!auth) {
1884
- return {
1885
- id: "syncs",
1886
- name: "Provider syncs",
1887
- status: "warn",
1888
- detail: "Login required to check"
1889
- };
1890
- }
1891
- try {
1892
- const { connections } = await getConnections(auth.keywayToken);
1893
- if (connections.length === 0) {
1894
- return {
1895
- id: "syncs",
1896
- name: "Provider syncs",
1897
- status: "pass",
1898
- detail: "No integrations connected"
1899
- };
1900
- }
1901
- const providers = [...new Set(connections.map((c) => c.provider))];
1902
- const linkedProviders = [];
1903
- const repoLower = repoFullName.toLowerCase();
1904
- for (const provider of providers) {
1905
- try {
1906
- const { projects } = await getAllProviderProjects(auth.keywayToken, provider);
1907
- const hasLinked = projects.some((p5) => p5.linkedRepo?.toLowerCase() === repoLower);
1908
- if (hasLinked) {
1909
- linkedProviders.push(provider);
1910
- }
1911
- } catch {
1912
- }
1913
- }
1914
- if (linkedProviders.length === 0) {
1915
- return {
1916
- id: "syncs",
1917
- name: "Provider syncs",
1918
- status: "pass",
1919
- detail: "None for this repo"
1920
- };
1921
- }
1922
- return {
1923
- id: "syncs",
1924
- name: "Provider syncs",
1925
- status: "pass",
1926
- detail: linkedProviders.join(", ")
1927
- };
1928
- } catch {
1929
- return {
1930
- id: "syncs",
1931
- name: "Provider syncs",
1932
- status: "warn",
1933
- detail: "Unable to check"
1934
- };
1935
- }
1936
- } catch {
1937
- return {
1938
- id: "syncs",
1939
- name: "Provider syncs",
1940
- status: "warn",
1941
- detail: "Unable to check"
1942
- };
1943
- }
1944
- }
1945
- async function checkGitignore() {
1946
- try {
1947
- if (!existsSync(".gitignore")) {
1948
- return {
1949
- id: "gitignore",
1950
- name: ".gitignore configuration",
1951
- status: "warn",
1952
- detail: "No .gitignore file found"
1953
- };
1954
- }
1955
- const gitignoreContent = readFileSync(".gitignore", "utf-8");
1956
- const hasEnvPattern = gitignoreContent.includes("*.env") || gitignoreContent.includes(".env*");
1957
- const hasDotEnv = gitignoreContent.includes(".env");
1958
- if (hasEnvPattern || hasDotEnv) {
1959
- return {
1960
- id: "gitignore",
1961
- name: ".gitignore configuration",
1962
- status: "pass",
1963
- detail: "Environment files are ignored"
1964
- };
1965
- }
1966
- return {
1967
- id: "gitignore",
1968
- name: ".gitignore configuration",
1969
- status: "warn",
1970
- detail: "Missing .env patterns in .gitignore"
1971
- };
1972
- } catch {
1973
- return {
1974
- id: "gitignore",
1975
- name: ".gitignore configuration",
1976
- status: "warn",
1977
- detail: "Could not read .gitignore"
1978
- };
1979
- }
1980
- }
1981
- async function runAllChecks(options = {}) {
1982
- const githubResult = await checkGitHubRemote();
1983
- const repoFullName = githubResult.status === "pass" ? githubResult.detail || null : null;
1984
- const [authResult, networkResult, syncsResult, envFileResult, gitignoreResult] = await Promise.all([
1985
- checkAuth(),
1986
- checkNetwork(),
1987
- checkSyncs(repoFullName),
1988
- checkEnvFile(),
1989
- checkGitignore()
1990
- ]);
1991
- const checks = [authResult, githubResult, networkResult, syncsResult, envFileResult, gitignoreResult];
1992
- if (options.strict) {
1993
- checks.forEach((check) => {
1994
- if (check.status === "warn") {
1995
- check.status = "fail";
1996
- }
1997
- });
1998
- }
1999
- const summary = {
2000
- pass: checks.filter((c) => c.status === "pass").length,
2001
- warn: checks.filter((c) => c.status === "warn").length,
2002
- fail: checks.filter((c) => c.status === "fail").length
2003
- };
2004
- const exitCode = summary.fail > 0 ? 1 : 0;
2005
- return {
2006
- checks,
2007
- summary,
2008
- exitCode
2009
- };
2010
- }
2011
-
2012
- // src/cmds/doctor.ts
2013
- function formatSummary(results) {
2014
- const parts = [
2015
- pc4.green(`${results.summary.pass} passed`),
2016
- results.summary.warn > 0 ? pc4.yellow(`${results.summary.warn} warnings`) : null,
2017
- results.summary.fail > 0 ? pc4.red(`${results.summary.fail} failed`) : null
2018
- ].filter(Boolean);
2019
- return parts.join(", ");
2020
- }
2021
- async function doctorCommand(options = {}) {
2022
- try {
2023
- const results = await runAllChecks({ strict: !!options.strict });
2024
- trackEvent(AnalyticsEvents.CLI_DOCTOR, {
2025
- pass: results.summary.pass,
2026
- warn: results.summary.warn,
2027
- fail: results.summary.fail,
2028
- strict: !!options.strict
2029
- });
2030
- if (options.json) {
2031
- process.stdout.write(JSON.stringify(results, null, 0) + "\n");
2032
- process.exit(results.exitCode);
2033
- }
2034
- intro2("doctor");
2035
- results.checks.forEach((check) => {
2036
- const detail = check.detail ? dim(` \u2014 ${check.detail}`) : "";
2037
- if (check.status === "pass") {
2038
- success(`${check.name}${detail}`);
2039
- } else if (check.status === "warn") {
2040
- warn(`${check.name}${detail}`);
2041
- } else {
2042
- error(`${check.name}${detail}`);
2043
- }
2044
- });
2045
- message(`Summary: ${formatSummary(results)}`);
2046
- if (results.summary.fail > 0) {
2047
- outro2("Some checks failed. Please resolve the issues above.");
2048
- } else if (results.summary.warn > 0) {
2049
- outro2("Some warnings detected. Keyway should work but consider addressing them.");
2050
- } else {
2051
- outro2("All checks passed! Your environment is ready.");
2052
- }
2053
- process.exit(results.exitCode);
2054
- } catch (error2) {
2055
- const message2 = error2 instanceof Error ? truncateMessage(error2.message) : "Doctor failed";
2056
- trackEvent(AnalyticsEvents.CLI_DOCTOR, {
2057
- pass: 0,
2058
- warn: 0,
2059
- fail: 1,
2060
- strict: !!options.strict,
2061
- error: "doctor_failed"
2062
- });
2063
- if (options.json) {
2064
- const errorResult = {
2065
- checks: [],
2066
- summary: { pass: 0, warn: 0, fail: 1 },
2067
- exitCode: 1,
2068
- error: message2
2069
- };
2070
- process.stdout.write(JSON.stringify(errorResult, null, 0) + "\n");
2071
- } else {
2072
- error(message2);
2073
- }
2074
- process.exit(1);
2075
- }
2076
- }
2077
-
2078
- // src/cmds/ci.ts
2079
- import { execSync as execSync3 } from "child_process";
2080
- import { Octokit } from "@octokit/rest";
2081
- import * as p3 from "@clack/prompts";
2082
- function isGhAvailable() {
2083
- try {
2084
- execSync3("gh auth status", { stdio: "ignore" });
2085
- return true;
2086
- } catch {
2087
- return false;
2088
- }
2089
- }
2090
- function addSecretWithGh(repo, secretName, secretValue) {
2091
- execSync3(`gh secret set ${secretName} --repo ${repo}`, {
2092
- input: secretValue,
2093
- stdio: ["pipe", "ignore", "ignore"]
2094
- });
2095
- }
2096
- async function ciSetupCommand(options) {
2097
- const repo = options.repo || detectGitRepo();
2098
- if (!repo) {
2099
- error("Not in a git repository. Use --repo owner/repo");
2100
- process.exit(1);
2101
- }
2102
- intro2("ci setup");
2103
- step(`Setting up GitHub Actions for ${value(repo)}`);
2104
- message(dim("Step 1: Keyway Authentication"));
2105
- let keywayToken;
2106
- try {
2107
- keywayToken = await ensureLogin({ allowPrompt: true });
2108
- success("Authenticated with Keyway");
2109
- } catch {
2110
- error("Failed to authenticate with Keyway");
2111
- message(dim("Run `keyway login` first"));
2112
- process.exit(1);
2113
- }
2114
- const useGh = isGhAvailable();
2115
- if (useGh) {
2116
- message(dim("Step 2: Adding secret via GitHub CLI"));
2117
- try {
2118
- addSecretWithGh(repo, "KEYWAY_TOKEN", keywayToken);
2119
- success(`Secret KEYWAY_TOKEN added to ${repo}`);
2120
- } catch (error2) {
2121
- const message2 = error2 instanceof Error ? error2.message : String(error2);
2122
- error(`Failed to add secret: ${message2}`);
2123
- message(dim("Try running: gh auth login"));
2124
- process.exit(1);
2125
- }
2126
- } else {
2127
- message(dim("Step 2: Temporary GitHub PAT"));
2128
- info("gh CLI not found. We need a one-time GitHub PAT.");
2129
- message(dim("You can delete it immediately after setup."));
2130
- const patUrl = "https://github.com/settings/tokens/new?scopes=repo&description=Keyway%20CI%20Setup%20(temporary)";
2131
- await openUrl(patUrl);
2132
- const githubToken = await p3.password({
2133
- message: "Paste your GitHub PAT:"
2134
- });
2135
- if (p3.isCancel(githubToken) || !githubToken) {
2136
- error("GitHub PAT is required");
2137
- process.exit(1);
2138
- }
2139
- const octokit = new Octokit({ auth: githubToken });
2140
- try {
2141
- await octokit.users.getAuthenticated();
2142
- success("GitHub PAT validated");
2143
- } catch {
2144
- error("Invalid GitHub PAT");
2145
- process.exit(1);
2146
- }
2147
- message(dim("Step 3: Adding secret to repository"));
2148
- const [owner, repoName] = repo.split("/");
2149
- try {
2150
- await addRepoSecret(octokit, owner, repoName, "KEYWAY_TOKEN", keywayToken);
2151
- success(`Secret KEYWAY_TOKEN added to ${repo}`);
2152
- } catch (error2) {
2153
- const message2 = error2 instanceof Error ? error2.message : String(error2);
2154
- if (message2.includes("Not Found")) {
2155
- error(`Repository not found or no access: ${repo}`);
2156
- message(dim("Make sure the PAT has access to this repository"));
2157
- } else {
2158
- error(`Failed to add secret: ${message2}`);
2159
- }
2160
- process.exit(1);
2161
- }
2162
- }
2163
- success("Setup complete!");
2164
- note2(
2165
- `- uses: keywaysh/keyway-action@v1
2166
- with:
2167
- token: \${{ secrets.KEYWAY_TOKEN }}
2168
- environment: production`,
2169
- "Add this to your workflow"
2170
- );
2171
- if (!useGh) {
2172
- message(`Delete the temporary PAT: ${link("https://github.com/settings/tokens")}`);
2173
- }
2174
- outro2(`Docs: ${link("https://docs.keyway.sh/ci")}`);
2175
- }
2176
- async function addRepoSecret(octokit, owner, repo, secretName, secretValue) {
2177
- const { data: publicKey } = await octokit.rest.actions.getRepoPublicKey({
2178
- owner,
2179
- repo
2180
- });
2181
- const encryptedValue = await encryptSecret(publicKey.key, secretValue);
2182
- await octokit.rest.actions.createOrUpdateRepoSecret({
2183
- owner,
2184
- repo,
2185
- secret_name: secretName,
2186
- encrypted_value: encryptedValue,
2187
- key_id: publicKey.key_id
2188
- });
2189
- }
2190
- async function encryptSecret(publicKey, secret) {
2191
- const sodiumModule = await import("libsodium-wrappers");
2192
- const sodium = sodiumModule.default || sodiumModule;
2193
- await sodium.ready;
2194
- const binkey = sodium.from_base64(publicKey, sodium.base64_variants.ORIGINAL);
2195
- const binsec = sodium.from_string(secret);
2196
- const encBytes = sodium.crypto_box_seal(binsec, binkey);
2197
- return sodium.to_base64(encBytes, sodium.base64_variants.ORIGINAL);
2198
- }
2199
-
2200
- // src/cmds/connect.ts
2201
- import * as p4 from "@clack/prompts";
2202
- var TOKEN_AUTH_PROVIDERS = ["railway"];
2203
- function getTokenCreationUrl(provider) {
2204
- switch (provider) {
2205
- case "railway":
2206
- return "https://railway.com/account/tokens";
2207
- default:
2208
- return "";
2209
- }
2210
- }
2211
- async function connectWithTokenFlow(accessToken, provider, displayName) {
2212
- const tokenUrl = getTokenCreationUrl(provider);
2213
- if (provider === "railway") {
2214
- warn("Tip: Select the workspace containing your projects.");
2215
- message(dim(`Do NOT use "No workspace" - it won't have access to your projects.`));
2216
- }
2217
- await openUrl(tokenUrl);
2218
- const token = await p4.password({
2219
- message: `${displayName} API Token:`
2220
- });
2221
- if (p4.isCancel(token) || !token) {
2222
- message(dim("Cancelled."));
2223
- return false;
2224
- }
2225
- const s = spinner2();
2226
- s.start("Validating token...");
2227
- try {
2228
- const result = await connectWithToken(accessToken, provider, token);
2229
- s.stop("Validated");
2230
- if (result.success) {
2231
- success(`Connected to ${displayName}!`);
2232
- message(dim(`Account: ${result.user.username}`));
2233
- if (result.user.teamName) {
2234
- message(dim(`Team: ${result.user.teamName}`));
2235
- }
2236
- return true;
2237
- } else {
2238
- error("Connection failed.");
2239
- return false;
2240
- }
2241
- } catch (error2) {
2242
- s.stop("Failed");
2243
- const message2 = error2 instanceof Error ? error2.message : "Token validation failed";
2244
- error(message2);
2245
- return false;
2246
- }
2247
- }
2248
- async function connectWithOAuthFlow(accessToken, provider, displayName) {
2249
- const authUrl = getProviderAuthUrl(provider, accessToken);
2250
- const startTime = /* @__PURE__ */ new Date();
2251
- await openUrl(authUrl);
2252
- const s = spinner2();
2253
- s.start("Waiting for authorization...");
2254
- const maxAttempts = 60;
2255
- let attempts = 0;
2256
- while (attempts < maxAttempts) {
2257
- await new Promise((resolve) => setTimeout(resolve, 5e3));
2258
- attempts++;
2259
- try {
2260
- const { connections } = await getConnections(accessToken);
2261
- const newConn = connections.find(
2262
- (c) => c.provider === provider && new Date(c.createdAt) > startTime
2263
- );
2264
- if (newConn) {
2265
- s.stop("Authorized");
2266
- success(`Connected to ${displayName}!`);
2267
- return true;
2268
- }
2269
- } catch {
2270
- }
2271
- }
2272
- s.stop("Timeout");
2273
- error("Authorization timeout.");
2274
- message(dim("Run `keyway connections` to check if the connection was established."));
2275
- return false;
2276
- }
2277
- async function connectCommand(provider, options = {}) {
2278
- try {
2279
- const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
2280
- const { providers } = await getProviders();
2281
- const providerInfo = providers.find((p5) => p5.name === provider.toLowerCase());
2282
- if (!providerInfo) {
2283
- const available = providers.map((p5) => p5.name).join(", ");
2284
- error(`Unknown provider: ${provider}`);
2285
- message(dim(`Available providers: ${available || "none"}`));
2286
- process.exit(1);
2287
- }
2288
- if (!providerInfo.configured) {
2289
- error(`Provider ${providerInfo.displayName} is not configured on the server.`);
2290
- message(dim("Contact your administrator to enable this integration."));
2291
- process.exit(1);
2292
- }
2293
- const { connections } = await getConnections(accessToken);
2294
- const existingConnections = connections.filter((c) => c.provider === provider.toLowerCase());
2295
- if (existingConnections.length > 0) {
2296
- message(dim(`You have ${existingConnections.length} ${providerInfo.displayName} connection(s):`));
2297
- for (const conn of existingConnections) {
2298
- const teamInfo = conn.providerTeamId ? `(Team: ${conn.providerTeamId})` : "(Personal)";
2299
- message(dim(` - ${teamInfo}`));
2300
- }
2301
- const action = await select2({
2302
- message: "What would you like to do?",
2303
- options: [
2304
- { label: "Add another account/team", value: "add" },
2305
- { label: "Cancel", value: "cancel" }
2306
- ]
2307
- });
2308
- if (action !== "add") {
2309
- message(dim("Keeping existing connections."));
2310
- return;
2311
- }
2312
- }
2313
- step(`Connecting to ${providerInfo.displayName}...`);
2314
- let connected = false;
2315
- if (TOKEN_AUTH_PROVIDERS.includes(provider.toLowerCase())) {
2316
- connected = await connectWithTokenFlow(accessToken, provider.toLowerCase(), providerInfo.displayName);
2317
- } else {
2318
- connected = await connectWithOAuthFlow(accessToken, provider.toLowerCase(), providerInfo.displayName);
2319
- }
2320
- trackEvent(AnalyticsEvents.CLI_CONNECT, {
2321
- provider: provider.toLowerCase(),
2322
- success: connected
2323
- });
2324
- } catch (error2) {
2325
- const message2 = error2 instanceof Error ? error2.message : "Connection failed";
2326
- trackEvent(AnalyticsEvents.CLI_ERROR, {
2327
- command: "connect",
2328
- error: truncateMessage(message2)
2329
- });
2330
- error(message2);
2331
- process.exit(1);
2332
- }
2333
- }
2334
- async function connectionsCommand(options = {}) {
2335
- try {
2336
- const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
2337
- const { connections } = await getConnections(accessToken);
2338
- if (connections.length === 0) {
2339
- info("No provider connections found.");
2340
- message(dim("Connect to a provider with: keyway connect <provider>"));
2341
- message(dim("Available providers: vercel, railway"));
2342
- return;
2343
- }
2344
- intro2("connections");
2345
- for (const conn of connections) {
2346
- const providerName = conn.provider.charAt(0).toUpperCase() + conn.provider.slice(1);
2347
- const teamInfo = conn.providerTeamId ? dim(` (Team: ${conn.providerTeamId})`) : "";
2348
- const date = new Date(conn.createdAt).toLocaleDateString();
2349
- success(`${bold(providerName)}${teamInfo}`);
2350
- message(dim(` Connected: ${date}`));
2351
- message(dim(` ID: ${conn.id}`));
2352
- }
2353
- outro2("");
2354
- } catch (error2) {
2355
- const message2 = error2 instanceof Error ? error2.message : "Failed to list connections";
2356
- error(message2);
2357
- process.exit(1);
2358
- }
2359
- }
2360
- async function disconnectCommand(provider, options = {}) {
2361
- try {
2362
- const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
2363
- const { connections } = await getConnections(accessToken);
2364
- const connection = connections.find((c) => c.provider === provider.toLowerCase());
2365
- if (!connection) {
2366
- info(`No connection found for provider: ${provider}`);
2367
- return;
2368
- }
2369
- const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
2370
- const confirm3 = await confirm2({
2371
- message: `Disconnect from ${providerName}?`,
2372
- initialValue: false
2373
- });
2374
- if (!confirm3) {
2375
- message(dim("Cancelled."));
2376
- return;
2377
- }
2378
- await deleteConnection(accessToken, connection.id);
2379
- success(`Disconnected from ${providerName}`);
2380
- trackEvent(AnalyticsEvents.CLI_DISCONNECT, {
2381
- provider: provider.toLowerCase()
2382
- });
2383
- } catch (error2) {
2384
- const message2 = error2 instanceof Error ? error2.message : "Disconnect failed";
2385
- trackEvent(AnalyticsEvents.CLI_ERROR, {
2386
- command: "disconnect",
2387
- error: truncateMessage(message2)
2388
- });
2389
- error(message2);
2390
- process.exit(1);
2391
- }
2392
- }
2393
-
2394
- // src/cmds/sync.ts
2395
- import pc5 from "picocolors";
2396
- function mapToVercelEnvironment(keywayEnv) {
2397
- const mapping = {
2398
- production: "production",
2399
- staging: "preview",
2400
- dev: "development",
2401
- development: "development"
2402
- };
2403
- return mapping[keywayEnv.toLowerCase()] || "production";
2404
- }
2405
- function mapToRailwayEnvironment(keywayEnv) {
2406
- const mapping = {
2407
- production: "production",
2408
- staging: "staging",
2409
- dev: "development",
2410
- development: "development"
2411
- };
2412
- return mapping[keywayEnv.toLowerCase()] || "production";
2413
- }
2414
- function mapToNetlifyEnvironment(keywayEnv) {
2415
- const mapping = {
2416
- production: "production",
2417
- staging: "branch-deploy",
2418
- preview: "deploy-preview",
2419
- dev: "dev",
2420
- development: "dev"
2421
- };
2422
- return mapping[keywayEnv.toLowerCase()] || "production";
2423
- }
2424
- function mapToProviderEnvironment(provider, keywayEnv) {
2425
- switch (provider.toLowerCase()) {
2426
- case "vercel":
2427
- return mapToVercelEnvironment(keywayEnv);
2428
- case "railway":
2429
- return mapToRailwayEnvironment(keywayEnv);
2430
- case "netlify":
2431
- return mapToNetlifyEnvironment(keywayEnv);
2432
- default:
2433
- return keywayEnv;
2434
- }
2435
- }
2436
- function displayDiffSummary(diff, providerName) {
2437
- const totalDiff = diff.onlyInKeyway.length + diff.onlyInProvider.length + diff.different.length;
2438
- if (totalDiff === 0 && diff.same.length > 0) {
2439
- success(`Already in sync (${diff.same.length} secrets)`);
2440
- return;
2441
- }
2442
- step("Comparison Summary");
2443
- message(dim(`Keyway: ${diff.keywayCount} secrets | ${providerName}: ${diff.providerCount} secrets`));
2444
- if (diff.onlyInKeyway.length > 0) {
2445
- message(pc5.cyan(`\u2192 ${diff.onlyInKeyway.length} only in Keyway`));
2446
- diff.onlyInKeyway.slice(0, 3).forEach((key) => message(dim(` ${key}`)));
2447
- if (diff.onlyInKeyway.length > 3) {
2448
- message(dim(` ... and ${diff.onlyInKeyway.length - 3} more`));
2449
- }
2450
- }
2451
- if (diff.onlyInProvider.length > 0) {
2452
- message(pc5.magenta(`\u2190 ${diff.onlyInProvider.length} only in ${providerName}`));
2453
- diff.onlyInProvider.slice(0, 3).forEach((key) => message(dim(` ${key}`)));
2454
- if (diff.onlyInProvider.length > 3) {
2455
- message(dim(` ... and ${diff.onlyInProvider.length - 3} more`));
2456
- }
2457
- }
2458
- if (diff.different.length > 0) {
2459
- message(pc5.yellow(`\u2260 ${diff.different.length} with different values`));
2460
- diff.different.slice(0, 3).forEach((key) => message(dim(` ${key}`)));
2461
- if (diff.different.length > 3) {
2462
- message(dim(` ... and ${diff.different.length - 3} more`));
2463
- }
2464
- }
2465
- if (diff.same.length > 0) {
2466
- message(dim(`= ${diff.same.length} identical`));
2467
- }
2468
- }
2469
- function getProjectDisplayName(project) {
2470
- return project.serviceName || project.name;
2471
- }
2472
- function findMatchingProject(projects, repoFullName) {
2473
- const repoFullNameLower = repoFullName.toLowerCase();
2474
- const repoName = repoFullName.split("/")[1]?.toLowerCase();
2475
- if (!repoName) return void 0;
2476
- const linkedMatch = projects.find(
2477
- (p5) => p5.linkedRepo?.toLowerCase() === repoFullNameLower
2478
- );
2479
- if (linkedMatch) {
2480
- return { project: linkedMatch, matchType: "linked_repo" };
2481
- }
2482
- const exactNameMatch = projects.find((p5) => p5.name.toLowerCase() === repoName);
2483
- if (exactNameMatch) {
2484
- return { project: exactNameMatch, matchType: "exact_name" };
2485
- }
2486
- const partialMatches = projects.filter(
2487
- (p5) => p5.name.toLowerCase().includes(repoName) || repoName.includes(p5.name.toLowerCase())
2488
- );
2489
- if (partialMatches.length === 1) {
2490
- return { project: partialMatches[0], matchType: "partial_name" };
2491
- }
2492
- return void 0;
2493
- }
2494
- function projectMatchesRepo(project, repoFullName) {
2495
- const repoFullNameLower = repoFullName.toLowerCase();
2496
- const repoName = repoFullName.split("/")[1]?.toLowerCase();
2497
- if (project.linkedRepo?.toLowerCase() === repoFullNameLower) {
2498
- return true;
2499
- }
2500
- if (repoName && project.name.toLowerCase() === repoName) {
2501
- return true;
2502
- }
2503
- return false;
2504
- }
2505
- async function selectProjectWithConnectOption(accessToken, provider, providerDisplayName, repoFullName, initialProjects) {
2506
- let projects = initialProjects;
2507
- while (true) {
2508
- const result = await promptProjectSelection(projects, repoFullName, providerDisplayName);
2509
- if (result === "connect_new") {
2510
- await connectCommand(provider, { loginPrompt: false });
2511
- const { projects: allProjects } = await getAllProviderProjects(accessToken, provider.toLowerCase());
2512
- projects = allProjects.map((p5) => ({
2513
- id: p5.id,
2514
- name: p5.name,
2515
- serviceId: p5.serviceId,
2516
- serviceName: p5.serviceName,
2517
- linkedRepo: p5.linkedRepo,
2518
- environments: p5.environments,
2519
- connectionId: p5.connectionId,
2520
- teamId: p5.teamId,
2521
- teamName: p5.teamName
2522
- }));
2523
- if (projects.length === 0) {
2524
- error("No projects found after connecting.");
2525
- process.exit(1);
2526
- }
2527
- success(`Found ${projects.length} projects. Select one:`);
2528
- continue;
2529
- }
2530
- return { project: result, projects };
2531
- }
2532
- }
2533
- async function promptProjectSelection(projects, repoFullName, providerDisplayName) {
2534
- const repoName = repoFullName.split("/")[1]?.toLowerCase() || "";
2535
- const uniqueTeams = new Set(projects.map((p5) => p5.teamId || "personal"));
2536
- const hasMultipleAccounts = uniqueTeams.size > 1;
2537
- const options = projects.map((p5) => {
2538
- const displayName = getProjectDisplayName(p5);
2539
- let label = displayName;
2540
- const badges = [];
2541
- if (hasMultipleAccounts) {
2542
- if (p5.teamName) {
2543
- badges.push(pc5.cyan(`[${p5.teamName}]`));
2544
- } else if (p5.teamId) {
2545
- const shortTeamId = p5.teamId.length > 12 ? p5.teamId.slice(0, 12) + "..." : p5.teamId;
2546
- badges.push(pc5.cyan(`[team:${shortTeamId}]`));
2547
- } else {
2548
- badges.push(pc5.cyan("[personal]"));
2549
- }
2550
- }
2551
- if (p5.linkedRepo?.toLowerCase() === repoFullName.toLowerCase()) {
2552
- badges.push(pc5.green("\u2190 linked"));
2553
- } else if (p5.name.toLowerCase() === repoName || p5.serviceName?.toLowerCase() === repoName) {
2554
- badges.push(pc5.green("\u2190 same name"));
2555
- } else if (p5.linkedRepo) {
2556
- badges.push(pc5.gray(`\u2192 ${p5.linkedRepo}`));
2557
- }
2558
- if (badges.length > 0) {
2559
- label = `${displayName} ${badges.join(" ")}`;
2560
- }
2561
- return { label, value: p5.id };
2562
- });
2563
- options.push({
2564
- label: pc5.blue(`+ Connect another ${providerDisplayName} account`),
2565
- value: "__connect_new__"
2566
- });
2567
- const projectChoice = await select2({
2568
- message: "Select a project:",
2569
- options
2570
- });
2571
- if (!projectChoice) {
2572
- message(dim("Cancelled."));
2573
- process.exit(0);
2574
- }
2575
- if (projectChoice === "__connect_new__") {
2576
- return "connect_new";
2577
- }
2578
- return projects.find((p5) => p5.id === projectChoice);
2579
- }
2580
- async function syncCommand(provider, options = {}) {
2581
- try {
2582
- if (options.pull && options.allowDelete) {
2583
- error("--allow-delete cannot be used with --pull");
2584
- message(dim("The --allow-delete flag is only for push operations."));
2585
- process.exit(1);
2586
- }
2587
- const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
2588
- const repoFullName = detectGitRepo();
2589
- if (!repoFullName) {
2590
- error("Could not detect Git repository.");
2591
- message(dim("Run this command from a Git repository directory."));
2592
- process.exit(1);
2593
- }
2594
- step(`Repository: ${value(repoFullName)}`);
2595
- const vaultExists = await checkVaultExists(accessToken, repoFullName);
2596
- if (!vaultExists) {
2597
- warn(`No vault found for ${repoFullName}.`);
2598
- const shouldCreate = await confirm2({
2599
- message: "Create vault now?",
2600
- initialValue: true
2601
- });
2602
- if (!shouldCreate) {
2603
- message(dim("Cancelled. Run `keyway init` to create a vault first."));
2604
- process.exit(0);
2605
- }
2606
- const s = spinner2();
2607
- s.start("Creating vault...");
2608
- try {
2609
- await initVault(repoFullName, accessToken);
2610
- s.stop(`Vault created for ${repoFullName}`);
2611
- } catch (error2) {
2612
- s.stop("Failed");
2613
- const message2 = error2 instanceof Error ? error2.message : "Failed to create vault";
2614
- error(message2);
2615
- process.exit(1);
2616
- }
2617
- }
2618
- const providerDisplayName = provider.charAt(0).toUpperCase() + provider.slice(1);
2619
- let { projects: allProjects, connections } = await getAllProviderProjects(accessToken, provider.toLowerCase());
2620
- if (connections.length === 0) {
2621
- warn(`Not connected to ${providerDisplayName}.`);
2622
- const shouldConnect = await confirm2({
2623
- message: `Connect to ${providerDisplayName} now?`,
2624
- initialValue: true
2625
- });
2626
- if (!shouldConnect) {
2627
- message(dim("Cancelled."));
2628
- process.exit(0);
2629
- }
2630
- await connectCommand(provider, { loginPrompt: false });
2631
- const refreshed = await getAllProviderProjects(accessToken, provider.toLowerCase());
2632
- allProjects = refreshed.projects;
2633
- connections = refreshed.connections;
2634
- if (connections.length === 0) {
2635
- error(`Connection to ${providerDisplayName} failed.`);
2636
- process.exit(1);
2637
- }
2638
- }
2639
- let projects = allProjects.map((p5) => ({
2640
- id: p5.id,
2641
- name: p5.name,
2642
- serviceId: p5.serviceId,
2643
- serviceName: p5.serviceName,
2644
- linkedRepo: p5.linkedRepo,
2645
- environments: p5.environments,
2646
- connectionId: p5.connectionId,
2647
- teamId: p5.teamId,
2648
- teamName: p5.teamName
2649
- }));
2650
- if (options.team) {
2651
- const teamFilter = options.team.toLowerCase();
2652
- const filteredProjects = projects.filter(
2653
- (p5) => p5.teamId?.toLowerCase() === teamFilter || p5.teamName?.toLowerCase() === teamFilter || // Match "personal" for null teamId
2654
- teamFilter === "personal" && !p5.teamId
2655
- );
2656
- if (filteredProjects.length === 0) {
2657
- error(`No projects found for team: ${options.team}`);
2658
- message(dim("Available teams:"));
2659
- const teams = /* @__PURE__ */ new Set();
2660
- projects.forEach((p5) => {
2661
- if (p5.teamName) teams.add(p5.teamName);
2662
- else if (p5.teamId) teams.add(p5.teamId);
2663
- else teams.add("personal");
2664
- });
2665
- teams.forEach((t) => message(dim(` - ${t}`)));
2666
- process.exit(1);
2667
- }
2668
- projects = filteredProjects;
2669
- message(dim(`Filtered to ${projects.length} projects in team: ${options.team}`));
2670
- }
2671
- if (projects.length === 0) {
2672
- error(`No projects found in your ${providerDisplayName} account(s).`);
2673
- if (connections.length > 1) {
2674
- message(dim(`Checked ${connections.length} connected accounts.`));
2675
- }
2676
- process.exit(1);
2677
- }
2678
- if (connections.length > 1 && !options.team) {
2679
- message(dim(`Searching ${projects.length} projects across ${connections.length} ${providerDisplayName} accounts...`));
2680
- }
2681
- let selectedProject;
2682
- if (options.project) {
2683
- const found = projects.find(
2684
- (p5) => p5.id === options.project || p5.name.toLowerCase() === options.project?.toLowerCase() || p5.serviceName?.toLowerCase() === options.project?.toLowerCase()
2685
- );
2686
- if (!found) {
2687
- error(`Project not found: ${options.project}`);
2688
- message(dim("Available projects:"));
2689
- projects.forEach((p5) => message(dim(` - ${getProjectDisplayName(p5)}`)));
2690
- process.exit(1);
2691
- }
2692
- selectedProject = found;
2693
- if (!projectMatchesRepo(selectedProject, repoFullName)) {
2694
- warn("Project does not match current repository");
2695
- message(pc5.yellow(`Current repo: ${repoFullName}`));
2696
- message(pc5.yellow(`Selected project: ${getProjectDisplayName(selectedProject)}`));
2697
- if (selectedProject.linkedRepo) {
2698
- message(pc5.yellow(`Project linked to: ${selectedProject.linkedRepo}`));
2699
- }
2700
- }
2701
- } else {
2702
- const autoMatch = findMatchingProject(projects, repoFullName);
2703
- if (autoMatch && (autoMatch.matchType === "linked_repo" || autoMatch.matchType === "exact_name")) {
2704
- selectedProject = autoMatch.project;
2705
- const matchReason = autoMatch.matchType === "linked_repo" ? `linked to ${repoFullName}` : "exact name match";
2706
- let teamInfo = "";
2707
- if (selectedProject.teamName) {
2708
- teamInfo = dim(` (${selectedProject.teamName})`);
2709
- } else if (selectedProject.teamId && connections.length > 1) {
2710
- const shortTeamId = selectedProject.teamId.length > 12 ? selectedProject.teamId.slice(0, 12) + "..." : selectedProject.teamId;
2711
- teamInfo = dim(` (team:${shortTeamId})`);
2712
- }
2713
- success(`Auto-selected project: ${getProjectDisplayName(selectedProject)}${teamInfo} (${matchReason})`);
2714
- } else if (autoMatch && autoMatch.matchType === "partial_name") {
2715
- const partialDisplayName = getProjectDisplayName(autoMatch.project);
2716
- info(`Detected project: ${partialDisplayName} (partial match)`);
2717
- const useDetected = await confirm2({
2718
- message: `Use ${partialDisplayName}?`,
2719
- initialValue: true
2720
- });
2721
- if (useDetected) {
2722
- selectedProject = autoMatch.project;
2723
- } else {
2724
- const result = await selectProjectWithConnectOption(accessToken, provider, providerDisplayName, repoFullName, projects);
2725
- selectedProject = result.project;
2726
- projects = result.projects;
2727
- }
2728
- } else if (projects.length === 1) {
2729
- selectedProject = projects[0];
2730
- if (!projectMatchesRepo(selectedProject, repoFullName)) {
2731
- warn("Project does not match current repository");
2732
- message(pc5.yellow(`Current repo: ${repoFullName}`));
2733
- message(pc5.yellow(`Only project: ${getProjectDisplayName(selectedProject)}`));
2734
- if (selectedProject.linkedRepo) {
2735
- message(pc5.yellow(`Project linked to: ${selectedProject.linkedRepo}`));
2736
- }
2737
- const continueAnyway = await confirm2({
2738
- message: "Continue anyway?",
2739
- initialValue: false
2740
- });
2741
- if (!continueAnyway) {
2742
- message(dim("Cancelled."));
2743
- process.exit(0);
2744
- }
2745
- }
2746
- } else {
2747
- warn(`No matching project found for ${repoFullName}`);
2748
- message(dim("Select a project manually:"));
2749
- const result = await selectProjectWithConnectOption(accessToken, provider, providerDisplayName, repoFullName, projects);
2750
- selectedProject = result.project;
2751
- projects = result.projects;
2752
- }
2753
- }
2754
- if (!options.project && !projectMatchesRepo(selectedProject, repoFullName)) {
2755
- const autoMatch = findMatchingProject(projects, repoFullName);
2756
- if (autoMatch && autoMatch.project.id !== selectedProject.id) {
2757
- warn("You selected a different project");
2758
- message(pc5.yellow(`Current repo: ${repoFullName}`));
2759
- message(pc5.yellow(`Selected project: ${getProjectDisplayName(selectedProject)}`));
2760
- if (selectedProject.linkedRepo) {
2761
- message(pc5.yellow(`Project linked to: ${selectedProject.linkedRepo}`));
2762
- }
2763
- const continueAnyway = await confirm2({
2764
- message: "Are you sure you want to sync with this project?",
2765
- initialValue: false
2766
- });
2767
- if (!continueAnyway) {
2768
- message(dim("Cancelled."));
2769
- process.exit(0);
2770
- }
2771
- }
2772
- }
2773
- const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
2774
- let keywayEnv = options.environment;
2775
- let providerEnv = options.providerEnv;
2776
- let direction = options.push ? "push" : options.pull ? "pull" : void 0;
2777
- const needsEnvPrompt = !options.environment;
2778
- const needsDirectionPrompt = !direction;
2779
- if (needsEnvPrompt || needsDirectionPrompt) {
2780
- if (needsEnvPrompt) {
2781
- const vaultEnvs = await getVaultEnvironments(accessToken, repoFullName);
2782
- const selectedEnv = await select2({
2783
- message: "Keyway environment:",
2784
- options: vaultEnvs.map((e) => ({ label: e, value: e })),
2785
- initialValue: vaultEnvs.includes("production") ? "production" : vaultEnvs[0]
2786
- });
2787
- if (!selectedEnv) {
2788
- message(dim("Cancelled."));
2789
- process.exit(0);
2790
- }
2791
- keywayEnv = selectedEnv;
2792
- if (!options.providerEnv) {
2793
- if (selectedProject.environments && selectedProject.environments.length > 0) {
2794
- const mappedEnv = mapToProviderEnvironment(provider, keywayEnv);
2795
- const envExists = selectedProject.environments.some(
2796
- (e) => e.toLowerCase() === mappedEnv.toLowerCase()
2797
- );
2798
- if (envExists) {
2799
- providerEnv = mappedEnv;
2800
- } else if (selectedProject.environments.length === 1) {
2801
- providerEnv = selectedProject.environments[0];
2802
- message(dim(`Using ${providerName} environment: ${providerEnv}`));
2803
- } else {
2804
- const selectedProviderEnv = await select2({
2805
- message: `${providerName} environment:`,
2806
- options: selectedProject.environments.map((e) => ({ label: e, value: e })),
2807
- initialValue: selectedProject.environments.includes("production") ? "production" : selectedProject.environments[0]
2808
- });
2809
- if (!selectedProviderEnv) {
2810
- message(dim("Cancelled."));
2811
- process.exit(0);
2812
- }
2813
- providerEnv = selectedProviderEnv;
2814
- }
2815
- } else {
2816
- providerEnv = mapToProviderEnvironment(provider, keywayEnv);
2817
- }
2818
- }
2819
- }
2820
- let diff;
2821
- if (needsDirectionPrompt) {
2822
- const effectiveKeywayEnv = keywayEnv || "production";
2823
- const effectiveProviderEnv = providerEnv || mapToProviderEnvironment(provider, effectiveKeywayEnv);
2824
- const s = spinner2();
2825
- s.start("Comparing secrets...");
2826
- diff = await getSyncDiff(accessToken, repoFullName, {
2827
- connectionId: selectedProject.connectionId,
2828
- projectId: selectedProject.id,
2829
- serviceId: selectedProject.serviceId,
2830
- // Railway: service ID for service-specific variables
2831
- keywayEnvironment: effectiveKeywayEnv,
2832
- providerEnvironment: effectiveProviderEnv
2833
- });
2834
- s.stop("Compared");
2835
- displayDiffSummary(diff, providerName);
2836
- const totalDiff = diff.onlyInKeyway.length + diff.onlyInProvider.length + diff.different.length;
2837
- if (totalDiff === 0) {
2838
- return;
2839
- }
2840
- }
2841
- if (needsDirectionPrompt && diff) {
2842
- const defaultDirection = diff.keywayCount === 0 && diff.providerCount > 0 ? "pull" : "push";
2843
- const selectedDirection = await select2({
2844
- message: "Sync direction:",
2845
- options: [
2846
- { label: `Keyway \u2192 ${providerName}`, value: "push" },
2847
- { label: `${providerName} \u2192 Keyway`, value: "pull" }
2848
- ],
2849
- initialValue: defaultDirection
2850
- });
2851
- if (!selectedDirection) {
2852
- message(dim("Cancelled."));
2853
- process.exit(0);
2854
- }
2855
- direction = selectedDirection;
2856
- }
2857
- }
2858
- keywayEnv = keywayEnv || "production";
2859
- providerEnv = providerEnv || "production";
2860
- direction = direction || "push";
2861
- const status = await getSyncStatus(
2862
- accessToken,
2863
- repoFullName,
2864
- selectedProject.connectionId,
2865
- selectedProject.id,
2866
- keywayEnv
2867
- );
2868
- if (status.isFirstSync && direction === "push" && status.vaultIsEmpty && status.providerHasSecrets) {
2869
- warn(`Your Keyway vault is empty for "${keywayEnv}", but ${providerName} has ${status.providerSecretCount} secrets.`);
2870
- message(dim("(Use --environment to sync a different environment)"));
2871
- const importFirst = await confirm2({
2872
- message: `Import secrets from ${providerName} first?`,
2873
- initialValue: true
2874
- });
2875
- if (importFirst) {
2876
- await executeSyncOperation(
2877
- accessToken,
2878
- repoFullName,
2879
- selectedProject.connectionId,
2880
- selectedProject,
2881
- keywayEnv,
2882
- providerEnv,
2883
- "pull",
2884
- false,
2885
- // Never delete on import
2886
- options.yes || false,
2887
- provider
2888
- );
2889
- return;
2890
- }
2891
- }
2892
- await executeSyncOperation(
2893
- accessToken,
2894
- repoFullName,
2895
- selectedProject.connectionId,
2896
- selectedProject,
2897
- keywayEnv,
2898
- providerEnv,
2899
- direction,
2900
- options.allowDelete || false,
2901
- options.yes || false,
2902
- provider
2903
- );
2904
- } catch (error2) {
2905
- const message2 = error2 instanceof Error ? error2.message : "Sync failed";
2906
- trackEvent(AnalyticsEvents.CLI_ERROR, {
2907
- command: "sync",
2908
- error: truncateMessage(message2)
2909
- });
2910
- error(message2);
2911
- process.exit(1);
2912
- }
2913
- }
2914
- async function executeSyncOperation(accessToken, repoFullName, connectionId, project, keywayEnv, providerEnv, direction, allowDelete, skipConfirm, provider) {
2915
- const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
2916
- const preview = await getSyncPreview(accessToken, repoFullName, {
2917
- connectionId,
2918
- projectId: project.id,
2919
- serviceId: project.serviceId,
2920
- // Railway: service ID for service-specific variables
2921
- keywayEnvironment: keywayEnv,
2922
- providerEnvironment: providerEnv,
2923
- direction,
2924
- allowDelete
2925
- });
2926
- const totalChanges = preview.toCreate.length + preview.toUpdate.length + preview.toDelete.length;
2927
- if (totalChanges === 0) {
2928
- success("Already in sync. No changes needed.");
2929
- return;
2930
- }
2931
- step("Sync Preview");
2932
- if (preview.toCreate.length > 0) {
2933
- message(pc5.green(`+ ${preview.toCreate.length} to create`));
2934
- preview.toCreate.slice(0, 5).forEach((key) => message(dim(` ${key}`)));
2935
- if (preview.toCreate.length > 5) {
2936
- message(dim(` ... and ${preview.toCreate.length - 5} more`));
2937
- }
2938
- }
2939
- if (preview.toUpdate.length > 0) {
2940
- message(pc5.yellow(`~ ${preview.toUpdate.length} to update`));
2941
- preview.toUpdate.slice(0, 5).forEach((key) => message(dim(` ${key}`)));
2942
- if (preview.toUpdate.length > 5) {
2943
- message(dim(` ... and ${preview.toUpdate.length - 5} more`));
2944
- }
2945
- }
2946
- if (preview.toDelete.length > 0) {
2947
- message(pc5.red(`- ${preview.toDelete.length} to delete`));
2948
- preview.toDelete.slice(0, 5).forEach((key) => message(dim(` ${key}`)));
2949
- if (preview.toDelete.length > 5) {
2950
- message(dim(` ... and ${preview.toDelete.length - 5} more`));
2951
- }
2952
- }
2953
- if (preview.toSkip.length > 0) {
2954
- message(dim(`\u25CB ${preview.toSkip.length} unchanged`));
2955
- }
2956
- if (!skipConfirm) {
2957
- const target = direction === "push" ? providerName : "Keyway";
2958
- const confirm3 = await confirm2({
2959
- message: `Apply ${totalChanges} changes to ${target}?`,
2960
- initialValue: true
2961
- });
2962
- if (!confirm3) {
2963
- message(dim("Cancelled."));
2964
- return;
2965
- }
2966
- }
2967
- const s = spinner2();
2968
- s.start("Syncing...");
2969
- const result = await executeSync(accessToken, repoFullName, {
2970
- connectionId,
2971
- projectId: project.id,
2972
- serviceId: project.serviceId,
2973
- // Railway: service ID for service-specific variables
2974
- keywayEnvironment: keywayEnv,
2975
- providerEnvironment: providerEnv,
2976
- direction,
2977
- allowDelete
2978
- });
2979
- if (result.success) {
2980
- s.stop("Sync complete");
2981
- message(dim(`Created: ${result.stats.created}`));
2982
- message(dim(`Updated: ${result.stats.updated}`));
2983
- if (result.stats.deleted > 0) {
2984
- message(dim(`Deleted: ${result.stats.deleted}`));
2985
- }
2986
- trackEvent(AnalyticsEvents.CLI_SYNC, {
2987
- provider,
2988
- direction,
2989
- created: result.stats.created,
2990
- updated: result.stats.updated,
2991
- deleted: result.stats.deleted
2992
- });
2993
- } else {
2994
- s.stop("Failed");
2995
- error(result.error || "Sync failed");
2996
- process.exit(1);
2997
- }
2998
- }
2999
-
3000
- // src/cli.ts
3001
- process.on("unhandledRejection", (reason) => {
3002
- error(`Unhandled error: ${reason}`);
3003
- process.exit(1);
3004
- });
3005
- var program = new Command();
3006
- var TAGLINE = "Sync secrets with your team and infra";
3007
- program.name("keyway").description(TAGLINE).version(package_default.version);
3008
- program.command("init").description("Initialize a vault for the current repository").option("--no-login-prompt", "Fail instead of prompting to login if unauthenticated").action(async (options) => {
3009
- await initCommand(options);
3010
- });
3011
- program.command("push").description("Upload secrets from an env file to the vault").option("-e, --env <environment>", "Environment name", "development").option("-f, --file <file>", "Env file to push").option("-y, --yes", "Skip confirmation prompt").option("--no-login-prompt", "Fail instead of prompting to login if unauthenticated").action(async (options) => {
3012
- await pushCommand(options);
3013
- });
3014
- program.command("pull").description("Download secrets from the vault to an env file").option("-e, --env <environment>", "Environment name", "development").option("-f, --file <file>", "Env file to write to").option("-y, --yes", "Overwrite target file without confirmation").option("--no-login-prompt", "Fail instead of prompting to login if unauthenticated").action(async (options) => {
3015
- await pullCommand(options);
3016
- });
3017
- program.command("login").description("Authenticate with GitHub via Keyway").option("--token", "Authenticate using a GitHub fine-grained PAT").action(async (options) => {
3018
- await loginCommand(options);
3019
- });
3020
- program.command("logout").description("Clear stored Keyway credentials").action(async () => {
3021
- await logoutCommand();
3022
- });
3023
- 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) => {
3024
- await doctorCommand(options);
3025
- });
3026
- 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) => {
3027
- await connectCommand(provider, options);
3028
- });
3029
- program.command("connections").description("List your provider connections").option("--no-login-prompt", "Fail instead of prompting to login if unauthenticated").action(async (options) => {
3030
- await connectionsCommand(options);
3031
- });
3032
- 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) => {
3033
- await disconnectCommand(provider, options);
3034
- });
3035
- program.command("sync <provider>").description("Sync secrets with a provider (e.g., vercel)").option("--push", "Export secrets from Keyway to provider").option("--pull", "Import secrets from provider to Keyway").option("-e, --environment <env>", "Keyway environment (default: production)").option("--provider-env <env>", "Provider environment (default: production)").option("--project <project>", "Provider project name or ID").option("--team <team>", "Team/org name or ID (for multi-account)").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) => {
3036
- await syncCommand(provider, options);
3037
- });
3038
- var ci = program.command("ci").description("CI/CD integration commands");
3039
- ci.command("setup").description("Setup GitHub Actions integration (adds KEYWAY_TOKEN secret)").option("--repo <repo>", "Repository in owner/repo format (auto-detected)").action(async (options) => {
3040
- await ciSetupCommand(options);
3041
- });
3042
- (async () => {
3043
- await program.parseAsync();
3044
- })().catch((error2) => {
3045
- error(error2.message || error2);
3046
- process.exit(1);
3047
- });