Преглед на файлове

feat(install): consolidate checkouts under ~/.dsh/source and route PATH through a stable current symlink

Turtle преди 1 месец
родител
ревизия
bd4bc84283
променени са 5 файла, в които са добавени 120 реда и са изтрити 38 реда
  1. 3 3
      README.i18n.yaml
  2. 9 3
      README.md
  3. 9 3
      README.zh.md
  4. 99 29
      scripts/install.sh
  5. 0 0
      scripts/snapshots/translation-prompt-v4/request-response.expected.json

+ 3 - 3
README.i18n.yaml

@@ -1,6 +1,6 @@
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write
-README.md: 8b3a46081503ac9a29cc791cc302066e33e0f995
-README.zh.md: c73e2c70119d5b82d4629041a7a16848f7acbe92
+#   pnpm run verify-translation-pairing --write README.md
+README.md: f9f7294b42e29132d5cd46c0ab6a5f5265a1d8f3
+README.zh.md: 88cbf8522d8f1a183a48dc7e80858d1a0ced8f0f

+ 9 - 3
README.md

@@ -16,16 +16,22 @@ curl -fsSL https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/m
 
 The installer requires `git` and Node `^22.19 || >=24`, offers to install `pnpm` when it is missing, and prompts for a DeepSeek API key.
 
-The installer clones DeepSeek Harness to `~/.dsh/source`, links `dsh` into `~/.local/bin`, and launches it. Re-running the command updates the checkout. See [`scripts/install.sh`](scripts/install.sh) for alternate install locations and other options.
+The installer keeps every checkout under `~/.dsh/source`: the master clone at `~/.dsh/source/master` and each install's staging checkout as a git worktree `~/.dsh/source/staging-<timestamp>`. The stable symlink `~/.dsh/source/current` points at the active staging worktree, and `dsh` in `~/.local/bin` links to `current/bin/dsh`, so an upgrade repoints one symlink and the `dsh` on PATH never moves. Re-running the command adds a fresh staging worktree from an updated master and repoints `current` at it. See [`scripts/install.sh`](scripts/install.sh) for alternate install locations and other options.
 
 ## Use DeepSeek Harness
 
 ### Web UI
 
-For the recommended local interface, build the frontend after installation and after each update, then start the Web UI:
+For the recommended local interface, build the frontend after installation and after each update, then start the Web UI. Resolve the running checkout from the `dsh` launcher so the command holds regardless of which staging worktree is current (the launcher resolves through the stable `current` symlink):
 
 ```sh
-pnpm --dir ~/.dsh/source run build && pnpm --dir ~/.dsh/source run build:web
+dsh_bin=$(cd "$(dirname "$(command -v dsh)")" && pwd -P)/$(basename "$(command -v dsh)")
+while [ -L "$dsh_bin" ]; do
+  link=$(readlink "$dsh_bin")
+  case $link in /*) dsh_bin=$link ;; *) dsh_bin=$(cd "$(dirname "$dsh_bin")" && cd "$(dirname "$link")" && pwd -P)/$(basename "$link") ;; esac
+done
+dsh_dir=$(cd "$(dirname "$dsh_bin")/.." && pwd -P)
+pnpm --dir "$dsh_dir" run build && pnpm --dir "$dsh_dir" run build:web
 dsh web
 ```
 

+ 9 - 3
README.zh.md

@@ -16,16 +16,22 @@ curl -fsSL https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/m
 
 安装器要求系统已安装 `git` 和 Node `^22.19 || >=24`,缺少 `pnpm` 时可代为安装,并会提示输入 DeepSeek API 密钥。
 
