@keywaysh/cli 0.0.1 → 0.0.3
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 +285 -10
- package/dist/cli.js +1322 -123
- package/package.json +19 -12
package/dist/cli.js
CHANGED
|
@@ -1,138 +1,1337 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
var __create = Object.create;
|
|
4
|
-
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
var __copyProps = (to, from, except, desc) => {
|
|
10
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
-
for (let key of __getOwnPropNames(from))
|
|
12
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
-
}
|
|
15
|
-
return to;
|
|
16
|
-
};
|
|
17
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
-
mod
|
|
24
|
-
));
|
|
25
2
|
|
|
26
3
|
// src/cli.ts
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import chalk7 from "chalk";
|
|
6
|
+
|
|
7
|
+
// src/cmds/init.ts
|
|
8
|
+
import chalk3 from "chalk";
|
|
9
|
+
|
|
10
|
+
// src/utils/git.ts
|
|
11
|
+
import { execSync } from "child_process";
|
|
12
|
+
function getCurrentRepoFullName() {
|
|
13
|
+
try {
|
|
14
|
+
if (!isGitRepository()) {
|
|
15
|
+
throw new Error("Not in a git repository");
|
|
16
|
+
}
|
|
17
|
+
const remoteUrl = execSync("git config --get remote.origin.url", {
|
|
18
|
+
encoding: "utf-8"
|
|
19
|
+
}).trim();
|
|
20
|
+
return parseGitHubUrl(remoteUrl);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
throw new Error("Failed to get repository name. Make sure you are in a git repository with a GitHub remote.");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function isGitRepository() {
|
|
26
|
+
try {
|
|
27
|
+
execSync("git rev-parse --is-inside-work-tree", {
|
|
28
|
+
encoding: "utf-8",
|
|
29
|
+
stdio: "pipe"
|
|
30
|
+
});
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function detectGitRepo() {
|
|
37
|
+
try {
|
|
38
|
+
const remoteUrl = execSync("git remote get-url origin", {
|
|
39
|
+
encoding: "utf-8",
|
|
40
|
+
stdio: "pipe"
|
|
41
|
+
}).trim();
|
|
42
|
+
return parseGitHubUrl(remoteUrl);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function parseGitHubUrl(url) {
|
|
48
|
+
const sshMatch = url.match(/git@github\.com:(.+)\/(.+)\.git/);
|
|
49
|
+
if (sshMatch) {
|
|
50
|
+
return `${sshMatch[1]}/${sshMatch[2]}`;
|
|
51
|
+
}
|
|
52
|
+
const httpsMatch = url.match(/https:\/\/github\.com\/(.+)\/(.+)\.git/);
|
|
53
|
+
if (httpsMatch) {
|
|
54
|
+
return `${httpsMatch[1]}/${httpsMatch[2]}`;
|
|
55
|
+
}
|
|
56
|
+
const httpsMatch2 = url.match(/https:\/\/github\.com\/(.+)\/(.+)/);
|
|
57
|
+
if (httpsMatch2) {
|
|
58
|
+
return `${httpsMatch2[1]}/${httpsMatch2[2]}`;
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`Invalid GitHub URL: ${url}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/config/internal.ts
|
|
64
|
+
var INTERNAL_API_URL = "https://api.keyway.sh";
|
|
65
|
+
var INTERNAL_POSTHOG_KEY = "phc_duG0qqI5z8LeHrS9pNxR5KaD4djgD0nmzUxuD3zP0ov";
|
|
66
|
+
var INTERNAL_POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
67
|
+
|
|
68
|
+
// src/utils/api.ts
|
|
69
|
+
var API_BASE_URL = process.env.KEYWAY_API_URL || INTERNAL_API_URL;
|
|
70
|
+
var APIError = class extends Error {
|
|
71
|
+
constructor(statusCode, error, message) {
|
|
72
|
+
super(message);
|
|
73
|
+
this.statusCode = statusCode;
|
|
74
|
+
this.error = error;
|
|
75
|
+
this.name = "APIError";
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
async function handleResponse(response) {
|
|
79
|
+
const contentType = response.headers.get("content-type") || "";
|
|
80
|
+
const text = await response.text();
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
if (contentType.includes("application/json")) {
|
|
83
|
+
try {
|
|
84
|
+
const error = JSON.parse(text);
|
|
85
|
+
throw new APIError(response.status, error.error, error.message);
|
|
86
|
+
} catch {
|
|
87
|
+
throw new APIError(response.status, "http_error", text || `HTTP ${response.status}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
throw new APIError(response.status, "http_error", text || `HTTP ${response.status}`);
|
|
91
|
+
}
|
|
92
|
+
if (!text) {
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
if (contentType.includes("application/json")) {
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(text);
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { content: text };
|
|
102
|
+
}
|
|
103
|
+
async function initVault(repoFullName, accessToken) {
|
|
104
|
+
const body = { repoFullName };
|
|
105
|
+
const headers = { "Content-Type": "application/json" };
|
|
106
|
+
if (accessToken) {
|
|
107
|
+
headers.Authorization = `Bearer ${accessToken}`;
|
|
108
|
+
}
|
|
109
|
+
const response = await fetch(`${API_BASE_URL}/vaults/init`, {
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers,
|
|
112
|
+
body: JSON.stringify(body)
|
|
113
|
+
});
|
|
114
|
+
return handleResponse(response);
|
|
115
|
+
}
|
|
116
|
+
async function pushSecrets(repoFullName, environment, content, accessToken) {
|
|
117
|
+
const body = { content };
|
|
118
|
+
const headers = { "Content-Type": "application/json" };
|
|
119
|
+
if (accessToken) {
|
|
120
|
+
headers.Authorization = `Bearer ${accessToken}`;
|
|
121
|
+
}
|
|
122
|
+
const encodedRepo = encodeURIComponent(repoFullName);
|
|
123
|
+
const response = await fetch(
|
|
124
|
+
`${API_BASE_URL}/vaults/${encodedRepo}/${environment}/push`,
|
|
125
|
+
{
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers,
|
|
128
|
+
body: JSON.stringify(body)
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
return handleResponse(response);
|
|
132
|
+
}
|
|
133
|
+
async function pullSecrets(repoFullName, environment, accessToken) {
|
|
134
|
+
const encodedRepo = encodeURIComponent(repoFullName);
|
|
135
|
+
const headers = { "Content-Type": "application/json" };
|
|
136
|
+
if (accessToken) {
|
|
137
|
+
headers.Authorization = `Bearer ${accessToken}`;
|
|
138
|
+
}
|
|
139
|
+
const response = await fetch(
|
|
140
|
+
`${API_BASE_URL}/vaults/${encodedRepo}/${environment}/pull`,
|
|
141
|
+
{
|
|
142
|
+
method: "GET",
|
|
143
|
+
headers
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
return handleResponse(response);
|
|
147
|
+
}
|
|
148
|
+
async function startDeviceLogin(repository) {
|
|
149
|
+
const response = await fetch(`${API_BASE_URL}/auth/device/start`, {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: { "Content-Type": "application/json" },
|
|
152
|
+
body: JSON.stringify(repository ? { repository } : {})
|
|
153
|
+
});
|
|
154
|
+
return handleResponse(response);
|
|
155
|
+
}
|
|
156
|
+
async function pollDeviceLogin(deviceCode) {
|
|
157
|
+
const response = await fetch(`${API_BASE_URL}/auth/device/poll`, {
|
|
158
|
+
method: "POST",
|
|
159
|
+
headers: { "Content-Type": "application/json" },
|
|
160
|
+
body: JSON.stringify({ deviceCode })
|
|
161
|
+
});
|
|
162
|
+
return handleResponse(response);
|
|
163
|
+
}
|
|
164
|
+
async function validateToken(token) {
|
|
165
|
+
const response = await fetch(`${API_BASE_URL}/auth/token/validate`, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: {
|
|
168
|
+
"Content-Type": "application/json",
|
|
169
|
+
Authorization: `Bearer ${token}`
|
|
170
|
+
},
|
|
171
|
+
body: JSON.stringify({})
|
|
172
|
+
});
|
|
173
|
+
return handleResponse(response);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/utils/analytics.ts
|
|
177
|
+
import { PostHog } from "posthog-node";
|
|
178
|
+
import crypto from "crypto";
|
|
179
|
+
import path from "path";
|
|
180
|
+
import os from "os";
|
|
181
|
+
import fs from "fs";
|
|
182
|
+
|
|
183
|
+
// package.json
|
|
184
|
+
var package_default = {
|
|
185
|
+
name: "@keywaysh/cli",
|
|
186
|
+
version: "0.0.3",
|
|
187
|
+
description: "One link to all your secrets",
|
|
188
|
+
type: "module",
|
|
189
|
+
bin: {
|
|
190
|
+
keyway: "./dist/cli.js"
|
|
191
|
+
},
|
|
192
|
+
main: "./dist/cli.js",
|
|
193
|
+
files: [
|
|
194
|
+
"dist"
|
|
195
|
+
],
|
|
196
|
+
scripts: {
|
|
197
|
+
dev: "pnpm exec tsx src/cli.ts",
|
|
198
|
+
build: "pnpm exec tsup",
|
|
199
|
+
"build:watch": "pnpm exec tsup --watch",
|
|
200
|
+
prepublishOnly: "pnpm run build",
|
|
201
|
+
test: "pnpm exec vitest run",
|
|
202
|
+
"test:watch": "pnpm exec vitest"
|
|
203
|
+
},
|
|
204
|
+
keywords: [
|
|
205
|
+
"secrets",
|
|
206
|
+
"env",
|
|
207
|
+
"keyway",
|
|
208
|
+
"cli",
|
|
209
|
+
"devops"
|
|
210
|
+
],
|
|
211
|
+
author: "Nicolas Ritouet",
|
|
212
|
+
license: "MIT",
|
|
213
|
+
homepage: "https://keyway.sh",
|
|
214
|
+
repository: {
|
|
215
|
+
type: "git",
|
|
216
|
+
url: "https://github.com/keywaysh/cli.git"
|
|
217
|
+
},
|
|
218
|
+
bugs: {
|
|
219
|
+
url: "https://github.com/keywaysh/cli/issues"
|
|
220
|
+
},
|
|
221
|
+
packageManager: "pnpm@10.6.1",
|
|
222
|
+
engines: {
|
|
223
|
+
node: ">=18.0.0"
|
|
224
|
+
},
|
|
225
|
+
dependencies: {
|
|
226
|
+
chalk: "^4.1.2",
|
|
227
|
+
commander: "^14.0.0",
|
|
228
|
+
conf: "^15.0.2",
|
|
229
|
+
open: "^11.0.0",
|
|
230
|
+
"posthog-node": "^3.5.0",
|
|
231
|
+
prompts: "^2.4.2"
|
|
232
|
+
},
|
|
233
|
+
devDependencies: {
|
|
234
|
+
"@types/node": "^24.2.0",
|
|
235
|
+
"@types/prompts": "^2.4.9",
|
|
236
|
+
tsup: "^8.5.0",
|
|
237
|
+
tsx: "^4.20.3",
|
|
238
|
+
typescript: "^5.9.2",
|
|
239
|
+
vitest: "^3.2.4"
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// src/utils/analytics.ts
|
|
244
|
+
var posthog = null;
|
|
245
|
+
var distinctId = null;
|
|
246
|
+
var CONFIG_DIR = path.join(os.homedir(), ".config", "keyway");
|
|
247
|
+
var ID_FILE = path.join(CONFIG_DIR, "id.json");
|
|
248
|
+
var TELEMETRY_DISABLED = process.env.KEYWAY_DISABLE_TELEMETRY === "1";
|
|
249
|
+
var CI = process.env.CI === "true" || process.env.CI === "1";
|
|
250
|
+
function getDistinctId() {
|
|
251
|
+
if (distinctId) return distinctId;
|
|
252
|
+
try {
|
|
253
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
254
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
255
|
+
}
|
|
256
|
+
if (fs.existsSync(ID_FILE)) {
|
|
257
|
+
const content = fs.readFileSync(ID_FILE, "utf-8");
|
|
258
|
+
const config2 = JSON.parse(content);
|
|
259
|
+
distinctId = config2.distinctId;
|
|
260
|
+
return distinctId;
|
|
261
|
+
}
|
|
262
|
+
distinctId = crypto.randomUUID();
|
|
263
|
+
const config = { distinctId };
|
|
264
|
+
fs.writeFileSync(ID_FILE, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
|
|
265
|
+
try {
|
|
266
|
+
fs.chmodSync(ID_FILE, 384);
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
return distinctId;
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.warn("Failed to persist distinct ID, using session-based ID");
|
|
272
|
+
distinctId = `session-${crypto.randomUUID()}`;
|
|
273
|
+
return distinctId;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function initPostHog() {
|
|
277
|
+
if (posthog) return;
|
|
278
|
+
if (TELEMETRY_DISABLED) return;
|
|
279
|
+
const apiKey = process.env.KEYWAY_POSTHOG_KEY || INTERNAL_POSTHOG_KEY;
|
|
280
|
+
if (!apiKey) return;
|
|
281
|
+
posthog = new PostHog(apiKey, {
|
|
282
|
+
host: process.env.KEYWAY_POSTHOG_HOST || INTERNAL_POSTHOG_HOST
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
function trackEvent(event, properties) {
|
|
286
|
+
try {
|
|
287
|
+
if (TELEMETRY_DISABLED) return;
|
|
288
|
+
if (!posthog) initPostHog();
|
|
289
|
+
if (!posthog) return;
|
|
290
|
+
const id = getDistinctId();
|
|
291
|
+
const sanitizedProperties = properties ? sanitizeProperties(properties) : {};
|
|
292
|
+
posthog.capture({
|
|
293
|
+
distinctId: id,
|
|
294
|
+
event,
|
|
295
|
+
properties: {
|
|
296
|
+
...sanitizedProperties,
|
|
297
|
+
source: "cli",
|
|
298
|
+
platform: process.platform,
|
|
299
|
+
nodeVersion: process.version,
|
|
300
|
+
version: package_default.version,
|
|
301
|
+
ci: CI
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.debug("Analytics error:", error);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function sanitizeProperties(properties) {
|
|
309
|
+
const sanitized = {};
|
|
310
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
311
|
+
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")) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (value && typeof value === "string" && value.length > 500) {
|
|
315
|
+
sanitized[key] = `${value.slice(0, 200)}...`;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
sanitized[key] = value;
|
|
319
|
+
}
|
|
320
|
+
return sanitized;
|
|
321
|
+
}
|
|
322
|
+
async function shutdownAnalytics() {
|
|
323
|
+
if (posthog) {
|
|
324
|
+
await posthog.shutdown();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
var AnalyticsEvents = {
|
|
328
|
+
CLI_INIT: "cli_init",
|
|
329
|
+
CLI_PUSH: "cli_push",
|
|
330
|
+
CLI_PULL: "cli_pull",
|
|
331
|
+
CLI_ERROR: "cli_error",
|
|
332
|
+
CLI_LOGIN: "cli_login",
|
|
333
|
+
CLI_DOCTOR: "cli_doctor"
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// src/cmds/login.ts
|
|
337
|
+
import chalk from "chalk";
|
|
338
|
+
import readline from "readline";
|
|
339
|
+
import open from "open";
|
|
340
|
+
import prompts from "prompts";
|
|
341
|
+
|
|
342
|
+
// src/utils/auth.ts
|
|
343
|
+
import Conf from "conf";
|
|
344
|
+
var store = new Conf({
|
|
345
|
+
projectName: "keyway",
|
|
346
|
+
configName: "config",
|
|
347
|
+
fileMode: 384
|
|
348
|
+
});
|
|
349
|
+
function isExpired(auth) {
|
|
350
|
+
if (!auth.expiresAt) return false;
|
|
351
|
+
const expires = Date.parse(auth.expiresAt);
|
|
352
|
+
if (Number.isNaN(expires)) return false;
|
|
353
|
+
return expires <= Date.now();
|
|
354
|
+
}
|
|
355
|
+
function getStoredAuth() {
|
|
356
|
+
const auth = store.get("auth");
|
|
357
|
+
if (auth && isExpired(auth)) {
|
|
358
|
+
clearAuth();
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
return auth ?? null;
|
|
362
|
+
}
|
|
363
|
+
function saveAuthToken(token, meta) {
|
|
364
|
+
const auth = {
|
|
365
|
+
keywayToken: token,
|
|
366
|
+
githubLogin: meta?.githubLogin,
|
|
367
|
+
expiresAt: meta?.expiresAt,
|
|
368
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
369
|
+
};
|
|
370
|
+
store.set("auth", auth);
|
|
371
|
+
}
|
|
372
|
+
function clearAuth() {
|
|
373
|
+
store.delete("auth");
|
|
374
|
+
}
|
|
375
|
+
function getAuthFilePath() {
|
|
376
|
+
return store.path;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/cmds/login.ts
|
|
380
|
+
function sleep(ms) {
|
|
381
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
382
|
+
}
|
|
383
|
+
function isInteractive() {
|
|
384
|
+
return Boolean(process.stdout.isTTY && process.stdin.isTTY && !process.env.CI);
|
|
385
|
+
}
|
|
386
|
+
async function promptYesNo(question, defaultYes = true) {
|
|
387
|
+
return new Promise((resolve) => {
|
|
388
|
+
const rl = readline.createInterface({
|
|
389
|
+
input: process.stdin,
|
|
390
|
+
output: process.stdout
|
|
391
|
+
});
|
|
392
|
+
rl.question(question, (answer) => {
|
|
393
|
+
rl.close();
|
|
394
|
+
const normalized = answer.trim().toLowerCase();
|
|
395
|
+
if (!normalized) return resolve(defaultYes);
|
|
396
|
+
if (["y", "yes"].includes(normalized)) return resolve(true);
|
|
397
|
+
if (["n", "no"].includes(normalized)) return resolve(false);
|
|
398
|
+
return resolve(defaultYes);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
async function runLoginFlow() {
|
|
403
|
+
console.log(chalk.blue("\u{1F510} Starting Keyway login...\n"));
|
|
404
|
+
const repoName = detectGitRepo();
|
|
405
|
+
const start = await startDeviceLogin(repoName);
|
|
406
|
+
const verifyUrl = start.verificationUriComplete || start.verificationUri;
|
|
407
|
+
if (!verifyUrl) {
|
|
408
|
+
throw new Error("Missing verification URL from the auth server.");
|
|
409
|
+
}
|
|
410
|
+
console.log(`Code: ${chalk.green.bold(start.userCode)}`);
|
|
411
|
+
console.log("Waiting for auth...");
|
|
412
|
+
open(verifyUrl).catch(() => {
|
|
413
|
+
console.log(chalk.gray(`Open this URL in your browser: ${verifyUrl}`));
|
|
414
|
+
});
|
|
415
|
+
const pollIntervalMs = (start.interval ?? 5) * 1e3;
|
|
416
|
+
while (true) {
|
|
417
|
+
await sleep(pollIntervalMs);
|
|
418
|
+
const result = await pollDeviceLogin(start.deviceCode);
|
|
419
|
+
if (result.status === "pending") {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (result.status === "approved" && result.keywayToken) {
|
|
423
|
+
saveAuthToken(result.keywayToken, {
|
|
424
|
+
githubLogin: result.githubLogin,
|
|
425
|
+
expiresAt: result.expiresAt
|
|
426
|
+
});
|
|
427
|
+
trackEvent(AnalyticsEvents.CLI_LOGIN, {
|
|
428
|
+
method: "device",
|
|
429
|
+
repo: repoName
|
|
430
|
+
});
|
|
431
|
+
console.log(chalk.green("\n\u2713 Login successful"));
|
|
432
|
+
if (result.githubLogin) {
|
|
433
|
+
console.log(`Authenticated GitHub user: ${chalk.cyan(result.githubLogin)}`);
|
|
434
|
+
}
|
|
435
|
+
return result.keywayToken;
|
|
436
|
+
}
|
|
437
|
+
throw new Error(result.message || "Authentication failed");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
async function ensureLogin(options = {}) {
|
|
441
|
+
const envToken = process.env.KEYWAY_TOKEN || process.env.GITHUB_TOKEN;
|
|
442
|
+
if (envToken) {
|
|
443
|
+
return envToken;
|
|
444
|
+
}
|
|
445
|
+
const stored = getStoredAuth();
|
|
446
|
+
if (stored?.keywayToken) {
|
|
447
|
+
return stored.keywayToken;
|
|
448
|
+
}
|
|
449
|
+
const allowPrompt = options.allowPrompt !== false;
|
|
450
|
+
const canPrompt = allowPrompt && isInteractive();
|
|
451
|
+
if (!canPrompt) {
|
|
452
|
+
throw new Error('No Keyway session found. Run "keyway login" to authenticate.');
|
|
453
|
+
}
|
|
454
|
+
const proceed = await promptYesNo("No Keyway session found. Open the browser to sign in now? (Y/n) ");
|
|
455
|
+
if (!proceed) {
|
|
456
|
+
throw new Error("Login required. Aborting.");
|
|
457
|
+
}
|
|
458
|
+
return runLoginFlow();
|
|
459
|
+
}
|
|
460
|
+
async function runTokenLogin() {
|
|
461
|
+
const repoName = detectGitRepo();
|
|
462
|
+
if (repoName) {
|
|
463
|
+
console.log(`\u{1F4C1} Detected: ${chalk.cyan(repoName)}`);
|
|
464
|
+
}
|
|
465
|
+
const description = repoName ? `Keyway CLI for ${repoName}` : "Keyway CLI";
|
|
466
|
+
const url = `https://github.com/settings/personal-access-tokens/new?description=${encodeURIComponent(description)}`;
|
|
467
|
+
console.log("Opening GitHub...");
|
|
468
|
+
open(url).catch(() => {
|
|
469
|
+
console.log(chalk.gray(`Open this URL in your browser: ${url}`));
|
|
470
|
+
});
|
|
471
|
+
console.log(chalk.gray("Select the detected repo (or scope manually)."));
|
|
472
|
+
console.log(chalk.gray("Permissions: Metadata \u2192 Read-only; Account permissions: None."));
|
|
473
|
+
const { token } = await prompts(
|
|
474
|
+
{
|
|
475
|
+
type: "password",
|
|
476
|
+
name: "token",
|
|
477
|
+
message: "Paste token:",
|
|
478
|
+
validate: (value) => {
|
|
479
|
+
if (!value || typeof value !== "string") return "Token is required";
|
|
480
|
+
if (!value.startsWith("github_pat_")) return "Token must start with github_pat_";
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
onCancel: () => {
|
|
486
|
+
throw new Error("Login cancelled.");
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
);
|
|
490
|
+
if (!token || typeof token !== "string") {
|
|
491
|
+
throw new Error("Token is required.");
|
|
492
|
+
}
|
|
493
|
+
const trimmedToken = token.trim();
|
|
494
|
+
if (!trimmedToken.startsWith("github_pat_")) {
|
|
495
|
+
throw new Error("Token must start with github_pat_.");
|
|
496
|
+
}
|
|
497
|
+
const validation = await validateToken(trimmedToken);
|
|
498
|
+
saveAuthToken(trimmedToken, {
|
|
499
|
+
githubLogin: validation.username
|
|
500
|
+
});
|
|
501
|
+
trackEvent(AnalyticsEvents.CLI_LOGIN, {
|
|
502
|
+
method: "pat",
|
|
503
|
+
repo: repoName
|
|
504
|
+
});
|
|
505
|
+
console.log(chalk.green("\u2705 Authenticated"), `as ${chalk.cyan(`@${validation.username}`)}`);
|
|
506
|
+
return trimmedToken;
|
|
507
|
+
}
|
|
508
|
+
async function loginCommand(options = {}) {
|
|
509
|
+
try {
|
|
510
|
+
if (options.token) {
|
|
511
|
+
await runTokenLogin();
|
|
512
|
+
} else {
|
|
513
|
+
await runLoginFlow();
|
|
514
|
+
}
|
|
515
|
+
} catch (error) {
|
|
516
|
+
const message = error instanceof Error ? error.message : "Unexpected login error";
|
|
517
|
+
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
518
|
+
command: "login",
|
|
519
|
+
error: message.slice(0, 200)
|
|
520
|
+
});
|
|
521
|
+
console.error(chalk.red(`
|
|
522
|
+
\u2717 ${message}`));
|
|
523
|
+
process.exit(1);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
async function logoutCommand() {
|
|
527
|
+
clearAuth();
|
|
528
|
+
console.log(chalk.green("\u2713 Logged out of Keyway"));
|
|
529
|
+
console.log(chalk.gray(`Auth cache cleared: ${getAuthFilePath()}`));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/cmds/readme.ts
|
|
533
|
+
import fs2 from "fs";
|
|
534
|
+
import path2 from "path";
|
|
535
|
+
import prompts2 from "prompts";
|
|
536
|
+
import chalk2 from "chalk";
|
|
537
|
+
function generateBadge(repo) {
|
|
538
|
+
return `[](https://keyway.sh/repo/${repo})`;
|
|
539
|
+
}
|
|
540
|
+
function insertBadgeIntoReadme(readmeContent, badge) {
|
|
541
|
+
if (readmeContent.includes("keyway.sh/badge.svg")) {
|
|
542
|
+
return readmeContent;
|
|
543
|
+
}
|
|
544
|
+
const lines = readmeContent.split(/\r?\n/);
|
|
545
|
+
const titleIndex = lines.findIndex((line) => /^#\s+/.test(line.trim()));
|
|
546
|
+
if (titleIndex !== -1) {
|
|
547
|
+
const before = lines.slice(0, titleIndex + 1);
|
|
548
|
+
const after = lines.slice(titleIndex + 1);
|
|
549
|
+
const newLines = [...before, "", badge, "", ...after];
|
|
550
|
+
return newLines.join("\n");
|
|
551
|
+
}
|
|
552
|
+
return `${badge}
|
|
553
|
+
|
|
554
|
+
${readmeContent}`;
|
|
555
|
+
}
|
|
556
|
+
function findReadmePath(cwd) {
|
|
557
|
+
const candidates = ["README.md", "readme.md", "Readme.md"];
|
|
558
|
+
for (const candidate of candidates) {
|
|
559
|
+
const candidatePath = path2.join(cwd, candidate);
|
|
560
|
+
if (fs2.existsSync(candidatePath)) {
|
|
561
|
+
return candidatePath;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
async function ensureReadme(repoName, cwd) {
|
|
567
|
+
const existing = findReadmePath(cwd);
|
|
568
|
+
if (existing) return existing;
|
|
569
|
+
const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
|
|
570
|
+
if (!isInteractive2) {
|
|
571
|
+
console.log(chalk2.yellow('No README found. Run "keyway readme add-badge" from a repo with a README.'));
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
const { confirm } = await prompts2(
|
|
575
|
+
{
|
|
576
|
+
type: "confirm",
|
|
577
|
+
name: "confirm",
|
|
578
|
+
message: "No README found. Create a default README.md?",
|
|
579
|
+
initial: false
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
onCancel: () => ({ confirm: false })
|
|
583
|
+
}
|
|
584
|
+
);
|
|
585
|
+
if (!confirm) {
|
|
586
|
+
console.log(chalk2.yellow("Skipping badge insertion (no README)."));
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
const defaultPath = path2.join(cwd, "README.md");
|
|
590
|
+
const content = `# ${repoName}
|
|
591
|
+
|
|
592
|
+
`;
|
|
593
|
+
fs2.writeFileSync(defaultPath, content, "utf-8");
|
|
594
|
+
return defaultPath;
|
|
595
|
+
}
|
|
596
|
+
async function addBadgeToReadme() {
|
|
597
|
+
const repo = detectGitRepo();
|
|
598
|
+
if (!repo) {
|
|
599
|
+
throw new Error("This directory is not a Git repository.");
|
|
600
|
+
}
|
|
601
|
+
const cwd = process.cwd();
|
|
602
|
+
const readmePath = await ensureReadme(repo, cwd);
|
|
603
|
+
if (!readmePath) return;
|
|
604
|
+
const badge = generateBadge(repo);
|
|
605
|
+
const content = fs2.readFileSync(readmePath, "utf-8");
|
|
606
|
+
const updated = insertBadgeIntoReadme(content, badge);
|
|
607
|
+
if (updated === content) {
|
|
608
|
+
console.log(chalk2.gray("Keyway badge already present in README."));
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
fs2.writeFileSync(readmePath, updated, "utf-8");
|
|
612
|
+
console.log(chalk2.green(`\u2713 Keyway badge added to ${path2.basename(readmePath)}`));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// src/cmds/init.ts
|
|
616
|
+
async function initCommand(options = {}) {
|
|
617
|
+
try {
|
|
618
|
+
console.log(chalk3.blue("\u{1F510} Initializing Keyway vault...\n"));
|
|
619
|
+
const repoFullName = getCurrentRepoFullName();
|
|
620
|
+
console.log(`Repository: ${chalk3.cyan(repoFullName)}`);
|
|
621
|
+
const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
|
|
622
|
+
trackEvent(AnalyticsEvents.CLI_INIT, { repoFullName });
|
|
623
|
+
console.log("\nInitializing vault...");
|
|
624
|
+
const response = await initVault(repoFullName, accessToken);
|
|
625
|
+
console.log(chalk3.green("\n\u2713 " + response.message));
|
|
626
|
+
console.log(`
|
|
627
|
+
Vault ID: ${chalk3.gray(response.vaultId)}`);
|
|
628
|
+
console.log("\nNext steps:");
|
|
629
|
+
console.log(` 1. Create a ${chalk3.cyan(".env")} file with your secrets`);
|
|
630
|
+
console.log(` 2. Run ${chalk3.cyan("keyway push")} to upload your secrets`);
|
|
631
|
+
try {
|
|
632
|
+
await addBadgeToReadme();
|
|
633
|
+
} catch (badgeError) {
|
|
634
|
+
console.log(chalk3.yellow("Badge insertion skipped:"), badgeError instanceof Error ? badgeError.message : String(badgeError));
|
|
635
|
+
}
|
|
636
|
+
await shutdownAnalytics();
|
|
637
|
+
} catch (error) {
|
|
638
|
+
const message = error instanceof APIError ? `API ${error.statusCode}: ${error.message}` : error instanceof Error ? error.message.slice(0, 200) : "Unknown error";
|
|
639
|
+
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
640
|
+
command: "init",
|
|
641
|
+
error: message
|
|
642
|
+
});
|
|
643
|
+
await shutdownAnalytics();
|
|
644
|
+
console.error(chalk3.red(`
|
|
645
|
+
\u2717 Error: ${message}`));
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/cmds/push.ts
|
|
651
|
+
import chalk4 from "chalk";
|
|
652
|
+
import fs3 from "fs";
|
|
653
|
+
import path3 from "path";
|
|
654
|
+
import prompts3 from "prompts";
|
|
655
|
+
function deriveEnvFromFile(file) {
|
|
656
|
+
const base = path3.basename(file);
|
|
657
|
+
const match = base.match(/\.env(?:\.(.+))?$/);
|
|
658
|
+
if (match) {
|
|
659
|
+
return match[1] || "development";
|
|
660
|
+
}
|
|
661
|
+
return "development";
|
|
662
|
+
}
|
|
663
|
+
function discoverEnvCandidates(cwd) {
|
|
664
|
+
try {
|
|
665
|
+
const entries = fs3.readdirSync(cwd);
|
|
666
|
+
const hasEnvLocal = entries.includes(".env.local");
|
|
667
|
+
if (hasEnvLocal) {
|
|
668
|
+
console.log(chalk4.gray("\u2139\uFE0F Detected .env.local \u2014 not synced by design (machine-specific secrets)"));
|
|
669
|
+
}
|
|
670
|
+
const candidates = entries.filter((name) => name.startsWith(".env") && name !== ".env.local").map((name) => {
|
|
671
|
+
const fullPath = path3.join(cwd, name);
|
|
672
|
+
try {
|
|
673
|
+
const stat = fs3.statSync(fullPath);
|
|
674
|
+
if (!stat.isFile()) return null;
|
|
675
|
+
return { file: name, env: deriveEnvFromFile(name) };
|
|
676
|
+
} catch {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
}).filter((c) => Boolean(c));
|
|
680
|
+
const seen = /* @__PURE__ */ new Set();
|
|
681
|
+
const unique = [];
|
|
682
|
+
for (const c of candidates) {
|
|
683
|
+
if (seen.has(c.file)) continue;
|
|
684
|
+
seen.add(c.file);
|
|
685
|
+
unique.push(c);
|
|
686
|
+
}
|
|
687
|
+
return unique;
|
|
688
|
+
} catch {
|
|
689
|
+
return [];
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
async function pushCommand(options) {
|
|
693
|
+
try {
|
|
694
|
+
console.log(chalk4.blue("\u{1F510} Pushing secrets to Keyway...\n"));
|
|
695
|
+
const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
|
|
696
|
+
let environment = options.env;
|
|
697
|
+
let envFile = options.file;
|
|
698
|
+
const candidates = discoverEnvCandidates(process.cwd());
|
|
699
|
+
if (environment && !envFile) {
|
|
700
|
+
const match = candidates.find((c) => c.env === environment);
|
|
48
701
|
if (match) {
|
|
49
|
-
|
|
702
|
+
envFile = match.file;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (!environment && !envFile && isInteractive2 && candidates.length > 0) {
|
|
706
|
+
const { choice } = await prompts3(
|
|
707
|
+
{
|
|
708
|
+
type: "select",
|
|
709
|
+
name: "choice",
|
|
710
|
+
message: "Select an env file to push:",
|
|
711
|
+
choices: [
|
|
712
|
+
...candidates.map((c) => ({
|
|
713
|
+
title: `${c.file} (env: ${c.env})`,
|
|
714
|
+
value: c
|
|
715
|
+
})),
|
|
716
|
+
{ title: "Enter a different file...", value: "custom" }
|
|
717
|
+
]
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
onCancel: () => {
|
|
721
|
+
throw new Error("Push cancelled by user.");
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
);
|
|
725
|
+
if (choice && choice !== "custom") {
|
|
726
|
+
envFile = choice.file;
|
|
727
|
+
environment = choice.env;
|
|
728
|
+
} else if (choice === "custom") {
|
|
729
|
+
const { fileInput } = await prompts3(
|
|
730
|
+
{
|
|
731
|
+
type: "text",
|
|
732
|
+
name: "fileInput",
|
|
733
|
+
message: "Path to env file:",
|
|
734
|
+
validate: (value) => {
|
|
735
|
+
if (!value) return "Path is required";
|
|
736
|
+
const resolved = path3.resolve(process.cwd(), value);
|
|
737
|
+
if (!fs3.existsSync(resolved)) return `File not found: ${value}`;
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
onCancel: () => {
|
|
743
|
+
throw new Error("Push cancelled by user.");
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
);
|
|
747
|
+
envFile = fileInput;
|
|
748
|
+
environment = deriveEnvFromFile(fileInput);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (!environment) {
|
|
752
|
+
environment = "development";
|
|
753
|
+
}
|
|
754
|
+
if (!envFile) {
|
|
755
|
+
envFile = ".env";
|
|
756
|
+
}
|
|
757
|
+
let envFilePath = path3.resolve(process.cwd(), envFile);
|
|
758
|
+
if (!fs3.existsSync(envFilePath)) {
|
|
759
|
+
if (!isInteractive2) {
|
|
760
|
+
throw new Error(`File not found: ${envFile}. Provide --file <path> or run interactively to choose a file.`);
|
|
761
|
+
}
|
|
762
|
+
const { newPath } = await prompts3(
|
|
763
|
+
{
|
|
764
|
+
type: "text",
|
|
765
|
+
name: "newPath",
|
|
766
|
+
message: `File not found: ${envFile}. Enter an env file path to use:`,
|
|
767
|
+
validate: (value) => {
|
|
768
|
+
if (!value || typeof value !== "string") return "Path is required";
|
|
769
|
+
const resolved = path3.resolve(process.cwd(), value);
|
|
770
|
+
if (!fs3.existsSync(resolved)) return `File not found: ${value}`;
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
onCancel: () => {
|
|
776
|
+
throw new Error("Push cancelled (no env file provided).");
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
);
|
|
780
|
+
if (!newPath || typeof newPath !== "string") {
|
|
781
|
+
throw new Error("Push cancelled (no env file provided).");
|
|
782
|
+
}
|
|
783
|
+
envFile = newPath.trim();
|
|
784
|
+
envFilePath = path3.resolve(process.cwd(), envFile);
|
|
785
|
+
}
|
|
786
|
+
const content = fs3.readFileSync(envFilePath, "utf-8");
|
|
787
|
+
if (content.trim().length === 0) {
|
|
788
|
+
throw new Error(`File is empty: ${envFile}`);
|
|
789
|
+
}
|
|
790
|
+
const lines = content.split("\n").filter((line) => {
|
|
791
|
+
const trimmed = line.trim();
|
|
792
|
+
return trimmed.length > 0 && !trimmed.startsWith("#");
|
|
793
|
+
});
|
|
794
|
+
console.log(`File: ${chalk4.cyan(envFile)}`);
|
|
795
|
+
console.log(`Environment: ${chalk4.cyan(environment)}`);
|
|
796
|
+
console.log(`Variables: ${chalk4.cyan(lines.length.toString())}`);
|
|
797
|
+
const repoFullName = getCurrentRepoFullName();
|
|
798
|
+
console.log(`Repository: ${chalk4.cyan(repoFullName)}`);
|
|
799
|
+
if (!options.yes) {
|
|
800
|
+
const isInteractive3 = process.stdin.isTTY && process.stdout.isTTY;
|
|
801
|
+
if (!isInteractive3) {
|
|
802
|
+
throw new Error("Confirmation required. Re-run with --yes in non-interactive environments.");
|
|
803
|
+
}
|
|
804
|
+
const { confirm } = await prompts3(
|
|
805
|
+
{
|
|
806
|
+
type: "confirm",
|
|
807
|
+
name: "confirm",
|
|
808
|
+
message: `Send ${lines.length} secrets from ${envFile} (env: ${environment}) to ${repoFullName}?`,
|
|
809
|
+
initial: true
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
onCancel: () => {
|
|
813
|
+
throw new Error("Push cancelled by user.");
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
);
|
|
817
|
+
if (!confirm) {
|
|
818
|
+
console.log(chalk4.yellow("Push aborted."));
|
|
819
|
+
return;
|
|
50
820
|
}
|
|
51
821
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
822
|
+
const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
|
|
823
|
+
trackEvent(AnalyticsEvents.CLI_PUSH, {
|
|
824
|
+
repoFullName,
|
|
825
|
+
environment,
|
|
826
|
+
variableCount: lines.length
|
|
827
|
+
});
|
|
828
|
+
console.log("\nUploading secrets...");
|
|
829
|
+
const response = await pushSecrets(repoFullName, environment, content, accessToken);
|
|
830
|
+
console.log(chalk4.green("\n\u2713 " + response.message));
|
|
831
|
+
console.log(`
|
|
832
|
+
Your secrets are now encrypted and stored securely.`);
|
|
833
|
+
console.log(`To retrieve them, run: ${chalk4.cyan(`keyway pull --env ${environment}`)}`);
|
|
834
|
+
await shutdownAnalytics();
|
|
835
|
+
} catch (error) {
|
|
836
|
+
const message = error instanceof APIError ? `API ${error.statusCode}: ${error.message}` : error instanceof Error ? error.message.slice(0, 200) : "Unknown error";
|
|
837
|
+
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
838
|
+
command: "push",
|
|
839
|
+
error: message
|
|
840
|
+
});
|
|
841
|
+
await shutdownAnalytics();
|
|
842
|
+
console.error(chalk4.red(`
|
|
843
|
+
\u2717 Error: ${message}`));
|
|
844
|
+
process.exit(1);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// src/cmds/pull.ts
|
|
849
|
+
import chalk5 from "chalk";
|
|
850
|
+
import fs4 from "fs";
|
|
851
|
+
import path4 from "path";
|
|
852
|
+
import prompts4 from "prompts";
|
|
853
|
+
async function pullCommand(options) {
|
|
854
|
+
try {
|
|
855
|
+
const environment = options.env || "development";
|
|
856
|
+
const envFile = options.file || ".env";
|
|
857
|
+
console.log(chalk5.blue("\u{1F510} Pulling secrets from Keyway...\n"));
|
|
858
|
+
console.log(`Environment: ${chalk5.cyan(environment)}`);
|
|
859
|
+
const repoFullName = getCurrentRepoFullName();
|
|
860
|
+
console.log(`Repository: ${chalk5.cyan(repoFullName)}`);
|
|
861
|
+
const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
|
|
862
|
+
trackEvent(AnalyticsEvents.CLI_PULL, {
|
|
863
|
+
repoFullName,
|
|
864
|
+
environment
|
|
865
|
+
});
|
|
866
|
+
console.log("\nDownloading secrets...");
|
|
867
|
+
const response = await pullSecrets(repoFullName, environment, accessToken);
|
|
868
|
+
const envFilePath = path4.resolve(process.cwd(), envFile);
|
|
869
|
+
if (fs4.existsSync(envFilePath)) {
|
|
870
|
+
const isInteractive2 = process.stdin.isTTY && process.stdout.isTTY;
|
|
871
|
+
if (options.yes) {
|
|
872
|
+
console.log(chalk5.yellow(`
|
|
873
|
+
\u26A0 Overwriting existing file: ${envFile}`));
|
|
874
|
+
} else if (!isInteractive2) {
|
|
875
|
+
throw new Error(`File ${envFile} exists. Re-run with --yes to overwrite or choose a different --file.`);
|
|
876
|
+
} else {
|
|
877
|
+
const { confirm } = await prompts4(
|
|
878
|
+
{
|
|
879
|
+
type: "confirm",
|
|
880
|
+
name: "confirm",
|
|
881
|
+
message: `${envFile} exists. Overwrite with secrets from ${environment}?`,
|
|
882
|
+
initial: false
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
onCancel: () => {
|
|
886
|
+
throw new Error("Pull cancelled by user.");
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
);
|
|
890
|
+
if (!confirm) {
|
|
891
|
+
console.log(chalk5.yellow("Pull aborted."));
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
63
895
|
}
|
|
896
|
+
fs4.writeFileSync(envFilePath, response.content, "utf-8");
|
|
897
|
+
const lines = response.content.split("\n").filter((line) => {
|
|
898
|
+
const trimmed = line.trim();
|
|
899
|
+
return trimmed.length > 0 && !trimmed.startsWith("#");
|
|
900
|
+
});
|
|
901
|
+
console.log(chalk5.green(`
|
|
902
|
+
\u2713 Secrets downloaded successfully`));
|
|
903
|
+
console.log(`
|
|
904
|
+
File: ${chalk5.cyan(envFile)}`);
|
|
905
|
+
console.log(`Variables: ${chalk5.cyan(lines.length.toString())}`);
|
|
906
|
+
await shutdownAnalytics();
|
|
907
|
+
} catch (error) {
|
|
908
|
+
const message = error instanceof APIError ? `API ${error.statusCode}: ${error.message}` : error instanceof Error ? error.message.slice(0, 200) : "Unknown error";
|
|
909
|
+
trackEvent(AnalyticsEvents.CLI_ERROR, {
|
|
910
|
+
command: "pull",
|
|
911
|
+
error: message
|
|
912
|
+
});
|
|
913
|
+
await shutdownAnalytics();
|
|
914
|
+
console.error(chalk5.red(`
|
|
915
|
+
\u2717 Error: ${message}`));
|
|
916
|
+
process.exit(1);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// src/cmds/doctor.ts
|
|
921
|
+
import chalk6 from "chalk";
|
|
922
|
+
|
|
923
|
+
// src/core/doctor.ts
|
|
924
|
+
import { execSync as execSync2 } from "child_process";
|
|
925
|
+
import { writeFileSync, unlinkSync, readFileSync, existsSync } from "fs";
|
|
926
|
+
import { tmpdir } from "os";
|
|
927
|
+
import { join } from "path";
|
|
928
|
+
var API_HEALTH_URL = `${process.env.KEYWAY_API_URL || INTERNAL_API_URL}/`;
|
|
929
|
+
async function checkNode() {
|
|
930
|
+
const nodeVersion = process.versions.node;
|
|
931
|
+
const [major] = nodeVersion.split(".").map(Number);
|
|
932
|
+
if (major >= 18) {
|
|
933
|
+
return {
|
|
934
|
+
id: "node",
|
|
935
|
+
name: "Node.js version",
|
|
936
|
+
status: "pass",
|
|
937
|
+
detail: `v${nodeVersion} (>=18.0.0 required)`
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
return {
|
|
941
|
+
id: "node",
|
|
942
|
+
name: "Node.js version",
|
|
943
|
+
status: "fail",
|
|
944
|
+
detail: `v${nodeVersion} (<18.0.0, please upgrade)`
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
async function checkGit() {
|
|
948
|
+
try {
|
|
949
|
+
const gitVersion = execSync2("git --version", { encoding: "utf-8" }).trim();
|
|
950
|
+
try {
|
|
951
|
+
execSync2("git rev-parse --is-inside-work-tree", {
|
|
952
|
+
encoding: "utf-8",
|
|
953
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
954
|
+
});
|
|
955
|
+
return {
|
|
956
|
+
id: "git",
|
|
957
|
+
name: "Git repository",
|
|
958
|
+
status: "pass",
|
|
959
|
+
detail: `${gitVersion} - inside repository`
|
|
960
|
+
};
|
|
961
|
+
} catch {
|
|
962
|
+
return {
|
|
963
|
+
id: "git",
|
|
964
|
+
name: "Git repository",
|
|
965
|
+
status: "warn",
|
|
966
|
+
detail: `${gitVersion} - not in a repository`
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
} catch {
|
|
970
|
+
return {
|
|
971
|
+
id: "git",
|
|
972
|
+
name: "Git repository",
|
|
973
|
+
status: "warn",
|
|
974
|
+
detail: "Git not installed"
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
async function checkNetwork() {
|
|
979
|
+
const fetchFn = globalThis.fetch;
|
|
980
|
+
if (!fetchFn) {
|
|
981
|
+
return {
|
|
982
|
+
id: "network",
|
|
983
|
+
name: "API connectivity",
|
|
984
|
+
status: "warn",
|
|
985
|
+
detail: "Fetch API not available in this Node.js runtime"
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
try {
|
|
989
|
+
const controller = new AbortController();
|
|
990
|
+
const timeout = setTimeout(() => controller.abort(), 2e3);
|
|
991
|
+
const response = await fetchFn(API_HEALTH_URL, {
|
|
992
|
+
method: "HEAD",
|
|
993
|
+
signal: controller.signal
|
|
994
|
+
});
|
|
995
|
+
clearTimeout(timeout);
|
|
996
|
+
if (response.ok || response.status < 500) {
|
|
997
|
+
return {
|
|
998
|
+
id: "network",
|
|
999
|
+
name: "API connectivity",
|
|
1000
|
+
status: "pass",
|
|
1001
|
+
detail: `Connected to ${API_HEALTH_URL}`
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
return {
|
|
1005
|
+
id: "network",
|
|
1006
|
+
name: "API connectivity",
|
|
1007
|
+
status: "warn",
|
|
1008
|
+
detail: `Server returned ${response.status}`
|
|
1009
|
+
};
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
if (error.name === "AbortError") {
|
|
1012
|
+
return {
|
|
1013
|
+
id: "network",
|
|
1014
|
+
name: "API connectivity",
|
|
1015
|
+
status: "warn",
|
|
1016
|
+
detail: "Connection timeout (>2s)"
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
if (error.code === "ENOTFOUND") {
|
|
1020
|
+
return {
|
|
1021
|
+
id: "network",
|
|
1022
|
+
name: "API connectivity",
|
|
1023
|
+
status: "fail",
|
|
1024
|
+
detail: "DNS resolution failed"
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
if (error.code === "CERT_HAS_EXPIRED" || error.code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") {
|
|
1028
|
+
return {
|
|
1029
|
+
id: "network",
|
|
1030
|
+
name: "API connectivity",
|
|
1031
|
+
status: "fail",
|
|
1032
|
+
detail: "SSL certificate error"
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
return {
|
|
1036
|
+
id: "network",
|
|
1037
|
+
name: "API connectivity",
|
|
1038
|
+
status: "warn",
|
|
1039
|
+
detail: error.message || "Connection failed"
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
async function checkFileSystem() {
|
|
1044
|
+
const testFile = join(tmpdir(), `.keyway-test-${Date.now()}.tmp`);
|
|
1045
|
+
try {
|
|
1046
|
+
writeFileSync(testFile, "test");
|
|
1047
|
+
unlinkSync(testFile);
|
|
1048
|
+
return {
|
|
1049
|
+
id: "filesystem",
|
|
1050
|
+
name: "File system permissions",
|
|
1051
|
+
status: "pass",
|
|
1052
|
+
detail: "Write permissions verified"
|
|
1053
|
+
};
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
return {
|
|
1056
|
+
id: "filesystem",
|
|
1057
|
+
name: "File system permissions",
|
|
1058
|
+
status: "fail",
|
|
1059
|
+
detail: `Cannot write to temp directory: ${error.message}`
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
async function checkGitignore() {
|
|
1064
|
+
try {
|
|
1065
|
+
if (!existsSync(".gitignore")) {
|
|
1066
|
+
return {
|
|
1067
|
+
id: "gitignore",
|
|
1068
|
+
name: ".gitignore configuration",
|
|
1069
|
+
status: "warn",
|
|
1070
|
+
detail: "No .gitignore file found"
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
const gitignoreContent = readFileSync(".gitignore", "utf-8");
|
|
1074
|
+
const hasEnvPattern = gitignoreContent.includes("*.env") || gitignoreContent.includes(".env*");
|
|
1075
|
+
const hasDotEnv = gitignoreContent.includes(".env");
|
|
1076
|
+
if (hasEnvPattern || hasDotEnv) {
|
|
1077
|
+
return {
|
|
1078
|
+
id: "gitignore",
|
|
1079
|
+
name: ".gitignore configuration",
|
|
1080
|
+
status: "pass",
|
|
1081
|
+
detail: "Environment files are ignored"
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
return {
|
|
1085
|
+
id: "gitignore",
|
|
1086
|
+
name: ".gitignore configuration",
|
|
1087
|
+
status: "warn",
|
|
1088
|
+
detail: "Missing .env patterns in .gitignore"
|
|
1089
|
+
};
|
|
1090
|
+
} catch {
|
|
1091
|
+
return {
|
|
1092
|
+
id: "gitignore",
|
|
1093
|
+
name: ".gitignore configuration",
|
|
1094
|
+
status: "warn",
|
|
1095
|
+
detail: "Could not read .gitignore"
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
async function checkSystemClock() {
|
|
1100
|
+
try {
|
|
1101
|
+
const controller = new AbortController();
|
|
1102
|
+
const timeout = setTimeout(() => controller.abort(), 2e3);
|
|
1103
|
+
const response = await fetch("https://api.keyway.sh/", {
|
|
1104
|
+
method: "HEAD",
|
|
1105
|
+
signal: controller.signal
|
|
1106
|
+
});
|
|
1107
|
+
clearTimeout(timeout);
|
|
1108
|
+
const serverDate = response.headers.get("date");
|
|
1109
|
+
if (!serverDate) {
|
|
1110
|
+
return {
|
|
1111
|
+
id: "clock",
|
|
1112
|
+
name: "System clock",
|
|
1113
|
+
status: "pass",
|
|
1114
|
+
detail: "Unable to verify (no server date)"
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
const serverTime = new Date(serverDate).getTime();
|
|
1118
|
+
const localTime = Date.now();
|
|
1119
|
+
const diffMinutes = Math.abs(serverTime - localTime) / 1e3 / 60;
|
|
1120
|
+
if (diffMinutes < 5) {
|
|
1121
|
+
return {
|
|
1122
|
+
id: "clock",
|
|
1123
|
+
name: "System clock",
|
|
1124
|
+
status: "pass",
|
|
1125
|
+
detail: `Synchronized (drift: ${Math.round(diffMinutes * 60)}s)`
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
return {
|
|
1129
|
+
id: "clock",
|
|
1130
|
+
name: "System clock",
|
|
1131
|
+
status: "warn",
|
|
1132
|
+
detail: `Clock drift: ${Math.round(diffMinutes)} minutes`
|
|
1133
|
+
};
|
|
64
1134
|
} catch {
|
|
65
|
-
|
|
66
|
-
|
|
1135
|
+
return {
|
|
1136
|
+
id: "clock",
|
|
1137
|
+
name: "System clock",
|
|
1138
|
+
status: "pass",
|
|
1139
|
+
detail: "Unable to verify"
|
|
1140
|
+
};
|
|
67
1141
|
}
|
|
1142
|
+
}
|
|
1143
|
+
async function runAllChecks(options = {}) {
|
|
1144
|
+
const checks = await Promise.all([
|
|
1145
|
+
checkNode(),
|
|
1146
|
+
checkGit(),
|
|
1147
|
+
checkNetwork(),
|
|
1148
|
+
checkFileSystem(),
|
|
1149
|
+
checkGitignore(),
|
|
1150
|
+
checkSystemClock()
|
|
1151
|
+
]);
|
|
1152
|
+
if (options.strict) {
|
|
1153
|
+
checks.forEach((check) => {
|
|
1154
|
+
if (check.status === "warn") {
|
|
1155
|
+
check.status = "fail";
|
|
1156
|
+
}
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
const summary = {
|
|
1160
|
+
pass: checks.filter((c) => c.status === "pass").length,
|
|
1161
|
+
warn: checks.filter((c) => c.status === "warn").length,
|
|
1162
|
+
fail: checks.filter((c) => c.status === "fail").length
|
|
1163
|
+
};
|
|
1164
|
+
const exitCode = summary.fail > 0 ? 1 : 0;
|
|
1165
|
+
return {
|
|
1166
|
+
checks,
|
|
1167
|
+
summary,
|
|
1168
|
+
exitCode
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// src/cmds/doctor.ts
|
|
1173
|
+
function formatSummary(results) {
|
|
1174
|
+
const parts = [
|
|
1175
|
+
chalk6.green(`${results.summary.pass} passed`),
|
|
1176
|
+
results.summary.warn > 0 ? chalk6.yellow(`${results.summary.warn} warnings`) : null,
|
|
1177
|
+
results.summary.fail > 0 ? chalk6.red(`${results.summary.fail} failed`) : null
|
|
1178
|
+
].filter(Boolean);
|
|
1179
|
+
return parts.join(", ");
|
|
1180
|
+
}
|
|
1181
|
+
async function doctorCommand(options = {}) {
|
|
1182
|
+
try {
|
|
1183
|
+
const results = await runAllChecks({ strict: !!options.strict });
|
|
1184
|
+
trackEvent(AnalyticsEvents.CLI_DOCTOR, {
|
|
1185
|
+
pass: results.summary.pass,
|
|
1186
|
+
warn: results.summary.warn,
|
|
1187
|
+
fail: results.summary.fail,
|
|
1188
|
+
strict: !!options.strict
|
|
1189
|
+
});
|
|
1190
|
+
if (options.json) {
|
|
1191
|
+
process.stdout.write(JSON.stringify(results, null, 0) + "\n");
|
|
1192
|
+
process.exit(results.exitCode);
|
|
1193
|
+
}
|
|
1194
|
+
console.log(chalk6.cyan("\n\u{1F50D} Keyway Doctor - Environment Check\n"));
|
|
1195
|
+
results.checks.forEach((check) => {
|
|
1196
|
+
const icon = check.status === "pass" ? chalk6.green("\u2713") : check.status === "warn" ? chalk6.yellow("!") : chalk6.red("\u2717");
|
|
1197
|
+
const detail = check.detail ? chalk6.dim(` \u2014 ${check.detail}`) : "";
|
|
1198
|
+
console.log(` ${icon} ${check.name}${detail}`);
|
|
1199
|
+
});
|
|
1200
|
+
console.log(`
|
|
1201
|
+
Summary: ${formatSummary(results)}`);
|
|
1202
|
+
if (results.summary.fail > 0) {
|
|
1203
|
+
console.log(chalk6.red("\u26A0 Some checks failed. Please resolve the issues above before using Keyway."));
|
|
1204
|
+
} else if (results.summary.warn > 0) {
|
|
1205
|
+
console.log(chalk6.yellow("\u26A0 Some warnings detected. Keyway should work but consider addressing them."));
|
|
1206
|
+
} else {
|
|
1207
|
+
console.log(chalk6.green("\u2728 All checks passed! Your environment is ready for Keyway."));
|
|
1208
|
+
}
|
|
1209
|
+
process.exit(results.exitCode);
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
const message = error instanceof Error ? error.message : "Doctor failed";
|
|
1212
|
+
trackEvent(AnalyticsEvents.CLI_DOCTOR, {
|
|
1213
|
+
pass: 0,
|
|
1214
|
+
warn: 0,
|
|
1215
|
+
fail: 1,
|
|
1216
|
+
strict: !!options.strict,
|
|
1217
|
+
error: "doctor_failed"
|
|
1218
|
+
});
|
|
1219
|
+
if (options.json) {
|
|
1220
|
+
const errorResult = {
|
|
1221
|
+
checks: [],
|
|
1222
|
+
summary: { pass: 0, warn: 0, fail: 1 },
|
|
1223
|
+
exitCode: 1,
|
|
1224
|
+
error: message
|
|
1225
|
+
};
|
|
1226
|
+
process.stdout.write(JSON.stringify(errorResult, null, 0) + "\n");
|
|
1227
|
+
} else {
|
|
1228
|
+
console.error(chalk6.red(`\u2716 Doctor check failed: ${message}`));
|
|
1229
|
+
}
|
|
1230
|
+
process.exit(1);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// package.json with { type: 'json' }
|
|
1235
|
+
var package_default2 = {
|
|
1236
|
+
name: "@keywaysh/cli",
|
|
1237
|
+
version: "0.0.3",
|
|
1238
|
+
description: "One link to all your secrets",
|
|
1239
|
+
type: "module",
|
|
1240
|
+
bin: {
|
|
1241
|
+
keyway: "./dist/cli.js"
|
|
1242
|
+
},
|
|
1243
|
+
main: "./dist/cli.js",
|
|
1244
|
+
files: [
|
|
1245
|
+
"dist"
|
|
1246
|
+
],
|
|
1247
|
+
scripts: {
|
|
1248
|
+
dev: "pnpm exec tsx src/cli.ts",
|
|
1249
|
+
build: "pnpm exec tsup",
|
|
1250
|
+
"build:watch": "pnpm exec tsup --watch",
|
|
1251
|
+
prepublishOnly: "pnpm run build",
|
|
1252
|
+
test: "pnpm exec vitest run",
|
|
1253
|
+
"test:watch": "pnpm exec vitest"
|
|
1254
|
+
},
|
|
1255
|
+
keywords: [
|
|
1256
|
+
"secrets",
|
|
1257
|
+
"env",
|
|
1258
|
+
"keyway",
|
|
1259
|
+
"cli",
|
|
1260
|
+
"devops"
|
|
1261
|
+
],
|
|
1262
|
+
author: "Nicolas Ritouet",
|
|
1263
|
+
license: "MIT",
|
|
1264
|
+
homepage: "https://keyway.sh",
|
|
1265
|
+
repository: {
|
|
1266
|
+
type: "git",
|
|
1267
|
+
url: "https://github.com/keywaysh/cli.git"
|
|
1268
|
+
},
|
|
1269
|
+
bugs: {
|
|
1270
|
+
url: "https://github.com/keywaysh/cli/issues"
|
|
1271
|
+
},
|
|
1272
|
+
packageManager: "pnpm@10.6.1",
|
|
1273
|
+
engines: {
|
|
1274
|
+
node: ">=18.0.0"
|
|
1275
|
+
},
|
|
1276
|
+
dependencies: {
|
|
1277
|
+
chalk: "^4.1.2",
|
|
1278
|
+
commander: "^14.0.0",
|
|
1279
|
+
conf: "^15.0.2",
|
|
1280
|
+
open: "^11.0.0",
|
|
1281
|
+
"posthog-node": "^3.5.0",
|
|
1282
|
+
prompts: "^2.4.2"
|
|
1283
|
+
},
|
|
1284
|
+
devDependencies: {
|
|
1285
|
+
"@types/node": "^24.2.0",
|
|
1286
|
+
"@types/prompts": "^2.4.9",
|
|
1287
|
+
tsup: "^8.5.0",
|
|
1288
|
+
tsx: "^4.20.3",
|
|
1289
|
+
typescript: "^5.9.2",
|
|
1290
|
+
vitest: "^3.2.4"
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1294
|
+
// src/cli.ts
|
|
1295
|
+
var program = new Command();
|
|
1296
|
+
var shouldShowBanner = () => {
|
|
1297
|
+
if (process.env.KEYWAY_NO_BANNER === "1") return false;
|
|
1298
|
+
const argv = process.argv.slice(2);
|
|
1299
|
+
return !argv.includes("--no-banner") && argv.length > 0;
|
|
1300
|
+
};
|
|
1301
|
+
var showBanner = () => {
|
|
1302
|
+
const text = chalk7.cyan.bold("Keyway CLI");
|
|
1303
|
+
const subtitle = chalk7.gray("GitHub-native secrets manager for dev teams");
|
|
1304
|
+
console.log(`
|
|
1305
|
+
${text}
|
|
1306
|
+
${subtitle}
|
|
1307
|
+
`);
|
|
1308
|
+
};
|
|
1309
|
+
if (shouldShowBanner()) {
|
|
1310
|
+
showBanner();
|
|
1311
|
+
}
|
|
1312
|
+
program.name("keyway").description("GitHub-native secrets manager for dev teams").version(package_default2.version).option("--no-banner", "Disable the startup banner");
|
|
1313
|
+
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) => {
|
|
1314
|
+
await initCommand(options);
|
|
68
1315
|
});
|
|
69
|
-
program.command("
|
|
70
|
-
|
|
71
|
-
console.log("How it will work:\n");
|
|
72
|
-
console.log(import_chalk.default.white("1. Initialize your project:"));
|
|
73
|
-
console.log(import_chalk.default.gray(" $ ") + import_chalk.default.green("keyway init"));
|
|
74
|
-
console.log(import_chalk.default.gray(" \u2713 Vault created at https://keyway.sh/your/repo\n"));
|
|
75
|
-
console.log(import_chalk.default.white("2. Add the link to your README:"));
|
|
76
|
-
console.log(import_chalk.default.gray(" ## \u{1F511} Secrets"));
|
|
77
|
-
console.log(import_chalk.default.gray(" Access vault: https://keyway.sh/your/repo\n"));
|
|
78
|
-
console.log(import_chalk.default.white("3. Team members pull secrets:"));
|
|
79
|
-
console.log(import_chalk.default.gray(" $ ") + import_chalk.default.green("keyway pull"));
|
|
80
|
-
console.log(import_chalk.default.gray(" \u2713 Authenticated via GitHub"));
|
|
81
|
-
console.log(import_chalk.default.gray(" \u2713 Pulled 23 secrets in 12ms\n"));
|
|
82
|
-
console.log(import_chalk.default.white("4. That's it! No more:"));
|
|
83
|
-
console.log(import_chalk.default.gray(' \u274C "Can you send me the .env file?"'));
|
|
84
|
-
console.log(import_chalk.default.gray(" \u274C API keys in Slack"));
|
|
85
|
-
console.log(import_chalk.default.gray(" \u274C Outdated credentials"));
|
|
86
|
-
console.log(import_chalk.default.gray(" \u274C Onboarding delays\n"));
|
|
87
|
-
console.log(import_chalk.default.cyan("Ready to simplify your secret management?"));
|
|
88
|
-
console.log(import_chalk.default.white("Get early access: ") + import_chalk.default.underline("https://keyway.sh"));
|
|
1316
|
+
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) => {
|
|
1317
|
+
await pushCommand(options);
|
|
89
1318
|
});
|
|
90
|
-
program.command("
|
|
91
|
-
|
|
92
|
-
console.log("Get early access at: " + import_chalk.default.underline("https://keyway.sh"));
|
|
93
|
-
console.log();
|
|
94
|
-
console.log("Or email us directly: " + import_chalk.default.underline("unlock@keyway.sh"));
|
|
95
|
-
console.log();
|
|
96
|
-
console.log(import_chalk.default.gray("We'll notify you as soon as Keyway is ready!"));
|
|
1319
|
+
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) => {
|
|
1320
|
+
await pullCommand(options);
|
|
97
1321
|
});
|
|
98
|
-
program.command("
|
|
99
|
-
|
|
100
|
-
console.log(import_chalk.default.white("The Problem:"));
|
|
101
|
-
console.log(" \u2022 Secrets scattered across Slack, email, and docs");
|
|
102
|
-
console.log(" \u2022 New developer onboarding takes hours");
|
|
103
|
-
console.log(" \u2022 No single source of truth for env variables");
|
|
104
|
-
console.log(" \u2022 Complex solutions like HashiCorp Vault are overkill\n");
|
|
105
|
-
console.log(import_chalk.default.white("Our Solution:"));
|
|
106
|
-
console.log(" \u2022 One link in your README");
|
|
107
|
-
console.log(" \u2022 GitHub access = vault access");
|
|
108
|
-
console.log(" \u2022 Zero-trust architecture");
|
|
109
|
-
console.log(" \u2022 12ms to pull all secrets\n");
|
|
110
|
-
console.log(import_chalk.default.white("Built for:"));
|
|
111
|
-
console.log(" \u2022 Small to medium dev teams");
|
|
112
|
-
console.log(" \u2022 Projects with multiple environments");
|
|
113
|
-
console.log(" \u2022 Teams tired of complexity\n");
|
|
114
|
-
console.log(import_chalk.default.gray("Learn more at https://keyway.sh"));
|
|
1322
|
+
program.command("login").description("Authenticate with GitHub via Keyway").option("--token", "Authenticate using a GitHub fine-grained PAT").action(async (options) => {
|
|
1323
|
+
await loginCommand(options);
|
|
115
1324
|
});
|
|
116
|
-
program.command("
|
|
117
|
-
|
|
118
|
-
console.log(import_chalk.default.cyan("\n\u{1F4EC} Thank you for your feedback!\n"));
|
|
119
|
-
console.log("Your message: " + import_chalk.default.italic(message.join(" ")));
|
|
120
|
-
console.log();
|
|
121
|
-
console.log("Please email it to: " + import_chalk.default.underline("unlock@keyway.sh"));
|
|
122
|
-
console.log(import_chalk.default.gray("We read every message!"));
|
|
123
|
-
} else {
|
|
124
|
-
console.log(import_chalk.default.cyan("\n\u{1F4EC} We'd love your feedback!\n"));
|
|
125
|
-
console.log("Email us at: " + import_chalk.default.underline("unlock@keyway.sh"));
|
|
126
|
-
console.log();
|
|
127
|
-
console.log("Or use: " + import_chalk.default.gray('keyway feedback "your message here"'));
|
|
128
|
-
}
|
|
1325
|
+
program.command("logout").description("Clear stored Keyway credentials").action(async () => {
|
|
1326
|
+
await logoutCommand();
|
|
129
1327
|
});
|
|
130
|
-
program.command("
|
|
131
|
-
|
|
132
|
-
|
|
1328
|
+
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) => {
|
|
1329
|
+
await doctorCommand(options);
|
|
1330
|
+
});
|
|
1331
|
+
program.command("readme").description("README utilities").command("add-badge").description("Insert the Keyway badge into README").action(async () => {
|
|
1332
|
+
await addBadgeToReadme();
|
|
1333
|
+
});
|
|
1334
|
+
program.parseAsync().catch((error) => {
|
|
1335
|
+
console.error(chalk7.red("Error:"), error.message || error);
|
|
1336
|
+
process.exit(1);
|
|
133
1337
|
});
|
|
134
|
-
program.parse();
|
|
135
|
-
if (!process.argv.slice(2).length) {
|
|
136
|
-
console.log(COMING_SOON_MESSAGE);
|
|
137
|
-
console.log(import_chalk.default.gray("Try: keyway demo"));
|
|
138
|
-
}
|