gesh75's picture
Upload folder using huggingface_hub
1bd64c1 verified
Raw History Blame Contribute Delete
64.7 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Multivendor Network CLI Tools — 70,006 commands · 17 vendors · one page</title>
<meta name="description" content="A zero-dependency single-file HTML reference for network engineers: 70,006 CLI commands across 17 vendors and tools, searchable, comparable, deep-linkable, with vendor-correct automation snippet generation. Served statically by GitHub Pages.">
<meta name="color-scheme" content="dark">
<style>
:root{
--bg-0:#0b0f0a; --bg-1:#0c120d; --bg-2:#0f1814;
--green:#34d399; --green-bright:#39ff14; --amber:#fbbf24; --teal:#5eead4;
--ink:#e6f0e6; --muted:#9fb1a4; --muted-dim:#7c8d80;
--card:rgba(255,255,255,.04); --card-line:rgba(94,234,212,.16);
--glow-green:rgba(52,211,153,.35); --glow-amber:rgba(251,191,36,.30);
--mono:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
--sans:ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--maxw:1120px; --radius:16px;
}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{
margin:0; font-family:var(--sans); color:var(--ink); line-height:1.6;
background:
radial-gradient(1100px 540px at 78% -8%, rgba(52,211,153,.10), transparent 60%),
radial-gradient(900px 480px at 10% 8%, rgba(251,191,36,.06), transparent 60%),
linear-gradient(160deg, var(--bg-0) 0%, var(--bg-1) 55%, var(--bg-2) 100%);
background-attachment:fixed; min-height:100vh;
-webkit-font-smoothing:antialiased;
}
body::before{
content:""; position:fixed; inset:0; z-index:0; pointer-events:none; opacity:.5;
background-image:linear-gradient(rgba(52,211,153,.045) 1px, transparent 1px),
linear-gradient(90deg, rgba(52,211,153,.045) 1px, transparent 1px);
background-size:34px 34px;
-webkit-mask-image:radial-gradient(closest-side at 50% 30%, #000 40%, transparent 92%);
mask-image:radial-gradient(closest-side at 50% 30%, #000 40%, transparent 92%);
}
a{color:var(--teal); text-decoration:none}
a:hover{color:var(--green)}
:focus-visible{outline:2.5px solid var(--green); outline-offset:3px; border-radius:6px}
.skip{position:absolute; left:-999px; top:0; z-index:200; background:var(--green);
color:#04140d; padding:.6rem 1rem; border-radius:0 0 10px 0; font-weight:700}
.skip:focus{left:0}
/* scroll progress */
#progress{position:fixed; top:0; left:0; height:3px; width:0%; z-index:120;
background:linear-gradient(90deg, var(--amber), var(--green), var(--teal));
box-shadow:0 0 12px var(--glow-green)}
/* nav */
header.nav{position:sticky; top:0; z-index:100;
backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px);
background:rgba(8,14,10,.72); border-bottom:1px solid rgba(52,211,153,.14)}
.nav-inner{max-width:var(--maxw); margin:0 auto; padding:.7rem 1.1rem;
display:flex; align-items:center; gap:1rem; justify-content:space-between}
.brand{display:flex; align-items:center; gap:.6rem; font-weight:800; letter-spacing:.2px}
.brand .dot{width:11px; height:11px; border-radius:50%; background:var(--green);
box-shadow:0 0 10px var(--green), 0 0 20px var(--glow-green); animation:pulse 2.6s ease-in-out infinite}
.brand b{font-family:var(--mono); font-size:.96rem;
background:linear-gradient(90deg,var(--green),var(--amber),var(--teal));
-webkit-background-clip:text; background-clip:text; color:transparent}
nav.links{display:flex; gap:.2rem; flex-wrap:wrap}
nav.links a{position:relative; color:var(--muted); font-size:.86rem; font-weight:600;
padding:.45rem .6rem; border-radius:8px; white-space:nowrap}
nav.links a::after{content:""; position:absolute; left:.6rem; right:.6rem; bottom:.28rem; height:2px;
background:linear-gradient(90deg,var(--green),var(--teal)); transform:scaleX(0); transform-origin:left;
transition:transform .28s ease}
nav.links a:hover{color:var(--ink)}
nav.links a:hover::after,nav.links a.active::after{transform:scaleX(1)}
nav.links a.active{color:var(--green)}
.nav-cta{display:inline-flex; align-items:center; gap:.4rem; font-size:.84rem; font-weight:700;
padding:.5rem .85rem; border-radius:10px; color:#04140d;
background:linear-gradient(135deg,var(--green),var(--teal)); white-space:nowrap}
.nav-cta:hover{color:#04140d; filter:brightness(1.08)}
#menu-btn{display:none; background:transparent; border:1px solid var(--card-line); color:var(--ink);
font-size:1.2rem; border-radius:9px; padding:.3rem .55rem; cursor:pointer}
main{position:relative; z-index:1; max-width:var(--maxw); margin:0 auto; padding:0 1.1rem}
section{padding:4.4rem 0 2rem; scroll-margin-top:74px}
.eyebrow{font-family:var(--mono); font-size:.78rem; letter-spacing:.22em; text-transform:uppercase;
color:var(--amber); margin:0 0 .5rem}
h2.title{font-size:clamp(1.7rem,3.6vw,2.4rem); margin:.1rem 0 .6rem; font-weight:800; letter-spacing:-.5px;
background:linear-gradient(100deg,var(--ink),var(--green) 55%,var(--teal));
-webkit-background-clip:text; background-clip:text; color:transparent}
.lede{color:var(--muted); max-width:64ch; font-size:1.04rem}
/* hero */
#hero{padding-top:2.6rem; text-align:center}
.hero-art{position:relative; max-width:980px; margin:0 auto 1.4rem; border-radius:18px; overflow:hidden;
border:1px solid rgba(52,211,153,.20); box-shadow:0 30px 90px -40px rgba(0,0,0,.9), 0 0 60px -30px var(--glow-green)}
.hero-art svg{display:block; width:100%; height:auto}
.hero-art::after{content:""; position:absolute; inset:0; pointer-events:none;
box-shadow:inset 0 0 120px -40px rgba(57,255,20,.25)}
h1.hero-title{font-size:clamp(2.1rem,6vw,3.7rem); line-height:1.04; margin:.3rem 0 .5rem; font-weight:900; letter-spacing:-1px;
background:linear-gradient(96deg,var(--green-bright),var(--amber) 48%,var(--teal));
-webkit-background-clip:text; background-clip:text; color:transparent;
filter:drop-shadow(0 4px 24px rgba(52,211,153,.25))}
.tagline{color:var(--muted); font-size:clamp(1rem,2.1vw,1.22rem); max-width:62ch; margin:.2rem auto 1.4rem}
.tagline b{color:var(--green); font-weight:700}
.chips{display:flex; gap:.7rem; flex-wrap:wrap; justify-content:center; margin:1.5rem auto .4rem; max-width:920px}
.chip{flex:1 1 130px; min-width:118px; max-width:200px; padding:1rem .6rem; border-radius:14px;
background:var(--card); border:1px solid var(--card-line); backdrop-filter:blur(10px);
box-shadow:0 10px 30px -20px rgba(0,0,0,.8); transition:transform .3s ease, box-shadow .3s ease}
.chip:hover{transform:translateY(-4px); box-shadow:0 18px 40px -22px var(--glow-green)}
.chip .num{font-family:var(--mono); font-weight:800; font-size:clamp(1.3rem,3.4vw,1.85rem);
color:var(--green); text-shadow:0 0 18px var(--glow-green)}
.chip .lbl{font-size:.72rem; color:var(--muted-dim); text-transform:uppercase; letter-spacing:.08em; margin-top:.25rem}
.cta-row{display:flex; gap:.8rem; flex-wrap:wrap; justify-content:center; margin-top:1.8rem}
.btn{display:inline-flex; align-items:center; gap:.5rem; font-weight:700; font-size:.95rem;
padding:.8rem 1.3rem; border-radius:12px; transition:transform .2s ease, filter .2s ease, box-shadow .2s ease}
.btn-primary{color:#04140d; background:linear-gradient(135deg,var(--green),var(--teal));
box-shadow:0 12px 32px -14px var(--glow-green)}
.btn-primary:hover{transform:translateY(-2px); filter:brightness(1.08); color:#04140d}
.btn-ghost{color:var(--ink); border:1px solid var(--card-line); background:var(--card)}
.btn-ghost:hover{transform:translateY(-2px); border-color:var(--green); color:var(--green)}
/* cards / glass */
.grid{display:grid; gap:1.1rem; grid-template-columns:repeat(auto-fit,minmax(270px,1fr))}
.card{background:var(--card); border:1px solid var(--card-line); border-radius:var(--radius);
padding:1.3rem 1.25rem; backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px);
box-shadow:0 14px 40px -28px rgba(0,0,0,.9); position:relative; overflow:hidden;
transition:transform .3s ease, box-shadow .3s ease, border-color .3s ease}
.card::before{content:""; position:absolute; inset:0 0 auto 0; height:2px;
background:linear-gradient(90deg,transparent,var(--green),var(--teal),transparent); opacity:.5}
.card:hover{transform:translateY(-5px); border-color:rgba(52,211,153,.4);
box-shadow:0 26px 56px -28px rgba(0,0,0,.95), 0 0 38px -22px var(--glow-green)}
.card .ic{font-size:1.6rem; display:block; margin-bottom:.55rem}
.card h3{margin:.1rem 0 .45rem; font-size:1.06rem; color:var(--ink)}
.card p{margin:0; color:var(--muted); font-size:.92rem}
/* reveal */
.reveal{opacity:0; transform:translateY(22px); transition:opacity .6s ease, transform .6s ease}
.reveal.in{opacity:1; transform:none}
/* overview band */
.panel{background:var(--card); border:1px solid var(--card-line); border-radius:var(--radius);
padding:1.6rem 1.5rem; backdrop-filter:blur(12px)}
.overview-grid{display:grid; gap:1.1rem; grid-template-columns:1.4fr .9fr; align-items:start}
.mini-stats{display:grid; gap:.7rem}
.mini{display:flex; justify-content:space-between; align-items:baseline; gap:.8rem;
padding:.7rem .9rem; border-radius:11px; background:rgba(52,211,153,.05); border:1px solid rgba(52,211,153,.12)}
.mini .v{font-family:var(--mono); color:var(--amber); font-weight:800}
.mini .k{color:var(--muted); font-size:.86rem}
/* mermaid */
.diagram{background:rgba(8,14,10,.55); border:1px solid var(--card-line); border-radius:var(--radius);
padding:1rem; margin-top:1.1rem; overflow-x:auto}
.diagram .cap{font-size:.84rem; color:var(--muted-dim); margin:.2rem .2rem .8rem; font-style:italic}
.diagram h3{margin:.2rem .2rem .2rem; font-size:1rem; color:var(--teal); font-family:var(--mono)}
pre.mermaid{margin:0; text-align:center; min-height:40px}
.diagram-stack{display:grid; gap:1.4rem}
/* tech chips */
.stack{display:flex; flex-wrap:wrap; gap:.6rem}
.tag{font-family:var(--mono); font-size:.85rem; padding:.5rem .85rem; border-radius:999px;
background:rgba(52,211,153,.06); border:1px solid rgba(52,211,153,.2); color:var(--teal);
transition:transform .2s ease, background .2s ease, color .2s ease}
.tag:hover{transform:translateY(-3px); background:rgba(251,191,36,.1); color:var(--amber); border-color:rgba(251,191,36,.4)}
.tag::before{content:"▸ "; color:var(--green)}
/* components table */
.tbl-wrap{overflow-x:auto; border:1px solid var(--card-line); border-radius:var(--radius); background:var(--card)}
table{border-collapse:collapse; width:100%; min-width:560px}
thead th{text-align:left; font-size:.74rem; letter-spacing:.1em; text-transform:uppercase; color:var(--amber);
padding:.85rem 1rem; border-bottom:1px solid rgba(251,191,36,.2)}
tbody td{padding:.85rem 1rem; border-bottom:1px solid rgba(255,255,255,.05); vertical-align:top; font-size:.9rem; color:var(--muted)}
tbody tr:last-child td{border-bottom:none}
tbody tr:hover td{background:rgba(52,211,153,.04)}
td .mod{font-family:var(--mono); color:var(--green); font-weight:600; font-size:.85rem}
td .te{display:inline-block; margin-top:.35rem; font-family:var(--mono); font-size:.72rem; color:var(--muted-dim)}
/* quickstart code */
.code{position:relative; background:#070b08; border:1px solid rgba(52,211,153,.22); border-radius:13px;
margin:1rem 0 0; overflow:hidden}
.code .bar{display:flex; align-items:center; justify-content:space-between; padding:.5rem .8rem;
background:rgba(52,211,153,.05); border-bottom:1px solid rgba(52,211,153,.14)}
.code .bar .ttl{font-family:var(--mono); font-size:.78rem; color:var(--muted)}
.code .bar .dots{display:flex; gap:.35rem}
.code .bar .dots i{width:10px; height:10px; border-radius:50%; display:inline-block}
.copy{font-family:var(--mono); font-size:.74rem; color:var(--teal); background:transparent;
border:1px solid rgba(94,234,212,.3); border-radius:7px; padding:.28rem .6rem; cursor:pointer; transition:all .2s ease}
.copy:hover{background:rgba(94,234,212,.12); color:var(--green)}
.copy.ok{color:var(--green); border-color:var(--green)}
pre.sh{margin:0; padding:1rem 1.1rem; overflow-x:auto; font-family:var(--mono); font-size:.84rem; line-height:1.65; color:#cfe6d4}
pre.sh .c{color:var(--muted-dim)}
pre.sh .g{color:var(--green)}
pre.sh .a{color:var(--amber)}
/* footer */
footer{position:relative; z-index:1; max-width:var(--maxw); margin:2rem auto 0; padding:2.4rem 1.1rem 3rem;
border-top:1px solid rgba(52,211,153,.14); color:var(--muted)}
.foot-grid{display:flex; flex-wrap:wrap; gap:1.4rem; justify-content:space-between; align-items:flex-start}
.foot-links{display:flex; flex-wrap:wrap; gap:1.2rem}
.foot-links a{font-size:.9rem}
.foot-note{font-size:.8rem; color:var(--muted-dim); margin-top:1.2rem}
/* back to top */
#top{position:fixed; right:1.1rem; bottom:1.1rem; z-index:90; width:46px; height:46px; border-radius:50%;
border:1px solid var(--card-line); background:rgba(8,14,10,.8); color:var(--green); font-size:1.2rem;
cursor:pointer; opacity:0; transform:translateY(14px); transition:opacity .3s ease, transform .3s ease;
backdrop-filter:blur(10px); box-shadow:0 8px 24px -12px var(--glow-green)}
#top.show{opacity:1; transform:none}
#top:hover{color:var(--amber); border-color:var(--amber)}
@keyframes pulse{0%,100%{opacity:.55; transform:scale(.92)} 50%{opacity:1; transform:scale(1.12)}}
@media (max-width:880px){
.overview-grid{grid-template-columns:1fr}
nav.links{display:none; position:absolute; top:100%; left:0; right:0; flex-direction:column;
background:rgba(8,14,10,.97); border-bottom:1px solid rgba(52,211,153,.14); padding:.6rem 1rem; gap:.1rem}
nav.links.open{display:flex}
nav.links a{padding:.7rem .5rem}
#menu-btn{display:inline-block}
.nav-cta{display:none}
}
@media (max-width:480px){
.chip{flex:1 1 44%}
}
@media (prefers-reduced-motion: reduce){
*,*::before,*::after{animation:none!important; transition:none!important; scroll-behavior:auto!important}
.reveal{opacity:1; transform:none}
}
</style>
</head>
<body>
<a class="skip" href="#main">Skip to content</a>
<div id="progress" aria-hidden="true"></div>
<header class="nav">
<div class="nav-inner">
<div class="brand"><span class="dot" aria-hidden="true"></span><b>multivendor-cli</b></div>
<button id="menu-btn" aria-label="Toggle navigation" aria-expanded="false">☰</button>
<nav class="links" id="navlinks" aria-label="Section navigation">
<a href="#overview">Overview</a>
<a href="#features">Features</a>
<a href="#architecture">Architecture</a>
<a href="#dataflow">Data flow</a>
<a href="#stack">Tech stack</a>
<a href="#components">Components</a>
<a href="#studio">Studio</a>
<a href="#status">Status</a>
<a href="#develop">Develop</a>
<a href="#changelog">Changelog</a>
<a href="#quickstart">Quickstart</a>
<a href="../studio.html">Open Studio</a>
</nav>
<a class="nav-cta" href="https://github.com/gesh75/multivendor-cli-configurator" target="_blank" rel="noopener">GitHub ↗</a>
</div>
</header>
<main id="main">
<!-- ================= HERO ================= -->
<section id="hero" aria-label="Introduction">
<div class="hero-art reveal">
<!-- inlined docs/assets/hero.svg -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 360" role="img" aria-labelledby="heroTitle heroDesc" fill="none" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace">
<title id="heroTitle">multivendor-cli-configurator — architecture</title>
<desc id="heroDesc">Animated terminal banner: a phosphor-green prompt types a network command while 17 vendor chips light up in sequence, connector lines pulse from the command to each matched vendor, a stream of grayed commands scrolls behind, and a counter ticks toward 70,006 commands.</desc>
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#0b0f0a"/>
<stop offset="0.55" stop-color="#0c120d"/>
<stop offset="1" stop-color="#0f1814"/>
</linearGradient>
<radialGradient id="crt" cx="0.5" cy="0.32" r="0.85">
<stop offset="0" stop-color="#39ff14" stop-opacity="0.10"/>
<stop offset="0.6" stop-color="#39ff14" stop-opacity="0.02"/>
<stop offset="1" stop-color="#000000" stop-opacity="0.35"/>
</radialGradient>
<linearGradient id="titleGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#39ff14"/>
<stop offset="0.5" stop-color="#ffd152"/>
<stop offset="1" stop-color="#5eead4"/>
</linearGradient>
<linearGradient id="panelGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#11201a"/>
<stop offset="1" stop-color="#0c130e"/>
</linearGradient>
<pattern id="grid" width="30" height="30" patternUnits="userSpaceOnUse">
<path d="M30 0H0V30" fill="none" stroke="#39ff14" stroke-opacity="0.05" stroke-width="1"/>
</pattern>
<filter id="glow" x="-60%" y="-60%" width="220%" height="220%">
<feGaussianBlur stdDeviation="3.4" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="softGlow" x="-80%" y="-80%" width="260%" height="260%">
<feGaussianBlur stdDeviation="7" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<style>
@keyframes blink { 0%,49%{opacity:1} 50%,100%{opacity:0} }
@keyframes pulse { 0%,100%{opacity:.45} 50%{opacity:1} }
@keyframes flicker { 0%,100%{opacity:.92} 47%{opacity:1} 62%{opacity:.85} }
@keyframes drawline { to { stroke-dashoffset: 0 } }
@keyframes scroll { from { transform: translateY(0) } to { transform: translateY(-150px) } }
@keyframes chipOn {
0%, 12% { fill: #0f1814; stroke: #2a3a30; }
20%, 78% { fill: #14271c; stroke: #39ff14; }
88%, 100% { fill: #0f1814; stroke: #2a3a30; }
}
@keyframes chipText {
0%, 12% { fill: #8b9a8e; }
20%, 78% { fill: #39ff14; }
88%, 100% { fill: #8b9a8e; }
}
@keyframes linkPulse {
0%, 10% { opacity: 0; }
22%, 70% { opacity: 0.9; }
86%, 100% { opacity: 0; }
}
.node { transform-box: fill-box; transform-origin: center; }
.cursor { animation: blink 1.05s step-end infinite; }
.crt { animation: flicker 5.5s ease-in-out infinite; }
.pulse { animation: pulse 2.8s ease-in-out infinite; }
.p2 { animation-delay: .5s } .p3 { animation-delay: 1s } .p4 { animation-delay: 1.5s }
.underline { stroke-dasharray: 540; stroke-dashoffset: 540; animation: drawline 1.7s ease-out .35s forwards; }
.stream { animation: scroll 9s linear infinite; }
.chipBox { animation: chipOn 9.5s ease-in-out infinite; }
.chipLbl { animation: chipText 9.5s ease-in-out infinite; }
.link { animation: linkPulse 9.5s ease-in-out infinite; }
.d0{animation-delay:0s} .d1{animation-delay:.5s} .d2{animation-delay:1s} .d3{animation-delay:1.5s}
.d4{animation-delay:2s} .d5{animation-delay:2.5s} .d6{animation-delay:3s} .d7{animation-delay:3.5s}
.d8{animation-delay:4s} .d9{animation-delay:4.5s} .d10{animation-delay:5s} .d11{animation-delay:5.5s}
.d12{animation-delay:6s} .d13{animation-delay:6.5s} .d14{animation-delay:7s} .d15{animation-delay:7.5s}
.d16{animation-delay:8s}
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; }
.underline { stroke-dashoffset: 0; }
.cursor { opacity: 1; }
.chipBox { fill: #14271c; stroke: #39ff14; }
.chipLbl { fill: #39ff14; }
.link { opacity: 0.55; }
}
</style>
</defs>
<rect width="1200" height="360" fill="url(#bg)"/>
<rect width="1200" height="360" fill="url(#grid)"/>
<rect width="1200" height="360" fill="url(#crt)" class="crt"/>
<g clip-path="url(#streamClip)" opacity="0.16">
<clipPath id="streamClip"><rect x="812" y="44" width="350" height="150" rx="8"/></clipPath>
<g class="stream" font-size="11" fill="#8b9a8e">
<text x="824" y="58">router bgp 65001</text>
<text x="824" y="76">set protocols ospf area 0</text>
<text x="824" y="94">ip route 10.0.0.0/24</text>
<text x="824" y="112">nv set interface swp1</text>
<text x="824" y="130">config system interface</text>
<text x="824" y="148">vlan 100 name CORE</text>
<text x="824" y="166">show ip bgp summary</text>
<text x="824" y="184">enter candidate</text>
<text x="824" y="202">router bgp 65001</text>
<text x="824" y="220">set protocols ospf area 0</text>
<text x="824" y="238">ip route 10.0.0.0/24</text>
<text x="824" y="256">nv set interface swp1</text>
<text x="824" y="274">config system interface</text>
<text x="824" y="292">vlan 100 name CORE</text>
</g>
</g>
<g class="node">
<rect x="44" y="44" width="540" height="150" rx="12" fill="url(#panelGrad)" stroke="#1d3326" stroke-width="1.5"/>
<circle cx="66" cy="64" r="5" fill="#ff5f56"/>
<circle cx="84" cy="64" r="5" fill="#ffb000"/>
<circle cx="102" cy="64" r="5" fill="#00d97e"/>
<text x="120" y="68" font-size="11" fill="#8b9a8e">multivendor-cli — terminal</text>
<text x="66" y="108" font-size="16" fill="#39ff14" filter="url(#glow)">
<tspan fill="#ffb000" font-weight="700">&#8227; </tspan><tspan fill="#5eead4">cli</tspan><tspan fill="#8b9a8e"> ~ </tspan><tspan fill="#ffd152">$</tspan>
</text>
<text x="118" y="108" font-size="16" fill="#e6f0e6" filter="url(#glow)">
<tspan fill="#39ff14">router</tspan> bgp <tspan fill="#ffd152">65001</tspan><tspan x="118" dy="0"> </tspan>
</text>
<rect class="cursor" x="298" y="96" width="9" height="16" fill="#39ff14"/>
<text x="66" y="138" font-size="11" fill="#8b9a8e"># one intent &#8594; 17 vendor syntaxes, concept-aligned</text>
<text x="66" y="170" font-size="12" fill="#8b9a8e">corpus</text>
<text x="116" y="170" font-size="13" font-weight="700" fill="#39ff14" filter="url(#glow)">70,006 commands</text>
<text x="290" y="170" font-size="11" fill="#5eead4">cache-then-revalidate</text>
</g>
<g stroke-width="1.6" fill="none">
<path class="link d0" d="M584 118 C 660 130, 740 240, 786 252" stroke="#39ff14"/>
<path class="link d2" d="M584 122 C 680 150, 800 244, 866 252" stroke="#00d97e"/>
<path class="link d4" d="M584 126 C 720 170, 880 246, 946 252" stroke="#5eead4"/>
<path class="link d6" d="M584 130 C 780 190, 960 248,1026 252" stroke="#39ff14"/>
<path class="link d8" d="M584 134 C 840 200,1040 250,1106 252" stroke="#ffd152"/>
</g>
<text x="615" y="118" font-size="40" font-weight="800" fill="url(#titleGrad)" filter="url(#softGlow)"
font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif">multivendor-cli-configurator</text>
<path class="underline" d="M616 134 H1118" stroke="#39ff14" stroke-width="2.5" stroke-linecap="round"/>
<text x="616" y="162" font-size="16" fill="#8b9a8e"
font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif">70,000+ CLI commands &#183; 17 vendors &#183; one zero-dependency HTML file</text>
<g font-size="11" font-weight="600" text-anchor="middle">
<g class="node"><rect class="chipBox d0" x="44" y="234" width="84" height="26" rx="13" fill="#0f1814" stroke="#2a3a30" stroke-width="1.4"/><text class="chipLbl d0" x="86" y="251" fill="#8b9a8e">Cisco</text></g>
<g class="node"><rect class="chipBox d1" x="136" y="234" width="84" height="26" rx="13" fill="#0f1814" stroke="#2a3a30" stroke-width="1.4"/><text class="chipLbl d1" x="178" y="251" fill="#8b9a8e">Juniper</text></g>
<g class="node"><rect class="chipBox d2" x="228" y="234" width="84" height="26" rx="13" fill="#0f1814" stroke="#2a3a30" stroke-width="1.4"/><text class="chipLbl d2" x="270" y="251" fill="#8b9a8e">Arista</text></g>
<g class="node"><rect class="chipBox d3" x="320" y="234" width="84" height="26" rx="13" fill="#0f1814" stroke="#2a3a30" stroke-width="1.4"/><text class="chipLbl d3" x="362" y="251" fill="#8b9a8e">FRR</text></g>
<g class="node"><rect class="chipBox d4" x="412" y="234" width="84" height="26" rx="13" fill="#0f1814" stroke="#2a3a30" stroke-width="1.4"/><text class="chipLbl d4" x="454" y="251" fill="#8b9a8e">VyOS</text></g>
<g class="node"><rect class="chipBox d5" x="504" y="234" width="84" height="26" rx="13" fill="#0f1814" stroke="#2a3a30" stroke-width="1.4"/><text class="chipLbl d5" x="546" y="251" fill="#8b9a8e">Huawei</text></g>
<g class="node"><rect class="chipBox d6" x="596" y="234" width="84" height="26" rx="13" fill="#0f1814" stroke="#2a3a30" stroke-width="1.4"/><text class="chipLbl d6" x="638" y="251" fill="#8b9a8e">Aruba</text></g>
<g class="node"><rect class="chipBox d7" x="688" y="234" width="84" height="26" rx="13" fill="#0f1814" stroke="#2a3a30" stroke-width="1.4"/><text class="chipLbl d7" x="730" y="251" fill="#8b9a8e">Extreme</text></g>
<g class="node"><rect class="chipBox d8" x="780" y="234" width="84" height="26" rx="13" fill="#0f1814" stroke="#2a3a30" stroke-width="1.4"/><text class="chipLbl d8" x="822" y="251" fill="#8b9a8e">FortiOS</text></g>
</g>
<g font-size="11" font-weight="600" text-anchor="middle">
<g class="node"><rect class="chipBox d9" x="44" y="272" width="84" height="26" rx="13" fill="#0f1814" stroke="#2a3a30" stroke-width="1.4"/><text class="chipLbl d9" x="86" y="289" fill="#8b9a8e">PAN-OS</text></g>
<g class="node"><rect class="chipBox d10" x="136" y="272" width="84" height="26" rx="13" fill="#0f1814" stroke="#2a3a30" stroke-width="1.4"/><text class="chipLbl d10" x="178" y="289" fill="#8b9a8e">Mikrotik</text></g>
<g class="node"><rect class="chipBox d11" x="228" y="272" width="84" height="26" rx="13" fill="#0f1814" stroke="#2a3a30" stroke-width="1.4"/><text class="chipLbl d11" x="270" y="289" fill="#8b9a8e">NVIDIA</text></g>
<g class="node"><rect class="chipBox d12" x="320" y="272" width="84" height="26" rx="13" fill="#0f1814" stroke="#2a3a30" stroke-width="1.4"/><text class="chipLbl d12" x="362" y="289" fill="#8b9a8e">Nokia</text></g>
<g class="node"><rect class="chipBox d13" x="412" y="272" width="84" height="26" rx="13" fill="#0f1814" stroke="#2a3a30" stroke-width="1.4"/><text class="chipLbl d13" x="454" y="289" fill="#8b9a8e">SONiC</text></g>
<g class="node"><rect class="chipBox d14" x="504" y="272" width="84" height="26" rx="13" fill="#0f1814" stroke="#2a3a30" stroke-width="1.4"/><text class="chipLbl d14" x="546" y="289" fill="#8b9a8e">Microsoft</text></g>
<g class="node"><rect class="chipBox d15" x="596" y="272" width="84" height="26" rx="13" fill="#0f1814" stroke="#2a3a30" stroke-width="1.4"/><text class="chipLbl d15" x="638" y="289" fill="#8b9a8e">Linux</text></g>
<g class="node"><rect class="chipBox d16" x="688" y="272" width="84" height="26" rx="13" fill="#0f1814" stroke="#2a3a30" stroke-width="1.4"/><text class="chipLbl d16" x="730" y="289" fill="#8b9a8e">Wireshark</text></g>
<g class="node"><rect x="780" y="272" width="84" height="26" rx="13" fill="#0c130e" stroke="#ffb000" stroke-width="1.4" opacity="0.9"/><text x="822" y="289" fill="#ffd152">+ Python ETL</text></g>
</g>
<g font-size="11" text-anchor="middle">
<path id="pipe" d="M120 330 H1080" stroke="#1d3326" stroke-width="2"/>
<text x="120" y="334" fill="#8b9a8e" text-anchor="start">sources</text>
<text x="420" y="334" fill="#8b9a8e">parse + merge</text>
<text x="700" y="334" fill="#5eead4">commands.json</text>
<text x="1080" y="334" fill="#39ff14" text-anchor="end">index.html</text>
<circle r="3.5" fill="#39ff14" filter="url(#glow)">
<animateMotion dur="3.2s" repeatCount="indefinite" rotate="auto"><mpath href="#pipe"/></animateMotion>
</circle>
<circle r="3.5" fill="#ffd152" filter="url(#glow)">
<animateMotion dur="3.2s" begin="1.05s" repeatCount="indefinite" rotate="auto"><mpath href="#pipe"/></animateMotion>
</circle>
<circle r="3.5" fill="#5eead4" filter="url(#glow)">
<animateMotion dur="3.2s" begin="2.1s" repeatCount="indefinite" rotate="auto"><mpath href="#pipe"/></animateMotion>
</circle>
</g>
<g filter="url(#softGlow)">
<circle class="node pulse" cx="700" cy="330" r="4.5" fill="#5eead4"/>
<circle class="node pulse p2" cx="1080" cy="330" r="4.5" fill="#39ff14"/>
<circle class="node pulse p3" cx="120" cy="330" r="4.5" fill="#ffb000"/>
</g>
</svg>
</div>
<h1 class="hero-title">Multivendor Network CLI Tools</h1>
<p class="tagline"><b>70,000+ CLI commands</b> across 17 vendors and tools in one searchable, comparable, deep-linkable single-file page.</p>
<div class="chips" role="list" aria-label="Key statistics">
<div class="chip" role="listitem"><div class="num" data-count="70006">0</div><div class="lbl">CLI commands</div></div>
<div class="chip" role="listitem"><div class="num" data-count="17">0</div><div class="lbl">vendors &amp; tools</div></div>
<div class="chip" role="listitem"><div class="num" data-count="3">0</div><div class="lbl">view modes</div></div>
<div class="chip" role="listitem"><div class="num" data-count="870">0</div><div class="lbl">FRR rows verified live</div></div>
<div class="chip" role="listitem"><div class="num" data-count="0" data-suffix="" data-text="zero">0</div><div class="lbl">JS dependencies</div></div>
</div>
<div class="cta-row">
<a class="btn btn-primary" href="https://github.com/gesh75/multivendor-cli-configurator" target="_blank" rel="noopener">↗ View on GitHub</a>
<a class="btn btn-ghost" href="#quickstart">↓ Quickstart</a>
</div>
</section>
<!-- ================= OVERVIEW ================= -->
<section id="overview" aria-labelledby="overview-h">
<p class="eyebrow">Overview</p>
<h2 class="title" id="overview-h">One page, every vendor's syntax</h2>
<div class="overview-grid">
<div class="panel reveal">
<p class="lede">Multivendor Network CLI Tools is a zero-dependency, single-file HTML reference for network engineers that puts <strong style="color:var(--green)">70,006 CLI commands</strong> across 17 vendors and tools into one searchable, filterable, deep-linkable browser page.</p>
<p class="lede" style="margin-top:1rem">It solves the everyday pain of juggling per-vendor syntax — letting engineers search, compare commands concept-aligned side by side, and generate vendor-correct automation snippets (NETCONF, ncclient, Netmiko, Ansible, Bash) pre-filled from their own command values.</p>
<p class="lede" style="margin-top:1rem">An offline, stdlib-only Python pipeline turns published vendor docs — and a live 10-node Docker FRR lab — into one deduped <code style="font-family:var(--mono);color:var(--teal)">commands.json</code> that GitHub Pages serves statically. No backend, no build, no JS dependencies.</p>
</div>
<div class="mini-stats reveal" aria-label="Corpus breakdown">
<div class="mini"><span class="k">index.html (vanilla JS lines)</span><span class="v">5,861</span></div>
<div class="mini"><span class="k">CONCEPT_SYNONYMS entries</span><span class="v">37</span></div>
<div class="mini"><span class="k">YANG automation patterns</span><span class="v">15</span></div>
<div class="mini"><span class="k">FRR lab nodes (Docker)</span><span class="v">10</span></div>
<div class="mini"><span class="k">commands.json size</span><span class="v">~18 MB</span></div>
<div class="mini"><span class="k">Roles: router / switch / firewall</span><span class="v">43,008 / 22,848 / 4,150</span></div>
</div>
</div>
</section>
<!-- ================= FEATURES ================= -->
<section id="features" aria-labelledby="features-h">
<p class="eyebrow">Capabilities</p>
<h2 class="title" id="features-h">Key features</h2>
<p class="lede">Search, compare, and automate across 17 CLIs — all from one static file.</p>
<div class="grid" style="margin-top:1.4rem">
<article class="card reveal"><span class="ic" aria-hidden="true">📚</span><h3>70,006 commands, 17 vendors, one page</h3><p>Arista EOS, Cisco IOS/IOS-XE/ASA/NX-OS, Juniper Junos, FRR, VyOS, Huawei VRP, Aruba AOS-CX, Extreme EXOS, FortiOS, PAN-OS, Mikrotik RouterOS, NVIDIA Cumulus NVUE, Nokia SR Linux, SONiC, plus Microsoft PowerShell, Linux iproute2, and Wireshark tshark — all searchable in a single static HTML file.</p></article>
<article class="card reveal"><span class="ic" aria-hidden="true">🪟</span><h3>Three view modes</h3><p>Cards (auto-fit grid grouped by category), Table (sortable, exportable to CSV/Markdown/JSON/TXT), and Compare — a concept-aligned, N-vendor-aware matrix that lines up Cisco's neighbor row with Junos's peer row, paginated and virtualized for the full corpus.</p></article>
<article class="card reveal"><span class="ic" aria-hidden="true">🔧</span><h3>Automate drawer (YANG + SSH)</h3><p>Regex-extracts your IP/VLAN/ASN/interface from a command and renders vendor-correct NETCONF XML, ncclient Python, Netmiko, NAPALM, and Ansible tasks pre-filled with your values across 15 model-driven patterns, with a universal SSH fallback for every command.</p></article>
<article class="card reveal"><span class="ic" aria-hidden="true">🔐</span><h3>Secret mode for snippets</h3><p>Toggle how snippets read credentials — Inline (demo only), .env via python-dotenv (default, safer pasteboard), or OS keyring — and every snippet re-renders in place with a matching .env.example / keyring set one-liner.</p></article>
<article class="card reveal"><span class="ic" aria-hidden="true">🔀</span><h3>Concept-aligned translation</h3><p>A 37-entry CONCEPT_SYNONYMS table normalizes semantic intent (bgp:peer, ospf:auth, …) so equivalent commands align across vendors; conceptKey() drives Compare-view row alignment and openEquivalents() adds a "See equivalents" drawer.</p></article>
<article class="card reveal"><span class="ic" aria-hidden="true">🔗</span><h3>Shareable, deep-linkable workspace</h3><p>Filters, view, search, favorites, and the cross-vendor CLI Builder queue serialize into a base64 <code style="font-family:var(--mono);color:var(--teal)">?ws=</code> URL (plus readable <code style="font-family:var(--mono);color:var(--teal)">?cat=BGP&amp;view=compare&amp;v=Juniper</code> params) so a shared link recreates exactly what you saw.</p></article>
<article class="card reveal"><span class="ic" aria-hidden="true">⚡</span><h3>Instant cache-then-revalidate boot</h3><p>loadCommandsWithCache() renders immediately from IndexedDB, then HEAD-revalidates commands.json against a stored ETag + Content-Length and only re-fetches when the artifact changed — a header badge shows fresh / cached / revalidating.</p></article>
<article class="card reveal"><span class="ic" aria-hidden="true">🐍</span><h3>Reproducible stdlib-only pipeline</h3><p>Every command traces back to a published source via pure Python 3 standard-library parsers under scripts/; parse.py merges and dedupes by normalized cmd, and clean_titles.py / audit_data_quality.py keep the corpus clean. No runtime dependencies.</p></article>
</div>
</section>
<!-- ================= ARCHITECTURE ================= -->
<section id="architecture" aria-labelledby="arch-h">
<p class="eyebrow">Architecture</p>
<h2 class="title" id="arch-h">A static page plus one JSON file</h2>
<p class="lede">The whole system is one static page plus one JSON file: engineers use the browser app, an offline Python pipeline turns vendor docs and a live FRR lab into <code style="font-family:var(--mono);color:var(--teal)">commands.json</code>, and GitHub Pages serves the artifacts. <code style="font-family:var(--mono);color:var(--teal)">index.html</code> is a monolith by file count but cleanly layered into a boot/cache loader, a state + render dispatcher, the filter engine, a concept-align Compare engine, and automation generators.</p>
<div class="diagram-stack">
<div class="diagram reveal">
<h3>System context</h3>
<pre class="mermaid">flowchart TB
eng["Network Engineers - browse, search, compare"]:::actor
docs["Vendor Docs - Cisco, Junos, EOS, FRR, DCN"]:::source
lab["10-node Docker FRR Lab - live capture"]:::source
subgraph SYS["multivendor-cli-configurator"]
app["index.html - single-file web app"]:::core
data["commands.json - 70,006 records"]:::store
pipe["scripts Python ETL - parse, merge, clean"]:::build
end
pages["GitHub Pages - static host, auto-deploy"]:::ext
devices["Target Devices - Netmiko, Ansible, NETCONF"]:::target
eng -->|"open page"| app
app -->|"fetch once"| data
docs --> pipe
lab --> pipe
pipe -->|"generates"| data
pipe -->|"push to main"| pages
pages -->|"serves"| app
app -.->|"copy snippets - user-run"| devices
classDef actor fill:#0e7490,stroke:#5eead4,color:#f0fdfa
classDef source fill:#475569,stroke:#94a3b8,color:#f8fafc
classDef core fill:#15803d,stroke:#34d399,color:#ecfdf5
classDef store fill:#0d9488,stroke:#5eead4,color:#f0fdfa
classDef build fill:#a16207,stroke:#fbbf24,color:#fffbeb
classDef ext fill:#334155,stroke:#94a3b8,color:#f8fafc
classDef target fill:#b45309,stroke:#fbbf24,color:#fffbeb</pre>
<p class="cap">Engineers use the browser app; an offline Python pipeline turns vendor docs and a live FRR lab into commands.json; GitHub Pages serves the artifacts.</p>
</div>
<div class="diagram reveal">
<h3>Container &amp; component map</h3>
<pre class="mermaid">flowchart TB
subgraph BROWSER["index.html - single-file client app"]
direction TB
boot["Data Loader + Cache - loadCommandsWithCache, bootRender, HEAD revalidate"]:::svc
state["Global state + render - DATA, vendor/os/role/cat Sets, replaceChildren"]:::core
filter["Filter + Deep-Link - matches, parseQuery, encodeWorkspace"]:::svc
compare["Compare + Concept Align - conceptKey, lookupConcept, CONCEPT_SYNONYMS 37"]:::accent
auto["Automation Generators - Netmiko, Ansible, NETCONF, Bash"]:::amber
end
json[("commands.json - 70,006 rows")]:::store
persist[["IndexedDB + localStorage - ?ws= base64 deep-link"]]:::ext
json --> boot --> state
state --> filter --> state
state --> compare
state --> auto
boot <--> persist
filter <--> persist
classDef core fill:#15803d,stroke:#34d399,color:#ecfdf5
classDef svc fill:#0e7490,stroke:#5eead4,color:#f0fdfa
classDef accent fill:#0d9488,stroke:#5eead4,color:#f0fdfa
classDef amber fill:#a16207,stroke:#fbbf24,color:#fffbeb
classDef store fill:#0d9488,stroke:#5eead4,color:#f0fdfa
classDef ext fill:#475569,stroke:#94a3b8,color:#f8fafc</pre>
<p class="cap">A boot/cache loader hydrates DATA; a state + render dispatcher feeds the filter, concept-align Compare, and automation generators, all backed by IndexedDB/localStorage.</p>
</div>
</div>
</section>
<!-- ================= DATA FLOW ================= -->
<section id="dataflow" aria-labelledby="flow-h">
<p class="eyebrow">How it works</p>
<h2 class="title" id="flow-h">From vendor docs to a live page</h2>
<p class="lede">An offline, stdlib-only pipeline produces the committed corpus; at runtime the app boots instantly from cache and only re-fetches when the artifact actually changed.</p>
<div class="diagram-stack">
<div class="diagram reveal">
<h3>Build &amp; data-flow pipeline</h3>
<pre class="mermaid">flowchart LR
src["scripts/sources - vendor books + docs, gitignored"]:::source
parsers["parse_*.py - encor, junos, arista, frr, cisco extras"]:::build
inter["per-source JSON - encor.json, junos.json, frr.json"]:::build
master["parse.py + merge_dcn_corpus.py - merge + dedupe by normalized cmd"]:::accent
clean["clean_titles.py + audit_data_quality.py - repair, quarantine"]:::accent
out[("commands.json - 70,006 records")]:::store
web["index.html - fetch at boot"]:::core
src --> parsers --> inter --> master --> clean --> out --> web
classDef source fill:#475569,stroke:#94a3b8,color:#f8fafc
classDef build fill:#a16207,stroke:#fbbf24,color:#fffbeb
classDef accent fill:#0d9488,stroke:#5eead4,color:#f0fdfa
classDef store fill:#0d9488,stroke:#5eead4,color:#f0fdfa
classDef core fill:#15803d,stroke:#34d399,color:#ecfdf5</pre>
<p class="cap">Per-source parsers emit intermediate JSON; parse.py and merge_dcn_corpus.py merge and dedupe by normalized cmd; quality utilities repair/quarantine bad rows before the final committed commands.json.</p>
</div>
<div class="diagram reveal">
<h3>Runtime boot — cache then revalidate</h3>
<pre class="mermaid">sequenceDiagram
actor User
participant B as Browser index.html
participant IDB as IndexedDB mvc-cli-cache
participant CDN as GitHub Pages
User->>B: open page
B->>IDB: idbGet cached corpus
alt cache hit
IDB-->>B: cached DATA
B->>B: bootRender, badge cached
end
B->>CDN: HEAD commands.json (ETag + length)
alt changed or no cache
CDN-->>B: new ETag + length
B->>CDN: GET commands.json (no-store)
CDN-->>B: 70,006 records
B->>IDB: idbPut corpus + ETag
B->>B: bootRender, badge fresh
else unchanged
CDN-->>B: 304 same ETag, badge revalidated
end
B-->>User: filtered cards / table / compare</pre>
<p class="cap">On load the app renders instantly from the IndexedDB cache, then HEAD-revalidates commands.json against a stored ETag and only re-fetches and re-renders when the artifact actually changed.</p>
</div>
</div>
</section>
<!-- ================= TECH STACK ================= -->
<section id="stack" aria-labelledby="stack-h">
<p class="eyebrow">Tech stack</p>
<h2 class="title" id="stack-h">Zero-dependency by design</h2>
<p class="lede">No framework, no bundler, no JS dependencies — just the platform and a stdlib-only Python build.</p>
<div class="stack" style="margin-top:1.3rem" role="list" aria-label="Technologies">
<span class="tag" role="listitem">Vanilla JavaScript</span>
<span class="tag" role="listitem">HTML5</span>
<span class="tag" role="listitem">CSS grid + custom properties</span>
<span class="tag" role="listitem">Fetch API (HEAD/GET no-store)</span>
<span class="tag" role="listitem">IndexedDB (mvc-cli-cache)</span>
<span class="tag" role="listitem">localStorage + sessionStorage</span>
<span class="tag" role="listitem">base64 deep-link URLs (?ws=)</span>
<span class="tag" role="listitem">Python 3 stdlib (json, re, pathlib, argparse)</span>
<span class="tag" role="listitem">GitHub Pages (static, auto-deploy)</span>
<span class="tag" role="listitem">Node.js (test harness, assert)</span>
</div>
</section>
<!-- ================= COMPONENTS ================= -->
<section id="components" aria-labelledby="comp-h">
<p class="eyebrow">Components</p>
<h2 class="title" id="comp-h">Modules &amp; responsibilities</h2>
<p class="lede">One single-file client app, one JSON corpus, and a fleet of stdlib-only Python parsers that build it.</p>
<div class="tbl-wrap reveal" style="margin-top:1.3rem">
<table>
<thead><tr><th scope="col">Module</th><th scope="col">Responsibility</th></tr></thead>
<tbody>
<tr><td><span class="mod">index.html</span><span class="te">Vanilla JS · HTML · CSS · Fetch · IndexedDB · localStorage</span></td><td>The entire single-file client app (~5,861 lines of vanilla JS): boot/cache loader, global DATA state + render dispatcher, filter/deep-link engine, concept-aligned Compare engine, and automation snippet generators. No framework, no build.</td></tr>
<tr><td><span class="mod">commands.json</span><span class="te">Static JSON ~18 MB · GitHub Pages</span></td><td>Single source of truth — flat array of 70,006 command records across 17 vendors/tools. Fetched once at boot (cache-then-revalidate) and queried entirely client-side. FRR rows carry optional live/in_docs provenance flags.</td></tr>
<tr><td><span class="mod">scripts/parse.py</span><span class="te">Python 3 stdlib</span></td><td>Master merger — generic markdown walker that folds in community sources, seed entries, and all per-source JSONs, deduping globally by normalized cmd to emit commands.json.</td></tr>
<tr><td><span class="mod">scripts/parse_frr.py</span><span class="te">Python 3 stdlib</span></td><td>Parses the FRR Master CLI reference JSON (docs ∪ Docker-FRR live scrape) into frr.json, mapping daemons to categories and tagging each row with live/in_docs provenance flags surfaced as a green "live" badge in the UI.</td></tr>
<tr><td><span class="mod">scripts/merge_dcn_corpus.py</span><span class="te">Python 3 stdlib (argparse, json, re, pathlib)</span></td><td>Merges the DCN tool's pre-built cli-export.json corpus into commands.json, normalizing vendor labels and roles and deduping by (vendor, normalized cmd) so existing FRR provenance flags survive. Added Microsoft/Linux/Wireshark surfaces.</td></tr>
<tr><td><span class="mod">scripts/clean_titles.py</span><span class="te">Python 3 stdlib</span></td><td>Idempotent maintenance utility that derives clean command labels from the cmd field for any record whose title is leaked prose; writes .titlebak backups.</td></tr>
<tr><td><span class="mod">scripts/audit_data_quality.py</span><span class="te">Python 3 stdlib</span></td><td>Flags records where description prose has leaked into title/cmd fields and can quarantine unrecoverable rows to keep the corpus clean.</td></tr>
<tr><td><span class="mod">scripts/parse_encor.py · parse_junos.py · parse_arista.py · parse_cisco_ospf.py · parse_cisco_extras.py</span><span class="te">Python 3 stdlib</span></td><td>Per-source parsers, one per vendor doc format (Cisco Press chapter+verb regex, Junos two-mode index, Arista bold-backtick, ASA/NX-OS word-boundary role classifier), each emitting an intermediate per-source JSON.</td></tr>
<tr><td><span class="mod">tests/stress_test.js</span><span class="te">Node.js assert · process.hrtime.bigint</span></td><td>Node-based pure-JS performance + correctness suite that extracts functions (extractPlaceholders, substitutePlaceholders, lookupConcept, CONCEPT_SYNONYMS) directly from index.html and benchmarks them against the production corpus.</td></tr>
<tr><td><span class="mod">studio.html + studio-data.json</span><span class="te">Vanilla JS · static JSON ~56 KB</span></td><td>CLI Studio — intent, recipes (incl. IS-IS / SR-MPLS / IPv6 BGP), migrate, parse (Cisco/Junos/EOS/FRR/VyOS + TextFSM/TTP export), FRR lab, hardening lint, AEGIS-style pre-change. Curated ~160+ concept-aligned rows. Zero npm.</td></tr>
<tr><td><span class="mod">docs/DEVELOP.md</span><span class="te">Markdown runbook</span></td><td>Developer/ops runbook: HTTP-only serve, URL contract, regen order, CI, Pages inventory, pitfalls. Supersedes draft PRs #11 and #12.</td></tr>
<tr><td><span class="mod">configurator.html</span><span class="te">Static HTML · vanilla JS</span></td><td>Legacy form-based CLI generator kept for archive purposes only — superseded by the cheatsheet's Compare view + Automate drawer; not linked from the live site.</td></tr>
</tbody>
</table>
</div>
</section>
<!-- ================= QUICKSTART ================= -->
<section id="quickstart" aria-labelledby="qs-h">
<p class="eyebrow">Quickstart</p>
<h2 class="title" id="qs-h">Run it in seconds</h2>
<p class="lede">No install, no build, no dependencies. Use the hosted demo, or regenerate the corpus locally with Python 3 stdlib.</p>
<div class="code">
<div class="bar"><span class="dots"><i style="background:#ff5f56"></i><i style="background:#ffb000"></i><i style="background:#00d97e"></i></span><span class="ttl">use it</span><button class="copy" type="button">Copy</button></div>
<pre class="sh"><span class="c"># Open the live demo (no install) —</span>
<span class="c"># https://gesh75.github.io/multivendor-cli-configurator/</span>
<span class="c"># HTTP only — file:// looks empty (fetch of commands.json)</span>
<span class="g">python3</span> -m http.server 8000
<span class="c"># then open http://127.0.0.1:8000/</span></pre>
</div>
<div class="code">
<div class="bar"><span class="dots"><i style="background:#ff5f56"></i><i style="background:#ffb000"></i><i style="background:#00d97e"></i></span><span class="ttl">regenerate the corpus — Python 3 stdlib only</span><button class="copy" type="button">Copy</button></div>
<pre class="sh"><span class="g">cd</span> scripts
<span class="g">python3</span> parse_encor.py
<span class="g">python3</span> parse_junos.py
<span class="g">python3</span> parse_arista.py
<span class="g">python3</span> parse_cisco_ospf.py
<span class="g">python3</span> parse_cisco_extras.py
<span class="g">python3</span> parse_frr.py
<span class="g">python3</span> parse.py <span class="c"># master merge -&gt; ../commands.json</span></pre>
</div>
<div class="code">
<div class="bar"><span class="dots"><i style="background:#ff5f56"></i><i style="background:#ffb000"></i><i style="background:#00d97e"></i></span><span class="ttl">maintenance + tests</span><button class="copy" type="button">Copy</button></div>
<pre class="sh"><span class="c"># Maintenance utilities:</span>
<span class="g">python3</span> scripts/clean_titles.py
<span class="g">python3</span> scripts/audit_data_quality.py
<span class="c"># Gates (CI):</span>
<span class="g">python3</span> scripts/check_consistency.py
<span class="g">python3</span> scripts/deep_gap_dig.py
<span class="g">node</span> tests/stress_test.js</pre>
</div>
</section>
<!-- ================= STUDIO ================= -->
<section id="studio" aria-labelledby="studio-h">
<p class="eyebrow">CLI Studio</p>
<h2 class="title" id="studio-h">The 70k cheatsheet is too wide to teach. Studio is the workbench.</h2>
<p class="lede">Shipped on Pages as <a href="../studio.html"><code style="font-family:var(--mono);color:var(--teal)">studio.html</code></a> + <code style="font-family:var(--mono);color:var(--teal)">studio-data.json</code>. Still zero npm. Still GitHub Pages. Credentials stay in <code style="font-family:var(--mono);color:var(--teal)">$NET_*</code> — nothing stored.</p>
<div class="grid" style="margin-top:1.4rem">
<article class="card reveal"><h3>Intent</h3><p>English → concept → N-vendor CLI. Pattern match is instant; optional Grok explain is user-initiated.</p></article>
<article class="card reveal"><h3>Recipes</h3><p>Parameterized BGP, OSPF, EVPN, LACP, ACL, NTP, BFD, STP, plus the 2026 backlog: <strong>IS-IS L2, SR-MPLS node-SID, IPv6 eBGP</strong>.</p></article>
<article class="card reveal"><h3>Migrate</h3><p>Paste one vendor, emit the others as a unified diff. Concept keys, not string replace. No silent Cisco fallback.</p></article>
<article class="card reveal"><h3>Parse + export</h3><p>Cisco, Junos, EOS, FRR, VyOS show-output tables. Export CSV / JSON / <strong>TextFSM</strong> / <strong>TTP</strong>.</p></article>
<article class="card reveal"><h3>Harden</h3><p>NTP / AAA / SSH / syslog / BFD / secret heuristics on a running-config. AEGIS-style pre-change: risk, blast radius, rollback.</p></article>
<article class="card reveal"><h3>Lab + coverage</h3><p>10-node FRR Clos with live-flagged commands, plus an honest golden-path heatmap across 17 vendors.</p></article>
</div>
</section>
<!-- ================= STATUS ================= -->
<section id="status" aria-labelledby="status-h">
<p class="eyebrow">Project status · August 2026</p>
<h2 class="title" id="status-h">Production. Plan items closed. Two corpus gaps remain.</h2>
<p class="lede">70,006 CLI commands · 17 vendors · 43,008 router / 22,848 switch / 4,150 firewall. Live cheatsheet and Studio both deploy from the lean <code style="font-family:var(--mono);color:var(--teal)">_site/</code> Pages artifact.</p>
<div class="tbl-wrap reveal" style="margin-top:1.3rem">
<table>
<thead><tr><th scope="col">Item</th><th scope="col">State</th><th scope="col">Notes</th></tr></thead>
<tbody>
<tr><td>EOS / FRR / VyOS parse</td><td>shipped</td><td>ARCHITECTURE.md follow-up. Cisco auto-detect no longer steals every <code>local AS number</code> paste.</td></tr>
<tr><td>CLI Studio on Pages</td><td>shipped</td><td>PR #13. Intent, recipes, migrate, parse, lab, lint, coverage.</td></tr>
<tr><td>IS-IS / SR-MPLS / IPv6 BGP recipes</td><td>shipped</td><td>Research backlog from the 2026-08-29 changelog.</td></tr>
<tr><td>TextFSM / TTP export</td><td>shipped</td><td>Parse tables download as templates, not just CSV.</td></tr>
<tr><td>AEGIS pre-change gate</td><td>shipped</td><td>Client-side risk / blast / rollback / secret scan. Not a substitute for the air-gapped AEGIS repo.</td></tr>
<tr><td>Developer runbook</td><td>shipped</td><td><a href="DEVELOP.md">docs/DEVELOP.md</a> — supersedes draft PRs #11 and #12.</td></tr>
<tr><td>Grow SONiC (459)</td><td>open</td><td>Studio golden-path grew. Full corpus still the thinnest vendor — <code>expand_thin_vendors.py</code>.</td></tr>
<tr><td>Deepen classic SR OS</td><td>open</td><td>Studio now has BGP/OSPF/IS-IS/SR-MPLS/NTP/BFD. Full corpus still thin vs SR Linux.</td></tr>
<tr><td>Offline service worker</td><td>open</td><td>Cheatsheet already cache-then-revalidates via IndexedDB.</td></tr>
</tbody>
</table>
</div>
</section>
<!-- ================= DEVELOP ================= -->
<section id="develop" aria-labelledby="dev-h">
<p class="eyebrow">Develop</p>
<h2 class="title" id="dev-h">HTTP-only, stdlib-only, lean Pages.</h2>
<p class="lede">The public URL contract, regen order, and CI gates that used to live only in draft PRs. Full text: <a href="DEVELOP.md">docs/DEVELOP.md</a>.</p>
<div class="grid" style="margin-top:1.4rem">
<article class="card reveal"><h3>Serve over HTTP</h3><p><code style="font-family:var(--mono);color:var(--teal)">file://</code> looks empty because <code style="font-family:var(--mono);color:var(--teal)">index.html</code> fetches <code style="font-family:var(--mono);color:var(--teal)">commands.json</code>. <code style="font-family:var(--mono);color:var(--teal)">python3 -m http.server</code> is the local contract.</p></article>
<article class="card reveal"><h3>Regen order matters</h3><p><code style="font-family:var(--mono);color:var(--teal)">parse.py</code> rebuilds from intermediates and <strong>drops</strong> DCN / modern-ops / thin-vendor fills unless those passes run next.</p></article>
<article class="card reveal"><h3>CI gates</h3><p><code style="font-family:var(--mono);color:var(--teal)">check_consistency.py</code> asserts 70,006 / roles / 17 vendors still appear in README + this page. <code style="font-family:var(--mono);color:var(--teal)">tests/stress_test.js</code> is the Node suite.</p></article>
<article class="card reveal"><h3>Pages artifact</h3><p>Lean <code style="font-family:var(--mono);color:var(--teal)">_site/</code>. <code style="font-family:var(--mono);color:var(--teal)">.nojekyll</code> is required so Ansible <code style="font-family:var(--mono);color:var(--teal)">{{ }}</code> does not trip Jekyll. Source dumps in <code style="font-family:var(--mono);color:var(--teal)">scripts/*.json</code> stay unpublished.</p></article>
</div>
<p class="lede" style="margin-top:1.2rem">Search operators on the cheatsheet: <code style="font-family:var(--mono);color:var(--teal)">role:</code> <code style="font-family:var(--mono);color:var(--teal)">category:</code> <code style="font-family:var(--mono);color:var(--teal)">vendor:</code> plus shareable <code style="font-family:var(--mono);color:var(--teal)">?ws=</code> workspace and readable <code style="font-family:var(--mono);color:var(--teal)">?cat=BGP&view=compare&v=Juniper</code>. Press <code style="font-family:var(--mono);color:var(--teal)">?</code> for the in-app map.</p>
</section>
<!-- ================= CHANGELOG ================= -->
<section id="changelog" aria-labelledby="cl-h">
<p class="eyebrow">Changelog</p>
<h2 class="title" id="cl-h">What landed.</h2>
<div class="panel reveal" style="margin-top:1.2rem">
<p class="lede"><strong>2026-08-29 — Studio finish.</strong> IS-IS / SR-MPLS / IPv6 BGP recipes, TextFSM/TTP export, hardening lint + AEGIS pre-change, developer runbook on this docs site. Draft PRs #11/#12 superseded. Full notes in <a href="../CHANGELOG.md">CHANGELOG.md</a>.</p>
<p class="lede" style="margin-top:1rem"><strong>2026-08-29 — CLI Studio on Pages (PR #13).</strong> Second static surface next to the 70,006-command cheatsheet. EOS/FRR/VyOS parsers on the cheatsheet too.</p>
<p class="lede" style="margin-top:1rem"><strong>2026-08-06 — 70,006 corpus.</strong> STP/BFD/LACP promotion, thin OS fills, Automate vendor coverage, lean Pages deploy, <code>.nojekyll</code>.</p>
</div>
</section>
</main>
<footer>
<div class="foot-grid">
<div>
<div class="brand" style="margin-bottom:.5rem"><span class="dot" aria-hidden="true"></span><b>multivendor-cli</b></div>
<p style="margin:0; max-width:42ch; font-size:.9rem; color:var(--muted-dim)">70,006 CLI commands across 17 vendors and tools in one searchable, comparable, deep-linkable single-file page.</p>
</div>
<nav class="foot-links" aria-label="Footer links">
<a href="https://github.com/gesh75/multivendor-cli-configurator" target="_blank" rel="noopener">GitHub repo ↗</a>
<a href="https://gesh75.github.io/multivendor-cli-configurator/" target="_blank" rel="noopener">Live demo ↗</a>
<a href="https://gesh75.github.io/multivendor-cli-configurator/studio.html">Studio ↗</a>
<a href="DEVELOP.md">Develop</a>
<a href="#hero">Back to top ↑</a>
<span style="color:var(--muted-dim)">License: MIT</span>
</nav>
</div>
<p class="foot-note">Documentation for 70,006 commands · 17 vendors · MIT. Figures gated by scripts/check_consistency.py against commands.json. Studio + cheatsheet + this page ship in the lean Pages artifact.</p>
</footer>
<button id="top" aria-label="Back to top" title="Back to top">↑</button>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
mermaid.initialize({
startOnLoad: true,
theme: 'dark',
securityLevel: 'strict',
themeVariables: {
primaryColor: '#1e293b', primaryTextColor: '#e2e8f0', lineColor: '#64748b',
primaryBorderColor: '#475569', fontFamily: 'ui-sans-serif, system-ui, sans-serif'
}
});
/* ---- scroll progress bar ---- */
const progress = document.getElementById('progress');
const onScroll = () => {
const h = document.documentElement;
const max = h.scrollHeight - h.clientHeight;
progress.style.width = (max > 0 ? (h.scrollTop / max) * 100 : 0) + '%';
topBtn.classList.toggle('show', h.scrollTop > 600);
};
/* ---- back to top ---- */
const topBtn = document.getElementById('top');
topBtn.addEventListener('click', () =>
window.scrollTo({ top: 0, behavior: reduce ? 'auto' : 'smooth' }));
window.addEventListener('scroll', onScroll, { passive: true });
/* ---- mobile menu ---- */
const menuBtn = document.getElementById('menu-btn');
const navLinks = document.getElementById('navlinks');
menuBtn.addEventListener('click', () => {
const open = navLinks.classList.toggle('open');
menuBtn.setAttribute('aria-expanded', String(open));
});
navLinks.addEventListener('click', e => {
if (e.target.tagName === 'A') {
navLinks.classList.remove('open');
menuBtn.setAttribute('aria-expanded', 'false');
}
});
/* ---- scroll-spy active nav ---- */
const sections = [...document.querySelectorAll('main section[id]')];
const linkFor = id => navLinks.querySelector('a[href="#' + id + '"]');
const spy = new IntersectionObserver(entries => {
entries.forEach(en => {
if (en.isIntersecting) {
navLinks.querySelectorAll('a').forEach(a => a.classList.remove('active'));
const l = linkFor(en.target.id);
if (l) l.classList.add('active');
}
});
}, { rootMargin: '-45% 0px -50% 0px', threshold: 0 });
sections.forEach(s => spy.observe(s));
/* ---- scroll reveal (staggered) ---- */
const reveals = [...document.querySelectorAll('.reveal')];
if (reduce) {
reveals.forEach(r => r.classList.add('in'));
} else {
const ro = new IntersectionObserver((entries, obs) => {
entries.forEach(en => {
if (en.isIntersecting) {
const sibs = [...en.target.parentElement.children].filter(c => c.classList.contains('reveal'));
const i = Math.max(0, sibs.indexOf(en.target));
en.target.style.transitionDelay = Math.min(i * 80, 400) + 'ms';
en.target.classList.add('in');
obs.unobserve(en.target);
}
});
}, { threshold: 0.12 });
reveals.forEach(r => ro.observe(r));
}
/* ---- count-up stat chips ---- */
const fmt = n => n.toLocaleString('en-US');
const animateCount = el => {
const target = +el.dataset.count;
if (el.dataset.text && target === 0) { el.textContent = el.dataset.text; return; }
if (reduce) { el.textContent = fmt(target); return; }
const dur = 1400, start = performance.now();
const step = now => {
const p = Math.min((now - start) / dur, 1);
const eased = 1 - Math.pow(1 - p, 3);
el.textContent = fmt(Math.round(target * eased));
if (p < 1) requestAnimationFrame(step);
else el.textContent = fmt(target);
};
requestAnimationFrame(step);
};
const countObs = new IntersectionObserver((entries, obs) => {
entries.forEach(en => {
if (en.isIntersecting) { animateCount(en.target); obs.unobserve(en.target); }
});
}, { threshold: 0.5 });
document.querySelectorAll('.chip .num').forEach(el => countObs.observe(el));
/* ---- copy-to-clipboard for code blocks ---- */
document.querySelectorAll('.code .copy').forEach(btn => {
btn.addEventListener('click', async () => {
const pre = btn.closest('.code').querySelector('pre.sh');
const text = pre.innerText;
try {
await navigator.clipboard.writeText(text);
const old = btn.textContent;
btn.textContent = 'Copied ✓'; btn.classList.add('ok');
setTimeout(() => { btn.textContent = old; btn.classList.remove('ok'); }, 1600);
} catch {
btn.textContent = 'Copy failed';
setTimeout(() => { btn.textContent = 'Copy'; }, 1600);
}
});
});
onScroll();
</script>
</body>
</html>