It’s written by gpt-5.2 pro

Python 중심으로 개발해 온 사람이 opencode 레포를 읽기 시작하면, “TypeScript 문법 자체”보다 **이 레포가 코드를 어떻게 나누고(모듈), 타입을 어떻게 다루고(타입/런타임 경계), 서버와 도구를 어떻게 엮는지(Bun/Hono/Zod)**가 먼저 걸립니다.

이 글은 바로 코드로 들어가기 위한 최소 개념 + 실제 파일 위치를 중심으로 정리한 “온보딩 글”입니다.


0) 먼저 잡아야 할 큰 그림: opencode는 무엇을 하는가?

opencode를 한 문장으로 요약하면 다음이 가장 읽기 편합니다.

  • CLI가 로컬에서 서버를 띄우고
  • 클라이언트(TUI/웹/SDK)가 HTTP API를 호출하며
  • 이벤트는 SSE(Server‑Sent Events) 처럼 스트리밍으로 흘린다

이 큰 그림을 가장 빠르게 보여주는 문서가 specs/project.md입니다.

  • 설계 목표: specs/project.md:3
  • API 스케치: specs/project.md:9

1) “Spec 문서”는 정확히 무엇을 말하나?

specs/project.md는 “현재 구현의 완전한 문서”라기보단, opencode가 지향하는 API/구조를 스케치합니다.

핵심은 이 목표입니다.

  • 단일 opencode 인스턴스가
  • 여러 프로젝트/여러 worktree를 대상으로
  • 세션을 실행/관리할 수 있게 한다 (specs/project.md:3)

그리고 그 목표를 이루기 위해 /project 아래에서 프로젝트/세션/메시지/파일을 다루는 형태로 엔드포인트가 제안됩니다. (specs/project.md:9)


2) API는 어떻게 실행/호출하나?

결론부터 말하면:

  • 서버를 먼저 띄우고
  • 그 서버에 HTTP 요청을 보내면 됩니다

2.1 서버는 어떤 코드가 띄우나?

CLI의 serve 커맨드가 대표적이며, 결국 Server.listen(...)을 호출합니다.

  • serve 커맨드 정의: packages/opencode/src/cli/cmd/serve.ts:5
  • 실제 서버 시작: packages/opencode/src/cli/cmd/serve.ts:11

2.2 기본 host/port는 어디서 정해지나?

네트워크 옵션은 한 곳에서 정리됩니다.

  • 옵션 정의: packages/opencode/src/cli/network.ts:5
  • 기본값: packages/opencode/src/cli/network.ts:13 (hostname=127.0.0.1, port=0)

port=0은 “랜덤 포트(빈 포트를 OS가 선택)”로 이해하면 됩니다.

2.3 웹 UI까지 띄우고 싶다면?

web 커맨드는 서버를 띄운 다음 브라우저를 여는 흐름을 포함합니다.

  • packages/opencode/src/cli/cmd/web.ts:30

3) 서버 코드는 어디서 보고, 어떤 방식으로 구성되나?

처음 읽을 때는 아래 3개만 잡아도 흐름이 보입니다.

  • 메인 서버/라우터: packages/opencode/src/server/server.ts
  • 프로젝트 라우트: packages/opencode/src/server/project.ts
  • TUI 라우트: packages/opencode/src/server/tui.ts

3.1 Hono + Bun 조합

서버는 Hono로 라우팅을 만들고, Bun의 HTTP 서버로 이를 서빙합니다.

  • Server 네임스페이스 시작: packages/opencode/src/server/server.ts:58
  • /project 하위 라우트 마운트: packages/opencode/src/server/server.ts:273
  • OpenAPI 문서 엔드포인트(/doc) 근방: packages/opencode/src/server/server.ts:259
  • 서버 리스닝(listen) 시작: packages/opencode/src/server/server.ts:2792

Hono는 app.fetch 형태의 핸들러를 제공하므로(“fetch 함수처럼 동작”), Bun이 이를 Bun.serve({ fetch: ... })로 바로 붙일 수 있습니다.

3.2 OpenAPI/입력 검증 패턴

이 레포는 “문서화 + 검증”을 코드 레벨에서 같이 가져갑니다.

  • describeRoute(...): 라우트 설명(요약/operationId/응답 스키마 등)
  • validator(...): 요청 파라미터/바디 검증

관련 import를 보면 hono-openapi를 쓰는 것을 알 수 있습니다.

  • packages/opencode/src/server/server.ts:5

