Files
udoandClaude Fable 5.1 decfa661a3 Add named CLI entry points and align service docs with the runtime
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>
2026-10-13 22:56:16 +00:00

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