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.

An example of a plugin-starter can be found on https://github.com/cloudcli-ai/cloudcli-plugin-starter

Prerequisites

  • A running CloudCLI UI instance
  • Node.js 18+ installed
  • Git installed
  • A GitHub (or any Git host) account for publishing

Step 1: Create the Plugin Directory

bash
mkdir my-first-plugin
cd my-first-plugin
git init

Step 2: Create the Manifest

Create manifest.json — this tells CloudCLI UI about 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": "index.js",
  "server": "server.js"
}

The key fields:

  • name — Unique identifier. Only letters, numbers, hyphens, and underscores.
  • entry — Your frontend JavaScript file.
  • server — Your backend Node.js file (optional — remove this line if you don't need a backend).

For all fields, see the Manifest Reference.

Step 3: Write the Frontend Module

Create index.js. Your plugin needs to export two functions: mount and unmount.

javascript
// index.js

export function mount(container, api) {
  // 1. Read current context
  const ctx = 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.message}`;
    })
    .catch(err => {
      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.style.color = newCtx.theme === 'dark' ? '#fff' : '#000';
    }
  });

  // Store unsubscribe for cleanup
  container._cleanup = unsubscribe;
}

export function unmount(container) {
  if (container._cleanup) {
    container._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 an 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.

Step 4: Write the Backend Server

Create server.js. Your server must listen on a random port and print a JSON ready signal to stdout.

javascript
// server.js
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 port = server.address().port;

  // CRITICAL: Print the ready signal. The host waits for this.
  console.log(JSON.stringify({ ready: true, 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 5: Test Locally (Optional)

You can test your server standalone:

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

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

Step 6: 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/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/my-first-plugin.git
  4. Click Install

Your plugin tab should appear immediately in the main content area.

Step 7: 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 and restarts your server

Frontend-Only Plugin

If you don't need a backend server, remove the server field from your manifest:

json
{
  "name": "simple-widget",
  "displayName": "Simple Widget",
  "version": "1.0.0",
  "type": "module",
  "slot": "tab",
  "entry": "index.js"
}

Your mount function can still use api.context and api.onContextChange — you just won't have api.rpc calls.

Next Steps

Last updated March 9, 2026