neonctl 1.19.1 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +33 -4
- package/commands/connection_string.test.js +20 -68
- 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 +17 -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,11 +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';
|
|
4
|
+
import { writer } from '../writer.js';
|
|
5
|
+
import { psql } from '../utils/psql.js';
|
|
3
6
|
export const command = 'connection-string [branch]';
|
|
4
7
|
export const aliases = ['cs'];
|
|
5
8
|
export const describe = 'Get connection string';
|
|
6
9
|
export const builder = (argv) => {
|
|
7
10
|
return argv
|
|
8
|
-
.usage('
|
|
11
|
+
.usage('$0 connection-string [branch] [options]')
|
|
12
|
+
.middleware(showHelpMiddleware(argv, true))
|
|
9
13
|
.positional('branch', {
|
|
10
14
|
describe: 'Branch name or id. If ommited will use the primary branch',
|
|
11
15
|
type: 'string',
|
|
@@ -38,6 +42,15 @@ export const builder = (argv) => {
|
|
|
38
42
|
choices: Object.values(EndpointType),
|
|
39
43
|
describe: 'Endpoint type',
|
|
40
44
|
},
|
|
45
|
+
extended: {
|
|
46
|
+
type: 'boolean',
|
|
47
|
+
describe: 'Show extended information',
|
|
48
|
+
},
|
|
49
|
+
psql: {
|
|
50
|
+
type: 'boolean',
|
|
51
|
+
describe: 'Connect to a database via psql using connection string',
|
|
52
|
+
default: false,
|
|
53
|
+
},
|
|
41
54
|
})
|
|
42
55
|
.middleware(fillSingleProject);
|
|
43
56
|
};
|
|
@@ -81,14 +94,14 @@ export const handler = async (props) => {
|
|
|
81
94
|
.map((d) => d.name)
|
|
82
95
|
.join(', ')}`);
|
|
83
96
|
}));
|
|
84
|
-
const { data: password } = await props.apiClient.getProjectBranchRolePassword(props.projectId, endpoint.branch_id, role);
|
|
97
|
+
const { data: { password }, } = await props.apiClient.getProjectBranchRolePassword(props.projectId, endpoint.branch_id, role);
|
|
85
98
|
const host = props.pooled
|
|
86
99
|
? endpoint.host.replace(endpoint.id, `${endpoint.id}-pooler`)
|
|
87
100
|
: endpoint.host;
|
|
88
101
|
const connectionString = new URL(`postgres://${host}`);
|
|
89
102
|
connectionString.pathname = database;
|
|
90
103
|
connectionString.username = role;
|
|
91
|
-
connectionString.password = password
|
|
104
|
+
connectionString.password = password;
|
|
92
105
|
if (props.prisma) {
|
|
93
106
|
connectionString.searchParams.set('connect_timeout', '30');
|
|
94
107
|
if (props.pooled) {
|
|
@@ -96,5 +109,21 @@ export const handler = async (props) => {
|
|
|
96
109
|
connectionString.searchParams.set('pgbouncer', 'true');
|
|
97
110
|
}
|
|
98
111
|
}
|
|
99
|
-
|
|
112
|
+
if (props.psql) {
|
|
113
|
+
const psqlArgs = props['--'];
|
|
114
|
+
await psql(connectionString.toString(), psqlArgs);
|
|
115
|
+
}
|
|
116
|
+
else if (props.extended) {
|
|
117
|
+
writer(props).end({
|
|
118
|
+
connection_string: connectionString.toString(),
|
|
119
|
+
host,
|
|
120
|
+
role,
|
|
121
|
+
password,
|
|
122
|
+
database,
|
|
123
|
+
options: connectionString.searchParams.toString(),
|
|
124
|
+
}, { fields: ['host', 'role', 'password', 'database'] });
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
process.stdout.write(connectionString.toString() + '\n');
|
|
128
|
+
}
|
|
100
129
|
};
|
|
@@ -34,7 +34,7 @@ describe('connection_string', () => {
|
|
|
34
34
|
},
|
|
35
35
|
});
|
|
36
36
|
testCliCommand({
|
|
37
|
-
name: 'connection_string branch id',
|
|
37
|
+
name: 'connection_string branch id 8 digits',
|
|
38
38
|
args: [
|
|
39
39
|
'connection-string',
|
|
40
40
|
'br-cloudy-branch-12345678',
|
|
@@ -66,40 +66,6 @@ describe('connection_string', () => {
|
|
|
66
66
|
snapshot: true,
|
|
67
67
|
},
|
|
68
68
|
});
|
|
69
|
-
testCliCommand({
|
|
70
|
-
name: 'connection_string pooled branch id',
|
|
71
|
-
args: [
|
|
72
|
-
'connection-string',
|
|
73
|
-
'br-sunny-branch-123456',
|
|
74
|
-
'--project-id',
|
|
75
|
-
'test',
|
|
76
|
-
'--database-name',
|
|
77
|
-
'test_db',
|
|
78
|
-
'--role-name',
|
|
79
|
-
'test_role',
|
|
80
|
-
'--pooled',
|
|
81
|
-
],
|
|
82
|
-
expected: {
|
|
83
|
-
snapshot: true,
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
testCliCommand({
|
|
87
|
-
name: 'connection_string pooled branch id',
|
|
88
|
-
args: [
|
|
89
|
-
'connection-string',
|
|
90
|
-
'br-cloudy-branch-12345678',
|
|
91
|
-
'--project-id',
|
|
92
|
-
'test',
|
|
93
|
-
'--database-name',
|
|
94
|
-
'test_db',
|
|
95
|
-
'--role-name',
|
|
96
|
-
'test_role',
|
|
97
|
-
'--pooled',
|
|
98
|
-
],
|
|
99
|
-
expected: {
|
|
100
|
-
snapshot: true,
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
69
|
testCliCommand({
|
|
104
70
|
name: 'connection_string prisma',
|
|
105
71
|
args: [
|
|
@@ -118,16 +84,17 @@ describe('connection_string', () => {
|
|
|
118
84
|
},
|
|
119
85
|
});
|
|
120
86
|
testCliCommand({
|
|
121
|
-
name: 'connection_string pooled
|
|
87
|
+
name: 'connection_string prisma pooled',
|
|
122
88
|
args: [
|
|
123
89
|
'connection-string',
|
|
124
|
-
'
|
|
90
|
+
'test_branch',
|
|
125
91
|
'--project-id',
|
|
126
92
|
'test',
|
|
127
93
|
'--database-name',
|
|
128
94
|
'test_db',
|
|
129
95
|
'--role-name',
|
|
130
96
|
'test_role',
|
|
97
|
+
'--prisma',
|
|
131
98
|
'--pooled',
|
|
132
99
|
],
|
|
133
100
|
expected: {
|
|
@@ -135,82 +102,67 @@ describe('connection_string', () => {
|
|
|
135
102
|
},
|
|
136
103
|
});
|
|
137
104
|
testCliCommand({
|
|
138
|
-
name: 'connection_string pooled
|
|
105
|
+
name: 'connection_string prisma pooled extended',
|
|
139
106
|
args: [
|
|
140
107
|
'connection-string',
|
|
141
|
-
'
|
|
108
|
+
'test_branch',
|
|
142
109
|
'--project-id',
|
|
143
110
|
'test',
|
|
144
111
|
'--database-name',
|
|
145
112
|
'test_db',
|
|
146
113
|
'--role-name',
|
|
147
114
|
'test_role',
|
|
115
|
+
'--prisma',
|
|
148
116
|
'--pooled',
|
|
117
|
+
'--extended',
|
|
149
118
|
],
|
|
150
119
|
expected: {
|
|
151
120
|
snapshot: true,
|
|
152
121
|
},
|
|
153
122
|
});
|
|
154
123
|
testCliCommand({
|
|
155
|
-
name: 'connection_string
|
|
156
|
-
args: [
|
|
157
|
-
|
|
158
|
-
'test_branch',
|
|
159
|
-
'--project-id',
|
|
160
|
-
'test',
|
|
161
|
-
'--database-name',
|
|
162
|
-
'test_db',
|
|
163
|
-
'--role-name',
|
|
164
|
-
'test_role',
|
|
165
|
-
'--prisma',
|
|
166
|
-
'--pooled',
|
|
167
|
-
],
|
|
124
|
+
name: 'connection_string without any args should pass',
|
|
125
|
+
args: ['connection-string'],
|
|
126
|
+
mockDir: 'single_project',
|
|
168
127
|
expected: {
|
|
169
128
|
snapshot: true,
|
|
170
129
|
},
|
|
171
130
|
});
|
|
172
131
|
testCliCommand({
|
|
173
|
-
name: 'connection_string
|
|
132
|
+
name: 'connection_string with psql',
|
|
174
133
|
args: [
|
|
175
134
|
'connection-string',
|
|
176
|
-
'
|
|
135
|
+
'test_branch',
|
|
177
136
|
'--project-id',
|
|
178
137
|
'test',
|
|
179
138
|
'--database-name',
|
|
180
139
|
'test_db',
|
|
181
140
|
'--role-name',
|
|
182
141
|
'test_role',
|
|
183
|
-
'--
|
|
184
|
-
'--pooled',
|
|
142
|
+
'--psql',
|
|
185
143
|
],
|
|
186
144
|
expected: {
|
|
187
145
|
snapshot: true,
|
|
188
146
|
},
|
|
189
147
|
});
|
|
190
148
|
testCliCommand({
|
|
191
|
-
name: 'connection_string
|
|
149
|
+
name: 'connection_string with psql and psql args',
|
|
192
150
|
args: [
|
|
193
151
|
'connection-string',
|
|
194
|
-
'
|
|
152
|
+
'test_branch',
|
|
195
153
|
'--project-id',
|
|
196
154
|
'test',
|
|
197
155
|
'--database-name',
|
|
198
156
|
'test_db',
|
|
199
157
|
'--role-name',
|
|
200
158
|
'test_role',
|
|
201
|
-
'--
|
|
202
|
-
'--
|
|
159
|
+
'--psql',
|
|
160
|
+
'--',
|
|
161
|
+
'-c',
|
|
162
|
+
'SELECT 1',
|
|
203
163
|
],
|
|
204
164
|
expected: {
|
|
205
165
|
snapshot: true,
|
|
206
166
|
},
|
|
207
167
|
});
|
|
208
|
-
testCliCommand({
|
|
209
|
-
name: 'connection_string without any args should pass',
|
|
210
|
-
args: ['connection-string'],
|
|
211
|
-
mockDir: 'single_project',
|
|
212
|
-
expected: {
|
|
213
|
-
snapshot: true,
|
|
214
|
-
},
|
|
215
|
-
});
|
|
216
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.0",
|
|
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,17 @@
|
|
|
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
|
+
psql.on('exit', (code) => {
|
|
15
|
+
process.exit(code === null ? 1 : code);
|
|
16
|
+
});
|
|
17
|
+
};
|
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
|
+
};
|