#!/usr/bin/env bash set -euo pipefail REPO="fredalxin/dsh-solo-thinking" PACKAGE="dsh-plugin-solo-thinking" SIDEBAR_PACKAGE="dsh-better-sidebar" SIDEBAR_VERSION="^0.12.1" VERSION="latest" PROFILE="web" DRY_RUN=false usage() { cat <<'EOF' DSH Solo Thinking installer (includes Better Sidebar) Usage: bash install.sh [version] [--profile ] [--dry-run] version GitHub Release version, for example 0.1.18 or v0.1.18. Defaults to the latest published release. --profile DSH profile to install into. Defaults to web. --dry-run Print the resolved command without changing the profile. The installer mounts dsh-better-sidebar first. Solo Thinking still works in the full top tab if Better Sidebar is later removed. EOF } die() { printf '[error] %s\n' "$*" >&2 exit 1 } curl_get() { curl --fail --silent --show-error --location \ --retry 3 --retry-all-errors --retry-delay 2 \ --connect-timeout 15 --max-time 120 "$@" } while [ "$#" -gt 0 ]; do case "$1" in -h|--help) usage exit 0 ;; --profile) [ "$#" -ge 2 ] || die "--profile requires a value" PROFILE="$2" shift 2 ;; --dry-run) DRY_RUN=true shift ;; -*) die "unknown option: $1" ;; *) [ "$VERSION" = "latest" ] || die "only one version may be specified" VERSION="$1" shift ;; esac done command -v node >/dev/null 2>&1 || die "Node.js is required by DSH" command -v curl >/dev/null 2>&1 || die "curl is required to resolve GitHub Releases" if [ "$VERSION" = "latest" ]; then RELEASE_JSON="$(curl_get "https://api.github.com/repos/${REPO}/releases/latest")" \ || die "unable to resolve the latest GitHub Release" TAG="$(printf '%s' "$RELEASE_JSON" | node -e ' let input = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", chunk => { input += chunk }); process.stdin.on("end", () => { const tag = JSON.parse(input).tag_name; if (typeof tag !== "string" || tag.length === 0) process.exit(1); process.stdout.write(tag); }); ')" || die "latest GitHub Release has no tag" else TAG="v${VERSION#v}" fi RELEASE_VERSION="${TAG#v}" ASSET="${PACKAGE}-${RELEASE_VERSION}.tgz" URL="https://github.com/${REPO}/releases/download/${TAG}/${ASSET}" CHECKSUM_URL="${URL}.sha256" if command -v dsh >/dev/null 2>&1; then CLI=(dsh) elif command -v npx >/dev/null 2>&1; then CLI=(npx -y --package @deepseek-ai/dsh dsh) else die "dsh or npx is required; install DSH first" fi printf '[install] %s %s into DSH profile %s\n' "$PACKAGE" "$TAG" "$PROFILE" printf '[install] source: %s\n' "$URL" if [ "$DRY_RUN" = true ]; then printf '[dry-run] prepare the profile for node-pty/protobufjs builds\n' printf '[dry-run] install sidebar:' printf ' %q' "${CLI[@]}" plugin --profile "$PROFILE" add "${SIDEBAR_PACKAGE}@${SIDEBAR_VERSION}" printf '\n' printf '[dry-run] download %s and %s\n' "$URL" "$CHECKSUM_URL" printf '[dry-run] verify SHA-256, then run:' printf ' %q' "${CLI[@]}" plugin --profile "$PROFILE" add "" printf '\n' exit 0 fi DSH_ROOT="${DSH_HOME:-${HOME:?HOME is required}/.dsh}" PROFILE_DIR="$DSH_ROOT/profiles/$PROFILE" WORKSPACE_FILE="$PROFILE_DIR/pnpm-workspace.yaml" [ -f "$WORKSPACE_FILE" ] || die "DSH profile $PROFILE is not initialized; run dsh --profile $PROFILE once, then retry" node -e ' const fs = require("fs"); const file = process.argv[1]; let text = fs.readFileSync(file, "utf8"); const before = text; text = text.replace(/^(\s*)(node-pty|protobufjs):.*$/gm, "$1$2: true"); if (!/^\s*allowBuilds:\s*$/m.test(text)) { text += "\nallowBuilds:\n node-pty: true\n protobufjs: true\n"; } else { for (const name of ["node-pty", "protobufjs"]) { if (!new RegExp("^\\s*" + name + ":\\s*true\\s*$", "m").test(text)) { text = text.replace(/^(\s*allowBuilds:\s*)$/m, "$1\n " + name + ": true"); } } } if (!/^\s*-\s+dsh-better-sidebar\s*$/m.test(text)) { text = /^\s*minimumReleaseAgeExclude:\s*$/m.test(text) ? text.replace(/^(\s*minimumReleaseAgeExclude:\s*)$/m, "$1\n - dsh-better-sidebar") : text + "\nminimumReleaseAgeExclude:\n - dsh-better-sidebar\n"; } if (text !== before) fs.writeFileSync(file, text); ' "$WORKSPACE_FILE" printf '[install] mounting %s %s\n' "$SIDEBAR_PACKAGE" "$SIDEBAR_VERSION" "${CLI[@]}" plugin --profile "$PROFILE" add "${SIDEBAR_PACKAGE}@${SIDEBAR_VERSION}" TEMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/dsh-solo-thinking.XXXXXX")" cleanup() { rm -f "$TEMP_DIR/$ASSET" "$TEMP_DIR/${ASSET}.sha256" rmdir "$TEMP_DIR" 2>/dev/null || true } trap cleanup EXIT curl_get "$URL" -o "$TEMP_DIR/$ASSET" curl_get "$CHECKSUM_URL" -o "$TEMP_DIR/${ASSET}.sha256" ( cd "$TEMP_DIR" if command -v sha256sum >/dev/null 2>&1; then sha256sum -c "${ASSET}.sha256" elif command -v shasum >/dev/null 2>&1; then shasum -a 256 -c "${ASSET}.sha256" else die "sha256sum or shasum is required to verify the Release asset" fi ) "${CLI[@]}" plugin --profile "$PROFILE" add "$TEMP_DIR/$ASSET" CONFIG="$("${CLI[@]}" --profile "$PROFILE" --dump-config)" case "$CONFIG" in *"name: ${PACKAGE}"*) ;; *) die "installation completed but ${PACKAGE} is not mounted in the composed profile" ;; esac case "$CONFIG" in *"name: ${SIDEBAR_PACKAGE}"*) ;; *) die "installation completed but ${SIDEBAR_PACKAGE} is not mounted in the composed profile" ;; esac printf '[install] verified: %s and %s are mounted\n' "$PACKAGE" "$SIDEBAR_PACKAGE" printf '[install] restart DSH, then hard-refresh the browser (Cmd/Ctrl+Shift+R)\n'