refactor: rombak jadi full CLI/TUI only — hapus semua interface server (api/ws/grpc/web/daemon) + wire TUI ke agent beneran
- Hapus total interfaces: api (REST), ws (WebSocket), grpc, web, daemon (IPC) - Hapus infra server-only yang jadi dead code: infrastructure/ipc + seluruh stack auth (domain/auth, application/auth, persistence/iam, infrastructure/auth jwt/password/oauth_loopback) - TUI (OpenTUI) jadi interface utama: wire ke runSingleProcess() real runtime, ganti mock '(no LLM configured)' dengan turnService.runTurn asli + streaming stream_token/reasoning/tool_result/system_note/error ke transcript - Tampilkan model asli di header (mis. claude-opus-5), abort turn via Ctrl+C - CLI jadi mode headless tambahan: zesdex --headless '<prompt>' one-shot turn - Bersihkan package.json (script tui/headless), tsconfig paths, docker-compose, AGENTS.md; hapus StartOAuth/login stub - verified: tsc 0 err, 70 test pass, build dist/zesdex; headless 'pong' real; TUI interactive render + real turn 'say pong in one word' -> assistant 'pong'
This commit is contained in:
+31
-113
@@ -1,17 +1,19 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Zesdex CLI — main entry point.
|
||||
* Mirrors `apps/gateway/src/main.rs`.
|
||||
*
|
||||
* Parses argv, sets up file logging, and dispatches to the requested mode.
|
||||
* Default mode runs the single-process REPL loop.
|
||||
* Zesdex is a pure CLI/TUI tool:
|
||||
* - `zesdex` → boots the interactive OpenTUI (primary interface)
|
||||
* - `zesdex --headless "<p>"` → runs a single one-shot agent turn and exits
|
||||
*
|
||||
* All server modes (REST API / WebSocket / gRPC / web / daemon) were removed in
|
||||
* the "full cli tui" rombak. The TUI owns the interactive agent runtime; this
|
||||
* entry point only routes to it or runs a one-shot headless turn.
|
||||
*/
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { newStore } from "@zesdex/domain";
|
||||
import { parseCli, modeCount } from "./args.ts";
|
||||
import { runSingleProcess } from "./compose.ts";
|
||||
import { parseCli } from "./args.ts";
|
||||
|
||||
/** Initialise file logging (append to <data-dir>/zesdex/zesdex.log). */
|
||||
function initLogging(): void {
|
||||
@@ -22,8 +24,6 @@ function initLogging(): void {
|
||||
const stream = fs.createWriteStream(logPath, { flags: "a" });
|
||||
const originalLog = console.log;
|
||||
const originalError = console.error;
|
||||
const originalWarn = console.warn;
|
||||
const originalInfo = console.info;
|
||||
|
||||
const write = (level: string, args: unknown[]) => {
|
||||
const line = `[${new Date().toISOString()}] ${level} ${args
|
||||
@@ -31,8 +31,6 @@ function initLogging(): void {
|
||||
.join(" ")}`;
|
||||
stream.write(line + "\n");
|
||||
if (level === "ERROR") originalError(...args);
|
||||
else if (level === "WARN") originalWarn(...args);
|
||||
else if (level === "INFO") originalInfo(...args);
|
||||
else originalLog(...args);
|
||||
};
|
||||
|
||||
@@ -42,66 +40,28 @@ function initLogging(): void {
|
||||
console.error = (...a: unknown[]) => write("ERROR", a);
|
||||
}
|
||||
|
||||
/** Run the REPL loop (default single-process mode). */
|
||||
async function runRepl(): Promise<void> {
|
||||
/** Run a single one-shot agent turn non-interactively, printing the outcome. */
|
||||
async function runHeadless(prompt: string): Promise<void> {
|
||||
const { runSingleProcess, buildTurnParams } = await import("./compose.ts");
|
||||
const runtime = await runSingleProcess();
|
||||
console.info("zesdex ready — single-process REPL mode. Type `exit` to quit.");
|
||||
|
||||
// Minimal REPL over stdin.
|
||||
// Use node readline.
|
||||
const readline = (await import("node:readline")).default;
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||
const events: import("@zesdex/domain").TurnEvent[] = [];
|
||||
const abort = new AbortController();
|
||||
const params = buildTurnParams(runtime, prompt, events, abort);
|
||||
|
||||
rl.on("line", async (line: string) => {
|
||||
const text = line.trim();
|
||||
if (text === "exit" || text === "quit" || text === "/exit") {
|
||||
rl.close();
|
||||
process.exit(0);
|
||||
}
|
||||
if (text === "" || text.startsWith("/")) {
|
||||
if (text === "/help") {
|
||||
console.log("Commands: type a message to talk to the agent; `exit` to quit.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
await runtime.turnService.runTurn(params);
|
||||
|
||||
const events: import("@zesdex/domain").TurnEvent[] = [];
|
||||
const abort = new AbortController();
|
||||
const params = buildTurnParamsFrom(runtime, text, events, abort);
|
||||
|
||||
process.stdout.write("…");
|
||||
try {
|
||||
await runtime.turnService.runTurn(params);
|
||||
// Print the final assistant content from events.
|
||||
const lastTokens: string[] = [];
|
||||
for (const ev of events) {
|
||||
if (ev.kind === "stream_token") lastTokens.push(ev.content);
|
||||
}
|
||||
const full = lastTokens.join("");
|
||||
process.stdout.write("\n");
|
||||
if (full.trim() !== "") console.log(full.trim());
|
||||
else console.log("(no response content)");
|
||||
} catch (e) {
|
||||
console.error(`turn failed: ${(e as Error).message}`);
|
||||
}
|
||||
rl.prompt();
|
||||
});
|
||||
|
||||
rl.prompt();
|
||||
const tokens: string[] = [];
|
||||
for (const ev of events) {
|
||||
if (ev.kind === "stream_token") tokens.push(ev.content);
|
||||
}
|
||||
const full = tokens.join("");
|
||||
process.stdout.write("\n");
|
||||
if (full.trim() !== "") console.log(full.trim());
|
||||
else console.log("(no response content)");
|
||||
}
|
||||
|
||||
// Imported lazily to avoid a circular type-only dependency.
|
||||
import { buildTurnParams } from "./compose.ts";
|
||||
function buildTurnParamsFrom(
|
||||
runtime: Parameters<typeof buildTurnParams>[0],
|
||||
text: string,
|
||||
events: import("@zesdex/domain").TurnEvent[],
|
||||
abort: AbortController,
|
||||
) {
|
||||
return buildTurnParams(runtime, text, events, abort);
|
||||
}
|
||||
|
||||
/** Main entry. */
|
||||
/** Main entry. `zesdex` → TUI; `--headless <prompt>` → one-shot turn. */
|
||||
async function main(): Promise<void> {
|
||||
initLogging();
|
||||
|
||||
@@ -119,57 +79,15 @@ async function main(): Promise<void> {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const opts = parsed;
|
||||
if (modeCount(opts) > 1) {
|
||||
console.error(
|
||||
"Cannot specify multiple modes: --daemon, --attach, --api, --ws, --grpc, --web are mutually exclusive",
|
||||
);
|
||||
process.exit(2);
|
||||
if (parsed.headless !== null) {
|
||||
await runHeadless(parsed.headless);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (opts.daemon) {
|
||||
const { runDaemon } = await import("@zesdex/daemon");
|
||||
console.info("daemon mode — creating per-session IPC socket");
|
||||
const handle = await runDaemon();
|
||||
const stop = () => void handle.stop().then(() => process.exit(0));
|
||||
process.on("SIGINT", stop);
|
||||
process.on("SIGTERM", stop);
|
||||
await new Promise(() => {});
|
||||
} else if (opts.attach !== null) {
|
||||
const { runAttach } = await import("@zesdex/daemon");
|
||||
console.info(`attach mode for daemon session '${opts.attach}'`);
|
||||
const store = newStore();
|
||||
const socketPath = path.join(store.base_dir, "run", `${opts.attach}.sock`);
|
||||
await runAttach(socketPath);
|
||||
} else if (opts.api) {
|
||||
const { newApiState, startApiServer } = await import("@zesdex/api");
|
||||
const baseDir =
|
||||
process.env.ZESDEX_STORE ?? path.join(os.homedir(), ".local", "share", "zesdex");
|
||||
const state = newApiState(
|
||||
baseDir,
|
||||
process.env.ZESDEX_JWT_SECRET ?? "dev-secret-change-me",
|
||||
process.env.OPENAI_API_KEY ?? "",
|
||||
process.env.ZESDEX_MODEL ?? "claude-opus-5",
|
||||
process.env.OPENAI_API_BASE ?? undefined,
|
||||
);
|
||||
startApiServer(state, opts.apiPort);
|
||||
await new Promise(() => {});
|
||||
} else if (opts.ws) {
|
||||
const { startWsServer } = await import("@zesdex/ws");
|
||||
startWsServer(opts.wsPort);
|
||||
await new Promise(() => {});
|
||||
} else if (opts.grpc) {
|
||||
const { startGrpcServer } = await import("@zesdex/grpc");
|
||||
startGrpcServer(opts.grpcPort);
|
||||
await new Promise(() => {});
|
||||
} else if (opts.web) {
|
||||
const { startWebServer } = await import("@zesdex/web");
|
||||
startWebServer(opts.webPort, process.env.ZESDEX_WEB_DIR ?? undefined);
|
||||
await new Promise(() => {});
|
||||
} else {
|
||||
// Default: single-process REPL.
|
||||
await runRepl();
|
||||
}
|
||||
// Default: boot the interactive TUI.
|
||||
const { runTui } = await import("@zesdex/tui");
|
||||
await runTui();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
|
||||
Reference in New Issue
Block a user