@hybroai/cli 0.3.0 → 0.4.1
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/bin/hybro +722 -0
- package/package.json +4 -12
- package/bin/hybro.js +0 -69
package/bin/hybro
ADDED
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Hybro command line: runs the single Hybro image with Docker.
|
|
3
|
+
#
|
|
4
|
+
# One POSIX sh file serves as the checkout entry point (dev mode), the npm
|
|
5
|
+
# package's bin, and the release asset that install.sh installs (release mode).
|
|
6
|
+
set -u
|
|
7
|
+
|
|
8
|
+
HYBRO_VERSION="0.4.1" # x-release-please-version
|
|
9
|
+
|
|
10
|
+
GITHUB_REPO="hybroai/hybro"
|
|
11
|
+
RELEASE_IMAGE="ghcr.io/hybroai/hybro"
|
|
12
|
+
LATEST_API="https://api.github.com/repos/$GITHUB_REPO/releases/latest"
|
|
13
|
+
DOWNLOAD_BASE="https://github.com/$GITHUB_REPO/releases/download"
|
|
14
|
+
INSTALL_HINT="curl -fsSL https://hybro.ai/install.sh | sh"
|
|
15
|
+
MARKER="/data/hybro-volume.json"
|
|
16
|
+
|
|
17
|
+
say() { printf '%s\n' "$*"; }
|
|
18
|
+
err() { printf 'hybro: %s\n' "$*" >&2; }
|
|
19
|
+
die() {
|
|
20
|
+
err "$*"
|
|
21
|
+
exit 1
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# ------------------------------------------------------------------ location
|
|
25
|
+
|
|
26
|
+
# Resolve this file through symlinks (the npm bin is one) without readlink -f.
|
|
27
|
+
resolve_self() {
|
|
28
|
+
case $0 in
|
|
29
|
+
*/*) p=$0 ;;
|
|
30
|
+
*) p=$(command -v -- "$0" 2>/dev/null) || p=./$0 ;;
|
|
31
|
+
esac
|
|
32
|
+
while [ -L "$p" ]; do
|
|
33
|
+
target=$(readlink "$p")
|
|
34
|
+
case $target in
|
|
35
|
+
/*) p=$target ;;
|
|
36
|
+
*) p=${p%/*}/$target ;;
|
|
37
|
+
esac
|
|
38
|
+
done
|
|
39
|
+
dir=$(cd -P -- "${p%/*}" && pwd) || die "cannot resolve the location of $0"
|
|
40
|
+
SELF=$dir/${p##*/}
|
|
41
|
+
SCRIPT_DIR=$dir
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
resolve_self
|
|
45
|
+
ROOT=${SCRIPT_DIR%/*}
|
|
46
|
+
if [ -d "$ROOT/backend" ] && [ -f "$ROOT/Dockerfile" ]; then
|
|
47
|
+
MODE=dev
|
|
48
|
+
CLI=./scripts/hybro
|
|
49
|
+
IMAGE=hybro:dev
|
|
50
|
+
CONTAINER=hybro-dev
|
|
51
|
+
VOLUME=hybro-dev-data
|
|
52
|
+
DEFAULT_PORT=3100
|
|
53
|
+
SOURCE_PROJECT=hybro-dev
|
|
54
|
+
INSTANCE=dev
|
|
55
|
+
OLD_IMAGES='^hybro-dev-[a-z0-9_-]*:'
|
|
56
|
+
else
|
|
57
|
+
MODE=release
|
|
58
|
+
CLI=hybro
|
|
59
|
+
IMAGE=$RELEASE_IMAGE:$HYBRO_VERSION
|
|
60
|
+
CONTAINER=hybro
|
|
61
|
+
VOLUME=hybro-data
|
|
62
|
+
DEFAULT_PORT=3000
|
|
63
|
+
SOURCE_PROJECT=hybro
|
|
64
|
+
INSTANCE=release
|
|
65
|
+
OLD_IMAGES='^ghcr\.io/hybroai/hybro-[a-z0-9-]*:'
|
|
66
|
+
fi
|
|
67
|
+
case $SELF in
|
|
68
|
+
*/node_modules/*) CHANNEL=npm ;;
|
|
69
|
+
*) CHANNEL=script ;;
|
|
70
|
+
esac
|
|
71
|
+
|
|
72
|
+
# ------------------------------------------------------------------- helpers
|
|
73
|
+
|
|
74
|
+
usage() {
|
|
75
|
+
cat <<EOF
|
|
76
|
+
Hybro $HYBRO_VERSION: AI agents and rooms in one container.
|
|
77
|
+
|
|
78
|
+
Usage: $CLI [command] [options]
|
|
79
|
+
|
|
80
|
+
Commands:
|
|
81
|
+
start Start Hybro and follow its logs (the default command)
|
|
82
|
+
stop Stop Hybro
|
|
83
|
+
status Show whether Hybro is running and its address
|
|
84
|
+
logs Show Hybro's logs; docker logs options such as -f pass through
|
|
85
|
+
upgrade Install the latest release, update the image, migrate 0.3 data
|
|
86
|
+
cleanup-old Remove the Hybro 0.3 containers, volumes, and images
|
|
87
|
+
version Print the version
|
|
88
|
+
|
|
89
|
+
Start options:
|
|
90
|
+
-d, --detach Run in the background
|
|
91
|
+
--port <port> Host port (default $DEFAULT_PORT)
|
|
92
|
+
--listen <address> Address to listen on (default 127.0.0.1); any other
|
|
93
|
+
address needs sign-in or --insecure-no-auth
|
|
94
|
+
--insecure-no-auth Allow a non-loopback listener without sign-in
|
|
95
|
+
--fresh Start with an empty volume even when 0.3 data exists
|
|
96
|
+
--build Rebuild the image (checkout only)
|
|
97
|
+
--recreate Recreate a running container (checkout only)
|
|
98
|
+
|
|
99
|
+
Upgrade options:
|
|
100
|
+
--check Only report whether a newer release exists
|
|
101
|
+
|
|
102
|
+
cleanup-old options:
|
|
103
|
+
-y, --yes Remove without asking
|
|
104
|
+
|
|
105
|
+
Environment:
|
|
106
|
+
HYBRO_HOME Configuration directory (default ~/.hybro)
|
|
107
|
+
HYBRO_NO_UPDATE_CHECK=1 Turn off the daily update notice
|
|
108
|
+
EOF
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
require_docker() {
|
|
112
|
+
command -v docker >/dev/null 2>&1 ||
|
|
113
|
+
die "Docker is not installed. Install it from https://docs.docker.com/get-docker/"
|
|
114
|
+
docker info >/dev/null 2>&1 ||
|
|
115
|
+
die "Docker is not running. Start Docker, then run $CLI again."
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
prepare_home() {
|
|
119
|
+
HYBRO_HOME=${HYBRO_HOME:-${HOME:?HOME is not set}/.hybro}
|
|
120
|
+
case $HYBRO_HOME in
|
|
121
|
+
/*) ;;
|
|
122
|
+
*) die "HYBRO_HOME must be an absolute path: $HYBRO_HOME" ;;
|
|
123
|
+
esac
|
|
124
|
+
# Created here, so Docker never creates the mount source as root.
|
|
125
|
+
[ -d "$HYBRO_HOME" ] || (umask 077 && mkdir -p -- "$HYBRO_HOME") ||
|
|
126
|
+
die "cannot create $HYBRO_HOME"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
container_state() {
|
|
130
|
+
docker container inspect -f '{{.State.Status}}' "$CONTAINER" 2>/dev/null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
volume_exists() {
|
|
134
|
+
docker volume inspect "$1" >/dev/null 2>&1
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
is_version() {
|
|
138
|
+
printf '%s\n' "$1" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# version_gt A B: A is a newer X.Y.Z than B.
|
|
142
|
+
version_gt() {
|
|
143
|
+
awk -v a="$1" -v b="$2" 'BEGIN {
|
|
144
|
+
split(a, x, "."); split(b, y, ".")
|
|
145
|
+
for (i = 1; i <= 3; i++) {
|
|
146
|
+
if (x[i] + 0 > y[i] + 0) exit 0
|
|
147
|
+
if (x[i] + 0 < y[i] + 0) exit 1
|
|
148
|
+
}
|
|
149
|
+
exit 1
|
|
150
|
+
}'
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# latest_version <timeout seconds>: the latest release's X.Y.Z, or failure.
|
|
154
|
+
latest_version() {
|
|
155
|
+
command -v curl >/dev/null 2>&1 || return 1
|
|
156
|
+
tag=$(curl -fsSL --max-time "$1" -H 'Accept: application/vnd.github+json' \
|
|
157
|
+
"$LATEST_API" 2>/dev/null |
|
|
158
|
+
sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"v\{0,1\}\([^"]*\)".*/\1/p' |
|
|
159
|
+
head -n 1)
|
|
160
|
+
is_version "$tag" || return 1
|
|
161
|
+
printf '%s\n' "$tag"
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# url_for <host ip> <port>, the ip as Docker prints it.
|
|
165
|
+
url_for() {
|
|
166
|
+
case $1 in
|
|
167
|
+
127.0.0.1 | '[::1]') say "http://localhost:$2" ;;
|
|
168
|
+
0.0.0.0 | '[::]' | '') say "http://<this machine's address>:$2" ;;
|
|
169
|
+
*) say "http://$1:$2" ;;
|
|
170
|
+
esac
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
running_url() {
|
|
174
|
+
binding=$(docker port "$CONTAINER" 8080/tcp 2>/dev/null | head -n 1)
|
|
175
|
+
url_for "${binding%:*}" "${binding##*:}"
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# ------------------------------------------------------------------- image
|
|
179
|
+
|
|
180
|
+
IMAGE_READY=0
|
|
181
|
+
BUILD=0
|
|
182
|
+
|
|
183
|
+
ensure_image() {
|
|
184
|
+
[ "$IMAGE_READY" = 1 ] && return 0
|
|
185
|
+
if [ "$MODE" = dev ]; then
|
|
186
|
+
if [ "$BUILD" = 1 ] || ! docker image inspect "$IMAGE" >/dev/null 2>&1; then
|
|
187
|
+
say "Building $IMAGE from $ROOT..."
|
|
188
|
+
docker build -t "$IMAGE" "$ROOT" || die "the image build failed"
|
|
189
|
+
fi
|
|
190
|
+
elif ! docker image inspect "$IMAGE" >/dev/null 2>&1; then
|
|
191
|
+
say "Pulling $IMAGE..."
|
|
192
|
+
docker pull "$IMAGE" || die "cannot pull $IMAGE"
|
|
193
|
+
fi
|
|
194
|
+
IMAGE_READY=1
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# ------------------------------------------------------------ 0.3 detection
|
|
198
|
+
|
|
199
|
+
find_sources() {
|
|
200
|
+
SOURCE_VOLUMES=
|
|
201
|
+
for suffix in mongo_data room_files redis_data; do
|
|
202
|
+
if volume_exists "${SOURCE_PROJECT}_$suffix"; then
|
|
203
|
+
SOURCE_VOLUMES="$SOURCE_VOLUMES ${SOURCE_PROJECT}_$suffix"
|
|
204
|
+
fi
|
|
205
|
+
done
|
|
206
|
+
SOURCE_CONTAINERS=$(docker ps -a --format '{{.Names}}' \
|
|
207
|
+
--filter "label=com.docker.compose.project=$SOURCE_PROJECT" 2>/dev/null)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# Whether the target volume has the completion marker; a Docker failure is
|
|
211
|
+
# not a missing marker, so it never leads to removing the volume.
|
|
212
|
+
marker_present() {
|
|
213
|
+
docker run --rm --entrypoint test -v "$VOLUME:/data" "$IMAGE" -f "$MARKER"
|
|
214
|
+
status=$?
|
|
215
|
+
case $status in
|
|
216
|
+
0) return 0 ;;
|
|
217
|
+
1) return 1 ;;
|
|
218
|
+
*) die "cannot read the completion marker of volume $VOLUME (docker exit $status)" ;;
|
|
219
|
+
esac
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# Sets LAYOUT to ok, old (0.3 data, no target volume), or incomplete (a target
|
|
223
|
+
# volume without the marker while 0.3 data exists).
|
|
224
|
+
detect_layout() {
|
|
225
|
+
find_sources
|
|
226
|
+
LAYOUT=ok
|
|
227
|
+
if volume_exists "$VOLUME"; then
|
|
228
|
+
# Without 0.3 data, a volume without a marker is a fresh one that the
|
|
229
|
+
# entrypoint completes.
|
|
230
|
+
[ -n "$SOURCE_VOLUMES" ] || return 0
|
|
231
|
+
ensure_image
|
|
232
|
+
marker_present || LAYOUT=incomplete
|
|
233
|
+
elif [ -n "$SOURCE_VOLUMES$SOURCE_CONTAINERS" ]; then
|
|
234
|
+
LAYOUT=old
|
|
235
|
+
fi
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
list_sources() {
|
|
239
|
+
for item in $SOURCE_VOLUMES; do err " volume $item"; done
|
|
240
|
+
for item in $SOURCE_CONTAINERS; do err " container $item"; done
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
layout_notice() {
|
|
244
|
+
if [ "$LAYOUT" = old ]; then
|
|
245
|
+
err "found data from Hybro 0.3 (Compose project $SOURCE_PROJECT):"
|
|
246
|
+
else
|
|
247
|
+
err "volume $VOLUME is from a migration that did not finish; the 0.3 data is still there:"
|
|
248
|
+
fi
|
|
249
|
+
list_sources
|
|
250
|
+
cat >&2 <<EOF
|
|
251
|
+
Nothing was started. Choose one:
|
|
252
|
+
$CLI upgrade copy the 0.3 data into the new layout (the old volumes are kept)
|
|
253
|
+
$CLI --fresh start with an empty volume instead
|
|
254
|
+
EOF
|
|
255
|
+
exit 1
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
# ------------------------------------------------------------------- running
|
|
259
|
+
|
|
260
|
+
PORT=
|
|
261
|
+
LISTEN=127.0.0.1
|
|
262
|
+
INSECURE=0
|
|
263
|
+
DETACH=0
|
|
264
|
+
FRESH=0
|
|
265
|
+
RECREATE=0
|
|
266
|
+
BUILD_FLAG=0
|
|
267
|
+
|
|
268
|
+
# Sets LISTEN_MODE and BIND (the docker -p host address) from LISTEN.
|
|
269
|
+
apply_listen() {
|
|
270
|
+
case $LISTEN in
|
|
271
|
+
127.0.0.1 | localhost)
|
|
272
|
+
LISTEN_MODE=loopback
|
|
273
|
+
BIND=127.0.0.1
|
|
274
|
+
;;
|
|
275
|
+
::1)
|
|
276
|
+
LISTEN_MODE=loopback
|
|
277
|
+
BIND='[::1]'
|
|
278
|
+
;;
|
|
279
|
+
'' | *[!0-9A-Fa-f.:]*) die "--listen needs an IP address, for example 0.0.0.0: $LISTEN" ;;
|
|
280
|
+
*:*)
|
|
281
|
+
LISTEN_MODE=public
|
|
282
|
+
BIND="[$LISTEN]"
|
|
283
|
+
;;
|
|
284
|
+
*)
|
|
285
|
+
LISTEN_MODE=public
|
|
286
|
+
BIND=$LISTEN
|
|
287
|
+
;;
|
|
288
|
+
esac
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
check_port() {
|
|
292
|
+
case $1 in
|
|
293
|
+
'' | 0* | *[!0-9]*) die "--port needs a number from 1 to 65535: $1" ;;
|
|
294
|
+
esac
|
|
295
|
+
if [ "${#1}" -gt 5 ] || [ "$1" -gt 65535 ]; then
|
|
296
|
+
die "--port needs a number from 1 to 65535: $1"
|
|
297
|
+
fi
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
# run_container <publish the OAuth port: 1 or 0>
|
|
301
|
+
run_container() {
|
|
302
|
+
oauth=$1
|
|
303
|
+
set -- run -d --name "$CONTAINER" -p "$BIND:$PORT:8080" \
|
|
304
|
+
-e "HYBRO_LISTEN=$LISTEN_MODE" -e "HYBRO_PUBLIC_PORT=$PORT"
|
|
305
|
+
[ "$INSECURE" = 1 ] && set -- "$@" -e HYBRO_INSECURE_NO_AUTH=1
|
|
306
|
+
[ "$oauth" = 1 ] && set -- "$@" -p 127.0.0.1:1455:1455
|
|
307
|
+
set -- "$@" -v "$VOLUME:/data" -v "$HYBRO_HOME:/var/lib/hybro/runtime" \
|
|
308
|
+
--add-host host.docker.internal:host-gateway --stop-timeout 60 "$IMAGE"
|
|
309
|
+
docker "$@"
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
start_container() {
|
|
313
|
+
# A stopped container with the same name only holds a stale configuration;
|
|
314
|
+
# the data lives in the volume.
|
|
315
|
+
if [ -n "$(container_state)" ]; then
|
|
316
|
+
docker rm -f "$CONTAINER" >/dev/null || die "cannot remove the old $CONTAINER container"
|
|
317
|
+
fi
|
|
318
|
+
if output=$(run_container 1 2>&1 >/dev/null); then
|
|
319
|
+
return 0
|
|
320
|
+
fi
|
|
321
|
+
case $output in
|
|
322
|
+
*1455*)
|
|
323
|
+
docker rm -f "$CONTAINER" >/dev/null 2>&1
|
|
324
|
+
say "Port 1455 is in use, so ChatGPT login will use the paste flow."
|
|
325
|
+
if output=$(run_container 0 2>&1 >/dev/null); then
|
|
326
|
+
return 0
|
|
327
|
+
fi
|
|
328
|
+
;;
|
|
329
|
+
esac
|
|
330
|
+
docker rm -f "$CONTAINER" >/dev/null 2>&1
|
|
331
|
+
printf '%s\n' "$output" >&2
|
|
332
|
+
die "cannot start the $CONTAINER container"
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
follow_logs() {
|
|
336
|
+
say "Following the logs; press Ctrl-C to stop Hybro."
|
|
337
|
+
INTERRUPTED=0
|
|
338
|
+
trap 'INTERRUPTED=1' INT
|
|
339
|
+
docker logs -f "$CONTAINER"
|
|
340
|
+
trap - INT
|
|
341
|
+
if [ "$INTERRUPTED" = 1 ]; then
|
|
342
|
+
say "Stopping Hybro..."
|
|
343
|
+
docker stop "$CONTAINER" >/dev/null || die "cannot stop $CONTAINER"
|
|
344
|
+
exit 0
|
|
345
|
+
fi
|
|
346
|
+
code=$(docker container inspect -f '{{.State.ExitCode}}' "$CONTAINER" 2>/dev/null)
|
|
347
|
+
die "Hybro stopped (exit code ${code:-unknown}). Show the logs with: $CLI logs"
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
update_notice() {
|
|
351
|
+
[ "$MODE" = release ] && [ "${HYBRO_NO_UPDATE_CHECK:-}" != 1 ] || return 0
|
|
352
|
+
cache=$HYBRO_HOME/update-check
|
|
353
|
+
latest=
|
|
354
|
+
if [ -f "$cache" ] && [ -n "$(find "$cache" -mtime -1 2>/dev/null)" ]; then
|
|
355
|
+
read -r latest <"$cache" 2>/dev/null
|
|
356
|
+
else
|
|
357
|
+
latest=$(latest_version 3) || latest=
|
|
358
|
+
printf '%s\n' "$latest" >"$cache" 2>/dev/null
|
|
359
|
+
fi
|
|
360
|
+
if is_version "$latest" && version_gt "$latest" "$HYBRO_VERSION"; then
|
|
361
|
+
say "Hybro $latest is available (you have $HYBRO_VERSION). Upgrade with: $CLI upgrade"
|
|
362
|
+
fi
|
|
363
|
+
return 0
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
# ------------------------------------------------------------------ commands
|
|
367
|
+
|
|
368
|
+
cmd_start() {
|
|
369
|
+
# The 0.3 CLI prints `hybro start --recreate` after an npm upgrade, so the
|
|
370
|
+
# layout check runs before the flags are validated.
|
|
371
|
+
for arg in "$@"; do
|
|
372
|
+
case $arg in
|
|
373
|
+
--fresh) FRESH=1 ;;
|
|
374
|
+
--build) [ "$MODE" = dev ] && BUILD=1 ;;
|
|
375
|
+
esac
|
|
376
|
+
done
|
|
377
|
+
require_docker
|
|
378
|
+
prepare_home
|
|
379
|
+
detect_layout
|
|
380
|
+
[ "$LAYOUT" = ok ] || [ "$FRESH" = 1 ] || layout_notice
|
|
381
|
+
|
|
382
|
+
while [ $# -gt 0 ]; do
|
|
383
|
+
case $1 in
|
|
384
|
+
-d | --detach) DETACH=1 ;;
|
|
385
|
+
--port)
|
|
386
|
+
[ $# -ge 2 ] || die "--port needs a value"
|
|
387
|
+
PORT=$2
|
|
388
|
+
shift
|
|
389
|
+
;;
|
|
390
|
+
--port=*) PORT=${1#*=} ;;
|
|
391
|
+
--listen)
|
|
392
|
+
[ $# -ge 2 ] || die "--listen needs a value"
|
|
393
|
+
LISTEN=$2
|
|
394
|
+
shift
|
|
395
|
+
;;
|
|
396
|
+
--listen=*) LISTEN=${1#*=} ;;
|
|
397
|
+
--insecure-no-auth) INSECURE=1 ;;
|
|
398
|
+
--fresh) ;;
|
|
399
|
+
--build) BUILD_FLAG=1 ;;
|
|
400
|
+
--recreate) RECREATE=1 ;;
|
|
401
|
+
-h | --help)
|
|
402
|
+
usage
|
|
403
|
+
exit 0
|
|
404
|
+
;;
|
|
405
|
+
*)
|
|
406
|
+
err "unknown option: $1"
|
|
407
|
+
usage >&2
|
|
408
|
+
exit 2
|
|
409
|
+
;;
|
|
410
|
+
esac
|
|
411
|
+
shift
|
|
412
|
+
done
|
|
413
|
+
PORT=${PORT:-$DEFAULT_PORT}
|
|
414
|
+
check_port "$PORT"
|
|
415
|
+
apply_listen
|
|
416
|
+
# Only after every flag is valid; the volume never finished its first start.
|
|
417
|
+
if [ "$LAYOUT" = incomplete ]; then
|
|
418
|
+
say "Removing volume $VOLUME, left by a migration that did not finish."
|
|
419
|
+
docker volume rm "$VOLUME" >/dev/null || die "cannot remove volume $VOLUME"
|
|
420
|
+
fi
|
|
421
|
+
if [ "$MODE" = release ]; then
|
|
422
|
+
[ "$BUILD_FLAG" = 1 ] && say "Note: --build applies to a checkout only; a release install pulls its image."
|
|
423
|
+
[ "$RECREATE" = 1 ] && say "Note: --recreate applies to a checkout only; every release start uses a new container."
|
|
424
|
+
RECREATE=0
|
|
425
|
+
fi
|
|
426
|
+
|
|
427
|
+
if [ "$(container_state)" = running ]; then
|
|
428
|
+
if [ "$RECREATE" = 1 ]; then
|
|
429
|
+
docker stop "$CONTAINER" >/dev/null || die "cannot stop $CONTAINER"
|
|
430
|
+
else
|
|
431
|
+
say "Hybro is already running at $(running_url)"
|
|
432
|
+
say "To change its options, run $CLI stop first."
|
|
433
|
+
return 0
|
|
434
|
+
fi
|
|
435
|
+
fi
|
|
436
|
+
update_notice
|
|
437
|
+
ensure_image
|
|
438
|
+
start_container
|
|
439
|
+
url=$(url_for "$BIND" "$PORT")
|
|
440
|
+
say "Hybro is starting at $url"
|
|
441
|
+
if [ "$DETACH" = 1 ]; then
|
|
442
|
+
say "Running in the background. Stop it with: $CLI stop"
|
|
443
|
+
return 0
|
|
444
|
+
fi
|
|
445
|
+
follow_logs
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
cmd_stop() {
|
|
449
|
+
[ $# -eq 0 ] || die "stop takes no options"
|
|
450
|
+
require_docker
|
|
451
|
+
if [ "$(container_state)" = running ]; then
|
|
452
|
+
docker stop "$CONTAINER" >/dev/null || die "cannot stop $CONTAINER"
|
|
453
|
+
say "Hybro stopped. Your data is kept in volume $VOLUME."
|
|
454
|
+
else
|
|
455
|
+
say "Hybro is not running."
|
|
456
|
+
fi
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
cmd_status() {
|
|
460
|
+
[ $# -eq 0 ] || die "status takes no options"
|
|
461
|
+
require_docker
|
|
462
|
+
state=$(container_state)
|
|
463
|
+
case $state in
|
|
464
|
+
running)
|
|
465
|
+
say "Hybro is running at $(running_url)"
|
|
466
|
+
say "Container $CONTAINER, image $(docker container inspect -f '{{.Config.Image}}' "$CONTAINER"), volume $VOLUME"
|
|
467
|
+
;;
|
|
468
|
+
'')
|
|
469
|
+
say "Hybro is not running. Start it with: $CLI"
|
|
470
|
+
return 1
|
|
471
|
+
;;
|
|
472
|
+
*)
|
|
473
|
+
say "Hybro is not running (container $CONTAINER is $state). Start it with: $CLI"
|
|
474
|
+
return 1
|
|
475
|
+
;;
|
|
476
|
+
esac
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
cmd_logs() {
|
|
480
|
+
require_docker
|
|
481
|
+
[ -n "$(container_state)" ] || die "there is no $CONTAINER container. Start Hybro with: $CLI"
|
|
482
|
+
exec docker logs "$@" "$CONTAINER"
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
sha256_of() {
|
|
486
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
487
|
+
sha256sum "$1" | awk '{ print $1 }'
|
|
488
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
489
|
+
shasum -a 256 "$1" | awk '{ print $1 }'
|
|
490
|
+
else
|
|
491
|
+
die "neither sha256sum nor shasum is installed"
|
|
492
|
+
fi
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
# install_version <X.Y.Z>: replace this script through its install channel.
|
|
496
|
+
install_version() {
|
|
497
|
+
if [ "$CHANNEL" = npm ]; then
|
|
498
|
+
command -v npm >/dev/null 2>&1 || die "npm is not on PATH; it installed this hybro"
|
|
499
|
+
say "Installing @hybroai/cli@$1 with npm..."
|
|
500
|
+
npm install -g "@hybroai/cli@$1" || die "npm install failed; nothing else was changed"
|
|
501
|
+
return 0
|
|
502
|
+
fi
|
|
503
|
+
[ -w "$SCRIPT_DIR" ] || die "cannot write to $SCRIPT_DIR. Reinstall with: $INSTALL_HINT"
|
|
504
|
+
# A temporary file in the same directory, so the final mv is atomic.
|
|
505
|
+
new=$SCRIPT_DIR/.hybro-upgrade.$$
|
|
506
|
+
sums=$new.sums
|
|
507
|
+
say "Downloading hybro $1..."
|
|
508
|
+
if ! curl -fsSL --max-time 60 -o "$new" "$DOWNLOAD_BASE/v$1/hybro" ||
|
|
509
|
+
! curl -fsSL --max-time 60 -o "$sums" "$DOWNLOAD_BASE/v$1/SHA256SUMS"; then
|
|
510
|
+
rm -f "$new" "$sums"
|
|
511
|
+
die "the download failed; nothing was changed"
|
|
512
|
+
fi
|
|
513
|
+
expected=$(awk '$2 == "hybro" || $2 == "*hybro" { print $1 }' "$sums")
|
|
514
|
+
actual=$(sha256_of "$new")
|
|
515
|
+
rm -f "$sums"
|
|
516
|
+
if [ -z "$expected" ] || [ "$expected" != "$actual" ]; then
|
|
517
|
+
rm -f "$new"
|
|
518
|
+
die "the downloaded script does not match SHA256SUMS; nothing was changed"
|
|
519
|
+
fi
|
|
520
|
+
if ! chmod 755 "$new" || ! mv -f "$new" "$SELF"; then
|
|
521
|
+
rm -f "$new"
|
|
522
|
+
die "cannot replace $SELF"
|
|
523
|
+
fi
|
|
524
|
+
say "Installed hybro $1 at $SELF."
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
# Copy 0.3 data into the target volume (plan 4.11); the sources stay read-only.
|
|
528
|
+
migrate() {
|
|
529
|
+
say "Migrating Hybro 0.3 data (Compose project $SOURCE_PROJECT) into volume $VOLUME."
|
|
530
|
+
ensure_image
|
|
531
|
+
if [ -n "$SOURCE_CONTAINERS" ]; then
|
|
532
|
+
say "Stopping the 0.3 containers..."
|
|
533
|
+
# shellcheck disable=SC2086 # one container name per word
|
|
534
|
+
docker stop $SOURCE_CONTAINERS >/dev/null ||
|
|
535
|
+
die "cannot stop the 0.3 containers; stop them, then run $CLI upgrade again"
|
|
536
|
+
# shellcheck disable=SC2086
|
|
537
|
+
docker rm $SOURCE_CONTAINERS >/dev/null ||
|
|
538
|
+
die "cannot remove the 0.3 containers; remove them, then run $CLI upgrade again"
|
|
539
|
+
fi
|
|
540
|
+
if [ "$LAYOUT" = incomplete ]; then
|
|
541
|
+
docker volume rm "$VOLUME" >/dev/null ||
|
|
542
|
+
die "cannot remove volume $VOLUME, left by a migration that did not finish"
|
|
543
|
+
fi
|
|
544
|
+
docker volume create "$VOLUME" >/dev/null || die "cannot create volume $VOLUME"
|
|
545
|
+
set -- run --rm --entrypoint /app/backend/.venv/bin/python
|
|
546
|
+
if volume_exists "${SOURCE_PROJECT}_mongo_data"; then
|
|
547
|
+
set -- "$@" -v "${SOURCE_PROJECT}_mongo_data:/source/db:ro"
|
|
548
|
+
fi
|
|
549
|
+
if volume_exists "${SOURCE_PROJECT}_room_files"; then
|
|
550
|
+
set -- "$@" -v "${SOURCE_PROJECT}_room_files:/source/files:ro"
|
|
551
|
+
fi
|
|
552
|
+
set -- "$@" -v "$VOLUME:/data" -v "$HYBRO_HOME:/var/lib/hybro/runtime:ro" \
|
|
553
|
+
"$IMAGE" /app/backend/volume_migration.py "$INSTANCE"
|
|
554
|
+
if ! result=$(docker "$@"); then
|
|
555
|
+
docker volume rm "$VOLUME" >/dev/null 2>&1
|
|
556
|
+
die "the migration failed; nothing was started and the 0.3 volumes are unchanged. Fix the problem above, then run $CLI upgrade again"
|
|
557
|
+
fi
|
|
558
|
+
OLD_PORT=
|
|
559
|
+
case $result in
|
|
560
|
+
port=*) OLD_PORT=${result#port=} ;;
|
|
561
|
+
esac
|
|
562
|
+
case $OLD_PORT in
|
|
563
|
+
*[!0-9]*) OLD_PORT= ;;
|
|
564
|
+
esac
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
# After an image update, run a running container again with the same address.
|
|
568
|
+
recreate_if_running() {
|
|
569
|
+
[ "$(container_state)" = running ] || return 0
|
|
570
|
+
current=$(docker container inspect -f '{{.Config.Image}}' "$CONTAINER")
|
|
571
|
+
[ "$current" != "$IMAGE" ] || return 0
|
|
572
|
+
binding=$(docker port "$CONTAINER" 8080/tcp | head -n 1)
|
|
573
|
+
PORT=${binding##*:}
|
|
574
|
+
BIND=${binding%:*}
|
|
575
|
+
environment=$(docker container inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$CONTAINER")
|
|
576
|
+
LISTEN_MODE=public
|
|
577
|
+
INSECURE=0
|
|
578
|
+
printf '%s\n' "$environment" | grep -qx 'HYBRO_LISTEN=loopback' && LISTEN_MODE=loopback
|
|
579
|
+
printf '%s\n' "$environment" | grep -qx 'HYBRO_INSECURE_NO_AUTH=1' && INSECURE=1
|
|
580
|
+
say "Restarting Hybro on $IMAGE..."
|
|
581
|
+
docker stop "$CONTAINER" >/dev/null || die "cannot stop $CONTAINER"
|
|
582
|
+
start_container
|
|
583
|
+
say "Hybro $HYBRO_VERSION is running at $(url_for "$BIND" "$PORT")"
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
cmd_upgrade() {
|
|
587
|
+
step=
|
|
588
|
+
for arg in "$@"; do
|
|
589
|
+
case $arg in
|
|
590
|
+
--check) step="check" ;;
|
|
591
|
+
--continue) step="continue" ;;
|
|
592
|
+
-h | --help)
|
|
593
|
+
usage
|
|
594
|
+
exit 0
|
|
595
|
+
;;
|
|
596
|
+
*) die "unknown upgrade option: $arg" ;;
|
|
597
|
+
esac
|
|
598
|
+
done
|
|
599
|
+
if [ "$MODE" = dev ]; then
|
|
600
|
+
if [ "$step" = check ]; then
|
|
601
|
+
say "This is a checkout; update it with git pull."
|
|
602
|
+
return 0
|
|
603
|
+
fi
|
|
604
|
+
say "This is a checkout: upgrade migrates 0.3 data only. Update the code with git pull."
|
|
605
|
+
elif [ "$step" != continue ]; then
|
|
606
|
+
latest=$(latest_version 10) || die "cannot read the latest release from GitHub"
|
|
607
|
+
if version_gt "$latest" "$HYBRO_VERSION"; then
|
|
608
|
+
if [ "$step" = check ]; then
|
|
609
|
+
say "Hybro $latest is available (you have $HYBRO_VERSION). Upgrade with: $CLI upgrade"
|
|
610
|
+
return 1
|
|
611
|
+
fi
|
|
612
|
+
install_version "$latest"
|
|
613
|
+
# The rest runs with the new version's image and migration.
|
|
614
|
+
exec "$SELF" upgrade --continue
|
|
615
|
+
fi
|
|
616
|
+
if [ "$step" = check ]; then
|
|
617
|
+
say "Hybro $HYBRO_VERSION is the latest release."
|
|
618
|
+
return 0
|
|
619
|
+
fi
|
|
620
|
+
fi
|
|
621
|
+
|
|
622
|
+
require_docker
|
|
623
|
+
prepare_home
|
|
624
|
+
if [ "$MODE" = release ]; then
|
|
625
|
+
say "Pulling $IMAGE..."
|
|
626
|
+
docker pull "$IMAGE" || die "cannot pull $IMAGE"
|
|
627
|
+
IMAGE_READY=1
|
|
628
|
+
fi
|
|
629
|
+
detect_layout
|
|
630
|
+
if [ "$LAYOUT" != ok ]; then
|
|
631
|
+
migrate
|
|
632
|
+
PORT=$DEFAULT_PORT
|
|
633
|
+
LISTEN=127.0.0.1
|
|
634
|
+
apply_listen
|
|
635
|
+
start_container
|
|
636
|
+
say "Migration complete. Hybro is starting at $(url_for "$BIND" "$PORT")"
|
|
637
|
+
say "The 0.3 volumes are kept:$SOURCE_VOLUMES"
|
|
638
|
+
say "When you no longer need them, remove them with: $CLI cleanup-old"
|
|
639
|
+
if [ -n "$OLD_PORT" ] && [ "$OLD_PORT" != "$DEFAULT_PORT" ]; then
|
|
640
|
+
say "Your 0.3 install used port $OLD_PORT. To keep it: $CLI stop && $CLI start --port $OLD_PORT"
|
|
641
|
+
fi
|
|
642
|
+
return 0
|
|
643
|
+
fi
|
|
644
|
+
[ "$MODE" = release ] && recreate_if_running
|
|
645
|
+
say "Hybro $HYBRO_VERSION is up to date."
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
cmd_cleanup_old() {
|
|
649
|
+
yes=0
|
|
650
|
+
for arg in "$@"; do
|
|
651
|
+
case $arg in
|
|
652
|
+
-y | --yes) yes=1 ;;
|
|
653
|
+
*) die "unknown cleanup-old option: $arg" ;;
|
|
654
|
+
esac
|
|
655
|
+
done
|
|
656
|
+
require_docker
|
|
657
|
+
find_sources
|
|
658
|
+
images=$(docker image ls --format '{{.Repository}}:{{.Tag}}' 2>/dev/null |
|
|
659
|
+
grep -E "$OLD_IMAGES" | grep -v ':<none>$')
|
|
660
|
+
if [ -z "$SOURCE_VOLUMES$SOURCE_CONTAINERS$images" ]; then
|
|
661
|
+
say "No Hybro 0.3 containers, volumes, or images found."
|
|
662
|
+
return 0
|
|
663
|
+
fi
|
|
664
|
+
# The 0.3 volumes are the only copy until a migration has completed.
|
|
665
|
+
if [ -n "$SOURCE_VOLUMES" ]; then
|
|
666
|
+
volume_exists "$VOLUME" ||
|
|
667
|
+
die "volume $VOLUME does not exist yet; migrate first with $CLI upgrade, or start fresh with $CLI --fresh"
|
|
668
|
+
detect_layout
|
|
669
|
+
[ "$LAYOUT" = ok ] ||
|
|
670
|
+
die "the migration into $VOLUME did not finish; run $CLI upgrade first"
|
|
671
|
+
fi
|
|
672
|
+
say "These Hybro 0.3 items will be removed:"
|
|
673
|
+
for item in $SOURCE_CONTAINERS; do say " container $item"; done
|
|
674
|
+
for item in $SOURCE_VOLUMES; do say " volume $item"; done
|
|
675
|
+
for item in $images; do say " image $item"; done
|
|
676
|
+
if [ "$yes" != 1 ]; then
|
|
677
|
+
printf 'Remove them? [y/N] '
|
|
678
|
+
answer=
|
|
679
|
+
{ read -r answer </dev/tty; } 2>/dev/null
|
|
680
|
+
case $answer in
|
|
681
|
+
y | Y | yes | YES) ;;
|
|
682
|
+
*)
|
|
683
|
+
say "Nothing was removed. Use --yes to remove without asking."
|
|
684
|
+
return 0
|
|
685
|
+
;;
|
|
686
|
+
esac
|
|
687
|
+
fi
|
|
688
|
+
failed=0
|
|
689
|
+
for item in $SOURCE_CONTAINERS; do docker rm -f "$item" >/dev/null || failed=1; done
|
|
690
|
+
for item in $SOURCE_VOLUMES; do docker volume rm "$item" >/dev/null || failed=1; done
|
|
691
|
+
for item in $images; do docker image rm "$item" >/dev/null || failed=1; done
|
|
692
|
+
[ "$failed" = 0 ] || die "some items could not be removed; see the errors above"
|
|
693
|
+
say "Removed the Hybro 0.3 items."
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
# ------------------------------------------------------------------ dispatch
|
|
697
|
+
|
|
698
|
+
command=${1:-start}
|
|
699
|
+
case $command in
|
|
700
|
+
--version | -V) command=version ;;
|
|
701
|
+
-h | --help) command=help ;;
|
|
702
|
+
-*) command=start ;;
|
|
703
|
+
*) [ $# -eq 0 ] || shift ;;
|
|
704
|
+
esac
|
|
705
|
+
case $command in
|
|
706
|
+
start) cmd_start "$@" ;;
|
|
707
|
+
stop) cmd_stop "$@" ;;
|
|
708
|
+
status) cmd_status "$@" ;;
|
|
709
|
+
logs) cmd_logs "$@" ;;
|
|
710
|
+
upgrade) cmd_upgrade "$@" ;;
|
|
711
|
+
cleanup-old) cmd_cleanup_old "$@" ;;
|
|
712
|
+
version) say "hybro $HYBRO_VERSION" ;;
|
|
713
|
+
help) usage ;;
|
|
714
|
+
setup | config | mcp)
|
|
715
|
+
die "'$command' is not a command since 0.4: providers are set up on the Settings page, and MCP is served at /mcp. $CLI status shows the address."
|
|
716
|
+
;;
|
|
717
|
+
*)
|
|
718
|
+
err "unknown command: $command"
|
|
719
|
+
usage >&2
|
|
720
|
+
exit 2
|
|
721
|
+
;;
|
|
722
|
+
esac
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hybroai/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Hybro command line:
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Hybro command line: run Hybro, AI agents and rooms in one Docker container",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hybro",
|
|
7
7
|
"agents",
|
|
8
8
|
"a2a",
|
|
9
|
+
"mcp",
|
|
9
10
|
"cli"
|
|
10
11
|
],
|
|
11
12
|
"homepage": "https://github.com/hybroai/hybro",
|
|
@@ -16,21 +17,12 @@
|
|
|
16
17
|
},
|
|
17
18
|
"license": "Apache-2.0",
|
|
18
19
|
"bin": {
|
|
19
|
-
"hybro": "bin/hybro
|
|
20
|
+
"hybro": "bin/hybro"
|
|
20
21
|
},
|
|
21
22
|
"files": [
|
|
22
23
|
"bin/"
|
|
23
24
|
],
|
|
24
|
-
"engines": {
|
|
25
|
-
"node": ">=18"
|
|
26
|
-
},
|
|
27
25
|
"publishConfig": {
|
|
28
26
|
"access": "public"
|
|
29
|
-
},
|
|
30
|
-
"optionalDependencies": {
|
|
31
|
-
"@hybroai/cli-darwin-arm64": "0.3.0",
|
|
32
|
-
"@hybroai/cli-darwin-x64": "0.3.0",
|
|
33
|
-
"@hybroai/cli-linux-arm64": "0.3.0",
|
|
34
|
-
"@hybroai/cli-linux-x64": "0.3.0"
|
|
35
27
|
}
|
|
36
28
|
}
|
package/bin/hybro.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Entry point for `hybro`. It locates the frozen CLI for this platform and
|
|
3
|
-
// replaces itself with it.
|
|
4
|
-
//
|
|
5
|
-
// The platform binaries are optionalDependencies, not postinstall downloads:
|
|
6
|
-
// npm already resolves os/cpu, so only the matching package is installed, and
|
|
7
|
-
// `npm install --ignore-scripts` still yields a working CLI.
|
|
8
|
-
//
|
|
9
|
-
// No signal handlers are installed on purpose. With stdio inherited, the child
|
|
10
|
-
// shares this process's foreground process group, so Ctrl-C reaches it directly;
|
|
11
|
-
// forwarding as well would deliver SIGINT twice and the TUI treats that as two
|
|
12
|
-
// separate interrupts.
|
|
13
|
-
|
|
14
|
-
"use strict";
|
|
15
|
-
|
|
16
|
-
const { spawn } = require("node:child_process");
|
|
17
|
-
const { accessSync, constants, chmodSync } = require("node:fs");
|
|
18
|
-
|
|
19
|
-
const PLATFORM_PACKAGES = {
|
|
20
|
-
"darwin arm64": "@hybroai/cli-darwin-arm64",
|
|
21
|
-
"darwin x64": "@hybroai/cli-darwin-x64",
|
|
22
|
-
"linux arm64": "@hybroai/cli-linux-arm64",
|
|
23
|
-
"linux x64": "@hybroai/cli-linux-x64",
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
function fail(message) {
|
|
27
|
-
process.stderr.write(`hybro: ${message}\n`);
|
|
28
|
-
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const key = `${process.platform} ${process.arch}`;
|
|
32
|
-
const packageName = PLATFORM_PACKAGES[key];
|
|
33
|
-
if (!packageName) {
|
|
34
|
-
fail(
|
|
35
|
-
`no build for ${key}. Supported: ${Object.keys(PLATFORM_PACKAGES).join(", ")}.`,
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
let binary;
|
|
40
|
-
try {
|
|
41
|
-
binary = require.resolve(`${packageName}/hybro/hybro`);
|
|
42
|
-
} catch {
|
|
43
|
-
fail(
|
|
44
|
-
`${packageName} is not installed. Reinstall with ` +
|
|
45
|
-
`"npm install -g @hybroai/cli" without --omit=optional.`,
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// npm preserves the executable bit, but a copied or re-packed tree may not.
|
|
50
|
-
try {
|
|
51
|
-
accessSync(binary, constants.X_OK);
|
|
52
|
-
} catch {
|
|
53
|
-
try {
|
|
54
|
-
chmodSync(binary, 0o755);
|
|
55
|
-
} catch {
|
|
56
|
-
fail(`${binary} is not executable.`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const child = spawn(binary, process.argv.slice(2), { stdio: "inherit" });
|
|
61
|
-
|
|
62
|
-
child.on("error", (error) => fail(`cannot start ${binary}: ${error.message}`));
|
|
63
|
-
child.on("exit", (code, signal) => {
|
|
64
|
-
if (signal) {
|
|
65
|
-
process.kill(process.pid, signal);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
process.exit(code === null ? 1 : code);
|
|
69
|
-
});
|