logikonline 3bd09651de docs: expand README with quick start and detailed API examples
- Add quick start section for development and distribution
- Expand project structure to include host and CI/CD files
- Add detailed code examples for lifecycle methods and context API
- Document invoke method pattern for handling addon actions
- Improve descriptions of manifest fields and permissions
- Add native host build instructions
2026-01-18 15:49:32 -05:00

My First Addon

A comprehensive demo addon for GitCaddy that showcases all available addon features and APIs. Use this as a reference implementation and template for building your own addons.

Overview

This addon demonstrates:

  • Addon Manifest (addon.json) - How to define your addon's metadata, permissions, and contributions
  • Main Process Module (main/index.js) - Lifecycle methods, IPC, settings, and method invocations
  • Renderer Process Module (renderer/index.js) - UI components and renderer-side communication
  • Custom Views - HTML-based views for repository panels and settings
  • Native Host (host/) - .NET backend for computationally intensive or platform-specific operations
  • Context Menus - Right-click menu items for files and commits
  • Notifications - System notifications from addon actions
  • CI/CD - Automated build and release workflow

Project Structure

myfirst-addon/
├── addon.json              # Addon manifest (required)
├── main/
│   └── index.js            # Main process module (required)
├── renderer/
│   └── index.js            # Renderer process module (optional)
├── shared/
│   └── types.js            # Shared constants and types
├── views/
│   ├── demo.html           # Demo repository view
│   └── settings.html       # Settings panel view
├── host/                   # Native .NET host (optional)
│   ├── Program.cs          # Host entry point
│   └── MyFirstAddon.Host.csproj
├── .gitea/
│   └── workflows/
│       └── release.yml     # CI/CD workflow
├── .gitignore
└── README.md               # This file

Quick Start

For Development

  1. Clone/copy this folder to GitCaddy's sideload directory:

    Windows: %APPDATA%/GitCaddy/sideload-addons/
    macOS:   ~/Library/Application Support/GitCaddy/sideload-addons/
    Linux:   ~/.config/GitCaddy/sideload-addons/
    
  2. If using the .NET host, build it first:

    cd host
    dotnet publish -c Release -r win-x64 --self-contained -p:PublishSingleFile=true -o ../host/win-x64
    
  3. Restart GitCaddy or reload addons from the Addon Manager

For Distribution

Package as a .gcaddon file (zip with different extension) and distribute via:

  • Addon Manager's "Install from file" option
  • Your addon repository/marketplace

Addon Manifest (addon.json)

The manifest defines everything about your addon.

Required Fields

Field Description
id Unique identifier (reverse domain notation: com.yourcompany.addon-name)
name Display name shown in Addon Manager
version Semantic version (major.minor.patch)
minAppVersion Minimum GitCaddy version required
description Short description for the addon list
main Path to main process JavaScript module

Optional Fields

Field Description
renderer Path to renderer process module for UI components
author Author information (name, email, url)
host Native host configuration for .NET/Go/Rust backends
permissions Required permissions (shown to user before install)
capabilities What your addon can do
contributes UI elements your addon adds

Permissions

"permissions": [
  "repository:read",     // Read repository data
  "repository:write",    // Modify repository
  "diff:read",           // Read diff/changes
  "commit:read",         // Read commit history
  "branch:read",         // Read branch information
  "network:localhost",   // Make local network requests
  "network:external",    // Make external network requests
  "settings:store",      // Store addon settings
  "notifications:show"   // Show system notifications
]

Capabilities

"capabilities": [
  "commit-message-generation",  // Can generate commit messages
  "repository-analysis",        // Can analyze repositories
  "custom-view"                 // Has custom UI views
]

Main Process Module

The main module (main/index.js) runs in Electron's main process with full Node.js access.

Lifecycle Methods

class MyAddon {
  constructor() {
    this.context = null;
    this.isActive = false;
  }

  // Called once when addon is loaded
  async initialize(context) {
    this.context = context;
    context.log.info('Addon initializing...');

    // Register event listeners
    const disposable = context.events.onRepositorySelected((repo) => {
      this.onRepositoryChanged(repo);
    });
    this.disposables.push(disposable);
  }

