@hybroai/cli 0.4.3 → 0.4.5
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 +259 -2
- package/package.json +1 -1
package/bin/hybro
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# package's bin, and the release asset that install.sh installs (release mode).
|
|
6
6
|
set -u
|
|
7
7
|
|
|
8
|
-
HYBRO_VERSION="0.4.
|
|
8
|
+
HYBRO_VERSION="0.4.5" # x-release-please-version
|
|
9
9
|
|
|
10
10
|
GITHUB_REPO="hybroai/hybro"
|
|
11
11
|
RELEASE_IMAGE="ghcr.io/hybroai/hybro"
|
|
@@ -82,6 +82,7 @@ Commands:
|
|
|
82
82
|
stop Stop Hybro
|
|
83
83
|
status Show whether Hybro is running and its address
|
|
84
84
|
logs Show Hybro's logs; docker logs options such as -f pass through
|
|
85
|
+
doctor Check a running Hybro and list its problems
|
|
85
86
|
upgrade Install the latest release, update the image, migrate 0.3 data
|
|
86
87
|
cleanup-old Remove the Hybro 0.3 containers, volumes, and images
|
|
87
88
|
version Print the version
|
|
@@ -96,6 +97,21 @@ Start options:
|
|
|
96
97
|
--build Rebuild the image (checkout only)
|
|
97
98
|
--recreate Recreate a running container (checkout only)
|
|
98
99
|
|
|
100
|
+
Logs options:
|
|
101
|
+
--service <name> Only lines from one program (backend, mcp, caddy,
|
|
102
|
+
frontend, mongod, or an agent)
|
|
103
|
+
--level <level> Only lines at this level or above: debug, info,
|
|
104
|
+
warning, error, critical; hides lines without a level
|
|
105
|
+
--trace <id> Only lines for one request or trace id
|
|
106
|
+
|
|
107
|
+
Doctor options:
|
|
108
|
+
--json Print the diagnostics snapshot as JSON
|
|
109
|
+
--bundle Write a diagnostics archive to share when reporting a
|
|
110
|
+
problem; works while Hybro is stopped
|
|
111
|
+
--output <file> Archive path for --bundle (default
|
|
112
|
+
hybro-diagnostics-<UTC time>.tar.gz here)
|
|
113
|
+
Exit status: 0 no problems, 1 problems found, 2 the check could not run.
|
|
114
|
+
|
|
99
115
|
Upgrade options:
|
|
100
116
|
--check Only report whether a newer release exists
|
|
101
117
|
|
|
@@ -476,10 +492,250 @@ cmd_status() {
|
|
|
476
492
|
esac
|
|
477
493
|
}
|
|
478
494
|
|
|
495
|
+
# Prints the level alternation at or above a threshold; warn is Caddy's warning
|
|
496
|
+
# and dpanic, panic and fatal count as critical.
|
|
497
|
+
# Accepts lower and upper case, as backend.log_level is written upper case.
|
|
498
|
+
log_levels() {
|
|
499
|
+
severe="critical|dpanic|panic|fatal"
|
|
500
|
+
case "$1" in
|
|
501
|
+
debug | DEBUG) echo "debug|info|warning|warn|error|$severe" ;;
|
|
502
|
+
info | INFO) echo "info|warning|warn|error|$severe" ;;
|
|
503
|
+
warning | warn | WARNING | WARN) echo "warning|warn|error|$severe" ;;
|
|
504
|
+
error | ERROR) echo "error|$severe" ;;
|
|
505
|
+
critical | CRITICAL) echo "$severe" ;;
|
|
506
|
+
*) return 1 ;;
|
|
507
|
+
esac
|
|
508
|
+
}
|
|
509
|
+
|
|
479
510
|
cmd_logs() {
|
|
511
|
+
service="" levels="" trace="" filtered=""
|
|
512
|
+
for arg in "$@"; do
|
|
513
|
+
shift
|
|
514
|
+
case "$arg" in
|
|
515
|
+
--service=* | --level=* | --trace=*)
|
|
516
|
+
set -- "$@" "${arg%%=*}" "${arg#*=}"
|
|
517
|
+
continue
|
|
518
|
+
;;
|
|
519
|
+
esac
|
|
520
|
+
set -- "$@" "$arg"
|
|
521
|
+
done
|
|
522
|
+
for arg in "$@"; do
|
|
523
|
+
shift
|
|
524
|
+
if [ -n "$filtered" ]; then
|
|
525
|
+
[ -n "$arg" ] || die "$filtered needs a value"
|
|
526
|
+
case "$filtered" in
|
|
527
|
+
--service) service=$arg ;;
|
|
528
|
+
--level)
|
|
529
|
+
levels=$(log_levels "$arg") || die "--level must be debug, info, warning, error, or critical"
|
|
530
|
+
;;
|
|
531
|
+
--trace) trace=$arg ;;
|
|
532
|
+
esac
|
|
533
|
+
filtered=""
|
|
534
|
+
continue
|
|
535
|
+
fi
|
|
536
|
+
case "$arg" in
|
|
537
|
+
--service | --level | --trace) filtered=$arg ;;
|
|
538
|
+
*) set -- "$@" "$arg" ;;
|
|
539
|
+
esac
|
|
540
|
+
done
|
|
541
|
+
[ -z "$filtered" ] || die "$filtered needs a value"
|
|
480
542
|
require_docker
|
|
481
543
|
[ -n "$(container_state)" ] || die "there is no $CONTAINER container. Start Hybro with: $CLI"
|
|
482
|
-
|
|
544
|
+
if [ -z "$service$levels$trace" ]; then
|
|
545
|
+
exec docker logs "$@" "$CONTAINER"
|
|
546
|
+
fi
|
|
547
|
+
# docker's own errors carry no program prefix and are always shown; its exit
|
|
548
|
+
# status reaches awk as a sentinel line so a failure is not masked by the pipe.
|
|
549
|
+
{ docker logs "$@" "$CONTAINER" 2>&1 || echo "hybro_docker_exit=$?"; } |
|
|
550
|
+
awk -v service="$service" -v levels="$levels" -v trace="$trace" '
|
|
551
|
+
/^hybro_docker_exit=[0-9]+$/ { code = substr($0, 19) + 0; next }
|
|
552
|
+
{
|
|
553
|
+
split_at = index($0, " | ")
|
|
554
|
+
if (split_at == 0) { print; fflush(); next }
|
|
555
|
+
name = substr($0, 1, split_at - 1)
|
|
556
|
+
body = substr($0, split_at + 3)
|
|
557
|
+
# docker logs -t puts a timestamp before the program name.
|
|
558
|
+
sub(/^.* /, "", name)
|
|
559
|
+
if (service != "" && name != service) next
|
|
560
|
+
if (levels != "" && level_of(body) !~ ("^(" levels ")$")) next
|
|
561
|
+
if (trace != "" && !has_trace(body)) next
|
|
562
|
+
print
|
|
563
|
+
fflush()
|
|
564
|
+
}
|
|
565
|
+
# JSON lines write level before any message text; logfmt lines start
|
|
566
|
+
# with it or with timestamp then level. Plain text has no level.
|
|
567
|
+
function level_of(text, value) {
|
|
568
|
+
if (substr(text, 1, 1) == "{") {
|
|
569
|
+
if (!match(text, /"level":"[^"]*"/)) return ""
|
|
570
|
+
return substr(text, RSTART + 9, RLENGTH - 10)
|
|
571
|
+
}
|
|
572
|
+
if (!match(text, /^(timestamp=[^ ]* )?level=[^ ]*/)) return ""
|
|
573
|
+
value = substr(text, RSTART, RLENGTH)
|
|
574
|
+
sub(/^.*level=/, "", value)
|
|
575
|
+
return value
|
|
576
|
+
}
|
|
577
|
+
function has_trace(text, at, rest) {
|
|
578
|
+
if (index(text, "\"trace_id\":\"" trace "\"")) return 1
|
|
579
|
+
rest = text
|
|
580
|
+
while ((at = index(rest, "trace_id=" trace)) > 0) {
|
|
581
|
+
if ((at == 1 || substr(rest, at - 1, 1) == " ") && (at + 9 + length(trace) > length(rest) || substr(rest, at + 9 + length(trace), 1) == " ")) return 1
|
|
582
|
+
rest = substr(rest, at + 1)
|
|
583
|
+
}
|
|
584
|
+
return 0
|
|
585
|
+
}
|
|
586
|
+
END { exit code }'
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
DIAG_PYTHON=/app/backend/.venv/bin/python
|
|
590
|
+
DIAG_HELPER=/app/backend/diagnostics_cli.py
|
|
591
|
+
|
|
592
|
+
# hybro doctor exits 2 whenever the check could not run, never through die.
|
|
593
|
+
doctor_fail() {
|
|
594
|
+
err "$*"
|
|
595
|
+
exit 2
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
# Whether the running container has the helper; an older image does not.
|
|
599
|
+
has_helper() {
|
|
600
|
+
docker exec "$CONTAINER" test -f "$DIAG_HELPER"
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
# Exits 2 when the container stopped or went away after the first check.
|
|
604
|
+
require_running() {
|
|
605
|
+
state=$(container_state)
|
|
606
|
+
[ "$state" = running ] ||
|
|
607
|
+
doctor_fail "Hybro is not running (container $CONTAINER is ${state:-gone}). Collect its logs with: $CLI doctor --bundle"
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
# The stop-and-start hint only after doctor --bundle, since a start removes the
|
|
611
|
+
# container and its logs.
|
|
612
|
+
require_helper() {
|
|
613
|
+
has_helper && return 0
|
|
614
|
+
require_running
|
|
615
|
+
doctor_fail "this container runs an older image. Collect its logs first with: $CLI doctor --bundle; then run $CLI stop, then $CLI start"
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
# doctor_bundle <container state> <output file>
|
|
619
|
+
doctor_bundle() {
|
|
620
|
+
state=$1
|
|
621
|
+
out=$2
|
|
622
|
+
partial=$out.partial
|
|
623
|
+
offline=1
|
|
624
|
+
if [ "$state" = running ] && has_helper; then
|
|
625
|
+
offline=0
|
|
626
|
+
else
|
|
627
|
+
# A stopped container, or a running one of an older image: the
|
|
628
|
+
# script's own image has the helper; no volume is mounted.
|
|
629
|
+
(ensure_image) || exit 2
|
|
630
|
+
fi
|
|
631
|
+
# Created exclusively and private, so a file that appeared since the check
|
|
632
|
+
# is never overwritten or removed; from here on the partial file is ours.
|
|
633
|
+
(
|
|
634
|
+
umask 077
|
|
635
|
+
set -C
|
|
636
|
+
: >"$partial"
|
|
637
|
+
) 2>/dev/null ||
|
|
638
|
+
doctor_fail "cannot create $partial: it already exists or the directory is not writable"
|
|
639
|
+
trap 'rm -f -- "$partial"; exit 130' INT
|
|
640
|
+
trap 'rm -f -- "$partial"; exit 143' TERM
|
|
641
|
+
if [ "$offline" = 0 ]; then
|
|
642
|
+
docker logs --tail 2000 "$CONTAINER" 2>&1 |
|
|
643
|
+
docker exec -i "$CONTAINER" "$DIAG_PYTHON" "$DIAG_HELPER" bundle >"$partial"
|
|
644
|
+
else
|
|
645
|
+
docker logs --tail 2000 "$CONTAINER" 2>&1 |
|
|
646
|
+
docker run --rm -i --entrypoint "$DIAG_PYTHON" "$IMAGE" \
|
|
647
|
+
"$DIAG_HELPER" bundle --offline >"$partial"
|
|
648
|
+
fi
|
|
649
|
+
status=$?
|
|
650
|
+
# docker logs of a container removed meanwhile prints nothing, and its
|
|
651
|
+
# failure is hidden by the pipe.
|
|
652
|
+
if [ "$status" -eq 0 ] && [ -z "$(container_state)" ]; then
|
|
653
|
+
rm -f -- "$partial"
|
|
654
|
+
doctor_fail "the $CONTAINER container was removed while its logs were read"
|
|
655
|
+
fi
|
|
656
|
+
if [ "$status" -ne 0 ] || ! mv -- "$partial" "$out"; then
|
|
657
|
+
rm -f -- "$partial"
|
|
658
|
+
doctor_fail "cannot write the diagnostics bundle"
|
|
659
|
+
fi
|
|
660
|
+
# Kept until the partial file is gone, so an interrupt never leaves it.
|
|
661
|
+
trap - INT TERM
|
|
662
|
+
say "Wrote $out"
|
|
663
|
+
if [ "$offline" = 1 ]; then
|
|
664
|
+
say "Contains recent container logs only (no snapshot or stored records); review it before sharing."
|
|
665
|
+
else
|
|
666
|
+
say "Contains the runtime snapshot, recent system events and logs; review it before sharing."
|
|
667
|
+
fi
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
cmd_doctor() {
|
|
671
|
+
json=0 bundle=0 out="" output_given=0
|
|
672
|
+
while [ $# -gt 0 ]; do
|
|
673
|
+
case $1 in
|
|
674
|
+
--json) json=1 ;;
|
|
675
|
+
--bundle) bundle=1 ;;
|
|
676
|
+
--output)
|
|
677
|
+
[ $# -ge 2 ] || doctor_fail "--output needs a file name"
|
|
678
|
+
out=$2
|
|
679
|
+
output_given=1
|
|
680
|
+
shift
|
|
681
|
+
;;
|
|
682
|
+
--output=*)
|
|
683
|
+
out=${1#*=}
|
|
684
|
+
output_given=1
|
|
685
|
+
;;
|
|
686
|
+
-h | --help)
|
|
687
|
+
usage
|
|
688
|
+
exit 0
|
|
689
|
+
;;
|
|
690
|
+
*)
|
|
691
|
+
err "unknown doctor option: $1"
|
|
692
|
+
usage >&2
|
|
693
|
+
exit 2
|
|
694
|
+
;;
|
|
695
|
+
esac
|
|
696
|
+
shift
|
|
697
|
+
done
|
|
698
|
+
if [ "$json" = 1 ] && [ "$bundle" = 1 ]; then
|
|
699
|
+
doctor_fail "--json and --bundle cannot be combined"
|
|
700
|
+
fi
|
|
701
|
+
if [ "$output_given" = 1 ] && [ "$bundle" = 0 ]; then
|
|
702
|
+
doctor_fail "--output needs --bundle"
|
|
703
|
+
fi
|
|
704
|
+
if [ "$bundle" = 1 ]; then
|
|
705
|
+
[ "$output_given" = 0 ] || [ -n "$out" ] || doctor_fail "--output needs a file name"
|
|
706
|
+
[ -n "$out" ] || out=hybro-diagnostics-$(date -u +%Y%m%d-%H%M%S).tar.gz
|
|
707
|
+
if [ -e "$out" ] || [ -L "$out" ]; then
|
|
708
|
+
doctor_fail "$out already exists; choose another file with --output"
|
|
709
|
+
fi
|
|
710
|
+
if [ -e "$out.partial" ] || [ -L "$out.partial" ]; then
|
|
711
|
+
doctor_fail "$out.partial already exists; remove it or choose another file with --output"
|
|
712
|
+
fi
|
|
713
|
+
fi
|
|
714
|
+
(require_docker) || exit 2
|
|
715
|
+
state=$(container_state)
|
|
716
|
+
[ -n "$state" ] || doctor_fail "there is no $CONTAINER container. Start Hybro with: $CLI"
|
|
717
|
+
if [ "$bundle" = 1 ]; then
|
|
718
|
+
doctor_bundle "$state" "$out"
|
|
719
|
+
return
|
|
720
|
+
fi
|
|
721
|
+
[ "$state" = running ] ||
|
|
722
|
+
doctor_fail "Hybro is not running (container $CONTAINER is $state). Collect its logs with: $CLI doctor --bundle"
|
|
723
|
+
require_helper
|
|
724
|
+
if [ "$json" = 1 ]; then
|
|
725
|
+
docker exec "$CONTAINER" "$DIAG_PYTHON" "$DIAG_HELPER" doctor --json
|
|
726
|
+
else
|
|
727
|
+
docker exec "$CONTAINER" "$DIAG_PYTHON" "$DIAG_HELPER" doctor
|
|
728
|
+
fi
|
|
729
|
+
# 0 and 1 are the helper's answers; anything else means it could not
|
|
730
|
+
# check. A container that stopped mid-run also ends docker exec with 1.
|
|
731
|
+
case $? in
|
|
732
|
+
0) return 0 ;;
|
|
733
|
+
1)
|
|
734
|
+
require_running
|
|
735
|
+
return 1
|
|
736
|
+
;;
|
|
737
|
+
*) exit 2 ;;
|
|
738
|
+
esac
|
|
483
739
|
}
|
|
484
740
|
|
|
485
741
|
sha256_of() {
|
|
@@ -707,6 +963,7 @@ start) cmd_start "$@" ;;
|
|
|
707
963
|
stop) cmd_stop "$@" ;;
|
|
708
964
|
status) cmd_status "$@" ;;
|
|
709
965
|
logs) cmd_logs "$@" ;;
|
|
966
|
+
doctor) cmd_doctor "$@" ;;
|
|
710
967
|
upgrade) cmd_upgrade "$@" ;;
|
|
711
968
|
cleanup-old) cmd_cleanup_old "$@" ;;
|
|
712
969
|
version) say "hybro $HYBRO_VERSION" ;;
|