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

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
@@ -2,7 +2,7 @@
2
2
  import { createRequire } from 'module';
3
3
  import 'crypto';
4
4
  import { readFileSync, existsSync, statSync, writeFileSync, unlinkSync, mkdirSync, rmSync, readdirSync } from 'fs';
5
- import { homedir } from 'os';
5
+ import { hostname, platform, homedir } from 'os';
6
6
  import { isAbsolute, resolve, join, dirname, basename } from 'path';
7
7
  import { Entry } from '@napi-rs/keyring';
8
8
  import { styleText } from 'util';
@@ -579,16 +579,16 @@ var require_lib = __commonJS({
579
579
  }
580
580
  var TypeAssertionError = class extends Error {
581
581
  constructor({ errors } = {}) {
582
- let errorMessage = `Type mismatch`;
582
+ let errorMessage2 = `Type mismatch`;
583
583
  if (errors && errors.length > 0) {
584
- errorMessage += `
584
+ errorMessage2 += `
585
585
  `;
586
586
  for (const error of errors) {
587
- errorMessage += `
587
+ errorMessage2 += `
588
588
  - ${error}`;
589
589
  }
590
590
  }
591
- super(errorMessage);
591
+ super(errorMessage2);
592
592
  }
593
593
  };
594
594
  function assert(val, validator) {
@@ -1056,7 +1056,7 @@ var require_lib = __commonJS({
1056
1056
  var VERSION, AGENT;
1057
1057
  var init_version = __esm({
1058
1058
  "src/version.ts"() {
1059
- VERSION = "0.1.0-rc.10" ;
1059
+ VERSION = "0.1.0-rc.11" ;
1060
1060
  AGENT = `keynv-cli/${VERSION}`;
1061
1061
  }
1062
1062
  });
@@ -1695,15 +1695,15 @@ var init_parseUtil = __esm({
1695
1695
  message: issueData.message
1696
1696
  };
1697
1697
  }
1698
- let errorMessage = "";
1698
+ let errorMessage2 = "";
1699
1699
  const maps = errorMaps.filter((m) => !!m).slice().reverse();
1700
1700
  for (const map of maps) {
1701
- errorMessage = map(fullIssue, { data, defaultError: errorMessage }).message;
1701
+ errorMessage2 = map(fullIssue, { data, defaultError: errorMessage2 }).message;
1702
1702
  }
1703
1703
  return {
1704
1704
  ...issueData,
1705
1705
  path: fullPath,
1706
- message: errorMessage
1706
+ message: errorMessage2
1707
1707
  };
1708
1708
  };
1709
1709
  EMPTY_PATH = [];
@@ -5604,6 +5604,7 @@ function buildUrl(base, path, query) {
5604
5604
  return url.toString();
5605
5605
  }
5606
5606
  async function tryRefresh(creds) {
5607
+ if (creds.auth_kind === "cli_token" || creds.refresh_token.length === 0) return null;
5607
5608
  const res = await fetch(buildUrl(creds.server_url, "/v1/auth/refresh"), {
5608
5609
  method: "POST",
5609
5610
  headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
@@ -5748,7 +5749,7 @@ function parseEnvFile(content, filename) {
5748
5749
  `invalid key '${name}' (must match /^[A-Za-z_][A-Za-z0-9_]*$/)`
5749
5750
  );
5750
5751
  }
5751
- let valueRaw = body.slice(eq + 1);
5752
+ const valueRaw = body.slice(eq + 1);
5752
5753
  let value;
5753
5754
  const valueLeading = valueRaw.replace(/^\s+/, "");
5754
5755
  const firstCh = valueLeading.charAt(0);
@@ -5854,19 +5855,6 @@ var init_envFile = __esm({
5854
5855
  }
5855
5856
  });
5856
5857
 
5857
- // src/ui/helpers/tty.ts
5858
- var tty_exports = {};
5859
- __export(tty_exports, {
5860
- isInteractive: () => isInteractive
5861
- });
5862
- function isInteractive() {
5863
- return process.stdin.isTTY === true && process.stdout.isTTY === true;
5864
- }
5865
- var init_tty = __esm({
5866
- "src/ui/helpers/tty.ts"() {
5867
- }
5868
- });
5869
-
5870
5858
  // ../../node_modules/.pnpm/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/utils.js
5871
5859
  var getCodePointsLength, isFullWidth, isWideNotCJKTNotEmoji;
5872
5860
  var init_utils = __esm({
@@ -7021,6 +7009,95 @@ ${c2}
7021
7009
  } }).prompt();
7022
7010
  }
7023
7011
  });
