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:
@@ -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();
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import {
|
||||
openCodeBridgeToolNames,
|
||||
renderOpenCodeBridgeConfigContent,
|
||||
renderOpenCodeBridgeToolFile,
|
||||
renderOpenCodeBridgeToolFiles,
|
||||
} from '../packages/opencode-bridge/ts/index.js';
|
||||
|
||||
tap.test('should render managed OpenCode bridge config', async () => {
|
||||
const config = JSON.parse(renderOpenCodeBridgeConfigContent());
|
||||
|
||||
expect(config.snapshot).toEqual(false);
|
||||
expect(config.autoupdate).toEqual(false);
|
||||
expect(config.permission.lsp).toEqual('deny');
|
||||
expect(config.permission.skill).toEqual('deny');
|
||||
});
|
||||
|
||||
tap.test('should render tool overrides for remote bridge', async () => {
|
||||
const files = renderOpenCodeBridgeToolFiles();
|
||||
|
||||
for (const toolName of openCodeBridgeToolNames) {
|
||||
expect(files[`tools/${toolName}.js`]).toInclude(`forwardTool(${JSON.stringify(toolName)}`);
|
||||
}
|
||||
expect(files['tools/bash.js']).toInclude('GITZONE_IDE_TOOL_BRIDGE_URL');
|
||||
expect(files['tools/apply_patch.js']).toInclude('patchText');
|
||||
});
|
||||
|
||||
tap.test('should allow custom bridge environment names', async () => {
|
||||
const toolFile = renderOpenCodeBridgeToolFile('read', {
|
||||
bridgeUrlEnvName: 'CUSTOM_BRIDGE_URL',
|
||||
bridgeTokenEnvName: 'CUSTOM_BRIDGE_TOKEN',
|
||||
});
|
||||
|
||||
expect(toolFile).toInclude('CUSTOM_BRIDGE_URL');
|
||||
expect(toolFile).toInclude('CUSTOM_BRIDGE_TOKEN');
|
||||
expect(toolFile).toInclude('filePath');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -8,7 +8,9 @@ tap.test('should keep Theia backend config under Git.Zone IDE home path', async
|
||||
const packageJsonPath = path.join(process.cwd(), 'applications', 'remote-theia', 'package.json');
|
||||
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
||||
|
||||
expect(packageJson.theia.backend.config.singleInstance).toEqual(false);
|
||||
expect(packageJson.theia.backend.config.configurationFolder).toEqual('.git.zone/ide/theia');
|
||||
expect(packageJson.theia.frontend.config.defaultTheme).toEqual('dark');
|
||||
});
|
||||
|
||||
tap.test('should avoid legacy .theia workspace preference folders', async () => {
|
||||
|
||||
Reference in New Issue
Block a user