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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'module';
3
- import 'crypto';
3
+ import { randomBytes } from 'crypto';
4
4
  import { readFileSync, existsSync, statSync, writeFileSync, unlinkSync, mkdirSync, rmSync, readdirSync } from 'fs';
5
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.12" ;
1060
1060
  AGENT = `keynv-cli/${VERSION}`;
1061
1061
  }
1062
1062
  });
@@ -1146,10 +1146,10 @@ var init_parser = __esm({
1146
1146
  "../../packages/core/dist/reference/parser.js"() {
1147
1147
  PROJECT_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
1148
1148
  ENV_RE = /^[a-z0-9][a-z0-9-]{0,23}$/;
1149
- KEY_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
1149
+ KEY_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
1150
1150
  TRAILING_BOUNDARY = "(?!\\w|\\.[a-z0-9])";
1151
- TEXT_FIND_SOURCE = `(?<![\\w.@/])@[a-z0-9][a-z0-9-]{0,47}\\.[a-z0-9][a-z0-9-]{0,23}\\.[a-z0-9][a-z0-9_-]{0,63}${TRAILING_BOUNDARY}`;
1152
- ARGV_FIND_SOURCE = `@[a-z0-9][a-z0-9-]{0,47}\\.[a-z0-9][a-z0-9-]{0,23}\\.[a-z0-9][a-z0-9_-]{0,63}${TRAILING_BOUNDARY}`;
1151
+ TEXT_FIND_SOURCE = `(?<![\\w.@/])@[a-z0-9][a-z0-9-]{0,47}\\.[a-z0-9][a-z0-9-]{0,23}\\.[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}${TRAILING_BOUNDARY}`;
1152
+ ARGV_FIND_SOURCE = `@[a-z0-9][a-z0-9-]{0,47}\\.[a-z0-9][a-z0-9-]{0,23}\\.[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}${TRAILING_BOUNDARY}`;
1153
1153
  TEXT_FIND_RE = new RegExp(TEXT_FIND_SOURCE, "g");
1154
1154
  ARGV_FIND_RE = new RegExp(ARGV_FIND_SOURCE, "g");
1155
1155
  }
@@ -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({
@@ -5723,6 +5732,21 @@ var init_format = __esm({
5723
5732
  "src/ui/format.ts"() {
5724
5733
  }
5725
5734
  });
5735
+ function walkUp(startDir, predicate) {
5736
+ let dir = resolve(startDir);
5737
+ for (let i = 0; i < 64; i++) {
5738
+ const result = predicate(dir);
5739
+ if (result !== null && result !== void 0) return result;
5740
+ const parent = dirname(dir);
5741
+ if (parent === dir) return null;
5742
+ dir = parent;
5743
+ }
5744
+ return null;
5745
+ }
5746
+ var init_fs = __esm({
5747
+ "src/util/fs.ts"() {
5748
+ }
5749
+ });
5726
5750
  function parseEnvFile(content, filename) {
5727
5751
  const normalized = content.charCodeAt(0) === 65279 ? content.slice(1) : content;
5728
5752
  const lines = normalized.split(/\r?\n/);
@@ -5781,20 +5805,14 @@ function parseEnvFile(content, filename) {
5781
5805
  return entries;
5782
5806
  }
5783
5807
  function findEnvFile(startDir) {
5784
- let dir = resolve(startDir);
5785
- for (let i = 0; i < 64; i++) {
5808
+ return walkUp(startDir, (dir) => {
5786
5809
  const candidate = join(dir, ENV_FILE_BASENAME);
5787
- if (existsSync(candidate)) {
5788
- try {
5789
- if (statSync(candidate).isFile()) return candidate;
5790
- } catch {
5791
- }
5810
+ try {
5811
+ if (statSync(candidate).isFile()) return candidate;
5812
+ } catch {
5792
5813
  }
5793
- const parent = dirname(dir);
5794
- if (parent === dir) return null;
5795
- dir = parent;
5796
- }
5797
- return null;
5814
+ return null;
5815
+ });
5798
5816
  }
5799
5817
  function loadEnvFile(opts) {
5800
5818
  if (opts.disabled) return null;
@@ -5819,6 +5837,7 @@ var MAX_FILE_BYTES, KEY_RE2, ENV_FILE_BASENAME, EnvFileParseError, EnvFileNotFou
5819
5837
  var init_envFile = __esm({
5820
5838
  "src/exec/envFile.ts"() {
5821
5839
  init_dist();
5840
+ init_fs();
5822
5841
  MAX_FILE_BYTES = 1e6;
5823
5842
  KEY_RE2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
5824
5843
  ENV_FILE_BASENAME = ".keynv.env";
@@ -5855,6 +5874,132 @@ var init_envFile = __esm({
5855
5874
  }
5856
5875
  });
5857
5876
 
5877
+ // src/init/heuristics.ts
5878
+ function shannonEntropyBits(s) {
5879
+ if (s.length === 0) return 0;
5880
+ const counts = /* @__PURE__ */ new Map();
5881
+ for (const ch of s) counts.set(ch, (counts.get(ch) ?? 0) + 1);
5882
+ let h2 = 0;
5883
+ for (const c2 of counts.values()) {
5884
+ const p2 = c2 / s.length;
5885
+ h2 -= p2 * Math.log2(p2);
5886
+ }
5887
+ return h2;
5888
+ }
5889
+ function nameMatchesLiteralPrefix(name) {
5890
+ const upper = name.toUpperCase();
5891
+ return NAME_LITERAL_PREFIX.some((p2) => upper.startsWith(p2));
5892
+ }
5893
+ function nameMatchesSecretSuffix(name) {
5894
+ const upper = name.toUpperCase();
5895
+ for (const { suffix, hint } of NAME_SECRET_SUFFIXES) {
5896
+ if (upper.endsWith(suffix) || upper.endsWith(`_${suffix}`)) {
5897
+ return { matched: true, hint };
5898
+ }
5899
+ }
5900
+ return { matched: false, hint: "" };
5901
+ }
5902
+ function classifyEntry(name, value) {
5903
+ const upper = name.toUpperCase();
5904
+ if (NAME_FRAMEWORK_MANAGED.has(upper)) {
5905
+ return { verdict: "skip", hint: "framework/shell-managed" };
5906
+ }
5907
+ if (NAME_LITERAL_EXACT.has(upper)) {
5908
+ return { verdict: "literal", hint: "common config var" };
5909
+ }
5910
+ if (nameMatchesLiteralPrefix(name)) {
5911
+ return { verdict: "literal", hint: "public env, build-time bundled" };
5912
+ }
5913
+ if (value.length === 0) {
5914
+ return { verdict: "literal", hint: "empty" };
5915
+ }
5916
+ for (const { re, hint } of VALUE_PATTERNS) {
5917
+ if (re.test(value)) return { verdict: "secret", hint };
5918
+ }
5919
+ const suffix = nameMatchesSecretSuffix(name);
5920
+ if (suffix.matched) {
5921
+ return { verdict: "secret", hint: suffix.hint };
5922
+ }
5923
+ if (NAME_DB_URL.test(name)) {
5924
+ return { verdict: "secret", hint: "database URL" };
5925
+ }
5926
+ if (value.length >= 32) {
5927
+ const bits = shannonEntropyBits(value);
5928
+ if (bits >= 3.5) {
5929
+ return { verdict: "secret", hint: `${value.length}-char random-looking string` };
5930
+ }
5931
+ }
5932
+ return { verdict: "ambiguous", hint: "" };
5933
+ }
5934
+ function previewValue(value, max = 40) {
5935
+ if (value.length === 0) return "(empty)";
5936
+ if (value.length <= max) return value;
5937
+ return `${value.slice(0, max - 1)}\u2026`;
5938
+ }
5939
+ var NAME_FRAMEWORK_MANAGED, NAME_LITERAL_EXACT, NAME_LITERAL_PREFIX, NAME_SECRET_SUFFIXES, NAME_DB_URL, VALUE_PATTERNS;
5940
+ var init_heuristics = __esm({
5941
+ "src/init/heuristics.ts"() {
5942
+ NAME_FRAMEWORK_MANAGED = /* @__PURE__ */ new Set([
5943
+ "NODE_ENV",
5944
+ "PORT",
5945
+ "HOST",
5946
+ "HOSTNAME",
5947
+ "PATH",
5948
+ "HOME",
5949
+ "USER",
5950
+ "SHELL",
5951
+ "PWD",
5952
+ "CI"
5953
+ ]);
5954
+ NAME_LITERAL_EXACT = /* @__PURE__ */ new Set([
5955
+ "DEBUG",
5956
+ "LOG_LEVEL",
5957
+ "LOGLEVEL",
5958
+ "TZ",
5959
+ "LANG",
5960
+ "LC_ALL",
5961
+ "NODE_OPTIONS",
5962
+ "NPM_CONFIG_LOGLEVEL",
5963
+ "TS_NODE_PROJECT"
5964
+ ]);
5965
+ NAME_LITERAL_PREFIX = ["NEXT_PUBLIC_", "VITE_", "REACT_APP_", "PUBLIC_", "EXPO_PUBLIC_"];
5966
+ NAME_SECRET_SUFFIXES = [
5967
+ { suffix: "PRIVATE_KEY", hint: "private key" },
5968
+ { suffix: "API_KEY", hint: "API key" },
5969
+ { suffix: "ACCESS_KEY", hint: "access key" },
5970
+ { suffix: "SECRET_KEY", hint: "secret key" },
5971
+ { suffix: "SECRET", hint: "secret" },
5972
+ { suffix: "PASSWORD", hint: "password" },
5973
+ { suffix: "PASSPHRASE", hint: "passphrase" },
5974
+ { suffix: "TOKEN", hint: "token" },
5975
+ { suffix: "KEY", hint: "key" },
5976
+ { suffix: "CREDENTIALS", hint: "credentials" },
5977
+ { suffix: "AUTH", hint: "auth" },
5978
+ { suffix: "DSN", hint: "connection string" }
5979
+ ];
5980
+ NAME_DB_URL = /^(DATABASE|DB|POSTGRES|MYSQL|MONGO|REDIS)_URL$/i;
5981
+ VALUE_PATTERNS = [
5982
+ { re: /^sk-proj-/, hint: "OpenAI project key" },
5983
+ { re: /^sk-[A-Za-z0-9]{20,}/, hint: "OpenAI key" },
5984
+ { re: /^sk_live_/, hint: "Stripe live key" },
5985
+ { re: /^sk_test_/, hint: "Stripe test key" },
5986
+ { re: /^pk_live_/, hint: "Stripe publishable \u2014 often public, double-check" },
5987
+ { re: /^xoxb-/, hint: "Slack bot token" },
5988
+ { re: /^xoxp-/, hint: "Slack user token" },
5989
+ { re: /^ghp_[A-Za-z0-9]{30,}/, hint: "GitHub personal access token" },
5990
+ { re: /^github_pat_/, hint: "GitHub fine-grained PAT" },
5991
+ { re: /^gho_/, hint: "GitHub OAuth token" },
5992
+ { re: /^AKIA[0-9A-Z]{16}$/, hint: "AWS access key id" },
5993
+ { re: /^ASIA[0-9A-Z]{16}$/, hint: "AWS temporary key id" },
5994
+ { re: /^AIza[0-9A-Za-z_-]{35}$/, hint: "Google API key" },
5995
+ { re: /^ya29\./, hint: "Google OAuth access token" },
5996
+ { re: /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, hint: "JWT" },
5997
+ { re: /^-----BEGIN [A-Z ]+PRIVATE KEY-----/, hint: "PEM private key" },
5998
+ { re: /^([a-z]+):\/\/[^@]+:[^@]+@/i, hint: "connection string with credentials" }
5999
+ ];
6000
+ }
6001
+ });
6002
+
5858
6003
  // ../../node_modules/.pnpm/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/utils.js
5859
6004
  var getCodePointsLength, isFullWidth, isWideNotCJKTNotEmoji;
5860
6005
  var init_utils = __esm({
@@ -7099,22 +7244,19 @@ var init_aiContext = __esm({
7099
7244
  }
7100
7245
  });
7101
7246
  function findProjectRoot(startDir) {
7102
- let dir = resolve(startDir);
7103
- for (let i = 0; i < 64; i++) {
7247
+ const result = walkUp(startDir, (dir) => {
7104
7248
  for (const marker of PROJECT_MARKERS) {
7105
- const candidate = join(dir, marker);
7106
- if (existsSync(candidate)) {
7107
- return buildRoot(dir, marker);
7249
+ if (existsSync(join(dir, marker))) {
7250
+ return { dir, marker };
7108
7251
  }
7109
7252
  }
7110
7253
  if (existsSync(join(dir, GIT_MARKER))) {
7111
- return buildRoot(dir, GIT_MARKER);
7254
+ return { dir, marker: GIT_MARKER };
7112
7255
  }
7113
- const parent = dirname(dir);
7114
- if (parent === dir) return null;
7115
- dir = parent;
7116
- }
7117
- return null;
7256
+ return null;
7257
+ });
7258
+ if (!result) return null;
7259
+ return buildRoot(result.dir, result.marker);
7118
7260
  }
7119
7261
  function buildRoot(dir, marker) {
7120
7262
  let suggestedName = basename(dir);
@@ -7199,6 +7341,7 @@ function hasExistingKeynvEnv(rootDir) {
7199
7341
  var PROJECT_MARKERS, GIT_MARKER, ENV_GLOB, ENV_EXAMPLE, KEYNV_ENV_BASENAME;
7200
7342
  var init_detect = __esm({
7201
7343
  "src/init/detect.ts"() {
7344
+ init_fs();
7202
7345
  PROJECT_MARKERS = [
7203
7346
  "package.json",
7204
7347
  "pyproject.toml",
@@ -7216,132 +7359,6 @@ var init_detect = __esm({
7216
7359
  }
7217
7360
  });
7218
7361
 
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
7362
  // src/init/scriptWrap.ts
7346
7363
  function analyzeScript(name, command) {
7347
7364
  const trimmed = command.trim();
@@ -7989,7 +8006,8 @@ function composeKeynvEnv(opts) {
7989
8006
  const { uploadedAliases, literals, mergeWithExisting } = opts;
7990
8007
  const lines = [];
7991
8008
  if (mergeWithExisting !== null) {
7992
- const trimmed = mergeWithExisting.replace(/\n+$/, "");
8009
+ const normalized = mergeWithExisting.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
8010
+ const trimmed = normalized.replace(/\n+$/, "");
7993
8011
  if (trimmed.length > 0) {
7994
8012
  lines.push(...trimmed.split("\n"));
7995
8013
  lines.push("");
@@ -8099,8 +8117,27 @@ async function runBrowserAuth(serverUrl) {
8099
8117
  }
8100
8118
  const start = await startRes.json();
8101
8119
  const opened = openBrowser(start.verification_uri_complete);
8102
- if (!opened) {
8103
- throw new BrowserAuthError(`Open this URL in your browser: ${start.verification_uri_complete}`);
8120
+ if (opened) {
8121
+ process.stderr.write(
8122
+ `
8123
+ Your code: ${start.user_code}
8124
+ Complete auth in your browser, then return here.
8125
+
8126
+ `
8127
+ );
8128
+ } else {
8129
+ process.stderr.write(
8130
+ `
8131
+ Could not open a browser automatically.
8132
+ Open this URL manually:
8133
+
8134
+ ${start.verification_uri_complete}
8135
+
8136
+ Your code: ${start.user_code}
8137
+ Waiting for you to complete auth in the browser...
8138
+
8139
+ `
8140
+ );
8104
8141
  }
8105
8142
  const deadline = Date.now() + start.expires_in * 1e3;
8106
8143
  const intervalMs = Math.max(1, start.interval) * 1e3;
@@ -28679,7 +28716,7 @@ var require_utils_webcrypto = __commonJS({
28679
28716
  var nodeCrypto = __require("crypto");
28680
28717
  module.exports = {
28681
28718
  postgresMd5PasswordHash,
28682
- randomBytes,
28719
+ randomBytes: randomBytes2,
28683
28720
  deriveKey,
28684
28721
  sha256,
28685
28722
  hashByName,
@@ -28689,7 +28726,7 @@ var require_utils_webcrypto = __commonJS({
28689
28726
  var webCrypto = nodeCrypto.webcrypto || globalThis.crypto;
28690
28727
  var subtleCrypto = webCrypto.subtle;
28691
28728
  var textEncoder = new TextEncoder();
28692
- function randomBytes(length) {
28729
+ function randomBytes2(length) {
28693
28730
  return webCrypto.getRandomValues(Buffer.alloc(length));
28694
28731
  }
28695
28732
  async function md5(string) {
@@ -42554,7 +42591,7 @@ var init_audit2 = __esm({
42554
42591
  async function runLoginFlow(client, options = {}) {
42555
42592
  const server = options.server ?? DEFAULT_SERVER_URL;
42556
42593
  const s = ft();
42557
- s.start("Opening browser for keynv login");
42594
+ s.start("Waiting for authorization");
42558
42595
  let creds;
42559
42596
  try {
42560
42597
  creds = await runBrowserAuth(server);
@@ -42814,13 +42851,17 @@ async function runMenu() {
42814
42851
  if (u) R2.message(`${u.email} (${u.org_role}) @ ${u.server_url}`);
42815
42852
  }
42816
42853
  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 });
42854
+ const root = findProjectRoot(process.cwd());
42855
+ const alreadyInitialized = root !== null && hasExistingKeynvEnv(root.path);
42856
+ if (!alreadyInitialized) {
42857
+ const setup = await ue({
42858
+ message: "Set up this project now?",
42859
+ initialValue: true
42860
+ });
42861
+ if (!q(setup) && setup) {
42862
+ const { runInitFlow: runInitFlow2 } = await Promise.resolve().then(() => (init_init(), init_exports));
42863
+ await runInitFlow2(client, { cwd: process.cwd(), dryRun: false, noScripts: false });
42864
+ }
42824
42865
  }
42825
42866
  }
42826
42867
  while (true) {
@@ -42889,6 +42930,7 @@ var init_menu = __esm({
42889
42930
  "src/ui/menu.ts"() {
42890
42931
  init_dist5();
42891
42932
  init_http();
42933
+ init_detect();
42892
42934
  init_store();
42893
42935
  init_version();
42894
42936
  init_audit2();
@@ -44719,11 +44761,10 @@ async function resolveAllAliases(client, argv2, extraStrings = []) {
44719
44761
  return resolved;
44720
44762
  }
44721
44763
  function substitute(text, resolved) {
44764
+ const sorted = [...resolved].sort((a, b2) => b2.alias.literal.length - a.alias.literal.length);
44722
44765
  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
- }
44766
+ for (const r of sorted) {
44767
+ out = out.split(r.alias.literal).join(r.value);
44727
44768
  }
44728
44769
  return out;
44729
44770
  }
@@ -44826,10 +44867,18 @@ var BUILTIN_LINE_PATTERNS = BUILTIN_PATTERNS.filter((p2) => !p2.multiline);
44826
44867
 
44827
44868
  // ../../packages/redactor/dist/entropy.js
44828
44869
  var TOKEN_BOUNDARY_RE = /[\s,;:'"<>(){}[\]=]+/;
44870
+ var DEFAULT_EXCLUDE_PREFIXES = [
44871
+ "sha1-",
44872
+ "sha256:",
44873
+ "sha256-",
44874
+ "sha384-",
44875
+ "sha512-"
44876
+ ];
44829
44877
  var DEFAULTS = {
44830
44878
  enabled: true,
44831
44879
  minLength: 24,
44832
- minBitsPerChar: 4.5
44880
+ minBitsPerChar: 4.5,
44881
+ excludePrefixes: DEFAULT_EXCLUDE_PREFIXES
44833
44882
  };
44834
44883
  function shannonEntropy(s) {
44835
44884
  if (s.length === 0)
@@ -44862,9 +44911,13 @@ function findEntropyMatches(text, opts = {}) {
44862
44911
  const end = i;
44863
44912
  const token = text.slice(start, end);
44864
44913
  if (token.length >= cfg.minLength) {
44865
- const h2 = shannonEntropy(token);
44866
- if (h2 >= cfg.minBitsPerChar) {
44867
- matches.push({ start, end, token });
44914
+ const lower = token.toLowerCase();
44915
+ const excluded = cfg.excludePrefixes.some((p2) => lower.startsWith(p2));
44916
+ if (!excluded) {
44917
+ const h2 = shannonEntropy(token);
44918
+ if (h2 >= cfg.minBitsPerChar) {
44919
+ matches.push({ start, end, token });
44920
+ }
44868
44921
  }
44869
44922
  }
44870
44923
  }
@@ -45008,7 +45061,19 @@ var ENV_ALLOWLIST = [
45008
45061
  "PWD",
45009
45062
  "OLDPWD",
45010
45063
  "TMPDIR",
45011
- "SSH_AUTH_SOCK"
45064
+ "SSH_AUTH_SOCK",
45065
+ // Windows-specific
45066
+ "USERPROFILE",
45067
+ "USERNAME",
45068
+ "COMPUTERNAME",
45069
+ "TEMP",
45070
+ "TMP",
45071
+ "SYSTEMROOT",
45072
+ "SYSTEMDRIVE",
45073
+ "WINDIR",
45074
+ "COMSPEC",
45075
+ "APPDATA",
45076
+ "LOCALAPPDATA"
45012
45077
  ];
45013
45078
  function spawnPrivileged(opts) {
45014
45079
  const startedAt = Date.now();
@@ -45024,7 +45089,8 @@ function spawnPrivileged(opts) {
45024
45089
  const child = spawn(opts.command, opts.args, {
45025
45090
  env: env2,
45026
45091
  stdio,
45027
- detached: false
45092
+ detached: false,
45093
+ shell: process.platform === "win32"
45028
45094
  });
45029
45095
  const literals = opts.resolved.map((r) => r.value).filter((v2) => v2.length > 0);
45030
45096
  if (!opts.noRedact && child.stdout) {
@@ -45152,7 +45218,11 @@ spelled \`--from\` to avoid the collision.)
45152
45218
  `);
45153
45219
  return 2;
45154
45220
  }
45155
- throw err;
45221
+ this.context.stderr.write(
45222
+ `keynv: unexpected error loading env file: ${err instanceof Error ? err.message : String(err)}
45223
+ `
45224
+ );
45225
+ return 2;
45156
45226
  }
45157
45227
  const viaEnvSpecs = [];
45158
45228
  for (const spec of this.viaEnv ?? []) {
@@ -45224,11 +45294,18 @@ spelled \`--from\` to avoid the collision.)
45224
45294
  injectedEnv[spec.name] = value;
45225
45295
  }
45226
45296
  if (envFileLoaded && !this.quiet) {
45227
- const total = envFileLoaded.entries.length;
45228
- const aliasCount = envFileLoaded.entries.filter((e2) => e2.isAlias).length;
45297
+ const aliasEntries = envFileLoaded.entries.filter((e2) => e2.isAlias);
45298
+ const plainEntries = envFileLoaded.entries.filter((e2) => !e2.isAlias);
45299
+ const displayPath = relative(process.cwd(), envFileLoaded.path) || envFileLoaded.path;
45300
+ const parts = [];
45301
+ if (aliasEntries.length > 0) {
45302
+ parts.push(aliasEntries.map((e2) => `${e2.name}=${e2.value}`).join(", ") + " (vault)");
45303
+ }
45304
+ if (plainEntries.length > 0) {
45305
+ parts.push(plainEntries.map((e2) => e2.name).join(", ") + " (plain)");
45306
+ }
45229
45307
  this.context.stderr.write(
45230
- `keynv: loaded ${total} var${total === 1 ? "" : "s"} from ${envFileLoaded.path} (${aliasCount} resolved from vault)
45231
- `
45308
+ `keynv: loaded ${displayPath}` + (parts.length > 0 ? ` \u2014 ${parts.join("; ")}` : "") + "\n"
45232
45309
  );
45233
45310
  }
45234
45311
  const timeoutS = this.timeout ? Number.parseInt(this.timeout, 10) : void 0;
@@ -45268,10 +45345,142 @@ function signalNumber(sig) {
45268
45345
  }
45269
45346
 
45270
45347
  // src/commands/init.ts
45348
+ init_dist();
45271
45349
  init_http();
45350
+ init_envFile();
45351
+ init_heuristics();
45272
45352
  init_init();
45273
45353
  init_cancel();
45274
45354
  init_tty();
45355
+
45356
+ // src/commands/project.ts
45357
+ init_http();
45358
+ init_format();
45359
+ function isProjectId(input) {
45360
+ return input.startsWith("p_") || /^[a-f0-9]{20,}$/i.test(input);
45361
+ }
45362
+ async function resolveProjectId(client, input) {
45363
+ if (isProjectId(input)) {
45364
+ return input;
45365
+ }
45366
+ const data = await client.request("/v1/projects");
45367
+ const match = data.projects.find((p2) => p2.name === input);
45368
+ if (!match) throw new Error(`project not found: ${input}`);
45369
+ return match.id;
45370
+ }
45371
+ var ProjectListCommand = class extends Command {
45372
+ static paths = [["project", "list"]];
45373
+ static usage = Command.Usage({
45374
+ description: "List projects visible to the current user."
45375
+ });
45376
+ json = options_exports.Boolean("--json", false);
45377
+ async execute() {
45378
+ const client = new ApiClient();
45379
+ const data = await client.request("/v1/projects");
45380
+ if (this.json) {
45381
+ this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45382
+ `);
45383
+ return 0;
45384
+ }
45385
+ if (data.projects.length === 0) {
45386
+ this.context.stdout.write("no projects\n");
45387
+ return 0;
45388
+ }
45389
+ this.context.stdout.write(
45390
+ `${table(
45391
+ ["name", "id", "created_at"],
45392
+ data.projects.map((p2) => [p2.name, p2.id, p2.created_at])
45393
+ )}
45394
+ `
45395
+ );
45396
+ return 0;
45397
+ }
45398
+ };
45399
+ var ProjectCreateCommand = class extends Command {
45400
+ static paths = [["project", "create"]];
45401
+ static usage = Command.Usage({
45402
+ description: "Create a new project with one or more environments.",
45403
+ examples: [["Two envs", "$0 project create billing --env dev --env prod:production:approval"]]
45404
+ });
45405
+ name = options_exports.String();
45406
+ envs = options_exports.Array("--env", {
45407
+ description: 'Environment spec: name[:tier[:approval]]. Tier \u2208 {production,non-production}; "approval" sets require_approval=true.'
45408
+ });
45409
+ async execute() {
45410
+ const envs = (this.envs ?? ["dev"]).map((spec) => {
45411
+ const [name, tier, approval] = spec.split(":");
45412
+ return {
45413
+ name: name ?? "",
45414
+ tier: tier ?? "non-production",
45415
+ require_approval: approval === "approval"
45416
+ };
45417
+ });
45418
+ const client = new ApiClient();
45419
+ const result = await client.request("/v1/projects", {
45420
+ method: "POST",
45421
+ body: { name: this.name, environments: envs }
45422
+ });
45423
+ this.context.stdout.write(`created project ${result.name} (${result.id})
45424
+ `);
45425
+ for (const e2 of envs) {
45426
+ this.context.stdout.write(
45427
+ ` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
45428
+ `
45429
+ );
45430
+ }
45431
+ return 0;
45432
+ }
45433
+ };
45434
+ var ProjectDescribeCommand = class extends Command {
45435
+ static paths = [["project", "describe"]];
45436
+ static usage = Command.Usage({
45437
+ description: "Show metadata for one project.",
45438
+ examples: [
45439
+ ["By ID", "$0 project describe p_go6rqgwz0wlokdsl55ikn"],
45440
+ ["By name", "$0 project describe myproject"]
45441
+ ]
45442
+ });
45443
+ id = options_exports.String();
45444
+ json = options_exports.Boolean("--json", false);
45445
+ async execute() {
45446
+ const client = new ApiClient();
45447
+ const projectId2 = await resolveProjectId(client, this.id);
45448
+ const data = await client.request(`/v1/projects/${projectId2}`);
45449
+ if (this.json) {
45450
+ this.context.stdout.write(`${JSON.stringify(data, null, 2)}
45451
+ `);
45452
+ return 0;
45453
+ }
45454
+ this.context.stdout.write(`project: ${data.name} (${data.id})
45455
+ `);
45456
+ for (const e2 of data.environments) {
45457
+ this.context.stdout.write(
45458
+ ` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
45459
+ `
45460
+ );
45461
+ }
45462
+ return 0;
45463
+ }
45464
+ };
45465
+ var ProjectDeleteCommand = class extends Command {
45466
+ static paths = [["project", "delete"]];
45467
+ static usage = Command.Usage({ description: "Soft-delete a project." });
45468
+ id = options_exports.String();
45469
+ force = options_exports.Boolean("--force", false);
45470
+ async execute() {
45471
+ if (!this.force) {
45472
+ this.context.stderr.write("keynv: refusing to delete without --force\n");
45473
+ return 2;
45474
+ }
45475
+ const client = new ApiClient();
45476
+ await client.request(`/v1/projects/${this.id}`, { method: "DELETE" });
45477
+ this.context.stdout.write(`deleted project ${this.id}
45478
+ `);
45479
+ return 0;
45480
+ }
45481
+ };
45482
+
45483
+ // src/commands/init.ts
45275
45484
  var InitCommand = class extends Command {
45276
45485
  static paths = [["init"]];
45277
45486
  static usage = Command.Usage({
@@ -45285,6 +45494,11 @@ package.json scripts with \`keynv exec\`.
45285
45494
  Safe to re-run: existing .keynv.env entries are preserved; new
45286
45495
  entries are appended below a marker.
45287
45496
 
45497
+ --dry-run prints the secrets that would be uploaded and the alias
45498
+ mappings that would be written to .keynv.env, then exits without
45499
+ touching any files or making any network calls. Use it to preview
45500
+ what init will do before committing.
45501
+
45288
45502
  Requires an interactive terminal (clack TUI). For scripted
45289
45503
  migration, use the lower-level \`keynv project\` and \`keynv secret\`
45290
45504
  commands directly.
@@ -45292,28 +45506,47 @@ commands directly.
45292
45506
  examples: [
45293
45507
  ["Walk the current project", "$0 init"],
45294
45508
  ["Preview without writing or uploading", "$0 init --dry-run"],
45295
- ["Skip the package.json script-wrapping step", "$0 init --no-scripts"]
45509
+ ["Skip the package.json script-wrapping step", "$0 init --no-scripts"],
45510
+ ["Non-interactive (CI/CD)", "$0 init --env-file .env --project myproject --env dev"]
45296
45511
  ]
45297
45512
  });
45298
45513
  dryRun = options_exports.Boolean("--dry-run", false, {
45299
- description: "Show what would be done without writing files or uploading secrets."
45514
+ description: "Scan .env files and print the secrets that would be uploaded and the alias mappings that would be written \u2014 no files written, no vault calls made."
45300
45515
  });
45301
45516
  noScripts = options_exports.Boolean("--no-scripts", false, {
45302
45517
  description: "Skip the package.json script-wrapping step."
45303
45518
  });
45519
+ envFile = options_exports.String("--env-file", {
45520
+ description: "Path to .env file to migrate (non-interactive mode)."
45521
+ });
45522
+ project = options_exports.String("--project", {
45523
+ description: "Project name or ID (non-interactive mode)."
45524
+ });
45525
+ env = options_exports.String("--env", {
45526
+ description: "Environment name for --env-file (default: dev)."
45527
+ });
45528
+ secret = options_exports.Array("--secret", {
45529
+ description: "KEY=value secret to upload (non-interactive). Can be specified multiple times."
45530
+ });
45304
45531
  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
45532
  const client = new ApiClient();
45312
45533
  await client.ensureHydrated();
45313
45534
  if (!client.isLoggedIn) {
45314
45535
  this.context.stderr.write("keynv: not logged in. Run `keynv login` first.\n");
45315
45536
  return 1;
45316
45537
  }
45538
+ const hasEnvFile = this.envFile != null;
45539
+ const hasSecrets = this.secret != null && this.secret.length > 0;
45540
+ const isNonInteractive = hasEnvFile || hasSecrets;
45541
+ if (isNonInteractive) {
45542
+ return this.runNonInteractive(client);
45543
+ }
45544
+ if (!isInteractive()) {
45545
+ this.context.stderr.write(
45546
+ "keynv init requires an interactive terminal. Use --env-file or --secret for scripted setup.\n"
45547
+ );
45548
+ return 1;
45549
+ }
45317
45550
  try {
45318
45551
  const outcome = await runInitFlow(client, {
45319
45552
  cwd: process.cwd(),
@@ -45329,6 +45562,97 @@ commands directly.
45329
45562
  return 1;
45330
45563
  }
45331
45564
  }
45565
+ async runNonInteractive(client) {
45566
+ const projectName = this.project;
45567
+ if (!projectName) {
45568
+ this.context.stderr.write("keynv: --project is required in non-interactive mode.\n");
45569
+ return 1;
45570
+ }
45571
+ const resolved = await resolveProjectId(client, projectName);
45572
+ if (!resolved) {
45573
+ this.context.stderr.write(`keynv: project not found: ${projectName}
45574
+ `);
45575
+ return 1;
45576
+ }
45577
+ const projectId2 = resolved;
45578
+ const envName = this.env ?? "dev";
45579
+ const secrets = [];
45580
+ if (this.envFile) {
45581
+ try {
45582
+ const content = readFileSync(this.envFile, "utf8");
45583
+ const entries = parseEnvFile(content, this.envFile);
45584
+ for (const e2 of entries) {
45585
+ if (classifyEntry(e2.name, e2.value).verdict !== "skip") {
45586
+ secrets.push({ name: e2.name, value: e2.value });
45587
+ }
45588
+ }
45589
+ } catch (err) {
45590
+ this.context.stderr.write(
45591
+ `keynv: cannot read ${this.envFile}: ${err instanceof Error ? err.message : String(err)}
45592
+ `
45593
+ );
45594
+ return 1;
45595
+ }
45596
+ }
45597
+ if (this.secret) {
45598
+ for (const spec of this.secret) {
45599
+ const eq = spec.indexOf("=");
45600
+ if (eq <= 0) {
45601
+ this.context.stderr.write(`keynv: invalid --secret '${spec}', expected KEY=value
45602
+ `);
45603
+ return 1;
45604
+ }
45605
+ const name = spec.slice(0, eq);
45606
+ const value = spec.slice(eq + 1);
45607
+ secrets.push({ name, value });
45608
+ }
45609
+ }
45610
+ if (secrets.length === 0) {
45611
+ this.context.stdout.write("keynv: nothing to migrate.\n");
45612
+ return 0;
45613
+ }
45614
+ if (this.dryRun) {
45615
+ this.context.stdout.write(
45616
+ `keynv: dry-run \u2014 would upload ${secrets.length} secret(s) to project ${projectName} (${projectId2}) in env ${envName}
45617
+ `
45618
+ );
45619
+ for (const { name } of secrets) {
45620
+ const aliasKey = name.toLowerCase().replace(/_/g, "-");
45621
+ this.context.stdout.write(` ${name}=@${projectName}.${envName}.${aliasKey}
45622
+ `);
45623
+ }
45624
+ return 0;
45625
+ }
45626
+ let uploaded = 0;
45627
+ const failed = [];
45628
+ for (const { name, value } of secrets) {
45629
+ const aliasKey = name.toLowerCase().replace(/_/g, "-");
45630
+ const alias2 = buildAlias({ project: projectName, environment: envName, key: aliasKey });
45631
+ if (!alias2) {
45632
+ failed.push(`${name} (invalid alias key: ${aliasKey})`);
45633
+ continue;
45634
+ }
45635
+ try {
45636
+ await client.request(`/v1/projects/${projectId2}/secrets`, {
45637
+ method: "POST",
45638
+ body: { env: envName, key: aliasKey, value }
45639
+ });
45640
+ uploaded++;
45641
+ } catch (err) {
45642
+ failed.push(`${name}: ${err instanceof Error ? err.message : String(err)}`);
45643
+ }
45644
+ }
45645
+ this.context.stdout.write(
45646
+ `keynv: uploaded ${uploaded}/${secrets.length} secret(s) to ${projectName}.${envName}
45647
+ `
45648
+ );
45649
+ if (failed.length > 0) {
45650
+ for (const f of failed) this.context.stderr.write(` failed: ${f}
45651
+ `);
45652
+ return 1;
45653
+ }
45654
+ return 0;
45655
+ }
45332
45656
  };
45333
45657
 
45334
45658
  // src/commands/login.ts
@@ -45574,7 +45898,8 @@ var WhoamiCommand = class extends Command {
45574
45898
  }
45575
45899
  this.context.stdout.write(`user: ${data.email} (${data.id})
45576
45900
  `);
45577
- this.context.stdout.write(`org: ${data.org_id}
45901
+ const orgDisplay = data.org_name ? `${data.org_name} (${data.org_id})` : data.org_id;
45902
+ this.context.stdout.write(`org: ${orgDisplay}
45578
45903
  `);
45579
45904
  this.context.stdout.write(`role: ${data.org_role}
45580
45905
  `);
@@ -45583,7 +45908,8 @@ var WhoamiCommand = class extends Command {
45583
45908
  } else {
45584
45909
  this.context.stdout.write("memberships:\n");
45585
45910
  for (const m of data.memberships) {
45586
- this.context.stdout.write(` ${m.project_id}: ${m.role}
45911
+ const proj = m.project_name ? `${m.project_name} (${m.project_id})` : m.project_id;
45912
+ this.context.stdout.write(` ${proj}: ${m.role}
45587
45913
  `);
45588
45914
  }
45589
45915
  }
@@ -45594,14 +45920,6 @@ var WhoamiCommand = class extends Command {
45594
45920
  // src/commands/member.ts
45595
45921
  init_http();
45596
45922
  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
45923
  var MemberAddCommand = class extends Command {
45606
45924
  static paths = [["member", "add"]];
45607
45925
  static usage = Command.Usage({
@@ -45618,7 +45936,7 @@ var MemberAddCommand = class extends Command {
45618
45936
  return 1;
45619
45937
  }
45620
45938
  const client = new ApiClient();
45621
- const projectId2 = await findProjectIdByName(client, this.project);
45939
+ const projectId2 = await resolveProjectId(client, this.project);
45622
45940
  await client.request(`/v1/projects/${projectId2}/members`, {
45623
45941
  method: "POST",
45624
45942
  body: { email: this.email, role: role2 }
@@ -45635,7 +45953,7 @@ var MemberRemoveCommand = class extends Command {
45635
45953
  email = options_exports.String();
45636
45954
  async execute() {
45637
45955
  const client = new ApiClient();
45638
- const projectId2 = await findProjectIdByName(client, this.project);
45956
+ const projectId2 = await resolveProjectId(client, this.project);
45639
45957
  const members = await client.request(`/v1/projects/${projectId2}/members`);
45640
45958
  const target = members.members.find((m) => m.email === this.email);
45641
45959
  if (!target) {
@@ -45658,7 +45976,7 @@ var MemberListCommand = class extends Command {
45658
45976
  json = options_exports.Boolean("--json", false);
45659
45977
  async execute() {
45660
45978
  const client = new ApiClient();
45661
- const projectId2 = await findProjectIdByName(client, this.project);
45979
+ const projectId2 = await resolveProjectId(client, this.project);
45662
45980
  const data = await client.request(`/v1/projects/${projectId2}/members`);
45663
45981
  if (this.json) {
45664
45982
  this.context.stdout.write(`${JSON.stringify(data, null, 2)}
@@ -45679,114 +45997,6 @@ var MemberListCommand = class extends Command {
45679
45997
  return 0;
45680
45998
  }
45681
45999
  };
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})
45739
- `);
45740
- for (const e2 of envs) {
45741
- this.context.stdout.write(
45742
- ` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
45743
- `
45744
- );
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
- return 0;
45761
- }
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
- }
45789
- };
45790
46000
  var RedactCommand = class extends Command {
45791
46001
  static paths = [["redact"]];
45792
46002
  static usage = Command.Usage({
@@ -45829,11 +46039,9 @@ var RedactStreamCommand = class extends Command {
45829
46039
  static usage = Command.Usage({
45830
46040
  description: "Stream stdin through the line-buffered redactor to stdout.",
45831
46041
  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).
46042
+ Pipe any output through the line-buffered redactor to stdout.
46043
+ Preserves original line structure. Multi-line patterns are NOT applied
46044
+ here (see the streaming-mode limitation in the redactor package).
45837
46045
  `
45838
46046
  });
45839
46047
  async execute() {
@@ -45854,12 +46062,7 @@ init_cancel();
45854
46062
  init_pickProject();
45855
46063
  init_pickSecret();
45856
46064
  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
- }
46065
+ var ALIAS_FORMAT_HINT = "Format: @<project>.<env>.<KEY> (project/env: lowercase kebab-case; key: letters, digits, _ or -)";
45863
46066
  function missingAlias(stderr) {
45864
46067
  stderr.write("keynv: missing <alias> (TTY required for interactive prompt).\n");
45865
46068
  return 1;
@@ -45908,7 +46111,8 @@ var SecretCreateCommand = class extends Command {
45908
46111
  }
45909
46112
  const parsed = parseAlias(alias2);
45910
46113
  if (!parsed) {
45911
- this.context.stderr.write(`keynv: invalid alias '${alias2}'. Expected @project.env.key.
46114
+ this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46115
+ ${ALIAS_FORMAT_HINT}
45912
46116
  `);
45913
46117
  return 1;
45914
46118
  }
@@ -45921,7 +46125,7 @@ var SecretCreateCommand = class extends Command {
45921
46125
  } else if (value === void 0) {
45922
46126
  value = await promptHidden("value: ");
45923
46127
  }
45924
- const projectId2 = await findProjectIdByName2(client, parsed.project);
46128
+ const projectId2 = await resolveProjectId(client, parsed.project);
45925
46129
  await client.request(`/v1/projects/${projectId2}/secrets`, {
45926
46130
  method: "POST",
45927
46131
  body: { env: parsed.environment, key: parsed.key, value }
@@ -45955,10 +46159,11 @@ var SecretGetCommand = class extends Command {
45955
46159
  const parsed = parseAlias(alias2);
45956
46160
  if (!parsed) {
45957
46161
  this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46162
+ ${ALIAS_FORMAT_HINT}
45958
46163
  `);
45959
46164
  return 1;
45960
46165
  }
45961
- const projectId2 = await findProjectIdByName2(client, parsed.project);
46166
+ const projectId2 = await resolveProjectId(client, parsed.project);
45962
46167
  const data = await client.request(
45963
46168
  `/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}`
45964
46169
  );
@@ -45999,7 +46204,7 @@ var SecretListCommand = class extends Command {
45999
46204
  throw err;
46000
46205
  }
46001
46206
  }
46002
- const projectId2 = await findProjectIdByName2(client, projectName);
46207
+ const projectId2 = await resolveProjectId(client, projectName);
46003
46208
  const data = await client.request(`/v1/projects/${projectId2}/secrets`);
46004
46209
  if (this.json) {
46005
46210
  this.context.stdout.write(`${JSON.stringify(data, null, 2)}
@@ -46043,6 +46248,7 @@ var SecretRotateCommand = class extends Command {
46043
46248
  const parsed = parseAlias(alias2);
46044
46249
  if (!parsed) {
46045
46250
  this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46251
+ ${ALIAS_FORMAT_HINT}
46046
46252
  `);
46047
46253
  return 1;
46048
46254
  }
@@ -46058,7 +46264,7 @@ var SecretRotateCommand = class extends Command {
46058
46264
  } else {
46059
46265
  value = await promptHidden("new value: ");
46060
46266
  }
46061
- const projectId2 = await findProjectIdByName2(client, parsed.project);
46267
+ const projectId2 = await resolveProjectId(client, parsed.project);
46062
46268
  const data = await client.request(
46063
46269
  `/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}/rotate`,
46064
46270
  { method: "POST", body: { new_value: value } }
@@ -46089,10 +46295,11 @@ var SecretDeleteCommand = class extends Command {
46089
46295
  const parsed = parseAlias(alias2);
46090
46296
  if (!parsed) {
46091
46297
  this.context.stderr.write(`keynv: invalid alias '${alias2}'.
46298
+ ${ALIAS_FORMAT_HINT}
46092
46299
  `);
46093
46300
  return 1;
46094
46301
  }
46095
- const projectId2 = await findProjectIdByName2(client, parsed.project);
46302
+ const projectId2 = await resolveProjectId(client, parsed.project);
46096
46303
  await client.request(`/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}`, {
46097
46304
  method: "DELETE"
46098
46305
  });
@@ -46101,6 +46308,140 @@ var SecretDeleteCommand = class extends Command {
46101
46308
  return 0;
46102
46309
  }
46103
46310
  };
46311
+ function generateSecret(bytes = 32) {
46312
+ return randomBytes(bytes).toString("base64");
46313
+ }
46314
+ var ServerInitCommand = class extends Command {
46315
+ static paths = [["server", "init"]];
46316
+ static usage = Command.Usage({
46317
+ description: "Generate secrets and deployment files for a new keynv server.",
46318
+ details: `
46319
+ Interactive onboarding wizard that generates the configuration
46320
+ a fresh keynv server needs: JWT signing secret, master encryption
46321
+ key, and deployment templates.
46322
+
46323
+ Outputs:
46324
+ \u2022 A .keynv-server.env file you feed into your deployment
46325
+ (Docker Compose, Coolify, or bare-metal).
46326
+ \u2022 Optionally a docker-compose.yml in the current directory.
46327
+
46328
+ Non-interactive with --yes to skip prompts.
46329
+ `,
46330
+ examples: [
46331
+ ["Interactive wizard", "$0 server init"],
46332
+ ["Non-interactive (CI/scripts)", "$0 server init --yes"],
46333
+ ["Docker Compose output", "$0 server init --compose"],
46334
+ ["Custom output path", "$0 server init --out ./config/env"]
46335
+ ]
46336
+ });
46337
+ yes = options_exports.Boolean("--yes", false, {
46338
+ description: "Skip all prompts (non-interactive)."
46339
+ });
46340
+ compose = options_exports.Boolean("--compose", false, {
46341
+ description: "Also write a docker-compose.yml to the current directory."
46342
+ });
46343
+ out = options_exports.String("--out", {
46344
+ description: "Write server env file to this path (default: .keynv-server.env)."
46345
+ });
46346
+ async execute() {
46347
+ const jwtSecret = generateSecret(48);
46348
+ const masterKey = generateSecret(32);
46349
+ const webSessionSecret = generateSecret(48);
46350
+ const outPath = this.out ?? ".keynv-server.env";
46351
+ const contents = [
46352
+ "# Generated by `keynv server init`.",
46353
+ "# Keep this file secret \u2014 it contains cryptographic key material.",
46354
+ `KEYNV_JWT_SECRET=${jwtSecret}`,
46355
+ `KEYNV_MASTER_KEY=${masterKey}`,
46356
+ "",
46357
+ "# Web dashboard session secret (required for Next.js JWE session tokens).",
46358
+ `KEYNV_WEB_SESSION_SECRET=${webSessionSecret}`,
46359
+ "",
46360
+ "# Server configuration (uncomment and adjust):",
46361
+ "# KEYNV_PORT=8080",
46362
+ "# KEYNV_WEB_URL=https://keynv.example.com",
46363
+ "# KEYNV_PUBLIC_REGISTRATION=false",
46364
+ "# KEYNV_DB_PATH=./keynv.db",
46365
+ "# KEYNV_MASTER_KEY_FILE=./master.key",
46366
+ "# KEYNV_RATE_LIMIT_PER_MINUTE=120",
46367
+ "# KEYNV_ARGON2_MEMORY_KIB=46080",
46368
+ "# KEYNV_ARGON2_TIME_COST=3"
46369
+ ].join("\n");
46370
+ const { writeFileSync: writeFileSync4 } = await import('fs');
46371
+ writeFileSync4(outPath, `${contents}
46372
+ `);
46373
+ this.context.stdout.write(`Wrote ${outPath}
46374
+
46375
+ `);
46376
+ this.context.stdout.write("Generated secrets:\n");
46377
+ this.context.stdout.write(
46378
+ ` KEYNV_JWT_SECRET ${jwtSecret.slice(0, 8)}... (base64, 48 bytes)
46379
+ `
46380
+ );
46381
+ this.context.stdout.write(
46382
+ ` KEYNV_MASTER_KEY ${masterKey.slice(0, 8)}... (base64, 32 bytes)
46383
+ `
46384
+ );
46385
+ this.context.stdout.write(
46386
+ ` KEYNV_WEB_SESSION_SECRET ${webSessionSecret.slice(0, 8)}... (base64, 48 bytes)
46387
+ `
46388
+ );
46389
+ this.context.stdout.write("\nNext steps:\n");
46390
+ this.context.stdout.write(` 1. Feed ${outPath} into your deployment:
46391
+ `);
46392
+ this.context.stdout.write(" docker compose --env-file .keynv-server.env up -d\n");
46393
+ this.context.stdout.write(" or load it into Coolify as environment variables.\n");
46394
+ this.context.stdout.write(" 2. Bootstrap the first owner:\n");
46395
+ this.context.stdout.write(
46396
+ " docker exec -it <server-container> node dist/bootstrap.js \\\n"
46397
+ );
46398
+ this.context.stdout.write(" --owner-email alice@example.com \\\n");
46399
+ this.context.stdout.write(' --owner-password "<a long random password>" \\\n');
46400
+ this.context.stdout.write(' --org-name "Acme Inc"\n');
46401
+ this.context.stdout.write(" 3. Install the CLI and log in:\n");
46402
+ this.context.stdout.write(" npm install -g @keynv/cli\n");
46403
+ this.context.stdout.write(" keynv login --server https://keynv.example.com\n");
46404
+ this.context.stdout.write(" 4. Onboard your first project:\n");
46405
+ this.context.stdout.write(" cd your-project && keynv init\n");
46406
+ if (this.compose) {
46407
+ const composeContent = `# keynv server + Litestream sidecar
46408
+ # Generated by \`keynv server init\`.
46409
+ # Load with: docker compose --env-file .keynv-server.env up -d
46410
+
46411
+ services:
46412
+ server:
46413
+ image: ghcr.io/keynv-labs/keynv-server:latest
46414
+ restart: unless-stopped
46415
+ ports:
46416
+ - '8080:8080'
46417
+ env_file:
46418
+ - .keynv-server.env
46419
+ volumes:
46420
+ - keynv-data:/data
46421
+
46422
+ litestream:
46423
+ image: litestream/litestream:latest
46424
+ restart: unless-stopped
46425
+ volumes:
46426
+ - keynv-data:/data
46427
+ command: replicate
46428
+ environment:
46429
+ LITESTREAM_DB_PATH: /data/keynv.db
46430
+ LITESTREAM_REPLICA_URL: s3://your-bucket/keynv
46431
+ AWS_ACCESS_KEY_ID: \${LITESTREAM_AWS_ACCESS_KEY_ID}
46432
+ AWS_SECRET_ACCESS_KEY: \${LITESTREAM_AWS_SECRET_ACCESS_KEY}
46433
+
46434
+ volumes:
46435
+ keynv-data:
46436
+ `;
46437
+ writeFileSync4("docker-compose.yml", composeContent);
46438
+ this.context.stdout.write(
46439
+ "\nWrote docker-compose.yml (update Litestream S3 config before deploying).\n"
46440
+ );
46441
+ }
46442
+ return 0;
46443
+ }
46444
+ };
46104
46445
 
46105
46446
  // src/commands/test.ts
46106
46447
  init_dist();
@@ -46453,18 +46794,9 @@ var TESTERS = [
46453
46794
  sshTester,
46454
46795
  httpTester
46455
46796
  ];
46456
- function findTester(type) {
46457
- return TESTERS.find((t) => t.type === type) ?? null;
46458
- }
46459
46797
 
46460
46798
  // src/commands/test.ts
46461
46799
  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
46800
  function parseTargets(specs) {
46469
46801
  const out = {};
46470
46802
  for (const spec of specs) {
@@ -46518,12 +46850,17 @@ the error message.
46518
46850
  );
46519
46851
  return 2;
46520
46852
  }
46521
- const tester = findTester(this.as);
46853
+ const tester = TESTERS.find((t) => t.type === this.as);
46522
46854
  if (!tester) {
46523
46855
  this.context.stderr.write(`keynv: unknown tester '${this.as}'
46524
46856
  `);
46525
46857
  return 1;
46526
46858
  }
46859
+ const timeoutMs = this.timeout ? Number.parseInt(this.timeout, 10) * 1e3 : void 0;
46860
+ if (this.timeout && (timeoutMs === void 0 || Number.isNaN(timeoutMs))) {
46861
+ this.context.stderr.write("keynv: invalid --timeout (expected integer seconds)\n");
46862
+ return 2;
46863
+ }
46527
46864
  let target;
46528
46865
  try {
46529
46866
  target = parseTargets(this.targets ?? []);
@@ -46540,7 +46877,7 @@ the error message.
46540
46877
  }
46541
46878
  let value;
46542
46879
  try {
46543
- const projectId2 = await findProjectIdByName3(client, parsedAlias.project);
46880
+ const projectId2 = await resolveProjectId(client, parsedAlias.project);
46544
46881
  const data = await client.request(
46545
46882
  `/v1/projects/${projectId2}/secrets/${parsedAlias.environment}/${parsedAlias.key}`
46546
46883
  );
@@ -46551,11 +46888,6 @@ the error message.
46551
46888
  `);
46552
46889
  return 1;
46553
46890
  }
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
46891
  const result = await runTest({
46560
46892
  tester,
46561
46893
  secret: { alias: parsedAlias.literal, value },
@@ -46597,25 +46929,6 @@ the error message.
46597
46929
  }
46598
46930
  };
46599
46931
 
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
46932
  // src/index.ts
46620
46933
  init_format();
46621
46934
  init_version();
@@ -46649,7 +46962,7 @@ cli.register(InitCommand);
46649
46962
  cli.register(RedactCommand);
46650
46963
  cli.register(RedactStreamCommand);
46651
46964
  cli.register(TestCommand);
46652
- cli.register(UICommand);
46965
+ cli.register(ServerInitCommand);
46653
46966
  var argv = process.argv.slice(2);
46654
46967
  if (argv.length === 0) {
46655
46968
  const { runMenu: runMenu2 } = await Promise.resolve().then(() => (init_menu(), menu_exports));