neon 4.14.5 → 4.15.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.
package/README.md CHANGED
@@ -116,22 +116,23 @@ to skip the dotenv write.
116
116
 
117
117
  The command writes:
118
118
 
119
- - a `.neon` context that identifies the project and Claimable Neon service;
119
+ - a `.neon` context with the project id and pinned branch;
120
120
  - an owner-only identity assertion under the CLI config directory;
121
121
  - `DATABASE_URL`, `DATABASE_URL_UNPOOLED`, `NEON_BRANCH`, and Auth / Data API variables
122
122
  when `neon.ts` declares them (or, with no `neon.ts`, when they are provisioned) to `.env`
123
123
  or `.env.local` (disable this with `--no-env-pull`). The write is the same `env pull` used
124
124
  after create.
125
125
 
126
- Subsequent project commands automatically exchange the assertion for a short-lived agent
127
- token and send API calls to Claimable Neon. The service decides which operations are
128
- allowed before claim.
126
+ Subsequent project commands find that assertion by the linked project id, exchange it for a
127
+ short-lived agent token, and send API calls to Claimable Neon. The service decides which
128
+ operations are allowed before claim.
129
129
 
130
130
  ```bash
131
131
  neon claim status # lifecycle and transfer status
132
132
  neon projects get <project-id> # regular CLI command, same agent token
133
133
  neon psql --role-name neondb_owner -- -c "select now()"
134
134
  neon config plan
135
+ neon checkout main
135
136
  neon env pull
136
137
 
137
138
  neon claim accept # create a claim code and open the transfer URL
@@ -439,8 +440,8 @@ The branch argument is **optional**: run `neon checkout` with no branch in an in
439
440
 
440
441
  Branch **id vs name** is detected automatically (a `br-…` value is treated as an id):
441
442
 
442
- - **id** — matched strictly by id. A non-existent id is a hard "not found" error (ids are server-assigned, so checkout never creates one).
443
- - **name** — matched by name. If the name doesn't exist, in an interactive terminal `checkout` offers to **create** it (equivalent to `neon branch create --name <name>`: branched from the project's default branch with a read-write compute), then checks it out. In a non-interactive context a missing name is the usual "not found" error.
443
+ - **id** — matched strictly by id. A non-existent id is a hard "not found" error (ids are server-assigned, so checkout never creates one, including with `--create`).
444
+ - **name** — matched by name. If the name doesn't exist, pass `--create` to create it (equivalent to `neon branch create --name <name>`: branched from the project's default branch with a read-write compute, or from `neon.ts` when that file is present), then check it out. `--create` is a no-op when the name already exists. Without `--create`, an interactive terminal offers to create the branch; in CI or with no TTY the error names `--create`.
444
445
 
445
446
  The project is resolved through the standard neon chain, each entry winning over the next:
446
447
 
@@ -456,11 +457,15 @@ The resolved branch is then written (by name) to the same `.neon` file `link` us
456
457
  $ neon checkout main --project-id polished-snowflake-12345678
457
458
  INFO: Checked out branch br-main-branch-87654321 on project polished-snowflake-12345678. Updated /path/to/cwd/.neon.
458
459
 
460
+ $ neon checkout dev --create --project-id polished-snowflake-12345678
461
+ INFO: Created branch dev (br-dev-branch-12345678).
462
+ INFO: Checked out branch br-dev-branch-12345678 on project polished-snowflake-12345678. Updated /path/to/cwd/.neon.
463
+
459
464
  $ cat .neon
460
465
  {
461
466
  "orgId": "org-abc123",
462
467
  "projectId": "polished-snowflake-12345678",
463
- "branch": "main"
468
+ "branch": "dev"
464
469
  }
465
470
  ```
466
471
 
@@ -77,35 +77,34 @@ const removeClaimableCredentials = (configDir, projectId) => {
77
77
  throw error;
78
78
  }
79
79
  };
