const SUCCESS_LINES = [
{ t: 800, text: '$ whetstone setup --full', cls: 'cmd' },
{ t: 600, text: ' ┌─ PREFLIGHT ─────────────────────────', cls: 'rule' },
{ t: 120, text: ' ✓ python 3.11.5', cls: 'ok' },
{ t: 120, text: ' ✓ git 2.45.0', cls: 'ok' },
{ t: 120, text: ' ✓ curl 8.6.0', cls: 'ok' },
{ t: 120, text: ' ✓ uv 0.4.18', cls: 'ok' },
{ t: 120, text: ' ✓ git repo · 47 commits ahead of origin', cls: 'ok' },
{ t: 200, text: ' └─────────────────────────────────────', cls: 'rule' },
{ t: 400, text: ' ┌─ INSTALL ───────────────────────────', cls: 'rule' },
{ t: 240, text: ' ✓ headroom-ai[proxy,code,mcp] → ~/.local/bin/headroom', cls: 'ok' },
{ t: 240, text: ' ✓ rtk → ~/.local/bin/rtk', cls: 'ok' },
{ t: 240, text: ' ✓ whetstone → ~/.local/bin/whetstone', cls: 'ok' },
{ t: 200, text: ' └─────────────────────────────────────', cls: 'rule' },
{ t: 400, text: ' ┌─ CONFIGURE ─────────────────────────', cls: 'rule' },
{ t: 200, text: ' ✓ ANTHROPIC_BASE_URL → ~/.zshrc', cls: 'ok' },
{ t: 200, text: ' ✓ rtk PreToolUse hook → ~/.claude/settings.json', cls: 'ok' },
{ t: 200, text: ' ✓ 20 skills · 5 hooks · 8 rules → .claude/', cls: 'ok' },
{ t: 200, text: ' ✓ memory provider: icm (sqlite)', cls: 'ok' },
{ t: 200, text: ' └─────────────────────────────────────', cls: 'rule' },
{ t: 600, text: '', cls: '' },
{ t: 200, text: '$ whetstone version', cls: 'cmd' },
{ t: 200, text: ' whetstone 2.2.2 · headroom 0.9.1 · rtk 1.2.0', cls: 'dim' },
{ t: 600, text: '$ ', cls: 'cmd-end' },
];
const ERROR_LINES = [
{ t: 600, text: '$ whetstone setup', cls: 'cmd' },
{ t: 120, text: ' ✓ preflight ok', cls: 'ok' },
{ t: 220, text: ' ✗ rtk · name collides with Rust Type Kit v0.2.1', cls: 'err' },
{ t: 80, text: ' found at: ~/.cargo/bin/rtk', cls: 'dim' },
{ t: 80, text: ' whetstone rtk wants: ~/.local/bin/rtk', cls: 'dim' },
];
function useTypewriter(lines, active) {
const [shown, setShown] = React.useState(0);
const [cursorOn, setCursorOn] = React.useState(true);
React.useEffect(() => {
setShown(0);
if (!active) return;
let cancelled = false;
let i = 0;
function step() {
if (cancelled || i >= lines.length) return;
const delay = lines[i].t;
setTimeout(() => {
if (cancelled) return;
i += 1;
setShown(i);
step();
}, delay);
}
step();
return () => { cancelled = true; };
}, [active, lines]);
React.useEffect(() => {
const id = setInterval(() => setCursorOn((c) => !c), 600);
return () => clearInterval(id);
}, []);
const replay = () => setShown(0);
return { shown, cursorOn, replay };
}
function TerminalView({ lines, active, showCursorAtEnd }) {
const { shown, cursorOn } = useTypewriter(lines, active);
const visible = lines.slice(0, shown);
return (
<pre style={{
fontFamily: 'var(--f-mono)', fontSize: '13px', lineHeight: 1.65,
margin: 0, color: 'var(--c-mist)',
minHeight: '480px', whiteSpace: 'pre-wrap', wordBreak: 'break-word',
}}>
{visible.map((l, i) => (
<div key={i} style={{
color:
l.cls === 'ok' ? 'var(--c-acid)' :
l.cls === 'err' ? 'var(--c-magenta)' :
l.cls === 'dim' ? 'var(--c-lilac)' :
l.cls === 'rule' ? 'var(--c-lilac)' :
l.cls === 'cmd' ? 'var(--c-bone)' :
'var(--c-mist)',
opacity: l.cls === 'dim' || l.cls === 'rule' ? 0.75 : 1,
fontWeight: l.cls === 'cmd' || l.cls === 'cmd-end' ? 700 : 400,
}}>
{l.text || '\u00A0'}
{showCursorAtEnd && i === visible.length - 1 && (
<span style={{ color: 'var(--c-magenta)', opacity: cursorOn ? 1 : 0 }}>▌</span>
)}
</div>
))}
</pre>
);
}
function InstallTerminal() {
const [branch, setBranch] = React.useState('success');
const isSuccess = branch === 'success';
const lines = isSuccess ? SUCCESS_LINES : ERROR_LINES;
return (
<section className="section ws-wrap" id="install">
<div className="ws-sec-head">
<div className="ws-sec-tag">02 · INSTALL</div>
<h2>One command.<br />Two outcomes shown.</h2>
<div className="ws-sec-meta">whetstone setup --full<br />~ 8 SECONDS · IDEMPOTENT</div>
</div>
<div style={{
border: 'var(--b-med) solid var(--fg)', boxShadow: 'var(--sh-stack)',
background: 'var(--c-void)', position: 'relative', marginBottom: 'var(--s-7)',
}}>
<div style={{
position: 'absolute', top: '-14px', left: 'var(--s-5)',
background: 'var(--c-magenta)', color: 'var(--c-bone)',
font: 'var(--t-tag)', padding: '5px 12px', letterSpacing: '0.18em',
}}>
INSTALLATION TERMINAL · whetstone setup
</div>
{}
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: 'var(--s-3) var(--s-5)',
borderBottom: 'var(--b-med) solid var(--c-bone)',
background: 'var(--c-obsidian)',
}}>
<div style={{ display: 'flex', gap: '8px' }}>
<div style={{ width: '12px', height: '12px', background: 'var(--c-magenta)' }}></div>
<div style={{ width: '12px', height: '12px', background: 'var(--c-acid)' }}></div>
<div style={{ width: '12px', height: '12px', background: 'var(--c-bone)' }}></div>
</div>
<div style={{
font: 'var(--t-tag)', letterSpacing: '0.14em', textTransform: 'uppercase',
color: 'var(--c-acid)',
}}>~/projects/my-app · zsh</div>
<div style={{ font: 'var(--t-mono)', fontSize: '11px', color: 'var(--c-lilac)' }}>
[RTK · LIVE]
</div>
</div>
{}
<div style={{
display: 'flex', gap: 0,
borderBottom: 'var(--b-med) solid var(--c-bone)',
background: 'var(--c-void)',
}}>
<button
className={'demo-tab' + (isSuccess ? ' is-active' : '')}
onClick={() => setBranch('success')}
style={{ flex: 1 }}
>
<span className="ix">✓</span>
<span>SUCCESS BRANCH · v2.5.0</span>
</button>
<button
className={'demo-tab' + (!isSuccess ? ' is-active' : '')}
onClick={() => setBranch('error')}
style={{ flex: 1, borderRight: 0 }}
>
<span className="ix">✗</span>
<span>ERROR BRANCH · RTK COLLISION</span>
</button>
</div>
{}
<div style={{ padding: 'var(--s-6) var(--s-7)', background: 'var(--c-void)' }}>
<div style={{
fontSize: '11px',
color: isSuccess ? 'var(--c-lilac)' : 'var(--c-magenta)',
letterSpacing: '0.14em', textTransform: 'uppercase',
marginBottom: 'var(--s-3)',
fontFamily: 'var(--f-mono)',
}}>
{isSuccess ? '// WHETSTONE SETUP · v2.5.0' : '// ERROR · RTK COLLISION DETECTED'}
</div>
<TerminalView lines={lines} active={true} showCursorAtEnd={isSuccess} key={branch} />
{!isSuccess && (
<React.Fragment>
<div className="ws-callout ws-callout--danger" style={{ margin: 'var(--s-5) 0 var(--s-4)' }}>
<div className="ws-callout-mark">×</div>
<div className="ws-callout-body">
<h4>// CONFLICT</h4>
<p>Both binaries can coexist if <code>~/.local/bin</code> precedes <code>~/.cargo/bin</code> in <code>$PATH</code>. Or rename one. Whetstone won't overwrite without explicit consent.</p>
</div>
</div>
<div style={{ display: 'flex', gap: 'var(--s-3)', flexWrap: 'wrap' }}>
<button className="ws-btn ws-btn--sm ws-btn--primary">REORDER PATH</button>
<button className="ws-btn ws-btn--sm">RENAME RTK</button>
<button className="ws-btn ws-btn--sm ws-btn--ghost">SKIP RTK</button>
</div>
</React.Fragment>
)}
</div>
</div>
{}
{isSuccess && (
<div style={{
border: 'var(--b-med) solid var(--fg)',
background: 'var(--bg-card)',
boxShadow: '4px 4px 0 0 var(--brand)',
}}>
<div style={{
padding: '10px var(--s-5)',
borderBottom: 'var(--b-med) solid var(--rule)',
font: 'var(--t-tag)',
letterSpacing: '0.14em', textTransform: 'uppercase',
color: 'var(--accent)',
}}>// POST-INSTALL STATUS</div>
<div className="ws-kv">
<div className="ws-kv-row"><div className="k">HEADROOM</div><div className="v">proxy :8787 · indexing</div><div className="s ok">running</div></div>
<div className="ws-kv-row"><div className="k">RTK</div><div className="v">hook · pretooluse(bash)</div><div className="s ok">active</div></div>
<div className="ws-kv-row"><div className="k">MEMORY</div><div className="v">icm · .claude/db/whetstone.db</div><div className="s ok">ready</div></div>
<div className="ws-kv-row"><div className="k">SKILLS</div><div className="v">20 copied to .claude/skills/</div><div className="s ok">ok</div></div>
<div className="ws-kv-row"><div className="k">HOOKS</div><div className="v">5 hooks merged into ~/.claude/settings.json</div><div className="s ok">ok</div></div>
<div className="ws-kv-row"><div className="k">SHELL</div><div className="v">ANTHROPIC_BASE_URL → ~/.zshrc · restart needed</div><div className="s err">action</div></div>
</div>
</div>
)}
</section>
);
}
Object.assign(window, { InstallTerminal });