neon 2.45.0 → 2.46.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.
Files changed (52) hide show
  1. package/README.md +62 -1
  2. package/dist/_shared/paths.js +3 -4
  3. package/dist/commands/bootstrap.js +12 -11
  4. package/dist/commands/checkout.js +7 -6
  5. package/dist/commands/config.js +2 -1
  6. package/dist/commands/data_api.js +4 -3
  7. package/dist/commands/env.js +5 -4
  8. package/dist/commands/functions.js +3 -2
  9. package/dist/commands/init.js +82 -37
  10. package/dist/commands/ip_allow.js +3 -2
  11. package/dist/commands/link.js +17 -16
  12. package/dist/commands/projects.js +3 -2
  13. package/dist/commands/set_context.js +5 -4
  14. package/dist/current_branch_fast_path.js +2 -1
  15. package/dist/dev/env.js +8 -7
  16. package/dist/index.js +2 -2
  17. package/dist/init/agents.js +127 -0
  18. package/dist/init/auth.js +77 -0
  19. package/dist/init/bootstrap.js +448 -0
  20. package/dist/init/build_config.js +2 -0
  21. package/dist/init/detect_agent.js +108 -0
  22. package/dist/init/editors.js +62 -0
  23. package/dist/init/enrich_output.js +71 -0
  24. package/dist/init/extension.js +191 -0
  25. package/dist/init/inspect.js +287 -0
  26. package/dist/init/interactive.js +651 -0
  27. package/dist/init/neonctl.js +184 -0
  28. package/dist/init/orchestrate.js +190 -0
  29. package/dist/init/phases/auth.js +209 -0
  30. package/dist/init/phases/cleanup.js +27 -0
  31. package/dist/init/phases/db.js +283 -0
  32. package/dist/init/phases/getting_started.js +228 -0
  33. package/dist/init/phases/mcp.js +227 -0
  34. package/dist/init/phases/migrations.js +251 -0
  35. package/dist/init/phases/neon_auth.js +135 -0
  36. package/dist/init/phases/setup.js +729 -0
  37. package/dist/init/phases/skills.js +89 -0
  38. package/dist/init/phases/status.js +70 -0
  39. package/dist/init/resolve_context.js +107 -0
  40. package/dist/init/route_command.js +100 -0
  41. package/dist/init/skills.js +248 -0
  42. package/dist/init/types.js +1 -0
  43. package/dist/init/vsix.js +111 -0
  44. package/dist/psql/command/cmd_meta.js +2 -2
  45. package/dist/psql/core/mainloop.js +1 -1
  46. package/dist/psql/core/startup.js +1 -1
  47. package/dist/psql/core/syncVars.js +3 -3
  48. package/dist/psql/index.js +1 -1
  49. package/dist/utils/cli_name.js +14 -0
  50. package/dist/utils/esbuild.js +1 -1
  51. package/dist/utils/write_sync.js +39 -0
  52. package/package.json +18 -12
package/README.md CHANGED
@@ -593,6 +593,66 @@ $ neon bootstrap . --template hono
593
593
 
594
594
  The target directory must be empty unless you pass `--force` (a lone `.git` is ignored, so a freshly `git init`ed folder is fine). Symlinks and executable bits in the template are preserved.
595
595
 
