@keynv/cli 0.1.0-rc.8 → 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.8" ;
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,132 +7400,11 @@ 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
7407
 
7132
- // src/init/heuristics.ts
7133
- function shannonEntropyBits(s) {
7134
- if (s.length === 0) return 0;
7135
- const counts = /* @__PURE__ */ new Map();
7136
- for (const ch of s) counts.set(ch, (counts.get(ch) ?? 0) + 1);
7137
- let h2 = 0;
7138
- for (const c2 of counts.values()) {
7139
- const p2 = c2 / s.length;
7140
- h2 -= p2 * Math.log2(p2);
7141
- }
7142
- return h2;
7143
- }
7144
- function nameMatchesLiteralPrefix(name) {
7145
- const upper = name.toUpperCase();
7146
- return NAME_LITERAL_PREFIX.some((p2) => upper.startsWith(p2));
7147
- }
7148
- function nameMatchesSecretSuffix(name) {
7149
- const upper = name.toUpperCase();
7150
- for (const { suffix, hint } of NAME_SECRET_SUFFIXES) {
7151
- if (upper.endsWith(suffix) || upper.endsWith(`_${suffix}`)) {
7152
- return { matched: true, hint };
7153
- }
7154
- }
7155
- return { matched: false, hint: "" };
7156
- }
7157
- function classifyEntry(name, value) {
7158
- const upper = name.toUpperCase();
7159
- if (NAME_LITERAL_EXACT.has(upper)) {
7160
- return { verdict: "literal", hint: "common config var" };
7161
- }
7162
- if (nameMatchesLiteralPrefix(name)) {
7163
- return { verdict: "literal", hint: "public env (build-time bundled)" };
7164
- }
7165
- if (value.length === 0) {
7166
- return { verdict: "literal", hint: "empty" };
7167
- }
7168
- for (const { re, hint } of VALUE_PATTERNS) {
7169
- if (re.test(value)) return { verdict: "secret", hint };
7170
- }
7171
- const suffix = nameMatchesSecretSuffix(name);
7172
- if (suffix.matched) {
7173
- return { verdict: "secret", hint: suffix.hint };
7174
- }
7175
- if (NAME_DB_URL.test(name)) {
7176
- return { verdict: "secret", hint: "database URL" };
7177
- }
7178
- if (value.length >= 32) {
7179
- const bits = shannonEntropyBits(value);
7180
- if (bits >= 3.5) {
7181
- return { verdict: "secret", hint: `${value.length}-char random-looking string` };
7182
- }
7183
- }
7184
- return { verdict: "ambiguous", hint: "" };
7185
- }
7186
- function previewValue(value, max = 40) {
7187
- if (value.length === 0) return "(empty)";
7188
- if (value.length <= max) return value;
7189
- return `${value.slice(0, max - 1)}\u2026`;
7190
- }
7191
- var NAME_LITERAL_EXACT, NAME_LITERAL_PREFIX, NAME_SECRET_SUFFIXES, NAME_DB_URL, VALUE_PATTERNS;
7192
- var init_heuristics = __esm({
7193
- "src/init/heuristics.ts"() {
7194
- NAME_LITERAL_EXACT = /* @__PURE__ */ new Set([
7195
- "NODE_ENV",
7196
- "PORT",
7197
- "HOST",
7198
- "HOSTNAME",
7199
- "DEBUG",
7200
- "LOG_LEVEL",
7201
- "LOGLEVEL",
7202
- "TZ",
7203
- "LANG",
7204
- "LC_ALL",
7205
- "PATH",
7206
- "HOME",
7207
- "USER",
7208
- "SHELL",
7209
- "PWD",
7210
- "CI",
7211
- "NODE_OPTIONS",
7212
- "NPM_CONFIG_LOGLEVEL",
7213
- "TS_NODE_PROJECT"
7214
- ]);
7215
- NAME_LITERAL_PREFIX = ["NEXT_PUBLIC_", "VITE_", "REACT_APP_", "PUBLIC_", "EXPO_PUBLIC_"];
7216
- NAME_SECRET_SUFFIXES = [
7217
- { suffix: "PRIVATE_KEY", hint: "private key" },
7218
- { suffix: "API_KEY", hint: "API key" },
7219
- { suffix: "ACCESS_KEY", hint: "access key" },
7220
- { suffix: "SECRET_KEY", hint: "secret key" },
7221
- { suffix: "SECRET", hint: "secret" },
7222
- { suffix: "PASSWORD", hint: "password" },
7223
- { suffix: "PASSPHRASE", hint: "passphrase" },
7224
- { suffix: "TOKEN", hint: "token" },
7225
- { suffix: "KEY", hint: "key" },
7226
- { suffix: "CREDENTIALS", hint: "credentials" },
7227
- { suffix: "AUTH", hint: "auth" },
7228
- { suffix: "DSN", hint: "connection string" }
7229
- ];
7230
- NAME_DB_URL = /^(DATABASE|DB|POSTGRES|MYSQL|MONGO|REDIS)_URL$/i;
7231
- VALUE_PATTERNS = [
7232
- { re: /^sk-proj-/, hint: "OpenAI project key" },
7233
- { re: /^sk-[A-Za-z0-9]{20,}/, hint: "OpenAI key" },
7234
- { re: /^sk_live_/, hint: "Stripe live key" },
7235
- { re: /^sk_test_/, hint: "Stripe test key" },
7236
- { re: /^pk_live_/, hint: "Stripe publishable (often public, double-check)" },
7237
- { re: /^xoxb-/, hint: "Slack bot token" },
7238
- { re: /^xoxp-/, hint: "Slack user token" },
7239
- { re: /^ghp_[A-Za-z0-9]{30,}/, hint: "GitHub personal access token" },
7240
- { re: /^github_pat_/, hint: "GitHub fine-grained PAT" },
7241
- { re: /^gho_/, hint: "GitHub OAuth token" },
7242
- { re: /^AKIA[0-9A-Z]{16}$/, hint: "AWS access key id" },
7243
- { re: /^ASIA[0-9A-Z]{16}$/, hint: "AWS temporary key id" },
7244
- { re: /^AIza[0-9A-Za-z_-]{35}$/, hint: "Google API key" },
7245
- { re: /^ya29\./, hint: "Google OAuth access token" },
7246
- { re: /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, hint: "JWT" },
7247
- { re: /^-----BEGIN [A-Z ]+PRIVATE KEY-----/, hint: "PEM private key" },
7248
- { re: /^([a-z]+):\/\/[^@]+:[^@]+@/i, hint: "connection string with credentials" }
7249
- ];
7250
- }
7251
- });
7252
-
7253
7408
  // src/init/scriptWrap.ts
