smithery/outfitter-dev

outfitter-daemon

Patterns for @outfitter/daemon including lifecycle management, IPC communication, health checks, and PID files. Use when building background services, daemons, or when "daemon", "IPC", "health check", "background service", or "@outfitter/daemon" are mentioned.

Installation

$ npx skills add smithery/outfitter-dev --skill outfitter-daemon

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/outfitter-dev · top by installs.

npx skills add smithery/outfitter-dev

Browse all from smithery/outfitter-dev

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Skill metadata

Parsed from SKILL.md frontmatter.

Version0.1.0
Allowed toolsRead Write Edit Glob Grep Bash(bun *)

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 8,534 B
  • docs SUMMARY.md 284 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

Daemon Patterns

Deep dive into @outfitter/daemon patterns.

Creating a Daemon

import { createDaemon, getPidPath } from "@outfitter/daemon";
import { createLogger } from "@outfitter/logging";

const logger = createLogger({ name: "my-daemon" });

const daemon = createDaemon({
  name: "my-daemon",
  pidFile: getPidPath("my-daemon"),
  logger,
  shutdownTimeout: 10000, // 10s graceful shutdown
});

Lifecycle

// Register shutdown handlers (called on SIGTERM, SIGINT, or daemon.stop())
daemon.onShutdown(async () => {
  logger.info("Shutting down...");
  await closeConnections();
  await flushBuffers();
});

// Start the daemon (writes PID file, registers signal handlers)
const result = await daemon.start();
if (result.isErr()) {
  logger.error("Failed to start", { error: result.error });
  process.exit(1);
}

Communication Patterns

Two approaches for daemon communication, each suited to different use cases.

HTTP on Unix Socket (recommended for scaffolds)

Simpler pattern using Bun.serve() on a Unix socket. Standard REST-style endpoints — any tool can curl them. Best for health checks, metrics, admin APIs.

import { getSocketPath } from "@outfitter/daemon";

const socketPath = getSocketPath("my-daemon");

const server = Bun.serve({
  unix: socketPath,
  fetch(request) {
    const url = new URL(request.url);

    if (url.pathname === "/health") {
      return Response.json({
        status: "ok",
        uptime: process.uptime(),
      });
    }

    if (url.pathname === "/shutdown" && request.method === "POST") {
      setTimeout(() => void daemon.stop(), 100);
      return new Response("Shutting down");
    }

    return new Response("Not found", { status: 404 });
  },
});

// Register server cleanup as a shutdown handler
daemon.onShutdown(async () => {
  server.stop();
});

CLI commands use fetch over the Unix socket:

// Stop via HTTP
await fetch(`http://unix:${socketPath}:/shutdown`, { method: "POST" });

// Health check via HTTP
const health = await fetch(`http://unix:${socketPath}:/health`);

IPC (structured messaging)

Lower-level pattern using createIpcServer/createIpcClient for typed bidirectional messaging over Unix sockets. Best for language servers, editor plugins, agent coordination, or high-frequency communication.

import { createIpcServer, getSocketPath } from "@outfitter/daemon";

const ipcServer = createIpcServer(getSocketPath("my-daemon"));

// Handle typed messages
ipcServer.onMessage(async (msg) => {
  const message = msg as { type: string; payload?: unknown };

  switch (message.type) {
    case "status":
      return { status: "ok", uptime: process.uptime() };
    case "reload":
      await reloadConfig();
      return { success: true };
    case "metrics":
      return getMetrics();
    default:
      return { error: "Unknown command" };
  }
});

// Register cleanup
daemon.onShutdown(async () => {
  await ipcServer.close();
});

// Start listening
await ipcServer.listen();

CLI commands use the IPC client:

import { createIpcClient, getSocketPath } from "@outfitter/daemon";

const client = createIpcClient(getSocketPath("my-daemon"));
await client.connect();

const status = await client.send<{ status: string }>({ type: "status" });
console.log("Status:", status);

client.close();

When to Use Each

