Headless Commands
Build background commands without UI
Headless commands run background operations without displaying a UI. They're triggered from the command palette or programmatically, execute their logic, and optionally show a notification when complete.
Modes
| Mode | Runtime | Node.js Access | Use Case |
|---|---|---|---|
worker-headless | Browser Web Worker | No | Simple operations, no filesystem |
node-headless | Node.js process | Yes | System operations, filesystem access |
Worker-Headless
Runs in a browser Web Worker. No filesystem or shell access.
Project Setup
// build.ts
await Bun.build({
entrypoints: ["./src/Headless.ts"],
outdir: "./dist",
target: "browser",
format: "esm",
minify: true,
plugins: [kunkunCommandPlugin({ mode: "no-view" }) as BunPlugin],
});Command Implementation
// src/Headless.ts
import { showToast, Toast, LocalStorage } from "@kunkunsh/api";
import type { LaunchProps } from "@kunkunsh/api";
export default async function main(_props: LaunchProps): Promise<void> {
// Increment persistent counter
const raw = await LocalStorage.getItem("trigger-count");
const count = (parseInt(raw ?? "0", 10) || 0) + 1;
await LocalStorage.setItem("trigger-count", String(count));
console.log(`[headless] trigger #${count}`);
await showToast({
title: "Command Executed",
message: `Run count: ${count}`,
style: Toast.Style.Success,
});
}Manifest
{
"name": "my-headless",
"title": "My Headless Command",
"description": "A background command without UI",
"mode": "worker-headless",
"main": "dist/Headless.js"
}Node-Headless
Runs in a Node.js process with full system access.
Project Setup
// build.ts
await Bun.build({
entrypoints: ["./src/HeadlessNode.ts"],
outdir: "./dist/node",
target: "node",
format: "esm",
minify: true,
plugins: [kunkunCommandPlugin({ mode: "no-view" }) as BunPlugin],
});Command Implementation
// src/HeadlessNode.ts
import { showToast, Toast, LocalStorage, fs, shell } from "@kunkunsh/api";
import type { LaunchProps } from "@kunkunsh/api";
export default async function main(props: LaunchProps): Promise<void> {
// Access filesystem
const homeDir = await fs.readDir(await path.homeDir());
// Execute shell commands
const result = await shell.execute("echo", ["Hello from headless"]);
await showToast({
title: "Filesystem Access",
message: `Found ${homeDir.length} items in home`,
style: Toast.Style.Success,
});
}Manifest with Permissions
{
"name": "my-node-headless",
"title": "Node Headless Command",
"description": "Background command with system access",
"mode": "node-headless",
"main": "dist/node/HeadlessNode.js",
"permissions": [
{ "permission": "fs-read", "allow": ["$HOME/**"] },
"shell"
]
}Launch Props
Headless commands receive launch properties:
interface LaunchProps {
// Arguments defined in manifest
arguments?: Record<string, string>;
// Environment information
environment?: {
extensionName: string;
extensionVersion: string;
platform: "darwin" | "win32" | "linux";
};
}Command Arguments
Define arguments in your manifest for user input:
{
"name": "search-web",
"title": "Search Web",
"mode": "worker-headless",
"main": "dist/SearchWeb.js",
"arguments": [
{
"name": "query",
"type": "text",
"placeholder": "Enter search query",
"required": true
}
]
}Access arguments in your command:
export default async function main(props: LaunchProps): Promise<void> {
const query = props.arguments?.query;
if (!query) return;
// Perform search...
await showToast({ title: `Searching: ${query}` });
}Runtime Selection
For node-headless, the runtime is selected automatically:
| Priority | Runtime | Condition |
|---|---|---|
| 1 | Deno | Available (best sandboxing) |
| 2 | Node.js v20+ | Available |
| 3 | utilityProcess | Fallback |
Force a specific runtime:
{
"name": "deno-only",
"mode": "node-headless",
"main": "dist/DenoCommand.js",
"runtime": "deno"
}| Runtime Value | Behavior |
|---|---|
omit / "auto" | Best available |
"deno" | Deno only, throws if not found |
"node" | Node.js only |
Lifecycle
The host manages the headless command lifecycle:
User triggers command
→ Host spawns process/worker
→ api.init() // Fetch preferences, setup
→ api.onTrigger(ctx) // Execute command logic
→ api.destroy() // Cleanup
→ Process/worker terminatedError Handling
export default async function main(props: LaunchProps): Promise<void> {
try {
// Your logic here
await performOperation();
} catch (error) {
await showToast({
title: "Error",
message: String(error),
style: Toast.Style.Failure,
});
}
}Best Practices
- Keep it fast: Headless commands should complete quickly
- Show feedback: Use
showToastto inform users of completion - Handle errors gracefully: Always catch and report errors
- Use storage: Persist state with
LocalStoragefor counters, caches - Log to console: Use
console.logfor debugging (appears in dev tools)
Full Example
See apps/sample-worker-ext/src/Headless.ts for a complete working example.