  // Called when addon is enabled - MUST return true
  async activate() {
    this.context?.log.info('Addon activating...');
    this.isActive = true;
    return true; // Required for successful activation
  }

  // Called when addon is disabled
  async deactivate() {
    this.context?.log.info('Addon deactivating...');
    this.isActive = false;
  }

  // Called when addon is unloaded - clean up resources
  dispose() {
    for (const disposable of this.disposables) {
      disposable.dispose();
    }
    this.disposables = [];
    this.context = null;
  }
}

module.exports = MyAddon;
module.exports.default = MyAddon;

Important: The activate() method must return true to indicate successful activation.

Context Object

The context object provides access to GitCaddy APIs:

context.addonId       // Your addon's ID
context.addonPath     // Path to addon directory
context.manifest      // Parsed addon.json
context.log           // Logger (.debug, .info, .warn, .error)
context.settings      // Settings storage
context.events        // Event subscription
context.ipc           // IPC communication
context.hostPort      // Port of native host (if configured)

Invoke Method

GitCaddy calls your addon's invoke method for all actions (toolbar clicks, menu items, context menus, etc.):

async invoke(method, args) {
  switch (method) {
    case 'getSettings':
      return this.getSettings();

    case 'processFiles':
      return this.processFiles(args[0]);

    case 'analyzeCommit':
      return this.analyzeCommit(args[0]);

    default:
      throw new Error(`Unknown method: ${method}`);
  }
}

Showing Notifications

Display system notifications from your addon:

this.context?.ipc.send('show-notification', {
  title: 'My Addon',
  body: 'Operation completed successfully!',
});

Event Subscription

Subscribe to app events via context.events:

// Repository changed
context.events.onRepositorySelected((repo) => {
  console.log('Repository:', repo.path);
});

// Commit created
context.events.onCommitCreated((commit) => {
  console.log('New commit:', commit.sha);
});

// Files changed
context.events.onFilesChanged((files) => {
  console.log('Files changed:', files.length);
});

// App lifecycle
context.events.onAppReady(() => { });
context.events.onAppWillQuit(() => { });

Settings Storage

// Load settings
const stored = context.settings.getAll();
const value = context.settings.get('myKey');

// Save settings
await context.settings.set('myKey', 'myValue');

// Listen for changes
context.settings.onDidChange((key, value) => {
  console.log(`Setting ${key} changed to:`, value);
});

License Status (Optional)

For commercial addons:

getLicenseStatus() {
  return { valid: true }; // Free addon

  // Commercial addon:
  // return {
  //   valid: this.isLicenseValid,
  //   message: this.isLicenseValid ? undefined : 'License expired'
  // };
}

Contributions

UI elements your addon adds to GitCaddy.

Toolbar Buttons

{
  "id": "my-button",
  "tooltip": "Button Tooltip",
  "icon": { "type": "octicon", "value": "rocket" },
  "position": "after-push-pull",
  "onClick": {
    "action": "show-view",
    "target": "my-view"
  },
  "badge": {
    "method": "getBadgeContent"
  }
}

Position options: start, end, after-push-pull, after-branch

onClick actions:

  • show-view - Show a repository view
  • invoke-method - Call an addon method
  • open-url - Open a URL in browser

Menu Items

{
  "id": "my-menu-item",
  "menu": "repository",
  "label": "My Action",
  "accelerator": "CmdOrCtrl+Shift+M",
  "onClick": {
    "action": "invoke-method",
    "method": "menuAction"
  }
}

Context Menu Items

Add items to right-click menus:

"contextMenuItems": [
  {
    "id": "file-action",
    "context": "file-list",
    "label": "Process with My Addon",
    "onClick": {
      "action": "invoke-method",
      "method": "processFiles"
    }
  },
  {
    "id": "commit-action",
    "context": "commit-list",
    "label": "Analyze with My Addon",
    "onClick": {
      "action": "invoke-method",
      "method": "analyzeCommit"
    }
  }
]

Context options: file-list (Changes tab), commit-list (History tab)

