#!/usr/bin/env bash
# plugin hotswap · ride BGF · NO SERVER · spit/pack/history
set -euo pipefail
_SELF="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")"
HERE="$(cd "$(dirname "$_SELF")" && pwd)"
export PYTHONPATH="$HERE/lib${PYTHONPATH:+:$PYTHONPATH}"

die() { echo "wb: $*" >&2; exit 1; }
[[ -f "$HERE/lib/hotswap_core.py" ]] || die "missing lib/hotswap_core.py"

py() {
  python3 -c "
import sys
sys.path.insert(0, '$HERE/lib')
import hotswap_core as H
import json
H.ensure_dirs()
$1
"
}

cmd="${1:-list}"
shift || true

case "$cmd" in
  help|-h|--help)
    cat <<EOF
wb · ESSIE hotswap system · ride BGF · NO SERVER

  ESSIE  = this hotswap system (seats · pack · history · spit)
  EZZIE  = ez free eor atom · family PHI + THERMO (not ESSIE)

  wb list                 sockets + UI + GPU + seated
  wb in <id>              seat UI (html5_all|mp4|grin|ezzie|phi|thermo|…) or GPU
  wb unseat [socket]      default ui.stage
  wb next | prev          cycle UI plugs
  wb undo | redo          history
  wb history              show stack
  wb validate             check plugs
  wb spit                 JSON current files
  wb out [spvs]           spit → out/ (+ library write-back)
  wb pack                 offline HTML5 pack (file://) · internet viewing
  wb open                 open latest pack (browser perfect CSS display)
  wb css-parse            optional CSS.spv token parse · SPEED for Host only
  wb new <id>             scaffold rides=BGF
  wb status | sockets
  wb test                 100% SPV/GRIN + HTML5_ALL + MP4 suite

AUDIO · dual ALSA outs (Host write path · works first time)
  wb audio                status + OUT1/OUT2
  wb audio list           ALSA playback devices (aplay -L)
  wb audio init           smart dual defaults (analog + HDMI TV)
  wb audio set out1=DEV out2=DEV route=both|out1|out2
  wb audio test [both|out1|out2]   tone → real ALSA write
  wb audio save           rewrite settings + asound.essie.conf

STREAM · camera + mic → Twitch (secure · was MP4 tab)
  wb stream               status (key masked)
  wb stream key set       read key from stdin · 0600 secrets file
  wb stream key clear
  wb stream start|stop    nv_mp4 NVENC prefer · cross-plat · RTMP Twitch
  wb stream init          default cam + mic · prefer NVENC

  wb nv | nv_mp4          SMART H.264 cross-plat SPV host
  wb nv probe|status|init NV / Arc / AMD / VT / soft · loose ffmpeg
  wb nv prefer auto|nvidia|intel|amd|soft
  wb nv cmd               dry encode line (smart pick)
  wb in nv_mp4|nv         seat SPV (HTML5 play · no ffmpeg req)
  Linux capture: v4l2 + ALSA · Win dshow · Apple avfoundation

  Key NEVER in pack/git · settings/secrets/twitch.stream_key
  or export TWITCH_STREAM_KEY=… (env not written to disk)
  ffmpeg is LOOSE · playback SPV works without it

DISPLAY LAW
  perfect display = real browser CSS cascade (css/spv.css)
  css_spv_parse     = token extract only · Host/native speed · NOT display

LINEAR CHAIN (always · never exit · SDF_OUT between SPVs)
  wb rfc-list
  wb chain-start 0001     # or 0002-linear-gpu-ui · 0003 grin
  wb chain-tick [n]
  wb chain-auto [n]       # auto hotswap along RFC
  wb chain-status
  wb push                 # show push_constants.json

  WBS = Web BGS host ground · WBF = Web BGF step measure
  SDF_OUT chains SPV→SPV · Host never exits

STONE SDF · language storage + recovery (HTML/JS/CSS + atoms)
  wb stone list           12 birthstone cells
  wb stone store [all|html|css|js|essie|ezzie|phi|thermo|…]
  wb stone recover <facet> [write]   # all stays dry-run
  wb stone verify         GRIN free=1 check
  wb stone vocab          STONE-SDF-VOCAB sheet head
  Facets: Garnet=HTML Amethyst=CSS Aquamarine=JS Diamond=ESSIE
          Emerald=EZZIE Pearl=PHI Ruby=THERMO … Turquoise=H7

Law: SPV free · rides BGF · never folds · no desk server · .com Atomic
EOF
    ;;
  list|ls)
    python3 -c "
