返回更新列表
新发布Sep 4, 2026

sonar v0.4.1

用于检查和管理监听本地主机端口的服务的CLI工具

分享
██████    ███    ██████    ██    ██████
██           ██      ██  ██  ██      ██
██      ██   ██  ██  ██  ██  ██      ██
  ██  ██   ██  ██  ██  ██  ██  ████
    ██  ██       ██  ██  ██  ██  ██  ██
██████    ███    ██  ██  ██  ██  ██  ██

了解你的机器上正在运行什么。

Sonar 会显示所有在 localhost 上监听的内容,并将其整理得井井有条:每个端口 都属于一个组——通常是它启动时所在的仓库——而在该组内部, 则属于一个命名的服务。使用 sonar start 启动你的开发服务器, 整个项目就变成一个整体,你可以将其作为树状结构列出、 等待、跟踪日志,并用一条命令停止。Docker 容器、Compose 项目以及你手动启动的进程也会被自动识别,无需任何 配置。

$ sonar list --tree
my-app  (3 ports, running)                        ~/code/my-app
├─ 5432  db          postgres:17                  http://localhost:5432
├─ 5173  frontend    vite (v5.4)                  http://localhost:5173
└─ 8000  api         uvicorn app:app              http://localhost:8000
ungrouped (1 port)
└─ 3000  next-server (v16.1.6)                    http://localhost:3000

安装

Homebrew (macOS / Linux)

brew install raskrebs/sonar/sonar

Homebrew 6 会拒绝来自第三方 tap 的 formula,直到你信任该 tap 一次 (Error: Refusing to load formula raskrebs/sonar/sonar from untrusted tap):

brew trust raskrebs/sonar

安装脚本

curl -sfL https://raw.githubusercontent.com/raskrebs/sonar/main/scripts/install.sh | bash

下载最新二进制文件到 ~/.local/bin,并在需要时将其添加到你的 PATH 中。重启终端或执行 source ~/.zshrc。

在 Windows 上(PowerShell):

irm https://raw.githubusercontent.com/raskrebs/sonar/main/scripts/install.ps1 | iex

自定义安装位置:

curl -sfL https://raw.githubusercontent.com/raskrebs/sonar/main/scripts/install.sh | SONAR_INSTALL_DIR=/usr/local/bin bash

安装特定版本:

curl -sfL https://raw.githubusercontent.com/raskrebs/sonar/main/scripts/install.sh | SONAR_VERSION=vX.Y.Z bash
$env:SONAR_VERSION="vX.Y.Z"; irm https://raw.githubusercontent.com/raskrebs/sonar/main/scripts/install.ps1 | iex

使用 Go

go install github.com/raskrebs/sonar@latest

Shell 补全(端口号 Tab 补全):

sonar completion zsh > "${fpath[1]}/_sonar"   # zsh
sonar completion bash > /etc/bash_completion.d/sonar  # bash
sonar completion fish | source                 # fish

六十秒

在仓库根目录下用 sonar.yaml 描述你的项目如何运行, 以替代 dev.sh:

name: my-app
services:
  - name: db
    cmd: docker compose up db
    port: 5432
  - name: api
    cmd: uv run uvicorn app:app --port ${port}
    port: auto
    depends_on: [db]
  - name: frontend
    cmd: npm run dev -- --port ${port} --strictPort
    port: auto
    depends_on: [api]
    env:
      VITE_API_URL: ${api.url}

然后启动它:

sonar start
  ✓ db        http://localhost:5432   pid 41022  ~/.config/sonar/logs/my-app/db.log
  ✓ api       http://localhost:21408  pid 41040  ~/.config/sonar/logs/my-app/api.log
  ✓ frontend  http://localhost:21409  pid 41077  ~/.config/sonar/logs/my-app/frontend.log

3 started
following the logs; Ctrl+C stops the 3 services started here
api      | INFO:     Uvicorn running on http://127.0.0.1:21408
frontend | VITE v5.4  ready in 312 ms

Sonar 会为每个 port: auto 服务挑选一个空闲端口,并告知每个服务其他服务的位置,因此第二个工作树可以与第一个并行运行,而不会发生任何端口冲突。sonar start -d 在后台执行相同的操作,而 sonar down 会停止它并归还端口。在另一个终端中:

sonar list --tree
my-app  (3 ports, running)                        ~/code/my-app
├─ 5432   db          postgres:17                 http://localhost:5432
├─ 21408  api         uvicorn app:app             http://localhost:21408
└─ 21409  frontend    vite (v5.4)                 http://localhost:21409

没有 sonar.yaml?sonar start -- npm run dev 会将一条命令作为命名服务运行,而 sonar list 和 sonar kill 可作用于任何正在监听的对象。

下面标记为 # check 的示例会在每次 CI 运行时,由 scripts/readme-check.sh 针对全新构建执行。

命令

sonar list

sonar list
sonar list --tree
sonar list --group my-app
sonar list --json
# check
sonar list --stats             # CPU, memory, threads, uptime, state
sonar list --health            # HTTP health checks
sonar list --filter docker     # only Docker ports
sonar list --sort name         # port | pid | name | type
sonar list -a                  # include desktop apps
sonar list -c port,process,group,cpu,mem
sonar list --host user@server  # scan a remote machine over SSH

默认列是 port、process、group、container、image、 containerport、url,其中 process 显示你为端口指定的名称 (sonar rename),然后是服务名称,最后是检测到的内容。

