neonctl 2.21.2 → 2.22.2

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.
@@ -0,0 +1,119 @@
1
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest';
5
+ import { applyContext, currentContextFile, ensureGitignored, } from './context.js';
6
+ describe('currentContextFile', () => {
7
+ let workspace;
8
+ beforeEach(() => {
9
+ workspace = mkdtempSync(join(tmpdir(), 'neonctl-ctx-'));
10
+ });
11
+ afterEach(() => {
12
+ rmSync(workspace, { recursive: true, force: true });
13
+ });
14
+ test('defaults to <cwd>/.neon when no .neon exists anywhere upward', () => {
15
+ const sub = join(workspace, 'sub');
16
+ mkdirSync(sub);
17
+ expect(currentContextFile(sub)).toBe(join(sub, '.neon'));
18
+ });
19
+ test('walks up to an existing .neon in a parent directory', () => {
20
+ writeFileSync(join(workspace, '.neon'), JSON.stringify({ projectId: 'parent-project' }));
21
+ const sub = join(workspace, 'nested', 'deeper');
22
+ mkdirSync(sub, { recursive: true });
23
+ expect(currentContextFile(sub)).toBe(join(workspace, '.neon'));
24
+ });
25
+ test('does NOT walk up for unrelated project markers (package.json, .git)', () => {
26
+ // Regression: previously `currentContextFile` treated `package.json` and
27
+ // `.git` as project markers and walked up to them, which made
28
+ // `neonctl link` from a fresh sub-directory inside an existing repo land
29
+ // its `.neon` at the parent repo's root instead of the cwd.
30
+ writeFileSync(join(workspace, 'package.json'), '{}');
31
+ mkdirSync(join(workspace, '.git'));
32
+ const sub = join(workspace, 'fresh-sub');
33
+ mkdirSync(sub);
34
+ expect(currentContextFile(sub)).toBe(join(sub, '.neon'));
35
+ });
36
+ });
37
+ describe('ensureGitignored', () => {
38
+ let workspace;
39
+ beforeEach(() => {
40
+ workspace = mkdtempSync(join(tmpdir(), 'neonctl-gi-'));
41
+ });
42
+ afterEach(() => {
43
+ rmSync(workspace, { recursive: true, force: true });
44
+ });
45
+ test('creates a .gitignore listing .neon when none exists', () => {
46
+ const contextFile = join(workspace, '.neon');
47
+ ensureGitignored(contextFile);
48
+ const gitignore = readFileSync(join(workspace, '.gitignore'), 'utf-8');
49
+ expect(gitignore).toBe('.neon\n');
50
+ });
51
+ test('appends .neon to an existing .gitignore that does not have it', () => {
52
+ const gi = join(workspace, '.gitignore');
53
+ writeFileSync(gi, 'node_modules\ndist\n');
54
+ ensureGitignored(join(workspace, '.neon'));
55
+ expect(readFileSync(gi, 'utf-8')).toBe('node_modules\ndist\n.neon\n');
56
+ });
57
+ test('does not duplicate .neon when already present', () => {
58
+ const gi = join(workspace, '.gitignore');
59
+ writeFileSync(gi, 'node_modules\n.neon\ndist\n');
60
+ ensureGitignored(join(workspace, '.neon'));
61
+ expect(readFileSync(gi, 'utf-8')).toBe('node_modules\n.neon\ndist\n');
62
+ });
63
+ test('tolerates a .gitignore that has no trailing newline', () => {
64
+ const gi = join(workspace, '.gitignore');
65
+ writeFileSync(gi, 'node_modules');
66
+ ensureGitignored(join(workspace, '.neon'));
67
+ expect(readFileSync(gi, 'utf-8')).toBe('node_modules\n.neon\n');
68
+ });
69
+ test('treats surrounding whitespace as part of the line', () => {
70
+ const gi = join(workspace, '.gitignore');
71
+ // Trailing spaces around the entry should still count as a match.
72
+ writeFileSync(gi, ' .neon \n');
73
+ ensureGitignored(join(workspace, '.neon'));
74
+ expect(readFileSync(gi, 'utf-8')).toBe(' .neon \n');
75
+ });
76
+ test('does NOT match partial entries like *.neon or foo/.neon', () => {
77
+ const gi = join(workspace, '.gitignore');
78
+ writeFileSync(gi, '*.neon\nfoo/.neon\n');
79
+ ensureGitignored(join(workspace, '.neon'));
80
+ expect(readFileSync(gi, 'utf-8')).toBe('*.neon\nfoo/.neon\n.neon\n');
81
+ });
82
+ });
83
+ describe('applyContext', () => {
84
+ let workspace;
85
+ beforeEach(() => {
86
+ workspace = mkdtempSync(join(tmpdir(), 'neonctl-apply-'));
87
+ });
88
+ afterEach(() => {
89
+ rmSync(workspace, { recursive: true, force: true });
90
+ });
91
+ test('scaffolds .gitignore only when the context file is created', () => {
92
+ const file = join(workspace, '.neon');
93
+ applyContext(file, {
94
+ orgId: 'org-x',
95
+ projectId: 'proj-y',
96
+ branchId: 'br-z',
97
+ });
98
+ expect(JSON.parse(readFileSync(file, 'utf-8'))).toEqual({
99
+ orgId: 'org-x',
100
+ projectId: 'proj-y',
101
+ branchId: 'br-z',
102
+ });
103
+ expect(readFileSync(join(workspace, '.gitignore'), 'utf-8')).toBe('.neon\n');
104
+ });
105
+ test('does NOT re-add .neon to .gitignore on updates to an existing file', () => {
106
+ const file = join(workspace, '.neon');
107
+ // First write creates the file and scaffolds .gitignore.
108
+ applyContext(file, { projectId: 'proj-y', branchId: 'br-1' });
109
+ // The user deliberately un-ignores .neon (e.g. to commit shared context).
110
+ writeFileSync(join(workspace, '.gitignore'), 'node_modules\n');
111
+ // A subsequent update must NOT re-add the entry.
112
+ applyContext(file, { projectId: 'proj-y', branchId: 'br-2' });
113
+ expect(JSON.parse(readFileSync(file, 'utf-8'))).toEqual({
114
+ projectId: 'proj-y',
115
+ branchId: 'br-2',
116
+ });
117
+ expect(readFileSync(join(workspace, '.gitignore'), 'utf-8')).toBe('node_modules\n');
118
+ });
119
+ });
package/index.js CHANGED
@@ -33,7 +33,10 @@ const NO_SUBCOMMANDS_VERBS = [
33
33
  // aliases
34
34
  'cs',
35
35
  'connection-string',
36
+ 'psql',
36
37
  'set-context',
38
+ 'checkout',
39
+ 'link',
37
40
  'init',
38
41
  // aliases
39
42
  ];
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "git+ssh://git@github.com/neondatabase/neonctl.git"
6
6
  },
