neonctl 2.22.2 → 2.23.1
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 +84 -0
- package/analytics.js +5 -2
- package/commands/branches.js +9 -1
- package/commands/connection_string.js +9 -1
- package/commands/functions.js +268 -0
- package/commands/index.js +4 -0
- package/commands/neon_auth.js +1013 -0
- package/commands/projects.js +9 -1
- package/commands/psql.js +6 -1
- package/functions_api.js +43 -0
- package/package.json +15 -5
- 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/esbuild.js +147 -0
- 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/checkout.test.js +0 -170
- package/commands/connection_string.test.js +0 -196
- package/commands/data_api.test.js +0 -169
- 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/link.test.js +0 -381
- package/commands/operations.test.js +0 -7
- package/commands/orgs.test.js +0 -7
- package/commands/projects.test.js +0 -144
- package/commands/psql.test.js +0 -49
- package/commands/roles.test.js +0 -37
- package/commands/set_context.test.js +0 -159
- package/commands/vpc_endpoints.test.js +0 -69
- package/context.test.js +0 -119
- package/env.test.js +0 -55
- package/utils/formats.test.js +0 -32
- package/writer.test.js +0 -104
package/README.md
CHANGED
|
@@ -58,6 +58,80 @@ 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.
|
|
@@ -205,6 +279,7 @@ $ cat .neon
|
|
|
205
279
|
| [me](https://neon.com/docs/reference/cli-me) | | Show current user |
|
|
206
280
|
| [branches](https://neon.com/docs/reference/cli-branches) | `list`, `create`, `rename`, `add-compute`, `set-default`, `set-expiration`, `delete`, `get` | Manage branches |
|
|
207
281
|
| [databases](https://neon.com/docs/reference/cli-databases) | `list`, `create`, `delete` | Manage databases |
|
|
282
|
+
| functions | `deploy`, `list`, `get`, `delete` | Manage Neon Functions |
|
|
208
283
|
| [roles](https://neon.com/docs/reference/cli-roles) | `list`, `create`, `delete` | Manage roles |
|
|
209
284
|
| [operations](https://neon.com/docs/reference/cli-operations) | `list` | Manage operations |
|
|
210
285
|
| [connection-string](https://neon.com/docs/reference/cli-connection-string) | | Get connection string |
|
|
@@ -299,6 +374,15 @@ To run commands from the local build, replace the `neonctl` command with `node d
|
|
|
299
374
|
node dist branches --help
|
|
300
375
|
```
|
|
301
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
|
+
|
|
302
386
|
## Releasing
|
|
303
387
|
|
|
304
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) => {
|
|
@@ -59,6 +59,12 @@ export const builder = (argv) => {
|
|
|
59
59
|
describe: 'Connect to a database via psql using connection string',
|
|
60
60
|
default: false,
|
|
61
61
|
},
|
|
62
|
+
fallback: {
|
|
63
|
+
type: 'boolean',
|
|
64
|
+
describe: 'Force the embedded TypeScript psql fallback (for testing)',
|
|
65
|
+
default: false,
|
|
66
|
+
hidden: true,
|
|
67
|
+
},
|
|
62
68
|
ssl: {
|
|
63
69
|
type: 'string',
|
|
64
70
|
choices: SSL_MODES,
|
|
@@ -146,7 +152,9 @@ export const handler = async (props) => {
|
|
|
146
152
|
}
|
|
147
153
|
if (props.psql) {
|
|
148
154
|
const psqlArgs = props['--'];
|
|
149
|
-
await psql(connectionString.toString(), psqlArgs
|
|
155
|
+
await psql(connectionString.toString(), psqlArgs, {
|
|
156
|
+
mode: props.fallback ? 'ts' : 'auto',
|
|
157
|
+
});
|
|
150
158
|
}
|
|
151
159
|
else if (props.extended) {
|
|
152
160
|
writer(props).end({
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { isAxiosError } from 'axios';
|
|
4
|
+
import { retryOnLock } from '../api.js';
|
|
5
|
+
import { log } from '../log.js';
|
|
6
|
+
import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
|
|
7
|
+
import { zipBundle } from '../utils/zip.js';
|
|
8
|
+
import { bundleEntry } from '../utils/esbuild.js';
|
|
9
|
+
import { writer } from '../writer.js';
|
|
10
|
+
import { createDeployment, deleteFunction, getFunction, listFunctions, } from '../functions_api.js';
|
|
11
|
+
const FUNCTION_FIELDS = [
|
|
12
|
+
'slug',
|
|
13
|
+
'name',
|
|
14
|
+
'invocation_url',
|
|
15
|
+
'created_at',
|
|
16
|
+
];
|
|
17
|
+
const DEPLOYMENT_FIELDS = [
|
|
18
|
+
'id',
|
|
19
|
+
'status',
|
|
20
|
+
'runtime',
|
|
21
|
+
'memory_mib',
|
|
22
|
+
'created_at',
|
|
23
|
+
];
|
|
24
|
+
const SLUG_PATTERN = /^[a-z0-9]{1,20}$/;
|
|
25
|
+
const SLUG_HELP = 'Use 1-20 lowercase letters and digits (no hyphens or other characters).';
|
|
26
|
+
// Overridable so tests can poll fast; defaults to 2s in real use.
|
|
27
|
+
const POLL_INTERVAL_MS = Number(process.env.NEON_FUNCTIONS_POLL_INTERVAL_MS) || 2000;
|
|
28
|
+
// Upper bound on --wait polling so the CLI never hangs (e.g. if our deployment
|
|
29
|
+
// never becomes active_deployment). Overridable so tests can time out fast;
|
|
30
|
+
// defaults to 10 minutes in real use.
|
|
31
|
+
const POLL_TIMEOUT_MS = Number(process.env.NEON_FUNCTIONS_POLL_TIMEOUT_MS) || 600000;
|
|
32
|
+
export const command = 'functions';
|
|
33
|
+
export const describe = 'Manage Neon Functions';
|
|
34
|
+
export const aliases = ['function'];
|
|
35
|
+
export const builder = (argv) => argv
|
|
36
|
+
.usage('$0 functions <sub-command> [options]')
|
|
37
|
+
.options({
|
|
38
|
+
'project-id': {
|
|
39
|
+
describe: 'Project ID',
|
|
40
|
+
type: 'string',
|
|
41
|
+
},
|
|
42
|
+
branch: {
|
|
43
|
+
describe: 'Branch ID or name',
|
|
44
|
+
type: 'string',
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
.middleware(fillSingleProject)
|
|
48
|
+
.command('deploy <slug>', 'Deploy a function from a local directory', (yargs) => yargs
|
|
49
|
+
.positional('slug', {
|
|
50
|
+
describe: 'Function slug (1-20 lowercase letters and digits)',
|
|
51
|
+
type: 'string',
|
|
52
|
+
demandOption: true,
|
|
53
|
+
})
|
|
54
|
+
.options({
|
|
55
|
+
path: {
|
|
56
|
+
describe: 'Base directory for the function (resolves --entry)',
|
|
57
|
+
type: 'string',
|
|
58
|
+
},
|
|
59
|
+
entry: {
|
|
60
|
+
describe: 'Entry file to bundle, relative to --path',
|
|
61
|
+
type: 'string',
|
|
62
|
+
},
|
|
63
|
+
runtime: {
|
|
64
|
+
describe: 'Function runtime',
|
|
65
|
+
type: 'string',
|
|
66
|
+
choices: ['nodejs24'],
|
|
67
|
+
},
|
|
68
|
+
env: {
|
|
69
|
+
describe: 'Environment variable as KEY=VALUE (repeatable)',
|
|
70
|
+
type: 'string',
|
|
71
|
+
array: true,
|
|
72
|
+
},
|
|
73
|
+
wait: {
|
|
74
|
+
describe: 'Wait for the deployment to finish building',
|
|
75
|
+
type: 'boolean',
|
|
76
|
+
default: true,
|
|
77
|
+
},
|
|
78
|
+
}), (args) => deploy(args))
|
|
79
|
+
.command('list', 'List functions on the branch', (yargs) => yargs, (args) => list(args))
|
|
80
|
+
.command('get <slug>', "Show a function's details", (yargs) => yargs.positional('slug', {
|
|
81
|
+
describe: 'Function slug',
|
|
82
|
+
type: 'string',
|
|
83
|
+
demandOption: true,
|
|
84
|
+
}), (args) => get(args))
|
|
85
|
+
.command('delete <slug>', 'Delete a function on the branch', (yargs) => yargs.positional('slug', {
|
|
86
|
+
describe: 'Function slug',
|
|
87
|
+
type: 'string',
|
|
88
|
+
demandOption: true,
|
|
89
|
+
}), (args) => deleteFn(args));
|
|
90
|
+
export const handler = (args) => {
|
|
91
|
+
return args;
|
|
92
|
+
};
|
|
93
|
+
const parseEnv = (entries) => {
|
|
94
|
+
if (!entries || entries.length === 0)
|
|
95
|
+
return undefined;
|
|
96
|
+
const map = {};
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const eq = entry.indexOf('=');
|
|
99
|
+
if (eq <= 0) {
|
|
100
|
+
throw new Error(`Invalid --env value "${entry}". Expected KEY=VALUE.`);
|
|
101
|
+
}
|
|
102
|
+
map[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
103
|
+
}
|
|
104
|
+
return JSON.stringify(map);
|
|
105
|
+
};
|
|
106
|
+
const statusHint = (slug, projectId, branchId) => `Check status with: neonctl functions get ${slug} --project-id ${projectId} --branch ${branchId}`;
|
|
107
|
+
// A poll error worth retrying: a network error (no HTTP response), a 5xx, or a
|
|
108
|
+
// 404 from eventual consistency. Anything else (e.g. 401/403) is surfaced.
|
|
109
|
+
const isTransient = (err) => isAxiosError(err) &&
|
|
110
|
+
(err.response === undefined ||
|
|
111
|
+
err.response.status === 404 ||
|
|
112
|
+
err.response.status >= 500);
|
|
113
|
+
const deploy = async (props) => {
|
|
114
|
+
// At least one deploy option must be passed (--wait is excluded: it controls
|
|
115
|
+
// output, not what gets deployed).
|
|
116
|
+
const hasOption = props.path !== undefined ||
|
|
117
|
+
props.entry !== undefined ||
|
|
118
|
+
props.env !== undefined ||
|
|
119
|
+
props.runtime !== undefined;
|
|
120
|
+
if (!hasOption) {
|
|
121
|
+
throw new Error('Provide at least one option to deploy, e.g. --path, --entry, or --env. ' +
|
|
122
|
+
'See: neonctl functions deploy --help.');
|
|
123
|
+
}
|
|
124
|
+
// Cheap, offline validation first - fail before any network round-trip.
|
|
125
|
+
if (!SLUG_PATTERN.test(props.slug)) {
|
|
126
|
+
throw new Error(`Invalid function slug "${props.slug}". ${SLUG_HELP}`);
|
|
127
|
+
}
|
|
128
|
+
const path = props.path ?? '.';
|
|
129
|
+
const entry = props.entry ?? 'index.ts';
|
|
130
|
+
const runtime = props.runtime ?? 'nodejs24';
|
|
131
|
+
const environment = parseEnv(props.env);
|
|
132
|
+
const source = join(path, entry);
|
|
133
|
+
if (!existsSync(source)) {
|
|
134
|
+
throw new Error(`Entry file not found: ${source}. Pass --entry to point at your function's entry file (defaults to index.ts).`);
|
|
135
|
+
}
|
|
136
|
+
// Bundle before any network round-trip so a bundling failure fails fast.
|
|
137
|
+
const zip = zipBundle(await bundleEntry(source));
|
|
138
|
+
const branchId = await branchIdFromProps(props);
|
|
139
|
+
// Snapshot the active version before deploy so we can detect the new one
|
|
140
|
+
// afterward. A missing function (404) or no active version → undefined.
|
|
141
|
+
let before;
|
|
142
|
+
try {
|
|
143
|
+
const fn = await getFunction(props.apiClient, props.projectId, branchId, props.slug);
|
|
144
|
+
before = fn.active_deployment?.id;
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
if (!(isAxiosError(err) && err.response?.status === 404))
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
await retryOnLock(() => createDeployment(props.apiClient, props.projectId, branchId, props.slug, {
|
|
151
|
+
zip,
|
|
152
|
+
runtime,
|
|
153
|
+
environment,
|
|
154
|
+
}));
|
|
155
|
+
log.info(`Function deployment triggered for function ${props.slug}.`);
|
|
156
|
+
// Best-effort interrupt: a Ctrl-C lands at the next poll boundary. (No
|
|
157
|
+
// automated test; mirrors the resolution branches below, verified manually.)
|
|
158
|
+
let interrupted = false;
|
|
159
|
+
const onSignal = () => {
|
|
160
|
+
interrupted = true;
|
|
161
|
+
};
|
|
162
|
+
process.once('SIGINT', onSignal);
|
|
163
|
+
process.once('SIGTERM', onSignal);
|
|
164
|
+
// Poll until a NEW active version appears (id greater than the snapshot, or
|
|
165
|
+
// any version if there was none). --no-wait stops there; --wait stops at a
|
|
166
|
+
// terminal status. Bounded by POLL_TIMEOUT_MS so it never hangs.
|
|
167
|
+
let resolved;
|
|
168
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
169
|
+
try {
|
|
170
|
+
while (!interrupted && Date.now() < deadline) {
|
|
171
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
172
|
+
if (interrupted)
|
|
173
|
+
break;
|
|
174
|
+
// The deploy already succeeded server-side; tolerate transient poll
|
|
175
|
+
// failures and retry on the next interval. Surface anything else.
|
|
176
|
+
let dep;
|
|
177
|
+
try {
|
|
178
|
+
dep = (await getFunction(props.apiClient, props.projectId, branchId, props.slug)).active_deployment;
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
if (isTransient(err))
|
|
182
|
+
continue;
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
const isNew = dep !== undefined && (before === undefined || dep.id > before);
|
|
186
|
+
if (isNew && dep) {
|
|
187
|
+
resolved = dep;
|
|
188
|
+
if (!props.wait)
|
|
189
|
+
break;
|
|
190
|
+
if (dep.status === 'completed' || dep.status === 'failed')
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
process.removeListener('SIGINT', onSignal);
|
|
197
|
+
process.removeListener('SIGTERM', onSignal);
|
|
198
|
+
}
|
|
199
|
+
if (interrupted) {
|
|
200
|
+
log.info(statusHint(props.slug, props.projectId, branchId));
|
|
201
|
+
if (resolved)
|
|
202
|
+
writer(props).end(resolved, { fields: DEPLOYMENT_FIELDS });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (resolved === undefined) {
|
|
206
|
+
log.info(statusHint(props.slug, props.projectId, branchId));
|
|
207
|
+
throw new Error(`Timed out waiting for the deployment of ${props.slug} to start. It may still be in progress.`);
|
|
208
|
+
}
|
|
209
|
+
writer(props).end(resolved, { fields: DEPLOYMENT_FIELDS });
|
|
210
|
+
if (!props.wait) {
|
|
211
|
+
log.info(statusHint(props.slug, props.projectId, branchId));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (resolved.status === 'completed') {
|
|
215
|
+
log.info(`Function deployment ${props.slug}/${resolved.id} completed.`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (resolved.status === 'failed') {
|
|
219
|
+
throw new Error(`Function deployment ${props.slug}/${resolved.id} failed.`);
|
|
220
|
+
}
|
|
221
|
+
// --wait, new version appeared but the deadline hit before it finished.
|
|
222
|
+
log.info(statusHint(props.slug, props.projectId, branchId));
|
|
223
|
+
throw new Error(`Timed out waiting for function deployment ${props.slug}/${resolved.id} to finish. It may still be building.`);
|
|
224
|
+
};
|
|
225
|
+
const get = async (props) => {
|
|
226
|
+
const branchId = await branchIdFromProps(props);
|
|
227
|
+
const fn = await getFunction(props.apiClient, props.projectId, branchId, props.slug);
|
|
228
|
+
if (props.output === 'json' || props.output === 'yaml') {
|
|
229
|
+
writer(props).end(fn, { fields: FUNCTION_FIELDS });
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const out = writer(props).write(fn, {
|
|
233
|
+
fields: FUNCTION_FIELDS,
|
|
234
|
+
title: 'function',
|
|
235
|
+
});
|
|
236
|
+
if (fn.active_deployment) {
|
|
237
|
+
out.write(fn.active_deployment, {
|
|
238
|
+
fields: DEPLOYMENT_FIELDS,
|
|
239
|
+
title: 'active deployment',
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
out.end();
|
|
243
|
+
};
|
|
244
|
+
const deleteFn = async (props) => {
|
|
245
|
+
const branchId = await branchIdFromProps(props);
|
|
246
|
+
try {
|
|
247
|
+
await retryOnLock(() => deleteFunction(props.apiClient, props.projectId, branchId, props.slug));
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
if (isAxiosError(err) && err.response?.status === 404) {
|
|
251
|
+
throw new Error(`Function "${props.slug}" not found on branch ${branchId}.`);
|
|
252
|
+
}
|
|
253
|
+
throw err;
|
|
254
|
+
}
|
|
255
|
+
log.info(`Function ${props.slug} deleted from branch ${branchId}`);
|
|
256
|
+
};
|
|
257
|
+
const list = async (props) => {
|
|
258
|
+
const branchId = await branchIdFromProps(props);
|
|
259
|
+
const functions = await listFunctions(props.apiClient, props.projectId, branchId);
|
|
260
|
+
if (props.output === 'json' || props.output === 'yaml') {
|
|
261
|
+
writer(props).end(functions, { fields: FUNCTION_FIELDS });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
writer(props).end(functions, {
|
|
265
|
+
fields: FUNCTION_FIELDS,
|
|
266
|
+
emptyMessage: 'No functions found on this branch.',
|
|
267
|
+
});
|
|
268
|
+
};
|
package/commands/index.js
CHANGED
|
@@ -15,6 +15,8 @@ import * as checkout from './checkout.js';
|
|
|
15
15
|
import * as link from './link.js';
|
|
16
16
|
import * as init from './init.js';
|
|
17
17
|
import * as dataApi from './data_api.js';
|
|
18
|
+
import * as neonAuth from './neon_auth.js';
|
|
19
|
+
import * as functions from './functions.js';
|
|
18
20
|
export default [
|
|
19
21
|
auth,
|
|
20
22
|
users,
|
|
@@ -22,6 +24,7 @@ export default [
|
|
|
22
24
|
projects,
|
|
23
25
|
ipAllow,
|
|
24
26
|
vpcEndpoints,
|
|
27
|
+
neonAuth,
|
|
25
28
|
branches,
|
|
26
29
|
databases,
|
|
27
30
|
roles,
|
|
@@ -33,4 +36,5 @@ export default [
|
|
|
33
36
|
link,
|
|
34
37
|
init,
|
|
35
38
|
dataApi,
|
|
39
|
+
functions,
|
|
36
40
|
];
|