OpenCode 기반으로 사내 AI 코딩 어시스턴트를 개발하던 중 마주친 에러다. 커스터마이즈를 마치고 Custom Tool까지 만들었는데, 실행하자마자 이런 메시지가 떴다. 같은 상황이라면 이 글이 도움이 될 것이다.
error: Cannot find module '@opencode-ai/plugin' from
'C:\Users\username\.config\opencode\tools\my-tool.ts'
package.json에 "@opencode-ai/plugin": "latest"로 바꿔봐도, 재시작하면 또 이상한 버전으로 돌아오고, 그렇게 한참 삽질했다. 결론부터 말하면 빌드 스크립트 한 줄로 해결됐다.
1. 무슨 일이 벌어지고 있는가
OpenCode를 시작할 때 config.ts 안의 installDependencies 함수가 실행된다.
export async function installDependencies(dir: string) {
const targetVersion = Installation.isLocal() ? "*" : Installation.VERSION
json.dependencies = {
...json.dependencies,
"@opencode-ai/plugin": targetVersion, // 시작할 때마다 덮어씀
}
await Filesystem.writeJson(pkg, json)
// bun install 실행
}
package.json을 시작할 때마다 강제로 덮어쓰고 bun install을 실행한다. 그래서 아무리 수동으로 버전을 고쳐도 소용이 없었던 것이다.
targetVersion이 뭔지 타고 올라가면 installation.ts에 닿는다.
export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local"
export const CHANNEL = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local"
export function isLocal() {
return CHANNEL === "local"
}
OPENCODE_VERSION과 OPENCODE_CHANNEL은 빌드 시 실행 파일에 박히는 전역 상수다. 그렇다면 이 값은 어디서 결정되는가?
2. 범인은 git 브랜치명
packages/script/src/index.ts를 보면 답이 있다.
const CHANNEL = await (async () => {
if (env.OPENCODE_CHANNEL) return env.OPENCODE_CHANNEL // 환경변수 있으면 사용
if (env.OPENCODE_BUMP) return "latest"
return await $`git branch --show-current`.text().then((x) => x.trim())
// ↑ 환경변수 없으면 현재 git 브랜치명을 채널로 사용
})()
const VERSION = await (async () => {
if (env.OPENCODE_VERSION) return env.OPENCODE_VERSION
if (IS_PREVIEW) return `0.0.0-${CHANNEL}-${날짜}` // "latest"가 아니면 이 형식
// ...
})()
OPENCODE_CHANNEL 환경변수를 설정하지 않으면, 현재 git 브랜치명이 채널 값이 된다.
브랜치명이 feat/something이었다면 빌드 결과는 이렇게 된다.
CHANNEL = "feat/something"
VERSION = "0.0.0-feat/something-202503060904"
그리고 isLocal()은 CHANNEL === "local"을 확인하므로 false가 된다.
const targetVersion = Installation.isLocal() ? "*" : Installation.VERSION
// false "0.0.0-feat/something-..."
결국 npm 공개 레지스트리에 존재하지 않는 버전으로 package.json을 덮어쓰고, bun install이 실패하면서 @opencode-ai/plugin을 찾지 못하게 된다.
2-1. 전체 흐름 정리
빌드 시
└── OPENCODE_CHANNEL 환경변수 없음
└── 현재 git 브랜치명 → "feat/something"
└── VERSION = "0.0.0-feat/something-..." (실행 파일에 박힘)
실행 시
└── installDependencies()
└── isLocal() → "feat/something" ≠ "local" → false
└── targetVersion = "0.0.0-feat/something-..."
└── package.json 덮어씀
└── bun install → npm에 없는 버전 → 설치 실패
└── @opencode-ai/plugin 없음 → 에러
3. 해결 방법
isLocal()이 true를 반환하면 targetVersion이 "*"(npm 최신버전)이 된다. 즉, 빌드 시 OPENCODE_CHANNEL=local을 주입하면 된다.
packages/opencode/package.json의 build 스크립트를 이렇게 수정한다.
"scripts": {
"build": "OPENCODE_CHANNEL=local bun run script/build.ts"
}
이제 bun run build를 실행하면 실행 파일에 CHANNEL = "local"이 박히고, 사용자가 도구를 시작할 때 @opencode-ai/plugin npm 최신버전이 정상 설치된다.
빌드 시
└── OPENCODE_CHANNEL=local
└── CHANNEL = "local" (실행 파일에 박힘)
실행 시
└── installDependencies()
└── isLocal() → "local" === "local" → true
└── targetVersion = "*"
└── bun install → npm 최신버전 설치 성공
└── @opencode-ai/plugin 정상 로드
└── Custom Tool 동작
4. 느낀점
솔직히 처음엔 단순한 패키지 설치 문제인 줄 알았다. package.json 버전 바꾸면 되겠지 했는데, 바꿔도 바꿔도 원래대로 돌아오니까 점점 이상하다는 생각이 들었다. 에러 메시지만 보고 표면적인 해결책만 찾다가 시간을 꽤 날렸다. 결국 소스코드를 직접 파고들어서 config.ts → installation.ts → script/index.ts 순서로 추적했을 때 "아, 이래서 그랬구나" 하는 순간이 왔는데, 그 느낌이 꽤 재밌었다. 오픈소스를 그냥 가져다 쓰는 게 아니라 내부 동작 방식을 이해하면서 쓰게 됐고, CI에서는 환경변수로 버전을 명시적으로 관리한다는 것도 덤으로 배웠다. 에러 하나를 제대로 쫓아가면 생각보다 많은 걸 배울 수 있다는 걸 다시 한번 느꼈다.
5. 마무리
OpenCode 공식 빌드는 CI에서 OPENCODE_CHANNEL 환경변수를 항상 명시적으로 설정한다. 그래서 원본 프로젝트는 이 문제가 없다. 하지만 포크해서 로컬 빌드를 할 때는 이 환경변수가 없으면 git 브랜치명이 채널로 들어가고, 그 버전이 npm에 없어서 의존성 설치가 조용히 실패한다.
OpenCode를 포크해서 빌드한다면 OPENCODE_CHANNEL=local은 필수다.
'트러블 슈팅' 카테고리의 다른 글
| 폐쇄망에서의 Jenkins CI/CD 구축 삽질 기록 (0) | 2026.04.09 |
|---|---|
| 사내 AI 도구 배포 시스템 구축기 — 브라우저 캐시와 행망 보안이 설치를 망쳤다 (0) | 2026.04.09 |
| Docker + Bash + Python으로 이기종 DB 간 데이터 이관 자동화하기 (0) | 2026.03.16 |
| Python + Inno Setup으로 사내 AI 도구 배포 자동화하기 (0) | 2026.03.11 |
| RTR을 활용한 토큰 탈취 안정성에 대한 나의 생각 (2) | 2024.12.18 |