@neon/env 1.4.8 → 1.5.1

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/README.md CHANGED
@@ -202,6 +202,6 @@ Pass `databaseName` / `roleName` to override the auto-pick.
202
202
 
203
203
  ## Resolution
204
204
 
205
- The **CLI** (`neon-env run`) resolves project + branch itself: `--project-id` / `--branch` flag → `NEON_PROJECT_ID` / `NEON_BRANCH` (name) / `NEON_BRANCH_ID` (legacy id) env → `.neon[/project.json]` walked up from the working directory (its `branch` field, name or id; legacy `branchId` still read). The API key resolves via `--api-key` → `NEON_API_KEY` → `~/.config/neonctl/credentials.json`.
205
+ The **CLI** (`neon-env run`) resolves project + branch itself: `--project-id` / `--branch` flag → `NEON_PROJECT_ID` / `NEON_BRANCH` (name) / `NEON_BRANCH_ID` (legacy id) env → `.neon[/project.json]` walked up from the working directory (its `branch` field, name or id; legacy `branchId` still read). The policy is the `neon.ts` in the directory holding that `.neon` (or the working directory without one), and `.env.local` is read from the same directory; `--config <path>` overrides the policy file. The API key resolves via `--api-key` → `NEON_API_KEY` → `~/.config/neonctl/credentials.json`.
206
206
 
207
207
  The **library functions** do none of this — pass `projectId` / `branch` explicitly. This keeps `.neon` parsing in one place (the CLI / neon) and the functions pure.
