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.
- package/README.md +158 -16
- package/commands/checkout.js +249 -0
- package/commands/checkout.test.js +170 -0
- package/commands/connection_string.js +6 -1
- package/commands/data_api.js +286 -0
- package/commands/data_api.test.js +169 -0
- package/commands/index.js +8 -0
- package/commands/link.js +667 -0
- package/commands/link.test.js +381 -0
- package/commands/psql.js +57 -0
- package/commands/psql.test.js +49 -0
- package/commands/set_context.js +7 -2
- package/context.js +86 -14
- package/context.test.js +119 -0
- package/index.js +3 -0
- package/package.json +48 -52
- package/utils/enrichers.js +18 -1
- package/utils/middlewares.js +1 -1
package/context.test.js
ADDED
|
@@ -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
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.
|
|
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": "
|
|
23
|
-
"@commitlint/cli": "
|
|
24
|
-
"@commitlint/config-conventional": "
|
|
25
|
-
"@eslint/js": "
|
|
26
|
-
"@rollup/plugin-commonjs": "
|
|
27
|
-
"@rollup/plugin-json": "
|
|
28
|
-
"@rollup/plugin-node-resolve": "
|
|
29
|
-
"@
|
|
30
|
-
"@
|
|
31
|
-
"@types/
|
|
32
|
-
"@types/
|
|
33
|
-
"@types/
|
|
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/
|
|
39
|
-
"@types/
|
|
40
|
-
"@
|
|
41
|
-
"emocks": "
|
|
42
|
-
"eslint": "
|
|
43
|
-
"express": "
|
|
44
|
-
"husky": "
|
|
45
|
-
"lint-staged": "
|
|
46
|
-
"oauth2-mock-server": "
|
|
47
|
-
"prettier": "
|
|
48
|
-
"rollup": "
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"typescript": "
|
|
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": "
|
|
51
|
+
"vitest": "1.6.1"
|
|
54
52
|
},
|
|
55
53
|
"dependencies": {
|
|
56
54
|
"@neondatabase/api-client": "2.6.0",
|
|
57
|
-
"@segment/analytics-node": "
|
|
58
|
-
"
|
|
59
|
-
"axios": "
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
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
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
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": "
|
|
97
|
-
"lint:fix": "
|
|
98
|
-
"build": "
|
|
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": "
|
|
101
|
-
"start": "
|
|
102
|
-
"pretest": "
|
|
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
|
},
|
package/utils/enrichers.js
CHANGED
|
@@ -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;
|
package/utils/middlewares.js
CHANGED