Repository navigation
Expand file tree
/
Copy pathsetup.sh
More file actions
executable file
·471 lines (444 loc) · 23.8 KB
/
Copy pathsetup.sh
File metadata and controls
executable file
·471 lines (444 loc) · 23.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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
#!/usr/bin/env bash
#
# setup.sh — one-command onboarding for mystical-assistant.
# Idempotent: re-run any time; it only asks for what's still missing.
# ./setup.sh full wizard
# ./setup.sh --check-only prereq doctor, then exit (used by `mystical doctor`)
set -euo pipefail
REPO="$(cd "$(dirname "$(realpath "$0")")" && pwd)"
ENV_FILE="$REPO/.env"
ONBOARD=(python3 "$REPO/bridge/onboard.py")
# Colour only on a terminal: `mystical doctor` pipes this, and escape codes in a
# log or a CI capture are noise.
if [ -t 1 ]; then
c_g=$'\033[32m'; c_r=$'\033[31m'; c_y=$'\033[33m'; c_c=$'\033[36m'
c_d=$'\033[2m'; c_b=$'\033[1m'; c_0=$'\033[0m'
else
c_g=; c_r=; c_y=; c_c=; c_d=; c_b=; c_0=
fi
ok() { printf ' %s✔%s %s\n' "$c_g" "$c_0" "$*"; }
bad() { printf ' %s✘%s %s\n' "$c_r" "$c_0" "$*"; }
warn() { printf ' %s▲%s %s\n' "$c_y" "$c_0" "$*"; }
# Only the questions we actually ask get a heading, so a re-run that has nothing
# to ask stays quiet instead of printing a wall of skipped steps. $1 is the icon,
# so each step is recognisable at a glance while scrolling back.
step() { printf '\n%s%s %s%s%s\n' "$c_b" "$1" "$c_c" "${*:2}" "$c_0"; }
have() { command -v "$1" >/dev/null 2>&1; }
# opencode's own installer drops it in ~/.opencode/bin, which is off PATH until
# a new shell — probe both, the way freeagent.py resolves the binary.
have_opencode() { have opencode || [ -x "$HOME/.opencode/bin/opencode" ]; }
# Kimi Code runs through npx at the version bridge/acp_agents.py pins (KIMI_PKG),
# so the pin is read from there: one place to bump it.
KIMI_PKG="$(sed -n 's/^KIMI_PKG = "\(.*\)"$/\1/p' "$REPO/bridge/acp_agents.py")"
node_ok_for_kimi() { # Kimi Code's engines field: node >= 22.19
have npx && node -e 'const [a,b]=process.versions.node.split(".").map(Number);process.exit(a>22||(a===22&&b>=19)?0:1)' 2>/dev/null
}
PATH_ORIG="$PATH" # remember what the user's shell really has
export PATH="$HOME/.local/bin:$PATH" # so a tunnel client/mystical we install is visible now
doctor() {
local hard=0
printf '%s🔧 Required%s\n' "$c_b" "$c_0"
if have claude; then ok "claude"
else bad "claude — install it and log in: https://claude.com/claude-code"; hard=1; fi
if ! have python3; then bad "python3 — not installed (need 3.10+)"; hard=1
elif python3 -c 'import sys;sys.exit(sys.version_info<(3,10))'; then
ok "python3 $(python3 -c 'import platform;print(platform.python_version())') — no pip packages needed"
else bad "python3 $(python3 -c 'import platform;print(platform.python_version())') — need 3.10+"; hard=1; fi
printf '\n%s🧩 Optional%s %s— each one you skip only turns its own feature off%s\n' \
"$c_b" "$c_0" "$c_d" "$c_0"
if have npm; then ok "npm — builds the dashboard and Mini App"
else warn "npm — missing: the bot runs, but the dashboard and Mini App can't be built"; fi
if have cloudflared; then ok "tunnel client — lets your phone reach the Mini App"
else warn "tunnel client — missing: no Mini App on your phone (setup offers to install it)"; fi
if have_opencode; then ok "opencode — free-agent fallback when the Claude accounts run out"
else warn "opencode — missing: sessions stop at the usage limit (setup offers to install it)"; fi
if node_ok_for_kimi; then ok "node $(node -v) — runs Kimi Code (and Codex) sessions through npx"
else warn "node 22.19+ — missing: no Kimi Code sessions (setup offers Node LTS when npm is missing)"; fi
if have graphify; then ok "graphify — projects map themselves after the first turn"
else warn "graphify — missing: no project maps. Install: pipx install graphifyy"; fi
if [ "$(uname -s)" = Darwin ]; then
if have swift; then ok "swift — builds the Mac app (Mystical.app)"
else warn "swift — missing: no Mac app. Install: xcode-select --install"; fi
fi
if grep -qi microsoft /proc/sys/kernel/osrelease 2>/dev/null; then
if command -v dotnet.exe >/dev/null 2>&1 || [ -x "/mnt/c/Program Files/dotnet/dotnet.exe" ]; then ok "dotnet — builds the Windows app (Mystical.exe)"
else warn "dotnet — missing: no Windows app (the PowerShell tray still works). Install: winget install Microsoft.DotNet.SDK.10"; fi
fi
# A Linux desktop (WSL's tray is PowerShell, always there): the tray icon
# needs the distro's GTK bindings, and linux/tray.py --check names them.
if [ "$(uname -s)" = Linux ] && [ -n "${XDG_CURRENT_DESKTOP:-}" ] \
&& ! grep -qi microsoft /proc/sys/kernel/osrelease 2>/dev/null; then
if msg="$(/usr/bin/python3 "$REPO/linux/tray.py" --check 2>&1)"; then ok "tray bindings — the tray icon can run"
else warn "tray bindings — missing: no tray icon. ${msg#mystical tray: }"; fi
fi
return $hard
}
# Fetch the tunnel client for the user rather than sending them to a download page.
install_tunnel_client() {
mkdir -p "$HOME/.local/bin"
if [ "$(uname -s)" = "Darwin" ]; then
have brew || { warn "Homebrew not found — install the tunnel client manually."; return 1; }
brew install cloudflared >/dev/null || return 1
else
local a; case "$(uname -m)" in
x86_64) a=amd64;; aarch64|arm64) a=arm64;; armv7l) a=arm;;
*) warn "unsupported arch $(uname -m) — install the tunnel client manually."; return 1;;
esac
curl -fsSL -o "$HOME/.local/bin/cloudflared" \
"https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-$a" || return 1
chmod +x "$HOME/.local/bin/cloudflared"
fi
have cloudflared
}
# The bridge is nothing without the CLI it shells out to, so a missing `claude`
# is the one prereq worth fetching before anything else can run.
install_claude() {
curl -fsSL https://claude.ai/install.sh | bash >/dev/null 2>&1 || return 1
have claude
}
# npm is only ever used to BUILD the web bundles, but without them the dashboard
# — the thing most people install this for — serves nothing. Same rules as the
# tunnel client: user-local, no sudo, no shell rc.
install_node() {
if [ "$(uname -s)" = "Darwin" ]; then
have brew || { warn "Homebrew not found — install Node manually."; return 1; }
brew install node >/dev/null || return 1
else
local a; case "$(uname -m)" in
x86_64) a=x64;; aarch64|arm64) a=arm64;; armv7l) a=armv7l;;
*) warn "unsupported arch $(uname -m) — install Node manually."; return 1;;
esac
# nodejs.org has no `latest-lts` path, so the index is the only way to learn
# which version that is today.
local v; v="$(curl -fsSL https://nodejs.org/dist/index.json | python3 -c \
'import json,sys;print(next(r["version"] for r in json.load(sys.stdin) if r["lts"]))')" || return 1
[ -n "$v" ] || return 1
mkdir -p "$HOME/.local/share" "$HOME/.local/bin"
rm -rf "$HOME/.local/share/node" "$HOME/.local/share/node-$v-linux-$a"
curl -fsSL "https://nodejs.org/dist/$v/node-$v-linux-$a.tar.gz" \
| tar -xz -C "$HOME/.local/share" || return 1
mv "$HOME/.local/share/node-$v-linux-$a" "$HOME/.local/share/node" || return 1
for b in node npm npx; do ln -sf "$HOME/.local/share/node/bin/$b" "$HOME/.local/bin/$b"; done
fi
have npm
}
# Same for the free-agent rung. opencode's installer already covers the
# os/arch/musl/CPU-baseline matrix, so call it instead of guessing a release
# asset — but keep it out of the user's shell rc and link it into ~/.local/bin,
# which setup already puts on PATH and freeagent.py already looks in.
install_opencode() {
curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path >/dev/null || return 1
[ -x "$HOME/.opencode/bin/opencode" ] || return 1
mkdir -p "$HOME/.local/bin"
ln -sf "$HOME/.opencode/bin/opencode" "$HOME/.local/bin/opencode"
}
get_env() { [ -f "$ENV_FILE" ] && sed -n "s/^$1=\"\{0,1\}\([^\"]*\)\"\{0,1\}\$/\1/p" "$ENV_FILE" | head -n1 || true; }
if [ "${1:-}" = "--check-only" ]; then doctor; exit $?; fi
printf '\n%s✦ mystical%s//%sassistant%s %ssetup%s\n' "$c_c" "$c_d" "$c_0$c_c" "$c_0" "$c_d" "$c_0"
printf '%sAnswers are saved to .env, so a re-run only asks for what'"'"'s still missing.%s\n\n' "$c_d" "$c_0"
# doctor only ever reports — `mystical doctor` pipes it and must stay side-effect
# free — so the offers to fix what it found live out here, in the wizard.
if ! doctor; then
if ! have claude; then
step "🧠" "Claude Code"
echo "The bridge shells out to the 'claude' CLI for every turn, so nothing"
echo "runs without it. The installer puts it in ~/.local/bin; no sudo."
printf "Install Claude Code now? [Y/n]: "
read -r ans || ans=n # EOF (piped/CI) = don't download
case "${ans:-y}" in
[Nn]*) : ;;
*) echo "Installing Claude Code…"
if install_claude; then
ok "claude installed → $(command -v claude)"
warn "log in once before you start: run 'claude' in a terminal"
else warn "install failed — see https://claude.com/claude-code"; fi ;;
esac
fi
if ! have python3 || ! python3 -c 'import sys;sys.exit(sys.version_info<(3,10))' 2>/dev/null; then
# Can't self-install the interpreter this script is already using for
# onboard.py — the most it can do is name the right command.
case "$(uname -s)" in
Darwin) warn "install Python 3.10+: brew install python" ;;
*) have apt-get && warn "install Python 3.10+: sudo apt-get install python3" \
|| warn "install Python 3.10+ with your package manager" ;;
esac
fi
echo
if ! doctor; then echo; bad "Fix the required items above, then re-run ./setup.sh."; exit 1; fi
fi
# Optional, but each one silently costs a feature, so offer rather than warn. The
# `have` guards mean a fully-equipped machine sees none of these questions.
if ! have npm; then
step "📦" "Node"
echo "npm builds the dashboard and Mini App bundles. Without it the bridge"
echo "still runs, but the dashboard has nothing to serve."
printf "Install Node LTS into ~/.local (no sudo)? [Y/n]: "
read -r ans || ans=n
case "${ans:-y}" in
[Nn]*) warn "skipped — the dashboard and Mini App won't be built" ;;
*) echo "Downloading Node…"
if install_node; then ok "node $(node -v 2>/dev/null) → ~/.local/bin/node"
else warn "install failed — install Node yourself, then re-run ./setup.sh"; fi ;;
esac
fi
if ! have graphify && have pipx; then
step "🗺" "Project maps"
echo "graphify maps each project's code after its first turn, so /map answers"
echo "without spending a turn on it."
printf "Install it (pipx install graphifyy)? [Y/n]: "
read -r ans || ans=n
case "${ans:-y}" in
[Nn]*) warn "skipped — no project maps" ;;
*) if pipx install graphifyy >/dev/null 2>&1; then ok "graphify installed"
else warn "install failed — no project maps"; fi ;;
esac
fi
touch "$ENV_FILE"; chmod 600 "$ENV_FILE"
# -- Telegram, or dashboard only ---------------------------------------------
# Key presence, not value: TELEGRAM_BOT_TOKEN="" is the recorded answer "no
# Telegram", so a re-run doesn't march a desktop-only install back to BotFather.
# Only an ENTIRELY absent key asks — a token that's on file but didn't validate
# (network down, typo) still goes straight to the paste loop, as it always did.
token="$(get_env TELEGRAM_BOT_TOKEN)"
botname="$([ -n "$token" ] && "${ONBOARD[@]}" get-me "$token" || true)"
if [ -n "$botname" ] || [ -n "$token" ]; then
tg=1; [ -n "$botname" ] || step "🤖" "Telegram bot"
elif grep -q '^TELEGRAM_BOT_TOKEN=' "$ENV_FILE"; then tg=0
else
step "🤖" "Telegram"
echo "Telegram adds the bot and the phone Mini App: start a session at your desk,"
echo "answer it from your phone. It needs a bot token — about a minute of setup."
echo "Skip it and you get the desktop dashboard on its own, which is most of it."
printf "Set up Telegram? [Y/n]: "
read -r ans || ans=n # EOF (piped/CI) = nobody there to paste one
case "${ans:-y}" in [Nn]*) tg=0;; *) tg=1;; esac
if [ "$tg" = 0 ]; then
"${ONBOARD[@]}" set-env "$ENV_FILE" TELEGRAM_BOT_TOKEN ""
"${ONBOARD[@]}" set-env "$ENV_FILE" MINIAPP_ENABLE 0 # the panel is signed by the token
fi
fi
while [ "$tg" = 1 ] && [ -z "$botname" ]; do
echo
echo "Create a Telegram bot to get a token:"
echo " 1. Open @BotFather: https://t.me/BotFather"
echo " 2. Send /newbot and follow the prompts (name + username)."
echo " 3. Copy the token (looks like 123456:ABC-...)."
printf "Paste your bot token: "; read -r token
[ -n "$token" ] || { bad "No token entered."; exit 1; }
botname="$("${ONBOARD[@]}" get-me "$token" || true)"
[ -n "$botname" ] || bad "Telegram rejected that token — check it and try again."
done
if [ "$tg" = 1 ]; then
if [ "$token" != "$(get_env TELEGRAM_BOT_TOKEN)" ]; then
"${ONBOARD[@]}" set-env "$ENV_FILE" TELEGRAM_BOT_TOKEN "$token"
# Only on a new/changed token, so a re-run never clobbers a picture you chose.
"${ONBOARD[@]}" set-avatar "$token" >/dev/null 2>&1 || true
fi
ok "bot @$botname — https://t.me/$botname"
else
ok "dashboard only — add a bot token later in the dashboard's SYSTEM tab"
fi
# -- BASE_PATH ---------------------------------------------------------------
if [ -z "$(get_env BASE_PATH)" ]; then
step "📁" "Projects folder"
echo "Every project you can open from the dashboard or your phone lives here."
base=""
# Browse it rather than recall it: the picker labels which folders hold repos.
# No terminal (piped/CI) or a cancel drops through to typing, where -e buys
# readline tab-completion of paths.
[ -t 0 ] && base="$("${ONBOARD[@]}" pick-dir "$HOME" || true)"
if [ -z "$base" ]; then
read -e -r -p "Root folder for your projects [$HOME/projects]: " base || true
base="${base:-$HOME/projects}"
fi
"${ONBOARD[@]}" set-env "$ENV_FILE" BASE_PATH "$base"
ok "BASE_PATH → $base"
fi
# -- dashboard token (stable + secure) ---------------------------------------
# Key presence, not value: DASH_TOKEN="" is a deliberate choice (localhost-only
# dashboard, no token gate), and a re-run must not mint one and break the URL.
if ! grep -q '^DASH_TOKEN=' "$ENV_FILE"; then
dtok="$(openssl rand -hex 24 2>/dev/null || python3 -c 'import secrets;print(secrets.token_urlsafe(24))')"
"${ONBOARD[@]}" set-env "$ENV_FILE" DASH_TOKEN "$dtok"
fi
# -- chat id (auto-capture) --------------------------------------------------
if [ "$tg" = 1 ] && [ -z "$(get_env ALLOWED_CHAT_IDS)" ]; then
step "💬" "Your chat"
echo "Only chat ids listed here can drive Claude on this machine."
echo "Open https://t.me/$botname and send it ANY message (or tap Start)"
echo "so it can learn yours…"
cid="$("${ONBOARD[@]}" capture-chat-id "$token" || true)"
if [ -n "$cid" ]; then
"${ONBOARD[@]}" set-env "$ENV_FILE" ALLOWED_CHAT_IDS "$cid"
ok "captured chat id $cid — you are the only allowed user"
else
warn "No message received in time. Re-run ./setup.sh, or start 'mystical',"
warn "message the bot, and add the printed id to ALLOWED_CHAT_IDS in .env."
fi
fi
# -- permission posture (an explicit choice, never a silent default) ----------
# Sessions started from the dashboard or Mini App carry this mode, so it decides
# whether Claude runs commands on your machine without asking. Shipping a default
# would mean strangers cloning this get full autonomy they never opted into.
if [ -z "$(get_env NEW_SESSION_PERMISSION_MODE)" ]; then
step "🔐" "Permissions"
echo "How should Claude behave in sessions you start from the dashboard or phone?"
echo " 1) Ask before running commands — edits files freely, asks for the rest"
echo " 2) Full autonomy — runs commands without asking (fastest,"
echo " and a prompt injection runs as you)"
printf "Choose [1/2, default 1]: "
read -r ans || ans=1
case "${ans:-1}" in
2) mode="bypassPermissions"; extra="--dangerously-skip-permissions" ;;
*) mode="acceptEdits"; extra="--permission-mode acceptEdits" ;;
esac
"${ONBOARD[@]}" set-env "$ENV_FILE" NEW_SESSION_PERMISSION_MODE "$mode"
"${ONBOARD[@]}" set-env "$ENV_FILE" EXTRA_CLAUDE_ARGS "$extra"
ok "permission posture → $mode (change it in .env any time)"
fi
# -- Mini App toggle ---------------------------------------------------------
if [ -z "$(get_env MINIAPP_ENABLE)" ]; then
step "📱" "Phone Mini App"
echo "A control panel that opens inside Telegram: start sessions, watch them"
echo "stream, browse files. Your phone reaches this machine over a tunnel, so"
echo "no router port is opened; only your chat id can use it."
printf "Enable it? [Y/n]: "
read -r ans; case "${ans:-y}" in [Nn]*) mini=0;; *) mini=1;; esac
if [ "$mini" = 1 ] && ! have cloudflared; then
printf "It needs a small tunnel client, which isn't installed. Install it now? [Y/n]: "
read -r ans; case "${ans:-y}" in
[Nn]*) warn "skipped — the panel stays on localhost only"; mini=0;;
*) if install_tunnel_client; then ok "tunnel client installed"
else warn "install failed — disabling the Mini App (bot + dashboard still work)."; mini=0; fi;;
esac
fi
"${ONBOARD[@]}" set-env "$ENV_FILE" MINIAPP_ENABLE "$mini"
fi
# -- free-agent fallback -----------------------------------------------------
# The ladder's last rung hands a limited turn to a non-Anthropic model through
# opencode. Offering the install here is why the dashboard never has to ask
# anyone to paste a curl command. Asked only while it's missing, so a re-run
# skips it once installed.
if ! have_opencode; then
step "🪄" "Free-agent fallback"
echo "When your Claude accounts run out of quota, sessions can hand off to a free"
echo "provider (opencode zen, Gemini, Qwen, local Ollama). That runs through the"
echo "'opencode' CLI — a ~60MB download. Provider keys are added later, in the dashboard."
printf "Install it now? [Y/n]: "
read -r ans || ans=n # EOF (piped/CI) = don't download
case "${ans:-y}" in
[Nn]*) warn "skipped — re-run ./setup.sh any time to install it" ;;
*) echo "Downloading opencode…"
if install_opencode; then ok "opencode installed → ~/.local/bin/opencode"
else warn "install failed — free-agent rungs stay off (bot, dashboard and Claude are unaffected)."; fi ;;
esac
fi
# -- Kimi Code ---------------------------------------------------------------
# A session can run on Kimi Code (Settings ▸ Accounts ▸ Agents, then a profile).
# It runs as `npx -y <pin>`; fetching the pin now keeps a ~50 MB download out of
# the first turn. The bridge fetches it again at every boot (bridge/acp_setup.py),
# which is what installs a new pin after an update. Logins happen in Settings.
step "🌙" "Kimi Code"
if [ -z "$KIMI_PKG" ]; then warn "couldn't read the Kimi Code pin from bridge/acp_agents.py — skipped"
elif ! node_ok_for_kimi; then warn "needs Node 22.19+ (have: $(node -v 2>/dev/null || echo none)) — Kimi Code sessions stay off"
else echo "Fetching $KIMI_PKG…"
if npx -y "$KIMI_PKG" --version >/dev/null 2>&1; then ok "Kimi Code ready — sign in under Settings ▸ Accounts ▸ Agents"
else warn "fetch failed — the bridge retries at start; Claude is unaffected"; fi
fi
# -- link `mystical` onto PATH -----------------------------------------------
mkdir -p "$HOME/.local/bin"
ln -sf "$REPO/bin/mystical" "$HOME/.local/bin/mystical"
ok "linked 'mystical' → ~/.local/bin/mystical"
# -- build the web bundles (dist/ is git-ignored, so a fresh clone has none) ---
"$REPO/bin/mystical" build
case ":${PATH_ORIG:-$PATH}:" in
*":$HOME/.local/bin:"*) : ;;
*) rc="$HOME/.bashrc"; case "${SHELL:-}" in *zsh) rc="$HOME/.zshrc";; esac
printf "~/.local/bin isn't on your PATH. Add it to %s? [Y/n]: " "${rc/#$HOME/\~}"
read -r ans || ans=n; case "${ans:-y}" in
[Nn]*) warn "Add manually: export PATH=\"\$HOME/.local/bin:\$PATH\"" ;;
*) printf '\nexport PATH="$HOME/.local/bin:$PATH"\n' >> "$rc"
ok "added to ${rc/#$HOME/\~} (open a new shell, or run: export PATH=\"\$HOME/.local/bin:\$PATH\")" ;;
esac ;;
esac
# -- macOS: the app, and the bridge at login ----------------------------------
# Both asked only while missing, like everything above. The agent goes through
# bridge/startup.py — the same code as the dashboard's START AT LOGIN switch —
# and the "start now" below then goes through launchd.
if [ "$(uname -s)" = Darwin ]; then
if [ ! -d "$HOME/Applications/Mystical.app" ] && have swift; then
step "🍎" "Mac app"
echo "Mystical.app puts the dashboard in its own window and the menu bar, and"
echo "notifies you when a session needs you — even with the window closed."
printf "Build it into ~/Applications (about a minute)? [Y/n]: "
read -r ans || ans=n
case "${ans:-y}" in
[Nn]*) warn "skipped — build it any time with: mystical app" ;;
*) "$REPO/macos/build.sh" || warn "build failed — retry with: mystical app" ;;
esac
fi
if [ ! -f "$HOME/Library/LaunchAgents/cloud.ainurhq.mystical.bridge.plist" ]; then
step "🔁" "Start at login"
echo "A launchd agent can run the bridge for you: up when you log in, and"
echo "restarted if it ever crashes. Toggle it later in the dashboard's SYSTEM tab."
printf "Start the bridge at login? [Y/n]: "
read -r ans || ans=n
case "${ans:-y}" in
[Nn]*) warn "skipped — the bridge runs only when you start it" ;;
*) win=False; [ -d "$HOME/Applications/Mystical.app" ] && win=True
if PYTHONPATH="$REPO" python3 -c "from bridge import startup; startup.apply(True, $win)"; then
ok "launchd agent installed${c_d} — it takes over at the next 'mystical restart' or login${c_0}"
else warn "couldn't install the launchd agent — see the error above"; fi ;;
esac
fi
fi
# -- Windows (WSL): the app ----------------------------------------------------
# Asked only while missing, like the Mac's. START AT LOGIN is the existing
# systemd unit plus the Startup entry; the install points that entry at the exe.
if grep -qi microsoft /proc/sys/kernel/osrelease 2>/dev/null \
&& { command -v dotnet.exe >/dev/null 2>&1 || [ -x "/mnt/c/Program Files/dotnet/dotnet.exe" ]; } \
&& ! ls /mnt/c/Users/*/AppData/Local/Programs/Mystical/Mystical.exe >/dev/null 2>&1; then
step "🪟" "Windows app"
echo "Mystical.exe puts the dashboard in its own window and the tray, and"
echo "notifies you when a session needs you — even with the window closed."
printf 'Build it into %%LOCALAPPDATA%%\\Programs\\Mystical (about a minute)? [Y/n]: '
read -r ans || ans=n
case "${ans:-y}" in
[Nn]*) warn "skipped — build it any time with: mystical app" ;;
*) if "$REPO/windows/build.sh"; then
PYTHONPATH="$REPO" python3 -c "from bridge import startup; startup.refresh_launcher()" >/dev/null 2>&1 || true
else warn "build failed — retry with: mystical app"; fi ;;
esac
fi
# -- recap: on a re-run this is the only output, so it has to stand alone -----
step "✨" "Ready"
port="$(get_env DASH_PORT)"; port="${port:-8790}"
dtok="$(get_env DASH_TOKEN)"
dash="http://127.0.0.1:$port/"
if [ -n "$dtok" ]; then dash="$dash?token=$dtok"; fi
case "$(get_env NEW_SESSION_PERMISSION_MODE)" in
bypassPermissions) perm="${c_y}full autonomy${c_0} — runs commands without asking" ;;
*) perm="asks before running commands" ;;
esac
case "$(get_env MINIAPP_ENABLE)" in
1) mini="${c_g}on${c_0}" ;;
*) mini="${c_d}off${c_0}" ;;
esac
row() { printf ' %s %s%-11s%s %s\n' "$1" "$c_d" "$2" "$c_0" "$3"; }
if [ "$tg" = 1 ]; then
row 🤖 bot "@$botname ${c_d}—${c_0} https://t.me/$botname"
else
row 🤖 bot "${c_d}skipped — dashboard only$c_0"
fi
row 📁 projects "$(get_env BASE_PATH)"
row 🔐 permissions "$perm"
row 📱 "mini app" "$mini"
row 🌐 dashboard "$dash"
echo
printf '%sThen:%s %smystical status%s · %smystical logs%s · %smystical stop%s\n' \
"$c_d" "$c_0" "$c_c" "$c_0" "$c_c" "$c_0" "$c_c" "$c_0"
printf "Start the bridge now? [Y/n]: "
read -r ans || ans=n; case "${ans:-y}" in # EOF (piped/CI) = don't start
[Nn]*) echo "Start it later with: mystical" ;;
*) exec "$HOME/.local/bin/mystical" ;;
esac