neonctl 2.22.0 → 2.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.
Files changed (116) hide show
  1. package/README.md +242 -16
  2. package/analytics.js +5 -2
  3. package/commands/branches.js +9 -1
  4. package/commands/checkout.js +249 -0
  5. package/commands/connection_string.js +15 -2
  6. package/commands/data_api.js +286 -0
  7. package/commands/functions.js +277 -0
  8. package/commands/index.js +12 -0
  9. package/commands/link.js +667 -0
  10. package/commands/neon_auth.js +1013 -0
  11. package/commands/projects.js +9 -1
  12. package/commands/psql.js +62 -0
  13. package/commands/set_context.js +7 -2
  14. package/context.js +86 -14
  15. package/functions_api.js +44 -0
  16. package/index.js +3 -0
  17. package/package.json +60 -51
  18. package/psql/cli.js +51 -0
  19. package/psql/command/cmd_cond.js +437 -0
  20. package/psql/command/cmd_connect.js +815 -0
  21. package/psql/command/cmd_copy.js +1025 -0
  22. package/psql/command/cmd_describe.js +1810 -0
  23. package/psql/command/cmd_format.js +909 -0
  24. package/psql/command/cmd_io.js +2187 -0
  25. package/psql/command/cmd_lo.js +385 -0
  26. package/psql/command/cmd_meta.js +970 -0
  27. package/psql/command/cmd_misc.js +187 -0
  28. package/psql/command/cmd_pipeline.js +1141 -0
  29. package/psql/command/cmd_restrict.js +171 -0
  30. package/psql/command/cmd_show.js +751 -0
  31. package/psql/command/dispatch.js +343 -0
  32. package/psql/command/inputQueue.js +42 -0
  33. package/psql/command/shared.js +71 -0
  34. package/psql/complete/filenames.js +139 -0
  35. package/psql/complete/index.js +104 -0
  36. package/psql/complete/matcher.js +314 -0
  37. package/psql/complete/psqlVars.js +247 -0
  38. package/psql/complete/queries.js +491 -0
  39. package/psql/complete/rules.js +2387 -0
  40. package/psql/core/common.js +1250 -0
  41. package/psql/core/help.js +576 -0
  42. package/psql/core/mainloop.js +1353 -0
  43. package/psql/core/prompt.js +437 -0
  44. package/psql/core/settings.js +684 -0
  45. package/psql/core/sqlHelp.js +1066 -0
  46. package/psql/core/startup.js +840 -0
  47. package/psql/core/syncVars.js +116 -0
  48. package/psql/core/variables.js +287 -0
  49. package/psql/describe/formatters.js +1277 -0
  50. package/psql/describe/processNamePattern.js +270 -0
  51. package/psql/describe/queries.js +2373 -0
  52. package/psql/describe/versionGate.js +43 -0
  53. package/psql/index.js +2005 -0
  54. package/psql/io/history.js +299 -0
  55. package/psql/io/input.js +120 -0
  56. package/psql/io/lineEditor/buffer.js +323 -0
  57. package/psql/io/lineEditor/complete.js +227 -0
  58. package/psql/io/lineEditor/filename.js +159 -0
  59. package/psql/io/lineEditor/index.js +891 -0
  60. package/psql/io/lineEditor/keymap.js +738 -0
  61. package/psql/io/lineEditor/vt100.js +363 -0
  62. package/psql/io/pgpass.js +202 -0
  63. package/psql/io/pgservice.js +194 -0
  64. package/psql/io/psqlrc.js +422 -0
  65. package/psql/print/aligned.js +1756 -0
  66. package/psql/print/asciidoc.js +248 -0
  67. package/psql/print/crosstab.js +460 -0
  68. package/psql/print/csv.js +92 -0
  69. package/psql/print/html.js +258 -0
  70. package/psql/print/json.js +96 -0
  71. package/psql/print/latex.js +396 -0
  72. package/psql/print/pager.js +265 -0
  73. package/psql/print/troff.js +258 -0
  74. package/psql/print/unaligned.js +118 -0
  75. package/psql/print/units.js +135 -0
  76. package/psql/scanner/slash.js +513 -0
  77. package/psql/scanner/sql.js +910 -0
  78. package/psql/scanner/stringutils.js +390 -0
  79. package/psql/types/backslash.js +1 -0
  80. package/psql/types/connection.js +1 -0
  81. package/psql/types/index.js +7 -0
  82. package/psql/types/printer.js +1 -0
  83. package/psql/types/repl.js +1 -0
  84. package/psql/types/scanner.js +24 -0
  85. package/psql/types/settings.js +1 -0
  86. package/psql/types/variables.js +1 -0
  87. package/psql/wire/connection.js +2844 -0
  88. package/psql/wire/copy.js +108 -0
  89. package/psql/wire/notify.js +59 -0
  90. package/psql/wire/pipeline.js +519 -0
  91. package/psql/wire/protocol.js +466 -0
  92. package/psql/wire/sasl.js +296 -0
  93. package/psql/wire/tls.js +596 -0
  94. package/test_utils/fixtures.js +1 -0
  95. package/utils/enrichers.js +18 -1
  96. package/utils/esbuild.js +147 -0
  97. package/utils/middlewares.js +1 -1
  98. package/utils/psql.js +107 -11
  99. package/utils/zip.js +4 -0
  100. package/writer.js +1 -1
  101. package/commands/auth.test.js +0 -211
  102. package/commands/branches.test.js +0 -460
  103. package/commands/connection_string.test.js +0 -196
  104. package/commands/databases.test.js +0 -39
  105. package/commands/help.test.js +0 -9
  106. package/commands/init.test.js +0 -56
  107. package/commands/ip_allow.test.js +0 -59
  108. package/commands/operations.test.js +0 -7
  109. package/commands/orgs.test.js +0 -7
  110. package/commands/projects.test.js +0 -144
  111. package/commands/roles.test.js +0 -37
  112. package/commands/set_context.test.js +0 -159
  113. package/commands/vpc_endpoints.test.js +0 -69
  114. package/env.test.js +0 -55
  115. package/utils/formats.test.js +0 -32
  116. package/writer.test.js +0 -104