7012
+ function aiContextBody() {
7013
+ return `## keynv (secrets)
7014
+
7015
+ This project uses [keynv](https://keynv.dev) for secrets. There is a \`.keynv.env\` file at the project root containing **alias references** like \`OPENAI_API_KEY=@projectname.dev.openai-key\` \u2014 these are NOT real values, they are references to secrets stored in the keynv vault.
7016
+
7017
+ ### Mental model
7018
+
7019
+ - **\`.keynv.env\`** \u2014 checked into git, contains aliases. Safe to read, edit, commit.
7020
+ - **Vault** \u2014 holds the real values. The \`keynv\` CLI reads it on demand.
7021
+ - **\`keynv exec -- <command>\`** \u2014 forks the command in a subprocess where the real values are injected into env vars. The parent process (where you, the AI agent, run) NEVER sees the real values.
7022
+ - **Redactor** \u2014 pipes subprocess stdout/stderr through a masker before they reach this terminal. Even if a script accidentally echoes a secret, you see \`<REDACTED:...>\` here.
7023
+
7024
+ ### What you should do
7025
+
7026
+ When the user asks about secrets or env vars, follow this decision tree:
7027
+
7028
+ | User intent | Run / suggest |
7029
+ |---|---|
7030
+ | "Add a new API key" / "set up a secret" | \`keynv secret create\` (interactive prompt picks project + env + key) |
7031
+ | "I have a \`.env\`, migrate it" | \`keynv init\` (you're seeing this file because that already happened) |
7032
+ | "Run the app / dev server / tests" | \`keynv exec -- <their existing command>\` (auto-loads \`.keynv.env\`) |
7033
+ | "Show me the value of X" | \`keynv secret get @alias\` \u2014 but copy to clipboard, do NOT print the value into chat / terminal |
7034
+ | "Rotate this key" | \`keynv secret rotate @alias\` |
7035
+ | "Add a new env var to the project" | Edit \`.keynv.env\` and add \`NAME=@project.env.key\` (after creating the secret with \`keynv secret create\`) |
7036
+ | "Who has access?" | \`keynv member list <project>\` |
7037
+
7038
+ ### Hard rules \u2014 do not violate
7039
+
7040
+ 1. **Never print resolved secret values** to the chat, terminal, log, or any file. The user's editor / scrollback / chat history is an attack surface.
7041
+ 2. **Never write a \`.env\` file** (or any file) containing real secret values. The whole point of keynv is that those don't exist on disk.
7042
+ 3. **Never edit \`.keynv.env\` to substitute resolved values for aliases.** The file should only ever contain \`@project.env.key\` references and plain non-secret literals.
7043
+ 4. **Use \`keynv exec --\` to run commands that need env vars.** Don't try to load \`.keynv.env\` yourself by parsing it manually; the wrapper handles resolution + redaction + subprocess isolation.
7044
+ 5. **If a tool fails with "blocked by keynv-guard" or similar**, do NOT try to work around it. Tell the user \u2014 that's a sign of a misconfiguration, not an obstacle to bypass.
7045
+ 6. **Treat the alias literal (\`@project.env.key\`) as the canonical reference**, never the resolved value. When you write code, configs, or commands that need a secret, write the alias.
7046
+
7047
+ ### Quick reference
7048
+
7049
+ \`\`\`bash
7050
+ keynv # interactive TUI menu (pick projects, secrets, members)
7051
+ keynv exec -- <cmd> # run cmd with .keynv.env loaded
7052
+ keynv secret create # walk through creating a new secret
7053
+ keynv secret list <project> # list aliases (no values)
7054
+ keynv whoami # who am I logged in as
7055
+ keynv --help # full command list
7056
+ \`\`\`
7057
+
7058
+ If \`.keynv.env\` is missing or empty, the user probably hasn't run \`keynv init\` yet \u2014 suggest they do so before they paste a secret into a \`.env\`.`;
7059
+ }
7060
+ function renderKeynvBlock() {
7061
+ return `${KEYNV_BLOCK_START}
7062
+ ${aiContextBody()}
7063
+ ${KEYNV_BLOCK_END}`;
7064
+ }
7065
+ function writeAiContext(rootPath) {
7066
+ const path = join(rootPath, AGENTS_FILE_BASENAME);
7067
+ const block = renderKeynvBlock();
7068
+ if (!existsSync(path)) {
7069
+ const initial = `# Agent guidance for this project
7070
+
7071
+ ${block}
7072
+ `;
7073
+ writeFileSync(path, initial);
7074
+ return "created";
7075
+ }
7076
+ const existing = readFileSync(path, "utf8");
7077
+ const startIdx = existing.indexOf(KEYNV_BLOCK_START);
7078
+ const endIdx = existing.indexOf(KEYNV_BLOCK_END);
7079
+ if (startIdx >= 0 && endIdx > startIdx) {
7080
+ const before = existing.slice(0, startIdx);
7081
+ const after = existing.slice(endIdx + KEYNV_BLOCK_END.length);
7082
+ const next = `${before}${block}${after}`;
7083
+ if (next === existing) return "unchanged";
7084
+ writeFileSync(path, next);
7085
+ return "updated";
7086
+ }
7087
+ const sep = existing.length === 0 ? "" : existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
7088
+ const trailingNewline = existing.endsWith("\n") ? "" : "\n";
7089
+ writeFileSync(path, `${existing}${trailingNewline}${sep}${block}
7090
+ `);
7091
+ return "appended";
7092
+ }
7093
+ var AGENTS_FILE_BASENAME, KEYNV_BLOCK_START, KEYNV_BLOCK_END;
7094
+ var init_aiContext = __esm({
7095
+ "src/init/aiContext.ts"() {
7096
+ AGENTS_FILE_BASENAME = "AGENTS.md";
7097
+ KEYNV_BLOCK_START = "<!-- keynv:start -- generated by `keynv init`; safe to leave intact, will be refreshed on re-run -->";
7098
+ KEYNV_BLOCK_END = "<!-- keynv:end -->";
7099
+ }
7100
+ });
7024
7101
  function findProjectRoot(startDir) {
7025
7102
  let dir = resolve(startDir);
7026
7103
  for (let i = 0; i < 64; i++) {
@@ -7051,16 +7128,20 @@ function buildRoot(dir, marker) {
7051
7128
  }
7052
7129
  if (pkg.scripts && typeof pkg.scripts === "object") {
7053
7130
  scripts = Object.fromEntries(
7054
- Object.entries(pkg.scripts).filter(
7055
- ([, v2]) => typeof v2 === "string"
7056
- )
7131
+ Object.entries(pkg.scripts).filter(([, v2]) => typeof v2 === "string")
7057
7132
  );
7058
7133
  }
7059
7134
  } catch {
7060
7135
  invalid = true;
7061
7136
  }
7062
7137
  }
7063
- return { path: dir, suggestedName, marker, packageJsonScripts: scripts, packageJsonInvalid: invalid };
7138
+ return {
7139
+ path: dir,
7140
+ suggestedName,
7141
+ marker,
7142
+ packageJsonScripts: scripts,
7143
+ packageJsonInvalid: invalid
7144
+ };
7064
7145
  }
7065
7146
  function findEnvFiles(rootDir) {
7066
7147
  let entries;
@@ -7134,95 +7215,6 @@ var init_detect = __esm({
7134
7215
  KEYNV_ENV_BASENAME = ".keynv.env";
7135
7216
  }
7136
7217
  });
