neon 4.14.6 → 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
@@ -440,8 +440,8 @@ The branch argument is **optional**: run `neon checkout` with no branch in an in
440
440
 
441
441
  Branch **id vs name** is detected automatically (a `br-…` value is treated as an id):
442
442
 
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).
444
- - **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`.
445
445
 
446
446
  The project is resolved through the standard neon chain, each entry winning over the next:
447
447
 
@@ -457,11 +457,15 @@ The resolved branch is then written (by name) to the same `.neon` file `link` us
457
457
  $ neon checkout main --project-id polished-snowflake-12345678
458
458
  INFO: Checked out branch br-main-branch-87654321 on project polished-snowflake-12345678. Updated /path/to/cwd/.neon.
459
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
+
460
464
  $ cat .neon
461
465
  {
462
466
  "orgId": "org-abc123",
463
467
  "projectId": "polished-snowflake-12345678",
464
- "branch": "main"
468
+ "branch": "dev"
465
469
  }
466
470
  ```
467
471
 
@@ -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
  *
@@ -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
  }
package/dist/context.js CHANGED
@@ -199,21 +199,33 @@ const setContext = (file, context) => {
199
199
  * must not be blocked by a `.gitignore` write error.
200
200
  */
201
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) => {
202
214
  try {
203
- const dir = dirname(file);
204
- const entry = basenameOf(file);
215
+ const dir = dirname(path);
205
216
  const gitignorePath = resolve(dir, GITIGNORE_FILE);
217
+ const coveredEntry = entry.replace(/\/$/, "");
206
218
  if (!existsSync(gitignorePath)) {
207
219
  writeFileSync(gitignorePath, `${entry}\n`);
208
220
  return;
209
221
  }
210
222
  const current = readFileSync(gitignorePath, "utf-8");
211
- if (hasGitignoreEntry(current, entry)) return;
223
+ if (hasGitignoreEntry(current, coveredEntry)) return;
212
224
  const addition = `${current.length > 0 && !current.endsWith("\n") ? "\n" : ""}${entry}\n`;
213
225
  writeFileSync(gitignorePath, current + addition);
214
226
  } catch (err) {
215
227
  const message = err instanceof Error ? err.message : String(err);
216
- 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);
217
229
  }
218
230
  };
219
231
  const basenameOf = (file) => {
@@ -248,4 +260,4 @@ const globToRegExp = (pattern) => {
248
260
  return new RegExp(`^${source}$`);
249
261
  };
250
262
  //#endregion
251
- export { applyContext, contextBranch, currentContextFile, enrichFromContext, ensureGitignored, isApiKeysCommand, isAskCommand, isClaimCommand, isConfigInit, isCurrentBranchProbe, isInspectDbUrl, 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.6",
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/config": "1.3.2",
68
- "@neon/sdk": "4.0.0"
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",
@@ -98,8 +98,8 @@
98
98
  "typescript": "^5.9.0",
99
99
  "vitest": "^3.0.9",
100
100
  "@neon-internals/cli-core": "0.0.0",
101
+ "@neon-internals/env-core": "0.0.11",
101
102
  "@neon/e2e-harness": "0.0.0",
102
- "@neon-internals/env-core": "0.0.10",
103
103
  "@neon/functions": "0.9.0"
104
104
  },
105
105
  "publishConfig": {