swarm-mail 0.1.4 → 0.2.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.
@@ -0,0 +1,304 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * swarm-mail-daemon CLI
4
+ *
5
+ * Command-line interface for managing the swarm-mail pglite-server daemon.
6
+ *
7
+ * Commands:
8
+ * start [options] - Start the daemon
9
+ * stop - Stop the daemon
10
+ * status - Show daemon status
11
+ *
12
+ * @example
13
+ * ```bash
14
+ * # Start daemon on default port
15
+ * swarm-mail-daemon start
16
+ *
17
+ * # Start with custom port
18
+ * swarm-mail-daemon start --port 5555
19
+ *
20
+ * # Start with Unix socket
21
+ * swarm-mail-daemon start --path /tmp/swarm-mail.sock
22
+ *
23
+ * # Check status
24
+ * swarm-mail-daemon status
25
+ *
26
+ * # Stop daemon
27
+ * swarm-mail-daemon stop
28
+ * ```
29
+ */
30
+
31
+ import { existsSync } from "node:fs";
32
+ import { readFile } from "node:fs/promises";
33
+ import { parseArgs } from "node:util";
34
+ import {
35
+ type DaemonOptions,
36
+ getPidFilePath,
37
+ healthCheck,
38
+ isDaemonRunning,
39
+ startDaemon,
40
+ stopDaemon,
41
+ } from "../src/daemon";
42
+
43
+ // Colors for terminal output
44
+ const colors = {
45
+ reset: "\x1b[0m",
46
+ red: "\x1b[31m",
47
+ green: "\x1b[32m",
48
+ yellow: "\x1b[33m",
49
+ blue: "\x1b[34m",
50
+ dim: "\x1b[2m",
51
+ bold: "\x1b[1m",
52
+ };
53
+
54
+ function success(msg: string) {
55
+ console.log(`${colors.green}✓${colors.reset} ${msg}`);
56
+ }
57
+
58
+ function error(msg: string) {
59
+ console.error(`${colors.red}✗${colors.reset} ${msg}`);
60
+ }
61
+
62
+ function info(msg: string) {
63
+ console.log(`${colors.blue}ℹ${colors.reset} ${msg}`);
64
+ }
65
+
66
+ function showHelp() {
67
+ console.log(`
68
+ ${colors.bold}swarm-mail-daemon${colors.reset} - Manage pglite-server daemon for swarm-mail
69
+
70
+ ${colors.bold}USAGE${colors.reset}
71
+ swarm-mail-daemon <command> [options]
72
+
73
+ ${colors.bold}COMMANDS${colors.reset}
74
+ start [options] Start the daemon
75
+ stop Stop the daemon
76
+ status Show daemon status
77
+
78
+ ${colors.bold}START OPTIONS${colors.reset}
79
+ --port <number> TCP port to bind (default: 5433)
80
+ --host <string> Host to bind (default: 127.0.0.1)
81
+ --path <string> Unix socket path (alternative to port/host)
82
+ --db <string> Database path (default: .opencode/streams or ~/.opencode/streams)
83
+ --project <string> Project path for PID file location
84
+
85
+ ${colors.bold}EXAMPLES${colors.reset}
86
+ # Start daemon on default port
87
+ swarm-mail-daemon start
88
+
89
+ # Start with custom port
90
+ swarm-mail-daemon start --port 5555
91
+
92
+ # Start with Unix socket
93
+ swarm-mail-daemon start --path /tmp/swarm-mail.sock
94
+
95
+ # Start with custom database path
96
+ swarm-mail-daemon start --db /custom/db/path
97
+
98
+ # Check status
99
+ swarm-mail-daemon status
100
+
101
+ # Stop daemon
102
+ swarm-mail-daemon stop
103
+ `);
104
+ }
105
+
106
+ /**
107
+ * Read PID from PID file
108
+ */
109
+ async function readPid(projectPath?: string): Promise<number | null> {
110
+ const pidFilePath = getPidFilePath(projectPath);
111
+ if (!existsSync(pidFilePath)) {
112
+ return null;
113
+ }
114
+ try {
115
+ const content = await readFile(pidFilePath, "utf-8");
116
+ const pid = Number.parseInt(content.trim(), 10);
117
+ if (Number.isNaN(pid) || pid <= 0) {
118
+ return null;
119
+ }
120
+ return pid;
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Start command handler
128
+ */
129
+ async function startCommand(args: string[]): Promise<void> {
130
+ const { values } = parseArgs({
131
+ args,
132
+ options: {
133
+ port: { type: "string", short: "p" },
134
+ host: { type: "string", short: "h" },
135
+ path: { type: "string" },
136
+ db: { type: "string" },
137
+ project: { type: "string" },
138
+ help: { type: "boolean" },
139
+ },
140
+ });
141
+
142
+ if (values.help) {
143
+ showHelp();
144
+ return;
145
+ }
146
+
147
+ const options: DaemonOptions = {
148
+ port: values.port ? Number.parseInt(values.port, 10) : undefined,
149
+ host: values.host,
150
+ path: values.path,
151
+ dbPath: values.db,
152
+ projectPath: values.project,
153
+ };
154
+
155
+ try {
156
+ // Check if already running
157
+ if (await isDaemonRunning(options.projectPath)) {
158
+ const pid = await readPid(options.projectPath);
159
+ const connInfo = options.path
160
+ ? `socket=${options.path}`
161
+ : `port=${options.port || 5433}`;
162
+ info(`Daemon already running (PID: ${pid}, ${connInfo})`);
163
+ return;
164
+ }
165
+
166
+ info("Starting daemon...");
167
+ const daemonInfo = await startDaemon(options);
168
+
169
+ const connInfo = daemonInfo.socketPath
170
+ ? `socket=${daemonInfo.socketPath}`
171
+ : `port=${daemonInfo.port}`;
172
+ success(`Daemon started (PID: ${daemonInfo.pid}, ${connInfo})`);
173
+ } catch (err) {
174
+ error(
175
+ `Failed to start daemon: ${err instanceof Error ? err.message : String(err)}`,
176
+ );
177
+ process.exit(1);
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Stop command handler
183
+ */
184
+ async function stopCommand(args: string[]): Promise<void> {
185
+ const { values } = parseArgs({
186
+ args,
187
+ options: {
188
+ project: { type: "string" },
189
+ help: { type: "boolean" },
190
+ },
191
+ });
192
+
193
+ if (values.help) {
194
+ showHelp();
195
+ return;
196
+ }
197
+
198
+ try {
199
+ const pid = await readPid(values.project);
200
+
201
+ if (!pid || !(await isDaemonRunning(values.project))) {
202
+ info("Daemon is not running");
203
+ return;
204
+ }
205
+
206
+ info(`Stopping daemon (PID: ${pid})...`);
207
+ await stopDaemon(values.project);
208
+ success("Daemon stopped");
209
+ } catch (err) {
210
+ error(
211
+ `Failed to stop daemon: ${err instanceof Error ? err.message : String(err)}`,
212
+ );
213
+ process.exit(1);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Status command handler
219
+ */
220
+ async function statusCommand(args: string[]): Promise<void> {
221
+ const { values } = parseArgs({
222
+ args,
223
+ options: {
224
+ port: { type: "string", short: "p" },
225
+ host: { type: "string", short: "h" },
226
+ path: { type: "string" },
227
+ project: { type: "string" },
228
+ help: { type: "boolean" },
229
+ },
230
+ });
231
+
232
+ if (values.help) {
233
+ showHelp();
234
+ return;
235
+ }
236
+
237
+ const projectPath = values.project;
238
+ const pid = await readPid(projectPath);
239
+ const running = await isDaemonRunning(projectPath);
240
+
241
+ if (!running) {
242
+ console.log(`${colors.bold}Status:${colors.reset} ${colors.red}Stopped${colors.reset}`);
243
+ console.log(`${colors.bold}PID File:${colors.reset} ${getPidFilePath(projectPath)}`);
244
+ return;
245
+ }
246
+
247
+ // Daemon is running - check health
248
+ const port = values.port ? Number.parseInt(values.port, 10) : 5433;
249
+ const host = values.host || "127.0.0.1";
250
+ const path = values.path;
251
+
252
+ const healthOptions = path ? { path } : { port, host };
253
+ const healthy = await healthCheck(healthOptions);
254
+
255
+ console.log(
256
+ `${colors.bold}Status:${colors.reset} ${colors.green}Running${colors.reset}`,
257
+ );
258
+ console.log(`${colors.bold}PID:${colors.reset} ${pid}`);
259
+ console.log(`${colors.bold}PID File:${colors.reset} ${getPidFilePath(projectPath)}`);
260
+
261
+ if (path) {
262
+ console.log(`${colors.bold}Socket:${colors.reset} ${path}`);
263
+ } else {
264
+ console.log(`${colors.bold}Host:${colors.reset} ${host}`);
265
+ console.log(`${colors.bold}Port:${colors.reset} ${port}`);
266
+ }
267
+
268
+ console.log(
269
+ `${colors.bold}Health:${colors.reset} ${healthy ? `${colors.green}OK${colors.reset}` : `${colors.red}Failed${colors.reset}`}`,
270
+ );
271
+ }
272
+
273
+ /**
274
+ * Main CLI entrypoint
275
+ */
276
+ async function main() {
277
+ const [command, ...args] = process.argv.slice(2);
278
+
279
+ if (!command || command === "help" || command === "--help" || command === "-h") {
280
+ showHelp();
281
+ process.exit(0);
282
+ }
283
+
284
+ switch (command) {
285
+ case "start":
286
+ await startCommand(args);
287
+ break;
288
+ case "stop":
289
+ await stopCommand(args);
290
+ break;
291
+ case "status":
292
+ await statusCommand(args);
293
+ break;
294
+ default:
295
+ error(`Unknown command: ${command}`);
296
+ showHelp();
297
+ process.exit(1);
298
+ }
299
+ }
300
+
301
+ main().catch((err) => {
302
+ error(`Unexpected error: ${err instanceof Error ? err.message : String(err)}`);
303
+ process.exit(1);
304
+ });
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Daemon Lifecycle Management for pglite-server
3
+ *
4
+ * Provides start/stop/health functionality for the pglite-server daemon process.
5
+ * Uses detached child_process for background operation with PID file tracking.
6
+ *
7
+ * ## Usage
8
+ * ```typescript
9
+ * import { startDaemon, stopDaemon, isDaemonRunning, healthCheck } from 'swarm-mail/daemon';
10
+ *
11
+ * // Start daemon
12
+ * const { pid, port } = await startDaemon({ port: 5433 });
13
+ *
14
+ * // Check health
15
+ * const healthy = await healthCheck({ port: 5433 });
16
+ *
17
+ * // Stop daemon
18
+ * await stopDaemon('/path/to/project');
19
+ * ```
20
+ */
21
+ /**
22
+ * Daemon start options
23
+ */
24
+ export interface DaemonOptions {
25
+ /** TCP port to bind (default: 5433) */
26
+ port?: number;
27
+ /** Host to bind (default: 127.0.0.1) */
28
+ host?: string;
29
+ /** Unix socket path (alternative to port/host) */
30
+ path?: string;
31
+ /** Database path (default: project .opencode/streams or ~/.opencode/streams) */
32
+ dbPath?: string;
33
+ /** Project path for PID file location (default: global ~/.opencode) */
34
+ projectPath?: string;
35
+ }
36
+ /**
37
+ * Daemon info returned by startDaemon
38
+ */
39
+ export interface DaemonInfo {
40
+ /** Process ID */
41
+ pid: number;
42
+ /** TCP port (if using TCP) */
43
+ port?: number;
44
+ /** Unix socket path (if using socket) */
45
+ socketPath?: string;
46
+ }
47
+ /**
48
+ * Get PID file path for a project
49
+ *
50
+ * Stores PID file in $TMPDIR alongside the streams database.
51
+ * Path format: `$TMPDIR/opencode-<project-name>-<hash>/pglite-server.pid`
52
+ * Falls back to global `$TMPDIR/opencode-global/pglite-server.pid`
53
+ *
54
+ * @param projectPath - Optional project root path
55
+ * @returns Absolute path to PID file
56
+ */
57
+ export declare function getPidFilePath(projectPath?: string): string;
58
+ /**
59
+ * Check if daemon is running
60
+ *
61
+ * Checks both PID file existence and process liveness.
62
+ *
63
+ * @param projectPath - Optional project root path
64
+ * @returns true if daemon is running
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * if (!await isDaemonRunning()) {
69
+ * await startDaemon();
70
+ * }
71
+ * ```
72
+ */
73
+ export declare function isDaemonRunning(projectPath?: string): Promise<boolean>;
74
+ /**
75
+ * Health check - verify daemon is responding
76
+ *
77
+ * Connects to the daemon and runs SELECT 1 query.
78
+ * Times out after 5 seconds.
79
+ *
80
+ * @param options - Connection options (port/host or path)
81
+ * @returns true if daemon is healthy
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * const healthy = await healthCheck({ port: 5433 });
86
+ * if (!healthy) {
87
+ * console.error('Daemon not responding');
88
+ * }
89
+ * ```
90
+ */
91
+ export declare function healthCheck(options: Pick<DaemonOptions, "port" | "host" | "path">): Promise<boolean>;
92
+ /**
93
+ * Start pglite-server daemon
94
+ *
95
+ * Spawns pglite-server as a detached background process.
96
+ * Writes PID file and waits for server to be ready via health check.
97
+ *
98
+ * If daemon is already running, returns existing daemon info.
99
+ *
100
+ * @param options - Daemon configuration
101
+ * @returns Daemon info (PID and connection details)
102
+ * @throws Error if daemon fails to start
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * // Start with TCP port
107
+ * const { pid, port } = await startDaemon({ port: 5433 });
108
+ *
109
+ * // Start with Unix socket
110
+ * const { pid, socketPath } = await startDaemon({
111
+ * path: '/tmp/swarm-mail-pglite.sock'
112
+ * });
113
+ *
114
+ * // Start with custom database path
115
+ * const { pid, port } = await startDaemon({
116
+ * port: 5433,
117
+ * dbPath: '/custom/path/to/db'
118
+ * });
119
+ * ```
120
+ */
121
+ export declare function startDaemon(options?: DaemonOptions): Promise<DaemonInfo>;
122
+ /**
123
+ * Stop pglite-server daemon
124
+ *
125
+ * Sends SIGTERM to the daemon process and waits for clean shutdown.
126
+ * Cleans up PID file after process exits.
127
+ *
128
+ * If daemon is not running, this is a no-op (not an error).
129
+ *
130
+ * @param projectPath - Optional project root path
131
+ * @throws Error if daemon doesn't stop within timeout
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * await stopDaemon('/path/to/project');
136
+ * ```
137
+ */
138
+ export declare function stopDaemon(projectPath?: string): Promise<void>;
139
+ //# sourceMappingURL=daemon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AASH;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,uCAAuC;IACvC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,iBAAiB;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,8BAA8B;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAkB3D;AAqGD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAM5E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC,GACrD,OAAO,CAAC,OAAO,CAAC,CA2BlB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,WAAW,CAC/B,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,UAAU,CAAC,CAgErB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,UAAU,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4CpE"}
package/dist/index.d.ts CHANGED
@@ -17,7 +17,11 @@
17
17
  export declare const SWARM_MAIL_VERSION = "0.1.0";
18
18
  export { createSwarmMailAdapter } from "./adapter";
19
19
  export type { DatabaseAdapter, SwarmMailAdapter, EventStoreAdapter, AgentAdapter, MessagingAdapter, ReservationAdapter, SchemaAdapter, ReadEventsOptions, InboxOptions, Message, Reservation, Conflict, } from "./types";
20
- export { getSwarmMail, createInMemorySwarmMail, closeSwarmMail, closeAllSwarmMail, getDatabasePath, PGlite, } from "./pglite";
20
+ export { getSwarmMail, getSwarmMailSocket, createInMemorySwarmMail, closeSwarmMail, closeAllSwarmMail, getDatabasePath, getProjectTempDirName, hashProjectPath, PGlite, } from "./pglite";
21
+ export { wrapPostgres, createSocketAdapter, } from "./socket-adapter";
22
+ export type { SocketAdapterOptions } from "./socket-adapter";
21
23
  export * from "./streams";
22
24
  export * from "./beads";
25
+ export { startDaemon, stopDaemon, isDaemonRunning, healthCheck, getPidFilePath, } from "./daemon";
26
+ export type { DaemonOptions, DaemonInfo } from "./daemon";
23
27
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,kBAAkB,UAAU,CAAC;AAM1C,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACnD,YAAY,EACV,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,OAAO,EACP,WAAW,EACX,QAAQ,GACT,MAAM,SAAS,CAAC;AAMjB,OAAO,EACL,YAAY,EACZ,uBAAuB,EACvB,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,MAAM,GACP,MAAM,UAAU,CAAC;AAMlB,cAAc,WAAW,CAAC;AAM1B,cAAc,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,kBAAkB,UAAU,CAAC;AAM1C,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACnD,YAAY,EACV,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,OAAO,EACP,WAAW,EACX,QAAQ,GACT,MAAM,SAAS,CAAC;AAMjB,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,uBAAuB,EACvB,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,qBAAqB,EACrB,eAAe,EACf,MAAM,GACP,MAAM,UAAU,CAAC;AAMlB,OAAO,EACL,YAAY,EACZ,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAM7D,cAAc,WAAW,CAAC;AAM1B,cAAc,SAAS,CAAC;AAMxB,OAAO,EACL,WAAW,EACX,UAAU,EACV,eAAe,EACf,WAAW,EACX,cAAc,GACf,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC"}