feat: add remote host case domain

This commit is contained in:
Saqeb Akhter
2026-07-09 08:51:08 -04:00
committed by Aamer Akhter
parent 1fa88cd187
commit 3bf991d730
3 changed files with 176 additions and 0 deletions
+80
View File
@@ -0,0 +1,80 @@
import { existsSync, mkdirSync } from 'node:fs';
import fs from 'node:fs/promises';
import { join } from 'node:path';
import type { RemoteCase, RemoteCommandMode, RemoteHost, SessionMode, SessionRemote } from './types.js';
const REMOTE_HOSTS_FILE = 'remote-hosts.json';
const REMOTE_CASES_FILE = 'remote-cases.json';
export function remoteHostsPath(configDir: string): string {
return join(configDir, REMOTE_HOSTS_FILE);
}
export function remoteCasesPath(configDir: string): string {
return join(configDir, REMOTE_CASES_FILE);
}
async function readJsonArray<T>(path: string): Promise<T[]> {
try {
const raw = await fs.readFile(path, 'utf-8');
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? (parsed as T[]) : [];
} catch {
return [];
}
}
async function writeJsonArray<T>(configDir: string, path: string, value: T[]): Promise<void> {
if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
await fs.writeFile(path, JSON.stringify(value, null, 2));
}
export async function readRemoteHosts(configDir: string): Promise<RemoteHost[]> {
return readJsonArray<RemoteHost>(remoteHostsPath(configDir));
}
export async function writeRemoteHosts(configDir: string, hosts: RemoteHost[]): Promise<void> {
await writeJsonArray(configDir, remoteHostsPath(configDir), hosts);
}
export async function readRemoteCases(configDir: string): Promise<RemoteCase[]> {
return readJsonArray<RemoteCase>(remoteCasesPath(configDir));
}
export async function writeRemoteCases(configDir: string, cases: RemoteCase[]): Promise<void> {
await writeJsonArray(configDir, remoteCasesPath(configDir), cases);
}
export function defaultRemoteCommandForMode(mode: SessionMode): string {
const commands: Record<RemoteCommandMode, string> = {
shell: 'exec bash -l',
claude: 'exec claude',
opencode: 'exec opencode',
codex: 'exec codex',
gemini: 'exec gemini',
};
return commands[mode as RemoteCommandMode] || commands.shell;
}
export function remoteSshTarget(host: Pick<RemoteHost, 'username' | 'host'>): string {
return `${host.username}@${host.host}`;
}
export function remoteDisplayPath(
remote: Pick<SessionRemote, 'username' | 'host' | 'remotePath'> | { username: string; host: string; path: string }
): string {
const path = 'remotePath' in remote ? remote.remotePath : remote.path;
return `${remote.username}@${remote.host}:${path}`;
}
export function toSessionRemote(host: RemoteHost, remoteCase: RemoteCase): SessionRemote {
return {
hostId: host.id,
label: host.label,
host: host.host,
username: host.username,
port: host.port,
remotePath: remoteCase.remotePath,
commands: host.commands,
};
}
+30
View File
@@ -43,6 +43,34 @@ export type ClaudeMode = 'dangerously-skip-permissions' | 'normal' | 'allowedToo
/** Session mode: which CLI backend a session runs */
export type SessionMode = 'claude' | 'shell' | 'opencode' | 'codex' | 'gemini';
export type RemoteCommandMode = Extract<SessionMode, 'shell' | 'claude' | 'opencode' | 'codex' | 'gemini'>;
export interface RemoteHost {
id: string;
label: string;
host: string;
username: string;
port?: number;
commands?: Partial<Record<RemoteCommandMode, string>>;
}
export interface RemoteCase {
name: string;
type: 'remote';
hostId: string;
remotePath: string;
}
export interface SessionRemote {
hostId: string;
label: string;
host: string;
username: string;
port?: number;
remotePath: string;
commands?: Partial<Record<RemoteCommandMode, string>>;
}
/**
* Valid Claude CLI effort levels (claude >= 2.1.154).
* `ultracode` = xhigh effort + standing dynamic-workflow orchestration; it is a
@@ -160,6 +188,8 @@ export interface SessionState {
status: SessionStatus;
/** Working directory path */
workingDir: string;
/** Remote execution metadata, present when this session runs over SSH through local tmux */
remote?: SessionRemote;
/** ID of currently assigned task, null if none */
currentTaskId: string | null;
/** Timestamp when session was created */
+66
View File
@@ -0,0 +1,66 @@
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import {
defaultRemoteCommandForMode,
readRemoteCases,
readRemoteHosts,
remoteDisplayPath,
remoteSshTarget,
writeRemoteCases,
writeRemoteHosts,
} from '../src/remote-hosts.js';
describe('remote-hosts domain', () => {
let dir: string | null = null;
afterEach(() => {
if (dir) rmSync(dir, { recursive: true, force: true });
dir = null;
});
function configDir(): string {
dir = mkdtempSync(join(tmpdir(), 'codeman-remote-hosts-'));
return dir;
}
it('round-trips remote hosts and remote cases from a config directory', async () => {
const root = configDir();
await writeRemoteHosts(root, [
{
id: 'gpu-box',
label: 'GPU Box',
host: '10.0.0.42',
username: 'ubuntu',
commands: { codex: 'exec codx personal' },
},
]);
await writeRemoteCases(root, [
{ name: 'gpu-work', type: 'remote', hostId: 'gpu-box', remotePath: '/home/ubuntu/work' },
]);
await expect(readRemoteHosts(root)).resolves.toEqual([
{
id: 'gpu-box',
label: 'GPU Box',
host: '10.0.0.42',
username: 'ubuntu',
commands: { codex: 'exec codx personal' },
},
]);
await expect(readRemoteCases(root)).resolves.toEqual([
{ name: 'gpu-work', type: 'remote', hostId: 'gpu-box', remotePath: '/home/ubuntu/work' },
]);
});
it('returns safe mode defaults and remote display values', () => {
expect(defaultRemoteCommandForMode('shell')).toBe('exec bash -l');
expect(defaultRemoteCommandForMode('codex')).toBe('exec codex');
expect(defaultRemoteCommandForMode('claude')).toBe('exec claude');
expect(remoteSshTarget({ id: 'h1', label: 'H1', host: 'box.local', username: 'aamer' })).toBe('aamer@box.local');
expect(remoteDisplayPath({ username: 'aamer', host: 'box.local', path: '/opt/work' })).toBe(
'aamer@box.local:/opt/work'
);
});
});