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
8.3 KiB
Bash
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
|
|
}
|