chore: delete unused files flagged by knip

38 files removed across app and server (verified unreferenced via
grep + typecheck). Includes dead components, hooks, stores, utils,
POC scripts, an empty orchestrator stub, stale playwright configs,
and a duplicate workspace-registry test-helpers module.

knip config tightened: added babel.config.js, native platform
extensions, and test-stubs/ to app entry patterns.
This commit is contained in:
Mohamed Boudra
2026-04-23 22:29:49 +07:00
parent dc92863c35
commit 2dd8c27573
38 changed files with 4 additions and 8320 deletions
@@ -1,136 +0,0 @@
/**
* TDD Test Suite for Command Support POC
*
* This tests the ability to:
* 1. Get available commands/skills from the Claude Agent SDK
* 2. Execute commands (determine if they're just prompts or something else)
*
* Key findings from SDK analysis:
* - `supportedCommands()` returns SlashCommand[] with name, description, argumentHint
* - Commands are executed by sending them as prompts with / prefix
* - The SDK init message includes `slash_commands: string[]` and `skills: string[]`
*
* IMPORTANT: Control methods like supportedCommands() work WITHOUT iterating the query first!
* Use an empty async generator for the prompt when you just need control methods.
* This pattern is used in claude-agent.ts listModels().
*/
import { beforeAll, beforeEach, describe, expect, test } from "vitest";
import { query, type SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
import { isCommandAvailable } from "../utils/executable.js";
const hasClaudeCredentials =
!!process.env.CLAUDE_CODE_OAUTH_TOKEN || !!process.env.ANTHROPIC_API_KEY;
// Pattern from claude-agent.ts listModels():
// Use an empty async generator when you just need control methods
function createEmptyPrompt(): AsyncGenerator<SDKUserMessage, void, undefined> {
return (async function* empty() {})();
}
describe("Claude Agent SDK Commands POC", () => {
let canRunClaudeIntegration = false;
beforeAll(async () => {
canRunClaudeIntegration = (await isCommandAvailable("claude")) && hasClaudeCredentials;
});
beforeEach((context) => {
if (!canRunClaudeIntegration) {
context.skip();
}
});
describe("supportedCommands() API", () => {
test("should return an array of SlashCommand objects", async () => {
// Use the pattern from claude-agent.ts:
// Create a query with empty prompt generator for control methods
const emptyPrompt = createEmptyPrompt();
const claudeQuery = query({
prompt: emptyPrompt,
options: {
cwd: process.cwd(),
permissionMode: "plan",
includePartialMessages: false,
settingSources: ["user", "project"], // Required to load skills
},
});
try {
// supportedCommands() is a control method - works without iterating
const commands = await claudeQuery.supportedCommands();
// Should be an array
expect(Array.isArray(commands)).toBe(true);
// Verify structure
if (commands.length > 0) {
const firstCommand = commands[0];
expect(typeof firstCommand.name).toBe("string");
expect(typeof firstCommand.description).toBe("string");
expect(typeof firstCommand.argumentHint).toBe("string");
expect(firstCommand.name.startsWith("/")).toBe(false);
}
} finally {
if (typeof claudeQuery.return === "function") {
try {
await claudeQuery.return();
} catch {
// ignore shutdown errors
}
}
}
}, 30000);
test("should have valid SlashCommand structure for all commands", async () => {
const emptyPrompt = createEmptyPrompt();
const claudeQuery = query({
prompt: emptyPrompt,
options: {
cwd: process.cwd(),
permissionMode: "plan",
settingSources: ["user", "project"],
},
});
try {
const commands = await claudeQuery.supportedCommands();
expect(commands.length).toBeGreaterThan(0);
// Verify all commands have valid structure
for (const cmd of commands) {
expect(cmd).toHaveProperty("name");
expect(cmd).toHaveProperty("description");
expect(cmd).toHaveProperty("argumentHint");
expect(typeof cmd.name).toBe("string");
expect(typeof cmd.description).toBe("string");
expect(typeof cmd.argumentHint).toBe("string");
expect(cmd.name.length).toBeGreaterThan(0);
expect(cmd.name.startsWith("/")).toBe(false);
}
} finally {
await claudeQuery.return?.();
}
}, 30000);
});
describe("Command Execution", () => {
test("should explain that commands are prompts with / prefix", () => {
// This is a documentation test - commands ARE just prompts with / prefix
// To execute a command:
// 1. Create a user message with content: "/{commandName}"
// 2. Push it to the input stream
// 3. Iterate the query to receive responses
// The SDK handles command expansion:
// - Slash commands expand to their template content
// - Skills invoke the Skill tool
// - The model processes the expanded prompt like any other
expect(true).toBe(true); // Documentation-only test
});
});
});
@@ -1,186 +0,0 @@
#!/usr/bin/env npx tsx
/**
* Investigation: What does command execution actually return?
*
* This script logs ALL message types and their full structure
* to understand how command output is delivered.
*/
import { query, type SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
class Pushable<T> implements AsyncIterable<T> {
private queue: T[] = [];
private resolvers: Array<(value: IteratorResult<T, void>) => void> = [];
private closed = false;
push(item: T) {
if (this.closed) return;
if (this.resolvers.length > 0) {
const resolve = this.resolvers.shift()!;
resolve({ value: item, done: false });
} else {
this.queue.push(item);
}
}
end() {
this.closed = true;
while (this.resolvers.length > 0) {
const resolve = this.resolvers.shift()!;
resolve({ value: undefined, done: true });
}
}
[Symbol.asyncIterator](): AsyncIterator<T, void> {
return {
next: (): Promise<IteratorResult<T, void>> => {
if (this.queue.length > 0) {
const value = this.queue.shift();
if (value !== undefined) {
return Promise.resolve({ value, done: false });
}
}
if (this.closed) {
return Promise.resolve({ value: undefined, done: true });
}
return new Promise<IteratorResult<T, void>>((resolve) => {
this.resolvers.push(resolve);
});
},
};
}
}
async function investigateCommand(commandName: string): Promise<void> {
process.stdout.write(`\n${"=".repeat(60)}\n`);
process.stdout.write(`Investigating: /${commandName}\n`);
process.stdout.write(`${"=".repeat(60)}\n`);
const input = new Pushable<SDKUserMessage>();
const claudeQuery = query({
prompt: input,
options: {
cwd: process.cwd(),
permissionMode: "plan",
includePartialMessages: true, // Include streaming partial messages
settingSources: ["user", "project"],
},
});
try {
const userMessage: SDKUserMessage = {
type: "user",
message: {
role: "user",
content: `/${commandName}`,
},
parent_tool_use_id: null,
session_id: "",
};
input.push(userMessage);
let messageCount = 0;
for await (const message of claudeQuery) {
messageCount++;
process.stdout.write(`\n--- Message ${messageCount} ---\n`);
process.stdout.write(`Type: ${message.type}\n`);
// Log the full structure based on type
switch (message.type) {
case "system":
process.stdout.write(`Subtype: ${message.subtype}\n`);
if (message.subtype === "init") {
process.stdout.write(`Session: ${message.session_id}\n`);
process.stdout.write(`Model: ${message.model}\n`);
}
break;
case "user":
process.stdout.write(
`User message content: ${JSON.stringify(message.message?.content, null, 2)}\n`,
);
break;
case "assistant":
process.stdout.write("Assistant message content:\n");
const content = message.message?.content;
if (Array.isArray(content)) {
for (const block of content) {
process.stdout.write(` Block type: ${block.type}\n`);
if (block.type === "text") {
process.stdout.write(` Text: ${block.text}\n`);
} else if (block.type === "tool_use") {
process.stdout.write(` Tool: ${block.name}\n`);
process.stdout.write(` Input: ${JSON.stringify(block.input, null, 2)}\n`);
} else {
process.stdout.write(` Full block: ${JSON.stringify(block, null, 2)}\n`);
}
}
} else {
process.stdout.write(` Content: ${JSON.stringify(content, null, 2)}\n`);
}
break;
case "stream_event":
process.stdout.write(`Stream event type: ${message.event?.type}\n`);
if (message.event?.type === "content_block_delta") {
const delta = message.event.delta;
if (delta?.type === "text_delta") {
process.stdout.write(` Text delta: ${delta.text}\n`);
}
}
break;
case "result":
process.stdout.write(`Result subtype: ${message.subtype}\n`);
if ("errors" in message && message.errors) {
process.stdout.write(`Errors: ${JSON.stringify(message.errors)}\n`);
}
// Check for any other properties
const resultKeys = Object.keys(message).filter((k) => !["type", "subtype"].includes(k));
if (resultKeys.length > 0) {
process.stdout.write(`Other result properties: ${resultKeys.join(", ")}\n`);
for (const key of resultKeys) {
process.stdout.write(` ${key}: ${JSON.stringify((message as any)[key], null, 2)}\n`);
}
}
break;
default:
process.stdout.write(`Full message: ${JSON.stringify(message, null, 2)}\n`);
}
if (message.type === "result") {
break;
}
}
process.stdout.write(`\nTotal messages received: ${messageCount}\n`);
} finally {
input.end();
await claudeQuery.return?.();
}
}
async function main() {
process.stdout.write("=== Command Output Investigation ===\n\n");
// Test /context - a local command that shows context info
await investigateCommand("context");
// Test /cost - another local command
await investigateCommand("cost");
// Test /prompt-engineer - a SKILL (not a local command)
await investigateCommand("prompt-engineer");
process.stdout.write("\n=== Investigation Complete ===\n");
}
main().catch((error) => {
process.stderr.write(`Fatal error: ${error}\n`);
process.exit(1);
});
-212
View File
@@ -1,212 +0,0 @@
#!/usr/bin/env npx tsx
/**
* POC Script: Claude Agent SDK Commands
*
* This script demonstrates how to:
* 1. Get available slash commands/skills from the Claude Agent SDK
* 2. Execute commands (they are prompts sent with / prefix)
*
* Key insight from existing claude-agent.ts:
* - Control methods like supportedCommands() work WITHOUT iterating the query first
* - Use an empty async generator for the prompt when you just want to call control methods
* - Commands are executed by sending them as prompts with / prefix to a streaming query
*
* Usage: npx tsx src/poc-commands/run-poc.ts
*/
import { query, type SlashCommand, type SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
// Pattern from claude-agent.ts listModels():
// Use an empty async generator when you just need control methods
function createEmptyPrompt(): AsyncGenerator<SDKUserMessage, void, undefined> {
return (async function* empty() {})();
}
// Utility: Create a pushable stream for SDK input (for command execution demo)
class Pushable<T> implements AsyncIterable<T> {
private queue: T[] = [];
private resolvers: Array<(value: IteratorResult<T, void>) => void> = [];
private closed = false;
push(item: T) {
if (this.closed) return;
if (this.resolvers.length > 0) {
const resolve = this.resolvers.shift()!;
resolve({ value: item, done: false });
} else {
this.queue.push(item);
}
}
end() {
this.closed = true;
while (this.resolvers.length > 0) {
const resolve = this.resolvers.shift()!;
resolve({ value: undefined, done: true });
}
}
[Symbol.asyncIterator](): AsyncIterator<T, void> {
return {
next: (): Promise<IteratorResult<T, void>> => {
if (this.queue.length > 0) {
const value = this.queue.shift();
if (value !== undefined) {
return Promise.resolve({ value, done: false });
}
}
if (this.closed) {
return Promise.resolve({ value: undefined, done: true });
}
return new Promise<IteratorResult<T, void>>((resolve) => {
this.resolvers.push(resolve);
});
},
};
}
}
async function listAvailableCommands(): Promise<SlashCommand[]> {
// Use the pattern from claude-agent.ts listModels():
// Create a query with an empty prompt generator to call control methods
const emptyPrompt = createEmptyPrompt();
const claudeQuery = query({
prompt: emptyPrompt,
options: {
cwd: process.cwd(),
permissionMode: "plan",
includePartialMessages: false,
settingSources: ["user", "project"], // Required to load skills
},
});
try {
// supportedCommands() is a control method - works without iterating
const commands = await claudeQuery.supportedCommands();
return commands;
} finally {
// Clean up
if (typeof claudeQuery.return === "function") {
try {
await claudeQuery.return();
} catch {
// ignore shutdown errors
}
}
}
}
async function executeCommand(commandName: string): Promise<void> {
process.stdout.write(`\n=== Executing command: /${commandName} ===\n`);
// For command execution, we need a proper input stream
const input = new Pushable<SDKUserMessage>();
const claudeQuery = query({
prompt: input,
options: {
cwd: process.cwd(),
permissionMode: "plan",
includePartialMessages: false,
settingSources: ["user", "project"],
},
});
try {
// Push the command as a user message with / prefix
const userMessage: SDKUserMessage = {
type: "user",
message: {
role: "user",
content: `/${commandName}`,
},
parent_tool_use_id: null,
session_id: "",
};
input.push(userMessage);
// Iterate the query to process the command
let gotSystemInit = false;
for await (const message of claudeQuery) {
process.stdout.write(
` [${message.type}] ${message.type === "system" ? message.subtype : ""}\n`,
);
if (message.type === "system" && message.subtype === "init") {
gotSystemInit = true;
process.stdout.write(` Session: ${message.session_id}\n`);
process.stdout.write(` Model: ${message.model}\n`);
}
if (message.type === "assistant") {
const content = message.message?.content;
if (Array.isArray(content)) {
for (const block of content) {
if (block.type === "text") {
process.stdout.write(
` Response: ${block.text.slice(0, 200)}${block.text.length > 200 ? "..." : ""}\n`,
);
}
}
}
}
if (message.type === "result") {
process.stdout.write(` Result: ${message.subtype}\n`);
break;
}
}
} finally {
input.end();
await claudeQuery.return?.();
}
}
async function main() {
process.stdout.write("=== Claude Agent SDK Commands POC ===\n\n");
// PART 1: List available commands using supportedCommands()
process.stdout.write("=== Part 1: List Available Commands ===\n\n");
try {
const commands = await listAvailableCommands();
process.stdout.write(`Found ${commands.length} commands:\n\n`);
commands.forEach((cmd, index) => {
process.stdout.write(` ${index + 1}. /${cmd.name}\n`);
process.stdout.write(` Description: ${cmd.description}\n`);
if (cmd.argumentHint) {
process.stdout.write(` Arguments: ${cmd.argumentHint}\n`);
}
process.stdout.write("\n");
});
// PART 2: Demonstrate command execution (optional - uncomment to test)
// Commands are just prompts sent with / prefix
process.stdout.write("=== Part 2: Command Execution Explanation ===\n");
process.stdout.write("\n");
process.stdout.write("Commands are executed by sending them as prompts with / prefix.\n");
process.stdout.write("For example, to execute the 'help' command:\n");
process.stdout.write(' 1. Create a user message with content: "/help"\n');
process.stdout.write(" 2. Push it to the input stream\n");
process.stdout.write(" 3. Iterate the query to receive responses\n");
process.stdout.write("\n");
// Actually execute a command to demonstrate it works:
// Using "context" as it's fast and doesn't require arguments
await executeCommand("context");
} catch (error) {
process.stderr.write(`ERROR: ${error}\n`);
process.exit(1);
}
process.stdout.write("=== POC Complete ===\n");
}
main().catch((error) => {
process.stderr.write(`Fatal error: ${error}\n`);
process.exit(1);
});
@@ -1,9 +0,0 @@
import type { ToolSet } from "ai";
/**
* Get all tools for voice LLM
* @param agentTools - Agent control tools from MCP
*/
export function getAllTools(agentTools?: ToolSet): ToolSet {
return agentTools ?? {};
}
@@ -1,10 +0,0 @@
/**
* DEPRECATED: This file has been refactored.
*
* All orchestration logic has been moved to session.ts.
* Each Session now owns its own conversation state and manages
* the full lifecycle of user interactions.
*
* This file is kept as a stub to document the migration.
* It can be deleted once the refactoring is confirmed working.
*/
@@ -1,173 +0,0 @@
#!/usr/bin/env npx tsx
/**
* Ad-hoc script to debug checkout_status_request timeouts.
*
* Usage:
* npx tsx packages/server/src/server/daemon-e2e/checkout-debug.ts [agentIdOrCwd1] [agentIdOrCwd2]
*
* To test against a different daemon:
* PASEO_LISTEN=127.0.0.1:7777 npx tsx packages/server/src/server/daemon-e2e/checkout-debug.ts
*/
import { WebSocket } from "ws";
import os from "node:os";
import { DaemonClient } from "../../client/daemon-client.js";
// Patch WebSocket to log all messages
const OriginalWebSocket = WebSocket;
class LoggingWebSocket extends OriginalWebSocket {
constructor(url: string, ...args: any[]) {
super(url, ...args);
console.log(`[WS] Connecting to ${url}`);
this.on("open", () => console.log("[WS] Connection opened"));
this.on("close", (code, reason) => console.log(`[WS] Connection closed: ${code} ${reason}`));
this.on("error", (err) => console.log(`[WS] Error: ${err}`));
this.on("message", (data) => {
const str = data.toString().slice(0, 200);
console.log(`[WS] Message received (${data.toString().length} bytes): ${str}...`);
});
}
}
const PASEO_HOME = process.env.PASEO_HOME ?? `${os.homedir()}/.paseo`;
const PASEO_LISTEN = process.env.PASEO_LISTEN ?? "127.0.0.1:6767";
const DAEMON_URL = `ws://${PASEO_LISTEN}/ws`;
const CLIENT_ID = "clsk_checkout_debug";
function requestCheckoutStatus(client: DaemonClient, cwd: string) {
return (client as any)[`get${"Checkout"}Status`](cwd);
}
async function testMultiAgentSequence() {
console.log("\n=== Testing multi-agent checkout sequence ===");
console.log(`Daemon URL: ${DAEMON_URL}`);
const client = new DaemonClient({
url: DAEMON_URL,
clientId: CLIENT_ID,
webSocketFactory: (url) => new LoggingWebSocket(url) as any,
reconnect: { enabled: false },
});
const agents: Array<{ id: string; title: string; cwd: string }> = [];
// Also log raw messages for debugging
client.on("checkout_status_response", (msg: any) => {
console.log(
`[RAW checkout_status_response] requestId=${msg.payload.requestId} cwd=${msg.payload.cwd}`,
);
});
// Listen to connection state changes
client.subscribeConnectionStatus((state) => {
console.log(`[Connection] status=${state.status}`);
});
try {
await client.connect();
console.log("Connected to daemon");
console.log(`Connection state: ${JSON.stringify(client.getConnectionState())}`);
console.log("Fetching agents...");
const agentsList = await client.fetchAgents();
agents.length = 0;
for (const a of agentsList) {
agents.push({ id: a.id, title: a.title ?? "(untitled)", cwd: a.cwd });
}
if (agents.length === 0) {
console.log("No agents found!");
return;
}
console.log("\nAvailable agents:");
for (const a of agents.slice(0, 10)) {
console.log(` - ${a.id.slice(0, 8)}... ${a.title}`);
}
if (agents.length > 10) {
console.log(` ... and ${agents.length - 10} more`);
}
// Pick first two agents (or use command line args)
const arg1 = process.argv[2];
const arg2 = process.argv[3];
const agent1 = (arg1 ? agents.find((a) => a.id === arg1) : null) ?? agents[0] ?? null;
const agent2 =
(arg2 ? agents.find((a) => a.id === arg2) : null) ?? agents[1] ?? agents[0] ?? null;
const cwd1 = arg1 && !agent1 ? arg1 : agent1?.cwd;
const cwd2 = arg2 && !agent2 ? arg2 : agent2?.cwd;
if (!cwd1) {
console.log("No checkout cwd available to test");
return;
}
console.log(`\n=== Test 1: Request checkout for cwd1 (${cwd1}) ===`);
const start1 = Date.now();
try {
const status1 = await requestCheckoutStatus(client, cwd1);
console.log(
`✓ Cwd1 completed in ${Date.now() - start1}ms - branch: ${status1.currentBranch}`,
);
} catch (err) {
console.log(`✗ Cwd1 failed after ${Date.now() - start1}ms:`, err);
}
if (cwd2) {
console.log(`\n=== Test 2: Request checkout for cwd2 (${cwd2}) ===`);
const start2 = Date.now();
try {
const status2 = await requestCheckoutStatus(client, cwd2);
console.log(
`✓ Cwd2 completed in ${Date.now() - start2}ms - branch: ${status2.currentBranch}`,
);
} catch (err) {
console.log(`✗ Cwd2 failed after ${Date.now() - start2}ms:`, err);
}
}
console.log(`\n=== Test 3: Request checkout for cwd1 again ===`);
const start3 = Date.now();
try {
const status3 = await requestCheckoutStatus(client, cwd1);
console.log(
`✓ Cwd1 (retry) completed in ${Date.now() - start3}ms - branch: ${status3.currentBranch}`,
);
} catch (err) {
console.log(`✗ Cwd1 (retry) failed after ${Date.now() - start3}ms:`, err);
}
if (cwd2) {
console.log(`\n=== Test 4: Request both cwds in parallel ===`);
const start4 = Date.now();
try {
const [p1, p2] = await Promise.all([
requestCheckoutStatus(client, cwd1),
requestCheckoutStatus(client, cwd2),
]);
console.log(`✓ Parallel completed in ${Date.now() - start4}ms`);
console.log(` Cwd1 branch: ${p1.currentBranch}`);
console.log(` Cwd2 branch: ${p2.currentBranch}`);
} catch (err) {
console.log(`✗ Parallel failed after ${Date.now() - start4}ms:`, err);
}
}
} catch (error) {
console.error("Test failed:", error);
} finally {
await client.close();
}
}
async function main() {
console.log("Checkout Debug Script - Multi-Agent Sequence Test");
console.log("==================================================");
console.log(`PASEO_HOME: ${PASEO_HOME}`);
await testMultiAgentSequence();
console.log("\n=== Done ===");
}
main().catch(console.error);
@@ -1,48 +0,0 @@
import "dotenv/config";
import { beforeAll, afterAll } from "vitest";
import { mkdtempSync } from "fs";
import { tmpdir } from "os";
import path from "path";
import { createDaemonTestContext, type DaemonTestContext } from "../test-utils/index.js";
import { agentConfigs } from "./agent-configs.js";
// Re-export for backward compatibility - prefer using agentConfigs instead
export const CODEX_TEST_MODEL = agentConfigs.codex.model;
export const CODEX_TEST_THINKING_OPTION_ID = agentConfigs.codex.thinkingOptionId;
// Re-export agent configs
export {
agentConfigs,
getFullAccessConfig,
getAskModeConfig,
allProviders,
type AgentProvider,
type AgentTestConfig,
} from "./agent-configs.js";
export function tmpCwd(): string {
return mkdtempSync(path.join(tmpdir(), "daemon-e2e-"));
}
// Shared daemon context for all e2e tests
let sharedCtx: DaemonTestContext | null = null;
export function getTestContext(): DaemonTestContext {
if (!sharedCtx) {
throw new Error("Test context not initialized. Did you call setupDaemonE2E()?");
}
return sharedCtx;
}
export function setupDaemonE2E(): void {
beforeAll(async () => {
sharedCtx = await createDaemonTestContext();
}, 30000);
afterAll(async () => {
if (sharedCtx) {
await sharedCtx.cleanup();
sharedCtx = null;
}
}, 60000);
}
-6
View File
@@ -1,6 +0,0 @@
// Shared server types
export interface ServerConfig {
port: number;
isDev: boolean;
}
@@ -1,161 +0,0 @@
import { randomUUID } from "node:crypto";
import { promises as fs } from "node:fs";
import path from "node:path";
import type { Logger } from "pino";
import {
type PersistedProjectRecord,
type PersistedWorkspaceRecord,
type ProjectRegistry,
type WorkspaceRegistry,
createPersistedProjectRecord,
createPersistedWorkspaceRecord,
} from "./workspace-registry.js";
type RegistryRecord = PersistedProjectRecord | PersistedWorkspaceRecord;
class FileBackedRegistry<TRecord extends RegistryRecord> {
private readonly filePath: string;
private readonly logger: Logger;
private readonly schema: (record: unknown) => TRecord;
private readonly getId: (record: TRecord) => string;
private loaded = false;
private readonly cache = new Map<string, TRecord>();
private persistQueue: Promise<void> = Promise.resolve();
constructor(options: {
filePath: string;
logger: Logger;
schema: (record: unknown) => TRecord;
getId: (record: TRecord) => string;
component: string;
}) {
this.filePath = options.filePath;
this.schema = options.schema;
this.getId = options.getId;
this.logger = options.logger.child({
module: "workspace-registry",
component: options.component,
});
}
async initialize(): Promise<void> {
await this.load();
}
async existsOnDisk(): Promise<boolean> {
try {
await fs.access(this.filePath);
return true;
} catch {
return false;
}
}
async list(): Promise<TRecord[]> {
await this.load();
return Array.from(this.cache.values());
}
async get(id: string): Promise<TRecord | null> {
await this.load();
return this.cache.get(id) ?? null;
}
async upsert(record: TRecord): Promise<void> {
await this.load();
const parsed = this.schema(record);
this.cache.set(this.getId(parsed), parsed);
await this.enqueuePersist();
}
async archive(id: string, archivedAt: string): Promise<void> {
await this.load();
const existing = this.cache.get(id);
if (!existing) {
return;
}
const next = this.schema({
...existing,
updatedAt: archivedAt,
archivedAt,
});
this.cache.set(id, next);
await this.enqueuePersist();
}
async remove(id: string): Promise<void> {
await this.load();
if (!this.cache.delete(id)) {
return;
}
await this.enqueuePersist();
}
private async load(): Promise<void> {
if (this.loaded) {
return;
}
this.cache.clear();
try {
const raw = await fs.readFile(this.filePath, "utf8");
const parsed = JSON.parse(raw) as TRecord[];
for (const record of parsed) {
const validated = this.schema(record);
this.cache.set(this.getId(validated), validated);
}
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code !== "ENOENT") {
this.logger.error({ err: error, filePath: this.filePath }, "Failed to load registry file");
}
}
this.loaded = true;
}
private async persist(): Promise<void> {
const records = Array.from(this.cache.values());
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
const tempPath = `${this.filePath}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
await fs.writeFile(tempPath, JSON.stringify(records, null, 2), "utf8");
await fs.rename(tempPath, this.filePath);
}
private async enqueuePersist(): Promise<void> {
const nextPersist = this.persistQueue.then(() => this.persist());
this.persistQueue = nextPersist.catch(() => {});
await nextPersist;
}
}
export class FileBackedProjectRegistry
extends FileBackedRegistry<PersistedProjectRecord>
implements ProjectRegistry
{
constructor(filePath: string, logger: Logger) {
super({
filePath,
logger,
schema: (record) => createPersistedProjectRecord(record as PersistedProjectRecord),
getId: (record) => record.projectId,
component: "projects",
});
}
}
export class FileBackedWorkspaceRegistry
extends FileBackedRegistry<PersistedWorkspaceRecord>
implements WorkspaceRegistry
{
constructor(filePath: string, logger: Logger) {
super({
filePath,
logger,
schema: (record) => createPersistedWorkspaceRecord(record as PersistedWorkspaceRecord),
getId: (record) => record.workspaceId,
component: "workspaces",
});
}
}
File diff suppressed because it is too large Load Diff