@keynv/cli 0.1.0-rc.9 → 0.1.0

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.9" ;
1059
+ VERSION = "0.1.0" ;
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 = [];
@@ -5415,6 +5415,12 @@ var init_payload_schemas = __esm({
5415
5415
  }).strict(),
5416
5416
  "project.deleted": external_exports.object({ project_id: projectId, name: external_exports.string() }).strict(),
5417
5417
  "project.dek_rotated": external_exports.object({ project_id: projectId }).strict(),
5418
+ "environment.created": external_exports.object({
5419
+ project_id: projectId,
5420
+ environment: external_exports.string().min(1),
5421
+ tier: external_exports.enum(["production", "non-production"]),
5422
+ require_approval: external_exports.boolean()
5423
+ }).strict(),
5418
5424
  "member.added": external_exports.object({ project_id: projectId, target_user_id: userId, role }).strict(),
5419
5425
  "member.removed": external_exports.object({ project_id: projectId, target_user_id: userId }).strict(),
5420
5426
  "member.role_changed": external_exports.object({ project_id: projectId, target_user_id: userId, role }).strict(),
@@ -5530,7 +5536,15 @@ function clearCredentialsFile() {
5530
5536
  if (existsSync(legacy)) rmSync(legacy, { force: true });
5531
5537
  try {
5532
5538
  entry().deletePassword();
5533
- } 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;
5534
5548
  }
5535
5549
  }
5536
5550
  var SERVICE, KEY_ACCOUNT;
@@ -5573,8 +5587,9 @@ async function saveCredentials(creds) {
5573
5587
  cache = creds;
5574
5588
  }
5575
5589
  function clearCredentials() {
5576
- clearCredentialsFile();
5590
+ const ok = clearCredentialsFile();
5577
5591
  cache = null;
5592
+ return ok;
5578
5593
  }
5579
5594
  var cache;
