agencyx-cli 0.0.0-stage → 0.1.0-dev.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/adapters/agent.js +73 -0
  2. package/dist/adapters/ambient.js +35 -0
  3. package/dist/adapters/instructions.js +102 -0
  4. package/dist/adapters/mcp.js +237 -0
  5. package/dist/adapters/permissions.js +42 -0
  6. package/dist/adapters/plugins.js +484 -0
  7. package/dist/agency/invocation.js +97 -0
  8. package/dist/agency/prepared-input.js +124 -0
  9. package/dist/bridge/execution.js +119 -0
  10. package/dist/bridge/main.js +26 -0
  11. package/dist/bridge/plan.js +268 -0
  12. package/dist/bridge/version.js +2 -0
  13. package/dist/cli-options.js +96 -0
  14. package/dist/cli.js +41 -0
  15. package/dist/codex/agents.js +72 -0
  16. package/dist/codex/app-server.js +116 -0
  17. package/dist/codex/inspection.js +217 -0
  18. package/dist/codex/mcp-readiness.js +45 -0
  19. package/dist/codex/native-plugins.js +109 -0
  20. package/dist/codex/options.js +170 -0
  21. package/dist/codex/plugin-hooks.js +93 -0
  22. package/dist/codex/rpc.js +170 -0
  23. package/dist/codex/session-proxy.js +258 -0
  24. package/dist/codex/skill-policy.js +69 -0
  25. package/dist/codex/skill-views.js +104 -0
  26. package/dist/codex/toml.js +46 -0
  27. package/dist/commands/codex.js +118 -0
  28. package/dist/commands/doctor.js +58 -0
  29. package/dist/commands/passthrough.js +14 -0
  30. package/dist/diagnostics/plan.js +87 -0
  31. package/dist/diagnostics.js +39 -0
  32. package/dist/model.js +2 -0
  33. package/dist/runtime/ipc.js +316 -0
  34. package/dist/runtime/process.js +286 -0
  35. package/dist/runtime/resolve-command.js +38 -0
  36. package/dist/runtime/session.js +107 -0
  37. package/dist/validation.js +31 -0
  38. package/package.json +41 -4
  39. package/README.md +0 -3
