diff options
| author | Adam Malczewski <[email protected]> | 2026-06-05 21:20:34 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-05 21:20:34 +0900 |
| commit | 552c22d74e5df915088d9e9ff4a286c96c2a54d6 (patch) | |
| tree | 7d9db1052bab91ef994446d80efc3bfc38026cad /packages/cli/src/main.ts | |
| parent | 7fb3269c698ae583ea7997ce206c4ae252fd3218 (diff) | |
| download | dispatch-552c22d74e5df915088d9e9ff4a286c96c2a54d6.tar.gz dispatch-552c22d74e5df915088d9e9ff4a286c96c2a54d6.zip | |
feat(cli): one-shot terminal client (models, chat, --text/--file/--cwd/--conversation)
HTTP client of transport-contract; pure-core arg/render/ndjson + injected fetch/fs shell.
Docs: GLOSSARY (credential/key/model name/model catalog), tasks.md milestone, ORCHESTRATOR geography.
Diffstat (limited to 'packages/cli/src/main.ts')
| -rw-r--r-- | packages/cli/src/main.ts | 74 |
1 files changed, 74 insertions, 0 deletions
diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts new file mode 100644 index 0000000..fc70c0c --- /dev/null +++ b/packages/cli/src/main.ts @@ -0,0 +1,74 @@ +/** + * Composition root — the thin, untested shell that wires everything together. + * + * Reads process.argv, reads files, writes to stdout/stderr. + * This is the ONLY file that touches I/O. + */ + +import { readFile } from "node:fs/promises"; +import { parseArgs } from "./args.js"; +import { formatCatalog } from "./catalog.js"; +import { fetchModels, streamChat } from "./http.js"; +import { buildChatRequest, composeMessage } from "./message.js"; +import { renderEvent } from "./render.js"; + +const USAGE = `Usage: + dispatch models [--server <url>] + dispatch <modelName> --text "..." [--file <path>] [--cwd <dir>] [--conversation <id>] [--server <url>] [--show-reasoning] + dispatch --help`; + +async function main(): Promise<void> { + const defaultServer = `http://localhost:${process.env.BACKEND_PORT ?? "24203"}`; + const parsed = parseArgs(process.argv.slice(2), { defaultServer }); + + switch (parsed.kind) { + case "help": + process.stdout.write(`${USAGE}\n`); + process.exit(0); + break; + case "error": + process.stderr.write(`Error: ${parsed.message}\n`); + process.exit(1); + break; + case "models": { + const result = await fetchModels({ fetchImpl: globalThis.fetch }, { server: parsed.server }); + process.stdout.write(`${formatCatalog(result)}\n`); + break; + } + case "chat": { + let fileContent: string | undefined; + if (parsed.file) { + fileContent = await readFile(parsed.file, "utf-8"); + } + + const cwd = parsed.cwd ?? process.cwd(); + const message = composeMessage({ + ...(parsed.text !== undefined && { text: parsed.text }), + ...(parsed.file !== undefined && { file: parsed.file }), + ...(fileContent !== undefined && { fileContent }), + }); + const request = buildChatRequest(parsed, { cwd, message }); + + const { conversationId, events } = await streamChat( + { fetchImpl: globalThis.fetch }, + { server: parsed.server, request }, + ); + + for await (const event of events) { + const rendered = renderEvent(event, { showReasoning: parsed.showReasoning }); + if (rendered?.stdout) process.stdout.write(rendered.stdout); + if (rendered?.stderr) process.stderr.write(rendered.stderr); + } + + if (conversationId) { + process.stdout.write(`\n[conversation] ${conversationId}\n`); + } + break; + } + } +} + +main().catch((err: unknown) => { + process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`); + process.exit(1); +}); |