7
7
  "type": "module",
8
- "version": "2.21.2",
8
+ "version": "2.22.2",
9
9
  "description": "CLI tool for NeonDB Cloud management",
10
10
  "main": "index.js",
11
11
  "author": "NeonDB",
@@ -14,62 +14,58 @@
14
14
  "engines": {
15
15
  "node": ">=18"
16
16
  },
17
+ "packageManager": "pnpm@9.15.9",
17
18
  "bin": {
18
19
  "neonctl": "cli.js",
19
20
  "neon": "cli.js"
20
21
  },
21
22
  "devDependencies": {
22
- "@apidevtools/swagger-parser": "^10.1.0",
23
- "@commitlint/cli": "^17.6.5",
24
- "@commitlint/config-conventional": "^17.6.5",
25
- "@eslint/js": "^9.23.0",
26
- "@rollup/plugin-commonjs": "^25.0.2",
27
- "@rollup/plugin-json": "^6.0.0",
28
- "@rollup/plugin-node-resolve": "^15.1.0",
29
- "@semantic-release/exec": "^6.0.3",
30
- "@semantic-release/git": "^10.0.1",
31
- "@types/bun": "^1.1.4",
32
- "@types/cli-table": "^0.3.0",
33
- "@types/diff": "^5.2.1",
34
- "@types/eslint__js": "^8.42.3",
35
- "@types/express": "^4.17.17",
36
- "@types/node": "^18.7.13",
23
+ "@apidevtools/swagger-parser": "10.1.0",
24
+ "@commitlint/cli": "17.8.1",
25
+ "@commitlint/config-conventional": "17.8.1",
26
+ "@eslint/js": "9.29.0",
27
+ "@rollup/plugin-commonjs": "25.0.8",
28
+ "@rollup/plugin-json": "6.1.0",
29
+ "@rollup/plugin-node-resolve": "15.2.3",
30
+ "@types/cli-table": "0.3.4",
31
+ "@types/diff": "5.2.1",
32
+ "@types/eslint__js": "8.42.3",
33
+ "@types/express": "4.17.21",
34
+ "@types/node": "18.19.41",
37
35
  "@types/prompts": "2.4.9",
38
- "@types/validate-npm-package-name": "4.0.2",
39
- "@types/which": "^3.0.0",
40
- "@types/yargs": "^17.0.24",
41
- "emocks": "^3.0.1",
42
- "eslint": "^9.23.0",
43
- "express": "^4.18.2",
44
- "husky": "^8.0.3",
45
- "lint-staged": "^13.0.3",
46
- "oauth2-mock-server": "^8.1.0",
47
- "prettier": "^3.1.0",
48
- "rollup": "^3.26.2",
49
- "semantic-release": "^23.0.8",
50
- "strip-ansi": "^7.1.0",
51
- "typescript": "^4.7.4",
36
+ "@types/which": "3.0.4",
37
+ "@types/yargs": "17.0.32",
38
+ "@yao-pkg/pkg": "6.10.0",
39
+ "emocks": "3.0.4",
40
+ "eslint": "9.29.0",
41
+ "express": "4.19.2",
42
+ "husky": "8.0.3",
43
+ "lint-staged": "13.3.0",
44
+ "oauth2-mock-server": "8.1.0",
45
+ "prettier": "3.3.3",
46
+ "rollup": "3.29.4",
47
+ "strip-ansi": "7.1.0",
48
+ "tsx": "4.22.3",
49
+ "typescript": "4.9.5",
52
50
  "typescript-eslint": "8.28.0",
53
- "vitest": "^1.6.0"
51
+ "vitest": "1.6.1"
54
52
  },