import sys
sys.path.insert(0, '$HERE/lib')
import hotswap_core as H
H.ensure_dirs()
man = H.load_manifest()
print('=== SOCKETS ===')
for sid, s in H.SOCKETS.items():
    print('  %-14s %-8s %s' % (sid, s['kind'], s['desc']))
print('=== UI library ===')
for p in man.get('plugs', []):
    print('  %-12s rides=%-4s js=%-16s dom=%s' % (p['id'], p.get('rides','BGF'), p.get('js','-'), p.get('dom','-')))
print('=== GPU library ===')
for g in H.list_gpu_plugs():
    print('  %-12s comp=%-16s spv=%s' % (g['id'], g.get('comp') or '-', g.get('spv') or '-'))
print('=== SEATED ===')
seats = H.load_seats().get('seats') or {}
if not seats:
    print('  (none)')
for sock, plug in seats.items():
    pid = plug.get('id') if isinstance(plug, dict) else plug
    print('  %-14s → %s' % (sock, pid))
st = H.status()
print('=== history i=%s len=%s free=%s ===' % (st['history_i'], st['history_len'], st.get('free')))
"
    ;;
  in|hotswap|use|seat)
    id="${1:?plug id}"
    # stream tab alias → mp4 · nv aliases → nv_mp4
    [[ "$id" == "stream" || "$id" == "twitch" ]] && id="mp4"
    [[ "$id" == "nv" || "$id" == "nvenc" || "$id" == "nvidia" ]] && id="nv_mp4"
    py "
pid='$id'
if pid in H.plugs_by_id():
    p=H.seat_ui(pid)
    print('SEAT ui.stage →', p['id'], '· rides BGF')
    for k in ('js','dom'):
        if p.get(k): print('  IN', p[k])
else:
    g=H.seat_gpu(pid)
    print('SEAT gpu.compute →', g['id'])
    print('  comp', g.get('comp'), 'spv', g.get('spv'))
"
    ;;
  unseat|rm)
    sock="${1:-ui.stage}"
    py "H.unseat('$sock'); print('UNSEAT', '$sock')"
    ;;
  next) py "p=H.next_ui(1); print('NEXT', p['id'])" ;;
  prev) py "p=H.next_ui(-1); print('PREV', p['id'])" ;;
  undo) py "s=H.history_undo(); print('UNDO', list((s or {}).keys()) if s is not None else None)" ;;
  redo) py "s=H.history_redo(); print('REDO', list((s or {}).keys()) if s is not None else None)" ;;
  history|hist)
    py '
h=H.load_history()
for i,e in enumerate(h.get("stack") or []):
    mark="<<" if i==h.get("i") else "  "
    seats=e.get("seats") or {}
    ids={k:(v.get("id") if isinstance(v,dict) else v) for k,v in seats.items()}
    print(f"{mark} [{i}] {ids}")
'
    ;;
  validate|check)
    py '
import json,sys
r=H.validate_all()
print(json.dumps(r, indent=2))
sys.exit(0 if r.get("ok") else 1)
'
    ;;
  status|st)
    py "import json; print(json.dumps(H.status(), indent=2))"
    ;;
  sockets)
    py "import json; print(json.dumps(H.SOCKETS, indent=2))"
    ;;
  spit|dump)
    py '
import json
seats=H.load_seats()
files={}
for f in H.CUR.iterdir():
    if f.is_file() and f.name not in ("history.json","manifest.json","gpu.seat.json"):
        files[f.name]=f.read_text(errors="replace")
print(json.dumps({"seats":seats.get("seats"),"active":seats.get("active"),"files":files,"ride":"BGF","free":seats.get("free")}, indent=2))
'
    ;;
  out|export)
    dest="${1:-}"
    py "
path=H.spit_bundle()
print('SPIT', path)
"
    if [[ "$dest" == "spvs" || "$dest" == "library" ]]; then
      py "w=H.write_back_library(); print('OUT→spvs', ', '.join(w) if w else '(nothing)')"
    fi
    ;;
  new|scaffold)
    id="${1:?id}"
    py "
