@neondatabase/env 0.13.2 → 0.14.0

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,76 @@
1
+ import "./profiles.js";
2
+ //#region src/_shared/auth_selection.ts
3
+ /**
4
+ * # Which credential an invocation authenticates with
5
+ *
6
+ * Four inputs can each answer "who am I": the `--api-key` flag, `NEON_API_KEY`, the
7
+ * `--profile` flag, and `NEON_PROFILE`. This module decides between them, and it is pure so
8
+ * the decision can be tested without a filesystem, a network, or a config directory.
9
+ *
10
+ * ## The rule
11
+ *
12
+ * **An explicit flag beats an ambient environment variable.** That single rule fixes the bug
13
+ * this module exists for: before it, any API key — including one merely exported into the
14
+ * shell — silently voided `--profile`, so `neon --profile work …` would quietly run as
15
+ * whoever `NEON_API_KEY` belonged to and say nothing about it.
16
+ *
17
+ * | Given | What runs |
18
+ * | --- | --- |
19
+ * | `--api-key` and `--profile` | neither: contradictory explicit flags, so this throws |
20
+ * | `--api-key` and `NEON_PROFILE` | the flag's key |
21
+ * | `--profile` and `NEON_API_KEY` | the profile |
22
+ * | `NEON_API_KEY` and `NEON_PROFILE` | the key, and the ignored profile is named in a warning |
23
+ * | `--profile` or `NEON_PROFILE` alone | that profile |
24
+ * | nothing | `DEFAULT` |
25
+ *
26
+ * Two explicit flags throw rather than picking a winner. They express different intents —
27
+ * `--api-key` supplies a credential, `--profile` selects a stored one — so there is no
28
+ * reading of the command that makes both true, and guessing is how the original bug behaved.
29
+ *
30
+ * When both are merely ambient, the key wins. That keeps CI exactly as it was: a pipeline
31
+ * that injects `NEON_API_KEY` must not change behaviour because a `NEON_PROFILE` leaked into
32
+ * the environment. It warns instead of staying silent, because a disregarded account
33
+ * selection is precisely what nobody noticed last time.
34
+ *
35
+ * `auth` and the `profile` subcommands do not use any of this. They read the same flags with
36
+ * different meanings — `neon auth --profile work` names where to *write* a credential, and
37
+ * `neon profile create work --api-key …` names one to *store* — so their callers skip
38
+ * selection entirely rather than passing exemptions down here.
39
+ */
40
+ const selectCredential = ({ apiKeyFlag, profileFlag, apiKeyEnv, profileEnv }) => {
41
+ const flagKey = nonEmpty(apiKeyFlag);
42
+ const flagProfile = nonEmpty(profileFlag);
43
+ if (flagKey !== void 0 && flagProfile !== void 0) throw new Error("Pass either --api-key or --profile, not both. --api-key supplies a credential directly; --profile selects a stored one.");
44
+ if (flagKey !== void 0) return {
45
+ source: "explicit-api-key",
46
+ apiKey: flagKey
47
+ };
48
+ if (flagProfile !== void 0) return {
49
+ source: "profile",
50
+ profile: flagProfile,
51
+ explicit: true
52
+ };
53
+ const envKey = nonEmpty(apiKeyEnv);
54
+ const envProfile = nonEmpty(profileEnv);
55
+ if (envKey !== void 0) return {
56
+ source: "ambient-api-key",
57
+ apiKey: envKey,
58
+ ...envProfile !== void 0 ? { ignoredProfile: envProfile } : {}
59
+ };
60
+ return {
61
+ source: "profile",
62
+ profile: envProfile ?? "DEFAULT",
63
+ explicit: envProfile !== void 0
64
+ };
65
+ };
66
+ /** The warning for an ambient key that displaced an ambient profile, or `null`. */
67
+ const displacedProfileWarning = (selection) => selection.source === "ambient-api-key" && selection.ignoredProfile !== void 0 ? `NEON_API_KEY is set, so profile "${selection.ignoredProfile}" from NEON_PROFILE was ignored. Pass --profile ${selection.ignoredProfile} to use it instead.` : null;
68
+ function nonEmpty(value) {
69
+ if (typeof value !== "string") return void 0;
70
+ const trimmed = value.trim();
71
+ return trimmed === "" ? void 0 : trimmed;
72
+ }
73
+ //#endregion
74
+ export { displacedProfileWarning, selectCredential };
75
+
76
+ //# sourceMappingURL=auth_selection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth_selection.js","names":[],"sources":["../../src/_shared/auth_selection.ts"],"sourcesContent":["/**\n * # Which credential an invocation authenticates with\n *\n * Four inputs can each answer \"who am I\": the `--api-key` flag, `NEON_API_KEY`, the\n * `--profile` flag, and `NEON_PROFILE`. This module decides between them, and it is pure so\n * the decision can be tested without a filesystem, a network, or a config directory.\n *\n * ## The rule\n *\n * **An explicit flag beats an ambient environment variable.** That single rule fixes the bug\n * this module exists for: before it, any API key — including one merely exported into the\n * shell — silently voided `--profile`, so `neon --profile work …` would quietly run as\n * whoever `NEON_API_KEY` belonged to and say nothing about it.\n *\n * | Given | What runs |\n * | --- | --- |\n * | `--api-key` and `--profile` | neither: contradictory explicit flags, so this throws |\n * | `--api-key` and `NEON_PROFILE` | the flag's key |\n * | `--profile` and `NEON_API_KEY` | the profile |\n * | `NEON_API_KEY` and `NEON_PROFILE` | the key, and the ignored profile is named in a warning |\n * | `--profile` or `NEON_PROFILE` alone | that profile |\n * | nothing | `DEFAULT` |\n *\n * Two explicit flags throw rather than picking a winner. They express different intents —\n * `--api-key` supplies a credential, `--profile` selects a stored one — so there is no\n * reading of the command that makes both true, and guessing is how the original bug behaved.\n *\n * When both are merely ambient, the key wins. That keeps CI exactly as it was: a pipeline\n * that injects `NEON_API_KEY` must not change behaviour because a `NEON_PROFILE` leaked into\n * the environment. It warns instead of staying silent, because a disregarded account\n * selection is precisely what nobody noticed last time.\n *\n * `auth` and the `profile` subcommands do not use any of this. They read the same flags with\n * different meanings — `neon auth --profile work` names where to *write* a credential, and\n * `neon profile create work --api-key …` names one to *store* — so their callers skip\n * selection entirely rather than passing exemptions down here.\n */\n\nimport { DEFAULT_PROFILE } from \"./profiles.js\";\n\nexport type CredentialSelection =\n\t/** `--api-key`. Used as given; no profile is consulted and no stored file is touched. */\n\t| { source: \"explicit-api-key\"; apiKey: string }\n\t/** `NEON_API_KEY`, with the profile it displaced when there was one. */\n\t| { source: \"ambient-api-key\"; apiKey: string; ignoredProfile?: string }\n\t/** A profile, whose file decides whether that means an API key or OAuth. */\n\t| { source: \"profile\"; profile: string; explicit: boolean };\n\nexport type SelectionInput = {\n\t/** The `--api-key` flag, before any environment fallback has been folded into it. */\n\tapiKeyFlag?: string;\n\t/** The `--profile` flag. */\n\tprofileFlag?: string;\n\t/** `NEON_API_KEY`. */\n\tapiKeyEnv?: string;\n\t/** `NEON_PROFILE`. */\n\tprofileEnv?: string;\n};\n\n/**\n * What the four credential inputs were for this invocation, captured by\n * `resolveApiKeyFromEnv` — which is the one place that reads the environment.\n *\n * Two reasons this is module state rather than fields on the parsed arguments, the same two\n * that put `auth_context` here: an extra key on `args` is rejected by every command calling\n * `.strict()`, and a hidden option to carry it would be a second undocumented way to pass a\n * credential. One process is one invocation, so there is nothing to get out of step.\n *\n * Capturing the environment here rather than reading it inside {@link selectCredential} keeps\n * the selection a function of its arguments. That is not tidiness: `ensureAuth` is called\n * directly by tests, and reading `process.env` down in the decision made those tests depend on\n * whether the developer running them happened to have `NEON_API_KEY` exported.\n */\nexport type CredentialInputs = {\n\tapiKeyFlag: string;\n\tapiKeyEnv: string;\n\tprofileEnv: string;\n};\n\nconst NO_INPUTS: CredentialInputs = {\n\tapiKeyFlag: \"\",\n\tapiKeyEnv: \"\",\n\tprofileEnv: \"\",\n};\n\nlet inputs: CredentialInputs = NO_INPUTS;\n\nexport const recordCredentialInputs = (recorded: CredentialInputs): void => {\n\tinputs = recorded;\n};\n\nexport const credentialInputs = (): CredentialInputs => inputs;\n\nexport const selectCredential = ({\n\tapiKeyFlag,\n\tprofileFlag,\n\tapiKeyEnv,\n\tprofileEnv,\n}: SelectionInput): CredentialSelection => {\n\tconst flagKey = nonEmpty(apiKeyFlag);\n\tconst flagProfile = nonEmpty(profileFlag);\n\n\tif (flagKey !== undefined && flagProfile !== undefined) {\n\t\tthrow new Error(\n\t\t\t\"Pass either --api-key or --profile, not both. --api-key supplies a credential directly; --profile selects a stored one.\",\n\t\t);\n\t}\n\n\tif (flagKey !== undefined) {\n\t\treturn { source: \"explicit-api-key\", apiKey: flagKey };\n\t}\n\n\tif (flagProfile !== undefined) {\n\t\treturn { source: \"profile\", profile: flagProfile, explicit: true };\n\t}\n\n\tconst envKey = nonEmpty(apiKeyEnv);\n\tconst envProfile = nonEmpty(profileEnv);\n\n\tif (envKey !== undefined) {\n\t\treturn {\n\t\t\tsource: \"ambient-api-key\",\n\t\t\tapiKey: envKey,\n\t\t\t...(envProfile !== undefined ? { ignoredProfile: envProfile } : {}),\n\t\t};\n\t}\n\n\treturn {\n\t\tsource: \"profile\",\n\t\tprofile: envProfile ?? DEFAULT_PROFILE,\n\t\texplicit: envProfile !== undefined,\n\t};\n};\n\n/** The warning for an ambient key that displaced an ambient profile, or `null`. */\nexport const displacedProfileWarning = (\n\tselection: CredentialSelection,\n): string | null =>\n\tselection.source === \"ambient-api-key\" &&\n\tselection.ignoredProfile !== undefined\n\t\t? `NEON_API_KEY is set, so profile \"${selection.ignoredProfile}\" from NEON_PROFILE was ignored. Pass --profile ${selection.ignoredProfile} to use it instead.`\n\t\t: null;\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6FA,MAAa,oBAAoB,EAChC,YACA,aACA,WACA,iBAC0C;CAC1C,MAAM,UAAU,SAAS,UAAU;CACnC,MAAM,cAAc,SAAS,WAAW;CAExC,IAAI,YAAY,KAAA,KAAa,gBAAgB,KAAA,GAC5C,MAAM,IAAI,MACT,yHACD;CAGD,IAAI,YAAY,KAAA,GACf,OAAO;EAAE,QAAQ;EAAoB,QAAQ;CAAQ;CAGtD,IAAI,gBAAgB,KAAA,GACnB,OAAO;EAAE,QAAQ;EAAW,SAAS;EAAa,UAAU;CAAK;CAGlE,MAAM,SAAS,SAAS,SAAS;CACjC,MAAM,aAAa,SAAS,UAAU;CAEtC,IAAI,WAAW,KAAA,GACd,OAAO;EACN,QAAQ;EACR,QAAQ;EACR,GAAI,eAAe,KAAA,IAAY,EAAE,gBAAgB,WAAW,IAAI,CAAC;CAClE;CAGD,OAAO;EACN,QAAQ;EACR,SAAS,cAAA;EACT,UAAU,eAAe,KAAA;CAC1B;AACD;;AAGA,MAAa,2BACZ,cAEA,UAAU,WAAW,qBACrB,UAAU,mBAAmB,KAAA,IAC1B,oCAAoC,UAAU,eAAe,kDAAkD,UAAU,eAAe,uBACxI;AAEJ,SAAS,SAAS,OAA+C;CAChE,IAAI,OAAO,UAAU,UAAU,OAAO,KAAA;CACtC,MAAM,UAAU,MAAM,KAAK;CAC3B,OAAO,YAAY,KAAK,KAAA,IAAY;AACrC"}
@@ -0,0 +1,131 @@
1
+ import { readFileSync } from "node:fs";
2
+ //#region src/_shared/credentials.ts
3
+ /**
4
+ * # Stored credentials — one file per account, two kinds
5
+ *
6
+ * A profile points at exactly one credentials file (see `./profiles.ts`), and that file says
7
+ * what kind of credential it holds. Adding API-key support this way rather than adding a
8
+ * second pointer to `profiles.json` keeps a profile what it already was — one name, one path
9
+ * — and means `profiles.json` needs no schema change at all.
10
+ *
11
+ * ```json
12
+ * // oauth: every file written before this existed. An absent `type` means this.
13
+ * { "access_token": "…", "refresh_token": "…", "expires_at": 1786…, "user_id": "…" }
14
+ *
15
+ * // api_key, stored by `neon profile create --api-key`
16
+ * { "type": "api_key", "api_key": "napi_…", "user_id": "…" }
17
+ *
18
+ * // api_key minted by `--mint --org-id`, which records the scope it was issued at
19
+ * { "type": "api_key", "api_key": "napi_…", "key_id": 123, "org_id": "org-…" }
20
+ * ```
21
+ *
22
+ * ## One profile, one kind
23
+ *
24
+ * A credentials file holds an API key or an OAuth session, never both, and `type` states
25
+ * which. An earlier draft let the two coexist — the idea being that a key could keep the
26
+ * session it was minted from and so rotate without a browser. It did not survive review, for
27
+ * two reasons that are worth recording so nobody rebuilds it:
28
+ *
29
+ * 1. **It never worked.** The resolver returned the key without testing it, so a revoked key
30
+ * failed to mint and never fell back to the session sitting beside it.
31
+ * 2. **It could mix accounts.** Nothing compared the identity of the credential being written
32
+ * with the one already there, so a profile could hold one account's session and another's
33
+ * key, told apart only by a single string. Flip or lose `type` and the profile silently
34
+ * becomes a different person.
35
+ *
36
+ * Recovery from a dead key is therefore one browser login — `neon profile create <name>
37
+ * --mint --force` — which is what the retained session was supposed to save and never did.
38
+ *
39
+ * ## Older releases
40
+ *
41
+ * A CLI predating this reads the pointer, finds no `type` it understands, ignores it, and
42
+ * looks for `access_token`. An `api_key` profile has none, so an older release falls through
43
+ * to its browser login rather than crashing. That it does not crash is why `credentials`
44
+ * stays a required pointer: an entry without one makes 2.41 and 2.42 throw
45
+ * `ERR_INVALID_ARG_TYPE` from `resolveEntryPath`.
46
+ */
47
+ const OAUTH = "oauth";
48
+ const API_KEY = "api_key";
49
+ /**
50
+ * Which credential in this file authenticates, by declaration alone.
51
+ *
52
+ * An unrecognised `type` throws rather than falling back to `oauth`. A file we cannot
53
+ * interpret is a misconfiguration the user has to see: treating it as OAuth would send them
54
+ * to a browser login that silently replaces a credential they meant to keep, and treating it
55
+ * as an API key would authenticate with whatever `api_key` happened to be there.
56
+ *
57
+ * This deliberately does not check that an `api_key` file has a key — `neon profile list`
58
+ * needs the kind of a file it is not about to authenticate with, and must be able to report a
59
+ * broken one rather than throwing halfway through a table.
60
+ */
61
+ const credentialKind = (credentials, at) => {
62
+ const declared = credentials.type;
63
+ if (declared === void 0 || declared === "oauth") return OAUTH;
64
+ if (declared === "api_key") return API_KEY;
65
+ throw new Error(`${at.path} declares a "type" this version does not understand. Expected "${OAUTH}" or "${API_KEY}". ${repair(at)}`);
66
+ };
67
+ /**
68
+ * The way out of a credentials file that cannot be read.
69
+ *
70
+ * One sentence, shared by every such error, because they all have the same two answers: write
71
+ * a new credential over it, or delete it and start again.
72
+ */
73
+ const repair = (at) => `Replace it deliberately with \`neon profile create ${at.profile} --force\`, or delete the file.`;
74
+ /**
75
+ * Resolve what to authenticate with, validating that the declared kind is actually usable.
76
+ *
77
+ * An `api_key` file with no key is a hard error rather than a fall-through to OAuth: the user
78
+ * asked for a key, and quietly opening a browser instead would replace the credential they
79
+ * were trying to fix.
80
+ */
81
+ const interpretCredentials = (credentials, at) => {
82
+ if (credentialKind(credentials, at) === "oauth") return { kind: OAUTH };
83
+ const apiKey = nonEmpty(credentials.api_key);
84
+ if (apiKey === void 0) throw new Error(`${at.path} declares "type": "${API_KEY}" but has no "api_key" value. ${repair(at)}`);
85
+ return {
86
+ kind: API_KEY,
87
+ apiKey
88
+ };
89
+ };
90
+ /**
91
+ * Read and classify a credentials file, without deciding what to do about it.
92
+ *
93
+ * A permission or I/O error still throws: there may be a perfectly good credential here that
94
+ * we cannot see, and treating that as absent would send the user to a browser login that
95
+ * overwrites it.
96
+ */
97
+ const inspectCredentials = (path) => {
98
+ let contents;
99
+ try {
100
+ contents = readFileSync(path, "utf8");
101
+ } catch (err) {
102
+ if (err.code === "ENOENT") return { kind: "absent" };
103
+ throw err;
104
+ }
105
+ let parsed;
106
+ try {
107
+ parsed = JSON.parse(contents);
108
+ } catch {
109
+ return {
110
+ kind: "unusable",
111
+ reason: `${path} is not valid JSON, so the credential in it cannot be read`
112
+ };
113
+ }
114
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {
115
+ kind: "unusable",
116
+ reason: `${path} does not contain a credentials object`
117
+ };
118
+ return {
119
+ kind: "ok",
120
+ credentials: parsed
121
+ };
122
+ };
123
+ function nonEmpty(value) {
124
+ if (typeof value !== "string") return void 0;
125
+ const trimmed = value.trim();
126
+ return trimmed === "" ? void 0 : trimmed;
127
+ }
128
+ //#endregion
129
+ export { API_KEY, OAUTH, credentialKind, inspectCredentials, interpretCredentials };
130
+
131
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.js","names":[],"sources":["../../src/_shared/credentials.ts"],"sourcesContent":["/**\n * # Stored credentials — one file per account, two kinds\n *\n * A profile points at exactly one credentials file (see `./profiles.ts`), and that file says\n * what kind of credential it holds. Adding API-key support this way rather than adding a\n * second pointer to `profiles.json` keeps a profile what it already was — one name, one path\n * — and means `profiles.json` needs no schema change at all.\n *\n * ```json\n * // oauth: every file written before this existed. An absent `type` means this.\n * { \"access_token\": \"…\", \"refresh_token\": \"…\", \"expires_at\": 1786…, \"user_id\": \"…\" }\n *\n * // api_key, stored by `neon profile create --api-key`\n * { \"type\": \"api_key\", \"api_key\": \"napi_…\", \"user_id\": \"…\" }\n *\n * // api_key minted by `--mint --org-id`, which records the scope it was issued at\n * { \"type\": \"api_key\", \"api_key\": \"napi_…\", \"key_id\": 123, \"org_id\": \"org-…\" }\n * ```\n *\n * ## One profile, one kind\n *\n * A credentials file holds an API key or an OAuth session, never both, and `type` states\n * which. An earlier draft let the two coexist — the idea being that a key could keep the\n * session it was minted from and so rotate without a browser. It did not survive review, for\n * two reasons that are worth recording so nobody rebuilds it:\n *\n * 1. **It never worked.** The resolver returned the key without testing it, so a revoked key\n * failed to mint and never fell back to the session sitting beside it.\n * 2. **It could mix accounts.** Nothing compared the identity of the credential being written\n * with the one already there, so a profile could hold one account's session and another's\n * key, told apart only by a single string. Flip or lose `type` and the profile silently\n * becomes a different person.\n *\n * Recovery from a dead key is therefore one browser login — `neon profile create <name>\n * --mint --force` — which is what the retained session was supposed to save and never did.\n *\n * ## Older releases\n *\n * A CLI predating this reads the pointer, finds no `type` it understands, ignores it, and\n * looks for `access_token`. An `api_key` profile has none, so an older release falls through\n * to its browser login rather than crashing. That it does not crash is why `credentials`\n * stays a required pointer: an entry without one makes 2.41 and 2.42 throw\n * `ERR_INVALID_ARG_TYPE` from `resolveEntryPath`.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { writeSecretFile } from \"./secure_file.js\";\n\nexport const OAUTH = \"oauth\";\nexport const API_KEY = \"api_key\";\n\nexport type CredentialKind = typeof OAUTH | typeof API_KEY;\n\n/**\n * The on-disk shape. Every field is optional because the two kinds overlap and because an\n * OAuth token endpoint response carries more than we name here — the index signature keeps\n * those extra fields on a round-trip rather than dropping them.\n */\nexport type StoredCredentials = {\n\ttype?: string;\n\tapi_key?: string;\n\tkey_id?: number;\n\t/** Set when the key was minted for an organization rather than the account. */\n\torg_id?: string;\n\t/** Set when the key was narrowed to a single project. Implies `org_id`. */\n\tproject_id?: string;\n\tuser_id?: string;\n\taccess_token?: string;\n\trefresh_token?: string;\n\texpires_at?: number;\n\t[key: string]: unknown;\n};\n\n/**\n * Where a credential lives, and which profile points at it.\n *\n * Both halves are needed to report a broken file: the path says which file to open, and the\n * profile is what every recovery command takes as its argument. Carrying only the path is what\n * produced errors telling the user to run `neon profile create <name> --force` with the\n * placeholder intact — a command an agent will run verbatim and be told `Invalid profile name\n * \"<name>\"`.\n */\nexport type CredentialLocation = {\n\tpath: string;\n\tprofile: string;\n};\n\n/**\n * Which credential in this file authenticates, by declaration alone.\n *\n * An unrecognised `type` throws rather than falling back to `oauth`. A file we cannot\n * interpret is a misconfiguration the user has to see: treating it as OAuth would send them\n * to a browser login that silently replaces a credential they meant to keep, and treating it\n * as an API key would authenticate with whatever `api_key` happened to be there.\n *\n * This deliberately does not check that an `api_key` file has a key — `neon profile list`\n * needs the kind of a file it is not about to authenticate with, and must be able to report a\n * broken one rather than throwing halfway through a table.\n */\nexport const credentialKind = (\n\tcredentials: StoredCredentials,\n\tat: CredentialLocation,\n): CredentialKind => {\n\tconst declared = credentials.type;\n\tif (declared === undefined || declared === OAUTH) return OAUTH;\n\tif (declared === API_KEY) return API_KEY;\n\t// The value is not quoted back. Everything in this file is secret material, and a\n\t// corrupted or hand-edited file can put a key anywhere in it — including here. Naming the\n\t// file is enough to act on, and it cannot leak what the file holds.\n\tthrow new Error(\n\t\t`${at.path} declares a \"type\" this version does not understand. Expected \"${OAUTH}\" or \"${API_KEY}\". ${repair(at)}`,\n\t);\n};\n\n/**\n * The way out of a credentials file that cannot be read.\n *\n * One sentence, shared by every such error, because they all have the same two answers: write\n * a new credential over it, or delete it and start again.\n */\nconst repair = (at: CredentialLocation): string =>\n\t`Replace it deliberately with \\`neon profile create ${at.profile} --force\\`, or delete the file.`;\n\n/** A credentials file resolved far enough to authenticate with. */\nexport type InterpretedCredentials =\n\t| { kind: typeof API_KEY; apiKey: string }\n\t| { kind: typeof OAUTH };\n\n/**\n * Resolve what to authenticate with, validating that the declared kind is actually usable.\n *\n * An `api_key` file with no key is a hard error rather than a fall-through to OAuth: the user\n * asked for a key, and quietly opening a browser instead would replace the credential they\n * were trying to fix.\n */\nexport const interpretCredentials = (\n\tcredentials: StoredCredentials,\n\tat: CredentialLocation,\n): InterpretedCredentials => {\n\tif (credentialKind(credentials, at) === OAUTH) return { kind: OAUTH };\n\tconst apiKey = nonEmpty(credentials.api_key);\n\tif (apiKey === undefined) {\n\t\tthrow new Error(\n\t\t\t`${at.path} declares \"type\": \"${API_KEY}\" but has no \"api_key\" value. ${repair(at)}`,\n\t\t);\n\t}\n\treturn { kind: API_KEY, apiKey };\n};\n\n/**\n * Read a credentials file, or `null` when there is nothing usable there.\n *\n * Missing and unparseable both return `null`, because both are recoverable by authenticating\n * again and the callers already treat \"no credentials\" as \"log in\". A file that parses but\n * contradicts itself is different — {@link credentialKind} throws for that, since re-running\n * `auth` would paper over a mistake rather than fix it.\n */\nexport type CredentialsRead =\n\t| { kind: \"ok\"; credentials: StoredCredentials }\n\t| { kind: \"absent\" }\n\t/** The file is there but cannot be understood. `reason` is safe to print. */\n\t| { kind: \"unusable\"; reason: string };\n\n/**\n * Read and classify a credentials file, without deciding what to do about it.\n *\n * A permission or I/O error still throws: there may be a perfectly good credential here that\n * we cannot see, and treating that as absent would send the user to a browser login that\n * overwrites it.\n */\nexport const inspectCredentials = (path: string): CredentialsRead => {\n\tlet contents: string;\n\ttry {\n\t\tcontents = readFileSync(path, \"utf8\");\n\t} catch (err) {\n\t\tif ((err as NodeJS.ErrnoException).code === \"ENOENT\")\n\t\t\treturn { kind: \"absent\" };\n\t\tthrow err;\n\t}\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(contents);\n\t} catch {\n\t\t// The parser's message is deliberately discarded. V8 quotes a window of the input\n\t\t// around the syntax error — on Node 24 a truncated credentials file produced\n\t\t// `Unexpected token 'a', ...\"api_key\":napi_SUPERS\"... is not valid JSON` — and this\n\t\t// reason is printed by `profile list` and by every failed authentication. A malformed\n\t\t// secret file is exactly when a diagnostic must say less, not more.\n\t\treturn {\n\t\t\tkind: \"unusable\",\n\t\t\treason: `${path} is not valid JSON, so the credential in it cannot be read`,\n\t\t};\n\t}\n\tif (\n\t\tparsed === null ||\n\t\ttypeof parsed !== \"object\" ||\n\t\tArray.isArray(parsed)\n\t) {\n\t\treturn {\n\t\t\tkind: \"unusable\",\n\t\t\treason: `${path} does not contain a credentials object`,\n\t\t};\n\t}\n\treturn { kind: \"ok\", credentials: parsed as StoredCredentials };\n};\n\n/**\n * The credential at `path`, or `null` when the file is not there.\n *\n * A damaged file is an error, not an absence. Treating it as absent — which is what this used to\n * do — meant any read-only command could repair it by starting a browser sign-in and overwriting\n * it, **possibly as a different account**, with the user never having asked for a repair and no\n * way back to whatever was in the file. Failing here costs one deliberate command; the message\n * names it.\n *\n * `profile list` and telemetry use {@link inspectCredentials} instead, because describing a\n * broken credential is not the same as using one.\n */\nexport const readCredentials = (\n\tat: CredentialLocation,\n): StoredCredentials | null => {\n\tconst read = inspectCredentials(at.path);\n\tif (read.kind === \"unusable\") {\n\t\tthrow new Error(`${read.reason}. ${repair(at)}`);\n\t}\n\treturn read.kind === \"ok\" ? read.credentials : null;\n};\n\nexport const writeCredentials = (\n\tpath: string,\n\tcredentials: StoredCredentials,\n): void => {\n\twriteSecretFile(path, JSON.stringify(credentials));\n};\n\n/** The scope a minted key was issued at. Absent org means an account key. */\nexport type KeyScope = {\n\torgId?: string;\n\tprojectId?: string;\n};\n\n/**\n * Build an `api_key` credentials object. Nothing from a previous credential is carried over.\n *\n * The scope is stored because it is not recoverable from the secret: `rotate-key` has to mint\n * the replacement on the same endpoint, and an org or project key minted as an account key\n * would silently widen what the profile reaches.\n */\nexport const apiKeyCredentials = ({\n\tapiKey,\n\tkeyId,\n\tuserId,\n\tscope,\n}: {\n\tapiKey: string;\n\tkeyId?: number;\n\tuserId?: string;\n\tscope?: KeyScope;\n}): StoredCredentials => ({\n\ttype: API_KEY,\n\tapi_key: apiKey,\n\t...(keyId !== undefined ? { key_id: keyId } : {}),\n\t...(userId !== undefined ? { user_id: userId } : {}),\n\t...(scope?.orgId !== undefined ? { org_id: scope.orgId } : {}),\n\t...(scope?.projectId !== undefined ? { project_id: scope.projectId } : {}),\n});\n\n/** The scope recorded on a stored credential. */\nexport const scopeOf = (credentials: StoredCredentials): KeyScope => ({\n\t...(typeof credentials.org_id === \"string\"\n\t\t? { orgId: credentials.org_id }\n\t\t: {}),\n\t...(typeof credentials.project_id === \"string\"\n\t\t? { projectId: credentials.project_id }\n\t\t: {}),\n});\n\n/** How to describe a scope in output. */\nexport const describeScope = (scope: KeyScope): string => {\n\tif (scope.projectId !== undefined) return `project ${scope.projectId}`;\n\tif (scope.orgId !== undefined) return `org ${scope.orgId}`;\n\treturn \"account\";\n};\n\nfunction nonEmpty(value: unknown): string | undefined {\n\tif (typeof value !== \"string\") return undefined;\n\tconst trimmed = value.trim();\n\treturn trimmed === \"\" ? undefined : trimmed;\n}\n\n/**\n * Whether a stored credential is the same secret as the one about to replace it.\n *\n * Re-storing the key a profile already holds is a no-op, not a replacement — and retiring it\n * would revoke the credential the command has just committed to. Trimmed on both sides, because\n * a key read from a file or a pipe arrives with a trailing newline.\n */\nexport const isSameCredential = (\n\texistingKey: string | undefined,\n\treplacementKey: string | undefined,\n): boolean => {\n\tif (existingKey === undefined || replacementKey === undefined) return false;\n\tconst trimmed = existingKey.trim();\n\treturn trimmed !== \"\" && trimmed === replacementKey.trim();\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,MAAa,QAAQ;AACrB,MAAa,UAAU;;;;;;;;;;;;;AAkDvB,MAAa,kBACZ,aACA,OACoB;CACpB,MAAM,WAAW,YAAY;CAC7B,IAAI,aAAa,KAAA,KAAa,aAAA,SAAoB,OAAO;CACzD,IAAI,aAAA,WAAsB,OAAO;CAIjC,MAAM,IAAI,MACT,GAAG,GAAG,KAAK,iEAAiE,MAAM,QAAQ,QAAQ,KAAK,OAAO,EAAE,GACjH;AACD;;;;;;;AAQA,MAAM,UAAU,OACf,sDAAsD,GAAG,QAAQ;;;;;;;;AAclE,MAAa,wBACZ,aACA,OAC4B;CAC5B,IAAI,eAAe,aAAa,EAAE,MAAA,SAAa,OAAO,EAAE,MAAM,MAAM;CACpE,MAAM,SAAS,SAAS,YAAY,OAAO;CAC3C,IAAI,WAAW,KAAA,GACd,MAAM,IAAI,MACT,GAAG,GAAG,KAAK,qBAAqB,QAAQ,gCAAgC,OAAO,EAAE,GAClF;CAED,OAAO;EAAE,MAAM;EAAS;CAAO;AAChC;;;;;;;;AAuBA,MAAa,sBAAsB,SAAkC;CACpE,IAAI;CACJ,IAAI;EACH,WAAW,aAAa,MAAM,MAAM;CACrC,SAAS,KAAK;EACb,IAAK,IAA8B,SAAS,UAC3C,OAAO,EAAE,MAAM,SAAS;EACzB,MAAM;CACP;CAEA,IAAI;CACJ,IAAI;EACH,SAAS,KAAK,MAAM,QAAQ;CAC7B,QAAQ;EAMP,OAAO;GACN,MAAM;GACN,QAAQ,GAAG,KAAK;EACjB;CACD;CACA,IACC,WAAW,QACX,OAAO,WAAW,YAClB,MAAM,QAAQ,MAAM,GAEpB,OAAO;EACN,MAAM;EACN,QAAQ,GAAG,KAAK;CACjB;CAED,OAAO;EAAE,MAAM;EAAM,aAAa;CAA4B;AAC/D;AAgFA,SAAS,SAAS,OAAoC;CACrD,IAAI,OAAO,UAAU,UAAU,OAAO,KAAA;CACtC,MAAM,UAAU,MAAM,KAAK;CAC3B,OAAO,YAAY,KAAK,KAAA,IAAY;AACrC"}
@@ -0,0 +1,132 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ //#region src/_shared/paths.ts
4
+ /**
5
+ * # Where the Neon CLIs keep their files on disk
6
+ *
7
+ **Deliberately impure.** It reads environment variables and touches the filesystem, which
8
+ * `@neon/config` — the package this used to be a subpath of — must never do from its root
9
+ * export. It lives here instead of there precisely so that a policy-facing package does not
10
+ * carry implementor-only code, and so `neon-init`, which has no workspace dependencies, can use
11
+ * the same resolution as everything else.
12
+ *
13
+ * It exists because three separate readers each grew their own answer to "where is the
14
+ * config directory", and all three disagreed: `packages/cli` honoured `XDG_CONFIG_HOME` but
15
+ * not `NEONCTL_CONFIG_DIR`, `packages/env` honoured the env var but not XDG, and
16
+ * `packages/init` hardcoded `~/.config/neonctl`. With `XDG_CONFIG_HOME` set, the CLI wrote
17
+ * credentials somewhere the other two never looked.
18
+ *
19
+ * ## The directory
20
+ *
21
+ * `neon` is the current name; `neonctl` is the legacy one, kept readable forever. Resolution,
22
+ * each entry winning over the next:
23
+ *
24
+ * 1. An explicit directory (a `--config-dir` flag) — **exact**, no legacy fallback.
25
+ * 2. `NEON_CONFIG_DIR` — exact.
26
+ * 3. `NEONCTL_CONFIG_DIR` (legacy name) — exact.
27
+ * 4. `$XDG_CONFIG_HOME/neon`, else `<home>/.config/neon`.
28
+ *
29
+ * An explicitly chosen directory is never paired with a fallback: `--config-dir /tmp/ci` that
30
+ * quietly read `~/.config/neonctl` would defeat the point of passing it.
31
+ *
32
+ * ## The files
33
+ *
34
+ * {@link resolveConfigFile} answers "which path should I use for this file", and it is the
35
+ * same answer for reading and writing:
36
+ *
37
+ * - Present in `neon/` → use it.
38
+ * - Present only in `neonctl/` → **use it there, in place.** An existing credentials file is
39
+ * never copied or moved, so nothing is left behind to go stale and no other tool starts
40
+ * reading an abandoned token.
41
+ * - Present in neither → the new location. New files only ever appear under `neon/`.
42
+ */
43
+ /** Current directory name. New files are created here. */
44
+ const CONFIG_DIR_NAME = "neon";
45
+ /** Legacy directory name, read forever so existing installs keep working untouched. */
46
+ const LEGACY_CONFIG_DIR_NAME = "neonctl";
47
+ /** Where files are created. See the module docs for the precedence. */
48
+ function configDir(options = {}) {
49
+ const explicit = explicitDir(options);
50
+ if (explicit) return explicit;
51
+ return join(configHome(options.env ?? process.env), CONFIG_DIR_NAME);
52
+ }
53
+ /**
54
+ * The legacy directory, or `undefined` when the location was chosen explicitly (in which
55
+ * case there is no legacy counterpart to fall back to).
56
+ */
57
+ function legacyConfigDir(options = {}) {
58
+ if (explicitDir(options)) return void 0;
59
+ return join(configHome(options.env ?? process.env), LEGACY_CONFIG_DIR_NAME);
60
+ }
61
+ /**
62
+ * Resolve one file inside the config directory. Prefers the current location, falls back to
63
+ * an existing legacy file **in place**, and otherwise points at the current location so new
64
+ * files are created there.
65
+ */
66
+ function resolveConfigFile(fileName, options = {}) {
67
+ const dir = configDir(options);
68
+ const current = resolve(dir, fileName);
69
+ if (existsSync(current)) return {
70
+ path: current,
71
+ dir,
72
+ isLegacy: false,
73
+ exists: true
74
+ };
75
+ const legacyDir = legacyConfigDir(options);
76
+ if (legacyDir) {
77
+ const legacy = resolve(legacyDir, fileName);
78
+ if (existsSync(legacy)) return {
79
+ path: legacy,
80
+ dir: legacyDir,
81
+ isLegacy: true,
82
+ exists: true
83
+ };
84
+ }
85
+ return {
86
+ path: current,
87
+ dir,
88
+ isLegacy: false,
89
+ exists: false
90
+ };
91
+ }
92
+ /** `$XDG_CONFIG_HOME`, else `<home>/.config`. Falls back to a relative `.config` with no home. */
93
+ function configHome(env) {
94
+ const xdg = nonEmpty(env.XDG_CONFIG_HOME);
95
+ if (xdg) return xdg;
96
+ const home = nonEmpty(env.HOME) ?? nonEmpty(env.USERPROFILE);
97
+ return home ? join(home, ".config") : ".config";
98
+ }
99
+ function explicitDir(options) {
100
+ const env = options.env ?? process.env;
101
+ return nonEmpty(options.dir) ?? nonEmpty(env.NEON_CONFIG_DIR) ?? nonEmpty(env.NEONCTL_CONFIG_DIR);
102
+ }
103
+ function nonEmpty(value) {
104
+ if (typeof value !== "string") return void 0;
105
+ const trimmed = value.trim();
106
+ return trimmed === "" ? void 0 : trimmed;
107
+ }
108
+ const CREDENTIALS_FILE = "credentials.json";
109
+ /**
110
+ * Default for `--config-dir`: `$XDG_CONFIG_HOME/neon`, else `~/.config/neon`.
111
+ *
112
+ * The directory was called `neonctl` until the CLI was renamed. An existing one is still read —
113
+ * see {@link credentialsPath} — but it is never written to, moved, or deleted.
114
+ */
115
+ const defaultDir = configDir();
116
+ /**
117
+ * Where this invocation's `credentials.json` lives.
118
+ *
119
+ * When `--config-dir` was left at its default, an existing file in the legacy `neonctl`
120
+ * directory is used **in place**: an install that predates the rename keeps working, and its
121
+ * credentials are never duplicated into a second location where one copy could go stale while
122
+ * another tool still reads it.
123
+ *
124
+ * A `--config-dir` the user actually passed is used exactly as given. Falling back out of an
125
+ * explicitly chosen directory would defeat the reason for choosing it — a CI run pointed at a
126
+ * scratch directory must never pick up a developer's real credentials.
127
+ */
128
+ const credentialsPath = (dir) => resolveConfigFile(CREDENTIALS_FILE, dir === defaultDir ? {} : { dir }).path;
129
+ //#endregion
130
+ export { CONFIG_DIR_NAME, CREDENTIALS_FILE, LEGACY_CONFIG_DIR_NAME, configDir, credentialsPath, defaultDir, legacyConfigDir, resolveConfigFile };
131
+
132
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","names":[],"sources":["../../src/_shared/paths.ts"],"sourcesContent":["/**\n * # Where the Neon CLIs keep their files on disk\n *\n **Deliberately impure.** It reads environment variables and touches the filesystem, which\n * `@neon/config` — the package this used to be a subpath of — must never do from its root\n * export. It lives here instead of there precisely so that a policy-facing package does not\n * carry implementor-only code, and so `neon-init`, which has no workspace dependencies, can use\n * the same resolution as everything else.\n *\n * It exists because three separate readers each grew their own answer to \"where is the\n * config directory\", and all three disagreed: `packages/cli` honoured `XDG_CONFIG_HOME` but\n * not `NEONCTL_CONFIG_DIR`, `packages/env` honoured the env var but not XDG, and\n * `packages/init` hardcoded `~/.config/neonctl`. With `XDG_CONFIG_HOME` set, the CLI wrote\n * credentials somewhere the other two never looked.\n *\n * ## The directory\n *\n * `neon` is the current name; `neonctl` is the legacy one, kept readable forever. Resolution,\n * each entry winning over the next:\n *\n * 1. An explicit directory (a `--config-dir` flag) — **exact**, no legacy fallback.\n * 2. `NEON_CONFIG_DIR` — exact.\n * 3. `NEONCTL_CONFIG_DIR` (legacy name) — exact.\n * 4. `$XDG_CONFIG_HOME/neon`, else `<home>/.config/neon`.\n *\n * An explicitly chosen directory is never paired with a fallback: `--config-dir /tmp/ci` that\n * quietly read `~/.config/neonctl` would defeat the point of passing it.\n *\n * ## The files\n *\n * {@link resolveConfigFile} answers \"which path should I use for this file\", and it is the\n * same answer for reading and writing:\n *\n * - Present in `neon/` → use it.\n * - Present only in `neonctl/` → **use it there, in place.** An existing credentials file is\n * never copied or moved, so nothing is left behind to go stale and no other tool starts\n * reading an abandoned token.\n * - Present in neither → the new location. New files only ever appear under `neon/`.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\n\n/** Current directory name. New files are created here. */\nexport const CONFIG_DIR_NAME = \"neon\";\n\n/** Legacy directory name, read forever so existing installs keep working untouched. */\nexport const LEGACY_CONFIG_DIR_NAME = \"neonctl\";\n\nexport interface ConfigPathOptions {\n\t/**\n\t * An explicit directory, e.g. from a `--config-dir` flag. Used exactly as given: no\n\t * environment variables are consulted and the legacy directory is never searched.\n\t */\n\tdir?: string;\n\t/** Environment to read. Defaults to `process.env`. Injectable for tests. */\n\tenv?: NodeJS.ProcessEnv;\n}\n\n/** Where files are created. See the module docs for the precedence. */\nexport function configDir(options: ConfigPathOptions = {}): string {\n\tconst explicit = explicitDir(options);\n\tif (explicit) return explicit;\n\treturn join(configHome(options.env ?? process.env), CONFIG_DIR_NAME);\n}\n\n/**\n * The legacy directory, or `undefined` when the location was chosen explicitly (in which\n * case there is no legacy counterpart to fall back to).\n */\nexport function legacyConfigDir(\n\toptions: ConfigPathOptions = {},\n): string | undefined {\n\tif (explicitDir(options)) return undefined;\n\treturn join(configHome(options.env ?? process.env), LEGACY_CONFIG_DIR_NAME);\n}\n\nexport interface ResolvedConfigFile {\n\t/** The path to use, for both reading and writing. */\n\tpath: string;\n\t/** The directory `path` lives in. */\n\tdir: string;\n\t/** True when the file was found in the legacy `neonctl` directory. */\n\tisLegacy: boolean;\n\t/** Whether the file exists at `path` right now. */\n\texists: boolean;\n}\n\n/**\n * Resolve one file inside the config directory. Prefers the current location, falls back to\n * an existing legacy file **in place**, and otherwise points at the current location so new\n * files are created there.\n */\nexport function resolveConfigFile(\n\tfileName: string,\n\toptions: ConfigPathOptions = {},\n): ResolvedConfigFile {\n\tconst dir = configDir(options);\n\tconst current = resolve(dir, fileName);\n\tif (existsSync(current))\n\t\treturn { path: current, dir, isLegacy: false, exists: true };\n\n\tconst legacyDir = legacyConfigDir(options);\n\tif (legacyDir) {\n\t\tconst legacy = resolve(legacyDir, fileName);\n\t\tif (existsSync(legacy))\n\t\t\treturn {\n\t\t\t\tpath: legacy,\n\t\t\t\tdir: legacyDir,\n\t\t\t\tisLegacy: true,\n\t\t\t\texists: true,\n\t\t\t};\n\t}\n\n\treturn { path: current, dir, isLegacy: false, exists: false };\n}\n\n/** `$XDG_CONFIG_HOME`, else `<home>/.config`. Falls back to a relative `.config` with no home. */\nfunction configHome(env: NodeJS.ProcessEnv): string {\n\tconst xdg = nonEmpty(env.XDG_CONFIG_HOME);\n\tif (xdg) return xdg;\n\tconst home = nonEmpty(env.HOME) ?? nonEmpty(env.USERPROFILE);\n\treturn home ? join(home, \".config\") : \".config\";\n}\n\nfunction explicitDir(options: ConfigPathOptions): string | undefined {\n\tconst env = options.env ?? process.env;\n\treturn (\n\t\tnonEmpty(options.dir) ??\n\t\tnonEmpty(env.NEON_CONFIG_DIR) ??\n\t\tnonEmpty(env.NEONCTL_CONFIG_DIR)\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\nexport const CREDENTIALS_FILE = \"credentials.json\";\n\n/**\n * Default for `--config-dir`: `$XDG_CONFIG_HOME/neon`, else `~/.config/neon`.\n *\n * The directory was called `neonctl` until the CLI was renamed. An existing one is still read —\n * see {@link credentialsPath} — but it is never written to, moved, or deleted.\n */\nexport const defaultDir = configDir();\n\n/**\n * Where this invocation's `credentials.json` lives.\n *\n * When `--config-dir` was left at its default, an existing file in the legacy `neonctl`\n * directory is used **in place**: an install that predates the rename keeps working, and its\n * credentials are never duplicated into a second location where one copy could go stale while\n * another tool still reads it.\n *\n * A `--config-dir` the user actually passed is used exactly as given. Falling back out of an\n * explicitly chosen directory would defeat the reason for choosing it — a CI run pointed at a\n * scratch directory must never pick up a developer's real credentials.\n */\nexport const credentialsPath = (dir: string): string =>\n\tresolveConfigFile(CREDENTIALS_FILE, dir === defaultDir ? {} : { dir }).path;\n\n/**\n * Whether a credentials file is one the CLI created, rather than a path a profile adopted.\n *\n * Anything that deletes a credential has to ask this first. A profile entry may point anywhere —\n * that is what makes adopting an existing directory a one-line edit — and a file we did not\n * create is not ours to remove.\n */\nexport const isInsideConfigDir = (\n\tconfigDirectory: string,\n\tfile: string,\n): boolean => `${resolve(file)}/`.startsWith(`${resolve(configDirectory)}/`);\n\n/**\n * Whether a credentials file is one the CLI owns, counting the legacy `neonctl` directory.\n *\n * {@link credentialsPath} deliberately reads an existing legacy file in place rather than\n * migrating it, so for a default config directory that file is ours even though it sits outside\n * `neon/`. Judging ownership on the current directory alone would call an install that predates\n * the rename \"adopted\".\n */\nexport const isOwnedCredentialPath = (\n\tconfigDirectory: string,\n\tfile: string,\n): boolean => {\n\tif (isInsideConfigDir(configDirectory, file)) return true;\n\tif (configDirectory !== defaultDir) return false;\n\tconst legacy = legacyConfigDir();\n\treturn legacy !== undefined && isInsideConfigDir(legacy, file);\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,MAAa,kBAAkB;;AAG/B,MAAa,yBAAyB;;AAatC,SAAgB,UAAU,UAA6B,CAAC,GAAW;CAClE,MAAM,WAAW,YAAY,OAAO;CACpC,IAAI,UAAU,OAAO;CACrB,OAAO,KAAK,WAAW,QAAQ,OAAO,QAAQ,GAAG,GAAG,eAAe;AACpE;;;;;AAMA,SAAgB,gBACf,UAA6B,CAAC,GACT;CACrB,IAAI,YAAY,OAAO,GAAG,OAAO,KAAA;CACjC,OAAO,KAAK,WAAW,QAAQ,OAAO,QAAQ,GAAG,GAAG,sBAAsB;AAC3E;;;;;;AAkBA,SAAgB,kBACf,UACA,UAA6B,CAAC,GACT;CACrB,MAAM,MAAM,UAAU,OAAO;CAC7B,MAAM,UAAU,QAAQ,KAAK,QAAQ;CACrC,IAAI,WAAW,OAAO,GACrB,OAAO;EAAE,MAAM;EAAS;EAAK,UAAU;EAAO,QAAQ;CAAK;CAE5D,MAAM,YAAY,gBAAgB,OAAO;CACzC,IAAI,WAAW;EACd,MAAM,SAAS,QAAQ,WAAW,QAAQ;EAC1C,IAAI,WAAW,MAAM,GACpB,OAAO;GACN,MAAM;GACN,KAAK;GACL,UAAU;GACV,QAAQ;EACT;CACF;CAEA,OAAO;EAAE,MAAM;EAAS;EAAK,UAAU;EAAO,QAAQ;CAAM;AAC7D;;AAGA,SAAS,WAAW,KAAgC;CACnD,MAAM,MAAM,SAAS,IAAI,eAAe;CACxC,IAAI,KAAK,OAAO;CAChB,MAAM,OAAO,SAAS,IAAI,IAAI,KAAK,SAAS,IAAI,WAAW;CAC3D,OAAO,OAAO,KAAK,MAAM,SAAS,IAAI;AACvC;AAEA,SAAS,YAAY,SAAgD;CACpE,MAAM,MAAM,QAAQ,OAAO,QAAQ;CACnC,OACC,SAAS,QAAQ,GAAG,KACpB,SAAS,IAAI,eAAe,KAC5B,SAAS,IAAI,kBAAkB;AAEjC;AAEA,SAAS,SAAS,OAA+C;CAChE,IAAI,OAAO,UAAU,UAAU,OAAO,KAAA;CACtC,MAAM,UAAU,MAAM,KAAK;CAC3B,OAAO,YAAY,KAAK,KAAA,IAAY;AACrC;AAEA,MAAa,mBAAmB;;;;;;;AAQhC,MAAa,aAAa,UAAU;;;;;;;;;;;;;AAcpC,MAAa,mBAAmB,QAC/B,kBAAkB,kBAAkB,QAAQ,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC"}
@@ -0,0 +1,7 @@
1
+ //#region src/_shared/profiles.d.ts
2
+
3
+ /** The implicit profile. Backed by plain `credentials.json`, with or without a profiles file. */
4
+ declare const DEFAULT_PROFILE = "DEFAULT";
5
+ //#endregion
6
+ export { DEFAULT_PROFILE };
7
+ //# sourceMappingURL=profiles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profiles.d.ts","names":[],"sources":["../../src/_shared/profiles.ts"],"mappings":";;;cAmDa,eAAA"}
@@ -0,0 +1,124 @@
1
+ import { credentialsPath, defaultDir, resolveConfigFile } from "./paths.js";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { isAbsolute, resolve } from "node:path";
4
+ //#region src/_shared/profiles.ts
5
+ /**
6
+ * # Profiles — several Neon accounts in one config directory
7
+ *
8
+ * A profile is **a pointer to a credentials file**. Nothing more. That constraint is what
9
+ * keeps the feature small: there is no mirror, no per-profile directory tree, no persistent
10
+ * "active profile" state to fall out of sync, and no migration.
11
+ *
12
+ * ```
13
+ * ~/.config/neon/
14
+ * ├── credentials.json # this IS the DEFAULT profile, not a copy of it
15
+ * ├── credentials.work.json # created by `neon auth --profile work`
16
+ * └── profiles.json # created only once a second profile exists
17
+ * ```
18
+ *
19
+ * `profiles.json` maps a name to a path, and the path may point anywhere — which is what
20
+ * makes adopting an existing directory a one-line edit rather than an import command:
21
+ *
22
+ * ```json
23
+ * {
24
+ * "version": 1,
25
+ * "profiles": {
26
+ * "DEFAULT": { "credentials": "credentials.json" },
27
+ * "work": {
28
+ * "credentials": "../neonctl-databricks/credentials.json",
29
+ * "label": "someone@example.com"
30
+ * }
31
+ * }
32
+ * }
33
+ * ```
34
+ *
35
+ * ## Selection
36
+ *
37
+ * `--profile` → `NEON_PROFILE` → `DEFAULT`. Per invocation, like `AWS_PROFILE`; there is no
38
+ * `profile use` command, so nothing persists that could disagree with what you typed.
39
+ *
40
+ * ## Compatibility
41
+ *
42
+ * An install with no `profiles.json` is already a valid `DEFAULT`-only state: `DEFAULT`
43
+ * resolves to `credentials.json` in the config directory (including an existing one in the
44
+ * legacy `neonctl` directory — see `./paths.ts`). Nothing is created until a second
45
+ * profile is, and nothing is ever moved.
46
+ */
47
+ const PROFILES_FILE = "profiles.json";
48
+ /** The implicit profile. Backed by plain `credentials.json`, with or without a profiles file. */
49
+ const DEFAULT_PROFILE = "DEFAULT";
50
+ /** Profile names become part of a filename, so keep them boring. */
51
+ const NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
52
+ /** Where `profiles.json` lives for this config directory (whether or not it exists yet). */
53
+ const profilesFilePath = (dir) => resolveConfigFile(PROFILES_FILE, dir === defaultDir ? {} : { dir }).path;
54
+ /**
55
+ * Read and classify `profiles.json` without deciding what to do about it.
56
+ *
57
+ * Entry keys and shapes are validated here rather than at each use. A key is a profile name,
58
+ * and a name that `assertValidProfileName` would reject cannot have been written by this CLI —
59
+ * it would travel into error messages as a recovery command nobody can run, and into a
60
+ * `credentials.<name>.json` filename.
61
+ */
62
+ const inspectProfiles = (dir) => {
63
+ const path = profilesFilePath(dir);
64
+ if (!existsSync(path)) return { kind: "absent" };
65
+ const broken = (why) => ({
66
+ kind: "unusable",
67
+ reason: `${path} could not be read as a profiles file: ${why}`
68
+ });
69
+ let contents;
70
+ try {
71
+ contents = readFileSync(path, "utf8");
72
+ } catch (err) {
73
+ const code = err.code;
74
+ return broken(code ? `reading it failed with ${code}` : "reading it failed");
75
+ }
76
+ let parsed;
77
+ try {
78
+ parsed = JSON.parse(contents);
79
+ } catch {
80
+ return broken("it is not valid JSON");
81
+ }
82
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return broken("it does not contain an object");
83
+ const profiles = parsed.profiles;
84
+ if (profiles === null || typeof profiles !== "object" || Array.isArray(profiles)) return broken("it has no `profiles` object");
85
+ for (const [name, entry] of Object.entries(profiles)) {
86
+ if (!NAME_PATTERN.test(name)) return broken(`"${name}" is not a valid profile name`);
87
+ if (entry === null || typeof entry !== "object" || typeof entry.credentials !== "string" || entry.credentials.trim() === "") return broken(`profile "${name}" has no \`credentials\` path`);
88
+ }
89
+ return {
90
+ kind: "ok",
91
+ file: {
92
+ version: 1,
93
+ profiles
94
+ }
95
+ };
96
+ };
97
+ /** Resolve a profile to an absolute credentials path. Throws when a named profile is unknown. */
98
+ const resolveProfile = (dir, name) => {
99
+ const read = inspectProfiles(dir);
100
+ if (read.kind === "unusable" && name !== "DEFAULT") throw new Error(`${read.reason}. Fix or delete the file — every named profile is defined in it.`);
101
+ const file = read.kind === "ok" ? read.file : null;
102
+ const entry = file?.profiles[name];
103
+ if (entry) return {
104
+ name,
105
+ credentialsPath: resolveEntryPath(dir, entry.credentials),
106
+ ...entry.label ? { label: entry.label } : {},
107
+ ...entry.userId ? { userId: entry.userId } : {},
108
+ declared: true
109
+ };
110
+ if (name === "DEFAULT") return {
111
+ name,
112
+ credentialsPath: credentialsPath(dir),
113
+ declared: false
114
+ };
115
+ const known = file ? Object.keys(file.profiles).join(", ") : DEFAULT_PROFILE;
116
+ throw new Error(`Unknown profile "${name}". Known profiles: ${known}. Create it with \`neon profile create ${name}\`.`);
117
+ };
118
+ const resolveEntryPath = (dir, entry) => isAbsolute(entry) ? entry : resolve(profilesDir(dir), entry);
119
+ /** `profiles.json` may sit in the legacy directory, so entries resolve against its own dir. */
120
+ const profilesDir = (dir) => resolve(profilesFilePath(dir), "..");
121
+ //#endregion
122
+ export { DEFAULT_PROFILE, PROFILES_FILE, inspectProfiles, profilesFilePath, resolveProfile };
123
+
124
+ //# sourceMappingURL=profiles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profiles.js","names":[],"sources":["../../src/_shared/profiles.ts"],"sourcesContent":["/**\n * # Profiles — several Neon accounts in one config directory\n *\n * A profile is **a pointer to a credentials file**. Nothing more. That constraint is what\n * keeps the feature small: there is no mirror, no per-profile directory tree, no persistent\n * \"active profile\" state to fall out of sync, and no migration.\n *\n * ```\n * ~/.config/neon/\n * ├── credentials.json # this IS the DEFAULT profile, not a copy of it\n * ├── credentials.work.json # created by `neon auth --profile work`\n * └── profiles.json # created only once a second profile exists\n * ```\n *\n * `profiles.json` maps a name to a path, and the path may point anywhere — which is what\n * makes adopting an existing directory a one-line edit rather than an import command:\n *\n * ```json\n * {\n * \"version\": 1,\n * \"profiles\": {\n * \"DEFAULT\": { \"credentials\": \"credentials.json\" },\n * \"work\": {\n * \"credentials\": \"../neonctl-databricks/credentials.json\",\n * \"label\": \"someone@example.com\"\n * }\n * }\n * }\n * ```\n *\n * ## Selection\n *\n * `--profile` → `NEON_PROFILE` → `DEFAULT`. Per invocation, like `AWS_PROFILE`; there is no\n * `profile use` command, so nothing persists that could disagree with what you typed.\n *\n * ## Compatibility\n *\n * An install with no `profiles.json` is already a valid `DEFAULT`-only state: `DEFAULT`\n * resolves to `credentials.json` in the config directory (including an existing one in the\n * legacy `neonctl` directory — see `./paths.ts`). Nothing is created until a second\n * profile is, and nothing is ever moved.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { isAbsolute, relative, resolve } from \"node:path\";\nimport { credentialsPath, defaultDir, resolveConfigFile } from \"./paths.js\";\nimport { writeSecretFile } from \"./secure_file.js\";\n\nexport const PROFILES_FILE = \"profiles.json\";\n\n/** The implicit profile. Backed by plain `credentials.json`, with or without a profiles file. */\nexport const DEFAULT_PROFILE = \"DEFAULT\";\n\n/** Profile names become part of a filename, so keep them boring. */\nconst NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\n\nexport type ProfileEntry = {\n\t/** Path to the credentials file, relative to `profiles.json` or absolute. */\n\tcredentials: string;\n\t/** Account email, captured at login. Display only. */\n\tlabel?: string;\n\t/** Neon user id, captured at login. Display only. */\n\tuserId?: string;\n};\n\nexport type ProfilesFile = {\n\tversion: 1;\n\tprofiles: Record<string, ProfileEntry>;\n};\n\nexport type ResolvedProfile = {\n\tname: string;\n\t/** Absolute path to this profile's credentials file. */\n\tcredentialsPath: string;\n\tlabel?: string;\n\tuserId?: string;\n\t/** True when the profile comes from `profiles.json` rather than the implicit default. */\n\tdeclared: boolean;\n};\n\n/** Which profile this invocation should use: `--profile` → `NEON_PROFILE` → `DEFAULT`. */\nexport const selectProfileName = (\n\tflag?: string,\n\tenv: NodeJS.ProcessEnv = process.env,\n): string => nonEmpty(flag) ?? nonEmpty(env.NEON_PROFILE) ?? DEFAULT_PROFILE;\n\nexport const assertValidProfileName = (name: string): void => {\n\tif (!NAME_PATTERN.test(name)) {\n\t\tthrow new Error(\n\t\t\t`Invalid profile name \"${name}\". Use letters, digits, dot, dash or underscore, starting with a letter or digit.`,\n\t\t);\n\t}\n};\n\n/** Where `profiles.json` lives for this config directory (whether or not it exists yet). */\nexport const profilesFilePath = (dir: string): string =>\n\tresolveConfigFile(PROFILES_FILE, dir === defaultDir ? {} : { dir }).path;\n\n/** What is at `profiles.json`: nothing, something readable, or something broken. */\nexport type ProfilesRead =\n\t| { kind: \"ok\"; file: ProfilesFile }\n\t| { kind: \"absent\" }\n\t/** The file is there and cannot be trusted. `reason` names the file and is safe to print. */\n\t| { kind: \"unusable\"; reason: string };\n\n/**\n * Read and classify `profiles.json` without deciding what to do about it.\n *\n * Entry keys and shapes are validated here rather than at each use. A key is a profile name,\n * and a name that `assertValidProfileName` would reject cannot have been written by this CLI —\n * it would travel into error messages as a recovery command nobody can run, and into a\n * `credentials.<name>.json` filename.\n */\nexport const inspectProfiles = (dir: string): ProfilesRead => {\n\tconst path = profilesFilePath(dir);\n\tif (!existsSync(path)) return { kind: \"absent\" };\n\tconst broken = (why: string): ProfilesRead => ({\n\t\tkind: \"unusable\",\n\t\treason: `${path} could not be read as a profiles file: ${why}`,\n\t});\n\t// Reading and parsing are separate failures with separate answers. Sharing one catch\n\t// reported `EACCES` as \"not valid JSON\", which sends the user to edit a file that is\n\t// perfectly valid and that they cannot open.\n\tlet contents: string;\n\ttry {\n\t\tcontents = readFileSync(path, \"utf8\");\n\t} catch (err) {\n\t\tconst code = (err as NodeJS.ErrnoException).code;\n\t\treturn broken(\n\t\t\tcode ? `reading it failed with ${code}` : \"reading it failed\",\n\t\t);\n\t}\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(contents);\n\t} catch {\n\t\treturn broken(\"it is not valid JSON\");\n\t}\n\tif (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed))\n\t\treturn broken(\"it does not contain an object\");\n\tconst profiles = (parsed as ProfilesFile).profiles;\n\tif (\n\t\tprofiles === null ||\n\t\ttypeof profiles !== \"object\" ||\n\t\tArray.isArray(profiles)\n\t)\n\t\treturn broken(\"it has no `profiles` object\");\n\tfor (const [name, entry] of Object.entries(profiles)) {\n\t\tif (!NAME_PATTERN.test(name))\n\t\t\treturn broken(`\"${name}\" is not a valid profile name`);\n\t\tif (\n\t\t\tentry === null ||\n\t\t\ttypeof entry !== \"object\" ||\n\t\t\ttypeof entry.credentials !== \"string\" ||\n\t\t\tentry.credentials.trim() === \"\"\n\t\t) {\n\t\t\treturn broken(`profile \"${name}\" has no \\`credentials\\` path`);\n\t\t}\n\t}\n\treturn { kind: \"ok\", file: { version: 1, profiles } };\n};\n\n/**\n * Read `profiles.json`, or `null` when there is nothing usable there.\n *\n * A malformed file is reported through `onWarn` and treated as absent, because for a *read* the\n * worst case is a named profile turning up missing, which is recoverable — whereas throwing\n * would lock the user out of `neon auth` itself. Writing is the opposite: see\n * {@link upsertProfile}, which refuses rather than rebuilding a file it cannot read.\n */\nexport const readProfiles = (\n\tdir: string,\n\t/** Called with the reason a profiles file was ignored. The consumer owns how it reports. */\n\tonWarn: (message: string) => void = () => {},\n): ProfilesFile | null => {\n\tconst read = inspectProfiles(dir);\n\tif (read.kind === \"ok\") return read.file;\n\tif (read.kind === \"unusable\") onWarn(read.reason);\n\treturn null;\n};\n\n/**\n * Refuse to act on a named profile when the file that defines it cannot be read.\n *\n * Call this **before** anything that writes a credential, opens a browser, or spends an API\n * call. {@link upsertProfile} refuses too, but it runs last: by then `create` has already\n * overwritten `credentials.<name>.json` and revoked the key it replaced, and `neon auth\n * --profile` has already signed in over it — a refusal that arrives after the destruction it\n * exists to prevent. The path resolution itself is the unsound part, since with the metadata\n * unreadable the conventional filename is a guess about which account that file belongs to.\n *\n * `DEFAULT` is exempt: it is defined by the absence of metadata rather than by an entry, so\n * signing in normally must keep working while a broken `profiles.json` is repaired.\n */\nexport const assertProfilesUsable = (dir: string, name: string): void => {\n\tif (name === DEFAULT_PROFILE) return;\n\tconst read = inspectProfiles(dir);\n\tif (read.kind === \"unusable\") {\n\t\tthrow new Error(\n\t\t\t`${read.reason}. Fix or delete the file before working with profile \"${name}\" — it is the only record of where each account's credentials live.`,\n\t\t);\n\t}\n};\n\n/** Resolve a profile to an absolute credentials path. Throws when a named profile is unknown. */\nexport const resolveProfile = (dir: string, name: string): ResolvedProfile => {\n\tconst read = inspectProfiles(dir);\n\t// A broken file must not be reported as `Unknown profile \"work\"`. That names the wrong\n\t// problem, and the user goes looking for a profile they can see in the file in front of them.\n\tif (read.kind === \"unusable\" && name !== DEFAULT_PROFILE) {\n\t\tthrow new Error(\n\t\t\t`${read.reason}. Fix or delete the file — every named profile is defined in it.`,\n\t\t);\n\t}\n\tconst file = read.kind === \"ok\" ? read.file : null;\n\tconst entry = file?.profiles[name];\n\n\tif (entry) {\n\t\treturn {\n\t\t\tname,\n\t\t\tcredentialsPath: resolveEntryPath(dir, entry.credentials),\n\t\t\t...(entry.label ? { label: entry.label } : {}),\n\t\t\t...(entry.userId ? { userId: entry.userId } : {}),\n\t\t\tdeclared: true,\n\t\t};\n\t}\n\n\t// DEFAULT works with no profiles.json at all, and keeps working when one exists but\n\t// doesn't mention it — that is the pre-profiles behaviour, unchanged.\n\tif (name === DEFAULT_PROFILE) {\n\t\treturn {\n\t\t\tname,\n\t\t\tcredentialsPath: credentialsPath(dir),\n\t\t\tdeclared: false,\n\t\t};\n\t}\n\n\tconst known = file\n\t\t? Object.keys(file.profiles).join(\", \")\n\t\t: DEFAULT_PROFILE;\n\tthrow new Error(\n\t\t`Unknown profile \"${name}\". Known profiles: ${known}. Create it with \\`neon profile create ${name}\\`.`,\n\t);\n};\n\n/** Default location for a new named profile's credentials file. */\nexport const newProfileCredentialsPath = (dir: string, name: string): string =>\n\tresolve(dir, `credentials.${name}.json`);\n\n/**\n * Record a profile, creating `profiles.json` if this is the first named one.\n *\n * When the file is created, `DEFAULT` is written explicitly and pointed at wherever\n * `credentials.json` actually is. That matters for an install predating the directory\n * rename: `profiles.json` is created in `neon/` while the credentials are still in\n * `neonctl/`, so `DEFAULT` is recorded as `../neonctl/credentials.json` rather than a\n * relative name that would resolve to a file that isn't there.\n */\nexport const upsertProfile = (\n\tdir: string,\n\tname: string,\n\tentry: { credentials: string; label?: string; userId?: string },\n): void => {\n\tassertValidProfileName(name);\n\tconst path = profilesFilePath(dir);\n\tconst read = inspectProfiles(dir);\n\t// Refusing is the point. Treating a broken file as absent here rebuilt it from a single\n\t// `DEFAULT` entry and dropped every named profile in it — silent data loss, in the file\n\t// that is the only record of where each account's credentials live. The credentials\n\t// themselves survive, so fixing the file by hand recovers everything.\n\tif (read.kind === \"unusable\") {\n\t\tthrow new Error(\n\t\t\t`${read.reason}. Refusing to rewrite it, because doing so would discard the profiles it defines. Fix or delete the file, then re-run.`,\n\t\t);\n\t}\n\tconst file =\n\t\tread.kind === \"ok\"\n\t\t\t? read.file\n\t\t\t: {\n\t\t\t\t\tversion: 1 as const,\n\t\t\t\t\tprofiles: {\n\t\t\t\t\t\t[DEFAULT_PROFILE]: {\n\t\t\t\t\t\t\tcredentials: relativeToProfiles(\n\t\t\t\t\t\t\t\tpath,\n\t\t\t\t\t\t\t\tcredentialsPath(dir),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t};\n\n\tfile.profiles[name] = {\n\t\tcredentials: relativeToProfiles(path, entry.credentials),\n\t\t...(entry.label ? { label: entry.label } : {}),\n\t\t...(entry.userId ? { userId: entry.userId } : {}),\n\t};\n\n\twriteProfiles(path, file);\n};\n\n/** Remove an entry. Returns false when it wasn't there. */\nexport const removeProfileEntry = (dir: string, name: string): boolean => {\n\tconst path = profilesFilePath(dir);\n\tconst file = readProfiles(dir);\n\tif (!file?.profiles[name]) return false;\n\tdelete file.profiles[name];\n\twriteProfiles(path, file);\n\treturn true;\n};\n\n/**\n * True when only `DEFAULT` is left, so `profiles.json` no longer earns its place. Mirrors\n * lazy creation: a single-account install has no profiles file, before or after.\n */\nexport const onlyDefaultRemains = (file: ProfilesFile): boolean => {\n\tconst names = Object.keys(file.profiles);\n\treturn (\n\t\tnames.length === 0 ||\n\t\t(names.length === 1 && names[0] === DEFAULT_PROFILE)\n\t);\n};\n\nexport const listProfiles = (dir: string): ResolvedProfile[] => {\n\tconst read = inspectProfiles(dir);\n\t// Listing is the command run to find out what is there, so a broken file is the answer\n\t// rather than an obstacle. Showing only `DEFAULT` would state, as fact, that the profiles\n\t// in that file do not exist.\n\tif (read.kind === \"unusable\") {\n\t\tthrow new Error(\n\t\t\t`${read.reason}. Fix or delete the file — every named profile is defined in it.`,\n\t\t);\n\t}\n\tconst file = read.kind === \"ok\" ? read.file : null;\n\tif (!file) return [resolveProfile(dir, DEFAULT_PROFILE)];\n\tconst names = Object.keys(file.profiles);\n\tif (!names.includes(DEFAULT_PROFILE)) names.unshift(DEFAULT_PROFILE);\n\treturn names.map((name) => resolveProfile(dir, name));\n};\n\nconst writeProfiles = (path: string, file: ProfilesFile): void => {\n\twriteSecretFile(path, `${JSON.stringify(file, null, 2)}\\n`);\n};\n\nconst resolveEntryPath = (dir: string, entry: string): string =>\n\tisAbsolute(entry) ? entry : resolve(profilesDir(dir), entry);\n\n/** `profiles.json` may sit in the legacy directory, so entries resolve against its own dir. */\nconst profilesDir = (dir: string): string =>\n\tresolve(profilesFilePath(dir), \"..\");\n\n/** Keep entries relative when they sit near `profiles.json`; absolute paths stay absolute. */\nconst relativeToProfiles = (profilesPath: string, target: string): string => {\n\tconst rel = relative(resolve(profilesPath, \"..\"), target);\n\treturn rel && !isAbsolute(rel) ? rel : target;\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,MAAa,gBAAgB;;AAG7B,MAAa,kBAAkB;;AAG/B,MAAM,eAAe;;AAyCrB,MAAa,oBAAoB,QAChC,kBAAkB,eAAe,QAAQ,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;;;;;;;;;AAiBrE,MAAa,mBAAmB,QAA8B;CAC7D,MAAM,OAAO,iBAAiB,GAAG;CACjC,IAAI,CAAC,WAAW,IAAI,GAAG,OAAO,EAAE,MAAM,SAAS;CAC/C,MAAM,UAAU,SAA+B;EAC9C,MAAM;EACN,QAAQ,GAAG,KAAK,yCAAyC;CAC1D;CAIA,IAAI;CACJ,IAAI;EACH,WAAW,aAAa,MAAM,MAAM;CACrC,SAAS,KAAK;EACb,MAAM,OAAQ,IAA8B;EAC5C,OAAO,OACN,OAAO,0BAA0B,SAAS,mBAC3C;CACD;CACA,IAAI;CACJ,IAAI;EACH,SAAS,KAAK,MAAM,QAAQ;CAC7B,QAAQ;EACP,OAAO,OAAO,sBAAsB;CACrC;CACA,IAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GACxE,OAAO,OAAO,+BAA+B;CAC9C,MAAM,WAAY,OAAwB;CAC1C,IACC,aAAa,QACb,OAAO,aAAa,YACpB,MAAM,QAAQ,QAAQ,GAEtB,OAAO,OAAO,6BAA6B;CAC5C,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,QAAQ,GAAG;EACrD,IAAI,CAAC,aAAa,KAAK,IAAI,GAC1B,OAAO,OAAO,IAAI,KAAK,8BAA8B;EACtD,IACC,UAAU,QACV,OAAO,UAAU,YACjB,OAAO,MAAM,gBAAgB,YAC7B,MAAM,YAAY,KAAK,MAAM,IAE7B,OAAO,OAAO,YAAY,KAAK,8BAA8B;CAE/D;CACA,OAAO;EAAE,MAAM;EAAM,MAAM;GAAE,SAAS;GAAG;EAAS;CAAE;AACrD;;AA6CA,MAAa,kBAAkB,KAAa,SAAkC;CAC7E,MAAM,OAAO,gBAAgB,GAAG;CAGhC,IAAI,KAAK,SAAS,cAAc,SAAA,WAC/B,MAAM,IAAI,MACT,GAAG,KAAK,OAAO,iEAChB;CAED,MAAM,OAAO,KAAK,SAAS,OAAO,KAAK,OAAO;CAC9C,MAAM,QAAQ,MAAM,SAAS;CAE7B,IAAI,OACH,OAAO;EACN;EACA,iBAAiB,iBAAiB,KAAK,MAAM,WAAW;EACxD,GAAI,MAAM,QAAQ,EAAE,OAAO,MAAM,MAAM,IAAI,CAAC;EAC5C,GAAI,MAAM,SAAS,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;EAC/C,UAAU;CACX;CAKD,IAAI,SAAA,WACH,OAAO;EACN;EACA,iBAAiB,gBAAgB,GAAG;EACpC,UAAU;CACX;CAGD,MAAM,QAAQ,OACX,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,KAAK,IAAI,IACpC;CACH,MAAM,IAAI,MACT,oBAAoB,KAAK,qBAAqB,MAAM,yCAAyC,KAAK,IACnG;AACD;AAmGA,MAAM,oBAAoB,KAAa,UACtC,WAAW,KAAK,IAAI,QAAQ,QAAQ,YAAY,GAAG,GAAG,KAAK;;AAG5D,MAAM,eAAe,QACpB,QAAQ,iBAAiB,GAAG,GAAG,IAAI"}
package/dist/cli.js CHANGED
@@ -22,6 +22,9 @@ const argv = yargs(hideBin(process.argv)).scriptName("neon-env").usage("$0 <comm
22
22
  }).option("api-key", {
23
23
  type: "string",
24
24
  describe: "Neon API key (defaults to NEON_API_KEY)"
25
+ }).option("profile", {
26
+ type: "string",
27
+ describe: "Neon CLI profile whose stored credential to use (defaults to NEON_PROFILE, else DEFAULT)"
25
28
  })).command("export", "Print the branch's Neon env vars (from your neon.ts policy) to stdout, as dotenv lines or JSON. Useful for piping into other env tools, e.g. `neon-env export --format json`.", (y) => y.option("format", {
26
29
  choices: ["dotenv", "json"],
27
30
  default: "dotenv",
@@ -38,6 +41,9 @@ const argv = yargs(hideBin(process.argv)).scriptName("neon-env").usage("$0 <comm
38
41
  }).option("api-key", {
39
42
  type: "string",
40
43
  describe: "Neon API key (defaults to NEON_API_KEY)"
44
+ }).option("profile", {
45
+ type: "string",
46
+ describe: "Neon CLI profile whose stored credential to use (defaults to NEON_PROFILE, else DEFAULT)"
41
47
  })).demandCommand(1, "Run `neon-env --help` to see the available commands.").strict().help().version(pkgVersion).parseSync();
42
48
  const command = String(argv._[0]);
43
49
  const cwd = process.cwd();
@@ -49,7 +55,8 @@ switch (command) {
49
55
  ...typeof argv.config === "string" ? { configPath: argv.config } : {},
50
56
  ...typeof argv["project-id"] === "string" ? { projectId: argv["project-id"] } : {},
51
57
  ...typeof argv.branch === "string" ? { branch: argv.branch } : {},
52
- ...typeof argv["api-key"] === "string" ? { apiKey: argv["api-key"] } : {}
58
+ ...typeof argv["api-key"] === "string" ? { apiKey: argv["api-key"] } : {},
59
+ ...typeof argv.profile === "string" ? { profile: argv.profile } : {}
53
60
  }, { cwd });
