Kunkun

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

ModeRuntimeNode.js AccessUse Case
worker-headlessBrowser Web WorkerNoSimple operations, no filesystem
node-headlessNode.js processYesSystem 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:

PriorityRuntimeCondition
1DenoAvailable (best sandboxing)
2Node.js v20+Available
3utilityProcessFallback

Force a specific runtime:

{
  "name": "deno-only",
  "mode": "node-headless",
  "main": "dist/DenoCommand.js",
  "runtime": "deno"
}
Runtime ValueBehavior
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 terminated

Error 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

  1. Keep it fast: Headless commands should complete quickly
  2. Show feedback: Use showToast to inform users of completion
  3. Handle errors gracefully: Always catch and report errors
  4. Use storage: Persist state with LocalStorage for counters, caches
  5. Log to console: Use console.log for debugging (appears in dev tools)

Full Example

See apps/sample-worker-ext/src/Headless.ts for a complete working example.

On this page