# 本地开发脚本（dev.sh）清理与 AI 自动启停支持

- **状态**：🟡 部分解决（worktree 隔离阻塞点已通过 agent-pool 解决，dev.sh 其余清理待做）
- **发现于**：2026-04-25 与 Claude 的脚本审查会话
- **优先级**：低（关键阻塞点已解决；剩余项是 dev.sh 内部清理，无 AI 阻塞）
- **关联文件**：`scripts/dev/dev.sh`、`scripts/deploy/deploy.sh`、`CLAUDE.md`「本地启动约定」、`scripts/dev/agent-pool/`

## 2026-05-09 更新：worktree 隔离阻塞点已解决

- 通过 **agent-pool（开发 agent 池）** 实现：常驻 N 个预热 slot，每个独立端口段 / PG / Redis 容器 / .env / Caddy 域名。
- AI 通过 `agent-claim.sh` 拿 slot，秒级完成（vs setup-worktree.sh 一次 80-90 秒），加速 ~140×。
- 协作者友好：`FFOA_AGENT_POOL_ENABLED=false` 一票禁用 fallback 到 setup-worktree.sh，CI 安全。
- 详见 `scripts/dev/agent-pool/README.md` 和 `docs/standards/05-development-workflow.md` § "并行开发选择树"。
- **下方第 47 行 "AI 自动启停的唯一阻塞点" 现状：已解开**。剩下的 dev.sh 内部 P0/P1/P2 问题与本阻塞点无关，按优先级慢慢推。

## 背景与决策

### 为什么保留两套脚本（dev.sh + deploy.sh）

讨论过把本地启动统一到 `deploy.sh`（它已支持 `dev/uat/production` 三套环境），结论是**不合并**：

1. **`deploy.sh dev start` 不是热重载**：`create_pm2_config`（行 1560-1617）写死 `NODE_ENV=production`、跑 `dist/main.js` 和 `next start`，启动前要求 `dist/` 与 `.next/` 已存在。它的语义是"本地模拟生产"，不是日常开发。
2. **`deploy.sh` 要求 bash ≥ 4.0**（用了关联数组 `declare -A ENV_CONFIG`）。macOS 自带 bash 3.2，需要 `brew install bash` + 改 PATH 才能跑。`dev.sh` 不依赖 bash 4，是 mac 用户的兼容兜底。
3. **本地不需要 PM2**：PM2 解决"后台常驻 + 自动重启 + cluster + 日志归集"。本地热重载下，`nohup &` + 文件日志已够，PM2 反而遮蔽报错输出。

→ 结论：`dev.sh`（本地开发，nohup + nest start:dev / next dev 热重载）和 `deploy.sh`（部署 + 模拟生产，PM2 + build 产物）**职责清晰分工**，不合并。

### mac 上跑 deploy.sh 的兼容操作（已验证）

如果 mac 用户需要跑 `deploy.sh`（比如本地模拟生产），步骤：

1. `brew install bash`（装到 `/opt/homebrew/bin/bash`，**不会**替换 `/bin/bash`）
2. `echo /opt/homebrew/bin/bash | sudo tee -a /etc/shells` 加白名单
3. `chsh -s /opt/homebrew/bin/bash` 改默认 shell
4. **完全 Cmd+Q 退出 Terminal.app 再重开**（关窗口不够）
5. 在 `~/.bash_profile` 加 `export PATH="/opt/homebrew/bin:$PATH"`，让 `bash xxx.sh` 也走 5.x

陷阱：`chsh: no changes made` 通常意味着 `dscl . -read /Users/$USER UserShell` 已经是目标值，但当前终端窗口是 chsh 之前开的，需要重开。

## dev.sh 已知问题清单（按优先级）

### P0 — 阻塞 / 数据风险

#### 1. ENV_FILE 软链接逻辑会毁用户文件（行 240-244）

```bash
if [ ! -e "$ENV_FILE" ] || [ ! -L "$ENV_FILE" ]; then
    ln -sf ../.env .env
```

判断是"`docker/.env` 不存在 **或** 不是软链接"。如果用户在 `docker/.env` 写了一份**普通文件**配置，脚本会**直接覆盖成软链接**，原配置丢失。

**修复**：去掉 `|| [ ! -L … ]` 条件，只在文件不存在时创建软链接。

#### 2. worktree 端口/容器隔离（**AI 自动启停的唯一阻塞点**）

- `PROJECT_NAME="ffws-dev"`、`CONTAINER_PREFIX="ffws-dev"` 是硬编码常量（行 42-43）
- 多个 worktree 同时跑 `dev.sh up` → docker compose project 同名 → 实际复用同一组容器，schema 互踩
- 端口同理：3000/3001/3002/3003 全局唯一，第二个 worktree 撞端口起不来
- `deploy.sh` 也有同样问题（CONTAINER_PREFIX 同样硬编码 `ffws-dev`），换脚本不解决

**修复方向**：
- worktree 创建时按编号偏移端口（3000 → 3100 → 3200…）
- 容器前缀注入 worktree 名（`ffws-dev-rbac-audit`）
- 在 `setup-worktree.sh` 里写入各自的 `.env`，dev.sh 读取后透传给 docker compose
- 或者：所有 worktree 共享同一组 Docker 容器（端口固定），但应用层端口偏移（`PORT=3100` 等）

### P1 — 行为不一致 / 文档脱节

#### 3. `cmd_up` 没调用 `load_env`，端口默认值与 help 不符

- 行 46-49：默认值 `POSTGRES_PORT:-5432`、`REDIS_PORT:-6379`
- 行 226（help）：声称 `PostgreSQL: localhost:3002`、`Redis: localhost:3003`
- `cmd_up` 只调 `check_env_file`，**不调 `load_env`**。如果 `.env` 没显式设 `POSTGRES_PORT=3002`，输出会是 5432
- `cmd_db_*` 反而都调了 `load_env`，行为不一致

