omnilane 0.7.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.
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env bash
2
+ set -u
3
+ # Read-only health report for routing, state, watchdog, and optional UI support.
4
+
5
+ JSON_MODE=0
6
+ if [[ $# -eq 1 && "$1" == "--json" ]]; then
7
+ JSON_MODE=1
8
+ elif [[ $# -ne 0 ]]; then
9
+ echo "usage: omnilane doctor [--json]" >&2
10
+ exit 2
11
+ fi
12
+ REPO="${OMNILANE_DOCTOR_REPO:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
13
+ OMNILANE_HOME="${OMNILANE_HOME:-$HOME/.omnilane}"
14
+ PASS_COUNT=0
15
+ WARN_COUNT=0
16
+ FAIL_COUNT=0
17
+ JSON_REPORTS=""
18
+ JSON_FIRST=1
19
+
20
+ json_escape() {
21
+ local s="$1" out="" ch escaped code i
22
+ for ((i = 0; i < ${#s}; i++)); do
23
+ ch="${s:i:1}"
24
+ case "$ch" in
25
+ '"') out="$out\\\"" ;;
26
+ '\\') out="$out\\\\" ;;
27
+ $'\b') out="$out\\b" ;;
28
+ $'\f') out="$out\\f" ;;
29
+ $'\n') out="$out\\n" ;;
30
+ $'\r') out="$out\\r" ;;
31
+ $'\t') out="$out\\t" ;;
32
+ *)
33
+ LC_CTYPE=C printf -v code '%d' "'$ch"
34
+ if [[ "$code" -ge 0 && "$code" -lt 32 ]]; then
35
+ printf -v escaped '\\u%04x' "$code"
36
+ out="$out$escaped"
37
+ else
38
+ out="$out$ch"
39
+ fi
40
+ ;;
41
+ esac
42
+ done
43
+ printf '%s' "$out"
44
+ }
45
+
46
+ report() {
47
+ local level="$1" check="$2" message="$3"
48
+ # Diagnostic inputs include user-controlled paths and routing errors. Strip
49
+ # terminal control bytes so a failed check cannot forge the report display.
50
+ message="$(printf '%s' "$message" | LC_ALL=C tr -d '\000-\010\013\014\016-\037\177')"
51
+ if [[ "$JSON_MODE" -eq 1 ]]; then
52
+ [[ "$JSON_FIRST" -eq 1 ]] || JSON_REPORTS="$JSON_REPORTS,"
53
+ JSON_FIRST=0
54
+ JSON_REPORTS="$JSON_REPORTS{\"level\":\"$(json_escape "$level")\",\"check\":\"$(json_escape "$check")\",\"message\":\"$(json_escape "$message")\"}"
55
+ else
56
+ printf '%-5s %-12s %s\n' "$level" "$check" "$message"
57
+ fi
58
+ case "$level" in
59
+ PASS) PASS_COUNT=$((PASS_COUNT + 1)) ;;
60
+ WARN) WARN_COUNT=$((WARN_COUNT + 1)) ;;
61
+ FAIL) FAIL_COUNT=$((FAIL_COUNT + 1)) ;;
62
+ esac
63
+ }
64
+
65
+ DISPATCH="$REPO/scripts/dispatch.sh"
66
+ if [[ -x "$DISPATCH" ]]; then
67
+ report PASS dispatch "$DISPATCH is executable"
68
+ else
69
+ report FAIL dispatch "$DISPATCH is missing or not executable"
70
+ fi
71
+
72
+ if [[ -r "$REPO/routing.yaml" ]]; then
73
+ report PASS config "$REPO/routing.yaml is readable"
74
+ else
75
+ report FAIL config "$REPO/routing.yaml is missing or unreadable"
76
+ fi
77
+
78
+ if [[ -x "$DISPATCH" ]]; then
79
+ route_output="$(OMNILANE_HOME="$OMNILANE_HOME" /bin/bash "$DISPATCH" --list 2>&1)"
80
+ route_rc=$?
81
+ route_count="$(printf '%s\n' "$route_output" | awk 'NF { count++ } END { print count + 0 }')"
82
+ usable_count="$(printf '%s\n' "$route_output" | awk \
83
+ 'NF && $0 !~ /unavailable/ && $0 !~ /^[^:]+:[[:space:]]+off([[:space:]]|$)/ { count++ } END { print count + 0 }')"
84
+ if [[ "$route_rc" -ne 0 ]]; then
85
+ first_error="$(printf '%s\n' "$route_output" | sed -n '1p')"
86
+ report FAIL routing "effective routing failed: ${first_error:-unknown error}"
87
+ elif [[ "$route_count" -eq 0 ]]; then
88
+ report FAIL routing "effective routing is empty"
89
+ elif [[ "$usable_count" -eq 0 ]]; then
90
+ report WARN routing "$route_count lanes parsed, but none is currently usable"
91
+ else
92
+ report PASS routing "$route_count lanes parsed; $usable_count currently usable"
93
+ fi
94
+ fi
95
+
96
+ if [[ ! -e "$OMNILANE_HOME" ]]; then
97
+ report WARN state "$OMNILANE_HOME does not exist yet; doctor left it unchanged"
98
+ elif [[ ! -d "$OMNILANE_HOME" ]]; then
99
+ report FAIL state "$OMNILANE_HOME exists but is not a directory"
100
+ elif [[ ! -r "$OMNILANE_HOME" || ! -w "$OMNILANE_HOME" || ! -x "$OMNILANE_HOME" ]]; then
101
+ report FAIL state "$OMNILANE_HOME is not readable, writable, and searchable"
102
+ else
103
+ report PASS state "$OMNILANE_HOME is accessible"
104
+ fi
105
+
106
+ if [[ -f "$OMNILANE_HOME/local.sh" ]]; then
107
+ if /bin/bash -n "$OMNILANE_HOME/local.sh" 2>/dev/null; then
108
+ report PASS local-config "$OMNILANE_HOME/local.sh syntax is valid"
109
+ else
110
+ report FAIL local-config "$OMNILANE_HOME/local.sh has invalid Bash syntax"
111
+ fi
112
+ fi
113
+
114
+ if [[ -L "$OMNILANE_HOME/jobs" ||
115
+ ( -e "$OMNILANE_HOME/jobs" && ! -d "$OMNILANE_HOME/jobs" ) ]]; then
116
+ report FAIL job-privacy "$OMNILANE_HOME/jobs must be a real directory, not a symlink or file"
117
+ elif [[ -d "$OMNILANE_HOME/jobs" ]]; then
118
+ jobs_mode="$(stat -f '%Lp' "$OMNILANE_HOME/jobs" 2>/dev/null || stat -c '%a' "$OMNILANE_HOME/jobs" 2>/dev/null || true)"
119
+ if [[ "$jobs_mode" =~ ^[0-7]*00$ ]]; then
120
+ report PASS job-privacy "$OMNILANE_HOME/jobs mode is $jobs_mode"
121
+ elif [[ -n "$jobs_mode" ]]; then
122
+ report WARN job-privacy "$OMNILANE_HOME/jobs mode is $jobs_mode; owner-only 700 is safer"
123
+ else
124
+ report WARN job-privacy "could not determine $OMNILANE_HOME/jobs permissions"
125
+ fi
126
+ fi
127
+
128
+ if [[ -L "$OMNILANE_HOME/locks" ||
129
+ ( -e "$OMNILANE_HOME/locks" && ! -d "$OMNILANE_HOME/locks" ) ]]; then
130
+ report FAIL lock-store "$OMNILANE_HOME/locks must be a real directory, not a symlink or file"
131
+ fi
132
+
133
+ if command -v timeout >/dev/null 2>&1; then
134
+ report PASS watchdog "timeout is available"
135
+ elif command -v gtimeout >/dev/null 2>&1; then
136
+ report PASS watchdog "gtimeout is available"
137
+ elif command -v perl >/dev/null 2>&1; then
138
+ report PASS watchdog "Perl alarm fallback is available"
139
+ else
140
+ report FAIL watchdog "timeout, gtimeout, and perl are all unavailable"
141
+ fi
142
+
143
+ if command -v python3 >/dev/null 2>&1; then
144
+ if python3 -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 9) else 1)' \
145
+ >/dev/null 2>&1; then
146
+ python_version="$(python3 -c 'import platform; print(platform.python_version())')"
147
+ report PASS live-ui "Python $python_version supports the optional UI"
148
+ else
149
+ report WARN live-ui "Python 3.9 or newer is required for the optional UI"
150
+ fi
151
+ else
152
+ report WARN live-ui "python3 is absent; model routing still works"
153
+ fi
154
+
155
+ if [[ "$JSON_MODE" -eq 1 ]]; then
156
+ ok=true
157
+ [[ "$FAIL_COUNT" -eq 0 ]] || ok=false
158
+ printf '{"ok":%s,"checks":[%s],"summary":{"passed":%s,"warnings":%s,"failed":%s}}\n' \
159
+ "$ok" "$JSON_REPORTS" "$PASS_COUNT" "$WARN_COUNT" "$FAIL_COUNT"
160
+ else
161
+ warning_suffix=s
162
+ [[ "$WARN_COUNT" -eq 1 ]] && warning_suffix=""
163
+ printf '\nSummary: %s passed, %s warning%s, %s failed\n' \
164
+ "$PASS_COUNT" "$WARN_COUNT" "$warning_suffix" "$FAIL_COUNT"
165
+ fi
166
+ [[ "$FAIL_COUNT" -eq 0 ]]