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.
- package/README.md +111 -4
- package/dist/_shared/paths.js +3 -4
- package/dist/analytics.js +82 -25
- package/dist/commands/bootstrap.js +12 -11
- package/dist/commands/checkout.js +7 -6
- package/dist/commands/config.js +26 -11
- package/dist/commands/data_api.js +4 -3
- package/dist/commands/dev.js +67 -14
- package/dist/commands/env.js +131 -9
- package/dist/commands/functions.js +3 -2
- package/dist/commands/init.js +82 -37
- package/dist/commands/ip_allow.js +3 -2
- package/dist/commands/link.js +17 -16
- package/dist/commands/projects.js +3 -2
- package/dist/commands/set_context.js +5 -4
- package/dist/config_template.js +20 -42
- package/dist/current_branch_fast_path.js +2 -1
- package/dist/dev/env.js +214 -10
- package/dist/env_services.js +51 -0
- package/dist/index.js +2 -2
- package/dist/init/agents.js +127 -0
- package/dist/init/auth.js +77 -0
- package/dist/init/bootstrap.js +448 -0
- package/dist/init/build_config.js +2 -0
- package/dist/init/detect_agent.js +108 -0
- package/dist/init/editors.js +62 -0
- package/dist/init/enrich_output.js +71 -0
- package/dist/init/extension.js +191 -0
- package/dist/init/inspect.js +287 -0
- package/dist/init/interactive.js +651 -0
- package/dist/init/neonctl.js +184 -0
- package/dist/init/orchestrate.js +190 -0
- package/dist/init/phases/auth.js +209 -0
- package/dist/init/phases/cleanup.js +27 -0
- package/dist/init/phases/db.js +283 -0
- package/dist/init/phases/getting_started.js +228 -0
- package/dist/init/phases/mcp.js +227 -0
- package/dist/init/phases/migrations.js +251 -0
- package/dist/init/phases/neon_auth.js +135 -0
- package/dist/init/phases/setup.js +729 -0
- package/dist/init/phases/skills.js +89 -0
- package/dist/init/phases/status.js +70 -0
- package/dist/init/resolve_context.js +107 -0
- package/dist/init/route_command.js +100 -0
- package/dist/init/skills.js +248 -0
- package/dist/init/types.js +1 -0
- package/dist/init/vsix.js +111 -0
- package/dist/neon_services.js +143 -0
- package/dist/psql/command/cmd_meta.js +2 -2
- package/dist/psql/core/mainloop.js +1 -1
- package/dist/psql/core/startup.js +1 -1
- package/dist/psql/core/syncVars.js +3 -3
- package/dist/psql/index.js +1 -1
- package/dist/utils/cli_name.js +14 -0
- package/dist/utils/esbuild.js +1 -1
- package/dist/utils/package_manager.js +51 -4
- package/dist/utils/service_picker.js +6 -6
- package/dist/utils/write_sync.js +39 -0
- 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
|
|
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
|
-
|
|
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
|
|
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
|
|
package/dist/_shared/paths.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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:
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
log.debug("
|
|
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 (
|
|
134
|
+
if (keyToQuery) {
|
|
79
135
|
const apiClient = getApiClient({
|
|
80
|
-
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
|
-
|
|
97
|
-
args.
|
|
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
|
|
161
|
+
userId: analyticsUserId(userId),
|
|
105
162
|
});
|
|
106
163
|
client.track({
|
|
107
|
-
userId: userId
|
|
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
|
|
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
|
|
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:
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
244
|
+
`then \`${getCliName()} link\`.`);
|
|
244
245
|
}
|
|
245
|
-
else if (await confirm(
|
|
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(
|
|
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:
|
|
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:
|
|
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}
|
|
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
|
-
|
|
108
|
-
`Fix the cause above, then run
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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) {
|
package/dist/commands/config.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|