@keynv/cli 0.1.0-rc.10 → 0.1.0-rc.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'module';
3
- import 'crypto';
3
+ import { randomBytes } from 'crypto';
4
4
  import { readFileSync, existsSync, statSync, writeFileSync, unlinkSync, mkdirSync, rmSync, readdirSync } from 'fs';
5
- import { homedir } from 'os';
6
- import { isAbsolute, resolve, join, dirname, basename } from 'path';
5
+ import { hostname, platform, homedir } from 'os';
6
+ import { relative, isAbsolute, resolve, join, dirname, basename } from 'path';
7
7
  import { Entry } from '@napi-rs/keyring';
8
8
  import { styleText } from 'util';
9
9
  import j2, { stdin, stdout } from 'process';
@@ -579,16 +579,16 @@ var require_lib = __commonJS({
579
579
  }
580
580
  var TypeAssertionError = class extends Error {
581
581
  constructor({ errors } = {}) {
582
- let errorMessage = `Type mismatch`;
582
+ let errorMessage2 = `Type mismatch`;
583
583
  if (errors && errors.length > 0) {
584
- errorMessage += `
584
+ errorMessage2 += `
585
585
  `;
586
586
  for (const error of errors) {
587
- errorMessage += `
587
+ errorMessage2 += `
588
588
  - ${error}`;
589
589
  }
590
590
  }
591
- super(errorMessage);
591
+ super(errorMessage2);
592
592
  }
593
593
  };