54
61
  break;
55
62
  case "export":
@@ -58,7 +65,8 @@ switch (command) {
58
65
  ...typeof argv.config === "string" ? { configPath: argv.config } : {},
59
66
  ...typeof argv["project-id"] === "string" ? { projectId: argv["project-id"] } : {},
60
67
  ...typeof argv.branch === "string" ? { branch: argv.branch } : {},
61
- ...typeof argv["api-key"] === "string" ? { apiKey: argv["api-key"] } : {}
68
+ ...typeof argv["api-key"] === "string" ? { apiKey: argv["api-key"] } : {},
69
+ ...typeof argv.profile === "string" ? { profile: argv.profile } : {}
62
70
  }, { cwd });
63
71
  break;
64
72
  default: result = {
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { readFileSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport yargs from \"yargs\";\nimport { hideBin } from \"yargs/helpers\";\nimport {\n\ttype CommandResult,\n\trunEnvExport,\n\trunEnvRun,\n} from \"./lib/cli/commands.js\";\n\nconst pkgVersion = readPackageVersion();\n\nconst argv = yargs(hideBin(process.argv))\n\t.scriptName(\"neon-env\")\n\t.usage(\"$0 <command> [options]\")\n\t.parserConfiguration({ \"populate--\": true })\n\t.option(\"debug\", {\n\t\ttype: \"boolean\",\n\t\tdefault: false,\n\t\tdescribe:\n\t\t\t\"Print stack traces and structured error details when something fails\",\n\t})\n\t.command(\n\t\t\"run\",\n\t\t\"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`.\",\n\t\t(y) =>\n\t\t\ty\n\t\t\t\t.option(\"config\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tdescribe:\n\t\t\t\t\t\t\"Path to neon.ts (defaults to walking up from cwd)\",\n\t\t\t\t})\n\t\t\t\t.option(\"project-id\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tdescribe: \"Override the .neon/project.json projectId\",\n\t\t\t\t})\n\t\t\t\t.option(\"branch\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tdescribe:\n\t\t\t\t\t\t\"Branch name or id to target (overrides .neon / NEON_BRANCH / NEON_BRANCH_ID)\",\n\t\t\t\t})\n\t\t\t\t.option(\"api-key\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tdescribe: \"Neon API key (defaults to NEON_API_KEY)\",\n\t\t\t\t}),\n\t)\n\t.command(\n\t\t\"export\",\n\t\t\"Print the branch's Neon env vars (from your neon.ts policy) to stdout, as dotenv lines or JSON. Useful for piping into other env tools, e.g. `neon-env export --format json`.\",\n\t\t(y) =>\n\t\t\ty\n\t\t\t\t.option(\"format\", {\n\t\t\t\t\tchoices: [\"dotenv\", \"json\"] as const,\n\t\t\t\t\tdefault: \"dotenv\",\n\t\t\t\t\tdescribe: \"Output format: dotenv (KEY=value lines) or json\",\n\t\t\t\t})\n\t\t\t\t.option(\"config\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tdescribe:\n\t\t\t\t\t\t\"Path to neon.ts (defaults to walking up from cwd)\",\n\t\t\t\t})\n\t\t\t\t.option(\"project-id\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tdescribe: \"Override the .neon/project.json projectId\",\n\t\t\t\t})\n\t\t\t\t.option(\"branch\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tdescribe:\n\t\t\t\t\t\t\"Branch name or id to target (overrides .neon / NEON_BRANCH / NEON_BRANCH_ID)\",\n\t\t\t\t})\n\t\t\t\t.option(\"api-key\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tdescribe: \"Neon API key (defaults to NEON_API_KEY)\",\n\t\t\t\t}),\n\t)\n\t.demandCommand(1, \"Run `neon-env --help` to see the available commands.\")\n\t.strict()\n\t.help()\n\t.version(pkgVersion)\n\t.parseSync();\n\nconst command = String(argv._[0]);\nconst cwd = process.cwd();\n\nlet result: CommandResult;\nswitch (command) {\n\tcase \"run\": {\n\t\tconst passthrough = Array.isArray(argv[\"--\"])\n\t\t\t? argv[\"--\"].map(String)\n\t\t\t: [];\n\t\tresult = await runEnvRun(\n\t\t\t{\n\t\t\t\tcommand: passthrough,\n\t\t\t\t...(typeof argv.config === \"string\"\n\t\t\t\t\t? { configPath: argv.config }\n\t\t\t\t\t: {}),\n\t\t\t\t...(typeof argv[\"project-id\"] === \"string\"\n\t\t\t\t\t? { projectId: argv[\"project-id\"] }\n\t\t\t\t\t: {}),\n\t\t\t\t...(typeof argv.branch === \"string\"\n\t\t\t\t\t? { branch: argv.branch }\n\t\t\t\t\t: {}),\n\t\t\t\t...(typeof argv[\"api-key\"] === \"string\"\n\t\t\t\t\t? { apiKey: argv[\"api-key\"] }\n\t\t\t\t\t: {}),\n\t\t\t},\n\t\t\t{ cwd },\n\t\t);\n\t\tbreak;\n\t}\n\tcase \"export\": {\n\t\tresult = await runEnvExport(\n\t\t\t{\n\t\t\t\tformat: argv.format === \"json\" ? \"json\" : \"dotenv\",\n\t\t\t\t...(typeof argv.config === \"string\"\n\t\t\t\t\t? { configPath: argv.config }\n\t\t\t\t\t: {}),\n\t\t\t\t...(typeof argv[\"project-id\"] === \"string\"\n\t\t\t\t\t? { projectId: argv[\"project-id\"] }\n\t\t\t\t\t: {}),\n\t\t\t\t...(typeof argv.branch === \"string\"\n\t\t\t\t\t? { branch: argv.branch }\n\t\t\t\t\t: {}),\n\t\t\t\t...(typeof argv[\"api-key\"] === \"string\"\n\t\t\t\t\t? { apiKey: argv[\"api-key\"] }\n\t\t\t\t\t: {}),\n\t\t\t},\n\t\t\t{ cwd },\n\t\t);\n\t\tbreak;\n\t}\n\tdefault:\n\t\tresult = {\n\t\t\texitCode: 1,\n\t\t\tstdout: \"\",\n\t\t\tstderr: `Unknown command: ${command}\\n`,\n\t\t};\n}\n\nif (result.stdout) process.stdout.write(result.stdout);\nif (result.stderr) process.stderr.write(result.stderr);\nif (argv.debug && result.exitCode !== 0 && result.debugInfo) {\n\tprocess.stderr.write(`\\n--- debug ---\\n${result.debugInfo}\\n`);\n}\nprocess.exit(result.exitCode);\n\nfunction readPackageVersion(): string {\n\t// The built CLI lives at `dist/cli.js`, so `package.json` is one directory up. When\n\t// running from source (tsx, vitest), the file lives at `src/cli.ts` and `package.json`\n\t// is again one directory up. Single resolution covers both layouts.\n\ttry {\n\t\tconst pkgUrl = new URL(\"../package.json\", import.meta.url);\n\t\tconst raw = readFileSync(fileURLToPath(pkgUrl), \"utf-8\");\n\t\tconst parsed = JSON.parse(raw) as { version?: unknown };\n\t\treturn typeof parsed.version === \"string\" ? parsed.version : \"0.0.0\";\n\t} catch {\n\t\treturn \"0.0.0\";\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { readFileSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport yargs from \"yargs\";\nimport { hideBin } from \"yargs/helpers\";\nimport {\n\ttype CommandResult,\n\trunEnvExport,\n\trunEnvRun,\n} from \"./lib/cli/commands.js\";\n\nconst pkgVersion = readPackageVersion();\n\nconst argv = yargs(hideBin(process.argv))\n\t.scriptName(\"neon-env\")\n\t.usage(\"$0 <command> [options]\")\n\t.parserConfiguration({ \"populate--\": true })\n\t.option(\"debug\", {\n\t\ttype: \"boolean\",\n\t\tdefault: false,\n\t\tdescribe:\n\t\t\t\"Print stack traces and structured error details when something fails\",\n\t})\n\t.command(\n\t\t\"run\",\n\t\t\"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`.\",\n\t\t(y) =>\n\t\t\ty\n\t\t\t\t.option(\"config\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tdescribe:\n\t\t\t\t\t\t\"Path to neon.ts (defaults to walking up from cwd)\",\n\t\t\t\t})\n\t\t\t\t.option(\"project-id\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tdescribe: \"Override the .neon/project.json projectId\",\n\t\t\t\t})\n\t\t\t\t.option(\"branch\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tdescribe:\n\t\t\t\t\t\t\"Branch name or id to target (overrides .neon / NEON_BRANCH / NEON_BRANCH_ID)\",\n\t\t\t\t})\n\t\t\t\t.option(\"api-key\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tdescribe: \"Neon API key (defaults to NEON_API_KEY)\",\n\t\t\t\t})\n\t\t\t\t.option(\"profile\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tdescribe:\n\t\t\t\t\t\t\"Neon CLI profile whose stored credential to use (defaults to NEON_PROFILE, else DEFAULT)\",\n\t\t\t\t}),\n\t)\n\t.command(\n\t\t\"export\",\n\t\t\"Print the branch's Neon env vars (from your neon.ts policy) to stdout, as dotenv lines or JSON. Useful for piping into other env tools, e.g. `neon-env export --format json`.\",\n\t\t(y) =>\n\t\t\ty\n\t\t\t\t.option(\"format\", {\n\t\t\t\t\tchoices: [\"dotenv\", \"json\"] as const,\n\t\t\t\t\tdefault: \"dotenv\",\n\t\t\t\t\tdescribe: \"Output format: dotenv (KEY=value lines) or json\",\n\t\t\t\t})\n\t\t\t\t.option(\"config\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tdescribe:\n\t\t\t\t\t\t\"Path to neon.ts (defaults to walking up from cwd)\",\n\t\t\t\t})\n\t\t\t\t.option(\"project-id\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tdescribe: \"Override the .neon/project.json projectId\",\n\t\t\t\t})\n\t\t\t\t.option(\"branch\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tdescribe:\n\t\t\t\t\t\t\"Branch name or id to target (overrides .neon / NEON_BRANCH / NEON_BRANCH_ID)\",\n\t\t\t\t})\n\t\t\t\t.option(\"api-key\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tdescribe: \"Neon API key (defaults to NEON_API_KEY)\",\n\t\t\t\t})\n\t\t\t\t.option(\"profile\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tdescribe:\n\t\t\t\t\t\t\"Neon CLI profile whose stored credential to use (defaults to NEON_PROFILE, else DEFAULT)\",\n\t\t\t\t}),\n\t)\n\t.demandCommand(1, \"Run `neon-env --help` to see the available commands.\")\n\t.strict()\n\t.help()\n\t.version(pkgVersion)\n\t.parseSync();\n\nconst command = String(argv._[0]);\nconst cwd = process.cwd();\n\nlet result: CommandResult;\nswitch (command) {\n\tcase \"run\": {\n\t\tconst passthrough = Array.isArray(argv[\"--\"])\n\t\t\t? argv[\"--\"].map(String)\n\t\t\t: [];\n\t\tresult = await runEnvRun(\n\t\t\t{\n\t\t\t\tcommand: passthrough,\n\t\t\t\t...(typeof argv.config === \"string\"\n\t\t\t\t\t? { configPath: argv.config }\n\t\t\t\t\t: {}),\n\t\t\t\t...(typeof argv[\"project-id\"] === \"string\"\n\t\t\t\t\t? { projectId: argv[\"project-id\"] }\n\t\t\t\t\t: {}),\n\t\t\t\t...(typeof argv.branch === \"string\"\n\t\t\t\t\t? { branch: argv.branch }\n\t\t\t\t\t: {}),\n\t\t\t\t...(typeof argv[\"api-key\"] === \"string\"\n\t\t\t\t\t? { apiKey: argv[\"api-key\"] }\n\t\t\t\t\t: {}),\n\t\t\t\t...(typeof argv.profile === \"string\"\n\t\t\t\t\t? { profile: argv.profile }\n\t\t\t\t\t: {}),\n\t\t\t},\n\t\t\t{ cwd },\n\t\t);\n\t\tbreak;\n\t}\n\tcase \"export\": {\n\t\tresult = await runEnvExport(\n\t\t\t{\n\t\t\t\tformat: argv.format === \"json\" ? \"json\" : \"dotenv\",\n\t\t\t\t...(typeof argv.config === \"string\"\n\t\t\t\t\t? { configPath: argv.config }\n\t\t\t\t\t: {}),\n\t\t\t\t...(typeof argv[\"project-id\"] === \"string\"\n\t\t\t\t\t? { projectId: argv[\"project-id\"] }\n\t\t\t\t\t: {}),\n\t\t\t\t...(typeof argv.branch === \"string\"\n\t\t\t\t\t? { branch: argv.branch }\n\t\t\t\t\t: {}),\n\t\t\t\t...(typeof argv[\"api-key\"] === \"string\"\n\t\t\t\t\t? { apiKey: argv[\"api-key\"] }\n\t\t\t\t\t: {}),\n\t\t\t\t...(typeof argv.profile === \"string\"\n\t\t\t\t\t? { profile: argv.profile }\n\t\t\t\t\t: {}),\n\t\t\t},\n\t\t\t{ cwd },\n\t\t);\n\t\tbreak;\n\t}\n\tdefault:\n\t\tresult = {\n\t\t\texitCode: 1,\n\t\t\tstdout: \"\",\n\t\t\tstderr: `Unknown command: ${command}\\n`,\n\t\t};\n}\n\nif (result.stdout) process.stdout.write(result.stdout);\nif (result.stderr) process.stderr.write(result.stderr);\nif (argv.debug && result.exitCode !== 0 && result.debugInfo) {\n\tprocess.stderr.write(`\\n--- debug ---\\n${result.debugInfo}\\n`);\n}\nprocess.exit(result.exitCode);\n\nfunction readPackageVersion(): string {\n\t// The built CLI lives at `dist/cli.js`, so `package.json` is one directory up. When\n\t// running from source (tsx, vitest), the file lives at `src/cli.ts` and `package.json`\n\t// is again one directory up. Single resolution covers both layouts.\n\ttry {\n\t\tconst pkgUrl = new URL(\"../package.json\", import.meta.url);\n\t\tconst raw = readFileSync(fileURLToPath(pkgUrl), \"utf-8\");\n\t\tconst parsed = JSON.parse(raw) as { version?: unknown };\n\t\treturn typeof parsed.version === \"string\" ? parsed.version : \"0.0.0\";\n\t} catch {\n\t\treturn \"0.0.0\";\n\t}\n}\n"],"mappings":"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"}
@@ -38,6 +38,8 @@ interface EnvResolveOptions {
38
38
  projectId?: string;
39
39
  branch?: string;
40
40
  apiKey?: string;
41
+ /** Neon CLI profile whose stored credential to use. `--profile`, else `NEON_PROFILE`. */
42
+ profile?: string;
41
43
  }
