@neon/env 0.0.0 → 0.8.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.
@@ -0,0 +1,219 @@
1
+ import { fetchEnv, toEntries } from "../env.js";
2
+ import { resolveContext } from "./resolve-context.js";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { spawn } from "node:child_process";
5
+ import { dirname, join } from "node:path";
6
+ import { ConfigLoadError, ErrorCode, MissingContextError, PlatformError, loadConfigFromFile } from "@neon/config/v1";
7
+ //#region src/lib/cli/commands.ts
8
+ /** File `env run` reads to layer one-time auth keys. Matches the Vercel/Next.js convention. */
9
+ const DEFAULT_ENV_FILE = ".env.local";
10
+ /**
11
+ * Implementation of `neon-env run -- <cmd...>`. Loads `neon.ts`, fetches the env from
12
+ * Neon, then spawns the user-supplied command with the env vars injected on top of the
13
+ * inherited `process.env`. Stdio is inherited so interactive dev servers keep working.
14
+ * The parent process exits with the child's exit code.
15
+ */
16
+ async function runEnvRun(options, ctx) {
17
+ if (options.command.length === 0) return failure([
18
+ "`env run` requires a command to spawn.",
19
+ "Usage: neon-env run -- <command> [args...]",
20
+ "Example: neon-env run -- npm run dev"
21
+ ].join("\n"));
22
+ const resolved = resolveContext({
23
+ cwd: ctx.cwd,
24
+ ...options.projectId ? { projectId: options.projectId } : {},
25
+ ...options.branch ? { branch: options.branch } : {}
26
+ });
27
+ if (!resolved.ok) return failure(["`env run` could not resolve the Neon project and branch:", ...resolved.missing.map((m) => ` - ${m}`)].join("\n"), 3);
28
+ let injected;
29
+ try {
30
+ injected = toEntries(await loadConfigAndFetchEnv(options, ctx, resolved.context));
31
+ } catch (err) {
32
+ return handleError(err);
33
+ }
34
+ const [executable, ...args] = options.command;
35
+ return {
36
+ exitCode: await spawnAndWait(executable, args, {
37
+ cwd: ctx.cwd,
38
+ env: {
39
+ ...process.env,
40
+ ...injected
41
+ }
42
+ }),
43
+ stdout: "",
44
+ stderr: ""
45
+ };
46
+ }
47
+ /**
48
+ * Implementation of `neon-env export`. Resolves the branch's Neon env the same way `run`
49
+ * does (neon.ts policy + linked branch), then writes it to stdout — as dotenv lines or JSON —
50
+ * instead of spawning a process, so other env tools can consume it. For example, varlock can
51
+ * bulk-load it with `@setValuesBulk(exec("neon-env export --format json"), format=json)`.
52
+ */
53
+ async function runEnvExport(options, ctx) {
54
+ const resolved = resolveContext({
55
+ cwd: ctx.cwd,
56
+ ...options.projectId ? { projectId: options.projectId } : {},
57
+ ...options.branch ? { branch: options.branch } : {}
58
+ });
59
+ if (!resolved.ok) return failure(["`env export` could not resolve the Neon project and branch:", ...resolved.missing.map((m) => ` - ${m}`)].join("\n"), 3);
60
+ let entries;
61
+ try {
62
+ entries = toEntries(await loadConfigAndFetchEnv(options, ctx, resolved.context));
63
+ } catch (err) {
64
+ return handleError(err);
65
+ }
66
+ return {
67
+ exitCode: 0,
68
+ stdout: options.format === "json" ? `${JSON.stringify(entries, null, 2)}\n` : toDotenv(entries),
69
+ stderr: ""
70
+ };
71
+ }
72
+ /** Render an env map as dotenv `KEY=value` lines, quoting values that need it. */
73
+ function toDotenv(entries) {
74
+ const lines = Object.entries(entries).map(([key, value]) => formatDotenvLine(key, value));
75
+ return lines.length > 0 ? `${lines.join("\n")}\n` : "";
76
+ }
77
+ /**
78
+ * Render a single `KEY=value` dotenv line, double-quoting (and escaping) values that contain
79
+ * whitespace, `#`, quotes, or `=` so connection strings round-trip through dotenv parsers.
80
+ */
81
+ function formatDotenvLine(key, value) {
82
+ if (!/[\s#"'=]/.test(value)) return `${key}=${value}`;
83
+ return `${key}="${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
84
+ }
85
+ /**
86
+ * Load `neon.ts`, then call `fetchEnv` with the explicitly-resolved project + branch.
87
+ * Layers any one-time Auth keys from `.env.local` (next to the config file) into the env
88
+ * source so re-runs keep round-tripping values the Neon API only returns once at
89
+ * integration-creation time.
90
+ */
91
+ async function loadConfigAndFetchEnv(options, ctx, resolved) {
92
+ const { config, resolvedPath } = await loadConfigFromFile({
93
+ ...options.configPath ? { path: options.configPath } : {},
94
+ cwd: ctx.cwd
95
+ });
96
+ const envFileSource = join(dirname(resolvedPath), DEFAULT_ENV_FILE);
97
+ const fileEnv = existsSync(envFileSource) ? parseEnvFile(readFileSync(envFileSource, "utf-8")) : {};
98
+ return fetchEnv(config, {
99
+ projectId: resolved.projectId,
100
+ branchId: resolved.branchId,
101
+ env: {
102
+ ...process.env,
103
+ ...fileEnv
104
+ },
105
+ ...ctx.api ? { api: ctx.api } : {},
106
+ ...options.apiKey ? { apiKey: options.apiKey } : {}
107
+ });
108
+ }
109
+ /**
110
+ * Spawn a child process with stdio inherited so dev servers stay interactive. Resolves
111
+ * with the child's exit code (treating signal terminations as code 1 so the CLI surfaces
112
+ * a non-zero exit consistently).
113
+ */
114
+ function spawnAndWait(command, args, options) {
115
+ return new Promise((resolve) => {
116
+ const child = spawn(command, args, {
117
+ cwd: options.cwd,
118
+ env: options.env,
119
+ stdio: "inherit"
120
+ });
121
+ child.on("error", (err) => {
122
+ process.stderr.write(`neon-env run: failed to spawn '${command}': ${err.message}\n`);
123
+ resolve(1);
124
+ });
125
+ child.on("exit", (code, signal) => {
126
+ if (typeof code === "number") {
127
+ resolve(code);
128
+ return;
129
+ }
130
+ if (signal) {
131
+ process.stderr.write(`neon-env run: child terminated by signal ${signal}\n`);
132
+ resolve(1);
133
+ return;
134
+ }
135
+ resolve(1);
136
+ });
137
+ });
138
+ }
139
+ function parseEnvFile(body) {
140
+ const out = {};
141
+ for (const line of body.split("\n")) {
142
+ const parsed = parseEnvLine(line);
143
+ if (parsed) out[parsed.key] = parsed.value;
144
+ }
145
+ return out;
146
+ }
147
+ function parseEnvLine(line) {
148
+ const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
149
+ const key = match?.[1];
150
+ const rawValue = match?.[2];
151
+ if (key === void 0 || rawValue === void 0) return null;
152
+ return {
153
+ key,
154
+ value: unescapeEnvValue(rawValue.trim())
155
+ };
156
+ }
157
+ function unescapeEnvValue(value) {
158
+ if (value.length >= 2 && value.startsWith("\"") && value.endsWith("\"")) return value.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
159
+ if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
160
+ return value;
161
+ }
162
+ /**
163
+ * Stable exit code per `PlatformError` code. Mirrors the table in the config package so
164
+ * shell pipelines can branch on the specific failure mode without parsing free text.
165
+ */
166
+ const EXIT_CODE_BY_PLATFORM_ERROR_CODE = {
167
+ [ErrorCode.MissingApiKey]: 1,
168
+ [ErrorCode.Unauthorized]: 6,
169
+ [ErrorCode.Forbidden]: 7,
170
+ [ErrorCode.NotFound]: 8,
171
+ [ErrorCode.RateLimited]: 9,
172
+ [ErrorCode.NetworkError]: 10,
173
+ [ErrorCode.ServerError]: 11,
174
+ [ErrorCode.Locked]: 11,
175
+ [ErrorCode.InternalError]: 99
176
+ };
177
+ function handleError(err) {
178
+ if (err instanceof MissingContextError) return errorResult(err, `Missing context: ${err.message}`, 3);
179
+ if (err instanceof ConfigLoadError) return errorResult(err, `Failed to load config: ${err.message}`, 4);
180
+ if (err instanceof PlatformError) {
181
+ const exitCode = EXIT_CODE_BY_PLATFORM_ERROR_CODE[err.code];
182
+ if (exitCode !== void 0) return errorResult(err, err.message, exitCode);
183
+ return errorResult(err, `[${err.code}] ${err.message}`, 5);
184
+ }
185
+ if (err instanceof Error) return errorResult(err, err.message, 1);
186
+ return failure(String(err), 1);
187
+ }
188
+ function errorResult(err, message, exitCode) {
189
+ const result = {
190
+ exitCode,
191
+ stdout: "",
192
+ stderr: `${message}\n`
193
+ };
194
+ const debug = buildDebugInfo(err);
195
+ if (debug) result.debugInfo = debug;
196
+ return result;
197
+ }
198
+ function buildDebugInfo(err) {
199
+ if (!(err instanceof Error)) return void 0;
200
+ const lines = [];
201
+ if (err instanceof PlatformError) {
202
+ lines.push(`code : ${err.code}`);
203
+ if (Object.keys(err.details).length > 0) lines.push(`details : ${JSON.stringify(err.details, null, 2)}`);
204
+ }
205
+ if (err.cause instanceof Error) lines.push(`cause : ${err.cause.name}: ${err.cause.message}`);
206
+ if (err.stack) lines.push(err.stack);
207
+ return lines.length > 0 ? lines.join("\n") : void 0;
208
+ }
209
+ function failure(message, exitCode = 1) {
210
+ return {
211
+ exitCode,
212
+ stdout: "",
213
+ stderr: `${message}\n`
214
+ };
215
+ }
216
+ //#endregion
217
+ export { runEnvExport, runEnvRun };
218
+
219
+ //# sourceMappingURL=commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.js","names":[],"sources":["../../../src/lib/cli/commands.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport {\n\tConfigLoadError,\n\tErrorCode,\n\tloadConfigFromFile,\n\tMissingContextError,\n\ttype NeonApi,\n\tPlatformError,\n} from \"@neon/config/v1\";\nimport { fetchEnv, toEntries } from \"../env.js\";\nimport { resolveContext } from \"./resolve-context.js\";\n\n/** File `env run` reads to layer one-time auth keys. Matches the Vercel/Next.js convention. */\nconst DEFAULT_ENV_FILE = \".env.local\";\n\n/**\n * Cross-cutting environment a CLI command is allowed to touch. Injected so tests can drive\n * the handler with a custom NeonApi and a controlled `cwd` without spawning child\n * processes.\n */\nexport interface CommandEnv {\n\tcwd: string;\n\t/**\n\t * When set, used directly as the NeonApi. When omitted, the real adapter is built from\n\t * `options.apiKey ?? NEON_API_KEY` inside `fetchEnv`.\n\t */\n\tapi?: NeonApi;\n}\n\nexport interface CommandResult {\n\t/** Process exit code. `0` for success, non-zero for failure. */\n\texitCode: number;\n\t/** Text intended for stdout. */\n\tstdout: string;\n\t/** Text intended for stderr (human-readable status / error messages). */\n\tstderr: string;\n\t/** Optional structured debug payload — printed only when `--debug` is passed. */\n\tdebugInfo?: string;\n}\n\n/**\n * Inputs needed to resolve a branch and fetch its env, shared by `run` and `export`: an\n * optional explicit `neon.ts` path, project/branch overrides, and an API key (otherwise\n * resolved from `.neon` / `NEON_*` env by the CLI and `NEON_API_KEY` by `fetchEnv`).\n */\nexport interface EnvResolveOptions {\n\tconfigPath?: string;\n\tprojectId?: string;\n\tbranch?: string;\n\tapiKey?: string;\n}\n\nexport interface EnvRunCommandOptions extends EnvResolveOptions {\n\t/** The user command to spawn (after `--`). The first element is the executable. */\n\tcommand: string[];\n}\n\n/**\n * Implementation of `neon-env run -- <cmd...>`. Loads `neon.ts`, fetches the env from\n * Neon, then spawns the user-supplied command with the env vars injected on top of the\n * inherited `process.env`. Stdio is inherited so interactive dev servers keep working.\n * The parent process exits with the child's exit code.\n */\nexport async function runEnvRun(\n\toptions: EnvRunCommandOptions,\n\tctx: CommandEnv,\n): Promise<CommandResult> {\n\tif (options.command.length === 0) {\n\t\treturn failure(\n\t\t\t[\n\t\t\t\t\"`env run` requires a command to spawn.\",\n\t\t\t\t\"Usage: neon-env run -- <command> [args...]\",\n\t\t\t\t\"Example: neon-env run -- npm run dev\",\n\t\t\t].join(\"\\n\"),\n\t\t);\n\t}\n\n\t// The CLI owns project/branch resolution (flags → NEON_* env → .neon file) so the\n\t// library functions stay filesystem/env-agnostic.\n\tconst resolved = resolveContext({\n\t\tcwd: ctx.cwd,\n\t\t...(options.projectId ? { projectId: options.projectId } : {}),\n\t\t...(options.branch ? { branch: options.branch } : {}),\n\t});\n\tif (!resolved.ok) {\n\t\treturn failure(\n\t\t\t[\n\t\t\t\t\"`env run` could not resolve the Neon project and branch:\",\n\t\t\t\t...resolved.missing.map((m) => ` - ${m}`),\n\t\t\t].join(\"\\n\"),\n\t\t\t3,\n\t\t);\n\t}\n\n\tlet injected: Record<string, string>;\n\ttry {\n\t\tconst env = await loadConfigAndFetchEnv(options, ctx, resolved.context);\n\t\tinjected = toEntries(env);\n\t} catch (err) {\n\t\treturn handleError(err);\n\t}\n\n\tconst [executable, ...args] = options.command;\n\tconst exitCode = await spawnAndWait(executable, args, {\n\t\tcwd: ctx.cwd,\n\t\tenv: { ...process.env, ...injected },\n\t});\n\treturn { exitCode, stdout: \"\", stderr: \"\" };\n}\n\nexport interface EnvExportCommandOptions extends EnvResolveOptions {\n\t/** Output format. `dotenv` (KEY=value lines) by default; `json` for tooling / bulk loaders. */\n\tformat?: \"dotenv\" | \"json\";\n}\n\n/**\n * Implementation of `neon-env export`. Resolves the branch's Neon env the same way `run`\n * does (neon.ts policy + linked branch), then writes it to stdout — as dotenv lines or JSON —\n * instead of spawning a process, so other env tools can consume it. For example, varlock can\n * bulk-load it with `@setValuesBulk(exec(\"neon-env export --format json\"), format=json)`.\n */\nexport async function runEnvExport(\n\toptions: EnvExportCommandOptions,\n\tctx: CommandEnv,\n): Promise<CommandResult> {\n\tconst resolved = resolveContext({\n\t\tcwd: ctx.cwd,\n\t\t...(options.projectId ? { projectId: options.projectId } : {}),\n\t\t...(options.branch ? { branch: options.branch } : {}),\n\t});\n\tif (!resolved.ok) {\n\t\treturn failure(\n\t\t\t[\n\t\t\t\t\"`env export` could not resolve the Neon project and branch:\",\n\t\t\t\t...resolved.missing.map((m) => ` - ${m}`),\n\t\t\t].join(\"\\n\"),\n\t\t\t3,\n\t\t);\n\t}\n\n\tlet entries: Record<string, string>;\n\ttry {\n\t\tconst env = await loadConfigAndFetchEnv(options, ctx, resolved.context);\n\t\tentries = toEntries(env);\n\t} catch (err) {\n\t\treturn handleError(err);\n\t}\n\n\tconst stdout =\n\t\toptions.format === \"json\"\n\t\t\t? `${JSON.stringify(entries, null, 2)}\\n`\n\t\t\t: toDotenv(entries);\n\treturn { exitCode: 0, stdout, stderr: \"\" };\n}\n\n/** Render an env map as dotenv `KEY=value` lines, quoting values that need it. */\nfunction toDotenv(entries: Record<string, string>): string {\n\tconst lines = Object.entries(entries).map(([key, value]) =>\n\t\tformatDotenvLine(key, value),\n\t);\n\treturn lines.length > 0 ? `${lines.join(\"\\n\")}\\n` : \"\";\n}\n\n/**\n * Render a single `KEY=value` dotenv line, double-quoting (and escaping) values that contain\n * whitespace, `#`, quotes, or `=` so connection strings round-trip through dotenv parsers.\n */\nfunction formatDotenvLine(key: string, value: string): string {\n\tif (!/[\\s#\"'=]/.test(value)) return `${key}=${value}`;\n\tconst escaped = value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n\treturn `${key}=\"${escaped}\"`;\n}\n\n/**\n * Load `neon.ts`, then call `fetchEnv` with the explicitly-resolved project + branch.\n * Layers any one-time Auth keys from `.env.local` (next to the config file) into the env\n * source so re-runs keep round-tripping values the Neon API only returns once at\n * integration-creation time.\n */\nasync function loadConfigAndFetchEnv(\n\toptions: EnvResolveOptions,\n\tctx: CommandEnv,\n\tresolved: { projectId: string; branchId: string },\n): Promise<Awaited<ReturnType<typeof fetchEnv>>> {\n\tconst { config, resolvedPath } = await loadConfigFromFile({\n\t\t...(options.configPath ? { path: options.configPath } : {}),\n\t\tcwd: ctx.cwd,\n\t});\n\tconst envFileSource = join(dirname(resolvedPath), DEFAULT_ENV_FILE);\n\tconst fileEnv = existsSync(envFileSource)\n\t\t? parseEnvFile(readFileSync(envFileSource, \"utf-8\"))\n\t\t: {};\n\treturn fetchEnv(config, {\n\t\tprojectId: resolved.projectId,\n\t\tbranchId: resolved.branchId,\n\t\tenv: { ...process.env, ...fileEnv },\n\t\t...(ctx.api ? { api: ctx.api } : {}),\n\t\t...(options.apiKey ? { apiKey: options.apiKey } : {}),\n\t});\n}\n\n/**\n * Spawn a child process with stdio inherited so dev servers stay interactive. Resolves\n * with the child's exit code (treating signal terminations as code 1 so the CLI surfaces\n * a non-zero exit consistently).\n */\nfunction spawnAndWait(\n\tcommand: string,\n\targs: string[],\n\toptions: { cwd: string; env: Record<string, string | undefined> },\n): Promise<number> {\n\treturn new Promise((resolve) => {\n\t\tconst child = spawn(command, args, {\n\t\t\tcwd: options.cwd,\n\t\t\tenv: options.env,\n\t\t\tstdio: \"inherit\",\n\t\t});\n\t\tchild.on(\"error\", (err) => {\n\t\t\tprocess.stderr.write(\n\t\t\t\t`neon-env run: failed to spawn '${command}': ${err.message}\\n`,\n\t\t\t);\n\t\t\tresolve(1);\n\t\t});\n\t\tchild.on(\"exit\", (code, signal) => {\n\t\t\tif (typeof code === \"number\") {\n\t\t\t\tresolve(code);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (signal) {\n\t\t\t\tprocess.stderr.write(\n\t\t\t\t\t`neon-env run: child terminated by signal ${signal}\\n`,\n\t\t\t\t);\n\t\t\t\tresolve(1);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tresolve(1);\n\t\t});\n\t});\n}\n\nfunction parseEnvFile(body: string): NodeJS.ProcessEnv {\n\tconst out: NodeJS.ProcessEnv = {};\n\tfor (const line of body.split(\"\\n\")) {\n\t\tconst parsed = parseEnvLine(line);\n\t\tif (parsed) out[parsed.key] = parsed.value;\n\t}\n\treturn out;\n}\n\nfunction parseEnvLine(line: string): { key: string; value: string } | null {\n\tconst match = line.match(\n\t\t/^\\s*(?:export\\s+)?([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*(.*)$/,\n\t);\n\tconst key = match?.[1];\n\tconst rawValue = match?.[2];\n\tif (key === undefined || rawValue === undefined) return null;\n\treturn { key, value: unescapeEnvValue(rawValue.trim()) };\n}\n\nfunction unescapeEnvValue(value: string): string {\n\tif (value.length >= 2 && value.startsWith('\"') && value.endsWith('\"')) {\n\t\treturn value.slice(1, -1).replace(/\\\\\"/g, '\"').replace(/\\\\\\\\/g, \"\\\\\");\n\t}\n\tif (value.length >= 2 && value.startsWith(\"'\") && value.endsWith(\"'\")) {\n\t\treturn value.slice(1, -1);\n\t}\n\treturn value;\n}\n\n/**\n * Stable exit code per `PlatformError` code. Mirrors the table in the config package so\n * shell pipelines can branch on the specific failure mode without parsing free text.\n */\nconst EXIT_CODE_BY_PLATFORM_ERROR_CODE: Readonly<Record<string, number>> = {\n\t[ErrorCode.MissingApiKey]: 1,\n\t[ErrorCode.Unauthorized]: 6,\n\t[ErrorCode.Forbidden]: 7,\n\t[ErrorCode.NotFound]: 8,\n\t[ErrorCode.RateLimited]: 9,\n\t[ErrorCode.NetworkError]: 10,\n\t[ErrorCode.ServerError]: 11,\n\t[ErrorCode.Locked]: 11,\n\t[ErrorCode.InternalError]: 99,\n};\n\nfunction handleError(err: unknown): CommandResult {\n\tif (err instanceof MissingContextError)\n\t\treturn errorResult(err, `Missing context: ${err.message}`, 3);\n\tif (err instanceof ConfigLoadError)\n\t\treturn errorResult(err, `Failed to load config: ${err.message}`, 4);\n\tif (err instanceof PlatformError) {\n\t\tconst exitCode = EXIT_CODE_BY_PLATFORM_ERROR_CODE[err.code];\n\t\tif (exitCode !== undefined)\n\t\t\treturn errorResult(err, err.message, exitCode);\n\t\treturn errorResult(err, `[${err.code}] ${err.message}`, 5);\n\t}\n\tif (err instanceof Error) return errorResult(err, err.message, 1);\n\treturn failure(String(err), 1);\n}\n\nfunction errorResult(\n\terr: unknown,\n\tmessage: string,\n\texitCode: number,\n): CommandResult {\n\tconst result: CommandResult = {\n\t\texitCode,\n\t\tstdout: \"\",\n\t\tstderr: `${message}\\n`,\n\t};\n\tconst debug = buildDebugInfo(err);\n\tif (debug) result.debugInfo = debug;\n\treturn result;\n}\n\nfunction buildDebugInfo(err: unknown): string | undefined {\n\tif (!(err instanceof Error)) return undefined;\n\tconst lines: string[] = [];\n\tif (err instanceof PlatformError) {\n\t\tlines.push(`code : ${err.code}`);\n\t\tif (Object.keys(err.details).length > 0) {\n\t\t\tlines.push(`details : ${JSON.stringify(err.details, null, 2)}`);\n\t\t}\n\t}\n\tif (err.cause instanceof Error) {\n\t\tlines.push(`cause : ${err.cause.name}: ${err.cause.message}`);\n\t}\n\tif (err.stack) {\n\t\tlines.push(err.stack);\n\t}\n\treturn lines.length > 0 ? lines.join(\"\\n\") : undefined;\n}\n\nfunction failure(message: string, exitCode = 1): CommandResult {\n\treturn { exitCode, stdout: \"\", stderr: `${message}\\n` };\n}\n"],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ //#region src/lib/cli/resolve-context.d.ts
2
+ /**
3
+ * Resolved project + branch context for the `neon-env` CLI. The CLI owns this resolution
4
+ * (flags → `NEON_*` env → `.neon[/project.json]` file) so the `@neon/env` library
5
+ * functions can stay filesystem- and env-agnostic.
6
+ */
7
+ interface ResolvedContext {
8
+ projectId: string;
9
+ branchId: string;
10
+ }
11
+ interface ResolveContextOptions {
12
+ projectId?: string;
13
+ branch?: string;
14
+ cwd: string;
15
+ env?: NodeJS.ProcessEnv;
16
+ }
17
+ /**
18
+ * Resolve `projectId` and `branch` for a CLI invocation. Precedence (each wins over the
19
+ * next): explicit flag → `NEON_*` env var → `.neon[/project.json]` walked up from `cwd`.
20
+ *
21
+ * Returns the resolved values plus a list of human-readable reasons for any field that
22
+ * could not be resolved (so the caller can render one combined error).
23
+ */
24
+ declare function resolveContext(options: ResolveContextOptions): {
25
+ ok: true;
26
+ context: ResolvedContext;
27
+ } | {
28
+ ok: false;
29
+ missing: string[];
30
+ };
31
+ //#endregion
32
+ export { ResolveContextOptions, ResolvedContext, resolveContext };
33
+ //# sourceMappingURL=resolve-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-context.d.ts","names":[],"sources":["../../../src/lib/cli/resolve-context.ts"],"mappings":";;AASA;AAKA;AAcA;;AACU,UApBO,eAAA,CAoBP;WACc,EAAA,MAAA;EAAe,QAAA,EAAA,MAAA;;UAhBtB,qBAAA;;;;QAIV,MAAA,CAAO;;;;;;;;;iBAUE,cAAA,UACN;;WACc"}
@@ -0,0 +1,87 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { homedir } from "node:os";
4
+ //#region src/lib/cli/resolve-context.ts
5
+ /**
6
+ * Resolve `projectId` and `branch` for a CLI invocation. Precedence (each wins over the
7
+ * next): explicit flag → `NEON_*` env var → `.neon[/project.json]` walked up from `cwd`.
8
+ *
9
+ * Returns the resolved values plus a list of human-readable reasons for any field that
10
+ * could not be resolved (so the caller can render one combined error).
11
+ */
12
+ function resolveContext(options) {
13
+ const env = options.env ?? process.env;
14
+ const file = findNeonFile(options.cwd);
15
+ const projectId = nonEmpty(options.projectId) ?? nonEmpty(env.NEON_PROJECT_ID) ?? file?.projectId;
16
+ const branchId = nonEmpty(options.branch) ?? nonEmpty(env.NEON_BRANCH_ID) ?? file?.branchId;
17
+ const missing = [];
18
+ if (!projectId) missing.push("project id — pass `--project-id`, set `NEON_PROJECT_ID`, or add `projectId` to `.neon/project.json` (run `npx neonctl link`).");
19
+ if (!branchId) missing.push("branch — pass `--branch`, set `NEON_BRANCH_ID`, or add `branchId` to `.neon/project.json` (run `npx neonctl checkout <branch>`).");
20
+ if (!projectId || !branchId) return {
21
+ ok: false,
22
+ missing
23
+ };
24
+ return {
25
+ ok: true,
26
+ context: {
27
+ projectId,
28
+ branchId
29
+ }
30
+ };
31
+ }
32
+ /**
33
+ * Walk up from `cwd` looking for `.neon/project.json` (preferred) or `.neon` (neonctl
34
+ * convention). Stops at the first `.git` directory or the home directory. Read-only.
35
+ */
36
+ function findNeonFile(cwd) {
37
+ let current = resolve(cwd);
38
+ const stop = resolve(homedir());
39
+ let lastSeen = null;
40
+ while (true) {
41
+ const parsed = readNeonFileAt(resolve(current, ".neon", "project.json")) ?? readNeonFileAt(resolve(current, ".neon"));
42
+ if (parsed) return parsed;
43
+ if (current === stop) return null;
44
+ if (existsSync(resolve(current, ".git"))) return null;
45
+ const parent = dirname(current);
46
+ if (parent === current || parent === lastSeen) return null;
47
+ lastSeen = current;
48
+ current = parent;
49
+ }
50
+ }
51
+ function readNeonFileAt(path) {
52
+ if (!isFile(path)) return null;
53
+ let raw;
54
+ try {
55
+ raw = readFileSync(path, "utf-8");
56
+ } catch {
57
+ return null;
58
+ }
59
+ let parsed;
60
+ try {
61
+ parsed = JSON.parse(raw);
62
+ } catch {
63
+ return null;
64
+ }
65
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
66
+ const obj = parsed;
67
+ const out = {};
68
+ if (typeof obj.projectId === "string" && obj.projectId !== "") out.projectId = obj.projectId;
69
+ if (typeof obj.branchId === "string" && obj.branchId !== "") out.branchId = obj.branchId;
70
+ return out;
71
+ }
72
+ function isFile(path) {
73
+ try {
74
+ return statSync(path).isFile();
75
+ } catch {
76
+ return false;
77
+ }
78
+ }
79
+ function nonEmpty(value) {
80
+ if (typeof value !== "string") return void 0;
81
+ const trimmed = value.trim();
82
+ return trimmed === "" ? void 0 : trimmed;
83
+ }
84
+ //#endregion
85
+ export { resolveContext };
86
+
87
+ //# sourceMappingURL=resolve-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-context.js","names":[],"sources":["../../../src/lib/cli/resolve-context.ts"],"sourcesContent":["import { existsSync, readFileSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, resolve } from \"node:path\";\n\n/**\n * Resolved project + branch context for the `neon-env` CLI. The CLI owns this resolution\n * (flags → `NEON_*` env → `.neon[/project.json]` file) so the `@neon/env` library\n * functions can stay filesystem- and env-agnostic.\n */\nexport interface ResolvedContext {\n\tprojectId: string;\n\tbranchId: string;\n}\n\nexport interface ResolveContextOptions {\n\tprojectId?: string;\n\tbranch?: string;\n\tcwd: string;\n\tenv?: NodeJS.ProcessEnv;\n}\n\n/**\n * Resolve `projectId` and `branch` for a CLI invocation. Precedence (each wins over the\n * next): explicit flag → `NEON_*` env var → `.neon[/project.json]` walked up from `cwd`.\n *\n * Returns the resolved values plus a list of human-readable reasons for any field that\n * could not be resolved (so the caller can render one combined error).\n */\nexport function resolveContext(\n\toptions: ResolveContextOptions,\n): { ok: true; context: ResolvedContext } | { ok: false; missing: string[] } {\n\tconst env = options.env ?? process.env;\n\tconst file = findNeonFile(options.cwd);\n\n\tconst projectId =\n\t\tnonEmpty(options.projectId) ??\n\t\tnonEmpty(env.NEON_PROJECT_ID) ??\n\t\tfile?.projectId;\n\n\tconst branchId =\n\t\tnonEmpty(options.branch) ??\n\t\tnonEmpty(env.NEON_BRANCH_ID) ??\n\t\tfile?.branchId;\n\n\tconst missing: string[] = [];\n\tif (!projectId) {\n\t\tmissing.push(\n\t\t\t\"project id — pass `--project-id`, set `NEON_PROJECT_ID`, or add `projectId` to `.neon/project.json` (run `npx neonctl link`).\",\n\t\t);\n\t}\n\tif (!branchId) {\n\t\tmissing.push(\n\t\t\t\"branch — pass `--branch`, set `NEON_BRANCH_ID`, or add `branchId` to `.neon/project.json` (run `npx neonctl checkout <branch>`).\",\n\t\t);\n\t}\n\tif (!projectId || !branchId) return { ok: false, missing };\n\n\treturn {\n\t\tok: true,\n\t\tcontext: { projectId, branchId },\n\t};\n}\n\ninterface NeonFile {\n\tprojectId?: string;\n\tbranchId?: string;\n}\n\n/**\n * Walk up from `cwd` looking for `.neon/project.json` (preferred) or `.neon` (neonctl\n * convention). Stops at the first `.git` directory or the home directory. Read-only.\n */\nfunction findNeonFile(cwd: string): NeonFile | null {\n\tlet current = resolve(cwd);\n\tconst stop = resolve(homedir());\n\tlet lastSeen: string | null = null;\n\n\twhile (true) {\n\t\tconst parsed =\n\t\t\treadNeonFileAt(resolve(current, \".neon\", \"project.json\")) ??\n\t\t\treadNeonFileAt(resolve(current, \".neon\"));\n\t\tif (parsed) return parsed;\n\n\t\tif (current === stop) return null;\n\t\tif (existsSync(resolve(current, \".git\"))) return null;\n\n\t\tconst parent = dirname(current);\n\t\tif (parent === current || parent === lastSeen) return null;\n\t\tlastSeen = current;\n\t\tcurrent = parent;\n\t}\n}\n\nfunction readNeonFileAt(path: string): NeonFile | null {\n\tif (!isFile(path)) return null;\n\tlet raw: string;\n\ttry {\n\t\traw = readFileSync(path, \"utf-8\");\n\t} catch {\n\t\treturn null;\n\t}\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch {\n\t\treturn null;\n\t}\n\tif (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed))\n\t\treturn null;\n\tconst obj = parsed as Record<string, unknown>;\n\tconst out: NeonFile = {};\n\tif (typeof obj.projectId === \"string\" && obj.projectId !== \"\")\n\t\tout.projectId = obj.projectId;\n\tif (typeof obj.branchId === \"string\" && obj.branchId !== \"\")\n\t\tout.branchId = obj.branchId;\n\treturn out;\n}\n\nfunction isFile(path: string): boolean {\n\ttry {\n\t\treturn statSync(path).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction nonEmpty(value: string | undefined): string | undefined {\n\tif (typeof value !== \"string\") return undefined;\n\tconst trimmed = value.trim();\n\treturn trimmed === \"\" ? undefined : trimmed;\n}\n"],"mappings":";;;;;;;;;;;AA4BA,SAAgB,eACf,SAC4E;CAC5E,MAAM,MAAM,QAAQ,OAAO,QAAQ;CACnC,MAAM,OAAO,aAAa,QAAQ,GAAG;CAErC,MAAM,YACL,SAAS,QAAQ,SAAS,KAC1B,SAAS,IAAI,eAAe,KAC5B,MAAM;CAEP,MAAM,WACL,SAAS,QAAQ,MAAM,KACvB,SAAS,IAAI,cAAc,KAC3B,MAAM;CAEP,MAAM,UAAoB,CAAC;CAC3B,IAAI,CAAC,WACJ,QAAQ,KACP,+HACD;CAED,IAAI,CAAC,UACJ,QAAQ,KACP,kIACD;CAED,IAAI,CAAC,aAAa,CAAC,UAAU,OAAO;EAAE,IAAI;EAAO;CAAQ;CAEzD,OAAO;EACN,IAAI;EACJ,SAAS;GAAE;GAAW;EAAS;CAChC;AACD;;;;;AAWA,SAAS,aAAa,KAA8B;CACnD,IAAI,UAAU,QAAQ,GAAG;CACzB,MAAM,OAAO,QAAQ,QAAQ,CAAC;CAC9B,IAAI,WAA0B;CAE9B,OAAO,MAAM;EACZ,MAAM,SACL,eAAe,QAAQ,SAAS,SAAS,cAAc,CAAC,KACxD,eAAe,QAAQ,SAAS,OAAO,CAAC;EACzC,IAAI,QAAQ,OAAO;EAEnB,IAAI,YAAY,MAAM,OAAO;EAC7B,IAAI,WAAW,QAAQ,SAAS,MAAM,CAAC,GAAG,OAAO;EAEjD,MAAM,SAAS,QAAQ,OAAO;EAC9B,IAAI,WAAW,WAAW,WAAW,UAAU,OAAO;EACtD,WAAW;EACX,UAAU;CACX;AACD;AAEA,SAAS,eAAe,MAA+B;CACtD,IAAI,CAAC,OAAO,IAAI,GAAG,OAAO;CAC1B,IAAI;CACJ,IAAI;EACH,MAAM,aAAa,MAAM,OAAO;CACjC,QAAQ;EACP,OAAO;CACR;CACA,IAAI;CACJ,IAAI;EACH,SAAS,KAAK,MAAM,GAAG;CACxB,QAAQ;EACP,OAAO;CACR;CACA,IAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GACxE,OAAO;CACR,MAAM,MAAM;CACZ,MAAM,MAAgB,CAAC;CACvB,IAAI,OAAO,IAAI,cAAc,YAAY,IAAI,cAAc,IAC1D,IAAI,YAAY,IAAI;CACrB,IAAI,OAAO,IAAI,aAAa,YAAY,IAAI,aAAa,IACxD,IAAI,WAAW,IAAI;CACpB,OAAO;AACR;AAEA,SAAS,OAAO,MAAuB;CACtC,IAAI;EACH,OAAO,SAAS,IAAI,CAAC,CAAC,OAAO;CAC9B,QAAQ;EACP,OAAO;CACR;AACD;AAEA,SAAS,SAAS,OAA+C;CAChE,IAAI,OAAO,UAAU,UAAU,OAAO,KAAA;CACtC,MAAM,UAAU,MAAM,KAAK;CAC3B,OAAO,YAAY,KAAK,KAAA,IAAY;AACrC"}