neon 2.45.0 → 2.47.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 (59) hide show
  1. package/README.md +111 -4
  2. package/dist/_shared/paths.js +3 -4
  3. package/dist/analytics.js +82 -25
  4. package/dist/commands/bootstrap.js +12 -11
  5. package/dist/commands/checkout.js +7 -6
  6. package/dist/commands/config.js +26 -11
  7. package/dist/commands/data_api.js +4 -3
  8. package/dist/commands/dev.js +67 -14
  9. package/dist/commands/env.js +131 -9
  10. package/dist/commands/functions.js +3 -2
  11. package/dist/commands/init.js +82 -37
  12. package/dist/commands/ip_allow.js +3 -2
  13. package/dist/commands/link.js +17 -16
  14. package/dist/commands/projects.js +3 -2
  15. package/dist/commands/set_context.js +5 -4
  16. package/dist/config_template.js +20 -42
  17. package/dist/current_branch_fast_path.js +2 -1
  18. package/dist/dev/env.js +214 -10
  19. package/dist/env_services.js +51 -0
  20. package/dist/index.js +2 -2
  21. package/dist/init/agents.js +127 -0
  22. package/dist/init/auth.js +77 -0
  23. package/dist/init/bootstrap.js +448 -0
  24. package/dist/init/build_config.js +2 -0
  25. package/dist/init/detect_agent.js +108 -0
  26. package/dist/init/editors.js +62 -0
  27. package/dist/init/enrich_output.js +71 -0
  28. package/dist/init/extension.js +191 -0
  29. package/dist/init/inspect.js +287 -0
  30. package/dist/init/interactive.js +651 -0
  31. package/dist/init/neonctl.js +184 -0
  32. package/dist/init/orchestrate.js +190 -0
  33. package/dist/init/phases/auth.js +209 -0
  34. package/dist/init/phases/cleanup.js +27 -0
  35. package/dist/init/phases/db.js +283 -0
  36. package/dist/init/phases/getting_started.js +228 -0
  37. package/dist/init/phases/mcp.js +227 -0
  38. package/dist/init/phases/migrations.js +251 -0
  39. package/dist/init/phases/neon_auth.js +135 -0
  40. package/dist/init/phases/setup.js +729 -0
  41. package/dist/init/phases/skills.js +89 -0
  42. package/dist/init/phases/status.js +70 -0
  43. package/dist/init/resolve_context.js +107 -0
  44. package/dist/init/route_command.js +100 -0
  45. package/dist/init/skills.js +248 -0
  46. package/dist/init/types.js +1 -0
  47. package/dist/init/vsix.js +111 -0
  48. package/dist/neon_services.js +143 -0
  49. package/dist/psql/command/cmd_meta.js +2 -2
  50. package/dist/psql/core/mainloop.js +1 -1
  51. package/dist/psql/core/startup.js +1 -1
  52. package/dist/psql/core/syncVars.js +3 -3
  53. package/dist/psql/index.js +1 -1
  54. package/dist/utils/cli_name.js +14 -0
  55. package/dist/utils/esbuild.js +1 -1
  56. package/dist/utils/package_manager.js +51 -4
  57. package/dist/utils/service_picker.js +6 -6
  58. package/dist/utils/write_sync.js +39 -0
  59. package/package.json +18 -12
