内容
<div align='center'>
<p>
<img src="https://raw.githubusercontent.com/boshold/zaps/main/assets/banner.png" alt="ZAPS" width="600">
<p>
<a href="https://www.npmjs.com/package/@bosdev/zaps">
<picture>
<source media="(prefers-color-scheme: dark)" type="image/svg+xml" srcset="https://img.shields.io/npm/v/@bosdev/zaps?logo=npm&label=npm&color=f38ba8&labelColor=313244&logoColor=f38ba8">
<img alt="npm version" src="https://img.shields.io/npm/v/@bosdev/zaps?logo=npm&label=npm&color=d20f39&labelColor=ccd0da&logoColor=d20f39"/>
</picture>
</a>
<a href="https://nodejs.org">
<picture>
<source media="(prefers-color-scheme: dark)" type="image/svg+xml" srcset="https://img.shields.io/node/v/@bosdev/zaps?logo=nodedotjs&label=node&color=a6e3a1&labelColor=313244&logoColor=a6e3a1">
<img alt="Node engine" src="https://img.shields.io/node/v/@bosdev/zaps?logo=nodedotjs&label=node&color=40a02b&labelColor=ccd0da&logoColor=40a02b"/>
</picture>
</a>
<a href="https://github.com/boshold/zaps/actions/workflows/ci.yml">
<picture>
<source media="(prefers-color-scheme: dark)" type="image/svg+xml" srcset="https://img.shields.io/github/actions/workflow/status/boshold/zaps/ci.yml?branch=main&logo=githubactions&label=ci&color=89b4fa&labelColor=313244&logoColor=89b4fa">
<img alt="CI status" src="https://img.shields.io/github/actions/workflow/status/boshold/zaps/ci.yml?branch=main&logo=githubactions&label=ci&color=1e66f5&labelColor=ccd0da&logoColor=1e66f5"/>
</picture>
</a>
<a href="https://github.com/boshold/zaps/blob/main/LICENSE">
<picture>
<source media="(prefers-color-scheme: dark)" type="image/svg+xml" srcset="https://img.shields.io/github/license/boshold/zaps.svg?color=cba6f7&labelColor=313244">
<img src="https://img.shields.io/github/license/boshold/zaps.svg?color=8839ef&labelColor=ccd0da" alt="MIT License"/>
</picture>
</a>
<a href="https://github.com/tmux/tmux/wiki#-is-awesome">
<picture>
<source media="(prefers-color-scheme: dark)" type="image/svg+xml" srcset="https://img.shields.io/badge/%3E%3D3.5a-94e2d5?logo=tmux&label=tmux&labelColor=313244&logoColor=94e2d5">
<img alt="Tmux >= 3.5a" src="https://img.shields.io/badge/%3E%3D3.5a-179299?logo=tmux&label=tmux&labelColor=ccd0da&logoColor=179299">
</picture>
</a>
</p>
<hr>
<p>
<h3>⚡ 无痛项目设置 ⚡</h3>
<div>零痛苦设置 - 一个终端会话管理器,在 tmux 中协调开发服务,并带有交互式 TUI。</div>
<div>在单个配置文件中定义服务、依赖项、任务和布局。ZAPS 处理 tmux 窗格布局、服务生命周期、崩溃恢复和键盘驱动的仪表板。</div>
</p>
</div>
## 先决条件
- [tmux](https://github.com/tmux/tmux/wiki#-is-awesome) (>= 3.5a)
## 安装
### npm(Node >= 22)
```bash
npm install -g @bosdev/zaps
# 或:pnpm add -g @bosdev/zaps
```
软件包名称为 `@bosdev/zaps`;安装的命令为 `zaps`。适用于您平台的原生二进制文件(`linux`/`darwin`,`x64`/`arm64`)会自动作为可选依赖项安装,并在存在时使用;否则,捆绑的 Node 构建会运行。
### 预构建二进制文件(不需要 Node)
从最新的发行版中下载适用于您平台的资产(`zaps-linux-x64`、`zaps-linux-arm64`、`zaps-darwin-x64`、`zaps-darwin-arm64`),
然后使其可执行并将其放在您的 `PATH` 中:
```bash
# 示例:Linux x64
curl -fsSL -o zaps https://github.com/boshold/zaps/releases/latest/download/zaps-linux-x64
chmod +x zaps
sudo mv zaps /usr/local/bin/zaps
```
可选地,使用同一发行版中的 `SHA256SUMS` 验证下载内容:
```bash
curl -fsSL -O https://github.com/boshold/zaps/releases/latest/download/SHA256SUMS
sha256sum --check --ignore-missing SHA256SUMS
```
## 快速开始
```bash
# 在 tmux 会话中:
zaps init # 搭建 .zaps.mts 配置文件
# 编辑 .zaps.mts 以定义您的服务
zaps # 启动
```
> ZAPS 必须在 tmux 会话内运行。
## 命令
### 生命周期
| 命令 | 描述 |
| ------------------ | -------------------------------------------------------------------------- |
| `zaps` / `zaps up` | 智能默认:附加如果会话正在运行,否则创建 + 启动 + 附加 TUI |
| `zaps up -d` | 创建会话并启动服务而不附加 TUI(分离) |
| `zaps down` | 停止所有服务并销毁会话 |
### 服务操作
| 命令 | 描述 |
| --------------------------- | ---------------------------------- |
| `zaps start [service...]` | 启动服务(s)。如果省略,则全部启动 |
| `zaps stop [service...]` | 停止服务(s)。如果省略,则全部停止 |
| `zaps restart [service...]` | 重启服务(s)。如果省略,则全部重启 |
### 查询
| 命令 | 描述 |
| ------------------------ | ---------------------------------------------- |
| `zaps ps` | 列出服务及其状态、端口、URL。 `--json` |
| `zaps ls` | 列出活动会话。 `--json` |
| `zaps inspect <service>` | 显示服务详情。 `--json` |
### 任务和日志
| 命令 | 描述 |
| ------------------------ | ---------------------------------------------------------------- |
| `zaps run <task>` | 运行任务。流式输出。 `--json` |
| `zaps tasks` | 列出已配置的任务。 `--json` |
| `zaps logs [service...]` | 转储日志缓冲区。 `-f` 流式传输实时日志。 `--tail <n>` (默认 100) |
| `zaps events` | 流式传输守护进程事件为 ndjson。 `--filter <type>` |
### 配置和设置
| 命令 | 描述 |
| --------------------------------------- | --------------------------------------------------------------------- |
| `zaps config` | 验证并打印已解析的配置。 `--json`、`--toon`、`--path` |
| `zaps reload` | 为正在运行的会话重新加载配置(CLI 形式的仪表板 `c`) |
| `zaps init` | 搭建启动器 `.zaps.mts` 配置 |
| `zaps attach` | 附加 TUI 到正在运行的会话(使用 `-s` 选择一个) |
| `zaps daemon start\|stop\|status\|ping` | 守护进程管理(`ping` 检查守护进程是否响应) |
### 选择会话和输出格式
- **`-s, --session <id\|name>`** 是一个全局标志(将其放在命令前面):`zaps -s my-app attach`、`zaps -s my-app down`。它按确切的 id、确切的名称,然后是 id/名称前缀进行匹配。当省略时,ZAPS 解析当前目录的会话;如果多个匹配,它会列出它们并要求您消除歧义。
- **输出格式** — 查询/服务命令默认打印一个人类表格。传递 `--json` (漂亮的 JSON)或 `--toon` ([TOON](https://github.com/toon-format/toon))以获取机器输出,或者设置 `ZAPS_FORMAT=json|toon` 以使其成为默认值。编码代理(通过 `CLAUDECODE` / `CURSOR_TRACE_DIR` 检测)自动默认为 TOON。
## 配置
### 配置发现
ZAPS 从当前目录向上遍历,查找这些文件名(第一个匹配的文件获胜):
1. `.local.zaps.mts` — 本地覆盖(gitignore 此文件)
2. `local.zaps.mts`
3. `.local.zaps.ts`
4. `local.zaps.ts`
5. `.zaps.mts` — 主要配置文件
6. `.zaps.ts`
### 最小配置
```typescript
import type { Library } from "@bosdev/zaps";
export function config({ define }: Library) {
return define({
services: {
api: {
start: "npm run dev",
ready: { port: 3000 },
},
},
});
}
```
配置文件接收 `Library` 对象,该对象将所有内容分组到命名空间中:`define`、`find`、`cli`、`task`、`service`、`browser` 和 `node`。解构您需要的部分。参见 [Library API](#library-api) 以获取完整的引用。函数可以是 **异步** 的 — 声明为 `export async function config(lib) { … }` (结果将被等待)。
### 完整配置参考
```typescript
import type { Library } from "@bosdev/zaps";
export function config({ define }: Library) {
return define({
name: "my-app",
cwd: "./packages/app",
services: {
db: {
docker: {
service: "postgres",
file: "./docker-compose.yml",
build: true,
forceRecreate: false,
renewVolumes: false,
removeOrphans: true,
pull: "missing",
noDeps: false,
},
restart: { maxRetries: 5, backoff: 2000 },
},
api: {
start: "npm run dev:api",
ready: { output: /listening on port/ },
dependsOn: ["db"],
cwd: "./packages/api",
env: { NODE_ENV: "development" },
url: "http://localhost:4000",
},
web: {
start: "npm run dev:web",
ready: { port: true },
dependsOn: ["api"],
url: (ctx) => `http://localhost:${ctx.services.web.port}`,
onOutput: (line) => {
if (/error/i.test(line)) notifySlack(line);
},
},
worker: {
run: "npm run worker",
detached: true,
flags: { start: false },
},
},
tasks: {
migrate: {
name: "运行迁移",
description: "应用待处理的数据库迁移",
commands: "npx prisma migrate deploy",
cwd: "./packages/api",
shortcut: "m",
},
seed: {
name: "种子数据库",
commands: ["npx prisma db seed", "npx prisma db fixtures"],
dependsOn: ["migrate"],
popup: true,
},
},
layout: {
direction: "columns",
children: [
{ pane: "@tui", size: "30" },
{
direction: "rows",
children: [
{ pane: "api", size: "50" },
{ pane: "web", size: "50" },
},
},
],
},
hooks: {
onBeforeStart: () => console.log("运行设置"),
onStart: () => console.log("所有服务已启动"),
onStop: () => console.log("关闭"),
},
ui: {
icons: "nerd",
notifications: "osc9",
failOutput: "overlay",
wideThreshold: 100,
task: { defaultMode: "background", popupPicker: false },
},
});
}
```
参见 [UI 配置](#ui-config) 以获取完整的 `ui` 块引用。
## 项目选项
| 选项 | 类型 | 默认值 | 描述 |
| ------ | --------------------------------------- | ---------- | ------------------------- |
| `name` | `string` | 目录名称 | TUI 中显示的项目名称 |
| `cwd` | `string \| (ctx: CwdContext) => string` | 调用目录 | 项目工作目录 |
### `cwd`
默认情况下,`projectDir` 是调用 `zaps` 的目录。使用 `cwd` 覆盖此选项 — 在跨多个项目共享配置时很有用:
```
workspace/
customer/
.zaps.mts ← 共享配置
customer-1/ ← 运行 `zaps` 的位置
customer-2/ ← 或此处
```
**字符串** — 相对于配置文件目录解析:
```typescript
cwd: "./customer-1";
```
**函数** — 接收 `{ configDir, invokeDir }` 进行动态解析:
```typescript
cwd: ({ invokeDir }) => invokeDir; // 已经是默认值
```
**`find.up(filename, opts?)`** — 从调用目录向上遍历到第一个包含 `filename` 的目录,并使用它作为 `cwd`。适用于 monorepos,从嵌套包中运行 `zaps` 但希望项目根目录:
```typescript
export function config({ define, find }) {
return define({
cwd: find.up("package.json"),
services: {
/* … */
},
});
}
```
选项:
- `stopAt` — 停止在边界处遍历:绝对路径,或字面 `"config"` 到配置文件目录。`cwd: find.up("package.json", { stopAt: "config" })`.
- `orFatal` — 如果 `filename` 永远找不到,则使用的消息:
`cwd: find.up("Cargo.toml", { orFatal: "Run zaps inside a Rust crate" })`。
`find.up` 始终返回一个解析器(永远不为 `null`)。如果找不到文件,它会在配置加载时抛出 `ConfigError` — 没有 `?? cli.fatal()` 需要编写。参见
[错误模型](#error-model)。
### Node 内置模块
`Library` 对象包含一个 `node` 命名空间,包含常见的 Node.js 模块(`path`、`fs`、`process`、`url`、`os`、`child_process`),因此配置文件不需要原始 `import` 语句:
```typescript
export function config({ define, node }: Library) {
return define({
cwd: ({ configDir }) => node.path.join(configDir, "backend"),
services: {
api: {
start: "npm run dev",
env: { HOME: node.process.env.HOME ?? "" },
},
},
});
}
### 配置加载和重新加载
配置文件(`.zaps.ts` / `.zaps.mts` 及其 `local.` 变体)使用 [jiti](https://github.com/unjs/jiti) 进行评估。每次加载都会重新评估 **整个**导入图 - 入口文件加上它导入的每个相对帮助/环境文件,因此 reload 拾取对任何文件的编辑:
```typescript
// helper.mts
export const apiPort = 3000;
// .zaps.mts
import { apiPort } from "./helper.mts";
export function config(z) {
/* 使用 apiPort */
}
```
警告(jiti 的每载入 CJS 转换):
- 没有 ESM 实时绑定 - 导出在加载时快照。
- 模块身份每次加载都会更改:一个负载的值与下一个负载不相同/ `instanceof` 相同。不要依赖跨加载的对象身份。
- `node_modules` 从配置达到的 `node_modules` 每次加载都会重新评估。如果配置加载变得缓慢因为重依赖项,成本每次 reload。
**重新加载正在运行的会话** 是 **验证然后交换**:新的配置加载并验证后,仅当它解析干净时才交换到新的配置中。无效编辑永远不会撕掉您的运行会话 - 旧配置保持活动期间不会破坏运行会话(validate-then-swap),会话的 TUI 头显示一个
`config changed — press c to reload` 提示;按下 **`c`** 仪表盘(没有操作服务)以应用它。
## 库 API
`Library` 对象传递给您的 `config` 函数,将所有内容分组到命名空间中。解构您需要的部分:`config(({ define, find, cli, service }) => …)`。
| 命名空间 | 成员 | 描述 |
| --------- | -------------------------------- | --------------------------------------------------------------------------- |
| `define` | `(config) => config` | 验证并返回项目配置。如果配置错误,则抛出 `ConfigError`。 |
| `find` | `up(filename, opts?)` | 构建一个 `cwd` 解析器,它向上遍历目录(参见 [`cwd`](#cwd))。 |
| `cli` | `fatal(message, opts?)` | 通过抛出 `ConfigError` 终止配置评估。永远不会返回。 |
| `cli` | `warn / info / success(message)` | 发出通知(CLI 中的 stderr;在 TUI 守护进程期间显示 toast)。 |
| `task` | `run(key)` | 运行命名任务。只有在会话运行期间可用。 |
| `service` | `start / stop / restart(name)` | 控制服务。只有在服务钩子内可用。 |
| `service` | `isRunning(name)` | 服务是否当前正在运行。只有钩子可用。 |
| `browser` | `open(url)` | 在默认浏览器中打开 URL。 |
| `node` | `path / fs / process / …` | 常见的 Node.js 模块,不需要 `import`。 |
`task.*` 和 `service.*` 仅在会话运行时连接,因此仅可在钩子中调用它们(例如 `onReady`),而不可在 `config` 顶部调用。过早调用它们会抛出一个明确的错误。
```typescript
export function config({ define, cli, service }) {
return define({
services: {
api: {
start: "npm run dev",
ready: { port: 3000 },
env: { API_KEY: process.env.API_KEY ?? cli.fatal("API_KEY is required") },
},
worker: {
run: "npm run worker",
onReady: () => service.restart("api"),
},
},
});
}
```
### 错误模型
配置评估失败 **抛出** 一个 `ConfigError` 而不是调用 `process.exit`。这样可以保护守护进程:
- **进程内加载** (`zaps config`) 渲染一个样式化的 `✖ <message>` 行并以非零退出。
- **守护进程路径命令** (`zaps up`、`start` 等)以 **未样式化** 的相同消息报告,并以非零退出。
- **在守护进程重新加载期间**,抛出 `ConfigError` 被捕获:正在运行的会话保持完整(验证然后交换),旧配置保持活动,错误被报告 — 它永远不会破坏您的服务。
`cli.fatal(message, opts?)` 是显式的出口通道 — 它会抛出一个带有您的消息(和可选字段)的 `ConfigError`。由于它返回 `never`,因此它可以适用于任何值位置:`name: pkg.name ?? cli.fatal("name required")`。
### 配置通知
`cli.warn`、`cli.info` 和 `cli.success` 在不终止程序的情况下显示一条短消息:
- 在一次性 CLI 运行中,它们会向 stderr 打印一行样式化的文本。
- 在守护进程重载(带有 TUI 附件)时,它们会作为瞬时通知显示。
```typescript
export function config({ define, cli }) {
if (!process.env.STRIPE_KEY) cli.warn("STRIPE_KEY 未设置 — 支付功能已禁用");
return define({ services: { app: { start: "npm run dev" } } });
}
```
### 异步配置
配置函数可以是 `async` 的:
```typescript
export async function config({ define, node }) {
const pkg = JSON.parse(await node.fs.promises.readFile("package.json", "utf8"));
return define({ name: pkg.name, services: { app: { start: "npm run dev" } } });
}
```
异步评估受 30 秒超时限制:如果异步配置挂起(等待永远不会解析的内容),则加载失败并出现 `ConfigError`,而不是永远阻塞重新加载。超时仅涵盖异步等待 — 同步无限循环不可中断。
## 服务
### 选项
| 选项 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `start` | `string \| () => string` | — | 启动服务的命令(长期运行的进程) |
| `run` | `string \| () => string` | — | 与 `start` 互换 — 满足“需要命令”的规则(如果两者都设置,则 `start` 优先) |
| `stop` | `string \| () => string` | — | 自定义停止命令(默认:Ctrl-C) |
| `docker` | `DockerConfig` | — | Docker Compose 服务配置 |
| `ready` | `ReadyConfig` | — | 如何检测服务就绪 |
| `dependsOn` | `string[]` | `[]` | 必须先就绪的服务 |
| `env` | `Record<string, string> \| (ctx) => Record` | — | 环境变量 |
| `cwd` | `string` | — | 工作目录 |
| `url` | `string \| (ctx) => string` | — | 浏览器打开的 URL(`o` 键) |
| `flags` | `{ start?: boolean, open?: boolean }` | — | `start`: 启动时自动启动(默认 `true`),`open`: 就绪时自动打开 URL |
| `detached` | `boolean` | `false` | 在 tmux 外运行(无窗格) |
| `lazyPane` | `boolean` | _auto_ | 在启动时创建窗格,在显式停止时删除(默认 `true`,当 `flags.start: false` 时) |
| `raw` | `boolean` | `false` | 绕过包装器 — 在窗格中直接显示环境变量 |
| `restart` | `{ maxRetries?, backoff? }` | — | 崩溃后自动重启 |
| `onBeforeStart` | `() => void \| Promise<void>` | — | 命令发送前的回调 |
| `onReady` | `() => void \| Promise<void>` | — | 服务就绪时的回调 |
| `onStop` | `() => void \| Promise<void>` | — | 服务停止时的回调 |
| `onOutput` | `(line: string) => void \| Promise<void>` | — | 每个新输出行调用 |
| `optional` | `boolean \| () => Promise<boolean>` | — | 标记服务为可选(见下文) |
### 可选服务
当二进制文件可能未在所有机器上安装时,标记服务为可选:
```typescript
services: {
rainfrog: {
optional: true,
start: "rainfrog -u postgres://localhost:5432",
ready: { port: 5432 },
},
}
```
当 `optional: true` 时,ZAPS 通过 `command -v` 检查二进制文件是否存在。二进制文件是 `start`/`run` 的第一个 **非赋值** 标记,因此跳过前导 `FOO=bar` 环境前缀(`FOO=bar rainfrog …` 探测 `rainfrog` 而不是 `FOO=bar`)。没有真正的二进制标记的命令(仅赋值或空白)被视为 **不可用**。对于函数命令或自定义检查,请使用上下文帮助程序:
```typescript
services: {
rainfrog: {
optional: (ctx) => ctx.hasBinary("rainfrog"),
start: (ctx) => `rainfrog --url postgres://localhost:${ctx.services.db.ports[0]}`,
dependsOn: ["db"],
},
}
```
`optional` 谓词接收一个带有帮助程序的上下文:
- `ctx.hasBinary(name)` — 通过 `command -v` 检查二进制文件是否存在
自然组合检查:
```typescript
optional: async (ctx) => await ctx.hasBinary("grafana") && await ctx.hasBinary("prometheus"),
```
**不可用时的行为:**
- 不分配 tmux 窗格
- 在 TUI 仪表板中显示为灰色
- 静默删除 `dependsOn`/`restartWith` 引用
- 自动调整布局(折叠空分区)
> **注意:** `optional: true` 需要 `start` 或 `run` 作为字符串(不是函数)。对函数命令或仅限 Docker 的服务使用带有 `ctx.hasBinary()` 的函数形式。
### detached 服务
设置 `detached: true` 以运行服务 **无窗格** — 在 tmux 布局之外,没有可见的窗格:
```typescript
services: {
worker: {
start: "node worker.js",
detached: true,
ready: { port: 4000 },
},
}
```
- 仍然应用完整的生命周期 — 启动/停止/重启,就绪检测和崩溃恢复都像带窗格的服务一样工作。
- `zaps up -d` 以分离模式启动整个会话(服务运行,无 TUI 附件);以后可以使用 `zaps attach` 查看。
- **没有 tmux 窗格**,因此没有实时终端可滚动 — 通过日志缓冲区读取其输出(`zaps logs worker`,`-f` 流式传输)。
- 分离服务 **不得** 出现在 `layout` 中,并且不能与 `docker` 或 `raw` 组合(两者都需要窗格)— 这些是配置加载错误。
### 惰性窗格
**惰性** 服务是指其 tmux 窗格仅在进程运行时存在:服务启动时在精确的声明位置创建窗格,服务 **显式** 停止时销毁。崩溃保留窗格(以便您可以检查事后输出);重启保留窗格(进程就地替换)。第一次手动启动创建窗格;第一次手动停止删除窗格。
```typescript
services: {
mailpit: {
start: "mailpit",
flags: { start: false }, // 需要手动启动 — 通过 `s` / TUI / CLI 选择加入。
ready: { port: 8025 },
},
}
```
**默认规则:** 当服务为非自动启动(`flags.start: false`)时,`lazyPane: true`;对于自动启动服务,`lazyPane: false`。显式 `lazyPane: true | false` 始终优先。
**Docker 组和分离服务永远不会惰性** — 即使设置 `flags.start: false`,Docker 组成员保留其共享组窗格,而 `detached: true` 服务保持无窗格。启动跳过规则仅适用于拥有窗格的服务,因此组/分离行为保持不变。
`lazyPane` 是生命周期对应于 [`detached`](#detached-services):**分离** 服务永远不会窗格化(根本没有终端),而 **惰性** 服务则 **按需** 窗格化(启动时出现真实窗格,显式停止时消失)。
**非法组合**(配置加载错误):
- `lazyPane: true` 与 `detached: true` — 分离服务没有窗格。
- Docker 组成员的 `lazyPane: true`(从 `docker.service: [...]` 使用 `expand` 扩展的服务)— 组成员共享一个窗格,因此成员的惰性不明确;在组级别应用 `lazyPane`。
### 就绪检测
五种检测服务就绪的策略:
**端口** — 等待 TCP 端口:
```typescript
ready: {
port: 3000;
} // 特定端口
ready: {
port: true;
} // 任意端口
ready: {
port: () => getPort();
} // 动态端口
```
**输出** — 与窗格输出匹配:
```typescript
ready: {
output: /listening on port \d+/;
}
ready: {
output: (line) => line.includes("ready");
}
```
**HTTP** — 轮询 HTTP 端点:
```typescript
ready: { http: "/health" } // 路径 — 自动检测端口,然后探测
ready: { http: "http://localhost:3000/health" } // 完整 URL — 直接探测
ready: { http: { url: "/api/health", status: 200 } } // 需要特定状态码
```
当 URL 以 `/` 开头时,ZAPS 每隔一段时间重新检测服务的端口并探测路径,跳过调试器/HMR 端口(9229-9240,24678)。完整 URL 直接探测。如果省略 `status`,则任何 HTTP 响应都视为就绪。
**Docker** — 等待容器运行 + 健康:
```typescript
ready: { docker: "postgres" }
ready: { docker: ["postgres", "redis"] } // 必须全部就绪
ready: { docker: "postgres", file: "./docker-compose.yml" }
```
> **Docker + `ready.port` / 路径-http 是配置加载错误。** 已发布的 Docker 端口由 `dockerd` 持有,而不是服务的窗格,因此端口/PID 检测永远无法匹配 — 它将始终超时。使用默认的 Docker 就绪性,或完整的 URL `ready: { http: "http://127.0.0.1:<port>/path" }`。
**函数** — 自定义异步检查:
```typescript
ready: async () => {
const res = await fetch("http://localhost:3000/health");
return res.ok;
};
```
就绪检查每 500ms 轮询一次,超时时间为 60s。
### Docker 集成
当服务具有 `docker` 配置且没有 `start`/`run` 时,ZAPS 自动生成 `docker compose up` 命令。
如果未提供 `ready` 配置,ZAPS 默认检查 Docker 容器状态(运行 + 健康)。
| 选项 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `service` | `string \| string[]` | — | Docker Compose 服务名称(必填) |
| `file` | `string` | — | 编写文件路径 |
| `projectName` | `string` | — | 固定编组项目名称(见下文) |
| `build` | `boolean` | — | `--build` 标志 |
| `forceRecreate` | `boolean` | — | `--force-recreate` 标志 |
| `renewVolumes` | `boolean` | — | `-V` 标志(重新创建卷) |
| `removeOrphans` | `boolean` | — | `--remove-orphans` 标志 |
| `pull` | `"always" \| "missing" \| "never"` | — | `--pull` 策略 |
| `noDeps` | `boolean` | — | `--no-deps` 标志 |
| `expand` | `boolean \| Record<string, object>` | — | 展开为单独的服务(见下文) |
> **编组版本:** Docker Compose **v2.21+** 是测试基准。
#### 编组项目固定
每次编组调用都固定为确定性项目名称,以便在相同命名目录中(例如 `…/foo/backend` 和 `…/bar/backend`)不能将两个检出误认为彼此。项目名称按优先级解析:
1. `docker.projectName`(此配置字段)
2. `ZAPS_COMPOSE_PROJECT` 环境变量(在守护进程中读取 — 在守护进程启动处设置)
3. 编写文件的顶级 `name:`
4. `zaps-<sanitized-dir-name>-<hash>`(默认)
> **一次性重新创建:** 切换到固定的项目会一次性重新创建服务的容器。如果容器已经存在于旧的(未固定的)名称下,ZAPS 会打印一次性警告,建议使用 `docker compose -p <old> down`。
> **任务不继承固定值。** 固定值仅应用于 `docker` **服务的** ZAPS 为 `docker` 运行的命令。调用 `docker compose …` 的任务会使用 Compose 自己的默认项目名称,因此它看不到 ZAPS 启动的容器。显式传递项目 — `docker compose -p "$ZAPS_COMPOSE_PROJECT" …`(在守护进程启动处设置 `ZAPS_COMPOSE_PROJECT`)— 或逻辑操作(例如 `prisma migrate reset` 代替 `docker compose down -v`)。
### 扩展的 Docker 服务
当您有多个 Docker Compose 服务可以共享一个 tmux 窗格时,请使用 `expand: true` 将它们分成单独的、可独立寻址的服务:
```typescript
services: {
infra: {
docker: {
service: ["postgres", "redis", "mailpit"],
expand: true,
},
restart: { maxRetries: 3 },
},
api: {
start: "npm run dev",
dependsOn: ["postgres"], // 引用单独的扩展服务
},
}
```
这会创建三个单独的服务(`postgres`、`redis`、`mailpit`):
- 共享一个 tmux 窗格(一个 `docker compose up` 命令)
- 每个都有独立的状态、就绪检测和生命周期
- 可以单独启动/停止/重启
- 可以在 `dependsOn` 中单独引用
- 在 TUI 仪表板中显示为分组行
布局引用使用组名:`{ pane: "infra" }`。
使用 `expand: { ... }` 代替 `expand: true` 以提供每个子服务的覆盖:
```typescript
services: {
infra: {
docker: {
service: ["caddy", "postgres", "mailpit", "bugsink"],
expand: {
postgres: {
onReady: () => task.run("prisma:deploy"),
},
bugsink: {
ready: { http: "http://localhost:8000/health/ready" },
},
},
},
},
}
```
子服务没有覆盖的继承父配置。覆盖可以设置 `ready`、`env`、`onReady`、`onStop`、`onBeforeStart`、`url`、`flags`、`restart` 等。
覆盖 **不能** 设置 `start`、`run` 或 `docker`(命令和 Docker 配置从父组继承 — 覆盖它们将默默地破坏共享窗格),而未知键(例如输入错误,如 `redy:`)被拒绝。任何禁止或未知键都是配置加载错误,命名组、子服务和冲突键。`expand` 也适用于 `service` 是单个字符串(它扩展为一个子服务),不仅适用于数组。
### 依赖
服务以拓扑顺序启动。相同级别的服务并行启动:
```typescript
services: {
db: { start: "..." }, // 0 级 — 首先启动
cache: { start: "..." }, // 0 级 — 与 db 并行
api: {
start: "...",
dependsOn: ["db", "cache"], // 1 级 — 等待两者
},
web: {
start: "...",
dependsOn: ["api"], // 2 级 — 等待 api
},
}
```
关闭以拓扑逆序运行。
### 崩溃恢复
启用带有指数退火的自动重启:
```typescript
restart: {
maxRetries: 5, // 默认:3
backoff: 2000, // 基本延迟(毫秒),默认:1000
}
```
退火每次重试都会加倍:2s → 4s → 8s → 16s → 32s。崩溃监控每 2 秒轮询一次 `raw` 模式服务,每 10 秒轮询一次包装器模式服务(包装器的退出通知是主要信号,因此轮询仅作为备用)。
### 动态环境
通过 `ServiceContext` 访问其他服务的运行时信息:
```typescript
env: (ctx) => ({
DATABASE_URL: `postgres://localhost:${ctx.services.db.port}/mydb`,
API_PORTS: ctx.services.api.ports.join(","),
PROJECT_DIR: ctx.projectDir,
}),
```
`ServiceContext` 形状:
```typescript
{
services: Record<
string,
{
port: number | undefined; // 检测到的第一个端口
ports: number[]; // 检测到的所有端口
cwd: string | undefined; // 服务配置的 cwd,否则为 projectDir
}
>;
projectDir: string;
url(service: string, opts?: UrlOptions): string | null;
}
```
### `ctx.url()`
`ctx.url(service, opts?)` 根据服务的检测端口构建 URL,这样你就不必手动组装字符串:
```typescript
env: (ctx) => ({
API_URL: ctx.url("api"), // "http://localhost:3000"
DATABASE_URL: ctx.url("db", { protocol: "postgres", auth: "user:pass", path: "/mydb" }),
// → "postgres://user:pass@localhost:5432/mydb"
});
```
`UrlOptions`: `protocol` (默认 `"http"`),`auth` (例如 `"user:pass"`),`host`
(默认 `"localhost"`),`port` (覆盖检测到的端口),`path` (如果缺少,则添加前导 `/`)。IPv6 主机自动括起来(`http://[::1]:3000`)。
行为:
- **未知服务** → 抛出 `ConfigError`。
- **尚未检测到端口**(且没有 `port` 覆盖)→ 返回 `null`。
- `null` 结果从 env 对象中 **删除** — 变量直接省略,而不是设置为一个空字符串,因此 `API_URL: ctx.url("api")` 在端口启动之前是安全的。
`ctx.url()` 在任何具有 `ServiceContext` 的地方都可用 — 动态 `env`、命令函数、`url:` 字段和任务 `run` 回调(也作为 `ctx.services.url()`)。
## 任务
TUI 中可运行的一次性命令:
```typescript
tasks: {
migrate: {
name: "运行迁移",
description: "应用待处理的数据库迁移",
commands: "npx prisma migrate deploy",
cwd: "./packages/api",
dependsOn: ["seed"],
env: { NODE_ENV: "production" },
shortcut: "m",
},
"reset-db": {
name: "重置数据库",
commands: [
"npx prisma migrate reset --force",
"npx prisma db seed",
],
},
"interactive-migrate": {
name: "交互式迁移",
commands: "npx prisma migrate dev",
popup: { width: "80%", height: "80%" },
},
}
```
### 任务选项
| 选项 | 类型 | 默认值 | 描述 |
| ------------- | ------------------------------------------------ | ------- | --------------------------------------------------------------- |
| `name` | `string` | — | TUI 中显示的名称 |
| `description` | `string` | — | 任务选择器中显示的描述 |
| `commands` | `string \| string[]` | — | 要运行的 shell 命令 |
| `run` | `(ctx: TaskRunContext) => void \| Promise<void>` | — | 程序化任务函数(与 `commands` 互斥) |
| `cwd` | `string` | — | 工作目录 |
| `env` | `Record<string, string>` | — | 环境变量 |
| `dependsOn` | `string[]` | `[]` | 必须首先运行的任务 |
| `shortcut` | `string` | — | 在选择器中显示的提示键 |
| `popup` | `boolean \| { width?: string; height?: string }` | — | 在 tmux 弹出窗口中运行(仅限命令) |
任务依赖关系被解析并在任务本身之前执行。
### 程序化任务
使用 `run` 代替 `commands` 以获得完全的程序控制:
```typescript
tasks: {
"check-health": {
name: "健康检查",
run: async ({ exec, stdout, services, projectDir }) => {
const { success, output } = await exec("curl -sf http://localhost:3000/health");
stdout.write(success ? "API 健康" : "API 宕机");
const result = await exec("npm test", { cwd: "./packages/api" });
if (!result.success) throw new Error("测试失败");
},
},
}
```
`TaskRunContext` 形状:
```typescript
{
exec(cmd: string, opts?: { cwd?: string; env?: Record<string, string> }): Promise<ExecResult>;
stdout: { write(text: string): void };
services: ServiceContext; // 与动态 env 上下文相同
projectDir: string;
url(service: string, opts?: UrlOptions): string | null; // 镜像 services.url()
}
```
`ExecResult` 形状:
```typescript
{
success: boolean;
exitCode: number;
output: string[];
}
```
> `commands` 和 `run` 是互斥的 — 任务必须使用其中之一。
### 快捷键
任务可以定义一个 `shortcut` 键,在选择器中作为提示显示。
如果没有指定快捷键,ZAPS 会自动从任务键中分配第一个唯一字符。
按 `t` 在仪表板上打开模糊任务选择器,然后输入过滤并按 `Enter` / `Tab` 运行(参见 [任务选择器](#任务选择器-t))。
#### 保留键
**`q`、`j` 和 `k` 键是保留的**,并且永远不会自动分配给任务(`q` 断开连接,`j` / `k` 是列表导航)。如果任务显式请求其中一个,则快捷键 **被丢弃** (不分配备用),并且 ZAPS 打印一个加载时警告:
```
警告:任务 'deploy' ('部署') 请求保留的快捷键 'q';'q' 是保留的(q=退出,j/k=导航)并且快捷键被丢弃。
```
为任务选择不同的键以保留快捷键。
## 布局
定义自定义的 tmux 窗格布局。**必填** 窗格是 `@tui` — 它承载 ZAPS 仪表板。
```typescript
layout: {
direction: "columns",
children: [
{ pane: "@tui", size: "25" },
{
direction: "rows",
children: [
{ pane: "api", size: "60" },
{ pane: "web", size: "40" },
],
},
],
}
```
- `direction`: `"rows"` (垂直分割)或 `"columns"` (水平分割)
- `size`: 父级百分比(默认为均等分割)
- `focus`: 在布局创建后设置为 `true` 以自动聚焦一个叶窗格(最多一个)
- 未在布局中的服务将获得自己的垂直分割窗格在 `@tui` 窗口中
- detached 服务 **不得** 出现在布局中
如果未指定布局,`@tui` 将获得主窗格,每个非分离服务将在同一窗口中获得一个垂直分割窗格。
## TUI
TUI 建立在仪表板、叠加层(命令调色板、任务选择器、帮助、失败输出)和根据窗格大小调整的响应式布局之上。
### 全局键
从任何基本视图工作;生存断开连接;让位于打开的叠加层。
| 键 | 操作 |
| ------------ | ----------------------------------------------- |
| `Ctrl-K` `:` | 打开命令调色板(模糊操作) |
| `?` | 切换帮助叠加层 |
| `t` | 打开任务选择器 |
| `f` | 打开最新失败的捕获输出 |
| `x` | 确认 — 清除粘性失败通知 |
| `q` `Ctrl-C` | 分离(服务继续运行) |
| `Ctrl-D` | 关闭会话(停止服务 + 销毁) |
| `Esc` | 关闭顶部叠加层 / 离开当前视图 |
### 仪表板
| 键 | 操作 |
| ------------- | ------------------------------------ |
| `Up/Down/j/k` | 导航服务 |
| `r` | 重启选定的服务 |
| `s` | 启动/停止选定的服务 |
| `l` | 查看选定服务的日志 |
| `o` | 在浏览器中打开服务网址 |
| `R` | Docker 重建(Docker 服务) |
| `z` / `Z` | 缩放服务窗格 / TUI 窗格 |
| `E` | 编辑捕获选定的窗格 |
| `a` | 重启所有服务 |
| `c` | 重新加载配置(更改 + 空闲) |
| `t` | 打开任务选择器 |
| `d` | 关闭会话 |
在宽窗格上,**详细信息窗格** 显示选定服务的字段;当 `cols < ui.wideThreshold` 时,它会折叠(默认 `100`)。参见 [UI 配置](#ui-配置)。
### 命令调色板(`Ctrl-K` / `:`)
| 键 | 操作 |
| --------- | -------------------- |
| 输入 | 模糊过滤操作 |
| `Up/Down` | 移动选择 |
| `Enter` | 运行 / 确认 |
| `Esc` | 关闭 |
### 任务选择器(`t`)
项目任务的模糊选择器。每个任务都可以在两种模式下运行:
| 键 | 操作 |
| --------- | --------------------------------------------------------------------- |
| 输入 | 模糊过滤任务 |
| `Up/Down` | 移动选择 |
| `Enter` | 默认模式下运行(`ui.task.defaultMode`,默认为 `background`) |
| `Tab` | 在 tmux 窗格中实时运行 |
| `Esc` | 关闭 |
- **后台** 流式传输到守护进程的保留输出缓冲区;成功显示一个短暂的通知,失败显示一个粘性通知。
- **在窗格中运行** 在 tmux 窗格/窗口中打开运行任务;输出在完成后保持可检查。
将 `ui.task.popupPicker: true` 设置为使用 `fzf` tmux 弹出窗口而不是应用内选择器(当 tmux < 3.2 或 `fzf` 丢失时回退到应用内选择器)。
### 日志视图(`l`)
| 键 | 操作 |
| ------------- | ----------------- |
| `Up/Down/j/k` | 滚动日志 |
| `Esc` | 返回仪表板 |
向上滚动以暂停自动跟随;向下滚动到底部以恢复实时跟随。
### 通知和失败输出
已完成的后台任务会引发应用内通知:**成功** 是短暂的,**失败** 是粘性的并且保留直到确认 `x`。在失败时,ZAPS 还会根据 `ui.notifications` 发出一个独立的终端通知(默认 `osc9`)。
按 `f` 打开 **失败输出叠加层** 以获取最新的粘性失败(`Up/Down/j/k` 滚动,`Esc` 关闭)。在其中,`p` 升级到更大的 tmux `display-popup`(当 tmux 支持时)。使用 `ui.failOutput: popup`,叠加层直接升级到打开时的弹出窗口。
## UI 配置
可选的 `ui` 块调整 TUI 呈现。每个字段都有一个安全的默认值,因此省略 `ui`(或任何字段)是可以的。
```typescript
ui: {
icons: "nerd", // "nerd" | "unicode" | "ascii"
notifications: "osc9", // "off" | "bell" | "osc9" | "osc9+bell"
failOutput: "overlay", // "overlay" | "popup"
wideThreshold: 100, // 详细窗格显示的最小列数(整数 ≥ 40)
task: {
defaultMode: "background", // "background" | "pane" — 任务选择器中的 Enter
popupPicker: false, // 打开任务选择器为 fzf tmux 弹出窗口
},
}
```
| 选项 | 类型 | 默认值 | 描述 |
| ------------------ | ------------------------------------------ | -------------- | ------------------------------------------------------------------------------------------- |
| `icons` | `"nerd" \| "unicode" \| "ascii"` | `"nerd"` | 字符层级。`ZAPS_ICONS` 环境变量覆盖配置值。 |
| `notifications` | `"off" \| "bell" \| "osc9" \| "osc9+bell"` | `"osc9"` | 带外故障通知通道(OSC 9 桌面通知和/或终端铃)。 |
| `failOutput` | `"overlay" \| "popup"` | `"overlay"` | `f` 失败输出视图打开的位置;`popup` 直接升级到 tmux 弹出窗口。 |
| `wideThreshold` | `number` (int ≥ 40) | `100` | 显示详细窗格的最小终端列数;低于此值窗格会折叠。 |
| `task.defaultMode` | `"background" \| "pane"` | `"background"` | 任务选择器中的 `Enter` 模式(`Tab` 始终在窗格中运行)。 |
| `task.popupPicker` | `boolean` | `false` | 使用 `fzf` tmux 弹出窗口选择器而不是应用内选择器(不可用时回退)。 |
## 服务状态
有效转换(管理器拒绝任何其他移动):
```
stopped → starting
starting → ready | error | stopping
ready → stopping | restarting | error
stopping → stopped
restarting → starting | stopping | error
error → starting (重试 / 手动启动)
unavailable → — (终端:可选服务,其二进制/检查失败)
```
| 状态 | 指示器 |
| -------------------------------------- | -------------- |
| `ready` | 绿色 `●` |
| `starting` / `stopping` / `restarting` | 黄色旋转图标 |
| `error` | 红色 `✖` |
| `stopped` | 灰色 `○` |
| `unavailable` | 灰色 `○` |
## 钩子
### 全局钩子
项目级别的自定义逻辑的生命周期钩子:
```typescript
hooks: {
onBeforeStart: async () => { /* 在任何服务启动前运行 */ },
onStart: async () => { /* 所有服务启动 */ },
onStop: async () => { /* 退出前清理 */ },
}
```
### 每服务钩子
服务还支持其自己的钩子用于服务特定的逻辑:
```typescript
services: {
api: {
start: "npm run dev",
onBeforeStart: () => console.log("设置 API"),
onReady: () => console.log("API 已启动"),
onStop: () => console.log("API 已停止"),
onOutput: (line) => {
if (/error/i.test(line)) sendAlert(line);
},
},
}
```
## AI 集成
ZAPS 为 AI 编码代理提供两种集成路径:**Claude Code Skills** (推荐)和 **MCP**。Skills 由于按需加载上下文而更高效,而 MCP 提供一个协议级接口,可由任何与 MCP 兼容的客户端使用。
### 代理预处理
使用 `zaps prime-agent` 获取所有服务(带有运行时状态和端口)和任务的简洁 TOON 概述。适用于为当前项目引导 AI 代理的上下文。
### Claude Code Skills
ZAPS 在 `.claude/skills/` 中附带两个 [Claude Code 技能](https://docs.anthropic.com/en/docs/claude-code/skills):
| 技能 | 描述 |
| ------------- | ---------------------------------------------------------------------------- |
| `zaps-usage` | 与开发会话交互 — 启动/停止服务、运行任务、查看日志 |
| `zaps-config` | 编写和编辑 ZAPS 配置文件(`.zaps.mts`、`.zaps.ts`、`local.zaps.ts`) |
Skills **优于 MCP**,因为它们按需加载参考文档,而不是占用持久上下文,从而显著降低 token 使用量。
#### 安装
将 `.claude/skills/` 目录复制到您的项目中:
```bash
cp -r node_modules/@bosdev/zaps/.claude/skills/ .claude/skills/
```
Claude Code 将在相关时自动发现并使用技能。
### MCP 服务器
ZAPS 公开一个 [MCP](https://modelcontextprotocol.io/) 服务器,允许 AI 代理管理服务、运行任务和读取日志。
```bash
zaps mcp # 从 CWD 自动检测会话
zaps mcp --session my-app # 目标特定会话
```
#### 可用工具
| 工具 | 描述 |
| ---------------------- | ------------------------------------ |
| `services_list` | 列出所有服务及其状态 |
| `services_details` | 获取特定服务的详细信息 |
| `services_start` | 启动服务 |
| `services_stop` | 停止服务 |
| `services_restart` | 重启服务 |
| `services_start_all` | 启动所有(或特定)服务 |
| `services_stop_all` | 停止所有(或特定)服务 |
| `services_restart_all` | 重启所有(或特定)服务 |
| `logs_snapshot` | 获取服务的最近日志行数 |
| `tasks_list` | 列出可用任务 |
| `tasks_run` | 运行任务并返回其输出 |
#### 资源
| URI | 描述 |
| --------------------------- | ------------------------------ |
| `zaps://logs/{serviceName}` | 实时日志输出(可订阅) |
#### Claude Code 设置
```bash
claude mcp add zaps -- zaps mcp
```
或手动添加到项目根目录下的 `.mcp.json`:
```json
{
"mcpServers": {
"zaps": {
"command": "zaps",
"args": ["mcp"]
}
}
}
```
### CLAUDE.md 设置
在项目中将以下内容添加到 `CLAUDE.md` 以帮助 Claude 有效使用 ZAPS:
```markdown
## ZAPS
- 使用 `zaps-usage` 技能管理开发会话(启动/停止服务、运行任务、查看日志)
- 在编辑 ZAPS 配置文件时使用 `zaps-config` 技能(`.zaps.mts`、`.zaps.ts`、`local.zaps.ts`)
```
## 故障排除
### `zaps daemon stop` 停止服务
`zaps daemon stop` 现已执行 **完整清理**:它停止守护进程管理的每个会话中的每个服务,然后关闭守护进程,并报告已停止的内容(`停止 <n> 会话(s),<m> 服务(s)`)。它不再是一个简单的进程终止。
### 您可能看到的错误消息
- **端口已被占用** — 在启动服务之前,ZAPS 预先检查其预期的主机端口。如果其中一个被占用,它会快速失败并带有属性消息,而不是让服务在绑定时崩溃:
```
端口 5432 已被占用(pid 1234 postgres)
```
- **依赖未就绪** — 当服务无法启动,因为其 `dependsOn` 服务从未变为就绪时,依赖项的 `lastError` (在仪表板中显示)会读取:
```
依赖项 "db" 未就绪
```
## 许可证
MIT
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器