mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
feat(ai): support opencode 2 cli without pure flag
This commit is contained in:
@@ -6221,6 +6221,7 @@ const aiEditPiAgentCliPath = ref("");
|
||||
const aiEditPiAgentCliEnvRows = ref<AiEnvRow[]>([]);
|
||||
const aiEditOpenCodeCliPath = ref("");
|
||||
const aiEditOpenCodeCliEnvRows = ref<AiEnvRow[]>([]);
|
||||
const aiEditOpenCodeCliVersion = ref("auto");
|
||||
const aiEditCursorCliPath = ref("");
|
||||
const aiEditCursorCliEnvRows = ref<AiEnvRow[]>([]);
|
||||
const aiEditGrokCliPath = ref("");
|
||||
@@ -6511,6 +6512,7 @@ function currentAiEditConfig() {
|
||||
piAgentCliEnv: aiIsPiAgentCli.value ? cliEnvFromRows(aiEditPiAgentCliEnvRows.value) : {},
|
||||
opencodeCliPath: aiEditOpenCodeCliPath.value.trim() || undefined,
|
||||
opencodeCliEnv: aiIsOpenCodeCli.value ? cliEnvFromRows(aiEditOpenCodeCliEnvRows.value) : {},
|
||||
opencodeCliVersion: aiIsOpenCodeCli.value ? aiEditOpenCodeCliVersion.value || undefined : undefined,
|
||||
cursorCliPath: aiEditCursorCliPath.value.trim() || undefined,
|
||||
cursorCliEnv: aiIsCursorCli.value ? cliEnvFromRows(aiEditCursorCliEnvRows.value) : {},
|
||||
grokCliPath: aiEditGrokCliPath.value.trim() || undefined,
|
||||
@@ -6552,6 +6554,7 @@ function aiSelectProvider(presetId: string) {
|
||||
aiEditSkipTlsVerify.value = false;
|
||||
aiEditEnableThinking.value = true;
|
||||
aiEditReasoningLevel.value = "default";
|
||||
aiEditOpenCodeCliVersion.value = "auto";
|
||||
if (CLI_AI_PROVIDERS.has(provider)) void ensureCliMcpStatus();
|
||||
}
|
||||
|
||||
@@ -6603,6 +6606,7 @@ function aiEnterEditMode(configId?: string) {
|
||||
aiEditPiAgentCliEnvRows.value = aiEnvRowsFromConfig(config.piAgentCliEnv);
|
||||
aiEditOpenCodeCliPath.value = config.opencodeCliPath ?? "";
|
||||
aiEditOpenCodeCliEnvRows.value = aiEnvRowsFromConfig(config.opencodeCliEnv);
|
||||
aiEditOpenCodeCliVersion.value = config.opencodeCliVersion ?? "auto";
|
||||
aiEditCursorCliPath.value = config.cursorCliPath ?? "";
|
||||
aiEditCursorCliEnvRows.value = aiEnvRowsFromConfig(config.cursorCliEnv);
|
||||
aiEditGrokCliPath.value = config.grokCliPath ?? "";
|
||||
@@ -6638,6 +6642,7 @@ function aiEnterEditMode(configId?: string) {
|
||||
aiEditPiAgentCliEnvRows.value = [];
|
||||
aiEditOpenCodeCliPath.value = "";
|
||||
aiEditOpenCodeCliEnvRows.value = [];
|
||||
aiEditOpenCodeCliVersion.value = "auto";
|
||||
aiEditCursorCliPath.value = "";
|
||||
aiEditCursorCliEnvRows.value = [];
|
||||
aiEditGrokCliPath.value = "";
|
||||
@@ -11752,6 +11757,26 @@ LIMIT 100;</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OpenCode CLI Version -->
|
||||
<div v-if="!aiIsCcSwitchProvider && aiIsOpenCodeCli" class="grid grid-cols-3 items-start gap-3">
|
||||
<Label class="pt-2 text-right text-xs">{{ t("ai.openCodeCliVersion") }}</Label>
|
||||
<div class="col-span-2 space-y-1.5">
|
||||
<Select v-model="aiEditOpenCodeCliVersion">
|
||||
<SelectTrigger class="h-8 text-xs">
|
||||
<SelectValue :placeholder="t('ai.openCodeCliVersionAuto')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="auto">{{ t("ai.openCodeCliVersionAuto") }}</SelectItem>
|
||||
<SelectItem value="v2">{{ t("ai.openCodeCliVersionV2") }}</SelectItem>
|
||||
<SelectItem value="v1">{{ t("ai.openCodeCliVersionV1") }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p class="text-[11px] text-muted-foreground">
|
||||
{{ t("ai.openCodeCliVersionHint") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CLI Env -->
|
||||
<div v-if="!aiIsCcSwitchProvider && aiIsCliProvider" class="grid grid-cols-3 items-start gap-3">
|
||||
<Label class="pt-2 text-right text-xs">{{ t("ai.cliEnv") }}</Label>
|
||||
|
||||
@@ -115,7 +115,10 @@ describe("useAiModelCatalog", () => {
|
||||
).resolves.toEqual([{ id: "first" }]);
|
||||
await expect(catalog.loadModels({ ...initial, opencodeCliPath: "/usr/local/bin/opencode" })).resolves.toEqual([{ id: "second" }]);
|
||||
|
||||
expect(apiMock.aiListModels).toHaveBeenCalledTimes(2);
|
||||
apiMock.aiListModels.mockResolvedValueOnce([{ id: "third" }]);
|
||||
await expect(catalog.loadModels({ ...initial, opencodeCliVersion: "v2" })).resolves.toEqual([{ id: "third" }]);
|
||||
|
||||
expect(apiMock.aiListModels).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("tracks Cursor executable and environment changes without depending on environment key order", async () => {
|
||||
|
||||
@@ -59,6 +59,7 @@ function configSignature(config: AiConfigItem): string {
|
||||
claudeCodeCliPath: config.claudeCodeCliPath ?? null,
|
||||
piAgentCliPath: config.piAgentCliPath ?? null,
|
||||
opencodeCliPath: config.opencodeCliPath ?? null,
|
||||
opencodeCliVersion: config.opencodeCliVersion ?? null,
|
||||
cursorCliPath: config.cursorCliPath ?? null,
|
||||
codebuddyCliPath: config.codebuddyCliPath ?? null,
|
||||
qoderCliPath: config.qoderCliPath ?? null,
|
||||
@@ -71,6 +72,7 @@ function configSignature(config: AiConfigItem): string {
|
||||
claudeCodeCliEnv: sortedRecord(config.claudeCodeCliEnv),
|
||||
piAgentCliEnv: sortedRecord(config.piAgentCliEnv),
|
||||
opencodeCliEnv: sortedRecord(config.opencodeCliEnv),
|
||||
opencodeCliVersion: config.opencodeCliVersion ?? "",
|
||||
cursorCliEnv: sortedRecord(config.cursorCliEnv),
|
||||
codebuddyCliEnv: sortedRecord(config.codebuddyCliEnv),
|
||||
qoderCliEnv: sortedRecord(config.qoderCliEnv),
|
||||
|
||||
@@ -3328,6 +3328,11 @@ export default withEnglishFallback({
|
||||
cliPath: "{provider} yolu",
|
||||
cliPathHint: "PATH-dən {command} əmrindən istifadə etmək üçün boş saxlayın. {loginCommand} ilə ayrıca daxil olun.",
|
||||
cliPathEnvError: "{provider} yolu yalnız icra faylının yolunu ehtiva etməlidir. Mühit dəyişənlərini aşağıda əlavə edin.",
|
||||
openCodeCliVersion: "OpenCode CLI versiyası",
|
||||
openCodeCliVersionAuto: "Avtomatik aşkarlama",
|
||||
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
|
||||
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
|
||||
openCodeCliVersionHint: "OpenCode 2 köhnə --pure parametrini ləğv edir. Avtomatik aşkarlama icra faylının adını, CLI versiyasını və xəta çıxışını yoxlayır.",
|
||||
cliEnv: "Mühit dəyişənləri",
|
||||
cliEnvAdd: "Dəyişən əlavə et",
|
||||
cliEnvHint: "{provider} üçün ötürülən istəyə bağlı dəyişənlər, məsələn, HTTPS_PROXY, NO_PROXY və ya SSL_CERT_FILE.",
|
||||
|
||||
@@ -4208,6 +4208,11 @@ export default {
|
||||
cliPath: "{provider} Path",
|
||||
cliPathHint: "Leave empty to use {command} from PATH. Sign in separately with {loginCommand}.",
|
||||
cliPathEnvError: "{provider} path should contain only the executable path. Add environment variables below.",
|
||||
openCodeCliVersion: "OpenCode CLI Version",
|
||||
openCodeCliVersionAuto: "Auto-detect",
|
||||
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
|
||||
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
|
||||
openCodeCliVersionHint: "OpenCode 2 removes the legacy --pure flag. Auto-detection checks the executable name, CLI version, and error output.",
|
||||
cliEnv: "Environment variables",
|
||||
cliEnvAdd: "Add variable",
|
||||
cliEnvHint: "Optional variables passed to {provider}, such as HTTPS_PROXY, NO_PROXY, or SSL_CERT_FILE.",
|
||||
|
||||
@@ -3739,6 +3739,11 @@ export default withEnglishFallback({
|
||||
cliPath: "{provider} Path",
|
||||
cliPathHint: "Leave empty to use {command} from PATH. Sign in separately with {loginCommand}.",
|
||||
cliPathEnvError: "{provider} path should contain only the executable path. Add environment variables below.",
|
||||
openCodeCliVersion: "Versión de OpenCode CLI",
|
||||
openCodeCliVersionAuto: "Detección automática",
|
||||
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
|
||||
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
|
||||
openCodeCliVersionHint: "OpenCode 2 elimina el parámetro --pure heredado. La detección automática comprueba el nombre del ejecutable, la versión y la salida de error.",
|
||||
cliEnv: "Environment variables",
|
||||
cliEnvAdd: "Add variable",
|
||||
cliEnvHint: "Optional variables passed to {provider}, such as HTTPS_PROXY, NO_PROXY, or SSL_CERT_FILE.",
|
||||
|
||||
@@ -3655,6 +3655,11 @@ export default withEnglishFallback({
|
||||
cliPath: "Path {provider}",
|
||||
cliPathHint: "Biarkan kosong untuk menggunakan {command} dari PATH. Masuk secara terpisah dengan {loginCommand}.",
|
||||
cliPathEnvError: "Path {provider} hanya boleh berisi path eksekutabel. Tambahkan variabel lingkungan di bawah.",
|
||||
openCodeCliVersion: "Versi OpenCode CLI",
|
||||
openCodeCliVersionAuto: "Deteksi otomatis",
|
||||
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
|
||||
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
|
||||
openCodeCliVersionHint: "OpenCode 2 menghapus flag lama --pure. Deteksi otomatis memeriksa nama eksekutabel, versi CLI, dan keluaran galat.",
|
||||
cliEnv: "Variabel lingkungan",
|
||||
cliEnvAdd: "Tambah variabel",
|
||||
cliEnvHint: "Variabel opsional yang dikirim ke {provider}, seperti HTTPS_PROXY, NO_PROXY, atau SSL_CERT_FILE.",
|
||||
|
||||
@@ -3918,6 +3918,11 @@ export default withEnglishFallback({
|
||||
cliPath: "{provider} Path",
|
||||
cliPathHint: "Leave empty to use {command} from PATH. Sign in separately with {loginCommand}.",
|
||||
cliPathEnvError: "{provider} path should contain only the executable path. Add environment variables below.",
|
||||
openCodeCliVersion: "Versione OpenCode CLI",
|
||||
openCodeCliVersionAuto: "Rileva automaticamente",
|
||||
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
|
||||
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
|
||||
openCodeCliVersionHint: "OpenCode 2 rimuove il flag legacy --pure. Il rilevamento automatico verifica il nome dell'eseguibile, la versione CLI e l'output di errore.",
|
||||
cliEnv: "Environment variables",
|
||||
cliEnvAdd: "Add variable",
|
||||
cliEnvHint: "Optional variables passed to {provider}, such as HTTPS_PROXY, NO_PROXY, or SSL_CERT_FILE.",
|
||||
|
||||
@@ -3608,6 +3608,11 @@ export default withEnglishFallback({
|
||||
cliPath: "{provider} Path",
|
||||
cliPathHint: "Leave empty to use {command} from PATH. Sign in separately with {loginCommand}.",
|
||||
cliPathEnvError: "{provider} path should contain only the executable path. Add environment variables below.",
|
||||
openCodeCliVersion: "OpenCode CLI バージョン",
|
||||
openCodeCliVersionAuto: "自動検出",
|
||||
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
|
||||
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
|
||||
openCodeCliVersionHint: "OpenCode 2 では従来の --pure フラグが削除されています。自動検出で実行可能ファイル名、CLI バージョン、エラー出力を確認します。",
|
||||
cliEnv: "Environment variables",
|
||||
cliEnvAdd: "Add variable",
|
||||
cliEnvHint: "Optional variables passed to {provider}, such as HTTPS_PROXY, NO_PROXY, or SSL_CERT_FILE.",
|
||||
|
||||
@@ -3757,6 +3757,11 @@ export default withEnglishFallback({
|
||||
cliPath: "{provider} 경로",
|
||||
cliPathHint: "PATH의 {command}를 사용하려면 비워 두세요. {loginCommand}로 별도로 로그인하세요.",
|
||||
cliPathEnvError: "{provider} 경로에는 실행 파일 경로만 포함해야 합니다. 환경 변수는 아래에 추가하세요.",
|
||||
openCodeCliVersion: "OpenCode CLI 버전",
|
||||
openCodeCliVersionAuto: "자동 감지",
|
||||
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
|
||||
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
|
||||
openCodeCliVersionHint: "OpenCode 2에서는 기존의 --pure 플래그가 제거되었습니다. 자동 감지는 실행 파일 이름, CLI 버전 및 오류 출력을 확인합니다.",
|
||||
cliEnv: "환경 변수",
|
||||
cliEnvAdd: "변수 추가",
|
||||
cliEnvHint: "{provider}에 전달할 선택적 변수 (예: HTTPS_PROXY, NO_PROXY, SSL_CERT_FILE).",
|
||||
|
||||
@@ -3585,6 +3585,11 @@ export default withEnglishFallback({
|
||||
cliPath: "{provider} Path",
|
||||
cliPathHint: "Leave empty to use {command} from PATH. Sign in separately with {loginCommand}.",
|
||||
cliPathEnvError: "{provider} path should contain only the executable path. Add environment variables below.",
|
||||
openCodeCliVersion: "Versão do OpenCode CLI",
|
||||
openCodeCliVersionAuto: "Detectar automaticamente",
|
||||
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
|
||||
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
|
||||
openCodeCliVersionHint: "O OpenCode 2 remove a flag legada --pure. A detecção automática verifica o nome do executável, versão da CLI e saída de erro.",
|
||||
cliEnv: "Environment variables",
|
||||
cliEnvAdd: "Add variable",
|
||||
cliEnvHint: "Optional variables passed to {provider}, such as HTTPS_PROXY, NO_PROXY, or SSL_CERT_FILE.",
|
||||
|
||||
@@ -3921,6 +3921,11 @@ export default withEnglishFallback({
|
||||
cliPath: "Путь к {provider}",
|
||||
cliPathHint: "Оставьте пустым, чтобы использовать {command} из PATH. Войдите отдельно командой {loginCommand}.",
|
||||
cliPathEnvError: "Путь к {provider} должен содержать только путь к исполняемому файлу. Добавьте переменные окружения ниже.",
|
||||
openCodeCliVersion: "Версия OpenCode CLI",
|
||||
openCodeCliVersionAuto: "Автоопределение",
|
||||
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
|
||||
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
|
||||
openCodeCliVersionHint: "В OpenCode 2 удален устаревший флаг --pure. Автоопределение проверяет имя исполняемого файла, версию CLI и вывод ошибок.",
|
||||
cliEnv: "Переменные окружения",
|
||||
cliEnvAdd: "Добавить переменную",
|
||||
cliEnvHint: "Необязательные переменные, передаваемые {provider}, например HTTPS_PROXY, NO_PROXY или SSL_CERT_FILE.",
|
||||
|
||||
@@ -4121,6 +4121,11 @@ export default withEnglishFallback({
|
||||
cliPath: "Path ของ {provider}",
|
||||
cliPathHint: "ปล่อยว่างเพื่อใช้ {command} จาก PATH ลงชื่อเข้าใช้แยกต่างหากด้วย {loginCommand}",
|
||||
cliPathEnvError: "Path ของ {provider} ควรมีเฉพาะ Path ของ Executable เพิ่ม Environment variables ด้านล่าง",
|
||||
openCodeCliVersion: "เวอร์ชัน OpenCode CLI",
|
||||
openCodeCliVersionAuto: "ตรวจหาอัตโนมัติ",
|
||||
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
|
||||
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
|
||||
openCodeCliVersionHint: "OpenCode 2 ยกเลิกการใช้ flag --pure เดิม การตรวจหาอัตโนมัติจะตรวจสอบชื่อไฟล์ executable เวอร์ชัน CLI และข้อผิดพลาด",
|
||||
cliEnv: "Environment variables",
|
||||
cliEnvAdd: "เพิ่มตัวแปร",
|
||||
cliEnvHint: "ตัวแปรที่ส่งไปยัง {provider} (ไม่บังคับ) เช่น HTTPS_PROXY, NO_PROXY หรือ SSL_CERT_FILE",
|
||||
|
||||
@@ -3299,6 +3299,11 @@ export default withEnglishFallback({
|
||||
cliPath: "{provider} Yolu",
|
||||
cliPathHint: "PATH üzerindeki {command} komutunu kullanmak için boş bırakın. {loginCommand} ile ayrıca oturum açın.",
|
||||
cliPathEnvError: "{provider} yolu yalnızca çalıştırılabilir dosyanın yolunu içermelidir. Ortam değişkenlerini aşağıya ekleyin.",
|
||||
openCodeCliVersion: "OpenCode CLI Sürümü",
|
||||
openCodeCliVersionAuto: "Otomatik algıla",
|
||||
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
|
||||
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
|
||||
openCodeCliVersionHint: "OpenCode 2 eski --pure bayrağını kaldırır. Otomatik algılama çalıştırılabilir dosya adını, CLI sürümünü ve hata çıktısını denetler.",
|
||||
cliEnv: "Ortam değişkenleri",
|
||||
cliEnvAdd: "Değişken ekle",
|
||||
cliEnvHint: "{provider} sağlayıcısına iletilen isteğe bağlı değişkenler; örneğin HTTPS_PROXY, NO_PROXY veya SSL_CERT_FILE.",
|
||||
|
||||
@@ -4138,6 +4138,11 @@ export default withEnglishFallback({
|
||||
cliPath: "{provider} 路径",
|
||||
cliPathHint: "留空则使用 PATH 中的 {command}。请先在终端执行 {loginCommand} 登录。",
|
||||
cliPathEnvError: "{provider} 路径只能填写可执行文件路径。请在下方添加环境变量。",
|
||||
openCodeCliVersion: "OpenCode CLI 版本",
|
||||
openCodeCliVersionAuto: "自动检测",
|
||||
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
|
||||
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
|
||||
openCodeCliVersionHint: "OpenCode 2 移除了旧版的 --pure 参数。自动检测会检查可执行文件名、CLI 版本及报错输出。",
|
||||
cliEnv: "环境变量",
|
||||
cliEnvAdd: "添加变量",
|
||||
cliEnvHint: "传递给 {provider} 的可选变量,例如 HTTPS_PROXY、NO_PROXY 或 SSL_CERT_FILE。",
|
||||
|
||||
@@ -3893,6 +3893,11 @@ export default withEnglishFallback({
|
||||
cliPath: "{provider} 路径",
|
||||
cliPathHint: "留空则使用 PATH 中的 {command}。请先在终端执行 {loginCommand} 登录。",
|
||||
cliPathEnvError: "{provider} 路径只能填写可执行文件路径。请在下方添加环境变量。",
|
||||
openCodeCliVersion: "OpenCode CLI 版本",
|
||||
openCodeCliVersionAuto: "自動檢測",
|
||||
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
|
||||
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
|
||||
openCodeCliVersionHint: "OpenCode 2 移除了舊版的 --pure 參數。自動檢測會檢查執行檔名稱、CLI 版本及報錯輸出。",
|
||||
cliEnv: "环境变量",
|
||||
cliEnvAdd: "添加变量",
|
||||
cliEnvHint: "传递给 {provider} 的可选变量,例如 HTTPS_PROXY、NO_PROXY 或 SSL_CERT_FILE。",
|
||||
|
||||
@@ -1454,6 +1454,7 @@ describe("settingsStore AI API key normalization", () => {
|
||||
provider: "opencode-cli",
|
||||
opencodeCliPath: " /opt/homebrew/bin/opencode ",
|
||||
opencodeCliEnv: { HTTPS_PROXY: "http://127.0.0.1:7890", EMPTY: null as unknown as string },
|
||||
opencodeCliVersion: " v2 ",
|
||||
}),
|
||||
).toMatchObject({
|
||||
provider: "opencode-cli",
|
||||
@@ -1461,6 +1462,7 @@ describe("settingsStore AI API key normalization", () => {
|
||||
model: "default",
|
||||
opencodeCliPath: "/opt/homebrew/bin/opencode",
|
||||
opencodeCliEnv: { HTTPS_PROXY: "http://127.0.0.1:7890", EMPTY: "" },
|
||||
opencodeCliVersion: "v2",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -728,6 +728,7 @@ export function normalizeAiConfig(config: Partial<AiConfig> | null | undefined):
|
||||
piAgentCliEnv: normalizeAiEnv(config?.piAgentCliEnv),
|
||||
opencodeCliPath: config?.opencodeCliPath?.trim() || undefined,
|
||||
opencodeCliEnv: normalizeAiEnv(config?.opencodeCliEnv),
|
||||
opencodeCliVersion: config?.opencodeCliVersion?.trim() || undefined,
|
||||
cursorCliPath: config?.cursorCliPath?.trim() || undefined,
|
||||
cursorCliEnv: normalizeAiEnv(config?.cursorCliEnv),
|
||||
grokCliPath: config?.grokCliPath?.trim() || undefined,
|
||||
|
||||
@@ -75,6 +75,7 @@ export interface AiConfig {
|
||||
piAgentCliEnv?: Record<string, string>;
|
||||
opencodeCliPath?: string | null;
|
||||
opencodeCliEnv?: Record<string, string>;
|
||||
opencodeCliVersion?: string | null;
|
||||
cursorCliPath?: string | null;
|
||||
cursorCliEnv?: Record<string, string>;
|
||||
grokCliPath?: string | null;
|
||||
|
||||
@@ -99,7 +99,7 @@ pub enum AiProvider {
|
||||
ClaudeCodeCli,
|
||||
#[serde(rename = "pi-agent-cli")]
|
||||
PiAgentCli,
|
||||
#[serde(rename = "opencode-cli")]
|
||||
#[serde(rename = "opencode-cli", alias = "opencode2-cli")]
|
||||
OpenCodeCli,
|
||||
#[serde(rename = "cursor-cli")]
|
||||
CursorCli,
|
||||
@@ -459,6 +459,8 @@ pub struct AiConfig {
|
||||
#[serde(default)]
|
||||
pub opencode_cli_env: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub opencode_cli_version: Option<String>,
|
||||
#[serde(default)]
|
||||
pub cursor_cli_path: Option<String>,
|
||||
#[serde(default)]
|
||||
pub cursor_cli_env: HashMap<String, String>,
|
||||
@@ -5634,6 +5636,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -6271,6 +6274,7 @@ mod tests {
|
||||
assert!(config.pi_agent_cli_env.is_empty());
|
||||
assert!(config.opencode_cli_path.is_none());
|
||||
assert!(config.opencode_cli_env.is_empty());
|
||||
assert!(config.opencode_cli_version.is_none());
|
||||
assert!(config.cursor_cli_path.is_none());
|
||||
assert!(config.cursor_cli_env.is_empty());
|
||||
assert!(config.codebuddy_cli_path.is_none());
|
||||
@@ -6307,6 +6311,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -6350,6 +6355,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -6408,6 +6414,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -6449,6 +6456,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -6494,6 +6502,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -6536,6 +6545,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -6597,6 +6607,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -6634,6 +6645,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -6674,6 +6686,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -6781,6 +6794,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -6869,6 +6883,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -6984,6 +6999,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -7034,6 +7050,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -7206,6 +7223,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -7252,6 +7270,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -7346,6 +7365,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -7695,6 +7715,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -7928,6 +7949,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -7981,6 +8003,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -8037,6 +8060,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -8087,6 +8111,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -8481,6 +8506,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -8558,6 +8584,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -8631,6 +8658,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
|
||||
@@ -586,6 +586,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
|
||||
@@ -941,6 +941,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
|
||||
@@ -1132,6 +1132,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
|
||||
@@ -423,6 +423,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
|
||||
@@ -482,6 +482,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
|
||||
@@ -803,6 +803,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
|
||||
@@ -5,11 +5,12 @@ use crate::ai_cli_agent::{
|
||||
CliAgentCommandSpec, CliAgentJsonlDialect, CliAgentProcessSpec, CliAgentRunOptions,
|
||||
};
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::Notify;
|
||||
use tokio::sync::{Notify, RwLock};
|
||||
|
||||
const OPENCODE_MODEL_DISCOVERY_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const OPENCODE_CONTROL_ENV: &[&str] = &[
|
||||
@@ -24,6 +25,103 @@ const OPENCODE_CONTROL_ENV: &[&str] = &[
|
||||
pub type OpenCodeRunOptions = CliAgentRunOptions;
|
||||
pub type OpenCodeCommandSpec = CliAgentCommandSpec;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum OpenCodeVersion {
|
||||
V1,
|
||||
V2,
|
||||
}
|
||||
|
||||
static OPENCODE_DETECTED_V2: LazyLock<RwLock<HashMap<String, bool>>> = LazyLock::new(|| RwLock::new(HashMap::new()));
|
||||
|
||||
pub fn resolve_configured_opencode_version(config: &AiConfig) -> Option<OpenCodeVersion> {
|
||||
if let Some(version) = &config.opencode_cli_version {
|
||||
let trimmed = version.trim().to_ascii_lowercase();
|
||||
if trimmed == "v2" || trimmed == "2" || trimmed == "opencode2" {
|
||||
return Some(OpenCodeVersion::V2);
|
||||
}
|
||||
if trimmed == "v1" || trimmed == "1" || trimmed == "opencode1" {
|
||||
return Some(OpenCodeVersion::V1);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn is_opencode_v2_program_name(program: &str) -> bool {
|
||||
let lower = program.to_ascii_lowercase();
|
||||
let file_name = Path::new(&lower).file_name().and_then(|n| n.to_str()).unwrap_or(&lower);
|
||||
file_name.contains("opencode2")
|
||||
|| file_name.contains("opencode-2")
|
||||
|| file_name.contains("opencode-v2")
|
||||
|| file_name.contains("opencode_v2")
|
||||
}
|
||||
|
||||
pub fn parse_opencode_version_is_v2(output: &str) -> bool {
|
||||
let lower = output.to_ascii_lowercase();
|
||||
for token in lower.split_whitespace() {
|
||||
let cleaned = token.trim_start_matches('v');
|
||||
if let Some(major_str) = cleaned.split('.').next() {
|
||||
if let Ok(major) = major_str.parse::<u32>() {
|
||||
if major >= 2 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn record_opencode_version_v2(program: &str) {
|
||||
OPENCODE_DETECTED_V2.write().await.insert(program.to_string(), true);
|
||||
}
|
||||
|
||||
pub async fn detect_opencode_version(program: &str) -> OpenCodeVersion {
|
||||
if is_opencode_v2_program_name(program) {
|
||||
return OpenCodeVersion::V2;
|
||||
}
|
||||
if let Some(&is_v2) = OPENCODE_DETECTED_V2.read().await.get(program) {
|
||||
return if is_v2 { OpenCodeVersion::V2 } else { OpenCodeVersion::V1 };
|
||||
}
|
||||
|
||||
let mut process = cli_command(program);
|
||||
process.arg("--version");
|
||||
let is_v2 = match tokio::time::timeout(Duration::from_millis(1500), process.output()).await {
|
||||
Ok(Ok(output)) if output.status.success() => {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
parse_opencode_version_is_v2(&stdout)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
OPENCODE_DETECTED_V2.write().await.insert(program.to_string(), is_v2);
|
||||
if is_v2 {
|
||||
OpenCodeVersion::V2
|
||||
} else {
|
||||
OpenCodeVersion::V1
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_opencode_version_fast(config: &AiConfig) -> OpenCodeVersion {
|
||||
if let Some(version) = resolve_configured_opencode_version(config) {
|
||||
return version;
|
||||
}
|
||||
let program = opencode_program(config);
|
||||
if is_opencode_v2_program_name(&program) {
|
||||
return OpenCodeVersion::V2;
|
||||
}
|
||||
if let Ok(guard) = OPENCODE_DETECTED_V2.try_read() {
|
||||
if let Some(&is_v2) = guard.get(&program) {
|
||||
return if is_v2 { OpenCodeVersion::V2 } else { OpenCodeVersion::V1 };
|
||||
}
|
||||
}
|
||||
OpenCodeVersion::V1
|
||||
}
|
||||
|
||||
pub fn is_unrecognized_pure_flag(message: &str) -> bool {
|
||||
let lower = message.to_ascii_lowercase();
|
||||
lower.contains("--pure")
|
||||
&& (lower.contains("unrecognized") || lower.contains("unknown") || lower.contains("unexpected"))
|
||||
}
|
||||
|
||||
struct OpenCodeIsolatedCwd {
|
||||
path: PathBuf,
|
||||
}
|
||||
@@ -216,21 +314,48 @@ fn opencode_selection_args(config: &AiConfig) -> Vec<String> {
|
||||
}
|
||||
|
||||
pub fn build_opencode_command(config: &AiConfig) -> OpenCodeCommandSpec {
|
||||
let version = resolve_opencode_version_fast(config);
|
||||
build_opencode_command_for_version(config, version)
|
||||
}
|
||||
|
||||
pub fn build_opencode_command_for_version(config: &AiConfig, version: OpenCodeVersion) -> OpenCodeCommandSpec {
|
||||
let mut command = OpenCodeCommandSpec { program: opencode_program(config), args: Vec::new() };
|
||||
command.args.extend(["run".to_string(), "--format".to_string(), "json".to_string(), "--pure".to_string()]);
|
||||
command.args.extend(["run".to_string(), "--format".to_string(), "json".to_string()]);
|
||||
if version == OpenCodeVersion::V1 {
|
||||
command.args.push("--pure".to_string());
|
||||
}
|
||||
command.args.extend(opencode_selection_args(config));
|
||||
command
|
||||
}
|
||||
|
||||
fn resolved_models_command(config: &AiConfig) -> Result<OpenCodeCommandSpec, String> {
|
||||
pub fn resolved_models_command(config: &AiConfig) -> Result<OpenCodeCommandSpec, String> {
|
||||
let version = resolve_opencode_version_fast(config);
|
||||
resolved_models_command_for_version(config, version)
|
||||
}
|
||||
|
||||
pub fn resolved_models_command_for_version(
|
||||
config: &AiConfig,
|
||||
version: OpenCodeVersion,
|
||||
) -> Result<OpenCodeCommandSpec, String> {
|
||||
let mut command = resolve_opencode_command(config)?;
|
||||
command.args.extend(["models".to_string(), "--verbose".to_string(), "--pure".to_string()]);
|
||||
command.args.extend(["models".to_string(), "--verbose".to_string()]);
|
||||
if version == OpenCodeVersion::V1 {
|
||||
command.args.push("--pure".to_string());
|
||||
}
|
||||
Ok(command)
|
||||
}
|
||||
|
||||
fn resolved_run_command(config: &AiConfig) -> Result<OpenCodeCommandSpec, String> {
|
||||
pub fn resolved_run_command(config: &AiConfig) -> Result<OpenCodeCommandSpec, String> {
|
||||
let version = resolve_opencode_version_fast(config);
|
||||
resolved_run_command_for_version(config, version)
|
||||
}
|
||||
|
||||
pub fn resolved_run_command_for_version(
|
||||
config: &AiConfig,
|
||||
version: OpenCodeVersion,
|
||||
) -> Result<OpenCodeCommandSpec, String> {
|
||||
let resolved = resolve_opencode_command(config)?;
|
||||
let mut command = build_opencode_command(config);
|
||||
let mut command = build_opencode_command_for_version(config, version);
|
||||
command.program = resolved.program;
|
||||
command.args.splice(0..0, resolved.args);
|
||||
Ok(command)
|
||||
@@ -241,7 +366,13 @@ pub fn build_opencode_prompt(system_prompt: &str, messages: &[crate::ai::AiMessa
|
||||
}
|
||||
|
||||
pub async fn list_opencode_models(config: &AiConfig) -> Result<Vec<AiModelInfo>, String> {
|
||||
let command = resolved_models_command(config)?;
|
||||
let mut version = if let Some(configured) = resolve_configured_opencode_version(config) {
|
||||
configured
|
||||
} else {
|
||||
let program = opencode_program(config);
|
||||
detect_opencode_version(&program).await
|
||||
};
|
||||
let command = resolved_models_command_for_version(config, version)?;
|
||||
let runtime = OpenCodeIsolatedCwd::create()?;
|
||||
let mut process = cli_command(&command.program);
|
||||
process.args(&command.args);
|
||||
@@ -258,7 +389,34 @@ pub async fn list_opencode_models(config: &AiConfig) -> Result<Vec<AiModelInfo>,
|
||||
.map_err(|error| classify_opencode_spawn_error(&error.to_string()))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(classify_opencode_run_error(&combined_output(&output.stderr, &output.stdout)));
|
||||
let err_output = combined_output(&output.stderr, &output.stdout);
|
||||
if version == OpenCodeVersion::V1 && is_unrecognized_pure_flag(&err_output) {
|
||||
let program = opencode_program(config);
|
||||
record_opencode_version_v2(&program).await;
|
||||
version = OpenCodeVersion::V2;
|
||||
let retry_command = resolved_models_command_for_version(config, version)?;
|
||||
let mut retry_process = cli_command(&retry_command.program);
|
||||
retry_process.args(&retry_command.args);
|
||||
for key in OPENCODE_CONTROL_ENV {
|
||||
retry_process.env_remove(key);
|
||||
}
|
||||
retry_process
|
||||
.envs(opencode_process_env(config, opencode_runtime_config(None))?)
|
||||
.current_dir(&runtime.path)
|
||||
.kill_on_drop(true);
|
||||
let retry_output = tokio::time::timeout(OPENCODE_MODEL_DISCOVERY_TIMEOUT, retry_process.output())
|
||||
.await
|
||||
.map_err(|_| "[openCodeTimeout] OpenCode model discovery timed out".to_string())?
|
||||
.map_err(|error| classify_opencode_spawn_error(&error.to_string()))?;
|
||||
if !retry_output.status.success() {
|
||||
return Err(classify_opencode_run_error(&combined_output(&retry_output.stderr, &retry_output.stdout)));
|
||||
}
|
||||
return parse_opencode_models(&String::from_utf8_lossy(&retry_output.stdout)).ok_or_else(|| {
|
||||
"[openCodeNotAuthenticated] OpenCode returned no configured models. Sign in or configure a provider in OpenCode, then retry."
|
||||
.to_string()
|
||||
});
|
||||
}
|
||||
return Err(classify_opencode_run_error(&err_output));
|
||||
}
|
||||
parse_opencode_models(&String::from_utf8_lossy(&output.stdout)).ok_or_else(|| {
|
||||
"[openCodeNotAuthenticated] OpenCode returned no configured models. Sign in or configure a provider in OpenCode, then retry."
|
||||
@@ -413,10 +571,22 @@ pub async fn run_opencode_agent(
|
||||
cancelled: &Notify,
|
||||
on_event: impl Fn(AgentEvent) + Send + Sync + 'static,
|
||||
) -> Result<String, String> {
|
||||
let version = if let Some(configured) = resolve_configured_opencode_version(config) {
|
||||
configured
|
||||
} else {
|
||||
let program = opencode_program(config);
|
||||
detect_opencode_version(&program).await
|
||||
};
|
||||
let runtime = OpenCodeIsolatedCwd::create()?;
|
||||
let result = run_cli_jsonl_agent(
|
||||
let command = resolved_run_command_for_version(config, version)?;
|
||||
let on_event_arc = Arc::new(on_event);
|
||||
let on_event_fn = {
|
||||
let on_event_arc = Arc::clone(&on_event_arc);
|
||||
move |event| on_event_arc(event)
|
||||
};
|
||||
let run_res = run_cli_jsonl_agent(
|
||||
CliAgentProcessSpec {
|
||||
command: resolved_run_command(config)?,
|
||||
command,
|
||||
env: opencode_process_env(config, opencode_runtime_config(Some(&options)))?,
|
||||
env_remove: OPENCODE_CONTROL_ENV.iter().map(|value| (*value).to_string()).collect(),
|
||||
current_dir: Some(runtime.path.clone()),
|
||||
@@ -426,9 +596,38 @@ pub async fn run_opencode_agent(
|
||||
classify_run_error: classify_opencode_run_error,
|
||||
},
|
||||
cancelled,
|
||||
on_event,
|
||||
on_event_fn,
|
||||
)
|
||||
.await?;
|
||||
.await;
|
||||
|
||||
let result = match run_res {
|
||||
Err(err) if version == OpenCodeVersion::V1 && is_unrecognized_pure_flag(&err) => {
|
||||
let program = opencode_program(config);
|
||||
record_opencode_version_v2(&program).await;
|
||||
let v2_command = resolved_run_command_for_version(config, OpenCodeVersion::V2)?;
|
||||
let on_event_fn2 = {
|
||||
let on_event_arc = Arc::clone(&on_event_arc);
|
||||
move |event| on_event_arc(event)
|
||||
};
|
||||
run_cli_jsonl_agent(
|
||||
CliAgentProcessSpec {
|
||||
command: v2_command,
|
||||
env: opencode_process_env(config, opencode_runtime_config(Some(&options)))?,
|
||||
env_remove: OPENCODE_CONTROL_ENV.iter().map(|value| (*value).to_string()).collect(),
|
||||
current_dir: Some(runtime.path.clone()),
|
||||
stdin: Some(prompt.to_string()),
|
||||
dialect: CliAgentJsonlDialect::OpenCodeRun,
|
||||
classify_spawn_error: classify_opencode_spawn_error,
|
||||
classify_run_error: classify_opencode_run_error,
|
||||
},
|
||||
cancelled,
|
||||
on_event_fn2,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
other => other?,
|
||||
};
|
||||
|
||||
if result.trim().is_empty() {
|
||||
Err("[openCodeProtocolError] OpenCode completed without a text response".to_string())
|
||||
} else {
|
||||
@@ -469,6 +668,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -528,6 +728,70 @@ mod tests {
|
||||
assert_eq!(command.args, ["run", "--format", "json", "--pure"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_run_command_for_opencode2_executable() {
|
||||
let mut config = config("default");
|
||||
config.opencode_cli_path = Some("/usr/local/bin/opencode2".to_string());
|
||||
|
||||
let command = build_opencode_command(&config);
|
||||
|
||||
assert_eq!(command.program, "/usr/local/bin/opencode2");
|
||||
assert_eq!(command.args, ["run", "--format", "json"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_run_command_for_opencode2_configured_version() {
|
||||
let mut config = config("default");
|
||||
config.opencode_cli_version = Some("v2".to_string());
|
||||
|
||||
let command = build_opencode_command(&config);
|
||||
|
||||
assert_eq!(command.program, "opencode");
|
||||
assert_eq!(command.args, ["run", "--format", "json"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn models_command_omits_pure_flag_for_v2() {
|
||||
let mut config = config("default");
|
||||
config.opencode_cli_version = Some("v2".to_string());
|
||||
|
||||
let command = resolved_models_command(&config).unwrap();
|
||||
|
||||
assert_eq!(command.program, "opencode");
|
||||
assert_eq!(command.args, ["models", "--verbose"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_opencode_version_string() {
|
||||
assert!(parse_opencode_version_is_v2("opencode v2.0.1"));
|
||||
assert!(parse_opencode_version_is_v2("opencode 2.0.0"));
|
||||
assert!(parse_opencode_version_is_v2("2.1.0"));
|
||||
assert!(parse_opencode_version_is_v2("v2.0.0-beta.1"));
|
||||
assert!(!parse_opencode_version_is_v2("opencode v1.4.2"));
|
||||
assert!(!parse_opencode_version_is_v2("opencode 0.9.1"));
|
||||
assert!(!parse_opencode_version_is_v2(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_opencode_v2_from_program_name() {
|
||||
assert!(is_opencode_v2_program_name("opencode2"));
|
||||
assert!(is_opencode_v2_program_name("opencode-2"));
|
||||
assert!(is_opencode_v2_program_name("opencode-v2"));
|
||||
assert!(is_opencode_v2_program_name("/usr/local/bin/opencode2"));
|
||||
assert!(is_opencode_v2_program_name(r"C:\Program Files\OpenCode\opencode2.exe"));
|
||||
assert!(!is_opencode_v2_program_name("opencode"));
|
||||
assert!(!is_opencode_v2_program_name("/usr/bin/opencode"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_unrecognized_pure_flag_error() {
|
||||
assert!(is_unrecognized_pure_flag("Unrecognized flag: --pure in command opencode run"));
|
||||
assert!(is_unrecognized_pure_flag("error: unknown flag: --pure"));
|
||||
assert!(is_unrecognized_pure_flag("ERROR\n Unrecognized flag: --pure"));
|
||||
assert!(!is_unrecognized_pure_flag("connection failed"));
|
||||
assert!(!is_unrecognized_pure_flag("unknown model"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connection_check_uses_model_discovery_without_a_model_request() {
|
||||
let command = resolved_models_command(&config("default")).unwrap();
|
||||
|
||||
@@ -877,6 +877,7 @@ mod tests {
|
||||
pi_agent_cli_env: HashMap::new(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: HashMap::new(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: HashMap::new(),
|
||||
grok_cli_path: None,
|
||||
|
||||
@@ -545,6 +545,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
|
||||
@@ -3476,6 +3476,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
|
||||
@@ -14229,6 +14229,7 @@ mod tests {
|
||||
pi_agent_cli_env: std::collections::HashMap::new(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: std::collections::HashMap::new(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: std::collections::HashMap::new(),
|
||||
grok_cli_path: None,
|
||||
|
||||
@@ -607,6 +607,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
@@ -716,6 +717,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
|
||||
+2
@@ -1141,9 +1141,11 @@ test("normalizes legacy AI config and fills provider defaults", () => {
|
||||
provider: "opencode-cli",
|
||||
opencodeCliPath: " /opt/homebrew/bin/opencode ",
|
||||
opencodeCliEnv: { HTTPS_PROXY: "http://proxy:9800" },
|
||||
opencodeCliVersion: " v2 ",
|
||||
} as any);
|
||||
assert.equal(openCode.opencodeCliPath, "/opt/homebrew/bin/opencode");
|
||||
assert.deepEqual(openCode.opencodeCliEnv, { HTTPS_PROXY: "http://proxy:9800" });
|
||||
assert.equal(openCode.opencodeCliVersion, "v2");
|
||||
assert.equal(openCode.model, "default");
|
||||
const grokCli = normalizeAiConfig({
|
||||
provider: "grok-cli",
|
||||
|
||||
@@ -482,7 +482,22 @@ fn resolve_cli_provider_config(mut config: AiConfig) -> AiConfig {
|
||||
AiProvider::CodexCli => (&mut config.codex_cli_path, "codex"),
|
||||
AiProvider::ClaudeCodeCli => (&mut config.claude_code_cli_path, "claude"),
|
||||
AiProvider::PiAgentCli => (&mut config.pi_agent_cli_path, "pi"),
|
||||
AiProvider::OpenCodeCli => (&mut config.opencode_cli_path, "opencode"),
|
||||
AiProvider::OpenCodeCli => {
|
||||
let default_cmd = if matches!(config.opencode_cli_version.as_deref(), Some("v2" | "2")) {
|
||||
if super::mcp::locate_command("opencode2").is_some() {
|
||||
"opencode2"
|
||||
} else {
|
||||
"opencode"
|
||||
}
|
||||
} else if super::mcp::locate_command("opencode").is_none()
|
||||
&& super::mcp::locate_command("opencode2").is_some()
|
||||
{
|
||||
"opencode2"
|
||||
} else {
|
||||
"opencode"
|
||||
};
|
||||
(&mut config.opencode_cli_path, default_cmd)
|
||||
}
|
||||
AiProvider::CursorCli => (&mut config.cursor_cli_path, "agent"),
|
||||
AiProvider::GrokCli => (&mut config.grok_cli_path, "grok"),
|
||||
AiProvider::CodeBuddyCli => (&mut config.codebuddy_cli_path, "codebuddy"),
|
||||
@@ -739,6 +754,7 @@ mod tests {
|
||||
pi_agent_cli_env: Default::default(),
|
||||
opencode_cli_path: None,
|
||||
opencode_cli_env: Default::default(),
|
||||
opencode_cli_version: None,
|
||||
cursor_cli_path: None,
|
||||
cursor_cli_env: Default::default(),
|
||||
grok_cli_path: None,
|
||||
|
||||
Reference in New Issue
Block a user