Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/small-apes-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@calycode/cli': patch
---

Scope OpenCode server and native-host child processes to a dedicated CalyCode workspace directory while preserving project-relative cwd for direct CLI proxy usage.
53 changes: 50 additions & 3 deletions packages/cli/src/commands/opencode/implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ const OPENCODE_PKG = 'opencode-ai@latest';
function getSpawnOptions(
stdio: 'inherit' | 'pipe' | 'ignore' = 'inherit',
extraEnv?: Record<string, string>,
cwd?: string,
) {
// On Windows, npx is a batch file and requires shell: true
// On Unix, we can run without shell for better security
const isWindows = process.platform === 'win32';
return {
stdio,
shell: isWindows,
cwd,
env: extraEnv ? { ...process.env, ...extraEnv } : process.env,
};
}
Expand Down Expand Up @@ -187,6 +189,45 @@ function getCalycodeOpencodeConfigDir(): string {
return path.join(os.homedir(), '.calycode', 'opencode');
}

/**
* Get the scoped workspace directory used by OpenCode server/native host processes.
* This limits default execution scope for background/browser-triggered runs.
*/
function getCalycodeOpencodeWorkspaceDir(): string {
return path.join(getCalycodeOpencodeConfigDir(), 'workspace');
}

function ensureDirectoryExists(dirPath: string): void {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}

/**
* Resolve the working directory for OpenCode child processes.
*
* Priority:
* 1. CALY_OPENCODE_WORKDIR env var (absolute or relative path)
* 2. mode='proxy': current shell cwd (user project context)
* 3. mode='server': ~/.calycode/opencode/workspace (scoped sandbox)
*/
function getOpencodeWorkingDir(mode: 'proxy' | 'server'): string {
const envWorkdir = process.env.CALY_OPENCODE_WORKDIR?.trim();
if (envWorkdir) {
const resolvedPath = path.resolve(envWorkdir);
ensureDirectoryExists(resolvedPath);
return resolvedPath;
}

if (mode === 'proxy') {
return process.cwd();
}

const workspaceDir = getCalycodeOpencodeWorkspaceDir();
ensureDirectoryExists(workspaceDir);
return workspaceDir;
}

/**
* Get the base allowed CORS origins for the OpenCode server.
*
Expand Down Expand Up @@ -233,12 +274,14 @@ async function proxyOpencode(args: string[]) {

// Set the CalyCode OpenCode config directory
const configDir = getCalycodeOpencodeConfigDir();
const workingDir = getOpencodeWorkingDir('proxy');
log.info(`OpenCode working directory: ${workingDir}`);

return new Promise<void>((resolve, reject) => {
// Use 'npx' to execute the opencode-ai CLI with the provided arguments
// Set OPENCODE_CONFIG_DIR to use our custom config without polluting user's global config
const proc = spawn('npx', ['-y', OPENCODE_PKG, ...args], {
...getSpawnOptions('inherit', { OPENCODE_CONFIG_DIR: configDir }),
...getSpawnOptions('inherit', { OPENCODE_CONFIG_DIR: configDir }, workingDir),
});

proc.on('close', (code) => {
Expand Down Expand Up @@ -453,10 +496,12 @@ async function startNativeHost() {

// Set OPENCODE_CONFIG_DIR to use CalyCode-specific config
const configDir = getCalycodeOpencodeConfigDir();
const workingDir = getOpencodeWorkingDir('server');
logger.log(`Using OpenCode config directory: ${configDir}`);
logger.log(`Using OpenCode working directory: ${workingDir}`);

serverProc = spawn('npx', args, {
...getSpawnOptions('ignore', { OPENCODE_CONFIG_DIR: configDir }),
...getSpawnOptions('ignore', { OPENCODE_CONFIG_DIR: configDir }, workingDir),
});

serverProc.on('error', (err) => {
Expand Down Expand Up @@ -1199,6 +1244,7 @@ async function serveOpencode({ port = 4096, detach = false }: { port?: number; d

// Set the CalyCode OpenCode config directory
const configDir = getCalycodeOpencodeConfigDir();
const workingDir = getOpencodeWorkingDir('server');

// On Windows, npx is a batch file and requires shell: true
const isWindows = process.platform === 'win32';
Expand All @@ -1216,6 +1262,7 @@ async function serveOpencode({ port = 4096, detach = false }: { port?: number; d
...process.env,
OPENCODE_CONFIG_DIR: configDir,
},
cwd: workingDir,
},
);
proc.unref();
Expand All @@ -1230,7 +1277,7 @@ async function serveOpencode({ port = 4096, detach = false }: { port?: number; d
'npx',
['-y', OPENCODE_PKG, 'serve', '--port', String(port), ...getCorsArgs()],
{
...getSpawnOptions('inherit', { OPENCODE_CONFIG_DIR: configDir }),
...getSpawnOptions('inherit', { OPENCODE_CONFIG_DIR: configDir }, workingDir),
},
);

Expand Down