@hybroai/cli 0.2.19 → 0.4.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.
Files changed (3) hide show
  1. package/bin/hybro +722 -0
  2. package/package.json +4 -12
  3. 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.0" # 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.2.19",
4
- "description": "Hybro command line: configure models and run the Hybro container stack",
3
+ "version": "0.4.0",
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.js"
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.2.19",
32
- "@hybroai/cli-darwin-x64": "0.2.19",
33
- "@hybroai/cli-linux-arm64": "0.2.19",
34
- "@hybroai/cli-linux-x64": "0.2.19"
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
- });