メインコンテンツへスキップ
H

dsh-git-flow

hunan36/dsh-git-flow

Branch chip in the composer tool row that switches, creates, and refreshes workspace branches, commits a checked subset of changed files with a message the session's own model writes from the diff, and pushes the current branch.

インストール

dsh plugin --profile web add github:hunan36/dsh-git-flow

README

dsh-git-flow

English | 中文

A DeepSeek Harness workspace git plugin: a branch chip in the composer tool row. Open it to browse branches, switch branches, create one, and commit a checked subset of files — the commit message is written by the session's own model from the diff.

composer tool row:  [⑂ feature/login  3]  ← click
                      ├ Search branches
                      ├ Local branches · 2   ✓ feature/login
                      │                        main
                      ├ New branch…
                      ├ Commit…                (enabled while the tree is dirty)
                      └ Refresh status

Install

Two ways in; either one is enough.

From the plugin page (GUI)

Sidebar → Plugins → Add plugin, then put this in "Package or address":

https://github.com/hunan36/dsh-git-flow

Press Install and enable it afterwards ("Enable"). That path runs pnpm add <spec> in the profile serving the page (usually web) and writes this package into dsh.profile.bundles for you — you do neither by hand. The URL is accepted with or without the .git suffix.

Reload the page (⌘⇧R) afterwards so the browser fetches the new client bundle; if that profile does not have patchReload: live, restart dsh web once for the host side.

# 1. Create a dedicated profile (leaves your existing web profile alone)
dsh --profile gitflow-web --from-default-profile web

# 2. Add the plugin — a git URL, or the absolute path of a local checkout
dsh plugin --profile gitflow-web add https://github.com/hunan36/dsh-git-flow
# dsh plugin --profile gitflow-web add "$PWD"

# 3. Enable it on the plugin page ("Enable"), or edit the bundle list directly:
#    dsh.profile.bundles += "dsh-git-flow"
#    The package declares dsh.bundle.patch, so enabling it mounts the GitFlow
#    service from the bundle's own cordis.patch.yml.

# 4. Boot
dsh --profile gitflow-web