4) Python 개발자가 TypeScript 레포를 읽을 때 제일 헷갈리는 지점: “타입은 어디에 존재하나?”

TypeScript에서 가장 중요한 감각은 **“타입은 런타임에 없다”**입니다.

  • 런타임(실제 실행되는 값): const, function, class, z.object(...) 같은 것
  • 타입(컴파일 타임 전용): type, interface, Omit<...>, T extends ... 같은 것

Python에서는 타입 힌트가 런타임 로직과 느슨하게 공존할 수 있지만, TS는 “타입 정보가 빌드 결과물에서 사라진다”는 점이 훨씬 강하게 체감됩니다.

그래서 opencode는 **Zod(런타임 스키마/검증) + TypeScript 타입(정적 타입)**을 함께 씁니다.


5) import가 ../..(상대경로)로 많은 이유와, @/…는 뭔가?

5.1 상대경로 import가 흔한 이유

TS/JS에서 상대경로 import(../x)는 “기본값”에 가깝습니다.

  • 추가 설정 없이도 항상 동작하고
  • 런타임/번들러 차이에 덜 민감합니다

5.2 그런데 opencode는 alias도 같이 쓴다

코드에서 @/…가 보이면, 이건 “npm scope(@opencode-ai/…)”가 아니라 tsconfig path alias입니다.

  • 예: packages/opencode/src/server/server.ts:1

실제로 alias는 패키지의 tsconfig에 정의되어 있습니다.

  • packages/opencode/tsconfig.json:11

6) “절대경로 하나로 통일하면 더 깔끔하지 않나?”에 대한 현실적인 답

Python에서 절대 import는 보통 패키지 구조/런타임 import 시스템이 잘 맞아떨어지는 경우가 많습니다.

반면 TS/JS에서 “절대경로처럼 보이는 import”는 대부분:

  • TypeScript의 compilerOptions.paths
  • 런타임/번들러(Bun, Vite, Webpack 등)의 해석 규칙

이 둘이 동시에 맞아야 합니다.

그래서 많은 코드베이스가 현실적으로:

  • 가까운 곳은 상대경로(설정 의존 ↓)
  • 멀리 건너가는 공용 모듈은 alias(가독성 ↑, ../../.. 지옥 회피)

를 섞어서 씁니다.

VSCode에서 파일 이동 시 import 자동 갱신은?

가능합니다. (TypeScript language service 기능)

  • VSCode 설정: "typescript.updateImportsOnFileMove.enabled": "always"

7) packages/opencode/tsconfig.jsonpaths는 어떻게 읽나?

packages/opencode/tsconfig.json에 이런 설정이 있습니다.

  • "@/*": ["./src/*"]
  • "@tui/*": ["./src/cli/cmd/tui/*"]

의미는 단순합니다.

  • @/xpackages/opencode/src/x
  • @tui/foopackages/opencode/src/cli/cmd/tui/foo

즉, packages/opencode 패키지 안에서 src/를 루트처럼 쓰겠다는 뜻입니다.

주의할 점:

  • pathsTS 컴파일러/IDE가 모듈을 찾는 규칙입니다.
  • 런타임에서도 동일하게 동작하려면, 실행 환경(Bun)이나 빌드가 그 규칙을 동일하게 해석해야 합니다.

8) Bun은 뭐고, import { $ } from "bun"은 뭔가?

8.1 Bun의 역할

Bun은 이 레포에서:

  • 런타임
  • 패키지 매니저
  • 테스트 러너

역할을 동시에 합니다.

  • 테스트: packages/opencode/package.json:10
  • 개발 실행: packages/opencode/package.json:12

8.2 import { $ } from "bun"의 의미

이건 from package import * 같은 “전체 import”가 아니라, named import입니다.

  • Bun이 export하는 것 중 이름이 $인 심볼 “하나만” 가져옵니다.

이 레포에서 $는 주로 **서브프로세스 실행(쉘 커맨드 실행)**에 사용됩니다.

  • 예: packages/opencode/src/tool/bash.ts:11

9) Zod는 왜 쓰나? (타입이 있는데 굳이?)

TypeScript 타입은 런타임에 사라지기 때문에, 외부 입력(HTTP 요청, 파일, 유저 입력)을 다룰 때는 런타임 검증이 필요합니다.

Zod는:

  • 런타임 검증: schema.parse(input)
  • 타입 추론: z.infer<typeof schema>

을 동시에 제공합니다.