55
53
  "dependencies": {
56
54
  "@neondatabase/api-client": "2.6.0",
57
- "@segment/analytics-node": "^1.0.0-beta.26",
58
- "@yao-pkg/pkg": "^6.10.0",
59
- "axios": "^1.4.0",
60
- "axios-debug-log": "^1.0.0",
61
- "chalk": "^5.2.0",
62
- "cli-table": "^0.3.11",
63
- "crypto-random-string": "^5.0.0",
64
- "diff": "^5.2.0",
65
- "neon-init": "^0.13.1",
66
- "open": "^10.1.0",
67
- "openid-client": "^6.8.1",
55
+ "@segment/analytics-node": "1.3.0",
56
+ "axios": "1.7.2",
57
+ "axios-debug-log": "1.0.0",
58
+ "chalk": "5.3.0",
59
+ "cli-table": "0.3.11",
60
+ "cliui": "8.0.1",
61
+ "diff": "5.2.0",
62
+ "neon-init": "0.14.0",
63
+ "open": "10.1.0",
64
+ "openid-client": "6.8.1",
68
65
  "prompts": "2.4.2",
69
- "validate-npm-package-name": "5.0.1",
70
- "which": "^3.0.1",
71
- "yaml": "^2.1.1",
72
- "yargs": "^17.7.2"
66
+ "which": "3.0.1",
67
+ "yaml": "2.4.5",
68
+ "yargs": "17.7.2"
73
69
  },
74
70
  "publishConfig": {
75
71
  "access": "public",
@@ -93,13 +89,13 @@
93
89
  "scripts": {
94
90
  "watch": "tsc --watch",
95
91
  "typecheck": "tsc --noEmit",
96
- "lint": "npm run typecheck && eslint src && prettier --check .",
97
- "lint:fix": "npm run typecheck && eslint src --fix && prettier --w .",
98
- "build": "bun generateParams && bun clean && tsc && cp src/*.html package*.json README.md ./dist",
92
+ "lint": "pnpm typecheck && eslint src && prettier --check .",
93
+ "lint:fix": "pnpm typecheck && eslint src --fix && prettier --w .",
94
+ "build": "pnpm generateParams && pnpm clean && tsc && cp src/*.html package*.json README.md ./dist",
99
95
  "clean": "rm -rf dist",
100
- "generateParams": "bun generateOptionsFromSpec.ts",
101
- "start": "bun dist/index.js",
102
- "pretest": "bun run build",
96
+ "generateParams": "tsx generateOptionsFromSpec.ts",
97
+ "start": "node dist/index.js",
98
+ "pretest": "pnpm build",
103
99
  "test": "vitest run",
104
100
  "prepare": "test -d .git && husky install || true"
105
101
  },
@@ -40,9 +40,26 @@ export const branchIdFromProps = async (props) => {
40
40
  props.branchId = await getBranchIdFromProps(props);
41
41
  return props.branchId;
42
42
  };
43
+ export const resolveSingleDatabase = async (props) => {
44
+ const { data } = await props.apiClient.listProjectBranchDatabases(props.projectId, props.branchId);
45
+ const databases = data.databases;
46
+ if (props.database !== undefined) {
47
+ if (!databases.find((d) => d.name === props.database)) {
48
+ throw new Error(`Database not found: ${props.database}. Available databases on branch ${props.branchId}: ${databases.map((d) => d.name).join(', ')}`);
49
+ }
50
+ return props.database;
51
+ }
52
+ if (databases.length === 0) {
53
+ throw new Error(`No databases found for the branch: ${props.branchId}`);
54
+ }
55
+ if (databases.length === 1) {
56
+ return databases[0].name;
57
+ }
58
+ throw new Error(`Multiple databases found for the branch, please provide one with the --database option: ${databases.map((d) => d.name).join(', ')}`);
59
+ };
43
60
  export const fillSingleProject = async (props) => {
44
61
  if (props.projectId) {
45
- return props;
62
+ return { ...props, projectId: props.projectId };
46
63
  }
47
64
  // If no orgId is provided, try to auto-fill it if there's only one org
48
65
  let orgId = props.orgId;
@@ -5,7 +5,7 @@
5
5
  */
6
6
  export const fillInArgs = (args, currentArgs = args, acc = []) => {
7
7
  Object.entries(currentArgs).forEach(([k, v]) => {
8
- if (k === '_') {
8
+ if (k === '_' || k === '--') {
9
9
  return;
10
10
  }
11
11
  // check if the value is an Object