@outfitter/daemon 0.2.0 → 0.2.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.
@@ -0,0 +1,2 @@
1
+ import { ConnectionRefusedError, ConnectionTimeoutError, DaemonConnectionError, LockError, ProtocolError, StaleSocketError } from "./shared/@outfitter/daemon-8tpctdgw";
2
+ export { StaleSocketError, ProtocolError, LockError, DaemonConnectionError, ConnectionTimeoutError, ConnectionRefusedError };
package/dist/errors.js ADDED
@@ -0,0 +1,15 @@
1
+ // @bun
2
+ import {
3
+ ConnectionRefusedError,
4
+ ConnectionTimeoutError,
5
+ LockError,
6
+ ProtocolError,
7
+ StaleSocketError
8
+ } from "./shared/@outfitter/daemon-qqn2jpsg.js";
9
+ export {
10
+ StaleSocketError,
11
+ ProtocolError,
12
+ LockError,
13
+ ConnectionTimeoutError,
14
+ ConnectionRefusedError
15
+ };
@@ -0,0 +1,2 @@
1
+ import { HealthCheck, HealthCheckResult, HealthChecker, HealthStatus, createHealthChecker } from "./shared/@outfitter/daemon-3qezw7hc";
2
+ export { createHealthChecker, HealthStatus, HealthChecker, HealthCheckResult, HealthCheck };
package/dist/health.js ADDED
@@ -0,0 +1,7 @@
1
+ // @bun
2
+ import {
3
+ createHealthChecker
4
+ } from "./shared/@outfitter/daemon-c1zbfqq5.js";
5
+ export {
6
+ createHealthChecker
7
+ };
package/dist/ipc.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { IpcClient, IpcMessageHandler, IpcServer, createIpcClient, createIpcServer } from "./shared/@outfitter/daemon-6efpehdg";
2
+ export { createIpcServer, createIpcClient, IpcServer, IpcMessageHandler, IpcClient };
package/dist/ipc.js ADDED
@@ -0,0 +1,9 @@
1
+ // @bun
2
+ import {
3
+ createIpcClient,
4
+ createIpcServer
5
+ } from "./shared/@outfitter/daemon-cy5wntm2.js";
6
+ export {
7
+ createIpcServer,
8
+ createIpcClient
9
+ };
@@ -0,0 +1,3 @@
1
+ import { createDaemon } from "./shared/@outfitter/daemon-bd6kcdnj";
2
+ import "./shared/@outfitter/daemon-9w2ey87r";
3
+ export { createDaemon };
@@ -0,0 +1,8 @@
1
+ // @bun
2
+ import {
3
+ createDaemon
4
+ } from "./shared/@outfitter/daemon-3j14csts.js";
5
+ import"./shared/@outfitter/daemon-dzt3fqvp.js";
6
+ export {
7
+ createDaemon
8
+ };
@@ -0,0 +1,3 @@
1
+ import { LockHandle, acquireDaemonLock, isDaemonAlive, isProcessAlive, readLockPid, releaseDaemonLock } from "./shared/@outfitter/daemon-h536nv4k";
2
+ import "./shared/@outfitter/daemon-8tpctdgw";
3
+ export { releaseDaemonLock, readLockPid, isProcessAlive, isDaemonAlive, acquireDaemonLock, LockHandle };
@@ -0,0 +1,16 @@
1
+ // @bun
2
+ import {
3
+ acquireDaemonLock,
4
+ isDaemonAlive,
5
+ isProcessAlive,
6
+ readLockPid,
7
+ releaseDaemonLock
8
+ } from "./shared/@outfitter/daemon-906e24jc.js";
9
+ import"./shared/@outfitter/daemon-qqn2jpsg.js";
10
+ export {
11
+ releaseDaemonLock,
12
+ readLockPid,
13
+ isProcessAlive,
14
+ isDaemonAlive,
15
+ acquireDaemonLock
16
+ };
@@ -0,0 +1,2 @@
1
+ import { getDaemonDir, getLockPath, getPidPath, getSocketPath, isUnixPlatform } from "./shared/@outfitter/daemon-5as2p88e";
2
+ export { isUnixPlatform, getSocketPath, getPidPath, getLockPath, getDaemonDir };
@@ -0,0 +1,15 @@
1
+ // @bun
2
+ import {
3
+ getDaemonDir,
4
+ getLockPath,
5
+ getPidPath,
6
+ getSocketPath,
7
+ isUnixPlatform
8
+ } from "./shared/@outfitter/daemon-wz4peqjh.js";
9
+ export {
10
+ isUnixPlatform,
11
+ getSocketPath,
12
+ getPidPath,
13
+ getLockPath,
14
+ getDaemonDir
15
+ };
@@ -0,0 +1,180 @@
1
+ // @bun
2
+ import {
3
+ DaemonError
4
+ } from "./daemon-dzt3fqvp.js";
5
+
6
+ // packages/daemon/src/lifecycle.ts
7
+ import { mkdir, unlink, writeFile } from "fs/promises";
8
+ import { dirname } from "path";
9
+ import { Result } from "@outfitter/contracts";
10
+ function pidFileExists(path) {
11
+ return Bun.file(path).exists();
12
+ }
13
+ async function writePidFile(path, pid) {
14
+ try {
15
+ const dir = dirname(path);
16
+ await mkdir(dir, { recursive: true });
17
+ await writeFile(path, String(pid), { flag: "wx" });
18
+ return Result.ok(undefined);
19
+ } catch (error) {
20
+ return Result.err(new DaemonError({
21
+ code: "PID_ERROR",
22
+ message: `Failed to write PID file: ${error instanceof Error ? error.message : "Unknown error"}`
23
+ }));
24
+ }
25
+ }
26
+ async function removePidFile(path) {
27
+ try {
28
+ await unlink(path);
29
+ return Result.ok(undefined);
30
+ } catch (error) {
31
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
32
+ return Result.ok(undefined);
33
+ }
34
+ return Result.err(new DaemonError({
35
+ code: "PID_ERROR",
36
+ message: `Failed to remove PID file: ${error instanceof Error ? error.message : "Unknown error"}`
37
+ }));
38
+ }
39
+ }
40
+ async function runShutdownHandlers(handlers, timeout, logger) {
41
+ const errors = [];
42
+ const runHandlers = async () => {
43
+ for (const handler of handlers) {
44
+ try {
45
+ await handler();
46
+ } catch (error) {
47
+ const err = error instanceof Error ? error : new Error(String(error));
48
+ errors.push(err);
49
+ logger?.warn("Shutdown handler failed", { error: err.message });
50
+ }
51
+ }
52
+ };
53
+ const timeoutPromise = new Promise((resolve) => {
54
+ setTimeout(() => resolve("timeout"), timeout);
55
+ });
56
+ const result = await Promise.race([
57
+ runHandlers().then(() => "completed"),
58
+ timeoutPromise
59
+ ]);
60
+ return {
61
+ completed: result === "completed",
62
+ errors
63
+ };
64
+ }
65
+ function createDaemon(options) {
66
+ const internalState = {
67
+ state: "stopped",
68
+ options: {
69
+ name: options.name,
70
+ pidFile: options.pidFile,
71
+ logger: options.logger,
72
+ shutdownTimeout: options.shutdownTimeout ?? 5000
73
+ },
74
+ shutdownHandlers: [],
75
+ signalHandlers: {},
76
+ isShuttingDown: false
77
+ };
78
+ async function doStop() {
79
+ const { logger } = internalState.options;
80
+ if (internalState.isShuttingDown) {
81
+ return Result.ok(undefined);
82
+ }
83
+ if (internalState.state === "stopped") {
84
+ return Result.err(new DaemonError({
85
+ code: "NOT_RUNNING",
86
+ message: `Daemon "${internalState.options.name}" is not running`
87
+ }));
88
+ }
89
+ internalState.isShuttingDown = true;
90
+ internalState.state = "stopping";
91
+ logger?.info("Daemon stopping", { name: internalState.options.name });
92
+ const { completed } = await runShutdownHandlers(internalState.shutdownHandlers, internalState.options.shutdownTimeout, logger);
93
+ if (!completed) {
94
+ logger?.warn("Shutdown handlers timed out", {
95
+ name: internalState.options.name,
96
+ timeout: internalState.options.shutdownTimeout
97
+ });
98
+ }
99
+ if (internalState.signalHandlers.sigterm) {
100
+ process.off("SIGTERM", internalState.signalHandlers.sigterm);
101
+ }
102
+ if (internalState.signalHandlers.sigint) {
103
+ process.off("SIGINT", internalState.signalHandlers.sigint);
104
+ }
105
+ const removeResult = await removePidFile(internalState.options.pidFile);
106
+ if (removeResult.isErr()) {
107
+ logger?.error("Failed to remove PID file", {
108
+ error: removeResult.error.message
109
+ });
110
+ }
111
+ internalState.state = "stopped";
112
+ internalState.isShuttingDown = false;
113
+ logger?.info("Daemon stopped", { name: internalState.options.name });
114
+ if (!completed) {
115
+ return Result.err(new DaemonError({
116
+ code: "SHUTDOWN_TIMEOUT",
117
+ message: `Shutdown handlers exceeded timeout of ${internalState.options.shutdownTimeout}ms`
118
+ }));
119
+ }
120
+ return Result.ok(undefined);
121
+ }
122
+ const daemon = {
123
+ get state() {
124
+ return internalState.state;
125
+ },
126
+ async start() {
127
+ const { logger } = internalState.options;
128
+ if (internalState.state !== "stopped") {
129
+ return Result.err(new DaemonError({
130
+ code: "ALREADY_RUNNING",
131
+ message: `Daemon "${internalState.options.name}" is already running`
132
+ }));
133
+ }
134
+ internalState.state = "starting";
135
+ logger?.info("Daemon starting", { name: internalState.options.name });
136
+ if (await pidFileExists(internalState.options.pidFile)) {
137
+ internalState.state = "stopped";
138
+ return Result.err(new DaemonError({
139
+ code: "ALREADY_RUNNING",
140
+ message: `PID file already exists: ${internalState.options.pidFile}`
141
+ }));
142
+ }
143
+ const writeResult = await writePidFile(internalState.options.pidFile, process.pid);
144
+ if (writeResult.isErr()) {
145
+ internalState.state = "stopped";
146
+ return writeResult;
147
+ }
148
+ const sigTermHandler = () => {
149
+ logger?.info("Received SIGTERM signal");
150
+ doStop();
151
+ };
152
+ const sigIntHandler = () => {
153
+ logger?.info("Received SIGINT signal");
154
+ doStop();
155
+ };
156
+ internalState.signalHandlers.sigterm = sigTermHandler;
157
+ internalState.signalHandlers.sigint = sigIntHandler;
158
+ process.on("SIGTERM", sigTermHandler);
159
+ process.on("SIGINT", sigIntHandler);
160
+ internalState.state = "running";
161
+ logger?.info("Daemon started", {
162
+ name: internalState.options.name,
163
+ pid: process.pid
164
+ });
165
+ return Result.ok(undefined);
166
+ },
167
+ stop() {
168
+ return doStop();
169
+ },
170
+ isRunning() {
171
+ return internalState.state === "running";
172
+ },
173
+ onShutdown(handler) {
174
+ internalState.shutdownHandlers.push(handler);
175
+ }
176
+ };
177
+ return daemon;
178
+ }
179
+
180
+ export { createDaemon };
@@ -0,0 +1,154 @@
1
+ import { Result } from "@outfitter/contracts";
2
+ /**
3
+ * A single health check definition.
4
+ *
5
+ * Health checks are used to verify that a service or resource is functioning
6
+ * correctly. Each check has a name for identification and a check function
7
+ * that returns a Result indicating success or failure.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const databaseCheck: HealthCheck = {
12
+ * name: "database",
13
+ * check: async () => {
14
+ * try {
15
+ * await db.ping();
16
+ * return Result.ok(undefined);
17
+ * } catch (error) {
18
+ * return Result.err(error);
19
+ * }
20
+ * },
21
+ * };
22
+ * ```
23
+ */
24
+ interface HealthCheck {
25
+ /**
26
+ * Unique name identifying this health check.
27
+ * Used as the key in the HealthStatus.checks record.
28
+ */
29
+ name: string;
30
+ /**
31
+ * Function that performs the health check.
32
+ *
33
+ * Should return Result.ok(undefined) if healthy, or Result.err(error)
34
+ * with details about the failure.
35
+ */
36
+ check(): Promise<Result<void, Error>>;
37
+ }
38
+ /**
39
+ * Result of an individual health check.
40
+ *
41
+ * Contains the healthy/unhealthy status and an optional message
42
+ * providing more details (typically the error message on failure).
43
+ */
44
+ interface HealthCheckResult {
45
+ /** Whether this check passed (true) or failed (false) */
46
+ healthy: boolean;
47
+ /** Optional message, typically the error message on failure */
48
+ message?: string;
49
+ }
50
+ /**
51
+ * Aggregated health status from all registered checks.
52
+ *
53
+ * The overall healthy status is true only if ALL individual checks pass.
54
+ * Includes the uptime of the health checker in seconds.
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * const status: HealthStatus = {
59
+ * healthy: false,
60
+ * checks: {
61
+ * database: { healthy: true },
62
+ * cache: { healthy: false, message: "Connection refused" },
63
+ * },
64
+ * uptime: 3600,
65
+ * };
66
+ * ```
67
+ */
68
+ interface HealthStatus {
69
+ /** Overall health status - true only if ALL checks pass */
70
+ healthy: boolean;
71
+ /** Individual check results keyed by check name */
72
+ checks: Record<string, HealthCheckResult>;
73
+ /** Uptime in seconds since the health checker was created */
74
+ uptime: number;
75
+ }
76
+ /**
77
+ * Health checker interface for managing and running health checks.
78
+ *
79
+ * Provides methods to run all registered checks and get the aggregated
80
+ * health status, as well as dynamically registering new checks at runtime.
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * const checker = createHealthChecker([
85
+ * { name: "db", check: checkDatabase },
86
+ * { name: "cache", check: checkCache },
87
+ * ]);
88
+ *
89
+ * // Later, add more checks
90
+ * checker.register({ name: "queue", check: checkQueue });
91
+ *
92
+ * // Get health status
93
+ * const status = await checker.check();
94
+ * console.log("Healthy:", status.healthy);
95
+ * ```
96
+ */
97
+ interface HealthChecker {
98
+ /**
99
+ * Run all registered health checks and return aggregated status.
100
+ *
101
+ * Checks are run in parallel for efficiency. The overall healthy
102
+ * status is true only if all individual checks pass.
103
+ *
104
+ * @returns Aggregated health status
105
+ */
106
+ check(): Promise<HealthStatus>;
107
+ /**
108
+ * Register a new health check at runtime.
109
+ *
110
+ * The check will be included in all subsequent calls to check().
111
+ *
112
+ * @param check - Health check to register
113
+ */
114
+ register(check: HealthCheck): void;
115
+ }
116
+ /**
117
+ * Create a health checker with initial checks.
118
+ *
119
+ * The health checker runs all registered checks in parallel and aggregates
120
+ * their results. Individual check failures are isolated and don't prevent
121
+ * other checks from running.
122
+ *
123
+ * @param checks - Initial health checks to register
124
+ * @returns HealthChecker instance
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * const checker = createHealthChecker([
129
+ * {
130
+ * name: "database",
131
+ * check: async () => {
132
+ * await db.ping();
133
+ * return Result.ok(undefined);
134
+ * },
135
+ * },
136
+ * {
137
+ * name: "cache",
138
+ * check: async () => {
139
+ * await redis.ping();
140
+ * return Result.ok(undefined);
141
+ * },
142
+ * },
143
+ * ]);
144
+ *
145
+ * // Run health checks
146
+ * const status = await checker.check();
147
+ *
148
+ * if (!status.healthy) {
149
+ * console.error("Service unhealthy:", status.checks);
150
+ * }
151
+ * ```
152
+ */
153
+ declare function createHealthChecker(checks: HealthCheck[]): HealthChecker;
154
+ export { HealthCheck, HealthCheckResult, HealthStatus, HealthChecker, createHealthChecker };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Check if running on a Unix-like platform (macOS or Linux).
3
+ *
4
+ * @returns true on macOS/Linux, false on Windows
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * if (isUnixPlatform()) {
9
+ * // Use Unix domain sockets
10
+ * } else {
11
+ * // Use named pipes
12
+ * }
13
+ * ```
14
+ */
15
+ declare function isUnixPlatform(): boolean;
16
+ /**
17
+ * Get the Unix domain socket path for a tool's daemon.
18
+ *
19
+ * @param toolName - Name of the tool (e.g., "waymark", "firewatch")
20
+ * @returns Absolute path to the daemon socket
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const socketPath = getSocketPath("waymark");
25
+ * // "/run/user/1000/waymark/daemon.sock" on Linux
26
+ * // "/var/folders/.../waymark/daemon.sock" on macOS
27
+ * ```
28
+ */
29
+ declare function getSocketPath(toolName: string): string;
30
+ /**
31
+ * Get the lock file path for a tool's daemon.
32
+ *
33
+ * @param toolName - Name of the tool (e.g., "waymark", "firewatch")
34
+ * @returns Absolute path to the daemon lock file
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const lockPath = getLockPath("waymark");
39
+ * // "/run/user/1000/waymark/daemon.lock" on Linux
40
+ * ```
41
+ */
42
+ declare function getLockPath(toolName: string): string;
43
+ /**
44
+ * Get the PID file path for a tool's daemon.
45
+ *
46
+ * @param toolName - Name of the tool (e.g., "waymark", "firewatch")
47
+ * @returns Absolute path to the daemon PID file
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * const pidPath = getPidPath("waymark");
52
+ * // "/run/user/1000/waymark/daemon.pid" on Linux
53
+ * ```
54
+ */
55
+ declare function getPidPath(toolName: string): string;
56
+ /**
57
+ * Get the directory containing daemon files for a tool.
58
+ *
59
+ * @param toolName - Name of the tool (e.g., "waymark", "firewatch")
60
+ * @returns Absolute path to the daemon directory
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * const daemonDir = getDaemonDir("waymark");
65
+ * // "/run/user/1000/waymark" on Linux
66
+ * ```
67
+ */
68
+ declare function getDaemonDir(toolName: string): string;
69
+ export { isUnixPlatform, getSocketPath, getLockPath, getPidPath, getDaemonDir };
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Message handler type for processing incoming IPC messages.
3
+ *
4
+ * Receives a parsed message and returns a response to send back to the client.
5
+ * Throwing an error will result in an error response to the client.
6
+ */
7
+ type IpcMessageHandler = (message: unknown) => Promise<unknown>;
8
+ /**
9
+ * IPC server interface for receiving messages from clients.
10
+ *
11
+ * The server listens on a Unix socket and processes incoming messages
12
+ * using the registered message handler.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const server = createIpcServer("/var/run/my-daemon.sock");
17
+ *
18
+ * server.onMessage(async (msg) => {
19
+ * if (msg.type === "status") {
20
+ * return { status: "ok", uptime: process.uptime() };
21
+ * }
22
+ * return { error: "Unknown command" };
23
+ * });
24
+ *
25
+ * await server.listen();
26
+ * ```
27
+ */
28
+ interface IpcServer {
29
+ /**
30
+ * Start listening for connections on the Unix socket.
31
+ *
32
+ * Creates the socket file and begins accepting client connections.
33
+ * Messages are processed using the handler registered via onMessage.
34
+ */
35
+ listen(): Promise<void>;
36
+ /**
37
+ * Stop listening and close all connections.
38
+ *
39
+ * Removes the socket file and cleans up resources.
40
+ */
41
+ close(): Promise<void>;
42
+ /**
43
+ * Register a message handler for incoming messages.
44
+ *
45
+ * Only one handler can be registered. Calling this multiple times
46
+ * replaces the previous handler.
47
+ *
48
+ * @param handler - Function to process incoming messages
49
+ */
50
+ onMessage(handler: IpcMessageHandler): void;
51
+ }
52
+ /**
53
+ * IPC client interface for sending messages to a server.
54
+ *
55
+ * The client connects to a Unix socket and can send messages,
56
+ * receiving responses asynchronously.
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * const client = createIpcClient("/var/run/my-daemon.sock");
61
+ *
62
+ * await client.connect();
63
+ *
64
+ * const response = await client.send<StatusResponse>({ type: "status" });
65
+ * console.log("Daemon uptime:", response.uptime);
66
+ *
67
+ * client.close();
68
+ * ```
69
+ */
70
+ interface IpcClient {
71
+ /**
72
+ * Connect to the IPC server.
73
+ *
74
+ * Establishes a connection to the Unix socket. Throws if the
75
+ * server is not available.
76
+ */
77
+ connect(): Promise<void>;
78
+ /**
79
+ * Send a message and wait for a response.
80
+ *
81
+ * Serializes the message to JSON, sends it to the server, and
82
+ * waits for a response.
83
+ *
84
+ * @typeParam T - Expected response type
85
+ * @param message - Message to send (must be JSON-serializable)
86
+ * @returns Promise resolving to the server's response
87
+ * @throws Error if not connected or communication fails
88
+ */
89
+ send<T>(message: unknown): Promise<T>;
90
+ /**
91
+ * Close the connection to the server.
92
+ *
93
+ * Can be called multiple times safely.
94
+ */
95
+ close(): void;
96
+ }
97
+ /**
98
+ * Create an IPC server listening on a Unix socket.
99
+ *
100
+ * @param socketPath - Path to the Unix socket file
101
+ * @returns IpcServer instance
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * const server = createIpcServer("/var/run/my-daemon.sock");
106
+ *
107
+ * server.onMessage(async (msg) => {
108
+ * return { echo: msg };
109
+ * });
110
+ *
111
+ * await server.listen();
112
+ * // Server is now accepting connections
113
+ * ```
114
+ */
115
+ declare function createIpcServer(socketPath: string): IpcServer;
116
+ /**
117
+ * Create an IPC client for connecting to a server.
118
+ *
119
+ * @param socketPath - Path to the Unix socket file
120
+ * @returns IpcClient instance
121
+ *
122
+ * @example
123
+ * ```typescript
124
+ * const client = createIpcClient("/var/run/my-daemon.sock");
125
+ *
126
+ * await client.connect();
127
+ * const response = await client.send({ command: "status" });
128
+ * console.log(response);
129
+ * client.close();
130
+ * ```
131
+ */
132
+ declare function createIpcClient(socketPath: string): IpcClient;
133
+ export { IpcMessageHandler, IpcServer, IpcClient, createIpcServer, createIpcClient };