neonctl 1.25.3 → 1.25.5

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 CHANGED
@@ -1,40 +1,44 @@
1
- The Neon CLI supports numerous operations, such as authentication and management of Neon projects, branches, compute endpoints, databases, roles, and more.
2
-
3
- The Neon CLI command name is `neonctl`. The GitHub repository for the Neon CLI is found [here](https://github.com/neondatabase/neonctl).
1
+ The Neon CLI is a command-line interface that lets you manage [Neon Serverless Postgres](https://neon.tech/) directly from the terminal. For the complete documentation, see [Neon CLI](https://neon.tech/docs/reference/neon-cli).
4
2
 
5
3
  ## Install the Neon CLI
6
4
 
7
- This section describes how to install the Neon CLI.
5
+ **npm**
8
6
 
9
- ### Prerequisites
7
+ ```shell
8
+ npm i -g neonctl
9
+ ```
10
10
 
11
- Before installing, ensure that you have met the following prerequisites:
11
+ Requires Node.js 18.0 or higher.
12
12
 
13
- - Node.js 16.0 or higher. To check if you already have Node.js, run the following command:
13
+ **Howebrew**
14
14
 
15
- ```shell
16
- node -v
17
- ```
15
+ ```shell
16
+ brew install neonctl
17
+ ```
18
18
 
19
- - The `npm` package manager. To check if you already have `npm`, run the following command:
19
+ **Binary (macOS, Linux, Windows)**
20
20
 
21
- ```shell
22
- npm -v
23
- ```
21
+ Download a binary file [here](https://github.com/neondatabase/neonctl/releases).
24
22
 
25
- If you need to install `Node.js` or `npm`, refer to instructions on the [official nodejs page](https://nodejs.org) or use the [Node version manager](https://github.com/nvm-sh/nvm).
23
+ ### Upgrade
24
+
25
+ **npm**
26
26
 
27
- ### Install
27
+ ```shell
28
+ npm update -g neonctl
29
+ ```
28
30
 
29
- To install the Neon CLI, run the following command:
31
+ Requires Node.js 18.0 or higher.
32
+
33
+ **Howebrew**
30
34
 
31
35
  ```shell
32
- npm i -g neonctl
36
+ brew upgrade neonctl
33
37
  ```
34
38
 
35
- ### Upgrade
39
+ **Binary (macOS, Linux, Windows)**
36
40
 
37
- To upgrade to the latest version of the Neon CLI, run the `npm i -g neonctl` command again.
41
+ To upgrade a binary version, download the latest binary file, as described above, and replace your old binary with the new one.
38
42
 
39
43
  ## Connect
40
44
 
@@ -70,6 +74,7 @@ The Neon CLI supports autocompletion, which you can configure in a few easy step
70
74
  | [roles](https://neon.tech/docs/reference/cli-roles) | `list`, `create`, `delete` | Manage roles |
71
75
  | [operations](https://neon.tech/reference/cli-operations) | `list` | Manage operations |
72
76
  | [connection-string](https://neon.tech/reference/cli-connection-string) | | Get connection string |
77
+ | [set-context](https://neon.tech/reference/cli-set-context) | | Set context for session |
73
78
  | [completion](https://neon.tech/reference/cli-completion) | | Generate a completion script |
74
79
 
75
80
  ## Global options
@@ -136,7 +141,7 @@ Global options are supported with any Neon CLI command.
136
141
 
137
142
  ## Contribute
138
143
 
139
- To run the CLI locally execute build command after making changes:
144
+ To run the CLI locally, execute the build command after making changes:
140
145
 
141
146
  ```shell
142
147
  npm run build
@@ -148,7 +153,7 @@ To develop continuously:
148
153
  npm run watch
149
154
  ```
150
155
 
151
- To run commands from the local build replace the `neonctl` command with `node dist`, for example:
156
+ To run commands from the local build, replace the `neonctl` command with `node dist`; for example:
152
157
 
153
158
  ```shell
154
159
  node dist branches --help
package/commands/auth.js CHANGED
@@ -9,7 +9,9 @@ import { CREDENTIALS_FILE } from '../config.js';
9
9
  export const command = 'auth';
10
10
  export const aliases = ['login'];
11
11
  export const describe = 'Authenticate';
12
- export const builder = (yargs) => yargs;
12
+ export const builder = (yargs) => yargs.option('context-file', {
13
+ hidden: true,
14
+ });
13
15
  export const handler = async (args) => {
14
16
  await authFlow(args);
15
17
  };
package/commands/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as auth from './auth.js';
2
2
  import * as projects from './projects.js';
3
+ import * as ipAllow from './ip_allow.js';
3
4
  import * as users from './user.js';
4
5
  import * as branches from './branches.js';
5
6
  import * as databases from './databases.js';
@@ -11,6 +12,7 @@ export default [
11
12
  auth,
12
13
  users,
13
14
  projects,
15
+ ipAllow,
14
16
  branches,
15
17
  databases,
16
18
  roles,
@@ -0,0 +1,135 @@
1
+ import { writer } from '../writer.js';
2
+ import { projectUpdateRequest } from '../parameters.gen.js';
3
+ import { log } from '../log.js';
4
+ const IP_ALLOW_FIELDS = [
5
+ 'id',
6
+ 'name',
7
+ 'IP_addresses',
8
+ 'primary_branch_only',
9
+ ];
10
+ export const command = 'ip-allow';
11
+ export const describe = 'Manage IP Allow';
12
+ export const builder = (argv) => {
13
+ return argv
14
+ .usage('$0 ip-allow <sub-command> [options]')
15
+ .options({
16
+ 'project-id': {
17
+ describe: 'Project ID',
18
+ type: 'string',
19
+ },
20
+ })
21
+ .command('list', 'List the IP allowlist', (yargs) => yargs, async (args) => {
22
+ await list(args);
23
+ })
24
+ .command('add [ips...]', 'Add IP addresses to the IP allowlist', (yargs) => yargs
25
+ .usage('$0 ip-allow add [ips...]')
26
+ .positional('ips', {
27
+ describe: 'The list of IP addresses to add',
28
+ type: 'string',
29
+ default: [],
30
+ array: true,
31
+ })
32
+ .options({
33
+ 'primary-only': {
34
+ describe: projectUpdateRequest['project.settings.allowed_ips.primary_branch_only'].description,
35
+ type: 'boolean',
36
+ },
37
+ }), async (args) => {
38
+ await add(args);
39
+ })
40
+ .command('remove [ips...]', 'Remove IP addresses from the IP allowlist', (yargs) => yargs.usage('$0 ip-allow remove [ips...]').positional('ips', {
41
+ describe: 'The list of IP addresses to remove',
42
+ type: 'string',
43
+ default: [],
44
+ array: true,
45
+ }), async (args) => {
46
+ await remove(args);
47
+ })
48
+ .command('reset [ips...]', 'Reset the IP allowlist', (yargs) => yargs.usage('$0 ip-allow remove [ips...]').positional('ips', {
49
+ describe: 'The list of IP addresses to reset',
50
+ type: 'string',
51
+ default: [],
52
+ array: true,
53
+ }), async (args) => {
54
+ await reset(args);
55
+ });
56
+ };
57
+ export const handler = (args) => {
58
+ return args;
59
+ };
60
+ const list = async (props) => {
61
+ const { data } = await props.apiClient.getProject(props.projectId);
62
+ writer(props).end(parse(data.project), {
63
+ fields: IP_ALLOW_FIELDS,
64
+ });
65
+ };
66
+ const add = async (props) => {
67
+ if (props.ips.length <= 0) {
68
+ log.error(`Enter individual IP addresses, define ranges with a dash, or use CIDR notation for more flexibility.
69
+ Example: neonctl ip-allow add 192.168.1.1, 192.168.1.20-192.168.1.50, 192.168.1.0/24 --project-id <id>`);
70
+ return;
71
+ }
72
+ const project = {};
73
+ const { data } = await props.apiClient.getProject(props.projectId);
74
+ const existingAllowedIps = data.project.settings?.allowed_ips;
75
+ project.settings = {
76
+ allowed_ips: {
77
+ ips: [...new Set(props.ips.concat(existingAllowedIps?.ips ?? []))],
78
+ primary_branch_only: props.primaryOnly ?? existingAllowedIps?.primary_branch_only ?? false,
79
+ },
80
+ };
81
+ const { data: response } = await props.apiClient.updateProject(props.projectId, {
82
+ project,
83
+ });
84
+ writer(props).end(parse(response.project), {
85
+ fields: IP_ALLOW_FIELDS,
86
+ });
87
+ };
88
+ const remove = async (props) => {
89
+ if (props.ips.length <= 0) {
90
+ log.error(`Remove individual IP addresses and ranges. Example: neonctl ip-allow remove 192.168.1.1 --project-id <id>`);
91
+ return;
92
+ }
93
+ const project = {};
94
+ const { data } = await props.apiClient.getProject(props.projectId);
95
+ const existingAllowedIps = data.project.settings?.allowed_ips;
96
+ project.settings = {
97
+ allowed_ips: {
98
+ ips: existingAllowedIps?.ips.filter((ip) => !props.ips.includes(ip)) ?? [],
99
+ primary_branch_only: existingAllowedIps?.primary_branch_only ?? false,
100
+ },
101
+ };
102
+ const { data: response } = await props.apiClient.updateProject(props.projectId, {
103
+ project,
104
+ });
105
+ writer(props).end(parse(response.project), {
106
+ fields: IP_ALLOW_FIELDS,
107
+ });
108
+ };
109
+ const reset = async (props) => {
110
+ const project = {};
111
+ project.settings = {
112
+ allowed_ips: {
113
+ ips: props.ips,
114
+ primary_branch_only: false,
115
+ },
116
+ };
117
+ const { data } = await props.apiClient.updateProject(props.projectId, {
118
+ project,
119
+ });
120
+ writer(props).end(parse(data.project), {
121
+ fields: IP_ALLOW_FIELDS,
122
+ });
123
+ if (props.ips.length <= 0) {
124
+ log.info(`The IP allowlist has been reset. All databases on project "${data.project.name}" are now exposed to the internet`);
125
+ }
126
+ };
127
+ const parse = (project) => {
128
+ const ips = project.settings?.allowed_ips?.ips ?? [];
129
+ return {
130
+ id: project.id,
131
+ name: project.name,
132
+ IP_addresses: ips,
133
+ primary_branch_only: project.settings?.allowed_ips?.primary_branch_only ?? false,
134
+ };
135
+ };
@@ -0,0 +1,71 @@
1
+ import { describe } from '@jest/globals';
2
+ import { testCliCommand } from '../test_utils/test_cli_command.js';
3
+ describe('ip-allow', () => {
4
+ testCliCommand({
5
+ name: 'list IP allow',
6
+ args: ['ip-allow', 'list', '--project-id', 'test'],
7
+ expected: {
8
+ snapshot: true,
9
+ },
10
+ });
11
+ testCliCommand({
12
+ name: 'Add IP allow - Error',
13
+ args: ['ip-allow', 'add', '--projectId', 'test'],
14
+ expected: {
15
+ stderr: `ERROR: Enter individual IP addresses, define ranges with a dash, or use CIDR notation for more flexibility.
16
+ Example: neonctl ip-allow add 192.168.1.1, 192.168.1.20-192.168.1.50, 192.168.1.0/24 --project-id <id>
17
+ `,
18
+ },
19
+ });
20
+ testCliCommand({
21
+ name: 'Add IP allow',
22
+ args: [
23
+ 'ip-allow',
24
+ 'add',
25
+ '127.0.0.1',
26
+ '192.168.10.1-192.168.10.15',
27
+ '--primary-only',
28
+ '--project-id',
29
+ 'test',
30
+ ],
31
+ expected: {
32
+ snapshot: true,
33
+ },
34
+ });
35
+ testCliCommand({
36
+ name: 'Remove IP allow - Error',
37
+ args: ['ip-allow', 'remove', '--project-id', 'test'],
38
+ expected: {
39
+ stderr: `ERROR: Remove individual IP addresses and ranges. Example: neonctl ip-allow remove 192.168.1.1 --project-id <id>
40
+ `,
41
+ },
42
+ });
43
+ testCliCommand({
44
+ name: 'Remove IP allow',
45
+ args: ['ip-allow', 'remove', '192.168.1.1', '--project-id', 'test'],
46
+ expected: {
47
+ snapshot: true,
48
+ },
49
+ });
50
+ testCliCommand({
51
+ name: 'Reset IP allow',
52
+ args: ['ip-allow', 'reset', '--project-id', 'test'],
53
+ expected: {
54
+ snapshot: true,
55
+ stdout: `id: test
56
+ name: test_project
57
+ IP_addresses: []
58
+ primary_branch_only: false
59
+ `,
60
+ stderr: `INFO: The IP allowlist has been reset. All databases on project "test_project" are now exposed to the internet
61
+ `,
62
+ },
63
+ });
64
+ testCliCommand({
65
+ name: 'Reset IP allow to new list',
66
+ args: ['ip-allow', 'reset', '192.168.2.2', '--project-id', 'test'],
67
+ expected: {
68
+ snapshot: true,
69
+ },
70
+ });
71
+ });
@@ -54,10 +54,12 @@ export const builder = (argv) => {
54
54
  .description,
55
55
  type: 'string',
56
56
  array: true,
57
+ deprecated: "Deprecated. Use 'ip-allow' command",
57
58
  },
58
59
  'ip-primary-only': {
59
60
  describe: projectUpdateRequest['project.settings.allowed_ips.primary_branch_only'].description,
60
61
  type: 'boolean',
62
+ deprecated: "Deprecated. Use 'ip-allow' command",
61
63
  },
62
64
  }), async (args) => {
63
65
  await update(args);
package/commands/user.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { writer } from '../writer.js';
2
2
  export const command = 'me';
3
3
  export const describe = 'Show current user';
4
- export const builder = (yargs) => yargs;
4
+ export const builder = (yargs) => yargs.option('context-file', {
5
+ hidden: true,
6
+ });
5
7
  export const handler = async (args) => {
6
8
  await me(args);
7
9
  };
package/index.js CHANGED
@@ -128,6 +128,7 @@ builder = builder
128
128
  .alias('version', 'v')
129
129
  .completion()
130
130
  .scriptName(basename(process.argv[1]) === 'neon' ? 'neon' : 'neonctl')
131
+ .epilog('For more information, visit https://neon.tech/docs/reference/neon-cli')
131
132
  .fail(async (msg, err) => {
132
133
  if (process.argv.some((arg) => arg === '--help' || arg === '-h')) {
133
134
  await showHelp(builder);
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "git@github.com:neondatabase/neonctl.git"
6
6
  },
7
7
  "type": "module",
8
- "version": "1.25.3",
8
+ "version": "1.25.5",
9
9
  "description": "CLI tool for NeonDB Cloud management",
10
10
  "main": "index.js",
11
11
  "author": "NeonDB",
@@ -90,6 +90,7 @@
90
90
  "clean": "rm -rf dist",
91
91
  "generateParams": "node --loader ts-node/esm ./generateOptionsFromSpec.ts",
92
92
  "start": "node src/index.js",
93
+ "pretest": "npm run build",
93
94
  "test": "node --experimental-vm-modules node_modules/.bin/jest",
94
95
  "prepare": "test -d .git && husky install || true"
95
96
  },
package/utils/string.js CHANGED
@@ -2,3 +2,4 @@ export const toSnakeCase = (str) => str
2
2
  .split(' ')
3
3
  .map((word) => word.toLowerCase())
4
4
  .join('_');
5
+ export const isObject = (value) => value != null && value === Object(value);
package/writer.js CHANGED
@@ -2,7 +2,53 @@ import YAML from 'yaml';
2
2
  import Table from 'cli-table';
3
3
  import chalk from 'chalk';
4
4
  import { isCi } from './env.js';
5
- import { toSnakeCase } from './utils/string.js';
5
+ import { isObject, toSnakeCase } from './utils/string.js';
6
+ const writeYaml = (chunks) => {
7
+ return YAML.stringify(chunks.length === 1
8
+ ? chunks[0].data
9
+ : Object.fromEntries(chunks.map(({ config, data }, idx) => [
10
+ config.title ? toSnakeCase(config.title) : idx,
11
+ data,
12
+ ])), null, 2);
13
+ };
14
+ const writeJson = (chunks) => {
15
+ return JSON.stringify(chunks.length === 1
16
+ ? chunks[0].data
17
+ : Object.fromEntries(chunks.map(({ config, data }, idx) => [
18
+ config.title ? toSnakeCase(config.title) : idx,
19
+ data,
20
+ ])), null, 2);
21
+ };
22
+ const writeTable = (chunks, out) => {
23
+ chunks.forEach(({ data, config }) => {
24
+ const arrayData = Array.isArray(data) ? data : [data];
25
+ const fields = config.fields.filter((field) => arrayData.some((item) => item[field] !== undefined && item[field] !== ''));
26
+ const table = new Table({
27
+ style: {
28
+ head: ['green'],
29
+ },
30
+ head: fields.map((field) => field
31
+ .split('_')
32
+ .map((word) => word[0].toUpperCase() + word.slice(1))
33
+ .join(' ')),
34
+ });
35
+ arrayData.forEach((item) => {
36
+ table.push(fields.map((field) => {
37
+ const value = item[field];
38
+ return Array.isArray(value)
39
+ ? value.join('\n')
40
+ : isObject(value)
41
+ ? JSON.stringify(value, null, 2)
42
+ : value;
43
+ }));
44
+ });
45
+ if (config.title) {
46
+ out.write((isCi() ? config.title : chalk.bold(config.title)) + '\n');
47
+ }
48
+ out.write(table.toString());
49
+ out.write('\n');
50
+ });
51
+ };
6
52
  /**
7
53
  *
8
54
  * Parses the output format, takes data and writes the output to stdout.
@@ -30,44 +76,12 @@ export const writer = (props) => {
30
76
  chunks.push({ data: args[0], config: args[1] });
31
77
  }
32
78
  if (props.output == 'yaml') {
33
- out.write(YAML.stringify(chunks.length === 1
34
- ? chunks[0].data
35
- : Object.fromEntries(chunks.map(({ config, data }, idx) => [
36
- config.title ? toSnakeCase(config.title) : idx,
37
- data,
38
- ])), null, 2));
39
- return;
79
+ return out.write(writeYaml(chunks));
40
80
  }
41
81
  if (props.output == 'json') {
42
- out.write(JSON.stringify(chunks.length === 1
43
- ? chunks[0].data
44
- : Object.fromEntries(chunks.map(({ config, data }, idx) => [
45
- config.title ? toSnakeCase(config.title) : idx,
46
- data,
47
- ])), null, 2));
48
- return;
82
+ return out.write(writeJson(chunks));
49
83
  }
50
- chunks.forEach(({ data, config }) => {
51
- const arrayData = Array.isArray(data) ? data : [data];
52
- const fields = config.fields.filter((field) => arrayData.some((item) => item[field] !== undefined && item[field] !== ''));
53
- const table = new Table({
54
- style: {
55
- head: ['green'],
56
- },
57
- head: fields.map((field) => field
58
- .split('_')
59
- .map((word) => word[0].toUpperCase() + word.slice(1))
60
- .join(' ')),
61
- });
62
- arrayData.forEach((item) => {
63
- table.push(fields.map((field) => item[field]));
64
- });
65
- if (config.title) {
66
- out.write((isCi() ? config.title : chalk.bold(config.title)) + '\n');
67
- }
68
- out.write(table.toString());
69
- out.write('\n');
70
- });
84
+ return writeTable(chunks, out);
71
85
  },
72
86
  };
73
87
  };