The headless profile installs too, but it has no webServer: no /api/dsh-git-flow/* route is registered, the browser half does not exist there, and boot reports no error.

A machine without pnpm

dsh's plugin management — the plugin page's Install, and dsh plugin … — shells out to pnpm, and fails outright when there is none:

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

Install one (Node ships npm):

npm install -g pnpm        # the latest 10.x
# or: corepack enable && corepack prepare pnpm@10 --activate

Do not let Corepack use its own default: after a bare corepack enable you get pnpm 8.15.7, which walks straight into the ERR_PNPM_ADDING_TO_ROOT failure below.

If you would rather not install pnpm at all, the plugin itself does not need it (its dependencies are empty):

  • Use Node's npm: cd ~/.dsh/profiles/<profile> && npm install https://github.com/hunan36/dsh-git-flow
  • Copy the directory: drop the whole dsh-git-flow folder (including lib/) into <profile>/node_modules/, for offline or fully manual setups

Both need one extra step: add dsh-git-flow to that profile's dsh.profile.bundles in its package.json, then restart dsh web. The plugin page and dsh plugin add do that for you; a manual install does not, and the package stays unloaded even though it sits in node_modules.

Point the plugin page at npm (a machine that only has npm)

The plugin page runs pnpm add <spec> by default, but the plugin manager's command is configurable — set it to npm in the profile's cordis.patch.yml and the page installs with npm:

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

Then use the plugin page exactly as usual (paste the address → Install → Enable). The CLI's dsh plugin … add does not honour this setting and still needs pnpm.

Three caveats:

  • This is an internal dsh setting and may move between versions; prefer installing pnpm when the machine allows it.
  • The arguments dsh passes are add <spec>, remove <name>, and view <spec> … --json, all of which npm accepts; but pnpm-specific semantics (-w, onlyBuiltDependencies, lockfiles) do not exist under npm, so do not mix both managers in one profile.
  • The command line shown in the UI and logs still reads pnpm add … (dsh builds that label literally). To tell which manager really ran, look for package-lock.json versus pnpm-lock.yaml in the profile directory.

Installation fails with ERR_PNPM_ADDING_TO_ROOT

A dsh profile directory is itself a pnpm workspace (pnpm-workspace.yaml with packages: [.]), and pnpm 8 treats pnpm add there as adding to a workspace root — which it refuses. When the profile has no packageManager field, Node's Corepack fills one in (commonly pnpm@8.15.7), and the install then dies on that check before it ever resolves this plugin, so the failure is not about this package.

Any one of these gets you through:

  1. Installing from the plugin page (GUI): that path runs pnpm add <spec> and cannot pass -w, so do step 2 first (write the .npmrc) and then press "Retry" in the dialog — the same command then succeeds under pnpm 8.

  2. Teach the profile to accept a root dependency, then install exactly as documented:

    echo 'ignore-workspace-root-check=true' >> ~/.dsh/profiles/gitflow-web/.npmrc
    
  3. From the CLI, pass -w through (dsh forwards extra arguments to pnpm):

    dsh plugin --profile gitflow-web add -w https://github.com/hunan36/dsh-git-flow.git
    
  4. Put the profile on pnpm 10 (the version this plugin is tested with): run corepack use pnpm@10 in the profile, or set the packageManager field Corepack added to pnpm@10.22.0.

Note that after the first failure Corepack has already written packageManager: pnpm@8.15.7+sha512… into that profile's package.json, so every later plugin operation there uses pnpm 8 — step 4 is the durable fix.

Requires Node ^22.19.0 || >=24.0.0 and pnpm 10; this repository pins it with packageManager: pnpm@10.22.0.

Configuration

Set in cordis.patch.yml:

KeyDefaultMeaning
timeoutMs15000Deadline for read-only and local git commands
pushTimeoutMs120000Deadline for git push, which waits on a remote
messageMaxDiffBytes65536Cap on the diff bytes handed to the model (truncated past it)
messageLanguageenDefault commit-message language, zh or en

The commit panel carries an English | 中文 switch beside "Draft message". It overrides messageLanguage for that call and remembers the choice in localStorage, so English stays the default until you pick otherwise.

Security boundaries

  • The browser sends only a sessionId. The host resolves the repository from that session's workspace, so a page can never name a directory for git to run in.
  • Arguments are always an array, never a shell string, with stdin: 'ignore' and GIT_TERMINAL_PROMPT=0, so a credential prompt can never hang a call.
  • push never passes --force or --force-with-lease, and only pushes the current branch's upstream (with --set-upstream <remote> HEAD:refs/heads/<branch> when it has none).
  • Commits stage only the files checked in the panel. An empty selection answers git/no-files-selected and never falls back to an implicit git add -A.
  • Switching branches on a dirty worktree fails by default (git/dirty-worktree). Only the user's second confirmation in the dialog sends --force.
  • Discarding runs only after the destructive action is picked in a confirmation dialog: tracked paths go back to HEAD with git restore --source=HEAD --staged --worktree, untracked paths are removed with git clean -fd (no -x, so ignored files stay), and a conflicted path is refused rather than guessed at.
  • workspaceRegistry is read lazily through ctx.get('workspaceRegistry') instead of being declared in static inject, so a profile without one (the shipped headless profile) still activates this entry rather than leaving a permanently pending plugin; every git call there answers git/not-a-repository.
  • A missing git, a non-repository workspace, a timeout, and every refusal travel as structured codes (git/not-installed, git/not-a-repository, git/timeout, …) that the UI renders as copy. Outside a repository — or without git — the chip renders nothing at all.

Layout

src/
├── index.ts       host entry (default-exports the GitFlow service)
├── service.ts     GitFlow: status / branches / checkout / createBranch / commit / discard / push / generateMessage
├── git.ts         GitRunner: subprocess + scrubbed env + timeout + porcelain v2 parsing
├── routes.ts      /api/dsh-git-flow/* (registered only where a webServer exists)
├── message.ts     commit message: ctx.llm.stream on the session's own provider/model, template fallback
├── contract.ts    host↔browser types only
└── client/
    ├── index.ts        slots.inject('conversation.input.left') + locale registration
    ├── BranchChip.tsx  container: status, menu, dialogs, toast
    ├── BranchMenu.tsx  the chip and its anchored menu
    ├── CommitDialog.tsx commit panel / new branch / forced-switch and discard confirmations
    ├── api.ts          fetch wrapper + error codes to copy
    ├── styles.ts       the few CSS rules the popover width and file-row ellipsis need
    └── locales.ts      zh/en dictionaries (en is typed against zh, so a missing key is a compile error)

Build

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

lib/ is committed to the repository (it is not in .gitignore) and the package no longer carries a prepare script, so installing from the git URL needs no build, no dev dependencies, and no entry in pnpm's onlyBuiltDependencies allowlist:

  • pnpm 10.34+ blocks a git dependency's prepare (ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED), and the allowlist entry it demands is dsh-git-flow@<spec>#<commit sha> — a string that goes stale on every upstream commit;
  • shipping the build instead makes dsh plugin add <spec> take lib/ as-is: measured at 263 ms under pnpm 10.34.5.

After editing src/, run pnpm build and commit lib/ along with it, or everyone installing from git gets the previous build.

Artifact contract:

  • lib/index.js: Node ESM, imported directly by the cordis loader.
  • lib/client.js: a CJS factory wrapped as window.__ModuleLoader__.load({ id: "dsh-git-flow", factory: (require) => … }); react, react/jsx-runtime, and @deepseek-ai/dsh-client-ui-primitives stay external and come from the page's static module table.

Compatibility

Developed and verified against @deepseek-ai/dsh@0.1.6-alpha.2, with peer and dev dependencies pinned to it. dsh is still alpha, so contracts such as conversation.input.left, ctx.webServer.register, and ctx.slots.inject may drift between minor versions. After upgrading dsh, rebuild and confirm the first line of lib/client.js is still window.__ModuleLoader__.load({ id: "dsh-git-flow".

Known limits

  • An AI commit message needs one turn already sent in the session (the provider/model recorded in request/header). Without a route the panel answers git/no-route and tells you to send one message first; when the model fails or returns nothing usable, the chore: update N files template fills in and says so.
  • Messages default to English. The panel's language switch (remembered per browser) and the messageLanguage config are the only two inputs; the template fallback follows the same choice.
  • Remote branches appear only once refs/remotes/** exists locally (the plugin never fetches in the background); "Refresh status" makes no network call.
  • Conflicted files cannot be checked; resolve the conflict in the conversation first.
  • git switch --force discards all local changes, not just the conflicting ones — the confirmation copy says exactly that. To drop a single file's changes instead, use that file's Discard action in the commit panel.
  • Switching to a ref that exists nowhere fails inside git and is reported as git/failed with git's own text. Only stale UI state can reach that path, since the menu lists branches that exist.

Verification (0.1.6-alpha.2)

Measured on an isolated DSH_HOME with profile gitflow-web (dsh-git-flow appended to dsh.profile.bundles): the chip text equals git branch --show-current; the menu lists every local branch plus each remote-only one; search filters rows; switching moves both the chip and the worktree files; a dirty switch is refused and only confirms --force after a second dialog; an invalid branch name errors while a valid one is created and switched to; with a single file checked, the model returned a feat: … message and git log -1 --name-only showed only that file; commit-and-push made the branch appear in git ls-remote with its upstream set; a session whose workspace is not a repository renders no chip; the headless profile boots with no warning and no routes.

関連プラグイン