Your first plugin
An AppOS plugin is a directory with two required files: a plugin.json
manifest and a compiled JS bundle.
1. The manifest
Section titled “1. The manifest”{ "$schema": "https://appos.space/schemas/plugin-v1.json", "id": "com.example.hello", "name": "Hello AppOS", "version": "1.0.0", "runtime": "javascript", "entrypoint": "dist/main.js", "activation": { "events": ["onStartup"] }, "permissions": [ { "scope": "ui.sidebar", "reason": "Show the file list panel" }, { "scope": "filesystem.read", "reason": "List the current folder" } ]}idis a reverse-domain identifier (^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$).permissionsentries can be bare scope strings or{ scope, reason }objects — thereasonshows up in the approval sheet. See the permission scopes catalog.- The full field list is in the manifest reference.
2. The entrypoint
Section titled “2. The entrypoint”The host calls globalThis.activate(context) when the plugin activates. The
context object (PluginContext) carries every API
namespace.
import type { PluginContext, PluginFileDescriptor } from "@appos.space/plugin-types";import { vstack, section, listItem, button } from "@appos.space/view-builders";import { formatSize } from "@appos.space/plugin-utils";
const PANEL_ID = "hello-panel";
function fileListView(files: PluginFileDescriptor[]) { return vstack([ section("Current Folder", { icon: "folder" }, files.map((f) => listItem(f.name, { icon: f.isDirectory ? "folder" : "doc", trailing: f.size !== null ? formatSize(f.size) : undefined, }), ), ), button("Refresh", { action: "refresh" }), ]);}
async function listActiveDirectory(ctx: PluginContext) { const dir = await ctx.fileOps.getActiveDirectory(); return ctx.fileOps.listDirectory(dir);}
export async function activate(ctx: PluginContext) { ctx.ui.registerPanel(PANEL_ID, { title: "Hello AppOS", icon: "folder", target: "sidebar", view: fileListView(await listActiveDirectory(ctx)), handler: async (action) => { if (action === "refresh") { ctx.ui.updatePanel(PANEL_ID, { view: fileListView(await listActiveDirectory(ctx)), }); } }, });}
export function deactivate() { // optional cleanup — registrations are auto-cancelled on teardown}
// The host looks for globals in an IIFE bundle. The cast is needed because// `typeof globalThis` has no `activate`/`deactivate` properties under strict TS:(globalThis as any).activate = activate;(globalThis as any).deactivate = deactivate;3. Build and validate
Section titled “3. Build and validate”esbuild src/main.ts --bundle --format=iife --target=es2020 --outfile=dist/main.jsValidate your manifest against the schema shipped in this repo:
node scripts/validate-schema.mjs path/to/plugin.jsonWhere to go next
Section titled “Where to go next”- API namespaces — the full
context.*surface - Manifest & permissions — every manifest field and scope
- Extension points — contributing to core plugins via
extensions[]