-安装器会将 DeepSeek Harness 克隆到 `~/.dsh/source`,把 `dsh` 链接到 `~/.local/bin`,然后启动它。再次运行该命令会更新源码目录。其他安装位置和选项见 [`scripts/install.sh`](scripts/install.sh)。
+安装器会把所有检出都放在 `~/.dsh/source` 下:master 克隆位于 `~/.dsh/source/master`,每次安装的 staging 检出是一个 git worktree `~/.dsh/source/staging-<时间戳>`。稳定符号链接 `~/.dsh/source/current` 指向当前生效的 staging worktree,`~/.local/bin` 中的 `dsh` 链接到 `current/bin/dsh`,因此升级只需重指一个符号链接,PATH 上的 `dsh` 从不移动。再次运行该命令会基于更新后的 master 新增一个 staging worktree,并把 `current` 重指到它。其他安装位置和选项见 [`scripts/install.sh`](scripts/install.sh)。
 
 ## 使用 DeepSeek Harness
 
 ### Web UI
 
-推荐在本地使用 Web UI。安装完成后以及每次更新后,请先构建前端,再启动 Web UI:
+推荐在本地使用 Web UI。安装完成后以及每次更新后,请先构建前端,再启动 Web UI。通过 `dsh` 启动器解析当前运行的检出,这样无论当前是哪个 staging worktree,命令都成立(启动器会经由稳定的 `current` 符号链接解析)
 
 ```sh
-pnpm --dir ~/.dsh/source run build && pnpm --dir ~/.dsh/source run build:web
+dsh_bin=$(cd "$(dirname "$(command -v dsh)")" && pwd -P)/$(basename "$(command -v dsh)")
+while [ -L "$dsh_bin" ]; do
+  link=$(readlink "$dsh_bin")
+  case $link in /*) dsh_bin=$link ;; *) dsh_bin=$(cd "$(dirname "$dsh_bin")" && cd "$(dirname "$link")" && pwd -P)/$(basename "$link") ;; esac
+done
+dsh_dir=$(cd "$(dirname "$dsh_bin")/.." && pwd -P)
+pnpm --dir "$dsh_dir" run build && pnpm --dir "$dsh_dir" run build:web
 dsh web
 ```
 

+ 99 - 29
scripts/install.sh

@@ -3,16 +3,27 @@
 #
 #   curl -fsSL https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/master/scripts/install.sh | sh
 #
-# It clones the harness to ~/.dsh/source, checks host dependencies (git, Node,
-# pnpm) and offers to install a missing pnpm, runs `pnpm install` (no build —
-# the `bin/dsh` launcher runs the TypeScript source through the repo's own tsx),
-# symlinks `dsh` onto PATH, records your API credentials in the Harness home
-# (`~/.dsh`) dsh reads at boot, and drops you into `dsh`.
+# It clones the harness under ~/.dsh/source (the master clone at
+# ~/.dsh/source/master), adds a per-install staging worktree at
+# ~/.dsh/source/staging-<timestamp> on branch dsh-staging/<timestamp>, checks
+# host dependencies (git, Node, pnpm) and offers to install a missing pnpm, runs
+# `pnpm install` (no build — the `bin/dsh` launcher runs the TypeScript source
+# through the repo's own tsx), points the stable `~/.dsh/source/current` symlink
+# at that staging worktree and symlinks `dsh` onto PATH at `current/bin/dsh`,
+# records your API credentials in the Harness home (`~/.dsh`) dsh reads at boot,
+# and drops you into `dsh`. Keeping every checkout under ~/.dsh/source keeps
+# successive upgrades in one place instead of scattered sibling clones, and lets
+# staging worktrees share the master clone's object store. The PATH symlink
+# resolves through `current`, so an upgrade repoints one stable symlink instead
+# of relinking PATH: the `dsh` on PATH never moves and can never dangle.
 #
 # When run from inside an existing checkout (e.g. `sh scripts/install.sh` rather
