git-contextor
Version:
A code context tool with vector search and real-time monitoring, with optional Git integration.
308 lines (253 loc) • 7.95 kB
JavaScript
const { spawn } = require('child_process');
const EventEmitter = require('events');
const path = require('path');
class CorrentlyTunnelProvider extends EventEmitter {
constructor(config) {
super();
this.config = config;
this.tunnelId = null;
this.tunnelUrl = null;
this.status = 'stopped';
this.tunnelClient = null;
this.tunnel = null;
this.localPort = null;
this.serverUrl = config.serverUrl || 'https://tunnel.corrently.cloud';
this.apiKey = config.apiKey;
}
async createTunnel(options = {}) {
if (this.status !== 'stopped') {
throw new Error(`Tunnel is already active with status: ${this.status}`);
}
if (!this.apiKey) {
throw new Error('API key is required for tunnel.corrently.cloud service');
}
this.status = 'starting';
try {
this.localPort = options.localPort || 3333;
const description = options.description || 'Git Contextor Share';
const subdomain = options.subdomain;
// Create tunnel via API - using the correct format from the docs
const requestBody = {
localPort: this.localPort,
description
};
// Add subdomain if provided (for paid plans)
if (subdomain) {
requestBody.subdomain = subdomain;
}
// Add Git Contextor share flag if needed
if (options.gitContextorShare) {
requestBody.gitContextorShare = true;
}
const response = await fetch(`${this.serverUrl}/api/tunnels`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`Tunnel creation failed: ${response.status} ${response.statusText} - ${errorData.message || 'Unknown error'}`);
}
this.tunnel = await response.json();
this.tunnelId = this.tunnel._id || this.tunnel.id;
this.tunnelUrl = this.tunnel.url;
// Check tunnel expiration
if (this.tunnel.expiresAt) {
const expiresAt = new Date(this.tunnel.expiresAt);
if (expiresAt < new Date()) {
this.status = 'error';
throw new Error(`Tunnel has expired at ${this.tunnel.expiresAt}`);
}
}
// Start tunnel client and verify connection
const clientConnected = await this.startTunnelClient();
if (!clientConnected) {
this.status = 'error';
throw new Error('Tunnel client failed to connect or forward requests');
}
this.status = 'running';
this.emit('tunnel-created', {
id: this.tunnelId,
url: this.tunnelUrl,
status: this.status
});
return {
id: this.tunnelId,
url: this.tunnelUrl,
status: this.status
};
} catch (error) {
this.status = 'error';
this.emit('tunnel-error', error);
throw error;
}
}
async startTunnelClient() {
if (!this.tunnel) {
throw new Error('Tunnel not created yet');
}
const tunnelClientPath = path.join(__dirname, '..', '..', 'tunnel-client.js');
this.tunnelClient = spawn('node', [
tunnelClientPath,
this.serverUrl,
this.tunnel.connectionId,
this.localPort.toString()
], {
stdio: ['ignore', 'pipe', 'pipe'],
detached: false
});
let clientConnected = false;
// Handle tunnel client output
this.tunnelClient.stdout.on('data', (data) => {
const output = data.toString();
console.log('Tunnel client:', output);
this.emit('tunnel-client-output', output);
if (output.includes('connected') || output.includes('ready')) {
clientConnected = true;
}
});
this.tunnelClient.stderr.on('data', (data) => {
const error = data.toString();
console.error('Tunnel client error:', error);
this.emit('tunnel-client-error', error);
});
this.tunnelClient.on('close', (code) => {
console.log(`Tunnel client exited with code ${code}`);
if (this.status === 'running') {
this.status = 'error';
this.emit('tunnel-disconnected', { code });
}
});
this.tunnelClient.on('error', (error) => {
console.error('Tunnel client spawn error:', error);
this.emit('tunnel-error', error);
});
// Wait for client to connect
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Tunnel client connection timeout'));
}, 10000);
const onOutput = (data) => {
if (data.includes('connected') || data.includes('ready')) {
clearTimeout(timeout);
this.tunnelClient.stdout.removeListener('data', onOutput);
resolve();
}
};
this.tunnelClient.stdout.on('data', onOutput);
// Also resolve after 5 seconds as fallback
setTimeout(() => {
clearTimeout(timeout);
this.tunnelClient.stdout.removeListener('data', onOutput);
resolve();
}, 5000);
});
return clientConnected;
}
async deleteTunnel() {
if (!this.tunnelId) {
return;
}
try {
await fetch(`${this.serverUrl}/api/tunnels/${this.tunnelId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${this.apiKey}`
}
});
} catch (error) {
console.error('Error deleting tunnel:', error);
}
}
async stopTunnel() {
this.status = 'stopping';
// Stop tunnel client
if (this.tunnelClient) {
this.tunnelClient.kill('SIGTERM');
// Force kill if it doesn't stop gracefully
setTimeout(() => {
if (this.tunnelClient && !this.tunnelClient.killed) {
this.tunnelClient.kill('SIGKILL');
}
}, 5000);
this.tunnelClient = null;
}
// Delete tunnel from service
await this.deleteTunnel();
// Reset state
this.tunnelId = null;
this.tunnelUrl = null;
this.tunnel = null;
this.status = 'stopped';
this.emit('tunnel-stopped');
}
getTunnelStatus() {
return {
status: this.status,
url: this.tunnelUrl,
service: 'corrently',
id: this.tunnelId
};
}
isRunning() {
return this.status === 'running';
}
async testConnection() {
if (!this.apiKey) {
throw new Error('API key is required');
}
try {
const response = await fetch(`${this.serverUrl}/api/health`, {
headers: {
'Authorization': `Bearer ${this.apiKey}`
}
});
if (!response.ok) {
throw new Error(`Health check failed: ${response.status}`);
}
return await response.json();
} catch (error) {
throw new Error(`Connection test failed: ${error.message}`);
}
}
async getUserInfo() {
if (!this.apiKey) {
throw new Error('API key is required');
}
try {
const response = await fetch(`${this.serverUrl}/api/auth/me`, {
headers: {
'Authorization': `Bearer ${this.apiKey}`
}
});
if (!response.ok) {
throw new Error(`User info failed: ${response.status}`);
}
return await response.json();
} catch (error) {
throw new Error(`Failed to get user info: ${error.message}`);
}
}
async listTunnels() {
if (!this.apiKey) {
throw new Error('API key is required');
}
try {
const response = await fetch(`${this.serverUrl}/api/tunnels`, {
headers: {
'Authorization': `Bearer ${this.apiKey}`
}
});
if (!response.ok) {
throw new Error(`List tunnels failed: ${response.status}`);
}
return await response.json();
} catch (error) {
throw new Error(`Failed to list tunnels: ${error.message}`);
}
}
}
module.exports = CorrentlyTunnelProvider;