7137
- function aiContextBody() {
7138
- return `## keynv (secrets)
7139
-
7140
- This project uses [keynv](https://keynv.dev) for secrets. There is a \`.keynv.env\` file at the project root containing **alias references** like \`OPENAI_API_KEY=@projectname.dev.openai-key\` \u2014 these are NOT real values, they are references to secrets stored in the keynv vault.
7141
-
7142
- ### Mental model
7143
-
7144
- - **\`.keynv.env\`** \u2014 checked into git, contains aliases. Safe to read, edit, commit.
7145
- - **Vault** \u2014 holds the real values. The \`keynv\` CLI reads it on demand.
7146
- - **\`keynv exec -- <command>\`** \u2014 forks the command in a subprocess where the real values are injected into env vars. The parent process (where you, the AI agent, run) NEVER sees the real values.
7147
- - **Redactor** \u2014 pipes subprocess stdout/stderr through a masker before they reach this terminal. Even if a script accidentally echoes a secret, you see \`<REDACTED:...>\` here.
7148
-
7149
- ### What you should do
7150
-
7151
- When the user asks about secrets or env vars, follow this decision tree:
7152
-
7153
- | User intent | Run / suggest |
7154
- |---|---|
7155
- | "Add a new API key" / "set up a secret" | \`keynv secret create\` (interactive prompt picks project + env + key) |
7156
- | "I have a \`.env\`, migrate it" | \`keynv init\` (you're seeing this file because that already happened) |
7157
- | "Run the app / dev server / tests" | \`keynv exec -- <their existing command>\` (auto-loads \`.keynv.env\`) |
7158
- | "Show me the value of X" | \`keynv secret get @alias\` \u2014 but copy to clipboard, do NOT print the value into chat / terminal |
7159
- | "Rotate this key" | \`keynv secret rotate @alias\` |
7160
- | "Add a new env var to the project" | Edit \`.keynv.env\` and add \`NAME=@project.env.key\` (after creating the secret with \`keynv secret create\`) |
7161
- | "Who has access?" | \`keynv member list <project>\` |
7162
-
7163
- ### Hard rules \u2014 do not violate
7164
-
7165
- 1. **Never print resolved secret values** to the chat, terminal, log, or any file. The user's editor / scrollback / chat history is an attack surface.
7166
- 2. **Never write a \`.env\` file** (or any file) containing real secret values. The whole point of keynv is that those don't exist on disk.
7167
- 3. **Never edit \`.keynv.env\` to substitute resolved values for aliases.** The file should only ever contain \`@project.env.key\` references and plain non-secret literals.
7168
- 4. **Use \`keynv exec --\` to run commands that need env vars.** Don't try to load \`.keynv.env\` yourself by parsing it manually; the wrapper handles resolution + redaction + subprocess isolation.
7169
- 5. **If a tool fails with "blocked by keynv-guard" or similar**, do NOT try to work around it. Tell the user \u2014 that's a sign of a misconfiguration, not an obstacle to bypass.
7170
- 6. **Treat the alias literal (\`@project.env.key\`) as the canonical reference**, never the resolved value. When you write code, configs, or commands that need a secret, write the alias.
7171
-
7172
- ### Quick reference
7173
-
7174
- \`\`\`bash
7175
- keynv # interactive TUI menu (pick projects, secrets, members)
7176
- keynv exec -- <cmd> # run cmd with .keynv.env loaded
7177
- keynv secret create # walk through creating a new secret
7178
- keynv secret list <project> # list aliases (no values)
7179
- keynv whoami # who am I logged in as
7180
- keynv --help # full command list
7181
- \`\`\`
7182
-
7183
- If \`.keynv.env\` is missing or empty, the user probably hasn't run \`keynv init\` yet \u2014 suggest they do so before they paste a secret into a \`.env\`.`;
7184
- }
7185
- function renderKeynvBlock() {
7186
- return `${KEYNV_BLOCK_START}
7187
- ${aiContextBody()}
7188
- ${KEYNV_BLOCK_END}`;
7189
- }
7190
- function writeAiContext(rootPath) {
7191
- const path = join(rootPath, AGENTS_FILE_BASENAME);
7192
- const block = renderKeynvBlock();
7193
- if (!existsSync(path)) {
7194
- const initial = `# Agent guidance for this project
7195
-
7196
- ${block}
7197
- `;
7198
- writeFileSync(path, initial);
7199
- return "created";
7200
- }
7201
- const existing = readFileSync(path, "utf8");
7202
- const startIdx = existing.indexOf(KEYNV_BLOCK_START);
7203
- const endIdx = existing.indexOf(KEYNV_BLOCK_END);
7204
- if (startIdx >= 0 && endIdx > startIdx) {
7205
- const before = existing.slice(0, startIdx);
7206
- const after = existing.slice(endIdx + KEYNV_BLOCK_END.length);
7207
- const next = `${before}${block}${after}`;
7208
- if (next === existing) return "unchanged";
7209
- writeFileSync(path, next);
7210
- return "updated";
7211
- }
7212
- const sep = existing.length === 0 ? "" : existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
7213
- const trailingNewline = existing.endsWith("\n") ? "" : "\n";
7214
- writeFileSync(path, `${existing}${trailingNewline}${sep}${block}
7215
- `);
7216
- return "appended";
7217
- }
7218
- var AGENTS_FILE_BASENAME, KEYNV_BLOCK_START, KEYNV_BLOCK_END;
7219
- var init_aiContext = __esm({
7220
- "src/init/aiContext.ts"() {
7221
- AGENTS_FILE_BASENAME = "AGENTS.md";
7222
- KEYNV_BLOCK_START = "<!-- keynv:start -- generated by `keynv init`; safe to leave intact, will be refreshed on re-run -->";
7223
- KEYNV_BLOCK_END = "<!-- keynv:end -->";
7224
- }
7225
- });
7226
7218
 
7227
7219
  // src/init/heuristics.ts
