neonctl 2.23.1 → 2.24.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/env_file.js ADDED
@@ -0,0 +1,146 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * Default dotenv file `env pull` writes to: `.env` when one already exists in the working
5
+ * directory (update where secrets already live), otherwise `.env.local` — matching the
6
+ * `vercel env pull` convention. An explicit `--file` always wins over this.
7
+ */
8
+ export const resolveEnvFilePath = (cwd, file) => {
9
+ if (file)
10
+ return join(cwd, file);
11
+ if (existsSync(join(cwd, '.env')))
12
+ return join(cwd, '.env');
13
+ return join(cwd, '.env.local');
14
+ };
15
+ /**
16
+ * Merge `updates` into the dotenv content at `path`, preserving every other line
17
+ * (comments, blank lines, unrelated keys) and the file's existing order. Keys present in
18
+ * both are updated in place; keys only in `updates` are appended. A non-existent file is
19
+ * treated as empty. Returns the list of keys that were written (for reporting).
20
+ */
21
+ export const mergeEnvFile = (path, updates) => {
22
+ const original = existsSync(path) ? readFileSync(path, 'utf8') : '';
23
+ const { content, written } = mergeEnvContent(original, updates);
24
+ writeFileSync(path, content);
25
+ return { written };
26
+ };
27
+ /**
28
+ * Pure core of {@link mergeEnvFile}: takes the current file content and the updates, and
29
+ * returns the new content plus which keys were written. Kept side-effect-free so it can be
30
+ * unit-tested without touching the filesystem.
31
+ */
32
+ export const mergeEnvContent = (original, updates) => {
33
+ const keys = Object.keys(updates);
34
+ if (keys.length === 0)
35
+ return { content: original, written: [] };
36
+ const remaining = new Set(keys);
37
+ const lines = original === '' ? [] : original.split('\n');
38
+ // Update keys in place where they already appear, so their position and any surrounding
39
+ // comments are preserved.
40
+ const updatedLines = lines.map((line) => {
41
+ const key = parseKey(line);
42
+ if (key !== null && remaining.has(key)) {
43
+ remaining.delete(key);
44
+ return formatLine(key, updates[key]);
45
+ }
46
+ return line;
47
+ });
48
+ // Append keys that weren't already present, in the order they were given.
49
+ const appended = keys
50
+ .filter((key) => remaining.has(key))
51
+ .map((key) => formatLine(key, updates[key]));
52
+ const body = trimTrailingBlank(updatedLines);
53
+ const content = [...body, ...appended].join('\n');
54
+ return {
55
+ // A dotenv file ends with a trailing newline.
56
+ content: content === '' ? '' : `${content}\n`,
57
+ written: keys,
58
+ };
59
+ };
60
+ /**
61
+ * Read a dotenv file at `path` into a plain `{ KEY: value }` map. A non-existent file is an
62
+ * error (callers pass an explicit `--env` path, so a typo should fail loudly rather than
63
+ * silently load nothing). Quotes are stripped and `\"` / `\\` unescaped, matching the
64
+ * quoting {@link mergeEnvFile} writes. Comments, blank lines, and non-assignment lines are
65
+ * ignored.
66
+ */
67
+ export const readEnvFile = (path) => {
68
+ if (!existsSync(path)) {
69
+ throw new Error(`Env file not found: ${path}`);
70
+ }
71
+ const out = {};
72
+ for (const line of readFileSync(path, 'utf8').split('\n')) {
73
+ const parsed = parseAssignment(line);
74
+ if (parsed)
75
+ out[parsed.key] = parsed.value;
76
+ }
77
+ return out;
78
+ };
79
+ /**
80
+ * Load a dotenv file into `process.env` so values are available to anything read later
81
+ * (e.g. a `neon.ts` whose function `env` values come from `process.env.X`). Existing
82
+ * `process.env` entries are **not** overridden — an already-exported var wins over the
83
+ * file, matching dotenv's default. Returns the keys that were applied.
84
+ */
85
+ export const loadEnvFileIntoProcess = (path) => {
86
+ const parsed = readEnvFile(path);
87
+ const applied = [];
88
+ for (const [key, value] of Object.entries(parsed)) {
89
+ if (process.env[key] === undefined) {
90
+ process.env[key] = value;
91
+ applied.push(key);
92
+ }
93
+ }
94
+ return applied;
95
+ };
96
+ /**
97
+ * Extract the variable name from a dotenv line, or `null` for comments / blank lines / any
98
+ * line that isn't a `KEY=value` assignment. Tolerates a leading `export ` and surrounding
99
+ * whitespace, matching common `.env` styles.
100
+ */
101
+ const parseKey = (line) => {
102
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line);
103
+ return match ? match[1] : null;
104
+ };
105
+ /**
106
+ * Parse a `KEY=value` dotenv line into its key and unquoted value, or `null` for
107
+ * comments / blank lines / non-assignments. Mirrors {@link formatLine}'s quoting.
108
+ */
109
+ const parseAssignment = (line) => {
110
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
111
+ const key = match?.[1];
112
+ const raw = match?.[2];
113
+ if (key === undefined || raw === undefined)
114
+ return null;
115
+ return { key, value: unquote(raw.trim()) };
116
+ };
117
+ /** Strip matching surrounding quotes and unescape `\"` / `\\` inside double quotes. */
118
+ const unquote = (value) => {
119
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
120
+ return value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
121
+ }
122
+ if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
123
+ return value.slice(1, -1);
124
+ }
125
+ return value;
126
+ };
127
+ /**
128
+ * Render a `KEY=value` line. The value is wrapped in double quotes when it contains
129
+ * characters that would otherwise break parsing (spaces, `#`, quotes, `=`), with inner
130
+ * quotes/backslashes escaped — Neon connection strings and URLs are safe either way, but
131
+ * quoting defensively avoids surprises for tools that re-parse the file.
132
+ */
133
+ const formatLine = (key, value) => {
134
+ const needsQuotes = /[\s#"'=]/.test(value);
135
+ if (!needsQuotes)
136
+ return `${key}=${value}`;
137
+ const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
138
+ return `${key}="${escaped}"`;
139
+ };
140
+ /** Drop trailing blank lines so we don't accumulate them across repeated merges. */
141
+ const trimTrailingBlank = (lines) => {
142
+ const out = [...lines];
143
+ while (out.length > 0 && out[out.length - 1]?.trim() === '')
144
+ out.pop();
145
+ return out;
146
+ };
package/index.js CHANGED
@@ -38,6 +38,8 @@ const NO_SUBCOMMANDS_VERBS = [
38
38
  'checkout',
39
39
  'link',
40
40
  'init',
41
+ 'dev',
42
+ 'deploy',
41
43
  // aliases
42
44
  ];
43
45
  let builder = yargs(hideBin(process.argv));
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.23.1",
8
+ "version": "2.24.0",
9
9
  "description": "CLI tool for NeonDB Cloud management",
10
10
  "main": "index.js",
11
11
  "author": "NeonDB",
@@ -49,7 +49,7 @@
49
49
  "rollup": "3.29.4",
50
50
  "strip-ansi": "7.1.0",
51
51
  "tsx": "4.22.3",
52
- "typescript": "4.9.5",
52
+ "typescript": "5.8.3",
53
53
  "typescript-eslint": "8.28.0",
54
54
  "vitest": "1.6.1"
55
55
  },
