Refactor command builders (#601)

* refactor command builders

* remove log

* codex

* cursor model

* consistent cmd overrides

* shared types

* default for CmdOverrides
This commit is contained in:
Louis Knight-Webb
2025-09-02 11:20:04 +01:00
committed by GitHub
parent a8515d788e
commit 0bf8138742
11 changed files with 107 additions and 60 deletions

View File

@@ -42,7 +42,7 @@
"CODEX": {
"DEFAULT": {
"CODEX": {
"dangerously_bypass_approvals_and_sandbox": true
"sandbox": "danger-full-access"
}
}
},

View File

@@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
use ts_rs::TS;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, Default)]
pub struct CmdOverrides {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_command_override: Option<String>,

View File

@@ -32,7 +32,7 @@ impl Amp {
let mut builder = CommandBuilder::new("npx -y @sourcegraph/amp@latest")
.params(["--execute", "--stream-json"]);
if self.dangerously_allow_all.unwrap_or(false) {
builder = builder.params(["--dangerously-allow-all"]);
builder = builder.extend_params(["--dangerously-allow-all"]);
}
apply_overrides(builder, &self.cmd)
}

View File

@@ -32,23 +32,6 @@ fn base_command(claude_code_router: bool) -> &'static str {
}
}
fn build_command_builder(
claude_code_router: bool,
plan: bool,
dangerously_skip_permissions: bool,
) -> CommandBuilder {
let mut params: Vec<&'static str> = vec!["-p"];
if plan {
params.push("--permission-mode=plan");
}
if dangerously_skip_permissions {
params.push("--dangerously-skip-permissions");
}
params.extend_from_slice(&["--verbose", "--output-format=stream-json"]);
CommandBuilder::new(base_command(claude_code_router)).params(params)
}
/// An executor that uses Claude CLI to process tasks
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
pub struct ClaudeCode {
@@ -73,14 +56,19 @@ impl ClaudeCode {
);
}
apply_overrides(
build_command_builder(
self.claude_code_router.unwrap_or(false),
self.plan.unwrap_or(false),
self.dangerously_skip_permissions.unwrap_or(false),
),
&self.cmd,
)
let mut builder =
CommandBuilder::new(base_command(self.claude_code_router.unwrap_or(false)))
.params(["-p"]);
if self.plan.unwrap_or(false) {
builder = builder.extend_params(["--permission-mode=plan"]);
}
if self.dangerously_skip_permissions.unwrap_or(false) {
builder = builder.extend_params(["--dangerously-skip-permissions"]);
}
builder = builder.extend_params(["--verbose", "--output-format=stream-json"]);
apply_overrides(builder, &self.cmd)
}
}

View File

