style: 完成所有文件的lint

This commit is contained in:
claude-code-best
2026-05-01 21:39:30 +08:00
parent d136872cc9
commit 6182015005
1333 changed files with 68255 additions and 77882 deletions

View File

@@ -342,4 +342,4 @@ fetchInstances();
setInterval(fetchInstances, 3000);
</script>
</body>
</html>`;
</html>`

View File

@@ -1,44 +1,46 @@
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { ProcessManager } from "./manager.js";
import { createApp } from "./routes.js";
import { Hono } from 'hono'
import { serve } from '@hono/node-server'
import { ProcessManager } from './manager.js'
import { createApp } from './routes.js'
export async function startManager(port: number): Promise<void> {
const manager = new ProcessManager();
const app = createApp(manager);
const manager = new ProcessManager()
const app = createApp(manager)
// Health check
app.get("/health", (c) => c.json({ status: "ok" }));
app.get('/health', c => c.json({ status: 'ok' }))
let shuttingDown = false;
let shuttingDown = false
const shutdown = async () => {
if (shuttingDown) return;
shuttingDown = true;
console.log("Shutting down...");
await manager.shutdownAll();
process.exit(0);
};
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
if (shuttingDown) return
shuttingDown = true
console.log('Shutting down...')
await manager.shutdownAll()
process.exit(0)
}
process.on('SIGTERM', shutdown)
process.on('SIGINT', shutdown)
const server = serve({ fetch: app.fetch, port });
server.on("error", (err: NodeJS.ErrnoException) => {
if (err.code === "EADDRINUSE") {
console.error(`\n Error: port ${port} is already in use. Use --port to specify a different port.\n`);
const server = serve({ fetch: app.fetch, port })
server.on('error', (err: NodeJS.ErrnoException) => {
if (err.code === 'EADDRINUSE') {
console.error(
`\n Error: port ${port} is already in use. Use --port to specify a different port.\n`,
)
} else {
console.error(`\n Error: ${err.message}\n`);
console.error(`\n Error: ${err.message}\n`)
}
process.exit(1);
});
process.exit(1)
})
console.log();
console.log(` 🖥️ ACP Manager`);
console.log();
console.log(` URL: http://localhost:${port}`);
console.log();
console.log(` Press Ctrl+C to stop`);
console.log();
console.log()
console.log(` 🖥️ ACP Manager`)
console.log()
console.log(` URL: http://localhost:${port}`)
console.log()
console.log(` Press Ctrl+C to stop`)
console.log()
// Keep running
await new Promise(() => {});
await new Promise(() => {})
}

View File

