从零开发一个 Coding Agent(十二):实现版本化 JSONL 与真实 CLI 入口
ä»é¶å¼åä¸ä¸ª Coding Agentï¼åäºï¼ï¼å®ç°çæ¬å JSONL ä¸çå® CLI å ¥å£
䏿¹å°æ 2026-08-20 0 é 读5åéCoding Agentç®åå·²ç»åå¸å°npmï¼å¤§å®¶å¯ä»¥å è¡ç´æ¥å ä½éªä¸ä¸æç»ä¼åæä»ä¹æ ·ãgithubå°åï¼github.com æ¬¢è¿ Star æ¯æï¼npmå°å@di-code/coding-agent - npmãç´æ¥
npm install -g @di-code/coding-agentå³å¯å®è£ 使ç¨
æ¬ç¯æç« æ¯ãä»é¶å¼åä¸ä¸ª Coding Agentãç³»å第åäºç¯ãå¨å两ç¯ä¸ï¼æä»¬å·²ç»å®æäº CLI åæ°è§£æå print 模å¼ï¼
åæ°æ°ç» -> runCli() -> Agent.prompt() -> æç»ææ¬ -> stdout
print 模å¼éåäººç´æ¥é 读ï¼ä½èæ¬åå ¶ä»ç¨åºå¾å¾éè¦æ´å¤ä¿¡æ¯ãä¾å¦ï¼
- Agent ä»ä¹æ¶åå¼å§å·¥ä½ï¼
- æ¨¡åæ£å¨çæä»ä¹å 容ï¼
- æ¯å¦æ§è¡äºå·¥å ·ï¼
- è¿æ¬¡è¯·æ±æåæ¯æåãåæ¶è¿æ¯å¤±è´¥ï¼
- æç» transcript æ¯ä»ä¹ï¼
妿åªè¾åºæç»ææ¬ï¼è¿äºè¿ç¨å°±å ¨é¨ä¸¢å¤±äºãå æ¤è¿ä¸ç¯å¢å 第äºç§è¾åºæ¨¡å¼ï¼JSONL 模å¼ã
JSONLï¼JSON Linesï¼æè¡æåç JSONï¼ä¸æ¯ä¸ä¸ªå·¨å¤§ç JSON æ°ç»ï¼èæ¯âæ¯è¡ä¸ä¸ªç¬ç« JSON 对象âï¼
{"version":1,"event":{"type":"agent_start"}}
{"version":1,"event":{"type":"turn_start"}}
{"version":1,"event":{"type":"agent_end","messages":[...]}}
宿åï¼ç¨åºè¿è¦çæ£æä¸ºä¸ä¸ªå¯ä»¥ä»å½ä»¤è¡å¯å¨ç Node.js ç¨åºï¼è¯»å process.argvï¼æ stdout/stderr æ¥å°çå®ç»ç«¯ï¼å¹¶éè¿ package ç bin åæ®µæä¾ di-code å½ä»¤ã
æ¬ç¯ä»ç¶ä½¿ç¨ Faux Providerãå®åªè¿ååºå®ç Faux response.ï¼ä¸è®¿é®ç½ç»ãä¸è¯»å API Keyï¼ä¹ä¸æåå®ç° read å·¥å
·ãä¼è¯å卿çå® Providerã
JSON 模å¼ä¸ print 模å¼çåºå«
ä¸¤ç§æ¨¡å¼é½è°ç¨åä¸ä¸ª Agentï¼ä½è§å¯ç®æ ä¸åï¼
| æ¨¡å¼ | æ¶è´¹ä»ä¹ | stdout å 容 |
|---|---|---|
æç» AssistantMessage | æç» text block | |
| JSON | æ¯ä¸ä¸ª AgentEvent | ä¸è¡ä¸ä¸ªçæ¬å JSON record |
å¯ä»¥æ Agent æ³æä¸åºæ¯èµï¼
- print 模å¼åªå ³å¿æåçæ¯åï¼
- JSON 模å¼è¦è®°å½åä»¤ãæ¯ä¸å忝èµç»æç宿´è¿ç¨ã
å æ¤ä¸è½æ JSON 模å¼å®ç°æâè°ç¨ runPrintMode() ååæææ¬å
æ JSONâã飿 ·ä¼ä¸¢å¤± agent_startãmessage_update å agent_end çäºä»¶ã
为ä»ä¹æ¯ä¸è¡é½è¦å¸¦çæ¬
æç®åç JSON è¾åºå¯è½æ¯ï¼
{"event":{"type":"agent_start"}}
使ªæ¥æä»¬å¯è½å¢å åæ®µãæ¹åäºä»¶ç»æï¼æè æ°å¢äºä»¶ç±»åãæ¶è´¹è æ¿å°ä¸è¡åï¼éè¦ç¥éå®éµå¾ªåªä¸çåè®®ãå æ¤æ¬é¡¹ç®åºå®ä½¿ç¨ï¼
export const JSON_EVENT_VERSION = 1 as const;
export interface JsonEventRecord {
readonly version: typeof JSON_EVENT_VERSION;
readonly event: AgentEvent;
}
è¾åºå¯¹è±¡çå½¢ç¶æ¯ï¼
{
version: 1,
event: AgentEvent,
}
çæ¬æ¾å¨æ¯ä¸è¡ï¼è䏿¯åªæ¾é¦è¡ï¼æä¸¤ä¸ªå¥½å¤ï¼
- æ¶è´¹è å¯ä»¥ç¬ç«è§£æä»»æä¸è¡ï¼ä¸å¿ ä¾èµåé¢çç¶æã
- æ¥å¿è¢«æªæãåçæä»ä¸é´å¼å§è¯»åæ¶ï¼ä»ç¶è½è¯å«åè®®çæ¬ã
è¿æ¯ä¸ç§å°èéè¦çå ¬å ±æ¥å£è®¾è®¡ãJSONL 䏿¯ä¸´æ¶è°è¯ææ¬ï¼èæ¯ CLI ä¸èæ¬ä¹é´çéä¿¡å¥çº¦ã
䏿¬¡ JSON å½ä»¤çæ°æ®æµ
ä¸é¢æ¯ --mode json hello ç宿´è·¯å¾ï¼
sequenceDiagram
participant OS as æä½ç³»ç»
participant Entry as entry.ts
participant Main as runMain
participant CLI as runCli
participant Agent as Agent
participant Json as runJsonMode
OS->>Entry: process.argv / stdout / stderr
Entry->>Main: åæ°å I/O writer
Main->>CLI: run åè°
CLI-->>Main: { kind: 'run', mode: 'json' }
Main->>Agent: å建 Faux Provider + Agent
Main->>Json: prompt + Agent + writer
Json->>Agent: subscribe(listener)
Json->>Agent: prompt("hello")
Agent-->>Json: AgentEvent 1, 2, ...
Json-->>OS: æ¯ä¸ªäºä»¶ä¸è¡ JSON
Agent-->>Json: AssistantMessage
Json-->>OS: è¿åéåºç 0 æ 1
Json->>Agent: finally unsubscribe()
è¿éè¦æ³¨æä¸¤ä¸ªæ¶é´ç¹ï¼è®¢é
å¿
é¡»åçå¨è°ç¨ prompt() ä¹åï¼å¦åå¼å¤´äºä»¶å¯è½å·²ç»ååºï¼åæ¶è®¢é
å¿
é¡»æ¾å¨ finallyï¼å¦åæåã失败å reject 䏿¡è·¯å¾ä¼åºç°ä¸åçæ¸
çè¡ä¸ºã
ç¬¬ä¸æ¥ï¼å®ç° JSON è¾åºæ¨¡å¼
å建æä»¶ï¼
di-code/packages/coding-agent/src/modes/json.ts
å å¯¼å ¥ Agent äºä»¶ãå©ææ¶æ¯åä¸ä¸ç¯å®ä¹ç I/Oï¼
import type { AgentEvent, AgentListener } from "@di-code/agent";
import type { AssistantMessage } from "@di-code/ai";
import type { PrintIo } from "./print.ts";
AgentListener çå½¢ç¶æ¯æ¥æ¶ä¸ä¸ª AgentEvent ç彿°ï¼å¹¶å¯ä»¥è¿å PromiseãJSON writer æ¬èº«æ¯åæ¥çï¼æä»¥è¿éåªéè¦æäºä»¶å
è£
åäº¤ç» stdoutã
ç¶åå®ä¹åè®®çæ¬åæå°è¿è¡å¨æ¥å£ï¼
export const JSON_EVENT_VERSION = 1 as const;
export interface JsonEventRecord {
readonly version: typeof JSON_EVENT_VERSION;
readonly event: AgentEvent;
}
export interface JsonRunner {
prompt(text: string): Promise<AssistantMessage>;
subscribe(listener: AgentListener): () => void;
}
JsonRunner æ¯å®æ´ç Agent æ´å°ï¼ä½å®æ¯ PromptRunner å¤äº subscribe()ãè¿æ¯å 为 JSON 模å¼è¦è§å¯è¿ç¨äºä»¶ï¼ä¸è½åªçå¾
æç»æ¶æ¯ã
ææªç¥å¼å¸¸è½¬æ Error
å print 模å¼ä¸æ ·ï¼å¤é¨ Promise å¯è½ reject ä¸ä¸ªåç¬¦ä¸²ãæ°åææ®é对象ãå
ç»ä¸ä¸º Errorï¼
function toError(cause: unknown): Error {
return cause instanceof Error ? cause : new Error(String(cause));
}
å®ç° runJsonMode
ç»§ç»å¨åä¸ä¸ªæä»¶ä¸å å ¥ï¼
export async function runJsonMode(prompt: string, runner: JsonRunner, io: PrintIo): Promise<number> {
const unsubscribe = runner.subscribe((event) => {
const record: JsonEventRecord = { version: JSON_EVENT_VERSION, event };
io.stdout(`${JSON.stringify(record)} `);
});
try {
const assistant = await runner.prompt(prompt);
if (assistant.stopReason === "error" || assistant.stopReason === "aborted") {
io.stderr(`${assistant.errorMessage} `);
return 1;
}
return 0;
} catch (cause) {
io.stderr(`${toError(cause).message} `);
return 1;
} finally {
unsubscribe();
}
}
éæ®µçè¿æ®µä»£ç ï¼
subscribe()æ¾å¨tryä¹åè°ç¨ï¼å¹¶ç«å³ä¿ååæ¶è®¢é 彿°ãè¿æ ·prompt()ååºç第ä¸ä¸ªäºä»¶ä¹ä¸ä¼ä¸¢å¤±ã- listener æ¯æ¶å°ä¸ä¸ªäºä»¶ï¼å°±å建
{ version: 1, event }ï¼ä½¿ç¨JSON.stringify()转æä¸è¡ææ¬ï¼å¹¶è¡¥ä¸æ¢è¡ç¬¦ã prompt()è¿åå¤±è´¥æ¶æ¯æ¶ï¼å·²ç»äº§çç JSONL ä»ç¶ä¿çå¨ stdoutï¼é误说æå¦å¤åå ¥ stderrï¼éåºç è¿å1ãprompt()reject æ¶åæ ·åªå stderrï¼ä¸æå¼å¸¸å æ æ±¡æ JSONLãfinallyæ 论æåãç»æåå¤±è´¥è¿æ¯å¼å¸¸ï¼é½æ§è¡unsubscribe()ã
为ä»ä¹ä¸æææäºä»¶å æ¶éèµ·æ¥
JSONL çä»·å¼ä¹ä¸å°±æ¯å®æ¶æ§ãæ¶è´¹è å¯ä»¥å¨ Agent ä»ç¶è¿è¡æ¶è¯»å第ä¸è¡ã第äºè¡ï¼èä¸ç¨çæ´ä¸ªè¯·æ±ç»æãéäºä»¶ååºè¿å¯ä»¥éä½å åå ç¨ï¼å¹¶ä¿çäºä»¶åç顺åºã
为ä»ä¹å¤±è´¥äºä»¶ä»çå¨ stdout
失败ä¹å¯è½å·²ç»äº§çäºæç¨ççå½å¨æäºä»¶ï¼ä¾å¦ï¼
agent_start
turn_start
message_start
message_end(error)
agent_end
è¿äºäºä»¶æ¯åæ³ç AgentEventï¼åºè¯¥ç
§å¸¸è®°å½ãstderr åªè¡¥å
é¢å人çè¯æææ¬ï¼ä¸è½ä¸ºäºæ¥éèæ¸
空æè¦çå·²ç»è¾åºç JSONLã
ç¬¬äºæ¥ï¼ä¸º JSON 模å¼ç¼ååå æµè¯
å建ï¼
di-code/packages/coding-agent/test/json.test.ts
è¿éä½¿ç¨ fake JsonRunnerï¼ä¸é¨æµè¯è¾åºæå½±ï¼ä¸é夿µè¯ Agent Loopã
æµè¯æ¯ä¸ªäºä»¶ç¬å ä¸è¡
æµè¯ runner å¯ä»¥å¨ prompt() 䏿å¨è§¦å订é
è
ï¼
function createRunner(options: { message?: AssistantMessage; reject?: Error; events?: AgentEvent[] }) {
let listener: AgentListener | undefined;
const unsubscribe = vi.fn();
const runner: JsonRunner = {
subscribe(next) {
listener = next;
return unsubscribe;
},
async prompt() {
for (const event of options.events ?? []) {
await listener?.(event);
}
if (options.reject) {
throw options.reject;
}
return options.message ?? assistant("stop");
},
};
return { runner, unsubscribe };
}
ç¶åéªè¯æ¯ä¸è¡é½è½ç¬ç«è§£æï¼
const io = createIo();
const { runner } = createRunner({
events: [{ type: "agent_start" }, { type: "turn_start" }],
});
expect(await runJsonMode("hello", runner, io)).toBe(0);
const records = io.stdout.mock.calls.map(
([line]) => JSON.parse(line.trim()) as { version: number; event: AgentEvent },
);
expect(records).toHaveLength(2);
expect(records.every((record) => record.version === 1)).toBe(true);
expect(records.map((record) => record.event.type)).toEqual(["agent_start", "turn_start"]);
expect(io.stderr).not.toHaveBeenCalled();
è¿éç JSON.parse() æ¯ç´æ¥æ£æ¥åç¬¦ä¸²æ´ææä¹ï¼å 为å®è¯ææ¶è´¹è
ççå¯ä»¥è¯»åè¾åºã
æµè¯å¤±è´¥æ¶æ¯å reject
å¤±è´¥æ¶æ¯çæµè¯è¦åæ¶æè¨ stdout å stderrï¼
const io = createIo();
const { runner } = createRunner({
message: assistant("error", "model failed"),
events: [{ type: "agent_start" }],
});
expect(await runJsonMode("fail", runner, io)).toBe(1);
expect(io.stdout).toHaveBeenCalledTimes(1);
expect(io.stderr).toHaveBeenCalledWith("model failed ");
åéªè¯ Promise rejection 伿¸ ç订é ï¼
const io = createIo();
const { runner, unsubscribe } = createRunner({ reject: new Error("listener failed") });
expect(await runJsonMode("reject", runner, io)).toBe(1);
expect(io.stderr).toHaveBeenCalledWith("listener failed ");
expect(unsubscribe).toHaveBeenCalledTimes(1);
è¿è¡ï¼
Set-Location D:\pi\di-code
npm test --workspace packages/coding-agent -- --run json.test.ts
å¨ GREEN é¶æ®µåºçå° 1 个æµè¯æä»¶ã3 个æµè¯å ¨é¨éè¿ã
ç¬¬ä¸æ¥ï¼è®© runMain åæ´¾ JSON
ä¸ä¸ç¯ç runMain() 对 JSON è¿è¿åå ä½é误ãç°å¨æå¼ï¼
di-code/packages/coding-agent/src/main.ts
å¢å importï¼
import { runJsonMode } from "./modes/json.ts";
ç¶åæ run åè°åºå®ä¸ºâåªåå»ºä¸æ¬¡è¿è¡æ¶ï¼åææ¨¡å¼éæ©è¾åºå±âï¼
run: async (command) => {
const faux = createFauxProvider({ responses: options.fauxResponses, now: options.now });
const agent = new Agent({ provider: faux.provider, model: faux.model, now: options.now });
if (command.mode === "json") {
return runJsonMode(command.prompt, agent, options);
}
return runPrintMode(command.prompt, agent, options);
},
è¿éä¸è½å¤å¶ parseCliArgs()ï¼ä¹ä¸è½è®© JSON 模å¼èªå·±å建å¦ä¸ä»½ Agentãä¸¤ç§æ¨¡å¼åºè¯¥å
±äº«å䏿¡ AgentSession è¾¹çï¼åªæ¹åâææ ·è§å¯åè¾åºç»æâã
æ´æ° main éææµè¯
æå¼ï¼
di-code/packages/coding-agent/test/main.test.ts
æ 6b ä¸âJSON å°æªå®ç°âçæµè¯æ¹ææåæè¨ï¼
it("runs a faux prompt through versioned JSON mode", async () => {
const io = createIo();
const exitCode = await runMain(["--mode", "json", "hello"], {
...io,
version: "0.0.0",
fauxResponses: [{ type: "success", content: [{ type: "text", text: "done" }] }],
});
expect(exitCode).toBe(0);
expect(io.stderr).not.toHaveBeenCalled();
const records = io.stdout.mock.calls.map(
([line]) => JSON.parse(line.trim()) as { version: number; event: { type: string } },
);
expect(records.length).toBeGreaterThan(0);
expect(records.every((record) => record.version === 1)).toBe(true);
expect(records.map((record) => record.event.type)).toContain("agent_start");
expect(records.map((record) => record.event.type)).toContain("agent_end");
});
è¿è¡å彿µè¯ï¼
Set-Location D:\pi\di-code
npm test --workspace packages/coding-agent -- --run main.test.ts
npm test --workspace packages/coding-agent -- --run print.test.ts
npm test --workspace packages/coding-agent -- --run cli.test.ts
颿åå«ä¸º 3/3ã4/4 å 8/8ãè¿è¯æ JSON æ¥å
¥æ²¡æç ´åå·²æ print ååæ°è¡ä¸ºã
ç¬¬åæ¥ï¼åå»ºçæ£ç Node å ¥å£
å°ç°å¨ä¸ºæ¢ï¼runMain() ä»ç¶æ¯ä¸ä¸ªå¯æµè¯å½æ°ãç¨æ·ç´æ¥è¿è¡ç¨åºæ¶ï¼è¿éè¦ä¸ä¸ªå¾èçå
¥å£æ Node å
¨å±å¯¹è±¡æ¥è¿æ¥ã
å建ï¼
di-code/packages/coding-agent/src/entry.ts
åå ¥ï¼
#!/usr/bin/env node
import packageMetadata from "../package.json" with { type: "json" };
import { runMain } from "./main.ts";
const exitCode = await runMain(process.argv.slice(2), {
version: packageMetadata.version,
fauxResponses: [{ type: "success", content: [{ type: "text", text: "Faux response." }] }],
stdout: (text) => process.stdout.write(text),
stderr: (text) => process.stderr.write(text),
});
process.exitCode = exitCode;
éè¡çè§£ï¼
- shebang 让 Unix ç¯å¢å¯ä»¥æçæåçæä»¶å½ä½èæ¬æ§è¡ï¼Windows ä»ç± Node/npm è´è´£å¯å¨ã
process.argv.slice(2)廿 Node åèæ¬è·¯å¾ï¼åªæç¨æ·åæ°äº¤ç»runMain()ã- package JSON æä¾å½åçæ¬ï¼ä¸å¨ CLI è§£æå¨ä¸ç¡¬ç¼ç æä»¶è¯»åã
fauxResponsesæ¯å½åæ ç½ç»åç´åççåºå®ååºï¼å®ä¸æ¯ç¨æ·è¾å ¥ï¼ä¹ä¸æ¯æªæ¥çå® Provider çæ¿ä»£åã- writer ææ¨¡å¼å±ç
PrintIoæ¥å°çå® stdout/stderrã process.exitCode设置éåºç¶æï¼ä½ä¸ä¼ç«å»ä¸æå¼æ¥æ¸ çã
å
¥å£æä»¶åºè¯¥ä¿æå¾èãåæ°è¯æ³å±äº cli.tsï¼æ¨¡å¼è¡ä¸ºå±äº modes/ï¼è¿è¡æ¶ç»åå±äº main.tsï¼entry.ts åªè´è´£æ¥çº¿ã
ç¬¬äºæ¥ï¼é ç½® package å½ä»¤å bin
卿 ¹ç®å½ç di-code/package.json ä¸å å
¥ï¼
"dev": "node --experimental-strip-types packages/coding-agent/src/entry.ts"
--experimental-strip-types 让å½å Node çæ¬ç´æ¥è¿è¡ TypeScript ç±»åæ¦é¤åçæºç ï¼é忬å¦ä¹ 项ç®çå¼åå
¥å£ãå®ä¸ä¼æ¿ä»£æ£å¼æå»ºã
å¨ di-code/packages/coding-agent/package.json ä¸å å
¥ï¼
"bin": {
"di-code": "./dist/entry.js"
},
"scripts": {
"build": "tsc -p tsconfig.build.json",
"dev": "node --experimental-strip-types src/entry.ts",
"test": "vitest run --passWithNoTests"
}
bin çå«ä¹æ¯ï¼å®è£
æé¾æ¥è¿ä¸ª package åï¼å½ä»¤ di-code æåæå»ºäº§ç© dist/entry.jsãä¸è¦æå src/entry.tsï¼å 为åå¸ååè¿ç¨æµè¯ä½¿ç¨çæ¯æå»ºåç JavaScriptã
为ä»ä¹ dev ä¸èªå¨ build
妿 dev å
æ§è¡ buildï¼åå¯å¨ç¨åºï¼build çè¾åºå¯è½æ··å
¥æºå¨æ£å¨æ¶è´¹ç stdoutï¼è䏿¯æ¬¡è¿è¡é½å¢å é¢å¤æ¥éª¤ãæå»ºåºè¯¥ç±ç¬ç«ç npm run build 宿ï¼å¼å smoke test åè¿è¡æºç å
¥å£ã
ä½¿ç¨ npm æ§è¡ JSON å½ä»¤æ¶ï¼å»ºè®®å --silentï¼
npm run --silent dev -- --mode json hello
æ®é npm run dev å¯è½ç± npm èªå·±æå°èæ¬ bannerãé£ä¸æ¯åºç¨è¾åºï¼ä½ä¼è®©éè¡ JSON æ¶è´¹è
çå°é JSON ææ¬ãç´æ¥è¿è¡ node dist/entry.js æä½¿ç¨å®è£
åç di-code bin ä¹å¯ä»¥é¿å
è¿ä¸ªé®é¢ã
ç¬¬å æ¥ï¼ç¨çå®åè¿ç¨æµè¯å ¥å£
ç´æ¥è°ç¨ runMain() åªè½è¯æå½æ°ç»åæ£ç¡®ï¼ä¸è½è¯æï¼
process.argv.slice(2)æ¯å¦æ£ç¡®ï¼- package JSON æ¯å¦è½è¢«å ¥å£å è½½ï¼
process.exitCodeæ¯å¦ä¼ å°äºæä½ç³»ç»ï¼- stdout/stderr æ¯å¦ççå离ï¼
- æå»ºäº§ç©
dist/entry.jsæ¯å¦å¯ä»¥å¯å¨ã
å æ¤å建ï¼
di-code/packages/coding-agent/test/cli-process.test.ts
æµè¯ä½¿ç¨ Node ç spawn() å¯å¨ç¬ç«è¿ç¨ï¼
const entryPath = resolve(process.cwd(), "dist/entry.js");
async function runCli(args: string[]): Promise<{ code: number | null; stdout: string; stderr: string }> {
return await new Promise((resolveResult, reject) => {
const child = spawn(process.execPath, [entryPath, ...args], {
cwd: process.cwd(),
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk: Buffer) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk: Buffer) => {
stderr += chunk.toString();
});
child.on("error", reject);
child.on("close", (code) => resolveResult({ code, stdout, stderr }));
});
}
ç¶åè¦çäºæ¡å¤é¨è¡ä¸ºï¼
it("prints help without runtime diagnostics", async () => {
const result = await runCli(["--help"]);
expect(result.code).toBe(0);
expect(result.stdout).toContain("Usage: di-code");
expect(result.stderr).toBe("");
});
it("prints the package version", async () => {
const result = await runCli(["--version"]);
expect(result.code).toBe(0);
expect(result.stdout).toBe("0.0.0 ");
expect(result.stderr).toBe("");
});
it("runs the deterministic print path", async () => {
const result = await runCli(["--print", "hello"]);
expect(result.code).toBe(0);
expect(result.stdout).toBe("Faux response. ");
expect(result.stderr).toBe("");
});
it("writes versioned JSON events", async () => {
const result = await runCli(["--mode", "json", "hello"]);
expect(result.code).toBe(0);
expect(result.stderr).toBe("");
const records = result.stdout
.trim()
.split(" ")
.map((line) => JSON.parse(line) as { version: number; event: { type: string } });
expect(records.length).toBeGreaterThan(0);
expect(records.every((record) => record.version === 1)).toBe(true);
expect(records.map((record) => record.event.type)).toContain("agent_start");
expect(records.map((record) => record.event.type)).toContain("agent_end");
});
it("keeps usage errors off stdout", async () => {
const result = await runCli(["--unknown"]);
expect(result.code).toBe(1);
expect(result.stdout).toBe("");
expect(result.stderr).toContain('Unknown option "--unknown".');
});
è¿ç±»æµè¯æ¯åå æµè¯æ´æ¥è¿ç¨æ·å®é ä½éªï¼å 为å®éªè¯çæ¯å®æ´çè¿ç¨è¾¹çï¼è䏿¯æä¸ªå½æ°çè¿åå¼ã
è¿è¡åå¿ é¡»å æå»ºï¼
Set-Location D:\pi\di-code
npm run build
npm test --workspace packages/coding-agent -- --run cli-process.test.ts
颿åè¿ç¨æµè¯ä¸º 1 个æä»¶ã5 个æµè¯å ¨é¨éè¿ã
æç»éªè¯ä¸ smoke test
宿ä¸è¿°æ¥éª¤åï¼è¿è¡ï¼
Set-Location D:\pi\di-code
npm run build
npm test --workspace packages/coding-agent
npm run check
npm run dev -- --help
npm run dev -- --version
npm run dev -- --print hello
npm run --silent dev -- --mode json hello
é¢æç»æï¼
- coding-agent æ¶é 5 个æµè¯æä»¶ã23 个æµè¯ï¼å ¨é¨éè¿ã
- æ ¹
npm run checkåæ ¹npm run buildæåã - help è¾åº Usageï¼éåºç 为
0ï¼ä¸éè¦åæ®ã - version è¾åº
0.0.0ã - print stdout åªæ
Faux response.ã - JSON stdout çæ¯ä¸ªé空è¡é½è½ç¬ç«
JSON.parseï¼ä¸å¸¦æversion: 1ï¼å ¶ä¸å å«agent_startåagent_endã - æ£å¸¸ JSON è¿è¡ç stderr 为空ã
æ£æ¥ JSON è¾åºæ¶ï¼å¯ä»¥å¨ PowerShell ä¸è¿æ ·è§å¯è¡æ°ï¼
$jsonLines = npm run --silent dev -- --mode json hello
$jsonLines | ForEach-Object { $_ | ConvertFrom-Json }
妿æä¸è¡ä¸æ¯ JSONï¼ConvertFrom-Json ä¼ç«å³æ¥éï¼è¿æ¯èç¼æ¥çé¿å符串å¯é ã
æ»ç»
è¿ä¸ç¯å®æäº CLI çç¬¬ä¸æ¡å®æ´äº§åé¾è·¯ï¼
runJsonMode()订é AgentEventï¼ææ¯ä¸ªäºä»¶å è£ ä¸º{ version: 1, event }ã- æ¯ä¸ª JSON record ç¬å ä¸è¡ï¼æ¶è´¹è å¯ä»¥éè¡è¯»ååè§£æã
- ç»æå失败ä»ä¿çå·²ç»äº§çç JSONLï¼è¯æåªå stderrï¼éåºç 为
1ã finallyä¸åæ¶è®¢é ï¼é¿å æåã失败å reject è·¯å¾çä¸ listenerãrunMain()å¨åä¸ä¸ª Agent ä¸éæ© print æ JSON è¾åºï¼ä¸å¤å¶ CLI è§£æé»è¾ãentry.tsæprocess.argvãçå® stdout/stderr åéåºç æ¥å ¥åºç¨ãbinæådist/entry.jsï¼åè¿ç¨æµè¯éªè¯äºçå®çå½ä»¤è¡è¾¹çã
å°è¿éï¼Task 6 çåæ°ãprintãJSONL åå¼åå ¥å£å·²ç»è¿æä¸æ¡ç¡®å®æ§é¾è·¯ãå®è¿ä¸ä¼è¯»åæä»¶ï¼ä½å·²ç»å ·å¤äºä¸ä¸ªå¯è¢«èæ¬æ¶è´¹ãå¯éè¿äºä»¶è§å¯ãå¹¶ä¸ä¸ä¾èµçå®ç½ç»ç CLI åºç¡ã
ä¸ä¸ç¯å°è¿å
¥ Task 7ï¼å®ç°åå·¥ä½ç®å½çº¦æç read å·¥å
·ï¼å¹¶å®æâ模åè¯·æ± read -> æ¬å°æ§è¡ -> å·¥å
·ç»æåé -> 模åæç»åçâç端å°ç«¯æµç¨ã
弿ºå°åï¼github.com
Aitishiku.com