Files
claude-mode/tests/static.sh
T
smoidoandClaude Opus 5 b514e00745 README down to what a user needs; the rest into docs/
The README had grown to 1,010 lines of user docs and design notes in one file,
and had gone stale: it still showed the old numbered menu, set up only three
providers, and listed preflight checks and a file layout that predate the last
three releases. It now carries install, first run, the full command
reference, the providers at a glance, troubleshooting and a docs index.

docs/:
- providers.md    presets, defaults, the model cache, context windows, each
                  provider (OpenRouter's cost guard and guardrail check are
                  written up for the first time), adding a provider
- live-sessions.md  what a switch does to running sessions, and repair
- design.md       why settings.json, why keys stay out of it (and the vault
                  per platform), the preflight checks as they are now
- bar-widget.md   the widget as it is now: providers from health.json, every
                  server provider's settings, the restart after an upgrade
- architecture.md the pieces, every file on disk and who writes it, the
                  contracts between them, where to change what
- development.md  running the tests, the conventions the code follows,
                  working on the widget, releasing
CONTRIBUTING.md points at it.

Also:
- The per-project session listing used awk, which the CLI avoids because it
  is missing from minimal images; it uses the script's own TSV helpers now,
  and tests/static.sh fails on any awk in the CLI.
- tests/static.sh checks every relative Markdown link and #anchor.
- A unit test pins the managed env keys between cm-json.py and
  claude-mode.ps1, which only a comment kept in step before.
- test_sessions covers the per-project listing, which nothing ran.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 01:52:03 +03:00