pid='$id'
js, dom = f'{pid}.js', f'{pid}.dom.html'
if (H.SPVS / js).exists():
    raise SystemExit('exists '+js)
(H.SPVS / dom).write_text('''<!-- SPV DOM · %s · rides BGF -->
<div class=\"spv-dom\" data-plug=\"%s.dom\">
  <div class=\"spv-title\">%s</div>
  <p class=\"spv-body\">New plug · wb in %s · ride BGF · no server</p>
</div>
''' % (pid, pid, pid, pid))
(H.SPVS / js).write_text(r'''(function (global) {
  const NAME = \"%s\";
  function bgf(a,b){ return (a-b)|1; }
  function mount(root, api) {
    const el = root.querySelector(\"[data-plug='%s.dom']\") || root;
    const h = document.createElement(\"div\");
    h.className = \"spv-hud\"; el.appendChild(h);
    let f=0, raf=0;
    function loop(ts){
      f++;
      const field = bgf((ts|0)&255, f&127);
      const free = (field ^ f) | 1;
      h.textContent = \"%s BGF=\"+field+\" free=0x\"+(free>>>0).toString(16);
      if(api&&api.onSample) api.onSample({field, free, frame:f, atom:\"%s\"});
      raf = requestAnimationFrame(loop);
    }
    raf = requestAnimationFrame(loop);
    return function(){ cancelAnimationFrame(raf); h.remove(); };
  }
  global.BGRTX_SPV_PLUGS = global.BGRTX_SPV_PLUGS || {};
  global.BGRTX_SPV_PLUGS[NAME] = { name: NAME, kind: \"js\", mount, rides: \"BGF\" };
})(typeof window !== \"undefined\" ? window : globalThis);
''' % (js, pid, pid, pid.upper()))
man=H.load_manifest()
plugs=man.setdefault('plugs',[])
if not any(p.get('id')==pid for p in plugs):
    plugs.append({'id':pid,'js':js,'dom':dom,'title':pid,'rides':'BGF'})
H.save_manifest(man)
print('NEW', pid, js, dom)
"
    ;;
  # ---- linear chain · WBS/WBF · SDF_OUT · never exit ----
  rfc-list|rfcs)
    python3 -c "
import sys
sys.path.insert(0,'$HERE/lib')
import chain_linear as C
for p in C.list_rfcs():
    print(' ', p.name)
"
    ;;
  chain-start|chain)
    rfc="${1:-0001}"
    python3 -c "
import sys, json
sys.path.insert(0,'$HERE/lib')
import chain_linear as C
st=C.start_chain('$rfc')
print('CHAIN START', st.get('rfc'), 'title=', st.get('rfc_title'))
print('  step_i', st.get('step_i'), 'last', st.get('last_kind'), st.get('last_plug'))
print('  WBS', st.get('wbs'), 'WBF', st.get('wbf'), 'never_exit', st.get('never_exit'))
print('  push_constants', json.dumps(st.get('push_constants')))
"
    ;;
  chain-tick|tick)
    n="${1:-1}"
    python3 -c "
import sys, json
sys.path.insert(0,'$HERE/lib')
import chain_linear as C
n=int('$n')
for i in range(max(1,n)):
    st=C.tick_chain()
    print(f\"tick{i} step={st.get('step_i')} kind={st.get('last_kind')} plug={st.get('last_plug')} WBF={st.get('wbf')} push_step={st.get('push_constants',{}).get('step')}\")
"
    ;;
  chain-auto|auto)
    n="${1:-6}"
    python3 -c "
import sys, json
sys.path.insert(0,'$HERE/lib')
import chain_linear as C
rows=C.auto_run(int('$n'))
for i,r in enumerate(rows):
    print(f\"auto[{i}] step→{r['step_i']} {r['last_kind']} {r.get('last_plug')} WBF={r['wbf']}\")
    print('   push', json.dumps(r['push']))
"
    ;;
  chain-status|cstat)
    python3 -c "
import sys, json
sys.path.insert(0,'$HERE/lib')
import chain_linear as C
print(json.dumps(C.chain_status(), indent=2))
"
    ;;
  push|push-constants)
    python3 -c "
import sys, json
from pathlib import Path
sys.path.insert(0,'$HERE/lib')
import hotswap_core as H
p=H.CUR/'push_constants.json'
print(p.read_text() if p.exists() else '{}')
"
    ;;
  pack|bundle)
    python3 -c "import sys; sys.path.insert(0,'$HERE/lib'); import pack_offline as P; print('PACK', P.build_pack())"
    ;;
  css-parse|css|spv-css)
    python3 -c "
