pragma-so 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/server/db.js +242 -9
  2. package/package.json +4 -2
package/dist/server/db.js CHANGED
@@ -24,13 +24,12 @@ const promises_1 = require("node:fs/promises");
24
24
  const node_os_1 = require("node:os");
25
25
  const node_path_1 = require("node:path");
26
26
  const pglite_1 = require("@electric-sql/pglite");
27
+ const pglite_socket_1 = require("@electric-sql/pglite-socket");
28
+ const pg_1 = require("pg");
27
29
  const gitWorkflow_1 = require("./conversation/gitWorkflow");
28
30
  const store_1 = require("./conversation/store");
29
31
  const bundledSkills_1 = require("./bundledSkills");
30
- const CONFIGURED_PRAGMA_DIR = process.env.PRAGMA_DIR?.trim();
31
- exports.PRAGMA_DIR = CONFIGURED_PRAGMA_DIR
32
- ? (0, node_path_1.resolve)(CONFIGURED_PRAGMA_DIR)
33
- : (0, node_path_1.join)((0, node_os_1.homedir)(), ".pragma");
32
+ exports.PRAGMA_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), ".pragma");
34
33
  const ACTIVE_WORKSPACE_FILE = (0, node_path_1.join)(exports.PRAGMA_DIR, "active_workspace");
35
34
  const RESERVED_ROOT_NAMES = new Set(["db", "workspace", "worktrees"]);
36
35
  exports.DEFAULT_AGENT_ID = "pragma-orchestrator";
@@ -43,6 +42,9 @@ function getDefaultModelForHarness(harness) {
43
42
  return DEFAULT_HARNESS_MODELS[harness] ?? DEFAULT_HARNESS_MODELS.claude_code;
44
43
  }
45
44
  const REAL_CLOSE = new WeakMap();
