Files
logikonline 0275df601d feat(secrets): add native file dialog support and documentation
Adds support for native file dialogs through the ui:dialogs permission. Includes comprehensive documentation and demo methods showing how to use saveFile, showSaveDialog, and showOpenDialog APIs.

The dialogs API allows addons to:
- Show native save dialogs and write files directly
- Show open dialogs to select files/directories
- Configure file filters, default paths, and dialog titles

Also adds demo methods (saveFileDemo, openFileDemo) to the starter addon that demonstrate proper usage patterns including permission checks and error handling.
2026-01-23 10:17:35 -05:00

755 lines
18 KiB
Markdown

# 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:
```bash
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
```json
"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
"ui:dialogs" // Access native file dialogs
]
```
### Capabilities
```json
"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
```javascript
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:
```javascript
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)
context.dialogs // Native file dialogs (requires ui:dialogs permission)
```
### Invoke Method
GitCaddy calls your addon's `invoke` method for all actions (toolbar clicks, menu items, context menus, etc.):
```javascript
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:
```javascript
this.context?.ipc.send('show-notification', {
title: 'My Addon',
body: 'Operation completed successfully!',
});
```
### Event Subscription
Subscribe to app events via `context.events`:
```javascript
// 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
```javascript
// 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);
});
```
### Native File Dialogs
Addons with the `ui:dialogs` permission can access native file dialogs via `context.dialogs`:
```javascript
// Check if dialogs are available
if (!this.context?.dialogs) {
return { error: 'Dialogs not available' };
}
// Save file with native dialog
const filePath = await this.context.dialogs.saveFile(content, {
defaultPath: 'export.txt',
filters: [
{ name: 'Text Files', extensions: ['txt'] },
{ name: 'All Files', extensions: ['*'] }
],
title: 'Save Export'
});
// Returns: string (file path) or undefined if canceled
// Open file with native dialog
const result = await this.context.dialogs.showOpenDialog({
filters: [
{ name: 'Text Files', extensions: ['txt', 'md'] },
{ name: 'All Files', extensions: ['*'] }
],
title: 'Select File',
multiSelections: false, // Allow multiple file selection
openDirectory: false // Select directories instead of files
});
// Returns: { canceled: boolean, filePaths: string[] }
// Show save dialog without writing file
const saveResult = await this.context.dialogs.showSaveDialog({
defaultPath: 'document.txt',
filters: [{ name: 'Text', extensions: ['txt'] }],
title: 'Choose Save Location'
});
// Returns: { canceled: boolean, filePath?: string }
```
**Methods:**
- `saveFile(content, options)` - Show save dialog and write content to selected file
- `showSaveDialog(options)` - Show save dialog, returns path only (you handle writing)
- `showOpenDialog(options)` - Show open dialog, returns selected path(s)
### License Status (Optional)
For commercial addons:
```javascript
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
```json
{
"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
```json
{
"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:
```json
"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:
```javascript
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
```json
{
"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
```json
"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
```javascript
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
```javascript
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
```javascript
// 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:
```css
/* 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
```css
::-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
```json
"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)
```csharp
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
```bash
# 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
```javascript
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
```bash
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
- [Octicons](https://primer.style/octicons/) - Icon library for toolbar buttons
- [GitCaddy Documentation](https://gitcaddy.com/docs) - Full addon API documentation
## License
MIT - Use this as a template for your own addons!