package/dist/cli.js CHANGED
@@ -44,9 +44,9 @@ import { homedir } from "node:os";
44
44
  * ```
45
45
  */
46
46
  async function fetchEnvReusingSecrets(config, options) {
47
- const { env: source = process.env, keys: requestedKeys, revokeSuperseded = true, ...fetchOptions } = options;
47
+ const { env: source = process.env, keys: requestedKeys, revokeSuperseded = true, resolvedBranch, ...fetchOptions } = options;
48
48
  const api = options.api ?? createApiFromOptions(options);
49
- const { branch, desired } = await resolveBranchPolicy(config, options, api);
49
+ const { branch, desired } = await resolveBranchPolicy(config, options, api, resolvedBranch);
50
50
  const allPolicyKeys = policyEnvKeys(desired);
51
51
  const requested = requestedKeys ? new Set(requestedKeys) : null;
52
52
  const selectedPolicyKeys = requested === null ? allPolicyKeys : [...allPolicyKeys.filter((key) => requested.has(key)), ...[...requested].filter(isFunctionBaseUrlKey).sort()];
@@ -59,12 +59,16 @@ async function fetchEnvReusingSecrets(config, options) {
59
59
  aiGateway: gatewayCredentialSelected
60
60
  }).filter((key) => selected.has(key));
61
61
  if (secretKeys.length === 0) {
62
- const fetched = await fetchEnvKeysState(config, fetchOptions, requested === null ? null : selectedPolicyKeys);
62
+ const fetched = await fetchEnvKeysState(config, fetchOptions, requested === null ? null : selectedPolicyKeys, {
63
+ branch,
64
+ desired
65
+ });
63
66
  return {
64
67
  vars: preferPersisted(toEntries(fetched.env), source),
65
68
  credential: {
66
69
  issued: false,
67
70
  keys: [],
71
+ fresh: [],
68
72
  revoked: [],
69
73
  superseded: []
70
74
  },
@@ -93,10 +97,12 @@ async function fetchEnvReusingSecrets(config, options) {
93
97
  const fetchKeys = requested === null ? null : selectedPolicyKeys.filter((key) => !keptSecretKeys.includes(key));
94
98
  const fetched = await fetchEnvKeysState(config, {
95
99
  ...fetchOptions,
96
- branchId: branch.id,
97
100
  api,
98
101
  ...keptSecretKeys.length > 0 && requested === null ? { omitKeys: keptSecretKeys } : {}
99
- }, fetchKeys);
102
+ }, fetchKeys, {
103
+ branch,
104
+ desired
105
+ });
100
106
  const vars = preferPersisted(toEntries(fetched.env), source);
101
107
  const unavailable = fetched.functionUrlsUnavailable ? { functionUrlsUnavailable: true } : {};
102
108
  for (const key of keptSecretKeys) {
@@ -108,6 +114,7 @@ async function fetchEnvReusingSecrets(config, options) {
108
114
  credential: {
109
115
  issued: false,
110
116
  keys: secretKeys,
117
+ fresh: [],
111
118
  revoked: [],
112
119
  superseded: []
113
120
  },
@@ -121,6 +128,7 @@ async function fetchEnvReusingSecrets(config, options) {
121
128
  credential: {
122
129
  issued: true,
123
130
  keys: secretKeys,
131
+ fresh: secretKeys.filter((key) => !keptSecretKeys.includes(key)),
124
132
  revoked: revokeSuperseded ? [...ours] : [],
125
133
  superseded: revokeSuperseded ? [] : [...ours]
126
134
  },
@@ -1006,7 +1014,8 @@ function readStoredCredential(selection, env) {
1006
1014
  */
1007
1015
  function resolveContext(options) {
1008
1016
  const env = options.env ?? process.env;
1009
- const file = findNeonFile(options.cwd);
1017
+ const found = findNeonFile(options.cwd);
1018
+ const file = found?.file;
1010
1019
  const projectId = nonEmpty(options.projectId) ?? nonEmpty(env.NEON_PROJECT_ID) ?? file?.projectId;
1011
1020
  const branch = nonEmpty(options.branch) ?? nonEmpty(env.NEON_BRANCH) ?? nonEmpty(env.NEON_BRANCH_ID) ?? file?.branch;
1012
1021
  const missing = [];
@@ -1020,28 +1029,31 @@ function resolveContext(options) {
1020
1029
  ok: true,
1021
1030
  context: {
1022
1031
  projectId,
1023
- branch
1032
+ branch,
1033
+ projectDir: found?.dir ?? resolve(options.cwd)
1024
1034
  }
1025
1035
  };
1026
1036
  }
1027
1037
  /**
1028
1038
  * Walk up from `cwd` looking for `.neon/project.json` (preferred) or `.neon` (neonctl
1029
- * convention). Stops at the first `.git` directory or the home directory. Read-only.
1039
+ * convention), with the same boundaries as the `neon` CLI's walk so both resolve the same
1040
+ * project directory: the home directory and the filesystem root are never checked, and
1041
+ * `.git` is not a boundary. Read-only.
1030
1042
  */
1031
1043
  function findNeonFile(cwd) {
1044
+ const home = resolve(homedir());
1032
1045
  let current = resolve(cwd);
1033
- const stop = resolve(homedir());
1034
- let lastSeen = null;
1035
- while (true) {
1036
- const parsed = readNeonFileAt(resolve(current, ".neon", "project.json")) ?? readNeonFileAt(resolve(current, ".neon"));
1037
- if (parsed) return parsed;
1038
- if (current === stop) return null;
1039
- if (existsSync(resolve(current, ".git"))) return null;
1046
+ while (current !== home) {
1040
1047
  const parent = dirname(current);
1041
- if (parent === current || parent === lastSeen) return null;
1042
- lastSeen = current;
1048
+ if (parent === current) return null;
1049
+ const parsed = readNeonFileAt(resolve(current, ".neon", "project.json")) ?? readNeonFileAt(resolve(current, ".neon"));
1050
+ if (parsed) return {
1051
+ file: parsed,
1052
+ dir: current
1053
+ };
1043
1054
  current = parent;
1044
1055
  }
1056
+ return null;
1045
1057
  }
1046
1058
  function readNeonFileAt(path) {
1047
1059
  if (!isFile(path)) return null;
@@ -1081,6 +1093,22 @@ function nonEmpty(value) {
1081
1093
  //#region src/lib/cli/commands.ts
1082
1094
  /** File `env run` reads to layer one-time auth keys. Matches the Vercel/Next.js convention. */
1083
1095
  const DEFAULT_ENV_FILE = ".env.local";
1096
+ const NEON_CONFIG_FILENAMES = [
1097
+ "neon.ts",
1098
+ "neon.mts",
1099
+ "neon.js",
1100
+ "neon.mjs"
1101
+ ];
1102
+ /** The project's `neon.ts` (or other supported extension). Never searches parent directories. */
1103
+ function projectConfigPath(projectDir) {
1104
+ const name = NEON_CONFIG_FILENAMES.find((candidate) => existsSync(join(projectDir, candidate)));
1105
+ if (name === void 0) throw new ConfigLoadError([
1106
+ `Could not find a Neon config file in ${projectDir}.`,
1107
+ `Looked for: ${NEON_CONFIG_FILENAMES.join(", ")} next to the project's .neon (or in the current directory when there is no .neon).`,
1108
+ "Create one with `neon config init`, or pass `--config <path>`."
1109
+ ].join("\n"));
1110
+ return join(projectDir, name);
1111
+ }
1084
1112
  /**
1085
1113
  * Implementation of `neon-env run -- <cmd...>`. Loads `neon.ts`, fetches the env from
1086
1114
  * Neon, then spawns the user-supplied command with the env vars injected on top of the
@@ -1165,11 +1193,11 @@ function formatDotenvLine(key, value) {
1165
1193
  * working credential verifies and keeps it instead of minting another one per invocation.
1166
1194
  */