@@ -1,205 +1,217 @@
import type { AcpInstance, InstanceSummary, LogEntry } from "./types.js";
import type { AcpInstance, InstanceSummary, LogEntry } from './types.js'
function log(tag: string, msg: string) {
const ts = new Date().toISOString();
console.log(`[${ts}] [${tag}] ${msg}`);
const ts = new Date().toISOString()
console.log(`[${ts}] [${tag}] ${msg}`)
}
const MAX_LOG_LINES = 2000;
const SHUTDOWN_TIMEOUT_MS = 5000;
const MAX_LOG_LINES = 2000
const SHUTDOWN_TIMEOUT_MS = 5000
export class ProcessManager {
private instances = new Map<string, AcpInstance>();
private instances = new Map<string, AcpInstance>()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private processes = new Map<string, any>();
private processes = new Map<string, any>()
create(group: string, command: string): AcpInstance {
const id = crypto.randomUUID();
const id = crypto.randomUUID()
const instance: AcpInstance = {
id,
group,
command,
status: "running",
status: 'running',
pid: undefined,
startTime: Date.now(),
exitCode: null,
logs: [],
subscribers: new Set(),
};
}
const args = this.parseCommand(command);
const fullArgs = ["--group", group, ...args];
const args = this.parseCommand(command)
const fullArgs = ['--group', group, ...args]
const proc = Bun.spawn(["acp-link", ...fullArgs], {
stdout: "pipe",
stderr: "pipe",
env: { ...Bun.env, ACP_CHILD: "1" },
});
const proc = Bun.spawn(['acp-link', ...fullArgs], {
stdout: 'pipe',
stderr: 'pipe',
env: { ...Bun.env, ACP_CHILD: '1' },
})
instance.pid = proc.pid;
this.instances.set(id, instance);
this.processes.set(id, proc);
log("manager", `created instance ${id.slice(0, 8)} group=${group} pid=${proc.pid} cmd="acp-link ${fullArgs.join(" ")}"`);
instance.pid = proc.pid
this.instances.set(id, instance)
this.processes.set(id, proc)
log(
'manager',
`created instance ${id.slice(0, 8)} group=${group} pid=${proc.pid} cmd="acp-link ${fullArgs.join(' ')}"`,
)
this.pipeStream(proc.stdout, id, "stdout");
this.pipeStream(proc.stderr, id, "stderr");
this.pipeStream(proc.stdout, id, 'stdout')
this.pipeStream(proc.stderr, id, 'stderr')
proc.exited.then((code) => {
instance.status = code === 0 ? "stopped" : "failed";
instance.exitCode = code;
instance.pid = undefined;
this.processes.delete(id);
log("manager", `instance ${id.slice(0, 8)} ${instance.status} exit=${code}`);
this.notifyStatus(instance);
});
proc.exited.then(code => {
instance.status = code === 0 ? 'stopped' : 'failed'
instance.exitCode = code
instance.pid = undefined
this.processes.delete(id)
log(
'manager',
`instance ${id.slice(0, 8)} ${instance.status} exit=${code}`,
)
this.notifyStatus(instance)
})
return instance;
return instance
}
stop(id: string): boolean {
const proc = this.processes.get(id);
if (!proc) return false;
const inst = this.instances.get(id);
log("manager", `stopping instance ${id.slice(0, 8)} pid=${proc.pid}`);
proc.kill("SIGTERM");
const proc = this.processes.get(id)
if (!proc) return false
const inst = this.instances.get(id)
log('manager', `stopping instance ${id.slice(0, 8)} pid=${proc.pid}`)
proc.kill('SIGTERM')
// Immediately mark as stopped to prevent stale state
if (inst) {
inst.status = "stopped";
inst.status = 'stopped'
}
return true;
return true
}
remove(id: string): boolean {
const instance = this.instances.get(id);
if (!instance) return false;
if (instance.status === "running") return false;
instance.subscribers.clear();
this.instances.delete(id);
log("manager", `removed instance ${id.slice(0, 8)} group=${instance.group}`);
return true;
const instance = this.instances.get(id)
if (!instance) return false
if (instance.status === 'running') return false
instance.subscribers.clear()
this.instances.delete(id)
log('manager', `removed instance ${id.slice(0, 8)} group=${instance.group}`)
return true
}
list(): InstanceSummary[] {
return Array.from(this.instances.values()).map(this.toSummary);
return Array.from(this.instances.values()).map(this.toSummary)
}
get(id: string): AcpInstance | undefined {
return this.instances.get(id);
return this.instances.get(id)
}
subscribe(id: string, callback: (entry: LogEntry) => void): () => void {
const instance = this.instances.get(id);
if (!instance) return () => {};
instance.subscribers.add(callback);
return () => instance.subscribers.delete(callback);
const instance = this.instances.get(id)
if (!instance) return () => {}
instance.subscribers.add(callback)
return () => instance.subscribers.delete(callback)
}
async shutdownAll(): Promise<void> {
const running = Array.from(this.processes.entries());
if (running.length === 0) return;
const running = Array.from(this.processes.entries())
if (running.length === 0) return
log("manager", `shutting down ${running.length} running instance(s)...`);
log('manager', `shutting down ${running.length} running instance(s)...`)
for (const [id, proc] of running) {
try {
proc.kill("SIGTERM");
log("manager", `sent SIGTERM to ${id.slice(0, 8)} pid=${proc.pid}`);
proc.kill('SIGTERM')
log('manager', `sent SIGTERM to ${id.slice(0, 8)} pid=${proc.pid}`)
} catch {
// already dead
}
}
const timeout = new Promise<void>((resolve) => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS));
const timeout = new Promise<void>(resolve =>
setTimeout(resolve, SHUTDOWN_TIMEOUT_MS),
)
await Promise.race([
Promise.all(running.map(([, proc]) => proc.exited.catch(() => {}))),
timeout,
]);
])
for (const [id, proc] of running) {
try {
proc.kill("SIGKILL");
log("manager", `sent SIGKILL to ${id.slice(0, 8)}`);
proc.kill('SIGKILL')
log('manager', `sent SIGKILL to ${id.slice(0, 8)}`)
} catch {
// already dead
}
}
log("manager", "all instances shut down");
log('manager', 'all instances shut down')
}
private parseCommand(command: string): string[] {
const args: string[] = [];
let current = "";
let inQuote: string | null = null;
const args: string[] = []
let current = ''
let inQuote: string | null = null
for (const ch of command) {
if (inQuote) {
if (ch === inQuote) {
inQuote = null;
inQuote = null
} else {
current += ch;
current += ch
}
} else if (ch === '"' || ch === "'") {
inQuote = ch;
} else if (ch === " " || ch === "\t") {
inQuote = ch
} else if (ch === ' ' || ch === '\t') {
if (current) {
args.push(current);
current = "";
args.push(current)
current = ''
}
} else {
current += ch;
current += ch
}
}
if (current) args.push(current);
return args;
if (current) args.push(current)
return args
}
private pipeStream(
readable: ReadableStream<Uint8Array>,
instanceId: string,
stream: "stdout" | "stderr",
stream: 'stdout' | 'stderr',
) {
const reader = readable.getReader();
const decoder = new TextDecoder();
let buffer = "";
const reader = readable.getReader()
const decoder = new TextDecoder()
let buffer = ''
const processChunk = () => {
reader
.read()
.then(({ done, value }) => {
if (done) {
if (buffer) this.appendLog(instanceId, buffer, stream);
return;
if (buffer) this.appendLog(instanceId, buffer, stream)
return
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() ?? ''
for (const line of lines) {
if (line) this.appendLog(instanceId, line, stream);
if (line) this.appendLog(instanceId, line, stream)
}
processChunk();
processChunk()
})
.catch(() => {
// stream ended or error
});
};
processChunk();
})
}
processChunk()
}
private appendLog(instanceId: string, text: string, stream: "stdout" | "stderr") {
const instance = this.instances.get(instanceId);
if (!instance) return;
private appendLog(
instanceId: string,
text: string,
stream: 'stdout' | 'stderr',
) {
const instance = this.instances.get(instanceId)
if (!instance) return
const entry: LogEntry = { timestamp: Date.now(), stream, text };
instance.logs.push(entry);
const entry: LogEntry = { timestamp: Date.now(), stream, text }
instance.logs.push(entry)
if (instance.logs.length > MAX_LOG_LINES) {
instance.logs.splice(0, instance.logs.length - MAX_LOG_LINES);
instance.logs.splice(0, instance.logs.length - MAX_LOG_LINES)
}
for (const sub of instance.subscribers) {
try {
sub(entry);
sub(entry)
} catch {
// subscriber error, remove it
instance.subscribers.delete(sub);
instance.subscribers.delete(sub)
}
}
}
@@ -207,14 +219,14 @@ export class ProcessManager {
private notifyStatus(instance: AcpInstance) {
const statusEntry: LogEntry = {
timestamp: Date.now(),
stream: "stderr",
stream: 'stderr',
text: `[${instance.status}] exit code: ${instance.exitCode}`,
};
}
for (const sub of instance.subscribers) {
try {
sub(statusEntry);
sub(statusEntry)
} catch {
instance.subscribers.delete(sub);
instance.subscribers.delete(sub)
}
}
}
@@ -228,6 +240,6 @@ export class ProcessManager {
pid: inst.pid,
startTime: inst.startTime,
exitCode: inst.exitCode,
};
}
}
}

