Skip to content

The aero.* runtime API

This is the global aero object available to a sandboxed extension's main code — and only there. It is the entire surface command logic can touch. Authoritative typings: aero.d.ts. The CLI/SDK ships these as the installable @aero/ext package.

Where this runs

main JS executes inside a sandboxed <iframe sandbox="allow-scripts"> — no DOM, no Node, no window.api, no arbitrary network. The only channel is a postMessage RPC bridge to aero.*. Calls that return data are async (Promises resolved by the host).

The host enforces, on every message: a known-method allow-list, a 256 KB arg size cap, one in-flight activation per extension, and 5 s timeouts on host→sandbox calls. Unknown or oversized messages are dropped.

TypeScript

Install the typings for autocompletion and aero typed globally:

bash
npm install --save-dev @aero/ext
ts
/// <reference types="@aero/ext" />

The entry point

The host calls activate after loading main; deactivate is optional.

ts
function activate(context: {
  extensionId: string;     // canonical id "<publisher>.<name>"
  subscriptions: Disposable[]; // push Disposables here; host disposes on deactivate
}): void;

function deactivate?(): void;

interface Disposable { dispose(): void; }
js
'use strict';

function activate(context) {
  const sub = aero.commands.registerCommand('my-ext.hello', () => {
    aero.window.showInformationMessage('Hello from my-ext!');
  });
  context.subscriptions.push(sub);   // cleaned up automatically
}

function deactivate() {
  // Optional: release anything not on context.subscriptions.
}

aero.commands

ts
registerCommand(id: string, handler: (...args: any[]) => any): Disposable;
executeCommand<T = unknown>(id: string, ...args: any[]): Promise<T>;
  • registerCommand(id, handler) — register a handler for a command contributed in aero.json. Returns a Disposable; push it onto context.subscriptions.
  • executeCommand(id, …args) — run any command: your own, another extension's, or a built-in. Resolves with the command's result.
js
function activate(context) {
  context.subscriptions.push(
    aero.commands.registerCommand('my-ext.double', async () => {
      const sel = await aero.editor.getSelection();
      if (sel) await aero.editor.insertText(sel + sel);
    })
  );
}

// Elsewhere — invoke another command:
const out = await aero.commands.executeCommand('my-ext.double');

aero.window

ts
showInformationMessage(message: string): void;
showErrorMessage(message: string): void;
setStatusBarMessage(text: string, hideAfterMs?: number): void;
  • showInformationMessage(message) — a non-blocking info toast.
  • showErrorMessage(message) — an error toast.
  • setStatusBarMessage(text, hideAfterMs?) — transient status-bar text; auto-clears after hideAfterMs if given.
js
aero.window.showInformationMessage('Formatted 3 files.');
aero.window.showErrorMessage('Could not parse selection.');
aero.window.setStatusBarMessage('Working…', 1500);

aero.theme

ts
apply(label: string): Promise<void>;   // switch to a registered theme by label
list(): Promise<string[]>;             // labels of all registered themes
  • apply(label) — switch to a registered theme by its contributed label (built-in or installed).
  • list() — all currently registered theme labels.
js
const labels = await aero.theme.list();         // ['Aero Light', 'Aero Dark', 'Dracula Warm', …]
await aero.theme.apply('Dracula Warm');

aero.editor

ts
getActiveText(): Promise<string | null>;   // full text of the active editor, or null
getSelection(): Promise<string | null>;    // selected text, or null
insertText(text: string): Promise<void>;   // insert at cursor, replacing any selection
js
function activate(context) {
  context.subscriptions.push(
    aero.commands.registerCommand('my-ext.wrap', async () => {
      const sel = await aero.editor.getSelection();
      if (sel == null) {
        aero.window.showErrorMessage('Select some text first.');
        return;
      }
      await aero.editor.insertText('/* ' + sel + ' */');
    })
  );
}

aero.workspace

ts
rootPath(): Promise<string | null>;   // absolute path of the open folder, or null
js
const root = await aero.workspace.rootPath();
if (root) aero.window.setStatusBarMessage('Workspace: ' + root, 2000);

aero.storage

Per-extension persisted key/value store (localStorage-backed). Values are scoped to your extension — other extensions can't read them.

ts
get<T = unknown>(key: string): Promise<T | null>;
set(key: string, value: unknown): Promise<void>;
js
async function activate(context) {
  const runs = (await aero.storage.get('runs')) ?? 0;
  await aero.storage.set('runs', runs + 1);
  aero.window.setStatusBarMessage(`Run #${runs + 1}`, 1500);
}

The full type surface

For reference, the complete aero.d.ts interface tree:

ts
interface AeroApi {
  readonly commands: AeroCommands;
  readonly window: AeroWindow;
  readonly theme: AeroTheme;
  readonly editor: AeroEditor;
  readonly workspace: AeroWorkspace;
  readonly storage: AeroStorage;
}

interface AeroCommands {
  registerCommand(id: string, handler: (...args: any[]) => any): Disposable;
  executeCommand<T = unknown>(id: string, ...args: any[]): Promise<T>;
}
interface AeroWindow {
  showInformationMessage(message: string): void;
  showErrorMessage(message: string): void;
  setStatusBarMessage(text: string, hideAfterMs?: number): void;
}
interface AeroTheme {
  apply(label: string): Promise<void>;
  list(): Promise<string[]>;
}
interface AeroEditor {
  getActiveText(): Promise<string | null>;
  getSelection(): Promise<string | null>;
  insertText(text: string): Promise<void>;
}
interface AeroWorkspace {
  rootPath(): Promise<string | null>;
}
interface AeroStorage {
  get<T = unknown>(key: string): Promise<T | null>;
  set(key: string, value: unknown): Promise<void>;
}

interface ExtensionContext {
  readonly extensionId: string;
  readonly subscriptions: Disposable[];
}
interface Disposable { dispose(): void; }

v1 boundary

This is the whole API in v1. A malicious extension can, at worst, misuse this surface (show a toast, switch a theme, read/insert text in the active editor when the user runs its command). It cannot read arbitrary files, reach the network, or touch other extensions. Broader APIs arrive in later tiers behind the same bridge.

Lean, AI-ready, under your control.