7254
7409
  function analyzeScript(name, command) {
7255
7410
  const trimmed = command.trim();
@@ -7482,14 +7637,14 @@ async function runInitFlow(client, opts) {
7482
7637
  R2.warn(`package.json at ${root.path} is not valid JSON \u2014 script wrapping will be skipped.`);
7483
7638
  }
7484
7639
  const envFiles = findEnvFiles(root.path);
7485
- if (envFiles.length === 0 && !hasExistingKeynvEnv(root.path)) {
7640
+ const intoExisting = hasExistingKeynvEnv(root.path);
7641
+ if (envFiles.length === 0 && !intoExisting) {
7486
7642
  R2.info(
7487
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.`
7488
7644
  );
7489
7645
  ye("Nothing to do.");
7490
7646
  return { exitCode: 0 };
7491
7647
  }
7492
- const intoExisting = hasExistingKeynvEnv(root.path);
7493
7648
  Se(
7494
7649
  [
7495
7650
  `Project root: ${root.path}`,
@@ -7499,43 +7654,79 @@ async function runInitFlow(client, opts) {
7499
7654
  ].filter(Boolean).join("\n"),
7500
7655
  "Detected"
7501
7656
  );
7502
- const merged = mergeEnvFiles(envFiles);
7503
- if (merged.length === 0) {
7504
- R2.info("All env files were empty (only comments/blanks). Nothing to upload.");
7505
- ye("Done.");
7506
- return { exitCode: 0 };
7507
- }
7508
7657
  const projectChoice = await pickOrCreateProject(client, root.suggestedName);
7509
7658
  if (projectChoice === null) {
7510
7659
  me("No project selected.");
7511
7660
  return { exitCode: 130 };
7512
7661
  }
7513
- const envName = await pickEnvForUpload(client, projectChoice, envFiles);
7514
- if (envName === null) {
7515
- me("No environment selected.");
7662
+ const fileMapping = await pickFileEnvMapping(envFiles, projectChoice);
7663
+ if (fileMapping === null) {
7664
+ me("No env mapping selected.");
7516
7665
  return { exitCode: 130 };
7517
7666
  }
7518
- const choices = merged.map((e2) => {
7519
- const verdict = classifyEntry(e2.name, e2.value);
7520
- const hint = verdict.hint || (e2.isAlias ? "looks like an alias literal" : "no signal");
7521
- const preview2 = e2.isAlias ? e2.value : previewValue(e2.value, 32);
7522
- return {
7523
- name: e2.name,
7524
- value: e2.value,
7525
- isAlias: e2.isAlias,
7526
- verdict: verdict.verdict,
7527
- label: `${e2.name} ${preview2}`,
7528
- hint,
7529
- sourceLine: e2.sourceLine,
7530
- source: e2.source
7531
- };
7532
- });
7533
- const initialSecretSelection = choices.filter((c2) => c2.verdict === "secret" && !c2.isAlias).map((c2) => c2.name);
7534
- 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(
7535
7726
  await ve({
7536
7727
  message: "Mark which keys are secrets (vault-uploaded). Unchecked keys stay as literals.",
7537
7728
  options: choices.map((c2) => ({
7538
- value: c2.name,
7729
+ value: c2.composite,
7539
7730
  label: c2.label,
7540
7731
  hint: c2.isAlias ? `${c2.hint} \u2014 already aliased; will pass through` : c2.hint
7541
7732
  })),
@@ -7543,50 +7734,26 @@ async function runInitFlow(client, opts) {
7543
7734
  required: false
7544
7735
  })
7545
7736
  );
7546
- const selected = new Set(selectedSecretNames);
7737
+ const selected = new Set(selectedComposites);
7738
+ const defaultEnv = pickDefaultEnv(distinctEnvs);
7547
7739
  let scriptWrapSelection = [];
7548
- let scriptPlan = root.packageJsonScripts ? planScriptWrap(root.packageJsonScripts) : null;
7740
+ const scriptPlan = root.packageJsonScripts ? planScriptWrap(root.packageJsonScripts) : null;
7549
7741
  if (!opts.noScripts && scriptPlan && scriptPlan.recommended.length > 0) {
7550
- const wrapAnswer = unwrap(
7551
- await ve({
7552
- message: "Wrap package.json scripts with `keynv exec`? (recommended)",
7553
- options: [
7554
- ...scriptPlan.recommended.map((a) => ({
7555
- value: a.name,
7556
- label: `${a.name} \u2192 ${a.wrapped}`,
7557
- hint: a.hint
7558
- })),
7559
- ...scriptPlan.unknown.map((a) => ({
7560
- value: a.name,
7561
- label: `${a.name} \u2192 ${a.wrapped}`,
7562
- hint: `unknown tool \u2014 opt in if you know it reads env`
7563
- }))
7564
- ],
7565
- initialValues: scriptPlan.recommended.map((a) => a.name),
7566
- required: false
7567
- })
7568
- );
7569
- scriptWrapSelection = wrapAnswer;
7570
- } else if (opts.noScripts) {
7571
- scriptPlan = null;
7572
- }
7573
- const envFileFateRaw = unwrap(
7574
- await Ee({
7575
- message: "After upload, what about the original .env files?",
7576
- options: [
7577
- { value: "delete", label: "Delete (recommended \u2014 eliminates the leak vector)" },
7578
- { value: "gitignore", label: "Keep but make sure .gitignore covers them" }
7579
- ],
7580
- initialValue: "delete"
7581
- })
7582
- );
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");
7583
7750
  const planSummary = [
7584
- `Project: ${projectChoice.name}${projectChoice.created ? " (will be created)" : ""}`,
7585
- `Environment: ${envName}`,
7586
- `Secrets to vault: ${selected.size}`,
7587
- `Literals in file: ${merged.length - selected.size}`,
7588
- `Script wraps: ${scriptWrapSelection.length}`,
7589
- `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",
7590
7757
  opts.dryRun ? "Dry-run: no changes will be made." : ""
7591
7758
  ].filter(Boolean).join("\n");
7592
7759
  Se(planSummary, "About to apply");
@@ -7599,82 +7766,114 @@ async function runInitFlow(client, opts) {
7599
7766
  ye("Dry-run complete \u2014 no changes were made.");
7600
7767
  return { exitCode: 0 };
7601
7768
  }
7602
- let projectId2;
7603
- if (projectChoice.created) {
7604
- const s = ft();
7605
- s.start(`Creating project "${projectChoice.name}"`);
7606
- try {
7607
- const created = await client.request("/v1/projects", {
7608
- method: "POST",
7609
- body: {
7610
- name: projectChoice.name,
7611
- environments: [{ name: envName, tier: "non-production", require_approval: false }]
7612
- }
7613
- });
7614
- projectId2 = created.id;
7615
- s.stop(`Created project ${created.name}`);
7616
- } catch (err) {
7617
- s.error(`Failed to create project: ${err instanceof Error ? err.message : String(err)}`);
7618
- return { exitCode: 1 };
7619
- }
7620
- } else {
7621
- projectId2 = projectChoice.id;
7622
- }
7623
- const uploaded = [];
7769
+ const projectId2 = await ensureProjectAndEnvs(client, projectChoice, distinctEnvs);
7770
+ if (projectId2 === null) return { exitCode: 1 };
7771
+ const uploadedByEnv = /* @__PURE__ */ new Map();
7624
7772
  const failed = [];
7625
- if (selected.size > 0) {
7773
+ const totalToUpload = selected.size;
7774
+ if (totalToUpload > 0) {
7626
7775
  const s = ft();
7627
- s.start(`Uploading ${selected.size} secrets`);
7776
+ s.start(`Uploading ${totalToUpload} secret${totalToUpload === 1 ? "" : "s"}`);
7628
7777
  let i = 0;
7629
- for (const entry2 of merged) {
7630
- if (!selected.has(entry2.name)) continue;
7631
- i++;
7632
- s.message(`Uploading (${i}/${selected.size}) ${entry2.name}`);
7633
- const aliasKey = entry2.name.toLowerCase().replace(/_/g, "-");
7634
- try {
7635
- await client.request(`/v1/projects/${projectId2}/secrets`, {
7636
- method: "POST",
7637
- body: { env: envName, key: aliasKey, value: entry2.value }
7638
- });
7639
- const alias2 = buildAlias({ project: projectChoice.name, environment: envName, key: aliasKey });
7640
- 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) {
7641
7806
  failed.push({
7642
- name: entry2.name,
7643
- 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)
7644
7810
  });
7645
- } else {
7646
- uploaded.push({ name: entry2.name, aliasLiteral: alias2.literal });
7647
7811
  }
7648
- } catch (err) {
7649
- failed.push({ name: entry2.name, reason: err instanceof Error ? err.message : String(err) });
7650
7812
  }
7651
7813
  }
7652
7814
  if (failed.length === 0) {
7653
- s.stop(`Uploaded ${uploaded.length} secrets`);
7815
+ s.stop(`Uploaded ${totalToUpload} secret${totalToUpload === 1 ? "" : "s"}`);
7654
7816
  } else {
7655
- s.error(`${uploaded.length}/${selected.size} uploaded; ${failed.length} failed`);
7656
- 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}`);
7657
7821
  }
7658
7822
  }
7659
- const literals = merged.filter((e2) => !selected.has(e2.name));
7660
- 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
+ );
7661
7827
  const keynvEnvPath = join(root.path, ".keynv.env");
7662
- let writtenLines;
7663
7828
  try {
7664
- writtenLines = composeKeynvEnv({
7665
- uploadedAliases: successUploads,
7666
- literals,
7829
+ const lines = composeKeynvEnv({
7830
+ uploadedAliases: defaultUploaded,
7831
+ literals: defaultLiterals,
7667
7832
  mergeWithExisting: intoExisting ? readFileSync(keynvEnvPath, "utf8") : null
7668
7833
  });
7669
- writeFileSync(keynvEnvPath, `${writtenLines.join("\n")}
7834
+ writeFileSync(keynvEnvPath, `${lines.join("\n")}
7670
7835
  `);
7671
7836
  R2.success(
7672
- `${intoExisting ? "Updated" : "Wrote"} ${keynvEnvPath} (${successUploads.size + literals.length} entries)`
7837
+ `${intoExisting ? "Updated" : "Wrote"} ${keynvEnvPath} (${defaultUploaded.size + defaultLiterals.length} entries from "${defaultEnv}")`
7673
7838
  );
7674
7839
  } catch (err) {
7675
7840
  R2.error(`Failed to write .keynv.env: ${err instanceof Error ? err.message : String(err)}`);
7676
7841
  return { exitCode: 1 };
7677
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
+ }
7869
+ try {
7870
+ const outcome = writeAiContext(root.path);
7871
+ if (outcome === "created") R2.success("Wrote AGENTS.md (so AI agents understand keynv)");
7872
+ else if (outcome === "updated") R2.success("Refreshed keynv section in AGENTS.md");
7873
+ else if (outcome === "appended") R2.success("Appended keynv section to AGENTS.md");
7874
+ } catch (err) {
7875
+ R2.warn(`Could not write AGENTS.md: ${err instanceof Error ? err.message : String(err)}`);
7876
+ }
7678
7877
  if (scriptWrapSelection.length > 0 && root.packageJsonScripts) {
7679
7878
  try {
7680
7879
  updatePackageJsonScripts(
@@ -7682,66 +7881,37 @@ async function runInitFlow(client, opts) {
7682
7881
  root.packageJsonScripts,
7683
7882
  scriptWrapSelection
7684
7883
  );
7685
- 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
+ );
7686
7887
  } catch (err) {
7687
7888
  R2.warn(
7688
7889
  `Could not update package.json scripts: ${err instanceof Error ? err.message : String(err)}`
7689
7890
  );
7690
7891
  }
7691
7892
  }
7692
- if (envFileFateRaw === "delete") {
7693
- for (const f of envFiles) {
7694
- try {
7695
- unlinkSync(f.path);
7696
- R2.success(`Removed ${f.name}`);
7697
- } catch (err) {
7698
- R2.warn(`Could not remove ${f.name}: ${err instanceof Error ? err.message : String(err)}`);
7699
- }
7700
- }
7701
- } else {
7702
- const gitignorePath = join(root.path, ".gitignore");
7893
+ for (const f of envFiles) {
7703
7894
  try {
7704
- ensureGitignoreEntries(gitignorePath, envFiles.map((f) => f.name));
7705
- R2.success(`Updated .gitignore (${envFiles.length} entries ensured)`);
7895
+ unlinkSync(f.path);
7896
+ R2.success(`Removed ${f.name}`);
7706
7897
  } catch (err) {
7707
- 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)}`);
7708
7899
  }
7709
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
+ }
7710
7910
  ye(
7711
- 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>"}`
7712
7912
  );
7713
7913
  return { exitCode: failed.length > 0 ? 1 : 0 };
7714
7914
  }
7715
- function mergeEnvFiles(files) {
7716
- const map = /* @__PURE__ */ new Map();
7717
- for (const f of files) {
7718
- let entries;
7719
- try {
7720
- entries = parseEnvFile(readFileSync(f.path, "utf8"), f.path);
7721
- } catch (err) {
7722
- R2.warn(`${f.name}: ${err instanceof Error ? err.message : String(err)} \u2014 skipping file`);
7723
- continue;
7724
- }
7725
- for (const e2 of entries) {
7726
- const existing = map.get(e2.name);
7727
- if (existing) {
7728
- existing.shadowedBy.push(f.name);
7729
- existing.value = e2.value;
7730
- existing.isAlias = e2.isAlias;
7731
- } else {
7732
- map.set(e2.name, {
7733
- name: e2.name,
7734
- value: e2.value,
7735
- isAlias: e2.isAlias,
7736
- source: f.name,
7737
- sourceLine: e2.line,
7738
- shadowedBy: []
7739
- });
7740
- }
7741
- }
7742
- }
7743
- return [...map.values()];
7744
- }
7745
7915
  async function pickOrCreateProject(client, suggestedName) {
7746
7916
  const projects = await listProjects(client);
7747
7917
  const value = unwrap(
@@ -7761,53 +7931,156 @@ async function pickOrCreateProject(client, suggestedName) {
7761
7931
  validate: (v2) => v2 && /^[a-z0-9][a-z0-9-]{0,47}$/.test(v2) ? void 0 : "lowercase letters, digits, dashes; up to 48 chars"
7762
7932
  })
7763
7933
  );
7764
- return { id: "", name, created: true };
7934
+ return { id: "", name, created: true, existingEnvs: [] };
7765
7935
  }