@@ -57,11 +57,16 @@
57
57
  "esbuild": "0.28.0"
58
58
  },
59
59
  "dependencies": {
60
+ "@hono/node-server": "2.0.4",
60
61
  "@neondatabase/api-client": "2.7.1",
62
+ "@neondatabase/config": "0.4.0",
63
+ "@neondatabase/config-runtime": "0.4.0",
64
+ "@neondatabase/env": "0.3.0",
61
65
  "@segment/analytics-node": "1.3.0",
62
66
  "axios": "1.7.2",
63
67
  "axios-debug-log": "1.0.0",
64
68
  "chalk": "5.3.0",
69
+ "chokidar": "5.0.0",
65
70
  "cli-table": "0.3.11",
66
71
  "cliui": "8.0.1",
67
72
  "diff": "5.2.0",
@@ -233,18 +233,18 @@ const NORMALISED_ENCODINGS = new Set([
233
233
  'johab',
234
234
  'shiftjis2004',
235
235
  // Aliases upstream's encoding_match_list accepts as recognised names.
236
- 'unicode',
237
- 'mskanji',
238
- 'shiftjis',
239
- 'windows949',
240
- 'windows950',
241
- 'windows936',
242
- 'tcvn',
243
- 'tcvn5712',
244
- 'vscii',
245
- 'alt',
246
- 'win',
247
- 'koi8',
236
+ 'unicode', // → UTF8
237
+ 'mskanji', // → SJIS
238
+ 'shiftjis', // → SJIS
239
+ 'windows949', // → UHC
240
+ 'windows950', // → BIG5
241
+ 'windows936', // → GBK
242
+ 'tcvn', // → WIN1258
243
+ 'tcvn5712', // → WIN1258
244
+ 'vscii', // → WIN1258
245
+ 'alt', // → WIN866
246
+ 'win', // → WIN1251
247
+ 'koi8', // → KOI8R
248
248
  'abc', // → WIN1258
249
249
  ]);
250
250
  /**
@@ -1514,7 +1514,7 @@ const GDESC_SYNTHETIC_FIELDS = [
1514
1514
  name: 'Column',
1515
1515
  tableID: 0,
1516
1516
  columnID: 0,
1517
- dataTypeID: 25,
1517
+ dataTypeID: 25, // text
1518
1518
  dataTypeSize: -1,
1519
1519
  dataTypeModifier: -1,
1520
1520
  format: 0,
@@ -1523,7 +1523,7 @@ const GDESC_SYNTHETIC_FIELDS = [
1523
1523
  name: 'Type',
1524
1524
  tableID: 0,
1525
1525
  columnID: 0,
1526
- dataTypeID: 25,
1526
+ dataTypeID: 25, // text
1527
1527
  dataTypeSize: -1,
1528
1528
  dataTypeModifier: -1,
1529
1529
  format: 0,
@@ -48,18 +48,18 @@ export const RESTRICTED_REFUSAL_MESSAGE = (cmdName) => `\\${cmdName}: command is
48
48
  * registry level" and avoids touching cmd_copy.ts internals.
49
49
  */
50
50
  export const RESTRICTED_COMMANDS = new Set([
51
- '!',
52
- 'cd',
53
- 'copy',
54
- 'setenv',
55
- 'w',
51
+ '!', // shell escape
52
+ 'cd', // change directory
53
+ 'copy', // \copy — including \copy FROM PROGRAM
54
+ 'setenv', // mutate process env
55
+ 'w', // \w / \write — file write of query buffer
56
56
  // `\o`/`\g`/`\gx` route through openWriter(), which spawns `sh -c <cmd>`
57
57
  // for a `|command` target (arbitrary shell exec) and writes the filesystem
58
58
  // for a FILE target; `\s FILE` writes history to disk. Block them by name
59
59
  // (review item #13). Plain query execution still works via `;`.
60
- 'o',
61
- 'g',
62
- 'gx',
60
+ 'o', // \o [FILE | |cmd]
61
+ 'g', // \g [FILE | |cmd]
62
+ 'gx', // \gx [FILE | |cmd]
63
63
  's', // \s [FILE] — write command history
64
64
  ]);
65
65
  /**
@@ -1270,7 +1270,7 @@ const fakeField = (name) => ({
1270
1270
  name,
1271
1271
  tableID: 0,
1272
1272
  columnID: 0,
1273
- dataTypeID: 25,
1273
+ dataTypeID: 25, // text
1274
1274
  dataTypeSize: -1,
1275
1275
  dataTypeModifier: -1,
1276
1276
  format: 0,
@@ -16,28 +16,28 @@ import { formatNumericLocale } from './units.js';
16
16
  // East-Asian wide / fullwidth ranges from Unicode Standard Annex #11.
17
17
  // Trimmed to the ranges psql's `ucs_wcwidth` considers width 2.
18
18
  const WIDE_RANGES = [
19
- [0x1100, 0x115f],
20
- [0x2329, 0x232a],
21
- [0x2e80, 0x303e],
22
- [0x3041, 0x33ff],
23
- [0x3400, 0x4dbf],
24
- [0x4e00, 0x9fff],
25
- [0xa000, 0xa4cf],
26
- [0xac00, 0xd7a3],
27
- [0xf900, 0xfaff],
28
- [0xfe10, 0xfe19],
29
- [0xfe30, 0xfe6f],
30
- [0xff00, 0xff60],
31
- [0xffe0, 0xffe6],
32
- [0x1f300, 0x1f64f],
33
- [0x1f900, 0x1f9ff],
34
- [0x20000, 0x2fffd],
19
+ [0x1100, 0x115f], // Hangul Jamo
20
+ [0x2329, 0x232a], // Angle brackets
21
+ [0x2e80, 0x303e], // CJK Radicals, Kangxi, ...
22
+ [0x3041, 0x33ff], // Hiragana, Katakana, Bopomofo, etc.
23
+ [0x3400, 0x4dbf], // CJK Unified Ideographs Extension A
24
+ [0x4e00, 0x9fff], // CJK Unified Ideographs
25
+ [0xa000, 0xa4cf], // Yi Syllables
26
+ [0xac00, 0xd7a3], // Hangul Syllables
27
+ [0xf900, 0xfaff], // CJK Compatibility Ideographs
28
+ [0xfe10, 0xfe19], // Vertical forms
29
+ [0xfe30, 0xfe6f], // CJK Compatibility Forms, Small Form Variants
30
+ [0xff00, 0xff60], // Fullwidth Forms
31
+ [0xffe0, 0xffe6], // Fullwidth signs
32
+ [0x1f300, 0x1f64f], // Misc Symbols & Pictographs, Emoticons
33
+ [0x1f900, 0x1f9ff], // Supplemental Symbols & Pictographs
34
+ [0x20000, 0x2fffd], // CJK Extension B..F
35
35
  [0x30000, 0x3fffd], // CJK Extension G
36
36
  ];
37
37
  // Combining marks: width 0. From Unicode general categories Mn/Me/Cf
38
38
  // and the zero-width controls upstream treats as width 0.
39
39
  const ZERO_RANGES = [
40
- [0x0300, 0x036f],
40
+ [0x0300, 0x036f], // Combining Diacritical Marks
41
41
  [0x0483, 0x0489],
42
42
  [0x0591, 0x05bd],
43
43
  [0x05bf, 0x05bf],
@@ -190,7 +190,7 @@ const ZERO_RANGES = [
190
190
  [0x1cf8, 0x1cf9],
191
191
  [0x1dc0, 0x1df9],
192
192
  [0x1dfb, 0x1dff],
193
- [0x200b, 0x200f],
193
+ [0x200b, 0x200f], // zero-width space, ZWNJ, ZWJ, LRM, RLM
194
194
  [0x202a, 0x202e],
195
195
  [0x2060, 0x206f],
196
196
  [0x20d0, 0x20f0],
@@ -233,9 +233,9 @@ const ZERO_RANGES = [
233
233
  [0xabe8, 0xabe8],
234
234
  [0xabed, 0xabed],
235
235
  [0xfb1e, 0xfb1e],
236
- [0xfe00, 0xfe0f],
237
- [0xfe20, 0xfe2f],
238
- [0xfeff, 0xfeff],
236
+ [0xfe00, 0xfe0f], // variation selectors
237
+ [0xfe20, 0xfe2f], // combining half-marks
238
+ [0xfeff, 0xfeff], // BOM
239
239
  [0xfff9, 0xfffb],
240
240
  [0x101fd, 0x101fd],
241
241
  [0x102e0, 0x102e0],
@@ -342,15 +342,15 @@ export const padToWidth = (text, width, alignment) => {
342
342
  // divergences — we previously right-aligned interval & pg_lsn and omitted
343
343
  // xid & cid).
344
344
  const RIGHT_ALIGNED_OIDS = new Set([
345
- 20,
346
- 21,
347
- 23,
348
- 26,
349
- 28,
350
- 29,
351
- 700,
352
- 701,
353
- 790,
345
+ 20, // int8
346
+ 21, // int2
347
+ 23, // int4
348
+ 26, // oid
349
+ 28, // xid
350
+ 29, // cid
351
+ 700, // float4
352
+ 701, // float8
353
+ 790, // money (cash)
354
354
  1700, // numeric
355
355
  ]);
356
356
  const isRightAlignedField = (oid) => RIGHT_ALIGNED_OIDS.has(oid);
@@ -22,11 +22,11 @@
22
22
  // PostgreSQL type OIDs for the numeric family that map cleanly to
23
23
  // JSON numbers. NUMERIC is included but guarded by isFinite().
24
24
  const NUMERIC_TYPE_OIDS = new Set([
25
- 21,
26
- 23,
27
- 20,
28
- 700,
29
- 701,
25
+ 21, // INT2
26
+ 23, // INT4
27
+ 20, // INT8
28
+ 700, // FLOAT4
29
+ 701, // FLOAT8
30
30
  1700, // NUMERIC
31
31
  ]);
32
32
  export const jsonPrinter = {
@@ -142,10 +142,10 @@ const cloneState = (s) => ({
142
142
  // blocks, dollar-quoted bodies). Identifier letters are stored lower-cased.
143
143
  // ---------------------------------------------------------------------------
144
144
  const KEYWORD_PREFIX_LETTERS = new Set([
145
- 'c',
146
- 'f',
147
- 'p',
148
- 'o',
145
+ 'c', // create
146
+ 'f', // function
147
+ 'p', // procedure
148
+ 'o', // or
149
149
  'r', // replace
150
150
  ]);
151
151
  const PREFIX_MATCHES_CREATE_FN_OR_PROC = (letters) => {