42
44
  interface EnvRunCommandOptions extends EnvResolveOptions {
43
45
  /** The user command to spawn (after `--`). The first element is the executable. */
@@ -1 +1 @@
1
- {"version":3,"file":"commands.d.ts","names":[],"sources":["../../../src/lib/cli/commands.ts"],"mappings":";;;;;;;;AAuBA;AAUA;AAiBiB,UA3BA,UAAA,CA2BiB;EAOjB,GAAA,EAAA,MAAA;EAWK;AAAS;AACrB;AACJ;AACK;EAAR,GAAA,CAAA,EAzCI,OAyCJ;AAAO;AA2CO,UAjFA,aAAA,CAiFwB;EAWnB;EAAY,QAAA,EAAA,MAAA;EACxB;EACJ,MAAA,EAAA,MAAA;EACK;EAAR,MAAA,EAAA,MAAA;EAAO;;;;;;;;;UA9EO,iBAAA;;;;;;UAOA,oBAAA,SAA6B;;;;;;;;;;iBAWxB,SAAA,UACZ,2BACJ,aACH,QAAQ;UA2CM,uBAAA,SAAgC;;;;;;;;;;iBAW3B,YAAA,UACZ,8BACJ,aACH,QAAQ"}
1
+ {"version":3,"file":"commands.d.ts","names":[],"sources":["../../../src/lib/cli/commands.ts"],"mappings":";;;;;;;;AAuBA;AAUA;AAiBiB,UA3BA,UAAA,CA2BiB;EASjB,GAAA,EAAA,MAAA;EAWK;AAAS;AACrB;AACJ;AACK;EAAR,GAAA,CAAA,EA3CI,OA2CJ;AAAO;AA2CO,UAnFA,aAAA,CAmFwB;EAWnB;EAAY,QAAA,EAAA,MAAA;EACxB;EACJ,MAAA,EAAA,MAAA;EACK;EAAR,MAAA,EAAA,MAAA;EAAO;;;;;;;;;UAhFO,iBAAA;;;;;;;;UASA,oBAAA,SAA6B;;;;;;;;;;iBAWxB,SAAA,UACZ,2BACJ,aACH,QAAQ;UA2CM,uBAAA,SAAgC;;;;;;;;;;iBAW3B,YAAA,UACZ,8BACJ,aACH,QAAQ"}
@@ -98,7 +98,10 @@ async function loadConfigAndFetchEnv(options, ctx, resolved) {
98
98
  });
