Repository navigation
Expand file tree
/
Copy pathinstall-server.sh
More file actions
executable file
·234 lines (217 loc) · 10.8 KB
/
Copy pathinstall-server.sh
File metadata and controls
executable file
·234 lines (217 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
#!/usr/bin/env bash
# Subshell control plane installer.
#
# curl -fsSL https://subshell.sh/install-server.sh | bash
#
# Downloads the `subshell-server` binary for this platform from the newest
# `cli-server-vX.Y.Z` GitHub Release, VERIFIES its digest before the file is ever
# made executable, installs it to ~/.local/bin, and hands over to
# `subshell-server init` — which asks the setup questions and prints where to
# go next. This script deliberately says nothing about what comes after `init`:
# the CLI owns the questions and the handoff, the script owns the download
# (spec 2026-09-15 §3.1).
#
# Knobs, all optional and all read from the environment because a piped curl
# has no argv:
# SUBSHELL_SERVER_VERSION install this exact X.Y.Z instead of the newest
# SUBSHELL_SERVER_RELEASE_API releases listing URL (default: GitHub's)
# SUBSHELL_SERVER_RELEASE_BASE directory holding the release assets
# SUBSHELL_SERVER_PORT -> init --port
# SUBSHELL_SERVER_HOST -> init --host
# SUBSHELL_SERVER_BASE_URL -> init --base-url
# SUBSHELL_SERVER_TRUSTED_ORIGINS -> init --trusted-origins
# SUBSHELL_NO_SERVICE=1 -> init --no-service
set -eu
REPO="subshell-ai/subshell"
BIN_DIR="$HOME/.local/bin"
DEST="$BIN_DIR/subshell-server"
fail() {
echo "subshell-server: $1" >&2
shift
for line in "$@"; do
echo " $line" >&2
done
exit 1
}
# --- 1. which binary does this machine need? --------------------------------
OS="$(uname -s)"
ARCH="$(uname -m)"
case "$OS/$ARCH" in
Linux/x86_64) TARGET="linux-x64" ;;
Linux/aarch64|Linux/arm64) TARGET="linux-arm64" ;;
Darwin/arm64) TARGET="darwin-arm64" ;;
Darwin/x86_64) TARGET="darwin-x64" ;;
*)
fail "unsupported platform: $OS/$ARCH" \
"Published targets are linux-x64, linux-arm64, darwin-arm64 and darwin-x64."
;;
esac
# --- 2. which release? ------------------------------------------------------
# The releases LISTING, not /releases/latest: this repo ships four components
# from one repo, so the newest release overall is as likely to be a cli-node-v*
# or desktop-* cut as a server one. Tags are filtered to `cli-server-v` and ordered
# with sort -V, which knows 1.10.0 > 1.9.0 and plain `sort` does not.
API="${SUBSHELL_SERVER_RELEASE_API:-https://api.github.com/repos/$REPO/releases}"
# Refuse a plaintext hop on the PUBLIC path — including a redirect into one,
# which `--location` would otherwise follow silently. Not applied when an
# operator has pointed this at their own API or asset host: an override is a
# deliberate choice about their own network, the same posture the plugin
# registry documents for an http mirror, and a test fake is exactly that.
CURL_PROTO="--proto =https --tlsv1.2"
case "${SUBSHELL_SERVER_RELEASE_API:-}${SUBSHELL_SERVER_RELEASE_BASE:-}" in
"") ;;
*) CURL_PROTO="" ;;
esac
VERSION="${SUBSHELL_SERVER_VERSION:-}"
if [ -z "$VERSION" ]; then
echo "==> finding the newest server release"
# No jq: a fresh headless box has curl and coreutils and frequently nothing
# else. grep -o over the tag_name fields is enough for a flat list of tags,
# and a tag that does not match the mint shape simply does not appear.
if ! BODY="$(curl $CURL_PROTO --silent --show-error --location --fail "$API")"; then
fail "could not reach the release index at $API" \
"Check outbound network, or pass SUBSHELL_SERVER_VERSION=X.Y.Z to skip this lookup."
fi
VERSION="$(
printf '%s' "$BODY" |
grep -o '"tag_name"[[:space:]]*:[[:space:]]*"cli-server-v[0-9][0-9.]*"' |
sed -e 's/.*"cli-server-v//' -e 's/"$//' |
sort -V |
tail -n 1
)"
if [ -z "$VERSION" ]; then
fail "no cli-server-vX.Y.Z release is published yet." \
"The index at $API listed no matching tag."
fi
fi
TAG="cli-server-v$VERSION"
echo "==> installing $TAG ($TARGET)"
ASSET="subshell-server-cli-$TARGET"
BASE="${SUBSHELL_SERVER_RELEASE_BASE:-https://github.com/$REPO/releases/download/$TAG}"
# --- 3. download ------------------------------------------------------------
mkdir -p "$BIN_DIR"
TMP="$DEST.part"
rm -f "$TMP" "$TMP.sha256"
# The HTTP code is inspected rather than curl's exit status alone: "404, this
# release has no asset for your platform" and "the network is down" need
# different advice, and an exit-status-only guard says the same thing for both.
echo "==> downloading $ASSET"
if ! HTTP="$(curl $CURL_PROTO --silent --show-error --location "$BASE/$ASSET" \
--output "$TMP" --write-out '%{http_code}')"; then
rm -f "$TMP"
fail "could not download $BASE/$ASSET; nothing was installed." \
"Check outbound network access to the release host."
fi
case "$HTTP" in
200) ;;
404)
rm -f "$TMP"
fail "$TAG publishes no $TARGET binary (asset $ASSET is missing)." \
"Current published targets are linux-x64, linux-arm64, darwin-arm64 and darwin-x64;" \
"older releases may carry fewer." \
"Pick another release with SUBSHELL_SERVER_VERSION=X.Y.Z, or build from a checkout."
;;
*)
rm -f "$TMP"
fail "the release host answered HTTP $HTTP for $ASSET; nothing was installed."
;;
esac
# --- 4. verify BEFORE anything is made executable ---------------------------
# The sidecar is a BARE 64-hex digest, so the "<hash> <file>" line the -c
# checkers want is paired here rather than downloaded. Verifying before the
# first chmod +x is what makes `curl | bash` sound: nothing this script fetched
# can run until its bytes match the digest the release published.
#
# Be precise about what that buys, because it reads stronger than it is: the
# digest comes from the SAME host as the binary, so it bounds a corrupt
# download and a bad mirror, and NOT a compromised release host — which would
# serve a matching pair. Transport integrity is the `--proto '=https'` below;
# provenance beyond that would need a signature this project does not yet
# publish.
if ! EXPECTED="$(curl $CURL_PROTO --silent --show-error --location --fail "$BASE/$ASSET.sha256" | tr -d '[:space:]')"; then
rm -f "$TMP"
fail "could not fetch the checksum for $ASSET; nothing was installed."
fi
if [ -z "$EXPECTED" ]; then
rm -f "$TMP"
fail "the checksum for $ASSET was empty; nothing was installed."
fi
printf '%s %s\n' "$EXPECTED" "$TMP" > "$TMP.sha256"
if command -v sha256sum >/dev/null 2>&1; then
VERIFY="sha256sum -c"
elif command -v shasum >/dev/null 2>&1; then
VERIFY="shasum -a 256 -c"
else
rm -f "$TMP" "$TMP.sha256"
fail "need sha256sum or shasum to verify the download; nothing was installed."
fi
echo "==> verifying checksum"
if ! $VERIFY "$TMP.sha256" >/dev/null; then
rm -f "$TMP" "$TMP.sha256"
fail "checksum mismatch for $ASSET: corrupt download or a tampered mirror." \
"Nothing was installed and nothing was executed."
fi
rm -f "$TMP.sha256"
# --- 5. install -------------------------------------------------------------
# mv onto $DEST, THEN chmod: an interrupted download can never leave a partial
# executable where the service definition expects a server.
mv -f "$TMP" "$DEST"
chmod +x "$DEST"
echo "==> installed $DEST"
# The old PATH note lived here. It is GONE (spec 2026-09-26): the file this
# script execs into is a program now, so `init` asks whether to add
# ~/.local/bin to the shell profile and can actually do the writing — a note
# is a human doing the last step alone, and half of piped installs never
# finished the sentence.
# tmux is what every local subshell's pane runs under. This is a warning and
# not a refusal because `init` has its own preflight and refusing here would
# take its handling away. Wording kept honest (spec 2026-09-26): the setup
# step CAN install tmux — on a terminal it asks, with --yes it does — but a
# non-interactive run without --yes declines on the operator's behalf.
if ! command -v tmux >/dev/null 2>&1; then
echo "==> note: tmux is not installed. Subshell needs it to run panes on this host;"
echo " the setup step can install it."
fi
# --- 6. hand over to the CLI ------------------------------------------------
# Every one of these is an `if` and not a `test && append`. Not because `&&`
# would abort — measured on bash, sh and dash, `set -e` is ignored for a
# non-last command of an AND-OR list and a short-circuited list does not exit
# the shell. It is that `test && arr+=(...)` makes the LIST's status the
# statement's status, so the last knob being unset leaves the script's exit
# code at 1 if nothing follows it, and a reader has to know that rule to see
# the difference. An `if` says what it does and owes nothing to `set -e`.
INIT_ARGS=()
if [ -n "${SUBSHELL_SERVER_PORT:-}" ]; then INIT_ARGS+=(--port "$SUBSHELL_SERVER_PORT"); fi
if [ -n "${SUBSHELL_SERVER_HOST:-}" ]; then INIT_ARGS+=(--host "$SUBSHELL_SERVER_HOST"); fi
if [ -n "${SUBSHELL_SERVER_BASE_URL:-}" ]; then INIT_ARGS+=(--base-url "$SUBSHELL_SERVER_BASE_URL"); fi
if [ -n "${SUBSHELL_SERVER_TRUSTED_ORIGINS:-}" ]; then
INIT_ARGS+=(--trusted-origins "$SUBSHELL_SERVER_TRUSTED_ORIGINS")
fi
if [ "${SUBSHELL_NO_SERVICE:-}" = "1" ]; then INIT_ARGS+=(--no-service); fi
# A piped curl leaves stdin reading the drained SCRIPT. The old ending pointed
# stdin back at the controlling terminal there — and that rewire is GONE
# (spec 2026-09-26), because its guard tested STDOUT while the act moved
# STDIN, and the terminal open itself can BLOCK on some macOS terminals
# (Terminal's "Restored session" reproduces it): the install hung before
# `init` rendered anything. `init` now decides its own prompt input — it
# attaches the controlling terminal with a never-waiting open, runs the full
# interview wherever a terminal exists, and when one genuinely does not it
# prints every default it takes. The script hands stdin to `init` exactly as
# it received it, and nothing else rewires anything.
# A piped-curl install is a distribution and the recipient never sees a LICENSE
# file — what lands is one bare binary. Naming the terms once, and pointing at
# the subcommand that prints them in full, is the only moment this path has to
# do that. The control plane is AGPL-3.0-only, NOT Apache-2.0 like the rest of
# the repo: apps/server/** is the copyleft half of the split (root AGENTS.md,
# "The licence boundary IS this directory line").
echo " Copyright 2026 Disaresta, LLC. AGPL-3.0-only, with the API Type Surface"
echo " exception. Run \"$DEST\" license for the full notice."
# Said ONCE, here, because this script is the only moment a piped-curl install
# has to name the way back: nothing else ever tells that operator a newer
# server exists, and re-running this installer is not it (it would overwrite
# the binary without backing the database up first).
echo " Later: \"$DEST\" update --check to see what is available,"
echo " \"$DEST\" update to install it (the database is backed up first)."
# The last word belongs to `init`: it asks about the background service and
# prints the "open <url>/setup" handoff. Nothing is echoed after it.
exec "$DEST" init ${INIT_ARGS[@]+"${INIT_ARGS[@]}"}