View File

@@ -1,41 +1,41 @@
import { Hono } from "hono";
import type { ProcessManager } from "./manager.js";
import { MANAGER_HTML } from "./html.js";
import { Hono } from 'hono'
import type { ProcessManager } from './manager.js'
import { MANAGER_HTML } from './html.js'
function logReq(method: string, path: string, status?: number) {
const ts = new Date().toISOString();
const suffix = status != null ? ` -> ${status}` : "";
console.log(`[${ts}] [http] ${method} ${path}${suffix}`);
const ts = new Date().toISOString()
const suffix = status != null ? ` -> ${status}` : ''
console.log(`[${ts}] [http] ${method} ${path}${suffix}`)
}
export function createApp(manager: ProcessManager): Hono {
const app = new Hono();
const app = new Hono()
app.get("/", (c) => {
logReq("GET", "/", 200);
return c.html(MANAGER_HTML);
});
app.get('/', c => {
logReq('GET', '/', 200)
return c.html(MANAGER_HTML)
})
app.get("/api/instances", (c) => {
const list = manager.list();
logReq("GET", "/api/instances", 200);
return c.json(list);
});
app.get('/api/instances', c => {
const list = manager.list()
logReq('GET', '/api/instances', 200)
return c.json(list)
})
app.post("/api/instances", async (c) => {
let body: { group?: string; command?: string };
app.post('/api/instances', async c => {
let body: { group?: string; command?: string }
try {
body = await c.req.json<{ group?: string; command?: string }>();
body = await c.req.json<{ group?: string; command?: string }>()
} catch {
logReq("POST", "/api/instances", 400);
return c.json({ error: "invalid JSON body" }, 400);
logReq('POST', '/api/instances', 400)
return c.json({ error: 'invalid JSON body' }, 400)
}
if (!body.group?.trim() || !body.command?.trim()) {
logReq("POST", "/api/instances", 400);
return c.json({ error: "group and command are required" }, 400);
logReq('POST', '/api/instances', 400)
return c.json({ error: 'group and command are required' }, 400)
}
const instance = manager.create(body.group.trim(), body.command.trim());
logReq("POST", `/api/instances group=${body.group}`, 201);
const instance = manager.create(body.group.trim(), body.command.trim())
logReq('POST', `/api/instances group=${body.group}`, 201)
return c.json(
{
id: instance.id,
@@ -47,107 +47,107 @@ export function createApp(manager: ProcessManager): Hono {
exitCode: instance.exitCode,
},
201,
);
});
)
})
app.post("/api/instances/:id/stop", (c) => {
const id = c.req.param("id");
const inst = manager.get(id);
app.post('/api/instances/:id/stop', c => {
const id = c.req.param('id')
const inst = manager.get(id)
if (!inst) {
logReq("POST", `/api/instances/${id.slice(0, 8)}/stop`, 404);
return c.json({ error: "not found" }, 404);
logReq('POST', `/api/instances/${id.slice(0, 8)}/stop`, 404)
return c.json({ error: 'not found' }, 404)
}
if (inst.status !== "running") {
logReq("POST", `/api/instances/${id.slice(0, 8)}/stop`, 400);
return c.json({ error: "not running" }, 400);
if (inst.status !== 'running') {
logReq('POST', `/api/instances/${id.slice(0, 8)}/stop`, 400)
return c.json({ error: 'not running' }, 400)
}
manager.stop(inst.id);
logReq("POST", `/api/instances/${id.slice(0, 8)}/stop`, 200);
return c.json({ ok: true });
});
manager.stop(inst.id)
logReq('POST', `/api/instances/${id.slice(0, 8)}/stop`, 200)
return c.json({ ok: true })
})
app.delete("/api/instances/:id", (c) => {
const id = c.req.param("id");
const inst = manager.get(id);
app.delete('/api/instances/:id', c => {
const id = c.req.param('id')
const inst = manager.get(id)
if (!inst) {
logReq("DELETE", `/api/instances/${id.slice(0, 8)}`, 404);
return c.json({ error: "not found" }, 404);
logReq('DELETE', `/api/instances/${id.slice(0, 8)}`, 404)
return c.json({ error: 'not found' }, 404)
}
if (inst.status === "running") {
logReq("DELETE", `/api/instances/${id.slice(0, 8)}`, 400);
return c.json({ error: "still running" }, 400);
if (inst.status === 'running') {
logReq('DELETE', `/api/instances/${id.slice(0, 8)}`, 400)
return c.json({ error: 'still running' }, 400)
}
manager.remove(inst.id);
logReq("DELETE", `/api/instances/${id.slice(0, 8)}`, 200);
return c.json({ ok: true });
});
manager.remove(inst.id)
logReq('DELETE', `/api/instances/${id.slice(0, 8)}`, 200)
return c.json({ ok: true })
})
app.get("/api/instances/:id/logs", (c) => {
const id = c.req.param("id");
const inst = manager.get(id);
app.get('/api/instances/:id/logs', c => {
const id = c.req.param('id')
const inst = manager.get(id)
if (!inst) {
logReq("GET", `/api/instances/${id.slice(0, 8)}/logs`, 404);
return c.json({ error: "not found" }, 404);
logReq('GET', `/api/instances/${id.slice(0, 8)}/logs`, 404)
return c.json({ error: 'not found' }, 404)
}
logReq("GET", `/api/instances/${id.slice(0, 8)}/logs SSE`);
logReq('GET', `/api/instances/${id.slice(0, 8)}/logs SSE`)
const stream = new ReadableStream({
start(controller) {
const encoder = new TextEncoder();
const encoder = new TextEncoder()
const send = (data: string) => {
try {
controller.enqueue(encoder.encode(data));
controller.enqueue(encoder.encode(data))
} catch {
// stream closed
}
};
}
// send historical logs
for (const log of inst.logs) {
send(`data: ${JSON.stringify(log)}\n\n`);
send(`data: ${JSON.stringify(log)}\n\n`)
}
// subscribe to new logs
const unsub = manager.subscribe(inst.id, (entry) => {
send(`data: ${JSON.stringify(entry)}\n\n`);
});
const unsub = manager.subscribe(inst.id, entry => {
send(`data: ${JSON.stringify(entry)}\n\n`)
})
// keepalive every 15s
const keepalive = setInterval(() => {
send(": keepalive\n\n");
}, 15000);
send(': keepalive\n\n')
}, 15000)
const cleanup = () => {
unsub();
clearInterval(keepalive);
logReq("SSE", `/api/instances/${id.slice(0, 8)}/logs closed`);
unsub()
clearInterval(keepalive)
logReq('SSE', `/api/instances/${id.slice(0, 8)}/logs closed`)
try {
controller.close();
controller.close()
} catch {
// already closed
}
};
}
c.req.raw.signal.addEventListener("abort", cleanup, { once: true });
c.req.raw.signal.addEventListener('abort', cleanup, { once: true })
},
});
})
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"X-Accel-Buffering": "no",
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
'X-Accel-Buffering': 'no',
},
});
});
})
})
// Catch-all: log unmatched routes for debugging
app.all("*", (c) => {
logReq(c.req.method, c.req.path, 404);
return c.json({ error: "not found", path: c.req.path }, 404);
});
app.all('*', c => {
logReq(c.req.method, c.req.path, 404)
return c.json({ error: 'not found', path: c.req.path }, 404)
})
return app;
return app
}

View File

@@ -1,34 +1,34 @@
export type InstanceStatus = "running" | "stopped" | "failed";
export type InstanceStatus = 'running' | 'stopped' | 'failed'
export interface AcpInstance {
id: string;
group: string;
command: string;
status: InstanceStatus;
pid: number | undefined;
startTime: number;
exitCode: number | null;
logs: LogEntry[];
subscribers: Set<(entry: LogEntry) => void>;
id: string
group: string
command: string
status: InstanceStatus
pid: number | undefined
startTime: number
exitCode: number | null
logs: LogEntry[]
subscribers: Set<(entry: LogEntry) => void>
}
export interface LogEntry {
timestamp: number;
stream: "stdout" | "stderr";
text: string;
timestamp: number
stream: 'stdout' | 'stderr'
text: string
}
export interface CreateInstanceRequest {
group: string;
command: string;
group: string
command: string
}
export interface InstanceSummary {
id: string;
group: string;
command: string;
status: InstanceStatus;
pid: number | undefined;
startTime: number;
exitCode: number | null;
id: string
group: string
command: string
status: InstanceStatus
pid: number | undefined
startTime: number
exitCode: number | null
}