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.
This commit is contained in:
2026-01-18 14:48:43 -05:00
parent d5be7cc604
commit e79fd3f281
6 changed files with 225 additions and 2 deletions

17
.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
# .NET build outputs
host/bin/
host/obj/
host/win-x64/
host/linux-x64/
host/osx-x64/
host/osx-arm64/
# IDE
.vs/
.vscode/
*.user
*.suo
# OS
.DS_Store
Thumbs.db

View File

@@ -29,8 +29,19 @@
"_comment_renderer": "Optional: Path to renderer process module for UI components", "_comment_renderer": "Optional: Path to renderer process module for UI components",
"renderer": "renderer/index.js", "renderer": "renderer/index.js",
"_comment_host": "Optional: Native host process configuration (for .NET, Go, Rust, etc.)", "_comment_host": "Native host process configuration (.NET backend)",
"_comment_host_note": "Remove this section if your addon is pure JavaScript", "host": {
"directory": "host",
"executable": "MyFirstAddon.Host",
"platforms": {
"win-x64": "host/win-x64/MyFirstAddon.Host.exe",
"linux-x64": "host/linux-x64/MyFirstAddon.Host",
"osx-x64": "host/osx-x64/MyFirstAddon.Host",
"osx-arm64": "host/osx-arm64/MyFirstAddon.Host"
},
"healthCheck": "/health",
"startupTimeout": 10000
},
"_comment_permissions": "Required permissions - user sees these before installing", "_comment_permissions": "Required permissions - user sees these before installing",
"permissions": [ "permissions": [

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>MyFirstAddon.Host</AssemblyName>
<RootNamespace>MyFirstAddon.Host</RootNamespace>
</PropertyGroup>
</Project>

35
host/Program.cs Normal file
View File

@@ -0,0 +1,35 @@
var builder = WebApplication.CreateBuilder(args);
// Configure port from environment variable (set by GitCaddy AddonManager)
var port = Environment.GetEnvironmentVariable("ADDON_HOST_PORT") ?? "47933";
builder.WebHost.UseUrls($"http://127.0.0.1:{port}");
var app = builder.Build();
// Root endpoint
app.MapGet("/", () => new {
status = "ok",
service = "MyFirstAddon.Host",
version = "1.0.0"
});
// Health check endpoint (required by GitCaddy)
app.MapGet("/health", () => new {
status = "healthy",
timestamp = DateTime.UtcNow
});
// Hello World endpoint - demonstrates a simple addon host function
app.MapGet("/hello", () => new {
message = "Hello World from MyFirstAddon.Host!",
timestamp = DateTime.UtcNow
});
// Hello endpoint with name parameter
app.MapGet("/hello/{name}", (string name) => new {
message = $"Hello, {name}!",
timestamp = DateTime.UtcNow
});
Console.WriteLine($"MyFirstAddon.Host starting on http://127.0.0.1:{port}");
app.Run();

View File

@@ -201,6 +201,11 @@ class MyFirstAddon {
case 'performAction': case 'performAction':
return this.performAction(args[0]); return this.performAction(args[0]);
// ---- Host Methods ----
// These call the .NET host process
case 'callHelloWorld':
return this.callHelloWorld(args[0]);
// ---- Capability Methods ---- // ---- Capability Methods ----
// These are called by GitCaddy for registered capabilities // These are called by GitCaddy for registered capabilities
case 'generateCommitMessage': case 'generateCommitMessage':
@@ -460,6 +465,46 @@ class MyFirstAddon {
}; };
} }
// ============================================================
// 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 // EVENT HANDLERS
// ============================================================ // ============================================================

View File

