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:
npm install --save-dev @aero/ext/// <reference types="@aero/ext" />The entry point
The host calls activate after loading main; deactivate is optional.
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; }'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
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 inaero.json. Returns aDisposable; push it ontocontext.subscriptions.executeCommand(id, …args)— run any command: your own, another extension's, or a built-in. Resolves with the command's result.
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
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 afterhideAfterMsif given.
aero.window.showInformationMessage('Formatted 3 files.');
aero.window.showErrorMessage('Could not parse selection.');
aero.window.setStatusBarMessage('Working…', 1500);aero.theme
apply(label: string): Promise<void>; // switch to a registered theme by label
list(): Promise<string[]>; // labels of all registered themesapply(label)— switch to a registered theme by its contributedlabel(built-in or installed).list()— all currently registered theme labels.
const labels = await aero.theme.list(); // ['Aero Light', 'Aero Dark', 'Dracula Warm', …]
await aero.theme.apply('Dracula Warm');aero.editor
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 selectionfunction 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
rootPath(): Promise<string | null>; // absolute path of the open folder, or nullconst 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.
get<T = unknown>(key: string): Promise<T | null>;
set(key: string, value: unknown): Promise<void>;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:
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.
Related
- Commands — declaring the commands these handlers back.
- Activation events — when
activateruns. - Examples —
aero.*used in a complete extension.