99
99
  const envFileSource = join(dirname(resolvedPath), DEFAULT_ENV_FILE);
100
100
  const fileEnv = existsSync(envFileSource) ? parseEnvFile(readFileSync(envFileSource, "utf-8")) : {};
101
- const apiKey = resolveApiKey({ ...options.apiKey ? { apiKey: options.apiKey } : {} });
101
+ const apiKey = resolveApiKey({
102
+ ...options.apiKey ? { apiKey: options.apiKey } : {},
103
+ ...options.profile ? { profile: options.profile } : {}
104
+ });
102
105
  const { vars } = await fetchEnvReusingSecrets(config, {
103
106
  projectId: resolved.projectId,
104
107
  branch: resolved.branch,
@@ -1 +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 { fetchEnvReusingSecrets } from \"../reuse-secrets.js\";\nimport { resolveApiKey } from \"./resolve-api-key.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 * the key {@link resolveApiKey} resolves (`--api-key` → `NEON_API_KEY` → the Neon CLI's\n\t * stored credentials).\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. Everything\n * ambient — `.neon`, `NEON_*` env, the Neon CLI's stored credentials — is resolved by the\n * CLI (see `resolveContext` and `resolveApiKey`), never by the library.\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\tinjected = await loadConfigAndFetchEnv(options, ctx, resolved.context);\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\tentries = await loadConfigAndFetchEnv(options, ctx, resolved.context);\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 resolve the branch env for the explicitly-resolved project + branch.\n * Layers `.env.local` (next to the config file) into the env source so re-runs keep the\n * one-time secrets the Neon API only returns once — the branch credential's, and any Auth\n * values a pre-`base_url` integration can no longer report. Uses\n * {@link fetchEnvReusingSecrets} rather than a bare `fetchEnv` so a run that already has a\n * working credential verifies and keeps it instead of minting another one per invocation.\n */\nasync function loadConfigAndFetchEnv(\n\toptions: EnvResolveOptions,\n\tctx: CommandEnv,\n\tresolved: { projectId: string; branch: string },\n): Promise<Record<string, string>> {\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\tconst apiKey = resolveApiKey({\n\t\t...(options.apiKey ? { apiKey: options.apiKey } : {}),\n\t});\n\tconst { vars } = await fetchEnvReusingSecrets(config, {\n\t\tprojectId: resolved.projectId,\n\t\tbranch: resolved.branch,\n\t\tenv: { ...process.env, ...fileEnv },\n\t\t...(ctx.api ? { api: ctx.api } : {}),\n\t\t...(apiKey ? { apiKey } : {}),\n\t});\n\treturn vars;\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\t// The library's own wording is right for a library (\"this package never reads\n\t// NEON_API_KEY on your behalf\") and wrong here: `neon-env` does read it. Render the\n\t// chain this CLI actually implements, the same way an unresolved context is rendered.\n\tif (err instanceof PlatformError && err.code === ErrorCode.MissingApiKey) {\n\t\treturn errorResult(\n\t\t\terr,\n\t\t\t[\n\t\t\t\t\"No Neon API key. `neon-env` looks for one in this order:\",\n\t\t\t\t\" - the `--api-key` flag\",\n\t\t\t\t\" - the `NEON_API_KEY` environment variable\",\n\t\t\t\t\" - `credentials.json` in `NEONCTL_CONFIG_DIR` (else `~/.config/neonctl`) — run `neon auth` to create it\",\n\t\t\t].join(\"\\n\"),\n\t\t\tEXIT_CODE_BY_PLATFORM_ERROR_CODE[ErrorCode.MissingApiKey] ?? 1,\n\t\t);\n\t}\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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"}
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 { fetchEnvReusingSecrets } from \"../reuse-secrets.js\";\nimport { resolveApiKey } from \"./resolve-api-key.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 * the key {@link resolveApiKey} resolves (`--api-key` → `NEON_API_KEY` → the Neon CLI's\n\t * stored credentials).\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. Everything\n * ambient — `.neon`, `NEON_*` env, the Neon CLI's stored credentials — is resolved by the\n * CLI (see `resolveContext` and `resolveApiKey`), never by the library.\n */\nexport interface EnvResolveOptions {\n\tconfigPath?: string;\n\tprojectId?: string;\n\tbranch?: string;\n\tapiKey?: string;\n\t/** Neon CLI profile whose stored credential to use. `--profile`, else `NEON_PROFILE`. */\n\tprofile?: 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\tinjected = await loadConfigAndFetchEnv(options, ctx, resolved.context);\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\tentries = await loadConfigAndFetchEnv(options, ctx, resolved.context);\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 resolve the branch env for the explicitly-resolved project + branch.\n * Layers `.env.local` (next to the config file) into the env source so re-runs keep the\n * one-time secrets the Neon API only returns once — the branch credential's, and any Auth\n * values a pre-`base_url` integration can no longer report. Uses\n * {@link fetchEnvReusingSecrets} rather than a bare `fetchEnv` so a run that already has a\n * working credential verifies and keeps it instead of minting another one per invocation.\n */\nasync function loadConfigAndFetchEnv(\n\toptions: EnvResolveOptions,\n\tctx: CommandEnv,\n\tresolved: { projectId: string; branch: string },\n): Promise<Record<string, string>> {\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\tconst apiKey = resolveApiKey({\n\t\t...(options.apiKey ? { apiKey: options.apiKey } : {}),\n\t\t...(options.profile ? { profile: options.profile } : {}),\n\t});\n\tconst { vars } = await fetchEnvReusingSecrets(config, {\n\t\tprojectId: resolved.projectId,\n\t\tbranch: resolved.branch,\n\t\tenv: { ...process.env, ...fileEnv },\n\t\t...(ctx.api ? { api: ctx.api } : {}),\n\t\t...(apiKey ? { apiKey } : {}),\n\t});\n\treturn vars;\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\t// The library's own wording is right for a library (\"this package never reads\n\t// NEON_API_KEY on your behalf\") and wrong here: `neon-env` does read it. Render the\n\t// chain this CLI actually implements, the same way an unresolved context is rendered.\n\tif (err instanceof PlatformError && err.code === ErrorCode.MissingApiKey) {\n\t\treturn errorResult(\n\t\t\terr,\n\t\t\t[\n\t\t\t\t\"No Neon API key. `neon-env` looks for one in this order:\",\n\t\t\t\t\" - the `--api-key` flag\",\n\t\t\t\t\" - the `NEON_API_KEY` environment variable\",\n\t\t\t\t\" - `credentials.json` in `NEONCTL_CONFIG_DIR` (else `~/.config/neonctl`) — run `neon auth` to create it\",\n\t\t\t].join(\"\\n\"),\n\t\t\tEXIT_CODE_BY_PLATFORM_ERROR_CODE[ErrorCode.MissingApiKey] ?? 1,\n\t\t);\n\t}\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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"}
@@ -1,21 +1,29 @@
1
+ import { DEFAULT_PROFILE } from "../../_shared/profiles.js";
2
+
1
3
  //#region src/lib/cli/resolve-api-key.d.ts
