dsh-repo-ops
把 git 與 GitHub 的日常工作包成九個結構化工具,讓 agent 用一次呼叫就知道「我在哪裡、改了什麼、PR 與 CI 現在怎樣」,而不是連續跑十條 shell 指令再自己解析輸出。
功能
| 工具 | 做什麼 | 讀寫 |
|---|---|---|
repo_status |
branch、HEAD、ahead/behind、未提交檔案、預設分支,外加這個分支目前的 PR 與 CI 摘要 | 唯讀 |
repo_diff |
改了哪些檔(+/-)、diff 原文、commit 清單、單一 commit 的內容 | 唯讀 |
repo_commit |
stage 指定檔案並 commit,支援 amend 與 undo(soft reset) | 寫入 |
repo_push |
推分支,回傳推前/推後的 sha 與遠端回應(含 GitHub 給的 PR 網址) | 寫入 |
repo_branch |
分支清單與各自 ahead/behind、開、切、刪、fetch、pull、與 base 同步 | 寫入 |
repo_pr |
PR 的 status / list / view / create / update / comment / reply / diff / checkout / ready / merge | 混合 |
repo_ci |
GitHub Actions 的 status / list / view / logs / watch / rerun / workflow | 混合 |
repo_issue |
issue 的 list / view / create / comment / close / reopen | 混合 |
repo_release |
release 與 tag 的 list / view / create / edit / delete / tags | 混合 |
三個設計取向決定了它長這樣:
- 一個工具一類工作,不是一個子指令一個工具。 九個工具各有 action,而不是四十個包著每個 git 子指令的工具,也不是單一 mega-tool 帶一個四十個值的 enum。
- 破壞性動作要
confirm:true。 裸--force推送、刪未合併分支、合併 PR、重跑 workflow、觸發 workflow_dispatch、刪 release,在confirm不為 true 時一律「只回傳會跑的這一行,但不執行」。工具不会静默動遠端。 - 沒有
--no-verify。 pre-commit 與 pre-push hook 照跑,這是刻意的。
安裝
需要本機有 git(實測 2.51.0)與 gh(實測 2.92.0,且已 gh auth login)。兩者預設走 PATH;找不到時可在設定裡給絕對路徑。
安裝是把 repo 首頁交給 plugin_manager(不是 tarball 網址):請 agent 用 plugin_manager 安裝 github:Joker2377/dsh-repo-ops,或直接指定 release 裡的 tarball,裝完重載 profile。
repo 首頁:https://github.com/Joker2377/dsh-repo-ops
要在 cordis.patch.yml 手動掛載:
- insert:
- id: repo-ops
name: dsh-repo-ops
config: {}
可用的 config 鍵(lib/index.js 的 Config 是 cordis 的 standard-schema:型別不對或打錯鍵名會回可讀的 issues,而不是靜默吃掉):
| 鍵 | 預設 | 意義 |
|---|---|---|
ghPath |
空字串 | gh 不在 PATH 時的絕對路徑,例 C:/Program Files/GitHub CLI/gh.exe |
gitPath |
空字串 | 同上,給 git |
timeoutMs |
60000 | 一條指令的最長執行時間 |
watchTimeoutMs |
300000 | repo_ci 的 watch 預設等待上限 |
readOnly |
false | true 時所有寫入 action 直接拒絕,只准查詢 |
使用
工具是给 agent 用的,典型一輪是這樣:
repo_status() -> branch=feat/login,+2/-0,3 個未提交檔,PR #41 open,checks: 1 FAIL
repo_diff(action=files) -> lib/auth.ts +12 -3 …
repo_commit(paths=["lib/auth.ts"], message="fix: reject empty token")
repo_push() -> 8f2a1c9 -> 41b7d02,遠端回應裡帶著 PR #41 的網址
repo_ci(action=watch, runId=12345) -> completed / failure(等了 74s)
repo_ci(action=logs, runId=12345) -> 失敗 job 的 log 尾端 200 行
repo_pr(action=view) -> 含未結的 review 執行緒,要回話用的 commentId 就在裡面
幾個要記住的參數語意:
path:repo 內任何目錄的絕對路徑,會自動上溯到 repo 根;不給就用這個 session 的工作目錄(不是 host 程序的 cwd)。repo:owner/name或完整 GitHub URL,直接進 gh 的--repo。不給時由本機 git remote 推得。confirm:true:只對破壞性 action 有意義,語意是「我已經跟使用者確認過了」,不是「先試試看」。- 多行的
message/body會被當成單一參數傳給 CLI,不經 shell,所以引號、換行、美元符號都不會出錯。
疑難排解
回報找不到 gh — 先確認本機 gh --version 跑得到。若 DSH 桌面版的啟動環境 PATH 不含安裝目錄,就把絕對路徑寫進 ghPath;工具會列出它試過哪些位置。
未登入 / 401 / 403 — 這是 gh 的憑證問題,工具不會代跑 gh auth login(那需要互動與瀏覽器)。請在本機登入一次,並確認 token 有 repo scope。
repo_status 說 GitHub 那側沒回應,但 git 那側正常 — 這是刻意設計:兩側獨立,任一側壞了另一側照常回報,結尾會附一行說明哪側失敗、原因為何。
ahead/behind 顯示 null — 這個分支還沒有 upstream。第一次推請用 repo_push(setUpstream:true)。
repo_pr(action=view) 找不到 PR — 該分支沒有開著的 PR;改用 repo_pr(action=list),或直接給 number。
CI 的 watch 回報 timedOut — 是等待到期而 run 還在跑,不是失敗。再呼叫一次 watch 就續等。
升級到 0.1.2 之前那個版本載入就炸(Cannot read properties of undefined (reading 'validate')) — 0.1.1 把 Config 寫成普通的 { type, default } 物件,但 cordis 呼叫的是 Config['~standard'].validate(...),於是外掛在 profile 載入階段就拋錯、九個工具一個都不會出現。0.1.2 起 Config 是真正的 standard-schema,並有測試直接跑 cordis 那四行 resolveConfig。裝了 0.1.1 的話先 plugin_manager 移除再重裝。
運作原理
lib/index.js 讀 config -> 建 runner -> 建 git/github -> 在 ctx.effect 內註冊九個工具
lib/cli.js 二進位探索(設定值 -> PATH -> 常見安裝路徑)+ spawn(argv 陣列、逾時 kill、
暫態錯誤重試、缺檔 30 秒冷卻)+ secret 遮罩
lib/git.js 只用 git 的穩定介面:--porcelain=v2、--format、for-each-ref、ls-remote
lib/github.js gh 的 --json 為主,欄位名集中在可匯出的常數(全部扁平,理由見下),回傳前正規化成 bucket
lib/tools.js 九個 defineTool:參數表、read-only 與併行宣告、把 args 折成模組呼叫
lib/render.js 資料 -> 一行一事實的文字;缺東西時這行要順便講下一步
test/ 全部注入假 runner,不碰真實 CLI 與網路
幾個值得知道的實作決定:
- 不走 shell。 一律 spawn(command, argv[]),避免引號注入,也讓多行 commit message 正常傳下去。
- 二進位探索不只靠 PATH。 從桌面版啟動時 PATH 可能不含
C:/Program Files/GitHub CLI,所以依序試設定值、PATH、固定安裝路徑,並帶 30 秒冷卻避免每次重探。 - per-session 的工作目錄取自 exec.agent.session.header.cwd。process.cwd() 是 host 的啟動目錄,對 web 部署來說是安裝目錄而不是專案目錄,因此只作最後備援。
- ciWatch 用輪詢而非 gh run watch。 後者會佔滿整個 timeout,也無法中途回報進度。
- 失敗一律回字串,不擲例外。 讓模型看得到原因與下一步,比讓工具顯示成一個失敗狀態有用。
- gh 的 --json 一律只寫扁平欄位名。 gh 不支援
assets{name}、jobs{name}這種巢狀選欄(實測 2.92 會回Unknown JSON field),所以每個常數都是頂層欄位,欄位內容由 mapper 收窄;要列附檔就是--json assets一欄拿整個物件。 gh repo view沒有--repo。 它的對象是位置參數,所以 repoInfo 走gh repo view <owner/name> --json …,其餘子命令才用--repo。- 受限沙箱裡不能開管線子程序。 桌面沙箱會讓
spawn的 piped stdio 直接 EPERM(git 自己的 sh.exe 也開不了 signal pipe),所以 lib/cli.js 失敗時改把 stdout/stderr 接到暫存檔再試一次;這條路讓測試與 smoke 在受限環境仍能跑真的 git。
開發
git clone https://github.com/Joker2377/dsh-repo-ops.git
cd dsh-repo-ops
npm run check # 形狀檢查:manifest / patch / 工具面 / README 段落 / 測試是否離線
npm test # 逐檔 import 跑完 153 個測試,不需要 npm install
npm run test:spawn # 同一套測試改用 node --test(每檔一個子程序;受限沙箱內會 EPERM)
node scripts/smoke.mjs # 真的 git:在暫存 repo 上跑完讀寫流程,不碰網路
$env:SMOKE_ONLINE=1; node scripts/smoke.mjs # 再加唯讀 gh:pr / ci list+view / issue / release / tags
node scripts/verify-gh.mjs # 逐一打真的 gh:13 個唯讀呼叫都要拿到資料,任何一個失敗就 exit 1
單元測試全部注入假 runner,不碰真實 git/gh,也不碰網路,只要 Node 22+ 就能跑;smoke 才是碰真 CLI 的那一層。 scripts/check.mjs 是 CI 的第一道關卡:它檢查「裝得起來嗎」這一類會讓單元測試全綠、但安裝壞掉的問題。
已驗證到哪裡
| 面 | 證據 |
|---|---|
| 153 個單元/整合測試 | 假 runner;CI 矩陣 4 個 job(ubuntu-latest + windows-latest × node 22/24)全綠 |
| 真的 git | node scripts/smoke.mjs:init、status、diff、commit、branch、push 失敗訊息,全部在暫存 repo 上實際跑過 |
| 真的 gh(唯讀) | node scripts/verify-gh.mjs 13/13:authStatus、repoInfo、prList、prView、prChecks、ciList、ciView、ciStatus、issueList、issueView、releaseList、releaseView、workflowList 對 Joker2377/dsh-plugin-updates 與 cli/cli 實際拿到資料 |
| 真的 gh(寫入) | repo_push 推過這個 repo 自己的 main 與 tag(含 --set-upstream 後的 push);repo_release 的 create/merge/rerun 等動作仍未在真實 repo 上執行,只驗到 argv 與 confirm 閘門 |
| 打包與安裝 | release 上的 dsh-repo-ops-0.1.0.tgz(58 KB,只含 lib/、patch、README、LICENSE)已用 pnpm add <tgz> 裝進一個暫存 profile,apply() 註冊 9 個工具、dispose 兩次也不重複;該 profile 用完已刪除。桌面版 profile 尚未安裝 |
已知限制
- 需要本機
git與gh。這版不自帶 libgit2,也沒有內建 REST 備援通道。 - gh 只在已
gh auth login的前提下工作;工具不處理裝置流程登入。 - GitHub 專屬。GitLab、Gitea 的 PR 與 CI 面不支援(git 那側走 porcelain,本來就與托管無關)。
- 不碰 git-lfs、submodule 與 worktree 建立(
create_worktree是 host 自己的工具),也不做互動式 rebase 流程。 - 一次只針對一個 repo;跨 repo 的工作請多次呼叫,各給自己的 path 或 repo。
- 輸出是給模型讀的紧凑文字,不是給人看的彩色報表;要做漂亮報表請用 host 的 artifact。
No comments yet. Be the first to write one.