-# than `curl ... | sh`) it reuses that checkout and skips the clone/update, leaving
-# the working tree untouched; DSH_REF is ignored in that mode. Setting DSH_SOURCE
-# to a different directory opts back into the normal clone/update path.
+# than `curl ... | sh`) it reuses that checkout in place and skips the
+# clone/worktree setup, leaving the working tree untouched and linking `dsh`
+# straight at that checkout's `bin/dsh` (no `current` indirection — the checkout
+# is not a managed staging worktree under the source container); DSH_REF is
+# ignored in that mode. Setting DSH_SOURCE to a different directory opts back
+# into the normal clone/worktree path.
 #
 # When run through `curl | sh` the script text arrives on stdin, so every
 # prompt and the final launch read the controlling terminal (/dev/tty) directly;
@@ -21,7 +32,9 @@
 # Overridable via environment:
 #   DSH_REF          branch or tag to clone/checkout    (default: master)
 #   DSH_REPO         clone URL                           (default: the GitHub repo)
-#   DSH_SOURCE       checkout location                   (default: ~/.dsh/source)
+#   DSH_SOURCE       source container directory          (default: ~/.dsh/source)
+#   DSH_MASTER       master clone directory              (default: $DSH_SOURCE/master)
+#   DSH_CURRENT      stable symlink to the active worktree (default: $DSH_SOURCE/current)
 #   DSH_BIN_DIR      directory the `dsh` symlink lands in (default: ~/.local/bin)
 #   DSH_HOME         Harness home holding the personal config (default: ~/.dsh)
 # FIXME(install-ts): Move the post-checkout workflow into a tested TypeScript
@@ -30,19 +43,31 @@ set -eu
 
 DSH_REF=${DSH_REF:-master}
 DSH_REPO=${DSH_REPO:-https://github.com/deepseek-harness/deepseek-harness.git}
-# Remember whether the caller pinned a source location before defaulting it, so
-# in-repo detection only repoints an unset DSH_SOURCE.
+# DSH_SOURCE is the container directory that holds the master clone and every
+# staging worktree; DSH_MASTER is the one real clone inside it. Remember whether
+# the caller pinned the source container before defaulting it, so in-repo
+# detection only repoints an unset DSH_SOURCE.
 if [ -n "${DSH_SOURCE:-}" ]; then DSH_SOURCE_EXPLICIT=1; else DSH_SOURCE_EXPLICIT=0; fi
 DSH_SOURCE=${DSH_SOURCE:-$HOME/.dsh/source}
+DSH_MASTER=${DSH_MASTER:-$DSH_SOURCE/master}
+# The stable symlink the PATH launcher resolves through: PATH -> current/bin/dsh
+# -> <staging>/bin/dsh. Fresh installs and upgrades repoint this one symlink; the
+# PATH launcher itself is written once and never moves. In-repo reuse ignores it.
+DSH_CURRENT=${DSH_CURRENT:-$DSH_SOURCE/current}
 DSH_BIN_DIR=${DSH_BIN_DIR:-$HOME/.local/bin}
+# One UTC basic timestamp names this install's staging branch and worktree.
+DSH_STAMP=$(date -u +%Y%m%dT%H%M%SZ)
+DSH_STAGING_BRANCH=dsh-staging/$DSH_STAMP
+DSH_STAGING=$DSH_SOURCE/staging-$DSH_STAMP
 
 # --- in-repo detection ---------------------------------------------------------
 # Under `curl ... | sh` the script text arrives on stdin, so $0 is the shell
 # name and no file path resolves; running a checked-out copy (`sh
 # scripts/install.sh`) makes $0 the script file. When $0 is a readable file whose
 # parent is a scripts/ dir inside a real dsh checkout (bin/dsh launcher present),
-# reuse that checkout and skip the clone. An explicit DSH_SOURCE pointing
-# elsewhere opts back into the clone/update path.
+# reuse that checkout in place — link `dsh` straight at it and skip the
+# clone/worktree setup. An explicit DSH_SOURCE pointing elsewhere opts back into
+# the clone/worktree path.
 IN_REPO=0
 if [ -f "$0" ]; then
   _self_dir=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd -P) || _self_dir=''
