@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.
Files changed (3) hide show
  1. package/README.md +285 -10
  2. package/dist/cli.js +1322 -123
  3. 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
- var import_commander = require("commander");
28
- var import_chalk = __toESM(require("chalk"));
29
- var import_child_process = require("child_process");
30
- var program = new import_commander.Command();
31
- var COMING_SOON_MESSAGE = import_chalk.default.cyan(`
32
- \u{1F6A7} Keyway is coming soon!
33
-
34
- We're building the simplest way to manage your team's secrets.
35
- One link in your README = instant access to all secrets.
36
-
37
- ${import_chalk.default.white("Get early access:")} ${import_chalk.default.underline("https://keyway.sh")}
38
- ${import_chalk.default.white("Contact:")} ${import_chalk.default.underline("unlock@keyway.sh")}
39
- `);
40
- program.name("keyway").description("One link to all your secrets (Coming Soon)").version("0.0.1");
41
- program.command("init").description("Initialize Keyway in your project").action(async () => {
42
- console.log(import_chalk.default.cyan("\n\u{1F511} Keyway Init (Preview)\n"));
43
- try {
44
- const gitRemote = (0, import_child_process.execSync)("git remote get-url origin", { encoding: "utf-8" }).trim();
45
- let repoPath;
46
- if (gitRemote.includes("github.com")) {
47
- const match = gitRemote.match(/github\.com[:/](.+?)(\.git)?$/);
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 `[![Keyway Secrets](https://keyway.sh/badge.svg?repo=${repo})](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
- repoPath = match[1].replace(".git", "");
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
- if (repoPath) {
53
- console.log(import_chalk.default.green("\u2713") + " GitHub repository detected:");
54
- console.log(` ${import_chalk.default.gray("Repository:")} ${repoPath}`);
55
- console.log(` ${import_chalk.default.gray("Future vault URL:")} ${import_chalk.default.white(`https://keyway.sh/${repoPath}`)}`);
56
- console.log();
57
- console.log(import_chalk.default.yellow("When launched, you'll be able to:"));
58
- console.log(" \u2022 Store all your secrets securely");
59
- console.log(" \u2022 Share with your team instantly");
60
- console.log(" \u2022 Pull secrets with one command");
61
- console.log();
62
- console.log(import_chalk.default.gray("Want early access? Visit https://keyway.sh"));
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
- console.log(import_chalk.default.yellow("No git repository detected"));
66
- console.log(import_chalk.default.gray("Keyway will work with any GitHub repository"));
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("demo").description("See how Keyway will work").action(() => {
70
- console.log(import_chalk.default.cyan("\n\u{1F3AC} Keyway Demo\n"));
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("waitlist").description("Join the early access waitlist").action(() => {
91
- console.log(import_chalk.default.cyan("\n\u{1F680} Join the Keyway Waitlist\n"));
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("why").description("Why we're building Keyway").action(() => {
99
- console.log(import_chalk.default.cyan("\n\u{1F4A1} Why Keyway?\n"));
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("feedback [message...]").description("Send us feedback").action((message) => {
117
- if (message && message.length > 0) {
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("speed").description("Test CLI speed").action(() => {
131
- const start = Date.now();
132
- console.log(import_chalk.default.green(`\u26A1 Executed in ${Date.now() - start}ms`));
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
- }