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 +47 -27
- package/commands/auth.js +2 -1
- package/commands/branches.js +27 -6
- package/commands/branches.test.js +56 -1
- package/commands/connection_string.js +13 -2
- package/commands/connection_string.test.js +37 -0
- package/commands/databases.js +3 -4
- package/commands/help.test.js +2 -1
- package/commands/operations.js +3 -4
- package/commands/projects.js +14 -4
- package/commands/projects.test.js +14 -0
- package/commands/roles.js +3 -4
- package/commands/user.js +2 -1
- package/errors.js +3 -0
- package/help.js +119 -0
- package/index.js +17 -3
- package/package.json +3 -1
- package/test_utils/test_cli_command.js +3 -0
- package/utils/middlewares.js +0 -9
- package/utils/psql.js +24 -0
- package/utils/ui.js +52 -0
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
```shell
|
|
16
|
+
node -v
|
|
17
|
+
```
|
|
18
18
|
|
|
19
|
-
- The `npm` package manager.
|
|
19
|
+
- The `npm` package manager. To check if you already have `npm`, run the following command:
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
```shell
|
|
22
|
+
npm -v
|
|
23
|
+
```
|
|
24
24
|
|
|
25
|
-
If you need to install
|
|
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
|
|
64
|
-
|
|
65
|
-
| [auth](https://neon.tech/docs/reference/cli-auth)
|
|
66
|
-
| [projects](https://neon.tech/docs/reference/cli-projects)
|
|
67
|
-
| [me](../reference/cli-me)
|
|
68
|
-
| [branches](https://neon.tech/docs/reference/cli-branches)
|
|
69
|
-
| [databases](https://neon.tech/docs/reference/cli-databases)
|
|
70
|
-
| [roles](https://neon.tech/docs/reference/cli-roles)
|
|
71
|
-
| [operations](https://neon.tech/reference/cli-operations)
|
|
72
|
-
| [connection-string](https://neon.tech/reference/cli-connection-string)
|
|
73
|
-
| [completion](https://neon.tech/reference/cli-completion)
|
|
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
|
|
80
|
-
|
|
|
81
|
-
| [-o, --output](#output)| Set the Neon CLI output format (`json`, `yaml`, or `table`)
|
|
82
|
-
| [--config-dir](#config-dir)| Path to the Neon CLI configuration directory
|
|
83
|
-
| [--api-key](#api-key)
|
|
84
|
-
| [--analytics](#analytics)
|
|
85
|
-
| [-v, --version](#version) | Show the Neon CLI version number
|
|
86
|
-
| [-h, --help](#help)
|
|
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
|
};
|
package/commands/branches.js
CHANGED
|
@@ -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
|
-
.
|
|
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: [
|
|
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('
|
|
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.
|
|
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
|
});
|
package/commands/databases.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 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
|
-
.
|
|
11
|
-
.
|
|
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',
|
package/commands/help.test.js
CHANGED
|
@@ -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(
|
|
9
|
+
stderr: expect.stringContaining(`neonctl <command> ${chalk.green('[options]')}`),
|
|
9
10
|
},
|
|
10
11
|
});
|
|
11
12
|
});
|
package/commands/operations.js
CHANGED
|
@@ -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
|
-
.
|
|
10
|
-
.
|
|
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',
|
package/commands/projects.js
CHANGED
|
@@ -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
|
-
.
|
|
20
|
-
.
|
|
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
|
-
.
|
|
11
|
-
.
|
|
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('
|
|
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
|
-
|
|
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.
|
|
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) => {
|
package/utils/middlewares.js
CHANGED
|
@@ -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
|
+
};
|