7228
7220
  function shannonEntropyBits(s) {
@@ -7641,7 +7633,9 @@ ${detail}`
7641
7633
  }
7642
7634
  const totalEntries = [...perEnv.values()].reduce((n, arr) => n + arr.length, 0);
7643
7635
  if (totalEntries === 0) {
7644
- R2.info("All env files were empty or only contained framework-managed vars. Nothing to upload.");
7636
+ R2.info(
7637
+ "All env files were empty or only contained framework-managed vars. Nothing to upload."
7638
+ );
7645
7639
  ye("Done.");
7646
7640
  return { exitCode: 0 };
7647
7641
  }
@@ -7693,10 +7687,10 @@ ${detail}`
7693
7687
  const planSummary = [
7694
7688
  `Project: ${projectChoice.name}${projectChoice.created ? " (will be created)" : ""}`,
7695
7689
  `Environments: ${distinctEnvs.join(", ")}`,
7696
- `Per-env breakdown:`,
7690
+ "Per-env breakdown:",
7697
7691
  perEnvCounts,
7698
7692
  `Script wraps: ${scriptWrapSelection.length}`,
7699
- `Original .env: delete after upload`,
7693
+ "Original .env: delete after upload",
7700
7694
  opts.dryRun ? "Dry-run: no changes will be made." : ""
7701
7695
  ].filter(Boolean).join("\n");
7702
7696
  Se(planSummary, "About to apply");
@@ -7731,7 +7725,11 @@ ${detail}`
7731
7725
  method: "POST",
7732
7726
  body: { env: env2, key: aliasKey, value: e2.value }
7733
7727
  });
7734
- const alias2 = buildAlias({ project: projectChoice.name, environment: env2, key: aliasKey });
7728
+ const alias2 = buildAlias({
7729
+ project: projectChoice.name,
7730
+ environment: env2,
7731
+ key: aliasKey
7732
+ });
7735
7733
  if (alias2 === null) {
7736
7734
  failed.push({
7737
7735
  env: env2,
@@ -7753,7 +7751,9 @@ ${detail}`
7753
7751
  if (failed.length === 0) {
7754
7752
  s.stop(`Uploaded ${totalToUpload} secret${totalToUpload === 1 ? "" : "s"}`);
7755
7753
  } else {
7756
- s.error(`${totalToUpload - failed.length}/${totalToUpload} uploaded; ${failed.length} failed`);
7754
+ s.error(
7755
+ `${totalToUpload - failed.length}/${totalToUpload} uploaded; ${failed.length} failed`
7756
+ );
7757
7757
  for (const f of failed) R2.warn(` [${f.env}] ${f.name}: ${f.reason}`);
7758
7758
  }
7759
7759
  }
@@ -7968,7 +7968,9 @@ async function ensureProjectAndEnvs(client, projectChoice, distinctEnvs) {
7968
7968
  body: envBodyFor(envName)
7969
7969
  });
7970
7970
  } catch (err) {
7971
- s.error(`Failed to add env "${envName}": ${err instanceof Error ? err.message : String(err)}`);
7971
+ s.error(
7972
+ `Failed to add env "${envName}": ${err instanceof Error ? err.message : String(err)}`
7973
+ );
7972
7974
  return null;
7973
7975
  }
7974
7976
  }
@@ -8014,7 +8016,7 @@ function composeKeynvEnv(opts) {
8014
8016
  }
8015
8017
  }
8016
8018
  if (mergeWithExisting !== null) {
8017
- lines.push(`# <<< keynv init <<<`);
8019
+ lines.push("# <<< keynv init <<<");
8018
8020
  }
8019
8021
  return lines;
8020
8022
  }
@@ -8035,11 +8037,11 @@ function updatePackageJsonScripts(path, originalScripts, selectedNames) {
8035
8037
  }
