@hybroai/cli 0.4.4 → 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.
Files changed (2) hide show
  1. package/bin/hybro +259 -2
  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.4" # x-release-please-version
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
- exec docker logs "$@" "$CONTAINER"
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" ;;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hybroai/cli",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Hybro command line: run Hybro, AI agents and rooms in one Docker container",
5
5
  "keywords": [
6
6
  "hybro",