@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 +1 -1
- package/dist/cli.js +50 -22
- package/dist/cli.js.map +1 -1
- package/dist/env.js +45 -17
- package/dist/env.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/package.json +4 -4
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
|
|
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)
|
|
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
|
-
|
|
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
|
|
1042
|
-
|
|
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
|
|
1169
|
-
|
|
1196
|
+
const { config } = await loadConfigFromFile({
|
|
1197
|
+
path: options.configPath ?? projectConfigPath(resolved.projectDir),
|
|
1170
1198
|
cwd: ctx.cwd
|
|
1171
1199
|
});
|
|
1172
|
-
const envFileSource = join(
|
|
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
|
|
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
|
|
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"
|