summaryrefslogtreecommitdiffhomepage
path: root/packages/cli/src/main.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-05 21:20:34 +0900
committerAdam Malczewski <[email protected]>2026-06-05 21:20:34 +0900
commit552c22d74e5df915088d9e9ff4a286c96c2a54d6 (patch)
tree7d9db1052bab91ef994446d80efc3bfc38026cad /packages/cli/src/main.ts
parent7fb3269c698ae583ea7997ce206c4ae252fd3218 (diff)
downloaddispatch-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.ts74
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);
+});