# dsh-git-flow

[English](README.md) | 中文

DeepSeek Harness 工作区 Git 插件：在输入框工具行放一个分支胶囊，点开能看分支、切分支、新建分支，并在面板里勾选文件提交 —— 提交信息由当前会话的模型根据改动自动生成。

```
输入框工具行:  [⑂ feature/login  3]  ← 点击（旁边有 Feature 类型徽标）
                 ├ 搜索分支
                 ├ 本地分支 · 2      ✓ feature/login
                 │                   main
                 ├ 新建分支…
                 ├ 提交…                （有改动时可用）
                 ├ 刷新状态
                 ├ 启动 Git Flow 分支…
                 └ 完成 Git Flow 分支…   （在 flow 分支上且工作区干净时可用）
```

## 安装

两种方式，任选其一。

### 方式一：插件页（GUI）

界面左侧「插件」→ 右上「添加插件」→ 「包名或地址」填：

```
@hunan36/dsh-git-flow
```

点「安装」，装好后在「已安装」里启用（立即启用）。插件页会在**它自己所在的 profile**（通常是 `web`）里执行 `pnpm add` 并把本包写进 `dsh.profile.bundles`，两条都不用你手动做。

也可以填 GitHub 地址 `https://github.com/hunan36/dsh-git-flow`，但 `pnpm` 会把依赖锁到当时的那个 commit，之后**不会自动追新**。走 npm 包名才能比较版本、更新到最新。

装完**重启一次 `dsh web`**（宿主半的改动不会热更新；前端 bundle 会在重启后重新拉取）。

### 方式二：命令行（推荐用独立 profile）

```bash
# 1. 用一个独立 profile，别动现有 web profile
dsh --profile gitflow-web --from-default-profile web

# 2. 装本插件 —— 给 npm 包名，或给 GitHub 地址 / 本地克隆目录的绝对路径
dsh plugin --profile gitflow-web add @hunan36/dsh-git-flow
# dsh plugin --profile gitflow-web add https://github.com/hunan36/dsh-git-flow
# dsh plugin --profile gitflow-web add "$PWD"

# 3. 插件页「立即启用」，或直接在 profile 的 package.json 里加 bundle
#    dsh.profile.bundles += "@hunan36/dsh-git-flow"
#    由于本包声明了 dsh.bundle.patch，启用后它的 cordis.patch.yml 会自动挂载 GitFlow 服务

# 4. 起服务
dsh --profile gitflow-web
```

headless profile 也能装，但没有 `webServer`，`/api/dsh-git-flow/*` 不会注册，浏览器半也不存在 —— 不会报错。

## 更新

DSH 不为第三方插件提供自动更新，需要手动执行一次：

```bash
pnpm --dir ~/.dsh/profiles/web add @hunan36/dsh-git-flow@latest
```

然后**重启 `dsh web`**。宿主半（服务与 API 路由）的改动必须重启才生效；浏览器半在重启后会重新拉取。

查当前版本与最新版本：

```bash
npm view @hunan36/dsh-git-flow version
```

装 GitHub 地址的用户不会被自动追新（`pnpm` 已锁到具体 commit），更新时要显式重装：

```bash
pnpm --dir ~/.dsh/profiles/web add github:hunan36/dsh-git-flow#main
```

### 机器上没有 pnpm

dsh 的插件管理（插件页的「安装」和 `dsh plugin …`）底层调用 pnpm，机器上没有时会直接失败：

```
dsh: pnpm was not found; install pnpm and make it available on PATH.
```

装一个就好（Node 自带 npm）：

```bash
npm install -g pnpm        # 装最新的 10.x
# 或者：corepack enable && corepack prepare pnpm@10 --activate
```

注意别让 Corepack 用它的默认版本 —— `corepack enable` 之后直接跑拿到的是 pnpm **8.15.7**，会踩下面那个 `ERR_PNPM_ADDING_TO_ROOT`。

不想装 pnpm 也有两条路，插件本身不依赖它（包内 `dependencies` 为空）：

- **用 Node 自带的 npm**：`cd ~/.dsh/profiles/<profile> && npm install https://github.com/hunan36/dsh-git-flow`
- **直接拷贝**：把整个 `dsh-git-flow` 目录（含 `lib/`）拷进 `<profile>/node_modules/`，适用于离线或纯手工环境

