neonctl 1.20.0 → 1.21.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 CHANGED
@@ -12,17 +12,17 @@ Before installing, ensure that you have met the following prerequisites:
12
12
 
13
13
  - Node.js 16.0 or higher. To check if you already have Node.js, run the following command:
14
14
 
15
- ```shell
16
- node -v
17
- ```
15
+ ```shell
16
+ node -v
17
+ ```
18
18
 
19
- - The `npm` package manager. To check if you already have `npm`, run the following command:
19
+ - The `npm` package manager. To check if you already have `npm`, run the following command:
20
20
 
21
- ```shell
22
- npm -v
23
- ```
21
+ ```shell
22
+ npm -v
23
+ ```
24
24
 
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).
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).
26
26
 
27
27
  ### Install
28
28
 
@@ -60,30 +60,30 @@ The Neon CLI supports autocompletion, which you can configure in a few easy step
60
60
 
61
61
  ## Commands
62
62
 
63
- | Command | Subcommands | Description |
64
- |---------------------------------------------------------|----------------------------------------|---------------------------|
65
- | [auth](https://neon.tech/docs/reference/cli-auth) | | Authenticate |
66
- | [projects](https://neon.tech/docs/reference/cli-projects) | `list`, `create`, `update`, `delete`, `get` | Manage projects |
67
- | [me](../reference/cli-me) | | Show current user |
68
- | [branches](https://neon.tech/docs/reference/cli-branches) | `list`, `create`, `rename`, `add-compute`, `set-primary`, `delete`, `get` | Manage branches |
69
- | [databases](https://neon.tech/docs/reference/cli-databases) | `list`, `create`, `delete` | Manage databases |
70
- | [roles](https://neon.tech/docs/reference/cli-roles) | `list`, `create`, `delete` | Manage roles |
71
- | [operations](https://neon.tech/reference/cli-operations) | `list` | Manage operations |
72
- | [connection-string](https://neon.tech/reference/cli-connection-string) | | Get connection string |
73
- | [completion](https://neon.tech/reference/cli-completion) | | Generate a completion script |
63
+ | Command | Subcommands | Description |
64
+ | ---------------------------------------------------------------------- | ------------------------------------------------------------------------- | ---------------------------- |
65
+ | [auth](https://neon.tech/docs/reference/cli-auth) | | Authenticate |
66
+ | [projects](https://neon.tech/docs/reference/cli-projects) | `list`, `create`, `update`, `delete`, `get` | Manage projects |
67
+ | [me](../reference/cli-me) | | Show current user |
68
+ | [branches](https://neon.tech/docs/reference/cli-branches) | `list`, `create`, `rename`, `add-compute`, `set-primary`, `delete`, `get` | Manage branches |
69
+ | [databases](https://neon.tech/docs/reference/cli-databases) | `list`, `create`, `delete` | Manage databases |
70
+ | [roles](https://neon.tech/docs/reference/cli-roles) | `list`, `create`, `delete` | Manage roles |
71
+ | [operations](https://neon.tech/reference/cli-operations) | `list` | Manage operations |
72
+ | [connection-string](https://neon.tech/reference/cli-connection-string) | | Get connection string |
73
+ | [completion](https://neon.tech/reference/cli-completion) | | Generate a completion script |
74
74
 
75
75
  ## Global options
76
76
 
77
77
  Global options are supported with any Neon CLI command.
78
78
 
79
- | Option | Description | Type | Default |
80
- | :--------- | :---------------------------------- | :----- | :-------------------------------- |
81
- | [-o, --output](#output)| Set the Neon CLI output format (`json`, `yaml`, or `table`) | string | table |
82
- | [--config-dir](#config-dir)| Path to the Neon CLI configuration directory | string | `/home/<user>/.config/neonctl` |
83
- | [--api-key](#api-key) | Neon API key | string | "" |
84
- | [--analytics](#analytics) | Manage analytics | boolean| true |
85
- | [-v, --version](#version) | Show the Neon CLI version number | boolean| - |
86
- | [-h, --help](#help) | Show the Neon CLI help | boolean| - |
79
+ | Option | Description | Type | Default |
80
+ | :-------------------------- | :---------------------------------------------------------- | :------ | :----------------------------- |
81
+ | [-o, --output](#output) | Set the Neon CLI output format (`json`, `yaml`, or `table`) | string | table |
82
+ | [--config-dir](#config-dir) | Path to the Neon CLI configuration directory | string | `/home/<user>/.config/neonctl` |
83
+ | [--api-key](#api-key) | Neon API key | string | "" |
84
+ | [--analytics](#analytics) | Manage analytics | boolean | true |
85
+ | [-v, --version](#version) | Show the Neon CLI version number | boolean | - |
86
+ | [-h, --help](#help) | Show the Neon CLI help | boolean | - |
87
87
 
88
88
  - <a id="output"></a>`-o, --output`
89
89
 
@@ -133,3 +133,23 @@ Global options are supported with any Neon CLI command.
133
133
 
134
134
  neonctl branches create --help
135
135
  ```
136
+
137
+ ## Contribute
138
+
139
+ To run the CLI locally execute build command after making changes:
140
+
141
+ ```shell
142
+ npm run build
143
+ ```
144
+
145
+ To develop continuously:
146
+
147
+ ```shell
148
+ npm run watch
149
+ ```
150
+
151
+ To run commands from the local build replace the `neonctl` command with `node dist`, for example:
152
+
153
+ ```shell
154
+ node dist branches --help
155
+ ```
package/commands/auth.js CHANGED
@@ -6,10 +6,11 @@ import { log } from '../log.js';
6
6
  import { getApiClient } from '../api.js';
7
7
  import { isCi } from '../env.js';
8
8
  import { CREDENTIALS_FILE } from '../config.js';
9
+ import { showHelpMiddleware } from '../help.js';
9
10
  export const command = 'auth';
10
11
  export const aliases = ['login'];
11
12
  export const describe = 'Authenticate';
12
- export const builder = (yargs) => yargs;
13
+ export const builder = (yargs) => yargs.middleware(showHelpMiddleware(yargs, true));
13
14
  export const handler = async (args) => {
14
15
  await authFlow(args);
15
16
  };
@@ -1,10 +1,11 @@
1
1
  import { EndpointType } from '@neondatabase/api-client';
2
2
  import { writer } from '../writer.js';
3
- import { branchCreateRequest } from '../parameters.gen.js';
4
- import { commandFailHandler } from '../utils/middlewares.js';
3
+ import { branchCreateRequest, branchCreateRequestEndpointOptions, } from '../parameters.gen.js';
5
4
  import { retryOnLock } from '../api.js';
6
5
  import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
7
6
  import { looksLikeBranchId, looksLikeLSN, looksLikeTimestamp, } from '../utils/formats.js';
7
+ import { showHelpMiddleware } from '../help.js';
8
+ import { psql } from '../utils/psql.js';
8
9
  const BRANCH_FIELDS = [
9
10
  'id',
10
11
  'name',
@@ -16,9 +17,7 @@ export const command = 'branches';
16
17
  export const describe = 'Manage branches';
17
18
  export const aliases = ['branch'];
18
19
  export const builder = (argv) => argv
19
- .demandCommand(1, '')
20
- .fail(commandFailHandler)
21
- .usage('usage: $0 branches <sub-command> [options]')
20
+ .usage('$0 branches <sub-command> [options]')
22
21
  .options({
23
22
  'project-id': {
24
23
  describe: 'Project ID',
@@ -45,6 +44,18 @@ export const builder = (argv) => argv
45
44
  default: EndpointType.ReadWrite,
46
45
  choices: Object.values(EndpointType),
47
46
  },
47
+ 'suspend-timeout': {
48
+ describe: branchCreateRequestEndpointOptions.suspend_timeout_seconds
49
+ .description,
50
+ type: 'number',
51
+ implies: 'compute',
52
+ default: 0,
53
+ },
54
+ psql: {
55
+ type: 'boolean',
56
+ describe: 'Connect to a new branch via psql',
57
+ default: false,
58
+ },
48
59
  }), async (args) => await create(args))
49
60
  .command('rename <id|name> <new-name>', 'Rename a branch', (yargs) => yargs, async (args) => await rename(args))
50
61
  .command('set-primary <id|name>', 'Set a branch as primary', (yargs) => yargs, async (args) => await setPrimary(args))
@@ -57,7 +68,8 @@ export const builder = (argv) => argv
57
68
  },
58
69
  }), async (args) => await addCompute(args))
59
70
  .command('delete <id|name>', 'Delete a branch', (yargs) => yargs, async (args) => await deleteBranch(args))
60
- .command('get <id|name>', 'Get a branch', (yargs) => yargs, async (args) => await get(args));
71
+ .command('get <id|name>', 'Get a branch', (yargs) => yargs, async (args) => await get(args))
72
+ .middleware(showHelpMiddleware(argv), true);
61
73
  export const handler = (args) => {
62
74
  return args;
63
75
  };
@@ -108,6 +120,7 @@ const create = async (props) => {
108
120
  ? [
109
121
  {
110
122
  type: props.type,
123
+ suspend_timeout_seconds: props.suspendTimeout,
111
124
  },
112
125
  ]
113
126
  : [],
@@ -130,6 +143,14 @@ const create = async (props) => {
130
143
  });
131
144
  }
132
145
  out.end();
146
+ if (props.psql) {
147
+ if (!data.connection_uris || !data.connection_uris?.length) {
148
+ throw new Error(`Branch ${data.branch.id} doesn't have a connection uri`);
149
+ }
150
+ const connection_uri = data.connection_uris[0].connection_uri;
151
+ const psqlArgs = props['--'];
152
+ await psql(connection_uri, psqlArgs);
153
+ }
133
154
  };
134
155
  const rename = async (props) => {
135
156
  const branchId = await branchIdFromProps(props);
@@ -22,6 +22,39 @@ describe('branches', () => {
22
22
  snapshot: true,
23
23
  },
24
24
  });
25
+ testCliCommand({
26
+ name: 'create branch and connect with psql',
27
+ args: [
28
+ 'branches',
29
+ 'create',
30
+ '--project-id',
31
+ 'test',
32
+ '--name',
33
+ 'test_branch',
34
+ '--psql',
35
+ ],
36
+ expected: {
37
+ snapshot: true,
38
+ },
39
+ });
40
+ testCliCommand({
41
+ name: 'create branch and connect with psql and psql args',
42
+ args: [
43
+ 'branches',
44
+ 'create',
45
+ '--project-id',
46
+ 'test',
47
+ '--name',
48
+ 'test_branch',
49
+ '--psql',
50
+ '--',
51
+ '-c',
52
+ 'SELECT 1',
53
+ ],
54
+ expected: {
55
+ snapshot: true,
56
+ },
57
+ });
25
58
  testCliCommand({
26
59
  name: 'create with readonly endpoint',
27
60
  args: [
@@ -101,6 +134,22 @@ describe('branches', () => {
101
134
  snapshot: true,
102
135
  },
103
136
  });
137
+ testCliCommand({
138
+ name: 'create with suspend timeout',
139
+ args: [
140
+ 'branches',
141
+ 'create',
142
+ '--project-id',
143
+ 'test',
144
+ '--name',
145
+ 'test_branch_with_suspend_timeout',
146
+ '--suspend-timeout',
147
+ '60',
148
+ ],
149
+ expected: {
150
+ snapshot: true,
151
+ },
152
+ });
104
153
  testCliCommand({
105
154
  name: 'delete by id',
106
155
  args: [
@@ -176,7 +225,13 @@ describe('branches', () => {
176
225
  });
177
226
  testCliCommand({
178
227
  name: 'get by id',
179
- args: ['branches', 'get', 'br-cloudy-branch-12345678', '--project-id', 'test'],
228
+ args: [
229
+ 'branches',
230
+ 'get',
231
+ 'br-cloudy-branch-12345678',
232
+ '--project-id',
233
+ 'test',
234
+ ],
180
235
  expected: {
181
236
  snapshot: true,
182
237
  },
@@ -1,12 +1,15 @@
1
1
  import { EndpointType } from '@neondatabase/api-client';
2
2
  import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
3
+ import { showHelpMiddleware } from '../help.js';
3
4
  import { writer } from '../writer.js';
5
+ import { psql } from '../utils/psql.js';
4
6
  export const command = 'connection-string [branch]';
5
7
  export const aliases = ['cs'];
6
8
  export const describe = 'Get connection string';
7
9
  export const builder = (argv) => {
8
10
  return argv
9
- .usage('usage: $0 connection-string [branch] [options]')
11
+ .usage('$0 connection-string [branch] [options]')
12
+ .middleware(showHelpMiddleware(argv, true))
10
13
  .positional('branch', {
11
14
  describe: 'Branch name or id. If ommited will use the primary branch',
12
15
  type: 'string',
@@ -42,6 +45,10 @@ export const builder = (argv) => {
42
45
  extended: {
43
46
  type: 'boolean',
44
47
  describe: 'Show extended information',
48
+ },
49
+ psql: {
50
+ type: 'boolean',
51
+ describe: 'Connect to a database via psql using connection string',
45
52
  default: false,
46
53
  },
47
54
  })
@@ -102,7 +109,11 @@ export const handler = async (props) => {
102
109
  connectionString.searchParams.set('pgbouncer', 'true');
103
110
  }
104
111
  }
105
- if (props.extended) {
112
+ if (props.psql) {
113
+ const psqlArgs = props['--'];
114
+ await psql(connectionString.toString(), psqlArgs);
115
+ }
116
+ else if (props.extended) {
106
117
  writer(props).end({
107
118
  connection_string: connectionString.toString(),
108
119
  host,
@@ -128,4 +128,41 @@ describe('connection_string', () => {
128
128
  snapshot: true,
129
129
  },
130
130
  });
131
+ testCliCommand({
132
+ name: 'connection_string with psql',
133
+ args: [
134
+ 'connection-string',
135
+ 'test_branch',
136
+ '--project-id',
137
+ 'test',
138
+ '--database-name',
139
+ 'test_db',
140
+ '--role-name',
141
+ 'test_role',
142
+ '--psql',
143
+ ],
144
+ expected: {
145
+ snapshot: true,
146
+ },
147
+ });
148
+ testCliCommand({
149
+ name: 'connection_string with psql and psql args',
150
+ args: [
151
+ 'connection-string',
152
+ 'test_branch',
153
+ '--project-id',
154
+ 'test',
155
+ '--database-name',
156
+ 'test_db',
157
+ '--role-name',
158
+ 'test_role',
159
+ '--psql',
160
+ '--',
161
+ '-c',
162
+ 'SELECT 1',
163
+ ],
164
+ expected: {
165
+ snapshot: true,
166
+ },
167
+ });
131
168
  });
@@ -1,15 +1,14 @@
1
1
  import { retryOnLock } from '../api.js';
2
2
  import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
3
- import { commandFailHandler } from '../utils/middlewares.js';
4
3
  import { writer } from '../writer.js';
4
+ import { showHelpMiddleware } from '../help.js';
5
5
  const DATABASE_FIELDS = ['name', 'owner_name', 'created_at'];
6
6
  export const command = 'databases';
7
7
  export const describe = 'Manage databases';
8
8
  export const aliases = ['database', 'db'];
9
9
  export const builder = (argv) => argv
10
- .demandCommand(1, '')
11
- .fail(commandFailHandler)
12
- .usage('usage: $0 databases <sub-command> [options]')
10
+ .usage('$0 databases <sub-command> [options]')
11
+ .middleware(showHelpMiddleware(argv))
13
12
  .options({
14
13
  'project-id': {
15
14
  describe: 'Project ID',
@@ -1,11 +1,12 @@
1
1
  import { describe, expect } from '@jest/globals';
2
+ import chalk from 'chalk';
2
3
  import { testCliCommand } from '../test_utils/test_cli_command.js';
3
4
  describe('help', () => {
4
5
  testCliCommand({
5
6
  name: 'without args',
6
7
  args: [],
7
8
  expected: {
8
- stderr: expect.stringContaining('usage: neonctl <command> [options]'),
9
+ stderr: expect.stringContaining(`neonctl <command> ${chalk.green('[options]')}`),
9
10
  },
10
11
  });
11
12
  });
@@ -1,14 +1,13 @@
1
1
  import { fillSingleProject } from '../utils/enrichers.js';
2
- import { commandFailHandler } from '../utils/middlewares.js';
3
2
  import { writer } from '../writer.js';
3
+ import { showHelpMiddleware } from '../help.js';
4
4
  const OPERATIONS_FIELDS = ['id', 'action', 'status', 'created_at'];
5
5
  export const command = 'operations';
6
6
  export const describe = 'Manage operations';
7
7
  export const aliases = ['operation'];
8
8
  export const builder = (argv) => argv
9
- .demandCommand(1, '')
10
- .fail(commandFailHandler)
11
- .usage('usage: $0 operations <sub-command> [options]')
9
+ .usage('$0 operations <sub-command> [options]')
10
+ .middleware(showHelpMiddleware(argv))
12
11
  .options({
13
12
  'project-id': {
14
13
  describe: 'Project ID',
@@ -1,7 +1,8 @@
1
+ import { showHelpMiddleware } from '../help.js';
1
2
  import { log } from '../log.js';
2
3
  import { projectCreateRequest } from '../parameters.gen.js';
3
- import { commandFailHandler } from '../utils/middlewares.js';
4
4
  import { writer } from '../writer.js';
5
+ import { psql } from '../utils/psql.js';
5
6
  const PROJECT_FIELDS = ['id', 'name', 'region_id', 'created_at'];
6
7
  const REGIONS = [
7
8
  'aws-us-west-2',
@@ -16,9 +17,8 @@ export const describe = 'Manage projects';
16
17
  export const aliases = ['project'];
17
18
  export const builder = (argv) => {
18
19
  return argv
19
- .demandCommand(1, '')
20
- .fail(commandFailHandler)
21
- .usage('usage: $0 projects <sub-command> [options]')
20
+ .usage('$0 projects <sub-command> [options]')
21
+ .middleware(showHelpMiddleware(argv))
22
22
  .command('list', 'List projects', (yargs) => yargs, async (args) => {
23
23
  await list(args);
24
24
  })
@@ -31,6 +31,11 @@ export const builder = (argv) => {
31
31
  describe: `The region ID. Possible values: ${REGIONS.join(', ')}`,
32
32
  type: 'string',
33
33
  },
34
+ psql: {
35
+ type: 'boolean',
36
+ describe: 'Connect to a new project via psql',
37
+ default: false,
38
+ },
34
39
  }), async (args) => {
35
40
  await create(args);
36
41
  })
@@ -88,6 +93,11 @@ const create = async (props) => {
88
93
  title: 'Connection URIs',
89
94
  });
90
95
  out.end();
96
+ if (props.psql) {
97
+ const connection_uri = data.connection_uris[0].connection_uri;
98
+ const psqlArgs = props['--'];
99
+ await psql(connection_uri, psqlArgs);
100
+ }
91
101
  };
92
102
  const deleteProject = async (props) => {
93
103
  const { data } = await props.apiClient.deleteProject(props.id);
@@ -15,6 +15,20 @@ describe('projects', () => {
15
15
  snapshot: true,
16
16
  },
17
17
  });
18
+ testCliCommand({
19
+ name: 'create and connect with psql',
20
+ args: ['projects', 'create', '--name', 'test_project', '--psql'],
21
+ expected: {
22
+ snapshot: true,
23
+ },
24
+ });
25
+ testCliCommand({
26
+ name: 'create and connect with psql and psql args',
27
+ args: ['projects', 'create', '--name', 'test_project', '--psql', '--', '-c', 'SELECT 1'],
28
+ expected: {
29
+ snapshot: true,
30
+ },
31
+ });
18
32
  testCliCommand({
19
33
  name: 'delete',
20
34
  args: ['projects', 'delete', 'test'],
package/commands/roles.js CHANGED
@@ -1,15 +1,14 @@
1
1
  import { retryOnLock } from '../api.js';
2
2
  import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
3
- import { commandFailHandler } from '../utils/middlewares.js';
4
3
  import { writer } from '../writer.js';
4
+ import { showHelpMiddleware } from '../help.js';
5
5
  const ROLES_FIELDS = ['name', 'created_at'];
6
6
  export const command = 'roles';
7
7
  export const describe = 'Manage roles';
8
8
  export const aliases = ['role'];
9
9
  export const builder = (argv) => argv
10
- .demandCommand(1, '')
11
- .fail(commandFailHandler)
12
- .usage('usage: $0 roles <sub-command> [options]')
10
+ .middleware(showHelpMiddleware(argv))
11
+ .usage('$0 roles <sub-command> [options]')
13
12
  .options({
14
13
  'project-id': {
15
14
  describe: 'Project ID',
package/commands/user.js CHANGED
@@ -1,7 +1,8 @@
1
+ import { showHelpMiddleware } from '../help.js';
1
2
  import { writer } from '../writer.js';
2
3
  export const command = 'me';
3
4
  export const describe = 'Show current user';
4
- export const builder = (yargs) => yargs;
5
+ export const builder = (yargs) => yargs.middleware(showHelpMiddleware(yargs, true));
5
6
  export const handler = async (args) => {
6
7
  await me(args);
7
8
  };
package/errors.js CHANGED
@@ -3,6 +3,9 @@ const ERROR_MATCHERS = [
3
3
  [/^Missing required argument: (.*)$/, 'MISSING_ARGUMENT'],
4
4
  ];
5
5
  export const matchErrorCode = (message) => {
6
+ if (!message) {
7
+ return 'UNKNOWN_ERROR';
8
+ }
6
9
  for (const [matcher, code] of ERROR_MATCHERS) {
7
10
  const match = message.match(matcher);
8
11
  if (match) {
package/help.js ADDED
@@ -0,0 +1,119 @@
1
+ import cliui from 'cliui';
2
+ import chalk from 'chalk';
3
+ import { consumeBlockIfMatches, consumeNextMatching, drawPointer, splitColumns, } from './utils/ui.js';
4
+ // target width for the leftmost column
5
+ const SPACE_WIDTH = 20;
6
+ const formatHelp = (help) => {
7
+ const lines = help.split('\n');
8
+ const result = [];
9
+ // full command, like `neonctl projects list`
10
+ const topLevelCommand = consumeNextMatching(lines, /^.*/);
11
+ if (topLevelCommand) {
12
+ result.push(chalk.bold(topLevelCommand.replace('[options]', chalk.reset.green('[options]'))));
13
+ result.push('');
14
+ }
15
+ // commands description block
16
+ // example command to see: neonctl projects
17
+ const commandsBlock = consumeBlockIfMatches(lines, /^Commands:/);
18
+ if (commandsBlock.length > 0) {
19
+ const header = commandsBlock.shift();
20
+ result.push(header);
21
+ const ui = cliui({
22
+ width: 0,
23
+ });
24
+ commandsBlock.forEach((line) => {
25
+ const [command, description] = splitColumns(line);
26
+ ui.div(chalk.cyan(command));
27
+ ui.div({
28
+ text: chalk.gray(drawPointer(SPACE_WIDTH)),
29
+ width: SPACE_WIDTH,
30
+ padding: [0, 0, 0, 0],
31
+ }, { text: description, padding: [0, 0, 0, 2] });
32
+ });
33
+ result.push(ui.toString());
34
+ result.push('');
35
+ }
36
+ // positional args block
37
+ // example command to see: neonctl branches rename
38
+ const positionalsBlock = consumeBlockIfMatches(lines, /Positionals:/);
39
+ if (positionalsBlock.length > 0) {
40
+ const header = positionalsBlock.shift();
41
+ result.push(header);
42
+ const ui = cliui({
43
+ width: 0,
44
+ });
45
+ positionalsBlock.forEach((line) => {
46
+ const [positional, description] = splitColumns(line);
47
+ ui.div({
48
+ text: positional,
49
+ width: SPACE_WIDTH,
50
+ padding: [0, 2, 0, 0],
51
+ }, {
52
+ text: description,
53
+ padding: [0, 0, 0, 0],
54
+ });
55
+ });
56
+ result.push(ui.toString());
57
+ result.push('');
58
+ }
59
+ // command description
60
+ // example command to see: neonctl projects list
61
+ const descritpionBlock = consumeBlockIfMatches(lines, /^(?!.*options:)/i);
62
+ if (descritpionBlock.length > 0) {
63
+ result.push(descritpionBlock.shift());
64
+ result.push('');
65
+ }
66
+ while (true) {
67
+ // there are two options blocks: global and specific
68
+ // example to see both: neonctl projects create
69
+ const optionsBlock = consumeBlockIfMatches(lines, /.*options:/i);
70
+ if (optionsBlock.length === 0) {
71
+ break;
72
+ }
73
+ result.push(optionsBlock.shift());
74
+ optionsBlock.forEach((line) => {
75
+ const [option, description] = splitColumns(line);
76
+ const ui = cliui({
77
+ width: 0,
78
+ });
79
+ if (option.startsWith('-')) {
80
+ ui.div({
81
+ text: chalk.green(option),
82
+ padding: [0, 0, 0, 0],
83
+ });
84
+ ui.div({
85
+ text: chalk.gray(drawPointer(SPACE_WIDTH)),
86
+ width: SPACE_WIDTH,
87
+ padding: [0, 2, 0, 0],
88
+ }, {
89
+ text: chalk.rgb(210, 210, 210)(description ?? ''),
90
+ padding: [0, 0, 0, 0],
91
+ });
92
+ }
93
+ else {
94
+ ui.div({
95
+ padding: [0, 0, 0, 0],
96
+ text: '',
97
+ width: SPACE_WIDTH,
98
+ }, {
99
+ text: chalk.rgb(210, 210, 210)(option),
100
+ padding: [0, 0, 0, 0],
101
+ });
102
+ }
103
+ result.push(ui.toString());
104
+ });
105
+ result.push('');
106
+ }
107
+ return [...result, ...lines];
108
+ };
109
+ export const showHelp = async (argv) => {
110
+ // add wrap to ensure that there are no line breaks
111
+ const help = await argv.wrap(500).getHelp();
112
+ process.stderr.write(formatHelp(help).join('\n') + '\n');
113
+ process.exit(0);
114
+ };
115
+ export const showHelpMiddleware = (argv, ignoreSubCmdPresence) => async (args) => {
116
+ if ((!ignoreSubCmdPresence && args._.length === 1) || args.help) {
117
+ await showHelp(argv);
118
+ }
119
+ };
package/index.js CHANGED
@@ -24,10 +24,14 @@ import { analyticsMiddleware, sendError } from './analytics.js';
24
24
  import { isCi } from './env.js';
25
25
  import { isAxiosError } from 'axios';
26
26
  import { matchErrorCode } from './errors.js';
27
+ import { showHelp } from './help.js';
27
28
  let builder = yargs(hideBin(process.argv));
28
29
  builder = builder
29
30
  .scriptName(pkg.name)
30
- .usage('usage: $0 <command> [options]')
31
+ .usage('$0 <command> [options]')
32
+ .parserConfiguration({
33
+ 'populate--': true,
34
+ })
31
35
  .help()
32
36
  .option('output', {
33
37
  alias: 'o',
@@ -92,11 +96,21 @@ builder = builder
92
96
  .middleware(analyticsMiddleware, true)
93
97
  .group('version', 'Global options:')
94
98
  .alias('version', 'v')
99
+ .help(false)
95
100
  .group('help', 'Global options:')
101
+ .option('help', {
102
+ describe: 'Show help',
103
+ type: 'boolean',
104
+ default: false,
105
+ })
96
106
  .alias('help', 'h')
97
107
  .completion()
98
108
  .scriptName(basename(process.argv[1]) === 'neon' ? 'neon' : 'neonctl')
99
109
  .fail(async (msg, err) => {
110
+ if (process.argv.some((arg) => arg === '--help' || arg === '-h')) {
111
+ await showHelp(builder);
112
+ process.exit(0);
113
+ }
100
114
  if (isAxiosError(err)) {
101
115
  if (err.code === 'ECONNABORTED') {
102
116
  log.error('Request timed out');
@@ -121,8 +135,8 @@ builder = builder
121
135
  });
122
136
  (async () => {
123
137
  const args = await builder.argv;
124
- if (args._.length === 0) {
125
- builder.showHelp();
138
+ if (args._.length === 0 || args.help) {
139
+ await showHelp(builder);
126
140
  process.exit(0);
127
141
  }
128
142
  })();
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.20.0",
8
+ "version": "1.21.1",
9
9
  "description": "CLI tool for NeonDB Cloud management",
10
10
  "main": "index.js",
11
11
  "author": "NeonDB",
@@ -32,6 +32,7 @@
32
32
  "@types/express": "^4.17.17",
33
33
  "@types/inquirer": "^9.0.3",
34
34
  "@types/node": "^18.7.13",
35
+ "@types/which": "^3.0.0",
35
36
  "@types/yargs": "^17.0.24",
36
37
  "@typescript-eslint/eslint-plugin": "^5.34.0",
37
38
  "@typescript-eslint/parser": "^5.34.0",
@@ -60,6 +61,7 @@
60
61
  "inquirer": "^9.2.6",
61
62
  "open": "^8.4.0",
62
63
  "openid-client": "^5.1.9",
64
+ "which": "^3.0.1",
63
65
  "yaml": "^2.1.1",
64
66
  "yargs": "^17.7.2"
65
67
  },
@@ -29,6 +29,9 @@ export const testCliCommand = ({ args, name, expected, mockDir = 'main', }) => {
29
29
  ...args,
30
30
  ], {
31
31
  stdio: 'pipe',
32
+ env: {
33
+ PATH: `mocks/bin:${process.env.PATH}`,
34
+ },
32
35
  });
33
36
  return new Promise((resolve, reject) => {
34
37
  cp.stdout?.on('data', (data) => {
@@ -1,5 +1,3 @@
1
- import yargs from 'yargs';
2
- import { hideBin } from 'yargs/helpers';
3
1
  /**
4
2
  * This middleware is needed to fill in the args for nested objects,
5
3
  * so that required arguments would work
@@ -20,10 +18,3 @@ export const fillInArgs = (args, currentArgs = args, acc = []) => {
20
18
  }
21
19
  });
22
20
  };
23
- export const commandFailHandler = async (_msg, _err, yyargs) => {
24
- const argv = yargs(hideBin(process.argv));
25
- if (argv.argv._.length === 1) {
26
- yyargs.showHelp();
27
- process.exit(1);
28
- }
29
- };
package/utils/psql.js ADDED
@@ -0,0 +1,24 @@
1
+ import { log } from '../log.js';
2
+ import { spawn } from 'child_process';
3
+ import which from 'which';
4
+ export const psql = async (connection_uri, args = []) => {
5
+ const psqlPathOrNull = await which('psql', { nothrow: true });
6
+ if (psqlPathOrNull === null) {
7
+ log.error(`psql is not available in the PATH`);
8
+ process.exit(1);
9
+ }
10
+ log.info('Connecting to the database using psql...');
11
+ const psql = spawn(psqlPathOrNull, [connection_uri, ...args], {
12
+ stdio: 'inherit',
13
+ });
14
+ for (const signame of ['SIGINT', 'SIGTERM']) {
15
+ process.on(signame, (code) => {
16
+ if (!psql.killed && code !== null) {
17
+ psql.kill(code);
18
+ }
19
+ });
20
+ }
21
+ psql.on('exit', (code) => {
22
+ process.exit(code === null ? 1 : code);
23
+ });
24
+ };
package/utils/ui.js ADDED
@@ -0,0 +1,52 @@
1
+ // returns the next string if matches the given matcher,
2
+ // otherwise returns null
3
+ // consumes the line from the lines array
4
+ export const consumeNextMatching = (lines, matcher) => {
5
+ while (lines.length > 0) {
6
+ const line = lines.shift().trim();
7
+ if (line === '') {
8
+ continue;
9
+ }
10
+ if (matcher.test(line)) {
11
+ return line;
12
+ }
13
+ return null;
14
+ }
15
+ return null;
16
+ };
17
+ // returns strings if next non-empty line matches the given matcher,
18
+ // otherwise returns empty array
19
+ // consumes the lines from the lines array
20
+ export const consumeBlockIfMatches = (lines, matcher) => {
21
+ const result = [];
22
+ if (lines.length === 0) {
23
+ return result;
24
+ }
25
+ let line = lines.shift();
26
+ while (line.trim() === '') {
27
+ line = lines.shift();
28
+ }
29
+ if (!matcher.test(line)) {
30
+ lines.unshift(line);
31
+ return result;
32
+ }
33
+ result.push(line);
34
+ while (lines.length > 0) {
35
+ line = lines.shift();
36
+ if (line.trim() === '') {
37
+ break;
38
+ }
39
+ result.push(line);
40
+ }
41
+ return result;
42
+ };
43
+ export const splitColumns = (line) => line.trim().split(/\s{2,}/);
44
+ export const drawPointer = (width) => {
45
+ const result = [];
46
+ result.push('└');
47
+ for (let i = 0; i < width - 4; i++) {
48
+ result.push('─');
49
+ }
50
+ result.push('>');
51
+ return result.join('');
52
+ };