#!/bin/sh # # sam-cli — agent-friendly CLI for the webOS SAM daemon # (System Application Manager, Luna service com.webos.applicationManager). # # Thin wrapper around luna-send with NO hard argument schemas: unknown/extra # args never crash it, JSON is passed through verbatim, and it works both # directly on the TV board and remotely over SSH. # # Pure POSIX sh (busybox ash compatible) — runs on the webOS TV itself, which # has no real bash. jq is used when present, otherwise built-in fallbacks. # # Sources: .pi/skills/sam-usage/SKILL.md # # Usage: # sam-cli [GLOBAL OPTS] [args...] # # Global options (before the command): # --host SSH target (user@host) to run luna-send on. # Defaults to $SAM_HOST. If unset, luna-send runs locally. # --ssh-args Extra ssh options as a whitespace-separated string, # e.g. "--ssh-args '-p 2222 -i ~/.ssh/id_ed25519'". # --dev Use the dev-only /dev category (needs devmode enabled). # --pretty Pretty-print JSON responses (needs jq). # --timeout Cap total execution time, 0 disables (default 30). # --wait Sleep after a successful launch/close — helps on slow # SSH links where the reply arrives with delay. # -h|--help Show this help. # # Commands (URI => luna://com.webos.applicationManager[/dev]/): # launch [extra-json] Launch app by id / launchPointId / instanceId. # extra-json (optional) is merged into the # payload, e.g. '{"params":{"key":"value"}}'. # close Stop an app by id or instanceId (method: close; # same as closeByAppId). # running List running apps (JSON under "running"). # list | list-apps | ls List installed apps (JSON under "apps"). # status getAppStatus for one app. # info getAppInfo for one app. # launch-points listLaunchPoints (JSON). # manager-info managerInfo (only valid with --dev). # # Agent conveniences: # --ids After `running`/`list`, print only app ids, # one per line (jq or grep fallback). # # Examples: # sam-cli launch com.webos.app.enactbrowser # sam-cli launch com.webos.app.enactbrowser '{"params":{"url":"https://x"}}' # sam-cli close com.webos.app.enactbrowser # sam-cli running --ids # sam-cli --host root@tv-ip list --pretty | jq '.apps[].id' # set -eu SERVICE="com.webos.applicationManager" LUNASEND="${LUNASEND:-luna-send}" SSH_TARGET="${SAM_HOST:-}" SSH_ARGS="" DEV=0 PRETTY=0 TIMEOUT=30 WAIT=0 usage() { sed -n '2,70p' "$0" | sed 's/^# \{0,1\}//' } die() { printf 'sam-cli: %s\n' "$*" >&2 exit 1 } # --- global option parsing: consumes options until the first positional ---- while [ "$#" -gt 0 ]; do case "$1" in -h|--help) usage; exit 0 ;; --host) [ "$#" -ge 2 ] || die "--host needs a value" SSH_TARGET="$2"; shift 2 ;; --host=*) SSH_TARGET="${1#*=}"; shift ;; --ssh-args) [ "$#" -ge 2 ] || die "--ssh-args needs a value" SSH_ARGS="$SSH_ARGS $2"; shift 2 ;; --dev) DEV=1; shift ;; --pretty) PRETTY=1; shift ;; --timeout) [ "$#" -ge 2 ] || die "--timeout needs a value" TIMEOUT="$2"; shift 2 ;; --wait) [ "$#" -ge 2 ] || die "--wait needs a value" WAIT="$2"; shift 2 ;; --) shift; break ;; -*) die "unknown option: $1 (run 'sam-cli --help')" ;; *) break ;; esac done CMD="${1:-}" [ -n "$CMD" ] || { usage; exit 2; } shift # --- payload helpers -------------------------------------------------------- json_base() { # $1 = field name, $2 = value -> {"":""} printf '{"%s":"%s"}' "$1" "$2" } # Merge extra JSON into a base payload, output COMPACT (single line). # Uses jq when available; POSIX fallback strips outer braces of the extra # object and splices the inner content into the base object. merge_json() { base="$1" extra="$2" inner="" merged="" if command -v jq >/dev/null 2>&1; then # jq absent OR fails (bad json, stub) -> fall through to sed splice merged=$(jq -c -n --argjson base "$base" --argjson extra "$extra" '$base + $extra' 2>/dev/null) || merged="" fi if [ -n "$merged" ]; then printf '%s' "$merged" return fi inner=$(printf '%s' "$extra" | sed 's/^ *{//; s/} *$//') if [ -n "$inner" ]; then printf '%s,%s}' "${base%\}}" "$inner" else printf '%s' "$base" fi } print_out() { # payload -> stdout (pretty if requested and possible) if [ "$PRETTY" -eq 1 ] && command -v jq >/dev/null 2>&1; then printf '%s\n' "$1" | jq . else printf '%s\n' "$1" fi } # --- execution -------------------------------------------------------------- run_luna() { # uri payload uri="$1" payload="$2" out="" rc=0 base_uri="luna://$SERVICE" [ "$DEV" -eq 1 ] && base_uri="$base_uri/dev" uri="$base_uri/$uri" if [ -n "$SSH_TARGET" ]; then # payload is piped over stdin and read back remotely via "$(cat)", # so any quotes/newlines survive without fragile shell escaping. if [ "$TIMEOUT" -gt 0 ]; then out=$(printf '%s\n' "$payload" | timeout "$TIMEOUT" ssh \ -o BatchMode=yes -o ConnectTimeout=10 $SSH_ARGS "$SSH_TARGET" \ "luna-send -n 1 '$uri' \"\$(cat)\"") || rc=$? else out=$(printf '%s\n' "$payload" | ssh \ -o BatchMode=yes -o ConnectTimeout=10 $SSH_ARGS "$SSH_TARGET" \ "luna-send -n 1 '$uri' \"\$(cat)\"") || rc=$? fi else if [ "$TIMEOUT" -gt 0 ]; then out=$(timeout "$TIMEOUT" "$LUNASEND" -n 1 "$uri" "$payload") || rc=$? else out=$("$LUNASEND" -n 1 "$uri" "$payload") || rc=$? fi fi if [ "$rc" -ne 0 ]; then printf 'sam-cli: luna-send failed (rc=%s): %s\n' "$rc" "$out" >&2 return "$rc" fi print_out "$out" return 0 } ids_only() { # payload jq-expr -> prints ids, one per line if command -v jq >/dev/null 2>&1; then printf '%s\n' "$1" | jq -r "$2" 2>/dev/null && return 0 fi # grep fallback: extract every "id":"..." field printf '%s\n' "$1" | grep -o '"id":"[^"]*"' | sed 's/"id":"//; s/"$//' } # --- command dispatch ------------------------------------------------------- case "$CMD" in launch) [ "$#" -ge 1 ] || die "launch needs an app id, e.g. 'launch com.webos.app.enactbrowser'" payload=$(json_base id "$1") if [ "$#" -ge 2 ]; then payload=$(merge_json "$payload" "$2"); fi run_luna launch "$payload" || exit $? if [ "$WAIT" -gt 0 ]; then sleep "$WAIT"; fi ;; close) [ "$#" -ge 1 ] || die "close needs an app id or instanceId" run_luna close "$(json_base id "$1")" || exit $? if [ "$WAIT" -gt 0 ]; then sleep "$WAIT"; fi ;; running) payload=$(run_luna running '{}') || exit $? if [ "$#" -ge 1 ] && [ "$1" = "--ids" ]; then ids_only "$payload" '.running[]?.id' else printf '%s\n' "$payload" fi ;; list|list-apps|ls) payload=$(run_luna listApps '{}') || exit $? if [ "$#" -ge 1 ] && [ "$1" = "--ids" ]; then ids_only "$payload" '.apps[]?.id' else printf '%s\n' "$payload" fi ;; status) [ "$#" -ge 1 ] || die "status needs an appId, e.g. 'status com.webos.app.enactbrowser'" run_luna getAppStatus "$(json_base appId "$1")" || exit $? ;; info) [ "$#" -ge 1 ] || die "info needs an app id, e.g. 'info com.webos.app.enactbrowser'" run_luna getAppInfo "$(json_base id "$1")" || exit $? ;; launch-points) run_luna listLaunchPoints '{}' || exit $? ;; manager-info) run_luna managerInfo '{}' || exit $? ;; *) die "unknown command: $CMD (run 'sam-cli --help' for the list)" ;; esac