80
- const resolveClaimableContext = (context) => {
81
- const marker = context.claimable;
82
- if (marker === void 0) return null;
83
- if (!isRecord(marker)) throw new Error("The linked .neon file has an invalid \"claimable\" marker. Delete the claimable field from .neon, or delete .neon.");
84
- if (marker.version !== 1) throw new Error(`Unsupported Claimable Neon context version in .neon. Update the Neon CLI before using this project.`);
85
- if (!nonEmptyString(marker.origin) || !nonEmptyString(context.projectId)) throw new Error("The linked .neon file has an incomplete \"claimable\" marker. Delete the claimable field from .neon, or delete .neon.");
86
- assertProjectId(context.projectId);
87
- return {
88
- origin: marker.origin,
89
- projectId: context.projectId,
90
- ...nonEmptyString(context.branch) ? { branch: context.branch } : {}
91
- };
80
+ const readLinkedClaimableCredentials = (configDir, context) => {
81
+ if (!nonEmptyString(context.projectId)) return null;
82
+ if (!PROJECT_ID.test(context.projectId)) return null;
83
+ return readClaimableCredentials(configDir, context.projectId);
84
+ };
85
+ const claimableCredentialsExist = (configDir, projectId) => {
86
+ if (!PROJECT_ID.test(projectId)) return false;
87
+ return existsSync(claimableCredentialsPath(configDir, projectId));
92
88
  };
93
- const shouldUseClaimableCredentials = (inputs, profileFlag, context) => inputs.apiKeyFlag.trim() === "" && inputs.apiKeyEnv.trim() === "" && inputs.profileEnv.trim() === "" && (profileFlag === void 0 || profileFlag.trim() === "") && resolveClaimableContext(context) !== null;
89
+ const shouldUseClaimableCredentials = (inputs, profileFlag, context, configDir) => inputs.apiKeyFlag.trim() === "" && inputs.apiKeyEnv.trim() === "" && inputs.profileEnv.trim() === "" && (profileFlag === void 0 || profileFlag.trim() === "") && readLinkedClaimableCredentials(configDir, context) !== null;
94
90
  /** Management API base URL the CLI uses for a Claimable Neon origin. */
95
91
  const claimableApiHost = (origin) => `${new ClaimableClient(origin).origin}/v1`;
96
92
  /**
97
93
  * Whether this invocation is talking to Claimable Neon, so env resolve must not
98
94
  * mint credentials or pull services that require claim.
99
95
  *
100
- * Auth source wins so a same-process checkout that rewrote `.neon` without the
101
- * marker still skips minting. A `.neon` marker plus a production API host is an
102
- * account override and is not claimable.
96
+ * Auth source wins so a same-process checkout that rewrote `.neon` still skips
97
+ * minting. A credential file plus a production API host is an account override
98
+ * and is not claimable.
103
99
  */
104
100
  const isClaimableEnvTarget = (props) => {
105
- if (getAuthContext()?.source === "claimable") return true;
106
- const linked = resolveClaimableContext(readContextFile(props.contextFile));
107
- if (linked === null) return false;
108
- return props.apiHost.trim().replace(/\/+$/, "") === claimableApiHost(linked.origin);
101
+ const source = getAuthContext()?.source;
102
+ if (source === "claimable") return true;
103
+ if (source !== void 0) return false;
104
+ if (props.configDir.trim() === "") return false;
105
+ const stored = readLinkedClaimableCredentials(props.configDir, readContextFile(props.contextFile));
106
+ if (stored === null) return false;
107
+ return props.apiHost.trim().replace(/\/+$/, "") === claimableApiHost(stored.origin);
109
108
  };
110
109
  //#endregion
111
- export { assertionHasExpired, claimableApiHost, claimableCredentialsPath, isClaimableEnvTarget, listClaimableCredentials, readClaimableCredentials, removeClaimableCredentials, resolveClaimableContext, shouldUseClaimableCredentials, writeClaimableCredentials };
110
+ export { assertionHasExpired, claimableApiHost, claimableCredentialsExist, claimableCredentialsPath, isClaimableEnvTarget, listClaimableCredentials, readClaimableCredentials, readLinkedClaimableCredentials, removeClaimableCredentials, shouldUseClaimableCredentials, writeClaimableCredentials };
@@ -5,13 +5,13 @@ import { log } from "../log.js";
5
5
  import { getApiClient, isNeonApiError } from "../api.js";
6
6
  import { setAuthContext } from "../auth_context.js";
7
7
  import { a as isOwnedCredentialPath } from "../_chunks/paths-DMq0Lt7a.js";
8
- import { currentContextFile, isAskCommand, isClaimCommand, isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand, readContextFile } from "../context.js";
8
+ import { currentContextFile, isAskCommand, isClaimCommand, isConfigInit, isCurrentBranchProbe, isInspectDbUrl, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand, readContextFile } from "../context.js";
9
9
  import { _ as upsertProfile, d as newProfileLocation, g as selectProfileName, h as resolveProfile, i as assertValidProfileName, l as locationForName, m as readProfiles, n as KEYRING_CREDENTIALS, p as profilesUsingPath, r as assertProfilesUsable, s as isKeyringPointer, t as DEFAULT_PROFILE, u as locationOf } from "../_chunks/profiles-CvnFEQyd.js";
10
10
  import { extendTokenSet } from "../utils/auth.js";
11
11
  import { AuthRefreshError, auth, classifyRefreshFailure, refreshToken } from "../auth.js";
12
12
  import { i as selectCredential, n as displacedProfileWarning, t as credentialInputs } from "../_chunks/auth_selection-pitMFzDe.js";
13
13
  import { ClaimableClient, ClaimableServiceError } from "../claimable/api.js";
14
- import { assertionHasExpired, claimableCredentialsPath, readClaimableCredentials, resolveClaimableContext, shouldUseClaimableCredentials } from "../claimable/state.js";
14
+ import { assertionHasExpired, claimableCredentialsExist, claimableCredentialsPath, readLinkedClaimableCredentials, shouldUseClaimableCredentials } from "../claimable/state.js";
15
15
  import { withExclusiveLock } from "../refresh_lock.js";
16
16
  import { readOutgoingCredential, retirePreviousCredential } from "../retire_credential.js";
17
17
  import { existsSync } from "node:fs";
@@ -291,6 +291,7 @@ const ensureAuth = async (props) => {
291
291
  if (isConfigInit(props)) return;
292
292
  if (isProfileCommand(props)) return;
293
293
  if (isMcpOauth(props)) return;
294
+ if (isInspectDbUrl(props)) return;
294
295
  if (props._[0] === "open") return;
295
296
  if (isSkillsCommand(props) || isPluginsCommand(props)) return;
296
297
  if (isAskCommand(props)) return;
@@ -316,15 +317,12 @@ const ensureAuth = async (props) => {
316
317
  const inputs = credentialInputs();
317
318
  const contextFile = typeof props.contextFile === "function" ? props.contextFile() : props.contextFile ?? currentContextFile();
318
319
  const localContext = readContextFile(contextFile);
319
- if (shouldUseClaimableCredentials(inputs, props.profile, localContext)) {
320
- const linked = resolveClaimableContext(localContext);
321
- if (linked === null) throw new Error("The linked Claimable Neon context could not be resolved.");
322
- const stored = readClaimableCredentials(props.configDir, linked.projectId);
323
- const path = claimableCredentialsPath(props.configDir, linked.projectId);
324
- if (stored === null) throw new Error(`The linked project is claimable, but its identity assertion is missing from ${path}. Run \`neon claim create\` in a new directory, or \`neon link\` after claiming the project.`);
325
- if (assertionHasExpired(stored)) throw new Error(`The identity assertion for ${linked.projectId} has expired. Run \`neon claim delete ${linked.projectId} --yes\` to drop the local record.`);
320
+ if (shouldUseClaimableCredentials(inputs, props.profile, localContext, props.configDir)) {
321
+ const stored = readLinkedClaimableCredentials(props.configDir, localContext);
322
+ if (stored === null || localContext.projectId === void 0) throw new Error("The linked Claimable Neon identity assertion could not be read.");
323
+ const path = claimableCredentialsPath(props.configDir, localContext.projectId);
324
+ if (assertionHasExpired(stored)) throw new Error(`The identity assertion for ${localContext.projectId} has expired. Run \`neon claim delete ${localContext.projectId} --yes\` to drop the local record.`);
326
325
  const client = new ClaimableClient(stored.origin);
327
- if (client.origin !== new ClaimableClient(linked.origin).origin) throw new Error(`The linked .neon file and ${path} name different Claimable Neon services. Delete .neon or the assertion file and run \`neon claim create\` in a new directory.`);
328
326
  let token;
329
327
  try {
330
328
  token = await client.exchange(stored.identityAssertion);
@@ -346,7 +344,7 @@ const ensureAuth = async (props) => {
346
344
  log.debug("Using the linked Claimable Neon project's short-lived access token");
347
345
  return;
348
346
  }
349
- if (localContext.claimable !== void 0) log.warning("This directory is linked to a claimable project, but NEON_API_KEY or NEON_PROFILE is set. This command will use that account credential instead of the unclaimed project. Unset them to keep using the unclaimed project.");
347
+ if (localContext.projectId !== void 0 && claimableCredentialsExist(props.configDir, localContext.projectId)) log.warning("This directory is linked to a claimable project, but NEON_API_KEY or NEON_PROFILE is set. This command will use that account credential instead of the unclaimed project. Unset them to keep using the unclaimed project.");
350
348
  const selection = selectCredential({
351
349
  ...inputs,
352
350
  profileFlag: props.profile
@@ -28,7 +28,7 @@ const formatCheckoutPolicyFailure = (opts) => {
28
28
  return [
29
29
  `Branch ${opts.branchName} (${opts.branchId}) was created and checked out, but applying neon.ts to it failed: ${opts.failure}`,
30
30
  `The branch is usable but does not match the policy, and \`${cli} checkout\` never reconciles a branch that already exists.`,
31
- `Fix the cause above, then run \`${cli} deploy --update-existing${envArg}\` to apply the policy to it — or, if your policy only configures new branches (keyed on \`!branch.exists\`), delete the branch and check it out again: \`${cli} branches delete ${opts.branchName}\` then \`${cli} checkout ${opts.branchName}${envArg}\`.`
31
+ `Fix the cause above, then run \`${cli} deploy --update-existing${envArg}\` to apply the policy to it — or, if your policy only configures new branches (keyed on \`!branch.exists\`), delete the branch and check it out again: \`${cli} branches delete ${opts.branchName}\` then \`${cli} checkout ${opts.branchName} --create${envArg}\`.`
32
32
  ].join("\n");
33
33
  };
34
34
  const builder = (argv) => argv.usage("$0 checkout [id|name] [options]").positional("id", {
@@ -44,6 +44,11 @@ const builder = (argv) => argv.usage("$0 checkout [id|name] [options]").position
44
44
  type: "boolean",
45
45
  default: true
46
46
  },
47
+ create: {
48
+ describe: "Create the named branch if it does not exist, then check it out",
49
+ type: "boolean",
50
+ default: false
51
+ },
47
52
  ...envFlag,
48
53
  env: {
49
54
  ...envFlag.env,
@@ -52,12 +57,14 @@ const builder = (argv) => argv.usage("$0 checkout [id|name] [options]").position
52
57
  }).example([
53
58
  ["$0 checkout", "Pick a branch interactively from the project in the closest .neon file"],
54
59
  ["$0 checkout main", "Pin the branch named \"main\" in the closest .neon file"],
55
- ["$0 checkout feat --env .env.local", "Create feat from neon.ts, loading Function env from .env.local first"],
60
+ ["$0 checkout dev --create", "Create and pin a missing branch named dev"],
61
+ ["$0 checkout feat --create --env .env.local", "Create feat from neon.ts, loading Function env from .env.local first"],
56
62
  ["$0 checkout br-cool-snow-12345678 --project-id project-id-123", "Pin a branch by id for an explicit project"]
57
63
  ]);
58
64
  const handler = async (props) => {
59
65
  const previousBranch = contextBranch(readContextFile(props.contextFile));
60
66
  if (previousBranch) log.info("%s Currently on branch %s", chalk.dim("→"), chalk.cyan.bold(previousBranch));
67
+ if (props.create && !props.id?.trim()) throw new Error(`No branch specified. Pass a branch name with --create (e.g. \`${getCliName()} checkout dev --create\`).`);
61
68
  const projectId = await resolveProjectId(props);
62
69
  const { branchId, branchName, created, policyApplied, policyFailure } = await resolveBranchId(props, projectId);
63
70
  const orgId = await resolveOrgId(props, projectId);
@@ -137,8 +144,10 @@ const resolveBranchId = async (props, projectId) => {
137
144
  created: false,
138
145
  policyApplied: false
139
146
  };
140
- if (isCi() || !process.stdout.isTTY) throw new Error(notFoundMessage(ref, branches));
141
- log.error(notFoundMessage(ref, branches));
147
+ if (props.create) return createCheckoutBranch(props, projectId, ref, branches);
148
+ const missingName = notFoundMessage(ref, branches, { suggestCreate: true });
149
+ if (isCi() || !process.stdout.isTTY) throw new Error(missingName);
150
+ log.error(missingName);
142
151
  const { create } = await prompts({
143
152
  type: "confirm",
144
153
  name: "create",
@@ -178,7 +187,7 @@ const createCheckoutBranch = async (props, projectId, name, branches) => {
178
187
  policyApplied: false
179
188
  };
180
189
  };
181
- const notFoundMessage = (ref, branches) => `Branch ${ref} not found.\nAvailable branches: ${branches.map((b) => b.name).join(", ")}`;
190
+ const notFoundMessage = (ref, branches, opts) => `Branch ${ref} not found.${opts?.suggestCreate ? " Pass --create to create it." : ""}\nAvailable branches: ${branches.map((b) => b.name).join(", ")}`;
182
191
  /**
183
192
  * Resolve the org id to heal into the context file.
184
193
  *
@@ -8,7 +8,7 @@ import { NEON_SERVICES, deprecatedServiceMessage, parseServices, servicesFlagVal
8
8
  import { resolveEnvFilePath } from "../env_file.js";
9
9
  import { t as credentialInputs } from "../_chunks/auth_selection-pitMFzDe.js";
10
10
  import { ClaimableClient, ClaimableServiceError } from "../claimable/api.js";
11
- import { assertionHasExpired, listClaimableCredentials, readClaimableCredentials, removeClaimableCredentials, resolveClaimableContext, writeClaimableCredentials } from "../claimable/state.js";
11
+ import { assertionHasExpired, listClaimableCredentials, readClaimableCredentials, readLinkedClaimableCredentials, removeClaimableCredentials, writeClaimableCredentials } from "../claimable/state.js";
12
12
  import { writer } from "../writer.js";
13
13
  import { noPassthrough } from "../utils/flags.js";
14
14
  import { pull } from "./env.js";
@@ -184,19 +184,23 @@ const resolveTarget = (props) => {
184
184
  rejectExplicitAccountCredential(props);
185
185
  const requested = props.projectId?.trim();
186
186
  const context = readContextFile(props.contextFile);
187
- const linked = resolveClaimableContext(context);
188
- const projectId = requested || linked?.projectId;
189
- if (projectId === void 0) throw new Error("This directory is not linked to a claimable project. Pass a project id from `neon claim list`, or run `neon claim create` first.");
190
- const credentials = readClaimableCredentials(props.configDir, projectId);
191
- if (credentials === null) throw new Error(`The identity assertion for ${projectId} is missing. The project cannot be managed from this machine; claim it through its existing verification URL or run \`neon link\` after it is claimed.`);
192
- const client = new ClaimableClient(credentials.origin);
193
- const contextMatches = linked?.projectId === projectId;
194
- if (contextMatches && linked !== null && client.origin !== new ClaimableClient(linked.origin).origin) throw new Error("The .neon context and saved identity assertion name different Claimable Neon services. Delete .neon or the assertion file and run `neon claim create` in a new directory.");
187
+ if (requested) {
188
+ const credentials = readClaimableCredentials(props.configDir, requested);
189
+ if (credentials === null) throw new Error(`The identity assertion for ${requested} is missing. The project cannot be managed from this machine; claim it through its existing verification URL or run \`neon link\` after it is claimed.`);
190
+ return {
191
+ projectId: requested,
192
+ credentials,
193
+ client: new ClaimableClient(credentials.origin),
194
+ contextMatches: context.projectId === requested
195
+ };
196
+ }
197
+ const linked = readLinkedClaimableCredentials(props.configDir, context);
198
+ if (linked === null) throw new Error("This directory is not linked to a claimable project. Pass a project id from `neon claim list`, or run `neon claim create` first.");
195
199
  return {
196
- projectId,
197
- credentials,
198
- client,
199
- contextMatches
200
+ projectId: linked.projectId,
201
+ credentials: linked,
202
+ client: new ClaimableClient(linked.origin),
203
+ contextMatches: true
200
204
  };
201
205
  };
202
206
  const requireLiveIdentity = (credentials) => {
@@ -210,7 +214,7 @@ const clearLocalRecord = (props, projectId, contextMatches) => {
210
214
  const create = async (props) => {
211
215
  rejectExplicitAccountCredential(props);
212
216
  const existing = readContextFile(props.contextFile);
213
- if (existing.projectId || existing.orgId || existing.claimable) throw new Error(`${props.contextFile} already links this directory to a Neon project. Run \`neon claim create\` from an unlinked directory.`);
217
+ if (existing.projectId || existing.orgId) throw new Error(`${props.contextFile} already links this directory to a Neon project. Run \`neon claim create\` from an unlinked directory.`);
214
218
  const contextFileExisted = existsSync(props.contextFile);
215
219
  const cwd = props.cwd ?? process.cwd();
216
220
  const envFile = props.envPull ? resolveEnvFilePath(cwd, props.file) : void 0;
@@ -241,11 +245,7 @@ const create = async (props) => {
241
245
  localStateWritten = true;
242
246
  applyContext(props.contextFile, {
243
247
  projectId: registration.project.id,
244
- branch: registration.project.branchId,
245
- claimable: {
246
- version: 1,
247
- origin: client.origin
248
- }
248
+ branch: registration.project.branchId
249
249
  });
250
250
  contextWritten = true;
251
251
  const token = await client.exchange(registration.identityAssertion);
@@ -261,6 +261,7 @@ const create = async (props) => {
261
261
  apiClient,
262
262
  apiKey: token.accessToken,
263
263
  apiHost,
264
+ configDir: props.configDir,
264
265
  contextFile: props.contextFile,
265
266
  output: props.output,
266
267
  projectId: registration.project.id,
@@ -2,7 +2,7 @@ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { isCi } from "../env.js";
3
3
  import { log } from "../log.js";
4
4
  import { getApiClient } from "../api.js";
5
- import { contextBranch, readContextFile } from "../context.js";
5
+ import { contextBranch, ensureDirectoryGitignored, readContextFile } from "../context.js";
6
6
  import { getCliName } from "../utils/cli_name.js";
7
7
  import { toNeonConfigView } from "../config_format.js";
8
8
  import { declaredNeonServices } from "../config_services.js";
@@ -229,6 +229,7 @@ const initCmd = async (props) => {
229
229
  const args = installArgs(pm, missing);
230
230
  if (props.install === false) log.info("Install the Neon config packages to use neon.ts: %s", formatInstallCommand(pm, missing));
231
231
  else {
232
+ ensureDirectoryGitignored(join(cwd, "node_modules"));
232
233
  log.info("Installing %s with %s…", missing.join(", "), pm);
233
234
  if (!await run(pm, args, cwd)) log.warning("Could not install the config packages automatically. Run by hand: %s", formatInstallCommand(pm, missing));
234
235
  }
@@ -89,7 +89,8 @@ const devEnvContext = (props, branchId, cwd) => {
89
89
  ...props.apiHost ? { apiHost: props.apiHost } : {},
90
90
  ...isClaimableEnvTarget({
91
91
  apiHost: props.apiHost,
92
- contextFile: props.contextFile ?? ""
92
+ contextFile: props.contextFile ?? "",
93
+ configDir: props.configDir ?? ""
93
94
  }) ? { claimable: true } : {}
94
95
  };
95
96
  };
@@ -122,7 +122,11 @@ const NEON_OWNED_ENV_KEYS = [
122
122
  ];
123
123
  const pull = async (props, opts = {}) => {
124
124
  const cwd = props.cwd ?? process.cwd();
125
- const claimable = isClaimableEnvTarget(props);
125
+ const claimable = isClaimableEnvTarget({
126
+ apiHost: props.apiHost,
127
+ contextFile: props.contextFile,
128
+ configDir: props.configDir ?? ""
129
+ });
126
130
  const dropped = claimable && (props.services !== void 0 || props.envKeys !== void 0) ? dropClaimableUnsupported(props.services ?? [], props.envKeys ?? []) : null;
127
131
  const selectionServices = dropped ? dropped.services : props.services ?? [];
128
132
  const selectionEnvKeys = dropped ? dropped.envKeys : props.envKeys ?? [];
package/dist/context.js CHANGED
@@ -55,6 +55,12 @@ const isMcpCommand = (args) => args._[0] === "mcp";
55
55
  const isSkillsCommand = (args) => args._[0] === "skills" || args._[0] === "skill";
56
56
  const isPluginsCommand = (args) => args._[0] === "plugins" || args._[0] === "plugin";
57
57
  const isAskCommand = (args) => args._[0] === "ask";
58
+ /**
59
+ * `inspect db --db-url` talks to Postgres with that URI. Skip only when the
60
+ * parsed value is nonempty: empty `--db-url` still goes through project
61
+ * resolution and needs an API client.
62
+ */
63
+ const isInspectDbUrl = (args) => (args._[0] === "inspect" || args._[0] === "inspection") && typeof args.dbUrl === "string" && args.dbUrl.length > 0;
58
64
  /** Raw argv is required because auth middleware runs before MCP flags are parsed. */
59
65
  const isMcpOauth = (args) => isMcpCommand(args) && argvEnablesMcpOauth(process.argv);
60
66
  const OAUTH_FALSE = /* @__PURE__ */ new Set([
@@ -193,21 +199,33 @@ const setContext = (file, context) => {
193
199
  * must not be blocked by a `.gitignore` write error.
194
200
  */
195
201
  const ensureGitignored = (file) => {
202
+ ensureGitignoreEntry(file, basenameOf(file));
203
+ };
204
+ /**
205
+ * Make sure a directory is ignored by the `.gitignore` in its parent. The written entry keeps
206
+ * the conventional trailing slash, while coverage checks also recognize an existing entry
207
+ * without one.
208
+ */
209
+ const ensureDirectoryGitignored = (directory) => {
210
+ const normalized = directory.replace(/[\\/]+$/, "");
211
+ ensureGitignoreEntry(directory, `${basenameOf(normalized)}/`);
212
+ };
213
+ const ensureGitignoreEntry = (path, entry) => {
196
214
  try {
197
- const dir = dirname(file);
198
- const entry = basenameOf(file);
215
+ const dir = dirname(path);
199
216
  const gitignorePath = resolve(dir, GITIGNORE_FILE);
217
+ const coveredEntry = entry.replace(/\/$/, "");
200
218
  if (!existsSync(gitignorePath)) {
201
219
  writeFileSync(gitignorePath, `${entry}\n`);
202
220
  return;
203
221
  }
204
222
  const current = readFileSync(gitignorePath, "utf-8");
205
- if (hasGitignoreEntry(current, entry)) return;
223
+ if (hasGitignoreEntry(current, coveredEntry)) return;
206
224
  const addition = `${current.length > 0 && !current.endsWith("\n") ? "\n" : ""}${entry}\n`;
207
225
  writeFileSync(gitignorePath, current + addition);
208
226
  } catch (err) {
209
227
  const message = err instanceof Error ? err.message : String(err);
210
- log.debug("Failed to update .gitignore next to %s: %s", file, message);
228
+ log.debug("Failed to update .gitignore next to %s: %s", path, message);
211
229
  }
212
230
  };
213
231
  const basenameOf = (file) => {
@@ -242,4 +260,4 @@ const globToRegExp = (pattern) => {
242
260
  return new RegExp(`^${source}$`);
243
261
  };
244
262
  //#endregion
245
- export { applyContext, contextBranch, currentContextFile, enrichFromContext, ensureGitignored, isApiKeysCommand, isAskCommand, isClaimCommand, isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand, readContextFile, setContext, updateContextFile, walkContextFile };
263
+ export { applyContext, contextBranch, currentContextFile, enrichFromContext, ensureDirectoryGitignored, ensureGitignored, isApiKeysCommand, isAskCommand, isClaimCommand, isConfigInit, isCurrentBranchProbe, isInspectDbUrl, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand, readContextFile, setContext, updateContextFile, walkContextFile };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "4.14.5",
3
+ "version": "4.15.0",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -63,9 +63,9 @@
63
63
  "yaml": "^2.9.0",
64
64
  "yargs": "17.7.2",
65
65
  "yoctocolors": "^2.1.2",
66
- "@neon/config-runtime": "1.2.3",
67
- "@neon/sdk": "4.0.0",
68
- "@neon/config": "1.3.2"
66
+ "@neon/config": "1.3.3",
67
+ "@neon/config-runtime": "1.2.4",
68
+ "@neon/sdk": "4.1.0"
69
69
  },
70
70
  "optionalDependencies": {
71
71
  "@napi-rs/keyring": "1.3.0",
@@ -97,8 +97,8 @@
97
97
  "tsx": "4.22.3",
98
98
  "typescript": "^5.9.0",
99
99
  "vitest": "^3.0.9",
100
- "@neon-internals/env-core": "0.0.10",
101
100
  "@neon-internals/cli-core": "0.0.0",
101
+ "@neon-internals/env-core": "0.0.11",
102
102
  "@neon/e2e-harness": "0.0.0",
103
103
  "@neon/functions": "0.9.0"
104
104
  },