package/README.md CHANGED
@@ -417,9 +417,15 @@ The human-readable summary line goes to stderr and the diff body to stdout, so `
417
417
 
418
418
  ### env pull
419
419
 
420
- `env pull` writes the linked branch's Neon environment variables into a local dotenv file: an existing `.env` if you have one, otherwise `.env.local` (override with `--file <path>`). Only Neon-managed keys (`DATABASE_URL`, `DATABASE_URL_UNPOOLED`, and the Neon Auth / Data API URLs when those services are enabled) are written; any other lines in the file are preserved. The branch comes from the closest `.neon` file, so no `--branch` is needed (pass `--branch <id|name>` to target another branch).
420
+ `env pull` writes the linked branch's Neon environment variables into a local dotenv file: an existing `.env` if you have one, otherwise `.env.local` (override with `--file <path>`). Only Neon-managed keys are written (see the table below); any other lines in the file are preserved. The branch comes from the closest `.neon` file, so no `--branch` is needed (pass `--branch <id|name>` to target another branch).
421
421
 
422
- `link` and `checkout` invoke `env pull` automatically (see above), so you usually only run it by hand to refresh vars or to pull a different branch into a specific file:
422
+ **What gets pulled**, in precedence order:
423
+
424
+ 1. **`--service`**, when you pass it — exactly those services, whatever else is on the branch and whatever a `neon.ts` says.
425
+ 2. **`neon.ts`**, when the working directory has one — the policy is the source of truth, same as `neon dev` and `neon deploy`.
426
+ 3. **Everything the branch has** otherwise — Postgres, Neon Auth, the Data API, and object storage read back from the branch, plus the AI Gateway. The gateway has no branch-level state to read back (it is credential-gated, not provisioned), so a bare `env pull` asks for it rather than detecting it, which mints a branch credential. To leave it out, name the services you do want with `--service`.
427
+
428
+ If the gateway can't be resolved, it is dropped with a warning and the rest of the pull still lands. Gateway variables already in your file for *this* branch are left alone — a pull that couldn't reach the gateway is no evidence the branch has stopped having one — while ones left over from a different branch are pruned like any other stale value.
423
429
 
424
430
  ```bash
425
431
  # Refresh the linked branch's vars in place
@@ -427,10 +433,45 @@ neon env pull
427
433
 
428
434
  # Pull a specific branch into a specific file
429
435
  neon env pull --branch preview --file .env.preview
436
+
437
+ # Only the AI Gateway
438
+ neon env pull --service ai-gateway
439
+
440
+ # Repeat the flag or comma-separate; -s, --service and --services are all accepted
441
+ neon env pull -s postgres -s data-api
442
+ neon env pull -s postgres,auth
430
443
  ```
431
444
 
445
+ Every services flag in the CLI takes those three spellings, the same value syntax, and the same service names — see [`config init --services`](#getting-a-neonts-config-init).
446
+
447
+ | `--service` | Variables |
448
+ | --- | --- |
449
+ | `postgres` | `DATABASE_URL`, `DATABASE_URL_UNPOOLED` |
450
+ | `auth` | `NEON_AUTH_BASE_URL`, `NEON_AUTH_JWKS_URL` |
451
+ | `data-api` | `NEON_DATA_API_URL` |
452
+ | `object-storage` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_ENDPOINT_URL_S3`, `AWS_REGION` |
453
+ | `ai-gateway` | `NEON_AI_GATEWAY_TOKEN`, `NEON_AI_GATEWAY_BASE_URL` |
454
+
455
+ `NEON_BRANCH` is written by every pull — it is branch identity, not a service.
456
+
457
+ **A scoped pull is scoped in both directions.** An unscoped `env pull` owns the Neon-named variables: pointing a directory at a branch without Neon Auth prunes the stale `NEON_AUTH_*` lines. `--service` narrows that to the services you named, so `env pull -s ai-gateway` never touches your `DATABASE_URL`. (`AWS_*` is never pruned by any pull: those names collide with credentials you may set yourself, so `env pull` only ever writes them.)
458
+
459
+ **A scoped pull also never revokes a credential.** Where an unscoped pull revokes the credential it replaces, a scoped one leaves the old one live — it can't tell which other services still use it. It says so when it happens; revoke it in the Neon Console if nothing does.
460
+
461
+ Naming a service the branch does not have is an error, not an empty pull:
462
+
463
+ ```
464
+ --service auth: branch br-snowy-frost-12345 has no Neon Auth integration, so there are no
465
+ auth env vars to pull. Provision it first (`neon deploy`, `neon config apply`, or the Neon
466
+ Console), or drop auth from --service.
467
+ ```
468
+
469
+ `link`, `checkout`, and `config apply` invoke `env pull` automatically (see above). Those bundled pulls follow rules 2 and 3 above **without** the implied AI Gateway: minting a credential for a service you never named isn't something a side effect of another command should do. Run `neon env pull` to get it.
470
+
432
471
  If you'd rather not keep env vars on disk, inject them at runtime instead with `neon-env run -- <your dev command>` (from `@neon/env`) or `neon dev`, and pass `--no-env-pull` to `link` / `checkout`.
