feat(ssh): rework remote instance setup and fix remote installs

Adding an SSH connection now starts from the hosts in the SSH config
instead of a blank command field, ports and install options sit behind
Advanced settings, and each connection reports one of three states with
the failure text and an action that resolves it.

Remote installs no longer touch the root-owned global npm prefix: npm is
pinned to a prefix under $HOME and bun is resolved at its known location,
because an SSH login shell exposes neither on PATH. The opencode CLI is
resolved the same way and handed to the remote server through
OPENCODE_BINARY, and the server is started and stopped through the
resolved binary rather than PATH — the HTTP shutdown route sits behind UI
authentication and never stopped anything.

A managed remote server can also be published to the remote machine's own
network. That requires a UI password, enforced in the form and again in
the SSH manager.
This commit is contained in:
Bohdan Triapitsyn
2026-08-22 16:45:56 +03:00
parent eed317f18d
commit f7a0d1f0f6
16 changed files with 1234 additions and 302 deletions
+157
View File
@@ -289,4 +289,161 @@ describe('ElectronSshManager', () => {
});
expect(settings.desktopHosts).toEqual([{ id: 'ssh-1', label: 'SSH Host', url: localUrl, apiUrl: localUrl, clientToken: 'ssh-client-token' }]);
});
test('installs OpenChamber into a home-owned npm prefix instead of the root-owned global one', async () => {
const commands = [];
const manager = new ElectronSshManager({
settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'),
appVersion: '1.2.3',
emit: () => undefined,
});
manager.resolveRemoteTool = async (_parsed, _controlPath, name) => (name === 'npm' ? '/usr/bin/npm' : null);
manager.runRemoteCommand = async (_parsed, _controlPath, script) => {
commands.push(script);
return '';
};
await manager.installOpenChamberManaged({ destination: 'user@example.test', args: [] }, '/tmp/control.sock', '1.2.3', 'auto');
expect(commands).toHaveLength(1);
expect(commands[0]).toContain('--prefix "$HOME/.openchamber/npm-global"');
expect(commands[0]).not.toMatch(/npm install -g @openchamber/);
});
test('lists every remote OpenChamber binary with its reported version', async () => {
const manager = new ElectronSshManager({
settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'),
appVersion: '1.2.3',
emit: () => undefined,
});
manager.runRemoteCommand = async () => [
'/home/pi/.openchamber/npm-global/bin/openchamber\t1.2.3',
'/usr/bin/openchamber\t0.9.0',
'',
].join('\n');
const candidates = await manager.remoteOpenChamberCandidates({ destination: 'user@example.test', args: [] }, '/tmp/control.sock');
expect(candidates).toEqual([
{ binPath: '/home/pi/.openchamber/npm-global/bin/openchamber', version: '1.2.3' },
{ binPath: '/usr/bin/openchamber', version: '0.9.0' },
]);
});
test('starts the resolved OpenChamber binary rather than whatever PATH exposes', async () => {
let started = '';
const manager = new ElectronSshManager({
settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'),
appVersion: '1.2.3',
emit: () => undefined,
});
manager.resolveRemoteTool = async () => '/home/pi/.opencode/bin/opencode';
manager.runRemoteCommand = async (_parsed, _controlPath, script) => {
started = script;
return '4321\n';
};
const instance = { id: 'ssh-1', auth: {}, remoteOpenchamber: { mode: 'managed' } };
const port = await manager.startRemoteServerManaged(
{ destination: 'user@example.test', args: [] },
'/tmp/control.sock',
instance,
4321,
'/home/pi/.openchamber/npm-global/bin/openchamber',
);
expect(port).toBe(4321);
expect(started).toContain("'/home/pi/.openchamber/npm-global/bin/openchamber' serve");
expect(started).toContain("OPENCODE_BINARY='/home/pi/.opencode/bin/opencode'");
expect(started).toContain('$HOME/.opencode/bin:');
});
test('refuses to start when the remote machine has no opencode CLI', async () => {
const manager = new ElectronSshManager({
settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'),
appVersion: '1.2.3',
emit: () => undefined,
});
manager.resolveRemoteTool = async () => null;
manager.runRemoteCommand = async () => {
throw new Error('should not start the server without a CLI');
};
await expect(manager.startRemoteServerManaged(
{ destination: 'user@example.test', args: [] },
'/tmp/control.sock',
{ id: 'ssh-1', auth: {}, remoteOpenchamber: { mode: 'managed' } },
4321,
'/home/pi/.bun/bin/openchamber',
)).rejects.toThrow(/opencode CLI is not installed/);
});
test('prefers a bun that only exists in the home directory over npm', async () => {
const commands = [];
const manager = new ElectronSshManager({
settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'),
appVersion: '1.2.3',
emit: () => undefined,
});
// A login shell over SSH does not put ~/.bun/bin on PATH.
manager.resolveRemoteTool = async (_parsed, _controlPath, name) =>
(name === 'bun' ? '/home/pi/.bun/bin/bun' : '/usr/bin/npm');
manager.runRemoteCommand = async (_parsed, _controlPath, script) => {
commands.push(script);
return '';
};
await manager.installOpenChamberManaged({ destination: 'user@example.test', args: [] }, '/tmp/control.sock', '1.2.3', 'auto');
expect(commands).toEqual(["'/home/pi/.bun/bin/bun' add -g @openchamber/web@1.2.3"]);
});
test('stops a remote server it started through the CLI, not the authenticated HTTP route', async () => {
const scripts = [];
const manager = new ElectronSshManager({
settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'),
appVersion: '1.2.3',
emit: () => undefined,
});
manager.runRemoteCommand = async (_parsed, _controlPath, script) => {
scripts.push(script);
return '';
};
await manager.stopRemoteServerBestEffort(
{ destination: 'user@example.test', args: [] },
'/tmp/control.sock',
41777,
'/home/pi/.bun/bin/openchamber',
);
expect(scripts).toEqual(["'/home/pi/.bun/bin/openchamber' stop --port 41777"]);
});
test('publishes the remote server to its network only with a UI password', async () => {
const manager = new ElectronSshManager({
settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'),
appVersion: '1.2.3',
emit: () => undefined,
});
manager.resolveRemoteTool = async () => '/home/pi/.opencode/bin/opencode';
let started = '';
manager.runRemoteCommand = async (_parsed, _controlPath, script) => {
started = script;
return '4321\n';
};
const parsed = { destination: 'user@example.test', args: [] };
const exposed = {
id: 'ssh-1',
auth: {},
remoteOpenchamber: { mode: 'managed', bindHost: '0.0.0.0' },
};
await expect(manager.startRemoteServerManaged(parsed, '/tmp/control.sock', exposed, 4321, '/bin/openchamber'))
.rejects.toThrow(/requires a UI password/);
const secured = {
...exposed,
auth: { openchamberPassword: { enabled: true, value: 'remote-secret', store: 'settings' } },
};
await manager.startRemoteServerManaged(parsed, '/tmp/control.sock', secured, 4321, '/bin/openchamber');
expect(started).toContain('--hostname 0.0.0.0');
});
});