594
594
  function assert(val, validator) {
@@ -1056,7 +1056,7 @@ var require_lib = __commonJS({
1056
1056
  var VERSION, AGENT;
1057
1057
  var init_version = __esm({
1058
1058
  "src/version.ts"() {
1059
- VERSION = "0.1.0-rc.10" ;
1059
+ VERSION = "0.1.0-rc.12" ;
1060
1060
  AGENT = `keynv-cli/${VERSION}`;
1061
1061
  }
1062
1062
  });
@@ -1146,10 +1146,10 @@ var init_parser = __esm({
1146
1146
  "../../packages/core/dist/reference/parser.js"() {
1147
1147
  PROJECT_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
1148
1148
  ENV_RE = /^[a-z0-9][a-z0-9-]{0,23}$/;
1149
- KEY_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
1149
+ KEY_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
1150
1150
  TRAILING_BOUNDARY = "(?!\\w|\\.[a-z0-9])";
1151
- TEXT_FIND_SOURCE = `(?<![\\w.@/])@[a-z0-9][a-z0-9-]{0,47}\\.[a-z0-9][a-z0-9-]{0,23}\\.[a-z0-9][a-z0-9_-]{0,63}${TRAILING_BOUNDARY}`;
1152
- ARGV_FIND_SOURCE = `@[a-z0-9][a-z0-9-]{0,47}\\.[a-z0-9][a-z0-9-]{0,23}\\.[a-z0-9][a-z0-9_-]{0,63}${TRAILING_BOUNDARY}`;
1151
+ TEXT_FIND_SOURCE = `(?<![\\w.@/])@[a-z0-9][a-z0-9-]{0,47}\\.[a-z0-9][a-z0-9-]{0,23}\\.[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}${TRAILING_BOUNDARY}`;
1152
+ ARGV_FIND_SOURCE = `@[a-z0-9][a-z0-9-]{0,47}\\.[a-z0-9][a-z0-9-]{0,23}\\.[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}${TRAILING_BOUNDARY}`;
1153
1153
  TEXT_FIND_RE = new RegExp(TEXT_FIND_SOURCE, "g");
1154
1154
  ARGV_FIND_RE = new RegExp(ARGV_FIND_SOURCE, "g");
1155
1155
  }
@@ -1695,15 +1695,15 @@ var init_parseUtil = __esm({
1695
1695
  message: issueData.message
1696
1696
  };
1697
1697
  }
1698
- let errorMessage = "";
1698
+ let errorMessage2 = "";
1699
1699
  const maps = errorMaps.filter((m) => !!m).slice().reverse();
1700
1700
  for (const map of maps) {
1701
- errorMessage = map(fullIssue, { data, defaultError: errorMessage }).message;
1701
+ errorMessage2 = map(fullIssue, { data, defaultError: errorMessage2 }).message;
1702
1702
  }
1703
1703
  return {
1704
1704
  ...issueData,
1705
1705
  path: fullPath,
1706
- message: errorMessage
1706
+ message: errorMessage2
1707
1707
  };
1708
1708
  };
1709
1709
  EMPTY_PATH = [];
@@ -5536,7 +5536,15 @@ function clearCredentialsFile() {
5536
5536
  if (existsSync(legacy)) rmSync(legacy, { force: true });
5537
5537
  try {
5538
5538
  entry().deletePassword();
5539
- } catch {
5539
+ return true;
5540
+ } catch (err) {
5541
+ process.stderr.write(
5542
+ `keynv: warning \u2014 could not remove OS keychain entry (${err instanceof Error ? err.message : String(err)}).
5543
+ You may need to remove the '${SERVICE}' / '${KEY_ACCOUNT}' entry manually
5544
+ via your OS credential manager (Windows Credential Manager, macOS Keychain, or libsecret).
5545
+ `
5546
+ );
5547
+ return false;
5540
5548
  }
5541
5549
  }
5542
5550
  var SERVICE, KEY_ACCOUNT;
@@ -5579,8 +5587,9 @@ async function saveCredentials(creds) {
5579
5587
  cache = creds;
5580
5588
  }
5581
5589
  function clearCredentials() {
5582
- clearCredentialsFile();
5590
+ const ok = clearCredentialsFile();
5583
5591
  cache = null;
5592
+ return ok;
5584
5593
  }
5585
5594
  var cache;
5586
5595
  var init_store = __esm({
@@ -5604,6 +5613,7 @@ function buildUrl(base, path, query) {
5604
5613
  return url.toString();
5605
5614
  }
5606
5615
  async function tryRefresh(creds) {
5616
+ if (creds.auth_kind === "cli_token" || creds.refresh_token.length === 0) return null;
5607
5617
  const res = await fetch(buildUrl(creds.server_url, "/v1/auth/refresh"), {
5608
5618
  method: "POST",
5609
5619
  headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
@@ -5722,6 +5732,21 @@ var init_format = __esm({
5722
5732
  "src/ui/format.ts"() {
5723
5733
  }
5724
5734
  });
5735
+ function walkUp(startDir, predicate) {
5736
+ let dir = resolve(startDir);
5737
+ for (let i = 0; i < 64; i++) {
5738
+ const result = predicate(dir);
5739
+ if (result !== null && result !== void 0) return result;
5740
+ const parent = dirname(dir);
5741
+ if (parent === dir) return null;
5742
+ dir = parent;
5743
+ }
5744
+ return null;
5745
+ }
5746
+ var init_fs = __esm({
5747
+ "src/util/fs.ts"() {
5748
+ }
5749
+ });
5725
5750
  function parseEnvFile(content, filename) {
5726
5751
  const normalized = content.charCodeAt(0) === 65279 ? content.slice(1) : content;
5727
5752
  const lines = normalized.split(/\r?\n/);
@@ -5748,7 +5773,7 @@ function parseEnvFile(content, filename) {
5748
5773
  `invalid key '${name}' (must match /^[A-Za-z_][A-Za-z0-9_]*$/)`
5749
5774
  );
5750
5775
  }
5751
- let valueRaw = body.slice(eq + 1);
5776
+ const valueRaw = body.slice(eq + 1);
5752
5777
  let value;
5753
5778
  const valueLeading = valueRaw.replace(/^\s+/, "");
5754
5779
  const firstCh = valueLeading.charAt(0);
@@ -5780,20 +5805,14 @@ function parseEnvFile(content, filename) {
5780
5805
  return entries;
5781
5806
  }
5782
5807
  function findEnvFile(startDir) {
5783
- let dir = resolve(startDir);
5784
- for (let i = 0; i < 64; i++) {
5808
+ return walkUp(startDir, (dir) => {
5785
5809
  const candidate = join(dir, ENV_FILE_BASENAME);
5786
- if (existsSync(candidate)) {
5787
- try {
5788
- if (statSync(candidate).isFile()) return candidate;
5789
- } catch {
5790
- }
5810
+ try {
5811
+ if (statSync(candidate).isFile()) return candidate;
5812
+ } catch {
5791
5813
  }
5792
- const parent = dirname(dir);
5793
- if (parent === dir) return null;
5794
- dir = parent;
5795
- }
5796
- return null;
5814
+ return null;
5815
+ });
5797
5816
  }
5798
5817
  function loadEnvFile(opts) {
5799
5818
  if (opts.disabled) return null;
@@ -5818,6 +5837,7 @@ var MAX_FILE_BYTES, KEY_RE2, ENV_FILE_BASENAME, EnvFileParseError, EnvFileNotFou
5818
5837
  var init_envFile = __esm({
5819
5838
  "src/exec/envFile.ts"() {
5820
5839
  init_dist();
5840
+ init_fs();
5821
5841
  MAX_FILE_BYTES = 1e6;
5822
5842
  KEY_RE2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
5823
5843
  ENV_FILE_BASENAME = ".keynv.env";
@@ -5854,16 +5874,129 @@ var init_envFile = __esm({
5854
5874
  }
5855
5875
  });
5856
5876
 
5857
- // src/ui/helpers/tty.ts
5858
- var tty_exports = {};
5859
- __export(tty_exports, {
5860
- isInteractive: () => isInteractive
5861
- });
5862
- function isInteractive() {
5863
- return process.stdin.isTTY === true && process.stdout.isTTY === true;
5877
+ // src/init/heuristics.ts
5878
+ function shannonEntropyBits(s) {
5879
+ if (s.length === 0) return 0;
5880
+ const counts = /* @__PURE__ */ new Map();
5881
+ for (const ch of s) counts.set(ch, (counts.get(ch) ?? 0) + 1);
5882
+ let h2 = 0;
5883
+ for (const c2 of counts.values()) {
5884
+ const p2 = c2 / s.length;
5885
+ h2 -= p2 * Math.log2(p2);
5886
+ }
5887
+ return h2;
5864
5888
  }
5865
- var init_tty = __esm({
5866
- "src/ui/helpers/tty.ts"() {
5889
+ function nameMatchesLiteralPrefix(name) {
5890
+ const upper = name.toUpperCase();
5891
+ return NAME_LITERAL_PREFIX.some((p2) => upper.startsWith(p2));
5892
+ }
5893
+ function nameMatchesSecretSuffix(name) {
5894
+ const upper = name.toUpperCase();
5895
+ for (const { suffix, hint } of NAME_SECRET_SUFFIXES) {
5896
+ if (upper.endsWith(suffix) || upper.endsWith(`_${suffix}`)) {
5897
+ return { matched: true, hint };
5898
+ }
5899
+ }
5900
+ return { matched: false, hint: "" };
5901
+ }
5902
+ function classifyEntry(name, value) {
5903
+ const upper = name.toUpperCase();
5904
+ if (NAME_FRAMEWORK_MANAGED.has(upper)) {
5905
+ return { verdict: "skip", hint: "framework/shell-managed" };
5906
+ }
5907
+ if (NAME_LITERAL_EXACT.has(upper)) {
5908
+ return { verdict: "literal", hint: "common config var" };
5909
+ }
5910
+ if (nameMatchesLiteralPrefix(name)) {
5911
+ return { verdict: "literal", hint: "public env, build-time bundled" };
5912
+ }
5913
+ if (value.length === 0) {
5914
+ return { verdict: "literal", hint: "empty" };
5915
+ }
5916
+ for (const { re, hint } of VALUE_PATTERNS) {
5917
+ if (re.test(value)) return { verdict: "secret", hint };
5918
+ }
5919
+ const suffix = nameMatchesSecretSuffix(name);
5920
+ if (suffix.matched) {
5921
+ return { verdict: "secret", hint: suffix.hint };
5922
+ }
5923
+ if (NAME_DB_URL.test(name)) {
5924
+ return { verdict: "secret", hint: "database URL" };
5925
+ }
5926
+ if (value.length >= 32) {
5927
+ const bits = shannonEntropyBits(value);
5928
+ if (bits >= 3.5) {
5929
+ return { verdict: "secret", hint: `${value.length}-char random-looking string` };
5930
+ }
5931
+ }
5932
+ return { verdict: "ambiguous", hint: "" };
5933
+ }
5934
+ function previewValue(value, max = 40) {
5935
+ if (value.length === 0) return "(empty)";
5936
+ if (value.length <= max) return value;
5937
+ return `${value.slice(0, max - 1)}\u2026`;
5938
+ }
5939
+ var NAME_FRAMEWORK_MANAGED, NAME_LITERAL_EXACT, NAME_LITERAL_PREFIX, NAME_SECRET_SUFFIXES, NAME_DB_URL, VALUE_PATTERNS;
5940
+ var init_heuristics = __esm({
5941
+ "src/init/heuristics.ts"() {
5942
+ NAME_FRAMEWORK_MANAGED = /* @__PURE__ */ new Set([
5943
+ "NODE_ENV",
5944
+ "PORT",
5945
+ "HOST",
5946
+ "HOSTNAME",
5947
+ "PATH",
5948
+ "HOME",
5949
+ "USER",
5950
+ "SHELL",
5951
+ "PWD",
5952
+ "CI"
5953
+ ]);
5954
+ NAME_LITERAL_EXACT = /* @__PURE__ */ new Set([
5955
+ "DEBUG",
5956
+ "LOG_LEVEL",
5957
+ "LOGLEVEL",
5958
+ "TZ",
5959
+ "LANG",
5960
+ "LC_ALL",
5961
+ "NODE_OPTIONS",
5962
+ "NPM_CONFIG_LOGLEVEL",
5963
+ "TS_NODE_PROJECT"
5964
+ ]);
5965
+ NAME_LITERAL_PREFIX = ["NEXT_PUBLIC_", "VITE_", "REACT_APP_", "PUBLIC_", "EXPO_PUBLIC_"];
5966
+ NAME_SECRET_SUFFIXES = [
5967
+ { suffix: "PRIVATE_KEY", hint: "private key" },
5968
+ { suffix: "API_KEY", hint: "API key" },
5969
+ { suffix: "ACCESS_KEY", hint: "access key" },
5970
+ { suffix: "SECRET_KEY", hint: "secret key" },
5971
+ { suffix: "SECRET", hint: "secret" },
5972
+ { suffix: "PASSWORD", hint: "password" },
5973
+ { suffix: "PASSPHRASE", hint: "passphrase" },
5974
+ { suffix: "TOKEN", hint: "token" },
5975
+ { suffix: "KEY", hint: "key" },
5976
+ { suffix: "CREDENTIALS", hint: "credentials" },
5977
+ { suffix: "AUTH", hint: "auth" },
5978
+ { suffix: "DSN", hint: "connection string" }
5979
+ ];
5980
+ NAME_DB_URL = /^(DATABASE|DB|POSTGRES|MYSQL|MONGO|REDIS)_URL$/i;
5981
+ VALUE_PATTERNS = [
5982
+ { re: /^sk-proj-/, hint: "OpenAI project key" },
5983
+ { re: /^sk-[A-Za-z0-9]{20,}/, hint: "OpenAI key" },
5984
+ { re: /^sk_live_/, hint: "Stripe live key" },
5985
+ { re: /^sk_test_/, hint: "Stripe test key" },
5986
+ { re: /^pk_live_/, hint: "Stripe publishable \u2014 often public, double-check" },
5987
+ { re: /^xoxb-/, hint: "Slack bot token" },
5988
+ { re: /^xoxp-/, hint: "Slack user token" },
5989
+ { re: /^ghp_[A-Za-z0-9]{30,}/, hint: "GitHub personal access token" },
5990
+ { re: /^github_pat_/, hint: "GitHub fine-grained PAT" },
5991
+ { re: /^gho_/, hint: "GitHub OAuth token" },
5992
+ { re: /^AKIA[0-9A-Z]{16}$/, hint: "AWS access key id" },
5993
+ { re: /^ASIA[0-9A-Z]{16}$/, hint: "AWS temporary key id" },
5994
+ { re: /^AIza[0-9A-Za-z_-]{35}$/, hint: "Google API key" },
5995
+ { re: /^ya29\./, hint: "Google OAuth access token" },
5996
+ { re: /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, hint: "JWT" },
5997
+ { re: /^-----BEGIN [A-Z ]+PRIVATE KEY-----/, hint: "PEM private key" },
5998
+ { re: /^([a-z]+):\/\/[^@]+:[^@]+@/i, hint: "connection string with credentials" }
5999
+ ];
5867
6000
  }
5868
6001
  });
5869
6002
 
@@ -7021,23 +7154,109 @@ ${c2}
7021
7154
  } }).prompt();
7022
7155
  }
7023
7156
  });
7157
+ function aiContextBody() {
7158
+ return `## keynv (secrets)
7159
+
7160
+ This project uses [keynv](https://keynv.dev) for secrets. There is a \`.keynv.env\` file at the project root containing **alias references** like \`OPENAI_API_KEY=@projectname.dev.openai-key\` \u2014 these are NOT real values, they are references to secrets stored in the keynv vault.
7161
+
7162
+ ### Mental model
7163
+
7164
+ - **\`.keynv.env\`** \u2014 checked into git, contains aliases. Safe to read, edit, commit.
7165
+ - **Vault** \u2014 holds the real values. The \`keynv\` CLI reads it on demand.
7166
+ - **\`keynv exec -- <command>\`** \u2014 forks the command in a subprocess where the real values are injected into env vars. The parent process (where you, the AI agent, run) NEVER sees the real values.
7167
+ - **Redactor** \u2014 pipes subprocess stdout/stderr through a masker before they reach this terminal. Even if a script accidentally echoes a secret, you see \`<REDACTED:...>\` here.
7168
+
7169
+ ### What you should do
7170
+
7171
+ When the user asks about secrets or env vars, follow this decision tree:
7172
+
7173
+ | User intent | Run / suggest |
7174
+ |---|---|
7175
+ | "Add a new API key" / "set up a secret" | \`keynv secret create\` (interactive prompt picks project + env + key) |
7176
+ | "I have a \`.env\`, migrate it" | \`keynv init\` (you're seeing this file because that already happened) |
7177
+ | "Run the app / dev server / tests" | \`keynv exec -- <their existing command>\` (auto-loads \`.keynv.env\`) |
7178
+ | "Show me the value of X" | \`keynv secret get @alias\` \u2014 but copy to clipboard, do NOT print the value into chat / terminal |
7179
+ | "Rotate this key" | \`keynv secret rotate @alias\` |
7180
+ | "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\`) |
7181
+ | "Who has access?" | \`keynv member list <project>\` |
7182
+
7183
+ ### Hard rules \u2014 do not violate
7184
+
7185
+ 1. **Never print resolved secret values** to the chat, terminal, log, or any file. The user's editor / scrollback / chat history is an attack surface.
7186
+ 2. **Never write a \`.env\` file** (or any file) containing real secret values. The whole point of keynv is that those don't exist on disk.
7187
+ 3. **Never edit \`.keynv.env\` to substitute resolved values for aliases.** The file should only ever contain \`@project.env.key\` references and plain non-secret literals.
7188
+ 4. **Use \`keynv exec --\` to run commands that need env vars.** Don't try to load \`.keynv.env\` yourself by parsing it manually; the wrapper handles resolution + redaction + subprocess isolation.
7189
+ 5. **If a tool fails with "blocked by keynv-guard" or similar**, do NOT try to work around it. Tell the user \u2014 that's a sign of a misconfiguration, not an obstacle to bypass.
7190
+ 6. **Treat the alias literal (\`@project.env.key\`) as the canonical reference**, never the resolved value. When you write code, configs, or commands that need a secret, write the alias.
7191
+
7192
+ ### Quick reference
7193
+
7194
+ \`\`\`bash
7195
+ keynv # interactive TUI menu (pick projects, secrets, members)
7196
+ keynv exec -- <cmd> # run cmd with .keynv.env loaded
7197
+ keynv secret create # walk through creating a new secret
7198
+ keynv secret list <project> # list aliases (no values)
7199
+ keynv whoami # who am I logged in as
7200
+ keynv --help # full command list
7201
+ \`\`\`
7202
+
7203
+ If \`.keynv.env\` is missing or empty, the user probably hasn't run \`keynv init\` yet \u2014 suggest they do so before they paste a secret into a \`.env\`.`;
7204
+ }
7205
+ function renderKeynvBlock() {
7206
+ return `${KEYNV_BLOCK_START}
7207
+ ${aiContextBody()}
7208
+ ${KEYNV_BLOCK_END}`;
7209
+ }
7210
+ function writeAiContext(rootPath) {
7211
+ const path = join(rootPath, AGENTS_FILE_BASENAME);
7212
+ const block = renderKeynvBlock();
7213
+ if (!existsSync(path)) {
7214
+ const initial = `# Agent guidance for this project
7215
+
7216
+ ${block}
7217
+ `;
7218
+ writeFileSync(path, initial);
7219
+ return "created";
7220
+ }
7221
+ const existing = readFileSync(path, "utf8");
7222
+ const startIdx = existing.indexOf(KEYNV_BLOCK_START);
7223
+ const endIdx = existing.indexOf(KEYNV_BLOCK_END);
7224
+ if (startIdx >= 0 && endIdx > startIdx) {
7225
+ const before = existing.slice(0, startIdx);
7226
+ const after = existing.slice(endIdx + KEYNV_BLOCK_END.length);
7227
+ const next = `${before}${block}${after}`;
7228
+ if (next === existing) return "unchanged";
7229
+ writeFileSync(path, next);
7230
+ return "updated";
7231
+ }
7232
+ const sep = existing.length === 0 ? "" : existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
7233
+ const trailingNewline = existing.endsWith("\n") ? "" : "\n";
7234
+ writeFileSync(path, `${existing}${trailingNewline}${sep}${block}
7235
+ `);
7236
+ return "appended";
7237
+ }
7238
+ var AGENTS_FILE_BASENAME, KEYNV_BLOCK_START, KEYNV_BLOCK_END;
7239
+ var init_aiContext = __esm({
7240
+ "src/init/aiContext.ts"() {
7241
+ AGENTS_FILE_BASENAME = "AGENTS.md";
7242
+ KEYNV_BLOCK_START = "<!-- keynv:start -- generated by `keynv init`; safe to leave intact, will be refreshed on re-run -->";
7243
+ KEYNV_BLOCK_END = "<!-- keynv:end -->";
7244
+ }
7245
+ });
7024
7246
  function findProjectRoot(startDir) {
7025
- let dir = resolve(startDir);
7026
- for (let i = 0; i < 64; i++) {
7247
+ const result = walkUp(startDir, (dir) => {
7027
7248
  for (const marker of PROJECT_MARKERS) {
7028
- const candidate = join(dir, marker);
7029
- if (existsSync(candidate)) {
7030
- return buildRoot(dir, marker);
7249
+ if (existsSync(join(dir, marker))) {
7250
+ return { dir, marker };
7031
7251
  }
7032
7252
  }
7033
7253
  if (existsSync(join(dir, GIT_MARKER))) {
7034
- return buildRoot(dir, GIT_MARKER);
7254
+ return { dir, marker: GIT_MARKER };
7035
7255
  }
7036
- const parent = dirname(dir);
7037
- if (parent === dir) return null;
7038
- dir = parent;
7039
- }
7040
- return null;
7256
+ return null;
7257
+ });
7258
+ if (!result) return null;
7259
+ return buildRoot(result.dir, result.marker);
7041
7260
  }
7042
7261
  function buildRoot(dir, marker) {
7043
7262
  let suggestedName = basename(dir);
@@ -7051,16 +7270,20 @@ function buildRoot(dir, marker) {
7051
7270
  }
7052
7271
  if (pkg.scripts && typeof pkg.scripts === "object") {
7053
7272
  scripts = Object.fromEntries(
7054
- Object.entries(pkg.scripts).filter(
7055
- ([, v2]) => typeof v2 === "string"
7056
- )
7273
+ Object.entries(pkg.scripts).filter(([, v2]) => typeof v2 === "string")
7057
7274
  );
7058
7275
  }
7059
7276
  } catch {
7060
7277
  invalid = true;
7061
7278
  }
7062
7279
  }
7063
- return { path: dir, suggestedName, marker, packageJsonScripts: scripts, packageJsonInvalid: invalid };
7280
+ return {
7281
+ path: dir,
7282
+ suggestedName,
7283
+ marker,
7284
+ packageJsonScripts: scripts,
7285
+ packageJsonInvalid: invalid
7286
+ };
7064
7287
  }
7065
7288
  function findEnvFiles(rootDir) {
7066
7289
  let entries;
@@ -7118,6 +7341,7 @@ function hasExistingKeynvEnv(rootDir) {
7118
7341
  var PROJECT_MARKERS, GIT_MARKER, ENV_GLOB, ENV_EXAMPLE, KEYNV_ENV_BASENAME;
7119
7342
  var init_detect = __esm({
7120
7343
  "src/init/detect.ts"() {
7344
+ init_fs();
7121
7345
  PROJECT_MARKERS = [
7122
7346
  "package.json",
7123
7347
  "pyproject.toml",
@@ -7134,221 +7358,6 @@ var init_detect = __esm({
7134
7358
  KEYNV_ENV_BASENAME = ".keynv.env";
7135
7359
  }
7136
7360
  });
7137
- function aiContextBody() {
7138
- return `## keynv (secrets)
7139
-
7140
- This project uses [keynv](https://keynv.dev) for secrets. There is a \`.keynv.env\` file at the project root containing **alias references** like \`OPENAI_API_KEY=@projectname.dev.openai-key\` \u2014 these are NOT real values, they are references to secrets stored in the keynv vault.
7141
-
7142
- ### Mental model
7143
-
7144
- - **\`.keynv.env\`** \u2014 checked into git, contains aliases. Safe to read, edit, commit.
7145
- - **Vault** \u2014 holds the real values. The \`keynv\` CLI reads it on demand.
7146
- - **\`keynv exec -- <command>\`** \u2014 forks the command in a subprocess where the real values are injected into env vars. The parent process (where you, the AI agent, run) NEVER sees the real values.
7147
- - **Redactor** \u2014 pipes subprocess stdout/stderr through a masker before they reach this terminal. Even if a script accidentally echoes a secret, you see \`<REDACTED:...>\` here.
7148
-
7149
- ### What you should do
7150
-
7151
- When the user asks about secrets or env vars, follow this decision tree:
7152
-
7153
- | User intent | Run / suggest |
7154
- |---|---|
7155
- | "Add a new API key" / "set up a secret" | \`keynv secret create\` (interactive prompt picks project + env + key) |
7156
- | "I have a \`.env\`, migrate it" | \`keynv init\` (you're seeing this file because that already happened) |
7157
- | "Run the app / dev server / tests" | \`keynv exec -- <their existing command>\` (auto-loads \`.keynv.env\`) |
7158
- | "Show me the value of X" | \`keynv secret get @alias\` \u2014 but copy to clipboard, do NOT print the value into chat / terminal |
7159
- | "Rotate this key" | \`keynv secret rotate @alias\` |
7160
- | "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\`) |
7161
- | "Who has access?" | \`keynv member list <project>\` |
7162
-
7163
- ### Hard rules \u2014 do not violate
7164
-
7165
- 1. **Never print resolved secret values** to the chat, terminal, log, or any file. The user's editor / scrollback / chat history is an attack surface.
7166
- 2. **Never write a \`.env\` file** (or any file) containing real secret values. The whole point of keynv is that those don't exist on disk.
7167
- 3. **Never edit \`.keynv.env\` to substitute resolved values for aliases.** The file should only ever contain \`@project.env.key\` references and plain non-secret literals.
7168
- 4. **Use \`keynv exec --\` to run commands that need env vars.** Don't try to load \`.keynv.env\` yourself by parsing it manually; the wrapper handles resolution + redaction + subprocess isolation.
7169
- 5. **If a tool fails with "blocked by keynv-guard" or similar**, do NOT try to work around it. Tell the user \u2014 that's a sign of a misconfiguration, not an obstacle to bypass.
7170
- 6. **Treat the alias literal (\`@project.env.key\`) as the canonical reference**, never the resolved value. When you write code, configs, or commands that need a secret, write the alias.
7171
-
7172
- ### Quick reference
7173
-
7174
- \`\`\`bash
7175
- keynv # interactive TUI menu (pick projects, secrets, members)
7176
- keynv exec -- <cmd> # run cmd with .keynv.env loaded
7177
- keynv secret create # walk through creating a new secret
7178
- keynv secret list <project> # list aliases (no values)
7179
- keynv whoami # who am I logged in as
7180
- keynv --help # full command list
7181
- \`\`\`
7182
-
7183
- If \`.keynv.env\` is missing or empty, the user probably hasn't run \`keynv init\` yet \u2014 suggest they do so before they paste a secret into a \`.env\`.`;
7184
- }
7185
- function renderKeynvBlock() {
7186
- return `${KEYNV_BLOCK_START}
7187
- ${aiContextBody()}
7188
- ${KEYNV_BLOCK_END}`;
7189
- }
7190
- function writeAiContext(rootPath) {
7191
- const path = join(rootPath, AGENTS_FILE_BASENAME);
7192
- const block = renderKeynvBlock();
7193
- if (!existsSync(path)) {
7194
- const initial = `# Agent guidance for this project
7195
-
7196
- ${block}
7197
- `;
7198
- writeFileSync(path, initial);
7199
- return "created";
7200
- }
7201
- const existing = readFileSync(path, "utf8");
7202
- const startIdx = existing.indexOf(KEYNV_BLOCK_START);
7203
- const endIdx = existing.indexOf(KEYNV_BLOCK_END);
7204
- if (startIdx >= 0 && endIdx > startIdx) {
7205
- const before = existing.slice(0, startIdx);
7206
- const after = existing.slice(endIdx + KEYNV_BLOCK_END.length);
7207
- const next = `${before}${block}${after}`;
7208
- if (next === existing) return "unchanged";
7209
- writeFileSync(path, next);
7210
- return "updated";
7211
- }
7212
- const sep = existing.length === 0 ? "" : existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
7213
- const trailingNewline = existing.endsWith("\n") ? "" : "\n";
7214
- writeFileSync(path, `${existing}${trailingNewline}${sep}${block}
7215
- `);
7216
- return "appended";
7217
- }
7218
- var AGENTS_FILE_BASENAME, KEYNV_BLOCK_START, KEYNV_BLOCK_END;
7219
- var init_aiContext = __esm({
7220
- "src/init/aiContext.ts"() {
7221
- AGENTS_FILE_BASENAME = "AGENTS.md";
7222
- KEYNV_BLOCK_START = "<!-- keynv:start -- generated by `keynv init`; safe to leave intact, will be refreshed on re-run -->";
7223
- KEYNV_BLOCK_END = "<!-- keynv:end -->";
7224
- }
7225
- });
7226
-
7227
- // src/init/heuristics.ts
7228
- function shannonEntropyBits(s) {
7229
- if (s.length === 0) return 0;
7230
- const counts = /* @__PURE__ */ new Map();
7231
- for (const ch of s) counts.set(ch, (counts.get(ch) ?? 0) + 1);
7232
- let h2 = 0;
7233
- for (const c2 of counts.values()) {
7234
- const p2 = c2 / s.length;
7235
- h2 -= p2 * Math.log2(p2);
7236
- }
7237
- return h2;
7238
- }
7239
- function nameMatchesLiteralPrefix(name) {
7240
- const upper = name.toUpperCase();
7241
- return NAME_LITERAL_PREFIX.some((p2) => upper.startsWith(p2));
7242
- }
7243
- function nameMatchesSecretSuffix(name) {
7244
- const upper = name.toUpperCase();
7245
- for (const { suffix, hint } of NAME_SECRET_SUFFIXES) {
7246
- if (upper.endsWith(suffix) || upper.endsWith(`_${suffix}`)) {
7247
- return { matched: true, hint };
7248
- }
7249
- }
7250
- return { matched: false, hint: "" };
7251
- }
7252
- function classifyEntry(name, value) {
7253
- const upper = name.toUpperCase();
7254
- if (NAME_FRAMEWORK_MANAGED.has(upper)) {
7255
- return { verdict: "skip", hint: "framework/shell-managed" };
7256
- }
7257
- if (NAME_LITERAL_EXACT.has(upper)) {
7258
- return { verdict: "literal", hint: "common config var" };
7259
- }
7260
- if (nameMatchesLiteralPrefix(name)) {
7261
- return { verdict: "literal", hint: "public env, build-time bundled" };
7262
- }
7263
- if (value.length === 0) {
7264
- return { verdict: "literal", hint: "empty" };
7265
- }
7266
- for (const { re, hint } of VALUE_PATTERNS) {
7267
- if (re.test(value)) return { verdict: "secret", hint };
7268
- }
7269
- const suffix = nameMatchesSecretSuffix(name);
7270
- if (suffix.matched) {
7271
- return { verdict: "secret", hint: suffix.hint };
7272
- }
7273
- if (NAME_DB_URL.test(name)) {
7274
- return { verdict: "secret", hint: "database URL" };
7275
- }
7276
- if (value.length >= 32) {
7277
- const bits = shannonEntropyBits(value);
7278
- if (bits >= 3.5) {
7279
- return { verdict: "secret", hint: `${value.length}-char random-looking string` };
7280
- }
7281
- }
7282
- return { verdict: "ambiguous", hint: "" };
7283
- }
7284
- function previewValue(value, max = 40) {
7285
- if (value.length === 0) return "(empty)";
7286
- if (value.length <= max) return value;
7287
- return `${value.slice(0, max - 1)}\u2026`;
7288
- }
7289
- var NAME_FRAMEWORK_MANAGED, NAME_LITERAL_EXACT, NAME_LITERAL_PREFIX, NAME_SECRET_SUFFIXES, NAME_DB_URL, VALUE_PATTERNS;
7290
- var init_heuristics = __esm({
7291
- "src/init/heuristics.ts"() {
7292
- NAME_FRAMEWORK_MANAGED = /* @__PURE__ */ new Set([
7293
- "NODE_ENV",
7294
- "PORT",
7295
- "HOST",
7296
- "HOSTNAME",
7297
- "PATH",
7298
- "HOME",
7299
- "USER",
7300
- "SHELL",
7301
- "PWD",
7302
- "CI"
7303
- ]);
7304
- NAME_LITERAL_EXACT = /* @__PURE__ */ new Set([
7305
- "DEBUG",
7306
- "LOG_LEVEL",
7307
- "LOGLEVEL",
7308
- "TZ",
7309
- "LANG",
7310
- "LC_ALL",
7311
- "NODE_OPTIONS",
7312
- "NPM_CONFIG_LOGLEVEL",
7313
- "TS_NODE_PROJECT"
7314
- ]);
7315
- NAME_LITERAL_PREFIX = ["NEXT_PUBLIC_", "VITE_", "REACT_APP_", "PUBLIC_", "EXPO_PUBLIC_"];
7316
- NAME_SECRET_SUFFIXES = [
7317
- { suffix: "PRIVATE_KEY", hint: "private key" },
7318
- { suffix: "API_KEY", hint: "API key" },
7319
- { suffix: "ACCESS_KEY", hint: "access key" },
7320
- { suffix: "SECRET_KEY", hint: "secret key" },
7321
- { suffix: "SECRET", hint: "secret" },
7322
- { suffix: "PASSWORD", hint: "password" },
7323
- { suffix: "PASSPHRASE", hint: "passphrase" },
7324
- { suffix: "TOKEN", hint: "token" },
7325
- { suffix: "KEY", hint: "key" },
7326
- { suffix: "CREDENTIALS", hint: "credentials" },
7327
- { suffix: "AUTH", hint: "auth" },
7328
- { suffix: "DSN", hint: "connection string" }
7329
- ];
7330
- NAME_DB_URL = /^(DATABASE|DB|POSTGRES|MYSQL|MONGO|REDIS)_URL$/i;
7331
- VALUE_PATTERNS = [
7332
- { re: /^sk-proj-/, hint: "OpenAI project key" },
7333
- { re: /^sk-[A-Za-z0-9]{20,}/, hint: "OpenAI key" },
7334
- { re: /^sk_live_/, hint: "Stripe live key" },
7335
- { re: /^sk_test_/, hint: "Stripe test key" },
7336
- { re: /^pk_live_/, hint: "Stripe publishable \u2014 often public, double-check" },
7337
- { re: /^xoxb-/, hint: "Slack bot token" },
7338
- { re: /^xoxp-/, hint: "Slack user token" },
7339
- { re: /^ghp_[A-Za-z0-9]{30,}/, hint: "GitHub personal access token" },
7340
- { re: /^github_pat_/, hint: "GitHub fine-grained PAT" },
7341
- { re: /^gho_/, hint: "GitHub OAuth token" },
7342
- { re: /^AKIA[0-9A-Z]{16}$/, hint: "AWS access key id" },
7343
- { re: /^ASIA[0-9A-Z]{16}$/, hint: "AWS temporary key id" },
7344
- { re: /^AIza[0-9A-Za-z_-]{35}$/, hint: "Google API key" },
7345
- { re: /^ya29\./, hint: "Google OAuth access token" },
7346
- { re: /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, hint: "JWT" },
7347
- { re: /^-----BEGIN [A-Z ]+PRIVATE KEY-----/, hint: "PEM private key" },
7348
- { re: /^([a-z]+):\/\/[^@]+:[^@]+@/i, hint: "connection string with credentials" }
7349
- ];
7350
- }
7351
- });
7352
7361
 
7353
7362
  // src/init/scriptWrap.ts
7354
7363
  function analyzeScript(name, command) {
@@ -7641,7 +7650,9 @@ ${detail}`
7641
7650
  }
7642
7651
  const totalEntries = [...perEnv.values()].reduce((n, arr) => n + arr.length, 0);
7643
7652
  if (totalEntries === 0) {
7644
- R2.info("All env files were empty or only contained framework-managed vars. Nothing to upload.");
7653
+ R2.info(
7654
+ "All env files were empty or only contained framework-managed vars. Nothing to upload."
7655
+ );
7645
7656
  ye("Done.");
7646
7657
  return { exitCode: 0 };
7647
7658
  }
@@ -7693,10 +7704,10 @@ ${detail}`
7693
7704
  const planSummary = [
7694
7705
  `Project: ${projectChoice.name}${projectChoice.created ? " (will be created)" : ""}`,
7695
7706
  `Environments: ${distinctEnvs.join(", ")}`,
7696
- `Per-env breakdown:`,
7707
+ "Per-env breakdown:",
7697
7708
  perEnvCounts,
7698
7709
  `Script wraps: ${scriptWrapSelection.length}`,
7699
- `Original .env: delete after upload`,
7710
+ "Original .env: delete after upload",
7700
7711
  opts.dryRun ? "Dry-run: no changes will be made." : ""
7701
7712
  ].filter(Boolean).join("\n");
7702
7713
  Se(planSummary, "About to apply");
@@ -7731,7 +7742,11 @@ ${detail}`
7731
7742
  method: "POST",
7732
7743
  body: { env: env2, key: aliasKey, value: e2.value }
7733
7744
  });
7734
- const alias2 = buildAlias({ project: projectChoice.name, environment: env2, key: aliasKey });
7745
+ const alias2 = buildAlias({
7746
+ project: projectChoice.name,
7747
+ environment: env2,
7748
+ key: aliasKey
7749
+ });
7735
7750
  if (alias2 === null) {
7736
7751
  failed.push({
7737
7752
  env: env2,
@@ -7753,7 +7768,9 @@ ${detail}`
7753
7768
  if (failed.length === 0) {
7754
7769
  s.stop(`Uploaded ${totalToUpload} secret${totalToUpload === 1 ? "" : "s"}`);
7755
7770
  } else {
7756
- s.error(`${totalToUpload - failed.length}/${totalToUpload} uploaded; ${failed.length} failed`);
7771
+ s.error(
7772
+ `${totalToUpload - failed.length}/${totalToUpload} uploaded; ${failed.length} failed`
7773
+ );
7757
7774
  for (const f of failed) R2.warn(` [${f.env}] ${f.name}: ${f.reason}`);
7758
7775
  }
7759
7776
  }
@@ -7968,7 +7985,9 @@ async function ensureProjectAndEnvs(client, projectChoice, distinctEnvs) {
7968
7985
  body: envBodyFor(envName)
7969
7986
  });
7970
7987
  } catch (err) {
7971
- s.error(`Failed to add env "${envName}": ${err instanceof Error ? err.message : String(err)}`);
7988
+ s.error(
7989
+ `Failed to add env "${envName}": ${err instanceof Error ? err.message : String(err)}`
7990
+ );
7972
7991
  return null;
7973
7992
  }
7974
7993
  }
@@ -7987,7 +8006,8 @@ function composeKeynvEnv(opts) {
7987
8006
  const { uploadedAliases, literals, mergeWithExisting } = opts;
7988
8007
  const lines = [];
7989
8008
  if (mergeWithExisting !== null) {
7990
- const trimmed = mergeWithExisting.replace(/\n+$/, "");
8009
+ const normalized = mergeWithExisting.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
8010
+ const trimmed = normalized.replace(/\n+$/, "");
7991
8011
  if (trimmed.length > 0) {
7992
8012
  lines.push(...trimmed.split("\n"));
7993
8013
  lines.push("");
@@ -8014,7 +8034,7 @@ function composeKeynvEnv(opts) {
8014
8034
  }
8015
8035
  }
8016
8036
  if (mergeWithExisting !== null) {
8017
- lines.push(`# <<< keynv init <<<`);
8037
+ lines.push("# <<< keynv init <<<");
8018
8038
  }
8019
8039
  return lines;
8020
8040
  }
@@ -8035,11 +8055,11 @@ function updatePackageJsonScripts(path, originalScripts, selectedNames) {
8035
8055
  }
8036
8056
  var init_init = __esm({
8037
8057
  "src/ui/flows/init.ts"() {
8038
- init_dist();
8039
8058
  init_dist5();
8059
+ init_dist();
8040
8060
  init_envFile();
8041
- init_detect();
8042
8061
  init_aiContext();
8062
+ init_detect();
8043
8063
  init_heuristics();
8044
8064
  init_scriptWrap();
8045
8065
  init_cancel();
@@ -8047,30 +8067,122 @@ var init_init = __esm({
8047
8067
  }
8048
8068
  });
8049
8069
 
8050
- // src/ui/helpers/pickSecret.ts
8051
- async function listSecrets(client, projectId2) {
8052
- const data = await client.request(
8053
- `/v1/projects/${projectId2}/secrets`
8054
- );
8055
- return data.secrets;
8070
+ // src/ui/helpers/tty.ts
8071
+ var tty_exports = {};
8072
+ __export(tty_exports, {
8073
+ isInteractive: () => isInteractive
8074
+ });
8075
+ function isInteractive() {
8076
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
8056
8077
  }
8057
- async function pickSecret(client, projectId2, message = "Secret:") {
8058
- const secrets = await listSecrets(client, projectId2);
8059
- if (secrets.length === 0) return null;
8060
- const value = await Ee({
8061
- message,
8062
- options: secrets.map((s) => ({
8063
- value: s.alias,
8064
- label: s.alias,
8065
- hint: `v${s.version}`
8066
- }))
8078
+ var init_tty = __esm({
8079
+ "src/ui/helpers/tty.ts"() {
8080
+ }
8081
+ });
8082
+ function sleep(ms) {
8083
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
8084
+ }
8085
+ async function errorMessage(res, fallback) {
8086
+ const text = await res.text();
8087
+ if (!text) return fallback;
8088
+ try {
8089
+ const parsed = JSON.parse(text);
8090
+ return parsed.error?.message ?? fallback;
8091
+ } catch {
8092
+ return fallback;
8093
+ }
8094
+ }
8095
+ function openBrowser(url) {
8096
+ try {
8097
+ const os = platform();
8098
+ const command = os === "win32" ? "cmd" : os === "darwin" ? "open" : "xdg-open";
8099
+ const args = os === "win32" ? ["/c", "start", "", url] : [url];
8100
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
8101
+ child.unref();
8102
+ return true;
8103
+ } catch {
8104
+ return false;
8105
+ }
8106
+ }
8107
+ async function runBrowserAuth(serverUrl) {
8108
+ const startRes = await fetch(new URL("/v1/auth/cli/browser/start", serverUrl).toString(), {
8109
+ method: "POST",
8110
+ headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
8111
+ body: JSON.stringify({ device_name: hostname() })
8067
8112
  });
8068
- if (q(value)) return null;
8069
- return secrets.find((s) => s.alias === value) ?? null;
8113
+ if (!startRes.ok) {
8114
+ throw new BrowserAuthError(
8115
+ await errorMessage(startRes, `Browser auth failed to start (${startRes.status}).`)
8116
+ );
8117
+ }
8118
+ const start = await startRes.json();
8119
+ const opened = openBrowser(start.verification_uri_complete);
8120
+ if (opened) {
8121
+ process.stderr.write(
8122
+ `
8123
+ Your code: ${start.user_code}
8124
+ Complete auth in your browser, then return here.
8125
+
8126
+ `
8127
+ );
8128
+ } else {
8129
+ process.stderr.write(
8130
+ `
8131
+ Could not open a browser automatically.
8132
+ Open this URL manually:
8133
+
8134
+ ${start.verification_uri_complete}
8135
+
8136
+ Your code: ${start.user_code}
8137
+ Waiting for you to complete auth in the browser...
8138
+
8139
+ `
8140
+ );
8141
+ }
8142
+ const deadline = Date.now() + start.expires_in * 1e3;
8143
+ const intervalMs = Math.max(1, start.interval) * 1e3;
8144
+ while (Date.now() < deadline) {
8145
+ await sleep(intervalMs);
8146
+ const pollRes = await fetch(new URL("/v1/auth/cli/browser/poll", serverUrl).toString(), {
8147
+ method: "POST",
8148
+ headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
8149
+ body: JSON.stringify({ device_code: start.device_code })
8150
+ });
8151
+ if (pollRes.status === 202) continue;
8152
+ if (!pollRes.ok) {
8153
+ throw new BrowserAuthError(
8154
+ await errorMessage(pollRes, `Browser auth failed (${pollRes.status}).`)
8155
+ );
8156
+ }
8157
+ const data = await pollRes.json();
8158
+ return {
8159
+ auth_kind: "session",
8160
+ server_url: serverUrl,
8161
+ user_id: data.user.id,
8162
+ email: data.user.email,
8163
+ org_id: data.user.org_id,
8164
+ org_role: data.user.org_role,
8165
+ access_token: data.access_token,
8166
+ refresh_token: data.refresh_token,
8167
+ access_expires_at: new Date(Date.now() + data.expires_in * 1e3).toISOString()
8168
+ };
8169
+ }
8170
+ throw new BrowserAuthError("Browser auth timed out. Run `keynv login` to try again.");
8070
8171
  }
8071
- var init_pickSecret = __esm({
8072
- "src/ui/helpers/pickSecret.ts"() {
8073
- init_dist5();
8172
+ var BrowserAuthError;
8173
+ var init_browser_auth = __esm({
8174
+ "src/client/browser-auth.ts"() {
8175
+ init_version();
8176
+ BrowserAuthError = class extends Error {
8177
+ };
8178
+ }
8179
+ });
8180
+
8181
+ // src/client/defaults.ts
8182
+ var DEFAULT_SERVER_URL;
8183
+ var init_defaults = __esm({
8184
+ "src/client/defaults.ts"() {
8185
+ DEFAULT_SERVER_URL = "https://api.keynv.dev";
8074
8186
  }
8075
8187
  });
8076
8188
 
@@ -8098,6 +8210,33 @@ var init_pickEnv = __esm({
8098
8210
  init_dist5();
8099
8211
  }
8100
8212
  });
8213
+
8214
+ // src/ui/helpers/pickSecret.ts
8215
+ async function listSecrets(client, projectId2) {
8216
+ const data = await client.request(
8217
+ `/v1/projects/${projectId2}/secrets`
8218
+ );
8219
+ return data.secrets;
8220
+ }
8221
+ async function pickSecret(client, projectId2, message = "Secret:") {
8222
+ const secrets = await listSecrets(client, projectId2);
8223
+ if (secrets.length === 0) return null;
8224
+ const value = await Ee({
8225
+ message,
8226
+ options: secrets.map((s) => ({
8227
+ value: s.alias,
8228
+ label: s.alias,
8229
+ hint: `v${s.version}`
8230
+ }))
8231
+ });
8232
+ if (q(value)) return null;
8233
+ return secrets.find((s) => s.alias === value) ?? null;
8234
+ }
8235
+ var init_pickSecret = __esm({
8236
+ "src/ui/helpers/pickSecret.ts"() {
8237
+ init_dist5();
8238
+ }
8239
+ });
8101
8240
  async function runSecretsFlow(client, project) {
8102
8241
  let target = project;
8103
8242
  if (!target) {
@@ -8161,7 +8300,10 @@ async function runSecretMenu(client, project, alias2) {
8161
8300
  }
8162
8301
  } else if (choice === "rotate") {
8163
8302
  const newValue = unwrap(
8164
- await Ce({ message: "New value", validate: (v2) => v2 && v2.length ? void 0 : "required" })
8303
+ await Ce({
8304
+ message: "New value",
8305
+ validate: (v2) => v2?.length ? void 0 : "required"
8306
+ })
8165
8307
  );
8166
8308
  const data = await client.request(`${path}/rotate`, {
8167
8309
  method: "POST",
@@ -8169,9 +8311,7 @@ async function runSecretMenu(client, project, alias2) {
8169
8311
  });
8170
8312
  R2.success(`Rotated ${data.alias} \u2192 v${data.version}`);
8171
8313
  } else if (choice === "delete") {
8172
- const confirmed = unwrap(
8173
- await ue({ message: `Delete ${alias2}?`, initialValue: false })
8174
- );
8314
+ const confirmed = unwrap(await ue({ message: `Delete ${alias2}?`, initialValue: false }));
8175
8315
  if (!confirmed) continue;
8176
8316
  await client.request(path, { method: "DELETE" });
8177
8317
  R2.success(`Deleted ${alias2}`);
@@ -8180,11 +8320,13 @@ async function runSecretMenu(client, project, alias2) {
8180
8320
  }
8181
8321
  }
8182
8322
  async function copyToClipboard(value) {
8183
- const platform = process.platform;
8184
- const cmd = platform === "darwin" ? ["pbcopy", []] : platform === "win32" ? ["clip", []] : ["xclip", ["-selection", "clipboard"]];
8323
+ const platform2 = process.platform;
8324
+ const cmd = platform2 === "darwin" ? ["pbcopy", []] : platform2 === "win32" ? ["clip", []] : ["xclip", ["-selection", "clipboard"]];
8185
8325
  return new Promise((resolve3) => {
8186
8326
  try {
8187
- const child = spawn(cmd[0], cmd[1], { stdio: ["pipe", "ignore", "ignore"] });
8327
+ const child = spawn(cmd[0], cmd[1], {
8328
+ stdio: ["pipe", "ignore", "ignore"]
8329
+ });
8188
8330
  child.on("error", () => resolve3(false));
8189
8331
  child.on("exit", (code) => resolve3(code === 0));
8190
8332
  child.stdin.end(value);
@@ -8211,7 +8353,7 @@ async function promptNewSecret(client, project) {
8211
8353
  }),
8212
8354
  value: () => Ce({
8213
8355
  message: "Value (hidden)",
8214
- validate: (v2) => v2 && v2.length ? void 0 : "required"
8356
+ validate: (v2) => v2?.length ? void 0 : "required"
8215
8357
  })
8216
8358
  },
8217
8359
  {
@@ -8240,8 +8382,8 @@ async function createSecretInteractive(client, project) {
8240
8382
  }
8241
8383
  var init_secret = __esm({
8242
8384
  "src/ui/flows/secret.ts"() {
8243
- init_dist();
8244
8385
  init_dist5();
8386
+ init_dist();
8245
8387
  init_cancel();
8246
8388
  init_pickEnv();
8247
8389
  init_pickProject();
@@ -28574,7 +28716,7 @@ var require_utils_webcrypto = __commonJS({
28574
28716
  var nodeCrypto = __require("crypto");
28575
28717
  module.exports = {
28576
28718
  postgresMd5PasswordHash,
28577
- randomBytes,
28719
+ randomBytes: randomBytes2,
28578
28720
  deriveKey,
28579
28721
  sha256,
28580
28722
  hashByName,
@@ -28584,7 +28726,7 @@ var require_utils_webcrypto = __commonJS({
28584
28726
  var webCrypto = nodeCrypto.webcrypto || globalThis.crypto;
28585
28727
  var subtleCrypto = webCrypto.subtle;
28586
28728
  var textEncoder = new TextEncoder();
28587
- function randomBytes(length) {
28729
+ function randomBytes2(length) {
28588
28730
  return webCrypto.getRandomValues(Buffer.alloc(length));
28589
28731
  }
28590
28732
  async function md5(string) {
@@ -28980,11 +29122,11 @@ var require_pg_connection_string = __commonJS({
28980
29122
  config.client_encoding = result.searchParams.get("encoding");
28981
29123
  return config;
28982
29124
  }
28983
- const hostname = dummyHost ? "" : result.hostname;
29125
+ const hostname2 = dummyHost ? "" : result.hostname;
28984
29126
  if (!config.host) {
28985
- config.host = decodeURIComponent(hostname);
28986
- } else if (hostname && /^%2f/i.test(hostname)) {
28987
- result.pathname = hostname + result.pathname;
29127
+ config.host = decodeURIComponent(hostname2);
29128
+ } else if (hostname2 && /^%2f/i.test(hostname2)) {
29129
+ result.pathname = hostname2 + result.pathname;
28988
29130
  }
28989
29131
  if (!config.port) {
28990
29132
  config.port = result.port;
@@ -40090,9 +40232,9 @@ var require_cluster = __commonJS({
40090
40232
  }
40091
40233
  });
40092
40234
  }
40093
- resolveSrv(hostname) {
40235
+ resolveSrv(hostname2) {
40094
40236
  return new Promise((resolve3, reject) => {
40095
- this.options.resolveSrv(hostname, (err, records) => {
40237
+ this.options.resolveSrv(hostname2, (err, records) => {
40096
40238
  if (err) {
40097
40239
  return reject(err);
40098
40240
  }
@@ -40114,14 +40256,14 @@ var require_cluster = __commonJS({
40114
40256
  });
40115
40257
  });
40116
40258
  }
40117
- dnsLookup(hostname) {
40259
+ dnsLookup(hostname2) {
40118
40260
  return new Promise((resolve3, reject) => {
40119
- this.options.dnsLookup(hostname, (err, address) => {
40261
+ this.options.dnsLookup(hostname2, (err, address) => {
40120
40262
  if (err) {
40121
- debug2("failed to resolve hostname %s to IP: %s", hostname, err.message);
40263
+ debug2("failed to resolve hostname %s to IP: %s", hostname2, err.message);
40122
40264
  reject(err);
40123
40265
  } else {
40124
- debug2("resolved hostname %s to IP %s", hostname, address);
40266
+ debug2("resolved hostname %s to IP %s", hostname2, address);
40125
40267
  resolve3(address);
40126
40268
  }
40127
40269
  });
@@ -42446,85 +42588,27 @@ var init_audit2 = __esm({
42446
42588
  });
42447
42589
 
42448
42590
  // src/ui/flows/login.ts
42449
- async function runLoginFlow(client) {
42450
- const answers = await he(
42451
- {
42452
- server: () => Re({
42453
- message: "Server URL",
42454
- placeholder: "http://localhost:8080",
42455
- defaultValue: "http://localhost:8080"
42456
- }),
42457
- email: () => Re({
42458
- message: "Email",
42459
- validate: (v2) => v2 && v2.includes("@") ? void 0 : "enter an email"
42460
- }),
42461
- password: () => Ce({
42462
- message: "Password",
42463
- validate: (v2) => v2 && v2.length >= 1 ? void 0 : "required"
42464
- })
42465
- },
42466
- {
42467
- onCancel: () => {
42468
- throw new UserCancelled();
42469
- }
42470
- }
42471
- );
42472
- const server = answers.server || "http://localhost:8080";
42591
+ async function runLoginFlow(client, options = {}) {
42592
+ const server = options.server ?? DEFAULT_SERVER_URL;
42473
42593
  const s = ft();
42474
- s.start("Authenticating");
42475
- let res;
42594
+ s.start("Waiting for authorization");
42595
+ let creds;
42476
42596
  try {
42477
- res = await fetch(new URL("/v1/auth/login", server).toString(), {
42478
- method: "POST",
42479
- headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
42480
- body: JSON.stringify({ email: answers.email, password: answers.password })
42481
- });
42597
+ creds = await runBrowserAuth(server);
42482
42598
  } catch (err) {
42483
- s.error("Network error");
42484
- me(err instanceof Error ? err.message : "unreachable");
42485
- return false;
42486
- }
42487
- if (!res.ok) {
42488
- let msg = `login failed (${res.status})`;
42489
- try {
42490
- const parsed = await res.json();
42491
- msg = parsed.error?.message ?? msg;
42492
- } catch {
42493
- }
42494
- s.error(msg);
42599
+ s.error("Browser login failed");
42600
+ me(err instanceof Error ? err.message : "Unable to authenticate.");
42495
42601
  return false;
42496
42602
  }
42497
- const data = await res.json();
42498
- await saveCredentials({
42499
- server_url: server,
42500
- user_id: data.user.id,
42501
- email: data.user.email,
42502
- org_id: data.user.org_id,
42503
- org_role: data.user.org_role,
42504
- access_token: data.access_token,
42505
- refresh_token: data.refresh_token,
42506
- access_expires_at: new Date(Date.now() + data.expires_in * 1e3).toISOString()
42507
- });
42508
- await client.setCredentials({
42509
- server_url: server,
42510
- user_id: data.user.id,
42511
- email: data.user.email,
42512
- org_id: data.user.org_id,
42513
- org_role: data.user.org_role,
42514
- access_token: data.access_token,
42515
- refresh_token: data.refresh_token,
42516
- access_expires_at: new Date(Date.now() + data.expires_in * 1e3).toISOString()
42517
- });
42518
- s.stop(`Logged in as ${data.user.email}`);
42603
+ await client.setCredentials(creds);
42604
+ s.stop(`Logged in as ${creds.email}`);
42519
42605
  return true;
42520
42606
  }
42521
42607
  var init_login = __esm({
42522
42608
  "src/ui/flows/login.ts"() {
42523
42609
  init_dist5();
42524
- init_http();
42525
- init_store();
42526
- init_version();
42527
- init_cancel();
42610
+ init_browser_auth();
42611
+ init_defaults();
42528
42612
  }
42529
42613
  });
42530
42614
 
@@ -42583,7 +42667,7 @@ async function addMemberInteractive(client, project) {
42583
42667
  {
42584
42668
  email: () => Re({
42585
42669
  message: "Email",
42586
- validate: (v2) => v2 && v2.includes("@") ? void 0 : "enter an email"
42670
+ validate: (v2) => v2?.includes("@") ? void 0 : "enter an email"
42587
42671
  }),
42588
42672
  role: () => Ee({
42589
42673
  message: "Role",
@@ -42682,8 +42766,11 @@ function printDetail(detail) {
42682
42766
  const envLines = detail.environments.map(
42683
42767
  (e2) => ` ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})`
42684
42768
  );
42685
- Se(`${detail.name} (${detail.id})
42686
- ${envLines.join("\n") || " (no environments)"}`, "Project");
42769
+ Se(
42770
+ `${detail.name} (${detail.id})
42771
+ ${envLines.join("\n") || " (no environments)"}`,
42772
+ "Project"
42773
+ );
42687
42774
  }
42688
42775
  async function createProjectInteractive(client) {
42689
42776
  const answers = await he(
@@ -42742,6 +42829,7 @@ async function runMenu() {
42742
42829
  ge(`keynv ${VERSION}`);
42743
42830
  const client = new ApiClient();
42744
42831
  await client.ensureHydrated();
42832
+ let didLogin = false;
42745
42833
  if (!client.isLoggedIn) {
42746
42834
  R2.info("Not logged in.");
42747
42835
  try {
@@ -42750,6 +42838,7 @@ async function runMenu() {
42750
42838
  ye("Login cancelled.");
42751
42839
  return 1;
42752
42840
  }
42841
+ didLogin = true;
42753
42842
  } catch (err) {
42754
42843
  if (err instanceof UserCancelled) {
42755
42844
  me("Login cancelled.");
@@ -42761,6 +42850,20 @@ async function runMenu() {
42761
42850
  const u = client.currentUser;
42762
42851
  if (u) R2.message(`${u.email} (${u.org_role}) @ ${u.server_url}`);
42763
42852
  }
42853
+ if (didLogin) {
42854
+ const root = findProjectRoot(process.cwd());
42855
+ const alreadyInitialized = root !== null && hasExistingKeynvEnv(root.path);
42856
+ if (!alreadyInitialized) {
42857
+ const setup = await ue({
42858
+ message: "Set up this project now?",
42859
+ initialValue: true
42860
+ });
42861
+ if (!q(setup) && setup) {
42862
+ const { runInitFlow: runInitFlow2 } = await Promise.resolve().then(() => (init_init(), init_exports));
42863
+ await runInitFlow2(client, { cwd: process.cwd(), dryRun: false, noScripts: false });
42864
+ }
42865
+ }
42866
+ }
42764
42867
  while (true) {
42765
42868
  let choice;
42766
42869
  try {
@@ -42827,15 +42930,16 @@ var init_menu = __esm({
42827
42930
  "src/ui/menu.ts"() {
42828
42931
  init_dist5();
42829
42932
  init_http();
42933
+ init_detect();
42830
42934
  init_store();
42831
- init_format();
42832
- init_cancel();
42935
+ init_version();
42833
42936
  init_audit2();
42834
42937
  init_login();
42835
42938
  init_member();
42836
42939
  init_project();
42837
42940
  init_secret();
42838
- init_version();
42941
+ init_format();
42942
+ init_cancel();
42839
42943
  }
42840
42944
  });
42841
42945
 
@@ -43516,11 +43620,11 @@ var reducers = {
43516
43620
  return { ...state, options: [{ name: `-c`, value: String(command) }] };
43517
43621
  }
43518
43622
  },
43519
- setError: (state, segment, segmentIndex, errorMessage) => {
43623
+ setError: (state, segment, segmentIndex, errorMessage2) => {
43520
43624
  if (segment === SpecialToken.EndOfInput || segment === SpecialToken.EndOfPartialInput) {
43521
- return { ...state, errorMessage: `${errorMessage}.` };
43625
+ return { ...state, errorMessage: `${errorMessage2}.` };
43522
43626
  } else {
43523
- return { ...state, errorMessage: `${errorMessage} ("${segment}").` };
43627
+ return { ...state, errorMessage: `${errorMessage2} ("${segment}").` };
43524
43628
  }
43525
43629
  },
43526
43630
  setOptionArityError: (state, segment) => {
@@ -44657,11 +44761,10 @@ async function resolveAllAliases(client, argv2, extraStrings = []) {
44657
44761
  return resolved;
44658
44762
  }
44659
44763
  function substitute(text, resolved) {
44764
+ const sorted = [...resolved].sort((a, b2) => b2.alias.literal.length - a.alias.literal.length);
44660
44765
  let out = text;
44661
- for (const r of resolved) {
44662
- if (out.includes(r.alias.literal)) {
44663
- out = out.split(r.alias.literal).join(r.value);
44664
- }
44766
+ for (const r of sorted) {
44767
+ out = out.split(r.alias.literal).join(r.value);
44665
44768
  }
44666
44769
  return out;
44667
44770
  }
@@ -44764,10 +44867,18 @@ var BUILTIN_LINE_PATTERNS = BUILTIN_PATTERNS.filter((p2) => !p2.multiline);
44764
44867
 
44765
44868
  // ../../packages/redactor/dist/entropy.js
44766
44869
  var TOKEN_BOUNDARY_RE = /[\s,;:'"<>(){}[\]=]+/;
44870
+ var DEFAULT_EXCLUDE_PREFIXES = [
44871
+ "sha1-",
44872
+ "sha256:",
44873
+ "sha256-",
44874
+ "sha384-",
44875
+ "sha512-"
44876
+ ];
44767
44877
  var DEFAULTS = {
44768
44878
  enabled: true,
44769
44879
  minLength: 24,
44770
- minBitsPerChar: 4.5
44880
+ minBitsPerChar: 4.5,
44881
+ excludePrefixes: DEFAULT_EXCLUDE_PREFIXES
44771
44882
  };
44772
44883
  function shannonEntropy(s) {
44773
44884
  if (s.length === 0)
@@ -44800,9 +44911,13 @@ function findEntropyMatches(text, opts = {}) {
44800
44911
  const end = i;
44801
44912
  const token = text.slice(start, end);
44802
44913
  if (token.length >= cfg.minLength) {
44803
- const h2 = shannonEntropy(token);
44804
- if (h2 >= cfg.minBitsPerChar) {
44805
- matches.push({ start, end, token });
44914
+ const lower = token.toLowerCase();
44915
+ const excluded = cfg.excludePrefixes.some((p2) => lower.startsWith(p2));
44916
+ if (!excluded) {
44917
+ const h2 = shannonEntropy(token);
44918
+ if (h2 >= cfg.minBitsPerChar) {
44919
+ matches.push({ start, end, token });
44920
+ }
44806
44921
  }
44807
44922
  }
44808
44923
  }
@@ -44946,7 +45061,19 @@ var ENV_ALLOWLIST = [
44946
45061
  "PWD",
44947
45062
  "OLDPWD",
44948
45063
  "TMPDIR",
44949
- "SSH_AUTH_SOCK"
45064
+ "SSH_AUTH_SOCK",
45065
+ // Windows-specific
45066
+ "USERPROFILE",
45067
+ "USERNAME",
45068
+ "COMPUTERNAME",
45069
+ "TEMP",
45070
+ "TMP",
45071
+ "SYSTEMROOT",
45072
+ "SYSTEMDRIVE",
45073
+ "WINDIR",
45074
+ "COMSPEC",
45075
+ "APPDATA",
45076
+ "LOCALAPPDATA"
44950
45077
  ];
44951
45078
  function spawnPrivileged(opts) {
44952
45079
  const startedAt = Date.now();
@@ -44962,7 +45089,8 @@ function spawnPrivileged(opts) {
44962
45089
  const child = spawn(opts.command, opts.args, {
44963
45090
  env: env2,
44964
45091
  stdio,
44965
- detached: false
45092
+ detached: false,
45093
+ shell: process.platform === "win32"
44966
45094
  });
44967
45095
  const literals = opts.resolved.map((r) => r.value).filter((v2) => v2.length > 0);
44968
45096
  if (!opts.noRedact && child.stdout) {
@@ -45026,10 +45154,7 @@ to load a specific file, or set KEYNV_ENV_FILE in the environment.
45026
45154
  spelled \`--from\` to avoid the collision.)
45027
45155
  `,
45028
45156
  examples: [
45029
- [
45030
- "Auto-load .keynv.env from cwd or parents",
45031
- "$0 exec -- next dev"
45032
- ],
45157
+ ["Auto-load .keynv.env from cwd or parents", "$0 exec -- next dev"],
45033
45158
  [
45034
45159
  "Run mysql with the alias substituted at fork-exec time",
45035
45160
  "$0 exec -- mysql -p@billing.dev.db_pass -h db.example.com"
@@ -45093,7 +45218,11 @@ spelled \`--from\` to avoid the collision.)
45093
45218
  `);
45094
45219
  return 2;
45095
45220
  }
45096
- throw err;
45221
+ this.context.stderr.write(
45222
+ `keynv: unexpected error loading env file: ${err instanceof Error ? err.message : String(err)}
45223
+ `
45224
+ );
45225
+ return 2;
45097
45226
  }
45098
45227
  const viaEnvSpecs = [];
45099
45228
  for (const spec of this.viaEnv ?? []) {
@@ -45165,11 +45294,18 @@ spelled \`--from\` to avoid the collision.)
45165
45294
  injectedEnv[spec.name] = value;
45166
45295
  }
45167
45296
  if (envFileLoaded && !this.quiet) {
45168
- const total = envFileLoaded.entries.length;
45169
- const aliasCount = envFileLoaded.entries.filter((e2) => e2.isAlias).length;
45297
+ const aliasEntries = envFileLoaded.entries.filter((e2) => e2.isAlias);
45298
+ const plainEntries = envFileLoaded.entries.filter((e2) => !e2.isAlias);
45299
+ const displayPath = relative(process.cwd(), envFileLoaded.path) || envFileLoaded.path;
45300
+ const parts = [];
45301
+ if (aliasEntries.length > 0) {
45302
+ parts.push(aliasEntries.map((e2) => `${e2.name}=${e2.value}`).join(", ") + " (vault)");
45303
+ }
45304
+ if (plainEntries.length > 0) {
45305
+ parts.push(plainEntries.map((e2) => e2.name).join(", ") + " (plain)");
45306
+ }
45170
45307
  this.context.stderr.write(
45171
- `keynv: loaded ${total} var${total === 1 ? "" : "s"} from ${envFileLoaded.path} (${aliasCount} resolved from vault)
45172
- `
45308
+ `keynv: loaded ${displayPath}` + (parts.length > 0 ? ` \u2014 ${parts.join("; ")}` : "") + "\n"
45173
45309
  );
45174
45310
  }
45175
45311
  const timeoutS = this.timeout ? Number.parseInt(this.timeout, 10) : void 0;
@@ -45209,10 +45345,142 @@ function signalNumber(sig) {
45209
45345
  }
45210
45346
 
45211
45347
  // src/commands/init.ts
45348
+ init_dist();
45212
45349
  init_http();
45213
- init_tty();
45350
+ init_envFile();
45351
+ init_heuristics();
45214
45352
  init_init();
45215
45353
  init_cancel();
45354
+ init_tty();
45355
+
45356
+ // src/commands/project.ts
45357
+ init_http();
45358
+ init_format();
45359
+ function isProjectId(input) {
45360
+ return input.startsWith("p_") || /^[a-f0-9]{20,}$/i.test(input);
45361
+ }
45362
+ async function resolveProjectId(client, input) {
45363
+ if (isProjectId(input)) {
45364
+ return input;
45365
+ }
45366
+ const data = await client.request("/v1/projects");
45367
+ const match = data.projects.find((p2) => p2.name === input);
45368
+ if (!match) throw new Error(`project not found: ${input}`);
45369
+ return match.id;
45370
+ }
45371
+ var ProjectListCommand = class extends Command {
45372
+ static paths = [["project", "list"]];
45373
+ static usage = Command.Usage({
45374
+ description: "List projects visible to the current user."
45375
+ });
45376
+ json = options_exports.Boolean("--json", false);
45377
+ async execute() {
45378
+ const client = new ApiClient();
45379
+ const data = await client.request("/v1/projects");
45380
+ if (this.json) {
45381
+ this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45382
+ `);
45383
+ return 0;
45384
+ }
45385
+ if (data.projects.length === 0) {
45386
+ this.context.stdout.write("no projects\n");
45387
+ return 0;
45388
+ }
45389
+ this.context.stdout.write(
45390
+ `${table(
45391
+ ["name", "id", "created_at"],
45392
+ data.projects.map((p2) => [p2.name, p2.id, p2.created_at])
45393
+ )}
45394
+ `
45395
+ );
45396
+ return 0;
45397
+ }
45398
+ };
45399
+ var ProjectCreateCommand = class extends Command {
45400
+ static paths = [["project", "create"]];
45401
+ static usage = Command.Usage({
45402
+ description: "Create a new project with one or more environments.",
45403
+ examples: [["Two envs", "$0 project create billing --env dev --env prod:production:approval"]]
45404
+ });
45405
+ name = options_exports.String();
45406
+ envs = options_exports.Array("--env", {
45407
+ description: 'Environment spec: name[:tier[:approval]]. Tier \u2208 {production,non-production}; "approval" sets require_approval=true.'
45408
+ });
45409
+ async execute() {
45410
+ const envs = (this.envs ?? ["dev"]).map((spec) => {
45411
+ const [name, tier, approval] = spec.split(":");
45412
+ return {
45413
+ name: name ?? "",
45414
+ tier: tier ?? "non-production",
45415
+ require_approval: approval === "approval"
45416
+ };
45417
+ });
45418
+ const client = new ApiClient();
45419
+ const result = await client.request("/v1/projects", {
45420
+ method: "POST",
45421
+ body: { name: this.name, environments: envs }
45422
+ });
45423
+ this.context.stdout.write(`created project ${result.name} (${result.id})
45424
+ `);
45425
+ for (const e2 of envs) {
45426
+ this.context.stdout.write(
45427
+ ` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
45428
+ `
45429
+ );
45430
+ }
45431
+ return 0;
45432
+ }
45433
+ };
45434
+ var ProjectDescribeCommand = class extends Command {
45435
+ static paths = [["project", "describe"]];
45436
+ static usage = Command.Usage({
45437
+ description: "Show metadata for one project.",
45438
+ examples: [
45439
+ ["By ID", "$0 project describe p_go6rqgwz0wlokdsl55ikn"],
45440
+ ["By name", "$0 project describe myproject"]
45441
+ ]
45442
+ });
45443
+ id = options_exports.String();
45444
+ json = options_exports.Boolean("--json", false);
45445
+ async execute() {
45446
+ const client = new ApiClient();
45447
+ const projectId2 = await resolveProjectId(client, this.id);
45448
+ const data = await client.request(`/v1/projects/${projectId2}`);
45449
+ if (this.json) {
45450
+ this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45451
+ `);
45452
+ return 0;
45453
+ }
45454
+ this.context.stdout.write(`project: ${data.name} (${data.id})
45455
+ `);
45456
+ for (const e2 of data.environments) {
45457
+ this.context.stdout.write(
45458
+ ` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
45459
+ `
45460
+ );
45461
+ }
45462
+ return 0;
45463
+ }
45464
+ };
45465
+ var ProjectDeleteCommand = class extends Command {
45466
+ static paths = [["project", "delete"]];
45467
+ static usage = Command.Usage({ description: "Soft-delete a project." });
45468
+ id = options_exports.String();
45469
+ force = options_exports.Boolean("--force", false);
45470
+ async execute() {
45471
+ if (!this.force) {
45472
+ this.context.stderr.write("keynv: refusing to delete without --force\n");
45473
+ return 2;
45474
+ }
45475
+ const client = new ApiClient();
45476
+ await client.request(`/v1/projects/${this.id}`, { method: "DELETE" });
45477
+ this.context.stdout.write(`deleted project ${this.id}
45478
+ `);
45479
+ return 0;
45480
+ }
45481
+ };
45482
+
45483
+ // src/commands/init.ts
45216
45484
  var InitCommand = class extends Command {
45217
45485
  static paths = [["init"]];
45218
45486
  static usage = Command.Usage({
@@ -45226,6 +45494,11 @@ package.json scripts with \`keynv exec\`.
45226
45494
  Safe to re-run: existing .keynv.env entries are preserved; new
45227
45495
  entries are appended below a marker.
45228
45496
 
45497
+ --dry-run prints the secrets that would be uploaded and the alias
45498
+ mappings that would be written to .keynv.env, then exits without
45499
+ touching any files or making any network calls. Use it to preview
45500
+ what init will do before committing.
45501
+
45229
45502
  Requires an interactive terminal (clack TUI). For scripted
45230
45503
  migration, use the lower-level \`keynv project\` and \`keynv secret\`
45231
45504
  commands directly.
@@ -45233,28 +45506,47 @@ commands directly.
45233
45506
  examples: [
45234
45507
  ["Walk the current project", "$0 init"],
45235
45508
  ["Preview without writing or uploading", "$0 init --dry-run"],
45236
- ["Skip the package.json script-wrapping step", "$0 init --no-scripts"]
45509
+ ["Skip the package.json script-wrapping step", "$0 init --no-scripts"],
45510
+ ["Non-interactive (CI/CD)", "$0 init --env-file .env --project myproject --env dev"]
45237
45511
  ]
45238
45512
  });
45239
45513
  dryRun = options_exports.Boolean("--dry-run", false, {
45240
- description: "Show what would be done without writing files or uploading secrets."
45514
+ description: "Scan .env files and print the secrets that would be uploaded and the alias mappings that would be written \u2014 no files written, no vault calls made."
45241
45515
  });
45242
45516
  noScripts = options_exports.Boolean("--no-scripts", false, {
45243
45517
  description: "Skip the package.json script-wrapping step."
45244
45518
  });
45519
+ envFile = options_exports.String("--env-file", {
45520
+ description: "Path to .env file to migrate (non-interactive mode)."
45521
+ });
45522
+ project = options_exports.String("--project", {
45523
+ description: "Project name or ID (non-interactive mode)."
45524
+ });
45525
+ env = options_exports.String("--env", {
45526
+ description: "Environment name for --env-file (default: dev)."
45527
+ });
45528
+ secret = options_exports.Array("--secret", {
45529
+ description: "KEY=value secret to upload (non-interactive). Can be specified multiple times."
45530
+ });
45245
45531
  async execute() {
45246
- if (!isInteractive()) {
45247
- this.context.stderr.write(
45248
- "keynv init requires an interactive terminal. Use the lower-level commands (`keynv project`, `keynv secret`) for scripted setup.\n"
45249
- );
45250
- return 1;
45251
- }
45252
45532
  const client = new ApiClient();
45253
45533
  await client.ensureHydrated();
45254
45534
  if (!client.isLoggedIn) {
45255
45535
  this.context.stderr.write("keynv: not logged in. Run `keynv login` first.\n");
45256
45536
  return 1;
45257
45537
  }
45538
+ const hasEnvFile = this.envFile != null;
45539
+ const hasSecrets = this.secret != null && this.secret.length > 0;
45540
+ const isNonInteractive = hasEnvFile || hasSecrets;
45541
+ if (isNonInteractive) {
45542
+ return this.runNonInteractive(client);
45543
+ }
45544
+ if (!isInteractive()) {
45545
+ this.context.stderr.write(
45546
+ "keynv init requires an interactive terminal. Use --env-file or --secret for scripted setup.\n"
45547
+ );
45548
+ return 1;
45549
+ }
45258
45550
  try {
45259
45551
  const outcome = await runInitFlow(client, {
45260
45552
  cwd: process.cwd(),
@@ -45270,9 +45562,102 @@ commands directly.
45270
45562
  return 1;
45271
45563
  }
45272
45564
  }
45565
+ async runNonInteractive(client) {
45566
+ const projectName = this.project;
45567
+ if (!projectName) {
45568
+ this.context.stderr.write("keynv: --project is required in non-interactive mode.\n");
45569
+ return 1;
45570
+ }
45571
+ const resolved = await resolveProjectId(client, projectName);
45572
+ if (!resolved) {
45573
+ this.context.stderr.write(`keynv: project not found: ${projectName}
45574
+ `);
45575
+ return 1;
45576
+ }
45577
+ const projectId2 = resolved;
45578
+ const envName = this.env ?? "dev";
45579
+ const secrets = [];
45580
+ if (this.envFile) {
45581
+ try {
45582
+ const content = readFileSync(this.envFile, "utf8");
45583
+ const entries = parseEnvFile(content, this.envFile);
45584
+ for (const e2 of entries) {
45585
+ if (classifyEntry(e2.name, e2.value).verdict !== "skip") {
45586
+ secrets.push({ name: e2.name, value: e2.value });
45587
+ }
45588
+ }
45589
+ } catch (err) {
45590
+ this.context.stderr.write(
45591
+ `keynv: cannot read ${this.envFile}: ${err instanceof Error ? err.message : String(err)}
45592
+ `
45593
+ );
45594
+ return 1;
45595
+ }
45596
+ }
45597
+ if (this.secret) {
45598
+ for (const spec of this.secret) {
45599
+ const eq = spec.indexOf("=");
45600
+ if (eq <= 0) {
45601
+ this.context.stderr.write(`keynv: invalid --secret '${spec}', expected KEY=value
45602
+ `);
45603
+ return 1;
45604
+ }
45605
+ const name = spec.slice(0, eq);
45606
+ const value = spec.slice(eq + 1);
45607
+ secrets.push({ name, value });
45608
+ }
45609
+ }
45610
+ if (secrets.length === 0) {
45611
+ this.context.stdout.write("keynv: nothing to migrate.\n");
45612
+ return 0;
45613
+ }
45614
+ if (this.dryRun) {
45615
+ this.context.stdout.write(
45616
+ `keynv: dry-run \u2014 would upload ${secrets.length} secret(s) to project ${projectName} (${projectId2}) in env ${envName}
45617
+ `
45618
+ );
45619
+ for (const { name } of secrets) {
45620
+ const aliasKey = name.toLowerCase().replace(/_/g, "-");
45621
+ this.context.stdout.write(` ${name}=@${projectName}.${envName}.${aliasKey}
45622
+ `);
45623
+ }
45624
+ return 0;
45625
+ }
45626
+ let uploaded = 0;
45627
+ const failed = [];
45628
+ for (const { name, value } of secrets) {
45629
+ const aliasKey = name.toLowerCase().replace(/_/g, "-");
45630
+ const alias2 = buildAlias({ project: projectName, environment: envName, key: aliasKey });
45631
+ if (!alias2) {
45632
+ failed.push(`${name} (invalid alias key: ${aliasKey})`);
45633
+ continue;
45634
+ }
45635
+ try {
45636
+ await client.request(`/v1/projects/${projectId2}/secrets`, {
45637
+ method: "POST",
45638
+ body: { env: envName, key: aliasKey, value }
45639
+ });
45640
+ uploaded++;
45641
+ } catch (err) {
45642
+ failed.push(`${name}: ${err instanceof Error ? err.message : String(err)}`);
45643
+ }
45644
+ }
45645
+ this.context.stdout.write(
45646
+ `keynv: uploaded ${uploaded}/${secrets.length} secret(s) to ${projectName}.${envName}
45647
+ `
45648
+ );
45649
+ if (failed.length > 0) {
45650
+ for (const f of failed) this.context.stderr.write(` failed: ${f}
45651
+ `);
45652
+ return 1;
45653
+ }
45654
+ return 0;
45655
+ }
45273
45656
  };
45274
45657
 
45275
45658
  // src/commands/login.ts
45659
+ init_browser_auth();
45660
+ init_defaults();
45276
45661
  init_http();
45277
45662
  init_store();
45278
45663
  init_format();
@@ -45333,18 +45718,40 @@ var LoginCommand = class extends Command {
45333
45718
  static usage = Command.Usage({
45334
45719
  description: "Authenticate against a keynv server.",
45335
45720
  examples: [
45336
- ["Interactive", "$0 login --server http://localhost:8080"],
45721
+ ["Browser login", "$0 login"],
45722
+ ["Self-hosted browser login", "$0 login --server http://localhost:8080"],
45723
+ ["Headless token login", "$0 login --server http://... --token kt_..."],
45337
45724
  ["Non-interactive", "$0 login --server http://... --email a@b.com --password ..."]
45338
45725
  ]
45339
45726
  });
45340
45727
  server = options_exports.String("--server", { description: "Server base URL." });
45728
+ token = options_exports.String("--token", { description: "CLI token for headless auth." });
45341
45729
  email = options_exports.String("--email", { description: "Email address." });
45342
45730
  password = options_exports.String("--password", {
45343
45731
  description: "Password (use stdin to avoid argv leak)."
45344
45732
  });
45345
45733
  async execute() {
45346
- const serverUrl = this.server ?? await promptLine("server URL [http://localhost:8080]: ") ?? "";
45347
- const finalServerUrl = serverUrl.length > 0 ? serverUrl : "http://localhost:8080";
45734
+ const finalServerUrl = this.server ?? DEFAULT_SERVER_URL;
45735
+ if (this.token) {
45736
+ return this.loginWithToken(finalServerUrl, this.token);
45737
+ }
45738
+ if (!this.email && !this.password) {
45739
+ this.context.stdout.write(`Opening browser for ${finalServerUrl} ...
45740
+ `);
45741
+ try {
45742
+ const creds = await runBrowserAuth(finalServerUrl);
45743
+ await saveCredentials(creds);
45744
+ this.context.stdout.write(`logged in as ${creds.email} (${creds.org_role})
45745
+ `);
45746
+ return 0;
45747
+ } catch (err) {
45748
+ this.context.stderr.write(
45749
+ `keynv: ${err instanceof Error ? err.message : "browser login failed"}
45750
+ `
45751
+ );
45752
+ return 1;
45753
+ }
45754
+ }
45348
45755
  const email2 = this.email ?? await promptLine("email: ");
45349
45756
  const password = this.password ?? await promptHidden("password: ");
45350
45757
  const res = await fetch(new URL("/v1/auth/login", finalServerUrl).toString(), {
@@ -45375,6 +45782,7 @@ var LoginCommand = class extends Command {
45375
45782
  const data = await res.json();
45376
45783
  try {
45377
45784
  await saveCredentials({
45785
+ auth_kind: "session",
45378
45786
  server_url: finalServerUrl,
45379
45787
  user_id: data.user.id,
45380
45788
  email: data.user.email,
@@ -45392,6 +45800,54 @@ var LoginCommand = class extends Command {
45392
45800
  return 1;
45393
45801
  }
45394
45802
  this.context.stdout.write(`logged in as ${data.user.email} (${data.user.org_role})
45803
+ `);
45804
+ return 0;
45805
+ }
45806
+ async loginWithToken(serverUrl, token) {
45807
+ const res = await fetch(new URL("/v1/whoami", serverUrl).toString(), {
45808
+ headers: { authorization: `Bearer ${token}`, "x-keynv-agent": AGENT }
45809
+ });
45810
+ if (!res.ok) {
45811
+ const text = await res.text();
45812
+ let msg = `token login failed (${res.status})`;
45813
+ try {
45814
+ const parsed = JSON.parse(text);
45815
+ msg = parsed.error?.message ?? msg;
45816
+ this.context.stderr.write(
45817
+ `${fmtError({
45818
+ status: res.status,
45819
+ ...parsed.error?.code ? { code: parsed.error.code } : {},
45820
+ message: msg
45821
+ })}
45822
+ `
45823
+ );
45824
+ } catch {
45825
+ this.context.stderr.write(`keynv: ${msg}
45826
+ `);
45827
+ }
45828
+ return 1;
45829
+ }
45830
+ const data = await res.json();
45831
+ try {
45832
+ await saveCredentials({
45833
+ auth_kind: "cli_token",
45834
+ server_url: serverUrl,
45835
+ user_id: data.id,
45836
+ email: data.email,
45837
+ org_id: data.org_id,
45838
+ org_role: data.org_role,
45839
+ access_token: token,
45840
+ refresh_token: "",
45841
+ access_expires_at: "9999-12-31T23:59:59.999Z"
45842
+ });
45843
+ } catch (err) {
45844
+ this.context.stderr.write(
45845
+ `keynv: failed to persist credentials: ${err instanceof Error ? err.message : String(err)}
45846
+ `
45847
+ );
45848
+ return 1;
45849
+ }
45850
+ this.context.stdout.write(`logged in as ${data.email} (${data.org_role})
45395
45851
  `);
45396
45852
  return 0;
45397
45853
  }
@@ -45442,7 +45898,8 @@ var WhoamiCommand = class extends Command {
45442
45898
  }
45443
45899
  this.context.stdout.write(`user: ${data.email} (${data.id})
45444
45900
  `);
45445
- this.context.stdout.write(`org: ${data.org_id}
45901
+ const orgDisplay = data.org_name ? `${data.org_name} (${data.org_id})` : data.org_id;
45902
+ this.context.stdout.write(`org: ${orgDisplay}
45446
45903
  `);
45447
45904
  this.context.stdout.write(`role: ${data.org_role}
45448
45905
  `);
@@ -45451,7 +45908,8 @@ var WhoamiCommand = class extends Command {
45451
45908
  } else {
45452
45909
  this.context.stdout.write("memberships:\n");
45453
45910
  for (const m of data.memberships) {
45454
- this.context.stdout.write(` ${m.project_id}: ${m.role}
45911
+ const proj = m.project_name ? `${m.project_name} (${m.project_id})` : m.project_id;
45912
+ this.context.stdout.write(` ${proj}: ${m.role}
45455
45913
  `);
45456
45914
  }
45457
45915
  }
@@ -45462,14 +45920,6 @@ var WhoamiCommand = class extends Command {
45462
45920
  // src/commands/member.ts
45463
45921
  init_http();
45464
45922
  init_format();
45465
- async function findProjectIdByName(client, name) {
45466
- const data = await client.request(
45467
- "/v1/projects"
45468
- );
45469
- const match = data.projects.find((p2) => p2.name === name);
45470
- if (!match) throw new Error(`unknown project: ${name}`);
45471
- return match.id;
45472
- }
45473
45923
  var MemberAddCommand = class extends Command {
45474
45924
  static paths = [["member", "add"]];
45475
45925
  static usage = Command.Usage({
@@ -45486,7 +45936,7 @@ var MemberAddCommand = class extends Command {
45486
45936
  return 1;
45487
45937
  }
45488
45938
  const client = new ApiClient();
45489
- const projectId2 = await findProjectIdByName(client, this.project);
45939
+ const projectId2 = await resolveProjectId(client, this.project);
45490
45940
  await client.request(`/v1/projects/${projectId2}/members`, {
45491
45941
  method: "POST",
45492
45942
  body: { email: this.email, role: role2 }
@@ -45503,7 +45953,7 @@ var MemberRemoveCommand = class extends Command {
45503
45953
  email = options_exports.String();
45504
45954
  async execute() {
45505
45955
  const client = new ApiClient();
45506
- const projectId2 = await findProjectIdByName(client, this.project);
45956
+ const projectId2 = await resolveProjectId(client, this.project);
45507
45957
  const members = await client.request(`/v1/projects/${projectId2}/members`);
45508
45958
  const target = members.members.find((m) => m.email === this.email);
45509
45959
  if (!target) {
@@ -45526,7 +45976,7 @@ var MemberListCommand = class extends Command {
45526
45976
  json = options_exports.Boolean("--json", false);
45527
45977
  async execute() {
45528
45978
  const client = new ApiClient();
45529
- const projectId2 = await findProjectIdByName(client, this.project);
45979
+ const projectId2 = await resolveProjectId(client, this.project);
45530
45980
  const data = await client.request(`/v1/projects/${projectId2}/members`);
45531
45981
  if (this.json) {
45532
45982
  this.context.stdout.write(`${JSON.stringify(data, null, 2)}
@@ -45547,114 +45997,6 @@ var MemberListCommand = class extends Command {
45547
45997
  return 0;
45548
45998
  }
45549
45999
  };
45550
-
45551
- // src/commands/project.ts
45552
- init_http();
45553
- init_format();
45554
- var ProjectListCommand = class extends Command {
45555
- static paths = [["project", "list"]];
45556
- static usage = Command.Usage({
45557
- description: "List projects visible to the current user."
45558
- });
45559
- json = options_exports.Boolean("--json", false);
45560
- async execute() {
45561
- const client = new ApiClient();
45562
- const data = await client.request("/v1/projects");
45563
- if (this.json) {
45564
- this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45565
- `);
45566
- return 0;
45567
- }
45568
- if (data.projects.length === 0) {
45569
- this.context.stdout.write("no projects\n");
45570
- return 0;
45571
- }
45572
- this.context.stdout.write(
45573
- `${table(
45574
- ["name", "id", "created_at"],
45575
- data.projects.map((p2) => [p2.name, p2.id, p2.created_at])
45576
- )}
45577
- `
45578
- );
45579
- return 0;
45580
- }
45581
- };
45582
- var ProjectCreateCommand = class extends Command {
45583
- static paths = [["project", "create"]];
45584
- static usage = Command.Usage({
45585
- description: "Create a new project with one or more environments.",
45586
- examples: [["Two envs", "$0 project create billing --env dev --env prod:production:approval"]]
45587
- });
45588
- name = options_exports.String();
45589
- envs = options_exports.Array("--env", {
45590
- description: 'Environment spec: name[:tier[:approval]]. Tier \u2208 {production,non-production}; "approval" sets require_approval=true.'
45591
- });
45592
- async execute() {
45593
- const envs = (this.envs ?? ["dev"]).map((spec) => {
45594
- const [name, tier, approval] = spec.split(":");
45595
- return {
45596
- name: name ?? "",
45597
- tier: tier ?? "non-production",
45598
- require_approval: approval === "approval"
45599
- };
45600
- });
45601
- const client = new ApiClient();
45602
- const result = await client.request("/v1/projects", {
45603
- method: "POST",
45604
- body: { name: this.name, environments: envs }
45605
- });
45606
- this.context.stdout.write(`created project ${result.name} (${result.id})
45607
- `);
45608
- for (const e2 of envs) {
45609
- this.context.stdout.write(
45610
- ` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
45611
- `
45612
- );
45613
- }
45614
- return 0;
45615
- }
45616
- };
45617
- var ProjectDescribeCommand = class extends Command {
45618
- static paths = [["project", "describe"]];
45619
- static usage = Command.Usage({ description: "Show metadata for one project." });
45620
- id = options_exports.String();
45621
- json = options_exports.Boolean("--json", false);
45622
- async execute() {
45623
- const client = new ApiClient();
45624
- const data = await client.request(`/v1/projects/${this.id}`);
45625
- if (this.json) {
45626
- this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45627
- `);
45628
- return 0;
45629
- }
45630
- this.context.stdout.write(`project: ${data.name} (${data.id})
45631
- `);
45632
- for (const e2 of data.environments) {
45633
- this.context.stdout.write(
45634
- ` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
45635
- `
45636
- );
45637
- }
45638
- return 0;
45639
- }
45640
- };
45641
- var ProjectDeleteCommand = class extends Command {
45642
- static paths = [["project", "delete"]];
45643
- static usage = Command.Usage({ description: "Soft-delete a project." });
45644
- id = options_exports.String();
45645
- force = options_exports.Boolean("--force", false);
45646
- async execute() {
45647
- if (!this.force) {
45648
- this.context.stderr.write("keynv: refusing to delete without --force\n");
45649
- return 2;
45650
- }
45651
- const client = new ApiClient();
45652
- await client.request(`/v1/projects/${this.id}`, { method: "DELETE" });
45653
- this.context.stdout.write(`deleted project ${this.id}
45654
- `);
45655
- return 0;
45656
- }
45657
- };
45658
46000
  var RedactCommand = class extends Command {
45659
46001
  static paths = [["redact"]];
45660
46002
  static usage = Command.Usage({
@@ -45697,11 +46039,9 @@ var RedactStreamCommand = class extends Command {
45697
46039
  static usage = Command.Usage({
45698
46040
  description: "Stream stdin through the line-buffered redactor to stdout.",
45699
46041
  details: `
45700
- Used as a hook handler by per-agent integrations (e.g., Claude Code's
45701
- PostToolUse hook): \`keynv redact-stream\` reads its tool output on
45702
- stdin and writes a redacted version on stdout, preserving the original
45703
- line structure. Multi-line patterns are NOT applied here (see the
45704
- streaming-mode limitation in @keynv/redactor).
46042
+ Pipe any output through the line-buffered redactor to stdout.
46043
+ Preserves original line structure. Multi-line patterns are NOT applied
46044
+ here (see the streaming-mode limitation in the redactor package).
45705
46045
  `
45706
46046
  });
45707
46047
  async execute() {
@@ -45716,18 +46056,13 @@ streaming-mode limitation in @keynv/redactor).
45716
46056
  // src/commands/secret.ts
45717
46057
  init_dist();
45718
46058
  init_http();
46059
+ init_secret();
45719
46060
  init_format();
45720
- init_tty();
45721
46061
  init_cancel();
45722
46062
  init_pickProject();
45723
46063
  init_pickSecret();
45724
- init_secret();
45725
- async function findProjectIdByName2(client, name) {
45726
- const data = await client.request("/v1/projects");
45727
- const match = data.projects.find((p2) => p2.name === name);
45728
- if (!match) throw new Error(`unknown project: ${name}`);
45729
- return match.id;
45730
- }
46064
+ init_tty();
46065
+ var ALIAS_FORMAT_HINT = "Format: @<project>.<env>.<KEY> (project/env: lowercase kebab-case; key: letters, digits, _ or -)";
45731
46066
  function missingAlias(stderr) {
45732
46067
  stderr.write("keynv: missing <alias> (TTY required for interactive prompt).\n");
45733
46068
  return 1;
@@ -45776,7 +46111,8 @@ var SecretCreateCommand = class extends Command {
45776
46111
  }
45777
46112
  const parsed = parseAlias(alias2);
45778
46113
  if (!parsed) {
45779
- this.context.stderr.write(`keynv: invalid alias '${alias2}'. Expected @project.env.key.
46114
+ this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46115
+ ${ALIAS_FORMAT_HINT}
45780
46116
  `);
45781
46117
  return 1;
45782
46118
  }
@@ -45789,7 +46125,7 @@ var SecretCreateCommand = class extends Command {
45789
46125
  } else if (value === void 0) {
45790
46126
  value = await promptHidden("value: ");
45791
46127
  }
45792
- const projectId2 = await findProjectIdByName2(client, parsed.project);
46128
+ const projectId2 = await resolveProjectId(client, parsed.project);
45793
46129
  await client.request(`/v1/projects/${projectId2}/secrets`, {
45794
46130
  method: "POST",
45795
46131
  body: { env: parsed.environment, key: parsed.key, value }
@@ -45823,10 +46159,11 @@ var SecretGetCommand = class extends Command {
45823
46159
  const parsed = parseAlias(alias2);
45824
46160
  if (!parsed) {
45825
46161
  this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46162
+ ${ALIAS_FORMAT_HINT}
45826
46163
  `);
45827
46164
  return 1;
45828
46165
  }
45829
- const projectId2 = await findProjectIdByName2(client, parsed.project);
46166
+ const projectId2 = await resolveProjectId(client, parsed.project);
45830
46167
  const data = await client.request(
45831
46168
  `/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}`
45832
46169
  );
@@ -45867,7 +46204,7 @@ var SecretListCommand = class extends Command {
45867
46204
  throw err;
45868
46205
  }
45869
46206
  }
45870
- const projectId2 = await findProjectIdByName2(client, projectName);
46207
+ const projectId2 = await resolveProjectId(client, projectName);
45871
46208
  const data = await client.request(`/v1/projects/${projectId2}/secrets`);
45872
46209
  if (this.json) {
45873
46210
  this.context.stdout.write(`${JSON.stringify(data, null, 2)}
@@ -45911,6 +46248,7 @@ var SecretRotateCommand = class extends Command {
45911
46248
  const parsed = parseAlias(alias2);
45912
46249
  if (!parsed) {
45913
46250
  this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46251
+ ${ALIAS_FORMAT_HINT}
45914
46252
  `);
45915
46253
  return 1;
45916
46254
  }
@@ -45926,7 +46264,7 @@ var SecretRotateCommand = class extends Command {
45926
46264
  } else {
45927
46265
  value = await promptHidden("new value: ");
45928
46266
  }
45929
- const projectId2 = await findProjectIdByName2(client, parsed.project);
46267
+ const projectId2 = await resolveProjectId(client, parsed.project);
45930
46268
  const data = await client.request(
45931
46269
  `/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}/rotate`,
45932
46270
  { method: "POST", body: { new_value: value } }
@@ -45957,10 +46295,11 @@ var SecretDeleteCommand = class extends Command {
45957
46295
  const parsed = parseAlias(alias2);
45958
46296
  if (!parsed) {
45959
46297
  this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46298
+ ${ALIAS_FORMAT_HINT}
45960
46299
  `);
45961
46300
  return 1;
45962
46301
  }
45963
- const projectId2 = await findProjectIdByName2(client, parsed.project);
46302
+ const projectId2 = await resolveProjectId(client, parsed.project);
45964
46303
  await client.request(`/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}`, {
45965
46304
  method: "DELETE"
45966
46305
  });
@@ -45969,6 +46308,140 @@ var SecretDeleteCommand = class extends Command {
45969
46308
  return 0;
45970
46309
  }
45971
46310
  };
46311
+ function generateSecret(bytes = 32) {
46312
+ return randomBytes(bytes).toString("base64");
46313
+ }
46314
+ var ServerInitCommand = class extends Command {
46315
+ static paths = [["server", "init"]];
46316
+ static usage = Command.Usage({
46317
+ description: "Generate secrets and deployment files for a new keynv server.",
46318
+ details: `
46319
+ Interactive onboarding wizard that generates the configuration
46320
+ a fresh keynv server needs: JWT signing secret, master encryption
46321
+ key, and deployment templates.
46322
+
46323
+ Outputs:
46324
+ \u2022 A .keynv-server.env file you feed into your deployment
46325
+ (Docker Compose, Coolify, or bare-metal).
46326
+ \u2022 Optionally a docker-compose.yml in the current directory.
46327
+
46328
+ Non-interactive with --yes to skip prompts.
46329
+ `,
46330
+ examples: [
46331
+ ["Interactive wizard", "$0 server init"],
46332
+ ["Non-interactive (CI/scripts)", "$0 server init --yes"],
46333
+ ["Docker Compose output", "$0 server init --compose"],
46334
+ ["Custom output path", "$0 server init --out ./config/env"]
46335
+ ]
46336
+ });
46337
+ yes = options_exports.Boolean("--yes", false, {
46338
+ description: "Skip all prompts (non-interactive)."
46339
+ });
46340
+ compose = options_exports.Boolean("--compose", false, {
46341
+ description: "Also write a docker-compose.yml to the current directory."
46342
+ });
46343
+ out = options_exports.String("--out", {
46344
+ description: "Write server env file to this path (default: .keynv-server.env)."
46345
+ });
46346
+ async execute() {
46347
+ const jwtSecret = generateSecret(48);
46348
+ const masterKey = generateSecret(32);
46349
+ const webSessionSecret = generateSecret(48);
46350
+ const outPath = this.out ?? ".keynv-server.env";
46351
+ const contents = [
46352
+ "# Generated by `keynv server init`.",
46353
+ "# Keep this file secret \u2014 it contains cryptographic key material.",
46354
+ `KEYNV_JWT_SECRET=${jwtSecret}`,
46355
+ `KEYNV_MASTER_KEY=${masterKey}`,
46356
+ "",
46357
+ "# Web dashboard session secret (required for Next.js JWE session tokens).",
46358
+ `KEYNV_WEB_SESSION_SECRET=${webSessionSecret}`,
46359
+ "",
46360
+ "# Server configuration (uncomment and adjust):",
46361
+ "# KEYNV_PORT=8080",
46362
+ "# KEYNV_WEB_URL=https://keynv.example.com",
46363
+ "# KEYNV_PUBLIC_REGISTRATION=false",
46364
+ "# KEYNV_DB_PATH=./keynv.db",
46365
+ "# KEYNV_MASTER_KEY_FILE=./master.key",
46366
+ "# KEYNV_RATE_LIMIT_PER_MINUTE=120",
46367
+ "# KEYNV_ARGON2_MEMORY_KIB=46080",
46368
+ "# KEYNV_ARGON2_TIME_COST=3"
46369
+ ].join("\n");
46370
+ const { writeFileSync: writeFileSync4 } = await import('fs');
46371
+ writeFileSync4(outPath, `${contents}
46372
+ `);
46373
+ this.context.stdout.write(`Wrote ${outPath}
46374
+
46375
+ `);
46376
+ this.context.stdout.write("Generated secrets:\n");
46377
+ this.context.stdout.write(
46378
+ ` KEYNV_JWT_SECRET ${jwtSecret.slice(0, 8)}... (base64, 48 bytes)
46379
+ `
46380
+ );
46381
+ this.context.stdout.write(
46382
+ ` KEYNV_MASTER_KEY ${masterKey.slice(0, 8)}... (base64, 32 bytes)
46383
+ `
46384
+ );
46385
+ this.context.stdout.write(
46386
+ ` KEYNV_WEB_SESSION_SECRET ${webSessionSecret.slice(0, 8)}... (base64, 48 bytes)
46387
+ `
46388
+ );
46389
+ this.context.stdout.write("\nNext steps:\n");
46390
+ this.context.stdout.write(` 1. Feed ${outPath} into your deployment:
46391
+ `);
46392
+ this.context.stdout.write(" docker compose --env-file .keynv-server.env up -d\n");
46393
+ this.context.stdout.write(" or load it into Coolify as environment variables.\n");
46394
+ this.context.stdout.write(" 2. Bootstrap the first owner:\n");
46395
+ this.context.stdout.write(
46396
+ " docker exec -it <server-container> node dist/bootstrap.js \\\n"
46397
+ );
46398
+ this.context.stdout.write(" --owner-email alice@example.com \\\n");
46399
+ this.context.stdout.write(' --owner-password "<a long random password>" \\\n');
46400
+ this.context.stdout.write(' --org-name "Acme Inc"\n');
46401
+ this.context.stdout.write(" 3. Install the CLI and log in:\n");
46402
+ this.context.stdout.write(" npm install -g @keynv/cli\n");
46403
+ this.context.stdout.write(" keynv login --server https://keynv.example.com\n");
46404
+ this.context.stdout.write(" 4. Onboard your first project:\n");
46405
+ this.context.stdout.write(" cd your-project && keynv init\n");
46406
+ if (this.compose) {
46407
+ const composeContent = `# keynv server + Litestream sidecar
46408
+ # Generated by \`keynv server init\`.
46409
+ # Load with: docker compose --env-file .keynv-server.env up -d
46410
+
46411
+ services:
46412
+ server:
46413
+ image: ghcr.io/keynv-labs/keynv-server:latest
46414
+ restart: unless-stopped
46415
+ ports:
46416
+ - '8080:8080'
46417
+ env_file:
46418
+ - .keynv-server.env
46419
+ volumes:
46420
+ - keynv-data:/data
46421
+
46422
+ litestream:
46423
+ image: litestream/litestream:latest
46424
+ restart: unless-stopped
46425
+ volumes:
46426
+ - keynv-data:/data
46427
+ command: replicate
46428
+ environment:
46429
+ LITESTREAM_DB_PATH: /data/keynv.db
46430
+ LITESTREAM_REPLICA_URL: s3://your-bucket/keynv
46431
+ AWS_ACCESS_KEY_ID: \${LITESTREAM_AWS_ACCESS_KEY_ID}
46432
+ AWS_SECRET_ACCESS_KEY: \${LITESTREAM_AWS_SECRET_ACCESS_KEY}
46433
+
46434
+ volumes:
46435
+ keynv-data:
46436
+ `;
46437
+ writeFileSync4("docker-compose.yml", composeContent);
46438
+ this.context.stdout.write(
46439
+ "\nWrote docker-compose.yml (update Litestream S3 config before deploying).\n"
46440
+ );
46441
+ }
46442
+ return 0;
46443
+ }
46444
+ };
45972
46445
 
45973
46446
  // src/commands/test.ts
45974
46447
  init_dist();
@@ -46321,18 +46794,9 @@ var TESTERS = [
46321
46794
  sshTester,
46322
46795
  httpTester
46323
46796
  ];
46324
- function findTester(type) {
46325
- return TESTERS.find((t) => t.type === type) ?? null;
46326
- }
46327
46797
 
46328
46798
  // src/commands/test.ts
46329
46799
  init_http();
46330
- async function findProjectIdByName3(client, name) {
46331
- const data = await client.request("/v1/projects");
46332
- const match = data.projects.find((p2) => p2.name === name);
46333
- if (!match) throw new Error(`unknown project: ${name}`);
46334
- return match.id;
46335
- }
46336
46800
  function parseTargets(specs) {
46337
46801
  const out = {};
46338
46802
  for (const spec of specs) {
@@ -46386,12 +46850,17 @@ the error message.
46386
46850
  );
46387
46851
  return 2;
46388
46852
  }
46389
- const tester = findTester(this.as);
46853
+ const tester = TESTERS.find((t) => t.type === this.as);
46390
46854
  if (!tester) {
46391
46855
  this.context.stderr.write(`keynv: unknown tester '${this.as}'
46392
46856
  `);
46393
46857
  return 1;
46394
46858
  }
46859
+ const timeoutMs = this.timeout ? Number.parseInt(this.timeout, 10) * 1e3 : void 0;
46860
+ if (this.timeout && (timeoutMs === void 0 || Number.isNaN(timeoutMs))) {
46861
+ this.context.stderr.write("keynv: invalid --timeout (expected integer seconds)\n");
46862
+ return 2;
46863
+ }
46395
46864
  let target;
46396
46865
  try {
46397
46866
  target = parseTargets(this.targets ?? []);
@@ -46408,7 +46877,7 @@ the error message.
46408
46877
  }
46409
46878
  let value;
46410
46879
  try {
46411
- const projectId2 = await findProjectIdByName3(client, parsedAlias.project);
46880
+ const projectId2 = await resolveProjectId(client, parsedAlias.project);
46412
46881
  const data = await client.request(
46413
46882
  `/v1/projects/${projectId2}/secrets/${parsedAlias.environment}/${parsedAlias.key}`
46414
46883
  );
@@ -46419,11 +46888,6 @@ the error message.
46419
46888
  `);
46420
46889
  return 1;
46421
46890
  }
46422
- const timeoutMs = this.timeout ? Number.parseInt(this.timeout, 10) * 1e3 : void 0;
46423
- if (this.timeout && (timeoutMs === void 0 || Number.isNaN(timeoutMs))) {
46424
- this.context.stderr.write("keynv: invalid --timeout (expected integer seconds)\n");
46425
- return 2;
46426
- }
46427
46891
  const result = await runTest({
46428
46892
  tester,
46429
46893
  secret: { alias: parsedAlias.literal, value },
@@ -46465,25 +46929,6 @@ the error message.
46465
46929
  }
46466
46930
  };
46467
46931
 
46468
- // src/commands/ui.ts
46469
- init_menu();
46470
- init_tty();
46471
- var UICommand = class extends Command {
46472
- static paths = [["ui"]];
46473
- static usage = Command.Usage({
46474
- description: "Open the interactive menu (also runs by default when no args are given)."
46475
- });
46476
- async execute() {
46477
- if (!isInteractive()) {
46478
- this.context.stdout.write(
46479
- "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"
46480
- );
46481
- return 0;
46482
- }
46483
- return runMenu();
46484
- }
46485
- };
46486
-
46487
46932
  // src/index.ts
46488
46933
  init_format();
46489
46934
  init_version();
@@ -46517,7 +46962,7 @@ cli.register(InitCommand);
46517
46962
  cli.register(RedactCommand);
46518
46963
  cli.register(RedactStreamCommand);
46519
46964
  cli.register(TestCommand);
46520
- cli.register(UICommand);
46965
+ cli.register(ServerInitCommand);
46521
46966
  var argv = process.argv.slice(2);
46522
46967
  if (argv.length === 0) {
46523
46968
  const { runMenu: runMenu2 } = await Promise.resolve().then(() => (init_menu(), menu_exports));