45
+ const DB_SOCKET_HOST = "127.0.0.1";
46
+ const DB_SOCKET_WAIT_MS = 5000;
47
+ const DB_SOCKET_POLL_MS = 100;
46
48
  exports.DEFAULT_AGENT_FILE = `# Orchestrator
47
49
 
48
50
  You are the orchestrator agent for Pragma.
@@ -354,7 +356,7 @@ async function openDatabase(workspaceName) {
354
356
  if (existing) {
355
357
  return existing;
356
358
  }
357
- const pending = createWorkspaceDatabase(paths.dbDir).catch((error) => {
359
+ const pending = openWorkspaceDatabase(paths).catch((error) => {
358
360
  OPEN_DATABASES.delete(workspaceName);
359
361
  throw error;
360
362
  });
@@ -394,7 +396,46 @@ function validateWorkspaceName(name) {
394
396
  throw new PragmaError("INVALID_WORKSPACE_NAME", 400, "Workspace name is reserved.");
395
397
  }
396
398
  }
397
- async function createWorkspaceDatabase(dbDir) {
399
+ async function openWorkspaceDatabase(paths) {
400
+ const remote = await tryConnectToWorkspaceDatabase(paths);
401
+ if (remote) {
402
+ return remote;
403
+ }
404
+ if (await acquireWorkspaceOwnerLock(paths)) {
405
+ try {
406
+ return await createOwnedWorkspaceDatabase(paths);
407
+ }
408
+ catch (error) {
409
+ await cleanupWorkspaceSocketFiles(paths);
410
+ throw error;
411
+ }
412
+ }
413
+ const waited = await waitForWorkspaceDatabase(paths);
414
+ if (waited) {
415
+ return waited;
416
+ }
417
+ throw new Error(`Timed out waiting for shared workspace database: ${paths.name}`);
418
+ }
419
+ async function createOwnedWorkspaceDatabase(paths) {
420
+ const db = await initializeWorkspaceDatabase(paths.dbDir);
421
+ const rawClose = db.close.bind(db);
422
+ const socketServer = new pglite_socket_1.PGLiteSocketServer({
423
+ db,
424
+ host: DB_SOCKET_HOST,
425
+ port: 0,
426
+ maxConnections: 25,
427
+ });
428
+ await socketServer.start();
429
+ const socketInfo = createSocketInfo(paths.name, socketServer.getServerConn());
430
+ await writeSocketInfo(paths, socketInfo);
431
+ const realClose = async () => {
432
+ await socketServer.stop().catch(() => { });
433
+ await cleanupWorkspaceSocketFiles(paths);
434
+ await rawClose();
435
+ };
436
+ return patchDatabaseClose(db, realClose);
437
+ }
438
+ async function initializeWorkspaceDatabase(dbDir) {
398
439
  const db = new pglite_1.PGlite(dbDir);
399
440
  await db.waitReady;
400
441
  await ensureRequiredSchema(db);
@@ -402,17 +443,209 @@ async function createWorkspaceDatabase(dbDir) {
402
443
  await ensureDefaultHuman(db);
403
444
  await ensureDefaultSkills(db);
404
445
  await (0, store_1.ensureConversationSchema)(db);
405
- return patchDatabaseClose(db);
446
+ return db;
406
447
  }
407
- function patchDatabaseClose(db) {
448
+ function patchDatabaseClose(db, realClose) {
408
449
  if (REAL_CLOSE.has(db)) {
409
450
  return db;
410
451
  }
411
- const realClose = db.close.bind(db);
412
452
  REAL_CLOSE.set(db, realClose);
413
453
  db.close = async () => { };
414
454
  return db;
415
455
  }
456
+ async function tryConnectToWorkspaceDatabase(paths) {
457
+ const socketInfo = await readSocketInfo(paths);
458
+ if (!socketInfo) {
459
+ return null;
460
+ }
461
+ try {
462
+ return await connectToSharedDatabase(socketInfo);
463
+ }
464
+ catch {
465
+ if (!isProcessAlive(socketInfo.ownerPid)) {
466
+ await cleanupWorkspaceSocketFiles(paths);
467
+ }
468
+ return null;
469
+ }
470
+ }
471
+ async function waitForWorkspaceDatabase(paths) {
472
+ const startedAt = Date.now();
473
+ while (Date.now() - startedAt < DB_SOCKET_WAIT_MS) {
474
+ const remote = await tryConnectToWorkspaceDatabase(paths);
475
+ if (remote) {
476
+ return remote;
477
+ }
478
+ const ownerLock = await readOwnerLock(paths);
479
+ if (!ownerLock) {
480
+ if (await acquireWorkspaceOwnerLock(paths)) {
481
+ try {
482
+ return await createOwnedWorkspaceDatabase(paths);
483
+ }
484
+ catch (error) {
485
+ await cleanupWorkspaceSocketFiles(paths);
486
+ throw error;
487
+ }
488
+ }
489
+ }
490
+ else if (!isProcessAlive(ownerLock.ownerPid)) {
491
+ await cleanupWorkspaceSocketFiles(paths);
492
+ if (await acquireWorkspaceOwnerLock(paths)) {
493
+ try {
494
+ return await createOwnedWorkspaceDatabase(paths);
495
+ }
496
+ catch (error) {
497
+ await cleanupWorkspaceSocketFiles(paths);
498
+ throw error;
499
+ }
500
+ }
501
+ }
502
+ await sleep(DB_SOCKET_POLL_MS);
503
+ }
504
+ return null;
505
+ }
506
+ async function connectToSharedDatabase(socketInfo) {
507
+ const client = new pg_1.Client({
508
+ host: socketInfo.host,
509
+ port: socketInfo.port,
510
+ database: "template1",
511
+ ssl: false,
512
+ });
513
+ client.on("error", () => { });
514
+ await client.connect();
515
+ await client.query("SELECT 1");
516
+ const remote = patchDatabaseClose(new RemotePGliteClient(client), async () => {
517
+ await client.end().catch(() => { });
518
+ });
519
+ return remote;
520
+ }
521
+ async function acquireWorkspaceOwnerLock(paths) {
522
+ const lockPath = getSocketLockPath(paths);
523
+ for (;;) {
524
+ try {
525
+ const payload = {
526
+ ownerPid: process.pid,
527
+ createdAt: new Date().toISOString(),
528
+ };
529
+ await (0, promises_1.writeFile)(lockPath, JSON.stringify(payload), { flag: "wx" });
530
+ return true;
531
+ }
532
+ catch (error) {
533
+ if (!isNodeError(error) || error.code !== "EEXIST") {
534
+ throw error;
535
+ }
536
+ const lock = await readOwnerLock(paths);
537
+ if (!lock || !isProcessAlive(lock.ownerPid)) {
538
+ await (0, promises_1.rm)(lockPath, { force: true });
539
+ continue;
540
+ }
541
+ return false;
542
+ }
543
+ }
544
+ }
545
+ async function readSocketInfo(paths) {
546
+ try {
547
+ const raw = await (0, promises_1.readFile)(getSocketInfoPath(paths), "utf8");
548
+ const parsed = JSON.parse(raw);
549
+ if (parsed.workspaceName !== paths.name ||
550
+ typeof parsed.host !== "string" ||
551
+ !Number.isInteger(parsed.port) ||
552
+ parsed.port <= 0 ||
553
+ typeof parsed.ownerPid !== "number") {
554
+ return null;
555
+ }
556
+ return {
557
+ workspaceName: parsed.workspaceName,
558
+ host: parsed.host,
559
+ port: Number(parsed.port),
560
+ ownerPid: parsed.ownerPid,
561
+ startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : "",
562
+ };
563
+ }
564
+ catch {
565
+ return null;
566
+ }
567
+ }
568
+ async function writeSocketInfo(paths, socketInfo) {
569
+ await (0, promises_1.writeFile)(getSocketInfoPath(paths), JSON.stringify(socketInfo));
570
+ }
571
+ async function readOwnerLock(paths) {
572
+ try {
573
+ const raw = await (0, promises_1.readFile)(getSocketLockPath(paths), "utf8");
574
+ const parsed = JSON.parse(raw);
575
+ if (typeof parsed.ownerPid !== "number") {
576
+ return null;
577
+ }
578
+ return {
579
+ ownerPid: parsed.ownerPid,
580
+ createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : "",
581
+ };
582
+ }
583
+ catch {
584
+ return null;
585
+ }
586
+ }
587
+ async function cleanupWorkspaceSocketFiles(paths) {
588
+ await (0, promises_1.rm)(getSocketInfoPath(paths), { force: true });
589
+ await (0, promises_1.rm)(getSocketLockPath(paths), { force: true });
590
+ }
591
+ function getSocketInfoPath(paths) {
592
+ return (0, node_path_1.join)(paths.rootDir, ".db-socket.json");
593
+ }
594
+ function getSocketLockPath(paths) {
595
+ return (0, node_path_1.join)(paths.rootDir, ".db-socket.lock");
596
+ }
597
+ function createSocketInfo(workspaceName, serverConn) {
598
+ const [host, portValue] = serverConn.split(":");
599
+ const port = Number.parseInt(portValue ?? "", 10);
600
+ if (!host || !Number.isInteger(port) || port <= 0) {
601
+ throw new Error(`Invalid PGlite socket server address: ${serverConn}`);
602
+ }
603
+ return {
604
+ workspaceName,
605
+ host,
606
+ port,
607
+ ownerPid: process.pid,
608
+ startedAt: new Date().toISOString(),
609
+ };
610
+ }
611
+ function isProcessAlive(pid) {
612
+ if (!Number.isInteger(pid) || pid <= 0) {
613
+ return false;
614
+ }
615
+ try {
616
+ process.kill(pid, 0);
617
+ return true;
618
+ }
619
+ catch {
620
+ return false;
621
+ }
622
+ }
623
+ function isNodeError(error) {
624
+ return typeof error === "object" && error !== null && "code" in error;
625
+ }
626
+ function sleep(ms) {
627
+ return new Promise((resolve) => setTimeout(resolve, ms));
628
+ }
629
+ class RemotePGliteClient {
630
+ constructor(client) {
631
+ this.client = client;
632
+ }
633
+ async query(sql, params) {
634
+ const result = await this.client.query(sql, params);
635
+ return {
636
+ rows: result.rows,
637
+ fields: result.fields,
638
+ rowCount: result.rowCount ?? 0,
639
+ affectedRows: result.rowCount ?? 0,
640
+ };
641
+ }
642
+ async exec(sql) {
643
+ await this.client.query(sql);
644
+ }
645
+ async close() {
646
+ await this.client.end();
647
+ }
648
+ }
416
649
  async function ensureRequiredSchema(db) {
417
650
  await ensureTaskStatusEnumType(db);
418
651
  await db.exec(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pragma-so",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "Very minimal pragma-so CLI",
5
5
  "main": "dist/cli/index.js",
6
6
  "bin": {
@@ -8,7 +8,6 @@
8
8
  },
9
9
  "scripts": {
10
10
  "build": "tsc && npm run ui:build",
11
- "dev": "node scripts/dev-start.mjs",
12
11
  "prepack": "npm run build",
13
12
  "start": "node dist/cli/index.js",
14
13
  "ui:dev": "vite --config ui/vite.config.mjs ui",
@@ -29,6 +28,7 @@
29
28
  "type": "commonjs",
30
29
  "dependencies": {
31
30
  "@electric-sql/pglite": "^0.3.16",
31
+ "@electric-sql/pglite-socket": "^0.0.22",
32
32
  "@emoji-mart/data": "^1.2.1",
33
33
  "@emoji-mart/react": "^1.1.1",
34
34
  "@hono/node-server": "^1.19.11",
@@ -44,6 +44,7 @@
44
44
  "mime-types": "^3.0.2",
45
45
  "open": "^8.4.2",
46
46
  "papaparse": "^5.5.3",
47
+ "pg": "^8.20.0",
47
48
  "react": "^18.3.1",
48
49
  "react-dom": "^18.3.1",
49
50
  "react-markdown": "^10.1.0",
@@ -52,6 +53,7 @@
52
53
  },
53
54
  "devDependencies": {
54
55
  "@types/node": "^25.4.0",
56
+ "@types/pg": "^8.18.0",
55
57
  "@vitejs/plugin-react": "^4.3.1",
56
58
  "typescript": "^5.9.3",
57
59
  "vite": "^5.4.11"