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