4
+
2
5
  /**
3
- * Resolve the Neon API key for a `neon-env` CLI invocation. Precedence (each wins over the
4
- * next): `--api-key` flag → `NEON_API_KEY` → `access_token` from the Neon CLI's
5
- * `credentials.json`, located by `@neon/config/paths`.
6
+ * Resolve the Neon API key for a `neon-env` CLI invocation.
6
7
  *
7
- * The CLI owns this resolution `@neon/config` and `@neon/env` are deliberately
8
- * environment- and filesystem-agnostic and only ever accept an explicit `apiKey`, so the
9
- * ambient sources a *user* expects have to be read here. This mirrors `resolveContext`,
10
- * which does the same for project and branch.
8
+ * Precedence is the `neon` CLI's, from the same module: **an explicit flag beats an ambient
9
+ * environment variable.** `--api-key` and `--profile` together is an error; `--profile` beats
10
+ * `NEON_API_KEY`; `--api-key` beats `NEON_PROFILE`; two ambient sources resolve to the key.
11
11
  *
12
- * Returns `undefined` rather than throwing when nothing provides a key: the caller passes
13
- * it straight through, and the library raises the uniform `PLATFORM_MISSING_API_KEY` error.
12
+ * Sharing that decision rather than restating it is the point. An earlier version of this file
13
+ * checked `NEON_API_KEY` before the selected profile, so `NEON_API_KEY=… neon-env run --profile
14
+ * work` silently used the wrong account — the very bug this feature fixes in `neon`.
15
+ *
16
+ * The CLI owns the resolution because `@neon/config` and `@neon/env`'s root export are
17
+ * deliberately environment- and filesystem-agnostic: they accept an explicit `apiKey` and
18
+ * nothing else, so the ambient sources a *user* expects have to be read out here.
14
19
  */
