Skip to content

Commit 081300b

Browse files
authored
Merge branch 'code-yeongyu:dev' into dev
2 parents 03d5b4f + 5cb5dbe commit 081300b

5 files changed

Lines changed: 200 additions & 14 deletions

File tree

bun.lock

Lines changed: 7 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

signatures/cla.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,38 @@
559559
"created_at": "2026-01-16T09:14:21Z",
560560
"repoId": 1108837393,
561561
"pullRequestNo": 847
562+
},
563+
{
564+
"name": "vmlinuzx",
565+
"id": 233838569,
566+
"comment_id": 3760678754,
567+
"created_at": "2026-01-16T15:45:52Z",
568+
"repoId": 1108837393,
569+
"pullRequestNo": 837
570+
},
571+
{
572+
"name": "luojiyin1987",
573+
"id": 6524977,
574+
"comment_id": 3760712340,
575+
"created_at": "2026-01-16T15:54:07Z",
576+
"repoId": 1108837393,
577+
"pullRequestNo": 855
578+
},
579+
{
580+
"name": "qwertystars",
581+
"id": 62981066,
582+
"comment_id": 3761235668,
583+
"created_at": "2026-01-16T18:13:52Z",
584+
"repoId": 1108837393,
585+
"pullRequestNo": 859
586+
},
587+
{
588+
"name": "sgwannabe",
589+
"id": 33509021,
590+
"comment_id": 3762457370,
591+
"created_at": "2026-01-17T01:25:58Z",
592+
"repoId": 1108837393,
593+
"pullRequestNo": 863
562594
}
563595
]
564596
}

src/cli/doctor/checks/opencode.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,94 @@ describe("opencode check", () => {
4343
})
4444
})
4545

