embedded-postgres-node 0.0.0-stage → 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +113 -2
- package/dist/cjs/binary.d.ts +24 -0
- package/dist/cjs/binary.js +168 -0
- package/dist/cjs/binary.js.map +1 -0
- package/dist/cjs/errors.d.ts +9 -0
- package/dist/cjs/errors.js +35 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/index.d.ts +7 -0
- package/dist/cjs/index.js +13 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/lifecycle.d.ts +21 -0
- package/dist/cjs/lifecycle.js +209 -0
- package/dist/cjs/lifecycle.js.map +1 -0
- package/dist/cjs/options.d.ts +46 -0
- package/dist/cjs/options.js +79 -0
- package/dist/cjs/options.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/protocol.d.ts +22 -0
- package/dist/cjs/protocol.js +70 -0
- package/dist/cjs/protocol.js.map +1 -0
- package/dist/esm/binary.d.ts +24 -0
- package/dist/esm/binary.js +162 -0
- package/dist/esm/binary.js.map +1 -0
- package/dist/esm/errors.d.ts +9 -0
- package/dist/esm/errors.js +29 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/index.d.ts +7 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/lifecycle.d.ts +21 -0
- package/dist/esm/lifecycle.js +205 -0
- package/dist/esm/lifecycle.js.map +1 -0
- package/dist/esm/options.d.ts +46 -0
- package/dist/esm/options.js +76 -0
- package/dist/esm/options.js.map +1 -0
- package/dist/esm/protocol.d.ts +22 -0
- package/dist/esm/protocol.js +66 -0
- package/dist/esm/protocol.js.map +1 -0
- package/docs/api.md +73 -0
- package/docs/binaries.md +50 -0
- package/docs/integration.md +39 -0
- package/docs/releasing.md +41 -0
- package/docs/validation.md +94 -0
- package/examples/jest.config.cjs +1 -0
- package/examples/jest.test.cjs +22 -0
- package/examples/migrations.mjs +17 -0
- package/examples/node-test.mjs +22 -0
- package/examples/options.mjs +6 -0
- package/examples/scoped.mjs +21 -0
- package/examples/vitest.config.mjs +2 -0
- package/examples/vitest.test.mjs +21 -0
- package/package.json +73 -4
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startPostgres = startPostgres;
|
|
4
|
+
exports.withPostgres = withPostgres;
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const node_util_1 = require("node:util");
|
|
7
|
+
const binary_js_1 = require("./binary.js");
|
|
8
|
+
const errors_js_1 = require("./errors.js");
|
|
9
|
+
const options_js_1 = require("./options.js");
|
|
10
|
+
const protocol_js_1 = require("./protocol.js");
|
|
11
|
+
/** Start a database whose lifetime is owned by the caller (or its AbortSignal). */
|
|
12
|
+
async function startPostgres(options = {}) {
|
|
13
|
+
const prepared = (0, options_js_1.prepareOptions)(options);
|
|
14
|
+
const deadline = new AbortController();
|
|
15
|
+
const lifetime = AbortSignal.any([deadline.signal, ...(options.signal ? [options.signal] : [])]);
|
|
16
|
+
const startupTimer = setTimeout(() => deadline.abort(), prepared.startupDeadlineMs);
|
|
17
|
+
let executable;
|
|
18
|
+
try {
|
|
19
|
+
executable = await (0, binary_js_1.resolveCli)(options.cli, lifetime);
|
|
20
|
+
if (lifetime.aborted)
|
|
21
|
+
throw new errors_js_1.PostgresError('ABORTED', 'PostgreSQL startup aborted');
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
clearTimeout(startupTimer);
|
|
25
|
+
if (deadline.signal.aborted)
|
|
26
|
+
throw new errors_js_1.PostgresError('TIMEOUT', 'PostgreSQL startup deadline exceeded during CLI resolution');
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const child = (0, node_child_process_1.spawn)(executable, prepared.args, {
|
|
31
|
+
stdio: ['pipe', 'pipe', 'pipe'], env: prepared.env, windowsHide: true, shell: false,
|
|
32
|
+
// Escape Node's Windows kill-on-parent-exit job so the CLI can observe
|
|
33
|
+
// stdin EOF and clean up. Keep the child referenced and its pipes owned.
|
|
34
|
+
detached: process.platform === 'win32',
|
|
35
|
+
});
|
|
36
|
+
let ready = false;
|
|
37
|
+
let stopped = false;
|
|
38
|
+
let stopping = false;
|
|
39
|
+
let finished = false;
|
|
40
|
+
let stderr = '';
|
|
41
|
+
let discardStderrLine = false;
|
|
42
|
+
let failure;
|
|
43
|
+
let brokenProtocol = false;
|
|
44
|
+
let shutdownTimer;
|
|
45
|
+
let reapTimer;
|
|
46
|
+
const secrets = [prepared.password];
|
|
47
|
+
let resolveClosed;
|
|
48
|
+
const closed = new Promise(resolve => { resolveClosed = resolve; });
|
|
49
|
+
let stopPromise;
|
|
50
|
+
const finish = (exitCode, signal) => {
|
|
51
|
+
if (finished)
|
|
52
|
+
return;
|
|
53
|
+
finished = true;
|
|
54
|
+
clearTimeout(startupTimer);
|
|
55
|
+
clearTimeout(shutdownTimer);
|
|
56
|
+
clearTimeout(reapTimer);
|
|
57
|
+
lifetime.removeEventListener('abort', abort);
|
|
58
|
+
if (!failure && (!ready || !stopped || exitCode !== 0)) {
|
|
59
|
+
failure = new errors_js_1.PostgresError('EXIT', `CLI exited ${ready ? 'after' : 'before'} readiness without clean shutdown (code ${exitCode}, signal ${signal})`);
|
|
60
|
+
}
|
|
61
|
+
const error = failure ? new errors_js_1.PostgresError(failure.code, (0, errors_js_1.redact)(failure.message, secrets), (0, errors_js_1.redact)(stderr, secrets)) : undefined;
|
|
62
|
+
const result = Object.freeze({ exitCode, signal, stopped, ...(error ? { error } : {}) });
|
|
63
|
+
resolveClosed(result);
|
|
64
|
+
if (!ready)
|
|
65
|
+
reject(error ?? new errors_js_1.PostgresError('EXIT', 'CLI exited before readiness'));
|
|
66
|
+
};
|
|
67
|
+
const requestStop = () => {
|
|
68
|
+
if (stopping || finished)
|
|
69
|
+
return;
|
|
70
|
+
stopping = true;
|
|
71
|
+
clearTimeout(startupTimer);
|
|
72
|
+
child.stdin.end();
|
|
73
|
+
shutdownTimer = setTimeout(() => {
|
|
74
|
+
failure = new errors_js_1.PostgresError('SHUTDOWN', `${failure ? failure.message + '; ' : ''}CLI shutdown deadline exceeded; forced owner termination, PostgreSQL cleanup cannot be confirmed`);
|
|
75
|
+
child.kill('SIGKILL');
|
|
76
|
+
// A descendant holding stdio open must not make teardown wait forever.
|
|
77
|
+
reapTimer = setTimeout(() => {
|
|
78
|
+
child.stdin.destroy();
|
|
79
|
+
child.stdout.destroy();
|
|
80
|
+
child.stderr.destroy();
|
|
81
|
+
child.unref();
|
|
82
|
+
finish(child.exitCode, child.signalCode);
|
|
83
|
+
}, 2_000);
|
|
84
|
+
}, prepared.shutdownDeadlineMs);
|
|
85
|
+
};
|
|
86
|
+
const fail = (error) => { failure ??= error; requestStop(); };
|
|
87
|
+
const abort = () => fail(new errors_js_1.PostgresError(deadline.signal.aborted ? 'TIMEOUT' : 'ABORTED', deadline.signal.aborted ? 'PostgreSQL startup deadline exceeded' : 'PostgreSQL lifetime aborted'));
|
|
88
|
+
const stop = () => {
|
|
89
|
+
requestStop();
|
|
90
|
+
stopPromise ??= closed.then(result => { if (result.error)
|
|
91
|
+
throw result.error; });
|
|
92
|
+
return stopPromise;
|
|
93
|
+
};
|
|
94
|
+
const parser = new protocol_js_1.ProtocolParser(event => {
|
|
95
|
+
if (event.event === 'error') {
|
|
96
|
+
fail(new errors_js_1.PostgresError(ready ? 'EXIT' : 'STARTUP', event.error));
|
|
97
|
+
}
|
|
98
|
+
else if (event.event === 'stopped') {
|
|
99
|
+
if (stopped || !ready) {
|
|
100
|
+
fail(new errors_js_1.PostgresError('PROTOCOL', 'Unexpected CLI stopped event'));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
stopped = true;
|
|
104
|
+
if (!stopping)
|
|
105
|
+
fail(new errors_js_1.PostgresError('EXIT', 'CLI stopped unexpectedly'));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
if (ready || stopped) {
|
|
109
|
+
fail(new errors_js_1.PostgresError('PROTOCOL', 'Duplicate or out-of-order CLI ready event'));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
secrets.push(event.connection_url, new URL(event.connection_url).password);
|
|
113
|
+
if (stopping)
|
|
114
|
+
return;
|
|
115
|
+
ready = true;
|
|
116
|
+
clearTimeout(startupTimer);
|
|
117
|
+
const instance = {
|
|
118
|
+
get connectionUrl() { return event.connection_url; },
|
|
119
|
+
port: event.port, closed, stop,
|
|
120
|
+
[Symbol.asyncDispose]: stop,
|
|
121
|
+
};
|
|
122
|
+
// Common inspection/serialization should not accidentally print credentials.
|
|
123
|
+
Object.defineProperties(instance, {
|
|
124
|
+
connectionUrl: { enumerable: false },
|
|
125
|
+
toJSON: { value: () => ({ port: event.port }) },
|
|
126
|
+
[node_util_1.inspect.custom]: { value: () => `PostgresInstance { port: ${event.port} }` },
|
|
127
|
+
});
|
|
128
|
+
resolve(Object.freeze(instance));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
child.stdout.setEncoding('utf8');
|
|
132
|
+
child.stderr.setEncoding('utf8');
|
|
133
|
+
child.stdout.on('data', (chunk) => {
|
|
134
|
+
if (brokenProtocol)
|
|
135
|
+
return;
|
|
136
|
+
try {
|
|
137
|
+
parser.push(chunk);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
brokenProtocol = true;
|
|
141
|
+
fail(error);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
child.stdout.on('end', () => {
|
|
145
|
+
if (brokenProtocol)
|
|
146
|
+
return;
|
|
147
|
+
try {
|
|
148
|
+
parser.finish();
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
brokenProtocol = true;
|
|
152
|
+
fail(error);
|
|
153
|
+
}
|
|
154
|
+
// Protocol EOF while the owner lives must also release our lifetime pipe.
|
|
155
|
+
if (!stopped)
|
|
156
|
+
requestStop();
|
|
157
|
+
});
|
|
158
|
+
child.stderr.on('data', (chunk) => {
|
|
159
|
+
if (discardStderrLine) {
|
|
160
|
+
const newline = chunk.indexOf('\n');
|
|
161
|
+
if (newline === -1)
|
|
162
|
+
return;
|
|
163
|
+
chunk = chunk.slice(newline + 1);
|
|
164
|
+
discardStderrLine = false;
|
|
165
|
+
}
|
|
166
|
+
stderr += chunk;
|
|
167
|
+
if (stderr.length > 64 * 1024) {
|
|
168
|
+
stderr = stderr.slice(-64 * 1024);
|
|
169
|
+
// Discard a truncated line entirely, including its future chunks.
|
|
170
|
+
const newline = stderr.indexOf('\n');
|
|
171
|
+
discardStderrLine = newline === -1;
|
|
172
|
+
stderr = discardStderrLine ? '' : stderr.slice(newline + 1);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
child.stdout.on('error', () => fail(new errors_js_1.PostgresError('PROTOCOL', 'Unable to read CLI protocol pipe')));
|
|
176
|
+
child.stderr.on('error', () => fail(new errors_js_1.PostgresError('EXIT', 'Unable to read CLI diagnostic pipe')));
|
|
177
|
+
child.stdin.on('error', () => fail(new errors_js_1.PostgresError('EXIT', 'CLI stdin pipe closed unexpectedly')));
|
|
178
|
+
child.on('error', () => fail(new errors_js_1.PostgresError('SPAWN', 'Unable to spawn CLI executable')));
|
|
179
|
+
child.on('close', finish);
|
|
180
|
+
lifetime.addEventListener('abort', abort, { once: true });
|
|
181
|
+
if (lifetime.aborted)
|
|
182
|
+
abort();
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/** User-owned migrations/pools belong inside fn; close pools before fn returns. */
|
|
186
|
+
async function withPostgres(options, fn) {
|
|
187
|
+
const postgres = await startPostgres(options);
|
|
188
|
+
let failed = false;
|
|
189
|
+
let callbackError;
|
|
190
|
+
try {
|
|
191
|
+
return await fn(postgres);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
failed = true;
|
|
195
|
+
callbackError = error;
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
try {
|
|
200
|
+
await postgres.stop();
|
|
201
|
+
}
|
|
202
|
+
catch (cleanupError) {
|
|
203
|
+
if (failed)
|
|
204
|
+
throw new AggregateError([callbackError, cleanupError], 'PostgreSQL callback and cleanup both failed');
|
|
205
|
+
throw cleanupError;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=lifecycle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../../.local/build/src/lifecycle.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport { inspect } from 'node:util';\nimport { resolveCli } from './binary.js';\nimport { PostgresError, redact } from './errors.js';\nimport { prepareOptions } from './options.js';\nimport type { PostgresOptions } from './options.js';\nimport { ProtocolParser } from './protocol.js';\n\nexport interface ExitResult {\n readonly exitCode: number | null;\n readonly signal: NodeJS.Signals | null;\n readonly stopped: boolean;\n readonly error?: PostgresError;\n}\n\nexport interface PostgresInstance extends AsyncDisposable {\n /** A credential: do not log or snapshot it. */\n readonly connectionUrl: string;\n readonly port: number;\n /** Resolves on termination; never rejects, so ignoring it cannot cause an unhandled rejection. */\n readonly closed: Promise<ExitResult>;\n /** Closes stdin and waits for stopped and process exit. Repeated calls share one result. */\n stop(): Promise<void>;\n}\n\n/** Start a database whose lifetime is owned by the caller (or its AbortSignal). */\nexport async function startPostgres(options: PostgresOptions = {}): Promise<PostgresInstance> {\n const prepared = prepareOptions(options);\n const deadline = new AbortController();\n const lifetime = AbortSignal.any([deadline.signal, ...(options.signal ? [options.signal] : [])]);\n const startupTimer = setTimeout(() => deadline.abort(), prepared.startupDeadlineMs);\n let executable: string;\n try {\n executable = await resolveCli(options.cli, lifetime);\n if (lifetime.aborted) throw new PostgresError('ABORTED', 'PostgreSQL startup aborted');\n } catch (error) {\n clearTimeout(startupTimer);\n if (deadline.signal.aborted) throw new PostgresError('TIMEOUT', 'PostgreSQL startup deadline exceeded during CLI resolution');\n throw error;\n }\n\n return new Promise<PostgresInstance>((resolve, reject) => {\n const child = spawn(executable, prepared.args, {\n stdio: ['pipe', 'pipe', 'pipe'], env: prepared.env, windowsHide: true, shell: false,\n // Escape Node's Windows kill-on-parent-exit job so the CLI can observe\n // stdin EOF and clean up. Keep the child referenced and its pipes owned.\n detached: process.platform === 'win32',\n });\n let ready = false;\n let stopped = false;\n let stopping = false;\n let finished = false;\n let stderr = '';\n let discardStderrLine = false;\n let failure: PostgresError | undefined;\n let brokenProtocol = false;\n let shutdownTimer: NodeJS.Timeout | undefined;\n let reapTimer: NodeJS.Timeout | undefined;\n const secrets = [prepared.password];\n let resolveClosed!: (result: ExitResult) => void;\n const closed = new Promise<ExitResult>(resolve => { resolveClosed = resolve; });\n let stopPromise: Promise<void> | undefined;\n\n const finish = (exitCode: number | null, signal: NodeJS.Signals | null) => {\n if (finished) return;\n finished = true;\n clearTimeout(startupTimer);\n clearTimeout(shutdownTimer);\n clearTimeout(reapTimer);\n lifetime.removeEventListener('abort', abort);\n if (!failure && (!ready || !stopped || exitCode !== 0)) {\n failure = new PostgresError('EXIT', `CLI exited ${ready ? 'after' : 'before'} readiness without clean shutdown (code ${exitCode}, signal ${signal})`);\n }\n const error = failure ? new PostgresError(failure.code, redact(failure.message, secrets), redact(stderr, secrets)) : undefined;\n const result: ExitResult = Object.freeze({ exitCode, signal, stopped, ...(error ? { error } : {}) });\n resolveClosed(result);\n if (!ready) reject(error ?? new PostgresError('EXIT', 'CLI exited before readiness'));\n };\n const requestStop = () => {\n if (stopping || finished) return;\n stopping = true;\n clearTimeout(startupTimer);\n child.stdin.end();\n shutdownTimer = setTimeout(() => {\n failure = new PostgresError('SHUTDOWN', `${failure ? failure.message + '; ' : ''}CLI shutdown deadline exceeded; forced owner termination, PostgreSQL cleanup cannot be confirmed`);\n child.kill('SIGKILL');\n // A descendant holding stdio open must not make teardown wait forever.\n reapTimer = setTimeout(() => {\n child.stdin.destroy();\n child.stdout.destroy();\n child.stderr.destroy();\n child.unref();\n finish(child.exitCode, child.signalCode);\n }, 2_000);\n }, prepared.shutdownDeadlineMs);\n };\n const fail = (error: PostgresError) => { failure ??= error; requestStop(); };\n const abort = () => fail(new PostgresError(deadline.signal.aborted ? 'TIMEOUT' : 'ABORTED', deadline.signal.aborted ? 'PostgreSQL startup deadline exceeded' : 'PostgreSQL lifetime aborted'));\n const stop = () => {\n requestStop();\n stopPromise ??= closed.then(result => { if (result.error) throw result.error; });\n return stopPromise;\n };\n const parser = new ProtocolParser(event => {\n if (event.event === 'error') {\n fail(new PostgresError(ready ? 'EXIT' : 'STARTUP', event.error));\n } else if (event.event === 'stopped') {\n if (stopped || !ready) { fail(new PostgresError('PROTOCOL', 'Unexpected CLI stopped event')); return; }\n stopped = true;\n if (!stopping) fail(new PostgresError('EXIT', 'CLI stopped unexpectedly'));\n } else {\n if (ready || stopped) { fail(new PostgresError('PROTOCOL', 'Duplicate or out-of-order CLI ready event')); return; }\n secrets.push(event.connection_url, new URL(event.connection_url).password);\n if (stopping) return;\n ready = true;\n clearTimeout(startupTimer);\n const instance: PostgresInstance = {\n get connectionUrl() { return event.connection_url; },\n port: event.port, closed, stop,\n [Symbol.asyncDispose]: stop,\n };\n // Common inspection/serialization should not accidentally print credentials.\n Object.defineProperties(instance, {\n connectionUrl: { enumerable: false },\n toJSON: { value: () => ({ port: event.port }) },\n [inspect.custom]: { value: () => `PostgresInstance { port: ${event.port} }` },\n });\n resolve(Object.freeze(instance));\n }\n });\n child.stdout.setEncoding('utf8');\n child.stderr.setEncoding('utf8');\n child.stdout.on('data', (chunk: string) => {\n if (brokenProtocol) return;\n try { parser.push(chunk); } catch (error) { brokenProtocol = true; fail(error as PostgresError); }\n });\n child.stdout.on('end', () => {\n if (brokenProtocol) return;\n try { parser.finish(); } catch (error) { brokenProtocol = true; fail(error as PostgresError); }\n // Protocol EOF while the owner lives must also release our lifetime pipe.\n if (!stopped) requestStop();\n });\n child.stderr.on('data', (chunk: string) => {\n if (discardStderrLine) {\n const newline = chunk.indexOf('\\n');\n if (newline === -1) return;\n chunk = chunk.slice(newline + 1);\n discardStderrLine = false;\n }\n stderr += chunk;\n if (stderr.length > 64 * 1024) {\n stderr = stderr.slice(-64 * 1024);\n // Discard a truncated line entirely, including its future chunks.\n const newline = stderr.indexOf('\\n');\n discardStderrLine = newline === -1;\n stderr = discardStderrLine ? '' : stderr.slice(newline + 1);\n }\n });\n child.stdout.on('error', () => fail(new PostgresError('PROTOCOL', 'Unable to read CLI protocol pipe')));\n child.stderr.on('error', () => fail(new PostgresError('EXIT', 'Unable to read CLI diagnostic pipe')));\n child.stdin.on('error', () => fail(new PostgresError('EXIT', 'CLI stdin pipe closed unexpectedly')));\n child.on('error', () => fail(new PostgresError('SPAWN', 'Unable to spawn CLI executable')));\n child.on('close', finish);\n lifetime.addEventListener('abort', abort, { once: true });\n if (lifetime.aborted) abort();\n });\n}\n\n/** User-owned migrations/pools belong inside fn; close pools before fn returns. */\nexport async function withPostgres<T>(options: PostgresOptions, fn: (postgres: PostgresInstance) => Promise<T>): Promise<T> {\n const postgres = await startPostgres(options);\n let failed = false;\n let callbackError: unknown;\n try { return await fn(postgres); } catch (error) { failed = true; callbackError = error; throw error; }\n finally {\n try { await postgres.stop(); } catch (cleanupError) {\n if (failed) throw new AggregateError([callbackError, cleanupError], 'PostgreSQL callback and cleanup both failed');\n throw cleanupError;\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { CliSource } from './binary.js';
|
|
2
|
+
export interface PostgresOptions {
|
|
3
|
+
/** Explicit executable/release; otherwise EMBEDDED_POSTGRES_CLI or the package's pinned release. */
|
|
4
|
+
cli?: CliSource;
|
|
5
|
+
postgresVersion?: string;
|
|
6
|
+
database?: string;
|
|
7
|
+
username?: string;
|
|
8
|
+
password?: string;
|
|
9
|
+
/** Zero (the default) lets PostgreSQL select an available port. */
|
|
10
|
+
port?: number;
|
|
11
|
+
cacheDir?: string;
|
|
12
|
+
binaries?: string;
|
|
13
|
+
storage?: {
|
|
14
|
+
type: 'disposable';
|
|
15
|
+
workDir?: string;
|
|
16
|
+
} | {
|
|
17
|
+
type: 'persistent';
|
|
18
|
+
dataDir: string;
|
|
19
|
+
workDir?: string;
|
|
20
|
+
};
|
|
21
|
+
runAs?: {
|
|
22
|
+
uid: number;
|
|
23
|
+
gid: number;
|
|
24
|
+
};
|
|
25
|
+
parameters?: Readonly<Record<string, string>>;
|
|
26
|
+
startTimeoutMs?: number;
|
|
27
|
+
stopTimeoutMs?: number;
|
|
28
|
+
/** Outer startup deadline, including executable resolution. Defaults to startTimeoutMs + 30s. */
|
|
29
|
+
startupDeadlineMs?: number;
|
|
30
|
+
/** Must exceed stopTimeoutMs + 2s. Defaults to stopTimeoutMs + 5s. */
|
|
31
|
+
shutdownDeadlineMs?: number;
|
|
32
|
+
/** Lifetime cancellation: abort before or after readiness requests cleanup. */
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
/** Additional child environment. EP_* keys are reserved. */
|
|
35
|
+
env?: Readonly<Record<string, string>>;
|
|
36
|
+
/** CLI flag names without --. Core, credential, config and lifecycle flags are reserved. */
|
|
37
|
+
cliOptions?: Readonly<Record<string, string | number | boolean | readonly string[]>>;
|
|
38
|
+
}
|
|
39
|
+
export interface PreparedOptions {
|
|
40
|
+
args: string[];
|
|
41
|
+
env: NodeJS.ProcessEnv;
|
|
42
|
+
password: string;
|
|
43
|
+
startupDeadlineMs: number;
|
|
44
|
+
shutdownDeadlineMs: number;
|
|
45
|
+
}
|
|
46
|
+
export declare function prepareOptions(options: PostgresOptions): PreparedOptions;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.prepareOptions = prepareOptions;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const errors_js_1 = require("./errors.js");
|
|
7
|
+
const reserved = new Set(['json', 'parent-stdin', 'config', 'state-file', 'password', 'port', 'database', 'username', 'start-timeout', 'stop-timeout', 'data-dir', 'work-dir', 'cache-dir', 'binaries', 'postgres-version', 'user', 'set', 'help']);
|
|
8
|
+
function prepareOptions(options) {
|
|
9
|
+
const start = (0, errors_js_1.positiveTimeout)(options.startTimeoutMs ?? 120_000, 'startTimeoutMs');
|
|
10
|
+
const stop = (0, errors_js_1.positiveTimeout)(options.stopTimeoutMs ?? 10_000, 'stopTimeoutMs');
|
|
11
|
+
if (stop > 60_000)
|
|
12
|
+
throw new errors_js_1.PostgresError('CONFIG', 'stopTimeoutMs must not exceed the CLI maximum of 60000');
|
|
13
|
+
const startupDeadlineMs = (0, errors_js_1.positiveTimeout)(options.startupDeadlineMs ?? start + 30_000, 'startupDeadlineMs');
|
|
14
|
+
const shutdownDeadlineMs = (0, errors_js_1.positiveTimeout)(options.shutdownDeadlineMs ?? stop + 5_000, 'shutdownDeadlineMs');
|
|
15
|
+
if (shutdownDeadlineMs <= stop + 2_000)
|
|
16
|
+
throw new errors_js_1.PostgresError('CONFIG', 'shutdownDeadlineMs must exceed stopTimeoutMs + 2000');
|
|
17
|
+
const port = options.port ?? 0;
|
|
18
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535)
|
|
19
|
+
throw new errors_js_1.PostgresError('CONFIG', 'port must be an integer between 0 and 65535');
|
|
20
|
+
if (options.storage?.type === 'persistent' && (!options.storage.dataDir || !options.password || !options.username || !options.database)) {
|
|
21
|
+
throw new errors_js_1.PostgresError('CONFIG', 'Persistent storage requires dataDir and explicit database, username and password');
|
|
22
|
+
}
|
|
23
|
+
if (options.password === '')
|
|
24
|
+
throw new errors_js_1.PostgresError('CONFIG', 'password must not be empty');
|
|
25
|
+
const password = options.password ?? (0, node_crypto_1.randomBytes)(24).toString('base64url');
|
|
26
|
+
const args = ['run', '--json', '--parent-stdin', `--port=${port}`, `--start-timeout=${start}ms`, `--stop-timeout=${stop}ms`];
|
|
27
|
+
const add = (name, value) => {
|
|
28
|
+
if (value !== undefined) {
|
|
29
|
+
if (value.includes('\0'))
|
|
30
|
+
throw new errors_js_1.PostgresError('CONFIG', 'CLI options cannot contain NUL characters');
|
|
31
|
+
args.push(`--${name}=${value}`);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
add('database', options.database);
|
|
35
|
+
add('username', options.username);
|
|
36
|
+
add('postgres-version', options.postgresVersion);
|
|
37
|
+
add('cache-dir', options.cacheDir === undefined ? undefined : (0, node_path_1.resolve)(options.cacheDir));
|
|
38
|
+
add('binaries', options.binaries === undefined ? undefined : (0, node_path_1.resolve)(options.binaries));
|
|
39
|
+
add('work-dir', options.storage?.workDir === undefined ? undefined : (0, node_path_1.resolve)(options.storage.workDir));
|
|
40
|
+
if (options.storage?.type === 'persistent')
|
|
41
|
+
add('data-dir', (0, node_path_1.resolve)(options.storage.dataDir));
|
|
42
|
+
if (options.runAs) {
|
|
43
|
+
for (const id of [options.runAs.uid, options.runAs.gid]) {
|
|
44
|
+
if (!Number.isInteger(id) || id < 0 || id >= 4_294_967_295)
|
|
45
|
+
throw new errors_js_1.PostgresError('CONFIG', 'runAs IDs must be unsigned integers below 4294967295');
|
|
46
|
+
}
|
|
47
|
+
if (options.runAs.uid === 0)
|
|
48
|
+
throw new errors_js_1.PostgresError('CONFIG', 'runAs.uid must be nonzero');
|
|
49
|
+
add('user', `${options.runAs.uid}:${options.runAs.gid}`);
|
|
50
|
+
}
|
|
51
|
+
for (const [name, value] of Object.entries(options.parameters ?? {})) {
|
|
52
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(name))
|
|
53
|
+
throw new errors_js_1.PostgresError('CONFIG', 'Invalid PostgreSQL parameter name');
|
|
54
|
+
add('set', `${name}=${value}`);
|
|
55
|
+
}
|
|
56
|
+
for (const [name, value] of Object.entries(options.cliOptions ?? {})) {
|
|
57
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name) || reserved.has(name))
|
|
58
|
+
throw new errors_js_1.PostgresError('CONFIG', 'Invalid or reserved advanced CLI option');
|
|
59
|
+
for (const item of Array.isArray(value) ? value : [value])
|
|
60
|
+
add(name, String(item));
|
|
61
|
+
}
|
|
62
|
+
const env = {};
|
|
63
|
+
for (const [name, value] of Object.entries(process.env)) {
|
|
64
|
+
// Do not allow ambient CLI configuration to turn disposable tests persistent.
|
|
65
|
+
if (!name.toUpperCase().startsWith('EP_'))
|
|
66
|
+
env[name] = value;
|
|
67
|
+
}
|
|
68
|
+
for (const [name, value] of Object.entries(options.env ?? {})) {
|
|
69
|
+
if (name.toUpperCase().startsWith('EP_') || name.includes('=') || name.includes('\0') || value.includes('\0')) {
|
|
70
|
+
throw new errors_js_1.PostgresError('CONFIG', 'Invalid or reserved child environment variable');
|
|
71
|
+
}
|
|
72
|
+
env[name] = value;
|
|
73
|
+
}
|
|
74
|
+
if (password.includes('\0'))
|
|
75
|
+
throw new errors_js_1.PostgresError('CONFIG', 'password cannot contain NUL characters');
|
|
76
|
+
env.EP_PASSWORD = password;
|
|
77
|
+
return { args, env, password, startupDeadlineMs, shutdownDeadlineMs };
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=options.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.js","sourceRoot":"","sources":["../../.local/build/src/options.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { resolve } from 'node:path';\nimport { PostgresError, positiveTimeout } from './errors.js';\nimport type { CliSource } from './binary.js';\n\nexport interface PostgresOptions {\n /** Explicit executable/release; otherwise EMBEDDED_POSTGRES_CLI or the package's pinned release. */\n cli?: CliSource;\n postgresVersion?: string;\n database?: string;\n username?: string;\n password?: string;\n /** Zero (the default) lets PostgreSQL select an available port. */\n port?: number;\n cacheDir?: string;\n binaries?: string;\n storage?: { type: 'disposable'; workDir?: string } | { type: 'persistent'; dataDir: string; workDir?: string };\n runAs?: { uid: number; gid: number };\n parameters?: Readonly<Record<string, string>>;\n startTimeoutMs?: number;\n stopTimeoutMs?: number;\n /** Outer startup deadline, including executable resolution. Defaults to startTimeoutMs + 30s. */\n startupDeadlineMs?: number;\n /** Must exceed stopTimeoutMs + 2s. Defaults to stopTimeoutMs + 5s. */\n shutdownDeadlineMs?: number;\n /** Lifetime cancellation: abort before or after readiness requests cleanup. */\n signal?: AbortSignal;\n /** Additional child environment. EP_* keys are reserved. */\n env?: Readonly<Record<string, string>>;\n /** CLI flag names without --. Core, credential, config and lifecycle flags are reserved. */\n cliOptions?: Readonly<Record<string, string | number | boolean | readonly string[]>>;\n}\n\nexport interface PreparedOptions {\n args: string[];\n env: NodeJS.ProcessEnv;\n password: string;\n startupDeadlineMs: number;\n shutdownDeadlineMs: number;\n}\n\nconst reserved = new Set(['json', 'parent-stdin', 'config', 'state-file', 'password', 'port', 'database', 'username', 'start-timeout', 'stop-timeout', 'data-dir', 'work-dir', 'cache-dir', 'binaries', 'postgres-version', 'user', 'set', 'help']);\n\nexport function prepareOptions(options: PostgresOptions): PreparedOptions {\n const start = positiveTimeout(options.startTimeoutMs ?? 120_000, 'startTimeoutMs');\n const stop = positiveTimeout(options.stopTimeoutMs ?? 10_000, 'stopTimeoutMs');\n if (stop > 60_000) throw new PostgresError('CONFIG', 'stopTimeoutMs must not exceed the CLI maximum of 60000');\n const startupDeadlineMs = positiveTimeout(options.startupDeadlineMs ?? start + 30_000, 'startupDeadlineMs');\n const shutdownDeadlineMs = positiveTimeout(options.shutdownDeadlineMs ?? stop + 5_000, 'shutdownDeadlineMs');\n if (shutdownDeadlineMs <= stop + 2_000) throw new PostgresError('CONFIG', 'shutdownDeadlineMs must exceed stopTimeoutMs + 2000');\n const port = options.port ?? 0;\n if (!Number.isInteger(port) || port < 0 || port > 65535) throw new PostgresError('CONFIG', 'port must be an integer between 0 and 65535');\n if (options.storage?.type === 'persistent' && (!options.storage.dataDir || !options.password || !options.username || !options.database)) {\n throw new PostgresError('CONFIG', 'Persistent storage requires dataDir and explicit database, username and password');\n }\n if (options.password === '') throw new PostgresError('CONFIG', 'password must not be empty');\n const password = options.password ?? randomBytes(24).toString('base64url');\n const args = ['run', '--json', '--parent-stdin', `--port=${port}`, `--start-timeout=${start}ms`, `--stop-timeout=${stop}ms`];\n const add = (name: string, value: string | undefined) => {\n if (value !== undefined) {\n if (value.includes('\\0')) throw new PostgresError('CONFIG', 'CLI options cannot contain NUL characters');\n args.push(`--${name}=${value}`);\n }\n };\n add('database', options.database);\n add('username', options.username);\n add('postgres-version', options.postgresVersion);\n add('cache-dir', options.cacheDir === undefined ? undefined : resolve(options.cacheDir));\n add('binaries', options.binaries === undefined ? undefined : resolve(options.binaries));\n add('work-dir', options.storage?.workDir === undefined ? undefined : resolve(options.storage.workDir));\n if (options.storage?.type === 'persistent') add('data-dir', resolve(options.storage.dataDir));\n if (options.runAs) {\n for (const id of [options.runAs.uid, options.runAs.gid]) {\n if (!Number.isInteger(id) || id < 0 || id >= 4_294_967_295) throw new PostgresError('CONFIG', 'runAs IDs must be unsigned integers below 4294967295');\n }\n if (options.runAs.uid === 0) throw new PostgresError('CONFIG', 'runAs.uid must be nonzero');\n add('user', `${options.runAs.uid}:${options.runAs.gid}`);\n }\n for (const [name, value] of Object.entries(options.parameters ?? {})) {\n if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(name)) throw new PostgresError('CONFIG', 'Invalid PostgreSQL parameter name');\n add('set', `${name}=${value}`);\n }\n for (const [name, value] of Object.entries(options.cliOptions ?? {})) {\n if (!/^[a-z][a-z0-9-]*$/.test(name) || reserved.has(name)) throw new PostgresError('CONFIG', 'Invalid or reserved advanced CLI option');\n for (const item of Array.isArray(value) ? value : [value]) add(name, String(item));\n }\n const env: NodeJS.ProcessEnv = {};\n for (const [name, value] of Object.entries(process.env)) {\n // Do not allow ambient CLI configuration to turn disposable tests persistent.\n if (!name.toUpperCase().startsWith('EP_')) env[name] = value;\n }\n for (const [name, value] of Object.entries(options.env ?? {})) {\n if (name.toUpperCase().startsWith('EP_') || name.includes('=') || name.includes('\\0') || value.includes('\\0')) {\n throw new PostgresError('CONFIG', 'Invalid or reserved child environment variable');\n }\n env[name] = value;\n }\n if (password.includes('\\0')) throw new PostgresError('CONFIG', 'password cannot contain NUL characters');\n env.EP_PASSWORD = password;\n return { args, env, password, startupDeadlineMs, shutdownDeadlineMs };\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type ProtocolEvent = {
|
|
2
|
+
protocol: 1;
|
|
3
|
+
event: 'ready';
|
|
4
|
+
connection_url: string;
|
|
5
|
+
port: number;
|
|
6
|
+
} | {
|
|
7
|
+
protocol: 1;
|
|
8
|
+
event: 'stopped';
|
|
9
|
+
} | {
|
|
10
|
+
protocol: 1;
|
|
11
|
+
event: 'error';
|
|
12
|
+
error: string;
|
|
13
|
+
};
|
|
14
|
+
/** Incremental JSON-lines parser. Never include raw protocol in diagnostics. */
|
|
15
|
+
export declare class ProtocolParser {
|
|
16
|
+
private readonly onEvent;
|
|
17
|
+
private pending;
|
|
18
|
+
constructor(onEvent: (event: ProtocolEvent) => void);
|
|
19
|
+
push(chunk: string): void;
|
|
20
|
+
finish(): void;
|
|
21
|
+
private line;
|
|
22
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ProtocolParser = void 0;
|
|
4
|
+
const errors_js_1 = require("./errors.js");
|
|
5
|
+
const limit = 64 * 1024;
|
|
6
|
+
/** Incremental JSON-lines parser. Never include raw protocol in diagnostics. */
|
|
7
|
+
class ProtocolParser {
|
|
8
|
+
onEvent;
|
|
9
|
+
pending = '';
|
|
10
|
+
constructor(onEvent) {
|
|
11
|
+
this.onEvent = onEvent;
|
|
12
|
+
}
|
|
13
|
+
push(chunk) {
|
|
14
|
+
this.pending += chunk;
|
|
15
|
+
let newline;
|
|
16
|
+
while ((newline = this.pending.indexOf('\n')) !== -1) {
|
|
17
|
+
const line = this.pending.slice(0, newline);
|
|
18
|
+
this.pending = this.pending.slice(newline + 1);
|
|
19
|
+
this.line(line);
|
|
20
|
+
}
|
|
21
|
+
if (this.pending.length > limit)
|
|
22
|
+
throw new errors_js_1.PostgresError('PROTOCOL', 'CLI protocol line exceeds 64 KiB');
|
|
23
|
+
}
|
|
24
|
+
finish() {
|
|
25
|
+
if (this.pending)
|
|
26
|
+
this.line(this.pending);
|
|
27
|
+
this.pending = '';
|
|
28
|
+
}
|
|
29
|
+
line(line) {
|
|
30
|
+
if (line.length > limit)
|
|
31
|
+
throw new errors_js_1.PostgresError('PROTOCOL', 'CLI protocol line exceeds 64 KiB');
|
|
32
|
+
if (!line.trim())
|
|
33
|
+
return;
|
|
34
|
+
let value;
|
|
35
|
+
try {
|
|
36
|
+
value = JSON.parse(line);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new errors_js_1.PostgresError('PROTOCOL', 'CLI emitted invalid JSON');
|
|
40
|
+
}
|
|
41
|
+
if (!value || typeof value !== 'object' || !('protocol' in value) || value.protocol !== 1 || !('event' in value)) {
|
|
42
|
+
throw new errors_js_1.PostgresError('PROTOCOL', 'CLI must speak protocol 1');
|
|
43
|
+
}
|
|
44
|
+
const event = value;
|
|
45
|
+
if (event.event === 'ready') {
|
|
46
|
+
if (!Number.isInteger(event.port) || event.port < 1 || event.port > 65535 || typeof event.connection_url !== 'string') {
|
|
47
|
+
throw new errors_js_1.PostgresError('PROTOCOL', 'Invalid CLI ready event');
|
|
48
|
+
}
|
|
49
|
+
let url;
|
|
50
|
+
try {
|
|
51
|
+
url = new URL(event.connection_url);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new errors_js_1.PostgresError('PROTOCOL', 'Invalid CLI connection URL');
|
|
55
|
+
}
|
|
56
|
+
if (!['postgres:', 'postgresql:'].includes(url.protocol))
|
|
57
|
+
throw new errors_js_1.PostgresError('PROTOCOL', 'Invalid CLI connection URL scheme');
|
|
58
|
+
}
|
|
59
|
+
else if (event.event === 'error') {
|
|
60
|
+
if (typeof event.error !== 'string')
|
|
61
|
+
throw new errors_js_1.PostgresError('PROTOCOL', 'Invalid CLI error event');
|
|
62
|
+
}
|
|
63
|
+
else if (event.event !== 'stopped') {
|
|
64
|
+
throw new errors_js_1.PostgresError('PROTOCOL', 'Unknown CLI protocol event');
|
|
65
|
+
}
|
|
66
|
+
this.onEvent(value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.ProtocolParser = ProtocolParser;
|
|
70
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../.local/build/src/protocol.ts"],"names":[],"mappings":"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","sourcesContent":["import { PostgresError } from './errors.js';\n\nexport type ProtocolEvent =\n | { protocol: 1; event: 'ready'; connection_url: string; port: number }\n | { protocol: 1; event: 'stopped' }\n | { protocol: 1; event: 'error'; error: string };\n\nconst limit = 64 * 1024;\n\n/** Incremental JSON-lines parser. Never include raw protocol in diagnostics. */\nexport class ProtocolParser {\n private pending = '';\n constructor(private readonly onEvent: (event: ProtocolEvent) => void) {}\n\n push(chunk: string): void {\n this.pending += chunk;\n let newline: number;\n while ((newline = this.pending.indexOf('\\n')) !== -1) {\n const line = this.pending.slice(0, newline);\n this.pending = this.pending.slice(newline + 1);\n this.line(line);\n }\n if (this.pending.length > limit) throw new PostgresError('PROTOCOL', 'CLI protocol line exceeds 64 KiB');\n }\n\n finish(): void {\n if (this.pending) this.line(this.pending);\n this.pending = '';\n }\n\n private line(line: string): void {\n if (line.length > limit) throw new PostgresError('PROTOCOL', 'CLI protocol line exceeds 64 KiB');\n if (!line.trim()) return;\n let value: unknown;\n try { value = JSON.parse(line); } catch { throw new PostgresError('PROTOCOL', 'CLI emitted invalid JSON'); }\n if (!value || typeof value !== 'object' || !('protocol' in value) || value.protocol !== 1 || !('event' in value)) {\n throw new PostgresError('PROTOCOL', 'CLI must speak protocol 1');\n }\n const event = value as Record<string, unknown>;\n if (event.event === 'ready') {\n if (!Number.isInteger(event.port) || (event.port as number) < 1 || (event.port as number) > 65535 || typeof event.connection_url !== 'string') {\n throw new PostgresError('PROTOCOL', 'Invalid CLI ready event');\n }\n let url: URL;\n try { url = new URL(event.connection_url); } catch { throw new PostgresError('PROTOCOL', 'Invalid CLI connection URL'); }\n if (!['postgres:', 'postgresql:'].includes(url.protocol)) throw new PostgresError('PROTOCOL', 'Invalid CLI connection URL scheme');\n } else if (event.event === 'error') {\n if (typeof event.error !== 'string') throw new PostgresError('PROTOCOL', 'Invalid CLI error event');\n } else if (event.event !== 'stopped') {\n throw new PostgresError('PROTOCOL', 'Unknown CLI protocol event');\n }\n this.onEvent(value as ProtocolEvent);\n }\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type CliPlatform = 'linux-amd64' | 'linux-arm64' | 'darwin-amd64' | 'darwin-arm64' | 'windows-amd64' | 'windows-arm64';
|
|
2
|
+
export interface CliRelease {
|
|
3
|
+
/** Exact published v2 tag. There is deliberately no implicit latest release. */
|
|
4
|
+
version: string;
|
|
5
|
+
/** Trusted SHA-256 pins, copied from reviewed upstream release checksums.txt. */
|
|
6
|
+
checksums: Readonly<Partial<Record<CliPlatform, string>>>;
|
|
7
|
+
/** Directory containing VERSION/ASSET, defaulting to upstream GitHub releases. */
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Reviewed upstream release; every platform asset was checked against checksums.txt. */
|
|
11
|
+
export declare const DEFAULT_CLI_RELEASE: Readonly<CliRelease>;
|
|
12
|
+
export type CliSource = {
|
|
13
|
+
path: string;
|
|
14
|
+
} | {
|
|
15
|
+
/** Omit to use this package's reviewed DEFAULT_CLI_RELEASE. */
|
|
16
|
+
release?: CliRelease;
|
|
17
|
+
cacheDir?: string;
|
|
18
|
+
offline?: boolean;
|
|
19
|
+
downloadTimeoutMs?: number;
|
|
20
|
+
};
|
|
21
|
+
export declare function cliPlatform(platform?: string, arch?: string): CliPlatform;
|
|
22
|
+
export declare function cliAsset(target: CliPlatform): string;
|
|
23
|
+
/** Explicit source, then EMBEDDED_POSTGRES_CLI, then the package's pinned release. */
|
|
24
|
+
export declare function resolveCli(source?: CliSource, signal?: AbortSignal): Promise<string>;
|