@dashclaw/cli 0.7.3 → 0.7.4

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/lib/up/db.js CHANGED
@@ -17,23 +17,83 @@
17
17
  // on resume so a working install never silently moves.
18
18
 
19
19
  import { join } from 'node:path';
20
- import { existsSync, rmSync } from 'node:fs';
20
+ import { tmpdir } from 'node:os';
21
+ import { existsSync, rmSync, writeFileSync } from 'node:fs';
21
22
  import { execFileSync, spawnSync } from 'node:child_process';
22
23
 
23
24
  // Keep this in sync with the "embedded-postgres" version in cli/package.json.
24
25
  const EMBEDDED_PG_VERSION = 'embedded-postgres@18.4.0-beta.17';
25
26
 
27
+ // Force a UTF-8 cluster regardless of host locale. Without this, initdb on
28
+ // Windows inherits the OS locale (e.g. English_United States.1252) and
29
+ // creates a WIN1252-encoded database — DashClaw's migration SQL is UTF-8 and
30
+ // statements containing characters with no WIN1252 equivalent hard-fail with
31
+ // 22P05, leaving a partial schema (observed live in Windows Sandbox).
32
+ const INITDB_FLAGS = ['--encoding=UTF8', '--no-locale'];
33
+
26
34
  // Windows exit status 0xC0000135 (STATUS_DLL_NOT_FOUND). The embedded
27
35
  // Postgres binaries link against the Microsoft Visual C++ runtime, which a
28
36
  // fresh Windows install (and Windows Sandbox) does not ship — initdb dies at
29
37
  // exec with this code and EMPTY stderr, which reads as pure noise to a
30
38
  // first-run user.
31
39
  const STATUS_DLL_NOT_FOUND = '3221225781';
40
+ const VC_REDIST_URL = 'https://aka.ms/vs/17/release/vc_redist.x64.exe';
32
41
  const VC_REDIST_HINT =
33
42
  'Embedded Postgres needs the Microsoft Visual C++ runtime, which this Windows machine does not have. '
34
- + 'Install it once from https://aka.ms/vs/17/release/vc_redist.x64.exe (or `winget install Microsoft.VCRedist.2015+.x64`), '
43
+ + `Install it once from ${VC_REDIST_URL} (or \`winget install Microsoft.VCRedist.2015+.x64\`), `
35
44
  + 'then re-run `npx dashclaw up`. Alternatively retry with --db docker or --db url.';
36
45
 
46
+ // Redistributable installer exit codes that mean the runtime is (or already
47
+ // was) in place: 0 = installed, 1638 = a newer version is already installed,
48
+ // 3010 = installed, reboot pending (the DLLs are on disk and loadable now).
49
+ const VC_REDIST_OK_EXIT_CODES = new Set([0, 1638, 3010]);
50
+
51
+ /**
52
+ * Downloads and silently installs the Microsoft Visual C++ redistributable so
53
+ * `npx dashclaw up` stays one command on a fresh Windows machine. Elevation
54
+ * goes through `Start-Process -Verb RunAs`: an already-elevated shell runs it
55
+ * directly, a standard shell surfaces the Windows UAC consent dialog — that
56
+ * dialog IS the user's approval for the system-wide install. Throws (with the
57
+ * manual remediation appended) when the download or install fails; the caller
58
+ * re-checks the DLLs afterwards as the authoritative success signal.
59
+ */
60
+ export async function installVcRedist({
61
+ logger = console,
62
+ fetchFn = fetch,
63
+ spawn = spawnSync,
64
+ downloadDir = tmpdir(),
65
+ } = {}) {
66
+ logger.error(
67
+ '[..] Embedded Postgres needs the Microsoft Visual C++ runtime — installing it now '
68
+ + '(one-time, ~25 MB from microsoft.com; Windows may show an admin consent prompt) ...',
69
+ );
70
+ const exePath = join(downloadDir, 'dashclaw-vc_redist.x64.exe');
71
+ let res;
72
+ try {
73
+ res = await fetchFn(VC_REDIST_URL);
74
+ } catch (e) {
75
+ throw new Error(`VC++ runtime download failed (${e.message}). ${VC_REDIST_HINT}`);
76
+ }
77
+ if (!res.ok) {
78
+ throw new Error(`VC++ runtime download failed (HTTP ${res.status}). ${VC_REDIST_HINT}`);
79
+ }
80
+ writeFileSync(exePath, Buffer.from(await res.arrayBuffer()));
81
+ const psPath = exePath.replace(/'/g, "''");
82
+ const script =
83
+ `$p = Start-Process -FilePath '${psPath}' -ArgumentList '/install','/quiet','/norestart' -Verb RunAs -Wait -PassThru; `
84
+ + 'exit $p.ExitCode';
85
+ const run = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script], { stdio: 'ignore' });
86
+ if (run.error) {
87
+ throw new Error(`VC++ runtime install could not start (${run.error.message}). ${VC_REDIST_HINT}`);
88
+ }
89
+ if (!VC_REDIST_OK_EXIT_CODES.has(run.status)) {
90
+ throw new Error(
91
+ `VC++ runtime installer exited with code ${run.status} (declined admin prompt, or install failure). ${VC_REDIST_HINT}`,
92
+ );
93
+ }
94
+ logger.error('[ok] Microsoft Visual C++ runtime installed.');
95
+ }
96
+
37
97
  /**
38
98
  * True on Windows machines missing the VC++ runtime DLLs the embedded
39
99
  * Postgres binaries need. Checked BEFORE the embedded attempt so the failure
@@ -51,8 +111,15 @@ export function missingVcRuntime({ platform = process.platform, exists = existsS
51
111
  * the preflight passed (a different DLL may be the missing one).
52
112
  */