433
472
 
473
+ **`neon dev` resolves the same set, by the same rules** — including the AI Gateway on a branch with no `neon.ts`. A function running locally gets what the deployed runtime would inject into it, which is the whole point of `dev`; a handler that reads `NEON_AI_GATEWAY_BASE_URL` should not work in production and fail on your machine. `dev` writes nothing, but it does *read* your `.env` / `.env.local` to reuse the branch credential behind the AI Gateway and object storage. Without a file to read from it issues one on every start and leaves the last one live — it has nowhere to keep it, and so cannot name it to revoke it. It says so when it happens; run `env pull` (or just `link` / `checkout`) once and restarts reuse the credential instead.
474
+
434
475
  **Where `.neon` lives**: `link` writes `.neon` into the **current working directory** by default. If an existing `.neon` is found in any parent directory, that file is reused — so commands run from a sub-directory of a linked project still pick up the project's context. To pin the location explicitly, pass `--context-file <path>`.
435
476
 
436
477
  **`.gitignore` scaffolding**: when `.neon` is **created** for the first time, the CLI also makes sure a `.gitignore` sits alongside it listing `.neon`. If `.gitignore` doesn't exist it's created with a single `.neon` line; if it does exist, `.neon` is appended only when missing (no duplicates, your other entries are left alone). On subsequent updates to an existing `.neon`, `.gitignore` is left untouched — so if you deliberately un-ignore `.neon` (e.g. to commit shared context), the entry is not re-added on every command.
@@ -479,7 +520,10 @@ Selecting nothing is a valid answer: you get the starter policy, which is also w
479
520
  neon config init
480
521
 
481
522
  # Declare services with no prompt
482
- neon config init --services auth,functions,storage,ai-gateway
523
+ neon config init --services auth,functions,object-storage,ai-gateway
524
+
525
+ # Repeat the flag instead, and shorten it — every services flag takes all three spellings
526
+ neon config init -s auth -s functions
483
527
 
484
528
  # Explicitly ask for the bare starter policy
485
529
  neon config init --services none
@@ -488,6 +532,8 @@ neon config init --services none
488
532
  neon config init --no-install
489
533
  ```
490
534
 
535
+ Object storage is spelled `object-storage` here, matching [`env pull --service`](#env-pull) and the rest of the CLI. The old `storage` still works and warns; it will be removed.
536
+
491
537
  Choosing **Functions** also writes the handler the policy points at, since `source` is only resolved when `apply` bundles it — a declared function with no file on disk fails at deploy:
492
538
 
493
539
  ```ts
@@ -593,6 +639,66 @@ $ neon bootstrap . --template hono
593
639
 
594
640
  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
641
 