**修复**：`cmd_up` 开头加 `load_env`。

#### 4. 与 CLAUDE.md 协调

- CLAUDE.md 标注 `init:permissions` / `init:admin` **已废弃**，推荐 `db:seed`，但 `dev.sh init:permissions` 仍直接 `npm run init:permissions`
- CLAUDE.md「本地启动约定」步骤 5/6 写的是 `cd backend && npm run start:dev`，没用 `dev.sh run/start`。两套并行启动路径，没人讲哪种是默认

**修复**：删 `dev.sh` 的 init:* 命令（或让它转调 `db:seed`）；CLAUDE.md 把启动步骤改成 `dev.sh start`。

### P2 — 自动化阻碍

#### 5. `run` 命令悄悄变成后台模式（行 832-849）

- help 行 179 标 `run` 为"启动开发服务器 ⭐"，未说后台
- 代码里 `run`（不带 `--backend`/`--frontend`）和 `start` **几乎一模一样**：都 nohup + 写 PID
- 用户预期 `run` 是前台（Ctrl-C 终止），实际命令返回但进程还在跑

**修复**：明确语义。建议：
- `run` = 前台（同时启动会用 `concurrently` 或拆两个终端）
- `start` = 后台 + PID + 日志落盘（AI 自动启停用这个）
- `stop` = 配套停 `start`

#### 6. `cmd_run --backend` / `--frontend` 不写 PID 文件

行 825 / 830 是真正的前台 `npm run start:dev` / `npm run dev`，但因为没写 PID，后续 `dev.sh stop --backend` 只能靠 `kill_port` 端口扫描，不是精确停止。

**修复**：要么前台分支也用 trap 写临时 PID，要么文档明示"前台模式由 Ctrl-C 终止，stop 不适用"。

#### 7. `status` 不看应用进程，只看 docker

`cmd_status` 行 501-511 只跑 `docker compose … ps`。AI 想自动化时，没法区分"应用已启动 / 已退出 / PID 还在但端口被别的进程占了"。

**修复**：`cmd_status` 合并应用进程检查（PID 是否存活 + 端口监听 + 健康端点），把 `cmd_health` 的逻辑并进来或两者合一。

### P3 — 健壮性 / UX

#### 8. help 文本里所有路径都是 `bash scripts/dev.sh ...`（行 149/205/207/...）

脚本实际位置是 `scripts/dev/dev.sh`，help 复制粘贴出来给用户用会 "command not found"。

**修复**：批量改为 `bash scripts/dev/dev.sh ...`。

#### 9. `kill_port` 的 PID 提取脆弱

- 行 117：`ss -tlnp | … sed -E 's/.*pid=([0-9]+).*/\1/'`，不匹配时 sed 原样输出整行
- 行 119：`fuser $port/tcp` 输出 PID 时混杂 stderr 内容
- 后面 `kill -9 $pid` 拿到非数字会报错

**修复**：加 `grep -oE '^[0-9]+$'` 或 `awk` 严格过滤数字。

#### 10. 重复 `mkdir` / 空通配符 / 参数透传等小冗余

- 行 53/54/55 + 101 重复创建日志目录
- 行 1154 `rm -rf "$LOGS_DIR"/*`，nullglob 关闭时 `*` 字面传入
- `cmd_restart` 把 up 参数透传给 down

低优先级，顺手修。

## 推荐方案：渐进改造

### 阶段 1：抽共享环境配置（小改造）

把 `dev.sh` 和 `deploy.sh` 都用到的端口/容器名/env 文件路径抽成 `scripts/common/env-config.sh`，**用普通变量（不用 bash 4 关联数组）**，两个脚本都 `source`。

收益：改端口、改容器前缀只动一处；mac 兼容性不变。

```bash
# scripts/common/env-config.sh 示例（兼容 bash 3.2）
DEV_FRONTEND_PORT=3000
DEV_BACKEND_PORT=3001
DEV_POSTGRES_PORT=3002
# ...
UAT_FRONTEND_PORT=7000
# ...
```

### 阶段 2：worktree 隔离（解锁 AI 自动启停）

具体方案细化时再设计。候选：
1. **方案 A（端口偏移）**：每个 worktree 在创建时分配 offset（0/100/200/...），写入 `.env`
2. **方案 B（独立容器栈）**：CONTAINER_PREFIX 注入 worktree 路径 hash
3. **方案 C（共享 Docker，应用层偏移）**：Docker 端口固定，只在应用 PORT 上偏移

### 阶段 3：修 P0/P1/P2 清单

按上面优先级逐项修，每个修改后跑一遍 dev.sh 各命令冒烟测试。

### 阶段 4：写 AI 测试自动启停 SOP

修完之后在 CLAUDE.md「本地启动约定」加一节，规定 AI 何时用 `dev.sh start` 起后台、何时 `dev.sh stop`、出错怎么收尾。

## 临时缓解（在改造完成前）

- AI 默认**不主动启动** `npm run start:dev` / `npm run dev` 等长驻进程；需要测试时由用户先在终端起好，AI 只跑测试
- 多 worktree 同时跑时，明确告诉 AI"端口是公共的，先看 `dev.sh status` 别盲启"
- `docker/.env` 用户手动维护时确认是软链接，避免被 dev.sh 覆盖

## 工作量估算

| 阶段 | 估算 |
|---|---|
| 阶段 1（共享配置） | 1-2 小时 |
| 阶段 2（worktree 隔离） | 半天到 1 天（含设计 + 测试） |
| 阶段 3（清单修复） | 半天 |
| 阶段 4（SOP 文档） | 1 小时 |

合计 1-2 个工作日，可拆分多次推进。