The invoke method receives context data:

async processFiles(contextData) {
  const files = contextData?.files || [];
  // files: [{ path: 'src/file.js', status: 'modified' }, ...]
}

async analyzeCommit(contextData) {
  const commit = contextData?.commit;
  // commit: { sha, summary, body, author: { name, email, date } }
}

Repository Views

{
  "id": "my-view",
  "title": "My View",
  "icon": "rocket",
  "renderer": {
    "type": "iframe",
    "source": "views/my-view.html"
  },
  "context": ["repository"],
  "headerActions": [
    {
      "id": "refresh",
      "label": "Refresh",
      "icon": "sync",
      "primary": true
    },
    {
      "id": "settings",
      "label": "Settings",
      "icon": "gear"
    }
  ]
}

Context options: repository, diff, commit, always

Header Actions: Buttons displayed in GitCaddy's view header (next to close button). Use primary: true to highlight.

Settings Definitions

"settingsDefinitions": [
  {
    "key": "enableFeature",
    "type": "boolean",
    "default": true,
    "description": "Enable the main feature"
  },
  {
    "key": "apiKey",
    "type": "string",
    "default": "",
    "description": "API key for external service"
  },
  {
    "key": "mode",
    "type": "select",
    "default": "normal",
    "options": [
      { "label": "Normal", "value": "normal" },
      { "label": "Advanced", "value": "advanced" }
    ]
  },
  {
    "key": "maxItems",
    "type": "number",
    "default": 10
  }
]

View Communication

Views (iframes) communicate with GitCaddy via postMessage.

Receiving Messages

const ADDON_ID = 'com.example.myfirst-addon';

window.addEventListener('message', (event) => {
  const { type, data, actionId, requestId, result, error } = event.data || {};

  switch (type) {
    case 'addon:init-context':
      // Initial context from GitCaddy
      const { repositoryPath, hostPort } = data;
      initializeView();
      break;

    case 'addon:header-action':
      // Header button clicked
      handleHeaderAction(actionId);
      break;

    case 'addon:invoke-response':
      // Response from invoke request
      handleInvokeResponse(requestId, result, error);
      break;
  }
});

Invoking Addon Methods

const pendingRequests = new Map();
let requestIdCounter = 0;

function invokeAddon(method, ...args) {
  return new Promise((resolve, reject) => {
    const requestId = ++requestIdCounter;
    pendingRequests.set(requestId, { resolve, reject });

    window.parent.postMessage({
      type: 'addon:invoke',
      requestId,
      addonId: ADDON_ID,
      method,
      args
    }, '*');

    setTimeout(() => {
      if (pendingRequests.has(requestId)) {
        pendingRequests.delete(requestId);
        reject(new Error('Request timed out'));
      }
    }, 30000);
  });
}

// Handle responses
window.addEventListener('message', (event) => {
  if (event.data?.type === 'addon:invoke-response') {
    const { requestId, result, error } = event.data;
    const pending = pendingRequests.get(requestId);
    if (pending) {
      pendingRequests.delete(requestId);
      error ? pending.reject(new Error(error)) : pending.resolve(result);
    }
  }
});

// Usage
const data = await invokeAddon('getDemoData');

Other Messages

// Open addon settings
window.parent.postMessage({
  type: 'addon:open-settings',
  addonId: ADDON_ID
}, '*');

Dark Mode Styling

GitCaddy injects CSS variables for theming. Always use these to match the app's appearance:

