fix: align the schemas, params, and descriptions (#128)
This commit is contained in:
@@ -8,7 +8,14 @@ interface GrepFilesArgs {
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
type GrepFilesResult = Awaited<ReturnType<typeof grep>>;
|
||||
interface GrepFilesResult {
|
||||
output: string;
|
||||
matches?: number;
|
||||
files?: number;
|
||||
truncated?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_LIMIT = 100;
|
||||
|
||||
/**
|
||||
* Codex-style grep_files tool.
|
||||
@@ -19,7 +26,7 @@ export async function grep_files(
|
||||
): Promise<GrepFilesResult> {
|
||||
validateRequiredParams(args, ["pattern"], "grep_files");
|
||||
|
||||
const { pattern, include, path } = args;
|
||||
const { pattern, include, path, limit = DEFAULT_LIMIT } = args;
|
||||
|
||||
const grepArgs: GrepArgs = {
|
||||
pattern,
|
||||
@@ -28,5 +35,34 @@ export async function grep_files(
|
||||
output_mode: "files_with_matches",
|
||||
};
|
||||
|
||||
return grep(grepArgs);
|
||||
const result = await grep(grepArgs);
|
||||
|
||||
// The underlying grep result already has the correct files count
|
||||
const totalFiles = result.files ?? 0;
|
||||
|
||||
// Apply limit to the file list
|
||||
if (result.output && limit > 0 && totalFiles > limit) {
|
||||
// The output format is: "Found N files\n/path/to/file1\n/path/to/file2..."
|
||||
const lines = result.output
|
||||
.split("\n")
|
||||
.filter((line) => line.trim() !== "");
|
||||
|
||||
// First line is "Found N files", rest are file paths
|
||||
const filePaths = lines.slice(1);
|
||||
|
||||
const truncatedFiles = filePaths.slice(0, limit);
|
||||
const truncatedOutput = `Found ${limit} file${limit !== 1 ? "s" : ""} (truncated from ${totalFiles})\n${truncatedFiles.join("\n")}`;
|
||||
|
||||
return {
|
||||
output: truncatedOutput,
|
||||
files: limit,
|
||||
truncated: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
output: result.output,
|
||||
files: totalFiles,
|
||||
truncated: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { ls } from "./LS.js";
|
||||
import { promises as fs } from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { validateRequiredParams } from "./validation.js";
|
||||
|
||||
const MAX_ENTRY_LENGTH = 500;
|
||||
const INDENTATION_SPACES = 2;
|
||||
|
||||
interface ListDirCodexArgs {
|
||||
dir_path: string;
|
||||
offset?: number;
|
||||
@@ -8,19 +12,212 @@ interface ListDirCodexArgs {
|
||||
depth?: number;
|
||||
}
|
||||
|
||||
type ListDirCodexResult = Awaited<ReturnType<typeof ls>>;
|
||||
interface ListDirCodexResult {
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface DirEntry {
|
||||
name: string; // Full relative path for sorting
|
||||
displayName: string; // Just the filename for display
|
||||
depth: number; // Indentation depth
|
||||
kind: "directory" | "file" | "symlink" | "other";
|
||||
}
|
||||
|
||||
/**
|
||||
* Codex-style list_dir tool.
|
||||
* Delegates to the existing LS implementation; offset/limit/depth are accepted but currently ignored.
|
||||
* Lists entries with pagination (offset/limit) and depth control.
|
||||
*/
|
||||
export async function list_dir(
|
||||
args: ListDirCodexArgs,
|
||||
): Promise<ListDirCodexResult> {
|
||||
validateRequiredParams(args, ["dir_path"], "list_dir");
|
||||
|
||||
const { dir_path } = args;
|
||||
const { dir_path, offset = 1, limit = 25, depth = 2 } = args;
|
||||
|
||||
// LS handles path resolution and formatting.
|
||||
return ls({ path: dir_path, ignore: [] });
|
||||
if (offset < 1) {
|
||||
throw new Error("offset must be a 1-indexed entry number");
|
||||
}
|
||||
|
||||
if (limit < 1) {
|
||||
throw new Error("limit must be greater than zero");
|
||||
}
|
||||
|
||||
if (depth < 1) {
|
||||
throw new Error("depth must be greater than zero");
|
||||
}
|
||||
|
||||
if (!path.isAbsolute(dir_path)) {
|
||||
throw new Error("dir_path must be an absolute path");
|
||||
}
|
||||
|
||||
const entries = await listDirSlice(dir_path, offset, limit, depth);
|
||||
const output = [`Absolute path: ${dir_path}`, ...entries];
|
||||
|
||||
return { content: output.join("\n") };
|
||||
}
|
||||
|
||||
/**
|
||||
* List directory entries with pagination.
|
||||
*/
|
||||
async function listDirSlice(
|
||||
dirPath: string,
|
||||
offset: number,
|
||||
limit: number,
|
||||
maxDepth: number,
|
||||
): Promise<string[]> {
|
||||
const entries: DirEntry[] = [];
|
||||
await collectEntries(dirPath, "", maxDepth, entries);
|
||||
|
||||
if (entries.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const startIndex = offset - 1;
|
||||
if (startIndex >= entries.length) {
|
||||
throw new Error("offset exceeds directory entry count");
|
||||
}
|
||||
|
||||
const remainingEntries = entries.length - startIndex;
|
||||
const cappedLimit = Math.min(limit, remainingEntries);
|
||||
const endIndex = startIndex + cappedLimit;
|
||||
|
||||
// Get the selected entries and sort by name
|
||||
const selectedEntries = entries.slice(startIndex, endIndex);
|
||||
selectedEntries.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const formatted: string[] = [];
|
||||
for (const entry of selectedEntries) {
|
||||
formatted.push(formatEntryLine(entry));
|
||||
}
|
||||
|
||||
if (endIndex < entries.length) {
|
||||
formatted.push(`More than ${cappedLimit} entries found`);
|
||||
}
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively collect directory entries using BFS.
|
||||
*/
|
||||
async function collectEntries(
|
||||
dirPath: string,
|
||||
relativePrefix: string,
|
||||
remainingDepth: number,
|
||||
entries: DirEntry[],
|
||||
): Promise<void> {
|
||||
const queue: Array<{ absPath: string; prefix: string; depth: number }> = [
|
||||
{ absPath: dirPath, prefix: relativePrefix, depth: remainingDepth },
|
||||
];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift();
|
||||
if (!current) break;
|
||||
const { absPath, prefix, depth } = current;
|
||||
|
||||
const dirEntries: Array<{
|
||||
absPath: string;
|
||||
relativePath: string;
|
||||
kind: DirEntry["kind"];
|
||||
entry: DirEntry;
|
||||
}> = [];
|
||||
|
||||
try {
|
||||
const items = await fs.readdir(absPath, { withFileTypes: true });
|
||||
|
||||
for (const item of items) {
|
||||
const itemAbsPath = path.join(absPath, item.name);
|
||||
const relativePath = prefix ? path.join(prefix, item.name) : item.name;
|
||||
const displayName = formatEntryComponent(item.name);
|
||||
const displayDepth = prefix ? prefix.split(path.sep).length : 0;
|
||||
const sortKey = formatEntryName(relativePath);
|
||||
|
||||
let kind: DirEntry["kind"];
|
||||
if (item.isSymbolicLink()) {
|
||||
kind = "symlink";
|
||||
} else if (item.isDirectory()) {
|
||||
kind = "directory";
|
||||
} else if (item.isFile()) {
|
||||
kind = "file";
|
||||
} else {
|
||||
kind = "other";
|
||||
}
|
||||
|
||||
dirEntries.push({
|
||||
absPath: itemAbsPath,
|
||||
relativePath,
|
||||
kind,
|
||||
entry: {
|
||||
name: sortKey,
|
||||
displayName,
|
||||
depth: displayDepth,
|
||||
kind,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`failed to read directory: ${err}`);
|
||||
}
|
||||
|
||||
// Sort entries alphabetically
|
||||
dirEntries.sort((a, b) => a.entry.name.localeCompare(b.entry.name));
|
||||
|
||||
for (const item of dirEntries) {
|
||||
// Queue subdirectories for traversal if depth allows
|
||||
if (item.kind === "directory" && depth > 1) {
|
||||
queue.push({
|
||||
absPath: item.absPath,
|
||||
prefix: item.relativePath,
|
||||
depth: depth - 1,
|
||||
});
|
||||
}
|
||||
entries.push(item.entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format entry name for sorting (normalize path separators).
|
||||
*/
|
||||
function formatEntryName(filePath: string): string {
|
||||
const normalized = filePath.replace(/\\/g, "/");
|
||||
if (normalized.length > MAX_ENTRY_LENGTH) {
|
||||
return normalized.substring(0, MAX_ENTRY_LENGTH);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a single path component.
|
||||
*/
|
||||
function formatEntryComponent(name: string): string {
|
||||
if (name.length > MAX_ENTRY_LENGTH) {
|
||||
return name.substring(0, MAX_ENTRY_LENGTH);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a directory entry for display.
|
||||
*/
|
||||
function formatEntryLine(entry: DirEntry): string {
|
||||
const indent = " ".repeat(entry.depth * INDENTATION_SPACES);
|
||||
let name = entry.displayName;
|
||||
|
||||
switch (entry.kind) {
|
||||
case "directory":
|
||||
name += "/";
|
||||
break;
|
||||
case "symlink":
|
||||
name += "@";
|
||||
break;
|
||||
case "other":
|
||||
name += "?";
|
||||
break;
|
||||
default:
|
||||
// "file" type has no suffix
|
||||
break;
|
||||
}
|
||||
|
||||
return `${indent}${name}`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { read } from "./Read.js";
|
||||
import { promises as fs } from "node:fs";
|
||||
import { validateRequiredParams } from "./validation.js";
|
||||
|
||||
const MAX_LINE_LENGTH = 500;
|
||||
const TAB_WIDTH = 4;
|
||||
const COMMENT_PREFIXES = ["#", "//", "--"];
|
||||
|
||||
interface IndentationOptions {
|
||||
anchor_line?: number;
|
||||
max_levels?: number;
|
||||
@@ -21,22 +25,275 @@ interface ReadFileCodexResult {
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface LineRecord {
|
||||
number: number;
|
||||
raw: string;
|
||||
display: string;
|
||||
indent: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Codex-style read_file tool.
|
||||
* Currently supports slice-style reading; indentation mode is ignored but accepted.
|
||||
* Supports both slice mode (simple range) and indentation mode (context-aware block reading).
|
||||
*/
|
||||
export async function read_file(
|
||||
args: ReadFileCodexArgs,
|
||||
): Promise<ReadFileCodexResult> {
|
||||
validateRequiredParams(args, ["file_path"], "read_file");
|
||||
|
||||
const { file_path, offset, limit } = args;
|
||||
|
||||
const result = await read({
|
||||
const {
|
||||
file_path,
|
||||
offset,
|
||||
limit,
|
||||
});
|
||||
offset = 1,
|
||||
limit = 2000,
|
||||
mode = "slice",
|
||||
indentation,
|
||||
} = args;
|
||||
|
||||
return { content: result.content };
|
||||
if (offset < 1) {
|
||||
throw new Error("offset must be a 1-indexed line number");
|
||||
}
|
||||
|
||||
if (limit < 1) {
|
||||
throw new Error("limit must be greater than zero");
|
||||
}
|
||||
|
||||
let lines: string[];
|
||||
|
||||
if (mode === "indentation") {
|
||||
lines = await readIndentationMode(
|
||||
file_path,
|
||||
offset,
|
||||
limit,
|
||||
indentation ?? {},
|
||||
);
|
||||
} else {
|
||||
lines = await readSliceMode(file_path, offset, limit);
|
||||
}
|
||||
|
||||
return { content: lines.join("\n") };
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple slice mode: read lines from offset to offset + limit.
|
||||
*/
|
||||
async function readSliceMode(
|
||||
filePath: string,
|
||||
offset: number,
|
||||
limit: number,
|
||||
): Promise<string[]> {
|
||||
const content = await fs.readFile(filePath, "utf8");
|
||||
const allLines = content.split(/\r?\n/);
|
||||
|
||||
const collected: string[] = [];
|
||||
for (
|
||||
let i = offset - 1;
|
||||
i < allLines.length && collected.length < limit;
|
||||
i++
|
||||
) {
|
||||
const line = allLines[i];
|
||||
if (line === undefined) break;
|
||||
const formatted = formatLine(line);
|
||||
collected.push(`L${i + 1}: ${formatted}`);
|
||||
}
|
||||
|
||||
if (offset > allLines.length) {
|
||||
throw new Error("offset exceeds file length");
|
||||
}
|
||||
|
||||
return collected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indentation mode: expand around an anchor line based on indentation levels.
|
||||
*/
|
||||
async function readIndentationMode(
|
||||
filePath: string,
|
||||
offset: number,
|
||||
limit: number,
|
||||
options: IndentationOptions,
|
||||
): Promise<string[]> {
|
||||
const anchorLine = options.anchor_line ?? offset;
|
||||
const maxLevels = options.max_levels ?? 0;
|
||||
const includeSiblings = options.include_siblings ?? false;
|
||||
const includeHeader = options.include_header ?? true;
|
||||
const maxLines = options.max_lines ?? limit;
|
||||
|
||||
if (anchorLine < 1) {
|
||||
throw new Error("anchor_line must be a 1-indexed line number");
|
||||
}
|
||||
|
||||
if (maxLines < 1) {
|
||||
throw new Error("max_lines must be greater than zero");
|
||||
}
|
||||
|
||||
// Read and parse all lines
|
||||
const content = await fs.readFile(filePath, "utf8");
|
||||
const rawLines = content.split(/\r?\n/);
|
||||
|
||||
if (rawLines.length === 0 || anchorLine > rawLines.length) {
|
||||
throw new Error("anchor_line exceeds file length");
|
||||
}
|
||||
|
||||
// Build line records
|
||||
const records: LineRecord[] = rawLines.map((raw, idx) => ({
|
||||
number: idx + 1,
|
||||
raw,
|
||||
display: formatLine(raw),
|
||||
indent: measureIndent(raw),
|
||||
}));
|
||||
|
||||
// Compute effective indents (blank lines inherit previous indent)
|
||||
const effectiveIndents = computeEffectiveIndents(records);
|
||||
|
||||
const anchorIndex = anchorLine - 1;
|
||||
const anchorRecord = records[anchorIndex];
|
||||
const anchorIndent = effectiveIndents[anchorIndex] ?? 0;
|
||||
|
||||
if (!anchorRecord) {
|
||||
throw new Error("anchor_line exceeds file length");
|
||||
}
|
||||
|
||||
// Calculate minimum indent to include
|
||||
const minIndent =
|
||||
maxLevels === 0 ? 0 : Math.max(0, anchorIndent - maxLevels * TAB_WIDTH);
|
||||
|
||||
// Cap by limits
|
||||
const finalLimit = Math.min(limit, maxLines, records.length);
|
||||
|
||||
if (finalLimit === 1) {
|
||||
return [`L${anchorRecord.number}: ${anchorRecord.display}`];
|
||||
}
|
||||
|
||||
// Expand from anchor line
|
||||
const out: LineRecord[] = [anchorRecord];
|
||||
let i = anchorIndex - 1; // up cursor
|
||||
let j = anchorIndex + 1; // down cursor
|
||||
let iCounterMinIndent = 0;
|
||||
let jCounterMinIndent = 0;
|
||||
|
||||
while (out.length < finalLimit) {
|
||||
let progressed = 0;
|
||||
|
||||
// Expand up
|
||||
if (i >= 0) {
|
||||
const iIndent = effectiveIndents[i];
|
||||
const iRecord = records[i];
|
||||
if (iIndent !== undefined && iRecord && iIndent >= minIndent) {
|
||||
out.unshift(iRecord);
|
||||
progressed++;
|
||||
|
||||
// Handle sibling exclusion
|
||||
if (iIndent === minIndent && !includeSiblings) {
|
||||
const allowHeaderComment = includeHeader && isComment(iRecord);
|
||||
const canTakeLine = allowHeaderComment || iCounterMinIndent === 0;
|
||||
|
||||
if (canTakeLine) {
|
||||
iCounterMinIndent++;
|
||||
} else {
|
||||
// Remove the line we just added
|
||||
out.shift();
|
||||
progressed--;
|
||||
i = -1; // Stop moving up
|
||||
}
|
||||
}
|
||||
|
||||
i--;
|
||||
|
||||
if (out.length >= finalLimit) break;
|
||||
} else {
|
||||
i = -1; // Stop moving up
|
||||
}
|
||||
}
|
||||
|
||||
// Expand down
|
||||
if (j < records.length) {
|
||||
const jIndent = effectiveIndents[j];
|
||||
const jRecord = records[j];
|
||||
if (jIndent !== undefined && jRecord && jIndent >= minIndent) {
|
||||
out.push(jRecord);
|
||||
progressed++;
|
||||
|
||||
// Handle sibling exclusion
|
||||
if (jIndent === minIndent && !includeSiblings) {
|
||||
if (jCounterMinIndent > 0) {
|
||||
// Remove the line we just added
|
||||
out.pop();
|
||||
progressed--;
|
||||
j = records.length; // Stop moving down
|
||||
}
|
||||
jCounterMinIndent++;
|
||||
}
|
||||
|
||||
j++;
|
||||
} else {
|
||||
j = records.length; // Stop moving down
|
||||
}
|
||||
}
|
||||
|
||||
if (progressed === 0) break;
|
||||
}
|
||||
|
||||
// Trim empty lines at start and end
|
||||
while (out.length > 0 && out[0]?.raw.trim() === "") {
|
||||
out.shift();
|
||||
}
|
||||
while (out.length > 0 && out[out.length - 1]?.raw.trim() === "") {
|
||||
out.pop();
|
||||
}
|
||||
|
||||
return out.map((record) => `L${record.number}: ${record.display}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute effective indents - blank lines inherit previous line's indent.
|
||||
*/
|
||||
function computeEffectiveIndents(records: LineRecord[]): number[] {
|
||||
const effective: number[] = [];
|
||||
let previousIndent = 0;
|
||||
|
||||
for (const record of records) {
|
||||
if (record.raw.trim() === "") {
|
||||
effective.push(previousIndent);
|
||||
} else {
|
||||
previousIndent = record.indent;
|
||||
effective.push(previousIndent);
|
||||
}
|
||||
}
|
||||
|
||||
return effective;
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure indentation of a line (tabs = TAB_WIDTH spaces).
|
||||
*/
|
||||
function measureIndent(line: string): number {
|
||||
let indent = 0;
|
||||
for (const char of line) {
|
||||
if (char === " ") {
|
||||
indent++;
|
||||
} else if (char === "\t") {
|
||||
indent += TAB_WIDTH;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return indent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a line is a comment.
|
||||
*/
|
||||
function isComment(record: LineRecord): boolean {
|
||||
const trimmed = record.raw.trim();
|
||||
return COMMENT_PREFIXES.some((prefix) => trimmed.startsWith(prefix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a line for display (truncate if too long).
|
||||
*/
|
||||
function formatLine(line: string): string {
|
||||
if (line.length > MAX_LINE_LENGTH) {
|
||||
return line.substring(0, MAX_LINE_LENGTH);
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { bash } from "./Bash.js";
|
||||
import { spawn } from "node:child_process";
|
||||
import * as path from "node:path";
|
||||
import { validateRequiredParams } from "./validation.js";
|
||||
|
||||
interface ShellArgs {
|
||||
@@ -15,58 +16,92 @@ interface ShellResult {
|
||||
stderr: string[];
|
||||
}
|
||||
|
||||
const DEFAULT_TIMEOUT = 120000;
|
||||
|
||||
/**
|
||||
* Codex-style shell tool.
|
||||
* Runs an array of shell arguments, typically ["bash", "-lc", "..."].
|
||||
* Runs an array of shell arguments using execvp-style semantics.
|
||||
* Typically called with ["bash", "-lc", "..."] for shell commands.
|
||||
*/
|
||||
export async function shell(args: ShellArgs): Promise<ShellResult> {
|
||||
validateRequiredParams(args, ["command"], "shell");
|
||||
|
||||
const { command, workdir, timeout_ms, justification: description } = args;
|
||||
const { command, workdir, timeout_ms } = args;
|
||||
if (!Array.isArray(command) || command.length === 0) {
|
||||
throw new Error("command must be a non-empty array of strings");
|
||||
}
|
||||
|
||||
const commandString = command.join(" ");
|
||||
|
||||
const previousUserCwd = process.env.USER_CWD;
|
||||
if (workdir) {
|
||||
process.env.USER_CWD = workdir;
|
||||
const [executable, ...execArgs] = command;
|
||||
if (!executable) {
|
||||
throw new Error("command must be a non-empty array of strings");
|
||||
}
|
||||
const timeout = timeout_ms ?? DEFAULT_TIMEOUT;
|
||||
|
||||
try {
|
||||
const result = await bash({
|
||||
command: commandString,
|
||||
timeout: timeout_ms ?? 120000,
|
||||
description,
|
||||
run_in_background: false,
|
||||
// Determine working directory
|
||||
const cwd = workdir
|
||||
? path.isAbsolute(workdir)
|
||||
? workdir
|
||||
: path.resolve(process.env.USER_CWD || process.cwd(), workdir)
|
||||
: process.env.USER_CWD || process.cwd();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const stdoutChunks: Buffer[] = [];
|
||||
const stderrChunks: Buffer[] = [];
|
||||
|
||||
const child = spawn(executable, execArgs, {
|
||||
cwd,
|
||||
env: process.env,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
const text = (result.content ?? [])
|
||||
.map((item) =>
|
||||
"text" in item && typeof item.text === "string" ? item.text : "",
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
const timeoutId = setTimeout(() => {
|
||||
child.kill("SIGKILL");
|
||||
reject(new Error(`Command timed out after ${timeout}ms`));
|
||||
}, timeout);
|
||||
|
||||
const stdout = text ? text.split("\n") : [];
|
||||
const stderr =
|
||||
result.status === "error"
|
||||
? ["Command reported an error. See output for details."]
|
||||
: [];
|
||||
child.stdout.on("data", (chunk: Buffer) => {
|
||||
stdoutChunks.push(chunk);
|
||||
});
|
||||
|
||||
return {
|
||||
output: text,
|
||||
stdout,
|
||||
stderr,
|
||||
};
|
||||
} finally {
|
||||
if (workdir) {
|
||||
if (previousUserCwd === undefined) {
|
||||
delete process.env.USER_CWD;
|
||||
child.stderr.on("data", (chunk: Buffer) => {
|
||||
stderrChunks.push(chunk);
|
||||
});
|
||||
|
||||
child.on("error", (err: Error) => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(new Error(`Failed to execute command: ${err.message}`));
|
||||
});
|
||||
|
||||
child.on("close", (code: number | null) => {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
const stdoutText = Buffer.concat(stdoutChunks).toString("utf8");
|
||||
const stderrText = Buffer.concat(stderrChunks).toString("utf8");
|
||||
|
||||
const stdoutLines = stdoutText
|
||||
.split("\n")
|
||||
.filter((line) => line.length > 0);
|
||||
const stderrLines = stderrText
|
||||
.split("\n")
|
||||
.filter((line) => line.length > 0);
|
||||
|
||||
// Combine stdout and stderr for output
|
||||
const output = [stdoutText, stderrText].filter(Boolean).join("\n").trim();
|
||||
|
||||
if (code !== 0 && code !== null) {
|
||||
// Command failed but we still return the output
|
||||
resolve({
|
||||
output: output || `Command exited with code ${code}`,
|
||||
stdout: stdoutLines,
|
||||
stderr: stderrLines,
|
||||
});
|
||||
} else {
|
||||
process.env.USER_CWD = previousUserCwd;
|
||||
resolve({
|
||||
output,
|
||||
stdout: stdoutLines,
|
||||
stderr: stderrLines,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user