Files
myfirst-addon/main/index.js
logikonline e79fd3f281 feat: add .NET host process integration
Implements native host process support with .NET backend:
- Add .NET host project with HTTP endpoints (health check, hello world)
- Configure multi-platform host executables in addon.json
- Add host communication methods in main process
- Create host demo tab in UI
- Add .gitignore for .NET build artifacts

The host process runs on a dynamic port and communicates via HTTP with the main addon process.
2026-01-18 14:48:43 -05:00

576 lines
16 KiB
JavaScript

/**
* My First Addon - Main Process Module
*
* This file runs in Electron's main process and handles:
* - Addon lifecycle (initialize, activate, deactivate, dispose)
* - Settings management
* - IPC communication with renderer process
* - Method invocations from GitCaddy
* - Event handling
*
* The main process has full Node.js access and can:
* - Read/write files
* - Make network requests
* - Spawn child processes
* - Access system APIs
*/
// Import shared types (optional - for code organization)
const { DEFAULT_SETTINGS } = require('../shared/types');
/**
* Main Addon Class
*
* GitCaddy will instantiate this class and call its lifecycle methods.
* The class must be the default export.
*/
class MyFirstAddon {
constructor() {
// Context provided by GitCaddy (set in initialize)
this.context = null;
// Local settings cache
this.settings = { ...DEFAULT_SETTINGS };
// Track disposables for cleanup
this.disposables = [];
// Track if addon is active
this.isActive = false;
}
// ============================================================
// LIFECYCLE METHODS
// These are called by GitCaddy at specific points
// ============================================================
/**
* Initialize the addon.
*
* Called once when the addon is first loaded.
* Use this to set up initial state and register event listeners.
*
* @param {IAddonMainContext} context - Context provided by GitCaddy
*/
async initialize(context) {
this.context = context;
// Log that we're initializing
context.log.info('MyFirstAddon initializing...');
// Load settings from storage
await this.loadSettings();
// Log loaded settings (be careful not to log sensitive data!)
context.log.debug('Settings loaded:', {
enableFeature: this.settings.enableFeature,
mode: this.settings.mode,
maxItems: this.settings.maxItems,
// Don't log apiKey!
});
// Register for settings changes
const settingsDisposable = context.settings.onDidChange((key, value) => {
this.onSettingChanged(key, value);
});
this.disposables.push(settingsDisposable);
// Register for repository events
// Note: Use the specific event methods, not a generic 'on' method
const repoDisposable = context.events.onRepositorySelected((repo) => {
this.onRepositoryChanged(repo);
});
this.disposables.push(repoDisposable);
// Register for commit events
const commitDisposable = context.events.onCommitCreated((commit) => {
this.onCommitCreated(commit);
});
this.disposables.push(commitDisposable);
context.log.info('MyFirstAddon initialized successfully');
}
/**
* Activate the addon.
*
* Called when the addon is enabled/activated.
* Start any background processes or services here.
*/
async activate() {
this.context?.log.info('MyFirstAddon activating...');
this.isActive = true;
// Example: Start a periodic task
// this.startPeriodicTask();
this.context?.log.info('MyFirstAddon activated');
return true; // Must return true to indicate successful activation
}
/**
* Deactivate the addon.
*
* Called when the addon is disabled.
* Stop background processes but don't fully clean up.
*/
async deactivate() {
this.context?.log.info('MyFirstAddon deactivating...');
this.isActive = false;
// Example: Stop periodic tasks
// this.stopPeriodicTask();
this.context?.log.info('MyFirstAddon deactivated');
}
/**
* Dispose of the addon.
*
* Called when the addon is being unloaded.
* Clean up all resources, event listeners, etc.
*/
dispose() {
this.context?.log.info('MyFirstAddon disposing...');
// Dispose all registered disposables
for (const disposable of this.disposables) {
disposable.dispose();
}
this.disposables = [];
// Clear references
this.context = null;
this.isActive = false;
console.log('MyFirstAddon disposed');
}
// ============================================================
// INVOKE METHOD
// GitCaddy calls this to invoke addon functionality
// ============================================================
/**
* Handle method invocations from GitCaddy.
*
* This is the main entry point for GitCaddy to call your addon's functionality.
* Methods can be invoked from:
* - Toolbar buttons (onClick.action = "invoke-method")
* - Menu items
* - Context menus
* - Other addons
* - The renderer process
*
* @param {string} method - Method name to invoke
* @param {unknown[]} args - Arguments passed to the method
* @returns {Promise<unknown>} - Result of the method
*/
async invoke(method, args) {
this.context?.log.debug(`Invoke called: ${method}`, args);
switch (method) {
// ---- Settings Methods ----
case 'getSettings':
return this.getSettings();
case 'updateSettings':
return this.updateSettings(args[0]);
// ---- Badge Methods ----
case 'getBadgeContent':
return this.getBadgeContent();
// ---- Action Methods ----
case 'quickAction':
return this.quickAction(args[0], args[1]);
case 'menuAction':
return this.menuAction();
case 'processFiles':
return this.processFiles(args[0]);
case 'analyzeCommit':
return this.analyzeCommit(args[0]);
// ---- Demo Methods ----
case 'getDemoData':
return this.getDemoData();
case 'performAction':
return this.performAction(args[0]);
// ---- Host Methods ----
// These call the .NET host process
case 'callHelloWorld':
return this.callHelloWorld(args[0]);
// ---- Capability Methods ----
// These are called by GitCaddy for registered capabilities
case 'generateCommitMessage':
return this.generateCommitMessage(args[0]);
default:
throw new Error(`Unknown method: ${method}`);
}
}
// ============================================================
// LICENSE METHODS
// Optional: Implement if your addon has licensing
// ============================================================
/**
* Get the license status for this addon.
*
* GitCaddy calls this method to check if the addon is licensed.
* If not implemented, the addon is assumed to be licensed (free addon).
*
* @returns {{ valid: boolean, message?: string }}
*/
getLicenseStatus() {
// For free/open-source addons, always return valid
return { valid: true };
// For commercial addons, you would check license here:
// return {
// valid: this.isLicenseValid,
// message: this.isLicenseValid ? undefined : 'License expired'
// };
}
// ============================================================
// SETTINGS METHODS
// ============================================================
/**
* Load settings from GitCaddy's settings store.
*/
async loadSettings() {
if (!this.context) return;
const stored = this.context.settings.getAll();
this.settings = {
enableFeature: stored.enableFeature ?? DEFAULT_SETTINGS.enableFeature,
apiKey: stored.apiKey ?? DEFAULT_SETTINGS.apiKey,
mode: stored.mode ?? DEFAULT_SETTINGS.mode,
maxItems: stored.maxItems ?? DEFAULT_SETTINGS.maxItems,
};
}
/**
* Handle setting changes.
*/
onSettingChanged(key, value) {
this.context?.log.debug(`Setting changed: ${key} =`, value);
if (key in this.settings) {
this.settings[key] = value;
}
// React to specific setting changes
if (key === 'mode') {
this.context?.log.info(`Mode changed to: ${value}`);
}
}
/**
* Get current settings (for UI).
*/
getSettings() {
return {
...this.settings,
// Don't expose the actual API key, just whether it's set
hasApiKey: !!this.settings.apiKey,
apiKey: undefined,
};
}
/**
* Update settings.
*/
async updateSettings(updates) {
if (!this.context) return;
for (const [key, value] of Object.entries(updates)) {
if (value !== undefined) {
await this.context.settings.set(key, value);
}
}
return { success: true };
}
// ============================================================
// BADGE METHODS
// ============================================================
/**
* Get badge content for toolbar button.
*
* Called by GitCaddy to update the badge on your toolbar button.
* Return null/undefined to hide the badge.
*
* @returns {string|number|null} Badge content
*/
getBadgeContent() {
// Example: Return a count or status indicator
if (!this.settings.enableFeature) {
return null; // Hide badge when feature is disabled
}
// Return a number, string, or null
return '3'; // Example: "3 items"
}
// ============================================================
// ACTION METHODS
// ============================================================
/**
* Quick action triggered by toolbar button.
*/
async quickAction(arg1, arg2) {
this.context?.log.info('Quick action triggered!', { arg1, arg2 });
// Show a notification
this.context?.ipc.send('show-notification', {
title: 'Quick Action',
body: `Action executed with args: ${arg1}, ${arg2}`,
});
return { success: true };
}
/**
* Menu action triggered from Repository menu.
*/
async menuAction() {
this.context?.log.info('Menu action triggered!');
// Example: Get the current repository
const repo = this.context?.appState?.getCurrentRepository();
if (repo) {
this.context?.log.info('Current repository:', repo.path);
}
return { success: true };
}
/**
* Process selected files from context menu.
*/
async processFiles(files) {
this.context?.log.info('Processing files:', files);
// Example file processing
const results = [];
for (const file of files || []) {
results.push({
path: file.path,
status: file.status,
processed: true,
});
}
return { files: results };
}
/**
* Analyze a commit from context menu.
*/
async analyzeCommit(commit) {
this.context?.log.info('Analyzing commit:', commit?.sha);
return {
sha: commit?.sha,
analysis: 'This is a demo analysis result',
};
}
// ============================================================
// DEMO METHODS (for the demo view)
// ============================================================
/**
* Get demo data for the view.
*/
getDemoData() {
return {
addonId: this.context?.addonId,
addonPath: this.context?.addonPath,
isActive: this.isActive,
settings: this.getSettings(),
features: [
'Toolbar buttons',
'Menu items',
'Context menus',
'Custom views',
'Settings storage',
'IPC communication',
'Event handling',
],
};
}
/**
* Perform a demo action.
*/
async performAction(actionType) {
this.context?.log.info(`Performing action: ${actionType}`);
switch (actionType) {
case 'notify':
this.context?.ipc.send('show-notification', {
title: 'Hello from Addon!',
body: 'This notification was triggered by the addon.',
});
return { message: 'Notification sent!' };
case 'log':
this.context?.log.info('This is an info log from the demo');
this.context?.log.warn('This is a warning log from the demo');
this.context?.log.debug('This is a debug log from the demo');
return { message: 'Check the logs!' };
case 'settings':
return this.getSettings();
default:
return { message: `Unknown action: ${actionType}` };
}
}
// ============================================================
// CAPABILITY METHODS
// ============================================================
/**
* Generate commit message (if you registered 'commit-message-generation' capability).
*
* GitCaddy will call this when the user requests a commit message.
*/
async generateCommitMessage(context) {
this.context?.log.info('Generating commit message...');
// Example: Simple commit message based on files
const fileCount = context.files?.length || 0;
const summary = `chore: update ${fileCount} file(s)`;
return {
summary,
description: 'This is a demo commit message from My First Addon.',
};
}
// ============================================================
// HOST METHODS
// These methods communicate with the .NET host process
// ============================================================
/**
* Call the Hello World endpoint on the .NET host.
*
* Demonstrates how to communicate with a native host process.
*
* @param {string} [name] - Optional name to greet
* @returns {Promise<{message: string, timestamp: string}>}
*/
async callHelloWorld(name) {
if (!this.context?.hostPort) {
this.context?.log.warn('Host port not available - host may not be running');
return { error: 'Host not available', message: null };
}
try {
const endpoint = name
? `http://127.0.0.1:${this.context.hostPort}/hello/${encodeURIComponent(name)}`
: `http://127.0.0.1:${this.context.hostPort}/hello`;
this.context?.log.info(`Calling host endpoint: ${endpoint}`);
const response = await fetch(endpoint);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
this.context?.log.info('Host response:', result);
return result;
} catch (error) {
this.context?.log.error('Failed to call host:', error);
return { error: error.message, message: null };
}
}
// ============================================================
// EVENT HANDLERS
// ============================================================
/**
* Handle repository changes.
*/
onRepositoryChanged(repo) {
this.context?.log.debug('Repository changed:', repo?.path);
// Example: Refresh badge when repo changes
// The badge will be refreshed automatically by GitCaddy
}
/**
* Handle new commits.
*/
onCommitCreated(commit) {
this.context?.log.debug('New commit created:', commit?.sha);
}
}
// ============================================================
// CONTEXT INTERFACE (for reference)
// ============================================================
/**
* @typedef {Object} IAddonMainContext
* @property {string} addonId - Unique addon identifier
* @property {string} addonPath - Path to addon directory
* @property {Object} manifest - Parsed addon.json
* @property {IAddonLogger} log - Logger instance
* @property {IAddonSettings} settings - Settings storage
* @property {IAddonEvents} events - Event emitter
* @property {IAddonIPC} ipc - IPC communication
* @property {number} [hostPort] - Port of native host (if configured)
* @property {IAppStateProxy} [appState] - Access to app state
*/
/**
* @typedef {Object} IAddonLogger
* @property {function} debug - Log debug message
* @property {function} info - Log info message
* @property {function} warn - Log warning message
* @property {function} error - Log error message
*/
/**
* @typedef {Object} IAddonSettings
* @property {function} get - Get a setting value
* @property {function} set - Set a setting value
* @property {function} getAll - Get all settings
* @property {function} onDidChange - Register for setting changes
*/
/**
* @typedef {Object} IAddonEvents
* @property {function} onAppReady - Called when app is ready
* @property {function} onAppWillQuit - Called before app quits
* @property {function} onRepositorySelected - Called when repository changes
* @property {function} onFilesChanged - Called when files change
* @property {function} onCommitCreated - Called when a commit is created
* @property {function} emit - Emit a custom event
*/
// Export the addon class as default
module.exports = MyFirstAddon;
module.exports.default = MyFirstAddon;