@keynv/cli 0.1.0-rc.22 → 0.1.0-rc.23

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 CHANGED
@@ -1,80 +1,88 @@
1
1
  # @keynv/cli
2
2
 
3
- > Runtime text-surface protection for AI coding workflows.
3
+ > Self-hosted secrets vault for teams and AI-assisted development.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@keynv/cli?color=F59E0B)](https://www.npmjs.com/package/@keynv/cli)
6
6
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/keynv-labs/keynv/blob/main/LICENSE)
7
7
 
8
- Your AI agent's transcripts, your shell history, and your terminal output are
9
- leaking secrets right now. `keynv` keeps them out, in real time, on your
10
- machine. No cloud, no re-architecting how you work.
11
-
12
- ```text
13
- $ keynv doctor
14
-
15
- ! zsh history 5 likely secrets across 1 file
16
- ! Claude Code transcripts 62,306 likely secrets across 73 files
17
- · Cursor logs clean
18
-
19
- Total: 62,311 likely secrets across 74 files.
20
- ```
21
-
22
- `doctor`, `scrub`, `shell`, and `watch` are fully local — no account, no server,
23
- no network calls. Match previews are bounded to 3 characters; raw secret values
24
- never appear in output.
8
+ Store your API keys, database URLs, and other secrets in one vault — hosted on
9
+ [keynv.dev](https://keynv.dev) or self-hosted and reference them everywhere by
10
+ alias (`@project.env.key`) instead of the real value. `keynv` resolves aliases
11
+ into your app at runtime, so the real values never touch your code, your shell
12
+ history, or your AI agent's transcripts. Your agent sees
13
+ `@billing.prod.stripe_key`, never the key itself.
25
14
 
26
15
  ## Install
27
16
 
28
17
  ```bash
29
18
  npm install -g @keynv/cli
30
- keynv doctor
31
19
  ```
32
20
 
33
- Prefer a standalone binary over Node? Every
21
+ Node 20+. Prefer a standalone binary over Node? Every
34
22
  [release](https://github.com/keynv-labs/keynv/releases) publishes signed
35
23
  executables for macOS / Linux / Windows with a `SHA256SUMS` manifest.
36
24
 
37
25
  ## Quick start
38
26
 
39
- ```bash
40
- # 1. Find out where you're leaking (scan only — nothing is rewritten)
41
- keynv doctor
27
+ ### Option A · Use keynv.dev (hosted — fastest)
42
28
 
43
- # 2. Clean what's already there (atomic, with backups; --dry-run to preview)
44
- keynv scrub --dry-run
45
- keynv scrub
29
+ 1. Create a free account at [keynv.dev/register](https://keynv.dev/register).
30
+ 2. Connect and set up your project:
46
31
 
47
- # 3. Prevent new leaks — a marked hook in your ~/.zshrc / ~/.bashrc / fish
48
- keynv shell install
49
- keynv shell status
32
+ ```bash
33
+ keynv # pick "keynv.dev", then "Set up this project"
34
+ ```
50
35
 
51
- # 4. Real-time watcher over Claude Code transcripts + Cursor logs
52
- keynv watch start
53
- keynv watch status
54
- ```
36
+ ### Option B · Self-host
55
37
 
56
- The shell hook scrubs secret-shaped substrings before each command lands in
57
- history (POSIX-ERE, no per-command subprocess unless a match fires). The watcher
58
- subscribes to `~/.claude/projects/**/*.jsonl` and Cursor logs via chokidar,
59
- debounces at 1 second, and atomically rewrites matched substrings.
38
+ Run your own server + panel in ~15 minutes see the
39
+ [Coolify walkthrough](https://github.com/keynv-labs/keynv/blob/main/deploy/COOLIFY.md)
40
+ or [Docker Compose guide](https://github.com/keynv-labs/keynv/blob/main/deploy/README.md).
41
+ Then run `keynv`, choose **Self-hosted server**, and enter your API URL.
60
42
 
61
- ## Aliases (optional needs a server)
43
+ > On a headless Linux box with no OS keychain (no libsecret), set
44
+ > `KEYNV_DISABLE_KEYCHAIN=1` before connecting — credentials then persist to an
45
+ > encrypted file instead of the system keychain.
62
46
 
63
- For the "AI agents never see real values" workflow, `keynv` stores secrets in a
64
- vault and hands out aliases (`@project.env.KEY`) instead of values. This needs a
65
- keynv server self-host in ~15 minutes with the
66
- [Coolify walkthrough](https://github.com/keynv-labs/keynv/blob/main/deploy/COOLIFY.md)
67
- or [Docker Compose](https://github.com/keynv-labs/keynv/blob/main/deploy/README.md).
47
+ ### Then run your app the way you already do
48
+
49
+ Setup imports your existing `.env` files into the vault and writes a committable
50
+ `.keynv.env` that maps your environment variables to aliases. It also wraps your
51
+ `package.json` scripts, so you just run them normally:
68
52
 
69
53
  ```bash
70
- keynv login # browser flow; session lands in the OS keychain
71
- keynv project create demo
72
- keynv secret create @demo.dev.api_key --value 'whatever'
73
- keynv exec -- npm run dev # aliases resolved into the child, redacted from output
54
+ npm run dev # secrets injected automatically no `keynv exec` to type
55
+ npm test
74
56
  ```
75
57
 
76
- Running `keynv` with no arguments opens an interactive TUI that walks through the
77
- same operations.
58
+ `.keynv.env` is safe to commit it holds references, not values:
59
+
60
+ ```bash
61
+ OPENAI_API_KEY=@myapp.dev.openai-key
62
+ DATABASE_URL=@myapp.prod.db-url
63
+ NODE_ENV=development # plain literals pass through unchanged
64
+ ```
65
+
66
+ Behind the scenes your wrapped script runs `keynv exec -- <your command>`, which
67
+ resolves each `@alias` against the vault and forks your command with the real
68
+ values in a subprocess your editor and AI agent can't read — redacting them from
69
+ output. **You never type `keynv exec` yourself.**
70
+
71
+ Teams get project-scoped roles (owner / admin / developer / reader), per-secret
72
+ access, rotation, and a tamper-evident audit log — all managed from the CLI or
73
+ the web panel.
74
+
75
+ ## Common commands
76
+
77
+ ```bash
78
+ keynv # interactive TUI (projects, secrets, members)
79
+ keynv secret create # add a secret to the vault
80
+ keynv secret get @a.dev.key --copy # copy a value to the clipboard without printing it
81
+ keynv exec -- <cmd> # run a one-off command with aliases resolved
82
+ keynv member list <project> # who has access to a project
83
+ keynv whoami # who am I logged in as
84
+ keynv --help # full command list
85
+ ```
78
86
 
79
87
  ## Use with Claude Code / Cursor (MCP)
80
88
 
@@ -83,11 +91,25 @@ never see the value. See the
83
91
  [AI setup guide](https://github.com/keynv-labs/keynv/blob/main/docs/ai-setup.md)
84
92
  for wiring it into Claude Code, Cursor, and other MCP clients.
85
93
 
94
+ ## Also: clean up leaks you already have
95
+
96
+ Secrets already sitting in your shell history or AI transcripts? These commands
97
+ run entirely locally — no server or login needed:
98
+
99
+ ```bash
100
+ keynv doctor # scan for leaked secrets (read-only, no network)
101
+ keynv scrub # clean them up, atomically, with backups
102
+ keynv watch start # scrub live AI-agent sessions in real time
103
+ ```
104
+
105
+ Match previews are bounded to 3 characters; raw secret values never appear in
106
+ output.
107
+
86
108
  ## Documentation
87
109
 
88
110
  - [Getting started](https://github.com/keynv-labs/keynv/blob/main/docs/getting-started.md)
89
111
  - [AI setup (Claude Code / Cursor / MCP)](https://github.com/keynv-labs/keynv/blob/main/docs/ai-setup.md)
90
- - [What keynv is — and is not](https://github.com/keynv-labs/keynv/blob/main/docs/00-vision.md)
112
+ - [Coolify deploy](https://github.com/keynv-labs/keynv/blob/main/deploy/COOLIFY.md)
91
113
  - [Threat model](https://github.com/keynv-labs/keynv/blob/main/docs/02-threat-model.md)
92
114
  - [Full README](https://github.com/keynv-labs/keynv#readme)
93
115
 
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'module';
3
- import { createHash, randomBytes } from 'crypto';
4
- import { readFileSync, existsSync, writeFileSync, statSync, realpathSync, readdirSync, renameSync, mkdirSync, rmSync, constants as constants$1, lstatSync } from 'fs';
5
- import { homedir, hostname, platform } from 'os';
6
3
  import * as nodePath from 'path';
7
4
  import { relative, join, isAbsolute, resolve, sep, dirname, basename } from 'path';
5
+ import { createHash } from 'crypto';
6
+ import { readFileSync, existsSync, writeFileSync, statSync, realpathSync, readdirSync, renameSync, mkdirSync, rmSync, constants as constants$1, lstatSync } from 'fs';
7
+ import { homedir, hostname, platform } from 'os';
8
8
  import { mkdir, writeFile, readFile, unlink, stat, rm, access, constants, appendFile, readdir, open, rename, copyFile } from 'fs/promises';
9
9
  import { createConnection, createServer } from 'net';
10
10
  import { styleText, stripVTControlCharacters } from 'util';
@@ -1070,16 +1070,17 @@ var require_lib = __commonJS({
1070
1070
  var VERSION, AGENT;
1071
1071
  var init_version = __esm({
1072
1072
  "src/version.ts"() {
1073
- VERSION = "0.1.0-rc.22" ;
1073
+ VERSION = "0.1.0-rc.23" ;
1074
1074
  AGENT = `keynv-cli/${VERSION}`;
1075
1075
  }
1076
1076
  });
1077
1077
 
1078
1078
  // src/client/defaults.ts
1079
- var DEFAULT_SERVER_URL;
1079
+ var DEFAULT_SERVER_URL, DEFAULT_WEB_URL;
1080
1080
  var init_defaults = __esm({
1081
1081
  "src/client/defaults.ts"() {
1082
1082
  DEFAULT_SERVER_URL = "https://api.keynv.dev";
1083
+ DEFAULT_WEB_URL = "https://keynv.dev";
1083
1084
  }
1084
1085
  });
1085
1086
 
@@ -1227,6 +1228,14 @@ var init_types2 = __esm({
1227
1228
  NONCE_BYTES = 24;
1228
1229
  }
1229
1230
  });
1231
+ function loadSodiumModule() {
1232
+ try {
1233
+ return moduleRequire("libsodium-wrappers");
1234
+ } catch {
1235
+ const runtimeRequire = createRequire(join(process.cwd(), "package.json"));
1236
+ return runtimeRequire("libsodium-wrappers");
1237
+ }
1238
+ }
1230
1239
  function loadSodium() {
1231
1240
  if (!readyPromise) {
1232
1241
  readyPromise = sodium.ready.then(() => sodium);
@@ -1240,11 +1249,11 @@ function zero(buf) {
1240
1249
  }
1241
1250
  buf.fill(0);
1242
1251
  }
1243
- var require2, sodium, readyPromise;
1252
+ var moduleRequire, sodium, readyPromise;
1244
1253
  var init_sodium = __esm({
1245
1254
  "../../packages/core/dist/crypto/sodium.js"() {
1246
- require2 = createRequire(import.meta.url);
1247
- sodium = require2("libsodium-wrappers");
1255
+ moduleRequire = createRequire(import.meta.url);
1256
+ sodium = loadSodiumModule();
1248
1257
  readyPromise = null;
1249
1258
  }
1250
1259
  });
@@ -5609,8 +5618,8 @@ function legacyPath() {
5609
5618
  }
5610
5619
  function loadEntryCtor() {
5611
5620
  if (!EntryCtor) {
5612
- const require3 = createRequire(import.meta.url);
5613
- EntryCtor = require3("@napi-rs/keyring").Entry;
5621
+ const require2 = createRequire(import.meta.url);
5622
+ EntryCtor = require2("@napi-rs/keyring").Entry;
5614
5623
  }
5615
5624
  return EntryCtor;
5616
5625
  }
@@ -6345,8 +6354,8 @@ When the user asks about secrets or env vars, follow this decision tree:
6345
6354
  |---|---|
6346
6355
  | "Add a new API key" / "set up a secret" | \`keynv secret create\` (interactive prompt picks project + env + key) |
6347
6356
  | "I have a \`.env\`, migrate it" | Run \`keynv\` and choose **Set up this project** |
6348
- | "Run the app / dev server / tests" | \`keynv exec -- <their existing command>\` (auto-loads \`.keynv.env\`) |
6349
- | "Show me the value of X" | \`keynv secret get @alias --copy\` \u2014 \`--copy\` puts the value on the clipboard and never prints it. Without \`--copy\` it prints to stdout, so in agent contexts always pass \`--copy\`. |
6357
+ | "Run the app / dev server / tests" | Run the project's script as usual (e.g. \`npm run dev\`) \u2014 setup already wrapped it with keynv. For a one-off command that isn't a package.json script, use \`keynv exec -- <cmd>\`. |
6358
+ | "Show me the value of X" | Don't reveal it. \`keynv secret get @alias --copy\` puts the value on the clipboard without printing it. Bare \`keynv secret get\` refuses to print in a non-interactive context; \`--reveal\` forces raw stdout \u2014 avoid it in agent contexts. |
6350
6359
  | "Rotate this key" | \`keynv secret rotate @alias\` |
6351
6360
  | "Add a new env var to the project" | Edit \`.keynv.env\` and add \`NAME=@project.env.key\` (after creating the secret with \`keynv secret create\`) |
6352
6361
  | "Who has access?" | \`keynv member list <project>\` |
@@ -9192,6 +9201,15 @@ var init_script_wrap = __esm({
9192
9201
  "forever",
9193
9202
  "nodemon",
9194
9203
  "concurrently",
9204
+ // Monorepo / task runners — wrapping the orchestrator injects secrets into
9205
+ // every child task it spawns, so `npm run dev` still needs no `keynv exec`.
9206
+ "turbo",
9207
+ "nx",
9208
+ "lerna",
9209
+ "make",
9210
+ "npm-run-all",
9211
+ "run-p",
9212
+ "run-s",
9195
9213
  "pytest",
9196
9214
  "python",
9197
9215
  "python3",
@@ -9300,10 +9318,20 @@ async function runInitFlow(client, opts) {
9300
9318
  const envFiles = findEnvFilesRecursive(root.path);
9301
9319
  const intoExisting = hasExistingKeynvEnv(root.path);
9302
9320
  if (envFiles.length === 0 && !intoExisting) {
9303
- R2.info(
9304
- `No .env files found in ${root.path} (scanned root and subdirectories). There's nothing to migrate yet \u2014 create a .keynv.env by hand or run \`keynv exec\` once you have one.`
9321
+ Se(
9322
+ [
9323
+ `No .env files found in ${root.path} (scanned root and subdirectories),`,
9324
+ "so there is nothing to migrate yet. To start from scratch:",
9325
+ "",
9326
+ " 1. keynv secret create \u2014 add your first secret to the vault",
9327
+ " 2. add its alias to .keynv.env, e.g. OPENAI_API_KEY=@myapp.dev.openai-key",
9328
+ " 3. run your app normally \u2014 keynv injects the real value",
9329
+ "",
9330
+ 'Or re-run `keynv` \u2192 "Set up this project" once you have a .env to import.'
9331
+ ].join("\n"),
9332
+ "Nothing to migrate yet"
9305
9333
  );
9306
- ye("Nothing to do.");
9334
+ ye("Create your first secret with `keynv secret create`.");
9307
9335
  return { exitCode: 0 };
9308
9336
  }
9309
9337
  Se(
@@ -9442,7 +9470,7 @@ ${detail}`
9442
9470
  const initialSecretSelection = choices.filter((c2) => c2.verdict === "secret" && !c2.isAlias).map((c2) => c2.composite);
9443
9471
  const selectedComposites = unwrap(
9444
9472
  await ve({
9445
- message: "Mark which keys are secrets (vault-uploaded). Unchecked keys stay as literals.",
9473
+ message: "Which keys are secrets? (pre-selected by keynv \u2014 press Enter to accept; unchecked stay as literals)",
9446
9474
  options: choices.map((c2) => ({
9447
9475
  value: c2.composite,
9448
9476
  label: c2.label,
@@ -9468,19 +9496,21 @@ ${detail}`
9468
9496
  const writeDirs = [...new Set(plan.resolved.map((r) => r.source.containingDir))];
9469
9497
  const writeDirsLines = writeDirs.map((d2) => ` ${relFromRoot(root.path, d2)}`).join("\n");
9470
9498
  const renameLine = plan.renamed.length > 0 ? `Renamed vault keys: ${plan.renamed.length} (to avoid cross-app collisions)` : "";
9499
+ const scriptWrapLine = scriptWrapSelection.length > 0 ? `package.json: wrap ${scriptWrapSelection.length} script(s) with \`keynv exec\` \u2014 ${scriptWrapSelection.join(", ")}` : "package.json: not modified (no wrappable scripts)";
9471
9500
  const planSummary = [
9472
9501
  `Project: ${projectChoice.name}${projectChoice.created ? " (will be created)" : ""}`,
9473
9502
  `Environments: ${distinctEnvs.join(", ")}`,
9474
9503
  "Per-env breakdown:",
9475
9504
  perEnvCounts,
9476
- `Script wraps: ${scriptWrapSelection.length}`,
9477
- ".keynv.env files: will be written under",
9505
+ ".keynv.env files: written under (safe to commit \u2014 aliases, not values)",
9478
9506
  writeDirsLines,
9479
- "Original .env files: rename to .env.backup after upload",
9507
+ scriptWrapLine,
9508
+ "AGENTS.md: write/update keynv guidance for AI agents at the project root",
9509
+ ".env files: renamed to .env.backup after their values are uploaded",
9480
9510
  renameLine,
9481
9511
  opts.dryRun ? "Dry-run: no changes will be made." : ""
9482
9512
  ].filter(Boolean).join("\n");
9483
- Se(planSummary, "About to apply");
9513
+ Se(planSummary, "About to apply \u2014 one confirmation applies all of the below");
9484
9514
  const proceed = unwrap(await ue({ message: "Proceed?", initialValue: true }));
9485
9515
  if (!proceed) {
9486
9516
  me("Aborted.");
@@ -9634,9 +9664,15 @@ ${detail}`
9634
9664
  });
9635
9665
  Se(lines.join("\n"), "Secrets in other envs");
9636
9666
  }
9637
- ye(
9638
- failed.length > 0 ? `Done with ${failed.length} failure(s) \u2014 see warnings above.` : `Done. Try: ${scriptWrapSelection.includes("dev") ? "pnpm dev" : "keynv exec -- <your command>"}`
9639
- );
9667
+ let doneMsg;
9668
+ if (failed.length > 0) {
9669
+ doneMsg = `Done with ${failed.length} failure(s) \u2014 see warnings above.`;
9670
+ } else if (scriptWrapSelection.length > 0) {
9671
+ doneMsg = "Done. Your scripts are wrapped \u2014 just run them as usual (e.g. `npm run dev`). Secrets are injected automatically; you never type `keynv exec`.";
9672
+ } else {
9673
+ doneMsg = "Done. Prefix commands with `keynv exec --` to inject secrets (e.g. `keynv exec -- npm run dev`).";
9674
+ }
9675
+ ye(doneMsg);
9640
9676
  return { exitCode: failed.length > 0 ? 1 : 0 };
9641
9677
  }
9642
9678
  async function pickOrCreateProject(client, suggestedName) {
@@ -26864,7 +26900,7 @@ var require_named_placeholders = __commonJS({
26864
26900
  }
26865
26901
  return s;
26866
26902
  }
26867
- function join17(tree) {
26903
+ function join18(tree) {
26868
26904
  if (tree.length === 1) {
26869
26905
  return tree;
26870
26906
  }
@@ -26890,7 +26926,7 @@ var require_named_placeholders = __commonJS({
26890
26926
  if (cache2 && (tree = cache2.get(query))) {
26891
26927
  return toArrayParams(tree, paramsObj);
26892
26928
  }
26893
- tree = join17(parse(query));
26929
+ tree = join18(parse(query));
26894
26930
  if (cache2) {
26895
26931
  cache2.set(query, tree);
26896
26932
  }
@@ -30503,7 +30539,7 @@ var require_utils_webcrypto = __commonJS({
30503
30539
  var nodeCrypto = __require("crypto");
30504
30540
  module.exports = {
30505
30541
  postgresMd5PasswordHash,
30506
- randomBytes: randomBytes2,
30542
+ randomBytes,
30507
30543
  deriveKey,
30508
30544
  sha256,
30509
30545
  hashByName,
@@ -30513,7 +30549,7 @@ var require_utils_webcrypto = __commonJS({
30513
30549
  var webCrypto = nodeCrypto.webcrypto || globalThis.crypto;
30514
30550
  var subtleCrypto = webCrypto.subtle;
30515
30551
  var textEncoder = new TextEncoder();
30516
- function randomBytes2(length) {
30552
+ function randomBytes(length) {
30517
30553
  return webCrypto.getRandomValues(Buffer.alloc(length));
30518
30554
  }
30519
30555
  async function md5(string) {
@@ -44730,17 +44766,20 @@ async function pickConnectionTarget() {
44730
44766
  const target = await Ee({
44731
44767
  message: "Connect to keynv",
44732
44768
  options: [
44733
- { value: "cloud", label: "keynv.dev", hint: "hosted (beta)" },
44769
+ { value: "cloud", label: "keynv.dev", hint: "hosted \u2014 fastest way to start" },
44734
44770
  {
44735
44771
  value: "self-hosted",
44736
44772
  label: "Self-hosted server",
44737
- hint: "recommended \u2014 enter your API URL"
44773
+ hint: "your own deployment \u2014 enter its API URL"
44738
44774
  },
44739
44775
  { value: "exit", label: "Exit" }
44740
44776
  ]
44741
44777
  });
44742
44778
  if (q(target) || target === "exit") return null;
44743
- if (target === "cloud") return DEFAULT_SERVER_URL;
44779
+ if (target === "cloud") {
44780
+ R2.info(`New to keynv? Create a free account first: ${DEFAULT_WEB_URL}/register`);
44781
+ return DEFAULT_SERVER_URL;
44782
+ }
44744
44783
  const server = await Re({
44745
44784
  message: "Server API URL",
44746
44785
  placeholder: "https://api.keynv.example.com",
@@ -49795,12 +49834,15 @@ var SecretCreateCommand = class extends Command {
49795
49834
  var SecretGetCommand = class extends Command {
49796
49835
  static paths = [["secret", "get"]];
49797
49836
  static usage = Command.Usage({
49798
- description: "Resolve a secret. Prints the value to stdout, or use --copy to put it on the clipboard without printing it."
49837
+ description: "Resolve a secret. By default the value is copied to the OS clipboard (never printed). Use --reveal to print it to stdout, or --json for automation."
49799
49838
  });
49800
49839
  alias = options_exports.String({ required: false });
49801
49840
  json = options_exports.Boolean("--json", false);
49802
49841
  copy = options_exports.Boolean("--copy", false, {
49803
- description: "Copy the value to the OS clipboard instead of printing it (never shown in output)."
49842
+ description: "Copy the value to the OS clipboard (the default in an interactive terminal)."
49843
+ });
49844
+ reveal = options_exports.Boolean("--reveal", false, {
49845
+ description: "Print the raw value to stdout. Opt-in, because it can land in terminal scrollback or an AI transcript."
49804
49846
  });
49805
49847
  async execute() {
49806
49848
  try {
@@ -49828,35 +49870,41 @@ var SecretGetCommand = class extends Command {
49828
49870
  const data = await client.request(
49829
49871
  `/v1/projects/${projectId3}/secrets/${parsed.environment}/${parsed.key}`
49830
49872
  );
49831
- if (this.copy) {
49832
- try {
49833
- await copyToClipboard2(data.value);
49834
- } catch (err) {
49835
- if (err instanceof ClipboardUnavailableError) {
49836
- this.context.stderr.write(
49837
- `keynv: ${err.message}.
49838
- Install one (pbcopy/clip are built in; Linux: wl-clipboard or xclip), or omit --copy to print to stdout.
49839
- `
49840
- );
49841
- return 1;
49842
- }
49843
- throw err;
49844
- } finally {
49845
- data.value = "";
49846
- }
49847
- this.context.stderr.write(`keynv: copied ${data.alias} (v${data.version}) to clipboard.
49848
- `);
49849
- return 0;
49850
- }
49851
49873
  if (this.json) {
49852
49874
  this.context.stdout.write(
49853
49875
  `${JSON.stringify({ alias: data.alias, version: data.version, value: data.value })}
49854
49876
  `
49855
49877
  );
49856
- } else {
49878
+ return 0;
49879
+ }
49880
+ if (this.reveal) {
49857
49881
  this.context.stdout.write(`${data.value}
49858
49882
  `);
49883
+ return 0;
49884
+ }
49885
+ if (!this.copy && !isInteractive()) {
49886
+ this.context.stderr.write(
49887
+ "keynv: refusing to print a secret to a non-interactive stdout.\n Pass --reveal to print it, --copy to copy it, or --json for automation.\n"
49888
+ );
49889
+ return 1;
49859
49890
  }
49891
+ try {
49892
+ await copyToClipboard2(data.value);
49893
+ } catch (err) {
49894
+ if (err instanceof ClipboardUnavailableError) {
49895
+ this.context.stderr.write(
49896
+ `keynv: ${err.message}.
49897
+ Install one (pbcopy/clip are built in; Linux: wl-clipboard or xclip), or pass --reveal to print to stdout / --json for automation.
49898
+ `
49899
+ );
49900
+ return 1;
49901
+ }
49902
+ throw err;
49903
+ } finally {
49904
+ data.value = "";
49905
+ }
49906
+ this.context.stderr.write(`keynv: copied ${data.alias} (v${data.version}) to clipboard.
49907
+ `);
49860
49908
  return 0;
49861
49909
  } catch (err) {
49862
49910
  return handleExecError(this.context.stderr, err);
@@ -50126,140 +50174,6 @@ var SecretRotationsCommand = class extends Command {
50126
50174
  }
50127
50175
  }
50128
50176
  };
50129
- function generateSecret(bytes = 32) {
50130
- return randomBytes(bytes).toString("base64");
50131
- }
50132
- var ServerInitCommand = class extends Command {
50133
- static paths = [["server", "init"]];
50134
- static usage = Command.Usage({
50135
- description: "Generate secrets and deployment files for a new keynv server.",
50136
- details: `
50137
- Interactive onboarding wizard that generates the configuration
50138
- a fresh keynv server needs: JWT signing secret, master encryption
50139
- key, and deployment templates.
50140
-
50141
- Outputs:
50142
- \u2022 A .keynv-server.env file you feed into your deployment
50143
- (Docker Compose, Coolify, or bare-metal).
50144
- \u2022 Optionally a docker-compose.yml in the current directory.
50145
-
50146
- Non-interactive with --yes to skip prompts.
50147
- `,
50148
- examples: [
50149
- ["Interactive wizard", "$0 server init"],
50150
- ["Non-interactive (CI/scripts)", "$0 server init --yes"],
50151
- ["Docker Compose output", "$0 server init --compose"],
50152
- ["Custom output path", "$0 server init --out ./config/env"]
50153
- ]
50154
- });
50155
- yes = options_exports.Boolean("--yes", false, {
50156
- description: "Skip all prompts (non-interactive)."
50157
- });
50158
- compose = options_exports.Boolean("--compose", false, {
50159
- description: "Also write a docker-compose.yml to the current directory."
50160
- });
50161
- out = options_exports.String("--out", {
50162
- description: "Write server env file to this path (default: .keynv-server.env)."
50163
- });
50164
- async execute() {
50165
- const jwtSecret = generateSecret(48);
50166
- const masterKey = generateSecret(32);
50167
- const webSessionSecret = generateSecret(48);
50168
- const outPath = this.out ?? ".keynv-server.env";
50169
- const contents = [
50170
- "# Generated by `keynv server init`.",
50171
- "# Keep this file secret \u2014 it contains cryptographic key material.",
50172
- `KEYNV_JWT_SECRET=${jwtSecret}`,
50173
- `KEYNV_MASTER_KEY=${masterKey}`,
50174
- "",
50175
- "# Web dashboard session secret (required for Next.js JWE session tokens).",
50176
- `KEYNV_WEB_SESSION_SECRET=${webSessionSecret}`,
50177
- "",
50178
- "# Server configuration (uncomment and adjust):",
50179
- "# KEYNV_PORT=8080",
50180
- "# KEYNV_WEB_URL=https://keynv.example.com",
50181
- "# KEYNV_PUBLIC_REGISTRATION=false",
50182
- "# KEYNV_DB_PATH=./keynv.db",
50183
- "# KEYNV_MASTER_KEY_FILE=./master.key",
50184
- "# KEYNV_RATE_LIMIT_PER_MINUTE=120",
50185
- "# KEYNV_ARGON2_MEMORY_KIB=46080",
50186
- "# KEYNV_ARGON2_TIME_COST=3"
50187
- ].join("\n");
50188
- const { writeFileSync: writeFileSync5 } = await import('fs');
50189
- writeFileSync5(outPath, `${contents}
50190
- `, { mode: 384 });
50191
- this.context.stdout.write(`Wrote ${outPath}
50192
-
50193
- `);
50194
- this.context.stdout.write("Generated secrets:\n");
50195
- this.context.stdout.write(
50196
- ` KEYNV_JWT_SECRET ${jwtSecret.slice(0, 8)}... (base64, 48 bytes)
50197
- `
50198
- );
50199
- this.context.stdout.write(
50200
- ` KEYNV_MASTER_KEY ${masterKey.slice(0, 8)}... (base64, 32 bytes)
50201
- `
50202
- );
50203
- this.context.stdout.write(
50204
- ` KEYNV_WEB_SESSION_SECRET ${webSessionSecret.slice(0, 8)}... (base64, 48 bytes)
50205
- `
50206
- );
50207
- this.context.stdout.write("\nNext steps:\n");
50208
- this.context.stdout.write(` 1. Feed ${outPath} into your deployment:
50209
- `);
50210
- this.context.stdout.write(" docker compose --env-file .keynv-server.env up -d\n");
50211
- this.context.stdout.write(" or load it into Coolify as environment variables.\n");
50212
- this.context.stdout.write(" 2. Bootstrap the first owner:\n");
50213
- this.context.stdout.write(
50214
- " docker exec -it <server-container> node dist/bootstrap.js \\\n"
50215
- );
50216
- this.context.stdout.write(" --owner-email alice@example.com \\\n");
50217
- this.context.stdout.write(' --owner-password "<a long random password>" \\\n');
50218
- this.context.stdout.write(' --org-name "Acme Inc"\n');
50219
- this.context.stdout.write(" 3. Install the CLI and connect from the TUI:\n");
50220
- this.context.stdout.write(" npm install -g @keynv/cli\n");
50221
- this.context.stdout.write(" keynv\n");
50222
- this.context.stdout.write(" 4. Onboard your first project from the same TUI:\n");
50223
- this.context.stdout.write(" cd your-project && keynv\n");
50224
- if (this.compose) {
50225
- const composeContent = `# keynv server + Litestream sidecar
50226
- # Generated by \`keynv server init\`.
50227
- # Load with: docker compose --env-file .keynv-server.env up -d
50228
-
50229
- services:
50230
- server:
50231
- image: ghcr.io/keynv-labs/keynv-server:latest
50232
- restart: unless-stopped
50233
- ports:
50234
- - '8080:8080'
50235
- env_file:
50236
- - .keynv-server.env
50237
- volumes:
50238
- - keynv-data:/data
50239
-
50240
- litestream:
50241
- image: litestream/litestream:latest
50242
- restart: unless-stopped
50243
- volumes:
50244
- - keynv-data:/data
50245
- command: replicate
50246
- environment:
50247
- LITESTREAM_DB_PATH: /data/keynv.db
50248
- LITESTREAM_REPLICA_URL: s3://your-bucket/keynv
50249
- AWS_ACCESS_KEY_ID: \${LITESTREAM_AWS_ACCESS_KEY_ID}
50250
- AWS_SECRET_ACCESS_KEY: \${LITESTREAM_AWS_SECRET_ACCESS_KEY}
50251
-
50252
- volumes:
50253
- keynv-data:
50254
- `;
50255
- writeFileSync5("docker-compose.yml", composeContent, { mode: 384 });
50256
- this.context.stdout.write(
50257
- "\nWrote docker-compose.yml (update Litestream S3 config before deploying).\n"
50258
- );
50259
- }
50260
- return 0;
50261
- }
50262
- };
50263
50177
 
50264
50178
  // src/shell/templates.ts
50265
50179
  var SHELL_SECRET_ERE = "(AKIA|ASIA)[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|gh[uops]_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{40,}|xox[abp]-[A-Za-z0-9-]{24,}|(sk|rk)_(live|test)_[A-Za-z0-9]{24,}|sk-ant-(api03|admin01)-[A-Za-z0-9_-]{20,}|sk-(proj-)?[A-Za-z0-9_-]{20,}|eyJ[A-Za-z0-9_-]{10,}\\.eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}|(postgresql|postgres|mysql|mongodb)(\\+srv)?://[^@ ]+@[^ ]+|rediss?://[^@ ]+@[^ ]+";
@@ -51329,6 +51243,25 @@ the error message.
51329
51243
  return result.ok ? 0 : 1;
51330
51244
  }
51331
51245
  };
51246
+
51247
+ // src/commands/ui.ts
51248
+ init_tty();
51249
+ init_menu();
51250
+ var UICommand = class extends Command {
51251
+ static paths = [["ui"]];
51252
+ static usage = Command.Usage({
51253
+ description: "Open the interactive menu (also runs by default when no args are given)."
51254
+ });
51255
+ async execute() {
51256
+ if (!isInteractive()) {
51257
+ this.context.stdout.write(
51258
+ "keynv \u2014 AI-safe secrets management.\nRun `keynv --help` for the full command list, or run `keynv` in an interactive\nterminal to open the menu.\n"
51259
+ );
51260
+ return 0;
51261
+ }
51262
+ return runMenu();
51263
+ }
51264
+ };
51332
51265
  init_format();
51333
51266
  init_state();
51334
51267
  init_version();
@@ -51837,7 +51770,7 @@ cli.register(WatchStartCommand);
51837
51770
  cli.register(WatchStopCommand);
51838
51771
  cli.register(WatchStatusCommand);
51839
51772
  cli.register(TestCommand);
51840
- cli.register(ServerInitCommand);
51773
+ cli.register(UICommand);
51841
51774
  var argv = process.argv.slice(2);
51842
51775
  async function main() {
51843
51776
  if (argv.length === 0) {