596
+ ## Set up a project for your coding agent (`init`)
597
+
598
+ `neon init` wires an existing project up to Neon: it signs you in, installs the Neon MCP server and agent skills into your editor, adds the Neon Local Connect extension for VS Code and Cursor, creates or picks a project, writes `DATABASE_URL` into `.env`, and offers to scaffold migrations.
599
+
600
+ ```bash
601
+ $ neon init
602
+ ```
603
+
604
+ Run in a terminal it prompts you through those steps. This is what the retired `neon-init` package used to do; `npx neon init` replaces it.
605
+
606
+ Two side effects worth knowing before you run it. It **installs or upgrades `neonctl` globally**, with whichever package manager invoked it — the flow drives Neon by shelling out to the CLI rather than calling the API in-process. And it **writes `.neon`** in the project directory, the same context file `neon link` and `neon checkout` use.
607
+
608
+ ### Agent mode
609
+
610
+ `--agent` turns the same flow into a state machine an AI coding assistant drives. It prints **one JSON object on stdout** and nothing else: a phase response carrying a `status` and a `nextAction` telling the agent what to do next — usually another `neon init` invocation, spelled out as a `command`. The two read-only steps, `status` and `finalize`, return a snapshot instead.
611
+
612
+ ```bash
613
+ $ neon init --agent --data '{"step":"status"}'
614
+ {
615
+ "auth": { "authenticated": true },
616
+ "tooling": { "mcpServer": { "configured": true, "scope": "global" }, "skills": { "installed": false, "scope": null } },
617
+ "project": { "databaseUrl": false },
618
+ "migrations": { "tool": "prisma", "hasMigrations": false },
619
+ "recommendations": [
620
+ { "priority": "high", "message": "No DATABASE_URL found in .env", "command": "neon init --agent --data '{\"step\":\"db\"}'" },
621
+ { "priority": "medium", "message": "Neon agent skills not detected in this project", "command": "neon init --agent --data '{\"step\":\"skills\",\"install\":true}'" },
622
+ { "priority": "medium", "message": "prisma detected but no migrations found", "command": "neon init --agent --data '{\"step\":\"migrations\"}'" }
623
+ ]
624
+ }
625
+ ```
626
+
627
+ `--agent` is implied when stdin is not a TTY and a known agent is detected from the environment (Claude Code, Codex, Cline, Cursor, VS Code, Windsurf).
628
+
629
+ `--data` takes a JSON object whose `step` selects the phase: `auth`, `db`, `setup`, `getting-started`, `mcp`, `skills`, `migrations`, `neon-auth`, `status`, or `finalize`. Remaining keys are that phase's options. Without `--data`, the orchestrator picks the next phase itself. An unrecognised `step` is refused with the full list.
630
+
631
+ **Failures are JSON too**, so an agent never has to distinguish "it broke" from "it returned nothing":
632
+
633
+ ```bash
634
+ $ neon init --agent --data '{not json'
635
+ {
636
+ "success": false,
637
+ "error": "Invalid JSON in --data flag at position 1. Expected a JSON object."
638
+ }
639
+ $ echo $?
640
+ 1
641
+ ```
642
+
643
+ That message reports where parsing stopped and nothing more. `--data` carries whatever you put in it, and the JSON parser's own message quotes a window of the input, so echoing either would put a connection string or an API key on stdout.
644
+
645
+ **One exception to "JSON on stdout".** Credentials are resolved before any command runs, so a failure in that step — an unknown `--profile` or `NEON_PROFILE`, `--api-key` and `--profile` together, or a `credentials.json` that cannot be read — prints `ERROR: …` on stderr, leaves stdout empty, and exits 1. Treat a non-zero exit with empty stdout as a credential problem and read stderr.
646
+
647
+ | Option | |
648
+ | --- | --- |
649
+ | `--agent`, `-a` | Emit the JSON state machine instead of prompting |
650
+ | `--data <json>` | Route to one phase, with that phase's options |
651
+ | `--skip-migrations` | Leave the migrations phase out of the flow |
652
+ | `--preview` | Enable preview features (scaffolding a project from a template) |
653
+
654
+ `neon init` refuses `--profile`; see [Which credential an invocation uses](#which-credential-an-invocation-uses).
655
+
596
656
  ## Snapshots (`snapshots`)
597
657
 
598
658
  `neon snapshots` (alias `neon snapshot`) manages **snapshots** — point-in-time backups of a branch that you can list, rename, expire, restore into a branch, or schedule automatically. Snapshots are a Beta Neon feature and were previously only available in the Console and REST API; this command group brings them to the CLI.
@@ -776,7 +836,7 @@ When both are only environment variables the key wins, which keeps a CI pipeline
776
836
 
777
837
  `neon auth` and the `profile` subcommands are outside all of this, because they read the same flags to mean something else: `neon auth --profile work` names where to write a credential, and `neon profile create work --api-key …` names one to store.
778
838
 
779
- `neon init` does not support `--profile` yet. It hands its whole auth flow to `neon-init`, which reads the default credentials directly, so passing the flag fails instead of quietly running as the default account.
839
+ `neon init` does not support `--profile` yet. It runs its own auth flow, which reads the default credentials directly and re-invokes the CLI as a subprocess without passing a profile down, so passing the flag fails instead of quietly running as the default account. `--api-key` and `NEON_API_KEY` reach `neon init` no better and are **not** refused. The flow reads the stored credential and nothing else, so a supplied key is ignored: with a credential on disk `neon init` runs silently as *that* account, and with none it sends you to a browser sign-in. The silent case is the one to watch — it is the same failure `--profile` is refused for, without the refusal. Until that is fixed, sign in as the account you want first, or use another command.
780
840
 
781
841
  ## API keys (`api-keys`)
782
842
 
@@ -876,6 +936,7 @@ API keys in org-7
876
936
  | config | `init`, `status`, `plan`, `apply` | Drive a branch from `neon.ts` |
877
937
  | deploy | | Alias for `config apply` |
878
938
  | bootstrap | | Scaffold a project from a template |
939
+ | init | | Set up a project for a coding agent |
879
940
  | bucket | `create`, `list`, `delete`, `object list`, `object get`, `object put`, `object delete` (incl. `--recursive`) | Manage buckets and their objects |
880
941
  | [completion](https://neon.com/docs/reference/cli-completion) | | Generate a completion script |
881
942
 
@@ -4,13 +4,12 @@
4
4
  **Deliberately impure.** It reads environment variables and touches the filesystem, which
5
5
  * `@neon/config` — the package this used to be a subpath of — must never do from its root
6
6
  * export. It lives here instead of there precisely so that a policy-facing package does not
7
- * carry implementor-only code, and so `neon-init`, which has no workspace dependencies, can use
8
- * the same resolution as everything else.
7
+ * carry implementor-only code.
9
8
  *
10
9
  * It exists because three separate readers each grew their own answer to "where is the
11
10
  * config directory", and all three disagreed: `packages/cli` honoured `XDG_CONFIG_HOME` but
12
- * not `NEONCTL_CONFIG_DIR`, `packages/env` honoured the env var but not XDG, and
13
- * `packages/init` hardcoded `~/.config/neonctl`. With `XDG_CONFIG_HOME` set, the CLI wrote
11
+ * not `NEONCTL_CONFIG_DIR`, `packages/env` honoured the env var but not XDG, and the init
12
+ * flow hardcoded `~/.config/neonctl`. With `XDG_CONFIG_HOME` set, the CLI wrote
14
13
  * credentials somewhere the other two never looked.
15
14
  *
16
15
  * ## The directory
@@ -1,11 +1,12 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join, relative, resolve } from "node:path";
3
3
  import chalk from "chalk";
4
- import { BootstrapInputError, ensureTargetUsable, FALLBACK_TEMPLATES, fetchTemplates, findTemplate, scaffoldTemplate, templateIds, } from "neon-init/bootstrap";
5
4
  import prompts from "prompts";
6
5
  import { credentialInputs } from "../_shared/auth_selection.js";
7
6
  import { isCi } from "../env.js";
7
+ import { BootstrapInputError, ensureTargetUsable, FALLBACK_TEMPLATES, fetchTemplates, findTemplate, scaffoldTemplate, templateIds, } from "../init/bootstrap.js";
8
8
  import { log } from "../log.js";
9
+ import { getCliName } from "../utils/cli_name.js";
9
10
  import { detectPackageManager, installedPackageManagers, runCommand, } from "../utils/package_manager.js";
10
11
  // The directory positional is optional: omitting it in an interactive terminal
11
12
  // prompts for one. In a non-interactive context a missing directory is an error.
@@ -55,7 +56,7 @@ export const builder = (argv) => argv
55
56
  default: true,
56
57
  },
