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:
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal 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
|
||||||
15
addon.json
15
addon.json
@@ -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": [
|
||||||
|
|||||||
11
host/MyFirstAddon.Host.csproj
Normal file
11
host/MyFirstAddon.Host.csproj
Normal 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
35
host/Program.cs
Normal 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();
|
||||||
@@ -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
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
104
views/demo.html
104
views/demo.html
@@ -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
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user