Skip to content

Commit

Permalink
Add exec
Browse files Browse the repository at this point in the history
  • Loading branch information
Hexagon committed Mar 16, 2024
1 parent a8e75c4 commit 514002c
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 2 deletions.
5 changes: 3 additions & 2 deletions deno.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"name": "@cross/utils",
"version": "0.2.2",
"version": "0.3.0",
"exports": {
".": "./mod.ts",
"./ansi": "./utils/ansi.ts",
"./args": "./utils/args.ts",
"./exit": "./utils/exit.ts"
"./exit": "./utils/exit.ts",
"./exec": "./utils/exec.ts"
},
"imports": {
"@cross/runtime": "jsr:@cross/runtime@^0.0.17",
Expand Down
1 change: 1 addition & 0 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { exit } from "./utils/exit.ts";
export { args, ArgsParser } from "./utils/args.ts";
export { Colors, Cursor, stripAnsi } from "./utils/ansi.ts";
export { exec } from "./utils/exec.ts";
Empty file removed test.ts
Empty file.
129 changes: 129 additions & 0 deletions utils/exec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { CurrentRuntime, Runtime } from "@cross/runtime";

type CommandArray = string[];

// Runtime-specific execution functions (also using async/await)
async function executeNode(
command: CommandArray,
env: Record<string, string> = {},
cwd?: string,
): Promise<{ code: number; stdout: string; stderr: string }> {
const { spawn } = await import("node:child_process");
//@ts-ignore Node specific
const options: SpawnOptionsWithoutStdio = {
env,
cwd: cwd,
shell: false,
};
const childProcess = spawn(
command[0],
command.length > 1 ? command.slice(1) : [],
options,
);

let stdout = "";
let stderr = "";

childProcess.stdout.on("data", (data: string) => stdout += data.toString());
childProcess.stderr.on("data", (data: string) => stderr += data.toString());

return new Promise((resolve, reject) => { // Still need Promise here due to event listeners
childProcess.on("error", (error: Error) => reject(error));
childProcess.on(
"close",
(code: number) => resolve({ code, stdout, stderr }),
);
});
}

async function executeDeno(
command: CommandArray,
env: Record<string, string> = {},
cwd?: string,
): Promise<{ code: number; stdout: string; stderr: string }> {
// @ts-ignore Deno is specific to Deno
const options: Deno.CommandOptions = {
args: command.length > 1 ? command.slice(1) : [],
env: { ...env },
cwd,
};
const cmd = new Deno.Command(command[0], options);
const output = await cmd.output();
return {
code: output.code,
stdout: new TextDecoder().decode(output.stdout),
stderr: new TextDecoder().decode(output.stderr),
};
}

async function executeBun(
command: CommandArray,
extraEnvVars: Record<string, string> = {},
cwd?: string,
): Promise<{ code: number; stdout: string; stderr: string }> {
// @ts-ignore Bun is runtime specific
const results = await Bun.spawn({
cmd: command,
// @ts-ignore process is runtime specific
env: { ...extraEnvVars }, // Merge environment variables
stdout: "pipe",
stderr: "pipe",
cwd,
});

// Convert ReadableStreams to strings
await results.exited;

// @ts-ignore Bun is runtime specific
const stdout = await Bun.readableStreamToText(results.stdout);
// @ts-ignore Bun is runtime specific
const stderr = await Bun.readableStreamToText(results.stderr);

return {
code: results.exitCode,
stdout,
stderr,
};
}

/**
* Starts a child processes.
*
* @param {CommandArray} command - An array of strings representing the command and its arguments.
* @param {Record<string, string>} [extraEnvVars] - An optional object containing additional environment variables to set for the command.
* @param {string} [cwd] - An optional path specifying the current working directory for the command.
* @returns {Promise<{ code: number; stdout: string; stderr: string }>} A Promise resolving with an object containing:
* * code: The exit code of the executed command.
* * stdout: The standard output of the command.
* * stderr: The standard error of the command.
*
* @throws {Error} If the current runtime is not supported.
*
* @example
* const command = ["/bin/bash", "-c", "echo \"running a shell\""];
*
* try {
* const result = await exec(command, { HOME: "/home/overridden/home/dir" });
* console.log("Return Code:", result.code);
* console.log("Stdout:", result.stdout);
* console.log("Stderr:", result.stderr);
* } catch (error) {
* console.error(error);
* }
*/
export async function exec(
command: CommandArray,
extraEnvVars: Record<string, string> = {},
cwd?: string,
): Promise<{ code: number; stdout: string; stderr: string }> {
switch (CurrentRuntime) {
case Runtime.Node:
return await executeNode(command, extraEnvVars, cwd);
case Runtime.Deno:
return await executeDeno(command, extraEnvVars, cwd);
case Runtime.Bun:
return await executeBun(command, extraEnvVars, cwd);
default:
throw new Error(`Unsupported runtime: ${CurrentRuntime}`);
}
}

0 comments on commit 514002c

Please sign in to comment.