Skip to content
Kunkun

Shell

Docs: https://docs.api.kunkun.sh/interfaces/ui.IShell

Shell API provides a way to execute shell commands/scripts and spawn processes.

Commands executed runs at once and are not interactive. stdout and stderr are captured and returned as a result.

If you are running a long running process and need to capture the stdout/stderr stream, or interact with stdin, you should use the spawn API.

More examples will be provided below.

All shell permissions are scoped, read the documentation carefully before using them. Without proper permissions, KK’s API will deny the shell command.

API and Permissions

Here is a list of APIs and permissions required to use them.

  • execute: [ shell:allshell:execute ]
  • kill: [ shell:allshell:kill ]
  • stdinWrite: [ shell:allshell:stdin-writeshell:execute ]
  • open: [ shell:allshell:open ]
  • rawSpawn: [ shell:allshell:spawn ]
  • executeBashScript: [ shell:allshell:execute ]
  • executePowershellScript: [ shell:allshell:execute ]
  • executeAppleScript: [ shell:allshell:execute ]
  • executePythonScript: [ shell:allshell:execute ]
  • executeZshScript: [ shell:allshell:execute ]
  • executeNodeScript: [ shell:allshell:execute ]
  • hasCommand: [ ]
  • likelyOnWindows: [ shell:allshell:execute ]

Command

Execute Command

import { shell } from "@kksh/api/ui/worker"
// or
import { shell } from "@kksh/api/ui/iframe"
const cmd = shell.createCommand("echo", ["Hello World"])
const output = await cmd.execute()
console.log(output.stdout) // Hello World

To execute a command, you need to add scoped permission for the command you are executing.

This is used to prevent extensions from being hacked and running injected malicious code.

So always make the permission as specific as possible.

Here is an example permission for the sample code above.

package.json
...
"permissions": [
{
"permission": "shell:execute",
"allow": [
{
"cmd": {
"program": "echo",
"args": [
"Hello World"
]
}
}
]
},
...
],
...

Each item in the args array is a regex to match the argument. You can add a permission like this to allow dynamic arguments.

package.json
...
"permissions": [
{
"permission": "shell:execute",
"allow": [
{
"cmd": {
"program": "echo",
"args": [
"[a-zA-Z0-9\s]+"
]
}
}
]
},
...
],
...

Spawn Command

Executed command runs at once and are not interactive. stdout and stderr are captured and returned as a result. If you are running a long running process and need to capture the stdout/stderr stream or event interact with stdin, you should use the spawn API.

For example, this could be useful if you are converting a video file with ffmpeg, and need to read stdout to get the progress throughout the conversion process.

Here is a simplified example of how to use the spawn API.

const cmd = shell.createCommand("echo", ["Hello World"])
let stdout = ""
let stderr = ""
cmd.on("close", (data) => {
console.log(`command finished with code ${data.code} and signal ${data.signal}`)
// sample output: "command finished with code 0 and signal null"
})
cmd.on("error", (error) => {
console.error(error)
})
cmd.stdout.on("data", (line) => {
stdout += line
})
cmd.stderr.on("data", (line) => {
stderr += line
})
await cmd.spawn()

Spawn also requires its own scoped permission (shell:spawn).

package.json
...
"permissions": [
{
"permission": "shell:spawn",
"allow": [
{
"cmd": {
"program": "echo",
"args": [
"Hello World"
]
}
}
]
},
...
],
...

Execute Scripts Directly

Sometimes adding args one by one is inconvenient, especially for extensions that need to run a lot of shell scripts (like an emulated terminal).

KK provides some convenient methods for executing scripts directly.

This API allows extension to run any shell script, which could be potentially unsafe.

  • executeBashScript (bash -c)
  • shellExecutePowershellScript (powershell -Command)
  • executeAppleScript (osascript -e)
  • executePythonScript (python -c)
  • executeZshScript (zsh -c)
  • executeNodeScript (node -e)
import { shell } from "@kksh/api/ui/worker"
await shell.executeAppleScript("display dialog \"Hello World\"") // macOS only, display a dialog
const ret = await shell.executePythonScript("print('Hello World')")
console.log(ret.stdout)

Permissions

Direct shell script execution use the entire script as one argument. Thus the scoped permission will look like this.

program is the command to execute, first arg can be -c, -e etc. depending on which shell you are using. Check the list above.

The second argument is a regex to match the script. In this example .+ is used to match any script, you should replace it with a more specific regex.

package.json
...
"permissions": [
{
"permission": "shell:execute",
"allow": [
{
"cmd": {
"program": "osascript",
"args": [
"-e",
".+"
]
}
},
{
"cmd": {
"program": "python",
"args": [
"-c",
".+"
]
}
}
]
},
...
],
...

Make Command Script

The previous section provides APIs to execute scripts directly. You can’t interact with the script.

In this section, we will create a command script object and execute/spawn it.

const cmd = shell.makeBashScript("echo \"Hello World\"")
const output = await cmd.execute()
console.log(output.stdout);
// or spawn the command
let stdout = ""
let stderr = ""
cmd.on("close", (data) => {
console.log(`command finished with code ${data.code} and signal ${data.signal}`)
// sample output: "command finished with code 0 and signal null"
})
cmd.on("error", (error) => {
console.error(error)
})
cmd.stdout.on("data", (line) => {
stdout += line
})
cmd.stderr.on("data", (line) => {
stderr += line
})
await cmd.spawn()

And of course, you need to add permission for running bash scripts.

package.json
...
"permissions": [
{
"permission": "shell:execute",
"allow": [
{
"cmd": {
"program": "bash",
"args": [
"-c",
".+"
]
}
}
]
},
...
],
...

More Example Code

Promisify spawn

You can turn the spawned command into a promise, so you can easily work with it.

Here we define a custom execute function that returns a promise. You can write custom logic to handle the stdout and stderr stream. The final output is returned as a promise.

function execute(
command: shell.Command<string>,
): Promise<{ stderr: string; stdout: string }> {
return new Promise((resolve, reject) => {
let stdout = "";
let stderr = "";
command.on("close", (data) => {
return resolve({ stdout, stderr });
});
command.on("error", (error) => reject(error));
command.stdout.on("data", (line) => {
stdout += line;
});
command.stderr.on("data", (line) => {
stderr += line;
});
command.spawn();
});
}
const command = shell.createCommand("ffprobe", [
"-v",
"quiet",
"-print_format",
"json",
"-show_format",
"-show_streams",
videoPath,
]);
return execute(command).then(({ stdout, stderr }) => {
console.log(stdout);
console.log(stderr);
});