Concern HTTP IPC
Simplicity Simpler — standard REST patterns More complex — message framing, correlation
External access Any tool can curl Requires the IPC client library
Type safety Untyped (parse JSON manually) Typed request/response protocol
Bidirectional Request/response only Full bidirectional messaging
Best for Admin APIs, health, metrics Language servers, editor plugins, agents

The scaffold daemon preset uses HTTP by default. Use --example ipc for the IPC pattern.

Health Checks

import { createHealthChecker } from "@outfitter/daemon";
import { Result } from "@outfitter/contracts";

const healthChecker = createHealthChecker([
  {
    name: "memory",
    check: async () => {
      const used = process.memoryUsage().heapUsed / 1024 / 1024;
      return used < 500
        ? Result.ok(undefined)
        : Result.err(new Error(`High memory: ${used.toFixed(2)}MB`));
    },
  },
  {
    name: "database",
    check: async () => {
      try {
        await db.ping();
        return Result.ok(undefined);
      } catch {
        return Result.err(new Error("Database unreachable"));
      }
    },
  },
]);

Exposing Health via HTTP

if (url.pathname === "/health") {
  const result = await healthChecker.check();
  return Response.json({
    healthy: result.isOk(),
    checks: result.isOk() ? result.value : result.error,
  });
}

PID File Management

XDG Paths

import { getPidPath, getSocketPath, getDaemonDir } from "@outfitter/daemon";

// PID file: ~/.local/state/my-daemon/my-daemon.pid
const pidPath = getPidPath("my-daemon");

// Socket: ~/.local/state/my-daemon/my-daemon.sock
const socketPath = getSocketPath("my-daemon");

// Daemon directory: ~/.local/state/my-daemon/
const daemonDir = getDaemonDir("my-daemon");

Checking if Running

import { isDaemonAlive, getPidPath } from "@outfitter/daemon";

const pidPath = getPidPath("my-daemon");
if (await isDaemonAlive(pidPath)) {
  console.log("Daemon already running");
  process.exit(1);
}

CLI Integration

Start Command

import { command } from "@outfitter/cli/command";
import { runHandler } from "@outfitter/cli/envelope";
import { ConflictError, Result } from "@outfitter/contracts";
import { getPidPath, isDaemonAlive } from "@outfitter/daemon";
import { spawn } from "node:child_process";

const pidPath = getPidPath("my-daemon");

command("start")
  .description("Start the daemon")
  .option("-f, --foreground", "Run in foreground")
  .action(async ({ flags }) => {
    const foreground = Boolean(flags["foreground"]);

    await runHandler({
      command: "start",
      input: { foreground },
      handler: async ({ foreground: fg }) => {
        if (await isDaemonAlive(pidPath)) {
          return Result.err(ConflictError.create("Daemon is already running"));
        }

        if (fg) {
          const { runDaemon } = await import("./daemon-main.js");
          await runDaemon();
          return Result.ok({ status: "exited", mode: "foreground" });
        }

        const daemon = spawn(
          process.execPath,
          [import.meta.dir + "/daemon.js"],
          {
            detached: true,
            stdio: "ignore",
          }
        );
        daemon.unref();
        return Result.ok({ status: "started", pid: daemon.pid ?? null });
      },
    });
  });

Stop Command (HTTP pattern)

import { NotFoundError, NetworkError, Result } from "@outfitter/contracts";
import { getSocketPath, getPidPath, isDaemonAlive } from "@outfitter/daemon";

const socketPath = getSocketPath("my-daemon");
const pidPath = getPidPath("my-daemon");

command("stop")
  .description("Stop the daemon")
  .action(async () => {
    await runHandler({
      command: "stop",
      input: {},
      handler: async () => {
        if (!(await isDaemonAlive(pidPath))) {
          return Result.err(NotFoundError.create("daemon", "process"));
        }

        try {
          const response = await fetch(`http://unix:${socketPath}:/shutdown`, {
            method: "POST",
          });
          if (!response.ok) {
            return Result.err(NetworkError.create("Failed to stop daemon"));
          }
          return Result.ok({ status: "stopped" });
        } catch {
          return Result.err(NetworkError.create("Failed to reach daemon"));
        }
      },
    });
  });