Files
smoidoandClaude Opus 5 441166d003 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>
2026-09-15 02:00:10 +03:00

219 lines
8.3 KiB
Bash

# shellcheck shell=bash
# linux/lib/setup.sh - first-run setup: key, server, models.
#
# 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.
# ---------------------------------------------------------------------------
# First-run setup
#
# A shipped preset is a starting point, not a working configuration. OpenRouter
# needs a key before it can serve anything; LM Studio needs to be told where the
# server is and which of the models it actually has installed to use - and the
# ids it ships with are whatever happened to be on the machine this was written
# on, which is almost certainly not yours.
#
# So a preset says whether it has been through setup. `configured: false` is
# written into the shipped presets and cleared once setup has run, and preflight
# treats it as a blocker: better to be walked through it once than to switch
# into something that half-works and produces a confusing failure later.
#
# Absent means configured. That is deliberate - presets that predate this, and
# ones the user built by hand with `preset new`, are their own business and must
# not suddenly start demanding a wizard.
# ---------------------------------------------------------------------------
preset_configured() {
local v; v="$(jget "$1" configured)"
[ "$v" = "false" ] && return 1
return 0
}
mark_configured() {
"$PY" "$JSON" set-flag "$1" configured true >/dev/null 2>&1
}
# A y/N prompt that defaults to no on anything that is not a clear yes.
ask_yes() {
local prompt="$1" reply
printf ' %s [y/N] ' "$prompt"
IFS= read -r reply || return 1
case "$reply" in y|Y|yes|YES) return 0 ;; *) return 1 ;; esac
}
# Prompt with a default shown in brackets; empty input keeps the default.
#
# The prompt goes to stderr for the same reason warn/err do: this is called
# inside $( ), where anything on stdout is captured as the return value. Printed
# to stdout it came back as part of the answer - " server base URL [...]: " with
# the typed URL glued on the end, which set-url then rejected.
ask_value() {
local prompt="$1" default="$2" reply
if [ -n "$default" ]; then printf ' %s [%s]: ' "$prompt" "$default" >&2
else printf ' %s: ' "$prompt" >&2; fi
IFS= read -r reply || return 1
[ -n "$reply" ] && printf '%s' "$reply" || printf '%s' "$default"
}
setup_key() {
local ref="$1" label="$2"
if cm_vault_has "$ref"; then
ok "a key is already stored for '$ref' ($(cm_vault_backend_label))"
ask_yes "replace it?" || return 0
else
say "$label needs an API key. It goes into $(cm_vault_backend_label),"
say 'not into settings.json.'
fi
cmd_set_key "$ref"
}
# Offer the provider's own catalogue rather than asking someone to type a model
# id from memory. Falls back to typing when the catalogue cannot be reached,
# because being offline should not block finishing setup.
setup_models() {
local pf="$1" provider="$2" name="$3"
printf '\n'
say 'current model map:'
local t v
for t in "${TIERS[@]}"; do
v="$(jget "$pf" "models.$t")"
[ -n "$v" ] && printf ' %-8s %s\n' "$t" "$v"
done
printf '\n'
ask_yes 'change which models back these tiers?' || return 0
if ! ui_interactive; then
warn 'model picking needs an interactive terminal'
return 0
fi
if [ "$(prov_field "$provider" 13)" = one-for-all ]; then
# One model for every tier is the normal shape for a local server: it
# has one loaded at a time, and mapping tiers to different models just
# means paying the load cost on every tier change.
local base ids=() id st ctx
base="$(jget "$pf" baseUrl)"
while IFS=$'\t' read -r id st ctx; do
[ -n "$id" ] || continue
ids+=("$id")
done < <(provider_catalogue "$provider" "$base" "$(cm_preset_token "$pf")")
if [ "${#ids[@]}" -eq 0 ]; then
warn 'the server returned no models; type an id by hand instead'
local manual; manual="$(ask_value 'model id for every tier' "$(jget "$pf" models.opus)")"
[ -n "$manual" ] && "$PY" "$JSON" set-all "$pf" "$manual" >/dev/null && ok "all tiers -> $manual"
return 0
fi
ui_reset_items
for id in "${ids[@]}"; do ui_add_item "$id" 'use this for every tier'; done
if ui_filter_select "model for all tiers of '$name'" 'esc = keep current'; then
"$PY" "$JSON" set-all "$pf" "${ids[$UI_SEL]}" >/dev/null
ok "all tiers -> ${ids[$UI_SEL]}"
fi
return 0
fi
# Remote gateways map a different model per tier, which is the whole point
# of them, so each tier is asked for separately.
local cur
for t in "${TIERS[@]}"; do
cur="$(jget "$pf" "models.$t")"
ui_pick_model "$pf" "$t" "$cur" || continue
[ -n "$UI_PICKED" ] || continue
"$PY" "$JSON" set-tier "$pf" "$t" "$UI_PICKED" >/dev/null && ok "$t -> $UI_PICKED"
done
}
# Where a server provider (LM Studio, Ollama, a custom endpoint) lives, and
# whether it wants a key. None of them has to be on this machine.
setup_server() {
local pf="$1" name="$2" mode="$3" url probe token title hint start
title="$(prov_field "$mode" 3)"; hint="$(prov_field "$mode" 20)"; start="$(prov_field "$mode" 21)"
url="$(jget "$pf" baseUrl)"; [ -n "$url" ] || url="$(prov_field "$mode" 19)"
printf '\n'
[ -n "$hint" ] && say "$hint"
url="$(ask_value 'server base URL' "$url")"
[ -n "$url" ] || { err 'a server address is needed'; return 1; }
"$PY" "$JSON" set-url "$pf" "$url" >/dev/null || return 1
ok "baseUrl -> $url"
printf '\n'
if ask_yes 'does that server require an API key?'; then
local ref; ref="$(ask_value 'key name to store it under' "$(prov_field "$mode" 17)")"
"$PY" "$JSON" set-auth "$pf" key "$ref" >/dev/null
ok "auth -> vault key '$ref'"
cm_vault_has "$ref" || cmd_set_key "$ref"
token="$(cm_vault_get "$ref" 2>/dev/null || true)"
else
"$PY" "$JSON" set-auth "$pf" none >/dev/null
token="$(prov_field "$mode" 18)"
ok "auth -> none (inline placeholder token '$token')"
fi
printf '\n'
say "checking $url ..."
probe="$(cm_probe_server "$url" "$token" "$(prov_field "$mode" 9)")"
case "$probe" in
ok) ok 'server answered' ;;
auth) err 'the server refused that credential'; return 1 ;;
notfound)
if [ "$(prov_field "$mode" 8)" = lenient ]; then
warn "something answered, but it lists no models - fine for a proxy; model ids are typed by hand"
else
err "something answered there, but not a $title API"; return 1
fi ;;
skip) warn 'curl is missing, so the server was not checked' ;;
*) err 'nothing answered at that address'
say "${start:+$start, then }run: claude-mode setup $mode"
return 1 ;;
esac
return 0
}
cmd_setup() {
local mode="${1:-}" name pf
case "$mode" in
anthropic)
head_ 'setup: anthropic'
ok 'nothing to configure - it uses your existing Claude login'
return 0 ;;
'') err 'usage: claude-mode setup <mode>'; return 1 ;;
*) mode="$(provider_resolve "$mode")" || { err "unknown mode '${1:-}'"; return 1; } ;;
esac
name="$(resolve_preset "$mode" "${2:-}")" || return 1
pf="$(preset_path "$name")"
if ! ui_interactive; then
err 'setup needs an interactive terminal'
say "run: claude-mode setup $mode"
return 1
fi
head_ "setup: $mode / preset '$name'"
say "$(mode_label "$mode")"
# A server provider is asked where it is and whether it wants a key; a
# hosted one always wants a key, and the entry says where to get one.
if [ "$(prov_field "$mode" 7)" = 1 ]; then
setup_server "$pf" "$name" "$mode" || return 1
else
local ref url
ref="$(jget "$pf" auth.keyRef)"; [ -n "$ref" ] || ref="$(prov_field "$mode" 17)"
url="$(prov_field "$mode" 14)"
printf '\n'
[ -n "$url" ] && say "get a key from $url"
setup_key "$ref" "$(prov_field "$mode" 3)"
fi
setup_models "$pf" "$mode" "$name"
mark_configured "$pf"
printf '\n'
ok "$mode is set up"
say "switch to it with: claude-mode $mode"
return 0
}