这两种方式都必须**再补一步**：把 `dsh-git-flow` 加进该 profile `package.json` 的 `dsh.profile.bundles`，然后重启 `dsh web` —— 插件页和 `dsh plugin add` 会自动做这一步，手工装则要自己写，否则包在 `node_modules` 里也不会被加载。

### 让插件页改用 npm（机器上只有 npm）

dsh 的插件页默认执行 `pnpm add <地址>`，但插件管理器的命令是可配置的 —— 在 profile 的 `cordis.patch.yml` 里改成 `npm`，插件页就会用 npm 安装：

```yaml
# ~/.dsh/profiles/<profile>/cordis.patch.yml
- id: plugin-manager
  config:
    pnpmCommand: npm
```

之后按方式一正常操作（填地址 → 安装 → 立即启用）即可。命令行的 `dsh plugin … add` **不吃**这个配置，仍然需要 pnpm。

三点提醒：

- 这属于 dsh 的内部配置项，随版本可能变动；机器上能装 pnpm 时，优先装 pnpm。
- dsh 传给它的参数是 `add <spec>` / `remove <name>` / `view <spec> … --json`，npm 都支持；但 pnpm 专有语义（`-w`、`onlyBuiltDependencies`、lockfile）在 npm 下不存在，**同一个 profile 别混用两种管理器**。
- 界面与日志里那条命令仍会显示成 `pnpm add …`（dsh 的日志标题是写死的），判断实际用的是谁，看 profile 目录里留的是 `package-lock.json` 还是 `pnpm-lock.yaml`。

### 安装报错：ERR_PNPM_ADDING_TO_ROOT

dsh 的 profile 目录本身就是一个 pnpm workspace（`pnpm-workspace.yaml` 里 `packages: [.]`），而 pnpm 8 会把 `pnpm add` 当成「往 workspace 根加依赖」直接拒绝。profile 的 `package.json` 没有 `packageManager` 字段时，Node 自带的 Corepack 会自动补一个（常见是 `pnpm@8.15.7`），于是安装卡在这个检查上 —— 它发生在 pnpm 解析本插件**之前**，与本插件无关。

四种走法任选其一：

1. **插件页（GUI）安装**：那里的命令是 `pnpm add <地址>`，加不了 `-w` —— 先做第 2 步写 `.npmrc`，再点对话框里的「重试」即可（pnpm 8 下实测通过）。
2. 让这个 profile 接受根依赖，之后照常按文档安装：

   ```bash
   echo 'ignore-workspace-root-check=true' >> ~/.dsh/profiles/gitflow-web/.npmrc
   ```

3. 命令行安装时透传 `-w`（dsh 会把多余参数原样转发给 pnpm）：

   ```bash
   dsh plugin --profile gitflow-web add -w https://github.com/hunan36/dsh-git-flow.git
   ```

4. 把这个 profile 切到 pnpm 10（本插件实测版本）：在 profile 目录执行 `corepack use pnpm@10`，或把 Corepack 补进 `package.json` 的 `packageManager` 改成 `pnpm@10.22.0`。

注意第一次失败后，Corepack 已经把 `packageManager: pnpm@8.15.7+sha512…` 写进了该 profile 的 `package.json`，之后这个 profile 的所有插件操作都会用 pnpm 8 —— 用第 4 步把它改掉最省事。

前置版本：Node `^22.19.0 || >=24.0.0`、pnpm 10；本仓库已用 `packageManager: pnpm@10.22.0` 声明。

## 配置

`cordis.patch.yml` 里可改：

| 键 | 默认 | 说明 |
|---|---|---|
| `timeoutMs` | 15000 | 只读/本地 git 命令超时 |
| `pushTimeoutMs` | 120000 | `git push` 超时（等远端） |
| `messageMaxDiffBytes` | 65536 | 喂给模型的 diff 字节上限，超出截断 |
| `messageLanguage` | `en` | 提交信息默认语言，`zh` / `en` |
| `productionBranch` | `main` | 生产分支名；老仓库用 `master` 时改这里 |
| `developBranch` | `develop` | 集成分支名 |
| `featurePrefix` | `feature/` | 功能分支前缀 |
| `releasePrefix` | `release/` | 发布分支前缀 |
| `hotfixPrefix` | `hotfix/` | 热修复分支前缀 |
| `tagPrefix` | `v` | 发布/热修复打完合并后打的标签前缀 |

