npm package: @loneship/zup
Description: 一个用于发布本地的前后端目录或可执行文件的命令行工具，负责优雅地替换和部署（仅支持pm2部署的产品），并能自动备份旧版本。
Latest version: 1.0.0

# zup

`zup` 是一个通过 SSH 部署本地构建产物的 CLI。它适合两类场景：

- 上传目录并以原子切换方式发布，例如前端 `dist`
- 上传单个文件并在发布后执行 `pm2 reload`

使用者只需要安装 Node.js，不需要 TypeScript。

## 安装

```bash
npm install -g @loneship/zup
```

也可以直接使用：

```bash
npx @loneship/zup
```

## 配置文件

默认读取当前目录下的 `zup.config.json`，也可以通过 `--config` 指定。

### 目录模式

适合前端构建目录发布。

```json
{
  "ssh": {
    "host": "example.com",
    "port": 22,
    "username": "root",
    "password": "your-password"
  },
  "paths": {
    "local": "./dist",
    "remote": "/var/www/app"
  },
  "deployment": {
    "mode": "directory",
    "preDeployCommand": "npm run build",
    "keepBackups": 10
  }
}
```

### 私钥登录

适合使用 SSH key 的服务器。

```json
{
  "ssh": {
    "host": "example.com",
    "port": 22,
    "username": "deploy",
    "privateKeyPath": "./keys/id_ed25519",
    "passphrase": "your-key-passphrase"
  },
  "paths": {
    "local": "./dist",
    "remote": "/var/www/app"
  },
  "deployment": {
    "mode": "directory",
    "preDeployCommand": "npm run build",
    "keepBackups": 10
  }
}
```

### 文件模式

适合单个 Node.js 脚本、Rust 可执行文件等发布。

```json
{
  "ssh": {
    "host": "example.com",
    "port": 22,
    "username": "root",
    "password": "your-password"
  },
  "paths": {
    "local": "./target/release/my-service",
    "remote": "/srv/my-service/current"
  },
  "deployment": {
    "mode": "file",
    "preDeployCommand": "cargo build --release",
    "keepBackups": 5,
    "pm2": {
      "appName": "my-service"
    }
  }
}
```

## 配置项说明

- `ssh.host`
  远端主机地址。
- `ssh.port`
  SSH 端口。
- `ssh.username`
  SSH 用户名。
- `ssh.password`
  SSH 密码，可与私钥认证二选一或同时提供。
- `ssh.privateKey`
  私钥内容，适合直接通过配置注入。
- `ssh.privateKeyPath`
  私钥文件路径。相对路径会基于配置文件所在目录解析，例如 `./keys/id_ed25519`。
- `ssh.passphrase`
  私钥解密口令，仅在使用 `ssh.privateKey` 或 `ssh.privateKeyPath` 时有效。
- `paths.local`
  本地待发布路径。相对路径会基于配置文件所在目录解析。
- `paths.remote`
  远端正式路径，必须是绝对路径。
- `deployment.mode`
  必填，支持 `directory` 或 `file`。
- `deployment.preDeployCommand`
  可选。本地前置命令，在上传前执行。失败时直接中止部署。
- `deployment.keepBackups`
  保留的最近备份数量，默认 `10`，允许为 `0`。
- `deployment.pm2.appName`
  仅 `file` 模式可用。切换成功后执行 `pm2 reload <appName>`，等待 5 秒，再执行 `pm2 list`。

## 使用命令

使用默认配置文件：

```bash
zup
```

指定配置文件：

```bash
zup --config ./deploy/zup.config.json
```

## 执行流程

### 目录模式

1. 可选执行本地前置命令，例如 `npm run build`
2. 连接 SSH 服务器
3. 上传到 `remote.tmp.时间戳`
4. 如果正式目录已存在，先备份到 `remote.old.时间戳`
5. 将临时目录原子重命名为正式目录
6. 清理超出 `keepBackups` 的旧备份

### 文件模式

1. 可选执行本地前置命令，例如 `cargo build --release`
2. 连接 SSH 服务器
3. 上传到 `remote.tmp.时间戳`
4. 如果正式文件已存在，先备份到 `remote.old.时间戳`
5. 将临时文件原子重命名为正式文件
6. 如果正式文件不是 `.js`、`.cjs`、`.mjs`，自动执行 `chmod 755`
7. 如果配置了 `pm2.appName`，执行 `pm2 reload <appName>`，等待 5 秒，再执行 `pm2 list`
8. 清理超出 `keepBackups` 的旧备份

## 前置命令

`deployment.preDeployCommand` 适合用来先做本地构建，例如：

- `npm run build`
- `pnpm build`
- `cargo build --release`

规则：

- 在配置文件所在目录执行
- 失败时直接中止部署
- 失败时不会继续 SSH 上传

## 日志

CLI 会输出带颜色的分层日志，包含：

- 主阶段
- 子步骤
- 文件级上传明细
- 错误详情

典型输出：

```text
[zup] === 开始执行原子部署
[zup]   | 部署模式:          directory
[zup]   | 本地路径:          /path/to/dist
[zup]   | 正式路径:          /var/www/app
[zup]   | 临时路径:          /var/www/app.tmp.2026-04-10_15.30.00
[zup]   | 备份路径:          /var/www/app.old.2026-04-10_15.30.00

[zup] ==> 上传临时目录
[zup]  -> 递归上传 /path/to/dist -> /var/www/app.tmp.2026-04-10_15.30.00
[zup]   | file assets/index.js -> /var/www/app.tmp.2026-04-10_15.30.00/assets/index.js

[zup] === 部署完成
```

## 远端环境要求

- Linux shell 环境
- 支持常见命令：`mkdir`、`mv`、`rm`、`ls`、`test`
- 文件模式如需自动加执行权限，还需要支持 `chmod`
- 文件模式如配置了 `pm2`，远端还需要安装并可用 `pm2`
- `paths.remote` 的父目录需要对当前 SSH 用户可写

## 常见失败场景

### 本地路径不存在

- `directory` 模式要求 `paths.local` 是目录
- `file` 模式要求 `paths.local` 是文件

### 本地前置命令失败

- 例如 `npm run build` 或 `cargo build --release` 执行失败
- 这种情况下不会继续连接服务器

### SSH 连接失败

- 主机、端口、用户名、密码、私钥或私钥口令配置错误
- 网络不可达
- 服务器禁止当前认证方式

### 上传失败

- 远端磁盘空间不足
- 远端目录无写权限
- SFTP 传输中断

### 备份或切换失败

- 同名临时路径或备份路径已存在
- 远端权限不足
- 目标路径被其他进程占用

### PM2 reload 失败

- `pm2` 未安装
- `pm2.appName` 配置错误
- reload 后服务启动失败

说明：

- `pm2 reload` 失败会让部署失败
- `pm2 list` 失败只会告警，不会让部署失败