@@ -0,0 +1,316 @@
1
+ import net from "node:net";
2
+ import { chmod, readFile } from "node:fs/promises";
3
+ import { timingSafeEqual } from "node:crypto";
4
+ import { EventEmitter } from "node:events";
5
+ import { AgencyxError, fail } from "../diagnostics.js";
6
+ import { isRecord } from "../validation.js";
7
+ const MAX_FRAME_BYTES = 2 * 1024 * 1024;
8
+ export class JsonLinePeer extends EventEmitter {
9
+ stream;
10
+ buffer = "";
11
+ closed = false;
12
+ constructor(stream) {
13
+ super();
14
+ this.stream = stream;
15
+ stream.setEncoding("utf8");
16
+ stream.on("data", (chunk) => this.receive(chunk));
17
+ stream.on("error", () => this.emit("fault"));
18
+ stream.on("close", () => {
19
+ this.closed = true;
20
+ this.emit("closed");
21
+ });
22
+ }
23
+ receive(chunk) {
24
+ this.buffer += chunk;
25
+ if (Buffer.byteLength(this.buffer) > MAX_FRAME_BYTES) {
26
+ this.stream.destroy();
27
+ this.emit("fault");
28
+ return;
29
+ }
30
+ let newline;
31
+ while ((newline = this.buffer.indexOf("\n")) >= 0) {
32
+ const line = this.buffer.slice(0, newline);
33
+ this.buffer = this.buffer.slice(newline + 1);
34
+ if (!line.trim())
35
+ continue;
36
+ let value;
37
+ try {
38
+ value = JSON.parse(line);
39
+ }
40
+ catch {
41
+ this.stream.destroy();
42
+ this.emit("fault");
43
+ return;
44
+ }
45
+ this.emit("message", value);
46
+ }
47
+ }
48
+ send(value) {
49
+ if (this.closed || this.stream.destroyed)
50
+ fail("HANDOFF_DISCONNECTED", "handoff", "The session control channel closed.", {}, 6);
51
+ this.stream.write(`${JSON.stringify(value)}\n`);
52
+ }
53
+ waitFor(type, timeoutMs = 5_000) {
54
+ return new Promise((resolve, reject) => {
55
+ const cleanup = () => {
56
+ clearTimeout(timer);
57
+ this.off("message", onMessage);
58
+ this.off("fault", onFailure);
59
+ this.off("closed", onFailure);
60
+ };
61
+ const onMessage = (message) => {
62
+ if (isRecord(message) && message.type === type) {
63
+ cleanup();
64
+ resolve(message);
65
+ }
66
+ };
67
+ const onFailure = () => {
68
+ cleanup();
69
+ reject(new AgencyxError({
70
+ code: "HANDOFF_DISCONNECTED",
71
+ stage: "handoff",
72
+ reason: "The session control channel closed before acknowledging the operation.",
73
+ }, 6));
74
+ };
75
+ const timer = setTimeout(onFailure, timeoutMs);
76
+ this.on("message", onMessage);
77
+ this.on("fault", onFailure);
78
+ this.on("closed", onFailure);
79
+ if (this.closed || this.stream.destroyed)
80
+ onFailure();
81
+ });
82
+ }
83
+ close() {
84
+ this.stream.end();
85
+ }
86
+ }
87
+ function equalToken(candidate, expected) {
88
+ if (typeof candidate !== "string")
89
+ return false;
90
+ const left = Buffer.from(candidate);
91
+ const right = Buffer.from(expected);
92
+ return left.length === right.length && timingSafeEqual(left, right);
93
+ }
94
+ function validExit(value) {
95
+ return (isRecord(value) &&
96
+ ((Number.isInteger(value.code) &&
97
+ Number(value.code) >= 0 &&
98
+ Number(value.code) <= 0xffffffff &&
99
+ value.signal === null) ||
100
+ (value.code === null &&
101
+ typeof value.signal === "string" &&
102
+ /^SIG[A-Z0-9]+$/.test(value.signal))));
103
+ }
104
+ export class SessionServer extends EventEmitter {
105
+ session;
106
+ context;
107
+ server;
108
+ peers = new Set();
109
+ execution;
110
+ executionConnected = false;
111
+ engineStarted = false;
112
+ result;
113
+ failure;
114
+ failureExitCode;
115
+ plan;
116
+ constructor(session, context) {
117
+ super();
118
+ this.session = session;
119
+ this.context = context;
120
+ this.server = net.createServer((socket) => this.accept(socket));
121
+ }
122
+ static async listen(session, context) {
123
+ const channel = new SessionServer(session, context);
124
+ await new Promise((resolve, reject) => {
125
+ channel.server.once("error", reject);
126
+ channel.server.listen(session.endpoint, () => {
127
+ channel.server.off("error", reject);
128
+ resolve();
129
+ });
130
+ });
131
+ channel.server.on("error", () => channel.emit("fault"));
132
+ if (process.platform !== "win32")
133
+ await chmod(session.endpoint, 0o600);
134
+ return channel;
135
+ }
136
+ accept(socket) {
137
+ const peer = new JsonLinePeer(socket);
138
+ this.peers.add(peer);
139
+ let authenticated = false;
140
+ let role;
141
+ const deadline = setTimeout(() => socket.destroy(), 3_000);
142
+ deadline.unref();
143
+ peer.on("message", (message) => {
144
+ if (!isRecord(message)) {
145
+ socket.destroy();
146
+ return;
147
+ }
148
+ if (!authenticated) {
149
+ if (message.type !== "hello" ||
150
+ message.protocol !== 1 ||
151
+ message.sessionId !== this.session.id ||
152
+ !equalToken(message.token, this.session.token) ||
153
+ (message.role !== "probe" && message.role !== "execution") ||
154
+ (message.role === "execution" && this.executionConnected)) {
155
+ socket.destroy();
156
+ return;
157
+ }
158
+ authenticated = true;
159
+ role = message.role;
160
+ clearTimeout(deadline);
161
+ if (role === "execution") {
162
+ this.execution = peer;
163
+ this.executionConnected = true;
164
+ }
165
+ peer.send({ type: "context", context: this.context });
166
+ return;
167
+ }
168
+ if (role !== "execution")
169
+ return;
170
+ if (message.type === "engine-started") {
171
+ this.engineStarted = true;
172
+ return;
173
+ }
174
+ if (message.type === "plan") {
175
+ this.plan = message.plan;
176
+ peer.send({ type: "ack" });
177
+ return;
178
+ }
179
+ if (message.type === "exit" &&
180
+ validExit(message.exit) &&
181
+ (this.engineStarted ||
182
+ (this.context.options.dryRun && message.stage === "dry-run")) &&
183
+ !this.result &&
184
+ !this.failure) {
185
+ this.result = message.exit;
186
+ peer.send({ type: "ack" });
187
+ }
188
+ else if (message.type === "failure" &&
189
+ isRecord(message.diagnostic) &&
190
+ typeof message.diagnostic.code === "string" &&
191
+ !this.result &&
192
+ !this.failure) {
193
+ this.failure = message.diagnostic;
194
+ this.failureExitCode =
195
+ Number.isInteger(message.exitCode) &&
196
+ Number(message.exitCode) > 0 &&
197
+ Number(message.exitCode) <= 255
198
+ ? Number(message.exitCode)
199
+ : 4;
200
+ peer.send({ type: "ack" });
201
+ }
202
+ else {
203
+ socket.destroy();
204
+ }
205
+ });
206
+ peer.on("fault", () => socket.destroy());
207
+ peer.on("closed", () => {
208
+ clearTimeout(deadline);
209
+ this.peers.delete(peer);
210
+ if (peer === this.execution && !this.result && !this.failure) {
211
+ this.failure = {
212
+ code: "HANDOFF_DISCONNECTED",
213
+ stage: "handoff",
214
+ reason: "The execution bridge exited without reporting a result.",
215
+ };
216
+ this.emit("execution-lost");
217
+ }
218
+ });
219
+ }
220
+ abort(signal) {
221
+ if (this.execution && !this.result && !this.failure) {
222
+ try {
223
+ this.execution.send({ type: "abort", signal });
224
+ }
225
+ catch {
226
+ /* The bridge already exited. */
227
+ }
228
+ }
229
+ }
230
+ async close() {
231
+ for (const peer of this.peers)
232
+ peer.stream.destroy();
233
+ await new Promise((resolve) => this.server.close(() => resolve()));
234
+ }
235
+ }
236
+ export async function connectBridge(env, role) {
237
+ let endpoint = env.AGENCYX_SESSION_CHANNEL;
238
+ let token = env.AGENCYX_SESSION_TOKEN;
239
+ let id = env.AGENCYX_SESSION_ID;
240
+ if (env.AGENCYX_SESSION_FILE) {
241
+ try {
242
+ const session = JSON.parse(await readFile(env.AGENCYX_SESSION_FILE, "utf8"));
243
+ if (!isRecord(session) ||
244
+ session.protocol !== 1 ||
245
+ typeof session.endpoint !== "string" ||
246
+ typeof session.token !== "string" ||
247
+ typeof session.id !== "string")
248
+ throw new Error("Invalid bootstrap data.");
249
+ endpoint = session.endpoint;
250
+ token = session.token;
251
+ id = session.id;
252
+ }
253
+ catch {
254
+ fail("HANDOFF_CONTEXT_INVALID", "handoff", "The private launcher session file cannot be read.", {}, 6);
255
+ }
256
+ }
257
+ if (!endpoint || !token || !id)
258
+ fail("HANDOFF_CONTEXT_REQUIRED", "handoff", "The private bridge can only be launched by an active agencyx session.", {}, 6);
259
+ const socket = net.createConnection(endpoint);
260
+ const peer = new JsonLinePeer(socket);
261
+ const contextResponse = peer.waitFor("context");
262
+ socket.once("connect", () => peer.send({ type: "hello", protocol: 1, sessionId: id, token, role }));
263
+ let response;
264
+ try {
265
+ response = await contextResponse;
266
+ }
267
+ catch (error) {
268
+ socket.destroy();
269
+ throw error;
270
+ }
271
+ const context = response.context;
272
+ if (!isRecord(context) ||
273
+ context.sessionId !== id ||
274
+ !isRecord(context.options) ||
275
+ typeof context.codexPath !== "string" ||
276
+ typeof context.cwd !== "string" ||
277
+ typeof context.sessionRoot !== "string") {
278
+ socket.destroy();
279
+ fail("HANDOFF_CONTEXT_INVALID", "handoff", "The wrapper returned an invalid session context.", {}, 6);
280
+ }
281
+ const abort = new AbortController();
282
+ const typedContext = context;
283
+ let finished = false;
284
+ peer.on("message", (message) => {
285
+ if (isRecord(message) && message.type === "abort")
286
+ abort.abort(message.signal === "SIGINT" ? "SIGINT" : "SIGTERM");
287
+ });
288
+ peer.on("closed", () => {
289
+ if (!finished)
290
+ abort.abort("SIGTERM");
291
+ });
292
+ const finish = async (value, final = true) => {
293
+ const acknowledgement = peer.waitFor("ack");
294
+ peer.send(value);
295
+ await acknowledgement;
296
+ if (final)
297
+ finished = true;
298
+ };
299
+ return {
300
+ context: typedContext,
301
+ signal: abort.signal,
302
+ engineStarted: () => peer.send({ type: "engine-started" }),
303
+ reportExit: (result) => finish({
304
+ type: "exit",
305
+ stage: typedContext.options.dryRun ? "dry-run" : "execution",
306
+ exit: result,
307
+ }),
308
+ reportFailure: (diagnostic, exitCode = 4) => finish({ type: "failure", diagnostic, exitCode }),
309
+ reportPlan: (plan) => finish({ type: "plan", plan }, false),
310
+ close: () => {
311
+ finished = true;
312
+ peer.close();
313
+ },
314
+ };
315
+ }
316
+ //# sourceMappingURL=ipc.js.map
@@ -0,0 +1,286 @@
1
+ import crossSpawn from "cross-spawn";
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ import { constants } from "node:os";
5
+ import { readFile } from "node:fs/promises";
6
+ import { setTimeout as delay } from "node:timers/promises";
7
+ import { AgencyxError } from "../diagnostics.js";
8
+ const execFileAsync = promisify(execFile);
9
+ export function exitCode(result) {
10
+ return (result.code ?? 128 + (constants.signals[result.signal ?? "SIGTERM"] ?? 1));
11
+ }
12
+ export function startProcess(command, args, options = {}) {
13
+ const child = crossSpawn(command, [...args], { ...options, shell: false });
14
+ const exited = new Promise((resolve, reject) => {
15
+ child.once("error", () => reject(new AgencyxError({
16
+ code: "PROCESS_START_FAILED",
17
+ stage: "codex",
18
+ reason: "The selected executable could not be started.",
19
+ }, 3)));
20
+ child.once("exit", (code, signal) => resolve({ code, signal }));
21
+ });
22
+ const completion = new Promise((resolve, reject) => {
23
+ child.once("error", () => reject(new AgencyxError({
24
+ code: "PROCESS_START_FAILED",
25
+ stage: "codex",
26
+ reason: "The selected executable could not be started.",
27
+ }, 3)));
28
+ child.once("close", (code, signal) => resolve({ code, signal }));
29
+ });
30
+ // Callers can finish preparing their listeners before awaiting completion.
31
+ void completion.catch(() => undefined);
32
+ void exited.catch(() => undefined);
33
+ let stopping;
34
+ return {
35
+ child,
36
+ exited,
37
+ completion,
38
+ stop: (signal = "SIGTERM") => (stopping ??= stopProcessTree(child, completion, signal, options.detached === true)),
39
+ };
40
+ }
41
+ async function descendantPids(pid) {
42
+ try {
43
+ const { stdout } = await execFileAsync("ps", ["-eo", "pid=,ppid="], {
44
+ timeout: 2_000,
45
+ maxBuffer: 2 * 1024 * 1024,
46
+ });
47
+ const rows = stdout
48
+ .trim()
49
+ .split("\n")
50
+ .map((line) => line.trim().split(/\s+/).map(Number));
51
+ const descendants = [];
52
+ const parents = [pid];
53
+ for (let index = 0; index < parents.length; index++) {
54
+ for (const [child, parent] of rows) {
55
+ if (parent === parents[index] &&
56
+ child !== undefined &&
57
+ child !== process.pid &&
58
+ !parents.includes(child)) {
59
+ parents.push(child);
60
+ descendants.push(child);
61
+ }
62
+ }
63
+ }
64
+ return descendants.reverse();
65
+ }
66
+ catch {
67
+ return [];
68
+ }
69
+ }
70
+ async function processIdentity(pid) {
71
+ try {
72
+ if (process.platform === "linux") {
73
+ const raw = await readFile(`/proc/${pid}/stat`, "utf8");
74
+ const state = raw.slice(raw.lastIndexOf(") ") + 2).split(" ");
75
+ return state[0] === "Z" ? undefined : state[19];
76
+ }
77
+ const { stdout } = await execFileAsync("ps", ["-p", String(pid), "-o", "lstart="], { timeout: 1_000 });
78
+ return stdout.trim() || undefined;
79
+ }
80
+ catch {
81
+ return undefined;
82
+ }
83
+ }
84
+ function groupExists(pid) {
85
+ try {
86
+ process.kill(-pid, 0);
87
+ return true;
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ }
93
+ async function stopProcessTree(child, completion, signal, detached) {
94
+ if (!child.pid)
95
+ return;
96
+ const pid = child.pid;
97
+ if (process.platform !== "win32" && detached) {
98
+ // The group belongs to this headless child, including workers that outlive
99
+ // its launcher. Never signal the user's interactive foreground group.
100
+ if (!groupExists(pid))
101
+ return;
102
+ try {
103
+ process.kill(-pid, signal);
104
+ }
105
+ catch {
106
+ /* The group already exited. */
107
+ }
108
+ const deadline = Date.now() + 2_000;
109
+ while (groupExists(pid) && Date.now() < deadline)
110
+ await delay(25);
111
+ if (groupExists(pid)) {
112
+ try {
113
+ process.kill(-pid, "SIGKILL");
114
+ }
115
+ catch {
116
+ /* The group already exited. */
117
+ }
118
+ }
119
+ await completion.catch(() => undefined);
120
+ return;
121
+ }
122
+ if (child.exitCode !== null || child.signalCode !== null)
123
+ return;
124
+ if (process.platform === "win32") {
125
+ // taskkill is restricted to this child and its descendants, never an image name.
126
+ try {
127
+ await execFileAsync("taskkill.exe", ["/PID", String(pid), "/T", "/F"], {
128
+ timeout: 5_000,
129
+ windowsHide: true,
130
+ });
131
+ }
132
+ catch {
133
+ child.kill();
134
+ }
135
+ }
136
+ else {
137
+ const descendants = await descendantPids(pid);
138
+ const identities = new Map(await Promise.all(descendants.map(async (target) => [target, await processIdentity(target)])));
139
+ for (const target of [...descendants, pid]) {
140
+ try {
141
+ process.kill(target, signal);
142
+ }
143
+ catch {
144
+ /* The owned process already exited. */
145
+ }
146
+ }
147
+ let deadline;
148
+ const ended = await Promise.race([
149
+ completion.then(() => true, () => true),
150
+ new Promise((resolve) => {
151
+ deadline = setTimeout(() => resolve(false), 2_000);
152
+ }),
153
+ ]);
154
+ clearTimeout(deadline);
155
+ const surviving = async () => {
156
+ const checks = await Promise.all([...identities].map(async ([target, identity]) => identity !== undefined && (await processIdentity(target)) === identity
157
+ ? target
158
+ : undefined));
159
+ return checks.filter((target) => target !== undefined);
160
+ };
161
+ const grace = Date.now() + 2_000;
162
+ let remaining = await surviving();
163
+ while (remaining.length && Date.now() < grace) {
164
+ await delay(25);
165
+ remaining = await surviving();
166
+ }
167
+ for (const target of remaining) {
168
+ try {
169
+ process.kill(target, "SIGKILL");
170
+ }
171
+ catch {
172
+ /* The owned process already exited. */
173
+ }
174
+ }
175
+ if (!ended)
176
+ child.kill("SIGKILL");
177
+ }
178
+ await completion.catch(() => undefined);
179
+ }
180
+ export async function captureProcess(command, args, options = {}) {
181
+ const running = startProcess(command, args, {
182
+ ...options,
183
+ stdio: ["ignore", "pipe", "pipe"],
184
+ windowsHide: true,
185
+ detached: process.platform !== "win32",
186
+ });
187
+ const limit = options.maxBytes ?? 8 * 1024 * 1024;
188
+ let stdout = "";
189
+ let stderr = "";
190
+ let size = 0;
191
+ let overflow = false;
192
+ let timedOut = false;
193
+ const collect = (stream, chunk) => {
194
+ size += Buffer.byteLength(chunk);
195
+ if (size > limit) {
196
+ overflow = true;
197
+ void running.stop();
198
+ return;
199
+ }
200
+ if (stream === "stdout")
201
+ stdout += chunk;
202
+ else
203
+ stderr += chunk;
204
+ };
205
+ running.child
206
+ .stdout.setEncoding("utf8")
207
+ .on("data", (chunk) => collect("stdout", chunk));
208
+ running.child
209
+ .stderr.setEncoding("utf8")
210
+ .on("data", (chunk) => collect("stderr", chunk));
211
+ const timer = setTimeout(() => {
212
+ if (running.child.exitCode !== null || running.child.signalCode !== null)
213
+ return;
214
+ timedOut = true;
215
+ void running.stop();
216
+ }, options.timeoutMs ?? 15_000);
217
+ timer.unref();
218
+ try {
219
+ const result = await Promise.race([
220
+ running.completion,
221
+ running.exited.then(async (status) => {
222
+ // Some official metadata commands detach helpers that retain their output
223
+ // handles. Drain the exited command, then release our readers; its helpers
224
+ // remain under the original CLI's ownership.
225
+ let timer;
226
+ await Promise.race([
227
+ running.completion.catch(() => undefined),
228
+ new Promise((resolve) => {
229
+ timer = setTimeout(resolve, 100);
230
+ }),
231
+ ]);
232
+ clearTimeout(timer);
233
+ running.child.stdout?.destroy();
234
+ running.child.stderr?.destroy();
235
+ return status;
236
+ }),
237
+ ]);
238
+ if (timedOut || overflow)
239
+ throw new AgencyxError({
240
+ code: timedOut ? "PROCESS_TIMEOUT" : "PROCESS_OUTPUT_LIMIT",
241
+ stage: "codex",
242
+ reason: timedOut
243
+ ? "A preparation command exceeded its time limit."
244
+ : "A preparation command exceeded its output limit.",
245
+ }, 6);
246
+ return { ...result, stdout, stderr };
247
+ }
248
+ finally {
249
+ clearTimeout(timer);
250
+ }
251
+ }
252
+ export async function runInteractive(command, args, options) {
253
+ const running = startProcess(command, args, {
254
+ cwd: options.cwd,
255
+ env: options.env,
256
+ stdio: "inherit",
257
+ });
258
+ const abort = () => {
259
+ void running.stop(options.signal?.reason === "SIGINT" ? "SIGINT" : "SIGTERM");
260
+ };
261
+ options.signal?.addEventListener("abort", abort, { once: true });
262
+ if (options.signal?.aborted)
263
+ abort();
264
+ try {
265
+ return await running.completion;
266
+ }
267
+ finally {
268
+ options.signal?.removeEventListener("abort", abort);
269
+ }
270
+ }
271
+ export function observeSignals(onSignal, interactive = Boolean(process.stdin.isTTY), interruptPreparation = () => false) {
272
+ // A terminal sends SIGINT to the entire foreground group. Forwarding it again
273
+ // would turn Codex's cancel-current-turn behavior into a second interrupt.
274
+ const interrupt = () => {
275
+ if (!interactive || interruptPreparation())
276
+ onSignal("SIGINT");
277
+ };
278
+ const terminate = () => onSignal("SIGTERM");
279
+ process.on("SIGINT", interrupt);
280
+ process.on("SIGTERM", terminate);
281
+ return () => {
282
+ process.off("SIGINT", interrupt);
283
+ process.off("SIGTERM", terminate);
284
+ };
285
+ }
286
+ //# sourceMappingURL=process.js.map
@@ -0,0 +1,38 @@
1
+ import { access, realpath, stat } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import path from "node:path";
4
+ import { fail } from "../diagnostics.js";
5
+ export async function resolveCommand(name, override, cwd = process.cwd(), env = process.env) {
6
+ const requested = override ?? name;
7
+ if (!requested || requested.includes("\0"))
8
+ fail("DEPENDENCY_NOT_FOUND", "discovery", "The executable path is empty or invalid.", { component: name }, 3);
9
+ const windows = process.platform === "win32";
10
+ const pathKey = Object.keys(env).find((key) => key.toLowerCase() === "path");
11
+ const search = path.isAbsolute(requested) || /[/\\]/.test(requested)
12
+ ? [""]
13
+ : (env[pathKey ?? "PATH"] ?? "").split(path.delimiter).filter(Boolean);
14
+ // npm also creates an extensionless POSIX shell shim on Windows. Prefer the
15
+ // native Windows executable/shim rather than accidentally starting Git Bash.
16
+ const extensions = windows && !path.extname(requested)
17
+ ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
18
+ .toLowerCase()
19
+ .split(";")
20
+ .filter((extension) => [".com", ".exe", ".bat", ".cmd"].includes(extension))
21
+ : [""];
22
+ for (const directory of search) {
23
+ for (const extension of extensions) {
24
+ const candidate = path.resolve(cwd, directory, requested + extension);
25
+ try {
26
+ if (!(await stat(candidate)).isFile())
27
+ continue;
28
+ await access(candidate, windows ? constants.F_OK : constants.X_OK);
29
+ return await realpath(candidate);
30
+ }
31
+ catch {
32
+ // Continue PATH lookup; a dependency error below never prints environment values.
33
+ }
34
+ }
35
+ }
36
+ fail("DEPENDENCY_NOT_FOUND", "discovery", `Install ${name} separately or set its AGENCYX executable-path override.`, { component: name }, 3);
37
+ }
38
+ //# sourceMappingURL=resolve-command.js.map