提交面板的「生成提交信息」旁有 `English | 中文` 切换，它覆盖本次调用的 `messageLanguage`，并把选择记在 `localStorage`——
不选就一直默认英文。

## Git Flow

胶囊菜单最下方的「启动 Git Flow 分支」「完成 Git Flow 分支」按标准 git flow 走：功能与发布分支从 `develop` 切出，
热修复从生产分支切出；完成后 `--no-ff` 合并回去。

| 类型 | 从哪切出 | 完成后合并到 | 打标签 + 改版本号 |
|---|---|---|---|
| `feature/` | `develop` | `develop` | 否 |
| `release/` | `develop` | 生产分支 + `develop` | 是 |
| `hotfix/` | 生产分支 | 生产分支 + `develop` | 是 |

完成顺序：合并 → 打标签 → 提交 `package.json` 的 `version` → 推送分支与标签 → 删除本地分支。

- 仓库还没有 `develop` 时，「启动」会引导一键从生产分支（或当前分支）创建。
- 合并、打标签、改版本号这些完成后不会自动回滚；推送失败会明确提示「本地已完成，但推送失败」，本地成果保留。
- 能提前拦下的失败都在任何写操作之前检查：工作区不干净、标签已存在、目标分支不存在。
- 合并冲突时保留冲突现场让你手动解决，不自动 `--abort` 丢弃改动。
- 仓库根目录没有 `package.json`（非 Node 项目）时自动跳过改版本号这一步。

## 安全边界

- 浏览器只发 `sessionId`，仓库目录由宿主从会话的工作区解析，页面无法指定任意路径执行 git。
- 参数永远是数组，从不拼 shell 字符串；`stdin: 'ignore'`，`GIT_TERMINAL_PROMPT=0`，不会卡在凭据提示上。
- `push` 永不带 `--force` / `--force-with-lease`，只推当前分支的上游（无上游时 `--set-upstream <remote> HEAD:refs/heads/<branch>`）。
- 提交只 `git add` 面板里勾选的文件；一个都没勾返回 `git/no-files-selected`，绝不隐式 `git add -A`。
- 脏工作区切分支默认失败（`git/dirty-worktree`），只有用户在二次确认框里点「强制切换」才带 `--force`。
- 放弃更改只在确认框里点了「放弃更改」才执行：已跟踪路径用 `git restore --source=HEAD --staged --worktree` 回到 HEAD，未跟踪路径用 `git clean -fd` 删除（不带 `-x`，被忽略的文件不动）；有冲突的文件直接拒绝，不做猜测。
- `workspaceRegistry` 用 `ctx.get('workspaceRegistry')` 惰性读取，而不是写进 `static inject`：headless 之类的 profile 没有它，
  插件仍然激活（不会留下 "entry did not activate" 警告），此时任何 git 调用返回 `git/not-a-repository`。
- 没装 git、不是仓库、超时、以及每一次拒绝都走结构化错误码（`git/not-installed` / `git/not-a-repository` / `git/timeout` / …），
  UI 显示对应文案；非仓库或无 git 时胶囊直接不渲染。

## 结构

```
src/
├── index.ts       宿主半入口（默认导出 GitFlow 服务）
├── service.ts     GitFlow：status / branches / checkout / createBranch / commit / discard / push / pull / generateMessage
│                 + gitflowState / startFlow / initDevelop / finishFlow
├── git.ts         GitRunner：subprocess + 净化 env + 超时 + porcelain v2 解析
├── routes.ts      /api/dsh-git-flow/*（仅挂载在有 webServer 的 profile）
├── message.ts     提交信息：ctx.llm.stream 走会话自己的 provider/model，失败退模板
├── contract.ts    宿主↔浏览器共享的纯类型
└── client/
    ├── index.ts       slots.inject('conversation.input.left') + locale 注册
    ├── BranchChip.tsx 状态、菜单、弹窗、toast 的容器
    ├── BranchMenu.tsx 分支胶囊 + 锚定菜单
    ├── CommitDialog.tsx 提交面板 / 新建分支 / 强制切换与放弃更改的确认
    ├── GitFlowDialog.tsx 启动 flow 分支 / 引导创建 develop / 完成 flow 分支
    ├── api.ts         fetch 包装 + 错误码到文案
    ├── styles.ts      弹层宽度、文件行省略所需的少量 CSS（唯一需要真 CSS 的地方）
    └── locales.ts     zh/en 字典（en 按 zh 收窄，缺键即编译错误）
```