import sys, json
sys.path.insert(0, '$HERE/lib')
import css_spv_parse as C
print(json.dumps(C.summary(), indent=2))
"
    ;;
  open)
    # not Firefox-first · engine sheet preferred · optional pack path
    mode="${1:-}"
    python3 -c "
import sys, subprocess
from pathlib import Path
sys.path.insert(0, '$HERE/lib')
import hotswap_core as H
H.ensure_dirs()
mode = '$mode'
root = H.ROOT
engine_sheet = root.parent / 'engine' / 'sheets' / 'essie_menu.html'
rtx = root.parent / 'rtx'
if mode in ('engine', 'sdf', 'sheet'):
    print('ENGINE sheet', engine_sheet)
    print('run:  cd', root.parent.parent, '&& ./RTXLayer/rtx engine')
    print('(native SDF · no browser)')
    raise SystemExit(0)
if mode == 'kate':
    paths = [str(engine_sheet), str(root / 'spvs' / 'manifest.json')]
    packs = sorted(H.OUT.glob('pack-*.html'))
    if packs: paths.append(str(packs[-1]))
    subprocess.Popen(['kate'] + paths, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    print('kate', ' '.join(paths))
    raise SystemExit(0)
packs = sorted(H.OUT.glob('pack-*.html'))
if not packs:
    raise SystemExit('no pack · wb pack first · or: wb open engine')
latest = packs[-1]
print('pack', latest)
print('prefer: wb open engine  ·  wb open kate  ·  (avoid Firefox-first)')
# xdg-open only if user asks pack explicitly
if mode == 'pack':
    subprocess.Popen(['xdg-open', str(latest)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
else:
    print('not auto-opening browser · wb open pack  if you really want file://')
"
    ;;
  kate)
    id="${1:-}"
    python3 -c "
import sys, subprocess
from pathlib import Path
sys.path.insert(0, '$HERE/lib')
import hotswap_core as H
root = H.ROOT
sheet = root.parent / 'engine' / 'sheets' / 'essie_menu.html'
pid = '$id'
paths = []
if pid in ('', 'sheet', 'menu'):
    paths = [str(sheet), str(H.SPVS / 'manifest.json'), str(root / 'js' / 'commands.js')]
elif pid:
    js, dom = H.SPVS / (pid + '.js'), H.SPVS / (pid + '.dom.html')
    if not js.exists() and pid == 'jseng':
        js, dom = H.SPVS / 'js_all.js', H.SPVS / 'js_all.dom.html'
    if js.exists(): paths.append(str(js))
    if dom.exists(): paths.append(str(dom))
    paths += [str(sheet), str(H.SPVS / 'manifest.json')]
else:
    paths = [str(sheet)]
if not paths:
    raise SystemExit('nothing to open')
subprocess.Popen(['kate'] + paths, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
print('kate', ' '.join(paths))
"
    ;;
  sheet)
    echo "ENGINE single-sheet SDF: $HERE/../engine/sheets/essie_menu.html"
    echo "run:  cd $(dirname "$HERE")/.. && ./RTXLayer/rtx engine"
    echo "edit: wb kate sheet"
    ;;
  test|tests|grin-test)
    python3 "$HERE/tests/test_spv_grin.py"
    ;;
  probe|probes)
    # Host protocol probes · optional list: wb probe ssh dns icmp
    python3 -c "
import sys, json
sys.path.insert(0, '$HERE/lib')
import proto_probe as P
args = sys.argv[1:]
blob = P.run_all(args or None)
print(json.dumps(blob, indent=2))
" "$@"
    ;;
  stream|twitch)
    sub="${1:-status}"
    shift || true
    case "$sub" in
      init)
        python3 -c "
import sys, json
sys.path.insert(0, '$HERE/lib')
import stream_io as S
s=S.load_settings()
S.save_settings(s)
print(json.dumps(S.status(), indent=2))
"
        ;;
      key)
        action="${1:-set}"
        shift || true
        if [[ "$action" == "clear" || "$action" == "rm" ]]; then
          python3 -c "