8036
8038
  var init_init = __esm({
8037
8039
  "src/ui/flows/init.ts"() {
8038
- init_dist();
8039
8040
  init_dist5();
8041
+ init_dist();
8040
8042
  init_envFile();
8041
- init_detect();
8042
8043
  init_aiContext();
8044
+ init_detect();
8043
8045
  init_heuristics();
8044
8046
  init_scriptWrap();
8045
8047
  init_cancel();
@@ -8047,30 +8049,103 @@ var init_init = __esm({
8047
8049
  }
8048
8050
  });
8049
8051
 
8050
- // src/ui/helpers/pickSecret.ts
8051
- async function listSecrets(client, projectId2) {
8052
- const data = await client.request(
8053
- `/v1/projects/${projectId2}/secrets`
8054
- );
8055
- return data.secrets;
8052
+ // src/ui/helpers/tty.ts
8053
+ var tty_exports = {};
8054
+ __export(tty_exports, {
8055
+ isInteractive: () => isInteractive
8056
+ });
8057
+ function isInteractive() {
8058
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
8056
8059
  }
8057
- async function pickSecret(client, projectId2, message = "Secret:") {
8058
- const secrets = await listSecrets(client, projectId2);
8059
- if (secrets.length === 0) return null;
8060
- const value = await Ee({
8061
- message,
8062
- options: secrets.map((s) => ({
8063
- value: s.alias,
8064
- label: s.alias,
8065
- hint: `v${s.version}`
8066
- }))
8060
+ var init_tty = __esm({
8061
+ "src/ui/helpers/tty.ts"() {
8062
+ }
8063
+ });
8064
+ function sleep(ms) {
8065
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
8066
+ }
8067
+ async function errorMessage(res, fallback) {
8068
+ const text = await res.text();
8069
+ if (!text) return fallback;
8070
+ try {
8071
+ const parsed = JSON.parse(text);
8072
+ return parsed.error?.message ?? fallback;
8073
+ } catch {
8074
+ return fallback;
8075
+ }
8076
+ }
8077
+ function openBrowser(url) {
8078
+ try {
8079
+ const os = platform();
8080
+ const command = os === "win32" ? "cmd" : os === "darwin" ? "open" : "xdg-open";
8081
+ const args = os === "win32" ? ["/c", "start", "", url] : [url];
8082
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
8083
+ child.unref();
8084
+ return true;
8085
+ } catch {
8086
+ return false;
8087
+ }
8088
+ }
8089
+ async function runBrowserAuth(serverUrl) {
8090
+ const startRes = await fetch(new URL("/v1/auth/cli/browser/start", serverUrl).toString(), {
8091
+ method: "POST",
8092
+ headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
8093
+ body: JSON.stringify({ device_name: hostname() })
8067
8094
  });
8068
- if (q(value)) return null;
8069
- return secrets.find((s) => s.alias === value) ?? null;
8095
+ if (!startRes.ok) {
8096
+ throw new BrowserAuthError(
8097
+ await errorMessage(startRes, `Browser auth failed to start (${startRes.status}).`)
8098
+ );
8099
+ }
8100
+ const start = await startRes.json();
8101
+ const opened = openBrowser(start.verification_uri_complete);
8102
+ if (!opened) {
8103
+ throw new BrowserAuthError(`Open this URL in your browser: ${start.verification_uri_complete}`);
8104
+ }
8105
+ const deadline = Date.now() + start.expires_in * 1e3;
8106
+ const intervalMs = Math.max(1, start.interval) * 1e3;
8107
+ while (Date.now() < deadline) {
8108
+ await sleep(intervalMs);
8109
+ const pollRes = await fetch(new URL("/v1/auth/cli/browser/poll", serverUrl).toString(), {
8110
+ method: "POST",
8111
+ headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
8112
+ body: JSON.stringify({ device_code: start.device_code })
8113
+ });
8114
+ if (pollRes.status === 202) continue;
8115
+ if (!pollRes.ok) {
8116
+ throw new BrowserAuthError(
8117
+ await errorMessage(pollRes, `Browser auth failed (${pollRes.status}).`)
8118
+ );
8119
+ }
8120
+ const data = await pollRes.json();
8121
+ return {
8122
+ auth_kind: "session",
8123
+ server_url: serverUrl,
8124
+ user_id: data.user.id,
8125
+ email: data.user.email,
8126
+ org_id: data.user.org_id,
8127
+ org_role: data.user.org_role,
8128
+ access_token: data.access_token,
8129
+ refresh_token: data.refresh_token,
8130
+ access_expires_at: new Date(Date.now() + data.expires_in * 1e3).toISOString()
8131
+ };
8132
+ }
8133
+ throw new BrowserAuthError("Browser auth timed out. Run `keynv login` to try again.");
8070
8134
  }
8071
- var init_pickSecret = __esm({
8072
- "src/ui/helpers/pickSecret.ts"() {
8073
- init_dist5();
8135
+ var BrowserAuthError;
8136
+ var init_browser_auth = __esm({
8137
+ "src/client/browser-auth.ts"() {
8138
+ init_version();
8139
+ BrowserAuthError = class extends Error {
8140
+ };
8141
+ }
8142
+ });
8143
+
8144
+ // src/client/defaults.ts
8145
+ var DEFAULT_SERVER_URL;
8146
+ var init_defaults = __esm({
8147
+ "src/client/defaults.ts"() {
8148
+ DEFAULT_SERVER_URL = "https://api.keynv.dev";
8074
8149
  }
8075
8150
  });
8076
8151
 
@@ -8098,6 +8173,33 @@ var init_pickEnv = __esm({
8098
8173
  init_dist5();
8099
8174
  }
8100
8175
  });
8176
+
8177
+ // src/ui/helpers/pickSecret.ts
8178
+ async function listSecrets(client, projectId2) {
8179
+ const data = await client.request(
8180
+ `/v1/projects/${projectId2}/secrets`
8181
+ );
8182
+ return data.secrets;
8183
+ }
8184
+ async function pickSecret(client, projectId2, message = "Secret:") {
8185
+ const secrets = await listSecrets(client, projectId2);
8186
+ if (secrets.length === 0) return null;
8187
+ const value = await Ee({
8188
+ message,
8189
+ options: secrets.map((s) => ({
8190
+ value: s.alias,
8191
+ label: s.alias,
8192
+ hint: `v${s.version}`
8193
+ }))
8194
+ });
8195
+ if (q(value)) return null;
8196
+ return secrets.find((s) => s.alias === value) ?? null;
8197
+ }
8198
+ var init_pickSecret = __esm({
8199
+ "src/ui/helpers/pickSecret.ts"() {
8200
+ init_dist5();
8201
+ }
8202
+ });
8101
8203
  async function runSecretsFlow(client, project) {
8102
8204
  let target = project;
8103
8205
  if (!target) {
@@ -8161,7 +8263,10 @@ async function runSecretMenu(client, project, alias2) {
8161
8263
  }
8162
8264
  } else if (choice === "rotate") {
8163
8265
  const newValue = unwrap(
8164
- await Ce({ message: "New value", validate: (v2) => v2 && v2.length ? void 0 : "required" })
8266
+ await Ce({
8267
+ message: "New value",
8268
+ validate: (v2) => v2?.length ? void 0 : "required"
8269
+ })
8165
8270
  );
8166
8271
  const data = await client.request(`${path}/rotate`, {
8167
8272
  method: "POST",
@@ -8169,9 +8274,7 @@ async function runSecretMenu(client, project, alias2) {
8169
8274
  });
8170
8275
  R2.success(`Rotated ${data.alias} \u2192 v${data.version}`);
8171
8276
  } else if (choice === "delete") {
8172
- const confirmed = unwrap(
8173
- await ue({ message: `Delete ${alias2}?`, initialValue: false })
8174
- );
8277
+ const confirmed = unwrap(await ue({ message: `Delete ${alias2}?`, initialValue: false }));
8175
8278
  if (!confirmed) continue;
8176
8279
  await client.request(path, { method: "DELETE" });
8177
8280
  R2.success(`Deleted ${alias2}`);
@@ -8180,11 +8283,13 @@ async function runSecretMenu(client, project, alias2) {
8180
8283
  }
8181
8284
  }
8182
8285
  async function copyToClipboard(value) {
8183
- const platform = process.platform;
8184
- const cmd = platform === "darwin" ? ["pbcopy", []] : platform === "win32" ? ["clip", []] : ["xclip", ["-selection", "clipboard"]];
8286
+ const platform2 = process.platform;
8287
+ const cmd = platform2 === "darwin" ? ["pbcopy", []] : platform2 === "win32" ? ["clip", []] : ["xclip", ["-selection", "clipboard"]];
8185
8288
  return new Promise((resolve3) => {
8186
8289
  try {
8187
- const child = spawn(cmd[0], cmd[1], { stdio: ["pipe", "ignore", "ignore"] });
8290
+ const child = spawn(cmd[0], cmd[1], {
8291
+ stdio: ["pipe", "ignore", "ignore"]
8292
+ });
8188
8293
  child.on("error", () => resolve3(false));
8189
8294
  child.on("exit", (code) => resolve3(code === 0));
8190
8295
  child.stdin.end(value);
@@ -8211,7 +8316,7 @@ async function promptNewSecret(client, project) {
8211
8316
  }),
8212
8317
  value: () => Ce({
8213
8318
  message: "Value (hidden)",
8214
- validate: (v2) => v2 && v2.length ? void 0 : "required"
8319
+ validate: (v2) => v2?.length ? void 0 : "required"
8215
8320
  })
8216
8321
  },
8217
8322
  {
@@ -8240,8 +8345,8 @@ async function createSecretInteractive(client, project) {
8240
8345
  }
8241
8346
  var init_secret = __esm({
8242
8347
  "src/ui/flows/secret.ts"() {
8243
- init_dist();
8244
8348
  init_dist5();
8349
+ init_dist();
8245
8350
  init_cancel();
8246
8351
  init_pickEnv();
8247
8352
  init_pickProject();
@@ -28980,11 +29085,11 @@ var require_pg_connection_string = __commonJS({
28980
29085
  config.client_encoding = result.searchParams.get("encoding");
28981
29086
  return config;
28982
29087
  }
28983
- const hostname = dummyHost ? "" : result.hostname;
29088
+ const hostname2 = dummyHost ? "" : result.hostname;
28984
29089
  if (!config.host) {
28985
- config.host = decodeURIComponent(hostname);
28986
- } else if (hostname && /^%2f/i.test(hostname)) {
28987
- result.pathname = hostname + result.pathname;
29090
+ config.host = decodeURIComponent(hostname2);
29091
+ } else if (hostname2 && /^%2f/i.test(hostname2)) {
29092
+ result.pathname = hostname2 + result.pathname;
28988
29093
  }
28989
29094
  if (!config.port) {
28990
29095
  config.port = result.port;
@@ -40090,9 +40195,9 @@ var require_cluster = __commonJS({
40090
40195
  }
40091
40196
  });
40092
40197
  }
40093
- resolveSrv(hostname) {
40198
+ resolveSrv(hostname2) {
40094
40199
  return new Promise((resolve3, reject) => {
40095
- this.options.resolveSrv(hostname, (err, records) => {
40200
+ this.options.resolveSrv(hostname2, (err, records) => {
40096
40201
  if (err) {
40097
40202
  return reject(err);
40098
40203
  }
@@ -40114,14 +40219,14 @@ var require_cluster = __commonJS({
40114
40219
  });
40115
40220
  });
40116
40221
  }
40117
- dnsLookup(hostname) {
40222
+ dnsLookup(hostname2) {
40118
40223
  return new Promise((resolve3, reject) => {
40119
- this.options.dnsLookup(hostname, (err, address) => {
40224
+ this.options.dnsLookup(hostname2, (err, address) => {
40120
40225
  if (err) {
40121
- debug2("failed to resolve hostname %s to IP: %s", hostname, err.message);
40226
+ debug2("failed to resolve hostname %s to IP: %s", hostname2, err.message);
40122
40227
  reject(err);
40123
40228
  } else {
40124
- debug2("resolved hostname %s to IP %s", hostname, address);
40229
+ debug2("resolved hostname %s to IP %s", hostname2, address);
40125
40230
  resolve3(address);
40126
40231
  }
40127
40232
  });
@@ -42446,85 +42551,27 @@ var init_audit2 = __esm({
42446
42551
  });
42447
42552
 
42448
42553
  // src/ui/flows/login.ts
42449
- async function runLoginFlow(client) {
42450
- const answers = await he(
42451
- {
42452
- server: () => Re({
42453
- message: "Server URL",
42454
- placeholder: "http://localhost:8080",
42455
- defaultValue: "http://localhost:8080"
42456
- }),
42457
- email: () => Re({
42458
- message: "Email",
42459
- validate: (v2) => v2 && v2.includes("@") ? void 0 : "enter an email"
42460
- }),
42461
- password: () => Ce({
42462
- message: "Password",
42463
- validate: (v2) => v2 && v2.length >= 1 ? void 0 : "required"
42464
- })
42465
- },
42466
- {
42467
- onCancel: () => {
42468
- throw new UserCancelled();
42469
- }
42470
- }
42471
- );
42472
- const server = answers.server || "http://localhost:8080";
42554
+ async function runLoginFlow(client, options = {}) {
42555
+ const server = options.server ?? DEFAULT_SERVER_URL;
42473
42556
  const s = ft();
42474
- s.start("Authenticating");
42475
- let res;
42557
+ s.start("Opening browser for keynv login");
42558
+ let creds;
42476
42559
  try {
42477
- res = await fetch(new URL("/v1/auth/login", server).toString(), {
42478
- method: "POST",
42479
- headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
42480
- body: JSON.stringify({ email: answers.email, password: answers.password })
42481
- });
42560
+ creds = await runBrowserAuth(server);
42482
42561
  } catch (err) {
42483
- s.error("Network error");
42484
- me(err instanceof Error ? err.message : "unreachable");
42562
+ s.error("Browser login failed");
42563
+ me(err instanceof Error ? err.message : "Unable to authenticate.");
42485
42564
  return false;
42486
42565
  }
42487
- if (!res.ok) {
42488
- let msg = `login failed (${res.status})`;
42489
- try {
42490
- const parsed = await res.json();
42491
- msg = parsed.error?.message ?? msg;
42492
- } catch {
42493
- }
42494
- s.error(msg);
42495
- return false;
42496
- }
42497
- const data = await res.json();
42498
- await saveCredentials({
42499
- server_url: server,
42500
- user_id: data.user.id,
42501
- email: data.user.email,
42502
- org_id: data.user.org_id,
42503
- org_role: data.user.org_role,
42504
- access_token: data.access_token,
42505
- refresh_token: data.refresh_token,
42506
- access_expires_at: new Date(Date.now() + data.expires_in * 1e3).toISOString()
42507
- });
42508
- await client.setCredentials({
42509
- server_url: server,
42510
- user_id: data.user.id,
42511
- email: data.user.email,
42512
- org_id: data.user.org_id,
42513
- org_role: data.user.org_role,
42514
- access_token: data.access_token,
42515
- refresh_token: data.refresh_token,
42516
- access_expires_at: new Date(Date.now() + data.expires_in * 1e3).toISOString()
42517
- });
42518
- s.stop(`Logged in as ${data.user.email}`);
42566
+ await client.setCredentials(creds);
42567
+ s.stop(`Logged in as ${creds.email}`);
42519
42568
  return true;
42520
42569
  }
42521
42570
  var init_login = __esm({
42522
42571
  "src/ui/flows/login.ts"() {
42523
42572
  init_dist5();
42524
- init_http();
42525
- init_store();
42526
- init_version();
42527
- init_cancel();
42573
+ init_browser_auth();
42574
+ init_defaults();
42528
42575
  }
42529
42576
  });
42530
42577
 
@@ -42583,7 +42630,7 @@ async function addMemberInteractive(client, project) {
42583
42630
  {
42584
42631
  email: () => Re({
42585
42632
  message: "Email",
42586
- validate: (v2) => v2 && v2.includes("@") ? void 0 : "enter an email"
42633
+ validate: (v2) => v2?.includes("@") ? void 0 : "enter an email"
42587
42634
  }),
42588
42635
  role: () => Ee({
42589
42636
  message: "Role",
@@ -42682,8 +42729,11 @@ function printDetail(detail) {
42682
42729
  const envLines = detail.environments.map(
42683
42730
  (e2) => ` ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})`
42684
42731
  );
42685
- Se(`${detail.name} (${detail.id})
42686
- ${envLines.join("\n") || " (no environments)"}`, "Project");
42732
+ Se(
42733
+ `${detail.name} (${detail.id})
42734
+ ${envLines.join("\n") || " (no environments)"}`,
42735
+ "Project"
42736
+ );
42687
42737
  }
42688
42738
  async function createProjectInteractive(client) {
42689
42739
  const answers = await he(
@@ -42742,6 +42792,7 @@ async function runMenu() {
42742
42792
  ge(`keynv ${VERSION}`);
42743
42793
  const client = new ApiClient();
42744
42794
  await client.ensureHydrated();
42795
+ let didLogin = false;
42745
42796
  if (!client.isLoggedIn) {
42746
42797
  R2.info("Not logged in.");
42747
42798
  try {
@@ -42750,6 +42801,7 @@ async function runMenu() {
42750
42801
  ye("Login cancelled.");
42751
42802
  return 1;
42752
42803
  }
42804
+ didLogin = true;
42753
42805
  } catch (err) {
42754
42806
  if (err instanceof UserCancelled) {
42755
42807
  me("Login cancelled.");
@@ -42761,6 +42813,16 @@ async function runMenu() {
42761
42813
  const u = client.currentUser;
42762
42814
  if (u) R2.message(`${u.email} (${u.org_role}) @ ${u.server_url}`);
42763
42815
  }
42816
+ 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 });
42824
+ }
42825
+ }
42764
42826
  while (true) {
42765
42827
  let choice;
42766
42828
  try {
@@ -42828,14 +42890,14 @@ var init_menu = __esm({
42828
42890
  init_dist5();
42829
42891
  init_http();
42830
42892
  init_store();
42831
- init_format();
42832
- init_cancel();
42893
+ init_version();
42833
42894
  init_audit2();
42834
42895
  init_login();
42835
42896
  init_member();
42836
42897
  init_project();
42837
42898
  init_secret();
42838
- init_version();
42899
+ init_format();
42900
+ init_cancel();
42839
42901
  }
42840
42902
  });
42841
42903
 
@@ -43516,11 +43578,11 @@ var reducers = {
43516
43578
  return { ...state, options: [{ name: `-c`, value: String(command) }] };
43517
43579
  }
43518
43580
  },
43519
- setError: (state, segment, segmentIndex, errorMessage) => {
43581
+ setError: (state, segment, segmentIndex, errorMessage2) => {
43520
43582
  if (segment === SpecialToken.EndOfInput || segment === SpecialToken.EndOfPartialInput) {
43521
- return { ...state, errorMessage: `${errorMessage}.` };
43583
+ return { ...state, errorMessage: `${errorMessage2}.` };
43522
43584
  } else {
43523
- return { ...state, errorMessage: `${errorMessage} ("${segment}").` };
43585
+ return { ...state, errorMessage: `${errorMessage2} ("${segment}").` };
43524
43586
  }
43525
43587
  },
43526
43588
  setOptionArityError: (state, segment) => {
@@ -45026,10 +45088,7 @@ to load a specific file, or set KEYNV_ENV_FILE in the environment.
45026
45088
  spelled \`--from\` to avoid the collision.)
45027
45089
  `,
45028
45090
  examples: [
45029
- [
45030
- "Auto-load .keynv.env from cwd or parents",
45031
- "$0 exec -- next dev"
45032
- ],
45091
+ ["Auto-load .keynv.env from cwd or parents", "$0 exec -- next dev"],
45033
45092
  [
45034
45093
  "Run mysql with the alias substituted at fork-exec time",
45035
45094
  "$0 exec -- mysql -p@billing.dev.db_pass -h db.example.com"
@@ -45210,9 +45269,9 @@ function signalNumber(sig) {
45210
45269
 
45211
45270
  // src/commands/init.ts
45212
45271
  init_http();
45213
- init_tty();
45214
45272
  init_init();
45215
45273
  init_cancel();
45274
+ init_tty();
45216
45275
  var InitCommand = class extends Command {
45217
45276
  static paths = [["init"]];
45218
45277
  static usage = Command.Usage({
@@ -45273,6 +45332,8 @@ commands directly.
45273
45332
  };
45274
45333
 
45275
45334
  // src/commands/login.ts
45335
+ init_browser_auth();
45336
+ init_defaults();
45276
45337
  init_http();
45277
45338
  init_store();
45278
45339
  init_format();
@@ -45333,18 +45394,40 @@ var LoginCommand = class extends Command {
45333
45394
  static usage = Command.Usage({
45334
45395
  description: "Authenticate against a keynv server.",
45335
45396
  examples: [
45336
- ["Interactive", "$0 login --server http://localhost:8080"],
45397
+ ["Browser login", "$0 login"],
45398
+ ["Self-hosted browser login", "$0 login --server http://localhost:8080"],
45399
+ ["Headless token login", "$0 login --server http://... --token kt_..."],
45337
45400
  ["Non-interactive", "$0 login --server http://... --email a@b.com --password ..."]
45338
45401
  ]
45339
45402
  });
45340
45403
  server = options_exports.String("--server", { description: "Server base URL." });
45404
+ token = options_exports.String("--token", { description: "CLI token for headless auth." });
45341
45405
  email = options_exports.String("--email", { description: "Email address." });
45342
45406
  password = options_exports.String("--password", {
45343
45407
  description: "Password (use stdin to avoid argv leak)."
45344
45408
  });
45345
45409
  async execute() {
45346
- const serverUrl = this.server ?? await promptLine("server URL [http://localhost:8080]: ") ?? "";
45347
- const finalServerUrl = serverUrl.length > 0 ? serverUrl : "http://localhost:8080";
45410
+ const finalServerUrl = this.server ?? DEFAULT_SERVER_URL;
45411
+ if (this.token) {
45412
+ return this.loginWithToken(finalServerUrl, this.token);
45413
+ }
45414
+ if (!this.email && !this.password) {
45415
+ this.context.stdout.write(`Opening browser for ${finalServerUrl} ...
45416
+ `);
45417
+ try {
45418
+ const creds = await runBrowserAuth(finalServerUrl);
45419
+ await saveCredentials(creds);
45420
+ this.context.stdout.write(`logged in as ${creds.email} (${creds.org_role})
45421
+ `);
45422
+ return 0;
45423
+ } catch (err) {
45424
+ this.context.stderr.write(
45425
+ `keynv: ${err instanceof Error ? err.message : "browser login failed"}
45426
+ `
45427
+ );
45428
+ return 1;
45429
+ }
45430
+ }
45348
45431
  const email2 = this.email ?? await promptLine("email: ");
45349
45432
  const password = this.password ?? await promptHidden("password: ");
45350
45433
  const res = await fetch(new URL("/v1/auth/login", finalServerUrl).toString(), {
@@ -45375,6 +45458,7 @@ var LoginCommand = class extends Command {
45375
45458
  const data = await res.json();
45376
45459
  try {
45377
45460
  await saveCredentials({
45461
+ auth_kind: "session",
45378
45462
  server_url: finalServerUrl,
45379
45463
  user_id: data.user.id,
45380
45464
  email: data.user.email,
@@ -45392,6 +45476,54 @@ var LoginCommand = class extends Command {
45392
45476
  return 1;
45393
45477
  }
45394
45478
  this.context.stdout.write(`logged in as ${data.user.email} (${data.user.org_role})
45479
+ `);
45480
+ return 0;
45481
+ }
45482
+ 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
+ });
45486
+ if (!res.ok) {
45487
+ const text = await res.text();
45488
+ let msg = `token login failed (${res.status})`;
45489
+ try {
45490
+ const parsed = JSON.parse(text);
45491
+ msg = parsed.error?.message ?? msg;
45492
+ this.context.stderr.write(
45493
+ `${fmtError({
45494
+ status: res.status,
45495
+ ...parsed.error?.code ? { code: parsed.error.code } : {},
45496
+ message: msg
45497
+ })}
45498
+ `
45499
+ );
45500
+ } catch {
45501
+ this.context.stderr.write(`keynv: ${msg}
45502
+ `);
45503
+ }
45504
+ return 1;
45505
+ }
45506
+ const data = await res.json();
45507
+ try {
45508
+ await saveCredentials({
45509
+ auth_kind: "cli_token",
45510
+ server_url: serverUrl,
45511
+ user_id: data.id,
45512
+ email: data.email,
45513
+ org_id: data.org_id,
45514
+ org_role: data.org_role,
45515
+ access_token: token,
45516
+ refresh_token: "",
45517
+ access_expires_at: "9999-12-31T23:59:59.999Z"
45518
+ });
45519
+ } catch (err) {
45520
+ this.context.stderr.write(
45521
+ `keynv: failed to persist credentials: ${err instanceof Error ? err.message : String(err)}
45522
+ `
45523
+ );
45524
+ return 1;
45525
+ }
45526
+ this.context.stdout.write(`logged in as ${data.email} (${data.org_role})
45395
45527
  `);
45396
45528
  return 0;
45397
45529
  }
@@ -45716,12 +45848,12 @@ streaming-mode limitation in @keynv/redactor).
45716
45848
  // src/commands/secret.ts
45717
45849
  init_dist();
45718
45850
  init_http();
45851
+ init_secret();
45719
45852
  init_format();
45720
- init_tty();
45721
45853
  init_cancel();
45722
45854
  init_pickProject();
45723
45855
  init_pickSecret();
45724
- init_secret();
45856
+ init_tty();
45725
45857
  async function findProjectIdByName2(client, name) {
45726
45858
  const data = await client.request("/v1/projects");
45727
45859
  const match = data.projects.find((p2) => p2.name === name);
@@ -46466,8 +46598,8 @@ the error message.
46466
46598
  };
46467
46599
 
46468
46600
  // src/commands/ui.ts
46469
- init_menu();
46470
46601
  init_tty();
46602
+ init_menu();
46471
46603
  var UICommand = class extends Command {
46472
46604
  static paths = [["ui"]];
46473
46605
  static usage = Command.Usage({