Split the POSIX CLI into modules
linux/claude-mode was 3,035 lines. It is now 177: the paths and flags, a loader, and the command dispatch. Everything else moved, verbatim, into twelve files under linux/lib/, one per concern - output, core, preflight, sessions, switch, catalogue, commands, doctor, presets, menu, setup, repair. The move was done by line range with a check that every original line landed in exactly one file; the only thing that changed place is the switch's running-sessions question, which now sits with session detection. The CLI finds lib/, cm-json.py and cm-vault.sh beside itself, following the ~/.local/bin symlink, so a checkout runs its own code rather than the installed version's (it used to mix the two). The key helper path written into settings.json is still the installed one. linux/install.sh ships bin/lib/, clearing old modules first so a removed one cannot linger. The package build copies linux/ recursively, which a flat copy would not. tests/cli/test_install.sh runs the real installer into a sandbox home: every file lands, the symlink runs, a reinstall keeps edited presets and drops a stale module. docs/architecture.md lists the modules. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
# shellcheck shell=bash
|
||||
# linux/lib/commands.sh - status, presets, models and set-key.
|
||||
#
|
||||
# Part of the claude-mode CLI: sourced by linux/claude-mode, which sets the
|
||||
# CM_* paths, PY and JSON used here. Not meant to run on its own.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Commands
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
cmd_status() {
|
||||
local mode preset
|
||||
mode="$(state_mode)"; preset="$(state_preset)"
|
||||
head_ "claude-mode: $mode"
|
||||
|
||||
if [ "$mode" = "anthropic" ]; then
|
||||
say 'native Anthropic login/subscription; no gateway env, no apiKeyHelper'
|
||||
else
|
||||
local pf; pf="$(preset_path "$preset")"
|
||||
say "preset: $preset"
|
||||
if [ -f "$pf" ]; then
|
||||
say "baseUrl: $(jget "$pf" baseUrl)"
|
||||
local t v
|
||||
for t in "${TIERS[@]}"; do
|
||||
v="$(jget "$pf" "models.$t")"; [ -n "$v" ] && printf ' %-10s%s\n' "$t:" "$v"
|
||||
done
|
||||
v="$(jget "$pf" subagentModel)"; [ -n "$v" ] && printf ' %-10s%s\n' "subagent:" "$v"
|
||||
v="$(jget "$pf" contextTokens)"; [ -n "$v" ] && printf ' %-10s%s tokens\n' "context:" "$v"
|
||||
local am; am="$(jget "$pf" auth.mode)"; [ -z "$am" ] && am=vault
|
||||
if [ "$am" = "vault" ]; then
|
||||
local ref; ref="$(jget "$pf" auth.keyRef)"; [ -z "$ref" ] && ref=openrouter
|
||||
printf ' %-10s%s -> %s [%s]\n' "key:" "$ref" "$(cm_vault_mask "$(cm_vault_get "$ref" 2>/dev/null || true)")" "$(cm_vault_backend_label)"
|
||||
else
|
||||
printf ' %-10s%s (inline, not a secret)\n' "token:" "$(jget "$pf" auth.token)"
|
||||
fi
|
||||
else
|
||||
err "preset '$preset' not found"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf '\n settings.json managed keys:\n'
|
||||
local any=0 line
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] || continue
|
||||
printf ' %s\n' "$line"; any=1
|
||||
done < <("$PY" "$JSON" settings-env "$CM_SETTINGS" "$CM_STATE" 2>/dev/null)
|
||||
[ "$any" -eq 0 ] && printf ' (none - clean)\n'
|
||||
|
||||
printf '\n'
|
||||
check_stray_env "$mode" || true
|
||||
|
||||
# Keep the machine-readable mirror current for readers that poll it (the
|
||||
# Omarchy bar widget among them) rather than leaving it as stale as the
|
||||
# last switch.
|
||||
write_health "$mode" "$preset"
|
||||
}
|
||||
|
||||
cmd_presets() {
|
||||
head_ 'presets'
|
||||
local active_mode active_preset name provider desc mark
|
||||
active_mode="$(state_mode)"; active_preset="$(state_preset)"
|
||||
while IFS=$'\t' read -r name provider desc; do
|
||||
mark=' '
|
||||
[ "$name" = "$active_preset" ] && [ "$active_mode" != "anthropic" ] && mark='*'
|
||||
printf ' %s %-18s [%-10s] %s\n' "$mark" "$name" "$provider" "$desc"
|
||||
done < <("$PY" "$JSON" presets "$CM_PRESETS")
|
||||
}
|
||||
|
||||
cmd_models() {
|
||||
local filter='' pname='' refresh=0 as_json=0 a pf='' provider base='' tsv rc
|
||||
while [ $# -gt 0 ]; do
|
||||
a="$1"; shift
|
||||
case "$a" in
|
||||
--preset)
|
||||
[ $# -gt 0 ] || { err '--preset needs a preset name'; return 1; }
|
||||
pname="$1"; shift ;;
|
||||
--refresh) refresh=1 ;;
|
||||
--json) as_json=1 ;;
|
||||
-*) err "unknown option '$a'"; return 1 ;;
|
||||
*) filter="$a" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# The active preset by default. The panel's editor names one instead,
|
||||
# because the preset being edited is often not the one in use - and two
|
||||
# LM Studio presets can point at two different servers.
|
||||
if [ -z "$pname" ] && [ "$(state_mode)" != "anthropic" ]; then
|
||||
pname="$(state_preset)"
|
||||
fi
|
||||
if [ -n "$pname" ]; then
|
||||
pf="$(preset_path "$pname")"
|
||||
[ -f "$pf" ] || { err "preset '$pname' not found"; return 1; }
|
||||
provider="$(jget "$pf" provider)"; [ -z "$provider" ] && provider=openrouter
|
||||
base="$(jget "$pf" baseUrl)"
|
||||
else
|
||||
provider=openrouter # on anthropic, the catalogue worth browsing
|
||||
fi
|
||||
|
||||
local quiet=$(( refresh || as_json )) kind title
|
||||
kind="$(prov_field "$provider" 10)"; title="$(prov_field "$provider" 3)"
|
||||
if [ "$quiet" -eq 0 ]; then
|
||||
case "$kind" in
|
||||
static) head_ "$title models (from its docs - no public catalogue endpoint)" ;;
|
||||
openrouter) head_ 'fetching https://openrouter.ai/api/v1/models ...' ;;
|
||||
*) head_ "models on the $title server at $base" ;;
|
||||
esac
|
||||
fi
|
||||
tsv="$(provider_catalogue "$provider" "$base" "$(cm_preset_token "$pf")")"; rc=$?
|
||||
|
||||
if [ "$as_json" -eq 1 ]; then
|
||||
jget "$CM_MODELS_CACHE" "providers.$provider"
|
||||
return "$rc"
|
||||
fi
|
||||
|
||||
if [ "$refresh" -eq 1 ]; then
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
err "could not fetch the $provider catalogue${base:+ from $base}; the cached list, if any, is kept"
|
||||
# Not necessarily a fault: plenty of proxies serve Messages and
|
||||
# nothing else.
|
||||
[ "$(prov_field "$provider" 8)" = lenient ] && \
|
||||
say 'some endpoints serve no model list at all - model ids can still be typed by hand'
|
||||
return 1
|
||||
fi
|
||||
ok "cached $(printf '%s\n' "$tsv" | grep -c .) $provider model(s)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
[ "$rc" -eq 0 ] || { err "could not fetch the $provider catalogue${base:+ from $base}"; return 1; }
|
||||
local id c2 c3 c4
|
||||
while IFS=$'\t' read -r id c2 c3 c4; do
|
||||
[ -n "$id" ] || continue
|
||||
[ -z "$filter" ] || case "$id" in *"$filter"*) ;; *) continue ;; esac
|
||||
case "$kind" in
|
||||
lmstudio) printf ' %-58s %-11s %s\n' "$id" "$c2" "$c3" ;;
|
||||
static) say "$(printf '%-8s - %s' "$id" "$c2")" ;;
|
||||
ollama) printf ' %-44s %-8s %s\n' "$id" "$c2" "$c3" ;;
|
||||
openrouter) printf ' %-52s %10s $%-8s $%s\n' "$id" "$c2" "$c3" "$c4" ;;
|
||||
*) printf ' %s\n' "$id" ;;
|
||||
esac
|
||||
done <<<"$tsv"
|
||||
}
|
||||
|
||||
cmd_set_key() {
|
||||
local ref="${1:-openrouter}" inline="${2:-}" secret unit
|
||||
init_root
|
||||
printf ' storage backend: %s\n' "$(cm_vault_backend_label)"
|
||||
if [ "$(cm_vault_backend)" = "file" ]; then
|
||||
warn 'no keyring available - the key will be stored in a 0600 file, NOT encrypted.'
|
||||
warn 'install libsecret-tools (secret-tool) or pass for encrypted storage.'
|
||||
fi
|
||||
if [ -n "$inline" ]; then
|
||||
secret="$inline"
|
||||
warn 'the key was given on the command line, so it is in this shell history - the hidden prompt leaves no trace'
|
||||
else
|
||||
printf ' paste the API key for ref '\''%s'\'' (input hidden): ' "$ref"
|
||||
IFS= read -rs secret; printf '\n'
|
||||
fi
|
||||
[ -n "$secret" ] || { err 'empty key, aborted'; return 1; }
|
||||
|
||||
# A hidden prompt will happily swallow a mis-paste. Guard the two shapes that
|
||||
# are never a real key - the failure is otherwise invisible until the
|
||||
# provider answers 401 and the UI just spins.
|
||||
case "$secret" in
|
||||
*[[:space:]]*) err 'that value contains whitespace, so it is not an API key (a pasted command line?). Nothing was stored.'; return 1 ;;
|
||||
claude-mode*) err 'that value is a claude-mode command, not an API key. Nothing was stored.'; return 1 ;;
|
||||
esac
|
||||
if [ "${#secret}" -lt 16 ]; then
|
||||
unit=characters; [ "${#secret}" -eq 1 ] && unit=character
|
||||
warn "that key is only ${#secret} $unit - unusually short. Storing anyway."
|
||||
fi
|
||||
if [ "$ref" = "openrouter" ] && [ "${secret#sk-or-}" = "$secret" ]; then
|
||||
warn "key does not start with 'sk-or-' - storing anyway"
|
||||
fi
|
||||
printf '%s' "$secret" | cm_vault_set "$ref" && ok "stored key '$ref' via $(cm_vault_backend_label)"
|
||||
}
|
||||
Reference in New Issue
Block a user