7766
7936
  const match = projects.find((p2) => p2.id === value);
7767
7937
  if (!match) return null;
7768
- 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
+ };
7769
7945
  }
7770
- async function pickEnvForUpload(client, project, envFiles) {
7771
- const firstSuffix = envFiles[0]?.suffix ?? null;
7772
- const suggested = suggestedEnvForSuffix(firstSuffix);
7773
- if (project.created) {
7774
- const value2 = unwrap(
7775
- await Re({
7776
- message: "Environment to create",
7777
- initialValue: suggested,
7778
- 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
7779
7974
  })
7780
7975
  );
7781
- 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 });
7782
7985
  }
7783
- const detail = await client.request(`/v1/projects/${project.id}`);
7784
- if (detail.environments.length === 0) {
7785
- me(
7786
- `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).`
7787
- );
7788
- 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
+ }
7789
8020
  }
7790
- if (detail.environments.length === 1) {
7791
- 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
+ }
7792
8048
  }
7793
- const value = unwrap(
7794
- await Ee({
7795
- message: "Upload to which environment?",
7796
- options: detail.environments.map((e2) => ({
7797
- value: e2.name,
7798
- label: e2.name,
7799
- hint: e2.tier
7800
- })),
7801
- initialValue: detail.environments.find((e2) => e2.name === suggested)?.name
7802
- })
7803
- );
7804
- 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
+ };
7805
8077
  }
7806
8078
  function composeKeynvEnv(opts) {
7807
8079
  const { uploadedAliases, literals, mergeWithExisting } = opts;
7808
8080
  const lines = [];
7809
8081
  if (mergeWithExisting !== null) {
7810
- const trimmed = mergeWithExisting.replace(/\n+$/, "");
8082
+ const normalized = mergeWithExisting.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
8083
+ const trimmed = normalized.replace(/\n+$/, "");
7811
8084
  if (trimmed.length > 0) {
7812
8085
  lines.push(...trimmed.split("\n"));
7813
8086
  lines.push("");
@@ -7834,7 +8107,7 @@ function composeKeynvEnv(opts) {
7834
8107
  }
7835
8108
  }
7836
8109
  if (mergeWithExisting !== null) {
7837
- lines.push(`# <<< keynv init <<<`);
8110
+ lines.push("# <<< keynv init <<<");
7838
8111
  }
7839
8112
  return lines;
7840
8113
  }
@@ -7853,21 +8126,12 @@ function updatePackageJsonScripts(path, originalScripts, selectedNames) {
7853
8126
  writeFileSync(path, trailingNewline ? `${out}
7854
8127
  ` : out);
7855
8128
  }
7856
- function ensureGitignoreEntries(path, basenames) {
7857
- let existing = "";
7858
- if (existsSync(path)) existing = readFileSync(path, "utf8");
7859
- const existingLines = new Set(existing.split("\n").map((l) => l.trim()));
7860
- const toAdd = basenames.filter((n) => !existingLines.has(n));
7861
- if (toAdd.length === 0) return;
7862
- const sep = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
7863
- const block = ["", "# added by keynv init", ...toAdd, ""].join("\n");
7864
- writeFileSync(path, `${existing}${sep}${block}`);
7865
- }
7866
8129
  var init_init = __esm({
7867
8130
  "src/ui/flows/init.ts"() {
7868
- init_dist();
7869
8131
  init_dist5();
8132
+ init_dist();
7870
8133
  init_envFile();
8134
+ init_aiContext();
7871
8135
  init_detect();
7872
8136
  init_heuristics();
7873
8137
  init_scriptWrap();
@@ -7876,30 +8140,122 @@ var init_init = __esm({
7876
8140
  }
7877
8141
  });
7878
8142
 
7879
- // src/ui/helpers/pickSecret.ts
7880
- async function listSecrets(client, projectId2) {
7881
- const data = await client.request(
7882
- `/v1/projects/${projectId2}/secrets`
7883
- );
7884
- 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;
7885
8150
  }
7886
- async function pickSecret(client, projectId2, message = "Secret:") {
7887
- const secrets = await listSecrets(client, projectId2);
7888
- if (secrets.length === 0) return null;
7889
- const value = await Ee({
7890
- message,
7891
- options: secrets.map((s) => ({
7892
- value: s.alias,
7893
- label: s.alias,
7894
- hint: `v${s.version}`
7895
- }))
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() })
7896
8185
  });
7897
- if (q(value)) return null;
7898
- 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.");
7899
8244
  }
7900
- var init_pickSecret = __esm({
7901
- "src/ui/helpers/pickSecret.ts"() {
7902
- 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";
7903
8259
  }
7904
8260
  });
7905
8261
 
@@ -7927,6 +8283,33 @@ var init_pickEnv = __esm({
7927
8283
  init_dist5();
7928
8284
  }
7929
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
+ });
7930
8313
  async function runSecretsFlow(client, project) {
7931
8314
  let target = project;
7932
8315
  if (!target) {
@@ -7990,7 +8373,10 @@ async function runSecretMenu(client, project, alias2) {
7990
8373
  }
7991
8374
  } else if (choice === "rotate") {
7992
8375
  const newValue = unwrap(
7993
- 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
+ })
7994
8380
  );
7995
8381
  const data = await client.request(`${path}/rotate`, {
7996
8382
  method: "POST",
@@ -7998,9 +8384,7 @@ async function runSecretMenu(client, project, alias2) {
7998
8384
  });
7999
8385
  R2.success(`Rotated ${data.alias} \u2192 v${data.version}`);
8000
8386
  } else if (choice === "delete") {
8001
- const confirmed = unwrap(
8002
- await ue({ message: `Delete ${alias2}?`, initialValue: false })
8003
- );
8387
+ const confirmed = unwrap(await ue({ message: `Delete ${alias2}?`, initialValue: false }));
8004
8388
  if (!confirmed) continue;
8005
8389
  await client.request(path, { method: "DELETE" });
8006
8390
  R2.success(`Deleted ${alias2}`);
@@ -8009,11 +8393,13 @@ async function runSecretMenu(client, project, alias2) {
8009
8393
  }
8010
8394
  }
8011
8395
  async function copyToClipboard(value) {
8012
- const platform = process.platform;
8013
- 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"]];
8014
8398
  return new Promise((resolve3) => {
8015
8399
  try {
8016
- 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
+ });
8017
8403
  child.on("error", () => resolve3(false));
8018
8404
  child.on("exit", (code) => resolve3(code === 0));
8019
8405
  child.stdin.end(value);
@@ -8040,7 +8426,7 @@ async function promptNewSecret(client, project) {
8040
8426
  }),
8041
8427
  value: () => Ce({
8042
8428
  message: "Value (hidden)",
8043
- validate: (v2) => v2 && v2.length ? void 0 : "required"
8429
+ validate: (v2) => v2?.length ? void 0 : "required"
8044
8430
  })
8045
8431
  },