## 构建

```bash
pnpm install
pnpm build       # tsc -> lib/types/**，tsdown -> lib/index.js + lib/client.js
```

**`lib/` 随仓库一起提交**（不在 `.gitignore` 里），包内也不再有 `prepare` 脚本 —— 这样从 git 地址安装时不需要构建、不需要 devDependencies，也不需要把包加进 pnpm 的 `onlyBuiltDependencies` 白名单：

- pnpm 10.34+ 会拦截 git 依赖的 `prepare`（`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`），而白名单必须写成 `dsh-git-flow@<地址>#<commit sha>` 这种**带 sha 的精确串**，上游每次提交都会失效；
- 因此改成「产物入库、装上即用」：`dsh plugin add <地址>` 直接拿 `lib/`，实测 pnpm 10.34.5 下 263ms 装完。

改完 `src/` 之后请**先 `pnpm build` 再提交**，把 `lib/` 一起带上（否则别人装到的是旧产物）。

产物契约：

- `lib/index.js`：Node ESM，被 cordis loader 直接 import。
- `lib/client.js`：CJS 工厂包成 `window.__ModuleLoader__.load({ id: "dsh-git-flow", factory: (require) => … })`；
  `react`、`react/jsx-runtime`、`@deepseek-ai/dsh-client-ui-primitives` 保持 external，由页面静态模块表提供。

## 兼容性

针对 `@deepseek-ai/dsh@0.1.6-alpha.2` 实测开发，peer/devDependencies 锁在该版本。dsh 尚在 alpha，
`conversation.input.left`、`ctx.webServer.register`、`ctx.slots.inject` 等契约可能在小版本间漂移；
升级 dsh 后请重跑构建并确认 `lib/client.js` 首行仍是 `window.__ModuleLoader__.load({ id: "dsh-git-flow"`。

## 已知边界

- AI 提交信息要求会话已经有过一轮对话（`request/header` 里记了 provider/model）。没有路由时返回 `git/no-route`，面板提示先发一轮消息；模型失败或输出不合规时用模板兜底（英文 `chore: update N files` / 中文 `chore: 更新 N 个文件`），且会明确告知。
- 提交信息默认英文。只有两个入口能改：面板里的语言切换（按浏览器记住）和 `messageLanguage` 配置；模板兜底跟随同一个选择。
- 远端分支只在本地已有 `refs/remotes/**` 时才可见（插件不做后台 fetch）；「刷新状态」不触发网络。
- 冲突文件不可勾选，需要先在对话里解决冲突。
- `git switch --force` 会丢弃**全部**本地改动（不只是冲突的那些），确认框里的文案就是这么写的；只想丢掉某一个文件的改动，用提交面板里该文件的「放弃更改」。
- 切到一个不存在且无同名远端的引用时，git 自己会失败，条目按 `git/failed` 上报 git 原文（这种情况只能来自过期的 UI 状态，正常菜单里列的都是真实存在的分支）。

## 验证记录（0.1.6-alpha.2）

独立 `DSH_HOME` + profile `gitflow-web`（bundles 追加 `dsh-git-flow`）实测：胶囊文本 == `git branch --show-current`；
弹层行数 == 本地分支 + 仅在远端；搜索过滤；切分支后胶囊与工作区文件同步；脏工作区切换被拦下并弹二次确认（`--force` 才丢弃改动）；
非法分支名报错、合法名创建并切换；勾选单个文件生成 AI 信息（`feat: …`）并只提交该文件（`git log -1 --name-only` 验证）；
提交并推送后 `git ls-remote` 出现该分支且建立 upstream；非 git 工作区会话里胶囊不渲染；`headless` profile 启动无警告、无路由。