57
58
  link: {
58
- describe: "Run `neon link` in the scaffolded directory after installing. In interactive mode this is offered as a prompt; use --no-link to skip without being asked.",
59
+ describe: `Run \`${getCliName()} link\` in the scaffolded directory after installing. In interactive mode this is offered as a prompt; use --no-link to skip without being asked.`,
59
60
  type: "boolean",
60
61
  default: true,
61
62
  },
@@ -160,7 +161,7 @@ const resolveTargetDir = async (props, interactive, template) => {
160
161
  return resolve(process.cwd(), defaultDirName(template));
161
162
  }
162
163
  if (!interactive) {
163
- throw new Error('No target directory given. Pass one, e.g. `neon bootstrap my-app` (or "." for the current directory).');
164
+ throw new Error(`No target directory given. Pass one, e.g. \`${getCliName()} bootstrap my-app\` (or "." for the current directory).`);
164
165
  }
165
166
  const { value } = await prompts({
166
167
  onState: onPromptState,
@@ -179,7 +180,7 @@ const resolveTargetDir = async (props, interactive, template) => {
179
180
  const defaultDirName = (template) => template.source.subdir.split("/").pop() || template.id;
180
181
  /**
181
182
  * Download and materialize the template into `targetDir`. The actual
182
- * download/extract/write lives in the shared `neon-init/bootstrap` core
183
+ * download/extract/write lives in `src/init/bootstrap.ts`, shared with `neon init`
183
184
  * (exec-bit and symlink fidelity, graceful symlink fallback); here we just
184
185
  * frame it with progress logging. Returns the number of files written.
185
186
  */
@@ -238,11 +239,11 @@ const runPostScaffoldSteps = async (props, targetDir, interactive) => {
238
239
  // and tell the user how to finish by hand.
239
240
  if (props.link) {
240
241
  if (!installed && hasNeonConfig(targetDir)) {
241
- log.info("Skipping the Neon link step: `neon link` reads this project's neon.ts " +
242
+ log.info(`Skipping the Neon link step: \`${getCliName()} link\` reads this project's neon.ts ` +
242
243
  `to pull env vars, which needs its dependencies. Run \`${pm} install\`, ` +
243
- "then `neon link`.");
244
+ `then \`${getCliName()} link\`.`);
244
245
  }
245
- else if (await confirm("Link this project to a Neon project now? (runs neon link)")) {
246
+ else if (await confirm(`Link this project to a Neon project now? (runs ${getCliName()} link)`)) {
246
247
  await runNeonLink(props, targetDir);
247
248
  // link prints its own summary (and pulls env), so end with just the run hint.
248
249
  printNextSteps(targetDir, pm, { installed, suggestLink: false });
@@ -366,7 +367,7 @@ const printNextSteps = (targetDir, pm, opts) => {
366
367
  log.info(" %s install", pm);
367
368
  }
368
369
  if (opts.suggestLink) {
369
- log.info(" neon link");
370
+ log.info(` ${getCliName()} link`);
370
371
  }
371
372
  log.info(" See the README to run it.");
372
373
  log.info("");
@@ -400,7 +401,7 @@ const runAgent = async (props) => {
400
401
  description: template.description,
401
402
  ...(template.services ? { services: template.services } : {}),
402
403
  })),
403
- next_command_template: `neon bootstrap --agent ${props.directory ? shellArg(props.directory) : "<directory>"} --template <template_id>`,
404
+ next_command_template: `${getCliName()} bootstrap --agent ${props.directory ? shellArg(props.directory) : "<directory>"} --template <template_id>`,
404
405
  });
405
406
  return;
406
407
  }
@@ -413,7 +414,7 @@ const runAgent = async (props) => {
413
414
  emitAgent({
414
415
  status: "needs_directory",
415
416
  instruction: 'Ask the user which directory to scaffold into (use "." for the current directory), then re-run the next_command_template with it.',
416
- next_command_template: `neon bootstrap --agent <directory> --template ${shellArg(template.id)}`,
417
+ next_command_template: `${getCliName()} bootstrap --agent <directory> --template ${shellArg(template.id)}`,
417
418
  });
418
419
  return;
419
420
  }
@@ -441,7 +442,7 @@ const runAgent = async (props) => {
441
442
  {
442
443
  action: "link_neon_project",
443
444
  instruction: "Ask the user whether to link the project to a Neon project now. This runs the link state machine — follow its JSON output for the next step.",
444
- command: `${runIn}neon link --agent`,
445
+ command: `${runIn}${getCliName()} link --agent`,
445
446
  },
446
447
  ],
447
448
  message: `Scaffolded "${template.title}" (${filesWritten} files) into ${dir}. Offer the next_steps to the user: install dependencies, initialize git, then link a Neon project.`,
@@ -5,6 +5,7 @@ import { applyContext, contextBranch, readContextFile } from "../context.js";
5
5
  import { isCi } from "../env.js";
6
6
  import { log } from "../log.js";
7
7
  import { createBranch, pickBranchInteractively, } from "../utils/branch_picker.js";
8
+ import { getCliName } from "../utils/cli_name.js";
8
9
  import { fillSingleProject } from "../utils/enrichers.js";
9
10
  import { looksLikeBranchId } from "../utils/formats.js";
10
11
  import { applyPolicyOnCreate, createBranchFromPolicyOnCheckout, } from "./config.js";
@@ -104,8 +105,8 @@ export const handler = async (props) => {
104
105
  if (failure) {
105
106
  throw new Error([
106
107
  `Branch ${branchName} (${branchId}) was created and checked out, but applying neon.ts to it failed: ${failure}`,
107
- "The branch is usable but does not match the policy, and `neonctl checkout` never reconciles a branch that already exists.",
108
- `Fix the cause above, then run \`neonctl deploy --update-existing\` 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: \`neonctl branches delete ${branchName}\` then \`neonctl checkout ${branchName}\`.`,
108
+ `The branch is usable but does not match the policy, and \`${getCliName()} checkout\` never reconciles a branch that already exists.`,
109
+ `Fix the cause above, then run \`${getCliName()} deploy --update-existing\` 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: \`${getCliName()} branches delete ${branchName}\` then \`${getCliName()} checkout ${branchName}\`.`,
109
110
  ].join("\n"));
110
111
  }
111
112
  };
@@ -129,7 +130,7 @@ const resolveBranchId = async (props, projectId) => {
129
130
  if (!props.id) {
130
131
  const picked = await pickBranchInteractively(branches, {
131
132
  message: "Which branch would you like to check out?",
132
- nonInteractiveMessage: "No branch specified. Pass a branch name or id (e.g. `neonctl checkout main`), " +
133
+ nonInteractiveMessage: `No branch specified. Pass a branch name or id (e.g. \`${getCliName()} checkout main\`), ` +
133
134
  "or run interactively to pick one from a list.",
134
135
  });
135
136
  if (picked.kind === "existing") {
@@ -266,7 +267,7 @@ const resolveProjectId = async (props) => {
266
267
  }
267
268
  const missingProjectMessage = "Could not determine which Neon project to check out a branch from. " +
268
269
  "Provide one via the --project-id flag " +
269
- "or a .neon file (created by `neonctl link` / `neonctl set-context`).";
270
+ `or a .neon file (created by \`${getCliName()} link\` / \`${getCliName()} set-context\`).`;
270
271
  if (isCi() || !process.stdout.isTTY) {
271
272
  throw new Error(missingProjectMessage);
272
273
  }
@@ -274,7 +275,7 @@ const resolveProjectId = async (props) => {
274
275
  const { runLink } = await prompts({
275
276
  type: "confirm",
276
277
  name: "runLink",
277
- message: "Run `neonctl link` in the current folder to pick a project now?",
278
+ message: `Run \`${getCliName()} link\` in the current folder to pick a project now?`,
278
279
  initial: true,
279
280
  });
280
281
  if (!runLink) {
@@ -289,7 +290,7 @@ const resolveProjectId = async (props) => {
289
290
  });
290
291
  const linked = readContextFile(props.contextFile);
291
292
  if (!linked.projectId) {
292
- throw new Error("Linking did not produce a project id. Re-run `neonctl checkout` once the directory is linked.");
293
+ throw new Error(`Linking did not produce a project id. Re-run \`${getCliName()} checkout\` once the directory is linked.`);
293
294
  }
294
295
  // Carry the freshly-linked org id forward so the merge below keeps it.
295
296
  if (linked.orgId) {
@@ -12,6 +12,7 @@ import { loadEnvFileIntoProcess } from "../env_file.js";
12
12
  import { log } from "../log.js";
13
13
  import { assertAiGatewayProvisionable, warnAiGateway, } from "../utils/ai_gateway_notice.js";
14
14
  import { announceTargetBranch } from "../utils/branch_notice.js";
15
+ import { getCliName } from "../utils/cli_name.js";
15
16
  import { renderAppliedChanges, renderBranchSettingConflicts, } from "../utils/config_diff.js";
16
17
  import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
17
18
  import { bundleEntry } from "../utils/esbuild.js";
@@ -354,7 +355,7 @@ export const status = async (props) => {
354
355
  else {
355
356
  // No branch pinned: hint on stderr and exit non-zero (grep-style) so a prompt's
356
357
  // `when` hides the segment cleanly instead of rendering a bare icon.
357
- log.info("No branch pinned. Run `neonctl checkout <branch>` to pin a branch and pull its env vars.");
358
+ log.info(`No branch pinned. Run \`${getCliName()} checkout <branch>\` to pin a branch and pull its env vars.`);
358
359
  process.exitCode = 1;
359
360
  }
360
361
  return;
@@ -1,5 +1,6 @@
1
1
  import { isNeonApiError, retryOnLock } from "../api.js";
2
2
  import { log } from "../log.js";
3
+ import { getCliName } from "../utils/cli_name.js";
3
4
  import { branchIdFromProps, fillSingleProject, resolveSingleDatabase, } from "../utils/enrichers.js";
4
5
  import { writer } from "../writer.js";
5
6
  const SETTINGS_FIELDS = [
@@ -221,7 +222,7 @@ const update = async (props) => {
221
222
  }
222
223
  catch (err) {
223
224
  if (isNeonApiError(err) && err.status === 404) {
224
- throw new Error(`Data API is not provisioned for ${database} on branch ${branchId}. Run \`neonctl data-api create\` first.`);
225
+ throw new Error(`Data API is not provisioned for ${database} on branch ${branchId}. Run \`${getCliName()} data-api create\` first.`);
225
226
  }
226
227
  throw err;
227
228
  }
@@ -238,7 +239,7 @@ const update = async (props) => {
238
239
  }
239
240
  catch (err) {
240
241
  if (isNeonApiError(err) && err.status === 404) {
241
- throw new Error(`Data API is not provisioned for ${database} on branch ${branchId}. Run \`neonctl data-api create\` first.`);
242
+ throw new Error(`Data API is not provisioned for ${database} on branch ${branchId}. Run \`${getCliName()} data-api create\` first.`);
242
243
  }
243
244
  throw err;
244
245
  }
@@ -257,7 +258,7 @@ const refreshSchema = async (props) => {
257
258
  }
258
259
  catch (err) {
259
260
  if (isNeonApiError(err) && err.status === 404) {
260
- throw new Error(`Data API is not provisioned for ${database} on branch ${branchId}. Run \`neonctl data-api create\` first.`);
261
+ throw new Error(`Data API is not provisioned for ${database} on branch ${branchId}. Run \`${getCliName()} data-api create\` first.`);
261
262
  }
262
263
  throw err;
263
264
  }
@@ -7,6 +7,7 @@ import { mergeEnvFile, readEnvFile, resolveEnvFilePath } from "../env_file.js";
7
7
  import { log } from "../log.js";
8
8
  import { warnAiGateway } from "../utils/ai_gateway_notice.js";
9
9
  import { announceTargetBranch } from "../utils/branch_notice.js";
10
+ import { getCliName } from "../utils/cli_name.js";
10
11
  import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
11
12
  export const command = "env";
12
13
  export const describe = "Manage a branch's Neon env variables locally";
@@ -15,7 +16,7 @@ export const describe = "Manage a branch's Neon env variables locally";
15
16
  * `--no-env-pull`. Names the two ways to get the branch's vars without an on-disk file written
16
17
  * eagerly: an explicit `neonctl env pull`, or runtime injection via `neon-env run`.
17
18
  */
18
- export const ENV_PULL_SKIPPED_HINT = "Skipped env pull (--no-env-pull). Run `neonctl env pull` to write this branch’s env vars " +
19
+ export const ENV_PULL_SKIPPED_HINT = `Skipped env pull (--no-env-pull). Run \`${getCliName()} env pull\` to write this branch’s env vars ` +
19
20
  "(DATABASE_URL, …) into a local .env, or inject them at runtime with `neon-env run -- <your dev command>`.";
20
21
  export const builder = (argv) => argv
21
22
  .usage("$0 env <sub-command> [options]")
@@ -165,7 +166,7 @@ export const autoPullEnvAfterPin = async (props) => {
165
166
  catch (err) {
166
167
  const message = err instanceof Error ? err.message : String(err);
167
168
  log.warning("Branch pinned, but pulling its Neon env vars failed: %s\n" +
168
- "Run `neonctl env pull` once resolved (e.g. `neonctl deploy` if a declared service " +
169
+ `Run \`${getCliName()} env pull\` once resolved (e.g. \`${getCliName()} deploy\` if a declared service ` +
169
170
  "is missing), or inject them at runtime with `neon-env run -- <your dev command>`.", message);
170
171
  return { status: "failed", message };
171
172
  }
@@ -187,10 +188,10 @@ export const renderAgentPullNote = (result) => {
187
188
  case "empty":
188
189
  return " No Neon env vars to pull for this branch yet.";
189
190
  case "skipped":
190
- return (" Skipped env pull (--no-env-pull); run `neonctl env pull` later, " +
191
+ return (` Skipped env pull (--no-env-pull); run \`${getCliName()} env pull\` later, ` +
191
192
  "or inject env at runtime with `neon-env run -- <your dev command>`.");
192
193
  case "failed":
193
- return ` Could not pull env vars (${result.message}); run \`neonctl env pull\` once resolved.`;
194
+ return ` Could not pull env vars (${result.message}); run \`${getCliName()} env pull\` once resolved.`;
194
195
  }
195
196
  };
196
197
  /**
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
  import { isNeonApiError, retryOnLock } from "../api.js";
4
4
  import { createDeployment, deleteFunction, getFunction, listFunctions, } from "../functions_api.js";
5
5
  import { log } from "../log.js";
6
+ import { getCliName } from "../utils/cli_name.js";
6
7
  import { branchIdFromProps, fillSingleProject } from "../utils/enrichers.js";
7
8
  import { bundleEntry } from "../utils/esbuild.js";
8
9
  import { zipBundle } from "../utils/zip.js";
@@ -146,7 +147,7 @@ const parseEnv = (entries) => {
146
147
  }
147
148
  return JSON.stringify(map);
148
149
  };
149
- const statusHint = (slug, projectId, branchId) => `Check status with: neonctl function get ${slug} --project-id ${projectId} --branch ${branchId}`;
150
+ const statusHint = (slug, projectId, branchId) => `Check status with: ${getCliName()} function get ${slug} --project-id ${projectId} --branch ${branchId}`;
150
151
  // Emit the resolved deployment together with the function's invocation_url, so the
151
152
  // deploy output shows where the function is reachable (not just the deployment id).
152
153
  const emitDeployResult = (props, deployment, fn) => {
@@ -172,7 +173,7 @@ const deploy = async (props) => {
172
173
  props.runtime !== undefined;
173
174
  if (!hasOption) {
174
175
  throw new Error("Provide at least one option to deploy, e.g. --src or --env. " +
175
- "See: neonctl function deploy --help.");
176
+ `See: ${getCliName()} function deploy --help.`);
176
177
  }
177
178
  // Cheap, offline validation first - fail before any network round-trip.
178
179
  if (!SLUG_PATTERN.test(props.slug)) {
@@ -1,7 +1,11 @@
1
- import { detectAgent, enrichResponse, interactiveInit, orchestrate, routeDataStep, } from "neon-init";
2
1
  import { credentialInputs } from "../_shared/auth_selection.js";
3
- import { sendError } from "../analytics.js";
4
- import { log } from "../log.js";
2
+ import { closeAnalytics, sendError } from "../analytics.js";
3
+ import { detectAgent } from "../init/detect_agent.js";
4
+ import { enrichResponse } from "../init/enrich_output.js";
5
+ import { interactiveInit } from "../init/interactive.js";
6
+ import { orchestrate } from "../init/orchestrate.js";
7
+ import { routeDataStep } from "../init/route_command.js";
8
+ import { STDOUT_FD, writeAllSync } from "../utils/write_sync.js";
5
9
  export const command = "init";
6
10
  export const describe = "Initialize a project with Neon using your AI coding assistant";
7
11
  export const builder = (yargs) => yargs
@@ -29,62 +33,103 @@ export const builder = (yargs) => yargs
29
33
  describe: "Enable preview features (e.g. project bootstrapping from templates).",
30
34
  })
31
35
  .strict(false);
36
+ /**
37
+ * The agent-facing half of `neon init` speaks JSON, and it speaks it on **stdout**:
38
+ * one object, no prefix, nothing else. `log.info` would prefix every line with
39
+ * `INFO: ` and send it to stderr, which is right for a diagnostic and wrong for the
40
+ * payload an agent is expected to parse.
41
+ */
42
+ const writeAgentResponse = (result) => {
43
+ writeAllSync(STDOUT_FD, `${JSON.stringify(enrichResponse(result), null, 2)}\n`);
44
+ };
45
+ /**
46
+ * A failure has to arrive in the shape the caller asked for. An agent parses stdout and
47
+ * has no branch for "empty stdout, exit 1" — it cannot tell a broken credentials file
48
+ * from a phase that legitimately produced nothing — so the error goes out as JSON too.
49
+ */
50
+ const writeAgentFailure = (error) => {
51
+ writeAllSync(STDOUT_FD, `${JSON.stringify({ success: false, error: error.message }, null, 2)}\n`);
52
+ };
53
+ /** ` at position 12`, or nothing when the parser did not report one. */
54
+ const parsePosition = (parseError) => {
55
+ const message = parseError instanceof Error ? parseError.message : "";
56
+ const at = message.match(/at position (\d+)/);
57
+ return at === null ? "" : ` at position ${at[1]}`;
58
+ };
32
59
  export const handler = async (argv) => {
33
- // `init` delegates its whole auth flow to `neon-init`, which reads the default credentials
34
- // directly and re-invokes the CLI as a subprocess. It has no way to be told which profile
35
- // to use, so honouring a selection here is not possible yet — and silently running as the
36
- // default account would be worse than refusing, because naming an account is the entire
37
- // job of the thing being ignored.
60
+ // Auto-detect agent from environment. When --agent is explicitly passed,
61
+ // always detect (the user asked for agent mode). Otherwise, require
62
+ // non-TTY stdin to distinguish agent from human in terminal.
38
63
  //
39
- // `NEON_PROFILE` counts just as much as the flag. Checking only the flag left the case that
40
- // is easier to hit by accident: a profile exported once into a shell then silently
41
- // disregarded by every `neon init` run in it.
42
- const selectedProfile = argv.profile?.trim() || credentialInputs().profileEnv.trim();
43
- if (selectedProfile) {
44
- const how = argv.profile?.trim()
45
- ? "--profile was passed, so"
46
- : "NEON_PROFILE is set, so";
47
- throw new Error(`${how} \`neon init\` would run as the default account instead of "${selectedProfile}", and it does not support profile selection yet. Run it without one, or set the project up with \`neon --profile ${selectedProfile} link\`.`);
48
- }
64
+ // Resolved before anything can fail, so that every failure this handler sees —
65
+ // including the profile refusal — is reported in the shape the caller can read.
66
+ // Failures raised by `ensureAuth` are not among them: it resolves credentials
67
+ // above its own `init` skip, so an unknown profile, a contradictory
68
+ // `--api-key`/`--profile` pair, and a damaged credentials file all report on
69
+ // stderr before this runs.
70
+ const agent = (argv.agent || !process.stdin.isTTY ? detectAgent() : null) ||
71
+ undefined;
72
+ const isAgentMode = argv.agent || agent !== undefined;
49
73
  try {
50
- // Auto-detect agent from environment. When --agent is explicitly passed,
51
- // always detect (the user asked for agent mode). Otherwise, require
52
- // non-TTY stdin to distinguish agent from human in terminal.
53
- const agent = (argv.agent || !process.stdin.isTTY ? detectAgent() : null) ||
54
- undefined;
55
- const isAgentMode = argv.agent || agent !== undefined;
74
+ // The init flow reads the default credentials directly and re-invokes the CLI as a
75
+ // subprocess. It has no way to be told which profile to use, so honouring a selection
76
+ // here is not possible yet — and silently running as the default account would be
77
+ // worse than refusing, because naming an account is the entire job of the thing being
78
+ // ignored.
79
+ //
80
+ // `NEON_PROFILE` counts just as much as the flag. Checking only the flag left the case
81
+ // that is easier to hit by accident: a profile exported once into a shell then
82
+ // silently disregarded by every `neon init` run in it.
83
+ const selectedProfile = argv.profile?.trim() || credentialInputs().profileEnv.trim();
84
+ if (selectedProfile) {
85
+ const how = argv.profile?.trim()
86
+ ? "--profile was passed, so"
87
+ : "NEON_PROFILE is set, so";
88
+ throw new Error(`${how} \`neon init\` would run as the default account instead of "${selectedProfile}", and it does not support profile selection yet. Run it without one, or set the project up with \`neon --profile ${selectedProfile} link\`.`);
89
+ }
56
90
  // --data with a "step" field routes to the appropriate phase
57
91
  if (argv.data && isAgentMode) {
58
92
  let data;
59
93
  try {
60
94
  data = JSON.parse(argv.data);
61
95
  }
62
- catch {
63
- log.error("Invalid JSON in --data flag. Expected a JSON object.");
64
- process.exit(1);
65
- return;
96
+ catch (parseError) {
97
+ // Neither the payload nor the parser's message may appear here. `--data`
98
+ // carries whatever the caller put in it — a connection string, an API key —
99
+ // and V8 quotes a window of the input around the syntax error, so both would
100
+ // travel into the error message, onto stdout, and into `sendError`'s
101
+ // analytics payload. `shared/cli-core/src/credentials.ts` discards the same
102
+ // message for the same reason. The position is a number and says enough.
103
+ throw new Error(`Invalid JSON in --data flag${parsePosition(parseError)}. Expected a JSON object.`);
66
104
  }
67
105
  if (typeof data.step === "string") {
68
- const result = await routeDataStep(data, agent);
69
- log.info(JSON.stringify(enrichResponse(result), null, 2));
106
+ writeAgentResponse(await routeDataStep(data, agent));
70
107
  return;
71
108
  }
72
109
  }
73
110
  if (isAgentMode) {
74
- const result = await orchestrate({
111
+ writeAgentResponse(await orchestrate({
75
112
  agent,
76
113
  skipMigrations: argv.skipMigrations,
77
114
  preview: argv.preview,
78
- });
79
- log.info(JSON.stringify(enrichResponse(result), null, 2));
115
+ }));
80
116
  }
81
117
  else {
82
118
  await interactiveInit({ preview: argv.preview });
83
119
  }
84
120
  }
85
- catch {
86
- const exitError = new Error(`failed to run neon-init`);
87
- sendError(exitError, "NEON_INIT_FAILED");
88
- process.exit(1);
121
+ catch (error) {
122
+ const cause = error instanceof Error ? error : new Error(String(error));
123
+ if (isAgentMode) {
124
+ // Agent mode answers and exits here, so nothing else will report this. Attribute
125
+ // it to init and flush before exiting — `process.exit` drops in-flight events.
126
+ sendError(cause, "NEON_INIT_FAILED");
127
+ writeAgentFailure(cause);
128
+ await closeAnalytics();
129
+ process.exit(1);
130
+ }
131
+ // A human gets the top-level handler's single `ERROR: <message>` line on stderr, and
132
+ // its `sendError`. Reporting here as well would file one failure as two events.
133
+ throw cause;
89
134
  }
90
135
  };
@@ -1,5 +1,6 @@
1
1
  import { log } from "../log.js";
2
2
  import { projectUpdateRequest } from "../parameters.gen.js";
3
+ import { getCliName } from "../utils/cli_name.js";
3
4
  import { fillSingleProject } from "../utils/enrichers.js";
4
5
  import { writer } from "../writer.js";
5
6
  const IP_ALLOW_FIELDS = [
@@ -68,7 +69,7 @@ const list = async (props) => {
68
69
  const add = async (props) => {
69
70
  if (props.ips.length <= 0) {
70
71
  throw new Error(`Enter individual IP addresses, define ranges with a dash, or use CIDR notation for more flexibility.
71
- Example: neonctl ip-allow add 192.168.1.1, 192.168.1.20-192.168.1.50, 192.168.1.0/24 --project-id <id>`);
72
+ Example: ${getCliName()} ip-allow add 192.168.1.1, 192.168.1.20-192.168.1.50, 192.168.1.0/24 --project-id <id>`);
72
73
  }
73
74
  const project = {};
74
75
  const { data } = await props.apiClient.getProject(props.projectId);
@@ -90,7 +91,7 @@ const add = async (props) => {
90
91
  };
91
92
  const remove = async (props) => {
92
93
  if (props.ips.length <= 0) {
93
- throw new Error(`Remove individual IP addresses and ranges. Example: neonctl ip-allow remove 192.168.1.1 --project-id <id>`);
94
+ throw new Error(`Remove individual IP addresses and ranges. Example: ${getCliName()} ip-allow remove 192.168.1.1 --project-id <id>`);
94
95
  }
95
96
  const project = {};
96
97
  const { data } = await props.apiClient.getProject(props.projectId);