@@ -5,6 +5,7 @@ use command_group::{AsyncCommandGroup, AsyncGroupChild};
use futures::StreamExt;
use regex::Regex;
use serde::{Deserialize, Serialize};
use strum_macros::AsRefStr;
use tokio::{io::AsyncWriteExt, process::Command};
use ts_rs::TS;
use utils::{
@@ -15,7 +16,7 @@ use utils::{
};
use crate::{
command::CommandBuilder,
command::{CmdOverrides, CommandBuilder, apply_overrides},
executors::{ExecutorError, StandardCodingAgentExecutor},
logs::{
ActionType, FileChange, NormalizedEntry, NormalizedEntryType,
@@ -23,6 +24,16 @@ use crate::{
},
};
/// Sandbox policy modes for Codex
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, AsRefStr)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
pub enum SandboxMode {
ReadOnly,
WorkspaceWrite,
DangerFullAccess,
}
/// Handles session management for Codex executor
pub struct SessionHandler;
@@ -110,20 +121,33 @@ pub struct Codex {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub append_prompt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dangerously_bypass_approvals_and_sandbox: Option<bool>,
pub sandbox: Option<SandboxMode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub oss: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(flatten)]
pub cmd: CmdOverrides,
}
impl Codex {
fn build_command_builder(&self) -> CommandBuilder {
let mut builder = CommandBuilder::new("npx -y @openai/codex exec")
.params(["--json", "--skip-git-repo-check"]);
if self
.dangerously_bypass_approvals_and_sandbox
.unwrap_or(false)
{
builder = builder.params(["--dangerously-bypass-approvals-and-sandbox"]);
if let Some(sandbox) = &self.sandbox {
builder = builder.extend_params(["--sandbox", sandbox.as_ref()]);
}
builder
if self.oss.unwrap_or(false) {
builder = builder.extend_params(["--oss"]);
}
if let Some(model) = &self.model {
builder = builder.extend_params(["--model", model]);
}
apply_overrides(builder, &self.cmd)
}
}

View File

@@ -18,7 +18,7 @@ use utils::{
};
use crate::{
command::CommandBuilder,
command::{CmdOverrides, CommandBuilder, apply_overrides},
executors::{ExecutorError, StandardCodingAgentExecutor},
logs::{
ActionType, FileChange, NormalizedEntry, NormalizedEntryType, TodoItem,
@@ -34,16 +34,26 @@ pub struct Cursor {
pub append_prompt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub force: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(flatten)]
pub cmd: CmdOverrides,
}
impl Cursor {
fn build_command_builder(&self) -> CommandBuilder {
let mut builder =
CommandBuilder::new("cursor-agent").params(["-p", "--output-format=stream-json"]);
if self.force.unwrap_or(false) {
builder = builder.params(["--force"]);
builder = builder.extend_params(["--force"]);
}
builder
if let Some(model) = &self.model {
builder = builder.extend_params(["--model", model]);
}
apply_overrides(builder, &self.cmd)
}
}
@@ -1060,6 +1070,8 @@ mod tests {
// No command field needed anymore
append_prompt: None,
force: None,
model: None,
cmd: Default::default(),
};
let msg_store = Arc::new(MsgStore::new());
let current_dir = std::path::PathBuf::from("/tmp/test-worktree");

View File

@@ -35,17 +35,14 @@ impl GeminiModel {
"npx -y @google/gemini-cli@latest"
}
fn build_command_builder(&self, yolo: bool) -> CommandBuilder {
let mut params: Vec<&'static str> = vec![];
if yolo {
params.push("--yolo");
}
fn build_command_builder(&self) -> CommandBuilder {
let mut builder = CommandBuilder::new(self.base_command());
if let GeminiModel::Flash = self {
params.extend_from_slice(&["--model", "gemini-2.5-flash"]);
builder = builder.extend_params(["--model", "gemini-2.5-flash"]);
}
CommandBuilder::new(self.base_command()).params(params)
builder
}
}
@@ -63,10 +60,13 @@ pub struct Gemini {
impl Gemini {
fn build_command_builder(&self) -> CommandBuilder {
apply_overrides(
self.model.build_command_builder(self.yolo.unwrap_or(false)),
&self.cmd,
)
let mut builder = self.model.build_command_builder();
if self.yolo.unwrap_or(false) {
builder = builder.extend_params(["--yolo"]);
}
apply_overrides(builder, &self.cmd)
}
}

View File

@@ -15,7 +15,7 @@ use utils::{
};
use crate::{
command::CommandBuilder,
command::{CmdOverrides, CommandBuilder, apply_overrides},
executors::{ExecutorError, StandardCodingAgentExecutor},
logs::{
ActionType, FileChange, NormalizedEntry, NormalizedEntryType, TodoItem,
@@ -29,11 +29,28 @@ use crate::{
pub struct Opencode {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub append_prompt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent: Option<String>,
#[serde(flatten)]
pub cmd: CmdOverrides,
}
impl Opencode {
fn build_command_builder(&self) -> CommandBuilder {
CommandBuilder::new("npx -y opencode-ai@latest run").params(["--print-logs"])
let mut builder =
CommandBuilder::new("npx -y opencode-ai@latest run").params(["--print-logs"]);
if let Some(model) = &self.model {
builder = builder.extend_params(["--model", model]);
}
if let Some(agent) = &self.agent {
builder = builder.extend_params(["--agent", agent]);
}
apply_overrides(builder, &self.cmd)
}
}

View File

@@ -8,7 +8,7 @@ use ts_rs::TS;
use utils::{msg_store::MsgStore, shell::get_shell_command};
use crate::{
command::CommandBuilder,
command::{CmdOverrides, CommandBuilder, apply_overrides},
executors::{ExecutorError, StandardCodingAgentExecutor, gemini::Gemini},
logs::{stderr_processor::normalize_stderr_logs, utils::EntryIndexProvider},
};
@@ -20,15 +20,19 @@ pub struct QwenCode {
pub append_prompt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub yolo: Option<bool>,
#[serde(flatten)]
pub cmd: CmdOverrides,
}
impl QwenCode {
fn build_command_builder(&self) -> CommandBuilder {
let mut builder = CommandBuilder::new("npx -y @qwen-code/qwen-code@latest");
if self.yolo.unwrap_or(false) {
builder = builder.params(["--yolo"]);
builder = builder.extend_params(["--yolo"]);
}
builder
apply_overrides(builder, &self.cmd)
}
}

View File

@@ -59,7 +59,6 @@ fn generate_types_content() -> String {
utils::diff::FileDiffDetails::decl(),
services::services::github_service::RepositoryInfo::decl(),
executors::command::CommandBuilder::decl(),
// New executor profile types
executors::profile::ExecutorProfileId::decl(),
executors::profile::ExecutorProfile::decl(),
executors::profile::VariantAgentConfig::decl(),
@@ -69,6 +68,7 @@ fn generate_types_content() -> String {
executors::executors::gemini::GeminiModel::decl(),
executors::executors::amp::Amp::decl(),
executors::executors::codex::Codex::decl(),
executors::executors::codex::SandboxMode::decl(),
executors::executors::cursor::Cursor::decl(),
executors::executors::opencode::Opencode::decl(),
executors::executors::qwen::QwenCode::decl(),

View File

@@ -136,13 +136,15 @@ export type GeminiModel = "default" | "flash";
export type Amp = { append_prompt?: string | null, dangerously_allow_all?: boolean | null, base_command_override?: string | null, additional_params?: Array<string> | null, };
export type Codex = { append_prompt?: string | null, dangerously_bypass_approvals_and_sandbox?: boolean | null, };
export type Codex = { append_prompt?: string | null, sandbox?: SandboxMode | null, oss?: boolean | null, model?: string | null, base_command_override?: string | null, additional_params?: Array<string> | null, };
export type Cursor = { append_prompt?: string | null, force?: boolean | null, };
export type SandboxMode = "read-only" | "workspace-write" | "danger-full-access";
export type Opencode = { append_prompt?: string | null, };
export type Cursor = { append_prompt?: string | null, force?: boolean | null, model?: string | null, base_command_override?: string | null, additional_params?: Array<string> | null, };
export type QwenCode = { append_prompt?: string | null, yolo?: boolean | null, };
export type Opencode = { append_prompt?: string | null, model?: string | null, agent?: string | null, base_command_override?: string | null, additional_params?: Array<string> | null, };
export type QwenCode = { append_prompt?: string | null, yolo?: boolean | null, base_command_override?: string | null, additional_params?: Array<string> | null, };
export type CodingAgentInitialRequest = { prompt: string,
/**