neonctl 2.23.0 → 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/README.md +60 -1
- package/commands/auth.js +12 -0
- package/commands/checkout.js +75 -30
- package/commands/config.js +249 -0
- package/commands/deploy.js +25 -0
- package/commands/dev.js +709 -0
- package/commands/env.js +67 -0
- package/commands/functions.js +3 -12
- package/commands/index.js +8 -0
- package/commands/link.js +4 -2
- package/config_format.js +66 -0
- package/dev/env.js +183 -0
- package/dev/functions.js +72 -0
- package/dev/inputs.js +63 -0
- package/dev/runtime.js +146 -0
- package/env_file.js +146 -0
- package/functions_api.js +0 -1
- package/index.js +2 -0
- package/package.json +7 -2
- package/psql/command/cmd_format.js +12 -12
- package/psql/command/cmd_io.js +2 -2
- package/psql/command/cmd_restrict.js +8 -8
- package/psql/describe/formatters.js +1 -1
- package/psql/print/aligned.js +30 -30
- package/psql/print/json.js +5 -5
- package/psql/scanner/sql.js +4 -4
package/dev/runtime.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { getRequestListener } from '@hono/node-server';
|
|
5
|
+
const isFunction = (value) => typeof value === 'function';
|
|
6
|
+
const hasFetchMethod = (value) => typeof value === 'object' &&
|
|
7
|
+
value !== null &&
|
|
8
|
+
'fetch' in value &&
|
|
9
|
+
typeof value.fetch === 'function';
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the user's exported handler to a single fetch callback.
|
|
12
|
+
*
|
|
13
|
+
* Resolution order (first match wins):
|
|
14
|
+
* 1. `export default { fetch }` — Workers / Neon Functions style
|
|
15
|
+
* 2. `export default function (req)` — bare (async) default function
|
|
16
|
+
*/
|
|
17
|
+
export const resolveFetchHandler = (mod) => {
|
|
18
|
+
const defaultExport = mod.default;
|
|
19
|
+
if (hasFetchMethod(defaultExport)) {
|
|
20
|
+
const target = defaultExport;
|
|
21
|
+
return (req) => target.fetch(req);
|
|
22
|
+
}
|
|
23
|
+
if (isFunction(defaultExport)) {
|
|
24
|
+
return defaultExport;
|
|
25
|
+
}
|
|
26
|
+
throw new Error('No request handler found in the source module. Export one of:\n' +
|
|
27
|
+
' export default { fetch(req) { /* ... */ } }\n' +
|
|
28
|
+
' export default function (req) { /* ... */ }');
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Wrap a fetch handler so user errors become a 500 response (with the message
|
|
32
|
+
* in the body during dev) instead of crashing the child process.
|
|
33
|
+
*/
|
|
34
|
+
export const withErrorBoundary = (handler) => {
|
|
35
|
+
return async (req) => {
|
|
36
|
+
try {
|
|
37
|
+
return await handler(req);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
const message = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
41
|
+
process.stderr.write(`Request handler threw an error:\n${message}\n`);
|
|
42
|
+
return new Response(`Internal Server Error\n\n${message}`, {
|
|
43
|
+
status: 500,
|
|
44
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
const isAddressInUse = (err) => typeof err === 'object' &&
|
|
50
|
+
err !== null &&
|
|
51
|
+
err.code === 'EADDRINUSE';
|
|
52
|
+
const DEFAULT_SEARCH_BASE = 8787;
|
|
53
|
+
const MAX_SEARCH_STEPS = 100;
|
|
54
|
+
const bindPort = async (server, selection, hostname) => {
|
|
55
|
+
if (selection.mode === 'explicit') {
|
|
56
|
+
return listen(server, selection.port, hostname);
|
|
57
|
+
}
|
|
58
|
+
for (let step = 0; step < MAX_SEARCH_STEPS; step++) {
|
|
59
|
+
try {
|
|
60
|
+
return await listen(server, selection.from + step, hostname);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
if (!isAddressInUse(err))
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`Could not find a free port in ${selection.from}-${selection.from + MAX_SEARCH_STEPS - 1}`);
|
|
68
|
+
};
|
|
69
|
+
const listen = (server, port, hostname) => new Promise((resolveListen, rejectListen) => {
|
|
70
|
+
const onError = (err) => {
|
|
71
|
+
server.off('listening', onListening);
|
|
72
|
+
rejectListen(err);
|
|
73
|
+
};
|
|
74
|
+
const onListening = () => {
|
|
75
|
+
server.off('error', onError);
|
|
76
|
+
resolveListen(server.address().port);
|
|
77
|
+
};
|
|
78
|
+
server.once('error', onError);
|
|
79
|
+
server.once('listening', onListening);
|
|
80
|
+
server.listen(port, hostname);
|
|
81
|
+
});
|
|
82
|
+
/**
|
|
83
|
+
* Load the (already-bundled) user module, build the listener, and start an HTTP
|
|
84
|
+
* server. Announces the bound port on stdout as `neon-dev:ready <port>` so the
|
|
85
|
+
* parent can render the URL. Resolves with the bound port.
|
|
86
|
+
*/
|
|
87
|
+
export const startRuntime = async ({ source, port, hostname, }) => {
|
|
88
|
+
const absoluteSource = resolve(process.cwd(), source);
|
|
89
|
+
const mod = (await import(pathToFileURL(absoluteSource).href));
|
|
90
|
+
const handler = withErrorBoundary(resolveFetchHandler(mod));
|
|
91
|
+
const listener = getRequestListener(handler, { hostname });
|
|
92
|
+
const server = createServer((incoming, outgoing) => {
|
|
93
|
+
void listener(incoming, outgoing);
|
|
94
|
+
});
|
|
95
|
+
const boundPort = await bindPort(server, port, hostname);
|
|
96
|
+
process.stdout.write(`neon-dev:ready ${boundPort}\n`);
|
|
97
|
+
return boundPort;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Build a {@link PortSelection} from the environment. Precedence:
|
|
101
|
+
* 1. `NEON_DEV_PORT` -> explicit bind (crash if taken). Set by `neon dev` from an
|
|
102
|
+
* explicit `--port` / `dev.port`.
|
|
103
|
+
* 2. `PORT` -> explicit bind. This is what `portless` injects (and what a bare
|
|
104
|
+
* `PORT=3000 neon dev` sets), so the runtime binds the port chosen for it.
|
|
105
|
+
* 3. otherwise -> search upward from `NEON_DEV_PORT_BASE` (or the default base).
|
|
106
|
+
*/
|
|
107
|
+
export const portSelectionFromEnv = (env) => {
|
|
108
|
+
const explicit = env.NEON_DEV_PORT;
|
|
109
|
+
if (explicit !== undefined && explicit !== '') {
|
|
110
|
+
return { mode: 'explicit', port: parsePort(explicit, 'NEON_DEV_PORT') };
|
|
111
|
+
}
|
|
112
|
+
const injected = env.PORT;
|
|
113
|
+
if (injected !== undefined && injected !== '') {
|
|
114
|
+
return { mode: 'explicit', port: parsePort(injected, 'PORT') };
|
|
115
|
+
}
|
|
116
|
+
const base = Number(env.NEON_DEV_PORT_BASE ?? DEFAULT_SEARCH_BASE);
|
|
117
|
+
return {
|
|
118
|
+
mode: 'search',
|
|
119
|
+
from: Number.isInteger(base) ? base : DEFAULT_SEARCH_BASE,
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
const parsePort = (value, varName) => {
|
|
123
|
+
const port = Number(value);
|
|
124
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
125
|
+
throw new Error(`Invalid ${varName}: "${value}"`);
|
|
126
|
+
}
|
|
127
|
+
return port;
|
|
128
|
+
};
|
|
129
|
+
const isDirectExecution = () => {
|
|
130
|
+
const entry = process.argv[1];
|
|
131
|
+
if (!entry)
|
|
132
|
+
return false;
|
|
133
|
+
return import.meta.url === pathToFileURL(entry).href;
|
|
134
|
+
};
|
|
135
|
+
if (isDirectExecution()) {
|
|
136
|
+
const source = process.env.NEON_DEV_SOURCE ?? process.argv[2];
|
|
137
|
+
if (!source) {
|
|
138
|
+
process.stderr.write('neon-dev runtime: missing source path\n');
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
startRuntime({ source, port: portSelectionFromEnv(process.env) }).catch((err) => {
|
|
142
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
143
|
+
process.stderr.write(`neon-dev runtime failed to start: ${msg}\n`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
});
|
|
146
|
+
}
|
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/functions_api.js
CHANGED
|
@@ -28,7 +28,6 @@ export const deleteFunction = async (apiClient, projectId, branchId, slug) => {
|
|
|
28
28
|
export const createDeployment = async (apiClient, projectId, branchId, slug, params) => {
|
|
29
29
|
const form = new FormData();
|
|
30
30
|
form.append('zip', new Blob([params.zip]), 'bundle.zip');
|
|
31
|
-
form.append('memory_mib', String(params.memoryMib));
|
|
32
31
|
form.append('runtime', params.runtime);
|
|
33
32
|
if (params.environment)
|
|
34
33
|
form.append('environment', params.environment);
|
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.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": "
|
|
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
|
/**
|
package/psql/command/cmd_io.js
CHANGED
|
@@ -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
|
/**
|
package/psql/print/aligned.js
CHANGED
|
@@ -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);
|
package/psql/print/json.js
CHANGED
|
@@ -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 = {
|
package/psql/scanner/sql.js
CHANGED
|
@@ -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) => {
|