neonctl 1.21.3 → 1.23.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/analytics.js CHANGED
@@ -9,7 +9,7 @@ import pkg from './pkg.js';
9
9
  const WRITE_KEY = '3SQXn5ejjXWLEJ8xU2PRYhAotLtTaeeV';
10
10
  let client;
11
11
  let userId = '';
12
- export const analyticsMiddleware = (args) => {
12
+ export const analyticsMiddleware = async (args) => {
13
13
  if (!args.analytics) {
14
14
  return;
15
15
  }
@@ -40,7 +40,7 @@ export const analyticsMiddleware = (args) => {
40
40
  ci: isCi(),
41
41
  },
42
42
  });
43
- client.closeAndFlush();
43
+ await client.closeAndFlush();
44
44
  log.debug('Sent CLI started event with userId: %s', userId);
45
45
  };
46
46
  export const sendError = (err, errCode) => {
package/callback.html CHANGED
@@ -14,6 +14,14 @@ body, html {
14
14
  flex-direction: column;
15
15
  justify-content: center;
16
16
  align-items: center;
17
+ background-color: #ffffff;
18
+ color: #2d374c;
19
+ }
20
+ @media (prefers-color-scheme: dark) {
21
+ body, html {
22
+ background-color: #191919;
23
+ color: #bfbfbf;
24
+ }
17
25
  }
18
26
  .logo {
19
27
  display: inline-block;
@@ -32,6 +40,6 @@ svg {
32
40
  </div>
33
41
  <h1>Thank you for using Neon</h1>
34
42
  <p>
35
- You may close the page now
43
+ You may close this page now
36
44
  </p>
37
45
  </body>
@@ -1,12 +1,11 @@
1
1
  import { describe, expect } from '@jest/globals';
2
- import chalk from 'chalk';
3
2
  import { testCliCommand } from '../test_utils/test_cli_command.js';
4
3
  describe('help', () => {
5
4
  testCliCommand({
6
5
  name: 'without args',
7
6
  args: [],
8
7
  expected: {
9
- stderr: expect.stringContaining(`neonctl <command> ${chalk.green('[options]')}`),
8
+ stderr: expect.stringContaining(`neonctl <command> [options]`),
10
9
  },
11
10
  });
12
11
  });
package/commands/index.js CHANGED
@@ -6,6 +6,7 @@ import * as databases from './databases.js';
6
6
  import * as roles from './roles.js';
7
7
  import * as operations from './operations.js';
8
8
  import * as cs from './connection_string.js';
9
+ import * as setContext from './set_context.js';
9
10
  export default [
10
11
  auth,
11
12
  users,
@@ -15,4 +16,5 @@ export default [
15
16
  roles,
16
17
  operations,
17
18
  cs,
19
+ setContext,
18
20
  ];
@@ -3,6 +3,7 @@ import { log } from '../log.js';
3
3
  import { projectCreateRequest } from '../parameters.gen.js';
4
4
  import { writer } from '../writer.js';
5
5
  import { psql } from '../utils/psql.js';
6
+ import { updateContextFile } from '../context.js';
6
7
  const PROJECT_FIELDS = ['id', 'name', 'region_id', 'created_at'];
7
8
  const REGIONS = [
8
9
  'aws-us-west-2',
@@ -10,6 +11,7 @@ const REGIONS = [
10
11
  'aws-eu-central-1',
11
12
  'aws-us-east-2',
12
13
  'aws-us-east-1',
14
+ 'aws-il-central-1',
13
15
  ];
14
16
  const PROJECTS_LIST_LIMIT = 100;
15
17
  export const command = 'projects';
@@ -36,6 +38,11 @@ export const builder = (argv) => {
36
38
  describe: 'Connect to a new project via psql',
37
39
  default: false,
38
40
  },
41
+ 'set-context': {
42
+ type: 'boolean',
43
+ describe: 'Set the current context to the new project',
44
+ default: false,
45
+ },
39
46
  }), async (args) => {
40
47
  await create(args);
41
48
  })
@@ -86,6 +93,12 @@ const create = async (props) => {
86
93
  const { data } = await props.apiClient.createProject({
87
94
  project,
88
95
  });
96
+ if (props.setContext) {
97
+ updateContextFile(props.contextFile, {
98
+ projectId: data.project.id,
99
+ branchId: data.branch.id,
100
+ });
101
+ }
89
102
  const out = writer(props);
90
103
  out.write(data.project, { fields: PROJECT_FIELDS, title: 'Project' });
91
104
  out.write(data.connection_uris, {
@@ -1,5 +1,9 @@
1
- import { describe } from '@jest/globals';
1
+ import { afterAll, describe, expect, test } from '@jest/globals';
2
+ import { readFileSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
2
5
  import { testCliCommand } from '../test_utils/test_cli_command.js';
6
+ const CONTEXT_FILE = join(tmpdir(), `neon_project_create_ctx_${Date.now()}`);
3
7
  describe('projects', () => {
4
8
  testCliCommand({
5
9
  name: 'list',
@@ -24,11 +28,41 @@ describe('projects', () => {
24
28
  });
25
29
  testCliCommand({
26
30
  name: 'create and connect with psql and psql args',
27
- args: ['projects', 'create', '--name', 'test_project', '--psql', '--', '-c', 'SELECT 1'],
31
+ args: [
32
+ 'projects',
33
+ 'create',
34
+ '--name',
35
+ 'test_project',
36
+ '--psql',
37
+ '--',
38
+ '-c',
39
+ 'SELECT 1',
40
+ ],
28
41
  expected: {
29
42
  snapshot: true,
30
43
  },
31
44
  });
45
+ testCliCommand({
46
+ name: 'create project with setting the context',
47
+ args: [
48
+ 'projects',
49
+ 'create',
50
+ '--name',
51
+ 'test_project',
52
+ '--context-file',
53
+ CONTEXT_FILE,
54
+ '--set-context',
55
+ ],
56
+ expected: {
57
+ snapshot: true,
58
+ },
59
+ });
60
+ afterAll(() => {
61
+ rmSync(CONTEXT_FILE);
62
+ });
63
+ test('context file should exist and contain the project id', () => {
64
+ expect(readFileSync(CONTEXT_FILE, 'utf-8')).toContain('new-project-123456');
65
+ });
32
66
  testCliCommand({
33
67
  name: 'delete',
34
68
  args: ['projects', 'delete', 'test'],
@@ -0,0 +1,27 @@
1
+ import yargs from 'yargs';
2
+ import { showHelpMiddleware } from '../help.js';
3
+ import { updateContextFile } from '../context.js';
4
+ import { branchIdFromProps } from '../utils/enrichers.js';
5
+ export const command = 'set-context';
6
+ export const describe = 'Set the current context';
7
+ export const builder = (argv) => argv
8
+ .middleware(showHelpMiddleware(yargs, true))
9
+ .usage('$0 set-context [options]')
10
+ .options({
11
+ 'project-id': {
12
+ describe: 'Project ID',
13
+ type: 'string',
14
+ },
15
+ branch: {
16
+ describe: 'Branch ID or name',
17
+ type: 'string',
18
+ },
19
+ });
20
+ export const handler = async (props) => {
21
+ const branchId = await branchIdFromProps(props);
22
+ const context = {
23
+ projectId: props.projectId,
24
+ branchId,
25
+ };
26
+ updateContextFile(props.contextFile, context);
27
+ };
@@ -0,0 +1,30 @@
1
+ import { tmpdir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { rmSync } from 'node:fs';
4
+ import { afterAll, describe } from '@jest/globals';
5
+ import { testCliCommand } from '../test_utils/test_cli_command';
6
+ const CONTEXT_FILE = join(tmpdir(), `neon_${Date.now()}`);
7
+ describe('set_context', () => {
8
+ afterAll(() => {
9
+ rmSync(CONTEXT_FILE);
10
+ });
11
+ describe('should set the context', () => {
12
+ testCliCommand({
13
+ name: 'set-context',
14
+ args: [
15
+ 'set-context',
16
+ '--project-id',
17
+ 'test',
18
+ '--context-file',
19
+ CONTEXT_FILE,
20
+ ],
21
+ });
22
+ testCliCommand({
23
+ name: 'list branches selecting project from the context',
24
+ args: ['branches', 'list', '--context-file', CONTEXT_FILE],
25
+ expected: {
26
+ snapshot: true,
27
+ },
28
+ });
29
+ });
30
+ });
package/context.js ADDED
@@ -0,0 +1,48 @@
1
+ import { accessSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { normalize, resolve } from 'node:path';
4
+ const CONTEXT_FILE = '.neon';
5
+ const CHECK_FILES = [CONTEXT_FILE, 'package.json', '.git'];
6
+ const wrapWithContextFile = (dir) => resolve(dir, CONTEXT_FILE);
7
+ export const currentContextFile = () => {
8
+ const cwd = process.cwd();
9
+ let currentDir = cwd;
10
+ const root = normalize('/');
11
+ const home = homedir();
12
+ while (currentDir !== root && currentDir !== home) {
13
+ for (const file of CHECK_FILES) {
14
+ try {
15
+ accessSync(resolve(currentDir, file));
16
+ return wrapWithContextFile(currentDir);
17
+ }
18
+ catch (e) {
19
+ // ignore
20
+ }
21
+ }
22
+ currentDir = resolve(currentDir, '..');
23
+ }
24
+ return wrapWithContextFile(cwd);
25
+ };
26
+ export const readContextFile = (file) => {
27
+ try {
28
+ return JSON.parse(readFileSync(file, 'utf-8'));
29
+ }
30
+ catch (e) {
31
+ return {};
32
+ }
33
+ };
34
+ export const enrichFromContext = (args) => {
35
+ if (args._[0] === 'set-context') {
36
+ return;
37
+ }
38
+ const context = readContextFile(args.contextFile);
39
+ if (!args.branch) {
40
+ args.branch = context.branchId;
41
+ }
42
+ if (!args.projectId) {
43
+ args.projectId = context.projectId;
44
+ }
45
+ };
46
+ export const updateContextFile = (file, context) => {
47
+ writeFileSync(file, JSON.stringify(context, null, 2));
48
+ };
package/index.js CHANGED
@@ -21,10 +21,10 @@ import { fillInArgs } from './utils/middlewares.js';
21
21
  import pkg from './pkg.js';
22
22
  import commands from './commands/index.js';
23
23
  import { analyticsMiddleware, sendError } from './analytics.js';
24
- import { isCi } from './env.js';
25
24
  import { isAxiosError } from 'axios';
26
25
  import { matchErrorCode } from './errors.js';
27
26
  import { showHelp } from './help.js';
27
+ import { currentContextFile, enrichFromContext } from './context.js';
28
28
  let builder = yargs(hideBin(process.argv));
29
29
  builder = builder
30
30
  .scriptName(pkg.name)
@@ -61,38 +61,45 @@ builder = builder
61
61
  default: false,
62
62
  })
63
63
  .middleware(ensureConfigDir)
64
- // Auth flow
65
- .option('oauth-host', {
66
- description: 'URL to Neon OAuth host',
67
- hidden: true,
68
- default: 'https://oauth2.neon.tech',
69
- })
70
- .option('client-id', {
71
- description: 'OAuth client id',
72
- hidden: true,
73
- type: 'string',
74
- default: defaultClientID,
75
- })
76
- .option('api-key', {
77
- describe: 'API key',
78
- group: 'Global options:',
79
- type: 'string',
80
- default: process.env.NEON_API_KEY ?? '',
81
- })
82
- .option('apiClient', {
83
- hidden: true,
84
- coerce: (v) => v,
85
- default: null,
64
+ .options({
65
+ 'oauth-host': {
66
+ description: 'URL to Neon OAuth host',
67
+ hidden: true,
68
+ default: 'https://oauth2.neon.tech',
69
+ },
70
+ 'client-id': {
71
+ description: 'OAuth client id',
72
+ hidden: true,
73
+ type: 'string',
74
+ default: defaultClientID,
75
+ },
76
+ 'api-key': {
77
+ describe: 'API key',
78
+ group: 'Global options:',
79
+ type: 'string',
80
+ default: process.env.NEON_API_KEY ?? '',
81
+ },
82
+ apiClient: {
83
+ hidden: true,
84
+ coerce: (v) => v,
85
+ default: null,
86
+ },
87
+ 'context-file': {
88
+ describe: 'Context file',
89
+ type: 'string',
90
+ default: currentContextFile,
91
+ },
86
92
  })
87
93
  .middleware((args) => fillInArgs(args), true)
88
94
  .middleware(ensureAuth)
95
+ .middleware(enrichFromContext)
89
96
  .command(commands)
90
97
  .strictCommands()
91
98
  .option('analytics', {
92
99
  describe: 'Manage analytics. Example: --no-analytics, --analytics false',
93
100
  group: 'Global options:',
94
101
  type: 'boolean',
95
- default: !isCi(),
102
+ default: true,
96
103
  })
97
104
  .middleware(analyticsMiddleware, true)
98
105
  .group('version', 'Global options:')
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.21.3",
8
+ "version": "1.23.0",
9
9
  "description": "CLI tool for NeonDB Cloud management",
10
10
  "main": "index.js",
11
11
  "author": "NeonDB",
@@ -47,6 +47,7 @@
47
47
  "prettier": "^2.7.1",
48
48
  "rollup": "^3.26.2",
49
49
  "semantic-release": "^21.0.2",
50
+ "strip-ansi": "^7.1.0",
50
51
  "ts-jest": "^29.1.0",
51
52
  "ts-node": "^10.9.1",
52
53
  "typescript": "^4.7.4"
@@ -2,6 +2,7 @@ import { test, expect, describe, beforeAll, afterAll } from '@jest/globals';
2
2
  import { fork } from 'node:child_process';
3
3
  import { join } from 'node:path';
4
4
  import { log } from '../log.js';
5
+ import strip from 'strip-ansi';
5
6
  import { runMockServer } from './mock_server.js';
6
7
  export const testCliCommand = ({ args, name, expected, mockDir = 'main', }) => {
7
8
  let server;
@@ -52,10 +53,10 @@ export const testCliCommand = ({ args, name, expected, mockDir = 'main', }) => {
52
53
  expect(output).toMatchSnapshot();
53
54
  }
54
55
  if (expected.stdout !== undefined) {
55
- expect(output).toEqual(expected.stdout);
56
+ expect(strip(output)).toEqual(expected.stdout);
56
57
  }
57
58
  if (expected.stderr !== undefined) {
58
- expect(error).toEqual(expected.stderr);
59
+ expect(strip(error)).toEqual(expected.stderr);
59
60
  }
60
61
  }
61
62
  resolve();