import sys
sys.path.insert(0, '$HERE/lib')
import stream_io as S
S.ensure_dirs()
if S.KEY_FILE.exists():
    S.KEY_FILE.unlink()
    print('cleared', S.KEY_FILE)
else:
    print('no key file')
s=S.load_settings(); s['key_set']=False; S.save_settings(s)
"
        else
          # read key securely from stdin (no echo in shell history if using read -s)
          if [[ -n "${1:-}" && "$1" != "-" ]]; then
            echo "wb stream key: do not pass key on CLI (shell history). Use: wb stream key set" >&2
            echo "Then paste key and press Enter." >&2
            exit 2
          fi
          echo -n "Twitch stream key (hidden input not available in pipe — paste + Enter): " >&2
          IFS= read -r KEY || true
          python3 -c "
import sys
sys.path.insert(0, '$HERE/lib')
import stream_io as S
key=sys.stdin.read().strip()
p=S.set_key(key)
print('KEY SAVED', p, 'mode 0600')
print('mask', S.mask_key())
" <<< "$KEY"
          unset KEY
        fi
        ;;
      start|go|live)
        python3 -c "
import sys, json
sys.path.insert(0, '$HERE/lib')
import stream_io as S
r=S.start_stream()
print(json.dumps(r, indent=2))
sys.exit(0 if r.get('ok') else 1)
"
        ;;
      stop|kill)
        python3 -c "
import sys, json
sys.path.insert(0, '$HERE/lib')
import stream_io as S
print(json.dumps(S.stop_stream(), indent=2))
"
        ;;
      status|st|'')
        python3 -c "
import sys, json
sys.path.insert(0, '$HERE/lib')
import stream_io as S
print(json.dumps(S.status(), indent=2))
"
        ;;
      cmd|dry)
        python3 -c "
import sys
sys.path.insert(0, '$HERE/lib')
import stream_io as S
print(' '.join(S.build_ffmpeg_cmd(dry=True)))
"
        ;;
      *)
        echo "wb stream: init|key set|key clear|start|stop|status|cmd  (got: $sub)" >&2
        exit 2
        ;;
    esac
    ;;
  nv|nv_mp4|nvenc)
    sub="${1:-probe}"
    shift || true
    case "$sub" in
      probe|status|st|'')
        python3 -c "
import sys, json
sys.path.insert(0, '$HERE/lib')
import nv_mp4 as N
print(json.dumps(N.probe(), indent=2))
"
        ;;
      init)
        python3 -c "
import sys, json
sys.path.insert(0, '$HERE/lib')
import nv_mp4 as N
print(json.dumps(N.init_settings(), indent=2))
"
        ;;
      prefer|vendor)
        pref="${1:-auto}"
        python3 -c "
import sys, json
sys.path.insert(0, '$HERE/lib')
import nv_mp4 as N
print(json.dumps(N.set_prefer('$pref'), indent=2))
"
        ;;
      cmd|dry)
        python3 -c "
import sys, json
sys.path.insert(0, '$HERE/lib')
import nv_mp4 as N
r=N.build_encode_cmd(rtmp_url='rtmp://live.twitch.tv/app/REDACTED', dry=True)
print(json.dumps(r, indent=2))
sys.exit(0 if r.get('ok') else 1)
"
        ;;
      export|pack)
        python3 -c "
import sys, json
sys.path.insert(0, '$HERE/lib')
import nv_mp4 as N
print(json.dumps(N.export_for_pack(), indent=2))
"
        ;;
      *)
        echo "wb nv: probe|init|prefer <auto|nvidia|intel|amd|soft>|cmd|export  (got: $sub)" >&2
        exit 2
        ;;
    esac
    ;;
  audio|alsa)
    sub="${1:-status}"
    shift || true
    case "$sub" in
      list|ls)
        python3 -c "
import sys
sys.path.insert(0, '$HERE/lib')
import alsa_io as A
for d in A.list_playback():
    print('%-48s %s' % (d['id'], (d.get('desc') or '')[:64]))
"
        ;;
      init|first|defaults)
        python3 -c "
import sys, json
sys.path.insert(0, '$HERE/lib')
import alsa_io as A
s=A.reset_first_time()
print('INIT dual ALSA')
print('  OUT1', s.get('out1'))
print('  OUT2', s.get('out2'))
print('  route', s.get('route'))
print('  settings', A.AUDIO_SETTINGS)
print('  asound  ', A.ASOUND_SNIPPET)
"
        ;;
      set)
        # wb audio set out1=... out2=... route=both
        python3 -c "
