@castlekit/castle 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -1
- package/bin/castle.js +94 -0
- package/install.sh +722 -0
- package/next.config.ts +7 -0
- package/package.json +54 -5
- package/postcss.config.mjs +7 -0
- package/src/app/api/avatars/[id]/route.ts +75 -0
- package/src/app/api/openclaw/agents/route.ts +107 -0
- package/src/app/api/openclaw/config/route.ts +94 -0
- package/src/app/api/openclaw/events/route.ts +96 -0
- package/src/app/api/openclaw/logs/route.ts +59 -0
- package/src/app/api/openclaw/ping/route.ts +68 -0
- package/src/app/api/openclaw/restart/route.ts +65 -0
- package/src/app/api/openclaw/sessions/route.ts +62 -0
- package/src/app/globals.css +286 -0
- package/src/app/icon.png +0 -0
- package/src/app/layout.tsx +42 -0
- package/src/app/page.tsx +269 -0
- package/src/app/ui-kit/page.tsx +684 -0
- package/src/cli/onboarding.ts +576 -0
- package/src/components/dashboard/agent-status.tsx +107 -0
- package/src/components/dashboard/glass-card.tsx +28 -0
- package/src/components/dashboard/goal-widget.tsx +174 -0
- package/src/components/dashboard/greeting-widget.tsx +78 -0
- package/src/components/dashboard/index.ts +7 -0
- package/src/components/dashboard/stat-widget.tsx +61 -0
- package/src/components/dashboard/stock-widget.tsx +164 -0
- package/src/components/dashboard/weather-widget.tsx +68 -0
- package/src/components/icons/castle-icon.tsx +21 -0
- package/src/components/kanban/index.ts +3 -0
- package/src/components/kanban/kanban-board.tsx +391 -0
- package/src/components/kanban/kanban-card.tsx +137 -0
- package/src/components/kanban/kanban-column.tsx +98 -0
- package/src/components/layout/index.ts +4 -0
- package/src/components/layout/page-header.tsx +20 -0
- package/src/components/layout/sidebar.tsx +128 -0
- package/src/components/layout/theme-toggle.tsx +59 -0
- package/src/components/layout/user-menu.tsx +72 -0
- package/src/components/ui/alert.tsx +72 -0
- package/src/components/ui/avatar.tsx +87 -0
- package/src/components/ui/badge.tsx +39 -0
- package/src/components/ui/button.tsx +43 -0
- package/src/components/ui/card.tsx +107 -0
- package/src/components/ui/checkbox.tsx +56 -0
- package/src/components/ui/clock.tsx +171 -0
- package/src/components/ui/dialog.tsx +105 -0
- package/src/components/ui/index.ts +34 -0
- package/src/components/ui/input.tsx +112 -0
- package/src/components/ui/option-card.tsx +151 -0
- package/src/components/ui/progress.tsx +103 -0
- package/src/components/ui/radio.tsx +109 -0
- package/src/components/ui/select.tsx +46 -0
- package/src/components/ui/slider.tsx +62 -0
- package/src/components/ui/tabs.tsx +132 -0
- package/src/components/ui/toggle-group.tsx +85 -0
- package/src/components/ui/toggle.tsx +78 -0
- package/src/components/ui/tooltip.tsx +145 -0
- package/src/components/ui/uptime.tsx +106 -0
- package/src/lib/config.ts +195 -0
- package/src/lib/gateway-connection.ts +391 -0
- package/src/lib/hooks/use-openclaw.ts +163 -0
- package/src/lib/utils.ts +6 -0
- package/tsconfig.json +34 -0
package/install.sh
ADDED
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Castle Installer for macOS and Linux
|
|
5
|
+
# Usage: curl -fsSL --proto '=https' --tlsv1.2 https://castlekit.com/install.sh | bash
|
|
6
|
+
|
|
7
|
+
BOLD='\033[1m'
|
|
8
|
+
ACCENT='\033[38;2;59;130;246m'
|
|
9
|
+
ACCENT_DIM='\033[38;2;37;99;235m'
|
|
10
|
+
INFO='\033[38;2;96;165;250m'
|
|
11
|
+
SUCCESS='\033[38;2;34;197;94m'
|
|
12
|
+
WARN='\033[38;2;245;158;11m'
|
|
13
|
+
ERROR='\033[38;2;239;68;68m'
|
|
14
|
+
MUTED='\033[38;2;113;113;122m'
|
|
15
|
+
NC='\033[0m' # No Color
|
|
16
|
+
|
|
17
|
+
DEFAULT_TAGLINE="The multi-agent workspace."
|
|
18
|
+
|
|
19
|
+
ORIGINAL_PATH="${PATH:-}"
|
|
20
|
+
|
|
21
|
+
TMPFILES=()
|
|
22
|
+
cleanup_tmpfiles() {
|
|
23
|
+
local f
|
|
24
|
+
for f in "${TMPFILES[@]:-}"; do
|
|
25
|
+
rm -f "$f" 2>/dev/null || true
|
|
26
|
+
done
|
|
27
|
+
}
|
|
28
|
+
trap cleanup_tmpfiles EXIT
|
|
29
|
+
|
|
30
|
+
mktempfile() {
|
|
31
|
+
local f
|
|
32
|
+
f="$(mktemp)"
|
|
33
|
+
TMPFILES+=("$f")
|
|
34
|
+
echo "$f"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
DOWNLOADER=""
|
|
38
|
+
detect_downloader() {
|
|
39
|
+
if command -v curl &> /dev/null; then
|
|
40
|
+
DOWNLOADER="curl"
|
|
41
|
+
return 0
|
|
42
|
+
fi
|
|
43
|
+
if command -v wget &> /dev/null; then
|
|
44
|
+
DOWNLOADER="wget"
|
|
45
|
+
return 0
|
|
46
|
+
fi
|
|
47
|
+
echo -e "${ERROR}Error: Missing downloader (curl or wget required)${NC}"
|
|
48
|
+
exit 1
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
download_file() {
|
|
52
|
+
local url="$1"
|
|
53
|
+
local output="$2"
|
|
54
|
+
if [[ -z "$DOWNLOADER" ]]; then
|
|
55
|
+
detect_downloader
|
|
56
|
+
fi
|
|
57
|
+
if [[ "$DOWNLOADER" == "curl" ]]; then
|
|
58
|
+
curl -fsSL --proto '=https' --tlsv1.2 --retry 3 --retry-delay 1 --retry-connrefused -o "$output" "$url"
|
|
59
|
+
return
|
|
60
|
+
fi
|
|
61
|
+
wget -q --https-only --secure-protocol=TLSv1_2 --tries=3 --timeout=20 -O "$output" "$url"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
run_remote_bash() {
|
|
65
|
+
local url="$1"
|
|
66
|
+
local tmp
|
|
67
|
+
tmp="$(mktempfile)"
|
|
68
|
+
download_file "$url" "$tmp"
|
|
69
|
+
/bin/bash "$tmp"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# ─── Taglines ────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
TAGLINES=()
|
|
75
|
+
TAGLINES+=("Your kingdom awaits, sire.")
|
|
76
|
+
TAGLINES+=("The throne room is ready.")
|
|
77
|
+
TAGLINES+=("A fortress for your AI agents.")
|
|
78
|
+
TAGLINES+=("All hail the command center.")
|
|
79
|
+
TAGLINES+=("Knights of the round terminal.")
|
|
80
|
+
TAGLINES+=("Raise the drawbridge, lower the latency.")
|
|
81
|
+
TAGLINES+=("By royal decree, your agents are assembled.")
|
|
82
|
+
TAGLINES+=("The court is now in session.")
|
|
83
|
+
TAGLINES+=("From castle walls to API calls.")
|
|
84
|
+
TAGLINES+=("Forged in code, ruled by you.")
|
|
85
|
+
TAGLINES+=("Every king needs a castle.")
|
|
86
|
+
TAGLINES+=("Where agents serve and dragons compile.")
|
|
87
|
+
TAGLINES+=("The siege of busywork ends here.")
|
|
88
|
+
TAGLINES+=("Hear ye, hear ye — your agents await.")
|
|
89
|
+
TAGLINES+=("A castle built on open source bedrock.")
|
|
90
|
+
TAGLINES+=("One does not simply walk in without a CLI.")
|
|
91
|
+
TAGLINES+=("The moat is deep but the docs are deeper.")
|
|
92
|
+
TAGLINES+=("Fear not the dark mode, for it is default.")
|
|
93
|
+
TAGLINES+=("In the land of AI, the castlekeeper wears a hoodie.")
|
|
94
|
+
TAGLINES+=("Your agents kneel before the terminal.")
|
|
95
|
+
TAGLINES+=("Excalibur was a sword. This is better.")
|
|
96
|
+
TAGLINES+=("npm install --save-the-kingdom.")
|
|
97
|
+
TAGLINES+=("The Round Table, but make it a dashboard.")
|
|
98
|
+
TAGLINES+=("Dragons? Handled. Bugs? Working on it.")
|
|
99
|
+
TAGLINES+=("A quest to automate the mundane.")
|
|
100
|
+
|
|
101
|
+
pick_tagline() {
|
|
102
|
+
local count=${#TAGLINES[@]}
|
|
103
|
+
if [[ "$count" -eq 0 ]]; then
|
|
104
|
+
echo "$DEFAULT_TAGLINE"
|
|
105
|
+
return
|
|
106
|
+
fi
|
|
107
|
+
if [[ -n "${CASTLE_TAGLINE_INDEX:-}" ]]; then
|
|
108
|
+
if [[ "${CASTLE_TAGLINE_INDEX}" =~ ^[0-9]+$ ]]; then
|
|
109
|
+
local idx=$((CASTLE_TAGLINE_INDEX % count))
|
|
110
|
+
echo "${TAGLINES[$idx]}"
|
|
111
|
+
return
|
|
112
|
+
fi
|
|
113
|
+
fi
|
|
114
|
+
local idx=$((RANDOM % count))
|
|
115
|
+
echo "${TAGLINES[$idx]}"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
TAGLINE=$(pick_tagline)
|
|
119
|
+
|
|
120
|
+
# ─── Options ─────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
NO_ONBOARD=${CASTLE_NO_ONBOARD:-0}
|
|
123
|
+
NO_PROMPT=${CASTLE_NO_PROMPT:-0}
|
|
124
|
+
DRY_RUN=${CASTLE_DRY_RUN:-0}
|
|
125
|
+
CASTLE_VERSION=${CASTLE_VERSION:-latest}
|
|
126
|
+
VERBOSE="${CASTLE_VERBOSE:-0}"
|
|
127
|
+
NPM_LOGLEVEL="${CASTLE_NPM_LOGLEVEL:-error}"
|
|
128
|
+
NPM_SILENT_FLAG="--silent"
|
|
129
|
+
CASTLE_BIN=""
|
|
130
|
+
HELP=0
|
|
131
|
+
|
|
132
|
+
print_usage() {
|
|
133
|
+
cat <<EOF
|
|
134
|
+
Castle installer (macOS + Linux)
|
|
135
|
+
|
|
136
|
+
Usage:
|
|
137
|
+
curl -fsSL --proto '=https' --tlsv1.2 https://castlekit.com/install.sh | bash -s -- [options]
|
|
138
|
+
|
|
139
|
+
Options:
|
|
140
|
+
--version <version> npm version to install (default: latest)
|
|
141
|
+
--no-onboard Skip setup wizard after install
|
|
142
|
+
--no-prompt Disable prompts (for CI/automation)
|
|
143
|
+
--dry-run Print what would happen (no changes)
|
|
144
|
+
--verbose Print debug output
|
|
145
|
+
--help, -h Show this help
|
|
146
|
+
|
|
147
|
+
Environment variables:
|
|
148
|
+
CASTLE_VERSION=latest|<semver>
|
|
149
|
+
CASTLE_NO_ONBOARD=0|1
|
|
150
|
+
CASTLE_NO_PROMPT=1
|
|
151
|
+
CASTLE_DRY_RUN=1
|
|
152
|
+
CASTLE_VERBOSE=1
|
|
153
|
+
CASTLE_NPM_LOGLEVEL=error|warn|notice
|
|
154
|
+
|
|
155
|
+
Examples:
|
|
156
|
+
curl -fsSL --proto '=https' --tlsv1.2 https://castlekit.com/install.sh | bash
|
|
157
|
+
curl -fsSL --proto '=https' --tlsv1.2 https://castlekit.com/install.sh | bash -s -- --no-onboard
|
|
158
|
+
EOF
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
parse_args() {
|
|
162
|
+
while [[ $# -gt 0 ]]; do
|
|
163
|
+
case "$1" in
|
|
164
|
+
--no-onboard)
|
|
165
|
+
NO_ONBOARD=1
|
|
166
|
+
shift
|
|
167
|
+
;;
|
|
168
|
+
--dry-run)
|
|
169
|
+
DRY_RUN=1
|
|
170
|
+
shift
|
|
171
|
+
;;
|
|
172
|
+
--verbose)
|
|
173
|
+
VERBOSE=1
|
|
174
|
+
shift
|
|
175
|
+
;;
|
|
176
|
+
--no-prompt)
|
|
177
|
+
NO_PROMPT=1
|
|
178
|
+
shift
|
|
179
|
+
;;
|
|
180
|
+
--help|-h)
|
|
181
|
+
HELP=1
|
|
182
|
+
shift
|
|
183
|
+
;;
|
|
184
|
+
--version)
|
|
185
|
+
if [[ -z "${2:-}" ]]; then
|
|
186
|
+
echo "Error: --version requires a value"
|
|
187
|
+
exit 1
|
|
188
|
+
fi
|
|
189
|
+
CASTLE_VERSION="$2"
|
|
190
|
+
shift 2
|
|
191
|
+
;;
|
|
192
|
+
*)
|
|
193
|
+
shift
|
|
194
|
+
;;
|
|
195
|
+
esac
|
|
196
|
+
done
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
configure_verbose() {
|
|
200
|
+
if [[ "$VERBOSE" != "1" ]]; then
|
|
201
|
+
return 0
|
|
202
|
+
fi
|
|
203
|
+
if [[ "$NPM_LOGLEVEL" == "error" ]]; then
|
|
204
|
+
NPM_LOGLEVEL="notice"
|
|
205
|
+
fi
|
|
206
|
+
NPM_SILENT_FLAG=""
|
|
207
|
+
set -x
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
is_promptable() {
|
|
211
|
+
if [[ "$NO_PROMPT" == "1" ]]; then
|
|
212
|
+
return 1
|
|
213
|
+
fi
|
|
214
|
+
if [[ -r /dev/tty && -w /dev/tty ]]; then
|
|
215
|
+
return 0
|
|
216
|
+
fi
|
|
217
|
+
return 1
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# ─── System detection ────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
is_root() {
|
|
223
|
+
[[ "$(id -u)" -eq 0 ]]
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
maybe_sudo() {
|
|
227
|
+
if is_root; then
|
|
228
|
+
if [[ "${1:-}" == "-E" ]]; then
|
|
229
|
+
shift
|
|
230
|
+
fi
|
|
231
|
+
"$@"
|
|
232
|
+
else
|
|
233
|
+
sudo "$@"
|
|
234
|
+
fi
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
require_sudo() {
|
|
238
|
+
if [[ "$OS" != "linux" ]]; then
|
|
239
|
+
return 0
|
|
240
|
+
fi
|
|
241
|
+
if is_root; then
|
|
242
|
+
return 0
|
|
243
|
+
fi
|
|
244
|
+
if command -v sudo &> /dev/null; then
|
|
245
|
+
return 0
|
|
246
|
+
fi
|
|
247
|
+
echo -e "${ERROR}Error: sudo is required for system installs on Linux${NC}"
|
|
248
|
+
echo "Install sudo or re-run as root."
|
|
249
|
+
exit 1
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# ─── Homebrew ────────────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
install_homebrew() {
|
|
255
|
+
if [[ "$OS" == "macos" ]]; then
|
|
256
|
+
if ! command -v brew &> /dev/null; then
|
|
257
|
+
echo -e "${WARN}→${NC} Installing Homebrew..."
|
|
258
|
+
run_remote_bash "https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh"
|
|
259
|
+
|
|
260
|
+
if [[ -f "/opt/homebrew/bin/brew" ]]; then
|
|
261
|
+
eval "$(/opt/homebrew/bin/brew shellenv)"
|
|
262
|
+
elif [[ -f "/usr/local/bin/brew" ]]; then
|
|
263
|
+
eval "$(/usr/local/bin/brew shellenv)"
|
|
264
|
+
fi
|
|
265
|
+
echo -e "${SUCCESS}✓${NC} Homebrew installed"
|
|
266
|
+
else
|
|
267
|
+
echo -e "${SUCCESS}✓${NC} Homebrew already installed"
|
|
268
|
+
fi
|
|
269
|
+
fi
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
# ─── Node.js ─────────────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
check_node() {
|
|
275
|
+
if command -v node &> /dev/null; then
|
|
276
|
+
NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
|
|
277
|
+
if [[ "$NODE_VERSION" -ge 22 ]]; then
|
|
278
|
+
echo -e "${SUCCESS}✓${NC} Node.js v$(node -v | cut -d'v' -f2) found"
|
|
279
|
+
return 0
|
|
280
|
+
else
|
|
281
|
+
echo -e "${WARN}→${NC} Node.js $(node -v) found, but v22+ required"
|
|
282
|
+
return 1
|
|
283
|
+
fi
|
|
284
|
+
else
|
|
285
|
+
echo -e "${WARN}→${NC} Node.js not found"
|
|
286
|
+
return 1
|
|
287
|
+
fi
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
install_node() {
|
|
291
|
+
if [[ "$OS" == "macos" ]]; then
|
|
292
|
+
echo -e "${WARN}→${NC} Installing Node.js via Homebrew..."
|
|
293
|
+
brew install node@22
|
|
294
|
+
brew link node@22 --overwrite --force 2>/dev/null || true
|
|
295
|
+
echo -e "${SUCCESS}✓${NC} Node.js installed"
|
|
296
|
+
elif [[ "$OS" == "linux" ]]; then
|
|
297
|
+
echo -e "${WARN}→${NC} Installing Node.js via NodeSource..."
|
|
298
|
+
require_sudo
|
|
299
|
+
if command -v apt-get &> /dev/null; then
|
|
300
|
+
local tmp
|
|
301
|
+
tmp="$(mktempfile)"
|
|
302
|
+
download_file "https://deb.nodesource.com/setup_22.x" "$tmp"
|
|
303
|
+
maybe_sudo -E bash "$tmp"
|
|
304
|
+
maybe_sudo apt-get install -y nodejs
|
|
305
|
+
elif command -v dnf &> /dev/null; then
|
|
306
|
+
local tmp
|
|
307
|
+
tmp="$(mktempfile)"
|
|
308
|
+
download_file "https://rpm.nodesource.com/setup_22.x" "$tmp"
|
|
309
|
+
maybe_sudo bash "$tmp"
|
|
310
|
+
maybe_sudo dnf install -y nodejs
|
|
311
|
+
elif command -v yum &> /dev/null; then
|
|
312
|
+
local tmp
|
|
313
|
+
tmp="$(mktempfile)"
|
|
314
|
+
download_file "https://rpm.nodesource.com/setup_22.x" "$tmp"
|
|
315
|
+
maybe_sudo bash "$tmp"
|
|
316
|
+
maybe_sudo yum install -y nodejs
|
|
317
|
+
else
|
|
318
|
+
echo -e "${ERROR}Error: Could not detect package manager${NC}"
|
|
319
|
+
echo "Please install Node.js 22+ manually: https://nodejs.org"
|
|
320
|
+
exit 1
|
|
321
|
+
fi
|
|
322
|
+
echo -e "${SUCCESS}✓${NC} Node.js installed"
|
|
323
|
+
fi
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
# ─── Git ─────────────────────────────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
check_git() {
|
|
329
|
+
if command -v git &> /dev/null; then
|
|
330
|
+
echo -e "${SUCCESS}✓${NC} Git already installed"
|
|
331
|
+
return 0
|
|
332
|
+
fi
|
|
333
|
+
echo -e "${WARN}→${NC} Git not found"
|
|
334
|
+
return 1
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
install_git() {
|
|
338
|
+
echo -e "${WARN}→${NC} Installing Git..."
|
|
339
|
+
if [[ "$OS" == "macos" ]]; then
|
|
340
|
+
brew install git
|
|
341
|
+
elif [[ "$OS" == "linux" ]]; then
|
|
342
|
+
require_sudo
|
|
343
|
+
if command -v apt-get &> /dev/null; then
|
|
344
|
+
maybe_sudo apt-get update -y
|
|
345
|
+
maybe_sudo apt-get install -y git
|
|
346
|
+
elif command -v dnf &> /dev/null; then
|
|
347
|
+
maybe_sudo dnf install -y git
|
|
348
|
+
elif command -v yum &> /dev/null; then
|
|
349
|
+
maybe_sudo yum install -y git
|
|
350
|
+
else
|
|
351
|
+
echo -e "${ERROR}Error: Could not detect package manager for Git${NC}"
|
|
352
|
+
exit 1
|
|
353
|
+
fi
|
|
354
|
+
fi
|
|
355
|
+
echo -e "${SUCCESS}✓${NC} Git installed"
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
# ─── npm permissions ─────────────────────────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
fix_npm_permissions() {
|
|
361
|
+
if [[ "$OS" != "linux" ]]; then
|
|
362
|
+
return 0
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
local npm_prefix
|
|
366
|
+
npm_prefix="$(npm config get prefix 2>/dev/null || true)"
|
|
367
|
+
if [[ -z "$npm_prefix" ]]; then
|
|
368
|
+
return 0
|
|
369
|
+
fi
|
|
370
|
+
|
|
371
|
+
if [[ -w "$npm_prefix" || -w "$npm_prefix/lib" ]]; then
|
|
372
|
+
return 0
|
|
373
|
+
fi
|
|
374
|
+
|
|
375
|
+
echo -e "${WARN}→${NC} Configuring npm for user-local installs..."
|
|
376
|
+
mkdir -p "$HOME/.npm-global"
|
|
377
|
+
npm config set prefix "$HOME/.npm-global"
|
|
378
|
+
|
|
379
|
+
# shellcheck disable=SC2016
|
|
380
|
+
local path_line='export PATH="$HOME/.npm-global/bin:$PATH"'
|
|
381
|
+
for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
|
|
382
|
+
if [[ -f "$rc" ]] && ! grep -q ".npm-global" "$rc"; then
|
|
383
|
+
echo "$path_line" >> "$rc"
|
|
384
|
+
fi
|
|
385
|
+
done
|
|
386
|
+
|
|
387
|
+
export PATH="$HOME/.npm-global/bin:$PATH"
|
|
388
|
+
echo -e "${SUCCESS}✓${NC} npm configured for user installs"
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
# ─── PATH helpers ────────────────────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
npm_global_bin_dir() {
|
|
394
|
+
local prefix=""
|
|
395
|
+
prefix="$(npm prefix -g 2>/dev/null || true)"
|
|
396
|
+
if [[ -n "$prefix" ]]; then
|
|
397
|
+
if [[ "$prefix" == /* ]]; then
|
|
398
|
+
echo "${prefix%/}/bin"
|
|
399
|
+
return 0
|
|
400
|
+
fi
|
|
401
|
+
fi
|
|
402
|
+
|
|
403
|
+
prefix="$(npm config get prefix 2>/dev/null || true)"
|
|
404
|
+
if [[ -n "$prefix" && "$prefix" != "undefined" && "$prefix" != "null" ]]; then
|
|
405
|
+
if [[ "$prefix" == /* ]]; then
|
|
406
|
+
echo "${prefix%/}/bin"
|
|
407
|
+
return 0
|
|
408
|
+
fi
|
|
409
|
+
fi
|
|
410
|
+
|
|
411
|
+
echo ""
|
|
412
|
+
return 1
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
refresh_shell_command_cache() {
|
|
416
|
+
hash -r 2>/dev/null || true
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
path_has_dir() {
|
|
420
|
+
local path="$1"
|
|
421
|
+
local dir="${2%/}"
|
|
422
|
+
if [[ -z "$dir" ]]; then
|
|
423
|
+
return 1
|
|
424
|
+
fi
|
|
425
|
+
case ":${path}:" in
|
|
426
|
+
*":${dir}:"*) return 0 ;;
|
|
427
|
+
*) return 1 ;;
|
|
428
|
+
esac
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
warn_shell_path_missing_dir() {
|
|
432
|
+
local dir="${1%/}"
|
|
433
|
+
local label="$2"
|
|
434
|
+
if [[ -z "$dir" ]]; then
|
|
435
|
+
return 0
|
|
436
|
+
fi
|
|
437
|
+
if path_has_dir "$ORIGINAL_PATH" "$dir"; then
|
|
438
|
+
return 0
|
|
439
|
+
fi
|
|
440
|
+
|
|
441
|
+
echo ""
|
|
442
|
+
echo -e "${WARN}→${NC} PATH warning: missing ${label}: ${INFO}${dir}${NC}"
|
|
443
|
+
echo -e "This can make ${INFO}castle${NC} show as \"command not found\" in new terminals."
|
|
444
|
+
echo -e "Fix (zsh: ~/.zshrc, bash: ~/.bashrc):"
|
|
445
|
+
echo -e " export PATH=\"${dir}:\$PATH\""
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
ensure_npm_global_bin_on_path() {
|
|
449
|
+
local bin_dir=""
|
|
450
|
+
bin_dir="$(npm_global_bin_dir || true)"
|
|
451
|
+
if [[ -n "$bin_dir" ]]; then
|
|
452
|
+
export PATH="${bin_dir}:$PATH"
|
|
453
|
+
fi
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
maybe_nodenv_rehash() {
|
|
457
|
+
if command -v nodenv &> /dev/null; then
|
|
458
|
+
nodenv rehash >/dev/null 2>&1 || true
|
|
459
|
+
fi
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
source_node_version_manager() {
|
|
463
|
+
# Source nvm if available (curl|bash subshells don't have it loaded)
|
|
464
|
+
if [[ -z "${NVM_DIR:-}" ]]; then
|
|
465
|
+
if [[ -d "$HOME/.nvm" ]]; then
|
|
466
|
+
export NVM_DIR="$HOME/.nvm"
|
|
467
|
+
fi
|
|
468
|
+
fi
|
|
469
|
+
if [[ -n "${NVM_DIR:-}" && -s "${NVM_DIR}/nvm.sh" ]]; then
|
|
470
|
+
source "${NVM_DIR}/nvm.sh" 2>/dev/null || true
|
|
471
|
+
fi
|
|
472
|
+
|
|
473
|
+
# Source fnm if available
|
|
474
|
+
if command -v fnm &> /dev/null; then
|
|
475
|
+
eval "$(fnm env 2>/dev/null)" || true
|
|
476
|
+
fi
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
resolve_castle_bin() {
|
|
480
|
+
refresh_shell_command_cache
|
|
481
|
+
local resolved=""
|
|
482
|
+
resolved="$(type -P castle 2>/dev/null || true)"
|
|
483
|
+
if [[ -n "$resolved" && -x "$resolved" ]]; then
|
|
484
|
+
echo "$resolved"
|
|
485
|
+
return 0
|
|
486
|
+
fi
|
|
487
|
+
|
|
488
|
+
# Source nvm/fnm in case we're in a curl|bash subshell
|
|
489
|
+
source_node_version_manager
|
|
490
|
+
refresh_shell_command_cache
|
|
491
|
+
resolved="$(type -P castle 2>/dev/null || true)"
|
|
492
|
+
if [[ -n "$resolved" && -x "$resolved" ]]; then
|
|
493
|
+
echo "$resolved"
|
|
494
|
+
return 0
|
|
495
|
+
fi
|
|
496
|
+
|
|
497
|
+
ensure_npm_global_bin_on_path
|
|
498
|
+
refresh_shell_command_cache
|
|
499
|
+
resolved="$(type -P castle 2>/dev/null || true)"
|
|
500
|
+
if [[ -n "$resolved" && -x "$resolved" ]]; then
|
|
501
|
+
echo "$resolved"
|
|
502
|
+
return 0
|
|
503
|
+
fi
|
|
504
|
+
|
|
505
|
+
local npm_bin=""
|
|
506
|
+
npm_bin="$(npm_global_bin_dir || true)"
|
|
507
|
+
if [[ -n "$npm_bin" && -x "${npm_bin}/castle" ]]; then
|
|
508
|
+
echo "${npm_bin}/castle"
|
|
509
|
+
return 0
|
|
510
|
+
fi
|
|
511
|
+
|
|
512
|
+
# Brute force: check common global bin locations
|
|
513
|
+
local common_paths=(
|
|
514
|
+
"$HOME/.nvm/versions/node/$(node -v 2>/dev/null || echo v0)/bin/castle"
|
|
515
|
+
"$HOME/.npm-global/bin/castle"
|
|
516
|
+
"/usr/local/bin/castle"
|
|
517
|
+
"/opt/homebrew/bin/castle"
|
|
518
|
+
)
|
|
519
|
+
for p in "${common_paths[@]}"; do
|
|
520
|
+
if [[ -x "$p" ]]; then
|
|
521
|
+
echo "$p"
|
|
522
|
+
return 0
|
|
523
|
+
fi
|
|
524
|
+
done
|
|
525
|
+
|
|
526
|
+
maybe_nodenv_rehash
|
|
527
|
+
refresh_shell_command_cache
|
|
528
|
+
resolved="$(type -P castle 2>/dev/null || true)"
|
|
529
|
+
if [[ -n "$resolved" && -x "$resolved" ]]; then
|
|
530
|
+
echo "$resolved"
|
|
531
|
+
return 0
|
|
532
|
+
fi
|
|
533
|
+
|
|
534
|
+
echo ""
|
|
535
|
+
return 1
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
warn_castle_not_found() {
|
|
539
|
+
echo -e "${WARN}→${NC} Installed, but ${INFO}castle${NC} is not discoverable on PATH in this shell."
|
|
540
|
+
echo -e "Try: ${INFO}hash -r${NC} (bash) or ${INFO}rehash${NC} (zsh), then retry."
|
|
541
|
+
local npm_bin=""
|
|
542
|
+
npm_bin="$(npm_global_bin_dir 2>/dev/null || true)"
|
|
543
|
+
if [[ -n "$npm_bin" ]]; then
|
|
544
|
+
echo -e "npm bin dir: ${INFO}${npm_bin}${NC}"
|
|
545
|
+
echo -e "If needed: ${INFO}export PATH=\"${npm_bin}:\$PATH\"${NC}"
|
|
546
|
+
fi
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
# ─── Install Castle ──────────────────────────────────────────────────────────
|
|
550
|
+
|
|
551
|
+
install_castle() {
|
|
552
|
+
local install_spec="@castlekit/castle@${CASTLE_VERSION}"
|
|
553
|
+
|
|
554
|
+
local resolved_version=""
|
|
555
|
+
resolved_version="$(npm view "${install_spec}" version 2>/dev/null || true)"
|
|
556
|
+
if [[ -n "$resolved_version" ]]; then
|
|
557
|
+
echo -e "${WARN}→${NC} Installing Castle ${INFO}${resolved_version}${NC}..."
|
|
558
|
+
else
|
|
559
|
+
echo -e "${WARN}→${NC} Installing Castle (${INFO}${CASTLE_VERSION}${NC})..."
|
|
560
|
+
fi
|
|
561
|
+
|
|
562
|
+
if ! npm --loglevel "$NPM_LOGLEVEL" ${NPM_SILENT_FLAG:+$NPM_SILENT_FLAG} --no-fund --no-audit install -g "$install_spec"; then
|
|
563
|
+
echo -e "${ERROR}npm install failed${NC}"
|
|
564
|
+
echo -e "Try: ${INFO}npm install -g --force ${install_spec}${NC}"
|
|
565
|
+
exit 1
|
|
566
|
+
fi
|
|
567
|
+
|
|
568
|
+
echo -e "${SUCCESS}✓${NC} Castle installed"
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
# ─── Main ────────────────────────────────────────────────────────────────────
|
|
572
|
+
|
|
573
|
+
main() {
|
|
574
|
+
if [[ "$HELP" == "1" ]]; then
|
|
575
|
+
print_usage
|
|
576
|
+
return 0
|
|
577
|
+
fi
|
|
578
|
+
|
|
579
|
+
if [[ "$DRY_RUN" == "1" ]]; then
|
|
580
|
+
echo -e "${SUCCESS}✓${NC} Dry run"
|
|
581
|
+
echo -e "${SUCCESS}✓${NC} Version: ${CASTLE_VERSION}"
|
|
582
|
+
echo -e "${MUTED}Dry run complete (no changes made).${NC}"
|
|
583
|
+
return 0
|
|
584
|
+
fi
|
|
585
|
+
|
|
586
|
+
# Check for existing installation
|
|
587
|
+
local is_upgrade=false
|
|
588
|
+
if [[ -n "$(type -P castle 2>/dev/null || true)" ]]; then
|
|
589
|
+
echo -e "${WARN}→${NC} Existing Castle installation detected"
|
|
590
|
+
is_upgrade=true
|
|
591
|
+
fi
|
|
592
|
+
|
|
593
|
+
# Step 1: Homebrew (macOS only)
|
|
594
|
+
install_homebrew
|
|
595
|
+
|
|
596
|
+
# Step 2: Node.js
|
|
597
|
+
if ! check_node; then
|
|
598
|
+
install_node
|
|
599
|
+
fi
|
|
600
|
+
|
|
601
|
+
# Step 3: Git
|
|
602
|
+
if ! check_git; then
|
|
603
|
+
install_git
|
|
604
|
+
fi
|
|
605
|
+
|
|
606
|
+
# Step 4: npm permissions (Linux)
|
|
607
|
+
fix_npm_permissions
|
|
608
|
+
|
|
609
|
+
# Step 5: Install Castle
|
|
610
|
+
install_castle
|
|
611
|
+
|
|
612
|
+
CASTLE_BIN="$(resolve_castle_bin || true)"
|
|
613
|
+
|
|
614
|
+
echo ""
|
|
615
|
+
echo -e "${SUCCESS}${BOLD}🏰 Castle installed successfully!${NC}"
|
|
616
|
+
|
|
617
|
+
if [[ "$is_upgrade" == "true" ]]; then
|
|
618
|
+
local update_messages=(
|
|
619
|
+
"The castle walls have been reinforced, my liege."
|
|
620
|
+
"New fortifications in place. The kingdom grows stronger."
|
|
621
|
+
"The royal engineers have been busy. Upgrade complete."
|
|
622
|
+
"Fresh stonework, same castle. Miss me?"
|
|
623
|
+
"The drawbridge has been upgraded. Smoother entry guaranteed."
|
|
624
|
+
)
|
|
625
|
+
local update_message
|
|
626
|
+
update_message="${update_messages[RANDOM % ${#update_messages[@]}]}"
|
|
627
|
+
echo -e "${MUTED}${update_message}${NC}"
|
|
628
|
+
else
|
|
629
|
+
local completion_messages=(
|
|
630
|
+
"The castle has been erected. Long may it stand!"
|
|
631
|
+
"Your fortress is ready, sire. What are your orders?"
|
|
632
|
+
"The court is assembled. Your agents await."
|
|
633
|
+
"A fine castle indeed. Time to rule."
|
|
634
|
+
"Stone by stone, the kingdom begins."
|
|
635
|
+
)
|
|
636
|
+
local completion_message
|
|
637
|
+
completion_message="${completion_messages[RANDOM % ${#completion_messages[@]}]}"
|
|
638
|
+
echo -e "${MUTED}${completion_message}${NC}"
|
|
639
|
+
fi
|
|
640
|
+
echo ""
|
|
641
|
+
|
|
642
|
+
# Step 6: Run onboarding
|
|
643
|
+
if [[ "$NO_ONBOARD" == "1" ]]; then
|
|
644
|
+
echo -e "Skipping setup (requested). Run ${INFO}castle setup${NC} later."
|
|
645
|
+
else
|
|
646
|
+
if [[ -r /dev/tty && -w /dev/tty ]]; then
|
|
647
|
+
echo -e "Starting setup..."
|
|
648
|
+
echo ""
|
|
649
|
+
exec </dev/tty
|
|
650
|
+
if [[ -n "$CASTLE_BIN" ]]; then
|
|
651
|
+
exec "$CASTLE_BIN" setup
|
|
652
|
+
else
|
|
653
|
+
# Fallback: run via npx (always works, no PATH needed)
|
|
654
|
+
exec npx --yes @castlekit/castle setup
|
|
655
|
+
fi
|
|
656
|
+
else
|
|
657
|
+
echo -e "${WARN}→${NC} No TTY available; skipping setup."
|
|
658
|
+
echo -e "Run ${INFO}castle setup${NC} later."
|
|
659
|
+
fi
|
|
660
|
+
fi
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
# ─── Entry ───────────────────────────────────────────────────────────────────
|
|
664
|
+
|
|
665
|
+
# ASCII castle banner with blue-to-purple gradient (ANSI 256-color)
|
|
666
|
+
print_banner() {
|
|
667
|
+
local lines=(
|
|
668
|
+
' |>>>'
|
|
669
|
+
' |'
|
|
670
|
+
' |>>> _ _|_ _ |>>>'
|
|
671
|
+
' | |;| |;| |;| |'
|
|
672
|
+
' _ _|_ _ \. . / _ _|_ _'
|
|
673
|
+
' |;|_|;|_|;| \:. , / |;|_|;|_|;|'
|
|
674
|
+
' \.. / ||; . | \. . /'
|
|
675
|
+
' \. , / ||: . | \: . /'
|
|
676
|
+
' ||: |_ _ ||_ . _ | _ _||: |'
|
|
677
|
+
' ||: .|||_|;|_|;|_|;|_|;|_|;||:. |'
|
|
678
|
+
' ||: ||. . . . ||: .|'
|
|
679
|
+
' ||: . || . . . . , ||: | \,/'
|
|
680
|
+
' ||: ||: , _______ . ||: , | /`\\'
|
|
681
|
+
' ||: || . /+++++++\ . ||: |'
|
|
682
|
+
' ||: ||. |+++++++| . ||: . |'
|
|
683
|
+
' __ ||: . ||: , |+++++++|. . _||_ |'
|
|
684
|
+
' ____--`~ '"'"'--~~__|. |+++++__|----~ ~`---, ___'
|
|
685
|
+
'-~--~ ~---__|,--~'"'"' ~~----_____-~'"'"' `~----~~'
|
|
686
|
+
)
|
|
687
|
+
# Blue-to-purple gradient using ANSI 256-color codes
|
|
688
|
+
local gradient=(27 27 33 33 63 63 99 99 135 135 141 141 177 177 177 176 176 176)
|
|
689
|
+
local i=0
|
|
690
|
+
echo ""
|
|
691
|
+
for line in "${lines[@]}"; do
|
|
692
|
+
local color=${gradient[$i]}
|
|
693
|
+
echo -e "\033[38;5;${color}m${line}\033[0m"
|
|
694
|
+
((i++)) || true
|
|
695
|
+
done
|
|
696
|
+
echo ""
|
|
697
|
+
echo -e " ${ACCENT}${BOLD}Castle${NC} ${MUTED}— The multi-agent workspace${NC}"
|
|
698
|
+
echo -e " ${MUTED}${TAGLINE}${NC}"
|
|
699
|
+
echo ""
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
print_banner
|
|
703
|
+
|
|
704
|
+
# Detect OS
|
|
705
|
+
OS="unknown"
|
|
706
|
+
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
707
|
+
OS="macos"
|
|
708
|
+
elif [[ "$OSTYPE" == "linux-gnu"* ]] || [[ -n "${WSL_DISTRO_NAME:-}" ]]; then
|
|
709
|
+
OS="linux"
|
|
710
|
+
fi
|
|
711
|
+
|
|
712
|
+
if [[ "$OS" == "unknown" ]]; then
|
|
713
|
+
echo -e "${ERROR}Error: Unsupported operating system${NC}"
|
|
714
|
+
echo "This installer supports macOS and Linux (including WSL)."
|
|
715
|
+
exit 1
|
|
716
|
+
fi
|
|
717
|
+
|
|
718
|
+
echo -e "${SUCCESS}✓${NC} Detected: $OS"
|
|
719
|
+
|
|
720
|
+
parse_args "$@"
|
|
721
|
+
configure_verbose
|
|
722
|
+
main
|