642
+ ## Set up a project for your coding agent (`init`)
643
+
644
+ `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.
645
+
646
+ ```bash
647
+ $ neon init
648
+ ```
649
+
650
+ 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.
651
+
652
+ 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.
653
+
654
+ ### Agent mode
655
+
656
+ `--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.
657
+
658
+ ```bash
659
+ $ neon init --agent --data '{"step":"status"}'
660
+ {
661
+ "auth": { "authenticated": true },
662
+ "tooling": { "mcpServer": { "configured": true, "scope": "global" }, "skills": { "installed": false, "scope": null } },
663
+ "project": { "databaseUrl": false },
664
+ "migrations": { "tool": "prisma", "hasMigrations": false },
665
+ "recommendations": [
666
+ { "priority": "high", "message": "No DATABASE_URL found in .env", "command": "neon init --agent --data '{\"step\":\"db\"}'" },
667
+ { "priority": "medium", "message": "Neon agent skills not detected in this project", "command": "neon init --agent --data '{\"step\":\"skills\",\"install\":true}'" },
668
+ { "priority": "medium", "message": "prisma detected but no migrations found", "command": "neon init --agent --data '{\"step\":\"migrations\"}'" }
669
+ ]
670
+ }
671
+ ```
672
+
673
+ `--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).
674
+
675
+ `--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.
676
+
677
+ **Failures are JSON too**, so an agent never has to distinguish "it broke" from "it returned nothing":
678
+
679
+ ```bash
680
+ $ neon init --agent --data '{not json'
681
+ {
682
+ "success": false,
683
+ "error": "Invalid JSON in --data flag at position 1. Expected a JSON object."
684
+ }
685
+ $ echo $?
686
+ 1
687
+ ```
688
+
689
+ 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.
690
+
691
+ **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.
692
+
693
+ | Option | |
694
+ | --- | --- |
695
+ | `--agent`, `-a` | Emit the JSON state machine instead of prompting |
696
+ | `--data <json>` | Route to one phase, with that phase's options |
697
+ | `--skip-migrations` | Leave the migrations phase out of the flow |
698
+ | `--preview` | Enable preview features (scaffolding a project from a template) |
699
+
700
+ `neon init` refuses `--profile`; see [Which credential an invocation uses](#which-credential-an-invocation-uses).
701
+
596
702
  ## Snapshots (`snapshots`)
597
703
 
598
704
  `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 +882,7 @@ When both are only environment variables the key wins, which keeps a CI pipeline
776
882
 
777
883
  `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
884
 
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.
885
+ `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
886
 
781
887
  ## API keys (`api-keys`)
782
888
 
@@ -876,6 +982,7 @@ API keys in org-7
876
982
  | config | `init`, `status`, `plan`, `apply` | Drive a branch from `neon.ts` |
877
983
  | deploy | | Alias for `config apply` |
878
984
  | bootstrap | | Scaffold a project from a template |
985
+ | init | | Set up a project for a coding agent |
879
986
  | bucket | `create`, `list`, `delete`, `object list`, `object get`, `object put`, `object delete` (incl. `--recursive`) | Manage buckets and their objects |
880
987
  | [completion](https://neon.com/docs/reference/cli-completion) | | Generate a completion script |
881
988
 
@@ -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
package/dist/analytics.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Analytics } from "@segment/analytics-node";
2
- import { inspectCredentials } from "./_shared/credentials.js";
2
+ import { inspectCredentials, OAUTH } from "./_shared/credentials.js";
3
3
  import { getApiClient, isNeonApiError } from "./api.js";
4
4
  import { getAuthContext } from "./auth_context.js";
5
5
  import { credentialsPath } from "./config.js";
@@ -14,6 +14,63 @@ const WRITE_KEY = "3SQXn5ejjXWLEJ8xU2PRYhAotLtTaeeV";
14
14
  * not be populated yet, so we also scan `process.argv` directly to be safe.
15
15
  */
16
16
  const hasCurrentBranchArgv = () => process.argv.includes("--current-branch");
17
+ const ANONYMOUS = "anonymous";
18
+ /**
19
+ * Who to attribute an event to, given whatever identified this invocation.
20
+ *
21
+ * Nothing is guaranteed to have identified it: a command can run with no credentials at all,
22
+ * leaving the id empty. Segment accepts an empty `userId` and forwards it as-is rather than
23
+ * rejecting it, so the substitution has to happen here. `""` is falsy but not nullish, which
24
+ * is why the fallback has to be `||`.
25
+ *
26
+ * Exported for tests.
27
+ */
28
+ export const analyticsUserId = (userId) => userId || ANONYMOUS;
29
+ /**
30
+ * The account an invocation that presented no API key may claim, which is nothing at all
31
+ * unless stored credentials named a user.
32
+ *
33
+ * Both fields are omitted together. An empty account reported under a named method describes
34
+ * an authentication that did not happen, which is worse than reporting neither.
35
+ *
36
+ * Exported for tests.
37
+ */
38
+ export const storedCredentialAttribution = (storedUserId) => storedUserId ? { accountId: storedUserId, authMethod: OAUTH } : {};
39
+ /**
40
+ * Which credential telemetry may describe this invocation with.
41
+ *
42
+ * `ensureAuth` records a context only when it selected a credential for this invocation, so a
43
+ * missing context means the global auth middleware selected nothing before this ran. A key
44
+ * sitting in `args.apiKey` is then not the credential the middleware chose — `neon profile list`
45
+ * never used it — and must not be queried on its behalf, which would attribute the run to an
46
+ * account it never authenticated as and add a telemetry-only API call. The local default is the
47
+ * guess.
48
+ *
49
+ * The boundary is deliberately the credential the middleware selected, not every key a handler
50
+ * may go on to use. Several `profile` subcommands authenticate inside their own handlers —
51
+ * `create --api-key` verifies the key it is about to store, `rotate-key` mints and revokes — and
52
+ * those runs are attributed to the local default rather than to the account the handler talked
53
+ * to. Attributing them to that key puts an `identify` for the signed-in user beside an
54
+ * `accountId` for a different account.
55
+ *
56
+ * A selected key records no file, because it authenticates as its own account rather than out
57
+ * of one. Reading `DEFAULT` for it would identify the run as whoever is signed in locally, and
58
+ * that borrowed id would suppress the API lookup that names the key's real owner.
59
+ *
60
+ * Exported for tests.
61
+ */
62
+ export const telemetryCredential = (authContext, apiKey, defaultCredentialsPath) => {
63
+ if (authContext === null) {
64
+ return { credentialsPath: defaultCredentialsPath };
65
+ }
66
+ if (authContext.source === "api-key") {
67
+ return { apiKey };
68
+ }
69
+ return {
70
+ apiKey,
71
+ credentialsPath: authContext.credentialsPath ?? defaultCredentialsPath,
72
+ };
73
+ };
17
74
  let client;
18
75
  let clientInitialized = false;
19
76
  let userId = "";
@@ -42,7 +99,7 @@ export const initAnalyticsClientMiddleware = (args) => {
42
99
  });
43
100
  log.debug("Initialized CLI analytics client");
44
101
  client.identify({
45
- userId: "anonymous",
102
+ userId: ANONYMOUS,
46
103
  });
47
104
  };
48
105
  /**
@@ -56,28 +113,27 @@ export const analyticsMiddleware = async (args) => {
56
113
  if (isCurrentBranchProbe(args)) {
57
114
  return;
58
115
  }
59
- // Read the credentials this invocation actually authenticated with, which `ensureAuth`
60
- // recorded. Reading `DEFAULT`'s unconditionally attributed every `--profile`-selected
61
- // command to whichever account happened to be the default one.
62
- const authenticatedAs = getAuthContext()?.credentialsPath ?? credentialsPath(args.configDir);
63
- // Telemetry must never turn a damaged or unreadable credentials file into a failed command.
64
- try {
65
- const read = inspectCredentials(authenticatedAs);
66
- if (read.kind === "ok" &&
67
- typeof read.credentials.user_id === "string") {
68
- userId = read.credentials.user_id;
116
+ const { apiKey: keyToQuery, credentialsPath: fileToRead } = telemetryCredential(getAuthContext(), args.apiKey, credentialsPath(args.configDir));
117
+ if (fileToRead !== undefined) {
118
+ // Telemetry must never turn a damaged or unreadable credentials file into a failed command.
119
+ try {
120
+ const read = inspectCredentials(fileToRead);
121
+ if (read.kind === "ok" &&
122
+ typeof read.credentials.user_id === "string") {
123
+ userId = read.credentials.user_id;
124
+ }
125
+ else if (read.kind !== "ok") {
126
+ log.debug("No usable credentials at %s", fileToRead);
127
+ }
69
128
  }
70
- else if (read.kind !== "ok") {
71
- log.debug("No usable credentials at %s", authenticatedAs);
129
+ catch (err) {
130
+ log.debug("Could not read %s: %s", fileToRead, err);
72
131
  }
73
132
  }
74
- catch (err) {
75
- log.debug("Could not read %s: %s", authenticatedAs, err);
76
- }
77
133
  try {
78
- if (args.apiKey) {
134
+ if (keyToQuery) {
79
135
  const apiClient = getApiClient({
80
- apiKey: args.apiKey,
136
+ apiKey: keyToQuery,
81
137
  apiHost: args.apiHost,
82
138
  });
83
139
  // Populating api key details for analytics
@@ -93,18 +149,19 @@ export const analyticsMiddleware = async (args) => {
93
149
  }
94
150
  }
95
151
  else {
96
- args.accountId = userId;
97
- args.authMethod = "oauth";
152
+ const { accountId, authMethod } = storedCredentialAttribution(userId);
153
+ args.accountId = accountId;
154
+ args.authMethod = authMethod;
98
155
  }
99
156
  }
100
157
  catch (err) {
101
158
  log.debug("Failed to get user id from api", err);
102
159
  }
103
160
  client.identify({
104
- userId: userId?.toString() ?? "anonymous",
161
+ userId: analyticsUserId(userId),
105
162
  });
106
163
  client.track({
107
- userId: userId || "anonymous",
164
+ userId: analyticsUserId(userId),
108
165
  event: "CLI Started",
109
166
  properties: getAnalyticsEventProperties(args),
110
167
  context: {
@@ -145,7 +202,7 @@ export const sendError = (err, errCode) => {
145
202
  }
146
203
  client.track({
147
204
  event: "CLI Error",
148
- userId: userId || "anonymous",
205
+ userId: analyticsUserId(userId),
149
206
  properties: getErrorAnalyticsEventProperties(err, errCode, errorEventContext),
150
207
  });
151
208
  log.debug("Sent CLI error event: %s", errCode);
@@ -156,7 +213,7 @@ export const trackEvent = (event, properties) => {
156
213
  }
157
214
  client.track({
158
215
  event,
159
- userId: userId || "anonymous",
216
+ userId: analyticsUserId(userId),
160
217
  properties,
161
218
  });
162
219
  log.debug("Sent CLI event: %s", event);
@@ -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) {
@@ -5,13 +5,15 @@ import { apply, createBranch as createBranchFromPolicy, inspect, isPartialBranch
5
5
  import chalk from "chalk";
6
6
  import { getApiClient } from "../api.js";
7
7
  import { toNeonConfigView } from "../config_format.js";
8
- import { FUNCTION_FILENAME, FUNCTION_SLUG, FUNCTION_TEMPLATE, NEON_SERVICES, NO_SERVICES, parseServices, REQUIRED_PACKAGES, renderNeonConfig, renderNeonConfigFromView, } from "../config_template.js";
8
+ import { CONFIG_INIT_NONE_MEANS, CONFIG_INIT_SERVICES, CONFIG_INIT_UNAVAILABLE, FUNCTION_FILENAME, FUNCTION_SLUG, FUNCTION_TEMPLATE, REQUIRED_PACKAGES, renderNeonConfig, renderNeonConfigFromView, } from "../config_template.js";
9
9
  import { contextBranch, readContextFile } from "../context.js";
10
10
  import { isCi } from "../env.js";
11
11
  import { loadEnvFileIntoProcess } from "../env_file.js";
12
12
  import { log } from "../log.js";
13
+ import { deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOption, } from "../neon_services.js";
13
14
  import { assertAiGatewayProvisionable, warnAiGateway, } from "../utils/ai_gateway_notice.js";
14
15
  import { announceTargetBranch } from "../utils/branch_notice.js";
16
+ import { getCliName } from "../utils/cli_name.js";
15
17
  import { renderAppliedChanges, renderBranchSettingConflicts, } from "../utils/config_diff.js";
16
18
  import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
17
19
  import { bundleEntry } from "../utils/esbuild.js";
@@ -119,7 +121,13 @@ const missingDependencies = (cwd) => {
119
121
  */
120
122
  const resolveServices = async (props) => {
121
123
  if (props.services !== undefined) {
122
- return parseServices(props.services);
124
+ return parseServices(props.services, {
125
+ allowed: CONFIG_INIT_SERVICES,
126
+ whyUnavailable: CONFIG_INIT_UNAVAILABLE,
127
+ flag: "--services",
128
+ noneMeans: CONFIG_INIT_NONE_MEANS,
129
+ onDeprecated: (used, canonical) => log.warning(deprecatedServiceMessage(used, canonical)),
130
+ });
123
131
  }
124
132
  if (props.pickServices) {
125
133
  return props.pickServices();
@@ -216,7 +224,7 @@ export const initCmd = async (props) => {
216
224
  log.info("%s are already installed.", REQUIRED_PACKAGES.join(" and "));
217
225
  }
218
226
  else {
219
- const pm = resolvePackageManager();
227
+ const pm = resolvePackageManager(cwd);
220
228
  const args = addDependenciesArgs(pm, missing);
221
229
  if (props.install === false) {
222
230
  log.info("Install the Neon config packages to use neon.ts: %s %s", pm, args.join(" "));
@@ -284,12 +292,14 @@ export const builder = (argv) => argv
284
292
  type: "boolean",
285
293
  default: true,
286
294
  },
287
- services: {
288
- describe: `Services the scaffolded neon.ts declares, comma-separated: ${NEON_SERVICES.join(", ")}. ` +
289
- `Pass "${NO_SERVICES}" for the bare starter policy. Omitted: pick interactively on a ` +
290
- "terminal, starter policy in CI or without a TTY.",
291
- type: "string",
292
- },
295
+ services: servicesOption({
296
+ key: "services",
297
+ allowed: CONFIG_INIT_SERVICES,
298
+ noneMeans: CONFIG_INIT_NONE_MEANS,
299
+ describe: "Services the scaffolded neon.ts declares",
300
+ also: "Omitted: pick interactively on a terminal, starter policy in " +
301
+ "CI or without a TTY.",
302
+ }),
293
303
  "from-branch": {
294
304
  describe: "Seed neon.ts from a branch's live Neon state instead of asking. Uses the " +
295
305
  "branch pinned in .neon, or --branch <name|id>, or the project's default " +
@@ -299,7 +309,12 @@ export const builder = (argv) => argv
299
309
  // `default: false` makes `conflicts` reject every `--services` run.
300
310
  conflicts: "services",
301
311
  },
302
- }), (args) => initCmd(args));
312
+ }), (args) => initCmd({
313
+ ...args,
314
+ // `args` is untyped here, and "flag omitted" has to stay distinct from
315
+ // "flag given" — it decides whether the picker runs at all.
316
+ services: servicesFlagValue(args.services),
317
+ }));
303
318
  export const handler = (args) => {
304
319
  return args;
305
320
  };
@@ -354,7 +369,7 @@ export const status = async (props) => {
354
369
  else {
355
370
  // No branch pinned: hint on stderr and exit non-zero (grep-style) so a prompt's
356
371
  // `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.");
372
+ log.info(`No branch pinned. Run \`${getCliName()} checkout <branch>\` to pin a branch and pull its env vars.`);
358
373
  process.exitCode = 1;
359
374
  }
360
375
  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
  }