Imported from wkqco33/port_finder (
AGENTS.md). Install upstream withnpx skills add wkqco33/port_finder. Copyright stays with the author.
AGENTS.md β νλ‘μ νΈ κ°λ° κ°μ΄λ
μ΄ λ¬Έμλ poff(port_finder) μ μ₯μμμ μμ νλ μμ΄μ νΈ(μΈκ°/AI λͺ¨λ)κ° μ§μΌμΌ ν ꡬ쑰μ κ·μ½κ³Ό TDD μν¬νλ‘, CLI κ°μ΄λλΌμΈ(clig.dev) μ€μ μ§μΉ¨μ μ μν©λλ€. μ½λλ₯Ό μμ νκ±°λ μΆκ°νκΈ° μ μ λ°λμ μ½κ³ λ°λ₯΄μΈμ.
νλ‘μ νΈ κ°μ
νΉμ ν¬νΈλ₯Ό μ μ ν νλ‘μΈμ€λ₯Ό μ°Ύμ μμ νκ² μ’ λ£νκ³ , AI(Ollama)λ‘ ν¬νΈ μ¬μ© νν©μ λΆμνλ ν¬λ‘μ€νλ«νΌ(Windows/Linux/macOS) CLI λꡬ.
- μΈμ΄: Go 1.25+
- CLI:
github.com/wkqco33/wcli - OS μ°λ:
github.com/shirou/gopsutil/v4(net/process) - LLM ν΄λΌμ΄μΈνΈ:
github.com/wkqco33/LLM_client_go - μΆλ ₯/μ€νμΌ:
github.com/fatih/color
λλ ν°λ¦¬ ꡬ쑰
main.go μ§μ
μ (cmd.Execute() νΈμΆλ§)
cmd/ CLI λ μ΄μ΄ β wcli λͺ
λ Ή μ μ + μ
μΆλ ₯/νλ¦ μ€μΌμ€νΈλ μ΄μ
root.go λ£¨νΈ μ»€λ§¨λ (ν¬νΈ κ²μ/μ’
λ£, --ai, νλκ·Έ λ° μ€νΈλ¦Ό)
config_cmd.go config μλΈμ»€λ§¨λ (show/init/set)
pkg/
port/ λλ©μΈ λ‘μ§ β ν¬νΈ μ‘°ν/νλ‘μΈμ€ μ’
λ£ (gopsutil 격리)
ai/ AI λ‘μ§ β Ollama/LLM ν¬νΈ λΆμ λ° ν둬ννΈ λΉλ
config/ μ€μ κ΄λ¦¬ β XDG κ·μ½/νκ²½λ³μ/λ κ±°μ νμΌ λ‘λ/μ μ₯
.github/workflows/ CI κ²μ΄νΈ (ci.yml) + λ¦΄λ¦¬μ€ (release.yml)
Taskfile.yml λΉλ/ν
μ€νΈ νμ€ν¬ λνΌ
μν€ν μ² κ·μΉ (λ°λμ μ€μ)
1. μμ‘΄μ± λ°©ν₯
main.go β cmd β pkg/port, pkg/ai, pkg/config.
- νμ κ³μΈ΅(pkg/*)μ΄ μμ κ³μΈ΅(cmd)μ μ λ μ°Έμ‘°νμ§ μλλ€.
pkg/λ΄λΆ ν¨ν€μ§ κ°μλ μν μμ‘΄μ±μ λ§λ€μ§ μλλ€.
2. OS λ° μΈλΆ SDK 격리
- gopsutil κ°μ OS λΌμ΄λΈλ¬λ¦¬ importλ
pkg/portμΈλΆ μ΄λμλ λμ§ λ§μΈμ. - LLM ν΄λΌμ΄μΈνΈ SDK importλ
pkg/aiμΈλΆ μ΄λμλ λμ§ λ§μΈμ. cmdλ μ€μ§pkg/κ° λ ΈμΆν μΈν°νμ΄μ€μ νμ λ§ μ¬μ©ν©λλ€.
3. μμ‘΄μ± μ£Όμ β TDD μ μ
λͺ¨λ ν¨ν€μ§λ λ¨μ ν μ€νΈμμ μΈλΆ λΆμν¨κ³Ό μμ΄ κ²°μ μ μΌλ‘ μ€νλ μ μλλ‘ μμ‘΄μ± μ£Όμ (DI)μ μ§μν΄μΌ ν©λλ€:
pkg/port:Finderꡬ쑰체 +Optionν¨μ μ΅μ port.NewFinder(port.WithConnectionSource(fake), port.WithProcessSource(fake))Process,ConnectionSource,ProcessSourceμΈν°νμ΄μ€λ₯Ό ν΅ν΄μλ§ OSμ μ κ·Ό.
pkg/ai:Analyzerꡬ쑰체 +Optionν¨μ μ΅μ ai.NewAnalyzer(ai.WithChatClient(fake), ai.WithModel(...), ai.WithBaseURL(...))ChatClientμΈν°νμ΄μ€λ₯Ό ν΅ν΄ μ€μ λ€νΈμν¬ ν΅μ μμ΄ κ°μ μλ΅ ν μ€νΈ.
pkg/config: μμ ν¨μ + κ²½λ‘ μ£ΌμResolvePath(homeDirFunc): κ°μ ν λλ ν°λ¦¬ ν¨μλ₯Ό μ£Όμ νμ¬ XDG λ° λ κ±°μ κ²½λ‘ κ²μ¦.- νμΌ IOλ
t.TempDir()λ‘ κ²©λ¦¬νμ¬ ν μ€νΈ.
cmd:Appλ°cfgAppꡬ쑰체μ μ€νΈλ¦Ό, ops μΈν°νμ΄μ€, TTY ν¨μ μ£Όμ- νλ:
Out,ErrW io.Writer,In io.Reader,Ops portOps,AI aiOps,IsTTY func() bool - μ½λ μμμ
os.Stdout/os.Stderr/os.Stdinμ μ§μ μ°μ§ λ§ κ². νμa.Out/a.ErrW/a.Inμ μ¬μ©. fmt.Println/fmt.Printfλ μ λ μ¬μ© κΈμ§ βfmt.Fprintln(a.Out, ...)/fmt.Fprintf(a.Out, ...)μ¬μ©.
- νλ:
4. CLI κ°μ΄λλΌμΈ μ€μ (clig.dev)
- μΆλ ₯ μ€νΈλ¦Ό μ격 λΆλ¦¬:
- κ²°κ³Ό λ°μ΄ν°λ
a.Out(stdout)μΌλ‘λ§ μΆλ ₯. - μ§ν μν(
π...,μ€μΊ μ€...), κ²½κ³ , μλ¬λa.ErrW(stderr)λ‘ μΆλ ₯. --jsonλͺ¨λλ-q/--quietλͺ¨λμμλ μ§ν λ©μμ§λ₯Ό μΆλ ₯νμ§ μμ.--jsonμΆλ ₯ μ stdoutμλ μ€μ§ μ ν¨ν JSONλ§ μΆλ ₯λμ΄μΌ νλ©°, κ²μ κ²°κ³Όκ° μμ λλ[]λΉ λ°°μ΄μ μΆλ ₯νμ¬ νμ΄νλΌμΈ(jqλ±)μ΄ κΉ¨μ§μ§ μκ² ν¨.
- κ²°κ³Ό λ°μ΄ν°λ
- λνν/λΉλνν λ°©μ΄:
- νκ΄΄μ μμ
(νλ‘μΈμ€ kill) νμΈ μ μ λ°λμ
isTTYμNoInputμ¬λΆλ₯Ό κ²μ¬. - λΉλνν νκ²½(νμ΄ν, CI)μμλ
--force(-f)λλ--yes(-y)νλκ·Έκ° μμΌλ©΄ μ¦μ μ’ λ£ μ½λ3(ExitCodePromptRequired)μΌλ‘ μ€ν¨ μ²λ¦¬.
- νκ΄΄μ μμ
(νλ‘μΈμ€ kill) νμΈ μ μ λ°λμ
- νμ€ νλκ·Έ μ§μ:
-h/--help,-v/--version,-f/--force,-y/--yes,-n/--dry-run,-q/--quiet,--no-input,--no-color
- νμ€ μ’
λ£ μ½λ 체κ³:
ExitCodeSuccess = 0: μ±κ³΅ExitCodeError = 1: μΌλ° μ€ν/μ’ λ£ μ€ν¨ExitCodeUsage = 2: CLI μΈμ/νλκ·Έ νμ± μ€ν¨ExitCodePromptRequired = 3: λΉλνν νκ²½μμ λνν μ λ ₯ νμ
ν μ€νΈ κ·μΉ (TDD)
ν μ€νΈ κ³μΈ΅ κ΅¬λΆ β λΉλ νκ·Έλ‘ λλλ€
| νμΌ | νκ·Έ | μ±κ²© |
|---|---|---|
pkg/port/port_unit_test.go |
//go:build !integration |
hermetic λ¨μ ν μ€νΈ (νμ΄ν¬ μ£Όμ ). κΈ°λ³Έ μ€ν. |
pkg/port/port_test.go |
//go:build integration |
ν΅ν© ν μ€νΈ (μ€μ μμΌ/OS νλ‘μΈμ€). λ³λ μ€ν. |
pkg/ai/ai_test.go |
(μμ) | hermetic λ¨μ ν μ€νΈ (νμ΄ν¬ ChatClient). |
pkg/ai/prompt_test.go |
(μμ) | μμ λ¨μ ν μ€νΈ (ν둬ννΈ λΉλ). |
pkg/ai/ai_integration_test.go |
//go:build integration |
ν΅ν© ν μ€νΈ (λ‘컬 Ollama μ°κ²°, λ―Έμ€ν μ Skip). |
pkg/config/config_test.go |
(μμ) | hermetic λ¨μ ν μ€νΈ (TempDir νμΌ IO). |
cmd/root_test.go |
(μμ) | hermetic λ¨μ ν μ€νΈ (νμ΄ν¬ ops/AI + λ²νΌ μ€νΈλ¦Ό). |
cmd/config_cmd_test.go |
(μμ) | hermetic λ¨μ ν μ€νΈ (νμ΄ν¬ κ²½λ‘ + λ²νΌ μ€νΈλ¦Ό). |
- κΈ°λ³Έ
go test ./...λ λ°λμ λΉ λ₯΄κ³ (λͺ©ν < 1s), OS λΉμμ‘΄, κ²°μ μ μ΄μ΄μΌ ν©λλ€.- μ€μ λ€νΈμν¬λ₯Ό μ΄κ±°λ OS νλ‘μΈμ€λ₯Ό λμ°λ ν
μ€νΈλ₯Ό κΈ°λ³Έ μ€μνΈμ λ£μ§ λ§μΈμ β
integrationνκ·Έλ‘ λΆλ¦¬. time.SleepμΌλ‘ "λ°μΈλ© λκΈ°" κ°μ λΉκ²°μ μ λκΈ°λ₯Ό μ°μ§ λ§μΈμ β νμ΄ν¬λ‘ λ체.
- μ€μ λ€νΈμν¬λ₯Ό μ΄κ±°λ OS νλ‘μΈμ€λ₯Ό λμ°λ ν
μ€νΈλ₯Ό κΈ°λ³Έ μ€μνΈμ λ£μ§ λ§μΈμ β
- ν΅ν© ν
μ€νΈλ
go test -tags integration ./...λ‘ μ€νν©λλ€.
νλ‘μΈμ€: Red β Green β Refactor
- λ¨Όμ μ€ν¨νλ ν
μ€νΈλ₯Ό μμ± (
Red). - μ΅μνμ ꡬνμΌλ‘ ν΅κ³Ό (
Green). - μμ‘΄μ±/μΈν°νμ΄μ€λ₯Ό κ°μ νλ©° 리ν©ν° (
Refactor). - ν μ€νΈμ νλ‘λμ μ½λλ₯Ό κ°μ 컀λ°μ ν¬ν¨νλΌ.
ν μ€νΈ μμ± κ·μΉ
- ν
μ€νΈ ν¨μλͺ
:
Test<κΈ°λ₯>_<μλ리μ€>(μ:TestFindByPortRange_Sorted). - μμ ν¨μλ ν
μ΄λΈ λ리λΈ(
tests := []struct{...}+t.Run)μΌλ‘. - νμ΄ν¬λ ν μ€νΈ νμΌ μμ μ μνκ³ , μκ²(μΈν°νμ΄μ€ λ§μ‘± μ΅μ) μ μ§.
- μμ‘΄μ± νμ΄ν¬ μ£Όμ μ ν¨μ μ΅μ λλ ꡬ쑰체 νλλ₯Ό ν΅ν΄ μν.
μ€ν λͺ λ Ή (Taskfile)
task test # hermetic λ¨μ ν
μ€νΈ (λΉ λ¦, < 1s)
task test:integration # μ€μ OS ν΅ν© ν
μ€νΈ
task test:all # λ λ€ μ€ν
task test:coverage # 컀λ²λ¦¬μ§ μΈ‘μ
CI κ²μ΄νΈ (.github/workflows/ci.yml)
- PR λ° main/master νΈμ μ:
gofmtν¬λ§· κ²μ¦ βgo vetμ μ λΆμ β λΉλ β-raceλ¨μ ν μ€νΈ(컀λ²λ¦¬μ§ μμ½) β ν΅ν© ν μ€νΈ (go test -tags integration ./...). - 릴리μ€λ λ³λ
release.ymlβ λ²μ νκ·Έ(v*) νΈμ μ νΈλ¦¬κ±°λμ΄ ppm λ°°ν¬ κ·μ½({bin_name}_{os}_{arch}.tar.gz|.zip)μ λ§λ μμΉ΄μ΄λΈμ.sha256체ν¬μ¬μ μμ±νλ€.
μ½λ μ€νμΌ
- Go νμ€
gofmt/go vetν΅κ³Όκ° κΈ°λ³Έ. - μ£Όμμ νκ΅μ΄λ‘ μμ± (κΈ°μ‘΄ μ½λ κ΄λ‘ λ°λ¦).
- μλ¬λ
fmt.Errorf("...: %w", err)λ‘ λν(barefmt.Errorfμ¬μ© κΈμ§). - λͺ¨λ κ³΅κ° ν¨ν€μ§μλ ν¨ν€μ§ λ 벨 μ£Όμ(
// Package ...)μ μμ±. - λΉλ νκ·Έ μ£Όμμ νμΌ μ΅μλ¨μ λͺ©μ μ μ€λͺ .
λ³κ²½ μ λ°λμ νμΈ
go build ./... # μ»΄νμΌ
go vet ./... # μ μ λΆμ
task test # hermetic λ¨μ ν
μ€νΈ
task test:integration # OS μμ‘΄ ν΅ν© ν
μ€νΈ (μ€μ νκ²½ νμΈ)