46+
describe("command helpers", () => {
47+
it("selects where on Windows", () => {
48+
// #given win32 platform
49+
// #when selecting lookup command
50+
// #then should use where
51+
expect(opencode.getBinaryLookupCommand("win32")).toBe("where")
52+
})
53+
54+
it("selects which on non-Windows", () => {
55+
// #given linux platform
56+
// #when selecting lookup command
57+
// #then should use which
58+
expect(opencode.getBinaryLookupCommand("linux")).toBe("which")
59+
expect(opencode.getBinaryLookupCommand("darwin")).toBe("which")
60+
})
61+
62+
it("parses command output into paths", () => {
63+
// #given raw output with multiple lines and spaces
64+
const output = "C:\\\\bin\\\\opencode.ps1\r\nC:\\\\bin\\\\opencode.exe\n\n"
65+
66+
// #when parsing
67+
const paths = opencode.parseBinaryPaths(output)
68+
69+
// #then should return trimmed, non-empty paths
70+
expect(paths).toEqual(["C:\\\\bin\\\\opencode.ps1", "C:\\\\bin\\\\opencode.exe"])
71+
})
72+
73+
it("prefers exe/cmd/bat over ps1 on Windows", () => {
74+
// #given windows paths
75+
const paths = [
76+
"C:\\\\bin\\\\opencode.ps1",
77+
"C:\\\\bin\\\\opencode.cmd",
78+
"C:\\\\bin\\\\opencode.exe",
79+
]
80+
81+
// #when selecting binary
82+
const selected = opencode.selectBinaryPath(paths, "win32")
83+
84+
// #then should prefer exe
85+
expect(selected).toBe("C:\\\\bin\\\\opencode.exe")
86+
})
87+
88+
it("falls back to ps1 when it is the only Windows candidate", () => {
89+
// #given only ps1 path
90+
const paths = ["C:\\\\bin\\\\opencode.ps1"]
91+
92+
// #when selecting binary
93+
const selected = opencode.selectBinaryPath(paths, "win32")
94+
95+
// #then should return ps1 path
96+
expect(selected).toBe("C:\\\\bin\\\\opencode.ps1")
97+
})
98+
99+
it("builds PowerShell command for ps1 on Windows", () => {
100+
// #given a ps1 path on Windows
101+
const command = opencode.buildVersionCommand(
102+
"C:\\\\bin\\\\opencode.ps1",
103+
"win32"
104+
)
105+
106+
// #when building command
107+
// #then should use PowerShell
108+
expect(command).toEqual([
109+
"powershell",
110+
"-NoProfile",
111+
"-ExecutionPolicy",
112+
"Bypass",
113+
"-File",
114+
"C:\\\\bin\\\\opencode.ps1",
115+
"--version",
116+
])
117+
})
118+
119+
it("builds direct command for non-ps1 binaries", () => {
120+
// #given an exe on Windows and a binary on linux
121+
const winCommand = opencode.buildVersionCommand(
122+
"C:\\\\bin\\\\opencode.exe",
123+
"win32"
124+
)
125+
const linuxCommand = opencode.buildVersionCommand("opencode", "linux")
126+
127+
// #when building commands
128+
// #then should execute directly
129+
expect(winCommand).toEqual(["C:\\\\bin\\\\opencode.exe", "--version"])
130+
expect(linuxCommand).toEqual(["opencode", "--version"])
131+
})
132+
})
133+
46134
describe("getOpenCodeInfo", () => {
47135
it("returns installed: false when binary not found", async () => {
48136
// #given no opencode binary

src/cli/doctor/checks/opencode.ts

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,70 @@
11
import type { CheckResult, CheckDefinition, OpenCodeInfo } from "../types"
22
import { CHECK_IDS, CHECK_NAMES, MIN_OPENCODE_VERSION, OPENCODE_BINARIES } from "../constants"
33

4+
const WINDOWS_EXECUTABLE_EXTS = [".exe", ".cmd", ".bat", ".ps1"]
5+
6+
export function getBinaryLookupCommand(platform: NodeJS.Platform): "which" | "where" {
7+
return platform === "win32" ? "where" : "which"
8+
}
9+
10+
export function parseBinaryPaths(output: string): string[] {
11+
return output
12+
.split(/\r?\n/)
13+
.map((line) => line.trim())
14+
.filter((line) => line.length > 0)
15+
}
16+
17+
export function selectBinaryPath(
18+
paths: string[],
19+
platform: NodeJS.Platform
20+
): string | null {
21+
if (paths.length === 0) return null
22+
if (platform !== "win32") return paths[0]
23+
24+
const normalized = paths.map((path) => path.toLowerCase())
25+
for (const ext of WINDOWS_EXECUTABLE_EXTS) {
26+
const index = normalized.findIndex((path) => path.endsWith(ext))
27+
if (index !== -1) return paths[index]
28+
}
29+
30+
return paths[0]
31+
}
32+
33+
export function buildVersionCommand(
34+
binaryPath: string,
35+
platform: NodeJS.Platform
36+
): string[] {
37+
if (
38+
platform === "win32" &&
39+
binaryPath.toLowerCase().endsWith(".ps1")
40+
) {
41+
return [
42+
"powershell",
43+
"-NoProfile",
44+
"-ExecutionPolicy",
45+
"Bypass",
46+
"-File",
47+
binaryPath,
48+
"--version",
49+
]
50+
}
51+
52+
return [binaryPath, "--version"]
53+
}
54+
455
export async function findOpenCodeBinary(): Promise<{ binary: string; path: string } | null> {
556
for (const binary of OPENCODE_BINARIES) {
657
try {
7-
const proc = Bun.spawn(["which", binary], { stdout: "pipe", stderr: "pipe" })
58+
const lookupCommand = getBinaryLookupCommand(process.platform)
59+
const proc = Bun.spawn([lookupCommand, binary], { stdout: "pipe", stderr: "pipe" })
860
const output = await new Response(proc.stdout).text()
961
await proc.exited
1062
if (proc.exitCode === 0) {
11-
return { binary, path: output.trim() }
63+
const paths = parseBinaryPaths(output)
64+
const selectedPath = selectBinaryPath(paths, process.platform)
65+
if (selectedPath) {
66+
return { binary, path: selectedPath }
67+
}
1268
}
1369
} catch {
1470
continue
@@ -17,9 +73,13 @@ export async function findOpenCodeBinary(): Promise<{ binary: string; path: stri
1773
return null
1874
}
1975

20-
export async function getOpenCodeVersion(binary: string): Promise<string | null> {
76+
export async function getOpenCodeVersion(
77+
binaryPath: string,
78+
platform: NodeJS.Platform = process.platform
79+
): Promise<string | null> {
2180
try {
22-
const proc = Bun.spawn([binary, "--version"], { stdout: "pipe", stderr: "pipe" })
81+
const command = buildVersionCommand(binaryPath, platform)
82+
const proc = Bun.spawn(command, { stdout: "pipe", stderr: "pipe" })
2383
const output = await new Response(proc.stdout).text()
2484
await proc.exited
2585
if (proc.exitCode === 0) {
@@ -61,7 +121,7 @@ export async function getOpenCodeInfo(): Promise<OpenCodeInfo> {
61121
}
62122
}
63123

64-
const version = await getOpenCodeVersion(binaryInfo.binary)
124+
const version = await getOpenCodeVersion(binaryInfo.path ?? binaryInfo.binary)
65125

66126
return {
67127
installed: true,

src/hooks/keyword-detector/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { PluginInput } from "@opencode-ai/plugin"
22
import { detectKeywordsWithType, extractPromptText, removeCodeBlocks } from "./detector"
33
import { log } from "../../shared"
44
import { isSystemDirective } from "../../shared/system-directive"
5-
import { getMainSessionID, getSessionAgent } from "../../features/claude-code-session-state"
5+
import { getMainSessionID, getSessionAgent, subagentSessions } from "../../features/claude-code-session-state"
66
import type { ContextCollector } from "../../features/context-injector"
77

88
export * from "./detector"
@@ -37,6 +37,13 @@ export function createKeywordDetectorHook(ctx: PluginInput, collector?: ContextC
3737
return
3838
}
3939

40+
// Skip keyword detection for background task sessions to prevent mode injection
41+
// (e.g., [analyze-mode]) which incorrectly triggers Prometheus restrictions
42+
const isBackgroundTaskSession = subagentSessions.has(input.sessionID)
43+
if (isBackgroundTaskSession) {
44+
return
45+
}
46+
4047
const mainSessionID = getMainSessionID()
4148
const isNonMainSession = mainSessionID && input.sessionID !== mainSessionID
4249

0 commit comments

Comments
 (0)