Support remote project tabs with local OpenCode bridge

Keeps provider credentials local while executing OpenCode shell and file tools against the selected remote workspace over SSH.
This commit is contained in:
2026-05-11 14:28:12 +00:00
parent 1ccf2fb1cf
commit 6f32a206b4
18 changed files with 1793 additions and 194 deletions
+95
View File
@@ -1,9 +1,18 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as childProcess from 'node:child_process';
import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';
import {
createRemoteEphemeralBootstrapCommand,
createRemoteEphemeralReadinessCommand,
createRemoteEphemeralPortAllocationCommand,
createRemoteEphemeralRuntimeCacheCheckCommand,
createRemoteEphemeralRuntimeMarkCommand,
createRemoteOpenCodeToolCommand,
createRemoteProjectListCommand,
createRemoteProjectUpsertCommand,
remoteOpenCodeToolScript,
createRemoteBootstrapCommand,
createRemoteInstallCommand,
createRemoteServerInstallPlan,
@@ -51,6 +60,7 @@ tap.test('should render install and bootstrap commands', async () => {
expect(installCommand).toInclude('GITZONE_IDE_MANIFEST');
expect(installCommand).toInclude('"$HOME"/\'.git.zone/ide/server/0.1.0\'');
expect(bootstrapCommand).toInclude('GITZONE_IDE_OPENCODE_PORT');
expect(bootstrapCommand).toInclude('GITZONE_IDE_DISABLE_OPENCODE_AUTOSTART');
expect(bootstrapCommand).toInclude('pnpm --dir');
});
@@ -85,6 +95,9 @@ tap.test('should render ephemeral runtime bootstrap without remote pnpm', async
expect(bootstrapCommand).not.toInclude('pnpm');
expect(bootstrapCommand).toInclude('LD_LIBRARY_PATH');
expect(bootstrapCommand).toInclude('THEIA_CONFIG_DIR="$HOME"/\'.git.zone/ide/theia\'');
expect(bootstrapCommand).toInclude('GITZONE_IDE_DISABLE_OPENCODE_AUTOSTART=\'1\'');
expect(bootstrapCommand).toInclude('GITZONE_IDE_THEIA_COLOR_THEME=\'dark\'');
expect(bootstrapCommand).toInclude("settings['workbench.colorTheme'] = colorTheme");
expect(bootstrapCommand).toInclude('"$HOME"/\'.git.zone/ide/logs\'');
expect(bootstrapCommand).toInclude('runtimeRoot=');
});
@@ -126,4 +139,86 @@ tap.test('should render ephemeral runtime mark command', async () => {
expect(markCommand).toInclude('runtimeCache=stored');
});
tap.test('should render remote port allocation command', async () => {
const portCommand = createRemoteEphemeralPortAllocationCommand({
runtimeRoot: '/tmp/gitzone-ide-runtime-test',
count: 2,
});
expect(portCommand).toInclude('/tmp/gitzone-ide-runtime-test/node/bin/node');
expect(portCommand).toInclude('ports=');
expect(portCommand).toInclude('LD_LIBRARY_PATH');
});
tap.test('should render remote OpenCode tool bridge command', async () => {
const command = createRemoteOpenCodeToolCommand({
runtimeRoot: '/tmp/gitzone-ide-runtime-test',
workspacePath: '$HOME/project',
toolName: 'read',
});
expect(command).toInclude('/tmp/gitzone-ide-runtime-test/node/bin/node');
expect(command).toInclude('GITZONE_IDE_TOOL_NAME=\'read\'');
expect(command).toInclude('GITZONE_IDE_WORKSPACE="$HOME"/\'project\'');
expect(command).toInclude('GITZONE_IDE_RG_PATH');
expect(command).toInclude('fs.readFileSync(0');
});
tap.test('should execute remote OpenCode tool script with stdin payloads', async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gitzone-opencode-tool-'));
const filePath = path.join(tempDir, 'sample.txt');
await fs.writeFile(filePath, 'hello\nworld\n');
const readResult = runRemoteOpenCodeToolScript('read', tempDir, { filePath: 'sample.txt' });
expect(readResult.output).toInclude('1: hello');
const editResult = runRemoteOpenCodeToolScript('edit', tempDir, {
filePath: 'sample.txt',
oldString: 'world',
newString: 'remote',
});
expect(editResult.output).toInclude('Edit applied successfully');
expect(await fs.readFile(filePath, 'utf8')).toEqual('hello\nremote\n');
const patchResult = runRemoteOpenCodeToolScript('apply_patch', tempDir, {
patchText: '*** Begin Patch\n*** Add File: nested/new.txt\n+created remotely\n*** End Patch',
});
expect(patchResult.output).toInclude('A nested/new.txt');
expect(await fs.readFile(path.join(tempDir, 'nested', 'new.txt'), 'utf8')).toEqual('created remotely\n');
});
tap.test('should render remote project registry commands', async () => {
const listCommand = createRemoteProjectListCommand({
runtimeRoot: '/tmp/gitzone-ide-runtime-test',
});
const upsertCommand = createRemoteProjectUpsertCommand({
runtimeRoot: '/tmp/gitzone-ide-runtime-test',
projectPath: '$HOME/project',
title: 'Project',
});
expect(listCommand).toInclude('projects.json');
expect(listCommand).toInclude('{"projects":[]}');
expect(upsertCommand).toInclude('GITZONE_IDE_PROJECT_PATH="$HOME"/\'project\'');
expect(upsertCommand).toInclude('test -d "$GITZONE_IDE_PROJECT_PATH"');
expect(upsertCommand).toInclude('crypto.createHash');
});
const runRemoteOpenCodeToolScript = (toolName: string, workspacePath: string, args: Record<string, unknown>) => {
const result = childProcess.spawnSync(process.execPath, ['-e', remoteOpenCodeToolScript], {
input: JSON.stringify({ args }),
encoding: 'utf8',
env: {
...process.env,
GITZONE_IDE_TOOL_NAME: toolName,
GITZONE_IDE_WORKSPACE: workspacePath,
GITZONE_IDE_RG_PATH: '/not-found/rg',
},
});
if (result.status !== 0) {
throw new Error(result.stderr || `tool script failed with ${result.status}`);
}
return JSON.parse(result.stdout) as { output: string; metadata?: Record<string, unknown> };
};
export default tap.start();