install.sh 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. #!/bin/sh
  2. # dsh one-line installer.
  3. #
  4. # curl -fsSL https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/master/scripts/install.sh | sh
  5. #
  6. # It clones the harness to ~/.dsh/source, checks host dependencies (git, Node,
  7. # pnpm) and offers to install a missing pnpm, runs `pnpm install` (no build —
  8. # the `bin/dsh` launcher runs the TypeScript source through the repo's own tsx),
  9. # symlinks `dsh` onto PATH, records your API credentials in the Harness home
  10. # (`~/.dsh`) dsh reads at boot, and drops you into `dsh`.
  11. #
  12. # When run from inside an existing checkout (e.g. `sh scripts/install.sh` rather
  13. # than `curl ... | sh`) it reuses that checkout and skips the clone/update, leaving
  14. # the working tree untouched; DSH_REF is ignored in that mode. Setting DSH_SOURCE
  15. # to a different directory opts back into the normal clone/update path.
  16. #
  17. # When run through `curl | sh` the script text arrives on stdin, so every
  18. # prompt and the final launch read the controlling terminal (/dev/tty) directly;
  19. # with no terminal the script prints the manual next steps instead.
  20. #
  21. # Overridable via environment:
  22. # DSH_REF branch or tag to clone/checkout (default: master)
  23. # DSH_REPO clone URL (default: the GitHub repo)
  24. # DSH_SOURCE checkout location (default: ~/.dsh/source)
  25. # DSH_BIN_DIR directory the `dsh` symlink lands in (default: ~/.local/bin)
  26. # DSH_HOME Harness home holding the personal config (default: ~/.dsh)
  27. # FIXME(install-ts): Move the post-checkout workflow into a tested TypeScript
  28. # entrypoint; keep this POSIX shell file as the curl/source bootstrap.
  29. set -eu
  30. DSH_REF=${DSH_REF:-master}
  31. DSH_REPO=${DSH_REPO:-https://github.com/deepseek-harness/deepseek-harness.git}
  32. # Remember whether the caller pinned a source location before defaulting it, so
  33. # in-repo detection only repoints an unset DSH_SOURCE.
  34. if [ -n "${DSH_SOURCE:-}" ]; then DSH_SOURCE_EXPLICIT=1; else DSH_SOURCE_EXPLICIT=0; fi
  35. DSH_SOURCE=${DSH_SOURCE:-$HOME/.dsh/source}
  36. DSH_BIN_DIR=${DSH_BIN_DIR:-$HOME/.local/bin}
  37. # --- in-repo detection ---------------------------------------------------------
  38. # Under `curl ... | sh` the script text arrives on stdin, so $0 is the shell
  39. # name and no file path resolves; running a checked-out copy (`sh
  40. # scripts/install.sh`) makes $0 the script file. When $0 is a readable file whose
  41. # parent is a scripts/ dir inside a real dsh checkout (bin/dsh launcher present),
  42. # reuse that checkout and skip the clone. An explicit DSH_SOURCE pointing
  43. # elsewhere opts back into the clone/update path.
  44. IN_REPO=0
  45. if [ -f "$0" ]; then
  46. _self_dir=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd -P) || _self_dir=''
  47. if [ -n "$_self_dir" ]; then
  48. _repo_root=$(dirname -- "$_self_dir")
  49. if [ "$(basename -- "$_self_dir")" = scripts ] \
  50. && [ -x "$_repo_root/bin/dsh" ] && [ -f "$_repo_root/scripts/install.sh" ]; then
  51. if [ "$DSH_SOURCE_EXPLICIT" = 0 ] || [ "$DSH_SOURCE" = "$_repo_root" ]; then
  52. IN_REPO=1
  53. DSH_SOURCE=$_repo_root
  54. fi
  55. fi
  56. fi
  57. fi
  58. # --- terminal-aware prompting --------------------------------------------------
  59. # stdin is the piped script, so read the controlling terminal for input.
  60. if { true </dev/tty; } 2>/dev/null; then
  61. HAS_TTY=1
  62. # Restore terminal echo on exit or interrupt: ask_secret disables echo between
  63. # its stty toggles, and dash (a common `sh`) does not run an EXIT trap when the
  64. # shell is killed by a signal, so the fatal signals need their own handler. A
  65. # successful run ends in exec, which replaces this process and drops the traps.
  66. trap 'stty echo </dev/tty 2>/dev/null || true' EXIT
  67. trap 'stty echo </dev/tty 2>/dev/null || true; exit 130' INT TERM HUP
  68. else
  69. HAS_TTY=0
  70. fi
  71. # Colour only when writing to a terminal.
  72. if [ -t 1 ]; then
  73. B=$(printf '\033[1m'); DIM=$(printf '\033[2m'); RED=$(printf '\033[31m')
  74. GRN=$(printf '\033[32m'); YEL=$(printf '\033[33m'); RST=$(printf '\033[0m')
  75. else
  76. B=''; DIM=''; RED=''; GRN=''; YEL=''; RST=''
  77. fi
  78. info() { printf '%s==>%s %s\n' "$GRN" "$RST" "$1"; }
  79. step() { printf '\n%s==>%s %s%s%s\n' "$GRN" "$RST" "$B" "$1" "$RST"; }
  80. warn() { printf '%s warn%s %s\n' "$YEL" "$RST" "$1" >&2; }
  81. die() { printf '%serror%s %s\n' "$RED" "$RST" "$1" >&2; exit 1; }
  82. # ask PROMPT [DEFAULT] -> answer on stdout (plain-text line).
  83. ask() {
  84. [ "$HAS_TTY" = 1 ] || die "no terminal available for input; re-run in an interactive shell"
  85. printf '%s%s%s ' "$B" "$1" "$RST" >/dev/tty
  86. IFS= read -r _ans </dev/tty || _ans=''
  87. [ -n "$_ans" ] || _ans=${2:-}
  88. printf '%s' "$_ans"
  89. }
  90. # ask_secret PROMPT -> answer on stdout, with terminal echo suppressed.
  91. ask_secret() {
  92. [ "$HAS_TTY" = 1 ] || die "no terminal available for input; re-run in an interactive shell"
  93. printf '%s%s%s ' "$B" "$1" "$RST" >/dev/tty
  94. stty -echo </dev/tty 2>/dev/null || true
  95. IFS= read -r _sec </dev/tty || _sec=''
  96. stty echo </dev/tty 2>/dev/null || true
  97. printf '\n' >/dev/tty
  98. printf '%s' "$_sec"
  99. }
  100. # confirm PROMPT [Y] -> exit 0 on yes. Default is no unless second arg is "Y".
  101. confirm() {
  102. _def=${2:-N}
  103. if [ "$HAS_TTY" != 1 ]; then
  104. [ "$_def" = Y ] # non-interactive: take the default
  105. return
  106. fi
  107. if [ "$_def" = Y ]; then _hint='[Y/n]'; else _hint='[y/N]'; fi
  108. printf '%s%s%s %s ' "$B" "$1" "$RST" "$_hint" >/dev/tty
  109. IFS= read -r _r </dev/tty || _r=''
  110. [ -n "$_r" ] || _r=$_def
  111. case "$_r" in [yY]|[yY][eE][sS]) return 0 ;; *) return 1 ;; esac
  112. }
  113. printf '%s\n' "${B}DeepSeek Harness — dsh installer${RST}"
  114. printf '%ssource %s @ %s%s\n' "$DIM" "$DSH_SOURCE" "$DSH_REF" "$RST"
  115. # --- 1. dependency check -------------------------------------------------------
  116. step "Checking dependencies"
  117. command -v git >/dev/null 2>&1 || die "git is required but not found. Install git, then re-run."
  118. info "git ... ok"
  119. # Node ^22.19.0 || >=24.0.0 (see the root package.json "engines" field).
  120. node_ok() {
  121. command -v node >/dev/null 2>&1 || return 1
  122. _v=$(node -v 2>/dev/null) || return 1
  123. _v=${_v#v}
  124. _major=${_v%%.*}
  125. _rest=${_v#*.}
  126. _minor=${_rest%%.*}
  127. case "$_major" in ''|*[!0-9]*) return 1 ;; esac
  128. case "$_minor" in ''|*[!0-9]*) _minor=0 ;; esac
  129. [ "$_major" -ge 24 ] && return 0
  130. [ "$_major" -eq 22 ] && [ "$_minor" -ge 19 ] && return 0
  131. return 1
  132. }
  133. if node_ok; then
  134. info "node $(node -v) ... ok"
  135. else
  136. if command -v node >/dev/null 2>&1; then
  137. die "Node $(node -v) is unsupported. dsh needs ^22.19.0 || >=24.0.0 — upgrade Node, then re-run."
  138. fi
  139. die "Node is required but not found. Install Node ^22.19.0 || >=24, then re-run."
  140. fi
  141. # pnpm is the only dependency we offer to install for you.
  142. if command -v pnpm >/dev/null 2>&1; then
  143. info "pnpm $(pnpm --version 2>/dev/null) ... ok"
  144. else
  145. warn "pnpm is not installed."
  146. if confirm "Install pnpm now?" Y; then
  147. if command -v corepack >/dev/null 2>&1 && corepack enable pnpm >/dev/null 2>&1; then
  148. info "enabled pnpm via corepack"
  149. elif command -v npm >/dev/null 2>&1 && npm install -g pnpm >/dev/null 2>&1; then
  150. info "installed pnpm via npm"
  151. else
  152. die "could not install pnpm automatically. Install it (https://pnpm.io/installation), then re-run."
  153. fi
  154. command -v pnpm >/dev/null 2>&1 || die "pnpm still not on PATH after install. Open a new shell, then re-run."
  155. else
  156. die "pnpm is required. Install it (https://pnpm.io/installation), then re-run."
  157. fi
  158. fi
  159. # --- 2. clone (or update) the source ------------------------------------------
  160. if [ "$IN_REPO" = 1 ]; then
  161. step "Using existing checkout at $DSH_SOURCE"
  162. info "running from inside the repo — skipping clone (DSH_REF ignored, working tree left untouched)"
  163. else
  164. step "Fetching source into $DSH_SOURCE"
  165. if [ -d "$DSH_SOURCE/.git" ]; then
  166. info "existing checkout found — updating"
  167. git -C "$DSH_SOURCE" fetch --depth 1 origin "$DSH_REF"
  168. # Reset the checkout to the freshly fetched tip. FETCH_HEAD (not
  169. # origin/<ref>) so this resolves for a tag as well as a branch, and -B makes
  170. # the re-run idempotent whether or not DSH_REF changed since the last install.
  171. git -C "$DSH_SOURCE" checkout -q -B "$DSH_REF" FETCH_HEAD
  172. else
  173. mkdir -p "$(dirname "$DSH_SOURCE")"
  174. git clone --depth 1 --branch "$DSH_REF" "$DSH_REPO" "$DSH_SOURCE"
  175. fi
  176. fi
  177. # --- 3. install dependencies (no build; the launcher runs from source) --------
  178. step "Installing dependencies with pnpm (this can take a while)"
  179. ( cd "$DSH_SOURCE" && pnpm install )
  180. [ -x "$DSH_SOURCE/bin/dsh" ] || die "launcher $DSH_SOURCE/bin/dsh missing after install — is DSH_REF a branch that ships apps/cli?"
  181. # --- 4. put `dsh` on PATH ------------------------------------------------------
  182. step "Linking dsh into $DSH_BIN_DIR"
  183. mkdir -p "$DSH_BIN_DIR"
  184. ln -sf "$DSH_SOURCE/bin/dsh" "$DSH_BIN_DIR/dsh"
  185. info "linked $DSH_BIN_DIR/dsh -> $DSH_SOURCE/bin/dsh"
  186. case ":$PATH:" in
  187. *":$DSH_BIN_DIR:"*) ON_PATH=1 ;;
  188. *) ON_PATH=0 ;;
  189. esac
  190. if [ "$ON_PATH" = 0 ]; then
  191. warn "$DSH_BIN_DIR is not on your PATH."
  192. _line="export PATH=\"$DSH_BIN_DIR:\$PATH\""
  193. _rc=''
  194. _sh=${SHELL:-} # SHELL may be unset; word-removal on an unset var trips set -u under dash.
  195. case "${_sh##*/}" in
  196. zsh) _rc="$HOME/.zshrc" ;;
  197. bash) _rc="$HOME/.bashrc" ;;
  198. esac
  199. if [ -n "$_rc" ] && [ -f "$_rc" ] && grep -qF "$_line" "$_rc" 2>/dev/null; then
  200. info "$_rc already exports $DSH_BIN_DIR — open a new shell to pick it up"
  201. elif [ -n "$_rc" ] && confirm "Add it to $_rc?" Y; then
  202. printf '\n# Added by the dsh installer\n%s\n' "$_line" >>"$_rc"
  203. info "updated $_rc — run 'source $_rc' or open a new shell to pick it up"
  204. else
  205. warn "add this line to your shell profile yourself:"
  206. printf ' %s\n' "$_line"
  207. fi
  208. fi
  209. # --- 5. credentials ------------------------------------------------------------
  210. # Mirror app-boot's resolveDshHome precedence ($DSH_HOME, else ~/.dsh) so creds land where dsh reads them.
  211. if [ -n "${DSH_HOME:-}" ]; then
  212. CONF="$DSH_HOME"
  213. else
  214. CONF="$HOME/.dsh"
  215. fi
  216. ENV_FILE="$CONF/.env"
  217. step "Configuring credentials"
  218. if [ -f "$ENV_FILE" ] && grep -q '^DEEPSEEK_API_KEY=' "$ENV_FILE" 2>/dev/null; then
  219. info "DEEPSEEK_API_KEY already set in $ENV_FILE"
  220. if ! confirm "Replace it?" N; then
  221. SKIP_CREDS=1
  222. fi
  223. fi
  224. if [ "${SKIP_CREDS:-0}" != 1 ]; then
  225. if [ "$HAS_TTY" = 1 ]; then
  226. API_KEY=$(ask_secret "DeepSeek API key (input hidden):")
  227. if [ -z "$API_KEY" ]; then
  228. warn "no key entered — skipping. Set DEEPSEEK_API_KEY in $ENV_FILE before using dsh."
  229. else
  230. BASE_URL=$(ask "DeepSeek base URL (optional, Enter to skip):")
  231. mkdir -p "$CONF"
  232. # The installer owns exactly the two DEEPSEEK_* lines; any other lines the
  233. # user keeps in this .env are preserved. The rewrite happens in a subshell
  234. # so umask 077 (which closes the create-time permission race) does not leak
  235. # into the exec'd dsh, and lands atomically via a same-dir temp + mv.
  236. _tmp="$ENV_FILE.dsh.$$"
  237. (
  238. umask 077
  239. if [ -f "$ENV_FILE" ]; then
  240. grep -v -e '^DEEPSEEK_API_KEY=' -e '^DEEPSEEK_BASE_URL=' "$ENV_FILE" >"$_tmp" || true
  241. else
  242. : >"$_tmp"
  243. fi
  244. printf 'DEEPSEEK_API_KEY=%s\n' "$API_KEY" >>"$_tmp"
  245. if [ -n "$BASE_URL" ]; then printf 'DEEPSEEK_BASE_URL=%s\n' "$BASE_URL" >>"$_tmp"; fi
  246. )
  247. mv "$_tmp" "$ENV_FILE"
  248. chmod 600 "$ENV_FILE" 2>/dev/null || true
  249. info "wrote $ENV_FILE"
  250. fi
  251. else
  252. warn "no terminal for credential input — set DEEPSEEK_API_KEY in $ENV_FILE before using dsh."
  253. fi
  254. fi
  255. # --- 6. launch -----------------------------------------------------------------
  256. step "Done"
  257. if [ "$HAS_TTY" = 1 ]; then
  258. info "launching dsh — run 'dsh' anytime to start again"
  259. exec "$DSH_BIN_DIR/dsh" </dev/tty
  260. else
  261. info "install complete. Start it with:"
  262. printf ' %s\n' "$DSH_BIN_DIR/dsh"
  263. fi