-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli-client.ts
More file actions
95 lines (78 loc) · 2.6 KB
/
Copy pathcli-client.ts
File metadata and controls
95 lines (78 loc) · 2.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import { spawn } from "node:child_process";
import type { CliConfig } from "../config/config.schema.js";
export class CliExitError extends Error {
public readonly exitCode: number;
public readonly stderr: string;
public constructor(exitCode: number, stderr: string, command: string) {
super(`CLI command failed with exit code ${exitCode}: ${command}`);
this.name = "CliExitError";
this.exitCode = exitCode;
this.stderr = stderr;
}
}
export class CliJsonParseError extends Error {
public constructor(raw: string) {
super(`Failed to parse CLI output as JSON. Raw output: ${raw.slice(0, 200)}`);
this.name = "CliJsonParseError";
}
}
export interface CliResult {
stdout: string;
stderr: string;
exitCode: number;
}
export class CliClient {
private readonly config: CliConfig;
public constructor(config: CliConfig) {
this.config = config;
}
/**
* Run the CLI with the given args. Never throws — always returns the result including
* non-zero exit codes. Use `runOrThrow` when a failure should surface as an error.
*/
public async run(args: string[]): Promise<CliResult> {
const fullArgs = [...(this.config.cliBaseArgs ?? []), ...args];
return new Promise((resolve, reject) => {
const proc = spawn(this.config.cliPath, fullArgs, {
cwd: this.config.cwd,
timeout: this.config.timeoutMs,
env: process.env
});
let stdout = "";
let stderr = "";
proc.stdout.on("data", (chunk: Buffer) => {
stdout += chunk.toString();
});
proc.stderr.on("data", (chunk: Buffer) => {
stderr += chunk.toString();
});
proc.on("close", (code) => {
resolve({ stdout: stdout.trim(), stderr: stderr.trim(), exitCode: code ?? 1 });
});
proc.on("error", reject);
});
}
/**
* Run the CLI and throw `CliExitError` if the exit code is non-zero.
*/
public async runOrThrow(args: string[]): Promise<CliResult> {
const result = await this.run(args);
if (result.exitCode !== 0) {
const fullCmd = [this.config.cliPath, ...(this.config.cliBaseArgs ?? []), ...args].join(" ");
throw new CliExitError(result.exitCode, result.stderr, fullCmd);
}
return result;
}
/**
* Run the CLI expecting JSON on stdout. Throws `CliExitError` on non-zero exit, or
* `CliJsonParseError` if the output cannot be parsed as JSON.
*/
public async runJson<T>(args: string[]): Promise<T> {
const result = await this.runOrThrow(args);
try {
return JSON.parse(result.stdout) as T;
} catch {
throw new CliJsonParseError(result.stdout);
}
}
}