neonctl 2.22.0 → 2.23.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 +242 -16
- package/analytics.js +5 -2
- package/commands/branches.js +9 -1
- package/commands/checkout.js +249 -0
- package/commands/connection_string.js +15 -2
- package/commands/data_api.js +286 -0
- package/commands/functions.js +277 -0
- package/commands/index.js +12 -0
- package/commands/link.js +667 -0
- package/commands/neon_auth.js +1013 -0
- package/commands/projects.js +9 -1
- package/commands/psql.js +62 -0
- package/commands/set_context.js +7 -2
- package/context.js +86 -14
- package/functions_api.js +44 -0
- package/index.js +3 -0
- package/package.json +60 -51
- package/psql/cli.js +51 -0
- package/psql/command/cmd_cond.js +437 -0
- package/psql/command/cmd_connect.js +815 -0
- package/psql/command/cmd_copy.js +1025 -0
- package/psql/command/cmd_describe.js +1810 -0
- package/psql/command/cmd_format.js +909 -0
- package/psql/command/cmd_io.js +2187 -0
- package/psql/command/cmd_lo.js +385 -0
- package/psql/command/cmd_meta.js +970 -0
- package/psql/command/cmd_misc.js +187 -0
- package/psql/command/cmd_pipeline.js +1141 -0
- package/psql/command/cmd_restrict.js +171 -0
- package/psql/command/cmd_show.js +751 -0
- package/psql/command/dispatch.js +343 -0
- package/psql/command/inputQueue.js +42 -0
- package/psql/command/shared.js +71 -0
- package/psql/complete/filenames.js +139 -0
- package/psql/complete/index.js +104 -0
- package/psql/complete/matcher.js +314 -0
- package/psql/complete/psqlVars.js +247 -0
- package/psql/complete/queries.js +491 -0
- package/psql/complete/rules.js +2387 -0
- package/psql/core/common.js +1250 -0
- package/psql/core/help.js +576 -0
- package/psql/core/mainloop.js +1353 -0
- package/psql/core/prompt.js +437 -0
- package/psql/core/settings.js +684 -0
- package/psql/core/sqlHelp.js +1066 -0
- package/psql/core/startup.js +840 -0
- package/psql/core/syncVars.js +116 -0
- package/psql/core/variables.js +287 -0
- package/psql/describe/formatters.js +1277 -0
- package/psql/describe/processNamePattern.js +270 -0
- package/psql/describe/queries.js +2373 -0
- package/psql/describe/versionGate.js +43 -0
- package/psql/index.js +2005 -0
- package/psql/io/history.js +299 -0
- package/psql/io/input.js +120 -0
- package/psql/io/lineEditor/buffer.js +323 -0
- package/psql/io/lineEditor/complete.js +227 -0
- package/psql/io/lineEditor/filename.js +159 -0
- package/psql/io/lineEditor/index.js +891 -0
- package/psql/io/lineEditor/keymap.js +738 -0
- package/psql/io/lineEditor/vt100.js +363 -0
- package/psql/io/pgpass.js +202 -0
- package/psql/io/pgservice.js +194 -0
- package/psql/io/psqlrc.js +422 -0
- package/psql/print/aligned.js +1756 -0
- package/psql/print/asciidoc.js +248 -0
- package/psql/print/crosstab.js +460 -0
- package/psql/print/csv.js +92 -0
- package/psql/print/html.js +258 -0
- package/psql/print/json.js +96 -0
- package/psql/print/latex.js +396 -0
- package/psql/print/pager.js +265 -0
- package/psql/print/troff.js +258 -0
- package/psql/print/unaligned.js +118 -0
- package/psql/print/units.js +135 -0
- package/psql/scanner/slash.js +513 -0
- package/psql/scanner/sql.js +910 -0
- package/psql/scanner/stringutils.js +390 -0
- package/psql/types/backslash.js +1 -0
- package/psql/types/connection.js +1 -0
- package/psql/types/index.js +7 -0
- package/psql/types/printer.js +1 -0
- package/psql/types/repl.js +1 -0
- package/psql/types/scanner.js +24 -0
- package/psql/types/settings.js +1 -0
- package/psql/types/variables.js +1 -0
- package/psql/wire/connection.js +2844 -0
- package/psql/wire/copy.js +108 -0
- package/psql/wire/notify.js +59 -0
- package/psql/wire/pipeline.js +519 -0
- package/psql/wire/protocol.js +466 -0
- package/psql/wire/sasl.js +296 -0
- package/psql/wire/tls.js +596 -0
- package/test_utils/fixtures.js +1 -0
- package/utils/enrichers.js +18 -1
- package/utils/esbuild.js +147 -0
- package/utils/middlewares.js +1 -1
- package/utils/psql.js +107 -11
- package/utils/zip.js +4 -0
- package/writer.js +1 -1
- package/commands/auth.test.js +0 -211
- package/commands/branches.test.js +0 -460
- package/commands/connection_string.test.js +0 -196
- package/commands/databases.test.js +0 -39
- package/commands/help.test.js +0 -9
- package/commands/init.test.js +0 -56
- package/commands/ip_allow.test.js +0 -59
- package/commands/operations.test.js +0 -7
- package/commands/orgs.test.js +0 -7
- package/commands/projects.test.js +0 -144
- package/commands/roles.test.js +0 -37
- package/commands/set_context.test.js +0 -159
- package/commands/vpc_endpoints.test.js +0 -69
- package/env.test.js +0 -55
- package/utils/formats.test.js +0 -32
- package/writer.test.js +0 -104
package/README.md
CHANGED
|
@@ -58,25 +58,236 @@ neonctl projects list --api-key <neon_api_key>
|
|
|
58
58
|
|
|
59
59
|
For information about obtaining an Neon API key, see [Authentication](https://api-docs.neon.tech/reference/authentication), in the _Neon API Reference_.
|
|
60
60
|
|
|
61
|
+
## Connect with psql
|
|
62
|
+
|
|
63
|
+
Several commands accept a `--psql` flag that opens a psql session against the resolved endpoint:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
neonctl connection-string --psql --project-id <id>
|
|
67
|
+
neonctl projects create --psql
|
|
68
|
+
neonctl branches create --psql
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Any arguments after `--` are forwarded to psql, for example:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
neonctl cs --psql --project-id <id> -- -c "SELECT version()"
|
|
75
|
+
neonctl cs --psql --project-id <id> -- -f script.sql --csv
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Embedded psql fallback
|
|
79
|
+
|
|
80
|
+
If the system has `psql` installed on `$PATH`, `--psql` continues to spawn the native binary — there is no behavior change for existing users.
|
|
81
|
+
|
|
82
|
+
If `psql` is not found on `$PATH`, neonctl now falls back to an embedded TypeScript implementation. There is nothing to install or configure; it ships with `neonctl`. This removes the "no psql binary" trap on machines (and CI runners) that don't have PostgreSQL client tools installed.
|
|
83
|
+
|
|
84
|
+
Automatic fallback is the intended path — there is normally no flag to set. The embedded implementation can also be force-selected (primarily for tests and CI, e.g. to exercise it even when a native `psql` is present):
|
|
85
|
+
|
|
86
|
+
- `--fallback` — force the embedded implementation on `connection-string`, `projects create`, and `branches create`. Intentionally hidden from `--help`: it's a test/CI knob, not a user-facing option (the automatic fallback above is the supported behavior).
|
|
87
|
+
- `NEONCTL_PSQL_FALLBACK=1` — environment variable with the same effect as `--fallback`. Convenient for scripts and CI.
|
|
88
|
+
|
|
89
|
+
The embedded implementation is verified against a conformance suite that
|
|
90
|
+
diffs its behavior against real PostgreSQL (14–18) and the upstream psql
|
|
91
|
+
regression + TAP tests.
|
|
92
|
+
|
|
93
|
+
#### What works
|
|
94
|
+
|
|
95
|
+
**REPL & scripting**
|
|
96
|
+
|
|
97
|
+
- Interactive REPL with a hand-rolled VT100 line editor (no native bindings); vi and emacs edit modes (`VI_MODE` psql variable)
|
|
98
|
+
- Persistent command history (`~/.psql_history`, libreadline format)
|
|
99
|
+
- `~/.psqlrc` autoload (including `$PGSYSCONFDIR/psqlrc` and version-suffixed variants)
|
|
100
|
+
- Scripted modes: `-c "SQL"`, `-f script.sql`, and stdin; `--single-transaction`, `ON_ERROR_STOP`, `ECHO`, `--echo-all`
|
|
101
|
+
- `SINGLELINE` (`-S`), `\timing`, `\watch` (named flags `c=`/`i=`/`m=`, unbounded continuous mode)
|
|
102
|
+
|
|
103
|
+
**Backslash commands**
|
|
104
|
+
|
|
105
|
+
- All output formats: aligned, unaligned, wrapped, csv, json, html, asciidoc, latex, latex-longtable, troff-ms (`\a \H \t \x \pset \f \C` …)
|
|
106
|
+
- All `\d*` describe commands with full upstream parity (columns, indexes, foreign keys, triggers, view definitions, sequences, RLS, replica identity, partitions, tablespaces, access methods, inheritance, FDW, stats objects, publications, subscriptions, per-column FDW options, TOAST owner)
|
|
107
|
+
- `\copy` to/from file, `PROGRAM`, `STDIN`, `STDOUT` (incl. the `\.` EOF marker); `\g` / `\gx` / `\gset` / `\gdesc` / `\gexec` and `\g | program` pipes
|
|
108
|
+
- Extended query + pipeline mode (`\bind`, `\bind_named`, `\startpipeline`, `\parse`, `\sendpipeline`)
|
|
109
|
+
- `\crosstabview`, `\lo_*` large objects, `\e`/`\edit` (external editor), `\s` (history), `\?`/`\h` help, `\if`/`\elif`/`\else`/`\endif`, `\set`/`\unset`, `\connect`, `\encoding` (live `SET client_encoding`), `\!`, `\cd`, `\prompt` (incl. no-echo `-`), `\password`
|
|
110
|
+
- Tab completion (~88 rules incl. live `pg_settings` GUC lookup, deep `ALTER` sub-actions, `JOIN` clauses, window `OVER`)
|
|
111
|
+
|
|
112
|
+
**Connection & authentication**
|
|
113
|
+
|
|
114
|
+
- libpq-equivalent lookup precedence: argv flags > URI > `PG*` env vars > `~/.pgpass` > `pg_service.conf` > libpq defaults
|
|
115
|
+
- SCRAM-SHA-256 / SCRAM-SHA-256-PLUS with `tls-server-end-point` channel binding (`channel_binding`); MD5 and cleartext; `require_auth`
|
|
116
|
+
- Multi-host failover & load balancing: `target_session_attrs` (any / read-write / read-only / primary / standby / prefer-standby), `load_balance_hosts`, DNS fan-out, `hostaddr`
|
|
117
|
+
- Unix-domain sockets (host beginning with `/`); TCP keepalives (`keepalives`, `keepalives_idle`)
|
|
118
|
+
|
|
119
|
+
**TLS**
|
|
120
|
+
|
|
121
|
+
- `sslmode` disable → verify-full; client certs in **PEM or DER** via `sslcert` / `sslkey` (+ `sslpassword` for encrypted keys, with the libpq group/world-readable-key check)
|
|
122
|
+
- Trust config: `sslrootcert` (incl. `=system` with `SSL_CERT_FILE` / `SSL_CERT_DIR`), default client-cert discovery (`~/.postgresql/postgresql.{crt,key}`), `sslcertmode`
|
|
123
|
+
- CRL: `sslcrl` and `sslcrldir`; `ssl_min_protocol_version` / `ssl_max_protocol_version`; `sslsni`
|
|
124
|
+
- Direct-SSL negotiation (`sslnegotiation=direct`, PostgreSQL 17+, via ALPN)
|
|
125
|
+
|
|
126
|
+
#### What's not supported
|
|
127
|
+
|
|
128
|
+
- **GSSAPI / SSPI** (`gssencmode`, Kerberos/SSPI auth, `requirepeer`). GSS transport encryption needs a native Kerberos binding, which the embedded psql deliberately avoids (pure TypeScript, zero native dependencies — the same reason the line editor is hand-rolled). `node-postgres` doesn't support it either, and Neon doesn't use it. `gssencmode=disable` / `prefer` are accepted; `gssencmode=require` is rejected with a clear error. `requirepeer` is parsed but a Unix-socket connection that sets it is refused (Node exposes no peer-credential API — it is not silently ignored).
|
|
129
|
+
- **`keepalives_interval` / `keepalives_count`** — Node's socket API exposes only keepalive enable + initial delay, so these are accepted but not applied.
|
|
130
|
+
|
|
131
|
+
### Known limitations
|
|
132
|
+
|
|
133
|
+
- **TLS cipher is runtime-dependent.** The negotiated TLS 1.3 ciphersuite is chosen by the host runtime's TLS library from an offer byte-identical to libpq's. Under Node (OpenSSL) that is `TLS_AES_256_GCM_SHA384`, matching vanilla psql; under Bun (BoringSSL) it is `TLS_AES_128_GCM_SHA256`. Both are TLS 1.3 AEAD suites with no practical security difference, and neither runtime exposes a client-side knob to steer the selection.
|
|
134
|
+
|
|
61
135
|
## Configure autocompletion
|
|
62
136
|
|
|
63
137
|
The Neon CLI supports autocompletion, which you can configure in a few easy steps. See [Neon CLI commands — completion](https://neon.tech/docs/reference/cli-completion) for instructions.
|
|
64
138
|
|
|
139
|
+
## Linking a project
|
|
140
|
+
|
|
141
|
+
`neonctl link` is a Vercel-style command that binds the current directory to a Neon project. It picks (or creates) an organization, picks (or creates) a project, resolves the project's default branch, and writes a `.neon` file with `{ "orgId", "projectId", "branchId" }`. Subsequent commands run in this directory (or any sub-directory) automatically pick up that context.
|
|
142
|
+
|
|
143
|
+
There are three modes:
|
|
144
|
+
|
|
145
|
+
**Interactive (default)** — guided prompts for humans:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
$ neonctl link
|
|
149
|
+
? Which organization would you like to link? › Personal Org (org-abc123)
|
|
150
|
+
? Which project would you like to link? › + Create new project
|
|
151
|
+
? Name for the new project: › my-app
|
|
152
|
+
? Which region should the new project run in? › AWS US East (Ohio) (aws-us-east-2)
|
|
153
|
+
Created project polished-snowflake-12345678 ("my-app") in aws-us-east-2.
|
|
154
|
+
Linked .neon:
|
|
155
|
+
orgId: org-abc123
|
|
156
|
+
projectId: polished-snowflake-12345678
|
|
157
|
+
branchId: br-main-branch-87654321
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Non-interactive (flags or `--params` JSON)** — for scripts and CI:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# Link to an existing project
|
|
164
|
+
neonctl link --org-id org-abc123 --project-id polished-snowflake-12345678
|
|
165
|
+
|
|
166
|
+
# Create a new project and link
|
|
167
|
+
neonctl link --org-id org-abc123 --project-name my-app --region-id aws-us-east-2
|
|
168
|
+
|
|
169
|
+
# Same payload, one JSON blob
|
|
170
|
+
neonctl link --params '{"orgId":"org-abc123","projectName":"my-app","regionId":"aws-us-east-2"}'
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Agent mode (`--agent`)** — a JSON state machine designed for AI coding assistants. Each invocation returns a single JSON object with a `status` discriminator describing the next step, the available options, and the exact follow-up command to run.
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
$ neonctl link --agent
|
|
177
|
+
{
|
|
178
|
+
"status": "needs_org",
|
|
179
|
+
"instruction": "Ask the user which of these 2 organizations they want to link the current directory to. After they pick one, re-run the next_command_template with the chosen --org-id value.",
|
|
180
|
+
"options": [
|
|
181
|
+
{ "id": "org-abc123", "name": "Personal Org" },
|
|
182
|
+
{ "id": "org-team", "name": "Team Org" }
|
|
183
|
+
],
|
|
184
|
+
"next_command_template": "neonctl link --agent --org-id <org_id>"
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
$ neonctl link --agent --org-id org-abc123
|
|
188
|
+
{
|
|
189
|
+
"status": "needs_project",
|
|
190
|
+
"instruction": "Ask the user whether to link to one of these 1 existing projects (use next_command_template with --project-id) or create a new project (use create_option.next_command_template).",
|
|
191
|
+
"options": [
|
|
192
|
+
{ "id": "polished-snowflake-12345678", "name": "my-app" }
|
|
193
|
+
],
|
|
194
|
+
"create_option": {
|
|
195
|
+
"instruction": "To create a new project, ask the user for a project name. The region can be omitted to receive a follow-up needs_project_details response that lists available regions.",
|
|
196
|
+
"next_command_template": "neonctl link --agent --org-id org-abc123 --project-name <name> --region-id <region_id>"
|
|
197
|
+
},
|
|
198
|
+
"next_command_template": "neonctl link --agent --org-id org-abc123 --project-id <project_id>"
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
$ neonctl link --agent --org-id org-abc123 --project-id polished-snowflake-12345678
|
|
202
|
+
{
|
|
203
|
+
"status": "linked",
|
|
204
|
+
"context_file": "/path/to/cwd/.neon",
|
|
205
|
+
"context": {
|
|
206
|
+
"orgId": "org-abc123",
|
|
207
|
+
"projectId": "polished-snowflake-12345678",
|
|
208
|
+
"branchId": "br-main-branch-87654321"
|
|
209
|
+
},
|
|
210
|
+
"project": { "id": "polished-snowflake-12345678" },
|
|
211
|
+
"message": "Linked /path/to/cwd/.neon to project polished-snowflake-12345678 (org org-abc123) on branch br-main-branch-87654321."
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The agent flow also handles project creation. If the agent sends `--project-name` without `--region-id`, the next response is `needs_project_details` with the list of supported regions.
|
|
216
|
+
|
|
217
|
+
**Organization-scoped API keys** (those created at the organization level rather than the user level) cannot list user organizations or call the regions endpoint. `link` handles this transparently:
|
|
218
|
+
|
|
219
|
+
- If the API key is org-scoped and at least one project already exists in the org, the CLI auto-detects the `org_id` from the first project. In interactive mode it prints an informational message; in `--agent` mode it skips straight to `needs_project`.
|
|
220
|
+
- If the API key is org-scoped and no projects exist yet, `--agent` returns a `needs_org` response with `options: []` and an instruction telling the user to find their org ID in the Neon Console. Interactive mode prints an error pointing to `--org-id`.
|
|
221
|
+
- When the regions endpoint is not allowed, `link` falls back to a built-in static region list.
|
|
222
|
+
|
|
223
|
+
**Agent error contract**: any unexpected failure in `--agent` mode is reported as JSON to stdout with exit code 1, so agents can always parse the response:
|
|
224
|
+
|
|
225
|
+
```json
|
|
226
|
+
{
|
|
227
|
+
"status": "error",
|
|
228
|
+
"code": "CLIENT_ERROR",
|
|
229
|
+
"message": "user has no access to projects"
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`link` is a thin wrapper around `set-context`: both write to the same `.neon` file via a shared `applyContext` helper, so anything `link` can write, `set-context` can write too (including the newly-supported `--branch-id` flag).
|
|
234
|
+
|
|
235
|
+
### checkout
|
|
236
|
+
|
|
237
|
+
`checkout [id|name]` pins a branch in the local context so subsequent commands target it — it's a focused helper over `set-context` for the common "switch the branch I'm working on" case. It resolves the branch (by name or id) against the project, then **heals** the `.neon` file: it always (re)writes `projectId`, `branchId`, and `orgId` (when the project has one), so a `.neon` that was missing fields or drifted ends up complete and consistent. When `orgId` isn't already known (from `--org-id` or the existing `.neon`), it's looked up from the project itself.
|
|
238
|
+
|
|
239
|
+
The branch argument is **optional**: run `neonctl checkout` with no branch in an interactive terminal to fetch the project's branches and pick one from a list. In a non-interactive context (CI or no TTY), a branch must be passed explicitly.
|
|
240
|
+
|
|
241
|
+
Branch **id vs name** is detected automatically (a `br-…` value is treated as an id):
|
|
242
|
+
|
|
243
|
+
- **id** — matched strictly by id. A non-existent id is a hard "not found" error (ids are server-assigned, so checkout never creates one).
|
|
244
|
+
- **name** — matched by name. If the name doesn't exist, in an interactive terminal `checkout` offers to **create** it (equivalent to `neonctl branch create --name <name>`: branched from the project's default branch with a read-write compute), then checks it out. In a non-interactive context a missing name is the usual "not found" error.
|
|
245
|
+
|
|
246
|
+
The project is resolved through the standard neonctl chain, each entry winning over the next:
|
|
247
|
+
|
|
248
|
+
1. `--project-id <id>` flag
|
|
249
|
+
2. `projectId` from the closest `.neon` file (found by walking up from the current directory — see "Where `.neon` lives" below)
|
|
250
|
+
3. If still unresolved and the API key maps to exactly one project, that project is auto-detected (same behaviour as `branches` and `connection-string`)
|
|
251
|
+
|
|
252
|
+
If none of those resolve a project, `checkout` prints a telling error explaining the chain above. In an interactive terminal it then offers to run `neonctl link` in the current folder so you can pick (or create) a project on the spot; once linked, it continues and pins the requested branch. In non-interactive contexts (CI or no TTY) it exits with a non-zero code and the same guidance instead of prompting.
|
|
253
|
+
|
|
254
|
+
The resolved branch id is then written to the same `.neon` file that `link` and `set-context` use:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
$ neonctl checkout main --project-id polished-snowflake-12345678
|
|
258
|
+
INFO: Checked out branch br-main-branch-87654321 on project polished-snowflake-12345678. Updated /path/to/cwd/.neon.
|
|
259
|
+
|
|
260
|
+
$ cat .neon
|
|
261
|
+
{
|
|
262
|
+
"orgId": "org-abc123",
|
|
263
|
+
"projectId": "polished-snowflake-12345678",
|
|
264
|
+
"branchId": "br-main-branch-87654321"
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Where `.neon` lives**: `link` (and `set-context`) write `.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>`.
|
|
269
|
+
|
|
270
|
+
**`.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.
|
|
271
|
+
|
|
65
272
|
## Commands
|
|
66
273
|
|
|
67
|
-
| Command | Subcommands | Description
|
|
68
|
-
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
69
|
-
| [auth](https://neon.com/docs/reference/cli-auth) | | Authenticate
|
|
70
|
-
| [projects](https://neon.com/docs/reference/cli-projects) | `list`, `create`, `update`, `delete`, `get` | Manage projects
|
|
71
|
-
| [ip-allow](https://neon.com/docs/reference/cli-ip-allow) | `list`, `add`, `remove`, `reset` | Manage IP Allow
|
|
72
|
-
| [me](https://neon.com/docs/reference/cli-me) | | Show current user
|
|
73
|
-
| [branches](https://neon.com/docs/reference/cli-branches) | `list`, `create`, `rename`, `add-compute`, `set-default`, `set-expiration`, `delete`, `get` | Manage branches
|
|
74
|
-
| [databases](https://neon.com/docs/reference/cli-databases) | `list`, `create`, `delete` | Manage databases
|
|
75
|
-
|
|
|
76
|
-
| [
|
|
77
|
-
| [
|
|
78
|
-
| [
|
|
79
|
-
|
|
|
274
|
+
| Command | Subcommands | Description |
|
|
275
|
+
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------ |
|
|
276
|
+
| [auth](https://neon.com/docs/reference/cli-auth) | | Authenticate |
|
|
277
|
+
| [projects](https://neon.com/docs/reference/cli-projects) | `list`, `create`, `update`, `delete`, `get` | Manage projects |
|
|
278
|
+
| [ip-allow](https://neon.com/docs/reference/cli-ip-allow) | `list`, `add`, `remove`, `reset` | Manage IP Allow |
|
|
279
|
+
| [me](https://neon.com/docs/reference/cli-me) | | Show current user |
|
|
280
|
+
| [branches](https://neon.com/docs/reference/cli-branches) | `list`, `create`, `rename`, `add-compute`, `set-default`, `set-expiration`, `delete`, `get` | Manage branches |
|
|
281
|
+
| [databases](https://neon.com/docs/reference/cli-databases) | `list`, `create`, `delete` | Manage databases |
|
|
282
|
+
| functions | `deploy`, `list`, `get`, `delete` | Manage Neon Functions |
|
|
283
|
+
| [roles](https://neon.com/docs/reference/cli-roles) | `list`, `create`, `delete` | Manage roles |
|
|
284
|
+
| [operations](https://neon.com/docs/reference/cli-operations) | `list` | Manage operations |
|
|
285
|
+
| [connection-string](https://neon.com/docs/reference/cli-connection-string) | | Get connection string |
|
|
286
|
+
| psql | | Connect to a database via psql |
|
|
287
|
+
| [set-context](https://neon.com/docs/reference/cli-set-context) | | Set context for session |
|
|
288
|
+
| checkout | | Pin a branch in `.neon` |
|
|
289
|
+
| [link](https://neon.com/docs/reference/cli-link) | | Link a directory to a project |
|
|
290
|
+
| [completion](https://neon.com/docs/reference/cli-completion) | | Generate a completion script |
|
|
80
291
|
|
|
81
292
|
## Global options
|
|
82
293
|
|
|
@@ -142,17 +353,19 @@ Global options are supported with any Neon CLI command.
|
|
|
142
353
|
|
|
143
354
|
## Contribute
|
|
144
355
|
|
|
356
|
+
This repo uses [pnpm](https://pnpm.io). The required version is pinned in `.tool-versions` and `package.json`'s `packageManager` field. The simplest way to get the right version is [mise](https://mise.jdx.dev): `mise install` reads `.tool-versions` and installs Node and pnpm. Alternatives: `npm install -g pnpm@9.15.9`, or [Corepack](https://nodejs.org/api/corepack.html) (`corepack enable pnpm`).
|
|
357
|
+
|
|
145
358
|
To run the CLI locally, execute the build command after making changes:
|
|
146
359
|
|
|
147
360
|
```shell
|
|
148
|
-
|
|
149
|
-
|
|
361
|
+
pnpm install
|
|
362
|
+
pnpm run build
|
|
150
363
|
```
|
|
151
364
|
|
|
152
365
|
To develop continuously:
|
|
153
366
|
|
|
154
367
|
```shell
|
|
155
|
-
|
|
368
|
+
pnpm run watch
|
|
156
369
|
```
|
|
157
370
|
|
|
158
371
|
To run commands from the local build, replace the `neonctl` command with `node dist`; for example:
|
|
@@ -160,3 +373,16 @@ To run commands from the local build, replace the `neonctl` command with `node d
|
|
|
160
373
|
```shell
|
|
161
374
|
node dist branches --help
|
|
162
375
|
```
|
|
376
|
+
|
|
377
|
+
### Embedded psql tests
|
|
378
|
+
|
|
379
|
+
The embedded TypeScript psql implementation has its own conformance test suite that runs the same scripts against the embedded psql and a reference `psql` binary, then diffs the output.
|
|
380
|
+
|
|
381
|
+
```shell
|
|
382
|
+
bun run test:conformance # run against $PSQL_BINARY (defaults to the system psql)
|
|
383
|
+
bun run test:conformance:matrix # run across PG 14/15/16/17/18 locally (requires Docker)
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Releasing
|
|
387
|
+
|
|
388
|
+
Maintainers: see [`RELEASING.md`](./RELEASING.md) for the two-stage publish flow.
|
package/analytics.js
CHANGED
|
@@ -84,10 +84,13 @@ export const analyticsMiddleware = async (args) => {
|
|
|
84
84
|
},
|
|
85
85
|
});
|
|
86
86
|
};
|
|
87
|
-
export const closeAnalytics = async () => {
|
|
87
|
+
export const closeAnalytics = async (opts) => {
|
|
88
88
|
if (client) {
|
|
89
89
|
log.debug('Flushing CLI analytics');
|
|
90
|
-
|
|
90
|
+
// `timeout` bounds how long we wait for in-flight events to flush so a
|
|
91
|
+
// slow / unreachable track.neon.tech can't hang a short-lived command
|
|
92
|
+
// (e.g. the psql launch path, which flushes here before process.exit).
|
|
93
|
+
await client.closeAndFlush(opts);
|
|
91
94
|
log.debug('Flushed CLI analytics');
|
|
92
95
|
}
|
|
93
96
|
};
|
package/commands/branches.js
CHANGED
|
@@ -75,6 +75,12 @@ export const builder = (argv) => argv
|
|
|
75
75
|
describe: 'Connect to a new branch via psql',
|
|
76
76
|
default: false,
|
|
77
77
|
},
|
|
78
|
+
fallback: {
|
|
79
|
+
type: 'boolean',
|
|
80
|
+
describe: 'Force the embedded TypeScript psql fallback (for testing)',
|
|
81
|
+
default: false,
|
|
82
|
+
hidden: true,
|
|
83
|
+
},
|
|
78
84
|
annotation: {
|
|
79
85
|
type: 'string',
|
|
80
86
|
hidden: true,
|
|
@@ -325,7 +331,9 @@ const create = async (props) => {
|
|
|
325
331
|
}
|
|
326
332
|
const connection_uri = data.connection_uris[0].connection_uri;
|
|
327
333
|
const psqlArgs = props['--'];
|
|
328
|
-
await psql(connection_uri, psqlArgs
|
|
334
|
+
await psql(connection_uri, psqlArgs, {
|
|
335
|
+
mode: props.fallback ? 'ts' : 'auto',
|
|
336
|
+
});
|
|
329
337
|
}
|
|
330
338
|
};
|
|
331
339
|
const rename = async (props) => {
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { EndpointType } from '@neondatabase/api-client';
|
|
2
|
+
import { isAxiosError } from 'axios';
|
|
3
|
+
import prompts from 'prompts';
|
|
4
|
+
import { retryOnLock } from '../api.js';
|
|
5
|
+
import { applyContext, readContextFile } from '../context.js';
|
|
6
|
+
import { isCi } from '../env.js';
|
|
7
|
+
import { log } from '../log.js';
|
|
8
|
+
import { fillSingleProject } from '../utils/enrichers.js';
|
|
9
|
+
import { looksLikeBranchId } from '../utils/formats.js';
|
|
10
|
+
import { handler as linkHandler } from './link.js';
|
|
11
|
+
// The positional is optional: omitting it in an interactive terminal opens a
|
|
12
|
+
// branch picker. In non-interactive contexts a missing branch is an error.
|
|
13
|
+
export const command = 'checkout [id|name]';
|
|
14
|
+
export const describe = 'Pin a branch in the local context (.neon) so subsequent commands target it';
|
|
15
|
+
export const builder = (argv) => argv
|
|
16
|
+
.usage('$0 checkout [id|name] [options]')
|
|
17
|
+
.positional('id', {
|
|
18
|
+
describe: 'Branch name or id to check out. Omit to pick interactively from the list of branches.',
|
|
19
|
+
type: 'string',
|
|
20
|
+
})
|
|
21
|
+
.options({
|
|
22
|
+
'project-id': {
|
|
23
|
+
describe: 'Project ID',
|
|
24
|
+
type: 'string',
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
.example([
|
|
28
|
+
[
|
|
29
|
+
'$0 checkout',
|
|
30
|
+
'Pick a branch interactively from the project in the closest .neon file',
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
'$0 checkout main',
|
|
34
|
+
'Pin the branch named "main" in the closest .neon file',
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
'$0 checkout br-cool-snow-12345678 --project-id project-id-123',
|
|
38
|
+
'Pin a branch by id for an explicit project',
|
|
39
|
+
],
|
|
40
|
+
]);
|
|
41
|
+
export const handler = async (props) => {
|
|
42
|
+
// Branch listing is project-scoped, so `projectId` is the only thing
|
|
43
|
+
// `checkout` actually needs. Resolve it through the standard chain
|
|
44
|
+
// (--project-id flag > .neon file > single-project auto-detect); when
|
|
45
|
+
// nothing resolves, fall back to an interactive `neonctl link`.
|
|
46
|
+
const projectId = await resolveProjectId(props);
|
|
47
|
+
const branchId = await resolveBranchId(props, projectId);
|
|
48
|
+
const orgId = await resolveOrgId(props, projectId);
|
|
49
|
+
// `checkout` is a thin helper over `set-context`. It fully "heals" the
|
|
50
|
+
// context file: it always (re)writes `projectId`, `branchId`, and `orgId`
|
|
51
|
+
// (when the project has one) so a `.neon` that drifted or was missing fields
|
|
52
|
+
// ends up complete and consistent after checkout.
|
|
53
|
+
applyContext(props.contextFile, {
|
|
54
|
+
projectId,
|
|
55
|
+
...(orgId ? { orgId } : {}),
|
|
56
|
+
branchId,
|
|
57
|
+
});
|
|
58
|
+
log.info('Checked out branch %s on project %s%s. Updated %s.', branchId, projectId, orgId ? ` (org ${orgId})` : '', props.contextFile);
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the branch id to check out.
|
|
62
|
+
*
|
|
63
|
+
* - Branch **id** (`br-…`): looked up by id. A non-existent id is a hard "not
|
|
64
|
+
* found" error — we never offer to create one, since ids are server-assigned.
|
|
65
|
+
* - Branch **name**: looked up by name. If it doesn't exist, in an interactive
|
|
66
|
+
* terminal we offer to create it (like `neonctl branch create --name <name>`);
|
|
67
|
+
* in a non-interactive context it's the usual "not found" error.
|
|
68
|
+
* - **Omitted**: open an interactive picker listing the project's branches (TTY
|
|
69
|
+
* only); in a non-interactive context a missing branch is a hard error.
|
|
70
|
+
*/
|
|
71
|
+
const resolveBranchId = async (props, projectId) => {
|
|
72
|
+
const branches = (await props.apiClient.listProjectBranches({ projectId }))
|
|
73
|
+
.data.branches;
|
|
74
|
+
if (!props.id) {
|
|
75
|
+
return pickBranchInteractively(branches, projectId);
|
|
76
|
+
}
|
|
77
|
+
const ref = props.id;
|
|
78
|
+
// A `br-…` value is an id; match strictly by id and never offer to create.
|
|
79
|
+
if (looksLikeBranchId(ref)) {
|
|
80
|
+
const byId = branches.find((b) => b.id === ref);
|
|
81
|
+
if (byId) {
|
|
82
|
+
return byId.id;
|
|
83
|
+
}
|
|
84
|
+
throw new Error(notFoundMessage(ref, branches));
|
|
85
|
+
}
|
|
86
|
+
const byName = branches.find((b) => b.name === ref);
|
|
87
|
+
if (byName) {
|
|
88
|
+
return byName.id;
|
|
89
|
+
}
|
|
90
|
+
// Name not found: offer to create it interactively, mirroring `branch create`.
|
|
91
|
+
if (isCi() || !process.stdout.isTTY) {
|
|
92
|
+
throw new Error(notFoundMessage(ref, branches));
|
|
93
|
+
}
|
|
94
|
+
log.error(notFoundMessage(ref, branches));
|
|
95
|
+
const { create } = await prompts({
|
|
96
|
+
type: 'confirm',
|
|
97
|
+
name: 'create',
|
|
98
|
+
message: `Branch "${ref}" does not exist. Create it now?`,
|
|
99
|
+
initial: true,
|
|
100
|
+
});
|
|
101
|
+
if (!create) {
|
|
102
|
+
throw new Error(`Aborted: branch "${ref}" was not found and not created.`);
|
|
103
|
+
}
|
|
104
|
+
return createBranch(props, projectId, ref, branches);
|
|
105
|
+
};
|
|
106
|
+
const notFoundMessage = (ref, branches) => `Branch ${ref} not found.\nAvailable branches: ${branches
|
|
107
|
+
.map((b) => b.name)
|
|
108
|
+
.join(', ')}`;
|
|
109
|
+
const pickBranchInteractively = async (branches, projectId) => {
|
|
110
|
+
if (isCi() || !process.stdout.isTTY) {
|
|
111
|
+
throw new Error('No branch specified. Pass a branch name or id (e.g. `neonctl checkout main`), ' +
|
|
112
|
+
'or run interactively to pick one from a list.');
|
|
113
|
+
}
|
|
114
|
+
if (branches.length === 0) {
|
|
115
|
+
throw new Error(`No branches found for project ${projectId}.`);
|
|
116
|
+
}
|
|
117
|
+
const defaultIndex = Math.max(0, branches.findIndex((b) => b.default));
|
|
118
|
+
const { branchId } = await prompts({
|
|
119
|
+
type: 'select',
|
|
120
|
+
name: 'branchId',
|
|
121
|
+
message: 'Which branch would you like to check out?',
|
|
122
|
+
choices: branches.map((b) => ({
|
|
123
|
+
title: `${b.default ? '✱ ' : ''}${b.name} (${b.id})`,
|
|
124
|
+
value: b.id,
|
|
125
|
+
})),
|
|
126
|
+
initial: defaultIndex,
|
|
127
|
+
});
|
|
128
|
+
if (!branchId) {
|
|
129
|
+
throw new Error('Aborted: no branch selected.');
|
|
130
|
+
}
|
|
131
|
+
return branchId;
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Create a branch with the same defaults as `neonctl branch create --name <name>`:
|
|
135
|
+
* branched from the project's default branch with a read-write compute endpoint.
|
|
136
|
+
*/
|
|
137
|
+
const createBranch = async (props, projectId, name, branches) => {
|
|
138
|
+
const defaultBranch = branches.find((b) => b.default);
|
|
139
|
+
if (!defaultBranch) {
|
|
140
|
+
throw new Error('No default branch found');
|
|
141
|
+
}
|
|
142
|
+
const { data } = await retryOnLock(() => props.apiClient.createProjectBranch(projectId, {
|
|
143
|
+
branch: { name, parent_id: defaultBranch.id },
|
|
144
|
+
endpoints: [{ type: EndpointType.ReadWrite }],
|
|
145
|
+
}));
|
|
146
|
+
if (defaultBranch.protected) {
|
|
147
|
+
log.warning('The parent branch is protected; a unique role password has been generated for the new branch.');
|
|
148
|
+
}
|
|
149
|
+
log.info('Created branch %s (%s).', data.branch.name, data.branch.id);
|
|
150
|
+
return data.branch.id;
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Resolve the org id to heal into the context file.
|
|
154
|
+
*
|
|
155
|
+
* Prefer an org id we already know (from `--org-id`, the `.neon` file, or a
|
|
156
|
+
* freshly-run `link`). Otherwise look it up from the project itself so the
|
|
157
|
+
* `.neon` file ends up with an accurate `orgId` even when it was previously
|
|
158
|
+
* missing. Projects on a personal account have no org; in that case (or if the
|
|
159
|
+
* lookup fails for a non-auth reason) we return `undefined` and simply omit the
|
|
160
|
+
* field rather than failing the checkout.
|
|
161
|
+
*/
|
|
162
|
+
const resolveOrgId = async (props, projectId) => {
|
|
163
|
+
if (props.orgId) {
|
|
164
|
+
return props.orgId;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const { data } = await props.apiClient.getProject(projectId);
|
|
168
|
+
return data.project.org_id ?? undefined;
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
if (isAxiosError(err) && err.response?.status === 401) {
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
log.debug('checkout: could not resolve org id for project %s: %s', projectId, err instanceof Error ? err.message : String(err));
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* Resolve the project id `checkout` should target.
|
|
180
|
+
*
|
|
181
|
+
* `props.projectId` is already populated from the `--project-id` flag or the
|
|
182
|
+
* closest `.neon` file (via the global `enrichFromContext` middleware). When
|
|
183
|
+
* it's still missing we try to auto-detect a single project (same behaviour as
|
|
184
|
+
* `branches` / `connection-string`). If that fails we surface a telling error
|
|
185
|
+
* and, in an interactive terminal, offer to run `neonctl link` in the current
|
|
186
|
+
* folder so the user can pick a project/branch without having to re-run the
|
|
187
|
+
* command by hand.
|
|
188
|
+
*/
|
|
189
|
+
const resolveProjectId = async (props) => {
|
|
190
|
+
if (props.projectId) {
|
|
191
|
+
return props.projectId;
|
|
192
|
+
}
|
|
193
|
+
const autoDetected = await tryAutoDetectProject(props);
|
|
194
|
+
if (autoDetected) {
|
|
195
|
+
return autoDetected;
|
|
196
|
+
}
|
|
197
|
+
const missingProjectMessage = 'Could not determine which Neon project to check out a branch from. ' +
|
|
198
|
+
'Provide one via the --project-id flag ' +
|
|
199
|
+
'or a .neon file (created by `neonctl link` / `neonctl set-context`).';
|
|
200
|
+
if (isCi() || !process.stdout.isTTY) {
|
|
201
|
+
throw new Error(missingProjectMessage);
|
|
202
|
+
}
|
|
203
|
+
log.error(missingProjectMessage);
|
|
204
|
+
const { runLink } = await prompts({
|
|
205
|
+
type: 'confirm',
|
|
206
|
+
name: 'runLink',
|
|
207
|
+
message: 'Run `neonctl link` in the current folder to pick a project now?',
|
|
208
|
+
initial: true,
|
|
209
|
+
});
|
|
210
|
+
if (!runLink) {
|
|
211
|
+
throw new Error('Aborted: no project selected. Re-run with --project-id or link a project first.');
|
|
212
|
+
}
|
|
213
|
+
await linkHandler({
|
|
214
|
+
...props,
|
|
215
|
+
agent: false,
|
|
216
|
+
yes: false,
|
|
217
|
+
});
|
|
218
|
+
const linked = readContextFile(props.contextFile);
|
|
219
|
+
if (!linked.projectId) {
|
|
220
|
+
throw new Error('Linking did not produce a project id. Re-run `neonctl checkout` once the directory is linked.');
|
|
221
|
+
}
|
|
222
|
+
// Carry the freshly-linked org id forward so the merge below keeps it.
|
|
223
|
+
if (linked.orgId) {
|
|
224
|
+
props.orgId = linked.orgId;
|
|
225
|
+
}
|
|
226
|
+
return linked.projectId;
|
|
227
|
+
};
|
|
228
|
+
/**
|
|
229
|
+
* Best-effort single-project auto-detection. Returns the project id when the
|
|
230
|
+
* API key maps to exactly one project, or `undefined` when the project can't be
|
|
231
|
+
* determined unambiguously (zero or multiple projects) so the caller can fall
|
|
232
|
+
* back to the interactive `link` flow.
|
|
233
|
+
*/
|
|
234
|
+
const tryAutoDetectProject = async (props) => {
|
|
235
|
+
try {
|
|
236
|
+
const filled = await fillSingleProject(props);
|
|
237
|
+
return filled.projectId;
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
// `fillSingleProject` throws on "No projects found" / "Multiple projects
|
|
241
|
+
// found" — both mean we can't pick a project automatically. Network/auth
|
|
242
|
+
// errors are real and should surface to the user.
|
|
243
|
+
if (isAxiosError(err)) {
|
|
244
|
+
throw err;
|
|
245
|
+
}
|
|
246
|
+
log.debug('checkout: could not auto-detect a single project: %s', err instanceof Error ? err.message : String(err));
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
};
|
|
@@ -3,7 +3,12 @@ import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
|
|
|
3
3
|
import { writer } from '../writer.js';
|
|
4
4
|
import { psql } from '../utils/psql.js';
|
|
5
5
|
import { parsePITBranch } from '../utils/point_in_time.js';
|
|
6
|
-
const SSL_MODES = [
|
|
6
|
+
export const SSL_MODES = [
|
|
7
|
+
'require',
|
|
8
|
+
'verify-ca',
|
|
9
|
+
'verify-full',
|
|
10
|
+
'omit',
|
|
11
|
+
];
|
|
7
12
|
export const command = 'connection-string [branch]';
|
|
8
13
|
export const aliases = ['cs'];
|
|
9
14
|
export const describe = 'Get connection string';
|
|
@@ -54,6 +59,12 @@ export const builder = (argv) => {
|
|
|
54
59
|
describe: 'Connect to a database via psql using connection string',
|
|
55
60
|
default: false,
|
|
56
61
|
},
|
|
62
|
+
fallback: {
|
|
63
|
+
type: 'boolean',
|
|
64
|
+
describe: 'Force the embedded TypeScript psql fallback (for testing)',
|
|
65
|
+
default: false,
|
|
66
|
+
hidden: true,
|
|
67
|
+
},
|
|
57
68
|
ssl: {
|
|
58
69
|
type: 'string',
|
|
59
70
|
choices: SSL_MODES,
|
|
@@ -141,7 +152,9 @@ export const handler = async (props) => {
|
|
|
141
152
|
}
|
|
142
153
|
if (props.psql) {
|
|
143
154
|
const psqlArgs = props['--'];
|
|
144
|
-
await psql(connectionString.toString(), psqlArgs
|
|
155
|
+
await psql(connectionString.toString(), psqlArgs, {
|
|
156
|
+
mode: props.fallback ? 'ts' : 'auto',
|
|
157
|
+
});
|
|
145
158
|
}
|
|
146
159
|
else if (props.extended) {
|
|
147
160
|
writer(props).end({
|