@tabbybyte/minion
Version:
A cross-runtime CLI tool for AI-powered command execution. Auto-detects and uses Bun for performance when available, falls back to Node.js.
280 lines (252 loc) • 6.73 kB
JavaScript
/**
* Runtime compatibility layer for Bun and Node.js
* Detects available runtime and provides unified APIs
*/
import { readFile, writeFile, access } from "fs/promises"
import { spawn } from "child_process"
// Runtime detection
let _bunAvailable = null
let _bunAPIs = null
/**
* Check if Bun runtime is available
* @returns {boolean} True if Bun APIs are available
*/
function isBunAvailable() {
if (_bunAvailable !== null) {
return _bunAvailable
}
try {
// Check if we're running in Bun by testing for Bun global
_bunAvailable = typeof globalThis.Bun !== "undefined"
&& typeof globalThis.Bun.file === "function"
&& typeof globalThis.Bun.write === "function"
&& typeof globalThis.Bun.spawn === "function"
if (_bunAvailable) {
_bunAPIs = globalThis.Bun
}
} catch {
_bunAvailable = false
}
return _bunAvailable
}
/**
* Get runtime information
* @returns {Object} Runtime details
*/
function getRuntimeInfo() {
const isBun = isBunAvailable()
return {
runtime: isBun ? "bun" : "node",
version: isBun ? _bunAPIs.version : process.version,
platform: process.platform,
arch: process.arch
}
}
/**
* Cross-runtime file operations
*/
const file = {
/**
* Read file text content
* @param {string} path - File path
* @returns {Promise<string>} File content
*/
async text(path) {
if (isBunAvailable()) {
const bunFile = _bunAPIs.file(path)
return bunFile.text()
}
return readFile(path, "utf-8")
},
/**
* Check if file exists
* @param {string} path - File path
* @returns {Promise<boolean>} True if file exists
*/
async exists(path) {
if (isBunAvailable()) {
const bunFile = _bunAPIs.file(path)
return bunFile.exists()
}
try {
await access(path)
return true
} catch {
return false
}
}
}
/**
* Cross-runtime file write operation
* @param {string} path - File path
* @param {string|Buffer} content - Content to write
* @returns {Promise<void>}
*/
async function writeContent(path, content) {
if (isBunAvailable()) {
await _bunAPIs.write(path, content)
} else {
await writeFile(path, content, "utf-8")
}
}
/**
* Cross-runtime process spawning
* @param {string[]} command - Command array [cmd, ...args]
* @param {Object} options - Spawn options
* @returns {Promise<Object>} Process result
*/
function spawnProcess(command, options = {}) {
const timeoutMs = options.timeout || 30000 // 30 second default timeout
if (isBunAvailable()) {
return spawnWithBun(command, options, timeoutMs)
}
return spawnWithNode(command, options, timeoutMs)
}
/**
* Spawn process using Bun.spawn
* @param {string[]} command - Command array
* @param {Object} options - Options
* @param {number} timeoutMs - Timeout in milliseconds
* @returns {Promise<Object>} Process result
*/
function spawnWithBun(command, options, timeoutMs) {
return new Promise((resolve, reject) => {
const proc = _bunAPIs.spawn(command, {
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
...options
})
let stdout = ""
let stderr = ""
// Set timeout
const timeout = setTimeout(() => {
proc.kill("SIGTERM")
reject(new Error("Command timed out after 30 seconds"))
}, timeoutMs)
// Handle stdout
if (proc.stdout) {
const stdoutReader = proc.stdout.getReader()
const readStdout = async () => {
try {
// eslint-disable-next-line no-constant-condition
while (true) {
// eslint-disable-next-line no-await-in-loop
const { done, value } = await stdoutReader.read()
if (done) break
const output = new TextDecoder().decode(value)
stdout += output
if (options.debug) {
process.stdout.write(output)
}
}
} catch (error) {
console.error("Error reading stdout:", error)
}
}
readStdout()
}
// Handle stderr
if (proc.stderr) {
const stderrReader = proc.stderr.getReader()
const readStderr = async () => {
try {
// eslint-disable-next-line no-constant-condition
while (true) {
// eslint-disable-next-line no-await-in-loop
const { done, value } = await stderrReader.read()
if (done) break
const output = new TextDecoder().decode(value)
stderr += output
if (options.debug) {
process.stderr.write(output)
}
}
} catch (error) {
console.error("Error reading stderr:", error)
}
}
readStderr()
}
// Wait for process to finish
proc.exited.then((exitCode) => {
clearTimeout(timeout)
resolve({
exitCode,
stdout,
stderr
})
}).catch((error) => {
clearTimeout(timeout)
reject(error)
})
})
}
/**
* Spawn process using Node.js child_process
* @param {string[]} command - Command array
* @param {Object} options - Options
* @param {number} timeoutMs - Timeout in milliseconds
* @returns {Promise<Object>} Process result
*/
function spawnWithNode(command, options, timeoutMs) {
return new Promise((resolve, reject) => {
const [cmd, ...args] = command
const proc = spawn(cmd, args, {
stdio: ["ignore", "pipe", "pipe"],
...options
})
let stdout = ""
let stderr = ""
// Set timeout
const timeout = setTimeout(() => {
proc.kill("SIGTERM")
reject(new Error("Command timed out after 30 seconds"))
}, timeoutMs)
proc.stdout?.on("data", (data) => {
const output = data.toString()
stdout += output
if (options.debug) {
process.stdout.write(output)
}
})
proc.stderr?.on("data", (data) => {
const output = data.toString()
stderr += output
if (options.debug) {
process.stderr.write(output)
}
})
proc.on("close", (exitCode) => {
clearTimeout(timeout)
resolve({
exitCode,
stdout,
stderr
})
})
proc.on("error", (error) => {
clearTimeout(timeout)
reject(error)
})
})
}
/**
* Log runtime information if debug is enabled
* @param {boolean} debug - Whether debug mode is enabled
*/
function logRuntimeInfo(debug = false) {
if (debug) {
const info = getRuntimeInfo()
console.log(`🚀 Runtime: ${info.runtime} ${info.version} (${info.platform}/${info.arch})`)
}
}
// Exports
export {
isBunAvailable,
getRuntimeInfo,
file,
writeContent,
spawnProcess,
logRuntimeInfo
}