@ayonli/jsext
Version:
A JavaScript extension package for building strong and modern applications.
345 lines (341 loc) • 11.8 kB
JavaScript
;
var string = require('./string.js');
var env = require('./env.js');
var runtime = require('./runtime.js');
require('./fs.js');
var path = require('./path.js');
var module_web = require('./module/web.js');
var cli_constants = require('./cli/constants.js');
var cli_common = require('./cli/common.js');
var error = require('./error.js');
var fs_util = require('./fs/util.js');
var fs_util_server = require('./fs/util/server.js');
var error_common = require('./error/common.js');
/**
* Useful utility functions for interacting with the terminal.
*
* NOTE: Despite the name of this module, many of its functions can also be used
* in the browser environment.
* @module
* @experimental
*/
/**
* Executes a command in the terminal and returns the exit code and outputs.
*
* In Windows, this function will use PowerShell to execute the command when
* possible, which has a lot UNIX-like aliases/commands available, such as `ls`,
* `cat`, `rm`, etc.
*
* @example
* ```ts
* import { run } from "@ayonli/jsext/cli";
*
* const { code, stdout, stderr } = await run("echo", ["Hello, World!"]);
*
* console.log(code); // 0
* console.log(JSON.stringify(stdout)); // "Hello, World!\n"
* console.log(JSON.stringify(stderr)); // ""
* ```
*/
async function run(cmd, args, options = {}) {
const signal = options.signal;
const isWindows = runtime.platform() === "windows";
const isWslPs = cli_common.isWSL() && cmd.endsWith("powershell.exe");
if (env.isNodeLike || env.isDeno) {
const { spawn } = await import('node:child_process');
const { decode } = await module_web.interop(import('iconv-lite'), false);
const child = isWindows && cli_constants.PowerShellCommands.includes(cmd)
? spawn("powershell", ["-c", cmd, ...args.map(cli_common.quote)], { signal })
: spawn(cmd, args, { signal });
const stdout = [];
const stderr = [];
child.stdout.on("data", chunk => {
if (isWindows || isWslPs) {
stdout.push(decode(chunk, "cp936"));
}
else {
stdout.push(String(chunk));
}
});
child.stderr.on("data", chunk => {
if (isWindows || isWslPs) {
stderr.push(decode(chunk, "cp936"));
}
else {
stderr.push(String(chunk));
}
});
const code = await new Promise((resolve, reject) => {
child.once("exit", (code, signal) => {
if (code === null && signal) {
resolve(1);
}
else {
resolve(code !== null && code !== void 0 ? code : 0);
}
}).once("error", reject);
});
return {
code,
stdout: stdout.join(""),
stderr: stderr.join(""),
};
}
else {
error.throwUnsupportedRuntimeError();
}
}
/**
* Executes the script inside PowerShell as if they were typed at the PowerShell
* command prompt.
*
* This function can also be called within Windows Subsystem for Linux to
* directly interact with PowerShell.
*
* NOTE: This function is only available in Windows and Windows Subsystem for
* Linux.
*
* @example
* ```ts
* import { powershell } from "@ayonli/jsext/cli";
*
* const cmd = "ls";
* const {
* code,
* stdout,
* stderr,
* } = await powershell(`Get-Command -Name ${cmd} | Select-Object -ExpandProperty Source`);
* ```
*/
async function powershell(script, options = {}) {
let command = "powershell";
if (cli_common.isWSL()) {
command = "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe";
}
return await run(command, ["-c", script], options);
}
/**
* Executes a command with elevated privileges using `sudo` (or UAC in Windows).
*
* @deprecated Running a command (or subprocess) with elevated privileges is
* dangerous, and the underlying dependency `sudo-prompt` is also deprecated.
* Use this function with caution.
*
* @example
* ```ts
* import { sudo } from "@ayonli/jsext/cli";
*
* await sudo("apt", ["install", "build-essential"]);
* ```
*/
async function sudo(cmd, args, options = {}) {
const _platform = runtime.platform();
if ((_platform !== "windows" && !(options === null || options === void 0 ? void 0 : options.gui)) ||
(_platform === "linux" && !runtime.env("DISPLAY")) ||
cli_common.isWSL()) {
return await run("sudo", [cmd, ...args]);
}
if (!["darwin", "windows", "linux"].includes(_platform)) {
throw new error_common.NotSupportedError("Unsupported platform");
}
const { exec } = await module_web.interop(import('sudo-prompt'));
return await new Promise((resolve, reject) => {
exec(`${cmd}` + (args.length ? ` ${args.map(cli_common.quote).join(" ")}` : ""), {
name: (options === null || options === void 0 ? void 0 : options.title) || (env.isDeno ? "Deno" : env.isBun ? "Bun" : "NodeJS"),
}, (error, stdout, stderr) => {
if (error) {
reject(error);
}
else {
let _stdout = String(stdout);
if (_platform === "windows" && cmd === "echo" && _stdout.startsWith(`"`)) {
// In Windows CMD, the `echo` command will output the string
// with double quotes. We need to remove them.
let lastIndex = _stdout.lastIndexOf(`"`);
_stdout = _stdout.slice(1, lastIndex) + _stdout.slice(lastIndex + 1);
}
resolve({
code: 0,
stdout: _stdout,
stderr: String(stderr),
});
}
});
});
}
/**
* Returns the path of the given command if it exists in the system,
* otherwise returns `null`.
*
* This function is available in Windows as well.
*
* @example
* ```ts
* import { which } from "@ayonli/jsext/cli";
*
* const path = await which("node");
*
* console.log(path);
* // e.g. "/usr/bin/node" in UNIX/Linux or "C:\\Program Files\\nodejs\\node.exe" in Windows
* ```
*/
async function which(cmd) {
if (runtime.platform() === "windows") {
const { code, stdout } = await run("powershell", [
"-Command",
`Get-Command -Name ${cmd} | Select-Object -ExpandProperty Source`
]);
return code ? null : stdout.trim();
}
else {
const { code, stdout } = await run("which", [cmd]);
return code ? null : stdout.trim();
}
}
/**
* Opens the given file in a text editor.
*
* The `filename` can include a line number by appending `:<number>` or `#L<number>`,
* however, this feature is not supported by all editors.
*
* This function will try to open VS Code if available, otherwise it will try to
* open the default editor or a preferred one, such as `vim` or `nano` when available.
*
* Some editor may hold the terminal until the editor is closed, while others may
* return immediately. Anyway, the operation is asynchronous and the function will
* not block the thread.
*
* In the browser, this function will always try to open the file in VS Code,
* regardless of whether it's available or not.
*
* @example
* ```ts
* import { edit } from "@ayonli/jsext/cli";
*
* await edit("path/to/file.txt");
*
* await edit("path/to/file.txt:10"); // open the file at line 10
* ```
*/
async function edit(filename) {
filename = fs_util.ensureFsTarget(filename);
filename = await fs_util_server.resolveHomeDir(filename);
const match = filename.match(/(:|#L)(\d+)/);
let line;
if (match) {
line = Number(match[2]);
filename = filename.slice(0, match.index);
}
if (env.isBrowserWindow) {
window.open("vscode://file/" + string.trimStart(filename, "/") + (line ? `:${line}` : ""));
return;
}
else if (env.isSharedWorker
|| env.isSharedWorker
|| (env.isDedicatedWorker && (["chrome", "firefox", "safari"]).includes(runtime.default().identity))) {
error.throwUnsupportedRuntimeError();
}
const _platform = runtime.platform();
const vscode = await which("code");
const throwOpenError = (stderr, filename) => {
throw new Error(stderr || `Unable to open ${filename} in the editor.`);
};
if (vscode) {
const args = line ? ["--goto", `${filename}:${line}`] : [filename];
const { code, stderr } = await run(vscode, args);
if (code)
throwOpenError(stderr, filename);
return;
}
else if (_platform === "darwin") {
const { code, stderr } = await run("open", ["-t", filename]);
if (code)
throwOpenError(stderr, filename);
return;
}
else if (_platform === "windows" || cli_common.isWSL()) {
const notepad = _platform === "windows"
? "notepad.exe"
: "/mnt/c/Windows/System32/notepad.exe";
const { code, stderr } = await run(notepad, [filename]);
if (code)
throwOpenError(stderr, filename);
return;
}
let cmd = runtime.env("EDITOR")
|| runtime.env("VISUAL")
|| (await which("gedit"))
|| (await which("kate"))
|| (await which("vim"))
|| (await which("vi"))
|| (await which("nano"));
let args;
if (!cmd) {
throw new Error("Cannot determine which editor to open.");
}
else {
cmd = path.basename(cmd);
}
if (["gedit", "kate", "vim", "vi", "nano"].includes(cmd)) {
args = line ? [`+${line}`, filename] : [filename];
}
if (["vim", "vi", "nano"].includes(cmd)) {
if (await which("gnome-terminal")) {
args = ["--", cmd, ...args];
cmd = "gnome-terminal";
}
else {
args = ["-e", `'${cmd} ${args.map(cli_common.quote).join(" ")}'`];
cmd = (await which("konsole"))
|| (await which("xfce4-terminal"))
|| (await which("deepin-terminal"))
|| (await which("xterm"));
}
if (!cmd) {
throw new Error("Cannot determine which terminal to open.");
}
}
else {
args = [filename];
}
const { code, stderr } = await run(cmd, args);
if (code)
throwOpenError(stderr, filename);
}
Object.defineProperty(exports, 'ControlKeys', {
enumerable: true,
get: function () { return cli_constants.ControlKeys; }
});
Object.defineProperty(exports, 'ControlSequences', {
enumerable: true,
get: function () { return cli_constants.ControlSequences; }
});
Object.defineProperty(exports, 'FunctionKeys', {
enumerable: true,
get: function () { return cli_constants.FunctionKeys; }
});
Object.defineProperty(exports, 'NavigationKeys', {
enumerable: true,
get: function () { return cli_constants.NavigationKeys; }
});
exports.args = cli_common.args;
exports.charWidth = cli_common.charWidth;
exports.getWindowSize = cli_common.getWindowSize;
exports.isTTY = cli_common.isTTY;
exports.isTypingInput = cli_common.isTypingInput;
exports.isWSL = cli_common.isWSL;
exports.lockStdin = cli_common.lockStdin;
exports.moveLeftBy = cli_common.moveLeftBy;
exports.moveRightBy = cli_common.moveRightBy;
exports.parseArgs = cli_common.parseArgs;
exports.quote = cli_common.quote;
exports.readStdin = cli_common.readStdin;
exports.stringWidth = cli_common.stringWidth;
exports.writeStdout = cli_common.writeStdout;
exports.writeStdoutSync = cli_common.writeStdoutSync;
exports.edit = edit;
exports.powershell = powershell;
exports.run = run;
exports.sudo = sudo;
exports.which = which;
//# sourceMappingURL=cli.js.map