1167
1195
  async function loadConfigAndFetchEnv(options, ctx, resolved) {
1168
- const { config, resolvedPath } = await loadConfigFromFile({
1169
- ...options.configPath ? { path: options.configPath } : {},
1196
+ const { config } = await loadConfigFromFile({
1197
+ path: options.configPath ?? projectConfigPath(resolved.projectDir),
1170
1198
  cwd: ctx.cwd
1171
1199
  });
1172
- const envFileSource = join(dirname(resolvedPath), DEFAULT_ENV_FILE);
1200
+ const envFileSource = join(resolved.projectDir, DEFAULT_ENV_FILE);
1173
1201
  const fileEnv = existsSync(envFileSource) ? parseEnvFile(readFileSync(envFileSource, "utf-8")) : {};
1174
1202
  const apiKey = resolveApiKey({
1175
1203
  ...options.apiKey ? { apiKey: options.apiKey } : {},
@@ -1309,7 +1337,7 @@ const argv = yargs(hideBin(process.argv)).scriptName("neon-env").usage("$0 <comm
1309
1337
  describe: "Print stack traces and structured error details when something fails"
1310
1338
  }).command("run", "Run a command with Neon env vars (from your neon.ts policy) injected into its environment. Use `--` to separate the command: `neon-env run -- npm run dev`.", (y) => y.option("config", {
1311
1339
  type: "string",
1312
- describe: "Path to neon.ts (defaults to walking up from cwd)"
1340
+ describe: "Path to neon.ts (defaults to the neon.ts next to .neon, or in cwd without one)"
1313
1341
  }).option("project-id", {
1314
1342
  type: "string",
1315
1343
  describe: "Override the .neon/project.json projectId"
@@ -1328,7 +1356,7 @@ const argv = yargs(hideBin(process.argv)).scriptName("neon-env").usage("$0 <comm
1328
1356
  describe: "Output format: dotenv (KEY=value lines) or json"
1329
1357
  }).option("config", {
1330
1358
  type: "string",
1331
- describe: "Path to neon.ts (defaults to walking up from cwd)"
1359
+ describe: "Path to neon.ts (defaults to the neon.ts next to .neon, or in cwd without one)"
1332
1360
  }).option("project-id", {
1333
1361
  type: "string",
1334
1362
  describe: "Override the .neon/project.json projectId"