15
20
  declare function resolveApiKey(options: {
16
21
  apiKey?: string;
22
+ profile?: string;
17
23
  env?: NodeJS.ProcessEnv;
24
+ /** Where to say that an exported key displaced an exported profile. Defaults to stderr. */
25
+ warn?: (message: string) => void;
18
26
  }): string | undefined;
19
27
  //#endregion
20
- export { resolveApiKey };
28
+ export { DEFAULT_PROFILE, resolveApiKey };
21
29
  //# sourceMappingURL=resolve-api-key.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"resolve-api-key.d.ts","names":[],"sources":["../../../src/lib/cli/resolve-api-key.ts"],"mappings":";;AAgBA;;;;;;;;;;;;iBAAgB,aAAA;;QAET,MAAA,CAAO"}
1
+ {"version":3,"file":"resolve-api-key.d.ts","names":[],"sources":["../../../src/lib/cli/resolve-api-key.ts"],"mappings":";;;;;;AA0BA;;;;;;;;;;;;;iBAAgB,aAAA;;;QAGT,MAAA,CAAO"}
@@ -1,57 +1,74 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { resolveConfigFile } from "@neon/config/paths";
1
+ import { configDir, resolveConfigFile } from "../../_shared/paths.js";
2
+ import { DEFAULT_PROFILE, resolveProfile } from "../../_shared/profiles.js";
3
+ import { displacedProfileWarning, selectCredential } from "../../_shared/auth_selection.js";
4
+ import { inspectCredentials, interpretCredentials } from "../../_shared/credentials.js";
3
5
  //#region src/lib/cli/resolve-api-key.ts
