Macotron

macotron.shell

Run a command-line tool and get back what it printed and its exit code.

const r = await macotron.shell.run("/usr/bin/defaults", ["read", "-g", "AppleInterfaceStyle"]);
const dark = r.exitCode === 0 && r.stdout.trim() === "Dark";

Running commands

run(command: string, args?: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }>

Runs the command with /bin/zsh -c, waits for it to exit, and resolves with its standard output, standard error, and exit status. The command runs off the main thread, so a slow tool does not freeze other plugins.

The promise does not reject when the tool fails. Check exitCode: 0 means success. A tool that cannot start at all also resolves, with a non-zero exitCode and the shell's error in stderr.

How the command line is built

Macotron puts each item of args in single quotes and appends it to command. The shell then runs the result. This has three effects:

// One fixed pipeline, no user input:
await macotron.shell.run("/usr/bin/pmset -g batt | /usr/bin/grep -o '[0-9]*%'");

// User input goes in args:
await macotron.shell.run("/usr/bin/mdfind", ["-name", query]);

Environment

Macotron is not sandboxed, and run() has no path rules or allow list. A command runs as your user, with the same file access as Macotron, and with Macotron's environment. It runs in a non-interactive, non-login shell, so zsh reads ~/.zshenv but not ~/.zshrc or ~/.zprofile. An app opened from the Finder gets only the system PATH (/usr/bin:/bin:/usr/sbin:/sbin), so call tools by full path.

There is no timeout. A tool that never exits keeps running, and its promise never resolves.

Plugins must work on a stock Mac. Call tools that ship with macOS, such as /usr/bin/open, /usr/bin/defaults, or /usr/bin/osascript, and not tools installed with Homebrew or npm.

See also