53
113
  export function embeddedFailureMessage(err) {
54
- const base = `Embedded Postgres (${EMBEDDED_PG_VERSION}) failed: ${err.message}`;
55
- if (String(err.message).includes(STATUS_DLL_NOT_FOUND)) {
114
+ // embedded-postgres rejects with NO value when the postgres process exits
115
+ // before it is ready (its start() does `reject()` bare) — the real reason is
116
+ // in the log lines it already printed. Guard, or this helper itself crashes
117
+ // with "Cannot read properties of undefined" and eats the actual error
118
+ // (observed live in Windows Sandbox).
119
+ const detail = err?.message
120
+ ?? 'the Postgres process exited before it was ready — the reason is in the log lines above';
121
+ const base = `Embedded Postgres (${EMBEDDED_PG_VERSION}) failed: ${detail}`;
122
+ if (String(detail).includes(STATUS_DLL_NOT_FOUND)) {
56
123
  return `${base} — this exit code means a required system DLL is missing. ${VC_REDIST_HINT}`;
57
124
  }
58
125
  return `${base}. Retry with --db docker or --db url.`;
@@ -91,7 +158,7 @@ export function dockerAvailableSync() {
91
158
  * @param {Function} [opts.promptFn] - async (message) => string
92
159
  * @returns {Promise<'docker'|'embedded'|'url'>}
93
160
  */
94
- export async function chooseDbMode({ flagDb, dockerAvailable, yes = false, promptFn }) {
161
+ export async function chooseDbMode({ flagDb, dockerAvailable, yes = false, promptFn, logger = console }) {
95
162
  if (flagDb) return flagDb;
96
163
  if (yes) return dockerAvailable ? 'docker' : 'embedded';
97
164
 
@@ -104,7 +171,12 @@ export async function chooseDbMode({ flagDb, dockerAvailable, yes = false, promp
104
171
  ' 3. I have a postgresql:// URL',
105
172
  ];
106
173
  const def = dockerAvailable ? '1' : '2';
107
- const answer = (await promptFn(`${lines.join('\n')}\nChoice [${def}]: `)).trim() || def;
174
+ // The menu is printed OUTSIDE the readline prompt: on Windows, readline
175
+ // re-renders multi-line prompts on input events, so the whole menu was
176
+ // echoed twice (observed live in Windows Sandbox). Only the one-line
177
+ // "Choice" question goes through promptFn.
178
+ logger.error(lines.join('\n'));
179
+ const answer = (await promptFn(`Choice [${def}]: `)).trim() || def;
108
180
  return { 1: 'docker', 2: 'embedded', 3: 'url' }[answer] ?? (dockerAvailable ? 'docker' : 'embedded');
109
181
  }
110
182
 
@@ -284,7 +356,7 @@ async function dockerProvision({ preferredPort, ops, isFree, logger }) {
284
356
  export async function provisionDatabase({
285
357
  mode, baseDir, promptFn, savedDatabaseUrl,
286
358
  dockerOps = realDockerOps, isFree = isPortFree, waitForDbPort = waitForPort, logger = console,
287
- checkVcRuntime = missingVcRuntime,
359
+ checkVcRuntime = missingVcRuntime, installVc = installVcRedist,
288
360
  }) {
289
361
  let preferredPort = DEFAULT_DB_PORT;
290
362
  if (savedDatabaseUrl) {
@@ -305,11 +377,24 @@ export async function provisionDatabase({
305
377
 
306
378
  // mode === 'embedded'
307
379
  if (checkVcRuntime()) {
308
- throw new Error(VC_REDIST_HINT);
380
+ // Fresh Windows machines (and Windows Sandbox) don't ship the VC++
381
+ // runtime the Postgres binaries link against. Install it in-flow instead
382
+ // of failing with homework — `dashclaw up` promises ONE command.
383
+ await installVc({ logger });
384
+ if (checkVcRuntime()) {
385
+ throw new Error(`The VC++ runtime installer finished but the runtime DLLs are still missing. ${VC_REDIST_HINT}`);
386
+ }
387
+ }
388
+ const pgDir = join(baseDir, 'pg');
389
+ if (process.platform === 'win32') {
390
+ // postgres.exe hard-refuses an elevated (admin) token — the norm in
391
+ // Windows Sandbox and admin terminals. pg_ctl creates the restricted
392
+ // token PostgreSQL requires, so on Windows the server lifecycle goes
393
+ // through pg_ctl instead of embedded-postgres spawning postgres directly.
394
+ return provisionEmbeddedWindows({ baseDir, pgDir, preferredPort, isFree, logger });
309
395
  }
310
396
  const port = await pickDbPort({ preferred: preferredPort, isFree, logger });
311
397
  const { default: EmbeddedPostgres } = await import('embedded-postgres');
312
- const pgDir = join(baseDir, 'pg');
313
398
  // Record whether the data dir existed BEFORE this run.
314
399
  // Only clean up on failure when WE created it (a pre-existing dir means
315
400
  // a working prior install whose data we must not delete).
@@ -323,9 +408,13 @@ export async function provisionDatabase({
323
408
  password: localDb.password,
324
409
  port,
325
410
  persistent: true,
411
+ initdbFlags: INITDB_FLAGS,
326
412
  });
327
413
  try {
328
- await pg.initialise();
414
+ // initdb refuses a non-empty data dir, so only initialise a cluster that
415
+ // does not exist yet (PG_VERSION marks an initialised cluster) — without
416
+ // this, every resumed `up` with an embedded DB fails at re-init.
417
+ if (!clusterInitialised(pgDir)) await pg.initialise();
329
418
  await pg.start();
330
419
  } catch (e) {
331
420
  if (!dirPreExisted) {
@@ -338,6 +427,122 @@ export async function provisionDatabase({
338
427
  return { databaseUrl: localDbUrlFor(port), stop: () => pg.stop() };
339
428
  }
340
429
 
430
+ /** True when pgDir already holds an initialised cluster (initdb completed). */
431
+ export function clusterInitialised(pgDir, exists = existsSync) {
432
+ return exists(join(pgDir, 'PG_VERSION'));
433
+ }
434
+
435
+ /** Last lines of the pg_ctl server log, for actionable start-failure errors. */
436
+ async function pgLogTail(logFile, lines = 12) {
437
+ try {
438
+ const { readFileSync } = await import('node:fs');
439
+ const content = readFileSync(logFile, 'utf8').trim().split(/\r?\n/);
440
+ return ` Server log (${logFile}):\n${content.slice(-lines).join('\n')}`;
441
+ } catch {
442
+ return '';
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Windows lifecycle for the embedded cluster, via pg_ctl.
448
+ *
449
+ * Why not embedded-postgres's own start(): it spawns postgres.exe directly,
450
+ * and postgres.exe refuses to run under a token with admin privileges
451
+ * ("Execution of PostgreSQL by a user with administrative permissions is not
452
+ * permitted"). initdb self-restricts its token, pg_ctl restricts the token it
453
+ * starts postgres with — postgres itself does not. Routing start/stop through
454
+ * pg_ctl works from BOTH elevated and normal shells. Consequence: the server
455
+ * is detached from this process, so a previous `up` may have left it running —
456
+ * `pg_ctl status` first, and reuse it on its saved port.
457
+ *
458
+ * All effects are injectable for unit tests.
459
+ */
460
+ export async function provisionEmbeddedWindows({
461
+ baseDir, pgDir, preferredPort, isFree = isPortFree, logger = console,
462
+ bins, // { pg_ctl } — default: @embedded-postgres/windows-x64
463
+ spawn = spawnSync,
464
+ initCluster, // default: embedded-postgres initialise()
465
+ connectClient, // default: pg Client — async (port) => { query, end }
466
+ }) {
467
+ const localDb = new URL(LOCAL_DB_URL);
468
+ const pgCtl = bins?.pg_ctl ?? (await import('@embedded-postgres/windows-x64')).pg_ctl;
469
+ const logFile = join(baseDir, 'pg.log');
470
+
471
+ const running = spawn(pgCtl, ['-D', pgDir, 'status'], { stdio: 'ignore' }).status === 0;
472
+ let port = preferredPort;
473
+ if (running) {
474
+ // A detached server from a previous `up` is still serving — reuse it on
475
+ // the port it was started with (the saved databaseUrl's port). Probing
476
+ // for a free port here would wrongly route around our own server.
477
+ logger.error(`[ok] Embedded Postgres already running (port ${port}, data in ${pgDir})`);
478
+ } else {
479
+ port = await pickDbPort({ preferred: preferredPort, isFree, logger });
480
+ const dirPreExisted = existsSync(pgDir);
481
+ try {
482
+ if (!clusterInitialised(pgDir)) {
483
+ if (initCluster) {
484
+ await initCluster({ pgDir, user: localDb.username, password: localDb.password });
485
+ } else {
486
+ const { default: EmbeddedPostgres } = await import('embedded-postgres');
487
+ await new EmbeddedPostgres({
488
+ databaseDir: pgDir,
489
+ user: localDb.username,
490
+ password: localDb.password,
491
+ port,
492
+ persistent: true,
493
+ initdbFlags: INITDB_FLAGS,
494
+ }).initialise();
495
+ }
496
+ }
497
+ const res = spawn(
498
+ pgCtl,
499
+ ['-D', pgDir, '-o', `-p ${port}`, '-l', logFile, '-w', '-t', '60', 'start'],
500
+ { stdio: 'ignore' },
501
+ );
502
+ if (res.error || res.status !== 0) {
503
+ const reason = res.error ? res.error.message : `pg_ctl start exited ${res.status}.`;
504
+ throw new Error(`${reason}${await pgLogTail(logFile)}`);
505
+ }
506
+ } catch (e) {
507
+ if (!dirPreExisted) {
508
+ rmSync(pgDir, { recursive: true, force: true });
509
+ }
510
+ throw new Error(embeddedFailureMessage(e));
511
+ }
512
+ logger.error(`[ok] Embedded Postgres running (port ${port}, data in ${pgDir})`);
513
+ }
514
+
515
+ await createDashclawDatabase({ port, localDb, connectClient });
516
+ return {
517
+ databaseUrl: localDbUrlFor(port),
518
+ stop: async () => {
519
+ spawn(pgCtl, ['-D', pgDir, '-m', 'fast', 'stop'], { stdio: 'ignore' });
520
+ },
521
+ };
522
+ }
523
+
524
+ /** CREATE DATABASE dashclaw, tolerating "already exists" (resume). */
525
+ async function createDashclawDatabase({ port, localDb, connectClient }) {
526
+ const connect = connectClient ?? (async (p) => {
527
+ const { default: pg } = await import('pg');
528
+ const client = new pg.Client({
529
+ host: 'localhost', port: p,
530
+ user: localDb.username, password: localDb.password,
531
+ database: 'postgres',
532
+ });
533
+ await client.connect();
534
+ return client;
535
+ });
536
+ const client = await connect(port);
537
+ try {
538
+ await client.query(`CREATE DATABASE ${localDb.pathname.slice(1)}`);
539
+ } catch (e) {
540
+ if (e.code !== '42P04') throw e; // 42P04 = duplicate_database (resume)
541
+ } finally {
542
+ await client.end();
543
+ }
544
+ }
545
+
341
546
  /** Polls :port until it accepts TCP connections, or throws on timeout. */
342
547
  async function waitForPort(port, timeoutMs) {
343
548
  const net = await import('node:net');
package/lib/up/index.js CHANGED
@@ -20,7 +20,7 @@ import { join } from 'node:path';
20
20
  import { STEPS, loadInstance, saveInstance, checkpoint } from './instance.js';
21
21
  import { resolveAppVersion, downloadAndExtract } from './fetch-app.js';
22
22
  import { dockerAvailableSync, chooseDbMode, provisionDatabase } from './db.js';
23
- import { installDeps, buildApp, startServer, waitForHealth, openBrowser } from './run.js';
23
+ import { installDeps, buildApp, startServer, waitForHealth, openBrowser, winSafeSpawnSync } from './run.js';
24
24
  import { installClaude } from '../claude/install.js';
25
25
  import { parseUpArgs } from './args.js';
26
26
  import { ask } from '../config.js';
@@ -58,7 +58,7 @@ export function resolveBaseDir(args) {
58
58
  * @param {object} [opts.logger]
59
59
  * @param {Function} [opts.spawn] injectable spawnSync for testing (default: spawnSync)
60
60
  */
61
- export function runSetupScriptReal({ appDir, databaseUrl, logger = console, spawn: spawnFn = spawnSync }) {
61
+ export function runSetupScriptReal({ appDir, databaseUrl, logger = console, spawn: spawnFn = winSafeSpawnSync }) {
62
62
  logger.error('-> Running setup (migrations + first admin) ...');
63
63
  const res = spawnFn(
64
64
  'node',
@@ -71,7 +71,9 @@ export function runSetupScriptReal({ appDir, databaseUrl, logger = console, spaw
71
71
  ],
72
72
  // stdin MUST be 'ignore': the default open pipe makes any stray readline
73
73
  // prompt in the child hang forever (observed: 12-minute silent hang).
74
- { cwd: appDir, encoding: 'utf8', shell: process.platform === 'win32', stdio: ['ignore', 'pipe', 'pipe'] },
74
+ // Windows shell handling (npm-style .cmd resolution + DEP0190 avoidance)
75
+ // lives in winSafeSpawnSync.
76
+ { cwd: appDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
75
77
  );
76
78
  const stdout = res.stdout || '';
77
79
  const lines = stdout.split('\n').map((l) => l.trim()).filter(Boolean);
@@ -178,7 +180,7 @@ export async function runUp({ args, baseDir = join(homedir(), '.dashclaw'), deps
178
180
  // is both wrong and hangs. Reuse the saved URL when db_ready is already
179
181
  // checkpointed and inst.databaseUrl is set.
180
182
  const dbMode = inst.dbMode ?? await deps.chooseDbMode({
181
- flagDb: args.db, dockerAvailable: deps.dockerAvailable, yes: args.yes, promptFn: deps.promptFn,
183
+ flagDb: args.db, dockerAvailable: deps.dockerAvailable, yes: args.yes, promptFn: deps.promptFn, logger,
182
184
  });
183
185
  const db = (dbMode === 'url' && done('db_ready') && inst.databaseUrl)
184
186
  ? { databaseUrl: inst.databaseUrl, stop: async () => {} }
@@ -313,6 +315,15 @@ export async function runDown({
313
315
  dockerStop('dashclaw-pg');
314
316
  logger.log('[ok] Stopped Docker Postgres (container dashclaw-pg).');
315
317
  }
318
+ if (inst.dbMode === 'embedded' && process.platform === 'win32') {
319
+ // On Windows the embedded server runs detached via pg_ctl (see db.js) and
320
+ // survives the up process — stop it here, best-effort.
321
+ try {
322
+ const { pg_ctl } = await import('@embedded-postgres/windows-x64');
323
+ const res = spawnSync(pg_ctl, ['-D', join(baseDir, 'pg'), '-m', 'fast', 'stop'], { stdio: 'ignore' });
324
+ if (res.status === 0) logger.log('[ok] Stopped embedded Postgres.');
325
+ } catch { /* binaries not installed — nothing to stop */ }
326
+ }
316
327
  }
317
328
 
318
329
  /**
package/lib/up/run.js CHANGED
@@ -6,26 +6,45 @@ import { spawn, spawnSync } from 'node:child_process';
6
6
 
7
7
  const shell = process.platform === 'win32';
8
8
 
9
- function npm(args, cwd, logger, runner = spawnSync) {
9
+ // npm/npx are .cmd files on Windows, which node refuses to spawn without a
10
+ // shell — but shell:true + an args ARRAY is deprecated (DEP0190, and the
11
+ // warning prints mid-flow for first-run users). So on Windows the command is
12
+ // joined into ONE string (quoting any arg with shell-special characters) and
13
+ // handed to the shell whole; elsewhere args pass through verbatim, no shell.
14
+ const quoteArg = (a) => (/^[\w\-.:/@=]+$/.test(String(a)) ? String(a) : `"${a}"`);
15
+ export function winSafeSpawnArgs(cmd, args) {
16
+ return shell
17
+ ? { cmd: [cmd, ...args.map(quoteArg)].join(' '), args: [], opts: { shell: true } }
18
+ : { cmd, args, opts: {} };
19
+ }
20
+
21
+ /** spawnSync that takes LOGICAL (cmd, args) and win-joins only at the edge. */
22
+ export function winSafeSpawnSync(cmd, args, opts) {
23
+ const safe = winSafeSpawnArgs(cmd, args);
24
+ return spawnSync(safe.cmd, safe.args, { ...opts, ...safe.opts });
25
+ }
26
+
27
+ function npm(args, cwd, logger, runner = winSafeSpawnSync) {
10
28
  logger.error(`-> npm ${args.join(' ')}`);
11
- const res = runner('npm', args, { cwd, stdio: ['ignore', 'inherit', 'inherit'], shell });
29
+ const res = runner('npm', args, { cwd, stdio: ['ignore', 'inherit', 'inherit'] });
12
30
  if (res.status !== 0) throw new Error(`npm ${args[0]} failed (exit ${res.status}). Re-run \`npx dashclaw up\` to resume from this step.`);
13
31
  }
14
32
 
15
- export function installDeps(appDir, logger = console, runner = spawnSync) {
33
+ export function installDeps(appDir, logger = console, runner = winSafeSpawnSync) {
16
34
  try { npm(['ci', '--no-audit', '--no-fund'], appDir, logger, runner); }
17
35
  catch { npm(['install', '--no-audit', '--no-fund'], appDir, logger, runner); } // lockfile mismatch fallback
18
36
  }
19
37
 
20
- export function buildApp(appDir, logger = console, runner = spawnSync) {
38
+ export function buildApp(appDir, logger = console, runner = winSafeSpawnSync) {
21
39
  npm(['run', 'build'], appDir, logger, runner);
22
40
  }
23
41
 
24
42
  /** Start `next start` as a child; .env.local in appDir is loaded by Next itself. */
25
43
  export function startServer({ appDir, port, logger = console }) {
26
44
  logger.error(`-> Starting server on :${port}`);
27
- const child = spawn('npx', ['next', 'start', '-p', String(port)], {
28
- cwd: appDir, stdio: ['ignore', 'inherit', 'inherit'], shell,
45
+ const safe = winSafeSpawnArgs('npx', ['next', 'start', '-p', String(port)]);
46
+ const child = spawn(safe.cmd, safe.args, {
47
+ cwd: appDir, stdio: ['ignore', 'inherit', 'inherit'], ...safe.opts,
29
48
  });
30
49
  return child;
31
50
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dashclaw/cli",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "description": "DashClaw terminal client — approve agent actions and diagnose your instance",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -27,6 +27,7 @@
27
27
  "dependencies": {
28
28
  "dashclaw": "^2.2.1",
29
29
  "embedded-postgres": "18.4.0-beta.17",
30
+ "pg": "^8.22.0",
30
31
  "tar": "^7.4.0"
31
32
  },
32
33
  "license": "MIT",