@keynv/cli 0.1.0-rc.11 → 0.1.0-rc.13

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
5
  import { hostname, platform, homedir } from 'os';
6
- import { isAbsolute, resolve, join, dirname, basename } from 'path';
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';
@@ -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.11" ;
1059
+ VERSION = "0.1.0-rc.13" ;
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
  }
@@ -5536,7 +5536,15 @@ function clearCredentialsFile() {
5536
5536
  if (existsSync(legacy)) rmSync(legacy, { force: true });
5537
5537
  try {
5538
5538
  entry().deletePassword();
5539
- } catch {
5539
+ return true;
5540
+ } catch (err) {
5541
+ process.stderr.write(
5542
+ `keynv: warning \u2014 could not remove OS keychain entry (${err instanceof Error ? err.message : String(err)}).
5543
+ You may need to remove the '${SERVICE}' / '${KEY_ACCOUNT}' entry manually
5544
+ via your OS credential manager (Windows Credential Manager, macOS Keychain, or libsecret).
5545
+ `
5546
+ );
5547
+ return false;
5540
5548
  }
5541
5549
  }
5542
5550
  var SERVICE, KEY_ACCOUNT;
@@ -5579,8 +5587,9 @@ async function saveCredentials(creds) {
5579
5587
  cache = creds;
5580
5588
  }
5581
5589
  function clearCredentials() {
5582
- clearCredentialsFile();
5590
+ const ok = clearCredentialsFile();
5583
5591
  cache = null;
5592
+ return ok;
5584
5593
  }
5585
5594
  var cache;