/* Background */
background-color: var(--background, #1e1e1e);
background-color: var(--background-secondary, #252526);

/* Text */
color: var(--foreground, #cccccc);
color: var(--foreground-muted, #999999);

/* Border */
border-color: var(--border, #3c3c3c);

/* Accent colors */
color: var(--accent, #0e639c);
color: var(--success, #4ec9b0);
color: var(--warning, #fcba03);
color: var(--error, #f14c4c);
color: var(--info, #75beff);

/* Buttons */
background-color: var(--button-background, #0e639c);
background-color: var(--button-secondary-background, #3c3c3c);

Dark Mode Scrollbars

::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}
::-webkit-scrollbar-track {
  background: var(--background-secondary, #252526);
}
::-webkit-scrollbar-thumb {
  background: var(--scrollbar-thumb, #5a5a5a);
  border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
  background: var(--scrollbar-thumb-hover, #7a7a7a);
}

/* Firefox */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar-thumb, #5a5a5a) var(--background-secondary, #252526);
}

Native Host (.NET)

For addons requiring native code, performance-critical operations, or platform-specific features.

Host Configuration

"host": {
  "directory": "host",
  "executable": "MyFirstAddon.Host",
  "platforms": {
    "win-x64": "host/win-x64/MyFirstAddon.Host.exe",
    "linux-x64": "host/linux-x64/MyFirstAddon.Host",
    "darwin-x64": "host/darwin-x64/MyFirstAddon.Host",
    "darwin-arm64": "host/darwin-arm64/MyFirstAddon.Host"
  },
  "healthCheck": "/health",
  "startupTimeout": 10000
}

Host Implementation (Program.cs)

var port = Environment.GetEnvironmentVariable("ADDON_HOST_PORT") ?? "5000";
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls($"http://127.0.0.1:{port}");
builder.Logging.SetMinimumLevel(LogLevel.Warning);

var app = builder.Build();

// Health check endpoint (required)
app.MapGet("/health", () => new {
    status = "healthy",
    timestamp = DateTime.UtcNow
});

// Your endpoints
app.MapGet("/hello", () => new {
    message = "Hello from .NET host!"
});

app.MapGet("/hello/{name}", (string name) => new {
    message = $"Hello, {name}!"
});

Console.WriteLine($"MyFirstAddon.Host starting on http://127.0.0.1:{port}");
app.Run();

Building the Host

# Windows
dotnet publish -c Release -r win-x64 --self-contained -p:PublishSingleFile=true -o host/win-x64

# Linux
dotnet publish -c Release -r linux-x64 --self-contained -p:PublishSingleFile=true -o host/linux-x64

# macOS (Intel)
dotnet publish -c Release -r osx-x64 --self-contained -p:PublishSingleFile=true -o host/darwin-x64

# macOS (Apple Silicon)
dotnet publish -c Release -r osx-arm64 --self-contained -p:PublishSingleFile=true -o host/darwin-arm64

Calling the Host

async callHost(endpoint, name) {
  if (!this.context?.hostPort) {
    return { error: 'Host not available' };
  }

  const url = name
    ? `http://127.0.0.1:${this.context.hostPort}${endpoint}/${encodeURIComponent(name)}`
    : `http://127.0.0.1:${this.context.hostPort}${endpoint}`;

  const response = await fetch(url);
  return await response.json();
}

CI/CD Workflow

The .gitea/workflows/release.yml automates building and releasing:

  1. Create Release - Creates a GitHub/Gitea release
  2. Build Host - Builds .NET host for all platforms (Windows, Linux, macOS x64/ARM64)
  3. Package Addon - Creates distribution packages:
    • myfirst-addon-full-{version}.gcaddon - Complete package with all platform hosts
    • myfirst-addon-base-{version}.gcaddon - JavaScript only (no hosts)
    • Individual host binaries for each platform

Triggering a Release

git tag v1.0.0
git push origin v1.0.0

Best Practices

  1. Use the logger - Always use context.log instead of console.log
  2. Handle errors - Wrap async operations in try/catch
  3. Clean up resources - Dispose event listeners in dispose()
  4. Secure secrets - Never log API keys; use hasApiKey pattern in settings
  5. Test all platforms - If using native hosts, test on Windows, macOS, and Linux
  6. Follow themes - Use CSS variables for dark mode compatibility
  7. Return true from activate() - Required for successful activation

Resources

License

MIT - Use this as a template for your own addons!

Description
A comprehensive demo addon for GitCaddy that showcases all available addon features and APIs.
http://www.gitcaddy.com
Readme MIT 162 KiB
2026-01-28 08:16:33 +00:00
Languages
HTML 61.1%
JavaScript 37.7%
C# 1.2%