opencode의 Tool 시스템은 이 패턴을 표준처럼 사용합니다.

  • Tool 타입 정의: packages/opencode/src/tool/tool.ts:25
  • BashTool 파라미터 스키마: packages/opencode/src/tool/bash.ts:59

10) 왜 packages/opencode/src/tool/.ts.txt가 쌍으로 있나?

packages/opencode/src/tool/를 보면 bash.tsbash.txt 같은 쌍이 반복됩니다.

의도는 명확합니다.

  • .ts: 도구 실행 로직
  • .txt: 도구 설명(긴 프롬프트/문서)

예를 들어 bash 도구는 설명을 텍스트 파일에서 읽어옵니다.

  • .txt import: packages/opencode/src/tool/bash.ts:5
  • description 적용: packages/opencode/src/tool/bash.ts:58

도구들을 모아서 “어떤 도구를 노출할지”를 결정하는 곳은 레지스트리입니다.

  • packages/opencode/src/tool/registry.ts:91

11) 타입이 어렵게 느껴질 때: 이 한 줄을 읽는 방법

실제 코드에 이런 줄이 있습니다.

export type Info = Omit<z.infer<typeof Info>, "template"> & { template: Promise<string> | string }
  • 위치: packages/opencode/src/command/index.ts:41

11.1 단계별로 해석하기

  1. z.infer<typeof Info>
  • “Zod 스키마 Info(런타임 값)로부터 TypeScript 타입을 추론”
  1. Omit<..., "template">
  • 그 타입에서 template만 제거
  1. & { template: Promise<string> | string }
  • template을 원하는 타입으로 다시 붙여서 합성(교차 타입)

11.2 왜 타입을 ‘보정’하나?

같은 파일에 이유가 코멘트로 남아 있습니다.

  • templatez.promise(z.string()).or(z.string())로 정의됨: packages/opencode/src/command/index.ts:32
  • 그런데 추론 결과가 기대와 달라 수동 override: packages/opencode/src/command/index.ts:40

즉, 런타임 검증(Zod)은 유지하면서, TS 타입만 정확히 보정하는 패턴입니다.


12) Info<Parameters extends z.ZodType> 같은 문법(제네릭)은 어떻게 읽나?

Tool 타입 정의를 보면 이런 형태가 있습니다.

  • packages/opencode/src/tool/tool.ts:25

핵심은:

  • Info<...>는 제네릭 타입
  • Parameters extends z.ZodType는 “Parameters는 Zod 스키마 타입이어야 한다”라는 제약

이렇게 해두면, 도구마다 다른 Zod 스키마를 넣어도 execute(args)args 타입이 자동으로 따라옵니다.

  • execute(args: z.infer<Parameters>, ...): packages/opencode/src/tool/tool.ts:30

13) TypeScript의 namespace/class/interface/type/enum은 이 레포에서 어떻게 쓰이나?

“TS 언어 기능”을 전부 공부하기보다, 이 레포에서 실제로 자주 보이는 형태만 잡아도 코드를 읽는 속도가 빨라집니다.

  • module(모듈): 파일 단위 (import/export)
  • namespace: 한 파일 안에서 기능을 묶는 그룹핑
    • Server: packages/opencode/src/server/server.ts:58
    • Tool: packages/opencode/src/tool/tool.ts:6
  • type alias: Omit, & 같은 조합에 자주 사용
    • packages/opencode/src/command/index.ts:41
  • interface: 객체의 “모양”/API 계약 정의
    • Tool.Info: packages/opencode/src/tool/tool.ts:25
  • class: 상태/행동을 가진 유틸
    • AsyncQueue<T>: packages/opencode/src/util/queue.ts:1
  • enum: 고정된 값 집합
    • ApplyPatchError: packages/opencode/src/patch/index.ts:52

Python 관점으로 비유하면:

  • type/interfacetypingTypedDict/Protocol 같은 “정적 계약”에 가깝지만, TS는 런타임에서 사라짐
  • 런타임 검증은 Python의 pydantic처럼, 여기서는 zod가 담당

14) 바로 코드 읽기 시작하는 추천 루트

아래 순서로 열면, opencode의 “서버/도구/검증/alias” 핵심이 빠르게 잡힙니다.

  1. specs/project.md:3
  2. packages/opencode/src/cli/cmd/serve.ts:11
  3. packages/opencode/src/server/server.ts:58
  4. packages/opencode/tsconfig.json:11
  5. packages/opencode/src/tool/tool.ts:25
  6. packages/opencode/src/tool/bash.ts:53
  7. packages/opencode/src/command/index.ts:41