5586
5595
  var init_store = __esm({
@@ -5590,6 +5599,9 @@ var init_store = __esm({
5590
5599
  });
5591
5600
 
5592
5601
  // src/client/http.ts
5602
+ function isClientError(err) {
5603
+ return err instanceof Error && typeof err.status === "number";
5604
+ }
5593
5605
  function clientError(status, code, message, details) {
5594
5606
  const err = Object.assign(new Error(message), { status, code, details });
5595
5607
  return err;
@@ -5719,8 +5731,38 @@ function fmtError(err) {
5719
5731
  const status = err.status ? ` (${err.status})` : "";
5720
5732
  return `keynv:${code}${status} ${err.message}`;
5721
5733
  }
5734
+ function handleExecError(stderr, err) {
5735
+ if (isClientError(err)) {
5736
+ const code = err.code ? `[${err.code}] ` : "";
5737
+ stderr.write(`keynv: ${code}${err.message}
5738
+ `);
5739
+ } else if (err instanceof Error) {
5740
+ stderr.write(`keynv: ${err.message}
5741
+ `);
5742
+ } else {
5743
+ stderr.write(`keynv: unexpected error
5744
+ `);
5745
+ }
5746
+ return 1;
5747
+ }
5722
5748
  var init_format = __esm({
5723
5749
  "src/ui/format.ts"() {
5750
+ init_http();
5751
+ }
5752
+ });
5753
+ function walkUp(startDir, predicate) {
5754
+ let dir = resolve(startDir);
5755
+ for (let i = 0; i < 64; i++) {
5756
+ const result = predicate(dir);
5757
+ if (result !== null && result !== void 0) return result;
5758
+ const parent = dirname(dir);
5759
+ if (parent === dir) return null;
5760
+ dir = parent;
5761
+ }
5762
+ return null;
5763
+ }
5764
+ var init_fs = __esm({
5765
+ "src/util/fs.ts"() {
5724
5766
  }
5725
5767
  });
5726
5768
  function parseEnvFile(content, filename) {
@@ -5781,20 +5823,14 @@ function parseEnvFile(content, filename) {
5781
5823
  return entries;
5782
5824
  }
5783
5825
  function findEnvFile(startDir) {
5784
- let dir = resolve(startDir);
5785
- for (let i = 0; i < 64; i++) {
5826
+ return walkUp(startDir, (dir) => {
5786
5827
  const candidate = join(dir, ENV_FILE_BASENAME);
5787
- if (existsSync(candidate)) {
5788
- try {
5789
- if (statSync(candidate).isFile()) return candidate;
5790
- } catch {
5791
- }
5828
+ try {
5829
+ if (statSync(candidate).isFile()) return candidate;
5830
+ } catch {
5792
5831
  }
5793
- const parent = dirname(dir);
5794
- if (parent === dir) return null;
5795
- dir = parent;
5796
- }
5797
- return null;
5832
+ return null;
5833
+ });
5798
5834
  }
5799
5835
  function loadEnvFile(opts) {
5800
5836
  if (opts.disabled) return null;
@@ -5819,6 +5855,7 @@ var MAX_FILE_BYTES, KEY_RE2, ENV_FILE_BASENAME, EnvFileParseError, EnvFileNotFou
5819
5855
  var init_envFile = __esm({
5820
5856
  "src/exec/envFile.ts"() {
5821
5857
  init_dist();
5858
+ init_fs();
5822
5859
  MAX_FILE_BYTES = 1e6;
5823
5860
  KEY_RE2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
5824
5861
  ENV_FILE_BASENAME = ".keynv.env";
@@ -5855,6 +5892,132 @@ var init_envFile = __esm({
5855
5892
  }
5856
5893
  });
5857
5894
 
5895
+ // src/init/heuristics.ts
5896
+ function shannonEntropyBits(s) {
5897
+ if (s.length === 0) return 0;
5898
+ const counts = /* @__PURE__ */ new Map();
5899
+ for (const ch of s) counts.set(ch, (counts.get(ch) ?? 0) + 1);
5900
+ let h2 = 0;
5901
+ for (const c2 of counts.values()) {
5902
+ const p2 = c2 / s.length;
5903
+ h2 -= p2 * Math.log2(p2);
5904
+ }
5905
+ return h2;
5906
+ }
5907
+ function nameMatchesLiteralPrefix(name) {
5908
+ const upper = name.toUpperCase();
5909
+ return NAME_LITERAL_PREFIX.some((p2) => upper.startsWith(p2));
5910
+ }
5911
+ function nameMatchesSecretSuffix(name) {
5912
+ const upper = name.toUpperCase();
5913
+ for (const { suffix, hint } of NAME_SECRET_SUFFIXES) {
5914
+ if (upper.endsWith(suffix) || upper.endsWith(`_${suffix}`)) {
5915
+ return { matched: true, hint };
5916
+ }
5917
+ }
5918
+ return { matched: false, hint: "" };
5919
+ }
5920
+ function classifyEntry(name, value) {
5921
+ const upper = name.toUpperCase();
5922
+ if (NAME_FRAMEWORK_MANAGED.has(upper)) {
5923
+ return { verdict: "skip", hint: "framework/shell-managed" };
5924
+ }
5925
+ if (NAME_LITERAL_EXACT.has(upper)) {
5926
+ return { verdict: "literal", hint: "common config var" };
5927
+ }
5928
+ if (nameMatchesLiteralPrefix(name)) {
5929
+ return { verdict: "literal", hint: "public env, build-time bundled" };
5930
+ }
5931
+ if (value.length === 0) {
5932
+ return { verdict: "literal", hint: "empty" };
5933
+ }
5934
+ for (const { re, hint } of VALUE_PATTERNS) {
5935
+ if (re.test(value)) return { verdict: "secret", hint };
5936
+ }
5937
+ const suffix = nameMatchesSecretSuffix(name);
5938
+ if (suffix.matched) {
5939
+ return { verdict: "secret", hint: suffix.hint };
5940
+ }
5941
+ if (NAME_DB_URL.test(name)) {
5942
+ return { verdict: "secret", hint: "database URL" };
5943
+ }
5944
+ if (value.length >= 32) {
5945
+ const bits = shannonEntropyBits(value);
5946
+ if (bits >= 3.5) {
5947
+ return { verdict: "secret", hint: `${value.length}-char random-looking string` };
5948
+ }
5949
+ }
5950
+ return { verdict: "ambiguous", hint: "" };
5951
+ }
5952
+ function previewValue(value, max = 40) {
5953
+ if (value.length === 0) return "(empty)";
5954
+ if (value.length <= max) return value;
5955
+ return `${value.slice(0, max - 1)}\u2026`;
5956
+ }
5957
+ var NAME_FRAMEWORK_MANAGED, NAME_LITERAL_EXACT, NAME_LITERAL_PREFIX, NAME_SECRET_SUFFIXES, NAME_DB_URL, VALUE_PATTERNS;
5958
+ var init_heuristics = __esm({
5959
+ "src/init/heuristics.ts"() {
5960
+ NAME_FRAMEWORK_MANAGED = /* @__PURE__ */ new Set([
5961
+ "NODE_ENV",
5962
+ "PORT",
5963
+ "HOST",
5964
+ "HOSTNAME",
5965
+ "PATH",
5966
+ "HOME",
5967
+ "USER",
5968
+ "SHELL",
5969
+ "PWD",
5970
+ "CI"
5971
+ ]);
5972
+ NAME_LITERAL_EXACT = /* @__PURE__ */ new Set([
5973
+ "DEBUG",
5974
+ "LOG_LEVEL",
5975
+ "LOGLEVEL",
5976
+ "TZ",
5977
+ "LANG",
5978
+ "LC_ALL",
5979
+ "NODE_OPTIONS",
5980
+ "NPM_CONFIG_LOGLEVEL",
5981
+ "TS_NODE_PROJECT"
5982
+ ]);
5983
+ NAME_LITERAL_PREFIX = ["NEXT_PUBLIC_", "VITE_", "REACT_APP_", "PUBLIC_", "EXPO_PUBLIC_"];
5984
+ NAME_SECRET_SUFFIXES = [
5985
+ { suffix: "PRIVATE_KEY", hint: "private key" },
5986
+ { suffix: "API_KEY", hint: "API key" },
5987
+ { suffix: "ACCESS_KEY", hint: "access key" },
5988
+ { suffix: "SECRET_KEY", hint: "secret key" },
5989
+ { suffix: "SECRET", hint: "secret" },
5990
+ { suffix: "PASSWORD", hint: "password" },
5991
+ { suffix: "PASSPHRASE", hint: "passphrase" },
5992
+ { suffix: "TOKEN", hint: "token" },
5993
+ { suffix: "KEY", hint: "key" },
5994
+ { suffix: "CREDENTIALS", hint: "credentials" },
5995
+ { suffix: "AUTH", hint: "auth" },
5996
+ { suffix: "DSN", hint: "connection string" }
5997
+ ];
5998
+ NAME_DB_URL = /^(DATABASE|DB|POSTGRES|MYSQL|MONGO|REDIS)_URL$/i;
5999
+ VALUE_PATTERNS = [
6000
+ { re: /^sk-proj-/, hint: "OpenAI project key" },
6001
+ { re: /^sk-[A-Za-z0-9]{20,}/, hint: "OpenAI key" },
6002
+ { re: /^sk_live_/, hint: "Stripe live key" },
6003
+ { re: /^sk_test_/, hint: "Stripe test key" },
6004
+ { re: /^pk_live_/, hint: "Stripe publishable \u2014 often public, double-check" },
6005
+ { re: /^xoxb-/, hint: "Slack bot token" },
6006
+ { re: /^xoxp-/, hint: "Slack user token" },
6007
+ { re: /^ghp_[A-Za-z0-9]{30,}/, hint: "GitHub personal access token" },
6008
+ { re: /^github_pat_/, hint: "GitHub fine-grained PAT" },
6009
+ { re: /^gho_/, hint: "GitHub OAuth token" },
6010
+ { re: /^AKIA[0-9A-Z]{16}$/, hint: "AWS access key id" },
6011
+ { re: /^ASIA[0-9A-Z]{16}$/, hint: "AWS temporary key id" },
6012
+ { re: /^AIza[0-9A-Za-z_-]{35}$/, hint: "Google API key" },
6013
+ { re: /^ya29\./, hint: "Google OAuth access token" },
6014
+ { re: /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, hint: "JWT" },
6015
+ { re: /^-----BEGIN [A-Z ]+PRIVATE KEY-----/, hint: "PEM private key" },
6016
+ { re: /^([a-z]+):\/\/[^@]+:[^@]+@/i, hint: "connection string with credentials" }
6017
+ ];
6018
+ }
6019
+ });
6020
+
5858
6021
  // ../../node_modules/.pnpm/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/utils.js
5859
6022
  var getCodePointsLength, isFullWidth, isWideNotCJKTNotEmoji;
5860
6023
  var init_utils = __esm({
@@ -7099,22 +7262,19 @@ var init_aiContext = __esm({
7099
7262
  }
7100
7263
  });
7101
7264
  function findProjectRoot(startDir) {
7102
- let dir = resolve(startDir);
7103
- for (let i = 0; i < 64; i++) {
7265
+ const result = walkUp(startDir, (dir) => {
7104
7266
  for (const marker of PROJECT_MARKERS) {
7105
- const candidate = join(dir, marker);
7106
- if (existsSync(candidate)) {
7107
- return buildRoot(dir, marker);
7267
+ if (existsSync(join(dir, marker))) {
7268
+ return { dir, marker };
7108
7269
  }
7109
7270
  }
7110
7271
  if (existsSync(join(dir, GIT_MARKER))) {
7111
- return buildRoot(dir, GIT_MARKER);
7272
+ return { dir, marker: GIT_MARKER };
7112
7273
  }
7113
- const parent = dirname(dir);
7114
- if (parent === dir) return null;
7115
- dir = parent;
7116
- }
7117
- return null;
7274
+ return null;
7275
+ });
7276
+ if (!result) return null;
7277
+ return buildRoot(result.dir, result.marker);
7118
7278
  }
7119
7279
  function buildRoot(dir, marker) {
7120
7280
  let suggestedName = basename(dir);
@@ -7199,6 +7359,7 @@ function hasExistingKeynvEnv(rootDir) {
7199
7359
  var PROJECT_MARKERS, GIT_MARKER, ENV_GLOB, ENV_EXAMPLE, KEYNV_ENV_BASENAME;
7200
7360
  var init_detect = __esm({
7201
7361
  "src/init/detect.ts"() {
7362
+ init_fs();
7202
7363
  PROJECT_MARKERS = [
7203
7364
  "package.json",
7204
7365
  "pyproject.toml",
@@ -7216,132 +7377,6 @@ var init_detect = __esm({
7216
7377
  }
7217
7378
  });
7218
7379
 
7219
- // src/init/heuristics.ts
7220
- function shannonEntropyBits(s) {
7221
- if (s.length === 0) return 0;
7222
- const counts = /* @__PURE__ */ new Map();
7223
- for (const ch of s) counts.set(ch, (counts.get(ch) ?? 0) + 1);
7224
- let h2 = 0;
7225
- for (const c2 of counts.values()) {
7226
- const p2 = c2 / s.length;
7227
- h2 -= p2 * Math.log2(p2);
7228
- }
7229
- return h2;
7230
- }
7231
- function nameMatchesLiteralPrefix(name) {
7232
- const upper = name.toUpperCase();
7233
- return NAME_LITERAL_PREFIX.some((p2) => upper.startsWith(p2));
7234
- }
7235
- function nameMatchesSecretSuffix(name) {
7236
- const upper = name.toUpperCase();
7237
- for (const { suffix, hint } of NAME_SECRET_SUFFIXES) {
7238
- if (upper.endsWith(suffix) || upper.endsWith(`_${suffix}`)) {
7239
- return { matched: true, hint };
7240
- }
7241
- }
7242
- return { matched: false, hint: "" };
7243
- }
7244
- function classifyEntry(name, value) {
7245
- const upper = name.toUpperCase();
7246
- if (NAME_FRAMEWORK_MANAGED.has(upper)) {
7247
- return { verdict: "skip", hint: "framework/shell-managed" };
7248
- }
7249
- if (NAME_LITERAL_EXACT.has(upper)) {
7250
- return { verdict: "literal", hint: "common config var" };
7251
- }
7252
- if (nameMatchesLiteralPrefix(name)) {
7253
- return { verdict: "literal", hint: "public env, build-time bundled" };
7254
- }
7255
- if (value.length === 0) {
7256
- return { verdict: "literal", hint: "empty" };
7257
- }
7258
- for (const { re, hint } of VALUE_PATTERNS) {
7259
- if (re.test(value)) return { verdict: "secret", hint };
7260
- }
7261
- const suffix = nameMatchesSecretSuffix(name);
7262
- if (suffix.matched) {
7263
- return { verdict: "secret", hint: suffix.hint };
7264
- }
7265
- if (NAME_DB_URL.test(name)) {
7266
- return { verdict: "secret", hint: "database URL" };
7267
- }
7268
- if (value.length >= 32) {
7269
- const bits = shannonEntropyBits(value);
7270
- if (bits >= 3.5) {
7271
- return { verdict: "secret", hint: `${value.length}-char random-looking string` };
7272
- }
7273
- }
7274
- return { verdict: "ambiguous", hint: "" };
7275
- }
7276
- function previewValue(value, max = 40) {
7277
- if (value.length === 0) return "(empty)";
7278
- if (value.length <= max) return value;
7279
- return `${value.slice(0, max - 1)}\u2026`;
7280
- }
7281
- var NAME_FRAMEWORK_MANAGED, NAME_LITERAL_EXACT, NAME_LITERAL_PREFIX, NAME_SECRET_SUFFIXES, NAME_DB_URL, VALUE_PATTERNS;
7282
- var init_heuristics = __esm({
7283
- "src/init/heuristics.ts"() {
7284
- NAME_FRAMEWORK_MANAGED = /* @__PURE__ */ new Set([
7285
- "NODE_ENV",
7286
- "PORT",
7287
- "HOST",
7288
- "HOSTNAME",
7289
- "PATH",
7290
- "HOME",
7291
- "USER",
7292
- "SHELL",
7293
- "PWD",
7294
- "CI"
7295
- ]);
7296
- NAME_LITERAL_EXACT = /* @__PURE__ */ new Set([
7297
- "DEBUG",
7298
- "LOG_LEVEL",
7299
- "LOGLEVEL",
7300
- "TZ",
7301
- "LANG",
7302
- "LC_ALL",
7303
- "NODE_OPTIONS",
7304
- "NPM_CONFIG_LOGLEVEL",
7305
- "TS_NODE_PROJECT"
7306
- ]);
7307
- NAME_LITERAL_PREFIX = ["NEXT_PUBLIC_", "VITE_", "REACT_APP_", "PUBLIC_", "EXPO_PUBLIC_"];
7308
- NAME_SECRET_SUFFIXES = [
7309
- { suffix: "PRIVATE_KEY", hint: "private key" },
7310
- { suffix: "API_KEY", hint: "API key" },
7311
- { suffix: "ACCESS_KEY", hint: "access key" },
7312
- { suffix: "SECRET_KEY", hint: "secret key" },
7313
- { suffix: "SECRET", hint: "secret" },
7314
- { suffix: "PASSWORD", hint: "password" },
7315
- { suffix: "PASSPHRASE", hint: "passphrase" },
7316
- { suffix: "TOKEN", hint: "token" },
7317
- { suffix: "KEY", hint: "key" },
7318
- { suffix: "CREDENTIALS", hint: "credentials" },
7319
- { suffix: "AUTH", hint: "auth" },
7320
- { suffix: "DSN", hint: "connection string" }
7321
- ];
7322
- NAME_DB_URL = /^(DATABASE|DB|POSTGRES|MYSQL|MONGO|REDIS)_URL$/i;
7323
- VALUE_PATTERNS = [
7324
- { re: /^sk-proj-/, hint: "OpenAI project key" },
7325
- { re: /^sk-[A-Za-z0-9]{20,}/, hint: "OpenAI key" },
7326
- { re: /^sk_live_/, hint: "Stripe live key" },
7327
- { re: /^sk_test_/, hint: "Stripe test key" },
7328
- { re: /^pk_live_/, hint: "Stripe publishable \u2014 often public, double-check" },
7329
- { re: /^xoxb-/, hint: "Slack bot token" },
7330
- { re: /^xoxp-/, hint: "Slack user token" },
7331
- { re: /^ghp_[A-Za-z0-9]{30,}/, hint: "GitHub personal access token" },
7332
- { re: /^github_pat_/, hint: "GitHub fine-grained PAT" },
7333
- { re: /^gho_/, hint: "GitHub OAuth token" },
7334
- { re: /^AKIA[0-9A-Z]{16}$/, hint: "AWS access key id" },
7335
- { re: /^ASIA[0-9A-Z]{16}$/, hint: "AWS temporary key id" },
7336
- { re: /^AIza[0-9A-Za-z_-]{35}$/, hint: "Google API key" },
7337
- { re: /^ya29\./, hint: "Google OAuth access token" },
7338
- { re: /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, hint: "JWT" },
7339
- { re: /^-----BEGIN [A-Z ]+PRIVATE KEY-----/, hint: "PEM private key" },
7340
- { re: /^([a-z]+):\/\/[^@]+:[^@]+@/i, hint: "connection string with credentials" }
7341
- ];
7342
- }
7343
- });
7344
-
7345
7380
  // src/init/scriptWrap.ts
7346
7381
  function analyzeScript(name, command) {
7347
7382
  const trimmed = command.trim();
@@ -7989,7 +8024,8 @@ function composeKeynvEnv(opts) {
7989
8024
  const { uploadedAliases, literals, mergeWithExisting } = opts;
7990
8025
  const lines = [];
7991
8026
  if (mergeWithExisting !== null) {
7992
- const trimmed = mergeWithExisting.replace(/\n+$/, "");
8027
+ const normalized = mergeWithExisting.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
8028
+ const trimmed = normalized.replace(/\n+$/, "");
7993
8029
  if (trimmed.length > 0) {
7994
8030
  lines.push(...trimmed.split("\n"));
7995
8031
  lines.push("");
@@ -8099,8 +8135,27 @@ async function runBrowserAuth(serverUrl) {
8099
8135
  }
8100
8136
  const start = await startRes.json();
8101
8137
  const opened = openBrowser(start.verification_uri_complete);
8102
- if (!opened) {
8103
- throw new BrowserAuthError(`Open this URL in your browser: ${start.verification_uri_complete}`);
8138
+ if (opened) {
8139
+ process.stderr.write(
8140
+ `
8141
+ Your code: ${start.user_code}
8142
+ Complete auth in your browser, then return here.
8143
+
8144
+ `
8145
+ );
8146
+ } else {
8147
+ process.stderr.write(
8148
+ `
8149
+ Could not open a browser automatically.
8150
+ Open this URL manually:
8151
+
8152
+ ${start.verification_uri_complete}
8153
+
8154
+ Your code: ${start.user_code}
8155
+ Waiting for you to complete auth in the browser...
8156
+
8157
+ `
8158
+ );
8104
8159
  }
8105
8160
  const deadline = Date.now() + start.expires_in * 1e3;
8106
8161
  const intervalMs = Math.max(1, start.interval) * 1e3;
@@ -28679,7 +28734,7 @@ var require_utils_webcrypto = __commonJS({
28679
28734
  var nodeCrypto = __require("crypto");
28680
28735
  module.exports = {
28681
28736
  postgresMd5PasswordHash,
28682
- randomBytes,
28737
+ randomBytes: randomBytes2,
28683
28738
  deriveKey,
28684
28739
  sha256,
28685
28740
  hashByName,
@@ -28689,7 +28744,7 @@ var require_utils_webcrypto = __commonJS({
28689
28744
  var webCrypto = nodeCrypto.webcrypto || globalThis.crypto;
28690
28745
  var subtleCrypto = webCrypto.subtle;
28691
28746
  var textEncoder = new TextEncoder();
28692
- function randomBytes(length) {
28747
+ function randomBytes2(length) {
28693
28748
  return webCrypto.getRandomValues(Buffer.alloc(length));
28694
28749
  }
28695
28750
  async function md5(string) {
@@ -42554,7 +42609,7 @@ var init_audit2 = __esm({
42554
42609
  async function runLoginFlow(client, options = {}) {
42555
42610
  const server = options.server ?? DEFAULT_SERVER_URL;
42556
42611
  const s = ft();
42557
- s.start("Opening browser for keynv login");
42612
+ s.start("Waiting for authorization");
42558
42613
  let creds;
42559
42614
  try {
42560
42615
  creds = await runBrowserAuth(server);
@@ -42814,13 +42869,17 @@ async function runMenu() {
42814
42869
  if (u) R2.message(`${u.email} (${u.org_role}) @ ${u.server_url}`);
42815
42870
  }
42816
42871
  if (didLogin) {
42817
- const setup = await ue({
42818
- message: "Set up this project now?",
42819
- initialValue: true
42820
- });
42821
- if (!q(setup) && setup) {
42822
- const { runInitFlow: runInitFlow2 } = await Promise.resolve().then(() => (init_init(), init_exports));
42823
- await runInitFlow2(client, { cwd: process.cwd(), dryRun: false, noScripts: false });
42872
+ const root = findProjectRoot(process.cwd());
42873
+ const alreadyInitialized = root !== null && hasExistingKeynvEnv(root.path);
42874
+ if (!alreadyInitialized) {
42875
+ const setup = await ue({
42876
+ message: "Set up this project now?",
42877
+ initialValue: true
42878
+ });
42879
+ if (!q(setup) && setup) {
42880
+ const { runInitFlow: runInitFlow2 } = await Promise.resolve().then(() => (init_init(), init_exports));
42881
+ await runInitFlow2(client, { cwd: process.cwd(), dryRun: false, noScripts: false });
42882
+ }
42824
42883
  }
42825
42884
  }
42826
42885
  while (true) {
@@ -42889,6 +42948,7 @@ var init_menu = __esm({
42889
42948
  "src/ui/menu.ts"() {
42890
42949
  init_dist5();
42891
42950
  init_http();
42951
+ init_detect();
42892
42952
  init_store();
42893
42953
  init_version();
42894
42954
  init_audit2();
@@ -44615,44 +44675,48 @@ var AuditListCommand = class extends Command {
44615
44675
  sinceId = options_exports.String("--since-id");
44616
44676
  json = options_exports.Boolean("--json", false);
44617
44677
  async execute() {
44618
- const client = new ApiClient();
44619
- const data = await client.request(
44620
- "/v1/audit",
44621
- {
44622
- query: {
44623
- event_type: this.eventType,
44624
- limit: this.limit,
44625
- since_id: this.sinceId
44678
+ try {
44679
+ const client = new ApiClient();
44680
+ const data = await client.request(
44681
+ "/v1/audit",
44682
+ {
44683
+ query: {
44684
+ event_type: this.eventType,
44685
+ limit: this.limit,
44686
+ since_id: this.sinceId
44687
+ }
44626
44688
  }
44627
- }
44628
- );
44629
- if (this.json) {
44630
- this.context.stdout.write(`${JSON.stringify(data, null, 2)}
44689
+ );
44690
+ if (this.json) {
44691
+ this.context.stdout.write(`${JSON.stringify(data, null, 2)}
44631
44692
  `);
44632
- return 0;
44633
- }
44634
- if (data.entries.length === 0) {
44635
- this.context.stdout.write("no audit entries\n");
44636
- return 0;
44637
- }
44638
- this.context.stdout.write(
44639
- `${table(
44640
- ["id", "ts", "actor", "agent", "event"],
44641
- data.entries.map((e2) => [
44642
- String(e2.id),
44643
- e2.ts,
44644
- e2.actor_user_id ?? "(none)",
44645
- e2.actor_agent,
44646
- e2.event_type
44647
- ])
44648
- )}
44693
+ return 0;
44694
+ }
44695
+ if (data.entries.length === 0) {
44696
+ this.context.stdout.write("no audit entries\n");
44697
+ return 0;
44698
+ }
44699
+ this.context.stdout.write(
44700
+ `${table(
44701
+ ["id", "ts", "actor", "agent", "event"],
44702
+ data.entries.map((e2) => [
44703
+ String(e2.id),
44704
+ e2.ts,
44705
+ e2.actor_user_id ?? "(none)",
44706
+ e2.actor_agent,
44707
+ e2.event_type
44708
+ ])
44709
+ )}
44649
44710
  `
44650
- );
44651
- if (data.next_cursor) {
44652
- this.context.stdout.write(`(next: --since-id ${data.next_cursor})
44711
+ );
44712
+ if (data.next_cursor) {
44713
+ this.context.stdout.write(`(next: --since-id ${data.next_cursor})
44653
44714
  `);
44715
+ }
44716
+ return 0;
44717
+ } catch (err) {
44718
+ return handleExecError(this.context.stderr, err);
44654
44719
  }
44655
- return 0;
44656
44720
  }
44657
44721
  };
44658
44722
  var AuditVerifyCommand = class extends Command {
@@ -44662,23 +44726,27 @@ var AuditVerifyCommand = class extends Command {
44662
44726
  });
44663
44727
  json = options_exports.Boolean("--json", false);
44664
44728
  async execute() {
44665
- const client = new ApiClient();
44666
- const data = await client.request("/v1/audit/verify", { method: "POST" });
44667
- if (this.json) {
44668
- this.context.stdout.write(`${JSON.stringify(data, null, 2)}
44729
+ try {
44730
+ const client = new ApiClient();
44731
+ const data = await client.request("/v1/audit/verify", { method: "POST" });
44732
+ if (this.json) {
44733
+ this.context.stdout.write(`${JSON.stringify(data, null, 2)}
44669
44734
  `);
44670
- return data.ok ? 0 : 1;
44671
- }
44672
- if (data.ok) {
44673
- this.context.stdout.write(`OK: ${data.checked} entries verified
44735
+ return data.ok ? 0 : 1;
44736
+ }
44737
+ if (data.ok) {
44738
+ this.context.stdout.write(`OK: ${data.checked} entries verified
44674
44739
  `);
44675
- return 0;
44676
- }
44677
- this.context.stdout.write(
44678
- `FAIL: chain broken at id ${data.broken_at_id} (${data.reason}); ${data.checked} entries verified before break
44740
+ return 0;
44741
+ }
44742
+ this.context.stdout.write(
44743
+ `FAIL: chain broken at id ${data.broken_at_id} (${data.reason}); ${data.checked} entries verified before break
44679
44744
  `
44680
- );
44681
- return 1;
44745
+ );
44746
+ return 1;
44747
+ } catch (err) {
44748
+ return handleExecError(this.context.stderr, err);
44749
+ }
44682
44750
  }
44683
44751
  };
44684
44752
 
@@ -44719,11 +44787,10 @@ async function resolveAllAliases(client, argv2, extraStrings = []) {
44719
44787
  return resolved;
44720
44788
  }
44721
44789
  function substitute(text, resolved) {
44790
+ const sorted = [...resolved].sort((a, b2) => b2.alias.literal.length - a.alias.literal.length);
44722
44791
  let out = text;
44723
- for (const r of resolved) {
44724
- if (out.includes(r.alias.literal)) {
44725
- out = out.split(r.alias.literal).join(r.value);
44726
- }
44792
+ for (const r of sorted) {
44793
+ out = out.split(r.alias.literal).join(r.value);
44727
44794
  }
44728
44795
  return out;
44729
44796
  }
@@ -44826,10 +44893,18 @@ var BUILTIN_LINE_PATTERNS = BUILTIN_PATTERNS.filter((p2) => !p2.multiline);
44826
44893
 
44827
44894
  // ../../packages/redactor/dist/entropy.js
44828
44895
  var TOKEN_BOUNDARY_RE = /[\s,;:'"<>(){}[\]=]+/;
44896
+ var DEFAULT_EXCLUDE_PREFIXES = [
44897
+ "sha1-",
44898
+ "sha256:",
44899
+ "sha256-",
44900
+ "sha384-",
44901
+ "sha512-"
44902
+ ];
44829
44903
  var DEFAULTS = {
44830
44904
  enabled: true,
44831
44905
  minLength: 24,
44832
- minBitsPerChar: 4.5
44906
+ minBitsPerChar: 4.5,
44907
+ excludePrefixes: DEFAULT_EXCLUDE_PREFIXES
44833
44908
  };
44834
44909
  function shannonEntropy(s) {
44835
44910
  if (s.length === 0)
@@ -44862,9 +44937,13 @@ function findEntropyMatches(text, opts = {}) {
44862
44937
  const end = i;
44863
44938
  const token = text.slice(start, end);
44864
44939
  if (token.length >= cfg.minLength) {
44865
- const h2 = shannonEntropy(token);
44866
- if (h2 >= cfg.minBitsPerChar) {
44867
- matches.push({ start, end, token });
44940
+ const lower = token.toLowerCase();
44941
+ const excluded = cfg.excludePrefixes.some((p2) => lower.startsWith(p2));
44942
+ if (!excluded) {
44943
+ const h2 = shannonEntropy(token);
44944
+ if (h2 >= cfg.minBitsPerChar) {
44945
+ matches.push({ start, end, token });
44946
+ }
44868
44947
  }
44869
44948
  }
44870
44949
  }
@@ -45008,7 +45087,19 @@ var ENV_ALLOWLIST = [
45008
45087
  "PWD",
45009
45088
  "OLDPWD",
45010
45089
  "TMPDIR",
45011
- "SSH_AUTH_SOCK"
45090
+ "SSH_AUTH_SOCK",
45091
+ // Windows-specific
45092
+ "USERPROFILE",
45093
+ "USERNAME",
45094
+ "COMPUTERNAME",
45095
+ "TEMP",
45096
+ "TMP",
45097
+ "SYSTEMROOT",
45098
+ "SYSTEMDRIVE",
45099
+ "WINDIR",
45100
+ "COMSPEC",
45101
+ "APPDATA",
45102
+ "LOCALAPPDATA"
45012
45103
  ];
45013
45104
  function spawnPrivileged(opts) {
45014
45105
  const startedAt = Date.now();
@@ -45021,7 +45112,32 @@ function spawnPrivileged(opts) {
45021
45112
  for (const [k2, v2] of Object.entries(opts.injectedEnv)) env2[k2] = v2;
45022
45113
  }
45023
45114
  const stdio = ["inherit", "pipe", "pipe"];
45024
- const child = spawn(opts.command, opts.args, {
45115
+ const WIN_BUILTINS = /* @__PURE__ */ new Set([
45116
+ "echo",
45117
+ "dir",
45118
+ "type",
45119
+ "copy",
45120
+ "del",
45121
+ "move",
45122
+ "ren",
45123
+ "md",
45124
+ "mkdir",
45125
+ "rd",
45126
+ "rmdir",
45127
+ "cd",
45128
+ "cls",
45129
+ "set",
45130
+ "pause",
45131
+ "find",
45132
+ "where"
45133
+ ]);
45134
+ let spawnCmd = opts.command;
45135
+ let spawnArgs = opts.args;
45136
+ if (process.platform === "win32" && WIN_BUILTINS.has(opts.command.toLowerCase())) {
45137
+ spawnCmd = process.env.COMSPEC ?? "cmd.exe";
45138
+ spawnArgs = ["/d", "/s", "/c", opts.command, ...opts.args];
45139
+ }
45140
+ const child = spawn(spawnCmd, spawnArgs, {
45025
45141
  env: env2,
45026
45142
  stdio,
45027
45143
  detached: false
@@ -45152,7 +45268,11 @@ spelled \`--from\` to avoid the collision.)
45152
45268
  `);
45153
45269
  return 2;
45154
45270
  }
45155
- throw err;
45271
+ this.context.stderr.write(
45272
+ `keynv: unexpected error loading env file: ${err instanceof Error ? err.message : String(err)}
45273
+ `
45274
+ );
45275
+ return 2;
45156
45276
  }
45157
45277
  const viaEnvSpecs = [];
45158
45278
  for (const spec of this.viaEnv ?? []) {
@@ -45224,11 +45344,18 @@ spelled \`--from\` to avoid the collision.)
45224
45344
  injectedEnv[spec.name] = value;
45225
45345
  }
45226
45346
  if (envFileLoaded && !this.quiet) {
45227
- const total = envFileLoaded.entries.length;
45228
- const aliasCount = envFileLoaded.entries.filter((e2) => e2.isAlias).length;
45347
+ const aliasEntries = envFileLoaded.entries.filter((e2) => e2.isAlias);
45348
+ const plainEntries = envFileLoaded.entries.filter((e2) => !e2.isAlias);
45349
+ const displayPath = relative(process.cwd(), envFileLoaded.path) || envFileLoaded.path;
45350
+ const parts = [];
45351
+ if (aliasEntries.length > 0) {
45352
+ parts.push(aliasEntries.map((e2) => `${e2.name}=${e2.value}`).join(", ") + " (vault)");
45353
+ }
45354
+ if (plainEntries.length > 0) {
45355
+ parts.push(plainEntries.map((e2) => e2.name).join(", ") + " (plain)");
45356
+ }
45229
45357
  this.context.stderr.write(
45230
- `keynv: loaded ${total} var${total === 1 ? "" : "s"} from ${envFileLoaded.path} (${aliasCount} resolved from vault)
45231
- `
45358
+ `keynv: loaded ${displayPath}` + (parts.length > 0 ? ` \u2014 ${parts.join("; ")}` : "") + "\n"
45232
45359
  );
45233
45360
  }
45234
45361
  const timeoutS = this.timeout ? Number.parseInt(this.timeout, 10) : void 0;
@@ -45268,10 +45395,158 @@ function signalNumber(sig) {
45268
45395
  }
45269
45396
 
45270
45397
  // src/commands/init.ts
45398
+ init_dist();
45271
45399
  init_http();
45400
+ init_envFile();
45401
+ init_heuristics();
45272
45402
  init_init();
45273
45403
  init_cancel();
45274
45404
  init_tty();
45405
+
45406
+ // src/commands/project.ts
45407
+ init_http();
45408
+ init_format();
45409
+ function isProjectId(input) {
45410
+ return input.startsWith("p_") || /^[a-f0-9]{20,}$/i.test(input);
45411
+ }
45412
+ async function resolveProjectId(client, input) {
45413
+ if (isProjectId(input)) {
45414
+ return input;
45415
+ }
45416
+ const data = await client.request("/v1/projects");
45417
+ const match = data.projects.find((p2) => p2.name === input);
45418
+ if (!match) throw new Error(`project not found: ${input}`);
45419
+ return match.id;
45420
+ }
45421
+ var ProjectListCommand = class extends Command {
45422
+ static paths = [["project", "list"]];
45423
+ static usage = Command.Usage({
45424
+ description: "List projects visible to the current user."
45425
+ });
45426
+ json = options_exports.Boolean("--json", false);
45427
+ async execute() {
45428
+ try {
45429
+ const client = new ApiClient();
45430
+ const data = await client.request("/v1/projects");
45431
+ if (this.json) {
45432
+ this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45433
+ `);
45434
+ return 0;
45435
+ }
45436
+ if (data.projects.length === 0) {
45437
+ this.context.stdout.write("no projects\n");
45438
+ return 0;
45439
+ }
45440
+ this.context.stdout.write(
45441
+ `${table(
45442
+ ["name", "id", "created_at"],
45443
+ data.projects.map((p2) => [p2.name, p2.id, p2.created_at])
45444
+ )}
45445
+ `
45446
+ );
45447
+ return 0;
45448
+ } catch (err) {
45449
+ return handleExecError(this.context.stderr, err);
45450
+ }
45451
+ }
45452
+ };
45453
+ var ProjectCreateCommand = class extends Command {
45454
+ static paths = [["project", "create"]];
45455
+ static usage = Command.Usage({
45456
+ description: "Create a new project with one or more environments.",
45457
+ examples: [["Two envs", "$0 project create billing --env dev --env prod:production:approval"]]
45458
+ });
45459
+ name = options_exports.String();
45460
+ envs = options_exports.Array("--env", {
45461
+ description: 'Environment spec: name[:tier[:approval]]. Tier \u2208 {production,non-production}; "approval" sets require_approval=true.'
45462
+ });
45463
+ async execute() {
45464
+ try {
45465
+ const envs = (this.envs ?? ["dev"]).map((spec) => {
45466
+ const [name, tier, approval] = spec.split(":");
45467
+ return {
45468
+ name: name ?? "",
45469
+ tier: tier ?? "non-production",
45470
+ require_approval: approval === "approval"
45471
+ };
45472
+ });
45473
+ const client = new ApiClient();
45474
+ const result = await client.request("/v1/projects", {
45475
+ method: "POST",
45476
+ body: { name: this.name, environments: envs }
45477
+ });
45478
+ this.context.stdout.write(`created project ${result.name} (${result.id})
45479
+ `);
45480
+ for (const e2 of envs) {
45481
+ this.context.stdout.write(
45482
+ ` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
45483
+ `
45484
+ );
45485
+ }
45486
+ return 0;
45487
+ } catch (err) {
45488
+ return handleExecError(this.context.stderr, err);
45489
+ }
45490
+ }
45491
+ };
45492
+ var ProjectDescribeCommand = class extends Command {
45493
+ static paths = [["project", "describe"]];
45494
+ static usage = Command.Usage({
45495
+ description: "Show metadata for one project.",
45496
+ examples: [
45497
+ ["By ID", "$0 project describe p_go6rqgwz0wlokdsl55ikn"],
45498
+ ["By name", "$0 project describe myproject"]
45499
+ ]
45500
+ });
45501
+ id = options_exports.String();
45502
+ json = options_exports.Boolean("--json", false);
45503
+ async execute() {
45504
+ try {
45505
+ const client = new ApiClient();
45506
+ const projectId2 = await resolveProjectId(client, this.id);
45507
+ const data = await client.request(`/v1/projects/${projectId2}`);
45508
+ if (this.json) {
45509
+ this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45510
+ `);
45511
+ return 0;
45512
+ }
45513
+ this.context.stdout.write(`project: ${data.name} (${data.id})
45514
+ `);
45515
+ for (const e2 of data.environments) {
45516
+ this.context.stdout.write(
45517
+ ` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
45518
+ `
45519
+ );
45520
+ }
45521
+ return 0;
45522
+ } catch (err) {
45523
+ return handleExecError(this.context.stderr, err);
45524
+ }
45525
+ }
45526
+ };
45527
+ var ProjectDeleteCommand = class extends Command {
45528
+ static paths = [["project", "delete"]];
45529
+ static usage = Command.Usage({ description: "Soft-delete a project." });
45530
+ id = options_exports.String();
45531
+ force = options_exports.Boolean("--force", false);
45532
+ async execute() {
45533
+ if (!this.force) {
45534
+ this.context.stderr.write("keynv: refusing to delete without --force\n");
45535
+ return 2;
45536
+ }
45537
+ try {
45538
+ const client = new ApiClient();
45539
+ await client.request(`/v1/projects/${this.id}`, { method: "DELETE" });
45540
+ this.context.stdout.write(`deleted project ${this.id}
45541
+ `);
45542
+ return 0;
45543
+ } catch (err) {
45544
+ return handleExecError(this.context.stderr, err);
45545
+ }
45546
+ }
45547
+ };
45548
+
45549
+ // src/commands/init.ts
45275
45550
  var InitCommand = class extends Command {
45276
45551
  static paths = [["init"]];
45277
45552
  static usage = Command.Usage({
@@ -45285,6 +45560,11 @@ package.json scripts with \`keynv exec\`.
45285
45560
  Safe to re-run: existing .keynv.env entries are preserved; new
45286
45561
  entries are appended below a marker.
45287
45562
 
45563
+ --dry-run prints the secrets that would be uploaded and the alias
45564
+ mappings that would be written to .keynv.env, then exits without
45565
+ touching any files or making any network calls. Use it to preview
45566
+ what init will do before committing.
45567
+
45288
45568
  Requires an interactive terminal (clack TUI). For scripted
45289
45569
  migration, use the lower-level \`keynv project\` and \`keynv secret\`
45290
45570
  commands directly.
@@ -45292,28 +45572,47 @@ commands directly.
45292
45572
  examples: [
45293
45573
  ["Walk the current project", "$0 init"],
45294
45574
  ["Preview without writing or uploading", "$0 init --dry-run"],
45295
- ["Skip the package.json script-wrapping step", "$0 init --no-scripts"]
45575
+ ["Skip the package.json script-wrapping step", "$0 init --no-scripts"],
45576
+ ["Non-interactive (CI/CD)", "$0 init --env-file .env --project myproject --env dev"]
45296
45577
  ]
45297
45578
  });
45298
45579
  dryRun = options_exports.Boolean("--dry-run", false, {
45299
- description: "Show what would be done without writing files or uploading secrets."
45580
+ 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."
45300
45581
  });
45301
45582
  noScripts = options_exports.Boolean("--no-scripts", false, {
45302
45583
  description: "Skip the package.json script-wrapping step."
45303
45584
  });
45585
+ envFile = options_exports.String("--env-file", {
45586
+ description: "Path to .env file to migrate (non-interactive mode)."
45587
+ });
45588
+ project = options_exports.String("--project", {
45589
+ description: "Project name or ID (non-interactive mode)."
45590
+ });
45591
+ env = options_exports.String("--env", {
45592
+ description: "Environment name for --env-file (default: dev)."
45593
+ });
45594
+ secret = options_exports.Array("--secret", {
45595
+ description: "KEY=value secret to upload (non-interactive). Can be specified multiple times."
45596
+ });
45304
45597
  async execute() {
45305
- if (!isInteractive()) {
45306
- this.context.stderr.write(
45307
- "keynv init requires an interactive terminal. Use the lower-level commands (`keynv project`, `keynv secret`) for scripted setup.\n"
45308
- );
45309
- return 1;
45310
- }
45311
45598
  const client = new ApiClient();
45312
45599
  await client.ensureHydrated();
45313
45600
  if (!client.isLoggedIn) {
45314
45601
  this.context.stderr.write("keynv: not logged in. Run `keynv login` first.\n");
45315
45602
  return 1;
45316
45603
  }
45604
+ const hasEnvFile = this.envFile != null;
45605
+ const hasSecrets = this.secret != null && this.secret.length > 0;
45606
+ const isNonInteractive = hasEnvFile || hasSecrets;
45607
+ if (isNonInteractive) {
45608
+ return this.runNonInteractive(client);
45609
+ }
45610
+ if (!isInteractive()) {
45611
+ this.context.stderr.write(
45612
+ "keynv init requires an interactive terminal. Use --env-file or --secret for scripted setup.\n"
45613
+ );
45614
+ return 1;
45615
+ }
45317
45616
  try {
45318
45617
  const outcome = await runInitFlow(client, {
45319
45618
  cwd: process.cwd(),
@@ -45329,6 +45628,97 @@ commands directly.
45329
45628
  return 1;
45330
45629
  }
45331
45630
  }
45631
+ async runNonInteractive(client) {
45632
+ const projectName = this.project;
45633
+ if (!projectName) {
45634
+ this.context.stderr.write("keynv: --project is required in non-interactive mode.\n");
45635
+ return 1;
45636
+ }
45637
+ const resolved = await resolveProjectId(client, projectName);
45638
+ if (!resolved) {
45639
+ this.context.stderr.write(`keynv: project not found: ${projectName}
45640
+ `);
45641
+ return 1;
45642
+ }
45643
+ const projectId2 = resolved;
45644
+ const envName = this.env ?? "dev";
45645
+ const secrets = [];
45646
+ if (this.envFile) {
45647
+ try {
45648
+ const content = readFileSync(this.envFile, "utf8");
45649
+ const entries = parseEnvFile(content, this.envFile);
45650
+ for (const e2 of entries) {
45651
+ if (classifyEntry(e2.name, e2.value).verdict === "secret") {
45652
+ secrets.push({ name: e2.name, value: e2.value });
45653
+ }
45654
+ }
45655
+ } catch (err) {
45656
+ this.context.stderr.write(
45657
+ `keynv: cannot read ${this.envFile}: ${err instanceof Error ? err.message : String(err)}
45658
+ `
45659
+ );
45660
+ return 1;
45661
+ }
45662
+ }
45663
+ if (this.secret) {
45664
+ for (const spec of this.secret) {
45665
+ const eq = spec.indexOf("=");
45666
+ if (eq <= 0) {
45667
+ this.context.stderr.write(`keynv: invalid --secret '${spec}', expected KEY=value
45668
+ `);
45669
+ return 1;
45670
+ }
45671
+ const name = spec.slice(0, eq);
45672
+ const value = spec.slice(eq + 1);
45673
+ secrets.push({ name, value });
45674
+ }
45675
+ }
45676
+ if (secrets.length === 0) {
45677
+ this.context.stdout.write("keynv: nothing to migrate.\n");
45678
+ return 0;
45679
+ }
45680
+ if (this.dryRun) {
45681
+ this.context.stdout.write(
45682
+ `keynv: dry-run \u2014 would upload ${secrets.length} secret(s) to project ${projectName} (${projectId2}) in env ${envName}
45683
+ `
45684
+ );
45685
+ for (const { name } of secrets) {
45686
+ const aliasKey = name.toLowerCase().replace(/_/g, "-");
45687
+ this.context.stdout.write(` ${name}=@${projectName}.${envName}.${aliasKey}
45688
+ `);
45689
+ }
45690
+ return 0;
45691
+ }
45692
+ let uploaded = 0;
45693
+ const failed = [];
45694
+ for (const { name, value } of secrets) {
45695
+ const aliasKey = name.toLowerCase().replace(/_/g, "-");
45696
+ const alias2 = buildAlias({ project: projectName, environment: envName, key: aliasKey });
45697
+ if (!alias2) {
45698
+ failed.push(`${name} (invalid alias key: ${aliasKey})`);
45699
+ continue;
45700
+ }
45701
+ try {
45702
+ await client.request(`/v1/projects/${projectId2}/secrets`, {
45703
+ method: "POST",
45704
+ body: { env: envName, key: aliasKey, value }
45705
+ });
45706
+ uploaded++;
45707
+ } catch (err) {
45708
+ failed.push(`${name}: ${err instanceof Error ? err.message : String(err)}`);
45709
+ }
45710
+ }
45711
+ this.context.stdout.write(
45712
+ `keynv: uploaded ${uploaded}/${secrets.length} secret(s) to ${projectName}.${envName}
45713
+ `
45714
+ );
45715
+ if (failed.length > 0) {
45716
+ for (const f of failed) this.context.stderr.write(` failed: ${f}
45717
+ `);
45718
+ return 1;
45719
+ }
45720
+ return 0;
45721
+ }
45332
45722
  };
45333
45723
 
45334
45724
  // src/commands/login.ts
@@ -45408,6 +45798,13 @@ var LoginCommand = class extends Command {
45408
45798
  });
45409
45799
  async execute() {
45410
45800
  const finalServerUrl = this.server ?? DEFAULT_SERVER_URL;
45801
+ try {
45802
+ new URL(finalServerUrl);
45803
+ } catch {
45804
+ this.context.stderr.write(`keynv: invalid server URL '${finalServerUrl}'
45805
+ `);
45806
+ return 1;
45807
+ }
45411
45808
  if (this.token) {
45412
45809
  return this.loginWithToken(finalServerUrl, this.token);
45413
45810
  }
@@ -45430,11 +45827,20 @@ var LoginCommand = class extends Command {
45430
45827
  }
45431
45828
  const email2 = this.email ?? await promptLine("email: ");
45432
45829
  const password = this.password ?? await promptHidden("password: ");
45433
- const res = await fetch(new URL("/v1/auth/login", finalServerUrl).toString(), {
45434
- method: "POST",
45435
- headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
45436
- body: JSON.stringify({ email: email2, password })
45437
- });
45830
+ let res;
45831
+ try {
45832
+ res = await fetch(new URL("/v1/auth/login", finalServerUrl).toString(), {
45833
+ method: "POST",
45834
+ headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
45835
+ body: JSON.stringify({ email: email2, password })
45836
+ });
45837
+ } catch (err) {
45838
+ this.context.stderr.write(
45839
+ `keynv: cannot reach server '${finalServerUrl}' \u2014 ${err instanceof Error ? err.message : String(err)}
45840
+ `
45841
+ );
45842
+ return 1;
45843
+ }
45438
45844
  if (!res.ok) {
45439
45845
  const text = await res.text();
45440
45846
  let msg = `login failed (${res.status})`;
@@ -45480,9 +45886,18 @@ var LoginCommand = class extends Command {
45480
45886
  return 0;
45481
45887
  }
45482
45888
  async loginWithToken(serverUrl, token) {
45483
- const res = await fetch(new URL("/v1/whoami", serverUrl).toString(), {
45484
- headers: { authorization: `Bearer ${token}`, "x-keynv-agent": AGENT }
45485
- });
45889
+ let res;
45890
+ try {
45891
+ res = await fetch(new URL("/v1/whoami", serverUrl).toString(), {
45892
+ headers: { authorization: `Bearer ${token}`, "x-keynv-agent": AGENT }
45893
+ });
45894
+ } catch (err) {
45895
+ this.context.stderr.write(
45896
+ `keynv: cannot reach server '${serverUrl}' \u2014 ${err instanceof Error ? err.message : String(err)}
45897
+ `
45898
+ );
45899
+ return 1;
45900
+ }
45486
45901
  if (!res.ok) {
45487
45902
  const text = await res.text();
45488
45903
  let msg = `token login failed (${res.status})`;
@@ -45560,48 +45975,46 @@ var WhoamiCommand = class extends Command {
45560
45975
  });
45561
45976
  json = options_exports.Boolean("--json", false);
45562
45977
  async execute() {
45563
- const client = new ApiClient();
45564
- await client.ensureHydrated();
45565
- if (!client.isLoggedIn) {
45566
- this.context.stderr.write("keynv: not logged in. Run `keynv login`.\n");
45567
- return 1;
45568
- }
45569
- const data = await client.request("/v1/whoami");
45570
- if (this.json) {
45571
- this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45978
+ try {
45979
+ const client = new ApiClient();
45980
+ await client.ensureHydrated();
45981
+ if (!client.isLoggedIn) {
45982
+ this.context.stderr.write("keynv: not logged in. Run `keynv login`.\n");
45983
+ return 1;
45984
+ }
45985
+ const data = await client.request("/v1/whoami");
45986
+ if (this.json) {
45987
+ this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45572
45988
  `);
45573
- return 0;
45574
- }
45575
- this.context.stdout.write(`user: ${data.email} (${data.id})
45989
+ return 0;
45990
+ }
45991
+ this.context.stdout.write(`user: ${data.email} (${data.id})
45576
45992
  `);
45577
- this.context.stdout.write(`org: ${data.org_id}
45993
+ const orgDisplay = data.org_name ? `${data.org_name} (${data.org_id})` : data.org_id;
45994
+ this.context.stdout.write(`org: ${orgDisplay}
45578
45995
  `);
45579
- this.context.stdout.write(`role: ${data.org_role}
45996
+ this.context.stdout.write(`role: ${data.org_role}
45580
45997
  `);
45581
- if (data.memberships.length === 0) {
45582
- this.context.stdout.write("memberships: (none)\n");
45583
- } else {
45584
- this.context.stdout.write("memberships:\n");
45585
- for (const m of data.memberships) {
45586
- this.context.stdout.write(` ${m.project_id}: ${m.role}
45998
+ if (data.memberships.length === 0) {
45999
+ this.context.stdout.write("memberships: (none)\n");
46000
+ } else {
46001
+ this.context.stdout.write("memberships:\n");
46002
+ for (const m of data.memberships) {
46003
+ const proj = m.project_name ? `${m.project_name} (${m.project_id})` : m.project_id;
46004
+ this.context.stdout.write(` ${proj}: ${m.role}
45587
46005
  `);
46006
+ }
45588
46007
  }
46008
+ return 0;
46009
+ } catch (err) {
46010
+ return handleExecError(this.context.stderr, err);
45589
46011
  }
45590
- return 0;
45591
46012
  }
45592
46013
  };
45593
46014
 
45594
46015
  // src/commands/member.ts
45595
46016
  init_http();
45596
46017
  init_format();
45597
- async function findProjectIdByName(client, name) {
45598
- const data = await client.request(
45599
- "/v1/projects"
45600
- );
45601
- const match = data.projects.find((p2) => p2.name === name);
45602
- if (!match) throw new Error(`unknown project: ${name}`);
45603
- return match.id;
45604
- }
45605
46018
  var MemberAddCommand = class extends Command {
45606
46019
  static paths = [["member", "add"]];
45607
46020
  static usage = Command.Usage({
@@ -45617,15 +46030,19 @@ var MemberAddCommand = class extends Command {
45617
46030
  this.context.stderr.write("keynv: --role must be one of lead, developer, reader\n");
45618
46031
  return 1;
45619
46032
  }
45620
- const client = new ApiClient();
45621
- const projectId2 = await findProjectIdByName(client, this.project);
45622
- await client.request(`/v1/projects/${projectId2}/members`, {
45623
- method: "POST",
45624
- body: { email: this.email, role: role2 }
45625
- });
45626
- this.context.stdout.write(`added ${this.email} to ${this.project} as ${role2}
46033
+ try {
46034
+ const client = new ApiClient();
46035
+ const projectId2 = await resolveProjectId(client, this.project);
46036
+ await client.request(`/v1/projects/${projectId2}/members`, {
46037
+ method: "POST",
46038
+ body: { email: this.email, role: role2 }
46039
+ });
46040
+ this.context.stdout.write(`added ${this.email} to ${this.project} as ${role2}
45627
46041
  `);
45628
- return 0;
46042
+ return 0;
46043
+ } catch (err) {
46044
+ return handleExecError(this.context.stderr, err);
46045
+ }
45629
46046
  }
45630
46047
  };
45631
46048
  var MemberRemoveCommand = class extends Command {
@@ -45634,21 +46051,25 @@ var MemberRemoveCommand = class extends Command {
45634
46051
  project = options_exports.String();
45635
46052
  email = options_exports.String();
45636
46053
  async execute() {
45637
- const client = new ApiClient();
45638
- const projectId2 = await findProjectIdByName(client, this.project);
45639
- const members = await client.request(`/v1/projects/${projectId2}/members`);
45640
- const target = members.members.find((m) => m.email === this.email);
45641
- if (!target) {
45642
- this.context.stderr.write(`keynv: ${this.email} is not a member of ${this.project}
46054
+ try {
46055
+ const client = new ApiClient();
46056
+ const projectId2 = await resolveProjectId(client, this.project);
46057
+ const members = await client.request(`/v1/projects/${projectId2}/members`);
46058
+ const target = members.members.find((m) => m.email === this.email);
46059
+ if (!target) {
46060
+ this.context.stderr.write(`keynv: ${this.email} is not a member of ${this.project}
45643
46061
  `);
45644
- return 1;
45645
- }
45646
- await client.request(`/v1/projects/${projectId2}/members/${target.user_id}`, {
45647
- method: "DELETE"
45648
- });
45649
- this.context.stdout.write(`removed ${this.email} from ${this.project}
46062
+ return 1;
46063
+ }
46064
+ await client.request(`/v1/projects/${projectId2}/members/${target.user_id}`, {
46065
+ method: "DELETE"
46066
+ });
46067
+ this.context.stdout.write(`removed ${this.email} from ${this.project}
45650
46068
  `);
45651
- return 0;
46069
+ return 0;
46070
+ } catch (err) {
46071
+ return handleExecError(this.context.stderr, err);
46072
+ }
45652
46073
  }
45653
46074
  };
45654
46075
  var MemberListCommand = class extends Command {
@@ -45657,134 +46078,30 @@ var MemberListCommand = class extends Command {
45657
46078
  project = options_exports.String();
45658
46079
  json = options_exports.Boolean("--json", false);
45659
46080
  async execute() {
45660
- const client = new ApiClient();
45661
- const projectId2 = await findProjectIdByName(client, this.project);
45662
- const data = await client.request(`/v1/projects/${projectId2}/members`);
45663
- if (this.json) {
45664
- this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45665
- `);
45666
- return 0;
45667
- }
45668
- if (data.members.length === 0) {
45669
- this.context.stdout.write("no members\n");
45670
- return 0;
45671
- }
45672
- this.context.stdout.write(
45673
- `${table(
45674
- ["email", "role", "granted_at"],
45675
- data.members.map((m) => [m.email, m.role, m.granted_at])
45676
- )}
45677
- `
45678
- );
45679
- return 0;
45680
- }
45681
- };
45682
-
45683
- // src/commands/project.ts
45684
- init_http();
45685
- init_format();
45686
- var ProjectListCommand = class extends Command {
45687
- static paths = [["project", "list"]];
45688
- static usage = Command.Usage({
45689
- description: "List projects visible to the current user."
45690
- });
45691
- json = options_exports.Boolean("--json", false);
45692
- async execute() {
45693
- const client = new ApiClient();
45694
- const data = await client.request("/v1/projects");
45695
- if (this.json) {
45696
- this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45697
- `);
45698
- return 0;
45699
- }
45700
- if (data.projects.length === 0) {
45701
- this.context.stdout.write("no projects\n");
45702
- return 0;
45703
- }
45704
- this.context.stdout.write(
45705
- `${table(
45706
- ["name", "id", "created_at"],
45707
- data.projects.map((p2) => [p2.name, p2.id, p2.created_at])
45708
- )}
45709
- `
45710
- );
45711
- return 0;
45712
- }
45713
- };
45714
- var ProjectCreateCommand = class extends Command {
45715
- static paths = [["project", "create"]];
45716
- static usage = Command.Usage({
45717
- description: "Create a new project with one or more environments.",
45718
- examples: [["Two envs", "$0 project create billing --env dev --env prod:production:approval"]]
45719
- });
45720
- name = options_exports.String();
45721
- envs = options_exports.Array("--env", {
45722
- description: 'Environment spec: name[:tier[:approval]]. Tier \u2208 {production,non-production}; "approval" sets require_approval=true.'
45723
- });
45724
- async execute() {
45725
- const envs = (this.envs ?? ["dev"]).map((spec) => {
45726
- const [name, tier, approval] = spec.split(":");
45727
- return {
45728
- name: name ?? "",
45729
- tier: tier ?? "non-production",
45730
- require_approval: approval === "approval"
45731
- };
45732
- });
45733
- const client = new ApiClient();
45734
- const result = await client.request("/v1/projects", {
45735
- method: "POST",
45736
- body: { name: this.name, environments: envs }
45737
- });
45738
- this.context.stdout.write(`created project ${result.name} (${result.id})
46081
+ try {
46082
+ const client = new ApiClient();
46083
+ const projectId2 = await resolveProjectId(client, this.project);
46084
+ const data = await client.request(`/v1/projects/${projectId2}/members`);
46085
+ if (this.json) {
46086
+ this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45739
46087
  `);
45740
- for (const e2 of envs) {
46088
+ return 0;
46089
+ }
46090
+ if (data.members.length === 0) {
46091
+ this.context.stdout.write("no members\n");
46092
+ return 0;
46093
+ }
45741
46094
  this.context.stdout.write(
45742
- ` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
46095
+ `${table(
46096
+ ["email", "role", "granted_at"],
46097
+ data.members.map((m) => [m.email, m.role, m.granted_at])
46098
+ )}
45743
46099
  `
45744
46100
  );
45745
- }
45746
- return 0;
45747
- }
45748
- };
45749
- var ProjectDescribeCommand = class extends Command {
45750
- static paths = [["project", "describe"]];
45751
- static usage = Command.Usage({ description: "Show metadata for one project." });
45752
- id = options_exports.String();
45753
- json = options_exports.Boolean("--json", false);
45754
- async execute() {
45755
- const client = new ApiClient();
45756
- const data = await client.request(`/v1/projects/${this.id}`);
45757
- if (this.json) {
45758
- this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45759
- `);
45760
46101
  return 0;
46102
+ } catch (err) {
46103
+ return handleExecError(this.context.stderr, err);
45761
46104
  }
45762
- this.context.stdout.write(`project: ${data.name} (${data.id})
45763
- `);
45764
- for (const e2 of data.environments) {
45765
- this.context.stdout.write(
45766
- ` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
45767
- `
45768
- );
45769
- }
45770
- return 0;
45771
- }
45772
- };
45773
- var ProjectDeleteCommand = class extends Command {
45774
- static paths = [["project", "delete"]];
45775
- static usage = Command.Usage({ description: "Soft-delete a project." });
45776
- id = options_exports.String();
45777
- force = options_exports.Boolean("--force", false);
45778
- async execute() {
45779
- if (!this.force) {
45780
- this.context.stderr.write("keynv: refusing to delete without --force\n");
45781
- return 2;
45782
- }
45783
- const client = new ApiClient();
45784
- await client.request(`/v1/projects/${this.id}`, { method: "DELETE" });
45785
- this.context.stdout.write(`deleted project ${this.id}
45786
- `);
45787
- return 0;
45788
46105
  }
45789
46106
  };
45790
46107
  var RedactCommand = class extends Command {
@@ -45829,11 +46146,9 @@ var RedactStreamCommand = class extends Command {
45829
46146
  static usage = Command.Usage({
45830
46147
  description: "Stream stdin through the line-buffered redactor to stdout.",
45831
46148
  details: `
45832
- Used as a hook handler by per-agent integrations (e.g., Claude Code's
45833
- PostToolUse hook): \`keynv redact-stream\` reads its tool output on
45834
- stdin and writes a redacted version on stdout, preserving the original
45835
- line structure. Multi-line patterns are NOT applied here (see the
45836
- streaming-mode limitation in @keynv/redactor).
46149
+ Pipe any output through the line-buffered redactor to stdout.
46150
+ Preserves original line structure. Multi-line patterns are NOT applied
46151
+ here (see the streaming-mode limitation in the redactor package).
45837
46152
  `
45838
46153
  });
45839
46154
  async execute() {
@@ -45854,12 +46169,7 @@ init_cancel();
45854
46169
  init_pickProject();
45855
46170
  init_pickSecret();
45856
46171
  init_tty();
45857
- async function findProjectIdByName2(client, name) {
45858
- const data = await client.request("/v1/projects");
45859
- const match = data.projects.find((p2) => p2.name === name);
45860
- if (!match) throw new Error(`unknown project: ${name}`);
45861
- return match.id;
45862
- }
46172
+ var ALIAS_FORMAT_HINT = "Format: @<project>.<env>.<KEY> (project/env: lowercase kebab-case; key: letters, digits, _ or -)";
45863
46173
  function missingAlias(stderr) {
45864
46174
  stderr.write("keynv: missing <alias> (TTY required for interactive prompt).\n");
45865
46175
  return 1;
@@ -45890,45 +46200,50 @@ var SecretCreateCommand = class extends Command {
45890
46200
  });
45891
46201
  stdin = options_exports.Boolean("--stdin", false);
45892
46202
  async execute() {
45893
- const client = new ApiClient();
45894
- await client.ensureHydrated();
45895
- let alias2 = this.alias;
45896
- let value = this.value;
45897
- if (!alias2) {
45898
- if (!isInteractive()) return missingAlias(this.context.stderr);
45899
- try {
45900
- const built = await promptNewSecret(client);
45901
- if (!built) return 1;
45902
- alias2 = built.alias;
45903
- value = built.value;
45904
- } catch (err) {
45905
- if (err instanceof UserCancelled) return 130;
45906
- throw err;
46203
+ try {
46204
+ const client = new ApiClient();
46205
+ await client.ensureHydrated();
46206
+ let alias2 = this.alias;
46207
+ let value = this.value;
46208
+ if (!alias2) {
46209
+ if (!isInteractive()) return missingAlias(this.context.stderr);
46210
+ try {
46211
+ const built = await promptNewSecret(client);
46212
+ if (!built) return 1;
46213
+ alias2 = built.alias;
46214
+ value = built.value;
46215
+ } catch (err) {
46216
+ if (err instanceof UserCancelled) return 130;
46217
+ throw err;
46218
+ }
45907
46219
  }
45908
- }
45909
- const parsed = parseAlias(alias2);
45910
- if (!parsed) {
45911
- this.context.stderr.write(`keynv: invalid alias '${alias2}'. Expected @project.env.key.
46220
+ const parsed = parseAlias(alias2);
46221
+ if (!parsed) {
46222
+ this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46223
+ ${ALIAS_FORMAT_HINT}
45912
46224
  `);
45913
- return 1;
45914
- }
45915
- if (this.stdin) {
45916
- const chunks = [];
45917
- for await (const chunk of this.context.stdin) {
45918
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
46225
+ return 1;
45919
46226
  }
45920
- value = Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
45921
- } else if (value === void 0) {
45922
- value = await promptHidden("value: ");
45923
- }
45924
- const projectId2 = await findProjectIdByName2(client, parsed.project);
45925
- await client.request(`/v1/projects/${projectId2}/secrets`, {
45926
- method: "POST",
45927
- body: { env: parsed.environment, key: parsed.key, value }
45928
- });
45929
- this.context.stdout.write(`created ${parsed.literal}
46227
+ if (this.stdin) {
46228
+ const chunks = [];
46229
+ for await (const chunk of this.context.stdin) {
46230
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
46231
+ }
46232
+ value = Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
46233
+ } else if (value === void 0) {
46234
+ value = await promptHidden("value: ");
46235
+ }
46236
+ const projectId2 = await resolveProjectId(client, parsed.project);
46237
+ await client.request(`/v1/projects/${projectId2}/secrets`, {
46238
+ method: "POST",
46239
+ body: { env: parsed.environment, key: parsed.key, value }
46240
+ });
46241
+ this.context.stdout.write(`created ${parsed.literal}
45930
46242
  `);
45931
- return 0;
46243
+ return 0;
46244
+ } catch (err) {
46245
+ return handleExecError(this.context.stderr, err);
46246
+ }
45932
46247
  }
45933
46248
  };
45934
46249
  var SecretGetCommand = class extends Command {
@@ -45939,39 +46254,44 @@ var SecretGetCommand = class extends Command {
45939
46254
  alias = options_exports.String({ required: false });
45940
46255
  json = options_exports.Boolean("--json", false);
45941
46256
  async execute() {
45942
- const client = new ApiClient();
45943
- await client.ensureHydrated();
45944
- let alias2 = this.alias;
45945
- if (!alias2) {
45946
- if (!isInteractive()) return missingAlias(this.context.stderr);
45947
- try {
45948
- alias2 = await pickAliasInteractive(client) ?? void 0;
45949
- } catch (err) {
45950
- if (err instanceof UserCancelled) return 130;
45951
- throw err;
46257
+ try {
46258
+ const client = new ApiClient();
46259
+ await client.ensureHydrated();
46260
+ let alias2 = this.alias;
46261
+ if (!alias2) {
46262
+ if (!isInteractive()) return missingAlias(this.context.stderr);
46263
+ try {
46264
+ alias2 = await pickAliasInteractive(client) ?? void 0;
46265
+ } catch (err) {
46266
+ if (err instanceof UserCancelled) return 130;
46267
+ throw err;
46268
+ }
46269
+ if (!alias2) return 1;
45952
46270
  }
45953
- if (!alias2) return 1;
45954
- }
45955
- const parsed = parseAlias(alias2);
45956
- if (!parsed) {
45957
- this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46271
+ const parsed = parseAlias(alias2);
46272
+ if (!parsed) {
46273
+ this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46274
+ ${ALIAS_FORMAT_HINT}
45958
46275
  `);
45959
- return 1;
45960
- }
45961
- const projectId2 = await findProjectIdByName2(client, parsed.project);
45962
- const data = await client.request(
45963
- `/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}`
45964
- );
45965
- if (this.json) {
45966
- this.context.stdout.write(
45967
- `${JSON.stringify({ alias: data.alias, version: data.version, value: data.value })}
45968
- `
46276
+ return 1;
46277
+ }
46278
+ const projectId2 = await resolveProjectId(client, parsed.project);
46279
+ const data = await client.request(
46280
+ `/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}`
45969
46281
  );
45970
- } else {
45971
- this.context.stdout.write(`${data.value}
46282
+ if (this.json) {
46283
+ this.context.stdout.write(
46284
+ `${JSON.stringify({ alias: data.alias, version: data.version, value: data.value })}
46285
+ `
46286
+ );
46287
+ } else {
46288
+ this.context.stdout.write(`${data.value}
45972
46289
  `);
46290
+ }
46291
+ return 0;
46292
+ } catch (err) {
46293
+ return handleExecError(this.context.stderr, err);
45973
46294
  }
45974
- return 0;
45975
46295
  }
45976
46296
  };
45977
46297
  var SecretListCommand = class extends Command {
@@ -45982,42 +46302,47 @@ var SecretListCommand = class extends Command {
45982
46302
  project = options_exports.String({ required: false });
45983
46303
  json = options_exports.Boolean("--json", false);
45984
46304
  async execute() {
45985
- const client = new ApiClient();
45986
- await client.ensureHydrated();
45987
- let projectName = this.project;
45988
- if (!projectName) {
45989
- if (!isInteractive()) {
45990
- this.context.stderr.write("keynv: missing <project>.\n");
45991
- return 1;
45992
- }
45993
- try {
45994
- const picked = await pickProject(client, "Project");
45995
- if (!picked) return 1;
45996
- projectName = picked.name;
45997
- } catch (err) {
45998
- if (err instanceof UserCancelled) return 130;
45999
- throw err;
46305
+ try {
46306
+ const client = new ApiClient();
46307
+ await client.ensureHydrated();
46308
+ let projectName = this.project;
46309
+ if (!projectName) {
46310
+ if (!isInteractive()) {
46311
+ this.context.stderr.write("keynv: missing <project>.\n");
46312
+ return 1;
46313
+ }
46314
+ try {
46315
+ const picked = await pickProject(client, "Project");
46316
+ if (!picked) return 1;
46317
+ projectName = picked.name;
46318
+ } catch (err) {
46319
+ if (err instanceof UserCancelled) return 130;
46320
+ throw err;
46321
+ }
46000
46322
  }
46001
- }
46002
- const projectId2 = await findProjectIdByName2(client, projectName);
46003
- const data = await client.request(`/v1/projects/${projectId2}/secrets`);
46004
- if (this.json) {
46005
- this.context.stdout.write(`${JSON.stringify(data, null, 2)}
46323
+ const resolvedProjectName = projectName.startsWith("@") ? projectName.slice(1).split(".")[0] ?? projectName : projectName;
46324
+ const projectId2 = await resolveProjectId(client, resolvedProjectName);
46325
+ const data = await client.request(`/v1/projects/${projectId2}/secrets`);
46326
+ if (this.json) {
46327
+ this.context.stdout.write(`${JSON.stringify(data, null, 2)}
46006
46328
  `);
46329
+ return 0;
46330
+ }
46331
+ if (data.secrets.length === 0) {
46332
+ this.context.stdout.write("no secrets\n");
46333
+ return 0;
46334
+ }
46335
+ this.context.stdout.write(
46336
+ `${table(
46337
+ ["alias", "version", "created_at"],
46338
+ data.secrets.map((s) => [s.alias, String(s.version), s.created_at])
46339
+ )}
46340
+ `
46341
+ );
46007
46342
  return 0;
46343
+ } catch (err) {
46344
+ return handleExecError(this.context.stderr, err);
46008
46345
  }
46009
- if (data.secrets.length === 0) {
46010
- this.context.stdout.write("no secrets\n");
46011
- return 0;
46012
- }
46013
- this.context.stdout.write(
46014
- `${table(
46015
- ["alias", "version", "created_at"],
46016
- data.secrets.map((s) => [s.alias, String(s.version), s.created_at])
46017
- )}
46018
- `
46019
- );
46020
- return 0;
46021
46346
  }
46022
46347
  };
46023
46348
  var SecretRotateCommand = class extends Command {
@@ -46027,45 +46352,50 @@ var SecretRotateCommand = class extends Command {
46027
46352
  value = options_exports.String("--value");
46028
46353
  stdin = options_exports.Boolean("--stdin", false);
46029
46354
  async execute() {
46030
- const client = new ApiClient();
46031
- await client.ensureHydrated();
46032
- let alias2 = this.alias;
46033
- if (!alias2) {
46034
- if (!isInteractive()) return missingAlias(this.context.stderr);
46035
- try {
46036
- alias2 = await pickAliasInteractive(client) ?? void 0;
46037
- } catch (err) {
46038
- if (err instanceof UserCancelled) return 130;
46039
- throw err;
46355
+ try {
46356
+ const client = new ApiClient();
46357
+ await client.ensureHydrated();
46358
+ let alias2 = this.alias;
46359
+ if (!alias2) {
46360
+ if (!isInteractive()) return missingAlias(this.context.stderr);
46361
+ try {
46362
+ alias2 = await pickAliasInteractive(client) ?? void 0;
46363
+ } catch (err) {
46364
+ if (err instanceof UserCancelled) return 130;
46365
+ throw err;
46366
+ }
46367
+ if (!alias2) return 1;
46040
46368
  }
46041
- if (!alias2) return 1;
46042
- }
46043
- const parsed = parseAlias(alias2);
46044
- if (!parsed) {
46045
- this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46369
+ const parsed = parseAlias(alias2);
46370
+ if (!parsed) {
46371
+ this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46372
+ ${ALIAS_FORMAT_HINT}
46046
46373
  `);
46047
- return 1;
46048
- }
46049
- let value;
46050
- if (this.stdin) {
46051
- const chunks = [];
46052
- for await (const chunk of this.context.stdin) {
46053
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
46374
+ return 1;
46054
46375
  }
46055
- value = Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
46056
- } else if (this.value !== void 0) {
46057
- value = this.value;
46058
- } else {
46059
- value = await promptHidden("new value: ");
46060
- }
46061
- const projectId2 = await findProjectIdByName2(client, parsed.project);
46062
- const data = await client.request(
46063
- `/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}/rotate`,
46064
- { method: "POST", body: { new_value: value } }
46065
- );
46066
- this.context.stdout.write(`rotated ${data.alias} \u2192 v${data.version}
46376
+ let value;
46377
+ if (this.stdin) {
46378
+ const chunks = [];
46379
+ for await (const chunk of this.context.stdin) {
46380
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
46381
+ }
46382
+ value = Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
46383
+ } else if (this.value !== void 0) {
46384
+ value = this.value;
46385
+ } else {
46386
+ value = await promptHidden("new value: ");
46387
+ }
46388
+ const projectId2 = await resolveProjectId(client, parsed.project);
46389
+ const data = await client.request(
46390
+ `/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}/rotate`,
46391
+ { method: "POST", body: { new_value: value } }
46392
+ );
46393
+ this.context.stdout.write(`rotated ${data.alias} \u2192 v${data.version}
46067
46394
  `);
46068
- return 0;
46395
+ return 0;
46396
+ } catch (err) {
46397
+ return handleExecError(this.context.stderr, err);
46398
+ }
46069
46399
  }
46070
46400
  };
46071
46401
  var SecretDeleteCommand = class extends Command {
@@ -46073,31 +46403,170 @@ var SecretDeleteCommand = class extends Command {
46073
46403
  static usage = Command.Usage({ description: "Soft-delete a secret." });
46074
46404
  alias = options_exports.String({ required: false });
46075
46405
  async execute() {
46076
- const client = new ApiClient();
46077
- await client.ensureHydrated();
46078
- let alias2 = this.alias;
46079
- if (!alias2) {
46080
- if (!isInteractive()) return missingAlias(this.context.stderr);
46081
- try {
46082
- alias2 = await pickAliasInteractive(client) ?? void 0;
46083
- } catch (err) {
46084
- if (err instanceof UserCancelled) return 130;
46085
- throw err;
46406
+ try {
46407
+ const client = new ApiClient();
46408
+ await client.ensureHydrated();
46409
+ let alias2 = this.alias;
46410
+ if (!alias2) {
46411
+ if (!isInteractive()) return missingAlias(this.context.stderr);
46412
+ try {
46413
+ alias2 = await pickAliasInteractive(client) ?? void 0;
46414
+ } catch (err) {
46415
+ if (err instanceof UserCancelled) return 130;
46416
+ throw err;
46417
+ }
46418
+ if (!alias2) return 1;
46086
46419
  }
46087
- if (!alias2) return 1;
46088
- }
46089
- const parsed = parseAlias(alias2);
46090
- if (!parsed) {
46091
- this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46420
+ const parsed = parseAlias(alias2);
46421
+ if (!parsed) {
46422
+ this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46423
+ ${ALIAS_FORMAT_HINT}
46092
46424
  `);
46093
- return 1;
46425
+ return 1;
46426
+ }
46427
+ const projectId2 = await resolveProjectId(client, parsed.project);
46428
+ await client.request(`/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}`, {
46429
+ method: "DELETE"
46430
+ });
46431
+ this.context.stdout.write(`deleted ${parsed.literal}
46432
+ `);
46433
+ return 0;
46434
+ } catch (err) {
46435
+ return handleExecError(this.context.stderr, err);
46094
46436
  }
46095
- const projectId2 = await findProjectIdByName2(client, parsed.project);
46096
- await client.request(`/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}`, {
46097
- method: "DELETE"
46098
- });
46099
- this.context.stdout.write(`deleted ${parsed.literal}
46437
+ }
46438
+ };
46439
+ function generateSecret(bytes = 32) {
46440
+ return randomBytes(bytes).toString("base64");
46441
+ }
46442
+ var ServerInitCommand = class extends Command {
46443
+ static paths = [["server", "init"]];
46444
+ static usage = Command.Usage({
46445
+ description: "Generate secrets and deployment files for a new keynv server.",
46446
+ details: `
46447
+ Interactive onboarding wizard that generates the configuration
46448
+ a fresh keynv server needs: JWT signing secret, master encryption
46449
+ key, and deployment templates.
46450
+
46451
+ Outputs:
46452
+ \u2022 A .keynv-server.env file you feed into your deployment
46453
+ (Docker Compose, Coolify, or bare-metal).
46454
+ \u2022 Optionally a docker-compose.yml in the current directory.
46455
+
46456
+ Non-interactive with --yes to skip prompts.
46457
+ `,
46458
+ examples: [
46459
+ ["Interactive wizard", "$0 server init"],
46460
+ ["Non-interactive (CI/scripts)", "$0 server init --yes"],
46461
+ ["Docker Compose output", "$0 server init --compose"],
46462
+ ["Custom output path", "$0 server init --out ./config/env"]
46463
+ ]
46464
+ });
46465
+ yes = options_exports.Boolean("--yes", false, {
46466
+ description: "Skip all prompts (non-interactive)."
46467
+ });
46468
+ compose = options_exports.Boolean("--compose", false, {
46469
+ description: "Also write a docker-compose.yml to the current directory."
46470
+ });
46471
+ out = options_exports.String("--out", {
46472
+ description: "Write server env file to this path (default: .keynv-server.env)."
46473
+ });
46474
+ async execute() {
46475
+ const jwtSecret = generateSecret(48);
46476
+ const masterKey = generateSecret(32);
46477
+ const webSessionSecret = generateSecret(48);
46478
+ const outPath = this.out ?? ".keynv-server.env";
46479
+ const contents = [
46480
+ "# Generated by `keynv server init`.",
46481
+ "# Keep this file secret \u2014 it contains cryptographic key material.",
46482
+ `KEYNV_JWT_SECRET=${jwtSecret}`,
46483
+ `KEYNV_MASTER_KEY=${masterKey}`,
46484
+ "",
46485
+ "# Web dashboard session secret (required for Next.js JWE session tokens).",
46486
+ `KEYNV_WEB_SESSION_SECRET=${webSessionSecret}`,
46487
+ "",
46488
+ "# Server configuration (uncomment and adjust):",
46489
+ "# KEYNV_PORT=8080",
46490
+ "# KEYNV_WEB_URL=https://keynv.example.com",
46491
+ "# KEYNV_PUBLIC_REGISTRATION=false",
46492
+ "# KEYNV_DB_PATH=./keynv.db",
46493
+ "# KEYNV_MASTER_KEY_FILE=./master.key",
46494
+ "# KEYNV_RATE_LIMIT_PER_MINUTE=120",
46495
+ "# KEYNV_ARGON2_MEMORY_KIB=46080",
46496
+ "# KEYNV_ARGON2_TIME_COST=3"
46497
+ ].join("\n");
46498
+ const { writeFileSync: writeFileSync4 } = await import('fs');
46499
+ writeFileSync4(outPath, `${contents}
46100
46500
  `);
46501
+ this.context.stdout.write(`Wrote ${outPath}
46502
+
46503
+ `);
46504
+ this.context.stdout.write("Generated secrets:\n");
46505
+ this.context.stdout.write(
46506
+ ` KEYNV_JWT_SECRET ${jwtSecret.slice(0, 8)}... (base64, 48 bytes)
46507
+ `
46508
+ );
46509
+ this.context.stdout.write(
46510
+ ` KEYNV_MASTER_KEY ${masterKey.slice(0, 8)}... (base64, 32 bytes)
46511
+ `
46512
+ );
46513
+ this.context.stdout.write(
46514
+ ` KEYNV_WEB_SESSION_SECRET ${webSessionSecret.slice(0, 8)}... (base64, 48 bytes)
46515
+ `
46516
+ );
46517
+ this.context.stdout.write("\nNext steps:\n");
46518
+ this.context.stdout.write(` 1. Feed ${outPath} into your deployment:
46519
+ `);
46520
+ this.context.stdout.write(" docker compose --env-file .keynv-server.env up -d\n");
46521
+ this.context.stdout.write(" or load it into Coolify as environment variables.\n");
46522
+ this.context.stdout.write(" 2. Bootstrap the first owner:\n");
46523
+ this.context.stdout.write(
46524
+ " docker exec -it <server-container> node dist/bootstrap.js \\\n"
46525
+ );
46526
+ this.context.stdout.write(" --owner-email alice@example.com \\\n");
46527
+ this.context.stdout.write(' --owner-password "<a long random password>" \\\n');
46528
+ this.context.stdout.write(' --org-name "Acme Inc"\n');
46529
+ this.context.stdout.write(" 3. Install the CLI and log in:\n");
46530
+ this.context.stdout.write(" npm install -g @keynv/cli\n");
46531
+ this.context.stdout.write(" keynv login --server https://keynv.example.com\n");
46532
+ this.context.stdout.write(" 4. Onboard your first project:\n");
46533
+ this.context.stdout.write(" cd your-project && keynv init\n");
46534
+ if (this.compose) {
46535
+ const composeContent = `# keynv server + Litestream sidecar
46536
+ # Generated by \`keynv server init\`.
46537
+ # Load with: docker compose --env-file .keynv-server.env up -d
46538
+
46539
+ services:
46540
+ server:
46541
+ image: ghcr.io/keynv-labs/keynv-server:latest
46542
+ restart: unless-stopped
46543
+ ports:
46544
+ - '8080:8080'
46545
+ env_file:
46546
+ - .keynv-server.env
46547
+ volumes:
46548
+ - keynv-data:/data
46549
+
46550
+ litestream:
46551
+ image: litestream/litestream:latest
46552
+ restart: unless-stopped
46553
+ volumes:
46554
+ - keynv-data:/data
46555
+ command: replicate
46556
+ environment:
46557
+ LITESTREAM_DB_PATH: /data/keynv.db
46558
+ LITESTREAM_REPLICA_URL: s3://your-bucket/keynv
46559
+ AWS_ACCESS_KEY_ID: \${LITESTREAM_AWS_ACCESS_KEY_ID}
46560
+ AWS_SECRET_ACCESS_KEY: \${LITESTREAM_AWS_SECRET_ACCESS_KEY}
46561
+
46562
+ volumes:
46563
+ keynv-data:
46564
+ `;
46565
+ writeFileSync4("docker-compose.yml", composeContent);
46566
+ this.context.stdout.write(
46567
+ "\nWrote docker-compose.yml (update Litestream S3 config before deploying).\n"
46568
+ );
46569
+ }
46101
46570
  return 0;
46102
46571
  }
46103
46572
  };
@@ -46453,18 +46922,9 @@ var TESTERS = [
46453
46922
  sshTester,
46454
46923
  httpTester
46455
46924
  ];
46456
- function findTester(type) {
46457
- return TESTERS.find((t) => t.type === type) ?? null;
46458
- }
46459
46925
 
46460
46926
  // src/commands/test.ts
46461
46927
  init_http();
46462
- async function findProjectIdByName3(client, name) {
46463
- const data = await client.request("/v1/projects");
46464
- const match = data.projects.find((p2) => p2.name === name);
46465
- if (!match) throw new Error(`unknown project: ${name}`);
46466
- return match.id;
46467
- }
46468
46928
  function parseTargets(specs) {
46469
46929
  const out = {};
46470
46930
  for (const spec of specs) {
@@ -46518,12 +46978,17 @@ the error message.
46518
46978
  );
46519
46979
  return 2;
46520
46980
  }
46521
- const tester = findTester(this.as);
46981
+ const tester = TESTERS.find((t) => t.type === this.as);
46522
46982
  if (!tester) {
46523
46983
  this.context.stderr.write(`keynv: unknown tester '${this.as}'
46524
46984
  `);
46525
46985
  return 1;
46526
46986
  }
46987
+ const timeoutMs = this.timeout ? Number.parseInt(this.timeout, 10) * 1e3 : void 0;
46988
+ if (this.timeout && (timeoutMs === void 0 || Number.isNaN(timeoutMs))) {
46989
+ this.context.stderr.write("keynv: invalid --timeout (expected integer seconds)\n");
46990
+ return 2;
46991
+ }
46527
46992
  let target;
46528
46993
  try {
46529
46994
  target = parseTargets(this.targets ?? []);
@@ -46540,7 +47005,7 @@ the error message.
46540
47005
  }
46541
47006
  let value;
46542
47007
  try {
46543
- const projectId2 = await findProjectIdByName3(client, parsedAlias.project);
47008
+ const projectId2 = await resolveProjectId(client, parsedAlias.project);
46544
47009
  const data = await client.request(
46545
47010
  `/v1/projects/${projectId2}/secrets/${parsedAlias.environment}/${parsedAlias.key}`
46546
47011
  );
@@ -46551,11 +47016,6 @@ the error message.
46551
47016
  `);
46552
47017
  return 1;
46553
47018
  }
46554
- const timeoutMs = this.timeout ? Number.parseInt(this.timeout, 10) * 1e3 : void 0;
46555
- if (this.timeout && (timeoutMs === void 0 || Number.isNaN(timeoutMs))) {
46556
- this.context.stderr.write("keynv: invalid --timeout (expected integer seconds)\n");
46557
- return 2;
46558
- }
46559
47019
  const result = await runTest({
46560
47020
  tester,
46561
47021
  secret: { alias: parsedAlias.literal, value },
@@ -46597,25 +47057,6 @@ the error message.
46597
47057
  }
46598
47058
  };
46599
47059
 
46600
- // src/commands/ui.ts
46601
- init_tty();
46602
- init_menu();
46603
- var UICommand = class extends Command {
46604
- static paths = [["ui"]];
46605
- static usage = Command.Usage({
46606
- description: "Open the interactive menu (also runs by default when no args are given)."
46607
- });
46608
- async execute() {
46609
- if (!isInteractive()) {
46610
- this.context.stdout.write(
46611
- "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"
46612
- );
46613
- return 0;
46614
- }
46615
- return runMenu();
46616
- }
46617
- };
46618
-
46619
47060
  // src/index.ts
46620
47061
  init_format();
46621
47062
  init_version();
@@ -46649,7 +47090,7 @@ cli.register(InitCommand);
46649
47090
  cli.register(RedactCommand);
46650
47091
  cli.register(RedactStreamCommand);
46651
47092
  cli.register(TestCommand);
46652
- cli.register(UICommand);
47093
+ cli.register(ServerInitCommand);
46653
47094
  var argv = process.argv.slice(2);
46654
47095
  if (argv.length === 0) {
46655
47096
  const { runMenu: runMenu2 } = await Promise.resolve().then(() => (init_menu(), menu_exports));