4
6
  /**
5
- * Resolve the Neon API key for a `neon-env` CLI invocation. Precedence (each wins over the
6
- * next): `--api-key` flag → `NEON_API_KEY` → `access_token` from the Neon CLI's
7
- * `credentials.json`, located by `@neon/config/paths`.
7
+ * Resolve the Neon API key for a `neon-env` CLI invocation.
8
8
  *
9
- * The CLI owns this resolution `@neon/config` and `@neon/env` are deliberately
10
- * environment- and filesystem-agnostic and only ever accept an explicit `apiKey`, so the
11
- * ambient sources a *user* expects have to be read here. This mirrors `resolveContext`,
12
- * which does the same for project and branch.
9
+ * Precedence is the `neon` CLI's, from the same module: **an explicit flag beats an ambient
10
+ * environment variable.** `--api-key` and `--profile` together is an error; `--profile` beats
11
+ * `NEON_API_KEY`; `--api-key` beats `NEON_PROFILE`; two ambient sources resolve to the key.
13
12
  *
14
- * Returns `undefined` rather than throwing when nothing provides a key: the caller passes
15
- * it straight through, and the library raises the uniform `PLATFORM_MISSING_API_KEY` error.
13
+ * Sharing that decision rather than restating it is the point. An earlier version of this file
14
+ * checked `NEON_API_KEY` before the selected profile, so `NEON_API_KEY=… neon-env run --profile
15
+ * work` silently used the wrong account — the very bug this feature fixes in `neon`.
16
+ *
17
+ * The CLI owns the resolution because `@neon/config` and `@neon/env`'s root export are
18
+ * deliberately environment- and filesystem-agnostic: they accept an explicit `apiKey` and
19
+ * nothing else, so the ambient sources a *user* expects have to be read out here.
16
20
  */
17
21
  function resolveApiKey(options) {
18
22
  const env = options.env ?? process.env;
19
- return nonEmpty(options.apiKey) ?? nonEmpty(env.NEON_API_KEY) ?? readStoredAccessToken(env);
23
+ const selection = selectCredential({
24
+ ...options.apiKey !== void 0 ? { apiKeyFlag: options.apiKey } : {},
25
+ ...options.profile !== void 0 ? { profileFlag: options.profile } : {},
26
+ ...env.NEON_API_KEY !== void 0 ? { apiKeyEnv: env.NEON_API_KEY } : {},
27
+ ...env.NEON_PROFILE !== void 0 ? { profileEnv: env.NEON_PROFILE } : {}
28
+ });
29
+ const displaced = displacedProfileWarning(selection);
30
+ if (displaced !== null) (options.warn ?? ((message) => process.stderr.write(`${message}\n`)))(displaced);
31
+ if (selection.source !== "profile") return selection.apiKey;
32
+ return readStoredCredential(selection, env);
20
33
  }
21
34
  /**
22
- * Read `access_token` from the Neon CLI's credentials file.
23
- *
24
- * Location resolution is delegated to `@neon/config/paths` so this agrees with the `neon`
25
- * CLI itself — `NEON_CONFIG_DIR` / `NEONCTL_CONFIG_DIR`, else `$XDG_CONFIG_HOME/neon`, else
26
- * `~/.config/neon`, with an existing legacy `neonctl` directory still read. Rolling that
27
- * lookup by hand here is how the two drifted in the first place: this file honoured the env
28
- * var but not XDG, while the CLI honoured XDG but not the env var, so with
29
- * `XDG_CONFIG_HOME` set they disagreed about where credentials lived.
35
+ * The credential stored for the selected profile.
30
36
  *
31
- * Reads only `DEFAULT` a profile is a CLI-invocation concept, and `neon-env` has no
32
- * `--profile` of its own to read one from.
33
- *
34
- * Never throws: a missing, unreadable, malformed, or token-less file is simply "no key",
35
- * so this can sit in a resolution chain without try/catch noise.
37
+ * Two different situations, deliberately not merged. A **missing** credential under `DEFAULT` is
38
+ * the ordinary not-signed-in state and resolves to no key; under a profile the user named it is
39
+ * an error, because reporting a missing credential would hide that the real problem is the name
40
+ * they typed. A **damaged** credential is always an error: the file is there, it is not an
41
+ * absence, and no amount of signing in elsewhere explains it.
36
42
  */
