stack-templates
Stack Templates
Templates for creating @outfitter/* components.
Component Types
| Type | Package | Template |
|---|---|---|
| Handler | @outfitter/contracts |
handler |
| Handler Test | @outfitter/testing |
handler-test |
| CLI Command | @outfitter/cli |
cli-command |
| MCP Tool | @outfitter/mcp |
mcp-tool |
| Daemon | @outfitter/daemon |
daemon-service |
Handler
Transport-agnostic business logic returning Result<T, E>:
import {
Result,
ValidationError,
NotFoundError,
createValidator,
type Handler,
} from "@outfitter/contracts";
import { z } from "zod";
// 1. Input schema
const InputSchema = z.object({
id: z.string().min(1),
});
type Input = z.infer<typeof InputSchema>;
// 2. Output type
interface Output {
id: string;
name: string;
}
// 3. Validator
const validateInput = createValidator(InputSchema);
// 4. Handler
export const myHandler: Handler<unknown, Output, ValidationError | NotFoundError> = async (
rawInput,
ctx
) => {
const inputResult = validateInput(rawInput);
if (inputResult.isErr()) return inputResult;
const input = inputResult.value;
ctx.logger.debug("Processing", { id: input.id });
const resource = await fetchResource(input.id);
if (!resource) {
return Result.err(new NotFoundError("resource", input.id));
}
return Result.ok(resource);
};
Handler Test
Test handlers directly without transport layer:
import { describe, test, expect } from "bun:test";
import { createContext } from "@outfitter/contracts";
import { myHandler } from "../handlers/my-handler.js";
describe("myHandler", () => {
test("returns success for valid input", async () => {
const ctx = createContext({});
const result = await myHandler({ id: "valid-id" }, ctx);
expect(result.isOk()).toBe(true);
expect(result.value).toMatchObject({ id: "valid-id" });
});
test("returns NotFoundError for missing resource", async () => {
const ctx = createContext({});
const result = await myHandler({ id: "missing" }, ctx);
expect(result.isErr()).toBe(true);
expect(result.error._tag).toBe("NotFoundError");
expect(result.error.resourceId).toBe("missing");
});
test("returns ValidationError for invalid input", async () => {
const ctx = createContext({});
const result = await myHandler({ id: "" }, ctx);
expect(result.isErr()).toBe(true);
expect(result.error._tag).toBe("ValidationError");
});
});
CLI Command
Commander.js command calling a handler:
import { command, output, exitWithError } from "@outfitter/cli";
import { createContext } from "@outfitter/contracts";
import { myHandler } from "../handlers/my-handler.js";
export const myCommand = command("my-command")
.description("What this command does")
.argument("<id>", "Resource ID")
.option("-l, --limit <n>", "Limit results", parseInt)
.action(async ({ args, flags }) => {
const ctx = createContext({});
const result = await myHandler({ id: args.id, limit: flags.limit }, ctx);
if (result.isErr()) {
exitWithError(result.error);
}
await output(result.value);
})
.build();
Register in CLI:
import { createCLI } from "@outfitter/cli";
import { myCommand } from "./commands/my-command.js";
const cli = createCLI({ name: "myapp", version: "1.0.0" });
cli.program.addCommand(myCommand);
cli.program.parse();
MCP Tool
Use defineTool() for type-safe tool definitions with automatic schema inference:
import { defineTool } from "@outfitter/mcp";
import { Result, ValidationError } from "@outfitter/contracts";
import { z } from "zod";
const InputSchema = z.object({
query: z.string().describe("Search query"),
limit: z.number().int().positive().default(10).describe("Max results"),
});
interface Output {
results: Array<{ id: string; title: string }>;
total: number;
}
export const myTool = defineTool({
name: "my_tool",
description: "Tool description for AI agent",
inputSchema: InputSchema,
handler: async (input): Promise<Result<Output, ValidationError>> => {
// input is automatically typed as z.infer<typeof InputSchema>
const results = await search(input.query, input.limit);
return Result.ok({ results, total: results.length });
},
});
Register in server:
import { createMcpServer } from "@outfitter/mcp";
import { myTool } from "./tools/my-tool.js";
const server = createMcpServer({ name: "my-server", version: "0.1.0" });
server.registerTool(myTool);
server.start();
Daemon Service
Background service with health checks and IPC:
import {
createDaemon,
createIpcServer,
createHealthChecker,
getSocketPath,
getLockPath,
} from "@outfitter/daemon";
import { createLogger, createConsoleSink } from "@outfitter/logging";
import { Result } from "@outfitter/contracts";
const logger = createLogger({
name: "my-daemon",
level: "info",
sinks: [createConsoleSink()],
redaction: { enabled: true },
});
const daemon = createDaemon({
name: "my-daemon",
pidFile: getLockPath("my-daemon"),
logger,
shutdownTimeout: 10000,
});
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`));
},
},
]);
const ipcServer = createIpcServer(getSocketPath("my-daemon"));
ipcServer.onMessage(async (msg) => {
const message = msg as { type: string };
switch (message.type) {
case "status": return { status: "ok", uptime: process.uptime() };
case "health": return await healthChecker.check();
default: return { error: "Unknown command" };
}
});
daemon.onShutdown(async () => {
logger.info("Shutting down...");
await ipcServer.close();
});
async function main() {
const startResult = await daemon.start();
if (startResult.isErr()) {
logger.error("Failed to start", { error: startResult.error });
process.exit(1);
}
await ipcServer.listen();
logger.info("Started", { socket: getSocketPath("my-daemon") });
}
main();
Best Practices
- Handler First - Write handler before adapter (CLI/MCP/API)
- Validate Early - Use
createValidatorat handler entry - Type Errors - List all error types in handler signature
- Context Propagation - Pass context through all handler calls
- Test Handlers - Test handlers directly without transport layer
References
More from outfitter-dev/agents
codebase-recon
This skill should be used when analyzing codebases, understanding architecture, or when "analyze", "investigate", "explore code", or "understand architecture" are mentioned.
92graphite-stacks
This skill should be used when the user asks to "create a stack", "submit stacked PRs", "gt submit", "gt create", "reorganize branches", "fix stack corruption", or mentions Graphite, stacked PRs, gt commands, or trunk-based development workflows.
76code-review
This skill should be used when reviewing code before commit, conducting quality gates, or when "review", "fresh eyes", "pre-commit review", or "quality gate" are mentioned.
34hono-dev
This skill should be used when building APIs with Hono, using hc client, implementing OpenAPI, or when "Hono", "RPC", or "type-safe API" are mentioned.
28software-craft
This skill should be used when making design decisions, evaluating trade-offs, assessing code quality, or when "engineering judgment" or "code quality" are mentioned.
28subagents
This skill should be used when coordinating agents, delegating tasks to specialists, or when "dispatch agents", "which agent", or "multi-agent" are mentioned.
25