可用列:port、process、pid、type、url、group、cpu、 mem、threads、uptime、state、connections、health、latency、 container、image、containerport、compose、project、user、bind、 ip。

恰好正在监听的桌面应用和系统服务——Figma、Discord、 Spotify、ControlCenter、macOS .app 包、/System/Library/ 守护进程—— 除非你传入 -a,否则会被隐藏。

sonar start

从项目的 sonar.yaml 启动项目:

sonar start                    # every service in the nearest sonar.yaml
sonar start api frontend       # only these (they still wait for their dependencies)
sonar start ../other-project   # the sonar.yaml in or above another directory
sonar start -d                 # in the background, like `sonar up`

在前台,sonar 通过守护进程启动服务,在每行日志前加上服务名称来跟踪其日志,当你按下 Ctrl+C 时,它会停止自己启动的服务。已经在运行的服务保持不变,一旦 sonar 启动的每个服务都退出,sonar 会自行返回。

-- 之前的参数指定一个目录或服务;-- 之后的所有内容都是一个命令。或者将一条命令作为组中的命名服务运行:

sonar start -- npm run dev
sonar start --group my-app --name frontend -- npm run dev
sonar start --port 5173 -- npm run dev        # expected port, before it binds
sonar start --detach --name api -- uv run uvicorn app:app
sonar start --list

无需传入任何参数:

  • Group — --group,否则取最近的 sonar.yaml 中的 name,否则取 git 根目录的目录名(工作树会变成 repo@worktree),否则取当前目录 的名称。
  • Name — --name,否则取 sonar.yaml 中 cmd 匹配的服务,否则 从命令推断(npm run dev → dev,uv run api → api, python -m uvicorn → uvicorn,./dev.sh → dev.sh)。
  • Port — --port 只是一个提示,并非绑定:在端口实际监听之前, 运行状态会显示为 starting,守护进程用它来将进程与端口进行匹配。

子进程继承 stdin、stdout、stderr、cwd 和环境变量,外加 SONAR_GROUP、SONAR_NAME 和 SONAR_RUN_ID。它会获得自己的进程组, 因此 sonar kill 会终止整棵进程树——包括开发服务器及其监视器和 工作进程。Ctrl+C 会被转发,sonar 以子进程的退出码退出。

--detach 会立即返回,并将输出写入 ~/.config/sonar/logs/<group>/<name>.log。--list 显示 sonar 启动了什么, 并在其下方显示此后已结束的内容——包括退出码,以及它是崩溃了 还是被停止了(--json 用于机器可读形式):

sonar start --list
sonar start --detach --name demo --port 8123 -- sleep 5
sonar start --list --json
# check

sonar.yaml

项目在仓库根目录的 sonar.yaml 中为自身及其服务命名。该文件是可选的——即使没有它,sonar 也会按 git 根目录进行分组——并且它应当被提交:

name: my-app
services:
  - name: db
    cmd: docker compose up db
    port: 5432
    health: /
    description: Postgres 17
    icon: database
    color: "#4f8cc9"
  - name: api
    cmd: uv run uvicorn app:app --port 8000
    cwd: backend
    port: 8000
    health: /healthz
    depends_on: [db]
  - name: frontend
    cmd: npm run dev
    port: 5173
    depends_on: [api]
ports: [9229]        # ports that belong to this project without a service
  • name — 组名。不能包含斜杠,不能包含空白字符。
  • cmd、cwd、port — sonar start 如何启动服务。cwd 相对于该文件,且不能逃逸其所在目录。
  • health — 服务运行期间守护进程轮询的 HTTP 路径,因此服务可能处于运行中但尚未健康。它会报告 ok、fail 或 unknown,并附带失败原因。
  • description、icon、color — 桌面应用的自由格式元数据;sonar 从不推断它们。
  • depends_on — 启动顺序。指定文件中不存在的服务,或形成循环,都是错误;无效文件只报告一次,且绝不会阻止扫描。
  • env — sonar 启动服务时为该服务设置的环境变量。

port: auto 与引用

服务可以将端口交给 sonar 决定,并通过其环境变量找到其他服务,而不是硬编码 localhost:8080:

services:
  - name: api
    cmd: uv run uvicorn app:app --port ${port}
    port: auto
  - name: frontend
    cmd: npm run dev -- --port ${port} --strictPort
    port: auto
    depends_on: [api]
    env:
      VITE_API_URL: ${api.url}

sonar start 会为它启动的每个 port: auto 服务占用一个空闲端口:在同一个检出中每次都是同一个端口,而在其他每个检出中都是不同的端口,因此工作树永远不会与主检出冲突。服务会以 PORT、SONAR_PORT 和 ${port} 的形式获得该端口,并且必须使用它——读取 PORT,或在命令行中传递 ${port}。已经在运行的服务会保留其端口,因此稍后启动的服务仍然能找到它。已分配端口背后的占用从最后一次启动起持续一天;claims.ttl 可以改变这一点(参见 The daemon)。

${port} 和 ${url}(http://localhost:<port>)是服务自身的;${<service>.port} 和 ${<service>.url} 是另一个服务的。它们可用于 cmd 和 env 中。引用文件中不存在的服务,或引用没有端口的服务,会在加载文件时报错。${ 和 } 之间的其他内容,如 ${HOME},会完全按原样保留。

分类