37
- function readStoredAccessToken(env) {
38
- const { path: credentialsPath } = resolveConfigFile("credentials.json", { env });
39
- if (!existsSync(credentialsPath)) return void 0;
40
- let parsed;
43
+ function readStoredCredential(selection, env) {
44
+ const { profile, explicit } = selection;
45
+ /**
46
+ * An *absence* is only an error when the user named the profile. Not being signed in under
47
+ * `DEFAULT` is the ordinary state, and the library's `PLATFORM_MISSING_API_KEY` says it
48
+ * better than a stack trace.
49
+ */
50
+ const absent = (reason) => {
51
+ if (explicit) throw new Error(reason);
52
+ };
53
+ let path;
41
54
  try {
42
- parsed = JSON.parse(readFileSync(credentialsPath, "utf-8"));
43
- } catch {
44
- return;
55
+ path = profile === "DEFAULT" ? resolveConfigFile("credentials.json", { env }).path : resolveProfile(configDir({ env }), profile).credentialsPath;
56
+ } catch (err) {
57
+ throw err instanceof Error ? err : new Error(String(err));
45
58
  }
46
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
47
- return nonEmpty(parsed.access_token);
48
- }
49
- function nonEmpty(value) {
50
- if (typeof value !== "string") return void 0;
51
- const trimmed = value.trim();
52
- return trimmed === "" ? void 0 : trimmed;
59
+ const read = inspectCredentials(path);
60
+ if (read.kind === "absent") return absent(`Profile "${profile}" has no stored credential at ${path}. Sign in with \`neon profile create ${profile}\`.`);
61
+ if (read.kind === "unusable") throw new Error(`${read.reason}. Replace it deliberately with \`neon profile create ${profile} --force\`, or delete the file.`);
62
+ const credential = interpretCredentials(read.credentials, {
63
+ path,
64
+ profile
65
+ });
66
+ if (credential.kind === "api_key") return credential.apiKey;
67
+ const token = read.credentials.access_token;
68
+ if (typeof token === "string" && token.trim() !== "") return token.trim();
69
+ throw new Error(`Profile "${profile}" holds a browser sign-in with no usable token at ${path}. Sign in again with \`neon auth --profile ${profile}\`.`);
53
70
  }
54
71
  //#endregion
55
- export { resolveApiKey };
72
+ export { DEFAULT_PROFILE, resolveApiKey };
56
73
 
57
74
  //# sourceMappingURL=resolve-api-key.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"resolve-api-key.js","names":[],"sources":["../../../src/lib/cli/resolve-api-key.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { resolveConfigFile } from \"@neon/config/paths\";\n\n/**\n * Resolve the Neon API key for a `neon-env` CLI invocation. Precedence (each wins over the\n * next): `--api-key` flag `NEON_API_KEY` `access_token` from the Neon CLI's\n * `credentials.json`, located by `@neon/config/paths`.\n *\n * The CLI owns this resolution `@neon/config` and `@neon/env` are deliberately\n * environment- and filesystem-agnostic and only ever accept an explicit `apiKey`, so the\n * ambient sources a *user* expects have to be read here. This mirrors `resolveContext`,\n * which does the same for project and branch.\n *\n * Returns `undefined` rather than throwing when nothing provides a key: the caller passes\n * it straight through, and the library raises the uniform `PLATFORM_MISSING_API_KEY` error.\n */\nexport function resolveApiKey(options: {\n\tapiKey?: string;\n\tenv?: NodeJS.ProcessEnv;\n}): string | undefined {\n\tconst env = options.env ?? process.env;\n\treturn (\n\t\tnonEmpty(options.apiKey) ??\n\t\tnonEmpty(env.NEON_API_KEY) ??\n\t\treadStoredAccessToken(env)\n\t);\n}\n\n/**\n * Read `access_token` from the Neon CLI's credentials file.\n *\n * Location resolution is delegated to `@neon/config/paths` so this agrees with the `neon`\n * CLI itself `NEON_CONFIG_DIR` / `NEONCTL_CONFIG_DIR`, else `$XDG_CONFIG_HOME/neon`, else\n * `~/.config/neon`, with an existing legacy `neonctl` directory still read. Rolling that\n * lookup by hand here is how the two drifted in the first place: this file honoured the env\n * var but not XDG, while the CLI honoured XDG but not the env var, so with\n * `XDG_CONFIG_HOME` set they disagreed about where credentials lived.\n *\n * Reads only `DEFAULT` a profile is a CLI-invocation concept, and `neon-env` has no\n * `--profile` of its own to read one from.\n *\n * Never throws: a missing, unreadable, malformed, or token-less file is simply \"no key\",\n * so this can sit in a resolution chain without try/catch noise.\n */\nfunction readStoredAccessToken(env: NodeJS.ProcessEnv): string | undefined {\n\tconst { path: credentialsPath } = resolveConfigFile(\"credentials.json\", {\n\t\tenv,\n\t});\n\tif (!existsSync(credentialsPath)) return undefined;\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(readFileSync(credentialsPath, \"utf-8\"));\n\t} catch {\n\t\treturn undefined;\n\t}\n\n\tif (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed))\n\t\treturn undefined;\n\treturn nonEmpty((parsed as Record<string, unknown>).access_token as string);\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":";;;;;;;;;;;;;;;;AAgBA,SAAgB,cAAc,SAGP;CACtB,MAAM,MAAM,QAAQ,OAAO,QAAQ;CACnC,OACC,SAAS,QAAQ,MAAM,KACvB,SAAS,IAAI,YAAY,KACzB,sBAAsB,GAAG;AAE3B;;;;;;;;;;;;;;;;;AAkBA,SAAS,sBAAsB,KAA4C;CAC1E,MAAM,EAAE,MAAM,oBAAoB,kBAAkB,oBAAoB,EACvE,IACD,CAAC;CACD,IAAI,CAAC,WAAW,eAAe,GAAG,OAAO,KAAA;CAEzC,IAAI;CACJ,IAAI;EACH,SAAS,KAAK,MAAM,aAAa,iBAAiB,OAAO,CAAC;CAC3D,QAAQ;EACP;CACD;CAEA,IAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GACxE,OAAO,KAAA;CACR,OAAO,SAAU,OAAmC,YAAsB;AAC3E;AAEA,SAAS,SAAS,OAA+C;CAChE,IAAI,OAAO,UAAU,UAAU,OAAO,KAAA;CACtC,MAAM,UAAU,MAAM,KAAK;CAC3B,OAAO,YAAY,KAAK,KAAA,IAAY;AACrC"}
1
+ {"version":3,"file":"resolve-api-key.js","names":[],"sources":["../../../src/lib/cli/resolve-api-key.ts"],"sourcesContent":["import {\n\tdisplacedProfileWarning,\n\tselectCredential,\n} from \"../../_shared/auth_selection.js\";\nimport {\n\tinspectCredentials,\n\tinterpretCredentials,\n} from \"../../_shared/credentials.js\";\nimport { configDir, resolveConfigFile } from \"../../_shared/paths.js\";\nimport { DEFAULT_PROFILE, resolveProfile } from \"../../_shared/profiles.js\";\n\n/**\n * Resolve the Neon API key for a `neon-env` CLI invocation.\n *\n * Precedence is the `neon` CLI's, from the same module: **an explicit flag beats an ambient\n * environment variable.** `--api-key` and `--profile` together is an error; `--profile` beats\n * `NEON_API_KEY`; `--api-key` beats `NEON_PROFILE`; two ambient sources resolve to the key.\n *\n * Sharing that decision rather than restating it is the point. An earlier version of this file\n * checked `NEON_API_KEY` before the selected profile, so `NEON_API_KEY=… neon-env run --profile\n * work` silently used the wrong account the very bug this feature fixes in `neon`.\n *\n * The CLI owns the resolution because `@neon/config` and `@neon/env`'s root export are\n * deliberately environment- and filesystem-agnostic: they accept an explicit `apiKey` and\n * nothing else, so the ambient sources a *user* expects have to be read out here.\n */\nexport function resolveApiKey(options: {\n\tapiKey?: string;\n\tprofile?: string;\n\tenv?: NodeJS.ProcessEnv;\n\t/** Where to say that an exported key displaced an exported profile. Defaults to stderr. */\n\twarn?: (message: string) => void;\n}): string | undefined {\n\tconst env = options.env ?? process.env;\n\tconst selection = selectCredential({\n\t\t...(options.apiKey !== undefined ? { apiKeyFlag: options.apiKey } : {}),\n\t\t...(options.profile !== undefined\n\t\t\t? { profileFlag: options.profile }\n\t\t\t: {}),\n\t\t...(env.NEON_API_KEY !== undefined\n\t\t\t? { apiKeyEnv: env.NEON_API_KEY }\n\t\t\t: {}),\n\t\t...(env.NEON_PROFILE !== undefined\n\t\t\t? { profileEnv: env.NEON_PROFILE }\n\t\t\t: {}),\n\t});\n\n\t// Sharing the decision is only half of it. The disclosure is the half that keeps a\n\t// displaced profile from being the original bug in a quieter form: `NEON_API_KEY=…\n\t// NEON_PROFILE=work neon-env run` legitimately uses the key, and saying nothing leaves the\n\t// user believing they ran as `work`. `neon` has warned here from the start; this did not.\n\tconst displaced = displacedProfileWarning(selection);\n\tif (displaced !== null) {\n\t\tconst warn =\n\t\t\toptions.warn ??\n\t\t\t((message: string) => process.stderr.write(`${message}\\n`));\n\t\twarn(displaced);\n\t}\n\n\tif (selection.source !== \"profile\") return selection.apiKey;\n\treturn readStoredCredential(selection, env);\n}\n\n/**\n * The credential stored for the selected profile.\n *\n * Two different situations, deliberately not merged. A **missing** credential under `DEFAULT` is\n * the ordinary not-signed-in state and resolves to no key; under a profile the user named it is\n * an error, because reporting a missing credential would hide that the real problem is the name\n * they typed. A **damaged** credential is always an error: the file is there, it is not an\n * absence, and no amount of signing in elsewhere explains it.\n */\nfunction readStoredCredential(\n\tselection: { profile: string; explicit: boolean },\n\tenv: NodeJS.ProcessEnv,\n): string | undefined {\n\tconst { profile, explicit } = selection;\n\t/**\n\t * An *absence* is only an error when the user named the profile. Not being signed in under\n\t * `DEFAULT` is the ordinary state, and the library's `PLATFORM_MISSING_API_KEY` says it\n\t * better than a stack trace.\n\t */\n\tconst absent = (reason: string): undefined => {\n\t\tif (explicit) throw new Error(reason);\n\t\treturn undefined;\n\t};\n\n\tlet path: string;\n\ttry {\n\t\tpath =\n\t\t\tprofile === DEFAULT_PROFILE\n\t\t\t\t? // Per *file*, so an install predating the rename still finds its\n\t\t\t\t\t// `credentials.json` in the legacy `neonctl` directory, in place.\n\t\t\t\t\tresolveConfigFile(\"credentials.json\", { env }).path\n\t\t\t\t: // From the config root, not from wherever `credentials.json` happens to live:\n\t\t\t\t\t// that file can still be in `neonctl/` while `profiles.json` is in `neon/`,\n\t\t\t\t\t// and deriving one from the other loses every named profile.\n\t\t\t\t\tresolveProfile(configDir({ env }), profile).credentialsPath;\n\t} catch (err) {\n\t\t// An unknown profile name is a naming error whoever typed it can fix, so it is fatal\n\t\t// either way — it cannot be reported as \"not signed in\".\n\t\tthrow err instanceof Error ? err : new Error(String(err));\n\t}\n\n\tconst read = inspectCredentials(path);\n\tif (read.kind === \"absent\") {\n\t\treturn absent(\n\t\t\t`Profile \"${profile}\" has no stored credential at ${path}. Sign in with \\`neon profile create ${profile}\\`.`,\n\t\t);\n\t}\n\n\t// A file that exists but cannot be read is never an absence, named or not. Returning\n\t// `undefined` here reported \"no API key\" for a credential that is present and broken,\n\t// which sends the reader looking for a missing login instead of at the damaged file — and\n\t// under `neon` the same file is a hard error, so the two CLIs disagreed about it.\n\tif (read.kind === \"unusable\") {\n\t\tthrow new Error(\n\t\t\t`${read.reason}. Replace it deliberately with \\`neon profile create ${profile} --force\\`, or delete the file.`,\n\t\t);\n\t}\n\n\tconst credential = interpretCredentials(read.credentials, {\n\t\tpath,\n\t\tprofile,\n\t});\n\tif (credential.kind === \"api_key\") return credential.apiKey;\n\tconst token = read.credentials.access_token;\n\tif (typeof token === \"string\" && token.trim() !== \"\") return token.trim();\n\tthrow new Error(\n\t\t`Profile \"${profile}\" holds a browser sign-in with no usable token at ${path}. Sign in again with \\`neon auth --profile ${profile}\\`.`,\n\t);\n}\n\nexport { DEFAULT_PROFILE };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,cAAc,SAMP;CACtB,MAAM,MAAM,QAAQ,OAAO,QAAQ;CACnC,MAAM,YAAY,iBAAiB;EAClC,GAAI,QAAQ,WAAW,KAAA,IAAY,EAAE,YAAY,QAAQ,OAAO,IAAI,CAAC;EACrE,GAAI,QAAQ,YAAY,KAAA,IACrB,EAAE,aAAa,QAAQ,QAAQ,IAC/B,CAAC;EACJ,GAAI,IAAI,iBAAiB,KAAA,IACtB,EAAE,WAAW,IAAI,aAAa,IAC9B,CAAC;EACJ,GAAI,IAAI,iBAAiB,KAAA,IACtB,EAAE,YAAY,IAAI,aAAa,IAC/B,CAAC;CACL,CAAC;CAMD,MAAM,YAAY,wBAAwB,SAAS;CACnD,IAAI,cAAc,MAIjB,CAFC,QAAQ,UACN,YAAoB,QAAQ,OAAO,MAAM,GAAG,QAAQ,GAAG,GAAA,CACrD,SAAS;CAGf,IAAI,UAAU,WAAW,WAAW,OAAO,UAAU;CACrD,OAAO,qBAAqB,WAAW,GAAG;AAC3C;;;;;;;;;;AAWA,SAAS,qBACR,WACA,KACqB;CACrB,MAAM,EAAE,SAAS,aAAa;;;;;;CAM9B,MAAM,UAAU,WAA8B;EAC7C,IAAI,UAAU,MAAM,IAAI,MAAM,MAAM;CAErC;CAEA,IAAI;CACJ,IAAI;EACH,OACC,YAAA,YAGE,kBAAkB,oBAAoB,EAAE,IAAI,CAAC,CAAC,CAAC,OAI/C,eAAe,UAAU,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC;CAChD,SAAS,KAAK;EAGb,MAAM,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;CACzD;CAEA,MAAM,OAAO,mBAAmB,IAAI;CACpC,IAAI,KAAK,SAAS,UACjB,OAAO,OACN,YAAY,QAAQ,gCAAgC,KAAK,uCAAuC,QAAQ,IACzG;CAOD,IAAI,KAAK,SAAS,YACjB,MAAM,IAAI,MACT,GAAG,KAAK,OAAO,uDAAuD,QAAQ,gCAC/E;CAGD,MAAM,aAAa,qBAAqB,KAAK,aAAa;EACzD;EACA;CACD,CAAC;CACD,IAAI,WAAW,SAAS,WAAW,OAAO,WAAW;CACrD,MAAM,QAAQ,KAAK,YAAY;CAC/B,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,MAAM,IAAI,OAAO,MAAM,KAAK;CACxE,MAAM,IAAI,MACT,YAAY,QAAQ,oDAAoD,KAAK,6CAA6C,QAAQ,IACnI;AACD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neondatabase/env",
3
- "version": "0.13.2",
3
+ "version": "0.14.0",
4
4
  "description": "Resolve and inject Neon connection strings for the branch selected by your neon.ts policy. fetchEnv / parseEnv plus a `neon-env` CLI with `run` and `export`.",
5
5
  "keywords": [
6
6
  "neon",
@@ -51,12 +51,12 @@
51
51
  "typescript": "^5.9.0",
52
52
  "vitest": "^3.0.9",
53
53
  "@neon/e2e-harness": "0.0.0",
54
- "@neon/sdk": "1.4.1"
54
+ "@neon/sdk": "1.5.0"
55
55
  },
56
56
  "dependencies": {
57
57
  "zod": "^4.4.3",
58
58
  "yargs": "^18.0.0",
59
- "@neon/config": "0.13.1"
59
+ "@neon/config": "0.14.0"
60
60
  },
61
61
  "engines": {
62
62
  "node": ">=20.19.0"
@@ -65,11 +65,11 @@
65
65
  "provenance": false
66
66
  },
67
67
  "scripts": {
68
- "build": "tsc --noEmit && tsdown",
69
- "test": "pnpm --filter @neon/env... build && vitest --passWithNoTests",
70
- "test:ci": "vitest run --passWithNoTests",
71
- "test:types": "vitest run --typecheck.enabled --typecheck.only",
72
- "test:e2e": "vitest run --config vitest.e2e.config.ts",
68
+ "build": "node ../../scripts/sync-shared.mjs . && tsc --noEmit && tsdown",
69
+ "test": "node ../../scripts/sync-shared.mjs . && pnpm --filter @neon/env... build && vitest --passWithNoTests",
70
+ "test:ci": "node ../../scripts/sync-shared.mjs . && vitest run --passWithNoTests",
71
+ "test:types": "node ../../scripts/sync-shared.mjs . && vitest run --typecheck.enabled --typecheck.only",
72
+ "test:e2e": "node ../../scripts/sync-shared.mjs . && vitest run --config vitest.e2e.config.ts",
73
73
  "tsc": "tsc"
74
74
  }
75
75
  }