Skip to content

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:

bash
git clone https://github.com/yourname/cloudcli-plugin-my-first-plugin.git
cd cloudcli-plugin-my-first-plugin
npm install

The 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:

bash
mkdir cloudcli-plugin-my-first-plugin
cd cloudcli-plugin-my-first-plugin
git init
npm init -y

Step 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:

json
{
  "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:

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:

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:

bash
mkdir src

Note: The flat src/ layout used in this guide is a recommended convention. The plugin loader only reads the entry and server paths from your manifest.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:

typescript
// 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.

typescript
// 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 typed api object.
  • api.context gives 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:

json
{
  "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.

typescript
// 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:

  1. Listen on port 0 — the OS assigns a random available port
  2. Bind to 127.0.0.1 — never expose to the network
  3. 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:

bash
npm run build

This compiles src/ to dist/. You can test your server standalone:

bash
node dist/server.js
# Should print: {"ready":true,"port":XXXXX}

# In another terminal:
curl http://127.0.0.1:XXXXX/hello

During development, use npm run dev to watch for changes and recompile automatically.

Step 8: Publish and Install

Push your plugin to a Git repository:

bash
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 main

Then install it in CloudCLI UI:

  1. Open Settings (gear icon in the sidebar)
  2. Go to the Plugins tab
  3. Paste your Git URL: https://github.com/yourname/cloudcli-plugin-my-first-plugin.git
  4. 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:

  1. Push changes to your Git repo
  2. In Settings → Plugins, click the refresh icon next to your plugin
  3. 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:

  1. Plugin name (following the cloudcli-plugin-* convention)
  2. Repo link (must be public on GitHub)
  3. Short description of what it does
  4. 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

Last updated March 18, 2026