Skip to content

Architecture

Layers

flowchart TD
    main["cmd/portcheck<br/><small>main: os.Exit(cli.Main(args))</small>"]
    cli["internal/cli<br/><small>cobra commands · flags · exit codes</small>"]
    out["internal/output<br/><small>table · plain · csv · json</small>"]
    tui["internal/tui<br/><small>Bubble Tea model</small>"]
    bi["internal/buildinfo<br/><small>version · commit · date</small>"]
    proj["internal/project<br/><small>.portcheck.yaml</small>"]
    portspec["pkg/portspec"]
    inspect["pkg/inspect"]
    freeport["pkg/freeport"]
    killer["pkg/killer"]
    scan["pkg/scan"]
    waitfor["pkg/waitfor"]
    containers["pkg/containers"]
    wslpkg["pkg/wsl"]
    os[("OS: libproc · /proc · IP Helper API<br/>signals · sockets · DNS")]

    main --> cli
    cli --> out & tui & bi & proj
    out --> bi
    cli --> portspec & inspect & freeport & killer & scan & waitfor & containers
    containers --> inspect
    cli --> wslpkg
    wslpkg --> inspect
    tui --> inspect & killer
    out --> inspect & freeport & scan
    freeport --> inspect & portspec
    inspect --> portspec
    scan --> portspec
    inspect -. "Provider (gopsutil)" .-> os
    killer -. "ops (gopsutil)" .-> os
    freeport -. "bind" .-> os
    scan -. "Dialer / Resolver" .-> os
    waitfor -. "Dialer" .-> os
    containers -. "Engine API (socket / pipe)" .-> os
    wslpkg -. "wsl.exe" .-> os

Dependencies point down only. pkg/* is the public library and must not import internal/*, cobra or the Charm libraries; depguard in .golangci.yml enforces this.

The VS Code extension in extensions/vscode/ sits outside this diagram: it runs the built portcheck binary with --json and never imports Go code (ADR-0004).

Packages

Package Responsibility OS boundary (faked in tests)
pkg/portspec Parse, normalize and iterate port sets none
pkg/inspect Filter, de-duplicate, enrich and sort sockets Provider: gopsutil (ADR-0001); on macOS libproc via purego (ADR-0003)
pkg/freeport Free = no visible listener and bindable on 4 addresses Lister, bind func
pkg/killer Guards → SIGTERM → wait → SIGKILL ops interface
pkg/scan Target expansion, worker pool, rate limit, classification Dialer, Resolver
pkg/waitfor Poll a condition until it holds or the context ends Check, Dialer
pkg/containers Map published ports to containers; implements inspect.Enricher Engine API over a socket (faked with an HTTP server in tests)
pkg/wsl Map WSL-relayed ports to distros and Linux processes; implements inspect.Enricher wsl.exe (faked with captured output in tests)
internal/output Render a Document (JSON payload + toned rows) in 4 formats writer
internal/project Find, parse (strict) and validate .portcheck.yaml filesystem
internal/buildinfo Version, commit and date from -ldflags or Go's embedded build info debug.ReadBuildInfo
internal/tui Pure model; I/O happens in tea.Cmds Lister, Killer
internal/cli Arguments → library calls → Document → exit code App struct

Data flow: portcheck kill 3000

sequenceDiagram
    actor U as User
    participant C as cli
    participant I as inspect
    participant K as killer
    participant O as output
    U->>C: portcheck kill 3000
    C->>I: List(Ports=3000, ListeningOnly)
    I-->>C: sockets with PIDs
    C->>K: Check(pid) (guards, before prompting)
    C->>U: About to terminate … Proceed? [y/N]
    U->>C: y
    C->>K: Kill(pid, Grace=5s)
    K->>K: SIGTERM, poll until exit or grace, then SIGKILL
    K-->>C: Result{Outcome: terminated}
    C->>O: KillDoc(results)
    O-->>U: table / json on stdout
    C-->>U: exit code 0

Design principles

  • Every OS call sits behind a small interface. This keeps business logic deterministic and testable without root, real processes or a network; real behavior is covered separately by integration tests on all three OSes.
  • Commands are thin. They parse flags, call pkg/*, build an output.Document and map errors to exit codes. They contain no business logic.
  • One Document, four formats. Commands never print directly, so table, plain, CSV and JSON output can't drift apart. JSON follows a versioned schema.
  • Errors are sentinel-wrapped (inspect.ErrAccessDenied, killer.ErrProtected, scan.ErrTooManyHosts, …), so callers and the CLI branch with errors.Is.
  • Safe defaults: confirmation before killing, guards that run before prompting, rate-limited scans and host caps.

Testing strategy

See Development & testing.