@@ -310,6 +310,7 @@
<button class="tab active" data-tab="overview">Overview</button> <button class="tab active" data-tab="overview">Overview</button>
<button class="tab" data-tab="actions">Actions</button> <button class="tab" data-tab="actions">Actions</button>
<button class="tab" data-tab="ipc">IPC Demo</button> <button class="tab" data-tab="ipc">IPC Demo</button>
<button class="tab" data-tab="host">Host Demo</button>
<button class="tab" data-tab="logs">Logs</button> <button class="tab" data-tab="logs">Logs</button>
</div> </div>
@@ -420,6 +421,46 @@
</div> </div>
</div> </div>
<!-- Host Demo Tab -->
<div id="host" class="tab-content">
<div class="section">
<h2>.NET Host Communication</h2>
<p>
This addon includes a .NET native host process that provides HTTP endpoints.
GitCaddy automatically starts the host when the addon is activated and
provides the port number via <code>context.hostPort</code>.
</p>
<h3>Hello World</h3>
<p>Call the /hello endpoint on the .NET host.</p>
<div class="button-group">
<button class="btn" onclick="callHelloWorld()">
Call Hello World
</button>
<button class="btn btn-secondary" onclick="callHelloWorldWithName()">
Call with Name
</button>
</div>
<h3>Host Response</h3>
<div class="code-block" id="host-result">
Click a button above to test host communication.
</div>
</div>
<div class="section">
<h2>Host Status</h2>
<div class="info-row">
<span class="info-label">Host Port:</span>
<span class="info-value" id="host-port">Loading...</span>
</div>
<div class="info-row">
<span class="info-label">Host Available:</span>
<span class="info-value" id="host-available">Unknown</span>
</div>
</div>
</div>
<!-- Logs Tab --> <!-- Logs Tab -->
<div id="logs" class="tab-content"> <div id="logs" class="tab-content">
<div class="section"> <div class="section">
@@ -558,6 +599,9 @@
async function initializeView() { async function initializeView() {
log('info', 'Demo view initialized'); log('info', 'Demo view initialized');
// Update host status display
updateHostStatus();
// Load addon data // Load addon data
try { try {
const data = await invokeAddon('getDemoData'); const data = await invokeAddon('getDemoData');
@@ -701,6 +745,66 @@
} }
} }
// ============================================================
// HOST DEMO FUNCTIONS
// ============================================================
async function callHelloWorld() {
const resultEl = document.getElementById('host-result');
resultEl.innerHTML = '<span class="loading"></span> Calling host...';
log('info', 'Calling host /hello endpoint');
try {
const result = await invokeAddon('callHelloWorld');
resultEl.textContent = JSON.stringify(result, null, 2);
if (result.error) {
log('error', `Host error: ${result.error}`);
document.getElementById('host-available').textContent = 'No (Error)';
} else {
log('info', `Host response: ${result.message}`);
document.getElementById('host-available').textContent = 'Yes';
}
} catch (err) {
resultEl.textContent = 'Error: ' + err.message;
log('error', `Host call failed: ${err.message}`);
document.getElementById('host-available').textContent = 'No (Exception)';
}
}
async function callHelloWorldWithName() {
const name = prompt('Enter your name:', 'World');
if (!name) return;
const resultEl = document.getElementById('host-result');
resultEl.innerHTML = '<span class="loading"></span> Calling host...';
log('info', `Calling host /hello/${name} endpoint`);
try {
const result = await invokeAddon('callHelloWorld', name);
resultEl.textContent = JSON.stringify(result, null, 2);
if (result.error) {
log('error', `Host error: ${result.error}`);
} else {
log('info', `Host response: ${result.message}`);
}
} catch (err) {
resultEl.textContent = 'Error: ' + err.message;
log('error', `Host call failed: ${err.message}`);
}
}
function updateHostStatus() {
// hostPort is received in addon:init-context
const portEl = document.getElementById('host-port');
if (hostPort) {
portEl.textContent = hostPort;
document.getElementById('host-available').textContent = 'Checking...';
} else {
portEl.textContent = 'Not available';
document.getElementById('host-available').textContent = 'No (No port)';
}
}
// ============================================================ // ============================================================
// LOGGING // LOGGING
// ============================================================ // ============================================================