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.
755 lines
18 KiB
Markdown
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!
|