@@ -77,6 +77,12 @@ export const builder = (argv) => {
77
77
  describe: 'Connect to a new project via psql',
78
78
  default: false,
79
79
  },
80
+ fallback: {
81
+ type: 'boolean',
82
+ describe: 'Force the embedded TypeScript psql fallback (for testing)',
83
+ default: false,
84
+ hidden: true,
85
+ },
80
86
  database: {
81
87
  describe: projectCreateRequest['project.branch.database_name'].description,
82
88
  type: 'string',
@@ -242,7 +248,9 @@ const create = async (props) => {
242
248
  if (props.psql) {
243
249
  const connection_uri = data.connection_uris[0].connection_uri;
244
250
  const psqlArgs = props['--'];
245
- await psql(connection_uri, psqlArgs);
251
+ await psql(connection_uri, psqlArgs, {
252
+ mode: props.fallback ? 'ts' : 'auto',
253
+ });
246
254
  }
247
255
  };
248
256
  const deleteProject = async (props) => {
@@ -0,0 +1,62 @@
1
+ import { EndpointType } from '@neondatabase/api-client';
2
+ import { fillSingleProject } from '../utils/enrichers.js';
3
+ import { handler as connectionStringHandler, SSL_MODES, } from './connection_string.js';
4
+ export const command = 'psql [branch]';
5
+ export const describe = 'Connect to a database via psql';
6
+ export const builder = (argv) => {
7
+ return argv
8
+ .usage('$0 psql [branch] [options] [-- psql-args]')
9
+ .example('$0 psql', 'Connect to the default branch via psql')
10
+ .example('$0 psql main', 'Connect to the main branch via psql')
11
+ .example('$0 psql main -- -c "SELECT 1"', 'Run a single query against the main branch')
12
+ .example('$0 psql main@2024-01-01T00:00:00Z', 'Connect to the main branch at a specific point in time')
13
+ .positional('branch', {
14
+ describe: `Branch name or id. Defaults to the default branch if omitted. Can be written in the point-in-time format: "branch@timestamp" or "branch@lsn"`,
15
+ type: 'string',
16
+ })
17
+ .options({
18
+ 'project-id': {
19
+ type: 'string',
20
+ describe: 'Project ID',
21
+ },
22
+ 'role-name': {
23
+ type: 'string',
24
+ describe: 'Role name',
25
+ },
26
+ 'database-name': {
27
+ type: 'string',
28
+ describe: 'Database name',
29
+ },
30
+ pooled: {
31
+ type: 'boolean',
32
+ describe: 'Use pooled connection',
33
+ default: false,
34
+ },
35
+ 'endpoint-type': {
36
+ type: 'string',
37
+ choices: Object.values(EndpointType),
38
+ describe: 'Endpoint type',
39
+ },
40
+ ssl: {
41
+ type: 'string',
42
+ choices: SSL_MODES,
43
+ default: 'require',
44
+ describe: 'SSL mode',
45
+ },
46
+ fallback: {
47
+ type: 'boolean',
48
+ describe: 'Force the embedded TypeScript psql fallback (for testing)',
49
+ default: false,
50
+ hidden: true,
51
+ },
52
+ })
53
+ .middleware(fillSingleProject);
54
+ };
55
+ export const handler = async (props) => {
56
+ await connectionStringHandler({
57
+ ...props,
58
+ psql: true,
59
+ prisma: false,
60
+ extended: false,
61
+ });
62
+ };
@@ -1,4 +1,4 @@
1
- import { updateContextFile } from '../context.js';
1
+ import { applyContext } from '../context.js';
2
2
  export const command = 'set-context';
3
3
  export const describe = 'Set the current context';
4
4
  export const builder = (argv) => argv.usage('$0 set-context [options]').options({
@@ -10,11 +10,16 @@ export const builder = (argv) => argv.usage('$0 set-context [options]').options(
10
10
  describe: 'Organization ID',
11
11
  type: 'string',
12
12
  },
13
+ 'branch-id': {
14
+ describe: 'Branch ID',
15
+ type: 'string',
16
+ },
13
17
  });
14
18
  export const handler = (props) => {
15
19
  const context = {
16
20
  projectId: props.projectId,
17
21
  orgId: props.orgId,
22
+ branchId: props.branchId,
18
23
  };
19
- updateContextFile(props.contextFile, context);
24
+ applyContext(props.contextFile, context);
20
25
  };
package/context.js CHANGED
@@ -1,23 +1,39 @@
1
- import { accessSync, readFileSync, writeFileSync } from 'node:fs';
1
+ import { accessSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
- import { normalize, resolve } from 'node:path';
3
+ import { dirname, normalize, resolve } from 'node:path';
4
+ import { log } from './log.js';
4
5
  const CONTEXT_FILE = '.neon';
5
- const CHECK_FILES = [CONTEXT_FILE, 'package.json', '.git'];
6
+ const GITIGNORE_FILE = '.gitignore';
6
7
  const wrapWithContextFile = (dir) => resolve(dir, CONTEXT_FILE);
7
- export const currentContextFile = () => {
8
- const cwd = process.cwd();
8
+ /**
9
+ * Resolve the default `.neon` path for the current working directory.
10
+ *
11
+ * Walks UP from `cwd` looking ONLY for an already-existing `.neon` file so
12
+ * commands run from a sub-directory of a linked project still pick up the
13
+ * project's context. If no `.neon` is found, the path defaults to
14
+ * `<cwd>/.neon`, which makes `neonctl link` and `neonctl set-context`
15
+ * predictable: they always write the context file into the directory they
16
+ * were invoked from.
17
+ *
18
+ * Historically the walk also considered `package.json` and `.git` as project
19
+ * markers, but that led to surprising behaviour when running `link` from a
20
+ * fresh sub-directory inside an unrelated repo (the new link would land in
21
+ * the parent repo's root instead of the cwd).
22
+ *
23
+ * `cwd` is overridable so tests can exercise the walk-up without mutating
24
+ * `process.cwd()` (which would race with other tests running in parallel).
25
+ */
26
+ export const currentContextFile = (cwd = process.cwd()) => {
9
27
  let currentDir = cwd;
10
28
  const root = normalize('/');
11
29
  const home = homedir();
12
30
  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 {
19
- // ignore
20
- }
31
+ try {
32
+ accessSync(resolve(currentDir, CONTEXT_FILE));
33
+ return wrapWithContextFile(currentDir);
34
+ }
35
+ catch {
36
+ // ignore
21
37
  }
22
38
  currentDir = resolve(currentDir, '..');
23
39
  }
@@ -32,7 +48,7 @@ export const readContextFile = (file) => {
32
48
  }
33
49
  };
34
50
  export const enrichFromContext = (args) => {
35
- if (args._[0] === 'set-context') {
51
+ if (args._[0] === 'set-context' || args._[0] === 'link') {
36
52
  return;
37
53
  }
38
54
  const context = readContextFile(args.contextFile);
@@ -52,3 +68,59 @@ export const enrichFromContext = (args) => {
52
68
  export const updateContextFile = (file, context) => {
53
69
  writeFileSync(file, JSON.stringify(context, null, 2));
54
70
  };
71
+ /**
72
+ * Shared primitive used by `set-context`, `link`, and `checkout` to persist
73
+ * context. Mirrors the destructive write semantics of `updateContextFile` —
74
+ * any field not present in `context` is dropped from the file.
75
+ *
76
+ * `.gitignore` scaffolding only happens when the context file is being
77
+ * *created* (it didn't exist before this write). On updates to an existing
78
+ * `.neon` we never touch `.gitignore`, so a user who deliberately un-ignored
79
+ * the file (e.g. to commit shared context) won't have the entry re-added on
80
+ * every subsequent command.
81
+ */
82
+ export const applyContext = (file, context) => {
83
+ const isNewFile = !existsSync(file);
84
+ updateContextFile(file, context);
85
+ if (isNewFile) {
86
+ ensureGitignored(file);
87
+ }
88
+ };
89
+ /**
90
+ * Make sure the `.gitignore` next to `file` lists the file's basename
91
+ * (currently always `.neon`). Creates the `.gitignore` if it doesn't exist,
92
+ * or appends `.neon` if it's missing — never duplicates an existing entry.
93
+ *
94
+ * Best-effort: a failure here (e.g. read-only filesystem) is logged at debug
95
+ * level and swallowed; persisting the context file is the primary goal and
96
+ * must not be blocked by a `.gitignore` write error.
97
+ */
98
+ export const ensureGitignored = (file) => {
99
+ try {
100
+ const dir = dirname(file);
101
+ const entry = basenameOf(file);
102
+ const gitignorePath = resolve(dir, GITIGNORE_FILE);
103
+ if (!existsSync(gitignorePath)) {
104
+ writeFileSync(gitignorePath, `${entry}\n`);
105
+ return;
106
+ }
107
+ const current = readFileSync(gitignorePath, 'utf-8');
108
+ if (hasGitignoreEntry(current, entry)) {
109
+ return;
110
+ }
111
+ const needsLeadingNewline = current.length > 0 && !current.endsWith('\n');
112
+ const addition = `${needsLeadingNewline ? '\n' : ''}${entry}\n`;
113
+ writeFileSync(gitignorePath, current + addition);
114
+ }
115
+ catch (err) {
116
+ const message = err instanceof Error ? err.message : String(err);
117
+ log.debug('Failed to update .gitignore next to %s: %s', file, message);
118
+ }
119
+ };
120
+ const basenameOf = (file) => {
121
+ const parts = file.split(/[\\/]/);
122
+ return parts[parts.length - 1] || CONTEXT_FILE;
123
+ };
124
+ const hasGitignoreEntry = (content, entry) => {
125
+ return content.split(/\r?\n/).some((line) => line.trim() === entry);
126
+ };
@@ -0,0 +1,44 @@
1
+ import { ContentType } from '@neondatabase/api-client';
2
+ const functionsPath = (projectId, branchId) => `/projects/${encodeURIComponent(projectId)}/branches/${encodeURIComponent(branchId)}/functions`;
3
+ export const listFunctions = async (apiClient, projectId, branchId) => {
4
+ const { data } = await apiClient.request({
5
+ path: functionsPath(projectId, branchId),
6
+ method: 'GET',
7
+ secure: true,
8
+ format: 'json',
9
+ });
10
+ return data.functions;
11
+ };
12
+ export const getFunction = async (apiClient, projectId, branchId, slug) => {
13
+ const { data } = await apiClient.request({
14
+ path: `${functionsPath(projectId, branchId)}/${encodeURIComponent(slug)}`,
15
+ method: 'GET',
16
+ secure: true,
17
+ format: 'json',
18
+ });
19
+ return data.function;
20
+ };
21
+ export const deleteFunction = async (apiClient, projectId, branchId, slug) => {
22
+ await apiClient.request({
23
+ path: `${functionsPath(projectId, branchId)}/${encodeURIComponent(slug)}`,
24
+ method: 'DELETE',
25
+ secure: true,
26
+ });
27
+ };
28
+ export const createDeployment = async (apiClient, projectId, branchId, slug, params) => {
29
+ const form = new FormData();
30
+ form.append('zip', new Blob([params.zip]), 'bundle.zip');
31
+ form.append('memory_mib', String(params.memoryMib));
32
+ form.append('runtime', params.runtime);
33
+ if (params.environment)
34
+ form.append('environment', params.environment);
35
+ // The deploy POST returns an operation the CLI cannot poll; the body is
36
+ // ignored. We only need the request to succeed.
37
+ await apiClient.request({
38
+ path: `${functionsPath(projectId, branchId)}/${encodeURIComponent(slug)}/deployments`,
39
+ method: 'POST',
40
+ type: ContentType.FormData,
41
+ body: form,
42
+ secure: true,
43
+ });
44
+ };
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,68 +5,75 @@
5
5
  "url": "git+ssh://git@github.com/neondatabase/neonctl.git"
6
6
  },
7
7
  "type": "module",
8
- "version": "2.22.0",
8
+ "version": "2.23.0",
9
9
  "description": "CLI tool for NeonDB Cloud management",
10
10
  "main": "index.js",
11
11
  "author": "NeonDB",
12
- "license": "MIT",
12
+ "license": "Apache-2.0",
13
13
  "private": false,
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
- "@yao-pkg/pkg": "^6.10.0",
33
- "@types/cli-table": "^0.3.0",
34
- "@types/diff": "^5.2.1",
35
- "@types/eslint__js": "^8.42.3",
36
- "@types/express": "^4.17.17",
37
- "@types/node": "^18.7.13",
23
+ "@apidevtools/swagger-parser": "12.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
+ "@testcontainers/postgresql": "^11.14.0",
31
+ "@types/cli-table": "0.3.4",
32
+ "@types/diff": "5.2.1",
33
+ "@types/eslint__js": "8.42.3",
34
+ "@types/express": "4.17.21",
35
+ "@types/node": "18.19.41",
38
36
  "@types/prompts": "2.4.9",
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",
37
+ "@types/which": "3.0.4",
38
+ "@types/yargs": "17.0.32",
39
+ "@yao-pkg/pkg": "6.10.0",
40
+ "emocks": "3.0.4",
41
+ "esbuild": "0.28.0",
42
+ "eslint": "9.29.0",
43
+ "express": "4.19.2",
44
+ "husky": "8.0.3",
45
+ "lint-staged": "13.3.0",
46
+ "node-pty": "1.1.0",
47
+ "oauth2-mock-server": "8.1.0",
48
+ "prettier": "3.3.3",
49
+ "rollup": "3.29.4",
50
+ "strip-ansi": "7.1.0",
51
+ "tsx": "4.22.3",
52
+ "typescript": "4.9.5",
52
53
  "typescript-eslint": "8.28.0",
53
- "vitest": "^1.6.0"
54
+ "vitest": "1.6.1"
55
+ },
56
+ "optionalDependencies": {
57
+ "esbuild": "0.28.0"
54
58
  },
55
59
  "dependencies": {
56
- "@neondatabase/api-client": "2.6.0",
57
- "@segment/analytics-node": "^1.0.0-beta.26",
58
- "axios": "^1.4.0",
59
- "axios-debug-log": "^1.0.0",
60
- "chalk": "^5.2.0",
61
- "cli-table": "^0.3.11",
62
- "diff": "^5.2.0",
63
- "neon-init": "^0.14.0",
64
- "open": "^10.1.0",
65
- "openid-client": "^6.8.1",
60
+ "@neondatabase/api-client": "2.7.1",
61
+ "@segment/analytics-node": "1.3.0",
62
+ "axios": "1.7.2",
63
+ "axios-debug-log": "1.0.0",
64
+ "chalk": "5.3.0",
65
+ "cli-table": "0.3.11",
66
+ "cliui": "8.0.1",
67
+ "diff": "5.2.0",
68
+ "fflate": "^0.8.3",
69
+ "neon-init": "0.14.0",
70
+ "open": "10.1.0",
71
+ "openid-client": "6.8.1",
72
+ "pg-protocol": "^1.14.0",
66
73
  "prompts": "2.4.2",
67
- "which": "^3.0.1",
68
- "yaml": "^2.1.1",
69
- "yargs": "^17.7.2"
74
+ "which": "3.0.1",
75
+ "yaml": "2.4.5",
76
+ "yargs": "17.7.2"
70
77
  },
71
78
  "publishConfig": {
72
79
  "access": "public",
@@ -90,14 +97,16 @@
90
97
  "scripts": {
91
98
  "watch": "tsc --watch",
92
99
  "typecheck": "tsc --noEmit",
93
- "lint": "npm run typecheck && eslint src && prettier --check .",
94
- "lint:fix": "npm run typecheck && eslint src --fix && prettier --w .",
95
- "build": "bun generateParams && bun clean && tsc && cp src/*.html package*.json README.md ./dist",
100
+ "lint": "pnpm typecheck && eslint src && prettier --check .",
101
+ "lint:fix": "pnpm typecheck && eslint src --fix && prettier --w .",
102
+ "build": "pnpm generateParams && pnpm clean && tsc -p tsconfig.build.json && cp src/*.html package*.json README.md ./dist",
96
103
  "clean": "rm -rf dist",
97
- "generateParams": "bun generateOptionsFromSpec.ts",
98
- "start": "bun dist/index.js",
99
- "pretest": "bun run build",
104
+ "generateParams": "tsx generateOptionsFromSpec.ts",
105
+ "start": "node dist/index.js",
106
+ "pretest": "pnpm build",
100
107
  "test": "vitest run",
108
+ "test:conformance": "vitest run --config tests/psql-conformance/vitest.config.ts",
109
+ "test:conformance:matrix": "tsx tests/psql-conformance/scripts/run-local-matrix.ts",
101
110
  "prepare": "test -d .git && husky install || true"
102
111
  },
103
112
  "lint-staged": {
package/psql/cli.js ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone psql shim.
4
+ *
5
+ * Thin wrapper around `runPsql` that lets the conformance harness — and any
6
+ * caller that wants a psql-compatible CLI — invoke our embedded TypeScript
7
+ * psql directly via `node dist/psql/cli.js`, without going through the
8
+ * neonctl yargs entrypoint.
9
+ *
10
+ * Why this exists: `dist/cli.js` is the neonctl entrypoint; yargs eats
11
+ * libpq-style flags (e.g. `-v ON_ERROR_STOP=0` collides with yargs'
12
+ * built-in `--version`). The conformance suite needs to spawn a binary
13
+ * that accepts the upstream psql flag grammar (`-h`/`-p`/`-U`/`-d`/`-X`/
14
+ * `-v VAR=VALUE`/`--no-psqlrc`/`--echo-all`/`--quiet`/etc.) and reads SQL
15
+ * from stdin. This shim wires `process.argv.slice(2)` straight into
16
+ * `runPsql`, which already speaks that grammar via `parseStartupArgs`.
17
+ *
18
+ * Argv shape:
19
+ * - If the first positional looks like a connection URI / conninfo
20
+ * (per `looksLikeConnectionString`), it stays at argv[0] — same shape
21
+ * as the legacy native-psql call site in `src/utils/psql.ts` and the
22
+ * existing `runPsql` API.
23
+ * - Otherwise (the regress harness case: bare libpq flags), we prepend
24
+ * an empty placeholder URI so `runPsql([''] + flags)` falls through to
25
+ * `parseStartupArgs` and resolves the connection from `-h`/`-p`/`-U`/
26
+ * `-d` plus PG* env / pgpass / service via the layered resolver.
27
+ *
28
+ * Exit code: forwarded straight from `runPsql` (matches upstream psql's
29
+ * EXIT_SUCCESS / EXIT_FAILURE / EXIT_USER / EXIT_BADCONN values; see
30
+ * `core/mainloop.ts`).
31
+ */
32
+ import { runPsql, looksLikeConnectionString } from './index.js';
33
+ const main = async () => {
34
+ const raw = process.argv.slice(2);
35
+ // Detect whether the first arg is a connection URI/conninfo string.
36
+ // libpq's `recognized_connection_string()` covers both `postgres[ql]://…`
37
+ // URIs and bare `key=value` conninfo strings — `looksLikeConnectionString`
38
+ // mirrors that test.
39
+ const argv = raw.length > 0 && looksLikeConnectionString(raw[0]) ? raw : ['', ...raw];
40
+ const code = await runPsql(argv, {
41
+ stdin: process.stdin,
42
+ stdout: process.stdout,
43
+ stderr: process.stderr,
44
+ });
45
+ process.exit(code);
46
+ };
47
+ main().catch((err) => {
48
+ const msg = err instanceof Error ? err.message : String(err);
49
+ process.stderr.write(`psql: fatal: ${msg}\n`);
50
+ process.exit(1);
51
+ });