mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-26 15:07:39 +00:00
feat: require unique --name parameter for remote servers
- Add mandatory --name parameter when using --hq-url - Ensure remote server names are unique across the HQ - Return 409 Conflict when duplicate name is registered - Track remotes by both ID and name in RemoteRegistry - Add /api/remotes endpoints for HQ mode - Improve error messages for registration failures 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
8bb22f291f
commit
862947b744
3 changed files with 89 additions and 6 deletions
|
|
@ -1,5 +1,4 @@
|
|||
import { v4 as uuidv4 } from 'uuid';
|
||||
import * as os from 'os';
|
||||
|
||||
export class HQClient {
|
||||
private readonly hqUrl: string;
|
||||
|
|
@ -10,10 +9,10 @@ export class HQClient {
|
|||
private readonly hqPassword: string;
|
||||
private registrationRetryTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(hqUrl: string, hqUsername: string, hqPassword: string) {
|
||||
constructor(hqUrl: string, hqUsername: string, hqPassword: string, remoteName: string) {
|
||||
this.hqUrl = hqUrl;
|
||||
this.remoteId = uuidv4();
|
||||
this.remoteName = `${os.hostname()}-${process.pid}`;
|
||||
this.remoteName = remoteName;
|
||||
this.token = uuidv4();
|
||||
this.hqUsername = hqUsername;
|
||||
this.hqPassword = hqPassword;
|
||||
|
|
@ -36,11 +35,13 @@ export class HQClient {
|
|||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Registration failed: ${response.statusText}`);
|
||||
const errorBody = await response.json().catch(() => ({ error: response.statusText }));
|
||||
throw new Error(`Registration failed: ${errorBody.error || response.statusText}`);
|
||||
}
|
||||
|
||||
console.log(`Successfully registered with HQ at ${this.hqUrl}`);
|
||||
console.log(`Remote ID: ${this.remoteId}`);
|
||||
console.log(`Remote name: ${this.remoteName}`);
|
||||
console.log(`Token: ${this.token}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to register with HQ:', error);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface RemoteServer {
|
|||
|
||||
export class RemoteRegistry {
|
||||
private remotes: Map<string, RemoteServer> = new Map();
|
||||
private remotesByName: Map<string, RemoteServer> = new Map();
|
||||
private healthCheckInterval: NodeJS.Timeout | null = null;
|
||||
private readonly HEALTH_CHECK_INTERVAL = 15000; // Check every 15 seconds
|
||||
private readonly HEALTH_CHECK_TIMEOUT = 5000; // 5 second timeout per check
|
||||
|
|
@ -20,6 +21,11 @@ export class RemoteRegistry {
|
|||
}
|
||||
|
||||
register(remote: Omit<RemoteServer, 'registeredAt' | 'lastHeartbeat' | 'status'>): RemoteServer {
|
||||
// Check if a remote with the same name already exists
|
||||
if (this.remotesByName.has(remote.name)) {
|
||||
throw new Error(`Remote with name '${remote.name}' is already registered`);
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const registeredRemote: RemoteServer = {
|
||||
...remote,
|
||||
|
|
@ -29,6 +35,7 @@ export class RemoteRegistry {
|
|||
};
|
||||
|
||||
this.remotes.set(remote.id, registeredRemote);
|
||||
this.remotesByName.set(remote.name, registeredRemote);
|
||||
console.log(`Remote registered: ${remote.name} (${remote.id}) from ${remote.url}`);
|
||||
|
||||
// Immediately check health of new remote
|
||||
|
|
@ -41,6 +48,7 @@ export class RemoteRegistry {
|
|||
const remote = this.remotes.get(remoteId);
|
||||
if (remote) {
|
||||
console.log(`Remote unregistered: ${remote.name} (${remoteId})`);
|
||||
this.remotesByName.delete(remote.name);
|
||||
return this.remotes.delete(remoteId);
|
||||
}
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ const app = express();
|
|||
const server = createServer(app);
|
||||
const wss = new WebSocketServer({ server });
|
||||
|
||||
// Add JSON body parser middleware
|
||||
app.use(express.json());
|
||||
|
||||
const PORT = process.env.PORT || 4020;
|
||||
|
||||
// ANSI color codes
|
||||
|
|
@ -33,6 +36,7 @@ let isHQMode = false;
|
|||
let hqUrl: string | null = null;
|
||||
let hqUsername: string | null = null;
|
||||
let hqPassword: string | null = null;
|
||||
let remoteName: string | null = null;
|
||||
|
||||
// Check for command line arguments
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
|
|
@ -53,6 +57,9 @@ for (let i = 0; i < args.length; i++) {
|
|||
} else if (args[i] === '--hq-password' && i + 1 < args.length) {
|
||||
hqPassword = args[i + 1];
|
||||
i++; // Skip the password value in next iteration
|
||||
} else if (args[i] === '--name' && i + 1 < args.length) {
|
||||
remoteName = args[i + 1];
|
||||
i++; // Skip the name value in next iteration
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -84,6 +91,13 @@ if (hqUrl && (!hqUsername || !hqPassword)) {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate remote name is provided when registering with HQ
|
||||
if (hqUrl && !remoteName) {
|
||||
console.error(`${RED}ERROR: Remote name required when --hq-url is specified${RESET}`);
|
||||
console.error('Use --name to specify a unique name for this remote server');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate HQ URL
|
||||
if (hqUrl) {
|
||||
try {
|
||||
|
|
@ -153,9 +167,10 @@ if (isHQMode) {
|
|||
console.log(`${GREEN}Running in HQ mode${RESET}`);
|
||||
}
|
||||
|
||||
if (hqUrl && hqUsername && hqPassword) {
|
||||
hqClient = new HQClient(hqUrl, hqUsername, hqPassword);
|
||||
if (hqUrl && hqUsername && hqPassword && remoteName) {
|
||||
hqClient = new HQClient(hqUrl, hqUsername, hqPassword, remoteName);
|
||||
console.log(`${GREEN}Will register with HQ at: ${hqUrl}${RESET}`);
|
||||
console.log(`${GREEN}Remote name: ${remoteName}${RESET}`);
|
||||
}
|
||||
|
||||
// Ensure control directory exists
|
||||
|
|
@ -248,6 +263,65 @@ app.use(express.static(path.join(__dirname, '..', 'public')));
|
|||
// Hot reload functionality for development
|
||||
const hotReloadClients = new Set<WebSocket>();
|
||||
|
||||
// === HQ/REMOTE MANAGEMENT ===
|
||||
|
||||
// Register a remote server (HQ mode only)
|
||||
app.post('/api/remotes/register', (req, res) => {
|
||||
if (!isHQMode || !remoteRegistry) {
|
||||
return res.status(403).json({ error: 'This endpoint is only available in HQ mode' });
|
||||
}
|
||||
|
||||
const { id, name, url, token } = req.body;
|
||||
|
||||
if (!id || !name || !url || !token) {
|
||||
return res.status(400).json({ error: 'Missing required fields: id, name, url, token' });
|
||||
}
|
||||
|
||||
try {
|
||||
const remote = remoteRegistry.register({
|
||||
id,
|
||||
name,
|
||||
url,
|
||||
token,
|
||||
sessionCount: 0,
|
||||
});
|
||||
|
||||
res.json({ success: true, remote });
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('already registered')) {
|
||||
return res.status(409).json({ error: error.message });
|
||||
}
|
||||
console.error('Failed to register remote:', error);
|
||||
res.status(500).json({ error: 'Failed to register remote' });
|
||||
}
|
||||
});
|
||||
|
||||
// Unregister a remote server (HQ mode only)
|
||||
app.delete('/api/remotes/:id', (req, res) => {
|
||||
if (!isHQMode || !remoteRegistry) {
|
||||
return res.status(403).json({ error: 'This endpoint is only available in HQ mode' });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const success = remoteRegistry.unregister(id);
|
||||
|
||||
if (success) {
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
res.status(404).json({ error: 'Remote not found' });
|
||||
}
|
||||
});
|
||||
|
||||
// List all remote servers (HQ mode only)
|
||||
app.get('/api/remotes', (req, res) => {
|
||||
if (!isHQMode || !remoteRegistry) {
|
||||
return res.status(403).json({ error: 'This endpoint is only available in HQ mode' });
|
||||
}
|
||||
|
||||
const remotes = remoteRegistry.getAllRemotes();
|
||||
res.json(remotes);
|
||||
});
|
||||
|
||||
// === SESSION MANAGEMENT ===
|
||||
|
||||
// List all sessions
|
||||
|
|
|
|||
Loading…
Reference in a new issue