import { RPCChannel, type IoInterface } from "kkrpc/browser" import { // Child, EventEmitter, hasCommand, likelyOnWindows, open as shellxOpen, // EventEmitter, // Command as ShellxCommand, type ChildProcess, type CommandEvent, type CommandEvents, type InternalSpawnOptions, type IOPayload, type OutputEvents, type SpawnOptions } from "tauri-plugin-shellx-api" import { type DenoRunConfig } from "./client.ts" import type { IShellServer } from "./server-types.ts" export class Child { /** The child process `pid`. */ pid: number api: IShellServer constructor(pid: number, api: IShellServer) { this.pid = pid this.api = api } /** * Writes `data` to the `stdin`. * * @param data The message to write, either a string or a byte array. * @example * ```typescript * import { Command } from '@tauri-apps/plugin-shell'; * const command = Command.create('node'); * const child = await command.spawn(); * await child.write('message'); * await child.write([0, 1, 2, 3, 4, 5]); * ``` * * @returns A promise indicating the success or failure of the operation. * * @since 2.0.0 */ async write(data: IOPayload | number[]): Promise { // await invoke('plugin:shellx|stdin_write', { // pid: this.pid, // buffer: data // }) this.api.stdinWrite(data.toString(), this.pid) } /** * Kills the child process. * * @returns A promise indicating the success or failure of the operation. * * @since 2.0.0 */ kill(): Promise { return this.api.kill(this.pid) // await invoke("plugin:shellx|kill", { // cmd: "killChild", // pid: this.pid // }) } } class BaseShellCommand extends EventEmitter { /** @ignore Program to execute. */ readonly program: string /** @ignore Program arguments */ readonly args: string[] /** @ignore Spawn options. */ readonly options: SpawnOptions /** Event emitter for the `stdout`. Emits the `data` event. */ readonly stdout = new EventEmitter>() /** Event emitter for the `stderr`. Emits the `data` event. */ readonly stderr = new EventEmitter>() /** * @ignore * Creates a new `Command` instance. * * @param program The program name to execute. * It must be configured on `tauri.conf.json > plugins > shell > scope`. * @param args Program arguments. * @param options Spawn options. */ constructor(program: string, args: string | string[] = [], options?: SpawnOptions) { super() this.program = program this.args = typeof args === "string" ? [args] : args this.options = options ?? {} } } export class Command extends BaseShellCommand { api: IShellServer constructor( program: string, args: string | string[] = [], api: IShellServer, options?: SpawnOptions ) { super(program, args, options) this.api = api } async spawn(): Promise { const args = this.args if (typeof args === "object") { Object.freeze(args) } return this.api .rawSpawn(this.program, args, this.options, (evt) => { switch (evt.event) { case "Error": this.emit("error", evt.payload) break case "Terminated": this.emit("close", evt.payload) break case "Stdout": this.stdout.emit("data", evt.payload as O) break case "Stderr": this.stderr.emit("data", evt.payload as O) break } }) .then(async (pid) => { await this.api.recordSpawnedProcess(pid) // report spawned process to main process for process auto cleanup return new Child(pid, this.api) }) } async execute(): Promise> { const args = this.args if (typeof args === "object") { Object.freeze(args) } return this.api.execute(this.program, this.args, this.options) as Promise> } } export class DenoCommand extends BaseShellCommand { config: DenoRunConfig scriptPath: string api: IShellServer constructor(scriptPath: string, args: string[], config: DenoRunConfig, api: IShellServer) { super("deno", args) this.config = config this.scriptPath = scriptPath this.api = api } execute(): Promise> { return this.api.denoExecute(this.scriptPath, this.config, this.args) as Promise> } spawn(): Promise { return this.api .denoRawSpawn(this.scriptPath, this.config, this.args, (evt) => { switch (evt.event) { case "Error": this.emit("error", evt.payload) break case "Terminated": this.emit("close", evt.payload) break case "Stdout": this.stdout.emit("data", evt.payload as O) break case "Stderr": this.stderr.emit("data", evt.payload as O) break } }) .then(async (pid) => { console.log("spawned deno process", pid) await this.api.recordSpawnedProcess(pid) return new Child(pid, this.api) }) } } export type IShell = { // Command: typeof Command // DenoCommand: typeof DenoCommand createCommand: ( program: string, args: string | string[], options?: SpawnOptions ) => Command createDenoCommand: ( scriptPath: string, args: string[], config: Partial & SpawnOptions ) => DenoCommand open: typeof shellxOpen makeBashScript: (script: string) => Command makePowershellScript: (script: string) => Command makeAppleScript: (script: string) => Command makePythonScript: (script: string) => Command makeZshScript: (script: string) => Command makeNodeScript: (script: string) => Command executeBashScript: (script: string) => Promise> executePowershellScript: (script: string) => Promise> executeAppleScript: (script: string) => Promise> executePythonScript: (script: string) => Promise> executeZshScript: (script: string) => Promise> executeNodeScript: (script: string) => Promise> hasCommand: typeof hasCommand likelyOnWindows: typeof likelyOnWindows Child: typeof Child TauriShellStdio: typeof TauriShellStdio createDenoRpcChannel( scriptPath: string, args: string[], config: Partial & SpawnOptions, localAPIImplementation: LocalAPI ): Promise<{ rpcChannel: RPCChannel process: Child command: DenoCommand }> RPCChannel: typeof RPCChannel whereIsCommand: (command: string) => Promise killPid: (pid: number) => Promise } export class TauriShellStdio implements IoInterface { name = "tauri-shell-stdio" constructor( private readStream: EventEmitter>, // stdout of child process private childProcess: Child ) {} read(): Promise { return new Promise((resolve, reject) => { this.readStream.on("data", (chunk) => { resolve(chunk) }) }) } async write(data: string): Promise { return this.childProcess.write(data + "\n") } } export function constructShellAPI(api: IShellServer): IShell { // const originalAPI = constructShellAPI1(api) function createCommand(program: string, args: string | string[] = [], options?: SpawnOptions) { return new Command(program, args, api, options) } function createDenoCommand( scriptPath: string, args: string[], config: Partial & SpawnOptions ) { return new DenoCommand(scriptPath, args, config, api) } async function createDenoRpcChannel( scriptPath: string, args: string[], config: Partial & SpawnOptions, localAPIImplementation: LocalAPI ) { const denoCmd = createDenoCommand(scriptPath, args, config) const denoProcess = await denoCmd.spawn() const stdio = new TauriShellStdio(denoCmd.stdout, denoProcess) const stdioRPC = new RPCChannel(stdio, { expose: localAPIImplementation }) return { rpcChannel: stdioRPC, process: denoProcess, command: denoCmd } } function makeBashScript(script: string): Command { return createCommand("bash", ["-c", script]) } function makePowershellScript(script: string): Command { return createCommand("powershell", ["-Command", script]) } function makeAppleScript(script: string): Command { return createCommand("osascript", ["-e", script]) } function makePythonScript(script: string): Command { return createCommand("python", ["-c", script]) } function makeZshScript(script: string): Command { return createCommand("zsh", ["-c", script]) } function makeNodeScript(script: string): Command { return createCommand("node", ["-e", script]) } async function executeBashScript(script: string): Promise> { return makeBashScript(script).execute() } async function executePowershellScript(script: string): Promise> { return makePowershellScript(script).execute() } async function executeAppleScript(script: string): Promise> { return makeAppleScript(script).execute() } async function executePythonScript(script: string): Promise> { return makePythonScript(script).execute() } async function executeZshScript(script: string): Promise> { return makeZshScript(script).execute() } async function executeNodeScript(script: string): Promise> { return makeNodeScript(script).execute() } /** * Run powershell.exe -Command 'echo $env:OS' to determine if the current platform is likely to be Windows. * Not 100% accurate. If a Mac or Linux somehow has powershell.exe in PATH, this will return true. * @returns Whether the current platform is likely to be Windows. */ function likelyOnWindows(): Promise { // return createCommand("powershell.exe", ["-Command", "echo $env:OS"]) // .execute() // .then((out) => out.code === 0 && out.stdout.toLowerCase().includes("windows")) // .catch(() => false) return api.likelyOnWindows() } return { open: api.open, killPid: api.killPid, makeBashScript, makePowershellScript, makeAppleScript, makePythonScript, makeZshScript, makeNodeScript, executeBashScript, executePowershellScript, executeAppleScript, executePythonScript, executeZshScript, executeNodeScript, hasCommand: api.hasCommand, likelyOnWindows, createCommand, createDenoCommand, Child, TauriShellStdio, createDenoRpcChannel, RPCChannel, whereIsCommand: api.whereIsCommand } } export { RPCChannel } from "kkrpc/browser"