8046
8432
  {
@@ -8069,8 +8455,8 @@ async function createSecretInteractive(client, project) {
8069
8455
  }
8070
8456
  var init_secret = __esm({
8071
8457
  "src/ui/flows/secret.ts"() {
8072
- init_dist();
8073
8458
  init_dist5();
8459
+ init_dist();
8074
8460
  init_cancel();
8075
8461
  init_pickEnv();
8076
8462
  init_pickProject();
@@ -24764,7 +25150,7 @@ var require_named_placeholders = __commonJS({
24764
25150
  }
24765
25151
  return s;
24766
25152
  }
24767
- function join5(tree) {
25153
+ function join6(tree) {
24768
25154
  if (tree.length === 1) {
24769
25155
  return tree;
24770
25156
  }
@@ -24790,7 +25176,7 @@ var require_named_placeholders = __commonJS({
24790
25176
  if (cache2 && (tree = cache2.get(query))) {
24791
25177
  return toArrayParams(tree, paramsObj);
24792
25178
  }
24793
- tree = join5(parse(query));
25179
+ tree = join6(parse(query));
24794
25180
  if (cache2) {
24795
25181
  cache2.set(query, tree);
24796
25182
  }
@@ -28403,7 +28789,7 @@ var require_utils_webcrypto = __commonJS({
28403
28789
  var nodeCrypto = __require("crypto");
28404
28790
  module.exports = {
28405
28791
  postgresMd5PasswordHash,
28406
- randomBytes,
28792
+ randomBytes: randomBytes2,
28407
28793
  deriveKey,
28408
28794
  sha256,
28409
28795
  hashByName,
@@ -28413,7 +28799,7 @@ var require_utils_webcrypto = __commonJS({
28413
28799
  var webCrypto = nodeCrypto.webcrypto || globalThis.crypto;
28414
28800
  var subtleCrypto = webCrypto.subtle;
28415
28801
  var textEncoder = new TextEncoder();
28416
- function randomBytes(length) {
28802
+ function randomBytes2(length) {
28417
28803
  return webCrypto.getRandomValues(Buffer.alloc(length));
28418
28804
  }
28419
28805
  async function md5(string) {
@@ -28809,11 +29195,11 @@ var require_pg_connection_string = __commonJS({
28809
29195
  config.client_encoding = result.searchParams.get("encoding");
28810
29196
  return config;
28811
29197
  }
28812
- const hostname = dummyHost ? "" : result.hostname;
29198
+ const hostname2 = dummyHost ? "" : result.hostname;
28813
29199
  if (!config.host) {
28814
- config.host = decodeURIComponent(hostname);
28815
- } else if (hostname && /^%2f/i.test(hostname)) {
28816
- result.pathname = hostname + result.pathname;
29200
+ config.host = decodeURIComponent(hostname2);
29201
+ } else if (hostname2 && /^%2f/i.test(hostname2)) {
29202
+ result.pathname = hostname2 + result.pathname;
28817
29203
  }
28818
29204
  if (!config.port) {
28819
29205
  config.port = result.port;
@@ -39919,9 +40305,9 @@ var require_cluster = __commonJS({
39919
40305
  }
39920
40306
  });
39921
40307
  }
39922
- resolveSrv(hostname) {
40308
+ resolveSrv(hostname2) {
39923
40309
  return new Promise((resolve3, reject) => {
39924
- this.options.resolveSrv(hostname, (err, records) => {
40310
+ this.options.resolveSrv(hostname2, (err, records) => {
39925
40311
  if (err) {
39926
40312
  return reject(err);
39927
40313
  }
@@ -39943,14 +40329,14 @@ var require_cluster = __commonJS({
39943
40329
  });
39944
40330
  });
39945
40331
  }
39946
- dnsLookup(hostname) {
40332
+ dnsLookup(hostname2) {
39947
40333
  return new Promise((resolve3, reject) => {
39948
- this.options.dnsLookup(hostname, (err, address) => {
40334
+ this.options.dnsLookup(hostname2, (err, address) => {
39949
40335
  if (err) {
39950
- 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);
39951
40337
  reject(err);
39952
40338
  } else {
39953
- debug2("resolved hostname %s to IP %s", hostname, address);
40339
+ debug2("resolved hostname %s to IP %s", hostname2, address);
39954
40340
  resolve3(address);
39955
40341
  }
39956
40342
  });
@@ -42275,85 +42661,27 @@ var init_audit2 = __esm({
42275
42661
  });
42276
42662
 
42277
42663
  // src/ui/flows/login.ts
42278
- async function runLoginFlow(client) {
42279
- const answers = await he(
42280
- {
42281
- server: () => Re({
42282
- message: "Server URL",
42283
- placeholder: "http://localhost:8080",
42284
- defaultValue: "http://localhost:8080"
42285
- }),
42286
- email: () => Re({
42287
- message: "Email",
42288
- validate: (v2) => v2 && v2.includes("@") ? void 0 : "enter an email"
42289
- }),
42290
- password: () => Ce({
42291
- message: "Password",
42292
- validate: (v2) => v2 && v2.length >= 1 ? void 0 : "required"
42293
- })
42294
- },
42295
- {
42296
- onCancel: () => {
42297
- throw new UserCancelled();
42298
- }
42299
- }
42300
- );
42301
- const server = answers.server || "http://localhost:8080";
42664
+ async function runLoginFlow(client, options = {}) {
42665
+ const server = options.server ?? DEFAULT_SERVER_URL;
42302
42666
  const s = ft();
42303
- s.start("Authenticating");
42304
- let res;
42667
+ s.start("Waiting for authorization");
42668
+ let creds;
42305
42669
  try {
42306
- res = await fetch(new URL("/v1/auth/login", server).toString(), {
42307
- method: "POST",
42308
- headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
42309
- body: JSON.stringify({ email: answers.email, password: answers.password })
42310
- });
42670
+ creds = await runBrowserAuth(server);
42311
42671
  } catch (err) {
42312
- s.error("Network error");
42313
- me(err instanceof Error ? err.message : "unreachable");
42314
- return false;
42315
- }
42316
- if (!res.ok) {
42317
- let msg = `login failed (${res.status})`;
42318
- try {
42319
- const parsed = await res.json();
42320
- msg = parsed.error?.message ?? msg;
42321
- } catch {
42322
- }
42323
- s.error(msg);
42672
+ s.error("Browser login failed");
42673
+ me(err instanceof Error ? err.message : "Unable to authenticate.");
42324
42674
  return false;
42325
42675
  }
42326
- const data = await res.json();
42327
- await saveCredentials({
42328
- server_url: server,
42329
- user_id: data.user.id,
42330
- email: data.user.email,
42331
- org_id: data.user.org_id,
42332
- org_role: data.user.org_role,
42333
- access_token: data.access_token,
42334
- refresh_token: data.refresh_token,
42335
- access_expires_at: new Date(Date.now() + data.expires_in * 1e3).toISOString()
42336
- });
42337
- await client.setCredentials({
42338
- server_url: server,
42339
- user_id: data.user.id,
42340
- email: data.user.email,
42341
- org_id: data.user.org_id,
42342
- org_role: data.user.org_role,
42343
- access_token: data.access_token,
42344
- refresh_token: data.refresh_token,
42345
- access_expires_at: new Date(Date.now() + data.expires_in * 1e3).toISOString()
42346
- });
42347
- s.stop(`Logged in as ${data.user.email}`);
42676
+ await client.setCredentials(creds);
42677
+ s.stop(`Logged in as ${creds.email}`);
42348
42678
  return true;
42349
42679
  }
42350
42680
  var init_login = __esm({
42351
42681
  "src/ui/flows/login.ts"() {
42352
42682
  init_dist5();
42353
- init_http();
42354
- init_store();
42355
- init_version();
42356
- init_cancel();
42683
+ init_browser_auth();
42684
+ init_defaults();
42357
42685
  }
42358
42686
  });
42359
42687
 
@@ -42412,7 +42740,7 @@ async function addMemberInteractive(client, project) {
42412
42740
  {
42413
42741
  email: () => Re({
42414
42742
  message: "Email",
42415
- validate: (v2) => v2 && v2.includes("@") ? void 0 : "enter an email"
42743
+ validate: (v2) => v2?.includes("@") ? void 0 : "enter an email"
42416
42744
  }),
42417
42745
  role: () => Ee({
42418
42746
  message: "Role",
@@ -42511,8 +42839,11 @@ function printDetail(detail) {
42511
42839
  const envLines = detail.environments.map(
42512
42840
  (e2) => ` ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})`
42513
42841
  );
42514
- Se(`${detail.name} (${detail.id})
42515
- ${envLines.join("\n") || " (no environments)"}`, "Project");
42842
+ Se(
42843
+ `${detail.name} (${detail.id})
42844
+ ${envLines.join("\n") || " (no environments)"}`,
42845
+ "Project"
42846
+ );
42516
42847
  }
42517
42848
  async function createProjectInteractive(client) {
42518
42849
  const answers = await he(
@@ -42571,6 +42902,7 @@ async function runMenu() {
42571
42902
  ge(`keynv ${VERSION}`);
42572
42903
  const client = new ApiClient();
42573
42904
  await client.ensureHydrated();
42905
+ let didLogin = false;
42574
42906
  if (!client.isLoggedIn) {
42575
42907
  R2.info("Not logged in.");
42576
42908
  try {
@@ -42579,6 +42911,7 @@ async function runMenu() {
42579
42911
  ye("Login cancelled.");
42580
42912
  return 1;
42581
42913
  }
42914
+ didLogin = true;
42582
42915
  } catch (err) {
42583
42916
  if (err instanceof UserCancelled) {
42584
42917
  me("Login cancelled.");
@@ -42590,6 +42923,20 @@ async function runMenu() {
42590
42923
  const u = client.currentUser;
42591
42924
  if (u) R2.message(`${u.email} (${u.org_role}) @ ${u.server_url}`);
42592
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
+ }
42593
42940
  while (true) {
42594
42941
  let choice;
42595
42942
  try {
@@ -42656,15 +43003,16 @@ var init_menu = __esm({
42656
43003
  "src/ui/menu.ts"() {
42657
43004
  init_dist5();
42658
43005
  init_http();
43006
+ init_detect();
42659
43007
  init_store();
42660
- init_format();
42661
- init_cancel();
43008
+ init_version();
42662
43009
  init_audit2();
42663
43010
  init_login();
42664
43011
  init_member();
42665
43012
  init_project();
42666
43013
  init_secret();
42667
- init_version();
43014
+ init_format();
43015
+ init_cancel();
42668
43016
  }
42669
43017
  });
42670
43018
 
@@ -43345,11 +43693,11 @@ var reducers = {
43345
43693
  return { ...state, options: [{ name: `-c`, value: String(command) }] };
43346
43694
  }
43347
43695
  },
43348
- setError: (state, segment, segmentIndex, errorMessage) => {
43696
+ setError: (state, segment, segmentIndex, errorMessage2) => {
43349
43697
  if (segment === SpecialToken.EndOfInput || segment === SpecialToken.EndOfPartialInput) {
43350
- return { ...state, errorMessage: `${errorMessage}.` };
43698
+ return { ...state, errorMessage: `${errorMessage2}.` };
43351
43699
  } else {
43352
- return { ...state, errorMessage: `${errorMessage} ("${segment}").` };
43700
+ return { ...state, errorMessage: `${errorMessage2} ("${segment}").` };
43353
43701
  }
43354
43702
  },
43355
43703
  setOptionArityError: (state, segment) => {
@@ -44382,44 +44730,48 @@ var AuditListCommand = class extends Command {
44382
44730
  sinceId = options_exports.String("--since-id");
44383
44731
  json = options_exports.Boolean("--json", false);
44384
44732
  async execute() {
44385
- const client = new ApiClient();
44386
- const data = await client.request(
44387
- "/v1/audit",
44388
- {
44389
- query: {
44390
- event_type: this.eventType,
44391
- limit: this.limit,
44392
- 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
+ }
44393
44743
  }
44394
- }
44395
- );
44396
- if (this.json) {
44397
- this.context.stdout.write(`${JSON.stringify(data, null, 2)}
44744
+ );
44745
+ if (this.json) {
44746
+ this.context.stdout.write(`${JSON.stringify(data, null, 2)}
44398
44747
  `);
44399
- return 0;
44400
- }
44401
- if (data.entries.length === 0) {
44402
- this.context.stdout.write("no audit entries\n");
44403
- return 0;
44404
- }
44405
- this.context.stdout.write(
44406
- `${table(
44407
- ["id", "ts", "actor", "agent", "event"],
44408
- data.entries.map((e2) => [
44409
- String(e2.id),
44410
- e2.ts,
44411
- e2.actor_user_id ?? "(none)",
44412
- e2.actor_agent,
44413
- e2.event_type
44414
- ])
44415
- )}
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
+ )}
44416
44765
  `
44417
- );
44418
- if (data.next_cursor) {
44419
- 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})
44420
44769
  `);
44770
+ }
44771
+ return 0;
44772
+ } catch (err) {
44773
+ return handleExecError(this.context.stderr, err);
44421
44774
  }
44422
- return 0;
44423
44775
  }
44424
44776
  };
44425
44777
  var AuditVerifyCommand = class extends Command {
@@ -44429,23 +44781,27 @@ var AuditVerifyCommand = class extends Command {
44429
44781
  });
44430
44782
  json = options_exports.Boolean("--json", false);
44431
44783
  async execute() {
44432
- const client = new ApiClient();
44433
- const data = await client.request("/v1/audit/verify", { method: "POST" });
44434
- if (this.json) {
44435
- 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)}
44436
44789
  `);
44437
- return data.ok ? 0 : 1;
44438
- }
44439
- if (data.ok) {
44440
- 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
44441
44794
  `);
44442
- return 0;
44443
- }
44444
- this.context.stdout.write(
44445
- `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
44446
44799
  `
44447
- );
44448
- return 1;
44800
+ );
44801
+ return 1;
44802
+ } catch (err) {
44803
+ return handleExecError(this.context.stderr, err);
44804
+ }
44449
44805
  }
44450
44806
  };
44451
44807
 
@@ -44486,11 +44842,10 @@ async function resolveAllAliases(client, argv2, extraStrings = []) {
44486
44842
  return resolved;
44487
44843
  }
44488
44844
  function substitute(text, resolved) {
44845
+ const sorted = [...resolved].sort((a, b2) => b2.alias.literal.length - a.alias.literal.length);
44489
44846
  let out = text;
44490
- for (const r of resolved) {
44491
- if (out.includes(r.alias.literal)) {
44492
- out = out.split(r.alias.literal).join(r.value);
44493
- }
44847
+ for (const r of sorted) {
44848
+ out = out.split(r.alias.literal).join(r.value);
44494
44849
  }
44495
44850
  return out;
44496
44851
  }
@@ -44593,10 +44948,18 @@ var BUILTIN_LINE_PATTERNS = BUILTIN_PATTERNS.filter((p2) => !p2.multiline);
44593
44948
 
44594
44949
  // ../../packages/redactor/dist/entropy.js
44595
44950
  var TOKEN_BOUNDARY_RE = /[\s,;:'"<>(){}[\]=]+/;
44951
+ var DEFAULT_EXCLUDE_PREFIXES = [
44952
+ "sha1-",
44953
+ "sha256:",
44954
+ "sha256-",
44955
+ "sha384-",
44956
+ "sha512-"
44957
+ ];
44596
44958
  var DEFAULTS = {
44597
44959
  enabled: true,
44598
44960
  minLength: 24,
44599
- minBitsPerChar: 4.5
44961
+ minBitsPerChar: 4.5,
44962
+ excludePrefixes: DEFAULT_EXCLUDE_PREFIXES
44600
44963
  };
44601
44964
  function shannonEntropy(s) {
44602
44965
  if (s.length === 0)
@@ -44629,9 +44992,13 @@ function findEntropyMatches(text, opts = {}) {
44629
44992
  const end = i;
44630
44993
  const token = text.slice(start, end);
44631
44994
  if (token.length >= cfg.minLength) {
44632
- const h2 = shannonEntropy(token);
44633
- if (h2 >= cfg.minBitsPerChar) {
44634
- 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
+ }
44635
45002
  }
44636
45003
  }
44637
45004
  }
@@ -44775,7 +45142,19 @@ var ENV_ALLOWLIST = [
44775
45142
  "PWD",
44776
45143
  "OLDPWD",
44777
45144
  "TMPDIR",
44778
- "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"
44779
45158
  ];
44780
45159
  function spawnPrivileged(opts) {
44781
45160
  const startedAt = Date.now();
@@ -44788,7 +45167,32 @@ function spawnPrivileged(opts) {
44788
45167
  for (const [k2, v2] of Object.entries(opts.injectedEnv)) env2[k2] = v2;
44789
45168
  }
44790
45169
  const stdio = ["inherit", "pipe", "pipe"];
44791
- 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, {
44792
45196
  env: env2,
44793
45197
  stdio,
44794
45198
  detached: false
@@ -44855,10 +45259,7 @@ to load a specific file, or set KEYNV_ENV_FILE in the environment.
44855
45259
  spelled \`--from\` to avoid the collision.)
44856
45260
  `,
44857
45261
  examples: [
44858
- [
44859
- "Auto-load .keynv.env from cwd or parents",
44860
- "$0 exec -- next dev"
44861
- ],
45262
+ ["Auto-load .keynv.env from cwd or parents", "$0 exec -- next dev"],
44862
45263
  [
44863
45264
  "Run mysql with the alias substituted at fork-exec time",
44864
45265
  "$0 exec -- mysql -p@billing.dev.db_pass -h db.example.com"
@@ -44922,7 +45323,18 @@ spelled \`--from\` to avoid the collision.)
44922
45323
  `);
44923
45324
  return 2;
44924
45325
  }
44925
- 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
+ );
44926
45338
  }
44927
45339
  const viaEnvSpecs = [];
44928
45340
  for (const spec of this.viaEnv ?? []) {
@@ -44994,11 +45406,18 @@ spelled \`--from\` to avoid the collision.)
44994
45406
  injectedEnv[spec.name] = value;
44995
45407
  }
44996
45408
  if (envFileLoaded && !this.quiet) {
44997
- const total = envFileLoaded.entries.length;
44998
- 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
+ }
44999
45419
  this.context.stderr.write(
45000
- `keynv: loaded ${total} var${total === 1 ? "" : "s"} from ${envFileLoaded.path} (${aliasCount} resolved from vault)
45001
- `
45420
+ `keynv: loaded ${displayPath}` + (parts.length > 0 ? ` \u2014 ${parts.join("; ")}` : "") + "\n"
45002
45421
  );
45003
45422
  }
45004
45423
  const timeoutS = this.timeout ? Number.parseInt(this.timeout, 10) : void 0;
@@ -45038,10 +45457,158 @@ function signalNumber(sig) {
45038
45457
  }
45039
45458
 
45040
45459
  // src/commands/init.ts
45460
+ init_dist();
45041
45461
  init_http();
45042
- init_tty();
45462
+ init_envFile();
45463
+ init_heuristics();
45043
45464
  init_init();
45044
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
45045
45612
  var InitCommand = class extends Command {
45046
45613
  static paths = [["init"]];
45047
45614
  static usage = Command.Usage({
@@ -45055,6 +45622,11 @@ package.json scripts with \`keynv exec\`.
45055
45622
  Safe to re-run: existing .keynv.env entries are preserved; new
45056
45623
  entries are appended below a marker.
45057
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
+
45058
45630
  Requires an interactive terminal (clack TUI). For scripted
45059
45631
  migration, use the lower-level \`keynv project\` and \`keynv secret\`
45060
45632
  commands directly.
@@ -45062,28 +45634,47 @@ commands directly.
45062
45634
  examples: [
45063
45635
  ["Walk the current project", "$0 init"],
45064
45636
  ["Preview without writing or uploading", "$0 init --dry-run"],
45065
- ["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"]
45066
45639
  ]
45067
45640
  });
45068
45641
  dryRun = options_exports.Boolean("--dry-run", false, {
45069
- 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."
45070
45643
  });
45071
45644
  noScripts = options_exports.Boolean("--no-scripts", false, {
45072
45645
  description: "Skip the package.json script-wrapping step."
45073
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
+ });
45074
45659
  async execute() {
45075
- if (!isInteractive()) {
45076
- this.context.stderr.write(
45077
- "keynv init requires an interactive terminal. Use the lower-level commands (`keynv project`, `keynv secret`) for scripted setup.\n"
45078
- );
45079
- return 1;
45080
- }
45081
45660
  const client = new ApiClient();
45082
45661
  await client.ensureHydrated();
45083
45662
  if (!client.isLoggedIn) {
45084
45663
  this.context.stderr.write("keynv: not logged in. Run `keynv login` first.\n");
45085
45664
  return 1;
45086
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
+ }
45087
45678
  try {
45088
45679
  const outcome = await runInitFlow(client, {
45089
45680
  cwd: process.cwd(),
@@ -45099,9 +45690,102 @@ commands directly.
45099
45690
  return 1;
45100
45691
  }
45101
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
+ }
45102
45784
  };
45103
45785
 
45104
45786
  // src/commands/login.ts
45787
+ init_browser_auth();
45788
+ init_defaults();
45105
45789
  init_http();
45106
45790
  init_store();
45107
45791
  init_format();
@@ -45162,25 +45846,63 @@ var LoginCommand = class extends Command {
45162
45846
  static usage = Command.Usage({
45163
45847
  description: "Authenticate against a keynv server.",
45164
45848
  examples: [
45165
- ["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_..."],
45166
45852
  ["Non-interactive", "$0 login --server http://... --email a@b.com --password ..."]
45167
45853
  ]
45168
45854
  });
45169
45855
  server = options_exports.String("--server", { description: "Server base URL." });
45856
+ token = options_exports.String("--token", { description: "CLI token for headless auth." });
45170
45857
  email = options_exports.String("--email", { description: "Email address." });
45171
45858
  password = options_exports.String("--password", {
45172
45859
  description: "Password (use stdin to avoid argv leak)."
45173
45860
  });
45174
45861
  async execute() {
45175
- const serverUrl = this.server ?? await promptLine("server URL [http://localhost:8080]: ") ?? "";
45176
- 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
+ }
45177
45890
  const email2 = this.email ?? await promptLine("email: ");
45178
45891
  const password = this.password ?? await promptHidden("password: ");
45179
- const res = await fetch(new URL("/v1/auth/login", finalServerUrl).toString(), {
45180
- method: "POST",
45181
- headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
45182
- body: JSON.stringify({ email: email2, password })
45183
- });
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
+ }
45184
45906
  if (!res.ok) {
45185
45907
  const text = await res.text();
45186
45908
  let msg = `login failed (${res.status})`;
@@ -45204,6 +45926,7 @@ var LoginCommand = class extends Command {
45204
45926
  const data = await res.json();
45205
45927
  try {
45206
45928
  await saveCredentials({
45929
+ auth_kind: "session",
45207
45930
  server_url: finalServerUrl,
45208
45931
  user_id: data.user.id,
45209
45932
  email: data.user.email,
@@ -45221,6 +45944,63 @@ var LoginCommand = class extends Command {
45221
45944
  return 1;
45222
45945
  }
45223
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})
45224
46004
  `);
45225
46005
  return 0;
45226
46006
  }
@@ -45257,48 +46037,46 @@ var WhoamiCommand = class extends Command {
45257
46037
  });
45258
46038
  json = options_exports.Boolean("--json", false);
45259
46039
  async execute() {
45260
- const client = new ApiClient();
45261
- await client.ensureHydrated();
45262
- if (!client.isLoggedIn) {
45263
- this.context.stderr.write("keynv: not logged in. Run `keynv login`.\n");
45264
- return 1;
45265
- }
45266
- const data = await client.request("/v1/whoami");
45267
- if (this.json) {
45268
- 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)}
45269
46050
  `);
45270
- return 0;
45271
- }
45272
- this.context.stdout.write(`user: ${data.email} (${data.id})
46051
+ return 0;
46052
+ }
46053
+ this.context.stdout.write(`user: ${data.email} (${data.id})
45273
46054
  `);
45274
- 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}
45275
46057
  `);
45276
- this.context.stdout.write(`role: ${data.org_role}
46058
+ this.context.stdout.write(`role: ${data.org_role}
45277
46059
  `);
45278
- if (data.memberships.length === 0) {
45279
- this.context.stdout.write("memberships: (none)\n");
45280
- } else {
45281
- this.context.stdout.write("memberships:\n");
45282
- for (const m of data.memberships) {
45283
- 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}
45284
46067
  `);
46068
+ }
45285
46069
  }
46070
+ return 0;
46071
+ } catch (err) {
46072
+ return handleExecError(this.context.stderr, err);
45286
46073
  }
45287
- return 0;
45288
46074
  }
45289
46075
  };
45290
46076
 
45291
46077
  // src/commands/member.ts
45292
46078
  init_http();
45293
46079
  init_format();
45294
- async function findProjectIdByName(client, name) {
45295
- const data = await client.request(
45296
- "/v1/projects"
45297
- );
45298
- const match = data.projects.find((p2) => p2.name === name);
45299
- if (!match) throw new Error(`unknown project: ${name}`);
45300
- return match.id;
45301
- }
45302
46080
  var MemberAddCommand = class extends Command {
45303
46081
  static paths = [["member", "add"]];
45304
46082
  static usage = Command.Usage({
@@ -45314,15 +46092,19 @@ var MemberAddCommand = class extends Command {
45314
46092
  this.context.stderr.write("keynv: --role must be one of lead, developer, reader\n");
45315
46093
  return 1;
45316
46094
  }
45317
- const client = new ApiClient();
45318
- const projectId2 = await findProjectIdByName(client, this.project);
45319
- await client.request(`/v1/projects/${projectId2}/members`, {
45320
- method: "POST",
45321
- body: { email: this.email, role: role2 }
45322
- });
45323
- 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}
45324
46103
  `);
45325
- return 0;
46104
+ return 0;
46105
+ } catch (err) {
46106
+ return handleExecError(this.context.stderr, err);
46107
+ }
45326
46108
  }
45327
46109
  };
45328
46110
  var MemberRemoveCommand = class extends Command {
@@ -45331,21 +46113,25 @@ var MemberRemoveCommand = class extends Command {
45331
46113
  project = options_exports.String();
45332
46114
  email = options_exports.String();
45333
46115
  async execute() {
45334
- const client = new ApiClient();
45335
- const projectId2 = await findProjectIdByName(client, this.project);
45336
- const members = await client.request(`/v1/projects/${projectId2}/members`);
45337
- const target = members.members.find((m) => m.email === this.email);
45338
- if (!target) {
45339
- 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}
45340
46123
  `);
45341
- return 1;
45342
- }
45343
- await client.request(`/v1/projects/${projectId2}/members/${target.user_id}`, {
45344
- method: "DELETE"
45345
- });
45346
- 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}
45347
46130
  `);
45348
- return 0;
46131
+ return 0;
46132
+ } catch (err) {
46133
+ return handleExecError(this.context.stderr, err);
46134
+ }
45349
46135
  }
45350
46136
  };
45351
46137
  var MemberListCommand = class extends Command {
@@ -45354,134 +46140,30 @@ var MemberListCommand = class extends Command {
45354
46140
  project = options_exports.String();
45355
46141
  json = options_exports.Boolean("--json", false);
45356
46142
  async execute() {
45357
- const client = new ApiClient();
45358
- const projectId2 = await findProjectIdByName(client, this.project);
45359
- const data = await client.request(`/v1/projects/${projectId2}/members`);
45360
- if (this.json) {
45361
- this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45362
- `);
45363
- return 0;
45364
- }
45365
- if (data.members.length === 0) {
45366
- this.context.stdout.write("no members\n");
45367
- return 0;
45368
- }
45369
- this.context.stdout.write(
45370
- `${table(
45371
- ["email", "role", "granted_at"],
45372
- data.members.map((m) => [m.email, m.role, m.granted_at])
45373
- )}
45374
- `
45375
- );
45376
- return 0;
45377
- }
45378
- };
45379
-
45380
- // src/commands/project.ts
45381
- init_http();
45382
- init_format();
45383
- var ProjectListCommand = class extends Command {
45384
- static paths = [["project", "list"]];
45385
- static usage = Command.Usage({
45386
- description: "List projects visible to the current user."
45387
- });
45388
- json = options_exports.Boolean("--json", false);
45389
- async execute() {
45390
- const client = new ApiClient();
45391
- const data = await client.request("/v1/projects");
45392
- if (this.json) {
45393
- this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45394
- `);
45395
- return 0;
45396
- }
45397
- if (data.projects.length === 0) {
45398
- this.context.stdout.write("no projects\n");
45399
- return 0;
45400
- }
45401
- this.context.stdout.write(
45402
- `${table(
45403
- ["name", "id", "created_at"],
45404
- data.projects.map((p2) => [p2.name, p2.id, p2.created_at])
45405
- )}
45406
- `
45407
- );
45408
- return 0;
45409
- }
45410
- };
45411
- var ProjectCreateCommand = class extends Command {
45412
- static paths = [["project", "create"]];
45413
- static usage = Command.Usage({
45414
- description: "Create a new project with one or more environments.",
45415
- examples: [["Two envs", "$0 project create billing --env dev --env prod:production:approval"]]
45416
- });
45417
- name = options_exports.String();
45418
- envs = options_exports.Array("--env", {
45419
- description: 'Environment spec: name[:tier[:approval]]. Tier \u2208 {production,non-production}; "approval" sets require_approval=true.'
45420
- });
45421
- async execute() {
45422
- const envs = (this.envs ?? ["dev"]).map((spec) => {
45423
- const [name, tier, approval] = spec.split(":");
45424
- return {
45425
- name: name ?? "",
45426
- tier: tier ?? "non-production",
45427
- require_approval: approval === "approval"
45428
- };
45429
- });
45430
- const client = new ApiClient();
45431
- const result = await client.request("/v1/projects", {
45432
- method: "POST",
45433
- body: { name: this.name, environments: envs }
45434
- });
45435
- 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)}
45436
46149
  `);
45437
- 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
+ }
45438
46156
  this.context.stdout.write(
45439
- ` 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
+ )}
45440
46161
  `
45441
46162
  );
45442
- }
45443
- return 0;
45444
- }
45445
- };
45446
- var ProjectDescribeCommand = class extends Command {
45447
- static paths = [["project", "describe"]];
45448
- static usage = Command.Usage({ description: "Show metadata for one project." });
45449
- id = options_exports.String();
45450
- json = options_exports.Boolean("--json", false);
45451
- async execute() {
45452
- const client = new ApiClient();
45453
- const data = await client.request(`/v1/projects/${this.id}`);
45454
- if (this.json) {
45455
- this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45456
- `);
45457
46163
  return 0;
46164
+ } catch (err) {
46165
+ return handleExecError(this.context.stderr, err);
45458
46166
  }
45459
- this.context.stdout.write(`project: ${data.name} (${data.id})
45460
- `);
45461
- for (const e2 of data.environments) {
45462
- this.context.stdout.write(
45463
- ` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
45464
- `
45465
- );
45466
- }
45467
- return 0;
45468
- }
45469
- };
45470
- var ProjectDeleteCommand = class extends Command {
45471
- static paths = [["project", "delete"]];
45472
- static usage = Command.Usage({ description: "Soft-delete a project." });
45473
- id = options_exports.String();
45474
- force = options_exports.Boolean("--force", false);
45475
- async execute() {
45476
- if (!this.force) {
45477
- this.context.stderr.write("keynv: refusing to delete without --force\n");
45478
- return 2;
45479
- }
45480
- const client = new ApiClient();
45481
- await client.request(`/v1/projects/${this.id}`, { method: "DELETE" });
45482
- this.context.stdout.write(`deleted project ${this.id}
45483
- `);
45484
- return 0;
45485
46167
  }
45486
46168
  };
45487
46169
  var RedactCommand = class extends Command {
@@ -45526,11 +46208,9 @@ var RedactStreamCommand = class extends Command {
45526
46208
  static usage = Command.Usage({
45527
46209
  description: "Stream stdin through the line-buffered redactor to stdout.",
45528
46210
  details: `
45529
- Used as a hook handler by per-agent integrations (e.g., Claude Code's
45530
- PostToolUse hook): \`keynv redact-stream\` reads its tool output on
45531
- stdin and writes a redacted version on stdout, preserving the original
45532
- line structure. Multi-line patterns are NOT applied here (see the
45533
- 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).
45534
46214
  `
45535
46215
  });
45536
46216
  async execute() {
@@ -45545,18 +46225,13 @@ streaming-mode limitation in @keynv/redactor).
45545
46225
  // src/commands/secret.ts
45546
46226
  init_dist();
45547
46227
  init_http();
46228
+ init_secret();
45548
46229
  init_format();
45549
- init_tty();
45550
46230
  init_cancel();
45551
46231
  init_pickProject();
45552
46232
  init_pickSecret();
45553
- init_secret();
45554
- async function findProjectIdByName2(client, name) {
45555
- const data = await client.request("/v1/projects");
45556
- const match = data.projects.find((p2) => p2.name === name);
45557
- if (!match) throw new Error(`unknown project: ${name}`);
45558
- return match.id;
45559
- }
46233
+ init_tty();
46234
+ var ALIAS_FORMAT_HINT = "Format: @<project>.<env>.<KEY> (project/env: lowercase kebab-case; key: letters, digits, _ or -)";
45560
46235
  function missingAlias(stderr) {
45561
46236
  stderr.write("keynv: missing <alias> (TTY required for interactive prompt).\n");
45562
46237
  return 1;
@@ -45587,45 +46262,50 @@ var SecretCreateCommand = class extends Command {
45587
46262
  });
45588
46263
  stdin = options_exports.Boolean("--stdin", false);
45589
46264
  async execute() {
45590
- const client = new ApiClient();
45591
- await client.ensureHydrated();
45592
- let alias2 = this.alias;
45593
- let value = this.value;
45594
- if (!alias2) {
45595
- if (!isInteractive()) return missingAlias(this.context.stderr);
45596
- try {
45597
- const built = await promptNewSecret(client);
45598
- if (!built) return 1;
45599
- alias2 = built.alias;
45600
- value = built.value;
45601
- } catch (err) {
45602
- if (err instanceof UserCancelled) return 130;
45603
- 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
+ }
45604
46281
  }
45605
- }
45606
- const parsed = parseAlias(alias2);
45607
- if (!parsed) {
45608
- 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}
45609
46286
  `);
45610
- return 1;
45611
- }
45612
- if (this.stdin) {
45613
- const chunks = [];
45614
- for await (const chunk of this.context.stdin) {
45615
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
46287
+ return 1;
45616
46288
  }
45617
- value = Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
45618
- } else if (value === void 0) {
45619
- value = await promptHidden("value: ");
45620
- }
45621
- const projectId2 = await findProjectIdByName2(client, parsed.project);
45622
- await client.request(`/v1/projects/${projectId2}/secrets`, {
45623
- method: "POST",
45624
- body: { env: parsed.environment, key: parsed.key, value }
45625
- });
45626
- 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}
45627
46304
  `);
45628
- return 0;
46305
+ return 0;
46306
+ } catch (err) {
46307
+ return handleExecError(this.context.stderr, err);
46308
+ }
45629
46309
  }
45630
46310
  };
45631
46311
  var SecretGetCommand = class extends Command {
@@ -45636,39 +46316,44 @@ var SecretGetCommand = class extends Command {
45636
46316
  alias = options_exports.String({ required: false });
45637
46317
  json = options_exports.Boolean("--json", false);
45638
46318
  async execute() {
45639
- const client = new ApiClient();
45640
- await client.ensureHydrated();
45641
- let alias2 = this.alias;
45642
- if (!alias2) {
45643
- if (!isInteractive()) return missingAlias(this.context.stderr);
45644
- try {
45645
- alias2 = await pickAliasInteractive(client) ?? void 0;
45646
- } catch (err) {
45647
- if (err instanceof UserCancelled) return 130;
45648
- 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;
45649
46332
  }
45650
- if (!alias2) return 1;
45651
- }
45652
- const parsed = parseAlias(alias2);
45653
- if (!parsed) {
45654
- 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}
45655
46337
  `);
45656
- return 1;
45657
- }
45658
- const projectId2 = await findProjectIdByName2(client, parsed.project);
45659
- const data = await client.request(
45660
- `/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}`
45661
- );
45662
- if (this.json) {
45663
- this.context.stdout.write(
45664
- `${JSON.stringify({ alias: data.alias, version: data.version, value: data.value })}
45665
- `
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}`
45666
46343
  );
45667
- } else {
45668
- 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}
45669
46351
  `);
46352
+ }
46353
+ return 0;
46354
+ } catch (err) {
46355
+ return handleExecError(this.context.stderr, err);
45670
46356
  }
45671
- return 0;
45672
46357
  }
45673
46358
  };
45674
46359
  var SecretListCommand = class extends Command {
@@ -45679,42 +46364,47 @@ var SecretListCommand = class extends Command {
45679
46364
  project = options_exports.String({ required: false });
45680
46365
  json = options_exports.Boolean("--json", false);
45681
46366
  async execute() {
45682
- const client = new ApiClient();
45683
- await client.ensureHydrated();
45684
- let projectName = this.project;
45685
- if (!projectName) {
45686
- if (!isInteractive()) {
45687
- this.context.stderr.write("keynv: missing <project>.\n");
45688
- return 1;
45689
- }
45690
- try {
45691
- const picked = await pickProject(client, "Project");
45692
- if (!picked) return 1;
45693
- projectName = picked.name;
45694
- } catch (err) {
45695
- if (err instanceof UserCancelled) return 130;
45696
- 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
+ }
45697
46384
  }
45698
- }
45699
- const projectId2 = await findProjectIdByName2(client, projectName);
45700
- const data = await client.request(`/v1/projects/${projectId2}/secrets`);
45701
- if (this.json) {
45702
- 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)}
45703
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
+ );
45704
46404
  return 0;
46405
+ } catch (err) {
46406
+ return handleExecError(this.context.stderr, err);
45705
46407
  }
45706
- if (data.secrets.length === 0) {
45707
- this.context.stdout.write("no secrets\n");
45708
- return 0;
45709
- }
45710
- this.context.stdout.write(
45711
- `${table(
45712
- ["alias", "version", "created_at"],
45713
- data.secrets.map((s) => [s.alias, String(s.version), s.created_at])
45714
- )}
45715
- `
45716
- );
45717
- return 0;
45718
46408
  }
45719
46409
  };
45720
46410
  var SecretRotateCommand = class extends Command {
@@ -45724,45 +46414,50 @@ var SecretRotateCommand = class extends Command {
45724
46414
  value = options_exports.String("--value");
45725
46415
  stdin = options_exports.Boolean("--stdin", false);
45726
46416
  async execute() {
45727
- const client = new ApiClient();
45728
- await client.ensureHydrated();
45729
- let alias2 = this.alias;
45730
- if (!alias2) {
45731
- if (!isInteractive()) return missingAlias(this.context.stderr);
45732
- try {
45733
- alias2 = await pickAliasInteractive(client) ?? void 0;
45734
- } catch (err) {
45735
- if (err instanceof UserCancelled) return 130;
45736
- 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;
45737
46430
  }
45738
- if (!alias2) return 1;
45739
- }
45740
- const parsed = parseAlias(alias2);
45741
- if (!parsed) {
45742
- 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}
45743
46435
  `);
45744
- return 1;
45745
- }
45746
- let value;
45747
- if (this.stdin) {
45748
- const chunks = [];
45749
- for await (const chunk of this.context.stdin) {
45750
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
46436
+ return 1;
45751
46437
  }
45752
- value = Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
45753
- } else if (this.value !== void 0) {
45754
- value = this.value;
45755
- } else {
45756
- value = await promptHidden("new value: ");
45757
- }
45758
- const projectId2 = await findProjectIdByName2(client, parsed.project);
45759
- const data = await client.request(
45760
- `/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}/rotate`,
45761
- { method: "POST", body: { new_value: value } }
45762
- );
45763
- 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}
45764
46456
  `);
45765
- return 0;
46457
+ return 0;
46458
+ } catch (err) {
46459
+ return handleExecError(this.context.stderr, err);
46460
+ }
45766
46461
  }
45767
46462
  };
45768
46463
  var SecretDeleteCommand = class extends Command {
@@ -45770,31 +46465,170 @@ var SecretDeleteCommand = class extends Command {
45770
46465
  static usage = Command.Usage({ description: "Soft-delete a secret." });
45771
46466
  alias = options_exports.String({ required: false });
45772
46467
  async execute() {
45773
- const client = new ApiClient();
45774
- await client.ensureHydrated();
45775
- let alias2 = this.alias;
45776
- if (!alias2) {
45777
- if (!isInteractive()) return missingAlias(this.context.stderr);
45778
- try {
45779
- alias2 = await pickAliasInteractive(client) ?? void 0;
45780
- } catch (err) {
45781
- if (err instanceof UserCancelled) return 130;
45782
- 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;
45783
46481
  }
45784
- if (!alias2) return 1;
45785
- }
45786
- const parsed = parseAlias(alias2);
45787
- if (!parsed) {
45788
- 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}
45789
46486
  `);
45790
- 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);
45791
46498
  }
45792
- const projectId2 = await findProjectIdByName2(client, parsed.project);
45793
- await client.request(`/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}`, {
45794
- method: "DELETE"
45795
- });
45796
- 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}
46562
+ `);
46563
+ this.context.stdout.write(`Wrote ${outPath}
46564
+
45797
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
+ }
45798
46632
  return 0;
45799
46633
  }
45800
46634
  };
@@ -46150,18 +46984,9 @@ var TESTERS = [
46150
46984
  sshTester,
46151
46985
  httpTester
46152
46986
  ];
46153
- function findTester(type) {
46154
- return TESTERS.find((t) => t.type === type) ?? null;
46155
- }
46156
46987
 
46157
46988
  // src/commands/test.ts
46158
46989
  init_http();
46159
- async function findProjectIdByName3(client, name) {
46160
- const data = await client.request("/v1/projects");
46161
- const match = data.projects.find((p2) => p2.name === name);
46162
- if (!match) throw new Error(`unknown project: ${name}`);
46163
- return match.id;
46164
- }
46165
46990
  function parseTargets(specs) {
46166
46991
  const out = {};
46167
46992
  for (const spec of specs) {
@@ -46215,12 +47040,17 @@ the error message.
46215
47040
  );
46216
47041
  return 2;
46217
47042
  }
46218
- const tester = findTester(this.as);
47043
+ const tester = TESTERS.find((t) => t.type === this.as);
46219
47044
  if (!tester) {
46220
47045
  this.context.stderr.write(`keynv: unknown tester '${this.as}'
46221
47046
  `);
46222
47047
  return 1;
46223
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
+ }
46224
47054
  let target;
46225
47055
  try {
46226
47056
  target = parseTargets(this.targets ?? []);
@@ -46237,7 +47067,7 @@ the error message.
46237
47067
  }
46238
47068
  let value;
46239
47069
  try {
46240
- const projectId2 = await findProjectIdByName3(client, parsedAlias.project);
47070
+ const projectId2 = await resolveProjectId(client, parsedAlias.project);
46241
47071
  const data = await client.request(
46242
47072
  `/v1/projects/${projectId2}/secrets/${parsedAlias.environment}/${parsedAlias.key}`
46243
47073
  );
@@ -46248,11 +47078,6 @@ the error message.
46248
47078
  `);
46249
47079
  return 1;
46250
47080
  }
46251
- const timeoutMs = this.timeout ? Number.parseInt(this.timeout, 10) * 1e3 : void 0;
46252
- if (this.timeout && (timeoutMs === void 0 || Number.isNaN(timeoutMs))) {
46253
- this.context.stderr.write("keynv: invalid --timeout (expected integer seconds)\n");
46254
- return 2;
46255
- }
46256
47081
  const result = await runTest({
46257
47082
  tester,
46258
47083
  secret: { alias: parsedAlias.literal, value },
@@ -46294,25 +47119,6 @@ the error message.
46294
47119
  }
46295
47120
  };
46296
47121
 
46297
- // src/commands/ui.ts
46298
- init_menu();
46299
- init_tty();
46300
- var UICommand = class extends Command {
46301
- static paths = [["ui"]];
46302
- static usage = Command.Usage({
46303
- description: "Open the interactive menu (also runs by default when no args are given)."
46304
- });
46305
- async execute() {
46306
- if (!isInteractive()) {
46307
- this.context.stdout.write(
46308
- "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"
46309
- );
46310
- return 0;
46311
- }
46312
- return runMenu();
46313
- }
46314
- };
46315
-
46316
47122
  // src/index.ts
46317
47123
  init_format();
46318
47124
  init_version();
@@ -46346,7 +47152,7 @@ cli.register(InitCommand);
46346
47152
  cli.register(RedactCommand);
46347
47153
  cli.register(RedactStreamCommand);
46348
47154
  cli.register(TestCommand);
46349
- cli.register(UICommand);
47155
+ cli.register(ServerInitCommand);
46350
47156
  var argv = process.argv.slice(2);
46351
47157
  if (argv.length === 0) {
46352
47158
  const { runMenu: runMenu2 } = await Promise.resolve().then(() => (init_menu(), menu_exports));