import sys, json
sys.path.insert(0, '$HERE/lib')
import alsa_io as A
args = sys.argv[1:]
kw = {}
for a in args:
    if '=' not in a: continue
    k,v = a.split('=',1)
    k=k.strip().lstrip('-')
    if k in ('out1','out2','route','theme'):
        kw[k]=v
    elif k in ('volume1','volume2'):
        kw[k]=float(v)
s=A.set_outputs(**kw)
print(json.dumps({'out1':s.get('out1'),'out2':s.get('out2'),'route':s.get('route'),'saved':str(A.AUDIO_SETTINGS)}, indent=2))
" "$@"
        ;;
      test|tone)
        which="${1:-both}"
        python3 -c "
import sys, json
sys.path.insert(0, '$HERE/lib')
import alsa_io as A
r=A.test_outputs('$which')
print(json.dumps(r, indent=2))
sys.exit(0 if r.get('ok') else 1)
"
        ;;
      save)
        python3 -c "
import sys
sys.path.insert(0, '$HERE/lib')
import alsa_io as A
s=A.load_settings()
p=A.save_settings(s)
print('SAVE', p)
print('ASOUND', A.ASOUND_SNIPPET)
"
        ;;
      status|st|'')
        python3 -c "
import sys, json
sys.path.insert(0, '$HERE/lib')
import alsa_io as A
st=A.status()
print('=== ESSIE dual ALSA ===')
print('OUT1', st.get('out1'), 'ok='+str(st.get('out1_ok')))
print('OUT2', st.get('out2'), 'ok='+str(st.get('out2_ok')))
print('route', st.get('route'))
print('devices', st.get('device_count'))
print('settings', st.get('settings_path'))
print('asound  ', st.get('asound_path'))
print('aplay', st.get('aplay'))
"
        ;;
      *)
        echo "wb audio: list|init|set|test|save|status  (got: $sub)" >&2
        exit 2
        ;;
    esac
    ;;
  # ---- STONE SDF · language storage + recovery ----
  stone|stones|vault)
    sub="${1:-list}"
    shift || true
    case "$sub" in
      list|ls|status|st|'')
        python3 -c "
import sys
sys.path.insert(0, '$HERE/lib')
import stone_sdf as S
S.print_list()
"
        ;;
      store|save|snapshot)
        key="${1:-all}"
        python3 -c "
import sys, json
sys.path.insert(0, '$HERE/lib')
import stone_sdf as S
r=S.store('$key')
print(json.dumps(r, indent=2))
sys.exit(0 if r.get('ok') else 1)
"
        ;;
      recover|restore)
        key="${1:-all}"
        force="${2:-}"
        python3 -c "
import sys, json
sys.path.insert(0, '$HERE/lib')
import stone_sdf as S
dry = '$force' not in ('write','--write','force')
# single facet defaults to write; all stays dry unless write
if '$key' not in ('all','*') and '$force' not in ('dry','--dry'):
    dry = False if '$force' in ('write','--write','force','') else dry
    if '$force' == '':
        dry = False
r=S.recover('$key', dry_run=dry)
print(json.dumps(r, indent=2))
sys.exit(0 if r.get('ok') else 1)
"
        ;;
      verify|check|grin)
        python3 -c "
import sys, json
sys.path.insert(0, '$HERE/lib')
import stone_sdf as S
r=S.verify()
print(json.dumps(r, indent=2))
sys.exit(0 if r.get('ok') else 1)
"
        ;;
      vocab|doc)
        if [[ -f "$HERE/STONE-SDF-VOCAB.txt" ]]; then
          sed -n '1,80p' "$HERE/STONE-SDF-VOCAB.txt"
          echo "… full: $HERE/STONE-SDF-VOCAB.txt"
        else
          die "missing STONE-SDF-VOCAB.txt"
        fi
        ;;
      *)
        echo "wb stone: list|store [facet|all]|recover [facet|all] [write]|verify|vocab" >&2
        exit 2
        ;;
    esac
    ;;
  serve)
    echo "wb: NO SERVER · ride BGF · wb pack && wb open"
    exit 2
    ;;
  *)
    echo "unknown: $cmd · wb help" >&2
    exit 2
    ;;
esac
