Repository navigation
Expand file tree
/
Copy pathdriver-availability.ts
More file actions
202 lines (181 loc) · 6.84 KB
/
Copy pathdriver-availability.ts
File metadata and controls
202 lines (181 loc) · 6.84 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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
import { constants as fsConstants, promises as fs } from "node:fs";
import { execFile } from "node:child_process";
import path from "node:path";
import { promisify } from "node:util";
import { DRIVER_BINARIES, DRIVER_DEFINITIONS, resolveDriverBinary, type DriverBinaryEnvVar, type DriverId } from "./driver-registry";
export type DriverAvailabilityStatus = "available" | "unavailable";
export type DriverAvailabilityItem = {
label: string;
driverId: DriverId;
status: DriverAvailabilityStatus;
available: boolean;
reason: string;
setupHint: string;
envVar?: DriverBinaryEnvVar;
};
export type DriverBinaryCheck = (binaryPath: string) => boolean | Promise<boolean>;
export type DriverAvailabilityEnv = Record<string, string | undefined>;
export type PiRuntimeCheckResult = {
available: boolean;
reason?: string;
setupHint?: string;
};
export type PiRuntimeCheck = (
binaryPath: string,
env: DriverAvailabilityEnv,
) => PiRuntimeCheckResult | Promise<PiRuntimeCheckResult>;
const execFileAsync = promisify(execFile);
export async function getDriverAvailability(options: {
env?: DriverAvailabilityEnv;
checkBinary?: DriverBinaryCheck;
checkPiRuntime?: PiRuntimeCheck;
} = {}): Promise<DriverAvailabilityItem[]> {
const env = options.env ?? process.env;
const checkBinary = options.checkBinary
?? ((binaryPath: string) => defaultBinaryCheck(binaryPath, env));
const checkPiRuntime = options.checkPiRuntime ?? defaultPiRuntimeCheck;
return Promise.all(
DRIVER_DEFINITIONS.map(async (definition) => {
if (definition.driverId === "webhook_agent") {
return {
label: definition.label,
driverId: definition.driverId,
status: "available",
available: true,
reason: "Webhook agents do not require a local CLI.",
setupHint: definition.setupHint,
} satisfies DriverAvailabilityItem;
}
const binaryPath = resolveDriverBinary(definition.driverId, env) ?? "";
let available = binaryPath ? await checkBinary(binaryPath) : false;
let runtimeReason: string | undefined;
let runtimeSetupHint: string | undefined;
if (available && definition.driverId === "pi_terminal") {
const runtime = await checkPiRuntime(binaryPath, env);
available = runtime.available;
runtimeReason = runtime.reason;
runtimeSetupHint = runtime.setupHint;
}
return {
label: definition.label,
driverId: definition.driverId,
status: available ? "available" : "unavailable",
available,
reason: runtimeReason ?? (available
? `${definition.label} CLI is available.`
: `${definition.label} CLI was not found.`),
setupHint: runtimeSetupHint ?? definition.setupHint,
envVar: DRIVER_BINARIES[definition.driverId]?.envVar,
} satisfies DriverAvailabilityItem;
}),
);
}
export async function defaultPiRuntimeCheck(
binaryPath: string,
env: DriverAvailabilityEnv = process.env,
): Promise<PiRuntimeCheckResult> {
const executable = await resolveExecutable(binaryPath, env);
if (!executable) {
return {
available: false,
reason: "Pi Agent executable could not be resolved for runtime validation.",
setupHint: "Install Pi Agent or set CHORUZ_PI_BINARY to an executable path.",
};
}
let entrypoint: string;
let shebang: string;
try {
entrypoint = await fs.realpath(/* turbopackIgnore: true */ executable);
shebang = (await fs.readFile(/* turbopackIgnore: true */ entrypoint, "utf8"))
.split(/\r?\n/, 1)[0] ?? "";
} catch {
return {
available: false,
reason: "Pi Agent executable could not be inspected for Node compatibility.",
setupHint: "Reinstall Pi Agent or set CHORUZ_PI_BINARY to a valid executable wrapper.",
};
}
const nodeBinary = await nodeInterpreterFromShebang(shebang, env);
// An explicit native/shell wrapper may manage its own compatible runtime.
// Do not guess a different global Node binary behind the operator's wrapper.
if (!nodeBinary) return { available: true };
try {
const { stdout } = await execFileAsync(
nodeBinary,
[
"-e",
"const z=require('node:zlib');process.stdout.write(JSON.stringify({version:process.version,zstd:typeof z.createZstdDecompress==='function'}))",
],
{
env: { ...process.env, ...env },
timeout: 5_000,
maxBuffer: 16 * 1024,
},
);
const probe = JSON.parse(stdout) as { version?: string; zstd?: boolean };
if (probe.zstd) return { available: true };
return incompatiblePiNode(probe.version, "a Node runtime with zlib.createZstdDecompress");
} catch {
return {
available: false,
reason: "Pi Agent's configured Node runtime could not complete its compatibility probe.",
setupHint:
"Put a Pi-compatible Node runtime first on PATH, or set CHORUZ_PI_BINARY to an explicit wrapper that launches Pi with one.",
};
}
}
export async function defaultBinaryCheck(
binaryPath: string,
env: DriverAvailabilityEnv = process.env,
): Promise<boolean> {
if (binaryPath.includes("/") || binaryPath.startsWith(".")) {
return canExecute(binaryPath);
}
const pathEntries = (env.PATH ?? "")
.split(path.delimiter)
.filter(Boolean);
for (const entry of pathEntries) {
if (await canExecute(path.join(entry, binaryPath))) return true;
}
return false;
}
async function canExecute(candidate: string): Promise<boolean> {
try {
await fs.access(candidate, fsConstants.X_OK);
return true;
} catch {
return false;
}
}
async function resolveExecutable(
binaryPath: string,
env: DriverAvailabilityEnv,
): Promise<string | undefined> {
if (binaryPath.includes("/") || binaryPath.startsWith(".")) {
return (await canExecute(binaryPath)) ? path.resolve(binaryPath) : undefined;
}
for (const entry of (env.PATH ?? process.env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
const candidate = path.join(entry, binaryPath);
if (await canExecute(candidate)) return candidate;
}
return undefined;
}
async function nodeInterpreterFromShebang(
shebang: string,
env: DriverAvailabilityEnv,
): Promise<string | undefined> {
if (/^#!\s*\/usr\/bin\/env(?:\s+-S)?\s+node(?:\s|$)/.test(shebang)) {
return resolveExecutable("node", env);
}
const match = shebang.match(/^#!\s*(\/\S*\/node)(?:\s|$)/);
return match?.[1];
}
function incompatiblePiNode(version: string | undefined, requirement: string): PiRuntimeCheckResult {
const actual = version?.trim() || "an unknown Node version";
return {
available: false,
reason: `Pi Agent requires ${requirement}; its configured launcher resolves ${actual}, which is incompatible.`,
setupHint:
"Put a compatible Node runtime first on PATH, or set CHORUZ_PI_BINARY to an explicit wrapper that launches Pi with one. Choruz will not guess another global Node installation.",
};
}