@@ -52,7 +77,9 @@ if [ -f "$0" ]; then
       && [ -x "$_repo_root/bin/dsh" ] && [ -f "$_repo_root/scripts/install.sh" ]; then
       if [ "$DSH_SOURCE_EXPLICIT" = 0 ] || [ "$DSH_SOURCE" = "$_repo_root" ]; then
         IN_REPO=1
-        DSH_SOURCE=$_repo_root
+        # In-repo reuse links `dsh` at this checkout as-is; the master/staging
+        # split applies only to fresh clone installs.
+        DSH_STAGING=$_repo_root
       fi
     fi
   fi
@@ -120,7 +147,13 @@ confirm() {
 }
 
 printf '%s\n' "${B}DeepSeek Harness — dsh installer${RST}"
-printf '%ssource %s @ %s%s\n' "$DIM" "$DSH_SOURCE" "$DSH_REF" "$RST"
+if [ "$IN_REPO" = 1 ]; then
+  printf '%ssource %s (in-repo reuse) @ %s%s\n' "$DIM" "$DSH_STAGING" "$DSH_REF" "$RST"
+else
+  printf '%smaster %s @ %s%s\n' "$DIM" "$DSH_MASTER" "$DSH_REF" "$RST"
+  printf '%sstaging %s%s\n' "$DIM" "$DSH_STAGING" "$RST"
+  printf '%scurrent %s%s\n' "$DIM" "$DSH_CURRENT" "$RST"
+fi
 
 # --- 1. dependency check -------------------------------------------------------
 step "Checking dependencies"
@@ -170,36 +203,73 @@ else
   fi
 fi
 
-# --- 2. clone (or update) the source ------------------------------------------
+# --- 2. clone the master and lay out the staging worktree ---------------------
+# Fresh installs keep one real clone at $DSH_MASTER and check the running code
+# out as a git worktree at $DSH_STAGING, so every checkout lives under
+# $DSH_SOURCE and shares one object store. In-repo reuse links `dsh` at the
+# existing checkout untouched.
 if [ "$IN_REPO" = 1 ]; then
-  step "Using existing checkout at $DSH_SOURCE"
+  step "Using existing checkout at $DSH_STAGING"
   info "running from inside the repo — skipping clone (DSH_REF ignored, working tree left untouched)"
 else
-step "Fetching source into $DSH_SOURCE"
-if [ -d "$DSH_SOURCE/.git" ]; then
-  info "existing checkout found — updating"
-  git -C "$DSH_SOURCE" fetch --depth 1 origin "$DSH_REF"
-  # Reset the checkout to the freshly fetched tip. FETCH_HEAD (not
+step "Fetching source into $DSH_MASTER"
+if [ -d "$DSH_MASTER/.git" ]; then
+  info "existing master clone found — updating"
+  git -C "$DSH_MASTER" fetch origin "$DSH_REF"
+  # Reset the master checkout to the freshly fetched tip. FETCH_HEAD (not
   # origin/<ref>) so this resolves for a tag as well as a branch, and -B makes
   # the re-run idempotent whether or not DSH_REF changed since the last install.
-  git -C "$DSH_SOURCE" checkout -q -B "$DSH_REF" FETCH_HEAD
+  git -C "$DSH_MASTER" checkout -q -B "$DSH_REF" FETCH_HEAD
 else