183 lines
8.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Checks that need nothing but the tree: script syntax, JSON validity, the
# version recorded in both places, and providers.json against the kinds of
# behaviour the code knows. qmllint and shellcheck run when installed and are
# skipped - out loud - when not.
set -uo pipefail
cd "$(dirname "$0")/.."
fails=0
err="$(mktemp)"
tmp="$(mktemp -d)"
trap 'rm -rf "$err" "$tmp"' EXIT
check() {
local what="$1"; shift
if "$@" >"$err" 2>&1; then
printf ' ok %s\n' "$what"
else
printf ' FAIL %s\n' "$what"
sed 's/^/ | /' "$err" | head -n 20
fails=$((fails + 1))
fi
}
bash_syntax() {
local f
for f in linux/claude-mode linux/*.sh linux/lib/*.sh omarchy/install.sh scripts/*.sh \
tests/*.sh tests/cli/*.sh tests/windows/*.sh; do
[ -e "$f" ] || continue
bash -n "$f" || { echo "$f"; return 1; }
done
}
check 'shell scripts parse (bash -n)' bash_syntax
# awk is absent from minimal images (a stock Fedora WSL rootfs, for one), so the
# CLI does without it - and one slipped in once, in a listing no test ran.
no_awk() {
! grep -nw awk linux/claude-mode linux/lib/*.sh 2>/dev/null | grep -vE '^[^:]+:[0-9]+:\s*#'
}
check 'no awk in the POSIX CLI' no_awk
check 'python compiles' env PYTHONPYCACHEPREFIX="$tmp" python3 -m py_compile \
linux/cm-json.py tests/fake_server.py tests/python/test_cm_json.py
check 'JSON files parse' python3 -c '
import json, sys, glob
for f in ["providers.json", "omarchy/smoido.claude-mode/manifest.json"] + glob.glob("presets/*.json"):
try:
json.load(open(f, encoding="utf-8"))
except ValueError as e:
sys.exit("%s: %s" % (f, e))'
check 'VERSION matches the widget manifest' python3 -c '
import json
v = open("VERSION").read().strip()
m = json.load(open("omarchy/smoido.claude-mode/manifest.json"))["version"]
assert v == m, "VERSION is %s but manifest.json says %s - use scripts/bump-version.sh" % (v, m)'
check 'CHANGELOG has an entry for VERSION' python3 -c '
v = open("VERSION").read().strip()
assert ("## %s " % v) in open("CHANGELOG.md", encoding="utf-8").read(), "no CHANGELOG.md entry for " + v'
# The values each field may take are the ones the code has a branch for. A new
# kind of behaviour needs code in cm-json.py, the bash CLI and claude-mode.ps1
# before it can appear here - this is where that is enforced.
check 'providers.json is consistent with the code and the presets' python3 - <<'PY'
import json, os, sys
KINDS = {"openrouter", "lmstudio", "ollama", "openai", "static"}
PROBES = {"always", "lenient", "local"}
KEYS = {"required", "optional"}
MODELS = {"per-tier", "one-for-all"}
CHECKS = {"openrouter-key", "guardrail", "message-check", "catalogue-models",
"ollama-context", "lmstudio-templates"}
COLORS = {"cyan", "green", "yellow", "magenta", "white", "gray", "dkcyan"}
COMMANDS = {"menu", "status", "anthropic", "presets", "preset", "set-key", "models",
"doctor", "health", "preflight", "setup", "sessions", "repair-session",
"repair", "help"}
errors = []
def bad(pid, msg): errors.append("%s: %s" % (pid, msg))
doc = json.load(open("providers.json", encoding="utf-8"))
seen = {}
for p in doc["providers"]:
pid = p.get("id", "?")
for name in [pid] + list(p.get("aliases") or []):
if name in COMMANDS:
bad(pid, "'%s' would shadow the '%s' command" % (name, name))
if name in seen:
bad(pid, "'%s' is already used by %s" % (name, seen[name]))
seen[name] = pid
for field in ("title", "label", "blurb", "defaultPreset", "preset", "catalogue", "setup", "logo"):
if not p.get(field):
bad(pid, "missing " + field)
cat, srv, setup = p.get("catalogue") or {}, p.get("server") or {}, p.get("setup") or {}
if cat.get("kind") not in KINDS:
bad(pid, "catalogue.kind %r is not one of %s" % (cat.get("kind"), sorted(KINDS)))
if cat.get("kind") == "static" and not cat.get("static"):
bad(pid, "a static catalogue needs a static list")
if srv.get("probe", "local") not in PROBES:
bad(pid, "server.probe %r is not one of %s" % (srv.get("probe"), sorted(PROBES)))
if srv.get("probe") in ("always", "lenient") and not srv.get("paths"):
bad(pid, "a probed server needs server.paths")
if setup.get("key", "required") not in KEYS:
bad(pid, "setup.key %r is not one of %s" % (setup.get("key"), sorted(KEYS)))
if setup.get("models", "per-tier") not in MODELS:
bad(pid, "setup.models %r is not one of %s" % (setup.get("models"), sorted(MODELS)))
for c in p.get("doctor") or []:
if c not in CHECKS:
bad(pid, "unknown doctor check %r" % c)
if p.get("color", "gray") not in COLORS:
bad(pid, "color %r is not one of %s" % (p.get("color"), sorted(COLORS)))
auth = (p.get("preset") or {}).get("auth") or {}
if auth.get("mode") not in ("vault", "literal"):
bad(pid, "preset.auth.mode must be vault or literal")
preset = os.path.join("presets", "%s.json" % p.get("defaultPreset"))
if not os.path.exists(preset):
bad(pid, "default preset %s does not exist" % preset)
elif json.load(open(preset, encoding="utf-8")).get("provider") != pid:
bad(pid, "default preset %s belongs to another provider" % preset)
for f in sorted(os.listdir("presets")):
pp = json.load(open(os.path.join("presets", f), encoding="utf-8"))
if pp.get("provider") not in {p["id"] for p in doc["providers"]}:
errors.append("presets/%s: provider %r is not in providers.json" % (f, pp.get("provider")))
if errors:
sys.exit("\n".join(errors))
PY
# Relative links, and the #anchors in them, against the headings they point at -
# so moving a section between files cannot leave a dead link behind. Anchors are
# slugged the way Gitea and GitHub do: lowercase, punctuation dropped, spaces to
# dashes.
check 'Markdown links resolve' python3 - <<'PY'
import glob, os, re, sys
def slug(title):
title = re.sub(r"`", "", title.strip().lower())
return re.sub(r"[^\w\- ]", "", title).replace(" ", "-")
def anchors(path):
text = open(path, encoding="utf-8").read()
text = re.sub(r"```.*?```", "", text, flags=re.S)
return {slug(m.group(1)) for m in re.finditer(r"^#{1,6}\s+(.+?)\s*$", text, flags=re.M)}
files = ["README.md", "CONTRIBUTING.md", "CHANGELOG.md"] + glob.glob("docs/*.md")
errors = []
for f in files:
text = re.sub(r"```.*?```", "", open(f, encoding="utf-8").read(), flags=re.S)
for target in re.findall(r"\]\(([^)\s]+)\)", text):
if re.match(r"[a-z]+:", target):
continue # http:, https:, mailto:
path, _, anchor = target.partition("#")
dest = os.path.normpath(os.path.join(os.path.dirname(f), path)) if path else f
if not os.path.exists(dest):
errors.append("%s: %s - no such file" % (f, target))
elif anchor and dest.endswith(".md") and anchor not in anchors(dest):
errors.append("%s: %s - no heading for #%s" % (f, target, anchor))
if errors:
sys.exit("\n".join(errors))
PY
QMLLINT="$(command -v qmllint || true)"
[ -z "$QMLLINT" ] && [ -x /usr/lib/qt6/bin/qmllint ] && QMLLINT=/usr/lib/qt6/bin/qmllint
if [ -n "$QMLLINT" ]; then
# The shell's own modules (qs.*, Quickshell) do not resolve outside it, so
# only real syntax errors count; unresolved-type warnings are expected.
qml_syntax() {
local f out
for f in omarchy/smoido.claude-mode/*.qml; do
out="$("$QMLLINT" "$f" 2>&1 | grep -iE 'error|expected token|syntax|unexpected|duplicate' || true)"
[ -z "$out" ] || { echo "$f"; echo "$out"; return 1; }
done
}
check 'QML has no syntax errors (qmllint)' qml_syntax
else
printf ' skip QML syntax (qmllint not installed)\n'
fi
if command -v shellcheck >/dev/null 2>&1; then
check 'shellcheck (errors only)' shellcheck -S error -s bash \
linux/claude-mode linux/*.sh omarchy/install.sh scripts/*.sh tests/*.sh tests/cli/*.sh
else
printf ' skip shellcheck (not installed)\n'
fi
[ "$fails" -eq 0 ]