Files
lyuksovannyy 78fce8e1ce dsh-remote 0.4.0: edge auth – slaves log in to an auth proxy in front of the master
- lib/edge-auth.mjs: basic / bearer / oauth2 client_credentials (Authentik M2M) /
  custom headers (Cloudflare Access), proxy session cookie jar, retry with fresh
  token, clear edge-refusal vs DSH-refusal errors
- link + enroll go through the edge; master.edgeAuth stored at pair time,
  secrets as DSH credential refs only; config edgeAuth override
- /dsh-remote/edge-auth: show/change/probe; Settings UI section + pair form
- pair.mjs / install-mac.sh: --edge … flags, secrets prompted via /dev/tty
- tests: fake Authentik-like proxy for all modes + WebSocket through the edge
2026-10-11 12:19:36 +00:00

156 lines
7.9 KiB
Bash

#!/usr/bin/env bash
# Install DeepSeek Harness + dsh-remote on a Mac as a slave of a dsh-remote master.
# No sudo, no Homebrew: Node goes to ~/.local/opt/node, DSH to ~/.local, and DSH runs as a
# LaunchAgent (starts at login, restarts on crash) on 127.0.0.1:3081.
#
# curl -fsSL https://git.rizzy.dev/DSH-Plugins/dsh-remote/raw/branch/main/deploy/install-mac.sh \
# | bash -s -- --master http://192.168.0.111:3080 --code ABCD-EFGH-JKLM-NPQR --name my-mac
#
# Options:
# --master URL master DSH (LAN http://… or public https://…) [required]
# --code CODE one-time pairing code (master → Settings → Remote devices) [required unless already paired]
# --name NAME device name shown everywhere (default: the Mac's ComputerName)
# --port PORT local DSH port (default 3081)
# --accept LIST comma list of secrets to accept from the master (default OMNI_API_KEY,FORGE_CREDENTIALS)
# --no-service don't install the LaunchAgent (run `dsh web` yourself)
# --edge MODE … the master sits behind an auth proxy: basic|bearer|oauth2|headers, with
# --edge-user, --edge-token-url, --edge-client-id, --edge-client-secret,
# --edge-header "Name: value" (secrets are prompted for, never in argv)
# Re-running is safe: it updates Node/DSH/dsh-remote and keeps the existing pairing.
set -euo pipefail
MASTER="" CODE="" NAME="" PORT=3081 ACCEPT="OMNI_API_KEY,FORGE_CREDENTIALS" SERVICE=1
EDGE_ARGS=()
NODE_MAJOR=22
REPO_TGZ="https://git.rizzy.dev/DSH-Plugins/dsh-remote/archive/main.tar.gz"
while [ $# -gt 0 ]; do
case "$1" in
--master) MASTER="${2%/}"; shift 2 ;;
--code) CODE="$2"; shift 2 ;;
--name) NAME="$2"; shift 2 ;;
--port) PORT="$2"; shift 2 ;;
--accept) ACCEPT="$2"; shift 2 ;;
--no-service) SERVICE=0; shift ;;
--edge|--edge-user|--edge-token-url|--edge-client-id|--edge-scope|--edge-header) EDGE_ARGS+=("$1" "$2"); shift 2 ;;
--edge-client-secret) EDGE_ARGS+=("$1"); shift ;;
-h|--help) sed -n '2,19p' "$0" 2>/dev/null || true; exit 0 ;;
*) echo "unknown option: $1" >&2; exit 2 ;;
esac
done
say() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
die() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
[ "$(uname -s)" = Darwin ] || die "this installer is for macOS (use deploy/install-local.mjs elsewhere)"
[ -n "$MASTER" ] || die "--master is required (e.g. http://192.168.0.111:3080)"
[ -n "$NAME" ] || NAME="$(scutil --get ComputerName 2>/dev/null || hostname -s)"
PREFIX="$HOME/.local"
DSH_HOME="${DSH_HOME:-$HOME/.dsh}"
PROFILE="$DSH_HOME/profiles/web"
PLUGIN_DIR="$DSH_HOME/plugins/dsh-remote"
export PATH="$PREFIX/bin:$PREFIX/opt/node/bin:$PATH"
mkdir -p "$PREFIX/bin" "$PREFIX/opt" "$DSH_HOME/plugins" "$DSH_HOME/logs"
# 1. Master reachable?
say "Checking master $MASTER"
code=$(curl -s -o /dev/null -w '%{http_code}' -m 10 "$MASTER/" || true)
case "$code" in 2*|3*|401|403) ;; *) die "master $MASTER not reachable (HTTP ${code:-none})" ;; esac
# 2. Node 22 (official tarball, per-user).
have_node() { command -v node >/dev/null && [ "$(node -p 'process.versions.node.split(".")[0]')" -ge "$NODE_MAJOR" ] 2>/dev/null; }
if have_node; then
say "Node $(node -v) already available"
else
arch=$(uname -m); [ "$arch" = arm64 ] || arch=x64
base="https://nodejs.org/dist/latest-v${NODE_MAJOR}.x"
file=$(curl -fsSL "$base/SHASUMS256.txt" | awk -v a="darwin-$arch.tar.gz" '$2 ~ a"$" {print $2; exit}')
sum=$(curl -fsSL "$base/SHASUMS256.txt" | awk -v f="$file" '$2==f {print $1}')
[ -n "$file" ] || die "could not find a Node $NODE_MAJOR build for darwin-$arch"
say "Installing $file into $PREFIX/opt/node"
tmp=$(mktemp -d)
curl -fsSL "$base/$file" -o "$tmp/node.tgz"
echo "$sum $tmp/node.tgz" | shasum -a 256 -c - >/dev/null || die "Node checksum mismatch"
rm -rf "$PREFIX/opt/node.new" && mkdir -p "$PREFIX/opt/node.new"
tar -xzf "$tmp/node.tgz" -C "$PREFIX/opt/node.new" --strip-components 1
rm -rf "$PREFIX/opt/node" && mv "$PREFIX/opt/node.new" "$PREFIX/opt/node"
rm -rf "$tmp"
for b in node npm npx corepack; do ln -sf "$PREFIX/opt/node/bin/$b" "$PREFIX/bin/$b"; done
fi
npm config set prefix "$PREFIX" >/dev/null
# 3. DSH + pnpm (DSH's plugin manager installs registry plugins with pnpm).
say "Installing @deepseek-ai/dsh and pnpm"
npm install -g --no-fund --no-audit @deepseek-ai/dsh pnpm@10 >/dev/null
command -v dsh >/dev/null || die "dsh not on PATH after install"
say "$(dsh --version 2>/dev/null | head -1 | sed 's/^/dsh /')"
# 4. Initialize the web profile (no server started).
[ -f "$PROFILE/package.json" ] || dsh web --dump-config >/dev/null 2>&1 || true
[ -f "$PROFILE/package.json" ] || die "could not initialize $PROFILE"
# 5. dsh-remote plugin.
say "Installing dsh-remote into $PLUGIN_DIR"
tmp=$(mktemp -d)
curl -fsSL "$REPO_TGZ" -o "$tmp/r.tgz"
mkdir -p "$tmp/x" && tar -xzf "$tmp/r.tgz" -C "$tmp/x"
rm -rf "$PLUGIN_DIR.new" && mv "$tmp/x/dsh-remote" "$PLUGIN_DIR.new"
rm -rf "$PLUGIN_DIR" && mv "$PLUGIN_DIR.new" "$PLUGIN_DIR"
rm -rf "$tmp"
RESTART_FLAG=""; [ "$SERVICE" = 1 ] && RESTART_FLAG="--restart-on-sync"
node "$PLUGIN_DIR/deploy/install-local.mjs" --role slave --name "$NAME" --accept "$ACCEPT" $RESTART_FLAG >/dev/null
# 6. Pair (exchange the one-time code for a device key) unless already paired.
STATE="$PROFILE/dsh-remote/state.json"
if [ -f "$STATE" ] && grep -q '"deviceId"' "$STATE"; then
say "Already paired (keeping $STATE)"
else
[ -n "$CODE" ] || die "--code is required for the first pairing (master → Settings → Remote devices → Create pairing code)"
say "Pairing with $MASTER as \"$NAME\""
node "$PLUGIN_DIR/deploy/pair.mjs" --master "$MASTER" --code "$CODE" --name "$NAME" --state "$STATE" ${EDGE_ARGS[@]+"${EDGE_ARGS[@]}"}
fi
# 7. LaunchAgent: dsh web on 127.0.0.1:$PORT, at login, restarted on exit.
if [ "$SERVICE" = 1 ]; then
LABEL="dev.rizzy.dsh-web"
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
mkdir -p "$HOME/Library/LaunchAgents"
cat > "$PLIST" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>$LABEL</string>
<key>ProgramArguments</key><array>
<string>$PREFIX/bin/dsh</string><string>web</string>
<string>--host</string><string>127.0.0.1</string>
<string>--port</string><string>$PORT</string><string>--no-open</string>
</array>
<key>WorkingDirectory</key><string>$HOME</string>
<key>EnvironmentVariables</key><dict>
<key>PATH</key><string>$PREFIX/bin:$PREFIX/opt/node/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
<key>DSH_HOME</key><string>$DSH_HOME</string>
</dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>$DSH_HOME/logs/dsh-web.out.log</string>
<key>StandardErrorPath</key><string>$DSH_HOME/logs/dsh-web.err.log</string>
</dict></plist>
PLIST
launchctl bootout "gui/$(id -u)/$LABEL" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" "$PLIST"
say "Starting DSH (LaunchAgent $LABEL)"
for _ in $(seq 1 60); do curl -s -m 2 -o /dev/null "http://127.0.0.1:$PORT/" && break; sleep 1; done
curl -s -m 2 -o /dev/null "http://127.0.0.1:$PORT/" || die "DSH did not come up; see $DSH_HOME/logs/dsh-web.err.log"
url=$(grep -Eo "http://127\.0\.0\.1:$PORT/\?token=[A-Za-z0-9_-]+" "$DSH_HOME/logs/dsh-web.out.log" 2>/dev/null | tail -1 || true)
fi
cat <<DONE
$(say "Done: \"$NAME\" is a dsh-remote slave of $MASTER")
Open DSH on this Mac: ${url:-http://127.0.0.1:$PORT/ (run: dsh web, it prints a login link)}
Sidebar → Devices shows this Mac, the master and every other device.
First link-up syncs plugins + config from the master; DSH restarts itself once
if new plugins arrived (or run: launchctl kickstart -k gui/\$(id -u)/dev.rizzy.dsh-web).
On the master: Settings → Remote devices → "$NAME" → Sync policy to allow secrets.
Logs: $DSH_HOME/logs/dsh-web.{out,err}.log
DONE