Private
Public Access
function CLI:NAME() compiles like the other named handlers and exports __bearer_cli_NAME; bearer-cli /unit.capy:NAME runs it, with --get pairs in request.query. The static gate, the doc checker, the handler form list, the CLI handler page, the language reference, and the skill file know the form, and a fixture with a default and a named entry point is in phase 1. The services handler page and guide now describe the mailbox bound as the kernel datagram queue, say that an unknown signal is dropped and logged, and that a send to a listening service is one datagram. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
181 lines
4.2 KiB
Bash
Executable File
181 lines
4.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
default_socket="/run/bearer/cli.sock"
|
|
if [[ -z "${BEARER_CLI_SOCKET:-}" && -r /etc/bearer/settings.cfg ]]; then
|
|
configured_socket=$(awk -F= '/^[[:space:]]*CLI_SOCKET_PATH[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/bearer/settings.cfg)
|
|
if [[ -n "${configured_socket:-}" ]]; then
|
|
default_socket="$configured_socket"
|
|
fi
|
|
fi
|
|
socket_path="${BEARER_CLI_SOCKET:-$default_socket}"
|
|
method="auto"
|
|
raw_json=""
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage:
|
|
scripts/bearer-cli [options] <unit-or-hook> [key=value ...]
|
|
|
|
Options:
|
|
--socket PATH Unix socket path (default: CLI_SOCKET_PATH from /etc/bearer/settings.cfg,
|
|
BEARER_CLI_SOCKET, or /run/bearer/cli.sock)
|
|
--get Send key=value parameters as a query string
|
|
--post Send key=value parameters as a JSON POST (default when params exist)
|
|
--json JSON Send raw JSON as POST body
|
|
-h, --help Show this help
|
|
|
|
Examples:
|
|
scripts/bearer-cli /ping
|
|
scripts/bearer-cli /tests/cli.capy action=echo message=hello
|
|
scripts/bearer-cli --json '{"action":"echo","message":"hello"}' /tests/cli.capy
|
|
scripts/bearer-cli --get /tests/cli.capy action=echo message=hello
|
|
scripts/bearer-cli --get /tests/cli.capy:ECHO message=hello # the CLI:ECHO entry point
|
|
USAGE
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--socket)
|
|
[[ $# -ge 2 ]] || { echo "--socket requires a path" >&2; exit 2; }
|
|
socket_path="$2"
|
|
shift 2
|
|
;;
|
|
--get)
|
|
method="get"
|
|
shift
|
|
;;
|
|
--post)
|
|
method="post"
|
|
shift
|
|
;;
|
|
--json)
|
|
[[ $# -ge 2 ]] || { echo "--json requires a JSON value" >&2; exit 2; }
|
|
raw_json="$2"
|
|
method="post"
|
|
shift 2
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
--)
|
|
shift
|
|
break
|
|
;;
|
|
-*)
|
|
echo "unknown option: $1" >&2
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
*)
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
|
|
[[ $# -ge 1 ]] || { usage >&2; exit 2; }
|
|
path="$1"
|
|
shift
|
|
|
|
if [[ "$path" != /* ]]; then
|
|
path="/$path"
|
|
fi
|
|
|
|
if [[ ! -S "$socket_path" ]]; then
|
|
echo "BEARER CLI socket not found: $socket_path" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$method" == "auto" ]]; then
|
|
if [[ $# -gt 0 || -n "$raw_json" ]]; then
|
|
method="post"
|
|
else
|
|
method="get"
|
|
fi
|
|
fi
|
|
|
|
curl_args=(-sS --fail-with-body --unix-socket "$socket_path")
|
|
|
|
run_curl() {
|
|
local body_file error_file http_status status
|
|
while true; do
|
|
body_file=$(mktemp)
|
|
error_file=$(mktemp)
|
|
status=0
|
|
http_status=$(curl "$@" --output "$body_file" --write-out '%{http_code}' 2>"$error_file") || status=$?
|
|
if [[ "$http_status" == 503 ]] && grep -qx 'BEARER_CLI_BUSY: another Capy CLI unit is active' "$body_file"; then
|
|
rm -f "$body_file" "$error_file"
|
|
sleep 1
|
|
continue
|
|
fi
|
|
cat "$body_file"
|
|
if ((status != 0)); then
|
|
cat "$error_file" >&2
|
|
fi
|
|
rm -f "$body_file" "$error_file"
|
|
return "$status"
|
|
done
|
|
}
|
|
|
|
if [[ -n "$raw_json" ]]; then
|
|
printf '%s' "$raw_json" | python3 -m json.tool >/dev/null
|
|
run_curl "${curl_args[@]}" \
|
|
-X POST \
|
|
-H 'Content-Type: application/json' \
|
|
--data-binary "$raw_json" \
|
|
"http://localhost$path"
|
|
exit $?
|
|
fi
|
|
|
|
if [[ "$method" == "get" ]]; then
|
|
url="http://localhost$path"
|
|
if [[ $# -gt 0 ]]; then
|
|
query=$(python3 - "$@" <<'PY'
|
|
import sys, urllib.parse
|
|
pairs = []
|
|
for arg in sys.argv[1:]:
|
|
if '=' not in arg:
|
|
raise SystemExit(f"argument is not key=value: {arg}")
|
|
key, value = arg.split('=', 1)
|
|
pairs.append((key, value))
|
|
print(urllib.parse.urlencode(pairs))
|
|
PY
|
|
)
|
|
url="$url?$query"
|
|
fi
|
|
run_curl "${curl_args[@]}" "$url"
|
|
else
|
|
json=$(python3 - "$@" <<'PY'
|
|
import json, sys
|
|
payload = {}
|
|
for arg in sys.argv[1:]:
|
|
if '=' not in arg:
|
|
raise SystemExit(f"argument is not key=value: {arg}")
|
|
key, value = arg.split('=', 1)
|
|
lowered = value.lower()
|
|
if lowered == 'true':
|
|
parsed = True
|
|
elif lowered == 'false':
|
|
parsed = False
|
|
elif lowered in ('null', 'none'):
|
|
parsed = None
|
|
else:
|
|
try:
|
|
parsed = int(value)
|
|
except ValueError:
|
|
try:
|
|
parsed = float(value)
|
|
except ValueError:
|
|
parsed = value
|
|
payload[key] = parsed
|
|
print(json.dumps(payload, separators=(',', ':')))
|
|
PY
|
|
)
|
|
run_curl "${curl_args[@]}" \
|
|
-X POST \
|
|
-H 'Content-Type: application/json' \
|
|
--data-binary "$json" \
|
|
"http://localhost$path"
|
|
fi
|