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>
219 lines
12 KiB
Bash
219 lines
12 KiB
Bash
# shellcheck shell=bash
|
|
# linux/lib/presets.sh - the preset subcommand, and re-applying the preset in use.
|
|
#
|
|
# 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.
|
|
|
|
cmd_preset() {
|
|
local sub="${1:-}" name="${2:-}"
|
|
if [ -n "$name" ] && ! valid_preset_name "$name"; then
|
|
err "invalid preset name '$name' - letters, digits, . _ and - only, not starting with a dot"
|
|
return 1
|
|
fi
|
|
case "$sub" in
|
|
show)
|
|
[ -n "$name" ] || { err 'usage: claude-mode preset show <name>'; return 1; }
|
|
[ -f "$(preset_path "$name")" ] || { err "preset '$name' not found"; return 1; }
|
|
cat "$(preset_path "$name")"
|
|
;;
|
|
new)
|
|
[ -n "$name" ] || { err 'usage: claude-mode preset new <name> [from] | <name> --provider <p> [--blank]'; return 1; }
|
|
[ -f "$(preset_path "$name")" ] && { err "preset '$name' already exists"; return 1; }
|
|
local from='' provider='' blank=0 a
|
|
shift 2
|
|
while [ $# -gt 0 ]; do
|
|
a="$1"; shift
|
|
case "$a" in
|
|
--provider)
|
|
[ $# -gt 0 ] || { err "--provider needs one of: $(provider_ids | tr '\n' ' ')"; return 1; }
|
|
provider="$1"; shift ;;
|
|
--blank) blank=1 ;;
|
|
-*) err "unknown option '$a'"; return 1 ;;
|
|
*) from="$a" ;;
|
|
esac
|
|
done
|
|
if [ -n "$provider" ]; then
|
|
local want="$provider"
|
|
provider="$(provider_resolve "$want")" || {
|
|
err "unknown provider '$want' - one of: $(provider_ids | tr '\n' ' ')"; return 1; }
|
|
fi
|
|
|
|
if [ "$blank" -eq 1 ]; then
|
|
[ -n "$provider" ] || { err '--blank needs --provider to know which template'; return 1; }
|
|
[ -z "$from" ] || { err '--blank starts from nothing; drop the source preset'; return 1; }
|
|
"$PY" "$JSON" scaffold "$provider" > "$(preset_path "$name")" || {
|
|
rm -f "$(preset_path "$name")"; return 1; }
|
|
ok "created blank $provider preset '$name'"
|
|
say 'every tier is empty: set them with claude-mode preset set, or the bar panel'
|
|
write_health "$(state_mode)" "$(state_preset)"
|
|
return 0
|
|
fi
|
|
|
|
# A literal `default` is only right for OpenRouter. Given a provider
|
|
# and no source, start from that provider's own default preset.
|
|
if [ -z "$from" ]; then
|
|
if [ -n "$provider" ]; then from="$(resolve_preset "$provider")" || return 1
|
|
else from=default; fi
|
|
fi
|
|
valid_preset_name "$from" || { err "invalid source preset name '$from'"; return 1; }
|
|
[ -f "$(preset_path "$from")" ] || { err "source preset '$from' not found"; return 1; }
|
|
if [ -n "$provider" ]; then
|
|
local got; got="$(jget "$(preset_path "$from")" provider)"; [ -z "$got" ] && got=openrouter
|
|
[ "$got" = "$provider" ] || { err "'$from' is for $got, not $provider"; return 1; }
|
|
fi
|
|
cp "$(preset_path "$from")" "$(preset_path "$name")"
|
|
ok "created $(preset_path "$name") from '$from'"
|
|
write_health "$(state_mode)" "$(state_preset)"
|
|
;;
|
|
rename)
|
|
local new="${3:-}" out
|
|
[ -n "$name" ] && [ -n "$new" ] || { err 'usage: claude-mode preset rename <name> <new-name>'; return 1; }
|
|
valid_preset_name "$new" || { err "invalid preset name '$new' - letters, digits, . _ and - only, not starting with a dot"; return 1; }
|
|
[ -f "$(preset_path "$name")" ] || { err "preset '$name' not found"; return 1; }
|
|
[ "$name" = "$new" ] && { ok "already called '$new'"; return 0; }
|
|
[ -e "$(preset_path "$new")" ] && { err "preset '$new' already exists"; return 1; }
|
|
out="$("$PY" "$JSON" preset-rename "$CM_PRESETS" "$name" "$new" "$CM_STATE" "$CM_DEFAULTS" 2>&1)" || {
|
|
err "$out"; return 1; }
|
|
ok "renamed '$name' -> '$new'"
|
|
case "$out" in
|
|
*'"active": true'*)
|
|
say 'it is the active preset; state.json now names it, and running sessions keep working' ;;
|
|
esac
|
|
case "$out" in
|
|
*'"default": true'*) say 'it was chosen as its provider'"'"'s default, and that follows the new name' ;;
|
|
esac
|
|
write_health "$(state_mode)" "$(state_preset)"
|
|
;;
|
|
rm)
|
|
[ -n "$name" ] || { err 'usage: claude-mode preset rm <name>'; return 1; }
|
|
[ -f "$(preset_path "$name")" ] || { err "preset '$name' not found"; return 1; }
|
|
if [ "$name" = "$(state_preset)" ] && [ "$(state_mode)" != "anthropic" ]; then
|
|
err "preset '$name' is active. Switch away first."; return 1
|
|
fi
|
|
local prov; prov="$(jget "$(preset_path "$name")" provider)"; [ -z "$prov" ] && prov=openrouter
|
|
rm -f "$(preset_path "$name")"; ok "deleted preset '$name'"
|
|
if [ "$(jget "$CM_DEFAULTS" "$prov")" = "$name" ]; then
|
|
"$PY" "$JSON" set-default "$CM_DEFAULTS" "$prov" "" >/dev/null
|
|
say "it was the chosen default for $prov; 'claude-mode $prov' means '$(resolve_preset "$prov" 2>/dev/null || printf 'nothing')' now"
|
|
fi
|
|
# Allowed, but not quietly: with no preset left, `claude-mode <provider>`
|
|
# has nothing to resolve to.
|
|
if [ -z "$(presets_for_provider "$prov")" ]; then
|
|
warn "that was the last $prov preset - 'claude-mode $prov' has nothing to switch to now"
|
|
say "create one with: claude-mode preset new <name> --provider $prov [--blank]"
|
|
fi
|
|
write_health "$(state_mode)" "$(state_preset)"
|
|
;;
|
|
default)
|
|
# Which preset `claude-mode <provider>` means when none is named.
|
|
local prov="$name" target="${3:-}" p cur got
|
|
if [ -z "$prov" ]; then
|
|
head_ 'default preset per provider'
|
|
for p in $(provider_ids); do
|
|
cur="$(jget "$CM_DEFAULTS" "$p")"
|
|
if [ -n "$cur" ] && [ -f "$(preset_path "$cur")" ]; then
|
|
printf ' %-11s %s (chosen)\n' "$p" "$cur"
|
|
else
|
|
# "built-in" only when it really is the built-in name;
|
|
# with that preset gone, resolve_preset takes the first.
|
|
local got_p why
|
|
got_p="$(resolve_preset "$p" 2>/dev/null)" || got_p=''
|
|
if [ -z "$got_p" ]; then why='no preset left'
|
|
elif [ "$got_p" = "$(builtin_default_preset "$p")" ]; then why='built-in'
|
|
else why='first by name'; fi
|
|
printf ' %-11s %s (%s%s)\n' "$p" "${got_p:--}" "$why" \
|
|
"$([ -n "$cur" ] && printf "; chosen '%s' no longer exists" "$cur")"
|
|
fi
|
|
done
|
|
printf '\n %sclaude-mode preset default <provider> <name> (or --clear)%s\n' "$C_DIM" "$C_RESET"
|
|
return 0
|
|
fi
|
|
prov="$(provider_resolve "$name")" || {
|
|
err "unknown provider '$name' - one of: $(provider_ids | tr '\n' ' ')"; return 1; }
|
|
if [ -z "$target" ]; then
|
|
resolve_preset "$prov" && printf '\n'
|
|
return
|
|
fi
|
|
if [ "$target" = "--clear" ]; then
|
|
"$PY" "$JSON" set-default "$CM_DEFAULTS" "$prov" "" >/dev/null || return 1
|
|
ok "$prov : no longer chosen; 'claude-mode $prov' means '$(resolve_preset "$prov" 2>/dev/null || printf 'nothing - no preset left')'"
|
|
else
|
|
valid_preset_name "$target" || { err "invalid preset name '$target'"; return 1; }
|
|
[ -f "$(preset_path "$target")" ] || { err "preset '$target' not found"; return 1; }
|
|
got="$(jget "$(preset_path "$target")" provider)"; [ -z "$got" ] && got=openrouter
|
|
[ "$got" = "$prov" ] || { err "'$target' is for $got, not $prov"; return 1; }
|
|
"$PY" "$JSON" set-default "$CM_DEFAULTS" "$prov" "$target" >/dev/null || return 1
|
|
ok "$prov : 'claude-mode $prov' now means '$target'"
|
|
fi
|
|
write_health "$(state_mode)" "$(state_preset)"
|
|
;;
|
|
set)
|
|
local tier="${3:-}" model="${4:-}"
|
|
[ -n "$name" ] && [ -n "$tier" ] && [ -n "$model" ] || { err 'usage: claude-mode preset set <name> <tier> <model-id>'; return 1; }
|
|
# set-tier saves whatever it loaded, so a mistyped name would
|
|
# otherwise quietly become a new, half-empty preset.
|
|
[ -f "$(preset_path "$name")" ] || { err "preset '$name' not found"; return 1; }
|
|
"$PY" "$JSON" set-tier "$(preset_path "$name")" "$tier" "$model" || return 1
|
|
ok "$name : $tier -> $model"
|
|
reapply_if_active "$name" models
|
|
;;
|
|
all)
|
|
local model="${3:-}"
|
|
[ -n "$name" ] && [ -n "$model" ] || { err 'usage: claude-mode preset all <name> <model-id>'; return 1; }
|
|
[ -f "$(preset_path "$name")" ] || { err "preset '$name' not found"; return 1; }
|
|
local t
|
|
for t in "${TIERS[@]}" subagent; do
|
|
"$PY" "$JSON" set-tier "$(preset_path "$name")" "$t" "$model" || return 1
|
|
done
|
|
ok "$name : all tiers + subagent -> $model"
|
|
reapply_if_active "$name" models
|
|
;;
|
|
url)
|
|
local url="${3:-}"
|
|
[ -n "$name" ] && [ -n "$url" ] || { err 'usage: claude-mode preset url <name> <base-url>'; return 1; }
|
|
[ -f "$(preset_path "$name")" ] || { err "preset '$name' not found"; return 1; }
|
|
"$PY" "$JSON" set-url "$(preset_path "$name")" "$url" >/dev/null || return 1
|
|
ok "$name : baseUrl -> $url"
|
|
reapply_if_active "$name"
|
|
;;
|
|
auth)
|
|
local amode="${3:-}" ref="${4:-}"
|
|
[ -n "$name" ] && [ -n "$amode" ] || { err 'usage: claude-mode preset auth <name> none|key [keyRef]'; return 1; }
|
|
[ -f "$(preset_path "$name")" ] || { err "preset '$name' not found"; return 1; }
|
|
# The provider's own slot name by default: lmstudio, ollama, custom.
|
|
[ -n "$ref" ] || ref="$(prov_field "$(jget "$(preset_path "$name")" provider)" 17)"
|
|
[ -n "$ref" ] || ref=lmstudio
|
|
"$PY" "$JSON" set-auth "$(preset_path "$name")" "$amode" "$ref" >/dev/null || return 1
|
|
if [ "$amode" = "key" ]; then
|
|
ok "$name : auth -> vault key '$ref'"
|
|
cm_vault_has "$ref" || warn "no key stored yet for '$ref' - run: claude-mode set-key $ref"
|
|
else
|
|
ok "$name : auth -> none (inline placeholder token)"
|
|
fi
|
|
reapply_if_active "$name"
|
|
;;
|
|
*) usage ;;
|
|
esac
|
|
}
|
|
|
|
# scope `models` marks an edit that changed tier mappings only. What makes a
|
|
# switch dangerous to running sessions is the endpoint or the key changing under
|
|
# them, and a tier edit changes neither - so there is nothing to ask them about,
|
|
# and the bar panel (which cannot answer a prompt) can edit the active preset.
|
|
reapply_if_active() {
|
|
local name="$1" scope="${2:-}" mode
|
|
mode="$(state_mode)"
|
|
if [ "$mode" != "anthropic" ] && [ "$(state_preset)" = "$name" ]; then
|
|
say 're-applying active preset...'
|
|
[ "$scope" = models ] && CM_SAME_ENDPOINT=1
|
|
if ! set_mode "$mode" "$name"; then
|
|
CM_SAME_ENDPOINT=0
|
|
# Last on stderr on purpose: it is the line the panel shows, and
|
|
# the file *was* written, which the failure above does not say.
|
|
err "saved, but re-applying the active preset failed - run: claude-mode $mode $name"
|
|
return 1
|
|
fi
|
|
CM_SAME_ENDPOINT=0
|
|
fi
|
|
}
|