Getting Started
Build your first CloudCLI UI plugin from scratch — a working tab plugin in under 10 minutes.
This guide walks you through building a complete plugin from scratch. By the end, you'll have a working tab plugin with a frontend UI and a backend server, all written in TypeScript.
Prerequisites
- A running CloudCLI UI instance
- Node.js 18+ installed
- Git installed
- A GitHub (or any Git host) account for publishing
Step 1: Start from the Template
The fastest way to get going is to use the official plugin starter template. Go to cloudcli-ai/cloudcli-plugin-starter on GitHub and click "Use this template" to create your own repository from it.
Name your repo following the convention cloudcli-plugin-your-plugin-name (for example cloudcli-plugin-git-stats or cloudcli-plugin-deploy-dashboard).
Then clone your new repo:
git clone https://github.com/yourname/cloudcli-plugin-my-first-plugin.git
cd cloudcli-plugin-my-first-plugin
npm installThe template gives you a working plugin out of the box with TypeScript source in src/, a manifest.json, package.json, tsconfig.json, and an icon.svg. You can build and run it as-is to see it in action, then modify it to build your own plugin.
If you prefer to start completely from scratch instead, create an empty directory and run git init:
mkdir cloudcli-plugin-my-first-plugin
cd cloudcli-plugin-my-first-plugin
git init
npm init -yStep 2: Create the Manifest
If you started from the template, you already have a manifest.json. Open it and update the fields to match your plugin:
{
"name": "my-first-plugin",
"displayName": "My First Plugin",
"version": "1.0.0",
"description": "A simple plugin that shows project info and a greeting.",
"author": "Your Name",
"icon": "Zap",
"type": "module",
"slot": "tab",
"entry": "dist/index.js",
"server": "dist/server.js"
}The key fields:
- name — Unique identifier. Only letters, numbers, hyphens, and underscores.
- entry — Your compiled frontend JavaScript file (in
dist/). - server — Your compiled backend Node.js file (optional, remove this line if you don't need a backend).
For all fields, see the Manifest Reference.
Step 3: Set Up TypeScript
Create a tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"lib": ["ES2020", "DOM"]
},
"include": ["src"]
}Add a build script and TypeScript to your package.json:
{
"name": "cloudcli-plugin-my-first-plugin",
"private": true,
"type": "module",
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
},
"devDependencies": {
"typescript": "^5.5.0",
"@types/node": "^20.0.0"
}
}Create a .gitignore to exclude compiled output:
dist/
node_modules/Create the src/ directory:
mkdir srcNote: The flat
src/layout used in this guide is a recommended convention. The plugin loader only reads theentryandserverpaths from yourmanifest.json— as long as your compiled files end up at those paths, you can organize your TypeScript source however you like (e.g.src/client/+src/server/, or any structure that suits your project).
Step 4: Add Type Definitions
Create src/types.ts with the plugin API types. These give you full autocomplete and type-safety:
// src/types.ts
export interface PluginContext {
theme: 'dark' | 'light';
project: { name: string; path: string } | null;
session: { id: string; title: string } | null;
}
export interface PluginAPI {
readonly context: PluginContext;
onContextChange(callback: (ctx: PluginContext) => void): () => void;
rpc(method: string, path: string, body?: unknown): Promise<unknown>;
}
export interface PluginModule {
mount(container: HTMLElement, api: PluginAPI): void | Promise<void>;
unmount?(container: HTMLElement): void;
}Step 5: Write the Frontend Module
Create src/index.ts. Your plugin needs to export two functions: mount and unmount.
// src/index.ts
import type { PluginAPI, PluginContext } from './types.js';
export function mount(container: HTMLElement, api: PluginAPI): void {
// 1. Read current context
const ctx: PluginContext = api.context;
// 2. Render UI
container.innerHTML = `
<div style="padding: 24px; font-family: system-ui, sans-serif;">
<h1 style="margin: 0 0 8px;">Hello from My First Plugin!</h1>
<p style="color: #888;">Theme: ${ctx.theme}</p>
<p style="color: #888;">Project: ${ctx.project?.name ?? 'None selected'}</p>
<div id="server-data" style="margin-top: 16px;">Loading server data...</div>
</div>
`;
// 3. Fetch data from your backend server
api.rpc('GET', '/hello')
.then((data) => {
const el = container.querySelector('#server-data');
if (el) el.textContent = `Server says: ${(data as any).message}`;
})
.catch((err: Error) => {
const el = container.querySelector('#server-data');
if (el) el.textContent = `Server error: ${err.message}`;
});
// 4. Subscribe to context changes (theme, project, session)
const unsubscribe = api.onContextChange((newCtx) => {
const h1 = container.querySelector('h1');
if (h1) {
(h1 as HTMLElement).style.color = newCtx.theme === 'dark' ? '#fff' : '#000';
}
});
// Store unsubscribe for cleanup
(container as any)._cleanup = unsubscribe;
}
export function unmount(container: HTMLElement): void {
(container as any)._cleanup?.();
container.innerHTML = '';
}What's happening:
mount(container, api)is called when the plugin tab opens. You get a DOM element to render into and a typedapiobject.api.contextgives you the current theme, project, and session.api.rpc(method, path, body?)calls your backend server through the host's proxy.api.onContextChange(callback)notifies you when context changes. Returns an unsubscribe function.unmount(container)is called when the tab closes, clean up here.
For the full API, see the Frontend API Reference.
Frontend-Only Plugins
If your plugin doesn't need a backend server, you can skip Step 6 entirely. Remove the server field from your manifest:
{
"name": "simple-widget",
"displayName": "Simple Widget",
"version": "1.0.0",
"type": "module",
"slot": "tab",
"entry": "dist/index.js"
}Your mount function can still use api.context and api.onContextChange, you just won't have api.rpc calls. This is a good option for plugins that only display static UI, render data from the context, or work entirely client-side.
Step 6: Write the Backend Server
Create src/server.ts. Your server must listen on a random port and print a JSON ready signal to stdout.
// src/server.ts
import http from 'node:http';
const server = http.createServer((req, res) => {
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
res.setHeader('Content-Type', 'application/json');
if (url.pathname === '/hello') {
res.writeHead(200);
res.end(JSON.stringify({
message: `Hello from the plugin server! (Node ${process.version})`,
pluginName: process.env.PLUGIN_NAME,
}));
return;
}
res.writeHead(404);
res.end(JSON.stringify({ error: 'Not found' }));
});
// Listen on a random port, bind to localhost only
server.listen(0, '127.0.0.1', () => {
const addr = server.address();
if (addr && typeof addr !== 'string') {
// CRITICAL: Print the ready signal. The host waits for this.
console.log(JSON.stringify({ ready: true, port: addr.port }));
}
});Three rules for your server:
- Listen on port 0 — the OS assigns a random available port
- Bind to 127.0.0.1 — never expose to the network
- Print the ready signal —
{"ready": true, "port": <number>}as a JSON line to stdout within 10 seconds
For advanced patterns (Express, secrets, async init, caching), see Backend Servers.
Step 7: Build and Test
Compile your TypeScript:
npm run buildThis compiles src/ to dist/. You can test your server standalone:
node dist/server.js
# Should print: {"ready":true,"port":XXXXX}
# In another terminal:
curl http://127.0.0.1:XXXXX/helloDuring development, use npm run dev to watch for changes and recompile automatically.
Step 8: Publish and Install
Push your plugin to a Git repository:
git add .
git commit -m "Initial plugin"
git remote add origin https://github.com/yourname/cloudcli-plugin-my-first-plugin.git
git push -u origin mainThen install it in CloudCLI UI:
- Open Settings (gear icon in the sidebar)
- Go to the Plugins tab
- Paste your Git URL:
https://github.com/yourname/cloudcli-plugin-my-first-plugin.git - Click Install
The host clones the repo, runs npm install, runs npm run build to compile your TypeScript, and your plugin tab should appear immediately.
Step 9: Iterate
To update your plugin after making changes:
- Push changes to your Git repo
- In Settings → Plugins, click the refresh icon next to your plugin
- The host pulls the latest code, reinstalls dependencies, recompiles TypeScript, and restarts your server
Submit Your Plugin to the Official Directory
Want other CloudCLI users to discover and install your plugin? Share it in the Show and Tell category on GitHub Discussions.
In your post, include:
- Plugin name (following the
cloudcli-plugin-*convention) - Repo link (must be public on GitHub)
- Short description of what it does
- Screenshot or GIF showing the plugin in action
Before posting, make sure your plugin installs cleanly from the Git URL via Settings → Plugins and that your repo has a clear README.md.
The team reviews submissions and adds approved plugins to the official Available Plugins list. The community can also upvote and comment on your plugin, which helps surface the most useful ones.
Next Steps
- Manifest Reference — Every field in
manifest.jsonexplained - Frontend API Reference — Full documentation of the
apiobject - Backend Servers — Advanced server patterns, secrets, and lifecycle
- Security Model — How plugins are isolated
- Example: Project Stats — Walk through the built-in example plugin