-  mkdir -p "$(dirname "$DSH_SOURCE")"
-  git clone --depth 1 --branch "$DSH_REF" "$DSH_REPO" "$DSH_SOURCE"
+  mkdir -p "$DSH_SOURCE"
+  git clone --branch "$DSH_REF" "$DSH_REPO" "$DSH_MASTER"
+fi
+
+step "Adding staging worktree at $DSH_STAGING"
+[ -e "$DSH_STAGING" ] && die "staging path $DSH_STAGING already exists — remove it or set DSH_SOURCE elsewhere, then re-run."
+# The staging worktree owns the branch dsh runs from; the master clone stays on
+# $DSH_REF as the fetch/upgrade base. Exclude the per-worktree merge lock in the
+# master clone's info/exclude, which every linked worktree inherits.
+git -C "$DSH_MASTER" worktree add -b "$DSH_STAGING_BRANCH" "$DSH_STAGING" FETCH_HEAD 2>/dev/null \
+  || git -C "$DSH_MASTER" worktree add -b "$DSH_STAGING_BRANCH" "$DSH_STAGING" HEAD
+_exclude="$DSH_MASTER/.git/info/exclude"
+if [ -f "$_exclude" ] && ! grep -qxF '.agents/merge.lock' "$_exclude" 2>/dev/null; then
+  printf '.agents/merge.lock\n' >>"$_exclude"
 fi
+mkdir -p "$DSH_STAGING/.agents"
+: >"$DSH_STAGING/.agents/merge.lock"
 fi
 
 # --- 3. install dependencies (no build; the launcher runs from source) --------
 step "Installing dependencies with pnpm (this can take a while)"
-( cd "$DSH_SOURCE" && pnpm install )
+( cd "$DSH_STAGING" && pnpm install )
 
-[ -x "$DSH_SOURCE/bin/dsh" ] || die "launcher $DSH_SOURCE/bin/dsh missing after install — is DSH_REF a branch that ships apps/cli?"
+[ -x "$DSH_STAGING/bin/dsh" ] || die "launcher $DSH_STAGING/bin/dsh missing after install — is DSH_REF a branch that ships apps/cli?"
 
 # --- 4. put `dsh` on PATH ------------------------------------------------------
+# Clone installs go through a stable `current` symlink so an upgrade repoints
+# one symlink (current -> new worktree) and the PATH launcher never moves:
+# PATH/dsh -> current/bin/dsh -> <staging>/bin/dsh. In-repo reuse links PATH
+# straight at the checkout, since that checkout is not a managed worktree.
 step "Linking dsh into $DSH_BIN_DIR"
 mkdir -p "$DSH_BIN_DIR"
-ln -sf "$DSH_SOURCE/bin/dsh" "$DSH_BIN_DIR/dsh"
-info "linked $DSH_BIN_DIR/dsh -> $DSH_SOURCE/bin/dsh"
+if [ "$IN_REPO" = 1 ]; then
+  DSH_LAUNCH_TARGET=$DSH_STAGING/bin/dsh
+  ln -sf "$DSH_LAUNCH_TARGET" "$DSH_BIN_DIR/dsh"
+  info "linked $DSH_BIN_DIR/dsh -> $DSH_LAUNCH_TARGET"
+else
+  # Point `current` at this staging worktree with `ln -sfn`: -f replaces an
+  # existing `current` (re-run or upgrade) and -n stops `ln` from dereferencing
+  # an existing symlink-to-directory and dropping the new link *inside* the old
+  # worktree. `mv` is unusable here — BSD/macOS `mv` follows the existing dir
+  # symlink the same way. The swap is one unlink+symlink pair on a local fs; the
+  # installer holds no other process racing this path.
+  ln -sfn "$DSH_STAGING" "$DSH_CURRENT"
+  info "pointed $DSH_CURRENT -> $DSH_STAGING"
+  DSH_LAUNCH_TARGET=$DSH_CURRENT/bin/dsh
+  ln -sf "$DSH_LAUNCH_TARGET" "$DSH_BIN_DIR/dsh"
+  info "linked $DSH_BIN_DIR/dsh -> $DSH_LAUNCH_TARGET"
+fi
 
 case ":$PATH:" in
   *":$DSH_BIN_DIR:"*) ON_PATH=1 ;;

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
scripts/snapshots/translation-prompt-v4/request-response.expected.json


Някои файлове не бяха показани, защото твърде много файлове са промени