fix: add script invocation of opencode bash

This commit is contained in:
Gautam Tayal 2026-03-28 14:51:35 +05:30
parent b1b32adb0f
commit 9a09d8be25
7 changed files with 79 additions and 22 deletions

@ -0,0 +1 @@
Subproject commit 754ac957381d4b32ed245f4e4346999f4ca1b5f8

1
packages/cli/legal-ai Submodule

@ -0,0 +1 @@
Subproject commit 866c056d82fdaea3786812fd42af5746e743741f

@ -0,0 +1 @@
Subproject commit b052b6af3c230ec50be57f6d18e70149ceacaf7e

@ -0,0 +1 @@
Subproject commit 59044e9ffe4681192a264d29816119c62126693f

View File

@ -64,8 +64,8 @@ function parseSessionList(raw: string): OpenCodeSessionListEntry[] {
/**
* Parse JSON stream lines from `opencode run --format json` output.
* Each line is a JSON object. We look for objects containing a session_id field.
* The step_start event typically contains the session_id.
* Each line is a JSON object. Different OpenCode versions emit either
* `session_id` or `sessionID`, so accept both.
*/
function buildSessionIdCaptureScript(): string {
const script = `
@ -81,8 +81,9 @@ process.stdin.on('data', chunk => {
if (!trimmed) continue;
try {
const obj = JSON.parse(trimmed);
if (obj && typeof obj.session_id === 'string' && /^ses_[A-Za-z0-9_-]+$/.test(obj.session_id)) {
captured = obj.session_id;
const candidate = obj?.session_id ?? obj?.sessionID;
if (typeof candidate === 'string' && /^ses_[A-Za-z0-9_-]+$/.test(candidate)) {
captured = candidate;
}
} catch {}
}
@ -90,8 +91,9 @@ process.stdin.on('data', chunk => {
if (buffer.trim()) {
try {
const obj = JSON.parse(buffer.trim());
if (obj && typeof obj.session_id === 'string' && /^ses_[A-Za-z0-9_-]+$/.test(obj.session_id)) {
captured = obj.session_id;
const candidate = obj?.session_id ?? obj?.sessionID;
if (typeof candidate === 'string' && /^ses_[A-Za-z0-9_-]+$/.test(candidate)) {
captured = candidate;
}
} catch {}
}
@ -204,7 +206,12 @@ function createOpenCodeAgent(): Agent {
];
const captureScript = buildSessionIdCaptureScript();
const fallbackScript = buildSessionLookupScript();
const runCommand = ["opencode", "run", ...runOptions, "--command", "true"].join(" ");
const runCommand = [
"opencode",
"run",
...runOptions,
shellEscape("Create a session and reply with exactly: READY"),
].join(" ");
const resumeOptions = [...(promptValue ? ["--prompt", promptValue] : []), ...sharedOptions];
const resumeOptionsSuffix = resumeOptions.length > 0 ? ` ${resumeOptions.join(" ")}` : "";
const missingSessionError = shellEscape(

View File

@ -148,6 +148,48 @@ describe("runtime.create()", () => {
);
});
it("uses a temp launch script for long launch commands", async () => {
const runtime = create();
const longCommand = "x".repeat(250);
mockTmuxSuccess();
mockTmuxSuccess();
mockTmuxSuccess();
await runtime.create({
sessionId: "launch-long",
workspacePath: "/tmp/ws",
launchCommand: longCommand,
environment: {},
});
expect(fs.writeFileSync).toHaveBeenCalledWith(
expect.stringContaining("ao-launch-test-uuid-1234.sh"),
expect.stringContaining(longCommand),
{ encoding: "utf-8", mode: 0o700 },
);
expect(mockExecFileCustom).toHaveBeenNthCalledWith(
2,
"tmux",
[
"send-keys",
"-t",
"launch-long",
"-l",
expect.stringContaining("bash "),
],
expectedTmuxOptions,
);
expect(mockExecFileCustom).toHaveBeenNthCalledWith(
3,
"tmux",
["send-keys", "-t", "launch-long", "Enter"],
expectedTmuxOptions,
);
});
it("cleans up session if send-keys fails", async () => {
const runtime = create();

View File

@ -33,6 +33,21 @@ function assertValidSessionId(id: string): void {
}
}
function shellQuote(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`;
}
function writeLaunchScript(command: string): string {
const scriptPath = join(tmpdir(), `ao-launch-${randomUUID()}.sh`);
const content = `#!/usr/bin/env bash
rm -- "$0" 2>/dev/null || true
${command}
`;
writeFileSync(scriptPath, content, { encoding: "utf-8", mode: 0o700 });
const invocation = `bash ${shellQuote(scriptPath)}`;
return invocation
}
/** Run a tmux command and return stdout */
async function tmux(...args: string[]): Promise<string> {
const { stdout } = await execFileAsync("tmux", args, {
@ -59,23 +74,12 @@ export function create(): Runtime {
await tmux("new-session", "-d", "-s", sessionName, "-c", config.workspacePath, ...envArgs);
// Send the launch command — clean up the session if this fails.
// Use load-buffer + paste-buffer for long commands to avoid tmux/zsh
// truncation issues (commands >200 chars get mangled by send-keys).
// Use a temp script for long commands so the pane shows a short
// invocation instead of a pasted wall of shell.
try {
if (config.launchCommand.length > 200) {
const bufferName = `ao-launch-${randomUUID().slice(0, 8)}`;
const tmpPath = join(tmpdir(), `ao-launch-${randomUUID()}.txt`);
writeFileSync(tmpPath, config.launchCommand, { encoding: "utf-8", mode: 0o600 });
try {
await tmux("load-buffer", "-b", bufferName, tmpPath);
await tmux("paste-buffer", "-b", bufferName, "-t", sessionName, "-d");
} finally {
try {
unlinkSync(tmpPath);
} catch {
/* ignore cleanup errors */
}
}
const invocation = writeLaunchScript(config.launchCommand);
await tmux("send-keys", "-t", sessionName, "-l", invocation);
await sleep(300);
await tmux("send-keys", "-t", sessionName, "Enter");
} else {