5580
5595
  var init_store = __esm({
@@ -5584,6 +5599,9 @@ var init_store = __esm({
5584
5599
  });
5585
5600
 
5586
5601
  // src/client/http.ts
5602
+ function isClientError(err) {
5603
+ return err instanceof Error && typeof err.status === "number";
5604
+ }
5587
5605
  function clientError(status, code, message, details) {
5588
5606
  const err = Object.assign(new Error(message), { status, code, details });
5589
5607
  return err;
@@ -5598,6 +5616,7 @@ function buildUrl(base, path, query) {
5598
5616
  return url.toString();
5599
5617
  }
5600
5618
  async function tryRefresh(creds) {
5619
+ if (creds.auth_kind === "cli_token" || creds.refresh_token.length === 0) return null;
5601
5620
  const res = await fetch(buildUrl(creds.server_url, "/v1/auth/refresh"), {
5602
5621
  method: "POST",
5603
5622
  headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
@@ -5637,7 +5656,16 @@ var init_http = __esm({
5637
5656
  return this.creds;
5638
5657
  }
5639
5658
  async ensureHydrated() {
5640
- if (this.hydrated) await this.hydrated;
5659
+ if (this.hydrated) {
5660
+ try {
5661
+ await this.hydrated;
5662
+ } catch (err) {
5663
+ process.stderr.write(
5664
+ `${err instanceof Error ? err.message : `keynv: credential load failed \u2014 ${String(err)}`}
5665
+ `
5666
+ );
5667
+ }
5668
+ }
5641
5669
  }
5642
5670
  async setCredentials(creds) {
5643
5671
  this.creds = creds;
@@ -5659,21 +5687,40 @@ var init_http = __esm({
5659
5687
  headers.authorization = `Bearer ${this.creds.access_token}`;
5660
5688
  }
5661
5689
  const url = this.creds ? buildUrl(this.creds.server_url, path, opts.query) : path;
5662
- let res = await fetch(url, {
5663
- method: opts.method ?? "GET",
5664
- headers,
5665
- ...opts.body !== void 0 ? { body: JSON.stringify(opts.body) } : {}
5666
- });
5690
+ let res;
5691
+ try {
5692
+ res = await fetch(url, {
5693
+ method: opts.method ?? "GET",
5694
+ headers,
5695
+ ...opts.body !== void 0 ? { body: JSON.stringify(opts.body) } : {}
5696
+ });
5697
+ } catch (err) {
5698
+ const cause = err instanceof Error ? err.message : String(err);
5699
+ const serverUrl = this.creds?.server_url ?? url;
5700
+ throw new Error(
5701
+ `keynv: cannot reach server '${serverUrl}' \u2014 ${cause}
5702
+ Check the server is running: curl ${serverUrl}/v1/health`
5703
+ );
5704
+ }
5667
5705
  if (res.status === 401 && this.creds && opts.authed !== false) {
5668
5706
  const refreshed = await tryRefresh(this.creds);
5669
5707
  if (refreshed) {
5670
5708
  this.creds = refreshed;
5671
5709
  headers.authorization = `Bearer ${refreshed.access_token}`;
5672
- res = await fetch(url, {
5673
- method: opts.method ?? "GET",
5674
- headers,
5675
- ...opts.body !== void 0 ? { body: JSON.stringify(opts.body) } : {}
5676
- });
5710
+ try {
5711
+ res = await fetch(url, {
5712
+ method: opts.method ?? "GET",
5713
+ headers,
5714
+ ...opts.body !== void 0 ? { body: JSON.stringify(opts.body) } : {}
5715
+ });
5716
+ } catch (err) {
5717
+ const cause = err instanceof Error ? err.message : String(err);
5718
+ const serverUrl = this.creds?.server_url ?? url;
5719
+ throw new Error(
5720
+ `keynv: cannot reach server '${serverUrl}' \u2014 ${cause}
5721
+ Check the server is running: curl ${serverUrl}/v1/health`
5722
+ );
5723
+ }
5677
5724
  }
5678
5725
  }
5679
5726
  if (res.status === 204) return void 0;
@@ -5712,8 +5759,38 @@ function fmtError(err) {
5712
5759
  const status = err.status ? ` (${err.status})` : "";
5713
5760
  return `keynv:${code}${status} ${err.message}`;
5714
5761
  }
5762
+ function handleExecError(stderr, err) {
5763
+ if (isClientError(err)) {
5764
+ const code = err.code ? `[${err.code}] ` : "";
5765
+ stderr.write(`keynv: ${code}${err.message}
5766
+ `);
5767
+ } else if (err instanceof Error) {
5768
+ stderr.write(`keynv: ${err.message}
5769
+ `);
5770
+ } else {
5771
+ stderr.write(`keynv: unexpected error
5772
+ `);
5773
+ }
5774
+ return 1;
5775
+ }
5715
5776
  var init_format = __esm({
5716
5777
  "src/ui/format.ts"() {
5778
+ init_http();
5779
+ }
5780
+ });
5781
+ function walkUp(startDir, predicate) {
5782
+ let dir = resolve(startDir);
5783
+ for (let i = 0; i < 64; i++) {
5784
+ const result = predicate(dir);
5785
+ if (result !== null && result !== void 0) return result;
5786
+ const parent = dirname(dir);
5787
+ if (parent === dir) return null;
5788
+ dir = parent;
5789
+ }
5790
+ return null;
5791
+ }
5792
+ var init_fs = __esm({
5793
+ "src/util/fs.ts"() {
5717
5794
  }
5718
5795
  });
5719
5796
  function parseEnvFile(content, filename) {
@@ -5742,7 +5819,7 @@ function parseEnvFile(content, filename) {
5742
5819
  `invalid key '${name}' (must match /^[A-Za-z_][A-Za-z0-9_]*$/)`
5743
5820
  );
5744
5821
  }
5745
- let valueRaw = body.slice(eq + 1);
5822
+ const valueRaw = body.slice(eq + 1);
5746
5823
  let value;
5747
5824
  const valueLeading = valueRaw.replace(/^\s+/, "");
5748
5825
  const firstCh = valueLeading.charAt(0);
@@ -5774,20 +5851,14 @@ function parseEnvFile(content, filename) {
5774
5851
  return entries;
5775
5852
  }
5776
5853
  function findEnvFile(startDir) {
5777
- let dir = resolve(startDir);
5778
- for (let i = 0; i < 64; i++) {
5854
+ return walkUp(startDir, (dir) => {
5779
5855
  const candidate = join(dir, ENV_FILE_BASENAME);
5780
- if (existsSync(candidate)) {
5781
- try {
5782
- if (statSync(candidate).isFile()) return candidate;
5783
- } catch {
5784
- }
5856
+ try {
5857
+ if (statSync(candidate).isFile()) return candidate;
5858
+ } catch {
5785
5859
  }
5786
- const parent = dirname(dir);
5787
- if (parent === dir) return null;
5788
- dir = parent;
5789
- }
5790
- return null;
5860
+ return null;
5861
+ });
5791
5862
  }
5792
5863
  function loadEnvFile(opts) {
5793
5864
  if (opts.disabled) return null;
@@ -5812,6 +5883,7 @@ var MAX_FILE_BYTES, KEY_RE2, ENV_FILE_BASENAME, EnvFileParseError, EnvFileNotFou
5812
5883
  var init_envFile = __esm({
5813
5884
  "src/exec/envFile.ts"() {
5814
5885
  init_dist();
5886
+ init_fs();
5815
5887
  MAX_FILE_BYTES = 1e6;
5816
5888
  KEY_RE2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
5817
5889
  ENV_FILE_BASENAME = ".keynv.env";
@@ -5848,16 +5920,129 @@ var init_envFile = __esm({
5848
5920
  }
5849
5921
  });
5850
5922
 
5851
- // src/ui/helpers/tty.ts
5852
- var tty_exports = {};
5853
- __export(tty_exports, {
5854
- isInteractive: () => isInteractive
5855
- });
5856
- function isInteractive() {
5857
- return process.stdin.isTTY === true && process.stdout.isTTY === true;
5923
+ // src/init/heuristics.ts
5924
+ function shannonEntropyBits(s) {
5925
+ if (s.length === 0) return 0;
5926
+ const counts = /* @__PURE__ */ new Map();
5927
+ for (const ch of s) counts.set(ch, (counts.get(ch) ?? 0) + 1);
5928
+ let h2 = 0;
5929
+ for (const c2 of counts.values()) {
5930
+ const p2 = c2 / s.length;
5931
+ h2 -= p2 * Math.log2(p2);
5932
+ }
5933
+ return h2;
5858
5934
  }
5859
- var init_tty = __esm({
5860
- "src/ui/helpers/tty.ts"() {
5935
+ function nameMatchesLiteralPrefix(name) {
5936
+ const upper = name.toUpperCase();
5937
+ return NAME_LITERAL_PREFIX.some((p2) => upper.startsWith(p2));
5938
+ }
5939
+ function nameMatchesSecretSuffix(name) {
5940
+ const upper = name.toUpperCase();
5941
+ for (const { suffix, hint } of NAME_SECRET_SUFFIXES) {
5942
+ if (upper.endsWith(suffix) || upper.endsWith(`_${suffix}`)) {
5943
+ return { matched: true, hint };
5944
+ }
5945
+ }
5946
+ return { matched: false, hint: "" };
5947
+ }
5948
+ function classifyEntry(name, value) {
5949
+ const upper = name.toUpperCase();
5950
+ if (NAME_FRAMEWORK_MANAGED.has(upper)) {
5951
+ return { verdict: "skip", hint: "framework/shell-managed" };
5952
+ }
5953
+ if (NAME_LITERAL_EXACT.has(upper)) {
5954
+ return { verdict: "literal", hint: "common config var" };
5955
+ }
5956
+ if (nameMatchesLiteralPrefix(name)) {
5957
+ return { verdict: "literal", hint: "public env, build-time bundled" };
5958
+ }
5959
+ if (value.length === 0) {
5960
+ return { verdict: "literal", hint: "empty" };
5961
+ }
5962
+ for (const { re, hint } of VALUE_PATTERNS) {
5963
+ if (re.test(value)) return { verdict: "secret", hint };
5964
+ }
5965
+ const suffix = nameMatchesSecretSuffix(name);
5966
+ if (suffix.matched) {
5967
+ return { verdict: "secret", hint: suffix.hint };
5968
+ }
5969
+ if (NAME_DB_URL.test(name)) {
5970
+ return { verdict: "secret", hint: "database URL" };
5971
+ }
5972
+ if (value.length >= 32) {
5973
+ const bits = shannonEntropyBits(value);
5974
+ if (bits >= 3.5) {
5975
+ return { verdict: "secret", hint: `${value.length}-char random-looking string` };
5976
+ }
5977
+ }
5978
+ return { verdict: "ambiguous", hint: "" };
5979
+ }
5980
+ function previewValue(value, max = 40) {
5981
+ if (value.length === 0) return "(empty)";
5982
+ if (value.length <= max) return value;
5983
+ return `${value.slice(0, max - 1)}\u2026`;
5984
+ }
5985
+ var NAME_FRAMEWORK_MANAGED, NAME_LITERAL_EXACT, NAME_LITERAL_PREFIX, NAME_SECRET_SUFFIXES, NAME_DB_URL, VALUE_PATTERNS;
5986
+ var init_heuristics = __esm({
5987
+ "src/init/heuristics.ts"() {
5988
+ NAME_FRAMEWORK_MANAGED = /* @__PURE__ */ new Set([
5989
+ "NODE_ENV",
5990
+ "PORT",
5991
+ "HOST",
5992
+ "HOSTNAME",
5993
+ "PATH",
5994
+ "HOME",
5995
+ "USER",
5996
+ "SHELL",
5997
+ "PWD",
5998
+ "CI"
5999
+ ]);
6000
+ NAME_LITERAL_EXACT = /* @__PURE__ */ new Set([
6001
+ "DEBUG",
6002
+ "LOG_LEVEL",
6003
+ "LOGLEVEL",
6004
+ "TZ",
6005
+ "LANG",
6006
+ "LC_ALL",
6007
+ "NODE_OPTIONS",
6008
+ "NPM_CONFIG_LOGLEVEL",
6009
+ "TS_NODE_PROJECT"
6010
+ ]);
6011
+ NAME_LITERAL_PREFIX = ["NEXT_PUBLIC_", "VITE_", "REACT_APP_", "PUBLIC_", "EXPO_PUBLIC_"];
6012
+ NAME_SECRET_SUFFIXES = [
6013
+ { suffix: "PRIVATE_KEY", hint: "private key" },
6014
+ { suffix: "API_KEY", hint: "API key" },
6015
+ { suffix: "ACCESS_KEY", hint: "access key" },
6016
+ { suffix: "SECRET_KEY", hint: "secret key" },
6017
+ { suffix: "SECRET", hint: "secret" },
6018
+ { suffix: "PASSWORD", hint: "password" },
6019
+ { suffix: "PASSPHRASE", hint: "passphrase" },
6020
+ { suffix: "TOKEN", hint: "token" },
6021
+ { suffix: "KEY", hint: "key" },
6022
+ { suffix: "CREDENTIALS", hint: "credentials" },
6023
+ { suffix: "AUTH", hint: "auth" },
6024
+ { suffix: "DSN", hint: "connection string" }
6025
+ ];
6026
+ NAME_DB_URL = /^(DATABASE|DB|POSTGRES|MYSQL|MONGO|REDIS)_URL$/i;
6027
+ VALUE_PATTERNS = [
6028
+ { re: /^sk-proj-/, hint: "OpenAI project key" },
6029
+ { re: /^sk-[A-Za-z0-9]{20,}/, hint: "OpenAI key" },
6030
+ { re: /^sk_live_/, hint: "Stripe live key" },
6031
+ { re: /^sk_test_/, hint: "Stripe test key" },
6032
+ { re: /^pk_live_/, hint: "Stripe publishable \u2014 often public, double-check" },
6033
+ { re: /^xoxb-/, hint: "Slack bot token" },
6034
+ { re: /^xoxp-/, hint: "Slack user token" },
6035
+ { re: /^ghp_[A-Za-z0-9]{30,}/, hint: "GitHub personal access token" },
6036
+ { re: /^github_pat_/, hint: "GitHub fine-grained PAT" },
6037
+ { re: /^gho_/, hint: "GitHub OAuth token" },
6038
+ { re: /^AKIA[0-9A-Z]{16}$/, hint: "AWS access key id" },
6039
+ { re: /^ASIA[0-9A-Z]{16}$/, hint: "AWS temporary key id" },
6040
+ { re: /^AIza[0-9A-Za-z_-]{35}$/, hint: "Google API key" },
6041
+ { re: /^ya29\./, hint: "Google OAuth access token" },
6042
+ { re: /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, hint: "JWT" },
6043
+ { re: /^-----BEGIN [A-Z ]+PRIVATE KEY-----/, hint: "PEM private key" },
6044
+ { re: /^([a-z]+):\/\/[^@]+:[^@]+@/i, hint: "connection string with credentials" }
6045
+ ];
5861
6046
  }
5862
6047
  });
5863
6048
 
@@ -7015,23 +7200,109 @@ ${c2}
7015
7200
  } }).prompt();
7016
7201
  }
7017
7202
  });
7203
+ function aiContextBody() {
7204
+ return `## keynv (secrets)
7205
+
7206
+ 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.
7207
+
7208
+ ### Mental model
7209
+
7210
+ - **\`.keynv.env\`** \u2014 checked into git, contains aliases. Safe to read, edit, commit.
7211
+ - **Vault** \u2014 holds the real values. The \`keynv\` CLI reads it on demand.
7212
+ - **\`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.
7213
+ - **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.
7214
+
7215
+ ### What you should do
7216
+
7217
+ When the user asks about secrets or env vars, follow this decision tree:
7218
+
7219
+ | User intent | Run / suggest |
7220
+ |---|---|
7221
+ | "Add a new API key" / "set up a secret" | \`keynv secret create\` (interactive prompt picks project + env + key) |
7222
+ | "I have a \`.env\`, migrate it" | \`keynv init\` (you're seeing this file because that already happened) |
7223
+ | "Run the app / dev server / tests" | \`keynv exec -- <their existing command>\` (auto-loads \`.keynv.env\`) |
7224
+ | "Show me the value of X" | \`keynv secret get @alias\` \u2014 but copy to clipboard, do NOT print the value into chat / terminal |
7225
+ | "Rotate this key" | \`keynv secret rotate @alias\` |
7226
+ | "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\`) |
7227
+ | "Who has access?" | \`keynv member list <project>\` |
7228
+
7229
+ ### Hard rules \u2014 do not violate
7230
+
7231
+ 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.
7232
+ 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.
7233
+ 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.
7234
+ 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.
7235
+ 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.
7236
+ 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.
7237
+
7238
+ ### Quick reference
7239
+
7240
+ \`\`\`bash
7241
+ keynv # interactive TUI menu (pick projects, secrets, members)
7242
+ keynv exec -- <cmd> # run cmd with .keynv.env loaded
7243
+ keynv secret create # walk through creating a new secret
7244
+ keynv secret list <project> # list aliases (no values)
7245
+ keynv whoami # who am I logged in as
7246
+ keynv --help # full command list
7247
+ \`\`\`
7248
+
7249
+ 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\`.`;
7250
+ }
7251
+ function renderKeynvBlock() {
7252
+ return `${KEYNV_BLOCK_START}
7253
+ ${aiContextBody()}
7254
+ ${KEYNV_BLOCK_END}`;
7255
+ }
7256
+ function writeAiContext(rootPath) {
7257
+ const path = join(rootPath, AGENTS_FILE_BASENAME);
7258
+ const block = renderKeynvBlock();
7259
+ if (!existsSync(path)) {
7260
+ const initial = `# Agent guidance for this project
7261
+
7262
+ ${block}
7263
+ `;
7264
+ writeFileSync(path, initial);
7265
+ return "created";
7266
+ }
7267
+ const existing = readFileSync(path, "utf8");
7268
+ const startIdx = existing.indexOf(KEYNV_BLOCK_START);
7269
+ const endIdx = existing.indexOf(KEYNV_BLOCK_END);
7270
+ if (startIdx >= 0 && endIdx > startIdx) {
7271
+ const before = existing.slice(0, startIdx);
7272
+ const after = existing.slice(endIdx + KEYNV_BLOCK_END.length);
7273
+ const next = `${before}${block}${after}`;
7274
+ if (next === existing) return "unchanged";
7275
+ writeFileSync(path, next);
7276
+ return "updated";
7277
+ }
7278
+ const sep = existing.length === 0 ? "" : existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
7279
+ const trailingNewline = existing.endsWith("\n") ? "" : "\n";
7280
+ writeFileSync(path, `${existing}${trailingNewline}${sep}${block}
7281
+ `);
7282
+ return "appended";
7283
+ }
7284
+ var AGENTS_FILE_BASENAME, KEYNV_BLOCK_START, KEYNV_BLOCK_END;
7285
+ var init_aiContext = __esm({
7286
+ "src/init/aiContext.ts"() {
7287
+ AGENTS_FILE_BASENAME = "AGENTS.md";
7288
+ KEYNV_BLOCK_START = "<!-- keynv:start -- generated by `keynv init`; safe to leave intact, will be refreshed on re-run -->";
7289
+ KEYNV_BLOCK_END = "<!-- keynv:end -->";
7290
+ }
7291
+ });
7018
7292
  function findProjectRoot(startDir) {
7019
- let dir = resolve(startDir);
7020
- for (let i = 0; i < 64; i++) {
7293
+ const result = walkUp(startDir, (dir) => {
7021
7294
  for (const marker of PROJECT_MARKERS) {
7022
- const candidate = join(dir, marker);
7023
- if (existsSync(candidate)) {
7024
- return buildRoot(dir, marker);
7295
+ if (existsSync(join(dir, marker))) {
7296
+ return { dir, marker };
7025
7297
  }
7026
7298
  }
7027
7299
  if (existsSync(join(dir, GIT_MARKER))) {
7028
- return buildRoot(dir, GIT_MARKER);
7300
+ return { dir, marker: GIT_MARKER };
7029
7301
  }
7030
- const parent = dirname(dir);
7031
- if (parent === dir) return null;
7032
- dir = parent;
7033
- }
7034
- return null;
7302
+ return null;
7303
+ });
7304
+ if (!result) return null;
7305
+ return buildRoot(result.dir, result.marker);
7035
7306
  }
7036
7307
  function buildRoot(dir, marker) {
7037
7308
  let suggestedName = basename(dir);
@@ -7045,16 +7316,20 @@ function buildRoot(dir, marker) {
7045
7316
  }
7046
7317
  if (pkg.scripts && typeof pkg.scripts === "object") {
7047
7318
  scripts = Object.fromEntries(
7048
- Object.entries(pkg.scripts).filter(
7049
- ([, v2]) => typeof v2 === "string"
7050
- )
7319
+ Object.entries(pkg.scripts).filter(([, v2]) => typeof v2 === "string")
7051
7320
  );
7052
7321
  }
7053
7322
  } catch {
7054
7323
  invalid = true;
7055
7324
  }
7056
7325
  }
7057
- return { path: dir, suggestedName, marker, packageJsonScripts: scripts, packageJsonInvalid: invalid };
7326
+ return {
7327
+ path: dir,
7328
+ suggestedName,
7329
+ marker,
7330
+ packageJsonScripts: scripts,
7331
+ packageJsonInvalid: invalid
7332
+ };
7058
7333
  }
7059
7334
  function findEnvFiles(rootDir) {
7060
7335
  let entries;
@@ -7112,6 +7387,7 @@ function hasExistingKeynvEnv(rootDir) {
7112
7387
  var PROJECT_MARKERS, GIT_MARKER, ENV_GLOB, ENV_EXAMPLE, KEYNV_ENV_BASENAME;
7113
7388
  var init_detect = __esm({
7114
7389
  "src/init/detect.ts"() {
7390
+ init_fs();
7115
7391
  PROJECT_MARKERS = [
7116
7392
  "package.json",
7117
7393
  "pyproject.toml",
@@ -7124,220 +7400,10 @@ var init_detect = __esm({
7124
7400
  ];
7125
7401
  GIT_MARKER = ".git";
7126
7402
  ENV_GLOB = /^\.env(\.[A-Za-z0-9_-]+)?$/;
7127
- ENV_EXAMPLE = /^\.env\.(example|sample|template)$/;
7403
+ ENV_EXAMPLE = /^\.env\.(example|sample|template|dist|defaults)$/;
7128
7404
  KEYNV_ENV_BASENAME = ".keynv.env";
7129
7405
  }
7130
7406
  });
7131
- function aiContextBody() {
7132
- return `## keynv (secrets)
7133
-
7134
- 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.
7135
-
7136
- ### Mental model
7137
-
7138
- - **\`.keynv.env\`** \u2014 checked into git, contains aliases. Safe to read, edit, commit.
7139
- - **Vault** \u2014 holds the real values. The \`keynv\` CLI reads it on demand.
7140
- - **\`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.
7141
- - **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.
7142
-
7143
- ### What you should do
7144
-
7145
- When the user asks about secrets or env vars, follow this decision tree:
7146
-
7147
- | User intent | Run / suggest |
7148
- |---|---|
7149
- | "Add a new API key" / "set up a secret" | \`keynv secret create\` (interactive prompt picks project + env + key) |
7150
- | "I have a \`.env\`, migrate it" | \`keynv init\` (you're seeing this file because that already happened) |
7151
- | "Run the app / dev server / tests" | \`keynv exec -- <their existing command>\` (auto-loads \`.keynv.env\`) |
7152
- | "Show me the value of X" | \`keynv secret get @alias\` \u2014 but copy to clipboard, do NOT print the value into chat / terminal |
7153
- | "Rotate this key" | \`keynv secret rotate @alias\` |
7154
- | "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\`) |
7155
- | "Who has access?" | \`keynv member list <project>\` |
7156
-
7157
- ### Hard rules \u2014 do not violate
7158
-
7159
- 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.
7160
- 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.
7161
- 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.
7162
- 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.
7163
- 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.
7164
- 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.
7165
-
7166
- ### Quick reference
7167
-
7168
- \`\`\`bash
7169
- keynv # interactive TUI menu (pick projects, secrets, members)
7170
- keynv exec -- <cmd> # run cmd with .keynv.env loaded
7171
- keynv secret create # walk through creating a new secret
7172
- keynv secret list <project> # list aliases (no values)
7173
- keynv whoami # who am I logged in as
7174
- keynv --help # full command list
7175
- \`\`\`
7176
-
7177
- 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\`.`;
7178
- }
7179
- function renderKeynvBlock() {
7180
- return `${KEYNV_BLOCK_START}
7181
- ${aiContextBody()}
7182
- ${KEYNV_BLOCK_END}`;
7183
- }
7184
- function writeAiContext(rootPath) {
7185
- const path = join(rootPath, AGENTS_FILE_BASENAME);
7186
- const block = renderKeynvBlock();
7187
- if (!existsSync(path)) {
7188
- const initial = `# Agent guidance for this project
7189
-
7190
- ${block}
7191
- `;
7192
- writeFileSync(path, initial);
7193
- return "created";
7194
- }
7195
- const existing = readFileSync(path, "utf8");
7196
- const startIdx = existing.indexOf(KEYNV_BLOCK_START);
7197
- const endIdx = existing.indexOf(KEYNV_BLOCK_END);
7198
- if (startIdx >= 0 && endIdx > startIdx) {
7199
- const before = existing.slice(0, startIdx);
7200
- const after = existing.slice(endIdx + KEYNV_BLOCK_END.length);
7201
- const next = `${before}${block}${after}`;
7202
- if (next === existing) return "unchanged";
7203
- writeFileSync(path, next);
7204
- return "updated";
7205
- }
7206
- const sep = existing.length === 0 ? "" : existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
7207
- const trailingNewline = existing.endsWith("\n") ? "" : "\n";
7208
- writeFileSync(path, `${existing}${trailingNewline}${sep}${block}
7209
- `);
7210
- return "appended";
7211
- }
7212
- var AGENTS_FILE_BASENAME, KEYNV_BLOCK_START, KEYNV_BLOCK_END;
7213
- var init_aiContext = __esm({
7214
- "src/init/aiContext.ts"() {
7215
- AGENTS_FILE_BASENAME = "AGENTS.md";
7216
- KEYNV_BLOCK_START = "<!-- keynv:start -- generated by `keynv init`; safe to leave intact, will be refreshed on re-run -->";
7217
- KEYNV_BLOCK_END = "<!-- keynv:end -->";
7218
- }
7219
- });
7220
-
7221
- // src/init/heuristics.ts
7222
- function shannonEntropyBits(s) {
7223
- if (s.length === 0) return 0;
7224
- const counts = /* @__PURE__ */ new Map();
7225
- for (const ch of s) counts.set(ch, (counts.get(ch) ?? 0) + 1);
7226
- let h2 = 0;
7227
- for (const c2 of counts.values()) {
7228
- const p2 = c2 / s.length;
7229
- h2 -= p2 * Math.log2(p2);
7230
- }
7231
- return h2;
7232
- }
7233
- function nameMatchesLiteralPrefix(name) {
7234
- const upper = name.toUpperCase();
7235
- return NAME_LITERAL_PREFIX.some((p2) => upper.startsWith(p2));
7236
- }
7237
- function nameMatchesSecretSuffix(name) {
7238
- const upper = name.toUpperCase();
7239
- for (const { suffix, hint } of NAME_SECRET_SUFFIXES) {
7240
- if (upper.endsWith(suffix) || upper.endsWith(`_${suffix}`)) {
7241
- return { matched: true, hint };
7242
- }
7243
- }
7244
- return { matched: false, hint: "" };
7245
- }
7246
- function classifyEntry(name, value) {
7247
- const upper = name.toUpperCase();
7248
- if (NAME_LITERAL_EXACT.has(upper)) {
7249
- return { verdict: "literal", hint: "common config var" };
7250
- }
7251
- if (nameMatchesLiteralPrefix(name)) {
7252
- return { verdict: "literal", hint: "public env (build-time bundled)" };
7253
- }
7254
- if (value.length === 0) {
7255
- return { verdict: "literal", hint: "empty" };
7256
- }
7257
- for (const { re, hint } of VALUE_PATTERNS) {
7258
- if (re.test(value)) return { verdict: "secret", hint };
7259
- }
7260
- const suffix = nameMatchesSecretSuffix(name);
7261
- if (suffix.matched) {
7262
- return { verdict: "secret", hint: suffix.hint };
7263
- }
7264
- if (NAME_DB_URL.test(name)) {
7265
- return { verdict: "secret", hint: "database URL" };
7266
- }
7267
- if (value.length >= 32) {
7268
- const bits = shannonEntropyBits(value);
7269
- if (bits >= 3.5) {
7270
- return { verdict: "secret", hint: `${value.length}-char random-looking string` };
7271
- }
7272
- }
7273
- return { verdict: "ambiguous", hint: "" };
7274
- }
7275
- function previewValue(value, max = 40) {
7276
- if (value.length === 0) return "(empty)";
7277
- if (value.length <= max) return value;
7278
- return `${value.slice(0, max - 1)}\u2026`;
7279
- }
7280
- var NAME_LITERAL_EXACT, NAME_LITERAL_PREFIX, NAME_SECRET_SUFFIXES, NAME_DB_URL, VALUE_PATTERNS;
7281
- var init_heuristics = __esm({
7282
- "src/init/heuristics.ts"() {
7283
- NAME_LITERAL_EXACT = /* @__PURE__ */ new Set([
7284
- "NODE_ENV",
7285
- "PORT",
7286
- "HOST",
7287
- "HOSTNAME",
7288
- "DEBUG",
7289
- "LOG_LEVEL",
7290
- "LOGLEVEL",
7291
- "TZ",
7292
- "LANG",
7293
- "LC_ALL",
7294
- "PATH",
7295
- "HOME",
7296
- "USER",
7297
- "SHELL",
7298
- "PWD",
7299
- "CI",
7300
- "NODE_OPTIONS",
7301
- "NPM_CONFIG_LOGLEVEL",
7302
- "TS_NODE_PROJECT"
7303
- ]);
7304
- NAME_LITERAL_PREFIX = ["NEXT_PUBLIC_", "VITE_", "REACT_APP_", "PUBLIC_", "EXPO_PUBLIC_"];
7305
- NAME_SECRET_SUFFIXES = [
7306
- { suffix: "PRIVATE_KEY", hint: "private key" },
7307
- { suffix: "API_KEY", hint: "API key" },
7308
- { suffix: "ACCESS_KEY", hint: "access key" },
7309
- { suffix: "SECRET_KEY", hint: "secret key" },
7310
- { suffix: "SECRET", hint: "secret" },
7311
- { suffix: "PASSWORD", hint: "password" },
7312
- { suffix: "PASSPHRASE", hint: "passphrase" },
7313
- { suffix: "TOKEN", hint: "token" },
7314
- { suffix: "KEY", hint: "key" },
7315
- { suffix: "CREDENTIALS", hint: "credentials" },
7316
- { suffix: "AUTH", hint: "auth" },
7317
- { suffix: "DSN", hint: "connection string" }
7318
- ];
7319
- NAME_DB_URL = /^(DATABASE|DB|POSTGRES|MYSQL|MONGO|REDIS)_URL$/i;
7320
- VALUE_PATTERNS = [
7321
- { re: /^sk-proj-/, hint: "OpenAI project key" },
7322
- { re: /^sk-[A-Za-z0-9]{20,}/, hint: "OpenAI key" },
7323
- { re: /^sk_live_/, hint: "Stripe live key" },
7324
- { re: /^sk_test_/, hint: "Stripe test key" },
7325
- { re: /^pk_live_/, hint: "Stripe publishable (often public, double-check)" },
7326
- { re: /^xoxb-/, hint: "Slack bot token" },
7327
- { re: /^xoxp-/, hint: "Slack user token" },
7328
- { re: /^ghp_[A-Za-z0-9]{30,}/, hint: "GitHub personal access token" },
7329
- { re: /^github_pat_/, hint: "GitHub fine-grained PAT" },
7330
- { re: /^gho_/, hint: "GitHub OAuth token" },
7331
- { re: /^AKIA[0-9A-Z]{16}$/, hint: "AWS access key id" },
7332
- { re: /^ASIA[0-9A-Z]{16}$/, hint: "AWS temporary key id" },
7333
- { re: /^AIza[0-9A-Za-z_-]{35}$/, hint: "Google API key" },
7334
- { re: /^ya29\./, hint: "Google OAuth access token" },
7335
- { re: /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, hint: "JWT" },
7336
- { re: /^-----BEGIN [A-Z ]+PRIVATE KEY-----/, hint: "PEM private key" },
7337
- { re: /^([a-z]+):\/\/[^@]+:[^@]+@/i, hint: "connection string with credentials" }
7338
- ];
7339
- }
7340
- });
7341
7407
 
7342
7408
  // src/init/scriptWrap.ts
7343
7409
  function analyzeScript(name, command) {
@@ -7571,14 +7637,14 @@ async function runInitFlow(client, opts) {
7571
7637
  R2.warn(`package.json at ${root.path} is not valid JSON \u2014 script wrapping will be skipped.`);
7572
7638
  }
7573
7639
  const envFiles = findEnvFiles(root.path);
7574
- if (envFiles.length === 0 && !hasExistingKeynvEnv(root.path)) {
7640
+ const intoExisting = hasExistingKeynvEnv(root.path);
7641
+ if (envFiles.length === 0 && !intoExisting) {
7575
7642
  R2.info(
7576
7643
  `No .env files found in ${root.path}. There's nothing to migrate yet \u2014 create a .keynv.env by hand or run \`keynv exec\` once you have one.`
7577
7644
  );
7578
7645
  ye("Nothing to do.");
7579
7646
  return { exitCode: 0 };
7580
7647
  }
7581
- const intoExisting = hasExistingKeynvEnv(root.path);
7582
7648
  Se(
7583
7649
  [
7584
7650
  `Project root: ${root.path}`,
@@ -7588,43 +7654,79 @@ async function runInitFlow(client, opts) {
7588
7654
  ].filter(Boolean).join("\n"),
7589
7655
  "Detected"
7590
7656
  );
7591
- const merged = mergeEnvFiles(envFiles);
7592
- if (merged.length === 0) {
7593
- R2.info("All env files were empty (only comments/blanks). Nothing to upload.");
7594
- ye("Done.");
7595
- return { exitCode: 0 };
7596
- }
7597
7657
  const projectChoice = await pickOrCreateProject(client, root.suggestedName);
7598
7658
  if (projectChoice === null) {
7599
7659
  me("No project selected.");
7600
7660
  return { exitCode: 130 };
7601
7661
  }
7602
- const envName = await pickEnvForUpload(client, projectChoice, envFiles);
7603
- if (envName === null) {
7604
- me("No environment selected.");
7662
+ const fileMapping = await pickFileEnvMapping(envFiles, projectChoice);
7663
+ if (fileMapping === null) {
7664
+ me("No env mapping selected.");
7605
7665
  return { exitCode: 130 };
7606
7666
  }
7607
- const choices = merged.map((e2) => {
7608
- const verdict = classifyEntry(e2.name, e2.value);
7609
- const hint = verdict.hint || (e2.isAlias ? "looks like an alias literal" : "no signal");
7610
- const preview2 = e2.isAlias ? e2.value : previewValue(e2.value, 32);
7611
- return {
7612
- name: e2.name,
7613
- value: e2.value,
7614
- isAlias: e2.isAlias,
7615
- verdict: verdict.verdict,
7616
- label: `${e2.name} ${preview2}`,
7617
- hint,
7618
- sourceLine: e2.sourceLine,
7619
- source: e2.source
7620
- };
7621
- });
7622
- const initialSecretSelection = choices.filter((c2) => c2.verdict === "secret" && !c2.isAlias).map((c2) => c2.name);
7623
- const selectedSecretNames = unwrap(
7667
+ const distinctEnvs = [...new Set(fileMapping.map((m) => m.envName))];
7668
+ const perEnv = parseAndMergePerEnv(fileMapping);
7669
+ const skipped = [];
7670
+ for (const env2 of distinctEnvs) {
7671
+ const before = perEnv.get(env2) ?? [];
7672
+ const kept = [];
7673
+ for (const e2 of before) {
7674
+ if (classifyEntry(e2.name, e2.value).verdict === "skip") {
7675
+ skipped.push({ env: env2, entry: e2 });
7676
+ } else {
7677
+ kept.push(e2);
7678
+ }
7679
+ }
7680
+ perEnv.set(env2, kept);
7681
+ }
7682
+ if (skipped.length > 0) {
7683
+ const names = [...new Set(skipped.map((s) => s.entry.name))];
7684
+ R2.info(
7685
+ `Skipped ${skipped.length} framework/shell-managed entr${skipped.length === 1 ? "y" : "ies"}: ${names.join(", ")}`
7686
+ );
7687
+ }
7688
+ for (const env2 of distinctEnvs) {
7689
+ const shadowed = (perEnv.get(env2) ?? []).filter((e2) => e2.shadowedBy.length > 0);
7690
+ if (shadowed.length === 0) continue;
7691
+ const detail = shadowed.map((e2) => ` [${env2}] ${e2.name}: ${e2.source} \u2192 ${e2.shadowedBy[e2.shadowedBy.length - 1]}`).join("\n");
7692
+ R2.warn(
7693
+ `Some keys appear in multiple env files mapped to the same env; using the last value (dotenv convention):
7694
+ ${detail}`
7695
+ );
7696
+ }
7697
+ const totalEntries = [...perEnv.values()].reduce((n, arr) => n + arr.length, 0);
7698
+ if (totalEntries === 0) {
7699
+ R2.info(
7700
+ "All env files were empty or only contained framework-managed vars. Nothing to upload."
7701
+ );
7702
+ ye("Done.");
7703
+ return { exitCode: 0 };
7704
+ }
7705
+ const choices = [];
7706
+ for (const env2 of distinctEnvs) {
7707
+ for (const e2 of perEnv.get(env2) ?? []) {
7708
+ const c2 = classifyEntry(e2.name, e2.value);
7709
+ const hint = c2.hint || (e2.isAlias ? "looks like an alias literal" : "no signal");
7710
+ const preview2 = e2.isAlias ? e2.value : previewValue(e2.value, 28);
7711
+ const envTag = distinctEnvs.length > 1 ? `[${env2}] ` : "";
7712
+ choices.push({
7713
+ composite: `${env2}|${e2.name}`,
7714
+ env: env2,
7715
+ name: e2.name,
7716
+ value: e2.value,
7717
+ isAlias: e2.isAlias,
7718
+ verdict: c2.verdict,
7719
+ label: `${envTag}${e2.name} ${preview2}`,
7720
+ hint
7721
+ });
7722
+ }
7723
+ }
7724
+ const initialSecretSelection = choices.filter((c2) => c2.verdict === "secret" && !c2.isAlias).map((c2) => c2.composite);
7725
+ const selectedComposites = unwrap(
7624
7726
  await ve({
7625
7727
  message: "Mark which keys are secrets (vault-uploaded). Unchecked keys stay as literals.",
7626
7728
  options: choices.map((c2) => ({
7627
- value: c2.name,
7729
+ value: c2.composite,
7628
7730
  label: c2.label,
7629
7731
  hint: c2.isAlias ? `${c2.hint} \u2014 already aliased; will pass through` : c2.hint
7630
7732
  })),
@@ -7632,50 +7734,26 @@ async function runInitFlow(client, opts) {
7632
7734
  required: false
7633
7735
  })
7634
7736
  );
7635
- const selected = new Set(selectedSecretNames);
7737
+ const selected = new Set(selectedComposites);
7738
+ const defaultEnv = pickDefaultEnv(distinctEnvs);
7636
7739
  let scriptWrapSelection = [];
7637
- let scriptPlan = root.packageJsonScripts ? planScriptWrap(root.packageJsonScripts) : null;
7740
+ const scriptPlan = root.packageJsonScripts ? planScriptWrap(root.packageJsonScripts) : null;
7638
7741
  if (!opts.noScripts && scriptPlan && scriptPlan.recommended.length > 0) {
7639
- const wrapAnswer = unwrap(
7640
- await ve({
7641
- message: "Wrap package.json scripts with `keynv exec`? (recommended)",
7642
- options: [
7643
- ...scriptPlan.recommended.map((a) => ({
7644
- value: a.name,
7645
- label: `${a.name} \u2192 ${a.wrapped}`,
7646
- hint: a.hint
7647
- })),
7648
- ...scriptPlan.unknown.map((a) => ({
7649
- value: a.name,
7650
- label: `${a.name} \u2192 ${a.wrapped}`,
7651
- hint: `unknown tool \u2014 opt in if you know it reads env`
7652
- }))
7653
- ],
7654
- initialValues: scriptPlan.recommended.map((a) => a.name),
7655
- required: false
7656
- })
7657
- );
7658
- scriptWrapSelection = wrapAnswer;
7659
- } else if (opts.noScripts) {
7660
- scriptPlan = null;
7661
- }
7662
- const envFileFateRaw = unwrap(
7663
- await Ee({
7664
- message: "After upload, what about the original .env files?",
7665
- options: [
7666
- { value: "delete", label: "Delete (recommended \u2014 eliminates the leak vector)" },
7667
- { value: "gitignore", label: "Keep but make sure .gitignore covers them" }
7668
- ],
7669
- initialValue: "delete"
7670
- })
7671
- );
7742
+ scriptWrapSelection = scriptPlan.recommended.map((a) => a.name);
7743
+ }
7744
+ const perEnvCounts = distinctEnvs.map((env2) => {
7745
+ const entries = perEnv.get(env2) ?? [];
7746
+ const sec = entries.filter((e2) => selected.has(`${env2}|${e2.name}`)).length;
7747
+ const lit = entries.length - sec;
7748
+ return ` ${env2}: ${sec} secrets, ${lit} literals${env2 === defaultEnv ? " (default \u2014 written to .keynv.env)" : ""}`;
7749
+ }).join("\n");
7672
7750
  const planSummary = [
7673
- `Project: ${projectChoice.name}${projectChoice.created ? " (will be created)" : ""}`,
7674
- `Environment: ${envName}`,
7675
- `Secrets to vault: ${selected.size}`,
7676
- `Literals in file: ${merged.length - selected.size}`,
7677
- `Script wraps: ${scriptWrapSelection.length}`,
7678
- `Original .env: ${envFileFateRaw === "delete" ? "delete" : "keep + gitignore"}`,
7751
+ `Project: ${projectChoice.name}${projectChoice.created ? " (will be created)" : ""}`,
7752
+ `Environments: ${distinctEnvs.join(", ")}`,
7753
+ "Per-env breakdown:",
7754
+ perEnvCounts,
7755
+ `Script wraps: ${scriptWrapSelection.length}`,
7756
+ "Original .env: delete after upload",
7679
7757
  opts.dryRun ? "Dry-run: no changes will be made." : ""
7680
7758
  ].filter(Boolean).join("\n");
7681
7759
  Se(planSummary, "About to apply");
@@ -7688,82 +7766,106 @@ async function runInitFlow(client, opts) {
7688
7766
  ye("Dry-run complete \u2014 no changes were made.");
7689
7767
  return { exitCode: 0 };
7690
7768
  }
7691
- let projectId2;
7692
- if (projectChoice.created) {
7693
- const s = ft();
7694
- s.start(`Creating project "${projectChoice.name}"`);
7695
- try {
7696
- const created = await client.request("/v1/projects", {
7697
- method: "POST",
7698
- body: {
7699
- name: projectChoice.name,
7700
- environments: [{ name: envName, tier: "non-production", require_approval: false }]
7701
- }
7702
- });
7703
- projectId2 = created.id;
7704
- s.stop(`Created project ${created.name}`);
7705
- } catch (err) {
7706
- s.error(`Failed to create project: ${err instanceof Error ? err.message : String(err)}`);
7707
- return { exitCode: 1 };
7708
- }
7709
- } else {
7710
- projectId2 = projectChoice.id;
7711
- }
7712
- const uploaded = [];
7769
+ const projectId2 = await ensureProjectAndEnvs(client, projectChoice, distinctEnvs);
7770
+ if (projectId2 === null) return { exitCode: 1 };
7771
+ const uploadedByEnv = /* @__PURE__ */ new Map();
7713
7772
  const failed = [];
7714
- if (selected.size > 0) {
7773
+ const totalToUpload = selected.size;
7774
+ if (totalToUpload > 0) {
7715
7775
  const s = ft();
7716
- s.start(`Uploading ${selected.size} secrets`);
7776
+ s.start(`Uploading ${totalToUpload} secret${totalToUpload === 1 ? "" : "s"}`);
7717
7777
  let i = 0;
7718
- for (const entry2 of merged) {
7719
- if (!selected.has(entry2.name)) continue;
7720
- i++;
7721
- s.message(`Uploading (${i}/${selected.size}) ${entry2.name}`);
7722
- const aliasKey = entry2.name.toLowerCase().replace(/_/g, "-");
7723
- try {
7724
- await client.request(`/v1/projects/${projectId2}/secrets`, {
7725
- method: "POST",
7726
- body: { env: envName, key: aliasKey, value: entry2.value }
7727
- });
7728
- const alias2 = buildAlias({ project: projectChoice.name, environment: envName, key: aliasKey });
7729
- if (alias2 === null) {
7778
+ for (const env2 of distinctEnvs) {
7779
+ const envUploaded = /* @__PURE__ */ new Map();
7780
+ uploadedByEnv.set(env2, envUploaded);
7781
+ for (const e2 of perEnv.get(env2) ?? []) {
7782
+ if (!selected.has(`${env2}|${e2.name}`)) continue;
7783
+ i++;
7784
+ s.message(`Uploading (${i}/${totalToUpload}) [${env2}] ${e2.name}`);
7785
+ const aliasKey = e2.name.toLowerCase().replace(/_/g, "-");
7786
+ try {
7787
+ await client.request(`/v1/projects/${projectId2}/secrets`, {
7788
+ method: "POST",
7789
+ body: { env: env2, key: aliasKey, value: e2.value }
7790
+ });
7791
+ const alias2 = buildAlias({
7792
+ project: projectChoice.name,
7793
+ environment: env2,
7794
+ key: aliasKey
7795
+ });
7796
+ if (alias2 === null) {
7797
+ failed.push({
7798
+ env: env2,
7799
+ name: e2.name,
7800
+ reason: `produced an invalid alias for project=${projectChoice.name} env=${env2} key=${aliasKey}`
7801
+ });
7802
+ } else {
7803
+ envUploaded.set(e2.name, alias2.literal);
7804
+ }
7805
+ } catch (err) {
7730
7806
  failed.push({
7731
- name: entry2.name,
7732
- reason: `produced an invalid alias for project=${projectChoice.name} env=${envName} key=${aliasKey}`
7807
+ env: env2,
7808
+ name: e2.name,
7809
+ reason: err instanceof Error ? err.message : String(err)
7733
7810
  });
7734
- } else {
7735
- uploaded.push({ name: entry2.name, aliasLiteral: alias2.literal });
7736
7811
  }
7737
- } catch (err) {
7738
- failed.push({ name: entry2.name, reason: err instanceof Error ? err.message : String(err) });
7739
7812
  }
7740
7813
  }
7741
7814
  if (failed.length === 0) {
7742
- s.stop(`Uploaded ${uploaded.length} secrets`);
7815
+ s.stop(`Uploaded ${totalToUpload} secret${totalToUpload === 1 ? "" : "s"}`);
7743
7816
  } else {
7744
- s.error(`${uploaded.length}/${selected.size} uploaded; ${failed.length} failed`);
7745
- for (const f of failed) R2.warn(` ${f.name}: ${f.reason}`);
7817
+ s.error(
7818
+ `${totalToUpload - failed.length}/${totalToUpload} uploaded; ${failed.length} failed`
7819
+ );
7820
+ for (const f of failed) R2.warn(` [${f.env}] ${f.name}: ${f.reason}`);
7746
7821
  }
7747
7822
  }
7748
- const literals = merged.filter((e2) => !selected.has(e2.name));
7749
- const successUploads = new Map(uploaded.map((u) => [u.name, u.aliasLiteral]));
7823
+ const defaultUploaded = uploadedByEnv.get(defaultEnv) ?? /* @__PURE__ */ new Map();
7824
+ const defaultLiterals = (perEnv.get(defaultEnv) ?? []).filter(
7825
+ (e2) => !selected.has(`${defaultEnv}|${e2.name}`)
7826
+ );
7750
7827
  const keynvEnvPath = join(root.path, ".keynv.env");
7751
- let writtenLines;
7752
7828
  try {
7753
- writtenLines = composeKeynvEnv({
7754
- uploadedAliases: successUploads,
7755
- literals,
7829
+ const lines = composeKeynvEnv({
7830
+ uploadedAliases: defaultUploaded,
7831
+ literals: defaultLiterals,
7756
7832
  mergeWithExisting: intoExisting ? readFileSync(keynvEnvPath, "utf8") : null
7757
7833
  });
7758
- writeFileSync(keynvEnvPath, `${writtenLines.join("\n")}
7834
+ writeFileSync(keynvEnvPath, `${lines.join("\n")}
7759
7835
  `);
7760
7836
  R2.success(
7761
- `${intoExisting ? "Updated" : "Wrote"} ${keynvEnvPath} (${successUploads.size + literals.length} entries)`
7837
+ `${intoExisting ? "Updated" : "Wrote"} ${keynvEnvPath} (${defaultUploaded.size + defaultLiterals.length} entries from "${defaultEnv}")`
7762
7838
  );
7763
7839
  } catch (err) {
7764
7840
  R2.error(`Failed to write .keynv.env: ${err instanceof Error ? err.message : String(err)}`);
7765
7841
  return { exitCode: 1 };
7766
7842
  }
7843
+ const otherEnvsWithAliases = distinctEnvs.filter(
7844
+ (env2) => env2 !== defaultEnv && (uploadedByEnv.get(env2)?.size ?? 0) > 0
7845
+ );
7846
+ for (const env2 of otherEnvsWithAliases) {
7847
+ const envUploaded = uploadedByEnv.get(env2) ?? /* @__PURE__ */ new Map();
7848
+ const envLiterals = (perEnv.get(env2) ?? []).filter(
7849
+ (e2) => !selected.has(`${env2}|${e2.name}`)
7850
+ );
7851
+ const envFilePath = join(root.path, `.keynv.${env2}.env`);
7852
+ try {
7853
+ const lines = composeKeynvEnv({
7854
+ uploadedAliases: envUploaded,
7855
+ literals: envLiterals,
7856
+ mergeWithExisting: null
7857
+ });
7858
+ writeFileSync(envFilePath, `${lines.join("\n")}
7859
+ `);
7860
+ R2.success(
7861
+ `Wrote .keynv.${env2}.env (${envUploaded.size} secret alias${envUploaded.size === 1 ? "" : "es"} from "${env2}")`
7862
+ );
7863
+ } catch (err) {
7864
+ R2.warn(
7865
+ `Could not write .keynv.${env2}.env: ${err instanceof Error ? err.message : String(err)}`
7866
+ );
7867
+ }
7868
+ }
7767
7869
  try {
7768
7870
  const outcome = writeAiContext(root.path);
7769
7871
  if (outcome === "created") R2.success("Wrote AGENTS.md (so AI agents understand keynv)");
@@ -7779,66 +7881,37 @@ async function runInitFlow(client, opts) {
7779
7881
  root.packageJsonScripts,
7780
7882
  scriptWrapSelection
7781
7883
  );
7782
- R2.success(`Wrapped ${scriptWrapSelection.length} script(s) in package.json`);
7884
+ R2.success(
7885
+ `Wrapped ${scriptWrapSelection.length} script${scriptWrapSelection.length === 1 ? "" : "s"} in package.json`
7886
+ );
7783
7887
  } catch (err) {
7784
7888
  R2.warn(
7785
7889
  `Could not update package.json scripts: ${err instanceof Error ? err.message : String(err)}`
7786
7890
  );
7787
7891
  }
7788
7892
  }
7789
- if (envFileFateRaw === "delete") {
7790
- for (const f of envFiles) {
7791
- try {
7792
- unlinkSync(f.path);
7793
- R2.success(`Removed ${f.name}`);
7794
- } catch (err) {
7795
- R2.warn(`Could not remove ${f.name}: ${err instanceof Error ? err.message : String(err)}`);
7796
- }
7797
- }
7798
- } else {
7799
- const gitignorePath = join(root.path, ".gitignore");
7893
+ for (const f of envFiles) {
7800
7894
  try {
7801
- ensureGitignoreEntries(gitignorePath, envFiles.map((f) => f.name));
7802
- R2.success(`Updated .gitignore (${envFiles.length} entries ensured)`);
7895
+ unlinkSync(f.path);
7896
+ R2.success(`Removed ${f.name}`);
7803
7897
  } catch (err) {
7804
- R2.warn(`Could not update .gitignore: ${err instanceof Error ? err.message : String(err)}`);
7898
+ R2.warn(`Could not remove ${f.name}: ${err instanceof Error ? err.message : String(err)}`);
7805
7899
  }
7806
7900
  }
7901
+ const otherEnvs = distinctEnvs.filter((e2) => e2 !== defaultEnv);
7902
+ if (otherEnvs.length > 0) {
7903
+ const lines = otherEnvs.map((env2) => {
7904
+ const count = uploadedByEnv.get(env2)?.size ?? 0;
7905
+ const hasFile = count > 0;
7906
+ return hasFile ? ` ${env2}: ${count} secret${count === 1 ? "" : "s"} \u2192 .keynv.${env2}.env (use \`keynv exec --from .keynv.${env2}.env -- <cmd>\`)` : ` ${env2}: 0 secrets in vault (no alias file written)`;
7907
+ });
7908
+ Se(lines.join("\n"), "Secrets in other envs");
7909
+ }
7807
7910
  ye(
7808
- failed.length > 0 ? `Done with ${failed.length} failure(s) \u2014 see warnings above.` : `Done. Try: ${scriptWrapSelection.includes("dev") ? "npm run dev" : "keynv exec -- <your command>"}`
7911
+ failed.length > 0 ? `Done with ${failed.length} failure(s) \u2014 see warnings above.` : `Done. Try: ${scriptWrapSelection.includes("dev") ? "pnpm dev" : "keynv exec -- <your command>"}`
7809
7912
  );
7810
7913
  return { exitCode: failed.length > 0 ? 1 : 0 };
7811
7914
  }
7812
- function mergeEnvFiles(files) {
7813
- const map = /* @__PURE__ */ new Map();
7814
- for (const f of files) {
7815
- let entries;
7816
- try {
7817
- entries = parseEnvFile(readFileSync(f.path, "utf8"), f.path);
7818
- } catch (err) {
7819
- R2.warn(`${f.name}: ${err instanceof Error ? err.message : String(err)} \u2014 skipping file`);
7820
- continue;
7821
- }
7822
- for (const e2 of entries) {
7823
- const existing = map.get(e2.name);
7824
- if (existing) {
7825
- existing.shadowedBy.push(f.name);
7826
- existing.value = e2.value;
7827
- existing.isAlias = e2.isAlias;
7828
- } else {
7829
- map.set(e2.name, {
7830
- name: e2.name,
7831
- value: e2.value,
7832
- isAlias: e2.isAlias,
7833
- source: f.name,
7834
- sourceLine: e2.line,
7835
- shadowedBy: []
7836
- });
7837
- }
7838
- }
7839
- }
7840
- return [...map.values()];
7841
- }
7842
7915
  async function pickOrCreateProject(client, suggestedName) {
7843
7916
  const projects = await listProjects(client);
7844
7917
  const value = unwrap(
@@ -7858,53 +7931,156 @@ async function pickOrCreateProject(client, suggestedName) {
7858
7931
  validate: (v2) => v2 && /^[a-z0-9][a-z0-9-]{0,47}$/.test(v2) ? void 0 : "lowercase letters, digits, dashes; up to 48 chars"
7859
7932
  })
7860
7933
  );
7861
- return { id: "", name, created: true };
7934
+ return { id: "", name, created: true, existingEnvs: [] };
7862
7935
  }
7863
7936
  const match = projects.find((p2) => p2.id === value);
7864
7937
  if (!match) return null;
7865
- return { id: match.id, name: match.name, created: false };
7938
+ const detail = await client.request(`/v1/projects/${match.id}`);
7939
+ return {
7940
+ id: match.id,
7941
+ name: match.name,
7942
+ created: false,
7943
+ existingEnvs: detail.environments
7944
+ };
7866
7945
  }
7867
- async function pickEnvForUpload(client, project, envFiles) {
7868
- const firstSuffix = envFiles[0]?.suffix ?? null;
7869
- const suggested = suggestedEnvForSuffix(firstSuffix);
7870
- if (project.created) {
7871
- const value2 = unwrap(
7872
- await Re({
7873
- message: "Environment to create",
7874
- initialValue: suggested,
7875
- validate: (v2) => v2 && /^[a-z0-9][a-z0-9-]{0,23}$/.test(v2) ? void 0 : "lowercase letters, digits, dashes; up to 24 chars"
7946
+ async function pickFileEnvMapping(files, project) {
7947
+ if (files.length === 0) return [];
7948
+ const existingEnvNames = new Set(project.existingEnvs.map((e2) => e2.name));
7949
+ if (files.length === 1) {
7950
+ const onlyFile = files[0];
7951
+ if (!onlyFile) return [];
7952
+ const suggested = suggestedEnvForSuffix(onlyFile.suffix);
7953
+ return [{ file: onlyFile, envName: suggested }];
7954
+ }
7955
+ const assignments = [];
7956
+ for (const f of files) {
7957
+ const suggested = suggestedEnvForSuffix(f.suffix);
7958
+ const opts = [];
7959
+ opts.push({
7960
+ value: suggested,
7961
+ label: suggested,
7962
+ hint: existingEnvNames.has(suggested) ? "existing env" : project.created ? "will be created with project" : "will be added to project"
7963
+ });
7964
+ for (const e2 of project.existingEnvs) {
7965
+ if (e2.name === suggested) continue;
7966
+ opts.push({ value: e2.name, label: e2.name, hint: e2.tier });
7967
+ }
7968
+ opts.push({ value: "__custom", label: "+ Custom env name\u2026" });
7969
+ let envName = unwrap(
7970
+ await Ee({
7971
+ message: `Map ${f.name} to which keynv env?`,
7972
+ options: opts,
7973
+ initialValue: suggested
7876
7974
  })
7877
7975
  );
7878
- return value2;
7976
+ if (envName === "__custom") {
7977
+ envName = unwrap(
7978
+ await Re({
7979
+ message: "New env name",
7980
+ validate: (v2) => v2 && /^[a-z0-9][a-z0-9-]{0,23}$/.test(v2) ? void 0 : "lowercase letters, digits, dashes; up to 24 chars"
7981
+ })
7982
+ );
7983
+ }
7984
+ assignments.push({ file: f, envName });
7879
7985
  }
7880
- const detail = await client.request(`/v1/projects/${project.id}`);
7881
- if (detail.environments.length === 0) {
7882
- me(
7883
- `Project "${project.name}" has no environments yet, and we can't add one from init in this version. Create one with \`keynv project create\` (or pick a different project).`
7884
- );
7885
- return null;
7986
+ return assignments;
7987
+ }
7988
+ function parseAndMergePerEnv(mapping) {
7989
+ const acc = /* @__PURE__ */ new Map();
7990
+ for (const { file, envName } of mapping) {
7991
+ let entries;
7992
+ try {
7993
+ entries = parseEnvFile(readFileSync(file.path, "utf8"), file.path);
7994
+ } catch (err) {
7995
+ R2.warn(`${file.name}: ${err instanceof Error ? err.message : String(err)} \u2014 skipping file`);
7996
+ continue;
7997
+ }
7998
+ let envMap = acc.get(envName);
7999
+ if (!envMap) {
8000
+ envMap = /* @__PURE__ */ new Map();
8001
+ acc.set(envName, envMap);
8002
+ }
8003
+ for (const e2 of entries) {
8004
+ const existing = envMap.get(e2.name);
8005
+ if (existing) {
8006
+ existing.shadowedBy.push(file.name);
8007
+ existing.value = e2.value;
8008
+ existing.isAlias = e2.isAlias;
8009
+ } else {
8010
+ envMap.set(e2.name, {
8011
+ name: e2.name,
8012
+ value: e2.value,
8013
+ isAlias: e2.isAlias,
8014
+ source: file.name,
8015
+ sourceLine: e2.line,
8016
+ shadowedBy: []
8017
+ });
8018
+ }
8019
+ }
7886
8020
  }
7887
- if (detail.environments.length === 1) {
7888
- return detail.environments[0]?.name ?? null;
8021
+ const out = /* @__PURE__ */ new Map();
8022
+ for (const [env2, m] of acc) out.set(env2, [...m.values()]);
8023
+ return out;
8024
+ }
8025
+ function pickDefaultEnv(envs) {
8026
+ if (envs.length === 0) return "dev";
8027
+ if (envs.includes("dev")) return "dev";
8028
+ return envs[0];
8029
+ }
8030
+ async function ensureProjectAndEnvs(client, projectChoice, distinctEnvs) {
8031
+ if (projectChoice.created) {
8032
+ const s2 = ft();
8033
+ s2.start(`Creating project "${projectChoice.name}" with ${distinctEnvs.length} env(s)`);
8034
+ try {
8035
+ const created = await client.request("/v1/projects", {
8036
+ method: "POST",
8037
+ body: {
8038
+ name: projectChoice.name,
8039
+ environments: distinctEnvs.map((name) => envBodyFor(name))
8040
+ }
8041
+ });
8042
+ s2.stop(`Created project ${created.name}`);
8043
+ return created.id;
8044
+ } catch (err) {
8045
+ s2.error(`Failed to create project: ${err instanceof Error ? err.message : String(err)}`);
8046
+ return null;
8047
+ }
7889
8048
  }
7890
- const value = unwrap(
7891
- await Ee({
7892
- message: "Upload to which environment?",
7893
- options: detail.environments.map((e2) => ({
7894
- value: e2.name,
7895
- label: e2.name,
7896
- hint: e2.tier
7897
- })),
7898
- initialValue: detail.environments.find((e2) => e2.name === suggested)?.name
7899
- })
7900
- );
7901
- return value;
8049
+ const existing = new Set(projectChoice.existingEnvs.map((e2) => e2.name));
8050
+ const missing = distinctEnvs.filter((e2) => !existing.has(e2));
8051
+ if (missing.length === 0) return projectChoice.id;
8052
+ const s = ft();
8053
+ s.start(`Adding ${missing.length} missing env(s) to project "${projectChoice.name}"`);
8054
+ for (const envName of missing) {
8055
+ try {
8056
+ await client.request(`/v1/projects/${projectChoice.id}/environments`, {
8057
+ method: "POST",
8058
+ body: envBodyFor(envName)
8059
+ });
8060
+ } catch (err) {
8061
+ s.error(
8062
+ `Failed to add env "${envName}": ${err instanceof Error ? err.message : String(err)}`
8063
+ );
8064
+ return null;
8065
+ }
8066
+ }
8067
+ s.stop(`Added env(s): ${missing.join(", ")}`);
8068
+ return projectChoice.id;
8069
+ }
8070
+ function envBodyFor(name) {
8071
+ const isProd = name === "prod" || name === "production";
8072
+ return {
8073
+ name,
8074
+ tier: isProd ? "production" : "non-production",
8075
+ require_approval: isProd
8076
+ };
7902
8077
  }
7903
8078
  function composeKeynvEnv(opts) {
7904
8079
  const { uploadedAliases, literals, mergeWithExisting } = opts;
7905
8080
  const lines = [];
7906
8081
  if (mergeWithExisting !== null) {
7907
- const trimmed = mergeWithExisting.replace(/\n+$/, "");
8082
+ const normalized = mergeWithExisting.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
8083
+ const trimmed = normalized.replace(/\n+$/, "");
7908
8084
  if (trimmed.length > 0) {
7909
8085
  lines.push(...trimmed.split("\n"));
7910
8086
  lines.push("");
@@ -7931,7 +8107,7 @@ function composeKeynvEnv(opts) {
7931
8107
  }
7932
8108
  }
7933
8109
  if (mergeWithExisting !== null) {
7934
- lines.push(`# <<< keynv init <<<`);
8110
+ lines.push("# <<< keynv init <<<");
7935
8111
  }
7936
8112
  return lines;
7937
8113
  }
@@ -7950,23 +8126,13 @@ function updatePackageJsonScripts(path, originalScripts, selectedNames) {
7950
8126
  writeFileSync(path, trailingNewline ? `${out}
7951
8127
  ` : out);
7952
8128
  }
7953
- function ensureGitignoreEntries(path, basenames) {
7954
- let existing = "";
7955
- if (existsSync(path)) existing = readFileSync(path, "utf8");
7956
- const existingLines = new Set(existing.split("\n").map((l) => l.trim()));
7957
- const toAdd = basenames.filter((n) => !existingLines.has(n));
7958
- if (toAdd.length === 0) return;
7959
- const sep = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
7960
- const block = ["", "# added by keynv init", ...toAdd, ""].join("\n");
7961
- writeFileSync(path, `${existing}${sep}${block}`);
7962
- }
7963
8129
  var init_init = __esm({
7964
8130
  "src/ui/flows/init.ts"() {
7965
- init_dist();
7966
8131
  init_dist5();
8132
+ init_dist();
7967
8133
  init_envFile();
7968
- init_detect();
7969
8134
  init_aiContext();
8135
+ init_detect();
7970
8136
  init_heuristics();
7971
8137
  init_scriptWrap();
7972
8138
  init_cancel();
@@ -7974,30 +8140,122 @@ var init_init = __esm({
7974
8140
  }
7975
8141
  });
7976
8142
 
7977
- // src/ui/helpers/pickSecret.ts
7978
- async function listSecrets(client, projectId2) {
7979
- const data = await client.request(
7980
- `/v1/projects/${projectId2}/secrets`
7981
- );
7982
- return data.secrets;
8143
+ // src/ui/helpers/tty.ts
8144
+ var tty_exports = {};
8145
+ __export(tty_exports, {
8146
+ isInteractive: () => isInteractive
8147
+ });
8148
+ function isInteractive() {
8149
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
7983
8150
  }
7984
- async function pickSecret(client, projectId2, message = "Secret:") {
7985
- const secrets = await listSecrets(client, projectId2);
7986
- if (secrets.length === 0) return null;
7987
- const value = await Ee({
7988
- message,
7989
- options: secrets.map((s) => ({
7990
- value: s.alias,
7991
- label: s.alias,
7992
- hint: `v${s.version}`
7993
- }))
8151
+ var init_tty = __esm({
8152
+ "src/ui/helpers/tty.ts"() {
8153
+ }
8154
+ });
8155
+ function sleep(ms) {
8156
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
8157
+ }
8158
+ async function errorMessage(res, fallback) {
8159
+ const text = await res.text();
8160
+ if (!text) return fallback;
8161
+ try {
8162
+ const parsed = JSON.parse(text);
8163
+ return parsed.error?.message ?? fallback;
8164
+ } catch {
8165
+ return fallback;
8166
+ }
8167
+ }
8168
+ function openBrowser(url) {
8169
+ try {
8170
+ const os = platform();
8171
+ const command = os === "win32" ? "cmd" : os === "darwin" ? "open" : "xdg-open";
8172
+ const args = os === "win32" ? ["/c", "start", "", url] : [url];
8173
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
8174
+ child.unref();
8175
+ return true;
8176
+ } catch {
8177
+ return false;
8178
+ }
8179
+ }
8180
+ async function runBrowserAuth(serverUrl) {
8181
+ const startRes = await fetch(new URL("/v1/auth/cli/browser/start", serverUrl).toString(), {
8182
+ method: "POST",
8183
+ headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
8184
+ body: JSON.stringify({ device_name: hostname() })
7994
8185
  });
7995
- if (q(value)) return null;
7996
- return secrets.find((s) => s.alias === value) ?? null;
8186
+ if (!startRes.ok) {
8187
+ throw new BrowserAuthError(
8188
+ await errorMessage(startRes, `Browser auth failed to start (${startRes.status}).`)
8189
+ );
8190
+ }
8191
+ const start = await startRes.json();
8192
+ const opened = openBrowser(start.verification_uri_complete);
8193
+ if (opened) {
8194
+ process.stderr.write(
8195
+ `
8196
+ Your code: ${start.user_code}
8197
+ Complete auth in your browser, then return here.
8198
+
8199
+ `
8200
+ );
8201
+ } else {
8202
+ process.stderr.write(
8203
+ `
8204
+ Could not open a browser automatically.
8205
+ Open this URL manually:
8206
+
8207
+ ${start.verification_uri_complete}
8208
+
8209
+ Your code: ${start.user_code}
8210
+ Waiting for you to complete auth in the browser...
8211
+
8212
+ `
8213
+ );
8214
+ }
8215
+ const deadline = Date.now() + start.expires_in * 1e3;
8216
+ const intervalMs = Math.max(1, start.interval) * 1e3;
8217
+ while (Date.now() < deadline) {
8218
+ await sleep(intervalMs);
8219
+ const pollRes = await fetch(new URL("/v1/auth/cli/browser/poll", serverUrl).toString(), {
8220
+ method: "POST",
8221
+ headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
8222
+ body: JSON.stringify({ device_code: start.device_code })
8223
+ });
8224
+ if (pollRes.status === 202) continue;
8225
+ if (!pollRes.ok) {
8226
+ throw new BrowserAuthError(
8227
+ await errorMessage(pollRes, `Browser auth failed (${pollRes.status}).`)
8228
+ );
8229
+ }
8230
+ const data = await pollRes.json();
8231
+ return {
8232
+ auth_kind: "session",
8233
+ server_url: serverUrl,
8234
+ user_id: data.user.id,
8235
+ email: data.user.email,
8236
+ org_id: data.user.org_id,
8237
+ org_role: data.user.org_role,
8238
+ access_token: data.access_token,
8239
+ refresh_token: data.refresh_token,
8240
+ access_expires_at: new Date(Date.now() + data.expires_in * 1e3).toISOString()
8241
+ };
8242
+ }
8243
+ throw new BrowserAuthError("Browser auth timed out. Run `keynv login` to try again.");
7997
8244
  }
7998
- var init_pickSecret = __esm({
7999
- "src/ui/helpers/pickSecret.ts"() {
8000
- init_dist5();
8245
+ var BrowserAuthError;
8246
+ var init_browser_auth = __esm({
8247
+ "src/client/browser-auth.ts"() {
8248
+ init_version();
8249
+ BrowserAuthError = class extends Error {
8250
+ };
8251
+ }
8252
+ });
8253
+
8254
+ // src/client/defaults.ts
8255
+ var DEFAULT_SERVER_URL;
8256
+ var init_defaults = __esm({
8257
+ "src/client/defaults.ts"() {
8258
+ DEFAULT_SERVER_URL = "https://api.keynv.dev";
8001
8259
  }
8002
8260
  });
8003
8261
 
@@ -8025,6 +8283,33 @@ var init_pickEnv = __esm({
8025
8283
  init_dist5();
8026
8284
  }
8027
8285
  });
8286
+
8287
+ // src/ui/helpers/pickSecret.ts
8288
+ async function listSecrets(client, projectId2) {
8289
+ const data = await client.request(
8290
+ `/v1/projects/${projectId2}/secrets`
8291
+ );
8292
+ return data.secrets;
8293
+ }
8294
+ async function pickSecret(client, projectId2, message = "Secret:") {
8295
+ const secrets = await listSecrets(client, projectId2);
8296
+ if (secrets.length === 0) return null;
8297
+ const value = await Ee({
8298
+ message,
8299
+ options: secrets.map((s) => ({
8300
+ value: s.alias,
8301
+ label: s.alias,
8302
+ hint: `v${s.version}`
8303
+ }))
8304
+ });
8305
+ if (q(value)) return null;
8306
+ return secrets.find((s) => s.alias === value) ?? null;
8307
+ }
8308
+ var init_pickSecret = __esm({
8309
+ "src/ui/helpers/pickSecret.ts"() {
8310
+ init_dist5();
8311
+ }
8312
+ });
8028
8313
  async function runSecretsFlow(client, project) {
8029
8314
  let target = project;
8030
8315
  if (!target) {
@@ -8088,7 +8373,10 @@ async function runSecretMenu(client, project, alias2) {
8088
8373
  }
8089
8374
  } else if (choice === "rotate") {
8090
8375
  const newValue = unwrap(
8091
- await Ce({ message: "New value", validate: (v2) => v2 && v2.length ? void 0 : "required" })
8376
+ await Ce({
8377
+ message: "New value",
8378
+ validate: (v2) => v2?.length ? void 0 : "required"
8379
+ })
8092
8380
  );
8093
8381
  const data = await client.request(`${path}/rotate`, {
8094
8382
  method: "POST",
@@ -8096,9 +8384,7 @@ async function runSecretMenu(client, project, alias2) {
8096
8384
  });
8097
8385
  R2.success(`Rotated ${data.alias} \u2192 v${data.version}`);
8098
8386
  } else if (choice === "delete") {
8099
- const confirmed = unwrap(
8100
- await ue({ message: `Delete ${alias2}?`, initialValue: false })
8101
- );
8387
+ const confirmed = unwrap(await ue({ message: `Delete ${alias2}?`, initialValue: false }));
8102
8388
  if (!confirmed) continue;
8103
8389
  await client.request(path, { method: "DELETE" });
8104
8390
  R2.success(`Deleted ${alias2}`);
@@ -8107,11 +8393,13 @@ async function runSecretMenu(client, project, alias2) {
8107
8393
  }
8108
8394
  }
8109
8395
  async function copyToClipboard(value) {
8110
- const platform = process.platform;
8111
- const cmd = platform === "darwin" ? ["pbcopy", []] : platform === "win32" ? ["clip", []] : ["xclip", ["-selection", "clipboard"]];
8396
+ const platform2 = process.platform;
8397
+ const cmd = platform2 === "darwin" ? ["pbcopy", []] : platform2 === "win32" ? ["clip", []] : ["xclip", ["-selection", "clipboard"]];
8112
8398
  return new Promise((resolve3) => {
8113
8399
  try {
8114
- const child = spawn(cmd[0], cmd[1], { stdio: ["pipe", "ignore", "ignore"] });
8400
+ const child = spawn(cmd[0], cmd[1], {
8401
+ stdio: ["pipe", "ignore", "ignore"]
8402
+ });
8115
8403
  child.on("error", () => resolve3(false));
8116
8404
  child.on("exit", (code) => resolve3(code === 0));
8117
8405
  child.stdin.end(value);
@@ -8138,7 +8426,7 @@ async function promptNewSecret(client, project) {
8138
8426
  }),
8139
8427
  value: () => Ce({
8140
8428
  message: "Value (hidden)",
8141
- validate: (v2) => v2 && v2.length ? void 0 : "required"
8429
+ validate: (v2) => v2?.length ? void 0 : "required"
8142
8430
  })
8143
8431
  },
8144
8432
  {
@@ -8167,8 +8455,8 @@ async function createSecretInteractive(client, project) {
8167
8455
  }
8168
8456
  var init_secret = __esm({
8169
8457
  "src/ui/flows/secret.ts"() {
8170
- init_dist();
8171
8458
  init_dist5();
8459
+ init_dist();
8172
8460
  init_cancel();
8173
8461
  init_pickEnv();
8174
8462
  init_pickProject();
@@ -28501,7 +28789,7 @@ var require_utils_webcrypto = __commonJS({
28501
28789
  var nodeCrypto = __require("crypto");
28502
28790
  module.exports = {
28503
28791
  postgresMd5PasswordHash,
28504
- randomBytes,
28792
+ randomBytes: randomBytes2,
28505
28793
  deriveKey,
28506
28794
  sha256,
28507
28795
  hashByName,
@@ -28511,7 +28799,7 @@ var require_utils_webcrypto = __commonJS({
28511
28799
  var webCrypto = nodeCrypto.webcrypto || globalThis.crypto;
28512
28800
  var subtleCrypto = webCrypto.subtle;
28513
28801
  var textEncoder = new TextEncoder();
28514
- function randomBytes(length) {
28802
+ function randomBytes2(length) {
28515
28803
  return webCrypto.getRandomValues(Buffer.alloc(length));
28516
28804
  }
28517
28805
  async function md5(string) {
@@ -28907,11 +29195,11 @@ var require_pg_connection_string = __commonJS({
28907
29195
  config.client_encoding = result.searchParams.get("encoding");
28908
29196
  return config;
28909
29197
  }
28910
- const hostname = dummyHost ? "" : result.hostname;
29198
+ const hostname2 = dummyHost ? "" : result.hostname;
28911
29199
  if (!config.host) {
28912
- config.host = decodeURIComponent(hostname);
28913
- } else if (hostname && /^%2f/i.test(hostname)) {
28914
- result.pathname = hostname + result.pathname;
29200
+ config.host = decodeURIComponent(hostname2);
29201
+ } else if (hostname2 && /^%2f/i.test(hostname2)) {
29202
+ result.pathname = hostname2 + result.pathname;
28915
29203
  }
28916
29204
  if (!config.port) {
28917
29205
  config.port = result.port;
@@ -40017,9 +40305,9 @@ var require_cluster = __commonJS({
40017
40305
  }
40018
40306
  });
40019
40307
  }
40020
- resolveSrv(hostname) {
40308
+ resolveSrv(hostname2) {
40021
40309
  return new Promise((resolve3, reject) => {
40022
- this.options.resolveSrv(hostname, (err, records) => {
40310
+ this.options.resolveSrv(hostname2, (err, records) => {
40023
40311
  if (err) {
40024
40312
  return reject(err);
40025
40313
  }
@@ -40041,14 +40329,14 @@ var require_cluster = __commonJS({
40041
40329
  });
40042
40330
  });
40043
40331
  }
40044
- dnsLookup(hostname) {
40332
+ dnsLookup(hostname2) {
40045
40333
  return new Promise((resolve3, reject) => {
40046
- this.options.dnsLookup(hostname, (err, address) => {
40334
+ this.options.dnsLookup(hostname2, (err, address) => {
40047
40335
  if (err) {
40048
- debug2("failed to resolve hostname %s to IP: %s", hostname, err.message);
40336
+ debug2("failed to resolve hostname %s to IP: %s", hostname2, err.message);
40049
40337
  reject(err);
40050
40338
  } else {
40051
- debug2("resolved hostname %s to IP %s", hostname, address);
40339
+ debug2("resolved hostname %s to IP %s", hostname2, address);
40052
40340
  resolve3(address);
40053
40341
  }
40054
40342
  });
@@ -42373,85 +42661,27 @@ var init_audit2 = __esm({
42373
42661
  });
42374
42662
 
42375
42663
  // src/ui/flows/login.ts
42376
- async function runLoginFlow(client) {
42377
- const answers = await he(
42378
- {
42379
- server: () => Re({
42380
- message: "Server URL",
42381
- placeholder: "http://localhost:8080",
42382
- defaultValue: "http://localhost:8080"
42383
- }),
42384
- email: () => Re({
42385
- message: "Email",
42386
- validate: (v2) => v2 && v2.includes("@") ? void 0 : "enter an email"
42387
- }),
42388
- password: () => Ce({
42389
- message: "Password",
42390
- validate: (v2) => v2 && v2.length >= 1 ? void 0 : "required"
42391
- })
42392
- },
42393
- {
42394
- onCancel: () => {
42395
- throw new UserCancelled();
42396
- }
42397
- }
42398
- );
42399
- const server = answers.server || "http://localhost:8080";
42664
+ async function runLoginFlow(client, options = {}) {
42665
+ const server = options.server ?? DEFAULT_SERVER_URL;
42400
42666
  const s = ft();
42401
- s.start("Authenticating");
42402
- let res;
42667
+ s.start("Waiting for authorization");
42668
+ let creds;
42403
42669
  try {
42404
- res = await fetch(new URL("/v1/auth/login", server).toString(), {
42405
- method: "POST",
42406
- headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
42407
- body: JSON.stringify({ email: answers.email, password: answers.password })
42408
- });
42670
+ creds = await runBrowserAuth(server);
42409
42671
  } catch (err) {
42410
- s.error("Network error");
42411
- me(err instanceof Error ? err.message : "unreachable");
42672
+ s.error("Browser login failed");
42673
+ me(err instanceof Error ? err.message : "Unable to authenticate.");
42412
42674
  return false;
42413
42675
  }
42414
- if (!res.ok) {
42415
- let msg = `login failed (${res.status})`;
42416
- try {
42417
- const parsed = await res.json();
42418
- msg = parsed.error?.message ?? msg;
42419
- } catch {
42420
- }
42421
- s.error(msg);
42422
- return false;
42423
- }
42424
- const data = await res.json();
42425
- await saveCredentials({
42426
- server_url: server,
42427
- user_id: data.user.id,
42428
- email: data.user.email,
42429
- org_id: data.user.org_id,
42430
- org_role: data.user.org_role,
42431
- access_token: data.access_token,
42432
- refresh_token: data.refresh_token,
42433
- access_expires_at: new Date(Date.now() + data.expires_in * 1e3).toISOString()
42434
- });
42435
- await client.setCredentials({
42436
- server_url: server,
42437
- user_id: data.user.id,
42438
- email: data.user.email,
42439
- org_id: data.user.org_id,
42440
- org_role: data.user.org_role,
42441
- access_token: data.access_token,
42442
- refresh_token: data.refresh_token,
42443
- access_expires_at: new Date(Date.now() + data.expires_in * 1e3).toISOString()
42444
- });
42445
- s.stop(`Logged in as ${data.user.email}`);
42676
+ await client.setCredentials(creds);
42677
+ s.stop(`Logged in as ${creds.email}`);
42446
42678
  return true;
42447
42679
  }
42448
42680
  var init_login = __esm({
42449
42681
  "src/ui/flows/login.ts"() {
42450
42682
  init_dist5();
42451
- init_http();
42452
- init_store();
42453
- init_version();
42454
- init_cancel();
42683
+ init_browser_auth();
42684
+ init_defaults();
42455
42685
  }
42456
42686
  });
42457
42687
 
@@ -42510,7 +42740,7 @@ async function addMemberInteractive(client, project) {
42510
42740
  {
42511
42741
  email: () => Re({
42512
42742
  message: "Email",
42513
- validate: (v2) => v2 && v2.includes("@") ? void 0 : "enter an email"
42743
+ validate: (v2) => v2?.includes("@") ? void 0 : "enter an email"
42514
42744
  }),
42515
42745
  role: () => Ee({
42516
42746
  message: "Role",
@@ -42609,8 +42839,11 @@ function printDetail(detail) {
42609
42839
  const envLines = detail.environments.map(
42610
42840
  (e2) => ` ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})`
42611
42841
  );
42612
- Se(`${detail.name} (${detail.id})
42613
- ${envLines.join("\n") || " (no environments)"}`, "Project");
42842
+ Se(
42843
+ `${detail.name} (${detail.id})
42844
+ ${envLines.join("\n") || " (no environments)"}`,
42845
+ "Project"
42846
+ );
42614
42847
  }
42615
42848
  async function createProjectInteractive(client) {
42616
42849
  const answers = await he(
@@ -42669,6 +42902,7 @@ async function runMenu() {
42669
42902
  ge(`keynv ${VERSION}`);
42670
42903
  const client = new ApiClient();
42671
42904
  await client.ensureHydrated();
42905
+ let didLogin = false;
42672
42906
  if (!client.isLoggedIn) {
42673
42907
  R2.info("Not logged in.");
42674
42908
  try {
@@ -42677,6 +42911,7 @@ async function runMenu() {
42677
42911
  ye("Login cancelled.");
42678
42912
  return 1;
42679
42913
  }
42914
+ didLogin = true;
42680
42915
  } catch (err) {
42681
42916
  if (err instanceof UserCancelled) {
42682
42917
  me("Login cancelled.");
@@ -42688,6 +42923,20 @@ async function runMenu() {
42688
42923
  const u = client.currentUser;
42689
42924
  if (u) R2.message(`${u.email} (${u.org_role}) @ ${u.server_url}`);
42690
42925
  }
42926
+ if (didLogin) {
42927
+ const root = findProjectRoot(process.cwd());
42928
+ const alreadyInitialized = root !== null && hasExistingKeynvEnv(root.path);
42929
+ if (!alreadyInitialized) {
42930
+ const setup = await ue({
42931
+ message: "Set up this project now?",
42932
+ initialValue: true
42933
+ });
42934
+ if (!q(setup) && setup) {
42935
+ const { runInitFlow: runInitFlow2 } = await Promise.resolve().then(() => (init_init(), init_exports));
42936
+ await runInitFlow2(client, { cwd: process.cwd(), dryRun: false, noScripts: false });
42937
+ }
42938
+ }
42939
+ }
42691
42940
  while (true) {
42692
42941
  let choice;
42693
42942
  try {
@@ -42754,15 +43003,16 @@ var init_menu = __esm({
42754
43003
  "src/ui/menu.ts"() {
42755
43004
  init_dist5();
42756
43005
  init_http();
43006
+ init_detect();
42757
43007
  init_store();
42758
- init_format();
42759
- init_cancel();
43008
+ init_version();
42760
43009
  init_audit2();
42761
43010
  init_login();
42762
43011
  init_member();
42763
43012
  init_project();
42764
43013
  init_secret();
42765
- init_version();
43014
+ init_format();
43015
+ init_cancel();
42766
43016
  }
42767
43017
  });
42768
43018
 
@@ -43443,11 +43693,11 @@ var reducers = {
43443
43693
  return { ...state, options: [{ name: `-c`, value: String(command) }] };
43444
43694
  }
43445
43695
  },
43446
- setError: (state, segment, segmentIndex, errorMessage) => {
43696
+ setError: (state, segment, segmentIndex, errorMessage2) => {
43447
43697
  if (segment === SpecialToken.EndOfInput || segment === SpecialToken.EndOfPartialInput) {
43448
- return { ...state, errorMessage: `${errorMessage}.` };
43698
+ return { ...state, errorMessage: `${errorMessage2}.` };
43449
43699
  } else {
43450
- return { ...state, errorMessage: `${errorMessage} ("${segment}").` };
43700
+ return { ...state, errorMessage: `${errorMessage2} ("${segment}").` };
43451
43701
  }
43452
43702
  },
43453
43703
  setOptionArityError: (state, segment) => {
@@ -44480,44 +44730,48 @@ var AuditListCommand = class extends Command {
44480
44730
  sinceId = options_exports.String("--since-id");
44481
44731
  json = options_exports.Boolean("--json", false);
44482
44732
  async execute() {
44483
- const client = new ApiClient();
44484
- const data = await client.request(
44485
- "/v1/audit",
44486
- {
44487
- query: {
44488
- event_type: this.eventType,
44489
- limit: this.limit,
44490
- since_id: this.sinceId
44733
+ try {
44734
+ const client = new ApiClient();
44735
+ const data = await client.request(
44736
+ "/v1/audit",
44737
+ {
44738
+ query: {
44739
+ event_type: this.eventType,
44740
+ limit: this.limit,
44741
+ since_id: this.sinceId
44742
+ }
44491
44743
  }
44492
- }
44493
- );
44494
- if (this.json) {
44495
- this.context.stdout.write(`${JSON.stringify(data, null, 2)}
44744
+ );
44745
+ if (this.json) {
44746
+ this.context.stdout.write(`${JSON.stringify(data, null, 2)}
44496
44747
  `);
44497
- return 0;
44498
- }
44499
- if (data.entries.length === 0) {
44500
- this.context.stdout.write("no audit entries\n");
44501
- return 0;
44502
- }
44503
- this.context.stdout.write(
44504
- `${table(
44505
- ["id", "ts", "actor", "agent", "event"],
44506
- data.entries.map((e2) => [
44507
- String(e2.id),
44508
- e2.ts,
44509
- e2.actor_user_id ?? "(none)",
44510
- e2.actor_agent,
44511
- e2.event_type
44512
- ])
44513
- )}
44748
+ return 0;
44749
+ }
44750
+ if (data.entries.length === 0) {
44751
+ this.context.stdout.write("no audit entries\n");
44752
+ return 0;
44753
+ }
44754
+ this.context.stdout.write(
44755
+ `${table(
44756
+ ["id", "ts", "actor", "agent", "event"],
44757
+ data.entries.map((e2) => [
44758
+ String(e2.id),
44759
+ e2.ts,
44760
+ e2.actor_user_id ?? "(none)",
44761
+ e2.actor_agent,
44762
+ e2.event_type
44763
+ ])
44764
+ )}
44514
44765
  `
44515
- );
44516
- if (data.next_cursor) {
44517
- this.context.stdout.write(`(next: --since-id ${data.next_cursor})
44766
+ );
44767
+ if (data.next_cursor) {
44768
+ this.context.stdout.write(`(next: --since-id ${data.next_cursor})
44518
44769
  `);
44770
+ }
44771
+ return 0;
44772
+ } catch (err) {
44773
+ return handleExecError(this.context.stderr, err);
44519
44774
  }
44520
- return 0;
44521
44775
  }
44522
44776
  };
44523
44777
  var AuditVerifyCommand = class extends Command {
@@ -44527,23 +44781,27 @@ var AuditVerifyCommand = class extends Command {
44527
44781
  });
44528
44782
  json = options_exports.Boolean("--json", false);
44529
44783
  async execute() {
44530
- const client = new ApiClient();
44531
- const data = await client.request("/v1/audit/verify", { method: "POST" });
44532
- if (this.json) {
44533
- this.context.stdout.write(`${JSON.stringify(data, null, 2)}
44784
+ try {
44785
+ const client = new ApiClient();
44786
+ const data = await client.request("/v1/audit/verify", { method: "POST" });
44787
+ if (this.json) {
44788
+ this.context.stdout.write(`${JSON.stringify(data, null, 2)}
44534
44789
  `);
44535
- return data.ok ? 0 : 1;
44536
- }
44537
- if (data.ok) {
44538
- this.context.stdout.write(`OK: ${data.checked} entries verified
44790
+ return data.ok ? 0 : 1;
44791
+ }
44792
+ if (data.ok) {
44793
+ this.context.stdout.write(`OK: ${data.checked} entries verified
44539
44794
  `);
44540
- return 0;
44541
- }
44542
- this.context.stdout.write(
44543
- `FAIL: chain broken at id ${data.broken_at_id} (${data.reason}); ${data.checked} entries verified before break
44795
+ return 0;
44796
+ }
44797
+ this.context.stdout.write(
44798
+ `FAIL: chain broken at id ${data.broken_at_id} (${data.reason}); ${data.checked} entries verified before break
44544
44799
  `
44545
- );
44546
- return 1;
44800
+ );
44801
+ return 1;
44802
+ } catch (err) {
44803
+ return handleExecError(this.context.stderr, err);
44804
+ }
44547
44805
  }
44548
44806
  };
44549
44807
 
@@ -44584,11 +44842,10 @@ async function resolveAllAliases(client, argv2, extraStrings = []) {
44584
44842
  return resolved;
44585
44843
  }
44586
44844
  function substitute(text, resolved) {
44845
+ const sorted = [...resolved].sort((a, b2) => b2.alias.literal.length - a.alias.literal.length);
44587
44846
  let out = text;
44588
- for (const r of resolved) {
44589
- if (out.includes(r.alias.literal)) {
44590
- out = out.split(r.alias.literal).join(r.value);
44591
- }
44847
+ for (const r of sorted) {
44848
+ out = out.split(r.alias.literal).join(r.value);
44592
44849
  }
44593
44850
  return out;
44594
44851
  }
@@ -44691,10 +44948,18 @@ var BUILTIN_LINE_PATTERNS = BUILTIN_PATTERNS.filter((p2) => !p2.multiline);
44691
44948
 
44692
44949
  // ../../packages/redactor/dist/entropy.js
44693
44950
  var TOKEN_BOUNDARY_RE = /[\s,;:'"<>(){}[\]=]+/;
44951
+ var DEFAULT_EXCLUDE_PREFIXES = [
44952
+ "sha1-",
44953
+ "sha256:",
44954
+ "sha256-",
44955
+ "sha384-",
44956
+ "sha512-"
44957
+ ];
44694
44958
  var DEFAULTS = {
44695
44959
  enabled: true,
44696
44960
  minLength: 24,
44697
- minBitsPerChar: 4.5
44961
+ minBitsPerChar: 4.5,
44962
+ excludePrefixes: DEFAULT_EXCLUDE_PREFIXES
44698
44963
  };
44699
44964
  function shannonEntropy(s) {
44700
44965
  if (s.length === 0)
@@ -44727,9 +44992,13 @@ function findEntropyMatches(text, opts = {}) {
44727
44992
  const end = i;
44728
44993
  const token = text.slice(start, end);
44729
44994
  if (token.length >= cfg.minLength) {
44730
- const h2 = shannonEntropy(token);
44731
- if (h2 >= cfg.minBitsPerChar) {
44732
- matches.push({ start, end, token });
44995
+ const lower = token.toLowerCase();
44996
+ const excluded = cfg.excludePrefixes.some((p2) => lower.startsWith(p2));
44997
+ if (!excluded) {
44998
+ const h2 = shannonEntropy(token);
44999
+ if (h2 >= cfg.minBitsPerChar) {
45000
+ matches.push({ start, end, token });
45001
+ }
44733
45002
  }
44734
45003
  }
44735
45004
  }
@@ -44873,7 +45142,19 @@ var ENV_ALLOWLIST = [
44873
45142
  "PWD",
44874
45143
  "OLDPWD",
44875
45144
  "TMPDIR",
44876
- "SSH_AUTH_SOCK"
45145
+ "SSH_AUTH_SOCK",
45146
+ // Windows-specific
45147
+ "USERPROFILE",
45148
+ "USERNAME",
45149
+ "COMPUTERNAME",
45150
+ "TEMP",
45151
+ "TMP",
45152
+ "SYSTEMROOT",
45153
+ "SYSTEMDRIVE",
45154
+ "WINDIR",
45155
+ "COMSPEC",
45156
+ "APPDATA",
45157
+ "LOCALAPPDATA"
44877
45158
  ];
44878
45159
  function spawnPrivileged(opts) {
44879
45160
  const startedAt = Date.now();
@@ -44886,7 +45167,32 @@ function spawnPrivileged(opts) {
44886
45167
  for (const [k2, v2] of Object.entries(opts.injectedEnv)) env2[k2] = v2;
44887
45168
  }
44888
45169
  const stdio = ["inherit", "pipe", "pipe"];
44889
- const child = spawn(opts.command, opts.args, {
45170
+ const WIN_BUILTINS = /* @__PURE__ */ new Set([
45171
+ "echo",
45172
+ "dir",
45173
+ "type",
45174
+ "copy",
45175
+ "del",
45176
+ "move",
45177
+ "ren",
45178
+ "md",
45179
+ "mkdir",
45180
+ "rd",
45181
+ "rmdir",
45182
+ "cd",
45183
+ "cls",
45184
+ "set",
45185
+ "pause",
45186
+ "find",
45187
+ "where"
45188
+ ]);
45189
+ let spawnCmd = opts.command;
45190
+ let spawnArgs = opts.args;
45191
+ if (process.platform === "win32" && WIN_BUILTINS.has(opts.command.toLowerCase())) {
45192
+ spawnCmd = process.env.COMSPEC ?? "cmd.exe";
45193
+ spawnArgs = ["/d", "/s", "/c", opts.command, ...opts.args];
45194
+ }
45195
+ const child = spawn(spawnCmd, spawnArgs, {
44890
45196
  env: env2,
44891
45197
  stdio,
44892
45198
  detached: false
@@ -44953,10 +45259,7 @@ to load a specific file, or set KEYNV_ENV_FILE in the environment.
44953
45259
  spelled \`--from\` to avoid the collision.)
44954
45260
  `,
44955
45261
  examples: [
44956
- [
44957
- "Auto-load .keynv.env from cwd or parents",
44958
- "$0 exec -- next dev"
44959
- ],
45262
+ ["Auto-load .keynv.env from cwd or parents", "$0 exec -- next dev"],
44960
45263
  [
44961
45264
  "Run mysql with the alias substituted at fork-exec time",
44962
45265
  "$0 exec -- mysql -p@billing.dev.db_pass -h db.example.com"
@@ -45020,7 +45323,18 @@ spelled \`--from\` to avoid the collision.)
45020
45323
  `);
45021
45324
  return 2;
45022
45325
  }
45023
- throw err;
45326
+ this.context.stderr.write(
45327
+ `keynv: unexpected error loading env file: ${err instanceof Error ? err.message : String(err)}
45328
+ `
45329
+ );
45330
+ return 2;
45331
+ }
45332
+ if (!envFileLoaded && !this.noEnvFile && !this.envFile && !process.env.KEYNV_ENV_FILE) {
45333
+ this.context.stderr.write(
45334
+ `keynv: no ${ENV_FILE_BASENAME} found in this directory or any parent.
45335
+ Run \`keynv init\` in your project root to migrate secrets and create one.
45336
+ `
45337
+ );
45024
45338
  }
45025
45339
  const viaEnvSpecs = [];
45026
45340
  for (const spec of this.viaEnv ?? []) {
@@ -45092,11 +45406,18 @@ spelled \`--from\` to avoid the collision.)
45092
45406
  injectedEnv[spec.name] = value;
45093
45407
  }
45094
45408
  if (envFileLoaded && !this.quiet) {
45095
- const total = envFileLoaded.entries.length;
45096
- const aliasCount = envFileLoaded.entries.filter((e2) => e2.isAlias).length;
45409
+ const aliasEntries = envFileLoaded.entries.filter((e2) => e2.isAlias);
45410
+ const plainEntries = envFileLoaded.entries.filter((e2) => !e2.isAlias);
45411
+ const displayPath = relative(process.cwd(), envFileLoaded.path) || envFileLoaded.path;
45412
+ const parts = [];
45413
+ if (aliasEntries.length > 0) {
45414
+ parts.push(aliasEntries.map((e2) => `${e2.name}=${e2.value}`).join(", ") + " (vault)");
45415
+ }
45416
+ if (plainEntries.length > 0) {
45417
+ parts.push(plainEntries.map((e2) => e2.name).join(", ") + " (plain)");
45418
+ }
45097
45419
  this.context.stderr.write(
45098
- `keynv: loaded ${total} var${total === 1 ? "" : "s"} from ${envFileLoaded.path} (${aliasCount} resolved from vault)
45099
- `
45420
+ `keynv: loaded ${displayPath}` + (parts.length > 0 ? ` \u2014 ${parts.join("; ")}` : "") + "\n"
45100
45421
  );
45101
45422
  }
45102
45423
  const timeoutS = this.timeout ? Number.parseInt(this.timeout, 10) : void 0;
@@ -45136,10 +45457,158 @@ function signalNumber(sig) {
45136
45457
  }
45137
45458
 
45138
45459
  // src/commands/init.ts
45460
+ init_dist();
45139
45461
  init_http();
45140
- init_tty();
45462
+ init_envFile();
45463
+ init_heuristics();
45141
45464
  init_init();
45142
45465
  init_cancel();
45466
+ init_tty();
45467
+
45468
+ // src/commands/project.ts
45469
+ init_http();
45470
+ init_format();
45471
+ function isProjectId(input) {
45472
+ return input.startsWith("p_") || /^[a-f0-9]{20,}$/i.test(input);
45473
+ }
45474
+ async function resolveProjectId(client, input) {
45475
+ if (isProjectId(input)) {
45476
+ return input;
45477
+ }
45478
+ const data = await client.request("/v1/projects");
45479
+ const match = data.projects.find((p2) => p2.name === input);
45480
+ if (!match) throw new Error(`project not found: ${input}`);
45481
+ return match.id;
45482
+ }
45483
+ var ProjectListCommand = class extends Command {
45484
+ static paths = [["project", "list"]];
45485
+ static usage = Command.Usage({
45486
+ description: "List projects visible to the current user."
45487
+ });
45488
+ json = options_exports.Boolean("--json", false);
45489
+ async execute() {
45490
+ try {
45491
+ const client = new ApiClient();
45492
+ const data = await client.request("/v1/projects");
45493
+ if (this.json) {
45494
+ this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45495
+ `);
45496
+ return 0;
45497
+ }
45498
+ if (data.projects.length === 0) {
45499
+ this.context.stdout.write("no projects\n");
45500
+ return 0;
45501
+ }
45502
+ this.context.stdout.write(
45503
+ `${table(
45504
+ ["name", "id", "created_at"],
45505
+ data.projects.map((p2) => [p2.name, p2.id, p2.created_at])
45506
+ )}
45507
+ `
45508
+ );
45509
+ return 0;
45510
+ } catch (err) {
45511
+ return handleExecError(this.context.stderr, err);
45512
+ }
45513
+ }
45514
+ };
45515
+ var ProjectCreateCommand = class extends Command {
45516
+ static paths = [["project", "create"]];
45517
+ static usage = Command.Usage({
45518
+ description: "Create a new project with one or more environments.",
45519
+ examples: [["Two envs", "$0 project create billing --env dev --env prod:production:approval"]]
45520
+ });
45521
+ name = options_exports.String();
45522
+ envs = options_exports.Array("--env", {
45523
+ description: 'Environment spec: name[:tier[:approval]]. Tier \u2208 {production,non-production}; "approval" sets require_approval=true.'
45524
+ });
45525
+ async execute() {
45526
+ try {
45527
+ const envs = (this.envs ?? ["dev"]).map((spec) => {
45528
+ const [name, tier, approval] = spec.split(":");
45529
+ return {
45530
+ name: name ?? "",
45531
+ tier: tier ?? "non-production",
45532
+ require_approval: approval === "approval"
45533
+ };
45534
+ });
45535
+ const client = new ApiClient();
45536
+ const result = await client.request("/v1/projects", {
45537
+ method: "POST",
45538
+ body: { name: this.name, environments: envs }
45539
+ });
45540
+ this.context.stdout.write(`created project ${result.name} (${result.id})
45541
+ `);
45542
+ for (const e2 of envs) {
45543
+ this.context.stdout.write(
45544
+ ` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
45545
+ `
45546
+ );
45547
+ }
45548
+ return 0;
45549
+ } catch (err) {
45550
+ return handleExecError(this.context.stderr, err);
45551
+ }
45552
+ }
45553
+ };
45554
+ var ProjectDescribeCommand = class extends Command {
45555
+ static paths = [["project", "describe"]];
45556
+ static usage = Command.Usage({
45557
+ description: "Show metadata for one project.",
45558
+ examples: [
45559
+ ["By ID", "$0 project describe p_go6rqgwz0wlokdsl55ikn"],
45560
+ ["By name", "$0 project describe myproject"]
45561
+ ]
45562
+ });
45563
+ id = options_exports.String();
45564
+ json = options_exports.Boolean("--json", false);
45565
+ async execute() {
45566
+ try {
45567
+ const client = new ApiClient();
45568
+ const projectId2 = await resolveProjectId(client, this.id);
45569
+ const data = await client.request(`/v1/projects/${projectId2}`);
45570
+ if (this.json) {
45571
+ this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45572
+ `);
45573
+ return 0;
45574
+ }
45575
+ this.context.stdout.write(`project: ${data.name} (${data.id})
45576
+ `);
45577
+ for (const e2 of data.environments) {
45578
+ this.context.stdout.write(
45579
+ ` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
45580
+ `
45581
+ );
45582
+ }
45583
+ return 0;
45584
+ } catch (err) {
45585
+ return handleExecError(this.context.stderr, err);
45586
+ }
45587
+ }
45588
+ };
45589
+ var ProjectDeleteCommand = class extends Command {
45590
+ static paths = [["project", "delete"]];
45591
+ static usage = Command.Usage({ description: "Soft-delete a project." });
45592
+ id = options_exports.String();
45593
+ force = options_exports.Boolean("--force", false);
45594
+ async execute() {
45595
+ if (!this.force) {
45596
+ this.context.stderr.write("keynv: refusing to delete without --force\n");
45597
+ return 2;
45598
+ }
45599
+ try {
45600
+ const client = new ApiClient();
45601
+ await client.request(`/v1/projects/${this.id}`, { method: "DELETE" });
45602
+ this.context.stdout.write(`deleted project ${this.id}
45603
+ `);
45604
+ return 0;
45605
+ } catch (err) {
45606
+ return handleExecError(this.context.stderr, err);
45607
+ }
45608
+ }
45609
+ };
45610
+
45611
+ // src/commands/init.ts
45143
45612
  var InitCommand = class extends Command {
45144
45613
  static paths = [["init"]];
45145
45614
  static usage = Command.Usage({
@@ -45153,6 +45622,11 @@ package.json scripts with \`keynv exec\`.
45153
45622
  Safe to re-run: existing .keynv.env entries are preserved; new
45154
45623
  entries are appended below a marker.
45155
45624
 
45625
+ --dry-run prints the secrets that would be uploaded and the alias
45626
+ mappings that would be written to .keynv.env, then exits without
45627
+ touching any files or making any network calls. Use it to preview
45628
+ what init will do before committing.
45629
+
45156
45630
  Requires an interactive terminal (clack TUI). For scripted
45157
45631
  migration, use the lower-level \`keynv project\` and \`keynv secret\`
45158
45632
  commands directly.
@@ -45160,28 +45634,47 @@ commands directly.
45160
45634
  examples: [
45161
45635
  ["Walk the current project", "$0 init"],
45162
45636
  ["Preview without writing or uploading", "$0 init --dry-run"],
45163
- ["Skip the package.json script-wrapping step", "$0 init --no-scripts"]
45637
+ ["Skip the package.json script-wrapping step", "$0 init --no-scripts"],
45638
+ ["Non-interactive (CI/CD)", "$0 init --env-file .env --project myproject --env dev"]
45164
45639
  ]
45165
45640
  });
45166
45641
  dryRun = options_exports.Boolean("--dry-run", false, {
45167
- description: "Show what would be done without writing files or uploading secrets."
45642
+ 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."
45168
45643
  });
45169
45644
  noScripts = options_exports.Boolean("--no-scripts", false, {
45170
45645
  description: "Skip the package.json script-wrapping step."
45171
45646
  });
45647
+ envFile = options_exports.String("--env-file", {
45648
+ description: "Path to .env file to migrate (non-interactive mode)."
45649
+ });
45650
+ project = options_exports.String("--project", {
45651
+ description: "Project name or ID (non-interactive mode)."
45652
+ });
45653
+ env = options_exports.String("--env", {
45654
+ description: "Environment name for --env-file (default: dev)."
45655
+ });
45656
+ secret = options_exports.Array("--secret", {
45657
+ description: "KEY=value secret to upload (non-interactive). Can be specified multiple times."
45658
+ });
45172
45659
  async execute() {
45173
- if (!isInteractive()) {
45174
- this.context.stderr.write(
45175
- "keynv init requires an interactive terminal. Use the lower-level commands (`keynv project`, `keynv secret`) for scripted setup.\n"
45176
- );
45177
- return 1;
45178
- }
45179
45660
  const client = new ApiClient();
45180
45661
  await client.ensureHydrated();
45181
45662
  if (!client.isLoggedIn) {
45182
45663
  this.context.stderr.write("keynv: not logged in. Run `keynv login` first.\n");
45183
45664
  return 1;
45184
45665
  }
45666
+ const hasEnvFile = this.envFile != null;
45667
+ const hasSecrets = this.secret != null && this.secret.length > 0;
45668
+ const isNonInteractive = hasEnvFile || hasSecrets;
45669
+ if (isNonInteractive) {
45670
+ return this.runNonInteractive(client);
45671
+ }
45672
+ if (!isInteractive()) {
45673
+ this.context.stderr.write(
45674
+ "keynv init requires an interactive terminal. Use --env-file or --secret for scripted setup.\n"
45675
+ );
45676
+ return 1;
45677
+ }
45185
45678
  try {
45186
45679
  const outcome = await runInitFlow(client, {
45187
45680
  cwd: process.cwd(),
@@ -45197,9 +45690,102 @@ commands directly.
45197
45690
  return 1;
45198
45691
  }
45199
45692
  }
45693
+ async runNonInteractive(client) {
45694
+ const projectName = this.project;
45695
+ if (!projectName) {
45696
+ this.context.stderr.write("keynv: --project is required in non-interactive mode.\n");
45697
+ return 1;
45698
+ }
45699
+ const resolved = await resolveProjectId(client, projectName);
45700
+ if (!resolved) {
45701
+ this.context.stderr.write(`keynv: project not found: ${projectName}
45702
+ `);
45703
+ return 1;
45704
+ }
45705
+ const projectId2 = resolved;
45706
+ const envName = this.env ?? "dev";
45707
+ const secrets = [];
45708
+ if (this.envFile) {
45709
+ try {
45710
+ const content = readFileSync(this.envFile, "utf8");
45711
+ const entries = parseEnvFile(content, this.envFile);
45712
+ for (const e2 of entries) {
45713
+ if (classifyEntry(e2.name, e2.value).verdict === "secret") {
45714
+ secrets.push({ name: e2.name, value: e2.value });
45715
+ }
45716
+ }
45717
+ } catch (err) {
45718
+ this.context.stderr.write(
45719
+ `keynv: cannot read ${this.envFile}: ${err instanceof Error ? err.message : String(err)}
45720
+ `
45721
+ );
45722
+ return 1;
45723
+ }
45724
+ }
45725
+ if (this.secret) {
45726
+ for (const spec of this.secret) {
45727
+ const eq = spec.indexOf("=");
45728
+ if (eq <= 0) {
45729
+ this.context.stderr.write(`keynv: invalid --secret '${spec}', expected KEY=value
45730
+ `);
45731
+ return 1;
45732
+ }
45733
+ const name = spec.slice(0, eq);
45734
+ const value = spec.slice(eq + 1);
45735
+ secrets.push({ name, value });
45736
+ }
45737
+ }
45738
+ if (secrets.length === 0) {
45739
+ this.context.stdout.write("keynv: nothing to migrate.\n");
45740
+ return 0;
45741
+ }
45742
+ if (this.dryRun) {
45743
+ this.context.stdout.write(
45744
+ `keynv: dry-run \u2014 would upload ${secrets.length} secret(s) to project ${projectName} (${projectId2}) in env ${envName}
45745
+ `
45746
+ );
45747
+ for (const { name } of secrets) {
45748
+ const aliasKey = name.toLowerCase().replace(/_/g, "-");
45749
+ this.context.stdout.write(` ${name}=@${projectName}.${envName}.${aliasKey}
45750
+ `);
45751
+ }
45752
+ return 0;
45753
+ }
45754
+ let uploaded = 0;
45755
+ const failed = [];
45756
+ for (const { name, value } of secrets) {
45757
+ const aliasKey = name.toLowerCase().replace(/_/g, "-");
45758
+ const alias2 = buildAlias({ project: projectName, environment: envName, key: aliasKey });
45759
+ if (!alias2) {
45760
+ failed.push(`${name} (invalid alias key: ${aliasKey})`);
45761
+ continue;
45762
+ }
45763
+ try {
45764
+ await client.request(`/v1/projects/${projectId2}/secrets`, {
45765
+ method: "POST",
45766
+ body: { env: envName, key: aliasKey, value }
45767
+ });
45768
+ uploaded++;
45769
+ } catch (err) {
45770
+ failed.push(`${name}: ${err instanceof Error ? err.message : String(err)}`);
45771
+ }
45772
+ }
45773
+ this.context.stdout.write(
45774
+ `keynv: uploaded ${uploaded}/${secrets.length} secret(s) to ${projectName}.${envName}
45775
+ `
45776
+ );
45777
+ if (failed.length > 0) {
45778
+ for (const f of failed) this.context.stderr.write(` failed: ${f}
45779
+ `);
45780
+ return 1;
45781
+ }
45782
+ return 0;
45783
+ }
45200
45784
  };
45201
45785
 
45202
45786
  // src/commands/login.ts
45787
+ init_browser_auth();
45788
+ init_defaults();
45203
45789
  init_http();
45204
45790
  init_store();
45205
45791
  init_format();
@@ -45260,25 +45846,63 @@ var LoginCommand = class extends Command {
45260
45846
  static usage = Command.Usage({
45261
45847
  description: "Authenticate against a keynv server.",
45262
45848
  examples: [
45263
- ["Interactive", "$0 login --server http://localhost:8080"],
45849
+ ["Browser login", "$0 login"],
45850
+ ["Self-hosted browser login", "$0 login --server http://localhost:8080"],
45851
+ ["Headless token login", "$0 login --server http://... --token kt_..."],
45264
45852
  ["Non-interactive", "$0 login --server http://... --email a@b.com --password ..."]
45265
45853
  ]
45266
45854
  });
45267
45855
  server = options_exports.String("--server", { description: "Server base URL." });
45856
+ token = options_exports.String("--token", { description: "CLI token for headless auth." });
45268
45857
  email = options_exports.String("--email", { description: "Email address." });
45269
45858
  password = options_exports.String("--password", {
45270
45859
  description: "Password (use stdin to avoid argv leak)."
45271
45860
  });
45272
45861
  async execute() {
45273
- const serverUrl = this.server ?? await promptLine("server URL [http://localhost:8080]: ") ?? "";
45274
- const finalServerUrl = serverUrl.length > 0 ? serverUrl : "http://localhost:8080";
45862
+ const finalServerUrl = this.server ?? DEFAULT_SERVER_URL;
45863
+ try {
45864
+ new URL(finalServerUrl);
45865
+ } catch {
45866
+ this.context.stderr.write(`keynv: invalid server URL '${finalServerUrl}'
45867
+ `);
45868
+ return 1;
45869
+ }
45870
+ if (this.token) {
45871
+ return this.loginWithToken(finalServerUrl, this.token);
45872
+ }
45873
+ if (!this.email && !this.password) {
45874
+ this.context.stdout.write(`Opening browser for ${finalServerUrl} ...
45875
+ `);
45876
+ try {
45877
+ const creds = await runBrowserAuth(finalServerUrl);
45878
+ await saveCredentials(creds);
45879
+ this.context.stdout.write(`logged in as ${creds.email} (${creds.org_role})
45880
+ `);
45881
+ return 0;
45882
+ } catch (err) {
45883
+ this.context.stderr.write(
45884
+ `keynv: ${err instanceof Error ? err.message : "browser login failed"}
45885
+ `
45886
+ );
45887
+ return 1;
45888
+ }
45889
+ }
45275
45890
  const email2 = this.email ?? await promptLine("email: ");
45276
45891
  const password = this.password ?? await promptHidden("password: ");
45277
- const res = await fetch(new URL("/v1/auth/login", finalServerUrl).toString(), {
45278
- method: "POST",
45279
- headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
45280
- body: JSON.stringify({ email: email2, password })
45281
- });
45892
+ let res;
45893
+ try {
45894
+ res = await fetch(new URL("/v1/auth/login", finalServerUrl).toString(), {
45895
+ method: "POST",
45896
+ headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
45897
+ body: JSON.stringify({ email: email2, password })
45898
+ });
45899
+ } catch (err) {
45900
+ this.context.stderr.write(
45901
+ `keynv: cannot reach server '${finalServerUrl}' \u2014 ${err instanceof Error ? err.message : String(err)}
45902
+ `
45903
+ );
45904
+ return 1;
45905
+ }
45282
45906
  if (!res.ok) {
45283
45907
  const text = await res.text();
45284
45908
  let msg = `login failed (${res.status})`;
@@ -45302,6 +45926,7 @@ var LoginCommand = class extends Command {
45302
45926
  const data = await res.json();
45303
45927
  try {
45304
45928
  await saveCredentials({
45929
+ auth_kind: "session",
45305
45930
  server_url: finalServerUrl,
45306
45931
  user_id: data.user.id,
45307
45932
  email: data.user.email,
@@ -45319,6 +45944,63 @@ var LoginCommand = class extends Command {
45319
45944
  return 1;
45320
45945
  }
45321
45946
  this.context.stdout.write(`logged in as ${data.user.email} (${data.user.org_role})
45947
+ `);
45948
+ return 0;
45949
+ }
45950
+ async loginWithToken(serverUrl, token) {
45951
+ let res;
45952
+ try {
45953
+ res = await fetch(new URL("/v1/whoami", serverUrl).toString(), {
45954
+ headers: { authorization: `Bearer ${token}`, "x-keynv-agent": AGENT }
45955
+ });
45956
+ } catch (err) {
45957
+ this.context.stderr.write(
45958
+ `keynv: cannot reach server '${serverUrl}' \u2014 ${err instanceof Error ? err.message : String(err)}
45959
+ `
45960
+ );
45961
+ return 1;
45962
+ }
45963
+ if (!res.ok) {
45964
+ const text = await res.text();
45965
+ let msg = `token login failed (${res.status})`;
45966
+ try {
45967
+ const parsed = JSON.parse(text);
45968
+ msg = parsed.error?.message ?? msg;
45969
+ this.context.stderr.write(
45970
+ `${fmtError({
45971
+ status: res.status,
45972
+ ...parsed.error?.code ? { code: parsed.error.code } : {},
45973
+ message: msg
45974
+ })}
45975
+ `
45976
+ );
45977
+ } catch {
45978
+ this.context.stderr.write(`keynv: ${msg}
45979
+ `);
45980
+ }
45981
+ return 1;
45982
+ }
45983
+ const data = await res.json();
45984
+ try {
45985
+ await saveCredentials({
45986
+ auth_kind: "cli_token",
45987
+ server_url: serverUrl,
45988
+ user_id: data.id,
45989
+ email: data.email,
45990
+ org_id: data.org_id,
45991
+ org_role: data.org_role,
45992
+ access_token: token,
45993
+ refresh_token: "",
45994
+ access_expires_at: "9999-12-31T23:59:59.999Z"
45995
+ });
45996
+ } catch (err) {
45997
+ this.context.stderr.write(
45998
+ `keynv: failed to persist credentials: ${err instanceof Error ? err.message : String(err)}
45999
+ `
46000
+ );
46001
+ return 1;
46002
+ }
46003
+ this.context.stdout.write(`logged in as ${data.email} (${data.org_role})
45322
46004
  `);
45323
46005
  return 0;
45324
46006
  }
@@ -45355,48 +46037,46 @@ var WhoamiCommand = class extends Command {
45355
46037
  });
45356
46038
  json = options_exports.Boolean("--json", false);
45357
46039
  async execute() {
45358
- const client = new ApiClient();
45359
- await client.ensureHydrated();
45360
- if (!client.isLoggedIn) {
45361
- this.context.stderr.write("keynv: not logged in. Run `keynv login`.\n");
45362
- return 1;
45363
- }
45364
- const data = await client.request("/v1/whoami");
45365
- if (this.json) {
45366
- this.context.stdout.write(`${JSON.stringify(data, null, 2)}
46040
+ try {
46041
+ const client = new ApiClient();
46042
+ await client.ensureHydrated();
46043
+ if (!client.isLoggedIn) {
46044
+ this.context.stderr.write("keynv: not logged in. Run `keynv login`.\n");
46045
+ return 1;
46046
+ }
46047
+ const data = await client.request("/v1/whoami");
46048
+ if (this.json) {
46049
+ this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45367
46050
  `);
45368
- return 0;
45369
- }
45370
- this.context.stdout.write(`user: ${data.email} (${data.id})
46051
+ return 0;
46052
+ }
46053
+ this.context.stdout.write(`user: ${data.email} (${data.id})
45371
46054
  `);
45372
- this.context.stdout.write(`org: ${data.org_id}
46055
+ const orgDisplay = data.org_name ? `${data.org_name} (${data.org_id})` : data.org_id;
46056
+ this.context.stdout.write(`org: ${orgDisplay}
45373
46057
  `);
45374
- this.context.stdout.write(`role: ${data.org_role}
46058
+ this.context.stdout.write(`role: ${data.org_role}
45375
46059
  `);
45376
- if (data.memberships.length === 0) {
45377
- this.context.stdout.write("memberships: (none)\n");
45378
- } else {
45379
- this.context.stdout.write("memberships:\n");
45380
- for (const m of data.memberships) {
45381
- this.context.stdout.write(` ${m.project_id}: ${m.role}
46060
+ if (data.memberships.length === 0) {
46061
+ this.context.stdout.write("memberships: (none)\n");
46062
+ } else {
46063
+ this.context.stdout.write("memberships:\n");
46064
+ for (const m of data.memberships) {
46065
+ const proj = m.project_name ? `${m.project_name} (${m.project_id})` : m.project_id;
46066
+ this.context.stdout.write(` ${proj}: ${m.role}
45382
46067
  `);
46068
+ }
45383
46069
  }
46070
+ return 0;
46071
+ } catch (err) {
46072
+ return handleExecError(this.context.stderr, err);
45384
46073
  }
45385
- return 0;
45386
46074
  }
45387
46075
  };
45388
46076
 
45389
46077
  // src/commands/member.ts
45390
46078
  init_http();
45391
46079
  init_format();
45392
- async function findProjectIdByName(client, name) {
45393
- const data = await client.request(
45394
- "/v1/projects"
45395
- );
45396
- const match = data.projects.find((p2) => p2.name === name);
45397
- if (!match) throw new Error(`unknown project: ${name}`);
45398
- return match.id;
45399
- }
45400
46080
  var MemberAddCommand = class extends Command {
45401
46081
  static paths = [["member", "add"]];
45402
46082
  static usage = Command.Usage({
@@ -45412,15 +46092,19 @@ var MemberAddCommand = class extends Command {
45412
46092
  this.context.stderr.write("keynv: --role must be one of lead, developer, reader\n");
45413
46093
  return 1;
45414
46094
  }
45415
- const client = new ApiClient();
45416
- const projectId2 = await findProjectIdByName(client, this.project);
45417
- await client.request(`/v1/projects/${projectId2}/members`, {
45418
- method: "POST",
45419
- body: { email: this.email, role: role2 }
45420
- });
45421
- this.context.stdout.write(`added ${this.email} to ${this.project} as ${role2}
46095
+ try {
46096
+ const client = new ApiClient();
46097
+ const projectId2 = await resolveProjectId(client, this.project);
46098
+ await client.request(`/v1/projects/${projectId2}/members`, {
46099
+ method: "POST",
46100
+ body: { email: this.email, role: role2 }
46101
+ });
46102
+ this.context.stdout.write(`added ${this.email} to ${this.project} as ${role2}
45422
46103
  `);
45423
- return 0;
46104
+ return 0;
46105
+ } catch (err) {
46106
+ return handleExecError(this.context.stderr, err);
46107
+ }
45424
46108
  }
45425
46109
  };
45426
46110
  var MemberRemoveCommand = class extends Command {
@@ -45429,21 +46113,25 @@ var MemberRemoveCommand = class extends Command {
45429
46113
  project = options_exports.String();
45430
46114
  email = options_exports.String();
45431
46115
  async execute() {
45432
- const client = new ApiClient();
45433
- const projectId2 = await findProjectIdByName(client, this.project);
45434
- const members = await client.request(`/v1/projects/${projectId2}/members`);
45435
- const target = members.members.find((m) => m.email === this.email);
45436
- if (!target) {
45437
- this.context.stderr.write(`keynv: ${this.email} is not a member of ${this.project}
46116
+ try {
46117
+ const client = new ApiClient();
46118
+ const projectId2 = await resolveProjectId(client, this.project);
46119
+ const members = await client.request(`/v1/projects/${projectId2}/members`);
46120
+ const target = members.members.find((m) => m.email === this.email);
46121
+ if (!target) {
46122
+ this.context.stderr.write(`keynv: ${this.email} is not a member of ${this.project}
45438
46123
  `);
45439
- return 1;
45440
- }
45441
- await client.request(`/v1/projects/${projectId2}/members/${target.user_id}`, {
45442
- method: "DELETE"
45443
- });
45444
- this.context.stdout.write(`removed ${this.email} from ${this.project}
46124
+ return 1;
46125
+ }
46126
+ await client.request(`/v1/projects/${projectId2}/members/${target.user_id}`, {
46127
+ method: "DELETE"
46128
+ });
46129
+ this.context.stdout.write(`removed ${this.email} from ${this.project}
45445
46130
  `);
45446
- return 0;
46131
+ return 0;
46132
+ } catch (err) {
46133
+ return handleExecError(this.context.stderr, err);
46134
+ }
45447
46135
  }
45448
46136
  };
45449
46137
  var MemberListCommand = class extends Command {
@@ -45452,134 +46140,30 @@ var MemberListCommand = class extends Command {
45452
46140
  project = options_exports.String();
45453
46141
  json = options_exports.Boolean("--json", false);
45454
46142
  async execute() {
45455
- const client = new ApiClient();
45456
- const projectId2 = await findProjectIdByName(client, this.project);
45457
- const data = await client.request(`/v1/projects/${projectId2}/members`);
45458
- if (this.json) {
45459
- this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45460
- `);
45461
- return 0;
45462
- }
45463
- if (data.members.length === 0) {
45464
- this.context.stdout.write("no members\n");
45465
- return 0;
45466
- }
45467
- this.context.stdout.write(
45468
- `${table(
45469
- ["email", "role", "granted_at"],
45470
- data.members.map((m) => [m.email, m.role, m.granted_at])
45471
- )}
45472
- `
45473
- );
45474
- return 0;
45475
- }
45476
- };
45477
-
45478
- // src/commands/project.ts
45479
- init_http();
45480
- init_format();
45481
- var ProjectListCommand = class extends Command {
45482
- static paths = [["project", "list"]];
45483
- static usage = Command.Usage({
45484
- description: "List projects visible to the current user."
45485
- });
45486
- json = options_exports.Boolean("--json", false);
45487
- async execute() {
45488
- const client = new ApiClient();
45489
- const data = await client.request("/v1/projects");
45490
- if (this.json) {
45491
- this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45492
- `);
45493
- return 0;
45494
- }
45495
- if (data.projects.length === 0) {
45496
- this.context.stdout.write("no projects\n");
45497
- return 0;
45498
- }
45499
- this.context.stdout.write(
45500
- `${table(
45501
- ["name", "id", "created_at"],
45502
- data.projects.map((p2) => [p2.name, p2.id, p2.created_at])
45503
- )}
45504
- `
45505
- );
45506
- return 0;
45507
- }
45508
- };
45509
- var ProjectCreateCommand = class extends Command {
45510
- static paths = [["project", "create"]];
45511
- static usage = Command.Usage({
45512
- description: "Create a new project with one or more environments.",
45513
- examples: [["Two envs", "$0 project create billing --env dev --env prod:production:approval"]]
45514
- });
45515
- name = options_exports.String();
45516
- envs = options_exports.Array("--env", {
45517
- description: 'Environment spec: name[:tier[:approval]]. Tier \u2208 {production,non-production}; "approval" sets require_approval=true.'
45518
- });
45519
- async execute() {
45520
- const envs = (this.envs ?? ["dev"]).map((spec) => {
45521
- const [name, tier, approval] = spec.split(":");
45522
- return {
45523
- name: name ?? "",
45524
- tier: tier ?? "non-production",
45525
- require_approval: approval === "approval"
45526
- };
45527
- });
45528
- const client = new ApiClient();
45529
- const result = await client.request("/v1/projects", {
45530
- method: "POST",
45531
- body: { name: this.name, environments: envs }
45532
- });
45533
- this.context.stdout.write(`created project ${result.name} (${result.id})
46143
+ try {
46144
+ const client = new ApiClient();
46145
+ const projectId2 = await resolveProjectId(client, this.project);
46146
+ const data = await client.request(`/v1/projects/${projectId2}/members`);
46147
+ if (this.json) {
46148
+ this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45534
46149
  `);
45535
- for (const e2 of envs) {
46150
+ return 0;
46151
+ }
46152
+ if (data.members.length === 0) {
46153
+ this.context.stdout.write("no members\n");
46154
+ return 0;
46155
+ }
45536
46156
  this.context.stdout.write(
45537
- ` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
46157
+ `${table(
46158
+ ["email", "role", "granted_at"],
46159
+ data.members.map((m) => [m.email, m.role, m.granted_at])
46160
+ )}
45538
46161
  `
45539
46162
  );
45540
- }
45541
- return 0;
45542
- }
45543
- };
45544
- var ProjectDescribeCommand = class extends Command {
45545
- static paths = [["project", "describe"]];
45546
- static usage = Command.Usage({ description: "Show metadata for one project." });
45547
- id = options_exports.String();
45548
- json = options_exports.Boolean("--json", false);
45549
- async execute() {
45550
- const client = new ApiClient();
45551
- const data = await client.request(`/v1/projects/${this.id}`);
45552
- if (this.json) {
45553
- this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45554
- `);
45555
46163
  return 0;
46164
+ } catch (err) {
46165
+ return handleExecError(this.context.stderr, err);
45556
46166
  }
45557
- this.context.stdout.write(`project: ${data.name} (${data.id})
45558
- `);
45559
- for (const e2 of data.environments) {
45560
- this.context.stdout.write(
45561
- ` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
45562
- `
45563
- );
45564
- }
45565
- return 0;
45566
- }
45567
- };
45568
- var ProjectDeleteCommand = class extends Command {
45569
- static paths = [["project", "delete"]];
45570
- static usage = Command.Usage({ description: "Soft-delete a project." });
45571
- id = options_exports.String();
45572
- force = options_exports.Boolean("--force", false);
45573
- async execute() {
45574
- if (!this.force) {
45575
- this.context.stderr.write("keynv: refusing to delete without --force\n");
45576
- return 2;
45577
- }
45578
- const client = new ApiClient();
45579
- await client.request(`/v1/projects/${this.id}`, { method: "DELETE" });
45580
- this.context.stdout.write(`deleted project ${this.id}
45581
- `);
45582
- return 0;
45583
46167
  }
45584
46168
  };
45585
46169
  var RedactCommand = class extends Command {
@@ -45624,11 +46208,9 @@ var RedactStreamCommand = class extends Command {
45624
46208
  static usage = Command.Usage({
45625
46209
  description: "Stream stdin through the line-buffered redactor to stdout.",
45626
46210
  details: `
45627
- Used as a hook handler by per-agent integrations (e.g., Claude Code's
45628
- PostToolUse hook): \`keynv redact-stream\` reads its tool output on
45629
- stdin and writes a redacted version on stdout, preserving the original
45630
- line structure. Multi-line patterns are NOT applied here (see the
45631
- streaming-mode limitation in @keynv/redactor).
46211
+ Pipe any output through the line-buffered redactor to stdout.
46212
+ Preserves original line structure. Multi-line patterns are NOT applied
46213
+ here (see the streaming-mode limitation in the redactor package).
45632
46214
  `
45633
46215
  });
45634
46216
  async execute() {
@@ -45643,18 +46225,13 @@ streaming-mode limitation in @keynv/redactor).
45643
46225
  // src/commands/secret.ts
45644
46226
  init_dist();
45645
46227
  init_http();
46228
+ init_secret();
45646
46229
  init_format();
45647
- init_tty();
45648
46230
  init_cancel();
45649
46231
  init_pickProject();
45650
46232
  init_pickSecret();
45651
- init_secret();
45652
- async function findProjectIdByName2(client, name) {
45653
- const data = await client.request("/v1/projects");
45654
- const match = data.projects.find((p2) => p2.name === name);
45655
- if (!match) throw new Error(`unknown project: ${name}`);
45656
- return match.id;
45657
- }
46233
+ init_tty();
46234
+ var ALIAS_FORMAT_HINT = "Format: @<project>.<env>.<KEY> (project/env: lowercase kebab-case; key: letters, digits, _ or -)";
45658
46235
  function missingAlias(stderr) {
45659
46236
  stderr.write("keynv: missing <alias> (TTY required for interactive prompt).\n");
45660
46237
  return 1;
@@ -45685,45 +46262,50 @@ var SecretCreateCommand = class extends Command {
45685
46262
  });
45686
46263
  stdin = options_exports.Boolean("--stdin", false);
45687
46264
  async execute() {
45688
- const client = new ApiClient();
45689
- await client.ensureHydrated();
45690
- let alias2 = this.alias;
45691
- let value = this.value;
45692
- if (!alias2) {
45693
- if (!isInteractive()) return missingAlias(this.context.stderr);
45694
- try {
45695
- const built = await promptNewSecret(client);
45696
- if (!built) return 1;
45697
- alias2 = built.alias;
45698
- value = built.value;
45699
- } catch (err) {
45700
- if (err instanceof UserCancelled) return 130;
45701
- throw err;
46265
+ try {
46266
+ const client = new ApiClient();
46267
+ await client.ensureHydrated();
46268
+ let alias2 = this.alias;
46269
+ let value = this.value;
46270
+ if (!alias2) {
46271
+ if (!isInteractive()) return missingAlias(this.context.stderr);
46272
+ try {
46273
+ const built = await promptNewSecret(client);
46274
+ if (!built) return 1;
46275
+ alias2 = built.alias;
46276
+ value = built.value;
46277
+ } catch (err) {
46278
+ if (err instanceof UserCancelled) return 130;
46279
+ throw err;
46280
+ }
45702
46281
  }
45703
- }
45704
- const parsed = parseAlias(alias2);
45705
- if (!parsed) {
45706
- this.context.stderr.write(`keynv: invalid alias '${alias2}'. Expected @project.env.key.
46282
+ const parsed = parseAlias(alias2);
46283
+ if (!parsed) {
46284
+ this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46285
+ ${ALIAS_FORMAT_HINT}
45707
46286
  `);
45708
- return 1;
45709
- }
45710
- if (this.stdin) {
45711
- const chunks = [];
45712
- for await (const chunk of this.context.stdin) {
45713
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
46287
+ return 1;
45714
46288
  }
45715
- value = Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
45716
- } else if (value === void 0) {
45717
- value = await promptHidden("value: ");
45718
- }
45719
- const projectId2 = await findProjectIdByName2(client, parsed.project);
45720
- await client.request(`/v1/projects/${projectId2}/secrets`, {
45721
- method: "POST",
45722
- body: { env: parsed.environment, key: parsed.key, value }
45723
- });
45724
- this.context.stdout.write(`created ${parsed.literal}
46289
+ if (this.stdin) {
46290
+ const chunks = [];
46291
+ for await (const chunk of this.context.stdin) {
46292
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
46293
+ }
46294
+ value = Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
46295
+ } else if (value === void 0) {
46296
+ value = await promptHidden("value: ");
46297
+ }
46298
+ const projectId2 = await resolveProjectId(client, parsed.project);
46299
+ await client.request(`/v1/projects/${projectId2}/secrets`, {
46300
+ method: "POST",
46301
+ body: { env: parsed.environment, key: parsed.key, value }
46302
+ });
46303
+ this.context.stdout.write(`created ${parsed.literal}
45725
46304
  `);
45726
- return 0;
46305
+ return 0;
46306
+ } catch (err) {
46307
+ return handleExecError(this.context.stderr, err);
46308
+ }
45727
46309
  }
45728
46310
  };
45729
46311
  var SecretGetCommand = class extends Command {
@@ -45734,39 +46316,44 @@ var SecretGetCommand = class extends Command {
45734
46316
  alias = options_exports.String({ required: false });
45735
46317
  json = options_exports.Boolean("--json", false);
45736
46318
  async execute() {
45737
- const client = new ApiClient();
45738
- await client.ensureHydrated();
45739
- let alias2 = this.alias;
45740
- if (!alias2) {
45741
- if (!isInteractive()) return missingAlias(this.context.stderr);
45742
- try {
45743
- alias2 = await pickAliasInteractive(client) ?? void 0;
45744
- } catch (err) {
45745
- if (err instanceof UserCancelled) return 130;
45746
- throw err;
46319
+ try {
46320
+ const client = new ApiClient();
46321
+ await client.ensureHydrated();
46322
+ let alias2 = this.alias;
46323
+ if (!alias2) {
46324
+ if (!isInteractive()) return missingAlias(this.context.stderr);
46325
+ try {
46326
+ alias2 = await pickAliasInteractive(client) ?? void 0;
46327
+ } catch (err) {
46328
+ if (err instanceof UserCancelled) return 130;
46329
+ throw err;
46330
+ }
46331
+ if (!alias2) return 1;
45747
46332
  }
45748
- if (!alias2) return 1;
45749
- }
45750
- const parsed = parseAlias(alias2);
45751
- if (!parsed) {
45752
- this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46333
+ const parsed = parseAlias(alias2);
46334
+ if (!parsed) {
46335
+ this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46336
+ ${ALIAS_FORMAT_HINT}
45753
46337
  `);
45754
- return 1;
45755
- }
45756
- const projectId2 = await findProjectIdByName2(client, parsed.project);
45757
- const data = await client.request(
45758
- `/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}`
45759
- );
45760
- if (this.json) {
45761
- this.context.stdout.write(
45762
- `${JSON.stringify({ alias: data.alias, version: data.version, value: data.value })}
45763
- `
46338
+ return 1;
46339
+ }
46340
+ const projectId2 = await resolveProjectId(client, parsed.project);
46341
+ const data = await client.request(
46342
+ `/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}`
45764
46343
  );
45765
- } else {
45766
- this.context.stdout.write(`${data.value}
46344
+ if (this.json) {
46345
+ this.context.stdout.write(
46346
+ `${JSON.stringify({ alias: data.alias, version: data.version, value: data.value })}
46347
+ `
46348
+ );
46349
+ } else {
46350
+ this.context.stdout.write(`${data.value}
45767
46351
  `);
46352
+ }
46353
+ return 0;
46354
+ } catch (err) {
46355
+ return handleExecError(this.context.stderr, err);
45768
46356
  }
45769
- return 0;
45770
46357
  }
45771
46358
  };
45772
46359
  var SecretListCommand = class extends Command {
@@ -45777,42 +46364,47 @@ var SecretListCommand = class extends Command {
45777
46364
  project = options_exports.String({ required: false });
45778
46365
  json = options_exports.Boolean("--json", false);
45779
46366
  async execute() {
45780
- const client = new ApiClient();
45781
- await client.ensureHydrated();
45782
- let projectName = this.project;
45783
- if (!projectName) {
45784
- if (!isInteractive()) {
45785
- this.context.stderr.write("keynv: missing <project>.\n");
45786
- return 1;
45787
- }
45788
- try {
45789
- const picked = await pickProject(client, "Project");
45790
- if (!picked) return 1;
45791
- projectName = picked.name;
45792
- } catch (err) {
45793
- if (err instanceof UserCancelled) return 130;
45794
- throw err;
46367
+ try {
46368
+ const client = new ApiClient();
46369
+ await client.ensureHydrated();
46370
+ let projectName = this.project;
46371
+ if (!projectName) {
46372
+ if (!isInteractive()) {
46373
+ this.context.stderr.write("keynv: missing <project>.\n");
46374
+ return 1;
46375
+ }
46376
+ try {
46377
+ const picked = await pickProject(client, "Project");
46378
+ if (!picked) return 1;
46379
+ projectName = picked.name;
46380
+ } catch (err) {
46381
+ if (err instanceof UserCancelled) return 130;
46382
+ throw err;
46383
+ }
45795
46384
  }
45796
- }
45797
- const projectId2 = await findProjectIdByName2(client, projectName);
45798
- const data = await client.request(`/v1/projects/${projectId2}/secrets`);
45799
- if (this.json) {
45800
- this.context.stdout.write(`${JSON.stringify(data, null, 2)}
46385
+ const resolvedProjectName = projectName.startsWith("@") ? projectName.slice(1).split(".")[0] ?? projectName : projectName;
46386
+ const projectId2 = await resolveProjectId(client, resolvedProjectName);
46387
+ const data = await client.request(`/v1/projects/${projectId2}/secrets`);
46388
+ if (this.json) {
46389
+ this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45801
46390
  `);
46391
+ return 0;
46392
+ }
46393
+ if (data.secrets.length === 0) {
46394
+ this.context.stdout.write("no secrets\n");
46395
+ return 0;
46396
+ }
46397
+ this.context.stdout.write(
46398
+ `${table(
46399
+ ["alias", "version", "created_at"],
46400
+ data.secrets.map((s) => [s.alias, String(s.version), s.created_at])
46401
+ )}
46402
+ `
46403
+ );
45802
46404
  return 0;
46405
+ } catch (err) {
46406
+ return handleExecError(this.context.stderr, err);
45803
46407
  }
45804
- if (data.secrets.length === 0) {
45805
- this.context.stdout.write("no secrets\n");
45806
- return 0;
45807
- }
45808
- this.context.stdout.write(
45809
- `${table(
45810
- ["alias", "version", "created_at"],
45811
- data.secrets.map((s) => [s.alias, String(s.version), s.created_at])
45812
- )}
45813
- `
45814
- );
45815
- return 0;
45816
46408
  }
45817
46409
  };
45818
46410
  var SecretRotateCommand = class extends Command {
@@ -45822,45 +46414,50 @@ var SecretRotateCommand = class extends Command {
45822
46414
  value = options_exports.String("--value");
45823
46415
  stdin = options_exports.Boolean("--stdin", false);
45824
46416
  async execute() {
45825
- const client = new ApiClient();
45826
- await client.ensureHydrated();
45827
- let alias2 = this.alias;
45828
- if (!alias2) {
45829
- if (!isInteractive()) return missingAlias(this.context.stderr);
45830
- try {
45831
- alias2 = await pickAliasInteractive(client) ?? void 0;
45832
- } catch (err) {
45833
- if (err instanceof UserCancelled) return 130;
45834
- throw err;
46417
+ try {
46418
+ const client = new ApiClient();
46419
+ await client.ensureHydrated();
46420
+ let alias2 = this.alias;
46421
+ if (!alias2) {
46422
+ if (!isInteractive()) return missingAlias(this.context.stderr);
46423
+ try {
46424
+ alias2 = await pickAliasInteractive(client) ?? void 0;
46425
+ } catch (err) {
46426
+ if (err instanceof UserCancelled) return 130;
46427
+ throw err;
46428
+ }
46429
+ if (!alias2) return 1;
45835
46430
  }
45836
- if (!alias2) return 1;
45837
- }
45838
- const parsed = parseAlias(alias2);
45839
- if (!parsed) {
45840
- this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46431
+ const parsed = parseAlias(alias2);
46432
+ if (!parsed) {
46433
+ this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46434
+ ${ALIAS_FORMAT_HINT}
45841
46435
  `);
45842
- return 1;
45843
- }
45844
- let value;
45845
- if (this.stdin) {
45846
- const chunks = [];
45847
- for await (const chunk of this.context.stdin) {
45848
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
46436
+ return 1;
45849
46437
  }
45850
- value = Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
45851
- } else if (this.value !== void 0) {
45852
- value = this.value;
45853
- } else {
45854
- value = await promptHidden("new value: ");
45855
- }
45856
- const projectId2 = await findProjectIdByName2(client, parsed.project);
45857
- const data = await client.request(
45858
- `/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}/rotate`,
45859
- { method: "POST", body: { new_value: value } }
45860
- );
45861
- this.context.stdout.write(`rotated ${data.alias} \u2192 v${data.version}
46438
+ let value;
46439
+ if (this.stdin) {
46440
+ const chunks = [];
46441
+ for await (const chunk of this.context.stdin) {
46442
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
46443
+ }
46444
+ value = Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
46445
+ } else if (this.value !== void 0) {
46446
+ value = this.value;
46447
+ } else {
46448
+ value = await promptHidden("new value: ");
46449
+ }
46450
+ const projectId2 = await resolveProjectId(client, parsed.project);
46451
+ const data = await client.request(
46452
+ `/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}/rotate`,
46453
+ { method: "POST", body: { new_value: value } }
46454
+ );
46455
+ this.context.stdout.write(`rotated ${data.alias} \u2192 v${data.version}
45862
46456
  `);
45863
- return 0;
46457
+ return 0;
46458
+ } catch (err) {
46459
+ return handleExecError(this.context.stderr, err);
46460
+ }
45864
46461
  }
45865
46462
  };
45866
46463
  var SecretDeleteCommand = class extends Command {
@@ -45868,31 +46465,170 @@ var SecretDeleteCommand = class extends Command {
45868
46465
  static usage = Command.Usage({ description: "Soft-delete a secret." });
45869
46466
  alias = options_exports.String({ required: false });
45870
46467
  async execute() {
45871
- const client = new ApiClient();
45872
- await client.ensureHydrated();
45873
- let alias2 = this.alias;
45874
- if (!alias2) {
45875
- if (!isInteractive()) return missingAlias(this.context.stderr);
45876
- try {
45877
- alias2 = await pickAliasInteractive(client) ?? void 0;
45878
- } catch (err) {
45879
- if (err instanceof UserCancelled) return 130;
45880
- throw err;
46468
+ try {
46469
+ const client = new ApiClient();
46470
+ await client.ensureHydrated();
46471
+ let alias2 = this.alias;
46472
+ if (!alias2) {
46473
+ if (!isInteractive()) return missingAlias(this.context.stderr);
46474
+ try {
46475
+ alias2 = await pickAliasInteractive(client) ?? void 0;
46476
+ } catch (err) {
46477
+ if (err instanceof UserCancelled) return 130;
46478
+ throw err;
46479
+ }
46480
+ if (!alias2) return 1;
45881
46481
  }
45882
- if (!alias2) return 1;
45883
- }
45884
- const parsed = parseAlias(alias2);
45885
- if (!parsed) {
45886
- this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46482
+ const parsed = parseAlias(alias2);
46483
+ if (!parsed) {
46484
+ this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46485
+ ${ALIAS_FORMAT_HINT}
45887
46486
  `);
45888
- return 1;
46487
+ return 1;
46488
+ }
46489
+ const projectId2 = await resolveProjectId(client, parsed.project);
46490
+ await client.request(`/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}`, {
46491
+ method: "DELETE"
46492
+ });
46493
+ this.context.stdout.write(`deleted ${parsed.literal}
46494
+ `);
46495
+ return 0;
46496
+ } catch (err) {
46497
+ return handleExecError(this.context.stderr, err);
45889
46498
  }
45890
- const projectId2 = await findProjectIdByName2(client, parsed.project);
45891
- await client.request(`/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}`, {
45892
- method: "DELETE"
45893
- });
45894
- this.context.stdout.write(`deleted ${parsed.literal}
46499
+ }
46500
+ };
46501
+ function generateSecret(bytes = 32) {
46502
+ return randomBytes(bytes).toString("base64");
46503
+ }
46504
+ var ServerInitCommand = class extends Command {
46505
+ static paths = [["server", "init"]];
46506
+ static usage = Command.Usage({
46507
+ description: "Generate secrets and deployment files for a new keynv server.",
46508
+ details: `
46509
+ Interactive onboarding wizard that generates the configuration
46510
+ a fresh keynv server needs: JWT signing secret, master encryption
46511
+ key, and deployment templates.
46512
+
46513
+ Outputs:
46514
+ \u2022 A .keynv-server.env file you feed into your deployment
46515
+ (Docker Compose, Coolify, or bare-metal).
46516
+ \u2022 Optionally a docker-compose.yml in the current directory.
46517
+
46518
+ Non-interactive with --yes to skip prompts.
46519
+ `,
46520
+ examples: [
46521
+ ["Interactive wizard", "$0 server init"],
46522
+ ["Non-interactive (CI/scripts)", "$0 server init --yes"],
46523
+ ["Docker Compose output", "$0 server init --compose"],
46524
+ ["Custom output path", "$0 server init --out ./config/env"]
46525
+ ]
46526
+ });
46527
+ yes = options_exports.Boolean("--yes", false, {
46528
+ description: "Skip all prompts (non-interactive)."
46529
+ });
46530
+ compose = options_exports.Boolean("--compose", false, {
46531
+ description: "Also write a docker-compose.yml to the current directory."
46532
+ });
46533
+ out = options_exports.String("--out", {
46534
+ description: "Write server env file to this path (default: .keynv-server.env)."
46535
+ });
46536
+ async execute() {
46537
+ const jwtSecret = generateSecret(48);
46538
+ const masterKey = generateSecret(32);
46539
+ const webSessionSecret = generateSecret(48);
46540
+ const outPath = this.out ?? ".keynv-server.env";
46541
+ const contents = [
46542
+ "# Generated by `keynv server init`.",
46543
+ "# Keep this file secret \u2014 it contains cryptographic key material.",
46544
+ `KEYNV_JWT_SECRET=${jwtSecret}`,
46545
+ `KEYNV_MASTER_KEY=${masterKey}`,
46546
+ "",
46547
+ "# Web dashboard session secret (required for Next.js JWE session tokens).",
46548
+ `KEYNV_WEB_SESSION_SECRET=${webSessionSecret}`,
46549
+ "",
46550
+ "# Server configuration (uncomment and adjust):",
46551
+ "# KEYNV_PORT=8080",
46552
+ "# KEYNV_WEB_URL=https://keynv.example.com",
46553
+ "# KEYNV_PUBLIC_REGISTRATION=false",
46554
+ "# KEYNV_DB_PATH=./keynv.db",
46555
+ "# KEYNV_MASTER_KEY_FILE=./master.key",
46556
+ "# KEYNV_RATE_LIMIT_PER_MINUTE=120",
46557
+ "# KEYNV_ARGON2_MEMORY_KIB=46080",
46558
+ "# KEYNV_ARGON2_TIME_COST=3"
46559
+ ].join("\n");
46560
+ const { writeFileSync: writeFileSync4 } = await import('fs');
46561
+ writeFileSync4(outPath, `${contents}
45895
46562
  `);
46563
+ this.context.stdout.write(`Wrote ${outPath}
46564
+
46565
+ `);
46566
+ this.context.stdout.write("Generated secrets:\n");
46567
+ this.context.stdout.write(
46568
+ ` KEYNV_JWT_SECRET ${jwtSecret.slice(0, 8)}... (base64, 48 bytes)
46569
+ `
46570
+ );
46571
+ this.context.stdout.write(
46572
+ ` KEYNV_MASTER_KEY ${masterKey.slice(0, 8)}... (base64, 32 bytes)
46573
+ `
46574
+ );
46575
+ this.context.stdout.write(
46576
+ ` KEYNV_WEB_SESSION_SECRET ${webSessionSecret.slice(0, 8)}... (base64, 48 bytes)
46577
+ `
46578
+ );
46579
+ this.context.stdout.write("\nNext steps:\n");
46580
+ this.context.stdout.write(` 1. Feed ${outPath} into your deployment:
46581
+ `);
46582
+ this.context.stdout.write(" docker compose --env-file .keynv-server.env up -d\n");
46583
+ this.context.stdout.write(" or load it into Coolify as environment variables.\n");
46584
+ this.context.stdout.write(" 2. Bootstrap the first owner:\n");
46585
+ this.context.stdout.write(
46586
+ " docker exec -it <server-container> node dist/bootstrap.js \\\n"
46587
+ );
46588
+ this.context.stdout.write(" --owner-email alice@example.com \\\n");
46589
+ this.context.stdout.write(' --owner-password "<a long random password>" \\\n');
46590
+ this.context.stdout.write(' --org-name "Acme Inc"\n');
46591
+ this.context.stdout.write(" 3. Install the CLI and log in:\n");
46592
+ this.context.stdout.write(" npm install -g @keynv/cli\n");
46593
+ this.context.stdout.write(" keynv login --server https://keynv.example.com\n");
46594
+ this.context.stdout.write(" 4. Onboard your first project:\n");
46595
+ this.context.stdout.write(" cd your-project && keynv init\n");
46596
+ if (this.compose) {
46597
+ const composeContent = `# keynv server + Litestream sidecar
46598
+ # Generated by \`keynv server init\`.
46599
+ # Load with: docker compose --env-file .keynv-server.env up -d
46600
+
46601
+ services:
46602
+ server:
46603
+ image: ghcr.io/keynv-labs/keynv-server:latest
46604
+ restart: unless-stopped
46605
+ ports:
46606
+ - '8080:8080'
46607
+ env_file:
46608
+ - .keynv-server.env
46609
+ volumes:
46610
+ - keynv-data:/data
46611
+
46612
+ litestream:
46613
+ image: litestream/litestream:latest
46614
+ restart: unless-stopped
46615
+ volumes:
46616
+ - keynv-data:/data
46617
+ command: replicate
46618
+ environment:
46619
+ LITESTREAM_DB_PATH: /data/keynv.db
46620
+ LITESTREAM_REPLICA_URL: s3://your-bucket/keynv
46621
+ AWS_ACCESS_KEY_ID: \${LITESTREAM_AWS_ACCESS_KEY_ID}
46622
+ AWS_SECRET_ACCESS_KEY: \${LITESTREAM_AWS_SECRET_ACCESS_KEY}
46623
+
46624
+ volumes:
46625
+ keynv-data:
46626
+ `;
46627
+ writeFileSync4("docker-compose.yml", composeContent);
46628
+ this.context.stdout.write(
46629
+ "\nWrote docker-compose.yml (update Litestream S3 config before deploying).\n"
46630
+ );
46631
+ }
45896
46632
  return 0;
45897
46633
  }
45898
46634
  };
@@ -46248,18 +46984,9 @@ var TESTERS = [
46248
46984
  sshTester,
46249
46985
  httpTester
46250
46986
  ];
46251
- function findTester(type) {
46252
- return TESTERS.find((t) => t.type === type) ?? null;
46253
- }
46254
46987
 
46255
46988
  // src/commands/test.ts
46256
46989
  init_http();
46257
- async function findProjectIdByName3(client, name) {
46258
- const data = await client.request("/v1/projects");
46259
- const match = data.projects.find((p2) => p2.name === name);
46260
- if (!match) throw new Error(`unknown project: ${name}`);
46261
- return match.id;
46262
- }
46263
46990
  function parseTargets(specs) {
46264
46991
  const out = {};
46265
46992
  for (const spec of specs) {
@@ -46313,12 +47040,17 @@ the error message.
46313
47040
  );
46314
47041
  return 2;
46315
47042
  }
46316
- const tester = findTester(this.as);
47043
+ const tester = TESTERS.find((t) => t.type === this.as);
46317
47044
  if (!tester) {
46318
47045
  this.context.stderr.write(`keynv: unknown tester '${this.as}'
46319
47046
  `);
46320
47047
  return 1;
46321
47048
  }
47049
+ const timeoutMs = this.timeout ? Number.parseInt(this.timeout, 10) * 1e3 : void 0;
47050
+ if (this.timeout && (timeoutMs === void 0 || Number.isNaN(timeoutMs))) {
47051
+ this.context.stderr.write("keynv: invalid --timeout (expected integer seconds)\n");
47052
+ return 2;
47053
+ }
46322
47054
  let target;
46323
47055
  try {
46324
47056
  target = parseTargets(this.targets ?? []);
@@ -46335,7 +47067,7 @@ the error message.
46335
47067
  }
46336
47068
  let value;
46337
47069
  try {
46338
- const projectId2 = await findProjectIdByName3(client, parsedAlias.project);
47070
+ const projectId2 = await resolveProjectId(client, parsedAlias.project);
46339
47071
  const data = await client.request(
46340
47072
  `/v1/projects/${projectId2}/secrets/${parsedAlias.environment}/${parsedAlias.key}`
46341
47073
  );
@@ -46346,11 +47078,6 @@ the error message.
46346
47078
  `);
46347
47079
  return 1;
46348
47080
  }
46349
- const timeoutMs = this.timeout ? Number.parseInt(this.timeout, 10) * 1e3 : void 0;
46350
- if (this.timeout && (timeoutMs === void 0 || Number.isNaN(timeoutMs))) {
46351
- this.context.stderr.write("keynv: invalid --timeout (expected integer seconds)\n");
46352
- return 2;
46353
- }
46354
47081
  const result = await runTest({
46355
47082
  tester,
46356
47083
  secret: { alias: parsedAlias.literal, value },
@@ -46392,25 +47119,6 @@ the error message.
46392
47119
  }
46393
47120
  };
46394
47121
 
46395
- // src/commands/ui.ts
46396
- init_menu();
46397
- init_tty();
46398
- var UICommand = class extends Command {
46399
- static paths = [["ui"]];
46400
- static usage = Command.Usage({
46401
- description: "Open the interactive menu (also runs by default when no args are given)."
46402
- });
46403
- async execute() {
46404
- if (!isInteractive()) {
46405
- this.context.stdout.write(
46406
- "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"
46407
- );
46408
- return 0;
46409
- }
46410
- return runMenu();
46411
- }
46412
- };
46413
-
46414
47122
  // src/index.ts
46415
47123
  init_format();
46416
47124
  init_version();
@@ -46444,7 +47152,7 @@ cli.register(InitCommand);
46444
47152
  cli.register(RedactCommand);
46445
47153
  cli.register(RedactStreamCommand);
46446
47154
  cli.register(TestCommand);
46447
- cli.register(UICommand);
47155
+ cli.register(ServerInitCommand);
46448
47156
  var argv = process.argv.slice(2);
46449
47157
  if (argv.length === 0) {
46450
47158
  const { runMenu: runMenu2 } = await Promise.resolve().then(() => (init_menu(), menu_exports));